{ if (e.target === e.currentTarget) close(); }} onkeydown={(e) => { if (e.key === "Escape") close(); }}>

Settings

{#each TABS as t} {/each}
{#each TABS as t} {#if t.id === tab} {/if} {/each}

{TABS.find((t) => t.id === tab)?.label}

{#if tab === "general"}

Interface Scale

updateSettings({ uiZoom: Number(e.currentTarget.value) / 100 })} class="scale-slider" /> { const n = parseInt(e.currentTarget.value, 10); if (!isNaN(n) && n >= 50 && n <= 200) updateSettings({ uiZoom: n / 100 }); }} onblur={(e) => { const n = parseInt(e.currentTarget.value, 10); if (isNaN(n) || n < 50) { updateSettings({ uiZoom: 0.5 }); e.currentTarget.value = "50"; } else if (n > 200) { updateSettings({ uiZoom: 2.0 }); e.currentTarget.value = "200"; } }} /> %

{#each [50,60,70,80,90,100,110,125,150,175,200] as v} {/each}

Server

Server URLBase URL of your Suwayomi instance
updateSettings({ serverUrl: e.currentTarget.value })} placeholder="http://localhost:4567" spellcheck="false" />

Inactivity

Idle screen timeoutShow the Moku idle splash after this much inactivity.
{#if selectOpen === "idle-timeout"}
{#each [["0","Never"],["1","1 minute"],["2","2 minutes"],["5","5 minutes"],["10","10 minutes"],["15","15 minutes"],["30","30 minutes"]] as [v, l]} {/each}
{/if}

Integrations

Language

Preferred source language Used to pre-select languages in Search and deduplicate sources
updateSettings({ preferredExtensionLang: e.currentTarget.value.trim().toLowerCase() })} placeholder="e.g. en" spellcheck="false" style="width:72px;text-align:center;text-transform:uppercase" />
{:else if tab === "appearance"}

Theme

{#each THEMES as theme} {@const active = (store.settings.theme ?? "dark") === theme.id}
{/each} {#each store.settings.customThemes ?? [] as custom} {@const active = store.settings.theme === custom.id}
{#if active}✓{/if}
{/each}
{:else if tab === "reader"}

Page Layout

Default layoutHow chapters open by default
{#if selectOpen === "page-style"}
{#each [["single","Single page"],["longstrip","Long strip"]] as [v, l]} {/each}
{/if}
Reading directionLeft-to-right for most manga, right-to-left for Japanese
{#if selectOpen === "reading-dir"}
{#each [["ltr","Left to right"],["rtl","Right to left"]] as [v, l]} {/each}
{/if}

Fit & Zoom

Default fit mode
{#if selectOpen === "fit-mode"}
{#each [["width","Fit width"],["height","Fit height"],["screen","Fit screen"],["original","Original (1:1)"]] as [v, l]} {/each}
{/if}
Default zoom 100% = fills the reader
updateSettings({ readerZoom: Number(e.currentTarget.value) / 100 })} class="scale-slider" /> { const n = parseInt(e.currentTarget.value, 10); if (!isNaN(n) && n >= 10 && n <= 400) updateSettings({ readerZoom: n / 100 }); }} onblur={(e) => { const n = parseInt(e.currentTarget.value, 10); if (isNaN(n) || n < 10) { updateSettings({ readerZoom: 0.1 }); e.currentTarget.value = "10"; } else if (n > 400) { updateSettings({ readerZoom: 4.0 }); e.currentTarget.value = "400"; } }} /> %

{#each [50, 75, 100, 125, 150, 200] as v} {/each}

Behaviour

{#if !(store.settings.autoNextChapter ?? false)} {/if}
Pages to preload
{store.settings.preloadPages}
{:else if tab === "library"}

Display

Chapters

Default sort direction
{#if selectOpen === "sort-dir"}
{#each [["desc","Newest first"],["asc","Oldest first"]] as [v, l]} {/each}
{/if}

History

Reading history{store.history.length} entries
Wipe all data History, stats, pins, and manga links
{:else if tab === "performance"}

Render Limit

Items per page Lower = faster on large libraries
{store.settings.renderLimit ?? 48}

{#each [12, 24, 48, 96, 200] as v} {/each}

Rendering

Idle / Splash Screen

Interface

Session Cache

Cache entries In-memory, cleared on restart
{perfSnapshot?.cacheEntries ?? 0} entries
{#if perfSnapshot && perfSnapshot.cacheEntries > 0}
Oldest entry
{fmtAge(perfSnapshot.oldestEntryMs)}
Newest entry
{fmtAge(perfSnapshot.newestEntryMs)}
Cached keys {perfSnapshot.cacheKeys.join(", ")}
{/if}
{:else if tab === "keybinds"}

Keyboard shortcuts

Click a binding to rebind, then press the new key combination.

{#each Object.keys(KEYBIND_LABELS) as key} {@const k = key as keyof Keybinds} {@const isListening = listeningKey === k} {@const isDefault = store.settings.keybinds[k] === DEFAULT_KEYBINDS[k]}
{KEYBIND_LABELS[k]}
{/each}
{:else if tab === "storage"}
{#if migrateFrom && !isExternalServer}
Manga found at previous path — move to new location? {migrateFrom} → {migrateTo} {#if migrateProgress && migrateProgress.total > 0}
{migrateProgress.current} {migrateProgress.done} / {migrateProgress.total}
{/if} {#if migrateError}{migrateError}{/if}
{/if}

Disk Usage

{#if storageLoading}

Reading filesystem…

{:else if storageError}

{storageError}

{:else if isExternalServer}

Disk usage is unavailable for external servers — filesystem access requires a local connection.

{:else if multiStorageInfos.length > 0} {#each multiStorageInfos as info} {@const limitGb = store.settings.storageLimitGb ?? null} {@const limitBytes = limitGb !== null ? limitGb * 1024 ** 3 : null} {@const available = info.manga_bytes + info.free_bytes} {@const cap = limitBytes !== null ? Math.min(limitBytes, available) : available} {@const pct = cap > 0 ? Math.min(100, (info.manga_bytes / cap) * 100) : 0}
{info.label} {fmtBytes(info.manga_bytes)} of {fmtBytes(cap)}
90} class:warn={pct > 75 && pct <= 90} style="width:{pct}%">
{info.path} {fmtBytes(info.free_bytes)} free
{/each} {:else}

No download path configured.

{/if}

Downloads Path

{#if isExternalServer}

Connected to an external server. The path below is read from the server — changes here will update the server's config directly. Make sure the path is valid on the server's filesystem.

{/if}
e.key === "Enter" && savePaths()} oninput={() => { pathsFieldError = { ...pathsFieldError, dl: undefined }; }} />
{#if pathsFieldError.dl} {pathsFieldError.dl} {#if !isExternalServer} {/if} {/if} {#if pathsError}{pathsError}{/if}

Storage Limit

Warn when limit is reached {store.settings.storageLimitGb === null ? "No limit set" : `Warn above ${store.settings.storageLimitGb} GB`}
{#if store.settings.storageLimitGb === null} {:else}
{ const n = parseFloat(e.currentTarget.value); if (!isNaN(n) && n > 0) updateSettings({ storageLimitGb: n }); }} /> GB
{/if}

Cache

Image cacheWebview page image cache
{#if advStorageOpen}
Local source path Read manga already on disk without an extension. Leave blank if unused.
e.key === "Enter" && savePaths()} oninput={() => { pathsFieldError = { ...pathsFieldError, loc: undefined }; }} /> {#if pathsFieldError.loc && !isExternalServer} {/if}
{#if pathsFieldError.loc}{pathsFieldError.loc}{/if}
{#each extraScanDirs as dir}
{dir} Extra scan directory
{/each}
Additional scan path Include an extra directory in disk usage readings
e.key === "Enter" && addExtraScanDir()} />
{/if}
{#if backupSectionOpen}
Create backup Snapshot your library, categories, and tracker links
{#if backupError}

{backupError}

{/if} {#if backupList.length === 0}

No backups yet — create one above.

{:else}
{#each backupList as backup}
{backup.name}
{/each}
{/if}
Restore from file {restoreFile ? restoreFile.name : "Select a .tachibk file"}
{#if restoreFile}
{/if} {#if validateError}

{validateError}

{/if} {#if validateResult} {#if validateResult.missingSources.length === 0 && validateResult.missingTrackers.length === 0}

✓ All sources and trackers present

{:else} {#if validateResult.missingSources.length > 0}
Missing sources {validateResult.missingSources.map(s => s.name).join(", ")}
{/if} {#if validateResult.missingTrackers.length > 0}
Missing trackers {validateResult.missingTrackers.map(t => t.name).join(", ")}
{/if} {/if} {/if} {#if restoreError}

{restoreError}

{/if} {#if restoreStatus}
{restoreStatus.state === "SUCCESS" ? "✓ Restore complete" : restoreStatus.state === "FAILURE" ? "✗ Restore failed" : "Restoring…"} {#if restoreStatus.totalManga > 0} {restoreStatus.mangaProgress} / {restoreStatus.totalManga} manga {/if}
{#if restoreStatus.state !== "SUCCESS" && restoreStatus.state !== "FAILURE" && restoreStatus.totalManga > 0}
{/if}
{/if}
{/if}
{:else if tab === "folders"}

Manage Folders

Folders are stored as Suwayomi categories. Changes sync across all clients.

{#if catsError}

{catsError}

{/if}
e.key === "Enter" && createFolder()} style="flex:1;width:auto" />
{#if catsLoading}

Loading folders…

{:else if store.categories.filter(c => c.id !== 0).length === 0}

No folders yet. Create one above.

{:else} {@const displayCats = store.categories .filter(c => c.id !== 0) .sort((a, b) => { const defaultId = store.settings.defaultLibraryCategoryId ?? null; if (a.id === defaultId) return -1; if (b.id === defaultId) return 1; return a.order - b.order; })}
{#each displayCats as cat, i}
{#if editingId === cat.id} { if (e.key === "Enter") commitEdit(); if (e.key === "Escape") editingId = null; }} onblur={commitEdit} style="flex:1;width:auto" use:focusInput /> {:else} {cat.name} {cat.mangas?.nodes.length ?? 0} manga {/if}
{/each}
{/if}
{:else if tab === "tracking"}

Connected Trackers

{#if trackersError}
{trackersError}
{/if} {#if trackersLoading}

Loading trackers…

{:else}
{#each trackers as tracker}
{tracker.name}
{tracker.name} {tracker.isLoggedIn ? "Connected" : "Not connected"}
{#if tracker.isLoggedIn}
{:else if oauthTrackerId === tracker.id}

Browser opened {tracker.name} login — after authorising, copy the full callback URL and paste it below.

{ if (e.key === "Enter") submitOAuth(); if (e.key === "Escape") cancelOAuth(); }} use:focusEl />
{:else if credsTrackerId === tracker.id}
e.key === "Escape" && cancelCredentials()} use:focusEl /> { if (e.key === "Enter") submitCredentials(); if (e.key === "Escape") cancelCredentials(); }} />
{:else} {/if}
{/each}
{/if}
{:else if tab === "security"}
{#if secError}
{secError}
{/if}

Server Authentication

{store.settings.serverAuthMode === "BASIC_AUTH" ? "Basic Auth" : store.settings.serverAuthMode === "SIMPLE_LOGIN" ? "Simple Login — unsupported" : store.settings.serverAuthMode === "UI_LOGIN" ? "UI Login — unsupported" : "Disabled"}
{#if authModeUnsupported}
{store.settings.serverAuthMode === "SIMPLE_LOGIN" ? "Simple Login" : "UI Login"} is not supported by this client — only Basic Auth works here. Switch your Suwayomi server to basic_auth and set the mode below to Basic, then save.
{/if}
Mode How Suwayomi verifies requests
{#each [ { value: "NONE", label: "None" }, { value: "BASIC_AUTH", label: "Basic" }, ] as opt} {/each}
{#if authMode !== "NONE"}
Username
Password
{/if} {#if store.settings.serverAuthMode === "BASIC_AUTH"}

Images are proxied through Tauri when Basic Auth is active, which reduces loading speed.

{/if}
{#if store.settings.serverAuthMode !== "NONE"} {/if}

App Lock

{#if store.settings.appLockEnabled}
PIN 4–8 digits, saved on Enter or Save button
{ pinInput = e.currentTarget.value.replace(/\D/g, "").slice(0, 8); pinError = ""; }} onkeydown={(e) => e.key === "Enter" && commitPin()} autocomplete="off" aria-label="Enter PIN" style="width:120px;letter-spacing:0.2em" />
{#if pinError}{pinError}{/if}
{/if}

SOCKS Proxy

{#if socksEnabled}
Version
{#if selectOpen === "socks-ver"}
{#each [[4,"SOCKS4"],[5,"SOCKS5"]] as [v, l]} {/each}
{/if}
Host
Port
Username Optional
Password Optional
{/if}

FlareSolverr

{#if flareEnabled}
URL FlareSolverr instance address
Timeout Max wait per request, in seconds
{flareTimeout}s
Session name Reuse browser session across requests
Session TTL Minutes before session is refreshed
{flareTtl}m
{/if}
{:else if tab === "content"}

Content Filter

Blocked Genre Tags

Manga matching any of these substrings are filtered. Case-insensitive, partial match.
{#if tagsRevealed}
{#each (store.settings.nsfwFilteredTags ?? []) as tag} {tag} {/each}
{/if}
{ if (e.key === "Enter") addTag(); }} style="flex:1;width:auto" />

Source Overrides

Allow lets a source through even if flagged NSFW. Block always hides it.

{#if contentSourcesLoading}

Loading sources…

{:else if contentSources.length === 0}

No sources found — check your server connection.

{:else}
{#each contentSourcesFiltered as group (group.name)} {@const ids = group.sources.map(s => s.id)} {@const allowed = store.settings.nsfwAllowedSourceIds ?? []} {@const blocked = store.settings.nsfwBlockedSourceIds ?? []} {@const isAllowed = ids.every(id => allowed.includes(id))} {@const isBlocked = ids.every(id => blocked.includes(id))}
{group.name} {group.sources[0].isNsfw ? "NSFW · " : ""}{group.sources.length > 1 ? `${group.sources.length} languages` : group.sources[0].lang.toUpperCase()}
{/each}
{/if}
{:else if tab === "about"}

Moku

A manga reader frontend for Suwayomi / Tachidesk.

Built with Tauri + Svelte.

Version

Installed v{appVersion}
{#if onLatestVersion}
✓ You're on the latest version.
{/if} {#if updatePhase === "downloading" && IS_WINDOWS}
Downloading {targetTag ?? "update"}… {fmtProgress()}
{/if} {#if updatePhase === "ready"}
{targetTag} downloaded — restart to finish installing.
{/if} {#if updatePhase === "error"}
{updateError}
{/if}

Releases

{#if releasesError}

{releasesError}

{:else if releasesLoading}

Fetching releases…

{:else if releases.length === 0}

No releases found.

{:else}
{#each releases as release} {@const isCurrent = isCurrentVersion(release.tag_name)} {@const isExpanded = expandedTag === release.tag_name} {@const isTarget = targetTag === release.tag_name} {@const isInstalling = isTarget && updatePhase === "downloading"}
{release.tag_name} {#if isCurrent} installed {/if} {#if release.published_at} {fmtDate(release.published_at)} {/if}
{#if release.body.trim()} {/if} {#if !isCurrent} {#if IS_WINDOWS} {:else} {/if} {/if}
{#if isExpanded && release.body.trim()}
{release.body.trim()}
{/if}
{/each}
{/if}

Links

GitHub → Discord →
{:else if tab === "devtools"}

Toasts

Fire test toastTriggers each kind with realistic content
{#each ([["success","S"],["error","E"],["info","I"],["download","D"]] as const) as [kind, label]} {/each}

Previews

Idle splashDismiss with any click or key
{#if expOpen}

3D tilt cards — hover to preview

{#each [ { title: "Berserk", sub: "Ch. 372", hue: "265" }, { title: "Vinland Saga", sub: "Ch. 208", hue: "200" }, { title: "Dungeon Meshi", sub: "Ch. 97", hue: "140" }, ] as card}
{card.title} {card.sub}
{/each}
{/if}

Runtime

Filter {store.libraryFilter} Folders {store.categories.filter(c => c.id !== 0).map(c => c.name).join(", ") || "none"} History {store.history.length} entries Cache {perfSnapshot?.cacheEntries ?? "—"} entries Toasts {store.toasts.length} queued Version {appVersion} · {import.meta.env.MODE}
{/if}