Compare commits

...

16 Commits

Author SHA1 Message Date
Savanni D'Gerinel f8d66bbb69 Update build tools for dashboard and fitnesstrax 2024-01-25 22:55:45 -05:00
Savanni D'Gerinel dce11dde2b Set up flake-based builds 2024-01-25 22:48:41 -05:00
Savanni D'Gerinel 3f9a7072eb Fitnesstrax, version 0.3.0 2024-01-21 10:14:50 -05:00
Savanni D'Gerinel 7ec48ded5d Make the day summary use the view model 2024-01-20 17:04:20 -05:00
Savanni D'Gerinel 9461c387fe Simplify the weight editor 2024-01-20 16:05:33 -05:00
Savanni D'Gerinel d4c48c4443 Add a step count editor field 2024-01-20 15:59:03 -05:00
Savanni D'Gerinel 9bedb7a76c Tons of linting and get tests running again 2024-01-20 15:04:46 -05:00
Savanni D'Gerinel 1fe318068b Set up a view model for the day detail view 2024-01-20 11:16:31 -05:00
Savanni D'Gerinel 18e7e4fe2f Start setting up the day detail view model
I've created the view model and added a getter function for the weight.
I'm passing the view model now to the DayDetailView, DayDetail, and
DayEdit.

I'm starting to set up the Save function for the view model, draining
all of the updated records and saving them.

None of the components yet save any updates to the view model, so
updated_records is always going to be empty until I figure that out.
2024-01-18 09:00:08 -05:00
Savanni D'Gerinel 1c2c4982a1 Update the record in the detail view on save 2024-01-18 07:43:18 -05:00
Savanni D'Gerinel c075b7ed6e Just barely get the data saveable again
Starting to see some pretty serious limitations already.
2024-01-17 22:59:20 -05:00
Savanni D'Gerinel 56d0a53666 Fix how DayEdit deals with the weight field 2024-01-17 22:35:13 -05:00
Savanni D'Gerinel b00acc64a3 Set up the ActionGroup component 2024-01-17 22:13:55 -05:00
Savanni D'Gerinel 104760c754 Be able to switch into edit mode 2024-01-15 23:27:55 -05:00
Savanni D'Gerinel 1e6555ef61 Create a day detail view
DayDetail, the component, I used to use as a view. Now I'm swapping
things out so that DayDetailView handles the view itself. The DayDetail
component will still show the details of the day, but I'll create a
DayEditComponent which is dedicated to showing the edit interface for
everything in a day.

The swapping will now happen in DayDetailView, not in DayDetail or an
even deeper component.
2024-01-15 15:53:01 -05:00
Savanni D'Gerinel 2e2ff6b47e Create a Singleton component and use it to simplify the weight view 2024-01-15 13:20:23 -05:00
36 changed files with 1056 additions and 1734 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"rust-analyzer.showUnlinkedFileNotification": false
}

360
Cargo.lock generated
View File

@ -120,12 +120,6 @@ version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "arrayvec"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]] [[package]]
name = "async-channel" name = "async-channel"
version = "2.1.1" version = "2.1.1"
@ -203,15 +197,6 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "bare-metal"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3"
dependencies = [
"rustc_version 0.2.3",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.9.3" version = "0.9.3"
@ -255,12 +240,6 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
[[package]]
name = "bitfield"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -549,38 +528,6 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "cortex-m"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9"
dependencies = [
"bare-metal",
"bitfield",
"embedded-hal",
"volatile-register",
]
[[package]]
name = "cortex-m-rt"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee84e813d593101b1723e13ec38b6ab6abbdbaaa4546553f5395ed274079ddb1"
dependencies = [
"cortex-m-rt-macros",
]
[[package]]
name = "cortex-m-rt-macros"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f6f3e36f203cfedbc78b357fb28730aa2c6dc1ab060ee5c2405e843988d3c7"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.11" version = "0.2.11"
@ -599,15 +546,6 @@ dependencies = [
"crc-catalog", "crc-catalog",
] ]
[[package]]
name = "crc-any"
version = "2.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "774646b687f63643eb0f4bf13dc263cb581c8c9e57973b6ddf78bda3994d88df"
dependencies = [
"debug-helper",
]
[[package]] [[package]]
name = "crc-catalog" name = "crc-catalog"
version = "2.4.0" version = "2.4.0"
@ -623,12 +561,6 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "critical-section"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216"
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.4" version = "0.8.4"
@ -699,7 +631,7 @@ dependencies = [
[[package]] [[package]]
name = "dashboard" name = "dashboard"
version = "0.1.1" version = "0.1.2"
dependencies = [ dependencies = [
"cairo-rs", "cairo-rs",
"chrono", "chrono",
@ -710,7 +642,7 @@ dependencies = [
"geo-types", "geo-types",
"gio", "gio",
"glib", "glib",
"glib-build-tools 0.16.3", "glib-build-tools 0.18.0",
"gtk4", "gtk4",
"ifc", "ifc",
"lazy_static", "lazy_static",
@ -730,12 +662,6 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "debug-helper"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e"
[[package]] [[package]]
name = "deflate" name = "deflate"
version = "0.8.6" version = "0.8.6"
@ -828,25 +754,6 @@ dependencies = [
"serde 1.0.193", "serde 1.0.193",
] ]
[[package]]
name = "embedded-dma"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "994f7e5b5cb23521c22304927195f236813053eb9c065dd2226a32ba64695446"
dependencies = [
"stable_deref_trait",
]
[[package]]
name = "embedded-hal"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff"
dependencies = [
"nb 0.1.3",
"void",
]
[[package]] [[package]]
name = "emseries" name = "emseries"
version = "0.6.0" version = "0.6.0"
@ -976,7 +883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
dependencies = [ dependencies = [
"memoffset", "memoffset",
"rustc_version 0.4.0", "rustc_version",
] ]
[[package]] [[package]]
@ -1018,7 +925,7 @@ checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
[[package]] [[package]]
name = "fitnesstrax" name = "fitnesstrax"
version = "0.2.0" version = "0.3.0"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"chrono", "chrono",
@ -1031,6 +938,7 @@ dependencies = [
"glib-build-tools 0.18.0", "glib-build-tools 0.18.0",
"gtk4", "gtk4",
"libadwaita", "libadwaita",
"thiserror",
"tokio", "tokio",
] ]
@ -1139,45 +1047,6 @@ dependencies = [
"percent-encoding 2.3.1", "percent-encoding 2.3.1",
] ]
[[package]]
name = "frunk"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11a351b59e12f97b4176ee78497dff72e4276fb1ceb13e19056aca7fa0206287"
dependencies = [
"frunk_core",
"frunk_derives",
]
[[package]]
name = "frunk_core"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af2469fab0bd07e64ccf0ad57a1438f63160c69b2e57f04a439653d68eb558d6"
[[package]]
name = "frunk_derives"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fa992f1656e1707946bbba340ad244f0814009ef8c0118eb7b658395f19a2e"
dependencies = [
"frunk_proc_macro_helpers",
"quote",
"syn 2.0.41",
]
[[package]]
name = "frunk_proc_macro_helpers"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35b54add839292b743aeda6ebedbd8b11e93404f902c56223e51b9ec18a13d2c"
dependencies = [
"frunk_core",
"proc-macro2",
"quote",
"syn 2.0.41",
]
[[package]] [[package]]
name = "ft-core" name = "ft-core"
version = "0.1.0" version = "0.1.0"
@ -1196,15 +1065,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
name = "fugit"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17186ad64927d5ac8f02c1e77ccefa08ccd9eaa314d5a4772278aa204a22f7e7"
dependencies = [
"gcd",
]
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.29" version = "0.3.29"
@ -1305,12 +1165,6 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "gcd"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a"
[[package]] [[package]]
name = "gdk-pixbuf" name = "gdk-pixbuf"
version = "0.18.3" version = "0.18.3"
@ -2076,15 +1930,6 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.12.0" version = "0.12.0"
@ -2445,21 +2290,6 @@ dependencies = [
"tempfile", "tempfile",
] ]
[[package]]
name = "nb"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f"
dependencies = [
"nb 1.1.0",
]
[[package]]
name = "nb"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d"
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.27.1" version = "0.27.1"
@ -2575,26 +2405,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "num_enum"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.32.1" version = "0.32.1"
@ -2679,12 +2489,6 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "panic-halt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de96540e0ebde571dc55c73d60ef407c653844e6f9a1e2fdbd40c07b9252d812"
[[package]] [[package]]
name = "parking" name = "parking"
version = "2.2.0" version = "2.2.0"
@ -2827,18 +2631,6 @@ dependencies = [
"siphasher 0.3.11", "siphasher 0.3.11",
] ]
[[package]]
name = "pico-blink"
version = "0.1.0"
dependencies = [
"cortex-m",
"cortex-m-rt",
"embedded-hal",
"fugit",
"panic-halt",
"rp-pico",
]
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.3" version = "1.1.3"
@ -2871,17 +2663,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pio"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76e09694b50f89f302ed531c1f2a7569f0be5867aee4ab4f8f729bbeec0078e3"
dependencies = [
"arrayvec",
"num_enum",
"paste",
]
[[package]] [[package]]
name = "pkcs1" name = "pkcs1"
version = "0.7.5" version = "0.7.5"
@ -3351,76 +3132,6 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "rp-pico"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6341771e6f8e5d130b2b3cbc23435b7847761adf198af09f4b2a60407d43bd56"
dependencies = [
"cortex-m-rt",
"fugit",
"rp2040-boot2",
"rp2040-hal",
"usb-device",
]
[[package]]
name = "rp2040-boot2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c92f344f63f950ee36cf4080050e4dce850839b9175da38f9d2ffb69b4dbb21"
dependencies = [
"crc-any",
]
[[package]]
name = "rp2040-hal"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f21deadb5f29f05be9e665049c9c6d6385c6ed3437574c995658a041f71453"
dependencies = [
"cortex-m",
"critical-section",
"embedded-dma",
"embedded-hal",
"frunk",
"fugit",
"itertools 0.10.5",
"nb 1.1.0",
"paste",
"pio",
"rand_core 0.6.4",
"rp2040-hal-macros",
"rp2040-pac",
"usb-device",
"vcell",
"void",
]
[[package]]
name = "rp2040-hal-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86479063e497efe1ae81995ef9071f54fd1c7427e04d6c5b84cde545ff672a5e"
dependencies = [
"cortex-m-rt",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "rp2040-pac"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12d9d8375815f543f54835d01160d4e47f9e2cae75f17ff8f1ec19ce1da96e4c"
dependencies = [
"cortex-m",
"cortex-m-rt",
"critical-section",
"vcell",
]
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.6" version = "0.9.6"
@ -3453,22 +3164,13 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
dependencies = [
"semver 0.9.0",
]
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [ dependencies = [
"semver 1.0.20", "semver",
] ]
[[package]] [[package]]
@ -3590,27 +3292,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e388332cd64eb80cd595a00941baf513caffae8dce9cfd0467fc9c66397dade6" checksum = "e388332cd64eb80cd595a00941baf513caffae8dce9cfd0467fc9c66397dade6"
[[package]]
name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser",
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.20" version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
[[package]]
name = "semver-parser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]] [[package]]
name = "serde" name = "serde"
version = "0.9.15" version = "0.9.15"
@ -3796,7 +3483,7 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c"
dependencies = [ dependencies = [
"itertools 0.12.0", "itertools",
"nom", "nom",
"unicode_categories", "unicode_categories",
] ]
@ -3997,12 +3684,6 @@ dependencies = [
"urlencoding", "urlencoding",
] ]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]] [[package]]
name = "stringprep" name = "stringprep"
version = "0.1.4" version = "0.1.4"
@ -4593,12 +4274,6 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "usb-device"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f6cc3adc849b5292b4075fc0d5fdcf2f24866e88e336dd27a8943090a520508"
[[package]] [[package]]
name = "utf-8" name = "utf-8"
version = "0.7.6" version = "0.7.6"
@ -4640,12 +4315,6 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "vcell"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"
@ -4682,21 +4351,6 @@ dependencies = [
"warp", "warp",
] ]
[[package]]
name = "void"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]]
name = "volatile-register"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc"
dependencies = [
"vcell",
]
[[package]] [[package]]
name = "wait-timeout" name = "wait-timeout"
version = "0.2.0" version = "0.2.0"

