mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-15 10:19:55 -05:00
Fix: Basic Auth Fall-back Management & Settings Drop-down Portal (WIP)
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
<script lang="ts">
|
||||
import logoUrl from '$lib/assets/moku-icon-splash.svg'
|
||||
import { appState } from '$lib/state/app.svelte'
|
||||
import { authVerifiedState } from '$lib/state/auth.svelte'
|
||||
import { boot, submitLogin, bypassBoot } from '$lib/state/boot.svelte'
|
||||
|
||||
function handleBypass() {
|
||||
bypassBoot(appState.authMode, boot.loginUser)
|
||||
bypassBoot(appState.authMode, boot.loginUser, boot.loginPass)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if appState.status === 'auth'}
|
||||
{#if appState.authRequired && !authVerifiedState.value}
|
||||
<div class="overlay overlay--clear">
|
||||
<div class="card anim-scale-in">
|
||||
<img src={logoUrl} alt="Moku" class="logo" />
|
||||
|
||||
@@ -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<HTMLSpanElement | undefined>(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 @@
|
||||
<button class="err-btn err-btn--primary" onclick={() => onBypass?.()}>Enter app</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if authRequired && ringFull}
|
||||
<div class="error-box anim-fade-up">
|
||||
<p class="error-label">Waiting for login</p>
|
||||
<div class="error-actions">
|
||||
<button class="err-btn" onclick={() => { onSkip?.() }}>Skip</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="status-text">{ringFull ? '' : `Initializing server${dots}`}</p>
|
||||
{/if}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
let libraryManga: Manga[] = $state([])
|
||||
|
||||
let ctrl: AbortController | null = null
|
||||
let ctrl: AbortController | null = null
|
||||
let statusPollTimer: ReturnType<typeof setTimeout> | 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}
|
||||
/>
|
||||
|
||||
<div class="content">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
ArrowsClockwise, BookOpen, CircleNotch,
|
||||
MagnifyingGlass, NewspaperClipping, Trash,
|
||||
MagnifyingGlass, NewspaperClipping, Trash, X,
|
||||
} from 'phosphor-svelte'
|
||||
|
||||
interface Props {
|
||||
@@ -11,18 +11,20 @@
|
||||
historyConfirmClear: boolean
|
||||
hasHistory: boolean
|
||||
updatesLoading: boolean
|
||||
updaterRunning: boolean
|
||||
onTabChange: (tab: 'updates' | 'history') => void
|
||||
onHistorySearchChange: (v: string) => void
|
||||
onUpdatesSearchChange: (v: string) => void
|
||||
onHistoryClear: () => void
|
||||
onRefreshUpdates: () => void
|
||||
onToggleUpdate: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
tab, historySearch, updatesSearch, historyConfirmClear, hasHistory,
|
||||
updatesLoading,
|
||||
updatesLoading, updaterRunning,
|
||||
onTabChange, onHistorySearchChange, onUpdatesSearchChange,
|
||||
onHistoryClear, onRefreshUpdates,
|
||||
onHistoryClear, onRefreshUpdates, onToggleUpdate,
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
@@ -57,12 +59,15 @@
|
||||
|
||||
<button
|
||||
class="icon-btn"
|
||||
onclick={onRefreshUpdates}
|
||||
disabled={updatesLoading}
|
||||
title="Reload update list"
|
||||
class:running={updaterRunning}
|
||||
onclick={updaterRunning ? onToggleUpdate : onRefreshUpdates}
|
||||
disabled={updatesLoading && !updaterRunning}
|
||||
title={updaterRunning ? 'Stop library update' : 'Run library update'}
|
||||
>
|
||||
{#if updatesLoading}
|
||||
{#if updatesLoading && !updaterRunning}
|
||||
<CircleNotch size={14} weight="light" class="anim-spin" />
|
||||
{:else if updaterRunning}
|
||||
<X size={14} weight="bold" />
|
||||
{:else}
|
||||
<ArrowsClockwise size={14} weight="bold" />
|
||||
{/if}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { settingsState, updateSettings } from '$lib/state/settings.svelte'
|
||||
import { eventToKeybind } from '$lib/core/keybinds/keybindEngine'
|
||||
import type { Keybinds } from '$lib/core/keybinds/defaultBinds'
|
||||
import { anchorToModal } from '$lib/core/ui/selectPortal'
|
||||
|
||||
import GeneralSettings from './sections/GeneralSettings.svelte'
|
||||
import AppearanceSettings from './sections/AppearanceSettings.svelte'
|
||||
@@ -49,6 +50,7 @@
|
||||
let tabSlideDir = $state<'up'|'down'>('down')
|
||||
let tabIconKey = $state(0)
|
||||
let contentBodyEl: HTMLDivElement
|
||||
let modalEl: HTMLDivElement
|
||||
let bugReporterOpen = $state(false)
|
||||
|
||||
$effect(() => { tab; tick().then(() => contentBodyEl?.scrollTo({ top: 0 })) })
|
||||
@@ -89,6 +91,7 @@
|
||||
let selectOpen: string | null = $state(null)
|
||||
let closingSelect: string | null = $state(null)
|
||||
const CLOSE_ANIM_MS = 120
|
||||
const selectTriggers = new Map<string, HTMLElement>()
|
||||
|
||||
function closeSelect() {
|
||||
if (!selectOpen) return
|
||||
@@ -102,6 +105,14 @@
|
||||
else { closingSelect = null; selectOpen = id }
|
||||
}
|
||||
|
||||
function registerTrigger(id: string, el: HTMLElement) {
|
||||
selectTriggers.set(id, el)
|
||||
}
|
||||
|
||||
function getTrigger(id: string): HTMLElement | undefined {
|
||||
return selectTriggers.get(id)
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (!selectOpen) return
|
||||
@@ -118,7 +129,7 @@
|
||||
<div class="s-backdrop" role="presentation" tabindex="-1"
|
||||
onclick={(e) => { if (e.target === e.currentTarget) close() }}
|
||||
onkeydown={(e) => { if (e.key === 'Escape') { e.stopPropagation(); close() } }}>
|
||||
<div class="s-modal" role="dialog" aria-label="Settings">
|
||||
<div class="s-modal" role="dialog" aria-label="Settings" bind:this={modalEl}>
|
||||
|
||||
<div class="s-sidebar">
|
||||
<p class="s-sidebar-title">Settings</p>
|
||||
@@ -164,13 +175,13 @@
|
||||
|
||||
<div class="s-content-body" bind:this={contentBodyEl}>
|
||||
{#if tab === 'general'}
|
||||
<GeneralSettings {selectOpen} {closingSelect} {toggleSelect} {anims} />
|
||||
<GeneralSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} {anims} />
|
||||
{:else if tab === 'appearance'}
|
||||
<AppearanceSettings {selectOpen} {closingSelect} {toggleSelect} {anims} {onOpenThemeEditor} />
|
||||
<AppearanceSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} {anims} {onOpenThemeEditor} />
|
||||
{:else if tab === 'reader'}
|
||||
<ReaderSettings {selectOpen} {closingSelect} {toggleSelect} {anims} />
|
||||
<ReaderSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} {anims} />
|
||||
{:else if tab === 'library'}
|
||||
<LibrarySettings {selectOpen} {closingSelect} {toggleSelect} {anims} />
|
||||
<LibrarySettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} {anims} />
|
||||
{:else if tab === 'automation'}
|
||||
<AutomationSettings />
|
||||
{:else if tab === 'performance'}
|
||||
@@ -178,13 +189,13 @@
|
||||
{:else if tab === 'keybinds'}
|
||||
<KeybindsSettings bind:listeningKey />
|
||||
{:else if tab === 'storage'}
|
||||
<StorageSettings {selectOpen} {closingSelect} {toggleSelect} />
|
||||
<StorageSettings {selectOpen} {closingSelect} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} />
|
||||
{:else if tab === 'folders'}
|
||||
<FoldersSettings />
|
||||
{:else if tab === 'tracking'}
|
||||
<TrackingSettings />
|
||||
{:else if tab === 'security'}
|
||||
<SecuritySettings {selectOpen} {toggleSelect} />
|
||||
<SecuritySettings {selectOpen} {toggleSelect} {registerTrigger} {getTrigger} {anchorToModal} {modalEl} />
|
||||
{:else if tab === 'content'}
|
||||
<ContentSettings />
|
||||
{:else if tab === 'about'}
|
||||
|
||||
@@ -4,15 +4,22 @@
|
||||
|
||||
const isTauri = platformService.platform === 'tauri'
|
||||
|
||||
import { selectPortal as _defaultPortal } from '$lib/core/ui/selectPortal'
|
||||
import type { Action } from 'svelte/action'
|
||||
|
||||
interface Props {
|
||||
selectOpen: string | null
|
||||
closingSelect: string | null
|
||||
toggleSelect: (id: string) => void
|
||||
anims: boolean
|
||||
selectOpen: string | null
|
||||
closingSelect: string | null
|
||||
toggleSelect: (id: string) => void
|
||||
registerTrigger: (id: string, el: HTMLElement) => void
|
||||
getTrigger: (id: string) => HTMLElement | undefined
|
||||
selectPortal: Action<HTMLElement, HTMLElement | undefined>
|
||||
anims: boolean
|
||||
}
|
||||
let { selectOpen, closingSelect, toggleSelect, anims }: Props = $props()
|
||||
let { selectOpen, closingSelect, toggleSelect, registerTrigger, getTrigger, selectPortal, anims }: Props = $props()
|
||||
|
||||
let triggerIdleTimeout = $state<HTMLButtonElement>(null!)
|
||||
$effect(() => { if (triggerIdleTimeout) registerTrigger('idle-timeout', triggerIdleTimeout) })
|
||||
let serverAdvancedOpen = $state(false)
|
||||
|
||||
async function pickServerBinary() {
|
||||
@@ -138,7 +145,7 @@
|
||||
<svg class="s-select-caret" class:open={selectOpen === 'idle-timeout'} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
|
||||
</button>
|
||||
{#if selectOpen === 'idle-timeout' || closingSelect === 'idle-timeout'}
|
||||
<div class="s-select-menu" class:anims class:closing={closingSelect === 'idle-timeout'}>
|
||||
<div use:selectPortal={getTrigger('idle-timeout')} class="s-select-menu" class:anims class:closing={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]}
|
||||
<button class="s-select-option" class:active={String(settingsState.settings.idleTimeoutMin ?? 5) === v} onclick={() => { updateSettings({ idleTimeoutMin: Number(v) }); toggleSelect('idle-timeout') }}>{l}</button>
|
||||
{/each}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { settingsState } from "$lib/state/settings.svelte";
|
||||
import { getBlobUrl } from "$lib/core/cache/imageCache";
|
||||
import { appState } from "$lib/state/app.svelte";
|
||||
|
||||
|
||||
|
||||
let {
|
||||
src,
|
||||
@@ -17,8 +20,8 @@
|
||||
id?: string | number;
|
||||
alt?: string;
|
||||
class?: string;
|
||||
loading?: string;
|
||||
decoding?: string;
|
||||
loading?: "lazy" | "eager";
|
||||
decoding?: "async" | "auto" | "sync";
|
||||
priority?: number;
|
||||
onerror?: ((e: Event) => void) | undefined;
|
||||
[key: string]: any;
|
||||
@@ -39,7 +42,7 @@
|
||||
return withBust(base);
|
||||
}
|
||||
|
||||
const isAuth = $derived((settingsState.settings.serverAuthMode ?? "NONE") !== "NONE");
|
||||
const isAuth = $derived(appState.authMode !== "NONE");
|
||||
|
||||
let blobUrl = $state("");
|
||||
let reqId = 0;
|
||||
@@ -59,7 +62,7 @@
|
||||
});
|
||||
|
||||
const plainUrl = $derived(plainThumbUrl(src));
|
||||
const resolved = $derived(isAuth ? (blobUrl || plainUrl) || undefined : plainUrl || undefined);
|
||||
const resolved = $derived(isAuth ? (blobUrl || undefined) : (plainUrl || undefined));
|
||||
</script>
|
||||
|
||||
<img src={resolved} {alt} class={cls} {loading} {decoding} {onerror} {...rest} />
|
||||
Reference in New Issue
Block a user