("spawn_server", { binary: store.settings.serverBinary }).catch((err: any) => {
+ if (err?.kind === "NotConfigured") {
+ notConfigured = true;
+ } else {
+ console.warn("Could not start server:", err);
+ }
+ });
}
if (!serverProbeOk) {
@@ -117,6 +122,7 @@
unlistenDownload = await listen("download-progress", e => { setActiveDownloads(e.payload); });
return () => {
+ cancelled = true;
if (store.settings.autoStartServer) invoke("kill_server").catch(() => {});
if (idleTimer) clearTimeout(idleTimer);
if (pollInterval) clearInterval(pollInterval);
@@ -125,14 +131,14 @@
};
});
- function handleRetry() { failed = false; serverProbeOk = false; }
+ function handleRetry() { failed = false; notConfigured = false; serverProbeOk = false; }
{#if devSplash}
setTimeout(() => devSplash = false, 340)} />
{:else if !appReady}
- appReady = true}
onRetry={handleRetry} />
diff --git a/src/assets/moku-icon-rounded.svg b/src/assets/moku-icon-rounded.svg
new file mode 100644
index 0000000..a215c63
--- /dev/null
+++ b/src/assets/moku-icon-rounded.svg
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/rounded-logo.png b/src/assets/rounded-logo.png
deleted file mode 100644
index 2abb47a..0000000
Binary files a/src/assets/rounded-logo.png and /dev/null differ
diff --git a/src/components/layout/SplashScreen.svelte b/src/components/layout/SplashScreen.svelte
index d5570de..14cf063 100644
--- a/src/components/layout/SplashScreen.svelte
+++ b/src/components/layout/SplashScreen.svelte
@@ -1,21 +1,23 @@
@@ -232,21 +255,32 @@
press any key to continue
{:else}
-
- {#if !failed}
+
+ {#if !failed && !notConfigured}
{/if}
-
+
moku
- {#if failed}
-
Could not reach Suwayomi
-
Make sure tachidesk-server is on your PATH
-
Retry
+ {#if notConfigured}
+
+
Server not configured
+
Set the server path in Settings, then retry
+
+ { store.settingsOpen = true; }}>Settings
+ Retry
+
+
+ {:else if failed}
+
+
Could not reach Suwayomi
+
Make sure tachidesk-server is on your PATH
+
Retry
+
{:else}
{ringFull ? "Ready" : `Initializing server${dots}`}
@@ -268,4 +302,9 @@
.hint { font-family: var(--font-ui); font-size: 10px; color: var(--text-faint); letter-spacing: 0.22em; text-transform: uppercase; margin: 0; user-select: none; animation: hintFade 3.5s ease-in-out infinite; }
.title-label { font-family: var(--font-ui); font-size: 11px; font-weight: 500; letter-spacing: 0.26em; text-transform: uppercase; color: var(--text-secondary); margin: 0 0 8px; z-index: 1; user-select: none; }
.retry-btn { margin-top: 4px; padding: 5px 16px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: var(--bg-raised); color: var(--text-muted); cursor: pointer; font-family: var(--font-ui); font-size: 11px; letter-spacing: 0.08em; }
+ .retry-btn:hover { border-color: var(--border-strong); color: var(--text-secondary); }
+ .error-box { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 14px 20px; border-radius: var(--radius-lg); background: rgba(0,0,0,0.55); border: 1px solid rgba(255,255,255,0.12); max-width: 260px; text-align: center; backdrop-filter: blur(4px); }
+ .error-box--danger { border-color: rgba(220,50,50,0.5); }
+ .error-title { font-family: var(--font-ui); font-size: 11px; font-weight: 500; color: var(--text-muted); letter-spacing: 0.1em; margin: 0; }
+ .error-body { font-family: var(--font-ui); font-size: 10px; color: var(--text-faint); letter-spacing: 0.05em; margin: 0; line-height: 1.6; }
diff --git a/src/components/pages/Explore.svelte b/src/components/pages/Explore.svelte
new file mode 100644
index 0000000..1229c65
--- /dev/null
+++ b/src/components/pages/Explore.svelte
@@ -0,0 +1,372 @@
+
+
+{#if $activeSource}
+
+{:else if $genreFilter}
+
+{:else}
+
+
+
+
+
+
+ {#if continueReading.length > 0 || loadingLib}
+
+
+ {#if loadingLib}
+
{#each Array(8) as _}
{/each}
+ {:else}
+
+ {#each continueReading.slice(0, ROW_CAP) as { manga, chapterName, progress }}
+
previewManga.set(manga)} on:contextmenu={(e) => { e.preventDefault(); ctx = { x: e.clientX, y: e.clientY, manga }; }}>
+
+
+ {#if manga.inLibrary}
Saved {/if}
+ {#if progress > 0}
{/if}
+
+ {manga.title}
+ {#if chapterName}{chapterName}
{/if}
+
+ {/each}
+ {#each Array(GHOST_COUNT) as _}
{/each}
+
+ {/if}
+
+ {/if}
+
+ {#if recommended.length > 0 || loadingLib}
+
+
+ {#if loadingLib}
+
{#each Array(8) as _}
{/each}
+ {:else}
+
+ {#each recommended.slice(0, ROW_CAP) as m}
+
previewManga.set(m)} on:contextmenu={(e) => { e.preventDefault(); ctx = { x: e.clientX, y: e.clientY, manga: m }; }}>
+ {#if m.inLibrary}
Saved {/if}
+ {m.title}
+
+ {/each}
+ {#each Array(GHOST_COUNT) as _}
{/each}
+
+ {/if}
+
+ {/if}
+
+ {#if popularManga.length > 0 || loadingPopular}
+
+
+ {#if loadingPopular}
+
{#each Array(8) as _}
{/each}
+ {:else if sources.length === 0}
+
No sources installed. Add extensions first.
+ {:else}
+
+ {#each popularManga.slice(0, ROW_CAP) as m}
+
previewManga.set(m)} on:contextmenu={(e) => { e.preventDefault(); ctx = { x: e.clientX, y: e.clientY, manga: m }; }}>
+ {#if m.inLibrary}
Saved {/if}
+ {m.title}
+
+ {/each}
+ {#each Array(GHOST_COUNT) as _}
{/each}
+
+ {/if}
+
+ {/if}
+
+ {#each frecencyGenres as genre}
+ {@const items = genreResultsMap.get(genre) ?? []}
+ {@const isLoading = loadingGenres && items.length === 0}
+ {#if isLoading || items.length > 0}
+
+
+ {#if isLoading}
+
{#each Array(8) as _}
{/each}
+ {:else}
+
+ {#each items.slice(0, ROW_CAP) as m}
+
previewManga.set(m)} on:contextmenu={(e) => { e.preventDefault(); ctx = { x: e.clientX, y: e.clientY, manga: m }; }}>
+ {#if m.inLibrary}
Saved {/if}
+ {m.title}
+
+ {/each}
+ {#if items.length >= ROW_CAP}
+
genreFilter.set(genre)}>
+
+
+
Explore more
+
{genre}
+
+
+ {/if}
+ {#each Array(GHOST_COUNT) as _}
{/each}
+
+ {/if}
+
+ {/if}
+ {/each}
+
+ {#if !loadingLib && !loadingPopular && !loadingGenres && continueReading.length === 0 && recommended.length === 0 && popularManga.length === 0 && frecencyGenres.every((g) => !genreResultsMap.get(g)?.length)}
+
+ {#if loadError}
+ Could not reach Suwayomi
+ Make sure the server is running, then try again.
+ { loadingLib = true; loadingPopular = true; retryCount++; }}>Retry
+ {:else}
+ Nothing to explore yet
+ Add manga to your library or install sources to get started.
+ {/if}
+
+ {/if}
+
+
+
+ {#if mode === "sources"}
{/if}
+
+{/if}
+
+{#if ctx}
+
ctx = null} />
+{/if}
+
+
diff --git a/src/components/settings/Settings.svelte b/src/components/settings/Settings.svelte
index 3fe9a03..9efe8a9 100644
--- a/src/components/settings/Settings.svelte
+++ b/src/components/settings/Settings.svelte
@@ -2,6 +2,7 @@
import { tick } from "svelte";
import { X, Book, Image, Sliders, Info, Keyboard, Gear, HardDrives, FolderSimple, Plus, Pencil, Trash, Wrench, PaintBrush } from "phosphor-svelte";
import { invoke } from "@tauri-apps/api/core";
+ import { getVersion } from "@tauri-apps/api/app";
import { gql } from "../../lib/client";
import { GET_DOWNLOADS_PATH } from "../../lib/queries";
import { store, updateSettings, resetKeybinds, addFolder, removeFolder, renameFolder, toggleFolderTab, clearHistory, wipeAllData, setSettingsOpen } from "../../store/state.svelte";
@@ -195,6 +196,33 @@
let splashTriggered = $state(false);
+
+ let appVersion = $state("…");
+ let latestVersion = $state(null);
+ let checkingUpdate = $state(false);
+ let updateError = $state(null);
+
+ $effect(() => {
+ if (tab === "about") {
+ getVersion().then(v => appVersion = v).catch(() => appVersion = "unknown");
+ }
+ });
+
+ async function checkForUpdate() {
+ checkingUpdate = true; updateError = null; latestVersion = null;
+ try {
+ const res = await fetch("https://api.github.com/repos/Youwes09/Moku/releases/latest", {
+ method: "GET",
+ headers: { "User-Agent": "Moku" },
+ });
+ const data = await res.json() as { tag_name: string };
+ latestVersion = data.tag_name.replace(/^v/, "");
+ } catch (e) {
+ updateError = "Could not reach GitHub";
+ } finally {
+ checkingUpdate = false;
+ }
+ }
function triggerSplash() {
splashTriggered = true;
setTimeout(() => splashTriggered = false, 200);
@@ -695,7 +723,41 @@
Moku
A manga reader frontend for Suwayomi / Tachidesk.
-
Built with Tauri + Svelte. Connects to tachidesk-server.
+
Built with Tauri + Svelte.
+
+
+
+
Version
+
+
+ Current version
+ v{appVersion}
+
+
+ {checkingUpdate ? "Checking…" : "Check for updates"}
+
+
+ {#if updateError}
+
{updateError}
+ {:else if latestVersion !== null}
+ {#if latestVersion === appVersion}
+
✓ You are on the latest version
+ {:else}
+
+ {/if}
+ {/if}
+
+
diff --git a/src/store/state.svelte.ts b/src/store/state.svelte.ts
index 5a3655e..106c0f0 100644
--- a/src/store/state.svelte.ts
+++ b/src/store/state.svelte.ts
@@ -132,7 +132,7 @@ export const DEFAULT_SETTINGS: Settings = {
compactSidebar: false,
gpuAcceleration: true,
serverUrl: "http://localhost:4567",
- serverBinary: "tachidesk-server",
+ serverBinary: "",
autoStartServer: true,
preferredExtensionLang: "en",
keybinds: DEFAULT_KEYBINDS,
@@ -151,6 +151,14 @@ export const DEFAULT_SETTINGS: Settings = {
// ── Persistence ───────────────────────────────────────────────────────────────
+const STORE_VERSION = 2;
+
+// Fields reset to their DEFAULT_SETTINGS value on each version bump.
+// Add a key here whenever its default changes meaning between releases.
+const RESET_ON_UPGRADE: (keyof Settings)[] = [
+ "serverBinary",
+];
+
function loadPersisted(): any {
try {
const raw = localStorage.getItem("moku-store");
@@ -167,7 +175,26 @@ function persist(patch: Record) {
} catch {}
}
-const saved = loadPersisted();
+const saved = (() => {
+ const data = loadPersisted();
+ if (!data) return null;
+ if ((data.storeVersion ?? 1) < STORE_VERSION) {
+ const resetPatch: Partial = {};
+ for (const key of RESET_ON_UPGRADE) {
+ (resetPatch as any)[key] = (DEFAULT_SETTINGS as any)[key];
+ }
+ const migrated = {
+ ...data,
+ storeVersion: STORE_VERSION,
+ settings: { ...data.settings, ...resetPatch },
+ };
+ try {
+ localStorage.setItem("moku-store", JSON.stringify(migrated));
+ } catch {}
+ return migrated;
+ }
+ return data;
+})();
function mergeSettings(saved: any): Settings {
const userFolders: Folder[] = saved?.settings?.folders ?? [];
@@ -222,6 +249,7 @@ class Store {
constructor() {
$effect.root(() => {
+ $effect(() => { persist({ storeVersion: STORE_VERSION }); });
$effect(() => { persist({ navPage: this.navPage }); });
$effect(() => { persist({ libraryFilter: this.libraryFilter }); });
$effect(() => { persist({ history: this.history }); });
diff --git a/vite.config.ts b/vite.config.ts
index e78c010..555abb8 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,14 +1,21 @@
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
+import path from "path";
export default defineConfig({
plugins: [svelte()],
clearScreen: false,
+ resolve: {
+ alias: {
+ $store: path.resolve("./src/store"),
+ $components: path.resolve("./src/components"),
+ },
+ },
server: {
port: 1420,
strictPort: true,
watch: {
- ignored: ["**/.flatpak-builder/**", "**/src-tauri/**"],
+ ignored: ["**/src-tauri/**"],
},
},
envPrefix: ["VITE_", "TAURI_"],