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 000000000..856160565 Binary files /dev/null and b/tauri/icons/128x128.png differ diff --git a/tauri/icons/128x128@2x.png b/tauri/icons/128x128@2x.png new file mode 100644 index 000000000..553387065 Binary files /dev/null and b/tauri/icons/128x128@2x.png differ diff --git a/tauri/icons/32x32.png b/tauri/icons/32x32.png new file mode 100644 index 000000000..940409931 Binary files /dev/null and b/tauri/icons/32x32.png differ diff --git a/tauri/icons/64x64.png b/tauri/icons/64x64.png new file mode 100644 index 000000000..fd08e0326 Binary files /dev/null and b/tauri/icons/64x64.png differ diff --git a/tauri/icons/Square107x107Logo.png b/tauri/icons/Square107x107Logo.png new file mode 100644 index 000000000..cac95a9ff Binary files /dev/null and b/tauri/icons/Square107x107Logo.png differ diff --git a/tauri/icons/Square142x142Logo.png b/tauri/icons/Square142x142Logo.png new file mode 100644 index 000000000..c336ad215 Binary files /dev/null and b/tauri/icons/Square142x142Logo.png differ diff --git a/tauri/icons/Square150x150Logo.png b/tauri/icons/Square150x150Logo.png new file mode 100644 index 000000000..4b534f8ac Binary files /dev/null and b/tauri/icons/Square150x150Logo.png differ diff --git a/tauri/icons/Square284x284Logo.png b/tauri/icons/Square284x284Logo.png new file mode 100644 index 000000000..e84f3469e Binary files /dev/null and b/tauri/icons/Square284x284Logo.png differ diff --git a/tauri/icons/Square30x30Logo.png b/tauri/icons/Square30x30Logo.png new file mode 100644 index 000000000..b0fcaa8b5 Binary files /dev/null and b/tauri/icons/Square30x30Logo.png differ diff --git a/tauri/icons/Square310x310Logo.png b/tauri/icons/Square310x310Logo.png new file mode 100644 index 000000000..d0bdfddfc Binary files /dev/null and b/tauri/icons/Square310x310Logo.png differ diff --git a/tauri/icons/Square44x44Logo.png b/tauri/icons/Square44x44Logo.png new file mode 100644 index 000000000..16f05083e Binary files /dev/null and b/tauri/icons/Square44x44Logo.png differ diff --git a/tauri/icons/Square71x71Logo.png b/tauri/icons/Square71x71Logo.png new file mode 100644 index 000000000..0a978c899 Binary files /dev/null and b/tauri/icons/Square71x71Logo.png differ diff --git a/tauri/icons/Square89x89Logo.png b/tauri/icons/Square89x89Logo.png new file mode 100644 index 000000000..408793e96 Binary files /dev/null and b/tauri/icons/Square89x89Logo.png differ diff --git a/tauri/icons/StoreLogo.png b/tauri/icons/StoreLogo.png new file mode 100644 index 000000000..74c94dade Binary files /dev/null and b/tauri/icons/StoreLogo.png differ diff --git a/tauri/icons/icon.icns b/tauri/icons/icon.icns new file mode 100644 index 000000000..036408260 Binary files /dev/null and b/tauri/icons/icon.icns differ diff --git a/tauri/icons/icon.ico b/tauri/icons/icon.ico new file mode 100644 index 000000000..df5ab9353 Binary files /dev/null and b/tauri/icons/icon.ico differ diff --git a/tauri/icons/icon.png b/tauri/icons/icon.png new file mode 100644 index 000000000..9818ff958 Binary files /dev/null and b/tauri/icons/icon.png differ 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; };