From 41b9116c6a9c7df411989ed4a034fa26e09a19aa Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 14 Oct 2026 08:51:38 +0200 Subject: [PATCH] progress --- tauri/.gitignore | 2 + tauri/Cargo.lock | 4404 +++++++++++++++++ tauri/Cargo.toml | 24 + tauri/build.rs | 3 + tauri/capabilities/default.json | 7 + tauri/icons/128x128.png | Bin 0 -> 2923 bytes tauri/icons/128x128@2x.png | Bin 0 -> 5967 bytes tauri/icons/32x32.png | Bin 0 -> 820 bytes tauri/icons/64x64.png | Bin 0 -> 1511 bytes tauri/icons/Square107x107Logo.png | Bin 0 -> 2527 bytes tauri/icons/Square142x142Logo.png | Bin 0 -> 3283 bytes tauri/icons/Square150x150Logo.png | Bin 0 -> 3418 bytes tauri/icons/Square284x284Logo.png | Bin 0 -> 6590 bytes tauri/icons/Square30x30Logo.png | Bin 0 -> 800 bytes tauri/icons/Square310x310Logo.png | Bin 0 -> 7374 bytes tauri/icons/Square44x44Logo.png | Bin 0 -> 1087 bytes tauri/icons/Square71x71Logo.png | Bin 0 -> 1709 bytes tauri/icons/Square89x89Logo.png | Bin 0 -> 2065 bytes tauri/icons/StoreLogo.png | Bin 0 -> 1224 bytes tauri/icons/icon.icns | Bin 0 -> 73228 bytes tauri/icons/icon.ico | Bin 0 -> 12089 bytes tauri/icons/icon.png | Bin 0 -> 12198 bytes tauri/package.json | 8 + tauri/src/commands/edit.rs | 246 + tauri/src/commands/files.rs | 161 + tauri/src/commands/mod.rs | 14 + tauri/src/commands/queries.rs | 136 + tauri/src/commands/routing.rs | 73 + tauri/src/commands/selection.rs | 102 + tauri/src/commands/statistics.rs | 135 + tauri/src/commands/storage.rs | 46 + tauri/src/dto.rs | 578 +++ tauri/src/lib.rs | 125 + tauri/src/main.rs | 6 + tauri/src/state.rs | 107 + tauri/src/tests.rs | 274 + tauri/tauri.conf.json | 37 + website/package-lock.json | 12 + website/package.json | 1 + .../elevation-profile/elevation-profile.ts | 11 +- .../src/lib/components/export/Export.svelte | 38 +- .../src/lib/components/export/utils.svelte.ts | 14 +- .../file-list/FileListNodeLabel.svelte | 4 +- .../lib/components/map/gpx-layer/gpx-layer.ts | 12 +- .../toolbar/tools/reduce/Reduce.svelte | 11 +- .../toolbar/tools/scissors/Scissors.svelte | 8 +- .../toolbar/tools/waypoint/waypoint.ts | 4 +- website/src/lib/data/example/example.test.ts | 16 +- website/src/lib/data/example/index.ts | 2 +- website/src/lib/engine/backend/index.ts | 18 + website/src/lib/engine/backend/tauri.ts | 217 + website/src/lib/engine/backend/types.ts | 211 + website/src/lib/engine/backend/wasm.ts | 172 + website/src/lib/engine/convert.ts | 25 - website/src/lib/engine/engine.test.ts | 32 +- website/src/lib/engine/engine.ts | 279 +- website/src/lib/engine/file-state.ts | 30 +- website/src/lib/engine/statistics.ts | 224 +- website/src/lib/engine/storage.ts | 28 +- website/src/lib/engine/types.ts | 8 +- 60 files changed, 7525 insertions(+), 340 deletions(-) create mode 100644 tauri/.gitignore create mode 100644 tauri/Cargo.lock create mode 100644 tauri/Cargo.toml create mode 100644 tauri/build.rs create mode 100644 tauri/capabilities/default.json create mode 100644 tauri/icons/128x128.png create mode 100644 tauri/icons/128x128@2x.png create mode 100644 tauri/icons/32x32.png create mode 100644 tauri/icons/64x64.png create mode 100644 tauri/icons/Square107x107Logo.png create mode 100644 tauri/icons/Square142x142Logo.png create mode 100644 tauri/icons/Square150x150Logo.png create mode 100644 tauri/icons/Square284x284Logo.png create mode 100644 tauri/icons/Square30x30Logo.png create mode 100644 tauri/icons/Square310x310Logo.png create mode 100644 tauri/icons/Square44x44Logo.png create mode 100644 tauri/icons/Square71x71Logo.png create mode 100644 tauri/icons/Square89x89Logo.png create mode 100644 tauri/icons/StoreLogo.png create mode 100644 tauri/icons/icon.icns create mode 100644 tauri/icons/icon.ico create mode 100644 tauri/icons/icon.png create mode 100644 tauri/package.json create mode 100644 tauri/src/commands/edit.rs create mode 100644 tauri/src/commands/files.rs create mode 100644 tauri/src/commands/mod.rs create mode 100644 tauri/src/commands/queries.rs create mode 100644 tauri/src/commands/routing.rs create mode 100644 tauri/src/commands/selection.rs create mode 100644 tauri/src/commands/statistics.rs create mode 100644 tauri/src/commands/storage.rs create mode 100644 tauri/src/dto.rs create mode 100644 tauri/src/lib.rs create mode 100644 tauri/src/main.rs create mode 100644 tauri/src/state.rs create mode 100644 tauri/src/tests.rs create mode 100644 tauri/tauri.conf.json create mode 100644 website/src/lib/engine/backend/index.ts create mode 100644 website/src/lib/engine/backend/tauri.ts create mode 100644 website/src/lib/engine/backend/types.ts create mode 100644 website/src/lib/engine/backend/wasm.ts diff --git a/tauri/.gitignore b/tauri/.gitignore new file mode 100644 index 000000000..79c707cba --- /dev/null +++ b/tauri/.gitignore @@ -0,0 +1,2 @@ +/target/ +/gen/ diff --git a/tauri/Cargo.lock b/tauri/Cargo.lock new file mode 100644 index 000000000..26240fd50 --- /dev/null +++ b/tauri/Cargo.lock @@ -0,0 +1,4404 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" + +[[package]] +name = "alloc-stdlib" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.2", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "cargo_toml" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f4b26e751e711a5302649417f2da046dce6391b2ea30a4820f37462314f0b9" +dependencies = [ + "semver", + "serde", + "toml 1.1.8+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" +dependencies = [ + "fnv", + "uuid", + "web-time", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" +dependencies = [ + "quote", + "syn 3.0.9", +] + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.9", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.8+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gpx-engine" +version = "0.1.0" +dependencies = [ + "chrono", + "postcard", + "quick-xml", + "serde", + "uuid", +] + +[[package]] +name = "gpx-studio-desktop" +version = "0.1.0" +dependencies = [ + "gpx-engine", + "serde", + "serde_json", + "tauri", + "tauri-build", + "uuid", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" +dependencies = [ + "bitflags 2.13.2", + "serde", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "markup5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca139e57da4383727f189e43e66e84fa372347027fc996f9f1a007ec0a37996" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" +dependencies = [ + "base64 0.23.1", + "indexmap 2.14.2", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.17+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" +dependencies = [ + "bitflags 2.13.2", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f" +dependencies = [ + "base64 0.23.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.2", + "jiff", + "schemars 0.9.0", + "schemars 1.2.3", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37f381f4e048e6cdf038b5705f8cf14ad108279d46eb968140a7b291aba9400" +dependencies = [ + "bitflags 2.13.2", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c404ecf956ff241bf6b39dd688614724e1bf1f63255abb4fc46d1c40371672ea" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.21", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eea0b6302f82c574b21af6fc6ae6e86183b9280853c22621d0abf0936911ffc" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02f468f9a9bcd404f57e5c191d91024e3de953d4b610f78c2907e7110615cb1c" +dependencies = [ + "base64 0.23.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.18.1", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.21", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "131bffec939b20642380317f8403a33c8888489925e14bb965ceaff0d949b1cb" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a57a2a9b8c8b7fc00d35b510fe808ded7f9f1b44fc1cdda6aee38632ea5413" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.21", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff55a614843b9a3f010f211df637c94df2607175b8d0f6e0f4a7c19e18621ee" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.21", + "toml 1.1.8+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.8+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.2", + "toml_datetime 1.1.2+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.2", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 1.1.2+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b9c52859a94554803ccd4a24b98f74148ebc73b90676d783f3490b1bff9d72" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b" +dependencies = [ + "icu_properties", + "regex", + "serde", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" +dependencies = [ + "thiserror 2.0.21", + "windows", + "windows-core", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "windows-sys 0.61.2", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wry" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a819957a01b3119af85e638a38d242af76dbc87d130dca67bfd0441072e21ff0" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.21", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tauri/Cargo.toml b/tauri/Cargo.toml new file mode 100644 index 000000000..39013bcc2 --- /dev/null +++ b/tauri/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "gpx-studio-desktop" +version = "0.1.0" +description = "Desktop shell for gpx.studio" +edition = "2021" + +[lib] +name = "gpx_studio_desktop_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[dev-dependencies] +tauri = { version = "2", features = ["test"] } + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +uuid = { version = "1", features = ["serde"] } +gpx-engine = { path = "../gpx-rs/engine" } + +[workspace] diff --git a/tauri/build.rs b/tauri/build.rs new file mode 100644 index 000000000..d860e1e6a --- /dev/null +++ b/tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/tauri/capabilities/default.json b/tauri/capabilities/default.json new file mode 100644 index 000000000..b3c3d11bc --- /dev/null +++ b/tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Permissions for the main window", + "windows": ["main"], + "permissions": ["core:default"] +} diff --git a/tauri/icons/128x128.png b/tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..856160565a7db4383eda219ae569e1fae413f3b1 GIT binary patch literal 2923 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 literal 0 HcmV?d00001 diff --git a/tauri/icons/128x128@2x.png b/tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5533870654802616903f33b20c8824d04788f8c4 GIT binary patch literal 5967 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 K(LrfBMEnoAi*Tj@ literal 0 HcmV?d00001 diff --git a/tauri/icons/32x32.png b/tauri/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..940409931bd7f0fc488b4d97c392f8260b1a6b21 GIT binary patch literal 820 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 DIm|dL literal 0 HcmV?d00001 diff --git a/tauri/icons/Square150x150Logo.png b/tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4b534f8ac88fadc710309c08bade51752217e06f GIT binary patch literal 3418 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 literal 0 HcmV?d00001 diff --git a/tauri/icons/Square284x284Logo.png b/tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e84f3469e3a242519300cdddedb6f497d28ced6e GIT binary patch literal 6590 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 RCq<_O=-xBbF4MFQ{a=dfD4zfT literal 0 HcmV?d00001 diff --git a/tauri/icons/Square44x44Logo.png b/tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..16f05083e4b516b5e7c613798f5dbaf316321241 GIT binary patch literal 1087 zcmV-F1i<@=P)15 zfRmCGqW~^ri~yuBUT7cHDE%F$5U4odR3Z`903yWdkh=O4vMd{}g7j3aB;LG!4G4Cp z`B0&+;((FeD^~HRPln9_Li*!lW9hO1mL#bgZ@W_v#V)tooz|s5NDMgb=GbK)1U;D=A3S)f#B@ZR0-pp-~#$o+A&xf$Be zUofwsw9D)FM{NhpSWy*aoDkwHuu(q|#VBK4_uWv)wL;YyGS-h<)tNf`QimkjK2bb(A7dgSJr)VOLK1E_4!PW&NSzD3zQe0fz%Ez{C-|@COo;V0Oz=zfi3~b4pCRp?h7Hj`W;m{PN8xLz+y;@N` zm`j%Tet!hT9g~AWkdGfny2TZ3|Ni!-*4D=+{U(zEl*G6}9z&RPS8YwYLRC(;HJ?xS z)^~>w&w!!lMK|c$Gc}dxBGZ!8$9p=fcA~>^q!`db@e0VVa_nH{4DSPhuo0}jv4s?L z7qKWnh01-&fZ3H>8a`_)RW+t=+SJRpkh>-((mYr;w@@EatR#x=C0LQ5iqeK)Ouax| zD&-Uw7Mxt7MLEr0L4S@Qla>{9eM3Ws?JkjJfPb`^T=(pm`C&5<_%=T<5W~KC1PjDW zHo=TN#3Z*zPqCd^X_YRS>>-po8RO0~XIK|!Y9SYI-v-6wv06GSP#SUj{c*jS!tM_f zGvoDoJN{V%&kuBlK4V|7;&#JQXQ!n`b8b?x!hz0^*Fyd{ecGB8O8WpRT z{zCyN*g8snYG!_F)Dw~ATlhOnIX^~cNHg8q1-N7lzxxHEHH&I?_tcaNpo(9&Vut5f z8+8amTe;rRl`M&D>5XGWi$fU)0jRx!uK}(Bt^xkT_z7rht$t(r__F{2002ovPDHLk FV1m6D1@Hg> literal 0 HcmV?d00001 diff --git a/tauri/icons/Square71x71Logo.png b/tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0a978c89921f4d2bd6e73eb22fc8be5f067b3e79 GIT binary patch literal 1709 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 literal 0 HcmV?d00001 diff --git a/tauri/icons/StoreLogo.png b/tauri/icons/StoreLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..74c94dade81b3b78e3289ce07615b3a5fdb39318 GIT binary patch literal 1224 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 Q+ChyMg-Ead<0lXQ54tN2ssI20 literal 0 HcmV?d00001 diff --git a/tauri/icons/icon.ico b/tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df5ab9353b13b28043f92bacc19535ed98180573 GIT binary patch literal 12089 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 QFci{Iy>lb?n)#Fe0m4aA$^ZZW literal 0 HcmV?d00001 diff --git a/tauri/package.json b/tauri/package.json new file mode 100644 index 000000000..e441ebfa7 --- /dev/null +++ b/tauri/package.json @@ -0,0 +1,8 @@ +{ + "name": "tauri", + "private": true, + "scripts": { + "dev": "cargo tauri dev", + "build": "cargo tauri build" + } +} diff --git a/tauri/src/commands/edit.rs b/tauri/src/commands/edit.rs new file mode 100644 index 000000000..c02397403 --- /dev/null +++ b/tauri/src/commands/edit.rs @@ -0,0 +1,246 @@ +//! Editing the metadata, the style, the structure and the waypoints of the files, and the tools: +//! reversing, cropping, reducing, timestamps, merging, cleaning... + +use gpx_engine::{self as engine, Command, LngLat, LngLatBounds}; +use tauri::State; +use uuid::Uuid; + +use crate::dto::*; +use crate::state::{act, edit, AppState}; + +#[tauri::command(async)] +pub fn metadata(state: State, name: String, desc: String) -> Outcome { + edit!( + state, + Command::Metadata(engine::Metadata { + name: &name, + desc: &desc, + }) + ) +} + +#[tauri::command(async)] +pub fn style( + state: State, + color: Option, + opacity: Option, + width: Option, +) -> Outcome { + edit!( + state, + Command::Style(engine::Style { + color: color.as_deref(), + opacity, + width, + }) + ) +} + +#[tauri::command(async)] +pub fn new_track(state: State) -> Outcome { + edit!(state, Command::NewTrack(engine::NewTrack)) +} + +#[tauri::command(async)] +pub fn new_track_segment(state: State) -> Outcome { + edit!(state, Command::NewTrackSegment(engine::NewTrackSegment)) +} + +// Waypoints + +#[tauri::command(async)] +#[allow(clippy::too_many_arguments)] +pub fn new_waypoint( + state: State, + lng: f64, + lat: f64, + ele: f64, + name: String, + desc: String, + icon: String, + link: String, +) -> Outcome { + edit!( + state, + Command::NewWaypoint(engine::NewWaypoint { + lng, + lat, + ele, + name: &name, + desc: &desc, + icon: &icon, + link: &link, + }) + ) +} + +#[tauri::command(async)] +pub fn move_waypoint( + state: State, + file_id: Uuid, + waypoint_id: Uuid, + lng: f64, + lat: f64, + ele: f64, +) -> Outcome { + edit!( + state, + Command::MoveWaypoint(engine::MoveWaypoint { + file_id: engine::FileId(file_id), + waypoint_id: engine::WaypointId(waypoint_id), + lng, + lat, + ele, + }) + ) +} + +#[tauri::command(async)] +pub fn delete_waypoint(state: State, file_id: Uuid, waypoint_id: Uuid) -> Outcome { + edit!( + state, + Command::DeleteWaypoint(engine::DeleteWaypoint { + file_id: engine::FileId(file_id), + waypoint_id: engine::WaypointId(waypoint_id), + }) + ) +} + +#[tauri::command(async)] +#[allow(clippy::too_many_arguments)] +pub fn update_waypoint( + state: State, + file_id: Uuid, + waypoint_id: Uuid, + lng: f64, + lat: f64, + ele: f64, + name: String, + desc: String, + icon: String, + link: String, +) -> Outcome { + edit!( + state, + Command::EditWaypoint(engine::EditWaypoint { + file_id: engine::FileId(file_id), + waypoint_id: engine::WaypointId(waypoint_id), + lng, + lat, + ele, + name: &name, + desc: &desc, + icon: &icon, + link: &link, + }) + ) +} + +// Tools + +#[tauri::command(async)] +pub fn reverse(state: State) -> Outcome { + edit!(state, Command::Reverse(engine::Reverse)) +} + +#[tauri::command(async)] +pub fn round_trip(state: State) -> Outcome { + edit!(state, Command::RoundTrip(engine::RoundTrip)) +} + +#[tauri::command(async)] +pub fn crop(state: State, start: u32, end: u32) -> Outcome { + if start > end { + return Outcome::failed(format!("the range {start}..{end} is reversed")); + } + edit!(state, Command::Crop(engine::Crop { start, end })) +} + +#[tauri::command(async)] +pub fn reduce(state: State, tolerance: f64) -> Outcome { + edit!(state, Command::Reduce(engine::Reduce { tolerance })) +} + +#[tauri::command(async)] +pub fn change_timestamps( + state: State, + start_time: f64, + speed: f64, + ratio: f64, +) -> Outcome { + edit!( + state, + Command::Time(engine::Time { + start_time: start_time as i64, + kind: engine::TimeKind::Change { speed, ratio }, + }) + ) +} + +#[tauri::command(async)] +pub fn create_artificial_timestamps( + state: State, + start_time: f64, + total_time: f64, +) -> Outcome { + edit!( + state, + Command::Time(engine::Time { + start_time: start_time as i64, + kind: engine::TimeKind::Artificial { total_time }, + }) + ) +} + +#[tauri::command(async)] +pub fn merge(state: State, merge_type: MergeType, remove_gaps: bool) -> Outcome { + edit!( + state, + Command::Merge(engine::Merge { + type_: merge_type.into(), + remove_gaps, + }) + ) +} + +#[tauri::command(async)] +pub fn extract(state: State) -> Outcome { + edit!(state, Command::Extract(engine::Extract)) +} + +#[tauri::command(async)] +pub fn elevation(state: State, ele: Vec) -> Outcome { + edit!(state, Command::Elevation(engine::Elevation { ele: &ele })) +} + +#[tauri::command(async)] +#[allow(clippy::too_many_arguments)] +pub fn clean( + state: State, + west: f64, + south: f64, + east: f64, + north: f64, + clean_type: CleanType, + trkpt: bool, + wpt: bool, +) -> Outcome { + edit!( + state, + Command::Clean(engine::Clean { + bounds: LngLatBounds { + sw: LngLat { + lng: west, + lat: south, + }, + ne: LngLat { + lng: east, + lat: north, + }, + }, + type_: clean_type.into(), + trkpt, + wpt, + }) + ) +} diff --git a/tauri/src/commands/files.rs b/tauri/src/commands/files.rs new file mode 100644 index 000000000..b35c96755 --- /dev/null +++ b/tauri/src/commands/files.rs @@ -0,0 +1,161 @@ +//! Creating, loading, deleting, moving, copying and pasting files and their elements, and undoing. + +use gpx_engine::{self as engine, Action, Command}; +use tauri::ipc::{InvokeBody, Request}; +use tauri::State; +use uuid::Uuid; + +use crate::dto::*; +use crate::state::{act, edit, AppState}; + +#[tauri::command(async)] +pub fn new_file( + state: State, + name: String, + lng: Option, + lat: Option, + ele: Option, +) -> Outcome { + let trackpoint = lng.zip(lat).map(|(lng, lat)| engine::NewTrackpoint { + lng, + lat, + ele: ele.unwrap_or_default(), + }); + edit!( + state, + Command::New(engine::New { + name: &name, + trackpoint, + }) + ) +} + +/// The files are in the body, one after the other, the headers `lengths` and `names` (JSON) telling +/// the number of bytes and the name of each one. +#[tauri::command(async)] +pub fn load_files(state: State, request: Request<'_>) -> Outcome { + let InvokeBody::Raw(data) = request.body() else { + return Outcome::failed("the files are missing"); + }; + let header = |key: &str| { + request + .headers() + .get(key) + .and_then(|value| value.to_str().ok()) + .and_then(urlencoding_decode) + }; + let (Some(lengths), Some(names)) = ( + header("lengths").and_then(|json| serde_json::from_str::>(&json).ok()), + header("names").and_then(|json| serde_json::from_str::>(&json).ok()), + ) else { + return Outcome::failed("the lengths or the names of the files are not valid"); + }; + if lengths.len() != names.len() { + return Outcome::failed(format!( + "{} lengths for {} names", + lengths.len(), + names.len() + )); + } + let (data, mut offset) = (data.clone(), 0); + let mut ranges = Vec::with_capacity(names.len()); + for (length, name) in lengths.iter().zip(&names) { + let end = offset + length; + if end > data.len() { + return Outcome::failed(format!("the data of {name} is shorter than its length")); + } + ranges.push(offset..end); + offset = end; + } + edit!(state, { + let files = ranges + .into_iter() + .zip(&names) + .map(|(range, name)| engine::Load { + data: &data[range], + name, + }) + .collect(); + Command::LoadFiles(engine::LoadFiles { files }) + }) +} + +/// Headers are ASCII: the frontend percent-encodes the JSON. +fn urlencoding_decode(value: &str) -> Option { + let bytes = value.as_bytes(); + let mut decoded = Vec::with_capacity(bytes.len()); + let mut i = 0; + while i < bytes.len() { + if bytes[i] == b'%' { + let hex = value.get(i + 1..i + 3)?; + decoded.push(u8::from_str_radix(hex, 16).ok()?); + i += 3; + } else { + decoded.push(bytes[i]); + i += 1; + } + } + String::from_utf8(decoded).ok() +} + +#[tauri::command(async)] +pub fn duplicate(state: State) -> Outcome { + edit!(state, Command::Duplicate(engine::Duplicate)) +} + +#[tauri::command(async)] +pub fn delete(state: State, whole_files: bool) -> Outcome { + edit!(state, Command::Delete(engine::Delete { whole_files })) +} + +#[tauri::command(async)] +pub fn delete_all(state: State) -> Outcome { + edit!(state, Command::DeleteAll(engine::DeleteAll)) +} + +#[tauri::command(async)] +pub fn reorder(state: State, file_ids: Vec, index: usize) -> Outcome { + act!( + state, + Action::Reorder { + file_ids: file_ids.into_iter().map(engine::FileId).collect(), + index, + } + ) +} + +#[tauri::command(async)] +pub fn copy(state: State) -> Outcome { + act!(state, Action::Copy) +} + +#[tauri::command(async)] +pub fn cut(state: State) -> Outcome { + act!(state, Action::Cut) +} + +#[tauri::command(async)] +pub fn paste(state: State) -> Outcome { + edit!(state, Command::Paste(engine::Paste)) +} + +#[tauri::command(async)] +pub fn move_elements(state: State, what: Selection, to: MoveTarget) -> Outcome { + edit!( + state, + Command::Move(engine::Move { + what: what.into(), + to: to.into(), + }) + ) +} + +#[tauri::command(async)] +pub fn undo(state: State) -> Outcome { + act!(state, Action::Undo) +} + +#[tauri::command(async)] +pub fn redo(state: State) -> Outcome { + act!(state, Action::Redo) +} diff --git a/tauri/src/commands/mod.rs b/tauri/src/commands/mod.rs new file mode 100644 index 000000000..9e4936727 --- /dev/null +++ b/tauri/src/commands/mod.rs @@ -0,0 +1,14 @@ +//! The functions of the frontend's interface to the engine, as Tauri commands. They are the +//! counterparts of the functions that `gpx-rs/wasm` exports, and cross the same data: see +//! `website/src/lib/engine/backend`. +//! +//! The commands that edit the files or change the selection return an `Outcome`. +//! They are `async` (so they run off the main thread), the frontend sends them one after the other. + +pub mod edit; +pub mod files; +pub mod queries; +pub mod routing; +pub mod selection; +pub mod statistics; +pub mod storage; diff --git a/tauri/src/commands/queries.rs b/tauri/src/commands/queries.rs new file mode 100644 index 000000000..93d246e48 --- /dev/null +++ b/tauri/src/commands/queries.rs @@ -0,0 +1,136 @@ +//! The files, their structure and statistics, the selection, the clipboard, and the coordinates of +//! the trackpoints and waypoints (flat `[lng, lat, ...]` little-endian `f64` arrays). + +use gpx_engine::{self as engine, FileId}; +use tauri::State; +use uuid::Uuid; + +use crate::dto::*; +use crate::state::AppState; + +#[tauri::command(async)] +pub fn file_order(state: State) -> Vec { + state.read(|e| ids(e.order())) +} + +#[tauri::command(async)] +pub fn file_structure(state: State, file_id: Uuid) -> Option { + state.read(move |e| e.file_structure(&FileId(file_id)).map(FileStructure::new)) +} + +#[tauri::command(async)] +pub fn file_statistics(state: State, file_id: Uuid) -> Option { + state.read(move |e| { + e.file_statistics(&FileId(file_id)) + .map(|s| GlobalStatistics::new(&s)) + }) +} + +#[tauri::command(async)] +pub fn selection(state: State) -> Selection { + state.read(|e| e.selection().into()) +} + +#[tauri::command(async)] +pub fn clipboard(state: State) -> Option { + state.read(|e| e.clipboard().map(Clipboard::new)) +} + +#[tauri::command(async)] +pub fn can_undo(state: State) -> bool { + state.read(|e| e.can_undo()) +} + +#[tauri::command(async)] +pub fn can_redo(state: State) -> bool { + state.read(|e| e.can_redo()) +} + +#[tauri::command(async)] +pub fn can_paste(state: State) -> bool { + state.read(|e| e.can_paste()) +} + +#[tauri::command(async)] +pub fn waypoint( + state: State, + file_id: Uuid, + waypoint_id: Uuid, +) -> Option { + state.read(move |e| { + e.waypoint(&FileId(file_id), &engine::WaypointId(waypoint_id)) + .map(WaypointDetails::new) + }) +} + +#[tauri::command(async)] +pub fn trackpoint( + state: State, + file_id: Uuid, + segment_id: Uuid, + index: usize, +) -> Option { + state.read(move |e| { + e.trackpoint(&FileId(file_id), &engine::TrackSegmentId(segment_id), index) + .map(TrackpointDetails::new) + }) +} + +/// A file as GPX (UTF-8 bytes), the first byte telling whether the file exists. +#[tauri::command(async)] +#[allow(clippy::too_many_arguments)] +pub fn export_file( + state: State, + file_id: Uuid, + time: bool, + hr: bool, + cad: bool, + atemp: bool, + power: bool, + osm: bool, + as_route: bool, +) -> Bytes { + let options = engine::ExportOptions { + time, + hr, + cad, + atemp, + power, + osm, + as_route, + }; + optional(state.read(move |e| e.export(&FileId(file_id), options))) +} + +/// The data that some files have: a bit for the time (1), heart rate (2), cadence (4), temperature +/// (8), power (16) and the OpenStreetMap data (32). +#[tauri::command(async)] +pub fn exportable_data(state: State, file_ids: Vec) -> u8 { + state.read(move |e| { + let ids: Vec = file_ids.into_iter().map(FileId).collect(); + let data = e.exportable_data(&ids); + [ + data.time, data.hr, data.cad, data.atemp, data.power, data.osm, + ] + .iter() + .enumerate() + .fold(0, |bits, (i, on)| bits | (u8::from(*on) << i)) + }) +} + +#[tauri::command(async)] +pub fn reduction_distances(state: State) -> Bytes { + Bytes::new(state.read(|e| f64s(&e.reduction_distances()))) +} + +#[tauri::command(async)] +pub fn segment_coordinates(state: State, segment_id: Uuid) -> Bytes { + Bytes::new( + state.read(move |e| f64s(e.segment_coordinates(&engine::TrackSegmentId(segment_id)))), + ) +} + +#[tauri::command(async)] +pub fn waypoint_coordinates(state: State, file_id: Uuid) -> Bytes { + Bytes::new(state.read(move |e| f64s(e.waypoint_coordinates(&FileId(file_id))))) +} diff --git a/tauri/src/commands/routing.rs b/tauri/src/commands/routing.rs new file mode 100644 index 000000000..56fd9770f --- /dev/null +++ b/tauri/src/commands/routing.rs @@ -0,0 +1,73 @@ +//! The commands that refer to trackpoints by their index in the routing buffers, which only mean +//! something for the `revision` they were read with: the commands do nothing if the selection is +//! not the one of that revision anymore. + +use gpx_engine::{self as engine, Command}; +use tauri::State; + +use crate::dto::*; +use crate::state::{edit_at, AppState}; + +#[tauri::command(async)] +#[allow(clippy::too_many_arguments)] +pub fn route( + state: State, + revision: u32, + start: u32, + end: u32, + lng: Vec, + lat: Vec, + ele: Vec, + attributes: RouteAttributes, + anchors: Vec, +) -> Outcome { + if lng.len() != lat.len() || lat.len() != ele.len() { + return Outcome::failed("lng, lat and ele differ in length"); + } + edit_at!( + state, + revision, + Command::Route(engine::Route { + start, + end, + lng: &lng, + lat: &lat, + ele: &ele, + surface: attributes.surface.view(), + highway: attributes.highway.view(), + sac_scale: attributes.sac_scale.view(), + mtb_scale: attributes.mtb_scale.view(), + anchors: &anchors, + }) + ) +} + +#[tauri::command(async)] +pub fn insert_anchor(state: State, revision: u32, lng: f64, lat: f64) -> Outcome { + edit_at!( + state, + revision, + Command::InsertAnchor(engine::InsertAnchor { lng, lat }) + ) +} + +#[tauri::command(async)] +pub fn change_loop_start(state: State, revision: u32, index: u32) -> Outcome { + edit_at!( + state, + revision, + Command::ChangeLoopStart(engine::ChangeLoopStart { index }) + ) +} + +#[tauri::command(async)] +pub fn split(state: State, revision: u32, at: u32, split_type: SplitType) -> Outcome { + edit_at!( + state, + revision, + Command::Split(engine::Split { + at, + split_type: split_type.into(), + }) + ) +} diff --git a/tauri/src/commands/selection.rs b/tauri/src/commands/selection.rs new file mode 100644 index 000000000..6f7658952 --- /dev/null +++ b/tauri/src/commands/selection.rs @@ -0,0 +1,102 @@ +//! Changing what is selected. + +use gpx_engine::{self as engine, Action}; +use tauri::State; +use uuid::Uuid; + +use crate::dto::*; +use crate::state::{act, AppState}; + +fn select_elements( + state: &AppState, + selection: Option, + mode: SelectMode, +) -> Outcome { + match selection { + Some(selection) => act!( + state, + Action::Select { + selection, + mode: mode.into(), + } + ), + None => Outcome::failed("the ids are not UUIDs"), + } +} + +fn to_file_ids(ids: Vec) -> Vec { + ids.into_iter().map(engine::FileId).collect() +} + +#[tauri::command(async)] +pub fn select(state: State, file_ids: Vec, mode: SelectMode) -> Outcome { + let selection = engine::Selection::File { + file_ids: to_file_ids(file_ids).into_iter().collect(), + }; + select_elements(&state, Some(selection), mode) +} + +#[tauri::command(async)] +pub fn select_all(state: State) -> Outcome { + act!(state, Action::SelectAll) +} + +#[tauri::command(async)] +pub fn arrow_select(state: State, down: bool, add: bool) -> Outcome { + act!(state, Action::ArrowSelect { down, add }) +} + +#[tauri::command(async)] +pub fn select_tracks( + state: State, + file_id: Uuid, + track_ids: Vec, + mode: SelectMode, +) -> Outcome { + let selection = engine::Selection::Track { + file_id: engine::FileId(file_id), + trk_ids: track_ids.into_iter().map(engine::TrackId).collect(), + }; + select_elements(&state, Some(selection), mode) +} + +#[tauri::command(async)] +pub fn select_segments( + state: State, + file_id: Uuid, + track_id: Uuid, + segment_ids: Vec, + mode: SelectMode, +) -> Outcome { + let selection = engine::Selection::TrackSegment { + file_id: engine::FileId(file_id), + trk_id: engine::TrackId(track_id), + trkseg_ids: segment_ids + .into_iter() + .map(engine::TrackSegmentId) + .collect(), + }; + select_elements(&state, Some(selection), mode) +} + +#[tauri::command(async)] +pub fn select_waypoint_group(state: State, file_id: Uuid, mode: SelectMode) -> Outcome { + let selection = engine::Selection::Waypoints { + file_id: engine::FileId(file_id), + }; + select_elements(&state, Some(selection), mode) +} + +#[tauri::command(async)] +pub fn select_waypoints( + state: State, + file_id: Uuid, + waypoint_ids: Vec, + mode: SelectMode, +) -> Outcome { + let selection = engine::Selection::Waypoint { + file_id: engine::FileId(file_id), + wpt_ids: waypoint_ids.into_iter().map(engine::WaypointId).collect(), + }; + select_elements(&state, Some(selection), mode) +} diff --git a/tauri/src/commands/statistics.rs b/tauri/src/commands/statistics.rs new file mode 100644 index 000000000..f5af50774 --- /dev/null +++ b/tauri/src/commands/statistics.rs @@ -0,0 +1,135 @@ +//! The statistics of the selection and the anchors of the routing tool, as little-endian arrays +//! (one per call, copies of the buffers of the engine). Optional ones start with a byte telling +//! whether they are there. + +use tauri::State; + +use crate::dto::*; +use crate::state::AppState; + +macro_rules! f64_getters { + ($($name:ident),*) => {$( + #[tauri::command(async)] + pub fn $name(state: State) -> Bytes { + Bytes::new(state.read(|e| f64s(&e.statistics().$name))) + } + )*}; +} + +f64_getters!( + total_distance, + slope, + slope_segment_slope, + slope_segment_distance, + lng, + lat, + ele +); + +macro_rules! optional_f64_getters { + ($($name:ident),*) => {$( + #[tauri::command(async)] + pub fn $name(state: State) -> Bytes { + optional(state.read(|e| e.statistics().$name.as_deref().map(f64s))) + } + )*}; +} + +optional_f64_getters!(speed, hr, cad, atemp, power); + +/// Timestamps in milliseconds since the epoch (`NO_TIME` when missing), as `i64`. +#[tauri::command(async)] +pub fn timestamps(state: State) -> Bytes { + optional(state.read(|e| e.statistics().time.as_deref().map(i64s))) +} + +#[tauri::command(async)] +pub fn selection_statistics(state: State) -> GlobalStatistics { + state.read(|e| GlobalStatistics::new(&e.statistics().global)) +} + +#[tauri::command(async)] +pub fn slice_statistics( + state: State, + start: usize, + end: usize, +) -> Option { + state.read(move |e| { + e.statistics() + .slice(start, end) + .map(|s| GlobalStatistics::new(&s)) + }) +} + +macro_rules! intervals_getters { + ($starts:ident, $values:ident, $field:ident) => { + #[tauri::command(async)] + pub fn $starts(state: State) -> Bytes { + Bytes::new(state.read(|e| u32s(&e.statistics().$field.starts))) + } + + #[tauri::command(async)] + pub fn $values(state: State) -> Bytes { + Bytes::new(state.read(|e| e.statistics().$field.values.clone())) + } + }; +} + +intervals_getters!(surface_starts, surface_values, surface); +intervals_getters!(highway_starts, highway_values, highway); +intervals_getters!(sac_scale_starts, sac_scale_values, sac_scale); +intervals_getters!(mtb_scale_starts, mtb_scale_values, mtb_scale); + +/// The names of the categories, in the order of their codes. The lists only grow. +#[tauri::command(async)] +pub fn surfaces(state: State) -> Vec { + state.read(|e| e.categories().surface.names().to_vec()) +} + +#[tauri::command(async)] +pub fn highways(state: State) -> Vec { + state.read(|e| e.categories().highway.names().to_vec()) +} + +#[tauri::command(async)] +pub fn sac_scales(state: State) -> Vec { + state.read(|e| e.categories().sac_scale.names().to_vec()) +} + +#[tauri::command(async)] +pub fn mtb_scales(state: State) -> Vec { + state.read(|e| e.categories().mtb_scale.names().to_vec()) +} + +// Routing buffers + +#[tauri::command(async)] +pub fn anchor_indices(state: State) -> Bytes { + Bytes::new(state.read(|e| u32s(&e.routing().anchor_indices))) +} + +#[tauri::command(async)] +pub fn anchor_zooms(state: State) -> Bytes { + Bytes::new(state.read(|e| e.routing().anchor_zooms.clone())) +} + +#[tauri::command(async)] +pub fn segment_starts(state: State) -> Bytes { + Bytes::new(state.read(|e| u32s(&e.routing().segment_starts))) +} + +#[tauri::command(async)] +pub fn segment_ids(state: State) -> Vec { + state.read(|e| { + e.routing() + .segment_ids + .iter() + .map(|id| id.0.to_string()) + .collect() + }) +} + +#[tauri::command(async)] +pub fn routing_revision(state: State) -> u32 { + state.read(|e| e.routing().revision) +} diff --git a/tauri/src/commands/storage.rs b/tauri/src/commands/storage.rs new file mode 100644 index 000000000..15c113289 --- /dev/null +++ b/tauri/src/commands/storage.rs @@ -0,0 +1,46 @@ +//! Keeping the files and the settings. +//! +//! For now nothing is written: the files only live as long as the app, and so do the settings. +//! The file system storage will go here. + +use serde::Serialize; +use tauri::State; + +use crate::dto::Outcome; +use crate::state::AppState; + +/// What opening the storage found. +#[derive(Serialize)] +pub struct StorageOpened { + settings: std::collections::BTreeMap, + outcome: Outcome, +} + +#[tauri::command(async)] +pub fn open_storage(state: State) -> StorageOpened { + StorageOpened { + settings: state.settings().clone(), + outcome: Outcome::failed_none(), + } +} + +/// Resolves when what was changed has been written to the storage. +#[tauri::command(async)] +pub fn flush_storage() {} + +/// Reads the files again if they were written by someone else. +#[tauri::command(async)] +pub fn sync_storage() -> Outcome { + Outcome::failed_none() +} + +/// Saves a setting, `value` being its JSON. +#[tauri::command(async)] +pub fn set_setting(state: State, key: String, value: String) { + state.settings().insert(key, value); +} + +#[tauri::command(async)] +pub fn delete_setting(state: State, key: String) { + state.settings().remove(&key); +} diff --git a/tauri/src/dto.rs b/tauri/src/dto.rs new file mode 100644 index 000000000..f05671fc3 --- /dev/null +++ b/tauri/src/dto.rs @@ -0,0 +1,578 @@ +//! What crosses to the frontend, as JSON: the same shapes as the objects of the WASM interface +//! (`gpx-rs/wasm/src/ts.rs` holds their TypeScript types). + +use gpx_engine::{self as engine, FileId}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// Bytes that cross as they are: typed arrays (little-endian), GPX files. +pub type Bytes = tauri::ipc::Response; + +pub fn f64s(values: &[f64]) -> Vec { + values.iter().flat_map(|v| v.to_le_bytes()).collect() +} + +pub fn i64s(values: &[i64]) -> Vec { + values.iter().flat_map(|v| v.to_le_bytes()).collect() +} + +pub fn u32s(values: &[u32]) -> Vec { + values.iter().flat_map(|v| v.to_le_bytes()).collect() +} + +/// An array that can be absent: the first byte tells whether it is there, the array follows. +pub fn optional(array: Option>) -> Bytes { + let mut bytes = vec![u8::from(array.is_some())]; + bytes.extend(array.unwrap_or_default()); + Bytes::new(bytes) +} + +pub fn ids(items: &[FileId]) -> Vec { + items.iter().map(|id| id.0.to_string()).collect() +} + +// Outcome + +/// What a call that edits the files or changes the selection did. +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Outcome { + changed: bool, + order_changed: bool, + selection_changed: bool, + clipboard_changed: bool, + added: Vec, + modified: Vec, + removed: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, +} + +impl Outcome { + pub fn new(outcome: &engine::Outcome, error: Option) -> Self { + let diff = outcome.diff.clone().unwrap_or_default(); + Self { + changed: outcome.changed, + order_changed: outcome.order_changed, + selection_changed: outcome.selection_changed, + clipboard_changed: outcome.clipboard_changed, + added: ids(&diff.added), + modified: ids(&diff.modified), + removed: ids(&diff.removed), + error, + } + } + + /// The outcome of a call that did nothing and has nothing to say. + pub fn failed_none() -> Self { + Self::new(&engine::Outcome::default(), None) + } + + /// What a call that does nothing returns. + pub fn failed(message: impl Into) -> Self { + Self::new(&engine::Outcome::default(), Some(message.into())) + } +} + +// Selection and clipboard + +#[derive(Serialize, Deserialize)] +#[serde( + tag = "type", + rename_all = "camelCase", + rename_all_fields = "camelCase" +)] +pub enum Selection { + Empty, + File { + file_ids: Vec, + }, + Track { + file_id: Uuid, + track_ids: Vec, + }, + Segment { + file_id: Uuid, + track_id: Uuid, + segment_ids: Vec, + }, + Waypoints { + file_id: Uuid, + }, + Waypoint { + file_id: Uuid, + waypoint_ids: Vec, + }, +} + +impl From<&engine::Selection> for Selection { + fn from(selection: &engine::Selection) -> Self { + use engine::Selection as S; + match selection { + S::Empty => Self::Empty, + S::File { file_ids } => Self::File { + file_ids: file_ids.iter().map(|id| id.0).collect(), + }, + S::Track { file_id, trk_ids } => Self::Track { + file_id: file_id.0, + track_ids: trk_ids.iter().map(|id| id.0).collect(), + }, + S::TrackSegment { + file_id, + trk_id, + trkseg_ids, + } => Self::Segment { + file_id: file_id.0, + track_id: trk_id.0, + segment_ids: trkseg_ids.iter().map(|id| id.0).collect(), + }, + S::Waypoints { file_id } => Self::Waypoints { file_id: file_id.0 }, + S::Waypoint { file_id, wpt_ids } => Self::Waypoint { + file_id: file_id.0, + waypoint_ids: wpt_ids.iter().map(|id| id.0).collect(), + }, + } + } +} + +impl From for engine::Selection { + fn from(selection: Selection) -> Self { + use engine::Selection as S; + match selection { + Selection::Empty => S::Empty, + Selection::File { file_ids } => S::File { + file_ids: file_ids.into_iter().map(FileId).collect(), + }, + Selection::Track { file_id, track_ids } => S::Track { + file_id: FileId(file_id), + trk_ids: track_ids.into_iter().map(engine::TrackId).collect(), + }, + Selection::Segment { + file_id, + track_id, + segment_ids, + } => S::TrackSegment { + file_id: FileId(file_id), + trk_id: engine::TrackId(track_id), + trkseg_ids: segment_ids + .into_iter() + .map(engine::TrackSegmentId) + .collect(), + }, + Selection::Waypoints { file_id } => S::Waypoints { + file_id: FileId(file_id), + }, + Selection::Waypoint { + file_id, + waypoint_ids, + } => S::Waypoint { + file_id: FileId(file_id), + wpt_ids: waypoint_ids.into_iter().map(engine::WaypointId).collect(), + }, + } + } +} + +/// Where moved elements go. A negative index is the start, an infinite one the end (which JSON +/// writes as `null`, so the frontend sends a very large number instead). +#[derive(Deserialize)] +#[serde( + tag = "type", + rename_all = "camelCase", + rename_all_fields = "camelCase" +)] +pub enum MoveTarget { + Files { + index: f64, + }, + Tracks { + file_id: Uuid, + index: f64, + }, + Segments { + file_id: Uuid, + track_id: Uuid, + index: f64, + }, + Waypoints { + file_id: Uuid, + index: f64, + }, +} + +impl From for engine::MoveTarget { + fn from(target: MoveTarget) -> Self { + use engine::MoveTarget as T; + let index = |index: f64| index.max(0.0) as usize; + match target { + MoveTarget::Files { index: i } => T::Files { index: index(i) }, + MoveTarget::Tracks { file_id, index: i } => T::Tracks { + file_id: FileId(file_id), + index: index(i), + }, + MoveTarget::Segments { + file_id, + track_id, + index: i, + } => T::Segments { + file_id: FileId(file_id), + trk_id: engine::TrackId(track_id), + index: index(i), + }, + MoveTarget::Waypoints { file_id, index: i } => T::Waypoints { + file_id: FileId(file_id), + index: index(i), + }, + } + } +} + +#[derive(Serialize)] +pub struct Clipboard { + #[serde(rename = "type")] + kind: &'static str, + ids: Vec, + cut: bool, +} + +impl Clipboard { + pub fn new(clipboard: &engine::Clipboard) -> Self { + let strings = |ids: Vec| ids.iter().map(Uuid::to_string).collect(); + let (kind, ids) = match clipboard.content.ids() { + engine::ClipboardIds::Files(ids) => { + ("files", strings(ids.into_iter().map(|id| id.0).collect())) + } + engine::ClipboardIds::Tracks(ids) => { + ("tracks", strings(ids.into_iter().map(|id| id.0).collect())) + } + engine::ClipboardIds::Segments(ids) => ( + "segments", + strings(ids.into_iter().map(|id| id.0).collect()), + ), + engine::ClipboardIds::Waypoints(ids) => ( + "waypoints", + strings(ids.into_iter().map(|id| id.0).collect()), + ), + }; + Self { + kind, + ids, + cut: clipboard.cut, + } + } +} + +// Files + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FileStructure { + id: String, + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + desc: Option, + tracks: Vec, + waypoints: Vec, + waypoints_rev: String, +} + +#[derive(Serialize)] +struct TrackNode { + id: String, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + desc: Option, + #[serde(skip_serializing_if = "Option::is_none")] + color: Option, + #[serde(skip_serializing_if = "Option::is_none")] + opacity: Option, + #[serde(skip_serializing_if = "Option::is_none")] + width: Option, + segments: Vec, +} + +#[derive(Serialize)] +struct SegmentNode { + id: String, + rev: String, + length: f64, +} + +#[derive(Serialize)] +struct WaypointNode { + id: String, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + sym: Option, +} + +impl FileStructure { + pub fn new(file: &engine::FileStructure) -> Self { + Self { + id: file.id.0.to_string(), + name: file.name.clone(), + desc: file.desc.clone(), + tracks: file + .tracks + .iter() + .map(|trk| TrackNode { + id: trk.id.0.to_string(), + name: trk.name.clone(), + desc: trk.desc.clone(), + color: trk.color.clone(), + opacity: trk.opacity, + width: trk.width, + segments: trk + .segments + .iter() + .map(|seg| SegmentNode { + id: seg.id.0.to_string(), + rev: seg.rev_id.0.to_string(), + length: seg.len as f64, + }) + .collect(), + }) + .collect(), + waypoints: file + .waypoints + .iter() + .map(|wpt| WaypointNode { + id: wpt.id.0.to_string(), + name: wpt.name.clone(), + sym: wpt.sym.clone(), + }) + .collect(), + waypoints_rev: file.wpt_rev_id.0.to_string(), + } + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GlobalStatistics { + total_distance: f64, + #[serde(skip_serializing_if = "Option::is_none")] + moving_distance: Option, + #[serde(skip_serializing_if = "Option::is_none")] + total_time: Option, + #[serde(skip_serializing_if = "Option::is_none")] + moving_time: Option, + elevation_gain: f64, + elevation_loss: f64, + #[serde(skip_serializing_if = "Option::is_none")] + start_time: Option, + #[serde(skip_serializing_if = "Option::is_none")] + end_time: Option, + #[serde(skip_serializing_if = "Option::is_none")] + total_speed: Option, + #[serde(skip_serializing_if = "Option::is_none")] + moving_speed: Option, + #[serde(skip_serializing_if = "Option::is_none")] + bounds: Option, +} + +#[derive(Serialize)] +struct Bounds { + west: f64, + south: f64, + east: f64, + north: f64, +} + +impl GlobalStatistics { + pub fn new(stats: &engine::GlobalStatistics) -> Self { + let seconds = |ms: Option| ms.map(|ms| ms as f64 / 1000.0); + let (sw, ne) = (&stats.bounds.sw, &stats.bounds.ne); + Self { + total_distance: stats.total_distance, + moving_distance: stats.moving_distance, + total_time: seconds(stats.total_time), + moving_time: seconds(stats.moving_time), + elevation_gain: stats.elevation_gain, + elevation_loss: stats.elevation_loss, + start_time: stats.start_time.map(|t| t as f64), + end_time: stats.end_time.map(|t| t as f64), + total_speed: stats.total_speed(), + moving_speed: stats.moving_speed(), + bounds: (sw.lng <= ne.lng && sw.lat <= ne.lat).then_some(Bounds { + west: sw.lng, + south: sw.lat, + east: ne.lng, + north: ne.lat, + }), + } + } +} + +#[derive(Serialize)] +pub struct WaypointDetails { + id: String, + lng: f64, + lat: f64, + ele: f64, + #[serde(skip_serializing_if = "Option::is_none")] + time: Option, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + desc: Option, + #[serde(skip_serializing_if = "Option::is_none")] + cmt: Option, + #[serde(skip_serializing_if = "Option::is_none")] + sym: Option, + #[serde(skip_serializing_if = "Option::is_none")] + links: Option>, +} + +#[derive(Serialize)] +struct Link { + href: String, + #[serde(skip_serializing_if = "Option::is_none")] + text: Option, +} + +impl WaypointDetails { + pub fn new(wpt: &engine::Waypoint) -> Self { + Self { + id: wpt.id.0.to_string(), + lng: wpt.coordinates.lng, + lat: wpt.coordinates.lat, + ele: wpt.ele, + time: wpt.time.map(|t| t as f64), + name: wpt.name.clone(), + desc: wpt.desc.clone(), + cmt: wpt.cmt.clone(), + sym: wpt.sym.clone(), + links: (!wpt.links.is_empty()).then(|| { + wpt.links + .iter() + .map(|link| Link { + href: link.href.clone(), + text: link.text.clone(), + }) + .collect() + }), + } + } +} + +#[derive(Serialize)] +pub struct TrackpointDetails { + lng: f64, + lat: f64, + ele: f64, + #[serde(skip_serializing_if = "Option::is_none")] + time: Option, +} + +impl TrackpointDetails { + pub fn new(pt: &engine::Trackpoint) -> Self { + Self { + lng: pt.coordinates.lng, + lat: pt.coordinates.lat, + ele: pt.ele, + time: pt.time.map(|t| t as f64), + } + } +} + +// Enumerations + +#[derive(Clone, Copy, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum SelectMode { + Replace, + Add, + Toggle, +} + +impl From for engine::SelectMode { + fn from(m: SelectMode) -> Self { + match m { + SelectMode::Replace => Self::Replace, + SelectMode::Add => Self::Add, + SelectMode::Toggle => Self::Toggle, + } + } +} + +#[derive(Clone, Copy, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum MergeType { + Connect, + Group, +} + +impl From for engine::MergeType { + fn from(t: MergeType) -> Self { + match t { + MergeType::Connect => Self::Connect, + MergeType::Group => Self::Group, + } + } +} + +#[derive(Clone, Copy, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum SplitType { + Files, + Tracks, + Segments, +} + +impl From for engine::SplitType { + fn from(t: SplitType) -> Self { + match t { + SplitType::Files => Self::Files, + SplitType::Tracks => Self::Tracks, + SplitType::Segments => Self::Segments, + } + } +} + +#[derive(Clone, Copy, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum CleanType { + Inside, + Outside, +} + +impl From for engine::CleanType { + fn from(t: CleanType) -> Self { + match t { + CleanType::Inside => Self::Inside, + CleanType::Outside => Self::Outside, + } + } +} + +// Routing + +/// A category of the trackpoints of a route, as in `RouteCategory` of the engine. +#[derive(Default, Deserialize)] +pub struct RouteCategory { + pub starts: Vec, + pub values: Vec, + pub names: Vec, +} + +impl RouteCategory { + pub fn view(&self) -> engine::RouteCategory<'_> { + engine::RouteCategory { + starts: &self.starts, + values: &self.values, + names: &self.names, + } + } +} + +#[derive(Default, Deserialize)] +#[serde(rename_all = "camelCase", default)] +pub struct RouteAttributes { + pub surface: RouteCategory, + pub highway: RouteCategory, + pub sac_scale: RouteCategory, + pub mtb_scale: RouteCategory, +} diff --git a/tauri/src/lib.rs b/tauri/src/lib.rs new file mode 100644 index 000000000..176905878 --- /dev/null +++ b/tauri/src/lib.rs @@ -0,0 +1,125 @@ +//! The desktop shell of gpx.studio. The engine of the files (`gpx-rs/engine`) runs in this +//! process, and the frontend talks to it through the commands of `commands`. + +mod commands; +mod dto; +mod state; + +use commands::*; + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + with_engine(tauri::Builder::default()) + .run(tauri::generate_context!()) + .expect("error while running gpx.studio"); +} + +/// Gives the builder the engine and the commands that reach it. +fn with_engine(builder: tauri::Builder) -> tauri::Builder { + builder + .manage(state::AppState::default()) + .invoke_handler(tauri::generate_handler![ + // selection + selection::select, + selection::select_all, + selection::arrow_select, + selection::select_tracks, + selection::select_segments, + selection::select_waypoint_group, + selection::select_waypoints, + // files + files::new_file, + files::load_files, + files::duplicate, + files::delete, + files::delete_all, + files::reorder, + files::copy, + files::cut, + files::paste, + files::move_elements, + files::undo, + files::redo, + // edits and tools + edit::metadata, + edit::style, + edit::new_track, + edit::new_track_segment, + edit::new_waypoint, + edit::move_waypoint, + edit::delete_waypoint, + edit::update_waypoint, + edit::reverse, + edit::round_trip, + edit::crop, + edit::reduce, + edit::change_timestamps, + edit::create_artificial_timestamps, + edit::merge, + edit::extract, + edit::elevation, + edit::clean, + routing::route, + routing::insert_anchor, + routing::change_loop_start, + routing::split, + // queries + queries::file_order, + queries::file_structure, + queries::file_statistics, + queries::selection, + queries::clipboard, + queries::can_undo, + queries::can_redo, + queries::can_paste, + queries::waypoint, + queries::trackpoint, + queries::export_file, + queries::exportable_data, + queries::reduction_distances, + queries::segment_coordinates, + queries::waypoint_coordinates, + // statistics + statistics::total_distance, + statistics::slope, + statistics::slope_segment_slope, + statistics::slope_segment_distance, + statistics::lng, + statistics::lat, + statistics::ele, + statistics::speed, + statistics::hr, + statistics::cad, + statistics::atemp, + statistics::power, + statistics::timestamps, + statistics::selection_statistics, + statistics::slice_statistics, + statistics::surface_starts, + statistics::surface_values, + statistics::highway_starts, + statistics::highway_values, + statistics::sac_scale_starts, + statistics::sac_scale_values, + statistics::mtb_scale_starts, + statistics::mtb_scale_values, + statistics::surfaces, + statistics::highways, + statistics::sac_scales, + statistics::mtb_scales, + statistics::anchor_indices, + statistics::anchor_zooms, + statistics::segment_starts, + statistics::segment_ids, + statistics::routing_revision, + // storage + storage::open_storage, + storage::flush_storage, + storage::sync_storage, + storage::set_setting, + storage::delete_setting, + ]) +} + +#[cfg(test)] +mod tests; diff --git a/tauri/src/main.rs b/tauri/src/main.rs new file mode 100644 index 000000000..90eb935a8 --- /dev/null +++ b/tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents an additional console window on Windows in release. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + gpx_studio_desktop_lib::run() +} diff --git a/tauri/src/state.rs b/tauri/src/state.rs new file mode 100644 index 000000000..54f6477dd --- /dev/null +++ b/tauri/src/state.rs @@ -0,0 +1,107 @@ +//! The state the commands share: the engine, and the settings of the frontend. +//! +//! The engine is not `Send` (it shares its data with `Rc`), so it lives on a thread of its own and +//! the commands send it jobs: they are done one after the other, in the order they were sent. + +use std::collections::BTreeMap; +use std::sync::mpsc::{self, Sender}; +use std::sync::{Mutex, MutexGuard}; +use std::thread; + +use gpx_engine::{Action, Engine}; + +use crate::dto::Outcome; + +type Job = Box; + +/// What the commands talk to. The engine is the same as in the browser (`gpx-rs`); here it lives in +/// the process of the app, and the frontend reaches it through the commands only. +pub struct AppState { + jobs: Mutex>, + /// The settings of the frontend, as JSON strings by key. + settings: Mutex>, +} + +impl Default for AppState { + fn default() -> Self { + let (jobs, receiver) = mpsc::channel::(); + thread::Builder::new() + .name("engine".into()) + .spawn(move || { + let mut engine = Engine::default(); + while let Ok(job) = receiver.recv() { + job(&mut engine); + } + }) + .expect("the engine thread could not be started"); + Self { + jobs: Mutex::new(jobs), + settings: Mutex::default(), + } + } +} + +impl AppState { + /// Runs `f` on the engine and waits for its result. + pub fn run(&self, f: impl FnOnce(&mut Engine) -> T + Send + 'static) -> T { + let (reply, result) = mpsc::channel(); + let job: Job = Box::new(move |engine| { + let _ = reply.send(f(engine)); + }); + self.jobs + .lock() + .unwrap() + .send(job) + .expect("the engine thread stopped"); + result.recv().expect("the engine thread stopped") + } + + /// Reads from the engine. + pub fn read(&self, f: impl FnOnce(&Engine) -> T + Send + 'static) -> T { + self.run(move |engine| f(engine)) + } + + pub fn settings(&self) -> MutexGuard<'_, BTreeMap> { + self.settings.lock().unwrap() + } +} + +/// Executes an action and tells what it did, see `Outcome`. +pub fn execute(engine: &mut Engine, action: Action) -> Outcome { + match engine.execute(action) { + Ok(outcome) => Outcome::new(&outcome, None), + Err(error) => Outcome::failed(error.to_string()), + } +} + +/// Executes an action on the engine. The values the action uses are moved to the engine thread. +macro_rules! act { + ($state:expr, $action:expr) => { + $state.run(move |engine| $crate::state::execute(engine, $action)) + }; +} + +/// Executes a command (the command can borrow what it uses). +macro_rules! edit { + ($state:expr, $command:expr) => { + act!($state, ::gpx_engine::Action::Edit($command)) + }; +} + +/// Like `edit!`, but does nothing if the selection is not the one of `revision` anymore (the +/// indices of the routing buffers only mean something for their revision). +macro_rules! edit_at { + ($state:expr, $revision:expr, $command:expr) => {{ + let revision = $revision; + $state.run(move |engine| { + if engine.routing().revision != revision { + return $crate::dto::Outcome::failed( + "the selection changed since the routing buffers were read", + ); + } + $crate::state::execute(engine, ::gpx_engine::Action::Edit($command)) + }) + }}; +} + +pub(crate) use {act, edit, edit_at}; diff --git a/tauri/src/tests.rs b/tauri/src/tests.rs new file mode 100644 index 000000000..2fb486a70 --- /dev/null +++ b/tauri/src/tests.rs @@ -0,0 +1,274 @@ +//! The commands, called the way the frontend calls them (through the IPC of Tauri), on the sample +//! files of the engine. + +use serde_json::{json, Value}; +use tauri::test::{get_ipc_response, mock_builder, mock_context, noop_assets, INVOKE_KEY}; +use tauri::webview::InvokeRequest; +use tauri::{ipc::CallbackFn, ipc::InvokeBody, ipc::InvokeResponseBody, WebviewWindow}; + +struct App(WebviewWindow); + +impl App { + fn new() -> Self { + let app = crate::with_engine(mock_builder()) + .build(mock_context(noop_assets())) + .unwrap(); + let window = tauri::WebviewWindowBuilder::new(&app, "main", Default::default()) + .build() + .unwrap(); + // the app has to outlive its window + std::mem::forget(app); + Self(window) + } + + fn request( + &self, + cmd: &str, + body: InvokeBody, + headers: tauri::http::HeaderMap, + ) -> InvokeResponseBody { + get_ipc_response( + &self.0, + InvokeRequest { + cmd: cmd.into(), + callback: CallbackFn(0), + error: CallbackFn(1), + url: "tauri://localhost".parse().unwrap(), + body, + headers, + invoke_key: INVOKE_KEY.to_string(), + }, + ) + .unwrap_or_else(|error| panic!("{cmd} failed: {error}")) + } + + fn call(&self, cmd: &str, args: Value) -> Value { + match self.request(cmd, InvokeBody::Json(args), Default::default()) { + InvokeResponseBody::Json(json) => serde_json::from_str(&json).unwrap(), + InvokeResponseBody::Raw(_) => panic!("{cmd} returned bytes"), + } + } + + fn bytes(&self, cmd: &str, args: Value) -> Vec { + match self.request(cmd, InvokeBody::Json(args), Default::default()) { + InvokeResponseBody::Raw(bytes) => bytes, + InvokeResponseBody::Json(_) => panic!("{cmd} returned JSON"), + } + } + + fn load(&self, names: &[&str]) -> Value { + let mut data = vec![]; + let mut lengths = vec![]; + for name in names { + let file = std::fs::read(format!("../gpx-rs/engine/data/{name}.gpx")).unwrap(); + lengths.push(file.len()); + data.extend(file); + } + let header = |value: Value| value.to_string().replace('"', "%22").parse().unwrap(); + let mut headers = tauri::http::HeaderMap::new(); + headers.insert("lengths", header(json!(lengths))); + headers.insert("names", header(json!(names))); + match self.request("load_files", InvokeBody::Raw(data), headers) { + InvokeResponseBody::Json(json) => serde_json::from_str(&json).unwrap(), + InvokeResponseBody::Raw(_) => panic!("load_files returned bytes"), + } + } +} + +fn f64s(bytes: &[u8]) -> Vec { + bytes + .chunks_exact(8) + .map(|chunk| f64::from_le_bytes(chunk.try_into().unwrap())) + .collect() +} + +#[test] +fn loads_files_and_describes_them() { + let app = App::new(); + let outcome = app.load(&["simple", "with_tracks"]); + assert_eq!(outcome["changed"], true); + assert_eq!(outcome["added"].as_array().unwrap().len(), 2); + assert!(outcome.get("error").is_none()); + + let order = app.call("file_order", json!({})); + let first = order[0].as_str().unwrap(); + assert_eq!( + app.call("selection", json!({})), + json!({ "type": "file", "fileIds": [first] }) + ); + + let structure = app.call("file_structure", json!({ "fileId": first })); + assert_eq!(structure["name"], "simple"); + let segment = &structure["tracks"][0]["segments"][0]; + let length = segment["length"].as_f64().unwrap() as usize; + assert!(length > 0); + + // coordinates are little-endian doubles, lng and lat for each trackpoint + let coordinates = + f64s(&app.bytes("segment_coordinates", json!({ "segmentId": segment["id"] }))); + assert_eq!(coordinates.len(), 2 * length); + + let statistics = app.call("file_statistics", json!({ "fileId": first })); + assert!(statistics["totalDistance"].as_f64().unwrap() > 0.0); + assert!( + statistics["bounds"]["west"].as_f64().unwrap() + <= statistics["bounds"]["east"].as_f64().unwrap() + ); + assert_eq!( + app.call( + "file_structure", + json!({ "fileId": "00000000-0000-0000-0000-000000000000" }) + ), + Value::Null + ); +} + +#[test] +fn reads_the_statistics_of_the_selection() { + let app = App::new(); + app.load(&["simple"]); + let distance = f64s(&app.bytes("total_distance", json!({}))); + assert!(distance.len() > 1); + assert_eq!(f64s(&app.bytes("lng", json!({}))).len(), distance.len()); + assert!( + app.call("selection_statistics", json!({}))["totalDistance"] + .as_f64() + .unwrap() + > 0.0 + ); + // optional arrays start with a byte telling whether they are there + let hr = app.bytes("hr", json!({})); + assert_eq!(hr[0], 0); + assert!( + app.call("slice_statistics", json!({ "start": 1, "end": 3 }))["totalDistance"].is_number() + ); + assert_eq!( + app.call("slice_statistics", json!({ "start": 0, "end": 100000 })), + Value::Null + ); + + app.load(&["with_hr"]); + app.call("select_all", json!({})); + let hr = app.bytes("hr", json!({})); + assert_eq!(hr[0], 1); + assert_eq!((hr.len() - 1) % 8, 0); +} + +#[test] +fn edits_undoes_and_selects() { + let app = App::new(); + let outcome = app.call( + "new_file", + json!({ "name": "route", "lng": 4.4, "lat": 50.8 }), + ); + assert_eq!(outcome["changed"], true); + assert_eq!(app.call("can_undo", json!({})), true); + + let id = app.call("file_order", json!({}))[0].clone(); + let outcome = app.call("metadata", json!({ "name": "renamed", "desc": "" })); + assert_eq!(outcome["modified"], json!([id])); + assert_eq!( + app.call("file_structure", json!({ "fileId": id }))["name"], + "renamed" + ); + + let outcome = app.call("select", json!({ "fileIds": [], "mode": "replace" })); + assert_eq!(outcome["selectionChanged"], true); + assert_eq!(app.call("selection", json!({})), json!({ "type": "empty" })); + app.call("select", json!({ "fileIds": [id], "mode": "toggle" })); + + app.call("undo", json!({})); + assert_eq!( + app.call("file_structure", json!({ "fileId": id }))["name"], + "route" + ); + assert_eq!(app.call("can_redo", json!({})), true); + app.call("undo", json!({})); + assert_eq!(app.call("file_order", json!({})), json!([])); + + // what cannot be done says why + let outcome = app.call("reverse", json!({})); + assert_eq!(outcome["changed"], false); + assert!(outcome["error"].is_string()); +} + +#[test] +fn moves_and_clipboard() { + let app = App::new(); + app.load(&["simple", "with_tracks"]); + let ids = app.call("file_order", json!({})); + app.call("select", json!({ "fileIds": [ids[0]], "mode": "replace" })); + app.call("copy", json!({})); + assert_eq!( + app.call("clipboard", json!({})), + json!({ "type": "files", "ids": [ids[0]], "cut": false }) + ); + + let outcome = app.call( + "move_elements", + json!({ + "what": { "type": "file", "fileIds": [ids[0]] }, + "to": { "type": "files", "index": 9007199254740991u64 }, + }), + ); + assert_eq!(outcome["orderChanged"], true); + assert_eq!(app.call("file_order", json!({})), json!([ids[1], ids[0]])); +} + +#[test] +fn waypoints_exports_and_routing_revisions() { + let app = App::new(); + app.call("new_file", json!({ "name": "file" })); + let outcome = app.call( + "new_waypoint", + json!({ "lng": 4.4, "lat": 50.8, "ele": 100, "name": "summit", "desc": "", "icon": "Summit", "link": "https://example.com" }), + ); + assert_eq!(outcome["changed"], true); + let file = app.call("file_order", json!({}))[0].clone(); + let waypoint = + app.call("file_structure", json!({ "fileId": file }))["waypoints"][0]["id"].clone(); + let details = app.call( + "waypoint", + json!({ "fileId": file, "waypointId": waypoint }), + ); + assert_eq!(details["name"], "summit"); + assert_eq!(details["sym"], "Summit"); + assert_eq!(details["links"][0]["href"], "https://example.com"); + + let exported = app.bytes( + "export_file", + json!({ "fileId": file, "time": true, "hr": true, "cad": true, "atemp": true, "power": true, "osm": true, "asRoute": false }), + ); + assert_eq!(exported[0], 1); + assert!(String::from_utf8(exported[1..].to_vec()) + .unwrap() + .contains(" { + if (global) { + this._slicedStatistics.set({ global, start, end }); + } + }); } } } diff --git a/website/src/lib/components/export/Export.svelte b/website/src/lib/components/export/Export.svelte index 65dd8589b..61317d469 100644 --- a/website/src/lib/components/export/Export.svelte +++ b/website/src/lib/components/export/Export.svelte @@ -54,18 +54,32 @@ return []; }); // the data that none of them has is not offered - let hide: Record = $derived.by(() => { - const available = engine.exportableData(fileIds); - return { - time: !available.time, - hr: !available.hr, - cad: !available.cad, - atemp: !available.atemp, - power: !available.power, - osm: !available.osm, - // always possible - asRoute: false, - }; + let hide: Record = $state({ + time: false, + hr: false, + cad: false, + atemp: false, + power: false, + osm: false, + asRoute: false, + }); + $effect(() => { + const ids = fileIds; + engine.exportableData(ids).then((available) => { + // the files may have changed since + if (ids === fileIds) { + hide = { + time: !available.time, + hr: !available.hr, + cad: !available.cad, + atemp: !available.atemp, + power: !available.power, + osm: !available.osm, + // always possible + asRoute: false, + }; + } + }); }); $effect(() => { diff --git a/website/src/lib/components/export/utils.svelte.ts b/website/src/lib/components/export/utils.svelte.ts index 81065509d..74966d38e 100644 --- a/website/src/lib/components/export/utils.svelte.ts +++ b/website/src/lib/components/export/utils.svelte.ts @@ -31,11 +31,15 @@ export function allFileIds(): string[] { } async function exportFiles(fileIds: string[], options: ExportOptions) { - const files = fileIds.flatMap((fileId) => { - const gpx = engine.exportFile(fileId, options); - const name = get(engine.files).get(fileId); - return gpx === undefined || !name ? [] : [{ name: get(name).structure.name, gpx }]; - }); + const files = ( + await Promise.all( + fileIds.map(async (fileId) => { + const gpx = await engine.exportFile(fileId, options); + const name = get(engine.files).get(fileId); + return gpx === undefined || !name ? [] : [{ name: get(name).structure.name, gpx }]; + }) + ) + ).flat(); if (files.length === 1) { const blob = new Blob([files[0].gpx], { type: 'application/gpx+xml' }); diff --git a/website/src/lib/components/file-list/FileListNodeLabel.svelte b/website/src/lib/components/file-list/FileListNodeLabel.svelte index ad570243e..459726a6b 100644 --- a/website/src/lib/components/file-list/FileListNodeLabel.svelte +++ b/website/src/lib/components/file-list/FileListNodeLabel.svelte @@ -152,9 +152,9 @@ engine.selectNode(node, 'toggle'); } }} - onmouseenter={() => { + onmouseenter={async () => { if (node.type === 'waypoint' && !isHidden(fileState, node.waypointId)) { - const waypoint = engine.waypoint(node.fileId, node.waypointId); + const waypoint = await engine.waypoint(node.fileId, node.waypointId); if (waypoint) { waypointPopup?.setItem({ item: waypoint, diff --git a/website/src/lib/components/map/gpx-layer/gpx-layer.ts b/website/src/lib/components/map/gpx-layer/gpx-layer.ts index 9937c6909..e904ebe65 100644 --- a/website/src/lib/components/map/gpx-layer/gpx-layer.ts +++ b/website/src/lib/components/map/gpx-layer/gpx-layer.ts @@ -393,13 +393,15 @@ export class GPXLayer { mapCursor.notify(MapCursorState.LAYER_HOVER, false); } - layerOnMouseMove(e: any) { + async layerOnMouseMove(e: any) { if (e.originalEvent.shiftKey) { const { segmentId } = e.features[0].properties; const lngLat = { lng: e.lngLat.lng, lat: e.lngLat.lat }; - const index = closestPointIndex(engine.segmentCoordinates(segmentId), lngLat); + const index = closestPointIndex(await engine.segmentCoordinates(segmentId), lngLat); const trackpoint = - index === undefined ? undefined : engine.trackpoint(this.fileId, segmentId, index); + index === undefined + ? undefined + : await engine.trackpoint(this.fileId, segmentId, index); if (trackpoint) { trackpointPopup?.setItem({ item: trackpoint, @@ -469,8 +471,8 @@ export class GPXLayer { mapCursor.notify(MapCursorState.WAYPOINT_HOVER, true); } - showWaypointPopup(waypointId: string) { - const waypoint = engine.waypoint(this.fileId, waypointId); + async showWaypointPopup(waypointId: string) { + const waypoint = await engine.waypoint(this.fileId, waypointId); if (waypoint) { waypointPopup?.setItem({ item: waypoint, kind: 'waypoint', fileId: this.fileId }); } diff --git a/website/src/lib/components/toolbar/tools/reduce/Reduce.svelte b/website/src/lib/components/toolbar/tools/reduce/Reduce.svelte index 32023891c..207eea23e 100644 --- a/website/src/lib/components/toolbar/tools/reduce/Reduce.svelte +++ b/website/src/lib/components/toolbar/tools/reduce/Reduce.svelte @@ -30,9 +30,14 @@ let reducedLayer = new ReducedLayer(); // read again only when the selection or the files change, not with the tolerance - let distances = $derived.by(() => { - void $statistics; - return engine.reductionDistances(); + let distances = $state(new Float64Array()); + $effect(() => { + const current = $statistics; + engine.reductionDistances().then((values) => { + if (current === $statistics) { + distances = values; + } + }); }); $effect(() => { diff --git a/website/src/lib/components/toolbar/tools/scissors/Scissors.svelte b/website/src/lib/components/toolbar/tools/scissors/Scissors.svelte index a50de3519..8cc8a1623 100644 --- a/website/src/lib/components/toolbar/tools/scissors/Scissors.svelte +++ b/website/src/lib/components/toolbar/tools/scissors/Scissors.svelte @@ -61,8 +61,12 @@ function updateSlicedStatistics([startStep, endStep]: number[]) { if (validSelection && (startStep !== 0 || endStep !== steps)) { const [start, end] = [toIndex(startStep), toIndex(endStep)]; - const global = $statistics.slice(start, end); - slicedStatistics.set(global && { global, start, end }); + $statistics.slice(start, end).then((global) => { + // the slider may have moved since + if (validSelection) { + slicedStatistics.set(global && { global, start, end }); + } + }); } else { slicedStatistics.set(undefined); } diff --git a/website/src/lib/components/toolbar/tools/waypoint/waypoint.ts b/website/src/lib/components/toolbar/tools/waypoint/waypoint.ts index 9692bbfb4..78eaa240e 100644 --- a/website/src/lib/components/toolbar/tools/waypoint/waypoint.ts +++ b/website/src/lib/components/toolbar/tools/waypoint/waypoint.ts @@ -92,8 +92,8 @@ export class WaypointSelection { } // read again each time the file changes - const read = () => { - const waypoint = engine.waypoint(target.fileId, target.id); + const read = async () => { + const waypoint = await engine.waypoint(target.fileId, target.id); this._selection.set(waypoint ? { ...target, waypoint } : undefined); }; const file = get(engine.files).get(target.fileId); diff --git a/website/src/lib/data/example/example.test.ts b/website/src/lib/data/example/example.test.ts index cb9ee516e..d8ac5947f 100644 --- a/website/src/lib/data/example/example.test.ts +++ b/website/src/lib/data/example/example.test.ts @@ -5,8 +5,8 @@ import { exampleData } from '$lib/data/example/example-data'; import { exampleStatistics } from '$lib/data/example'; describe('exampleStatistics', () => { - it('has the statistics of the whole example for the whole range', () => { - const whole = exampleStatistics.slice(0, exampleStatistics.length - 1); + it('has the statistics of the whole example for the whole range', async () => { + const whole = await exampleStatistics.slice(0, exampleStatistics.length - 1); const global = exampleStatistics.global; assert.ok(whole); for (const key of [ @@ -25,17 +25,17 @@ describe('exampleStatistics', () => { } }); - it('has no speed for a range that does not last', () => { - const point = exampleStatistics.slice(20, 20); + it('has no speed for a range that does not last', async () => { + const point = await exampleStatistics.slice(20, 20); assert.equal(point?.totalTime, 0); assert.equal(point?.totalSpeed, undefined); assert.equal(point?.movingSpeed, undefined); }); - it('has nothing for ranges that are not in the example', () => { - assert.equal(exampleStatistics.slice(5, 500), undefined); - assert.equal(exampleStatistics.slice(-1, 3), undefined); - assert.equal(exampleStatistics.slice(10, 5), undefined); + it('has nothing for ranges that are not in the example', async () => { + assert.equal(await exampleStatistics.slice(5, 500), undefined); + assert.equal(await exampleStatistics.slice(-1, 3), undefined); + assert.equal(await exampleStatistics.slice(10, 5), undefined); }); it( diff --git a/website/src/lib/data/example/index.ts b/website/src/lib/data/example/index.ts index 4281082c6..9e68dd5f9 100644 --- a/website/src/lib/data/example/index.ts +++ b/website/src/lib/data/example/index.ts @@ -20,7 +20,7 @@ const cumulative = { }; /** The statistics of the trackpoints from `start` to `end` (both included), as the engine does. */ -function slice(start: number, end: number): GlobalStatistics | undefined { +async function slice(start: number, end: number): Promise { if (!(start >= 0 && start <= end && end < length)) { return undefined; } diff --git a/website/src/lib/engine/backend/index.ts b/website/src/lib/engine/backend/index.ts new file mode 100644 index 000000000..6807e44d0 --- /dev/null +++ b/website/src/lib/engine/backend/index.ts @@ -0,0 +1,18 @@ +import type { Backend } from './types'; + +export type { Backend } from './types'; + +/** Whether the page runs in the desktop app, which has the engine in its own process. */ +function inDesktopApp() { + return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window; +} + +/** + * Starts the engine of the files: the one of the desktop app when the page runs in it, the + * WebAssembly module otherwise. + */ +export async function loadBackend(): Promise { + return inDesktopApp() + ? (await import('./tauri')).createTauriBackend() + : (await import('./wasm')).createWasmBackend(); +} diff --git a/website/src/lib/engine/backend/tauri.ts b/website/src/lib/engine/backend/tauri.ts new file mode 100644 index 000000000..45b34d477 --- /dev/null +++ b/website/src/lib/engine/backend/tauri.ts @@ -0,0 +1,217 @@ +import { invoke } from '@tauri-apps/api/core'; +import type { RouteAttributes, StorageOpened } from 'gpx-rs'; +import type { Backend } from './types'; + +/** An array that a command returns as its bytes. */ +const array = async ( + command: string, + type: { new (buffer: ArrayBuffer): T }, + args?: Record +) => new type(await invoke(command, args)); + +/** Like `array`, for the arrays that can be absent: the first byte tells whether they are there. */ +const optionalArray = async ( + command: string, + type: { new (buffer: ArrayBuffer): T }, + args?: Record +) => { + const bytes = await invoke(command, args); + return new Uint8Array(bytes)[0] === 1 ? new type(bytes.slice(1)) : undefined; +}; + +const category = (c?: RouteAttributes['surface']) => + c && { starts: Array.from(c.starts), values: Array.from(c.values), names: c.names }; + +/** + * The engine in the Rust process of the desktop app, reached through its commands (see + * `tauri/src/commands`). Typed arrays are returned as bytes, little-endian. + */ +export async function createTauriBackend(): Promise { + const f64 = (command: string, args?: Record) => + array(command, Float64Array, args); + const u32 = (command: string) => array(command, Uint32Array); + const u8 = (command: string) => array(command, Uint8Array); + const f64Optional = (command: string) => optionalArray(command, Float64Array); + + return { + select: (fileIds, mode) => invoke('select', { fileIds, mode }), + select_all: () => invoke('select_all'), + arrow_select: (down, add) => invoke('arrow_select', { down, add }), + select_tracks: (fileId, trackIds, mode) => + invoke('select_tracks', { fileId, trackIds, mode }), + select_segments: (fileId, trackId, segmentIds, mode) => + invoke('select_segments', { fileId, trackId, segmentIds, mode }), + select_waypoint_group: (fileId, mode) => invoke('select_waypoint_group', { fileId, mode }), + select_waypoints: (fileId, waypointIds, mode) => + invoke('select_waypoints', { fileId, waypointIds, mode }), + + undo: () => invoke('undo'), + redo: () => invoke('redo'), + copy: () => invoke('copy'), + cut: () => invoke('cut'), + paste: () => invoke('paste'), + + new_file: (name, lng, lat, ele) => invoke('new_file', { name, lng, lat, ele }), + load_files: (files) => { + const data = new Uint8Array(files.reduce((sum, file) => sum + file.data.length, 0)); + let offset = 0; + for (const file of files) { + data.set(file.data, offset); + offset += file.data.length; + } + // headers are ASCII + const header = (value: unknown) => encodeURIComponent(JSON.stringify(value)); + return invoke('load_files', data, { + headers: { + lengths: header(files.map((file) => file.data.length)), + names: header(files.map((file) => file.name)), + }, + }); + }, + duplicate: () => invoke('duplicate'), + delete: (wholeFiles) => invoke('delete', { wholeFiles }), + delete_all: () => invoke('delete_all'), + reorder: (fileIds, index) => invoke('reorder', { fileIds, index }), + // an infinite index is the end of the list, which JSON cannot tell + move_elements: (what, to) => + invoke('move_elements', { + what, + to: { ...to, index: Math.min(to.index, Number.MAX_SAFE_INTEGER) }, + }), + metadata: (name, desc) => invoke('metadata', { name, desc }), + style: (color, opacity, width) => invoke('style', { color, opacity, width }), + new_track: () => invoke('new_track'), + new_track_segment: () => invoke('new_track_segment'), + + new_waypoint: (lng, lat, ele, name, desc, icon, link) => + invoke('new_waypoint', { lng, lat, ele, name, desc, icon, link }), + update_waypoint: (fileId, waypointId, lng, lat, ele, name, desc, icon, link) => + invoke('update_waypoint', { + fileId, + waypointId, + lng, + lat, + ele, + name, + desc, + icon, + link, + }), + move_waypoint: (fileId, waypointId, lng, lat, ele) => + invoke('move_waypoint', { fileId, waypointId, lng, lat, ele }), + delete_waypoint: (fileId, waypointId) => invoke('delete_waypoint', { fileId, waypointId }), + + reverse: () => invoke('reverse'), + extract: () => invoke('extract'), + round_trip: () => invoke('round_trip'), + elevation: (ele) => invoke('elevation', { ele: Array.from(ele) }), + change_timestamps: (startTime, speed, ratio) => + invoke('change_timestamps', { startTime, speed, ratio }), + create_artificial_timestamps: (startTime, totalTime) => + invoke('create_artificial_timestamps', { startTime, totalTime }), + clean: (west, south, east, north, inside, trkpt, wpt) => + invoke('clean', { + west, + south, + east, + north, + cleanType: inside ? 'inside' : 'outside', + trkpt, + wpt, + }), + merge: (mergeType, removeGaps) => invoke('merge', { mergeType, removeGaps }), + reduce: (tolerance) => invoke('reduce', { tolerance }), + crop: (start, end) => invoke('crop', { start, end }), + split: (revision, at, splitType) => invoke('split', { revision, at, splitType }), + route: (revision, start, end, lng, lat, ele, attributes, anchors) => + invoke('route', { + revision, + start, + end, + lng: Array.from(lng), + lat: Array.from(lat), + ele: Array.from(ele), + attributes: { + surface: category(attributes.surface), + highway: category(attributes.highway), + sacScale: category(attributes.sacScale), + mtbScale: category(attributes.mtbScale), + }, + anchors: Array.from(anchors), + }), + insert_anchor: (revision, lng, lat) => invoke('insert_anchor', { revision, lng, lat }), + change_loop_start: (revision, index) => invoke('change_loop_start', { revision, index }), + + can_undo: () => invoke('can_undo'), + can_redo: () => invoke('can_redo'), + can_paste: () => invoke('can_paste'), + clipboard: async () => (await invoke('clipboard')) ?? undefined, + selection: () => invoke('selection'), + file_order: () => invoke('file_order'), + file_structure: async (fileId) => (await invoke('file_structure', { fileId })) ?? undefined, + file_statistics: async (fileId) => + (await invoke('file_statistics', { fileId })) ?? undefined, + waypoint: async (fileId, waypointId) => + (await invoke('waypoint', { fileId, waypointId })) ?? undefined, + trackpoint: async (fileId, segmentId, index) => + (await invoke('trackpoint', { fileId, segmentId, index })) ?? undefined, + export_file: async (fileId, time, hr, cad, atemp, power, osm, asRoute) => { + const file = await optionalArray('export_file', Uint8Array, { + fileId, + time, + hr, + cad, + atemp, + power, + osm, + asRoute, + }); + return file as Uint8Array | undefined; + }, + exportable_data: (fileIds) => invoke('exportable_data', { fileIds }), + reduction_distances: () => f64('reduction_distances'), + segment_coordinates: (segmentId) => f64('segment_coordinates', { segmentId }), + waypoint_coordinates: (fileId) => f64('waypoint_coordinates', { fileId }), + + selection_statistics: () => invoke('selection_statistics'), + slice_statistics: async (start, end) => + (await invoke('slice_statistics', { start, end })) ?? undefined, + total_distance: () => f64('total_distance'), + slope: () => f64('slope'), + lng: () => f64('lng'), + lat: () => f64('lat'), + ele: () => f64('ele'), + timestamps: () => optionalArray('timestamps', BigInt64Array), + speed: () => f64Optional('speed'), + hr: () => f64Optional('hr'), + cad: () => f64Optional('cad'), + atemp: () => f64Optional('atemp'), + power: () => f64Optional('power'), + slope_segment_slope: () => f64('slope_segment_slope'), + slope_segment_distance: () => f64('slope_segment_distance'), + surface_starts: () => u32('surface_starts'), + surface_values: () => u8('surface_values'), + surfaces: () => invoke('surfaces'), + highway_starts: () => u32('highway_starts'), + highway_values: () => u8('highway_values'), + highways: () => invoke('highways'), + sac_scale_starts: () => u32('sac_scale_starts'), + sac_scale_values: () => u8('sac_scale_values'), + sac_scales: () => invoke('sac_scales'), + mtb_scale_starts: () => u32('mtb_scale_starts'), + mtb_scale_values: () => u8('mtb_scale_values'), + mtb_scales: () => invoke('mtb_scales'), + anchor_indices: () => u32('anchor_indices'), + anchor_zooms: () => u8('anchor_zooms'), + segment_starts: () => u32('segment_starts'), + segment_ids: () => invoke('segment_ids'), + routing_revision: () => invoke('routing_revision'), + + importsLegacyData: false, + open_storage: () => invoke('open_storage'), + flush_storage: () => invoke('flush_storage'), + sync_storage: () => invoke('sync_storage'), + set_setting: (key, value) => invoke('set_setting', { key, value }), + delete_setting: (key) => invoke('delete_setting', { key }), + }; +} diff --git a/website/src/lib/engine/backend/types.ts b/website/src/lib/engine/backend/types.ts new file mode 100644 index 000000000..1ab899e40 --- /dev/null +++ b/website/src/lib/engine/backend/types.ts @@ -0,0 +1,211 @@ +import type { + Clipboard, + FileStructure, + GlobalStatistics, + MoveTarget, + Outcome, + RouteAttributes, + Selection, + StorageOpened, + TrackpointDetails, + WaypointDetails, +} from 'gpx-rs'; +import type { MergeType, SelectMode, SplitType } from '../types'; + +/** + * What the facade needs of the engine of the files, wherever it runs: the WASM module in the + * browser (`wasm.ts`), or the Rust process of the desktop app, through its commands (`tauri.ts`). + * Both give the same results: see `gpx-rs/wasm` for what each function does. + * + * Everything is asynchronous, because the desktop app is reached through messages. Ids are + * hyphenated UUID strings, the arrays of numbers are typed arrays, and what the functions that + * edit the files or change the selection return is an `Outcome`. + */ +export interface Backend { + // Selection + select(fileIds: string[], mode: SelectMode): Promise; + select_all(): Promise; + arrow_select(down: boolean, add: boolean): Promise; + select_tracks(fileId: string, trackIds: string[], mode: SelectMode): Promise; + select_segments( + fileId: string, + trackId: string, + segmentIds: string[], + mode: SelectMode + ): Promise; + select_waypoint_group(fileId: string, mode: SelectMode): Promise; + select_waypoints(fileId: string, waypointIds: string[], mode: SelectMode): Promise; + + // History and clipboard + undo(): Promise; + redo(): Promise; + copy(): Promise; + cut(): Promise; + paste(): Promise; + + // Files + new_file(name: string, lng?: number, lat?: number, ele?: number): Promise; + load_files(files: { data: Uint8Array; name: string }[]): Promise; + duplicate(): Promise; + delete(wholeFiles: boolean): Promise; + delete_all(): Promise; + reorder(fileIds: string[], index: number): Promise; + move_elements(what: Selection, to: MoveTarget): Promise; + metadata(name: string, desc: string): Promise; + style(color?: string, opacity?: number, width?: number): Promise; + new_track(): Promise; + new_track_segment(): Promise; + + // Waypoints + new_waypoint( + lng: number, + lat: number, + ele: number, + name: string, + desc: string, + icon: string, + link: string + ): Promise; + update_waypoint( + fileId: string, + waypointId: string, + lng: number, + lat: number, + ele: number, + name: string, + desc: string, + icon: string, + link: string + ): Promise; + move_waypoint( + fileId: string, + waypointId: string, + lng: number, + lat: number, + ele: number + ): Promise; + delete_waypoint(fileId: string, waypointId: string): Promise; + + // Tools + reverse(): Promise; + extract(): Promise; + round_trip(): Promise; + elevation(ele: Float64Array): Promise; + change_timestamps(startTime: number, speed: number, ratio: number): Promise; + create_artificial_timestamps(startTime: number, totalTime: number): Promise; + clean( + west: number, + south: number, + east: number, + north: number, + inside: boolean, + trackpoints: boolean, + waypoints: boolean + ): Promise; + merge(type: MergeType, removeGaps: boolean): Promise; + reduce(tolerance: number): Promise; + crop(start: number, end: number): Promise; + split(revision: number, at: number, type: SplitType): Promise; + route( + revision: number, + start: number, + end: number, + lng: Float64Array, + lat: Float64Array, + ele: Float64Array, + attributes: RouteAttributes, + anchors: Uint32Array + ): Promise; + insert_anchor(revision: number, lng: number, lat: number): Promise; + change_loop_start(revision: number, index: number): Promise; + + // The files and the selection + can_undo(): Promise; + can_redo(): Promise; + can_paste(): Promise; + clipboard(): Promise; + selection(): Promise; + file_order(): Promise; + file_structure(fileId: string): Promise; + file_statistics(fileId: string): Promise; + waypoint(fileId: string, waypointId: string): Promise; + trackpoint( + fileId: string, + segmentId: string, + index: number + ): Promise; + /** A file as GPX (UTF-8 bytes), `undefined` if it does not exist. */ + export_file( + fileId: string, + time: boolean, + hr: boolean, + cad: boolean, + atemp: boolean, + power: boolean, + osm: boolean, + asRoute: boolean + ): Promise | undefined>; + exportable_data(fileIds: string[]): Promise; + reduction_distances(): Promise; + segment_coordinates(segmentId: string): Promise; + waypoint_coordinates(fileId: string): Promise; + + // The statistics of the selection + selection_statistics(): Promise; + slice_statistics(start: number, end: number): Promise; + total_distance(): Promise; + slope(): Promise; + lng(): Promise; + lat(): Promise; + ele(): Promise; + timestamps(): Promise; + speed(): Promise; + hr(): Promise; + cad(): Promise; + atemp(): Promise; + power(): Promise; + slope_segment_slope(): Promise; + slope_segment_distance(): Promise; + surface_starts(): Promise; + surface_values(): Promise; + surfaces(): Promise; + highway_starts(): Promise; + highway_values(): Promise; + highways(): Promise; + sac_scale_starts(): Promise; + sac_scale_values(): Promise; + sac_scales(): Promise; + mtb_scale_starts(): Promise; + mtb_scale_values(): Promise; + mtb_scales(): Promise; + anchor_indices(): Promise; + anchor_zooms(): Promise; + segment_starts(): Promise; + segment_ids(): Promise; + routing_revision(): Promise; + + // Storage + /** + * Whether the files that the previous versions of the app kept in the browser can be imported + * (they are in the IndexedDB of the browser). + */ + readonly importsLegacyData: boolean; + /** + * Opens the storage: the files that it holds are put in the engine, and the changes of the + * files are kept from then on. `onSaved` is called after the files were written, `onBehind` + * when a write was refused because another window wrote since (call `sync_storage` then), and + * `onNewer` when what is stored comes from a newer version of the app. + */ + open_storage( + name: string, + onSaved: () => void, + onBehind: () => void, + onNewer: () => void + ): Promise; + /** Resolves when what was changed has been written to the storage. */ + flush_storage(): Promise; + /** Reads the files again if another window wrote them since, and tells what changed. */ + sync_storage(): Promise; + set_setting(key: string, json: string): Promise; + delete_setting(key: string): Promise; +} diff --git a/website/src/lib/engine/backend/wasm.ts b/website/src/lib/engine/backend/wasm.ts new file mode 100644 index 000000000..033092ed5 --- /dev/null +++ b/website/src/lib/engine/backend/wasm.ts @@ -0,0 +1,172 @@ +import type { Backend } from './types'; +import type { MergeType, SelectMode, SplitType } from '../types'; + +type Wasm = typeof import('gpx-rs'); + +/** A call of the module, which is synchronous, as an asynchronous one. */ +const lift = + (f: (...args: A) => R) => + async (...args: A): Promise => + f(...args); + +/** Hyphenated UUID strings to the concatenated 16-byte form the module takes. */ +function idsToBytes(ids: string[]): Uint8Array { + const bytes = new Uint8Array(ids.length * 16); + ids.forEach((id, i) => { + const hex = id.replaceAll('-', ''); + for (let j = 0; j < 16; j++) { + bytes[i * 16 + j] = parseInt(hex.slice(2 * j, 2 * j + 2), 16); + } + }); + return bytes; +} + +/** The engine in the page, as the WebAssembly module of `gpx-rs`. */ +export async function createWasmBackend(): Promise { + const w: Wasm = await import('gpx-rs'); + w.start(); + + const mode = (m: SelectMode) => + ({ replace: w.SelectMode.Replace, add: w.SelectMode.Add, toggle: w.SelectMode.Toggle })[m]; + const splitType = (t: SplitType) => + ({ files: w.SplitType.Files, tracks: w.SplitType.Tracks, segments: w.SplitType.Segments })[ + t + ]; + const mergeType = (t: MergeType) => (t === 'connect' ? w.MergeType.Connect : w.MergeType.Group); + + return { + select: lift((ids, m) => w.select(idsToBytes(ids), mode(m))), + select_all: lift(w.select_all), + arrow_select: lift(w.arrow_select), + select_tracks: lift((file, ids, m) => w.select_tracks(file, idsToBytes(ids), mode(m))), + select_segments: lift((file, track, ids, m) => + w.select_segments(file, track, idsToBytes(ids), mode(m)) + ), + select_waypoint_group: lift((file, m) => w.select_waypoint_group(file, mode(m))), + select_waypoints: lift((file, ids, m) => + w.select_waypoints(file, idsToBytes(ids), mode(m)) + ), + + undo: lift(w.undo), + redo: lift(w.redo), + copy: lift(w.copy), + cut: lift(w.cut), + paste: lift(w.paste), + + new_file: lift(w.new_file), + load_files: lift((files) => { + const lengths = Uint32Array.from(files.map((file) => file.data.length)); + const data = new Uint8Array(lengths.reduce((sum, length) => sum + length, 0)); + let offset = 0; + for (const file of files) { + data.set(file.data, offset); + offset += file.data.length; + } + return w.load_files( + data, + lengths, + files.map((file) => file.name) + ); + }), + duplicate: lift(w.duplicate), + delete: lift(w._delete), + delete_all: lift(w.delete_all), + reorder: lift((ids, index) => w.reorder(idsToBytes(ids), index)), + move_elements: lift(w.move_elements), + metadata: lift(w.metadata), + style: lift(w.style), + new_track: lift(w.new_track), + new_track_segment: lift(w.new_track_segment), + + new_waypoint: lift(w.new_waypoint), + update_waypoint: lift(w.update_waypoint), + move_waypoint: lift(w.move_waypoint), + delete_waypoint: lift(w.delete_waypoint), + + reverse: lift(w.reverse), + extract: lift(w.extract), + round_trip: lift(w.round_trip), + elevation: lift(w.elevation), + change_timestamps: lift(w.changeTimestamps), + create_artificial_timestamps: lift(w.createArtificialTimestamps), + clean: lift((west, south, east, north, inside, trackpoints, waypoints) => + w.clean( + west, + south, + east, + north, + inside ? w.CleanType.Inside : w.CleanType.Outside, + trackpoints, + waypoints + ) + ), + merge: lift((type, removeGaps) => w.merge(mergeType(type), removeGaps)), + reduce: lift(w.reduce), + crop: lift(w.crop), + split: lift((revision, at, type) => w.split(revision, at, splitType(type))), + route: lift(w.route), + insert_anchor: lift(w.insert_anchor), + change_loop_start: lift(w.change_loop_start), + + can_undo: lift(w.can_undo), + can_redo: lift(w.can_redo), + can_paste: lift(w.can_paste), + clipboard: lift(w.clipboard), + selection: lift(w.selection), + file_order: lift(w.file_order), + file_structure: lift(w.file_structure), + file_statistics: lift(w.file_statistics), + waypoint: lift(w.waypoint), + trackpoint: lift(w.trackpoint), + // a copy out of the memory of the engine, so a plain ArrayBuffer + export_file: lift( + (...args: Parameters) => + w.export_file(...args) as Uint8Array | undefined + ), + exportable_data: lift((ids) => w.exportable_data(idsToBytes(ids))), + reduction_distances: lift(w.reduction_distances), + segment_coordinates: lift(w.segment_coordinates), + waypoint_coordinates: lift(w.waypoint_coordinates), + + selection_statistics: lift(w.selection_statistics), + slice_statistics: lift(w.slice_statistics), + total_distance: lift(w.total_distance), + slope: lift(w.slope), + lng: lift(w.lng), + lat: lift(w.lat), + ele: lift(w.ele), + timestamps: lift(w.timestamps), + speed: lift(w.speed), + hr: lift(w.hr), + cad: lift(w.cad), + atemp: lift(w.atemp), + power: lift(w.power), + slope_segment_slope: lift(w.slope_segment_slope), + slope_segment_distance: lift(w.slope_segment_distance), + surface_starts: lift(w.surface_starts), + surface_values: lift(w.surface_values), + surfaces: lift(w.surfaces), + highway_starts: lift(w.highway_starts), + highway_values: lift(w.highway_values), + highways: lift(w.highways), + sac_scale_starts: lift(w.sac_scale_starts), + sac_scale_values: lift(w.sac_scale_values), + sac_scales: lift(w.sac_scales), + mtb_scale_starts: lift(w.mtb_scale_starts), + mtb_scale_values: lift(w.mtb_scale_values), + mtb_scales: lift(w.mtb_scales), + anchor_indices: lift(w.anchor_indices), + anchor_zooms: lift(w.anchor_zooms), + segment_starts: lift(w.segment_starts), + segment_ids: lift(w.segment_ids), + routing_revision: lift(w.routing_revision), + + importsLegacyData: true, + open_storage: (name, onSaved, onBehind, onNewer) => + w.open_storage(name, onSaved, onBehind, onNewer), + flush_storage: () => w.flush_storage(), + sync_storage: () => w.sync_storage(), + set_setting: lift(w.set_setting), + delete_setting: lift(w.delete_setting), + }; +} diff --git a/website/src/lib/engine/convert.ts b/website/src/lib/engine/convert.ts index 838dbda51..caaec5203 100644 --- a/website/src/lib/engine/convert.ts +++ b/website/src/lib/engine/convert.ts @@ -1,16 +1,3 @@ -import type { SelectMode } from './types'; - -export type Wasm = typeof import('gpx-rs'); - -/** The enumeration of the engine for a way to combine a selection. */ -export function selectMode(wasm: Wasm, mode: SelectMode) { - return { - replace: wasm.SelectMode.Replace, - add: wasm.SelectMode.Add, - toggle: wasm.SelectMode.Toggle, - }[mode]; -} - /** A flat `[lng, lat, ...]` array as positions. */ export function toPositions(flat: Float64Array): [number, number][] { const positions: [number, number][] = new Array(flat.length / 2); @@ -19,15 +6,3 @@ export function toPositions(flat: Float64Array): [number, number][] { } return positions; } - -export /** Hyphenated UUID strings (as found in the file structures) to the concatenated 16-byte form. */ -function idsToBytes(ids: string[]): Uint8Array { - const bytes = new Uint8Array(ids.length * 16); - ids.forEach((id, i) => { - const hex = id.replaceAll('-', ''); - for (let j = 0; j < 16; j++) { - bytes[i * 16 + j] = parseInt(hex.slice(2 * j, 2 * j + 2), 16); - } - }); - return bytes; -} diff --git a/website/src/lib/engine/engine.test.ts b/website/src/lib/engine/engine.test.ts index a20f90b2b..b880385af 100644 --- a/website/src/lib/engine/engine.test.ts +++ b/website/src/lib/engine/engine.test.ts @@ -218,7 +218,7 @@ describe('statistics', () => { assert.equal(get(engine.statistics).hr, undefined); const request = engine.requestStatistics(); - request.set(['hr', 'anchors']); + await request.set(['hr', 'anchors']); const statistics = get(engine.statistics); assert.equal(statistics.hr!.length, statistics.length); assert.ok(statistics.hr!.some((hr) => !Number.isNaN(hr))); @@ -239,14 +239,14 @@ describe('statistics', () => { it('gives the statistics of a part of the selection', async () => { await load('simple'); const statistics = get(engine.statistics); - const part = statistics.slice(2, 5)!; + const part = (await statistics.slice(2, 5))!; assert.ok(part.totalDistance > 0); assert.ok(part.totalDistance < statistics.global.totalDistance); - assert.equal(statistics.slice(0, statistics.length + 10), undefined); + assert.equal(await statistics.slice(0, statistics.length + 10), undefined); // a slice is only valid for the selection it was made on await engine.select([]); - assert.equal(statistics.slice(2, 5), undefined); + assert.equal(await statistics.slice(2, 5), undefined); }); }); @@ -304,7 +304,7 @@ describe('file state', () => { const file = fileStates()[0]; assert.ok(file.waypoints.features.length > 0); const feature = file.waypoints.features[0]; - const details = engine.waypoint(file.structure.id, feature.properties.waypointId)!; + const details = (await engine.waypoint(file.structure.id, feature.properties.waypointId))!; assert.deepEqual(feature.geometry.coordinates, [details.lng, details.lat]); assert.equal(feature.properties.name, details.name); assert.equal(details.links?.[0].href, 'https://gpx.studio'); @@ -330,23 +330,23 @@ describe('waypoints', () => { const id = file.waypoints.features[0].properties.waypointId; const fileId = file.structure.id; - let details = engine.waypoint(fileId, id)!; + let details = (await engine.waypoint(fileId, id))!; assert.equal(details.name, 'summit'); assert.equal(details.sym, 'Summit'); assert.equal(details.links![0].href, 'https://example.com'); await engine.updateWaypoint(fileId, id, { ...waypoint, name: 'peak', link: '' }); - details = engine.waypoint(fileId, id)!; + details = (await engine.waypoint(fileId, id))!; assert.equal(details.name, 'peak'); assert.equal(details.links, undefined); await engine.moveWaypoint(fileId, id, 5, 51, 200); assert.deepEqual(fileStates()[0].waypoints.features[0].geometry.coordinates, [5, 51]); - assert.equal(engine.waypoint(fileId, id)!.ele, 200); + assert.equal((await engine.waypoint(fileId, id))!.ele, 200); await engine.deleteWaypoint(fileId, id); assert.equal(fileStates()[0].waypoints.features.length, 0); - assert.equal(engine.waypoint(fileId, id), undefined); + assert.equal(await engine.waypoint(fileId, id), undefined); assert.equal(await engine.deleteWaypoint(fileId, id), false); }); }); @@ -395,8 +395,10 @@ describe('observer of the files', () => { }); describe('export', () => { - const text = (id: string, options: Partial = {}) => - new TextDecoder().decode(engine.exportFile(id, { ...ALL_EXPORT_OPTIONS, ...options })); + const text = async (id: string, options: Partial = {}) => + new TextDecoder().decode( + await engine.exportFile(id, { ...ALL_EXPORT_OPTIONS, ...options }) + ); it('writes tracks, or routes when asked to', async () => { await load('with_tracks_and_segments'); @@ -407,11 +409,11 @@ describe('export', () => { ); const id = file.structure.id; - const tracks = text(id); + const tracks = await text(id); assert.ok(tracks.includes('') && !tracks.includes('')); assert.equal(tracks.match(//g)?.length, segments); - const routes = text(id, { asRoute: true }); + const routes = await text(id, { asRoute: true }); assert.ok(routes.includes('') && !routes.includes('')); assert.equal(routes.match(//g)?.length, segments); assert.ok(routes.includes(' { it('does not count the way a file is written among its data', async () => { await load('simple'); - const data = engine.exportableData(get(engine.order)); + const data = await engine.exportableData(get(engine.order)); assert.equal(data.asRoute, false); }); @@ -427,7 +429,7 @@ describe('export', () => { await load('with_tracks_and_segments'); const id = get(engine.order)[0]; const before = fileStates()[0].segments.features.length; - const routes = engine.exportFile(id, { ...ALL_EXPORT_OPTIONS, asRoute: true })!; + const routes = (await engine.exportFile(id, { ...ALL_EXPORT_OPTIONS, asRoute: true }))!; await engine.loadFiles([{ data: routes, name: 'routes' }]); const [, loaded] = get(engine.order); const state = get(get(engine.files).get(loaded)!); diff --git a/website/src/lib/engine/engine.ts b/website/src/lib/engine/engine.ts index ad2032ad4..62cdb70b5 100644 --- a/website/src/lib/engine/engine.ts +++ b/website/src/lib/engine/engine.ts @@ -10,7 +10,7 @@ import { import { categoryIntervals } from '$lib/engine/trackpoint-categories'; import { selectedElementIds, type FileTreeNode } from '$lib/engine/selection-helpers'; import type { RouteAttributes } from 'gpx-rs'; -import { idsToBytes, selectMode, type Wasm } from './convert'; +import { loadBackend, type Backend } from './backend'; import { readFileState } from './file-state'; import { StatisticsReader } from './statistics'; import { openStoredFiles } from './storage'; @@ -34,7 +34,8 @@ import { } from './types'; /** - * Thin wrapper around the WASM engine. + * Thin wrapper around the engine of the files (the WASM module in the browser, the Rust process + * in the desktop app: see `backend`). * * The engine owns the files; this class mirrors what the UI needs in stores. After each action, * only what the engine reports as changed is read again: the order of the files, and the state @@ -42,7 +43,9 @@ import { * segments and waypoints that did not change are reused. */ class Engine { - private wasm: Wasm | null = null; + private backend: Backend | null = null; + /** The calls to the engine, one after the other: each one is applied before the next starts. */ + private _queue: Promise = Promise.resolve(); /** Resolves once the engine can be used (never on the server). */ readonly ready: Promise; @@ -61,7 +64,7 @@ class Engine { private _canRedo = writable(false); private _canPaste = writable(false); private _clipboard = writable(undefined); - private _statistics = new StatisticsReader(() => this.wasm); + private _statistics = new StatisticsReader((task) => this.enqueue(task)); /** Ids of the files in display order. */ readonly order: Readable = { subscribe: this._order.subscribe }; @@ -89,9 +92,8 @@ class Engine { constructor() { this.ready = browser - ? import('gpx-rs').then((wasm) => { - wasm.start(); - this.wasm = wasm; + ? loadBackend().then((backend) => { + this.backend = backend; }) : new Promise(() => {}); } @@ -110,9 +112,9 @@ class Engine { openStorage(): Promise> { this._storage ??= this.ready.then(() => openStoredFiles({ - wasm: this.wasm!, + backend: this.backend!, visibility: this._visibility, - sync: (update) => this.sync(this.wasm!, update), + sync: (update) => this.enqueue(() => this.sync(this.backend!, update)), outdated: () => this._outdated.set(true), hasFiles: () => get(this._order).length > 0, loadFiles: (files) => this.loadFiles(files), @@ -124,11 +126,11 @@ class Engine { /** Keeps a setting, `json` being its JSON. Nothing is kept before `openStorage` is done. */ setSetting(key: string, json: string) { - this.wasm?.set_setting(key, json); + void this.backend?.set_setting(key, json); } deleteSetting(key: string) { - this.wasm?.delete_setting(key); + void this.backend?.delete_setting(key); } // Selection. Like the edits, each action resolves to whether the engine changed something. @@ -138,7 +140,7 @@ class Engine { * `SelectMode` for how the files combine with the selection. */ select(fileIds: string[], mode: SelectMode = 'replace') { - return this.run((w) => w.select(idsToBytes(fileIds), selectMode(w, mode))); + return this.run((b) => b.select(fileIds, mode)); } /** @@ -147,7 +149,7 @@ class Engine { * the files when nothing is selected. */ selectAll() { - return this.run((w) => w.select_all()); + return this.run((b) => b.select_all()); } /** @@ -156,13 +158,13 @@ class Engine { * it replaces it. */ arrowSelect(down: boolean, add: boolean) { - return this.run((w) => w.arrow_select(down, add)); + return this.run((b) => b.arrow_select(down, add)); } // Selecting elements inside a file. Ids that do not exist are ignored. selectTracks(fileId: string, trackIds: string[], mode: SelectMode = 'replace') { - return this.run((w) => w.select_tracks(fileId, idsToBytes(trackIds), selectMode(w, mode))); + return this.run((b) => b.select_tracks(fileId, trackIds, mode)); } selectSegments( @@ -171,20 +173,16 @@ class Engine { segmentIds: string[], mode: SelectMode = 'replace' ) { - return this.run((w) => - w.select_segments(fileId, trackId, idsToBytes(segmentIds), selectMode(w, mode)) - ); + return this.run((b) => b.select_segments(fileId, trackId, segmentIds, mode)); } /** Selects the node standing for all the waypoints of the file. */ selectWaypointGroup(fileId: string, mode: SelectMode = 'replace') { - return this.run((w) => w.select_waypoint_group(fileId, selectMode(w, mode))); + return this.run((b) => b.select_waypoint_group(fileId, mode)); } selectWaypoints(fileId: string, waypointIds: string[], mode: SelectMode = 'replace') { - return this.run((w) => - w.select_waypoints(fileId, idsToBytes(waypointIds), selectMode(w, mode)) - ); + return this.run((b) => b.select_waypoints(fileId, waypointIds, mode)); } /** Selects a node of the file tree. */ @@ -206,23 +204,23 @@ class Engine { // History undo() { - return this.run((w) => w.undo()); + return this.run((b) => b.undo()); } redo() { - return this.run((w) => w.redo()); + return this.run((b) => b.redo()); } // Clipboard /** Puts the selected elements in the clipboard. */ copy() { - return this.run((w) => w.copy()); + return this.run((b) => b.copy()); } /** Like `copy`, but the elements are moved when they are pasted. */ cut() { - return this.run((w) => w.cut()); + return this.run((b) => b.cut()); } // Edits of the files @@ -232,7 +230,7 @@ class Engine { * segment that hold it, in the same undo step. */ newFile(name: string, trackpoint?: { lng: number; lat: number; ele?: number }) { - return this.run((w) => w.new_file(name, trackpoint?.lng, trackpoint?.lat, trackpoint?.ele)); + return this.run((b) => b.new_file(name, trackpoint?.lng, trackpoint?.lat, trackpoint?.ele)); } /** @@ -241,24 +239,11 @@ class Engine { * the first one that was read is selected. */ loadFiles(files: { data: Uint8Array; name: string }[]) { - const lengths = Uint32Array.from(files.map((file) => file.data.length)); - const data = new Uint8Array(lengths.reduce((sum, length) => sum + length, 0)); - let offset = 0; - for (const file of files) { - data.set(file.data, offset); - offset += file.data.length; - } - return this.run((w) => - w.load_files( - data, - lengths, - files.map((file) => file.name) - ) - ); + return this.run((b) => b.load_files(files)); } duplicate() { - return this.run((w) => w.duplicate()); + return this.run((b) => b.duplicate()); } /** @@ -266,16 +251,16 @@ class Engine { * are deleted instead, even if only a track or a waypoint is selected. */ delete(wholeFiles = false) { - return this.run((w) => w._delete(wholeFiles)); + return this.run((b) => b.delete(wholeFiles)); } deleteAll() { - return this.run((w) => w.delete_all()); + return this.run((b) => b.delete_all()); } /** Moves the files, in the given order, to `index` among the other files. */ reorder(fileIds: string[], index: number) { - return this.run((w) => w.reorder(idsToBytes(fileIds), index)); + return this.run((b) => b.reorder(fileIds, index)); } /** @@ -289,7 +274,7 @@ class Engine { * copied again, what was cut is moved the first time and copied after that. */ paste() { - return this.run((w) => w.paste()); + return this.run((b) => b.paste()); } /** @@ -303,7 +288,7 @@ class Engine { * node), among the waypoints of a file. */ move(what: Selection, to: MoveTarget) { - return this.run((w) => w.move_elements(what, to)); + return this.run((b) => b.move_elements(what, to)); } // Edits of the waypoints @@ -313,8 +298,8 @@ class Engine { * strings are fields that the waypoint does not have. */ newWaypoint(waypoint: NewWaypoint) { - return this.run((w) => - w.new_waypoint( + return this.run((b) => + b.new_waypoint( waypoint.lng, waypoint.lat, waypoint.ele, @@ -328,8 +313,8 @@ class Engine { /** Changes a waypoint, selected or not. Empty strings remove the field. */ updateWaypoint(fileId: string, waypointId: string, waypoint: NewWaypoint) { - return this.run((w) => - w.update_waypoint( + return this.run((b) => + b.update_waypoint( fileId, waypointId, waypoint.lng, @@ -345,22 +330,22 @@ class Engine { /** Moves a waypoint, selected or not: only its position and elevation change. */ moveWaypoint(fileId: string, waypointId: string, lng: number, lat: number, ele: number) { - return this.run((w) => w.move_waypoint(fileId, waypointId, lng, lat, ele)); + return this.run((b) => b.move_waypoint(fileId, waypointId, lng, lat, ele)); } /** Deletes a waypoint, selected or not. */ deleteWaypoint(fileId: string, waypointId: string) { - return this.run((w) => w.delete_waypoint(fileId, waypointId)); + return this.run((b) => b.delete_waypoint(fileId, waypointId)); } // Edits of the selection newTrack() { - return this.run((w) => w.new_track()); + return this.run((b) => b.new_track()); } newTrackSegment() { - return this.run((w) => w.new_track_segment()); + return this.run((b) => b.new_track_segment()); } /** @@ -369,7 +354,7 @@ class Engine { * nothing to reverse (fewer than two trackpoints). */ reverse() { - return this.run((w) => w.reverse()); + return this.run((b) => b.reverse()); } /** @@ -378,7 +363,7 @@ class Engine { * Resolves to `false` when there is nothing to extract. */ extract() { - return this.run((w) => w.extract()); + return this.run((b) => b.extract()); } /** @@ -387,7 +372,7 @@ class Engine { * number of elevations is not the number of trackpoints. */ setElevation(ele: Float64Array) { - return this.run((w) => w.elevation(ele)); + return this.run((b) => b.elevation(ele)); } /** @@ -396,7 +381,7 @@ class Engine { * `speed` (km/h). Resolves to `false` when there is nothing to set, or the values are invalid. */ changeTimestamps(startTime: Date, speed: number, ratio: number) { - return this.run((w) => w.changeTimestamps(startTime.getTime(), speed, ratio)); + return this.run((b) => b.change_timestamps(startTime.getTime(), speed, ratio)); } /** @@ -405,7 +390,7 @@ class Engine { * nothing to set. */ createArtificialTimestamps(startTime: Date, totalTime: number) { - return this.run((w) => w.createArtificialTimestamps(startTime.getTime(), totalTime)); + return this.run((b) => b.create_artificial_timestamps(startTime.getTime(), totalTime)); } /** @@ -418,13 +403,13 @@ class Engine { trackpoints: boolean, waypoints: boolean ) { - return this.run((w) => - w.clean( + return this.run((b) => + b.clean( bounds.west, bounds.south, bounds.east, bounds.north, - inside ? w.CleanType.Inside : w.CleanType.Outside, + inside, trackpoints, waypoints ) @@ -437,9 +422,7 @@ class Engine { * Resolves to `false` when there is nothing to merge. */ merge(type: MergeType, removeGaps = false) { - return this.run((w) => - w.merge(type === 'connect' ? w.MergeType.Connect : w.MergeType.Group, removeGaps) - ); + return this.run((b) => b.merge(type, removeGaps)); } /** @@ -448,7 +431,7 @@ class Engine { * is nothing to remove. */ reduce(tolerance: number) { - return this.run((w) => w.reduce(tolerance)); + return this.run((b) => b.reduce(tolerance)); } /** @@ -458,17 +441,7 @@ class Engine { * one of `SelectionAnchors`, which `at` is an index of. */ split(revision: number, at: number, splitType: SplitType) { - return this.run((w) => - w.split( - revision, - at, - { - files: w.SplitType.Files, - tracks: w.SplitType.Tracks, - segments: w.SplitType.Segments, - }[splitType] - ) - ); + return this.run((b) => b.split(revision, at, splitType)); } /** @@ -478,7 +451,7 @@ class Engine { * the range covers the whole selection. */ crop(start: number, end: number) { - return this.run((w) => w.crop(Math.max(start, 0), Math.max(end, 0))); + return this.run((b) => b.crop(Math.max(start, 0), Math.max(end, 0))); } /** @@ -487,16 +460,16 @@ class Engine { * repeated), and goes on in time. Resolves to `false` when no segment has the two trackpoints that a way back needs. */ roundTrip() { - return this.run((w) => w.round_trip()); + return this.run((b) => b.round_trip()); } metadata(name: string, desc: string) { - return this.run((w) => w.metadata(name, desc)); + return this.run((b) => b.metadata(name, desc)); } /** Only the given fields are changed. */ style(style: { color?: string; opacity?: number; width?: number }) { - return this.run((w) => w.style(style.color, style.opacity, style.width)); + return this.run((b) => b.style(style.color, style.opacity, style.width)); } // Routing @@ -529,8 +502,8 @@ class Engine { routeAttributes[key] = intervals; } } - return this.run((w) => - w.route( + return this.run((b) => + b.route( revision, start, end, @@ -549,7 +522,7 @@ class Engine { * one of `SelectionAnchors`. */ insertAnchor(revision: number, lng: number, lat: number) { - return this.run((w) => w.insert_anchor(revision, lng, lat)); + return this.run((b) => b.insert_anchor(revision, lng, lat)); } /** @@ -557,7 +530,7 @@ class Engine { * the trackpoints before it go to the end, and the loop closes on it. `revision` is the one of `SelectionAnchors`. */ changeLoopStart(revision: number, index: number) { - return this.run((w) => w.change_loop_start(revision, index)); + return this.run((b) => b.change_loop_start(revision, index)); } // Visibility, which is kept by the frontend and not by the engine @@ -601,16 +574,20 @@ class Engine { } } - // Requests, which do not change anything + // Requests, which do not change anything. They wait for the calls made before them. /** All the data of a waypoint, `undefined` if it does not exist (or before the engine is loaded). */ - waypoint(fileId: string, waypointId: string): WaypointDetails | undefined { - return this.wasm?.waypoint(fileId, waypointId); + waypoint(fileId: string, waypointId: string): Promise { + return this.read((b) => b.waypoint(fileId, waypointId), undefined); } /** A trackpoint of a segment, `undefined` if it does not exist. */ - trackpoint(fileId: string, segmentId: string, index: number): TrackpointDetails | undefined { - return this.wasm?.trackpoint(fileId, segmentId, index); + trackpoint( + fileId: string, + segmentId: string, + index: number + ): Promise { + return this.read((b) => b.trackpoint(fileId, segmentId, index), undefined); } /** @@ -620,23 +597,26 @@ class Engine { exportFile( fileId: string, options: ExportOptions = ALL_EXPORT_OPTIONS - ): Uint8Array | undefined { - // a copy out of the memory of the engine, so a plain ArrayBuffer - return this.wasm?.export_file( - fileId, - options.time, - options.hr, - options.cad, - options.atemp, - options.power, - options.osm, - options.asRoute - ) as Uint8Array | undefined; + ): Promise | undefined> { + return this.read( + (b) => + b.export_file( + fileId, + options.time, + options.hr, + options.cad, + options.atemp, + options.power, + options.osm, + options.asRoute + ), + undefined + ); } /** The data that some files have, which the options of `exportFile` can leave out. */ - exportableData(fileIds: string[]): ExportOptions { - const bits = this.wasm?.exportable_data(idsToBytes(fileIds)) ?? 0; + async exportableData(fileIds: string[]): Promise { + const bits = await this.read((b) => b.exportable_data(fileIds), 0); return { time: (bits & 1) !== 0, hr: (bits & 2) !== 0, @@ -654,19 +634,19 @@ class Engine { * (m) of `reduce` that keeps it: infinite for the ends of the segments. Not requested with * the statistics: ask when they changed. */ - reductionDistances(): Float64Array { - return this.wasm?.reduction_distances() ?? new Float64Array(); + reductionDistances(): Promise { + return this.read((b) => b.reduction_distances(), new Float64Array()); } - // Coordinates, as flat [lng, lat, ...] arrays. The getters copy them out of the WASM memory. + // Coordinates, as flat [lng, lat, ...] arrays: copies. // Refetch them when the `rev` of the segment (or the `waypointsRev` of the file) changed. - segmentCoordinates(segmentId: string): Float64Array { - return this.wasm?.segment_coordinates(segmentId) ?? new Float64Array(); + segmentCoordinates(segmentId: string): Promise { + return this.read((b) => b.segment_coordinates(segmentId), new Float64Array()); } - waypointCoordinates(fileId: string): Float64Array { - return this.wasm?.waypoint_coordinates(fileId) ?? new Float64Array(); + waypointCoordinates(fileId: string): Promise { + return this.read((b) => b.waypoint_coordinates(fileId), new Float64Array()); } /** @@ -682,17 +662,34 @@ class Engine { // Plumbing + /** Does `task` after the ones that were queued before it, whatever they do. */ + private enqueue(task: () => Promise): Promise { + const result = this._queue.then(task, task); + this._queue = result.catch(() => {}); + return result; + } + + /** + * Makes a request to the engine, once the calls made before it are done. Resolves to + * `fallback` when the engine is not loaded. + */ + private read(request: (backend: Backend) => Promise, fallback: T): Promise { + return this.enqueue(async () => (this.backend ? request(this.backend) : fallback)); + } + /** * Makes a call to the engine and applies its outcome to what is kept of the files. Resolves to * whether anything changed. When nothing did, `lastError` tells why. */ - private async run(action: (wasm: Wasm) => Outcome): Promise { + private async run(action: (backend: Backend) => Promise): Promise { await this.ready; - const wasm = this.wasm!; - const outcome = action(wasm); - this.sync(wasm, outcome); - this.report(outcome); - return outcome.changed; + return this.enqueue(async () => { + const backend = this.backend!; + const outcome = await action(backend); + await this.sync(backend, outcome); + this.report(outcome); + return outcome.changed; + }); } /** Logs why a call did nothing, and keeps it in `lastError`. */ @@ -706,26 +703,34 @@ class Engine { } /** Updates what is kept of the engine with what a call changed. */ - private sync(wasm: Wasm, update: Outcome) { - this._canUndo.set(wasm.can_undo()); - this._canRedo.set(wasm.can_redo()); - this._canPaste.set(wasm.can_paste()); + private async sync(backend: Backend, update: Outcome) { + const [canUndo, canRedo, canPaste] = await Promise.all([ + backend.can_undo(), + backend.can_redo(), + backend.can_paste(), + ]); + this._canUndo.set(canUndo); + this._canRedo.set(canRedo); + this._canPaste.set(canPaste); if (update.clipboardChanged) { - this._clipboard.set(wasm.clipboard()); + this._clipboard.set(await backend.clipboard()); } if (update.selectionChanged) { - this._selection.set(wasm.selection()); + this._selection.set(await backend.selection()); } if (update.orderChanged) { - this._order.set(wasm.file_order()); + this._order.set(await backend.file_order()); } const added: [string, FileState][] = []; - for (const id of update.added) { - const state = readFileState(wasm, id, this._colors, this._visibility); + const addedStates = await Promise.all( + update.added.map((id) => readFileState(backend, id, this._colors, this._visibility)) + ); + update.added.forEach((id, i) => { + const state = addedStates[i]; if (state) { added.push([id, state]); } - } + }); if (added.length > 0 || update.removed.length > 0) { this._files.update((files) => { const next = new Map(files); @@ -739,14 +744,22 @@ class Engine { } if (update.modified.length > 0) { const files = get(this._files); - for (const id of update.modified) { + const states = await Promise.all( + update.modified.map((id) => { + const store = files.get(id); + return ( + store && + readFileState(backend, id, this._colors, this._visibility, get(store)) + ); + }) + ); + update.modified.forEach((id, i) => { const store = files.get(id); - const state = - store && readFileState(wasm, id, this._colors, this._visibility, get(store)); + const state = states[i]; if (store && state) { store.set(state); } - } + }); } // the statistics depend on the selection, on its order, and on the files @@ -755,7 +768,7 @@ class Engine { update.orderChanged || update.added.length + update.modified.length + update.removed.length > 0 ) { - this._statistics.refresh(wasm); + await this._statistics.refresh(backend); } } } diff --git a/website/src/lib/engine/file-state.ts b/website/src/lib/engine/file-state.ts index 92599a2ae..37a2958aa 100644 --- a/website/src/lib/engine/file-state.ts +++ b/website/src/lib/engine/file-state.ts @@ -1,18 +1,19 @@ import type { Feature, LineString } from 'geojson'; import { normalizeColor, type FileColorAllocator } from '$lib/engine/file-colors'; import type { Visibility } from '$lib/engine/file-visibility'; -import { toPositions, type Wasm } from './convert'; +import type { Backend } from './backend'; +import { toPositions } from './convert'; import { EMPTY_STATISTICS, type FileState, type SegmentProperties } from './types'; /** Reads the state of a file, reusing from `previous` what did not change. */ -export function readFileState( - wasm: Wasm, +export async function readFileState( + backend: Backend, id: string, colors: FileColorAllocator, visibility: ReadonlyMap, previous?: FileState -): FileState | null { - const structure = wasm.file_structure(id); +): Promise { + const structure = await backend.file_structure(id); if (!structure) { return null; } @@ -25,7 +26,11 @@ export function readFileState( const previousSegments = new Map( previous?.segments.features.map((f) => [f.properties.segmentId, f]) ?? [] ); - const segments: Feature[] = []; + // the coordinates of the segments that changed, read together + const segments: ( + | Feature + | Promise> + )[] = []; structure.tracks.forEach((track, trackIndex) => { track.segments.forEach((segment, segmentIndex) => { const properties: SegmentProperties = { @@ -48,17 +53,18 @@ export function readFileState( segments.push( unchanged ? old - : { + : backend.segment_coordinates(segment.id).then((coordinates) => ({ type: 'Feature', geometry: { type: 'LineString', - coordinates: toPositions(wasm.segment_coordinates(segment.id)), + coordinates: toPositions(coordinates), }, properties, - } + })) ); }); }); + const segmentFeatures = await Promise.all(segments); const previousStructure = previous?.structure; const waypointsUnchanged = @@ -73,7 +79,7 @@ export function readFileState( ); let waypoints = previous?.waypoints; if (!waypoints || !waypointsUnchanged) { - const coordinates = toPositions(wasm.waypoint_coordinates(id)); + const coordinates = toPositions(await backend.waypoint_coordinates(id)); waypoints = { type: 'FeatureCollection', features: structure.waypoints.map((waypoint, index) => ({ @@ -93,8 +99,8 @@ export function readFileState( return { structure, color, - statistics: wasm.file_statistics(id) ?? EMPTY_STATISTICS, - segments: { type: 'FeatureCollection', features: segments }, + statistics: (await backend.file_statistics(id)) ?? EMPTY_STATISTICS, + segments: { type: 'FeatureCollection', features: segmentFeatures }, waypoints, visibility: visibility.get(id) ?? new Map(), }; diff --git a/website/src/lib/engine/statistics.ts b/website/src/lib/engine/statistics.ts index 00815b920..d5a3fec4e 100644 --- a/website/src/lib/engine/statistics.ts +++ b/website/src/lib/engine/statistics.ts @@ -1,6 +1,6 @@ import { get, writable, type Readable } from 'svelte/store'; import { categoryIntervals, type CategoryIntervals } from '$lib/engine/trackpoint-categories'; -import type { Wasm } from './convert'; +import type { Backend } from './backend'; import { EMPTY_SELECTION_STATISTICS, EMPTY_STATISTICS, @@ -23,35 +23,49 @@ export class StatisticsReader { /** The metrics that were read for the current statistics. */ private _loaded = new Set(); - /** @param wasm The engine, `null` before it is loaded. */ - constructor(private wasm: () => Wasm | null) {} + /** The engine, as of the last time the statistics were read. */ + private _backend: Backend | null = null; + + /** @param enqueue Does a task after the calls to the engine that were made before it. */ + constructor(private enqueue: (task: () => Promise) => Promise) {} /** Reads the statistics of the selection again, after it or the files changed. */ - refresh(wasm: Wasm) { - this._store.set(this.read(wasm)); + async refresh(backend: Backend) { + this._backend = backend; + this._store.set(await this.read(backend)); } /** * Reads the statistics buffers of the engine (the getters copy them), but only the metrics * that were requested. */ - private read(wasm: Wasm): SelectionStatistics { + private async read(backend: Backend): Promise { const version = ++this._version; this._loaded.clear(); - const totalDistance = wasm.total_distance(); + const [global, totalDistance, slope, lng, lat, ele, timestamps] = await Promise.all([ + backend.selection_statistics(), + backend.total_distance(), + backend.slope(), + backend.lng(), + backend.lat(), + backend.ele(), + backend.timestamps(), + ]); const statistics: SelectionStatistics = { - global: wasm.selection_statistics() ?? EMPTY_STATISTICS, + global: global ?? EMPTY_STATISTICS, length: totalDistance.length, totalDistance, - slope: wasm.slope(), - lng: wasm.lng(), - lat: wasm.lat(), - ele: wasm.ele(), - timestamps: wasm.timestamps(), + slope, + lng, + lat, + ele, + timestamps, slice: (start, end) => - version === this._version ? wasm.slice_statistics(start, end) : undefined, + this.enqueue(async () => + version === this._version ? backend.slice_statistics(start, end) : undefined + ), }; - this.readMetrics(wasm, statistics, this.requestedMetrics()); + await this.readMetrics(backend, statistics, this.requestedMetrics()); return statistics; } @@ -62,81 +76,95 @@ export class StatisticsReader { } /** Reads the metrics that were not read yet into `statistics`, which is the current one. */ - private readMetrics( - wasm: Wasm, + private async readMetrics( + backend: Backend, statistics: SelectionStatistics, metrics: Set ) { - const categories = ( - starts: Uint32Array, - values: Uint8Array, - names: () => string[] - ): CategoryIntervals => ({ - starts, - values, - names: names(), - }); + const categories = async ( + starts: Promise, + values: Promise, + names: Promise + ): Promise => { + const [s, v, n] = await Promise.all([starts, values, names]); + return { starts: s, values: v, names: n }; + }; - for (const metric of metrics) { - if (this._loaded.has(metric)) { - continue; - } - this._loaded.add(metric); - switch (metric) { - case 'speed': - statistics.speed = wasm.speed(); - break; - case 'hr': - statistics.hr = wasm.hr(); - break; - case 'cad': - statistics.cad = wasm.cad(); - break; - case 'atemp': - statistics.atemp = wasm.atemp(); - break; - case 'power': - statistics.power = wasm.power(); - break; - case 'slopeSegment': - statistics.slopeSegmentSlope = wasm.slope_segment_slope(); - statistics.slopeSegmentDistance = wasm.slope_segment_distance(); - break; - case 'surface': - statistics.surface = categories( - wasm.surface_starts(), - wasm.surface_values(), - wasm.surfaces - ); - break; - case 'anchors': - statistics.anchors = { - indices: wasm.anchor_indices(), - zooms: wasm.anchor_zooms(), - segmentStarts: wasm.segment_starts(), - segmentIds: wasm.segment_ids(), - revision: wasm.routing_revision(), - }; - break; - case 'highway': - statistics.highway = categories( - wasm.highway_starts(), - wasm.highway_values(), - wasm.highways - ); - statistics.sacScale = categories( - wasm.sac_scale_starts(), - wasm.sac_scale_values(), - wasm.sac_scales - ); - statistics.mtbScale = categories( - wasm.mtb_scale_starts(), - wasm.mtb_scale_values(), - wasm.mtb_scales - ); - break; - } - } + const missing = [...metrics].filter((metric) => !this._loaded.has(metric)); + missing.forEach((metric) => this._loaded.add(metric)); + await Promise.all( + missing.map(async (metric) => { + switch (metric) { + case 'speed': + statistics.speed = await backend.speed(); + break; + case 'hr': + statistics.hr = await backend.hr(); + break; + case 'cad': + statistics.cad = await backend.cad(); + break; + case 'atemp': + statistics.atemp = await backend.atemp(); + break; + case 'power': + statistics.power = await backend.power(); + break; + case 'slopeSegment': + [statistics.slopeSegmentSlope, statistics.slopeSegmentDistance] = + await Promise.all([ + backend.slope_segment_slope(), + backend.slope_segment_distance(), + ]); + break; + case 'surface': + statistics.surface = await categories( + backend.surface_starts(), + backend.surface_values(), + backend.surfaces() + ); + break; + case 'anchors': { + const [indices, zooms, segmentStarts, segmentIds, revision] = + await Promise.all([ + backend.anchor_indices(), + backend.anchor_zooms(), + backend.segment_starts(), + backend.segment_ids(), + backend.routing_revision(), + ]); + statistics.anchors = { + indices, + zooms, + segmentStarts, + segmentIds, + revision, + }; + break; + } + case 'highway': + [statistics.highway, statistics.sacScale, statistics.mtbScale] = + await Promise.all([ + categories( + backend.highway_starts(), + backend.highway_values(), + backend.highways() + ), + categories( + backend.sac_scale_starts(), + backend.sac_scale_values(), + backend.sac_scales() + ), + categories( + backend.mtb_scale_starts(), + backend.mtb_scale_values(), + backend.mtb_scales() + ), + ]); + break; + } + }) + ); } /** @@ -155,7 +183,7 @@ export class StatisticsReader { for (const metric of metrics) { wanted.add(metric); } - this.loadMissing(); + return this.loadMissing(); }, release: () => { this._requests.delete(wanted); @@ -164,16 +192,22 @@ export class StatisticsReader { } /** Reads the requested metrics that are not in the current statistics yet. */ - private loadMissing() { - const wasm = this.wasm(); + private async loadMissing() { const metrics = this.requestedMetrics(); - if (!wasm || [...metrics].every((metric) => this._loaded.has(metric))) { + if ([...metrics].every((metric) => this._loaded.has(metric))) { return; } - // the buffers of the engine still are the ones of the current statistics, as they are - // read after every action - const statistics = { ...get(this._store) }; - this.readMetrics(wasm, statistics, metrics); - this._store.set(statistics); + // queued, so that the buffers of the engine still are the ones of the current statistics: + // they are read after every action + await this.enqueue(async () => { + const backend = this._backend; + const requested = this.requestedMetrics(); + if (!backend || [...requested].every((metric) => this._loaded.has(metric))) { + return; + } + const statistics = { ...get(this._store) }; + await this.readMetrics(backend, statistics, requested); + this._store.set(statistics); + }); } } diff --git a/website/src/lib/engine/storage.ts b/website/src/lib/engine/storage.ts index aab5acf8f..a1d7f70c1 100644 --- a/website/src/lib/engine/storage.ts +++ b/website/src/lib/engine/storage.ts @@ -1,7 +1,7 @@ import { LEGACY_IMPORTED_KEY, readLegacyData } from '$lib/storage/legacy-import'; import { parseVisibility, VISIBILITY_KEY, type Visibility } from '$lib/engine/file-visibility'; import type { Outcome, StorageOpened } from 'gpx-rs'; -import type { Wasm } from './convert'; +import type { Backend } from './backend'; /** Name of the IndexedDB database that keeps the files and the settings. */ const STORAGE_NAME = 'gpx-studio'; @@ -11,7 +11,7 @@ const CHANNEL_NAME = 'gpx-studio-storage'; /** What opening the storage needs of the engine facade. */ export type StorageHost = { - wasm: Wasm; + backend: Backend; /** The explicit visibility per file, to fill with the one that was saved. */ visibility: Map; /** Applies what a call changed to what the facade keeps of the files. */ @@ -34,11 +34,11 @@ function followOtherTabs(host: StorageHost): { behind: () => void; newer: () => void; } { - const { wasm } = host; + const { backend } = host; let reading = Promise.resolve(); const refresh = () => { reading = reading - .then(async () => host.sync(await wasm.sync_storage())) + .then(async () => host.sync(await backend.sync_storage())) .catch((error) => console.error('The files could not be read again', error)); }; const channel = @@ -61,17 +61,17 @@ function followOtherTabs(host: StorageHost): { } /** - * Opens the storage of the browser: the files that it holds are put in the engine, and the + * Opens the storage: the files that it holds are put in the engine, and the * changes of the files are kept from then on. Resolves to the settings, as JSON strings by key. * * The first time, what the previous versions of the app kept in the browser is imported. */ export async function openStoredFiles(host: StorageHost): Promise> { - const { wasm } = host; + const { backend } = host; let opened: StorageOpened; const tabs = followOtherTabs(host); try { - opened = await wasm.open_storage(STORAGE_NAME, tabs.saved, tabs.behind, tabs.newer); + opened = await backend.open_storage(STORAGE_NAME, tabs.saved, tabs.behind, tabs.newer); } catch (error) { console.error('The files cannot be kept in this browser', error); return {}; @@ -83,16 +83,20 @@ export async function openStoredFiles(host: StorageHost): Promise> { - const { wasm } = host; + const { backend } = host; const imported: Record = {}; try { const legacy = await readLegacyData(); @@ -108,7 +112,7 @@ async function importLegacyData(host: StorageHost): Promise; }; export const EMPTY_SELECTION_STATISTICS: SelectionStatistics = { @@ -240,12 +240,12 @@ export const EMPTY_SELECTION_STATISTICS: SelectionStatistics = { lng: new Float64Array(), lat: new Float64Array(), ele: new Float64Array(), - slice: () => undefined, + slice: async () => undefined, }; export type StatisticsRequest = { - /** Replaces the metrics needed by this consumer. */ - set(metrics: Iterable): void; + /** Replaces the metrics needed by this consumer. Resolves when the missing ones are read. */ + set(metrics: Iterable): Promise; /** The consumer does not need any metric anymore. */ release(): void; };