diff --git a/src/lib/components/chrome/AuthGate.svelte b/src/lib/components/chrome/AuthGate.svelte index 48f09ae..0e66429 100644 --- a/src/lib/components/chrome/AuthGate.svelte +++ b/src/lib/components/chrome/AuthGate.svelte @@ -1,14 +1,15 @@ -{#if appState.status === 'auth'} +{#if appState.authRequired && !authVerifiedState.value}
diff --git a/src/lib/components/chrome/SplashScreen.svelte b/src/lib/components/chrome/SplashScreen.svelte index 0ca888e..5bcbff8 100644 --- a/src/lib/components/chrome/SplashScreen.svelte +++ b/src/lib/components/chrome/SplashScreen.svelte @@ -47,6 +47,7 @@ ringFull?: boolean failed?: boolean notConfigured?: boolean + authRequired?: boolean showCards?: boolean showFps?: boolean showDevOverlay?: boolean @@ -56,14 +57,15 @@ onUnlock?: () => void onRetry?: () => void onBypass?: () => void + onSkip?: () => void onDismiss?: () => void } let { mode = 'loading', ringFull = false, failed = false, - notConfigured = false, showCards = true, showFps = false, showDevOverlay = false, + notConfigured = false, authRequired = false, showCards = true, showFps = false, showDevOverlay = false, pinLen = 4, pinCorrect = '', - onReady, onUnlock, onRetry, onBypass, onDismiss, + onReady, onUnlock, onRetry, onBypass, onSkip, onDismiss, }: Props = $props() let fpsEl = $state(undefined) @@ -91,11 +93,10 @@ const PHASE2_MS = 10000 function triggerExit(cb?: () => void) { - console.log('[splash] triggerExit called — exitLock:', exitLock, 'mode:', mode, 'cb:', cb?.name ?? String(cb)) - if (exitLock) { console.log('[splash] triggerExit blocked by exitLock'); return } + if (exitLock) return exitLock = true exiting = true - setTimeout(() => { console.log('[splash] triggerExit timeout — calling cb'); cb?.() }, EXIT_MS) + setTimeout(() => cb?.(), EXIT_MS) } let animFrame = 0 @@ -126,13 +127,13 @@ }) $effect(() => { - console.log('[splash] ringFull effect — ringFull:', ringFull, 'mode:', mode, 'exitLock:', exitLock) if (!ringFull || mode === 'locked') { exitLock = false; exiting = false; return } cancelAnimationFrame(animFrame) animFrame = 0 ringProg = 1 - const t = setTimeout(() => { console.log('[splash] ringFull timeout firing — calling triggerExit(onReady)'); triggerExit(onReady) }, 650) - return () => { console.log('[splash] ringFull effect cleanup — cancelling timeout'); clearTimeout(t) } + if (authRequired) return + const t = setTimeout(() => triggerExit(onReady), 650) + return () => clearTimeout(t) }) function submitPin() { @@ -532,6 +533,13 @@
+ {:else if authRequired && ringFull} +
+

Waiting for login

+
+ +
+
{:else}

{ringFull ? '' : `Initializing server${dots}`}

{/if} diff --git a/src/lib/components/home/ActivityFeed.svelte b/src/lib/components/home/ActivityFeed.svelte index 3f93596..fb5f72e 100644 --- a/src/lib/components/home/ActivityFeed.svelte +++ b/src/lib/components/home/ActivityFeed.svelte @@ -19,7 +19,7 @@ const entries = $derived( historyState.sessions .filter((s, i, arr) => arr.findIndex(x => x.mangaId === s.mangaId) === i) - .slice(0, 6) + .slice(0, 5) ) diff --git a/src/lib/components/recent/Recent.svelte b/src/lib/components/recent/Recent.svelte index 4666bd2..1560b4f 100644 --- a/src/lib/components/recent/Recent.svelte +++ b/src/lib/components/recent/Recent.svelte @@ -36,7 +36,7 @@ let libraryManga: Manga[] = $state([]) - let ctrl: AbortController | null = null + let ctrl: AbortController | null = null let statusPollTimer: ReturnType | null = null onMount(() => { @@ -221,8 +221,11 @@ try { if (updaterRunning) { await getAdapter().stopLibraryUpdate() + updaterRunning = false + stopStatusPolling() } else { await getAdapter().startLibraryUpdate() + updaterRunning = true scheduleStatusPoll() } } catch (e: any) { @@ -239,11 +242,13 @@ {historyConfirmClear} hasHistory={historyState.sessions.length > 0} {updatesLoading} + {updaterRunning} onTabChange={(t) => tab = t} onHistorySearchChange={(v) => historySearch = v} onUpdatesSearchChange={(v) => updatesSearch = v} onHistoryClear={handleHistoryClear} onRefreshUpdates={() => loadUpdates(true)} + onToggleUpdate={toggleLibraryUpdate} />
diff --git a/src/lib/components/recent/RecentToolbar.svelte b/src/lib/components/recent/RecentToolbar.svelte index 357dca6..214964e 100644 --- a/src/lib/components/recent/RecentToolbar.svelte +++ b/src/lib/components/recent/RecentToolbar.svelte @@ -1,7 +1,7 @@ @@ -57,12 +59,15 @@ {#if selectOpen === 'idle-timeout' || closingSelect === 'idle-timeout'} -
+
{#each [['0','Never'],['1','1 minute'],['2','2 minutes'],['5','5 minutes'],['10','10 minutes'],['15','15 minutes'],['30','30 minutes']] as [v, l]} {/each} diff --git a/src/lib/components/shared/manga/Thumbnail.svelte b/src/lib/components/shared/manga/Thumbnail.svelte index 9b2f319..ada65d3 100644 --- a/src/lib/components/shared/manga/Thumbnail.svelte +++ b/src/lib/components/shared/manga/Thumbnail.svelte @@ -1,6 +1,9 @@ \ No newline at end of file diff --git a/src/lib/core/auth.ts b/src/lib/core/auth.ts index 7b4a0a1..dc00230 100644 --- a/src/lib/core/auth.ts +++ b/src/lib/core/auth.ts @@ -1,3 +1,7 @@ +import { appState } from '$lib/state/app.svelte' +import { authVerifiedState } from '$lib/state/auth.svelte' +import { LOGIN_MUTATION, REFRESH_MUTATION } from '$lib/server-adapters/suwayomi/meta' + const DEFAULT_URL = 'http://127.0.0.1:4567' const SKEW_MS = 60_000 * 2 @@ -24,6 +28,7 @@ let accessToken: string | null = null let refreshToken: string | null = null let accessExpiresAt: number | null = null let refreshInFlight = false +let authSnoozed = false function parseExpiry(token: string): number | null { try { @@ -56,16 +61,34 @@ export function getUiAuthDebugStatus(): UiAuthDebugStatus { } } +export function reportUnauthorized(): void { + if (config.mode === 'NONE') return + if (authSnoozed) return + appState.authRequired = true + authVerifiedState.value = false +} + +export function reportAuthOk(): void { + appState.authRequired = false +} + +export function snoozeAuthPrompt(): void { + authSnoozed = true + appState.authRequired = false +} + export function configureAuth( baseUrl: string, mode: 'NONE' | 'BASIC_AUTH' | 'UI_LOGIN', user?: string, pass?: string, ): void { - config = { baseUrl: baseUrl.replace(/\/$/, ''), mode, user, pass } - accessToken = null - refreshToken = null - accessExpiresAt = null + config = { baseUrl: baseUrl.replace(/\/$/, ''), mode, user, pass } + accessToken = null + refreshToken = null + accessExpiresAt = null + authSnoozed = false + appState.authRequired = false } export function authHeaders(): Record { @@ -86,9 +109,16 @@ async function gql(query: string, variables?: Record, bare = headers, body: JSON.stringify({ query, variables }), }) + if (res.status === 401 || res.status === 403) { + reportUnauthorized() + throw new Error(`HTTP ${res.status}`) + } if (!res.ok) throw new Error(`HTTP ${res.status}`) const json = await res.json() - if (json.errors?.length) throw new Error(json.errors[0].message) + if (json.errors?.length) { + if (/unauthorized|unauthenticated/i.test(json.errors[0].message)) reportUnauthorized() + throw new Error(json.errors[0].message) + } return json.data as T } @@ -109,34 +139,33 @@ export async function probeServer(): Promise<'ok' | 'auth_required' | 'unreachab } catch { return 'unreachable' } } -const LOGIN_MUTATION = ` - mutation Login($username: String!, $password: String!) { - login(input: { username: $username, password: $password }) { - accessToken refreshToken - } - } -` +export function loginBasic(user: string, pass: string): void { + config.user = user + config.pass = pass + config.mode = 'BASIC_AUTH' + authSnoozed = false + reportAuthOk() +} -const REFRESH_MUTATION = ` - mutation RefreshToken($refreshToken: String!) { - refreshToken(input: { refreshToken: $refreshToken }) { - accessToken - } - } -` - -export async function loginBasic(user: string, pass: string): Promise { +/** + * Verify basic-auth credentials by making a real GQL request with them. + * Throws if the server returns 401/403 or an auth error. + */ +export async function verifyBasicAuth(user: string, pass: string): Promise { const prev = { user: config.user, pass: config.pass, mode: config.mode } config.user = user config.pass = pass config.mode = 'BASIC_AUTH' - const probe = await probeServer() - if (probe !== 'ok') { + try { + await gql('{ settings { authMode } }') + } catch { config.user = prev.user config.pass = prev.pass config.mode = prev.mode as typeof config.mode throw new Error('Invalid credentials') } + authSnoozed = false + reportAuthOk() } export async function loginUI(user: string, pass: string): Promise { @@ -148,6 +177,8 @@ export async function loginUI(user: string, pass: string): Promise { accessExpiresAt = parseExpiry(accessToken) config.mode = 'UI_LOGIN' config.user = user + authSnoozed = false + reportAuthOk() } export async function refreshUiAccessToken(force = false): Promise { @@ -163,8 +194,10 @@ export async function refreshUiAccessToken(force = false): Promise(); const inflight = new Map>(); @@ -18,22 +17,8 @@ interface QueueEntry { const queue: QueueEntry[] = []; -async function getAuthHeaders(): Promise> { - const mode = settingsState.settings.serverAuthMode ?? "NONE"; - if (mode === "UI_LOGIN") { - const token = getUIAccessToken(); - return token ? { Authorization: `Bearer ${token}` } : {}; - } - if (mode === "BASIC_AUTH") { - const user = settingsState.settings.serverAuthUser?.trim() ?? ""; - const pass = settingsState.settings.serverAuthPass?.trim() ?? ""; - return user && pass ? { Authorization: `Basic ${btoa(`${user}:${pass}`)}` } : {}; - } - return {}; -} - async function doFetch(url: string, gen: number): Promise { - const headers = await getAuthHeaders(); + const headers = authHeaders(); if (gen !== generation) throw new DOMException("Cancelled", "AbortError"); const blob = await platformService.fetchImage(url, headers); if (gen !== generation) throw new DOMException("Cancelled", "AbortError"); diff --git a/src/lib/core/cache/pageCache.ts b/src/lib/core/cache/pageCache.ts index d2c8b9b..09ce288 100644 --- a/src/lib/core/cache/pageCache.ts +++ b/src/lib/core/cache/pageCache.ts @@ -1,5 +1,6 @@ import { getBlobUrl, preloadBlobUrls, revokeBlobUrl } from "$lib/core/cache/imageCache"; -import { settingsState } from "$lib/state/settings.svelte"; +import { authHeaders } from "$lib/core/auth"; +import { settingsState } from "$lib/state/settings.svelte"; const pageCache = new Map(); const inflight = new Map>(); @@ -12,13 +13,7 @@ function getServerUrl(): string { async function fetchChapterPagesFromServer(chapterId: number): Promise { const base = getServerUrl(); - const headers: Record = { "Content-Type": "application/json" }; - const mode = settingsState.settings.serverAuthMode ?? "NONE"; - if (mode === "BASIC_AUTH") { - const u = settingsState.settings.serverAuthUser?.trim() ?? ""; - const p = settingsState.settings.serverAuthPass?.trim() ?? ""; - if (u && p) headers["Authorization"] = `Basic ${btoa(`${u}:${p}`)}`; - } + const headers = { "Content-Type": "application/json", ...authHeaders() }; const query = `mutation FetchChapterPages($chapterId: Int!) { fetchChapterPages(input: { chapterId: $chapterId }) { pages } }`; const res = await fetch(`${base}/api/graphql`, { method: "POST", diff --git a/src/lib/core/ui/selectPortal.ts b/src/lib/core/ui/selectPortal.ts new file mode 100644 index 0000000..0313774 --- /dev/null +++ b/src/lib/core/ui/selectPortal.ts @@ -0,0 +1,67 @@ +/** + * position:fixed dropdown anchored to a trigger element. + * + * getBoundingClientRect() returns full viewport coords. + * position:fixed is also relative to the viewport. + * So we just divide by zoom — no sidebar/titlebar subtraction needed + * (those subtractions are only needed in ContextMenu because its x/y come + * from a MouseEvent which is relative to the zoomed content area, not the viewport). + */ +export function selectPortal( + node: HTMLElement, + trigger: HTMLElement | undefined, +): { update(t: HTMLElement | undefined): void; destroy(): void } { + let currentTrigger = trigger + + node.style.visibility = 'hidden' + + function getZoom(): number { + const raw = parseFloat(document.documentElement.style.zoom || '1') || 1 + return raw > 10 ? raw / 100 : raw + } + + function position() { + if (!currentTrigger) return + + const zoom = getZoom() + const r = currentTrigger.getBoundingClientRect() + + // Convert viewport px → CSS px by dividing by zoom + const left = r.left / zoom + const top = r.top / zoom + const bottom = r.bottom / zoom + const width = r.width / zoom + + const vw = window.innerWidth / zoom + const vh = window.innerHeight / zoom + + const menuH = node.offsetHeight + const menuW = node.offsetWidth + + const above = menuH > 0 && (vh - bottom) < menuH + 8 && top > menuH + 8 + + const cssLeft = Math.min(left, vw - menuW - 4) + const cssTop = above ? top - menuH - 4 : bottom + 4 + + node.style.left = `${Math.max(4, cssLeft)}px` + node.style.top = `${cssTop}px` + node.style.minWidth = `${width}px` + node.style.visibility = 'visible' + } + + requestAnimationFrame(() => position()) + window.addEventListener('scroll', position, { capture: true, passive: true }) + window.addEventListener('resize', position, { passive: true }) + + return { + update(t) { + currentTrigger = t + node.style.visibility = 'hidden' + requestAnimationFrame(() => position()) + }, + destroy() { + window.removeEventListener('scroll', position, true) + window.removeEventListener('resize', position) + }, + } +} \ No newline at end of file diff --git a/src/lib/server-adapters/suwayomi/index.ts b/src/lib/server-adapters/suwayomi/index.ts index 21fe3da..3b3a4a4 100644 --- a/src/lib/server-adapters/suwayomi/index.ts +++ b/src/lib/server-adapters/suwayomi/index.ts @@ -116,7 +116,7 @@ import { RESTORE_BACKUP, VALIDATE_BACKUP, } from './meta' -import { authHeaders } from '$lib/core/auth' +import { authHeaders, reportUnauthorized } from '$lib/core/auth' import { type GQLResponse, mapManga, @@ -171,9 +171,9 @@ export class SuwayomiAdapter implements ServerAdapter { } private async gql( - query: string, + query: string, variables?: Record, - signal?: AbortSignal, + signal?: AbortSignal, ): Promise { const res = await fetch(`${this.baseUrl}/api/graphql`, { method: 'POST', @@ -181,12 +181,40 @@ export class SuwayomiAdapter implements ServerAdapter { body: JSON.stringify({ query, variables }), signal, }) + if (res.status === 401 || res.status === 403) { + reportUnauthorized() + throw new Error(`Suwayomi HTTP ${res.status}`) + } if (!res.ok) throw new Error(`Suwayomi HTTP ${res.status}`) const json: GQLResponse = await res.json() - if (json.errors?.length) throw new Error(json.errors[0].message) + if (json.errors?.length) { + if (/unauthorized|unauthenticated/i.test(json.errors[0].message)) reportUnauthorized() + throw new Error(json.errors[0].message) + } return json.data } + private multipartGql(query: string, file: File): Promise { + const form = new FormData() + form.append('operations', JSON.stringify({ query, variables: { backup: null } })) + form.append('map', JSON.stringify({ '0': ['variables.backup'] })) + form.append('0', file, file.name) + const headers: Record = { Accept: 'application/json', ...authHeaders() } + return fetch(`${this.baseUrl}/api/graphql`, { method: 'POST', headers, body: form }) + .then(r => { + if (r.status === 401 || r.status === 403) { reportUnauthorized(); throw new Error(`Suwayomi HTTP ${r.status}`) } + if (!r.ok) throw new Error(`Suwayomi HTTP ${r.status}`) + return r.json() + }) + .then((json: GQLResponse) => { + if (json.errors?.length) { + if (/unauthorized|unauthenticated/i.test(json.errors[0].message)) reportUnauthorized() + throw new Error(json.errors[0].message) + } + return json.data + }) + } + async getAboutServer(): Promise { const data = await this.gql<{ aboutServer: AboutServer }>(GET_ABOUT_SERVER) return data.aboutServer @@ -502,7 +530,6 @@ export class SuwayomiAdapter implements ServerAdapter { ids: number[], patch: { includeInUpdate?: 'INCLUDE' | 'EXCLUDE'; includeInDownload?: 'INCLUDE' | 'EXCLUDE' }, ): Promise { - // Suwayomi has no bulk-category-patch mutation; fan out individually. await Promise.all(ids.map(id => this.gql(UPDATE_CATEGORY, { id, ...patch }))) } @@ -646,17 +673,6 @@ export class SuwayomiAdapter implements ServerAdapter { return data.createBackup } - private multipartGql(query: string, file: File): Promise { - const form = new FormData() - form.append('operations', JSON.stringify({ query, variables: { backup: null } })) - form.append('map', JSON.stringify({ '0': ['variables.backup'] })) - form.append('0', file, file.name) - const headers: Record = { Accept: 'application/json', ...authHeaders() } - return fetch(`${this.baseUrl}/api/graphql`, { method: 'POST', headers, body: form }) - .then(r => { if (!r.ok) throw new Error(`Suwayomi HTTP ${r.status}`); return r.json() }) - .then((json: GQLResponse) => { if (json.errors?.length) throw new Error(json.errors[0].message); return json.data }) - } - async restoreBackup(file: File): Promise<{ id: string; status: RestoreStatus }> { const data = await this.multipartGql<{ restoreBackup: { id: string; status: RestoreStatus } }>(RESTORE_BACKUP, file) return data.restoreBackup diff --git a/src/lib/server-adapters/suwayomi/meta.ts b/src/lib/server-adapters/suwayomi/meta.ts index 8571f34..44fe6b6 100644 --- a/src/lib/server-adapters/suwayomi/meta.ts +++ b/src/lib/server-adapters/suwayomi/meta.ts @@ -102,4 +102,20 @@ export const SET_FLARE_SOLVERR = ` settings { flareSolverrEnabled flareSolverrUrl } } } +` + +export const LOGIN_MUTATION = ` + mutation Login($username: String!, $password: String!) { + login(input: { username: $username, password: $password }) { + accessToken refreshToken + } + } +` + +export const REFRESH_MUTATION = ` + mutation RefreshToken($refreshToken: String!) { + refreshToken(input: { refreshToken: $refreshToken }) { + accessToken + } + } ` \ No newline at end of file diff --git a/src/lib/state/app.svelte.ts b/src/lib/state/app.svelte.ts index 940c3dc..310c0f5 100644 --- a/src/lib/state/app.svelte.ts +++ b/src/lib/state/app.svelte.ts @@ -1,6 +1,6 @@ import type { Platform } from '$lib/platform-adapters/types' -export type AppStatus = 'booting' | 'not-configured' | 'auth' | 'locked' | 'ready' | 'error' +export type AppStatus = 'booting' | 'not-configured' | 'locked' | 'ready' | 'error' class AppStore { settingsOpen: boolean = $state(false) @@ -23,6 +23,7 @@ export const app = new AppStore() export const appState = $state({ status: 'booting' as AppStatus, + authRequired: false as boolean, error: null as string | null, serverUrl: '', authenticated: false, diff --git a/src/lib/state/auth.svelte.ts b/src/lib/state/auth.svelte.ts new file mode 100644 index 0000000..b2e05d0 --- /dev/null +++ b/src/lib/state/auth.svelte.ts @@ -0,0 +1 @@ +export const authVerifiedState = $state({ value: false }) \ No newline at end of file diff --git a/src/lib/state/boot.svelte.ts b/src/lib/state/boot.svelte.ts index 4f782dd..f190ed2 100644 --- a/src/lib/state/boot.svelte.ts +++ b/src/lib/state/boot.svelte.ts @@ -1,13 +1,14 @@ import { detectAdapter } from '$lib/platform-adapters' import { initPlatformService } from '$lib/platform-service' import { platformService } from '$lib/platform-service' -import { probeServer, loginBasic, loginUI, configureAuth } from '$lib/core/auth' +import { probeServer, loginBasic, loginUI, verifyBasicAuth, configureAuth } from '$lib/core/auth' +import { authVerifiedState } from '$lib/state/auth.svelte' import { appState } from '$lib/state/app.svelte' import { settingsState } from '$lib/state/settings.svelte' -const MAX_ATTEMPTS = 40 +const MAX_ATTEMPTS = 40 const WEB_MAX_ATTEMPTS = 1 -const BG_MAX_ATTEMPTS = 120 +const BG_MAX_ATTEMPTS = 120 export const boot = $state({ failed: false, @@ -40,11 +41,12 @@ function pinLockEnabled(): boolean { function handleProbeSuccess(gen: number) { if (gen !== probeGeneration) return - boot.failed = false - boot.skipped = false - boot.serverProbeOk = true - appState.authenticated = true - appState.status = pinLockEnabled() ? 'locked' : 'ready' + boot.failed = false + boot.skipped = false + boot.serverProbeOk = true + authVerifiedState.value = true + appState.authenticated = true + appState.status = pinLockEnabled() ? 'locked' : 'ready' } function handleAuthRequired( @@ -59,20 +61,17 @@ function handleAuthRequired( appState.authMode = authMode if (authMode === 'BASIC_AUTH' && user && pass) { + // Saved creds — set optimistically; a real 401 will re-prompt via reportUnauthorized loginBasic(user, pass) - .then(() => { if (gen === probeGeneration) handleProbeSuccess(gen) }) - .catch(() => { - if (gen !== probeGeneration) return - boot.loginUser = user - boot.loginRequired = true - appState.status = 'auth' - }) + handleProbeSuccess(gen) return } - boot.loginUser = user - boot.loginRequired = true - appState.status = 'auth' + boot.loginUser = user + boot.loginRequired = true + authVerifiedState.value = false + appState.authRequired = true + appState.status = 'ready' // let layout render, AuthGate overlay will block } export async function startProbe( @@ -82,12 +81,13 @@ export async function startProbe( initialDelay = 100, ): Promise { const gen = ++probeGeneration - boot.failed = false - boot.loginRequired = false - boot.skipped = false - boot.serverProbeOk = false - appState.status = 'booting' - appState.authMode = authMode + boot.failed = false + boot.loginRequired = false + boot.skipped = false + boot.serverProbeOk = false + authVerifiedState.value = false + appState.status = 'booting' + appState.authMode = authMode const baseUrl = settingsState.settings.serverUrl ?? 'http://127.0.0.1:4567' configureAuth(baseUrl, authMode, user || undefined, pass || undefined) @@ -150,16 +150,18 @@ export async function submitLogin(): Promise { if (appState.authMode === 'UI_LOGIN') { await loginUI(boot.loginUser.trim(), boot.loginPass.trim()) } else { - await loginBasic(boot.loginUser.trim(), boot.loginPass.trim()) + await verifyBasicAuth(boot.loginUser.trim(), boot.loginPass.trim()) } - boot.loginRequired = false - boot.sessionExpired = false - boot.skipped = false - boot.loginPass = '' - boot.loginError = null - boot.serverProbeOk = true - appState.authenticated = true - appState.status = pinLockEnabled() ? 'locked' : 'ready' + boot.loginRequired = false + boot.sessionExpired = false + boot.skipped = false + boot.loginPass = '' + boot.loginError = null + boot.serverProbeOk = true + authVerifiedState.value = true + appState.authenticated = true + appState.authRequired = false + appState.status = pinLockEnabled() ? 'locked' : 'ready' } catch (e: unknown) { boot.loginError = e instanceof Error ? e.message : 'Login failed' } finally { @@ -184,9 +186,11 @@ export function bypassBoot( user = '', pass = '', ) { - boot.loginRequired = false - boot.sessionExpired = false - boot.skipped = true - appState.status = 'ready' + boot.loginRequired = false + boot.sessionExpired = false + boot.skipped = true + authVerifiedState.value = true // user explicitly opted out of the auth gate + appState.authRequired = false + appState.status = 'ready' startBackgroundProbe(probeGeneration, authMode, user, pass) } \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 54fe323..371c287 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -18,6 +18,7 @@ import { downloadStore } from '$lib/state/downloads.svelte' import { seriesState } from '$lib/state/series.svelte' import MangaPreview from '$lib/components/shared/manga/MangaPreview.svelte' + import { authVerifiedState } from '$lib/state/auth.svelte' import '../app.css' let { children } = $props() @@ -45,7 +46,6 @@ appState.status === 'booting' || appState.status === 'locked' || appState.status === 'error' || - appState.status === 'auth' || (appState.status === 'ready' && !splashDismissed) ) @@ -56,7 +56,7 @@ const ringFull = $derived(appState.status === 'ready') const showApp = $derived(!splashVisible) - function onSplashReady() { splashDismissed = true } + function onSplashReady() { if (!appState.authRequired || authVerifiedState.value) splashDismissed = true } function onSplashUnlock() { appState.status = 'ready'; splashDismissed = true } function onSplashBypass() { import('$lib/state/boot.svelte').then(({ bypassBoot }) => { @@ -208,11 +208,13 @@ {ringFull} failed={appState.status === 'error'} notConfigured={boot.notConfigured} + authRequired={appState.authRequired && !authVerifiedState.value} pinLen={settingsState.settings.appLockPin?.length ?? 0} pinCorrect={settingsState.settings.appLockPin ?? ''} onReady={onSplashReady} onUnlock={onSplashUnlock} onBypass={onSplashBypass} + onSkip={onSplashBypass} onRetry={onSplashRetry} /> {/if}