diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index ad49b1c..79e6315 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -136,6 +136,8 @@ jobs: uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} with: args: --target x86_64-pc-windows-msvc --config src-tauri/tauri.windows.conf.json @@ -145,3 +147,10 @@ jobs: name: moku-windows-x64 path: src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe retention-days: 7 + + - name: Upload latest.json (updater endpoint) + uses: actions/upload-artifact@v4 + with: + name: moku-windows-latest-json + path: src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/latest.json + retention-days: 7 diff --git a/flake.nix b/flake.nix index 9a4a827..2b85116 100644 --- a/flake.nix +++ b/flake.nix @@ -71,7 +71,7 @@ inherit version; src = frontendSrc; fetcherVersion = 1; - hash = "sha256-FsZTHeBS9qQ9KYgiwDX1vam6uJXK8OjLe5U6Jfu33lc="; + hash = "sha256-G82kmXm1prRpU9kqUyFHSABVt1fikMzvz78+w/gFKvQ="; }; buildPhase = "pnpm build"; diff --git a/package.json b/package.json index 9c47d11..85814d6 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-shell": "^2.3.5", "clsx": "^2.1.1", "phosphor-svelte": "^3.1.0", "svelte-spa-router": "^4.0.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7422033..7e9cc5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@tauri-apps/api': specifier: ^2.0.0 version: 2.10.1 + '@tauri-apps/plugin-shell': + specifier: ^2.3.5 + version: 2.3.5 clsx: specifier: ^2.1.1 version: 2.1.1 @@ -433,6 +436,9 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-shell@2.3.5': + resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1026,6 +1032,10 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.10.1 '@tauri-apps/cli-win32-x64-msvc': 2.10.1 + '@tauri-apps/plugin-shell@2.3.5': + dependencies: + '@tauri-apps/api': 2.10.1 + '@types/estree@1.0.8': {} '@types/pug@2.0.10': {} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8e8f5d7..547ddf9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -47,6 +47,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "atk" version = "0.18.2" @@ -285,6 +294,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.43" @@ -319,10 +334,57 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ + "percent-encoding", "time", "version_check", ] +[[package]] +name = "cookie_store" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -346,7 +408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", "foreign-types", "libc", @@ -359,7 +421,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -497,6 +559,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + [[package]] name = "deranged" version = "0.5.6" @@ -507,6 +575,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -622,6 +701,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dpi" version = "0.1.2" @@ -720,6 +808,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fdeflate" version = "0.3.7" @@ -739,6 +833,17 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1020,8 +1125,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1031,9 +1138,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1197,6 +1306,25 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1297,6 +1425,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1308,6 +1437,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1326,9 +1472,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1712,14 +1860,27 @@ checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.11.0", "libc", + "redox_syscall 0.7.3", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -1735,6 +1896,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mac" version = "0.1.1" @@ -1793,6 +1960,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1824,7 +1997,10 @@ dependencies = [ "sysinfo", "tauri", "tauri-build", + "tauri-plugin-http", + "tauri-plugin-process", "tauri-plugin-shell", + "tauri-plugin-updater", "walkdir", ] @@ -2098,6 +2274,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + [[package]] name = "objc2-quartz-core" version = "0.3.2" @@ -2167,6 +2355,12 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -2183,6 +2377,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "pango" version = "0.18.3" @@ -2226,7 +2434,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] @@ -2529,6 +2737,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -2538,6 +2762,61 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.44" @@ -2578,6 +2857,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -2598,6 +2887,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -2616,6 +2915,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -2669,6 +2977,15 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -2740,6 +3057,49 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "cookie", + "cookie_store 0.22.1", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + [[package]] name = "reqwest" version = "0.13.2" @@ -2754,15 +3114,20 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -2774,6 +3139,26 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2783,12 +3168,105 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -2798,6 +3276,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -2855,6 +3342,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.24.0" @@ -2978,6 +3488,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.16.1" @@ -3155,7 +3677,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.18", "tracing", "wasm-bindgen", "web-sys", @@ -3225,6 +3747,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3292,6 +3820,27 @@ dependencies = [ "windows 0.57.0", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -3313,7 +3862,7 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" dependencies = [ "bitflags 2.11.0", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dispatch", @@ -3356,6 +3905,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -3392,7 +3952,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "serde_repr", @@ -3493,6 +4053,62 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-fs" +version = "2.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-http" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f069451c4e87e7e2636b7f065a4c52866c4ce5e60e2d53fa1038edb6d184dc" +dependencies = [ + "bytes", + "cookie_store 0.21.1", + "data-url", + "http", + "regex", + "reqwest 0.12.28", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "tokio", + "url", + "urlpattern", +] + +[[package]] +name = "tauri-plugin-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" +dependencies = [ + "tauri", + "tauri-plugin", +] + [[package]] name = "tauri-plugin-shell" version = "2.3.5" @@ -3514,6 +4130,39 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-updater" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fe8e9bebd88fc222938ffdfbdcfa0307081423bd01e3252fc337d8bde81fc61" +dependencies = [ + "base64 0.22.1", + "dirs 6.0.0", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest 0.13.2", + "rustls", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.18", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + [[package]] name = "tauri-runtime" version = "2.10.0" @@ -3615,6 +4264,19 @@ dependencies = [ "toml 0.9.12+spec-1.1.0", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "tendril" version = "0.4.3" @@ -3707,6 +4369,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.49.0" @@ -3718,9 +4395,31 @@ dependencies = [ "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -3993,6 +4692,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -4239,6 +4944,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webkit2gtk" version = "2.0.2" @@ -4283,6 +4998,24 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" @@ -4512,6 +5245,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -4575,6 +5319,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -5051,6 +5804,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yoke" version = "0.8.1" @@ -5115,6 +5878,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" @@ -5148,6 +5917,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.13.0", + "memchr", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d7814aa..4763678 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,7 +16,10 @@ tauri-build = { version = "2.0", features = [] } [dependencies] tauri = { version = "2.0", features = [] } -tauri-plugin-shell = "2" +tauri-plugin-shell = "2" +tauri-plugin-updater = "2" +tauri-plugin-process = "2" +tauri-plugin-http = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" walkdir = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 35e3e04..02505fc 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -25,6 +25,13 @@ "core:window:allow-outer-size", "core:window:allow-inner-position", "core:window:allow-outer-position", - "core:window:allow-scale-factor" + "core:window:allow-scale-factor", + "updater:default", + "updater:allow-check", + "updater:allow-download-and-install", + "process:default", + "process:allow-restart", + "http:default", + "http:allow-fetch" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8560203..5c08c61 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -24,6 +24,26 @@ pub enum SpawnError { SpawnFailed(String), } +// ── Update types ────────────────────────────────────────────────────────────── + +/// A single GitHub release returned to the frontend. +#[derive(Serialize, Clone)] +pub struct ReleaseInfo { + pub tag_name: String, + pub name: String, + pub body: String, + pub published_at: String, + pub html_url: String, +} + +/// Progress event emitted during download — matches what the frontend listens for. +#[derive(Clone, serde::Serialize)] +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] +struct UpdateProgress { + downloaded: u64, + total: Option, +} + /// Strip the \\?\ extended-length path prefix that Windows adds to long paths. /// Java and many other tools do not accept this prefix and will fail silently. fn strip_unc(path: PathBuf) -> PathBuf { @@ -415,16 +435,113 @@ fn kill_server(app: tauri::AppHandle) -> Result<(), String> { Ok(()) } +// ── Update commands ─────────────────────────────────────────────────────────── + +/// Fetch the list of all GitHub releases so the frontend can show a version picker. +/// Uses tauri-plugin-http so it goes through Tauri's permission system. +#[tauri::command] +async fn list_releases() -> Result, String> { + use tauri_plugin_http::reqwest; + + let client = reqwest::Client::builder() + .user_agent("Moku") + .build() + .map_err(|e| e.to_string())?; + + let resp = client + .get("https://api.github.com/repos/Youwes09/Moku/releases?per_page=30") + .send() + .await + .map_err(|e| e.to_string())?; + + if !resp.status().is_success() { + return Err(format!("GitHub API returned {}", resp.status())); + } + + #[derive(serde::Deserialize)] + struct GhRelease { + tag_name: String, + name: Option, + body: Option, + published_at: Option, + html_url: String, + } + + let body = resp.text().await.map_err(|e| e.to_string())?; + let releases: Vec = serde_json::from_str(&body).map_err(|e| e.to_string())?; + + Ok(releases + .into_iter() + .map(|r| ReleaseInfo { + tag_name: r.tag_name.clone(), + name: r.name.unwrap_or_else(|| r.tag_name.clone()), + body: r.body.unwrap_or_default(), + published_at: r.published_at.unwrap_or_default(), + html_url: r.html_url, + }) + .collect()) +} + +/// Download and install the latest update using tauri-plugin-updater. +/// Emits `update-progress` events with `{ downloaded, total }` while downloading. +/// On Windows the installer runs in passive (silent) mode; the frontend prompts restart. +/// On other platforms this command is a no-op — the frontend opens the GitHub page instead. +#[tauri::command] +#[allow(unused_variables)] +async fn download_and_install_update(app: tauri::AppHandle) -> Result<(), String> { + #[cfg(not(target_os = "windows"))] + return Err("Native install is Windows-only; open the GitHub release page instead.".into()); + + #[cfg(target_os = "windows")] + { + use tauri_plugin_updater::UpdaterExt; + + let updater = app.updater().map_err(|e| e.to_string())?; + let update = updater.check().await.map_err(|e| e.to_string())?; + + let Some(update) = update else { + return Err("No update available from the updater endpoint.".into()); + }; + + let app_clone = app.clone(); + update + .download_and_install( + move |downloaded, total| { + let _ = app_clone.emit("update-progress", UpdateProgress { downloaded, total }); + }, + || {}, + ) + .await + .map_err(|e| e.to_string())?; + + Ok(()) + } +} + +/// Restart the app after a successful update install. +#[tauri::command] +fn restart_app(app: tauri::AppHandle) { + tauri::process::restart(&app.env()); +} + +// ── App entry point ─────────────────────────────────────────────────────────── + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_http::init()) + .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_updater::Builder::new().build()) .manage(ServerState(Mutex::new(None))) .invoke_handler(tauri::generate_handler![ get_storage_info, spawn_server, kill_server, get_platform_ui_scale, + list_releases, + download_and_install_update, + restart_app, ]) .setup(|_app| Ok(())) .on_window_event(|window, event| { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0ab044b..4322fe4 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -49,6 +49,12 @@ "plugins": { "shell": { "open": true + }, + "updater": { + "pubkey": "RWR87cX5fjRNNiuoLs057dMl5bKeHjP3yrbTvGixYDTchASwR8Bsp1Wt", + "endpoints": [ + "https://github.com/Youwes09/Moku/releases/download/v__VERSION__/latest.json" + ] } } } diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index e7295c9..172959c 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -1,5 +1,6 @@ { "bundle": { + "createUpdaterArtifacts": true, "resources": [ "binaries/suwayomi-bundle/bin/Suwayomi-Server.jar", "binaries/suwayomi-bundle/jre/**/*" diff --git a/src/App.svelte b/src/App.svelte index c913636..249a7b4 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -2,9 +2,10 @@ import { onMount } from "svelte"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; + import { getVersion } from "@tauri-apps/api/app"; import { gql } from "./lib/client"; import { GET_DOWNLOAD_STATUS } from "./lib/queries"; - import { store, addToast, setActiveDownloads } from "./store/state.svelte"; + import { store, addToast, setActiveDownloads, setSettingsOpen } from "./store/state.svelte"; import type { DownloadStatus, DownloadQueueItem } from "./lib/types"; import Layout from "./components/layout/Layout.svelte"; import Reader from "./components/reader/Reader.svelte"; @@ -92,6 +93,33 @@ return () => clearInterval(pollInterval); }); + // ── Auto-update check (runs once after app is ready) ───────────────────────── + // + // Fetches the GitHub releases list via the Rust command and compares the latest + // tag against the installed version. On mismatch, shows a single non-blocking + // info toast. No modal, no blocking UI. + async function checkForUpdateSilently() { + try { + const [currentVersion, releases] = await Promise.all([ + getVersion(), + invoke>("list_releases"), + ]); + if (!releases.length) return; + + const latestTag = releases[0].tag_name.replace(/^v/, ""); + if (latestTag !== currentVersion) { + addToast({ + kind: "info", + title: `Update available — v${latestTag}`, + body: "Open Settings → About to install.", + duration: 8000, + }); + } + } catch { + // Silently ignore — no network, private repo rate-limit, etc. + } + } + onMount(async () => { document.addEventListener("contextmenu", e => e.preventDefault()); (window as any).__mokuShowSplash = () => devSplash = true; @@ -142,6 +170,15 @@ }; }); + // Run the update check once, 5 seconds after the app finishes loading. + // The delay avoids adding to startup latency and ensures list_releases + // doesn't compete with the server probe. + $effect(() => { + if (!appReady) return; + const timer = setTimeout(checkForUpdateSilently, 5_000); + return () => clearTimeout(timer); + }); + function handleRetry() { failed = false; notConfigured = false; serverProbeOk = false; } diff --git a/src/components/pages/Discover.svelte b/src/components/pages/Discover.svelte index b39ba28..78a2040 100644 --- a/src/components/pages/Discover.svelte +++ b/src/components/pages/Discover.svelte @@ -5,18 +5,18 @@ import { GET_SOURCES, FETCH_SOURCE_MANGA, UPDATE_MANGA } from "../../lib/queries"; import { cache, CACHE_KEYS } from "../../lib/cache"; import { dedupeSources, dedupeMangaByTitle, dedupeMangaById } from "../../lib/util"; - import { store, addFolder, assignMangaToFolder, setPreviewManga } from "../../store/state.svelte"; + import { store, addFolder, assignMangaToFolder, setPreviewManga, clearDiscoverCache } from "../../store/state.svelte"; import type { Manga, Source } from "../../lib/types"; import ContextMenu from "../shared/ContextMenu.svelte"; import type { MenuEntry } from "../shared/ContextMenu.svelte"; import SourceBrowse from "../shared/SourceBrowse.svelte"; // ── Constants ───────────────────────────────────────────────────────────── - const GENRE_TABS = ["All", "Action", "Romance", "Fantasy", "Comedy", "Drama", "Horror", "Sci-Fi", "Adventure", "Thriller"]; - const GRID_LIMIT = 100; - const LOCAL_THRESHOLD = 20; - const CONCURRENCY = 4; - const BATCH_MS = 400; + const GENRE_TABS = ["All", "Action", "Romance", "Fantasy", "Comedy", "Drama", "Horror", "Sci-Fi", "Adventure", "Thriller"]; + const GRID_LIMIT = 200; + const CONCURRENCY = 6; + const PAGES_INIT = 3; // pages per source on All tab + const PAGES_GENRE = 2; // pages per source on genre tabs const EXPLORE_ALL_MANGA = ` query ExploreAllManga { @@ -33,28 +33,20 @@ } `; - // ── Dedicated discover cache ─────────────────────────────────────────────── - // Completely isolated from main app cache — refresh only wipes this, - // leaving library/chapter/source caches untouched. - const discoverStore = new Map(); - function dKey(srcId: string, type: string, tag: string) { return `${srcId}|${type}|${tag}`; } - function clearDiscover() { discoverStore.clear(); } + function dKey(srcId: string, type: string, genre: string, page: number) { + return `${srcId}|${type}|${genre}:p${page}`; + } - // ── State ───────────────────────────────────────────────────────────────── - let allManga: Manga[] = $state([]); - let allSources: Source[] = $state([]); - let libraryIds: Set = $state(new Set()); - let loadingLib = $state(true); - let loadError = $state(false); - let currentGenre = $state("All"); - let genreResults = $state(new Map()); - let genreLoading = $state(false); - let srcOffset = $state(0); + // ── Local component state ───────────────────────────────────────────────── + let allSources: Source[] = $state([]); + let loadingLib = $state(true); + let loadError = $state(false); + let currentGenre = $state("All"); + let genreResults = $state(new Map()); + let genreLoading = $state(false); + let refreshing = $state(false); let activeCtrl: AbortController | null = null; - let batchTimer: ReturnType | null = null; - let batchAccum = new Map(); - let ctx: { x: number; y: number; manga: Manga } | null = $state(null); const isLoading = $derived(genreLoading || refreshing || (currentGenre === "All" && loadingLib)); @@ -65,15 +57,15 @@ return dedupeMangaByTitle(dedupeMangaById(items), store.settings.mangaLinks); } - function filterSource(mangas: Manga[]): Manga[] { - return dedup(mangas.filter(m => !m.inLibrary && !libraryIds.has(m.id))); + function filterOut(mangas: Manga[]): Manga[] { + return dedup(mangas.filter(m => !m.inLibrary && !store.discoverLibraryIds.has(m.id))); } function rotatedSources(): Source[] { const lang = store.settings.preferredExtensionLang || "en"; const srcs = dedupeSources(allSources.filter(s => s.id !== "0"), lang); if (!srcs.length) return []; - const off = srcOffset % srcs.length; + const off = store.discoverSrcOffset % srcs.length; return [...srcs.slice(off), ...srcs.slice(0, off)]; } @@ -88,80 +80,64 @@ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, items.length) }, worker)); } - // ── Batch flush ─────────────────────────────────────────────────────────── - function startBatch() { - if (batchTimer) return; - batchTimer = setInterval(() => { - if (!batchAccum.size) return; - for (const [genre, incoming] of batchAccum) { - const cur = genreResults.get(genre) ?? []; - genreResults.set(genre, dedup([...cur, ...incoming]).slice(0, GRID_LIMIT)); - } - batchAccum.clear(); - genreResults = new Map(genreResults); - }, BATCH_MS); - } - - function flushBatch() { - if (batchTimer) { clearInterval(batchTimer); batchTimer = null; } - if (!batchAccum.size) return; - for (const [genre, incoming] of batchAccum) { - const cur = genreResults.get(genre) ?? []; - genreResults.set(genre, dedup([...cur, ...incoming]).slice(0, GRID_LIMIT)); - } - batchAccum.clear(); - genreResults = new Map(genreResults); - } - - function accumulate(genre: string, mangas: Manga[]) { - const filtered = filterSource(mangas); + // Push results into the reactive grid immediately — no batch delay. + function pushToGrid(genre: string, incoming: Manga[]) { + const filtered = filterOut(incoming); if (!filtered.length) return; - const existing = batchAccum.get(genre) ?? []; - batchAccum.set(genre, [...existing, ...filtered]); + const cur = genreResults.get(genre) ?? []; + genreResults.set(genre, dedup([...cur, ...filtered]).slice(0, GRID_LIMIT)); + genreResults = new Map(genreResults); } // ── Source fan-out ──────────────────────────────────────────────────────── async function fanOut(genre: string, ctrl: AbortController) { - const srcs = rotatedSources(); + const srcs = rotatedSources(); if (!srcs.length) return; - const isAll = genre === "All"; - const type = isAll ? "POPULAR" : "SEARCH"; - const query = isAll ? null : genre; - - startBatch(); + const isAll = genre === "All"; + const type = isAll ? "POPULAR" : "SEARCH"; + const query = isAll ? null : genre; + const maxPages = isAll ? PAGES_INIT : PAGES_GENRE; await runConcurrent(srcs, async src => { - if (ctrl.signal.aborted) return; - const key = dKey(src.id, type, genre); + for (let page = 1; page <= maxPages; page++) { + if (ctrl.signal.aborted) return; - let mangas: Manga[]; - if (discoverStore.has(key)) { - mangas = discoverStore.get(key)!; - } else { - const result = await gql<{ fetchSourceManga: { mangas: Manga[] } }>( - FETCH_SOURCE_MANGA, - { source: src.id, type, page: 1, query }, - ctrl.signal - ).then(d => d.fetchSourceManga).catch(() => null); - if (!result || ctrl.signal.aborted) return; - mangas = result.mangas; - discoverStore.set(key, mangas); - } + const key = dKey(src.id, type, genre, page); + let mangas: Manga[]; + let hasNextPage = false; - if (ctrl.signal.aborted) return; + if (store.discoverCache.has(key)) { + // Cache hit — no network call needed + mangas = store.discoverCache.get(key)!; + } else { + const result = await gql<{ fetchSourceManga: { mangas: Manga[]; hasNextPage: boolean } }>( + FETCH_SOURCE_MANGA, + { source: src.id, type, page, query }, + ctrl.signal + ).then(d => d.fetchSourceManga).catch(() => null); - if (isAll) { - accumulate("All", mangas); - } else { - const matching = mangas.filter(m => - (m.genre ?? []).some(g => g.toLowerCase() === genre.toLowerCase()) - ); - accumulate(genre, matching.length ? matching : mangas); + if (!result || ctrl.signal.aborted) return; + mangas = result.mangas; + hasNextPage = result.hasNextPage; + store.discoverCache.set(key, mangas); + } + + if (ctrl.signal.aborted) return; + + if (isAll) { + pushToGrid("All", mangas); + } else { + const matching = mangas.filter(m => + (m.genre ?? []).some(g => g.toLowerCase() === genre.toLowerCase()) + ); + pushToGrid(genre, matching.length ? matching : mangas); + } + + // Stop paging early if source is exhausted + if (!hasNextPage) return; } }, ctrl.signal); - - if (!ctrl.signal.aborted) flushBatch(); } // ── Tab switch ──────────────────────────────────────────────────────────── @@ -169,13 +145,18 @@ if (currentGenre === genre) return; activeCtrl?.abort(); - flushBatch(); currentGenre = genre; const ctrl = new AbortController(); activeCtrl = ctrl; if (genre === "All") { + // Already have results from this session — show instantly, re-fan in background + if ((genreResults.get("All") ?? []).length > 0) { + genreLoading = false; + fanOut("All", ctrl).catch(() => {}); + return; + } genreResults.set("All", []); genreResults = new Map(genreResults); genreLoading = true; @@ -184,18 +165,15 @@ return; } - // Genre tab: check local cache first, always fan out to sources too + // Genre tab: serve cached local results instantly, always fan out too const localKey = `local|${genre}`; - if (discoverStore.has(localKey)) { - // Serve cached local results immediately - genreResults.set(genre, discoverStore.get(localKey)!); + if (store.discoverCache.has(localKey)) { + genreResults.set(genre, store.discoverCache.get(localKey)!); genreResults = new Map(genreResults); - // Always fan out in background to get source results too fanOut(genre, ctrl).catch(() => {}); return; } - // Fetch local library results then fan out genreLoading = true; try { const d = await gql<{ mangas: { nodes: Manga[] } }>( @@ -204,12 +182,11 @@ if (ctrl.signal.aborted) return; const local = dedup(d.mangas.nodes); - discoverStore.set(localKey, local); + store.discoverCache.set(localKey, local); genreResults.set(genre, local.slice(0, GRID_LIMIT)); genreResults = new Map(genreResults); genreLoading = false; - // Always fan out — show source results alongside library results fanOut(genre, ctrl).catch(() => {}); } catch (e: any) { if (e?.name !== "AbortError") console.error(e); @@ -218,13 +195,9 @@ } // ── Refresh ─────────────────────────────────────────────────────────────── - let refreshing = $state(false); - async function refresh() { activeCtrl?.abort(); - flushBatch(); - clearDiscover(); - srcOffset++; + clearDiscoverCache(); // wipes store.discoverCache + bumps discoverSrcOffset genreResults = new Map(); refreshing = true; genreLoading = true; @@ -240,23 +213,29 @@ loadingLib = true; loadError = false; - // Load library for filtering — don't show stuff already in library + // Already have a session grid — show it immediately + if ((genreResults.get("All") ?? []).length > 0) { + loadingLib = false; + } + + // Refresh library ID set so newly-added manga get filtered out cache.get(CACHE_KEYS.DISCOVER, () => gql<{ mangas: { nodes: Manga[] } }>(EXPLORE_ALL_MANGA).then(d => d.mangas.nodes) ).then(m => { - allManga = dedupeMangaById(m); - libraryIds = new Set(allManga.filter(x => x.inLibrary).map(x => x.id)); + store.discoverLibraryIds = new Set( + dedupeMangaById(m).filter(x => x.inLibrary).map(x => x.id) + ); }).catch(e => { console.error(e); loadError = true; }) .finally(() => { loadingLib = false; }); - // Load sources then kick off initial All tab fan-out + // Load sources then kick off All tab fan-out (only if grid is empty) gql<{ sources: { nodes: Source[] } }>(GET_SOURCES) .then(d => { allSources = d.sources.nodes; - // Only trigger if still on All tab - if (currentGenre === "All" || currentGenre === "") { - const ctrl = new AbortController(); - activeCtrl = ctrl; + if ((currentGenre === "All" || currentGenre === "") && + (genreResults.get("All") ?? []).length === 0) { + const ctrl = new AbortController(); + activeCtrl = ctrl; genreLoading = true; fanOut("All", ctrl).then(() => { if (!ctrl.signal.aborted) genreLoading = false; @@ -266,7 +245,7 @@ .catch(console.error); } - onDestroy(() => { activeCtrl?.abort(); flushBatch(); }); + onDestroy(() => { activeCtrl?.abort(); }); loadAll(); @@ -284,7 +263,7 @@ onClick: () => gql(UPDATE_MANGA, { id: m.id, inLibrary: true }) .then(() => { cache.clear(CACHE_KEYS.LIBRARY); - libraryIds = new Set([...libraryIds, m.id]); + store.discoverLibraryIds = new Set([...store.discoverLibraryIds, m.id]); }).catch(console.error), }, ...(store.settings.folders.length > 0 ? [ @@ -332,7 +311,7 @@
- {#if isLoading} + {#if isLoading && visibleGrid.length === 0}
{#each Array(24) as _, i (i)}
diff --git a/src/components/pages/Extensions.svelte b/src/components/pages/Extensions.svelte index 948338b..f671f93 100644 --- a/src/components/pages/Extensions.svelte +++ b/src/components/pages/Extensions.svelte @@ -153,7 +153,7 @@ installError = null} - onkeydown={(e) => e.key === "Enter" && !installing && installExternal()} autofocus /> + onkeydown={(e) => e.key === "Enter" && !installing && installExternal()} use:focusOnMount />
{#if pickerOpen} -
{ if (e.target === e.currentTarget) closePicker(); }}> +