889
Cargo.nix

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,6 @@ members = [
"kifu/gtk", "kifu/gtk",
"memorycache", "memorycache",
"nom-training", "nom-training",
"pico-blink",
"result-extended", "result-extended",
"screenplay", "screenplay",
"sgf", "sgf",

View File

@ -28,5 +28,5 @@ tokio = { version = "1", features = ["full"] }
unic-langid = { version = "0.9" } unic-langid = { version = "0.9" }
[build-dependencies] [build-dependencies]
glib-build-tools = "0.16" glib-build-tools = "0.18"

View File

@ -1,7 +1,7 @@
fn main() { fn main() {
glib_build_tools::compile_resources( glib_build_tools::compile_resources(
"resources", &["resources"],
"resources/gresources.xml", "gresources.xml",
"com.luminescent-dreams.dashboard.gresource", "com.luminescent-dreams.dashboard.gresource",
); );
} }

View File

@ -108,7 +108,7 @@ where
Ok(line_) => { Ok(line_) => {
match serde_json::from_str::<RecordOnDisk<T>>(line_.as_ref()) match serde_json::from_str::<RecordOnDisk<T>>(line_.as_ref())
.map_err(EmseriesReadError::JSONParseError) .map_err(EmseriesReadError::JSONParseError)
.and_then(|record| Record::try_from(record)) .and_then(Record::try_from)
{ {
Ok(record) => records.insert(record.id.clone(), record.clone()), Ok(record) => records.insert(record.id.clone(), record.clone()),
Err(EmseriesReadError::RecordDeleted(id)) => records.remove(&id), Err(EmseriesReadError::RecordDeleted(id)) => records.remove(&id),

View File

@ -11,9 +11,6 @@ You should have received a copy of the GNU General Public License along with Lum
*/ */
use chrono::{DateTime, FixedOffset, NaiveDate}; use chrono::{DateTime, FixedOffset, NaiveDate};
use chrono_tz::UTC;
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use std::{cmp::Ordering, fmt, io, str}; use std::{cmp::Ordering, fmt, io, str};
use thiserror::Error; use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
@ -93,33 +90,9 @@ impl str::FromStr for Timestamp {
} }
} }
/*
impl PartialEq for Timestamp {
fn eq(&self, other: &Timestamp) -> bool {
match (self, other) {
(Timestamp::DateTime(dt1), Timestamp::DateTime(dt2)) => {
dt1.with_timezone(&UTC) == dt2.with_timezone(&UTC)
}
// It's not clear to me what would make sense when I'm comparing a date and a
// timestamp. I'm going with a naive date comparison on the idea that what I'm wanting
// here is human scale, again.
(Timestamp::DateTime(dt1), Timestamp::Date(dt2)) => dt1.date_naive() == *dt2,
(Timestamp::Date(dt1), Timestamp::DateTime(dt2)) => *dt1 == dt2.date_naive(),
(Timestamp::Date(dt1), Timestamp::Date(dt2)) => *dt1 == *dt2,
}
}
}
*/
impl PartialOrd for Timestamp { impl PartialOrd for Timestamp {
fn partial_cmp(&self, other: &Timestamp) -> Option<Ordering> { fn partial_cmp(&self, other: &Timestamp) -> Option<Ordering> {
// Some(self.cmp(other)) Some(self.cmp(other))
match (self, other) {
(Timestamp::DateTime(dt1), Timestamp::DateTime(dt2)) => dt1.partial_cmp(dt2),
(Timestamp::DateTime(dt1), Timestamp::Date(dt2)) => dt1.date_naive().partial_cmp(dt2),
(Timestamp::Date(dt1), Timestamp::DateTime(dt2)) => dt1.partial_cmp(&dt2.date_naive()),
(Timestamp::Date(dt1), Timestamp::Date(dt2)) => dt1.partial_cmp(dt2),
}
} }
} }

View File

@ -1,6 +1,6 @@
[package] [package]
name = "fitnesstrax" name = "fitnesstrax"
version = "0.2.0" version = "0.3.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -16,6 +16,7 @@ ft-core = { path = "../core" }
gio = { version = "0.18" } gio = { version = "0.18" }
glib = { version = "0.18" } glib = { version = "0.18" }
gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] } gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] }
thiserror = { version = "1.0" }
tokio = { version = "1.34", features = [ "full" ] } tokio = { version = "1.34", features = [ "full" ] }
[build-dependencies] [build-dependencies]

View File

@ -1,7 +1,7 @@
fn main() { fn main() {
glib_build_tools::compile_resources( glib_build_tools::compile_resources(
&["resources"], &["resources"],
"resources/gresources.xml", "gresources.xml",
"com.luminescent-dreams.fitnesstrax.gresource", "com.luminescent-dreams.fitnesstrax.gresource",
); );
} }

View File

