Compare commits

..

14 Commits

Author SHA1 Message Date
Savanni D'Gerinel 31d74588fb Clean up warnings and remove printlns 2024-01-31 09:42:07 -05:00
Savanni D'Gerinel 07f487b139 Reload data when the user saves on the DayEdit panel
This required some big overhauls. The view model no longer takes records. It only takes the date that it is responsible for, and it will ask the database for records pertaining to that date. This means that once the view model has saved all of its records, it can simply reload those records from the database. This has the effect that as soon as the user moves from DayEdit back to DayDetail, all of the interesting information has been repopulated.
2024-01-31 09:38:28 -05:00
Savanni D'Gerinel 79d705c1d0 The view model can no longer be initialized without an app 2024-01-31 09:38:28 -05:00
Savanni D'Gerinel 2d476f266c Create Duration and Distance structures to handle rendering
These structures handle parsing and rendering of a Duration and a Distance, allowing that knowledge to be centralized and reused. Then I'm using those structures in a variety of places in order to ensure that the information gets rendered consistently.
2024-01-31 09:38:28 -05:00
Savanni D'Gerinel 798fbff320 Show existing time/distance workout rows in day detail and editor 2024-01-31 09:28:22 -05:00
Savanni D'Gerinel 6e26923629 Save new time/distance records
This sets up a bunch of callbacks. We're starting to get into Callback Hell, where there are things that need knowledge that I really don't want them to have.

However, edit fields for TimeDistanceEdit now propogate data back into the view model, which is then able to save the results.
2024-01-31 09:28:22 -05:00
Savanni D'Gerinel 4fdf390ecf Build the facilities to add a new time/distance workout
This adds the code to show the new records in the UI, plus it adds them to the view model. Some of the representation changed in order to facilitate linking UI elements to particular records. There are now some buttons to create workouts of various types, clicking on a button adds a new row to the UI, and it also adds a new record to the view model. Saving the view model writes the records to the database.
2024-01-31 09:17:59 -05:00
Savanni D'Gerinel 29b1e6054b Make emseries::Record copyable 2024-01-31 09:16:35 -05:00
Savanni D'Gerinel 0f5af82cb5 Build some convenienc functions for measurement entry fields
Move the weight field into text_entry
2024-01-31 09:16:35 -05:00
Savanni D'Gerinel 525cc88c25 Add buttons with icons to represent workouts 2024-01-31 09:12:55 -05:00
Savanni D'Gerinel 06d118060e Add a test program for gnome icons 2024-01-31 09:12:55 -05:00
Savanni D'Gerinel 251077b0c1 Implement the Edit Cancel button 2024-01-31 09:12:55 -05:00
Savanni D'Gerinel ce8bed13f9 Render time distance details in the day detail view 2024-01-31 09:09:04 -05:00
Savanni D'Gerinel 279810f7d7 Show a summary of the day's biking stats when there is one 2024-01-31 09:09:04 -05:00
17 changed files with 382 additions and 749 deletions

50
Cargo.lock generated
View File

@ -133,17 +133,6 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "async-trait"
version = "0.1.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]] [[package]]
name = "atoi" name = "atoi"
version = "2.0.0" version = "2.0.0"
@ -431,7 +420,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.41",
] ]
[[package]] [[package]]
@ -747,7 +736,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.41",
] ]
[[package]] [[package]]
@ -939,7 +928,6 @@ name = "fitnesstrax"
version = "0.3.0" version = "0.3.0"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"async-trait",
"chrono", "chrono",
"chrono-tz", "chrono-tz",
"dimensioned 0.8.0", "dimensioned 0.8.0",
@ -1145,7 +1133,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.41",
] ]
[[package]] [[package]]
@ -1380,7 +1368,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.41",
] ]
[[package]] [[package]]
@ -2466,7 +2454,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.41",
] ]
[[package]] [[package]]
@ -2671,7 +2659,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.41",
] ]
[[package]] [[package]]
@ -2815,9 +2803,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.78" version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -2859,9 +2847,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.35" version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -3344,7 +3332,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.41",
] ]
[[package]] [[package]]
@ -3743,9 +3731,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.48" version = "2.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3841,7 +3829,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.41",
] ]
[[package]] [[package]]
@ -3965,7 +3953,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.41",
] ]
[[package]] [[package]]
@ -4086,7 +4074,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.41",
] ]
[[package]] [[package]]
@ -4456,7 +4444,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.41",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -4490,7 +4478,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.41",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -4731,7 +4719,7 @@ checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.48", "syn 2.0.41",
] ]
[[package]] [[package]]

View File

@ -655,32 +655,6 @@ rec {
}; };
resolvedDefaultFeatures = [ "default" "std" ]; resolvedDefaultFeatures = [ "default" "std" ];
}; };
"async-trait" = rec {
crateName = "async-trait";
version = "0.1.77";
edition = "2021";
sha256 = "1adf1jh2yg39rkpmqjqyr9xyd6849p0d95425i6imgbhx0syx069";
procMacro = true;
authors = [
"David Tolnay <dtolnay@gmail.com>"
];
dependencies = [
{
name = "proc-macro2";
packageId = "proc-macro2";
}
{
name = "quote";
packageId = "quote";
}
{
name = "syn";
packageId = "syn 2.0.48";
features = [ "full" "visit-mut" ];
}
];
};
"atoi" = rec { "atoi" = rec {
crateName = "atoi"; crateName = "atoi";
version = "2.0.0"; version = "2.0.0";
@ -1513,7 +1487,7 @@ rec {
} }
{ {
name = "syn"; name = "syn";
packageId = "syn 2.0.48"; packageId = "syn 2.0.41";
features = [ "full" ]; features = [ "full" ];
} }
]; ];
@ -2420,7 +2394,7 @@ rec {
} }
{ {
name = "syn"; name = "syn";
packageId = "syn 2.0.48"; packageId = "syn 2.0.41";
} }
]; ];
features = { features = {
@ -2998,10 +2972,6 @@ rec {
name = "async-channel"; name = "async-channel";
packageId = "async-channel"; packageId = "async-channel";
} }
{
name = "async-trait";
packageId = "async-trait";
}
{ {
name = "chrono"; name = "chrono";
packageId = "chrono"; packageId = "chrono";
@ -3606,7 +3576,7 @@ rec {
} }
{ {
name = "syn"; name = "syn";
packageId = "syn 2.0.48"; packageId = "syn 2.0.41";
features = [ "full" ]; features = [ "full" ];
} }
]; ];
@ -4371,7 +4341,7 @@ rec {
} }
{ {
name = "syn"; name = "syn";
packageId = "syn 2.0.48"; packageId = "syn 2.0.41";
features = [ "full" ]; features = [ "full" ];
} }
]; ];
@ -7720,7 +7690,7 @@ rec {
} }
{ {
name = "syn"; name = "syn";
packageId = "syn 2.0.48"; packageId = "syn 2.0.41";
features = [ "full" ]; features = [ "full" ];
} }
]; ];
@ -8217,7 +8187,7 @@ rec {
} }
{ {
name = "syn"; name = "syn";
packageId = "syn 2.0.48"; packageId = "syn 2.0.41";
features = [ "full" "visit-mut" ]; features = [ "full" "visit-mut" ];
} }
]; ];
@ -8559,9 +8529,9 @@ rec {
}; };
"proc-macro2" = rec { "proc-macro2" = rec {
crateName = "proc-macro2"; crateName = "proc-macro2";
version = "1.0.78"; version = "1.0.70";
edition = "2021"; edition = "2021";
sha256 = "1bjak27pqdn4f4ih1c9nr3manzyavsgqmf76ygw9k76q8pb2lhp2"; sha256 = "0fzxg3dkrjy101vv5b6llc8mh74xz1vhhsaiwrn68kzvynxqy9rr";
authors = [ authors = [
"David Tolnay <dtolnay@gmail.com>" "David Tolnay <dtolnay@gmail.com>"
"Alex Crichton <alex@alexcrichton.com>" "Alex Crichton <alex@alexcrichton.com>"
@ -8695,9 +8665,9 @@ rec {
}; };
"quote" = rec { "quote" = rec {
crateName = "quote"; crateName = "quote";
version = "1.0.35"; version = "1.0.33";
edition = "2018"; edition = "2018";
sha256 = "1vv8r2ncaz4pqdr78x7f138ka595sp2ncr1sa2plm4zxbsmwj7i9"; sha256 = "1biw54hbbr12wdwjac55z1m2x2rylciw83qnjn564a3096jgqrsj";
authors = [ authors = [
"David Tolnay <dtolnay@gmail.com>" "David Tolnay <dtolnay@gmail.com>"
]; ];
@ -10299,7 +10269,7 @@ rec {
} }
{ {
name = "syn"; name = "syn";
packageId = "syn 2.0.48"; packageId = "syn 2.0.41";
} }
]; ];
features = { features = {
@ -11778,11 +11748,11 @@ rec {
}; };
resolvedDefaultFeatures = [ "clone-impls" "default" "derive" "extra-traits" "full" "parsing" "printing" "proc-macro" "quote" ]; resolvedDefaultFeatures = [ "clone-impls" "default" "derive" "extra-traits" "full" "parsing" "printing" "proc-macro" "quote" ];
}; };
"syn 2.0.48" = rec { "syn 2.0.41" = rec {
crateName = "syn"; crateName = "syn";
version = "2.0.48"; version = "2.0.41";
edition = "2021"; edition = "2021";
sha256 = "0gqgfygmrxmp8q32lia9p294kdd501ybn6kn2h4gqza0irik2d8g"; sha256 = "0sg2lzkwbwbm229p3kx1yxai43hkc0s1wmk6g47bzhvw8y6b5j24";
authors = [ authors = [
"David Tolnay <dtolnay@gmail.com>" "David Tolnay <dtolnay@gmail.com>"
]; ];
@ -12020,7 +11990,7 @@ rec {
} }
{ {
name = "syn"; name = "syn";
packageId = "syn 2.0.48"; packageId = "syn 2.0.41";
} }
]; ];
@ -12436,7 +12406,7 @@ rec {
} }
{ {
name = "syn"; name = "syn";
packageId = "syn 2.0.48"; packageId = "syn 2.0.41";
features = [ "full" ]; features = [ "full" ];
} }
]; ];
@ -12850,7 +12820,7 @@ rec {
} }
{ {
name = "syn"; name = "syn";
packageId = "syn 2.0.48"; packageId = "syn 2.0.41";
usesDefaultFeatures = false; usesDefaultFeatures = false;
features = [ "full" "parsing" "printing" "visit-mut" "clone-impls" "extra-traits" "proc-macro" ]; features = [ "full" "parsing" "printing" "visit-mut" "clone-impls" "extra-traits" "proc-macro" ];
} }
@ -13844,7 +13814,7 @@ rec {
} }
{ {
name = "syn"; name = "syn";
packageId = "syn 2.0.48"; packageId = "syn 2.0.41";
features = [ "full" ]; features = [ "full" ];
} }
{ {
@ -13934,7 +13904,7 @@ rec {
} }
{ {
name = "syn"; name = "syn";
packageId = "syn 2.0.48"; packageId = "syn 2.0.41";
features = [ "visit" "full" ]; features = [ "visit" "full" ];
} }
{ {
@ -15418,7 +15388,7 @@ rec {
} }
{ {
name = "syn"; name = "syn";
packageId = "syn 2.0.48"; packageId = "syn 2.0.41";
} }
]; ];

