use crate::{
core::{i18n, i18n_f, ni18n_f},
plugins::{
steps::{GraphModelSteps, GraphModelStepsMocked},
PluginDetails,
},
prelude::*,
views::{GraphView, Point},
};
use gtk::glib::{self, subclass::prelude::*, Boxed};
use std::{cell::RefCell, rc::Rc};
mod imp {
use super::{DataProvider, DataProviderBoxed};
use crate::{
core::{Database, Settings},
plugins::PluginDetails,
prelude::*,
views::GraphView,
};
use adw::{prelude::*, subclass::prelude::*};
use gtk::{
gio,
glib::{self, Cast},
CompositeTemplate,
};
use once_cell::unsync::OnceCell;
use std::cell::RefCell;
#[derive(Debug, CompositeTemplate, Default)]
#[template(resource = "/dev/Cogitri/Health/ui/plugins/steps/details.ui")]
pub struct PluginStepsDetails {
#[template_child]
pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,
pub settings: Settings,
pub database: Database,
pub settings_handler_id: RefCell<Option<glib::SignalHandlerId>>,
pub steps_graph_view: OnceCell<GraphView>,
pub steps_graph_model: RefCell<Option<DataProvider>>,
}
#[glib::object_subclass]
impl ObjectSubclass for PluginStepsDetails {
const NAME: &'static str = "HealthPluginStepsDetails";
type ParentType = PluginDetails;
type Type = super::PluginStepsDetails;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for PluginStepsDetails {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
Database::instance().connect_activities_updated(glib::clone!(@weak obj => move |_| {
gtk_macros::spawn!(async move {
obj.update().await;
});
}));
}
fn dispose(&self) {
if let Some(id) = self.settings_handler_id.borrow_mut().take() {
self.settings.disconnect(id);
}
}
fn properties() -> &'static [glib::ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecBoxed::builder::<DataProviderBoxed>("data-provider")
.construct()
.write_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"data-provider" => {
self.steps_graph_model.replace(Some(
value
.get::<DataProviderBoxed>()
.unwrap()
.0
.borrow_mut()
.take()
.unwrap(),
));
}
_ => unimplemented!(),
}
}
}
impl WidgetImpl for PluginStepsDetails {}
impl BinImpl for PluginStepsDetails {}
impl PluginDetailsImpl for PluginStepsDetails {
fn update(&self, obj: &PluginDetails) -> PinnedResultFuture<()> {
Box::pin(gio::GioFuture::new(
obj,
glib::clone!(@weak obj => move |_, _, send| {
gtk_macros::spawn!(async move {
obj.downcast_ref::<super::PluginStepsDetails>()
.unwrap()
.update()
.await;
send.resolve(Ok(()));
});
}),
))
}
}
}
glib::wrapper! {
pub struct PluginStepsDetails(ObjectSubclass<imp::PluginStepsDetails>)
@extends gtk::Widget, adw::Bin, PluginDetails,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}
impl PluginStepsDetails {
pub fn new(data_provider: DataProvider) -> Self {
glib::Object::builder()
.property(
"is-mocked",
matches!(data_provider, DataProvider::Mocked(_)),
)
.property(
"data-provider",
&DataProviderBoxed(Rc::new(RefCell::new(Some(data_provider)))),
)
.build()
}
#[rustfmt::skip]
pub async fn update(&self) {
let imp = self.imp();
let mut steps_graph_model = { imp.steps_graph_model.borrow_mut().take().unwrap() };
if let Err(e) = steps_graph_model.reload(glib::TimeSpan::from_days(30)).await {
glib::g_warning!(
crate::config::LOG_DOMAIN,
"Failed to reload step data: {e}",
);
}
self.set_filled_title(&i18n_f(
"Today’s steps: {}",
&[&steps_graph_model
.today_step_count()
.unwrap_or(0)
.to_string()],
));
let user_id = i64::from(imp.settings.active_user_id());
let user = &imp.database.user(user_id).await.unwrap();
let streak_count = steps_graph_model.streak_count_today(user.user_stepgoal().unwrap_or(0) as u32);
match streak_count {
0 => {
let previous_streak =
steps_graph_model.streak_count_yesterday(user.user_stepgoal().unwrap_or(0) as u32);
if previous_streak == 0 {
self.set_filled_subtitle(&i18n(
"No streak yet. Reach your step goal for multiple days to start a streak!",
));
} else {
self.set_filled_subtitle(&ni18n_f(
"You’re on a streak for {} day. Reach your step goal today to continue it!",
"You’re on a streak for {} days. Reach your step goal today to continue it!",
previous_streak,
&[&previous_streak.to_string()],
));
}
}
1 => self.set_filled_subtitle(&i18n(
"You’ve reached your step goal today. Keep going to start a streak!",
)),
_ => self.set_filled_subtitle(&ni18n_f(
"You’re on a streak for {} day. Good job!",
"You’re on a streak for {} days. Good job!",
streak_count,
&[&streak_count.to_string()],
)),
}
if let Some(view) = imp.steps_graph_view.get() {
view.set_points(steps_graph_model.to_points());
} else if steps_graph_model.is_empty() {
self.switch_to_empty_page();
} else {
let steps_graph_view = GraphView::new();
steps_graph_view.set_points(steps_graph_model.to_points());
steps_graph_view.set_x_lines_interval(500.0);
steps_graph_view.set_hover_func(Some(Box::new(|p| {
ni18n_f( "{} step on {}",
"{} steps on {}",
p.value as u32,
&[&p.value.to_string(), &p.date.format_local()],
)
})));
steps_graph_view.set_limit(Some(user.user_stepgoal().unwrap_or(0) as u32 as f32));
steps_graph_view.set_limit_label(Some(i18n("Step goal")));
imp.scrolled_window.set_child(Some(&steps_graph_view));
self.switch_to_data_page();
imp.steps_graph_view.set(steps_graph_view).unwrap();
imp
.settings_handler_id
.replace(Some(imp.database.connect_user_updated(
glib::clone!(@weak self as obj => move |_| {
gtk_macros::spawn!(async move {
obj.update().await;
});
}),
)));
}
imp.steps_graph_model.replace(Some(steps_graph_model));
}
}
#[derive(Clone, Boxed)]
#[boxed_type(name = "HealthDataProviderStepssBoxed")]
pub struct DataProviderBoxed(Rc<RefCell<Option<DataProvider>>>);
#[derive(Debug)]
pub enum DataProvider {
Actual(GraphModelSteps),
Mocked(GraphModelStepsMocked),
}
impl DataProvider {
pub fn actual() -> Self {
Self::Actual(GraphModelSteps::new())
}
pub fn mocked() -> Self {
Self::Mocked(GraphModelStepsMocked::new())
}
pub fn today_step_count(&self) -> Option<u32> {
match self {
Self::Actual(m) => m.today_step_count(),
Self::Mocked(m) => m.today_step_count(),
}
}
pub fn streak_count_today(&self, step_goal: u32) -> u32 {
match self {
Self::Actual(m) => m.streak_count_today(step_goal),
Self::Mocked(m) => m.streak_count_today(step_goal),
}
}
pub fn streak_count_yesterday(&self, step_goal: u32) -> u32 {
match self {
Self::Actual(m) => m.streak_count_yesterday(step_goal),
Self::Mocked(m) => m.streak_count_yesterday(step_goal),
}
}
pub async fn reload(&mut self, duration: glib::TimeSpan) -> anyhow::Result<()> {
match self {
Self::Actual(m) => m.reload(duration).await,
Self::Mocked(m) => m.reload(duration).await,
}
}
pub fn to_points(&self) -> Vec<Point> {
match self {
Self::Actual(m) => m.to_points(),
Self::Mocked(m) => m.to_points(),
}
}
pub fn is_empty(&self) -> bool {
match self {
Self::Actual(m) => m.is_empty(),
Self::Mocked(m) => m.is_empty(),
}
}
}
#[cfg(test)]
mod test {
use super::{DataProvider, PluginStepsDetails};
use crate::utils::init_gtk;
#[gtk::test]
fn new() {
init_gtk();
PluginStepsDetails::new(DataProvider::mocked());
}
}