@ -11,8 +11,7 @@
padding: 8px; padding: 8px;
} }
.welcome__footer { .welcome__footer {}
}
.historical { .historical {
margin: 32px; margin: 32px;
@ -37,3 +36,7 @@
margin: 8px; margin: 8px;
} }
.step-view {
padding: 8px;
margin: 8px;
}

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 crate::types::DayInterval;
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;
@ -22,11 +21,16 @@ use std::{
path::PathBuf, path::PathBuf,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
use thiserror::Error;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
#[derive(Debug, Error)]
pub enum AppError { pub enum AppError {
#[error("no database loaded")]
NoDatabase, NoDatabase,
#[error("failed to open the database")]
FailedToOpenDatabase, FailedToOpenDatabase,
#[error("unhandled error")]
Unhandled, Unhandled,
} }
@ -47,12 +51,10 @@ impl App {
.unwrap(), .unwrap(),
); );
let s = Self { Self {
runtime, runtime,
database: Arc::new(RwLock::new(database)), database: Arc::new(RwLock::new(database)),
}; }
s
} }
pub async fn records( pub async fn records(
@ -71,7 +73,7 @@ impl App {
Timestamp::Date(end), Timestamp::Date(end),
true, true,
)) ))
.map(|record| record.clone()) .cloned()
.collect::<Vec<Record<TraxRecord>>>(); .collect::<Vec<Record<TraxRecord>>>();
Ok(records) Ok(records)
} else { } else {

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com> Copyright 2023-2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of FitnessTrax. This file is part of FitnessTrax.
@ -16,8 +16,8 @@ You should have received a copy of the GNU General Public License along with Fit
use crate::{ use crate::{
app::App, app::App,
components::DayDetail, view_models::DayDetailViewModel,
views::{HistoricalView, PlaceholderView, View, WelcomeView}, views::{DayDetailView, HistoricalView, PlaceholderView, View, WelcomeView},
}; };
use adw::prelude::*; use adw::prelude::*;
use chrono::{Duration, Local}; use chrono::{Duration, Local};
@ -81,7 +81,7 @@ impl AppWindow {
.orientation(gtk::Orientation::Vertical) .orientation(gtk::Orientation::Vertical)
.build(); .build();
let initial_view = View::Placeholder(PlaceholderView::new().upcast()); let initial_view = View::Placeholder(PlaceholderView::default().upcast());
layout.append(&initial_view.widget()); layout.append(&initial_view.widget());
@ -115,9 +115,10 @@ impl AppWindow {
s.navigation.connect_popped({ s.navigation.connect_popped({
let s = s.clone(); let s = s.clone();
move |_, _| match *s.current_view.borrow() { move |_, _| {
View::Historical(_) => s.load_records(), if let View::Historical(_) = *s.current_view.borrow() {
_ => {} s.load_records();
}
} }
}); });
@ -133,23 +134,17 @@ impl AppWindow {
} }
fn show_historical_view(&self, records: Vec<Record<TraxRecord>>) { fn show_historical_view(&self, records: Vec<Record<TraxRecord>>) {
let view = View::Historical(HistoricalView::new(records, { let view = View::Historical(HistoricalView::new(self.app.clone(), records, {
let s = self.clone(); let s = self.clone();
Rc::new(move |date, records| { Rc::new(move |date, records| {
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(&DayDetail::new( // layout.append(&DayDetailView::new(date, records, s.app.clone()));
layout.append(&DayDetailView::new(DayDetailViewModel::new(
date, date,
records, records,
{ s.app.clone(),
let s = s.clone(); )));
move |record| s.on_put_record(record)
},
{
let s = s.clone();
move |record| s.on_update_record(record)
},
));
let page = &adw::NavigationPage::builder() let page = &adw::NavigationPage::builder()
.title(date.format("%Y-%m-%d").to_string()) .title(date.format("%Y-%m-%d").to_string())
.child(&layout) .child(&layout)
@ -197,22 +192,4 @@ impl AppWindow {
} }
}); });
} }
fn on_put_record(&self, record: TraxRecord) {
glib::spawn_future_local({
let s = self.clone();
async move {
s.app.put_record(record).await;
}
});
}
fn on_update_record(&self, record: Record<TraxRecord>) {
glib::spawn_future_local({
let s = self.clone();
async move {
s.app.update_record(record).await;
}
});
}
} }

View File

@ -0,0 +1,137 @@
/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of FitnessTrax.
FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/
//! ActionGroup and related structures
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
#[derive(Default)]
pub struct ActionGroupPrivate;
#[glib::object_subclass]
impl ObjectSubclass for ActionGroupPrivate {
const NAME: &'static str = "ActionGroup";
type Type = ActionGroup;
type ParentType = gtk::Box;
}
impl ObjectImpl for ActionGroupPrivate {}
impl WidgetImpl for ActionGroupPrivate {}
impl BoxImpl for ActionGroupPrivate {}
glib::wrapper! {
pub struct ActionGroup(ObjectSubclass<ActionGroupPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
}
impl ActionGroup {
fn new(builder: ActionGroupBuilder) -> Self {
let s: Self = Object::builder().build();
s.set_orientation(builder.orientation);
let primary_button = builder.primary_action.button();
let secondary_button = builder.secondary_action.map(|action| action.button());
let tertiary_button = builder.tertiary_action.map(|action| action.button());
if let Some(button) = tertiary_button {
s.append(&button);
}
s.set_halign(gtk::Align::End);
if let Some(button) = secondary_button {
s.append(&button);
}
s.append(&primary_button);
s
}
pub fn builder() -> ActionGroupBuilder {
ActionGroupBuilder {
orientation: gtk::Orientation::Horizontal,
primary_action: Action {
label: "Ok".to_owned(),
action: Box::new(|| {}),
},
secondary_action: None,
tertiary_action: None,
}
}
}
struct Action {
label: String,
action: Box<dyn Fn()>,
}
impl Action {
fn button(self) -> gtk::Button {
let button = gtk::Button::builder().label(self.label).build();
button.connect_clicked(move |_| (self.action)());
button
}
}
pub struct ActionGroupBuilder {
orientation: gtk::Orientation,
primary_action: Action,
secondary_action: Option<Action>,
tertiary_action: Option<Action>,
}
impl ActionGroupBuilder {
pub fn orientation(mut self, orientation: gtk::Orientation) -> Self {
self.orientation = orientation;
self
}
pub fn primary_action<A>(mut self, label: &str, action: A) -> Self
where
A: Fn() + 'static,
{
self.primary_action = Action {
label: label.to_owned(),
action: Box::new(action),
};
self
}
pub fn secondary_action<A>(mut self, label: &str, action: A) -> Self
where
A: Fn() + 'static,
{
self.secondary_action = Some(Action {
label: label.to_owned(),
action: Box::new(action),
});
self
}
pub fn tertiary_action<A>(mut self, label: &str, action: A) -> Self
where
A: Fn() + 'static,
{
self.tertiary_action = Some(Action {
label: label.to_owned(),
action: Box::new(action),
});
self
}
pub fn build(self) -> ActionGroup {
ActionGroup::new(self)
}
}

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com> Copyright 2023-2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of FitnessTrax. This file is part of FitnessTrax.
@ -16,16 +16,16 @@ You should have received a copy of the GNU General Public License along with Fit
// use chrono::NaiveDate; // use chrono::NaiveDate;
// use ft_core::TraxRecord; // use ft_core::TraxRecord;
use crate::components::{TimeDistanceView, WeightView}; use crate::{
use emseries::Record; components::{steps_editor, weight_editor, ActionGroup, Steps, Weight},
use ft_core::{RecordType, TraxRecord, Weight}; view_models::DayDetailViewModel,
};
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use std::cell::RefCell; use std::cell::RefCell;
pub struct DaySummaryPrivate { pub struct DaySummaryPrivate {
date: gtk::Label, date: gtk::Label,
weight: RefCell<Option<gtk::Label>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -39,10 +39,7 @@ impl ObjectSubclass for DaySummaryPrivate {
.css_classes(["day-summary__date"]) .css_classes(["day-summary__date"])
.halign(gtk::Align::Start) .halign(gtk::Align::Start)
.build(); .build();
Self { Self { date }
date,
weight: RefCell::new(None),
}
} }
} }
@ -56,8 +53,8 @@ glib::wrapper! {
pub struct DaySummary(ObjectSubclass<DaySummaryPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; pub struct DaySummary(ObjectSubclass<DaySummaryPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
} }
impl DaySummary { impl Default for DaySummary {
pub fn new() -> Self { fn default() -> Self {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
s.set_orientation(gtk::Orientation::Vertical); s.set_orientation(gtk::Orientation::Vertical);
s.set_css_classes(&["day-summary"]); s.set_css_classes(&["day-summary"]);
@ -66,62 +63,52 @@ impl DaySummary {
s s
} }
}
pub fn set_data(&self, date: chrono::NaiveDate, records: Vec<Record<TraxRecord>>) { impl DaySummary {
pub fn new() -> Self {
Self::default()
}
pub fn set_data(&self, view_model: DayDetailViewModel) {
self.imp() self.imp()
.date .date
.set_text(&date.format("%Y-%m-%d").to_string()); .set_text(&view_model.date.format("%Y-%m-%d").to_string());
if let Some(ref weight_label) = *self.imp().weight.borrow() { let row = gtk::Box::builder().build();
self.remove(weight_label);
}
if let Some(Record {
data: TraxRecord::Weight(weight_record),
..
}) = records.iter().filter(|f| f.data.is_weight()).next()
{
let label = gtk::Label::builder() let label = gtk::Label::builder()
.halign(gtk::Align::Start) .halign(gtk::Align::Start)
.label(&format!("{}", weight_record.weight))
.css_classes(["day-summary__weight"]) .css_classes(["day-summary__weight"])
.build(); .build();
if let Some(w) = view_model.weight() {
label.set_label(&w.to_string())
}
row.append(&label);
self.append(&label); self.append(&label);
*self.imp().weight.borrow_mut() = Some(label);
}
/* let label = gtk::Label::builder()
self.append(
&gtk::Label::builder()
.halign(gtk::Align::Start) .halign(gtk::Align::Start)
.label("15km of biking in 60 minutes") .css_classes(["day-summary__weight"])
.build(), .build();
); if let Some(s) = view_model.steps() {
*/ label.set_label(&format!("{} steps", s.to_string()));
}
row.append(&label);
self.append(&row);
} }
} }
pub struct DayDetailPrivate { #[derive(Default)]
date: gtk::Label, pub struct DayDetailPrivate {}
weight: RefCell<Option<gtk::Label>>,
}
#[glib::object_subclass] #[glib::object_subclass]
impl ObjectSubclass for DayDetailPrivate { impl ObjectSubclass for DayDetailPrivate {
const NAME: &'static str = "DayDetail"; const NAME: &'static str = "DayDetail";
type Type = DayDetail; type Type = DayDetail;
type ParentType = gtk::Box; type ParentType = gtk::Box;
fn new() -> Self {
let date = gtk::Label::builder()
.css_classes(["daysummary-date"])
.halign(gtk::Align::Start)
.build();
Self {
date,
weight: RefCell::new(None),
}
}
} }
impl ObjectImpl for DayDetailPrivate {} impl ObjectImpl for DayDetailPrivate {}
@ -133,19 +120,21 @@ glib::wrapper! {
} }
impl DayDetail { impl DayDetail {
pub fn new<PutRecordFn, UpdateRecordFn>( pub fn new<OnEdit>(view_model: DayDetailViewModel, on_edit: OnEdit) -> Self
date: chrono::NaiveDate,
records: Vec<Record<TraxRecord>>,
on_put_record: PutRecordFn,
on_update_record: UpdateRecordFn,
) -> Self
where where
PutRecordFn: Fn(TraxRecord) + 'static, OnEdit: Fn() + 'static,
UpdateRecordFn: Fn(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);
s.set_hexpand(true);
s.append(
&ActionGroup::builder()
.primary_action("Edit", Box::new(on_edit))
.build(),
);
/*
let click_controller = gtk::GestureClick::new(); let click_controller = gtk::GestureClick::new();
click_controller.connect_released({ click_controller.connect_released({
let s = s.clone(); let s = s.clone();
@ -158,52 +147,63 @@ impl DayDetail {
} }
}); });
s.add_controller(click_controller); s.add_controller(click_controller);
*/
/*
let weight_record = records.iter().find_map(|record| match record { let weight_record = records.iter().find_map(|record| match record {
Record { Record {
id, id,
data: TraxRecord::Weight(record), data: ft_core::TraxRecord::Weight(record),
} => Some((id.clone(), record.clone())), } => Some((id.clone(), record.clone())),
_ => None, _ => None,
}); });
*/
let weight_view = match weight_record { let top_row = gtk::Box::builder()
Some((id, data)) => WeightView::new(date.clone(), Some(data.clone()), move |weight| { .orientation(gtk::Orientation::Horizontal)
on_update_record(Record { .build();
id: id.clone(), let weight_view = Weight::new(view_model.weight());
data: TraxRecord::Weight(Weight { date, weight }), top_row.append(&weight_view.widget());
})
}),
None => WeightView::new(date.clone(), None, move |weight| {
on_put_record(TraxRecord::Weight(Weight { date, weight }));
}),
};
s.append(&weight_view);
let steps_view = Steps::new(view_model.steps());
top_row.append(&steps_view.widget());
s.append(&top_row);
/*
records.into_iter().for_each(|record| { records.into_iter().for_each(|record| {
let record_view = match record { let record_view = match record {
Record { Record {
data: TraxRecord::BikeRide(record), data: ft_core::TraxRecord::BikeRide(record),
.. ..
} => Some( } => Some(
TimeDistanceView::new(RecordType::BikeRide, record).upcast::<gtk::Widget>(), TimeDistanceView::new(ft_core::RecordType::BikeRide, record)
.upcast::<gtk::Widget>(),
), ),
Record { Record {
data: TraxRecord::Row(record), data: ft_core::TraxRecord::Row(record),
.. ..
} => Some(TimeDistanceView::new(RecordType::Row, record).upcast::<gtk::Widget>()), } => Some(
TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::<gtk::Widget>(),
),
Record { Record {
data: TraxRecord::Run(record), data: ft_core::TraxRecord::Run(record),
.. ..
} => Some(TimeDistanceView::new(RecordType::Row, record).upcast::<gtk::Widget>()), } => Some(
TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::<gtk::Widget>(),
),
Record { Record {
data: TraxRecord::Swim(record), data: ft_core::TraxRecord::Swim(record),
.. ..
} => Some(TimeDistanceView::new(RecordType::Row, record).upcast::<gtk::Widget>()), } => Some(
TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::<gtk::Widget>(),
),
Record { Record {
data: TraxRecord::Walk(record), data: ft_core::TraxRecord::Walk(record),
.. ..
} => Some(TimeDistanceView::new(RecordType::Row, record).upcast::<gtk::Widget>()), } => Some(
TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::<gtk::Widget>(),
),
_ => None, _ => None,
}; };
@ -214,7 +214,97 @@ impl DayDetail {
s.append(&record_view); s.append(&record_view);
} }
}); });
*/
s s
} }
} }
pub struct DayEditPrivate {
on_finished: RefCell<Box<dyn Fn()>>,
}
impl Default for DayEditPrivate {
fn default() -> Self {
Self {
on_finished: RefCell::new(Box::new(|| {})),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for DayEditPrivate {
const NAME: &'static str = "DayEdit";
type Type = DayEdit;
type ParentType = gtk::Box;
}
impl ObjectImpl for DayEditPrivate {}
impl WidgetImpl for DayEditPrivate {}
impl BoxImpl for DayEditPrivate {}
glib::wrapper! {
pub struct DayEdit(ObjectSubclass<DayEditPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
}
impl DayEdit {
pub fn new<OnFinished>(view_model: DayDetailViewModel, on_finished: OnFinished) -> Self
where
OnFinished: Fn() + 'static,
{
let s: Self = Object::builder().build();
s.set_orientation(gtk::Orientation::Vertical);
s.set_hexpand(true);
*s.imp().on_finished.borrow_mut() = Box::new(on_finished);
s.append(
&ActionGroup::builder()
.primary_action("Save", {
let s = s.clone();
let view_model = view_model.clone();
move || {
view_model.save();
s.finish();
}
})
.secondary_action("Cancel", {
let s = s.clone();
let view_model = view_model.clone();
move || {
view_model.revert();
s.finish();
}
})
.build(),
);
let top_row = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.build();
top_row.append(
&weight_editor(view_model.weight(), {
let view_model = view_model.clone();
move |w| {
view_model.set_weight(w);
}
})
.widget(),
);
top_row.append(
&steps_editor(view_model.steps(), {
let view_model = view_model.clone();
move |s| view_model.set_steps(s)
})
.widget(),
);
s.append(&top_row);
s
}
fn finish(&self) {
(self.imp().on_finished.borrow())()
}
}

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com> Copyright 2023-2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of FitnessTrax. This file is part of FitnessTrax.
@ -14,11 +14,17 @@ 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/>.
*/ */
mod day; mod action_group;
pub use day::{DayDetail, DaySummary}; pub use action_group::ActionGroup;
mod edit_view; mod day;
pub use edit_view::EditView; pub use day::{DayDetail, DayEdit, DaySummary};
mod singleton;
pub use singleton::{Singleton, SingletonImpl};
mod steps;
pub use steps::{steps_editor, Steps};
mod text_entry; mod text_entry;
pub use text_entry::{ParseError, TextEntry}; pub use text_entry::{ParseError, TextEntry};
@ -27,7 +33,7 @@ mod time_distance;
pub use time_distance::TimeDistanceView; pub use time_distance::TimeDistanceView;
mod weight; mod weight;
pub use weight::WeightView; pub use weight::{weight_editor, Weight};
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};

View File

@ -0,0 +1,71 @@
/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of FitnessTrax.
FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/
//! A Widget container for a single components
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use std::cell::RefCell;
pub struct SingletonPrivate {
widget: RefCell<gtk::Widget>,
}
impl Default for SingletonPrivate {
fn default() -> Self {
Self {
widget: RefCell::new(gtk::Label::new(None).upcast()),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for SingletonPrivate {
const NAME: &'static str = "Singleton";
type Type = Singleton;
type ParentType = gtk::Box;
}
impl ObjectImpl for SingletonPrivate {}
impl WidgetImpl for SingletonPrivate {}
impl BoxImpl for SingletonPrivate {}
glib::wrapper! {
/// The Singleton component contains exactly one child widget. The swap function makes it easy
/// to handle the job of swapping that child out for a different one.
pub struct Singleton(ObjectSubclass<SingletonPrivate>) @extends gtk::Box, gtk::Widget;
}
impl Default for Singleton {
fn default() -> Self {
let s: Self = Object::builder().build();
s.append(&*s.imp().widget.borrow());
s
}
}
impl Singleton {
pub fn swap(&self, new_widget: &impl IsA<gtk::Widget>) {
let new_widget = new_widget.clone().upcast();
self.remove(&*self.imp().widget.borrow());
self.append(&new_widget);
*self.imp().widget.borrow_mut() = new_widget;
}
}
pub trait SingletonImpl: WidgetImpl + BoxImpl {}
unsafe impl<T: SingletonImpl> IsSubclassable<T> for Singleton {}

View File

@ -0,0 +1,61 @@
/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of FitnessTrax.
FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::components::{ParseError, TextEntry};
use gtk::prelude::*;
#[derive(Default)]
pub struct Steps {
label: gtk::Label,
}
impl Steps {
pub fn new(steps: Option<u32>) -> Self {
let label = gtk::Label::builder()
.css_classes(["card", "step-view"])
.can_focus(true)
.build();
match steps {
Some(s) => label.set_text(&format!("{}", s)),
None => label.set_text("No steps recorded"),
}
Self { label }
}
pub fn widget(&self) -> gtk::Widget {
self.label.clone().upcast()
}
}
pub fn steps_editor<OnUpdate>(value: Option<u32>, on_update: OnUpdate) -> TextEntry<u32>
where
OnUpdate: Fn(u32) + 'static,
{
TextEntry::new(
"0",
value,
|v| format!("{}", v),
move |v| match v.parse::<u32>() {
Ok(val) => {
on_update(val);
Ok(val)
}
Err(_) => Err(ParseError),
},
)
}

View File

@ -17,34 +17,48 @@ You should have received a copy of the GNU General Public License along with Fit
use gtk::prelude::*; use gtk::prelude::*;
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
#[derive(Clone, Debug)]
pub struct ParseError; pub struct ParseError;
type Renderer<T> = dyn Fn(&T) -> String;
type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
#[derive(Clone)] #[derive(Clone)]
pub struct TextEntry<T: Clone> { pub struct TextEntry<T: Clone + std::fmt::Debug> {
value: Rc<RefCell<Option<T>>>, value: Rc<RefCell<Option<T>>>,
widget: gtk::Entry, widget: gtk::Entry,
renderer: Rc<Box<dyn Fn(&T) -> String>>, #[allow(unused)]
validator: Rc<Box<dyn Fn(&str) -> Result<T, ParseError>>>, renderer: Rc<Renderer<T>>,
parser: Rc<Parser<T>>,
}
impl<T: Clone + std::fmt::Debug> std::fmt::Debug for TextEntry<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(
f,
"{{ value: {:?}, widget: {:?} }}",
self.value, self.widget
)
}
} }
// I do not understand why the data should be 'static. // I do not understand why the data should be 'static.
impl<T: Clone + 'static> TextEntry<T> { impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
pub fn new<R, V>(placeholder: &str, value: Option<T>, renderer: R, validator: V) -> Self pub fn new<R, V>(placeholder: &str, value: Option<T>, renderer: R, parser: V) -> Self
where where
R: Fn(&T) -> String + 'static, R: Fn(&T) -> String + 'static,
V: Fn(&str) -> Result<T, ParseError> + 'static, V: Fn(&str) -> Result<T, ParseError> + 'static,
{ {
let widget = gtk::Entry::builder().placeholder_text(placeholder).build(); let widget = gtk::Entry::builder().placeholder_text(placeholder).build();
match value { if let Some(ref v) = value {
Some(ref v) => widget.set_text(&renderer(&v)), widget.set_text(&renderer(v))
None => {}
} }
let s = Self { let s = Self {
value: Rc::new(RefCell::new(value)), value: Rc::new(RefCell::new(value)),
widget, widget,
renderer: Rc::new(Box::new(renderer)), renderer: Rc::new(renderer),
validator: Rc::new(Box::new(validator)), parser: Rc::new(parser),
}; };
s.widget.buffer().connect_text_notify({ s.widget.buffer().connect_text_notify({
@ -61,7 +75,7 @@ impl<T: Clone + 'static> TextEntry<T> {
self.widget.remove_css_class("error"); self.widget.remove_css_class("error");
return; return;
} }
match (self.validator)(buffer.text().as_str()) { match (self.parser)(buffer.text().as_str()) {
Ok(v) => { Ok(v) => {
*self.value.borrow_mut() = Some(v); *self.value.borrow_mut() = Some(v);
self.widget.remove_css_class("error"); self.widget.remove_css_class("error");
@ -73,18 +87,20 @@ impl<T: Clone + 'static> TextEntry<T> {
} }
} }
#[allow(unused)]
pub fn value(&self) -> Option<T> { pub fn value(&self) -> Option<T> {
let v = self.value.borrow().clone();
self.value.borrow().clone() self.value.borrow().clone()
} }
pub fn set_value(&self, value: Option<T>) { pub fn set_value(&self, value: Option<T>) {
match value { if let Some(ref v) = value {
Some(ref v) => self.widget.set_text(&(self.renderer)(&v)), self.widget.set_text(&(self.renderer)(v))
None => {}
} }
*self.value.borrow_mut() = value; *self.value.borrow_mut() = value;
} }
#[allow(unused)]
pub fn grab_focus(&self) { pub fn grab_focus(&self) {
self.widget.grab_focus(); self.widget.grab_focus();
} }

View File

@ -24,6 +24,7 @@ use std::cell::RefCell;
#[derive(Default)] #[derive(Default)]
pub struct TimeDistanceViewPrivate { pub struct TimeDistanceViewPrivate {
#[allow(unused)]
record: RefCell<Option<TimeDistance>>, record: RefCell<Option<TimeDistance>>,
} }
@ -53,7 +54,7 @@ impl TimeDistanceView {
first_row.append( first_row.append(
&gtk::Label::builder() &gtk::Label::builder()
.halign(gtk::Align::Start) .halign(gtk::Align::Start)
.label(&record.datetime.format("%H:%M").to_string()) .label(record.datetime.format("%H:%M").to_string())
.build(), .build(),
); );
@ -96,7 +97,7 @@ impl TimeDistanceView {
.label( .label(
record record
.comments .comments
.map(|comments| format!("{}", comments)) .map(|comments| comments.to_string())
.unwrap_or("".to_owned()), .unwrap_or("".to_owned()),
) )
.build(), .build(),

View File

@ -14,173 +14,54 @@ 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::components::{EditView, ParseError, TextEntry}; use crate::components::{ParseError, TextEntry};
use chrono::{Local, NaiveDate};
use dimensioned::si; use dimensioned::si;
use ft_core::Weight; use gtk::prelude::*;
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use std::cell::RefCell;
pub struct WeightViewPrivate { pub struct Weight {
date: RefCell<NaiveDate>, label: gtk::Label,
record: RefCell<Option<Weight>>,
widget: RefCell<EditView<gtk::Label, TextEntry<si::Kilogram<f64>>>>,
on_edit_finished: RefCell<Box<dyn Fn(si::Kilogram<f64>)>>,
} }
impl Default for WeightViewPrivate { impl Weight {
fn default() -> Self { pub fn new(weight: Option<si::Kilogram<f64>>) -> Self {
Self { let label = gtk::Label::builder()
date: RefCell::new(Local::now().date_naive()),
record: RefCell::new(None),
widget: RefCell::new(EditView::Unconfigured),
on_edit_finished: RefCell::new(Box::new(|_| {})),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for WeightViewPrivate {
const NAME: &'static str = "WeightView";
type Type = WeightView;
type ParentType = gtk::Box;
}
impl ObjectImpl for WeightViewPrivate {}
impl WidgetImpl for WeightViewPrivate {}
impl BoxImpl for WeightViewPrivate {}
glib::wrapper! {
pub struct WeightView(ObjectSubclass<WeightViewPrivate>) @extends gtk::Box, gtk::Widget;
}
impl WeightView {
pub fn new<OnEditFinished>(
date: NaiveDate,
weight: Option<Weight>,
on_edit_finished: OnEditFinished,
) -> Self
where
OnEditFinished: Fn(si::Kilogram<f64>) + 'static,
{
let s: Self = Object::builder().build();
*s.imp().on_edit_finished.borrow_mut() = Box::new(on_edit_finished);
*s.imp().date.borrow_mut() = date;
*s.imp().record.borrow_mut() = weight;
s.view();
s
}
fn view(&self) {
let view = gtk::Label::builder()
.css_classes(["card", "weight-view"]) .css_classes(["card", "weight-view"])
.halign(gtk::Align::Start)
.can_focus(true) .can_focus(true)
.build(); .build();
let view_click_controller = gtk::GestureClick::new(); match weight {
view_click_controller.connect_released({ Some(w) => label.set_text(&format!("{:?}", w)),
let s = self.clone(); None => label.set_text("No weight recorded"),
move |_, _, _, _| {
s.edit();
} }
});
view.add_controller(view_click_controller); Self { label }
match *self.imp().record.borrow() {
Some(ref record) => {
view.remove_css_class("dim_label");
view.set_label(&format!("{:?}", record.weight));
} }
None => {
view.add_css_class("dim_label"); pub fn widget(&self) -> gtk::Widget {
view.set_label("No weight recorded"); self.label.clone().upcast()
} }
} }
self.swap(EditView::View(view)); pub fn weight_editor<OnUpdate>(
} weight: Option<si::Kilogram<f64>>,
on_update: OnUpdate,
fn edit(&self) { ) -> TextEntry<si::Kilogram<f64>>
let edit = TextEntry::<si::Kilogram<f64>>::new( where
"weight", OnUpdate: Fn(si::Kilogram<f64>) + 'static,
None, {
TextEntry::new(
"0 kg",
weight,
|val: &si::Kilogram<f64>| val.to_string(), |val: &si::Kilogram<f64>| val.to_string(),
|v: &str| v.parse::<f64>().map(|w| w * si::KG).map_err(|_| ParseError), move |v: &str| {
); let new_weight = v.parse::<f64>().map(|w| w * si::KG).map_err(|_| ParseError);
match new_weight {
match *self.imp().record.borrow() { Ok(w) => {
Some(ref record) => edit.set_value(Some(record.weight)), on_update(w);
None => edit.set_value(None), Ok(w)
} }
Err(err) => Err(err),
self.swap(EditView::Edit(edit.clone()));
edit.grab_focus();
}
fn swap(&self, new_view: EditView<gtk::Label, TextEntry<si::Kilogram<f64>>>) {
let mut widget = self.imp().widget.borrow_mut();
match *widget {
EditView::Unconfigured => {}
EditView::View(ref view) => self.remove(view),
EditView::Edit(ref editor) => self.remove(&editor.widget()),
}
match new_view {
EditView::Unconfigured => {}
EditView::View(ref view) => self.append(view),
EditView::Edit(ref editor) => self.append(&editor.widget()),
}
*widget = new_view;
}
pub fn blur(&self) {
match *self.imp().widget.borrow() {
EditView::Unconfigured => {}
EditView::View(_) => {}
EditView::Edit(ref editor) => {
let weight = editor.value();
// This has really turned into rubbish
// on_edit_finished needs to accept a full record now.
// needs to be possible to delete a record if the value is None
// it's hard to be sure whether I need the full record object or if I need to update
// it. I probably don't. I think I need to borrow it and call on_edit_finished with an
// updated version of it.
// on_edit_finished still doesn't have a way to support a delete operation
let record = match (self.imp().record.borrow().clone(), weight) {
// update an existing record
(Some(record), Some(weight)) => Some(Weight {
date: record.date,
weight,
}),
// create a new record
(None, Some(weight)) => Some(Weight {
date: self.imp().date.borrow().clone(),
weight,
}),
// do nothing or delete an existing record
(_, None) => None,
};
match record {
Some(record) => {
self.imp().on_edit_finished.borrow()(record.weight);
*self.imp().record.borrow_mut() = Some(record);
}
None => {}
}
}
}
self.view();
} }
},
)
} }

View File

@ -18,12 +18,12 @@ mod app;
mod app_window; mod app_window;
mod components; mod components;
mod types; mod types;
mod view_models;
mod views; mod views;
use adw::prelude::*; use adw::prelude::*;
use app_window::AppWindow; use app_window::AppWindow;
use std::{env, path::PathBuf}; use std::{env, path::PathBuf};
use types::DayInterval;
const APP_ID_DEV: &str = "com.luminescent-dreams.fitnesstrax.dev"; const APP_ID_DEV: &str = "com.luminescent-dreams.fitnesstrax.dev";
const APP_ID_PROD: &str = "com.luminescent-dreams.fitnesstrax"; const APP_ID_PROD: &str = "com.luminescent-dreams.fitnesstrax";

View File

@ -21,8 +21,8 @@ impl Default for DayInterval {
impl DayInterval { impl DayInterval {
pub fn days(&self) -> impl Iterator<Item = NaiveDate> { pub fn days(&self) -> impl Iterator<Item = NaiveDate> {
DayIterator { DayIterator {
current: self.start.clone(), current: self.start,
end: self.end.clone(), end: self.end,
} }
} }
} }
@ -37,7 +37,7 @@ impl Iterator for DayIterator {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
if self.current <= self.end { if self.current <= self.end {
let val = self.current.clone(); let val = self.current;
self.current += Duration::days(1); self.current += Duration::days(1);
Some(val) Some(val)
} else { } else {

View File

@ -0,0 +1,235 @@
/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of FitnessTrax.
FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::app::App;
use dimensioned::si;
use emseries::{Record, RecordId, Recordable};
use ft_core::TraxRecord;
use std::{
collections::HashMap,
ops::Deref,
sync::{Arc, RwLock},
};
#[derive(Clone, Debug)]
enum RecordState<T: Clone + Recordable> {
Original(Record<T>),
New(T),
Updated(Record<T>),
#[allow(unused)]
Deleted(Record<T>),
}
impl<T: Clone + emseries::Recordable> RecordState<T> {
#[allow(unused)]
fn id(&self) -> Option<&RecordId> {
match self {
RecordState::Original(ref r) => Some(&r.id),
RecordState::New(ref r) => None,
RecordState::Updated(ref r) => Some(&r.id),
RecordState::Deleted(ref r) => Some(&r.id),
}
}
fn with_value(self, value: T) -> RecordState<T> {
match self {
RecordState::Original(r) => RecordState::Updated(Record { data: value, ..r }),
RecordState::New(_) => RecordState::New(value),
RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..r }),
RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..r }),
}
}
#[allow(unused)]
fn with_delete(self) -> Option<RecordState<T>> {
match self {
RecordState::Original(r) => Some(RecordState::Deleted(r)),
RecordState::New(r) => None,
RecordState::Updated(r) => Some(RecordState::Deleted(r)),
RecordState::Deleted(r) => Some(RecordState::Deleted(r)),
}
}
}
impl<T: Clone + emseries::Recordable> Deref for RecordState<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
RecordState::Original(ref r) => &r.data,
RecordState::New(ref r) => r,
RecordState::Updated(ref r) => &r.data,
RecordState::Deleted(ref r) => &r.data,
}
}
}
#[derive(Default)]
struct DayDetailViewModelInner {}
#[derive(Clone, Default)]
pub struct DayDetailViewModel {
app: Option<App>,
pub date: chrono::NaiveDate,
weight: Arc<RwLock<Option<RecordState<ft_core::Weight>>>>,
steps: Arc<RwLock<Option<RecordState<ft_core::Steps>>>>,
records: Arc<RwLock<HashMap<RecordId, RecordState<TraxRecord>>>>,
}
impl DayDetailViewModel {
pub fn new(date: chrono::NaiveDate, records: Vec<Record<TraxRecord>>, app: App) -> Self {
let (weight_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
records.into_iter().partition(|r| r.data.is_weight());
let (step_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
records.into_iter().partition(|r| r.data.is_steps());
Self {
app: Some(app),
date,
weight: Arc::new(RwLock::new(
weight_records
.first()
.and_then(|r| match r.data {
TraxRecord::Weight(ref w) => Some((r.id.clone(), w.clone())),
_ => None,
})
.map(|(id, w)| RecordState::Original(Record { id, data: w })),
)),
steps: Arc::new(RwLock::new(
step_records
.first()
.and_then(|r| match r.data {
TraxRecord::Steps(ref w) => Some((r.id.clone(), w.clone())),
_ => None,
})
.map(|(id, w)| RecordState::Original(Record { id, data: w })),
)),
records: Arc::new(RwLock::new(
records
.into_iter()
.map(|r| (r.id.clone(), RecordState::Original(r)))
.collect::<HashMap<RecordId, RecordState<TraxRecord>>>(),
)),
}
}
pub fn weight(&self) -> Option<si::Kilogram<f64>> {
(*self.weight.read().unwrap()).as_ref().map(|w| w.weight)
}
pub fn set_weight(&self, new_weight: si::Kilogram<f64>) {
let mut record = self.weight.write().unwrap();
let new_record = match *record {
Some(ref rstate) => rstate.clone().with_value(ft_core::Weight {
date: self.date,
weight: new_weight,
}),
None => RecordState::New(ft_core::Weight {
date: self.date,
weight: new_weight,
}),
};
*record = Some(new_record);
}
pub fn steps(&self) -> Option<u32> {
(*self.steps.read().unwrap()).as_ref().map(|w| w.count)
}
pub fn set_steps(&self, new_count: u32) {
let mut record = self.steps.write().unwrap();
let new_record = match *record {
Some(ref rstate) => rstate.clone().with_value(ft_core::Steps {
date: self.date,
count: new_count,
}),
None => RecordState::New(ft_core::Steps {
date: self.date,
count: new_count,
}),
};
*record = Some(new_record);
}
pub fn save(&self) {
glib::spawn_future({
let s = self.clone();
async move {
if let Some(app) = s.app {
let weight_record = s.weight.read().unwrap().clone();
match weight_record {
Some(RecordState::New(weight)) => {
let _ = app.put_record(TraxRecord::Weight(weight)).await;
}
Some(RecordState::Original(_)) => {}
Some(RecordState::Updated(weight)) => {
let _ = 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(steps)) => {
let _ = app.put_record(TraxRecord::Steps(steps)).await;
}
Some(RecordState::Original(_)) => {}
Some(RecordState::Updated(steps)) => {
let _ = 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(data) => {
let _ = app.put_record(data).await;
}
RecordState::Original(_) => {}
RecordState::Updated(r) => {
let _ = app.update_record(r.clone()).await;
}
RecordState::Deleted(_) => unimplemented!(),
}
}
}
}
});
}
pub fn revert(&self) {
unimplemented!();
}
}

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com> Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of FitnessTrax. This file is part of FitnessTrax.
@ -14,9 +14,5 @@ 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/>.
*/ */
#[derive(Clone)] mod day_detail;
pub enum EditView<View, Edit> { pub use day_detail::DayDetailViewModel;
Unconfigured,
View(View),
Edit(Edit),
}

View File

@ -0,0 +1,76 @@
/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of FitnessTrax.
FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::{
components::{DayDetail, DayEdit, Singleton, SingletonImpl},
view_models::DayDetailViewModel,
};
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use std::cell::RefCell;
#[derive(Default)]
pub struct DayDetailViewPrivate {
container: Singleton,
view_model: RefCell<DayDetailViewModel>,
}
#[glib::object_subclass]
impl ObjectSubclass for DayDetailViewPrivate {
const NAME: &'static str = "DayDetailView";
type Type = DayDetailView;
type ParentType = gtk::Box;
}
impl ObjectImpl for DayDetailViewPrivate {}
impl WidgetImpl for DayDetailViewPrivate {}
impl BoxImpl for DayDetailViewPrivate {}
impl SingletonImpl for DayDetailViewPrivate {}
glib::wrapper! {
pub struct DayDetailView(ObjectSubclass<DayDetailViewPrivate>) @extends gtk::Box, gtk::Widget;
}
impl DayDetailView {
pub fn new(view_model: DayDetailViewModel) -> Self {
let s: Self = Object::builder().build();
*s.imp().view_model.borrow_mut() = view_model;
s.append(&s.imp().container);
s.view();
s
}
fn view(&self) {
self.imp()
.container
.swap(&DayDetail::new(self.imp().view_model.borrow().clone(), {
let s = self.clone();
move || s.edit()
}));
}
fn edit(&self) {
self.imp()
.container
.swap(&DayEdit::new(self.imp().view_model.borrow().clone(), {
let s = self.clone();
move || s.view()
}));
}
}

View File

@ -14,8 +14,10 @@ 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::{components::DaySummary, types::DayInterval}; use crate::{
use chrono::{Duration, Local, NaiveDate}; app::App, components::DaySummary, types::DayInterval, view_models::DayDetailViewModel,
};
use chrono::NaiveDate;
use emseries::Record; use emseries::Record;
use ft_core::TraxRecord; use ft_core::TraxRecord;
use glib::Object; use glib::Object;
@ -26,7 +28,8 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc};
/// daily summaries, daily details, and will provide all functions the user may need for editing /// daily summaries, daily details, and will provide all functions the user may need for editing
/// records. /// records.
pub struct HistoricalViewPrivate { pub struct HistoricalViewPrivate {
time_window: RefCell<DayInterval>, app: Rc<RefCell<Option<App>>>,
time_window: Rc<RefCell<DayInterval>>,
list_view: gtk::ListView, list_view: gtk::ListView,
} }
@ -45,7 +48,17 @@ impl ObjectSubclass for HistoricalViewPrivate {
.set_child(Some(&DaySummary::new())); .set_child(Some(&DaySummary::new()));
}); });
factory.connect_bind(move |_, list_item| { let s = Self {
app: Rc::new(RefCell::new(None)),
time_window: Rc::new(RefCell::new(DayInterval::default())),
list_view: gtk::ListView::builder()
.factory(&factory)
.single_click_activate(true)
.build(),
};
factory.connect_bind({
let app = s.app.clone();
move |_, list_item| {
let records = list_item let records = list_item
.downcast_ref::<gtk::ListItem>() .downcast_ref::<gtk::ListItem>()
.expect("should be a ListItem") .expect("should be a ListItem")
@ -60,16 +73,17 @@ impl ObjectSubclass for HistoricalViewPrivate {
.and_downcast::<DaySummary>() .and_downcast::<DaySummary>()
.expect("should be a DaySummary"); .expect("should be a DaySummary");
summary.set_data(records.date(), records.records()); if let Some(app) = app.borrow().clone() {
summary.set_data(DayDetailViewModel::new(
records.date(),
records.records(),
app.clone(),
));
}
}
}); });
Self { s
time_window: RefCell::new(DayInterval::default()),
list_view: gtk::ListView::builder()
.factory(&factory)
.single_click_activate(true)
.build(),
}
} }
} }
@ -82,7 +96,11 @@ glib::wrapper! {
} }
impl HistoricalView { impl HistoricalView {
pub fn new<SelectFn>(records: Vec<Record<TraxRecord>>, on_select_day: Rc<SelectFn>) -> Self pub fn new<SelectFn>(
app: App,
records: Vec<Record<TraxRecord>>,
on_select_day: Rc<SelectFn>,
) -> Self
where where
SelectFn: Fn(chrono::NaiveDate, Vec<Record<TraxRecord>>) + 'static, SelectFn: Fn(chrono::NaiveDate, Vec<Record<TraxRecord>>) + 'static,
{ {
@ -90,6 +108,8 @@ impl HistoricalView {
s.set_orientation(gtk::Orientation::Vertical); s.set_orientation(gtk::Orientation::Vertical);
s.set_css_classes(&["historical"]); s.set_css_classes(&["historical"]);
*s.imp().app.borrow_mut() = Some(app);
let grouped_records = let grouped_records =
GroupedRecords::new((*s.imp().time_window.borrow()).clone()).with_data(records); GroupedRecords::new((*s.imp().time_window.borrow()).clone()).with_data(records);
@ -161,7 +181,7 @@ impl DayRecords {
} }
pub fn date(&self) -> chrono::NaiveDate { pub fn date(&self) -> chrono::NaiveDate {
self.imp().date.borrow().clone() *self.imp().date.borrow()
} }
pub fn records(&self) -> Vec<Record<TraxRecord>> { pub fn records(&self) -> Vec<Record<TraxRecord>> {
@ -204,11 +224,11 @@ impl GroupedRecords {
self self
} }
fn items<'a>(&'a self) -> impl Iterator<Item = DayRecords> + 'a { fn items(&self) -> impl Iterator<Item = DayRecords> + '_ {
self.interval.days().map(|date| { self.interval.days().map(|date| {
self.data self.data
.get(&date) .get(&date)
.map(|rec| rec.clone()) .cloned()
.unwrap_or(DayRecords::new(date, vec![])) .unwrap_or(DayRecords::new(date, vec![]))
}) })
} }
@ -217,6 +237,7 @@ impl GroupedRecords {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::GroupedRecords; use super::GroupedRecords;
use crate::types::DayInterval;
use chrono::{FixedOffset, NaiveDate, TimeZone}; use chrono::{FixedOffset, NaiveDate, TimeZone};
use dimensioned::si::{KG, M, S}; use dimensioned::si::{KG, M, S};
use emseries::{Record, RecordId}; use emseries::{Record, RecordId};
@ -272,7 +293,12 @@ mod test {
}, },
]; ];
let groups = GroupedRecords::from(records).0; let groups = GroupedRecords::new(DayInterval {
start: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(),
end: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(),
})
.with_data(records)
.data;
assert_eq!(groups.len(), 3); assert_eq!(groups.len(), 3);
} }
} }

View File

@ -16,6 +16,9 @@ You should have received a copy of the GNU General Public License along with Fit
use gtk::prelude::*; use gtk::prelude::*;
mod day_detail_view;
pub use day_detail_view::DayDetailView;
mod historical_view; mod historical_view;
pub use historical_view::HistoricalView; pub use historical_view::HistoricalView;

View File

@ -38,8 +38,8 @@ glib::wrapper! {
pub struct PlaceholderView(ObjectSubclass<PlaceholderViewPrivate>) @extends gtk::Box, gtk::Widget; pub struct PlaceholderView(ObjectSubclass<PlaceholderViewPrivate>) @extends gtk::Box, gtk::Widget;
} }
impl PlaceholderView { impl Default for PlaceholderView {
pub fn new() -> Self { fn default() -> Self {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
s s
} }

View File

@ -14,7 +14,7 @@ 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::App, components::FileChooserRow}; use crate::components::FileChooserRow;
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use std::path::PathBuf; use std::path::PathBuf;

View File

@ -1,23 +1,25 @@
use crate::types;
#[cfg(test)] #[cfg(test)]
mod test { mod test {
#[test] #[test]
#[ignore]
fn read_a_legacy_set_rep_record() { fn read_a_legacy_set_rep_record() {
unimplemented!() unimplemented!()
} }
#[test] #[test]
#[ignore]
fn read_a_legacy_steps_record() { fn read_a_legacy_steps_record() {
unimplemented!() unimplemented!()
} }
#[test] #[test]
#[ignore]
fn read_a_legacy_time_distance_record() { fn read_a_legacy_time_distance_record() {
unimplemented!() unimplemented!()
} }
#[test] #[test]
#[ignore]
fn read_a_legacy_weight_record() { fn read_a_legacy_weight_record() {
unimplemented!() unimplemented!()
} }

View File

@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
/// SetRep represents workouts like pushups or situps, which involve doing a "set" of a number of /// SetRep represents workouts like pushups or situps, which involve doing a "set" of a number of
/// actions, resting, and then doing another set. /// actions, resting, and then doing another set.
#[allow(dead_code)]
pub struct SetRep { pub struct SetRep {
/// I assume that a set/rep workout is only done once in a day. /// I assume that a set/rep workout is only done once in a day.
date: NaiveDate, date: NaiveDate,
@ -22,6 +23,16 @@ pub struct Steps {
pub count: u32, pub count: u32,
} }
impl Recordable for Steps {
fn timestamp(&self) -> Timestamp {
Timestamp::Date(self.date)
}
fn tags(&self) -> Vec<String> {
vec![]
}
}
/// 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
@ -54,6 +65,16 @@ pub struct Weight {
pub weight: si::Kilogram<f64>, pub weight: si::Kilogram<f64>,
} }
impl Recordable for Weight {
fn timestamp(&self) -> Timestamp {
Timestamp::Date(self.date)
}
fn tags(&self) -> Vec<String> {
vec![]
}
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum RecordType { pub enum RecordType {
BikeRide, BikeRide,
@ -91,23 +112,24 @@ impl TraxRecord {
} }
pub fn is_weight(&self) -> bool { pub fn is_weight(&self) -> bool {
match self { matches!(self, TraxRecord::Weight(_))
TraxRecord::Weight(_) => true,
_ => false,
} }
pub fn is_steps(&self) -> bool {
matches!(self, TraxRecord::Steps(_))
} }
} }
impl Recordable for TraxRecord { impl Recordable for TraxRecord {
fn timestamp(&self) -> Timestamp { fn timestamp(&self) -> Timestamp {
match self { match self {
TraxRecord::BikeRide(rec) => Timestamp::DateTime(rec.datetime.clone()), TraxRecord::BikeRide(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Row(rec) => Timestamp::DateTime(rec.datetime.clone()), TraxRecord::Row(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Run(rec) => Timestamp::DateTime(rec.datetime.clone()), TraxRecord::Run(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Steps(rec) => Timestamp::Date(rec.date), TraxRecord::Steps(rec) => rec.timestamp(),
TraxRecord::Swim(rec) => Timestamp::DateTime(rec.datetime.clone()), TraxRecord::Swim(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Walk(rec) => Timestamp::DateTime(rec.datetime.clone()), TraxRecord::Walk(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Weight(rec) => Timestamp::Date(rec.date), TraxRecord::Weight(rec) => rec.timestamp(),
} }
} }
@ -135,6 +157,6 @@ mod test {
let id = series.put(record.clone()).unwrap(); let id = series.put(record.clone()).unwrap();
let record_ = series.get(&id).unwrap(); let record_ = series.get(&id).unwrap();
assert_eq!(record_, record); assert_eq!(record_.data, record);
} }
} }

View File

@ -1,37 +1,6 @@
{ {
"nodes": { "nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1650374568,
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "b4a34015c698c7793d592d66adbab377907a2be8",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"locked": {
"lastModified": 1653893745,
"narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": { "inputs": {
"systems": "systems" "systems": "systems"
}, },
@ -51,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1703200384, "lastModified": 1704732714,
"narHash": "sha256-q5j06XOsy0qHOarsYPfZYJPWbTbc8sryRxianlEPJN0=", "narHash": "sha256-ABqK/HggMYA/jMUXgYyqVAcQ8QjeMyr1jcXfTpSHmps=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "0b3d618173114c64ab666f557504d6982665d328", "rev": "6723fa4e4f1a30d42a633bef5eb01caeb281adc3",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -65,22 +34,6 @@
} }
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": {
"lastModified": 1654275867,
"narHash": "sha256-pt14ZE4jVPGvfB2NynGsl34pgXfOqum5YJNpDK4+b9E=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "7a20c208aacf4964c19186dcad51f89165dc7ed0",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "release-22.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": { "locked": {
"lastModified": 1681303793, "lastModified": 1681303793,
"narHash": "sha256-JEdQHsYuCfRL2PICHlOiH/2ue3DwoxUX7DJ6zZxZXFk=", "narHash": "sha256-JEdQHsYuCfRL2PICHlOiH/2ue3DwoxUX7DJ6zZxZXFk=",
@ -95,60 +48,13 @@
"type": "indirect" "type": "indirect"
} }
}, },
"pkgs-cargo2nix": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1682891040,
"narHash": "sha256-hjajsi7lq24uYitUh4o04UJi1g0Qe6ruPL0s5DgPQMY=",
"owner": "cargo2nix",
"repo": "cargo2nix",
"rev": "0167b39f198d72acdf009265634504fd6f5ace15",
"type": "github"
},
"original": {
"owner": "cargo2nix",
"repo": "cargo2nix",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"pkgs-cargo2nix": "pkgs-cargo2nix",
"typeshare": "typeshare", "typeshare": "typeshare",
"unstable": "unstable" "unstable": "unstable"
} }
}, },
"rust-overlay": {
"inputs": {
"flake-utils": [
"pkgs-cargo2nix",
"flake-utils"
],
"nixpkgs": [
"pkgs-cargo2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1653878966,
"narHash": "sha256-T51Gck/vrJZi1m+uTbhEFTRgZmE59sydVONadADv358=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "8526d618af012a923ca116be9603e818b502a8db",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": { "systems": {
"locked": { "locked": {
"lastModified": 1681028828, "lastModified": 1681028828,
@ -166,15 +72,15 @@
}, },
"typeshare": { "typeshare": {
"inputs": { "inputs": {
"flake-utils": "flake-utils_2", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_3" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1690502632, "lastModified": 1698205128,
"narHash": "sha256-+k81RrxfphDUD5kekWbQ4xuZIHBEAQf67uivaQ34Afs=", "narHash": "sha256-jP+81TkldLtda8bzmsBWahETGsyFkoDOCT244YkA+S4=",
"owner": "1Password", "owner": "1Password",
"repo": "typeshare", "repo": "typeshare",
"rev": "9f74772af53759aee2f53e64478523e53083719e", "rev": "c3ee2ad8f27774c45db7af4f2ba746c4ae11de21",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -185,11 +91,11 @@
}, },
"unstable": { "unstable": {
"locked": { "locked": {
"lastModified": 1690367991, "lastModified": 1704722960,
"narHash": "sha256-2VwOn1l8y6+cu7zjNE8MgeGJNNz1eat1HwHrINeogFA=", "narHash": "sha256-mKGJ3sPsT6//s+Knglai5YflJUF2DGj7Ai6Ynopz0kI=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "c9cf0708f00fbe553319258e48ca89ff9a413703", "rev": "317484b1ead87b9c1b8ac5261a8d2dd748a0492d",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -4,11 +4,10 @@
inputs = { inputs = {
nixpkgs.url = "nixpkgs/nixos-23.11"; nixpkgs.url = "nixpkgs/nixos-23.11";
unstable.url = "nixpkgs/nixos-unstable"; unstable.url = "nixpkgs/nixos-unstable";
pkgs-cargo2nix.url = "github:cargo2nix/cargo2nix";
typeshare.url = "github:1Password/typeshare"; typeshare.url = "github:1Password/typeshare";
}; };
outputs = { self, nixpkgs, unstable, pkgs-cargo2nix, typeshare, ... }: outputs = { self, nixpkgs, unstable, typeshare, ... }:
let let
version = builtins.string 0 8 self.lastModifiedDate; version = builtins.string 0 8 self.lastModifiedDate;
supportedSystems = [ "x86_64-linux" ]; supportedSystems = [ "x86_64-linux" ];
@ -18,7 +17,6 @@
let let
pkgs = import nixpkgs { system = "x86_64-linux"; }; pkgs = import nixpkgs { system = "x86_64-linux"; };
pkgs-unstable = import unstable { system = "x86_64-linux"; }; pkgs-unstable = import unstable { system = "x86_64-linux"; };
cargo2nix = pkgs-cargo2nix.packages."x86_64-linux";
in in
pkgs.mkShell { pkgs.mkShell {
name = "ld-tools-devshell"; name = "ld-tools-devshell";
@ -26,8 +24,6 @@
pkgs.cargo-nextest pkgs.cargo-nextest
pkgs.clang pkgs.clang
pkgs.crate2nix pkgs.crate2nix
pkgs.entr
pkgs.glade
pkgs.glib pkgs.glib
pkgs.gst_all_1.gst-plugins-bad pkgs.gst_all_1.gst-plugins-bad
pkgs.gst_all_1.gst-plugins-base pkgs.gst_all_1.gst-plugins-base
@ -42,6 +38,8 @@
pkgs.pkg-config pkgs.pkg-config
pkgs.rustup pkgs.rustup
pkgs.sqlite pkgs.sqlite
pkgs.cargo-nextest
pkgs.wasm-pack
pkgs.sqlx-cli pkgs.sqlx-cli
pkgs.udev pkgs.udev
pkgs.wasm-pack pkgs.wasm-pack
@ -50,5 +48,51 @@
LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib"; LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
ENV = "dev"; ENV = "dev";
}; };
packages."x86_64-linux" =
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
gtkNativeInputs = [
pkgs.pkg-config
pkgs.gtk4
pkgs.libadwaita
];
cargoOverrides = pkgs: pkgs.buildRustCrate.override {
defaultCrateOverrides = pkgs.defaultCrateOverrides // {
gobject-sys = attrs: { nativeBuildInputs = gtkNativeInputs; };
gio-sys = attrs: { nativeBuildInputs = gtkNativeInputs; };
gdk-pixbuf-sys = attrs: { nativeBuildInputs = gtkNativeInputs; };
libadwaita-sys = attrs: { nativeBuildInputs = gtkNativeInputs; };
dashboard = attrs: { nativeBuildInputs = gtkNativeInputs; };
fitnesstrax = attrs: { nativeBuildInputs = gtkNativeInputs; };
};
};
cargo_nix = pkgs.callPackage ./Cargo.nix {
nixpkgs = nixpkgs;
buildRustCrateForPkgs = cargoOverrides;
};
in rec {
cyberpunk-splash = cargo_nix.workspaceMembers.cyberpunk-splash.build;
dashboard = cargo_nix.workspaceMembers.dashboard.build;
file-service = cargo_nix.workspaceMembers.file-service.build;
fitnesstrax = cargo_nix.workspaceMembers.fitnesstrax.build;
all = pkgs.symlinkJoin {
name = "all";
paths = [
cyberpunk-splash
dashboard
file-service
fitnesstrax
];
};
default = all;
};
}; };
} }