diff --git a/.gitignore b/.gitignore index 6635cf5..8ee8913 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* +target \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..c56a0f5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,15 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 4, + "arrowParens": "avoid", + "plugins": ["prettier-plugin-svelte"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} \ No newline at end of file diff --git a/.trae/rules/git-commit-message.md b/.trae/rules/git-commit-message.md new file mode 100644 index 0000000..76a55c7 --- /dev/null +++ b/.trae/rules/git-commit-message.md @@ -0,0 +1,4 @@ +alwaysApply: true +scene: git_message +--- +使用无序列表逐条描述具体变更,每条以 `- ` 开头,动词使用一般现在时(如 Add, Fix, Update, Merge, Improve)。 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..918d64c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6883 @@ +# 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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "almost" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aa2999eb46af81abb65c2d30d446778d7e613b60bbf4e174a027e80f90a3c14" + +[[package]] +name = "alsa" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" +dependencies = [ + "alsa-sys", + "bitflags 2.13.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arrayvec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[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 = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[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 = "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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[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 = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +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 = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +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.0", + "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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" +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.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +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.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[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.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "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.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "coreaudio-rs" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" +dependencies = [ + "bitflags 2.13.0", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "cpal" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f77b11176c37874be37e8d691c946e31b2b8c357abce9526f6a99eb469e1028" +dependencies = [ + "alsa", + "block2", + "coreaudio-rs", + "dasp_sample", + "jni 0.22.4", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "objc2", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", + "web-sys", + "windows 0.62.2", + "windows-core 0.62.2", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "creek" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3106d374fa70e8a7a8cf3abf6afbb7f3016e923a1e8941a3eee2b3a21583ee" +dependencies = [ + "creek-core", + "creek-decode-symphonia", + "creek-encode-wav", +] + +[[package]] +name = "creek-core" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8d299b2001167b9a51ac46458cbdcfafc42d6a59125a265d8c1de2e16b28a3" +dependencies = [ + "rtrb", +] + +[[package]] +name = "creek-decode-symphonia" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416063cb8f815ee27036075e544a778770b577e82348f017d145a704df90f246" +dependencies = [ + "creek-core", + "log", + "symphonia 0.5.5", +] + +[[package]] +name = "creek-encode-wav" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37cb9ea02c12a68eb40a0576086394b3129b297970612592222284ff060bbf31" +dependencies = [ + "byte-slice-cast", + "creek-core", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "dbus-crossroads" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bff0bd181fba667660276c6b7ebdc50cff37ce593e7adf9e734f89c8f444e8" +dependencies = [ + "dbus", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +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.118", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.6", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +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 = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[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.118", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[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 = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[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 = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+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 = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "eq-plugin" +version = "0.1.0" +dependencies = [ + "plugin-sdk", + "serde", + "serde_json", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[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 = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fft-convolver" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecb4fbed063c755ecaa4cd1356cd82cc48e9d5c7edd5c30cd144656833fc0661" +dependencies = [ + "realfft", + "rtsan-standalone", + "thiserror 2.0.18", +] + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float_eq" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a80e3145d8ad11ba0995949bbcf48b9df2be62772b3d351ef017dff6ecb853" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[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.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[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 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[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", + "rand_core", +] + +[[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.0", + "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.118", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[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 = "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.118", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[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 = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + +[[package]] +name = "hrtf" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4de47a84fd55fa33aa5ef337016814fdc869fdad23e7898b5322fa290248e6" +dependencies = [ + "byteorder", + "rubato 0.14.1", + "rustfft", +] + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +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 = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "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 0.62.2", +] + +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +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 = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[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 = "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" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + +[[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.118", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[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 0.7.4", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[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 = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "llq" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22edcb39a6fc7e511be7ba578a719af09125391ecde44e0bd61ad3c8e003a4a3" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "lofty" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec4feeff6c7d75093278133a06e827d7af6d2bfe20b0f331f9d10338a5ec7ca" +dependencies = [ + "byteorder", + "data-encoding", + "flate2", + "lofty_attr", + "log", + "ogg_pager", + "paste", +] + +[[package]] +name = "lofty_attr" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458ace39169e4b83c4f77ae3d42d5d1d11c422feef590219a97c973d3b524557" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lyrics-plugin" +version = "0.1.0" +dependencies = [ + "lofty", + "plugin-sdk", + "serde", + "serde_json", +] + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[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 = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +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.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "my-plugin" +version = "0.1.0" +dependencies = [ + "plugin-sdk", + "serde", + "serde_json", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "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 = "no_denormals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bcfe410abc339c9f8c226aceebf946bf26e3e2eca738be3fec9e9ee97d54aa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[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_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[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.118", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +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.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.13.0", + "libc", + "objc2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[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.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.13.0", + "objc2", +] + +[[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.0", + "block2", + "dispatch2", + "libc", + "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.0", + "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.0", + "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.0", + "block2", + "libc", + "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.0", + "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.0", + "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.0", + "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.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "ogg_pager" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d36b1d6964c3ac92b7aea701057e02b6b91143d70d83b20abf75a231a3c0216" +dependencies = [ + "byteorder", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[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" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[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 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[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.118", +] + +[[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 = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "piston-float" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad78bf43dcf80e8f950c92b84f938a0fc7590b7f6866fbcbeca781609c115590" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "plugin-sdk" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[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", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + +[[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.12+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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +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 = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "realfft" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" +dependencies = [ + "rustfft", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.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 = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rtrb" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ade083ccbb4bf536df69d1f6432cc23deb7acccff86b183f3923a6fd56a1153" + +[[package]] +name = "rtsan-standalone" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd1b6d61d69481a68e555916d3a52213846f5c2d2140bdc74ebc308e2338fc3" +dependencies = [ + "rtsan-standalone-macros", + "rtsan-standalone-sys", +] + +[[package]] +name = "rtsan-standalone-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8320af894782374141c8e0d4521ee613a8f24d7ab08b6ad359881311e37b9ef" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "rtsan-standalone-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "362a9c531f731e574a870cdb28ee7d81178ba7a87ed54380eb9b8d8f0c3b5301" +dependencies = [ + "num_cpus", + "tempfile", +] + +[[package]] +name = "rubato" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6dd52e80cfc21894deadf554a5673002938ae4625f7a283e536f9cf7c17b0d5" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "realfft", +] + +[[package]] +name = "rubato" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "realfft", +] + +[[package]] +name = "rust-echo-music" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "eq-plugin", + "libloading 0.9.0", + "lofty", + "lyrics-plugin", + "plugin-sdk", + "rand", + "serde", + "serde_json", + "souvlaki", + "sqlx", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-opener", + "tauri-plugin-store", + "tokio", + "trash", + "walkdir", + "web-audio-api", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustfft" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +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.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "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.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +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.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[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.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[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.118", +] + +[[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 = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +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 = "souvlaki" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5855c8f31521af07d896b852eaa9eca974ddd3211fc2ae292e58dda8eb129bc8" +dependencies = [ + "base64 0.22.1", + "block", + "cocoa", + "core-graphics 0.22.3", + "dbus", + "dbus-crossroads", + "dispatch", + "objc", + "thiserror 1.0.69", + "windows 0.44.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64 0.22.1", + "bytes", + "cfg-if", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap 2.14.0", + "log", + "memchr", + "percent-encoding", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.118", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.118", + "thiserror 2.0.18", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags 2.13.0", + "byteorder", + "bytes", + "crc", + "digest 0.11.3", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1", + "sha2 0.11.0", + "sqlx-core", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.13.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "rand", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[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 = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "symphonia" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac 0.5.5", + "symphonia-bundle-mp3 0.5.5", + "symphonia-codec-aac 0.5.5", + "symphonia-codec-adpcm 0.5.5", + "symphonia-codec-alac 0.5.5", + "symphonia-codec-pcm 0.5.5", + "symphonia-codec-vorbis 0.5.5", + "symphonia-core 0.5.5", + "symphonia-format-isomp4 0.5.5", + "symphonia-format-mkv 0.5.5", + "symphonia-format-ogg 0.5.5", + "symphonia-format-riff 0.5.5", + "symphonia-metadata 0.5.5", +] + +[[package]] +name = "symphonia" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1758d6c853020a7244de03cc3e0185eaea3f58715122422dd3cc7452e6d4c16a" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac 0.6.0", + "symphonia-bundle-mp3 0.6.0", + "symphonia-codec-aac 0.6.0", + "symphonia-codec-adpcm 0.6.0", + "symphonia-codec-alac 0.6.0", + "symphonia-codec-pcm 0.6.0", + "symphonia-codec-vorbis 0.6.0", + "symphonia-core 0.6.0", + "symphonia-format-caf", + "symphonia-format-isomp4 0.6.0", + "symphonia-format-mkv 0.6.0", + "symphonia-format-ogg 0.6.0", + "symphonia-format-riff 0.6.0", + "symphonia-metadata 0.6.0", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" +dependencies = [ + "log", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee69ad01236a67260b82fd1ff9790dd75ead29f2f46af145e63b7e72273e0e03" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" +dependencies = [ + "lazy_static", + "log", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350f1f2f2e19ad4dd315db94304d1eb361b29af070681f94e51b8fdaad769546" +dependencies = [ + "lazy_static", + "log", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790" +dependencies = [ + "lazy_static", + "log", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1979c515a76371b186aad2feff5f23e21cbec775bf95de08bf1e3af92a2ad76" +dependencies = [ + "lazy_static", + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f" +dependencies = [ + "log", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebbdfd76d6cc5a601c6292a44357c5b7c82f2cd7cdc0f171421f5c5cff0ea1f" +dependencies = [ + "log", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-codec-alac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8413fa754942ac16a73634c9dfd1500ed5c61430956b33728567f667fdd393ab" +dependencies = [ + "log", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-alac" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a149cbfc7fb5c405d123a273227d31de17138419552112bf1aa7b73e65827b8" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" +dependencies = [ + "log", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50baee168f0e9dcf6ba7fc06e8b57eb62072a4490cc7cf13af77e72baae5d328" +dependencies = [ + "log", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" +dependencies = [ + "log", + "symphonia-core 0.5.5", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45b07b4423cd8e0fc472575909a5554b12c2f58e3c190b38c24f042e732fd8de" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-common" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8257891ffa7f05e02b58f4761e2abf7e5278c8744fd59e981559e050f86eef55" +dependencies = [ + "log", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + +[[package]] +name = "symphonia-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-core" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ec293b5f288383b72a7bffcade6b2860b642cf66f28b3bd5967349a49938b1" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "lazy_static", + "log", + "num-complex", + "rustfft", + "smallvec", +] + +[[package]] +name = "symphonia-format-caf" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde3ca76633d3400ab57195456c09f8a58d775ff5452329f3f212b6efc8622f5" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d179a01305b3505940135a9f0180d6ef4b487912748fe97554756f120fbd05e" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122d786d2c43a49beb6f397551b4a050d8229eaa54c7ddf9ee4b98899b8742d0" +dependencies = [ + "lazy_static", + "log", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb17713e134f5ad316c2690fa3104590ccc85842cdbcf82c3cd1a845cb08aa74" +dependencies = [ + "lazy_static", + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" +dependencies = [ + "log", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05a67e02b1e4fca1a261ba4fe06910a9357489ad8c36aafdd2960e9c6559433" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" +dependencies = [ + "extended", + "log", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17424452a777666d3eaf09a5c651029b15b6a333812fcc5b5474f2a3f0cff3f0" +dependencies = [ + "extended", + "log", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-metadata" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31acf5cd623398a6208e2225d18f4b20f761c55098a796a5247ad516a4a8681" +dependencies = [ + "lazy_static", + "log", + "regex-lite", + "smallvec", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" +dependencies = [ + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", +] + +[[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.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +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.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[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.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation 0.10.1", + "core-graphics 0.25.0", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni 0.21.1", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[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.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2616f96cb644bf2c5c456d9de4d5d5100e592d7424c74d8b55c5cb96e359e93" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "image", + "jni 0.21.1", + "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.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows 0.61.3", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +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.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2 0.10.9", + "syn 2.0.118", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows 0.61.3", + "zbus", +] + +[[package]] +name = "tauri-plugin-store" +version = "2.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c72dda16786eb4a3f903e43a17b64d8d78dc0f00fe2aa4b757c28f617a8630b" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni 0.21.1", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" +dependencies = [ + "gtk", + "http", + "jni 0.21.1", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +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.18", + "toml 1.1.2+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.2+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[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.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "time" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +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.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +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 = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[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 = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +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.0", + "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.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[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.0", + "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 = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[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 = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + +[[package]] +name = "trash" +version = "5.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7602e0c7d66ec2d92a8c917219fbc7894039efa2063b9064260110828a356f46" +dependencies = [ + "chrono", + "libc", + "log", + "objc2", + "objc2-foundation", + "once_cell", + "percent-encoding", + "scopeguard", + "urlencoding", + "windows 0.56.0", +] + +[[package]] +name = "tray-icon" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +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.18", + "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 = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[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 = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[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.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vecmath" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956ae1e0d85bca567dee1dcf87fb1ca2e792792f66f87dced8381f99cd91156a" +dependencies = [ + "piston-float", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +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.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +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-audio-api" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2bc19aae4c9275d5f8c12f851bcc462630bb3e4996cb9a22b52cbcc8f386bd" +dependencies = [ + "almost", + "arc-swap", + "arrayvec", + "cpal", + "creek", + "crossbeam-channel", + "dasp_sample", + "fft-convolver", + "float_eq", + "futures-channel", + "futures-core", + "futures-util", + "hound", + "hrtf", + "llq", + "log", + "no_denormals", + "num-complex", + "realfft", + "rubato 0.16.2", + "smallvec", + "symphonia 0.6.0", + "vecmath", +] + +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +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.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-implement 0.60.2", + "windows-interface 0.59.3", +] + +[[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.118", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" + +[[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.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +dependencies = [ + "windows-core 0.56.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[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.118", +] + +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[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.118", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[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_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[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_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[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 = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni 0.21.1", + "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 0.10.9", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.3", + "windows-core 0.61.2", + "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.118", + "winnow 1.0.3", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5dae22f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["src-tauri", "src-tauri/crates/plugin-sdk", "src-tauri/crates/lyrics-plugin", "plugins/my-plugin"] +resolver = "2" diff --git a/package.json b/package.json index 2f37191..da3f08e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "tauri-app", + "name": "rust-echo-music", "version": "0.1.0", - "description": "", + "description": "A desktop music player built with Rust and Tauri", "type": "module", "scripts": { "dev": "vite dev", @@ -14,21 +14,24 @@ "license": "MIT", "dependencies": { "@mdui/icons": "^1.0.3", - "@tailwindcss/vite": "^4.2.2", - "@tauri-apps/api": "^2.10.1", - "@tauri-apps/plugin-opener": "^2.5.3", - "material-symbols": "^0.43.0", + "@tailwindcss/vite": "^4.3.0", + "@tanstack/svelte-virtual": "^3.13.29", + "@tauri-apps/api": "^2.11.0", + "@tauri-apps/plugin-dialog": "~2.7.1", + "@tauri-apps/plugin-opener": "^2.5.4", + "@tauri-apps/plugin-store": "^2.4.3", "mdui": "^2.1.4", - "tailwindcss": "^4.2.2" + "tailwindcss": "^4.3.0" }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.55.0", + "@sveltejs/kit": "^2.61.1", "@sveltejs/vite-plugin-svelte": "^5.1.1", - "@tauri-apps/cli": "^2.10.1", - "svelte": "^5.55.1", - "svelte-check": "^4.4.5", + "@tauri-apps/cli": "^2.11.2", + "svelte": "^5.56.0", + "svelte-check": "^4.4.8", "typescript": "~5.6.3", - "vite": "^6.4.1" - } + "vite": "^6.4.2" + }, + "packageManager": "pnpm@11.2.2+sha512.36e6621fad506178936455e70247b8808ef4ec25797a9f437a93281a020484e2607f6a469a22e982987c3dbb8866e3071514ab10a4a1749e06edcd1ec118436f" } diff --git a/plugins/eq/plugin.json b/plugins/eq/plugin.json new file mode 100644 index 0000000..1274561 --- /dev/null +++ b/plugins/eq/plugin.json @@ -0,0 +1,39 @@ +{ + "id": "eq", + "source": "builtin", + "name": "eq", + "displayName": "Equalizer", + "version": "1.0.0", + "author": "RustEchoMusic", + "description": "10-band audio equalizer with presets", + "entry": "eq", + "minAppVersion": "0.1.0", + "permissions": ["audio"], + "activationEvents": [ + "onStartup", + { "onCommand": "eq.apply_preset" }, + { "onCommand": "eq.reset" }, + { "onCommand": "eq.toggle" } + ], + "contributes": { + "commands": [ + { "id": "eq.apply_preset", "title": "应用 EQ 预设" }, + { "id": "eq.reset", "title": "重置 EQ" }, + { "id": "eq.toggle", "title": "切换 EQ 开关" } + ], + "menus": [ + { "command": "eq.apply_preset", "title": "Apply EQ Preset", "location": "TrackContextMenu", "group": "eq" } + ], + "sidebars": [ + { "id": "eq", "title": "均衡器", "icon": "graphic_eq" } + ], + "nativeViews": [ + { "id": "eq-panel-view", "title": "Equalizer", "token": "eq-panel", "icon": "graphic_eq" } + ] + }, + "settings": [ + { "key": "enabled", "title": "启用均衡器", "defaultValue": { "type": "Bool", "value": true } }, + { "key": "preset", "title": "频段预设", "defaultValue": { "type": "Json", "value": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] } }, + { "key": "preset_name", "title": "预设名称", "defaultValue": { "type": "Text", "value": "Flat" } } + ] +} diff --git a/plugins/lyrics/plugin.json b/plugins/lyrics/plugin.json new file mode 100644 index 0000000..0ad1825 --- /dev/null +++ b/plugins/lyrics/plugin.json @@ -0,0 +1,42 @@ +{ + "id": "lyrics", + "source": "builtin", + "name": "lyrics", + "displayName": "Lyrics", + "version": "1.0.0", + "author": "RustEchoMusic", + "description": "Lyrics search, load, cache and sync display", + "entry": "lyrics", + "minAppVersion": "0.1.0", + "permissions": [ + "playerRead", + "libraryRead" + ], + "activationEvents": [ + "onStartup", + "onTrackChanged", + {"onCommand": "lyrics.search"}, + {"onCommand": "lyrics.load"}, + {"onCommand": "lyrics.clearCache"} + ], + "contributes": { + "commands": [ + { "id": "lyrics.search", "title": "搜索歌词" }, + { "id": "lyrics.load", "title": "加载歌词" }, + { "id": "lyrics.clearCache", "title": "清除歌词缓存" } + ], + "menus": [], + "sidebars": [ + { "id": "lyrics", "title": "歌词", "icon": "lyrics" } + ], + "nativeViews": [ + { "id": "lyrics-panel-view", "title": "Lyrics", "token": "lyrics-panel", "icon": "lyrics" } + ] + }, + "settings": [ + { "key": "provider", "title": "歌词来源", "defaultValue": { "type": "Text", "value": "local" } }, + { "key": "auto_search", "title": "自动搜索歌词", "defaultValue": { "type": "Bool", "value": true } }, + { "key": "auto_scroll", "title": "自动滚动", "defaultValue": { "type": "Bool", "value": true } }, + { "key": "cache_enabled", "title": "启用缓存", "defaultValue": { "type": "Bool", "value": true } } + ] +} diff --git a/plugins/my-plugin/Cargo.toml b/plugins/my-plugin/Cargo.toml new file mode 100644 index 0000000..8f9edc8 --- /dev/null +++ b/plugins/my-plugin/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "my-plugin" +version = "0.1.0" +edition = "2021" + +[lib] +path = "lib.rs" +crate-type = ["cdylib"] + +[dependencies] +plugin-sdk = { path = "../../src-tauri/crates/plugin-sdk" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/plugins/my-plugin/lib.rs b/plugins/my-plugin/lib.rs new file mode 100644 index 0000000..4883888 --- /dev/null +++ b/plugins/my-plugin/lib.rs @@ -0,0 +1,291 @@ +use std::sync::{Arc, Mutex}; + +use plugin_sdk::errors::PluginError; +use plugin_sdk::ffi::*; +use plugin_sdk::traits::Plugin; + +struct FolderPlugin { + folders: Mutex>, + files: Mutex>, +} + +#[derive(serde::Serialize, serde::Deserialize, Default)] +struct FolderPluginState { + folders: Vec, + files: std::collections::HashMap, +} + +impl FolderPlugin { + fn new() -> Self { + let state = Self::load_state(); + Self { + folders: Mutex::new(state.folders), + files: Mutex::new(state.files), + } + } + + fn load_state() -> FolderPluginState { + let path = Self::state_path(); + match std::fs::read_to_string(&path) { + Ok(data) => serde_json::from_str(&data).unwrap_or_default(), + Err(_) => FolderPluginState { + folders: Vec::new(), + files: std::collections::HashMap::new(), + }, + } + } + + fn save_state(&self) { + let path = Self::state_path(); + let _ = std::fs::create_dir_all(path.parent().unwrap()); + let state = FolderPluginState { + folders: self.folders.lock().unwrap().clone(), + files: self.files.lock().unwrap().clone(), + }; + let _ = std::fs::write(&path, serde_json::to_string_pretty(&state).unwrap()); + } + + fn state_path() -> std::path::PathBuf { + std::env::temp_dir().join("my_plugin_state.json") + } + + fn handle_add_folder(&self, path: &str) -> Result { + let mut folders = self.folders.lock().map_err(|e| PluginError::Plugin(e.to_string()))?; + if folders.contains(&path.to_string()) { + return Err(PluginError::Plugin("Folder already added".into())); + } + folders.push(path.to_string()); + drop(folders); + self.save_state(); + Ok(format!("Added folder: {}", path)) + } + + fn handle_remove_folder(&self, path: &str) -> Result { + let mut folders = self.folders.lock().map_err(|e| PluginError::Plugin(e.to_string()))?; + folders.retain(|f| f != path); + drop(folders); + self.files.lock().map_err(|e| PluginError::Plugin(e.to_string()))? + .retain(|k, _| !k.starts_with(path)); + self.save_state(); + Ok(format!("Removed folder: {}", path)) + } + + fn handle_list_folders(&self) -> Result { + let folders = self.folders.lock().map_err(|e| PluginError::Plugin(e.to_string()))?; + Ok(serde_json::to_string(&*folders).unwrap_or_default()) + } + + fn handle_list_files(&self, path: &str) -> Result { + let dir = std::path::Path::new(path); + if !dir.is_dir() { + return Err(PluginError::Plugin(format!("Not a directory: {}", path))); + } + let entries: Vec = std::fs::read_dir(dir) + .map_err(|e| PluginError::Plugin(e.to_string()))? + .filter_map(|e| e.ok()) + .filter_map(|e| e.file_name().into_string().ok()) + .collect(); + Ok(serde_json::to_string(&entries).unwrap_or_default()) + } + + fn handle_read_file(&self, path: &str) -> Result { + if !self.is_allowed_path(path) { + return Err(PluginError::Plugin("Path not in allowed folders".into())); + } + std::fs::read_to_string(path).map_err(|e| PluginError::Plugin(e.to_string())) + } + + fn handle_write_file(&self, path: &str, content: &str) -> Result { + if !self.is_allowed_path(path) { + return Err(PluginError::Plugin("Path not in allowed folders".into())); + } + std::fs::write(path, content).map_err(|e| PluginError::Plugin(e.to_string()))?; + Ok(format!("Written: {}", path)) + } + + fn is_allowed_path(&self, path: &str) -> bool { + let folders = self.folders.lock().unwrap(); + folders.iter().any(|f| path.starts_with(f.as_str())) + } +} + +impl Plugin for FolderPlugin { + fn activate(&self, _ctx: &plugin_sdk::context::PluginContext) -> Result<(), plugin_sdk::errors::PluginError> { + println!("[my-plugin] activated, {} folders loaded", self.folders.lock().unwrap().len()); + Ok(()) + } + + fn deactivate(&self) -> Result<(), plugin_sdk::errors::PluginError> { + println!("[my-plugin] deactivated"); + Ok(()) + } + + fn on_event(&self, _event: &plugin_sdk::events::PluginEvent, _ctx: &plugin_sdk::context::PluginContext) -> Result<(), plugin_sdk::errors::PluginError> { + Ok(()) + } + + fn execute_command(&self, cmd: &str, args: &plugin_sdk::traits::CommandArgs, _ctx: &plugin_sdk::context::PluginContext) -> Result<(), plugin_sdk::errors::PluginError> { + let result = match cmd { + "folder.add" => { + if let plugin_sdk::traits::CommandArgs::RawPayload(path) = args { + self.handle_add_folder(path)? + } else { + return Err(PluginError::Plugin("folder.add requires RawPayload".into())); + } + } + "folder.remove" => { + if let plugin_sdk::traits::CommandArgs::RawPayload(path) = args { + self.handle_remove_folder(path)? + } else { + return Err(PluginError::Plugin("folder.remove requires RawPayload".into())); + } + } + "folder.list" => { + self.handle_list_folders()? + } + "folder.read" => { + if let plugin_sdk::traits::CommandArgs::RawPayload(path) = args { + self.handle_read_file(path)? + } else { + return Err(PluginError::Plugin("folder.read requires RawPayload".into())); + } + } + "folder.write" => { + if let plugin_sdk::traits::CommandArgs::RawPayload(content) = args { + self.handle_write_file("todo", content)? + } else { + return Err(PluginError::Plugin("folder.write requires RawPayload".into())); + } + } + _ => return Err(plugin_sdk::errors::PluginError::Plugin(format!("Unknown command: {}", cmd))), + }; + println!("[my-plugin] {} -> {}", cmd, result); + Ok(()) + } +} + +struct FolderPluginFactory; + +impl plugin_sdk::traits::PluginFactory for FolderPluginFactory { + fn create(&self) -> Arc { + Arc::new(FolderPlugin::new()) + } +} + +static mut PLUGIN: Option> = None; + +unsafe extern "C" fn create() -> PluginHandle { + let plugin = Arc::new(FolderPlugin::new()); + PLUGIN = Some(plugin.clone()); + Arc::into_raw(plugin) as *mut std::ffi::c_void +} + +unsafe extern "C" fn destroy(handle: PluginHandle) { + PLUGIN = None; + let _ = Arc::from_raw(handle as *const FolderPlugin); +} + +unsafe extern "C" fn free_buffer(buf: ByteBuffer) { + if !buf.ptr.is_null() { + Vec::from_raw_parts(buf.ptr, buf.len, buf.len); + } +} + +unsafe extern "C" fn api_activate(_h: PluginHandle) -> i32 { + if let Some(ref p) = PLUGIN { + let _ = p.activate(&plugin_sdk::context::PluginContext::new( + std::sync::Arc::new(NoopHost), + std::sync::Arc::new(NoopPlayer), + std::sync::Arc::new(NoopLibrary), + std::sync::Arc::new(NoopQueue), + std::sync::Arc::new(NoopSettings), + std::sync::Arc::new(NoopPluginSettings), + std::sync::Arc::new(NoopAudio), + )); + } + 0 +} + +unsafe extern "C" fn api_deactivate(_h: PluginHandle) -> i32 { + if let Some(ref p) = PLUGIN { + let _ = p.deactivate(); + } + 0 +} + +unsafe extern "C" fn api_on_event(_h: PluginHandle, _e: EventPayload) -> i32 { 0 } + +unsafe extern "C" fn api_execute_command(_h: PluginHandle, _req: *const CommandRequest) -> CommandResponse { + CommandResponse { success: true, data: ByteBuffer { ptr: std::ptr::null_mut(), len: 0 } } +} + +#[no_mangle] +pub extern "C" fn get_plugin_exports() -> PluginExports { + PluginExports { + abi_version: 1, + create, + destroy, + free_buffer, + api: PluginApi { + activate: api_activate, + deactivate: api_deactivate, + on_event: api_on_event, + execute_command: api_execute_command, + }, + audio_processor: std::ptr::null_mut(), + } +} + +struct NoopHost; +impl plugin_sdk::context::HostContext for NoopHost { + fn plugin_id(&self) -> &str { "my-plugin" } + fn cache_dir(&self) -> Result { + Ok(std::env::temp_dir().join("my-plugin")) + } + fn emit_lyrics_loaded(&self, _event: plugin_sdk::events::LyricsLoadedEvent) -> Result<(), plugin_sdk::errors::PluginError> { + Ok(()) + } +} + +struct NoopPlayer; +impl plugin_sdk::api::PlayerApi for NoopPlayer { + fn play(&self) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn pause(&self) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn next(&self) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn previous(&self) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn current_track_id(&self) -> Result, plugin_sdk::errors::PluginError> { Ok(None) } +} + +struct NoopLibrary; +impl plugin_sdk::api::LibraryApi for NoopLibrary { + fn get_track_path(&self, _track_id: i64) -> Result, plugin_sdk::errors::PluginError> { Ok(None) } + fn exists(&self, _track_id: i64) -> Result { Ok(false) } +} + +struct NoopQueue; +impl plugin_sdk::api::QueueApi for NoopQueue { + fn current_queue(&self) -> Result, plugin_sdk::errors::PluginError> { Ok(vec![]) } + fn remove_track(&self, _track_id: i64) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn clear(&self) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } +} + +struct NoopSettings; +impl plugin_sdk::api::SettingsApi for NoopSettings { + fn theme(&self) -> Result { Ok("light".into()) } + fn set_theme(&self, _theme: String) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } +} + +struct NoopPluginSettings; +impl plugin_sdk::api::PluginSettingsApi for NoopPluginSettings { + fn get_setting(&self, _key: &str) -> Option { None } + fn set_setting(&self, _key: &str, _value: plugin_sdk::settings::SettingValue) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } +} + +struct NoopAudio; +impl plugin_sdk::api::AudioProcessingApi for NoopAudio { + fn set_band_gain(&self, _band: usize, _gain_db: f64) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn apply_preset(&self, _gains: [f64; 10]) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn get_bands(&self) -> Result<[f64; 10], plugin_sdk::errors::PluginError> { Ok([0.0; 10]) } + fn set_enabled(&self, _enabled: bool) -> Result<(), plugin_sdk::errors::PluginError> { Ok(()) } + fn is_enabled(&self) -> Result { Ok(false) } +} diff --git a/plugins/my-plugin/plugin.json b/plugins/my-plugin/plugin.json new file mode 100644 index 0000000..ff55b70 --- /dev/null +++ b/plugins/my-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "id": "my-plugin", + "source": "packaged", + "name": "my-plugin", + "displayName": "My Plugin", + "version": "1.0.0", + "author": "Developer", + "description": "Folder read/write plugin", + "entry": "my-plugin", + "minAppVersion": "0.1.0", + "permissions": ["libraryRead"], + "activationEvents": [ + { "onCommand": "folder.list" }, + { "onCommand": "folder.read" }, + { "onCommand": "folder.write" }, + { "onCommand": "folder.add" }, + { "onCommand": "folder.remove" } + ], + "contributes": { + "commands": [ + { "id": "folder.list", "title": "List files in folder", "category": null }, + { "id": "folder.read", "title": "Read file content", "category": null }, + { "id": "folder.write", "title": "Write file content", "category": null }, + { "id": "folder.add", "title": "Add a folder", "category": null }, + { "id": "folder.remove", "title": "Remove a folder", "category": null } + ], + "menus": [], + "sidebars": [ + { "id": "folder", "title": "Files", "icon": "folder_open" } + ] + }, + "settings": [] +} diff --git a/plugins/plugin.template.json b/plugins/plugin.template.json new file mode 100644 index 0000000..921c61f --- /dev/null +++ b/plugins/plugin.template.json @@ -0,0 +1,30 @@ +{ + "id": "", + "name": "", + "displayName": "", + "version": "1.0.0", + "author": "", + "description": "", + "entry": "", + "minAppVersion": "0.1.0", + "permissions": [ + "pluginUI" + ], + "activationEvents": [ + "onStartup" + ], + "contributes": { + "commands": [], + "menus": [], + "sidebars": [], + "views": [ + { + "id": "main", + "title": "插件主页面", + "entry": "view/index.html", + "icon": "dashboard" + } + ] + }, + "settings": [] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99a3523..c3d18bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,48 +12,54 @@ importers: specifier: ^1.0.3 version: 1.0.3 '@tailwindcss/vite': - specifier: ^4.2.2 - version: 4.2.2(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + specifier: ^4.3.0 + version: 4.3.0(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) + '@tanstack/svelte-virtual': + specifier: ^3.13.29 + version: 3.13.29(svelte@5.56.0) '@tauri-apps/api': - specifier: ^2.10.1 - version: 2.10.1 + specifier: ^2.11.0 + version: 2.11.0 + '@tauri-apps/plugin-dialog': + specifier: ~2.7.1 + version: 2.7.1 '@tauri-apps/plugin-opener': - specifier: ^2.5.3 - version: 2.5.3 - material-symbols: - specifier: ^0.43.0 - version: 0.43.0 + specifier: ^2.5.4 + version: 2.5.4 + '@tauri-apps/plugin-store': + specifier: ^2.4.3 + version: 2.4.3 mdui: specifier: ^2.1.4 version: 2.1.4 tailwindcss: - specifier: ^4.2.2 - version: 4.2.2 + specifier: ^4.3.0 + version: 4.3.0 devDependencies: '@sveltejs/adapter-static': specifier: ^3.0.10 - version: 3.0.10(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0))) + version: 3.0.10(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.0)(typescript@5.6.3)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0))) '@sveltejs/kit': - specifier: ^2.55.0 - version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + specifier: ^2.61.1 + version: 2.61.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.0)(typescript@5.6.3)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + version: 5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) '@tauri-apps/cli': - specifier: ^2.10.1 - version: 2.10.1 + specifier: ^2.11.2 + version: 2.11.2 svelte: - specifier: ^5.55.1 - version: 5.55.1 + specifier: ^5.56.0 + version: 5.56.0 svelte-check: - specifier: ^4.4.5 - version: 4.4.5(picomatch@4.0.4)(svelte@5.55.1)(typescript@5.6.3) + specifier: ^4.4.8 + version: 4.4.8(picomatch@4.0.4)(svelte@5.56.0)(typescript@5.6.3) typescript: specifier: ~5.6.3 version: 5.6.3 vite: - specifier: ^6.4.1 - version: 6.4.1(jiti@2.6.1)(lightningcss@1.32.0) + specifier: ^6.4.2 + version: 6.4.2(jiti@2.7.0)(lightningcss@1.32.0) packages: @@ -232,8 +238,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@lit-labs/ssr-dom-shim@1.5.1': - resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==} + '@lit-labs/ssr-dom-shim@1.6.0': + resolution: {integrity: sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==} '@lit/localize@0.12.2': resolution: {integrity: sha512-Qv9kvgJKDq/JVSwXOxuWvQnnOBysHA99ti9im9a4fImCmx+fto+XXcUYQbjZHqiueEEc4V20PcRDPO+1g/6seQ==} @@ -256,149 +262,149 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@rollup/rollup-android-arm-eabi@4.60.1': - resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.60.1': - resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.60.1': - resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.1': - resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.60.1': - resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.60.1': - resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.60.1': - resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.60.1': - resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.60.1': - resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.60.1': - resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.60.1': - resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.60.1': - resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.60.1': - resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.60.1': - resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.60.1': - resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.60.1': - resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.60.1': - resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.60.1': - resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.1': - resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.1': - resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.60.1': - resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} cpu: [x64] os: [win32] '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@sveltejs/acorn-typescript@1.0.9': - resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} + '@sveltejs/acorn-typescript@1.0.10': + resolution: {integrity: sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==} peerDependencies: acorn: ^8.9.0 @@ -407,15 +413,15 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.55.0': - resolution: {integrity: sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==} + '@sveltejs/kit@2.61.1': + resolution: {integrity: sha512-Ny8s1SR1TyQS2hD2Rvw0XKzU2Nw1eUF52dTb6T2bdcgz7wSC+Nyb5IwjWYlR4b2dvbbR5NJDiQwHg3rnNseghg==} engines: {node: '>=18.13'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.0.0 '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: ^5.3.3 + typescript: ^5.3.3 || ^6.0.0 vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 peerDependenciesMeta: '@opentelemetry/api': @@ -438,69 +444,69 @@ packages: svelte: ^5.0.0 vite: ^6.0.0 - '@tailwindcss/node@4.2.2': - resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} - '@tailwindcss/oxide-android-arm64@4.2.2': - resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.2.2': - resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.2.2': - resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.2.2': - resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': - resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': - resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.2.2': - resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.2.2': - resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.2.2': - resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.2.2': - resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -511,108 +517,122 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': - resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.2.2': - resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.2.2': - resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} engines: {node: '>= 20'} - '@tailwindcss/vite@4.2.2': - resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} + '@tailwindcss/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - '@tauri-apps/api@2.10.1': - resolution: {integrity: sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==} + '@tanstack/svelte-virtual@3.13.29': + resolution: {integrity: sha512-9JWVzGK7JN72Fli9xKftpjUcDolOOtH4z/oefs13Xp3C2NGkJUqMjclKGJ4vS3k2EdlLKH0/tj25J7gTYFV2ZA==} + peerDependencies: + svelte: ^3.48.0 || ^4.0.0 || ^5.0.0 + + '@tanstack/virtual-core@3.17.1': + resolution: {integrity: sha512-VZyW2Uiml5tmBZwPGrSD3Sz73OxzljQMCmzYHsUTPEuTsERf5xwa+uWb01xEzkz3ZSYTjj8NEb/mKHvgKxyZdA==} + + '@tauri-apps/api@2.11.0': + resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} - '@tauri-apps/cli-darwin-arm64@2.10.1': - resolution: {integrity: sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==} + '@tauri-apps/cli-darwin-arm64@2.11.2': + resolution: {integrity: sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.10.1': - resolution: {integrity: sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==} + '@tauri-apps/cli-darwin-x64@2.11.2': + resolution: {integrity: sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.10.1': - resolution: {integrity: sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': + resolution: {integrity: sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.10.1': - resolution: {integrity: sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==} + '@tauri-apps/cli-linux-arm64-gnu@2.11.2': + resolution: {integrity: sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-arm64-musl@2.10.1': - resolution: {integrity: sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==} + '@tauri-apps/cli-linux-arm64-musl@2.11.2': + resolution: {integrity: sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@tauri-apps/cli-linux-riscv64-gnu@2.10.1': - resolution: {integrity: sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==} + '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': + resolution: {integrity: sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-x64-gnu@2.10.1': - resolution: {integrity: sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==} + '@tauri-apps/cli-linux-x64-gnu@2.11.2': + resolution: {integrity: sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-x64-musl@2.10.1': - resolution: {integrity: sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==} + '@tauri-apps/cli-linux-x64-musl@2.11.2': + resolution: {integrity: sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@tauri-apps/cli-win32-arm64-msvc@2.10.1': - resolution: {integrity: sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==} + '@tauri-apps/cli-win32-arm64-msvc@2.11.2': + resolution: {integrity: sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.10.1': - resolution: {integrity: sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==} + '@tauri-apps/cli-win32-ia32-msvc@2.11.2': + resolution: {integrity: sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.10.1': - resolution: {integrity: sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==} + '@tauri-apps/cli-win32-x64-msvc@2.11.2': + resolution: {integrity: sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.10.1': - resolution: {integrity: sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==} + '@tauri-apps/cli@2.11.2': + resolution: {integrity: sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==} engines: {node: '>= 10'} hasBin: true - '@tauri-apps/plugin-opener@2.5.3': - resolution: {integrity: sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==} + '@tauri-apps/plugin-dialog@2.7.1': + resolution: {integrity: sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==} + + '@tauri-apps/plugin-opener@2.5.4': + resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==} + + '@tauri-apps/plugin-store@2.4.3': + resolution: {integrity: sha512-9LWPj9yMphRi9czEtUv87XHbl1b6xgd9EXpPrUnq6nG7+nbtoF84d4Kwz9xhAv/Hf30sr58pq7EOlyI936y8qw==} '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -620,13 +640,12 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@typescript-eslint/types@8.57.2': - resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -672,11 +691,11 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.6.4: - resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==} + devalue@5.8.1: + resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} - enhanced-resolve@5.20.1: - resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} engines: {node: '>=10.13.0'} esbuild@0.25.12: @@ -687,8 +706,13 @@ packages: esm-env@1.2.2: resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} - esrap@2.2.4: - resolution: {integrity: sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==} + esrap@2.2.9: + resolution: {integrity: sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -713,8 +737,8 @@ packages: is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true kleur@4.1.5: @@ -798,11 +822,11 @@ packages: lit-element@4.2.2: resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==} - lit-html@3.3.2: - resolution: {integrity: sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==} + lit-html@3.3.3: + resolution: {integrity: sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==} - lit@3.3.2: - resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==} + lit@3.3.3: + resolution: {integrity: sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==} locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} @@ -810,9 +834,6 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - material-symbols@0.43.0: - resolution: {integrity: sha512-40zgnHCv6d+Nx/9WQ6vhZitQoqtntgZNXN1YJGyn1NJRQLoB7lWWD8vpu/i68nKtZs8OxHyMudBEkS2mrUHiqQ==} - mdui@2.1.4: resolution: {integrity: sha512-QtK5xia5HXtVO7yH30QjwvvNruw5JdrJL1MEc1k6S/ZfsbHOj6BxxdYjrdv2HiN5ikkGqt5CIbZdFyq6shaZyw==} @@ -827,8 +848,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -839,16 +860,16 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - rollup@4.60.1: - resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -870,27 +891,27 @@ packages: ssr-window@5.0.1: resolution: {integrity: sha512-WVXlhQsm54HC+FnJfEbccEgNF7mKXtnFUB8Xn7rx2dsWHOlBdqezdX88Vjh6pVGaa0ZvL+PoSu7rEcBuNmxt6g==} - svelte-check@4.4.5: - resolution: {integrity: sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==} + svelte-check@4.4.8: + resolution: {integrity: sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 typescript: '>=5.0.0' - svelte@5.55.1: - resolution: {integrity: sha512-QjvU7EFemf6mRzdMGlAFttMWtAAVXrax61SZYHdkD6yoVGQ89VeyKfZD4H1JrV1WLmJBxWhFch9H6ig/87VGjw==} + svelte@5.56.0: + resolution: {integrity: sha512-kTXr26t1bchFp28ROrb957LtbujpBmBDibmqMGziVpUs7awBi96TGgX6SovrA8BNoEUDVRK2Fb9FkeYlGspoVg==} engines: {node: '>=18'} - tailwindcss@4.2.2: - resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} - tapable@2.3.2: - resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} totalist@3.0.1: @@ -905,8 +926,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - vite@6.4.1: - resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + vite@6.4.2: + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -945,10 +966,10 @@ packages: yaml: optional: true - vitefu@1.1.2: - resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==} + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: vite: optional: true @@ -1057,22 +1078,22 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@lit-labs/ssr-dom-shim@1.5.1': {} + '@lit-labs/ssr-dom-shim@1.6.0': {} '@lit/localize@0.12.2': dependencies: - lit: 3.3.2 + lit: 3.3.3 '@lit/reactive-element@2.1.2': dependencies: - '@lit-labs/ssr-dom-shim': 1.5.1 + '@lit-labs/ssr-dom-shim': 1.6.0 '@material/material-color-utilities@0.3.0': {} '@mdui/icons@1.0.3': dependencies: '@mdui/shared': 1.0.8 - lit: 3.3.2 + lit: 3.3.3 tslib: 2.8.1 '@mdui/jq@3.0.3': @@ -1084,267 +1105,282 @@ snapshots: dependencies: '@lit/reactive-element': 2.1.2 '@mdui/jq': 3.0.3 - lit: 3.3.2 + lit: 3.3.3 ssr-window: 5.0.1 tslib: 2.8.1 '@polka/url@1.0.0-next.29': {} - '@rollup/rollup-android-arm-eabi@4.60.1': + '@rollup/rollup-android-arm-eabi@4.60.4': optional: true - '@rollup/rollup-android-arm64@4.60.1': + '@rollup/rollup-android-arm64@4.60.4': optional: true - '@rollup/rollup-darwin-arm64@4.60.1': + '@rollup/rollup-darwin-arm64@4.60.4': optional: true - '@rollup/rollup-darwin-x64@4.60.1': + '@rollup/rollup-darwin-x64@4.60.4': optional: true - '@rollup/rollup-freebsd-arm64@4.60.1': + '@rollup/rollup-freebsd-arm64@4.60.4': optional: true - '@rollup/rollup-freebsd-x64@4.60.1': + '@rollup/rollup-freebsd-x64@4.60.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.1': + '@rollup/rollup-linux-arm-musleabihf@4.60.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.1': + '@rollup/rollup-linux-arm64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.60.1': + '@rollup/rollup-linux-arm64-musl@4.60.4': optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.1': + '@rollup/rollup-linux-loong64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-loong64-musl@4.60.1': + '@rollup/rollup-linux-loong64-musl@4.60.4': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.1': + '@rollup/rollup-linux-ppc64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.1': + '@rollup/rollup-linux-ppc64-musl@4.60.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.1': + '@rollup/rollup-linux-riscv64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.1': + '@rollup/rollup-linux-riscv64-musl@4.60.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.1': + '@rollup/rollup-linux-s390x-gnu@4.60.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.60.1': + '@rollup/rollup-linux-x64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-x64-musl@4.60.1': + '@rollup/rollup-linux-x64-musl@4.60.4': optional: true - '@rollup/rollup-openbsd-x64@4.60.1': + '@rollup/rollup-openbsd-x64@4.60.4': optional: true - '@rollup/rollup-openharmony-arm64@4.60.1': + '@rollup/rollup-openharmony-arm64@4.60.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.1': + '@rollup/rollup-win32-arm64-msvc@4.60.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.1': + '@rollup/rollup-win32-ia32-msvc@4.60.4': optional: true - '@rollup/rollup-win32-x64-gnu@4.60.1': + '@rollup/rollup-win32-x64-gnu@4.60.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.60.1': + '@rollup/rollup-win32-x64-msvc@4.60.4': optional: true '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': + '@sveltejs/acorn-typescript@1.0.10(acorn@8.16.0)': dependencies: acorn: 8.16.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.0)(typescript@5.6.3)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))': dependencies: - '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + '@sveltejs/kit': 2.61.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.0)(typescript@5.6.3)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) - '@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0))': + '@sveltejs/kit@2.61.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.0)(typescript@5.6.3)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0))': dependencies: '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + '@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.0) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) '@types/cookie': 0.6.0 acorn: 8.16.0 cookie: 0.6.0 - devalue: 5.6.4 + devalue: 5.8.1 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 mrmime: 2.0.1 set-cookie-parser: 3.1.0 sirv: 3.0.2 - svelte: 5.55.1 - vite: 6.4.1(jiti@2.6.1)(lightningcss@1.32.0) + svelte: 5.56.0 + vite: 6.4.2(jiti@2.7.0)(lightningcss@1.32.0) optionalDependencies: typescript: 5.6.3 - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) debug: 4.4.3 - svelte: 5.55.1 - vite: 6.4.1(jiti@2.6.1)(lightningcss@1.32.0) + svelte: 5.56.0 + vite: 6.4.2(jiti@2.7.0)(lightningcss@1.32.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0))': + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)))(svelte@5.55.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.56.0)(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) debug: 4.4.3 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.21 - svelte: 5.55.1 - vite: 6.4.1(jiti@2.6.1)(lightningcss@1.32.0) - vitefu: 1.1.2(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)) + svelte: 5.56.0 + vite: 6.4.2(jiti@2.7.0)(lightningcss@1.32.0) + vitefu: 1.1.3(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)) transitivePeerDependencies: - supports-color - '@tailwindcss/node@4.2.2': + '@tailwindcss/node@4.3.0': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.20.1 - jiti: 2.6.1 + enhanced-resolve: 5.22.1 + jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.2.2 + tailwindcss: 4.3.0 - '@tailwindcss/oxide-android-arm64@4.2.2': + '@tailwindcss/oxide-android-arm64@4.3.0': optional: true - '@tailwindcss/oxide-darwin-arm64@4.2.2': + '@tailwindcss/oxide-darwin-arm64@4.3.0': optional: true - '@tailwindcss/oxide-darwin-x64@4.2.2': + '@tailwindcss/oxide-darwin-x64@4.3.0': optional: true - '@tailwindcss/oxide-freebsd-x64@4.2.2': + '@tailwindcss/oxide-freebsd-x64@4.3.0': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.2.2': + '@tailwindcss/oxide-linux-x64-musl@4.3.0': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.2.2': + '@tailwindcss/oxide-wasm32-wasi@4.3.0': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': optional: true - '@tailwindcss/oxide@4.2.2': + '@tailwindcss/oxide@4.3.0': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.2.2 - '@tailwindcss/oxide-darwin-arm64': 4.2.2 - '@tailwindcss/oxide-darwin-x64': 4.2.2 - '@tailwindcss/oxide-freebsd-x64': 4.2.2 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 - '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 - '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 - '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 - '@tailwindcss/oxide-linux-x64-musl': 4.2.2 - '@tailwindcss/oxide-wasm32-wasi': 4.2.2 - '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 - '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - - '@tailwindcss/vite@4.2.2(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0))': + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/vite@4.3.0(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + vite: 6.4.2(jiti@2.7.0)(lightningcss@1.32.0) + + '@tanstack/svelte-virtual@3.13.29(svelte@5.56.0)': dependencies: - '@tailwindcss/node': 4.2.2 - '@tailwindcss/oxide': 4.2.2 - tailwindcss: 4.2.2 - vite: 6.4.1(jiti@2.6.1)(lightningcss@1.32.0) + '@tanstack/virtual-core': 3.17.1 + svelte: 5.56.0 + + '@tanstack/virtual-core@3.17.1': {} - '@tauri-apps/api@2.10.1': {} + '@tauri-apps/api@2.11.0': {} - '@tauri-apps/cli-darwin-arm64@2.10.1': + '@tauri-apps/cli-darwin-arm64@2.11.2': optional: true - '@tauri-apps/cli-darwin-x64@2.10.1': + '@tauri-apps/cli-darwin-x64@2.11.2': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.10.1': + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.10.1': + '@tauri-apps/cli-linux-arm64-gnu@2.11.2': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.10.1': + '@tauri-apps/cli-linux-arm64-musl@2.11.2': optional: true - '@tauri-apps/cli-linux-riscv64-gnu@2.10.1': + '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.10.1': + '@tauri-apps/cli-linux-x64-gnu@2.11.2': optional: true - '@tauri-apps/cli-linux-x64-musl@2.10.1': + '@tauri-apps/cli-linux-x64-musl@2.11.2': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.10.1': + '@tauri-apps/cli-win32-arm64-msvc@2.11.2': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.10.1': + '@tauri-apps/cli-win32-ia32-msvc@2.11.2': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.10.1': + '@tauri-apps/cli-win32-x64-msvc@2.11.2': optional: true - '@tauri-apps/cli@2.10.1': + '@tauri-apps/cli@2.11.2': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.10.1 - '@tauri-apps/cli-darwin-x64': 2.10.1 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.10.1 - '@tauri-apps/cli-linux-arm64-gnu': 2.10.1 - '@tauri-apps/cli-linux-arm64-musl': 2.10.1 - '@tauri-apps/cli-linux-riscv64-gnu': 2.10.1 - '@tauri-apps/cli-linux-x64-gnu': 2.10.1 - '@tauri-apps/cli-linux-x64-musl': 2.10.1 - '@tauri-apps/cli-win32-arm64-msvc': 2.10.1 - '@tauri-apps/cli-win32-ia32-msvc': 2.10.1 - '@tauri-apps/cli-win32-x64-msvc': 2.10.1 - - '@tauri-apps/plugin-opener@2.5.3': + '@tauri-apps/cli-darwin-arm64': 2.11.2 + '@tauri-apps/cli-darwin-x64': 2.11.2 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.2 + '@tauri-apps/cli-linux-arm64-gnu': 2.11.2 + '@tauri-apps/cli-linux-arm64-musl': 2.11.2 + '@tauri-apps/cli-linux-riscv64-gnu': 2.11.2 + '@tauri-apps/cli-linux-x64-gnu': 2.11.2 + '@tauri-apps/cli-linux-x64-musl': 2.11.2 + '@tauri-apps/cli-win32-arm64-msvc': 2.11.2 + '@tauri-apps/cli-win32-ia32-msvc': 2.11.2 + '@tauri-apps/cli-win32-x64-msvc': 2.11.2 + + '@tauri-apps/plugin-dialog@2.7.1': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@tauri-apps/plugin-opener@2.5.4': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@tauri-apps/plugin-store@2.4.3': dependencies: - '@tauri-apps/api': 2.10.1 + '@tauri-apps/api': 2.11.0 '@types/cookie@0.6.0': {} '@types/estree@1.0.8': {} - '@types/trusted-types@2.0.7': {} + '@types/estree@1.0.9': {} - '@typescript-eslint/types@8.57.2': {} + '@types/trusted-types@2.0.7': {} acorn@8.16.0: {} @@ -1370,12 +1406,12 @@ snapshots: detect-libc@2.1.2: {} - devalue@5.6.4: {} + devalue@5.8.1: {} - enhanced-resolve@5.20.1: + enhanced-resolve@5.22.1: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.2 + tapable: 2.3.3 esbuild@0.25.12: optionalDependencies: @@ -1408,10 +1444,9 @@ snapshots: esm-env@1.2.2: {} - esrap@2.2.4: + esrap@2.2.9: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - '@typescript-eslint/types': 8.57.2 fdir@6.5.0(picomatch@4.0.4): optionalDependencies: @@ -1426,9 +1461,9 @@ snapshots: is-reference@3.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 - jiti@2.6.1: {} + jiti@2.7.0: {} kleur@4.1.5: {} @@ -1483,19 +1518,19 @@ snapshots: lit-element@4.2.2: dependencies: - '@lit-labs/ssr-dom-shim': 1.5.1 + '@lit-labs/ssr-dom-shim': 1.6.0 '@lit/reactive-element': 2.1.2 - lit-html: 3.3.2 + lit-html: 3.3.3 - lit-html@3.3.2: + lit-html@3.3.3: dependencies: '@types/trusted-types': 2.0.7 - lit@3.3.2: + lit@3.3.3: dependencies: '@lit/reactive-element': 2.1.2 lit-element: 4.2.2 - lit-html: 3.3.2 + lit-html: 3.3.3 locate-character@3.0.0: {} @@ -1503,8 +1538,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - material-symbols@0.43.0: {} - mdui@2.1.4: dependencies: '@floating-ui/utils': 0.2.11 @@ -1515,7 +1548,7 @@ snapshots: '@mdui/shared': 1.0.8 classcat: 5.0.5 is-promise: 4.0.0 - lit: 3.3.2 + lit: 3.3.3 ssr-window: 5.0.1 tslib: 2.8.1 @@ -1525,49 +1558,49 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.11: {} + nanoid@3.3.12: {} picocolors@1.1.1: {} picomatch@4.0.4: {} - postcss@8.5.8: + postcss@8.5.15: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 readdirp@4.1.2: {} - rollup@4.60.1: + rollup@4.60.4: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.1 - '@rollup/rollup-android-arm64': 4.60.1 - '@rollup/rollup-darwin-arm64': 4.60.1 - '@rollup/rollup-darwin-x64': 4.60.1 - '@rollup/rollup-freebsd-arm64': 4.60.1 - '@rollup/rollup-freebsd-x64': 4.60.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 - '@rollup/rollup-linux-arm-musleabihf': 4.60.1 - '@rollup/rollup-linux-arm64-gnu': 4.60.1 - '@rollup/rollup-linux-arm64-musl': 4.60.1 - '@rollup/rollup-linux-loong64-gnu': 4.60.1 - '@rollup/rollup-linux-loong64-musl': 4.60.1 - '@rollup/rollup-linux-ppc64-gnu': 4.60.1 - '@rollup/rollup-linux-ppc64-musl': 4.60.1 - '@rollup/rollup-linux-riscv64-gnu': 4.60.1 - '@rollup/rollup-linux-riscv64-musl': 4.60.1 - '@rollup/rollup-linux-s390x-gnu': 4.60.1 - '@rollup/rollup-linux-x64-gnu': 4.60.1 - '@rollup/rollup-linux-x64-musl': 4.60.1 - '@rollup/rollup-openbsd-x64': 4.60.1 - '@rollup/rollup-openharmony-arm64': 4.60.1 - '@rollup/rollup-win32-arm64-msvc': 4.60.1 - '@rollup/rollup-win32-ia32-msvc': 4.60.1 - '@rollup/rollup-win32-x64-gnu': 4.60.1 - '@rollup/rollup-win32-x64-msvc': 4.60.1 + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 fsevents: 2.3.3 sade@1.8.1: @@ -1586,42 +1619,44 @@ snapshots: ssr-window@5.0.1: {} - svelte-check@4.4.5(picomatch@4.0.4)(svelte@5.55.1)(typescript@5.6.3): + svelte-check@4.4.8(picomatch@4.0.4)(svelte@5.56.0)(typescript@5.6.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.4) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.55.1 + svelte: 5.56.0 typescript: 5.6.3 transitivePeerDependencies: - picomatch - svelte@5.55.1: + svelte@5.56.0: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@types/estree': 1.0.8 + '@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.0) + '@types/estree': 1.0.9 '@types/trusted-types': 2.0.7 acorn: 8.16.0 aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.6.4 + devalue: 5.8.1 esm-env: 1.2.2 - esrap: 2.2.4 + esrap: 2.2.9 is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' - tailwindcss@4.2.2: {} + tailwindcss@4.3.0: {} - tapable@2.3.2: {} + tapable@2.3.3: {} - tinyglobby@0.2.15: + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 @@ -1632,21 +1667,21 @@ snapshots: typescript@5.6.3: {} - vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0): + vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.8 - rollup: 4.60.1 - tinyglobby: 0.2.15 + postcss: 8.5.15 + rollup: 4.60.4 + tinyglobby: 0.2.17 optionalDependencies: fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 2.7.0 lightningcss: 1.32.0 - vitefu@1.1.2(vite@6.4.1(jiti@2.6.1)(lightningcss@1.32.0)): + vitefu@1.1.3(vite@6.4.2(jiti@2.7.0)(lightningcss@1.32.0)): optionalDependencies: - vite: 6.4.1(jiti@2.6.1)(lightningcss@1.32.0) + vite: 6.4.2(jiti@2.7.0)(lightningcss@1.32.0) zimmerframe@1.1.4: {} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6d60a71..009a825 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -25,13 +25,19 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "almost" version = "0.2.0" @@ -40,21 +46,21 @@ checksum = "3aa2999eb46af81abb65c2d30d446778d7e613b60bbf4e174a027e80f90a3c14" [[package]] name = "alsa" -version = "0.9.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" dependencies = [ "alsa-sys", - "bitflags 2.11.0", + "bitflags 2.13.0", "cfg-if", "libc", ] [[package]] name = "alsa-sys" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" dependencies = [ "libc", "pkg-config", @@ -77,18 +83,18 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arc-swap" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" dependencies = [ "rustversion", ] [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" [[package]] name = "async-broadcast" @@ -183,14 +189,14 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "async-signal" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" dependencies = [ "async-io", "async-lock", @@ -218,7 +224,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -244,6 +250,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -252,9 +267,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -268,24 +283,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.11.0", - "cexpr", - "clang-sys", - "itertools", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.117", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -309,13 +306,19 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -325,6 +328,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -349,9 +361,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.2" +version = "8.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -360,19 +372,28 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" 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.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byte-slice-cast" @@ -392,11 +413,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" dependencies = [ "serde", ] @@ -407,7 +434,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "cairo-sys-rs", "glib", "libc", @@ -428,9 +455,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" dependencies = [ "serde_core", ] @@ -470,13 +497,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.57" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -486,15 +511,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfb" version = "0.7.3" @@ -522,11 +538,22 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "num-traits", @@ -535,14 +562,39 @@ dependencies = [ ] [[package]] -name = "clang-sys" -version = "1.8.1" +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "cocoa" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" dependencies = [ - "glob", + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", "libc", - "libloading 0.8.9", + "objc", ] [[package]] @@ -564,12 +616,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "cookie" version = "0.18.1" @@ -580,6 +626,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -596,71 +652,96 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.3.2", + "libc", +] + [[package]] name = "core-graphics" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.11.0", - "core-foundation", - "core-graphics-types", - "foreign-types", + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types 0.5.0", "libc", ] [[package]] name = "core-graphics-types" -version = "0.2.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ - "bitflags 2.11.0", - "core-foundation", + "bitflags 1.3.2", + "core-foundation 0.9.4", "libc", ] [[package]] -name = "coreaudio-rs" -version = "0.11.3" +name = "core-graphics-types" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", + "bitflags 2.13.0", + "core-foundation 0.10.1", + "libc", ] [[package]] -name = "coreaudio-sys" -version = "0.2.17" +name = "coreaudio-rs" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" +checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" dependencies = [ - "bindgen", + "bitflags 2.13.0", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", ] [[package]] name = "cpal" -version = "0.15.3" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +checksum = "5f77b11176c37874be37e8d691c946e31b2b8c357abce9526f6a99eb469e1028" dependencies = [ "alsa", - "core-foundation-sys", + "block2", "coreaudio-rs", "dasp_sample", - "jni", + "jni 0.22.4", "js-sys", "libc", "mach2", - "ndk 0.8.0", + "ndk", "ndk-context", - "oboe", - "wasm-bindgen", - "wasm-bindgen-futures", + "num-derive", + "num-traits", + "objc2", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", "web-sys", - "windows 0.54.0", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] @@ -672,6 +753,30 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc32fast" version = "1.5.0" @@ -709,7 +814,7 @@ checksum = "416063cb8f815ee27036075e544a778770b577e82348f017d145a704df90f246" dependencies = [ "creek-core", "log", - "symphonia", + "symphonia 0.5.5", ] [[package]] @@ -731,6 +836,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -739,29 +853,21 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] -name = "cssparser" -version = "0.29.6" +name = "crypto-common" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "matches", - "phf 0.10.1", - "proc-macro2", - "quote", - "smallvec", - "syn 1.0.109", + "hybrid-array", ] [[package]] @@ -773,7 +879,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.13.1", + "phf", "smallvec", ] @@ -784,17 +890,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "ctor" -version = "0.2.9" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ - "quote", - "syn 2.0.117", + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", ] [[package]] @@ -817,7 +938,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -828,7 +949,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -838,26 +959,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] -name = "deranged" -version = "0.5.8" +name = "data-encoding" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" dependencies = [ - "powerfmt", - "serde_core", + "libc", + "libdbus-sys", + "windows-sys 0.61.2", ] [[package]] -name = "derive_more" -version = "0.99.20" +name = "dbus-crossroads" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +checksum = "64bff0bd181fba667660276c6b7ebdc50cff37ce593e7adf9e734f89c8f444e8" dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", + "dbus", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", ] [[package]] @@ -878,7 +1011,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -887,8 +1020,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.6", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -912,13 +1056,19 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + [[package]] name = "dispatch2" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2", "libc", "objc2", @@ -926,13 +1076,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -955,7 +1105,7 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -965,14 +1115,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" dependencies = [ "bit-set", - "cssparser 0.36.0", - "foldhash 0.2.0", - "html5ever 0.38.0", + "cssparser", + "foldhash", + "html5ever", "precomputed-hash", - "selectors 0.36.1", - "tendril 0.5.0", + "selectors", + "tendril", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dpi" version = "0.1.2" @@ -997,6 +1153,21 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dunce" version = "1.0.5" @@ -1011,20 +1182,23 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] [[package]] name = "embed-resource" -version = "3.0.7" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47ec73ddcf6b7f23173d5c3c5a32b5507dc0a734de7730aa14abc5d5e296bb5f" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "vswhom", "winreg", ] @@ -1068,7 +1242,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1098,6 +1272,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -1127,9 +1311,9 @@ checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fdeflate" @@ -1142,14 +1326,13 @@ dependencies = [ [[package]] name = "fft-convolver" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdcf0473d3952d710173f7f2acc8ff88065928e01b7d5467ec28449a3abae4fc" +checksum = "ecb4fbed063c755ecaa4cd1356cd82cc48e9d5c7edd5c30cd144656833fc0661" dependencies = [ - "num", "realfft", - "rustfft", - "thiserror 1.0.69", + "rtsan-standalone", + "thiserror 2.0.18", ] [[package]] @@ -1185,16 +1368,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28a80e3145d8ad11ba0995949bbcf48b9df2be62772b3d351ef017dff6ecb853" [[package]] -name = "fnv" -version = "1.0.7" +name = "flume" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] [[package]] -name = "foldhash" -version = "0.1.5" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" @@ -1202,6 +1390,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -1209,7 +1406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -1220,9 +1417,15 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1238,16 +1441,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures-channel" version = "0.3.32" @@ -1275,6 +1468,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -1302,7 +1506,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1333,15 +1537,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "gdk" version = "0.18.2" @@ -1443,25 +1638,14 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -1470,7 +1654,7 @@ checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", ] [[package]] @@ -1487,15 +1671,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "wasip2", - "wasip3", + "rand_core", ] [[package]] @@ -1536,7 +1719,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "futures-channel", "futures-core", "futures-executor", @@ -1564,7 +1747,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1643,7 +1826,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1654,18 +1837,29 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "foldhash 0.1.5", + "allocator-api2", + "equivalent", + "foldhash", ] [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] [[package]] name = "heck" @@ -1691,6 +1885,24 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "hound" version = "3.5.1" @@ -1708,18 +1920,6 @@ dependencies = [ "rustfft", ] -[[package]] -name = "html5ever" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" -dependencies = [ - "log", - "mac", - "markup5ever 0.14.1", - "match_token", -] - [[package]] name = "html5ever" version = "0.38.0" @@ -1727,14 +1927,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" dependencies = [ "log", - "markup5ever 0.38.0", + "markup5ever", ] [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1769,11 +1969,20 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1784,7 +1993,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1844,17 +2052,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1862,9 +2071,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1875,9 +2084,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1889,15 +2098,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1909,15 +2118,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1928,12 +2137,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -1953,14 +2156,27 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1974,12 +2190,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -1999,16 +2215,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-docker" version = "0.2.0" @@ -2028,20 +2234,11 @@ dependencies = [ "once_cell", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "javascriptcore-rs" @@ -2075,36 +2272,79 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] [[package]] -name = "jobserver" -version = "0.1.34" +name = "jni-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" dependencies = [ - "getrandom 0.3.4", - "libc", + "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.118", ] [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -2136,35 +2376,17 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "serde", "unicode-segmentation", ] -[[package]] -name = "kuchikiki" -version = "0.8.8-speedreader" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" -dependencies = [ - "cssparser 0.29.6", - "html5ever 0.29.1", - "indexmap 2.13.0", - "selectors 0.24.0", -] - [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libappindicator" version = "0.9.0" @@ -2191,9 +2413,18 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] [[package]] name = "libloading" @@ -2207,9 +2438,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.9" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" dependencies = [ "cfg-if", "windows-link 0.2.1", @@ -2217,13 +2448,24 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.14" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2232,9 +2474,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "llq" @@ -2252,38 +2494,60 @@ dependencies = [ ] [[package]] -name = "log" -version = "0.4.29" +name = "lofty" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "dec4feeff6c7d75093278133a06e827d7af6d2bfe20b0f331f9d10338a5ec7ca" +dependencies = [ + "byteorder", + "data-encoding", + "flate2", + "lofty_attr", + "log", + "ogg_pager", + "paste", +] [[package]] -name = "mac" -version = "0.1.1" +name = "lofty_attr" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +checksum = "458ace39169e4b83c4f77ae3d42d5d1d11c422feef590219a97c973d3b524557" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] [[package]] -name = "mach2" -version = "0.4.3" +name = "log" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lyrics-plugin" +version = "0.1.0" dependencies = [ - "libc", + "lofty", + "plugin-sdk", + "serde", + "serde_json", ] [[package]] -name = "markup5ever" -version = "0.14.1" +name = "mach2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" dependencies = [ - "log", - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache 0.8.9", - "string_cache_codegen 0.5.4", - "tendril 0.4.3", + "libc", ] [[package]] @@ -2293,32 +2557,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" dependencies = [ "log", - "tendril 0.5.0", + "tendril", "web_atoms", ] [[package]] -name = "match_token" -version = "0.1.0" +name = "md-5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "cfg-if", + "digest 0.11.3", ] -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memoffset" @@ -2335,12 +2592,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2353,20 +2604,30 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" -version = "0.17.1" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" dependencies = [ "crossbeam-channel", "dpi", @@ -2377,24 +2638,10 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", -] - -[[package]] -name = "ndk" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" -dependencies = [ - "bitflags 2.11.0", - "jni-sys", - "log", - "ndk-sys 0.5.0+25.2.9519653", - "num_enum", - "thiserror 1.0.69", + "windows-sys 0.61.2", ] [[package]] @@ -2403,10 +2650,10 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.11.0", - "jni-sys", + "bitflags 2.13.0", + "jni-sys 0.3.1", "log", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "num_enum", "raw-window-handle", "thiserror 1.0.69", @@ -2420,20 +2667,11 @@ checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "ndk-sys" -version = "0.5.0+25.2.9519653" +version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", -] - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys", + "jni-sys 0.3.1", ] [[package]] @@ -2444,49 +2682,9 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "no_denormals" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a63f8a8efd33b4706cecbc4b845b0c63e9c883f8a5404a0fd7ce2a0933421f" - -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] +checksum = "b6bcfe410abc339c9f8c226aceebf946bf26e3e2eca738be3fec9e9ee97d54aa" [[package]] name = "num-complex" @@ -2499,9 +2697,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -2511,7 +2709,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2524,34 +2722,22 @@ dependencies = [ ] [[package]] -name = "num-iter" -version = "0.1.45" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "num_cpus" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "autocfg", + "hermit-abi", + "libc", ] [[package]] @@ -2573,7 +2759,16 @@ dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", ] [[package]] @@ -2592,21 +2787,93 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2", "objc2", "objc2-core-foundation", "objc2-foundation", ] +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.13.0", + "libc", + "objc2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[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.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.13.0", + "objc2", +] + +[[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.11.0", + "bitflags 2.13.0", + "block2", "dispatch2", + "libc", "objc2", ] @@ -2616,13 +2883,45 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "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.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -2644,8 +2943,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -2656,7 +2956,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "objc2", "objc2-core-foundation", ] @@ -2667,7 +2967,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -2679,47 +2979,52 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", + "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-web-kit" +name = "objc2-user-notifications" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ - "bitflags 2.11.0", - "block2", "objc2", - "objc2-app-kit", - "objc2-core-foundation", "objc2-foundation", ] [[package]] -name = "oboe" -version = "0.6.1" +name = "objc2-web-kit" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "jni", - "ndk 0.8.0", - "ndk-context", - "num-derive", - "num-traits", - "oboe-sys", + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", ] [[package]] -name = "oboe-sys" -version = "0.6.1" +name = "ogg_pager" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +checksum = "9d36b1d6964c3ac92b7aea701057e02b6b91143d70d83b20abf75a231a3c0216" dependencies = [ - "cc", + "byteorder", ] [[package]] @@ -2730,9 +3035,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open" -version = "5.3.3" +version = "5.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" dependencies = [ "dunce", "is-wsl", @@ -2810,6 +3115,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.3" @@ -2822,105 +3133,25 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "phf" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" -dependencies = [ - "phf_shared 0.8.0", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_macros 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - [[package]] name = "phf" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros 0.13.1", - "phf_shared 0.13.1", + "phf_macros", + "phf_shared", "serde", ] -[[package]] -name = "phf_codegen" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" -dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - [[package]] name = "phf_codegen" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_generator" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" -dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.5", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", + "phf_generator", + "phf_shared", ] [[package]] @@ -2930,34 +3161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_macros" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.117", + "phf_shared", ] [[package]] @@ -2966,38 +3170,11 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator", + "phf_shared", "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher 1.0.2", + "syn 2.0.118", ] [[package]] @@ -3006,7 +3183,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ - "siphasher 1.0.2", + "siphasher", ] [[package]] @@ -3015,12 +3192,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "piper" version = "0.2.5" @@ -3040,23 +3211,31 @@ checksum = "ad78bf43dcf80e8f950c92b84f938a0fc7590b7f6866fbcbeca781609c115590" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plist" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" dependencies = [ "base64 0.22.1", - "indexmap 2.13.0", + "indexmap 2.14.0", "quick-xml", "serde", "time", ] +[[package]] +name = "plugin-sdk" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "png" version = "0.17.16" @@ -3070,6 +3249,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -3086,9 +3278,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -3099,31 +3291,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "precomputed-hash" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - [[package]] name = "primal-check" version = "0.3.4" @@ -3159,7 +3332,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.5+spec-1.1.0", + "toml_edit 0.25.12+spec-1.1.0", ] [[package]] @@ -3186,12 +3359,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro2" version = "1.0.106" @@ -3202,115 +3369,57 @@ dependencies = [ ] [[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -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 = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - -[[package]] -name = "rand" -version = "0.8.5" +name = "pxfm" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] -name = "rand_chacha" -version = "0.2.2" +name = "quick-xml" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", + "memchr", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "quote" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "proc-macro2", ] [[package]] -name = "rand_core" -version = "0.5.1" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rand_core" -version = "0.6.4" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "rand_hc" -version = "0.2.0" +name = "rand" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "rand_core 0.5.1", + "chacha20", + "getrandom 0.4.3", + "rand_core", ] [[package]] -name = "rand_pcg" -version = "0.2.1" +name = "rand_core" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "raw-window-handle" @@ -3333,7 +3442,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", ] [[package]] @@ -3364,14 +3473,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -3390,17 +3499,23 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", @@ -3430,11 +3545,65 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + [[package]] name = "rtrb" -version = "0.3.3" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ade083ccbb4bf536df69d1f6432cc23deb7acccff86b183f3923a6fd56a1153" + +[[package]] +name = "rtsan-standalone" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd1b6d61d69481a68e555916d3a52213846f5c2d2140bdc74ebc308e2338fc3" +dependencies = [ + "rtsan-standalone-macros", + "rtsan-standalone-sys", +] + +[[package]] +name = "rtsan-standalone-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8320af894782374141c8e0d4521ee613a8f24d7ab08b6ad359881311e37b9ef" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "rtsan-standalone-sys" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7204ed6420f698836b76d4d5c2ec5dec7585fd5c3a788fd1cde855d1de598239" +checksum = "362a9c531f731e574a870cdb28ee7d81178ba7a87ed54380eb9b8d8f0c3b5301" +dependencies = [ + "num_cpus", + "tempfile", +] [[package]] name = "rubato" @@ -3460,11 +3629,36 @@ dependencies = [ "realfft", ] +[[package]] +name = "rust-echo-music" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "libloading 0.9.0", + "lofty", + "lyrics-plugin", + "plugin-sdk", + "rand", + "serde", + "serde_json", + "souvlaki", + "sqlx", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-opener", + "tauri-plugin-store", + "tokio", + "trash", + "walkdir", + "web-audio-api", +] + [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -3495,7 +3689,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys", @@ -3565,7 +3759,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3574,48 +3768,30 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "selectors" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" -dependencies = [ - "bitflags 1.3.2", - "cssparser 0.29.6", - "derive_more 0.99.20", - "fxhash", - "log", - "phf 0.8.0", - "phf_codegen 0.8.0", - "precomputed-hash", - "servo_arc 0.2.0", - "smallvec", -] - [[package]] name = "selectors" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 2.11.0", - "cssparser 0.36.0", - "derive_more 2.1.1", + "bitflags 2.13.0", + "cssparser", + "derive_more", "log", "new_debug_unreachable", - "phf 0.13.1", - "phf_codegen 0.13.1", + "phf", + "phf_codegen", "precomputed-hash", "rustc-hash", - "servo_arc 0.4.3", + "servo_arc", "smallvec", ] [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -3660,7 +3836,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3671,14 +3847,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -3695,7 +3871,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3709,24 +3885,25 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] [[package]] name = "serde_with" -version = "3.18.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "base64 0.22.1", + "bs58", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.13.0", + "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", "serde_core", @@ -3737,14 +3914,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.18.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3766,26 +3943,27 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "servo_arc" -version = "0.2.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" dependencies = [ - "nodrop", "stable_deref_trait", ] [[package]] -name = "servo_arc" -version = "0.4.3" +name = "sha1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ - "stable_deref_trait", + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -3795,15 +3973,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" @@ -3817,21 +4006,31 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] -name = "siphasher" -version = "0.3.11" +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -3841,15 +4040,18 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3863,7 +4065,7 @@ checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "bytemuck", "js-sys", - "ndk 0.9.0", + "ndk", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -3894,13 +4096,211 @@ dependencies = [ name = "soup3-sys" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "souvlaki" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5855c8f31521af07d896b852eaa9eca974ddd3211fc2ae292e58dda8eb129bc8" +dependencies = [ + "base64 0.22.1", + "block", + "cocoa", + "core-graphics 0.22.3", + "dbus", + "dbus-crossroads", + "dispatch", + "objc", + "thiserror 1.0.69", + "windows 0.44.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64 0.22.1", + "bytes", + "cfg-if", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap 2.14.0", + "log", + "memchr", + "percent-encoding", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.118", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.118", + "thiserror 2.0.18", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags 2.13.0", + "byteorder", + "bytes", + "crc", + "digest 0.11.3", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1", + "sha2 0.11.0", + "sqlx-core", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.13.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "rand", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", + "atoi", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", ] [[package]] @@ -3915,19 +4315,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", - "serde", -] - [[package]] name = "string_cache" version = "0.9.0" @@ -3936,32 +4323,31 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared 0.13.1", + "phf_shared", "precomputed-hash", ] [[package]] name = "string_cache_codegen" -version = "0.5.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator", + "phf_shared", "proc-macro2", "quote", ] [[package]] -name = "string_cache_codegen" -version = "0.6.1" +name = "stringprep" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", - "proc-macro2", - "quote", + "unicode-bidi", + "unicode-normalization", + "unicode-properties", ] [[package]] @@ -3988,19 +4374,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" dependencies = [ "lazy_static", - "symphonia-bundle-flac", - "symphonia-bundle-mp3", - "symphonia-codec-aac", - "symphonia-codec-adpcm", - "symphonia-codec-alac", - "symphonia-codec-pcm", - "symphonia-codec-vorbis", - "symphonia-core", - "symphonia-format-isomp4", - "symphonia-format-mkv", - "symphonia-format-ogg", - "symphonia-format-riff", - "symphonia-metadata", + "symphonia-bundle-flac 0.5.5", + "symphonia-bundle-mp3 0.5.5", + "symphonia-codec-aac 0.5.5", + "symphonia-codec-adpcm 0.5.5", + "symphonia-codec-alac 0.5.5", + "symphonia-codec-pcm 0.5.5", + "symphonia-codec-vorbis 0.5.5", + "symphonia-core 0.5.5", + "symphonia-format-isomp4 0.5.5", + "symphonia-format-mkv 0.5.5", + "symphonia-format-ogg 0.5.5", + "symphonia-format-riff 0.5.5", + "symphonia-metadata 0.5.5", +] + +[[package]] +name = "symphonia" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1758d6c853020a7244de03cc3e0185eaea3f58715122422dd3cc7452e6d4c16a" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac 0.6.0", + "symphonia-bundle-mp3 0.6.0", + "symphonia-codec-aac 0.6.0", + "symphonia-codec-adpcm 0.6.0", + "symphonia-codec-alac 0.6.0", + "symphonia-codec-pcm 0.6.0", + "symphonia-codec-vorbis 0.6.0", + "symphonia-core 0.6.0", + "symphonia-format-caf", + "symphonia-format-isomp4 0.6.0", + "symphonia-format-mkv 0.6.0", + "symphonia-format-ogg 0.6.0", + "symphonia-format-riff 0.6.0", + "symphonia-metadata 0.6.0", ] [[package]] @@ -4010,11 +4419,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" dependencies = [ "log", - "symphonia-core", - "symphonia-metadata", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", "symphonia-utils-xiph", ] +[[package]] +name = "symphonia-bundle-flac" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee69ad01236a67260b82fd1ff9790dd75ead29f2f46af145e63b7e72273e0e03" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + [[package]] name = "symphonia-bundle-mp3" version = "0.5.5" @@ -4023,8 +4444,19 @@ checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" dependencies = [ "lazy_static", "log", - "symphonia-core", - "symphonia-metadata", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350f1f2f2e19ad4dd315db94304d1eb361b29af070681f94e51b8fdaad769546" +dependencies = [ + "lazy_static", + "log", + "symphonia-core 0.6.0", ] [[package]] @@ -4035,7 +4467,19 @@ checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790" dependencies = [ "lazy_static", "log", - "symphonia-core", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1979c515a76371b186aad2feff5f23e21cbec775bf95de08bf1e3af92a2ad76" +dependencies = [ + "lazy_static", + "log", + "symphonia-common", + "symphonia-core 0.6.0", ] [[package]] @@ -4045,7 +4489,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f" dependencies = [ "log", - "symphonia-core", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebbdfd76d6cc5a601c6292a44357c5b7c82f2cd7cdc0f171421f5c5cff0ea1f" +dependencies = [ + "log", + "symphonia-core 0.6.0", ] [[package]] @@ -4055,7 +4509,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8413fa754942ac16a73634c9dfd1500ed5c61430956b33728567f667fdd393ab" dependencies = [ "log", - "symphonia-core", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-alac" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a149cbfc7fb5c405d123a273227d31de17138419552112bf1aa7b73e65827b8" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", ] [[package]] @@ -4065,7 +4530,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" dependencies = [ "log", - "symphonia-core", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50baee168f0e9dcf6ba7fc06e8b57eb62072a4490cc7cf13af77e72baae5d328" +dependencies = [ + "log", + "symphonia-core 0.6.0", ] [[package]] @@ -4075,10 +4550,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" dependencies = [ "log", - "symphonia-core", + "symphonia-core 0.5.5", "symphonia-utils-xiph", ] +[[package]] +name = "symphonia-codec-vorbis" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45b07b4423cd8e0fc472575909a5554b12c2f58e3c190b38c24f042e732fd8de" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + +[[package]] +name = "symphonia-common" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8257891ffa7f05e02b58f4761e2abf7e5278c8744fd59e981559e050f86eef55" +dependencies = [ + "log", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + [[package]] name = "symphonia-core" version = "0.5.5" @@ -4092,6 +4589,32 @@ dependencies = [ "log", ] +[[package]] +name = "symphonia-core" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ec293b5f288383b72a7bffcade6b2860b642cf66f28b3bd5967349a49938b1" +dependencies = [ + "bitflags 2.13.0", + "bytemuck", + "lazy_static", + "log", + "num-complex", + "rustfft", + "smallvec", +] + +[[package]] +name = "symphonia-format-caf" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde3ca76633d3400ab57195456c09f8a58d775ff5452329f3f212b6efc8622f5" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + [[package]] name = "symphonia-format-isomp4" version = "0.5.5" @@ -4100,11 +4623,23 @@ checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" dependencies = [ "encoding_rs", "log", - "symphonia-core", - "symphonia-metadata", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", "symphonia-utils-xiph", ] +[[package]] +name = "symphonia-format-isomp4" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d179a01305b3505940135a9f0180d6ef4b487912748fe97554756f120fbd05e" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + [[package]] name = "symphonia-format-mkv" version = "0.5.5" @@ -4113,11 +4648,23 @@ checksum = "122d786d2c43a49beb6f397551b4a050d8229eaa54c7ddf9ee4b98899b8742d0" dependencies = [ "lazy_static", "log", - "symphonia-core", - "symphonia-metadata", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", "symphonia-utils-xiph", ] +[[package]] +name = "symphonia-format-mkv" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb17713e134f5ad316c2690fa3104590ccc85842cdbcf82c3cd1a845cb08aa74" +dependencies = [ + "lazy_static", + "log", + "symphonia-common", + "symphonia-core 0.6.0", +] + [[package]] name = "symphonia-format-ogg" version = "0.5.5" @@ -4125,11 +4672,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" dependencies = [ "log", - "symphonia-core", - "symphonia-metadata", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", "symphonia-utils-xiph", ] +[[package]] +name = "symphonia-format-ogg" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05a67e02b1e4fca1a261ba4fe06910a9357489ad8c36aafdd2960e9c6559433" +dependencies = [ + "log", + "symphonia-common", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", +] + [[package]] name = "symphonia-format-riff" version = "0.5.5" @@ -4138,8 +4697,20 @@ checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" dependencies = [ "extended", "log", - "symphonia-core", - "symphonia-metadata", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17424452a777666d3eaf09a5c651029b15b6a333812fcc5b5474f2a3f0cff3f0" +dependencies = [ + "extended", + "log", + "symphonia-core 0.6.0", + "symphonia-metadata 0.6.0", ] [[package]] @@ -4151,7 +4722,20 @@ dependencies = [ "encoding_rs", "lazy_static", "log", - "symphonia-core", + "symphonia-core 0.5.5", +] + +[[package]] +name = "symphonia-metadata" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31acf5cd623398a6208e2225d18f4b20f761c55098a796a5247ad516a4a8681" +dependencies = [ + "lazy_static", + "log", + "regex-lite", + "smallvec", + "symphonia-core 0.6.0", ] [[package]] @@ -4160,8 +4744,8 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" dependencies = [ - "symphonia-core", - "symphonia-metadata", + "symphonia-core 0.5.5", + "symphonia-metadata 0.5.5", ] [[package]] @@ -4171,15 +4755,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -4203,7 +4786,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4221,32 +4804,34 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.6" +version = "0.35.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d52c379e63da659a483a958110bbde891695a0ecb53e48cc7786d5eda7bb" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "block2", - "core-foundation", - "core-graphics", + "core-foundation 0.10.1", + "core-graphics 0.25.0", "crossbeam-channel", + "dbus", "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", - "jni", + "jni 0.21.1", "libc", "log", - "ndk 0.9.0", - "ndk-context", - "ndk-sys 0.6.0+11769913", + "ndk", + "ndk-sys", "objc2", "objc2-app-kit", "objc2-foundation", + "objc2-ui-kit", "once_cell", "parking_lot", + "percent-encoding", "raw-window-handle", "tao-macros", "unicode-segmentation", @@ -4265,7 +4850,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4276,9 +4861,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.10.3" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +checksum = "c2616f96cb644bf2c5c456d9de4d5d5100e592d7424c74d8b55c5cb96e359e93" dependencies = [ "anyhow", "bytes", @@ -4291,7 +4876,8 @@ dependencies = [ "gtk", "heck 0.5.0", "http", - "jni", + "image", + "jni 0.21.1", "libc", "log", "mime", @@ -4325,23 +4911,11 @@ dependencies = [ "windows 0.61.3", ] -[[package]] -name = "tauri-app" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-opener", - "web-audio-api", -] - [[package]] name = "tauri-build" -version = "2.5.6" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" dependencies = [ "anyhow", "cargo_toml", @@ -4355,29 +4929,28 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.12+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.5.5" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" dependencies = [ "base64 0.22.1", "brotli", "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", "serde", "serde_json", - "sha2", - "syn 2.0.117", + "sha2 0.10.9", + "syn 2.0.118", "tauri-utils", "thiserror 2.0.18", "time", @@ -4388,23 +4961,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.5" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.5.4" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" dependencies = [ "anyhow", "glob", @@ -4413,15 +4986,56 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.12+spec-1.1.0", "walkdir", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + [[package]] name = "tauri-plugin-opener" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" dependencies = [ "dunce", "glob", @@ -4439,17 +5053,33 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-store" +version = "2.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c72dda16786eb4a3f903e43a17b64d8d78dc0f00fe2aa4b757c28f617a8630b" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "tauri-runtime" -version = "2.10.1" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" dependencies = [ "cookie", "dpi", "gtk", "http", - "jni", + "jni 0.21.1", "objc2", "objc2-ui-kit", "objc2-web-kit", @@ -4466,13 +5096,13 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.10.1" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" dependencies = [ "gtk", "http", - "jni", + "jni 0.21.1", "log", "objc2", "objc2-app-kit", @@ -4492,24 +5122,24 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.3" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" dependencies = [ "anyhow", "brotli", "cargo_metadata", "ctor", + "dom_query", "dunce", "glob", - "html5ever 0.29.1", "http", "infer", "json-patch", - "kuchikiki", "log", "memchr", - "phf 0.11.3", + "phf", + "plist", "proc-macro2", "quote", "regex", @@ -4521,7 +5151,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.18", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", "url", "urlpattern", "uuid", @@ -4530,13 +5160,13 @@ dependencies = [ [[package]] name = "tauri-winres" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" dependencies = [ "dunce", "embed-resource", - "toml 0.9.12+spec-1.1.0", + "toml 1.1.2+spec-1.1.0", ] [[package]] @@ -4546,21 +5176,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -4599,7 +5218,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4610,17 +5229,16 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "time" -version = "0.3.47" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -4630,15 +5248,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" dependencies = [ "num-conv", "time-core", @@ -4646,28 +5264,66 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4699,15 +5355,30 @@ version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "serde_core", - "serde_spanned 1.0.4", + "serde_spanned 1.1.1", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.15", ] +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + [[package]] name = "toml_datetime" version = "0.6.3" @@ -4728,9 +5399,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.1+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -4741,7 +5412,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -4752,7 +5423,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.3", @@ -4761,30 +5432,30 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.5+spec-1.1.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ - "indexmap 2.13.0", - "toml_datetime 1.0.1+spec-1.1.0", + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.0", + "winnow 1.0.3", ] [[package]] name = "toml_parser" -version = "1.0.10+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.0", + "winnow 1.0.3", ] [[package]] name = "toml_writer" -version = "1.0.7+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" @@ -4803,20 +5474,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -4837,6 +5508,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -4850,7 +5522,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4872,11 +5544,29 @@ dependencies = [ "strength_reduce", ] +[[package]] +name = "trash" +version = "5.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7602e0c7d66ec2d92a8c917219fbc7894039efa2063b9064260110828a356f46" +dependencies = [ + "chrono", + "libc", + "log", + "objc2", + "objc2-foundation", + "once_cell", + "percent-encoding", + "scopeguard", + "urlencoding", + "windows 0.56.0", +] + [[package]] name = "tray-icon" -version = "0.21.3" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" dependencies = [ "crossbeam-channel", "dirs", @@ -4888,10 +5578,10 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4908,9 +5598,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "uds_windows" @@ -4964,6 +5654,12 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -4971,16 +5667,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "unicode-segmentation" -version = "1.12.0" +name = "unicode-normalization" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] [[package]] -name = "unicode-xid" -version = "0.2.6" +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "url" @@ -4995,6 +5700,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "urlpattern" version = "0.3.0" @@ -5021,16 +5732,22 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vecmath" version = "1.0.0" @@ -5091,12 +5808,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -5105,27 +5816,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" dependencies = [ "cfg-if", "once_cell", @@ -5136,23 +5838,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5160,48 +5858,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.13.0", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.5.0" @@ -5215,23 +5891,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.15.5", - "indexmap 2.13.0", - "semver", -] - [[package]] name = "web-audio-api" -version = "1.2.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "015053d21b75159d00b45f8b862d9c05b783440537d9751d659ad2982911eb7f" +checksum = "6f2bc19aae4c9275d5f8c12f851bcc462630bb3e4996cb9a22b52cbcc8f386bd" dependencies = [ "almost", "arc-swap", @@ -5254,15 +5918,15 @@ dependencies = [ "realfft", "rubato 0.16.2", "smallvec", - "symphonia", + "symphonia 0.6.0", "vecmath", ] [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" dependencies = [ "js-sys", "wasm-bindgen", @@ -5270,14 +5934,14 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" dependencies = [ - "phf 0.13.1", - "phf_codegen 0.13.1", - "string_cache 0.9.0", - "string_cache_codegen 0.6.1", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", ] [[package]] @@ -5334,8 +5998,8 @@ dependencies = [ "webview2-com-sys", "windows 0.61.3", "windows-core 0.61.2", - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", ] [[package]] @@ -5346,7 +6010,7 @@ checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5360,6 +6024,12 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" + [[package]] name = "winapi" version = "0.3.9" @@ -5408,11 +6078,20 @@ dependencies = [ [[package]] name = "windows" -version = "0.54.0" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" dependencies = [ - "windows-core 0.54.0", + "windows-core 0.56.0", "windows-targets 0.52.6", ] @@ -5422,11 +6101,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -5438,12 +6129,23 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" -version = "0.54.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", "windows-result 0.1.2", "windows-targets 0.52.6", ] @@ -5454,8 +6156,8 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -5467,8 +6169,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -5482,7 +6184,29 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", ] [[package]] @@ -5493,7 +6217,18 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", ] [[package]] @@ -5504,7 +6239,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5529,6 +6264,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -5667,6 +6412,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -5828,15 +6582,12 @@ name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] [[package]] name = "winnow" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -5853,103 +6604,21 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap 2.13.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.0", - "indexmap 2.13.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.13.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wry" -version = "0.54.4" +version = "0.55.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" dependencies = [ "base64 0.22.1", "block2", @@ -5963,9 +6632,9 @@ dependencies = [ "gtk", "http", "javascriptcore-rs", - "jni", + "jni 0.21.1", "libc", - "ndk 0.9.0", + "ndk", "objc2", "objc2-app-kit", "objc2-core-foundation", @@ -5975,7 +6644,7 @@ dependencies = [ "once_cell", "percent-encoding", "raw-window-handle", - "sha2", + "sha2 0.10.9", "soup3", "tao-macros", "thiserror 2.0.18", @@ -6012,9 +6681,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -6023,21 +6692,21 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zbus" -version = "5.14.0" +version = "5.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" dependencies = [ "async-broadcast", "async-executor", @@ -6062,7 +6731,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 0.7.15", + "winnow 1.0.3", "zbus_macros", "zbus_names", "zvariant", @@ -6070,14 +6739,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.14.0" +version = "5.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "zbus_names", "zvariant", "zvariant_utils", @@ -6085,61 +6754,41 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" dependencies = [ "serde", - "winnow 0.7.15", + "winnow 1.0.3", "zvariant", ] -[[package]] -name = "zerocopy" -version = "0.8.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5030500cb2d66bdfbb4ebc9563be6ce7005a4b5d0f26be0c523870fe372ca6" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5f86989a046a79640b9d8867c823349a139367bda96549794fcc3313ce91f4e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -6148,9 +6797,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -6159,13 +6808,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6176,40 +6825,40 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zvariant" -version = "5.10.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" dependencies = [ "endi", "enumflags2", "serde", - "winnow 0.7.15", + "winnow 1.0.3", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.10.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.117", - "winnow 0.7.15", + "syn 2.0.118", + "winnow 1.0.3", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 47b049b..00337c1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,26 +1,34 @@ [package] -name = "tauri-app" +name = "rust-echo-music" version = "0.1.0" -description = "A Tauri App" -authors = ["you"] +description = "A desktop music player built with Rust and Tauri" +authors = ["RustEchoMusic"] edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [lib] -# The `_lib` suffix may seem redundant but it is necessary -# to make the lib name unique and wouldn't conflict with the bin name. -# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 -name = "tauri_app_lib" +name = "rust_echo_music_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = [] } +tauri = { version = "2", features = ["tray-icon", "image-png"] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -web-audio-api = "1.2.0" - +web-audio-api = "1.6.0" +lofty = "0.24.0" +base64 = "0.22.1" +walkdir = "2.5.0" +tauri-plugin-dialog = "2" +tauri-plugin-store = "2" +trash = "5.2.6" +souvlaki = "0.8.3" +sqlx = { version = "0.9", features = ["runtime-tokio", "sqlite", "macros", "migrate"] } +rand = "0.10.1" +tokio = { version = "1.52.3", features = ["fs"] } +libloading = "0.9" +plugin-sdk = { path = "crates/plugin-sdk" } +lyrics-plugin = { path = "crates/lyrics-plugin" } +eq-plugin = { path = "crates/eq-plugin" } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 8ff4eff..f6b915d 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -12,6 +12,8 @@ "core:window:allow-start-dragging", "core:window:allow-minimize", "core:window:allow-toggle-maximize", - "core:window:allow-close" + "core:window:allow-close", + "dialog:default", + "store:default" ] } \ No newline at end of file diff --git a/src-tauri/crates/audio-gain/Cargo.lock b/src-tauri/crates/audio-gain/Cargo.lock new file mode 100644 index 0000000..183a387 --- /dev/null +++ b/src-tauri/crates/audio-gain/Cargo.lock @@ -0,0 +1,114 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "audio-gain" +version = "0.1.0" +dependencies = [ + "plugin-sdk", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "plugin-sdk" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src-tauri/crates/audio-gain/Cargo.toml b/src-tauri/crates/audio-gain/Cargo.toml new file mode 100644 index 0000000..693d1f0 --- /dev/null +++ b/src-tauri/crates/audio-gain/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "audio-gain" +version = "0.1.0" +edition = "2021" +description = "Example packaged audio plugin verifying the shared-memory FFI protocol" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +plugin-sdk = { path = "../plugin-sdk" } + +# 独立 workspace:使本 packaged 插件脱离项目根 workspace, +# 避免被 `d:\projects\RustEchoMusic\Cargo.toml` 的 members 声明吞并, +# 从而可独立 `cargo build --manifest-path ... --release` 编译。 +[workspace] diff --git a/src-tauri/crates/audio-gain/plugin.json b/src-tauri/crates/audio-gain/plugin.json new file mode 100644 index 0000000..eced309 --- /dev/null +++ b/src-tauri/crates/audio-gain/plugin.json @@ -0,0 +1,19 @@ +{ + "id": "audio-gain", + "source": "packaged", + "name": "audio-gain", + "displayName": "Audio Gain", + "version": "0.1.0", + "author": "RustEchoMusic", + "description": "示例音频插件:将音量降低 20%(验证共享内存 FFI 协议)", + "entry": "audio-gain", + "minAppVersion": "0.1.0", + "permissions": ["audio"], + "activationEvents": ["onStartup"], + "contributes": { + "commands": [], + "menus": [], + "sidebars": [], + "nativeViews": [] + } +} diff --git a/src-tauri/crates/audio-gain/src/lib.rs b/src-tauri/crates/audio-gain/src/lib.rs new file mode 100644 index 0000000..80bb886 --- /dev/null +++ b/src-tauri/crates/audio-gain/src/lib.rs @@ -0,0 +1,132 @@ +//! audio-gain 示例 packaged 插件:将音频乘以 0.8(音量降低约 1.94 dB)。 +//! +//! 用于验证共享内存 FFI 协议端到端可用: +//! - 宿主通过 `get_plugin_exports` 取得 `PluginExports` +//! - 宿主通过 `rem_audio_processor` 静态符号取得 `*const AudioProcessorApi` +//! - 激活后 `ExternalProcessorNode` 在每个量子调用 `process` 回调 + +use plugin_sdk::ffi::{ + AudioProcessorApi, ByteBuffer, CommandRequest, CommandResponse, EventPayload, PluginApi, + PluginExports, PluginHandle, +}; +use std::ptr; + +/// 增益系数(0.8 ≈ -1.94 dB)。 +const GAIN: f32 = 0.8; + +/// 实时音频处理回调:把交错 PCM 每个样本乘以 GAIN。 +/// +/// - `input` / `output`:交错 PCM,长度 = frames * channels +/// - 返回 0 表示成功,非 0 表示错误(宿主将跳过该插件) +unsafe extern "C" fn process_audio( + _handle: PluginHandle, + input: *const f32, + output: *mut f32, + frames: usize, + channels: usize, +) -> i32 { + let total = frames.checked_mul(channels).unwrap_or(0); + if input.is_null() || output.is_null() || total == 0 { + return -1; + } + let in_slice = std::slice::from_raw_parts(input, total); + let out_slice = std::slice::from_raw_parts_mut(output, total); + for (i, &s) in in_slice.iter().enumerate() { + out_slice[i] = s * GAIN; + } + 0 +} + +static AUDIO_PROCESSOR: AudioProcessorApi = AudioProcessorApi { + init: None, + process: Some(process_audio), + reset: None, +}; + +/// `repr(transparent)` Sync 包装:裸指针 `*const T` 不实现 `Sync`,无法直接作为 +/// `static` 暴露。透明布局保证宿主 `lib.get::<*const AudioProcessorApi>(...)` +/// 读取到的内存即裸指针本身,与无包装时逐字节一致。 +/// +/// 安全性:指向的 `AUDIO_PROCESSOR` 为不可变 `static`,且 `AudioProcessorApi` +/// 由函数指针组成(自动 `Sync`),跨线程共享该指针安全(与 `dynamic_factory` +/// 中 `unsafe impl Send/Sync for DynamicPluginFactory` 同源)。 +#[repr(transparent)] +pub struct SyncAudioProcessorPtr(*const AudioProcessorApi); +unsafe impl Sync for SyncAudioProcessorPtr {} + +/// 音频处理器符号:宿主 `dynamic_factory` 通过 +/// `lib.get::<*const AudioProcessorApi>(b"rem_audio_processor")` 读取此静态指针, +/// 解引用得到 `AudioProcessorApi` 拷贝(结构体 derive Copy)。 +#[no_mangle] +pub static rem_audio_processor: SyncAudioProcessorPtr = SyncAudioProcessorPtr(&AUDIO_PROCESSOR); + +// --- 最小化 PluginExports:本插件不响应命令,但仍需导出符号供 dynamic_factory 加载 --- + +unsafe extern "C" fn create_plugin() -> PluginHandle { + ptr::null_mut() +} + +unsafe extern "C" fn destroy_plugin(_handle: PluginHandle) {} + +unsafe extern "C" fn free_buffer(_buffer: ByteBuffer) {} + +unsafe extern "C" fn activate(_handle: PluginHandle) -> i32 { + 0 +} + +unsafe extern "C" fn deactivate(_handle: PluginHandle) -> i32 { + 0 +} + +unsafe extern "C" fn on_event(_handle: PluginHandle, _payload: EventPayload) -> i32 { + 0 +} + +unsafe extern "C" fn execute_command( + _handle: PluginHandle, + _req: *const CommandRequest, +) -> CommandResponse { + CommandResponse { + success: false, + data: ByteBuffer { + ptr: ptr::null_mut(), + len: 0, + }, + } +} + +/// `repr(transparent)` Sync 包装:`PluginExports` 含 `audio_processor: *const _` +/// 裸指针,编译器不再自动派生 `Sync`,无法直接作为 `static`。透明布局使内存 +/// 与裸 `PluginExports` 一致;宿主从不直接按符号读取本静态(仅通过 +/// `get_plugin_exports` 函数取值),布局仅为保险。 +#[repr(transparent)] +pub struct SyncPluginExports(PluginExports); +unsafe impl Sync for SyncPluginExports {} + +/// 插件导出表(单一真相源)。`audio_processor` 字段按 Task 8 设计直接填充为 +/// `&AudioProcessorApi`(新插件规范);宿主当前实际通过 `rem_audio_processor` +/// 符号读取以兼容旧插件,两者保持一致。 +#[no_mangle] +pub static rem_plugin_exports: SyncPluginExports = SyncPluginExports(PluginExports { + abi_version: 1, + create: create_plugin, + destroy: destroy_plugin, + free_buffer: free_buffer, + api: PluginApi { + activate, + deactivate, + on_event, + execute_command, + }, + audio_processor: &AUDIO_PROCESSOR, +}); + +/// 宿主实际调用的入口:`dynamic_factory` 通过 +/// `lib.get(b"get_plugin_exports")` 查找此函数并调用以取得 `PluginExports` 值。 +/// +/// `PluginExports` 含裸指针与函数指针(均 Copy),但结构体本身未 derive Copy, +/// 无法直接从 static move;用 `ptr::read` 对内部字段做按位拷贝返回。 +#[no_mangle] +pub extern "C" fn get_plugin_exports() -> PluginExports { + unsafe { ptr::read(&rem_plugin_exports.0) } +} diff --git a/src-tauri/crates/eq-plugin/Cargo.toml b/src-tauri/crates/eq-plugin/Cargo.toml new file mode 100644 index 0000000..ff7756e --- /dev/null +++ b/src-tauri/crates/eq-plugin/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "eq-plugin" +version = "0.1.0" +edition = "2021" +description = "Audio equalizer plugin for Rust Echo Music" + +[dependencies] +plugin-sdk = { path = "../plugin-sdk" } +serde_json = "1" +serde = { version = "1", features = ["derive"] } diff --git a/src-tauri/crates/eq-plugin/src/lib.rs b/src-tauri/crates/eq-plugin/src/lib.rs new file mode 100644 index 0000000..57d7432 --- /dev/null +++ b/src-tauri/crates/eq-plugin/src/lib.rs @@ -0,0 +1,166 @@ +use std::sync::{Arc, RwLock}; + +use plugin_sdk::context::PluginContext; +use plugin_sdk::errors::PluginError; +use plugin_sdk::events::PluginEvent; +use plugin_sdk::settings::SettingValue; +use plugin_sdk::traits::{CommandArgs, Plugin, PluginFactory}; + +/// 10 段音频均衡器插件。 +/// +/// 内部缓存当前 bands 与 enabled 状态,方便快速查询; +/// 真正作用于音频引擎的操作全部经由 `ctx.audio` 完成。 +pub struct EqPlugin { + bands: RwLock<[f64; 10]>, + enabled: RwLock, +} + +impl EqPlugin { + pub fn new() -> Self { + Self { + bands: RwLock::new([0.0; 10]), + enabled: RwLock::new(true), + } + } + + /// 从插件设置读取 `preset` 与 `enabled` 并应用到音频引擎。 + /// + /// - `preset` 解析失败(缺失 / 非 Json / 长度不符)时回退到 flat [0;10]。 + /// - `enabled` 缺失或非 Bool 时回退到 true。 + /// 解析结果会同步写入内部缓存。 + fn apply_from_settings(&self, ctx: &PluginContext) -> Result<(), PluginError> { + // 读取预设频段,默认 flat + let bands: [f64; 10] = ctx + .plugin_settings + .get_setting("preset") + .and_then(|v| match v { + SettingValue::Json(v) => serde_json::from_value::<[f64; 10]>(v).ok(), + _ => None, + }) + .unwrap_or([0.0; 10]); + + // 读取启用状态,默认 true + let enabled: bool = ctx + .plugin_settings + .get_setting("enabled") + .and_then(|v| match v { + SettingValue::Bool(b) => Some(b), + _ => None, + }) + .unwrap_or(true); + + // 应用到音频引擎 + ctx.audio.apply_preset(bands)?; + ctx.audio.set_enabled(enabled)?; + + // 同步内部缓存 + if let Ok(mut b) = self.bands.write() { + *b = bands; + } + if let Ok(mut e) = self.enabled.write() { + *e = enabled; + } + Ok(()) + } +} + +impl Plugin for EqPlugin { + fn activate(&self, ctx: &PluginContext) -> Result<(), PluginError> { + self.apply_from_settings(ctx) + } + + fn deactivate(&self) -> Result<(), PluginError> { + // deactivate 无 ctx 参数,仅清内部缓存状态; + // 音频引擎侧的启停由 activate / 事件处理负责。 + if let Ok(mut b) = self.bands.write() { + *b = [0.0; 10]; + } + if let Ok(mut e) = self.enabled.write() { + *e = false; + } + Ok(()) + } + + fn on_event(&self, event: &PluginEvent, ctx: &PluginContext) -> Result<(), PluginError> { + match event { + PluginEvent::Startup => self.apply_from_settings(ctx), + PluginEvent::SettingsChanged => self.apply_from_settings(ctx), + _ => Ok(()), + } + } + + fn execute_command( + &self, + command_id: &str, + args: &CommandArgs, + ctx: &PluginContext, + ) -> Result<(), PluginError> { + match command_id { + // 应用 EQ 预设:RawPayload 携带 JSON 数组,如 "[0,0,0,0,0,0,0,0,0,0]"。 + // 当从 TrackContextMenu 触发时,args 为 TrackId(EQ 是全局效果,忽略曲目上下文), + // 此时回退到设置中的当前 preset 重新应用,保证菜单项可点击以验证菜单扩展渲染。 + "eq.apply_preset" => { + let bands: [f64; 10] = match args { + CommandArgs::RawPayload(s) => serde_json::from_str(s).map_err(|e| { + PluginError::Plugin(format!("Invalid preset JSON: {}", e)) + })?, + CommandArgs::TrackId(_) => { + // 从已缓存的 bands 重新应用到音频引擎(无操作变化) + self.bands.read().map(|b| *b).unwrap_or([0.0; 10]) + } + _ => { + return Err(PluginError::Plugin( + "eq.apply_preset requires RawPayload or TrackId".into(), + )) + } + }; + ctx.audio.apply_preset(bands)?; + if let Ok(mut b) = self.bands.write() { + *b = bands; + } + let value = serde_json::to_value(&bands) + .map_err(|e| PluginError::Plugin(format!("Failed to serialize preset: {}", e)))?; + ctx.plugin_settings + .set_setting("preset", SettingValue::Json(value))?; + Ok(()) + } + // 重置 EQ 为 flat + "eq.reset" => { + let bands: [f64; 10] = [0.0; 10]; + ctx.audio.apply_preset(bands)?; + if let Ok(mut b) = self.bands.write() { + *b = bands; + } + let value = serde_json::to_value(&bands) + .map_err(|e| PluginError::Plugin(format!("Failed to serialize preset: {}", e)))?; + ctx.plugin_settings + .set_setting("preset", SettingValue::Json(value))?; + Ok(()) + } + // 切换 EQ 开关 + "eq.toggle" => { + let cur = ctx.audio.is_enabled()?; + let next = !cur; + ctx.audio.set_enabled(next)?; + if let Ok(mut e) = self.enabled.write() { + *e = next; + } + ctx.plugin_settings + .set_setting("enabled", SettingValue::Bool(next))?; + Ok(()) + } + _ => Err(PluginError::Plugin(format!( + "Unknown command: {}", + command_id + ))), + } + } +} + +pub struct EqPluginFactory; + +impl PluginFactory for EqPluginFactory { + fn create(&self) -> Arc { + Arc::new(EqPlugin::new()) + } +} diff --git a/src-tauri/crates/lyrics-plugin/Cargo.toml b/src-tauri/crates/lyrics-plugin/Cargo.toml new file mode 100644 index 0000000..a354368 --- /dev/null +++ b/src-tauri/crates/lyrics-plugin/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "lyrics-plugin" +version = "0.1.0" +edition = "2021" +description = "Lyrics plugin for Rust Echo Music" + +[dependencies] +plugin-sdk = { path = "../plugin-sdk" } +lofty = "0.24.0" +serde_json = "1" +serde = { version = "1", features = ["derive"] } diff --git a/src-tauri/crates/lyrics-plugin/src/cache.rs b/src-tauri/crates/lyrics-plugin/src/cache.rs new file mode 100644 index 0000000..31244cc --- /dev/null +++ b/src-tauri/crates/lyrics-plugin/src/cache.rs @@ -0,0 +1,54 @@ +use std::path::PathBuf; + +use super::models::LyricDocument; + +pub struct LyricsCacheService { + cache_dir: PathBuf, +} + +impl LyricsCacheService { + pub fn new(cache_dir: PathBuf) -> Self { + Self { cache_dir } + } + + pub fn init(&self) -> Result<(), String> { + std::fs::create_dir_all(&self.cache_dir) + .map_err(|e| format!("Failed to create lyrics cache dir: {}", e)) + } + + fn cache_path(&self, song_id: i64) -> PathBuf { + self.cache_dir.join(format!("{}.json", song_id)) + } + + pub fn save(&self, song_id: i64, document: &LyricDocument) -> Result<(), String> { + let content = serde_json::to_string_pretty(document) + .map_err(|e| format!("Failed to serialize lyrics: {}", e))?; + std::fs::write(self.cache_path(song_id), content) + .map_err(|e| format!("Failed to write lyrics cache: {}", e)) + } + + pub fn load(&self, song_id: i64) -> Option { + let path = self.cache_path(song_id); + if !path.exists() { + return None; + } + let content = std::fs::read_to_string(path).ok()?; + serde_json::from_str(&content).ok() + } + + pub fn exists(&self, song_id: i64) -> bool { + self.cache_path(song_id).exists() + } + + pub fn clear(&self) -> Result<(), String> { + if !self.cache_dir.exists() { + return Ok(()); + } + let entries = std::fs::read_dir(&self.cache_dir) + .map_err(|e| format!("Failed to read cache dir: {}", e))?; + for entry in entries.flatten() { + let _ = std::fs::remove_file(entry.path()); + } + Ok(()) + } +} diff --git a/src-tauri/crates/lyrics-plugin/src/lib.rs b/src-tauri/crates/lyrics-plugin/src/lib.rs new file mode 100644 index 0000000..48e62f6 --- /dev/null +++ b/src-tauri/crates/lyrics-plugin/src/lib.rs @@ -0,0 +1,196 @@ +use std::sync::{Arc, RwLock}; + +use plugin_sdk::context::PluginContext; +use plugin_sdk::errors::PluginError; +use plugin_sdk::events::{LyricsLoadedEvent, PluginEvent}; +use plugin_sdk::settings::SettingValue; +use plugin_sdk::traits::{CommandArgs, Plugin, PluginFactory}; + +mod cache; +mod models; +mod service; + +use cache::LyricsCacheService; +use models::LyricDocument; +use service::LyricsService; + +struct LyricsState { + song_id: Option, + lyrics: Option, +} + +pub struct LyricsPlugin { + cache: RwLock>, + state: RwLock, +} + +impl LyricsPlugin { + pub fn new() -> Self { + Self { + cache: RwLock::new(None), + state: RwLock::new(LyricsState { song_id: None, lyrics: None }), + } + } + + fn get_bool_setting(ctx: &PluginContext, key: &str) -> bool { + ctx.plugin_settings + .get_setting(key) + .and_then(|v| match v { + SettingValue::Bool(b) => Some(b), + _ => None, + }) + .unwrap_or(true) + } + + fn init_cache(&self, ctx: &PluginContext) { + let mut cache_lock = match self.cache.write() { + Ok(guard) => guard, + Err(_) => return, + }; + if cache_lock.is_some() { + return; + } + let cache_dir = match ctx.cache_dir() { + Ok(dir) => dir, + Err(_) => return, + }; + let svc = LyricsCacheService::new(cache_dir); + if let Err(e) = svc.init() { + eprintln!("[Lyrics] Failed to init cache: {}", e); + } + *cache_lock = Some(svc); + } + + fn handle_track_changed(&self, track_id: i64, file_path: &str, ctx: &PluginContext) { + { + let mut state = match self.state.write() { + Ok(guard) => guard, + Err(_) => return, + }; + if state.song_id == Some(track_id) { + return; + } + state.song_id = Some(track_id); + state.lyrics = None; + } + + let auto_search = Self::get_bool_setting(ctx, "auto_search"); + if !auto_search { + return; + } + + let cache_guard = match self.cache.read() { + Ok(guard) => guard, + Err(_) => return, + }; + let cache = match cache_guard.as_ref() { + Some(c) => c, + None => return, + }; + + let path = std::path::Path::new(file_path); + if let Some(doc) = LyricsService::load_or_fetch(cache, track_id, path) { + let event = LyricsLoadedEvent { + song_id: doc.song_id, + timestamp_ms_list: doc.lines.iter().map(|l| l.timestamp_ms).collect(), + text_list: doc.lines.iter().map(|l| l.text.clone()).collect(), + }; + if let Err(e) = ctx.emit_lyrics_loaded(event) { + eprintln!("[Lyrics] Failed to emit lyrics: {}", e); + } + if let Ok(mut state) = self.state.write() { + if state.song_id == Some(track_id) { + state.lyrics = Some(doc); + } + } + } + } +} + +impl Plugin for LyricsPlugin { + fn activate(&self, ctx: &PluginContext) -> Result<(), PluginError> { + self.init_cache(ctx); + Ok(()) + } + + fn deactivate(&self) -> Result<(), PluginError> { + if let Ok(mut state) = self.state.write() { + state.song_id = None; + state.lyrics = None; + } + Ok(()) + } + + fn on_event(&self, event: &PluginEvent, ctx: &PluginContext) -> Result<(), PluginError> { + if let PluginEvent::TrackChanged { track, .. } = event { + let path = ctx.library.get_track_path(track.id)?; + if let Some(path) = path { + self.handle_track_changed(track.id, &path.to_string_lossy(), ctx); + } + } + Ok(()) + } + + fn execute_command( + &self, + command_id: &str, + args: &CommandArgs, + ctx: &PluginContext, + ) -> Result<(), PluginError> { + match command_id { + "lyrics.search" => { + if let CommandArgs::LyricsSearch { title, artist } = args { + println!("[Lyrics] search '{} - {}' (local only)", title, artist); + Ok(()) + } else { + Err(PluginError::Plugin("lyrics.search requires LyricsSearch args".into())) + } + } + "lyrics.load" => { + if let CommandArgs::TrackId(song_id) = args { + let path = ctx.library.get_track_path(*song_id)? + .ok_or_else(|| PluginError::Plugin(format!("Track path not found: {}", song_id)))?; + + let cache_guard = self.cache.read().map_err(|e| PluginError::Plugin(e.to_string()))?; + let cache = cache_guard.as_ref() + .ok_or_else(|| PluginError::Plugin("Cache not initialized".into()))?; + + if let Some(doc) = LyricsService::load_or_fetch(cache, *song_id, &path) { + let event = LyricsLoadedEvent { + song_id: doc.song_id, + timestamp_ms_list: doc.lines.iter().map(|l| l.timestamp_ms).collect(), + text_list: doc.lines.iter().map(|l| l.text.clone()).collect(), + }; + ctx.emit_lyrics_loaded(event)?; + if let Ok(mut state) = self.state.write() { + state.song_id = Some(*song_id); + state.lyrics = Some(doc); + } + return Ok(()); + } + + Err(PluginError::Plugin(format!("No lyrics found for song_id: {}", song_id))) + } else { + Err(PluginError::Plugin("lyrics.load requires TrackId".into())) + } + } + "lyrics.clear_cache" => { + if let Ok(cache_guard) = self.cache.read() { + if let Some(ref cache) = *cache_guard { + cache.clear().map_err(|e| PluginError::Plugin(e.to_string()))?; + } + } + Ok(()) + } + _ => Err(PluginError::Plugin(format!("Unknown command: {}", command_id))), + } + } +} + +pub struct LyricsPluginFactory; + +impl PluginFactory for LyricsPluginFactory { + fn create(&self) -> Arc { + Arc::new(LyricsPlugin::new()) + } +} diff --git a/src-tauri/crates/lyrics-plugin/src/models.rs b/src-tauri/crates/lyrics-plugin/src/models.rs new file mode 100644 index 0000000..4cca026 --- /dev/null +++ b/src-tauri/crates/lyrics-plugin/src/models.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LyricLine { + pub timestamp_ms: u64, + pub text: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LyricDocument { + pub song_id: i64, + pub lines: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LyricsSearchResult { + pub song_id: i64, + pub title: String, + pub artist: String, + pub source: String, +} diff --git a/src-tauri/crates/lyrics-plugin/src/service.rs b/src-tauri/crates/lyrics-plugin/src/service.rs new file mode 100644 index 0000000..659de51 --- /dev/null +++ b/src-tauri/crates/lyrics-plugin/src/service.rs @@ -0,0 +1,113 @@ +use std::path::Path; + +use lofty::prelude::*; +use lofty::probe::Probe; +use lofty::tag::ItemKey; + +use super::cache::LyricsCacheService; +use super::models::{LyricDocument, LyricLine}; + +pub struct LyricsService; + +impl LyricsService { + pub fn read_from_file(file_path: &Path, song_id: i64) -> Option { + let tagged_file = Probe::open(file_path).ok()?.read().ok()?; + let tag = tagged_file + .primary_tag() + .or_else(|| tagged_file.first_tag())?; + + let lyrics_text = tag + .get_items(ItemKey::Lyrics) + .next() + .or_else(|| tag.get_items(ItemKey::UnsyncLyrics).next()) + .and_then(|item| item.value().text())?; + + let lines = Self::parse_lrc(lyrics_text); + + if lines.is_empty() { + return None; + } + + Some(LyricDocument { song_id, lines }) + } + + pub fn load_or_fetch( + cache: &LyricsCacheService, + song_id: i64, + file_path: &Path, + ) -> Option { + if let Some(doc) = cache.load(song_id) { + return Some(doc); + } + + let doc = Self::read_from_file(file_path, song_id)?; + + if let Err(e) = cache.save(song_id, &doc) { + eprintln!( + "[Lyrics] Failed to cache lyrics for song {}: {}", + song_id, e + ); + } + + Some(doc) + } + + pub fn parse_lrc(content: &str) -> Vec { + let mut lines = Vec::new(); + for line in content.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + if let Some(parsed) = Self::parse_lrc_line(trimmed) { + lines.push(parsed); + } + } + lines.sort_by(|a, b| a.timestamp_ms.cmp(&b.timestamp_ms)); + lines + } + + fn parse_lrc_line(line: &str) -> Option { + if !line.starts_with('[') { + return None; + } + let close = line.find(']')?; + let tag = &line[1..close]; + let text = line[close + 1..].trim().to_string(); + + let timestamp_ms = Self::parse_lrc_timestamp(tag)?; + Some(LyricLine { timestamp_ms, text }) + } + + fn parse_lrc_timestamp(tag: &str) -> Option { + let parts: Vec<&str> = tag.split(':').collect(); + + let (hours, minutes, sec_str) = match parts.len() { + 2 => (0u64, parts[0].parse::().ok()?, parts[1]), + 3 => ( + parts[0].parse::().ok()?, + parts[1].parse::().ok()?, + parts[2], + ), + _ => return None, + }; + + let sec_parts: Vec<&str> = sec_str.split('.').collect(); + if sec_parts.len() != 2 { + return None; + } + + let seconds: u64 = sec_parts[0].parse().ok()?; + let frac_str = sec_parts[1]; + let frac_value: u64 = frac_str.parse().ok()?; + + let milliseconds = match frac_str.len() { + 1 => frac_value * 100, + 2 => frac_value * 10, + 3 => frac_value, + _ => frac_value, + }; + + Some(hours * 3_600_000 + minutes * 60_000 + seconds * 1_000 + milliseconds) + } +} diff --git a/src-tauri/crates/plugin-sdk/Cargo.toml b/src-tauri/crates/plugin-sdk/Cargo.toml new file mode 100644 index 0000000..7d87273 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "plugin-sdk" +version = "0.1.0" +edition = "2021" +description = "Plugin SDK for Rust Echo Music" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/src-tauri/crates/plugin-sdk/src/api.rs b/src-tauri/crates/plugin-sdk/src/api.rs new file mode 100644 index 0000000..32b98a4 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/api.rs @@ -0,0 +1,46 @@ +use std::path::PathBuf; + +use crate::errors::PluginError; + +pub trait PlayerApi: Send + Sync { + fn play(&self) -> Result<(), PluginError>; + fn pause(&self) -> Result<(), PluginError>; + fn next(&self) -> Result<(), PluginError>; + fn previous(&self) -> Result<(), PluginError>; + fn current_track_id(&self) -> Result, PluginError>; +} + +pub trait LibraryApi: Send + Sync { + fn get_track_path(&self, track_id: i64) -> Result, PluginError>; + fn exists(&self, track_id: i64) -> Result; +} + +pub trait QueueApi: Send + Sync { + fn current_queue(&self) -> Result, PluginError>; + fn remove_track(&self, track_id: i64) -> Result<(), PluginError>; + fn clear(&self) -> Result<(), PluginError>; +} + +pub trait SettingsApi: Send + Sync { + fn theme(&self) -> Result; + fn set_theme(&self, theme: String) -> Result<(), PluginError>; +} + +pub trait PluginSettingsApi: Send + Sync { + fn get_setting(&self, key: &str) -> Option; + fn set_setting(&self, key: &str, value: crate::settings::SettingValue) -> Result<(), PluginError>; +} + +/// 音频处理 API:暴露 10 段 EQ 给插件控制。 +pub trait AudioProcessingApi: Send + Sync { + /// 设置指定频段(0..10)的增益(dB)。 + fn set_band_gain(&self, band: usize, gain_db: f64) -> Result<(), PluginError>; + /// 一次性应用 10 段 EQ 预设(dB)。 + fn apply_preset(&self, gains: [f64; 10]) -> Result<(), PluginError>; + /// 读取当前 10 段增益(dB)。 + fn get_bands(&self) -> Result<[f64; 10], PluginError>; + /// 启用 / 旁路 EQ。 + fn set_enabled(&self, enabled: bool) -> Result<(), PluginError>; + /// 查询 EQ 是否启用。 + fn is_enabled(&self) -> Result; +} diff --git a/src-tauri/crates/plugin-sdk/src/context.rs b/src-tauri/crates/plugin-sdk/src/context.rs new file mode 100644 index 0000000..81df525 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/context.rs @@ -0,0 +1,56 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use crate::api::{AudioProcessingApi, LibraryApi, PlayerApi, PluginSettingsApi, QueueApi, SettingsApi}; +use crate::errors::PluginError; +use crate::events::LyricsLoadedEvent; + +pub trait HostContext: Send + Sync { + fn plugin_id(&self) -> &str; + fn cache_dir(&self) -> Result; + fn emit_lyrics_loaded(&self, event: LyricsLoadedEvent) -> Result<(), PluginError>; +} + +pub struct PluginContext { + pub host: Arc, + pub player: Arc, + pub library: Arc, + pub queue: Arc, + pub settings: Arc, + pub plugin_settings: Arc, + pub audio: Arc, +} + +impl PluginContext { + pub fn new( + host: Arc, + player: Arc, + library: Arc, + queue: Arc, + settings: Arc, + plugin_settings: Arc, + audio: Arc, + ) -> Self { + Self { + host, + player, + library, + queue, + settings, + plugin_settings, + audio, + } + } + + pub fn plugin_id(&self) -> &str { + self.host.plugin_id() + } + + pub fn cache_dir(&self) -> Result { + self.host.cache_dir() + } + + pub fn emit_lyrics_loaded(&self, event: LyricsLoadedEvent) -> Result<(), PluginError> { + self.host.emit_lyrics_loaded(event) + } +} diff --git a/src-tauri/crates/plugin-sdk/src/contributes.rs b/src-tauri/crates/plugin-sdk/src/contributes.rs new file mode 100644 index 0000000..24fdb60 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/contributes.rs @@ -0,0 +1,48 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CommandContribution { + pub id: String, + pub title: String, + pub category: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum MenuLocation { + TrackContextMenu, + SidebarActions, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MenuContribution { + pub command: String, + pub title: String, + pub location: MenuLocation, + pub group: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SidebarContribution { + pub id: String, + pub title: String, + pub icon: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ViewContribution { + pub id: String, + pub title: String, + pub entry: String, + pub icon: Option, +} + +/// 原生视图贡献:插件声明它要使用应用预置的哪个原生组件。 +/// 应用按 `token` 查找对应原生组件并注入渲染。 +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NativeViewContribution { + pub id: String, + pub title: String, + /// 组件令牌,应用按此查找组件 + pub token: String, + pub icon: Option, +} diff --git a/src-tauri/crates/plugin-sdk/src/errors.rs b/src-tauri/crates/plugin-sdk/src/errors.rs new file mode 100644 index 0000000..766f67c --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/errors.rs @@ -0,0 +1,43 @@ +use crate::permissions::PluginPermission; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PluginError { + Plugin(String), + PermissionDenied { + plugin_id: String, + permission: PluginPermission, + }, + Io(String), +} + +impl std::fmt::Display for PluginError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PluginError::Plugin(msg) => write!(f, "Plugin error: {}", msg), + PluginError::PermissionDenied { plugin_id, permission } => { + write!(f, "Plugin '{}' denied permission {:?}", plugin_id, permission) + } + PluginError::Io(msg) => write!(f, "Plugin IO error: {}", msg), + } + } +} + +impl std::error::Error for PluginError {} + +impl From for PluginError { + fn from(e: std::io::Error) -> Self { + PluginError::Io(e.to_string()) + } +} + +impl From for PluginError { + fn from(s: String) -> Self { + PluginError::Plugin(s) + } +} + +impl From<&str> for PluginError { + fn from(s: &str) -> Self { + PluginError::Plugin(s.to_string()) + } +} diff --git a/src-tauri/crates/plugin-sdk/src/events.rs b/src-tauri/crates/plugin-sdk/src/events.rs new file mode 100644 index 0000000..77878ac --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/events.rs @@ -0,0 +1,33 @@ +use serde::Serialize; + +#[derive(Clone, Debug, Serialize)] +pub struct TrackSnapshot { + pub id: i64, + pub title: String, + pub artist: Option, + pub album: Option, + pub duration: i64, +} + +#[derive(Clone, Debug, Serialize)] +pub struct LyricsLoadedEvent { + pub song_id: i64, + pub timestamp_ms_list: Vec, + pub text_list: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub enum PluginEvent { + Startup, + TrackChanged { + track: TrackSnapshot, + index: usize, + }, + PlaybackStateChanged { + playing: bool, + current_time: f64, + }, + QueueChanged, + SettingsChanged, + LyricsLoaded(LyricsLoadedEvent), +} diff --git a/src-tauri/crates/plugin-sdk/src/ffi.rs b/src-tauri/crates/plugin-sdk/src/ffi.rs new file mode 100644 index 0000000..d33e43e --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/ffi.rs @@ -0,0 +1,74 @@ +use std::ffi::c_void; + +pub type PluginHandle = *mut c_void; +pub type EventPayload = *const c_void; +pub type CommandRequest = *const c_void; + +#[repr(C)] +pub struct ByteBuffer { + pub ptr: *mut u8, + pub len: usize, +} + +#[repr(C)] +pub struct CommandResponse { + pub success: bool, + pub data: ByteBuffer, +} + +#[repr(C)] +pub struct PluginApi { + pub activate: unsafe extern "C" fn(PluginHandle) -> i32, + pub deactivate: unsafe extern "C" fn(PluginHandle) -> i32, + pub on_event: unsafe extern "C" fn(PluginHandle, EventPayload) -> i32, + pub execute_command: + unsafe extern "C" fn(PluginHandle, *const CommandRequest) -> CommandResponse, +} + +#[repr(C)] +pub struct PluginExports { + pub abi_version: u32, + pub create: unsafe extern "C" fn() -> PluginHandle, + pub destroy: unsafe extern "C" fn(PluginHandle), + pub free_buffer: unsafe extern "C" fn(ByteBuffer), + pub api: PluginApi, + /// 音频处理器指针。null 表示插件不导出音频处理能力。 + /// + /// 注意:为了 ABI 兼容,**不**在 PluginExports 中内嵌 AudioProcessorApi, + /// 而是用单独导出的全局符号 `rem_audio_processor` 暴露。 + /// 旧插件(abi_version=1 但无此字段)在 dynamic_factory 读取时: + /// - 用 dlsym 单独查找 `rem_audio_processor` 符号;找到则构造 AudioProcessorApi 指针 + /// - 找不到则置 None + /// 新插件在 PluginExports 中直接填充此字段为 `&AudioProcessorApi` 或 null。 + pub audio_processor: *const AudioProcessorApi, +} + +/// 音频处理器 FFI 协议:packaged 插件通过此协议接收 PCM 帧、处理后写回。 +/// +/// - `init`: 可选初始化回调(sample_rate, channels)。返回 0 表示成功,非 0 失败。 +/// - `process`: 实时处理回调。 +/// - `handle`: PluginHandle +/// - `input`: *const f32,交错 PCM 输入(frames * channels 个 f32) +/// - `output`: *mut f32,交错 PCM 输出(同长度) +/// - `frames`: 帧数(每通道样本数,通常 128 或 256) +/// - `channels`: 通道数(1=mono, 2=stereo) +/// 返回 0 表示成功,非 0 表示错误(宿主应跳过该插件)。 +/// - `reset`: 可选状态重置回调(如清理延迟线 buffer)。 +/// +/// 3 个字段都用 `Option`,因为旧插件可能不导出音频处理能力。 +/// `repr(C)` 的 `Option` 等价 nullable pointer,能被 libloading 正确读取。 +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AudioProcessorApi { + pub init: Option i32>, + pub process: Option< + unsafe extern "C" fn( + PluginHandle, + input: *const f32, + output: *mut f32, + frames: usize, + channels: usize, + ) -> i32, + >, + pub reset: Option i32>, +} diff --git a/src-tauri/crates/plugin-sdk/src/lib.rs b/src-tauri/crates/plugin-sdk/src/lib.rs new file mode 100644 index 0000000..efa2457 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/lib.rs @@ -0,0 +1,25 @@ +pub mod api; +pub mod contributes; +pub mod context; +pub mod errors; +pub mod events; +pub mod ffi; +pub mod manifest; +pub mod model; +pub mod permissions; +pub mod settings; +pub mod traits; + +pub use api::{LibraryApi, PlayerApi, PluginSettingsApi, QueueApi, SettingsApi}; +pub use contributes::{NativeViewContribution, ViewContribution}; +pub use context::{HostContext, PluginContext}; +pub use errors::PluginError; +pub use events::{LyricsLoadedEvent, PluginEvent, TrackSnapshot}; +pub use manifest::{ + ActivationEvent, PluginContribution, PluginManifest, PluginSource, ResolvedPluginManifest, + SettingDefinition, +}; +pub use model::PluginIdentity; +pub use permissions::PluginPermission; +pub use settings::{PluginSetting, PluginSettingMeta, PluginSettings, SettingValue}; +pub use traits::{CommandArgs, Plugin, PluginFactory}; diff --git a/src-tauri/crates/plugin-sdk/src/manifest.rs b/src-tauri/crates/plugin-sdk/src/manifest.rs new file mode 100644 index 0000000..f67b563 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/manifest.rs @@ -0,0 +1,99 @@ +use serde::{Deserialize, Serialize}; + +use crate::contributes::{ + CommandContribution, MenuContribution, NativeViewContribution, SidebarContribution, + ViewContribution, +}; +use crate::permissions::PluginPermission; +use crate::settings::SettingValue; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum ActivationEvent { + OnStartup, + OnTrackChanged, + OnPlaybackStateChanged, + OnQueueChanged, + OnSettingsChanged, + OnCommand(String), +} + +/// Where a plugin's manifest and code come from. +/// +/// `id` never carries a source prefix; `source` is the single, +/// independent field that classifies provenance. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "snake_case")] +pub enum PluginSource { + Builtin, + Packaged, + User, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct PluginContribution { + pub commands: Vec, + pub menus: Vec, + pub sidebars: Vec, + #[serde(default)] + pub views: Vec, + #[serde(default, rename = "nativeViews")] + pub native_views: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SettingDefinition { + pub key: String, + pub title: String, + pub default_value: SettingValue, +} + +/// Raw manifest as authored in `plugin.json`. +/// +/// `route` is an optional override of the slug segment under +/// `/plugins/view/`. When absent, the slug defaults to `id`. +/// The raw manifest is loader-internal only; consumers must use +/// [`ResolvedPluginManifest`], which has every field resolved. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginManifest { + pub id: String, + pub source: PluginSource, + #[serde(default)] + pub route: Option, + pub name: String, + pub display_name: String, + pub version: String, + pub author: String, + pub description: String, + pub entry: String, + pub min_app_version: String, + pub permissions: Vec, + pub activation_events: Vec, + pub contributes: PluginContribution, + #[serde(default)] + pub settings: Vec, +} + +/// Fully resolved, normalized manifest — the only shape exposed to +/// the frontend. No `Option`s; `route` is already normalized to an +/// absolute `/plugins/view/` path. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResolvedPluginManifest { + pub id: String, + pub source: PluginSource, + pub route: String, + pub name: String, + pub display_name: String, + pub version: String, + pub author: String, + pub description: String, + pub entry: String, + pub min_app_version: String, + pub permissions: Vec, + pub activation_events: Vec, + pub contributes: PluginContribution, + pub settings: Vec, +} diff --git a/src-tauri/crates/plugin-sdk/src/model.rs b/src-tauri/crates/plugin-sdk/src/model.rs new file mode 100644 index 0000000..58509ee --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/model.rs @@ -0,0 +1,47 @@ +use serde::{Deserialize, Serialize}; + +use crate::errors::PluginError; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)] +pub struct PluginIdentity { + pub namespace: Option, + pub name: String, +} + +impl PluginIdentity { + pub fn bare(name: &str) -> Self { + Self { + namespace: None, + name: name.to_string(), + } + } +} + +impl std::fmt::Display for PluginIdentity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(ref ns) = self.namespace { + write!(f, "{}/{}", ns, self.name) + } else { + write!(f, "{}", self.name) + } + } +} + +impl std::str::FromStr for PluginIdentity { + type Err = crate::errors::PluginError; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split('/').collect(); + match parts.len() { + 2 if !parts[0].is_empty() && !parts[1].is_empty() => Ok(Self { + namespace: Some(parts[0].to_string()), + name: parts[1].to_string(), + }), + 1 if !parts[0].is_empty() => Ok(Self { + namespace: None, + name: parts[0].to_string(), + }), + _ => Err(PluginError::Plugin(format!("Invalid identity: {}", s))), + } + } +} diff --git a/src-tauri/crates/plugin-sdk/src/permissions.rs b/src-tauri/crates/plugin-sdk/src/permissions.rs new file mode 100644 index 0000000..a1131db --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/permissions.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "camelCase")] +pub enum PluginPermission { + PlayerRead, + PlayerControl, + QueueRead, + QueueWrite, + LibraryRead, + LibraryWrite, + SettingsRead, + SettingsWrite, + PluginUI, + /// 音频处理(EQ 等)读写权限,序列化为 "audio" + Audio, +} diff --git a/src-tauri/crates/plugin-sdk/src/settings.rs b/src-tauri/crates/plugin-sdk/src/settings.rs new file mode 100644 index 0000000..d809003 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/settings.rs @@ -0,0 +1,37 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", content = "value")] +pub enum SettingValue { + Bool(bool), + Integer(i64), + Float(f64), + Text(String), + List(Vec), + Json(serde_json::Value), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PluginSetting { + pub key: String, + pub title: String, + pub value: SettingValue, + pub default_value: SettingValue, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginSettingMeta { + pub key: String, + pub title: String, + pub value: SettingValue, + pub default_value: SettingValue, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PluginSettings { + pub plugin_id: String, + pub settings: HashMap, +} diff --git a/src-tauri/crates/plugin-sdk/src/traits.rs b/src-tauri/crates/plugin-sdk/src/traits.rs new file mode 100644 index 0000000..60c5d39 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/traits.rs @@ -0,0 +1,37 @@ +use std::sync::Arc; + +use crate::context::PluginContext; +use crate::errors::PluginError; +use crate::events::PluginEvent; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum CommandArgs { + None, + TrackId(i64), + TrackIds(Vec), + RawPayload(String), + LyricsSearch { title: String, artist: String }, +} + +/// Runtime behaviour of a plugin. Manifest is NOT part of this trait — +/// it lives in the `ManifestRegistry`, sourced from `plugin.json`. +/// This keeps `plugin.json` as the single source of truth and removes +/// the dual-maintenance problem of `build_manifest()`. +pub trait Plugin: Send + Sync { + fn activate(&self, ctx: &PluginContext) -> Result<(), PluginError>; + fn deactivate(&self) -> Result<(), PluginError>; + fn on_event(&self, event: &PluginEvent, ctx: &PluginContext) -> Result<(), PluginError>; + fn execute_command( + &self, + command_id: &str, + args: &CommandArgs, + ctx: &PluginContext, + ) -> Result<(), PluginError>; +} + +/// Constructs plugin instances. Knows nothing about manifests. +/// The association `id -> factory` lives in `FactoryRegistry`; +/// the association `id -> manifest` lives in `ManifestRegistry`. +pub trait PluginFactory: Send + Sync { + fn create(&self) -> Arc; +} diff --git a/src-tauri/migrations/0001_initial.sql b/src-tauri/migrations/0001_initial.sql new file mode 100644 index 0000000..923e76a --- /dev/null +++ b/src-tauri/migrations/0001_initial.sql @@ -0,0 +1,55 @@ +CREATE TABLE tracks ( + id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + artist TEXT, + album TEXT, + duration INTEGER NOT NULL, + path TEXT NOT NULL UNIQUE, + cover TEXT, + file_size INTEGER, + play_count INTEGER DEFAULT 0, + last_played_at TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE TABLE playlists ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + created_at TEXT NOT NULL +); + +CREATE TABLE playlist_tracks ( + id INTEGER PRIMARY KEY, + playlist_id INTEGER NOT NULL, + track_id INTEGER NOT NULL, + position INTEGER NOT NULL, + FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE, + FOREIGN KEY (track_id) REFERENCES tracks(id) ON DELETE CASCADE, + UNIQUE (playlist_id, track_id), + UNIQUE (playlist_id, position) +); + +CREATE TABLE recent_played ( + track_id INTEGER PRIMARY KEY, + played_at TEXT NOT NULL, + FOREIGN KEY (track_id) REFERENCES tracks(id) ON DELETE CASCADE +); + +CREATE TABLE settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + theme TEXT NOT NULL, + volume INTEGER NOT NULL, + scan_on_startup INTEGER NOT NULL, + reduce_motion INTEGER NOT NULL, + library_dirs TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE INDEX idx_tracks_title ON tracks(title); +CREATE INDEX idx_tracks_artist ON tracks(artist); +CREATE INDEX idx_tracks_album ON tracks(album); +CREATE INDEX idx_playlist_tracks_playlist_id ON playlist_tracks(playlist_id); +CREATE INDEX idx_playlist_tracks_track_id ON playlist_tracks(track_id); +CREATE INDEX idx_recent_played_played_at ON recent_played(played_at); diff --git a/src-tauri/migrations/0002_extend_settings.sql b/src-tauri/migrations/0002_extend_settings.sql new file mode 100644 index 0000000..6cc93a1 --- /dev/null +++ b/src-tauri/migrations/0002_extend_settings.sql @@ -0,0 +1,5 @@ +ALTER TABLE settings ADD COLUMN use_album_artist_grouping INTEGER NOT NULL DEFAULT 0; +ALTER TABLE settings ADD COLUMN plugin_dirs TEXT NOT NULL DEFAULT '[]'; +ALTER TABLE settings ADD COLUMN plugin_dev_mode INTEGER NOT NULL DEFAULT 0; +ALTER TABLE settings ADD COLUMN plugin_scan_on_startup INTEGER NOT NULL DEFAULT 1; +ALTER TABLE settings ADD COLUMN plugin_log_level TEXT NOT NULL DEFAULT 'warn'; diff --git a/src-tauri/plugins/demo-view/plugin.json b/src-tauri/plugins/demo-view/plugin.json new file mode 100644 index 0000000..02b8b5b --- /dev/null +++ b/src-tauri/plugins/demo-view/plugin.json @@ -0,0 +1,28 @@ +{ + "id": "demo-view", + "name": "demo-view", + "displayName": "Demo View Plugin", + "version": "0.1.0", + "author": "RustEchoMusic", + "description": "临时示例插件,用于验证 plugin:// scheme 与 view 字段结构(不会被注册为内置插件)。", + "entry": "view/index.html", + "minAppVersion": "0.1.0", + "permissions": [ + "pluginUI" + ], + "activationEvents": [], + "contributes": { + "commands": [], + "menus": [], + "sidebars": [], + "views": [ + { + "id": "main", + "title": "Demo View", + "entry": "view/index.html", + "icon": "dashboard" + } + ] + }, + "settings": [] +} diff --git a/src-tauri/plugins/demo-view/view/index.html b/src-tauri/plugins/demo-view/view/index.html new file mode 100644 index 0000000..edd4353 --- /dev/null +++ b/src-tauri/plugins/demo-view/view/index.html @@ -0,0 +1,102 @@ + + + + + + demo-view + + + +

demo-view

+
等待宿主握手...
+
(尚未收到 ready)
+
pluginId:
+
capabilities:
+ + + + + + diff --git a/src-tauri/src/audio/engine.rs b/src-tauri/src/audio/engine.rs new file mode 100644 index 0000000..912bafd --- /dev/null +++ b/src-tauri/src/audio/engine.rs @@ -0,0 +1,39 @@ +use crate::errors::AppError; + +pub trait AudioEngine: Send { + fn play(&mut self) -> Result<(), AppError>; + fn pause(&mut self); + fn seek(&mut self, time: f64); + fn set_volume(&self, volume: f32); + fn set_pan(&self, pan: f32); + fn current_time(&self) -> f64; + fn paused(&self) -> bool; + + // ===== 均衡器(EQ)扩展方法 ===== + // 以下方法均带默认 no-op 实现,保证现有实现 / mock 不被破坏。 + + /// 设置指定频段(0..10)的增益(dB)。 + fn set_eq_band_gain(&mut self, _band_index: usize, _gain_db: f64) -> Result<(), AppError> { + Ok(()) + } + + /// 一次性应用 10 段 EQ 预设(dB)。 + fn apply_eq_preset(&mut self, _gains: &[f64; 10]) -> Result<(), AppError> { + Ok(()) + } + + /// 启用 / 旁路 EQ。 + fn set_eq_enabled(&mut self, _enabled: bool) -> Result<(), AppError> { + Ok(()) + } + + /// 读取当前 10 段增益(dB)。 + fn get_eq_bands(&self) -> Result<[f64; 10], AppError> { + Ok([0.0; 10]) + } + + /// 查询 EQ 是否启用。 + fn is_eq_enabled(&self) -> Result { + Ok(false) + } +} diff --git a/src-tauri/src/audio/mod.rs b/src-tauri/src/audio/mod.rs new file mode 100644 index 0000000..d833d70 --- /dev/null +++ b/src-tauri/src/audio/mod.rs @@ -0,0 +1,12 @@ +pub mod engine; +pub mod state; +pub mod web_audio; + +pub use engine::AudioEngine; +pub use state::{init_audio_state, lock_audio_state, AudioState}; +pub use web_audio::WebAudioEngine; +use web_audio_api::context::AudioContext; + +pub fn get_audio_context() -> AudioContext { + AudioContext::default() +} diff --git a/src-tauri/src/audio/state.rs b/src-tauri/src/audio/state.rs new file mode 100644 index 0000000..81adab3 --- /dev/null +++ b/src-tauri/src/audio/state.rs @@ -0,0 +1,69 @@ +use crate::audio::engine::AudioEngine; +use crate::errors::AppError; +use crate::models::PlaybackQueue; +use std::sync::{Mutex, MutexGuard, OnceLock}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum PlayMode { + ListLoop, + SingleLoop, + Shuffle, +} + +pub struct AudioState { + pub engine: Option>, + pub volume: f32, + pub pan: f32, + pub playing: bool, + pub current_track_id: Option, + pub playback_queue: PlaybackQueue, + /// 10 段 EQ 增益(dB),与引擎同步 + pub eq_bands: [f64; 10], + /// EQ 是否启用 + pub eq_enabled: bool, +} + +impl AudioState { + pub fn with_engine(&mut self, f: F) -> Result + where + F: FnOnce(&mut Box) -> R, + { + if let Some(ref mut engine) = self.engine { + Ok(f(engine)) + } else { + Err(AppError::from("Audio engine is missing")) + } + } + + pub fn play_mode(&self) -> PlayMode { + self.playback_queue.play_mode + } + pub fn set_play_mode(&mut self, mode: PlayMode) { + self.playback_queue.play_mode = mode; + } +} + +static AUDIO_STATE: OnceLock> = OnceLock::new(); + +pub fn init_audio_state(queue: PlaybackQueue) { + AUDIO_STATE.get_or_init(|| { + Mutex::new(AudioState { + engine: None, + volume: 1.0, + pan: 0.0, + playing: false, + current_track_id: None, + playback_queue: queue, + eq_bands: [0.0; 10], + eq_enabled: false, + }) + }); +} + +pub fn lock_audio_state() -> Result, AppError> { + AUDIO_STATE + .get() + .ok_or_else(|| AppError::from("Audio state not initialized"))? + .lock() + .map_err(|_| AppError::from("Failed to lock audio state")) +} diff --git a/src-tauri/src/audio/web_audio.rs b/src-tauri/src/audio/web_audio.rs new file mode 100644 index 0000000..dfecde2 --- /dev/null +++ b/src-tauri/src/audio/web_audio.rs @@ -0,0 +1,320 @@ +use crate::audio::engine::AudioEngine; +use crate::audio::get_audio_context; +use crate::errors::AppError; +use crate::services::plugin::audio_processor_registry::AudioProcessorRegistry; +use std::collections::HashMap; +use std::path::Path; +use std::sync::Arc; +use web_audio_api::{ + context::BaseAudioContext, + node::{AudioNode, AudioNodeOptions, BiquadFilterNode, BiquadFilterType, GainNode, MediaElementAudioSourceNode, StereoPannerNode}, + worklet::{AudioParamValues, AudioWorkletGlobalScope, AudioWorkletNode, AudioWorkletNodeOptions, AudioWorkletProcessor}, + MediaElement, +}; + +/// 10 段 EQ 标准中心频率(Hz) +const EQ_FREQUENCIES: [f32; 10] = [ + 31.0, 62.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16000.0, +]; +/// EQ 标准 Q 值(约 √2) +const EQ_Q: f32 = 1.41; +const EQ_BAND_COUNT: usize = 10; + +/// 用户态 AudioWorklet 处理器:把渲染量子的 PCM 交错后依次喂给 +/// `AudioProcessorRegistry` 中所有已激活的 packaged 插件,再把结果解交错 +/// 写回输出。 +/// +/// 节点常驻于音频图(方案 A):无插件注册时透传 input → output,仅一次 +/// `copy_from_slice`,开销可忽略。这避免了在有/无插件之间频繁重建音频图。 +/// +/// 缓冲区作为字段复用(Task 9.3):`AudioWorkletProcessor` 实例的 `process` +/// 在单线程音频回调中调用,`Vec` 字段可安全持有。 +struct ExternalProcessorNode { + registry: Arc, + in_buf: Vec, + out_buf: Vec, +} + +impl AudioWorkletProcessor for ExternalProcessorNode { + type ProcessorOptions = Arc; + + fn constructor(registry: Self::ProcessorOptions) -> Self + where + Self: Sized, + { + Self { + registry, + in_buf: Vec::new(), + out_buf: Vec::new(), + } + } + + fn process<'a, 'b>( + &mut self, + inputs: &'b [&'a [&'a [f32]]], + outputs: &'b mut [&'a mut [&'a mut [f32]]], + _params: AudioParamValues<'b>, + _scope: &'b AudioWorkletGlobalScope, + ) -> bool { + // 单输入单输出节点:inputs[0] / outputs[0] 为各通道的样本切片。 + let input: &[&[f32]] = inputs.get(0).copied().unwrap_or(&[]); + let channels = input.len(); + let frames = input.first().map(|c| c.len()).unwrap_or(0); + + let processors = self.registry.iter_active(); + if processors.is_empty() { + // 无插件:透传(input → output)。 + if let Some(output) = outputs.get_mut(0) { + for (i, out_chan) in output.iter_mut().enumerate() { + if let Some(in_chan) = input.get(i) { + let len = in_chan.len().min(out_chan.len()); + if len > 0 { + out_chan[..len].copy_from_slice(&in_chan[..len]); + } + } + } + } + return true; + } + + if channels == 0 || frames == 0 { + // 无输入数据;输出保持静默。 + return true; + } + + // 构造交错缓冲区:[L0, R0, L1, R1, ...](frames * channels 个 f32)。 + let total = frames * channels; + self.in_buf.clear(); + self.in_buf.reserve(total); + for f in 0..frames { + for c in 0..channels { + let v = input + .get(c) + .and_then(|ch| ch.get(f)) + .copied() + .unwrap_or(0.0); + self.in_buf.push(v); + } + } + // out_buf 初值 = in_buf(透传给首个插件作为输出缓冲)。 + self.out_buf.clear(); + self.out_buf.extend_from_slice(&self.in_buf); + + // 依次调用各 processor 的 process FFI。成功后用 out_buf 作为下一轮输入。 + for (_plugin_id, api, handle) in processors { + let Some(process_fn) = api.process else { + continue; + }; + let rc = unsafe { + process_fn( + handle, + self.in_buf.as_ptr(), + self.out_buf.as_mut_ptr(), + frames, + channels, + ) + }; + if rc == 0 { + // 处理成功:用 out_buf 作为下一轮插件输入。 + self.in_buf[..total].copy_from_slice(&self.out_buf); + } + // 失败:跳过该插件,保留 out_buf / in_buf 不变。 + } + + // 解交错写回 outputs。 + if let Some(output) = outputs.get_mut(0) { + for c in 0..channels { + if let Some(out_chan) = output.get_mut(c) { + let n = out_chan.len().min(frames); + for f in 0..n { + out_chan[f] = self.out_buf[f * channels + c]; + } + } + } + } + + // 返回 true:节点常驻(即使输入断开,仍保留在图中以便随时接收插件输出)。 + true + } +} + +pub struct WebAudioEngine { + media: MediaElement, + src: MediaElementAudioSourceNode, + gain: GainNode, + panner: StereoPannerNode, + /// 10 段 BiquadFilter(Peaking)节点链,串联在 source 与 gain 之间 + eq_filters: Vec, + /// 外部处理器节点(packaged 插件音频处理入口),串联在 eq_chain 与 gain 之间。 + external_processor_node: AudioWorkletNode, + /// EQ 是否启用(false = 旁路,所有 biquad gain 设为 0dB) + eq_enabled: bool, + /// 各频段目标增益(dB),启用时同步到对应 biquad + eq_bands: [f64; 10], +} + +impl WebAudioEngine { + pub fn new( + file_path: &Path, + volume: f32, + pan: f32, + audio_processor_registry: Arc, + ) -> Result { + let context = get_audio_context(); + let _ = context.resume(); + + // 把当前音频输出格式同步给 registry,供插件 init 回调使用。 + audio_processor_registry.set_audio_format(context.sample_rate() as u32, 2); + + let mut media = web_audio_api::MediaElement::new(file_path) + .map_err(|e| format!("Failed to create media element: {}", e))?; + + let src = context.create_media_element_source(&mut media); + let gain = context.create_gain(); + let panner = context.create_stereo_panner(); + + // 构建 10 段 EQ BiquadFilter 链(始终串联,bypass 时 gain 全为 0dB,不改变频响) + let mut eq_filters: Vec = Vec::with_capacity(EQ_BAND_COUNT); + for &freq in EQ_FREQUENCIES.iter() { + let mut filter = context.create_biquad_filter(); + filter.set_type(BiquadFilterType::Peaking); + filter.frequency().set_value(freq); + filter.q().set_value(EQ_Q); + // 初始处于旁路状态:gain = 0 dB + filter.gain().set_value(0.0); + eq_filters.push(filter); + } + + // 外部处理器节点(AudioWorklet)。1 入 1 出,output_channel_count 为空时 + // 继承输入通道数(stereo)。 + let worklet_options = AudioWorkletNodeOptions { + number_of_inputs: 1, + number_of_outputs: 1, + output_channel_count: Vec::new(), + parameter_data: HashMap::new(), + processor_options: audio_processor_registry, + audio_node_options: AudioNodeOptions::default(), + }; + let external_processor_node = + AudioWorkletNode::new::(&context, worklet_options); + + // 节点链:src → biquad[0] → ... → biquad[9] → external_processor_node → gain → panner → destination + src.connect(&eq_filters[0]); + for i in 0..(EQ_BAND_COUNT - 1) { + eq_filters[i].connect(&eq_filters[i + 1]); + } + eq_filters[EQ_BAND_COUNT - 1].connect(&external_processor_node); + external_processor_node.connect(&gain); + gain.connect(&panner); + panner.connect(&context.destination()); + + media.set_loop(false); + media.set_current_time(0.0); + gain.gain().set_value(volume); + panner.pan().set_value(pan); + + Ok(WebAudioEngine { + media, + src, + gain, + panner, + eq_filters, + external_processor_node, + eq_enabled: false, + eq_bands: [0.0; 10], + }) + } +} + +impl AudioEngine for WebAudioEngine { + fn play(&mut self) -> Result<(), AppError> { + self.media.play(); + Ok(()) + } + + fn pause(&mut self) { + self.media.pause(); + } + + fn seek(&mut self, time: f64) { + self.media.set_current_time(time); + } + + fn set_volume(&self, volume: f32) { + self.gain.gain().set_value(volume); + } + + fn set_pan(&self, pan: f32) { + self.panner.pan().set_value(pan); + } + + fn current_time(&self) -> f64 { + self.media.current_time() + } + + fn paused(&self) -> bool { + self.media.paused() + } + + fn set_eq_band_gain(&mut self, band_index: usize, gain_db: f64) -> Result<(), AppError> { + if band_index >= EQ_BAND_COUNT { + return Err(AppError::Service(format!( + "EQ band index {} out of range (0..{})", + band_index, EQ_BAND_COUNT + ))); + } + self.eq_bands[band_index] = gain_db; + // 仅在 EQ 启用时同步到节点,旁路时保持 0dB + if self.eq_enabled { + self.eq_filters[band_index].gain().set_value(gain_db as f32); + } + Ok(()) + } + + fn apply_eq_preset(&mut self, gains: &[f64; 10]) -> Result<(), AppError> { + self.eq_bands = *gains; + if self.eq_enabled { + for (i, &g) in gains.iter().enumerate() { + self.eq_filters[i].gain().set_value(g as f32); + } + } + Ok(()) + } + + fn set_eq_enabled(&mut self, enabled: bool) -> Result<(), AppError> { + self.eq_enabled = enabled; + if enabled { + // 启用时恢复各频段目标增益 + for (i, &g) in self.eq_bands.iter().enumerate() { + self.eq_filters[i].gain().set_value(g as f32); + } + } else { + // 旁路时所有 biquad gain 设为 0dB(不改变频响) + for filter in &self.eq_filters { + filter.gain().set_value(0.0); + } + } + Ok(()) + } + + fn get_eq_bands(&self) -> Result<[f64; 10], AppError> { + Ok(self.eq_bands) + } + + fn is_eq_enabled(&self) -> Result { + Ok(self.eq_enabled) + } +} + +impl Drop for WebAudioEngine { + fn drop(&mut self) { + let _ = self.panner.disconnect(); + let _ = self.gain.disconnect(); + let _ = self.external_processor_node.disconnect(); + for filter in &self.eq_filters { + let _ = filter.disconnect(); + } + let _ = self.src.disconnect(); + self.media.pause(); + } +} diff --git a/src-tauri/src/commands/equalizer.rs b/src-tauri/src/commands/equalizer.rs new file mode 100644 index 0000000..7c51495 --- /dev/null +++ b/src-tauri/src/commands/equalizer.rs @@ -0,0 +1,119 @@ +use std::sync::Arc; + +use tauri::{command, State}; + +use plugin_sdk::settings::{PluginSettingMeta, SettingValue}; + +use crate::errors::AppError; +use crate::services::plugin::manager::PluginManager; +use crate::state::playback_state::with_audio_state; + +/// EQ 插件 ID,与 `plugins/eq/plugin.json` 中的 `id` 一致 +const EQ_PLUGIN_ID: &str = "eq"; + +/// 对外暴露的 EQ 状态快照,前端通过 `get_eq_state` 读取 +#[derive(serde::Serialize)] +pub struct EqState { + pub bands: [f64; 10], + pub enabled: bool, + pub preset_name: String, +} + +/// 读取持久化的预设名称;缺失或非 Text 时回退到 "Flat" +fn read_preset_name(manager: &PluginManager) -> String { + manager + .get_plugin_settings(EQ_PLUGIN_ID) + .into_iter() + .find(|m: &PluginSettingMeta| m.key == "preset_name") + .and_then(|m| match m.value { + SettingValue::Text(s) => Some(s), + _ => None, + }) + .unwrap_or_else(|| "Flat".to_string()) +} + +/// 把当前 audio state 的 eq_bands 序列化为 Json 写回 "preset" key +fn persist_preset(manager: &PluginManager) -> Result<(), AppError> { + let bands = with_audio_state(|s| s.eq_bands)?; + manager.update_plugin_setting( + EQ_PLUGIN_ID, + "preset", + SettingValue::Json(serde_json::to_value(&bands).unwrap_or_default()), + )?; + Ok(()) +} + +#[command] +pub async fn get_eq_state( + manager: State<'_, Arc>, +) -> Result { + let (bands, enabled) = with_audio_state(|s| (s.eq_bands, s.eq_enabled))?; + let preset_name = read_preset_name(&manager); + Ok(EqState { + bands, + enabled, + preset_name, + }) +} + +#[command] +pub async fn set_eq_band( + band: usize, + gain: f64, + manager: State<'_, Arc>, +) -> Result<(), AppError> { + if band >= 10 { + return Err(AppError::from("EQ band index out of range")); + } + with_audio_state(|state| { + state.eq_bands[band] = gain; + if let Some(ref mut engine) = state.engine { + engine.set_eq_band_gain(band, gain)?; + } + Ok::<(), AppError>(()) + })??; + persist_preset(&manager)?; + Ok(()) +} + +#[command] +pub async fn apply_eq_preset( + preset_name: String, + bands: [f64; 10], + manager: State<'_, Arc>, +) -> Result<(), AppError> { + with_audio_state(|state| { + state.eq_bands = bands; + if let Some(ref mut engine) = state.engine { + engine.apply_eq_preset(&bands)?; + } + Ok::<(), AppError>(()) + })??; + manager.update_plugin_setting( + EQ_PLUGIN_ID, + "preset", + SettingValue::Json(serde_json::to_value(&bands).unwrap_or_default()), + )?; + manager.update_plugin_setting( + EQ_PLUGIN_ID, + "preset_name", + SettingValue::Text(preset_name), + )?; + Ok(()) +} + +#[command] +pub async fn set_eq_enabled( + enabled: bool, + manager: State<'_, Arc>, +) -> Result<(), AppError> { + with_audio_state(|state| { + state.eq_enabled = enabled; + if let Some(ref mut engine) = state.engine { + engine.set_eq_enabled(enabled)?; + } + Ok::<(), AppError>(()) + })??; + manager.update_plugin_setting(EQ_PLUGIN_ID, "enabled", SettingValue::Bool(enabled))?; + Ok(()) +} diff --git a/src-tauri/src/commands/library.rs b/src-tauri/src/commands/library.rs new file mode 100644 index 0000000..9c11524 --- /dev/null +++ b/src-tauri/src/commands/library.rs @@ -0,0 +1,264 @@ +use std::collections::HashSet; +use std::path::Path; +use std::process::Command; + +use base64::{engine::general_purpose, Engine as _}; +use lofty::prelude::*; +use lofty::probe::Probe; +use tauri::{command, State}; +use walkdir::WalkDir; + +use crate::commands::playback::remove_track_from_queue; +use crate::errors::AppError; +use crate::metadata::parse_single_track; +use crate::models::{NewTrack, Track}; +use crate::state::AppState; + +const SUPPORTED_EXT: [&str; 5] = ["mp3", "flac", "m4a", "wav", "ogg"]; + +fn is_supported_audio_file(path: &Path) -> bool { + path.extension() + .and_then(|ext| ext.to_str()) + .map(|ext| SUPPORTED_EXT.contains(&ext.to_lowercase().as_str())) + .unwrap_or(false) +} + +fn scan_single_directory(dir: &str) -> Result, AppError> { + let root_path = Path::new(dir); + + if !root_path.exists() || !root_path.is_dir() { + return Err(format!("无效目录: {}", dir).into()); + } + + let mut tracks = Vec::new(); + + for entry in WalkDir::new(root_path) + .follow_links(false) + .into_iter() + .filter_map(Result::ok) + { + let path = entry.path(); + + if !path.is_file() || !is_supported_audio_file(path) { + continue; + } + + if let Some(mut track) = parse_single_track(path) { + track.cover = None; + tracks.push(track); + } + } + + Ok(tracks) +} + +fn dedupe_new_tracks(tracks: &mut Vec) { + let mut seen = HashSet::new(); + tracks.retain(|track| seen.insert(track.path.clone())); +} + +pub fn execute_scan(dirs: Vec) -> Result, AppError> { + let mut all_tracks = Vec::new(); + + for dir in dirs { + if let Ok(mut tracks) = scan_single_directory(&dir) { + all_tracks.append(&mut tracks); + } + } + + dedupe_new_tracks(&mut all_tracks); + Ok(all_tracks) +} + +async fn upsert_scanned_tracks( + state: &AppState, + tracks: Vec, +) -> Result, AppError> { + state + .tracks + .upsert_tracks(tracks) + .await +} + +#[command] +pub async fn get_track_cover(track_id: i64, state: State<'_, AppState>) -> Result, AppError> { + let track = state.tracks.get_track(track_id).await?; + let track = match track { + Some(t) => t, + None => return Err("曲目不存在".into()), + }; + + let file_path = Path::new(&track.path); + + if !file_path.exists() { + return Err("音频文件不存在".into()); + } + + let tagged_file = Probe::open(file_path) + .map_err(|e| e.to_string())? + .read() + .map_err(|e| e.to_string())?; + + let tag = tagged_file + .primary_tag() + .or_else(|| tagged_file.first_tag()); + + let cover = tag.and_then(|t| t.pictures().first()).map(|pic| { + let b64_encoded = general_purpose::STANDARD.encode(pic.data()); + let mime_type = pic.mime_type().map(|m| m.as_str()).unwrap_or("image/jpeg"); + + format!("data:{};base64,{}", mime_type, b64_encoded) + }); + + Ok(cover) +} + +fn validate_audio_path(path: &str) -> Result<&Path, AppError> { + let file_path = Path::new(path); + + if !file_path.exists() { + return Err("音频文件不存在".into()); + } + + if !file_path.is_file() || !is_supported_audio_file(file_path) { + return Err(format!("拒绝操作非音频文件: {}", path).into()); + } + + Ok(file_path) +} + +#[command] +pub async fn show_in_folder(track_id: i64, state: State<'_, AppState>) -> Result<(), AppError> { + let track = state.tracks.get_track(track_id).await?; + let track = track.ok_or_else(|| AppError::from("曲目不存在"))?; + let file_path = Path::new(&track.path); + + if !file_path.exists() { + return Err("文件不存在".into()); + } + + #[cfg(target_os = "linux")] + let parent = file_path + .parent() + .ok_or_else(|| AppError::from("无法定位文件所在目录"))?; + + #[cfg(target_os = "windows")] + let status = Command::new("explorer.exe") + .arg(format!("/select,{}", file_path.display())) + .status(); + + #[cfg(target_os = "macos")] + let status = Command::new("open").arg("-R").arg(file_path).status(); + + #[cfg(target_os = "linux")] + let status = Command::new("xdg-open").arg(parent).status(); + + let status = status?; + + if status.success() { + Ok(()) + } else { + Err("打开文件所在目录失败".into()) + } +} + +#[command] +pub async fn delete_track_file( + app_handle: tauri::AppHandle, + track_id: i64, + state: State<'_, AppState>, +) -> Result<(), AppError> { + let track = state.tracks.get_track(track_id).await?; + let track = track.ok_or_else(|| AppError::from("曲目不存在"))?; + + let file_path = validate_audio_path(&track.path)?; + trash::delete(file_path).map_err(|e| format!("移至回收站失败 {}: {}", track.path, e))?; + + remove_track_from_queue(app_handle, track_id).await?; + + Ok(()) +} + +#[command] +pub async fn trash_track_files(track_ids: Vec, state: State<'_, AppState>) -> Result<(), AppError> { + for track_id in track_ids { + let track = state.tracks.get_track(track_id).await?; + let track = match track { + Some(t) => t, + None => continue, + }; + + let file_path = Path::new(&track.path); + if !file_path.exists() { + continue; + } + + if !file_path.is_file() || !is_supported_audio_file(file_path) { + return Err(format!("拒绝移至回收站非音频文件: {}", track.path).into()); + } + + trash::delete(file_path).map_err(|e| format!("移至回收站失败 {}: {}", track.path, e))?; + } + + Ok(()) +} + +#[command] +pub async fn delete_track_files(track_ids: Vec, state: State<'_, AppState>) -> Result<(), AppError> { + for track_id in track_ids { + let track = state.tracks.get_track(track_id).await?; + let track = match track { + Some(t) => t, + None => continue, + }; + + let file_path = Path::new(&track.path); + if !file_path.exists() { + continue; + } + + if !file_path.is_file() || !is_supported_audio_file(file_path) { + return Err(format!("拒绝删除非音频文件: {}", track.path).into()); + } + + tokio::fs::remove_file(file_path) + .await + .map_err(|e| format!("删除文件失败 {}: {}", track.path, e))?; + } + + Ok(()) +} + +#[command] +pub async fn scan_track_directories( + state: State<'_, AppState>, + dirs: Vec, +) -> Result, AppError> { + let scanned = execute_scan(dirs)?; + let tracks = upsert_scanned_tracks(&state, scanned).await?; + Ok(tracks) +} + +#[command] +pub async fn load_track_library(state: State<'_, AppState>) -> Result, AppError> { + let tracks = state.tracks.list_tracks().await?; + + if tracks.is_empty() { + return rebuild_and_get_library(&state).await; + } + + Ok(tracks) +} + +async fn rebuild_and_get_library(state: &AppState) -> Result, AppError> { + let settings = state + .settings + .get_settings() + .await + .map_err(|error| error.to_string())?; + let scanned = tauri::async_runtime::spawn_blocking(move || execute_scan(settings.library_dirs)) + .await + .map_err(|e| e.to_string())??; + let tracks = upsert_scanned_tracks(state, scanned).await?; + Ok(tracks) +} diff --git a/src-tauri/src/commands/menu.rs b/src-tauri/src/commands/menu.rs new file mode 100644 index 0000000..879f3ee --- /dev/null +++ b/src-tauri/src/commands/menu.rs @@ -0,0 +1,13 @@ +use tauri::{command, menu::MenuBuilder, Manager, Window}; + +#[command] +pub fn show_context_menu(window: Window) { + let handle = window.app_handle(); + let menu = MenuBuilder::new(handle) + .text("quit", "退出程序") + .separator() + .text("play", "播放") + .build() + .unwrap(); + let _ = window.popup_menu(&menu); +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs new file mode 100644 index 0000000..903d4b1 --- /dev/null +++ b/src-tauri/src/commands/mod.rs @@ -0,0 +1,10 @@ +pub mod equalizer; +pub mod library; +pub mod menu; +pub mod playback; +pub mod playback_queue; +pub mod playlists; +pub mod plugin_extensions; +pub mod recent; +pub mod settings; +pub mod tracks; diff --git a/src-tauri/src/commands/playback.rs b/src-tauri/src/commands/playback.rs new file mode 100644 index 0000000..cb7d886 --- /dev/null +++ b/src-tauri/src/commands/playback.rs @@ -0,0 +1,139 @@ +use crate::audio::state::PlayMode; +use crate::errors::AppError; +use crate::events::{AppEvent, EventBus}; +use crate::models::playback::PlaybackStatusSnapshot; +use crate::models::Track; +use crate::services::playback_service::PlaybackService; +use crate::state::playback_state::{ + current_playback_snapshot, current_time_from_state, lock_audio_state, sanitize_track, + with_audio_state, +}; +use tauri::{command, AppHandle}; + +pub use crate::services::media_control_service::handle_media_control_event; + +#[command] +pub async fn sync_playback_queue( + app_handle: AppHandle, + playlist: Vec, + current_index: Option, + play_mode: PlayMode, + history: Vec, +) -> Result<(), AppError> { + { + let mut state = lock_audio_state()?; + + state.playback_queue.sync( + playlist.into_iter().map(sanitize_track).collect(), + current_index, + play_mode, + history, + ); + } + + PlaybackService::emit_queue_changed(&app_handle)?; + + Ok(()) +} + +#[command] +pub async fn remove_track_from_queue(app_handle: AppHandle, track_id: i64) -> Result<(), AppError> { + PlaybackService::new(app_handle) + .remove_track_from_queue(track_id) + .await +} + +#[command] +pub async fn insert_track_as_next(app_handle: AppHandle, track: Track) -> Result<(), AppError> { + with_audio_state(|state| { + state.playback_queue.insert_next(track); + })?; + PlaybackService::emit_queue_changed(&app_handle)?; + Ok(()) +} + +#[command] +pub async fn play_queue_track(app_handle: AppHandle, index: usize) -> Result<(), AppError> { + PlaybackService::new(app_handle).play_queue_index(index)?; + Ok(()) +} + +#[command] +pub async fn play_next_track(app_handle: AppHandle) -> Result<(), AppError> { + PlaybackService::new(app_handle).next().await?; + Ok(()) +} + +#[command] +pub async fn play_previous_track(app_handle: AppHandle) -> Result<(), AppError> { + PlaybackService::new(app_handle).previous().await?; + Ok(()) +} + +#[command] +pub async fn stop_track(app_handle: AppHandle) -> Result<(), AppError> { + PlaybackService::new(app_handle).stop().await?; + Ok(()) +} + +#[command] +pub async fn play_track( + app_handle: AppHandle, + track_id: i64, + state: tauri::State<'_, crate::state::AppState>, +) -> Result { + let track = state.tracks.get_track(track_id).await?; + let track = track.ok_or_else(|| AppError::from("曲目不存在"))?; + let service = PlaybackService::new(app_handle); + service.play_track(track, None)?; + Ok(format!("Playing track: {}", track_id)) +} + +#[command] +pub async fn resume_track(app_handle: AppHandle) -> Result<(), AppError> { + PlaybackService::new(app_handle).resume().await?; + Ok(()) +} + +#[command] +pub async fn pause_track(app_handle: AppHandle) -> Result<(), AppError> { + PlaybackService::new(app_handle).pause().await?; + Ok(()) +} + +#[command] +pub async fn toggle_track(app_handle: AppHandle) -> Result { + let status = PlaybackService::new(app_handle).toggle().await?; + Ok(status) +} + +#[command] +pub async fn current_time() -> f64 { + current_time_from_state() +} + +#[command] +pub async fn get_current_status() -> Result { + current_playback_snapshot() +} + +#[command] +pub async fn set_current_time(app_handle: AppHandle, time: f64) -> Result<(), AppError> { + PlaybackService::new(app_handle).seek(time).await +} + +#[command] +pub async fn set_volume(app_handle: tauri::AppHandle, volume: f32) -> Result<(), AppError> { + let mut state = lock_audio_state()?; + let safe_volume = (volume / 100.0).clamp(0.0, 1.0); + + state.volume = safe_volume; + + if let Some(ref engine) = state.engine { + engine.set_volume(safe_volume); + } + + EventBus::emit(&app_handle, AppEvent::VolumeChanged(safe_volume))?; + + Ok(()) +} diff --git a/src-tauri/src/commands/playback_queue.rs b/src-tauri/src/commands/playback_queue.rs new file mode 100644 index 0000000..3ab2a5f --- /dev/null +++ b/src-tauri/src/commands/playback_queue.rs @@ -0,0 +1,58 @@ +use crate::audio::state::PlayMode; +use crate::errors::AppError; +use crate::models::{PlaybackQueue, Track}; +use crate::services::playback_service::PlaybackService; +use crate::state::playback_state::{lock_audio_state, with_audio_state}; +use tauri::{command, AppHandle}; + +#[command] +pub fn get_playback_queue() -> Result { + with_audio_state(|state| state.playback_queue.clone()) +} + +#[command] +pub async fn clear_queue(app_handle: AppHandle) -> Result<(), AppError> { + { + let mut state = lock_audio_state()?; + state.playback_queue.clear(); + } + + PlaybackService::new(app_handle.clone()).stop().await?; + PlaybackService::emit_queue_changed(&app_handle)?; + Ok(()) +} + +#[command] +pub fn set_play_mode(app_handle: AppHandle, mode: PlayMode) -> Result<(), AppError> { + { + let mut state = lock_audio_state()?; + state.playback_queue.play_mode = mode; + } + PlaybackService::emit_queue_changed(&app_handle)?; + Ok(()) +} + +#[command] +pub fn insert_tracks_as_next(app_handle: AppHandle, tracks: Vec) -> Result<(), AppError> { + { + let mut state = lock_audio_state()?; + state.playback_queue.insert_tracks_as_next(tracks); + } + PlaybackService::emit_queue_changed(&app_handle)?; + Ok(()) +} + +#[command] +pub async fn replace_playlist_and_play( + app_handle: AppHandle, + tracks: Vec, + target_id: i64, +) -> Result<(), AppError> { + let play_index = { + let mut state = lock_audio_state()?; + state.playback_queue.replace_playlist(tracks, target_id)? + }; + PlaybackService::emit_queue_changed(&app_handle)?; + PlaybackService::new(app_handle).play_queue_index(play_index)?; + Ok(()) +} diff --git a/src-tauri/src/commands/playlists.rs b/src-tauri/src/commands/playlists.rs new file mode 100644 index 0000000..7d72d8b --- /dev/null +++ b/src-tauri/src/commands/playlists.rs @@ -0,0 +1,94 @@ +use tauri::{command, State}; + +use crate::errors::AppError; +use crate::models::{ + AddPlaylistTrack, NewPlaylist, Playlist, PlaylistTrack, PlaylistWithTracks, RenamePlaylist, +}; +use crate::state::AppState; + +#[command] +pub async fn create_playlist( + state: State<'_, AppState>, + playlist: NewPlaylist, +) -> Result { + state.playlists.create_playlist(playlist).await +} + +#[command] +pub async fn rename_playlist( + state: State<'_, AppState>, + playlist: RenamePlaylist, +) -> Result { + state.playlists.rename_playlist(playlist).await +} + +#[command] +pub async fn delete_playlist(state: State<'_, AppState>, id: i64) -> Result<(), AppError> { + state.playlists.delete_playlist(id).await +} + +#[command] +pub async fn get_playlist( + state: State<'_, AppState>, + id: i64, +) -> Result, AppError> { + state.playlists.get_playlist(id).await +} + +#[command] +pub async fn list_playlists(state: State<'_, AppState>) -> Result, AppError> { + state.playlists.list_playlists().await +} + +#[command] +pub async fn list_playlists_with_tracks( + state: State<'_, AppState>, +) -> Result, AppError> { + state.playlists.list_playlists_with_tracks().await +} + +#[command] +pub async fn get_playlist_with_tracks( + state: State<'_, AppState>, + id: i64, +) -> Result, AppError> { + state.playlists.get_playlist_with_tracks(id).await +} + +#[command] +pub async fn add_track_to_playlist( + state: State<'_, AppState>, + track: AddPlaylistTrack, +) -> Result { + state.playlists.add_track(track).await +} + +#[command] +pub async fn remove_track_from_playlist( + state: State<'_, AppState>, + playlist_id: i64, + track_id: i64, +) -> Result<(), AppError> { + state.playlists.remove_track(playlist_id, track_id).await +} + +#[command] +pub async fn clear_playlist_tracks( + state: State<'_, AppState>, + playlist_id: i64, +) -> Result<(), AppError> { + state.playlists.clear_tracks(playlist_id).await +} + +#[command] +pub async fn reorder_playlist_track( + state: State<'_, AppState>, + playlist_id: i64, + track_id: i64, + position: i64, +) -> Result<(), AppError> { + state + .playlists + .reorder_track(playlist_id, track_id, position) + .await +} diff --git a/src-tauri/src/commands/plugin_extensions.rs b/src-tauri/src/commands/plugin_extensions.rs new file mode 100644 index 0000000..fa8b964 --- /dev/null +++ b/src-tauri/src/commands/plugin_extensions.rs @@ -0,0 +1,104 @@ +use std::sync::Arc; + +use tauri::{command, State}; + +use crate::errors::AppError; +use plugin_sdk::contributes::MenuLocation; +use plugin_sdk::manifest::ResolvedPluginManifest; +use crate::services::plugin::extension::menu_extension::MenuExtension; +use crate::services::plugin::extension::native_view_extension::NativeViewExtension; +use crate::services::plugin::extension::sidebar_extension::SidebarExtension; +use crate::services::plugin::manager::{PluginManager, PluginViewResolution}; +use plugin_sdk::settings::{PluginSettingMeta, SettingValue}; +use plugin_sdk::traits::CommandArgs; + +#[command] +pub async fn get_menu_extensions( + location: MenuLocation, + manager: State<'_, Arc>, +) -> Result, AppError> { + Ok(manager.menu_extensions(location)) +} + +#[command] +pub async fn get_sidebar_extensions( + manager: State<'_, Arc>, +) -> Result, AppError> { + Ok(manager.sidebar_extensions()) +} + +#[command] +pub async fn get_all_sidebar_extensions( + manager: State<'_, Arc>, +) -> Result, AppError> { + Ok(manager.all_sidebar_extensions()) +} + +#[command] +pub async fn get_native_view_extensions( + manager: State<'_, Arc>, +) -> Result, AppError> { + Ok(manager.native_view_extensions()) +} + +/// All resolved plugin manifests — the only plugin-identity shape the +/// frontend consumes. Routes are already normalized to +/// `/plugins/view/`; no `Option`s leak across the IPC boundary. +#[command] +pub async fn get_plugin_manifests( + manager: State<'_, Arc>, +) -> Result, AppError> { + Ok(manager.all_manifests()) +} + +#[command] +pub async fn get_plugin_view( + plugin_id: String, + view_id: String, + manager: State<'_, Arc>, +) -> Result, AppError> { + Ok(manager.plugin_view(&plugin_id, &view_id)) +} + +#[command] +pub async fn get_plugin_settings( + plugin_id: String, + manager: State<'_, Arc>, +) -> Result, AppError> { + Ok(manager.get_plugin_settings(&plugin_id)) +} + +#[command] +pub async fn update_plugin_setting( + plugin_id: String, + key: String, + value: SettingValue, + manager: State<'_, Arc>, +) -> Result<(), AppError> { + manager.update_plugin_setting(&plugin_id, &key, value) +} + +#[command] +pub async fn enable_plugin_command( + plugin_id: String, + manager: State<'_, Arc>, +) -> Result<(), AppError> { + manager.enable_plugin(&plugin_id) +} + +#[command] +pub async fn disable_plugin_command( + plugin_id: String, + manager: State<'_, Arc>, +) -> Result<(), AppError> { + manager.disable_plugin(&plugin_id) +} + +#[command] +pub async fn execute_plugin_command( + command_id: String, + args: CommandArgs, + manager: State<'_, Arc>, +) -> Result<(), AppError> { + manager.execute_plugin_command(&command_id, args) +} diff --git a/src-tauri/src/commands/recent.rs b/src-tauri/src/commands/recent.rs new file mode 100644 index 0000000..343a14c --- /dev/null +++ b/src-tauri/src/commands/recent.rs @@ -0,0 +1,28 @@ +use tauri::{command, State}; + +use crate::errors::AppError; +use crate::models::RecentPlayedWithTrack; +use crate::state::AppState; + +#[command] +pub async fn load_recently_played( + state: State<'_, AppState>, + limit: i64, + offset: i64, +) -> Result, AppError> { + state.recent.load(limit, offset).await +} + +#[command] +pub async fn add_recently_played( + state: State<'_, AppState>, + track_id: i64, + played_at: String, +) -> Result<(), AppError> { + state.recent.add(track_id, played_at).await +} + +#[command] +pub async fn clear_recently_played(state: State<'_, AppState>) -> Result<(), AppError> { + state.recent.clear().await +} diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs new file mode 100644 index 0000000..9a43fd1 --- /dev/null +++ b/src-tauri/src/commands/settings.rs @@ -0,0 +1,47 @@ +use crate::errors::AppError; +use crate::events::{AppEvent, EventBus}; +use crate::models::AppSettings; +use crate::state::AppState; +use tauri::{command, AppHandle, State}; + +#[command] +pub async fn get_settings(state: State<'_, AppState>) -> Result { + state.settings.get_settings().await +} + +#[command] +pub async fn update_settings( + app_handle: AppHandle, + state: State<'_, AppState>, + settings: AppSettings, +) -> Result { + let updated_settings = state.settings.update_settings(settings).await?; + + EventBus::emit( + &app_handle, + AppEvent::SettingsChanged(updated_settings.clone()), + )?; + + Ok(updated_settings) +} + +#[command] +pub async fn load_settings(state: State<'_, AppState>) -> Result { + state.settings.get_settings().await +} + +#[command] +pub async fn save_settings( + app_handle: AppHandle, + state: State<'_, AppState>, + settings: AppSettings, +) -> Result { + let updated_settings = state.settings.update_settings(settings).await?; + + EventBus::emit( + &app_handle, + AppEvent::SettingsChanged(updated_settings.clone()), + )?; + + Ok(updated_settings) +} diff --git a/src-tauri/src/commands/tracks.rs b/src-tauri/src/commands/tracks.rs new file mode 100644 index 0000000..0d35e5b --- /dev/null +++ b/src-tauri/src/commands/tracks.rs @@ -0,0 +1,71 @@ +use tauri::{command, State}; + +use crate::errors::AppError; +use crate::models::{NewTrack, Track, TrackSearchQuery, UpdateTrack}; +use crate::state::AppState; + +#[command] +pub async fn create_track(state: State<'_, AppState>, track: NewTrack) -> Result { + state.tracks.create_track(track).await +} + +#[command] +pub async fn upsert_track(state: State<'_, AppState>, track: NewTrack) -> Result { + state.tracks.upsert_track(track).await +} + +#[command] +pub async fn update_track( + state: State<'_, AppState>, + track: UpdateTrack, +) -> Result { + state.tracks.update_track(track).await +} + +#[command] +pub async fn delete_track(state: State<'_, AppState>, id: i64) -> Result<(), AppError> { + state.tracks.delete_track(id).await +} + +#[command] +pub async fn delete_track_by_path( + state: State<'_, AppState>, + path: String, +) -> Result<(), AppError> { + state.tracks.delete_track_by_path(&path).await +} + +#[command] +pub async fn get_track(state: State<'_, AppState>, id: i64) -> Result, AppError> { + state.tracks.get_track(id).await +} + +#[command] +pub async fn get_track_by_path( + state: State<'_, AppState>, + path: String, +) -> Result, AppError> { + state.tracks.get_track_by_path(&path).await +} + +#[command] +pub async fn list_tracks(state: State<'_, AppState>) -> Result, AppError> { + state.tracks.list_tracks().await +} + +#[command] +pub async fn search_tracks( + state: State<'_, AppState>, + query: TrackSearchQuery, +) -> Result, AppError> { + state.tracks.search_tracks(query).await +} + +#[command] +pub async fn mark_track_played( + state: State<'_, AppState>, + id: i64, + played_at: String, +) -> Result<(), AppError> { + state.tracks.mark_track_played(id, played_at).await +} diff --git a/src-tauri/src/db/manager.rs b/src-tauri/src/db/manager.rs new file mode 100644 index 0000000..bc64f8a --- /dev/null +++ b/src-tauri/src/db/manager.rs @@ -0,0 +1,58 @@ +use std::path::PathBuf; + +use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous}; +use sqlx::{ConnectOptions, SqlitePool}; +use tauri::Manager; + +use crate::errors::AppError; + +#[derive(Clone)] +pub struct DatabaseManager { + pool: SqlitePool, +} + +impl DatabaseManager { + pub async fn new(app_handle: &tauri::AppHandle) -> Result { + let database_path = database_path(app_handle)?; + + if let Some(parent) = database_path.parent() { + let exists = tokio::fs::try_exists(parent).await.unwrap_or(false); + if !exists { + tokio::fs::create_dir_all(parent) + .await + .map_err(AppError::from)?; + } + } + + let options = SqliteConnectOptions::new() + .filename(&database_path) + .create_if_missing(true) + .journal_mode(SqliteJournalMode::Wal) + .synchronous(SqliteSynchronous::Normal) + .foreign_keys(true) + .disable_statement_logging(); + + let pool = SqlitePoolOptions::new() + .max_connections(5) + .connect_with(options) + .await + .map_err(AppError::from)?; + + sqlx::migrate!("./migrations") + .run(&pool) + .await + .map_err(AppError::from)?; + + Ok(Self { pool }) + } + + pub fn pool(&self) -> &SqlitePool { + &self.pool + } +} + +fn database_path(app_handle: &tauri::AppHandle) -> Result { + let app_data_path = app_handle.path().app_data_dir().map_err(AppError::from)?; + + Ok(app_data_path.join("music.sqlite")) +} diff --git a/src-tauri/src/db/mod.rs b/src-tauri/src/db/mod.rs new file mode 100644 index 0000000..ac04603 --- /dev/null +++ b/src-tauri/src/db/mod.rs @@ -0,0 +1,3 @@ +pub mod manager; + +pub use manager::DatabaseManager; diff --git a/src-tauri/src/errors/app_error.rs b/src-tauri/src/errors/app_error.rs new file mode 100644 index 0000000..da6a493 --- /dev/null +++ b/src-tauri/src/errors/app_error.rs @@ -0,0 +1,96 @@ +use serde::Serialize; + +use plugin_sdk::permissions::PluginPermission; +use plugin_sdk::errors::PluginError; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase", tag = "kind", content = "message")] +pub enum AppError { + Database(String), + Io(String), + Migration(String), + Service(String), + Command(String), + Platform(String), + Domain(String), + Plugin(String), + PluginPermissionDenied { + plugin_id: String, + permission: PluginPermission, + }, +} + +impl std::fmt::Display for AppError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AppError::Database(message) + | AppError::Io(message) + | AppError::Migration(message) + | AppError::Service(message) + | AppError::Command(message) + | AppError::Platform(message) + | AppError::Domain(message) + | AppError::Plugin(message) => formatter.write_str(message), + AppError::PluginPermissionDenied { + plugin_id, + permission, + } => { + write!( + formatter, + "Plugin '{}' denied permission {:?}", + plugin_id, permission + ) + } + } + } +} + +impl std::error::Error for AppError {} + +impl From for AppError { + fn from(error: sqlx::Error) -> Self { + AppError::Database(error.to_string()) + } +} + +impl From for AppError { + fn from(error: sqlx::migrate::MigrateError) -> Self { + AppError::Migration(error.to_string()) + } +} + +impl From for AppError { + fn from(error: std::io::Error) -> Self { + AppError::Io(error.to_string()) + } +} + +impl From for AppError { + fn from(error: String) -> Self { + AppError::Service(error) + } +} + +impl From<&str> for AppError { + fn from(error: &str) -> Self { + AppError::Service(error.to_string()) + } +} + +impl From for AppError { + fn from(error: tauri::Error) -> Self { + AppError::Platform(error.to_string()) + } +} + +impl From for AppError { + fn from(error: PluginError) -> Self { + match error { + PluginError::Plugin(msg) => AppError::Plugin(msg), + PluginError::PermissionDenied { plugin_id, permission } => { + AppError::PluginPermissionDenied { plugin_id, permission } + } + PluginError::Io(msg) => AppError::Io(msg), + } + } +} diff --git a/src-tauri/src/errors/mod.rs b/src-tauri/src/errors/mod.rs new file mode 100644 index 0000000..eaeaca9 --- /dev/null +++ b/src-tauri/src/errors/mod.rs @@ -0,0 +1,3 @@ +pub mod app_error; + +pub use app_error::AppError; diff --git a/src-tauri/src/events/bus.rs b/src-tauri/src/events/bus.rs new file mode 100644 index 0000000..cd42476 --- /dev/null +++ b/src-tauri/src/events/bus.rs @@ -0,0 +1,50 @@ +use std::sync::{LazyLock, RwLock}; + +use tauri::{AppHandle, Emitter}; + +use crate::errors::AppError; +use crate::events::payloads::{AppEvent, LyricsLoadedPayload}; + +pub type EventListener = Box; + +static LISTENERS: LazyLock>> = + LazyLock::new(|| RwLock::new(Vec::new())); + +pub struct EventBus; + +impl EventBus { + pub const CHANNEL: &'static str = "global-app-event"; + + pub fn subscribe(listener: EventListener) { + if let Ok(mut lock) = LISTENERS.write() { + lock.push(listener); + } + } + + pub fn emit(app_handle: &AppHandle, event: AppEvent) -> Result<(), AppError> { + Self::notify_local(&event); + app_handle.emit(Self::CHANNEL, event)?; + Ok(()) + } + + pub fn emit_local_only(event: &AppEvent) { + Self::notify_local(event); + } + + pub fn emit_lyrics_loaded( + app_handle: &AppHandle, + payload: LyricsLoadedPayload, + ) -> Result<(), AppError> { + Self::emit(app_handle, AppEvent::LyricsLoaded(payload)) + } + + fn notify_local(event: &AppEvent) { + let listeners = match LISTENERS.read() { + Ok(lock) => lock, + Err(_) => return, + }; + for listener in listeners.iter() { + listener(event); + } + } +} diff --git a/src-tauri/src/events/mod.rs b/src-tauri/src/events/mod.rs new file mode 100644 index 0000000..125b679 --- /dev/null +++ b/src-tauri/src/events/mod.rs @@ -0,0 +1,5 @@ +pub mod bus; +pub mod payloads; + +pub use bus::*; +pub use payloads::*; diff --git a/src-tauri/src/events/payloads.rs b/src-tauri/src/events/payloads.rs new file mode 100644 index 0000000..e699c54 --- /dev/null +++ b/src-tauri/src/events/payloads.rs @@ -0,0 +1,55 @@ +use serde::Serialize; + +use crate::models::{AppSettings, PlaybackQueue, Track}; + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaybackStatePayload { + pub playing: bool, + pub current_time: f64, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaybackProgressPayload { + pub current_time: f64, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TrackStartedPayload { + pub track: Track, + pub index: usize, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LyricLinePayload { + pub timestamp_ms: u64, + pub text: String, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LyricsLoadedPayload { + pub song_id: i64, + pub lines: Vec, +} + +#[derive(Clone, Serialize)] +#[serde(tag = "type", content = "payload")] +pub enum AppEvent { + VolumeChanged(f32), + + SettingsChanged(AppSettings), + + QueueChanged(PlaybackQueue), + + PlaybackStateChanged(PlaybackStatePayload), + + PlaybackProgress(PlaybackProgressPayload), + + TrackStarted(TrackStartedPayload), + + LyricsLoaded(LyricsLoadedPayload), +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e7f1264..fc4ef53 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,46 +1,369 @@ -use std::fs::File; +mod audio; +mod commands; +mod db; +mod errors; +mod events; +mod media_controls; +mod metadata; +mod models; +mod repositories; +mod services; +mod state; -use tauri::command; +use std::sync::Arc; -use web_audio_api::context::{AudioContext, BaseAudioContext}; -use web_audio_api::node::{AudioNode, AudioScheduledSourceNode}; +use tauri::{ + menu::{Menu, MenuBuilder, MenuItem, SubmenuBuilder}, + tray::{MouseButton, TrayIconBuilder, TrayIconEvent}, + Emitter, Manager, +}; -#[command] -fn play_music(name: &str) -> String { - let context = AudioContext::default(); +use commands::library::{ + delete_track_file, delete_track_files, execute_scan, get_track_cover, load_track_library, + scan_track_directories, show_in_folder, trash_track_files, +}; - let current_dir = std::env::current_dir().unwrap(); - let file_path = current_dir.join("..").join("static").join(name); +use commands::playback::{ + current_time, get_current_status, insert_track_as_next, pause_track, play_next_track, + play_previous_track, play_queue_track, play_track, remove_track_from_queue, resume_track, + set_current_time, set_volume, stop_track, sync_playback_queue, toggle_track, +}; +use services::playback_service::PlaybackService; - let file = File::open(file_path).unwrap(); - let buffer = context.decode_audio_data_sync(file).unwrap(); +use commands::recent::{add_recently_played, clear_recently_played, load_recently_played}; - let mut src = context.create_buffer_source(); - src.set_buffer(buffer); - src.set_loop(false); +use commands::settings::{get_settings, load_settings, save_settings, update_settings}; - // // create a biquad filter - // let biquad = context.create_biquad_filter(); - // biquad.frequency().set_value(125.); - // connect the audio nodes - // src.connect(&biquad); - // biquad.connect(&context.destination()); +use commands::menu::show_context_menu; +use commands::playlists::{ + add_track_to_playlist, clear_playlist_tracks, create_playlist, delete_playlist, get_playlist, + get_playlist_with_tracks, list_playlists, list_playlists_with_tracks, + remove_track_from_playlist, rename_playlist, reorder_playlist_track, +}; +use commands::tracks::{ + create_track, delete_track, delete_track_by_path, get_track, get_track_by_path, list_tracks, + mark_track_played, search_tracks, update_track, upsert_track, +}; +use db::DatabaseManager; +use media_controls::init_media_controls; +use repositories::sqlite::{ + SqlitePlaylistRepository, SqliteRecentRepository, SqliteSettingsRepository, + SqliteTrackRepository, +}; +use services::plugin::capability::permission_checker::PermissionChecker; +use services::plugin::capability::CapabilityRegistry; +use services::plugin::provider::{BuiltinManifestProvider, ManifestProvider}; +use services::plugin::registry::{FactoryRegistry, ManifestRegistry}; +use services::plugin::runtime::context_factory::PluginContextFactory; +use services::plugin::settings::persistence::JsonPluginStorage; +use services::plugin::settings::settings_registry::SettingsRegistry; +use services::plugin::PluginManager; +use services::{PlaylistService, RecentService, SettingsService, TrackService}; +use state::AppState; - src.connect(&context.destination()); +use commands::playback_queue::{ + clear_queue, get_playback_queue, insert_tracks_as_next, replace_playlist_and_play, + set_play_mode, +}; - src.start(); +use commands::plugin_extensions::{ + disable_plugin_command, enable_plugin_command, execute_plugin_command, + get_all_sidebar_extensions, get_menu_extensions, get_native_view_extensions, + get_plugin_manifests, get_plugin_settings, get_plugin_view, get_sidebar_extensions, + update_plugin_setting, +}; - let buffer_duration = src.buffer().unwrap().duration() as f64; - let sleep_duration = std::time::Duration::from_secs_f64(buffer_duration); +use commands::equalizer::{apply_eq_preset, get_eq_state, set_eq_band, set_eq_enabled}; - format!("Playing music: {}", name) -} +use audio::init_audio_state; + +use crate::errors::AppError; + +pub fn init_startup_scan(_app_handle: tauri::AppHandle) {} #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() - // .plugin(tauri_plugin_opener::init()) - .invoke_handler(tauri::generate_handler![play_music]) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_store::Builder::new().build()) + .register_uri_scheme_protocol("plugin", crate::services::plugin::asset::scheme::handle) + .setup(|app| { + println!("[Timer] setup start"); + let app_handle = app.handle().clone(); + + println!("[Timer] creating database..."); + let database = tauri::async_runtime::block_on(DatabaseManager::new(&app_handle))?; + let pool = database.pool().clone(); + + let track_repository = Arc::new(SqliteTrackRepository::new(pool.clone())); + let playlist_repository = Arc::new(SqlitePlaylistRepository::new(pool.clone())); + let setting_repository = Arc::new(SqliteSettingsRepository::new(pool.clone())); + let recent_repository = Arc::new(SqliteRecentRepository::new(pool)); + + println!("[Timer] loading settings..."); + let setting_service = Arc::new(SettingsService::new(setting_repository)); + tauri::async_runtime::block_on(setting_service.get_settings())?; + + println!("[Timer] creating services..."); + let track_service = Arc::new(TrackService::new(track_repository)); + let playlist_service = Arc::new(PlaylistService::new(playlist_repository)); + let recent_service = Arc::new(RecentService::new(recent_repository)); + + let capability_registry = Arc::new(CapabilityRegistry::new()); + let permission_checker = + Arc::new(PermissionChecker::new(Arc::clone(&capability_registry))); + let settings_registry = Arc::new(SettingsRegistry::new()); + + // 共享音频处理器 registry:由 PluginManager(激活/失活插件时写入) + // 与 WebAudioEngine 的 ExternalProcessorNode(渲染量子时读取)共同持有。 + let audio_processor_registry = Arc::new( + crate::services::plugin::AudioProcessorRegistry::new(), + ); + + let app_data_dir = app_handle + .path() + .app_data_dir() + .unwrap_or_else(|_| std::path::PathBuf::from(".")); + let storage = Arc::new(JsonPluginStorage::new(app_data_dir)); + + // Plugin manifest aggregation: collect → normalize → validate → + // resolve → cache. Builtin manifests are compile-time embedded; + // packaged/user providers are reserved for future expansion. + let manifest_registry = Arc::new(ManifestRegistry::new()); + let providers: Vec> = + vec![Box::new(BuiltinManifestProvider)]; + if let Err(e) = manifest_registry.register_from_providers(providers) { + eprintln!("[startup] Failed to load plugin manifests: {}", e); + } + let factory_registry = Arc::new(FactoryRegistry::new()); + + let context_factory = PluginContextFactory::new( + app_handle.clone(), + permission_checker, + Arc::new(PlaybackService::new(app.handle().clone())), + track_service.clone(), + setting_service.clone(), + Arc::clone(&settings_registry), + ); + + let plugin_manager = Arc::new(PluginManager::new( + app.handle().clone(), + manifest_registry.clone(), + factory_registry.clone(), + context_factory, + capability_registry, + settings_registry, + storage, + Arc::clone(&audio_processor_registry), + )); + plugin_manager.load_persisted_state(); + plugin_manager.load_builtin_plugins(); + let pm = plugin_manager.clone(); + let handle_for_plugins = app.handle().clone(); + tauri::async_runtime::spawn_blocking(move || { + pm.load_packaged_plugins(); + let _ = handle_for_plugins.emit("plugins:loaded", ()); + }); + + let app_state = AppState::new( + track_service, + playlist_service, + setting_service, + recent_service, + plugin_manager.clone(), + ); + + app.manage(app_state); + app.manage(plugin_manager); + + init_audio_state(models::PlaybackQueue::default()); + + println!("[Timer] init media controls..."); + let _ = init_media_controls(app.handle().clone()); + println!("[Timer] starting playback progress task..."); + PlaybackService::spawn_playback_progress_task(app.handle().clone()); + + println!("[Timer] setup complete, WebView should render now"); + + let settings_item = MenuItem::with_id(app, "settings", "设置", true, None::<&str>)?; + let quit_item = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?; + let tray_menu = Menu::with_items(app, &[&settings_item, &quit_item])?; + let file_menu = SubmenuBuilder::new(app, "文件") + .text("quit", "退出") + .build()?; + + let app_menu = MenuBuilder::new(app).items(&[&file_menu]).build()?; + app.set_menu(app_menu)?; + + let icon = app + .default_window_icon() + .cloned() + .ok_or_else(|| AppError::from("Application default window icon is missing"))?; + + let _tray = TrayIconBuilder::new() + .icon(icon) + .menu(&tray_menu) + .show_menu_on_left_click(false) + .on_menu_event( + |app_handle: &tauri::AppHandle, event| match event.id.as_ref() { + "settings" => { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.emit("tray:navigate", "settings"); + let _ = window.unminimize(); + let _ = window.show(); + let _ = window.set_focus(); + } + } + "quit" => { + app_handle.exit(0); + } + _ => {} + }, + ) + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + .. + } = event + { + let app = tray.app_handle(); + if let Some(window) = app.get_webview_window("main") { + let _ = window.unminimize(); + let _ = window.show(); + let _ = window.set_focus(); + } + } + }) + .build(app)?; + + let handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + let state = handle.state::(); + if let Ok(settings) = state.settings.get_settings().await { + if settings.scan_on_startup { + let library_dirs = settings.library_dirs; + let scanned_result = tauri::async_runtime::spawn_blocking(move || { + execute_scan(library_dirs) + }) + .await; + + match scanned_result { + Ok(Ok(scanned)) => { + let mut tracks = Vec::with_capacity(scanned.len()); + for track in scanned { + if let Ok(saved) = state.tracks.upsert_track(track).await { + tracks.push(saved); + } + } + let _ = handle.emit("library:refreshed", tracks); + } + Ok(Err(error)) => { + eprintln!("{}", error); + } + Err(error) => { + eprintln!("{}", error); + } + } + } + } + }); + + app.on_menu_event(move |app_handle: &tauri::AppHandle, event| { + match event.id().0.as_str() { + "quit" => { + app_handle.exit(0); + } + "play" => { + let handle = app_handle.clone(); + tauri::async_runtime::spawn(async move { + let _ = PlaybackService::new(handle).toggle().await; + }); + } + "next" => { + let handle = app_handle.clone(); + tauri::async_runtime::spawn(async move { + let _ = PlaybackService::new(handle).next().await; + }); + } + _ => {} + } + }); + + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + show_context_menu, + create_track, + upsert_track, + update_track, + delete_track, + delete_track_by_path, + get_track, + get_track_by_path, + list_tracks, + search_tracks, + mark_track_played, + create_playlist, + rename_playlist, + delete_playlist, + get_playlist, + list_playlists, + list_playlists_with_tracks, + get_playlist_with_tracks, + add_track_to_playlist, + remove_track_from_playlist, + clear_playlist_tracks, + reorder_playlist_track, + play_track, + play_queue_track, + insert_track_as_next, + remove_track_from_queue, + play_next_track, + play_previous_track, + stop_track, + sync_playback_queue, + resume_track, + pause_track, + toggle_track, + current_time, + get_current_status, + set_current_time, + set_volume, + scan_track_directories, + show_in_folder, + delete_track_file, + delete_track_files, + trash_track_files, + load_track_library, + get_track_cover, + load_recently_played, + add_recently_played, + clear_recently_played, + get_settings, + update_settings, + load_settings, + save_settings, + get_playback_queue, + replace_playlist_and_play, + insert_tracks_as_next, + set_play_mode, + clear_queue, + get_menu_extensions, + get_sidebar_extensions, + get_all_sidebar_extensions, + get_native_view_extensions, + get_plugin_manifests, + get_plugin_settings, + update_plugin_setting, + enable_plugin_command, + disable_plugin_command, + execute_plugin_command, + get_plugin_view, + get_eq_state, + set_eq_band, + apply_eq_preset, + set_eq_enabled, + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 2abccd9..4a233fd 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,5 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - tauri_app_lib::run() + rust_echo_music_lib::run() } diff --git a/src-tauri/src/media_controls.rs b/src-tauri/src/media_controls.rs new file mode 100644 index 0000000..dc510d7 --- /dev/null +++ b/src-tauri/src/media_controls.rs @@ -0,0 +1,107 @@ +use std::ffi::c_void; +use std::sync::mpsc::{self, Sender}; +use std::sync::OnceLock; +use std::thread; +use std::time::Duration; + +use souvlaki::{MediaControls, MediaMetadata, MediaPlayback, MediaPosition, PlatformConfig}; +use tauri::{AppHandle, Manager, WebviewWindow}; + +use crate::commands::playback::handle_media_control_event; +use crate::events::PlaybackStatePayload; +use crate::models::playback::NativeTrackMetadata; + +#[derive(Clone)] +enum MediaControlMessage { + Metadata(NativeTrackMetadata), + Playback(PlaybackStatePayload), +} + +static MEDIA_CONTROL_SENDER: OnceLock> = OnceLock::new(); + +#[cfg(target_os = "windows")] +fn window_handle(window: &WebviewWindow) -> Option { + window.hwnd().ok().map(|handle| handle.0 as isize) +} + +#[cfg(not(target_os = "windows"))] +fn window_handle(_window: &WebviewWindow) -> Option { + None +} + +pub fn init_media_controls(app_handle: AppHandle) -> Result<(), String> { + let window = app_handle + .get_webview_window("main") + .ok_or_else(|| "main window not found".to_string())?; + + let hwnd = window_handle(&window); + let (tx, rx) = mpsc::channel::(); + let app_for_events = app_handle.clone(); + + thread::spawn(move || { + #[cfg(target_os = "windows")] + if hwnd.is_none() { + return; + } + + let config = PlatformConfig { + display_name: "RustEchoMusic", + dbus_name: "rust_echo_music", + hwnd: hwnd.map(|value| value as *mut c_void), + }; + + let Ok(mut controls) = MediaControls::new(config) else { + return; + }; + + let _ = controls.attach(move |event| { + let app = app_for_events.clone(); + tauri::async_runtime::spawn(async move { + handle_media_control_event(app, event).await; + }); + }); + + while let Ok(message) = rx.recv() { + match message { + MediaControlMessage::Metadata(track) => { + let duration = track.duration.map(Duration::from_secs_f64); + let metadata = MediaMetadata { + title: Some(track.title.as_str()), + album: Some(track.album.as_str()), + artist: Some(track.artist.as_str()), + cover_url: None, + duration, + }; + let _ = controls.set_metadata(metadata); + } + MediaControlMessage::Playback(state) => { + let progress = Some(MediaPosition(Duration::from_secs_f64(state.current_time))); + let playback = if state.playing { + MediaPlayback::Playing { progress } + } else { + MediaPlayback::Paused { progress } + }; + let _ = controls.set_playback(playback); + } + } + } + }); + + let _ = MEDIA_CONTROL_SENDER.set(tx); + + Ok(()) +} + +fn send(message: MediaControlMessage) { + if let Some(sender) = MEDIA_CONTROL_SENDER.get() { + let _ = sender.send(message); + } +} + +pub fn update_media_controls_metadata(track: NativeTrackMetadata) { + send(MediaControlMessage::Metadata(track)); +} + +pub fn update_media_controls_playback(state: PlaybackStatePayload) { + send(MediaControlMessage::Playback(state)); +} diff --git a/src-tauri/src/metadata.rs b/src-tauri/src/metadata.rs new file mode 100644 index 0000000..f4960ae --- /dev/null +++ b/src-tauri/src/metadata.rs @@ -0,0 +1,53 @@ +use std::fs; +use std::path::Path; + +use lofty::prelude::*; +use lofty::probe::Probe; + +use crate::models::NewTrack; + +pub fn parse_single_track(file_path: &Path) -> Option { + let tagged_file = Probe::open(file_path).ok()?.read().ok()?; + + let tag = tagged_file + .primary_tag() + .or_else(|| tagged_file.first_tag()); + + let title = tag + .and_then(|t| t.title()) + .map(|s| s.to_string()) + .unwrap_or_else(|| { + file_path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("Unknown Track") + .to_string() + }); + + let artist = tag + .and_then(|t| t.artist()) + .map(|s| s.to_string()) + .or_else(|| Some("Unknown Artist".to_string())); + + let album = tag + .and_then(|t| t.album()) + .map(|s| s.to_string()) + .or_else(|| Some("Unknown Album".to_string())); + + let props = tagged_file.properties(); + let duration = (props.duration().as_secs_f64() * 1000.0).round() as i64; + let path = file_path.to_string_lossy().into_owned(); + let file_size = fs::metadata(file_path) + .ok() + .and_then(|metadata| i64::try_from(metadata.len()).ok()); + + Some(NewTrack { + title, + artist, + album, + duration, + path, + cover: None, + file_size, + }) +} diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs new file mode 100644 index 0000000..b4d7a0f --- /dev/null +++ b/src-tauri/src/models/mod.rs @@ -0,0 +1,14 @@ +pub mod playback; +pub mod playback_queue; +pub mod playlist; +pub mod recent; +pub mod settings; +pub mod track; + +pub use playback_queue::PlaybackQueue; +pub use playlist::{ + AddPlaylistTrack, NewPlaylist, Playlist, PlaylistTrack, PlaylistWithTracks, RenamePlaylist, +}; +pub use recent::RecentPlayedWithTrack; +pub use settings::{AppSettings, PluginLogLevel, SettingRow, ThemeMode}; +pub use track::{NewTrack, SortDirection, Track, TrackSearchQuery, TrackSortBy, UpdateTrack}; diff --git a/src-tauri/src/models/playback.rs b/src-tauri/src/models/playback.rs new file mode 100644 index 0000000..b7be620 --- /dev/null +++ b/src-tauri/src/models/playback.rs @@ -0,0 +1,31 @@ +use serde::Serialize; + +use crate::models::Track; + +#[derive(Clone)] +pub struct NativeTrackMetadata { + pub title: String, + pub album: String, + pub artist: String, + pub duration: Option, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaybackStatusSnapshot { + pub playing: bool, + pub current_time: f64, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaybackTrackStartedPayload { + pub track: Track, + pub index: usize, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaybackTrackInfo { + pub id: Option, +} diff --git a/src-tauri/src/models/playback_queue.rs b/src-tauri/src/models/playback_queue.rs new file mode 100644 index 0000000..4509d07 --- /dev/null +++ b/src-tauri/src/models/playback_queue.rs @@ -0,0 +1,263 @@ +use rand::RngExt; +use serde::{Deserialize, Serialize}; + +use crate::audio::state::PlayMode; +use crate::models::Track; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaybackQueue { + pub tracks: Vec, + pub current_index: Option, + pub play_mode: PlayMode, + pub history: Vec, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct QueueRemoveResult { + pub play_index: Option, + pub should_stop: bool, +} + +impl Default for PlaybackQueue { + fn default() -> Self { + Self { + tracks: Vec::new(), + current_index: None, + play_mode: PlayMode::ListLoop, + history: Vec::new(), + } + } +} + +impl PlaybackQueue { + pub fn sync( + &mut self, + tracks: Vec, + current_index: Option, + play_mode: PlayMode, + history: Vec, + ) { + self.tracks = tracks; + self.current_index = current_index.filter(|i| *i < self.tracks.len()); + self.play_mode = play_mode; + self.history = history; + } + + pub fn clear(&mut self) { + self.tracks.clear(); + self.current_index = None; + self.history.clear(); + } + + pub fn is_empty(&self) -> bool { + self.tracks.is_empty() + } + + pub fn len(&self) -> usize { + self.tracks.len() + } + + pub fn current_index(&self) -> Option { + self.current_index + } + + pub fn set_current_index(&mut self, index: Option) { + self.current_index = index.filter(|i| *i < self.tracks.len()); + } + + pub fn set_play_mode(&mut self, mode: PlayMode) { + self.play_mode = mode; + } + + pub fn current_track(&self) -> Option<&Track> { + self.current_index.and_then(|i| self.tracks.get(i)) + } + + pub fn current_track_cloned(&self) -> Option { + self.current_track().cloned() + } + + pub fn track(&self, index: usize) -> Option<&Track> { + self.tracks.get(index) + } + + pub fn require_track(&self, index: usize) -> Result { + self.tracks + .get(index) + .cloned() + .ok_or_else(|| "Invalid queue index".to_string()) + } + + pub fn contains_track(&self, track_id: i64) -> bool { + self.tracks.iter().any(|t| t.id == track_id) + } + + pub fn find_track(&self, track_id: i64) -> Option<&Track> { + self.tracks.iter().find(|t| t.id == track_id) + } + + pub fn remove_track(&mut self, track_id: i64) -> Option { + let remove_index = self.tracks.iter().position(|track| track.id == track_id)?; + + let was_current = self.current_index == Some(remove_index); + + self.tracks.remove(remove_index); + self.history.retain(|id| *id != track_id); + + if self.tracks.is_empty() { + self.current_index = None; + return Some(QueueRemoveResult { + play_index: None, + should_stop: true, + }); + } + + if was_current { + let next_index = remove_index.min(self.tracks.len() - 1); + self.current_index = Some(next_index); + return Some(QueueRemoveResult { + play_index: Some(next_index), + should_stop: false, + }); + } + + if let Some(current_index) = self.current_index { + if remove_index < current_index { + self.current_index = Some(current_index - 1); + } + } + + Some(QueueRemoveResult { + play_index: None, + should_stop: false, + }) + } + + pub fn insert_next(&mut self, mut track: Track) { + track.cover = None; + + if let Some(existing_index) = self.tracks.iter().position(|item| item.id == track.id) { + if self.current_index == Some(existing_index) { + return; + } + + self.tracks.remove(existing_index); + + if let Some(current_index) = self.current_index { + if existing_index < current_index { + self.current_index = Some(current_index - 1); + } + } + } + + if self.tracks.is_empty() || self.current_index.is_none() { + self.tracks.push(track); + self.current_index = Some(0); + return; + } + + let insert_index = self.current_index.unwrap() + 1; + + self.tracks.insert(insert_index, track); + } + + pub fn insert_tracks_as_next(&mut self, tracks: Vec) { + for track in tracks.into_iter().rev() { + self.insert_next(track); + } + } + + pub fn replace_playlist( + &mut self, + tracks: Vec, + target_id: i64, + ) -> Result { + self.tracks = tracks; + self.history.clear(); + let index = self + .tracks + .iter() + .position(|t| t.id == target_id) + .unwrap_or(0); + self.current_index = Some(index); + Ok(index) + } + + pub fn next_index(&self) -> Option { + if self.tracks.is_empty() { + return None; + } + + let current = self.current_index.unwrap_or(0); + + match self.play_mode { + PlayMode::SingleLoop => Some(current), + + PlayMode::ListLoop => Some((current + 1) % self.tracks.len()), + + PlayMode::Shuffle => { + if self.tracks.len() <= 1 { + return Some(0); + } + + let mut rng = rand::rng(); + + loop { + let next = rng.random_range(0..self.tracks.len()); + + if next != current { + return Some(next); + } + } + } + } + } + + pub fn previous_index(&self) -> Option { + if self.tracks.is_empty() { + return None; + } + + if let Some(last_id) = self.history.last() { + if let Some(index) = self.tracks.iter().position(|track| track.id == *last_id) { + return Some(index); + } + } + + let current = self.current_index.unwrap_or(0); + + Some(if current == 0 { + self.tracks.len() - 1 + } else { + current - 1 + }) + } + + pub fn move_next(&mut self) -> Option { + if let Some(track) = self.current_track() { + if self.history.last() != Some(&track.id) { + self.history.push(track.id); + } + } + + self.next_index() + } + + pub fn move_previous(&mut self) -> Option { + let index = self.previous_index()?; + + if let Some(track) = self.tracks.get(index) { + if self.history.last() == Some(&track.id) { + self.history.pop(); + } + } + + Some(index) + } + + pub fn payload(&self) -> Self { + self.clone() + } +} diff --git a/src-tauri/src/models/playlist.rs b/src-tauri/src/models/playlist.rs new file mode 100644 index 0000000..2692373 --- /dev/null +++ b/src-tauri/src/models/playlist.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; + +use super::track::Track; + +#[derive(Clone, Debug, Deserialize, FromRow, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Playlist { + pub id: i64, + pub name: String, + pub created_at: String, +} + +#[derive(Clone, Debug, Deserialize, FromRow, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaylistTrack { + pub id: i64, + pub playlist_id: i64, + pub track_id: i64, + pub position: i64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaylistWithTracks { + pub playlist: Playlist, + pub tracks: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NewPlaylist { + pub name: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RenamePlaylist { + pub id: i64, + pub name: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AddPlaylistTrack { + pub playlist_id: i64, + pub track_id: i64, + pub position: Option, +} diff --git a/src-tauri/src/models/recent.rs b/src-tauri/src/models/recent.rs new file mode 100644 index 0000000..8105007 --- /dev/null +++ b/src-tauri/src/models/recent.rs @@ -0,0 +1,8 @@ +use crate::models::Track; + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RecentPlayedWithTrack { + pub track: Track, + pub played_at: String, +} diff --git a/src-tauri/src/models/settings.rs b/src-tauri/src/models/settings.rs new file mode 100644 index 0000000..7373041 --- /dev/null +++ b/src-tauri/src/models/settings.rs @@ -0,0 +1,119 @@ +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSettings { + pub theme: ThemeMode, + pub volume: u8, + pub library_dirs: Vec, + pub scan_on_startup: bool, + pub reduce_motion: bool, + pub use_album_artist_grouping: bool, + pub plugin_dirs: Vec, + pub plugin_dev_mode: bool, + pub plugin_scan_on_startup: bool, + pub plugin_log_level: PluginLogLevel, +} + +impl Default for AppSettings { + fn default() -> Self { + Self { + theme: ThemeMode::Auto, + volume: 80, + library_dirs: Vec::new(), + scan_on_startup: false, + reduce_motion: false, + use_album_artist_grouping: false, + plugin_dirs: Vec::new(), + plugin_dev_mode: false, + plugin_scan_on_startup: true, + plugin_log_level: PluginLogLevel::Warn, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ThemeMode { + Auto, + Light, + Dark, +} + +impl ThemeMode { + pub fn as_str(&self) -> &'static str { + match self { + ThemeMode::Auto => "auto", + ThemeMode::Light => "light", + ThemeMode::Dark => "dark", + } + } +} + +impl TryFrom for ThemeMode { + type Error = String; + + fn try_from(value: String) -> Result>::Error> { + match value.as_str() { + "auto" => Ok(ThemeMode::Auto), + "light" => Ok(ThemeMode::Light), + "dark" => Ok(ThemeMode::Dark), + _ => Err(format!("无效主题: {}", value)), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum PluginLogLevel { + Off, + Error, + Warn, + Info, + Debug, +} + +impl PluginLogLevel { + pub fn as_str(&self) -> &'static str { + match self { + PluginLogLevel::Off => "off", + PluginLogLevel::Error => "error", + PluginLogLevel::Warn => "warn", + PluginLogLevel::Info => "info", + PluginLogLevel::Debug => "debug", + } + } +} + +impl TryFrom for PluginLogLevel { + type Error = String; + + fn try_from(value: String) -> Result>::Error> { + match value.as_str() { + "off" => Ok(PluginLogLevel::Off), + "error" => Ok(PluginLogLevel::Error), + "warn" => Ok(PluginLogLevel::Warn), + "info" => Ok(PluginLogLevel::Info), + "debug" => Ok(PluginLogLevel::Debug), + _ => Err(format!("无效插件日志级别: {}", value)), + } + } +} + +#[derive(Clone, Debug, FromRow)] +pub struct SettingRow { + pub id: i64, + pub theme: String, + pub volume: i64, + pub scan_on_startup: i64, + pub reduce_motion: i64, + pub library_dirs: String, + pub use_album_artist_grouping: i64, + pub plugin_dirs: String, + pub plugin_dev_mode: i64, + pub plugin_scan_on_startup: i64, + pub plugin_log_level: String, + pub created_at: String, + pub updated_at: String, +} diff --git a/src-tauri/src/models/track.rs b/src-tauri/src/models/track.rs new file mode 100644 index 0000000..d771da4 --- /dev/null +++ b/src-tauri/src/models/track.rs @@ -0,0 +1,74 @@ +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; + +#[derive(Clone, Debug, Deserialize, FromRow, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Track { + pub id: i64, + pub title: String, + pub artist: Option, + pub album: Option, + pub duration: i64, + pub path: String, + pub cover: Option, + pub file_size: Option, + pub play_count: i64, + pub last_played_at: Option, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NewTrack { + pub title: String, + pub artist: Option, + pub album: Option, + pub duration: i64, + pub path: String, + pub cover: Option, + pub file_size: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateTrack { + pub id: i64, + pub title: String, + pub artist: Option, + pub album: Option, + pub duration: i64, + pub path: String, + pub cover: Option, + pub file_size: Option, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum TrackSortBy { + Title, + Artist, + Album, + Duration, + CreatedAt, + UpdatedAt, + PlayCount, + LastPlayedAt, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum SortDirection { + Asc, + Desc, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct TrackSearchQuery { + pub keyword: Option, + pub sort_by: Option, + pub sort_direction: Option, + pub limit: Option, + pub offset: Option, +} diff --git a/src-tauri/src/repositories/mod.rs b/src-tauri/src/repositories/mod.rs new file mode 100644 index 0000000..6b1c108 --- /dev/null +++ b/src-tauri/src/repositories/mod.rs @@ -0,0 +1 @@ +pub mod sqlite; diff --git a/src-tauri/src/repositories/sqlite/mod.rs b/src-tauri/src/repositories/sqlite/mod.rs new file mode 100644 index 0000000..2230893 --- /dev/null +++ b/src-tauri/src/repositories/sqlite/mod.rs @@ -0,0 +1,9 @@ +pub mod playlist_repository; +pub mod recent_repository; +pub mod settings_repository; +pub mod track_repository; + +pub use playlist_repository::SqlitePlaylistRepository; +pub use recent_repository::SqliteRecentRepository; +pub use settings_repository::SqliteSettingsRepository; +pub use track_repository::SqliteTrackRepository; diff --git a/src-tauri/src/repositories/sqlite/playlist_repository.rs b/src-tauri/src/repositories/sqlite/playlist_repository.rs new file mode 100644 index 0000000..d6d9791 --- /dev/null +++ b/src-tauri/src/repositories/sqlite/playlist_repository.rs @@ -0,0 +1,178 @@ +use sqlx::SqlitePool; + +use crate::errors::AppError; +use crate::models::{ + AddPlaylistTrack, NewPlaylist, Playlist, PlaylistTrack, PlaylistWithTracks, RenamePlaylist, + Track, +}; + +#[derive(Clone)] +pub struct SqlitePlaylistRepository { + pool: SqlitePool, +} + +impl SqlitePlaylistRepository { + pub fn new(pool: SqlitePool) -> Self { + Self { pool } + } + + pub async fn create(&self, playlist: NewPlaylist) -> Result { + let res = sqlx::query_as::<_, Playlist>( + "INSERT INTO playlists (name, created_at) + VALUES (?1, datetime('now')) + RETURNING id, name, created_at", + ) + .bind(playlist.name) + .fetch_one(&self.pool) + .await?; + + Ok(res) + } + + pub async fn rename(&self, playlist: RenamePlaylist) -> Result { + let res = sqlx::query_as::<_, Playlist>( + "UPDATE playlists SET name = ?1 WHERE id = ?2 RETURNING id, name, created_at", + ) + .bind(playlist.name) + .bind(playlist.id) + .fetch_one(&self.pool) + .await?; + + Ok(res) + } + + pub async fn delete(&self, id: i64) -> Result<(), AppError> { + sqlx::query("DELETE FROM playlists WHERE id = ?1") + .bind(id) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn find_by_id(&self, id: i64) -> Result, AppError> { + let res = sqlx::query_as::<_, Playlist>( + "SELECT id, name, created_at FROM playlists WHERE id = ?1", + ) + .bind(id) + .fetch_optional(&self.pool) + .await?; + + Ok(res) + } + + pub async fn list_all(&self) -> Result, AppError> { + let rows = sqlx::query_as::<_, Playlist>( + "SELECT id, name, created_at FROM playlists ORDER BY created_at DESC, id DESC", + ) + .fetch_all(&self.pool) + .await?; + + Ok(rows) + } + + pub async fn list_with_tracks(&self) -> Result, AppError> { + let playlists = self.list_all().await?; + let mut result = Vec::with_capacity(playlists.len()); + + for playlist in playlists { + let tracks = tracks_for_playlist(&self.pool, playlist.id).await?; + result.push(PlaylistWithTracks { playlist, tracks }); + } + + Ok(result) + } + + pub async fn get_with_tracks(&self, id: i64) -> Result, AppError> { + let Some(playlist) = self.find_by_id(id).await? else { + return Ok(None); + }; + + let tracks = tracks_for_playlist(&self.pool, playlist.id).await?; + Ok(Some(PlaylistWithTracks { playlist, tracks })) + } + + pub async fn add_track(&self, track: AddPlaylistTrack) -> Result { + let position = match track.position { + Some(position) => position, + None => next_position(&self.pool, track.playlist_id).await?, + }; + + let res = sqlx::query_as::<_, PlaylistTrack>( + "INSERT INTO playlist_tracks (playlist_id, track_id, position) + VALUES (?1, ?2, ?3) + ON CONFLICT(playlist_id, track_id) DO UPDATE SET position = excluded.position + RETURNING id, playlist_id, track_id, position", + ) + .bind(track.playlist_id) + .bind(track.track_id) + .bind(position) + .fetch_one(&self.pool) + .await?; + + Ok(res) + } + + pub async fn remove_track(&self, playlist_id: i64, track_id: i64) -> Result<(), AppError> { + sqlx::query("DELETE FROM playlist_tracks WHERE playlist_id = ?1 AND track_id = ?2") + .bind(playlist_id) + .bind(track_id) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn clear_tracks(&self, playlist_id: i64) -> Result<(), AppError> { + sqlx::query("DELETE FROM playlist_tracks WHERE playlist_id = ?1") + .bind(playlist_id) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn reorder_track( + &self, + playlist_id: i64, + track_id: i64, + position: i64, + ) -> Result<(), AppError> { + sqlx::query( + "UPDATE playlist_tracks SET position = ?1 WHERE playlist_id = ?2 AND track_id = ?3", + ) + .bind(position) + .bind(playlist_id) + .bind(track_id) + .execute(&self.pool) + .await?; + + Ok(()) + } +} + +async fn next_position(pool: &SqlitePool, playlist_id: i64) -> Result { + let row: (i64,) = sqlx::query_as( + "SELECT coalesce(max(position), -1) + 1 FROM playlist_tracks WHERE playlist_id = ?1", + ) + .bind(playlist_id) + .fetch_one(pool) + .await?; + + Ok(row.0) +} + +async fn tracks_for_playlist(pool: &SqlitePool, playlist_id: i64) -> Result, AppError> { + let rows = sqlx::query_as::<_, Track>( + "SELECT t.id, t.title, t.artist, t.album, t.duration, t.path, t.cover, t.file_size, t.play_count, t.last_played_at, t.created_at, t.updated_at + FROM tracks t + INNER JOIN playlist_tracks pt ON pt.track_id = t.id + WHERE pt.playlist_id = ?1 + ORDER BY pt.position ASC, pt.id ASC", + ) + .bind(playlist_id) + .fetch_all(pool) + .await?; + + Ok(rows) +} diff --git a/src-tauri/src/repositories/sqlite/recent_repository.rs b/src-tauri/src/repositories/sqlite/recent_repository.rs new file mode 100644 index 0000000..0941ff4 --- /dev/null +++ b/src-tauri/src/repositories/sqlite/recent_repository.rs @@ -0,0 +1,114 @@ +use sqlx::SqlitePool; + +use crate::errors::AppError; +use crate::models::{RecentPlayedWithTrack, Track}; + +#[derive(Clone)] +pub struct SqliteRecentRepository { + pool: SqlitePool, +} + +impl SqliteRecentRepository { + pub fn new(pool: SqlitePool) -> Self { + Self { pool } + } + + pub async fn list_with_tracks( + &self, + limit: i64, + offset: i64, + ) -> Result, AppError> { + let rows = sqlx::query_as::<_, RecentRow>( + "SELECT t.id, t.title, t.artist, t.album, t.duration, t.path, t.cover, t.file_size, t.play_count, t.last_played_at, t.created_at, t.updated_at, r.played_at + FROM recent_played r + INNER JOIN tracks t ON t.id = r.track_id + ORDER BY r.played_at DESC + LIMIT ?1 OFFSET ?2", + ) + .bind(limit) + .bind(offset) + .fetch_all(&self.pool) + .await?; + + Ok(rows + .into_iter() + .map(|row| RecentPlayedWithTrack { + track: Track { + id: row.id, + title: row.title, + artist: row.artist, + album: row.album, + duration: row.duration, + path: row.path, + cover: row.cover, + file_size: row.file_size, + play_count: row.play_count, + last_played_at: row.last_played_at, + created_at: row.created_at, + updated_at: row.updated_at, + }, + played_at: row.played_at, + }) + .collect()) + } + + pub async fn upsert(&self, track_id: i64, played_at: String) -> Result<(), AppError> { + sqlx::query( + "INSERT INTO recent_played (track_id, played_at) + VALUES (?1, ?2) + ON CONFLICT(track_id) DO UPDATE SET played_at = excluded.played_at", + ) + .bind(track_id) + .bind(played_at) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn clear(&self) -> Result<(), AppError> { + sqlx::query("DELETE FROM recent_played") + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn count(&self) -> Result { + let row: (i64,) = sqlx::query_as("SELECT count(*) FROM recent_played") + .fetch_one(&self.pool) + .await?; + + Ok(row.0) + } + + pub async fn remove_oldest(&self, keep: i64) -> Result<(), AppError> { + sqlx::query( + "DELETE FROM recent_played WHERE track_id NOT IN ( + SELECT track_id FROM recent_played ORDER BY played_at DESC LIMIT ?1 + )", + ) + .bind(keep) + .execute(&self.pool) + .await?; + + Ok(()) + } +} + +#[derive(sqlx::FromRow)] +struct RecentRow { + id: i64, + title: String, + artist: Option, + album: Option, + duration: i64, + path: String, + cover: Option, + file_size: Option, + play_count: i64, + last_played_at: Option, + created_at: String, + updated_at: String, + played_at: String, +} diff --git a/src-tauri/src/repositories/sqlite/settings_repository.rs b/src-tauri/src/repositories/sqlite/settings_repository.rs new file mode 100644 index 0000000..f1651f8 --- /dev/null +++ b/src-tauri/src/repositories/sqlite/settings_repository.rs @@ -0,0 +1,102 @@ +use sqlx::SqlitePool; + +use crate::errors::AppError; +use crate::models::SettingRow; + +#[derive(Clone)] +pub struct SqliteSettingsRepository { + pool: SqlitePool, +} + +impl SqliteSettingsRepository { + pub fn new(pool: SqlitePool) -> Self { + Self { pool } + } + + pub async fn get(&self) -> Result, AppError> { + let res = sqlx::query_as::<_, SettingRow>( + "SELECT + id, + theme, + volume, + scan_on_startup, + reduce_motion, + library_dirs, + use_album_artist_grouping, + plugin_dirs, + plugin_dev_mode, + plugin_scan_on_startup, + plugin_log_level, + created_at, + updated_at + FROM settings + WHERE id = 1", + ) + .fetch_optional(&self.pool) + .await?; + + Ok(res) + } + + pub async fn update(&self, row: SettingRow) -> Result<(), AppError> { + sqlx::query( + "INSERT INTO settings ( + id, + theme, + volume, + scan_on_startup, + reduce_motion, + library_dirs, + use_album_artist_grouping, + plugin_dirs, + plugin_dev_mode, + plugin_scan_on_startup, + plugin_log_level, + created_at, + updated_at + ) + VALUES ( + 1, + ?1, + ?2, + ?3, + ?4, + ?5, + ?6, + ?7, + ?8, + ?9, + ?10, + coalesce(nullif(?11, ''), datetime('now')), + datetime('now') + ) + ON CONFLICT(id) DO UPDATE SET + theme = excluded.theme, + volume = excluded.volume, + scan_on_startup = excluded.scan_on_startup, + reduce_motion = excluded.reduce_motion, + library_dirs = excluded.library_dirs, + use_album_artist_grouping = excluded.use_album_artist_grouping, + plugin_dirs = excluded.plugin_dirs, + plugin_dev_mode = excluded.plugin_dev_mode, + plugin_scan_on_startup = excluded.plugin_scan_on_startup, + plugin_log_level = excluded.plugin_log_level, + updated_at = datetime('now')", + ) + .bind(row.theme) + .bind(row.volume) + .bind(row.scan_on_startup) + .bind(row.reduce_motion) + .bind(row.library_dirs) + .bind(row.use_album_artist_grouping) + .bind(row.plugin_dirs) + .bind(row.plugin_dev_mode) + .bind(row.plugin_scan_on_startup) + .bind(row.plugin_log_level) + .bind(row.created_at) + .execute(&self.pool) + .await?; + + Ok(()) + } +} diff --git a/src-tauri/src/repositories/sqlite/track_repository.rs b/src-tauri/src/repositories/sqlite/track_repository.rs new file mode 100644 index 0000000..564aafb --- /dev/null +++ b/src-tauri/src/repositories/sqlite/track_repository.rs @@ -0,0 +1,274 @@ +use sqlx::{QueryBuilder, Sqlite, SqlitePool}; + +use crate::errors::AppError; +use crate::models::{NewTrack, SortDirection, Track, TrackSearchQuery, TrackSortBy, UpdateTrack}; + +#[derive(Clone)] +pub struct SqliteTrackRepository { + pool: SqlitePool, +} + +impl SqliteTrackRepository { + pub fn new(pool: SqlitePool) -> Self { + Self { pool } + } + + pub async fn create(&self, track: NewTrack) -> Result { + let res = sqlx::query_as::<_, Track>( + "INSERT INTO tracks (title, artist, album, duration, path, cover, file_size, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, datetime('now'), datetime('now')) + RETURNING id, title, artist, album, duration, path, cover, file_size, play_count, last_played_at, created_at, updated_at", + ) + .bind(track.title) + .bind(track.artist) + .bind(track.album) + .bind(track.duration) + .bind(track.path) + .bind(track.cover) + .bind(track.file_size) + .fetch_one(&self.pool) + .await?; + + Ok(res) + } + + pub async fn upsert_by_path(&self, track: NewTrack) -> Result { + let res = sqlx::query_as::<_, Track>( + "INSERT INTO tracks (title, artist, album, duration, path, cover, file_size, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, datetime('now'), datetime('now')) + ON CONFLICT(path) DO UPDATE SET + title = excluded.title, + artist = excluded.artist, + album = excluded.album, + duration = excluded.duration, + cover = excluded.cover, + file_size = excluded.file_size, + updated_at = datetime('now') + RETURNING id, title, artist, album, duration, path, cover, file_size, play_count, last_played_at, created_at, updated_at", + ) + .bind(track.title) + .bind(track.artist) + .bind(track.album) + .bind(track.duration) + .bind(track.path) + .bind(track.cover) + .bind(track.file_size) + .fetch_one(&self.pool) + .await?; + + Ok(res) + } + + pub async fn upsert_tracks(&self, tracks: Vec) -> Result, AppError> { + let mut tx = self.pool.begin().await?; + + let mut saved = Vec::with_capacity(tracks.len()); + + for track in tracks { + let res = sqlx::query_as::<_, Track>( + " + INSERT INTO tracks ( + title, + artist, + album, + duration, + path, + cover, + file_size, + created_at, + updated_at + ) + VALUES ( + ?1, ?2, ?3, + ?4, ?5, ?6, + ?7, + datetime('now'), + datetime('now') + ) + + ON CONFLICT(path) + DO UPDATE SET + title=excluded.title, + artist=excluded.artist, + album=excluded.album, + duration=excluded.duration, + cover=excluded.cover, + file_size=excluded.file_size, + updated_at=datetime('now') + + RETURNING + id, + title, + artist, + album, + duration, + path, + cover, + file_size, + play_count, + last_played_at, + created_at, + updated_at + ", + ) + .bind(track.title) + .bind(track.artist) + .bind(track.album) + .bind(track.duration) + .bind(track.path) + .bind(track.cover) + .bind(track.file_size) + .fetch_one(&mut *tx) + .await?; + + saved.push(res); + } + + tx.commit().await?; + + Ok(saved) + } + + pub async fn update(&self, track: UpdateTrack) -> Result { + let res = sqlx::query_as::<_, Track>( + "UPDATE tracks SET + title = ?1, + artist = ?2, + album = ?3, + duration = ?4, + path = ?5, + cover = ?6, + file_size = ?7, + updated_at = datetime('now') + WHERE id = ?8 + RETURNING id, title, artist, album, duration, path, cover, file_size, play_count, last_played_at, created_at, updated_at", + ) + .bind(track.title) + .bind(track.artist) + .bind(track.album) + .bind(track.duration) + .bind(track.path) + .bind(track.cover) + .bind(track.file_size) + .bind(track.id) + .fetch_one(&self.pool) + .await?; + + Ok(res) + } + + pub async fn delete(&self, id: i64) -> Result<(), AppError> { + sqlx::query("DELETE FROM tracks WHERE id = ?1") + .bind(id) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn delete_by_path(&self, path: &str) -> Result<(), AppError> { + sqlx::query("DELETE FROM tracks WHERE path = ?1") + .bind(path) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn find_by_id(&self, id: i64) -> Result, AppError> { + let res = sqlx::query_as::<_, Track>( + "SELECT id, title, artist, album, duration, path, cover, file_size, play_count, last_played_at, created_at, updated_at + FROM tracks WHERE id = ?1", + ) + .bind(id) + .fetch_optional(&self.pool) + .await?; + + Ok(res) + } + + pub async fn find_by_path(&self, path: &str) -> Result, AppError> { + let res = sqlx::query_as::<_, Track>( + "SELECT id, title, artist, album, duration, path, cover, file_size, play_count, last_played_at, created_at, updated_at + FROM tracks WHERE path = ?1", + ) + .bind(path) + .fetch_optional(&self.pool) + .await?; + + Ok(res) + } + + pub async fn list_all(&self) -> Result, AppError> { + let rows = sqlx::query_as::<_, Track>( + "SELECT id, title, artist, album, duration, path, cover, file_size, play_count, last_played_at, created_at, updated_at + FROM tracks ORDER BY title COLLATE NOCASE ASC", + ) + .fetch_all(&self.pool) + .await?; + + Ok(rows) + } + + pub async fn search(&self, query: TrackSearchQuery) -> Result, AppError> { + let mut builder = QueryBuilder::::new( + "SELECT id, title, artist, album, duration, path, cover, file_size, play_count, last_played_at, created_at, updated_at FROM tracks", + ); + + if let Some(keyword) = query + .keyword + .as_ref() + .map(|value| value.trim()) + .filter(|value| !value.is_empty()) + { + let pattern = format!("%{}%", keyword.to_lowercase()); + builder.push(" WHERE lower(title) LIKE "); + builder.push_bind(pattern.clone()); + builder.push(" OR lower(coalesce(artist, '')) LIKE "); + builder.push_bind(pattern.clone()); + builder.push(" OR lower(coalesce(album, '')) LIKE "); + builder.push_bind(pattern); + } + + builder.push(" ORDER BY "); + match query.sort_by.unwrap_or(TrackSortBy::Title) { + TrackSortBy::Title => builder.push("title COLLATE NOCASE"), + TrackSortBy::Artist => builder.push("artist COLLATE NOCASE"), + TrackSortBy::Album => builder.push("album COLLATE NOCASE"), + TrackSortBy::Duration => builder.push("duration"), + TrackSortBy::CreatedAt => builder.push("created_at"), + TrackSortBy::UpdatedAt => builder.push("updated_at"), + TrackSortBy::PlayCount => builder.push("play_count"), + TrackSortBy::LastPlayedAt => builder.push("last_played_at"), + }; + + match query.sort_direction.unwrap_or(SortDirection::Asc) { + SortDirection::Asc => builder.push(" ASC"), + SortDirection::Desc => builder.push(" DESC"), + }; + + builder.push(" LIMIT "); + builder.push_bind(query.limit.unwrap_or(100).clamp(1, 500)); + builder.push(" OFFSET "); + builder.push_bind(query.offset.unwrap_or(0).max(0)); + + let rows = builder + .build_query_as::() + .fetch_all(&self.pool) + .await?; + + Ok(rows) + } + + pub async fn increment_play_count(&self, id: i64, played_at: String) -> Result<(), AppError> { + sqlx::query( + "UPDATE tracks SET play_count = play_count + 1, last_played_at = ?1, updated_at = datetime('now') WHERE id = ?2", + ) + .bind(played_at) + .bind(id) + .execute(&self.pool) + .await?; + + Ok(()) + } +} diff --git a/src-tauri/src/services/media_control_service.rs b/src-tauri/src/services/media_control_service.rs new file mode 100644 index 0000000..e1db4dc --- /dev/null +++ b/src-tauri/src/services/media_control_service.rs @@ -0,0 +1,50 @@ +use crate::services::playback_service::PlaybackService; +use crate::state::playback_state::current_time_from_state; +use souvlaki::{MediaControlEvent, SeekDirection}; +use tauri::AppHandle; + +pub async fn handle_media_control_event(app_handle: AppHandle, event: MediaControlEvent) { + let service = PlaybackService::new(app_handle); + + match event { + MediaControlEvent::Play => { + let _ = service.resume().await; + } + MediaControlEvent::Pause => { + let _ = service.pause().await; + } + MediaControlEvent::Toggle => { + let _ = service.toggle().await; + } + MediaControlEvent::Next => { + let _ = service.next().await; + } + MediaControlEvent::Previous => { + let _ = service.previous().await; + } + MediaControlEvent::Stop => { + let _ = service.stop().await; + } + MediaControlEvent::SetPosition(position) => { + let _ = service.seek(position.0.as_secs_f64()).await; + } + MediaControlEvent::Seek(direction) => { + let current_time = current_time_from_state(); + let offset = match direction { + SeekDirection::Forward => 10.0, + SeekDirection::Backward => -10.0, + }; + let _ = service.seek((current_time + offset).max(0.0)).await; + } + MediaControlEvent::SeekBy(direction, duration) => { + let current_time = current_time_from_state(); + let offset = duration.as_secs_f64(); + let next_time = match direction { + SeekDirection::Forward => current_time + offset, + SeekDirection::Backward => current_time - offset, + }; + let _ = service.seek(next_time.max(0.0)).await; + } + _ => {} + } +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs new file mode 100644 index 0000000..89ac128 --- /dev/null +++ b/src-tauri/src/services/mod.rs @@ -0,0 +1,12 @@ +pub mod media_control_service; +pub mod playback_service; +pub mod playlist_service; +pub mod plugin; +pub mod recent_service; +pub mod settings_service; +pub mod track_service; + +pub use playlist_service::PlaylistService; +pub use recent_service::RecentService; +pub use settings_service::SettingsService; +pub use track_service::TrackService; diff --git a/src-tauri/src/services/playback_service.rs b/src-tauri/src/services/playback_service.rs new file mode 100644 index 0000000..f75d4d4 --- /dev/null +++ b/src-tauri/src/services/playback_service.rs @@ -0,0 +1,291 @@ +use crate::audio::{lock_audio_state, AudioEngine, WebAudioEngine}; +use crate::errors::AppError; +use crate::events::{ + AppEvent, EventBus, PlaybackProgressPayload, PlaybackStatePayload, TrackStartedPayload, +}; +use crate::media_controls::{update_media_controls_metadata, update_media_controls_playback}; +use crate::models::Track; +use crate::state::app_state::AppState; +use crate::state::playback_state::{ + current_playback_snapshot, metadata_from_track, should_advance_track, with_audio_state, +}; +use std::path::Path; +use std::time::Duration; +use tauri::{AppHandle, Manager}; + +fn emit_and_dispatch(app_handle: &AppHandle, event: AppEvent) -> Result<(), AppError> { + EventBus::emit(app_handle, event.clone())?; + if let Some(state) = app_handle.try_state::() { + state.plugins.dispatch_event(&event); + } + Ok(()) +} + +pub struct PlaybackService { + app_handle: AppHandle, +} + +impl PlaybackService { + pub fn new(app_handle: AppHandle) -> Self { + Self { app_handle } + } + + pub fn play_track(&self, track: Track, index: Option) -> Result<(), AppError> { + let file_path = Path::new(&track.path); + + if !file_path.exists() { + return Err("音频文件不存在或已被移动".into()); + } + + let mut state = lock_audio_state()?; + + let volume = state.volume; + let pan = state.pan; + state.engine = None; + + // 取共享的音频处理器 registry(来自 PluginManager),喂给渲染图中的 + // ExternalProcessorNode。AppState 未就绪时退化为空 registry — 此时 + // 插件尚未注册,透传行为不受影响。 + let audio_processor_registry = self + .app_handle + .try_state::() + .map(|s| s.plugins.audio_processor_registry()) + .unwrap_or_else(|| std::sync::Arc::new(crate::services::plugin::AudioProcessorRegistry::new())); + + let mut engine = match WebAudioEngine::new(file_path, volume, pan, audio_processor_registry) { + Ok(eng) => eng, + Err(e) => { + state.current_track_id = None; + state.playing = false; + return Err(e); + } + }; + + if let Err(e) = engine.play() { + state.current_track_id = None; + state.playing = false; + return Err(e); + } + + state.engine = Some(Box::new(engine)); + state.current_track_id = Some(track.id); + + if let Some(next_index) = index { + state.playback_queue.current_index = Some(next_index); + } + + state.playing = true; + let target_index = index.or(state.playback_queue.current_index).unwrap_or(0); + + drop(state); + + Self::emit_track_started(&self.app_handle, track, target_index)?; + + Ok(()) + } + + pub fn play_queue_index(&self, index: usize) -> Result<(), AppError> { + let track = with_audio_state(|state| state.playback_queue.require_track(index))??; + self.play_track(track, Some(index)) + } + + pub async fn next(&self) -> Result<(), AppError> { + let (track, index) = with_audio_state(|state| { + let index = state + .playback_queue + .move_next() + .ok_or_else(|| AppError::from("Queue is empty"))?; + + let track = state.playback_queue.require_track(index)?; + + Ok::<(Track, usize), AppError>((track, index)) + })??; + + self.play_track(track, Some(index)) + } + + pub async fn previous(&self) -> Result<(), AppError> { + let (track, index) = with_audio_state(|state| { + let index = state + .playback_queue + .move_previous() + .ok_or_else(|| AppError::from("Queue is empty"))?; + + let track = state.playback_queue.require_track(index)?; + + Ok::<(Track, usize), AppError>((track, index)) + })??; + + self.play_track(track, Some(index)) + } + + pub async fn resume(&self) -> Result<(), AppError> { + let current_time = with_audio_state(|state| { + let res = state + .with_engine(|engine| { + engine.play()?; + Ok(engine.current_time()) + }) + .and_then(|inner| inner); + + if res.is_ok() { + state.playing = true; + } + res + })??; + + Self::sync_playback_state(&self.app_handle, true, current_time)?; + Ok(()) + } + + pub async fn pause(&self) -> Result<(), AppError> { + let current_time = with_audio_state(|state| { + let res = state.with_engine(|engine| { + engine.pause(); + engine.current_time() + }); + if res.is_ok() { + state.playing = false; + } + res + })??; + + Self::sync_playback_state(&self.app_handle, false, current_time)?; + Ok(()) + } + + pub async fn toggle(&self) -> Result { + let (playing, current_time) = with_audio_state(|state| { + let res = state + .with_engine(|engine| { + if engine.paused() { + engine.play()?; + Ok((true, engine.current_time())) + } else { + engine.pause(); + Ok((false, engine.current_time())) + } + }) + .and_then(|i| i); + if let Ok((p, _)) = res { + state.playing = p; + } + res + })??; + + Self::sync_playback_state(&self.app_handle, playing, current_time)?; + Ok(playing) + } + + pub async fn stop(&self) -> Result<(), AppError> { + with_audio_state(|state| { + let _ = state.with_engine(|engine| { + engine.pause(); + engine.seek(0.0); + }); + state.playing = false; + })?; + + Self::sync_playback_state(&self.app_handle, false, 0.0)?; + Ok(()) + } + + pub async fn seek(&self, time: f64) -> Result<(), AppError> { + let playing = with_audio_state(|state| { + let res = state.with_engine(|engine| { + engine.seek(time); + !engine.paused() + }); + if let Ok(p) = res { + state.playing = p; + } + res + })??; + + Self::sync_playback_state(&self.app_handle, playing, time)?; + Ok(()) + } + + pub fn emit_track_started( + app_handle: &AppHandle, + track: Track, + index: usize, + ) -> Result<(), AppError> { + update_media_controls_metadata(metadata_from_track(&track)); + + emit_and_dispatch( + app_handle, + AppEvent::TrackStarted(TrackStartedPayload { track, index }), + ) + } + + pub fn sync_playback_state( + app_handle: &AppHandle, + playing: bool, + current_time: f64, + ) -> Result<(), AppError> { + let payload = PlaybackStatePayload { + playing, + current_time, + }; + + update_media_controls_playback(payload.clone()); + + emit_and_dispatch(app_handle, AppEvent::PlaybackStateChanged(payload)) + } + + pub fn emit_progress(app_handle: &AppHandle) -> Result<(), AppError> { + if let Ok(snapshot) = current_playback_snapshot() { + emit_and_dispatch( + app_handle, + AppEvent::PlaybackProgress(PlaybackProgressPayload { + current_time: snapshot.current_time, + }), + )?; + } + Ok(()) + } + + pub fn spawn_playback_progress_task(app_handle: AppHandle) { + tauri::async_runtime::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_millis(500)); + + loop { + interval.tick().await; + + let _ = Self::emit_progress(&app_handle); + + if should_advance_track() { + let _ = PlaybackService::new(app_handle.clone()).next().await; + } + } + }); + } + + pub(crate) async fn remove_track_from_queue(&self, track_id: i64) -> Result<(), AppError> { + let result = { + let mut state = lock_audio_state()?; + state.playback_queue.remove_track(track_id) + }; + + let Some(remove_result) = result else { + return Ok(()); + }; + + if remove_result.should_stop { + self.stop().await?; + } else if let Some(index) = remove_result.play_index { + self.play_queue_index(index)?; + } + + Self::emit_queue_changed(&self.app_handle)?; + + Ok(()) + } + + pub(crate) fn emit_queue_changed(app_handle: &tauri::AppHandle) -> Result<(), AppError> { + let queue = with_audio_state(|state| state.playback_queue.clone())?; + + emit_and_dispatch(app_handle, AppEvent::QueueChanged(queue)) + } +} diff --git a/src-tauri/src/services/playlist_service.rs b/src-tauri/src/services/playlist_service.rs new file mode 100644 index 0000000..8a640f8 --- /dev/null +++ b/src-tauri/src/services/playlist_service.rs @@ -0,0 +1,72 @@ +use std::sync::Arc; + +use crate::errors::AppError; +use crate::models::{ + AddPlaylistTrack, NewPlaylist, Playlist, PlaylistTrack, PlaylistWithTracks, RenamePlaylist, +}; +use crate::repositories::sqlite::playlist_repository::SqlitePlaylistRepository; + +#[derive(Clone)] +pub struct PlaylistService { + playlists: Arc, +} + +impl PlaylistService { + pub fn new(playlists: Arc) -> Self { + Self { playlists } + } + + pub async fn create_playlist(&self, playlist: NewPlaylist) -> Result { + self.playlists.create(playlist).await + } + + pub async fn rename_playlist(&self, playlist: RenamePlaylist) -> Result { + self.playlists.rename(playlist).await + } + + pub async fn delete_playlist(&self, id: i64) -> Result<(), AppError> { + self.playlists.delete(id).await + } + + pub async fn get_playlist(&self, id: i64) -> Result, AppError> { + self.playlists.find_by_id(id).await + } + + pub async fn list_playlists(&self) -> Result, AppError> { + self.playlists.list_all().await + } + + pub async fn list_playlists_with_tracks(&self) -> Result, AppError> { + self.playlists.list_with_tracks().await + } + + pub async fn get_playlist_with_tracks( + &self, + id: i64, + ) -> Result, AppError> { + self.playlists.get_with_tracks(id).await + } + + pub async fn add_track(&self, track: AddPlaylistTrack) -> Result { + self.playlists.add_track(track).await + } + + pub async fn remove_track(&self, playlist_id: i64, track_id: i64) -> Result<(), AppError> { + self.playlists.remove_track(playlist_id, track_id).await + } + + pub async fn clear_tracks(&self, playlist_id: i64) -> Result<(), AppError> { + self.playlists.clear_tracks(playlist_id).await + } + + pub async fn reorder_track( + &self, + playlist_id: i64, + track_id: i64, + position: i64, + ) -> Result<(), AppError> { + self.playlists + .reorder_track(playlist_id, track_id, position) + .await + } +} diff --git a/src-tauri/src/services/plugin/adapters/audio_adapter.rs b/src-tauri/src/services/plugin/adapters/audio_adapter.rs new file mode 100644 index 0000000..114aa35 --- /dev/null +++ b/src-tauri/src/services/plugin/adapters/audio_adapter.rs @@ -0,0 +1,99 @@ +use std::sync::Arc; + +use plugin_sdk::api::AudioProcessingApi; +use plugin_sdk::errors::PluginError; +use plugin_sdk::permissions::PluginPermission; + +use crate::services::plugin::capability::permission_checker::PermissionChecker; +use crate::state::playback_state::with_audio_state; + +/// EQ 频段数量(与 trait 的 [f64; 10] 对应) +const EQ_BAND_COUNT: usize = 10; + +/// 将 AppError 转换为 PluginError,规则与 player_adapter 一致。 +fn sdk_err(e: crate::errors::AppError) -> PluginError { + match e { + crate::errors::AppError::Plugin(msg) => PluginError::Plugin(msg), + crate::errors::AppError::PluginPermissionDenied { plugin_id, permission } => { + PluginError::PermissionDenied { plugin_id, permission } + } + crate::errors::AppError::Io(msg) => PluginError::Io(msg), + other => PluginError::Plugin(other.to_string()), + } +} + +pub struct AudioAdapter { + plugin_id: String, + checker: Arc, +} + +impl AudioAdapter { + pub fn new(plugin_id: String, checker: Arc) -> Self { + Self { plugin_id, checker } + } +} + +impl AudioProcessingApi for AudioAdapter { + fn set_band_gain(&self, band: usize, gain_db: f64) -> Result<(), PluginError> { + self.checker + .require(&self.plugin_id, PluginPermission::Audio) + .map_err(sdk_err)?; + if band >= EQ_BAND_COUNT { + return Err(PluginError::Plugin("EQ band index out of range".into())); + } + with_audio_state(|state| -> Result<(), crate::errors::AppError> { + state.eq_bands[band] = gain_db; + if let Some(ref mut engine) = state.engine { + engine.set_eq_band_gain(band, gain_db)?; + } + Ok(()) + }) + .and_then(|inner| inner) + .map_err(sdk_err) + } + + fn apply_preset(&self, gains: [f64; 10]) -> Result<(), PluginError> { + self.checker + .require(&self.plugin_id, PluginPermission::Audio) + .map_err(sdk_err)?; + with_audio_state(|state| -> Result<(), crate::errors::AppError> { + state.eq_bands = gains; + if let Some(ref mut engine) = state.engine { + engine.apply_eq_preset(&gains)?; + } + Ok(()) + }) + .and_then(|inner| inner) + .map_err(sdk_err) + } + + fn get_bands(&self) -> Result<[f64; 10], PluginError> { + self.checker + .require(&self.plugin_id, PluginPermission::Audio) + .map_err(sdk_err)?; + // state.eq_bands 在所有写操作中与引擎同步,直接读取即可 + with_audio_state(|state| state.eq_bands).map_err(sdk_err) + } + + fn set_enabled(&self, enabled: bool) -> Result<(), PluginError> { + self.checker + .require(&self.plugin_id, PluginPermission::Audio) + .map_err(sdk_err)?; + with_audio_state(|state| -> Result<(), crate::errors::AppError> { + state.eq_enabled = enabled; + if let Some(ref mut engine) = state.engine { + engine.set_eq_enabled(enabled)?; + } + Ok(()) + }) + .and_then(|inner| inner) + .map_err(sdk_err) + } + + fn is_enabled(&self) -> Result { + self.checker + .require(&self.plugin_id, PluginPermission::Audio) + .map_err(sdk_err)?; + with_audio_state(|state| state.eq_enabled).map_err(sdk_err) + } +} diff --git a/src-tauri/src/services/plugin/adapters/library_adapter.rs b/src-tauri/src/services/plugin/adapters/library_adapter.rs new file mode 100644 index 0000000..6c6b8fe --- /dev/null +++ b/src-tauri/src/services/plugin/adapters/library_adapter.rs @@ -0,0 +1,49 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use plugin_sdk::api::LibraryApi; +use plugin_sdk::errors::PluginError; +use plugin_sdk::permissions::PluginPermission; +use crate::services::plugin::capability::permission_checker::PermissionChecker; +use crate::services::track_service::TrackService; + +fn sdk_err(e: crate::errors::AppError) -> PluginError { + match e { + crate::errors::AppError::Plugin(msg) => PluginError::Plugin(msg), + crate::errors::AppError::PluginPermissionDenied { plugin_id, permission } => { + PluginError::PermissionDenied { plugin_id, permission } + } + crate::errors::AppError::Io(msg) => PluginError::Io(msg), + other => PluginError::Plugin(other.to_string()), + } +} + +pub struct LibraryAdapter { + plugin_id: String, + checker: Arc, + track_service: Arc, +} + +impl LibraryAdapter { + pub fn new(plugin_id: String, checker: Arc, track_service: Arc) -> Self { + Self { plugin_id, checker, track_service } + } +} + +impl LibraryApi for LibraryAdapter { + fn get_track_path(&self, track_id: i64) -> Result, PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::LibraryRead).map_err(sdk_err)?; + let track = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.track_service.get_track(track_id)) + }).map_err(sdk_err)?; + Ok(track.map(|t| PathBuf::from(t.path))) + } + + fn exists(&self, track_id: i64) -> Result { + self.checker.require(&self.plugin_id, PluginPermission::LibraryRead).map_err(sdk_err)?; + let track = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.track_service.get_track(track_id)) + }).map_err(sdk_err)?; + Ok(track.is_some()) + } +} diff --git a/src-tauri/src/services/plugin/adapters/mod.rs b/src-tauri/src/services/plugin/adapters/mod.rs new file mode 100644 index 0000000..21fee2c --- /dev/null +++ b/src-tauri/src/services/plugin/adapters/mod.rs @@ -0,0 +1,13 @@ +pub mod audio_adapter; +pub mod library_adapter; +pub mod player_adapter; +pub mod plugin_settings_adapter; +pub mod queue_adapter; +pub mod settings_adapter; + +pub use audio_adapter::AudioAdapter; +pub use library_adapter::LibraryAdapter; +pub use player_adapter::PlayerAdapter; +pub use plugin_settings_adapter::PluginSettingsAdapter; +pub use queue_adapter::QueueAdapter; +pub use settings_adapter::SettingsAdapter; diff --git a/src-tauri/src/services/plugin/adapters/player_adapter.rs b/src-tauri/src/services/plugin/adapters/player_adapter.rs new file mode 100644 index 0000000..4b5688a --- /dev/null +++ b/src-tauri/src/services/plugin/adapters/player_adapter.rs @@ -0,0 +1,71 @@ +use std::sync::Arc; + +use crate::services::playback_service::PlaybackService; +use plugin_sdk::api::PlayerApi; +use plugin_sdk::errors::PluginError; +use plugin_sdk::permissions::PluginPermission; +use crate::services::plugin::capability::permission_checker::PermissionChecker; +use crate::state::playback_state::with_audio_state; + +fn sdk_err(e: crate::errors::AppError) -> PluginError { + match e { + crate::errors::AppError::Plugin(msg) => PluginError::Plugin(msg), + crate::errors::AppError::PluginPermissionDenied { plugin_id, permission } => { + PluginError::PermissionDenied { plugin_id, permission } + } + crate::errors::AppError::Io(msg) => PluginError::Io(msg), + other => PluginError::Plugin(other.to_string()), + } +} + +pub struct PlayerAdapter { + plugin_id: String, + checker: Arc, + playback_service: Arc, +} + +impl PlayerAdapter { + pub fn new( + plugin_id: String, + checker: Arc, + playback_service: Arc, + ) -> Self { + Self { plugin_id, checker, playback_service } + } +} + +impl PlayerApi for PlayerAdapter { + fn play(&self) -> Result<(), PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::PlayerControl).map_err(sdk_err)?; + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.playback_service.resume()) + }).map_err(sdk_err)?; + Ok(()) + } + + fn pause(&self) -> Result<(), PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::PlayerControl).map_err(sdk_err)?; + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.playback_service.pause()) + }).map_err(sdk_err) + } + + fn next(&self) -> Result<(), PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::PlayerControl).map_err(sdk_err)?; + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.playback_service.next()) + }).map_err(sdk_err) + } + + fn previous(&self) -> Result<(), PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::PlayerControl).map_err(sdk_err)?; + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.playback_service.previous()) + }).map_err(sdk_err) + } + + fn current_track_id(&self) -> Result, PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::PlayerRead).map_err(sdk_err)?; + with_audio_state(|state| state.current_track_id).map_err(sdk_err) + } +} diff --git a/src-tauri/src/services/plugin/adapters/plugin_settings_adapter.rs b/src-tauri/src/services/plugin/adapters/plugin_settings_adapter.rs new file mode 100644 index 0000000..6107e88 --- /dev/null +++ b/src-tauri/src/services/plugin/adapters/plugin_settings_adapter.rs @@ -0,0 +1,28 @@ +use std::sync::Arc; + +use plugin_sdk::api::PluginSettingsApi; +use plugin_sdk::errors::PluginError; +use plugin_sdk::settings::SettingValue; +use crate::services::plugin::settings::settings_registry::SettingsRegistry; + +pub struct PluginSettingsAdapter { + plugin_id: String, + registry: Arc, +} + +impl PluginSettingsAdapter { + pub fn new(plugin_id: String, registry: Arc) -> Self { + Self { plugin_id, registry } + } +} + +impl PluginSettingsApi for PluginSettingsAdapter { + fn get_setting(&self, key: &str) -> Option { + self.registry.get_setting(&self.plugin_id, key) + } + + fn set_setting(&self, key: &str, value: SettingValue) -> Result<(), PluginError> { + self.registry.update_setting(&self.plugin_id, key, value) + .map_err(|e| PluginError::Plugin(e.to_string())) + } +} diff --git a/src-tauri/src/services/plugin/adapters/queue_adapter.rs b/src-tauri/src/services/plugin/adapters/queue_adapter.rs new file mode 100644 index 0000000..cb8d30c --- /dev/null +++ b/src-tauri/src/services/plugin/adapters/queue_adapter.rs @@ -0,0 +1,48 @@ +use std::sync::Arc; + +use plugin_sdk::api::QueueApi; +use plugin_sdk::errors::PluginError; +use plugin_sdk::permissions::PluginPermission; +use crate::services::plugin::capability::permission_checker::PermissionChecker; +use crate::state::playback_state::with_audio_state; + +fn sdk_err(e: crate::errors::AppError) -> PluginError { + match e { + crate::errors::AppError::Plugin(msg) => PluginError::Plugin(msg), + crate::errors::AppError::PluginPermissionDenied { plugin_id, permission } => { + PluginError::PermissionDenied { plugin_id, permission } + } + crate::errors::AppError::Io(msg) => PluginError::Io(msg), + other => PluginError::Plugin(other.to_string()), + } +} + +pub struct QueueAdapter { + plugin_id: String, + checker: Arc, +} + +impl QueueAdapter { + pub fn new(plugin_id: String, checker: Arc) -> Self { + Self { plugin_id, checker } + } +} + +impl QueueApi for QueueAdapter { + fn current_queue(&self) -> Result, PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::QueueRead).map_err(sdk_err)?; + with_audio_state(|state| state.playback_queue.tracks.iter().map(|t| t.id).collect()).map_err(sdk_err) + } + + fn remove_track(&self, track_id: i64) -> Result<(), PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::QueueWrite).map_err(sdk_err)?; + with_audio_state(|state| { state.playback_queue.remove_track(track_id); }).map_err(sdk_err)?; + Ok(()) + } + + fn clear(&self) -> Result<(), PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::QueueWrite).map_err(sdk_err)?; + with_audio_state(|state| { state.playback_queue.clear(); }).map_err(sdk_err)?; + Ok(()) + } +} diff --git a/src-tauri/src/services/plugin/adapters/settings_adapter.rs b/src-tauri/src/services/plugin/adapters/settings_adapter.rs new file mode 100644 index 0000000..6dbd565 --- /dev/null +++ b/src-tauri/src/services/plugin/adapters/settings_adapter.rs @@ -0,0 +1,54 @@ +use std::sync::Arc; + +use crate::models::ThemeMode; +use plugin_sdk::api::SettingsApi; +use plugin_sdk::errors::PluginError; +use plugin_sdk::permissions::PluginPermission; +use crate::services::plugin::capability::permission_checker::PermissionChecker; +use crate::services::settings_service::SettingsService; + +fn sdk_err(e: crate::errors::AppError) -> PluginError { + match e { + crate::errors::AppError::Plugin(msg) => PluginError::Plugin(msg), + crate::errors::AppError::PluginPermissionDenied { plugin_id, permission } => { + PluginError::PermissionDenied { plugin_id, permission } + } + crate::errors::AppError::Io(msg) => PluginError::Io(msg), + other => PluginError::Plugin(other.to_string()), + } +} + +pub struct SettingsAdapter { + plugin_id: String, + checker: Arc, + settings_service: Arc, +} + +impl SettingsAdapter { + pub fn new(plugin_id: String, checker: Arc, settings_service: Arc) -> Self { + Self { plugin_id, checker, settings_service } + } +} + +impl SettingsApi for SettingsAdapter { + fn theme(&self) -> Result { + self.checker.require(&self.plugin_id, PluginPermission::SettingsRead).map_err(sdk_err)?; + let settings = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.settings_service.get_settings()) + }).map_err(sdk_err)?; + Ok(settings.theme.as_str().to_string()) + } + + fn set_theme(&self, theme: String) -> Result<(), PluginError> { + self.checker.require(&self.plugin_id, PluginPermission::SettingsWrite).map_err(sdk_err)?; + let theme_mode = ThemeMode::try_from(theme).map_err(|e| PluginError::Plugin(e.to_string()))?; + let mut settings = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.settings_service.get_settings()) + }).map_err(sdk_err)?; + settings.theme = theme_mode; + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.settings_service.update_settings(settings)) + }).map_err(sdk_err)?; + Ok(()) + } +} diff --git a/src-tauri/src/services/plugin/asset/mod.rs b/src-tauri/src/services/plugin/asset/mod.rs new file mode 100644 index 0000000..5453ae6 --- /dev/null +++ b/src-tauri/src/services/plugin/asset/mod.rs @@ -0,0 +1 @@ +pub mod scheme; diff --git a/src-tauri/src/services/plugin/asset/scheme.rs b/src-tauri/src/services/plugin/asset/scheme.rs new file mode 100644 index 0000000..0558c27 --- /dev/null +++ b/src-tauri/src/services/plugin/asset/scheme.rs @@ -0,0 +1,167 @@ +//! `plugin://` URI scheme handler. +//! +//! Exposes plugin asset files to sandboxed iframes via +//! `plugin:///`. The special `__host__` plugin id +//! serves the compile-time embedded host SDK so plugin HTML can reference it +//! without a runtime file dependency. + +use std::path::{Component, Path, PathBuf}; + +use tauri::http::{Request, Response, StatusCode}; +use tauri::{AppHandle, Manager, Runtime, UriSchemeContext}; + +/// Compile-time embedded host SDK. Path is relative to this source file: +/// `scheme.rs` lives at `src-tauri/src/services/plugin/asset/scheme.rs`, so +/// five `..` segments reach the project root where `static/` lives. +const HOST_SDK_JS: &str = + include_str!("../../../../../static/plugin-sdk/plugin-host-sdk.js"); + +/// Tauri `register_uri_scheme_protocol` handler for the `plugin` scheme. +pub fn handle( + ctx: UriSchemeContext<'_, R>, + request: Request>, +) -> Response> { + let app = ctx.app_handle(); + let uri = request.uri(); + let host = uri.host().unwrap_or(""); + let path = uri.path().trim_start_matches('/'); + + // Special segment: `plugin://__host__/plugin-sdk/plugin-host-sdk.js` + // serves the embedded host SDK without touching the filesystem. + if host == "__host__" && path == "plugin-sdk/plugin-host-sdk.js" { + return Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/javascript; charset=utf-8") + .body(HOST_SDK_JS.as_bytes().to_vec()) + .unwrap(); + } + + // Validate the plugin id: must be non-empty and contain no path + // separators or traversal segments. + let plugin_id = match host { + "" => return not_found("missing plugin id"), + id if id.contains('/') || id.contains("..") || id.contains('\0') => { + return forbidden("invalid plugin id") + } + id => id, + }; + + // Cross-plugin isolation: when a sub-resource request carries a + // `Referer` whose plugin id differs from the requested plugin id, + // reject with 403. Entry HTML loads have no Referer and are allowed. + if let Some(referer) = request.headers().get("referer").and_then(|v| v.to_str().ok()) { + if let Some(referer_plugin_id) = parse_plugin_referer(referer) { + if referer_plugin_id != plugin_id { + return forbidden("cross-plugin access denied"); + } + } + } + + // Path safety: reject absolute paths, traversal, and NUL bytes up front. + if path.is_empty() || path.starts_with('/') || path.contains("..") || path.contains('\0') { + return forbidden("invalid path"); + } + + // Component-level check: only `Normal` components are allowed. This + // rejects `RootDir`, `ParentDir`, `Prefix` (Windows drive), and + // `CurDir` (`.`). + let rel = Path::new(path); + for comp in rel.components() { + match comp { + Component::Normal(_) => {} + _ => return forbidden("invalid path component"), + } + } + + // Locate the plugin's resource root directory. + let root = match resolve_plugin_root(app, plugin_id) { + Some(p) => p, + None => return not_found("plugin root not found"), + }; + + let file_path = root.join(rel); + + // Canonicalize both the file and the root to defend against symlinks or + // other filesystem tricks that could escape the plugin sandbox. + let canonical = match file_path.canonicalize() { + Ok(p) => p, + Err(_) => return not_found("file not found"), + }; + let canonical_root = match root.canonicalize() { + Ok(p) => p, + Err(_) => return not_found("plugin root invalid"), + }; + if !canonical.starts_with(&canonical_root) { + return forbidden("path escapes plugin root"); + } + + match std::fs::read(&canonical) { + Ok(bytes) => Response::builder() + .status(StatusCode::OK) + .header("Content-Type", content_type_for(&canonical)) + .body(bytes) + .unwrap(), + Err(_) => not_found("file read error"), + } +} + +/// Parse the plugin id from a `Referer` header value of the form +/// `plugin:///...`. Returns `None` for non-`plugin:` URIs. +fn parse_plugin_referer(referer: &str) -> Option<&str> { + let stripped = referer.strip_prefix("plugin://")?; + let plugin_id = stripped.split('/').next()?; + if plugin_id.is_empty() || plugin_id.contains("..") || plugin_id.contains('\0') { + return None; + } + Some(plugin_id) +} + +/// Resolve a plugin's resource root by searching the same candidate +/// directories used by `PluginManager::load_packaged_plugins`: app data, +/// the Tauri resource dir, and the exe's parent directory. +fn resolve_plugin_root(app: &AppHandle, plugin_id: &str) -> Option { + let mut candidates = Vec::new(); + + if let Ok(app_data) = app.path().app_data_dir() { + candidates.push(app_data.join("plugins").join(plugin_id)); + } + + if let Ok(resource_dir) = app.path().resource_dir() { + candidates.push(resource_dir.join("plugins").join(plugin_id)); + } + + if let Ok(exe) = std::env::current_exe() { + if let Some(parent) = exe.parent() { + candidates.push(parent.join(plugin_id)); + } + } + + candidates.into_iter().find(|p| p.exists()) +} + +fn content_type_for(path: &Path) -> &'static str { + match path.extension().and_then(|e| e.to_str()) { + Some("html") => "text/html; charset=utf-8", + Some("js") => "application/javascript; charset=utf-8", + Some("css") => "text/css; charset=utf-8", + Some("json") => "application/json; charset=utf-8", + Some("png") => "image/png", + Some("jpg") | Some("jpeg") => "image/jpeg", + Some("svg") => "image/svg+xml", + _ => "application/octet-stream", + } +} + +fn not_found(msg: &str) -> Response> { + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(msg.as_bytes().to_vec()) + .unwrap() +} + +fn forbidden(msg: &str) -> Response> { + Response::builder() + .status(StatusCode::FORBIDDEN) + .body(msg.as_bytes().to_vec()) + .unwrap() +} diff --git a/src-tauri/src/services/plugin/audio_processor_registry.rs b/src-tauri/src/services/plugin/audio_processor_registry.rs new file mode 100644 index 0000000..2549405 --- /dev/null +++ b/src-tauri/src/services/plugin/audio_processor_registry.rs @@ -0,0 +1,141 @@ +//! Audio processor registry shared between the plugin manager and the +//! audio render graph. +//! +//! Packaged plugins that export the `rem_audio_processor` symbol register +//! an [`AudioProcessorApi`] here at activation time. The +//! [`crate::audio::web_audio::ExternalProcessorNode`] (an +//! `AudioWorkletProcessor`) holds an `Arc` and, on +//! every render quantum, iterates the active processors, calling each one's +//! `process` FFI callback with interleaved PCM. +//! +//! # Design notes +//! +//! - `AudioProcessorApi` is `Copy` (composed of `Option`), +//! so it is stored by value and returned by value — no lifetime juggling. +//! - `sample_rate` / `channels` are stored as atomics so the render thread +//! (worklet) and the control thread (plugin manager) can read them without +//! locking. They are populated by `WebAudioEngine` when it is constructed. +//! - The raw `PluginHandle` is currently stored as `null_mut()`: exposing +//! the FFI handle through the `Plugin` trait requires SDK changes that are +//! out of scope for this task (Task 8 forbade `ffi.rs` edits). The registry +//! plumbing is in place so a future trait extension can flow the real +//! handle through. + +use std::collections::HashMap; +use std::ffi::c_void; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::{Mutex, RwLock}; + +use plugin_sdk::ffi::AudioProcessorApi; + +pub struct AudioProcessorRegistry { + /// Active audio processors keyed by plugin id. `AudioProcessorApi` is + /// `Copy`, so by-value storage keeps `iter_active` allocation-free per + /// entry. + processors: RwLock>, + /// Per-plugin FFI handle (`*mut c_void`) to hand back to `process` / + /// `reset` callbacks. Stored separately so `iter_active` only needs a + /// short-lived `Mutex` guard to fetch the handle map. + handles: Mutex>, + /// Output sample rate (Hz). Set by `WebAudioEngine` at construction. + sample_rate: AtomicU32, + /// Output channel count. Set by `WebAudioEngine` at construction. + channels: AtomicU32, +} + +impl Default for AudioProcessorRegistry { + fn default() -> Self { + Self::new() + } +} + +impl AudioProcessorRegistry { + pub fn new() -> Self { + Self { + processors: RwLock::new(HashMap::new()), + handles: Mutex::new(HashMap::new()), + sample_rate: AtomicU32::new(44100), + channels: AtomicU32::new(2), + } + } + + /// Update the audio format the render graph is running at. Called by + /// `WebAudioEngine` when it is constructed so plugin `init` callbacks + /// receive accurate values. + pub fn set_audio_format(&self, sample_rate: u32, channels: u32) { + self.sample_rate.store(sample_rate, Ordering::Relaxed); + self.channels.store(channels, Ordering::Relaxed); + } + + pub fn sample_rate(&self) -> u32 { + self.sample_rate.load(Ordering::Relaxed) + } + + pub fn channels(&self) -> u32 { + self.channels.load(Ordering::Relaxed) + } + + /// Register an activated plugin's audio processor. + /// + /// `handle` is the FFI `PluginHandle` to pass to `process` / `reset`. + /// Currently `null_mut()` until the SDK exposes the raw handle. + pub fn register(&self, plugin_id: String, api: AudioProcessorApi, handle: *mut c_void) { + if let Ok(mut lock) = self.processors.write() { + lock.insert(plugin_id.clone(), api); + } + if let Ok(mut lock) = self.handles.lock() { + lock.insert(plugin_id, handle); + } + } + + /// Remove a plugin's audio processor, returning its api + handle so the + /// caller can invoke `reset` before dropping. + pub fn unregister(&self, plugin_id: &str) -> Option<(AudioProcessorApi, *mut c_void)> { + let api = self + .processors + .write() + .ok() + .and_then(|mut lock| lock.remove(plugin_id)); + let handle = self + .handles + .lock() + .ok() + .and_then(|mut lock| lock.remove(plugin_id)); + match (api, handle) { + (Some(api), Some(handle)) => Some((api, handle)), + (Some(api), None) => Some((api, std::ptr::null_mut())), + _ => None, + } + } + + /// Snapshot of all active processors. Called once per render quantum + /// (128 frames) on the audio thread. + /// + /// Returns `(plugin_id, api, handle)` triples so the worklet can pass + /// the handle straight to the `process` FFI callback without a second + /// lookup. + pub fn iter_active(&self) -> Vec<(String, AudioProcessorApi, *mut c_void)> { + let processors = match self.processors.read() { + Ok(guard) => guard, + Err(_) => return Vec::new(), + }; + // Acquire the handle map only for the duration of the snapshot build. + let handles = match self.handles.lock() { + Ok(guard) => guard, + Err(_) => return Vec::new(), + }; + processors + .iter() + .map(|(id, api)| { + let handle = handles.get(id).copied().unwrap_or(std::ptr::null_mut()); + (id.clone(), *api, handle) + }) + .collect() + } +} + +// SAFETY: `AudioProcessorApi` is `Copy` (function pointers / null), `*mut +// c_void` raw pointers are wrapped behind `RwLock`/`Mutex`. The registry is +// shared (`Arc`) between the control thread and the audio render thread. +unsafe impl Send for AudioProcessorRegistry {} +unsafe impl Sync for AudioProcessorRegistry {} diff --git a/src-tauri/src/services/plugin/capability/capability.rs b/src-tauri/src/services/plugin/capability/capability.rs new file mode 100644 index 0000000..c2fd4d0 --- /dev/null +++ b/src-tauri/src/services/plugin/capability/capability.rs @@ -0,0 +1,8 @@ +use std::collections::HashSet; + +use plugin_sdk::permissions::PluginPermission; + +pub struct PluginCapability { + pub plugin_id: String, + pub permissions: HashSet, +} diff --git a/src-tauri/src/services/plugin/capability/mod.rs b/src-tauri/src/services/plugin/capability/mod.rs new file mode 100644 index 0000000..a7b47bd --- /dev/null +++ b/src-tauri/src/services/plugin/capability/mod.rs @@ -0,0 +1,7 @@ +pub mod capability; +pub mod permission_checker; +pub mod plugin_capability; + +pub use capability::PluginCapability; +pub use permission_checker::PermissionChecker; +pub use plugin_capability::CapabilityRegistry; diff --git a/src-tauri/src/services/plugin/capability/permission_checker.rs b/src-tauri/src/services/plugin/capability/permission_checker.rs new file mode 100644 index 0000000..1b99258 --- /dev/null +++ b/src-tauri/src/services/plugin/capability/permission_checker.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use crate::errors::AppError; +use crate::services::plugin::capability::plugin_capability::CapabilityRegistry; +use plugin_sdk::permissions::PluginPermission; + +pub struct PermissionChecker { + capability_registry: Arc, +} + +impl PermissionChecker { + pub fn new(capability_registry: Arc) -> Self { + Self { + capability_registry, + } + } + + pub fn require(&self, plugin_id: &str, permission: PluginPermission) -> Result<(), AppError> { + if self + .capability_registry + .has_permission(plugin_id, permission) + { + Ok(()) + } else { + Err(AppError::PluginPermissionDenied { + plugin_id: plugin_id.to_string(), + permission, + }) + } + } +} diff --git a/src-tauri/src/services/plugin/capability/plugin_capability.rs b/src-tauri/src/services/plugin/capability/plugin_capability.rs new file mode 100644 index 0000000..0e48701 --- /dev/null +++ b/src-tauri/src/services/plugin/capability/plugin_capability.rs @@ -0,0 +1,44 @@ +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use crate::services::plugin::capability::capability::PluginCapability; + +pub struct CapabilityRegistry { + capabilities: RwLock>>, +} + +impl CapabilityRegistry { + pub fn new() -> Self { + Self { + capabilities: RwLock::new(HashMap::new()), + } + } + + pub fn register(&self, capability: Arc) { + let plugin_id = capability.plugin_id.clone(); + if let Ok(mut lock) = self.capabilities.write() { + lock.insert(plugin_id, capability); + } + } + + pub fn remove(&self, plugin_id: &str) { + if let Ok(mut lock) = self.capabilities.write() { + lock.remove(plugin_id); + } + } + + pub fn has_permission( + &self, + plugin_id: &str, + permission: plugin_sdk::permissions::PluginPermission, + ) -> bool { + let lock = match self.capabilities.read() { + Ok(guard) => guard, + Err(_) => return false, + }; + + lock.get(plugin_id) + .map(|cap| cap.permissions.contains(&permission)) + .unwrap_or(false) + } +} diff --git a/src-tauri/src/services/plugin/dynamic_factory.rs b/src-tauri/src/services/plugin/dynamic_factory.rs new file mode 100644 index 0000000..3f5fd07 --- /dev/null +++ b/src-tauri/src/services/plugin/dynamic_factory.rs @@ -0,0 +1,176 @@ +use std::sync::Arc; + +use plugin_sdk::errors::PluginError; +use plugin_sdk::ffi::{AudioProcessorApi, ByteBuffer, CommandResponse, PluginExports, PluginHandle}; +use plugin_sdk::traits::{CommandArgs, Plugin}; + +pub struct DynamicPluginFactory { + lib: Arc, + exports: PluginExports, + manifest: plugin_sdk::manifest::ResolvedPluginManifest, + /// 可选音频处理器协议。来自 `rem_audio_processor` 符号查找; + /// 旧插件未导出此符号时为 None,行为与改动前一致。 + pub audio_processor: Option, +} + +// `PluginExports` 现含 `audio_processor: *const AudioProcessorApi` 裸指针, +// 编译器不再自动派生 Send/Sync。工厂从不解引用该字段(音频处理器权威值 +// 存于 `audio_processor: Option`,由 Copy 的函数指针组成, +// 天然 Send+Sync),因此与 DynamicPluginInstance 一致地手动声明安全。 +unsafe impl Send for DynamicPluginFactory {} +unsafe impl Sync for DynamicPluginFactory {} + +pub struct DynamicPluginInstance { + handle: PluginHandle, + lib: Arc, + exports: PluginExports, +} + +unsafe impl Send for DynamicPluginInstance {} +unsafe impl Sync for DynamicPluginInstance {} + +impl DynamicPluginFactory { + pub fn new( + lib: Arc, + exports: PluginExports, + manifest: plugin_sdk::manifest::ResolvedPluginManifest, + audio_processor: Option, + ) -> Self { + Self { + lib, + exports, + manifest, + audio_processor, + } + } +} + +impl plugin_sdk::traits::PluginFactory for DynamicPluginFactory { + fn create(&self) -> Arc { + let handle = unsafe { (self.exports.create)() }; + Arc::new(DynamicPluginInstance { + handle, + lib: Arc::clone(&self.lib), + exports: PluginExports { + abi_version: self.exports.abi_version, + create: self.exports.create, + destroy: self.exports.destroy, + free_buffer: self.exports.free_buffer, + api: plugin_sdk::ffi::PluginApi { + activate: self.exports.api.activate, + deactivate: self.exports.api.deactivate, + on_event: self.exports.api.on_event, + execute_command: self.exports.api.execute_command, + }, + // 实例不直接使用 audio_processor;权威值在工厂的 audio_processor 字段中。 + // 置 null 以避免读取旧插件 exports 中可能未初始化的字段。 + audio_processor: std::ptr::null(), + }, + }) + } +} + +impl Drop for DynamicPluginInstance { + fn drop(&mut self) { + unsafe { (self.exports.destroy)(self.handle) } + } +} + +fn serialize_to_buffer(value: &T) -> Result { + let json = serde_json::to_vec(value) + .map_err(|e| PluginError::Plugin(format!("Failed to serialize: {}", e)))?; + let len = json.len(); + let ptr = Box::into_raw(json.into_boxed_slice()) as *mut u8; + Ok(ByteBuffer { ptr, len }) +} + +fn deserialize_from_buffer serde::Deserialize<'de>>( + buffer: &ByteBuffer, +) -> Result { + let slice = unsafe { std::slice::from_raw_parts(buffer.ptr, buffer.len) }; + serde_json::from_slice(slice).map_err(|e| PluginError::Plugin(format!("Failed to deserialize: {}", e))) +} + +impl Plugin for DynamicPluginInstance { + fn activate( + &self, + _ctx: &plugin_sdk::context::PluginContext, + ) -> Result<(), PluginError> { + let result = unsafe { (self.exports.api.activate)(self.handle) }; + if result == 0 { + Ok(()) + } else { + Err(PluginError::Plugin(format!( + "activate failed with code {}", + result + ))) + } + } + + fn deactivate(&self) -> Result<(), PluginError> { + let result = unsafe { (self.exports.api.deactivate)(self.handle) }; + if result == 0 { + Ok(()) + } else { + Err(PluginError::Plugin(format!( + "deactivate failed with code {}", + result + ))) + } + } + + fn on_event( + &self, + event: &plugin_sdk::events::PluginEvent, + _ctx: &plugin_sdk::context::PluginContext, + ) -> Result<(), PluginError> { + let buffer = serialize_to_buffer(event)?; + let result = unsafe { (self.exports.api.on_event)(self.handle, buffer.ptr as *const std::ffi::c_void) }; + unsafe { (self.exports.free_buffer)(buffer); } + if result == 0 { + Ok(()) + } else { + Err(PluginError::Plugin(format!( + "on_event failed with code {}", + result + ))) + } + } + + fn execute_command( + &self, + command_id: &str, + args: &CommandArgs, + _ctx: &plugin_sdk::context::PluginContext, + ) -> Result<(), PluginError> { + let request_payload = serde_json::json!({ + "command_id": command_id, + "args": args + }); + + let request_buffer = serialize_to_buffer(&request_payload)?; + let ptr_to_ptr = &request_buffer.ptr as *const *mut u8 as *const *const std::ffi::c_void; + let response: CommandResponse = unsafe { + (self.exports.api.execute_command)(self.handle, ptr_to_ptr) + }; + unsafe { (self.exports.free_buffer)(request_buffer); } + + if response.success { + if response.data.ptr.is_null() || response.data.len == 0 { + return Ok(()); + } + let _response_data: serde_json::Value = deserialize_from_buffer(&response.data)?; + unsafe { (self.exports.free_buffer)(response.data); } + Ok(()) + } else { + let msg = if response.data.ptr.is_null() || response.data.len == 0 { + "execute_command failed".to_string() + } else { + let data: serde_json::Value = deserialize_from_buffer(&response.data).unwrap_or_default(); + unsafe { (self.exports.free_buffer)(response.data); } + data.get("message").and_then(|m| m.as_str()).unwrap_or("unknown error").to_string() + }; + Err(PluginError::Plugin(msg)) + } + } +} diff --git a/src-tauri/src/services/plugin/embedded.rs b/src-tauri/src/services/plugin/embedded.rs new file mode 100644 index 0000000..1d1d813 --- /dev/null +++ b/src-tauri/src/services/plugin/embedded.rs @@ -0,0 +1,24 @@ +//! Compile-time embedding of all builtin plugin manifests. +//! +//! This is the **single** place that owns `include_str!` paths to +//! `plugin.json` files. Providers reference these constants instead of +//! reaching across the directory tree themselves, so moving a file only +//! requires changing the path in one location. +//! +//! Path math (relative to *this* file, +//! `src-tauri/src/services/plugin/embedded.rs`): +//! - `..` → `src-tauri/src/services/plugin/` +//! - `../..` → `src-tauri/src/services/` +//! - `../../..` → `src-tauri/src/` +//! - `../../../..` → `src-tauri/` +//! - `../../../../plugins/lyrics/plugin.json` → target (repo root) + +/// Embedded `plugins/lyrics/plugin.json` — the single source of truth +/// for the lyrics plugin manifest (no Rust `build_manifest()` needed). +pub const LYRICS_MANIFEST: &str = + include_str!("../../../../plugins/lyrics/plugin.json"); + +/// Embedded `plugins/eq/plugin.json` — the single source of truth +/// for the EQ plugin manifest. +pub const EQ_MANIFEST: &str = + include_str!("../../../../plugins/eq/plugin.json"); diff --git a/src-tauri/src/services/plugin/extension/command_extension.rs b/src-tauri/src/services/plugin/extension/command_extension.rs new file mode 100644 index 0000000..12b0d58 --- /dev/null +++ b/src-tauri/src/services/plugin/extension/command_extension.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +use crate::services::plugin::extension::state::ExtensionState; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandExtension { + pub id: String, + pub plugin_id: String, + pub title: String, + pub category: Option, + pub state: ExtensionState, +} diff --git a/src-tauri/src/services/plugin/extension/menu_extension.rs b/src-tauri/src/services/plugin/extension/menu_extension.rs new file mode 100644 index 0000000..9d89f1d --- /dev/null +++ b/src-tauri/src/services/plugin/extension/menu_extension.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use plugin_sdk::contributes::MenuLocation; +use crate::services::plugin::extension::state::ExtensionState; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MenuExtension { + pub id: String, + pub plugin_id: String, + pub command: String, + pub location: MenuLocation, + pub group: Option, + pub state: ExtensionState, +} diff --git a/src-tauri/src/services/plugin/extension/mod.rs b/src-tauri/src/services/plugin/extension/mod.rs new file mode 100644 index 0000000..d14dadf --- /dev/null +++ b/src-tauri/src/services/plugin/extension/mod.rs @@ -0,0 +1,13 @@ +pub mod command_extension; +pub mod menu_extension; +pub mod native_view_extension; +pub mod registry; +pub mod sidebar_extension; +pub mod state; + +pub use command_extension::CommandExtension; +pub use menu_extension::MenuExtension; +pub use native_view_extension::NativeViewExtension; +pub use registry::ExtensionRegistry; +pub use sidebar_extension::SidebarExtension; +pub use state::ExtensionState; diff --git a/src-tauri/src/services/plugin/extension/native_view_extension.rs b/src-tauri/src/services/plugin/extension/native_view_extension.rs new file mode 100644 index 0000000..a059361 --- /dev/null +++ b/src-tauri/src/services/plugin/extension/native_view_extension.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +use crate::services::plugin::extension::state::ExtensionState; + +/// 原生视图扩展:插件声明的原生组件贡献在运行期的具象形态。 +/// 应用按 `token` 查找对应的预置原生组件并注入渲染。 +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NativeViewExtension { + pub id: String, + pub plugin_id: String, + pub title: String, + /// 组件令牌,应用按此查找组件 + pub token: String, + pub icon: Option, + pub state: ExtensionState, +} diff --git a/src-tauri/src/services/plugin/extension/registry.rs b/src-tauri/src/services/plugin/extension/registry.rs new file mode 100644 index 0000000..ca062a4 --- /dev/null +++ b/src-tauri/src/services/plugin/extension/registry.rs @@ -0,0 +1,118 @@ +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use plugin_sdk::contributes::MenuLocation; +use crate::services::plugin::extension::command_extension::CommandExtension; +use crate::services::plugin::extension::menu_extension::MenuExtension; +use crate::services::plugin::extension::native_view_extension::NativeViewExtension; +use crate::services::plugin::extension::sidebar_extension::SidebarExtension; +use crate::services::plugin::extension::state::ExtensionState; + +pub struct ExtensionRegistry { + menus: RwLock>>, + sidebars: RwLock>>, + commands: RwLock>>, + native_views: RwLock>>, +} + +impl ExtensionRegistry { + pub fn new() -> Self { + Self { + menus: RwLock::new(HashMap::new()), + sidebars: RwLock::new(HashMap::new()), + commands: RwLock::new(HashMap::new()), + native_views: RwLock::new(HashMap::new()), + } + } + + pub fn register_menu(&self, extension: Arc) { + let key = format!("{}.{}", extension.plugin_id, extension.id); + if let Ok(mut lock) = self.menus.write() { + lock.insert(key, extension); + } + } + + pub fn register_sidebar(&self, extension: Arc) { + let key = format!("{}.{}", extension.plugin_id, extension.id); + if let Ok(mut lock) = self.sidebars.write() { + lock.insert(key, extension); + } + } + + pub fn register_command(&self, extension: Arc) { + let key = format!("{}.{}", extension.plugin_id, extension.id); + if let Ok(mut lock) = self.commands.write() { + lock.insert(key, extension); + } + } + + pub fn register_native_view(&self, extension: Arc) { + let key = format!("{}.{}", extension.plugin_id, extension.id); + if let Ok(mut lock) = self.native_views.write() { + lock.insert(key, extension); + } + } + + pub fn unregister_plugin(&self, plugin_id: &str) { + if let Ok(mut lock) = self.menus.write() { + lock.retain(|_, v| v.plugin_id != plugin_id); + } + if let Ok(mut lock) = self.sidebars.write() { + lock.retain(|_, v| v.plugin_id != plugin_id); + } + if let Ok(mut lock) = self.commands.write() { + lock.retain(|_, v| v.plugin_id != plugin_id); + } + if let Ok(mut lock) = self.native_views.write() { + lock.retain(|_, v| v.plugin_id != plugin_id); + } + } + + pub fn menu_extensions(&self, location: MenuLocation) -> Vec { + let lock = match self.menus.read() { + Ok(guard) => guard, + Err(_) => return Vec::new(), + }; + + lock.values() + .filter(|ext| ext.location == location && ext.state == ExtensionState::Enabled) + .map(|ext| (**ext).clone()) + .collect() + } + + pub fn sidebar_extensions(&self) -> Vec { + let lock = match self.sidebars.read() { + Ok(guard) => guard, + Err(_) => return Vec::new(), + }; + + lock.values() + .filter(|ext| ext.state == ExtensionState::Enabled) + .map(|ext| (**ext).clone()) + .collect() + } + + pub fn command_extensions(&self) -> Vec { + let lock = match self.commands.read() { + Ok(guard) => guard, + Err(_) => return Vec::new(), + }; + + lock.values() + .filter(|ext| ext.state == ExtensionState::Enabled) + .map(|ext| (**ext).clone()) + .collect() + } + + pub fn native_view_extensions(&self) -> Vec { + let lock = match self.native_views.read() { + Ok(guard) => guard, + Err(_) => return Vec::new(), + }; + + lock.values() + .filter(|ext| ext.state == ExtensionState::Enabled) + .map(|ext| (**ext).clone()) + .collect() + } +} diff --git a/src-tauri/src/services/plugin/extension/sidebar_extension.rs b/src-tauri/src/services/plugin/extension/sidebar_extension.rs new file mode 100644 index 0000000..0a1cd47 --- /dev/null +++ b/src-tauri/src/services/plugin/extension/sidebar_extension.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +use crate::services::plugin::extension::state::ExtensionState; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SidebarExtension { + pub id: String, + pub plugin_id: String, + pub title: String, + pub icon: String, + pub route: String, + pub state: ExtensionState, +} diff --git a/src-tauri/src/services/plugin/extension/state.rs b/src-tauri/src/services/plugin/extension/state.rs new file mode 100644 index 0000000..4345198 --- /dev/null +++ b/src-tauri/src/services/plugin/extension/state.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum ExtensionState { + Enabled, + Disabled, +} diff --git a/src-tauri/src/services/plugin/loader.rs b/src-tauri/src/services/plugin/loader.rs new file mode 100644 index 0000000..35ace79 --- /dev/null +++ b/src-tauri/src/services/plugin/loader.rs @@ -0,0 +1,118 @@ +use crate::errors::AppError; +use plugin_sdk::manifest::PluginManifest; + +/// Manifest validation utilities. +/// +/// Builtin manifest loading (compile-time embed) now lives in the +/// [`provider`] module; packaged/user discovery will live there too in +/// the future. This module keeps only the cross-cutting validation used +/// by [`ManifestRegistry`] before a manifest is resolved. +/// +/// [`provider`]: crate::services::plugin::provider +/// [`ManifestRegistry`]: crate::services::plugin::registry::ManifestRegistry +pub struct PluginLoader; + +impl PluginLoader { + /// Validate a raw manifest before it is normalized and cached. + /// + /// The `id` must be a pure logical identifier — it must NOT carry a + /// source/provenance prefix (e.g. `builtin/lyrics`). Provenance lives + /// in the separate `source` field, not in the id. + pub fn validate_manifest(manifest: &PluginManifest) -> Result<(), AppError> { + if manifest.id.is_empty() { + return Err(AppError::Plugin("Manifest 'id' cannot be empty".into())); + } + if manifest.name.is_empty() { + return Err(AppError::Plugin("Manifest 'name' cannot be empty".into())); + } + if manifest.entry.is_empty() { + return Err(AppError::Plugin("Manifest 'entry' cannot be empty".into())); + } + // Reject legacy "source/name" ids — provenance is now `source`, not a + // prefix. This guards against regressing to the old `builtin/lyrics` + // convention. + if manifest.id.contains('/') { + return Err(AppError::Plugin(format!( + "Manifest id '{}' must not contain '/'; provenance belongs in the 'source' field, not the id", + manifest.id + ))); + } + for view in &manifest.contributes.views { + if view.id.is_empty() { + return Err(AppError::Plugin(format!( + "Plugin '{}' has a view with empty id", + manifest.id + ))); + } + if !view + .id + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + { + return Err(AppError::Plugin(format!( + "Plugin '{}' view '{}' id may only contain [a-zA-Z0-9_-]", + manifest.id, view.id + ))); + } + if view.entry.is_empty() { + return Err(AppError::Plugin(format!( + "Plugin '{}' view '{}' has empty entry", + manifest.id, view.id + ))); + } + if !view.entry.ends_with(".html") { + return Err(AppError::Plugin(format!( + "Plugin '{}' view '{}' entry '{}' must end with .html", + manifest.id, view.id, view.entry + ))); + } + if view.entry.contains("..") || view.entry.starts_with('/') { + return Err(AppError::Plugin(format!( + "Plugin '{}' view '{}' entry '{}' must not contain '..' or start with '/'", + manifest.id, view.id, view.entry + ))); + } + if view.title.is_empty() { + return Err(AppError::Plugin(format!( + "Plugin '{}' view '{}' has empty title", + manifest.id, view.id + ))); + } + } + for nv in &manifest.contributes.native_views { + if nv.id.is_empty() { + return Err(AppError::Plugin(format!( + "Plugin '{}' has a nativeView with empty id", + manifest.id + ))); + } + if !nv + .id + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + { + return Err(AppError::Plugin(format!( + "Plugin '{}' nativeView '{}' id may only contain [a-zA-Z0-9_-]", + manifest.id, nv.id + ))); + } + if nv.token.is_empty() { + return Err(AppError::Plugin(format!( + "Plugin '{}' nativeView '{}' has empty token", + manifest.id, nv.id + ))); + } + if !nv + .token + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + { + return Err(AppError::Plugin(format!( + "Plugin '{}' nativeView '{}' token may only contain [a-zA-Z0-9_-]", + manifest.id, nv.id + ))); + } + } + Ok(()) + } +} diff --git a/src-tauri/src/services/plugin/manager.rs b/src-tauri/src/services/plugin/manager.rs new file mode 100644 index 0000000..4a5bc10 --- /dev/null +++ b/src-tauri/src/services/plugin/manager.rs @@ -0,0 +1,1071 @@ +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, RwLock}; + +use serde::{Deserialize, Serialize}; +use crate::errors::AppError; +use crate::events::AppEvent; +use crate::services::plugin::audio_processor_registry::AudioProcessorRegistry; +use crate::services::plugin::capability::capability::PluginCapability; +use crate::services::plugin::capability::plugin_capability::CapabilityRegistry; +use plugin_sdk::contributes::{MenuContribution, MenuLocation}; +use plugin_sdk::events::PluginEvent; +use plugin_sdk::ffi::AudioProcessorApi; +use crate::services::plugin::extension::command_extension::CommandExtension; +use crate::services::plugin::extension::menu_extension::MenuExtension; +use crate::services::plugin::extension::native_view_extension::NativeViewExtension; +use crate::services::plugin::extension::registry::ExtensionRegistry; +use crate::services::plugin::extension::sidebar_extension::SidebarExtension; +use crate::services::plugin::extension::state::ExtensionState; +use plugin_sdk::manifest::{ActivationEvent, PluginSource, ResolvedPluginManifest}; +use plugin_sdk::permissions::PluginPermission; +use crate::services::plugin::registry::command_registry::CommandRegistry; +use crate::services::plugin::registry::factory_registry::FactoryRegistry; +use crate::services::plugin::registry::manifest_registry::ManifestRegistry; +use crate::services::plugin::runtime::context_factory::PluginContextFactory; +use crate::services::plugin::runtime::runtime_instance::RuntimePluginInstance; +use crate::services::plugin::runtime::state::{PluginLifecycleState, PluginState}; +use crate::services::plugin::settings::persistence::PluginStorage; +use plugin_sdk::settings::SettingValue; +use crate::services::plugin::settings::settings_registry::SettingsRegistry; +use plugin_sdk::traits::{CommandArgs, Plugin, PluginFactory}; +use tauri::Manager; + +pub struct PluginManager { + app_handle: tauri::AppHandle, + manifest_registry: Arc, + factory_registry: Arc, + command_registry: Arc, + extension_registry: Arc, + capability_registry: Arc, + settings_registry: Arc, + storage: Arc, + context_factory: PluginContextFactory, + instances: RwLock>, + lifecycle_states: RwLock>, + /// Shared audio processor registry. Populated when packaged plugins that + /// export `rem_audio_processor` are activated; read by the render graph's + /// `ExternalProcessorNode` on every quantum. + audio_processor_registry: Arc, + /// Known `AudioProcessorApi` per packaged plugin (regardless of lifecycle + /// state). Stored separately from the registry's *active* set so we can + /// look up the api at activate time without downcasting the + /// `Arc` stored in `FactoryRegistry`. + audio_processor_apis: RwLock>, +} + +impl PluginManager { + pub fn new( + app_handle: tauri::AppHandle, + manifest_registry: Arc, + factory_registry: Arc, + context_factory: PluginContextFactory, + capability_registry: Arc, + settings_registry: Arc, + storage: Arc, + audio_processor_registry: Arc, + ) -> Self { + Self { + app_handle, + manifest_registry, + factory_registry, + command_registry: Arc::new(CommandRegistry::new()), + extension_registry: Arc::new(ExtensionRegistry::new()), + capability_registry, + settings_registry, + storage, + context_factory, + instances: RwLock::new(HashMap::new()), + lifecycle_states: RwLock::new(HashMap::new()), + audio_processor_registry, + audio_processor_apis: RwLock::new(HashMap::new()), + } + } + + /// Shared audio processor registry — also held by the audio render graph. + pub fn audio_processor_registry(&self) -> Arc { + Arc::clone(&self.audio_processor_registry) + } + + pub fn manifest_registry(&self) -> &Arc { + &self.manifest_registry + } + + pub fn factory_registry(&self) -> &Arc { + &self.factory_registry + } + + pub fn command_registry(&self) -> &Arc { + &self.command_registry + } + + pub fn extension_registry(&self) -> &Arc { + &self.extension_registry + } + + pub fn capability_registry(&self) -> &Arc { + &self.capability_registry + } + + pub fn settings_registry(&self) -> &Arc { + &self.settings_registry + } + + pub fn load_persisted_state(&self) { + match self.storage.load_config() { + Ok((states, settings)) => { + // One-time id migration: ids used to carry a source prefix + // (e.g. `builtin/lyrics`). They are now pure logical ids + // (`lyrics`). Strip any `/` prefix so previously stored + // lifecycle/settings aren't orphaned, then persist the fixup. + let migrated = migrate_prefixed_ids(states, settings); + if let Ok(mut lock) = self.lifecycle_states.write() { + *lock = migrated.states; + } + self.settings_registry.load_all(migrated.settings); + if migrated.changed { + self.persist_state(); + } + } + Err(e) => { + eprintln!("[PluginManager] Failed to load persisted state: {}", e); + } + } + } + + pub fn persist_state(&self) { + let states = match self.lifecycle_states.read() { + Ok(guard) => guard.clone(), + Err(_) => return, + }; + let settings = self.settings_registry.dump_all(); + if let Err(e) = self.storage.save_config(&states, &settings) { + eprintln!("[PluginManager] Failed to persist state: {}", e); + } + } + + /// Register all builtin plugins whose manifests were aggregated by + /// `ManifestRegistry`. Each builtin manifest is explicitly linked to + /// its factory via [`builtin_factory_for`]; this is an explicit + /// association (not an implicit hard-coded factory list), so a + /// builtin manifest with no matching factory is surfaced loudly + /// instead of silently half-loading. + pub fn load_builtin_plugins(&self) { + for manifest in self.manifest_registry.by_source(PluginSource::Builtin) { + let factory = match builtin_factory_for(&manifest.id) { + Some(f) => f, + None => { + eprintln!( + "[PluginManager] No builtin factory for plugin '{}'; skipping", + manifest.id + ); + continue; + } + }; + self.register_static_plugin(manifest, factory); + } + } + + pub fn load_packaged_plugins(&self) { + use crate::services::plugin::provider::PackagedManifestProvider; + use crate::services::plugin::provider::ManifestProvider; + + let mut search_dirs = Vec::new(); + + if let Ok(app_data) = self.app_handle.path().app_data_dir() { + search_dirs.push(app_data.join("plugins")); + } + + if let Ok(exe_dir) = std::env::current_exe() { + if let Some(parent) = exe_dir.parent() { + let exe_plugin_dir = parent.to_path_buf(); + if exe_plugin_dir.exists() && !search_dirs.contains(&exe_plugin_dir) { + search_dirs.push(exe_plugin_dir); + } + } + } + + println!("[PluginManager] Packaged search dirs: {:?}", search_dirs); + + if search_dirs.is_empty() { + println!("[PluginManager] No search dirs, skipping packaged plugins"); + return; + } + + let provider = PackagedManifestProvider::new(search_dirs.clone()); + let raw_manifests = match provider.load() { + Ok(m) => { + println!("[PluginManager] Found {} packaged manifests", m.len()); + m + } + Err(e) => { + eprintln!("[PluginManager] Failed to load packaged manifests: {}", e); + return; + } + }; + + for raw in raw_manifests { + println!("[PluginManager] Registering manifest: {}", raw.id); + if let Err(e) = self.manifest_registry.register_one(raw) { + eprintln!("[PluginManager] Failed to register manifest: {}", e); + continue; + } + } + + for manifest in self.manifest_registry.by_source(plugin_sdk::manifest::PluginSource::Packaged) { + let plugin_id = manifest.id.clone(); + + println!("[PluginManager] Loading dynamic factory for '{}'...", plugin_id); + match self.load_dynamic_factory_for(plugin_id.as_str(), &search_dirs) { + Ok(factory) => { + println!("[PluginManager] Dynamic factory loaded for '{}', calling register_static_plugin", plugin_id); + self.register_static_plugin(manifest, factory); + println!("[PluginManager] register_static_plugin completed for '{}'", plugin_id); + println!("[PluginManager] instance count: {}", self.instances.read().map(|m| m.len()).unwrap_or(0)); + } + Err(e) => { + eprintln!("[PluginManager] Failed to load dynamic factory for '{}': {}", plugin_id, e); + } + } + } + } + + fn load_dynamic_factory_for( + &self, + plugin_id: &str, + search_dirs: &[std::path::PathBuf], + ) -> Result, AppError> { + use crate::services::plugin::dynamic_factory::DynamicPluginFactory; + + let platform_suffix = if cfg!(target_os = "windows") { + ".dll" + } else if cfg!(target_os = "macos") { + ".dylib" + } else { + ".so" + }; + + let mut lib_path = None; + for dir in search_dirs { + let plugin_dir = dir.join(plugin_id); + if !plugin_dir.exists() { + println!("[PluginManager] {} does not exist, skipping", plugin_dir.display()); + continue; + } + + let candidates = [ + plugin_dir.join(format!("lib{}", plugin_id)), + plugin_dir.join(format!("lib{}{}", plugin_id, platform_suffix)), + plugin_dir.join(format!("{}{}", plugin_id, platform_suffix)), + ]; + + for candidate in &candidates { + println!("[PluginManager] checking: {}", candidate.display()); + if candidate.exists() { + lib_path = Some(candidate.clone()); + break; + } + } + + if lib_path.is_none() { + let alt = plugin_dir.join(plugin_id); + if alt.exists() { + lib_path = Some(alt); + } + } + + if lib_path.is_some() { + break; + } + } + + let lib_path = lib_path.ok_or_else(|| AppError::Plugin(format!("No library found for plugin '{}'", plugin_id)))?; + + let lib = unsafe { libloading::Library::new(&lib_path) } + .map_err(|e| AppError::Plugin(format!("Failed to load library '{}': {}", lib_path.display(), e)))?; + + let lib = Arc::new(lib); + + let get_exports: libloading::Symbol plugin_sdk::ffi::PluginExports> = + unsafe { lib.get(b"get_plugin_exports") } + .map_err(|e| AppError::Plugin(format!("Symbol 'get_plugin_exports' not found: {}", e)))?; + + let exports = unsafe { get_exports() }; + + const CURRENT_ABI_VERSION: u32 = 1; + if exports.abi_version != CURRENT_ABI_VERSION { + return Err(AppError::Plugin(format!( + "Plugin '{}' ABI version mismatch: expected {}, got {}", + plugin_id, CURRENT_ABI_VERSION, exports.abi_version + ))); + } + + let manifest = self.manifest_registry.manifest(&plugin_id) + .ok_or_else(|| AppError::Plugin(format!("No manifest for plugin '{}'", plugin_id)))?; + + // 查找可选的 `rem_audio_processor` 符号。旧插件未导出此符号时为 None, + // 行为与改动前一致(不破坏现有 packaged 插件加载逻辑)。 + // 注意:不从 exports.audio_processor 字段读取(旧插件结构体更短,读取为 UB), + // 而是用单独的全局符号查找。 + let audio_processor: Option = match unsafe { + lib.get::<*const plugin_sdk::ffi::AudioProcessorApi>(b"rem_audio_processor") + } { + Ok(sym) => { + let ptr: *const plugin_sdk::ffi::AudioProcessorApi = *sym; + if ptr.is_null() { + None + } else { + // 拷贝 AudioProcessorApi(Copy),脱离符号借用,之后 lib 可安全 move 进工厂。 + Some(unsafe { *ptr }) + } + } + Err(_) => None, + }; + + // 记下 api 以便 activate 时无需 downcast `Arc`。 + if let Some(api) = audio_processor { + if let Ok(mut lock) = self.audio_processor_apis.write() { + lock.insert(plugin_id.to_string(), api); + } + } + + Ok(Arc::new(DynamicPluginFactory::new( + lib, + exports, + manifest, + audio_processor, + ))) + } + + /// Link one resolved manifest to a factory and register all of the + /// plugin's contributed extensions, capabilities, settings defaults, + /// and lifecycle entry. Manifest comes from `ManifestRegistry`; + /// factory is registered with `FactoryRegistry`. + pub fn register_static_plugin( + &self, + manifest: ResolvedPluginManifest, + factory: Arc, + ) { + let plugin_id = manifest.id.clone(); + + let mut defaults = HashMap::new(); + for def in &manifest.settings { + defaults.insert(def.key.clone(), def.default_value.clone()); + } + if !defaults.is_empty() { + self.settings_registry + .register_defaults(&plugin_id, defaults); + } + + for cmd in &manifest.contributes.commands { + self.command_registry + .register_command(cmd.id.clone(), plugin_id.clone()); + + self.extension_registry + .register_command(Arc::new(CommandExtension { + id: cmd.id.clone(), + plugin_id: plugin_id.clone(), + title: cmd.title.clone(), + category: cmd.category.clone(), + state: ExtensionState::Enabled, + })); + } + + for menu in &manifest.contributes.menus { + self.extension_registry + .register_menu(Arc::new(MenuExtension { + id: menu.command.clone(), + plugin_id: plugin_id.clone(), + command: menu.command.clone(), + location: menu.location.clone(), + group: menu.group.clone(), + state: ExtensionState::Enabled, + })); + } + + for sidebar in &manifest.contributes.sidebars { + self.extension_registry + .register_sidebar(Arc::new(SidebarExtension { + id: sidebar.id.clone(), + plugin_id: plugin_id.clone(), + title: sidebar.title.clone(), + icon: sidebar.icon.clone(), + // Single source of truth: route comes from the resolved + // manifest, never duplicated on the sidebar contribution. + route: manifest.route.clone(), + state: ExtensionState::Enabled, + })); + } + + for nv in &manifest.contributes.native_views { + self.extension_registry + .register_native_view(Arc::new(NativeViewExtension { + id: nv.id.clone(), + plugin_id: plugin_id.clone(), + title: nv.title.clone(), + token: nv.token.clone(), + icon: nv.icon.clone(), + state: ExtensionState::Enabled, + })); + } + + let permissions: HashSet = manifest.permissions.iter().cloned().collect(); + + self.capability_registry + .register(Arc::new(PluginCapability { + plugin_id: plugin_id.clone(), + permissions, + })); + + self.factory_registry.register(&plugin_id, factory); + + let lifecycle = { + let states = match self.lifecycle_states.read() { + Ok(guard) => guard, + Err(_) => return, + }; + states + .get(&plugin_id) + .cloned() + .unwrap_or(PluginLifecycleState::Enabled) + }; + + if lifecycle == PluginLifecycleState::Enabled { + if let Ok(mut instances) = self.instances.write() { + instances.insert(plugin_id, RuntimePluginInstance::Registered); + } + } + } + + pub fn enable_plugin(&self, plugin_id: &str) -> Result<(), AppError> { + { + let mut states = self + .lifecycle_states + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + states.insert(plugin_id.to_string(), PluginLifecycleState::Enabled); + } + + { + let mut instances = self + .instances + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + if !instances.contains_key(plugin_id) { + if self.factory_registry.contains(plugin_id) { + instances.insert(plugin_id.to_string(), RuntimePluginInstance::Registered); + } + } + } + + self.persist_state(); + Ok(()) + } + + pub fn disable_plugin(&self, plugin_id: &str) -> Result<(), AppError> { + if !self.factory_registry.contains(plugin_id) { + return Err(AppError::Plugin(format!( + "Plugin '{}' is not registered", + plugin_id + ))); + } + + if self.plugin_state(plugin_id) == PluginLifecycleState::Disabled { + return Ok(()); + } + + { + let mut states = self + .lifecycle_states + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + states.insert(plugin_id.to_string(), PluginLifecycleState::Disabled); + } + + { + let mut instances = self + .instances + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + if let Some(RuntimePluginInstance::Active(plugin)) = instances.remove(plugin_id) { + let _ = plugin.deactivate(); + // 失活的同时从音频图中摘除其处理器,避免回调已停用插件。 + self.unregister_audio_processor(plugin_id); + } + } + + self.persist_state(); + Ok(()) + } + + pub fn plugin_state(&self, plugin_id: &str) -> PluginLifecycleState { + let states = match self.lifecycle_states.read() { + Ok(guard) => guard, + Err(_) => return PluginLifecycleState::Enabled, + }; + states + .get(plugin_id) + .cloned() + .unwrap_or(PluginLifecycleState::Enabled) + } + + fn is_plugin_enabled(&self, plugin_id: &str) -> bool { + self.plugin_state(plugin_id) == PluginLifecycleState::Enabled + } + + pub fn execute_plugin_command( + &self, + command_id: &str, + args: CommandArgs, + ) -> Result<(), AppError> { + let plugin_id = self + .command_registry + .find_plugin(command_id) + .ok_or_else(|| { + AppError::Plugin(format!("No plugin registered for command '{}'", command_id)) + })?; + + if !self.is_plugin_enabled(&plugin_id) { + return Err(AppError::Plugin(format!( + "Plugin '{}' is disabled", + plugin_id + ))); + } + + let plugin = self.ensure_active(&plugin_id)?; + let ctx = self.context_factory.create_context(&plugin_id); + plugin.execute_command(command_id, &args, &ctx).map_err(|e| e.into()) + } + + fn ensure_active(&self, plugin_id: &str) -> Result, AppError> { + { + let instances = self + .instances + .read() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + match instances.get(plugin_id) { + Some(RuntimePluginInstance::Active(plugin)) => { + return Ok(Arc::clone(plugin)); + } + Some(RuntimePluginInstance::Activating) => { + return Err(AppError::Plugin(format!( + "Plugin '{}' is currently activating", + plugin_id + ))); + } + Some(RuntimePluginInstance::Deactivating(_)) => { + return Err(AppError::Plugin(format!( + "Plugin '{}' is currently deactivating", + plugin_id + ))); + } + Some(RuntimePluginInstance::Disabled) => { + return Err(AppError::Plugin(format!( + "Plugin '{}' is disabled", + plugin_id + ))); + } + Some(RuntimePluginInstance::Registered) => {} + None => { + return Err(AppError::Plugin(format!( + "Plugin '{}' is not registered", + plugin_id + ))); + } + } + } + + self.activate_instance(plugin_id) + } + + fn activate_instance(&self, plugin_id: &str) -> Result, AppError> { + { + let mut instances = self + .instances + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + match instances.get(plugin_id) { + Some(RuntimePluginInstance::Registered) => { + instances.insert(plugin_id.to_string(), RuntimePluginInstance::Activating); + } + _ => { + return Err(AppError::Plugin(format!( + "Plugin '{}' cannot be activated from its current state", + plugin_id + ))); + } + } + } + + let factory = self + .factory_registry + .get(plugin_id) + .ok_or_else(|| AppError::Plugin(format!("No factory for plugin '{}'", plugin_id)))?; + + let plugin = factory.create(); + let ctx = self.context_factory.create_context(plugin_id); + + match plugin.activate(&ctx) { + Ok(()) => { + // 若该 packaged 插件导出了音频处理器,注册到音频图共享的 registry。 + // 仅在插件声明 `Audio` 权限时生效;缺少权限则记录告警并跳过。 + self.maybe_register_audio_processor(plugin_id); + + let mut instances = self + .instances + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + instances.insert( + plugin_id.to_string(), + RuntimePluginInstance::Active(Arc::clone(&plugin)), + ); + + Ok(plugin) + } + Err(e) => { + let mut instances = self + .instances + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + instances.insert(plugin_id.to_string(), RuntimePluginInstance::Registered); + + Err(e.into()) + } + } + } + + /// Activate-time audio processor registration (Task 10.2 + 10.4). + /// + /// If the plugin exported `rem_audio_processor`, verify it declared the + /// `Audio` permission, call its `init` callback, then add it to the shared + /// registry so the render graph's `ExternalProcessorNode` starts feeding + /// it PCM. Lacking the permission only logs a warning — plugin activation + /// itself is unaffected. + fn maybe_register_audio_processor(&self, plugin_id: &str) { + let api = match self + .audio_processor_apis + .read() + .ok() + .and_then(|lock| lock.get(plugin_id).copied()) + { + Some(api) => api, + None => return, // 该插件未导出音频处理能力 + }; + + let has_audio_perm = self + .manifest_registry + .manifest(plugin_id) + .map(|m| m.permissions.iter().any(|p| matches!(p, PluginPermission::Audio))) + .unwrap_or(false); + + if !has_audio_perm { + eprintln!( + "[PluginManager] plugin '{}' exports audio_processor but lacks 'audio' permission, skipping", + plugin_id + ); + return; + } + + let sample_rate = self.audio_processor_registry.sample_rate(); + let channels = self.audio_processor_registry.channels(); + if let Some(init_fn) = api.init { + // handle 暂用 null:FFI PluginHandle 当前封存在 DynamicPluginInstance + // 内部,需 SDK trait 扩展才能透出(本任务范围内不修改 ffi.rs)。 + let _ = unsafe { init_fn(std::ptr::null_mut(), sample_rate, channels) }; + } + + self.audio_processor_registry + .register(plugin_id.to_string(), api, std::ptr::null_mut()); + } + + /// Deactivate-time cleanup (Task 10.3). Removes the processor from the + /// shared registry and invokes its `reset` callback so the plugin can + /// release delay lines / internal buffers. + fn unregister_audio_processor(&self, plugin_id: &str) { + let Some((api, handle)) = self.audio_processor_registry.unregister(plugin_id) else { + return; + }; + if let Some(reset_fn) = api.reset { + let _ = unsafe { reset_fn(handle) }; + } + } + + pub fn activate_plugin(&self, plugin_id: &str) -> Result<(), AppError> { + self.ensure_active(plugin_id)?; + Ok(()) + } + + pub fn deactivate_plugin(&self, plugin_id: &str) -> Result<(), AppError> { + let plugin = { + let mut instances = self + .instances + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + match instances.remove(plugin_id) { + Some(RuntimePluginInstance::Active(p)) => { + instances.insert( + plugin_id.to_string(), + RuntimePluginInstance::Deactivating(Arc::clone(&p)), + ); + p + } + other => { + if let Some(original) = other { + instances.insert(plugin_id.to_string(), original); + } + return Err(AppError::Plugin(format!( + "Plugin '{}' is not in Active state", + plugin_id + ))); + } + } + }; + + let result = plugin.deactivate(); + + let mut instances = self + .instances + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + match result { + Ok(()) => { + // 失活成功后从音频图中摘除其处理器并触发 reset。 + self.unregister_audio_processor(plugin_id); + instances.insert(plugin_id.to_string(), RuntimePluginInstance::Registered); + } + Err(e) => { + instances.insert(plugin_id.to_string(), RuntimePluginInstance::Active(plugin)); + return Err(e.into()); + } + } + + Ok(()) + } + + pub fn get_active_plugin(&self, id: &str) -> Option> { + let instances = self.instances.read().ok()?; + match instances.get(id)? { + RuntimePluginInstance::Active(p) => Some(Arc::clone(p)), + _ => None, + } + } + + pub fn list_active_plugins(&self) -> Vec<(String, Arc)> { + let instances = match self.instances.read() { + Ok(guard) => guard, + Err(_) => return Vec::new(), + }; + + instances + .iter() + .filter_map(|(id, inst)| match inst { + RuntimePluginInstance::Active(p) => Some((id.clone(), Arc::clone(p))), + _ => None, + }) + .collect() + } + + pub fn active_count(&self) -> usize { + self.instances + .read() + .ok() + .map(|l| { + l.values() + .filter(|i| matches!(i, RuntimePluginInstance::Active(_))) + .count() + }) + .unwrap_or(0) + } + + pub fn dispatch_event(&self, app_event: &AppEvent) { + let plugin_event = match Self::bridge_app_event(app_event) { + Some(event) => event, + None => return, + }; + + let required_activation = match &plugin_event { + PluginEvent::Startup => ActivationEvent::OnStartup, + PluginEvent::TrackChanged { .. } => ActivationEvent::OnTrackChanged, + PluginEvent::PlaybackStateChanged { .. } => ActivationEvent::OnPlaybackStateChanged, + PluginEvent::QueueChanged => ActivationEvent::OnQueueChanged, + PluginEvent::SettingsChanged => ActivationEvent::OnSettingsChanged, + PluginEvent::LyricsLoaded { .. } => return, + }; + + let pending_activation: Vec = { + let instances = match self.instances.read() { + Ok(guard) => guard, + Err(_) => return, + }; + + instances + .iter() + .filter(|(id, inst)| { + self.is_plugin_enabled(id) && matches!(inst, RuntimePluginInstance::Registered) + }) + .filter_map(|(id, _)| { + let manifest = self.manifest_registry.manifest(id)?; + if manifest.activation_events.contains(&required_activation) { + Some(id.clone()) + } else { + None + } + }) + .collect() + }; + + for plugin_id in &pending_activation { + let _ = self.ensure_active(plugin_id); + } + + let instances = match self.instances.read() { + Ok(guard) => guard, + Err(_) => return, + }; + + for (plugin_id, instance) in instances.iter() { + if !self.is_plugin_enabled(plugin_id) { + continue; + } + + if let RuntimePluginInstance::Active(plugin) = instance { + // Manifest is sourced from ManifestRegistry (single source of + // truth), not from the plugin instance. + let Some(manifest) = self.manifest_registry.manifest(plugin_id) else { + continue; + }; + if manifest.activation_events.contains(&required_activation) { + let ctx = self.context_factory.create_context(plugin_id); + let _ = plugin.on_event(&plugin_event, &ctx); + } + } + } + } + + pub fn bridge_app_event(app_event: &AppEvent) -> Option { + match app_event { + AppEvent::TrackStarted(payload) => Some(PluginEvent::TrackChanged { + track: plugin_sdk::events::TrackSnapshot { + id: payload.track.id, + title: payload.track.title.clone(), + artist: payload.track.artist.clone(), + album: payload.track.album.clone(), + duration: payload.track.duration, + }, + index: payload.index, + }), + AppEvent::PlaybackStateChanged(payload) => Some(PluginEvent::PlaybackStateChanged { + playing: payload.playing, + current_time: payload.current_time, + }), + AppEvent::QueueChanged(_) => Some(PluginEvent::QueueChanged), + AppEvent::SettingsChanged(_) => Some(PluginEvent::SettingsChanged), + AppEvent::VolumeChanged(_) => None, + AppEvent::PlaybackProgress(_) => None, + AppEvent::LyricsLoaded(_) => None, + } + } + + pub fn menu_extensions(&self, location: MenuLocation) -> Vec { + self.extension_registry + .menu_extensions(location) + .into_iter() + .filter(|ext| self.is_plugin_enabled(&ext.plugin_id)) + .collect() + } + + pub fn sidebar_extensions(&self) -> Vec { + self.extension_registry + .sidebar_extensions() + .into_iter() + .filter(|ext| self.is_plugin_enabled(&ext.plugin_id)) + .collect() + } + + pub fn native_view_extensions(&self) -> Vec { + self.extension_registry + .native_view_extensions() + .into_iter() + .filter(|ext| self.is_plugin_enabled(&ext.plugin_id)) + .collect() + } + + pub fn all_sidebar_extensions(&self) -> Vec { + self.extension_registry + .sidebar_extensions() + .into_iter() + .map(|mut ext| { + if !self.is_plugin_enabled(&ext.plugin_id) { + ext.state = ExtensionState::Disabled; + } + ext + }) + .collect() + } + + pub fn get_plugin_settings( + &self, + plugin_id: &str, + ) -> Vec { + let definitions = self + .manifest_registry + .manifest(plugin_id) + .map(|m| m.settings) + .unwrap_or_default(); + + let current = self.settings_registry.get_all(plugin_id); + + definitions + .into_iter() + .map(|def| { + let value = current + .get(&def.key) + .cloned() + .unwrap_or_else(|| def.default_value.clone()); + plugin_sdk::settings::PluginSettingMeta { + key: def.key, + title: def.title, + value, + default_value: def.default_value, + } + }) + .collect() + } + + pub fn update_plugin_setting( + &self, + plugin_id: &str, + key: &str, + value: SettingValue, + ) -> Result<(), AppError> { + self.settings_registry + .update_setting(plugin_id, key, value)?; + self.persist_state(); + Ok(()) + } + + /// All resolved manifests, the only plugin-identity shape the frontend + /// ever consumes. Exposed via the `get_plugin_manifests` command. + pub fn all_manifests(&self) -> Vec { + self.manifest_registry.all() + } + + /// Resolve the view entry URL for a plugin's view. + /// Returns None if: + /// - plugin not found + /// - plugin has no views + /// - view_id not found among the plugin's views + /// - plugin lacks PluginUI permission + pub fn plugin_view(&self, plugin_id: &str, view_id: &str) -> Option { + let manifest = self.manifest_registry.manifest(plugin_id)?; + + // 校验权限 + let has_ui_permission = manifest + .permissions + .iter() + .any(|p| matches!(p, PluginPermission::PluginUI)); + if !has_ui_permission { + return None; + } + + // 查找 view + let view = manifest + .contributes + .views + .iter() + .find(|v| v.id == view_id) + .cloned()?; + + Some(PluginViewResolution { + plugin_id: plugin_id.to_string(), + view_id: view.id.clone(), + title: view.title.clone(), + entry_url: format!("plugin://{}/{}", plugin_id, view.entry), + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginViewResolution { + pub plugin_id: String, + pub view_id: String, + pub title: String, + pub entry_url: String, +} + +/// Explicit association from a builtin plugin id to its Rust factory. +/// +/// This is the single, intentional place where a builtin manifest (sourced +/// from `plugin.json`) is wired to the crate that implements its logic. +/// Adding a new builtin plugin means adding one line here plus one embedded +/// manifest in [`embedded`]. +/// +/// [`embedded`]: crate::services::plugin::embedded +fn builtin_factory_for(id: &str) -> Option> { + match id { + "lyrics" => Some(Arc::new(lyrics_plugin::LyricsPluginFactory)), + "eq" => Some(Arc::new(eq_plugin::EqPluginFactory)), + _ => None, + } +} + +/// Result of stripping legacy `/` prefixes from persisted ids. +struct MigratedState { + states: HashMap, + settings: HashMap, + changed: bool, +} + +/// Strip any `/` prefix from persisted plugin ids so configs written +/// under the old `builtin/lyrics` convention keep working under the new +/// pure-id convention (`lyrics`). +fn migrate_prefixed_ids( + states: HashMap, + mut settings: HashMap, +) -> MigratedState { + let mut changed = false; + + // Migrate state keys. + let migrated_states = states + .into_iter() + .map(|(id, state)| { + if let Some(stripped) = id.rsplit('/').next() { + if stripped != id { + changed = true; + return (stripped.to_string(), state); + } + } + (id, state) + }) + .collect::>(); + + // Migrate settings keys, and fix the `plugin_id` field carried inside + // each `PluginSettings` so it stays consistent with the map key. + let migrated_settings = std::mem::take(&mut settings); + let mut new_settings = HashMap::with_capacity(migrated_settings.len()); + for (id, mut ps) in migrated_settings { + if let Some(stripped) = id.rsplit('/').next() { + if stripped != id { + changed = true; + ps.plugin_id = stripped.to_string(); + new_settings.insert(stripped.to_string(), ps); + continue; + } + } + new_settings.insert(id, ps); + } + + MigratedState { + states: migrated_states, + settings: new_settings, + changed, + } +} diff --git a/src-tauri/src/services/plugin/mod.rs b/src-tauri/src/services/plugin/mod.rs new file mode 100644 index 0000000..2b84863 --- /dev/null +++ b/src-tauri/src/services/plugin/mod.rs @@ -0,0 +1,39 @@ +pub mod adapters; +pub mod asset; +pub mod audio_processor_registry; +pub mod capability; +pub mod dynamic_factory; +pub mod embedded; +pub mod extension; +pub mod loader; +pub mod manager; +pub mod provider; +pub mod registry; +pub mod runtime; +pub mod sdk_adapters; +pub mod settings; + +pub use audio_processor_registry::AudioProcessorRegistry; +pub use capability::{CapabilityRegistry, PermissionChecker, PluginCapability}; +pub use extension::{ + CommandExtension, ExtensionRegistry, ExtensionState, MenuExtension, SidebarExtension, +}; +pub use loader::PluginLoader; +pub use manager::PluginManager; +pub use provider::{BuiltinManifestProvider, ManifestProvider}; +pub use registry::{CommandRegistry, FactoryRegistry, ManifestRegistry}; +pub use runtime::{PluginContextFactory, PluginLifecycleState, PluginState, RuntimePluginInstance}; +pub use settings::{PluginStorage, SettingsRegistry}; + +pub use plugin_sdk::api::{LibraryApi, PlayerApi, PluginSettingsApi, QueueApi, SettingsApi}; +pub use plugin_sdk::context::{HostContext, PluginContext}; +pub use plugin_sdk::errors::PluginError; +pub use plugin_sdk::events::PluginEvent; +pub use plugin_sdk::manifest::{ + ActivationEvent, PluginContribution, PluginManifest, PluginSource, ResolvedPluginManifest, + SettingDefinition, +}; +pub use plugin_sdk::model::PluginIdentity; +pub use plugin_sdk::permissions::PluginPermission; +pub use plugin_sdk::settings::{PluginSetting, PluginSettingMeta, PluginSettings, SettingValue}; +pub use plugin_sdk::traits::{CommandArgs, Plugin, PluginFactory}; diff --git a/src-tauri/src/services/plugin/provider/builtin.rs b/src-tauri/src/services/plugin/provider/builtin.rs new file mode 100644 index 0000000..c298a7b --- /dev/null +++ b/src-tauri/src/services/plugin/provider/builtin.rs @@ -0,0 +1,40 @@ +//! Provider for builtin plugins. +//! +//! Builtin manifests are embedded at compile time via [`embedded`] and +//! their logic ships as Rust crates. Each builtin manifest is parsed +//! from its embedded JSON and force-stamped `source = Builtin` so the +//! source field can never lie about provenance. +//! +//! [`embedded`]: crate::services::plugin::embedded + +use crate::errors::AppError; +use crate::services::plugin::embedded; +use plugin_sdk::manifest::{PluginManifest, PluginSource}; + +use super::ManifestProvider; + +/// Loads builtin plugin manifests embedded at compile time. +pub struct BuiltinManifestProvider; + +impl ManifestProvider for BuiltinManifestProvider { + fn source(&self) -> PluginSource { + PluginSource::Builtin + } + + fn load(&self) -> Result, AppError> { + let raw_entries: &[&str] = &[embedded::LYRICS_MANIFEST, embedded::EQ_MANIFEST]; + let source = self.source(); + + let mut manifests = Vec::with_capacity(raw_entries.len()); + for raw in raw_entries { + let mut manifest: PluginManifest = serde_json::from_str(raw).map_err(|e| { + AppError::Plugin(format!("Failed to parse builtin manifest: {}", e)) + })?; + // Force-correct provenance: a builtin provider can only ever + // publish builtin manifests, regardless of what the json says. + manifest.source = source; + manifests.push(manifest); + } + Ok(manifests) + } +} diff --git a/src-tauri/src/services/plugin/provider/mod.rs b/src-tauri/src/services/plugin/provider/mod.rs new file mode 100644 index 0000000..e68b01a --- /dev/null +++ b/src-tauri/src/services/plugin/provider/mod.rs @@ -0,0 +1,35 @@ +//! Manifest source providers. +//! +//! Each provider knows how to load raw [`PluginManifest`]s from one +//! provenance (builtin / packaged / user). The [`ManifestRegistry`] +//! collects from all providers, normalizes, validates, and resolves +//! them into [`ResolvedPluginManifest`]s. +//! +//! [`ManifestRegistry`]: crate::services::plugin::registry::ManifestRegistry +//! [`ResolvedPluginManifest`]: plugin_sdk::manifest::ResolvedPluginManifest + +pub mod builtin; +pub mod packaged; + +pub use builtin::BuiltinManifestProvider; +pub use packaged::PackagedManifestProvider; + +use crate::errors::AppError; +use plugin_sdk::manifest::{PluginManifest, PluginSource}; + +/// Loads raw plugin manifests from a single provenance. +/// +/// Implementations must NOT normalize or validate beyond what is needed +/// to deserialize; the registry owns normalization/validation. They MAY, +/// however, override `source` to reflect their own provenance — this is +/// how builtin manifests stamped with the wrong `source` in their json +/// get corrected. +pub trait ManifestProvider: Send + Sync { + /// The provenance this provider represents. Used to (re)stamp each + /// loaded manifest's `source` field so a builtin provider can never + /// accidentally publish a `user`/`packaged` manifest and vice versa. + fn source(&self) -> PluginSource; + + /// Load every manifest this provider can supply. + fn load(&self) -> Result, AppError>; +} diff --git a/src-tauri/src/services/plugin/provider/packaged.rs b/src-tauri/src/services/plugin/provider/packaged.rs new file mode 100644 index 0000000..fca27cd --- /dev/null +++ b/src-tauri/src/services/plugin/provider/packaged.rs @@ -0,0 +1,64 @@ +use std::path::PathBuf; + +use crate::errors::AppError; +use crate::services::plugin::loader::PluginLoader; +use crate::services::plugin::provider::ManifestProvider; +use plugin_sdk::manifest::{PluginManifest, PluginSource}; + +pub struct PackagedManifestProvider { + search_dirs: Vec, +} + +impl PackagedManifestProvider { + pub fn new(search_dirs: Vec) -> Self { + Self { search_dirs } + } +} + +impl ManifestProvider for PackagedManifestProvider { + fn source(&self) -> PluginSource { + PluginSource::Packaged + } + + fn load(&self) -> Result, AppError> { + let mut manifests = Vec::new(); + + for dir in &self.search_dirs { + if !dir.exists() { + continue; + } + + let entries = std::fs::read_dir(dir).map_err(|e| { + AppError::Io(format!("Failed to read '{}': {}", dir.display(), e)) + })?; + + for entry in entries { + let entry = entry.map_err(|e| AppError::Io(format!("Failed to read entry: {}", e)))?; + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let manifest_path = path.join("plugin.json"); + if !manifest_path.exists() { + continue; + } + + let content = std::fs::read_to_string(&manifest_path).map_err(|e| { + AppError::Io(format!("Failed to read '{}': {}", manifest_path.display(), e)) + })?; + + let mut manifest: PluginManifest = serde_json::from_str(&content).map_err(|e| { + AppError::Plugin(format!("Failed to parse '{}': {}", manifest_path.display(), e)) + })?; + + PluginLoader::validate_manifest(&manifest)?; + + manifest.source = PluginSource::Packaged; + manifests.push(manifest); + } + } + + Ok(manifests) + } +} diff --git a/src-tauri/src/services/plugin/registry/command_registry.rs b/src-tauri/src/services/plugin/registry/command_registry.rs new file mode 100644 index 0000000..b05d710 --- /dev/null +++ b/src-tauri/src/services/plugin/registry/command_registry.rs @@ -0,0 +1,38 @@ +use std::collections::HashMap; +use std::sync::RwLock; + +/// Maps command ids to their owning plugin ids. +/// +/// Populated during factory registration, consulted during +/// `execute_plugin_command` dispatch. +pub struct CommandRegistry { + commands: RwLock>, +} + +impl CommandRegistry { + pub fn new() -> Self { + Self { + commands: RwLock::new(HashMap::new()), + } + } + + pub fn register_command(&self, command_id: String, plugin_id: String) { + if let Ok(mut lock) = self.commands.write() { + lock.insert(command_id, plugin_id); + } + } + + pub fn find_plugin(&self, command_id: &str) -> Option { + self.commands.read().ok()?.get(command_id).cloned() + } + + pub fn unregister_plugin(&self, plugin_id: &str) { + if let Ok(mut lock) = self.commands.write() { + lock.retain(|_, v| v != plugin_id); + } + } + + pub fn count(&self) -> usize { + self.commands.read().ok().map(|l| l.len()).unwrap_or(0) + } +} diff --git a/src-tauri/src/services/plugin/registry/factory_registry.rs b/src-tauri/src/services/plugin/registry/factory_registry.rs new file mode 100644 index 0000000..365c96a --- /dev/null +++ b/src-tauri/src/services/plugin/registry/factory_registry.rs @@ -0,0 +1,50 @@ +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use plugin_sdk::traits::PluginFactory; + +/// Maps plugin id → `PluginFactory`. **Only** owns factories — never +/// manifests (those live in [`ManifestRegistry`]) and never live +/// instances (those live in `PluginManager`). +/// +/// Keeping factory registration separate from manifest aggregation +/// avoids the half-loaded state where a manifest exists but its factory +/// was never registered (or vice versa): `PluginManager` is the only +/// component that links the two, by id. +/// +/// [`ManifestRegistry`]: super::ManifestRegistry +pub struct FactoryRegistry { + factories: RwLock>>, +} + +impl FactoryRegistry { + pub fn new() -> Self { + Self { + factories: RwLock::new(HashMap::new()), + } + } + + pub fn register(&self, id: &str, factory: Arc) { + if let Ok(mut lock) = self.factories.write() { + lock.insert(id.to_string(), factory); + } + } + + pub fn get(&self, id: &str) -> Option> { + self.factories.read().ok()?.get(id).cloned() + } + + pub fn contains(&self, id: &str) -> bool { + self.factories + .read() + .map(|l| l.contains_key(id)) + .unwrap_or(false) + } + + pub fn list_ids(&self) -> Vec { + self.factories + .read() + .map(|l| l.keys().cloned().collect()) + .unwrap_or_default() + } +} diff --git a/src-tauri/src/services/plugin/registry/manifest_registry.rs b/src-tauri/src/services/plugin/registry/manifest_registry.rs new file mode 100644 index 0000000..b44b5b0 --- /dev/null +++ b/src-tauri/src/services/plugin/registry/manifest_registry.rs @@ -0,0 +1,179 @@ +use std::collections::HashMap; +use std::sync::RwLock; + +use crate::errors::AppError; +use crate::services::plugin::provider::ManifestProvider; +use plugin_sdk::manifest::{ + PluginManifest, PluginSource, ResolvedPluginManifest, +}; + +/// Prefix prepended to every plugin slug to form an absolute route. +const VIEW_PREFIX: &str = "/plugins/view/"; + +/// Collects, normalizes, validates, resolves, and caches plugin +/// manifests. **Only** owns manifests — never factories (those live in +/// [`FactoryRegistry`]) and never live plugin instances (those live in +/// `PluginManager`). +/// +/// Pipeline: collect (providers) → normalize → validate → resolve → cache +/// → expose [`ResolvedPluginManifest`] (the only shape with no `Option`s). +/// +/// [`FactoryRegistry`]: super::FactoryRegistry +pub struct ManifestRegistry { + resolved: RwLock>, + by_route: RwLock>, // route -> id +} + +impl ManifestRegistry { + pub fn new() -> Self { + Self { + resolved: RwLock::new(HashMap::new()), + by_route: RwLock::new(HashMap::new()), + } + } + + /// Run the full collect → normalize → validate → resolve → cache + /// pipeline over every provider. Already-registered ids/routes are + /// overwritten by later providers in the slice order. + pub fn register_from_providers( + &self, + providers: Vec>, + ) -> Result<(), AppError> { + for provider in providers { + let manifests = provider.load()?; + for raw in manifests { + self.register_one(raw)?; + } + } + Ok(()) + } + + /// Normalize, validate, resolve and cache a single raw manifest. + pub fn register_one(&self, raw: PluginManifest) -> Result<(), AppError> { + crate::services::plugin::loader::PluginLoader::validate_manifest(&raw)?; + + let id = raw.id.clone(); + let resolved = resolve(raw)?; + + // Deduplicate: unique id AND unique normalized route. + { + let by_id = self + .resolved + .read() + .map_err(|e| AppError::Plugin(e.to_string()))?; + if let Some(existing) = by_id.get(&id) { + return Err(AppError::Plugin(format!( + "Duplicate plugin id '{}', already registered with route '{}'", + id, existing.route + ))); + } + if by_id + .values() + .any(|m| m.route == resolved.route && m.id != id) + { + return Err(AppError::Plugin(format!( + "Duplicate normalized route '{}' from plugin '{}'", + resolved.route, id + ))); + } + } + + let mut by_id = self + .resolved + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + let mut by_route = self + .by_route + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + by_route.insert(resolved.route.clone(), id.clone()); + by_id.insert(id, resolved); + Ok(()) + } + + pub fn manifest(&self, id: &str) -> Option { + self.resolved.read().ok()?.get(id).cloned() + } + + pub fn manifest_by_route(&self, route: &str) -> Option { + let id = self.by_route.read().ok()?.get(route)?.clone(); + self.manifest(&id) + } + + pub fn all(&self) -> Vec { + self.resolved + .read() + .map(|l| l.values().cloned().collect()) + .unwrap_or_default() + } + + pub fn contains(&self, id: &str) -> bool { + self.resolved + .read() + .map(|l| l.contains_key(id)) + .unwrap_or(false) + } + + /// All manifests whose provenance is builtin. Used by `PluginManager` + /// to wire builtin manifests to their factories. + pub fn by_source(&self, source: PluginSource) -> Vec { + self.resolved + .read() + .map(|l| l.values().filter(|m| m.source == source).cloned().collect()) + .unwrap_or_default() + } +} + +/// Validate a slug segment and turn it into an absolute route. +/// +/// The slug must be a single path segment (no `/`, `..`, `?`) and may +/// only contain `[a-zA-Z0-9_-]`. The result is always `VIEW_PREFIX + slug`. +fn normalize_route(id: &str, override_route: Option<&str>) -> Result { + let slug = override_route.unwrap_or(id); + validate_slug(slug)?; + Ok(format!("{}{}", VIEW_PREFIX, slug)) +} + +fn validate_slug(slug: &str) -> Result<(), AppError> { + if slug.is_empty() { + return Err(AppError::Plugin("Plugin route slug cannot be empty".into())); + } + if slug.starts_with('/') || slug.contains("..") || slug.contains('?') { + return Err(AppError::Plugin(format!( + "Plugin route slug '{}' must not start with '/', contain '..', or include '?'", + slug + ))); + } + if !slug + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + { + return Err(AppError::Plugin(format!( + "Plugin route slug '{}' may only contain [a-zA-Z0-9_-]", + slug + ))); + } + Ok(()) +} + +/// Promote a raw (loader-internal) manifest to a fully resolved one. +fn resolve(raw: PluginManifest) -> Result { + let route = normalize_route(&raw.id, raw.route.as_deref())?; + Ok(ResolvedPluginManifest { + id: raw.id, + source: raw.source, + route, + name: raw.name, + display_name: raw.display_name, + version: raw.version, + author: raw.author, + description: raw.description, + entry: raw.entry, + min_app_version: raw.min_app_version, + permissions: raw.permissions, + activation_events: raw.activation_events, + contributes: raw.contributes, + settings: raw.settings, + }) +} diff --git a/src-tauri/src/services/plugin/registry/mod.rs b/src-tauri/src/services/plugin/registry/mod.rs new file mode 100644 index 0000000..93269e9 --- /dev/null +++ b/src-tauri/src/services/plugin/registry/mod.rs @@ -0,0 +1,7 @@ +pub mod command_registry; +pub mod factory_registry; +pub mod manifest_registry; + +pub use command_registry::CommandRegistry; +pub use factory_registry::FactoryRegistry; +pub use manifest_registry::ManifestRegistry; diff --git a/src-tauri/src/services/plugin/runtime/context_factory.rs b/src-tauri/src/services/plugin/runtime/context_factory.rs new file mode 100644 index 0000000..c19a5bb --- /dev/null +++ b/src-tauri/src/services/plugin/runtime/context_factory.rs @@ -0,0 +1,79 @@ +use std::sync::Arc; + +use tauri::AppHandle; + +use plugin_sdk::context::PluginContext; +use crate::services::playback_service::PlaybackService; +use crate::services::plugin::adapters::{ + AudioAdapter, LibraryAdapter, PlayerAdapter, PluginSettingsAdapter, QueueAdapter, SettingsAdapter, +}; +use crate::services::plugin::capability::permission_checker::PermissionChecker; +use crate::services::plugin::sdk_adapters::SdkHostContext; +use crate::services::plugin::settings::settings_registry::SettingsRegistry; +use crate::services::settings_service::SettingsService; +use crate::services::track_service::TrackService; + +pub struct PluginContextFactory { + app_handle: AppHandle, + checker: Arc, + playback_service: Arc, + track_service: Arc, + settings_service: Arc, + settings_registry: Arc, +} + +impl PluginContextFactory { + pub fn new( + app_handle: AppHandle, + checker: Arc, + playback_service: Arc, + track_service: Arc, + settings_service: Arc, + settings_registry: Arc, + ) -> Self { + Self { + app_handle, + checker, + playback_service, + track_service, + settings_service, + settings_registry, + } + } + + pub fn create_context(&self, plugin_id: &str) -> PluginContext { + let host = Arc::new(SdkHostContext::new( + plugin_id.to_string(), + self.app_handle.clone(), + )); + let player = Arc::new(PlayerAdapter::new( + plugin_id.to_string(), + Arc::clone(&self.checker), + Arc::clone(&self.playback_service), + )); + let library = Arc::new(LibraryAdapter::new( + plugin_id.to_string(), + Arc::clone(&self.checker), + Arc::clone(&self.track_service), + )); + let queue = Arc::new(QueueAdapter::new( + plugin_id.to_string(), + Arc::clone(&self.checker), + )); + let settings = Arc::new(SettingsAdapter::new( + plugin_id.to_string(), + Arc::clone(&self.checker), + Arc::clone(&self.settings_service), + )); + let plugin_settings = Arc::new(PluginSettingsAdapter::new( + plugin_id.to_string(), + Arc::clone(&self.settings_registry), + )); + let audio = Arc::new(AudioAdapter::new( + plugin_id.to_string(), + Arc::clone(&self.checker), + )); + + PluginContext::new(host, player, library, queue, settings, plugin_settings, audio) + } +} diff --git a/src-tauri/src/services/plugin/runtime/mod.rs b/src-tauri/src/services/plugin/runtime/mod.rs new file mode 100644 index 0000000..c413d12 --- /dev/null +++ b/src-tauri/src/services/plugin/runtime/mod.rs @@ -0,0 +1,7 @@ +pub mod context_factory; +pub mod runtime_instance; +pub mod state; + +pub use context_factory::PluginContextFactory; +pub use runtime_instance::RuntimePluginInstance; +pub use state::{PluginLifecycleState, PluginState}; diff --git a/src-tauri/src/services/plugin/runtime/runtime_instance.rs b/src-tauri/src/services/plugin/runtime/runtime_instance.rs new file mode 100644 index 0000000..ad5d99a --- /dev/null +++ b/src-tauri/src/services/plugin/runtime/runtime_instance.rs @@ -0,0 +1,24 @@ +use std::sync::Arc; + +use plugin_sdk::traits::Plugin; + +pub enum RuntimePluginInstance { + Registered, + Activating, + Active(Arc), + Deactivating(Arc), + Disabled, +} + +impl RuntimePluginInstance { + pub fn plugin(&self) -> Option<&Arc> { + match self { + Self::Active(p) | Self::Deactivating(p) => Some(p), + _ => None, + } + } + + pub fn is_active(&self) -> bool { + matches!(self, Self::Active(_)) + } +} diff --git a/src-tauri/src/services/plugin/runtime/state.rs b/src-tauri/src/services/plugin/runtime/state.rs new file mode 100644 index 0000000..d47b292 --- /dev/null +++ b/src-tauri/src/services/plugin/runtime/state.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum PluginLifecycleState { + Enabled, + Disabled, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PluginState { + pub plugin_id: String, + pub state: PluginLifecycleState, +} diff --git a/src-tauri/src/services/plugin/sdk_adapters.rs b/src-tauri/src/services/plugin/sdk_adapters.rs new file mode 100644 index 0000000..6586053 --- /dev/null +++ b/src-tauri/src/services/plugin/sdk_adapters.rs @@ -0,0 +1,51 @@ +use std::path::PathBuf; + +use plugin_sdk::context::HostContext; +use plugin_sdk::errors::PluginError; +use plugin_sdk::events::LyricsLoadedEvent; + +use crate::events::{EventBus, LyricLinePayload, LyricsLoadedPayload}; +use tauri::{AppHandle, Emitter, Manager}; + +pub struct SdkHostContext { + plugin_id: String, + app_handle: AppHandle, +} + +impl SdkHostContext { + pub fn new(plugin_id: String, app_handle: AppHandle) -> Self { + Self { plugin_id, app_handle } + } +} + +impl HostContext for SdkHostContext { + fn plugin_id(&self) -> &str { + &self.plugin_id + } + + fn cache_dir(&self) -> Result { + let app_data = self + .app_handle + .path() + .app_data_dir() + .map_err(|e| PluginError::Io(e.to_string()))?; + Ok(app_data.join("plugins").join(&self.plugin_id).join("cache")) + } + + fn emit_lyrics_loaded(&self, event: LyricsLoadedEvent) -> Result<(), PluginError> { + let payload = LyricsLoadedPayload { + song_id: event.song_id, + lines: event + .timestamp_ms_list + .into_iter() + .zip(event.text_list.into_iter()) + .map(|(ts, text)| LyricLinePayload { + timestamp_ms: ts, + text, + }) + .collect(), + }; + EventBus::emit_lyrics_loaded(&self.app_handle, payload) + .map_err(|e| PluginError::Plugin(e.to_string())) + } +} diff --git a/src-tauri/src/services/plugin/settings/mod.rs b/src-tauri/src/services/plugin/settings/mod.rs new file mode 100644 index 0000000..e408d17 --- /dev/null +++ b/src-tauri/src/services/plugin/settings/mod.rs @@ -0,0 +1,5 @@ +pub mod persistence; +pub mod settings_registry; + +pub use persistence::{JsonPluginStorage, PluginStorage}; +pub use settings_registry::SettingsRegistry; diff --git a/src-tauri/src/services/plugin/settings/persistence.rs b/src-tauri/src/services/plugin/settings/persistence.rs new file mode 100644 index 0000000..674382a --- /dev/null +++ b/src-tauri/src/services/plugin/settings/persistence.rs @@ -0,0 +1,92 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; + +use crate::errors::AppError; +use crate::services::plugin::runtime::state::PluginLifecycleState; +use plugin_sdk::settings::PluginSettings; + +pub trait PluginStorage: Send + Sync { + fn load_config( + &self, + ) -> Result< + ( + HashMap, + HashMap, + ), + AppError, + >; + fn save_config( + &self, + states: &HashMap, + settings: &HashMap, + ) -> Result<(), AppError>; +} + +#[derive(Clone, serde::Serialize, serde::Deserialize)] +struct PluginConfigFile { + states: HashMap, + settings: HashMap, +} + +pub struct JsonPluginStorage { + path: PathBuf, +} + +impl JsonPluginStorage { + pub fn new(app_data_dir: PathBuf) -> Self { + let dir = app_data_dir.join("plugins"); + Self { + path: dir.join("plugins.config.json"), + } + } +} + +impl PluginStorage for JsonPluginStorage { + fn load_config( + &self, + ) -> Result< + ( + HashMap, + HashMap, + ), + AppError, + > { + if !self.path.exists() { + return Ok((HashMap::new(), HashMap::new())); + } + + let content = std::fs::read_to_string(&self.path) + .map_err(|e| AppError::Io(format!("Failed to read plugin config: {}", e)))?; + + let file: PluginConfigFile = serde_json::from_str(&content) + .map_err(|e| AppError::Plugin(format!("Failed to parse plugin config: {}", e)))?; + + Ok((file.states, file.settings)) + } + + fn save_config( + &self, + states: &HashMap, + settings: &HashMap, + ) -> Result<(), AppError> { + if let Some(parent) = self.path.parent() { + std::fs::create_dir_all(parent).map_err(|e| { + AppError::Io(format!("Failed to create plugin config directory: {}", e)) + })?; + } + + let file = PluginConfigFile { + states: states.clone(), + settings: settings.clone(), + }; + + let content = serde_json::to_string_pretty(&file) + .map_err(|e| AppError::Plugin(format!("Failed to serialize plugin config: {}", e)))?; + + std::fs::write(&self.path, content) + .map_err(|e| AppError::Io(format!("Failed to write plugin config: {}", e)))?; + + Ok(()) + } +} diff --git a/src-tauri/src/services/plugin/settings/settings_registry.rs b/src-tauri/src/services/plugin/settings/settings_registry.rs new file mode 100644 index 0000000..f9b580b --- /dev/null +++ b/src-tauri/src/services/plugin/settings/settings_registry.rs @@ -0,0 +1,88 @@ +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use crate::errors::AppError; +use plugin_sdk::settings::{PluginSettings, SettingValue}; + +pub struct SettingsRegistry { + settings: RwLock>, +} + +impl SettingsRegistry { + pub fn new() -> Self { + Self { + settings: RwLock::new(HashMap::new()), + } + } + + pub fn register_defaults(&self, plugin_id: &str, defaults: HashMap) { + let mut lock = match self.settings.write() { + Ok(guard) => guard, + Err(_) => return, + }; + + let entry = lock + .entry(plugin_id.to_string()) + .or_insert_with(|| PluginSettings { + plugin_id: plugin_id.to_string(), + settings: HashMap::new(), + }); + + for (key, value) in defaults { + entry.settings.entry(key).or_insert(value); + } + } + + pub fn get_setting(&self, plugin_id: &str, key: &str) -> Option { + let lock = self.settings.read().ok()?; + lock.get(plugin_id)?.settings.get(key).cloned() + } + + pub fn get_all(&self, plugin_id: &str) -> HashMap { + let lock = match self.settings.read() { + Ok(guard) => guard, + Err(_) => return HashMap::new(), + }; + lock.get(plugin_id) + .map(|ps| ps.settings.clone()) + .unwrap_or_default() + } + + pub fn update_setting( + &self, + plugin_id: &str, + key: &str, + value: SettingValue, + ) -> Result<(), AppError> { + let mut lock = self + .settings + .write() + .map_err(|e| AppError::Plugin(e.to_string()))?; + + let entry = lock + .entry(plugin_id.to_string()) + .or_insert_with(|| PluginSettings { + plugin_id: plugin_id.to_string(), + settings: HashMap::new(), + }); + + entry.settings.insert(key.to_string(), value); + Ok(()) + } + + pub fn dump_all(&self) -> HashMap { + let lock = match self.settings.read() { + Ok(guard) => guard, + Err(_) => return HashMap::new(), + }; + lock.clone() + } + + pub fn load_all(&self, data: HashMap) { + let mut lock = match self.settings.write() { + Ok(guard) => guard, + Err(_) => return, + }; + *lock = data; + } +} diff --git a/src-tauri/src/services/recent_service.rs b/src-tauri/src/services/recent_service.rs new file mode 100644 index 0000000..d57914b --- /dev/null +++ b/src-tauri/src/services/recent_service.rs @@ -0,0 +1,42 @@ +use std::sync::Arc; + +use crate::errors::AppError; +use crate::models::RecentPlayedWithTrack; +use crate::repositories::sqlite::recent_repository::SqliteRecentRepository; + +const MAX_RECENT_RECORDS: i64 = 100; + +#[derive(Clone)] +pub struct RecentService { + recent: Arc, +} + +impl RecentService { + pub fn new(recent: Arc) -> Self { + Self { recent } + } + + pub async fn load( + &self, + limit: i64, + offset: i64, + ) -> Result, AppError> { + self.recent.list_with_tracks(limit, offset).await + } + + pub async fn add(&self, track_id: i64, played_at: String) -> Result<(), AppError> { + self.recent.upsert(track_id, played_at).await?; + + let count = self.recent.count().await?; + + if count > MAX_RECENT_RECORDS { + self.recent.remove_oldest(MAX_RECENT_RECORDS).await?; + } + + Ok(()) + } + + pub async fn clear(&self) -> Result<(), AppError> { + self.recent.clear().await + } +} diff --git a/src-tauri/src/services/settings_service.rs b/src-tauri/src/services/settings_service.rs new file mode 100644 index 0000000..d2ca0b0 --- /dev/null +++ b/src-tauri/src/services/settings_service.rs @@ -0,0 +1,78 @@ +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::errors::AppError; +use crate::models::{AppSettings, PluginLogLevel, SettingRow, ThemeMode}; +use crate::repositories::sqlite::settings_repository::SqliteSettingsRepository; + +#[derive(Clone)] +pub struct SettingsService { + settings: Arc, +} + +impl SettingsService { + pub fn new(settings: Arc) -> Self { + Self { settings } + } + + pub async fn get_settings(&self) -> Result { + if let Some(row) = self.settings.get().await? { + return row_to_settings(row); + } + + let settings = AppSettings::default(); + self.update_settings(settings.clone()).await?; + Ok(settings) + } + + pub async fn update_settings(&self, settings: AppSettings) -> Result { + let row = settings_to_row(settings.clone())?; + self.settings.update(row).await?; + Ok(settings) + } +} + +fn row_to_settings(row: SettingRow) -> Result { + Ok(AppSettings { + theme: ThemeMode::try_from(row.theme).map_err(AppError::Service)?, + volume: row.volume.clamp(0, 100) as u8, + scan_on_startup: row.scan_on_startup != 0, + reduce_motion: row.reduce_motion != 0, + library_dirs: serde_json::from_str(&row.library_dirs) + .map_err(|error| AppError::Service(error.to_string()))?, + use_album_artist_grouping: row.use_album_artist_grouping != 0, + plugin_dirs: serde_json::from_str(&row.plugin_dirs) + .map_err(|error| AppError::Service(error.to_string()))?, + plugin_dev_mode: row.plugin_dev_mode != 0, + plugin_scan_on_startup: row.plugin_scan_on_startup != 0, + plugin_log_level: PluginLogLevel::try_from(row.plugin_log_level) + .map_err(AppError::Service)?, + }) +} + +fn settings_to_row(settings: AppSettings) -> Result { + Ok(SettingRow { + id: 1, + theme: settings.theme.as_str().to_string(), + volume: settings.volume as i64, + scan_on_startup: i64::from(settings.scan_on_startup), + reduce_motion: i64::from(settings.reduce_motion), + library_dirs: serde_json::to_string(&settings.library_dirs) + .map_err(|error| AppError::Service(error.to_string()))?, + use_album_artist_grouping: i64::from(settings.use_album_artist_grouping), + plugin_dirs: serde_json::to_string(&settings.plugin_dirs) + .map_err(|error| AppError::Service(error.to_string()))?, + plugin_dev_mode: i64::from(settings.plugin_dev_mode), + plugin_scan_on_startup: i64::from(settings.plugin_scan_on_startup), + plugin_log_level: settings.plugin_log_level.as_str().to_string(), + created_at: now_string(), + updated_at: now_string(), + }) +} + +fn now_string() -> String { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|value| value.as_secs().to_string()) + .unwrap_or_else(|_| "0".to_string()) +} diff --git a/src-tauri/src/services/track_service.rs b/src-tauri/src/services/track_service.rs new file mode 100644 index 0000000..997ae9f --- /dev/null +++ b/src-tauri/src/services/track_service.rs @@ -0,0 +1,64 @@ +use std::sync::Arc; + +use crate::errors::AppError; +use crate::models::{NewTrack, Track, TrackSearchQuery, UpdateTrack}; +use crate::repositories::sqlite::track_repository::SqliteTrackRepository; + +#[derive(Clone)] +pub struct TrackService { + tracks: Arc, +} + +impl TrackService { + pub fn new(tracks: Arc) -> Self { + Self { tracks } + } + + pub async fn create_track(&self, track: NewTrack) -> Result { + self.tracks.create(track).await + } + + pub async fn upsert_track(&self, track: NewTrack) -> Result { + self.tracks.upsert_by_path(track).await + } + + pub async fn upsert_tracks(&self, tracks: Vec) -> Result, AppError> { + let mut saved = Vec::with_capacity(tracks.len()); + for track in tracks { + saved.push(self.tracks.upsert_by_path(track).await?); + } + Ok(saved) + } + + pub async fn update_track(&self, track: UpdateTrack) -> Result { + self.tracks.update(track).await + } + + pub async fn delete_track(&self, id: i64) -> Result<(), AppError> { + self.tracks.delete(id).await + } + + pub async fn delete_track_by_path(&self, path: &str) -> Result<(), AppError> { + self.tracks.delete_by_path(path).await + } + + pub async fn get_track(&self, id: i64) -> Result, AppError> { + self.tracks.find_by_id(id).await + } + + pub async fn get_track_by_path(&self, path: &str) -> Result, AppError> { + self.tracks.find_by_path(path).await + } + + pub async fn list_tracks(&self) -> Result, AppError> { + self.tracks.list_all().await + } + + pub async fn search_tracks(&self, query: TrackSearchQuery) -> Result, AppError> { + self.tracks.search(query).await + } + + pub async fn mark_track_played(&self, id: i64, played_at: String) -> Result<(), AppError> { + self.tracks.increment_play_count(id, played_at).await + } +} diff --git a/src-tauri/src/state/app_state.rs b/src-tauri/src/state/app_state.rs new file mode 100644 index 0000000..341b7cd --- /dev/null +++ b/src-tauri/src/state/app_state.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use crate::services::plugin::PluginManager; +use crate::services::{PlaylistService, RecentService, SettingsService, TrackService}; + +#[derive(Clone)] +pub struct AppState { + pub tracks: Arc, + pub playlists: Arc, + pub settings: Arc, + pub recent: Arc, + pub plugins: Arc, +} + +impl AppState { + pub fn new( + tracks: Arc, + playlists: Arc, + settings: Arc, + recent: Arc, + plugins: Arc, + ) -> Self { + Self { + tracks, + playlists, + settings, + recent, + plugins, + } + } +} diff --git a/src-tauri/src/state/mod.rs b/src-tauri/src/state/mod.rs new file mode 100644 index 0000000..21772f9 --- /dev/null +++ b/src-tauri/src/state/mod.rs @@ -0,0 +1,4 @@ +pub mod app_state; +pub mod playback_state; + +pub use app_state::AppState; diff --git a/src-tauri/src/state/playback_state.rs b/src-tauri/src/state/playback_state.rs new file mode 100644 index 0000000..7dbcdcd --- /dev/null +++ b/src-tauri/src/state/playback_state.rs @@ -0,0 +1,90 @@ +pub use crate::audio::lock_audio_state; +use crate::audio::AudioState; +use crate::errors::AppError; +use crate::models::playback::{NativeTrackMetadata, PlaybackStatusSnapshot}; +use crate::models::Track; + +pub fn with_audio_state(f: impl FnOnce(&mut AudioState) -> T) -> Result { + let value = { + let mut state = lock_audio_state()?; + f(&mut state) + }; + + Ok(value) +} + +pub fn current_playback_state(state: &mut AudioState) -> PlaybackStatusSnapshot { + match state.with_engine(|engine| (!engine.paused(), engine.current_time())) { + Ok((playing, current_time)) => { + state.playing = playing; + + PlaybackStatusSnapshot { + playing, + current_time, + } + } + + Err(_) => { + state.playing = false; + + PlaybackStatusSnapshot { + playing: false, + current_time: 0.0, + } + } + } +} + +pub fn current_playback_snapshot() -> Result { + with_audio_state(current_playback_state) +} + +pub fn current_time_from_state() -> f64 { + current_playback_snapshot() + .map(|state| state.current_time) + .unwrap_or(0.0) +} + +pub fn current_track_duration(state: &AudioState) -> Option { + let index = state.playback_queue.current_index?; + state + .playback_queue + .tracks + .get(index) + .map(|track| track.duration as f64 / 1000.0) +} + +pub fn should_advance_track() -> bool { + let Ok((snapshot, duration)) = with_audio_state(|state| { + let snapshot = current_playback_state(state); + let duration = current_track_duration(state); + (snapshot, duration) + }) else { + return false; + }; + + snapshot.playing + && duration + .map(|duration| duration > 0.0 && snapshot.current_time >= duration - 0.5) + .unwrap_or(false) +} + +pub fn metadata_from_track(track: &Track) -> NativeTrackMetadata { + NativeTrackMetadata { + title: track.title.clone(), + album: track + .album + .clone() + .unwrap_or_else(|| "未知专辑".to_string()), + artist: track + .artist + .clone() + .unwrap_or_else(|| "未知歌手".to_string()), + duration: Some(track.duration as f64 / 1000.0), + } +} + +pub fn sanitize_track(mut track: Track) -> Track { + track.cover = None; + track +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f3fcfe9..aaf6d3f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "tauri-app", + "productName": "Rust Echo Music", "version": "0.1.0", "identifier": "com.rustechomusic.app", "build": { @@ -12,19 +12,19 @@ "app": { "windows": [ { - "title": "tauri-app", + "title": "Rust Echo Music", "decorations": false, "width": 1200, "height": 900 } ], "security": { - "csp": null + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data: asset: http://asset.localhost; connect-src 'self' ipc: http://ipc.localhost; frame-src 'self' plugin:" } }, "bundle": { "active": true, - "targets": "all", + "targets": ["nsis", "msi", "dmg", "appimage"], "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -33,7 +33,15 @@ "icons/icon.ico" ], "resources": [ - "../static/**/*" - ] + "../static/*" + ], + "windows": { + "wix": { + "language": "zh-CN" + }, + "nsis": { + "installMode": "both" + } + } } -} \ No newline at end of file +} diff --git a/src/app.css b/src/app.css index 348e34e..cfd3d65 100644 --- a/src/app.css +++ b/src/app.css @@ -3,404 +3,413 @@ @import "tailwindcss/theme.css" layer(theme); /* @import "tailwindcss/preflight.css" layer(base); */ @import "tailwindcss/utilities.css" layer(utilities); -@import 'material-symbols'; - -* { - transition: all 0.2s ease; -} - -/* - 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) - 2. Remove default margins and padding - 3. Reset all borders. -*/ - -/* *, */ -html, -body, -::after, -::before, -::backdrop, -::file-selector-button { - box-sizing: border-box; /* 1 */ - margin: 0; /* 2 */ - padding: 0; /* 2 */ - border: 0 solid; /* 3 */ -} - -/* - 1. Use a consistent sensible line-height in all browsers. - 2. Prevent adjustments of font size after orientation changes in iOS. - 3. Use a more readable tab size. - 4. Use the user's configured `sans` font-family by default. - 5. Use the user's configured `sans` font-feature-settings by default. - 6. Use the user's configured `sans` font-variation-settings by default. - 7. Disable tap highlights on iOS. -*/ - -html, -:host { - line-height: 1.5; /* 1 */ - -webkit-text-size-adjust: 100%; /* 2 */ - tab-size: 4; /* 3 */ - font-family: --theme( - --default-font-family, - ui-sans-serif, - system-ui, - sans-serif, - 'Apple Color Emoji', - 'Segoe UI Emoji', - 'Segoe UI Symbol', - 'Noto Color Emoji' - ); /* 4 */ - font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */ - font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */ - -webkit-tap-highlight-color: transparent; /* 7 */ -} - -/* - 1. Add the correct height in Firefox. - 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) - 3. Reset the default border style to a 1px solid border. -*/ - -hr { - height: 0; /* 1 */ - color: inherit; /* 2 */ - border-top-width: 1px; /* 3 */ -} - -/* - Add the correct text decoration in Chrome, Edge, and Safari. -*/ - -abbr:where([title]) { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; -} - -/* - Remove the default font size and weight for headings. -*/ - -h1, -h2, -h3, -h4, -h5, -h6 { - font-size: inherit; - font-weight: inherit; -} - -/* - Reset links to optimize for opt-in styling instead of opt-out. -*/ - -a { - color: inherit; - -webkit-text-decoration: inherit; - text-decoration: inherit; -} - -/* - Add the correct font weight in Edge and Safari. -*/ - -b, -strong { - font-weight: bolder; -} - -/* - 1. Use the user's configured `mono` font-family by default. - 2. Use the user's configured `mono` font-feature-settings by default. - 3. Use the user's configured `mono` font-variation-settings by default. - 4. Correct the odd `em` font sizing in all browsers. -*/ - -code, -kbd, -samp, -pre { - font-family: --theme( - --default-mono-font-family, - ui-monospace, - SFMono-Regular, - Menlo, - Monaco, - Consolas, - 'Liberation Mono', - 'Courier New', - monospace - ); /* 1 */ - font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */ - font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */ - font-size: 1em; /* 4 */ -} - -/* - Add the correct font size in all browsers. -*/ - -small { - font-size: 80%; -} - -/* - Prevent `sub` and `sup` elements from affecting the line height in all browsers. -*/ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* - 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) - 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) - 3. Remove gaps between table borders by default. -*/ - -table { - text-indent: 0; /* 1 */ - border-color: inherit; /* 2 */ - border-collapse: collapse; /* 3 */ -} - -/* - Use the modern Firefox focus style for all focusable elements. -*/ - -:-moz-focusring { - outline: auto; -} - -/* - Add the correct vertical alignment in Chrome and Firefox. -*/ - -progress { - vertical-align: baseline; -} - -/* - Add the correct display in Chrome and Safari. -*/ - -summary { - display: list-item; -} - -/* - Make lists unstyled by default. -*/ - -ol, -ul, -menu { - list-style: none; -} - -/* - 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) - 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) - This can trigger a poorly considered lint error in some tools but is included by design. -*/ - -img, -svg, -video, -canvas, -audio, -iframe, -embed, -object { - display: block; /* 1 */ - vertical-align: middle; /* 2 */ -} - -/* - Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) -*/ - -img, -video { - max-width: 100%; - height: auto; -} - -/* - 1. Inherit font styles in all browsers. - 2. Remove border radius in all browsers. - 3. Remove background color in all browsers. - 4. Ensure consistent opacity for disabled states in all browsers. -*/ - -button, -input, -select, -optgroup, -textarea, -::file-selector-button { - font: inherit; /* 1 */ - font-feature-settings: inherit; /* 1 */ - font-variation-settings: inherit; /* 1 */ - letter-spacing: inherit; /* 1 */ - color: inherit; /* 1 */ - border-radius: 0; /* 2 */ - background-color: transparent; /* 3 */ - opacity: 1; /* 4 */ -} - -/* - Restore default font weight. -*/ - -:where(select:is([multiple], [size])) optgroup { - font-weight: bolder; -} - -/* - Restore indentation. -*/ - -:where(select:is([multiple], [size])) optgroup option { - padding-inline-start: 20px; -} - -/* - Restore space after button. -*/ - -::file-selector-button { - margin-inline-end: 4px; -} - -/* - Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) -*/ - -::placeholder { - opacity: 1; -} - -/* - Set the default placeholder color to a semi-transparent version of the current text color in browsers that do not - crash when using `color-mix(…)` with `currentcolor`. (https://github.com/tailwindlabs/tailwindcss/issues/17194) -*/ - -@supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or - (contain-intrinsic-size: 1px) /* Safari 17+ */ { +@import "./fonts.css" layer(theme); + +@layer base { + + * { + transition: all 0.2s ease; + } + + /* + 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) + 2. Remove default margins and padding + 3. Reset all borders. + */ + + /* *, */ + html, + body, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + box-sizing: border-box; /* 1 */ + margin: 0; /* 2 */ + padding: 0; /* 2 */ + border: 0 solid; /* 3 */ + } + + /* + 1. Use a consistent sensible line-height in all browsers. + 2. Prevent adjustments of font size after orientation changes in iOS. + 3. Use a more readable tab size. + 4. Use the user's configured `sans` font-family by default. + 5. Use the user's configured `sans` font-feature-settings by default. + 6. Use the user's configured `sans` font-variation-settings by default. + 7. Disable tap highlights on iOS. + */ + + html, + :host { + line-height: 1.5; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + tab-size: 4; /* 3 */ + font-family: --theme( + --default-font-family, + ui-sans-serif, + system-ui, + sans-serif, + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji' + ); /* 4 */ + font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */ + font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */ + -webkit-tap-highlight-color: transparent; /* 7 */ + background-color: theme(--color-gray-100); + } + + /* + 1. Add the correct height in Firefox. + 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) + 3. Reset the default border style to a 1px solid border. + */ + + hr { + height: 0; /* 1 */ + color: inherit; /* 2 */ + border-top-width: 1px; /* 3 */ + } + + /* + Add the correct text decoration in Chrome, Edge, and Safari. + */ + + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + + /* + Remove the default font size and weight for headings. + */ + + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: inherit; + font-weight: inherit; + } + + p { + margin: 0; + padding: 0; + } + + /* + Reset links to optimize for opt-in styling instead of opt-out. + */ + + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + + /* + Add the correct font weight in Edge and Safari. + */ + + b, + strong { + font-weight: bolder; + } + + /* + 1. Use the user's configured `mono` font-family by default. + 2. Use the user's configured `mono` font-feature-settings by default. + 3. Use the user's configured `mono` font-variation-settings by default. + 4. Correct the odd `em` font sizing in all browsers. + */ + + code, + kbd, + samp, + pre { + font-family: --theme( + --default-mono-font-family, + ui-monospace, + SFMono-Regular, + Menlo, + Monaco, + Consolas, + 'Liberation Mono', + 'Courier New', + monospace + ); /* 1 */ + font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */ + font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */ + font-size: 1em; /* 4 */ + } + + /* + Add the correct font size in all browsers. + */ + + small { + font-size: 80%; + } + + /* + Prevent `sub` and `sup` elements from affecting the line height in all browsers. + */ + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + /* + 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) + 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) + 3. Remove gaps between table borders by default. + */ + + table { + text-indent: 0; /* 1 */ + border-color: inherit; /* 2 */ + border-collapse: collapse; /* 3 */ + } + + /* + Use the modern Firefox focus style for all focusable elements. + */ + + :-moz-focusring { + outline: auto; + } + + /* + Add the correct vertical alignment in Chrome and Firefox. + */ + + progress { + vertical-align: baseline; + } + + /* + Add the correct display in Chrome and Safari. + */ + + summary { + display: list-item; + } + + /* + Make lists unstyled by default. + */ + + ol, + ul, + menu { + list-style: none; + } + + /* + 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) + 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. + */ + + img, + svg, + video, + canvas, + audio, + iframe, + embed, + object { + display: block; /* 1 */ + vertical-align: middle; /* 2 */ + } + + /* + Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) + */ + + img, + video { + max-width: 100%; + height: auto; + } + + /* + 1. Inherit font styles in all browsers. + 2. Remove border radius in all browsers. + 3. Remove background color in all browsers. + 4. Ensure consistent opacity for disabled states in all browsers. + */ + + button, + input, + select, + optgroup, + textarea, + ::file-selector-button { + font: inherit; /* 1 */ + font-feature-settings: inherit; /* 1 */ + font-variation-settings: inherit; /* 1 */ + letter-spacing: inherit; /* 1 */ + color: inherit; /* 1 */ + border-radius: 0; /* 2 */ + background-color: transparent; /* 3 */ + opacity: 1; /* 4 */ + } + + /* + Restore default font weight. + */ + + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + + /* + Restore indentation. + */ + + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + + /* + Restore space after button. + */ + + ::file-selector-button { + margin-inline-end: 4px; + } + + /* + Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) + */ + ::placeholder { - color: color-mix(in oklab, currentcolor 50%, transparent); - } -} - -/* - Prevent resizing textareas horizontally by default. -*/ - -textarea { - resize: vertical; -} - -/* - Remove the inner padding in Chrome and Safari on macOS. -*/ - -::-webkit-search-decoration { - -webkit-appearance: none; -} - -/* - 1. Ensure date/time inputs have the same height when empty in iOS Safari. - 2. Ensure text alignment can be changed on date/time inputs in iOS Safari. -*/ - -::-webkit-date-and-time-value { - min-height: 1lh; /* 1 */ - text-align: inherit; /* 2 */ -} - -/* - Prevent height from changing on date/time inputs in macOS Safari when the input is set to `display: block`. -*/ - -::-webkit-datetime-edit { - display: inline-flex; -} - -/* - Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers. -*/ - -::-webkit-datetime-edit-fields-wrapper { - padding: 0; -} - -::-webkit-datetime-edit, -::-webkit-datetime-edit-year-field, -::-webkit-datetime-edit-month-field, -::-webkit-datetime-edit-day-field, -::-webkit-datetime-edit-hour-field, -::-webkit-datetime-edit-minute-field, -::-webkit-datetime-edit-second-field, -::-webkit-datetime-edit-millisecond-field, -::-webkit-datetime-edit-meridiem-field { - padding-block: 0; -} - -/* - Center dropdown marker shown on inputs with paired ``s in Chrome. (https://github.com/tailwindlabs/tailwindcss/issues/18499) -*/ - -::-webkit-calendar-picker-indicator { - line-height: 1; -} - -/* - Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) -*/ - -:-moz-ui-invalid { - box-shadow: none; -} - -/* - Correct the inability to style the border radius in iOS Safari. -*/ - -button, -input:where([type='button'], [type='reset'], [type='submit']), -::file-selector-button { - appearance: button; -} - -/* - Correct the cursor style of increment and decrement buttons in Safari. -*/ - -::-webkit-inner-spin-button, -::-webkit-outer-spin-button { - height: auto; -} - -/* - Make elements with the HTML hidden attribute stay hidden by default. -*/ - -[hidden]:where(:not([hidden='until-found'])) { - display: none !important; + opacity: 1; + } + + /* + Set the default placeholder color to a semi-transparent version of the current text color in browsers that do not + crash when using `color-mix(…)` with `currentcolor`. (https://github.com/tailwindlabs/tailwindcss/issues/17194) + */ + + @supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or + (contain-intrinsic-size: 1px) /* Safari 17+ */ { + ::placeholder { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + + /* + Prevent resizing textareas horizontally by default. + */ + + textarea { + resize: vertical; + } + + /* + Remove the inner padding in Chrome and Safari on macOS. + */ + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + /* + 1. Ensure date/time inputs have the same height when empty in iOS Safari. + 2. Ensure text alignment can be changed on date/time inputs in iOS Safari. + */ + + ::-webkit-date-and-time-value { + min-height: 1lh; /* 1 */ + text-align: inherit; /* 2 */ + } + + /* + Prevent height from changing on date/time inputs in macOS Safari when the input is set to `display: block`. + */ + + ::-webkit-datetime-edit { + display: inline-flex; + } + + /* + Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers. + */ + + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + + ::-webkit-datetime-edit, + ::-webkit-datetime-edit-year-field, + ::-webkit-datetime-edit-month-field, + ::-webkit-datetime-edit-day-field, + ::-webkit-datetime-edit-hour-field, + ::-webkit-datetime-edit-minute-field, + ::-webkit-datetime-edit-second-field, + ::-webkit-datetime-edit-millisecond-field, + ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + + /* + Center dropdown marker shown on inputs with paired ``s in Chrome. (https://github.com/tailwindlabs/tailwindcss/issues/18499) + */ + + ::-webkit-calendar-picker-indicator { + line-height: 1; + } + + /* + Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) + */ + + :-moz-ui-invalid { + box-shadow: none; + } + + /* + Correct the inability to style the border radius in iOS Safari. + */ + + button, + input:where([type='button'], [type='reset'], [type='submit']), + ::file-selector-button { + appearance: button; + } + + /* + Correct the cursor style of increment and decrement buttons in Safari. + */ + + ::-webkit-inner-spin-button, + ::-webkit-outer-spin-button { + height: auto; + } + + /* + Make elements with the HTML hidden attribute stay hidden by default. + */ + + [hidden]:where(:not([hidden='until-found'])) { + display: none !important; + } } \ No newline at end of file diff --git a/src/app.html b/src/app.html index d8b86cf..7a8b585 100644 --- a/src/app.html +++ b/src/app.html @@ -6,36 +6,7 @@ - Tauri + SvelteKit + Typescript App - - - - - - - - - - - - - - + Rust Echo Music %sveltekit.head% diff --git a/src/fonts.css b/src/fonts.css new file mode 100644 index 0000000..45bd03b --- /dev/null +++ b/src/fonts.css @@ -0,0 +1,71 @@ +/* fallback */ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: url(/fonts/Material_Icons.woff2) format('woff2'); +} + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; +} + +/* fallback */ +@font-face { + font-family: 'Material Icons Outlined'; + font-style: normal; + font-weight: 400; + src: url(/fonts/Material_Icons_Outlined.woff2) format('woff2'); +} + +.material-icons-outlined { + font-family: 'Material Icons Outlined'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; +} + +/* fallback */ +@font-face { + font-family: 'Material Icons Round'; + font-style: normal; + font-weight: 400; + src: url(/fonts/Material_Icons_Round.woff2) format('woff2'); +} + +.material-icons-round { + font-family: 'Material Icons Round'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; +} \ No newline at end of file diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte deleted file mode 100644 index d9319b5..0000000 --- a/src/lib/components/Header.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - -
-
Header PlaceHolder
- { - if (e.key === 'Enter' || e.key === ' ') playMusic() - }} - class="px-4 py-2" - role="none" - > - 播放音乐 - -
diff --git a/src/lib/components/NavRail.svelte b/src/lib/components/NavRail.svelte deleted file mode 100644 index ef952e9..0000000 --- a/src/lib/components/NavRail.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - Recent - Library - Track - Artist - - - diff --git a/src/lib/components/PlayerBar.svelte b/src/lib/components/PlayerBar.svelte deleted file mode 100644 index 1e37292..0000000 --- a/src/lib/components/PlayerBar.svelte +++ /dev/null @@ -1,82 +0,0 @@ - - - - - -
-
{playerState.current?.title}
-
{playerState.current?.artist}
-
- -
-
-
- {playerState.current?.title ?? '未在播放'} -
-
- {playerState.current?.artist ?? '未知艺术家'} -
-
- -
- e.key === 'Enter' && prev()} - > - e.key === 'Enter' && toggle()} - > - - e.key === 'Enter' && next()} - > -
- -
- - - -
-
-
diff --git a/src/lib/components/PlaylistCard.svelte b/src/lib/components/PlaylistCard.svelte deleted file mode 100644 index eed8668..0000000 --- a/src/lib/components/PlaylistCard.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - -
- -
- {playlist.name} -
-
-

{playlist.name}

-

播放列表

-
-
-
\ No newline at end of file diff --git a/src/lib/components/Playlists.svelte b/src/lib/components/Playlists.svelte deleted file mode 100644 index 7020867..0000000 --- a/src/lib/components/Playlists.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - -
-
- {#each playlists as playlist} - - {/each} -
-
\ No newline at end of file diff --git a/src/lib/components/Progress.svelte b/src/lib/components/Progress.svelte deleted file mode 100644 index 0d01747..0000000 --- a/src/lib/components/Progress.svelte +++ /dev/null @@ -1,147 +0,0 @@ - - -
-
-
-
- -
-
- - \ No newline at end of file diff --git a/src/lib/components/RecentSongs.svelte b/src/lib/components/RecentSongs.svelte deleted file mode 100644 index 1b9b6b2..0000000 --- a/src/lib/components/RecentSongs.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - -
-
- {#each songs as song} - - {/each} -
-
\ No newline at end of file diff --git a/src/lib/components/Slider.svelte b/src/lib/components/Slider.svelte deleted file mode 100644 index eebc193..0000000 --- a/src/lib/components/Slider.svelte +++ /dev/null @@ -1,54 +0,0 @@ - - - diff --git a/src/lib/components/SongCard.svelte b/src/lib/components/SongCard.svelte deleted file mode 100644 index adbe7c7..0000000 --- a/src/lib/components/SongCard.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - - -
- {song.title} -
-
-

{song.title}

-

- {song.artist} - {song.album} -

-
-
- - play_arrow - - - more_vert - -
-
\ No newline at end of file diff --git a/src/lib/components/base/Button.svelte b/src/lib/components/base/Button.svelte new file mode 100644 index 0000000..c59c789 --- /dev/null +++ b/src/lib/components/base/Button.svelte @@ -0,0 +1,14 @@ + + + onclick()} + onkeydown={(e: KeyboardEvent) => + (e.key === 'Enter' || e.key === ' ') && onclick()} + role="button" + tabindex="0" + {...props} +> + {@render children()} + diff --git a/src/lib/components/base/Heading.svelte b/src/lib/components/base/Heading.svelte new file mode 100644 index 0000000..ac1b195 --- /dev/null +++ b/src/lib/components/base/Heading.svelte @@ -0,0 +1,30 @@ + + +
+
+

+ {eyebrow} +

+ +

+ {title} +

+
+
diff --git a/src/lib/components/base/IconButton.svelte b/src/lib/components/base/IconButton.svelte new file mode 100644 index 0000000..bc094df --- /dev/null +++ b/src/lib/components/base/IconButton.svelte @@ -0,0 +1,18 @@ + + + onclick(event)} + onkeydown={(event: KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + onclick(event) + } + }} + role="button" + tabindex="0" + {...props} +> \ No newline at end of file diff --git a/src/lib/components/base/MduiSlider.svelte b/src/lib/components/base/MduiSlider.svelte new file mode 100644 index 0000000..673e1bd --- /dev/null +++ b/src/lib/components/base/MduiSlider.svelte @@ -0,0 +1,109 @@ + + + \ No newline at end of file diff --git a/src/lib/components/base/PlayingIndicator.svelte b/src/lib/components/base/PlayingIndicator.svelte new file mode 100644 index 0000000..b4ef748 --- /dev/null +++ b/src/lib/components/base/PlayingIndicator.svelte @@ -0,0 +1,32 @@ + + +
+ + + +
+ + \ No newline at end of file diff --git a/src/lib/components/base/SearchBar.svelte b/src/lib/components/base/SearchBar.svelte new file mode 100644 index 0000000..3a4e61a --- /dev/null +++ b/src/lib/components/base/SearchBar.svelte @@ -0,0 +1,32 @@ + + +
e.stopPropagation()} + role="searchbox" + tabindex="0" +> + + + {#if value} +
value = ''} + onkeydown={e => { if (e.key === 'Enter' || e.key === ' ') value = '' }} + role="button" + tabindex="0" + > + +
+ {/if} +
\ No newline at end of file diff --git a/src/lib/components/base/SearchField.svelte b/src/lib/components/base/SearchField.svelte new file mode 100644 index 0000000..79df8ad --- /dev/null +++ b/src/lib/components/base/SearchField.svelte @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/src/lib/components/base/Select.svelte b/src/lib/components/base/Select.svelte new file mode 100644 index 0000000..7aced8f --- /dev/null +++ b/src/lib/components/base/Select.svelte @@ -0,0 +1,73 @@ + + +
+ (opened = false)} + aria-expanded={opened} + {...props} + > + {#each options as option (option.value)} + + {option.label} + + {/each} + +
diff --git a/src/lib/components/base/Slider.svelte b/src/lib/components/base/Slider.svelte new file mode 100644 index 0000000..33553c8 --- /dev/null +++ b/src/lib/components/base/Slider.svelte @@ -0,0 +1,338 @@ + + +
+
+
+
+ +
+ +
+ {Math.round(value)} +
+
+ + diff --git a/src/lib/components/base/TextField.svelte b/src/lib/components/base/TextField.svelte new file mode 100644 index 0000000..a4fe110 --- /dev/null +++ b/src/lib/components/base/TextField.svelte @@ -0,0 +1,56 @@ + + + \ No newline at end of file diff --git a/src/lib/components/media/MediaGrid.svelte b/src/lib/components/media/MediaGrid.svelte new file mode 100644 index 0000000..8af3cce --- /dev/null +++ b/src/lib/components/media/MediaGrid.svelte @@ -0,0 +1,227 @@ + + +{#snippet mediaCardContent(item: MediaGridItem, isSelected: boolean)} +
+ {#if item.image} + {item.title} + {:else} +
+ {#if item.shape === 'circle'} + 👤 + {:else} + 🎵 + {/if} +
+ {/if} + + {#if onplay} +
+ handlePlay(item, event)} + onkeydown={(event: KeyboardEvent) => handlePlayKeydown(item, event)} + > +
+ {/if} +
+ +
+

+ {item.title} +

+ + {#if item.subtitle} +

+ {item.subtitle} +

+ {/if} +
+{/snippet} + +{#snippet mediaCard(item: MediaGridItem)} + {@const isSelected = selectedId === item.id} + {#if onselect} + handleSelect(item)} + onkeydown={(event: KeyboardEvent) => handleKeydown(item, event)} + role="button" + tabindex="0" + > + {@render mediaCardContent(item, isSelected)} + + {:else} + + {@render mediaCardContent(item, isSelected)} + + {/if} +{/snippet} + +{#if items.length === 0} +
+
🎧
+
{emptyTitle}
+
+ {emptyDescription} +
+
+{:else} +
+
+ {#each $virtualizer.getVirtualItems() as row (row.index)} + {@const startIndex = row.index * columns} + {@const rowItems = items.slice(startIndex, startIndex + columns)} + +
+ {#each rowItems as item (item.id)} + {@render mediaCard(item)} + {/each} +
+ {/each} +
+
+{/if} \ No newline at end of file diff --git a/src/lib/features/EqPanel.svelte b/src/lib/features/EqPanel.svelte new file mode 100644 index 0000000..b45b652 --- /dev/null +++ b/src/lib/features/EqPanel.svelte @@ -0,0 +1,62 @@ + + +
+
+ 均衡器 + + equalizer.setEnabled((e.currentTarget as HTMLInputElement).checked)} + > +
+ +
\ No newline at end of file diff --git a/src/lib/features/LyricsPanel.svelte b/src/lib/features/LyricsPanel.svelte new file mode 100644 index 0000000..75458f5 --- /dev/null +++ b/src/lib/features/LyricsPanel.svelte @@ -0,0 +1,80 @@ + + +
+
+ + 歌词 + + (pluginState.activeNativePanel = null)} + /> +
+ +
+ {#if lyrics.currentLyrics && lyrics.currentLyrics.lines.length > 0} +
+ {#each lyrics.currentLyrics.lines as line, i (line.timestampMs + i)} +
+ {line.text || '···'} +
+ {/each} +
+ {:else} +
+ 暂无歌词 +
+ {/if} +
+
diff --git a/src/lib/features/PlayerBar.svelte b/src/lib/features/PlayerBar.svelte new file mode 100644 index 0000000..c23c00e --- /dev/null +++ b/src/lib/features/PlayerBar.svelte @@ -0,0 +1,164 @@ + + +{#snippet progressArea()} +
+ + {formatTime(player.currentTime)} + + +
+ +
+ + + {formatTime((player.currentTrack?.duration ?? 0) / 1000)} + +
+{/snippet} + +{#snippet nowPlayingInfo()} +
+ Album Cover + +
+
+ {player.currentTrack?.title ?? '未在播放'} +
+ +
+ {player.currentTrack?.artist ?? '未知艺术家'} +
+
+
+{/snippet} + +{#snippet transportControls()} +
+ player.prev()} + /> + + player.toggle()} + /> + + player.next()} /> +
+{/snippet} + +{#snippet volumeControls()} +
+ player.cyclePlayMode()} + class="opacity-70 hover:opacity-100" + /> + + {#each pluginState.nativeViewExtensions.filter(nv => nv.state === 'Enabled') as nv (nv.pluginId + nv.id)} + toggleNativePanel(nv.pluginId)} + class={pluginState.activeNativePanel === nv.pluginId ? 'text-[rgb(var(--mdui-color-primary))]' : 'opacity-70 hover:opacity-100'} + /> + {/each} + + player.toggleQueue()} + class={player.queueOpen ? 'text-[rgb(var(--mdui-color-primary))]' : 'opacity-70 hover:opacity-100'} + /> + + (player.muted = !player.muted)} + class="text-lg opacity-70 hover:opacity-100" + /> + + +
+{/snippet} + + + {@render progressArea()} +
+ {@render nowPlayingInfo()} + {@render transportControls()} + {@render volumeControls()} +
+
diff --git a/src/lib/features/PlaylistGrid.svelte b/src/lib/features/PlaylistGrid.svelte new file mode 100644 index 0000000..b710cac --- /dev/null +++ b/src/lib/features/PlaylistGrid.svelte @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/src/lib/features/PluginNativeViewHost.svelte b/src/lib/features/PluginNativeViewHost.svelte new file mode 100644 index 0000000..a4ce671 --- /dev/null +++ b/src/lib/features/PluginNativeViewHost.svelte @@ -0,0 +1,34 @@ + + +{#if Comp} + +{:else if loadError} +
+ {loadError} +
+{:else} +
+ +
+{/if} diff --git a/src/lib/features/QueueDrawer.svelte b/src/lib/features/QueueDrawer.svelte new file mode 100644 index 0000000..fde4644 --- /dev/null +++ b/src/lib/features/QueueDrawer.svelte @@ -0,0 +1,107 @@ + + + { + player.queueOpen = false + }} + class={[ + 'mt-32', + 'h-[calc(100vh-var(--spacing)*(24+14+20))]', + '[&::part(panel)]:mt-20', + '[&::part(overlay)]:bg-transparent', + ]} +> +
+
+
+ + 播放队列 + + + {player.queue.tracks.length} + +
+
+ + { + player.queueOpen = false + }} + /> +
+
+ +
+ {#if player.queue.tracks.length === 0} +
+ 🎵 +

队列为空

+
+ {:else} + + {/if} +
+
+
+ + diff --git a/src/lib/features/settings/ListEditor.svelte b/src/lib/features/settings/ListEditor.svelte new file mode 100644 index 0000000..3c73fd7 --- /dev/null +++ b/src/lib/features/settings/ListEditor.svelte @@ -0,0 +1,61 @@ + + +
+ {#each value as item, i (i)} +
+ { + const target = e.currentTarget as HTMLInputElement + updateItem(i, target.value) + }} + role="textbox" + tabindex="0" + style="flex: 1;" + > + removeItem(i)} + onkeydown={(e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') removeItem(i) + }} + role="button" + tabindex="0" + > +
+ {/each} + { + if (e.key === 'Enter' || e.key === ' ') addItem() + }} + role="button" + tabindex="0">添加 +
diff --git a/src/lib/features/settings/SettingsListItem.svelte b/src/lib/features/settings/SettingsListItem.svelte new file mode 100644 index 0000000..8497606 --- /dev/null +++ b/src/lib/features/settings/SettingsListItem.svelte @@ -0,0 +1,41 @@ + + +
+
+
+ {title} +
+ + {#if description} +
+ {description} +
+ {/if} +
+ + {#if children} +
+ {@render children()} +
+ {/if} +
\ No newline at end of file diff --git a/src/lib/features/settings/SettingsRow.svelte b/src/lib/features/settings/SettingsRow.svelte new file mode 100644 index 0000000..ec9f3ea --- /dev/null +++ b/src/lib/features/settings/SettingsRow.svelte @@ -0,0 +1,37 @@ + + +
+
+
{title}
+ + {#if description} +
+ {description} +
+ {/if} +
+ + {#if children} +
+ {@render children()} +
+ {/if} +
\ No newline at end of file diff --git a/src/lib/features/settings/SettingsSection.svelte b/src/lib/features/settings/SettingsSection.svelte new file mode 100644 index 0000000..003da9a --- /dev/null +++ b/src/lib/features/settings/SettingsSection.svelte @@ -0,0 +1,33 @@ + + +
+

+ {title} +

+ + {#if children} +
+ {@render children()} +
+ {/if} +
\ No newline at end of file diff --git a/src/lib/components/Appbar.svelte b/src/lib/features/shell/Appbar.svelte similarity index 88% rename from src/lib/components/Appbar.svelte rename to src/lib/features/shell/Appbar.svelte index 6cfcf1f..6c2477d 100644 --- a/src/lib/components/Appbar.svelte +++ b/src/lib/features/shell/Appbar.svelte @@ -1,4 +1,6 @@ + + + { + if (e.key === 'Enter' || e.key === ' ') { + scanDirectory() + } + }} + role="button" + tabindex="0" + > + + {#each navItems as item (item.href)} + + {item.label} + + {/each} + diff --git a/src/lib/features/track-list/TrackContextMenu.svelte b/src/lib/features/track-list/TrackContextMenu.svelte new file mode 100644 index 0000000..02571c9 --- /dev/null +++ b/src/lib/features/track-list/TrackContextMenu.svelte @@ -0,0 +1,231 @@ + + +{#if open && track} + +{/if} + + diff --git a/src/lib/features/track-list/TrackList.svelte b/src/lib/features/track-list/TrackList.svelte new file mode 100644 index 0000000..16ebca8 --- /dev/null +++ b/src/lib/features/track-list/TrackList.svelte @@ -0,0 +1,235 @@ + + + + + + + {#if !hideHeader} + + {#if selectable} +
+ {/if} + + {#if columns.includes("index")} +
#
+ {/if} + + {#if columns.includes("title")} +
标题
+ {/if} + + {#if columns.includes("album")} + + {/if} + + {#if columns.includes("duration")} +
时长
+ {/if} +
+ {/if} + + +
+ +
+ {#each $virtualizer.getVirtualItems() as virtualItem (virtualItem.key)} + {@const track = tracks[virtualItem.index]} +
+ setTrackSelected(track, checked)} + {ondblclicktrack} + {onremovetrack} + onplay={() => playTrack(track, virtualItem.index)} + oncontextmenu={(e) => openTrackContextMenu(e, track, virtualItem.index)} + /> +
+ {/each} +
+
+
+ + + + \ No newline at end of file diff --git a/src/lib/features/track-list/TrackListRow.svelte b/src/lib/features/track-list/TrackListRow.svelte new file mode 100644 index 0000000..7a9d1c5 --- /dev/null +++ b/src/lib/features/track-list/TrackListRow.svelte @@ -0,0 +1,308 @@ + + +{#snippet trackIndexCell()} +
+ {#if isCurrent && playing} + + {:else} + + {index + 1} + + {/if} +
+{/snippet} + +{#snippet trackCoverCell(cover: string | null)} +
+ {#if cover} + cover + {:else} +
+ 🎵 +
+ {/if} + + {#if !columns.includes("index") && isCurrent} +
+ {#if playing} + + {:else} + + {/if} +
+ {/if} +
+{/snippet} + +{#snippet trackTitleCell(cover: string | null)} +
+ {@render trackCoverCell(cover)} + +
+ + {track.title} + + + + {track.artist ?? "未知歌手"} + +
+
+{/snippet} + +{#snippet trackAlbumCell()} + +{/snippet} + +{#snippet trackDurationCell()} +
+
+ {formatDuration(track.duration / 1000)} +
+ {#if onremovetrack} + { + e.stopPropagation(); + onremovetrack?.(track, index); + }} + /> + {/if} +
+{/snippet} + + + + +
+ {#if selectable} +
+ +
+ {/if} + + {#if columns.includes("index")} + {@render trackIndexCell()} + {/if} + + {#if columns.includes("title")} + {@render trackTitleCell(cover)} + {/if} + + {#if columns.includes("album")} + {@render trackAlbumCell()} + {/if} + + {#if columns.includes("duration")} + {@render trackDurationCell()} + {/if} +
+
+ + diff --git a/src/lib/player.svelte.ts b/src/lib/player.svelte.ts deleted file mode 100644 index 20ebdda..0000000 --- a/src/lib/player.svelte.ts +++ /dev/null @@ -1,19 +0,0 @@ -type PlayerState = { - current: Track | null - isPlaying: boolean - playlist: Track[] - progress: number -} - -type Track = { - title: string - artist: string - path: string -} - -export const playerState = $state({ - current: null, - isPlaying: false, - playlist: [], - progress: 0, -}) diff --git a/src/lib/plugins/component-registry.ts b/src/lib/plugins/component-registry.ts new file mode 100644 index 0000000..6b92800 --- /dev/null +++ b/src/lib/plugins/component-registry.ts @@ -0,0 +1,18 @@ +import type { Component } from 'svelte' + +type ComponentLoader = () => Promise<{ default: Component }> + +const registry = new Map() + +export function registerNativeView(token: string, loader: ComponentLoader): void { + if (registry.has(token)) { + console.warn(`[component-registry] token "${token}" already registered, overriding`) + } + registry.set(token, loader) +} + +export function resolveNativeView(token: string): Promise<{ default: Component } | null> { + const loader = registry.get(token) + if (!loader) return Promise.resolve(null) + return loader() +} diff --git a/src/lib/plugins/host-bridge.ts b/src/lib/plugins/host-bridge.ts new file mode 100644 index 0000000..611a752 --- /dev/null +++ b/src/lib/plugins/host-bridge.ts @@ -0,0 +1,311 @@ +/** + * Plugin Host Bridge + * + * 宿主端 postMessage 桥:负责将沙箱 iframe 内的插件 SDK 请求路由到对应的 + * Tauri 命令/前端状态,并把宿主事件广播给所有已挂载的插件 iframe。 + * + * 协议信封与 `static/plugin-sdk/plugin-host-sdk.js` 完全对齐: + * { source: 'rem-plugin-host' | 'rem-plugin', type: string, id?: string, payload?: unknown } + */ + +import { invoke } from '@tauri-apps/api/core' +import { listen, type UnlistenFn } from '@tauri-apps/api/event' +import { pluginState } from '$lib/state/plugins.svelte' +import { player } from '$lib/state/player.svelte' +import type { GlobalAppEvent, PluginSetting } from '$lib/types' + +const HOST_SOURCE = 'rem-plugin-host' +const PLUGIN_SOURCE = 'rem-plugin' + +export type PluginEventKind = + | 'trackChanged' + | 'playbackStateChanged' + | 'queueChanged' + | 'settingsChanged' + +type StateKind = 'playback' | 'track' | 'queue' + +interface PluginEnvelope { + source: 'rem-plugin' | 'rem-plugin-host' + type: string + id?: string + payload?: unknown +} + +interface Attachment { + pluginId: string + messageListener: (e: MessageEvent) => void + loadListener: () => void +} + +class PluginHostBridge { + #attachments = new Map() + #eventUnlisten: UnlistenFn | null = null + #started = false + + constructor() { + this.#startEventSubscription() + } + + /** + * 模块加载时启动一次 Tauri 事件订阅,将后端事件桥接为 + * 插件侧的 `event` 广播。单例,全程存活。 + */ + #startEventSubscription(): void { + if (this.#started) return + this.#started = true + + void listen('global-app-event', event => { + const { type, payload } = event.payload + switch (type) { + case 'TrackStarted': + this.broadcastEvent('trackChanged', payload) + break + case 'PlaybackStateChanged': + this.broadcastEvent('playbackStateChanged', payload) + break + case 'QueueChanged': + this.broadcastEvent('queueChanged', payload) + break + case 'SettingsChanged': + this.broadcastEvent('settingsChanged', payload) + break + // PlaybackProgress / VolumeChanged / LyricsLoaded 不广播给插件 + } + }).then(unlisten => { + this.#eventUnlisten = unlisten + }).catch(err => { + console.error('[host-bridge] 订阅 global-app-event 失败:', err) + }) + } + + /** + * 绑定 iframe,开始监听其 postMessage。返回清理函数。 + * 在 iframe `load` 时主动发送 `ready` 握手。 + */ + attach(iframe: HTMLIFrameElement, pluginId: string): () => void { + // 已绑定过则先解绑,避免重复监听 + if (this.#attachments.has(iframe)) { + this.detach(iframe) + } + + const messageListener = (e: MessageEvent) => { + // 来源校验:只接受本 iframe 内发出的消息(防越权) + if (e.source !== iframe.contentWindow) return + const data = e.data as PluginEnvelope | undefined + // 信封校验:必须是插件信封 + if (!data || data.source !== PLUGIN_SOURCE) return + void this.#handleMessage(iframe, pluginId, data) + } + + const loadListener = () => { + const capabilities = this.#getCapabilities(pluginId) + this.sendReady(iframe, pluginId, capabilities) + } + + window.addEventListener('message', messageListener) + iframe.addEventListener('load', loadListener) + + this.#attachments.set(iframe, { + pluginId, + messageListener, + loadListener, + }) + + // 兜底:若 iframe 已加载完成(load 事件不会再触发), + // 立即发一次 ready。SDK 对重复 ready 幂等处理。 + if (iframe.contentWindow) { + loadListener() + } + + return () => this.detach(iframe) + } + + /** 解绑 iframe,移除所有监听 */ + detach(iframe: HTMLIFrameElement): void { + const att = this.#attachments.get(iframe) + if (!att) return + window.removeEventListener('message', att.messageListener) + iframe.removeEventListener('load', att.loadListener) + this.#attachments.delete(iframe) + } + + /** 向指定 iframe 发送 ready 握手消息 */ + sendReady( + iframe: HTMLIFrameElement, + pluginId: string, + capabilities: string[], + ): void { + const win = iframe.contentWindow + if (!win) return + const envelope: PluginEnvelope = { + source: HOST_SOURCE, + type: 'ready', + payload: { pluginId, capabilities }, + } + win.postMessage(envelope, '*') + } + + /** 向所有已 attach 的 iframe 广播事件 */ + broadcastEvent(kind: PluginEventKind, data: unknown): void { + if (this.#attachments.size === 0) return + const envelope: PluginEnvelope = { + source: HOST_SOURCE, + type: 'event', + payload: { kind, data }, + } + for (const iframe of this.#attachments.keys()) { + const win = iframe.contentWindow + if (!win) continue + win.postMessage(envelope, '*') + } + } + + /** 从 manifest 中读取插件 permissions 作为 capabilities */ + #getCapabilities(pluginId: string): string[] { + const manifest = pluginState.manifests.find(m => m.id === pluginId) + return manifest?.permissions ?? [] + } + + /** 路由插件请求到对应 Tauri 命令或前端状态,并回写响应 */ + async #handleMessage( + iframe: HTMLIFrameElement, + pluginId: string, + env: PluginEnvelope, + ): Promise { + const { type, id, payload } = env + + switch (type) { + case 'command': { + try { + const p = (payload ?? {}) as { command: string; args?: unknown } + const result = await invoke('execute_plugin_command', { + commandId: p.command, + args: p.args ?? null, + }) + this.#sendResult(iframe, 'command:result', id, { + ok: true, + data: result, + }) + } catch (e) { + this.#sendResult(iframe, 'command:result', id, { + ok: false, + error: String(e), + }) + } + break + } + + case 'settings:get': { + try { + const p = (payload ?? {}) as { key: string } + const settings = await invoke( + 'get_plugin_settings', + { pluginId }, + ) + const found = settings.find(s => s.key === p.key) + if (!found) { + this.#sendResult(iframe, 'settings:get:result', id, { + ok: false, + error: 'key not found', + }) + } else { + // 裸值:直接返回 SettingValue(tagged union) + this.#sendResult( + iframe, + 'settings:get:result', + id, + found.value, + ) + } + } catch (e) { + this.#sendResult(iframe, 'settings:get:result', id, { + ok: false, + error: String(e), + }) + } + break + } + + case 'settings:set': { + try { + const p = (payload ?? {}) as { key: string; value: unknown } + await invoke('update_plugin_setting', { + pluginId, + key: p.key, + value: p.value, + }) + this.#sendResult(iframe, 'settings:set:result', id, { + ok: true, + }) + } catch (e) { + this.#sendResult(iframe, 'settings:set:result', id, { + ok: false, + error: String(e), + }) + } + break + } + + case 'state:get': { + try { + const p = (payload ?? {}) as { kind: StateKind } + const snapshot = this.#getStateSnapshot(p.kind) + this.#sendResult(iframe, 'state:get:result', id, snapshot) + } catch (e) { + this.#sendResult(iframe, 'state:get:result', id, { + ok: false, + error: String(e), + }) + } + break + } + + case 'subscribe': + case 'unsubscribe': + // 广播默认发给所有 iframe,无需精确订阅记账 + break + + default: + console.warn( + `[host-bridge] 未识别的插件消息类型: ${type}(plugin=${pluginId})`, + ) + break + } + } + + /** 从 player 状态读取快照 */ + #getStateSnapshot(kind: StateKind): unknown { + switch (kind) { + case 'playback': + return { + playing: player.playing, + currentTime: player.currentTime, + } + case 'track': + return { currentTrack: player.currentTrack } + case 'queue': + return { queue: player.queue } + } + } + + /** 向 iframe 回写响应消息 */ + #sendResult( + iframe: HTMLIFrameElement, + type: string, + id: string | undefined, + payload: unknown, + ): void { + const win = iframe.contentWindow + if (!win) return + const envelope: PluginEnvelope = { + source: HOST_SOURCE, + type, + payload, + } + if (id !== undefined) envelope.id = id + win.postMessage(envelope, '*') + } +} + +export const hostBridge = new PluginHostBridge() diff --git a/src/lib/plugins/register-builtin-views.ts b/src/lib/plugins/register-builtin-views.ts new file mode 100644 index 0000000..d5229ea --- /dev/null +++ b/src/lib/plugins/register-builtin-views.ts @@ -0,0 +1,11 @@ +import { registerNativeView } from './component-registry' + +let registered = false + +export function registerBuiltinViews(): void { + if (registered) return + registered = true + + registerNativeView('eq-panel', () => import('$lib/features/EqPanel.svelte')) + registerNativeView('lyrics-panel', () => import('$lib/features/LyricsPanel.svelte')) +} diff --git a/src/lib/state/covers.svelte.ts b/src/lib/state/covers.svelte.ts new file mode 100644 index 0000000..857c629 --- /dev/null +++ b/src/lib/state/covers.svelte.ts @@ -0,0 +1,44 @@ +import { invoke } from '@tauri-apps/api/core' +import type { Track } from '../types/music' + +class TrackCovers { + covers = $state>({}) + private promises = new Map>() + + async load(track: Track | null | undefined): Promise { + if (!track) return null + + if (this.covers[track.id] !== undefined) { + return this.covers[track.id] + } + + if (this.promises.has(track.id)) { + return await this.promises.get(track.id)! + } + + const promise = invoke('get_track_cover', { trackId: track.id }) + .then((cover) => { + this.covers[track.id] = cover + return cover + }) + .catch((err) => { + console.error('加载封面失败:', track.id, err) + this.covers[track.id] = null + return null + }) + .finally(() => { + this.promises.delete(track.id) + }) + + this.promises.set(track.id, promise) + + return await promise + } + + get(track: Track | null | undefined): string | null { + if (!track) return null + return this.covers[track.id] ?? null + } +} + +export const trackCovers = new TrackCovers() diff --git a/src/lib/state/equalizer.svelte.ts b/src/lib/state/equalizer.svelte.ts new file mode 100644 index 0000000..6684d45 --- /dev/null +++ b/src/lib/state/equalizer.svelte.ts @@ -0,0 +1,73 @@ +import { invoke } from '@tauri-apps/api/core' + +// 10 段 EQ 的中心频率(Hz) +const EQ_BANDS = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000] +// 与 EQ_BANDS 对应的显示标签 +const BAND_LABELS = ['31', '62', '125', '250', '500', '1k', '2k', '4k', '8k', '16k'] + +export type EqPreset = { + name: string + bands: number[] +} + +export const EQ_PRESETS: EqPreset[] = [ + { name: 'Flat', bands: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }, + { name: 'Bass Boost', bands: [6, 5, 4, 2, 0, 0, 0, 0, 0, 0] }, + { name: 'Treble Boost', bands: [0, 0, 0, 0, 0, 2, 4, 5, 6, 6] }, + { name: 'Vocal', bands: [-3, -2, 0, 2, 4, 4, 3, 1, 0, -1] }, +] + +class EqualizerState { + bands = $state([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + enabled = $state(true) + presetName = $state('Flat') + isOpen = $state(false) + loaded = $state(false) + + async load() { + try { + const state = await invoke<{ bands: number[]; enabled: boolean; preset_name: string }>('get_eq_state') + this.bands = state.bands + this.enabled = state.enabled + this.presetName = state.preset_name + this.loaded = true + } catch (err) { + console.error('Failed to load EQ state:', err) + } + } + + async setBand(index: number, gain: number) { + this.bands[index] = gain + try { + await invoke('set_eq_band', { band: index, gain }) + } catch (err) { + console.error(err) + } + } + + async applyPreset(preset: EqPreset) { + this.bands = [...preset.bands] + this.presetName = preset.name + try { + await invoke('apply_eq_preset', { presetName: preset.name, bands: preset.bands }) + } catch (err) { + console.error(err) + } + } + + async setEnabled(enabled: boolean) { + this.enabled = enabled + try { + await invoke('set_eq_enabled', { enabled }) + } catch (err) { + console.error(err) + } + } + + toggle() { + this.isOpen = !this.isOpen + } +} + +export const equalizer = new EqualizerState() +export { EQ_BANDS, BAND_LABELS } diff --git a/src/lib/state/library.svelte.ts b/src/lib/state/library.svelte.ts new file mode 100644 index 0000000..a0a9211 --- /dev/null +++ b/src/lib/state/library.svelte.ts @@ -0,0 +1,207 @@ +import { invoke } from '@tauri-apps/api/core' +import { listen, type UnlistenFn } from '@tauri-apps/api/event' +import { SvelteMap } from 'svelte/reactivity' +import type { Album, Artist, Track } from '../types/music' +import { settings } from './settings.svelte' + +class MusicLibrary { + tracks = $state([]) + isLoading = $state(false) + error = $state(null) + initialized = false + + get useAlbumArtistGrouping() { + return settings.data.useAlbumArtistGrouping ?? false + } + + #unlisten: UnlistenFn | null = null + + #refreshPromise: Promise | null = null + #albumsMap = $derived.by(() => { + const albumMap = new SvelteMap() + const useAlbumArtistGrouping = this.useAlbumArtistGrouping + + for (const track of this.tracks) { + const albumTitle = track.album?.trim() || '未知专辑' + + const artistName = track.artist?.trim() || '未知歌手' + const albumKey = useAlbumArtistGrouping + ? `${artistName}_${albumTitle}`.toLowerCase() + : albumTitle.toLowerCase() + const id = encodeURIComponent(albumKey) + + if (!albumMap.has(id)) { + albumMap.set(id, { + id, + title: albumTitle, + artist: artistName, + tracks: [], + cover: track.cover ?? null, + trackCount: 0, + representativeTrack: track, + }) + } + + const album = albumMap.get(id)! + album.tracks.push(track) + album.trackCount = album.tracks.length + + if (!album.cover && track.cover) { + album.cover = track.cover + } + } + + return albumMap + }) + + #artistsMap = $derived.by(() => { + const artistMap = new SvelteMap< + string, + Artist & { + albumKeys: Set + } + >() + + for (const track of this.tracks) { + const artistName = track.artist?.trim() || '未知歌手' + const artistKey = artistName.toLowerCase() + const artistId = encodeURIComponent(artistKey) + + if (!artistMap.has(artistId)) { + artistMap.set(artistId, { + id: artistId, + name: artistName, + cover: track.cover ?? null, + trackCount: 0, + albumCount: 0, + tracks: [], + albumKeys: new Set(), + }) + } + + const artist = artistMap.get(artistId)! + + artist.tracks.push(track) + artist.trackCount = artist.tracks.length + + const albumTitle = track.album?.trim() || '未知专辑' + const albumKey = albumTitle.toLowerCase() + + artist.albumKeys.add(albumKey) + artist.albumCount = artist.albumKeys.size + + if (!artist.cover && track.cover) { + artist.cover = track.cover + } + } + + return new SvelteMap( + Array.from(artistMap.values()).map(({ albumKeys, ...artist }) => [ + artist.id, + artist, + ]), + ) + }) + + constructor() { + void this.#setupListener() + } + + get albums(): Album[] { + return Array.from(this.#albumsMap.values()) + } + + get artists(): Artist[] { + return Array.from(this.#artistsMap.values()) + } + + get albumCount(): number { + return this.#albumsMap.size + } + + get artistCount(): number { + return this.#artistsMap.size + } + + get trackCount(): number { + return this.tracks.length + } + + getAlbum(id: string): Album | undefined { + return this.#albumsMap.get(id) + } + + getArtist(id: string): Artist | undefined { + return this.#artistsMap.get(id) + } + + async #setupListener() { + if (this.#unlisten) return + + this.#unlisten = await listen('library:refreshed', event => { + this.tracks = event.payload + }) + } + + async load(options: { force?: boolean } = {}): Promise { + const { force = false } = options + + if (this.#refreshPromise) { + return this.#refreshPromise + } + + if (!force && this.tracks.length > 0) { + return this.tracks + } + + this.isLoading = true + this.error = null + + this.#refreshPromise = (async () => { + try { + const tracks = await invoke('load_track_library') + this.tracks = tracks + return tracks + } catch (err) { + console.error('全局加载媒体库失败:', err) + this.error = String(err) + return this.tracks + } finally { + this.isLoading = false + this.#refreshPromise = null + } + })() + + return this.#refreshPromise + } + + async scan(): Promise { + try { + this.isLoading = true + this.error = null + + const tracks = await invoke('scan_track_directories', { + dirs: settings.data.libraryDirs, + }) + + this.tracks = tracks + + return tracks + } catch (err) { + console.error('扫描媒体库失败:', err) + this.error = String(err) + return this.tracks + } finally { + this.isLoading = false + } + } + + destroy() { + if (this.#unlisten) { + this.#unlisten() + this.#unlisten = null + } + } +} + +export const musicLibrary = new MusicLibrary() \ No newline at end of file diff --git a/src/lib/state/lyrics.svelte.ts b/src/lib/state/lyrics.svelte.ts new file mode 100644 index 0000000..38935b3 --- /dev/null +++ b/src/lib/state/lyrics.svelte.ts @@ -0,0 +1,39 @@ +import type { LyricDocument, LyricLine } from '$lib/types/lyrics' + +class LyricsState { + currentLyrics = $state(null) + currentLineIndex = $state(-1) + isOpen = $state(false) + + handleLyricsLoaded(songId: number, lines: LyricLine[]) { + this.currentLyrics = { songId, lines } + this.currentLineIndex = -1 + } + + updateCurrentTime(currentTimeMs: number) { + if (!this.currentLyrics || this.currentLyrics.lines.length === 0) { + this.currentLineIndex = -1 + return + } + + let idx = -1 + for (let i = this.currentLyrics.lines.length - 1; i >= 0; i--) { + if (this.currentLyrics.lines[i].timestampMs <= currentTimeMs) { + idx = i + break + } + } + this.currentLineIndex = idx + } + + clear() { + this.currentLyrics = null + this.currentLineIndex = -1 + } + + toggle() { + this.isOpen = !this.isOpen + } +} + +export const lyrics = new LyricsState() diff --git a/src/lib/state/player.svelte.ts b/src/lib/state/player.svelte.ts new file mode 100644 index 0000000..9177537 --- /dev/null +++ b/src/lib/state/player.svelte.ts @@ -0,0 +1,283 @@ +import type { GlobalAppEvent, PlaybackStatusSnapshot, PlayMode } from "$lib/types" +import { invoke } from "@tauri-apps/api/core" +import { listen, type UnlistenFn } from "@tauri-apps/api/event" +import type { PlaybackQueue, Track } from "../types/music" +import { lyrics } from "./lyrics.svelte" +import { recentlyPlayed } from "./recent.svelte" + +class Player { + queue = $state({ + tracks: [], + currentIndex: null, + playMode: 'ListLoop', + history: [], + }) + + playing = $state(false) + currentTime = $state(0) + queueOpen = $state(false) + #muted = $state(false) + #volume = $state(80) + #previousVolume = 80 + #globalUnlisten: UnlistenFn | null = null + #isInitialized = false + #eventBuffer: GlobalAppEvent[] = [] + + constructor() { + this.#setupMediaSession() + } + + #setupMediaSession() { + if (typeof navigator === 'undefined' || !('mediaSession' in navigator)) return + if (typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window) return + + navigator.mediaSession.setActionHandler('play', () => this.resume()) + navigator.mediaSession.setActionHandler('pause', () => this.pause()) + navigator.mediaSession.setActionHandler('previoustrack', () => this.prev()) + navigator.mediaSession.setActionHandler('nexttrack', () => this.next()) + navigator.mediaSession.setActionHandler('seekto', details => { + const seekTime = details.seekTime + if (seekTime == null) return + void this.seek(seekTime) + }) + navigator.mediaSession.setActionHandler('seekforward', () => { + if (!this.currentTrack) return + void this.seek(Math.min(this.currentTime + 10, this.#trackDurationSeconds(this.currentTrack))) + }) + navigator.mediaSession.setActionHandler('seekbackward', () => { + if (!this.currentTrack) return + void this.seek(Math.max(this.currentTime - 10, 0)) + }) + } + + #updateMediaMetadata() { + if (typeof navigator === 'undefined' || !('mediaSession' in navigator) || !this.currentTrack) return + + navigator.mediaSession.metadata = new MediaMetadata({ + title: this.currentTrack.title, + artist: this.currentTrack.artist ?? '未知歌手', + album: this.currentTrack.album ?? '未知专辑', + artwork: this.#buildArtwork(), + }) + } + + #buildArtwork() { + const cover = this.currentTrack?.cover + return cover ? [{ src: cover }] : [] + } + + #trackDurationSeconds(track: Track): number { + return track.duration / 1000 + } + + #updatePlaybackState() { + if (typeof navigator === 'undefined' || !('mediaSession' in navigator)) return + navigator.mediaSession.playbackState = this.playing ? 'playing' : 'paused' + } + + #updatePositionState() { + const track = this.currentTrack + if (!track || typeof navigator === 'undefined' || !('mediaSession' in navigator) || !('setPositionState' in navigator.mediaSession)) return + + navigator.mediaSession.setPositionState({ + duration: this.#trackDurationSeconds(track), + playbackRate: 1, + position: this.currentTime, + }) + } + + #syncMediaSession() { + this.#updateMediaMetadata() + this.#updatePlaybackState() + this.#updatePositionState() + } + + #clearMediaSession() { + if (typeof navigator === 'undefined' || !('mediaSession' in navigator)) return + navigator.mediaSession.metadata = null + navigator.mediaSession.playbackState = 'none' + } + + get currentTrack(): Track | undefined { + const index = this.queue.currentIndex + if (index === null || index < 0 || index >= this.queue.tracks.length) { + return undefined + } + return this.queue.tracks[index] + } + + get volume() { return this.#volume } + set volume(volume: number) { + invoke('set_volume', { volume }).catch(console.error) + } + + get muted() { return this.#muted } + set muted(value: boolean) { + if (value) { + this.#previousVolume = this.#volume + this.#muted = true + invoke('set_volume', { volume: 0 }).catch(console.error) + } else { + this.#muted = false + invoke('set_volume', { volume: this.#previousVolume }).catch(console.error) + } + } + + async replacePlaylistAndPlay(tracks: Track[], targetId: number) { + await invoke('replace_playlist_and_play', { tracks, targetId }) + } + + async insertTracksAsNext(tracks: Track[]) { + await invoke('insert_tracks_as_next', { tracks }) + } + + async insertTrackAsNext(track: Track) { + await invoke('insert_track_as_next', { track }) + } + + async removeTrack(trackId: number) { + await invoke('remove_track_from_queue', { trackId }) + } + + async playTrackInQueue(index: number) { + await invoke('play_queue_track', { index }) + } + + async cyclePlayMode() { + const modes: PlayMode[] = ['ListLoop', 'SingleLoop', 'Shuffle'] + const index = modes.indexOf(this.queue.playMode) + const mode = modes[(index + 1) % modes.length] + await invoke('set_play_mode', { mode }) + } + + async clearQueue() { + await invoke('clear_queue') + } + + async next() { + await invoke('play_next_track') + } + + async prev() { + await invoke('play_previous_track') + } + + resume = async () => { + try { + await invoke('resume_track') + } catch (err) { + console.error(err) + } + } + + pause = async () => { + try { + await invoke('pause_track') + } catch (err) { + console.error(err) + } + } + + toggle = async () => { + if (this.playing) { + await this.pause() + } else { + await this.resume() + } + } + + seek = async (time: number) => { + await invoke('set_current_time', { time }) + } + + toggleQueue() { + this.queueOpen = !this.queueOpen + } + + async loadState() { + await this.#setupPlaybackListeners() + + try { + const queue = await invoke('get_playback_queue') + this.queue = queue + + const status = await invoke('get_current_status') + this.playing = status.playing + this.currentTime = status.currentTime + this.#syncMediaSession() + } catch (err) { + console.error(err) + } finally { + this.#isInitialized = true + this.#flushEventBuffer() + } + } + + #flushEventBuffer() { + while (this.#eventBuffer.length > 0) { + const event = this.#eventBuffer.shift() + if (event) { + this.#handleGlobalEvent(event) + } + } + } + + async #setupPlaybackListeners() { + if (this.#globalUnlisten) return + + this.#globalUnlisten = await listen('global-app-event', event => { + if (!this.#isInitialized) { + this.#eventBuffer.push(event.payload) + return + } + this.#handleGlobalEvent(event.payload) + }) + } + + #handleGlobalEvent(payload: GlobalAppEvent) { + const { type, payload: data } = payload + + switch (type) { + case 'PlaybackProgress': + this.currentTime = data.currentTime + this.#updatePositionState() + break + + case 'TrackStarted': + this.currentTime = 0 + this.playing = true + this.queue = { ...this.queue, currentIndex: data.index } + this.#syncMediaSession() + recentlyPlayed.add(data.track) + break + + case 'VolumeChanged': + this.#volume = Math.round(data * 100) + this.#muted = this.#volume === 0 + break + + case 'QueueChanged': + this.queue = data + break + + case 'PlaybackStateChanged': + this.playing = data.playing + this.currentTime = data.currentTime + this.#syncMediaSession() + break + + case 'LyricsLoaded': + lyrics.handleLyricsLoaded(data.songId, data.lines) + break + } + } + + destroy() { + if (this.#globalUnlisten) { + this.#globalUnlisten() + this.#globalUnlisten = null + } + } +} + +export const player = new Player() \ No newline at end of file diff --git a/src/lib/state/plugins.svelte.ts b/src/lib/state/plugins.svelte.ts new file mode 100644 index 0000000..4f9f1a2 --- /dev/null +++ b/src/lib/state/plugins.svelte.ts @@ -0,0 +1,128 @@ +import { invoke } from '@tauri-apps/api/core' +import type { + MenuExtension, + NativeViewExtension, + PluginSetting, + ResolvedPluginManifest, + SettingValue, + SidebarExtension, + ViewContribution, +} from '$lib/types/plugin' + +class PluginState { + manifests = $state([]) + sidebarExtensions = $state([]) + nativeViewExtensions = $state([]) + /** 当前激活的原生面板(pluginId),null 表示无面板显示 */ + activeNativePanel = $state(null) + isLoading = $state(false) + error = $state(null) + + /** + * 按 slug 解析插件 view。默认渲染 manifest 的第一个 view + * (多 view 场景不在本期范围)。 + */ + getViewForSlug(slug: string): { manifest: ResolvedPluginManifest; view: ViewContribution } | null { + const manifest = this.manifests.find(m => m.route === `/plugins/view/${slug}`) + if (!manifest) return null + const view = manifest.contributes.views[0] + if (!view) return null + return { manifest, view } + } + + async loadManifests() { + try { + this.manifests = await invoke( + 'get_plugin_manifests', + ) + } catch (err) { + console.error(err) + this.error = String(err) + } + } + + async loadSidebarExtensions() { + this.isLoading = true + this.error = null + try { + this.sidebarExtensions = await invoke( + 'get_all_sidebar_extensions', + ) + } catch (err) { + console.error(err) + this.error = String(err) + } finally { + this.isLoading = false + } + } + + async loadNativeViewExtensions() { + this.isLoading = true + this.error = null + try { + this.nativeViewExtensions = await invoke( + 'get_native_view_extensions', + ) + } catch (err) { + console.error(err) + this.error = String(err) + } finally { + this.isLoading = false + } + } + + async getMenuExtensions(location: string): Promise { + try { + return await invoke('get_menu_extensions', { + location, + }) + } catch (err) { + console.error(err) + return [] + } + } + + async getPluginSettings(pluginId: string): Promise { + try { + return await invoke('get_plugin_settings', { + pluginId, + }) + } catch (err) { + console.error(err) + return [] + } + } + + async updatePluginSetting( + pluginId: string, + key: string, + value: SettingValue, + ) { + try { + await invoke('update_plugin_setting', { pluginId, key, value }) + } catch (err) { + console.error(err) + this.error = String(err) + } + } + + async enablePlugin(pluginId: string) { + try { + await invoke('enable_plugin_command', { pluginId }) + } catch (err) { + console.error(err) + this.error = String(err) + } + } + + async disablePlugin(pluginId: string) { + try { + await invoke('disable_plugin_command', { pluginId }) + } catch (err) { + console.error(err) + this.error = String(err) + } + } +} + +export const pluginState = new PluginState() diff --git a/src/lib/state/recent.svelte.ts b/src/lib/state/recent.svelte.ts new file mode 100644 index 0000000..a93fbc5 --- /dev/null +++ b/src/lib/state/recent.svelte.ts @@ -0,0 +1,81 @@ +import { invoke } from '@tauri-apps/api/core' +import type { Track } from '../types/music' + +type RecentPlayedTrack = { + track: Track + playedAt: string +} + +class RecentlyPlayed { + tracks = $state([]) + isLoading = $state(false) + error = $state(null) + + private loadPromise: Promise | null = null + + async load(): Promise { + if (this.loadPromise) { + return this.loadPromise + } + + if (this.tracks.length > 0) { + return this.tracks + } + + this.isLoading = true + this.error = null + + this.loadPromise = invoke('load_recently_played', { + limit: 100, + offset: 0, + }) + .then(records => { + const tracks = records.map(record => record.track) + this.tracks = tracks + return tracks + }) + .catch(err => { + console.error('加载最近播放失败:', err) + this.error = String(err) + return this.tracks + }) + .finally(() => { + this.isLoading = false + this.loadPromise = null + }) + + return this.loadPromise + } + + async add(track: Track): Promise { + this.tracks = [ + track, + ...this.tracks.filter(item => item.id !== track.id), + ].slice(0, 100) + + try { + await invoke('add_recently_played', { + trackId: track.id, + playedAt: new Date().toISOString(), + }) + + return this.tracks + } catch (err) { + console.error('写入最近播放失败:', err) + this.error = String(err) + return this.tracks + } + } + + async clear() { + try { + await invoke('clear_recently_played') + this.tracks = [] + } catch (err) { + console.error('清空最近播放失败:', err) + this.error = String(err) + } + } +} + +export const recentlyPlayed = new RecentlyPlayed() \ No newline at end of file diff --git a/src/lib/state/search.svelte.ts b/src/lib/state/search.svelte.ts new file mode 100644 index 0000000..1d649cf --- /dev/null +++ b/src/lib/state/search.svelte.ts @@ -0,0 +1,5 @@ +class SearchState { + query = $state('') +} + +export const globalSearch = new SearchState() \ No newline at end of file diff --git a/src/lib/state/settings.svelte.ts b/src/lib/state/settings.svelte.ts new file mode 100644 index 0000000..c79a6c3 --- /dev/null +++ b/src/lib/state/settings.svelte.ts @@ -0,0 +1,101 @@ +import type { AppSettings } from '$lib/types' +import { invoke } from '@tauri-apps/api/core' + +export const DEFAULT_SETTINGS: AppSettings = { + theme: 'auto', + volume: 80, + libraryDirs: [], + scanOnStartup: false, + reduceMotion: false, + useAlbumArtistGrouping: false, + pluginDirs: [], + pluginDevMode: false, + pluginScanOnStartup: true, + pluginLogLevel: 'warn', +} + +class SettingsState { + data = $state({ ...DEFAULT_SETTINGS }) + isLoading = $state(false) + error = $state(null) + + #saveTimer: ReturnType | null = null + #loaded = false + + async load(): Promise { + if (this.#loaded) { + return this.data + } + + this.#loaded = true + this.isLoading = true + this.error = null + + try { + this.data = await invoke('load_settings') + return this.data + } catch (error) { + console.error(error) + this.error = String(error) + return this.data + } finally { + this.isLoading = false + } + } + + updateLocalState(newSettings: AppSettings) { + this.data = newSettings + } + + update(patch: Partial) { + const nextSettings = { + ...this.data, + ...patch, + } + this.data = nextSettings + this.scheduleSave(nextSettings) + } + + async save(targetSettings: AppSettings): Promise { + this.error = null + + try { + const savedSettings = await invoke('save_settings', { + settings: targetSettings, + }) + this.data = savedSettings + } catch (error) { + console.error(error) + this.error = String(error) + } + } + + scheduleSave(targetSettings: AppSettings) { + if (this.#saveTimer) { + clearTimeout(this.#saveTimer) + } + + this.#saveTimer = setTimeout(() => { + void this.save(targetSettings) + }, 300) + } + + addLibraryDir(dir: string) { + const value = dir.trim() + + if (!value) return + if (this.data.libraryDirs.includes(value)) return + + this.update({ + libraryDirs: [...this.data.libraryDirs, value], + }) + } + + removeLibraryDir(dir: string) { + this.update({ + libraryDirs: this.data.libraryDirs.filter((item) => item !== dir), + }) + } +} + +export const settings = new SettingsState() diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts new file mode 100644 index 0000000..a1dbe26 --- /dev/null +++ b/src/lib/types/events.ts @@ -0,0 +1,13 @@ +import type { PlaybackQueue } from "./music" +import type { LyricLine } from "./lyrics" +import type { PlaybackProgressPayload, PlaybackStatePayload, PlaybackTrackStartedPayload } from "./playback" +import type { AppSettings } from "./settings" + +export type GlobalAppEvent = + | { type: 'VolumeChanged'; payload: number } + | { type: 'SettingsChanged'; payload: AppSettings } + | { type: 'TrackStarted'; payload: PlaybackTrackStartedPayload } + | { type: 'PlaybackProgress'; payload: PlaybackProgressPayload } + | { type: 'QueueChanged'; payload: PlaybackQueue } + | { type: 'PlaybackStateChanged'; payload: PlaybackStatePayload } + | { type: 'LyricsLoaded'; payload: { songId: number; lines: LyricLine[] } } \ No newline at end of file diff --git a/src/lib/types/extension.ts b/src/lib/types/extension.ts new file mode 100644 index 0000000..6873d28 --- /dev/null +++ b/src/lib/types/extension.ts @@ -0,0 +1,22 @@ +export interface ExtensionNavItem { + id: string + title: string + icon: string + href: string + order?: number +} + +export interface ExtensionCommand { + id: string + title: string + run: () => void | Promise +} + +export interface AppExtension { + id: string + name: string + description?: string + enabled?: boolean + navItems?: ExtensionNavItem[] + commands?: ExtensionCommand[] +} \ No newline at end of file diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts new file mode 100644 index 0000000..c28b762 --- /dev/null +++ b/src/lib/types/index.ts @@ -0,0 +1,17 @@ +export type { GlobalAppEvent } from './events' +export type { + AppExtension, + ExtensionCommand, + ExtensionNavItem +} from './extension' +export type { LyricDocument, LyricLine } from './lyrics' +export type { Album, Artist, Playlist, Track } from './music' +export type { BackendQueueTrack, PlayMode, PlaybackProgressPayload, PlaybackStatusSnapshot, PlaybackTrackInfo } from './playback' +export type { + MenuExtension, + PluginInfo, + PluginSetting, + SettingValue, + SidebarExtension, +} from './plugin' +export type { AppSettings, PluginLogLevel, ThemeMode } from './settings' diff --git a/src/lib/types/lyrics.ts b/src/lib/types/lyrics.ts new file mode 100644 index 0000000..9e32a01 --- /dev/null +++ b/src/lib/types/lyrics.ts @@ -0,0 +1,9 @@ +export interface LyricLine { + timestampMs: number + text: string +} + +export interface LyricDocument { + songId: number + lines: LyricLine[] +} diff --git a/src/lib/types/music.ts b/src/lib/types/music.ts new file mode 100644 index 0000000..6da8bb3 --- /dev/null +++ b/src/lib/types/music.ts @@ -0,0 +1,47 @@ +import type { PlayMode } from "./playback" + +export interface Track { + id: number + title: string + artist: string | null + album: string | null + duration: number + cover: string | null + fileSize: number | null + playCount: number + lastPlayedAt: string | null + createdAt: string + updatedAt: string +} + +export interface Playlist { + id: number + name: string + cover: string +} + +export interface Artist { + id: string + name: string + cover: string | null + trackCount: number + albumCount: number + tracks: Track[] +} + +export interface Album { + id: string + title: string + artist: string + cover: string | null + tracks: Track[] + trackCount: number + representativeTrack: Track +} + +export interface PlaybackQueue { + tracks: Track[] + currentIndex: number | null + playMode: PlayMode + history: number[] +} \ No newline at end of file diff --git a/src/lib/types/playback.ts b/src/lib/types/playback.ts new file mode 100644 index 0000000..8c27a8d --- /dev/null +++ b/src/lib/types/playback.ts @@ -0,0 +1,31 @@ +import type { Track } from "./music" + +export type PlayMode = 'ListLoop' | 'SingleLoop' | 'Shuffle' + +export type PlaybackTrackInfo = { + id: number | null + path: string | null +} + +export type PlaybackStatusSnapshot = { + hasMedia: boolean + playing: boolean + currentTime: number + track: PlaybackTrackInfo | null +} + +export type PlaybackProgressPayload = { + currentTime: number +} + +export type PlaybackStatePayload = { + playing: boolean + currentTime: number +} + +export type PlaybackTrackStartedPayload = { + track: Track + index: number +} + +export type BackendQueueTrack = Omit \ No newline at end of file diff --git a/src/lib/types/plugin.ts b/src/lib/types/plugin.ts new file mode 100644 index 0000000..ca0a56a --- /dev/null +++ b/src/lib/types/plugin.ts @@ -0,0 +1,111 @@ +export type PluginSource = 'builtin' | 'packaged' | 'user' + +export type PluginPermission = + | 'playerRead' + | 'playerControl' + | 'queueRead' + | 'queueWrite' + | 'libraryRead' + | 'libraryWrite' + | 'settingsRead' + | 'settingsWrite' + | 'pluginUI' + +export type SettingValue = + | { type: 'Bool'; value: boolean } + | { type: 'Integer'; value: number } + | { type: 'Float'; value: number } + | { type: 'Text'; value: string } + | { type: 'List'; value: string[] } + | { type: 'Json'; value: unknown } + +export interface PluginSetting { + key: string + title: string + value: SettingValue + defaultValue: SettingValue +} + +export interface SidebarExtension { + id: string + pluginId: string + title: string + icon: string + route: string + state: 'Enabled' | 'Disabled' +} + +export interface NativeViewExtension { + id: string + pluginId: string + title: string + token: string + icon: string | null + state: 'Enabled' | 'Disabled' +} + +export interface MenuExtension { + id: string + pluginId: string + command: string + location: string + group: string | null + state: 'Enabled' | 'Disabled' +} + +export interface ViewContribution { + id: string + title: string + entry: string + icon: string | null +} + +export interface NativeViewContribution { + id: string + title: string + token: string + icon: string | null +} + +/** Fully resolved, normalized manifest — the only plugin-identity shape + * the frontend ever consumes. Routes are already normalized to + * `/plugins/view/`; no optional fields. */ +export interface ResolvedPluginManifest { + id: string + source: PluginSource + route: string + name: string + displayName: string + version: string + author: string + description: string + entry: string + minAppVersion: string + permissions: string[] + activationEvents: string[] + contributes: { + commands: { id: string; title: string; category: string | null }[] + menus: { + command: string + title: string + location: string + group: string | null + }[] + sidebars: { id: string; title: string; icon: string }[] + views: ViewContribution[] + nativeViews: NativeViewContribution[] + } + settings: { + key: string + title: string + defaultValue: SettingValue + }[] +} + +export interface PluginInfo { + id: string + displayName: string + icon: string + route: string + state: 'Enabled' | 'Disabled' +} diff --git a/src/lib/types/settings.ts b/src/lib/types/settings.ts new file mode 100644 index 0000000..8500233 --- /dev/null +++ b/src/lib/types/settings.ts @@ -0,0 +1,16 @@ +export type ThemeMode = 'auto' | 'light' | 'dark' + +export type PluginLogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug' + +export interface AppSettings { + theme: ThemeMode + volume: number + libraryDirs: string[] + scanOnStartup: boolean + reduceMotion: boolean + useAlbumArtistGrouping: boolean + pluginDirs: string[] + pluginDevMode: boolean + pluginScanOnStartup: boolean + pluginLogLevel: PluginLogLevel +} diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts new file mode 100644 index 0000000..478680d --- /dev/null +++ b/src/lib/utils/index.ts @@ -0,0 +1 @@ +export { formatDuration, formatTime } from './time' diff --git a/src/lib/utils/library.ts b/src/lib/utils/library.ts new file mode 100644 index 0000000..d18c3e1 --- /dev/null +++ b/src/lib/utils/library.ts @@ -0,0 +1,26 @@ +import { musicLibrary } from '$lib/state/library.svelte' +import { settings } from '$lib/state/settings.svelte' +import { open } from '@tauri-apps/plugin-dialog' + +export async function scanDirectory() { + try { + const selected = await open({ + directory: true, + multiple: false, + title: '选择音乐媒体库目录', + }) + + if (!selected || typeof selected !== 'string') return + + settings.addLibraryDir(selected) + + await musicLibrary.scan() + } catch (err) { + console.error('扫描音乐媒体库目录失败:', err) + } +} + +export async function removeMusicDirectory(dir: string) { + settings.removeLibraryDir(dir) + await musicLibrary.scan() +} \ No newline at end of file diff --git a/src/lib/utils/time.ts b/src/lib/utils/time.ts new file mode 100644 index 0000000..9daf708 --- /dev/null +++ b/src/lib/utils/time.ts @@ -0,0 +1,34 @@ +export function formatSeconds( + seconds: number, + options: { + padMinutes?: boolean + showHours?: boolean + } = {} +): string { + const { padMinutes = false, showHours = false } = options + + if (!Number.isFinite(seconds) || seconds <= 0) { + return padMinutes ? '00:00' : '0:00' + } + + const total = Math.floor(seconds) + const h = Math.floor(total / 3600) + const m = Math.floor((total % 3600) / 60) + const s = total % 60 + + if (h > 0 || showHours) { + return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` + } + + const minutes = padMinutes ? String(m).padStart(2, '0') : String(m) + + return `${minutes}:${String(s).padStart(2, '0')}` +} + +export function formatTime(seconds: number): string { + return formatSeconds(seconds, { padMinutes: true }) +} + +export function formatDuration(seconds: number): string { + return formatSeconds(seconds) +} \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b084323..c7d63b8 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,25 +1,132 @@ - - - - + + + + + {@render children()} + + - -
+ - {@render children()} - - +{#if activeNativeView} +
+ +
+{/if} + + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte deleted file mode 100644 index 554da60..0000000 --- a/src/routes/+page.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - -
- - -
- - diff --git a/src/routes/+page.ts b/src/routes/+page.ts new file mode 100644 index 0000000..dca901d --- /dev/null +++ b/src/routes/+page.ts @@ -0,0 +1,5 @@ +import { redirect } from '@sveltejs/kit' + +export async function load() { + redirect(307, '/library') +} \ No newline at end of file diff --git a/src/routes/album/+page.svelte b/src/routes/album/+page.svelte new file mode 100644 index 0000000..4de94be --- /dev/null +++ b/src/routes/album/+page.svelte @@ -0,0 +1,218 @@ + + + + + + 专辑 + + +
+
+ +
+
+ +
+ +
+ 按专辑艺术家分组 + +
+
+
+ + {#if musicLibrary.isLoading} +
+ +
+ {:else if musicLibrary.error} +
+ {musicLibrary.error} +
+ {:else if isMobile} +
+ {#if selectedAlbum === undefined} +
+ {#if filteredAlbums.length === 0} +
+ 🎵 + 没有找到专辑 +
+ {:else} + + {#each filteredAlbums as album (album.id)} + {@const cover = trackCovers.get(album.representativeTrack) ?? album.cover} + { selectedAlbumId = album.id }} + onkeydown={(e: KeyboardEvent) => + (e.key === 'Enter' || e.key === ' ') && + (selectedAlbumId = album.id)} + role="button" + tabindex="0" + > + {#if cover} + + {:else} +
+ 🎵 +
+ {/if} +
+ {/each} +
+ {/if} +
+ {:else} +
+
+ { selectedAlbumId = null }} + /> +
+

+ {selectedAlbum.title} +

+

+ {selectedAlbum.trackCount} 首歌曲 +

+
+
+
+ +
+
+ {/if} +
+ {:else} +
+
+ +
+
+ {#if selectedAlbum === undefined} +
+ 🎵 + 选择一个专辑以查看歌曲 +
+ {:else} +
+ +
+ {/if} +
+
+ {/if} +
diff --git a/src/routes/artists/+page.svelte b/src/routes/artists/+page.svelte new file mode 100644 index 0000000..35153dd --- /dev/null +++ b/src/routes/artists/+page.svelte @@ -0,0 +1,220 @@ + + + + + + 艺术家 + + +
+
+ +
+
+ +
+
+
+ + {#if musicLibrary.isLoading} +
+ +
+ {:else if musicLibrary.error} +
+ {musicLibrary.error} +
+ {:else if isMobile} +
+ {#if selectedArtist === undefined} +
+ {#if filteredArtists.length === 0} +
+ 👤 + 没有找到歌手 +
+ {:else} + + {#each filteredArtists as artist (artist.id)} + { + selectedArtistId = artist.id + }} + onkeydown={(e: KeyboardEvent) => + (e.key === 'Enter' || e.key === ' ') && + (selectedArtistId = artist.id)} + role="button" + tabindex="0" + > + {#if artist.cover} + + {:else} +
+ 👤 +
+ {/if} +
+ {/each} +
+ {/if} +
+ {:else} +
+
+ { + selectedArtistId = null + }} + /> +
+

+ {selectedArtist.name} +

+

+ {selectedArtist.trackCount} 首歌曲 · {selectedArtist.albumCount} + 张专辑 +

+
+ +
+
+ +
+
+ {/if} +
+ {:else} +
+
+ +
+
+ {#if selectedArtist === undefined} +
+ 👤 + 选择一个歌手以查看歌曲 +
+ {:else} +
+ +
+ {/if} +
+
+ {/if} +
diff --git a/src/routes/library/+page.svelte b/src/routes/library/+page.svelte new file mode 100644 index 0000000..3d55da1 --- /dev/null +++ b/src/routes/library/+page.svelte @@ -0,0 +1,534 @@ + + + + 歌曲 + + +
+
+ +
+
+ + + musicLibrary.scan()} + /> + + + + + + + + + + + { + isLibraryDialogOpen = true + }} + /> + +
+
+
+ +
+
+ + 标题 + 歌手 + 专辑 + +
+
+
+ + {#if isBatchMode} +
+
+ 已选择 {selectedTrackIds.length} 首歌曲 +
+ +
+ + + + + + + + + +
+
+ {/if} +
+ + {#if musicLibrary.isLoading} +
+ +
+ {:else if musicLibrary.error} +
+ {musicLibrary.error} +
+ {:else if displayTracks.length === 0} +
+
🎵
+
没有找到歌曲
+
尝试刷新媒体库或修改搜索关键词
+
+ {:else} +
+ +
+ {/if} +
+ + { + isLibraryDialogOpen = false + }} +> +
+
+
+ 管理媒体文件夹 +
+
+ 配置音乐媒体库扫描目录和启动行为 +
+
+ + + + handleSwitchChange(event, 'scanOnStartup')} + > + + +
+
+
媒体库目录
+
+ 管理应用扫描音乐文件的本地文件夹 +
+
+ +
+ +
+ + {#if settings.data?.libraryDirs?.length > 0} +
+ {#each settings.data.libraryDirs as dir (dir)} + + + + {/each} +
+ {:else} +
+ 暂未添加媒体库目录,应用将无法扫描到任何歌曲。 +
+ {/if} +
+ +
+ +
+
+
+ + { + isDeleteConfirmDialogOpen = false + }} +> +
+
+
+ 确认删除所选歌曲 +
+
+ 将对已选择的 {selectedTrackIds.length} 首歌曲执行删除操作。移至回收站后通常可以恢复;从磁盘中删除将永久移除文件且无法撤销。 +
+
+ +
+ + + + + +
+
+
+ + { + isAddAllSnackbarOpen = false + }} +> + 成功添加至播放列表 + diff --git a/src/routes/plugins/+page.svelte b/src/routes/plugins/+page.svelte new file mode 100644 index 0000000..de05789 --- /dev/null +++ b/src/routes/plugins/+page.svelte @@ -0,0 +1,88 @@ + + + + 插件 + + +
+ + +
+ +
+ +
+ {#if pluginState.manifests.length === 0} +
+ 暂无已注册的插件。 +
+ {:else} + {#each pluginState.manifests as manifest (manifest.id)} + {@const ext = pluginState.sidebarExtensions.find(e => e.pluginId === manifest.id)} + {@const state = ext?.state ?? 'Enabled'} + + {/each} + {/if} + + {#if pluginState.error} +
+ {pluginState.error} +
+ {/if} +
+
diff --git a/src/routes/plugins/detail/+page.svelte b/src/routes/plugins/detail/+page.svelte new file mode 100644 index 0000000..f3d5246 --- /dev/null +++ b/src/routes/plugins/detail/+page.svelte @@ -0,0 +1,293 @@ + + + + 插件 - {sidebarEntry?.title ?? pluginId} + + +
+
+ + + + +
+ +
+ +
+ +
+ + + + {pluginId} + + + + + togglePlugin()} + > + + + + {#if settings.length > 0} + + {#each settings as setting (setting.key)} + + {#if setting.value.type === 'Bool'} + { + const target = + e.currentTarget as HTMLInputElement + handleSettingBoolChange( + setting.key, + target.checked, + ) + }} + > + {:else if setting.value.type === 'Integer'} + { + if (e.key === 'Enter') { + const target = + e.currentTarget as HTMLInputElement + handleSettingChange( + setting.key, + 'Integer', + target.value, + ) + } + }} + role="textbox" + tabindex="0" + style="width: 200px;" + > + {:else if setting.value.type === 'Float'} + { + if (e.key === 'Enter') { + const target = + e.currentTarget as HTMLInputElement + handleSettingChange( + setting.key, + 'Float', + target.value, + ) + } + }} + role="textbox" + tabindex="0" + style="width: 200px;" + > + {:else if setting.value.type === 'Text'} + { + if (e.key === 'Enter') { + const target = + e.currentTarget as HTMLInputElement + handleSettingChange( + setting.key, + 'Text', + target.value, + ) + } + }} + role="textbox" + tabindex="0" + style="width: 200px;" + > + {:else if setting.value.type === 'Json'} +
+ handleJsonInput(setting.key)} + onblur={(e: Event) => { + const target = + e.currentTarget as HTMLInputElement + handleSettingChange( + setting.key, + 'Json', + target.value, + ) + }} + role="textbox" + tabindex="0" + > + {#if jsonErrors[setting.key]} +
+ JSON 解析失败:{jsonErrors[setting.key]} +
+ {/if} +
+ {:else if setting.value.type === 'List'} + + handleListChange(setting.key, arr)} + /> + {/if} +
+ {/each} +
+ {/if} +
+
diff --git a/src/routes/plugins/view/[...slug]/+page.server.ts b/src/routes/plugins/view/[...slug]/+page.server.ts new file mode 100644 index 0000000..38c25cc --- /dev/null +++ b/src/routes/plugins/view/[...slug]/+page.server.ts @@ -0,0 +1,4 @@ +// 插件 view 路由按运行时 pluginState.manifests 解析,不能预渲染。 +// 全局 +layout.ts 设置了 prerender=true,此处覆盖为 false, +// 使 /plugins/view/<任意 slug> 走 SPA fallback 而非仅限 lyrics。 +export const prerender = false diff --git a/src/routes/plugins/view/[...slug]/+page.svelte b/src/routes/plugins/view/[...slug]/+page.svelte new file mode 100644 index 0000000..392e522 --- /dev/null +++ b/src/routes/plugins/view/[...slug]/+page.svelte @@ -0,0 +1,145 @@ + + + + {manifest?.displayName ?? 'Plugin'} — Plugins + + +
+ {#if !manifest} +
+

+ 未找到插件 "{slug}" +

+ + 返回插件列表 + +
+ {:else} +
+ + + + +
+ + {#if !resolved} + +
+
+
+
+ ID + {manifest.id} +
+
+ 来源 + {manifest.source} +
+
+ 版本 + {manifest.version} +
+
+ 路由 + {manifest.route} +
+
+ 作者 + {manifest.author} +
+
+ {manifest.description} +
+
+
+
+ {:else if !hasUIPermission} +
+

+ 插件未声明 UI 权限 +

+
+ {:else} +
+ +
+ {/if} + {/if} +
diff --git a/src/routes/recent/+page.svelte b/src/routes/recent/+page.svelte new file mode 100644 index 0000000..dbddb63 --- /dev/null +++ b/src/routes/recent/+page.svelte @@ -0,0 +1,58 @@ + + + + 最近播放 + + +
+ +
+ + + {#if recentlyPlayed.tracks.length > 0} + + {/if} +
+
+ +{#if recentlyPlayed.isLoading} +
+ +
+{:else if recentlyPlayed.error} +
+ {recentlyPlayed.error} +
+{:else if recentlyPlayed.tracks.length === 0} +
+
🎧
+
还没有最近播放记录
+
播放一首歌后,它会出现在这里
+
+{:else} +
+ +
+{/if} diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte new file mode 100644 index 0000000..0d27130 --- /dev/null +++ b/src/routes/settings/+page.svelte @@ -0,0 +1,258 @@ + + + + 设置 + + +
+ + +
+ +
+ +
+ + +
+ + settings.update({ + pluginLogLevel: value as PluginLogLevel, + })} + /> +
+
+ +
+
+
插件目录
+
+ 管理应用扫描插件的本地文件夹 +
+
+ +
+ +
+ + {#if settings.data?.pluginDirs?.length > 0} +
+ {#each settings.data.pluginDirs as dir (dir)} + + + + {/each} +
+ {:else} +
+ 暂未添加插件目录。 +
+ {/if} +
+
+ + {#if settings.error} +
+ {settings.error} +
+ {/if} +
+
diff --git a/static/default_cover.png b/static/default_cover.png new file mode 100644 index 0000000..6c7c6c1 Binary files /dev/null and b/static/default_cover.png differ diff --git a/static/fonts/Material_Icons.woff2 b/static/fonts/Material_Icons.woff2 new file mode 100644 index 0000000..5492a6e Binary files /dev/null and b/static/fonts/Material_Icons.woff2 differ diff --git a/static/fonts/Material_Icons_Outlined.woff2 b/static/fonts/Material_Icons_Outlined.woff2 new file mode 100644 index 0000000..d44b948 Binary files /dev/null and b/static/fonts/Material_Icons_Outlined.woff2 differ diff --git a/static/fonts/Material_Icons_Round.woff2 b/static/fonts/Material_Icons_Round.woff2 new file mode 100644 index 0000000..e9e305f Binary files /dev/null and b/static/fonts/Material_Icons_Round.woff2 differ diff --git "a/static/music/40mP \345\210\235\351\237\263\343\203\237\343\202\257 - \346\201\213\346\204\233\350\243\201\345\210\244.flac" "b/static/music/40mP \345\210\235\351\237\263\343\203\237\343\202\257 - \346\201\213\346\204\233\350\243\201\345\210\244.flac" deleted file mode 100644 index 82ea6bc..0000000 Binary files "a/static/music/40mP \345\210\235\351\237\263\343\203\237\343\202\257 - \346\201\213\346\204\233\350\243\201\345\210\244.flac" and /dev/null differ diff --git a/static/plugin-sdk/plugin-host-sdk.js b/static/plugin-sdk/plugin-host-sdk.js new file mode 100644 index 0000000..94bd433 --- /dev/null +++ b/static/plugin-sdk/plugin-host-sdk.js @@ -0,0 +1,177 @@ +/** + * RustEchoMusic 插件宿主 SDK + * + * 注入到插件 iframe 内,封装 postMessage 协议为 Promise-based API。 + * 零依赖 IIFE,挂载到 window.rem。 + * 协议信封:{ source, type, id?, payload? } + */ +(function (window) { + 'use strict'; + + var HOST_SOURCE = 'rem-plugin-host'; + var PLUGIN_SOURCE = 'rem-plugin'; + var DEFAULT_TIMEOUT = 10000; // 请求默认超时 10s + + // 请求-响应配对表:id -> { resolve, reject, timer } + var pendingRequests = new Map(); + // 事件监听器:eventName -> Set + var listeners = new Map(); + // 握手信息(收到宿主 ready 后填充) + var readyInfo = null; + var readyResolvers = []; + + // 生成唯一请求 id,优先用 crypto.randomUUID,降级到时间戳+随机串 + function genId() { + if (window.crypto && typeof window.crypto.randomUUID === 'function') { + return window.crypto.randomUUID(); + } + return 'r_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10); + } + + // 向宿主发送消息信封 + function send(type, payload, id) { + var envelope = { source: PLUGIN_SOURCE, type: type }; + if (id !== undefined) envelope.id = id; + if (payload !== undefined) envelope.payload = payload; + window.parent.postMessage(envelope, '*'); + } + + // 发起请求并等待对应 result + function request(type, payload) { + return new Promise(function (resolve, reject) { + var id = genId(); + var timer = window.setTimeout(function () { + if (pendingRequests.has(id)) { + pendingRequests.delete(id); + reject(new Error('请求超时: ' + type + ' (' + id + ')')); + } + }, DEFAULT_TIMEOUT); + pendingRequests.set(id, { resolve: resolve, reject: reject, timer: timer }); + send(type, payload, id); + }); + } + + // 按 kind 派发事件给监听器,单个异常不影响其他 + function dispatch(kind, data) { + var set = listeners.get(kind); + if (!set) return; + set.forEach(function (cb) { + try { cb(data); } catch (e) { /* swallow */ } + }); + } + + // 处理请求响应:command 始终 { ok, data|error };其余兼容裸值与错误信封 + function handleResult(data) { + var pending = pendingRequests.get(data.id); + if (!pending) return; + window.clearTimeout(pending.timer); + pendingRequests.delete(data.id); + var payload = data.payload; + + if (data.type === 'command:result') { + if (payload && payload.ok === false) { + pending.reject(new Error(payload.error || '命令执行失败')); + } else { + pending.resolve(payload ? payload.data : payload); + } + return; + } + // settings:get / settings:set / state:get:裸值原样返回,错误信封 reject + if (payload && typeof payload === 'object' && payload.ok === false) { + pending.reject(new Error(payload.error || ('请求失败: ' + data.type))); + return; + } + pending.resolve(payload); + } + + // 处理宿主 → 插件消息 + function onMessage(event) { + var data = event.data; + if (!data || data.source !== HOST_SOURCE) return; + + switch (data.type) { + case 'ready': + readyInfo = data.payload || {}; + readyResolvers.forEach(function (r) { r.resolve(readyInfo); }); + readyResolvers = []; + dispatch('ready', readyInfo); + break; + case 'event': { + var p = data.payload || {}; + dispatch(p.kind, p.data); + break; + } + case 'command:result': + case 'settings:get:result': + case 'settings:set:result': + case 'state:get:result': + handleResult(data); + break; + default: + break; // 未知类型忽略 + } + } + + window.addEventListener('message', onMessage); + + var sdk = { + // 等待宿主握手,返回 { pluginId, capabilities } + ready: function () { + return new Promise(function (resolve) { + if (readyInfo) { + resolve(readyInfo); + } else { + readyResolvers.push({ resolve: resolve }); + } + }); + }, + + // 执行宿主命令:command 名 + args + command: function (command, args) { + return request('command', { command: command, args: args }); + }, + + settings: { + get: function (key) { + return request('settings:get', { key: key }); + }, + set: function (key, value) { + return request('settings:set', { key: key, value: value }); + } + }, + + state: { + get: function (kind) { + return request('state:get', { kind: kind }); + } + }, + + // 订阅事件:ready | trackChanged | playbackStateChanged | queueChanged | settingsChanged + on: function (eventName, callback) { + if (!listeners.has(eventName)) listeners.set(eventName, new Set()); + listeners.get(eventName).add(callback); + if (eventName !== 'ready' && listeners.get(eventName).size === 1) { + // 首次监听非 ready 事件时通知宿主 + send('subscribe', { kind: eventName }); + } else if (eventName === 'ready' && readyInfo) { + // 已握手完成则异步补发,避免错过 ready + Promise.resolve().then(function () { callback(readyInfo); }); + } + return sdk; + }, + + off: function (eventName, callback) { + var set = listeners.get(eventName); + if (set) { + set.delete(callback); + if (set.size === 0 && eventName !== 'ready') { + listeners.delete(eventName); + send('unsubscribe', { kind: eventName }); + } + } + return sdk; + } + }; + + window.rem = sdk; +})(window); diff --git a/static/svelte.svg b/static/svelte.svg deleted file mode 100644 index c5e0848..0000000 --- a/static/svelte.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/tauri.svg b/static/tauri.svg deleted file mode 100644 index 31b62c9..0000000 --- a/static/tauri.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/static/vite.svg b/static/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/static/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 194baa0..2b4cb12 100644 --- a/vite.config.js +++ b/vite.config.js @@ -26,8 +26,8 @@ export default defineConfig(async () => ({ } : undefined, watch: { - // 3. tell Vite to ignore watching `src-tauri` - ignored: ['**/src-tauri/**'], + // 3. tell Vite to ignore watching `src-tauri` and `target` + ignored: ['**/src-tauri/**', '**/target/**'], }, }, }))