mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Cleanup routes and ux
This commit is contained in:
+119
-1
@@ -11,6 +11,7 @@
|
||||
import { notificationsState } from '$lib/state/notifications.svelte'
|
||||
import { readerState } from '$lib/state/reader.svelte'
|
||||
import { clearDiscordPresence, isSupported, setDiscordPresence } from '$lib/platform-service'
|
||||
import { loadDownloads } from '$lib/request-manager/downloads'
|
||||
import SplashScreen from '$lib/ui/chrome/SplashScreen.svelte'
|
||||
import AuthGate from '$lib/ui/chrome/AuthGate.svelte'
|
||||
import Sidebar from '$lib/ui/chrome/Sidebar.svelte'
|
||||
@@ -22,6 +23,7 @@
|
||||
|
||||
let splashVisible = $state(true)
|
||||
let bypassed = $state(false)
|
||||
let closeDialogOpen = $state(false)
|
||||
|
||||
const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
|
||||
const pathname = $derived($page.url.pathname as string)
|
||||
@@ -32,6 +34,32 @@
|
||||
const showShell = $derived(appState.status === 'ready' || bypassed)
|
||||
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 {
|
||||
try {
|
||||
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 () => {
|
||||
appState.idle = false
|
||||
stopZoomKey()
|
||||
stopIdleDetection()
|
||||
stopDownloadPolling()
|
||||
stopStatusWatch()
|
||||
window.removeEventListener('resize', handleResize)
|
||||
unmountSystemThemeSync()
|
||||
stopTauriScale?.()
|
||||
@@ -175,7 +238,7 @@
|
||||
<div class="frame">
|
||||
<div class="shell">
|
||||
{#if isTauri}
|
||||
<TitleBar onClose={() => import('@tauri-apps/api/window').then(m => m.getCurrentWindow().close())} />
|
||||
<TitleBar onClose={handleClose} />
|
||||
{/if}
|
||||
<div class="body">
|
||||
<Sidebar />
|
||||
@@ -193,6 +256,20 @@
|
||||
{/if}
|
||||
<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>
|
||||
.frame {
|
||||
display: flex;
|
||||
@@ -237,4 +314,45 @@
|
||||
overflow: hidden;
|
||||
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>
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
const sections = [
|
||||
['general', 'General'],
|
||||
['server', 'Server'],
|
||||
['appearance', 'Appearance'],
|
||||
['reader', 'Reader'],
|
||||
['library', 'Library'],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { settingsState, updateSettings } from '$lib/state/settings.svelte'
|
||||
|
||||
let advancedOpen = $state(false)
|
||||
const idleChoices = [0, 1, 2, 5, 10, 15, 30]
|
||||
</script>
|
||||
|
||||
@@ -13,7 +12,7 @@
|
||||
<header class="settings-page-header">
|
||||
<p class="settings-kicker">General</p>
|
||||
<h2>Application basics</h2>
|
||||
<p>Core behavior, server connection, and desktop shell preferences.</p>
|
||||
<p>Core behavior and desktop shell preferences.</p>
|
||||
</header>
|
||||
|
||||
<div class="settings-card">
|
||||
@@ -44,69 +43,6 @@
|
||||
</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>
|
||||
<div class="settings-label">Idle screen timeout</div>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user