mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Feat: Shift from Stable to Preview (WIP)
This commit is contained in:
@@ -88,10 +88,10 @@ jobs:
|
|||||||
- name: Download Suwayomi (Linux x64)
|
- name: Download Suwayomi (Linux x64)
|
||||||
run: |
|
run: |
|
||||||
curl -fsSL \
|
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
|
-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
|
mkdir -p suwayomi-extracted
|
||||||
tar -xzf suwayomi-linux.tar.gz -C suwayomi-extracted --strip-components=1
|
tar -xzf suwayomi-linux.tar.gz -C suwayomi-extracted --strip-components=1
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ jobs:
|
|||||||
download_suwayomi() {
|
download_suwayomi() {
|
||||||
local asset="$1" sha="$2" outdir="$3"
|
local asset="$1" sha="$2" outdir="$3"
|
||||||
curl -fsSL \
|
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"
|
-o "${outdir}.tar.gz"
|
||||||
echo "${sha} ${outdir}.tar.gz" | shasum -a 256 -c -
|
echo "${sha} ${outdir}.tar.gz" | shasum -a 256 -c -
|
||||||
mkdir -p "${outdir}"
|
mkdir -p "${outdir}"
|
||||||
@@ -87,13 +87,13 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
download_suwayomi \
|
download_suwayomi \
|
||||||
"Suwayomi-Server-v2.1.1867-macOS-arm64.tar.gz" \
|
"Suwayomi-Server-v2.1.2087-macOS-arm64.tar.gz" \
|
||||||
"c80abdbba29f7895e9556c6c9481368557d5f930b5f69bcb30639ba498925f3c" \
|
"59f73a53a139d5d843e16cab4f3ac425a410add6bee0a60920fa26eb0a4b8a5c" \
|
||||||
"suwayomi-arm64"
|
"suwayomi-arm64"
|
||||||
|
|
||||||
download_suwayomi \
|
download_suwayomi \
|
||||||
"Suwayomi-Server-v2.1.1867-macOS-x64.tar.gz" \
|
"Suwayomi-Server-v2.1.2087-macOS-x64.tar.gz" \
|
||||||
"c7590aeb645dd7135a05b9f3ea1fee384a4abeb465c0b3638d5b738d20dfe174" \
|
"da7e664e4c2615a0b9eac09ee38fe979feee1d6c0b266e19dba1ceea8ae3795c" \
|
||||||
"suwayomi-x64"
|
"suwayomi-x64"
|
||||||
|
|
||||||
- name: Stage Suwayomi sidecars
|
- name: Stage Suwayomi sidecars
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
curl -fsSL \
|
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
|
-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
|
unzip -q suwayomi-windows.zip -d suwayomi-raw
|
||||||
|
|
||||||
- name: Extract Suwayomi bundle
|
- name: Extract Suwayomi bundle
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ EOF
|
|||||||
|
|
||||||
install -Dm755 /dev/stdin "$pkgdir/usr/bin/moku-suwayomi" << 'EOF'
|
install -Dm755 /dev/stdin "$pkgdir/usr/bin/moku-suwayomi" << 'EOF'
|
||||||
#!/bin/sh
|
#!/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"
|
mkdir -p "$DATA_DIR"
|
||||||
|
|
||||||
if [ ! -f "$DATA_DIR/server.conf" ]; then
|
if [ ! -f "$DATA_DIR/server.conf" ]; then
|
||||||
|
|||||||
@@ -32,11 +32,20 @@ In-Progress:
|
|||||||
- Tracking
|
- Tracking
|
||||||
- Fix SeriesDetail Tracking Window (Maybe Link to TrackingPanel)
|
- Fix SeriesDetail Tracking Window (Maybe Link to TrackingPanel)
|
||||||
|
|
||||||
- Integrate Tauri JSON for Settings
|
- Apply Syer's Fix for Library on Backup Load (Manga Metadata)
|
||||||
- Create Migration Logic for Local Storage
|
- 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:
|
Notes from last time:
|
||||||
- Storage has been configured, now just need protocols
|
- Storage has been configured, now just need protocols
|
||||||
- Export/Import
|
- Export/Import
|
||||||
- Migration
|
- Migration
|
||||||
- Data-Clean
|
- Data-Clean
|
||||||
|
|
||||||
|
- MAJOR Migration
|
||||||
|
- Completed GQL Migration
|
||||||
|
- Completed Types Migration (May need Refactor)
|
||||||
|
- Completed Shared Migration
|
||||||
|
- Completed Features Migration
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
description = "Moku — manga reader frontend for Suwayomi";
|
description = "Moku — manga reader frontend for Suwayomi";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
crane.url = "github:ipetkov/crane";
|
crane.url = "github:ipetkov/crane";
|
||||||
rust-overlay = {
|
rust-overlay = {
|
||||||
url = "github:oxalica/rust-overlay";
|
url = "github:oxalica/rust-overlay";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -14,9 +14,13 @@
|
|||||||
outputs =
|
outputs =
|
||||||
inputs@{ flake-parts, crane, rust-overlay, ... }:
|
inputs@{ flake-parts, crane, rust-overlay, ... }:
|
||||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
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
|
let
|
||||||
version = "0.9.1";
|
version = "0.9.1";
|
||||||
|
|
||||||
@@ -26,7 +30,10 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||||
extensions = [ "rust-src" "rust-analyzer" ];
|
extensions = [
|
||||||
|
"rust-src"
|
||||||
|
"rust-analyzer"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||||
@@ -46,10 +53,14 @@
|
|||||||
gsettings-desktop-schemas
|
gsettings-desktop-schemas
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# ── source filters ──────────────────────────────────────────────
|
||||||
|
|
||||||
frontendSrc = lib.cleanSourceWith {
|
frontendSrc = lib.cleanSourceWith {
|
||||||
src = ./.;
|
src = ./.;
|
||||||
filter = path: type:
|
filter =
|
||||||
let base = builtins.baseNameOf path;
|
path: type:
|
||||||
|
let
|
||||||
|
base = builtins.baseNameOf path;
|
||||||
in
|
in
|
||||||
(lib.hasInfix "/src" path)
|
(lib.hasInfix "/src" path)
|
||||||
|| base == "index.html"
|
|| base == "index.html"
|
||||||
@@ -59,234 +70,47 @@
|
|||||||
|| base == "vite.config.ts";
|
|| 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 {
|
cargoSrc = lib.cleanSourceWith {
|
||||||
src = ./src-tauri;
|
src = ./src-tauri;
|
||||||
filter = path: type:
|
filter =
|
||||||
|
path: type:
|
||||||
(craneLib.filterCargoSources path type)
|
(craneLib.filterCargoSources path type)
|
||||||
|| (lib.hasInfix "/icons/" path)
|
|| (lib.hasInfix "/icons/" path)
|
||||||
|| (lib.hasInfix "/capabilities/" path)
|
|| (lib.hasInfix "/capabilities/" path)
|
||||||
|| (builtins.baseNameOf path == "tauri.conf.json");
|
|| (builtins.baseNameOf path == "tauri.conf.json");
|
||||||
};
|
};
|
||||||
|
|
||||||
commonArgs = {
|
# ── packages ────────────────────────────────────────────────────
|
||||||
src = cargoSrc;
|
|
||||||
cargoToml = ./src-tauri/Cargo.toml;
|
suwayomiServer = pkgs.callPackage ./nix/server.nix { };
|
||||||
cargoLock = ./src-tauri/Cargo.lock;
|
|
||||||
strictDeps = true;
|
frontend = pkgs.callPackage ./nix/frontend.nix {
|
||||||
buildInputs = runtimeLibs;
|
inherit version;
|
||||||
nativeBuildInputs = with pkgs; [ pkg-config wrapGAppsHook3 ];
|
src = frontendSrc;
|
||||||
preBuild = ''
|
|
||||||
cp -r ${frontend} ../dist
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
moku = import ./nix/moku.nix {
|
||||||
|
inherit lib craneLib pkgs runtimeLibs frontend suwayomiServer version cargoSrc;
|
||||||
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 -- <version>"; 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"
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
postTagBumpScript = pkgs.writeShellApplication {
|
# ── dev/release scripts ─────────────────────────────────────────
|
||||||
name = "moku-post-tag-bump";
|
|
||||||
runtimeInputs = with pkgs; [ gnused coreutils git curl ];
|
|
||||||
text = ''
|
|
||||||
[[ $# -lt 1 ]] && { echo "Usage: nix run .#post-tag-bump -- <version>"; 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 ──"
|
scripts = import ./nix/scripts.nix { inherit pkgs rustToolchain 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 -- <version>"; 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"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
apps = {
|
packages = {
|
||||||
default = { type = "app"; program = "${moku}/bin/moku"; };
|
inherit moku frontend suwayomiServer;
|
||||||
moku = { type = "app"; program = "${moku}/bin/moku"; };
|
default = 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 = {
|
apps = {
|
||||||
inherit moku frontend;
|
default = { type = "app"; program = "${moku}/bin/moku"; };
|
||||||
default = 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 {
|
devShells.default = pkgs.mkShell {
|
||||||
@@ -297,10 +121,13 @@ EOF
|
|||||||
wrapGAppsHook3
|
wrapGAppsHook3
|
||||||
nodejs_22
|
nodejs_22
|
||||||
pnpm
|
pnpm
|
||||||
suwayomi-server
|
suwayomiServer
|
||||||
cloudflared
|
cloudflared
|
||||||
xdg-utils
|
xdg-utils
|
||||||
(python3.withPackages (ps: [ ps.aiohttp ps.tomlkit ]))
|
(python3.withPackages (ps: [
|
||||||
|
ps.aiohttp
|
||||||
|
ps.tomlkit
|
||||||
|
]))
|
||||||
];
|
];
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export NO_STRIP=true
|
export NO_STRIP=true
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ modules:
|
|||||||
cat > /app/bin/tachidesk-server << 'EOF'
|
cat > /app/bin/tachidesk-server << 'EOF'
|
||||||
#!/bin/sh
|
#!/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"
|
mkdir -p "$DATA_DIR"
|
||||||
|
|
||||||
# Seed conf on first run
|
# Seed conf on first run
|
||||||
@@ -155,8 +155,8 @@ modules:
|
|||||||
|
|
||||||
sources:
|
sources:
|
||||||
- type: file
|
- type: file
|
||||||
url: https://github.com/Suwayomi/Suwayomi-Server/releases/download/v2.1.1867/suwayomi-server-v2.1.1867.jar
|
url: https://github.com/Suwayomi/Suwayomi-Server-preview/releases/download/v2.1.2087/Suwayomi-Server-v2.1.2087.jar
|
||||||
sha256: 51e307c2581e4e1a002991ab3e3a77503c8b074c42695987a984a7382d0ac5af
|
sha256: f589a422674252394c13b289a9c8be691905bf583efb7f4d5f1501ae5e91e6b3
|
||||||
dest-filename: Suwayomi-Server.jar
|
dest-filename: Suwayomi-Server.jar
|
||||||
|
|
||||||
- name: moku
|
- name: moku
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
'';
|
||||||
|
})
|
||||||
+133
@@ -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 -- <version>"; 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 -- <version>"; 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 -- <version>"; 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"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
};
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/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
|
# Tauri resolves this via resolve_server_binary() in lib.rs, which looks for
|
||||||
# "suwayomi-launcher" or "suwayomi-launcher.sh" in the resource directory.
|
# "suwayomi-launcher" or "suwayomi-launcher.sh" in the resource directory.
|
||||||
set -e
|
set -e
|
||||||
@@ -53,7 +53,7 @@ if [ ! -f "$JAR" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Data directory ─────────────────────────────────────────────────────────────
|
# ── 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"
|
mkdir -p "$DATA_DIR"
|
||||||
|
|
||||||
# ── Seed server.conf on first run ──────────────────────────────────────────────
|
# ── Seed server.conf on first run ──────────────────────────────────────────────
|
||||||
|
|||||||
@@ -21,20 +21,20 @@ pub fn suwayomi_data_dir() -> PathBuf {
|
|||||||
{
|
{
|
||||||
dirs::data_dir()
|
dirs::data_dir()
|
||||||
.unwrap_or_else(|| PathBuf::from("C:\\ProgramData"))
|
.unwrap_or_else(|| PathBuf::from("C:\\ProgramData"))
|
||||||
.join("moku\\tachidesk")
|
.join("Tachidesk")
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
dirs::data_dir()
|
dirs::data_dir()
|
||||||
.unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("~")))
|
.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")))]
|
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||||
{
|
{
|
||||||
let base = std::env::var("XDG_DATA_HOME")
|
let base = std::env::var("XDG_DATA_HOME")
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.unwrap_or_else(|_| dirs::data_dir().unwrap_or_else(|| PathBuf::from("/tmp")));
|
.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<std::fs::File>) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn data_root_args() -> Vec<String> {
|
||||||
|
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(
|
pub fn resolve_server_binary(
|
||||||
binary: &str,
|
binary: &str,
|
||||||
app: &tauri::AppHandle,
|
app: &tauri::AppHandle,
|
||||||
@@ -81,7 +89,7 @@ pub fn resolve_server_binary(
|
|||||||
if path.exists() {
|
if path.exists() {
|
||||||
return Ok(ServerInvocation {
|
return Ok(ServerInvocation {
|
||||||
bin: path.to_string_lossy().into_owned(),
|
bin: path.to_string_lossy().into_owned(),
|
||||||
args: vec![],
|
args: data_root_args(),
|
||||||
working_dir: path.parent().map(|p| p.to_path_buf()),
|
working_dir: path.parent().map(|p| p.to_path_buf()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -99,7 +107,7 @@ pub fn resolve_server_binary(
|
|||||||
if p.exists() {
|
if p.exists() {
|
||||||
return Ok(ServerInvocation {
|
return Ok(ServerInvocation {
|
||||||
bin: p.to_string_lossy().into_owned(),
|
bin: p.to_string_lossy().into_owned(),
|
||||||
args: vec![],
|
args: data_root_args(),
|
||||||
working_dir: Some(bin_dir.to_path_buf()),
|
working_dir: Some(bin_dir.to_path_buf()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -138,7 +146,7 @@ pub fn resolve_server_binary(
|
|||||||
do_log(log, "[resolve] using bundled JRE");
|
do_log(log, "[resolve] using bundled JRE");
|
||||||
return Ok(ServerInvocation {
|
return Ok(ServerInvocation {
|
||||||
bin: java.to_string_lossy().into_owned(),
|
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),
|
working_dir: Some(bundle_dir),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -158,7 +166,7 @@ pub fn resolve_server_binary(
|
|||||||
if p.exists() {
|
if p.exists() {
|
||||||
return Ok(ServerInvocation {
|
return Ok(ServerInvocation {
|
||||||
bin: p.to_string_lossy().into_owned(),
|
bin: p.to_string_lossy().into_owned(),
|
||||||
args: vec![],
|
args: data_root_args(),
|
||||||
working_dir: Some(resource_dir.clone()),
|
working_dir: Some(resource_dir.clone()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -182,7 +190,7 @@ pub fn resolve_server_binary(
|
|||||||
);
|
);
|
||||||
return Ok(ServerInvocation {
|
return Ok(ServerInvocation {
|
||||||
bin: java.to_string_lossy().into_owned(),
|
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),
|
working_dir: Some(resource_dir),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -233,7 +241,7 @@ pub fn resolve_server_binary(
|
|||||||
do_log(log, &format!("[resolve] found native binary: {:?}", p));
|
do_log(log, &format!("[resolve] found native binary: {:?}", p));
|
||||||
found_binary = Some(ServerInvocation {
|
found_binary = Some(ServerInvocation {
|
||||||
bin: p.to_string_lossy().into_owned(),
|
bin: p.to_string_lossy().into_owned(),
|
||||||
args: vec![],
|
args: data_root_args(),
|
||||||
working_dir: Some(dir.clone()),
|
working_dir: Some(dir.clone()),
|
||||||
});
|
});
|
||||||
break 'outer;
|
break 'outer;
|
||||||
@@ -288,7 +296,7 @@ pub fn resolve_server_binary(
|
|||||||
let working_dir = jar.parent().map(|p| p.to_path_buf());
|
let working_dir = jar.parent().map(|p| p.to_path_buf());
|
||||||
return Ok(ServerInvocation {
|
return Ok(ServerInvocation {
|
||||||
bin: java.to_string_lossy().into_owned(),
|
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,
|
working_dir,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -298,21 +306,26 @@ pub fn resolve_server_binary(
|
|||||||
|
|
||||||
for name in &["suwayomi-server", "tachidesk-server"] {
|
for name in &["suwayomi-server", "tachidesk-server"] {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let found = std::process::Command::new("where")
|
let resolved = std::process::Command::new("where")
|
||||||
.arg(name)
|
.arg(name)
|
||||||
.output()
|
.output()
|
||||||
.map(|o| o.status.success())
|
.ok()
|
||||||
.unwrap_or(false);
|
.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"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
let found = std::process::Command::new("which")
|
let resolved = std::process::Command::new("which")
|
||||||
.arg(name)
|
.arg(name)
|
||||||
.output()
|
.output()
|
||||||
.map(|o| o.status.success())
|
.ok()
|
||||||
.unwrap_or(false);
|
.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 {
|
return Ok(ServerInvocation {
|
||||||
bin: name.to_string(),
|
bin: bin_path,
|
||||||
args: vec![],
|
args: vec![],
|
||||||
working_dir: None,
|
working_dir: None,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export const FETCH_CHAPTERS = `
|
|||||||
fetchChapters(input: { mangaId: $mangaId }) {
|
fetchChapters(input: { mangaId: $mangaId }) {
|
||||||
chapters {
|
chapters {
|
||||||
id name chapterNumber sourceOrder isRead isDownloaded isBookmarked
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -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 = `
|
export const INSTALL_EXTERNAL_EXTENSION = `
|
||||||
mutation InstallExternalExtension($url: String!) {
|
mutation InstallExternalExtension($url: String!) {
|
||||||
installExternalExtension(input: { extensionUrl: $url }) {
|
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 = `
|
export const SET_EXTENSION_REPOS = `
|
||||||
mutation SetExtensionRepos($repos: [String!]!) {
|
mutation SetExtensionRepos($repos: [String!]!) {
|
||||||
setSettings(input: { settings: { extensionRepos: $repos } }) {
|
setSettings(input: { settings: { extensionRepos: $repos } }) {
|
||||||
|
|||||||
@@ -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 = `
|
export const CREATE_CATEGORY = `
|
||||||
mutation CreateCategory($name: String!) {
|
mutation CreateCategory($name: String!) {
|
||||||
createCategory(input: { name: $name }) {
|
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 = `
|
export const DELETE_CATEGORY = `
|
||||||
mutation DeleteCategory($id: Int!) {
|
mutation DeleteCategory($id: Int!) {
|
||||||
deleteCategory(input: { categoryId: $id }) {
|
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 = `
|
export const UPDATE_LIBRARY = `
|
||||||
mutation UpdateLibrary {
|
mutation UpdateLibrary {
|
||||||
updateLibrary(input: {}) {
|
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 = `
|
export const CREATE_BACKUP = `
|
||||||
mutation CreateBackup {
|
mutation CreateBackup {
|
||||||
createBackup(input: {}) { url }
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
+101
-421
@@ -2,449 +2,129 @@
|
|||||||
|
|
||||||
## Manga (`mutations/manga.ts`)
|
## Manga (`mutations/manga.ts`)
|
||||||
|
|
||||||
### `FETCH_MANGA`
|
| Mutation | Variables | Description |
|
||||||
Fetches and refreshes manga metadata from its source.
|
|----------|-----------|-------------|
|
||||||
|
| `FETCH_MANGA` | `id: Int!` | Fetch and refresh manga metadata from its source |
|
||||||
**Variables:**
|
| `UPDATE_MANGA` | `id: Int!`, `inLibrary: Boolean` | Update a single manga's library membership |
|
||||||
| Name | Type | Description |
|
| `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 |
|
||||||
| `id` | `Int!` | Manga ID |
|
| `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 |
|
||||||
### `UPDATE_MANGA`
|
| `DELETE_CATEGORY` | `id: Int!` | Delete a category |
|
||||||
Updates a single manga's library membership.
|
| `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 |
|
||||||
**Variables:**
|
| `UPDATE_LIBRARY` | — | Trigger a full library metadata refresh |
|
||||||
| Name | Type | Description |
|
| `UPDATE_LIBRARY_MANGA` | `mangaId: Int!` | Trigger a metadata update for a single manga |
|
||||||
|------|------|-------------|
|
| `UPDATE_STOP` | — | Stop the currently running library update job |
|
||||||
| `id` | `Int!` | Manga ID |
|
| `CREATE_BACKUP` | — | Create a backup and return its download URL |
|
||||||
| `inLibrary` | `Boolean` | Add/remove from library |
|
| `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 |
|
||||||
|
|
||||||
### `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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chapters (`mutations/chapters.ts`)
|
## Chapters (`mutations/chapters.ts`)
|
||||||
|
|
||||||
### `FETCH_CHAPTERS`
|
| Mutation | Variables | Description |
|
||||||
Fetches/refreshes the chapter list for a manga from its source.
|
|----------|-----------|-------------|
|
||||||
|
| `FETCH_CHAPTERS` | `mangaId: Int!` | Fetch/refresh the chapter list for a manga from its source |
|
||||||
**Variables:**
|
| `FETCH_CHAPTER_PAGES` | `chapterId: Int!` | Fetch the page URLs for a specific chapter |
|
||||||
| Name | Type | Description |
|
| `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 |
|
||||||
| `mangaId` | `Int!` | Manga ID |
|
| `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 |
|
||||||
### `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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Downloads (`mutations/downloads.ts`)
|
## Downloads (`mutations/downloads.ts`)
|
||||||
|
|
||||||
### `ENQUEUE_DOWNLOAD`
|
| Mutation | Variables | Description |
|
||||||
Adds a single chapter to the download queue.
|
|----------|-----------|-------------|
|
||||||
|
| `ENQUEUE_DOWNLOAD` | `chapterId: Int!` | Add a single chapter to the download queue |
|
||||||
**Variables:**
|
| `ENQUEUE_CHAPTERS_DOWNLOAD` | `chapterIds: [Int!]!` | Add multiple chapters to the download queue |
|
||||||
| Name | Type | Description |
|
| `DEQUEUE_DOWNLOAD` | `chapterId: Int!` | Remove a single chapter from the download queue |
|
||||||
|------|------|-------------|
|
| `DEQUEUE_CHAPTERS_DOWNLOAD` | `chapterIds: [Int!]!` | Remove multiple chapters from the download queue |
|
||||||
| `chapterId` | `Int!` | Chapter ID |
|
| `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 |
|
||||||
### `ENQUEUE_CHAPTERS_DOWNLOAD`
|
| `FETCH_SOURCE_MANGA` | `source: LongString!`, `type: FetchSourceMangaType!`, `page: Int!`, `query: String`, `filters: [FilterChangeInput!]` | Fetch manga from a source (browse/search) with pagination |
|
||||||
Adds multiple chapters to the download queue.
|
| `SET_DOWNLOADS_PATH` | `path: String!` | Set the downloads directory path |
|
||||||
|
| `SET_LOCAL_SOURCE_PATH` | `path: String!` | Set the local source directory path |
|
||||||
**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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Extensions (`mutations/extensions.ts`)
|
## Extensions (`mutations/extensions.ts`)
|
||||||
|
|
||||||
### `FETCH_EXTENSIONS`
|
| Mutation | Variables | Description |
|
||||||
Fetches the latest extension list from configured repos.
|
|----------|-----------|-------------|
|
||||||
|
| `FETCH_EXTENSIONS` | — | Fetch the latest extension list from configured repos |
|
||||||
**Variables:** none
|
| `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 |
|
||||||
### `UPDATE_EXTENSION`
|
| `SET_SOURCE_META` | `sourceId: LongString!`, `key: String!`, `value: String!` | Set a key/value meta entry on a source |
|
||||||
Installs, uninstalls, or updates an extension.
|
| `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 |
|
||||||
**Variables:**
|
| `DELETE_CATEGORY_META` | `categoryId: Int!`, `key: String!` | Delete a key/value meta entry from a category |
|
||||||
| Name | Type | Description |
|
| `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 |
|
||||||
| `id` | `String!` | Extension package name |
|
| `CLEAR_CACHED_IMAGES` | `cachedPages: Boolean`, `cachedThumbnails: Boolean`, `downloadedThumbnails: Boolean` | Selectively clear cached page images, cached thumbnails, or downloaded thumbnails |
|
||||||
| `install` | `Boolean` | Install the extension |
|
| `RESET_SETTINGS` | — | Reset all server settings to defaults |
|
||||||
| `uninstall` | `Boolean` | Uninstall the extension |
|
| `UPDATE_WEBUI` | — | Trigger a WebUI update and return live status |
|
||||||
| `update` | `Boolean` | Update the extension |
|
| `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 |
|
||||||
### `INSTALL_EXTERNAL_EXTENSION`
|
| `SET_FLARESOLVERR` | `flareSolverrEnabled: Boolean!`, `flareSolverrUrl: String!`, `flareSolverrTimeout: Int!`, `flareSolverrSessionName: String!`, `flareSolverrSessionTtl: Int!`, `flareSolverrAsResponseFallback: Boolean!` | Configure FlareSolverr integration |
|
||||||
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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Tracking (`mutations/tracking.ts`)
|
## Tracking (`mutations/tracking.ts`)
|
||||||
|
|
||||||
### `BIND_TRACK`
|
| Mutation | Variables | Description |
|
||||||
Binds a manga to a remote tracker entry.
|
|----------|-----------|-------------|
|
||||||
|
| `BIND_TRACK` | `mangaId: Int!`, `trackerId: Int!`, `remoteId: LongString!` | Bind a manga to a remote tracker entry |
|
||||||
**Variables:**
|
| `UPDATE_TRACK` | `recordId: Int!`, `status: Int`, `lastChapterRead: Float`, `scoreString: String`, `startDate: LongString`, `finishDate: LongString`, `private: Boolean` | Update tracking progress, status, score, and dates |
|
||||||
| Name | Type | Description |
|
| `UNBIND_TRACK` | `recordId: Int!` | Unbind a manga from a tracker record |
|
||||||
|------|------|-------------|
|
| `FETCH_TRACK` | `recordId: Int!` | Refresh a track record from the remote tracker |
|
||||||
| `mangaId` | `Int!` | Manga ID |
|
| `TRACK_PROGRESS` | `mangaId: Int!` | Sync current reading progress to all bound trackers for a manga |
|
||||||
| `trackerId` | `Int!` | Tracker ID |
|
| `LOGIN_TRACKER_OAUTH` | `trackerId: Int!`, `callbackUrl: String!` | Initiate OAuth login for a tracker |
|
||||||
| `remoteId` | `LongString!` | Remote entry ID on the 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`
|
## New in Preview
|
||||||
Updates tracking progress, status, score, and dates for a track record.
|
|
||||||
|
|
||||||
**Variables:**
|
Mutations now available and not yet wired to any feature in Moku:
|
||||||
| 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 |
|
|
||||||
|
|
||||||
---
|
| Mutation | Potential Feature |
|
||||||
|
|----------|-------------------|
|
||||||
### `UNBIND_TRACK`
|
| `UPDATE_MANGAS_CATEGORIES` | Bulk category editor — move/assign multiple manga at once |
|
||||||
Unbinds a manga from a tracker record.
|
| `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 |
|
||||||
**Variables:**
|
| `UPDATE_LIBRARY_MANGA` | Single manga refresh — trigger from series detail without a full library update |
|
||||||
| Name | Type | Description |
|
| `UPDATE_STOP` | Cancel button for library update jobs |
|
||||||
|------|------|-------------|
|
| `UPDATE_EXTENSIONS` | Bulk extension updater — "update all" button in extensions page |
|
||||||
| `recordId` | `Int!` | Track record ID |
|
| `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 |
|
||||||
### `FETCH_TRACK`
|
| `SET_GLOBAL_META` / `DELETE_GLOBAL_META` | Server-synced app state — replace local persistence for settings that should roam |
|
||||||
Refreshes a track record from the remote tracker.
|
| `CLEAR_CACHED_IMAGES` | Storage settings — granular cache clearing (pages, thumbnails, downloaded) |
|
||||||
|
| `RESET_SETTINGS` | Settings page — factory reset button |
|
||||||
**Variables:**
|
| `UPDATE_WEBUI` / `RESET_WEBUI_UPDATE_STATUS` | WebUI update flow in settings — trigger and monitor update progress |
|
||||||
| Name | Type | Description |
|
| `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 |
|
||||||
| `recordId` | `Int!` | Track record ID |
|
| `PULL_KOSYNC_PROGRESS` / `PUSH_KOSYNC_PROGRESS` | KOReader sync — manual pull/push per chapter, or auto-sync on chapter open/close |
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `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
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
const TRACK_RECORD_FRAGMENT = `
|
const TRACK_RECORD_FRAGMENT = `
|
||||||
id trackerId remoteId title status score displayScore
|
id trackerId remoteId title status score displayScore
|
||||||
lastChapterRead totalChapters remoteUrl startDate finishDate private
|
lastChapterRead totalChapters remoteUrl startDate finishDate private libraryId
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const BIND_TRACK = `
|
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) {
|
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 }) {
|
updateTrack(input: { recordId: $recordId, status: $status, lastChapterRead: $lastChapterRead, scoreString: $scoreString, startDate: $startDate, finishDate: $finishDate, private: $private }) {
|
||||||
trackRecord {
|
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!) {
|
mutation FetchTrack($recordId: Int!) {
|
||||||
fetchTrack(input: { recordId: $recordId }) {
|
fetchTrack(input: { recordId: $recordId }) {
|
||||||
trackRecord {
|
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!) {
|
mutation LoginTrackerOAuth($trackerId: Int!, $callbackUrl: String!) {
|
||||||
loginTrackerOAuth(input: { trackerId: $trackerId, callbackUrl: $callbackUrl }) {
|
loginTrackerOAuth(input: { trackerId: $trackerId, callbackUrl: $callbackUrl }) {
|
||||||
isLoggedIn
|
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!) {
|
mutation LoginTrackerCredentials($trackerId: Int!, $username: String!, $password: String!) {
|
||||||
loginTrackerCredentials(input: { trackerId: $trackerId, username: $username, password: $password }) {
|
loginTrackerCredentials(input: { trackerId: $trackerId, username: $username, password: $password }) {
|
||||||
isLoggedIn
|
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 = `
|
export const LOGOUT_TRACKER = `
|
||||||
mutation LogoutTracker($trackerId: Int!) {
|
mutation LogoutTracker($trackerId: Int!) {
|
||||||
logoutTracker(input: { trackerId: $trackerId }) {
|
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 = `
|
export const REFRESH_TOKEN = `
|
||||||
mutation RefreshToken {
|
mutation RefreshToken {
|
||||||
refreshToken { accessToken }
|
refreshToken(input: {}) { accessToken }
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -15,7 +15,7 @@ export const GET_CHAPTERS = `
|
|||||||
chapters(condition: { mangaId: $mangaId }) {
|
chapters(condition: { mangaId: $mangaId }) {
|
||||||
nodes {
|
nodes {
|
||||||
id name chapterNumber sourceOrder isRead isDownloaded isBookmarked
|
id name chapterNumber sourceOrder isRead isDownloaded isBookmarked
|
||||||
pageCount mangaId uploadDate realUrl lastPageRead scanlator
|
pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ export const GET_EXTENSIONS = `
|
|||||||
export const GET_SOURCES = `
|
export const GET_SOURCES = `
|
||||||
query GetSources {
|
query GetSources {
|
||||||
sources {
|
sources {
|
||||||
nodes { id name lang displayName iconUrl isNsfw }
|
nodes {
|
||||||
|
id name lang displayName iconUrl isNsfw
|
||||||
|
isConfigurable supportsLatest baseUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ export * from "./chapters";
|
|||||||
export * from "./downloads";
|
export * from "./downloads";
|
||||||
export * from "./extensions";
|
export * from "./extensions";
|
||||||
export * from "./tracking";
|
export * from "./tracking";
|
||||||
|
export * from "./updater";
|
||||||
|
export * from "./meta";
|
||||||
@@ -2,10 +2,15 @@ export const GET_LIBRARY = `
|
|||||||
query GetLibrary {
|
query GetLibrary {
|
||||||
mangas(condition: { inLibrary: true }) {
|
mangas(condition: { inLibrary: true }) {
|
||||||
nodes {
|
nodes {
|
||||||
id title thumbnailUrl inLibrary downloadCount unreadCount
|
id title thumbnailUrl inLibrary downloadCount unreadCount bookmarkCount
|
||||||
description status author artist genre
|
description status author artist genre
|
||||||
|
inLibraryAt lastFetchedAt chaptersLastFetchedAt thumbnailUrlLastFetched
|
||||||
source { id name displayName }
|
source { id name displayName }
|
||||||
chapters { totalCount }
|
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!) {
|
query GetManga($id: Int!) {
|
||||||
manga(id: $id) {
|
manga(id: $id) {
|
||||||
id title description thumbnailUrl status author artist genre inLibrary realUrl
|
id title description thumbnailUrl status author artist genre inLibrary realUrl
|
||||||
|
inLibraryAt lastFetchedAt thumbnailUrlLastFetched updateStrategy
|
||||||
source { id name displayName }
|
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 = `
|
export const LIBRARY_UPDATE_STATUS = `
|
||||||
query LibraryUpdateStatus {
|
query LibraryUpdateStatus {
|
||||||
libraryUpdateStatus {
|
libraryUpdateStatus {
|
||||||
jobsInfo { isRunning finishedJobs totalJobs skippedMangasCount }
|
jobsInfo {
|
||||||
|
isRunning finishedJobs totalJobs skippedMangasCount skippedCategoriesCount
|
||||||
|
}
|
||||||
mangaUpdates {
|
mangaUpdates {
|
||||||
status
|
status
|
||||||
manga { id title thumbnailUrl unreadCount }
|
manga { id title thumbnailUrl unreadCount }
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
+77
-131
@@ -2,170 +2,116 @@
|
|||||||
|
|
||||||
## Manga (`queries/manga.ts`)
|
## Manga (`queries/manga.ts`)
|
||||||
|
|
||||||
### `GET_LIBRARY`
|
| Query | Variables | Description |
|
||||||
Fetches all manga marked as in-library, including metadata, source info, chapter count, download count, and unread count.
|
|-------|-----------|-------------|
|
||||||
|
| `GET_LIBRARY` | — | All in-library manga with metadata, source, chapter counts, download count, unread count, bookmark count, and read progress anchors (`lastReadChapter`, `firstUnreadChapter`) |
|
||||||
**Variables:** none
|
| `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_ALL_MANGA`
|
| `GET_DOWNLOADS_PATH` | — | `downloadsPath` and `localSourcePath` from settings |
|
||||||
Fetches all manga (library and non-library) with minimal fields.
|
| `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` |
|
||||||
**Variables:** none
|
| `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` |
|
||||||
---
|
|
||||||
|
|
||||||
### `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 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Chapters (`queries/chapters.ts`)
|
## Chapters (`queries/chapters.ts`)
|
||||||
|
|
||||||
### `GET_CHAPTERS`
|
| Query | Variables | Description |
|
||||||
Fetches all chapters for a given manga, including read/download/bookmark state and page info.
|
|-------|-----------|-------------|
|
||||||
|
| `GET_RECENTLY_UPDATED` | — | Latest 300 chapters ordered by `FETCHED_AT DESC` with parent manga info |
|
||||||
**Variables:**
|
| `GET_CHAPTERS` | `mangaId: Int!` | All chapters for a manga — includes `lastReadAt`, `lastPageRead`, read/download/bookmark state, page count, scanlator |
|
||||||
| Name | Type | Description |
|
|
||||||
|------|------|-------------|
|
|
||||||
| `mangaId` | `Int!` | Manga ID |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Downloads (`queries/downloads.ts`)
|
## Downloads (`queries/downloads.ts`)
|
||||||
|
|
||||||
### `GET_DOWNLOAD_STATUS`
|
| Query | Variables | Description |
|
||||||
Fetches the current downloader state and full queue with chapter and manga info.
|
|-------|-----------|-------------|
|
||||||
|
| `GET_DOWNLOAD_STATUS` | — | Downloader state (`DownloaderState` enum) and full queue with chapter and manga info |
|
||||||
**Variables:** none
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Extensions (`queries/extensions.ts`)
|
## Extensions (`queries/extensions.ts`)
|
||||||
|
|
||||||
### `GET_EXTENSIONS`
|
| Query | Variables | Description |
|
||||||
Fetches all extensions with install status, update availability, and metadata.
|
|-------|-----------|-------------|
|
||||||
|
| `GET_LOCAL_MANGA` | — | Manga from the local source (`sourceId: "0"`) |
|
||||||
**Variables:** none
|
| `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 |
|
||||||
### `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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Tracking (`queries/tracking.ts`)
|
## Tracking (`queries/tracking.ts`)
|
||||||
|
|
||||||
### `GET_TRACKERS`
|
| Query | Variables | Description |
|
||||||
Fetches all trackers with login status, supported scores, statuses, and auth info.
|
|-------|-----------|-------------|
|
||||||
|
| `GET_TRACKERS` | — | All trackers with login state, token expiry, capability flags (`supportsPrivateTracking`, `supportsReadingDates`, `supportsTrackDeletion`), scores, and statuses |
|
||||||
**Variables:** none
|
| `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`
|
## Updater (`queries/updater.ts`)
|
||||||
Fetches all tracking records for a specific manga across all trackers.
|
|
||||||
|
|
||||||
**Variables:**
|
| Query | Variables | Description |
|
||||||
| Name | Type | Description |
|
|-------|-----------|-------------|
|
||||||
|------|------|-------------|
|
| `GET_ABOUT_SERVER` | — | Server name, version, build type, build time, GitHub and Discord links |
|
||||||
| `mangaId` | `Int!` | Manga ID |
|
| `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`
|
## Meta (`queries/meta.ts`)
|
||||||
Searches a tracker for manga by query string.
|
|
||||||
|
|
||||||
**Variables:**
|
| Query | Variables | Description |
|
||||||
| Name | Type | Description |
|
|-------|-----------|-------------|
|
||||||
|------|------|-------------|
|
| `GET_META` | `key: String!` | Single server-side key/value meta entry |
|
||||||
| `trackerId` | `Int!` | Tracker ID |
|
| `GET_METAS` | — | All global meta entries as a node list |
|
||||||
| `query` | `String!` | Search query |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `GET_ALL_TRACKER_RECORDS`
|
## KoSync (`queries/kosync.ts`)
|
||||||
Fetches all trackers and their full track records, including associated manga info.
|
|
||||||
|
|
||||||
**Variables:** none
|
| Query | Variables | Description |
|
||||||
|
|-------|-----------|-------------|
|
||||||
|
| `GET_KOSYNC_STATUS` | — | KOReader sync connection status |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `GET_TRACKER_RECORDS`
|
## New in Preview
|
||||||
Fetches track records for a specific tracker.
|
|
||||||
|
|
||||||
**Variables:**
|
Queries and fields now available but not yet wired to any feature in Moku:
|
||||||
| Name | Type | Description |
|
|
||||||
|------|------|-------------|
|
| Query / Field | Potential Feature |
|
||||||
| `trackerId` | `Int!` | Tracker ID |
|
|---------------|-------------------|
|
||||||
|
| `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 |
|
||||||
@@ -2,7 +2,9 @@ export const GET_TRACKERS = `
|
|||||||
query GetTrackers {
|
query GetTrackers {
|
||||||
trackers {
|
trackers {
|
||||||
nodes {
|
nodes {
|
||||||
id name icon isLoggedIn authUrl supportsPrivateTracking scores
|
id name icon isLoggedIn isTokenExpired authUrl
|
||||||
|
supportsPrivateTracking supportsReadingDates supportsTrackDeletion
|
||||||
|
scores
|
||||||
statuses { value name }
|
statuses { value name }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,7 +17,7 @@ export const GET_MANGA_TRACK_RECORDS = `
|
|||||||
trackRecords {
|
trackRecords {
|
||||||
nodes {
|
nodes {
|
||||||
id trackerId remoteId title status score displayScore
|
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 {
|
query GetAllTrackerRecords {
|
||||||
trackers {
|
trackers {
|
||||||
nodes {
|
nodes {
|
||||||
id name icon isLoggedIn scores
|
id name icon isLoggedIn isTokenExpired scores
|
||||||
statuses { value name }
|
statuses { value name }
|
||||||
trackRecords {
|
trackRecords {
|
||||||
nodes {
|
nodes {
|
||||||
id trackerId title status displayScore lastChapterRead
|
id trackerId title status displayScore lastChapterRead
|
||||||
totalChapters remoteUrl private
|
totalChapters remoteUrl private libraryId
|
||||||
manga { id title thumbnailUrl inLibrary }
|
manga { id title thumbnailUrl inLibrary }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
+3
-19
@@ -3,8 +3,6 @@ import type { Settings } from "@types";
|
|||||||
|
|
||||||
export { clsx as cn } from "clsx";
|
export { clsx as cn } from "clsx";
|
||||||
|
|
||||||
// ── Time / formatting ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function timeAgo(ts: number): string {
|
export function timeAgo(ts: number): string {
|
||||||
const diff = Date.now() - ts, m = Math.floor(diff / 60000);
|
const diff = Date.now() - ts, m = Math.floor(diff / 60000);
|
||||||
if (m < 1) return "Just now";
|
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`;
|
return r === 0 ? `${h}h` : `${h}h ${r}m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Content filtering ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const STRICT_TAGS: string[] = [
|
const STRICT_TAGS: string[] = [
|
||||||
"adult", "mature", "hentai", "ecchi", "erotic", "pornograph",
|
"adult", "mature", "hentai", "ecchi", "erotic", "pornograph",
|
||||||
"18+", "smut", "lemon", "explicit", "sexual violence",
|
"18+", "smut", "lemon", "explicit", "sexual violence",
|
||||||
@@ -50,8 +46,8 @@ type ContentFilterSettings = Pick<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
function blockedTagsForSettings(settings: ContentFilterSettings): string[] {
|
function blockedTagsForSettings(settings: ContentFilterSettings): string[] {
|
||||||
if (settings.contentLevel === "strict") return STRICT_TAGS;
|
if (settings.contentLevel === "strict") return STRICT_TAGS;
|
||||||
if (settings.contentLevel === "moderate") return MODERATE_TAGS;
|
if (settings.contentLevel === "moderate") return MODERATE_TAGS;
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,17 +56,13 @@ function genreMatchesBlocklist(genre: string[], blockedTags: string[]): boolean
|
|||||||
return genre.some(g => blockedTags.some(tag => g.toLowerCase().trim().includes(tag)));
|
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(
|
export function shouldHideNsfw(
|
||||||
manga: Pick<Manga, "genre" | "source">,
|
manga: Pick<Manga, "genre" | "source">,
|
||||||
settings: ContentFilterSettings,
|
settings: ContentFilterSettings,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (settings.contentLevel === "unrestricted") return false;
|
if (settings.contentLevel === "unrestricted") return false;
|
||||||
|
|
||||||
const srcId = manga.source?.id;
|
const srcId = manga.source?.id;
|
||||||
const blocked = settings.sourceOverridesEnabled ? (settings.nsfwBlockedSourceIds ?? []) : [];
|
const blocked = settings.sourceOverridesEnabled ? (settings.nsfwBlockedSourceIds ?? []) : [];
|
||||||
const allowed = settings.sourceOverridesEnabled ? (settings.nsfwAllowedSourceIds ?? []) : [];
|
const allowed = settings.sourceOverridesEnabled ? (settings.nsfwAllowedSourceIds ?? []) : [];
|
||||||
|
|
||||||
@@ -83,10 +75,6 @@ export function shouldHideNsfw(
|
|||||||
return genreMatchesBlocklist(manga.genre ?? [], blockedTags);
|
return genreMatchesBlocklist(manga.genre ?? [], blockedTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true when the source should be hidden.
|
|
||||||
* Used in extension lists and source fan-out.
|
|
||||||
*/
|
|
||||||
export function shouldHideSource(
|
export function shouldHideSource(
|
||||||
source: Pick<Source, "id" | "isNsfw">,
|
source: Pick<Source, "id" | "isNsfw">,
|
||||||
settings: ContentFilterSettings,
|
settings: ContentFilterSettings,
|
||||||
@@ -101,8 +89,6 @@ export function shouldHideSource(
|
|||||||
return source.isNsfw && settings.contentLevel === "strict";
|
return source.isNsfw && settings.contentLevel === "strict";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Source deduplication ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function dedupeSourcesByLang(
|
export function dedupeSourcesByLang(
|
||||||
sources: Source[],
|
sources: Source[],
|
||||||
preferredLang: string,
|
preferredLang: string,
|
||||||
@@ -138,8 +124,6 @@ export function dedupeSources(sources: Source[], preferredLang: string): Source[
|
|||||||
return picked;
|
return picked;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Manga deduplication ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function normalizeTitle(title: string): string {
|
export function normalizeTitle(title: string): string {
|
||||||
return title
|
return title
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|||||||
@@ -135,9 +135,9 @@
|
|||||||
|
|
||||||
const f = store.settings.libraryTabFilters?.[tab] ?? {};
|
const f = store.settings.libraryTabFilters?.[tab] ?? {};
|
||||||
if (f.unread) items = items.filter(m => (m.unreadCount ?? 0) > 0);
|
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.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<number, number>();
|
const recentlyReadMap = new Map<number, number>();
|
||||||
if (tabSortMode === "recentlyRead") {
|
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),
|
cache.get(CACHE_KEYS.LIBRARY, () => gql<{ mangas: { nodes: Manga[] } }>(GET_LIBRARY).then(d => d.mangas.nodes), DEFAULT_TTL_MS, CACHE_GROUPS.LIBRARY),
|
||||||
reloadCategories(),
|
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);
|
allManga = dedupeMangaByTitle(dedupeMangaById(mapped), store.settings.mangaLinks);
|
||||||
error = null;
|
error = null;
|
||||||
await migrateCategorizedToLibrary();
|
await migrateCategorizedToLibrary();
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
<div class="grid" style="--cols:{cols}">
|
<div class="grid" style="--cols:{cols}">
|
||||||
{#each visibleManga as m (m.id)}
|
{#each visibleManga as m (m.id)}
|
||||||
{@const isSelected = selectedIds.has(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}
|
||||||
<button
|
<button
|
||||||
class="card"
|
class="card"
|
||||||
class:card-selected={isSelected}
|
class:card-selected={isSelected}
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ export const librarySorter = createSorter<Manga>({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "totalChapters",
|
key: "totalChapters",
|
||||||
comparator: (a, b) => (a.chapterCount ?? 0) - (b.chapterCount ?? 0),
|
comparator: (a, b) => (a.chapters?.totalCount ?? 0) - (b.chapters?.totalCount ?? 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "recentlyAdded",
|
key: "recentlyAdded",
|
||||||
comparator: (a, b) => a.id - b.id,
|
comparator: (a, b) => Number(a.inLibraryAt ?? 0) - Number(b.inLibraryAt ?? 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "recentlyRead",
|
key: "recentlyRead",
|
||||||
@@ -33,11 +33,11 @@ export const librarySorter = createSorter<Manga>({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "latestFetched",
|
key: "latestFetched",
|
||||||
comparator: (a, b) => a.id - b.id,
|
comparator: (a, b) => Number(a.latestFetchedChapter?.uploadDate ?? 0) - Number(b.latestFetchedChapter?.uploadDate ?? 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "latestUploaded",
|
key: "latestUploaded",
|
||||||
comparator: (a, b) => a.id - b.id,
|
comparator: (a, b) => Number(a.latestUploadedChapter?.uploadDate ?? 0) - Number(b.latestUploadedChapter?.uploadDate ?? 0),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { gql } from "@api/client";
|
import { gql } from "@api/client";
|
||||||
import { LIBRARY_UPDATE_STATUS } from "@api/queries/manga";
|
import { LIBRARY_UPDATE_STATUS } from "@api/queries/manga";
|
||||||
import { UPDATE_LIBRARY } from "@api/mutations/manga";
|
import { UPDATE_LIBRARY, FETCH_MANGA } from "@api/mutations/manga";
|
||||||
import { GET_RECENTLY_UPDATED } from "@api/queries/chapters";
|
import { GET_LIBRARY } from "@api/queries/manga";
|
||||||
import type { LibraryUpdateEntry } from "@store/state.svelte";
|
import type { LibraryUpdateEntry } from "@store/state.svelte";
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 3000;
|
const POLL_INTERVAL_MS = 2000;
|
||||||
const POLL_INITIAL_MS = 2000;
|
const POLL_INITIAL_MS = 500;
|
||||||
|
|
||||||
export interface UpdateProgress {
|
export interface UpdateProgress {
|
||||||
finished: number;
|
finished: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
skippedManga: number;
|
||||||
|
skippedCategories: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateResult {
|
export interface UpdateResult {
|
||||||
@@ -21,89 +23,138 @@ export interface UpdateResult {
|
|||||||
export interface LibraryUpdaterCallbacks {
|
export interface LibraryUpdaterCallbacks {
|
||||||
onProgress: (p: UpdateProgress) => void;
|
onProgress: (p: UpdateProgress) => void;
|
||||||
onDone: (r: UpdateResult) => void;
|
onDone: (r: UpdateResult) => void;
|
||||||
onError: () => void;
|
onError: (e?: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshLibraryMetadata(
|
||||||
|
onProgress?: (done: number, total: number) => void,
|
||||||
|
): Promise<void> {
|
||||||
|
const data = await gql<{ mangas: { nodes: { id: number }[] } }>(GET_LIBRARY, {});
|
||||||
|
const ids = data.mangas.nodes.map(m => m.id);
|
||||||
|
let done = 0;
|
||||||
|
for (const id of ids) {
|
||||||
|
try {
|
||||||
|
await gql(FETCH_MANGA, { id });
|
||||||
|
} catch {}
|
||||||
|
onProgress?.(++done, ids.length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startLibraryUpdate(callbacks: LibraryUpdaterCallbacks): () => void {
|
export function startLibraryUpdate(callbacks: LibraryUpdaterCallbacks): () => void {
|
||||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const startedAt = Math.floor(Date.now() / 1000);
|
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
if (timer) { clearTimeout(timer); timer = null; }
|
if (timer) { clearTimeout(timer); timer = null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildEntries(
|
||||||
|
mangaUpdates: { status: string; manga: { id: number; title: string; thumbnailUrl: string; unreadCount: number } }[]
|
||||||
|
): LibraryUpdateEntry[] {
|
||||||
|
const byManga = new Map<number, LibraryUpdateEntry>();
|
||||||
|
for (const u of mangaUpdates) {
|
||||||
|
if (u.status !== "UPDATED") continue;
|
||||||
|
const existing = byManga.get(u.manga.id);
|
||||||
|
if (existing) {
|
||||||
|
existing.newChapters++;
|
||||||
|
} else {
|
||||||
|
byManga.set(u.manga.id, {
|
||||||
|
mangaId: u.manga.id,
|
||||||
|
mangaTitle: u.manga.title,
|
||||||
|
thumbnailUrl: u.manga.thumbnailUrl,
|
||||||
|
newChapters: 1,
|
||||||
|
checkedAt: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...byManga.values()];
|
||||||
|
}
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
let seenWork = false;
|
let jobsStarted = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await gql<{
|
const res = await gql<{
|
||||||
updateLibrary: { updateStatus: { jobsInfo: { isRunning: boolean; totalJobs: number } } }
|
updateLibrary: {
|
||||||
|
updateStatus: {
|
||||||
|
jobsInfo: {
|
||||||
|
isRunning: boolean;
|
||||||
|
totalJobs: number;
|
||||||
|
finishedJobs: number;
|
||||||
|
skippedMangasCount: number;
|
||||||
|
skippedCategoriesCount: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}>(UPDATE_LIBRARY, {});
|
}>(UPDATE_LIBRARY, {});
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
seenWork = res.updateLibrary.updateStatus.jobsInfo.totalJobs > 0;
|
|
||||||
} catch {
|
const { jobsInfo } = res.updateLibrary.updateStatus;
|
||||||
if (!cancelled) callbacks.onError();
|
jobsStarted = jobsInfo.totalJobs > 0;
|
||||||
|
|
||||||
|
callbacks.onProgress({
|
||||||
|
finished: jobsInfo.finishedJobs,
|
||||||
|
total: jobsInfo.totalJobs,
|
||||||
|
skippedManga: jobsInfo.skippedMangasCount,
|
||||||
|
skippedCategories: jobsInfo.skippedCategoriesCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!jobsStarted) {
|
||||||
|
callbacks.onDone({ entries: [], totalUpdated: 0, newChapters: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobsStarted && !jobsInfo.isRunning) {
|
||||||
|
callbacks.onDone({ entries: [], totalUpdated: 0, newChapters: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[libraryUpdater] failed to start update", e);
|
||||||
|
if (!cancelled) callbacks.onError(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function poll() {
|
function poll() {
|
||||||
gql<{
|
gql<{
|
||||||
libraryUpdateStatus: {
|
libraryUpdateStatus: {
|
||||||
jobsInfo: { isRunning: boolean; finishedJobs: number; totalJobs: number };
|
jobsInfo: {
|
||||||
mangaUpdates: { status: string; manga: { id: number } }[];
|
isRunning: boolean;
|
||||||
|
finishedJobs: number;
|
||||||
|
totalJobs: number;
|
||||||
|
skippedMangasCount: number;
|
||||||
|
skippedCategoriesCount: number;
|
||||||
|
};
|
||||||
|
mangaUpdates: {
|
||||||
|
status: string;
|
||||||
|
manga: { id: number; title: string; thumbnailUrl: string; unreadCount: number };
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
}>(LIBRARY_UPDATE_STATUS, {})
|
}>(LIBRARY_UPDATE_STATUS, {})
|
||||||
.then(async d => {
|
.then(async d => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const { jobsInfo } = d.libraryUpdateStatus;
|
const { jobsInfo, mangaUpdates } = d.libraryUpdateStatus;
|
||||||
|
|
||||||
if (jobsInfo.totalJobs > 0) seenWork = true;
|
if (jobsInfo.totalJobs > 0) jobsStarted = true;
|
||||||
callbacks.onProgress({ finished: jobsInfo.finishedJobs, total: jobsInfo.totalJobs });
|
callbacks.onProgress({
|
||||||
|
finished: jobsInfo.finishedJobs,
|
||||||
|
total: jobsInfo.totalJobs,
|
||||||
|
skippedManga: jobsInfo.skippedMangasCount,
|
||||||
|
skippedCategories: jobsInfo.skippedCategoriesCount,
|
||||||
|
});
|
||||||
|
|
||||||
if (!jobsInfo.isRunning && seenWork) {
|
if (!jobsInfo.isRunning && jobsStarted) {
|
||||||
const recent = await gql<{
|
const entries = buildEntries(mangaUpdates);
|
||||||
chapters: {
|
|
||||||
nodes: {
|
|
||||||
mangaId: number;
|
|
||||||
fetchedAt: string;
|
|
||||||
manga: { id: number; title: string; thumbnailUrl: string; inLibrary: boolean };
|
|
||||||
}[]
|
|
||||||
}
|
|
||||||
}>(GET_RECENTLY_UPDATED, {}).catch(() => ({ chapters: { nodes: [] } }));
|
|
||||||
|
|
||||||
if (cancelled) return;
|
|
||||||
|
|
||||||
const byManga = new Map<number, LibraryUpdateEntry>();
|
|
||||||
for (const ch of recent.chapters.nodes) {
|
|
||||||
if (!ch.manga.inLibrary) continue;
|
|
||||||
if (Number(ch.fetchedAt) < startedAt) continue;
|
|
||||||
const existing = byManga.get(ch.mangaId);
|
|
||||||
if (existing) {
|
|
||||||
existing.newChapters++;
|
|
||||||
} else {
|
|
||||||
byManga.set(ch.mangaId, {
|
|
||||||
mangaId: ch.mangaId,
|
|
||||||
mangaTitle: ch.manga.title,
|
|
||||||
thumbnailUrl: ch.manga.thumbnailUrl,
|
|
||||||
newChapters: 1,
|
|
||||||
checkedAt: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = [...byManga.values()];
|
|
||||||
const newChapters = entries.reduce((s, e) => s + e.newChapters, 0);
|
const newChapters = entries.reduce((s, e) => s + e.newChapters, 0);
|
||||||
|
|
||||||
callbacks.onDone({ entries, totalUpdated: entries.length, newChapters });
|
callbacks.onDone({ entries, totalUpdated: entries.length, newChapters });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
timer = setTimeout(poll, POLL_INTERVAL_MS);
|
timer = setTimeout(poll, POLL_INTERVAL_MS);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
if (!cancelled) callbacks.onError();
|
console.error("[libraryUpdater] poll error", e);
|
||||||
|
if (!cancelled) callbacks.onError(e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,16 @@
|
|||||||
import { getVersion } from "@tauri-apps/api/app";
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
import { open as openUrl } from "@tauri-apps/plugin-shell";
|
import { open as openUrl } from "@tauri-apps/plugin-shell";
|
||||||
import { autoBackupAppData } from "@core/backup";
|
import { autoBackupAppData } from "@core/backup";
|
||||||
|
import { gql } from "@api/client";
|
||||||
|
import { GET_ABOUT_SERVER, GET_ABOUT_WEBUI } from "@api/queries/updater";
|
||||||
|
|
||||||
interface ReleaseInfo { tag_name: string; name: string; body: string; published_at: string; html_url: string; }
|
interface ReleaseInfo { tag_name: string; name: string; body: string; published_at: string; html_url: string; }
|
||||||
type UpdatePhase = "idle" | "downloading" | "launching" | "ready" | "error";
|
type UpdatePhase = "idle" | "downloading" | "launching" | "ready" | "error";
|
||||||
const IS_WINDOWS = navigator.userAgent.includes("Windows");
|
const IS_WINDOWS = navigator.userAgent.includes("Windows");
|
||||||
|
|
||||||
|
interface AboutServer { name: string; version: string; buildType: string; buildTime: number; github: string; discord: string; }
|
||||||
|
interface AboutWebUI { channel: string; tag: string; updateTimestamp: number; }
|
||||||
|
|
||||||
let appVersion = $state("…");
|
let appVersion = $state("…");
|
||||||
let releases = $state<ReleaseInfo[]>([]);
|
let releases = $state<ReleaseInfo[]>([]);
|
||||||
let releasesLoading = $state(false);
|
let releasesLoading = $state(false);
|
||||||
@@ -21,9 +26,13 @@
|
|||||||
let targetTag = $state<string | null>(null);
|
let targetTag = $state<string | null>(null);
|
||||||
let releasesLoaded = false;
|
let releasesLoaded = false;
|
||||||
|
|
||||||
|
let serverInfo = $state<AboutServer | null>(null);
|
||||||
|
let webuiInfo = $state<AboutWebUI | null>(null);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
getVersion().then(v => appVersion = v).catch(() => appVersion = "unknown");
|
getVersion().then(v => appVersion = v).catch(() => appVersion = "unknown");
|
||||||
if (!releasesLoaded) { releasesLoaded = true; loadReleases(); }
|
if (!releasesLoaded) { releasesLoaded = true; loadReleases(); }
|
||||||
|
loadServerInfo();
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -52,6 +61,17 @@
|
|||||||
} finally { releasesLoading = false; }
|
} finally { releasesLoading = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadServerInfo() {
|
||||||
|
try {
|
||||||
|
const [s, w] = await Promise.all([
|
||||||
|
gql<{ aboutServer: AboutServer }>(GET_ABOUT_SERVER),
|
||||||
|
gql<{ aboutWebUI: AboutWebUI }>(GET_ABOUT_WEBUI),
|
||||||
|
]);
|
||||||
|
serverInfo = s.aboutServer;
|
||||||
|
webuiInfo = w.aboutWebUI;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
function stripV(v: string) { return v.replace(/^v/, ""); }
|
function stripV(v: string) { return v.replace(/^v/, ""); }
|
||||||
function isCurrentVersion(tag: string) { return stripV(tag) === appVersion; }
|
function isCurrentVersion(tag: string) { return stripV(tag) === appVersion; }
|
||||||
function parseSemver(v: string) { return stripV(v).split(".").map(Number); }
|
function parseSemver(v: string) { return stripV(v).split(".").map(Number); }
|
||||||
@@ -72,6 +92,11 @@
|
|||||||
return new Date(iso).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
|
return new Date(iso).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fmtBuildTime(unix: number) {
|
||||||
|
if (!unix) return "";
|
||||||
|
return new Date(unix).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
|
||||||
|
}
|
||||||
|
|
||||||
function fmtBytes(bytes: number) {
|
function fmtBytes(bytes: number) {
|
||||||
if (bytes === 0) return "0 B";
|
if (bytes === 0) return "0 B";
|
||||||
const units = ["B","KB","MB","GB"];
|
const units = ["B","KB","MB","GB"];
|
||||||
@@ -164,6 +189,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if serverInfo}
|
||||||
|
<div class="s-section">
|
||||||
|
<p class="s-section-title">Server</p>
|
||||||
|
<div class="s-section-body">
|
||||||
|
<div class="s-row">
|
||||||
|
<div class="s-row-info">
|
||||||
|
<span class="s-label">Version</span>
|
||||||
|
<span class="s-desc">
|
||||||
|
{serverInfo.version}
|
||||||
|
{#if serverInfo.buildType}
|
||||||
|
<span class="s-release-badge">{serverInfo.buildType}</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if serverInfo.buildTime}
|
||||||
|
<div class="s-row">
|
||||||
|
<div class="s-row-info">
|
||||||
|
<span class="s-label">Built</span>
|
||||||
|
<span class="s-desc">{fmtBuildTime(serverInfo.buildTime)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if webuiInfo?.channel}
|
||||||
|
<div class="s-row">
|
||||||
|
<div class="s-row-info">
|
||||||
|
<span class="s-label">Channel</span>
|
||||||
|
<span class="s-desc">{webuiInfo.channel}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="s-section">
|
<div class="s-section">
|
||||||
<p class="s-section-title">Releases</p>
|
<p class="s-section-title">Releases</p>
|
||||||
<div class="s-section-body">
|
<div class="s-section-body">
|
||||||
@@ -223,6 +283,12 @@
|
|||||||
<div class="s-row" style="flex-direction:column;align-items:flex-start;gap:var(--sp-2)">
|
<div class="s-row" style="flex-direction:column;align-items:flex-start;gap:var(--sp-2)">
|
||||||
<a href="https://github.com/moku-project/Moku" target="_blank" class="s-label" style="color:var(--accent-fg);text-decoration:none">GitHub →</a>
|
<a href="https://github.com/moku-project/Moku" target="_blank" class="s-label" style="color:var(--accent-fg);text-decoration:none">GitHub →</a>
|
||||||
<a href="https://discord.gg/Jq3pwuNqPp" target="_blank" class="s-label" style="color:var(--accent-fg);text-decoration:none">Discord →</a>
|
<a href="https://discord.gg/Jq3pwuNqPp" target="_blank" class="s-label" style="color:var(--accent-fg);text-decoration:none">Discord →</a>
|
||||||
|
{#if serverInfo?.github && serverInfo.github !== "https://github.com/moku-project/Moku"}
|
||||||
|
<a href={serverInfo.github} target="_blank" class="s-label" style="color:var(--accent-fg);text-decoration:none">Suwayomi GitHub →</a>
|
||||||
|
{/if}
|
||||||
|
{#if serverInfo?.discord && serverInfo.discord !== "https://discord.gg/Jq3pwuNqPp"}
|
||||||
|
<a href={serverInfo.discord} target="_blank" class="s-label" style="color:var(--accent-fg);text-decoration:none">Suwayomi Discord →</a>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
let flareTimeout = $state(store.settings.flareSolverrTimeout ?? 60);
|
let flareTimeout = $state(store.settings.flareSolverrTimeout ?? 60);
|
||||||
let flareSession = $state(store.settings.flareSolverrSessionName ?? "moku");
|
let flareSession = $state(store.settings.flareSolverrSessionName ?? "moku");
|
||||||
let flareTtl = $state(store.settings.flareSolverrSessionTtl ?? 15);
|
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) {
|
function showSaved(key: string) {
|
||||||
secSaved = key; secError = null;
|
secSaved = key; secError = null;
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
socksProxyVersion: socksVersion, socksProxyUsername: socksUsername,
|
socksProxyVersion: socksVersion, socksProxyUsername: socksUsername,
|
||||||
flareSolverrEnabled: flareEnabled, flareSolverrUrl: flareUrl,
|
flareSolverrEnabled: flareEnabled, flareSolverrUrl: flareUrl,
|
||||||
flareSolverrTimeout: flareTimeout, flareSolverrSessionName: flareSession,
|
flareSolverrTimeout: flareTimeout, flareSolverrSessionName: flareSession,
|
||||||
flareSolverrSessionTtl: flareTtl, flareSolverrFallback: flareFallback,
|
flareSolverrSessionTtl: flareTtl, flareSolverrAsResponseFallback: flareFallback,
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
updateSettings({
|
updateSettings({
|
||||||
flareSolverrEnabled: flareEnabled, flareSolverrUrl: flareUrl,
|
flareSolverrEnabled: flareEnabled, flareSolverrUrl: flareUrl,
|
||||||
flareSolverrTimeout: flareTimeout, flareSolverrSessionName: flareSession,
|
flareSolverrTimeout: flareTimeout, flareSolverrSessionName: flareSession,
|
||||||
flareSolverrSessionTtl: flareTtl, flareSolverrFallback: flareFallback,
|
flareSolverrSessionTtl: flareTtl, flareSolverrAsResponseFallback: flareFallback,
|
||||||
});
|
});
|
||||||
showSaved("flare");
|
showSaved("flare");
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export function removeRecord(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SyncBackOptions {
|
export interface SyncBackOptions {
|
||||||
|
threshold: number | null;
|
||||||
respectScanlatorFilter: boolean;
|
respectScanlatorFilter: boolean;
|
||||||
chapterPrefs: ChapterDisplayPrefs;
|
chapterPrefs: ChapterDisplayPrefs;
|
||||||
}
|
}
|
||||||
@@ -123,36 +124,30 @@ export async function syncBackFromTracker(
|
|||||||
chapters: Chapter[],
|
chapters: Chapter[],
|
||||||
opts: SyncBackOptions,
|
opts: SyncBackOptions,
|
||||||
gqlFn: (query: string, vars: Record<string, unknown>) => Promise<unknown>,
|
gqlFn: (query: string, vars: Record<string, unknown>) => Promise<unknown>,
|
||||||
): Promise<{ markedRead: number[]; markedUnread: number[] }> {
|
): Promise<number[]> {
|
||||||
const eligible = buildChapterList(
|
const base = opts.respectScanlatorFilter
|
||||||
opts.respectScanlatorFilter ? buildChapterList(chapters, opts.chapterPrefs) : chapters,
|
? buildChapterList(chapters, opts.chapterPrefs)
|
||||||
{ ...opts.chapterPrefs, sortDir: "asc" },
|
: chapters;
|
||||||
);
|
const eligible = buildChapterList(base, { ...opts.chapterPrefs, sortDir: "asc" });
|
||||||
|
|
||||||
const toMarkRead: number[] = [];
|
const toMarkRead: number[] = [];
|
||||||
const toMarkUnread: number[] = [];
|
|
||||||
|
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const remote = record.lastChapterRead;
|
const remote = record.lastChapterRead;
|
||||||
if (!remote || remote <= 0) continue;
|
if (!remote || remote <= 0) continue;
|
||||||
|
|
||||||
const position = Math.round(remote);
|
for (const chapter of eligible) {
|
||||||
const below = eligible.slice(0, position);
|
if (chapter.isRead) continue;
|
||||||
const above = eligible.slice(position);
|
const diff = Math.abs(chapter.chapterNumber - remote);
|
||||||
|
if (opts.threshold !== null && diff > opts.threshold) continue;
|
||||||
toMarkRead.push(...below.filter(c => !c.isRead).map(c => c.id));
|
if (chapter.chapterNumber <= remote) toMarkRead.push(chapter.id);
|
||||||
toMarkUnread.push(...above.filter(c => c.isRead).map(c => c.id));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const readIds = [...new Set(toMarkRead)];
|
const ids = [...new Set(toMarkRead)];
|
||||||
const unreadIds = [...new Set(toMarkUnread)];
|
if (ids.length > 0) {
|
||||||
|
await gqlFn(MARK_CHAPTERS_READ, { ids, isRead: true });
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { markedRead: readIds, markedUnread: unreadIds };
|
return ids;
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@ import { gql } from "@api/client";
|
|||||||
import { GET_MANGA_TRACK_RECORDS, GET_ALL_TRACKER_RECORDS } from "@api/queries/tracking";
|
import { GET_MANGA_TRACK_RECORDS, GET_ALL_TRACKER_RECORDS } from "@api/queries/tracking";
|
||||||
import { GET_CHAPTERS } from "@api/queries/chapters";
|
import { GET_CHAPTERS } from "@api/queries/chapters";
|
||||||
import { UPDATE_TRACK, FETCH_TRACK, UNBIND_TRACK } from "@api/mutations/tracking";
|
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 { buildChapterList, type ChapterDisplayPrefs } from "@features/series/lib/chapterList";
|
||||||
import { syncBackFromTracker } from "@features/tracking/lib/trackingSync";
|
import { syncBackFromTracker } from "@features/tracking/lib/trackingSync";
|
||||||
import { store } from "@store/state.svelte";
|
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 tracker of res.trackers.nodes.filter(t => t.isLoggedIn)) {
|
||||||
for (const record of tracker.trackRecords.nodes) {
|
for (const record of tracker.trackRecords.nodes) {
|
||||||
if (!record.manga?.id) continue;
|
if (!record.manga?.id) continue;
|
||||||
const mangaId = record.manga.id;
|
const mangaId = record.manga.id;
|
||||||
const existing = this.byManga.get(mangaId) ?? [];
|
const existing = this.byManga.get(mangaId) ?? [];
|
||||||
const merged = [...existing.filter(r => r.id !== record.id), record];
|
const merged = [...existing.filter(r => r.id !== record.id), record];
|
||||||
this.setFor(mangaId, merged);
|
this.setFor(mangaId, merged);
|
||||||
@@ -140,21 +139,22 @@ class TrackingState {
|
|||||||
const fresh = res.fetchTrack.trackRecord;
|
const fresh = res.fetchTrack.trackRecord;
|
||||||
this.patchFor(mangaId, fresh);
|
this.patchFor(mangaId, fresh);
|
||||||
|
|
||||||
const { markedRead } = await this._applyRemoteProgress(fresh, chapters, prefs);
|
const markedIds = await this._applyRemoteProgress(fresh, chapters, prefs);
|
||||||
return { fresh, markedIds: markedRead };
|
return { fresh, markedIds };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _applyRemoteProgress(
|
private async _applyRemoteProgress(
|
||||||
record: TrackRecord,
|
record: TrackRecord,
|
||||||
chapters: Chapter[],
|
chapters: Chapter[],
|
||||||
prefs: ChapterDisplayPrefs,
|
prefs: ChapterDisplayPrefs,
|
||||||
): Promise<{ markedRead: number[]; markedUnread: number[] }> {
|
): Promise<number[]> {
|
||||||
if (!store.settings.trackerSyncBack) return { markedRead: [], markedUnread: [] };
|
if (!store.settings.trackerSyncBack) return [];
|
||||||
|
|
||||||
return syncBackFromTracker(
|
return syncBackFromTracker(
|
||||||
[record],
|
[record],
|
||||||
chapters,
|
chapters,
|
||||||
{
|
{
|
||||||
|
threshold: store.settings.trackerSyncBackThreshold ?? null,
|
||||||
respectScanlatorFilter: store.settings.trackerRespectScanlatorFilter ?? true,
|
respectScanlatorFilter: store.settings.trackerRespectScanlatorFilter ?? true,
|
||||||
chapterPrefs: prefs,
|
chapterPrefs: prefs,
|
||||||
},
|
},
|
||||||
@@ -290,6 +290,7 @@ class TrackingState {
|
|||||||
freshRecords,
|
freshRecords,
|
||||||
chapters,
|
chapters,
|
||||||
{
|
{
|
||||||
|
threshold: store.settings.trackerSyncBackThreshold ?? null,
|
||||||
respectScanlatorFilter: store.settings.trackerRespectScanlatorFilter ?? true,
|
respectScanlatorFilter: store.settings.trackerRespectScanlatorFilter ?? true,
|
||||||
chapterPrefs: prefs,
|
chapterPrefs: prefs,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,3 +19,44 @@ export interface DownloadStatus {
|
|||||||
export interface Connection<T> {
|
export interface Connection<T> {
|
||||||
nodes: T[];
|
nodes: T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PageInfo {
|
||||||
|
hasNextPage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedConnection<T> extends Connection<T> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -8,8 +8,12 @@ export interface Chapter {
|
|||||||
isBookmarked: boolean;
|
isBookmarked: boolean;
|
||||||
pageCount: number;
|
pageCount: number;
|
||||||
mangaId: number;
|
mangaId: number;
|
||||||
|
fetchedAt?: string;
|
||||||
uploadDate?: string | null;
|
uploadDate?: string | null;
|
||||||
realUrl?: string | null;
|
realUrl?: string | null;
|
||||||
|
url?: string;
|
||||||
lastPageRead?: number;
|
lastPageRead?: number;
|
||||||
|
lastReadAt?: string;
|
||||||
scanlator?: string | null;
|
scanlator?: string | null;
|
||||||
|
manga?: { id: number; title: string; thumbnailUrl: string; inLibrary: boolean } | null;
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,9 @@ export interface Source {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
iconUrl: string;
|
iconUrl: string;
|
||||||
isNsfw: boolean;
|
isNsfw: boolean;
|
||||||
|
isConfigurable: boolean;
|
||||||
|
supportsLatest: boolean;
|
||||||
|
baseUrl?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Extension {
|
export interface Extension {
|
||||||
|
|||||||
+26
-1
@@ -8,20 +8,45 @@ export interface Category {
|
|||||||
mangas?: { nodes: Manga[] };
|
mangas?: { nodes: Manga[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChapterRef {
|
||||||
|
id: number;
|
||||||
|
chapterNumber: number;
|
||||||
|
uploadDate?: string;
|
||||||
|
lastPageRead?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Manga {
|
export interface Manga {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
thumbnailUrl: string;
|
thumbnailUrl: string;
|
||||||
inLibrary: boolean;
|
inLibrary: boolean;
|
||||||
|
initialized?: boolean;
|
||||||
downloadCount?: number;
|
downloadCount?: number;
|
||||||
unreadCount?: number;
|
unreadCount?: number;
|
||||||
chapterCount?: number;
|
bookmarkCount?: number;
|
||||||
|
hasDuplicateChapters?: boolean;
|
||||||
|
chapters?: { totalCount: number };
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
status?: string | null;
|
status?: string | null;
|
||||||
author?: string | null;
|
author?: string | null;
|
||||||
artist?: string | null;
|
artist?: string | null;
|
||||||
genre?: string[];
|
genre?: string[];
|
||||||
realUrl?: string | null;
|
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;
|
source?: { id: string; name: string; displayName: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export interface MangaPrefs {
|
|||||||
deleteDelayHours: number; maxKeepChapters: number; pauseUpdates: boolean;
|
deleteDelayHours: number; maxKeepChapters: number; pauseUpdates: boolean;
|
||||||
refreshInterval: "global" | "daily" | "weekly" | "manual";
|
refreshInterval: "global" | "daily" | "weekly" | "manual";
|
||||||
preferredScanlator: string; scanlatorFilter: string[];
|
preferredScanlator: string; scanlatorFilter: string[];
|
||||||
|
scanlatorBlacklist: string[]; scanlatorForce: boolean;
|
||||||
autoDownloadScanlators: string[];
|
autoDownloadScanlators: string[];
|
||||||
coverUrl?: string;
|
coverUrl?: string;
|
||||||
}
|
}
|
||||||
@@ -59,6 +60,7 @@ export const DEFAULT_MANGA_PREFS: MangaPrefs = {
|
|||||||
autoDownload: false, downloadAhead: 0, deleteOnRead: false,
|
autoDownload: false, downloadAhead: 0, deleteOnRead: false,
|
||||||
deleteDelayHours: 0, maxKeepChapters: 0, pauseUpdates: false,
|
deleteDelayHours: 0, maxKeepChapters: 0, pauseUpdates: false,
|
||||||
refreshInterval: "global", preferredScanlator: "", scanlatorFilter: [],
|
refreshInterval: "global", preferredScanlator: "", scanlatorFilter: [],
|
||||||
|
scanlatorBlacklist: [], scanlatorForce: false,
|
||||||
autoDownloadScanlators: [],
|
autoDownloadScanlators: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,7 +104,7 @@ export interface Settings {
|
|||||||
socksProxyEnabled: boolean; socksProxyHost: string; socksProxyPort: string;
|
socksProxyEnabled: boolean; socksProxyHost: string; socksProxyPort: string;
|
||||||
socksProxyVersion: number; socksProxyUsername: string; socksProxyPassword: string;
|
socksProxyVersion: number; socksProxyUsername: string; socksProxyPassword: string;
|
||||||
flareSolverrEnabled: boolean; flareSolverrUrl: string; flareSolverrTimeout: number;
|
flareSolverrEnabled: boolean; flareSolverrUrl: string; flareSolverrTimeout: number;
|
||||||
flareSolverrSessionName: string; flareSolverrSessionTtl: number; flareSolverrFallback: boolean;
|
flareSolverrSessionName: string; flareSolverrSessionTtl: number; flareSolverrAsResponseFallback: boolean;
|
||||||
appLockEnabled: boolean; appLockPin: string;
|
appLockEnabled: boolean; appLockPin: string;
|
||||||
customThemes: CustomTheme[]; hiddenCategoryIds: number[];
|
customThemes: CustomTheme[]; hiddenCategoryIds: number[];
|
||||||
defaultLibraryCategoryId: number | null; savedIsDefaultCategory: boolean;
|
defaultLibraryCategoryId: number | null; savedIsDefaultCategory: boolean;
|
||||||
@@ -145,7 +147,7 @@ export const DEFAULT_SETTINGS: Settings = {
|
|||||||
socksProxyVersion: 5, socksProxyUsername: "", socksProxyPassword: "",
|
socksProxyVersion: 5, socksProxyUsername: "", socksProxyPassword: "",
|
||||||
flareSolverrEnabled: false, flareSolverrUrl: "http://localhost:8191",
|
flareSolverrEnabled: false, flareSolverrUrl: "http://localhost:8191",
|
||||||
flareSolverrTimeout: 60, flareSolverrSessionName: "moku",
|
flareSolverrTimeout: 60, flareSolverrSessionName: "moku",
|
||||||
flareSolverrSessionTtl: 15, flareSolverrFallback: false,
|
flareSolverrSessionTtl: 15, flareSolverrAsResponseFallback: false,
|
||||||
appLockEnabled: false, appLockPin: "",
|
appLockEnabled: false, appLockPin: "",
|
||||||
customThemes: [], hiddenCategoryIds: [], defaultLibraryCategoryId: null,
|
customThemes: [], hiddenCategoryIds: [], defaultLibraryCategoryId: null,
|
||||||
savedIsDefaultCategory: false,
|
savedIsDefaultCategory: false,
|
||||||
|
|||||||
+10
-3
@@ -8,8 +8,11 @@ export interface Tracker {
|
|||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
|
isTokenExpired: boolean;
|
||||||
authUrl: string | null;
|
authUrl: string | null;
|
||||||
supportsPrivateTracking: boolean;
|
supportsPrivateTracking: boolean;
|
||||||
|
supportsReadingDates: boolean;
|
||||||
|
supportsTrackDeletion: boolean;
|
||||||
scores: string[];
|
scores: string[];
|
||||||
statuses: TrackerStatus[];
|
statuses: TrackerStatus[];
|
||||||
}
|
}
|
||||||
@@ -17,17 +20,21 @@ export interface Tracker {
|
|||||||
export interface TrackRecord {
|
export interface TrackRecord {
|
||||||
id: number;
|
id: number;
|
||||||
trackerId: number;
|
trackerId: number;
|
||||||
|
mangaId: number;
|
||||||
remoteId: string;
|
remoteId: string;
|
||||||
|
libraryId: string | null;
|
||||||
title: string;
|
title: string;
|
||||||
status: number;
|
status: number;
|
||||||
score: number;
|
score: number;
|
||||||
displayScore: string;
|
displayScore: string;
|
||||||
lastChapterRead: number;
|
lastChapterRead: number;
|
||||||
totalChapters: number;
|
totalChapters: number;
|
||||||
remoteUrl: string | null;
|
remoteUrl: string;
|
||||||
startDate: string | null;
|
startDate: string;
|
||||||
finishDate: string | null;
|
finishDate: string;
|
||||||
private: boolean;
|
private: boolean;
|
||||||
|
manga?: { id: number; title: string; thumbnailUrl: string; inLibrary?: boolean } | null;
|
||||||
|
tracker?: Pick<Tracker, "id" | "name" | "icon" | "isLoggedIn" | "statuses"> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrackSearch {
|
export interface TrackSearch {
|
||||||
|
|||||||
Reference in New Issue
Block a user