mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 17:29:55 -05:00
Merge pull request #95 from frozenKelp/main
fix: idle splash, Discord RPC, memory leaks, reader navigation, cover art, settings scroll
This commit is contained in:
@@ -1,9 +1,46 @@
|
|||||||
|
<script module lang="ts">
|
||||||
|
function getLiveSet(): Set<HTMLCanvasElement> {
|
||||||
|
const g = window as any
|
||||||
|
if (!g.__splashCanvasSet) g.__splashCanvasSet = new Set<HTMLCanvasElement>()
|
||||||
|
return g.__splashCanvasSet as Set<HTMLCanvasElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneSet(set: Set<HTMLCanvasElement>) {
|
||||||
|
for (const el of set) if (!el.isConnected) set.delete(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splashDevRegister(el: HTMLCanvasElement) {
|
||||||
|
const set = getLiveSet()
|
||||||
|
pruneSet(set)
|
||||||
|
set.add(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splashDevUnregister(el: HTMLCanvasElement) {
|
||||||
|
const set = getLiveSet()
|
||||||
|
set.delete(el)
|
||||||
|
pruneSet(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splashDevLiveCount(): number {
|
||||||
|
const set = getLiveSet()
|
||||||
|
pruneSet(set)
|
||||||
|
return set.size
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splashDevNextMount(): number {
|
||||||
|
const g = window as any
|
||||||
|
g.__splashTotalMounts = (g.__splashTotalMounts ?? 0) + 1
|
||||||
|
return g.__splashTotalMounts as number
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import logoUrl from '$lib/assets/moku-icon-splash.svg'
|
import logoUrl from '$lib/assets/moku-icon-splash.svg'
|
||||||
import { platformService } from '$lib/platform-service'
|
import { platformService } from '$lib/platform-service'
|
||||||
|
|
||||||
const isTauri = platformService.platform === 'tauri'
|
const isTauri = platformService.platform === 'tauri'
|
||||||
|
const isDev = import.meta.env.DEV
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mode?: 'loading' | 'idle' | 'locked'
|
mode?: 'loading' | 'idle' | 'locked'
|
||||||
@@ -12,6 +49,7 @@
|
|||||||
notConfigured?: boolean
|
notConfigured?: boolean
|
||||||
showCards?: boolean
|
showCards?: boolean
|
||||||
showFps?: boolean
|
showFps?: boolean
|
||||||
|
showDevOverlay?: boolean
|
||||||
pinLen?: number
|
pinLen?: number
|
||||||
pinCorrect?: string
|
pinCorrect?: string
|
||||||
onReady?: () => void
|
onReady?: () => void
|
||||||
@@ -23,7 +61,7 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
mode = 'loading', ringFull = false, failed = false,
|
mode = 'loading', ringFull = false, failed = false,
|
||||||
notConfigured = false, showCards = true, showFps = false,
|
notConfigured = false, showCards = true, showFps = false, showDevOverlay = false,
|
||||||
pinLen = 4, pinCorrect = '',
|
pinLen = 4, pinCorrect = '',
|
||||||
onReady, onUnlock, onRetry, onBypass, onDismiss,
|
onReady, onUnlock, onRetry, onBypass, onDismiss,
|
||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
@@ -80,7 +118,7 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (mode === 'loading' && !failed && !notConfigured && !ringFull) {
|
if (mode === 'loading' && !failed && !notConfigured && !ringFull) {
|
||||||
if (!isTauri) return // no ring animation on web; probe outcome drives exit
|
if (!isTauri) return
|
||||||
animStart = null
|
animStart = null
|
||||||
animPhase = 1
|
animPhase = 1
|
||||||
animFrame = requestAnimationFrame(animateRing)
|
animFrame = requestAnimationFrame(animateRing)
|
||||||
@@ -280,22 +318,87 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DevMetrics {
|
||||||
|
totalMounts: number
|
||||||
|
resizeCount: number
|
||||||
|
stampCount: number
|
||||||
|
mountedAt: number
|
||||||
|
lastResizeAt: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
let devMetrics = $state<DevMetrics | null>(null)
|
||||||
|
let uptimeSecs = $state(0)
|
||||||
|
let devLiveCount = $state(0)
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!isDev || mode !== 'idle' || !devMetrics) return
|
||||||
|
const start = Date.now()
|
||||||
|
const iv = setInterval(() => { uptimeSecs = Math.floor((Date.now() - start) / 1000) }, 1000)
|
||||||
|
return () => clearInterval(iv)
|
||||||
|
})
|
||||||
|
|
||||||
|
function fmtUptime(s: number): string {
|
||||||
|
if (s < 60) return `${s}s`
|
||||||
|
return `${Math.floor(s / 60)}m ${s % 60}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtAgo(ts: number | null): string {
|
||||||
|
if (ts === null) return '—'
|
||||||
|
const s = Math.floor((Date.now() - ts) / 1000)
|
||||||
|
if (s < 60) return `${s}s ago`
|
||||||
|
return `${Math.floor(s / 60)}m ago`
|
||||||
|
}
|
||||||
|
|
||||||
function mountCanvas(el: HTMLCanvasElement) {
|
function mountCanvas(el: HTMLCanvasElement) {
|
||||||
const ctx = el.getContext('2d')!
|
const ctx = el.getContext('2d')!
|
||||||
let live: RenderState | null = null
|
let live: RenderState | null = null
|
||||||
let lastLogW = 0, lastLogH = 0, lastScale = 0, buildGen = 0
|
let lastLogW = 0, lastLogH = 0, lastScale = 0, buildGen = 0
|
||||||
|
|
||||||
|
if (isDev && mode === 'idle') {
|
||||||
|
splashDevRegister(el)
|
||||||
|
devLiveCount = splashDevLiveCount()
|
||||||
|
uptimeSecs = 0
|
||||||
|
devMetrics = {
|
||||||
|
totalMounts: splashDevNextMount(),
|
||||||
|
resizeCount: 0,
|
||||||
|
stampCount: 0,
|
||||||
|
mountedAt: Date.now(),
|
||||||
|
lastResizeAt: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
if (live) {
|
||||||
|
live.stamps.forEach(c => { c.width = 0; c.height = 0 })
|
||||||
|
live.vignette.width = 0
|
||||||
|
live.vignette.height = 0
|
||||||
|
live = null
|
||||||
|
}
|
||||||
|
ctx.clearRect(0, 0, el.width, el.height)
|
||||||
|
}
|
||||||
|
|
||||||
function applySize(logW: number, logH: number, scale: number) {
|
function applySize(logW: number, logH: number, scale: number) {
|
||||||
const gen = ++buildGen
|
const gen = ++buildGen
|
||||||
if (logW <= 0 || logH <= 0) return
|
if (logW <= 0 || logH <= 0) return
|
||||||
if (logW === lastLogW && logH === lastLogH && scale === lastScale) return
|
if (logW === lastLogW && logH === lastLogH && scale === lastScale) return
|
||||||
lastLogW = logW; lastLogH = logH; lastScale = scale
|
lastLogW = logW; lastLogH = logH; lastScale = scale
|
||||||
|
if (live) cleanup()
|
||||||
const built = buildCards(logW, logH)
|
const built = buildCards(logW, logH)
|
||||||
const stamps = built.cards.map(c => buildStamp(c, scale))
|
const stamps = built.cards.map(c => buildStamp(c, scale))
|
||||||
const vig = buildVignette(logW, logH, scale)
|
const vig = buildVignette(logW, logH, scale)
|
||||||
el.width = Math.round(logW * scale)
|
el.width = Math.round(logW * scale)
|
||||||
el.height = Math.round(logH * scale)
|
el.height = Math.round(logH * scale)
|
||||||
if (gen === buildGen) live = { cards: built.cards, trigs: built.trigs, stamps, vignette: vig, CW: el.width, CH: el.height, scale }
|
if (gen === buildGen) {
|
||||||
|
live = { cards: built.cards, trigs: built.trigs, stamps, vignette: vig, CW: el.width, CH: el.height, scale }
|
||||||
|
if (isDev && mode === 'idle' && devMetrics) {
|
||||||
|
devMetrics = {
|
||||||
|
...devMetrics,
|
||||||
|
resizeCount: devMetrics.resizeCount + 1,
|
||||||
|
stampCount: stamps.length,
|
||||||
|
lastResizeAt: Date.now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let extraCleanup: (() => void) | undefined
|
let extraCleanup: (() => void) | undefined
|
||||||
@@ -343,8 +446,13 @@
|
|||||||
raf = requestAnimationFrame(frame)
|
raf = requestAnimationFrame(frame)
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(raf)
|
cancelAnimationFrame(raf)
|
||||||
|
cleanup()
|
||||||
extraCleanup?.()
|
extraCleanup?.()
|
||||||
document.removeEventListener('visibilitychange', onVis)
|
document.removeEventListener('visibilitychange', onVis)
|
||||||
|
if (isDev && mode === 'idle') {
|
||||||
|
splashDevUnregister(el)
|
||||||
|
devLiveCount = splashDevLiveCount()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -357,6 +465,20 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if isDev && mode === 'idle' && devMetrics && showDevOverlay}
|
||||||
|
<div class="dev-overlay">
|
||||||
|
<span class="dev-title">canvas · idle splash</span>
|
||||||
|
<div class="dev-grid">
|
||||||
|
<span class="dev-k">live</span> <span class="dev-v" class:dev-warn={devLiveCount > 1}>{devLiveCount}</span>
|
||||||
|
<span class="dev-k">total mounts</span> <span class="dev-v">{devMetrics.totalMounts}</span>
|
||||||
|
<span class="dev-k">stamps</span> <span class="dev-v">{devMetrics.stampCount}</span>
|
||||||
|
<span class="dev-k">resizes</span> <span class="dev-v">{devMetrics.resizeCount}</span>
|
||||||
|
<span class="dev-k">uptime</span> <span class="dev-v">{fmtUptime(uptimeSecs)}</span>
|
||||||
|
<span class="dev-k">last resize</span> <span class="dev-v">{fmtAgo(devMetrics.lastResizeAt)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if mode === 'idle'}
|
{#if mode === 'idle'}
|
||||||
<div style="z-index:1;display:flex;flex-direction:column;align-items:center">
|
<div style="z-index:1;display:flex;flex-direction:column;align-items:center">
|
||||||
<div style="position:relative;width:{logoIdleSize}px;height:{logoIdleSize}px;margin-bottom:32px">
|
<div style="position:relative;width:{logoIdleSize}px;height:{logoIdleSize}px;margin-bottom:32px">
|
||||||
@@ -449,4 +571,11 @@
|
|||||||
.err-btn:hover { border-color:var(--border-strong); color:var(--text-secondary); }
|
.err-btn:hover { border-color:var(--border-strong); color:var(--text-secondary); }
|
||||||
.err-btn--primary { border-color:var(--accent-dim); color:var(--accent-fg); background:var(--accent-muted); }
|
.err-btn--primary { border-color:var(--accent-dim); color:var(--accent-fg); background:var(--accent-muted); }
|
||||||
.err-btn--primary:hover { border-color:var(--accent); color:var(--accent-bright); }
|
.err-btn--primary:hover { border-color:var(--accent); color:var(--accent-bright); }
|
||||||
|
|
||||||
|
.dev-overlay { position:absolute; top:12px; left:12px; z-index:10; background:rgba(0,0,0,0.72); border:1px solid rgba(255,255,255,0.10); border-radius:6px; padding:8px 10px; pointer-events:none; backdrop-filter:blur(6px); }
|
||||||
|
.dev-title { display:block; font-family:var(--font-ui); font-size:9px; letter-spacing:0.14em; text-transform:uppercase; color:var(--accent); margin-bottom:6px; }
|
||||||
|
.dev-grid { display:grid; grid-template-columns:auto auto; column-gap:12px; row-gap:2px; }
|
||||||
|
.dev-k { font-family:var(--font-ui); font-size:10px; color:var(--text-faint); white-space:nowrap; }
|
||||||
|
.dev-v { font-family:var(--font-ui); font-size:10px; color:var(--text-secondary); text-align:right; white-space:nowrap; }
|
||||||
|
.dev-warn { color:#f87171; }
|
||||||
</style>
|
</style>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { onMount, untrack, tick } from "svelte";
|
import { onMount, untrack, tick } from "svelte";
|
||||||
import { readerState, PAGE_STYLES } from "$lib/state/reader.svelte";
|
import { readerState, PAGE_STYLES } from "$lib/state/reader.svelte";
|
||||||
import { settingsState, updateSettings } from "$lib/state/settings.svelte";
|
import { settingsState, updateSettings } from "$lib/state/settings.svelte";
|
||||||
import { app } from "$lib/state/app.svelte";
|
import { app, appState } from "$lib/state/app.svelte";
|
||||||
import { DEFAULT_KEYBINDS } from "$lib/core/keybinds/defaultBinds";
|
import { DEFAULT_KEYBINDS } from "$lib/core/keybinds/defaultBinds";
|
||||||
import { fetchPages, resolveUrl, preloadImage, measureAspect, buildPageGroups } from "$lib/components/reader/lib/pageLoader";
|
import { fetchPages, resolveUrl, preloadImage, measureAspect, buildPageGroups } from "$lib/components/reader/lib/pageLoader";
|
||||||
import { setupScrollTracking, appendNextChapter } from "$lib/components/reader/lib/scrollHandler";
|
import { setupScrollTracking, appendNextChapter } from "$lib/components/reader/lib/scrollHandler";
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
import { loadChapter, scheduleResumeDismiss } from "$lib/components/reader/lib/chapterLoader";
|
import { loadChapter, scheduleResumeDismiss } from "$lib/components/reader/lib/chapterLoader";
|
||||||
import { historyState } from "$lib/state/history.svelte";
|
import { historyState } from "$lib/state/history.svelte";
|
||||||
import { getAdapter } from "$lib/request-manager";
|
import { getAdapter } from "$lib/request-manager";
|
||||||
|
import { setReading, clearReading } from "$lib/core/discord";
|
||||||
|
import { revokeBlobUrl } from "$lib/core/cache/imageCache";
|
||||||
import type { ReaderSettings } from "$lib/state/reader.svelte";
|
import type { ReaderSettings } from "$lib/state/reader.svelte";
|
||||||
import ReaderControls from "$lib/components/reader/ReaderControls.svelte";
|
import ReaderControls from "$lib/components/reader/ReaderControls.svelte";
|
||||||
import PageView from "$lib/components/reader/PageView.svelte";
|
import PageView from "$lib/components/reader/PageView.svelte";
|
||||||
@@ -216,10 +218,20 @@
|
|||||||
? () => goForward(style, adjacent, lastPage, maybeMarkCurrentRead, startAtLast)
|
? () => goForward(style, adjacent, lastPage, maybeMarkCurrentRead, startAtLast)
|
||||||
: () => goBack(style, adjacent, startAtLast));
|
: () => goBack(style, adjacent, startAtLast));
|
||||||
|
|
||||||
|
// clear Discord presence and free page blob textures before closing
|
||||||
|
function handleCloseReader() {
|
||||||
|
clearReading().catch(() => {});
|
||||||
|
for (const url of readerState.pageUrls) revokeBlobUrl(url);
|
||||||
|
for (const strip of readerState.stripChapters) {
|
||||||
|
for (const url of strip.urls) revokeBlobUrl(url);
|
||||||
|
}
|
||||||
|
readerState.closeReader();
|
||||||
|
}
|
||||||
|
|
||||||
const onKey = createReaderKeyHandler({
|
const onKey = createReaderKeyHandler({
|
||||||
goNext: () => goNext(),
|
goNext: () => goNext(),
|
||||||
goPrev: () => goPrev(),
|
goPrev: () => goPrev(),
|
||||||
closeReader: () => readerState.closeReader(),
|
closeReader: () => handleCloseReader(),
|
||||||
goToPage: (p) => jumpToPage(p, style, lastPage, containerEl),
|
goToPage: (p) => jumpToPage(p, style, lastPage, containerEl),
|
||||||
lastPage: () => lastPage,
|
lastPage: () => lastPage,
|
||||||
adjustZoom: (d) => { captureZoomAnchor(containerEl, style, zoomAnchor); applySettings({ readerZoom: clampZoom(zoom + d) }); restoreZoomAnchor(containerEl, zoomAnchor); },
|
adjustZoom: (d) => { captureZoomAnchor(containerEl, style, zoomAnchor); applySettings({ readerZoom: clampZoom(zoom + d) }); restoreZoomAnchor(containerEl, zoomAnchor); },
|
||||||
@@ -313,6 +325,16 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Separate from chapter load: also re-fires when idle splash dismisses so presence is restored.
|
||||||
|
$effect(() => {
|
||||||
|
const ch = readerState.activeChapter;
|
||||||
|
const manga = readerState.activeManga;
|
||||||
|
const idle = appState.idleSplash;
|
||||||
|
if (ch && manga && !idle) {
|
||||||
|
untrack(() => setReading(manga, ch).catch(() => {}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const page = readerState.pageNumber;
|
const page = readerState.pageNumber;
|
||||||
const chId = style === "longstrip"
|
const chId = style === "longstrip"
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { readerState } from "$lib/state/reader.svelte";
|
import { readerState } from "$lib/state/reader.svelte";
|
||||||
import { fetchPages } from "./pageLoader";
|
import { fetchPages } from "./pageLoader";
|
||||||
import { cancelQueuedFetches } from "$lib/core/cache/imageCache";
|
import { cancelQueuedFetches, revokeBlobUrl } from "$lib/core/cache/imageCache";
|
||||||
import { clearResolvedUrlCache } from "$lib/core/cache/pageCache";
|
import { clearResolvedUrlCache, clearPageCache } from "$lib/core/cache/pageCache";
|
||||||
|
|
||||||
export function scheduleResumeDismiss() {
|
export function scheduleResumeDismiss() {
|
||||||
setTimeout(() => { readerState.resumeFading = true; }, 1500);
|
setTimeout(() => { readerState.resumeFading = true; }, 1500);
|
||||||
setTimeout(() => { readerState.resumeVisible = false; readerState.resumeFading = false; }, 2500);
|
setTimeout(() => { readerState.resumeVisible = false; readerState.resumeFading = false; }, 2500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let prefetchedChapterId: number | null = null;
|
||||||
|
|
||||||
export async function loadChapter(
|
export async function loadChapter(
|
||||||
id: number,
|
id: number,
|
||||||
useBlob: boolean,
|
useBlob: boolean,
|
||||||
@@ -21,7 +23,19 @@ export async function loadChapter(
|
|||||||
abortCtrl.current = ctrl;
|
abortCtrl.current = ctrl;
|
||||||
|
|
||||||
cancelQueuedFetches();
|
cancelQueuedFetches();
|
||||||
if (useBlob) clearResolvedUrlCache();
|
if (useBlob) {
|
||||||
|
clearResolvedUrlCache();
|
||||||
|
for (const url of readerState.pageUrls) revokeBlobUrl(url);
|
||||||
|
for (const strip of readerState.stripChapters) {
|
||||||
|
for (const url of strip.urls) revokeBlobUrl(url);
|
||||||
|
}
|
||||||
|
if (prefetchedChapterId !== null && prefetchedChapterId !== id) {
|
||||||
|
const prefetchedUrls = await fetchPages(prefetchedChapterId, false).catch(() => [] as string[]);
|
||||||
|
for (const url of prefetchedUrls) revokeBlobUrl(url);
|
||||||
|
clearPageCache(prefetchedChapterId);
|
||||||
|
}
|
||||||
|
prefetchedChapterId = null;
|
||||||
|
}
|
||||||
|
|
||||||
startAtLastPage.current = false;
|
startAtLastPage.current = false;
|
||||||
markedRead.clear();
|
markedRead.clear();
|
||||||
@@ -44,7 +58,10 @@ export async function loadChapter(
|
|||||||
else if (resumeTo > 1) readerState.pageNumber = Math.min(resumeTo, urls.length || resumeTo);
|
else if (resumeTo > 1) readerState.pageNumber = Math.min(resumeTo, urls.length || resumeTo);
|
||||||
readerState.pageReady = true;
|
readerState.pageReady = true;
|
||||||
readerState.loading = false;
|
readerState.loading = false;
|
||||||
if (adjacent.next) fetchPages(adjacent.next.id, useBlob, ctrl.signal).catch(() => {});
|
if (adjacent.next) {
|
||||||
|
prefetchedChapterId = adjacent.next.id;
|
||||||
|
fetchPages(adjacent.next.id, useBlob, ctrl.signal).catch(() => {});
|
||||||
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (ctrl.signal.aborted) return;
|
if (ctrl.signal.aborted) return;
|
||||||
readerState.error = e instanceof Error ? e.message : String(e);
|
readerState.error = e instanceof Error ? e.message : String(e);
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
|
|
||||||
<div class="cover-wrap">
|
<div class="cover-wrap">
|
||||||
<button class="cover-btn" onclick={() => manga && setPreviewManga(manga)} title="Quick preview" disabled={!manga}>
|
<button class="cover-btn" onclick={() => manga && setPreviewManga(manga)} title="Quick preview" disabled={!manga}>
|
||||||
<Thumbnail src={resolvedCover(seriesState.activeManga?.id ?? manga?.id ?? 0, seriesState.activeManga?.thumbnailUrl ?? manga?.thumbnailUrl ?? "")} alt={seriesState.activeManga?.title ?? manga?.title ?? ""} class="cover" id={seriesState.activeManga?.id ?? manga?.id} />
|
<Thumbnail src={resolvedCover(manga?.id ?? seriesState.activeManga?.id ?? 0, manga?.thumbnailUrl ?? seriesState.activeManga?.thumbnailUrl ?? "")} alt={manga?.title ?? seriesState.activeManga?.title ?? ""} class="cover" id={manga?.id ?? seriesState.activeManga?.id} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,7 @@
|
|||||||
/* ── Backdrop & Modal Shell ───────────────────────────────────────── */
|
/* ── Backdrop & Modal Shell ───────────────────────────────────────── */
|
||||||
.s-backdrop {
|
.s-backdrop {
|
||||||
position: fixed; inset: 0;
|
position: fixed; inset: 0;
|
||||||
background: rgba(0,0,0,0.6);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
-webkit-backdrop-filter: blur(8px);
|
|
||||||
z-index: var(--z-settings);
|
z-index: var(--z-settings);
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
animation: s-fade-in 0.14s ease both;
|
animation: s-fade-in 0.14s ease both;
|
||||||
@@ -29,10 +27,7 @@
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
position: relative;
|
position: relative;
|
||||||
animation: s-scale-in 0.2s cubic-bezier(0.16,1,0.3,1) both;
|
animation: s-scale-in 0.2s cubic-bezier(0.16,1,0.3,1) both;
|
||||||
box-shadow:
|
box-shadow: 0 0 0 1px var(--border-dim), 0 24px 64px rgba(0,0,0,0.6);
|
||||||
0 0 0 1px rgba(255,255,255,0.04) inset,
|
|
||||||
0 24px 80px rgba(0,0,0,0.7),
|
|
||||||
0 8px 24px rgba(0,0,0,0.4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -46,7 +41,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1px;
|
gap: 1px;
|
||||||
overflow-y: auto;
|
overflow-y: hidden;
|
||||||
border-radius: var(--radius-2xl) 0 0 var(--radius-2xl);
|
border-radius: var(--radius-2xl) 0 0 var(--radius-2xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
import ContentSettings from './sections/ContentSettings.svelte'
|
import ContentSettings from './sections/ContentSettings.svelte'
|
||||||
import AboutSettings from './sections/AboutSettings.svelte'
|
import AboutSettings from './sections/AboutSettings.svelte'
|
||||||
import DevtoolsSettings from './sections/DevToolsSettings.svelte'
|
import DevtoolsSettings from './sections/DevToolsSettings.svelte'
|
||||||
|
import ModalBlur from '$lib/components/shared/ui/ModalBlur.svelte'
|
||||||
|
|
||||||
interface Props { onclose?: () => void; onOpenThemeEditor?: (id?: string | null) => void }
|
interface Props { onclose?: () => void; onOpenThemeEditor?: (id?: string | null) => void }
|
||||||
let { onclose, onOpenThemeEditor }: Props = $props()
|
let { onclose, onOpenThemeEditor }: Props = $props()
|
||||||
@@ -111,6 +112,7 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<ModalBlur />
|
||||||
<div class="s-backdrop" role="presentation" tabindex="-1"
|
<div class="s-backdrop" role="presentation" tabindex="-1"
|
||||||
onclick={(e) => { if (e.target === e.currentTarget) close() }}
|
onclick={(e) => { if (e.target === e.currentTarget) close() }}
|
||||||
onkeydown={(e) => { if (e.key === 'Escape') { e.stopPropagation(); close() } }}>
|
onkeydown={(e) => { if (e.key === 'Escape') { e.stopPropagation(); close() } }}>
|
||||||
|
|||||||
@@ -102,9 +102,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function triggerSplash() {
|
function triggerSplash() {
|
||||||
|
if (appState.devSplash) return
|
||||||
splashTriggered = true
|
splashTriggered = true
|
||||||
setTimeout(() => splashTriggered = false, 200)
|
setTimeout(() => splashTriggered = false, 200)
|
||||||
appState.idleSplash = true
|
appState.devSplash = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testWindowsHello() {
|
async function testWindowsHello() {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
} from "$lib/state/series.svelte";
|
} from "$lib/state/series.svelte";
|
||||||
import { app } from "$lib/state/app.svelte";
|
import { app } from "$lib/state/app.svelte";
|
||||||
import type { Manga, Chapter, Category } from "$lib/types";
|
import type { Manga, Chapter, Category } from "$lib/types";
|
||||||
|
import ModalBlur from '$lib/components/shared/ui/ModalBlur.svelte'
|
||||||
|
|
||||||
|
|
||||||
let manga: Manga | null = $state(null);
|
let manga: Manga | null = $state(null);
|
||||||
@@ -353,6 +354,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if seriesState.previewManga}
|
{#if seriesState.previewManga}
|
||||||
|
<ModalBlur blur={4} dim={0.72} />
|
||||||
<div
|
<div
|
||||||
class="backdrop"
|
class="backdrop"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -679,10 +681,8 @@
|
|||||||
<style>
|
<style>
|
||||||
.backdrop {
|
.backdrop {
|
||||||
position: fixed; inset: 0;
|
position: fixed; inset: 0;
|
||||||
background: rgba(0,0,0,0.72);
|
|
||||||
z-index: var(--z-settings);
|
z-index: var(--z-settings);
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
|
|
||||||
animation: fadeIn 0.12s ease both;
|
animation: fadeIn 0.12s ease both;
|
||||||
}
|
}
|
||||||
.modal {
|
.modal {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
blur = 8,
|
||||||
|
dim = 0.6,
|
||||||
|
zIndex = 'var(--z-settings)',
|
||||||
|
animate = true,
|
||||||
|
}: {
|
||||||
|
blur?: number
|
||||||
|
dim?: number
|
||||||
|
zIndex?: string | number
|
||||||
|
animate?: boolean
|
||||||
|
} = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="modal-blur"
|
||||||
|
class:animate
|
||||||
|
style="--blur:{blur}px; --dim:{dim}; --z:{zIndex}"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modal-blur {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
backdrop-filter: blur(var(--blur));
|
||||||
|
-webkit-backdrop-filter: blur(var(--blur));
|
||||||
|
background: rgba(0, 0, 0, var(--dim));
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: var(--z);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-blur.animate {
|
||||||
|
animation: blur-in 0.14s ease both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blur-in {
|
||||||
|
from { opacity: 0 }
|
||||||
|
to { opacity: 1 }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
import { markManyRead } from "$lib/request-manager/chapters";
|
import { markManyRead } from "$lib/request-manager/chapters";
|
||||||
import type { Tracker, TrackRecord, TrackSearch } from "$lib/types";
|
import type { Tracker, TrackRecord, TrackSearch } from "$lib/types";
|
||||||
import type { Chapter } from "$lib/types";
|
import type { Chapter } from "$lib/types";
|
||||||
|
import ModalBlur from '$lib/components/shared/ui/ModalBlur.svelte'
|
||||||
|
|
||||||
let { mangaId, mangaTitle, onClose }: {
|
let { mangaId, mangaTitle, onClose }: {
|
||||||
mangaId: number;
|
mangaId: number;
|
||||||
@@ -250,6 +251,7 @@
|
|||||||
}
|
}
|
||||||
}} />
|
}} />
|
||||||
|
|
||||||
|
<ModalBlur blur={4} dim={0.68} />
|
||||||
<div class="backdrop" role="presentation" onclick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
|
<div class="backdrop" role="presentation" onclick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
|
||||||
<div class="modal" role="dialog" aria-label="Tracking">
|
<div class="modal" role="dialog" aria-label="Tracking">
|
||||||
|
|
||||||
@@ -497,6 +499,7 @@
|
|||||||
{#if confirmUnbindId !== null}
|
{#if confirmUnbindId !== null}
|
||||||
{@const rec = records.find(r => r.id === confirmUnbindId)}
|
{@const rec = records.find(r => r.id === confirmUnbindId)}
|
||||||
{@const trk = rec ? trackerFor(rec.trackerId) : null}
|
{@const trk = rec ? trackerFor(rec.trackerId) : null}
|
||||||
|
<ModalBlur blur={2} dim={0.45} zIndex="calc(var(--z-settings) + 1)" />
|
||||||
<div class="confirm-backdrop" role="button" tabindex="-1" aria-label="Cancel"
|
<div class="confirm-backdrop" role="button" tabindex="-1" aria-label="Cancel"
|
||||||
onclick={() => confirmUnbindId = null}
|
onclick={() => confirmUnbindId = null}
|
||||||
onkeydown={(e) => { if (e.key === "Escape") confirmUnbindId = null; }}>
|
onkeydown={(e) => { if (e.key === "Escape") confirmUnbindId = null; }}>
|
||||||
@@ -515,10 +518,9 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.backdrop {
|
.backdrop {
|
||||||
position: fixed; inset: 0; background: rgba(0,0,0,0.68);
|
position: fixed; inset: 0;
|
||||||
z-index: var(--z-settings);
|
z-index: var(--z-settings);
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
|
|
||||||
animation: fadeIn 0.12s ease both;
|
animation: fadeIn 0.12s ease both;
|
||||||
}
|
}
|
||||||
.modal {
|
.modal {
|
||||||
@@ -646,7 +648,7 @@
|
|||||||
.result-row:hover:not(:disabled) .result-action { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
|
.result-row:hover:not(:disabled) .result-action { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
|
||||||
.result-action-on { color: var(--accent-fg) !important; border-color: var(--accent-dim) !important; background: var(--accent-muted) !important; }
|
.result-action-on { color: var(--accent-fg) !important; border-color: var(--accent-dim) !important; background: var(--accent-muted) !important; }
|
||||||
|
|
||||||
.confirm-backdrop { position: fixed; inset: 0; z-index: calc(var(--z-settings) + 1); background: rgba(0,0,0,0.45); backdrop-filter: blur(2px); display: flex; align-items: center; justify-content: center; animation: fadeIn 0.1s ease both; }
|
.confirm-backdrop { position: fixed; inset: 0; z-index: calc(var(--z-settings) + 1); display: flex; align-items: center; justify-content: center; animation: fadeIn 0.1s ease both; }
|
||||||
.confirm-modal { background: var(--bg-surface); border: 1px solid var(--border-dim); border-radius: var(--radius-xl); padding: var(--sp-5); width: 260px; display: flex; flex-direction: column; gap: var(--sp-3); box-shadow: 0 16px 48px rgba(0,0,0,0.5); animation: scaleIn 0.15s ease both; }
|
.confirm-modal { background: var(--bg-surface); border: 1px solid var(--border-dim); border-radius: var(--radius-xl); padding: var(--sp-5); width: 260px; display: flex; flex-direction: column; gap: var(--sp-3); box-shadow: 0 16px 48px rgba(0,0,0,0.5); animation: scaleIn 0.15s ease both; }
|
||||||
.confirm-title { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-primary); margin: 0; }
|
.confirm-title { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-primary); margin: 0; }
|
||||||
.confirm-body { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); line-height: 1.5; margin: 0; letter-spacing: var(--tracking-wide); }
|
.confirm-body { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); line-height: 1.5; margin: 0; letter-spacing: var(--tracking-wide); }
|
||||||
|
|||||||
Vendored
+9
-7
@@ -7,7 +7,7 @@ const inflight = new Map<string, Promise<string>>();
|
|||||||
const MAX_CONCURRENT = 6;
|
const MAX_CONCURRENT = 6;
|
||||||
let active = 0;
|
let active = 0;
|
||||||
let drainScheduled = false;
|
let drainScheduled = false;
|
||||||
let clearing = false;
|
let generation = 0;
|
||||||
|
|
||||||
interface QueueEntry {
|
interface QueueEntry {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -32,10 +32,11 @@ async function getAuthHeaders(): Promise<Record<string, string>> {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doFetch(url: string): Promise<string> {
|
async function doFetch(url: string, gen: number): Promise<string> {
|
||||||
const headers = await getAuthHeaders();
|
const headers = await getAuthHeaders();
|
||||||
|
if (gen !== generation) throw new DOMException("Cancelled", "AbortError");
|
||||||
const blob = await platformService.fetchImage(url, headers);
|
const blob = await platformService.fetchImage(url, headers);
|
||||||
if (clearing) throw new DOMException("Cancelled", "AbortError");
|
if (gen !== generation) throw new DOMException("Cancelled", "AbortError");
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
cache.set(url, blobUrl);
|
cache.set(url, blobUrl);
|
||||||
return blobUrl;
|
return blobUrl;
|
||||||
@@ -55,8 +56,9 @@ function drain() {
|
|||||||
drainScheduled = false;
|
drainScheduled = false;
|
||||||
while (active < MAX_CONCURRENT && queue.length > 0) {
|
while (active < MAX_CONCURRENT && queue.length > 0) {
|
||||||
const entry = queue.shift()!;
|
const entry = queue.shift()!;
|
||||||
|
const gen = generation;
|
||||||
active++;
|
active++;
|
||||||
doFetch(entry.url)
|
doFetch(entry.url, gen)
|
||||||
.then(entry.resolve, entry.reject)
|
.then(entry.resolve, entry.reject)
|
||||||
.finally(() => { active--; drain(); });
|
.finally(() => { active--; drain(); });
|
||||||
}
|
}
|
||||||
@@ -107,6 +109,7 @@ export function preloadBlobUrls(urls: string[], basePriority = 0): void {
|
|||||||
export function revokeBlobUrl(url: string): void {
|
export function revokeBlobUrl(url: string): void {
|
||||||
const blob = cache.get(url);
|
const blob = cache.get(url);
|
||||||
if (blob) { URL.revokeObjectURL(blob); cache.delete(url); }
|
if (blob) { URL.revokeObjectURL(blob); cache.delete(url); }
|
||||||
|
inflight.delete(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deprioritizeQueue(): void {
|
export function deprioritizeQueue(): void {
|
||||||
@@ -123,10 +126,9 @@ export function cancelQueuedFetches(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clearBlobCache(): void {
|
export function clearBlobCache(): void {
|
||||||
clearing = true;
|
generation++;
|
||||||
cancelQueuedFetches();
|
cancelQueuedFetches();
|
||||||
|
inflight.clear();
|
||||||
cache.forEach(blob => URL.revokeObjectURL(blob));
|
cache.forEach(blob => URL.revokeObjectURL(blob));
|
||||||
cache.clear();
|
cache.clear();
|
||||||
inflight.clear();
|
|
||||||
clearing = false;
|
|
||||||
}
|
}
|
||||||
Vendored
+4
-1
@@ -1,4 +1,4 @@
|
|||||||
import { getBlobUrl, preloadBlobUrls } from "$lib/core/cache/imageCache";
|
import { getBlobUrl, preloadBlobUrls, revokeBlobUrl } from "$lib/core/cache/imageCache";
|
||||||
import { settingsState } from "$lib/state/settings.svelte";
|
import { settingsState } from "$lib/state/settings.svelte";
|
||||||
|
|
||||||
const pageCache = new Map<number, string[]>();
|
const pageCache = new Map<number, string[]>();
|
||||||
@@ -90,6 +90,9 @@ export function preloadImage(url: string, useBlob: boolean): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clearResolvedUrlCache(): void {
|
export function clearResolvedUrlCache(): void {
|
||||||
|
for (const promise of resolvedUrlCache.values()) {
|
||||||
|
promise.then(blobUrl => { if (blobUrl) revokeBlobUrl(blobUrl); }).catch(() => {});
|
||||||
|
}
|
||||||
resolvedUrlCache.clear();
|
resolvedUrlCache.clear();
|
||||||
aspectCache.clear();
|
aspectCache.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-16
@@ -1,4 +1,5 @@
|
|||||||
import { platformService } from '$lib/platform-service'
|
import { platformService } from '$lib/platform-service'
|
||||||
|
import { settingsState } from '$lib/state/settings.svelte'
|
||||||
import type { Manga } from '$lib/types/manga'
|
import type { Manga } from '$lib/types/manga'
|
||||||
import type { Chapter } from '$lib/types/chapter'
|
import type { Chapter } from '$lib/types/chapter'
|
||||||
|
|
||||||
@@ -10,10 +11,7 @@ const APP_BUTTONS = [
|
|||||||
const FALLBACK_IMAGE = 'moku_logo'
|
const FALLBACK_IMAGE = 'moku_logo'
|
||||||
|
|
||||||
let sessionStart: number | null = null
|
let sessionStart: number | null = null
|
||||||
|
let activeMangaId: number | null = null
|
||||||
function isPublicUrl(url: string | null | undefined): boolean {
|
|
||||||
return typeof url === 'string' && url.startsWith('https://')
|
|
||||||
}
|
|
||||||
|
|
||||||
function trunc(s: string, max = 128): string {
|
function trunc(s: string, max = 128): string {
|
||||||
return s.length <= max ? s : `${s.slice(0, max - 1)}…`
|
return s.length <= max ? s : `${s.slice(0, max - 1)}…`
|
||||||
@@ -24,6 +22,31 @@ function formatChapter(chapter: Chapter): string {
|
|||||||
return `Chapter ${Number.isInteger(n) ? n : n.toFixed(1)}`
|
return `Chapter ${Number.isInteger(n) ? n : n.toFixed(1)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Suwayomi always returns the proxy path (/api/v1/manga/{id}/thumbnail), never the raw CDN URL.
|
||||||
|
// The proxy URL is only useful to Discord when the server is publicly reachable over HTTPS.
|
||||||
|
// For localhost setups cover art falls back to the app logo until Suwayomi exposes rawThumbnailUrl.
|
||||||
|
function resolveCoverUrl(manga: Manga): string {
|
||||||
|
const serverBase = (settingsState.settings.serverUrl ?? '').replace(/\/$/, '')
|
||||||
|
if (!serverBase.startsWith('https://')) return FALLBACK_IMAGE
|
||||||
|
const path = manga.thumbnailUrl?.startsWith('/') ? manga.thumbnailUrl : `/api/v1/manga/${manga.id}/thumbnail`
|
||||||
|
return `${serverBase}${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPresence(manga: Manga, chapter: Chapter, coverUrl: string) {
|
||||||
|
return {
|
||||||
|
details: trunc(manga.title),
|
||||||
|
state: `${formatChapter(chapter)} · Reading`,
|
||||||
|
timestamps: { start: sessionStart ?? Date.now() },
|
||||||
|
assets: {
|
||||||
|
largeImage: coverUrl,
|
||||||
|
largeText: trunc(manga.title),
|
||||||
|
smallImage: FALLBACK_IMAGE,
|
||||||
|
smallText: 'Moku',
|
||||||
|
},
|
||||||
|
buttons: APP_BUTTONS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function initRpc(): Promise<void> {
|
export async function initRpc(): Promise<void> {
|
||||||
if (!platformService.isSupported('discord-rpc')) return
|
if (!platformService.isSupported('discord-rpc')) return
|
||||||
sessionStart = Date.now()
|
sessionStart = Date.now()
|
||||||
@@ -36,18 +59,9 @@ export async function destroyRpc(): Promise<void> {
|
|||||||
|
|
||||||
export async function setReading(manga: Manga, chapter: Chapter): Promise<void> {
|
export async function setReading(manga: Manga, chapter: Chapter): Promise<void> {
|
||||||
if (!platformService.isSupported('discord-rpc')) return
|
if (!platformService.isSupported('discord-rpc')) return
|
||||||
await platformService.setDiscordPresence({
|
|
||||||
details: trunc(manga.title),
|
activeMangaId = manga.id
|
||||||
state: `${formatChapter(chapter)} · Reading`,
|
await platformService.setDiscordPresence(buildPresence(manga, chapter, resolveCoverUrl(manga)))
|
||||||
timestamps: { start: sessionStart ?? Date.now() },
|
|
||||||
assets: {
|
|
||||||
largeImage: isPublicUrl(manga.thumbnailUrl) ? manga.thumbnailUrl : FALLBACK_IMAGE,
|
|
||||||
largeText: trunc(manga.title),
|
|
||||||
smallImage: FALLBACK_IMAGE,
|
|
||||||
smallText: 'Moku',
|
|
||||||
},
|
|
||||||
buttons: APP_BUTTONS,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setIdle(): Promise<void> {
|
export async function setIdle(): Promise<void> {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export const appState = $state({
|
|||||||
toasts: [] as unknown[],
|
toasts: [] as unknown[],
|
||||||
appDir: '',
|
appDir: '',
|
||||||
idleSplash: false,
|
idleSplash: false,
|
||||||
|
devSplash: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export function setSettingsOpen(next: boolean) { app.setSettingsOpen(next) }
|
export function setSettingsOpen(next: boolean) { app.setSettingsOpen(next) }
|
||||||
|
|||||||
@@ -204,10 +204,18 @@ class LibraryState {
|
|||||||
this.tabFilters = { ...this.tabFilters, [tab]: {} };
|
this.tabFilters = { ...this.tabFilters, [tab]: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
syncFromSettings(s: { hiddenLibraryTabs?: string[]; libraryPinnedTabOrder?: string[]; defaultLibraryCategoryId?: number | null }) {
|
syncFromSettings(s: {
|
||||||
|
hiddenLibraryTabs?: string[];
|
||||||
|
libraryPinnedTabOrder?: string[];
|
||||||
|
defaultLibraryCategoryId?: number | null;
|
||||||
|
libraryShowAllInSaved?: boolean;
|
||||||
|
libraryHideCompletedInSaved?: boolean;
|
||||||
|
}) {
|
||||||
if (s.hiddenLibraryTabs) this.hiddenTabs = new Set(s.hiddenLibraryTabs);
|
if (s.hiddenLibraryTabs) this.hiddenTabs = new Set(s.hiddenLibraryTabs);
|
||||||
if (s.libraryPinnedTabOrder) this.pinnedTabOrder = s.libraryPinnedTabOrder;
|
if (s.libraryPinnedTabOrder) this.pinnedTabOrder = s.libraryPinnedTabOrder;
|
||||||
if (s.defaultLibraryCategoryId !== undefined) this.defaultCategoryId = s.defaultLibraryCategoryId ?? null;
|
if (s.defaultLibraryCategoryId !== undefined) this.defaultCategoryId = s.defaultLibraryCategoryId ?? null;
|
||||||
|
if (s.libraryShowAllInSaved !== undefined) this.showAllInSaved = s.libraryShowAllInSaved;
|
||||||
|
if (s.libraryHideCompletedInSaved !== undefined) this.hideCompletedInSaved = s.libraryHideCompletedInSaved;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCategories(cats: Category[]) {
|
setCategories(cats: Category[]) {
|
||||||
|
|||||||
@@ -80,10 +80,11 @@ class ReaderState {
|
|||||||
get settings() { return settingsState.settings; }
|
get settings() { return settingsState.settings; }
|
||||||
|
|
||||||
openReader(chapter: Chapter, chapterList: Chapter[], manga?: Manga | null) {
|
openReader(chapter: Chapter, chapterList: Chapter[], manga?: Manga | null) {
|
||||||
|
const isChapterNav = this.activeChapter !== null;
|
||||||
this.activeChapter = chapter;
|
this.activeChapter = chapter;
|
||||||
this.activeChapterList = chapterList;
|
this.activeChapterList = chapterList;
|
||||||
if (manga !== undefined) this.activeManga = manga;
|
if (manga !== undefined) this.activeManga = manga;
|
||||||
goto(`/reader/${this.activeManga!.id}/${chapter.id}`);
|
goto(`/reader/${this.activeManga!.id}/${chapter.id}`, { replaceState: isChapterNav });
|
||||||
}
|
}
|
||||||
|
|
||||||
closeReader() {
|
closeReader() {
|
||||||
|
|||||||
+48
-16
@@ -34,13 +34,12 @@
|
|||||||
|
|
||||||
const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
|
const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
|
||||||
|
|
||||||
let _splashDismissed = $state(false)
|
let splashDismissed = $state(false)
|
||||||
let bypassed = $state(false)
|
|
||||||
let themeEditorOpen = $state(false)
|
let themeEditorOpen = $state(false)
|
||||||
let themeEditorId = $state<string | null>(null)
|
let themeEditorId = $state<string | null>(null)
|
||||||
|
|
||||||
const splashVisible = $derived(
|
const splashVisible = $derived(
|
||||||
!_splashDismissed ||
|
!splashDismissed ||
|
||||||
appState.status === 'booting' ||
|
appState.status === 'booting' ||
|
||||||
appState.status === 'locked' ||
|
appState.status === 'locked' ||
|
||||||
appState.status === 'error' ||
|
appState.status === 'error' ||
|
||||||
@@ -48,17 +47,16 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
const ringFull = $derived(appState.status === 'ready')
|
const ringFull = $derived(appState.status === 'ready')
|
||||||
|
const showApp = $derived(!splashVisible)
|
||||||
|
|
||||||
const showApp = $derived(
|
function onSplashReady() { splashDismissed = true }
|
||||||
!splashVisible && (
|
function onSplashUnlock() { appState.status = 'ready'; splashDismissed = true }
|
||||||
appState.status === 'ready' ||
|
function onSplashBypass() {
|
||||||
bypassed
|
import('$lib/state/boot.svelte').then(({ bypassBoot }) => {
|
||||||
)
|
bypassBoot(appState.authMode ?? 'NONE', appState.authUser ?? '', appState.authPass ?? '')
|
||||||
)
|
})
|
||||||
|
splashDismissed = true
|
||||||
function onSplashReady() { _splashDismissed = true }
|
}
|
||||||
function onSplashUnlock() { appState.status = 'ready'; _splashDismissed = true }
|
|
||||||
function onSplashBypass() { bypassed = true; _splashDismissed = true }
|
|
||||||
|
|
||||||
const isReaderRoute = $derived($page.url.pathname.startsWith('/reader'))
|
const isReaderRoute = $derived($page.url.pathname.startsWith('/reader'))
|
||||||
const readerContainerized = $derived(settingsState.settings.readerContainerized ?? false)
|
const readerContainerized = $derived(settingsState.settings.readerContainerized ?? false)
|
||||||
@@ -141,10 +139,40 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (appState.status === 'booting') _splashDismissed = false
|
if (appState.status === 'booting') splashDismissed = false
|
||||||
})
|
})
|
||||||
|
|
||||||
function onIdleDismiss() { appState.idleSplash = false }
|
let idleTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let idleDismissLock = false
|
||||||
|
|
||||||
|
function onIdleDismiss() {
|
||||||
|
if (idleDismissLock) return
|
||||||
|
idleDismissLock = true
|
||||||
|
appState.idleSplash = false
|
||||||
|
setTimeout(() => { idleDismissLock = false }, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
function armIdleTimer() {
|
||||||
|
if (idleTimer !== null) clearTimeout(idleTimer)
|
||||||
|
const mins = settingsState.settings.idleTimeoutMin ?? 5
|
||||||
|
if (mins <= 0) return
|
||||||
|
idleTimer = setTimeout(() => {
|
||||||
|
if (appState.status === 'ready' && !appState.idleSplash) appState.idleSplash = true
|
||||||
|
}, mins * 60_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (appState.status !== 'ready') return
|
||||||
|
|
||||||
|
const events = ['mousemove', 'mousedown', 'keydown', 'touchstart', 'touchmove', 'wheel', 'click'] as const
|
||||||
|
for (const e of events) document.addEventListener(e, armIdleTimer, { capture: true, passive: true })
|
||||||
|
armIdleTimer()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (idleTimer !== null) { clearTimeout(idleTimer); idleTimer = null }
|
||||||
|
for (const e of events) document.removeEventListener(e, armIdleTimer, { capture: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function onSplashRetry() {
|
function onSplashRetry() {
|
||||||
import('$lib/state/boot.svelte').then(({ retryBoot }) => {
|
import('$lib/state/boot.svelte').then(({ retryBoot }) => {
|
||||||
@@ -174,7 +202,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if appState.idleSplash}
|
{#if appState.idleSplash}
|
||||||
<SplashScreen mode="idle" onDismiss={onIdleDismiss} />
|
<SplashScreen mode="idle" showCards={settingsState.settings.splashCards ?? true} onDismiss={onIdleDismiss} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if appState.devSplash}
|
||||||
|
<SplashScreen mode="idle" showDevOverlay onDismiss={() => appState.devSplash = false} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showApp}
|
{#if showApp}
|
||||||
|
|||||||
Reference in New Issue
Block a user