View File

@ -124,10 +124,13 @@ where
/// Put a new record into the database. A unique id will be assigned to the record and /// Put a new record into the database. A unique id will be assigned to the record and
/// returned. /// returned.
pub fn put(&mut self, entry: T) -> Result<RecordId, EmseriesWriteError> { pub fn put(&mut self, entry: T) -> Result<RecordId, EmseriesWriteError> {
let id = RecordId::default(); let uuid = RecordId::default();
let record = Record { id, data: entry }; let record = Record {
id: uuid,
data: entry,
};
self.update(record)?; self.update(record)?;
Ok(id) Ok(uuid)
} }
/// Update an existing record. The [RecordId] of the record passed into this function must match /// Update an existing record. The [RecordId] of the record passed into this function must match

View File

@ -166,17 +166,6 @@ impl<T: Clone + Recordable> Record<T> {
pub fn timestamp(&self) -> Timestamp { pub fn timestamp(&self) -> Timestamp {
self.data.timestamp() self.data.timestamp()
} }
pub fn map<Map, U>(self, map: Map) -> Record<U>
where
Map: Fn(T) -> U,
U: Clone + Recordable,
{
Record {
id: self.id,
data: map(self.data),
}
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -8,7 +8,6 @@ edition = "2021"
[dependencies] [dependencies]
adw = { version = "0.5", package = "libadwaita", features = [ "v1_4" ] } adw = { version = "0.5", package = "libadwaita", features = [ "v1_4" ] }
async-channel = { version = "2.1" } async-channel = { version = "2.1" }
async-trait = { version = "0.1" }
chrono = { version = "0.4" } chrono = { version = "0.4" }
chrono-tz = { version = "0.8" } chrono-tz = { version = "0.8" }
dimensioned = { version = "0.8", features = [ "serde" ] } dimensioned = { version = "0.8", features = [ "serde" ] }

View File

@ -14,7 +14,6 @@ General Public License for more details.
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/ */
use async_trait::async_trait;
use chrono::NaiveDate; use chrono::NaiveDate;
use emseries::{time_range, Record, RecordId, Series, Timestamp}; use emseries::{time_range, Record, RecordId, Series, Timestamp};
use ft_core::TraxRecord; use ft_core::TraxRecord;
@ -35,32 +34,6 @@ pub enum AppError {
Unhandled, Unhandled,
} }
#[derive(Debug, Error)]
pub enum ReadError {
#[error("no database loaded")]
NoDatabase,
}
#[derive(Debug, Error)]
pub enum WriteError {
#[error("no database loaded")]
NoDatabase,
#[error("unhandled error")]
Unhandled,
}
#[async_trait]
pub trait RecordProvider: Send + Sync {
async fn records(
&self,
start: NaiveDate,
end: NaiveDate,
) -> Result<Vec<Record<TraxRecord>>, ReadError>;
async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError>;
async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError>;
async fn delete_record(&self, id: RecordId) -> Result<(), WriteError>;
}
/// The real, headless application. This is where all of the logic will reside. /// The real, headless application. This is where all of the logic will reside.
#[derive(Clone)] #[derive(Clone)]
pub struct App { pub struct App {
@ -84,18 +57,6 @@ impl App {
} }
} }
pub async fn open_db(&self, path: PathBuf) -> Result<(), AppError> {
let db_ref = self.database.clone();
self.runtime
.spawn_blocking(move || {
let db = Series::open(path).map_err(|_| AppError::FailedToOpenDatabase)?;
*db_ref.write().unwrap() = Some(db);
Ok(())
})
.await
.unwrap()
}
pub async fn get_record(&self, id: RecordId) -> Result<Option<Record<TraxRecord>>, AppError> { pub async fn get_record(&self, id: RecordId) -> Result<Option<Record<TraxRecord>>, AppError> {
let db = self.database.clone(); let db = self.database.clone();
self.runtime self.runtime
@ -109,15 +70,12 @@ impl App {
.await .await
.unwrap() .unwrap()
} }
}
#[async_trait] pub async fn records(
impl RecordProvider for App {
async fn records(
&self, &self,
start: NaiveDate, start: NaiveDate,
end: NaiveDate, end: NaiveDate,
) -> Result<Vec<Record<TraxRecord>>, ReadError> { ) -> Result<Vec<Record<TraxRecord>>, AppError> {
let db = self.database.clone(); let db = self.database.clone();
self.runtime self.runtime
.spawn_blocking(move || { .spawn_blocking(move || {
@ -133,14 +91,14 @@ impl RecordProvider for App {
.collect::<Vec<Record<TraxRecord>>>(); .collect::<Vec<Record<TraxRecord>>>();
Ok(records) Ok(records)
} else { } else {
Err(ReadError::NoDatabase) Err(AppError::NoDatabase)
} }
}) })
.await .await
.unwrap() .unwrap()
} }
async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError> { pub async fn put_record(&self, record: TraxRecord) -> Result<RecordId, AppError> {
let db = self.database.clone(); let db = self.database.clone();
self.runtime self.runtime
.spawn_blocking(move || { .spawn_blocking(move || {
@ -153,10 +111,10 @@ impl RecordProvider for App {
}) })
.await .await
.unwrap() .unwrap()
.map_err(|_| WriteError::Unhandled) .map_err(|_| AppError::Unhandled)
} }
async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError> { pub async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), AppError> {
let db = self.database.clone(); let db = self.database.clone();
self.runtime self.runtime
.spawn_blocking(move || { .spawn_blocking(move || {
@ -168,10 +126,18 @@ impl RecordProvider for App {
}) })
.await .await
.unwrap() .unwrap()
.map_err(|_| WriteError::Unhandled) .map_err(|_| AppError::Unhandled)
} }
async fn delete_record(&self, _id: RecordId) -> Result<(), WriteError> { pub async fn open_db(&self, path: PathBuf) -> Result<(), AppError> {
unimplemented!() let db_ref = self.database.clone();
self.runtime
.spawn_blocking(move || {
let db = Series::open(path).map_err(|_| AppError::FailedToOpenDatabase)?;
*db_ref.write().unwrap() = Some(db);
Ok(())
})
.await
.unwrap()
} }
} }

