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/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/lyrics/plugin.json b/plugins/lyrics/plugin.json new file mode 100644 index 0000000..ab67db6 --- /dev/null +++ b/plugins/lyrics/plugin.json @@ -0,0 +1,39 @@ +{ + "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" } + ] + }, + "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/plugin.template.json b/plugins/plugin.template.json new file mode 100644 index 0000000..3a5c18c --- /dev/null +++ b/plugins/plugin.template.json @@ -0,0 +1,20 @@ +{ + "id": "", + "name": "", + "displayName": "", + "version": "1.0.0", + "author": "", + "description": "", + "entry": "", + "minAppVersion": "0.1.0", + "permissions": [], + "activationEvents": [ + "onStartup" + ], + "contributes": { + "commands": [], + "menus": [], + "sidebars": [] + }, + "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..f62d973 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -32,6 +32,12 @@ 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,9 +83,9 @@ 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", ] @@ -188,9 +194,9 @@ dependencies = [ [[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", @@ -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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +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.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -360,19 +372,28 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" 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,6 +413,12 @@ 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" @@ -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", @@ -470,13 +497,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.57" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" 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 = "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 = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" dependencies = [ - "glob", + "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.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7" dependencies = [ "alsa", - "core-foundation-sys", "coreaudio-rs", "dasp_sample", "jni", "js-sys", "libc", "mach2", - "ndk 0.8.0", + "ndk", "ndk-context", - "oboe", + "num-derive", + "num-traits", + "objc2", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.54.0", + "windows 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", ] @@ -789,12 +895,27 @@ dependencies = [ [[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]] @@ -838,26 +959,39 @@ 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 = [ + "powerfmt", + "serde_core", ] [[package]] @@ -887,8 +1021,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.0", + "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -912,13 +1057,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,9 +1077,9 @@ 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", @@ -965,14 +1116,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" dependencies = [ "bit-set", - "cssparser 0.36.0", + "cssparser", "foldhash 0.2.0", - "html5ever 0.38.0", + "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 +1154,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 +1183,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", ] @@ -1098,6 +1273,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 +1312,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 +1327,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]] @@ -1184,6 +1368,17 @@ 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" @@ -1202,6 +1397,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 +1413,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]] @@ -1223,6 +1427,12 @@ dependencies = [ "syn 2.0.117", ] +[[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 +1448,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 +1475,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" @@ -1333,15 +1544,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 +1645,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 +1661,7 @@ checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", ] [[package]] @@ -1494,6 +1685,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core", "wasip2", "wasip3", ] @@ -1536,7 +1728,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", @@ -1666,6 +1858,26 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[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" @@ -1691,6 +1903,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 +1938,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 +1945,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.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -1769,11 +1987,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 +2011,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1844,17 +2070,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 +2089,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 +2102,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 +2116,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 +2136,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", @@ -1953,14 +2180,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 +2214,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 +2239,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 +2258,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,7 +2296,7 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", @@ -2084,26 +2305,40 @@ dependencies = [ [[package]] name = "jni-sys" -version = "0.3.0" +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 = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] [[package]] -name = "jobserver" -version = "0.1.34" +name = "jni-sys-macros" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ - "getrandom 0.3.4", - "libc", + "quote", + "syn 2.0.117", ] [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -2136,23 +2371,11 @@ 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" @@ -2185,15 +2408,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading 0.7.4", + "libloading", "once_cell", ] [[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" @@ -2206,22 +2438,23 @@ dependencies = [ ] [[package]] -name = "libloading" -version = "0.8.9" +name = "libredox" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ - "cfg-if", - "windows-link 0.2.1", + "libc", ] [[package]] -name = "libredox" -version = "0.1.14" +name = "libsqlite3-sys" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ - "libc", + "cc", + "pkg-config", + "vcpkg", ] [[package]] @@ -2232,9 +2465,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 +2485,63 @@ 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 = "458ace39169e4b83c4f77ae3d42d5d1d11c422feef590219a97c973d3b524557" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "log" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "lyrics-plugin" +version = "0.1.0" +dependencies = [ + "lofty", + "plugin-sdk", + "serde", + "serde_json", +] [[package]] name = "mach2" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" dependencies = [ "libc", ] [[package]] -name = "markup5ever" -version = "0.14.1" +name = "malloc_buf" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +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 +2551,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.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "memoffset" @@ -2335,12 +2586,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 +2598,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.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +checksum = "47a2e3dff89cd322c66647942668faee0a2b1f88ea6cbb4d374b4a8d7e92528c" dependencies = [ "crossbeam-channel", "dpi", @@ -2377,24 +2632,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 +2644,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", @@ -2418,22 +2659,13 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -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 +2676,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 +2691,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" @@ -2524,34 +2716,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]] @@ -2576,6 +2756,15 @@ dependencies = [ "syn 2.0.117", ] +[[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" @@ -2592,21 +2781,92 @@ 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 = [ + "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 +2876,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 +2936,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 +2949,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 +2960,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 +2972,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 +3028,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 +3108,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 +3126,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 +3154,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,47 +3163,20 @@ 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", -] - [[package]] name = "phf_shared" 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 +3185,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 +3204,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 +3242,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 +3271,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,15 +3284,6 @@ 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" @@ -3159,7 +3335,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 +3362,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" @@ -3201,11 +3371,17 @@ 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.38.4" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ "memchr", ] @@ -3233,84 +3409,20 @@ 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "getrandom 0.1.16", + "chacha20", + "getrandom 0.4.2", + "rand_core", ] [[package]] name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" +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 +3445,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]] @@ -3390,6 +3502,12 @@ 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" @@ -3398,9 +3516,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[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 +3548,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 = "7204ed6420f698836b76d4d5c2ec5dec7585fd5c3a788fd1cde855d1de598239" +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.117", +] + +[[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" @@ -3460,11 +3632,35 @@ dependencies = [ "realfft", ] +[[package]] +name = "rust-echo-music" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "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 +3691,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", @@ -3574,48 +3770,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", @@ -3676,9 +3854,9 @@ dependencies = [ [[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", @@ -3709,24 +3887,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,9 +3916,9 @@ 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", @@ -3771,21 +3950,22 @@ dependencies = [ [[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 +3975,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 +4008,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "siphasher" -version = "0.3.11" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[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" @@ -3844,12 +4029,15 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +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 +4051,7 @@ checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "bytemuck", "js-sys", - "ndk 0.9.0", + "ndk", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -3904,30 +4092,215 @@ dependencies = [ ] [[package]] -name = "stable_deref_trait" -version = "1.2.1" +name = "souvlaki" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +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 = "strength_reduce" -version = "0.2.4" +name = "spin" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] -name = "string_cache" -version = "0.8.9" +name = "sqlx" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", + "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.117", +] + +[[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.117", + "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" @@ -3936,32 +4309,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 +4360,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 +4405,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 +4430,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 +4453,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 +4475,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 +4495,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 +4516,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 +4536,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 +4575,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 +4609,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 +4634,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 +4658,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 +4683,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 +4708,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 +4730,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,7 +4741,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote", "unicode-ident", ] @@ -4221,15 +4790,16 @@ 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", @@ -4239,14 +4809,15 @@ dependencies = [ "jni", "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", @@ -4276,9 +4847,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.10.3" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" dependencies = [ "anyhow", "bytes", @@ -4291,6 +4862,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", + "image", "jni", "libc", "log", @@ -4322,26 +4894,14 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "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", + "windows 0.61.3", ] [[package]] name = "tauri-build" -version = "2.5.6" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" dependencies = [ "anyhow", "cargo_toml", @@ -4355,28 +4915,27 @@ 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.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" dependencies = [ "base64 0.22.1", "brotli", "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "syn 2.0.117", "tauri-utils", "thiserror 2.0.18", @@ -4388,9 +4947,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.5" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -4402,9 +4961,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.5.4" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" +checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" dependencies = [ "anyhow", "glob", @@ -4413,15 +4972,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,11 +5039,27 @@ 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.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" dependencies = [ "cookie", "dpi", @@ -4466,9 +5082,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.10.1" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" dependencies = [ "gtk", "http", @@ -4492,24 +5108,24 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.3" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" 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 +5137,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 +5146,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]] @@ -4552,17 +5168,6 @@ dependencies = [ "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", -] - [[package]] name = "tendril" version = "0.5.0" @@ -4646,28 +5251,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.117", +] + +[[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 +5342,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 +5386,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 +5399,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 +5410,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 +5419,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 +5461,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 +5495,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", @@ -4872,11 +5531,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.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" dependencies = [ "crossbeam-channel", "dirs", @@ -4888,10 +5565,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 +5585,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,17 +5641,38 @@ 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.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-xid" @@ -4995,6 +5693,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,9 +5725,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -5031,6 +5735,12 @@ dependencies = [ "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 +5801,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,11 +5809,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -5118,14 +5822,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -5136,23 +5840,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" 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.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5160,9 +5860,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -5173,9 +5873,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -5197,7 +5897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.13.0", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -5221,17 +5921,17 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.0", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap 2.14.0", "semver", ] [[package]] name = "web-audio-api" -version = "1.2.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "015053d21b75159d00b45f8b862d9c05b783440537d9751d659ad2982911eb7f" +checksum = "aae1d42666fbdedf3c4504894e1b615b1a74e158f131cba7a6e0138321b6de1c" dependencies = [ "almost", "arc-swap", @@ -5254,15 +5954,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.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -5270,14 +5970,14 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" 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 +6034,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]] @@ -5360,6 +6060,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 +6114,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 +6137,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 +6165,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 +6192,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 +6205,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 +6220,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.117", ] [[package]] @@ -5496,6 +6256,17 @@ dependencies = [ "syn 2.0.117", ] +[[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.117", +] + [[package]] name = "windows-interface" version = "0.59.3" @@ -5529,6 +6300,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 +6448,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 +6618,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", ] @@ -5860,6 +6647,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -5879,7 +6672,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.13.0", + "indexmap 2.14.0", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -5909,8 +6702,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", - "indexmap 2.13.0", + "bitflags 2.13.0", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -5929,7 +6722,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "semver", "serde", @@ -5941,15 +6734,15 @@ dependencies = [ [[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", @@ -5965,7 +6758,7 @@ dependencies = [ "javascriptcore-rs", "jni", "libc", - "ndk 0.9.0", + "ndk", "objc2", "objc2-app-kit", "objc2-core-foundation", @@ -5975,7 +6768,7 @@ dependencies = [ "once_cell", "percent-encoding", "raw-window-handle", - "sha2", + "sha2 0.10.9", "soup3", "tao-macros", "thiserror 2.0.18", @@ -6012,9 +6805,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,9 +6816,9 @@ 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", @@ -6035,9 +6828,9 @@ dependencies = [ [[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 +6855,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 0.7.15", + "winnow 1.0.3", "zbus_macros", "zbus_names", "zvariant", @@ -6070,9 +6863,9 @@ 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", @@ -6085,49 +6878,29 @@ 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", @@ -6137,9 +6910,9 @@ dependencies = [ [[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 +6921,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,9 +6932,9 @@ 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", @@ -6176,23 +6949,23 @@ 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", @@ -6203,13 +6976,13 @@ dependencies = [ [[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", + "winnow 1.0.3", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 47b049b..f2a7c91 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,26 +1,35 @@ +[workspace] +members = [".", "crates/plugin-sdk", "crates/lyrics-plugin"] + [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.5.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"] } +plugin-sdk = { path = "crates/plugin-sdk" } +lyrics-plugin = { path = "crates/lyrics-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/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..7d6c893 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/api.rs @@ -0,0 +1,32 @@ +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>; +} 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..d42c6bf --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/context.rs @@ -0,0 +1,53 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use crate::api::{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, +} + +impl PluginContext { + pub fn new( + host: Arc, + player: Arc, + library: Arc, + queue: Arc, + settings: Arc, + plugin_settings: Arc, + ) -> Self { + Self { + host, + player, + library, + queue, + settings, + plugin_settings, + } + } + + 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..c0bb3af --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/contributes.rs @@ -0,0 +1,29 @@ +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, +} 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/lib.rs b/src-tauri/crates/plugin-sdk/src/lib.rs new file mode 100644 index 0000000..a4368d7 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/lib.rs @@ -0,0 +1,23 @@ +pub mod api; +pub mod contributes; +pub mod context; +pub mod errors; +pub mod events; +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 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..7b41c71 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/manifest.rs @@ -0,0 +1,92 @@ +use serde::{Deserialize, Serialize}; + +use crate::contributes::{CommandContribution, MenuContribution, SidebarContribution}; +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, +} + +#[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..106b249 --- /dev/null +++ b/src-tauri/crates/plugin-sdk/src/permissions.rs @@ -0,0 +1,14 @@ +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, +} 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..ae8cb45 --- /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)] +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/src/audio/engine.rs b/src-tauri/src/audio/engine.rs new file mode 100644 index 0000000..c6137c9 --- /dev/null +++ b/src-tauri/src/audio/engine.rs @@ -0,0 +1,11 @@ +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; +} 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..b1b5b4c --- /dev/null +++ b/src-tauri/src/audio/state.rs @@ -0,0 +1,63 @@ +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, +} + +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, + }) + }); +} + +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..e838457 --- /dev/null +++ b/src-tauri/src/audio/web_audio.rs @@ -0,0 +1,86 @@ +use crate::audio::engine::AudioEngine; +use crate::audio::get_audio_context; +use crate::errors::AppError; +use std::path::Path; +use web_audio_api::{ + context::BaseAudioContext, + node::{AudioNode, GainNode, MediaElementAudioSourceNode, StereoPannerNode}, + MediaElement, +}; + +pub struct WebAudioEngine { + media: MediaElement, + src: MediaElementAudioSourceNode, + gain: GainNode, + panner: StereoPannerNode, +} + +impl WebAudioEngine { + pub fn new(file_path: &Path, volume: f32, pan: f32) -> Result { + let context = get_audio_context(); + let _ = context.resume(); + + 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(); + + src.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, + }) + } +} + +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() + } +} + +impl Drop for WebAudioEngine { + fn drop(&mut self) { + let _ = self.panner.disconnect(); + let _ = self.gain.disconnect(); + let _ = self.src.disconnect(); + self.media.pause(); + } +} 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..4efdf9d --- /dev/null +++ b/src-tauri/src/commands/mod.rs @@ -0,0 +1,9 @@ +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..e64bd39 --- /dev/null +++ b/src-tauri/src/commands/plugin_extensions.rs @@ -0,0 +1,77 @@ +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::sidebar_extension::SidebarExtension; +use crate::services::plugin::manager::PluginManager; +use plugin_sdk::settings::{PluginSettingMeta, SettingValue}; + +#[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()) +} + +/// 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_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) +} 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..27e022e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,46 +1,335 @@ -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, get_all_sidebar_extensions, get_menu_extensions, + get_plugin_manifests, get_plugin_settings, 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 audio::init_audio_state; - format!("Playing music: {}", name) -} +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()) + .setup(|app| { + let app_handle = app.handle().clone(); + + 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)); + + let setting_service = Arc::new(SettingsService::new(setting_repository)); + tauri::async_runtime::block_on(setting_service.get_settings())?; + + 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()); + + 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( + manifest_registry.clone(), + factory_registry.clone(), + context_factory, + capability_registry, + settings_registry, + storage, + )); + plugin_manager.load_persisted_state(); + plugin_manager.load_builtin_plugins(); + + 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()); + + let _ = init_media_controls(app.handle().clone()); + PlaybackService::spawn_playback_progress_task(app.handle().clone()); + + 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_plugin_manifests, + get_plugin_settings, + update_plugin_setting, + enable_plugin_command, + disable_plugin_command, + ]) .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..c7e98ba --- /dev/null +++ b/src-tauri/src/media_controls.rs @@ -0,0 +1,102 @@ +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 || { + 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..c10c7a2 --- /dev/null +++ b/src-tauri/src/services/playback_service.rs @@ -0,0 +1,282 @@ +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; + + let mut engine = match WebAudioEngine::new(file_path, volume, pan) { + 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/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..0569052 --- /dev/null +++ b/src-tauri/src/services/plugin/adapters/mod.rs @@ -0,0 +1,11 @@ +pub mod library_adapter; +pub mod player_adapter; +pub mod plugin_settings_adapter; +pub mod queue_adapter; +pub mod settings_adapter; + +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/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/embedded.rs b/src-tauri/src/services/plugin/embedded.rs new file mode 100644 index 0000000..b66c1bc --- /dev/null +++ b/src-tauri/src/services/plugin/embedded.rs @@ -0,0 +1,19 @@ +//! 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"); 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..26cf816 --- /dev/null +++ b/src-tauri/src/services/plugin/extension/mod.rs @@ -0,0 +1,11 @@ +pub mod command_extension; +pub mod menu_extension; +pub mod registry; +pub mod sidebar_extension; +pub mod state; + +pub use command_extension::CommandExtension; +pub use menu_extension::MenuExtension; +pub use registry::ExtensionRegistry; +pub use sidebar_extension::SidebarExtension; +pub use 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..a576399 --- /dev/null +++ b/src-tauri/src/services/plugin/extension/registry.rs @@ -0,0 +1,93 @@ +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::sidebar_extension::SidebarExtension; +use crate::services::plugin::extension::state::ExtensionState; + +pub struct ExtensionRegistry { + menus: RwLock>>, + sidebars: RwLock>>, + commands: RwLock>>, +} + +impl ExtensionRegistry { + pub fn new() -> Self { + Self { + menus: RwLock::new(HashMap::new()), + sidebars: RwLock::new(HashMap::new()), + commands: 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 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); + } + } + + 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() + } +} 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..fc9d7a0 --- /dev/null +++ b/src-tauri/src/services/plugin/loader.rs @@ -0,0 +1,42 @@ +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 + ))); + } + 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..13fb69f --- /dev/null +++ b/src-tauri/src/services/plugin/manager.rs @@ -0,0 +1,747 @@ +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, RwLock}; + +use crate::errors::AppError; +use crate::events::AppEvent; +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 crate::services::plugin::extension::command_extension::CommandExtension; +use crate::services::plugin::extension::menu_extension::MenuExtension; +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}; + +pub struct PluginManager { + 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>, +} + +impl PluginManager { + pub fn new( + manifest_registry: Arc, + factory_registry: Arc, + context_factory: PluginContextFactory, + capability_registry: Arc, + settings_registry: Arc, + storage: Arc, + ) -> Self { + Self { + 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()), + } + } + + 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); + } + } + + /// 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, + })); + } + + 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.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(()) => { + 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()) + } + } + } + + 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(()) => { + 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 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() + } +} + +/// 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)), + _ => 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..f623939 --- /dev/null +++ b/src-tauri/src/services/plugin/mod.rs @@ -0,0 +1,35 @@ +pub mod adapters; +pub mod capability; +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 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..0f69bc1 --- /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]; + 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..6762ae4 --- /dev/null +++ b/src-tauri/src/services/plugin/provider/mod.rs @@ -0,0 +1,36 @@ +//! 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; +// Reserved for future expansion: +// pub mod packaged; +// pub mod user; + +pub use builtin::BuiltinManifestProvider; + +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/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..7618ca0 --- /dev/null +++ b/src-tauri/src/services/plugin/runtime/context_factory.rs @@ -0,0 +1,75 @@ +use std::sync::Arc; + +use tauri::AppHandle; + +use plugin_sdk::context::PluginContext; +use crate::services::playback_service::PlaybackService; +use crate::services::plugin::adapters::{ + 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), + )); + + PluginContext::new(host, player, library, queue, settings, plugin_settings) + } +} 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..2a3d2ca 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' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: asset: https://asset.localhost; connect-src 'self' ipc: http://ipc.localhost" } }, "bundle": { "active": true, - "targets": "all", + "targets": ["nsis", "msi", "dmg", "appimage"], "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -36,4 +36,4 @@ "../static/**/*" ] } -} \ No newline at end of file +} diff --git a/src/app.css b/src/app.css index 348e34e..b857282 100644 --- a/src/app.css +++ b/src/app.css @@ -3,404 +3,412 @@ @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+ */ { + +@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..6afa1ad 100644 --- a/src/app.html +++ b/src/app.html @@ -6,36 +6,21 @@ - Tauri + SvelteKit + Typescript App + Rust Echo Music - - - - - - - - - - - - - + + + + + + + + + + + + + %sveltekit.head% 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..e4760da --- /dev/null +++ b/src/lib/components/base/Slider.svelte @@ -0,0 +1,222 @@ + + +
+
+
+
+ +
+ +
+ {Math.round(value)} +
+
+ + \ No newline at end of file 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..4baca99 --- /dev/null +++ b/src/lib/components/media/MediaGrid.svelte @@ -0,0 +1,226 @@ + + +{#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/Filters.svelte b/src/lib/features/Filters.svelte new file mode 100644 index 0000000..b451e16 --- /dev/null +++ b/src/lib/features/Filters.svelte @@ -0,0 +1,37 @@ + + +
+ + + setThemeMode(value as ThemeMode)} + /> +
+ + + + + handleSwitchChange(event, 'reduceMotion')} + > + + + + + + + handleSwitchChange(event, 'scanOnStartup')} + > + +
+
+
媒体库目录
+
+ 管理应用扫描音乐文件的本地文件夹 +
+
+ +
+ +
+ + {#if settings.data?.libraryDirs?.length > 0} +
+ {#each settings.data.libraryDirs as dir (dir)} + + + + {/each} +
+ {:else} +
+ 暂未添加媒体库目录,应用将无法扫描到任何歌曲。 +
+ {/if} +
+
+ + + + + handleSwitchChange(event, 'pluginScanOnStartup')} + > + + + + + handleSwitchChange(event, 'pluginDevMode')} + > + + + +
+