Cleanup routes and ux

This commit is contained in:
Zerebos
2026-05-23 21:19:07 -04:00
parent 5e2114810e
commit 71ee4052f3
4 changed files with 260 additions and 66 deletions
+119 -1
View File
@@ -11,6 +11,7 @@
import { notificationsState } from '$lib/state/notifications.svelte' import { notificationsState } from '$lib/state/notifications.svelte'
import { readerState } from '$lib/state/reader.svelte' import { readerState } from '$lib/state/reader.svelte'
import { clearDiscordPresence, isSupported, setDiscordPresence } from '$lib/platform-service' import { clearDiscordPresence, isSupported, setDiscordPresence } from '$lib/platform-service'
import { loadDownloads } from '$lib/request-manager/downloads'
import SplashScreen from '$lib/ui/chrome/SplashScreen.svelte' import SplashScreen from '$lib/ui/chrome/SplashScreen.svelte'
import AuthGate from '$lib/ui/chrome/AuthGate.svelte' import AuthGate from '$lib/ui/chrome/AuthGate.svelte'
import Sidebar from '$lib/ui/chrome/Sidebar.svelte' import Sidebar from '$lib/ui/chrome/Sidebar.svelte'
@@ -22,6 +23,7 @@
let splashVisible = $state(true) let splashVisible = $state(true)
let bypassed = $state(false) let bypassed = $state(false)
let closeDialogOpen = $state(false)
const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
const pathname = $derived($page.url.pathname as string) const pathname = $derived($page.url.pathname as string)
@@ -32,6 +34,32 @@
const showShell = $derived(appState.status === 'ready' || bypassed) const showShell = $derived(appState.status === 'ready' || bypassed)
const splashCards = $derived(settingsState.splashCards ?? true) const splashCards = $derived(settingsState.splashCards ?? true)
async function handleClose() {
if (!isTauri) return
const { getCurrentWindow } = await import('@tauri-apps/api/window')
const win = getCurrentWindow()
const action = settingsState.closeAction
if (action === 'tray') {
await win.hide()
} else if (action === 'ask') {
closeDialogOpen = true
} else {
await win.close()
}
}
async function confirmQuit() {
closeDialogOpen = false
const { getCurrentWindow } = await import('@tauri-apps/api/window')
await getCurrentWindow().close()
}
async function confirmTray() {
closeDialogOpen = false
const { getCurrentWindow } = await import('@tauri-apps/api/window')
await getCurrentWindow().hide()
}
function canUseDiscordRpc(): boolean { function canUseDiscordRpc(): boolean {
try { try {
return isSupported('discord-rpc') return isSupported('discord-rpc')
@@ -141,10 +169,45 @@
}) })
} }
const DOWNLOAD_POLL_MS = 8_000
let downloadPollId: ReturnType<typeof setInterval> | null = null
function startDownloadPolling() {
if (downloadPollId !== null) return
void loadDownloads()
downloadPollId = setInterval(() => {
void loadDownloads()
}, DOWNLOAD_POLL_MS)
}
function stopDownloadPolling() {
if (downloadPollId !== null) {
clearInterval(downloadPollId)
downloadPollId = null
}
}
if (appState.status === 'ready') {
startDownloadPolling()
}
const stopStatusWatch = $effect.root(() => {
$effect(() => {
if (appState.status === 'ready') {
startDownloadPolling()
} else {
stopDownloadPolling()
}
})
return () => {}
})
return () => { return () => {
appState.idle = false appState.idle = false
stopZoomKey() stopZoomKey()
stopIdleDetection() stopIdleDetection()
stopDownloadPolling()
stopStatusWatch()
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
unmountSystemThemeSync() unmountSystemThemeSync()
stopTauriScale?.() stopTauriScale?.()
@@ -175,7 +238,7 @@
<div class="frame"> <div class="frame">
<div class="shell"> <div class="shell">
{#if isTauri} {#if isTauri}
<TitleBar onClose={() => import('@tauri-apps/api/window').then(m => m.getCurrentWindow().close())} /> <TitleBar onClose={handleClose} />
{/if} {/if}
<div class="body"> <div class="body">
<Sidebar /> <Sidebar />
@@ -193,6 +256,20 @@
{/if} {/if}
<Toaster toasts={notificationsState.toasts} /> <Toaster toasts={notificationsState.toasts} />
{#if closeDialogOpen}
<div class="close-dialog-backdrop" role="presentation" onclick={() => (closeDialogOpen = false)}>
<div class="close-dialog" role="dialog" aria-modal="true" aria-labelledby="close-dialog-title">
<p id="close-dialog-title" class="close-dialog-title">Close Moku?</p>
<p class="close-dialog-desc">Choose what to do when closing the window.</p>
<div class="close-dialog-actions">
<button type="button" class="settings-button" onclick={confirmTray}>Minimize to tray</button>
<button type="button" class="settings-button danger" onclick={confirmQuit}>Quit</button>
<button type="button" class="settings-button" onclick={() => (closeDialogOpen = false)}>Cancel</button>
</div>
</div>
</div>
{/if}
<style> <style>
.frame { .frame {
display: flex; display: flex;
@@ -237,4 +314,45 @@
overflow: hidden; overflow: hidden;
background: var(--bg-base); background: var(--bg-base);
} }
.close-dialog-backdrop {
position: fixed;
inset: 0;
z-index: 9000;
background: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
}
.close-dialog {
background: var(--bg-overlay);
border: 1px solid var(--border-base);
border-radius: var(--radius-xl);
padding: var(--sp-5) var(--sp-6);
min-width: 280px;
display: flex;
flex-direction: column;
gap: var(--sp-3);
}
.close-dialog-title {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
}
.close-dialog-desc {
margin: 0;
font-size: 0.875rem;
color: var(--text-muted);
}
.close-dialog-actions {
display: flex;
gap: var(--sp-2);
justify-content: flex-end;
margin-top: var(--sp-1);
}
</style> </style>
+1
View File
@@ -3,6 +3,7 @@
const sections = [ const sections = [
['general', 'General'], ['general', 'General'],
['server', 'Server'],
['appearance', 'Appearance'], ['appearance', 'Appearance'],
['reader', 'Reader'], ['reader', 'Reader'],
['library', 'Library'], ['library', 'Library'],
+1 -65
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { settingsState, updateSettings } from '$lib/state/settings.svelte' import { settingsState, updateSettings } from '$lib/state/settings.svelte'
let advancedOpen = $state(false)
const idleChoices = [0, 1, 2, 5, 10, 15, 30] const idleChoices = [0, 1, 2, 5, 10, 15, 30]
</script> </script>
@@ -13,7 +12,7 @@
<header class="settings-page-header"> <header class="settings-page-header">
<p class="settings-kicker">General</p> <p class="settings-kicker">General</p>
<h2>Application basics</h2> <h2>Application basics</h2>
<p>Core behavior, server connection, and desktop shell preferences.</p> <p>Core behavior and desktop shell preferences.</p>
</header> </header>
<div class="settings-card"> <div class="settings-card">
@@ -44,69 +43,6 @@
</div> </div>
</div> </div>
<div class="settings-row">
<div>
<div class="settings-label">Server URL</div>
<div class="settings-desc">Base URL for the Suwayomi server.</div>
</div>
<input
class="settings-input settings-input-wide"
spellcheck="false"
value={settingsState.serverUrl}
oninput={(event) => updateSettings({serverUrl: (event.currentTarget as HTMLInputElement).value})}
/>
</div>
<label class="settings-row settings-toggle-row">
<div>
<div class="settings-label">Auto-start server</div>
<div class="settings-desc">Launch the server when Moku starts.</div>
</div>
<input type="checkbox" checked={settingsState.autoStartServer} onchange={() => updateSettings({autoStartServer: !settingsState.autoStartServer})} />
</label>
<label class="settings-row settings-toggle-row">
<div>
<div class="settings-label">Suwayomi Web UI</div>
<div class="settings-desc">Keep the server's web UI enabled alongside Moku.</div>
</div>
<input type="checkbox" checked={settingsState.suwayomiWebUI} onchange={() => updateSettings({suwayomiWebUI: !settingsState.suwayomiWebUI})} />
</label>
<div class="settings-row settings-row-stack">
<div class="settings-row-head">
<div>
<div class="settings-label">Advanced server options</div>
<div class="settings-desc">Custom binary path and launch args.</div>
</div>
<button class="settings-button" type="button" onclick={() => advancedOpen = !advancedOpen}>{advancedOpen ? 'Hide' : 'Show'}</button>
</div>
{#if advancedOpen}
<div class="settings-subcard">
<label class="settings-mini-row">
<span class="settings-label">Server binary</span>
<input
class="settings-input settings-input-wide"
spellcheck="false"
placeholder="auto-detect"
value={settingsState.serverBinary}
oninput={(event) => updateSettings({serverBinary: (event.currentTarget as HTMLInputElement).value})}
/>
</label>
<label class="settings-mini-row">
<span class="settings-label">Server args</span>
<input
class="settings-input settings-input-wide"
spellcheck="false"
placeholder=""
value={settingsState.serverBinaryArgs}
oninput={(event) => updateSettings({serverBinaryArgs: (event.currentTarget as HTMLInputElement).value})}
/>
</label>
</div>
{/if}
</div>
<div class="settings-row"> <div class="settings-row">
<div> <div>
<div class="settings-label">Idle screen timeout</div> <div class="settings-label">Idle screen timeout</div>
+139
View File
@@ -0,0 +1,139 @@
<script lang="ts">
import { settingsState, updateSettings } from '$lib/state/settings.svelte'
let advancedOpen = $state(false)
</script>
<svelte:head>
<title>Settings - Server</title>
</svelte:head>
<section class="settings-page">
<header class="settings-page-header">
<p class="settings-kicker">Server</p>
<h2>Server connection</h2>
<p>Configure the Suwayomi server URL, launch behavior, and file paths.</p>
</header>
<div class="settings-card">
<div class="settings-row">
<div>
<div class="settings-label">Server URL</div>
<div class="settings-desc">Base URL for the Suwayomi server.</div>
</div>
<input
class="settings-input settings-input-wide"
spellcheck="false"
value={settingsState.serverUrl}
oninput={(event) => updateSettings({ serverUrl: (event.currentTarget as HTMLInputElement).value })}
/>
</div>
<label class="settings-row settings-toggle-row">
<div>
<div class="settings-label">Auto-start server</div>
<div class="settings-desc">Launch the server when Moku starts.</div>
</div>
<input
type="checkbox"
checked={settingsState.autoStartServer}
onchange={() => updateSettings({ autoStartServer: !settingsState.autoStartServer })}
/>
</label>
<label class="settings-row settings-toggle-row">
<div>
<div class="settings-label">Suwayomi Web UI</div>
<div class="settings-desc">Keep the server's web UI enabled alongside Moku.</div>
</div>
<input
type="checkbox"
checked={settingsState.suwayomiWebUI}
onchange={() => updateSettings({ suwayomiWebUI: !settingsState.suwayomiWebUI })}
/>
</label>
<div class="settings-row settings-row-stack">
<div class="settings-row-head">
<div>
<div class="settings-label">Advanced launch options</div>
<div class="settings-desc">Custom binary path and launch args.</div>
</div>
<button class="settings-button" type="button" onclick={() => (advancedOpen = !advancedOpen)}>
{advancedOpen ? 'Hide' : 'Show'}
</button>
</div>
{#if advancedOpen}
<div class="settings-subcard">
<label class="settings-mini-row">
<span class="settings-label">Server binary</span>
<input
class="settings-input settings-input-wide"
spellcheck="false"
placeholder="auto-detect"
value={settingsState.serverBinary}
oninput={(event) => updateSettings({ serverBinary: (event.currentTarget as HTMLInputElement).value })}
/>
</label>
<label class="settings-mini-row">
<span class="settings-label">Server args</span>
<input
class="settings-input settings-input-wide"
spellcheck="false"
placeholder=""
value={settingsState.serverBinaryArgs}
oninput={(event) => updateSettings({ serverBinaryArgs: (event.currentTarget as HTMLInputElement).value })}
/>
</label>
</div>
{/if}
</div>
</div>
<div class="settings-card">
<div class="settings-row">
<div>
<div class="settings-label">Downloads path</div>
<div class="settings-desc">Directory where the server stores downloaded chapters.</div>
</div>
<input
class="settings-input settings-input-wide"
spellcheck="false"
placeholder="server default"
value={settingsState.serverDownloadsPath}
oninput={(event) => updateSettings({ serverDownloadsPath: (event.currentTarget as HTMLInputElement).value })}
/>
</div>
<div class="settings-row">
<div>
<div class="settings-label">Local source path</div>
<div class="settings-desc">Directory scanned for local manga sources.</div>
</div>
<input
class="settings-input settings-input-wide"
spellcheck="false"
placeholder="server default"
value={settingsState.serverLocalSourcePath}
oninput={(event) => updateSettings({ serverLocalSourcePath: (event.currentTarget as HTMLInputElement).value })}
/>
</div>
<div class="settings-row">
<div>
<div class="settings-label">Extra scan directories</div>
<div class="settings-desc">Comma-separated additional directories the server should scan.</div>
</div>
<input
class="settings-input settings-input-wide"
spellcheck="false"
placeholder=""
value={settingsState.extraScanDirs.join(', ')}
oninput={(event) => {
const raw = (event.currentTarget as HTMLInputElement).value
updateSettings({ extraScanDirs: raw.split(',').map((s) => s.trim()).filter(Boolean) })
}}
/>
</div>
</div>
</section>