diff --git a/src/hooks.client.ts b/src/hooks.client.ts index b80e60f..d29d5e9 100644 --- a/src/hooks.client.ts +++ b/src/hooks.client.ts @@ -1,12 +1,13 @@ -import { detectAdapter } from '$lib/platform-adapters' -import { initPlatformService } from '$lib/platform-service' -import { initRequestManager } from '$lib/request-manager' -import { appState } from '$lib/state/app.svelte' -import { configureAuth, probeServer } from '$lib/core/auth' -import { loadSettings, loadLibrary, loadUpdates } from '$lib/core/persistence/persist' -import { loadSettingsIntoState } from '$lib/state/settings.svelte' -import { historyState } from '$lib/state/history.svelte' -import { readerState } from '$lib/state/reader.svelte' +import { detectAdapter } from '$lib/platform-adapters' +import { initPlatformService } from '$lib/platform-service' +import { initRequestManager } from '$lib/request-manager' +import { appState } from '$lib/state/app.svelte' +import { configureAuth, probeServer } from '$lib/core/auth' +import { loadSettings, loadLibrary } from '$lib/core/persistence/persist' +import { loadSettingsIntoState, settingsState } from '$lib/state/settings.svelte' +import { historyState } from '$lib/state/history.svelte' +import { readerState } from '$lib/state/reader.svelte' +import { seriesState } from '$lib/state/series.svelte' const KEY_URL = 'moku_server_url' const KEY_AUTH = 'moku_auth_config' @@ -34,12 +35,11 @@ async function boot() { const [settingsData, libraryData] = await Promise.all([ loadSettings(), loadLibrary(), - loadUpdates(), ]) await loadSettingsIntoState(settingsData.settings) - readerState.bookmarks = libraryData.bookmarks + seriesState.bookmarks = libraryData.bookmarks readerState.markers = libraryData.markers historyState.load(libraryData.sessions, libraryData.dailyReadCounts) @@ -49,8 +49,6 @@ async function boot() { appState.serverUrl = savedUrl appState.authMode = savedAuth.mode - appState.authUser = savedAuth.user ?? '' - appState.authPass = savedAuth.pass ?? '' configureAuth(savedUrl, savedAuth.mode, savedAuth.user, savedAuth.pass) @@ -63,7 +61,7 @@ async function boot() { }) const isTauri = platformAdapter.platform === 'tauri' - const autoStartServer = settingsData.settings.autoStartServer ?? false + const autoStartServer = settingsState.settings.autoStartServer if (isTauri && autoStartServer) { appState.status = 'booting' diff --git a/src/lib/components/browse/GenreDrillPage.svelte b/src/lib/components/browse/GenreDrillPage.svelte index 7e6c9b0..dbe6032 100644 --- a/src/lib/components/browse/GenreDrillPage.svelte +++ b/src/lib/components/browse/GenreDrillPage.svelte @@ -170,7 +170,7 @@ label: "New folder & add", icon: FolderSimplePlusIcon, onClick: async () => { - const name = prompt("FolderIcon name:"); + const name = prompt("Folder name:"); if (!name?.trim()) return; const cat = await getAdapter().createCategory(name.trim()).catch(console.error); if (cat) { diff --git a/src/lib/components/chrome/SplashScreen.svelte b/src/lib/components/chrome/SplashScreen.svelte index 05e5415..38a435e 100644 --- a/src/lib/components/chrome/SplashScreen.svelte +++ b/src/lib/components/chrome/SplashScreen.svelte @@ -43,20 +43,20 @@ const isDev = import.meta.env.DEV interface Props { - mode?: 'loading' | 'idle' | 'locked' - ringFull?: boolean - failed?: boolean - notConfigured?: boolean - showCards?: boolean - showFps?: boolean + mode?: 'loading' | 'idle' | 'locked' + ringFull?: boolean + failed?: boolean + notConfigured?: boolean + showCards?: boolean + showFps?: boolean showDevOverlay?: boolean - pinLen?: number - pinCorrect?: string - onReady?: () => void - onUnlock?: () => void - onRetry?: () => void - onBypass?: () => void - onDismiss?: () => void + pinLen?: number + pinCorrect?: string + onReady?: () => void + onUnlock?: () => void + onRetry?: () => void + onBypass?: () => void + onDismiss?: () => void } let { @@ -97,7 +97,7 @@ setTimeout(() => cb?.(), EXIT_MS) } - let animFrame: number + let animFrame = 0 let animStart: number | null = null let animPhase = 1 @@ -117,19 +117,18 @@ } $effect(() => { - if (mode === 'loading' && !failed && !notConfigured && !ringFull) { - if (!isTauri) return - animStart = null - animPhase = 1 - animFrame = requestAnimationFrame(animateRing) - return () => cancelAnimationFrame(animFrame) - } + if (mode !== 'loading' || failed || notConfigured || ringFull || !isTauri) return + animStart = null + animPhase = 1 + animFrame = requestAnimationFrame(animateRing) + return () => cancelAnimationFrame(animFrame) }) $effect(() => { if (!ringFull || mode === 'locked') { exitLock = false; exiting = false; return } cancelAnimationFrame(animFrame) - ringProg = 1 + animFrame = 0 + ringProg = 1 setTimeout(() => triggerExit(onReady), 650) }) @@ -179,6 +178,7 @@ window.removeEventListener('touchstart', handler) } } + return () => clearInterval(iv) }) @@ -212,7 +212,7 @@ const speed = cfg.speedMin + hash(seed + 5) * (cfg.speedMax - cfg.speedMin) const travel = vh + h + BUF cards.push({ - cx: (col + 0.5) * laneW + (hash(seed + 2) * 2 - 1) * Math.max(0, (laneW - w) / 2 - 2), + cx: (col + 0.5) * laneW + (hash(seed + 2) * 2 - 1) * Math.max(0, (laneW - w) / 2 - 2), w, h, lines: 1 + Math.floor(hash(seed + 7) * 3), alpha: cfg.alpha, @@ -307,11 +307,12 @@ ctx.drawImage(vignette, 0, 0, cw, ch) } - let fps = 0, fpsFrames = 0, fpsLast = 0 + let fpsFrames = 0, fpsLast = -1 function tickFps(now: number) { + if (fpsLast < 0) { fpsLast = now; return } fpsFrames++ if (now - fpsLast >= 500) { - fps = Math.round(fpsFrames / ((now - fpsLast) / 1000)) + const fps = Math.round(fpsFrames / ((now - fpsLast) / 1000)) fpsFrames = 0 fpsLast = now if (fpsEl) fpsEl.textContent = `${fps} fps` @@ -370,7 +371,7 @@ function cleanup() { if (live) { live.stamps.forEach(c => { c.width = 0; c.height = 0 }) - live.vignette.width = 0 + live.vignette.width = 0 live.vignette.height = 0 live = null } @@ -444,14 +445,17 @@ document.addEventListener('visibilitychange', onVis) raf = requestAnimationFrame(frame) - return () => { - cancelAnimationFrame(raf) - cleanup() - extraCleanup?.() - document.removeEventListener('visibilitychange', onVis) - if (isDev && mode === 'idle') { - splashDevUnregister(el) - devLiveCount = splashDevLiveCount() + + return { + destroy() { + cancelAnimationFrame(raf) + cleanup() + extraCleanup?.() + document.removeEventListener('visibilitychange', onVis) + if (isDev && mode === 'idle') { + splashDevUnregister(el) + devLiveCount = splashDevLiveCount() + } } } } @@ -548,7 +552,7 @@ .logo-wrap { position:relative; width:72px; height:72px; display:flex; align-items:center; justify-content:center; } - .pin-card { z-index:1; width:min(280px,calc(100vw - 48px)); background:var(--bg-surface); border:1px solid var(--border-base); border-radius:var(--radius-xl); padding:var(--sp-6) var(--sp-5); display:flex; flex-direction:column; align-items:center; gap:var(--sp-3); box-shadow:0 32px 80px rgba(0,0,0,0.75); animation:cardIn 0.38s cubic-bezier(0.22,1,0.36,1) 0.06s both; } + .pin-card { z-index:1; width:min(280px,calc(100vw - 48px)); background:var(--bg-surface); border:1px solid var(--border-base); border-radius:var(--radius-xl); padding:var(--sp-6) var(--sp-5); display:flex; flex-direction:column; align-items:center; gap:var(--sp-3); box-shadow:0 32px 80px rgba(0,0,0,0.75); animation:cardIn 0.38s cubic-bezier(0.22,1,0.36,1) 0.06s both; } .pin-card--leaving { animation:cardOut 0.28s cubic-bezier(0.4,0,1,1) both; } .pin-label { font-family:var(--font-ui); font-size:var(--text-xs); color:var(--text-faint); letter-spacing:var(--tracking-wider); text-transform:uppercase; margin:0; } diff --git a/src/lib/components/chrome/TitleBar.svelte b/src/lib/components/chrome/TitleBar.svelte index b2a579f..c51f587 100644 --- a/src/lib/components/chrome/TitleBar.svelte +++ b/src/lib/components/chrome/TitleBar.svelte @@ -16,15 +16,22 @@ let closeDialogOpen = $state(false) let closeRemember = $state(false) - onMount(async () => { - isFullscreen = await win.isFullscreen() - const unlistenResize = await win.onResized(async () => { + onMount(() => { + let unlistenResize: (() => void) | undefined + let unlistenClose: (() => void) | undefined + + win.isFullscreen().then(v => { isFullscreen = v }) + + win.onResized(async () => { isFullscreen = await win.isFullscreen() - }) - const unlistenClose = await win.listen('tauri://close-requested', handleCloseRequested) + }).then(u => { unlistenResize = u }) + + win.listen('tauri://close-requested', handleCloseRequested) + .then(u => { unlistenClose = u }) + return () => { - unlistenResize() - unlistenClose() + unlistenResize?.() + unlistenClose?.() } }) @@ -56,6 +63,10 @@ if (choice === 'tray') await doHide() else await doQuit() } + + function onBackdropKey(e: KeyboardEvent) { + if (e.key === 'Escape') { closeDialogOpen = false; closeRemember = false } + } {#if !isFullscreen} @@ -99,8 +110,21 @@ {/if} {#if closeDialogOpen} -
Close Moku?
Choose how the app should exit.
@@ -169,6 +193,7 @@ 0 24px 64px rgba(0,0,0,0.7), 0 8px 24px rgba(0,0,0,0.4); animation: cdPop 0.22s cubic-bezier(0.16,1,0.3,1) both; + outline: none; } @keyframes cdPop { from { opacity: 0; transform: scale(0.96) translateY(6px) } to { opacity: 1; transform: none } } diff --git a/src/lib/components/chrome/splashCanvas.ts b/src/lib/components/chrome/splashCanvas.ts deleted file mode 100644 index aa72af2..0000000 --- a/src/lib/components/chrome/splashCanvas.ts +++ /dev/null @@ -1,171 +0,0 @@ -const CARD_COUNT = 18 -const CARD_W = 52 -const CARD_H = 72 -const CARD_RADIUS = 6 -const DRIFT_SPEED = 0.018 - -interface Card { - x: number - y: number - vx: number - vy: number - rot: number - vrot: number - opacity: number - scale: number - hue: number -} - -function makeCard(w: number, h: number): Card { - const side = Math.floor(Math.random() * 4) - const margin = 80 - let x = 0, y = 0 - if (side === 0) { x = Math.random() * w; y = -margin } - if (side === 1) { x = w + margin; y = Math.random() * h } - if (side === 2) { x = Math.random() * w; y = h + margin } - if (side === 3) { x = -margin; y = Math.random() * h } - const cx = w / 2, cy = h / 2 - const dx = cx - x, dy = cy - y - const len = Math.sqrt(dx * dx + dy * dy) || 1 - const spd = 0.12 + Math.random() * 0.1 - return { - x, - y, - vx: (dx / len) * spd * (0.3 + Math.random() * 0.4), - vy: (dy / len) * spd * (0.3 + Math.random() * 0.4), - rot: Math.random() * Math.PI * 2, - vrot: (Math.random() - 0.5) * 0.006, - opacity: 0.025 + Math.random() * 0.055, - scale: 0.7 + Math.random() * 0.7, - hue: 120 + Math.random() * 40, - } -} - -function drawCard(ctx: CanvasRenderingContext2D, c: Card) { - ctx.save() - ctx.globalAlpha = c.opacity - ctx.translate(c.x, c.y) - ctx.rotate(c.rot) - ctx.scale(c.scale, c.scale) - - const w = CARD_W, h = CARD_H, r = CARD_RADIUS - const x = -w / 2, y = -h / 2 - - ctx.beginPath() - ctx.moveTo(x + r, y) - ctx.lineTo(x + w - r, y) - ctx.quadraticCurveTo(x + w, y, x + w, y + r) - ctx.lineTo(x + w, y + h - r) - ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h) - ctx.lineTo(x + r, y + h) - ctx.quadraticCurveTo(x, y + h, x, y + h - r) - ctx.lineTo(x, y + r) - ctx.quadraticCurveTo(x, y, x + r, y) - ctx.closePath() - - ctx.strokeStyle = `hsla(${c.hue}, 28%, 62%, 0.9)` - ctx.lineWidth = 1 / c.scale - ctx.stroke() - - const grad = ctx.createLinearGradient(x, y, x, y + h) - grad.addColorStop(0, `hsla(${c.hue}, 20%, 40%, 0.18)`) - grad.addColorStop(1, `hsla(${c.hue}, 20%, 20%, 0.06)`) - ctx.fillStyle = grad - ctx.fill() - - ctx.restore() -} - -export function mountCardCanvas(canvas: HTMLCanvasElement) { - const ctx = canvas.getContext('2d')! - let cards = Array.from({ length: CARD_COUNT }, () => makeCard(canvas.offsetWidth || 800, canvas.offsetHeight || 600)) - let raf = 0 - let running = true - - function resize() { - const dpr = window.devicePixelRatio || 1 - canvas.width = canvas.offsetWidth * dpr - canvas.height = canvas.offsetHeight * dpr - ctx.scale(dpr, dpr) - cards = Array.from({ length: CARD_COUNT }, () => makeCard(canvas.offsetWidth, canvas.offsetHeight)) - } - - function tick() { - if (!running) return - const w = canvas.offsetWidth, h = canvas.offsetHeight - ctx.clearRect(0, 0, w, h) - for (const c of cards) { - c.x += c.vx - c.y += c.vy - c.rot += c.vrot - const pad = 120 - if (c.x < -pad || c.x > w + pad || c.y < -pad || c.y > h + pad) { - Object.assign(c, makeCard(w, h)) - } - drawCard(ctx, c) - } - raf = requestAnimationFrame(tick) - } - - const ro = new ResizeObserver(resize) - ro.observe(canvas) - resize() - tick() - - return { - destroy() { - running = false - cancelAnimationFrame(raf) - ro.disconnect() - }, - } -} - -export function ringGeometry(r: number, pad: number) { - const size = (r + pad) * 2 - const c = size / 2 - const circ = 2 * Math.PI * r - return { size, c, circ } -} - -const RING_STEPS = [ - { target: 0.15, duration: 400 }, - { target: 0.45, duration: 800 }, - { target: 0.72, duration: 600 }, - { target: 0.88, duration: 1000 }, - { target: 0.96, duration: 700 }, -] - -export function animateRingProgress(onProgress: (p: number) => void): () => void { - let current = 0.025 - let stepIdx = 0 - let start = performance.now() - let raf = 0 - let stopped = false - - function ease(t: number) { - return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t - } - - function tick(now: number) { - if (stopped) return - if (stepIdx >= RING_STEPS.length) return - - const step = RING_STEPS[stepIdx] - const elapsed = now - start - const t = Math.min(elapsed / step.duration, 1) - const from = stepIdx === 0 ? 0.025 : RING_STEPS[stepIdx - 1].target - current = from + (step.target - from) * ease(t) - onProgress(current) - - if (t >= 1) { - stepIdx++ - start = now - } - - raf = requestAnimationFrame(tick) - } - - raf = requestAnimationFrame(tick) - return () => { stopped = true; cancelAnimationFrame(raf) } -} \ No newline at end of file diff --git a/src/lib/components/downloads/DownloadItem.svelte b/src/lib/components/downloads/DownloadItem.svelte index 32095c3..a11c839 100644 --- a/src/lib/components/downloads/DownloadItem.svelte +++ b/src/lib/components/downloads/DownloadItem.svelte @@ -1,5 +1,5 @@ @@ -54,8 +52,6 @@ isSelected={selected.has(item.chapter.id)} {onRemove} {onRetry} - {onReorder} - {onReorderEdge} {onSelect} /> {/each} diff --git a/src/lib/components/downloads/Downloads.svelte b/src/lib/components/downloads/Downloads.svelte index 18f43d2..10e3de4 100644 --- a/src/lib/components/downloads/Downloads.svelte +++ b/src/lib/components/downloads/Downloads.svelte @@ -165,10 +165,8 @@ isRunning={downloadStore.isRunning} dequeueing={downloadStore.dequeueing} selected={downloadStore.selected} - onRemove={(id) => downloadStore.dequeue(id)} - onRetry={(id) => downloadStore.retryOne(id)} - onReorder={(id, dir) => downloadStore.reorder(id, dir)} - onReorderEdge={(id, edge) => downloadStore.reorderToEdge(id, edge)} + onRemove={(id: number) => downloadStore.dequeue(id)} + onRetry={(id: number) => downloadStore.retryOne(id)} onSelect={handleSelect} />