View File

@ -121,7 +121,6 @@ impl AppWindow {
s s
} }
#[allow(unused)]
fn show_welcome_view(&self) { fn show_welcome_view(&self) {
let view = View::Welcome(WelcomeView::new({ let view = View::Welcome(WelcomeView::new({
let s = self.clone(); let s = self.clone();
@ -130,13 +129,13 @@ impl AppWindow {
self.swap_main(view); self.swap_main(view);
} }
fn show_historical_view(&self, interval: DayInterval) { fn show_historical_view(&self, start_date: chrono::NaiveDate, end_date: chrono::NaiveDate) {
let on_select_day = { let on_select_day = {
let s = self.clone(); let s = self.clone();
move |date| { move |date, _records| {
let s = s.clone(); let s = s.clone();
glib::spawn_future_local(async move { glib::spawn_future_local(async move {
let view_model = DayDetailViewModel::new(date, s.app.clone()).await.unwrap(); let view_model = DayDetailViewModel::new(date, s.app.clone()).await;
let layout = gtk::Box::new(gtk::Orientation::Vertical, 0); let layout = gtk::Box::new(gtk::Orientation::Vertical, 0);
layout.append(&adw::HeaderBar::new()); layout.append(&adw::HeaderBar::new());
// layout.append(&DayDetailView::new(date, records, s.app.clone())); // layout.append(&DayDetailView::new(date, records, s.app.clone()));
@ -152,7 +151,10 @@ impl AppWindow {
let view = View::Historical(HistoricalView::new( let view = View::Historical(HistoricalView::new(
self.app.clone(), self.app.clone(),
interval, DayInterval {
start: start_date,
end: end_date,
},
Rc::new(on_select_day), Rc::new(on_select_day),
)); ));
self.swap_main(view); self.swap_main(view);
@ -164,7 +166,10 @@ impl AppWindow {
async move { async move {
let end = Local::now().date_naive(); let end = Local::now().date_naive();
let start = end - Duration::days(7); let start = end - Duration::days(7);
s.show_historical_view(DayInterval { start, end }); match s.app.records(start, end).await {
Ok(_records) => s.show_historical_view(start, end),
Err(_) => s.show_welcome_view(),
}
} }
}); });
} }
@ -181,7 +186,6 @@ impl AppWindow {
self.layout.append(&current_widget.widget()); self.layout.append(&current_widget.widget());
} }
#[allow(unused)]
fn on_apply_config(&self, path: PathBuf) { fn on_apply_config(&self, path: PathBuf) {
glib::spawn_future_local({ glib::spawn_future_local({
let s = self.clone(); let s = self.clone();

View File

@ -20,11 +20,10 @@ use crate::{
components::{ components::{
steps_editor, time_distance_summary, weight_field, ActionGroup, Steps, WeightLabel, steps_editor, time_distance_summary, weight_field, ActionGroup, Steps, WeightLabel,
}, },
types::{DistanceFormatter, DurationFormatter, WeightFormatter},
view_models::DayDetailViewModel, view_models::DayDetailViewModel,
}; };
use emseries::{Record, RecordId}; use emseries::{Record, RecordId};
use ft_core::{TimeDistanceActivity, TraxRecord}; use ft_core::{RecordType, TraxRecord};
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
@ -105,11 +104,8 @@ impl DaySummary {
row.append(&label); row.append(&label);
self.append(&row); self.append(&row);
let biking_summary = view_model.time_distance_summary(TimeDistanceActivity::BikeRide); let biking_summary = view_model.biking_summary();
if let Some(label) = time_distance_summary( if let Some(label) = time_distance_summary(biking_summary.0, biking_summary.1) {
DistanceFormatter::from(biking_summary.0),
DurationFormatter::from(biking_summary.1),
) {
self.append(&label); self.append(&label);
} }
} }
@ -151,7 +147,7 @@ impl DayDetail {
let top_row = gtk::Box::builder() let top_row = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal) .orientation(gtk::Orientation::Horizontal)
.build(); .build();
let weight_view = WeightLabel::new(view_model.weight().map(WeightFormatter::from)); let weight_view = WeightLabel::new(view_model.weight());
top_row.append(&weight_view.widget()); top_row.append(&weight_view.widget());
let steps_view = Steps::new(view_model.steps()); let steps_view = Steps::new(view_model.steps());
@ -159,9 +155,18 @@ impl DayDetail {
s.append(&top_row); s.append(&top_row);
let records = view_model.time_distance_records(); let records = view_model.records();
for emseries::Record { data, .. } in records { for emseries::Record { data, .. } in records {
s.append(&time_distance_detail(data)); match data {
TraxRecord::BikeRide(ride) => {
s.append(&time_distance_detail(RecordType::BikeRide, ride))
}
TraxRecord::Row(row) => s.append(&time_distance_detail(RecordType::Row, row)),
TraxRecord::Run(run) => s.append(&time_distance_detail(RecordType::Run, run)),
TraxRecord::Swim(walk) => s.append(&time_distance_detail(RecordType::Swim, walk)),
TraxRecord::Walk(walk) => s.append(&time_distance_detail(RecordType::Walk, walk)),
_ => {}
}
} }
s s
@ -170,7 +175,6 @@ impl DayDetail {
pub struct DayEditPrivate { pub struct DayEditPrivate {
on_finished: RefCell<Box<dyn Fn()>>, on_finished: RefCell<Box<dyn Fn()>>,
#[allow(unused)]
workout_rows: RefCell<gtk::Box>, workout_rows: RefCell<gtk::Box>,
view_model: RefCell<Option<DayDetailViewModel>>, view_model: RefCell<Option<DayDetailViewModel>>,
} }
@ -226,14 +230,23 @@ impl DayEdit {
.into_iter() .into_iter()
.filter_map({ .filter_map({
let s = s.clone(); let s = s.clone();
move |record| match record.data { move |record| {
TraxRecord::TimeDistance(workout) => Some(TimeDistanceEdit::new(workout, { let workout_type = record.data.workout_type();
let s = s.clone(); match record.data {
move |data| { TraxRecord::BikeRide(workout)
s.update_workout(record.id, data); | TraxRecord::Row(workout)
| TraxRecord::Run(workout)
| TraxRecord::Swim(workout)
| TraxRecord::Walk(workout) => {
Some(TimeDistanceEdit::new(workout_type, workout, {
let s = s.clone();
move |type_, data| {
s.update_workout(record.id, type_, data);
}
}))
} }
})), _ => None,
_ => None, }
} }
}) })
.for_each(|row| s.imp().workout_rows.borrow().append(&row)); .for_each(|row| s.imp().workout_rows.borrow().append(&row));
@ -253,11 +266,10 @@ impl DayEdit {
let view_model = { let view_model = {
let view_model = s.imp().view_model.borrow(); let view_model = s.imp().view_model.borrow();
view_model view_model
.as_ref()
.expect("DayEdit has not been initialized with the view model")
.clone() .clone()
.expect("DayEdit has not been initialized with the view model")
}; };
let _ = view_model.async_save().await; let _ = view_model.save().await;
(s.imp().on_finished.borrow())() (s.imp().on_finished.borrow())()
} }
}); });
@ -266,28 +278,39 @@ impl DayEdit {
fn add_row(&self, workout: Record<TraxRecord>) { fn add_row(&self, workout: Record<TraxRecord>) {
let workout_rows = self.imp().workout_rows.borrow(); let workout_rows = self.imp().workout_rows.borrow();
#[allow(clippy::single_match)] let workout_id = workout.id;
match workout.data { let workout_type = workout.data.workout_type();
TraxRecord::TimeDistance(r) => workout_rows.append(&TimeDistanceEdit::new(r, {
let s = self.clone();
move |data| {
println!("update workout callback on workout: {:?}", workout.id);
s.update_workout(workout.id, data)
}
})),
match workout.data {
TraxRecord::BikeRide(ref w)
| TraxRecord::Row(ref w)
| TraxRecord::Swim(ref w)
| TraxRecord::Run(ref w)
| TraxRecord::Walk(ref w) => {
workout_rows.append(&TimeDistanceEdit::new(workout_type, w.clone(), {
let s = self.clone();
move |type_, data| s.update_workout(workout_id, type_, data)
}));
}
_ => {} _ => {}
} }
} }
fn update_workout(&self, id: RecordId, data: ft_core::TimeDistance) { fn update_workout(&self, id: RecordId, type_: RecordType, data: ft_core::TimeDistance) {
if let Some(ref view_model) = *self.imp().view_model.borrow() { let data = match type_ {
let record = Record { RecordType::BikeRide => TraxRecord::BikeRide(data),
id, RecordType::Row => TraxRecord::Row(data),
data: TraxRecord::TimeDistance(data), RecordType::Swim => TraxRecord::Swim(data),
}; RecordType::Run => TraxRecord::Run(data),
view_model.update_record(record); RecordType::Walk => TraxRecord::Walk(data),
} _ => panic!("Record type {:?} is not a Time/Distance record", type_),
};
let record = Record { id, data };
let view_model = self.imp().view_model.borrow();
let view_model = view_model
.as_ref()
.expect("DayEdit has not been initialized with a view model");
view_model.update_record(record);
} }
} }
@ -295,18 +318,15 @@ fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup
ActionGroup::builder() ActionGroup::builder()
.primary_action("Save", { .primary_action("Save", {
let s = s.clone(); let s = s.clone();
let _view_model = view_model.clone();
move || s.finish() move || s.finish()
}) })
.secondary_action("Cancel", { .secondary_action("Cancel", {
let s = s.clone(); let s = s.clone();
let view_model = view_model.clone(); let view_model = view_model.clone();
move || { move || {
let s = s.clone(); view_model.revert();
let view_model = view_model.clone(); s.finish();
glib::spawn_future_local(async move {
view_model.revert().await;
s.finish();
});
} }
}) })
.build() .build()
@ -317,10 +337,10 @@ fn weight_and_steps_row(view_model: &DayDetailViewModel) -> gtk::Box {
.orientation(gtk::Orientation::Horizontal) .orientation(gtk::Orientation::Horizontal)
.build(); .build();
row.append( row.append(
&weight_field(view_model.weight().map(WeightFormatter::from), { &weight_field(view_model.weight(), {
let view_model = view_model.clone(); let view_model = view_model.clone();
move |w| match w { move |w| match w {
Some(w) => view_model.set_weight(*w), Some(w) => view_model.set_weight(w),
None => eprintln!("have not implemented record delete"), None => eprintln!("have not implemented record delete"),
} }
}) })
@ -356,8 +376,8 @@ where
let view_model = view_model.clone(); let view_model = view_model.clone();
let add_row = add_row.clone(); let add_row = add_row.clone();
move |_| { move |_| {
let workout = view_model.new_time_distance(TimeDistanceActivity::Walking); let workout = view_model.new_record(RecordType::Walk);
add_row(workout.map(TraxRecord::TimeDistance)); &add_row(workout);
} }
}); });
@ -369,8 +389,8 @@ where
running_button.connect_clicked({ running_button.connect_clicked({
let view_model = view_model.clone(); let view_model = view_model.clone();
move |_| { move |_| {
let workout = view_model.new_time_distance(TimeDistanceActivity::Running); let workout = view_model.new_record(RecordType::Walk);
add_row(workout.map(TraxRecord::TimeDistance)); add_row(workout);
} }
}); });
*/ */
@ -383,8 +403,8 @@ where
biking_button.connect_clicked({ biking_button.connect_clicked({
let view_model = view_model.clone(); let view_model = view_model.clone();
move |_| { move |_| {
let workout = view_model.new_time_distance(TimeDistanceActivity::BikeRide); let workout = view_model.new_record(RecordType::BikeRide);
add_row(workout.map(TraxRecord::TimeDistance)); add_row(workout);
} }
}); });

View File

@ -106,7 +106,6 @@ impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
} }
} }
#[allow(unused)]
pub fn time_field<OnUpdate>( pub fn time_field<OnUpdate>(
value: Option<TimeFormatter>, value: Option<TimeFormatter>,
on_update: OnUpdate, on_update: OnUpdate,
@ -123,7 +122,6 @@ where
) )
} }
#[allow(unused)]
pub fn distance_field<OnUpdate>( pub fn distance_field<OnUpdate>(
value: Option<DistanceFormatter>, value: Option<DistanceFormatter>,
on_update: OnUpdate, on_update: OnUpdate,
@ -140,7 +138,6 @@ where
) )
} }
#[allow(unused)]
pub fn duration_field<OnUpdate>( pub fn duration_field<OnUpdate>(
value: Option<DurationFormatter>, value: Option<DurationFormatter>,
on_update: OnUpdate, on_update: OnUpdate,

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2023-2024, Savanni D'Gerinel <savanni@luminescent-dreams.com> Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of FitnessTrax. This file is part of FitnessTrax.
@ -22,10 +22,10 @@ use crate::{
types::{DistanceFormatter, DurationFormatter, FormatOption, TimeFormatter}, types::{DistanceFormatter, DurationFormatter, FormatOption, TimeFormatter},
}; };
use dimensioned::si; use dimensioned::si;
use ft_core::{TimeDistance, TimeDistanceActivity}; use ft_core::{RecordType, TimeDistance};
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use std::{rc::Rc, cell::RefCell}; use std::cell::RefCell;
pub fn time_distance_summary( pub fn time_distance_summary(
distance: DistanceFormatter, distance: DistanceFormatter,
@ -45,7 +45,7 @@ pub fn time_distance_summary(
text.map(|text| gtk::Label::new(Some(&text))) text.map(|text| gtk::Label::new(Some(&text)))
} }
pub fn time_distance_detail(record: ft_core::TimeDistance) -> gtk::Box { pub fn time_distance_detail(type_: ft_core::RecordType, record: ft_core::TimeDistance) -> gtk::Box {
let layout = gtk::Box::builder() let layout = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical) .orientation(gtk::Orientation::Vertical)
.hexpand(true) .hexpand(true)
@ -62,7 +62,7 @@ pub fn time_distance_detail(record: ft_core::TimeDistance) -> gtk::Box {
first_row.append( first_row.append(
&gtk::Label::builder() &gtk::Label::builder()
.halign(gtk::Align::Start) .halign(gtk::Align::Start)
.label(format!("{:?}", record.activity)) .label(format!("{:?}", type_))
.build(), .build(),
); );
@ -109,25 +109,18 @@ pub fn time_distance_detail(record: ft_core::TimeDistance) -> gtk::Box {
layout layout
} }
type OnUpdate = Rc<RefCell<Box<dyn Fn(TimeDistance)>>>;
pub struct TimeDistanceEditPrivate { pub struct TimeDistanceEditPrivate {
#[allow(unused)] type_: RefCell<RecordType>,
workout: RefCell<ft_core::TimeDistance>, workout: RefCell<TimeDistance>,
on_update: OnUpdate, on_update: RefCell<Box<dyn Fn(RecordType, TimeDistance)>>,
} }
impl Default for TimeDistanceEditPrivate { impl Default for TimeDistanceEditPrivate {
fn default() -> Self { fn default() -> Self {
Self { Self {
workout: RefCell::new(TimeDistance { type_: RefCell::new(RecordType::BikeRide),
datetime: chrono::Utc::now().into(), workout: RefCell::new(TimeDistance::new(chrono::Utc::now().into())),
activity: TimeDistanceActivity::BikeRide, on_update: RefCell::new(Box::new(|_, _| {})),
duration: None,
distance: None,
comments: None,
}),
on_update: Rc::new(RefCell::new(Box::new(|_| {}))),
} }
} }
} }
@ -159,12 +152,13 @@ impl Default for TimeDistanceEdit {
} }
impl TimeDistanceEdit { impl TimeDistanceEdit {
pub fn new<OnUpdate>(workout: TimeDistance, on_update: OnUpdate) -> Self pub fn new<OnUpdate>(type_: RecordType, workout: TimeDistance, on_update: OnUpdate) -> Self
where where
OnUpdate: Fn(TimeDistance) + 'static, OnUpdate: Fn(ft_core::RecordType, ft_core::TimeDistance) + 'static,
{ {
let s = Self::default(); let s = Self::default();
*s.imp().type_.borrow_mut() = type_;
*s.imp().workout.borrow_mut() = workout.clone(); *s.imp().workout.borrow_mut() = workout.clone();
*s.imp().on_update.borrow_mut() = Box::new(on_update); *s.imp().on_update.borrow_mut() = Box::new(on_update);
@ -202,19 +196,19 @@ impl TimeDistanceEdit {
s s
} }
fn update_time(&self, _time: Option<TimeFormatter>) { fn update_time(&self, time: Option<TimeFormatter>) {
unimplemented!() unimplemented!()
} }
fn update_distance(&self, distance: Option<DistanceFormatter>) { fn update_distance(&self, distance: Option<DistanceFormatter>) {
let mut workout = self.imp().workout.borrow_mut(); let mut workout = self.imp().workout.borrow_mut();
workout.distance = distance.map(|d| *d); workout.distance = distance.map(|d| *d);
(self.imp().on_update.borrow())(workout.clone()); (self.imp().on_update.borrow())(self.imp().type_.borrow().clone(), workout.clone());
} }
fn update_duration(&self, duration: Option<DurationFormatter>) { fn update_duration(&self, duration: Option<DurationFormatter>) {
let mut workout = self.imp().workout.borrow_mut(); let mut workout = self.imp().workout.borrow_mut();
workout.duration = duration.map(|d| *d); workout.duration = duration.map(|d| *d);
(self.imp().on_update.borrow())(workout.clone()); (self.imp().on_update.borrow())(self.imp().type_.borrow().clone(), workout.clone());
} }
} }

View File

@ -14,7 +14,11 @@ General Public License for more details.
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::types::{FormatOption, WeightFormatter}; use crate::{
components::TextEntry,
types::{FormatOption, WeightFormatter},
};
use dimensioned::si;
use gtk::prelude::*; use gtk::prelude::*;
pub struct WeightLabel { pub struct WeightLabel {

View File

@ -53,7 +53,6 @@ impl Iterator for DayIterator {
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FormatOption { pub enum FormatOption {
Abbreviated, Abbreviated,
#[allow(unused)]
Full, Full,
} }
@ -61,7 +60,6 @@ pub enum FormatOption {
pub struct TimeFormatter(chrono::NaiveTime); pub struct TimeFormatter(chrono::NaiveTime);
impl TimeFormatter { impl TimeFormatter {
#[allow(unused)]
pub fn format(&self, option: FormatOption) -> String { pub fn format(&self, option: FormatOption) -> String {
match option { match option {
FormatOption::Abbreviated => self.0.format("%H:%M"), FormatOption::Abbreviated => self.0.format("%H:%M"),
@ -70,7 +68,6 @@ impl TimeFormatter {
.to_string() .to_string()
} }
#[allow(unused)]
pub fn parse(s: &str) -> Result<TimeFormatter, ParseError> { pub fn parse(s: &str) -> Result<TimeFormatter, ParseError> {
let parts = s let parts = s
.split(':') .split(':')
@ -107,7 +104,6 @@ impl From<chrono::NaiveTime> for TimeFormatter {
pub struct WeightFormatter(si::Kilogram<f64>); pub struct WeightFormatter(si::Kilogram<f64>);
impl WeightFormatter { impl WeightFormatter {
#[allow(unused)]
pub fn format(&self, option: FormatOption) -> String { pub fn format(&self, option: FormatOption) -> String {
match option { match option {
FormatOption::Abbreviated => format!("{} kg", self.0.value_unsafe), FormatOption::Abbreviated => format!("{} kg", self.0.value_unsafe),
@ -115,7 +111,6 @@ impl WeightFormatter {
} }
} }
#[allow(unused)]
pub fn parse(s: &str) -> Result<WeightFormatter, ParseError> { pub fn parse(s: &str) -> Result<WeightFormatter, ParseError> {
s.parse::<f64>() s.parse::<f64>()
.map(|w| WeightFormatter(w * si::KG)) .map(|w| WeightFormatter(w * si::KG))
@ -154,7 +149,6 @@ impl From<si::Kilogram<f64>> for WeightFormatter {
pub struct DistanceFormatter(si::Meter<f64>); pub struct DistanceFormatter(si::Meter<f64>);
impl DistanceFormatter { impl DistanceFormatter {
#[allow(unused)]
pub fn format(&self, option: FormatOption) -> String { pub fn format(&self, option: FormatOption) -> String {
match option { match option {
FormatOption::Abbreviated => format!("{} km", self.0.value_unsafe / 1000.), FormatOption::Abbreviated => format!("{} km", self.0.value_unsafe / 1000.),
@ -162,7 +156,6 @@ impl DistanceFormatter {
} }
} }
#[allow(unused)]
pub fn parse(s: &str) -> Result<DistanceFormatter, ParseError> { pub fn parse(s: &str) -> Result<DistanceFormatter, ParseError> {
let value = s.parse::<f64>().map_err(|_| ParseError)?; let value = s.parse::<f64>().map_err(|_| ParseError)?;
Ok(DistanceFormatter(value * 1000. * si::M)) Ok(DistanceFormatter(value * 1000. * si::M))
@ -200,7 +193,6 @@ impl From<si::Meter<f64>> for DistanceFormatter {
pub struct DurationFormatter(si::Second<f64>); pub struct DurationFormatter(si::Second<f64>);
impl DurationFormatter { impl DurationFormatter {
#[allow(unused)]
pub fn format(&self, option: FormatOption) -> String { pub fn format(&self, option: FormatOption) -> String {
let (hours, minutes) = self.hours_and_minutes(); let (hours, minutes) = self.hours_and_minutes();
let (h, m) = match option { let (h, m) = match option {
@ -214,13 +206,11 @@ impl DurationFormatter {
} }
} }
#[allow(unused)]
pub fn parse(s: &str) -> Result<DurationFormatter, ParseError> { pub fn parse(s: &str) -> Result<DurationFormatter, ParseError> {
let value = s.parse::<f64>().map_err(|_| ParseError)?; let value = s.parse::<f64>().map_err(|_| ParseError)?;
Ok(DurationFormatter(value * 60. * si::S)) Ok(DurationFormatter(value * 60. * si::S))
} }
#[allow(unused)]
fn hours_and_minutes(&self) -> (i64, i64) { fn hours_and_minutes(&self) -> (i64, i64) {
let minutes: i64 = (self.0.value_unsafe / 60.).round() as i64; let minutes: i64 = (self.0.value_unsafe / 60.).round() as i64;
let hours: i64 = minutes / 60; let hours: i64 = minutes / 60;

View File

@ -14,45 +14,42 @@ General Public License for more details.
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::app::{ReadError, RecordProvider}; use crate::{
use dimensioned::si; app::App,
types::{DistanceFormatter, DurationFormatter, WeightFormatter},
};
use emseries::{Record, RecordId, Recordable}; use emseries::{Record, RecordId, Recordable};
use ft_core::{TimeDistance, TimeDistanceActivity, TraxRecord}; use ft_core::{RecordType, TimeDistance, TraxRecord};
use std::{ use std::{
collections::HashMap, collections::HashMap,
ops::Deref, ops::Deref,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
// These are actually a used imports. Clippy isn't detecting their use, probably because of complexity around the async trait macros.
#[allow(unused_imports)]
use crate::app::WriteError;
#[allow(unused_imports)]
use chrono::NaiveDate;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
enum RecordState<T: Clone + Recordable> { enum RecordState<T: Clone + Recordable> {
Original(Record<T>), Original(Record<T>),
New(Record<T>), New(Record<T>),
Updated(Record<T>), Updated(Record<T>),
#[allow(unused)]
Deleted(Record<T>), Deleted(Record<T>),
} }
impl<T: Clone + emseries::Recordable> RecordState<T> { impl<T: Clone + emseries::Recordable> RecordState<T> {
fn exists(&self) -> bool { #[allow(unused)]
fn id(&self) -> Option<&RecordId> {
match self { match self {
RecordState::Original(_) => true, RecordState::Original(ref r) => Some(&r.id),
RecordState::New(_) => true, RecordState::New(ref r) => None,
RecordState::Updated(_) => true, RecordState::Updated(ref r) => Some(&r.id),
RecordState::Deleted(_) => false, RecordState::Deleted(ref r) => Some(&r.id),
} }
} }
#[allow(unused)]
fn data(&self) -> Option<&Record<T>> { fn data(&self) -> Option<&Record<T>> {
match self { match self {
RecordState::Original(ref r) => Some(r), RecordState::Original(ref r) => Some(r),
RecordState::New(ref r) => None, RecordState::New(ref _r) => None,
RecordState::Updated(ref r) => Some(r), RecordState::Updated(ref r) => Some(r),
RecordState::Deleted(ref r) => Some(r), RecordState::Deleted(ref r) => Some(r),
} }
@ -95,20 +92,9 @@ impl<T: Clone + emseries::Recordable> Deref for RecordState<T> {
} }
} }
impl<T: Clone + emseries::Recordable> std::ops::DerefMut for RecordState<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
RecordState::Original(ref mut r) => &mut r.data,
RecordState::New(ref mut r) => &mut r.data,
RecordState::Updated(ref mut r) => &mut r.data,
RecordState::Deleted(ref mut r) => &mut r.data,
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct DayDetailViewModel { pub struct DayDetailViewModel {
provider: Arc<dyn RecordProvider>, app: App,
pub date: chrono::NaiveDate, pub date: chrono::NaiveDate,
weight: Arc<RwLock<Option<RecordState<ft_core::Weight>>>>, weight: Arc<RwLock<Option<RecordState<ft_core::Weight>>>>,
steps: Arc<RwLock<Option<RecordState<ft_core::Steps>>>>, steps: Arc<RwLock<Option<RecordState<ft_core::Steps>>>>,
@ -116,37 +102,37 @@ pub struct DayDetailViewModel {
} }
impl DayDetailViewModel { impl DayDetailViewModel {
pub async fn new( pub async fn new(date: chrono::NaiveDate, app: App) -> Self {
date: chrono::NaiveDate,
provider: impl RecordProvider + 'static,
) -> Result<Self, ReadError> {
let s = Self { let s = Self {
provider: Arc::new(provider), app,
date, date,
weight: Arc::new(RwLock::new(None)), weight: Arc::new(RwLock::new(None)),
steps: Arc::new(RwLock::new(None)), steps: Arc::new(RwLock::new(None)),
records: Arc::new(RwLock::new(HashMap::new())), records: Arc::new(RwLock::new(HashMap::new())),
}; };
s.populate_records().await; s.populate_records().await;
Ok(s) s
} }
pub fn weight(&self) -> Option<si::Kilogram<f64>> { pub fn weight(&self) -> Option<WeightFormatter> {
(*self.weight.read().unwrap()).as_ref().map(|w| w.weight) (*self.weight.read().unwrap())
.as_ref()
.map(|w| WeightFormatter::from(w.weight))
} }
pub fn set_weight(&self, new_weight: si::Kilogram<f64>) { pub fn set_weight(&self, new_weight: WeightFormatter) {
let mut record = self.weight.write().unwrap(); let mut record = self.weight.write().unwrap();
let new_record = match *record { let new_record = match *record {
Some(ref rstate) => rstate.clone().with_value(ft_core::Weight { Some(ref rstate) => rstate.clone().with_value(ft_core::Weight {
date: self.date, date: self.date,
weight: new_weight, weight: *new_weight,
}), }),
None => RecordState::New(Record { None => RecordState::New(Record {
id: RecordId::default(), id: RecordId::default(),
data: ft_core::Weight { data: ft_core::Weight {
date: self.date, date: self.date,
weight: new_weight, weight: *new_weight,
}, },
}), }),
}; };
@ -175,58 +161,40 @@ impl DayDetailViewModel {
*record = Some(new_record); *record = Some(new_record);
} }
pub fn new_time_distance(&self, activity: TimeDistanceActivity) -> Record<TimeDistance> { pub fn biking_summary(&self) -> (DistanceFormatter, DurationFormatter) {
let id = RecordId::default(); self.records.read().unwrap().iter().fold(
let workout = TimeDistance { (DistanceFormatter::default(), DurationFormatter::default()),
datetime: chrono::Local::now().into(), |(acc_distance, acc_duration), (_, record)| match record.data() {
activity, Some(Record {
distance: None, data:
duration: None, TraxRecord::BikeRide(TimeDistance {
comments: None, distance, duration, ..
}),
..
}) => (
distance
.map(|distance| acc_distance + DistanceFormatter::from(distance))
.unwrap_or(acc_distance),
duration
.map(|duration| acc_duration + DurationFormatter::from(duration))
.unwrap_or(acc_duration),
),
_ => (acc_distance, acc_duration),
},
)
}
pub fn new_record(&self, type_: RecordType) -> Record<TraxRecord> {
let new_record = Record {
id: RecordId::default(),
data: ft_core::TraxRecord::new(type_, chrono::Local::now().into()),
}; };
let tr = TraxRecord::from(workout.clone());
self.records self.records
.write() .write()
.unwrap() .unwrap()
.insert(id, RecordState::New(Record { id, data: tr })); .insert(new_record.id, RecordState::New(new_record.clone()));
Record { id, data: workout } new_record
}
pub fn time_distance_records(&self) -> Vec<Record<TimeDistance>> {
self.records
.read()
.unwrap()
.iter()
.filter(|(_, record)| record.exists())
.filter_map(|(id, record_state)| match **record_state {
TraxRecord::TimeDistance(ref workout) => Some(Record {
id: *id,
data: workout.clone(),
}),
_ => None,
})
.collect()
}
pub fn time_distance_summary(
&self,
activity: TimeDistanceActivity,
) -> (si::Meter<f64>, si::Second<f64>) {
self.time_distance_records()
.into_iter()
.filter(|rec| rec.data.activity == activity)
.fold(
(0. * si::M, 0. * si::S),
|(distance, duration), workout| match (workout.data.distance, workout.data.duration)
{
(Some(distance_), Some(duration_)) => {
(distance + distance_, duration + duration_)
}
(Some(distance_), None) => (distance + distance_, duration),
(None, Some(duration_)) => (distance, duration + duration_),
(None, None) => (distance, duration),
},
)
} }
pub fn update_record(&self, update: Record<TraxRecord>) { pub fn update_record(&self, update: Record<TraxRecord>) {
@ -245,108 +213,85 @@ impl DayDetailViewModel {
.collect::<Vec<Record<TraxRecord>>>() .collect::<Vec<Record<TraxRecord>>>()
} }
#[allow(unused)] pub fn save(&self) -> glib::JoinHandle<()> {
fn get_record(&self, id: &RecordId) -> Option<Record<TraxRecord>> { glib::spawn_future({
let record_set = self.records.read().unwrap(); let s = self.clone();
record_set.get(id).map(|record| Record { async move {
id: *id, let weight_record = s.weight.read().unwrap().clone();
data: (**record).clone(), match weight_record {
Some(RecordState::New(Record { data, .. })) => {
let _ = s.app.put_record(TraxRecord::Weight(data)).await;
}
Some(RecordState::Original(_)) => {}
Some(RecordState::Updated(weight)) => {
let _ = s
.app
.update_record(Record {
id: weight.id,
data: TraxRecord::Weight(weight.data),
})
.await;
}
Some(RecordState::Deleted(_)) => {}
None => {}
}
let steps_record = s.steps.read().unwrap().clone();
match steps_record {
Some(RecordState::New(Record { data, .. })) => {
let _ = s.app.put_record(TraxRecord::Steps(data)).await;
}
Some(RecordState::Original(_)) => {}
Some(RecordState::Updated(steps)) => {
let _ = s
.app
.update_record(Record {
id: steps.id,
data: TraxRecord::Steps(steps.data),
})
.await;
}
Some(RecordState::Deleted(_)) => {}
None => {}
}
let records = s
.records
.write()
.unwrap()
.drain()
.map(|(_, record)| record)
.collect::<Vec<RecordState<TraxRecord>>>();
for record in records {
match record {
RecordState::New(Record { data, .. }) => {
let _ = s.app.put_record(data).await;
}
RecordState::Original(_) => {}
RecordState::Updated(r) => {
let _ = s.app.update_record(r.clone()).await;
}
RecordState::Deleted(_) => unimplemented!(),
}
}
s.populate_records().await;
}
}) })
} }
pub fn remove_record(&self, id: RecordId) { pub fn revert(&self) {
let mut record_set = self.records.write().unwrap(); glib::spawn_future({
let updated_record = match record_set.remove(&id) { let s = self.clone();
Some(RecordState::Original(r)) => Some(RecordState::Deleted(r)), async move {
Some(RecordState::New(_)) => None, s.populate_records().await;
Some(RecordState::Updated(r)) => Some(RecordState::Deleted(r)),
Some(RecordState::Deleted(r)) => Some(RecordState::Deleted(r)),
None => None,
};
if let Some(updated_record) = updated_record {
record_set.insert(id, updated_record);
}
}
pub fn save(&self) {
let s = self.clone();
glib::spawn_future(async move { s.async_save().await });
}
pub async fn async_save(&self) {
let weight_record = self.weight.read().unwrap().clone();
match weight_record {
Some(RecordState::New(data)) => {
let _ = self
.provider
.put_record(TraxRecord::Weight(data.data))
.await;
} }
Some(RecordState::Original(_)) => {} });
Some(RecordState::Updated(weight)) => {
let _ = self
.provider
.update_record(Record {
id: weight.id,
data: TraxRecord::Weight(weight.data),
})
.await;
}
Some(RecordState::Deleted(_)) => {}
None => {}
}
let steps_record = self.steps.read().unwrap().clone();
match steps_record {
Some(RecordState::New(data)) => {
let _ = self.provider.put_record(TraxRecord::Steps(data.data)).await;
}
Some(RecordState::Original(_)) => {}
Some(RecordState::Updated(steps)) => {
let _ = self
.provider
.update_record(Record {
id: steps.id,
data: TraxRecord::Steps(steps.data),
})
.await;
}
Some(RecordState::Deleted(_)) => {}
None => {}
}
let records = self
.records
.write()
.unwrap()
.drain()
.map(|(_, record)| record)
.collect::<Vec<RecordState<TraxRecord>>>();
for record in records {
println!("saving record: {:?}", record);
match record {
RecordState::New(data) => {
let _ = self.provider.put_record(data.data).await;
}
RecordState::Original(_) => {}
RecordState::Updated(r) => {
let _ = self.provider.update_record(r.clone()).await;
}
RecordState::Deleted(r) => {
let _ = self.provider.delete_record(r.id).await;
}
}
}
self.populate_records().await;
}
pub async fn revert(&self) {
self.populate_records().await;
} }
async fn populate_records(&self) { async fn populate_records(&self) {
let records = self.provider.records(self.date, self.date).await.unwrap(); let records = self.app.records(self.date, self.date).await.unwrap();
let (weight_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) = let (weight_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
records.into_iter().partition(|r| r.data.is_weight()); records.into_iter().partition(|r| r.data.is_weight());
@ -376,270 +321,30 @@ impl DayDetailViewModel {
} }
} }
#[cfg(test)] /*
mod test { struct SavedRecordIterator<'a> {
use super::*; read_lock: RwLockReadGuard<'a, HashMap<RecordId, RecordState<TraxRecord>>>,
use async_trait::async_trait; iter: Box<dyn Iterator<Item = &'a Record<TraxRecord>> + 'a>,
use chrono::{DateTime, FixedOffset}; }
use dimensioned::si;
use emseries::Record;
#[derive(Clone, Debug)] impl<'a> SavedRecordIterator<'a> {
struct MockProvider { fn new(records: Arc<RwLock<HashMap<RecordId, RecordState<TraxRecord>>>>) -> Self {
records: Arc<RwLock<HashMap<RecordId, Record<TraxRecord>>>>, let read_lock = records.read().unwrap();
let iter = read_lock
put_records: Arc<RwLock<Vec<Record<TraxRecord>>>>, .iter()
updated_records: Arc<RwLock<Vec<Record<TraxRecord>>>>, .map(|(_, record_state)| record_state.data())
deleted_records: Arc<RwLock<Vec<RecordId>>>, .filter_map(|r| r);
} Self {
read_lock,
impl MockProvider { iter: Box::new(iter),
fn new(records: Vec<Record<TraxRecord>>) -> Self {
let record_map = records
.into_iter()
.map(|r| (r.id, r))
.collect::<HashMap<RecordId, Record<TraxRecord>>>();
Self {
records: Arc::new(RwLock::new(record_map)),
put_records: Arc::new(RwLock::new(vec![])),
updated_records: Arc::new(RwLock::new(vec![])),
deleted_records: Arc::new(RwLock::new(vec![])),
}
} }
} }
#[async_trait]
impl RecordProvider for MockProvider {
async fn records(
&self,
start: NaiveDate,
end: NaiveDate,
) -> Result<Vec<Record<TraxRecord>>, ReadError> {
let start = emseries::Timestamp::Date(start);
let end = emseries::Timestamp::Date(end);
Ok(self
.records
.read()
.unwrap()
.iter()
.map(|(_, r)| r)
.filter(|r| r.timestamp() >= start && r.timestamp() <= end)
.cloned()
.collect::<Vec<Record<TraxRecord>>>())
}
async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError> {
let id = RecordId::default();
let record = Record {
id: id,
data: record,
};
self.put_records.write().unwrap().push(record.clone());
self.records.write().unwrap().insert(id, record);
Ok(id)
}
async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError> {
println!("updated record: {:?}", record);
self.updated_records.write().unwrap().push(record.clone());
self.records.write().unwrap().insert(record.id, record);
Ok(())
}
async fn delete_record(&self, id: RecordId) -> Result<(), WriteError> {
self.deleted_records.write().unwrap().push(id);
let _ = self.records.write().unwrap().remove(&id);
Ok(())
}
}
async fn create_empty_view_model() -> (DayDetailViewModel, MockProvider) {
let provider = MockProvider::new(vec![]);
let oct_13 = chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap();
let model = DayDetailViewModel::new(oct_13, provider.clone())
.await
.unwrap();
(model, provider)
}
async fn create_view_model() -> (DayDetailViewModel, MockProvider) {
let oct_12 = chrono::NaiveDate::from_ymd_opt(2023, 10, 12).unwrap();
let oct_13 = chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap();
let oct_13_am: DateTime<FixedOffset> = oct_13
.clone()
.and_hms_opt(3, 28, 0)
.unwrap()
.and_utc()
.with_timezone(&FixedOffset::east_opt(5 * 3600).unwrap());
let provider = MockProvider::new(vec![
Record {
id: RecordId::default(),
data: TraxRecord::Weight(ft_core::Weight {
date: oct_12,
weight: 93. * si::KG,
}),
},
Record {
id: RecordId::default(),
data: TraxRecord::Weight(ft_core::Weight {
date: oct_13,
weight: 95. * si::KG,
}),
},
Record {
id: RecordId::default(),
data: TraxRecord::Steps(ft_core::Steps {
date: oct_13,
count: 2500,
}),
},
Record {
id: RecordId::default(),
data: TraxRecord::TimeDistance(ft_core::TimeDistance {
datetime: oct_13_am.clone(),
activity: TimeDistanceActivity::BikeRide,
distance: Some(15000. * si::M),
duration: Some(3600. * si::S),
comments: Some("somecomments present".to_owned()),
}),
},
]);
let model = DayDetailViewModel::new(oct_13, provider.clone())
.await
.unwrap();
(model, provider)
}
#[tokio::test]
async fn it_honors_only_the_first_weight_and_step_record() {
let (view_model, _provider) = create_view_model().await;
assert_eq!(view_model.weight(), Some(95. * si::KG));
assert_eq!(view_model.steps(), Some(2500));
}
#[tokio::test]
async fn it_can_create_a_weight_and_stepcount() {
let (view_model, provider) = create_empty_view_model().await;
assert_eq!(view_model.weight(), None);
assert_eq!(view_model.steps(), None);
view_model.set_weight(95. * si::KG);
view_model.set_steps(250);
assert_eq!(view_model.weight(), Some(95. * si::KG));
assert_eq!(view_model.steps(), Some(250));
view_model.set_weight(93. * si::KG);
view_model.set_steps(255);
assert_eq!(view_model.weight(), Some(93. * si::KG));
assert_eq!(view_model.steps(), Some(255));
view_model.async_save().await;
println!("provider: {:?}", provider);
assert_eq!(provider.put_records.read().unwrap().len(), 2);
assert_eq!(provider.updated_records.read().unwrap().len(), 0);
assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
}
#[tokio::test]
async fn it_can_construct_new_records() {
let (view_model, provider) = create_empty_view_model().await;
assert_eq!(
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
(0. * si::M, 0. * si::S)
);
let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide);
record.data.duration = Some(60. * si::S);
view_model.async_save().await;
assert_eq!(provider.put_records.read().unwrap().len(), 1);
assert_eq!(provider.updated_records.read().unwrap().len(), 0);
assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
}
#[tokio::test]
async fn it_can_update_a_new_record_before_saving() {
let (view_model, provider) = create_empty_view_model().await;
assert_eq!(
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
(0. * si::M, 0. * si::S)
);
let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide);
record.data.duration = Some(60. * si::S);
let record = record.map(TraxRecord::TimeDistance);
view_model.update_record(record.clone());
assert_eq!(view_model.get_record(&record.id), Some(record));
assert_eq!(
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
(0. * si::M, 60. * si::S)
);
assert_eq!(
view_model.time_distance_summary(TimeDistanceActivity::Running),
(0. * si::M, 0. * si::S)
);
view_model.async_save().await;
assert_eq!(provider.put_records.read().unwrap().len(), 1);
assert_eq!(provider.updated_records.read().unwrap().len(), 0);
assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
}
#[tokio::test]
async fn it_can_update_an_existing_record() {
let (view_model, provider) = create_view_model().await;
let mut workout = view_model.time_distance_records().first().cloned().unwrap();
workout.data.duration = Some(1800. * si::S);
view_model.update_record(workout.map(TraxRecord::TimeDistance));
assert_eq!(
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
(15000. * si::M, 1800. * si::S)
);
view_model.async_save().await;
assert_eq!(provider.put_records.read().unwrap().len(), 0);
assert_eq!(provider.updated_records.read().unwrap().len(), 1);
assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
}
#[tokio::test]
async fn it_can_remove_a_new_record() {
let (view_model, provider) = create_empty_view_model().await;
assert_eq!(
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
(0. * si::M, 0. * si::S)
);
let record = view_model.new_time_distance(TimeDistanceActivity::BikeRide);
view_model.remove_record(record.id);
view_model.save();
assert_eq!(provider.put_records.read().unwrap().len(), 0);
assert_eq!(provider.updated_records.read().unwrap().len(), 0);
assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
}
#[tokio::test]
async fn it_can_delete_an_existing_record() {
let (view_model, provider) = create_view_model().await;
let workout = view_model.time_distance_records().first().cloned().unwrap();
view_model.remove_record(workout.id);
assert_eq!(
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
(0. * si::M, 0. * si::S)
);
view_model.async_save().await;
assert_eq!(provider.put_records.read().unwrap().len(), 0);
assert_eq!(provider.updated_records.read().unwrap().len(), 0);
assert_eq!(provider.deleted_records.read().unwrap().len(), 1);
}
} }
impl<'a> Iterator for SavedRecordIterator<'a> {
type Item = &'a Record<TraxRecord>;
fn next(&mut self) -> Option<Self::Item> {
None
}
}
*/

View File

@ -18,6 +18,8 @@ use crate::{
app::App, components::DaySummary, types::DayInterval, view_models::DayDetailViewModel, app::App, components::DaySummary, types::DayInterval, view_models::DayDetailViewModel,
}; };
use emseries::Record;
use ft_core::TraxRecord;
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
@ -74,9 +76,7 @@ impl ObjectSubclass for HistoricalViewPrivate {
if let Some(app) = app.borrow().clone() { if let Some(app) = app.borrow().clone() {
glib::spawn_future_local(async move { glib::spawn_future_local(async move {
let view_model = DayDetailViewModel::new(date.date(), app.clone()) let view_model = DayDetailViewModel::new(date.date(), app.clone()).await;
.await
.unwrap();
summary.set_data(view_model); summary.set_data(view_model);
}); });
} }
@ -98,7 +98,7 @@ glib::wrapper! {
impl HistoricalView { impl HistoricalView {
pub fn new<SelectFn>(app: App, interval: DayInterval, on_select_day: Rc<SelectFn>) -> Self pub fn new<SelectFn>(app: App, interval: DayInterval, on_select_day: Rc<SelectFn>) -> Self
where where
SelectFn: Fn(chrono::NaiveDate) + 'static, SelectFn: Fn(chrono::NaiveDate, Vec<Record<TraxRecord>>) + 'static,
{ {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
s.set_orientation(gtk::Orientation::Vertical); s.set_orientation(gtk::Orientation::Vertical);
@ -118,7 +118,7 @@ impl HistoricalView {
// This gets triggered whenever the user clicks on an item on the list. // This gets triggered whenever the user clicks on an item on the list.
let item = s.model().unwrap().item(idx).unwrap(); let item = s.model().unwrap().item(idx).unwrap();
let date = item.downcast_ref::<Date>().unwrap(); let date = item.downcast_ref::<Date>().unwrap();
on_select_day(date.date()); on_select_day(date.date(), vec![]);
} }
}); });
@ -127,14 +127,6 @@ impl HistoricalView {
s s
} }
pub fn set_interval(&self, interval: DayInterval) {
let mut model = gio::ListStore::new::<Date>();
model.extend(interval.days().map(Date::new));
self.imp()
.list_view
.set_model(Some(&gtk::NoSelection::new(Some(model))));
}
pub fn time_window(&self) -> DayInterval { pub fn time_window(&self) -> DayInterval {
self.imp().time_window.borrow().clone() self.imp().time_window.borrow().clone()
} }

View File

@ -30,7 +30,6 @@ pub use welcome_view::WelcomeView;
pub enum View { pub enum View {
Placeholder(PlaceholderView), Placeholder(PlaceholderView),
#[allow(unused)]
Welcome(WelcomeView), Welcome(WelcomeView),
Historical(HistoricalView), Historical(HistoricalView),
} }

View File

@ -1,4 +1,4 @@
mod legacy; mod legacy;
mod types; mod types;
pub use types::{Steps, TimeDistance, TimeDistanceActivity, TraxRecord, Weight}; pub use types::{RecordType, Steps, TimeDistance, TraxRecord, Weight};

View File

@ -33,15 +33,6 @@ impl Recordable for Steps {
} }
} }
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum TimeDistanceActivity {
BikeRide,
Running,
Rowing,
Swimming,
Walking,
}
/// TimeDistance represents workouts characterized by a duration and a distance travelled. These /// TimeDistance represents workouts characterized by a duration and a distance travelled. These
/// sorts of workouts can occur many times a day, depending on how one records things. I might /// sorts of workouts can occur many times a day, depending on how one records things. I might
/// record a single 30-km workout if I go on a long-distanec ride. Or I might record multiple 5km /// record a single 30-km workout if I go on a long-distanec ride. Or I might record multiple 5km
@ -57,8 +48,6 @@ pub struct TimeDistance {
/// in the database, but we can still get a Naive Date from the DateTime, which will still read /// in the database, but we can still get a Naive Date from the DateTime, which will still read
/// as the original day. /// as the original day.
pub datetime: DateTime<FixedOffset>, pub datetime: DateTime<FixedOffset>,
/// The activity
pub activity: TimeDistanceActivity,
/// The distance travelled. This is optional because such a workout makes sense even without /// The distance travelled. This is optional because such a workout makes sense even without
/// the distance. /// the distance.
pub distance: Option<si::Meter<f64>>, pub distance: Option<si::Meter<f64>>,
@ -68,13 +57,14 @@ pub struct TimeDistance {
pub comments: Option<String>, pub comments: Option<String>,
} }
impl Recordable for TimeDistance { impl TimeDistance {
fn timestamp(&self) -> Timestamp { pub fn new(time: DateTime<FixedOffset>) -> Self {
Timestamp::DateTime(self.datetime) Self {
} datetime: time,
distance: None,
fn tags(&self) -> Vec<String> { duration: None,
vec![] comments: None,
}
} }
} }
@ -96,15 +86,54 @@ impl Recordable for Weight {
} }
} }
#[derive(Clone, Debug, PartialEq)]
pub enum RecordType {
BikeRide,
Row,
Run,
Steps,
Swim,
Walk,
Weight,
}
/// The unified data structure for all records that are part of the app. /// The unified data structure for all records that are part of the app.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum TraxRecord { pub enum TraxRecord {
TimeDistance(TimeDistance), BikeRide(TimeDistance),
Row(TimeDistance),
Run(TimeDistance),
Steps(Steps), Steps(Steps),
Swim(TimeDistance),
Walk(TimeDistance),
Weight(Weight), Weight(Weight),
} }
impl TraxRecord { impl TraxRecord {
pub fn new(type_: RecordType, time: DateTime<FixedOffset>) -> TraxRecord {
match type_ {
RecordType::BikeRide => TraxRecord::BikeRide(TimeDistance::new(time)),
RecordType::Row => TraxRecord::Row(TimeDistance::new(time)),
RecordType::Run => TraxRecord::Run(TimeDistance::new(time)),
RecordType::Steps => unimplemented!(),
RecordType::Swim => unimplemented!(),
RecordType::Walk => TraxRecord::Walk(TimeDistance::new(time)),
RecordType::Weight => unimplemented!(),
}
}
pub fn workout_type(&self) -> RecordType {
match self {
TraxRecord::BikeRide(_) => RecordType::BikeRide,
TraxRecord::Row(_) => RecordType::Row,
TraxRecord::Run(_) => RecordType::Run,
TraxRecord::Steps(_) => RecordType::Steps,
TraxRecord::Swim(_) => RecordType::Swim,
TraxRecord::Walk(_) => RecordType::Walk,
TraxRecord::Weight(_) => RecordType::Weight,
}
}
pub fn is_weight(&self) -> bool { pub fn is_weight(&self) -> bool {
matches!(self, TraxRecord::Weight(_)) matches!(self, TraxRecord::Weight(_))
} }
@ -113,34 +142,24 @@ impl TraxRecord {
matches!(self, TraxRecord::Steps(_)) matches!(self, TraxRecord::Steps(_))
} }
pub fn is_time_distance(&self) -> bool { pub fn is_bike_ride(&self) -> bool {
matches!( matches!(self, TraxRecord::BikeRide(_))
self, }
TraxRecord::TimeDistance(TimeDistance {
activity: TimeDistanceActivity::BikeRide, pub fn is_run(&self) -> bool {
.. matches!(self, TraxRecord::Run(_))
}) | TraxRecord::TimeDistance(TimeDistance {
activity: TimeDistanceActivity::Running,
..
}) | TraxRecord::TimeDistance(TimeDistance {
activity: TimeDistanceActivity::Rowing,
..
}) | TraxRecord::TimeDistance(TimeDistance {
activity: TimeDistanceActivity::Swimming,
..
}) | TraxRecord::TimeDistance(TimeDistance {
activity: TimeDistanceActivity::Walking,
..
})
)
} }
} }
impl Recordable for TraxRecord { impl Recordable for TraxRecord {
fn timestamp(&self) -> Timestamp { fn timestamp(&self) -> Timestamp {
match self { match self {
TraxRecord::TimeDistance(rec) => Timestamp::DateTime(rec.datetime), TraxRecord::BikeRide(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Row(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Run(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Steps(rec) => rec.timestamp(), TraxRecord::Steps(rec) => rec.timestamp(),
TraxRecord::Swim(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Walk(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Weight(rec) => rec.timestamp(), TraxRecord::Weight(rec) => rec.timestamp(),
} }
} }
@ -150,12 +169,6 @@ impl Recordable for TraxRecord {
} }
} }
impl From<TimeDistance> for TraxRecord {
fn from(td: TimeDistance) -> Self {
Self::TimeDistance(td)
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;