add webview map, bike reservation, and desktop entry
- Replace list view with Leaflet.js WebView map with marker clustering - Add floating action buttons over map (Rent, Show Rentals) - Click station markers to show bikes with Rent/Reserve buttons - Add bike type filter menu (All/Standard/E-bikes) - Support bike reservations via booking API - Show reserved bikes in rentals overview - Add desktop entry for app launchers - Update dependencies: webkit6 0.6, gtk4 0.11, libadwaita 0.9 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9961a31936
commit
256da4b440
6 changed files with 847 additions and 171 deletions
257
Cargo.lock
generated
257
Cargo.lock
generated
|
|
@ -40,9 +40,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cairo-rs"
|
name = "cairo-rs"
|
||||||
version = "0.21.5"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b01fe135c0bd16afe262b6dea349bd5ea30e6de50708cec639aae7c5c14cc7e4"
|
checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cairo-sys-rs",
|
"cairo-sys-rs",
|
||||||
|
|
@ -52,9 +52,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cairo-sys-rs"
|
name = "cairo-sys-rs"
|
||||||
version = "0.21.5"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06c28280c6b12055b5e39e4554271ae4e6630b27c0da9148c4cf6485fc6d245c"
|
checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -63,9 +63,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.56"
|
version = "1.2.58"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
|
|
@ -230,9 +230,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gdk-pixbuf"
|
name = "gdk-pixbuf"
|
||||||
version = "0.21.5"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "debb0d39e3cdd84626edfd54d6e4a6ba2da9a0ef2e796e691c4e9f8646fda00c"
|
checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gdk-pixbuf-sys",
|
"gdk-pixbuf-sys",
|
||||||
"gio",
|
"gio",
|
||||||
|
|
@ -242,9 +242,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gdk-pixbuf-sys"
|
name = "gdk-pixbuf-sys"
|
||||||
version = "0.21.5"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd95ad50b9a3d2551e25dd4f6892aff0b772fe5372d84514e9d0583af60a0ce7"
|
checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gio-sys",
|
"gio-sys",
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
|
|
@ -255,9 +255,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gdk4"
|
name = "gdk4"
|
||||||
version = "0.10.3"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "756564212bbe4a4ce05d88ffbd2582581ac6003832d0d32822d0825cca84bfbf"
|
checksum = "fa528049fd8726974a7aa1a6e1421f891e7579bea6cc6d54056ab4d1a1b937e7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cairo-rs",
|
"cairo-rs",
|
||||||
"gdk-pixbuf",
|
"gdk-pixbuf",
|
||||||
|
|
@ -270,9 +270,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gdk4-sys"
|
name = "gdk4-sys"
|
||||||
version = "0.10.3"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6d4e5b3ccf591826a4adcc83f5f57b4e59d1925cb4bf620b0d645f79498b034"
|
checksum = "3dd48b1b03dce78ab52805ac35cfb69c48af71a03af5723231d8583718738377"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cairo-sys-rs",
|
"cairo-sys-rs",
|
||||||
"gdk-pixbuf-sys",
|
"gdk-pixbuf-sys",
|
||||||
|
|
@ -314,9 +314,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gio"
|
name = "gio"
|
||||||
version = "0.21.5"
|
version = "0.22.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c5ff48bf600c68b476e61dc6b7c762f2f4eb91deef66583ba8bb815c30b5811a"
|
checksum = "816b6743c46b217aa8fba679095ac6f2162fd53259dc8f186fcdbff9c555db03"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
|
@ -331,9 +331,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gio-sys"
|
name = "gio-sys"
|
||||||
version = "0.21.5"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22"
|
checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"gobject-sys",
|
"gobject-sys",
|
||||||
|
|
@ -344,9 +344,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glib"
|
name = "glib"
|
||||||
version = "0.21.5"
|
version = "0.22.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b"
|
checksum = "039f93465ac17e6cb02d16f16572cd3e43a77e736d5ecc461e71b9c9c5c0569c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
|
|
@ -365,12 +365,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glib-macros"
|
name = "glib-macros"
|
||||||
version = "0.21.5"
|
version = "0.22.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17"
|
checksum = "bda575994e3689b1bc12f89c3df621ead46ff292623b76b4710a3a5b79be54bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro-crate",
|
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
|
|
@ -378,9 +377,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glib-sys"
|
name = "glib-sys"
|
||||||
version = "0.21.5"
|
version = "0.22.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c"
|
checksum = "1eb23a616a3dbc7fc15bbd26f58756ff0b04c8a894df3f0680cd21011db6a642"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps",
|
"system-deps",
|
||||||
|
|
@ -388,9 +387,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gobject-sys"
|
name = "gobject-sys"
|
||||||
version = "0.21.5"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294"
|
checksum = "18eda93f09d3778f38255b231b17ef67195013a592c91624a4daf8bead875565"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -399,9 +398,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "graphene-rs"
|
name = "graphene-rs"
|
||||||
version = "0.21.5"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2730030ac9db663fd8bfe1e7093742c1cafb92db9c315c9417c29032341fe2f9"
|
checksum = "c7d1b7881f96869f49808b6adfe906a93a57a34204952253444d68c3208d71f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glib",
|
"glib",
|
||||||
"graphene-sys",
|
"graphene-sys",
|
||||||
|
|
@ -410,9 +409,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "graphene-sys"
|
name = "graphene-sys"
|
||||||
version = "0.21.5"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "915e32091ea9ad241e4b044af62b7351c2d68aeb24f489a0d7f37a0fc484fd93"
|
checksum = "517f062f3fd6b7fd3e57a3f038a74b3c23ca32f51199ff028aa704609943f79c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
@ -422,9 +421,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gsk4"
|
name = "gsk4"
|
||||||
version = "0.10.3"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e755de9d8c5896c5beaa028b89e1969d067f1b9bf1511384ede971f5983aa153"
|
checksum = "53c912dfcbd28acace5fc99c40bb9f25e1dcb73efb1f2608327f66a99acdcb62"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cairo-rs",
|
"cairo-rs",
|
||||||
"gdk4",
|
"gdk4",
|
||||||
|
|
@ -437,9 +436,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gsk4-sys"
|
name = "gsk4-sys"
|
||||||
version = "0.10.3"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ce91472391146f482065f1041876d8f869057b195b95399414caa163d72f4f7"
|
checksum = "d7d54bbc7a9d8b6ffe4f0c95eede15ccfb365c8bf521275abe6bcfb57b18fb8a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cairo-sys-rs",
|
"cairo-sys-rs",
|
||||||
"gdk4-sys",
|
"gdk4-sys",
|
||||||
|
|
@ -453,9 +452,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gtk4"
|
name = "gtk4"
|
||||||
version = "0.10.3"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acb21d53cfc6f7bfaf43549731c43b67ca47d87348d81c8cfc4dcdd44828e1a4"
|
checksum = "87f671029e3f5288fd35e03a6e6b19e1ce643b10a3d261d33d183e453f6c52fe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cairo-rs",
|
"cairo-rs",
|
||||||
"field-offset",
|
"field-offset",
|
||||||
|
|
@ -474,9 +473,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gtk4-macros"
|
name = "gtk4-macros"
|
||||||
version = "0.10.3"
|
version = "0.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ccfb5a14a3d941244815d5f8101fa12d4577b59cc47245778d8d907b0003e42"
|
checksum = "3581b242ba62fdff122ebb626ea641582ec326031622bd19d60f85029c804a87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|
@ -486,9 +485,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gtk4-sys"
|
name = "gtk4-sys"
|
||||||
version = "0.10.3"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "842577fe5a1ee15d166cd3afe804ce0cab6173bc789ca32e21308834f20088dd"
|
checksum = "d0786e7e8e0550d0ab2df4d0d90032f22033e07d5ed78b6a1b2e51b05340339e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cairo-sys-rs",
|
"cairo-sys-rs",
|
||||||
"gdk-pixbuf-sys",
|
"gdk-pixbuf-sys",
|
||||||
|
|
@ -735,9 +734,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iri-string"
|
name = "iri-string"
|
||||||
version = "0.7.10"
|
version = "0.7.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
|
checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -745,9 +744,32 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.17"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "javascriptcore6"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d8d4f64d976c6dc6068723b6ef7838acf954d56b675f376c826f7e773362ddb"
|
||||||
|
dependencies = [
|
||||||
|
"glib",
|
||||||
|
"javascriptcore6-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "javascriptcore6-sys"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2b9787581c8949a7061c9b8593c4d1faf4b0fe5e5643c6c7793df20dbe39cf6"
|
||||||
|
dependencies = [
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
|
|
@ -761,9 +783,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libadwaita"
|
name = "libadwaita"
|
||||||
version = "0.8.1"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fb09e12bf8f73342b3315c839d0a7668cc0ccebd78490c49fec48bab15d5484b"
|
checksum = "bc0da4e27b20d3e71f830e5b0f0188d22c257986bf421c02cfde777fe07932a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gdk4",
|
"gdk4",
|
||||||
"gio",
|
"gio",
|
||||||
|
|
@ -776,9 +798,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libadwaita-sys"
|
name = "libadwaita-sys"
|
||||||
version = "0.8.1"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d7f94227ba87eb596fecada2491f04e357d507324142f77bf76d9e6be4a3e31"
|
checksum = "aaee067051c5d3c058d050d167688b80b67de1950cfca77730549aa761fc5d7d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gdk4-sys",
|
"gdk4-sys",
|
||||||
"gio-sys",
|
"gio-sys",
|
||||||
|
|
@ -798,9 +820,9 @@ checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.14"
|
version = "0.1.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
@ -840,9 +862,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.1.1"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
|
|
@ -858,13 +880,15 @@ dependencies = [
|
||||||
"libadwaita",
|
"libadwaita",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"urlencoding",
|
||||||
|
"webkit6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
|
|
@ -874,9 +898,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.21.5"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52d1d85e2078077a065bb7fc072783d5bcd4e51b379f22d67107d0a16937eb69"
|
checksum = "25d8f224eddef627b896d2f7b05725b3faedbd140e0e8343446f0d34f34238ee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gio",
|
"gio",
|
||||||
"glib",
|
"glib",
|
||||||
|
|
@ -886,9 +910,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango-sys"
|
name = "pango-sys"
|
||||||
version = "0.21.5"
|
version = "0.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4f06627d36ed5ff303d2df65211fc2e52ba5b17bf18dd80ff3d9628d6e06cfd"
|
checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glib-sys",
|
"glib-sys",
|
||||||
"gobject-sys",
|
"gobject-sys",
|
||||||
|
|
@ -1161,9 +1185,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.103.9"
|
version = "0.103.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
|
|
@ -1233,9 +1257,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.0.4"
|
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 = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
|
checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
@ -1280,6 +1304,32 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "soup3"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92d38b59ff6d302538efd337e15d04d61c5b909ec223c60ae4061d74605a962a"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"gio",
|
||||||
|
"glib",
|
||||||
|
"libc",
|
||||||
|
"soup3-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "soup3-sys"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79d5d25225bb06f83b78ff8cc35973b56d45fcdd21af6ed6d2bbd67f5a6f9bea"
|
||||||
|
dependencies = [
|
||||||
|
"gio-sys",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"libc",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
|
@ -1394,9 +1444,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.10.0"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
|
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tinyvec_macros",
|
"tinyvec_macros",
|
||||||
]
|
]
|
||||||
|
|
@ -1443,7 +1493,7 @@ dependencies = [
|
||||||
"toml_datetime 0.7.5+spec-1.1.0",
|
"toml_datetime 0.7.5+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"toml_writer",
|
"toml_writer",
|
||||||
"winnow",
|
"winnow 0.7.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1457,39 +1507,39 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.0.0+spec-1.1.0"
|
version = "1.1.0+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
|
checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.25.4+spec-1.1.0"
|
version = "0.25.8+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
|
checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"toml_datetime 1.0.0+spec-1.1.0",
|
"toml_datetime 1.1.0+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow",
|
"winnow 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_parser"
|
||||||
version = "1.0.9+spec-1.1.0"
|
version = "1.1.0+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
|
checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_writer"
|
name = "toml_writer"
|
||||||
version = "1.0.6+spec-1.1.0"
|
version = "1.1.0+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
|
|
@ -1585,6 +1635,12 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8_iter"
|
name = "utf8_iter"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|
@ -1700,6 +1756,39 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webkit6"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4959dd2a92813d4b2ae134e71345a03030bcff189b4f79cd131e9218aba22b70"
|
||||||
|
dependencies = [
|
||||||
|
"gdk4",
|
||||||
|
"gio",
|
||||||
|
"glib",
|
||||||
|
"gtk4",
|
||||||
|
"javascriptcore6",
|
||||||
|
"libc",
|
||||||
|
"soup3",
|
||||||
|
"webkit6-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webkit6-sys"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "236078ce03ff041bf87904c8257e6a9b0e9e0f957267c15f9c1756aadcf02581"
|
||||||
|
dependencies = [
|
||||||
|
"gdk4-sys",
|
||||||
|
"gio-sys",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"gtk4-sys",
|
||||||
|
"javascriptcore6-sys",
|
||||||
|
"libc",
|
||||||
|
"soup3-sys",
|
||||||
|
"system-deps",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
|
|
@ -1942,6 +2031,12 @@ name = "winnow"
|
||||||
version = "0.7.15"
|
version = "0.7.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
@ -1983,18 +2078,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.42"
|
version = "0.8.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
|
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.42"
|
version = "0.8.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
|
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gtk = { package = "gtk4", version = "0.10" }
|
gtk = { package = "gtk4", version = "0.11" }
|
||||||
adw = { package = "libadwaita", version = "0.8", features = ["v1_6"] }
|
adw = { package = "libadwaita", version = "0.9", features = ["v1_6"] }
|
||||||
|
webkit6 = "0.6"
|
||||||
reqwest = { version = "0.12", features = ["blocking", "rustls-tls"], default-features = false }
|
reqwest = { version = "0.12", features = ["blocking", "rustls-tls"], default-features = false }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
urlencoding = "2"
|
||||||
dirs = "5"
|
dirs = "5"
|
||||||
|
|
|
||||||
10
data/org.nextbike.NextCompanion.desktop
Normal file
10
data/org.nextbike.NextCompanion.desktop
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=NextCompanion
|
||||||
|
Comment=Nextbike client for Linux
|
||||||
|
Exec=next-companion
|
||||||
|
Icon=org.nextbike.NextCompanion
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Utility;GTK;
|
||||||
|
Keywords=bike;rental;nextbike;
|
||||||
|
StartupNotify=true
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
gtk4
|
gtk4
|
||||||
libadwaita
|
libadwaita
|
||||||
glib
|
glib
|
||||||
|
webkitgtk_6_0
|
||||||
];
|
];
|
||||||
buildDeps = with pkgs; [
|
buildDeps = with pkgs; [
|
||||||
pkg-config
|
pkg-config
|
||||||
|
|
@ -40,6 +41,8 @@
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
install -Dm644 data/icons/org.nextbike.NextCompanion.png \
|
install -Dm644 data/icons/org.nextbike.NextCompanion.png \
|
||||||
$out/share/icons/hicolor/512x512/apps/org.nextbike.NextCompanion.png
|
$out/share/icons/hicolor/512x512/apps/org.nextbike.NextCompanion.png
|
||||||
|
install -Dm644 data/org.nextbike.NextCompanion.desktop \
|
||||||
|
$out/share/applications/org.nextbike.NextCompanion.desktop
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ modules:
|
||||||
/app/bin/next-companion
|
/app/bin/next-companion
|
||||||
- install -Dm644 data/icons/org.nextbike.NextCompanion.png
|
- install -Dm644 data/icons/org.nextbike.NextCompanion.png
|
||||||
/app/share/icons/hicolor/512x512/apps/org.nextbike.NextCompanion.png
|
/app/share/icons/hicolor/512x512/apps/org.nextbike.NextCompanion.png
|
||||||
|
- install -Dm644 data/org.nextbike.NextCompanion.desktop
|
||||||
|
/app/share/applications/org.nextbike.NextCompanion.desktop
|
||||||
sources:
|
sources:
|
||||||
- type: dir
|
- type: dir
|
||||||
path: .
|
path: .
|
||||||
|
|
|
||||||
740
src/main.rs
740
src/main.rs
|
|
@ -1,10 +1,13 @@
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use adw::{Application, ApplicationWindow, BottomSheet, Clamp, HeaderBar, NavigationPage, NavigationView};
|
use adw::{Application, ApplicationWindow, BottomSheet, Clamp, HeaderBar, NavigationPage, NavigationView};
|
||||||
use gtk::{
|
use gtk::{
|
||||||
Box, Button, Entry, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow, Spinner, Stack,
|
Box, Button, Entry, Image, Label, ListBox, ListBoxRow, MenuButton, Orientation,
|
||||||
|
Overlay, ScrolledWindow, Spinner, Stack,
|
||||||
};
|
};
|
||||||
use gtk::gio;
|
use gtk::gio;
|
||||||
use gtk::glib;
|
use gtk::glib;
|
||||||
|
use webkit6::prelude::*;
|
||||||
|
use webkit6::Settings;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
@ -20,6 +23,7 @@ struct Bike {
|
||||||
id: String,
|
id: String,
|
||||||
code: String,
|
code: String,
|
||||||
electric_lock: bool,
|
electric_lock: bool,
|
||||||
|
is_reserved: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Persistent login key ──────────────────────────────────────────────────────
|
// ── Persistent login key ──────────────────────────────────────────────────────
|
||||||
|
|
@ -68,7 +72,11 @@ fn api_login(phone: &str, pin: &str) -> Result<String, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_get_rentals(loginkey: &str) -> Result<Vec<Bike>, String> {
|
fn api_get_rentals(loginkey: &str) -> Result<Vec<Bike>, String> {
|
||||||
let resp = reqwest::blocking::Client::new()
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let mut bikes = Vec::new();
|
||||||
|
|
||||||
|
// Get active rentals
|
||||||
|
let resp = client
|
||||||
.post(format!("{BASE_URL}/api/getOpenRentals.json"))
|
.post(format!("{BASE_URL}/api/getOpenRentals.json"))
|
||||||
.form(&[("apikey", API_KEY), ("loginkey", loginkey)])
|
.form(&[("apikey", API_KEY), ("loginkey", loginkey)])
|
||||||
.send()
|
.send()
|
||||||
|
|
@ -76,17 +84,40 @@ fn api_get_rentals(loginkey: &str) -> Result<Vec<Bike>, String> {
|
||||||
let json: serde_json::Value =
|
let json: serde_json::Value =
|
||||||
serde_json::from_str(&resp.text().map_err(|e| e.to_string())?)
|
serde_json::from_str(&resp.text().map_err(|e| e.to_string())?)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let arr = json["rentalCollection"]
|
if let Some(arr) = json["rentalCollection"].as_array() {
|
||||||
.as_array()
|
for b in arr {
|
||||||
.ok_or_else(|| "No rental data".to_string())?;
|
bikes.push(Bike {
|
||||||
Ok(arr
|
id: b["bike"].as_str().unwrap_or("").to_string(),
|
||||||
.iter()
|
code: b["code"].as_str().unwrap_or("").to_string(),
|
||||||
.map(|b| Bike {
|
electric_lock: b["electric_lock"].as_str().map_or(false, |s| s == "true"),
|
||||||
id: b["bike"].as_str().unwrap_or("").to_string(),
|
is_reserved: false,
|
||||||
code: b["code"].as_str().unwrap_or("").to_string(),
|
});
|
||||||
electric_lock: b["electric_lock"].as_str().map_or(false, |s| s == "true"),
|
}
|
||||||
})
|
}
|
||||||
.collect())
|
|
||||||
|
// Get active bookings/reservations
|
||||||
|
if let Ok(resp) = client
|
||||||
|
.post(format!("{BASE_URL}/api/v1.1/activeBookings.json"))
|
||||||
|
.form(&[("apikey", API_KEY), ("loginkey", loginkey)])
|
||||||
|
.send()
|
||||||
|
{
|
||||||
|
if let Ok(text) = resp.text() {
|
||||||
|
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||||
|
if let Some(arr) = json["bookingCollection"].as_array() {
|
||||||
|
for b in arr {
|
||||||
|
bikes.push(Bike {
|
||||||
|
id: b["bike"].as_str().unwrap_or("").to_string(),
|
||||||
|
code: b["code"].as_str().unwrap_or("").to_string(),
|
||||||
|
electric_lock: b["electric_lock"].as_str().map_or(false, |s| s == "true"),
|
||||||
|
is_reserved: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(bikes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_rent(loginkey: &str, bike_id: &str) -> Result<(), String> {
|
fn api_rent(loginkey: &str, bike_id: &str) -> Result<(), String> {
|
||||||
|
|
@ -98,6 +129,25 @@ fn api_rent(loginkey: &str, bike_id: &str) -> Result<(), String> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn api_book(loginkey: &str, bike_id: &str) -> Result<(), String> {
|
||||||
|
let resp = reqwest::blocking::Client::new()
|
||||||
|
.post(format!("{BASE_URL}/api/v1.1/booking.json"))
|
||||||
|
.form(&[("apikey", API_KEY), ("loginkey", loginkey), ("bike", bike_id)])
|
||||||
|
.send()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let json: serde_json::Value =
|
||||||
|
serde_json::from_str(&resp.text().map_err(|e| e.to_string())?)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
// Check for error in response
|
||||||
|
if let Some(error) = json["error"].as_str() {
|
||||||
|
return Err(error.to_string());
|
||||||
|
}
|
||||||
|
if json["booking"].is_null() && json["bookingId"].is_null() {
|
||||||
|
return Err("Reservation failed".to_string());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn api_return(loginkey: &str, bike_id: &str, station_id: &str) -> Result<(), String> {
|
fn api_return(loginkey: &str, bike_id: &str, station_id: &str) -> Result<(), String> {
|
||||||
reqwest::blocking::Client::new()
|
reqwest::blocking::Client::new()
|
||||||
.post(format!("{BASE_URL}/api/return.json"))
|
.post(format!("{BASE_URL}/api/return.json"))
|
||||||
|
|
@ -115,39 +165,102 @@ fn api_return(loginkey: &str, bike_id: &str, station_id: &str) -> Result<(), Str
|
||||||
|
|
||||||
// ── Async helpers (run on GLib main context) ──────────────────────────────────
|
// ── Async helpers (run on GLib main context) ──────────────────────────────────
|
||||||
|
|
||||||
async fn load_rentals(key: String, bikes: Rc<RefCell<Vec<Bike>>>, bikes_list: ListBox, list_stack: Stack) {
|
async fn load_rentals(
|
||||||
|
key: String,
|
||||||
|
bikes: Rc<RefCell<Vec<Bike>>>,
|
||||||
|
rentals_list: ListBox,
|
||||||
|
rentals_btn: Button,
|
||||||
|
) {
|
||||||
let result = match gio::spawn_blocking(move || api_get_rentals(&key)).await {
|
let result = match gio::spawn_blocking(move || api_get_rentals(&key)).await {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(_) => return,
|
Err(_) => return,
|
||||||
};
|
};
|
||||||
if let Ok(new_bikes) = result {
|
if let Ok(new_bikes) = result {
|
||||||
while let Some(child) = bikes_list.first_child() {
|
// Clear the rentals list
|
||||||
bikes_list.remove(&child);
|
while let Some(child) = rentals_list.first_child() {
|
||||||
|
rentals_list.remove(&child);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Count rentals and reservations
|
||||||
|
let rentals = new_bikes.iter().filter(|b| !b.is_reserved).count();
|
||||||
|
let reservations = new_bikes.iter().filter(|b| b.is_reserved).count();
|
||||||
|
let total = new_bikes.len();
|
||||||
|
|
||||||
|
// Update button visibility and label
|
||||||
|
rentals_btn.set_visible(total > 0);
|
||||||
|
let label = match (rentals, reservations) {
|
||||||
|
(r, 0) => format!("Rentals ({})", r),
|
||||||
|
(0, b) => format!("Reserved ({})", b),
|
||||||
|
(r, b) => format!("Rentals ({}) + Reserved ({})", r, b),
|
||||||
|
};
|
||||||
|
rentals_btn.set_label(&label);
|
||||||
|
|
||||||
|
// Populate rentals list with styled rows
|
||||||
for bike in &new_bikes {
|
for bike in &new_bikes {
|
||||||
let text = format!(
|
let row = create_rental_row(bike);
|
||||||
"Bike {} · code: {}{}",
|
rentals_list.append(&row);
|
||||||
bike.id,
|
|
||||||
bike.code,
|
|
||||||
if bike.electric_lock { " ⚡" } else { "" }
|
|
||||||
);
|
|
||||||
let lbl = Label::builder()
|
|
||||||
.label(&text)
|
|
||||||
.xalign(0.0)
|
|
||||||
.margin_top(14)
|
|
||||||
.margin_bottom(14)
|
|
||||||
.margin_start(12)
|
|
||||||
.margin_end(12)
|
|
||||||
.build();
|
|
||||||
let row = ListBoxRow::new();
|
|
||||||
row.set_child(Some(&lbl));
|
|
||||||
bikes_list.append(&row);
|
|
||||||
}
|
}
|
||||||
list_stack.set_visible_child_name(if new_bikes.is_empty() { "empty" } else { "list" });
|
|
||||||
*bikes.borrow_mut() = new_bikes;
|
*bikes.borrow_mut() = new_bikes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_rental_row(bike: &Bike) -> ListBoxRow {
|
||||||
|
let row = ListBoxRow::new();
|
||||||
|
|
||||||
|
let hbox = Box::builder()
|
||||||
|
.orientation(Orientation::Horizontal)
|
||||||
|
.spacing(12)
|
||||||
|
.margin_top(12)
|
||||||
|
.margin_bottom(12)
|
||||||
|
.margin_start(16)
|
||||||
|
.margin_end(16)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Bike icon
|
||||||
|
let icon = Image::from_icon_name(if bike.is_reserved {
|
||||||
|
"alarm-symbolic"
|
||||||
|
} else {
|
||||||
|
"system-run-symbolic"
|
||||||
|
});
|
||||||
|
icon.add_css_class("dim-label");
|
||||||
|
|
||||||
|
// Info box
|
||||||
|
let info = Box::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.hexpand(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let title_text = if bike.is_reserved {
|
||||||
|
format!("Bike {} (Reserved)", bike.id)
|
||||||
|
} else {
|
||||||
|
format!("Bike {}", bike.id)
|
||||||
|
};
|
||||||
|
let title = Label::builder()
|
||||||
|
.label(&title_text)
|
||||||
|
.css_classes(["heading"])
|
||||||
|
.xalign(0.0)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let subtitle = Label::builder()
|
||||||
|
.label(&format!(
|
||||||
|
"Code: {}{}",
|
||||||
|
bike.code,
|
||||||
|
if bike.electric_lock { " ⚡" } else { "" }
|
||||||
|
))
|
||||||
|
.css_classes(["dim-label", "caption"])
|
||||||
|
.xalign(0.0)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
info.append(&title);
|
||||||
|
info.append(&subtitle);
|
||||||
|
|
||||||
|
hbox.append(&icon);
|
||||||
|
hbox.append(&info);
|
||||||
|
row.set_child(Some(&hbox));
|
||||||
|
row
|
||||||
|
}
|
||||||
|
|
||||||
// ── Entry point ───────────────────────────────────────────────────────────────
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn main() -> glib::ExitCode {
|
fn main() -> glib::ExitCode {
|
||||||
|
|
@ -208,39 +321,208 @@ fn build_ui(app: &Application) {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// ── Main page ─────────────────────────────────────────────────────────────
|
// ── Main page ─────────────────────────────────────────────────────────────
|
||||||
let bikes_list = ListBox::builder()
|
|
||||||
.css_classes(["boxed-list"])
|
|
||||||
.selection_mode(gtk::SelectionMode::None)
|
|
||||||
.margin_top(8).margin_bottom(8).margin_start(12).margin_end(12)
|
|
||||||
.build();
|
|
||||||
let empty_label = Label::builder()
|
|
||||||
.label("No active rentals")
|
|
||||||
.margin_top(48)
|
|
||||||
.css_classes(["dim-label"])
|
|
||||||
.build();
|
|
||||||
let list_stack = Stack::new();
|
|
||||||
list_stack.add_named(&empty_label, Some("empty"));
|
|
||||||
list_stack.add_named(&bikes_list, Some("list"));
|
|
||||||
list_stack.set_visible_child_name("empty");
|
|
||||||
|
|
||||||
let scroll = ScrolledWindow::builder().vexpand(true).child(&list_stack).build();
|
// WebView with custom map HTML
|
||||||
|
let webview = webkit6::WebView::new();
|
||||||
|
|
||||||
|
// Configure WebView settings for CORS and JavaScript
|
||||||
|
let settings = Settings::new();
|
||||||
|
settings.set_enable_javascript(true);
|
||||||
|
settings.set_allow_file_access_from_file_urls(true);
|
||||||
|
settings.set_allow_universal_access_from_file_urls(true);
|
||||||
|
webview.set_settings(&settings);
|
||||||
|
|
||||||
|
let map_html = r#"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css"/>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css"/>
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
|
||||||
|
<style>
|
||||||
|
html, body, #map { margin: 0; padding: 0; width: 100%; height: 100%; }
|
||||||
|
.bike-marker {
|
||||||
|
background: #0066cc;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 2px solid white;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.bike-marker.empty { background: #999; }
|
||||||
|
.marker-cluster {
|
||||||
|
background: rgba(0, 102, 204, 0.6);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.marker-cluster div {
|
||||||
|
background: #0066cc;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="map"></div>
|
||||||
|
<script>
|
||||||
|
const map = L.map('map').setView([49.0069, 8.4037], 14);
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
const markers = L.markerClusterGroup({
|
||||||
|
maxClusterRadius: 80,
|
||||||
|
disableClusteringAtZoom: 14,
|
||||||
|
spiderfyOnMaxZoom: false,
|
||||||
|
showCoverageOnHover: false,
|
||||||
|
zoomToBoundsOnClick: true
|
||||||
|
});
|
||||||
|
|
||||||
|
let currentFilter = 0; // 0=all, 1=city, 2=cargo, 3=ebike
|
||||||
|
|
||||||
|
function loadMarkers(filter) {
|
||||||
|
currentFilter = filter;
|
||||||
|
markers.clearLayers();
|
||||||
|
fetch('https://api.nextbike.net/maps/nextbike-live.json?city=21')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
const places = data.countries?.[0]?.cities?.[0]?.places || [];
|
||||||
|
places.forEach(place => {
|
||||||
|
let bikeList = place.bike_list || [];
|
||||||
|
const bikeNumbers = place.bike_numbers || [];
|
||||||
|
// Filter bikes based on type (121=E-bike, others=Standard)
|
||||||
|
if (filter > 0 && bikeList.length > 0) {
|
||||||
|
bikeList = bikeList.filter(b => {
|
||||||
|
const t = b.bike_type || 0;
|
||||||
|
const isEbike = t === 121 || (b.pedelec_battery !== null && b.pedelec_battery !== undefined);
|
||||||
|
if (filter === 1) return !isEbike; // standard
|
||||||
|
if (filter === 2) return isEbike; // ebike
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const count = filter === 0 ? (place.bikes_available_to_rent || 0) : bikeList.length;
|
||||||
|
if (filter > 0 && count === 0) return; // Skip empty stations when filtering
|
||||||
|
const icon = L.divIcon({
|
||||||
|
className: '',
|
||||||
|
html: `<div class="bike-marker ${count === 0 ? 'empty' : ''}">${count}</div>`,
|
||||||
|
iconSize: [28, 28],
|
||||||
|
iconAnchor: [14, 14]
|
||||||
|
});
|
||||||
|
const marker = L.marker([place.lat, place.lng], { icon });
|
||||||
|
marker.on('click', function(e) {
|
||||||
|
L.DomEvent.stopPropagation(e);
|
||||||
|
if (count > 0) {
|
||||||
|
let bikes = [];
|
||||||
|
if (bikeList.length > 0) {
|
||||||
|
bikes = bikeList.map(b => ({
|
||||||
|
number: String(b.number),
|
||||||
|
bikeType: b.bike_type || 0,
|
||||||
|
electricLock: b.electric_lock || false
|
||||||
|
}));
|
||||||
|
} else if (bikeNumbers.length > 0) {
|
||||||
|
bikes = bikeNumbers.map(num => ({
|
||||||
|
number: String(num),
|
||||||
|
bikeType: 0,
|
||||||
|
electricLock: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (bikes.length > 0) {
|
||||||
|
window.location.href = 'app://station?' + encodeURIComponent(JSON.stringify({
|
||||||
|
stationName: place.name,
|
||||||
|
stationId: place.uid,
|
||||||
|
bikes: bikes
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
markers.addLayer(marker);
|
||||||
|
});
|
||||||
|
map.removeLayer(markers);
|
||||||
|
map.addLayer(markers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMarkers(0);
|
||||||
|
map.addLayer(markers);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>"#;
|
||||||
|
webview.load_html(map_html, Some("https://nextbike.net/"));
|
||||||
|
webview.set_vexpand(true);
|
||||||
|
webview.set_hexpand(true);
|
||||||
|
|
||||||
|
// Floating buttons at bottom of map
|
||||||
|
let button_box = Box::builder()
|
||||||
|
.orientation(Orientation::Horizontal)
|
||||||
|
.spacing(12)
|
||||||
|
.halign(gtk::Align::Center)
|
||||||
|
.valign(gtk::Align::End)
|
||||||
|
.margin_bottom(16)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let rentals_btn = Button::builder()
|
||||||
|
.css_classes(["pill", "osd"])
|
||||||
|
.visible(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
let rent_btn = Button::builder()
|
let rent_btn = Button::builder()
|
||||||
.label("Rent a Bike")
|
.label("Rent a Bike")
|
||||||
.css_classes(["suggested-action", "pill"])
|
.css_classes(["suggested-action", "pill", "osd"])
|
||||||
.margin_top(8).margin_bottom(12).margin_start(12).margin_end(12)
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
button_box.append(&rent_btn);
|
||||||
|
button_box.append(&rentals_btn);
|
||||||
|
|
||||||
|
let overlay = Overlay::new();
|
||||||
|
overlay.set_child(Some(&webview));
|
||||||
|
overlay.add_overlay(&button_box);
|
||||||
|
|
||||||
let main_hdr = HeaderBar::new();
|
let main_hdr = HeaderBar::new();
|
||||||
let logout_btn = Button::builder()
|
|
||||||
.icon_name("system-log-out-symbolic")
|
// Menu with gio actions
|
||||||
.tooltip_text("Logout")
|
let menu = gio::Menu::new();
|
||||||
|
|
||||||
|
// Bike type submenu with radio items
|
||||||
|
let type_submenu = gio::Menu::new();
|
||||||
|
type_submenu.append(Some("All bikes"), Some("app.bike-filter::all"));
|
||||||
|
type_submenu.append(Some("Standard bikes"), Some("app.bike-filter::standard"));
|
||||||
|
type_submenu.append(Some("E-bikes"), Some("app.bike-filter::ebike"));
|
||||||
|
menu.append_submenu(Some("Bike Type"), &type_submenu);
|
||||||
|
|
||||||
|
// Logout action
|
||||||
|
menu.append(Some("Logout"), Some("app.logout"));
|
||||||
|
|
||||||
|
let menu_btn = MenuButton::builder()
|
||||||
|
.icon_name("open-menu-symbolic")
|
||||||
|
.menu_model(&menu)
|
||||||
|
.tooltip_text("Menu")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// Stateful action for bike filter (radio behavior)
|
||||||
|
let bike_filter_action = gio::SimpleAction::new_stateful(
|
||||||
|
"bike-filter",
|
||||||
|
Some(glib::VariantTy::STRING),
|
||||||
|
&"all".to_variant(),
|
||||||
|
);
|
||||||
|
|
||||||
let refresh_btn = Button::builder()
|
let refresh_btn = Button::builder()
|
||||||
.icon_name("view-refresh-symbolic")
|
.icon_name("view-refresh-symbolic")
|
||||||
.tooltip_text("Refresh")
|
.tooltip_text("Refresh")
|
||||||
.build();
|
.build();
|
||||||
main_hdr.pack_end(&logout_btn);
|
main_hdr.pack_end(&menu_btn);
|
||||||
main_hdr.pack_start(&refresh_btn);
|
main_hdr.pack_start(&refresh_btn);
|
||||||
|
|
||||||
// ── Bottom sheet (rent + return) ──────────────────────────────────────────
|
// ── Bottom sheet (rent + return) ──────────────────────────────────────────
|
||||||
|
|
@ -258,9 +540,23 @@ fn build_ui(app: &Application) {
|
||||||
let rent_submit = Button::builder()
|
let rent_submit = Button::builder()
|
||||||
.label("Rent")
|
.label("Rent")
|
||||||
.css_classes(["suggested-action", "pill"])
|
.css_classes(["suggested-action", "pill"])
|
||||||
|
.hexpand(true)
|
||||||
|
.build();
|
||||||
|
let reserve_submit = Button::builder()
|
||||||
|
.label("Reserve")
|
||||||
|
.css_classes(["pill"])
|
||||||
|
.hexpand(true)
|
||||||
.build();
|
.build();
|
||||||
let rent_spinner = Spinner::new();
|
let rent_spinner = Spinner::new();
|
||||||
|
|
||||||
|
let button_row = Box::builder()
|
||||||
|
.orientation(Orientation::Horizontal)
|
||||||
|
.spacing(12)
|
||||||
|
.homogeneous(true)
|
||||||
|
.build();
|
||||||
|
button_row.append(&rent_submit);
|
||||||
|
button_row.append(&reserve_submit);
|
||||||
|
|
||||||
let rent_sheet = Box::builder()
|
let rent_sheet = Box::builder()
|
||||||
.orientation(Orientation::Vertical)
|
.orientation(Orientation::Vertical)
|
||||||
.spacing(12)
|
.spacing(12)
|
||||||
|
|
@ -271,7 +567,7 @@ fn build_ui(app: &Application) {
|
||||||
.build();
|
.build();
|
||||||
rent_form.append(&bike_entry);
|
rent_form.append(&bike_entry);
|
||||||
rent_form.append(&rent_err);
|
rent_form.append(&rent_err);
|
||||||
rent_form.append(&rent_submit);
|
rent_form.append(&button_row);
|
||||||
rent_form.append(&rent_spinner);
|
rent_form.append(&rent_spinner);
|
||||||
rent_sheet.append(&rent_form);
|
rent_sheet.append(&rent_form);
|
||||||
|
|
||||||
|
|
@ -316,10 +612,42 @@ fn build_ui(app: &Application) {
|
||||||
.build();
|
.build();
|
||||||
ret_sheet.append(&ret_inner);
|
ret_sheet.append(&ret_inner);
|
||||||
|
|
||||||
|
// — Rentals sheet —
|
||||||
|
let rentals_list = ListBox::builder()
|
||||||
|
.css_classes(["boxed-list"])
|
||||||
|
.selection_mode(gtk::SelectionMode::None)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let rentals_scroll = ScrolledWindow::builder()
|
||||||
|
.vexpand(true)
|
||||||
|
.max_content_height(300)
|
||||||
|
.child(&rentals_list)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let rentals_sheet = Box::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.spacing(12)
|
||||||
|
.build();
|
||||||
|
rentals_sheet.append(&rentals_scroll);
|
||||||
|
|
||||||
|
// — Station bikes sheet —
|
||||||
|
let station_list = ListBox::builder()
|
||||||
|
.selection_mode(gtk::SelectionMode::None)
|
||||||
|
.css_classes(["navigation-sidebar"])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let station_sheet = Box::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.spacing(12)
|
||||||
|
.build();
|
||||||
|
station_sheet.append(&station_list);
|
||||||
|
|
||||||
// — Shared sheet stack —
|
// — Shared sheet stack —
|
||||||
let sheet_stack = Stack::new();
|
let sheet_stack = Stack::new();
|
||||||
sheet_stack.add_named(&rent_sheet, Some("rent"));
|
sheet_stack.add_named(&rent_sheet, Some("rent"));
|
||||||
sheet_stack.add_named(&ret_sheet, Some("return"));
|
sheet_stack.add_named(&ret_sheet, Some("return"));
|
||||||
|
sheet_stack.add_named(&rentals_sheet, Some("rentals"));
|
||||||
|
sheet_stack.add_named(&station_sheet, Some("station"));
|
||||||
|
|
||||||
let sheet_box = Box::builder()
|
let sheet_box = Box::builder()
|
||||||
.orientation(Orientation::Vertical)
|
.orientation(Orientation::Vertical)
|
||||||
|
|
@ -335,10 +663,7 @@ fn build_ui(app: &Application) {
|
||||||
.sheet(&sheet_box)
|
.sheet(&sheet_box)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let main_content = Box::builder().orientation(Orientation::Vertical).build();
|
bottom_sheet.set_content(Some(&overlay));
|
||||||
main_content.append(&scroll);
|
|
||||||
main_content.append(&rent_btn);
|
|
||||||
bottom_sheet.set_content(Some(&main_content));
|
|
||||||
|
|
||||||
let main_body = Box::builder().orientation(Orientation::Vertical).build();
|
let main_body = Box::builder().orientation(Orientation::Vertical).build();
|
||||||
main_body.append(&main_hdr);
|
main_body.append(&main_hdr);
|
||||||
|
|
@ -375,8 +700,8 @@ fn build_ui(app: &Application) {
|
||||||
let nav = nav.clone();
|
let nav = nav.clone();
|
||||||
let loginkey = loginkey.clone();
|
let loginkey = loginkey.clone();
|
||||||
let bikes = bikes.clone();
|
let bikes = bikes.clone();
|
||||||
let bikes_list = bikes_list.clone();
|
let rentals_list = rentals_list.clone();
|
||||||
let list_stack = list_stack.clone();
|
let rentals_btn = rentals_btn.clone();
|
||||||
login_btn.connect_clicked(move |_| {
|
login_btn.connect_clicked(move |_| {
|
||||||
let p = phone.text().to_string();
|
let p = phone.text().to_string();
|
||||||
let n = pin.text().to_string();
|
let n = pin.text().to_string();
|
||||||
|
|
@ -395,8 +720,8 @@ fn build_ui(app: &Application) {
|
||||||
let nav = nav.clone();
|
let nav = nav.clone();
|
||||||
let loginkey = loginkey.clone();
|
let loginkey = loginkey.clone();
|
||||||
let bikes = bikes.clone();
|
let bikes = bikes.clone();
|
||||||
let bikes_list = bikes_list.clone();
|
let rentals_list = rentals_list.clone();
|
||||||
let list_stack = list_stack.clone();
|
let rentals_btn = rentals_btn.clone();
|
||||||
glib::MainContext::default().spawn_local(async move {
|
glib::MainContext::default().spawn_local(async move {
|
||||||
let result = match gio::spawn_blocking(move || api_login(&p, &n)).await {
|
let result = match gio::spawn_blocking(move || api_login(&p, &n)).await {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
|
|
@ -409,7 +734,7 @@ fn build_ui(app: &Application) {
|
||||||
save_loginkey(&key);
|
save_loginkey(&key);
|
||||||
*loginkey.borrow_mut() = Some(key.clone());
|
*loginkey.borrow_mut() = Some(key.clone());
|
||||||
nav.pop();
|
nav.pop();
|
||||||
load_rentals(key, bikes, bikes_list, list_stack).await;
|
load_rentals(key, bikes, rentals_list, rentals_btn).await;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
err.set_label(&e);
|
err.set_label(&e);
|
||||||
|
|
@ -420,31 +745,73 @@ fn build_ui(app: &Application) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Logout button ─────────────────────────────────────────────────────────
|
// ── Menu actions ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Logout action
|
||||||
|
let logout_action = gio::SimpleAction::new("logout", None);
|
||||||
{
|
{
|
||||||
let nav = nav.clone();
|
let nav = nav.clone();
|
||||||
let login_page = login_page.clone();
|
let login_page = login_page.clone();
|
||||||
let loginkey = loginkey.clone();
|
let loginkey = loginkey.clone();
|
||||||
logout_btn.connect_clicked(move |_| {
|
logout_action.connect_activate(move |_, _| {
|
||||||
clear_loginkey();
|
clear_loginkey();
|
||||||
*loginkey.borrow_mut() = None;
|
*loginkey.borrow_mut() = None;
|
||||||
nav.push(&login_page);
|
nav.push(&login_page);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
app.add_action(&logout_action);
|
||||||
|
|
||||||
|
// Bike filter action (stateful for radio behavior)
|
||||||
|
{
|
||||||
|
let webview = webview.clone();
|
||||||
|
bike_filter_action.connect_activate(move |a, param| {
|
||||||
|
if let Some(filter_type) = param.and_then(|p| p.str()) {
|
||||||
|
a.set_state(&filter_type.to_variant());
|
||||||
|
let filter_num = match filter_type {
|
||||||
|
"all" => 0,
|
||||||
|
"standard" => 1,
|
||||||
|
"ebike" => 2,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
let js = format!("loadMarkers({});", filter_num);
|
||||||
|
webview.evaluate_javascript(&js, None::<&str>, None::<&str>, None::<&gio::Cancellable>, |_| {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
app.add_action(&bike_filter_action);
|
||||||
|
|
||||||
// ── Refresh button ────────────────────────────────────────────────────────
|
// ── Refresh button ────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
let loginkey = loginkey.clone();
|
let loginkey = loginkey.clone();
|
||||||
let bikes = bikes.clone();
|
let bikes = bikes.clone();
|
||||||
let bikes_list = bikes_list.clone();
|
let rentals_list = rentals_list.clone();
|
||||||
let list_stack = list_stack.clone();
|
let rentals_btn = rentals_btn.clone();
|
||||||
|
let webview = webview.clone();
|
||||||
|
let bike_filter_action = bike_filter_action.clone();
|
||||||
refresh_btn.connect_clicked(move |_| {
|
refresh_btn.connect_clicked(move |_| {
|
||||||
|
// Reload the map data via JavaScript using current filter
|
||||||
|
let filter_state = bike_filter_action.state().and_then(|s| s.str().map(|s| s.to_string()));
|
||||||
|
let filter = match filter_state.as_deref() {
|
||||||
|
Some("standard") => 1,
|
||||||
|
Some("ebike") => 2,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
let js = format!("loadMarkers({});", filter);
|
||||||
|
webview.evaluate_javascript(
|
||||||
|
&js,
|
||||||
|
None::<&str>,
|
||||||
|
None::<&str>,
|
||||||
|
None::<&gio::Cancellable>,
|
||||||
|
|_| {},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reload rentals
|
||||||
if let Some(key) = loginkey.borrow().clone() {
|
if let Some(key) = loginkey.borrow().clone() {
|
||||||
let bikes = bikes.clone();
|
let bikes = bikes.clone();
|
||||||
let bikes_list = bikes_list.clone();
|
let rentals_list = rentals_list.clone();
|
||||||
let list_stack = list_stack.clone();
|
let rentals_btn = rentals_btn.clone();
|
||||||
glib::MainContext::default().spawn_local(async move {
|
glib::MainContext::default().spawn_local(async move {
|
||||||
load_rentals(key, bikes, bikes_list, list_stack).await;
|
load_rentals(key, bikes, rentals_list, rentals_btn).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -464,6 +831,16 @@ fn build_ui(app: &Application) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Open rentals sheet ───────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
|
let sheet_stack = sheet_stack.clone();
|
||||||
|
rentals_btn.connect_clicked(move |_| {
|
||||||
|
sheet_stack.set_visible_child_name("rentals");
|
||||||
|
bottom_sheet.set_open(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Rent submit ───────────────────────────────────────────────────────────
|
// ── Rent submit ───────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
let loginkey = loginkey.clone();
|
let loginkey = loginkey.clone();
|
||||||
|
|
@ -473,8 +850,8 @@ fn build_ui(app: &Application) {
|
||||||
let btn = rent_submit.clone();
|
let btn = rent_submit.clone();
|
||||||
let bottom_sheet = bottom_sheet.clone();
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
let bikes = bikes.clone();
|
let bikes = bikes.clone();
|
||||||
let bikes_list = bikes_list.clone();
|
let rentals_list = rentals_list.clone();
|
||||||
let list_stack = list_stack.clone();
|
let rentals_btn = rentals_btn.clone();
|
||||||
rent_submit.connect_clicked(move |_| {
|
rent_submit.connect_clicked(move |_| {
|
||||||
let id = entry.text().to_string();
|
let id = entry.text().to_string();
|
||||||
if id.is_empty() {
|
if id.is_empty() {
|
||||||
|
|
@ -492,8 +869,8 @@ fn build_ui(app: &Application) {
|
||||||
let err = err.clone();
|
let err = err.clone();
|
||||||
let bottom_sheet = bottom_sheet.clone();
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
let bikes = bikes.clone();
|
let bikes = bikes.clone();
|
||||||
let bikes_list = bikes_list.clone();
|
let rentals_list = rentals_list.clone();
|
||||||
let list_stack = list_stack.clone();
|
let rentals_btn = rentals_btn.clone();
|
||||||
let key_reload = key.clone();
|
let key_reload = key.clone();
|
||||||
glib::MainContext::default().spawn_local(async move {
|
glib::MainContext::default().spawn_local(async move {
|
||||||
let result = match gio::spawn_blocking(move || api_rent(&key, &id)).await {
|
let result = match gio::spawn_blocking(move || api_rent(&key, &id)).await {
|
||||||
|
|
@ -507,7 +884,57 @@ fn build_ui(app: &Application) {
|
||||||
err.set_visible(true);
|
err.set_visible(true);
|
||||||
} else {
|
} else {
|
||||||
bottom_sheet.set_open(false);
|
bottom_sheet.set_open(false);
|
||||||
load_rentals(key_reload, bikes, bikes_list, list_stack).await;
|
load_rentals(key_reload, bikes, rentals_list, rentals_btn).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Reserve submit ───────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
let loginkey = loginkey.clone();
|
||||||
|
let entry = bike_entry.clone();
|
||||||
|
let err = rent_err.clone();
|
||||||
|
let spinner = rent_spinner.clone();
|
||||||
|
let btn = reserve_submit.clone();
|
||||||
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let rentals_list = rentals_list.clone();
|
||||||
|
let rentals_btn = rentals_btn.clone();
|
||||||
|
reserve_submit.connect_clicked(move |_| {
|
||||||
|
let id = entry.text().to_string();
|
||||||
|
if id.is_empty() {
|
||||||
|
err.set_label("Enter a bike number");
|
||||||
|
err.set_visible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(key) = loginkey.borrow().clone() {
|
||||||
|
err.set_visible(false);
|
||||||
|
spinner.set_spinning(true);
|
||||||
|
btn.set_sensitive(false);
|
||||||
|
|
||||||
|
let spinner = spinner.clone();
|
||||||
|
let btn = btn.clone();
|
||||||
|
let err = err.clone();
|
||||||
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let rentals_list = rentals_list.clone();
|
||||||
|
let rentals_btn = rentals_btn.clone();
|
||||||
|
let key_reload = key.clone();
|
||||||
|
glib::MainContext::default().spawn_local(async move {
|
||||||
|
let result = match gio::spawn_blocking(move || api_book(&key, &id)).await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => Err("Internal error".to_string()),
|
||||||
|
};
|
||||||
|
spinner.set_spinning(false);
|
||||||
|
btn.set_sensitive(true);
|
||||||
|
if let Err(e) = result {
|
||||||
|
err.set_label(&e);
|
||||||
|
err.set_visible(true);
|
||||||
|
} else {
|
||||||
|
bottom_sheet.set_open(false);
|
||||||
|
load_rentals(key_reload, bikes, rentals_list, rentals_btn).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -516,14 +943,13 @@ fn build_ui(app: &Application) {
|
||||||
|
|
||||||
// ── Click rental row → open return bottom sheet ───────────────────────────
|
// ── Click rental row → open return bottom sheet ───────────────────────────
|
||||||
{
|
{
|
||||||
let bottom_sheet = bottom_sheet.clone();
|
|
||||||
let sheet_stack = sheet_stack.clone();
|
let sheet_stack = sheet_stack.clone();
|
||||||
let bikes = bikes.clone();
|
let bikes = bikes.clone();
|
||||||
let return_bike = return_bike.clone();
|
let return_bike = return_bike.clone();
|
||||||
let ret_inner = ret_inner.clone();
|
let ret_inner = ret_inner.clone();
|
||||||
let station_entry = station_entry.clone();
|
let station_entry = station_entry.clone();
|
||||||
let ret_err = ret_err.clone();
|
let ret_err = ret_err.clone();
|
||||||
bikes_list.connect_row_activated(move |_, row| {
|
rentals_list.connect_row_activated(move |_, row| {
|
||||||
let idx = row.index() as usize;
|
let idx = row.index() as usize;
|
||||||
let bike = bikes.borrow().get(idx).cloned();
|
let bike = bikes.borrow().get(idx).cloned();
|
||||||
if let Some(bike) = bike {
|
if let Some(bike) = bike {
|
||||||
|
|
@ -532,7 +958,6 @@ fn build_ui(app: &Application) {
|
||||||
ret_inner.set_visible_child_name(if bike.electric_lock { "electric" } else { "manual" });
|
ret_inner.set_visible_child_name(if bike.electric_lock { "electric" } else { "manual" });
|
||||||
*return_bike.borrow_mut() = Some(bike);
|
*return_bike.borrow_mut() = Some(bike);
|
||||||
sheet_stack.set_visible_child_name("return");
|
sheet_stack.set_visible_child_name("return");
|
||||||
bottom_sheet.set_open(true);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -547,8 +972,8 @@ fn build_ui(app: &Application) {
|
||||||
let btn = ret_submit.clone();
|
let btn = ret_submit.clone();
|
||||||
let bottom_sheet = bottom_sheet.clone();
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
let bikes = bikes.clone();
|
let bikes = bikes.clone();
|
||||||
let bikes_list = bikes_list.clone();
|
let rentals_list = rentals_list.clone();
|
||||||
let list_stack = list_stack.clone();
|
let rentals_btn = rentals_btn.clone();
|
||||||
ret_submit.connect_clicked(move |_| {
|
ret_submit.connect_clicked(move |_| {
|
||||||
let station = entry.text().to_string();
|
let station = entry.text().to_string();
|
||||||
if station.is_empty() {
|
if station.is_empty() {
|
||||||
|
|
@ -568,8 +993,8 @@ fn build_ui(app: &Application) {
|
||||||
let err = err.clone();
|
let err = err.clone();
|
||||||
let bottom_sheet = bottom_sheet.clone();
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
let bikes = bikes.clone();
|
let bikes = bikes.clone();
|
||||||
let bikes_list = bikes_list.clone();
|
let rentals_list = rentals_list.clone();
|
||||||
let list_stack = list_stack.clone();
|
let rentals_btn = rentals_btn.clone();
|
||||||
let key_reload = key.clone();
|
let key_reload = key.clone();
|
||||||
glib::MainContext::default().spawn_local(async move {
|
glib::MainContext::default().spawn_local(async move {
|
||||||
let result = match gio::spawn_blocking(move || {
|
let result = match gio::spawn_blocking(move || {
|
||||||
|
|
@ -587,20 +1012,159 @@ fn build_ui(app: &Application) {
|
||||||
err.set_visible(true);
|
err.set_visible(true);
|
||||||
} else {
|
} else {
|
||||||
bottom_sheet.set_open(false);
|
bottom_sheet.set_open(false);
|
||||||
load_rentals(key_reload, bikes, bikes_list, list_stack).await;
|
load_rentals(key_reload, bikes, rentals_list, rentals_btn).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Handle station click via navigation ───────────────────────────────────
|
||||||
|
{
|
||||||
|
let station_list = station_list.clone();
|
||||||
|
let sheet_stack = sheet_stack.clone();
|
||||||
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
|
let loginkey = loginkey.clone();
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let rentals_list = rentals_list.clone();
|
||||||
|
let rentals_btn = rentals_btn.clone();
|
||||||
|
webview.connect_decide_policy(move |_, decision, decision_type| {
|
||||||
|
use webkit6::PolicyDecisionType;
|
||||||
|
if decision_type == PolicyDecisionType::NavigationAction {
|
||||||
|
if let Some(nav_decision) = decision.downcast_ref::<webkit6::NavigationPolicyDecision>() {
|
||||||
|
if let Some(nav_action) = nav_decision.navigation_action() {
|
||||||
|
if let Some(request) = nav_action.request() {
|
||||||
|
if let Some(uri) = request.uri() {
|
||||||
|
if uri.starts_with("app://station?") {
|
||||||
|
decision.ignore();
|
||||||
|
let json_encoded = uri.strip_prefix("app://station?").unwrap_or("");
|
||||||
|
if let Ok(json_str) = urlencoding::decode(json_encoded) {
|
||||||
|
if let Ok(data) = serde_json::from_str::<serde_json::Value>(&json_str) {
|
||||||
|
while let Some(child) = station_list.first_child() {
|
||||||
|
station_list.remove(&child);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bike_arr) = data["bikes"].as_array() {
|
||||||
|
for bike in bike_arr {
|
||||||
|
let number = bike["number"].as_str().unwrap_or("").to_string();
|
||||||
|
let bike_type = bike["bikeType"].as_i64().unwrap_or(0);
|
||||||
|
|
||||||
|
let is_ebike = bike_type == 121;
|
||||||
|
let icon = if is_ebike { "⚡" } else { "🚲" };
|
||||||
|
|
||||||
|
let row = ListBoxRow::builder()
|
||||||
|
.activatable(false)
|
||||||
|
.selectable(false)
|
||||||
|
.build();
|
||||||
|
let hbox = Box::builder()
|
||||||
|
.orientation(Orientation::Horizontal)
|
||||||
|
.spacing(8)
|
||||||
|
.margin_top(8)
|
||||||
|
.margin_bottom(8)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let label = Label::builder()
|
||||||
|
.label(&format!("{} {}", icon, number))
|
||||||
|
.hexpand(true)
|
||||||
|
.xalign(0.0)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let rent_btn = Button::builder()
|
||||||
|
.label("Rent")
|
||||||
|
.css_classes(["suggested-action", "pill"])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let reserve_btn = Button::builder()
|
||||||
|
.label("Reserve")
|
||||||
|
.css_classes(["pill"])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Connect rent button
|
||||||
|
{
|
||||||
|
let bike_id = number.clone();
|
||||||
|
let loginkey = loginkey.clone();
|
||||||
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let rentals_list = rentals_list.clone();
|
||||||
|
let rentals_btn = rentals_btn.clone();
|
||||||
|
rent_btn.connect_clicked(move |btn| {
|
||||||
|
if let Some(key) = loginkey.borrow().clone() {
|
||||||
|
btn.set_sensitive(false);
|
||||||
|
let bike_id = bike_id.clone();
|
||||||
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let rentals_list = rentals_list.clone();
|
||||||
|
let rentals_btn = rentals_btn.clone();
|
||||||
|
let key_reload = key.clone();
|
||||||
|
glib::MainContext::default().spawn_local(async move {
|
||||||
|
let result = gio::spawn_blocking(move || api_rent(&key, &bike_id)).await;
|
||||||
|
if result.is_ok() {
|
||||||
|
bottom_sheet.set_open(false);
|
||||||
|
load_rentals(key_reload, bikes, rentals_list, rentals_btn).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect reserve button
|
||||||
|
{
|
||||||
|
let bike_id = number.clone();
|
||||||
|
let loginkey = loginkey.clone();
|
||||||
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let rentals_list = rentals_list.clone();
|
||||||
|
let rentals_btn = rentals_btn.clone();
|
||||||
|
reserve_btn.connect_clicked(move |btn| {
|
||||||
|
if let Some(key) = loginkey.borrow().clone() {
|
||||||
|
btn.set_sensitive(false);
|
||||||
|
let bike_id = bike_id.clone();
|
||||||
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let rentals_list = rentals_list.clone();
|
||||||
|
let rentals_btn = rentals_btn.clone();
|
||||||
|
let key_reload = key.clone();
|
||||||
|
glib::MainContext::default().spawn_local(async move {
|
||||||
|
let result = gio::spawn_blocking(move || api_book(&key, &bike_id)).await;
|
||||||
|
if result.is_ok() {
|
||||||
|
bottom_sheet.set_open(false);
|
||||||
|
load_rentals(key_reload, bikes, rentals_list, rentals_btn).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hbox.append(&label);
|
||||||
|
hbox.append(&reserve_btn);
|
||||||
|
hbox.append(&rent_btn);
|
||||||
|
row.set_child(Some(&hbox));
|
||||||
|
station_list.append(&row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sheet_stack.set_visible_child_name("station");
|
||||||
|
bottom_sheet.set_open(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Initial rentals load ──────────────────────────────────────────────────
|
// ── Initial rentals load ──────────────────────────────────────────────────
|
||||||
if let Some(key) = loginkey.borrow().clone() {
|
if let Some(key) = loginkey.borrow().clone() {
|
||||||
let bikes = bikes.clone();
|
let bikes = bikes.clone();
|
||||||
let bikes_list = bikes_list.clone();
|
let rentals_list = rentals_list.clone();
|
||||||
let list_stack = list_stack.clone();
|
let rentals_btn = rentals_btn.clone();
|
||||||
glib::MainContext::default().spawn_local(async move {
|
glib::MainContext::default().spawn_local(async move {
|
||||||
load_rentals(key, bikes, bikes_list, list_stack).await;
|
load_rentals(key, bikes, rentals_list, rentals_btn).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue