use crate::{
core::{i18n, i18n_f, ni18n_f, UnitSystem},
model::User,
plugins::{
weight::{GraphModelWeight, GraphModelWeightMocked},
PluginDetails,
},
prelude::*,
views::GraphView,
};
use gtk::{
gio::subclass::prelude::*,
glib::{self, Boxed},
};
use std::{cell::RefCell, rc::Rc};
use uom::si::{
f32::Mass,
length::meter,
mass::{kilogram, pound},
};
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/weight/details.ui")]
pub struct PluginWeightDetails {
#[template_child]
pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,
pub settings: Settings,
pub database: Database,
pub settings_handler_id: RefCell<Option<glib::SignalHandlerId>>,
pub weight_graph_view: OnceCell<GraphView>,
pub weight_graph_model: RefCell<Option<DataProvider>>,
}
#[glib::object_subclass]
impl ObjectSubclass for PluginWeightDetails {
const NAME: &'static str = "HealthPluginWeightDetails";
type ParentType = PluginDetails;
type Type = super::PluginWeightDetails;
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 PluginWeightDetails {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
Database::instance().connect_weights_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.weight_graph_model.replace(Some(
value
.get::<DataProviderBoxed>()
.unwrap()
.0
.borrow_mut()
.take()
.unwrap(),
));
}
_ => unimplemented!(),
}
}
}
impl WidgetImpl for PluginWeightDetails {}
impl BinImpl for PluginWeightDetails {}
impl PluginDetailsImpl for PluginWeightDetails {
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::PluginWeightDetails>()
.unwrap()
.update()
.await;
send.resolve(Ok(()));
});
}),
))
}
}
}
glib::wrapper! {
pub struct PluginWeightDetails(ObjectSubclass<imp::PluginWeightDetails>)
@extends gtk::Widget, adw::Bin, PluginDetails,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}
impl PluginWeightDetails {
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()
}
pub async fn get_user(&self) -> User {
let imp = self.imp();
let user_id = i64::from(imp.settings.active_user_id());
let user = &imp.database.user(user_id).await.unwrap();
user.clone()
}
#[rustfmt::skip]
pub async fn update(&self) {
let imp = self.imp();
let user = self.get_user().await;
let mut weight_graph_model = { imp.weight_graph_model.borrow_mut().take().unwrap() };
if let Err(e) = weight_graph_model.reload(glib::TimeSpan::from_days(30)).await {
glib::g_warning!(
crate::config::LOG_DOMAIN,
"Failed to reload weight data: {e}",
);
}
self.set_filled_title(&i18n_f("Current BMI: {}", &[&self.bmi(&weight_graph_model).await]));
self.update_weight_goal_label(&weight_graph_model).await;
if let Some(view) = imp.weight_graph_view.get() {
view.set_points(weight_graph_model.to_points());
} else if weight_graph_model.is_empty() {
self.switch_to_empty_page();
} else {
let weight_graph_view = GraphView::new();
weight_graph_view.set_points(weight_graph_model.to_points());
weight_graph_view.set_x_lines_interval(10.0);
let settings = imp.settings.clone();
weight_graph_view.set_hover_func(Some(Box::new(move |p| {
if settings.unit_system() == UnitSystem::Imperial {
ni18n_f("{} pound on {}", "{} pounds on {}", p.value as u32, &[&p.value.to_string(), &p.date.format_local()])
} else {
ni18n_f("{} kilogram on {}", "{} kilograms on {}", p.value as u32, &[&p.value.to_string(), &p.date.format_local()])
}
})));
if let Some(unit_goal) = user.user_weightgoal() {
let weight_goal = if imp.settings.unit_system() == UnitSystem::Imperial {
unit_goal.get::<pound>()
} else {
unit_goal.get::<kilogram>()
};
weight_graph_view.set_limit(Some(weight_goal));
weight_graph_view.set_limit_label(Some(i18n("Weight goal")));
}
imp.scrolled_window.set_child(Some(&weight_graph_view));
self.switch_to_data_page();
imp.weight_graph_view.set(weight_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.weight_graph_model.replace(Some(weight_graph_model));
}
async fn bmi(&self, model: &DataProvider) -> String {
let user = self.get_user().await;
if let Some(last_weight) = model.last_weight() {
let height = user.user_height().unwrap().get::<meter>();
if height == 0.0 || last_weight.get::<kilogram>() == 0.0 {
return i18n("Unknown BMI");
}
let bmi = (last_weight.get::<kilogram>() / (height * height)).round_decimal_places(1);
format!("{bmi:.1}")
} else {
i18n("Unknown BMI")
}
}
async fn update_weight_goal_label(&self, model: &DataProvider) {
let imp = self.imp();
let user = self.get_user().await;
if let Some(weight_goal) = user.user_weightgoal() {
let unit_system = imp.settings.unit_system();
let weight_value = if unit_system == UnitSystem::Imperial {
weight_goal.get::<pound>()
} else {
weight_goal.get::<kilogram>()
};
if weight_value > 0.1 && model.is_empty() {
let goal_label_text = if unit_system == UnitSystem::Imperial {
ni18n_f(
"Your weight goal is {} pound. Add a first weight measurement to see how close you are to reaching it.",
"Your weight goal is {} pounds. Add a first weight measurement to see how close you are to reaching it.",
weight_value as u32,
&[&weight_value.to_string()],
)
} else {
ni18n_f(
"Your weight goal is {} kilogram. Add a first weight measurement to see how close you are to reaching it.",
"Your weight goal is {} kilograms. Add a first weight measurement to see how close you are to reaching it.",
weight_value as u32,
&[&weight_value.to_string()],
)
};
self.set_filled_subtitle(&goal_label_text);
} else if weight_value > 0.1 && !model.is_empty() {
if model.last_weight().unwrap() == weight_goal {
self.set_filled_subtitle(&i18n("You’ve reached your weight goal. Great job!"));
}
let unit_diff = model.last_weight().unwrap() - weight_goal;
let mut diff = if unit_system == UnitSystem::Imperial {
unit_diff.get::<pound>()
} else {
unit_diff.get::<kilogram>()
};
if diff < 0.0 {
diff *= -1.0;
}
let goal_label_text = if unit_system == UnitSystem::Imperial {
ni18n_f(
"Your weight goal is {} pound.",
"Your weight goal is {} pounds.",
weight_value as u32,
&[&format!("{weight_value:.1}")],
) + &ni18n_f(
"You have {} pound left to reach it.",
"You have {} pounds left to reach it.",
diff as u32,
&[&format!("{diff:.1}", diff = diff.round_decimal_places(1))],
)
} else {
ni18n_f(
"Your weight goal is {} kilogram.",
"Your weight goal is {} kilograms.",
weight_value as u32,
&[&format!("{weight_value:.1}")],
) + " "
+ &ni18n_f(
"You have {} kilogram left to reach it",
"You have {} kilograms left to reach it",
diff as u32,
&[&format!("{diff:.1}", diff = diff.round_decimal_places(1))],
)
};
self.set_filled_subtitle(&goal_label_text);
}
} else {
self.set_filled_subtitle(&i18n(
"No weight goal set yet. You can set it in Health’s preferences.",
));
}
}
}
#[derive(Clone, Boxed)]
#[boxed_type(name = "HealthDataProviderWeightsBoxed")]
pub struct DataProviderBoxed(Rc<RefCell<Option<DataProvider>>>);
#[derive(Debug)]
pub enum DataProvider {
Actual(GraphModelWeight),
Mocked(GraphModelWeightMocked),
}
impl DataProvider {
pub fn actual() -> Self {
Self::Actual(GraphModelWeight::new())
}
pub fn mocked() -> Self {
Self::Mocked(GraphModelWeightMocked::new())
}
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<crate::views::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(),
}
}
pub fn last_weight(&self) -> Option<Mass> {
match self {
Self::Actual(m) => m.last_weight(),
Self::Mocked(m) => m.last_weight(),
}
}
}
#[cfg(test)]
mod test {
use super::{DataProvider, PluginWeightDetails};
use crate::utils::init_gtk;
#[gtk::test]
fn new() {
init_gtk();
PluginWeightDetails::new(DataProvider::mocked());
}
}