diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index cb59eef..fad0b95 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -88,10 +88,10 @@ jobs: - name: Download Suwayomi (Linux x64) run: | curl -fsSL \ - "https://github.com/Suwayomi/Suwayomi-Server/releases/download/v2.1.1867/Suwayomi-Server-v2.1.1867-linux-x64.tar.gz" \ + "https://github.com/Suwayomi/Suwayomi-Server-preview/releases/download/v2.1.2087/Suwayomi-Server-v2.1.2087-linux-x64.tar.gz" \ -o suwayomi-linux.tar.gz - echo "b2344bd73c4e26bede63cdb4b44b1b4168d8a8500b3b2b1a0219519a3ef708fe suwayomi-linux.tar.gz" | sha256sum -c - + echo "888bee202649ce7e3e3468a729c4084fb465f024b4033cab3f8ab98b0c66fe76 suwayomi-linux.tar.gz" | sha256sum -c - mkdir -p suwayomi-extracted tar -xzf suwayomi-linux.tar.gz -C suwayomi-extracted --strip-components=1 diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index af6d830..f9db446 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -79,7 +79,7 @@ jobs: download_suwayomi() { local asset="$1" sha="$2" outdir="$3" curl -fsSL \ - "https://github.com/Suwayomi/Suwayomi-Server/releases/download/v2.1.1867/${asset}" \ + "https://github.com/Suwayomi/Suwayomi-Server-preview/releases/download/v2.1.2087/${asset}" \ -o "${outdir}.tar.gz" echo "${sha} ${outdir}.tar.gz" | shasum -a 256 -c - mkdir -p "${outdir}" @@ -87,13 +87,13 @@ jobs: } download_suwayomi \ - "Suwayomi-Server-v2.1.1867-macOS-arm64.tar.gz" \ - "c80abdbba29f7895e9556c6c9481368557d5f930b5f69bcb30639ba498925f3c" \ + "Suwayomi-Server-v2.1.2087-macOS-arm64.tar.gz" \ + "59f73a53a139d5d843e16cab4f3ac425a410add6bee0a60920fa26eb0a4b8a5c" \ "suwayomi-arm64" download_suwayomi \ - "Suwayomi-Server-v2.1.1867-macOS-x64.tar.gz" \ - "c7590aeb645dd7135a05b9f3ea1fee384a4abeb465c0b3638d5b738d20dfe174" \ + "Suwayomi-Server-v2.1.2087-macOS-x64.tar.gz" \ + "da7e664e4c2615a0b9eac09ee38fe979feee1d6c0b266e19dba1ceea8ae3795c" \ "suwayomi-x64" - name: Stage Suwayomi sidecars diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index e7ffa9e..c592761 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -79,9 +79,9 @@ jobs: shell: bash run: | curl -fsSL \ - "https://github.com/Suwayomi/Suwayomi-Server/releases/download/v2.1.1867/Suwayomi-Server-v2.1.1867-windows-x64.zip" \ + "https://github.com/Suwayomi/Suwayomi-Server-preview/releases/download/v2.1.2087/Suwayomi-Server-v2.1.2087-windows-x64.zip" \ -o suwayomi-windows.zip - echo "ab6687d278e0dd0984f67abbc853511a7e764f84b126a35d09bfd9b0307321ff suwayomi-windows.zip" | sha256sum -c - + echo "65c3ec544190bc4e52f8ba05b49c87448421d9825aaaeb902cb4e34e69ff7207 suwayomi-windows.zip" | sha256sum -c - unzip -q suwayomi-windows.zip -d suwayomi-raw - name: Extract Suwayomi bundle diff --git a/PKGBUILD b/PKGBUILD index d130b6e..c99c64d 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -64,7 +64,7 @@ EOF install -Dm755 /dev/stdin "$pkgdir/usr/bin/moku-suwayomi" << 'EOF' #!/bin/sh -DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/moku/tachidesk" +DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/Tachidesk" mkdir -p "$DATA_DIR" if [ ! -f "$DATA_DIR/server.conf" ]; then @@ -107,4 +107,4 @@ EOF "$pkgdir/usr/share/metainfo/io.github.moku_project.Moku.metainfo.xml" install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" -} \ No newline at end of file +} diff --git a/Todo b/Todo index f124eae..109105c 100644 --- a/Todo +++ b/Todo @@ -32,11 +32,20 @@ In-Progress: - Tracking - Fix SeriesDetail Tracking Window (Maybe Link to TrackingPanel) - - Integrate Tauri JSON for Settings - - Create Migration Logic for Local Storage + - Apply Syer's Fix for Library on Backup Load (Manga Metadata) + - Note User's have to always install extensions manually + - Create "Missing Source" for Manga + + - Wire Series-Detail Refresh to Fix Manga-Metadata (MAJOR) Notes from last time: - Storage has been configured, now just need protocols - Export/Import - Migration - - Data-Clean \ No newline at end of file + - Data-Clean + +- MAJOR Migration + - Completed GQL Migration + - Completed Types Migration (May need Refactor) + - Completed Shared Migration + - Completed Features Migration \ No newline at end of file diff --git a/flake.nix b/flake.nix index 5a8c0e8..e860c02 100644 --- a/flake.nix +++ b/flake.nix @@ -2,11 +2,11 @@ description = "Moku — manga reader frontend for Suwayomi"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; - crane.url = "github:ipetkov/crane"; + crane.url = "github:ipetkov/crane"; rust-overlay = { - url = "github:oxalica/rust-overlay"; + url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; }; }; @@ -14,9 +14,13 @@ outputs = inputs@{ flake-parts, crane, rust-overlay, ... }: flake-parts.lib.mkFlake { inherit inputs; } { - systems = [ "x86_64-linux" "aarch64-linux" ]; + systems = [ + "x86_64-linux" + "aarch64-linux" + ]; - perSystem = { system, lib, ... }: + perSystem = + { system, lib, ... }: let version = "0.9.1"; @@ -26,7 +30,10 @@ }; rustToolchain = pkgs.rust-bin.stable.latest.default.override { - extensions = [ "rust-src" "rust-analyzer" ]; + extensions = [ + "rust-src" + "rust-analyzer" + ]; }; craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; @@ -46,10 +53,14 @@ gsettings-desktop-schemas ]; + # ── source filters ────────────────────────────────────────────── + frontendSrc = lib.cleanSourceWith { src = ./.; - filter = path: type: - let base = builtins.baseNameOf path; + filter = + path: type: + let + base = builtins.baseNameOf path; in (lib.hasInfix "/src" path) || base == "index.html" @@ -59,234 +70,47 @@ || base == "vite.config.ts"; }; - frontend = pkgs.stdenv.mkDerivation { - pname = "moku-frontend"; - inherit version; - src = frontendSrc; - - nativeBuildInputs = with pkgs; [ nodejs_22 pnpm pnpmConfigHook ]; - - pnpmDeps = pkgs.fetchPnpmDeps { - pname = "moku-frontend"; - inherit version; - src = frontendSrc; - fetcherVersion = 1; - hash = "sha256-t6Gj84hCE3CuDAJfbdXi0FuqgPCqlkMmAzETcKL4e3U="; - }; - - buildPhase = "pnpm build"; - installPhase = "cp -r dist $out"; - }; - cargoSrc = lib.cleanSourceWith { - src = ./src-tauri; - filter = path: type: + src = ./src-tauri; + filter = + path: type: (craneLib.filterCargoSources path type) - || (lib.hasInfix "/icons/" path) + || (lib.hasInfix "/icons/" path) || (lib.hasInfix "/capabilities/" path) || (builtins.baseNameOf path == "tauri.conf.json"); }; - commonArgs = { - src = cargoSrc; - cargoToml = ./src-tauri/Cargo.toml; - cargoLock = ./src-tauri/Cargo.lock; - strictDeps = true; - buildInputs = runtimeLibs; - nativeBuildInputs = with pkgs; [ pkg-config wrapGAppsHook3 ]; - preBuild = '' - cp -r ${frontend} ../dist - ''; + # ── packages ──────────────────────────────────────────────────── + + suwayomiServer = pkgs.callPackage ./nix/server.nix { }; + + frontend = pkgs.callPackage ./nix/frontend.nix { + inherit version; + src = frontendSrc; }; - cargoArtifacts = craneLib.buildDepsOnly commonArgs; - - moku = craneLib.buildPackage (commonArgs // { - inherit cargoArtifacts; - meta.mainProgram = "moku"; - postInstall = '' - mkdir -p "$out/share/applications" - cat > "$out/share/applications/moku.desktop" << EOF -[Desktop Entry] -Version=1.0 -Type=Application -Name=Moku -Comment=Manga reader frontend for Suwayomi -Exec=$out/bin/moku -Icon=moku -Terminal=false -Categories=Graphics;Viewer; -Keywords=manga;comic;reader;suwayomi; -StartupWMClass=moku -EOF - - for size in 32x32 128x128 256x256 512x512; do - src="icons/$size.png" - [ -f "$src" ] && install -Dm644 "$src" \ - "$out/share/icons/hicolor/$size/apps/moku.png" - done - - for size in 128x128 256x256; do - src="icons/''${size}@2x.png" - [ -f "$src" ] && install -Dm644 "$src" \ - "$out/share/icons/hicolor/''${size}@2/apps/moku.png" - done - - install -Dm644 "${./src/assets/moku-icon.svg}" \ - "$out/share/icons/hicolor/scalable/apps/moku.svg" - - wrapProgram $out/bin/moku \ - --prefix XDG_DATA_DIRS : "${lib.makeSearchPath "share/gsettings-schemas" [ - pkgs.gsettings-desktop-schemas - pkgs.gtk3 - ]}" \ - --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath runtimeLibs}" \ - --prefix PATH : "${lib.makeBinPath [ pkgs.suwayomi-server ]}" \ - --set GDK_BACKEND wayland \ - --set WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS 1 - ''; - }); - - bumpScript = pkgs.writeShellApplication { - name = "moku-bump"; - runtimeInputs = with pkgs; [ - gnused coreutils git rustToolchain - nodejs_22 pnpm - (python3.withPackages (ps: [ ps.aiohttp ps.tomlkit ])) - ]; - text = '' - [[ $# -lt 1 ]] && { echo "Usage: nix run .#bump -- "; exit 1; } - VERSION="$1" - REPO="$(git rev-parse --show-toplevel)" - - echo "── Bumping version fields to $VERSION ──" - sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" \ - "$REPO/src-tauri/tauri.conf.json" - sed -i "0,/^version = \"[^\"]*\"/s//version = \"$VERSION\"/" \ - "$REPO/src-tauri/Cargo.toml" - sed -i "s/version = \"[^\"]*\";/version = \"$VERSION\";/g" \ - "$REPO/flake.nix" - sed -i "s/^pkgver=.*/pkgver=$VERSION/" "$REPO/PKGBUILD" - sed -i "s/^pkgrel=.*/pkgrel=1/" "$REPO/PKGBUILD" - echo "Done" - - echo "── Regenerating Cargo.lock ──" - (cd "$REPO/src-tauri" && cargo generate-lockfile) - echo "Done" - - echo "── Building frontend ──" - cd "$REPO" - pnpm install --frozen-lockfile - pnpm build - echo "Done" - - echo "── Repacking frontend-dist.tar.gz ──" - tar -czf "$REPO/packaging/frontend-dist.tar.gz" -C "$REPO" dist - FRONTEND_SHA=$(sha256sum "$REPO/packaging/frontend-dist.tar.gz" | awk '{print $1}') - echo "sha256: $FRONTEND_SHA" - - echo "── Regenerating cargo-sources.json ──" - python3 "$REPO/packaging/flatpak-cargo-generator.py" \ - "$REPO/src-tauri/Cargo.lock" \ - -o "$REPO/packaging/cargo-sources.json" - echo "Done" - - echo "── Patching flatpak manifest ──" - MANIFEST="$REPO/io.github.moku_project.Moku.yml" - sed -i "s/tag: v[^[:space:]]*/tag: v$VERSION/" "$MANIFEST" - python3 - "$MANIFEST" "$FRONTEND_SHA" <<'PYEOF' - import re, sys - path, sha = sys.argv[1], sys.argv[2] - text = open(path).read() - updated, n = re.subn( - r'(path:\s*packaging/frontend-dist\.tar\.gz\s*\n\s*sha256:\s*)[0-9a-f]+', - r'\g<1>' + sha, text) - if n == 0: - sys.exit("ERROR: could not find frontend-dist sha256 in manifest") - open(path, 'w').write(updated) - PYEOF - echo "Done" - - echo "" - echo "Bumped to v$VERSION — commit, tag, push, then: nix run .#post-tag-bump -- $VERSION" - ''; + moku = import ./nix/moku.nix { + inherit lib craneLib pkgs runtimeLibs frontend suwayomiServer version cargoSrc; }; - postTagBumpScript = pkgs.writeShellApplication { - name = "moku-post-tag-bump"; - runtimeInputs = with pkgs; [ gnused coreutils git curl ]; - text = '' - [[ $# -lt 1 ]] && { echo "Usage: nix run .#post-tag-bump -- "; exit 1; } - VERSION="$1" - REPO="$(git rev-parse --show-toplevel)" - MANIFEST="$REPO/io.github.moku_project.Moku.yml" - PKGBUILD="$REPO/PKGBUILD" + # ── dev/release scripts ───────────────────────────────────────── - echo "── Resolving commit for v$VERSION ──" - COMMIT=$(git ls-remote https://github.com/moku-project/Moku.git "refs/tags/v$VERSION" \ - | awk '{print $1}') - [[ -z "$COMMIT" ]] && { echo "ERROR: tag v$VERSION not found on remote"; exit 1; } - echo "commit: $COMMIT" - sed -i "s/commit: [0-9a-f]\{40\}/commit: $COMMIT/" "$MANIFEST" - echo "Done" - - echo "── Fetching PKGBUILD tarball sha256 ──" - TARBALL_URL="https://github.com/moku-project/Moku/archive/refs/tags/v$VERSION.tar.gz" - TARBALL_SHA=$(curl -fsSL "$TARBALL_URL" | sha256sum | awk '{print $1}') - sed -i "/sha256sums=/,/)/{ 0,/'/s/'[^']*'/'$TARBALL_SHA'/ }" "$PKGBUILD" - grep -q "$TARBALL_SHA" "$PKGBUILD" \ - || { echo "ERROR: PKGBUILD sha256 replacement failed"; exit 1; } - echo "Done" - - echo "" - echo "post-tag-bump complete for v$VERSION" - ''; - }; - - flatpakScript = pkgs.writeShellApplication { - name = "moku-flatpak"; - runtimeInputs = with pkgs; [ coreutils git appstream flatpak-builder flatpak ]; - text = '' - [[ $# -lt 1 ]] && { echo "Usage: nix run .#flatpak -- "; exit 1; } - REPO="$(git rev-parse --show-toplevel)" - MANIFEST="$REPO/io.github.moku_project.Moku.yml" - - rm -rf "$REPO/build-dir" "$REPO/repo" - flatpak-builder \ - --repo="$REPO/repo" \ - --force-clean \ - "$REPO/build-dir" \ - "$MANIFEST" - flatpak build-bundle "$REPO/repo" "$REPO/moku.flatpak" io.github.moku_project.Moku - rm -rf "$REPO/build-dir" "$REPO/repo" - - echo "moku.flatpak created" - ''; - }; - - tunnelScript = pkgs.writeShellApplication { - name = "moku-tunnel"; - runtimeInputs = with pkgs; [ cloudflared ]; - text = '' - PORT="''${1:-4567}" - cloudflared tunnel --url "http://localhost:$PORT" - ''; - }; + scripts = import ./nix/scripts.nix { inherit pkgs rustToolchain version; }; in { - apps = { - default = { type = "app"; program = "${moku}/bin/moku"; }; - moku = { type = "app"; program = "${moku}/bin/moku"; }; - bump = { type = "app"; program = "${bumpScript}/bin/moku-bump"; }; - post-tag-bump = { type = "app"; program = "${postTagBumpScript}/bin/moku-post-tag-bump"; }; - flatpak = { type = "app"; program = "${flatpakScript}/bin/moku-flatpak"; }; - tunnel = { type = "app"; program = "${tunnelScript}/bin/moku-tunnel"; }; + packages = { + inherit moku frontend suwayomiServer; + default = moku; }; - packages = { - inherit moku frontend; - default = moku; + apps = { + default = { type = "app"; program = "${moku}/bin/moku"; }; + moku = { type = "app"; program = "${moku}/bin/moku"; }; + bump = { type = "app"; program = "${scripts.bump}/bin/moku-bump"; }; + post-tag-bump = { type = "app"; program = "${scripts.postTagBump}/bin/moku-post-tag-bump"; }; + flatpak = { type = "app"; program = "${scripts.flatpak}/bin/moku-flatpak"; }; + tunnel = { type = "app"; program = "${scripts.tunnel}/bin/moku-tunnel"; }; }; devShells.default = pkgs.mkShell { @@ -297,10 +121,13 @@ EOF wrapGAppsHook3 nodejs_22 pnpm - suwayomi-server + suwayomiServer cloudflared xdg-utils - (python3.withPackages (ps: [ ps.aiohttp ps.tomlkit ])) + (python3.withPackages (ps: [ + ps.aiohttp + ps.tomlkit + ])) ]; shellHook = '' export NO_STRIP=true diff --git a/io.github.moku_project.Moku.yml b/io.github.moku_project.Moku.yml index cc736fc..3222aae 100644 --- a/io.github.moku_project.Moku.yml +++ b/io.github.moku_project.Moku.yml @@ -114,7 +114,7 @@ modules: cat > /app/bin/tachidesk-server << 'EOF' #!/bin/sh - DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/moku/tachidesk" + DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/Tachidesk" mkdir -p "$DATA_DIR" # Seed conf on first run @@ -155,8 +155,8 @@ modules: sources: - type: file - url: https://github.com/Suwayomi/Suwayomi-Server/releases/download/v2.1.1867/suwayomi-server-v2.1.1867.jar - sha256: 51e307c2581e4e1a002991ab3e3a77503c8b074c42695987a984a7382d0ac5af + url: https://github.com/Suwayomi/Suwayomi-Server-preview/releases/download/v2.1.2087/Suwayomi-Server-v2.1.2087.jar + sha256: f589a422674252394c13b289a9c8be691905bf583efb7f4d5f1501ae5e91e6b3 dest-filename: Suwayomi-Server.jar - name: moku diff --git a/nix/frontend.nix b/nix/frontend.nix new file mode 100644 index 0000000..8d7aef8 --- /dev/null +++ b/nix/frontend.nix @@ -0,0 +1,18 @@ +{ lib, stdenv, nodejs_22, pnpm, pnpmConfigHook, version, src }: + +stdenv.mkDerivation { + pname = "moku-frontend"; + inherit version src; + + nativeBuildInputs = [ nodejs_22 pnpm pnpmConfigHook ]; + + pnpmDeps = pnpm.fetchDeps { + pname = "moku-frontend"; + inherit version src; + fetcherVersion = 1; + hash = "sha256-t6Gj84hCE3CuDAJfbdXi0FuqgPCqlkMmAzETcKL4e3U="; + }; + + buildPhase = "pnpm build"; + installPhase = "cp -r dist $out"; +} diff --git a/nix/moku.nix b/nix/moku.nix new file mode 100644 index 0000000..7f14ec2 --- /dev/null +++ b/nix/moku.nix @@ -0,0 +1,73 @@ +{ + lib, + craneLib, + pkgs, + runtimeLibs, + frontend, + suwayomiServer, + version, + cargoSrc, +}: + +let + commonArgs = { + src = cargoSrc; + cargoToml = ./src-tauri/Cargo.toml; + cargoLock = ./src-tauri/Cargo.lock; + strictDeps = true; + buildInputs = runtimeLibs; + nativeBuildInputs = with pkgs; [ pkg-config wrapGAppsHook3 ]; + preBuild = '' + cp -r ${frontend} ../dist + ''; + }; + + cargoArtifacts = craneLib.buildDepsOnly commonArgs; +in +craneLib.buildPackage (commonArgs // { + inherit cargoArtifacts; + + meta.mainProgram = "moku"; + + postInstall = '' + mkdir -p "$out/share/applications" + cat > "$out/share/applications/moku.desktop" << EOF +[Desktop Entry] +Version=1.0 +Type=Application +Name=Moku +Comment=Manga reader frontend for Suwayomi +Exec=$out/bin/moku +Icon=moku +Terminal=false +Categories=Graphics;Viewer; +Keywords=manga;comic;reader;suwayomi; +StartupWMClass=moku +EOF + + for size in 32x32 128x128 256x256 512x512; do + src="icons/$size.png" + [ -f "$src" ] && install -Dm644 "$src" \ + "$out/share/icons/hicolor/$size/apps/moku.png" + done + + for size in 128x128 256x256; do + src="icons/''${size}@2x.png" + [ -f "$src" ] && install -Dm644 "$src" \ + "$out/share/icons/hicolor/''${size}@2/apps/moku.png" + done + + install -Dm644 "${./src/assets/moku-icon.svg}" \ + "$out/share/icons/hicolor/scalable/apps/moku.svg" + + wrapProgram $out/bin/moku \ + --prefix XDG_DATA_DIRS : "${lib.makeSearchPath "share/gsettings-schemas" [ + pkgs.gsettings-desktop-schemas + pkgs.gtk3 + ]}" \ + --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath runtimeLibs}" \ + --prefix PATH : "${lib.makeBinPath [ suwayomiServer ]}" \ + --set GDK_BACKEND wayland \ + --set WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS 1 + ''; +}) diff --git a/nix/scripts.nix b/nix/scripts.nix new file mode 100644 index 0000000..8c406aa --- /dev/null +++ b/nix/scripts.nix @@ -0,0 +1,133 @@ +{ pkgs, rustToolchain, version }: + +{ + bump = pkgs.writeShellApplication { + name = "moku-bump"; + runtimeInputs = with pkgs; [ + gnused + coreutils + git + rustToolchain + nodejs_22 + pnpm + (python3.withPackages (ps: [ ps.aiohttp ps.tomlkit ])) + ]; + text = '' + [[ $# -lt 1 ]] && { echo "Usage: nix run .#bump -- "; exit 1; } + VERSION="$1" + REPO="$(git rev-parse --show-toplevel)" + + echo "── Bumping version fields to $VERSION ──" + sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" \ + "$REPO/src-tauri/tauri.conf.json" + sed -i "0,/^version = \"[^\"]*\"/s//version = \"$VERSION\"/" \ + "$REPO/src-tauri/Cargo.toml" + sed -i "s/version = \"[^\"]*\";/version = \"$VERSION\";/g" \ + "$REPO/flake.nix" + sed -i "s/^pkgver=.*/pkgver=$VERSION/" "$REPO/PKGBUILD" + sed -i "s/^pkgrel=.*/pkgrel=1/" "$REPO/PKGBUILD" + echo "Done" + + echo "── Regenerating Cargo.lock ──" + (cd "$REPO/src-tauri" && cargo generate-lockfile) + echo "Done" + + echo "── Building frontend ──" + cd "$REPO" + pnpm install --frozen-lockfile + pnpm build + echo "Done" + + echo "── Repacking frontend-dist.tar.gz ──" + tar -czf "$REPO/packaging/frontend-dist.tar.gz" -C "$REPO" dist + FRONTEND_SHA=$(sha256sum "$REPO/packaging/frontend-dist.tar.gz" | awk '{print $1}') + echo "sha256: $FRONTEND_SHA" + + echo "── Regenerating cargo-sources.json ──" + python3 "$REPO/packaging/flatpak-cargo-generator.py" \ + "$REPO/src-tauri/Cargo.lock" \ + -o "$REPO/packaging/cargo-sources.json" + echo "Done" + + echo "── Patching flatpak manifest ──" + MANIFEST="$REPO/io.github.moku_project.Moku.yml" + sed -i "s/tag: v[^[:space:]]*/tag: v$VERSION/" "$MANIFEST" + python3 - "$MANIFEST" "$FRONTEND_SHA" <<'PYEOF' + import re, sys + path, sha = sys.argv[1], sys.argv[2] + text = open(path).read() + updated, n = re.subn( + r'(path:\s*packaging/frontend-dist\.tar\.gz\s*\n\s*sha256:\s*)[0-9a-f]+', + r'\g<1>' + sha, text) + if n == 0: + sys.exit("ERROR: could not find frontend-dist sha256 in manifest") + open(path, 'w').write(updated) + PYEOF + echo "Done" + + echo "" + echo "Bumped to v$VERSION — commit, tag, push, then: nix run .#post-tag-bump -- $VERSION" + ''; + }; + + postTagBump = pkgs.writeShellApplication { + name = "moku-post-tag-bump"; + runtimeInputs = with pkgs; [ gnused coreutils git curl ]; + text = '' + [[ $# -lt 1 ]] && { echo "Usage: nix run .#post-tag-bump -- "; exit 1; } + VERSION="$1" + REPO="$(git rev-parse --show-toplevel)" + MANIFEST="$REPO/io.github.moku_project.Moku.yml" + PKGBUILD="$REPO/PKGBUILD" + + echo "── Resolving commit for v$VERSION ──" + COMMIT=$(git ls-remote https://github.com/moku-project/Moku.git "refs/tags/v$VERSION" \ + | awk '{print $1}') + [[ -z "$COMMIT" ]] && { echo "ERROR: tag v$VERSION not found on remote"; exit 1; } + echo "commit: $COMMIT" + sed -i "s/commit: [0-9a-f]\{40\}/commit: $COMMIT/" "$MANIFEST" + echo "Done" + + echo "── Fetching PKGBUILD tarball sha256 ──" + TARBALL_URL="https://github.com/moku-project/Moku/archive/refs/tags/v$VERSION.tar.gz" + TARBALL_SHA=$(curl -fsSL "$TARBALL_URL" | sha256sum | awk '{print $1}') + sed -i "/sha256sums=/,/)/{ 0,/'/s/'[^']*'/'$TARBALL_SHA'/ }" "$PKGBUILD" + grep -q "$TARBALL_SHA" "$PKGBUILD" \ + || { echo "ERROR: PKGBUILD sha256 replacement failed"; exit 1; } + echo "Done" + + echo "" + echo "post-tag-bump complete for v$VERSION" + ''; + }; + + flatpak = pkgs.writeShellApplication { + name = "moku-flatpak"; + runtimeInputs = with pkgs; [ coreutils git appstream flatpak-builder flatpak ]; + text = '' + [[ $# -lt 1 ]] && { echo "Usage: nix run .#flatpak -- "; exit 1; } + REPO="$(git rev-parse --show-toplevel)" + MANIFEST="$REPO/io.github.moku_project.Moku.yml" + + rm -rf "$REPO/build-dir" "$REPO/repo" + flatpak-builder \ + --repo="$REPO/repo" \ + --force-clean \ + "$REPO/build-dir" \ + "$MANIFEST" + flatpak build-bundle "$REPO/repo" "$REPO/moku.flatpak" io.github.moku_project.Moku + rm -rf "$REPO/build-dir" "$REPO/repo" + + echo "moku.flatpak created" + ''; + }; + + tunnel = pkgs.writeShellApplication { + name = "moku-tunnel"; + runtimeInputs = with pkgs; [ cloudflared ]; + text = '' + PORT="''${1:-4567}" + cloudflared tunnel --url "http://localhost:$PORT" + ''; + }; +} diff --git a/nix/server.nix b/nix/server.nix new file mode 100644 index 0000000..3ed8fc2 --- /dev/null +++ b/nix/server.nix @@ -0,0 +1,46 @@ +{ + lib, + stdenvNoCC, + fetchurl, + makeWrapper, + jdk21_headless, +}: +let + jdk = jdk21_headless; +in +stdenvNoCC.mkDerivation (finalAttrs: { + pname = "suwayomi-server"; + version = "2.1.2087"; + + src = fetchurl { + url = "https://github.com/Suwayomi/Suwayomi-Server-preview/releases/download/v${finalAttrs.version}/Suwayomi-Server-v${finalAttrs.version}.jar"; + hash = "sha256-9YmkImdCUjlME7KJqci+aRkFv1g++39NXxUBrl6R5rM="; + }; + + nativeBuildInputs = [ makeWrapper ]; + + dontUnpack = true; + + buildPhase = '' + runHook preBuild + + install -Dm644 $src $out/share/suwayomi-server/suwayomi-server.jar + + makeWrapper ${jdk}/bin/java $out/bin/suwayomi-server \ + --add-flags "-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false" \ + --add-flags "-jar $out/share/suwayomi-server/suwayomi-server.jar" + + runHook postBuild + ''; + + meta = { + description = "Free and open source manga reader server that runs extensions built for Mihon (Tachiyomi)"; + homepage = "https://github.com/Suwayomi/Suwayomi-Server"; + downloadPage = "https://github.com/Suwayomi/Suwayomi-Server-preview/releases"; + changelog = "https://github.com/Suwayomi/Suwayomi-Server-preview/releases/tag/v${finalAttrs.version}"; + license = lib.licenses.mpl20; + platforms = jdk.meta.platforms; + sourceProvenance = [ lib.sourceTypes.binaryBytecode ]; + mainProgram = "suwayomi-server"; + }; +}) diff --git a/src-tauri/binaries/suwayomi-launcher-linux.sh b/src-tauri/binaries/suwayomi-launcher-linux.sh index 5b4fabb..a1baa5b 100644 --- a/src-tauri/binaries/suwayomi-launcher-linux.sh +++ b/src-tauri/binaries/suwayomi-launcher-linux.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Moku — Suwayomi launcher for Linux AppImage/deb. +# — Suwayomi launcher for Linux AppImage/deb. # Tauri resolves this via resolve_server_binary() in lib.rs, which looks for # "suwayomi-launcher" or "suwayomi-launcher.sh" in the resource directory. set -e @@ -53,7 +53,7 @@ if [ ! -f "$JAR" ]; then fi # ── Data directory ───────────────────────────────────────────────────────────── -DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/moku/tachidesk" +DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/Tachidesk" mkdir -p "$DATA_DIR" # ── Seed server.conf on first run ────────────────────────────────────────────── diff --git a/src-tauri/src/server/resolve.rs b/src-tauri/src/server/resolve.rs index 733dae4..81a5dcf 100644 --- a/src-tauri/src/server/resolve.rs +++ b/src-tauri/src/server/resolve.rs @@ -21,20 +21,20 @@ pub fn suwayomi_data_dir() -> PathBuf { { dirs::data_dir() .unwrap_or_else(|| PathBuf::from("C:\\ProgramData")) - .join("moku\\tachidesk") + .join("Tachidesk") } #[cfg(target_os = "macos")] { dirs::data_dir() .unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("~"))) - .join("io.github.moku_project.Moku.app/tachidesk") + .join("Tachidesk") } #[cfg(not(any(target_os = "windows", target_os = "macos")))] { let base = std::env::var("XDG_DATA_HOME") .map(PathBuf::from) .unwrap_or_else(|_| dirs::data_dir().unwrap_or_else(|| PathBuf::from("/tmp"))); - base.join("moku/tachidesk") + base.join("Tachidesk") } } @@ -65,6 +65,14 @@ fn find_java_in_bundle(bundle_dir: &PathBuf, log: &mut Option) -> } } +fn data_root_args() -> Vec { + vec!["--dataRoot".to_string(), suwayomi_data_dir().to_string_lossy().into_owned()] +} + +fn jar_data_root_flag() -> String { + format!("-Dsuwayomi.server.rootDir={}", suwayomi_data_dir().to_string_lossy()) +} + pub fn resolve_server_binary( binary: &str, app: &tauri::AppHandle, @@ -81,7 +89,7 @@ pub fn resolve_server_binary( if path.exists() { return Ok(ServerInvocation { bin: path.to_string_lossy().into_owned(), - args: vec![], + args: data_root_args(), working_dir: path.parent().map(|p| p.to_path_buf()), }); } @@ -99,7 +107,7 @@ pub fn resolve_server_binary( if p.exists() { return Ok(ServerInvocation { bin: p.to_string_lossy().into_owned(), - args: vec![], + args: data_root_args(), working_dir: Some(bin_dir.to_path_buf()), }); } @@ -138,7 +146,7 @@ pub fn resolve_server_binary( do_log(log, "[resolve] using bundled JRE"); return Ok(ServerInvocation { bin: java.to_string_lossy().into_owned(), - args: vec!["-jar".to_string(), jar.to_string_lossy().into_owned()], + args: vec![jar_data_root_flag(), "-jar".to_string(), jar.to_string_lossy().into_owned()], working_dir: Some(bundle_dir), }); } @@ -158,7 +166,7 @@ pub fn resolve_server_binary( if p.exists() { return Ok(ServerInvocation { bin: p.to_string_lossy().into_owned(), - args: vec![], + args: data_root_args(), working_dir: Some(resource_dir.clone()), }); } @@ -182,7 +190,7 @@ pub fn resolve_server_binary( ); return Ok(ServerInvocation { bin: java.to_string_lossy().into_owned(), - args: vec!["-jar".to_string(), jar_path.to_string_lossy().into_owned()], + args: vec![jar_data_root_flag(), "-jar".to_string(), jar_path.to_string_lossy().into_owned()], working_dir: Some(resource_dir), }); } @@ -233,7 +241,7 @@ pub fn resolve_server_binary( do_log(log, &format!("[resolve] found native binary: {:?}", p)); found_binary = Some(ServerInvocation { bin: p.to_string_lossy().into_owned(), - args: vec![], + args: data_root_args(), working_dir: Some(dir.clone()), }); break 'outer; @@ -288,7 +296,7 @@ pub fn resolve_server_binary( let working_dir = jar.parent().map(|p| p.to_path_buf()); return Ok(ServerInvocation { bin: java.to_string_lossy().into_owned(), - args: vec!["-jar".to_string(), jar.to_string_lossy().into_owned()], + args: vec![jar_data_root_flag(), "-jar".to_string(), jar.to_string_lossy().into_owned()], working_dir, }); } @@ -298,21 +306,26 @@ pub fn resolve_server_binary( for name in &["suwayomi-server", "tachidesk-server"] { #[cfg(target_os = "windows")] - let found = std::process::Command::new("where") + let resolved = std::process::Command::new("where") .arg(name) .output() - .map(|o| o.status.success()) - .unwrap_or(false); + .ok() + .filter(|o| o.status.success()) + .and_then(|o| String::from_utf8(o.stdout).ok()) + .and_then(|s| s.lines().next().map(|l| l.trim().to_string())); #[cfg(not(target_os = "windows"))] - let found = std::process::Command::new("which") + let resolved = std::process::Command::new("which") .arg(name) .output() - .map(|o| o.status.success()) - .unwrap_or(false); + .ok() + .filter(|o| o.status.success()) + .and_then(|o| String::from_utf8(o.stdout).ok()) + .and_then(|s| s.lines().next().map(|l| l.trim().to_string())); - if found { + if let Some(bin_path) = resolved { + do_log(log, &format!("[resolve] found on PATH: {:?}", bin_path)); return Ok(ServerInvocation { - bin: name.to_string(), + bin: bin_path, args: vec![], working_dir: None, }); @@ -322,4 +335,4 @@ pub fn resolve_server_binary( Err(SpawnError::NotConfigured( "Server binary not found. Install Suwayomi-Server or set the path in Settings.".to_string(), )) -} +} \ No newline at end of file diff --git a/src/api/mutations/chapters.ts b/src/api/mutations/chapters.ts index b2ea201..2418850 100644 --- a/src/api/mutations/chapters.ts +++ b/src/api/mutations/chapters.ts @@ -3,7 +3,7 @@ export const FETCH_CHAPTERS = ` fetchChapters(input: { mangaId: $mangaId }) { chapters { id name chapterNumber sourceOrder isRead isDownloaded isBookmarked - pageCount mangaId uploadDate realUrl lastPageRead scanlator + pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator } } } @@ -46,3 +46,19 @@ export const DELETE_DOWNLOADED_CHAPTERS = ` } } `; + +export const SET_CHAPTER_META = ` + mutation SetChapterMeta($chapterId: Int!, $key: String!, $value: String!) { + setChapterMeta(input: { meta: { chapterId: $chapterId, key: $key, value: $value } }) { + meta { key value } + } + } +`; + +export const DELETE_CHAPTER_META = ` + mutation DeleteChapterMeta($chapterId: Int!, $key: String!) { + deleteChapterMeta(input: { chapterId: $chapterId, key: $key }) { + meta { key value } + } + } +`; \ No newline at end of file diff --git a/src/api/mutations/extensions.ts b/src/api/mutations/extensions.ts index 26e1d5d..140ab4c 100644 --- a/src/api/mutations/extensions.ts +++ b/src/api/mutations/extensions.ts @@ -17,6 +17,14 @@ export const UPDATE_EXTENSION = ` } `; +export const UPDATE_EXTENSIONS = ` + mutation UpdateExtensions($ids: [String!]!, $install: Boolean, $uninstall: Boolean, $update: Boolean) { + updateExtensions(input: { ids: $ids, patch: { install: $install, uninstall: $uninstall, update: $update } }) { + extensions { apkName pkgName name isInstalled hasUpdate } + } + } +`; + export const INSTALL_EXTERNAL_EXTENSION = ` mutation InstallExternalExtension($url: String!) { installExternalExtension(input: { extensionUrl: $url }) { @@ -25,6 +33,82 @@ export const INSTALL_EXTERNAL_EXTENSION = ` } `; +export const UPDATE_SOURCE_PREFERENCE = ` + mutation UpdateSourcePreference($source: LongString!, $change: SourcePreferenceChangeInput!) { + updateSourcePreference(input: { source: $source, change: $change }) { + source { id displayName } + } + } +`; + +export const SET_SOURCE_META = ` + mutation SetSourceMeta($sourceId: LongString!, $key: String!, $value: String!) { + setSourceMeta(input: { meta: { sourceId: $sourceId, key: $key, value: $value } }) { + meta { key value } + } + } +`; + +export const DELETE_SOURCE_META = ` + mutation DeleteSourceMeta($sourceId: LongString!, $key: String!) { + deleteSourceMeta(input: { sourceId: $sourceId, key: $key }) { + meta { key value } + } + } +`; + +export const SET_CATEGORY_META = ` + mutation SetCategoryMeta($categoryId: Int!, $key: String!, $value: String!) { + setCategoryMeta(input: { meta: { categoryId: $categoryId, key: $key, value: $value } }) { + meta { key value } + } + } +`; + +export const DELETE_CATEGORY_META = ` + mutation DeleteCategoryMeta($categoryId: Int!, $key: String!) { + deleteCategoryMeta(input: { categoryId: $categoryId, key: $key }) { + meta { key value } + } + } +`; + +export const SET_GLOBAL_META = ` + mutation SetGlobalMeta($key: String!, $value: String!) { + setGlobalMeta(input: { meta: { key: $key, value: $value } }) { + meta { key value } + } + } +`; + +export const DELETE_GLOBAL_META = ` + mutation DeleteGlobalMeta($key: String!) { + deleteGlobalMeta(input: { key: $key }) { + meta { key value } + } + } +`; + +export const CLEAR_CACHED_IMAGES = ` + mutation ClearCachedImages($cachedPages: Boolean, $cachedThumbnails: Boolean, $downloadedThumbnails: Boolean) { + clearCachedImages(input: { + cachedPages: $cachedPages + cachedThumbnails: $cachedThumbnails + downloadedThumbnails: $downloadedThumbnails + }) { + cachedPages cachedThumbnails downloadedThumbnails + } + } +`; + +export const RESET_SETTINGS = ` + mutation ResetSettings { + resetSettings(input: {}) { + settings { extensionRepos } + } + } +`; + export const SET_EXTENSION_REPOS = ` mutation SetExtensionRepos($repos: [String!]!) { setSettings(input: { settings: { extensionRepos: $repos } }) { @@ -86,4 +170,4 @@ export const SET_FLARESOLVERR = ` } } } -`; +`; \ No newline at end of file diff --git a/src/api/mutations/index.ts b/src/api/mutations/index.ts index 35767c1..d075b68 100644 --- a/src/api/mutations/index.ts +++ b/src/api/mutations/index.ts @@ -2,4 +2,4 @@ export * from "./manga"; export * from "./chapters"; export * from "./downloads"; export * from "./extensions"; -export * from "./tracking"; +export * from "./tracking"; \ No newline at end of file diff --git a/src/api/mutations/manga.ts b/src/api/mutations/manga.ts index 176eca4..75c234a 100644 --- a/src/api/mutations/manga.ts +++ b/src/api/mutations/manga.ts @@ -33,6 +33,14 @@ export const UPDATE_MANGA_CATEGORIES = ` } `; +export const UPDATE_MANGAS_CATEGORIES = ` + mutation UpdateMangasCategories($ids: [Int!]!, $addTo: [Int!]!, $removeFrom: [Int!]!) { + updateMangasCategories(input: { ids: $ids, patch: { addToCategories: $addTo, removeFromCategories: $removeFrom } }) { + mangas { id } + } + } +`; + export const CREATE_CATEGORY = ` mutation CreateCategory($name: String!) { createCategory(input: { name: $name }) { @@ -49,6 +57,14 @@ export const UPDATE_CATEGORY = ` } `; +export const UPDATE_CATEGORIES = ` + mutation UpdateCategories($ids: [Int!]!, $patch: UpdateCategoryPatchInput!) { + updateCategories(input: { ids: $ids, patch: $patch }) { + categories { id name order default includeInUpdate includeInDownload } + } + } +`; + export const DELETE_CATEGORY = ` mutation DeleteCategory($id: Int!) { deleteCategory(input: { categoryId: $id }) { @@ -65,6 +81,16 @@ export const UPDATE_CATEGORY_ORDER = ` } `; +export const UPDATE_CATEGORY_MANGA = ` + mutation UpdateCategoryManga($categoryId: Int!) { + updateCategoryManga(input: { categoryId: $categoryId }) { + updateStatus { + jobsInfo { isRunning finishedJobs totalJobs } + } + } + } +`; + export const UPDATE_LIBRARY = ` mutation UpdateLibrary { updateLibrary(input: {}) { @@ -75,6 +101,26 @@ export const UPDATE_LIBRARY = ` } `; +export const UPDATE_LIBRARY_MANGA = ` + mutation UpdateLibraryManga($mangaId: Int!) { + updateLibraryManga(input: { mangaId: $mangaId }) { + updateStatus { + jobsInfo { isRunning finishedJobs totalJobs } + } + } + } +`; + +export const UPDATE_STOP = ` + mutation UpdateStop { + updateStop(input: {}) { + updateStatus { + jobsInfo { isRunning finishedJobs totalJobs } + } + } + } +`; + export const CREATE_BACKUP = ` mutation CreateBackup { createBackup(input: {}) { url } @@ -89,3 +135,19 @@ export const RESTORE_BACKUP = ` } } `; + +export const SET_MANGA_META = ` + mutation SetMangaMeta($mangaId: Int!, $key: String!, $value: String!) { + setMangaMeta(input: { meta: { mangaId: $mangaId, key: $key, value: $value } }) { + meta { key value } + } + } +`; + +export const DELETE_MANGA_META = ` + mutation DeleteMangaMeta($mangaId: Int!, $key: String!) { + deleteMangaMeta(input: { mangaId: $mangaId, key: $key }) { + meta { key value } + } + } +`; \ No newline at end of file diff --git a/src/api/mutations/mutations.md b/src/api/mutations/mutations.md index 8098ef9..9bbbce5 100644 --- a/src/api/mutations/mutations.md +++ b/src/api/mutations/mutations.md @@ -2,449 +2,129 @@ ## Manga (`mutations/manga.ts`) -### `FETCH_MANGA` -Fetches and refreshes manga metadata from its source. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `id` | `Int!` | Manga ID | - ---- - -### `UPDATE_MANGA` -Updates a single manga's library membership. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `id` | `Int!` | Manga ID | -| `inLibrary` | `Boolean` | Add/remove from library | - ---- - -### `UPDATE_MANGAS` -Bulk-updates library membership for multiple manga. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `ids` | `[Int!]!` | Manga IDs | -| `inLibrary` | `Boolean` | Add/remove from library | - ---- - -### `UPDATE_MANGA_CATEGORIES` -Adds or removes a manga from categories. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `mangaId` | `Int!` | Manga ID | -| `addTo` | `[Int!]!` | Category IDs to add to | -| `removeFrom` | `[Int!]!` | Category IDs to remove from | - ---- - -### `CREATE_CATEGORY` -Creates a new manga category. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `name` | `String!` | Category name | - ---- - -### `UPDATE_CATEGORY` -Updates a category's name. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `id` | `Int!` | Category ID | -| `name` | `String` | New name | - ---- - -### `DELETE_CATEGORY` -Deletes a category by ID. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `id` | `Int!` | Category ID | - ---- - -### `UPDATE_CATEGORY_ORDER` -Moves a category to a new position. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `id` | `Int!` | Category ID | -| `position` | `Int!` | New position index | - ---- - -### `UPDATE_LIBRARY` -Triggers a library-wide metadata refresh and returns job status. - -**Variables:** none - ---- - -### `CREATE_BACKUP` -Creates a backup and returns its download URL. - -**Variables:** none - ---- - -### `RESTORE_BACKUP` -Restores a backup from an uploaded file and returns restore job status. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `backup` | `Upload!` | Backup file | +| Mutation | Variables | Description | +|----------|-----------|-------------| +| `FETCH_MANGA` | `id: Int!` | Fetch and refresh manga metadata from its source | +| `UPDATE_MANGA` | `id: Int!`, `inLibrary: Boolean` | Update a single manga's library membership | +| `UPDATE_MANGAS` | `ids: [Int!]!`, `inLibrary: Boolean` | Bulk-update library membership for multiple manga | +| `UPDATE_MANGA_CATEGORIES` | `mangaId: Int!`, `addTo: [Int!]!`, `removeFrom: [Int!]!` | Add or remove a single manga from categories | +| `UPDATE_MANGAS_CATEGORIES` | `ids: [Int!]!`, `addTo: [Int!]!`, `removeFrom: [Int!]!` | Bulk add/remove multiple manga from categories | +| `CREATE_CATEGORY` | `name: String!` | Create a new category | +| `UPDATE_CATEGORY` | `id: Int!`, `name: String` | Update a category's name | +| `UPDATE_CATEGORIES` | `ids: [Int!]!`, `patch: UpdateCategoryPatchInput!` | Bulk-update multiple categories | +| `DELETE_CATEGORY` | `id: Int!` | Delete a category | +| `UPDATE_CATEGORY_ORDER` | `id: Int!`, `position: Int!` | Move a category to a new position | +| `UPDATE_CATEGORY_MANGA` | `categoryId: Int!` | Trigger a metadata update for all manga in a category | +| `UPDATE_LIBRARY` | — | Trigger a full library metadata refresh | +| `UPDATE_LIBRARY_MANGA` | `mangaId: Int!` | Trigger a metadata update for a single manga | +| `UPDATE_STOP` | — | Stop the currently running library update job | +| `CREATE_BACKUP` | — | Create a backup and return its download URL | +| `RESTORE_BACKUP` | `backup: Upload!` | Restore a backup file and return the restore job status | +| `SET_MANGA_META` | `mangaId: Int!`, `key: String!`, `value: String!` | Set a key/value meta entry on a manga | +| `DELETE_MANGA_META` | `mangaId: Int!`, `key: String!` | Delete a key/value meta entry from a manga | --- ## Chapters (`mutations/chapters.ts`) -### `FETCH_CHAPTERS` -Fetches/refreshes the chapter list for a manga from its source. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `mangaId` | `Int!` | Manga ID | - ---- - -### `FETCH_CHAPTER_PAGES` -Fetches the page URLs for a specific chapter. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `chapterId` | `Int!` | Chapter ID | - ---- - -### `MARK_CHAPTER_READ` -Marks a single chapter as read or unread. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `id` | `Int!` | Chapter ID | -| `isRead` | `Boolean!` | Read state | - ---- - -### `MARK_CHAPTERS_READ` -Bulk-marks multiple chapters as read or unread. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `ids` | `[Int!]!` | Chapter IDs | -| `isRead` | `Boolean!` | Read state | - ---- - -### `UPDATE_CHAPTERS_PROGRESS` -Bulk-updates read state, bookmark state, and last page read for multiple chapters. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `ids` | `[Int!]!` | Chapter IDs | -| `isRead` | `Boolean` | Read state | -| `isBookmarked` | `Boolean` | Bookmark state | -| `lastPageRead` | `Int` | Last page index read | - ---- - -### `DELETE_DOWNLOADED_CHAPTERS` -Deletes downloaded chapter files for the given chapter IDs. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `ids` | `[Int!]!` | Chapter IDs | +| Mutation | Variables | Description | +|----------|-----------|-------------| +| `FETCH_CHAPTERS` | `mangaId: Int!` | Fetch/refresh the chapter list for a manga from its source | +| `FETCH_CHAPTER_PAGES` | `chapterId: Int!` | Fetch the page URLs for a specific chapter | +| `MARK_CHAPTER_READ` | `id: Int!`, `isRead: Boolean!` | Mark a single chapter read or unread | +| `MARK_CHAPTERS_READ` | `ids: [Int!]!`, `isRead: Boolean!` | Bulk mark chapters read or unread | +| `UPDATE_CHAPTERS_PROGRESS` | `ids: [Int!]!`, `isRead: Boolean`, `isBookmarked: Boolean`, `lastPageRead: Int` | Bulk update read state, bookmark state, and last page read | +| `DELETE_DOWNLOADED_CHAPTERS` | `ids: [Int!]!` | Delete downloaded chapter files | +| `SET_CHAPTER_META` | `chapterId: Int!`, `key: String!`, `value: String!` | Set a key/value meta entry on a chapter | +| `DELETE_CHAPTER_META` | `chapterId: Int!`, `key: String!` | Delete a key/value meta entry from a chapter | --- ## Downloads (`mutations/downloads.ts`) -### `ENQUEUE_DOWNLOAD` -Adds a single chapter to the download queue. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `chapterId` | `Int!` | Chapter ID | - ---- - -### `ENQUEUE_CHAPTERS_DOWNLOAD` -Adds multiple chapters to the download queue. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `chapterIds` | `[Int!]!` | Chapter IDs | - ---- - -### `DEQUEUE_DOWNLOAD` -Removes a chapter from the download queue. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `chapterId` | `Int!` | Chapter ID | - ---- - -### `START_DOWNLOADER` -Starts the downloader and returns the current queue state. - -**Variables:** none - ---- - -### `STOP_DOWNLOADER` -Stops the downloader and returns the current queue state. - -**Variables:** none - ---- - -### `CLEAR_DOWNLOADER` -Clears all items from the download queue. - -**Variables:** none - ---- - -### `FETCH_SOURCE_MANGA` -Fetches manga from a source (browse/search), with pagination and optional filters. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `source` | `LongString!` | Source ID | -| `type` | `FetchSourceMangaType!` | Browse type (e.g. popular, latest, search) | -| `page` | `Int!` | Page number | -| `query` | `String` | Search query | -| `filters` | `[FilterChangeInput!]` | Source-specific filters | - ---- - -### `SET_DOWNLOADS_PATH` -Sets the downloads directory path in settings. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `path` | `String!` | Filesystem path | - ---- - -### `SET_LOCAL_SOURCE_PATH` -Sets the local source directory path in settings. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `path` | `String!` | Filesystem path | +| Mutation | Variables | Description | +|----------|-----------|-------------| +| `ENQUEUE_DOWNLOAD` | `chapterId: Int!` | Add a single chapter to the download queue | +| `ENQUEUE_CHAPTERS_DOWNLOAD` | `chapterIds: [Int!]!` | Add multiple chapters to the download queue | +| `DEQUEUE_DOWNLOAD` | `chapterId: Int!` | Remove a single chapter from the download queue | +| `DEQUEUE_CHAPTERS_DOWNLOAD` | `chapterIds: [Int!]!` | Remove multiple chapters from the download queue | +| `REORDER_DOWNLOAD` | `chapterId: Int!`, `to: Int!` | Move a queued chapter to a new position | +| `START_DOWNLOADER` | — | Start the downloader | +| `STOP_DOWNLOADER` | — | Stop the downloader | +| `CLEAR_DOWNLOADER` | — | Clear all items from the download queue | +| `FETCH_SOURCE_MANGA` | `source: LongString!`, `type: FetchSourceMangaType!`, `page: Int!`, `query: String`, `filters: [FilterChangeInput!]` | Fetch manga from a source (browse/search) with pagination | +| `SET_DOWNLOADS_PATH` | `path: String!` | Set the downloads directory path | +| `SET_LOCAL_SOURCE_PATH` | `path: String!` | Set the local source directory path | --- ## Extensions (`mutations/extensions.ts`) -### `FETCH_EXTENSIONS` -Fetches the latest extension list from configured repos. - -**Variables:** none - ---- - -### `UPDATE_EXTENSION` -Installs, uninstalls, or updates an extension. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `id` | `String!` | Extension package name | -| `install` | `Boolean` | Install the extension | -| `uninstall` | `Boolean` | Uninstall the extension | -| `update` | `Boolean` | Update the extension | - ---- - -### `INSTALL_EXTERNAL_EXTENSION` -Installs an extension from an external APK URL. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `url` | `String!` | APK download URL | - ---- - -### `SET_EXTENSION_REPOS` -Sets the list of extension repository URLs. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `repos` | `[String!]!` | Repository URLs | - ---- - -### `SET_SERVER_AUTH` -Configures server authentication mode and credentials. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `authMode` | `AuthMode!` | Auth mode | -| `authUsername` | `String!` | Username | -| `authPassword` | `String!` | Password | - ---- - -### `SET_SOCKS_PROXY` -Configures SOCKS proxy settings. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `socksProxyEnabled` | `Boolean!` | Enable/disable proxy | -| `socksProxyHost` | `String!` | Proxy host | -| `socksProxyPort` | `String!` | Proxy port | -| `socksProxyVersion` | `Int!` | SOCKS version (4 or 5) | -| `socksProxyUsername` | `String!` | Proxy username | -| `socksProxyPassword` | `String!` | Proxy password | - ---- - -### `SET_FLARESOLVERR` -Configures FlareSolverr integration settings. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `flareSolverrEnabled` | `Boolean!` | Enable/disable FlareSolverr | -| `flareSolverrUrl` | `String!` | FlareSolverr URL | -| `flareSolverrTimeout` | `Int!` | Request timeout (ms) | -| `flareSolverrSessionName` | `String!` | Session name | -| `flareSolverrSessionTtl` | `Int!` | Session TTL (seconds) | -| `flareSolverrAsResponseFallback` | `Boolean!` | Use as fallback only | +| Mutation | Variables | Description | +|----------|-----------|-------------| +| `FETCH_EXTENSIONS` | — | Fetch the latest extension list from configured repos | +| `UPDATE_EXTENSION` | `id: String!`, `install: Boolean`, `uninstall: Boolean`, `update: Boolean` | Install, uninstall, or update a single extension | +| `UPDATE_EXTENSIONS` | `ids: [String!]!`, `install: Boolean`, `uninstall: Boolean`, `update: Boolean` | Bulk install, uninstall, or update multiple extensions | +| `INSTALL_EXTERNAL_EXTENSION` | `url: String!` | Install an extension from an external APK URL | +| `UPDATE_SOURCE_PREFERENCE` | `source: LongString!`, `change: SourcePreferenceChangeInput!` | Update a source-specific preference value | +| `SET_SOURCE_META` | `sourceId: LongString!`, `key: String!`, `value: String!` | Set a key/value meta entry on a source | +| `DELETE_SOURCE_META` | `sourceId: LongString!`, `key: String!` | Delete a key/value meta entry from a source | +| `SET_CATEGORY_META` | `categoryId: Int!`, `key: String!`, `value: String!` | Set a key/value meta entry on a category | +| `DELETE_CATEGORY_META` | `categoryId: Int!`, `key: String!` | Delete a key/value meta entry from a category | +| `SET_GLOBAL_META` | `key: String!`, `value: String!` | Set a global key/value meta entry | +| `DELETE_GLOBAL_META` | `key: String!` | Delete a global key/value meta entry | +| `CLEAR_CACHED_IMAGES` | `cachedPages: Boolean`, `cachedThumbnails: Boolean`, `downloadedThumbnails: Boolean` | Selectively clear cached page images, cached thumbnails, or downloaded thumbnails | +| `RESET_SETTINGS` | — | Reset all server settings to defaults | +| `UPDATE_WEBUI` | — | Trigger a WebUI update and return live status | +| `RESET_WEBUI_UPDATE_STATUS` | — | Reset the WebUI update status back to idle | +| `SET_EXTENSION_REPOS` | `repos: [String!]!` | Set the list of extension repository URLs | +| `SET_SERVER_AUTH` | `authMode: AuthMode!`, `authUsername: String!`, `authPassword: String!` | Configure server auth mode and credentials | +| `SET_SOCKS_PROXY` | `socksProxyEnabled: Boolean!`, `socksProxyHost: String!`, `socksProxyPort: String!`, `socksProxyVersion: Int!`, `socksProxyUsername: String!`, `socksProxyPassword: String!` | Configure SOCKS proxy settings | +| `SET_FLARESOLVERR` | `flareSolverrEnabled: Boolean!`, `flareSolverrUrl: String!`, `flareSolverrTimeout: Int!`, `flareSolverrSessionName: String!`, `flareSolverrSessionTtl: Int!`, `flareSolverrAsResponseFallback: Boolean!` | Configure FlareSolverr integration | --- ## Tracking (`mutations/tracking.ts`) -### `BIND_TRACK` -Binds a manga to a remote tracker entry. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `mangaId` | `Int!` | Manga ID | -| `trackerId` | `Int!` | Tracker ID | -| `remoteId` | `LongString!` | Remote entry ID on the tracker | +| Mutation | Variables | Description | +|----------|-----------|-------------| +| `BIND_TRACK` | `mangaId: Int!`, `trackerId: Int!`, `remoteId: LongString!` | Bind a manga to a remote tracker entry | +| `UPDATE_TRACK` | `recordId: Int!`, `status: Int`, `lastChapterRead: Float`, `scoreString: String`, `startDate: LongString`, `finishDate: LongString`, `private: Boolean` | Update tracking progress, status, score, and dates | +| `UNBIND_TRACK` | `recordId: Int!` | Unbind a manga from a tracker record | +| `FETCH_TRACK` | `recordId: Int!` | Refresh a track record from the remote tracker | +| `TRACK_PROGRESS` | `mangaId: Int!` | Sync current reading progress to all bound trackers for a manga | +| `LOGIN_TRACKER_OAUTH` | `trackerId: Int!`, `callbackUrl: String!` | Initiate OAuth login for a tracker | +| `LOGIN_TRACKER_CREDENTIALS` | `trackerId: Int!`, `username: String!`, `password: String!` | Log into a tracker with username and password | +| `LOGOUT_TRACKER` | `trackerId: Int!` | Log out of a tracker | +| `CONNECT_KOSYNC` | `username: String!`, `password: String!`, `serverAddress: String!` | Connect a KOReader sync account | +| `LOGOUT_KOSYNC` | — | Disconnect the KOReader sync account | +| `PULL_KOSYNC_PROGRESS` | `chapterId: Int!` | Pull reading progress from KOReader sync for a chapter | +| `PUSH_KOSYNC_PROGRESS` | `chapterId: Int!` | Push reading progress to KOReader sync for a chapter | +| `LOGIN_USER` | `username: String!`, `password: String!` | Authenticate and return access + refresh tokens | +| `REFRESH_TOKEN` | — | Refresh the current access token | --- -### `UPDATE_TRACK` -Updates tracking progress, status, score, and dates for a track record. +## New in Preview -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `recordId` | `Int!` | Track record ID | -| `status` | `Int` | Reading status | -| `lastChapterRead` | `Float` | Last chapter read | -| `scoreString` | `String` | Score in tracker's format | -| `startDate` | `LongString` | Start date | -| `finishDate` | `LongString` | Finish date | -| `private` | `Boolean` | Mark as private | +Mutations now available and not yet wired to any feature in Moku: ---- - -### `UNBIND_TRACK` -Unbinds a manga from a tracker record. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `recordId` | `Int!` | Track record ID | - ---- - -### `FETCH_TRACK` -Refreshes a track record from the remote tracker. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `recordId` | `Int!` | Track record ID | - ---- - -### `LOGIN_TRACKER_OAUTH` -Initiates OAuth login for a tracker using a callback URL. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `trackerId` | `Int!` | Tracker ID | -| `callbackUrl` | `String!` | OAuth callback URL | - ---- - -### `LOGIN_TRACKER_CREDENTIALS` -Logs into a tracker using username and password. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `trackerId` | `Int!` | Tracker ID | -| `username` | `String!` | Username | -| `password` | `String!` | Password | - ---- - -### `LOGOUT_TRACKER` -Logs out of a tracker. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `trackerId` | `Int!` | Tracker ID | - ---- - -### `LOGIN_USER` -Authenticates a user and returns access and refresh tokens. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `username` | `String!` | Username | -| `password` | `String!` | Password | - ---- - -### `REFRESH_TOKEN` -Refreshes the current access token. - -**Variables:** none +| Mutation | Potential Feature | +|----------|-------------------| +| `UPDATE_MANGAS_CATEGORIES` | Bulk category editor — move/assign multiple manga at once | +| `UPDATE_CATEGORIES` | Bulk category settings — toggle update/download flags for multiple categories at once | +| `UPDATE_CATEGORY_MANGA` | Per-category refresh button — update only one category's manga | +| `UPDATE_LIBRARY_MANGA` | Single manga refresh — trigger from series detail without a full library update | +| `UPDATE_STOP` | Cancel button for library update jobs | +| `UPDATE_EXTENSIONS` | Bulk extension updater — "update all" button in extensions page | +| `UPDATE_SOURCE_PREFERENCE` | Source settings page — persist source-specific preferences | +| `SET_SOURCE_META` / `DELETE_SOURCE_META` | Per-source client state — store browse position, last filter, etc. | +| `SET_CATEGORY_META` / `DELETE_CATEGORY_META` | Per-category client state — store sort/filter preferences per category | +| `SET_CHAPTER_META` / `DELETE_CHAPTER_META` | Per-chapter client state — annotations, custom notes | +| `SET_GLOBAL_META` / `DELETE_GLOBAL_META` | Server-synced app state — replace local persistence for settings that should roam | +| `CLEAR_CACHED_IMAGES` | Storage settings — granular cache clearing (pages, thumbnails, downloaded) | +| `RESET_SETTINGS` | Settings page — factory reset button | +| `UPDATE_WEBUI` / `RESET_WEBUI_UPDATE_STATUS` | WebUI update flow in settings — trigger and monitor update progress | +| `TRACK_PROGRESS` | One-tap sync — push current reading position to all trackers without opening tracking panel | +| `CONNECT_KOSYNC` / `LOGOUT_KOSYNC` | KOReader sync settings section — connect/disconnect account | +| `PULL_KOSYNC_PROGRESS` / `PUSH_KOSYNC_PROGRESS` | KOReader sync — manual pull/push per chapter, or auto-sync on chapter open/close | \ No newline at end of file diff --git a/src/api/mutations/tracking.ts b/src/api/mutations/tracking.ts index f9baf12..9b5846b 100644 --- a/src/api/mutations/tracking.ts +++ b/src/api/mutations/tracking.ts @@ -1,6 +1,6 @@ const TRACK_RECORD_FRAGMENT = ` id trackerId remoteId title status score displayScore - lastChapterRead totalChapters remoteUrl startDate finishDate private + lastChapterRead totalChapters remoteUrl startDate finishDate private libraryId `; export const BIND_TRACK = ` @@ -15,7 +15,7 @@ export const UPDATE_TRACK = ` mutation UpdateTrack($recordId: Int!, $status: Int, $lastChapterRead: Float, $scoreString: String, $startDate: LongString, $finishDate: LongString, $private: Boolean) { updateTrack(input: { recordId: $recordId, status: $status, lastChapterRead: $lastChapterRead, scoreString: $scoreString, startDate: $startDate, finishDate: $finishDate, private: $private }) { trackRecord { - id trackerId status score displayScore lastChapterRead totalChapters startDate finishDate private + id trackerId status score displayScore lastChapterRead totalChapters startDate finishDate private libraryId } } } @@ -33,7 +33,17 @@ export const FETCH_TRACK = ` mutation FetchTrack($recordId: Int!) { fetchTrack(input: { recordId: $recordId }) { trackRecord { - id trackerId status score displayScore lastChapterRead totalChapters startDate finishDate + id trackerId status score displayScore lastChapterRead totalChapters startDate finishDate libraryId + } + } + } +`; + +export const TRACK_PROGRESS = ` + mutation TrackProgress($mangaId: Int!) { + trackProgress(input: { mangaId: $mangaId }) { + trackRecords { + id trackerId lastChapterRead status } } } @@ -43,7 +53,7 @@ export const LOGIN_TRACKER_OAUTH = ` mutation LoginTrackerOAuth($trackerId: Int!, $callbackUrl: String!) { loginTrackerOAuth(input: { trackerId: $trackerId, callbackUrl: $callbackUrl }) { isLoggedIn - tracker { id name isLoggedIn authUrl } + tracker { id name isLoggedIn isTokenExpired authUrl } } } `; @@ -52,7 +62,7 @@ export const LOGIN_TRACKER_CREDENTIALS = ` mutation LoginTrackerCredentials($trackerId: Int!, $username: String!, $password: String!) { loginTrackerCredentials(input: { trackerId: $trackerId, username: $username, password: $password }) { isLoggedIn - tracker { id name isLoggedIn authUrl } + tracker { id name isLoggedIn isTokenExpired authUrl } } } `; @@ -60,7 +70,39 @@ export const LOGIN_TRACKER_CREDENTIALS = ` export const LOGOUT_TRACKER = ` mutation LogoutTracker($trackerId: Int!) { logoutTracker(input: { trackerId: $trackerId }) { - tracker { id name isLoggedIn authUrl } + tracker { id name isLoggedIn isTokenExpired authUrl } + } + } +`; + +export const CONNECT_KOSYNC = ` + mutation ConnectKoSync($username: String!, $password: String!, $serverAddress: String!) { + connectKoSyncAccount(input: { username: $username, password: $password, serverAddress: $serverAddress }) { + isConnected + } + } +`; + +export const LOGOUT_KOSYNC = ` + mutation LogoutKoSync { + logoutKoSyncAccount(input: {}) { + isConnected + } + } +`; + +export const PULL_KOSYNC_PROGRESS = ` + mutation PullKoSyncProgress($chapterId: Int!) { + pullKoSyncProgress(input: { chapterId: $chapterId }) { + chapter { id lastPageRead isRead } + } + } +`; + +export const PUSH_KOSYNC_PROGRESS = ` + mutation PushKoSyncProgress($chapterId: Int!) { + pushKoSyncProgress(input: { chapterId: $chapterId }) { + chapter { id lastPageRead isRead } } } `; @@ -75,6 +117,6 @@ export const LOGIN_USER = ` export const REFRESH_TOKEN = ` mutation RefreshToken { - refreshToken { accessToken } + refreshToken(input: {}) { accessToken } } -`; +`; \ No newline at end of file diff --git a/src/api/queries/chapters.ts b/src/api/queries/chapters.ts index f5d973f..4c22666 100644 --- a/src/api/queries/chapters.ts +++ b/src/api/queries/chapters.ts @@ -15,7 +15,7 @@ export const GET_CHAPTERS = ` chapters(condition: { mangaId: $mangaId }) { nodes { id name chapterNumber sourceOrder isRead isDownloaded isBookmarked - pageCount mangaId uploadDate realUrl lastPageRead scanlator + pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator } } } diff --git a/src/api/queries/downloads.ts b/src/api/queries/downloads.ts index 416d000..4893b45 100644 --- a/src/api/queries/downloads.ts +++ b/src/api/queries/downloads.ts @@ -11,4 +11,4 @@ export const GET_DOWNLOAD_STATUS = ` } } } -`; +`; \ No newline at end of file diff --git a/src/api/queries/extensions.ts b/src/api/queries/extensions.ts index 8c41453..9bb9664 100644 --- a/src/api/queries/extensions.ts +++ b/src/api/queries/extensions.ts @@ -20,7 +20,10 @@ export const GET_EXTENSIONS = ` export const GET_SOURCES = ` query GetSources { sources { - nodes { id name lang displayName iconUrl isNsfw } + nodes { + id name lang displayName iconUrl isNsfw + isConfigurable supportsLatest baseUrl + } } } `; diff --git a/src/api/queries/index.ts b/src/api/queries/index.ts index 35767c1..de739e0 100644 --- a/src/api/queries/index.ts +++ b/src/api/queries/index.ts @@ -3,3 +3,5 @@ export * from "./chapters"; export * from "./downloads"; export * from "./extensions"; export * from "./tracking"; +export * from "./updater"; +export * from "./meta"; \ No newline at end of file diff --git a/src/api/queries/manga.ts b/src/api/queries/manga.ts index 9f32101..d2656eb 100644 --- a/src/api/queries/manga.ts +++ b/src/api/queries/manga.ts @@ -2,10 +2,15 @@ export const GET_LIBRARY = ` query GetLibrary { mangas(condition: { inLibrary: true }) { nodes { - id title thumbnailUrl inLibrary downloadCount unreadCount + id title thumbnailUrl inLibrary downloadCount unreadCount bookmarkCount description status author artist genre + inLibraryAt lastFetchedAt chaptersLastFetchedAt thumbnailUrlLastFetched source { id name displayName } chapters { totalCount } + latestFetchedChapter { id uploadDate } + latestUploadedChapter { id uploadDate } + lastReadChapter { id chapterNumber } + firstUnreadChapter { id chapterNumber } } } } @@ -23,7 +28,11 @@ export const GET_MANGA = ` query GetManga($id: Int!) { manga(id: $id) { id title description thumbnailUrl status author artist genre inLibrary realUrl + inLibraryAt lastFetchedAt thumbnailUrlLastFetched updateStrategy source { id name displayName } + lastReadChapter { id chapterNumber lastPageRead } + firstUnreadChapter { id chapterNumber } + highestNumberedChapter { id chapterNumber } } } `; @@ -58,7 +67,9 @@ export const GET_DOWNLOADS_PATH = ` export const LIBRARY_UPDATE_STATUS = ` query LibraryUpdateStatus { libraryUpdateStatus { - jobsInfo { isRunning finishedJobs totalJobs skippedMangasCount } + jobsInfo { + isRunning finishedJobs totalJobs skippedMangasCount skippedCategoriesCount + } mangaUpdates { status manga { id title thumbnailUrl unreadCount } @@ -93,4 +104,4 @@ export const MANGAS_BY_GENRE = ` totalCount } } -`; +`; \ No newline at end of file diff --git a/src/api/queries/meta.ts b/src/api/queries/meta.ts new file mode 100644 index 0000000..3593a6f --- /dev/null +++ b/src/api/queries/meta.ts @@ -0,0 +1,15 @@ +export const GET_META = ` + query GetMeta($key: String!) { + meta(key: $key) { + key value + } + } +`; + +export const GET_METAS = ` + query GetMetas { + metas { + nodes { key value } + } + } +`; \ No newline at end of file diff --git a/src/api/queries/queries.md b/src/api/queries/queries.md index 25d9064..e8dadef 100644 --- a/src/api/queries/queries.md +++ b/src/api/queries/queries.md @@ -2,170 +2,116 @@ ## Manga (`queries/manga.ts`) -### `GET_LIBRARY` -Fetches all manga marked as in-library, including metadata, source info, chapter count, download count, and unread count. - -**Variables:** none - ---- - -### `GET_ALL_MANGA` -Fetches all manga (library and non-library) with minimal fields. - -**Variables:** none - ---- - -### `GET_MANGA` -Fetches a single manga by ID with full metadata and source info. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `id` | `Int!` | Manga ID | - ---- - -### `GET_CATEGORIES` -Fetches all categories with their order, settings, and the manga assigned to each. - -**Variables:** none - ---- - -### `GET_DOWNLOADED_CHAPTERS_PAGES` -Fetches page counts for all downloaded chapters. - -**Variables:** none - ---- - -### `GET_DOWNLOADS_PATH` -Fetches the configured downloads path and local source path from settings. - -**Variables:** none - ---- - -### `LIBRARY_UPDATE_STATUS` -Fetches the current library update job status, including progress and any manga with new chapters. - -**Variables:** none - ---- - -### `GET_RESTORE_STATUS` -Fetches the status of a backup restore operation by its job ID. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `id` | `String!` | Restore job ID | - ---- - -### `VALIDATE_BACKUP` -Validates a backup file and returns any missing sources or trackers. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `backup` | `Upload!` | Backup file | +| Query | Variables | Description | +|-------|-----------|-------------| +| `GET_LIBRARY` | — | All in-library manga with metadata, source, chapter counts, download count, unread count, bookmark count, and read progress anchors (`lastReadChapter`, `firstUnreadChapter`) | +| `GET_ALL_MANGA` | — | Minimal manga list — id, title, thumbnail, library flag, download count | +| `GET_MANGA` | `id: Int!` | Full detail for a single manga — includes `updateStrategy`, `lastReadChapter`, `firstUnreadChapter`, `highestNumberedChapter` | +| `GET_CATEGORIES` | — | All categories with order/settings and their assigned manga (minimal fields) | +| `GET_DOWNLOADED_CHAPTERS_PAGES` | — | Page counts for all downloaded chapters — used for storage stats | +| `GET_DOWNLOADS_PATH` | — | `downloadsPath` and `localSourcePath` from settings | +| `LIBRARY_UPDATE_STATUS` | — | Current library update job — `jobsInfo` progress and `mangaUpdates` list with new chapters | +| `GET_RESTORE_STATUS` | `id: String!` | Backup restore job status by job ID — `mangaProgress`, `state`, `totalManga` | +| `VALIDATE_BACKUP` | `backup: Upload!` | Validate a backup file before restore — returns missing sources and trackers | +| `MANGAS_BY_GENRE` | `filter: MangaFilterInput`, `first: Int`, `offset: Int` | Paginated manga filtered by genre, ordered by `IN_LIBRARY_AT DESC` | --- ## Chapters (`queries/chapters.ts`) -### `GET_CHAPTERS` -Fetches all chapters for a given manga, including read/download/bookmark state and page info. - -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `mangaId` | `Int!` | Manga ID | +| Query | Variables | Description | +|-------|-----------|-------------| +| `GET_RECENTLY_UPDATED` | — | Latest 300 chapters ordered by `FETCHED_AT DESC` with parent manga info | +| `GET_CHAPTERS` | `mangaId: Int!` | All chapters for a manga — includes `lastReadAt`, `lastPageRead`, read/download/bookmark state, page count, scanlator | --- ## Downloads (`queries/downloads.ts`) -### `GET_DOWNLOAD_STATUS` -Fetches the current downloader state and full queue with chapter and manga info. - -**Variables:** none +| Query | Variables | Description | +|-------|-----------|-------------| +| `GET_DOWNLOAD_STATUS` | — | Downloader state (`DownloaderState` enum) and full queue with chapter and manga info | --- ## Extensions (`queries/extensions.ts`) -### `GET_EXTENSIONS` -Fetches all extensions with install status, update availability, and metadata. - -**Variables:** none - ---- - -### `GET_SOURCES` -Fetches all available sources with language and NSFW flags. - -**Variables:** none - ---- - -### `GET_SETTINGS` -Fetches extension repository settings. - -**Variables:** none - ---- - -### `GET_SERVER_SECURITY` -Fetches all server security settings including auth mode, SOCKS proxy config, and FlareSolverr config. - -**Variables:** none +| Query | Variables | Description | +|-------|-----------|-------------| +| `GET_LOCAL_MANGA` | — | Manga from the local source (`sourceId: "0"`) | +| `GET_EXTENSIONS` | — | All extensions — install status, update flag, obsolete flag, metadata | +| `GET_SOURCES` | — | All sources — id, name, lang, display name, icon, NSFW flag, `isConfigurable`, `supportsLatest`, `baseUrl` | +| `GET_SETTINGS` | — | `extensionRepos` from settings | +| `GET_SERVER_SECURITY` | — | Full security config — auth mode, SOCKS proxy settings, FlareSolverr settings | --- ## Tracking (`queries/tracking.ts`) -### `GET_TRACKERS` -Fetches all trackers with login status, supported scores, statuses, and auth info. - -**Variables:** none +| Query | Variables | Description | +|-------|-----------|-------------| +| `GET_TRACKERS` | — | All trackers with login state, token expiry, capability flags (`supportsPrivateTracking`, `supportsReadingDates`, `supportsTrackDeletion`), scores, and statuses | +| `GET_MANGA_TRACK_RECORDS` | `mangaId: Int!` | All track records for a specific manga — includes `libraryId`, score, dates, privacy flag | +| `SEARCH_TRACKER` | `trackerId: Int!`, `query: String!` | Search a tracker by query string — returns id, title, cover, summary, publishing info | +| `GET_ALL_TRACKER_RECORDS` | — | All trackers and their full record lists with associated manga — includes `isTokenExpired`, `libraryId` | +| `GET_TRACKER_RECORDS` | `trackerId: Int!` | Records for a specific tracker with associated manga | --- -### `GET_MANGA_TRACK_RECORDS` -Fetches all tracking records for a specific manga across all trackers. +## Updater (`queries/updater.ts`) -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `mangaId` | `Int!` | Manga ID | +| Query | Variables | Description | +|-------|-----------|-------------| +| `GET_ABOUT_SERVER` | — | Server name, version, build type, build time, GitHub and Discord links | +| `GET_ABOUT_WEBUI` | — | WebUI channel, tag, and last update timestamp | +| `CHECK_FOR_SERVER_UPDATES` | — | Available server updates — channel, tag, download URL | +| `CHECK_FOR_WEBUI_UPDATE` | — | Available WebUI updates — channel and tag | +| `GET_WEBUI_UPDATE_STATUS` | — | Live WebUI update state (`UpdateState` enum), progress percent, and info block | --- -### `SEARCH_TRACKER` -Searches a tracker for manga by query string. +## Meta (`queries/meta.ts`) -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `trackerId` | `Int!` | Tracker ID | -| `query` | `String!` | Search query | +| Query | Variables | Description | +|-------|-----------|-------------| +| `GET_META` | `key: String!` | Single server-side key/value meta entry | +| `GET_METAS` | — | All global meta entries as a node list | --- -### `GET_ALL_TRACKER_RECORDS` -Fetches all trackers and their full track records, including associated manga info. +## KoSync (`queries/kosync.ts`) -**Variables:** none +| Query | Variables | Description | +|-------|-----------|-------------| +| `GET_KOSYNC_STATUS` | — | KOReader sync connection status | --- -### `GET_TRACKER_RECORDS` -Fetches track records for a specific tracker. +## New in Preview -**Variables:** -| Name | Type | Description | -|------|------|-------------| -| `trackerId` | `Int!` | Tracker ID | +Queries and fields now available but not yet wired to any feature in Moku: + +| Query / Field | Potential Feature | +|---------------|-------------------| +| `GET_ABOUT_SERVER` | About page — server version, build info, links to GitHub and Discord | +| `GET_ABOUT_WEBUI` | About page — WebUI version and release channel | +| `CHECK_FOR_SERVER_UPDATES` | Update available banner or settings badge | +| `CHECK_FOR_WEBUI_UPDATE` | Update available banner or settings badge | +| `GET_WEBUI_UPDATE_STATUS` | Update progress indicator in settings | +| `GET_META` / `GET_METAS` | Server-side persistence — sync app state across clients without local storage | +| `GET_KOSYNC_STATUS` | KOReader sync settings section — show connection state | +| `trackRecords` (top-level) | Flat tracker record browser — filter by score, privacy, tracker | +| `category` (single by id) | Direct category detail without fetching all categories | +| `chapter` (single by id) | Direct chapter lookup without fetching full manga chapter list | +| `source` (single by id) | Source detail page — preferences, filters, browse | +| `tracker` (single by id) | Individual tracker detail — statuses, records | +| `trackRecord` (single by id) | Direct track record lookup for deep linking | +| `lastUpdateTimestamp` | Stale data detection — poll before refetching library | +| `MangaType.hasDuplicateChapters` | Library health view — flag manga with duplicate chapter numbers | +| `MangaType.age` / `chaptersAge` | Stale manga indicator — highlight series with no updates in N days | +| `MangaType.initialized` | Loading skeleton gating — skip detail render until manga is fully fetched | +| `SourceType.isConfigurable` | Source list — show gear icon only when source is configurable | +| `SourceType.supportsLatest` | Source browse UI — conditionally show Latest tab | +| `TrackerType.supportsTrackDeletion` | Tracking panel — show remove button only when tracker supports it | +| `TrackerType.supportsReadingDates` | Tracking panel — show date fields only when tracker supports them | +| `TrackerType.isTokenExpired` | Re-auth prompt — detect expired tokens before a request fails | \ No newline at end of file diff --git a/src/api/queries/tracking.ts b/src/api/queries/tracking.ts index 3387359..3374d25 100644 --- a/src/api/queries/tracking.ts +++ b/src/api/queries/tracking.ts @@ -2,7 +2,9 @@ export const GET_TRACKERS = ` query GetTrackers { trackers { nodes { - id name icon isLoggedIn authUrl supportsPrivateTracking scores + id name icon isLoggedIn isTokenExpired authUrl + supportsPrivateTracking supportsReadingDates supportsTrackDeletion + scores statuses { value name } } } @@ -15,7 +17,7 @@ export const GET_MANGA_TRACK_RECORDS = ` trackRecords { nodes { id trackerId remoteId title status score displayScore - lastChapterRead totalChapters remoteUrl startDate finishDate private + lastChapterRead totalChapters remoteUrl startDate finishDate private libraryId } } } @@ -37,12 +39,12 @@ export const GET_ALL_TRACKER_RECORDS = ` query GetAllTrackerRecords { trackers { nodes { - id name icon isLoggedIn scores + id name icon isLoggedIn isTokenExpired scores statuses { value name } trackRecords { nodes { id trackerId title status displayScore lastChapterRead - totalChapters remoteUrl private + totalChapters remoteUrl private libraryId manga { id title thumbnailUrl inLibrary } } } @@ -66,4 +68,4 @@ export const GET_TRACKER_RECORDS = ` } } } -`; +`; \ No newline at end of file diff --git a/src/api/queries/updater.ts b/src/api/queries/updater.ts new file mode 100644 index 0000000..6b02105 --- /dev/null +++ b/src/api/queries/updater.ts @@ -0,0 +1,23 @@ +export const GET_ABOUT_SERVER = ` + query GetAboutServer { + aboutServer { + name version buildType buildTime github discord + } + } +`; + +export const GET_ABOUT_WEBUI = ` + query GetAboutWebUI { + aboutWebUI { + channel tag updateTimestamp + } + } +`; + +export const CHECK_FOR_SERVER_UPDATES = ` + query CheckForServerUpdates { + checkForServerUpdates { + channel tag url + } + } +`; \ No newline at end of file diff --git a/src/core/util.ts b/src/core/util.ts index 4cbec57..967603e 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -3,8 +3,6 @@ import type { Settings } from "@types"; export { clsx as cn } from "clsx"; -// ── Time / formatting ───────────────────────────────────────────────────────── - export function timeAgo(ts: number): string { const diff = Date.now() - ts, m = Math.floor(diff / 60000); if (m < 1) return "Just now"; @@ -31,8 +29,6 @@ export function formatReadTime(m: number): string { return r === 0 ? `${h}h` : `${h}h ${r}m`; } -// ── Content filtering ───────────────────────────────────────────────────────── - const STRICT_TAGS: string[] = [ "adult", "mature", "hentai", "ecchi", "erotic", "pornograph", "18+", "smut", "lemon", "explicit", "sexual violence", @@ -50,8 +46,8 @@ type ContentFilterSettings = Pick< >; function blockedTagsForSettings(settings: ContentFilterSettings): string[] { - if (settings.contentLevel === "strict") return STRICT_TAGS; - if (settings.contentLevel === "moderate") return MODERATE_TAGS; + if (settings.contentLevel === "strict") return STRICT_TAGS; + if (settings.contentLevel === "moderate") return MODERATE_TAGS; return []; } @@ -60,17 +56,13 @@ function genreMatchesBlocklist(genre: string[], blockedTags: string[]): boolean return genre.some(g => blockedTags.some(tag => g.toLowerCase().trim().includes(tag))); } -/** - * Returns true when the manga should be hidden. - * Called by all views — library, search cache, discover. - */ export function shouldHideNsfw( manga: Pick, settings: ContentFilterSettings, ): boolean { if (settings.contentLevel === "unrestricted") return false; - const srcId = manga.source?.id; + const srcId = manga.source?.id; const blocked = settings.sourceOverridesEnabled ? (settings.nsfwBlockedSourceIds ?? []) : []; const allowed = settings.sourceOverridesEnabled ? (settings.nsfwAllowedSourceIds ?? []) : []; @@ -83,10 +75,6 @@ export function shouldHideNsfw( return genreMatchesBlocklist(manga.genre ?? [], blockedTags); } -/** - * Returns true when the source should be hidden. - * Used in extension lists and source fan-out. - */ export function shouldHideSource( source: Pick, settings: ContentFilterSettings, @@ -101,8 +89,6 @@ export function shouldHideSource( return source.isNsfw && settings.contentLevel === "strict"; } -// ── Source deduplication ────────────────────────────────────────────────────── - export function dedupeSourcesByLang( sources: Source[], preferredLang: string, @@ -138,8 +124,6 @@ export function dedupeSources(sources: Source[], preferredLang: string): Source[ return picked; } -// ── Manga deduplication ─────────────────────────────────────────────────────── - export function normalizeTitle(title: string): string { return title .toLowerCase() diff --git a/src/features/library/components/Library.svelte b/src/features/library/components/Library.svelte index 4ca529c..4c7f9f0 100644 --- a/src/features/library/components/Library.svelte +++ b/src/features/library/components/Library.svelte @@ -135,9 +135,9 @@ const f = store.settings.libraryTabFilters?.[tab] ?? {}; if (f.unread) items = items.filter(m => (m.unreadCount ?? 0) > 0); - if (f.started) items = items.filter(m => (m.unreadCount ?? 0) > 0 && (m.chapterCount ?? 0) > (m.unreadCount ?? 0)); + if (f.started) items = items.filter(m => (m.unreadCount ?? 0) > 0 && (m.chapters?.totalCount ?? 0) > (m.unreadCount ?? 0)); if (f.downloaded) items = items.filter(m => (m.downloadCount ?? 0) > 0); - if (f.bookmarked) items = items.filter(m => !!(m as any).hasBookmark); + if (f.bookmarked) items = items.filter(m => (m.bookmarkCount ?? 0) > 0); const recentlyReadMap = new Map(); if (tabSortMode === "recentlyRead") { @@ -247,7 +247,7 @@ cache.get(CACHE_KEYS.LIBRARY, () => gql<{ mangas: { nodes: Manga[] } }>(GET_LIBRARY).then(d => d.mangas.nodes), DEFAULT_TTL_MS, CACHE_GROUPS.LIBRARY), reloadCategories(), ]); - const mapped = nodes.map((m: any) => ({ ...m, chapterCount: m.chapters?.totalCount ?? m.chapterCount ?? 0 })); + const mapped = nodes.map((m: any) => ({ ...m })); allManga = dedupeMangaByTitle(dedupeMangaById(mapped), store.settings.mangaLinks); error = null; await migrateCategorizedToLibrary(); diff --git a/src/features/library/components/LibraryGrid.svelte b/src/features/library/components/LibraryGrid.svelte index ae7adc5..b57baee 100644 --- a/src/features/library/components/LibraryGrid.svelte +++ b/src/features/library/components/LibraryGrid.svelte @@ -115,7 +115,7 @@
{#each visibleManga as m (m.id)} {@const isSelected = selectedIds.has(m.id)} - {@const isCompleted = !m.unreadCount && (m.chapterCount ?? 0) > 0} + {@const isCompleted = !m.unreadCount && (m.chapters?.totalCount ?? 0) > 0}
+ {#if serverInfo} +
+

Server

+
+
+
+ Version + + {serverInfo.version} + {#if serverInfo.buildType} + {serverInfo.buildType} + {/if} + +
+
+ {#if serverInfo.buildTime} +
+
+ Built + {fmtBuildTime(serverInfo.buildTime)} +
+
+ {/if} + {#if webuiInfo?.channel} +
+
+ Channel + {webuiInfo.channel} +
+
+ {/if} +
+
+ {/if} +

Releases

@@ -223,6 +283,12 @@
GitHub → Discord → + {#if serverInfo?.github && serverInfo.github !== "https://github.com/moku-project/Moku"} + Suwayomi GitHub → + {/if} + {#if serverInfo?.discord && serverInfo.discord !== "https://discord.gg/Jq3pwuNqPp"} + Suwayomi Discord → + {/if}
diff --git a/src/features/settings/sections/SecuritySettings.svelte b/src/features/settings/sections/SecuritySettings.svelte index 1148cd9..58a4762 100644 --- a/src/features/settings/sections/SecuritySettings.svelte +++ b/src/features/settings/sections/SecuritySettings.svelte @@ -38,7 +38,7 @@ let flareTimeout = $state(store.settings.flareSolverrTimeout ?? 60); let flareSession = $state(store.settings.flareSolverrSessionName ?? "moku"); let flareTtl = $state(store.settings.flareSolverrSessionTtl ?? 15); - let flareFallback = $state(store.settings.flareSolverrFallback ?? false); + let flareFallback = $state(store.settings.flareSolverrAsResponseFallback ?? false); function showSaved(key: string) { secSaved = key; secError = null; @@ -74,7 +74,7 @@ socksProxyVersion: socksVersion, socksProxyUsername: socksUsername, flareSolverrEnabled: flareEnabled, flareSolverrUrl: flareUrl, flareSolverrTimeout: flareTimeout, flareSolverrSessionName: flareSession, - flareSolverrSessionTtl: flareTtl, flareSolverrFallback: flareFallback, + flareSolverrSessionTtl: flareTtl, flareSolverrAsResponseFallback: flareFallback, }); } catch {} } @@ -144,7 +144,7 @@ updateSettings({ flareSolverrEnabled: flareEnabled, flareSolverrUrl: flareUrl, flareSolverrTimeout: flareTimeout, flareSolverrSessionName: flareSession, - flareSolverrSessionTtl: flareTtl, flareSolverrFallback: flareFallback, + flareSolverrSessionTtl: flareTtl, flareSolverrAsResponseFallback: flareFallback, }); showSaved("flare"); } catch (e: any) { diff --git a/src/features/tracking/lib/trackingSync.ts b/src/features/tracking/lib/trackingSync.ts index 39c4872..6acb89e 100644 --- a/src/features/tracking/lib/trackingSync.ts +++ b/src/features/tracking/lib/trackingSync.ts @@ -114,6 +114,7 @@ export function removeRecord( } export interface SyncBackOptions { + threshold: number | null; respectScanlatorFilter: boolean; chapterPrefs: ChapterDisplayPrefs; } @@ -123,36 +124,30 @@ export async function syncBackFromTracker( chapters: Chapter[], opts: SyncBackOptions, gqlFn: (query: string, vars: Record) => Promise, -): Promise<{ markedRead: number[]; markedUnread: number[] }> { - const eligible = buildChapterList( - opts.respectScanlatorFilter ? buildChapterList(chapters, opts.chapterPrefs) : chapters, - { ...opts.chapterPrefs, sortDir: "asc" }, - ); +): Promise { + const base = opts.respectScanlatorFilter + ? buildChapterList(chapters, opts.chapterPrefs) + : chapters; + const eligible = buildChapterList(base, { ...opts.chapterPrefs, sortDir: "asc" }); - const toMarkRead: number[] = []; - const toMarkUnread: number[] = []; + const toMarkRead: number[] = []; for (const record of records) { const remote = record.lastChapterRead; if (!remote || remote <= 0) continue; - const position = Math.round(remote); - const below = eligible.slice(0, position); - const above = eligible.slice(position); - - toMarkRead.push(...below.filter(c => !c.isRead).map(c => c.id)); - toMarkUnread.push(...above.filter(c => c.isRead).map(c => c.id)); + for (const chapter of eligible) { + if (chapter.isRead) continue; + const diff = Math.abs(chapter.chapterNumber - remote); + if (opts.threshold !== null && diff > opts.threshold) continue; + if (chapter.chapterNumber <= remote) toMarkRead.push(chapter.id); + } } - const readIds = [...new Set(toMarkRead)]; - const unreadIds = [...new Set(toMarkUnread)]; - - if (readIds.length > 0) { - await gqlFn(MARK_CHAPTERS_READ, { ids: readIds, isRead: true }); - } - if (unreadIds.length > 0) { - await gqlFn(MARK_CHAPTERS_READ, { ids: unreadIds, isRead: false }); + const ids = [...new Set(toMarkRead)]; + if (ids.length > 0) { + await gqlFn(MARK_CHAPTERS_READ, { ids, isRead: true }); } - return { markedRead: readIds, markedUnread: unreadIds }; + return ids; } \ No newline at end of file diff --git a/src/features/tracking/store/trackingState.svelte.ts b/src/features/tracking/store/trackingState.svelte.ts index 428b25a..bd68d2d 100644 --- a/src/features/tracking/store/trackingState.svelte.ts +++ b/src/features/tracking/store/trackingState.svelte.ts @@ -2,7 +2,6 @@ import { gql } from "@api/client"; import { GET_MANGA_TRACK_RECORDS, GET_ALL_TRACKER_RECORDS } from "@api/queries/tracking"; import { GET_CHAPTERS } from "@api/queries/chapters"; import { UPDATE_TRACK, FETCH_TRACK, UNBIND_TRACK } from "@api/mutations/tracking"; -import { MARK_CHAPTERS_READ } from "@api/mutations/chapters"; import { buildChapterList, type ChapterDisplayPrefs } from "@features/series/lib/chapterList"; import { syncBackFromTracker } from "@features/tracking/lib/trackingSync"; import { store } from "@store/state.svelte"; @@ -82,7 +81,7 @@ class TrackingState { for (const tracker of res.trackers.nodes.filter(t => t.isLoggedIn)) { for (const record of tracker.trackRecords.nodes) { if (!record.manga?.id) continue; - const mangaId = record.manga.id; + const mangaId = record.manga.id; const existing = this.byManga.get(mangaId) ?? []; const merged = [...existing.filter(r => r.id !== record.id), record]; this.setFor(mangaId, merged); @@ -140,21 +139,22 @@ class TrackingState { const fresh = res.fetchTrack.trackRecord; this.patchFor(mangaId, fresh); - const { markedRead } = await this._applyRemoteProgress(fresh, chapters, prefs); - return { fresh, markedIds: markedRead }; + const markedIds = await this._applyRemoteProgress(fresh, chapters, prefs); + return { fresh, markedIds }; } private async _applyRemoteProgress( record: TrackRecord, chapters: Chapter[], prefs: ChapterDisplayPrefs, - ): Promise<{ markedRead: number[]; markedUnread: number[] }> { - if (!store.settings.trackerSyncBack) return { markedRead: [], markedUnread: [] }; + ): Promise { + if (!store.settings.trackerSyncBack) return []; return syncBackFromTracker( [record], chapters, { + threshold: store.settings.trackerSyncBackThreshold ?? null, respectScanlatorFilter: store.settings.trackerRespectScanlatorFilter ?? true, chapterPrefs: prefs, }, @@ -290,6 +290,7 @@ class TrackingState { freshRecords, chapters, { + threshold: store.settings.trackerSyncBackThreshold ?? null, respectScanlatorFilter: store.settings.trackerRespectScanlatorFilter ?? true, chapterPrefs: prefs, }, diff --git a/src/types/api.ts b/src/types/api.ts index 8f5d01f..31e5933 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -18,4 +18,45 @@ export interface DownloadStatus { export interface Connection { nodes: T[]; +} + +export interface PageInfo { + hasNextPage: boolean; +} + +export interface PaginatedConnection extends Connection { + pageInfo: PageInfo; + totalCount?: number; +} + +export interface MetaEntry { + key: string; + value: string; +} + +export interface UpdaterJobsInfo { + isRunning: boolean; + finishedJobs: number; + totalJobs: number; + skippedMangasCount: number; + skippedCategoriesCount: number; +} + +export interface UpdateStatus { + jobsInfo: UpdaterJobsInfo; +} + +export interface AboutServer { + name: string; + version: string; + buildType: string; + buildTime: string; + github: string; + discord: string; +} + +export interface ServerUpdateEntry { + channel: string; + tag: string; + url: string; } \ No newline at end of file diff --git a/src/types/chapter.ts b/src/types/chapter.ts index a3763db..cd2770b 100644 --- a/src/types/chapter.ts +++ b/src/types/chapter.ts @@ -8,8 +8,12 @@ export interface Chapter { isBookmarked: boolean; pageCount: number; mangaId: number; + fetchedAt?: string; uploadDate?: string | null; realUrl?: string | null; + url?: string; lastPageRead?: number; + lastReadAt?: string; scanlator?: string | null; -} + manga?: { id: number; title: string; thumbnailUrl: string; inLibrary: boolean } | null; +} \ No newline at end of file diff --git a/src/types/extension.ts b/src/types/extension.ts index cf1d424..b8fbe6c 100644 --- a/src/types/extension.ts +++ b/src/types/extension.ts @@ -5,6 +5,9 @@ export interface Source { displayName: string; iconUrl: string; isNsfw: boolean; + isConfigurable: boolean; + supportsLatest: boolean; + baseUrl?: string | null; } export interface Extension { @@ -17,4 +20,4 @@ export interface Extension { isObsolete: boolean; hasUpdate: boolean; iconUrl: string; -} +} \ No newline at end of file diff --git a/src/types/manga.ts b/src/types/manga.ts index df3f2ad..0401650 100644 --- a/src/types/manga.ts +++ b/src/types/manga.ts @@ -8,20 +8,45 @@ export interface Category { mangas?: { nodes: Manga[] }; } +export interface ChapterRef { + id: number; + chapterNumber: number; + uploadDate?: string; + lastPageRead?: number; +} + export interface Manga { id: number; title: string; thumbnailUrl: string; inLibrary: boolean; + initialized?: boolean; downloadCount?: number; unreadCount?: number; - chapterCount?: number; + bookmarkCount?: number; + hasDuplicateChapters?: boolean; + chapters?: { totalCount: number }; description?: string | null; status?: string | null; author?: string | null; artist?: string | null; genre?: string[]; realUrl?: string | null; + url?: string; + sourceId?: string; + inLibraryAt?: string | null; + lastFetchedAt?: string | null; + chaptersLastFetchedAt?: string | null; + thumbnailUrlLastFetched?: string | null; + age?: string | null; + chaptersAge?: string | null; + updateStrategy?: "ALWAYS_UPDATE" | "ONLY_FETCH_ONCE"; + latestFetchedChapter?: ChapterRef | null; + latestUploadedChapter?: ChapterRef | null; + latestReadChapter?: ChapterRef | null; + lastReadChapter?: ChapterRef | null; + firstUnreadChapter?: ChapterRef | null; + highestNumberedChapter?: ChapterRef | null; source?: { id: string; name: string; displayName: string } | null; } @@ -31,4 +56,4 @@ export interface MangaDetail extends Manga { artist: string | null; status: string | null; genre: string[]; -} +} \ No newline at end of file diff --git a/src/types/settings.ts b/src/types/settings.ts index ecb4cc1..8aae4a4 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -51,6 +51,7 @@ export interface MangaPrefs { deleteDelayHours: number; maxKeepChapters: number; pauseUpdates: boolean; refreshInterval: "global" | "daily" | "weekly" | "manual"; preferredScanlator: string; scanlatorFilter: string[]; + scanlatorBlacklist: string[]; scanlatorForce: boolean; autoDownloadScanlators: string[]; coverUrl?: string; } @@ -59,6 +60,7 @@ export const DEFAULT_MANGA_PREFS: MangaPrefs = { autoDownload: false, downloadAhead: 0, deleteOnRead: false, deleteDelayHours: 0, maxKeepChapters: 0, pauseUpdates: false, refreshInterval: "global", preferredScanlator: "", scanlatorFilter: [], + scanlatorBlacklist: [], scanlatorForce: false, autoDownloadScanlators: [], }; @@ -102,7 +104,7 @@ export interface Settings { socksProxyEnabled: boolean; socksProxyHost: string; socksProxyPort: string; socksProxyVersion: number; socksProxyUsername: string; socksProxyPassword: string; flareSolverrEnabled: boolean; flareSolverrUrl: string; flareSolverrTimeout: number; - flareSolverrSessionName: string; flareSolverrSessionTtl: number; flareSolverrFallback: boolean; + flareSolverrSessionName: string; flareSolverrSessionTtl: number; flareSolverrAsResponseFallback: boolean; appLockEnabled: boolean; appLockPin: string; customThemes: CustomTheme[]; hiddenCategoryIds: number[]; defaultLibraryCategoryId: number | null; savedIsDefaultCategory: boolean; @@ -145,7 +147,7 @@ export const DEFAULT_SETTINGS: Settings = { socksProxyVersion: 5, socksProxyUsername: "", socksProxyPassword: "", flareSolverrEnabled: false, flareSolverrUrl: "http://localhost:8191", flareSolverrTimeout: 60, flareSolverrSessionName: "moku", - flareSolverrSessionTtl: 15, flareSolverrFallback: false, + flareSolverrSessionTtl: 15, flareSolverrAsResponseFallback: false, appLockEnabled: false, appLockPin: "", customThemes: [], hiddenCategoryIds: [], defaultLibraryCategoryId: null, savedIsDefaultCategory: false, diff --git a/src/types/tracking.ts b/src/types/tracking.ts index 9f06d45..db90e06 100644 --- a/src/types/tracking.ts +++ b/src/types/tracking.ts @@ -8,8 +8,11 @@ export interface Tracker { name: string; icon: string; isLoggedIn: boolean; + isTokenExpired: boolean; authUrl: string | null; supportsPrivateTracking: boolean; + supportsReadingDates: boolean; + supportsTrackDeletion: boolean; scores: string[]; statuses: TrackerStatus[]; } @@ -17,17 +20,21 @@ export interface Tracker { export interface TrackRecord { id: number; trackerId: number; + mangaId: number; remoteId: string; + libraryId: string | null; title: string; status: number; score: number; displayScore: string; lastChapterRead: number; totalChapters: number; - remoteUrl: string | null; - startDate: string | null; - finishDate: string | null; + remoteUrl: string; + startDate: string; + finishDate: string; private: boolean; + manga?: { id: number; title: string; thumbnailUrl: string; inLibrary?: boolean } | null; + tracker?: Pick | null; } export interface TrackSearch { @@ -42,4 +49,4 @@ export interface TrackSearch { startDate: string | null; totalChapters: number; trackingUrl: string | null; -} +} \ No newline at end of file