From 7ba758b325e1c167c00cecd10475b50a9fb42791 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 19 Feb 2024 13:27:19 -0500 Subject: [PATCH 1/4] Build an application that can read the legacy record data structure --- Cargo.lock | 1 + emseries/src/date_time_tz.rs | 198 ------------------ fitnesstrax/core/Cargo.toml | 1 + fitnesstrax/core/src/bin/legacy-importer.rs | 219 ++++++++++++++++++++ 4 files changed, 221 insertions(+), 198 deletions(-) delete mode 100644 emseries/src/date_time_tz.rs create mode 100644 fitnesstrax/core/src/bin/legacy-importer.rs diff --git a/Cargo.lock b/Cargo.lock index 723ce30..733973c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1069,6 +1069,7 @@ dependencies = [ "dimensioned 0.8.0", "emseries", "serde 1.0.193", + "serde_json", "tempfile", ] diff --git a/emseries/src/date_time_tz.rs b/emseries/src/date_time_tz.rs deleted file mode 100644 index 1022767..0000000 --- a/emseries/src/date_time_tz.rs +++ /dev/null @@ -1,198 +0,0 @@ -/* -Copyright 2020-2023, Savanni D'Gerinel - -This file is part of the Luminescent Dreams Tools. - -Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with Lumeto. If not, see . -*/ - -extern crate chrono; -extern crate chrono_tz; - -use chrono::SecondsFormat; -use chrono_tz::Etc::UTC; -use serde::de::{self, Deserialize, Deserializer, Visitor}; -use serde::ser::{Serialize, Serializer}; -use std::{fmt, str::FromStr}; - -/// This is a wrapper around date time objects, using timezones from the chroon-tz database and -/// providing string representation and parsing of the form " ", i.e., -/// "2019-05-15T14:30:00Z US/Central". The to_string method, and serde serialization will -/// produce a string of this format. The parser will accept an RFC3339-only string of the forms -/// "2019-05-15T14:30:00Z", "2019-05-15T14:30:00+00:00", and also an "RFC3339 Timezone Name" -/// string. -/// -/// The function here is to generate as close to unambiguous time/date strings, (for earth's -/// gravitational frame of reference), as possible. Clumping together the time, offset from UTC, -/// and the named time zone allows future parsers to know the exact interpretation of the time in -/// the frame of reference of the original recording. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct DateTimeTz(pub chrono::DateTime); - -impl fmt::Display for DateTimeTz { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - if self.0.timezone() == UTC { - write!(f, "{}", self.0.to_rfc3339_opts(SecondsFormat::Secs, true)) - } else { - write!( - f, - "{} {}", - self.0 - .with_timezone(&chrono_tz::Etc::UTC) - .to_rfc3339_opts(SecondsFormat::Secs, true,), - self.0.timezone().name() - ) - } - } -} - -impl DateTimeTz { - pub fn map(&self, f: F) -> DateTimeTz - where - F: FnOnce(chrono::DateTime) -> chrono::DateTime, - { - DateTimeTz(f(self.0)) - } -} - -impl std::str::FromStr for DateTimeTz { - type Err = chrono::ParseError; - - fn from_str(s: &str) -> Result { - let v: Vec<&str> = s.split_terminator(' ').collect(); - if v.len() == 2 { - let tz = v[1].parse::().unwrap(); - chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&tz))) - } else { - chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&UTC))) - } - } -} - -impl From> for DateTimeTz { - fn from(dt: chrono::DateTime) -> DateTimeTz { - DateTimeTz(dt) - } -} - -struct DateTimeTzVisitor; - -impl<'de> Visitor<'de> for DateTimeTzVisitor { - type Value = DateTimeTz; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a string date time representation that can be parsed") - } - - fn visit_str(self, s: &str) -> Result { - DateTimeTz::from_str(s).or(Err(E::custom( - "string is not a parsable datetime representation".to_owned(), - ))) - } -} - -impl Serialize for DateTimeTz { - fn serialize(&self, serializer: S) -> Result { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for DateTimeTz { - fn deserialize>(deserializer: D) -> Result { - deserializer.deserialize_str(DateTimeTzVisitor) - } -} - -#[cfg(test)] -mod test { - extern crate serde_json; - - use super::*; - use chrono::TimeZone; - use chrono_tz::America::Phoenix; - use chrono_tz::Etc::UTC; - use chrono_tz::US::{Arizona, Central}; - use std::str::FromStr; - - #[test] - fn it_creates_timestamp_with_z() { - let t = DateTimeTz(UTC.with_ymd_and_hms(2019, 5, 15, 12, 0, 0).unwrap()); - assert_eq!(t.to_string(), "2019-05-15T12:00:00Z"); - } - - #[test] - fn it_parses_utc_rfc3339_z() { - let t = DateTimeTz::from_str("2019-05-15T12:00:00Z").unwrap(); - assert_eq!( - t, - DateTimeTz(UTC.with_ymd_and_hms(2019, 5, 15, 12, 0, 0).unwrap()) - ); - } - - #[test] - fn it_parses_rfc3339_with_offset() { - let t = DateTimeTz::from_str("2019-05-15T12:00:00-06:00").unwrap(); - assert_eq!( - t, - DateTimeTz(UTC.with_ymd_and_hms(2019, 5, 15, 18, 0, 0).unwrap()) - ); - } - - #[test] - fn it_parses_rfc3339_with_tz() { - let t = DateTimeTz::from_str("2019-06-15T19:00:00Z US/Arizona").unwrap(); - assert_eq!( - t, - DateTimeTz(UTC.with_ymd_and_hms(2019, 6, 15, 19, 0, 0).unwrap()) - ); - assert_eq!( - t, - DateTimeTz(Arizona.with_ymd_and_hms(2019, 6, 15, 12, 0, 0).unwrap()) - ); - assert_eq!( - t, - DateTimeTz(Central.with_ymd_and_hms(2019, 6, 15, 14, 0, 0).unwrap()) - ); - assert_eq!(t.to_string(), "2019-06-15T19:00:00Z US/Arizona"); - } - - #[derive(Serialize)] - struct DemoStruct { - id: String, - dt: DateTimeTz, - } - - // I used Arizona here specifically because large parts of Arizona do not honor DST, and so - // that adds in more ambiguity of the -0700 offset with Pacific time. - #[test] - fn it_json_serializes() { - let t = DateTimeTz::from_str("2019-06-15T19:00:00Z America/Phoenix").unwrap(); - assert_eq!( - serde_json::to_string(&t).unwrap(), - "\"2019-06-15T19:00:00Z America/Phoenix\"" - ); - - let demo = DemoStruct { - id: String::from("abcdefg"), - dt: t, - }; - assert_eq!( - serde_json::to_string(&demo).unwrap(), - "{\"id\":\"abcdefg\",\"dt\":\"2019-06-15T19:00:00Z America/Phoenix\"}" - ); - } - - #[test] - fn it_json_parses() { - let t = - serde_json::from_str::("\"2019-06-15T19:00:00Z America/Phoenix\"").unwrap(); - assert_eq!( - t, - DateTimeTz(Phoenix.with_ymd_and_hms(2019, 6, 15, 12, 0, 0).unwrap()) - ); - } -} diff --git a/fitnesstrax/core/Cargo.toml b/fitnesstrax/core/Cargo.toml index 520b69c..3e7a8ed 100644 --- a/fitnesstrax/core/Cargo.toml +++ b/fitnesstrax/core/Cargo.toml @@ -11,6 +11,7 @@ chrono-tz = { version = "0.8" } dimensioned = { version = "0.8", features = [ "serde" ] } emseries = { path = "../../emseries" } serde = { version = "1", features = [ "derive" ] } +serde_json = { version = "1" } [dev-dependencies] tempfile = "*" diff --git a/fitnesstrax/core/src/bin/legacy-importer.rs b/fitnesstrax/core/src/bin/legacy-importer.rs new file mode 100644 index 0000000..1f29c99 --- /dev/null +++ b/fitnesstrax/core/src/bin/legacy-importer.rs @@ -0,0 +1,219 @@ +/* +Copyright 2024, Savanni D'Gerinel + +This file is part of FitnessTrax. + +FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU +General Public License as published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without +even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . +*/ + +use std::{ + fs::File, + io::{BufRead, BufReader, Read}, + fmt, + str::FromStr, +}; +use chrono::SecondsFormat; +use chrono_tz::Etc::UTC; +use emseries::{Record, Series, Timestamp, RecordId}; +use ft_core::TraxRecord; +use serde::{Deserialize, Serialize, Deserializer, Serializer, de::{self, Visitor}}; + + + +/// This is a wrapper around date time objects, using timezones from the chroon-tz database and +/// providing string representation and parsing of the form " ", i.e., +/// "2019-05-15T14:30:00Z US/Central". The to_string method, and serde serialization will +/// produce a string of this format. The parser will accept an RFC3339-only string of the forms +/// "2019-05-15T14:30:00Z", "2019-05-15T14:30:00+00:00", and also an "RFC3339 Timezone Name" +/// string. +/// +/// The function here is to generate as close to unambiguous time/date strings, (for earth's +/// gravitational frame of reference), as possible. Clumping together the time, offset from UTC, +/// and the named time zone allows future parsers to know the exact interpretation of the time in +/// the frame of reference of the original recording. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct DateTimeTz(pub chrono::DateTime); + +impl fmt::Display for DateTimeTz { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + if self.0.timezone() == UTC { + write!(f, "{}", self.0.to_rfc3339_opts(SecondsFormat::Secs, true)) + } else { + write!( + f, + "{} {}", + self.0 + .with_timezone(&chrono_tz::Etc::UTC) + .to_rfc3339_opts(SecondsFormat::Secs, true,), + self.0.timezone().name() + ) + } + } +} + +impl DateTimeTz { + pub fn map(&self, f: F) -> DateTimeTz + where + F: FnOnce(chrono::DateTime) -> chrono::DateTime, + { + DateTimeTz(f(self.0)) + } +} + +impl std::str::FromStr for DateTimeTz { + type Err = chrono::ParseError; + + fn from_str(s: &str) -> Result { + let v: Vec<&str> = s.split_terminator(' ').collect(); + if v.len() == 2 { + let tz = v[1].parse::().unwrap(); + chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&tz))) + } else { + chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&UTC))) + } + } +} + +impl From> for DateTimeTz { + fn from(dt: chrono::DateTime) -> DateTimeTz { + DateTimeTz(dt) + } +} + +struct DateTimeTzVisitor; + +impl<'de> Visitor<'de> for DateTimeTzVisitor { + type Value = DateTimeTz; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string date time representation that can be parsed") + } + + fn visit_str(self, s: &str) -> Result { + DateTimeTz::from_str(s).or(Err(E::custom( + "string is not a parsable datetime representation".to_owned(), + ))) + } +} + +impl Serialize for DateTimeTz { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for DateTimeTz { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_str(DateTimeTzVisitor) + } +} + + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct Steps { + date: DateTimeTz, + steps: u64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct Weight { + date: DateTimeTz, + weight: f64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum TDActivity { + Cycling, + Running, + Walking, + Swimming, + Rowing, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct TimeDistance { + date: DateTimeTz, + activity: TDActivity, + comments: Option, + distance: Option, + duration: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum SRActivity { + Pushups, + Situps, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct SetRep { + date: DateTimeTz, + activity: SRActivity, + sets: Vec, + comments: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum RDActivity { + MartialArts, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct RepDuration { + date: DateTimeTz, + activity: RDActivity, + sets: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum LegacyData { + Steps(Steps), + Weight(Weight), + TimeDistance(TimeDistance), + SetRep(SetRep), + RepDuration(RepDuration), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct LegacyRecord { + id: RecordId, + data: LegacyData, +} + +fn main() { + let mut args = std::env::args(); + let _ = args.next().unwrap(); + let input_filename = args.next().unwrap(); + println!("input filename: {}", input_filename); + + let input_file = File::open(input_filename).unwrap(); + let mut buf_reader = BufReader::new(input_file); + // let mut contents = String::new(); + // buf_reader.read_(&mut contents).unwrap(); + + let mut count = 0; + + loop { + let mut line = String::new(); + let res = buf_reader.read_line(&mut line); + match res { + Err(err) => { + panic!("failed after {} lines: {:?}", count, err); + } + Ok(0) => std::process::exit(0), + Ok(_) => { + let record = serde_json::from_str::(&line).unwrap(); + println!("[{}] {:?}", count, record); + count += 1; + } + } + } +} -- 2.44.1 From 1527942f9c73887899229e6f1186d9c602825ac2 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 19 Feb 2024 14:24:52 -0500 Subject: [PATCH 2/4] Add support for DurationWorkout and SetRep, finishing the legacy importer --- fitnesstrax/core/src/bin/legacy-importer.rs | 153 +++++++++++++++++--- fitnesstrax/core/src/lib.rs | 4 +- fitnesstrax/core/src/types.rs | 67 ++++++++- 3 files changed, 196 insertions(+), 28 deletions(-) diff --git a/fitnesstrax/core/src/bin/legacy-importer.rs b/fitnesstrax/core/src/bin/legacy-importer.rs index 1f29c99..77b6423 100644 --- a/fitnesstrax/core/src/bin/legacy-importer.rs +++ b/fitnesstrax/core/src/bin/legacy-importer.rs @@ -14,19 +14,21 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ -use std::{ - fs::File, - io::{BufRead, BufReader, Read}, - fmt, - str::FromStr, -}; use chrono::SecondsFormat; use chrono_tz::Etc::UTC; -use emseries::{Record, Series, Timestamp, RecordId}; -use ft_core::TraxRecord; -use serde::{Deserialize, Serialize, Deserializer, Serializer, de::{self, Visitor}}; - - +use dimensioned::si; +use emseries::{Record, RecordId, Series, Timestamp}; +use ft_core::{self, DurationWorkout, DurationWorkoutActivity, SetRepActivity, TraxRecord}; +use serde::{ + de::{self, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use std::{ + fmt, + fs::File, + io::{BufRead, BufReader, Read}, + str::FromStr, +}; /// This is a wrapper around date time objects, using timezones from the chroon-tz database and /// providing string representation and parsing of the form " ", i.e., @@ -116,11 +118,19 @@ impl<'de> Deserialize<'de> for DateTimeTz { } } - #[derive(Clone, Debug, Serialize, Deserialize)] struct Steps { date: DateTimeTz, - steps: u64, + steps: u32, +} + +impl From for ft_core::Steps { + fn from(s: Steps) -> Self { + Self { + date: s.date.0.naive_utc().date(), + count: s.steps, + } + } } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -129,13 +139,34 @@ struct Weight { weight: f64, } +impl From for ft_core::Weight { + fn from(w: Weight) -> Self { + Self { + date: w.date.0.naive_utc().date(), + weight: w.weight * si::KG, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] enum TDActivity { Cycling, - Running, - Walking, - Swimming, Rowing, + Running, + Swimming, + Walking, +} + +impl From for ft_core::TimeDistanceActivity { + fn from(activity: TDActivity) -> Self { + match activity { + TDActivity::Cycling => ft_core::TimeDistanceActivity::Biking, + TDActivity::Rowing => ft_core::TimeDistanceActivity::Rowing, + TDActivity::Running => ft_core::TimeDistanceActivity::Running, + TDActivity::Swimming => ft_core::TimeDistanceActivity::Swimming, + TDActivity::Walking => ft_core::TimeDistanceActivity::Walking, + } + } } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -147,25 +178,63 @@ struct TimeDistance { duration: Option, } +impl From for ft_core::TimeDistance { + fn from(td: TimeDistance) -> Self { + Self { + datetime: td.date.0.fixed_offset(), + activity: td.activity.into(), + comments: td.comments, + distance: td.distance.map(|d| d * si::M), + duration: td.duration.map(|d| d * si::S), + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] enum SRActivity { Pushups, Situps, } +impl From for SetRepActivity { + fn from(activity: SRActivity) -> Self { + match activity { + SRActivity::Pushups => Self::Pushups, + SRActivity::Situps => Self::Situps, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] struct SetRep { date: DateTimeTz, activity: SRActivity, - sets: Vec, + sets: Vec, comments: Option, } +impl From for ft_core::SetRep { + fn from(sr: SetRep) -> Self { + Self { + date: sr.date.0.naive_utc().date(), + activity: sr.activity.into(), + sets: sr.sets, + comments: sr.comments, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] enum RDActivity { MartialArts, } +impl From for DurationWorkoutActivity { + fn from(_: RDActivity) -> Self { + Self::MartialArts + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] struct RepDuration { date: DateTimeTz, @@ -173,13 +242,24 @@ struct RepDuration { sets: Vec, } +impl From for DurationWorkout { + fn from(rd: RepDuration) -> Self { + Self { + datetime: rd.date.0.fixed_offset(), + activity: rd.activity.into(), + duration: rd.sets.into_iter().map(|d| d * si::S).next(), + comments: None, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] enum LegacyData { - Steps(Steps), - Weight(Weight), - TimeDistance(TimeDistance), - SetRep(SetRep), RepDuration(RepDuration), + SetRep(SetRep), + Steps(Steps), + TimeDistance(TimeDistance), + Weight(Weight), } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -188,11 +268,39 @@ struct LegacyRecord { data: LegacyData, } +impl From for Record { + fn from(record: LegacyRecord) -> Self { + match record.data { + LegacyData::RepDuration(rd) => Record { + id: record.id, + data: TraxRecord::DurationWorkout(rd.into()), + }, + LegacyData::SetRep(sr) => Record { + id: record.id, + data: TraxRecord::SetRep(sr.into()), + }, + LegacyData::Steps(s) => Record { + id: record.id, + data: TraxRecord::Steps(s.into()), + }, + LegacyData::TimeDistance(td) => Record { + id: record.id, + data: TraxRecord::TimeDistance(td.into()), + }, + LegacyData::Weight(weight) => Record { + id: record.id, + data: TraxRecord::Weight(weight.into()), + }, + } + } +} + fn main() { let mut args = std::env::args(); let _ = args.next().unwrap(); let input_filename = args.next().unwrap(); println!("input filename: {}", input_filename); + // let output: Series = Series::open_file("import.fitnesstrax").unwrap(); let input_file = File::open(input_filename).unwrap(); let mut buf_reader = BufReader::new(input_file); @@ -211,7 +319,8 @@ fn main() { Ok(0) => std::process::exit(0), Ok(_) => { let record = serde_json::from_str::(&line).unwrap(); - println!("[{}] {:?}", count, record); + let record: Record = record.into(); + println!("{}", serde_json::to_string(&record).unwrap()); count += 1; } } diff --git a/fitnesstrax/core/src/lib.rs b/fitnesstrax/core/src/lib.rs index f579628..15f4871 100644 --- a/fitnesstrax/core/src/lib.rs +++ b/fitnesstrax/core/src/lib.rs @@ -2,5 +2,7 @@ mod legacy; mod types; pub use types::{ - Steps, TimeDistance, TimeDistanceActivity, TraxRecord, Weight, TIME_DISTANCE_ACTIVITIES, + SetRep, SetRepActivity, Steps, TimeDistance, TimeDistanceActivity, TraxRecord, Weight, + DurationWorkoutActivity, DurationWorkout, + SET_REP_ACTIVITIES, TIME_DISTANCE_ACTIVITIES, }; diff --git a/fitnesstrax/core/src/types.rs b/fitnesstrax/core/src/types.rs index d2ef95c..97a9d69 100644 --- a/fitnesstrax/core/src/types.rs +++ b/fitnesstrax/core/src/types.rs @@ -19,17 +19,38 @@ use dimensioned::si; use emseries::{Recordable, Timestamp}; use serde::{Deserialize, Serialize}; +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum SetRepActivity { + Pushups, + Situps, +} + +pub const SET_REP_ACTIVITIES: [SetRepActivity; 2] = + [SetRepActivity::Pushups, SetRepActivity::Situps]; + /// SetRep represents workouts like pushups or situps, which involve doing a "set" of a number of /// actions, resting, and then doing another set. -#[allow(dead_code)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct SetRep { /// I assume that a set/rep workout is only done once in a day. - date: NaiveDate, + pub date: NaiveDate, + /// The activity involved + pub activity: SetRepActivity, /// Each set entry represents the number of times that the action was performed in a set. So, a /// pushup workout that involved five sets would have five entries. Each entry would be x /// number of pushups. A viable workout would be something like [6, 6, 4, 4, 5]. - sets: Vec, - comments: Option, + pub sets: Vec, + pub comments: Option, +} + +impl Recordable for SetRep { + fn timestamp(&self) -> Timestamp { + Timestamp::Date(self.date) + } + + fn tags(&self) -> Vec { + vec![] + } } /// The number of steps one takes in a single day. @@ -120,11 +141,45 @@ impl Recordable for Weight { } } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum DurationWorkoutActivity { + MartialArts, + Yoga, +} + +pub const DURATION_WORKOUT_ACTIVITIES: [DurationWorkoutActivity; 2] = [ + DurationWorkoutActivity::MartialArts, + DurationWorkoutActivity::Yoga, +]; + +/// Generic workouts for which only duration really matters. This is for things +/// such as Martial Arts or Yoga, which are activities done for an amount of +/// time, but with no other details. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct DurationWorkout { + pub datetime: DateTime, + pub activity: DurationWorkoutActivity, + pub duration: Option>, + pub comments: Option, +} + +impl Recordable for DurationWorkout { + fn timestamp(&self) -> Timestamp { + Timestamp::DateTime(self.datetime) + } + + fn tags(&self) -> Vec { + vec![] + } +} + /// The unified data structure for all records that are part of the app. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum TraxRecord { - TimeDistance(TimeDistance), + DurationWorkout(DurationWorkout), + SetRep(SetRep), Steps(Steps), + TimeDistance(TimeDistance), Weight(Weight), } @@ -164,8 +219,10 @@ impl Recordable for TraxRecord { fn timestamp(&self) -> Timestamp { match self { TraxRecord::TimeDistance(rec) => Timestamp::DateTime(rec.datetime), + TraxRecord::SetRep(rec) => rec.timestamp(), TraxRecord::Steps(rec) => rec.timestamp(), TraxRecord::Weight(rec) => rec.timestamp(), + TraxRecord::DurationWorkout(rec) => rec.timestamp(), } } -- 2.44.1 From 1c2f40c868ce428c23856e0bb28138ed759012bf Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 19 Feb 2024 16:14:08 -0500 Subject: [PATCH 3/4] Apply clippy suggestions --- fitnesstrax/app/src/main.rs | 5 ++--- fitnesstrax/core/src/lib.rs | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/fitnesstrax/app/src/main.rs b/fitnesstrax/app/src/main.rs index 921f708..b6ca965 100644 --- a/fitnesstrax/app/src/main.rs +++ b/fitnesstrax/app/src/main.rs @@ -25,8 +25,7 @@ mod views; use adw::prelude::*; use app_window::AppWindow; -use components::ActionGroup; -use gio::{Action, ActionEntry}; +use gio::ActionEntry; use std::{env, path::PathBuf}; const APP_ID_DEV: &str = "com.luminescent-dreams.fitnesstrax.dev"; @@ -81,7 +80,7 @@ fn main() { let icon_theme = gtk::IconTheme::for_display(&gdk::Display::default().unwrap()); icon_theme.add_resource_path(&(RESOURCE_BASE_PATH.to_owned() + "/icons/scalable/actions")); - setup_app_close_action(&adw_app); + setup_app_close_action(adw_app); AppWindow::new(app_id, RESOURCE_BASE_PATH, adw_app, ft_app.clone()); }); diff --git a/fitnesstrax/core/src/lib.rs b/fitnesstrax/core/src/lib.rs index 15f4871..007dc3f 100644 --- a/fitnesstrax/core/src/lib.rs +++ b/fitnesstrax/core/src/lib.rs @@ -2,7 +2,7 @@ mod legacy; mod types; pub use types::{ - SetRep, SetRepActivity, Steps, TimeDistance, TimeDistanceActivity, TraxRecord, Weight, - DurationWorkoutActivity, DurationWorkout, - SET_REP_ACTIVITIES, TIME_DISTANCE_ACTIVITIES, + DurationWorkout, DurationWorkoutActivity, SetRep, SetRepActivity, Steps, TimeDistance, + TimeDistanceActivity, TraxRecord, Weight, DURATION_WORKOUT_ACTIVITIES, SET_REP_ACTIVITIES, + TIME_DISTANCE_ACTIVITIES, }; -- 2.44.1 From 55b6327d423012f6d23dd6e0bd03a28706c2e48e Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 19 Feb 2024 16:27:11 -0500 Subject: [PATCH 4/4] Open the welcome screen if the database is not available --- fitnesstrax/app/src/app.rs | 14 ++------------ fitnesstrax/app/src/app_window.rs | 11 +++++++---- fitnesstrax/app/src/views/mod.rs | 1 - 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/fitnesstrax/app/src/app.rs b/fitnesstrax/app/src/app.rs index 94c83b8..546cecb 100644 --- a/fitnesstrax/app/src/app.rs +++ b/fitnesstrax/app/src/app.rs @@ -96,18 +96,8 @@ impl App { .unwrap() } - pub async fn get_record(&self, id: RecordId) -> Result>, AppError> { - let db = self.database.clone(); - self.runtime - .spawn_blocking(move || { - if let Some(ref db) = *db.read().unwrap() { - Ok(db.get(&id)) - } else { - Err(AppError::NoDatabase) - } - }) - .await - .unwrap() + pub fn database_is_open(&self) -> bool { + self.database.read().unwrap().is_some() } } diff --git a/fitnesstrax/app/src/app_window.rs b/fitnesstrax/app/src/app_window.rs index 31a05c3..3bdd4a4 100644 --- a/fitnesstrax/app/src/app_window.rs +++ b/fitnesstrax/app/src/app_window.rs @@ -136,7 +136,6 @@ impl AppWindow { s } - #[allow(unused)] fn show_welcome_view(&self) { let view = View::Welcome(WelcomeView::new({ let s = self.clone(); @@ -177,9 +176,13 @@ impl AppWindow { glib::spawn_future_local({ let s = self.clone(); async move { - let end = Local::now().date_naive(); - let start = end - Duration::days(7); - s.show_historical_view(DayInterval { start, end }); + if s.app.database_is_open() { + let end = Local::now().date_naive(); + let start = end - Duration::days(7); + s.show_historical_view(DayInterval { start, end }); + } else { + s.show_welcome_view(); + } } }); } diff --git a/fitnesstrax/app/src/views/mod.rs b/fitnesstrax/app/src/views/mod.rs index a8c7360..9957823 100644 --- a/fitnesstrax/app/src/views/mod.rs +++ b/fitnesstrax/app/src/views/mod.rs @@ -30,7 +30,6 @@ pub use welcome_view::WelcomeView; pub enum View { Placeholder(PlaceholderView), - #[allow(unused)] Welcome(WelcomeView), Historical(HistoricalView), } -- 2.44.1