Fix: Local-Source Popular Query + App-Pin Flow

This commit is contained in:
Youwes09
2026-06-06 15:00:59 -05:00
parent 5dfbc80bbe
commit ed4c11ca7e
7 changed files with 225 additions and 94 deletions
+37 -18
View File
@@ -1,7 +1,9 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { getCurrentWindow } from '@tauri-apps/api/window' import logoUrl from '$lib/assets/moku-icon-splash.svg'
import logoUrl from '$lib/assets/moku-icon-splash.svg' import { platformService } from '$lib/platform-service'
const isTauri = platformService.platform === 'tauri'
interface Props { interface Props {
mode?: 'loading' | 'idle' | 'locked' mode?: 'loading' | 'idle' | 'locked'
@@ -78,6 +80,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
animStart = null animStart = null
animPhase = 1 animPhase = 1
animFrame = requestAnimationFrame(animateRing) animFrame = requestAnimationFrame(animateRing)
@@ -278,28 +281,45 @@
} }
function mountCanvas(el: HTMLCanvasElement) { function mountCanvas(el: HTMLCanvasElement) {
const win = getCurrentWindow()
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
async function syncSize() { function applySize(logW: number, logH: number, scale: number) {
const gen = ++buildGen const gen = ++buildGen
const [phys, scale] = await Promise.all([win.innerSize(), win.scaleFactor()]) if (logW <= 0 || logH <= 0) return
if (gen !== buildGen) return
const logW = phys.width / scale, logH = phys.height / scale
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
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 = phys.width; el.height = phys.height el.width = Math.round(logW * scale)
live = { cards: built.cards, trigs: built.trigs, stamps, vignette: vig, CW: phys.width, CH: phys.height, 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 }
} }
const ro = new ResizeObserver(() => syncSize()) let extraCleanup: (() => void) | undefined
ro.observe(el)
syncSize() if (isTauri) {
let tauriRo: ResizeObserver | undefined
let tauriUnlisten: (() => void) | undefined
import('@tauri-apps/api/window').then(({ getCurrentWindow }) => {
const win = getCurrentWindow()
const doSync = () => Promise.all([win.innerSize(), win.scaleFactor()])
.then(([phys, scale]) => applySize(phys.width / scale, phys.height / scale, scale))
doSync()
tauriRo = new ResizeObserver(() => doSync())
tauriRo.observe(el)
win.onFocusChanged(() => doSync()).then(u => { tauriUnlisten = u })
})
extraCleanup = () => { tauriRo?.disconnect(); tauriUnlisten?.() }
} else {
const syncWeb = () => applySize(el.clientWidth, el.clientHeight, window.devicePixelRatio || 1)
const ro = new ResizeObserver(() => syncWeb())
ro.observe(el)
requestAnimationFrame(() => syncWeb())
extraCleanup = () => ro.disconnect()
}
let raf = 0, t0 = -1, paused = false let raf = 0, t0 = -1, paused = false
@@ -307,9 +327,11 @@
if (paused) { raf = 0; return } if (paused) { raf = 0; return }
raf = requestAnimationFrame(frame) raf = requestAnimationFrame(frame)
if (!live) return if (!live) return
const { cards, trigs, stamps, vignette, CW, CH, scale } = live
if (CW <= 0 || CH <= 0 || vignette.width <= 0 || vignette.height <= 0) return
if (stamps.some(s => s.width <= 0 || s.height <= 0)) return
if (t0 < 0) t0 = now if (t0 < 0) t0 = now
if (showFps) tickFps(now) if (showFps) tickFps(now)
const { cards, trigs, stamps, vignette, CW, CH, scale } = live
drawFrame(ctx, (now - t0) / 1000, CW, CH, scale, cards, trigs, stamps, vignette) drawFrame(ctx, (now - t0) / 1000, CW, CH, scale, cards, trigs, stamps, vignette)
} }
@@ -318,14 +340,11 @@
function onVis() { document.hidden ? pause() : resume() } function onVis() { document.hidden ? pause() : resume() }
document.addEventListener('visibilitychange', onVis) document.addEventListener('visibilitychange', onVis)
const unlistenFocus = win.onFocusChanged(({ payload: focused }) => { focused ? resume() : pause() })
raf = requestAnimationFrame(frame) raf = requestAnimationFrame(frame)
return () => { return () => {
cancelAnimationFrame(raf) cancelAnimationFrame(raf)
ro.disconnect() extraCleanup?.()
document.removeEventListener('visibilitychange', onVis) document.removeEventListener('visibilitychange', onVis)
unlistenFocus.then(f => f())
} }
} }
</script> </script>
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { ArrowLeft, MagnifyingGlass, GearSix, Swap, Funnel, Check } from "phosphor-svelte"; import { ArrowLeft, MagnifyingGlass, GearSix, Swap, Funnel, Check, CircleNotch } from "phosphor-svelte";
import Thumbnail from "$lib/components/shared/manga/Thumbnail.svelte"; import Thumbnail from "$lib/components/shared/manga/Thumbnail.svelte";
import { resolvedCover } from "$lib/core/cover/coverResolver"; import { resolvedCover } from "$lib/core/cover/coverResolver";
import { getAdapter } from "$lib/request-manager"; import { getAdapter } from "$lib/request-manager";
@@ -25,9 +25,22 @@
let { pkgName, extensionName, iconUrl, cols, cropCovers, statsAlways, anims, sources, onBack, onSettings }: Props = $props(); let { pkgName, extensionName, iconUrl, cols, cropCovers, statsAlways, anims, sources, onBack, onSettings }: Props = $props();
const isLocal = pkgName === '__local__';
// ── Library mode state ──────────────────────────────────────────────
let groups: SourceLibrary[] = $state([]); let groups: SourceLibrary[] = $state([]);
let sourceNodes: SourceNode[] = $state([]);
// ── Local/browse mode state ──────────────────────────────────────────
let localItems: any[] = $state([]);
let localPage: number = $state(1);
let localHasNext: boolean = $state(false);
let localLoadingMore: boolean = $state(false);
// ── Shared state ─────────────────────────────────────────────────────
let loading = $state(true); let loading = $state(true);
let search = $state(""); let search = $state("");
let searchInput = $state("");
type ContentFilter = "unread" | "downloaded"; type ContentFilter = "unread" | "downloaded";
let activeFilters = $state<Partial<Record<ContentFilter, boolean>>>({}); let activeFilters = $state<Partial<Record<ContentFilter, boolean>>>({});
@@ -37,35 +50,80 @@
let migrateTarget: { sourceId: string; sourceName: string; iconUrl: string; manga: LibraryManga[] } | null = $state(null); let migrateTarget: { sourceId: string; sourceName: string; iconUrl: string; manga: LibraryManga[] } | null = $state(null);
const allManga = $derived(groups.flatMap(g => g.manga)); // ── Derived filtered lists ────────────────────────────────────────────
const allManga = $derived(isLocal ? localItems : groups.flatMap(g => g.manga));
const filtered = $derived((() => { const filtered = $derived((() => {
let items = allManga; let items = allManga;
const q = search.trim().toLowerCase(); const q = search.trim().toLowerCase();
if (q) items = items.filter(m => m.title.toLowerCase().includes(q)); if (q && !isLocal) items = items.filter((m: any) => m.title.toLowerCase().includes(q));
if (activeFilters.unread) items = items.filter(m => m.unreadCount > 0); if (!isLocal) {
if (activeFilters.downloaded) items = items.filter(m => m.downloadCount > 0); if (activeFilters.unread) items = items.filter((m: any) => m.unreadCount > 0);
if (activeFilters.downloaded) items = items.filter((m: any) => m.downloadCount > 0);
}
return items; return items;
})()); })());
let sourceNodes: SourceNode[] = $state([]);
$effect(() => { load(); }); $effect(() => { load(); });
async function load() { async function load() {
loading = true; loading = true;
try { try {
const [libData, srcData] = await Promise.all([ if (isLocal) {
getAdapter().getMangaList({}).then(r => ({ mangas: { nodes: r.items as any } })), localPage = 1;
getAdapter().getSources().then(nodes => ({ sources: { nodes } })), localItems = [];
]); const result = await getAdapter().browseSource('0', 1);
sourceNodes = srcData.sources.nodes; localItems = result.items;
groups = libraryByExtension(libData.mangas.nodes, srcData.sources.nodes, pkgName); localHasNext = result.hasNextPage;
localPage = 1;
} else {
const [libData, srcData] = await Promise.all([
getAdapter().getMangaList({}).then(r => ({ mangas: { nodes: r.items as any } })),
getAdapter().getSources().then(nodes => ({ sources: { nodes } })),
]);
sourceNodes = srcData.sources.nodes;
groups = libraryByExtension(libData.mangas.nodes, srcData.sources.nodes, pkgName);
}
} finally { } finally {
loading = false; loading = false;
} }
} }
async function loadMoreLocal() {
if (localLoadingMore || !localHasNext) return;
localLoadingMore = true;
try {
const next = localPage + 1;
const result = await getAdapter().browseSource('0', next);
localItems = [...localItems, ...result.items];
localHasNext = result.hasNextPage;
localPage = next;
} finally {
localLoadingMore = false;
}
}
async function searchLocal() {
const q = searchInput.trim();
if (!q) { load(); return; }
loading = true;
try {
const result = await getAdapter().searchSource('0', q, 1);
localItems = result.items;
localHasNext = result.hasNextPage;
localPage = 1;
} finally {
loading = false;
}
search = q;
}
function onSearchKeydown(e: KeyboardEvent) {
if (!isLocal) return;
if (e.key === 'Enter') searchLocal();
if (e.key === 'Escape') { searchInput = ''; search = ''; load(); }
}
function toggleFilter(f: ContentFilter) { function toggleFilter(f: ContentFilter) {
activeFilters = { ...activeFilters, [f]: !activeFilters[f] }; activeFilters = { ...activeFilters, [f]: !activeFilters[f] };
} }
@@ -108,58 +166,72 @@
<Thumbnail src={iconUrl} alt={extensionName} class="header-icon" onerror={(e) => ((e.target as HTMLImageElement).style.display = "none")} /> <Thumbnail src={iconUrl} alt={extensionName} class="header-icon" onerror={(e) => ((e.target as HTMLImageElement).style.display = "none")} />
{/if} {/if}
<div class="title-block"> <div class="title-block">
<span class="eyebrow">In Library</span> <span class="eyebrow">{isLocal ? 'Local Source' : 'In Library'}</span>
<span class="title">{extensionName}</span> <span class="title">{extensionName}</span>
</div> </div>
{#if !loading} {#if !loading}
<span class="count-badge">{filtered.length}{filtered.length !== allManga.length ? ` / ${allManga.length}` : ""}</span> <span class="count-badge">
{isLocal ? allManga.length + (localHasNext ? '+' : '') : `${filtered.length}${filtered.length !== allManga.length ? ` / ${allManga.length}` : ''}`}
</span>
{/if} {/if}
<div class="header-right"> <div class="header-right">
<div class="search-wrap"> <div class="search-wrap">
<MagnifyingGlass size={12} class="search-icon" weight="light" /> <MagnifyingGlass size={12} class="search-icon" weight="light" />
<input class="search" placeholder="Search" bind:value={search} autocomplete="off" /> {#if isLocal}
</div> <input
class="search"
<div class="filter-wrap"> placeholder="Search…"
<button bind:value={searchInput}
class="filter-btn" autocomplete="off"
class:filter-btn-active={hasActiveFilters} onkeydown={onSearchKeydown}
title="Filter" />
onclick={() => filterOpen = !filterOpen} {:else}
> <input class="search" placeholder="Search" bind:value={search} autocomplete="off" />
<Funnel size={13} weight={hasActiveFilters ? "fill" : "bold"} />
</button>
{#if filterOpen}
<div class="filter-panel" role="menu">
<div class="filter-panel-header">
<span class="panel-heading">Filter</span>
{#if hasActiveFilters}
<button class="panel-clear-btn" onclick={clearFilters}>Clear all</button>
{/if}
</div>
<div class="panel-divider"></div>
<p class="panel-label">Content</p>
{#each CONTENT_FILTERS as [f, label]}
<button
class="panel-item"
class:panel-item-active={activeFilters[f]}
role="menuitem"
onclick={() => toggleFilter(f)}
>
<span class="panel-check" class:panel-check-on={activeFilters[f]}>
{#if activeFilters[f]}<Check size={9} weight="bold" />{/if}
</span>
{label}
</button>
{/each}
</div>
{/if} {/if}
</div> </div>
{#if sources.length > 0} {#if !isLocal}
<button class="settings-btn" onclick={onSettings} title="Extension settings"> <div class="filter-wrap">
<GearSix size={14} weight="bold" /> <button
</button> class="filter-btn"
class:filter-btn-active={hasActiveFilters}
title="Filter"
onclick={() => filterOpen = !filterOpen}
>
<Funnel size={13} weight={hasActiveFilters ? "fill" : "bold"} />
</button>
{#if filterOpen}
<div class="filter-panel" role="menu">
<div class="filter-panel-header">
<span class="panel-heading">Filter</span>
{#if hasActiveFilters}
<button class="panel-clear-btn" onclick={clearFilters}>Clear all</button>
{/if}
</div>
<div class="panel-divider"></div>
<p class="panel-label">Content</p>
{#each CONTENT_FILTERS as [f, label]}
<button
class="panel-item"
class:panel-item-active={activeFilters[f]}
role="menuitem"
onclick={() => toggleFilter(f)}
>
<span class="panel-check" class:panel-check-on={activeFilters[f]}>
{#if activeFilters[f]}<Check size={9} weight="bold" />{/if}
</span>
{label}
</button>
{/each}
</div>
{/if}
</div>
{#if sources.length > 0}
<button class="settings-btn" onclick={onSettings} title="Extension settings">
<GearSix size={14} weight="bold" />
</button>
{/if}
{/if} {/if}
</div> </div>
</div> </div>
@@ -176,10 +248,14 @@
</div> </div>
{:else if filtered.length === 0} {:else if filtered.length === 0}
<div class="empty"> <div class="empty">
{allManga.length === 0 ? "Nothing from this extension is in your library." : "No matches."} {isLocal
? 'No manga found in local source. Add manga folders to your local source directory.'
: allManga.length === 0
? 'Nothing from this extension is in your library.'
: 'No matches.'}
</div> </div>
{:else} {:else}
{#if groups.length > 1} {#if !isLocal && groups.length > 1}
<div class="source-groups"> <div class="source-groups">
{#each groups as group} {#each groups as group}
<div class="source-group-header"> <div class="source-group-header">
@@ -192,7 +268,7 @@
</div> </div>
{/each} {/each}
</div> </div>
{:else if groups.length === 1} {:else if !isLocal && groups.length === 1}
<div class="single-source-bar"> <div class="single-source-bar">
<span class="source-group-name">{groups[0].displayName}</span> <span class="source-group-name">{groups[0].displayName}</span>
<button class="migrate-btn" onclick={() => openMigrate(groups[0])} title="Migrate this source"> <button class="migrate-btn" onclick={() => openMigrate(groups[0])} title="Migrate this source">
@@ -214,23 +290,38 @@
style="object-fit:{cropCovers ? 'cover' : 'contain'}" style="object-fit:{cropCovers ? 'cover' : 'contain'}"
draggable="false" draggable="false"
/> />
<div class="card-info-overlay" class:anim={anims} class:instant={!anims} class:always={statsAlways}> {#if !isLocal}
<div class="overlay-badges"> <div class="card-info-overlay" class:anim={anims} class:instant={!anims} class:always={statsAlways}>
{#if isCompleted} <div class="overlay-badges">
<span class="badge badge-done">✓ Done</span> {#if isCompleted}
{:else if m.unreadCount} <span class="badge badge-done"> Done</span>
<span class="badge badge-unread">{m.unreadCount} new</span> {:else if m.unreadCount}
{/if} <span class="badge badge-unread">{m.unreadCount} new</span>
{#if m.downloadCount} {/if}
<span class="badge badge-dl">{m.downloadCount}</span> {#if m.downloadCount}
{/if} <span class="badge badge-dl"> {m.downloadCount}</span>
{/if}
</div>
</div> </div>
</div> {/if}
</div> </div>
<p class="card-title">{m.title}</p> <p class="card-title">{m.title}</p>
</button> </button>
{/each} {/each}
</div> </div>
{#if isLocal && localHasNext}
<div class="load-more">
<button class="load-more-btn" onclick={loadMoreLocal} disabled={localLoadingMore}>
{#if localLoadingMore}
<CircleNotch size={13} weight="light" class="anim-spin" />
Loading
{:else}
Load more
{/if}
</button>
</div>
{/if}
{/if} {/if}
</div> </div>
</div> </div>
@@ -330,7 +421,12 @@
.cover-skeleton { aspect-ratio: 2/3; border-radius: var(--radius-md); } .cover-skeleton { aspect-ratio: 2/3; border-radius: var(--radius-md); }
.title-skeleton { height: 12px; margin-top: var(--sp-2); width: 80%; border-radius: var(--radius-sm); } .title-skeleton { height: 12px; margin-top: var(--sp-2); width: 80%; border-radius: var(--radius-sm); }
.empty { display: flex; align-items: center; justify-content: center; height: 60%; color: var(--text-muted); font-size: var(--text-sm); } .empty { display: flex; align-items: center; justify-content: center; height: 60%; color: var(--text-muted); font-size: var(--text-sm); text-align: center; padding: 0 var(--sp-6); }
.load-more { display: flex; justify-content: center; padding: var(--sp-4) 0; }
.load-more-btn { display: flex; align-items: center; gap: var(--sp-2); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 7px 20px; border-radius: var(--radius-md); background: var(--bg-raised); color: var(--text-muted); border: 1px solid var(--border-dim); cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
.load-more-btn:hover:not(:disabled) { color: var(--text-primary); border-color: var(--border-strong); }
.load-more-btn:disabled { opacity: 0.5; cursor: default; }
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } } @keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
</style> </style>
@@ -28,7 +28,7 @@
} }
let extensions: Extension[] = $state([]); let extensions: Extension[] = $state([]);
let localMangaCount = $state(0); let localMangaCount = $state<string>("0");
let loading = $state(true); let loading = $state(true);
let refreshing = $state(false); let refreshing = $state(false);
let filter = $state<Filter>("installed"); let filter = $state<Filter>("installed");
@@ -84,8 +84,10 @@
} }
async function loadLocalManga() { async function loadLocalManga() {
const d = await Promise.resolve(null); try {
const r = await getAdapter().browseSource('0', 1)
localMangaCount = r.hasNextPage ? r.items.length + '+' : String(r.items.length)
} catch {}
} }
async function fetchFromRepo() { async function fetchFromRepo() {
@@ -338,7 +340,7 @@
{:else} {:else}
<div class="list"> <div class="list">
{#if showLocal} {#if showLocal}
<div class="local-row"> <div class="local-row" style="cursor:pointer" onclick={() => libraryTarget = { pkgName: '__local__', extensionName: 'Local Source', iconUrl: '' }}>
<div class="local-icon"><HardDrives size={18} weight="bold" /></div> <div class="local-icon"><HardDrives size={18} weight="bold" /></div>
<div class="info"> <div class="info">
<span class="name">Local Source</span> <span class="name">Local Source</span>
@@ -2,6 +2,8 @@
import { settingsState, updateSettings } from '$lib/state/settings.svelte' import { settingsState, updateSettings } from '$lib/state/settings.svelte'
import { platformService } from '$lib/platform-service' import { platformService } from '$lib/platform-service'
const isTauri = platformService.platform === 'tauri'
interface Props { interface Props {
selectOpen: string | null selectOpen: string | null
closingSelect: string | null closingSelect: string | null
@@ -67,6 +69,7 @@
</div> </div>
</div> </div>
{#if isTauri}
<label class="s-row"> <label class="s-row">
<div class="s-row-info"><span class="s-label">Auto-start server</span><span class="s-desc">Launch tachidesk-server when Moku opens</span></div> <div class="s-row-info"><span class="s-label">Auto-start server</span><span class="s-desc">Launch tachidesk-server when Moku opens</span></div>
<button role="switch" aria-checked={settingsState.settings.autoStartServer} aria-label="Auto-start server" <button role="switch" aria-checked={settingsState.settings.autoStartServer} aria-label="Auto-start server"
@@ -84,6 +87,7 @@
<span class="s-toggle-thumb"></span> <span class="s-toggle-thumb"></span>
</button> </button>
</label> </label>
{/if}
{#if serverAdvancedOpen} {#if serverAdvancedOpen}
<div class="srv-adv-panel"> <div class="srv-adv-panel">
+1 -1
View File
@@ -441,7 +441,7 @@ export class SuwayomiAdapter implements ServerAdapter {
async browseSource(sourceId: string, page: number): Promise<PaginatedResult<Manga>> { async browseSource(sourceId: string, page: number): Promise<PaginatedResult<Manga>> {
const data = await this.gql<{ const data = await this.gql<{
fetchSourceManga: { mangas: Record<string, unknown>[]; hasNextPage: boolean } fetchSourceManga: { mangas: Record<string, unknown>[]; hasNextPage: boolean }
}>(FETCH_SOURCE_MANGA, { source: sourceId, type: 'LATEST', page }) }>(FETCH_SOURCE_MANGA, { source: sourceId, type: sourceId === '0' ? 'POPULAR' : 'LATEST', page })
return { return {
items: data.fetchSourceManga.mangas.map(mapManga), items: data.fetchSourceManga.mangas.map(mapManga),
hasNextPage: data.fetchSourceManga.hasNextPage, hasNextPage: data.fetchSourceManga.hasNextPage,
+9 -1
View File
@@ -87,7 +87,15 @@ export function startProbe(
boot.skipped = false boot.skipped = false
boot.serverProbeOk = false boot.serverProbeOk = false
appState.status = 'booting' appState.status = 'booting'
let tries = 0
if (appState.platform === 'web') {
boot.failed = true
appState.status = 'error'
startBackgroundProbe(gen, authMode, user, pass)
return
}
let tries = 0
async function probe() { async function probe() {
if (gen !== probeGeneration) return if (gen !== probeGeneration) return
+2
View File
@@ -2,6 +2,7 @@
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { page } from '$app/stores' import { page } from '$app/stores'
import { appState, app } from '$lib/state/app.svelte' import { appState, app } from '$lib/state/app.svelte'
import { boot } from '$lib/state/boot.svelte'
import { notifications } from '$lib/state/notifications.svelte' import { notifications } from '$lib/state/notifications.svelte'
import { settingsState, loadSettingsIntoState, updateSettings } from '$lib/state/settings.svelte' import { settingsState, loadSettingsIntoState, updateSettings } from '$lib/state/settings.svelte'
import { applyTheme, mountSystemThemeSync } from '$lib/core/theme' import { applyTheme, mountSystemThemeSync } from '$lib/core/theme'
@@ -147,6 +148,7 @@
mode={appState.status === 'locked' ? 'locked' : 'loading'} mode={appState.status === 'locked' ? 'locked' : 'loading'}
{ringFull} {ringFull}
failed={appState.status === 'error'} failed={appState.status === 'error'}
notConfigured={boot.notConfigured}
pinLen={settingsState.settings.appLockPin?.length ?? 0} pinLen={settingsState.settings.appLockPin?.length ?? 0}
pinCorrect={settingsState.settings.appLockPin ?? ''} pinCorrect={settingsState.settings.appLockPin ?? ''}
onReady={onSplashReady} onReady={onSplashReady}