Chore: README Update

This commit is contained in:
Youwes09
2026-03-23 19:18:27 -05:00
parent 6d85be751a
commit b23292cff5
15 changed files with 218 additions and 55 deletions
+2
View File
@@ -37,5 +37,7 @@ src-tauri/gen/
# --- Flatpak build artifacts --- # --- Flatpak build artifacts ---
build-dir/ build-dir/
repo/ repo/
dist/
packaging/frontend-dist.tar.gz
*.flatpak *.flatpak
.flatpak-builder/\n# --- Staged sidecar binaries (platform-specific, never commit) ---\nsrc-tauri/binaries/ .flatpak-builder/\n# --- Staged sidecar binaries (platform-specific, never commit) ---\nsrc-tauri/binaries/
+94 -19
View File
@@ -1,46 +1,113 @@
<div align="center"> <div align="center">
<img src="src/assets/moku-icon.svg" width="96" /> <img src="docs/banner.svg" width="100%" alt="Moku" />
<h1>Moku</h1> </div>
<p>A fast, minimal manga reader for <a href="https://github.com/Suwayomi/Suwayomi-Server">Suwayomi-Server</a>.<br/>Built with Tauri v2 and Svelte.</p>
<div align="center">
[![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)
</div>
<br/>
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
<div align="center">
<img src="docs/screenshots/Moku-Home.png" width="49%" alt="Home" />
<img src="docs/screenshots/Moku-Discover.png" width="49%" alt="Discover" />
<img src="docs/screenshots/Moku-Reader.png" width="49%" alt="Reader" />
<img src="docs/screenshots/Moku-Preview.png" width="49%" alt="Preview" />
<img src="docs/screenshots/Moku-Tracker.png" width="49%" alt="Tracker" />
<img src="docs/screenshots/Moku-Settings.png" width="49%" alt="Settings" />
</div>
<div align="center">
<a href="docs/screenshots">View all screenshots →</a>
</div> </div>
--- ---
## 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`. - **Library management** — organize manga into folders, track unread counts, filter by genre
- **Built-in reader** — single page, long strip, configurable fit modes, customizable keybinds
> Moku will attempt to launch the server automatically on startup if the `suwayomi-server` binary is on your `PATH`. - **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 ## Installation
**Nix (recommended)** ### Flatpak (Linux, recommended)
Suwayomi-Server and a bundled JRE are included — no separate install needed.
```bash ```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: Add to your flake:
```nix ```nix
inputs.moku.url = "github:Youwes09/moku"; inputs.moku.url = "github:Youwes09/Moku";
``` ```
**From source** ### Windows
```bash Download the `.exe` installer from the [releases page](https://github.com/Youwes09/Moku/releases/latest). Suwayomi-Server and a JRE are bundled.
git clone https://github.com/Youwes09/moku
cd moku ### macOS
nix build
./result/bin/moku 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 ## 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 ```bash
nix develop nix develop
pnpm install pnpm install
@@ -54,12 +121,20 @@ pnpm tauri:dev
| | | | | |
|---|---| |---|---|
| [Tauri v2](https://tauri.app) | Native app shell | | [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 | | [Vite](https://vitejs.dev) | Frontend bundler |
| [Crane](https://github.com/ipetkov/crane) | Nix Rust builds | | [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 ## License
Distributed under the [Apache 2.0 License](./LICENSE). Distributed under the [Apache 2.0 License](./LICENSE).
@@ -68,4 +143,4 @@ Distributed under the [Apache 2.0 License](./LICENSE).
## Disclaimer ## 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.
+52
View File
@@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 320" width="1280" height="320">
<defs>
<linearGradient id="leafHero" x1="0.3" y1="0" x2="0.7" y2="1">
<stop offset="0%" stop-color="#52b888"/>
<stop offset="100%" stop-color="#1e5840"/>
</linearGradient>
<clipPath id="roundedBounds">
<rect width="1280" height="320" rx="18" ry="18"/>
</clipPath>
</defs>
<g clip-path="url(#roundedBounds)">
<rect width="1280" height="320" fill="#070e09"/>
<!-- Icon — rotate(7) from moku-icon-splash.svg -->
<g transform="translate(640, 148) rotate(7) scale(0.065,-0.065) translate(-5000,-4800)"
fill="url(#leafHero)" opacity="0.97">
<path d="M4908 6615 c-597 -629 -935 -1264 -934 -1755 0 -508 195 -778 790
-1098 l194 -104 22 -114 c33 -164 76 -271 140 -347 80 -94 106 -62 77 94 -47
255 43 443 294 613 797 539 714 1392 -250 2551 -230 278 -224 275 -333 160z
m52 -945 c0 -472 -8 -850 -17 -850 -36 0 -693 703 -692 740 2 48 167 321 309
509 175 233 359 451 380 451 13 0 20 -304 20 -850z m183 775 c120 -126 357
-447 356 -482 0 -18 -47 -83 -105 -144 -57 -61 -151 -163 -208 -225 -151 -166
-146 -180 -146 406 0 286 7 520 16 520 9 0 48 -34 87 -75z m541 -750 c86 -150
196 -391 196 -430 0 -8 -25 -42 -55 -75 -31 -33 -149 -163 -264 -290 -339
-374 -480 -520 -501 -520 -13 0 -20 167 -20 465 l1 465 151 170 c83 94 193
217 244 275 122 138 137 135 248 -60z m-1098 -633 l374 -378 0 -462 c0 -254
-5 -462 -11 -462 -6 0 -55 23 -110 50 l-99 51 0 208 c0 259 -11 288 -176 457
-196 200 -188 199 -326 62 l-117 -116 -35 79 c-71 161 -39 569 64 816 42 100
20 116 436 -305z m1349 23 c109 -390 -87 -848 -475 -1111 -207 -139 -305 -262
-338 -420 -7 -31 -21 -56 -32 -56 -36 -1 -46 86 -48 405 l-2 313 123 137 c68
75 214 236 325 357 454 493 421 466 447 375z m-1292 -785 c12 -29 15 -118 7
-215 l-14 -166 -73 44 c-174 104 -403 341 -403 416 0 15 47 70 105 123 l104
98 127 -125 c70 -69 136 -147 147 -175z"/>
</g>
<!-- Stack text pinned to bottom -->
<text
x="640" y="300"
text-anchor="middle"
font-family="'SF Mono', 'JetBrains Mono', 'Fira Code', monospace"
font-size="14"
letter-spacing="5"
fill="#a8c4a8"
opacity="0.32">TAURI v2 · SVELTE 5 · TYPESCRIPT</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.
+10 -30
View File
@@ -274,8 +274,7 @@ fn resolve_server_binary(
} }
}; };
// Windows: use bundled JRE + JAR #[cfg(not(target_os = "macos"))]
#[cfg(target_os = "windows")]
{ {
let bundle_dir = resource_dir.join("binaries").join("suwayomi-bundle"); let bundle_dir = resource_dir.join("binaries").join("suwayomi-bundle");
let jar = bundle_dir.join("bin").join("Suwayomi-Server.jar"); 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")] #[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 = [ let candidates = [
"suwayomi-server-aarch64-apple-darwin", "suwayomi-server-aarch64-apple-darwin",
"suwayomi-server-x86_64-apple-darwin", "suwayomi-server-x86_64-apple-darwin",
"suwayomi-server", "suwayomi-server",
]; ];
for name in &candidates { for name in &candidates {
// Check Contents/MacOS/ first (where externalBin sidecars live) let p = resource_dir.join(name);
let in_macos = macos_dir.join(name); do_log(log, &format!("[resolve] macOS candidate: {:?} exists={}", p, p.exists()));
// Also check resource_dir root as a fallback if p.exists() {
let in_resources = resource_dir.join(name); do_log(log, &format!("[resolve] using macOS candidate: {:?}", p));
return Ok(ServerInvocation {
for p in &[in_macos, in_resources] { bin: p.to_string_lossy().into_owned(),
do_log(log, &format!("[resolve] macOS candidate: {:?} exists={}", p, p.exists())); args: vec![],
if p.exists() { working_dir: None,
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"); do_log(log, "[resolve] trying PATH fallback");
for name in &["suwayomi-server", "tachidesk-server"] { for name in &["suwayomi-server", "tachidesk-server"] {
let found = std::process::Command::new("which") let found = std::process::Command::new("which")
+22 -3
View File
@@ -104,10 +104,29 @@
getVersion(), getVersion(),
invoke<Array<{ tag_name: string; html_url: string }>>("list_releases"), invoke<Array<{ tag_name: string; html_url: string }>>("list_releases"),
]); ]);
if (!releases.length) return;
const latestTag = releases[0].tag_name.replace(/^v/, ""); // Filter out drafts / incomplete releases that have no tag_name
if (latestTag !== currentVersion) { 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({ addToast({
kind: "info", kind: "info",
title: `Update available — v${latestTag}`, title: `Update available — v${latestTag}`,
+38 -3
View File
@@ -336,16 +336,26 @@
let dlTotal = $state<number | null>(null); let dlTotal = $state<number | null>(null);
let targetTag = $state<string | null>(null); // tag being installed let targetTag = $state<string | null>(null); // tag being installed
let releasesLoaded = false; // plain var — not $state, so effect doesn't re-run on change
$effect(() => { $effect(() => {
if (tab !== "about") return; if (tab !== "about") return;
getVersion().then(v => appVersion = v).catch(() => appVersion = "unknown"); getVersion().then(v => appVersion = v).catch(() => appVersion = "unknown");
if (releases.length === 0 && !releasesLoading) loadReleases(); if (!releasesLoaded) { releasesLoaded = true; loadReleases(); }
}); });
async function loadReleases() { async function loadReleases() {
releasesLoading = true; releasesError = null; releasesLoading = true; releasesError = null;
try { try {
releases = await invoke<ReleaseInfo[]>("list_releases"); const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Request timed out after 10s")), 10_000)
);
const all = await Promise.race([
invoke<ReleaseInfo[]>("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) { } catch (e: any) {
releasesError = e instanceof Error ? e.message : String(e); releasesError = e instanceof Error ? e.message : String(e);
} finally { } finally {
@@ -358,6 +368,25 @@
function isCurrentVersion(tag: string) { return stripV(tag) === appVersion; } 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 { function fmtDate(iso: string): string {
if (!iso) return ""; if (!iso) return "";
const d = new Date(iso); const d = new Date(iso);
@@ -1044,10 +1073,15 @@
<span class="toggle-desc">v{appVersion}</span> <span class="toggle-desc">v{appVersion}</span>
</div> </div>
<button class="step-btn" style="width:auto;padding:0 var(--sp-3);font-size:var(--text-xs);letter-spacing:var(--tracking-wide)" <button class="step-btn" style="width:auto;padding:0 var(--sp-3);font-size:var(--text-xs);letter-spacing:var(--tracking-wide)"
onclick={loadReleases} disabled={releasesLoading}> onclick={() => { releasesError = null; loadReleases(); }} disabled={releasesLoading}>
{releasesLoading ? "Loading…" : "Refresh"} {releasesLoading ? "Loading…" : "Refresh"}
</button> </button>
</div> </div>
{#if onLatestVersion}
<div class="step-row">
<span class="toggle-desc" style="padding:0 var(--sp-3);color:var(--accent-fg)">✓ You're on the latest version.</span>
</div>
{/if}
<!-- active download progress --> <!-- active download progress -->
{#if updatePhase === "downloading" && IS_WINDOWS} {#if updatePhase === "downloading" && IS_WINDOWS}
@@ -1157,6 +1191,7 @@
<p class="section-title">Links</p> <p class="section-title">Links</p>
<div class="about-block"> <div class="about-block">
<a href="https://github.com/Youwes09/Moku" target="_blank" class="about-line" style="color:var(--accent-fg);text-decoration:none">GitHub →</a> <a href="https://github.com/Youwes09/Moku" target="_blank" class="about-line" style="color:var(--accent-fg);text-decoration:none">GitHub →</a>
<a href="https://discord.gg/cfncTbJ2" target="_blank" class="about-line" style="color:var(--accent-fg);text-decoration:none;margin-top:var(--sp-1)">Discord →</a>
</div> </div>
</div> </div>