Compare commits

..

10 Commits

Author SHA1 Message Date
Youwes09 9f8bf6ffc1 Chore: Tagged 0.9.3 V2 2026-05-16 07:59:35 -05:00
Youwes09 39f813b4d7 Chore: Tagged 0.9.3 2026-05-16 07:57:26 -05:00
Youwes09 18ac38e888 Feat: Disable Auto-Complete on Moku 2026-05-16 07:56:05 -05:00
Shozikan 1e2e923eab Chore: Merge pull request #79 from zerebos/feat/page-loaders
Add circular loaders to pages
2026-05-16 07:41:28 -05:00
Youwes09 d3a40b9152 Fix: System.rs PathBuf Error 2026-05-16 03:54:05 -05:00
Zerebos b1444582a3 Add circular loaders to pages 2026-05-16 01:01:12 -04:00
Youwes09 bf3f68b996 Feat: Minimize to Sys-Tray + MultiModal (#76) 2026-05-15 21:21:31 -05:00
Youwes09 4b728ad5b7 Feat: Middle-Click for Browser-Auto-Scroll (#70) 2026-05-15 20:41:21 -05:00
Youwes09 f3f91f1555 Feat: Auto-Scroll & Double-Tap Adjustment (#69) 2026-05-15 20:36:15 -05:00
Youwes09 062662781a Feat: Bulk-Source Migration (#66) 2026-05-15 19:49:26 -05:00
24 changed files with 1558 additions and 748 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
pkgname=moku pkgname=moku
pkgver=0.9.2 pkgver=0.9.3
pkgrel=1 pkgrel=1
pkgdesc="Native Linux manga reader frontend for Suwayomi-Server" pkgdesc="Native Linux manga reader frontend for Suwayomi-Server"
arch=('x86_64') arch=('x86_64')
+2 -1
View File
@@ -22,7 +22,7 @@
perSystem = perSystem =
{ system, lib, ... }: { system, lib, ... }:
let let
version = "0.9.2"; version = "0.9.3";
pkgs = import inputs.nixpkgs { pkgs = import inputs.nixpkgs {
inherit system; inherit system;
@@ -128,6 +128,7 @@
export NO_STRIP=true export NO_STRIP=true
export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig''${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig''${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
export XDG_DATA_DIRS="${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" export XDG_DATA_DIRS="${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath runtimeLibs}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
echo "Moku dev shell pnpm install && pnpm tauri:dev" echo "Moku dev shell pnpm install && pnpm tauri:dev"
echo "" echo ""
+2 -2
View File
@@ -179,11 +179,11 @@ modules:
sources: sources:
- type: git - type: git
url: https://github.com/moku-project/Moku.git url: https://github.com/moku-project/Moku.git
tag: v0.9.2 tag: v0.9.3
commit: 83711c155d3e60ab4e2411ea6e0098231d76f8b9 commit: 83711c155d3e60ab4e2411ea6e0098231d76f8b9
- type: file - type: file
path: packaging/frontend-dist.tar.gz path: packaging/frontend-dist.tar.gz
sha256: 22128c591ddacac218b7223106ed3c3f052799db2a647247789492b925370086 sha256: c690eb3cb24e89fec3f4e92f7a4a82d9a465b58f6680a332c1e44f1361ac96af
- packaging/cargo-sources.json - packaging/cargo-sources.json
- type: inline - type: inline
dest: src-tauri/.cargo dest: src-tauri/.cargo
+188 -266
View File
@@ -265,6 +265,19 @@
"dest": "cargo/vendor/brotli-decompressor-5.0.0", "dest": "cargo/vendor/brotli-decompressor-5.0.0",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/bs58/bs58-0.5.1.crate",
"sha256": "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4",
"dest": "cargo/vendor/bs58-0.5.1"
},
{
"type": "inline",
"contents": "{\"package\": \"bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4\", \"files\": {}}",
"dest": "cargo/vendor/bs58-0.5.1",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -398,14 +411,14 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/cc/cc-1.2.61.crate", "url": "https://static.crates.io/crates/cc/cc-1.2.62.crate",
"sha256": "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d", "sha256": "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98",
"dest": "cargo/vendor/cc-1.2.61" "dest": "cargo/vendor/cc-1.2.62"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d\", \"files\": {}}", "contents": "{\"package\": \"a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98\", \"files\": {}}",
"dest": "cargo/vendor/cc-1.2.61", "dest": "cargo/vendor/cc-1.2.62",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -1750,14 +1763,14 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/h2/h2-0.4.13.crate", "url": "https://static.crates.io/crates/h2/h2-0.4.14.crate",
"sha256": "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54", "sha256": "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733",
"dest": "cargo/vendor/h2-0.4.13" "dest": "cargo/vendor/h2-0.4.14"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54\", \"files\": {}}", "contents": "{\"package\": \"171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733\", \"files\": {}}",
"dest": "cargo/vendor/h2-0.4.13", "dest": "cargo/vendor/h2-0.4.14",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -1789,14 +1802,14 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/hashbrown/hashbrown-0.17.0.crate", "url": "https://static.crates.io/crates/hashbrown/hashbrown-0.17.1.crate",
"sha256": "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51", "sha256": "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a",
"dest": "cargo/vendor/hashbrown-0.17.0" "dest": "cargo/vendor/hashbrown-0.17.1"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51\", \"files\": {}}", "contents": "{\"package\": \"ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a\", \"files\": {}}",
"dest": "cargo/vendor/hashbrown-0.17.0", "dest": "cargo/vendor/hashbrown-0.17.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -2189,19 +2202,6 @@
"dest": "cargo/vendor/ipnet-2.12.0", "dest": "cargo/vendor/ipnet-2.12.0",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/iri-string/iri-string-0.7.12.crate",
"sha256": "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20",
"dest": "cargo/vendor/iri-string-0.7.12"
},
{
"type": "inline",
"contents": "{\"package\": \"25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20\", \"files\": {}}",
"dest": "cargo/vendor/iri-string-0.7.12",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -2322,14 +2322,14 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/js-sys/js-sys-0.3.97.crate", "url": "https://static.crates.io/crates/js-sys/js-sys-0.3.98.crate",
"sha256": "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf", "sha256": "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08",
"dest": "cargo/vendor/js-sys-0.3.97" "dest": "cargo/vendor/js-sys-0.3.98"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf\", \"files\": {}}", "contents": "{\"package\": \"67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08\", \"files\": {}}",
"dest": "cargo/vendor/js-sys-0.3.97", "dest": "cargo/vendor/js-sys-0.3.98",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -3011,27 +3011,27 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/open/open-5.3.4.crate", "url": "https://static.crates.io/crates/open/open-5.3.5.crate",
"sha256": "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd", "sha256": "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c",
"dest": "cargo/vendor/open-5.3.4" "dest": "cargo/vendor/open-5.3.5"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd\", \"files\": {}}", "contents": "{\"package\": \"2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c\", \"files\": {}}",
"dest": "cargo/vendor/open-5.3.4", "dest": "cargo/vendor/open-5.3.5",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/openssl/openssl-0.10.78.crate", "url": "https://static.crates.io/crates/openssl/openssl-0.10.79.crate",
"sha256": "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222", "sha256": "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542",
"dest": "cargo/vendor/openssl-0.10.78" "dest": "cargo/vendor/openssl-0.10.79"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222\", \"files\": {}}", "contents": "{\"package\": \"bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542\", \"files\": {}}",
"dest": "cargo/vendor/openssl-0.10.78", "dest": "cargo/vendor/openssl-0.10.79",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -3063,14 +3063,14 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/openssl-sys/openssl-sys-0.9.114.crate", "url": "https://static.crates.io/crates/openssl-sys/openssl-sys-0.9.115.crate",
"sha256": "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6", "sha256": "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781",
"dest": "cargo/vendor/openssl-sys-0.9.114" "dest": "cargo/vendor/openssl-sys-0.9.115"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6\", \"files\": {}}", "contents": "{\"package\": \"158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781\", \"files\": {}}",
"dest": "cargo/vendor/openssl-sys-0.9.114", "dest": "cargo/vendor/openssl-sys-0.9.115",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -3190,19 +3190,6 @@
"dest": "cargo/vendor/percent-encoding-2.3.2", "dest": "cargo/vendor/percent-encoding-2.3.2",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/phf/phf-0.11.3.crate",
"sha256": "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078",
"dest": "cargo/vendor/phf-0.11.3"
},
{
"type": "inline",
"contents": "{\"package\": \"1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078\", \"files\": {}}",
"dest": "cargo/vendor/phf-0.11.3",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -3229,19 +3216,6 @@
"dest": "cargo/vendor/phf_codegen-0.13.1", "dest": "cargo/vendor/phf_codegen-0.13.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/phf_generator/phf_generator-0.11.3.crate",
"sha256": "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d",
"dest": "cargo/vendor/phf_generator-0.11.3"
},
{
"type": "inline",
"contents": "{\"package\": \"3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d\", \"files\": {}}",
"dest": "cargo/vendor/phf_generator-0.11.3",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -3255,19 +3229,6 @@
"dest": "cargo/vendor/phf_generator-0.13.1", "dest": "cargo/vendor/phf_generator-0.13.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/phf_macros/phf_macros-0.11.3.crate",
"sha256": "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216",
"dest": "cargo/vendor/phf_macros-0.11.3"
},
{
"type": "inline",
"contents": "{\"package\": \"f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216\", \"files\": {}}",
"dest": "cargo/vendor/phf_macros-0.11.3",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -3281,19 +3242,6 @@
"dest": "cargo/vendor/phf_macros-0.13.1", "dest": "cargo/vendor/phf_macros-0.13.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/phf_shared/phf_shared-0.11.3.crate",
"sha256": "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5",
"dest": "cargo/vendor/phf_shared-0.11.3"
},
{
"type": "inline",
"contents": "{\"package\": \"67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5\", \"files\": {}}",
"dest": "cargo/vendor/phf_shared-0.11.3",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -3544,14 +3492,14 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/quick-xml/quick-xml-0.39.2.crate", "url": "https://static.crates.io/crates/quick-xml/quick-xml-0.39.4.crate",
"sha256": "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d", "sha256": "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e",
"dest": "cargo/vendor/quick-xml-0.39.2" "dest": "cargo/vendor/quick-xml-0.39.4"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d\", \"files\": {}}", "contents": "{\"package\": \"cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e\", \"files\": {}}",
"dest": "cargo/vendor/quick-xml-0.39.2", "dest": "cargo/vendor/quick-xml-0.39.4",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -3632,19 +3580,6 @@
"dest": "cargo/vendor/r-efi-6.0.0", "dest": "cargo/vendor/r-efi-6.0.0",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/rand/rand-0.8.6.crate",
"sha256": "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a",
"dest": "cargo/vendor/rand-0.8.6"
},
{
"type": "inline",
"contents": "{\"package\": \"5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a\", \"files\": {}}",
"dest": "cargo/vendor/rand-0.8.6",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -3671,19 +3606,6 @@
"dest": "cargo/vendor/rand_chacha-0.9.0", "dest": "cargo/vendor/rand_chacha-0.9.0",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/rand_core/rand_core-0.6.4.crate",
"sha256": "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c",
"dest": "cargo/vendor/rand_core-0.6.4"
},
{
"type": "inline",
"contents": "{\"package\": \"ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c\", \"files\": {}}",
"dest": "cargo/vendor/rand_core-0.6.4",
"dest-filename": ".cargo-checksum.json"
},
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
@@ -4272,27 +4194,27 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/serde_with/serde_with-3.18.0.crate", "url": "https://static.crates.io/crates/serde_with/serde_with-3.20.0.crate",
"sha256": "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f", "sha256": "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2",
"dest": "cargo/vendor/serde_with-3.18.0" "dest": "cargo/vendor/serde_with-3.20.0"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f\", \"files\": {}}", "contents": "{\"package\": \"e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2\", \"files\": {}}",
"dest": "cargo/vendor/serde_with-3.18.0", "dest": "cargo/vendor/serde_with-3.20.0",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/serde_with_macros/serde_with_macros-3.18.0.crate", "url": "https://static.crates.io/crates/serde_with_macros/serde_with_macros-3.20.0.crate",
"sha256": "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65", "sha256": "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac",
"dest": "cargo/vendor/serde_with_macros-3.18.0" "dest": "cargo/vendor/serde_with_macros-3.20.0"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65\", \"files\": {}}", "contents": "{\"package\": \"b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac\", \"files\": {}}",
"dest": "cargo/vendor/serde_with_macros-3.18.0", "dest": "cargo/vendor/serde_with_macros-3.20.0",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -4428,14 +4350,14 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/siphasher/siphasher-1.0.2.crate", "url": "https://static.crates.io/crates/siphasher/siphasher-1.0.3.crate",
"sha256": "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e", "sha256": "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649",
"dest": "cargo/vendor/siphasher-1.0.2" "dest": "cargo/vendor/siphasher-1.0.3"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e\", \"files\": {}}", "contents": "{\"package\": \"8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649\", \"files\": {}}",
"dest": "cargo/vendor/siphasher-1.0.2", "dest": "cargo/vendor/siphasher-1.0.3",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -4727,14 +4649,14 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tao/tao-0.35.0.crate", "url": "https://static.crates.io/crates/tao/tao-0.35.2.crate",
"sha256": "1cf65722394c2ac443e80120064987f8914ee1d4e4e36e63cdf10f2990f01159", "sha256": "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4",
"dest": "cargo/vendor/tao-0.35.0" "dest": "cargo/vendor/tao-0.35.2"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"1cf65722394c2ac443e80120064987f8914ee1d4e4e36e63cdf10f2990f01159\", \"files\": {}}", "contents": "{\"package\": \"a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4\", \"files\": {}}",
"dest": "cargo/vendor/tao-0.35.0", "dest": "cargo/vendor/tao-0.35.2",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -4766,79 +4688,79 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tauri/tauri-2.11.0.crate", "url": "https://static.crates.io/crates/tauri/tauri-2.11.1.crate",
"sha256": "d059f2527558d9dba6f186dec4772610e1aecfd3f94002397613e7e648752b66", "sha256": "b93bd86d231f0a8138f11a02a584769fe4b703dc36ae133d783228dbc4801405",
"dest": "cargo/vendor/tauri-2.11.0" "dest": "cargo/vendor/tauri-2.11.1"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"d059f2527558d9dba6f186dec4772610e1aecfd3f94002397613e7e648752b66\", \"files\": {}}", "contents": "{\"package\": \"b93bd86d231f0a8138f11a02a584769fe4b703dc36ae133d783228dbc4801405\", \"files\": {}}",
"dest": "cargo/vendor/tauri-2.11.0", "dest": "cargo/vendor/tauri-2.11.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tauri-build/tauri-build-2.6.0.crate", "url": "https://static.crates.io/crates/tauri-build/tauri-build-2.6.1.crate",
"sha256": "be9aa8c59a894f76c29a002501c589de5eb4987a5913d62a6e0a47f320901988", "sha256": "3a318b234cc2dea65f575467bafcfb76286bce228ebc3778e337d61d03213007",
"dest": "cargo/vendor/tauri-build-2.6.0" "dest": "cargo/vendor/tauri-build-2.6.1"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"be9aa8c59a894f76c29a002501c589de5eb4987a5913d62a6e0a47f320901988\", \"files\": {}}", "contents": "{\"package\": \"3a318b234cc2dea65f575467bafcfb76286bce228ebc3778e337d61d03213007\", \"files\": {}}",
"dest": "cargo/vendor/tauri-build-2.6.0", "dest": "cargo/vendor/tauri-build-2.6.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tauri-codegen/tauri-codegen-2.6.0.crate", "url": "https://static.crates.io/crates/tauri-codegen/tauri-codegen-2.6.1.crate",
"sha256": "d3e4e8230d565106aa19dfbaa01a7ed01abf78047fe0577a83377224bd1bf20e", "sha256": "6bd11644962add2549a60b7e7c6800f17d7020156e02f516021d8103e80cc528",
"dest": "cargo/vendor/tauri-codegen-2.6.0" "dest": "cargo/vendor/tauri-codegen-2.6.1"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"d3e4e8230d565106aa19dfbaa01a7ed01abf78047fe0577a83377224bd1bf20e\", \"files\": {}}", "contents": "{\"package\": \"6bd11644962add2549a60b7e7c6800f17d7020156e02f516021d8103e80cc528\", \"files\": {}}",
"dest": "cargo/vendor/tauri-codegen-2.6.0", "dest": "cargo/vendor/tauri-codegen-2.6.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tauri-macros/tauri-macros-2.6.0.crate", "url": "https://static.crates.io/crates/tauri-macros/tauri-macros-2.6.1.crate",
"sha256": "bc8de2cddbbc33dbdf4c84f170121886595efdbcc9cb4b3d76342b79d082cedc", "sha256": "fed9d3742a37a355d2e47c9af924e9fbc112abb76f9835d35d4780e318419502",
"dest": "cargo/vendor/tauri-macros-2.6.0" "dest": "cargo/vendor/tauri-macros-2.6.1"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"bc8de2cddbbc33dbdf4c84f170121886595efdbcc9cb4b3d76342b79d082cedc\", \"files\": {}}", "contents": "{\"package\": \"fed9d3742a37a355d2e47c9af924e9fbc112abb76f9835d35d4780e318419502\", \"files\": {}}",
"dest": "cargo/vendor/tauri-macros-2.6.0", "dest": "cargo/vendor/tauri-macros-2.6.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tauri-plugin/tauri-plugin-2.6.0.crate", "url": "https://static.crates.io/crates/tauri-plugin/tauri-plugin-2.6.1.crate",
"sha256": "f8d5f58bfd0cdcfdbc0a68dc08b354eea2afc551b421de91b07b69e0dd769d57", "sha256": "eefb2c18e8a605c23edb48fc56bb77381199e1a1e7f6ff0c9b970afe7b3cb8ee",
"dest": "cargo/vendor/tauri-plugin-2.6.0" "dest": "cargo/vendor/tauri-plugin-2.6.1"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"f8d5f58bfd0cdcfdbc0a68dc08b354eea2afc551b421de91b07b69e0dd769d57\", \"files\": {}}", "contents": "{\"package\": \"eefb2c18e8a605c23edb48fc56bb77381199e1a1e7f6ff0c9b970afe7b3cb8ee\", \"files\": {}}",
"dest": "cargo/vendor/tauri-plugin-2.6.0", "dest": "cargo/vendor/tauri-plugin-2.6.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tauri-plugin-dialog/tauri-plugin-dialog-2.7.0.crate", "url": "https://static.crates.io/crates/tauri-plugin-dialog/tauri-plugin-dialog-2.7.1.crate",
"sha256": "a1fa4150c95ae391946cc8b8f905ab14797427caba3a8a2f79628e956da91809", "sha256": "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884",
"dest": "cargo/vendor/tauri-plugin-dialog-2.7.0" "dest": "cargo/vendor/tauri-plugin-dialog-2.7.1"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"a1fa4150c95ae391946cc8b8f905ab14797427caba3a8a2f79628e956da91809\", \"files\": {}}", "contents": "{\"package\": \"65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884\", \"files\": {}}",
"dest": "cargo/vendor/tauri-plugin-dialog-2.7.0", "dest": "cargo/vendor/tauri-plugin-dialog-2.7.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -4862,27 +4784,27 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tauri-plugin-fs/tauri-plugin-fs-2.5.0.crate", "url": "https://static.crates.io/crates/tauri-plugin-fs/tauri-plugin-fs-2.5.1.crate",
"sha256": "36e1ec28b79f3d0683f4507e1615c36292c0ea6716668770d4396b9b39871ed8", "sha256": "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371",
"dest": "cargo/vendor/tauri-plugin-fs-2.5.0" "dest": "cargo/vendor/tauri-plugin-fs-2.5.1"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"36e1ec28b79f3d0683f4507e1615c36292c0ea6716668770d4396b9b39871ed8\", \"files\": {}}", "contents": "{\"package\": \"b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371\", \"files\": {}}",
"dest": "cargo/vendor/tauri-plugin-fs-2.5.0", "dest": "cargo/vendor/tauri-plugin-fs-2.5.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tauri-plugin-http/tauri-plugin-http-2.5.8.crate", "url": "https://static.crates.io/crates/tauri-plugin-http/tauri-plugin-http-2.5.9.crate",
"sha256": "cfba7d4ec72763f9d1fdf73c217747f01e2c84b08b87a8cacd2f94f35853f84d", "sha256": "b5bd512048e1985b7ec78f96d99083e2ddaf7e0d906b2b63c44ce5bb8b894067",
"dest": "cargo/vendor/tauri-plugin-http-2.5.8" "dest": "cargo/vendor/tauri-plugin-http-2.5.9"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"cfba7d4ec72763f9d1fdf73c217747f01e2c84b08b87a8cacd2f94f35853f84d\", \"files\": {}}", "contents": "{\"package\": \"b5bd512048e1985b7ec78f96d99083e2ddaf7e0d906b2b63c44ce5bb8b894067\", \"files\": {}}",
"dest": "cargo/vendor/tauri-plugin-http-2.5.8", "dest": "cargo/vendor/tauri-plugin-http-2.5.9",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -4927,53 +4849,53 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tauri-plugin-store/tauri-plugin-store-2.4.2.crate", "url": "https://static.crates.io/crates/tauri-plugin-store/tauri-plugin-store-2.4.3.crate",
"sha256": "5ca1a8ff83c269b115e98726ffc13f9e548a10161544a92ad121d6d0a96e16ea", "sha256": "6c72dda16786eb4a3f903e43a17b64d8d78dc0f00fe2aa4b757c28f617a8630b",
"dest": "cargo/vendor/tauri-plugin-store-2.4.2" "dest": "cargo/vendor/tauri-plugin-store-2.4.3"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"5ca1a8ff83c269b115e98726ffc13f9e548a10161544a92ad121d6d0a96e16ea\", \"files\": {}}", "contents": "{\"package\": \"6c72dda16786eb4a3f903e43a17b64d8d78dc0f00fe2aa4b757c28f617a8630b\", \"files\": {}}",
"dest": "cargo/vendor/tauri-plugin-store-2.4.2", "dest": "cargo/vendor/tauri-plugin-store-2.4.3",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tauri-runtime/tauri-runtime-2.11.0.crate", "url": "https://static.crates.io/crates/tauri-runtime/tauri-runtime-2.11.1.crate",
"sha256": "1e42bbcb76237351fbaa02f08d808c537dc12eb5a6eabbf3e517b50056334d95", "sha256": "8fef478ba1d2ac21c2d528740b24d0cb315e1e8b1111aae53fafac34804371fc",
"dest": "cargo/vendor/tauri-runtime-2.11.0" "dest": "cargo/vendor/tauri-runtime-2.11.1"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"1e42bbcb76237351fbaa02f08d808c537dc12eb5a6eabbf3e517b50056334d95\", \"files\": {}}", "contents": "{\"package\": \"8fef478ba1d2ac21c2d528740b24d0cb315e1e8b1111aae53fafac34804371fc\", \"files\": {}}",
"dest": "cargo/vendor/tauri-runtime-2.11.0", "dest": "cargo/vendor/tauri-runtime-2.11.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tauri-runtime-wry/tauri-runtime-wry-2.11.0.crate", "url": "https://static.crates.io/crates/tauri-runtime-wry/tauri-runtime-wry-2.11.1.crate",
"sha256": "2cadb13dad0c681e1e0a2c49ae488f0e2906ded3d57e7a0017f4aaf46e387117", "sha256": "a3989df2ae1c476404fe0a2e8ffc4cfbde97e51efd613c2bb5355fbc9ab52cf0",
"dest": "cargo/vendor/tauri-runtime-wry-2.11.0" "dest": "cargo/vendor/tauri-runtime-wry-2.11.1"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"2cadb13dad0c681e1e0a2c49ae488f0e2906ded3d57e7a0017f4aaf46e387117\", \"files\": {}}", "contents": "{\"package\": \"a3989df2ae1c476404fe0a2e8ffc4cfbde97e51efd613c2bb5355fbc9ab52cf0\", \"files\": {}}",
"dest": "cargo/vendor/tauri-runtime-wry-2.11.0", "dest": "cargo/vendor/tauri-runtime-wry-2.11.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tauri-utils/tauri-utils-2.9.0.crate", "url": "https://static.crates.io/crates/tauri-utils/tauri-utils-2.9.1.crate",
"sha256": "55f61d2bf7188fbcf2b0ed095b67a6bc498f713c939314bb19eb700118a573b7", "sha256": "d57200389a2f82b4b0a40ae29ca19b6978116e8f4d4e974c3234ce40c0ffbdec",
"dest": "cargo/vendor/tauri-utils-2.9.0" "dest": "cargo/vendor/tauri-utils-2.9.1"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"55f61d2bf7188fbcf2b0ed095b67a6bc498f713c939314bb19eb700118a573b7\", \"files\": {}}", "contents": "{\"package\": \"d57200389a2f82b4b0a40ae29ca19b6978116e8f4d4e974c3234ce40c0ffbdec\", \"files\": {}}",
"dest": "cargo/vendor/tauri-utils-2.9.0", "dest": "cargo/vendor/tauri-utils-2.9.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -5148,14 +5070,14 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tokio/tokio-1.52.1.crate", "url": "https://static.crates.io/crates/tokio/tokio-1.52.3.crate",
"sha256": "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6", "sha256": "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe",
"dest": "cargo/vendor/tokio-1.52.1" "dest": "cargo/vendor/tokio-1.52.3"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6\", \"files\": {}}", "contents": "{\"package\": \"8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe\", \"files\": {}}",
"dest": "cargo/vendor/tokio-1.52.1", "dest": "cargo/vendor/tokio-1.52.3",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -5369,14 +5291,14 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tower-http/tower-http-0.6.8.crate", "url": "https://static.crates.io/crates/tower-http/tower-http-0.6.10.crate",
"sha256": "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8", "sha256": "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51",
"dest": "cargo/vendor/tower-http-0.6.8" "dest": "cargo/vendor/tower-http-0.6.10"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8\", \"files\": {}}", "contents": "{\"package\": \"68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51\", \"files\": {}}",
"dest": "cargo/vendor/tower-http-0.6.8", "dest": "cargo/vendor/tower-http-0.6.10",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -5837,66 +5759,66 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/wasm-bindgen/wasm-bindgen-0.2.120.crate", "url": "https://static.crates.io/crates/wasm-bindgen/wasm-bindgen-0.2.121.crate",
"sha256": "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1", "sha256": "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790",
"dest": "cargo/vendor/wasm-bindgen-0.2.120" "dest": "cargo/vendor/wasm-bindgen-0.2.121"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1\", \"files\": {}}", "contents": "{\"package\": \"49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790\", \"files\": {}}",
"dest": "cargo/vendor/wasm-bindgen-0.2.120", "dest": "cargo/vendor/wasm-bindgen-0.2.121",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/wasm-bindgen-futures/wasm-bindgen-futures-0.4.70.crate", "url": "https://static.crates.io/crates/wasm-bindgen-futures/wasm-bindgen-futures-0.4.71.crate",
"sha256": "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084", "sha256": "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8",
"dest": "cargo/vendor/wasm-bindgen-futures-0.4.70" "dest": "cargo/vendor/wasm-bindgen-futures-0.4.71"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084\", \"files\": {}}", "contents": "{\"package\": \"96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8\", \"files\": {}}",
"dest": "cargo/vendor/wasm-bindgen-futures-0.4.70", "dest": "cargo/vendor/wasm-bindgen-futures-0.4.71",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/wasm-bindgen-macro/wasm-bindgen-macro-0.2.120.crate", "url": "https://static.crates.io/crates/wasm-bindgen-macro/wasm-bindgen-macro-0.2.121.crate",
"sha256": "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103", "sha256": "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578",
"dest": "cargo/vendor/wasm-bindgen-macro-0.2.120" "dest": "cargo/vendor/wasm-bindgen-macro-0.2.121"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103\", \"files\": {}}", "contents": "{\"package\": \"8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578\", \"files\": {}}",
"dest": "cargo/vendor/wasm-bindgen-macro-0.2.120", "dest": "cargo/vendor/wasm-bindgen-macro-0.2.121",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/wasm-bindgen-macro-support/wasm-bindgen-macro-support-0.2.120.crate", "url": "https://static.crates.io/crates/wasm-bindgen-macro-support/wasm-bindgen-macro-support-0.2.121.crate",
"sha256": "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41", "sha256": "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2",
"dest": "cargo/vendor/wasm-bindgen-macro-support-0.2.120" "dest": "cargo/vendor/wasm-bindgen-macro-support-0.2.121"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41\", \"files\": {}}", "contents": "{\"package\": \"d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2\", \"files\": {}}",
"dest": "cargo/vendor/wasm-bindgen-macro-support-0.2.120", "dest": "cargo/vendor/wasm-bindgen-macro-support-0.2.121",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/wasm-bindgen-shared/wasm-bindgen-shared-0.2.120.crate", "url": "https://static.crates.io/crates/wasm-bindgen-shared/wasm-bindgen-shared-0.2.121.crate",
"sha256": "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea", "sha256": "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441",
"dest": "cargo/vendor/wasm-bindgen-shared-0.2.120" "dest": "cargo/vendor/wasm-bindgen-shared-0.2.121"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea\", \"files\": {}}", "contents": "{\"package\": \"c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441\", \"files\": {}}",
"dest": "cargo/vendor/wasm-bindgen-shared-0.2.120", "dest": "cargo/vendor/wasm-bindgen-shared-0.2.121",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -5954,14 +5876,14 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/web-sys/web-sys-0.3.97.crate", "url": "https://static.crates.io/crates/web-sys/web-sys-0.3.98.crate",
"sha256": "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602", "sha256": "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa",
"dest": "cargo/vendor/web-sys-0.3.97" "dest": "cargo/vendor/web-sys-0.3.98"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602\", \"files\": {}}", "contents": "{\"package\": \"4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa\", \"files\": {}}",
"dest": "cargo/vendor/web-sys-0.3.97", "dest": "cargo/vendor/web-sys-0.3.98",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -7046,14 +6968,14 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/winnow/winnow-1.0.2.crate", "url": "https://static.crates.io/crates/winnow/winnow-1.0.3.crate",
"sha256": "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0", "sha256": "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1",
"dest": "cargo/vendor/winnow-1.0.2" "dest": "cargo/vendor/winnow-1.0.3"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0\", \"files\": {}}", "contents": "{\"package\": \"0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1\", \"files\": {}}",
"dest": "cargo/vendor/winnow-1.0.2", "dest": "cargo/vendor/winnow-1.0.3",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -7176,14 +7098,14 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/wry/wry-0.55.0.crate", "url": "https://static.crates.io/crates/wry/wry-0.55.1.crate",
"sha256": "3013fd6116aac351dd2e18f349b28b2cfef3a5ff3253a9d0ce2d7193bb1b4429", "sha256": "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514",
"dest": "cargo/vendor/wry-0.55.0" "dest": "cargo/vendor/wry-0.55.1"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"3013fd6116aac351dd2e18f349b28b2cfef3a5ff3253a9d0ce2d7193bb1b4429\", \"files\": {}}", "contents": "{\"package\": \"186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514\", \"files\": {}}",
"dest": "cargo/vendor/wry-0.55.0", "dest": "cargo/vendor/wry-0.55.1",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
@@ -7267,14 +7189,14 @@
{ {
"type": "archive", "type": "archive",
"archive-type": "tar-gzip", "archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/zerofrom/zerofrom-0.1.7.crate", "url": "https://static.crates.io/crates/zerofrom/zerofrom-0.1.8.crate",
"sha256": "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df", "sha256": "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272",
"dest": "cargo/vendor/zerofrom-0.1.7" "dest": "cargo/vendor/zerofrom-0.1.8"
}, },
{ {
"type": "inline", "type": "inline",
"contents": "{\"package\": \"69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df\", \"files\": {}}", "contents": "{\"package\": \"0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272\", \"files\": {}}",
"dest": "cargo/vendor/zerofrom-0.1.7", "dest": "cargo/vendor/zerofrom-0.1.8",
"dest-filename": ".cargo-checksum.json" "dest-filename": ".cargo-checksum.json"
}, },
{ {
+104 -162
View File
@@ -163,6 +163,15 @@ dependencies = [
"alloc-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]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.2" version = "3.20.2"
@@ -259,9 +268,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.61" version = "1.2.62"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@@ -478,7 +487,7 @@ dependencies = [
"cssparser-macros", "cssparser-macros",
"dtoa-short", "dtoa-short",
"itoa", "itoa",
"phf 0.13.1", "phf",
"smallvec", "smallvec",
] ]
@@ -1331,9 +1340,9 @@ dependencies = [
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.4.13" version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes", "bytes",
@@ -1365,9 +1374,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.17.0" version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]] [[package]]
name = "heck" name = "heck"
@@ -1681,7 +1690,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.17.0", "hashbrown 0.17.1",
"serde", "serde",
"serde_core", "serde_core",
] ]
@@ -1701,16 +1710,6 @@ version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "iri-string"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
dependencies = [
"memchr",
"serde",
]
[[package]] [[package]]
name = "is-docker" name = "is-docker"
version = "0.2.0" version = "0.2.0"
@@ -1805,9 +1804,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.97" version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -2006,7 +2005,7 @@ dependencies = [
[[package]] [[package]]
name = "moku" name = "moku"
version = "0.9.2" version = "0.9.3"
dependencies = [ dependencies = [
"dirs 5.0.1", "dirs 5.0.1",
"reqwest 0.12.28", "reqwest 0.12.28",
@@ -2368,9 +2367,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]] [[package]]
name = "open" name = "open"
version = "5.3.4" version = "5.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c"
dependencies = [ dependencies = [
"dunce", "dunce",
"is-wsl", "is-wsl",
@@ -2380,15 +2379,14 @@ dependencies = [
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.78" version = "0.10.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"cfg-if", "cfg-if",
"foreign-types 0.3.2", "foreign-types 0.3.2",
"libc", "libc",
"once_cell",
"openssl-macros", "openssl-macros",
"openssl-sys", "openssl-sys",
] ]
@@ -2412,9 +2410,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.114" version = "0.9.115"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@@ -2514,24 +2512,14 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[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]] [[package]]
name = "phf" name = "phf"
version = "0.13.1" version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
dependencies = [ dependencies = [
"phf_macros 0.13.1", "phf_macros",
"phf_shared 0.13.1", "phf_shared",
"serde", "serde",
] ]
@@ -2541,18 +2529,8 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
dependencies = [ dependencies = [
"phf_generator 0.13.1", "phf_generator",
"phf_shared 0.13.1", "phf_shared",
]
[[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.6",
] ]
[[package]] [[package]]
@@ -2562,20 +2540,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"phf_shared 0.13.1", "phf_shared",
]
[[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",
] ]
[[package]] [[package]]
@@ -2584,22 +2549,13 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
dependencies = [ dependencies = [
"phf_generator 0.13.1", "phf_generator",
"phf_shared 0.13.1", "phf_shared",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
]
[[package]] [[package]]
name = "phf_shared" name = "phf_shared"
version = "0.13.1" version = "0.13.1"
@@ -2780,9 +2736,9 @@ dependencies = [
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.39.2" version = "0.39.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -2816,7 +2772,7 @@ dependencies = [
"bytes", "bytes",
"getrandom 0.3.4", "getrandom 0.3.4",
"lru-slab", "lru-slab",
"rand 0.9.4", "rand",
"ring", "ring",
"rustc-hash", "rustc-hash",
"rustls", "rustls",
@@ -2863,15 +2819,6 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"rand_core 0.6.4",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.9.4" version = "0.9.4"
@@ -2879,7 +2826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [ dependencies = [
"rand_chacha", "rand_chacha",
"rand_core 0.9.5", "rand_core",
] ]
[[package]] [[package]]
@@ -2889,15 +2836,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core 0.9.5", "rand_core",
] ]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
[[package]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.9.5" version = "0.9.5"
@@ -3317,7 +3258,7 @@ dependencies = [
"derive_more", "derive_more",
"log", "log",
"new_debug_unreachable", "new_debug_unreachable",
"phf 0.13.1", "phf",
"phf_codegen", "phf_codegen",
"precomputed-hash", "precomputed-hash",
"rustc-hash", "rustc-hash",
@@ -3444,11 +3385,12 @@ dependencies = [
[[package]] [[package]]
name = "serde_with" name = "serde_with"
version = "3.18.0" version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bs58",
"chrono", "chrono",
"hex", "hex",
"indexmap 1.9.3", "indexmap 1.9.3",
@@ -3463,9 +3405,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_with_macros" name = "serde_with_macros"
version = "3.18.0" version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac"
dependencies = [ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
@@ -3571,9 +3513,9 @@ checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "1.0.2" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
[[package]] [[package]]
name = "slab" name = "slab"
@@ -3659,7 +3601,7 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
dependencies = [ dependencies = [
"new_debug_unreachable", "new_debug_unreachable",
"parking_lot", "parking_lot",
"phf_shared 0.13.1", "phf_shared",
"precomputed-hash", "precomputed-hash",
] ]
@@ -3669,8 +3611,8 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"
dependencies = [ dependencies = [
"phf_generator 0.13.1", "phf_generator",
"phf_shared 0.13.1", "phf_shared",
"proc-macro2", "proc-macro2",
"quote", "quote",
] ]
@@ -3812,9 +3754,9 @@ dependencies = [
[[package]] [[package]]
name = "tao" name = "tao"
version = "0.35.0" version = "0.35.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cf65722394c2ac443e80120064987f8914ee1d4e4e36e63cdf10f2990f01159" checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2",
@@ -3869,9 +3811,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "2.11.0" version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d059f2527558d9dba6f186dec4772610e1aecfd3f94002397613e7e648752b66" checksum = "b93bd86d231f0a8138f11a02a584769fe4b703dc36ae133d783228dbc4801405"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -3920,9 +3862,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "2.6.0" version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be9aa8c59a894f76c29a002501c589de5eb4987a5913d62a6e0a47f320901988" checksum = "3a318b234cc2dea65f575467bafcfb76286bce228ebc3778e337d61d03213007"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@@ -3941,9 +3883,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "2.6.0" version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e4e8230d565106aa19dfbaa01a7ed01abf78047fe0577a83377224bd1bf20e" checksum = "6bd11644962add2549a60b7e7c6800f17d7020156e02f516021d8103e80cc528"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"brotli", "brotli",
@@ -3968,9 +3910,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "2.6.0" version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc8de2cddbbc33dbdf4c84f170121886595efdbcc9cb4b3d76342b79d082cedc" checksum = "fed9d3742a37a355d2e47c9af924e9fbc112abb76f9835d35d4780e318419502"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -3982,9 +3924,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin" name = "tauri-plugin"
version = "2.6.0" version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8d5f58bfd0cdcfdbc0a68dc08b354eea2afc551b421de91b07b69e0dd769d57" checksum = "eefb2c18e8a605c23edb48fc56bb77381199e1a1e7f6ff0c9b970afe7b3cb8ee"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"glob", "glob",
@@ -3998,9 +3940,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-dialog" name = "tauri-plugin-dialog"
version = "2.7.0" version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1fa4150c95ae391946cc8b8f905ab14797427caba3a8a2f79628e956da91809" checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884"
dependencies = [ dependencies = [
"log", "log",
"raw-window-handle", "raw-window-handle",
@@ -4033,9 +3975,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-fs" name = "tauri-plugin-fs"
version = "2.5.0" version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36e1ec28b79f3d0683f4507e1615c36292c0ea6716668770d4396b9b39871ed8" checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dunce", "dunce",
@@ -4051,15 +3993,15 @@ dependencies = [
"tauri-plugin", "tauri-plugin",
"tauri-utils", "tauri-utils",
"thiserror 2.0.18", "thiserror 2.0.18",
"toml 0.9.12+spec-1.1.0", "toml 1.1.2+spec-1.1.0",
"url", "url",
] ]
[[package]] [[package]]
name = "tauri-plugin-http" name = "tauri-plugin-http"
version = "2.5.8" version = "2.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfba7d4ec72763f9d1fdf73c217747f01e2c84b08b87a8cacd2f94f35853f84d" checksum = "b5bd512048e1985b7ec78f96d99083e2ddaf7e0d906b2b63c44ce5bb8b894067"
dependencies = [ dependencies = [
"bytes", "bytes",
"cookie_store", "cookie_store",
@@ -4130,9 +4072,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-store" name = "tauri-plugin-store"
version = "2.4.2" version = "2.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca1a8ff83c269b115e98726ffc13f9e548a10161544a92ad121d6d0a96e16ea" checksum = "6c72dda16786eb4a3f903e43a17b64d8d78dc0f00fe2aa4b757c28f617a8630b"
dependencies = [ dependencies = [
"dunce", "dunce",
"serde", "serde",
@@ -4146,9 +4088,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.11.0" version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e42bbcb76237351fbaa02f08d808c537dc12eb5a6eabbf3e517b50056334d95" checksum = "8fef478ba1d2ac21c2d528740b24d0cb315e1e8b1111aae53fafac34804371fc"
dependencies = [ dependencies = [
"cookie", "cookie",
"dpi", "dpi",
@@ -4171,9 +4113,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "2.11.0" version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cadb13dad0c681e1e0a2c49ae488f0e2906ded3d57e7a0017f4aaf46e387117" checksum = "a3989df2ae1c476404fe0a2e8ffc4cfbde97e51efd613c2bb5355fbc9ab52cf0"
dependencies = [ dependencies = [
"gtk", "gtk",
"http", "http",
@@ -4197,9 +4139,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "2.9.0" version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55f61d2bf7188fbcf2b0ed095b67a6bc498f713c939314bb19eb700118a573b7" checksum = "d57200389a2f82b4b0a40ae29ca19b6978116e8f4d4e974c3234ce40c0ffbdec"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"brotli", "brotli",
@@ -4213,7 +4155,7 @@ dependencies = [
"json-patch", "json-patch",
"log", "log",
"memchr", "memchr",
"phf 0.11.3", "phf",
"plist", "plist",
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -4365,9 +4307,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.52.1" version = "1.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
@@ -4461,7 +4403,7 @@ dependencies = [
"toml_datetime 1.1.1+spec-1.1.0", "toml_datetime 1.1.1+spec-1.1.0",
"toml_parser", "toml_parser",
"toml_writer", "toml_writer",
"winnow 1.0.2", "winnow 1.0.3",
] ]
[[package]] [[package]]
@@ -4524,7 +4466,7 @@ dependencies = [
"indexmap 2.14.0", "indexmap 2.14.0",
"toml_datetime 1.1.1+spec-1.1.0", "toml_datetime 1.1.1+spec-1.1.0",
"toml_parser", "toml_parser",
"winnow 1.0.2", "winnow 1.0.3",
] ]
[[package]] [[package]]
@@ -4533,7 +4475,7 @@ version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [ dependencies = [
"winnow 1.0.2", "winnow 1.0.3",
] ]
[[package]] [[package]]
@@ -4559,20 +4501,20 @@ dependencies = [
[[package]] [[package]]
name = "tower-http" name = "tower-http"
version = "0.6.8" version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http",
"http-body", "http-body",
"iri-string",
"pin-project-lite", "pin-project-lite",
"tower", "tower",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"url",
] ]
[[package]] [[package]]
@@ -4870,9 +4812,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.120" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -4883,9 +4825,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.70" version = "0.4.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -4893,9 +4835,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.120" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -4903,9 +4845,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.120" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -4916,9 +4858,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.120" version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -4972,9 +4914,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.97" version = "0.3.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -4996,7 +4938,7 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538"
dependencies = [ dependencies = [
"phf 0.13.1", "phf",
"phf_codegen", "phf_codegen",
"string_cache", "string_cache",
"string_cache_codegen", "string_cache_codegen",
@@ -5736,9 +5678,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "1.0.2" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -5855,9 +5797,9 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]] [[package]]
name = "wry" name = "wry"
version = "0.55.0" version = "0.55.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3013fd6116aac351dd2e18f349b28b2cfef3a5ff3253a9d0ce2d7193bb1b4429" checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"block2", "block2",
@@ -5963,9 +5905,9 @@ dependencies = [
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.7" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
dependencies = [ dependencies = [
"zerofrom-derive", "zerofrom-derive",
] ]
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "moku" name = "moku"
version = "0.9.2" version = "0.9.3"
edition = "2021" edition = "2021"
[lib] [lib]
@@ -15,7 +15,7 @@ path = "src/main.rs"
tauri-build = { version = "2.0", features = [] } tauri-build = { version = "2.0", features = [] }
[dependencies] [dependencies]
tauri = { version = "2.0", features = [] } tauri = { version = "2.0", features = ["tray-icon"] }
tauri-plugin-shell = "2" tauri-plugin-shell = "2"
tauri-plugin-process = "2" tauri-plugin-process = "2"
tauri-plugin-http = "2" tauri-plugin-http = "2"
+5 -1
View File
@@ -5,6 +5,10 @@
"windows": ["main"], "windows": ["main"],
"permissions": [ "permissions": [
"core:default", "core:default",
"core:tray:default",
"core:app:allow-default-window-icon",
"core:window:allow-hide",
"core:window:allow-show",
"shell:allow-open", "shell:allow-open",
"shell:allow-kill", "shell:allow-kill",
"shell:allow-spawn", "shell:allow-spawn",
@@ -38,4 +42,4 @@
"discord-rpc:allow-clear-activity", "discord-rpc:allow-clear-activity",
"discord-rpc:allow-is-running" "discord-rpc:allow-is-running"
] ]
} }
+2
View File
@@ -1,5 +1,7 @@
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use crate::server::resolve::strip_unc; use crate::server::resolve::strip_unc;
#[cfg(target_os = "windows")]
use std::path::PathBuf;
use tauri::Manager; use tauri::Manager;
#[tauri::command] #[tauri::command]
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Moku", "productName": "Moku",
"version": "0.9.2", "version": "0.9.3",
"identifier": "io.github.MokuProject.Moku", "identifier": "io.github.MokuProject.Moku",
"build": { "build": {
"frontendDist": "../dist", "frontendDist": "../dist",
+218 -2
View File
@@ -3,8 +3,11 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import { defaultWindowIcon } from "@tauri-apps/api/app";
import { TrayIcon } from "@tauri-apps/api/tray";
import { Menu } from "@tauri-apps/api/menu";
import { platform } from "@tauri-apps/plugin-os"; import { platform } from "@tauri-apps/plugin-os";
import { store, setActiveDownloads } from "@store/state.svelte"; import { store, updateSettings, setActiveDownloads } from "@store/state.svelte";
import { downloadStore } from "@features/downloads/store/downloadState.svelte"; import { downloadStore } from "@features/downloads/store/downloadState.svelte";
import { boot, initStore, startProbe, stopProbe, retryBoot, bypassBoot } from "@store/boot.svelte"; import { boot, initStore, startProbe, stopProbe, retryBoot, bypassBoot } from "@store/boot.svelte";
import { initRpc, setIdle, clearReading, destroyRpc } from "@store/discord"; import { initRpc, setIdle, clearReading, destroyRpc } from "@store/discord";
@@ -31,6 +34,9 @@
let themeEditorOpen = $state(false); let themeEditorOpen = $state(false);
let themeEditorEditId = $state<string | null>(null); let themeEditorEditId = $state<string | null>(null);
let closeDialogOpen = $state(false);
let closeRemember = $state(false);
function openThemeEditor(id?: string | null) { function openThemeEditor(id?: string | null) {
themeEditorEditId = id ?? null; themeEditorEditId = id ?? null;
themeEditorOpen = true; themeEditorOpen = true;
@@ -41,6 +47,30 @@
themeEditorEditId = null; themeEditorEditId = null;
} }
async function doQuit() {
if (store.settings.autoStartServer) await invoke("kill_server").catch(() => {});
await win.destroy();
}
async function doHide() {
await win.hide();
}
async function handleCloseRequested() {
const action = store.settings.closeAction ?? "ask";
if (action === "tray") { await doHide(); return; }
if (action === "quit") { await doQuit(); return; }
closeDialogOpen = true;
}
async function confirmClose(choice: "tray" | "quit") {
closeDialogOpen = false;
if (closeRemember) updateSettings({ closeAction: choice });
closeRemember = false;
if (choice === "tray") await doHide();
else await doQuit();
}
$effect(() => { void store.settings.theme; applyTheme(); }); $effect(() => { void store.settings.theme; applyTheme(); });
$effect(() => { void store.settings.uiZoom; applyZoom(); }); $effect(() => { void store.settings.uiZoom; applyZoom(); });
$effect(() => mountZoomKey()); $effect(() => mountZoomKey());
@@ -93,6 +123,39 @@
applyZoom(); applyZoom();
}); });
const menu = await Menu.new({
items: [
{
id: "show",
text: "Show Moku",
action: async () => {
await win.show();
await win.setFocus();
},
},
{
id: "quit",
text: "Quit",
action: doQuit,
},
],
});
await TrayIcon.new({
icon: await defaultWindowIcon(),
menu,
menuOnLeftClick: false,
tooltip: "Moku",
action: async (e) => {
if (e.type === "Click") {
await win.show();
await win.setFocus();
}
},
});
const unlistenClose = await win.listen("tauri://close-requested", handleCloseRequested);
if (store.settings.autoStartServer) { if (store.settings.autoStartServer) {
invoke<void>("spawn_server", { binary: store.settings.serverBinary }).catch((err: any) => { invoke<void>("spawn_server", { binary: store.settings.serverBinary }).catch((err: any) => {
if (err?.kind === "NotConfigured") boot.notConfigured = true; if (err?.kind === "NotConfigured") boot.notConfigured = true;
@@ -117,8 +180,8 @@
unlistenResize(); unlistenResize();
unlistenScale(); unlistenScale();
unlistenDownload(); unlistenDownload();
unlistenClose();
destroyRpc(); destroyRpc();
if (store.settings.autoStartServer) invoke("kill_server").catch(() => {});
delete (window as any).__mokuShowSplash; delete (window as any).__mokuShowSplash;
}; };
}); });
@@ -165,7 +228,160 @@
</div> </div>
{/if} {/if}
{#if closeDialogOpen}
<div class="close-backdrop" role="presentation" onclick={() => { closeDialogOpen = false; closeRemember = false; }}>
<div class="close-dialog" role="dialog" aria-modal="true" onclick={(e) => e.stopPropagation()}>
<div class="close-header">
<p class="close-title">Close Moku?</p>
<p class="close-sub">Choose how the app should exit.</p>
</div>
<div class="close-actions">
<button class="close-btn" onclick={() => confirmClose("tray")}>
<span class="close-btn-label">Minimize to Tray</span>
<span class="close-btn-desc">Keep running in the background</span>
</button>
<button class="close-btn close-btn-danger" onclick={() => confirmClose("quit")}>
<span class="close-btn-label">Quit</span>
<span class="close-btn-desc">Stop Moku entirely</span>
</button>
</div>
<button class="close-remember" onclick={() => closeRemember = !closeRemember}>
<span class="close-remember-toggle" class:on={closeRemember}><span class="close-remember-thumb"></span></span>
<span class="close-remember-label">Remember my choice</span>
</button>
</div>
</div>
{/if}
<style> <style>
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; } .root { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
.content { flex: 1; overflow: hidden; } .content { flex: 1; overflow: hidden; }
.close-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-modal);
}
.close-dialog {
background: var(--bg-surface);
border: 1px solid var(--border-base);
border-radius: var(--radius-2xl);
padding: var(--sp-5);
display: flex;
flex-direction: column;
gap: var(--sp-3);
width: 300px;
box-shadow:
0 0 0 1px rgba(255,255,255,0.04) inset,
0 20px 60px rgba(0,0,0,0.65),
0 6px 20px rgba(0,0,0,0.35);
}
.close-header { display: flex; flex-direction: column; gap: 3px; }
.close-title {
font-family: var(--font-ui);
font-size: var(--text-base);
font-weight: var(--weight-medium);
color: var(--text-primary);
letter-spacing: var(--tracking-tight);
margin: 0;
}
.close-sub {
font-family: var(--font-ui);
font-size: var(--text-xs);
color: var(--text-faint);
letter-spacing: var(--tracking-wide);
margin: 0;
}
.close-actions { display: flex; flex-direction: column; gap: var(--sp-1); }
.close-btn {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
width: 100%;
padding: 10px var(--sp-3);
border-radius: var(--radius-lg);
border: 1px solid var(--border-dim);
background: var(--bg-raised);
cursor: pointer;
text-align: left;
transition: background var(--t-base), border-color var(--t-base);
}
.close-btn:hover { background: var(--bg-overlay); border-color: var(--border-strong); }
.close-btn-danger { border-color: color-mix(in srgb, var(--color-error) 30%, transparent); }
.close-btn-danger:hover { background: var(--color-error-bg); border-color: color-mix(in srgb, var(--color-error) 55%, transparent); }
.close-btn-danger .close-btn-label { color: var(--color-error); }
.close-btn-danger .close-btn-desc { color: color-mix(in srgb, var(--color-error) 55%, var(--text-faint)); }
.close-btn-label {
font-family: var(--font-ui);
font-size: var(--text-sm);
color: var(--text-secondary);
font-weight: var(--weight-medium);
}
.close-btn-desc {
font-family: var(--font-ui);
font-size: var(--text-2xs);
color: var(--text-faint);
letter-spacing: var(--tracking-wide);
}
.close-remember {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: var(--sp-1) 0 0;
background: none;
border: none;
cursor: pointer;
user-select: none;
}
.close-remember-toggle {
position: relative;
width: 28px;
height: 16px;
border-radius: var(--radius-full);
border: 1px solid var(--border-strong);
background: var(--bg-overlay);
flex-shrink: 0;
transition: background var(--t-base), border-color var(--t-base);
}
.close-remember-toggle.on { background: var(--accent); border-color: var(--accent); }
.close-remember-thumb {
position: absolute;
top: 1px;
left: 1px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--text-faint);
transition: transform var(--t-base), background var(--t-base);
}
.close-remember-toggle.on .close-remember-thumb {
transform: translateX(12px);
background: var(--bg-void);
}
.close-remember-label {
font-family: var(--font-ui);
font-size: var(--text-xs);
color: var(--text-faint);
letter-spacing: var(--tracking-wide);
}
</style> </style>
+4 -1
View File
@@ -12,6 +12,7 @@ export interface Keybinds {
openSettings: string; openSettings: string;
toggleBookmark: string; toggleBookmark: string;
toggleMarker: string; toggleMarker: string;
toggleAutoScroll: string;
} }
export const DEFAULT_KEYBINDS: Keybinds = { export const DEFAULT_KEYBINDS: Keybinds = {
@@ -28,6 +29,7 @@ export const DEFAULT_KEYBINDS: Keybinds = {
openSettings: "o", openSettings: "o",
toggleBookmark: "m", toggleBookmark: "m",
toggleMarker: "n", toggleMarker: "n",
toggleAutoScroll: "s",
}; };
export const KEYBIND_LABELS: Record<keyof Keybinds, string> = { export const KEYBIND_LABELS: Record<keyof Keybinds, string> = {
@@ -44,4 +46,5 @@ export const KEYBIND_LABELS: Record<keyof Keybinds, string> = {
openSettings: "Open settings", openSettings: "Open settings",
toggleBookmark: "Toggle bookmark", toggleBookmark: "Toggle bookmark",
toggleMarker: "Toggle marker", toggleMarker: "Toggle marker",
}; toggleAutoScroll: "Toggle auto scroll",
};
@@ -1,11 +1,12 @@
<script lang="ts"> <script lang="ts">
import { ArrowLeft, MagnifyingGlass, GearSix } from "phosphor-svelte"; import { ArrowLeft, MagnifyingGlass, GearSix, Swap } from "phosphor-svelte";
import Thumbnail from "@shared/manga/Thumbnail.svelte"; import Thumbnail from "@shared/manga/Thumbnail.svelte";
import { resolvedCover } from "@core/cover/coverResolver"; import { resolvedCover } from "@core/cover/coverResolver";
import { gql } from "@api/client"; import { gql } from "@api/client";
import { setPreviewManga } from "@store/state.svelte"; import { setPreviewManga } from "@store/state.svelte";
import { GET_LIBRARY, GET_SOURCES } from "@api/queries"; import { GET_LIBRARY, GET_SOURCES } from "@api/queries";
import { libraryByExtension, type LibraryManga, type SourceNode, type SourceLibrary } from "../lib/extensionLibrary"; import { libraryByExtension, type LibraryManga, type SourceNode, type SourceLibrary } from "../lib/extensionLibrary";
import SourceMigrateModal from "../panels/SourceMigrateModal.svelte";
type SourceEntry = { id: string; displayName: string }; type SourceEntry = { id: string; displayName: string };
@@ -28,13 +29,17 @@
let loading = $state(true); let loading = $state(true);
let search = $state(""); let search = $state("");
const allManga = $derived(groups.flatMap(g => g.manga)); let migrateTarget: { sourceId: string; sourceName: string; iconUrl: string; manga: LibraryManga[] } | null = $state(null);
const filtered = $derived(
const allManga = $derived(groups.flatMap(g => g.manga));
const filtered = $derived(
search.trim() search.trim()
? allManga.filter(m => m.title.toLowerCase().includes(search.toLowerCase())) ? allManga.filter(m => m.title.toLowerCase().includes(search.toLowerCase()))
: allManga : allManga
); );
let sourceNodes: SourceNode[] = $state([]);
$effect(() => { load(); }); $effect(() => { load(); });
async function load() { async function load() {
@@ -44,16 +49,27 @@
gql<{ mangas: { nodes: LibraryManga[] } }>(GET_LIBRARY), gql<{ mangas: { nodes: LibraryManga[] } }>(GET_LIBRARY),
gql<{ sources: { nodes: SourceNode[] } }>(GET_SOURCES), gql<{ sources: { nodes: SourceNode[] } }>(GET_SOURCES),
]); ]);
sourceNodes = srcData.sources.nodes;
groups = libraryByExtension(libData.mangas.nodes, srcData.sources.nodes, pkgName); groups = libraryByExtension(libData.mangas.nodes, srcData.sources.nodes, pkgName);
} finally { } finally {
loading = false; loading = false;
} }
} }
function openMigrate(group: SourceLibrary) {
const node = sourceNodes.find(s => s.id === group.sourceId);
migrateTarget = {
sourceId: group.sourceId,
sourceName: group.displayName,
iconUrl: (node as any)?.iconUrl ?? iconUrl,
manga: group.manga,
};
}
</script> </script>
<div class="root"> <div class="root">
<div class="header"> <div class="header">
<button class="back-btn" onclick={onBack}> <button class="header-btn" onclick={onBack}>
<ArrowLeft size={14} weight="bold" /> <ArrowLeft size={14} weight="bold" />
</button> </button>
{#if iconUrl} {#if iconUrl}
@@ -71,7 +87,7 @@
<input class="search" placeholder="Search" bind:value={search} autocomplete="off" /> <input class="search" placeholder="Search" bind:value={search} autocomplete="off" />
</div> </div>
{#if sources.length > 0} {#if sources.length > 0}
<button class="settings-btn" onclick={onSettings} title="Extension settings"> <button class="header-btn" onclick={onSettings} title="Extension settings">
<GearSix size={14} weight="bold" /> <GearSix size={14} weight="bold" />
</button> </button>
{/if} {/if}
@@ -92,9 +108,32 @@
{allManga.length === 0 ? "Nothing from this extension is in your library." : "No matches."} {allManga.length === 0 ? "Nothing from this extension is in your library." : "No matches."}
</div> </div>
{:else} {:else}
{#if groups.length > 1}
<div class="source-groups">
{#each groups as group}
<div class="source-group-header">
<span class="source-group-name">{group.displayName}</span>
<span class="source-group-count">{group.manga.length}</span>
<button class="migrate-btn" onclick={() => openMigrate(group)} title="Migrate this source">
<Swap size={12} weight="bold" />
Migrate source
</button>
</div>
{/each}
</div>
{:else if groups.length === 1}
<div class="single-source-bar">
<span class="source-group-name">{groups[0].displayName}</span>
<button class="migrate-btn" onclick={() => openMigrate(groups[0])} title="Migrate this source">
<Swap size={12} weight="bold" />
Migrate source
</button>
</div>
{/if}
<div class="grid" style="--cols:{cols}"> <div class="grid" style="--cols:{cols}">
{#each filtered as m (m.id)} {#each filtered as m (m.id)}
{@const isCompleted = !m.unreadCount && (m.downloadCount > 0)} {@const isCompleted = !m.unreadCount && m.downloadCount > 0}
<button class="card" class:anims onclick={() => setPreviewManga(m as any)}> <button class="card" class:anims onclick={() => setPreviewManga(m as any)}>
<div class="cover-wrap" class:completed={isCompleted}> <div class="cover-wrap" class:completed={isCompleted}>
<Thumbnail <Thumbnail
@@ -125,6 +164,17 @@
</div> </div>
</div> </div>
{#if migrateTarget}
<SourceMigrateModal
sourceId={migrateTarget.sourceId}
sourceName={migrateTarget.sourceName}
sourceIconUrl={migrateTarget.iconUrl}
manga={migrateTarget.manga}
onClose={() => migrateTarget = null}
onDone={() => { migrateTarget = null; load(); }}
/>
{/if}
<style> <style>
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; } .root { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
@@ -132,8 +182,8 @@
.header { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-4) var(--sp-6); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; } .header { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-4) var(--sp-6); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
.back-btn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-md); color: var(--text-faint); flex-shrink: 0; transition: color var(--t-base), background var(--t-base); } .header-btn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-md); color: var(--text-faint); flex-shrink: 0; transition: color var(--t-base), background var(--t-base); }
.back-btn:hover { color: var(--text-primary); background: var(--bg-raised); } .header-btn:hover { color: var(--text-primary); background: var(--bg-raised); }
.title-block { display: flex; flex-direction: column; gap: 1px; } .title-block { display: flex; flex-direction: column; gap: 1px; }
.eyebrow { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; } .eyebrow { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; }
@@ -147,10 +197,15 @@
.search::placeholder { color: var(--text-faint); } .search::placeholder { color: var(--text-faint); }
.search:focus { border-color: var(--border-strong); } .search:focus { border-color: var(--border-strong); }
.settings-btn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-md); color: var(--text-faint); flex-shrink: 0; transition: color var(--t-base), background var(--t-base); } .content { flex: 1; overflow-y: auto; padding: var(--sp-4) var(--sp-6) var(--sp-6); will-change: scroll-position; display: flex; flex-direction: column; gap: var(--sp-3); }
.settings-btn:hover { color: var(--text-primary); background: var(--bg-raised); }
.content { flex: 1; overflow-y: auto; padding: var(--sp-5) var(--sp-6) var(--sp-6); will-change: scroll-position; } .source-groups { display: flex; flex-direction: column; gap: var(--sp-1); }
.source-group-header { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) 0; border-bottom: 1px solid var(--border-dim); }
.single-source-bar { display: flex; align-items: center; gap: var(--sp-2); padding-bottom: var(--sp-2); border-bottom: 1px solid var(--border-dim); }
.source-group-name { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); letter-spacing: var(--tracking-wide); font-weight: var(--weight-medium); }
.source-group-count { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); padding: 1px 6px; border-radius: var(--radius-sm); background: var(--bg-overlay); border: 1px solid var(--border-dim); }
.migrate-btn { display: flex; align-items: center; gap: 5px; margin-left: auto; font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); padding: 3px 9px; border-radius: var(--radius-sm); background: none; color: var(--text-faint); border: 1px solid var(--border-dim); cursor: pointer; flex-shrink: 0; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
.migrate-btn:hover { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
.grid { display: grid; grid-template-columns: repeat(var(--cols, auto-fill), minmax(130px, 1fr)); gap: var(--sp-4); } .grid { display: grid; grid-template-columns: repeat(var(--cols, auto-fill), minmax(130px, 1fr)); gap: var(--sp-4); }
@@ -11,7 +11,7 @@
import ExtensionFilters from "./ExtensionFilters.svelte"; import ExtensionFilters from "./ExtensionFilters.svelte";
import ExtensionCard from "./ExtensionCard.svelte"; import ExtensionCard from "./ExtensionCard.svelte";
import ExtensionSettingsPanel from "../panels/ExtensionSettingsPanel.svelte"; import ExtensionSettingsPanel from "../panels/ExtensionSettingsPanel.svelte";
import ExtensionLibraryPanel from "../panels/ExtensionLibraryPanel.svelte"; import ExtensionLibrary from "./ExtensionLibrary.svelte";
const anims = $derived(store.settings.qolAnimations ?? true); const anims = $derived(store.settings.qolAnimations ?? true);
const cols = $derived(store.settings.libraryCols ?? 5); const cols = $derived(store.settings.libraryCols ?? 5);
@@ -247,7 +247,7 @@
</script> </script>
{#if libraryTarget} {#if libraryTarget}
<ExtensionLibraryPanel <ExtensionLibrary
pkgName={libraryTarget.pkgName} pkgName={libraryTarget.pkgName}
extensionName={libraryTarget.extensionName} extensionName={libraryTarget.extensionName}
iconUrl={libraryTarget.iconUrl} iconUrl={libraryTarget.iconUrl}
@@ -0,0 +1,448 @@
<script lang="ts">
import { X, MagnifyingGlass, CircleNotch, ArrowRight, Check, Warning, Sparkle, Swap } from "phosphor-svelte";
import { gql } from "@api/client";
import Thumbnail from "@shared/manga/Thumbnail.svelte";
import { resolvedCover } from "@core/cover/coverResolver";
import { GET_SOURCES } from "@api/queries/extensions";
import { UPDATE_MANGA } from "@api/mutations/manga";
import { FETCH_SOURCE_MANGA } from "@api/mutations/downloads";
import { FETCH_CHAPTERS, UPDATE_CHAPTERS_PROGRESS } from "@api/mutations/chapters";
import { store, addToast } from "@store/state.svelte";
import type { Manga, Chapter, Source } from "@types";
import type { LibraryManga } from "../lib/extensionLibrary";
interface Props {
sourceId: string;
sourceName: string;
sourceIconUrl: string;
manga: LibraryManga[];
onClose: () => void;
onDone: () => void;
}
let { sourceId, sourceName, sourceIconUrl, manga, onClose, onDone }: Props = $props();
type Phase = "pick-target" | "review" | "migrating" | "done";
interface EntryResult {
manga: LibraryManga;
match: Manga | null;
chapters: Chapter[];
similarity: number;
status: "pending" | "searching" | "found" | "no-match" | "migrated" | "failed";
error?: string;
}
function titleSimilarity(a: string, b: string): number {
const norm = (s: string) =>
s.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter(Boolean);
const wordsA = new Set(norm(a));
const wordsB = new Set(norm(b));
if (wordsA.size === 0 || wordsB.size === 0) return 0;
const intersection = [...wordsA].filter(w => wordsB.has(w)).length;
return intersection / new Set([...wordsA, ...wordsB]).size;
}
function onKey(e: KeyboardEvent) { if (e.key === "Escape" && phase !== "migrating") onClose(); }
let phase: Phase = $state("pick-target");
let allSources: Source[] = $state([]);
let loadingSources = $state(true);
let targetSource: Source | null = $state(null);
let selectedLang = $state("all");
let langStripEl: HTMLDivElement | undefined = $state();
let entries: EntryResult[] = $state([]);
let searchProgress = $state({ done: 0, total: 0 });
let migrateProgress = $state({ done: 0, total: 0, failed: 0 });
const availableLangs = $derived.by(() => {
const langs = Array.from(new Set<string>(allSources.map(s => s.lang))).sort();
const en = langs.indexOf("en");
if (en > 0) { langs.splice(en, 1); langs.unshift("en"); }
return langs;
});
const hasMultipleLangs = $derived(availableLangs.length > 1);
const visibleSources = $derived.by(() => {
if (selectedLang !== "all") return allSources.filter(s => s.lang === selectedLang);
const map = new Map<string, Source>();
for (const s of allSources) {
const existing = map.get(s.name);
if (!existing || s.lang < existing.lang) map.set(s.name, s);
}
return Array.from(map.values());
});
const foundCount = $derived(entries.filter(e => e.status === "found").length);
const noMatchCount = $derived(entries.filter(e => e.status === "no-match").length);
const migratedCount = $derived(entries.filter(e => e.status === "migrated").length);
const failedCount = $derived(entries.filter(e => e.status === "failed").length);
$effect(() => {
gql<{ sources: { nodes: Source[] } }>(GET_SOURCES)
.then(d => {
allSources = d.sources.nodes.filter(s => s.id !== "0" && s.id !== sourceId);
const prefLang = store?.settings?.preferredExtensionLang ?? "";
const langs = new Set(allSources.map(s => s.lang));
if (prefLang && langs.has(prefLang) && langs.size > 1) selectedLang = prefLang;
})
.catch(console.error)
.finally(() => { loadingSources = false; });
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
});
function scrollLangStrip(dir: -1 | 1) {
if (!langStripEl) return;
const chips = Array.from(langStripEl.children) as HTMLElement[];
const viewEnd = langStripEl.scrollLeft + langStripEl.clientWidth;
if (dir === 1) {
const next = chips.find(c => c.offsetLeft + c.offsetWidth > viewEnd + 2);
if (next) langStripEl.scrollTo({ left: next.offsetLeft, behavior: "smooth" });
} else {
const prev = [...chips].reverse().find(c => c.offsetLeft < langStripEl!.scrollLeft - 2);
if (prev) langStripEl.scrollTo({ left: prev.offsetLeft + prev.offsetWidth - langStripEl.clientWidth, behavior: "smooth" });
}
}
async function startSearch(target: Source) {
targetSource = target;
phase = "review";
entries = manga.map(m => ({ manga: m, match: null, chapters: [], similarity: 0, status: "pending" }));
searchProgress = { done: 0, total: manga.length };
for (let i = 0; i < entries.length; i++) {
entries[i] = { ...entries[i], status: "searching" };
try {
const d = await gql<{ fetchSourceManga: { mangas: Manga[] } }>(FETCH_SOURCE_MANGA, {
source: target.id, type: "SEARCH", page: 1, query: entries[i].manga.title,
});
const results = d.fetchSourceManga.mangas
.map(m => ({ manga: m, similarity: titleSimilarity(entries[i].manga.title, m.title) }))
.sort((a, b) => b.similarity - a.similarity);
if (results.length > 0 && results[0].similarity > 0.3) {
entries[i] = { ...entries[i], match: results[0].manga, similarity: results[0].similarity, status: "found" };
} else {
entries[i] = { ...entries[i], status: "no-match" };
}
} catch (e: any) {
entries[i] = { ...entries[i], status: "no-match", error: e.message };
}
searchProgress = { done: i + 1, total: manga.length };
}
}
function setEntryMatch(idx: number, match: Manga, similarity: number) {
entries[idx] = { ...entries[idx], match, similarity, status: "found" };
}
function excludeEntry(idx: number) {
entries[idx] = { ...entries[idx], status: "no-match", match: null };
}
async function startMigration() {
const toMigrate = entries.filter(e => e.status === "found" && e.match);
migrateProgress = { done: 0, total: toMigrate.length, failed: 0 };
phase = "migrating";
for (const entry of toMigrate) {
const idx = entries.indexOf(entry);
try {
const d = await gql<{ fetchChapters: { chapters: Chapter[] } }>(FETCH_CHAPTERS, { mangaId: entry.match!.id });
const newChaps = d.fetchChapters.chapters;
const toMarkRead: number[] = [];
const toMarkBookmarked: number[] = [];
for (const nc of newChaps) {
const oldIdx = entries[idx].manga;
if (oldIdx) {
toMarkRead.push(nc.id);
}
}
if (toMarkRead.length)
await gql(UPDATE_CHAPTERS_PROGRESS, { ids: toMarkRead, isRead: true });
await gql(UPDATE_MANGA, { id: entry.match!.id, inLibrary: true });
await gql(UPDATE_MANGA, { id: entry.manga.id, inLibrary: false });
entries[idx] = { ...entries[idx], status: "migrated" };
migrateProgress = { ...migrateProgress, done: migrateProgress.done + 1 };
} catch (e: any) {
entries[idx] = { ...entries[idx], status: "failed", error: e.message };
migrateProgress = { ...migrateProgress, done: migrateProgress.done + 1, failed: migrateProgress.failed + 1 };
}
}
phase = "done";
addToast({
kind: "success",
title: "Migration complete",
body: `${migrateProgress.done - migrateProgress.failed} migrated, ${migrateProgress.failed} failed`,
});
}
</script>
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div class="overlay" onclick={(e) => { if (e.target === e.currentTarget && phase !== "migrating") onClose(); }}>
<div class="modal">
<div class="modal-header">
<div class="source-context">
<div class="source-icon-wrap">
<Thumbnail src={sourceIconUrl} alt={sourceName} class="src-icon" onerror={(e) => (e.target as HTMLImageElement).style.display = "none"} />
</div>
<div class="source-context-info">
<span class="modal-eyebrow">Source migration</span>
<span class="modal-title">{sourceName}</span>
<span class="modal-sub">{manga.length} {manga.length === 1 ? "title" : "titles"} in library</span>
</div>
</div>
{#if phase !== "migrating"}
<button class="close-btn" onclick={onClose}><X size={14} weight="light" /></button>
{/if}
</div>
<div class="body">
{#if phase === "pick-target"}
<div class="phase-label-row">
<span class="phase-label">Select destination source</span>
</div>
{#if loadingSources}
<div class="centered"><CircleNotch size={16} weight="light" class="anim-spin" style="color:var(--text-faint)" /></div>
{:else if allSources.length === 0}
<div class="centered"><span class="hint">No other sources installed.</span></div>
{:else}
{#if hasMultipleLangs}
<div class="src-lang-bar">
<button class="src-lang-nav" onclick={() => scrollLangStrip(-1)}></button>
<div class="src-lang-chips" bind:this={langStripEl}>
<button class="src-lang-chip" class:src-lang-chip-active={selectedLang === "all"} onclick={() => selectedLang = "all"}>All</button>
{#each availableLangs as lang}
<button class="src-lang-chip" class:src-lang-chip-active={selectedLang === lang} onclick={() => selectedLang = lang}>
{lang.toUpperCase()}
</button>
{/each}
</div>
<button class="src-lang-nav" onclick={() => scrollLangStrip(1)}></button>
</div>
{/if}
<div class="source-list">
{#each visibleSources as src}
<button class="source-row" onclick={() => startSearch(src)}>
<div class="source-icon-wrap">
<Thumbnail src={src.iconUrl} alt={src.name} class="source-icon" onerror={(e) => (e.target as HTMLImageElement).style.display = "none"} />
</div>
<div class="source-info">
<span class="source-name">{src.displayName}</span>
<span class="source-meta">{src.lang.toUpperCase()}{src.isNsfw ? " · NSFW" : ""}</span>
</div>
<ArrowRight size={13} weight="light" class="source-arrow" />
</button>
{/each}
</div>
{/if}
{:else if phase === "review" || phase === "migrating" || phase === "done"}
<div class="review-header">
<div class="review-route">
<div class="review-source">
<div class="source-icon-wrap small">
<Thumbnail src={sourceIconUrl} alt={sourceName} class="source-icon" onerror={(e) => (e.target as HTMLImageElement).style.display = "none"} />
</div>
<span class="review-source-name">{sourceName}</span>
</div>
<ArrowRight size={14} weight="light" style="color:var(--text-faint);flex-shrink:0" />
{#if targetSource}
<div class="review-source">
<div class="source-icon-wrap small">
<Thumbnail src={targetSource.iconUrl} alt={targetSource.name} class="source-icon" onerror={(e) => (e.target as HTMLImageElement).style.display = "none"} />
</div>
<span class="review-source-name">{targetSource.displayName}</span>
</div>
{/if}
</div>
{#if phase === "review"}
<div class="review-progress-row">
<div class="review-progress-bar">
<div class="review-progress-fill" style="width:{searchProgress.total ? (searchProgress.done / searchProgress.total) * 100 : 0}%"></div>
</div>
<span class="review-progress-label">
{#if searchProgress.done < searchProgress.total}
Searching {searchProgress.done + 1} / {searchProgress.total}
{:else}
{foundCount} found · {noMatchCount} no match
{/if}
</span>
</div>
{:else if phase === "migrating"}
<div class="review-progress-row">
<div class="review-progress-bar">
<div class="review-progress-fill" style="width:{migrateProgress.total ? (migrateProgress.done / migrateProgress.total) * 100 : 0}%"></div>
</div>
<span class="review-progress-label">Migrating {migrateProgress.done} / {migrateProgress.total}</span>
</div>
{:else}
<div class="done-summary">
<Check size={13} weight="bold" style="color:var(--color-success)" />
<span class="done-label">{migratedCount} migrated{failedCount > 0 ? ` · ${failedCount} failed` : ""}</span>
</div>
{/if}
</div>
<div class="entry-list">
{#each entries as entry, idx}
<div class="entry-row" class:entry-migrated={entry.status === "migrated"} class:entry-failed={entry.status === "failed"}>
<div class="entry-cover-wrap">
<Thumbnail src={resolvedCover(entry.manga.id, entry.manga.thumbnailUrl)} alt={entry.manga.title} class="entry-cover" />
</div>
<div class="entry-info">
<span class="entry-title">{entry.manga.title}</span>
{#if entry.status === "found" && entry.match}
<span class="entry-match">
<Sparkle size={9} weight="fill" style="color:var(--accent-fg);flex-shrink:0" />
{entry.match.title}
<span class="entry-sim">{Math.round(entry.similarity * 100)}%</span>
</span>
{:else if entry.status === "no-match"}
<span class="entry-no-match">No match found</span>
{:else if entry.status === "searching"}
<span class="entry-searching">Searching…</span>
{:else if entry.status === "migrated"}
<span class="entry-done">Migrated</span>
{:else if entry.status === "failed"}
<span class="entry-fail">{entry.error ?? "Failed"}</span>
{/if}
</div>
<div class="entry-status">
{#if entry.status === "searching"}
<CircleNotch size={13} weight="light" class="anim-spin" style="color:var(--text-faint)" />
{:else if entry.status === "found"}
<div class="entry-cover-match">
<Thumbnail src={resolvedCover(entry.match!.id, entry.match!.thumbnailUrl)} alt={entry.match!.title} class="entry-match-cover" />
</div>
{#if phase === "review"}
<button class="entry-exclude-btn" onclick={() => excludeEntry(idx)} title="Exclude from migration">
<X size={10} weight="bold" />
</button>
{/if}
{:else if entry.status === "migrated"}
<Check size={13} weight="bold" style="color:var(--color-success)" />
{:else if entry.status === "failed"}
<Warning size={13} weight="light" style="color:var(--color-error)" />
{/if}
</div>
</div>
{/each}
</div>
{#if phase === "review" && searchProgress.done === searchProgress.total}
<div class="review-actions">
<button class="back-btn" onclick={() => { phase = "pick-target"; entries = []; }}>Change source</button>
<button class="migrate-btn" onclick={startMigration} disabled={foundCount === 0}>
<Swap size={13} weight="bold" />
Migrate {foundCount} {foundCount === 1 ? "title" : "titles"}
</button>
</div>
{/if}
{#if phase === "done"}
<div class="review-actions">
<button class="migrate-btn" onclick={onDone}><Check size={13} weight="bold" /> Done</button>
</div>
{/if}
{/if}
</div>
</div>
</div>
<style>
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 200; animation: fadeIn 0.1s ease both; }
.modal { background: var(--bg-base); border: 1px solid var(--border-base); border-radius: var(--radius-xl); width: 560px; max-height: 84vh; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 8px 40px rgba(0,0,0,0.5); }
.modal-header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
.source-context { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
.source-icon-wrap { width: 36px; height: 36px; border-radius: var(--radius-md); overflow: hidden; flex-shrink: 0; background: var(--bg-raised); border: 1px solid var(--border-dim); }
.source-icon-wrap.small { width: 20px; height: 20px; border-radius: var(--radius-sm); }
:global(.src-icon) { width: 100%; height: 100%; object-fit: cover; }
:global(.source-icon) { width: 100%; height: 100%; object-fit: cover; }
.source-context-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.modal-eyebrow { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; }
.modal-title { font-size: var(--text-base); font-weight: var(--weight-medium); color: var(--text-primary); letter-spacing: var(--tracking-tight); }
.modal-sub { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
.close-btn { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: var(--radius-md); color: var(--text-faint); background: none; border: none; cursor: pointer; transition: color var(--t-base), background var(--t-base); flex-shrink: 0; margin-top: 2px; }
.close-btn:hover { color: var(--text-muted); background: var(--bg-raised); }
.body { flex: 1; overflow: hidden; display: flex; flex-direction: column; min-height: 0; }
.phase-label-row { padding: var(--sp-3) var(--sp-4) var(--sp-2); flex-shrink: 0; }
.phase-label { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-widest); text-transform: uppercase; }
.centered { flex: 1; display: flex; align-items: center; justify-content: center; padding: var(--sp-8); }
.hint { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
.src-lang-bar { display: flex; align-items: center; gap: var(--sp-1); padding: var(--sp-2); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
.src-lang-nav { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; flex-shrink: 0; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); font-size: 15px; line-height: 1; cursor: pointer; transition: color var(--t-base), background var(--t-base); }
.src-lang-nav:hover { color: var(--text-muted); background: var(--bg-raised); }
.src-lang-chips { display: flex; align-items: center; gap: var(--sp-1); flex: 1; min-width: 0; overflow-x: auto; scrollbar-width: none; }
.src-lang-chips::-webkit-scrollbar { display: none; }
.src-lang-chip { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); padding: 3px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); cursor: pointer; white-space: nowrap; flex-shrink: 0; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
.src-lang-chip:hover { color: var(--text-muted); background: var(--bg-raised); }
.src-lang-chip-active { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
.source-list { flex: 1; overflow-y: auto; padding: var(--sp-2); display: flex; flex-direction: column; gap: 1px; }
.source-row { display: flex; align-items: center; gap: var(--sp-3); padding: 8px var(--sp-3); border-radius: var(--radius-md); border: 1px solid transparent; background: none; text-align: left; width: 100%; cursor: pointer; transition: background var(--t-fast), border-color var(--t-fast); }
.source-row:hover { background: var(--bg-raised); border-color: var(--border-dim); }
.source-info { flex: 1; display: flex; flex-direction: column; gap: 2px; overflow: hidden; }
.source-name { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.source-meta { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
:global(.source-arrow) { color: var(--text-faint); opacity: 0; transition: opacity var(--t-base); flex-shrink: 0; }
.source-row:hover :global(.source-arrow) { opacity: 1; }
.review-header { padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.review-route { display: flex; align-items: center; gap: var(--sp-2); }
.review-source { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.review-source-name { font-size: var(--text-xs); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: var(--weight-medium); }
.review-progress-row { display: flex; align-items: center; gap: var(--sp-3); }
.review-progress-bar { flex: 1; height: 3px; background: var(--bg-overlay); border-radius: var(--radius-full); overflow: hidden; }
.review-progress-fill { height: 100%; background: var(--accent); border-radius: var(--radius-full); transition: width 0.3s ease; }
.review-progress-label { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); white-space: nowrap; flex-shrink: 0; }
.done-summary { display: flex; align-items: center; gap: var(--sp-2); }
.done-label { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-secondary); letter-spacing: var(--tracking-wide); }
.entry-list { flex: 1; overflow-y: auto; padding: var(--sp-2); display: flex; flex-direction: column; gap: 1px; }
.entry-row { display: flex; align-items: center; gap: var(--sp-3); padding: 7px var(--sp-3); border-radius: var(--radius-md); border: 1px solid transparent; transition: background var(--t-fast); }
.entry-row:hover { background: var(--bg-raised); }
.entry-migrated { opacity: 0.5; }
.entry-failed { border-color: rgba(180,60,60,0.15); background: rgba(180,60,60,0.04); }
.entry-cover-wrap { width: 28px; height: 42px; border-radius: var(--radius-sm); overflow: hidden; background: var(--bg-raised); border: 1px solid var(--border-dim); flex-shrink: 0; }
:global(.entry-cover) { width: 100%; height: 100%; object-fit: cover; }
.entry-info { flex: 1; display: flex; flex-direction: column; gap: 3px; min-width: 0; overflow: hidden; }
.entry-title { font-size: var(--text-sm); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.entry-match { display: flex; align-items: center; gap: 4px; font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.entry-sim { background: var(--bg-overlay); border: 1px solid var(--border-dim); border-radius: var(--radius-sm); padding: 0 4px; font-size: 9px; flex-shrink: 0; }
.entry-no-match { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
.entry-searching { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
.entry-done { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--color-success); letter-spacing: var(--tracking-wide); }
.entry-fail { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--color-error); letter-spacing: var(--tracking-wide); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.entry-status { display: flex; align-items: center; gap: var(--sp-1); flex-shrink: 0; }
.entry-cover-match { width: 24px; height: 36px; border-radius: var(--radius-sm); overflow: hidden; background: var(--bg-raised); border: 1px solid var(--border-dim); flex-shrink: 0; }
:global(.entry-match-cover) { width: 100%; height: 100%; object-fit: cover; }
.entry-exclude-btn { display: flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: var(--radius-sm); color: var(--text-faint); background: none; border: none; cursor: pointer; transition: color var(--t-base), background var(--t-base); flex-shrink: 0; }
.entry-exclude-btn:hover { color: var(--color-error); background: var(--bg-raised); }
.review-actions { display: flex; justify-content: flex-end; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); border-top: 1px solid var(--border-dim); flex-shrink: 0; }
.back-btn { font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 6px 10px; border-radius: var(--radius-md); background: none; color: var(--text-muted); border: 1px solid var(--border-dim); cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
.back-btn:hover { color: var(--text-secondary); border-color: var(--border-strong); background: var(--bg-raised); }
.migrate-btn { display: flex; align-items: center; gap: var(--sp-2); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 7px 16px; border-radius: var(--radius-md); background: var(--accent-dim); border: 1px solid var(--accent); color: var(--accent-fg); cursor: pointer; transition: background var(--t-base), border-color var(--t-base); }
.migrate-btn:hover:not(:disabled) { background: var(--accent-muted); border-color: var(--accent-bright); }
.migrate-btn:disabled { opacity: 0.4; cursor: default; }
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
</style>
+133 -13
View File
@@ -211,6 +211,50 @@
let stripDragStartY = 0; let stripDragStartY = 0;
let stripScrollStart = 0; let stripScrollStart = 0;
let autoScrollPaused = false;
let autoScrollPauseTimer: ReturnType<typeof setTimeout> | null = null;
let midScrollActive = $state(false);
let midScrollOriginY = 0;
let midScrollRaf: number | null = null;
function startMidScroll(originY: number) {
midScrollActive = true;
midScrollOriginY = originY;
if (midScrollRaf) cancelAnimationFrame(midScrollRaf);
const tick = () => {
if (!midScrollActive || !containerEl) return;
const dy = (window as any)._midScrollCurrentY - midScrollOriginY;
const deadZone = 24;
const speed = Math.sign(dy) * Math.max(0, Math.abs(dy) - deadZone) * 0.12;
containerEl.scrollTop += speed;
midScrollRaf = requestAnimationFrame(tick);
};
midScrollRaf = requestAnimationFrame(tick);
}
function stopMidScroll() {
midScrollActive = false;
if (midScrollRaf) { cancelAnimationFrame(midScrollRaf); midScrollRaf = null; }
}
function pauseAutoScroll() {
autoScrollPaused = true;
if (autoScrollPauseTimer) clearTimeout(autoScrollPauseTimer);
autoScrollPauseTimer = setTimeout(() => { autoScrollPaused = false; }, 2500);
}
$effect(() => {
if (style !== "longstrip" || !store.settings.autoScroll) return;
let rafId: number;
const tick = () => {
if (!autoScrollPaused && containerEl) containerEl.scrollTop += (store.settings.autoScrollSpeed ?? 5) * 0.5;
rafId = requestAnimationFrame(tick);
};
rafId = requestAnimationFrame(tick);
return () => cancelAnimationFrame(rafId);
});
let pinch: PinchTracker | null = null; let pinch: PinchTracker | null = null;
$effect(() => { $effect(() => {
@@ -230,11 +274,17 @@
export function onInspectMouseDown(e: MouseEvent) { export function onInspectMouseDown(e: MouseEvent) {
if ((e.target as Element).closest(".bar")) return; if ((e.target as Element).closest(".bar")) return;
if (e.button === 1 && style === "longstrip") {
e.preventDefault();
if (midScrollActive) { stopMidScroll(); } else { startMidScroll(e.clientY); }
return;
}
if (style === "longstrip") { if (style === "longstrip") {
stripDragging = true; stripDragging = true;
stripDragMoved = false; stripDragMoved = false;
stripDragStartY = e.clientY; stripDragStartY = e.clientY;
stripScrollStart = containerEl?.scrollTop ?? 0; stripScrollStart = containerEl?.scrollTop ?? 0;
pauseAutoScroll();
e.preventDefault(); e.preventDefault();
return; return;
} }
@@ -249,6 +299,7 @@
} }
export function onInspectMouseMove(e: MouseEvent) { export function onInspectMouseMove(e: MouseEvent) {
(window as any)._midScrollCurrentY = e.clientY;
if (stripDragging) { if (stripDragging) {
const dy = e.clientY - stripDragStartY; const dy = e.clientY - stripDragStartY;
if (!stripDragMoved && Math.abs(dy) > 4) stripDragMoved = true; if (!stripDragMoved && Math.abs(dy) > 4) stripDragMoved = true;
@@ -305,6 +356,7 @@
export function handleWheel(e: WheelEvent) { export function handleWheel(e: WheelEvent) {
if (style === "longstrip") { if (style === "longstrip") {
if (e.ctrlKey) { onWheel(e); } if (e.ctrlKey) { onWheel(e); }
else pauseAutoScroll();
return; return;
} }
if (!e.ctrlKey) { onWheel(e); return; } if (!e.ctrlKey) { onWheel(e); return; }
@@ -328,7 +380,10 @@
} }
function handleTap(e: MouseEvent) { function handleTap(e: MouseEvent) {
if (style === "longstrip") return; if (style === "longstrip") {
if (stripDragMoved) { stripDragMoved = false; return; }
return;
}
if (inspectDragMoved) { inspectDragMoved = false; return; } if (inspectDragMoved) { inspectDragMoved = false; return; }
if (stripDragMoved) { stripDragMoved = false; return; } if (stripDragMoved) { stripDragMoved = false; return; }
onTap(e); onTap(e);
@@ -346,8 +401,13 @@
} else if (style !== "longstrip") { } else if (style !== "longstrip") {
observer?.disconnect(); observer?.disconnect();
observer = null; observer = null;
stopMidScroll();
} }
}); });
$effect(() => {
(window as any)._midScrollCurrentY = 0;
});
</script> </script>
<div <div
@@ -355,17 +415,22 @@
class="viewer" class="viewer"
class:strip={style === "longstrip"} class:strip={style === "longstrip"}
class:inspect-active={readerState.inspectScale > 1} class:inspect-active={readerState.inspectScale > 1}
class:midscroll-active={midScrollActive}
style={effectiveWidth != null ? `--effective-width:${effectiveWidth}px` : ""} style={effectiveWidth != null ? `--effective-width:${effectiveWidth}px` : ""}
role="presentation" role="presentation"
tabindex="-1" tabindex="-1"
onclick={handleTap} onclick={handleTap}
ondblclick={(e) => { if (tapToToggleBar) { const x = e.clientX / window.innerWidth; if (x >= 0.3 && x <= 0.7) onToggleUi(); } }} onauxclick={(e) => { if (e.button === 1 && style === "longstrip") e.preventDefault(); }}
ondblclick={() => { if (tapToToggleBar) onToggleUi(); }}
onmousedown={onInspectMouseDown} onmousedown={onInspectMouseDown}
onpointerdown={pinchZoomEnabled ? onPointerDown : undefined} onpointerdown={pinchZoomEnabled ? onPointerDown : undefined}
onwheel={(e) => { if (e.ctrlKey || style !== "longstrip") e.preventDefault(); }} onwheel={(e) => { if (e.ctrlKey || style !== "longstrip") e.preventDefault(); }}
style:cursor={style === "longstrip" ? (stripDragging ? "grabbing" : "grab") : undefined} style:cursor={midScrollActive ? "none" : style === "longstrip" ? (stripDragging ? "grabbing" : "grab") : undefined}
onkeydown={(e) => { if (e.key === " " && style === "longstrip") { e.preventDefault(); containerEl?.scrollBy({ top: containerEl.clientHeight * 0.85, behavior: "smooth" }); } }} onkeydown={(e) => { if (e.key === " " && style === "longstrip") { e.preventDefault(); store.settings.autoScroll = !store.settings.autoScroll; } }}
> >
{#if midScrollActive}
<div class="midscroll-cursor" style="top:{midScrollOriginY}px"></div>
{/if}
{#if loading} {#if loading}
<div class="center-overlay"><CircleNotch size={20} weight="light" class="anim-spin" style="color:var(--text-faint)" /></div> <div class="center-overlay"><CircleNotch size={20} weight="light" class="anim-spin" style="color:var(--text-faint)" /></div>
@@ -376,7 +441,7 @@
{#key chapterEpoch} {#key chapterEpoch}
{#if style === "longstrip"} {#if style === "longstrip"}
{#each flatPages as page, gi} {#each flatPages as page, gi (page.chapterId + ":" + page.localIndex)}
{@const src = resolvedSrc[gi]} {@const src = resolvedSrc[gi]}
{@const isLoaded = loadedSet.has(gi)} {@const isLoaded = loadedSet.has(gi)}
<div <div
@@ -384,9 +449,9 @@
use:observePage={gi} use:observePage={gi}
data-gi={gi} data-gi={gi}
> >
{#if isLoaded} {#if isLoaded && src}
<img <img
src={src ?? ""} src={src}
alt="{page.chapterName} Page {page.localIndex + 1}" alt="{page.chapterName} Page {page.localIndex + 1}"
data-local-page={page.localIndex + 1} data-local-page={page.localIndex + 1}
data-chapter={page.chapterId} data-chapter={page.chapterId}
@@ -404,7 +469,9 @@
}} }}
/> />
{:else} {:else}
<div class="strip-placeholder"></div> <div class="strip-placeholder page-loader" aria-hidden="true">
<CircleNotch size={20} weight="light" class="anim-spin" style="color:var(--text-faint)" />
</div>
{/if} {/if}
</div> </div>
{/each} {/each}
@@ -413,7 +480,9 @@
{:else if style === "fade" && pageReady} {:else if style === "fade" && pageReady}
<div class="inspect-wrap" style="transform:scale({readerState.inspectScale}) translate({readerState.inspectPanX / readerState.inspectScale}px,{readerState.inspectPanY / readerState.inspectScale}px)"> <div class="inspect-wrap" style="transform:scale({readerState.inspectScale}) translate({readerState.inspectPanX / readerState.inspectScale}px,{readerState.inspectPanY / readerState.inspectScale}px)">
{#await resolveUrl(store.pageUrls[store.pageNumber - 1], 999)} {#await resolveUrl(store.pageUrls[store.pageNumber - 1], 999)}
<img src="" alt="Page {store.pageNumber}" class={imgCls} decoding="async" style="opacity:0" draggable="false" /> <div class="page-loader page-loader-single" aria-hidden="true">
<CircleNotch size={20} weight="light" class="anim-spin" style="color:var(--text-faint)" />
</div>
{:then src} {:then src}
<img {src} alt="Page {store.pageNumber}" class={imgCls} decoding="async" style="opacity: {fadingOut ? 0 : 1}; transition: opacity 0.1s ease;" draggable="false" /> <img {src} alt="Page {store.pageNumber}" class={imgCls} decoding="async" style="opacity: {fadingOut ? 0 : 1}; transition: opacity 0.1s ease;" draggable="false" />
{/await} {/await}
@@ -423,9 +492,11 @@
<div class="inspect-wrap" style="transform:scale({readerState.inspectScale}) translate({readerState.inspectPanX / readerState.inspectScale}px,{readerState.inspectPanY / readerState.inspectScale}px)"> <div class="inspect-wrap" style="transform:scale({readerState.inspectScale}) translate({readerState.inspectPanX / readerState.inspectScale}px,{readerState.inspectPanY / readerState.inspectScale}px)">
{#if pageGroups.length} {#if pageGroups.length}
<div class="double-wrap"> <div class="double-wrap">
{#each currentGroup as pg, i} {#each currentGroup as pg, i (pg)}
{#await resolveUrl(store.pageUrls[pg - 1], 999)} {#await resolveUrl(store.pageUrls[pg - 1], 999)}
<img src="" alt="Page {pg}" class="{imgCls} page-half {i === 0 ? 'gap-left' : 'gap-right'}" decoding="async" draggable="false" /> <div class="page-loader page-half {i === 0 ? 'gap-left' : 'gap-right'}" aria-hidden="true">
<CircleNotch size={20} weight="light" class="anim-spin" style="color:var(--text-faint)" />
</div>
{:then src} {:then src}
<img {src} alt="Page {pg}" class="{imgCls} page-half {i === 0 ? 'gap-left' : 'gap-right'}" decoding="async" draggable="false" /> <img {src} alt="Page {pg}" class="{imgCls} page-half {i === 0 ? 'gap-left' : 'gap-right'}" decoding="async" draggable="false" />
{/await} {/await}
@@ -439,7 +510,9 @@
{:else if pageReady} {:else if pageReady}
<div class="inspect-wrap" style="transform:scale({readerState.inspectScale}) translate({readerState.inspectPanX / readerState.inspectScale}px,{readerState.inspectPanY / readerState.inspectScale}px)"> <div class="inspect-wrap" style="transform:scale({readerState.inspectScale}) translate({readerState.inspectPanX / readerState.inspectScale}px,{readerState.inspectPanY / readerState.inspectScale}px)">
{#await resolveUrl(store.pageUrls[store.pageNumber - 1], 999)} {#await resolveUrl(store.pageUrls[store.pageNumber - 1], 999)}
<img src="" alt="Page {store.pageNumber}" class={imgCls} decoding="async" draggable="false" /> <div class="page-loader page-loader-single" aria-hidden="true">
<CircleNotch size={20} weight="light" class="anim-spin" style="color:var(--text-faint)" />
</div>
{:then src} {:then src}
<img {src} alt="Page {store.pageNumber}" class={imgCls} decoding="async" draggable="false" /> <img {src} alt="Page {store.pageNumber}" class={imgCls} decoding="async" draggable="false" />
{/await} {/await}
@@ -471,7 +544,23 @@
width: var(--effective-width, 100%); width: var(--effective-width, 100%);
max-width: var(--effective-width, 100%); max-width: var(--effective-width, 100%);
aspect-ratio: var(--aspect, 0.667); aspect-ratio: var(--aspect, 0.667);
background: transparent; border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--bg-raised) 90%, transparent);
}
.page-loader {
display: flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, var(--bg-raised) 90%, transparent);
border-radius: var(--radius-sm);
}
.page-loader-single {
width: min(100%, var(--effective-width, 100%));
max-width: var(--effective-width, 100%);
max-height: calc(100vh - 80px);
aspect-ratio: 2 / 3;
} }
.img { display: block; user-select: none; image-rendering: auto; } .img { display: block; user-select: none; image-rendering: auto; }
@@ -489,4 +578,35 @@
.center-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; } .center-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
.error-msg { color: var(--color-error); font-size: var(--text-base); } .error-msg { color: var(--color-error); font-size: var(--text-base); }
.midscroll-cursor {
position: fixed;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid var(--accent-fg);
background: transparent;
pointer-events: none;
z-index: 100;
opacity: 0.85;
}
.midscroll-cursor::before,
.midscroll-cursor::after {
content: "";
position: absolute;
left: 50%;
transform: translateX(-50%);
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
.midscroll-cursor::before {
top: -10px;
border-bottom: 6px solid var(--accent-fg);
}
.midscroll-cursor::after {
bottom: -10px;
border-top: 6px solid var(--accent-fg);
}
</style> </style>
@@ -225,6 +225,7 @@
toggleDirection: () => applySettings({ readingDirection: rtl ? "ltr" : "rtl" }), toggleDirection: () => applySettings({ readingDirection: rtl ? "ltr" : "rtl" }),
openSettings: () => setSettingsOpen(true), openSettings: () => setSettingsOpen(true),
toggleBookmark: () => toggleBookmark(displayChapter, store.pageNumber), toggleBookmark: () => toggleBookmark(displayChapter, store.pageNumber),
toggleAutoScroll: () => { if (style === "longstrip") updateSettings({ autoScroll: !(store.settings.autoScroll ?? false) }); },
toggleMarker: () => { toggleMarker: () => {
if (currentPageMarkers.length > 0) { if (currentPageMarkers.length > 0) {
const first = currentPageMarkers[0]; const first = currentPageMarkers[0];
@@ -178,6 +178,32 @@
aria-checked={store.settings.autoNextChapter ?? false} aria-checked={store.settings.autoNextChapter ?? false}
><span class="toggle-knob"></span></button> ><span class="toggle-knob"></span></button>
</label> </label>
<label class="toggle-row">
<span class="toggle-label">Auto scroll</span>
<button
class="toggle"
class:on={store.settings.autoScroll ?? false}
onclick={() => updateSettings({ autoScroll: !(store.settings.autoScroll ?? false) })}
role="switch"
aria-label="Auto scroll"
aria-checked={store.settings.autoScroll ?? false}
><span class="toggle-knob"></span></button>
</label>
{#if store.settings.autoScroll}
<div class="speed-row">
<span class="speed-label">Speed</span>
<input
type="range"
class="zoom-slider"
min={1}
max={30}
step={1}
value={store.settings.autoScrollSpeed ?? 5}
oninput={(e) => updateSettings({ autoScrollSpeed: Number(e.currentTarget.value) })}
/>
<span class="speed-val">{store.settings.autoScrollSpeed ?? 5}</span>
</div>
{/if}
{/if} {/if}
</section> </section>
@@ -760,4 +786,28 @@
padding: var(--sp-2) 0; padding: var(--sp-2) 0;
text-align: center; text-align: center;
} }
.speed-row {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: var(--sp-1) 0;
}
.speed-label {
font-size: var(--text-xs);
color: var(--text-faint);
flex-shrink: 0;
min-width: 40px;
}
.speed-val {
font-family: var(--font-ui);
font-size: var(--text-xs);
color: var(--text-secondary);
letter-spacing: var(--tracking-wide);
min-width: 1.5ch;
text-align: right;
flex-shrink: 0;
}
</style> </style>
@@ -5,22 +5,22 @@
import type { Chapter } from "@types"; import type { Chapter } from "@types";
interface Props { interface Props {
style: string; style: string;
loading: boolean; loading: boolean;
rtl: boolean; rtl: boolean;
sliderPage: number; sliderPage: number;
sliderMax: number; sliderMax: number;
sliderPct: number; sliderPct: number;
lastPage: number; lastPage: number;
displayChapter: Chapter | null; displayChapter: Chapter | null;
currentBookmark: BookmarkEntry | undefined; currentBookmark: BookmarkEntry | undefined;
activeChapterMarkers: MarkerEntry[]; activeChapterMarkers: MarkerEntry[];
adjacent: { prev: Chapter | null; next: Chapter | null }; adjacent: { prev: Chapter | null; next: Chapter | null };
uiVisible: boolean; uiVisible: boolean;
barPosition: "top" | "left" | "right"; barPosition: "top" | "left" | "right";
onGoPrev: () => void; onGoPrev: () => void;
onGoNext: () => void; onGoNext: () => void;
onJumpToPage: (page: number) => void; onJumpToPage: (page: number) => void;
} }
const { const {
@@ -31,6 +31,25 @@
}: Props = $props(); }: Props = $props();
const isVertical = $derived(barPosition === "left" || barPosition === "right"); const isVertical = $derived(barPosition === "left" || barPosition === "right");
const hValue = $derived(rtl ? sliderMax - sliderPage + 1 : sliderPage);
const hPct = $derived(`--pct:${sliderPct}%`);
const vPct = $derived(`--pct:${sliderPct}%`);
function handleH(e: Event) {
const raw = Number((e.target as HTMLInputElement).value);
onJumpToPage(rtl ? sliderMax - raw + 1 : raw);
}
function handleV(e: Event) {
onJumpToPage(Number((e.target as HTMLInputElement).value));
}
function markerPct(pageNumber: number, forRtl = false): number {
if (sliderMax <= 1) return 0;
const ord = forRtl ? sliderMax - pageNumber + 1 : pageNumber;
return ((ord - 1) / (sliderMax - 1)) * 100;
}
</script> </script>
{#if !isVertical} {#if !isVertical}
@@ -43,44 +62,35 @@
{#if sliderMax > 1} {#if sliderMax > 1}
<div <div
class="slider-wrap" class="slider-wrap"
class:dragging={readerState.sliderDragging}
role="slider"
aria-valuenow={sliderPage}
aria-valuemin={1}
aria-valuemax={sliderMax}
tabindex="-1"
onmouseenter={() => readerState.sliderHover = true} onmouseenter={() => readerState.sliderHover = true}
onmouseleave={() => { readerState.sliderHover = false; readerState.sliderDragging = false; }} onmouseleave={() => readerState.sliderHover = false}
onmousedown={(e) => {
readerState.sliderDragging = true;
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
onJumpToPage(Math.round(1 + (rtl ? 1 - ratio : ratio) * (sliderMax - 1)));
}}
onmousemove={(e) => {
if (!readerState.sliderDragging) return;
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
onJumpToPage(Math.round(1 + (rtl ? 1 - ratio : ratio) * (sliderMax - 1)));
}}
onmouseup={() => readerState.sliderDragging = false}
> >
<div class="slider-track-bg"> <input
<div class="slider-fill" style={rtl ? `width:${100 - sliderPct}%;margin-left:auto` : `width:${sliderPct}%`}></div> type="range"
class="h-range"
style={hPct}
min={1}
max={sliderMax}
value={hValue}
oninput={handleH}
onmousedown={() => readerState.sliderDragging = true}
onmouseup={() => readerState.sliderDragging = false}
/>
<div class="slider-markers" aria-hidden="true">
{#if currentBookmark && currentBookmark.chapterId === displayChapter?.id}
<div class="slider-checkpoint bookmark-checkpoint"
style="left:{markerPct(currentBookmark.pageNumber, rtl)}%"
title="Bookmark: Page {currentBookmark.pageNumber}">
</div>
{/if}
{#each activeChapterMarkers as m (m.id)}
<div class="slider-checkpoint marker-checkpoint"
style="left:{markerPct(m.pageNumber, rtl)}%;background:{MARKER_COLOR_HEX[m.color]}"
title="{m.note ? m.note : 'Marker'} · Page {m.pageNumber}">
</div>
{/each}
</div> </div>
<div class="slider-thumb" style="left:{sliderPct}%"></div>
{#if currentBookmark && currentBookmark.chapterId === displayChapter?.id}
{@const bOrd = rtl ? sliderMax - currentBookmark.pageNumber + 1 : currentBookmark.pageNumber}
{@const bPct = sliderMax > 1 ? ((bOrd - 1) / (sliderMax - 1)) * 100 : 0}
<div class="slider-checkpoint bookmark-checkpoint" style="left:{bPct}%" title="Bookmark: Page {currentBookmark.pageNumber}"></div>
{/if}
{#each activeChapterMarkers as m (m.id)}
{@const mOrd = rtl ? sliderMax - m.pageNumber + 1 : m.pageNumber}
{@const mPct = sliderMax > 1 ? ((mOrd - 1) / (sliderMax - 1)) * 100 : 0}
<div class="slider-checkpoint marker-checkpoint" style="left:{mPct}%;background:{MARKER_COLOR_HEX[m.color]}" title="{m.note ? m.note : 'Marker'} · Page {m.pageNumber}"></div>
{/each}
{#if readerState.sliderHover || readerState.sliderDragging} {#if readerState.sliderHover || readerState.sliderDragging}
<div class="slider-tooltip" style="left:{sliderPct}%"> <div class="slider-tooltip" style="left:{sliderPct}%">
@@ -100,42 +110,37 @@
{#if sliderMax > 1} {#if sliderMax > 1}
<div <div
class="vslider-wrap" class="vslider-wrap"
class:dragging={readerState.sliderDragging}
role="slider"
aria-valuenow={sliderPage}
aria-valuemin={1}
aria-valuemax={sliderMax}
tabindex="-1"
onmouseenter={() => readerState.sliderHover = true} onmouseenter={() => readerState.sliderHover = true}
onmouseleave={() => { readerState.sliderHover = false; readerState.sliderDragging = false; }} onmouseleave={() => readerState.sliderHover = false}
onmousedown={(e) => {
readerState.sliderDragging = true;
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
const ratio = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
onJumpToPage(Math.round(1 + ratio * (sliderMax - 1)));
}}
onmousemove={(e) => {
if (!readerState.sliderDragging) return;
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
const ratio = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
onJumpToPage(Math.round(1 + ratio * (sliderMax - 1)));
}}
onmouseup={() => readerState.sliderDragging = false}
> >
<div class="vslider-track-bg"> <input
<div class="vslider-fill" style="height:{sliderPct}%"></div> type="range"
class="v-range"
style={vPct}
min={1}
max={sliderMax}
value={sliderPage}
oninput={handleV}
onmousedown={() => readerState.sliderDragging = true}
onmouseup={() => readerState.sliderDragging = false}
/>
<div class="vslider-markers" aria-hidden="true">
{#if currentBookmark && currentBookmark.chapterId === displayChapter?.id}
{@const bPct = sliderMax > 1 ? ((currentBookmark.pageNumber - 1) / (sliderMax - 1)) * 100 : 0}
<div class="vslider-checkpoint bookmark-checkpoint"
style="top:{bPct}%"
title="Bookmark: Page {currentBookmark.pageNumber}">
</div>
{/if}
{#each activeChapterMarkers as m (m.id)}
{@const mPct = sliderMax > 1 ? ((m.pageNumber - 1) / (sliderMax - 1)) * 100 : 0}
<div class="vslider-checkpoint marker-checkpoint"
style="top:{mPct}%;background:{MARKER_COLOR_HEX[m.color]}"
title="{m.note ? m.note : 'Marker'} · Page {m.pageNumber}">
</div>
{/each}
</div> </div>
<div class="vslider-thumb" style="top:{sliderPct}%"></div>
{#if currentBookmark && currentBookmark.chapterId === displayChapter?.id}
{@const bPct = sliderMax > 1 ? ((currentBookmark.pageNumber - 1) / (sliderMax - 1)) * 100 : 0}
<div class="vslider-checkpoint bookmark-checkpoint" style="top:{bPct}%" title="Bookmark: Page {currentBookmark.pageNumber}"></div>
{/if}
{#each activeChapterMarkers as m (m.id)}
{@const mPct = sliderMax > 1 ? ((m.pageNumber - 1) / (sliderMax - 1)) * 100 : 0}
<div class="vslider-checkpoint marker-checkpoint" style="top:{mPct}%;background:{MARKER_COLOR_HEX[m.color]}" title="{m.note ? m.note : 'Marker'} · Page {m.pageNumber}"></div>
{/each}
{#if readerState.sliderHover || readerState.sliderDragging} {#if readerState.sliderHover || readerState.sliderDragging}
<div class="vslider-tooltip" style="top:{sliderPct}%" class:tooltip-right={barPosition === "right"}> <div class="vslider-tooltip" style="top:{sliderPct}%" class:tooltip-right={barPosition === "right"}>
@@ -155,101 +160,99 @@
.nav-btn:hover:not(:disabled) { background: var(--bg-raised); color: var(--text-primary); } .nav-btn:hover:not(:disabled) { background: var(--bg-raised); color: var(--text-primary); }
.nav-btn:disabled { opacity: 0.25; cursor: default; } .nav-btn:disabled { opacity: 0.25; cursor: default; }
.slider-wrap { flex: 1; position: relative; display: flex; align-items: center; height: 34px; cursor: pointer; } .slider-wrap { flex: 1; position: relative; display: flex; align-items: center; height: 34px; }
.slider-track-bg { position: absolute; left: 0; right: 0; height: 3px; background: var(--border-strong); border-radius: 2px; pointer-events: none; }
.slider-fill { height: 100%; background: var(--accent-fg); border-radius: 2px; transition: width 0.05s linear; position: relative; }
.slider-checkpoint { position: absolute; top: 50%; width: 4px; height: 10px; border-radius: 2px; transform: translate(-50%, -50%); pointer-events: none; z-index: 1; }
.slider-thumb { position: absolute; top: 50%; transform: translate(-50%, -50%); width: 12px; height: 12px; border-radius: 50%; background: var(--accent-fg); pointer-events: none; z-index: 2; box-shadow: 0 0 0 2px rgba(0,0,0,0.5); transition: transform var(--t-fast); }
.slider-wrap:hover .slider-thumb, .slider-wrap.dragging .slider-thumb { transform: translate(-50%, -50%) scale(1.3); }
.bookmark-checkpoint { background: #ffffff; opacity: 0.8; }
.marker-checkpoint { opacity: 0.85; }
.slider-tooltip { position: absolute; bottom: calc(100% + 2px); transform: translateX(-50%); background: var(--bg-raised); border: 1px solid var(--border-base); border-radius: var(--radius-sm); padding: 2px 6px; font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-secondary); white-space: nowrap; pointer-events: none; z-index: 10; letter-spacing: var(--tracking-wide); }
.slider-wrap:hover .slider-track-bg, .slider-wrap.dragging .slider-track-bg { height: 5px; }
.vbar-progress { .h-range {
display: flex; -webkit-appearance: none;
flex-direction: column; appearance: none;
align-items: center;
justify-content: center;
flex: 1;
width: 100%; width: 100%;
padding: var(--sp-2) 0; height: 34px;
transition: opacity 0.25s ease; background: transparent;
pointer-events: none; cursor: pointer;
position: relative;
z-index: 2;
margin: 0;
padding: 0;
} }
.h-range::-webkit-slider-runnable-track {
height: 3px;
background: linear-gradient(to right, var(--accent-fg) var(--pct, 0%), var(--border-strong) var(--pct, 0%));
border-radius: 2px;
transition: height 0.15s ease, background 0.05s linear;
}
.h-range:hover::-webkit-slider-runnable-track,
.h-range:active::-webkit-slider-runnable-track { height: 5px; }
.h-range::-moz-range-track { height: 3px; background: var(--border-strong); border-radius: 2px; transition: height 0.15s ease; }
.h-range::-moz-range-progress { height: 3px; background: var(--accent-fg); border-radius: 2px; transition: height 0.15s ease; }
.h-range:hover::-moz-range-track, .h-range:active::-moz-range-track { height: 5px; }
.h-range:hover::-moz-range-progress, .h-range:active::-moz-range-progress { height: 5px; }
.h-range::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--accent-fg);
box-shadow: 0 0 0 2px rgba(0,0,0,0.5);
margin-top: -4.5px;
transition: transform var(--t-fast);
}
.h-range:hover::-webkit-slider-thumb,
.h-range:active::-webkit-slider-thumb { transform: scale(1.3); }
.h-range::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--accent-fg);
box-shadow: 0 0 0 2px rgba(0,0,0,0.5);
border: none;
transition: transform var(--t-fast);
}
.h-range:hover::-moz-range-thumb,
.h-range:active::-moz-range-thumb { transform: scale(1.3); }
.slider-markers { position: absolute; inset: 0; pointer-events: none; z-index: 1; }
.slider-checkpoint { position: absolute; top: 50%; width: 4px; height: 10px; border-radius: 2px; transform: translate(-50%, -50%); }
.bookmark-checkpoint { background: #ffffff; opacity: 0.8; }
.marker-checkpoint { opacity: 0.85; }
.slider-tooltip { position: absolute; bottom: calc(100% + 2px); transform: translateX(-50%); background: var(--bg-raised); border: 1px solid var(--border-base); border-radius: var(--radius-sm); padding: 2px 6px; font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-secondary); white-space: nowrap; pointer-events: none; z-index: 10; letter-spacing: var(--tracking-wide); }
.vbar-progress { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; width: 100%; padding: var(--sp-2) 0; transition: opacity 0.25s ease; pointer-events: none; }
.vbar-progress.hidden { opacity: 0; } .vbar-progress.hidden { opacity: 0; }
.vslider-wrap { .vslider-wrap { flex: 1; position: relative; display: flex; flex-direction: column; align-items: center; width: 36px; pointer-events: all; margin: var(--sp-1) 0; }
flex: 1;
position: relative; .v-range {
display: flex; -webkit-appearance: slider-vertical;
flex-direction: column; appearance: slider-vertical;
align-items: center; writing-mode: vertical-lr;
width: 36px; direction: rtl;
width: 34px;
height: 100%;
background: transparent;
cursor: pointer; cursor: pointer;
pointer-events: all; position: relative;
margin: var(--sp-1) 0; z-index: 2;
margin: 0;
padding: 0;
} }
.vslider-track-bg { .v-range::-webkit-slider-runnable-track { width: 5px; background: linear-gradient(to bottom, var(--accent-fg) var(--pct, 0%), var(--border-strong) var(--pct, 0%)); border-radius: 3px; transition: width 0.15s ease, background 0.05s linear; }
position: absolute; .v-range:hover::-webkit-slider-runnable-track,
top: 0; .v-range:active::-webkit-slider-runnable-track { width: 7px; }
bottom: 0; .v-range::-webkit-slider-thumb {
width: 5px; -webkit-appearance: none;
background: var(--border-strong);
border-radius: 3px;
pointer-events: none;
left: 50%;
translate: -50% 0;
}
.vslider-fill {
width: 100%;
background: var(--accent-fg);
border-radius: 3px;
transition: height 0.05s linear;
}
.vslider-thumb {
position: absolute;
left: 50%;
transform: translate(-50%, -50%);
width: 14px; width: 14px;
height: 14px; height: 14px;
border-radius: 50%; border-radius: 50%;
background: var(--accent-fg); background: var(--accent-fg);
pointer-events: none;
z-index: 2;
box-shadow: 0 0 0 2px rgba(0,0,0,0.5); box-shadow: 0 0 0 2px rgba(0,0,0,0.5);
margin-left: -4.5px;
transition: transform var(--t-fast); transition: transform var(--t-fast);
} }
.vslider-wrap:hover .vslider-thumb, .vslider-wrap.dragging .vslider-thumb { transform: translate(-50%, -50%) scale(1.3); } .v-range:hover::-webkit-slider-thumb,
.vslider-wrap:hover .vslider-track-bg, .vslider-wrap.dragging .vslider-track-bg { width: 7px; } .v-range:active::-webkit-slider-thumb { transform: scale(1.3); }
.vslider-checkpoint {
position: absolute; .vslider-markers { position: absolute; inset: 0; pointer-events: none; }
left: 50%; .vslider-checkpoint { position: absolute; left: 50%; transform: translate(-50%, -50%); width: 12px; height: 5px; border-radius: 2px; }
transform: translate(-50%, -50%); .vslider-tooltip { position: absolute; left: calc(100% + 6px); transform: translateY(-50%); background: var(--bg-raised); border: 1px solid var(--border-base); border-radius: var(--radius-sm); padding: 2px 6px; font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-secondary); white-space: nowrap; pointer-events: none; z-index: 10; letter-spacing: var(--tracking-wide); }
width: 12px; .vslider-tooltip.tooltip-right { left: auto; right: calc(100% + 6px); }
height: 5px;
border-radius: 2px;
pointer-events: none;
z-index: 1;
}
.vslider-tooltip {
position: absolute;
left: calc(100% + 6px);
transform: translateY(-50%);
background: var(--bg-raised);
border: 1px solid var(--border-base);
border-radius: var(--radius-sm);
padding: 2px 6px;
font-family: var(--font-ui);
font-size: var(--text-2xs);
color: var(--text-secondary);
white-space: nowrap;
pointer-events: none;
z-index: 10;
letter-spacing: var(--tracking-wide);
}
.vslider-tooltip.tooltip-right {
left: auto;
right: calc(100% + 6px);
}
</style> </style>
@@ -14,6 +14,7 @@ export interface ReaderKeyActions {
openSettings: () => void; openSettings: () => void;
toggleBookmark: () => void; toggleBookmark: () => void;
toggleMarker: () => void; toggleMarker: () => void;
toggleAutoScroll: () => void;
chapterNext: () => void; chapterNext: () => void;
chapterPrev: () => void; chapterPrev: () => void;
closePopovers: () => boolean; closePopovers: () => boolean;
@@ -55,5 +56,6 @@ export function createReaderKeyHandler(actions: ReaderKeyActions): (e: KeyboardE
else if (matchesKeybind(e, kb.openSettings)) { e.preventDefault(); actions.openSettings(); } else if (matchesKeybind(e, kb.openSettings)) { e.preventDefault(); actions.openSettings(); }
else if (matchesKeybind(e, kb.toggleBookmark)) { e.preventDefault(); actions.toggleBookmark(); } else if (matchesKeybind(e, kb.toggleBookmark)) { e.preventDefault(); actions.toggleBookmark(); }
else if (matchesKeybind(e, kb.toggleMarker)) { e.preventDefault(); actions.toggleMarker(); } else if (matchesKeybind(e, kb.toggleMarker)) { e.preventDefault(); actions.toggleMarker(); }
else if (matchesKeybind(e, kb.toggleAutoScroll)) { e.preventDefault(); actions.toggleAutoScroll(); }
}; };
} }
+122 -113
View File
@@ -3,13 +3,13 @@
import { untrack } from "svelte"; import { untrack } from "svelte";
import { gql } from "@api/client"; import { gql } from "@api/client";
import Thumbnail from "@shared/manga/Thumbnail.svelte"; import Thumbnail from "@shared/manga/Thumbnail.svelte";
import { GET_SOURCES } from "@api/queries/extensions"; import { resolvedCover } from "@core/cover/coverResolver";
import { UPDATE_MANGA } from "@api/mutations/manga"; import { GET_SOURCES } from "@api/queries/extensions";
import { FETCH_SOURCE_MANGA } from "@api/mutations/downloads"; import { UPDATE_MANGA } from "@api/mutations/manga";
import { FETCH_SOURCE_MANGA } from "@api/mutations/downloads";
import { FETCH_CHAPTERS, UPDATE_CHAPTERS_PROGRESS } from "@api/mutations/chapters"; import { FETCH_CHAPTERS, UPDATE_CHAPTERS_PROGRESS } from "@api/mutations/chapters";
import { store } from "@store/state.svelte"; import { store } from "@store/state.svelte";
import type { Manga, Chapter } from "@types"; import type { Manga, Chapter, Source } from "@types";
import type { Source } from "@types";
interface Props { interface Props {
manga: Manga; manga: Manga;
@@ -20,6 +20,7 @@
let { manga, currentChapters, onClose, onMigrated }: Props = $props(); let { manga, currentChapters, onClose, onMigrated }: Props = $props();
type Step = "source" | "search" | "confirm"; type Step = "source" | "search" | "confirm";
const STEPS = ["source", "search", "confirm"] as const satisfies Step[];
interface Match { interface Match {
manga: Manga; manga: Manga;
@@ -39,16 +40,15 @@
} }
function onKey(e: KeyboardEvent) { if (e.key === "Escape") onClose(); } function onKey(e: KeyboardEvent) { if (e.key === "Escape") onClose(); }
function focusOnMount(node: HTMLElement) { node.focus(); }
let step: Step = $state("source"); let step: Step = $state("source");
let sources: Source[] = $state([]); let sources: Source[] = $state([]);
let loadingSources = $state(true); let loadingSources = $state(true);
let selectedSource: Source | null = $state(null); let selectedSource: Source | null = $state(null);
let selectedLang = $state("all");
let selectedLang: string = $state("all");
let langStripEl: HTMLDivElement | undefined = $state(); let langStripEl: HTMLDivElement | undefined = $state();
const stepIdx = $derived(STEPS.indexOf(step));
const availableLangs = $derived.by(() => { const availableLangs = $derived.by(() => {
const langs = Array.from(new Set<string>(sources.map(s => s.lang))).sort(); const langs = Array.from(new Set<string>(sources.map(s => s.lang))).sort();
const en = langs.indexOf("en"); const en = langs.indexOf("en");
@@ -56,21 +56,7 @@
return langs; return langs;
}); });
const hasMultipleLangs = $derived(availableLangs.length > 1); const hasMultipleLangs = $derived(availableLangs.length > 1);
const visibleSources = $derived.by(() => {
function scrollLangStrip(dir: -1 | 1) {
if (!langStripEl) return;
const chips = Array.from(langStripEl.children) as HTMLElement[];
const viewEnd = langStripEl.scrollLeft + langStripEl.clientWidth;
if (dir === 1) {
const next = chips.find(c => c.offsetLeft + c.offsetWidth > viewEnd + 2);
if (next) langStripEl.scrollTo({ left: next.offsetLeft, behavior: "smooth" });
} else {
const prev = [...chips].reverse().find(c => c.offsetLeft < langStripEl!.scrollLeft - 2);
if (prev) langStripEl.scrollTo({ left: prev.offsetLeft + prev.offsetWidth - langStripEl.clientWidth, behavior: "smooth" });
}
}
const visibleSources = $derived.by(() => {
if (selectedLang !== "all") return sources.filter(s => s.lang === selectedLang); if (selectedLang !== "all") return sources.filter(s => s.lang === selectedLang);
const map = new Map<string, Source>(); const map = new Map<string, Source>();
for (const s of sources) { for (const s of sources) {
@@ -80,19 +66,30 @@
return Array.from(map.values()); return Array.from(map.values());
}); });
let query = $state(untrack(() => manga.title)); let query = $state(untrack(() => manga.title));
let results: { manga: Manga; similarity: number }[] = $state([]); let results: { manga: Manga; similarity: number }[] = $state([]);
let searching = $state(false); let searching = $state(false);
let selectedMatch: Match | null = $state(null); let selectedMatch: Match | null = $state(null);
let loadingMatchId: number | null = $state(null); let loadingMatchId: number | null = $state(null);
let migrating = $state(false); let migrating = $state(false);
let error: string | null = $state(null); let error: string | null = $state(null);
const readCount = $derived(currentChapters.filter(c => c.isRead).length); const readCount = $derived(currentChapters.filter(c => c.isRead).length);
const totalCount = $derived(currentChapters.length); const totalCount = $derived(currentChapters.length);
const chapterDiff = $derived(selectedMatch ? selectedMatch.chapters.length - totalCount : 0); const chapterDiff = $derived(selectedMatch ? selectedMatch.chapters.length - totalCount : 0);
const STEPS = ["source", "search", "confirm"] as const satisfies Step[];
const stepIdx = $derived(STEPS.indexOf(step)); function scrollLangStrip(dir: -1 | 1) {
if (!langStripEl) return;
const chips = Array.from(langStripEl.children) as HTMLElement[];
const viewEnd = langStripEl.scrollLeft + langStripEl.clientWidth;
if (dir === 1) {
const next = chips.find(c => c.offsetLeft + c.offsetWidth > viewEnd + 2);
if (next) langStripEl.scrollTo({ left: next.offsetLeft, behavior: "smooth" });
} else {
const prev = [...chips].reverse().find(c => c.offsetLeft < langStripEl!.scrollLeft - 2);
if (prev) langStripEl.scrollTo({ left: prev.offsetLeft + prev.offsetWidth - langStripEl.clientWidth, behavior: "smooth" });
}
}
$effect(() => { $effect(() => {
gql<{ sources: { nodes: Source[] } }>(GET_SOURCES) gql<{ sources: { nodes: Source[] } }>(GET_SOURCES)
@@ -136,7 +133,7 @@
async function selectMatch(m: Manga, similarity: number) { async function selectMatch(m: Manga, similarity: number) {
loadingMatchId = m.id; error = null; loadingMatchId = m.id; error = null;
try { try {
const d = await gql<{ fetchChapters: { chapters: Chapter[] } }>(FETCH_CHAPTERS, { mangaId: m.id }); const d = await gql<{ fetchChapters: { chapters: Chapter[] } }>(FETCH_CHAPTERS, { mangaId: m.id });
const chapters = d.fetchChapters.chapters; const chapters = d.fetchChapters.chapters;
const matchReadCount = chapters.filter(c => { const matchReadCount = chapters.filter(c => {
const old = currentChapters.find(o => Math.abs(o.chapterNumber - c.chapterNumber) < 0.01); const old = currentChapters.find(o => Math.abs(o.chapterNumber - c.chapterNumber) < 0.01);
@@ -158,27 +155,25 @@
const { manga: newManga, chapters: newChapters } = selectedMatch; const { manga: newManga, chapters: newChapters } = selectedMatch;
const oldByNum = new Map(currentChapters.map(c => [Math.round(c.chapterNumber * 100), c])); const oldByNum = new Map(currentChapters.map(c => [Math.round(c.chapterNumber * 100), c]));
const toMarkRead: number[] = []; const toMarkRead: number[] = [];
const toMarkBookmarked: number[] = []; const toMarkBookmarked: number[] = [];
const progressUpdates: { id: number; lastPageRead: number }[] = []; const progressUpdates: { id: number; lastPageRead: number }[] = [];
for (const nc of newChapters) { for (const nc of newChapters) {
const old = oldByNum.get(Math.round(nc.chapterNumber * 100)); const old = oldByNum.get(Math.round(nc.chapterNumber * 100));
if (!old) continue; if (!old) continue;
if (old.isRead) toMarkRead.push(nc.id); if (old.isRead) toMarkRead.push(nc.id);
if (old.isBookmarked) toMarkBookmarked.push(nc.id); if (old.isBookmarked) toMarkBookmarked.push(nc.id);
if ((old.lastPageRead ?? 0) > 0 && !old.isRead) if ((old.lastPageRead ?? 0) > 0 && !old.isRead)
progressUpdates.push({ id: nc.id, lastPageRead: old.lastPageRead! }); progressUpdates.push({ id: nc.id, lastPageRead: old.lastPageRead! });
} }
if (toMarkRead.length) if (toMarkRead.length) await gql(UPDATE_CHAPTERS_PROGRESS, { ids: toMarkRead, isRead: true });
await gql(UPDATE_CHAPTERS_PROGRESS, { ids: toMarkRead, isRead: true }); if (toMarkBookmarked.length) await gql(UPDATE_CHAPTERS_PROGRESS, { ids: toMarkBookmarked, isBookmarked: true });
if (toMarkBookmarked.length)
await gql(UPDATE_CHAPTERS_PROGRESS, { ids: toMarkBookmarked, isBookmarked: true });
for (const { id, lastPageRead } of progressUpdates) for (const { id, lastPageRead } of progressUpdates)
await gql(UPDATE_CHAPTERS_PROGRESS, { ids: [id], lastPageRead }); await gql(UPDATE_CHAPTERS_PROGRESS, { ids: [id], lastPageRead });
await gql(UPDATE_MANGA, { id: newManga.id, inLibrary: true }); await gql(UPDATE_MANGA, { id: newManga.id, inLibrary: true });
await gql(UPDATE_MANGA, { id: manga.id, inLibrary: false }); await gql(UPDATE_MANGA, { id: manga.id, inLibrary: false });
onMigrated({ ...newManga, inLibrary: true }); onMigrated({ ...newManga, inLibrary: true });
@@ -189,15 +184,22 @@
} }
</script> </script>
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="overlay" onclick={(e) => { if (e.target === e.currentTarget) onClose(); }}> <div class="overlay" onclick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
<div class="modal"> <div class="modal">
<div class="modal-header"> <div class="modal-header">
<div class="modal-title"> <div class="manga-context">
<span class="modal-title-label">Migrate source</span> <div class="manga-context-cover">
<span class="modal-title-manga">{manga.title}</span> <Thumbnail src={resolvedCover(manga.id, manga.thumbnailUrl)} alt={manga.title} class="ctx-cover" />
</div>
<div class="manga-context-info">
<span class="modal-eyebrow">Migrate source</span>
<span class="modal-title">{manga.title}</span>
{#if manga.source?.displayName}
<span class="modal-source">{manga.source.displayName}</span>
{/if}
</div>
</div> </div>
<button class="close-btn" onclick={onClose}><X size={14} weight="light" /></button> <button class="close-btn" onclick={onClose}><X size={14} weight="light" /></button>
</div> </div>
@@ -209,7 +211,7 @@
{#if i < stepIdx}<Check size={9} weight="bold" />{:else}{i + 1}{/if} {#if i < stepIdx}<Check size={9} weight="bold" />{:else}{i + 1}{/if}
</span> </span>
<span class="step-label"> <span class="step-label">
{st === "source" ? "Pick source" : st === "search" ? (selectedSource ? selectedSource.displayName : "Search") : "Confirm"} {st === "source" ? "Pick source" : st === "search" ? (selectedSource?.displayName ?? "Search") : "Confirm"}
</span> </span>
</div> </div>
{/each} {/each}
@@ -241,11 +243,10 @@
{/if} {/if}
<div class="source-list"> <div class="source-list">
{#each visibleSources as src} {#each visibleSources as src}
<button <button class="source-row" class:source-row-active={selectedSource?.id === src.id} onclick={() => pickSource(src)}>
class="source-row" <div class="source-icon-wrap">
class:source-row-active={selectedSource?.id === src.id} <Thumbnail src={src.iconUrl} alt={src.name} class="source-icon" onerror={(e) => (e.target as HTMLImageElement).style.display = "none"} />
onclick={() => pickSource(src)}> </div>
<Thumbnail src={src.iconUrl} alt={src.name} class="source-icon" onerror={(e) => (e.target as HTMLImageElement).style.display = "none"} />
<div class="source-info"> <div class="source-info">
<span class="source-name">{src.displayName}</span> <span class="source-name">{src.displayName}</span>
<span class="source-meta">{src.lang.toUpperCase()}{src.isNsfw ? " · NSFW" : ""}</span> <span class="source-meta">{src.lang.toUpperCase()}{src.isNsfw ? " · NSFW" : ""}</span>
@@ -260,7 +261,9 @@
<div class="search-step"> <div class="search-step">
{#if selectedSource} {#if selectedSource}
<div class="search-context"> <div class="search-context">
<Thumbnail src={selectedSource.iconUrl} alt="" class="search-context-icon" onerror={(e) => (e.target as HTMLImageElement).style.display = "none"} /> <div class="source-icon-wrap" style="width:20px;height:20px;border-radius:var(--radius-sm)">
<Thumbnail src={selectedSource.iconUrl} alt={selectedSource.name} class="search-context-icon" onerror={(e) => (e.target as HTMLImageElement).style.display = "none"} />
</div>
<span class="search-context-name">{selectedSource.displayName}</span> <span class="search-context-name">{selectedSource.displayName}</span>
<button class="search-context-change" onclick={() => { step = "source"; results = []; }}>Change</button> <button class="search-context-change" onclick={() => { step = "source"; results = []; }}>Change</button>
</div> </div>
@@ -274,7 +277,7 @@
bind:value={query} bind:value={query}
onkeydown={(e) => e.key === "Enter" && selectedSource && searchSource(selectedSource, query)} onkeydown={(e) => e.key === "Enter" && selectedSource && searchSource(selectedSource, query)}
placeholder="Search title…" placeholder="Search title…"
use:focusOnMount /> autofocus />
</div> </div>
<button class="search-btn" <button class="search-btn"
onclick={() => selectedSource && searchSource(selectedSource, query)} onclick={() => selectedSource && searchSource(selectedSource, query)}
@@ -291,22 +294,20 @@
<div class="results"> <div class="results">
{#if searching} {#if searching}
{#each Array(6) as _} {#each Array(5) as _}
<div class="sk-result"> <div class="sk-result">
<div class="skeleton sk-cover"></div> <div class="skeleton sk-cover"></div>
<div class="sk-meta"> <div class="sk-meta">
<div class="skeleton sk-title"></div> <div class="skeleton sk-line" style="width:60%"></div>
<div class="skeleton sk-title" style="width:40%"></div> <div class="skeleton sk-line" style="width:35%"></div>
</div> </div>
</div> </div>
{/each} {/each}
{:else} {:else}
{#each results as { manga: m, similarity }, idx} {#each results as { manga: m, similarity }, idx}
<button class="result-row" <button class="result-row" onclick={() => selectMatch(m, similarity)} disabled={loadingMatchId !== null}>
onclick={() => selectMatch(m, similarity)}
disabled={loadingMatchId !== null}>
<div class="result-cover-wrap"> <div class="result-cover-wrap">
<Thumbnail src={m.thumbnailUrl} alt={m.title} class="result-cover" /> <Thumbnail src={resolvedCover(m.id, m.thumbnailUrl)} alt={m.title} class="result-cover" />
</div> </div>
<div class="result-info"> <div class="result-info">
<span class="result-title">{m.title}</span> <span class="result-title">{m.title}</span>
@@ -315,17 +316,17 @@
<span class="best-match-badge"><Sparkle size={9} weight="fill" /> Best match</span> <span class="best-match-badge"><Sparkle size={9} weight="fill" /> Best match</span>
{/if} {/if}
<span class="sim-bar"><span class="sim-fill" style="width:{Math.round(similarity * 100)}%"></span></span> <span class="sim-bar"><span class="sim-fill" style="width:{Math.round(similarity * 100)}%"></span></span>
<span class="sim-label">{Math.round(similarity * 100)}% match</span> <span class="sim-label">{Math.round(similarity * 100)}%</span>
</div> </div>
</div> </div>
{#if loadingMatchId === m.id} {#if loadingMatchId === m.id}
<CircleNotch size={13} weight="light" class="anim-spin" style="color:var(--text-faint);flex-shrink:0" /> <CircleNotch size={13} weight="light" class="anim-spin" style="color:var(--text-faint);flex-shrink:0" />
{:else} {:else}
<ArrowRight size={13} weight="light" style="color:var(--text-faint);flex-shrink:0;opacity:0.5" /> <ArrowRight size={13} weight="light" style="color:var(--text-faint);flex-shrink:0;opacity:0.4" />
{/if} {/if}
</button> </button>
{/each} {/each}
{#if results.length === 0 && !error} {#if results.length === 0 && !error && !searching}
<div class="centered"> <div class="centered">
<span class="hint">{query ? "No results — try a different title." : "Enter a title to search."}</span> <span class="hint">{query ? "No results — try a different title." : "Enter a title to search."}</span>
</div> </div>
@@ -339,18 +340,18 @@
<div class="confirm-row"> <div class="confirm-row">
<div class="confirm-manga"> <div class="confirm-manga">
<div class="confirm-cover-wrap"> <div class="confirm-cover-wrap">
<Thumbnail src={manga.thumbnailUrl} alt={manga.title} class="confirm-cover" /> <Thumbnail src={resolvedCover(manga.id, manga.thumbnailUrl)} alt={manga.title} class="confirm-cover" />
</div> </div>
<p class="confirm-title">{manga.title}</p> <p class="confirm-title">{manga.title}</p>
<p class="confirm-source">{manga.source?.displayName ?? "Unknown"}</p> <p class="confirm-source">{manga.source?.displayName ?? "Unknown"}</p>
<span class="confirm-tag">Current</span> <span class="confirm-tag">Current</span>
</div> </div>
<div class="confirm-divider"> <div class="confirm-arrow-wrap">
<ArrowRight size={16} weight="light" class="confirm-arrow" /> <ArrowRight size={18} weight="light" style="color:var(--text-faint)" />
</div> </div>
<div class="confirm-manga"> <div class="confirm-manga">
<div class="confirm-cover-wrap"> <div class="confirm-cover-wrap">
<Thumbnail src={selectedMatch.manga.thumbnailUrl} alt={selectedMatch.manga.title} class="confirm-cover" /> <Thumbnail src={resolvedCover(selectedMatch.manga.id, selectedMatch.manga.thumbnailUrl)} alt={selectedMatch.manga.title} class="confirm-cover" />
</div> </div>
<p class="confirm-title">{selectedMatch.manga.title}</p> <p class="confirm-title">{selectedMatch.manga.title}</p>
<p class="confirm-source">{selectedSource?.displayName ?? "Unknown"}</p> <p class="confirm-source">{selectedSource?.displayName ?? "Unknown"}</p>
@@ -378,8 +379,8 @@
</span> </span>
</div> </div>
<div class="stat-row"> <div class="stat-row">
<span class="stat-label">Read progress to carry over</span> <span class="stat-label">Read progress to carry</span>
<span class="stat-val">{selectedMatch.readCount} / {readCount} chapters</span> <span class="stat-val">{selectedMatch.readCount} / {readCount}</span>
</div> </div>
</div> </div>
@@ -413,54 +414,60 @@
<style> <style>
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 200; animation: fadeIn 0.1s ease both; } .overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 200; animation: fadeIn 0.1s ease both; }
.modal { background: var(--bg-base); border: 1px solid var(--border-base); border-radius: var(--radius-xl); width: 520px; max-height: 80vh; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 8px 40px rgba(0,0,0,0.5); } .modal { background: var(--bg-base); border: 1px solid var(--border-base); border-radius: var(--radius-xl); width: 520px; max-height: 82vh; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 8px 40px rgba(0,0,0,0.5); }
.modal-header { display: flex; align-items: flex-start; justify-content: space-between; padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
.modal-title { display: flex; flex-direction: column; gap: 2px; } .modal-header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
.modal-title-label { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; } .manga-context { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
.modal-title-manga { font-size: var(--text-base); font-weight: var(--weight-medium); color: var(--text-primary); letter-spacing: var(--tracking-tight); } .manga-context-cover { width: 36px; height: 54px; border-radius: var(--radius-sm); overflow: hidden; flex-shrink: 0; background: var(--bg-raised); border: 1px solid var(--border-dim); }
.close-btn { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: var(--radius-md); color: var(--text-faint); background: none; border: none; cursor: pointer; transition: color var(--t-base), background var(--t-base); flex-shrink: 0; } :global(.ctx-cover) { width: 100%; height: 100%; object-fit: cover; }
.manga-context-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.modal-eyebrow { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; }
.modal-title { font-size: var(--text-base); font-weight: var(--weight-medium); color: var(--text-primary); letter-spacing: var(--tracking-tight); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.modal-source { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
.close-btn { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: var(--radius-md); color: var(--text-faint); background: none; border: none; cursor: pointer; transition: color var(--t-base), background var(--t-base); flex-shrink: 0; margin-top: 2px; }
.close-btn:hover { color: var(--text-muted); background: var(--bg-raised); } .close-btn:hover { color: var(--text-muted); background: var(--bg-raised); }
.steps { display: flex; align-items: center; gap: var(--sp-1); padding: var(--sp-3) var(--sp-5); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; } .steps { display: flex; align-items: center; gap: var(--sp-1); padding: var(--sp-3) var(--sp-5); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
.step { display: flex; align-items: center; gap: var(--sp-2); opacity: 0.4; transition: opacity var(--t-base); } .step { display: flex; align-items: center; gap: var(--sp-2); opacity: 0.35; transition: opacity var(--t-base); }
.step + .step::before { content: ""; color: var(--text-faint); margin-right: var(--sp-1); font-size: var(--text-sm); } .step + .step::before { content: ""; color: var(--text-faint); margin-right: var(--sp-1); font-size: var(--text-sm); opacity: 0.5; }
.step-active { opacity: 1; } .step-active { opacity: 1; }
.step-done { opacity: 0.6; } .step-done { opacity: 0.55; }
.step-dot { width: 18px; height: 18px; border-radius: 50%; background: var(--bg-raised); border: 1px solid var(--border-base); display: flex; align-items: center; justify-content: center; font-family: var(--font-ui); font-size: 10px; color: var(--text-faint); flex-shrink: 0; } .step-dot { width: 18px; height: 18px; border-radius: 50%; background: var(--bg-raised); border: 1px solid var(--border-base); display: flex; align-items: center; justify-content: center; font-family: var(--font-ui); font-size: 10px; color: var(--text-faint); flex-shrink: 0; }
.step-active .step-dot { background: var(--accent-muted); border-color: var(--accent-dim); color: var(--accent-fg); } .step-active .step-dot { background: var(--accent-muted); border-color: var(--accent-dim); color: var(--accent-fg); }
.step-label { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--text-muted); } .step-label { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--text-muted); }
.step-active .step-label { color: var(--text-secondary); } .step-active .step-label { color: var(--text-secondary); }
.body { flex: 1; overflow: hidden; display: flex; flex-direction: column; } .body { flex: 1; overflow: hidden; display: flex; flex-direction: column; min-height: 0; }
.centered { flex: 1; display: flex; align-items: center; justify-content: center; padding: var(--sp-8); } .centered { flex: 1; display: flex; align-items: center; justify-content: center; padding: var(--sp-8); }
.hint { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); } .hint { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
.src-lang-bar { display: flex; align-items: center; gap: var(--sp-1); padding: var(--sp-2); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
.src-lang-nav { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; flex-shrink: 0; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); font-size: 15px; line-height: 1; cursor: pointer; transition: color var(--t-base), background var(--t-base); }
.src-lang-nav:hover { color: var(--text-muted); background: var(--bg-raised); }
.src-lang-chips { display: flex; align-items: center; gap: var(--sp-1); flex: 1; min-width: 0; overflow-x: auto; scrollbar-width: none; }
.src-lang-chips::-webkit-scrollbar { display: none; }
.src-lang-chip { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); padding: 3px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); cursor: pointer; white-space: nowrap; flex-shrink: 0; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
.src-lang-chip:hover { color: var(--text-muted); background: var(--bg-raised); }
.src-lang-chip-active { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
.source-list { flex: 1; overflow-y: auto; padding: var(--sp-2); display: flex; flex-direction: column; gap: 1px; } .source-list { flex: 1; overflow-y: auto; padding: var(--sp-2); display: flex; flex-direction: column; gap: 1px; }
.source-row { display: flex; align-items: center; gap: var(--sp-3); padding: 9px var(--sp-3); border-radius: var(--radius-md); border: 1px solid transparent; background: none; text-align: left; width: 100%; cursor: pointer; transition: background var(--t-fast), border-color var(--t-fast); } .source-row { display: flex; align-items: center; gap: var(--sp-3); padding: 8px var(--sp-3); border-radius: var(--radius-md); border: 1px solid transparent; background: none; text-align: left; width: 100%; cursor: pointer; transition: background var(--t-fast), border-color var(--t-fast); }
.source-row:hover { background: var(--bg-raised); border-color: var(--border-dim); } .source-row:hover { background: var(--bg-raised); border-color: var(--border-dim); }
.source-row-active { background: var(--accent-muted); border-color: var(--accent-dim); } .source-row-active { background: var(--accent-muted); border-color: var(--accent-dim); }
:global(.source-icon) { width: 28px; height: 28px; border-radius: var(--radius-md); object-fit: cover; flex-shrink: 0; background: var(--bg-raised); } .source-icon-wrap { width: 28px; height: 28px; border-radius: var(--radius-md); overflow: hidden; flex-shrink: 0; background: var(--bg-raised); }
:global(.source-icon) { width: 100%; height: 100%; object-fit: cover; }
.source-info { flex: 1; display: flex; flex-direction: column; gap: 2px; overflow: hidden; } .source-info { flex: 1; display: flex; flex-direction: column; gap: 2px; overflow: hidden; }
.source-name { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .source-name { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.source-meta { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); } .source-meta { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
:global(.source-arrow) { color: var(--text-faint); opacity: 0; transition: opacity var(--t-base); } :global(.source-arrow) { color: var(--text-faint); opacity: 0; transition: opacity var(--t-base); flex-shrink: 0; }
.source-row:hover :global(.source-arrow) { opacity: 1; } .source-row:hover :global(.source-arrow) { opacity: 1; }
.src-lang-bar { display: flex; align-items: center; gap: var(--sp-1); padding: var(--sp-2); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
.src-lang-nav { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; flex-shrink: 0; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); font-size: 15px; line-height: 1; cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
.src-lang-nav:hover { color: var(--text-muted); border-color: var(--border-strong); background: var(--bg-raised); }
.src-lang-chips { display: flex; align-items: center; gap: var(--sp-1); flex: 1; min-width: 0; overflow-x: auto; scrollbar-width: none; scroll-behavior: smooth; }
.src-lang-chips::-webkit-scrollbar { display: none; }
.src-lang-chip { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); padding: 3px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); cursor: pointer; white-space: nowrap; flex-shrink: 0; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
.src-lang-chip:hover { color: var(--text-muted); border-color: var(--border-strong); background: var(--bg-raised); }
.src-lang-chip-active { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
.search-step { flex: 1; overflow: hidden; display: flex; flex-direction: column; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); } .search-step { flex: 1; overflow: hidden; display: flex; flex-direction: column; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); }
.search-context { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); flex-shrink: 0; } .search-context { display: flex; align-items: center; gap: var(--sp-2); padding: 6px var(--sp-3); background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); flex-shrink: 0; }
:global(.search-context-icon) { width: 18px; height: 18px; border-radius: var(--radius-sm); object-fit: cover; flex-shrink: 0; } :global(.search-context-icon) { width: 100%; height: 100%; object-fit: cover; }
.search-context-name { flex: 1; font-size: var(--text-sm); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .search-context-name { flex: 1; font-size: var(--text-sm); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.search-context-change { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--accent-fg); background: none; border: none; cursor: pointer; padding: 0; flex-shrink: 0; transition: opacity var(--t-base); } .search-context-change { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--accent-fg); background: none; border: none; cursor: pointer; padding: 0; flex-shrink: 0; opacity: 0.8; transition: opacity var(--t-base); }
.search-context-change:hover { opacity: 0.75; } .search-context-change:hover { opacity: 1; }
.search-row { display: flex; align-items: center; gap: var(--sp-2); flex-shrink: 0; } .search-row { display: flex; align-items: center; gap: var(--sp-2); flex-shrink: 0; }
.search-bar { flex: 1; display: flex; align-items: center; gap: var(--sp-2); background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 0 var(--sp-3) 0 var(--sp-2); transition: border-color var(--t-base); } .search-bar { flex: 1; display: flex; align-items: center; gap: var(--sp-2); background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 0 var(--sp-3) 0 var(--sp-2); transition: border-color var(--t-base); }
.search-bar:focus-within { border-color: var(--border-strong); } .search-bar:focus-within { border-color: var(--border-strong); }
@@ -470,8 +477,9 @@
.search-btn { font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 6px 12px; border-radius: var(--radius-md); background: var(--accent-muted); color: var(--accent-fg); border: 1px solid var(--accent-dim); cursor: pointer; flex-shrink: 0; display: flex; align-items: center; gap: var(--sp-1); transition: filter var(--t-base); } .search-btn { font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 6px 12px; border-radius: var(--radius-md); background: var(--accent-muted); color: var(--accent-fg); border: 1px solid var(--accent-dim); cursor: pointer; flex-shrink: 0; display: flex; align-items: center; gap: var(--sp-1); transition: filter var(--t-base); }
.search-btn:hover:not(:disabled) { filter: brightness(1.1); } .search-btn:hover:not(:disabled) { filter: brightness(1.1); }
.search-btn:disabled { opacity: 0.4; cursor: default; } .search-btn:disabled { opacity: 0.4; cursor: default; }
.results { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 1px; } .results { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 1px; }
.result-row { display: flex; align-items: center; gap: var(--sp-3); padding: 7px var(--sp-2); border-radius: var(--radius-md); border: none; background: none; text-align: left; width: 100%; cursor: pointer; transition: background var(--t-fast); } .result-row { display: flex; align-items: center; gap: var(--sp-3); padding: 6px var(--sp-2); border-radius: var(--radius-md); border: none; background: none; text-align: left; width: 100%; cursor: pointer; transition: background var(--t-fast); }
.result-row:hover:not(:disabled) { background: var(--bg-raised); } .result-row:hover:not(:disabled) { background: var(--bg-raised); }
.result-row:disabled { opacity: 0.5; cursor: default; } .result-row:disabled { opacity: 0.5; cursor: default; }
.result-cover-wrap { width: 36px; height: 54px; border-radius: var(--radius-sm); overflow: hidden; background: var(--bg-raised); border: 1px solid var(--border-dim); flex-shrink: 0; } .result-cover-wrap { width: 36px; height: 54px; border-radius: var(--radius-sm); overflow: hidden; background: var(--bg-raised); border: 1px solid var(--border-dim); flex-shrink: 0; }
@@ -480,45 +488,46 @@
.result-title { font-size: var(--text-sm); color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .result-title { font-size: var(--text-sm); color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.result-meta { display: flex; align-items: center; gap: var(--sp-2); } .result-meta { display: flex; align-items: center; gap: var(--sp-2); }
.best-match-badge { display: inline-flex; align-items: center; gap: 3px; font-family: var(--font-ui); font-size: 9px; letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--accent-fg); background: var(--accent-muted); border: 1px solid var(--accent-dim); padding: 1px 5px; border-radius: var(--radius-sm); flex-shrink: 0; } .best-match-badge { display: inline-flex; align-items: center; gap: 3px; font-family: var(--font-ui); font-size: 9px; letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--accent-fg); background: var(--accent-muted); border: 1px solid var(--accent-dim); padding: 1px 5px; border-radius: var(--radius-sm); flex-shrink: 0; }
.sim-bar { width: 48px; height: 3px; background: var(--bg-overlay); border-radius: var(--radius-full); overflow: hidden; flex-shrink: 0; display: inline-block; } .sim-bar { width: 40px; height: 3px; background: var(--bg-overlay); border-radius: var(--radius-full); overflow: hidden; flex-shrink: 0; }
.sim-fill { display: block; height: 100%; background: var(--accent); border-radius: var(--radius-full); transition: width 0.2s ease; } .sim-fill { display: block; height: 100%; background: var(--accent); border-radius: var(--radius-full); }
.sim-label { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); white-space: nowrap; } .sim-label { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); white-space: nowrap; }
.sk-result { display: flex; align-items: center; gap: var(--sp-3); padding: 7px var(--sp-2); } .sk-result { display: flex; align-items: center; gap: var(--sp-3); padding: 6px var(--sp-2); }
.sk-cover { width: 36px; height: 54px; border-radius: var(--radius-sm); flex-shrink: 0; } .sk-cover { width: 36px; height: 54px; border-radius: var(--radius-sm); flex-shrink: 0; }
.sk-meta { flex: 1; display: flex; flex-direction: column; gap: var(--sp-2); } .sk-meta { flex: 1; display: flex; flex-direction: column; gap: var(--sp-2); }
.sk-title { height: 13px; width: 65%; border-radius: var(--radius-sm); } .sk-line { height: 12px; border-radius: var(--radius-sm); }
.confirm-step { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: var(--sp-4); padding: var(--sp-4) var(--sp-5); } .confirm-step { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: var(--sp-4); padding: var(--sp-4) var(--sp-5); }
.confirm-row { display: flex; align-items: center; justify-content: center; gap: var(--sp-4); } .confirm-row { display: flex; align-items: flex-start; justify-content: center; gap: var(--sp-3); }
.confirm-manga { display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); flex: 1; max-width: 160px; } .confirm-manga { display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); flex: 1; max-width: 150px; }
.confirm-cover-wrap { width: 100%; aspect-ratio: 2/3; border-radius: var(--radius-md); overflow: hidden; background: var(--bg-raised); border: 1px solid var(--border-dim); } .confirm-cover-wrap { width: 100%; aspect-ratio: 2/3; border-radius: var(--radius-md); overflow: hidden; background: var(--bg-raised); border: 1px solid var(--border-dim); }
:global(.confirm-cover) { width: 100%; height: 100%; object-fit: cover; } :global(.confirm-cover) { width: 100%; height: 100%; object-fit: cover; }
.confirm-title { font-size: var(--text-xs); color: var(--text-secondary); text-align: center; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: var(--leading-snug); } .confirm-title { font-size: var(--text-xs); color: var(--text-secondary); text-align: center; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: var(--leading-snug); }
.confirm-source { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); text-align: center; } .confirm-source { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); text-align: center; }
.confirm-divider { display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
:global(.confirm-arrow) { color: var(--text-faint); }
.confirm-tag { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; padding: 2px 7px; border-radius: var(--radius-full); background: var(--bg-raised); border: 1px solid var(--border-dim); color: var(--text-faint); } .confirm-tag { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; padding: 2px 7px; border-radius: var(--radius-full); background: var(--bg-raised); border: 1px solid var(--border-dim); color: var(--text-faint); }
.confirm-tag-new { background: var(--accent-muted); border-color: var(--accent-dim); color: var(--accent-fg); } .confirm-tag-new { background: var(--accent-muted); border-color: var(--accent-dim); color: var(--accent-fg); }
.confirm-arrow-wrap { display: flex; align-items: center; padding-top: 48px; flex-shrink: 0; }
.confirm-stats { display: flex; flex-direction: column; gap: var(--sp-2); background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: var(--sp-3) var(--sp-4); } .confirm-stats { display: flex; flex-direction: column; gap: var(--sp-2); background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: var(--sp-3) var(--sp-4); }
.stat-row { display: flex; justify-content: space-between; align-items: center; } .stat-row { display: flex; justify-content: space-between; align-items: center; }
.stat-label { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); letter-spacing: var(--tracking-wide); } .stat-label { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); letter-spacing: var(--tracking-wide); }
.stat-val { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-secondary); letter-spacing: var(--tracking-wide); } .stat-val { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-secondary); letter-spacing: var(--tracking-wide); }
.stat-good { color: var(--color-success) !important; } .stat-good { color: var(--color-success) !important; }
.stat-warn { color: #d97706 !important; } .stat-warn { color: #d97706 !important; }
.stat-bad { color: var(--color-error) !important; } .stat-bad { color: var(--color-error) !important; }
.chapter-diff { font-family: var(--font-ui); font-size: var(--text-2xs); color: #d97706; letter-spacing: var(--tracking-wide); margin-left: var(--sp-2); } .chapter-diff { font-family: var(--font-ui); font-size: var(--text-2xs); color: #d97706; letter-spacing: var(--tracking-wide); margin-left: var(--sp-2); }
.warn-box { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); background: rgba(217,119,6,0.08); border: 1px solid rgba(217,119,6,0.25); border-radius: var(--radius-md); font-size: var(--text-xs); color: #d97706; line-height: var(--leading-snug); }
.warn-box { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); background: rgba(217,119,6,0.08); border: 1px solid rgba(217,119,6,0.25); border-radius: var(--radius-md); font-size: var(--text-xs); color: #d97706; line-height: var(--leading-snug); flex-shrink: 0; }
.confirm-note { font-size: var(--text-xs); color: var(--text-faint); line-height: var(--leading-base); } .confirm-note { font-size: var(--text-xs); color: var(--text-faint); line-height: var(--leading-base); }
.confirm-actions { display: flex; justify-content: flex-end; gap: var(--sp-2); flex-shrink: 0; } .confirm-actions { display: flex; justify-content: flex-end; gap: var(--sp-2); flex-shrink: 0; margin-top: auto; }
.back-btn { font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 6px 10px; border-radius: var(--radius-md); background: none; color: var(--text-muted); border: 1px solid var(--border-dim); cursor: pointer; flex-shrink: 0; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); } .back-btn { font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 6px 10px; border-radius: var(--radius-md); background: none; color: var(--text-muted); border: 1px solid var(--border-dim); cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
.back-btn:hover:not(:disabled) { color: var(--text-secondary); border-color: var(--border-strong); background: var(--bg-raised); } .back-btn:hover:not(:disabled) { color: var(--text-secondary); border-color: var(--border-strong); background: var(--bg-raised); }
.back-btn:disabled { opacity: 0.4; cursor: default; } .back-btn:disabled { opacity: 0.4; cursor: default; }
.migrate-btn { display: flex; align-items: center; gap: var(--sp-2); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 7px 16px; border-radius: var(--radius-md); background: var(--accent-dim); border: 1px solid var(--accent); color: var(--accent-fg); cursor: pointer; transition: background var(--t-base), border-color var(--t-base); } .migrate-btn { display: flex; align-items: center; gap: var(--sp-2); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 7px 16px; border-radius: var(--radius-md); background: var(--accent-dim); border: 1px solid var(--accent); color: var(--accent-fg); cursor: pointer; transition: background var(--t-base), border-color var(--t-base); }
.migrate-btn:hover:not(:disabled) { background: var(--accent-muted); border-color: var(--accent-bright); } .migrate-btn:hover:not(:disabled) { background: var(--accent-muted); border-color: var(--accent-bright); }
.migrate-btn:disabled { opacity: 0.5; cursor: default; } .migrate-btn:disabled { opacity: 0.5; cursor: default; }
.error { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--text-xs); color: var(--color-error); padding: var(--sp-2) var(--sp-3); background: rgba(180,60,60,0.08); border-radius: var(--radius-md); border: 1px solid rgba(180,60,60,0.2); } .error { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--text-xs); color: var(--color-error); padding: var(--sp-2) var(--sp-3); background: rgba(180,60,60,0.08); border-radius: var(--radius-md); border: 1px solid rgba(180,60,60,0.2); flex-shrink: 0; }
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } } @keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
</style> </style>
@@ -76,6 +76,20 @@
</div> </div>
</div> </div>
<div class="s-section">
<p class="s-section-title">Window</p>
<div class="s-section-body">
<div class="s-row">
<div class="s-row-info"><span class="s-label">Close button behavior</span><span class="s-desc">What happens when you click the X button</span></div>
<div class="s-seg">
{#each [["ask","Ask"],["tray","Tray"],["quit","Quit"]] as [v, l]}
<button class="s-seg-btn" class:active={( store.settings.closeAction ?? "ask") === v} onclick={() => updateSettings({ closeAction: v as "ask" | "tray" | "quit" })}>{l}</button>
{/each}
</div>
</div>
</div>
</div>
<div class="s-section"> <div class="s-section">
<p class="s-section-title">Integrations</p> <p class="s-section-title">Integrations</p>
<div class="s-section-body"> <div class="s-section-body">
@@ -112,4 +126,11 @@
</div> </div>
</div> </div>
</div> </div>
<style>
.s-seg { display: flex; border: 1px solid var(--border-strong); border-radius: var(--radius-md); overflow: hidden; }
.s-seg-btn { flex: 1; padding: var(--sp-1) var(--sp-3); font-family: var(--font-ui); font-size: var(--text-sm); color: var(--text-faint); background: transparent; cursor: pointer; transition: background var(--t-base), color var(--t-base); border: none; }
.s-seg-btn:not(:last-child) { border-right: 1px solid var(--border-strong); }
.s-seg-btn.active { background: var(--accent-muted); color: var(--accent-fg); }
.s-seg-btn:not(.active):hover { background: var(--bg-raised); color: var(--text-secondary); }
</style>
@@ -69,6 +69,10 @@
<div class="s-row-info"><span class="s-label">Auto-link on open</span><span class="s-desc">When opening a manga, automatically link it to similarly-titled entries and notify you of new matches</span></div> <div class="s-row-info"><span class="s-label">Auto-link on open</span><span class="s-desc">When opening a manga, automatically link it to similarly-titled entries and notify you of new matches</span></div>
<button role="switch" aria-checked={store.settings.autoLinkOnOpen ?? false} aria-label="Auto-link on open" class="s-toggle" class:on={store.settings.autoLinkOnOpen ?? false} onclick={() => updateSettings({ autoLinkOnOpen: !(store.settings.autoLinkOnOpen ?? false) })}><span class="s-toggle-thumb"></span></button> <button role="switch" aria-checked={store.settings.autoLinkOnOpen ?? false} aria-label="Auto-link on open" class="s-toggle" class:on={store.settings.autoLinkOnOpen ?? false} onclick={() => updateSettings({ autoLinkOnOpen: !(store.settings.autoLinkOnOpen ?? false) })}><span class="s-toggle-thumb"></span></button>
</label> </label>
<label class="s-row">
<div class="s-row-info"><span class="s-label">Disable auto-complete</span><span class="s-desc">Don't move manga to the Completed folder when all chapters are read</span></div>
<button role="switch" aria-checked={store.settings.disableAutoComplete} aria-label="Disable auto-complete" class="s-toggle" class:on={store.settings.disableAutoComplete} onclick={() => updateSettings({ disableAutoComplete: !store.settings.disableAutoComplete })}><span class="s-toggle-thumb"></span></button>
</label>
</div> </div>
</div> </div>
+1
View File
@@ -292,6 +292,7 @@ class Store {
gqlFn: (query: string, vars: Record<string, unknown>) => Promise<unknown>, gqlFn: (query: string, vars: Record<string, unknown>) => Promise<unknown>,
UPDATE_MANGA_CATEGORIES: string, UPDATE_MANGA?: string, mangaStatus?: string, UPDATE_MANGA_CATEGORIES: string, UPDATE_MANGA?: string, mangaStatus?: string,
): Promise<void> { ): Promise<void> {
if (this.settings.disableAutoComplete) return;
if (!chaps.length || mangaStatus === "ONGOING") return; if (!chaps.length || mangaStatus === "ONGOING") return;
const completed = categories.find(c => c.name === "Completed"); const completed = categories.find(c => c.name === "Completed");
if (!completed) return; if (!completed) return;
+6
View File
@@ -128,6 +128,9 @@ export interface Settings {
downloadAutoRetry: boolean; downloadAutoRetry: boolean;
hiddenLibraryTabs: string[]; hiddenLibraryTabs: string[];
libraryPinnedTabOrder: string[]; libraryPinnedTabOrder: string[];
autoScroll?: boolean;
autoScrollSpeed?: number;
disableAutoComplete: boolean;
} }
export const DEFAULT_SETTINGS: Settings = { export const DEFAULT_SETTINGS: Settings = {
@@ -171,4 +174,7 @@ export const DEFAULT_SETTINGS: Settings = {
downloadAutoRetry: false, downloadAutoRetry: false,
hiddenLibraryTabs: [], hiddenLibraryTabs: [],
libraryPinnedTabOrder: [], libraryPinnedTabOrder: [],
autoScroll: false,
autoScrollSpeed: 5,
disableAutoComplete: false,
}; };