diff --git a/.gitignore b/.gitignore index 5aaf47f..e73ab2a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,7 @@ src-tauri/gen/ # --- Flatpak build artifacts --- build-dir/ repo/ +dist/ +packaging/frontend-dist.tar.gz *.flatpak .flatpak-builder/\n# --- Staged sidecar binaries (platform-specific, never commit) ---\nsrc-tauri/binaries/ diff --git a/README.md b/README.md index 1a41f2b..16069f7 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,113 @@
- -

Moku

-

A fast, minimal manga reader for Suwayomi-Server.
Built with Tauri v2 and Svelte.

+ Moku +
+ +
+ +[![release](https://img.shields.io/github/v/release/Youwes09/Moku?style=flat&color=a8c4a8&labelColor=151515)](https://github.com/Youwes09/Moku/releases/latest) +[![downloads](https://img.shields.io/github/downloads/Youwes09/Moku/total?style=flat&color=a8c4a8&labelColor=151515)](https://github.com/Youwes09/Moku/releases/latest) +[![license](https://img.shields.io/github/license/Youwes09/Moku?style=flat&color=a8c4a8&labelColor=151515)](./LICENSE) +[![discord](https://img.shields.io/discord/1485151759511978077?style=flat&color=a8c4a8&labelColor=151515&label=discord)](https://discord.gg/cfncTbJ2) + +
+ +
+ +Moku is a fast, minimal manga reader frontend for [Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server). It wraps Suwayomi's GraphQL API in a native desktop app — no browser required, no Electron overhead. + +--- + +## Screenshots + +
+ Home + Discover + Reader + Preview + Tracker + Settings +
+ +
+ View all screenshots →
--- -## Requirements +## Features -[Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server) must be running. By default Moku expects it at `http://127.0.0.1:4567`. - -> Moku will attempt to launch the server automatically on startup if the `suwayomi-server` binary is on your `PATH`. +- **Library management** — organize manga into folders, track unread counts, filter by genre +- **Built-in reader** — single page, long strip, configurable fit modes, customizable keybinds +- **Extension support** — install and manage Suwayomi extensions directly from the app +- **Download management** — queue and monitor chapter downloads with progress toasts +- **Tracker integration** — sync reading progress with AniList, MyAnimeList, Kitsu, and more +- **Auto-start server** — optionally launch Suwayomi in the background on startup +- **Multiple themes** — Dark, Light, Midnight, Warm, High Contrast, and more +- **Auto-updates** — in-app update checker with silent background notifications --- ## Installation -**Nix (recommended)** +### Flatpak (Linux, recommended) + +Suwayomi-Server and a bundled JRE are included — no separate install needed. ```bash -nix run github:Youwes09/moku +flatpak install moku.flatpak +flatpak run dev.moku.app +``` + +Download the latest `moku.flatpak` from the [releases page](https://github.com/Youwes09/Moku/releases/latest). + +### Nix + +```bash +nix run github:Youwes09/Moku ``` Add to your flake: ```nix -inputs.moku.url = "github:Youwes09/moku"; +inputs.moku.url = "github:Youwes09/Moku"; ``` -**From source** +### Windows -```bash -git clone https://github.com/Youwes09/moku -cd moku -nix build -./result/bin/moku -``` +Download the `.exe` installer from the [releases page](https://github.com/Youwes09/Moku/releases/latest). Suwayomi-Server and a JRE are bundled. + +### macOS + +Download the `.dmg` from the [releases page](https://github.com/Youwes09/Moku/releases/latest). + +> **Note:** Builds are ad-hoc signed. On first launch you may need to run: +> ```bash +> xattr -rd com.apple.quarantine /Applications/Moku.app +> ``` + +--- + +## Requirements + +If you're not using the bundled Flatpak or Windows installer, [Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server) must be running separately. By default Moku connects to `http://127.0.0.1:4567`. + +You can point Moku at any Suwayomi instance — local or remote — via **Settings → General → Server URL**. --- ## Development +**Prerequisites:** [Rust](https://rustup.rs), [Node.js](https://nodejs.org), [pnpm](https://pnpm.io), and [Tauri v2 prerequisites](https://tauri.app/start/prerequisites/). + +```bash +git clone https://github.com/Youwes09/Moku +cd Moku +pnpm install +pnpm tauri:dev +``` + +Or with Nix: + ```bash nix develop pnpm install @@ -54,12 +121,20 @@ pnpm tauri:dev | | | |---|---| | [Tauri v2](https://tauri.app) | Native app shell | -| [Svelte](https://svelte.dev) + [TypeScript](https://www.typescriptlang.org) | UI | +| [Svelte 5](https://svelte.dev) + [TypeScript](https://www.typescriptlang.org) | UI | | [Vite](https://vitejs.dev) | Frontend bundler | | [Crane](https://github.com/ipetkov/crane) | Nix Rust builds | --- +## Community + +Questions, feedback, or just want to hang out — join the Discord. + +[![Discord](https://img.shields.io/discord/1485151759511978077?style=for-the-badge&color=a8c4a8&labelColor=151515&label=Join+the+Discord)](https://discord.gg/cfncTbJ2) + +--- + ## License Distributed under the [Apache 2.0 License](./LICENSE). @@ -68,4 +143,4 @@ Distributed under the [Apache 2.0 License](./LICENSE). ## Disclaimer -Moku does not host or distribute any content. The developers have no affiliation with any content providers accessible through connected sources. +Moku does not host or distribute any content. The developers have no affiliation with any content providers accessible through connected sources. \ No newline at end of file diff --git a/docs/banner.svg b/docs/banner.svg new file mode 100644 index 0000000..b936378 --- /dev/null +++ b/docs/banner.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + TAURI v2 · SVELTE 5 · TYPESCRIPT + + + diff --git a/docs/screenshots/Moku-Discover.png b/docs/screenshots/Moku-Discover.png new file mode 100644 index 0000000..61bad4f Binary files /dev/null and b/docs/screenshots/Moku-Discover.png differ diff --git a/docs/screenshots/Moku-Extensions.png b/docs/screenshots/Moku-Extensions.png new file mode 100644 index 0000000..f7e8cec Binary files /dev/null and b/docs/screenshots/Moku-Extensions.png differ diff --git a/docs/screenshots/Moku-Home.png b/docs/screenshots/Moku-Home.png new file mode 100644 index 0000000..19274ca Binary files /dev/null and b/docs/screenshots/Moku-Home.png differ diff --git a/docs/screenshots/Moku-Preview.png b/docs/screenshots/Moku-Preview.png new file mode 100644 index 0000000..ff96ba3 Binary files /dev/null and b/docs/screenshots/Moku-Preview.png differ diff --git a/docs/screenshots/Moku-Reader.png b/docs/screenshots/Moku-Reader.png new file mode 100644 index 0000000..ca9c5b8 Binary files /dev/null and b/docs/screenshots/Moku-Reader.png differ diff --git a/docs/screenshots/Moku-Settings.png b/docs/screenshots/Moku-Settings.png new file mode 100644 index 0000000..4b345e3 Binary files /dev/null and b/docs/screenshots/Moku-Settings.png differ diff --git a/docs/screenshots/Moku-SplashScreen.png b/docs/screenshots/Moku-SplashScreen.png new file mode 100644 index 0000000..0b451b0 Binary files /dev/null and b/docs/screenshots/Moku-SplashScreen.png differ diff --git a/docs/screenshots/Moku-Tracker.png b/docs/screenshots/Moku-Tracker.png new file mode 100644 index 0000000..518c9e9 Binary files /dev/null and b/docs/screenshots/Moku-Tracker.png differ diff --git a/packaging/frontend-dist.tar.gz b/packaging/frontend-dist.tar.gz deleted file mode 100644 index e54fabd..0000000 Binary files a/packaging/frontend-dist.tar.gz and /dev/null differ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index df52be4..886fb2d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -274,8 +274,7 @@ fn resolve_server_binary( } }; - // Windows: use bundled JRE + JAR - #[cfg(target_os = "windows")] + #[cfg(not(target_os = "macos"))] { let bundle_dir = resource_dir.join("binaries").join("suwayomi-bundle"); let jar = bundle_dir.join("bin").join("Suwayomi-Server.jar"); @@ -308,46 +307,27 @@ fn resolve_server_binary( } } - // macOS: use the arch-specific sidecar shell script registered via externalBin. - // Tauri's shell plugin resolves externalBin names relative to Contents/MacOS/, - // so we look there directly rather than in resource_dir. #[cfg(target_os = "macos")] { - // Contents/MacOS/ is the parent of resource_dir's parent on macOS: - // resource_dir = Moku.app/Contents/Resources - // macos_dir = Moku.app/Contents/MacOS - let macos_dir = resource_dir - .parent() // Contents/ - .map(|p| p.join("MacOS")) - .unwrap_or_else(|| resource_dir.clone()); - let candidates = [ "suwayomi-server-aarch64-apple-darwin", "suwayomi-server-x86_64-apple-darwin", "suwayomi-server", ]; - for name in &candidates { - // Check Contents/MacOS/ first (where externalBin sidecars live) - let in_macos = macos_dir.join(name); - // Also check resource_dir root as a fallback - let in_resources = resource_dir.join(name); - - for p in &[in_macos, in_resources] { - do_log(log, &format!("[resolve] macOS candidate: {:?} exists={}", p, p.exists())); - if p.exists() { - do_log(log, &format!("[resolve] using macOS candidate: {:?}", p)); - return Ok(ServerInvocation { - bin: p.to_string_lossy().into_owned(), - args: vec![], - working_dir: None, - }); - } + let p = resource_dir.join(name); + do_log(log, &format!("[resolve] macOS candidate: {:?} exists={}", p, p.exists())); + if p.exists() { + do_log(log, &format!("[resolve] using macOS candidate: {:?}", p)); + return Ok(ServerInvocation { + bin: p.to_string_lossy().into_owned(), + args: vec![], + working_dir: None, + }); } } } - // Linux / PATH fallback for all platforms do_log(log, "[resolve] trying PATH fallback"); for name in &["suwayomi-server", "tachidesk-server"] { let found = std::process::Command::new("which") diff --git a/src/App.svelte b/src/App.svelte index 249a7b4..8485e4a 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -104,10 +104,29 @@ getVersion(), invoke>("list_releases"), ]); - if (!releases.length) return; - const latestTag = releases[0].tag_name.replace(/^v/, ""); - if (latestTag !== currentVersion) { + // Filter out drafts / incomplete releases that have no tag_name + const valid = releases.filter(r => typeof r.tag_name === "string" && r.tag_name.trim()); + if (!valid.length) return; + + const parse = (tag: string): number[] => + tag.replace(/^v/, "").split(".").map(Number); + + const compare = (a: number[], b: number[]): number => { + for (let i = 0; i < 3; i++) { + if ((a[i] ?? 0) !== (b[i] ?? 0)) return (b[i] ?? 0) - (a[i] ?? 0); + } + return 0; + }; + + const latestTag = valid + .map(r => r.tag_name) + .sort((a, b) => compare(parse(a), parse(b)))[0] + .replace(/^v/, ""); + + // Only toast if latest is strictly newer than installed + const isNewer = compare(parse(latestTag), parse(currentVersion)) < 0; + if (isNewer) { addToast({ kind: "info", title: `Update available — v${latestTag}`, diff --git a/src/components/settings/Settings.svelte b/src/components/settings/Settings.svelte index 360fc5d..c657825 100644 --- a/src/components/settings/Settings.svelte +++ b/src/components/settings/Settings.svelte @@ -336,16 +336,26 @@ let dlTotal = $state(null); let targetTag = $state(null); // tag being installed + let releasesLoaded = false; // plain var — not $state, so effect doesn't re-run on change + $effect(() => { if (tab !== "about") return; getVersion().then(v => appVersion = v).catch(() => appVersion = "unknown"); - if (releases.length === 0 && !releasesLoading) loadReleases(); + if (!releasesLoaded) { releasesLoaded = true; loadReleases(); } }); async function loadReleases() { releasesLoading = true; releasesError = null; try { - releases = await invoke("list_releases"); + const timeout = new Promise((_, reject) => + setTimeout(() => reject(new Error("Request timed out after 10s")), 10_000) + ); + const all = await Promise.race([ + invoke("list_releases"), + timeout, + ]); + // Filter out drafts / incomplete entries with no tag_name + releases = all.filter(r => typeof r.tag_name === "string" && r.tag_name.trim()); } catch (e: any) { releasesError = e instanceof Error ? e.message : String(e); } finally { @@ -358,6 +368,25 @@ function isCurrentVersion(tag: string) { return stripV(tag) === appVersion; } + function parseSemver(v: string): number[] { + return stripV(v).split(".").map(Number); + } + + function compareSemver(a: string, b: string): number { + const pa = parseSemver(a), pb = parseSemver(b); + for (let i = 0; i < 3; i++) { + if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pb[i] ?? 0) - (pa[i] ?? 0); + } + return 0; + } + + // True when releases are loaded and installed version is >= highest published tag + let onLatestVersion = $derived((() => { + if (releasesLoading || releases.length === 0 || !appVersion || appVersion === "…") return false; + const sorted = releases.slice().sort((a, b) => compareSemver(a.tag_name, b.tag_name)); + return compareSemver(appVersion, sorted[0].tag_name) >= 0; + })()); + function fmtDate(iso: string): string { if (!iso) return ""; const d = new Date(iso); @@ -1044,10 +1073,15 @@ v{appVersion} + {#if onLatestVersion} +
+ ✓ You're on the latest version. +
+ {/if} {#if updatePhase === "downloading" && IS_WINDOWS} @@ -1157,6 +1191,7 @@

Links