Compare commits
14 Commits
ea867812bc
...
31d74588fb
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | 31d74588fb | |
Savanni D'Gerinel | 07f487b139 | |
Savanni D'Gerinel | 79d705c1d0 | |
Savanni D'Gerinel | 2d476f266c | |
Savanni D'Gerinel | 798fbff320 | |
Savanni D'Gerinel | 6e26923629 | |
Savanni D'Gerinel | 4fdf390ecf | |
Savanni D'Gerinel | 29b1e6054b | |
Savanni D'Gerinel | 0f5af82cb5 | |
Savanni D'Gerinel | 525cc88c25 | |
Savanni D'Gerinel | 06d118060e | |
Savanni D'Gerinel | 251077b0c1 | |
Savanni D'Gerinel | ce8bed13f9 | |
Savanni D'Gerinel | 279810f7d7 |
|
@ -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]]
|
||||||
|
|
70
Cargo.nix
70
Cargo.nix
|
@ -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";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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" ] }
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(¤t_widget.widget());
|
self.layout.append(¤t_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();
|
||||||
|
|
|
@ -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,15 +230,24 @@ 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();
|
||||||
|
match record.data {
|
||||||
|
TraxRecord::BikeRide(workout)
|
||||||
|
| TraxRecord::Row(workout)
|
||||||
|
| TraxRecord::Run(workout)
|
||||||
|
| TraxRecord::Swim(workout)
|
||||||
|
| TraxRecord::Walk(workout) => {
|
||||||
|
Some(TimeDistanceEdit::new(workout_type, workout, {
|
||||||
let s = s.clone();
|
let s = s.clone();
|
||||||
move |data| {
|
move |type_, data| {
|
||||||
s.update_workout(record.id, 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,47 +278,55 @@ 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),
|
||||||
|
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);
|
view_model.update_record(record);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup {
|
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();
|
|
||||||
glib::spawn_future_local(async move {
|
|
||||||
view_model.revert().await;
|
|
||||||
s.finish();
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
>k::Label::builder()
|
>k::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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,47 +213,19 @@ 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();
|
|
||||||
record_set.get(id).map(|record| Record {
|
|
||||||
id: *id,
|
|
||||||
data: (**record).clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_record(&self, id: RecordId) {
|
|
||||||
let mut record_set = self.records.write().unwrap();
|
|
||||||
let updated_record = match record_set.remove(&id) {
|
|
||||||
Some(RecordState::Original(r)) => Some(RecordState::Deleted(r)),
|
|
||||||
Some(RecordState::New(_)) => None,
|
|
||||||
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();
|
let s = self.clone();
|
||||||
glib::spawn_future(async move { s.async_save().await });
|
async move {
|
||||||
}
|
let weight_record = s.weight.read().unwrap().clone();
|
||||||
|
|
||||||
pub async fn async_save(&self) {
|
|
||||||
let weight_record = self.weight.read().unwrap().clone();
|
|
||||||
match weight_record {
|
match weight_record {
|
||||||
Some(RecordState::New(data)) => {
|
Some(RecordState::New(Record { data, .. })) => {
|
||||||
let _ = self
|
let _ = s.app.put_record(TraxRecord::Weight(data)).await;
|
||||||
.provider
|
|
||||||
.put_record(TraxRecord::Weight(data.data))
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
Some(RecordState::Original(_)) => {}
|
Some(RecordState::Original(_)) => {}
|
||||||
Some(RecordState::Updated(weight)) => {
|
Some(RecordState::Updated(weight)) => {
|
||||||
let _ = self
|
let _ = s
|
||||||
.provider
|
.app
|
||||||
.update_record(Record {
|
.update_record(Record {
|
||||||
id: weight.id,
|
id: weight.id,
|
||||||
data: TraxRecord::Weight(weight.data),
|
data: TraxRecord::Weight(weight.data),
|
||||||
|
@ -296,15 +236,15 @@ impl DayDetailViewModel {
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let steps_record = self.steps.read().unwrap().clone();
|
let steps_record = s.steps.read().unwrap().clone();
|
||||||
match steps_record {
|
match steps_record {
|
||||||
Some(RecordState::New(data)) => {
|
Some(RecordState::New(Record { data, .. })) => {
|
||||||
let _ = self.provider.put_record(TraxRecord::Steps(data.data)).await;
|
let _ = s.app.put_record(TraxRecord::Steps(data)).await;
|
||||||
}
|
}
|
||||||
Some(RecordState::Original(_)) => {}
|
Some(RecordState::Original(_)) => {}
|
||||||
Some(RecordState::Updated(steps)) => {
|
Some(RecordState::Updated(steps)) => {
|
||||||
let _ = self
|
let _ = s
|
||||||
.provider
|
.app
|
||||||
.update_record(Record {
|
.update_record(Record {
|
||||||
id: steps.id,
|
id: steps.id,
|
||||||
data: TraxRecord::Steps(steps.data),
|
data: TraxRecord::Steps(steps.data),
|
||||||
|
@ -315,7 +255,7 @@ impl DayDetailViewModel {
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let records = self
|
let records = s
|
||||||
.records
|
.records
|
||||||
.write()
|
.write()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -324,29 +264,34 @@ impl DayDetailViewModel {
|
||||||
.collect::<Vec<RecordState<TraxRecord>>>();
|
.collect::<Vec<RecordState<TraxRecord>>>();
|
||||||
|
|
||||||
for record in records {
|
for record in records {
|
||||||
println!("saving record: {:?}", record);
|
|
||||||
match record {
|
match record {
|
||||||
RecordState::New(data) => {
|
RecordState::New(Record { data, .. }) => {
|
||||||
let _ = self.provider.put_record(data.data).await;
|
let _ = s.app.put_record(data).await;
|
||||||
}
|
}
|
||||||
RecordState::Original(_) => {}
|
RecordState::Original(_) => {}
|
||||||
RecordState::Updated(r) => {
|
RecordState::Updated(r) => {
|
||||||
let _ = self.provider.update_record(r.clone()).await;
|
let _ = s.app.update_record(r.clone()).await;
|
||||||
}
|
}
|
||||||
RecordState::Deleted(r) => {
|
RecordState::Deleted(_) => unimplemented!(),
|
||||||
let _ = self.provider.delete_record(r.id).await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
self.populate_records().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn revert(&self) {
|
s.populate_records().await;
|
||||||
self.populate_records().await;
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn revert(&self) {
|
||||||
|
glib::spawn_future({
|
||||||
|
let s = self.clone();
|
||||||
|
async move {
|
||||||
|
s.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>>>>,
|
|
||||||
updated_records: Arc<RwLock<Vec<Record<TraxRecord>>>>,
|
|
||||||
deleted_records: Arc<RwLock<Vec<RecordId>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MockProvider {
|
|
||||||
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()
|
.iter()
|
||||||
.map(|(_, r)| r)
|
.map(|(_, record_state)| record_state.data())
|
||||||
.filter(|r| r.timestamp() >= start && r.timestamp() <= end)
|
.filter_map(|r| r);
|
||||||
.cloned()
|
Self {
|
||||||
.collect::<Vec<Record<TraxRecord>>>())
|
read_lock,
|
||||||
|
iter: Box::new(iter),
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
|
@ -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(>k::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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -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,
|
||||||
|
duration: None,
|
||||||
|
comments: None,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tags(&self) -> Vec<String> {
|
|
||||||
vec![]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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::*;
|
||||||
|
|
Loading…
Reference in New Issue