mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Fix: WebUI Auth & Tauri Auth
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { settingsState, updateSettings } from '$lib/state/settings.svelte'
|
||||
import { requestManager } from '$lib/request-manager'
|
||||
import { authSession, loginUI } from '$lib/core/auth'
|
||||
import { retryBoot } from '$lib/state/boot.svelte'
|
||||
import { authSession, configureAuth } from '$lib/core/auth'
|
||||
|
||||
interface Props { selectOpen: string | null; toggleSelect: (id: string) => void }
|
||||
let { selectOpen, toggleSelect }: Props = $props()
|
||||
@@ -13,9 +14,15 @@
|
||||
let secSaved = $state<string | null>(null)
|
||||
let secLoaded = $state(false)
|
||||
|
||||
let authMode = $state(settingsState.settings.serverAuthMode ?? 'NONE')
|
||||
function normalizeForUI(mode: string | undefined): 'NONE' | 'BASIC_AUTH' | 'UI_LOGIN' {
|
||||
if (mode === 'BASIC_AUTH' || mode === 'UI_LOGIN') return mode
|
||||
return 'NONE'
|
||||
}
|
||||
|
||||
let authMode = $state(normalizeForUI(settingsState.settings.serverAuthMode))
|
||||
let authUsername = $state(settingsState.settings.serverAuthUser ?? '')
|
||||
let authPassword = $state('')
|
||||
let authDirty = $state(false)
|
||||
|
||||
let socksEnabled = $state(settingsState.settings.socksProxyEnabled ?? false)
|
||||
let socksHost = $state(settingsState.settings.socksProxyHost ?? '')
|
||||
@@ -60,28 +67,23 @@
|
||||
setTimeout(() => lockSaved = false, 2000)
|
||||
}
|
||||
|
||||
function normalizeAuthMode(mode: string): 'NONE' | 'BASIC_AUTH' | 'UI_LOGIN' {
|
||||
if (mode === 'BASIC_AUTH' || mode === 'UI_LOGIN' || mode === 'NONE') return mode
|
||||
return 'NONE'
|
||||
}
|
||||
|
||||
function showSaved(key: string) {
|
||||
secSaved = key; secError = null
|
||||
setTimeout(() => { if (secSaved === key) secSaved = null }, 2000)
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!secLoaded) { secLoaded = true; authSession.clearTokens(); loadServerSecurity() }
|
||||
if (!secLoaded) { secLoaded = true; loadServerSecurity() }
|
||||
})
|
||||
|
||||
async function loadServerSecurity() {
|
||||
try {
|
||||
const s = await requestManager.extensions.getServerSecurity()
|
||||
const serverMode = normalizeAuthMode(s.authMode)
|
||||
if (serverMode !== 'UI_LOGIN') authSession.clearTokens()
|
||||
authMode = serverMode
|
||||
authUsername = s.authUsername || ''
|
||||
updateSettings({ serverAuthMode: serverMode, serverAuthUser: authUsername })
|
||||
if (!authDirty) {
|
||||
authMode = normalizeForUI(s.authMode)
|
||||
authUsername = s.authUsername || ''
|
||||
updateSettings({ serverAuthMode: authMode, serverAuthUser: authUsername })
|
||||
}
|
||||
socksEnabled = s.socksProxyEnabled; socksHost = s.socksProxyHost
|
||||
socksPort = s.socksProxyPort; socksVersion = s.socksProxyVersion
|
||||
socksUsername = s.socksProxyUsername
|
||||
@@ -95,37 +97,28 @@
|
||||
flareSolverrTimeout: flareTimeout, flareSolverrSessionName: flareSession,
|
||||
flareSolverrSessionTtl: flareTtl, flareSolverrAsResponseFallback: flareFallback,
|
||||
})
|
||||
} catch {}
|
||||
} catch (e: any) {
|
||||
console.warn('[SecuritySettings] loadServerSecurity failed:', e?.message ?? e)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAuth() {
|
||||
if (authMode === 'NONE') { await clearAuth(); return }
|
||||
if (!authUsername.trim() || !authPassword.trim()) { secError = 'Username and password are required'; return }
|
||||
secLoading = true; secError = null
|
||||
const prev = { mode: settingsState.settings.serverAuthMode, user: settingsState.settings.serverAuthUser, pass: settingsState.settings.serverAuthPass }
|
||||
try {
|
||||
const newUser = authUsername.trim()
|
||||
const newPass = authPassword.trim()
|
||||
authSession.clearTokens()
|
||||
if (authMode === 'UI_LOGIN') {
|
||||
await loginUI(newUser, newPass)
|
||||
updateSettings({ serverAuthMode: 'UI_LOGIN', serverAuthUser: newUser, serverAuthPass: '' })
|
||||
} else {
|
||||
updateSettings({ serverAuthMode: 'BASIC_AUTH', serverAuthUser: newUser, serverAuthPass: newPass })
|
||||
}
|
||||
await requestManager.extensions.setServerAuth({ authMode, authUsername: newUser, authPassword: newPass })
|
||||
authSession.clearTokens()
|
||||
updateSettings({ serverAuthMode: authMode as any, serverAuthUser: newUser, serverAuthPass: authMode === 'BASIC_AUTH' ? newPass : '' })
|
||||
configureAuth(settingsState.settings.serverUrl ?? '', authMode as any, newUser, authMode === 'BASIC_AUTH' ? newPass : undefined)
|
||||
authPassword = ''
|
||||
authDirty = false
|
||||
showSaved('auth')
|
||||
retryBoot(authMode as any, newUser, newPass)
|
||||
} catch (e: any) {
|
||||
const msg = e?.message ?? 'Failed to save authentication settings'
|
||||
const authMismatch = /unauthorized|unauthenticated|authentication|401/i.test(msg)
|
||||
if (!authMismatch) {
|
||||
authSession.clearTokens()
|
||||
updateSettings({ serverAuthMode: prev.mode, serverAuthUser: prev.user, serverAuthPass: prev.pass })
|
||||
}
|
||||
secError = authMismatch
|
||||
? 'Saved local auth settings, but the server rejected the update. Verify your new credentials with the current server configuration.'
|
||||
: msg
|
||||
secError = e?.message ?? 'Failed to save authentication settings'
|
||||
} finally { secLoading = false }
|
||||
}
|
||||
|
||||
@@ -134,9 +127,11 @@
|
||||
const prev = { mode: settingsState.settings.serverAuthMode, user: settingsState.settings.serverAuthUser, pass: settingsState.settings.serverAuthPass }
|
||||
try {
|
||||
await requestManager.extensions.setServerAuth({ authMode: 'NONE', authUsername: '', authPassword: '' })
|
||||
authSession.clearTokens()
|
||||
configureAuth(settingsState.settings.serverUrl ?? '', 'NONE')
|
||||
updateSettings({ serverAuthMode: 'NONE', serverAuthUser: '', serverAuthPass: '' })
|
||||
authMode = 'NONE'; authUsername = ''; authPassword = ''
|
||||
authSession.clearTokens(); showSaved('auth')
|
||||
authMode = 'NONE'; authUsername = ''; authPassword = ''; authDirty = false
|
||||
showSaved('auth')
|
||||
} catch (e: any) {
|
||||
updateSettings({ serverAuthMode: prev.mode, serverAuthUser: prev.user, serverAuthPass: prev.pass })
|
||||
secError = e?.message ?? 'Failed to disable authentication'
|
||||
@@ -186,6 +181,7 @@
|
||||
authMode = 'NONE'
|
||||
authUsername = ''
|
||||
authPassword = ''
|
||||
authDirty = false
|
||||
updateSettings({ serverAuthMode: 'NONE', serverAuthUser: '', serverAuthPass: '' })
|
||||
showSaved('auth')
|
||||
}
|
||||
@@ -214,23 +210,28 @@
|
||||
<div class="s-segment">
|
||||
{#each [{ value: 'NONE', label: 'None' }, { value: 'BASIC_AUTH', label: 'Basic' }, { value: 'UI_LOGIN', label: 'UI Login' }] as opt}
|
||||
<button class="s-segment-btn" class:active={authMode === opt.value}
|
||||
onclick={() => authMode = opt.value as any} disabled={secLoading}>{opt.label}</button>
|
||||
onclick={() => { authMode = opt.value as any; authPassword = ''; authDirty = true }} disabled={secLoading}>{opt.label}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{#if authMode !== 'NONE'}
|
||||
<div class="s-row">
|
||||
<div class="s-row-info"><span class="s-label">Username</span></div>
|
||||
<input class="s-input" bind:value={authUsername} placeholder="Username" autocomplete="off" spellcheck="false" disabled={secLoading} />
|
||||
<input class="s-input" bind:value={authUsername} oninput={() => authDirty = true} placeholder="Username" autocomplete="off" spellcheck="false" disabled={secLoading} />
|
||||
</div>
|
||||
<div class="s-row">
|
||||
<div class="s-row-info"><span class="s-label">Password</span></div>
|
||||
<div class="s-field-wrap">
|
||||
<input class="s-input" type={showAuthPass ? 'text' : 'password'} bind:value={authPassword} placeholder="Password" autocomplete="off" spellcheck="false" disabled={secLoading} />
|
||||
<input class="s-input" type={showAuthPass ? 'text' : 'password'} bind:value={authPassword} oninput={() => authDirty = true} placeholder="Password" autocomplete="off" spellcheck="false" disabled={secLoading} />
|
||||
<button class="s-eye-btn" onclick={() => showAuthPass = !showAuthPass} tabindex="-1" aria-label={showAuthPass ? 'Hide password' : 'Show password'}>{@html showAuthPass ? EyeClose : EyeOpen}</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if authMode !== 'NONE' && settingsState.settings.serverAuthMode === authMode && !authPassword}
|
||||
<div class="s-row">
|
||||
<span class="s-desc" style="color: var(--text-muted)">Re-enter your password to update credentials.</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if settingsState.settings.serverAuthMode === 'BASIC_AUTH'}
|
||||
<div class="s-row">
|
||||
<span class="s-desc">Images are proxied through Tauri when Basic Auth is active, which reduces loading speed.</span>
|
||||
@@ -250,8 +251,16 @@
|
||||
</button>
|
||||
{/if}
|
||||
<button class="s-btn s-btn-accent" onclick={saveAuth}
|
||||
disabled={secLoading || ((authMode === 'BASIC_AUTH' || authMode === 'UI_LOGIN') && (!authUsername.trim() || !authPassword.trim()))}>
|
||||
{secLoading ? 'Saving…' : secSaved === 'auth' ? 'Saved ✓' : settingsState.settings.serverAuthMode === 'BASIC_AUTH' ? 'Update' : authMode === 'NONE' ? 'Save' : 'Enable'}
|
||||
disabled={secLoading || (authMode !== 'NONE' && (!authUsername.trim() || !authPassword.trim()))}>
|
||||
{#if secLoading}
|
||||
Saving…
|
||||
{:else if secSaved === 'auth'}
|
||||
Saved ✓
|
||||
{:else if authMode === 'NONE'}
|
||||
Save
|
||||
{:else}
|
||||
{authDirty ? 'Enable' : 'Save'}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+59
-74
@@ -1,4 +1,5 @@
|
||||
const DEFAULT_URL = 'http://127.0.0.1:4567'
|
||||
const SKEW_MS = 60_000 * 2
|
||||
|
||||
interface AuthConfig {
|
||||
baseUrl: string
|
||||
@@ -10,74 +11,61 @@ interface AuthConfig {
|
||||
export interface UiAuthDebugStatus {
|
||||
mode: 'NONE' | 'BASIC_AUTH' | 'UI_LOGIN'
|
||||
hasSession: boolean
|
||||
hasRefreshToken: boolean
|
||||
accessExpiresAt: number | null
|
||||
refreshExpiresAt: number | null
|
||||
accessExpiresInMs: number | null
|
||||
refreshExpiresInMs: number | null
|
||||
shouldRefreshSoon: boolean
|
||||
refreshInFlight: boolean
|
||||
skewMs: number
|
||||
}
|
||||
|
||||
const SKEW_MS = 60_000 * 2
|
||||
|
||||
let config: AuthConfig = { baseUrl: DEFAULT_URL, mode: 'NONE' }
|
||||
|
||||
let accessToken: string | null = null
|
||||
let refreshToken: string | null = null
|
||||
let accessExpiresAt: number | null = null
|
||||
let refreshExpiresAt: number | null = null
|
||||
let refreshInFlight = false
|
||||
let accessToken: string | null = null
|
||||
let refreshToken: string | null = null
|
||||
let accessExpiresAt: number | null = null
|
||||
let refreshInFlight = false
|
||||
|
||||
function parseExpiry(token: string): number | null {
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]))
|
||||
return typeof payload.exp === 'number' ? payload.exp * 1000 : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
} catch { return null }
|
||||
}
|
||||
|
||||
export const authSession = {
|
||||
clearTokens() {
|
||||
accessToken = null
|
||||
refreshToken = null
|
||||
accessExpiresAt = null
|
||||
refreshExpiresAt = null
|
||||
accessToken = null
|
||||
refreshToken = null
|
||||
accessExpiresAt = null
|
||||
},
|
||||
}
|
||||
|
||||
export function getUIAccessToken(): string | null {
|
||||
return accessToken
|
||||
}
|
||||
export function getUIAccessToken(): string | null { return accessToken }
|
||||
|
||||
export function getUiAuthDebugStatus(): UiAuthDebugStatus {
|
||||
const now = Date.now()
|
||||
const accessExpiresInMs = accessExpiresAt !== null ? accessExpiresAt - now : null
|
||||
const refreshExpiresInMs = refreshExpiresAt !== null ? refreshExpiresAt - now : null
|
||||
const now = Date.now()
|
||||
const accessExpiresInMs = accessExpiresAt !== null ? accessExpiresAt - now : null
|
||||
return {
|
||||
mode: config.mode,
|
||||
hasSession: accessToken !== null,
|
||||
hasRefreshToken: refreshToken !== null,
|
||||
mode: config.mode,
|
||||
hasSession: accessToken !== null,
|
||||
accessExpiresAt,
|
||||
refreshExpiresAt,
|
||||
accessExpiresInMs,
|
||||
refreshExpiresInMs,
|
||||
shouldRefreshSoon: accessExpiresInMs !== null && accessExpiresInMs < SKEW_MS,
|
||||
shouldRefreshSoon: accessExpiresInMs !== null && accessExpiresInMs < SKEW_MS,
|
||||
refreshInFlight,
|
||||
skewMs: SKEW_MS,
|
||||
skewMs: SKEW_MS,
|
||||
}
|
||||
}
|
||||
|
||||
export function configureAuth(
|
||||
baseUrl: string,
|
||||
mode: 'NONE' | 'BASIC_AUTH' | 'UI_LOGIN',
|
||||
user?: string,
|
||||
pass?: string,
|
||||
mode: 'NONE' | 'BASIC_AUTH' | 'UI_LOGIN',
|
||||
user?: string,
|
||||
pass?: string,
|
||||
): void {
|
||||
config = { baseUrl: baseUrl.replace(/\/$/, ''), mode, user, pass }
|
||||
authSession.clearTokens()
|
||||
config = { baseUrl: baseUrl.replace(/\/$/, ''), mode, user, pass }
|
||||
accessToken = null
|
||||
refreshToken = null
|
||||
accessExpiresAt = null
|
||||
}
|
||||
|
||||
export function authHeaders(): Record<string, string> {
|
||||
@@ -90,16 +78,18 @@ export function authHeaders(): Record<string, string> {
|
||||
return {}
|
||||
}
|
||||
|
||||
async function gqlRaw(query: string, variables?: Record<string, unknown>): Promise<unknown> {
|
||||
async function gql<T>(query: string, variables?: Record<string, unknown>, bare = false): Promise<T> {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (!bare) Object.assign(headers, authHeaders())
|
||||
const res = await fetch(`${config.baseUrl}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
||||
headers,
|
||||
body: JSON.stringify({ query, variables }),
|
||||
})
|
||||
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)
|
||||
return json.data
|
||||
return json.data as T
|
||||
}
|
||||
|
||||
export async function probeServer(): Promise<'ok' | 'auth_required' | 'unreachable'> {
|
||||
@@ -107,7 +97,7 @@ export async function probeServer(): Promise<'ok' | 'auth_required' | 'unreachab
|
||||
const res = await fetch(`${config.baseUrl}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
||||
body: JSON.stringify({ query: '{ aboutServer { name } }' }),
|
||||
body: JSON.stringify({ query: '{ settings { authMode } }' }),
|
||||
})
|
||||
if (res.status === 401 || res.status === 403) return 'auth_required'
|
||||
if (!res.ok) return 'unreachable'
|
||||
@@ -116,17 +106,7 @@ export async function probeServer(): Promise<'ok' | 'auth_required' | 'unreachab
|
||||
/unauthorized|unauthenticated/i.test(e.message)
|
||||
)
|
||||
return isAuthError ? 'auth_required' : 'ok'
|
||||
} catch {
|
||||
return 'unreachable'
|
||||
}
|
||||
}
|
||||
|
||||
export async function loginBasic(user: string, pass: string): Promise<void> {
|
||||
config.user = user
|
||||
config.pass = pass
|
||||
config.mode = 'BASIC_AUTH'
|
||||
const probe = await probeServer()
|
||||
if (probe !== 'ok') throw new Error('Invalid credentials')
|
||||
} catch { return 'unreachable' }
|
||||
}
|
||||
|
||||
const LOGIN_MUTATION = `
|
||||
@@ -145,30 +125,29 @@ const REFRESH_MUTATION = `
|
||||
}
|
||||
`
|
||||
|
||||
export async function loginUI(user: string, pass: string): Promise<void> {
|
||||
const data = await gqlRaw(LOGIN_MUTATION, { username: user, password: pass }) as {
|
||||
login: { accessToken: string; refreshToken: string }
|
||||
export async function loginBasic(user: string, pass: string): Promise<void> {
|
||||
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') {
|
||||
config.user = prev.user
|
||||
config.pass = prev.pass
|
||||
config.mode = prev.mode as typeof config.mode
|
||||
throw new Error('Invalid credentials')
|
||||
}
|
||||
accessToken = data.login.accessToken
|
||||
refreshToken = data.login.refreshToken
|
||||
accessExpiresAt = parseExpiry(accessToken)
|
||||
refreshExpiresAt = parseExpiry(refreshToken)
|
||||
config.mode = 'UI_LOGIN'
|
||||
config.user = user
|
||||
}
|
||||
|
||||
export async function refreshAccessToken(): Promise<boolean> {
|
||||
if (!refreshToken) return false
|
||||
try {
|
||||
const data = await gqlRaw(REFRESH_MUTATION, { refreshToken }) as {
|
||||
refreshToken: { accessToken: string }
|
||||
}
|
||||
accessToken = data.refreshToken.accessToken
|
||||
accessExpiresAt = parseExpiry(accessToken)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
export async function loginUI(user: string, pass: string): Promise<void> {
|
||||
const data = await gql<{ login: { accessToken: string; refreshToken: string } }>(
|
||||
LOGIN_MUTATION, { username: user, password: pass }, true
|
||||
)
|
||||
accessToken = data.login.accessToken
|
||||
refreshToken = data.login.refreshToken
|
||||
accessExpiresAt = parseExpiry(accessToken)
|
||||
config.mode = 'UI_LOGIN'
|
||||
config.user = user
|
||||
}
|
||||
|
||||
export async function refreshUiAccessToken(force = false): Promise<string | null> {
|
||||
@@ -179,9 +158,15 @@ export async function refreshUiAccessToken(force = false): Promise<string | null
|
||||
if (refreshInFlight) return accessToken
|
||||
refreshInFlight = true
|
||||
try {
|
||||
const ok = await refreshAccessToken()
|
||||
return ok ? accessToken : null
|
||||
const data = await gql<{ refreshToken: { accessToken: string } }>(
|
||||
REFRESH_MUTATION, { refreshToken }
|
||||
)
|
||||
accessToken = data.refreshToken.accessToken
|
||||
accessExpiresAt = parseExpiry(accessToken)
|
||||
return accessToken
|
||||
} catch {
|
||||
return null
|
||||
} finally {
|
||||
refreshInFlight = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+8
-10
@@ -1,6 +1,6 @@
|
||||
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
|
||||
import { settingsState } from "$lib/state/settings.svelte";
|
||||
import { getUIAccessToken } from "$lib/core/auth";
|
||||
import { platformService } from "$lib/platform-service";
|
||||
import { settingsState } from "$lib/state/settings.svelte";
|
||||
import { getUIAccessToken } from "$lib/core/auth";
|
||||
|
||||
const cache = new Map<string, string>();
|
||||
const inflight = new Map<string, Promise<string>>();
|
||||
@@ -19,14 +19,14 @@ interface QueueEntry {
|
||||
const queue: QueueEntry[] = [];
|
||||
|
||||
async function getAuthHeaders(): Promise<Record<string, string>> {
|
||||
const mode = settingsState.serverAuthMode ?? "NONE";
|
||||
const mode = settingsState.settings.serverAuthMode ?? "NONE";
|
||||
if (mode === "UI_LOGIN") {
|
||||
const token = await getUIAccessToken();
|
||||
const token = getUIAccessToken();
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
}
|
||||
if (mode === "BASIC_AUTH") {
|
||||
const user = settingsState.serverAuthUser?.trim() ?? "";
|
||||
const pass = settingsState.serverAuthPass?.trim() ?? "";
|
||||
const user = settingsState.settings.serverAuthUser?.trim() ?? "";
|
||||
const pass = settingsState.settings.serverAuthPass?.trim() ?? "";
|
||||
return user && pass ? { Authorization: `Basic ${btoa(`${user}:${pass}`)}` } : {};
|
||||
}
|
||||
return {};
|
||||
@@ -34,9 +34,7 @@ async function getAuthHeaders(): Promise<Record<string, string>> {
|
||||
|
||||
async function doFetch(url: string): Promise<string> {
|
||||
const headers = await getAuthHeaders();
|
||||
const res = await tauriFetch(url, { method: "GET", headers });
|
||||
if (!res.ok) throw new Error(`${res.status}`);
|
||||
const blob = await res.blob();
|
||||
const blob = await platformService.fetchImage(url, headers);
|
||||
if (clearing) throw new DOMException("Cancelled", "AbortError");
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
cache.set(url, blobUrl);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getCurrentWindow } from '@tauri-apps/api
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { readFile, writeFile } from '@tauri-apps/plugin-fs'
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
|
||||
import { open as openUrl } from '@tauri-apps/plugin-shell'
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
import { connect, disconnect, setActivity, clearActivity } from 'tauri-plugin-discord-rpc-api'
|
||||
@@ -116,6 +117,12 @@ export class TauriAdapter implements PlatformAdapter {
|
||||
return invoke('get_auto_backup_dir')
|
||||
}
|
||||
|
||||
async fetchImage(url: string, headers: Record<string, string>): Promise<Blob> {
|
||||
const res = await tauriFetch(url, { method: 'GET', headers })
|
||||
if (!res.ok) throw new Error(`${res.status}`)
|
||||
return res.blob()
|
||||
}
|
||||
|
||||
async launchServer(config: ServerLaunchConfig): Promise<void> {
|
||||
await invoke('spawn_server', {
|
||||
binary: config.binary ?? '',
|
||||
|
||||
@@ -93,6 +93,8 @@ export interface PlatformAdapter {
|
||||
migrateDownloads(src: string, dst: string): Promise<void>
|
||||
getAutoBackupDir(): Promise<string>
|
||||
|
||||
fetchImage(url: string, headers: Record<string, string>): Promise<Blob>
|
||||
|
||||
launchServer(config: ServerLaunchConfig): Promise<void>
|
||||
stopServer(): Promise<void>
|
||||
getServerStatus(): Promise<'running' | 'stopped' | 'error'>
|
||||
|
||||
@@ -61,6 +61,12 @@ export class WebAdapter implements PlatformAdapter {
|
||||
async migrateDownloads(_src: string, _dst: string): Promise<void> {}
|
||||
async getAutoBackupDir(): Promise<string> { return '' }
|
||||
|
||||
async fetchImage(url: string, headers: Record<string, string>): Promise<Blob> {
|
||||
const res = await fetch(url, { method: 'GET', headers })
|
||||
if (!res.ok) throw new Error(`${res.status}`)
|
||||
return res.blob()
|
||||
}
|
||||
|
||||
async launchServer(_config: ServerLaunchConfig): Promise<void> {}
|
||||
async stopServer(): Promise<void> {}
|
||||
async getServerStatus(): Promise<'running' | 'stopped' | 'error'> { return 'stopped' }
|
||||
|
||||
@@ -42,6 +42,8 @@ export const platformService = {
|
||||
migrateDownloads:(src: string, dst: string) => get().migrateDownloads(src, dst),
|
||||
getAutoBackupDir:() => get().getAutoBackupDir(),
|
||||
|
||||
fetchImage: (url: string, headers: Record<string, string>) => get().fetchImage(url, headers),
|
||||
|
||||
launchServer: (c: ServerLaunchConfig) => get().launchServer(c),
|
||||
stopServer: () => get().stopServer(),
|
||||
getServerStatus: () => get().getServerStatus(),
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
UPDATE_STOP,
|
||||
SET_MANGA_META,
|
||||
DELETE_MANGA_META,
|
||||
CREATE_BACKUP,
|
||||
FETCH_SOURCE_MANGA,
|
||||
LIBRARY_UPDATE_STATUS,
|
||||
MANGAS_BY_GENRE,
|
||||
@@ -114,8 +115,8 @@ import {
|
||||
SET_FLARE_SOLVERR,
|
||||
RESTORE_BACKUP,
|
||||
VALIDATE_BACKUP,
|
||||
CREATE_BACKUP,
|
||||
} from './meta'
|
||||
import { authHeaders } from '$lib/core/auth'
|
||||
import {
|
||||
type GQLResponse,
|
||||
mapManga,
|
||||
@@ -141,15 +142,10 @@ function mapDownloadStatus(raw: { state: string; queue: RawQueueItem[] }): Downl
|
||||
}
|
||||
|
||||
export class SuwayomiAdapter implements ServerAdapter {
|
||||
private baseUrl = 'http://127.0.0.1:4567'
|
||||
private authHeader: string | null = null
|
||||
private baseUrl = 'http://127.0.0.1:4567'
|
||||
|
||||
async connect(config: ServerConfig): Promise<void> {
|
||||
this.baseUrl = config.baseUrl.replace(/\/$/, '')
|
||||
if (config.credentials) {
|
||||
const { username, password } = config.credentials
|
||||
this.authHeader = 'Basic ' + btoa(`${username}:${password}`)
|
||||
}
|
||||
initPageCache(this.gql.bind(this), this.getServerUrl.bind(this))
|
||||
}
|
||||
|
||||
@@ -160,9 +156,9 @@ export class SuwayomiAdapter implements ServerAdapter {
|
||||
async getStatus(): Promise<ServerStatus> {
|
||||
try {
|
||||
const res = await fetch(`${this.baseUrl}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ query: '{ aboutServer { name } }' }),
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
||||
body: JSON.stringify({ query: '{ aboutServer { name } }' }),
|
||||
})
|
||||
return res.ok ? 'connected' : 'error'
|
||||
} catch {
|
||||
@@ -171,9 +167,7 @@ export class SuwayomiAdapter implements ServerAdapter {
|
||||
}
|
||||
|
||||
private headers(): Record<string, string> {
|
||||
const h: Record<string, string> = { 'Content-Type': 'application/json' }
|
||||
if (this.authHeader) h['Authorization'] = this.authHeader
|
||||
return h
|
||||
return { 'Content-Type': 'application/json', ...authHeaders() }
|
||||
}
|
||||
|
||||
private async gql<T>(
|
||||
@@ -182,9 +176,9 @@ export class SuwayomiAdapter implements ServerAdapter {
|
||||
signal?: AbortSignal,
|
||||
): Promise<T> {
|
||||
const res = await fetch(`${this.baseUrl}/api/graphql`, {
|
||||
method: 'POST',
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ query, variables }),
|
||||
body: JSON.stringify({ query, variables }),
|
||||
signal,
|
||||
})
|
||||
if (!res.ok) throw new Error(`Suwayomi HTTP ${res.status}`)
|
||||
@@ -642,8 +636,7 @@ export class SuwayomiAdapter implements ServerAdapter {
|
||||
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<string, string> = { Accept: 'application/json' }
|
||||
if (this.authHeader) headers['Authorization'] = this.authHeader
|
||||
const headers: Record<string, string> = { 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<T>) => { if (json.errors?.length) throw new Error(json.errors[0].message); return json.data })
|
||||
|
||||
@@ -1,237 +1,126 @@
|
||||
import type { DownloadStatus } from '$lib/types/api'
|
||||
import type { Manga, Chapter, Extension, Source, Tracker, TrackRecord, Category } from '$lib/types'
|
||||
export type PlatformFeature =
|
||||
| 'server-management'
|
||||
| 'biometric-auth'
|
||||
| 'native-window'
|
||||
| 'filesystem'
|
||||
| 'app-updates'
|
||||
| 'discord-rpc'
|
||||
|
||||
export interface ServerConfig {
|
||||
baseUrl: string
|
||||
credentials?: { username: string; password: string }
|
||||
export type Platform = 'tauri' | 'capacitor' | 'web'
|
||||
|
||||
export interface ServerLaunchConfig {
|
||||
binary?: string
|
||||
binaryArgs?: string
|
||||
webUiEnabled?: boolean
|
||||
}
|
||||
|
||||
export type ServerStatus = 'connected' | 'disconnected' | 'error'
|
||||
|
||||
export interface MangaFilters {
|
||||
inLibrary?: boolean
|
||||
status?: MangaStatus
|
||||
tags?: string[]
|
||||
unread?: boolean
|
||||
sourceId?: string
|
||||
export interface DiscordAssets {
|
||||
largeImage?: string
|
||||
largeText?: string
|
||||
smallImage?: string
|
||||
smallText?: string
|
||||
}
|
||||
|
||||
export type MangaStatus =
|
||||
| 'ONGOING'
|
||||
| 'COMPLETED'
|
||||
| 'LICENSED'
|
||||
| 'PUBLISHING_FINISHED'
|
||||
| 'CANCELLED'
|
||||
| 'ON_HIATUS'
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
items: T[]
|
||||
hasNextPage: boolean
|
||||
total?: number
|
||||
export interface DiscordButton {
|
||||
label: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface MangaMeta {
|
||||
customTitle?: string
|
||||
customCover?: string
|
||||
notes?: string
|
||||
[key: string]: unknown
|
||||
export interface DiscordPresence {
|
||||
state?: string
|
||||
details?: string
|
||||
assets?: DiscordAssets
|
||||
buttons?: DiscordButton[]
|
||||
timestamps?: { start?: number; end?: number }
|
||||
}
|
||||
|
||||
export interface Page {
|
||||
index: number
|
||||
url: string
|
||||
imageData?: string
|
||||
export interface AppUpdateInfo {
|
||||
version: string
|
||||
url: string
|
||||
notes: string
|
||||
}
|
||||
|
||||
export interface AboutServer {
|
||||
name: string
|
||||
version: string
|
||||
buildType: string
|
||||
buildTime: number
|
||||
github: string
|
||||
discord: string
|
||||
export interface StorageInfo {
|
||||
manga_bytes: number
|
||||
total_bytes: number
|
||||
free_bytes: number
|
||||
path: string
|
||||
}
|
||||
|
||||
export interface AboutWebUI {
|
||||
channel: string
|
||||
tag: string
|
||||
updateTimestamp: number
|
||||
export interface MigrateProgress {
|
||||
done: number
|
||||
total: number
|
||||
current: string
|
||||
}
|
||||
|
||||
export interface DownloadItem {
|
||||
chapterId: string
|
||||
mangaId: string
|
||||
chapterName: string
|
||||
mangaTitle: string
|
||||
thumbnailUrl?: string
|
||||
progress: number
|
||||
state: 'queued' | 'downloading' | 'finished' | 'error'
|
||||
export interface UpdateProgress {
|
||||
downloaded: number
|
||||
total: number | null
|
||||
}
|
||||
|
||||
export interface UpdateResult {
|
||||
mangaId: string
|
||||
newChapters: number
|
||||
export interface ReleaseInfo {
|
||||
tag_name: string
|
||||
name: string
|
||||
body: string
|
||||
published_at: string
|
||||
html_url: string
|
||||
}
|
||||
|
||||
export interface LibraryUpdateProgress {
|
||||
isRunning: boolean
|
||||
finishedJobs: number
|
||||
totalJobs: number
|
||||
}
|
||||
export interface PlatformAdapter {
|
||||
readonly platform: Platform
|
||||
|
||||
export interface ServerSecurity {
|
||||
authMode: string
|
||||
authUsername: string
|
||||
socksProxyEnabled: boolean
|
||||
socksProxyHost: string
|
||||
socksProxyPort: string
|
||||
socksProxyVersion: number
|
||||
socksProxyUsername: string
|
||||
flareSolverrEnabled: boolean
|
||||
flareSolverrUrl: string
|
||||
flareSolverrTimeout: number
|
||||
flareSolverrSessionName: string
|
||||
flareSolverrSessionTtl: number
|
||||
flareSolverrAsResponseFallback: boolean
|
||||
}
|
||||
init(): Promise<void>
|
||||
destroy(): Promise<void>
|
||||
isSupported(feature: PlatformFeature): boolean
|
||||
|
||||
export interface SetServerAuthInput {
|
||||
authMode: string
|
||||
authUsername: string
|
||||
authPassword: string
|
||||
}
|
||||
getAppDir(): Promise<string>
|
||||
|
||||
export interface SetSocksProxyInput {
|
||||
socksProxyEnabled: boolean
|
||||
socksProxyHost: string
|
||||
socksProxyPort: string
|
||||
socksProxyVersion: number
|
||||
socksProxyUsername: string
|
||||
socksProxyPassword: string
|
||||
}
|
||||
loadStore(key: string): Promise<unknown>
|
||||
saveStore(key: string, value: unknown): Promise<void>
|
||||
|
||||
export interface SetFlareSolverrInput {
|
||||
flareSolverrEnabled: boolean
|
||||
flareSolverrUrl: string
|
||||
flareSolverrTimeout: number
|
||||
flareSolverrSessionName: string
|
||||
flareSolverrSessionTtl: number
|
||||
flareSolverrAsResponseFallback: boolean
|
||||
}
|
||||
storeCredential(key: string, value: string): Promise<void>
|
||||
getCredential(key: string): Promise<string | null>
|
||||
authenticateBiometric(): Promise<boolean>
|
||||
|
||||
export interface TrackRecordPatch {
|
||||
status?: number
|
||||
score?: number
|
||||
lastChapterRead?: number
|
||||
startDate?: string
|
||||
finishDate?: string
|
||||
private?: boolean
|
||||
}
|
||||
readFile(path: string): Promise<Uint8Array>
|
||||
writeFile(path: string, data: Uint8Array): Promise<void>
|
||||
pickFolder(): Promise<string | null>
|
||||
checkPathExists(path: string): Promise<boolean>
|
||||
createDirectory(path: string): Promise<void>
|
||||
openPath(path: string): Promise<void>
|
||||
getDefaultDownloadsPath(): Promise<string>
|
||||
getStorageInfo(downloadsPath: string): Promise<StorageInfo>
|
||||
migrateDownloads(src: string, dst: string): Promise<void>
|
||||
getAutoBackupDir(): Promise<string>
|
||||
|
||||
export interface RestoreStatus {
|
||||
mangaProgress: number
|
||||
state: string
|
||||
totalManga: number
|
||||
}
|
||||
fetchImage(url: string, headers: Record<string, string>): Promise<Blob>
|
||||
|
||||
export interface ValidateBackupResult {
|
||||
missingSources: { id: string; name: string }[]
|
||||
missingTrackers: { name: string }[]
|
||||
}
|
||||
launchServer(config: ServerLaunchConfig): Promise<void>
|
||||
stopServer(): Promise<void>
|
||||
getServerStatus(): Promise<'running' | 'stopped' | 'error'>
|
||||
|
||||
export interface ServerAdapter {
|
||||
connect(config: ServerConfig): Promise<void>
|
||||
getStatus(): Promise<ServerStatus>
|
||||
getServerUrl(): string
|
||||
setTitle(title: string): Promise<void>
|
||||
minimize(): Promise<void>
|
||||
maximize(): Promise<void>
|
||||
close(): Promise<void>
|
||||
toggleFullscreen(): Promise<void>
|
||||
|
||||
getManga(id: string, signal?: AbortSignal): Promise<Manga>
|
||||
getMangaList(filters: MangaFilters): Promise<PaginatedResult<Manga>>
|
||||
getMangasByGenre(filter: Record<string, unknown>, first: number, offset: number, signal?: AbortSignal): Promise<{ items: Manga[]; hasNextPage: boolean; totalCount: number }>
|
||||
searchManga(query: string, sourceId?: string): Promise<Manga[]>
|
||||
fetchManga(id: string): Promise<Manga>
|
||||
addToLibrary(mangaId: string): Promise<void>
|
||||
removeFromLibrary(mangaId: string): Promise<void>
|
||||
updateMangas(ids: string[], patch: { inLibrary?: boolean }): Promise<void>
|
||||
updateMangaMeta(id: string, meta: Partial<MangaMeta>): Promise<void>
|
||||
deleteMangaMeta(id: string, key: string): Promise<void>
|
||||
setDiscordPresence(presence: DiscordPresence): Promise<void>
|
||||
clearDiscordPresence(): Promise<void>
|
||||
|
||||
getChapters(mangaId: string): Promise<Chapter[]>
|
||||
getChapter(id: string): Promise<Chapter>
|
||||
getChapterPages(id: string, signal?: AbortSignal): Promise<Page[]>
|
||||
fetchChapters(mangaId: string): Promise<Chapter[]>
|
||||
getRecentlyUpdated(): Promise<Chapter[]>
|
||||
markChapterRead(id: string, read: boolean): Promise<void>
|
||||
markChaptersRead(ids: string[], read: boolean): Promise<void>
|
||||
updateChaptersProgress(ids: string[], patch: { isRead?: boolean; isBookmarked?: boolean; lastPageRead?: number }): Promise<void>
|
||||
deleteDownloadedChapters(ids: string[]): Promise<void>
|
||||
setChapterMeta(chapterId: string, key: string, value: string): Promise<void>
|
||||
deleteChapterMeta(chapterId: string, key: string): Promise<void>
|
||||
getVersion(): Promise<string>
|
||||
openExternal(url: string): Promise<void>
|
||||
checkForAppUpdate(): Promise<AppUpdateInfo | null>
|
||||
installAppUpdate(tag: string): Promise<void>
|
||||
restartApp(): Promise<void>
|
||||
exitApp(): Promise<void>
|
||||
listReleases(): Promise<ReleaseInfo[]>
|
||||
|
||||
getAboutServer(): Promise<AboutServer>
|
||||
getAboutWebUI(): Promise<AboutWebUI>
|
||||
clearMokuCache(): Promise<void>
|
||||
clearSuwayomiCache(): Promise<void>
|
||||
resetSuwayomiData(): Promise<void>
|
||||
|
||||
getDownloads(): Promise<DownloadItem[]>
|
||||
getDownloadStatus(): Promise<DownloadStatus>
|
||||
enqueueDownload(chapterId: string): Promise<void>
|
||||
enqueueDownloads(chapterIds: string[]): Promise<void>
|
||||
dequeueDownload(chapterId: string): Promise<void>
|
||||
dequeueDownloads(chapterIds: string[]): Promise<void>
|
||||
reorderDownload(chapterId: string, to: number): Promise<DownloadStatus | null>
|
||||
clearDownloads(): Promise<void>
|
||||
startDownloader(): Promise<DownloadStatus | null>
|
||||
stopDownloader(): Promise<DownloadStatus | null>
|
||||
|
||||
getExtensions(): Promise<Extension[]>
|
||||
installExtension(id: string): Promise<void>
|
||||
uninstallExtension(id: string): Promise<void>
|
||||
updateExtension(id: string): Promise<void>
|
||||
updateExtensions(ids: string[]): Promise<void>
|
||||
installExternalExtension(url: string): Promise<void>
|
||||
getExtensionRepos(): Promise<string[]>
|
||||
setExtensionRepos(repos: string[]): Promise<string[]>
|
||||
|
||||
getSources(): Promise<Source[]>
|
||||
browseSource(sourceId: string, page: number): Promise<PaginatedResult<Manga>>
|
||||
getSourceSettings(sourceId: string): Promise<unknown[]>
|
||||
updateSourcePreference(sourceId: string, position: number, changeType: string, value: unknown): Promise<unknown[]>
|
||||
|
||||
getCategories(): Promise<Category[]>
|
||||
createCategory(name: string): Promise<Category>
|
||||
deleteCategory(id: number): Promise<void>
|
||||
updateCategoryOrder(id: number, position: number): Promise<Category[]>
|
||||
updateMangaCategories(mangaId: string, addTo: number[], removeFrom: number[]): Promise<void>
|
||||
updateMangasCategories(mangaIds: string[], addTo: number[], removeFrom: number[]): Promise<void>
|
||||
updateCategoryManga(categoryId: number): Promise<void>
|
||||
|
||||
getTrackers(): Promise<Tracker[]>
|
||||
getAllTrackerRecords(): Promise<unknown[]>
|
||||
getMangaTrackRecords(mangaId: string): Promise<unknown[]>
|
||||
searchTracker(trackerId: string, query: string): Promise<unknown[]>
|
||||
linkTracker(mangaId: string, trackerId: string, remoteId: string): Promise<void>
|
||||
unlinkTracker(recordId: string): Promise<void>
|
||||
updateTrackRecord(recordId: string, patch: TrackRecordPatch): Promise<TrackRecord>
|
||||
fetchTrackRecord(recordId: string): Promise<TrackRecord>
|
||||
syncTracking(mangaId: string): Promise<void>
|
||||
loginTrackerOAuth(trackerId: string, callbackUrl: string): Promise<void>
|
||||
loginTrackerCredentials(trackerId: string, username: string, password: string): Promise<void>
|
||||
logoutTracker(trackerId: string): Promise<void>
|
||||
|
||||
getServerSecurity(): Promise<ServerSecurity>
|
||||
setServerAuth(input: SetServerAuthInput): Promise<void>
|
||||
setSocksProxy(input: SetSocksProxyInput): Promise<void>
|
||||
setFlareSolverr(input: SetFlareSolverrInput): Promise<void>
|
||||
|
||||
getDownloadsPath(): Promise<{ downloadsPath: string; localSourcePath: string }>
|
||||
setDownloadsPath(path: string): Promise<void>
|
||||
setLocalSourcePath(path: string): Promise<void>
|
||||
createBackup(): Promise<{ url: string }>
|
||||
restoreBackup(file: File): Promise<{ id: string; status: RestoreStatus }>
|
||||
validateBackup(file: File): Promise<ValidateBackupResult>
|
||||
pollRestoreStatus(id: string): Promise<RestoreStatus>
|
||||
clearCachedImages(opts: { cachedPages: boolean; cachedThumbnails: boolean; downloadedThumbnails: boolean }): Promise<void>
|
||||
|
||||
checkForUpdates(mangaIds?: string[]): Promise<UpdateResult[]>
|
||||
stopLibraryUpdate(): Promise<void>
|
||||
getLibraryUpdateStatus(): Promise<LibraryUpdateProgress>
|
||||
clearPageCache(chapterId?: number): void
|
||||
onUpdateProgress(cb: (p: UpdateProgress) => void): Promise<() => void>
|
||||
onUpdateLaunching(cb: () => void): Promise<() => void>
|
||||
onMigrateProgress(cb: (p: MigrateProgress) => void): Promise<() => void>
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { detectAdapter } from '$lib/platform-adapters'
|
||||
import { initPlatformService } from '$lib/platform-service'
|
||||
import { platformService } from '$lib/platform-service'
|
||||
import { probeServer, loginBasic, loginUI } from '$lib/core/auth'
|
||||
import { probeServer, loginBasic, loginUI, configureAuth } from '$lib/core/auth'
|
||||
import { appState } from '$lib/state/app.svelte'
|
||||
import { settingsState } from '$lib/state/settings.svelte'
|
||||
import { settingsState } from '$lib/state/settings.svelte'
|
||||
|
||||
const MAX_ATTEMPTS = 40
|
||||
const BG_MAX_ATTEMPTS = 120
|
||||
@@ -42,9 +42,9 @@ function pinLockEnabled(): boolean {
|
||||
|
||||
function handleProbeSuccess(gen: number) {
|
||||
if (gen !== probeGeneration) return
|
||||
boot.failed = false
|
||||
boot.skipped = false
|
||||
boot.serverProbeOk = true
|
||||
boot.failed = false
|
||||
boot.skipped = false
|
||||
boot.serverProbeOk = true
|
||||
appState.authenticated = true
|
||||
appState.status = pinLockEnabled() ? 'locked' : 'ready'
|
||||
}
|
||||
@@ -56,7 +56,8 @@ function handleAuthRequired(
|
||||
pass: string,
|
||||
) {
|
||||
if (gen !== probeGeneration) return
|
||||
boot.failed = false
|
||||
boot.failed = false
|
||||
appState.authMode = authMode
|
||||
|
||||
if (authMode === 'BASIC_AUTH' && user && pass) {
|
||||
loginBasic(user, pass)
|
||||
@@ -75,21 +76,25 @@ function handleAuthRequired(
|
||||
appState.status = 'auth'
|
||||
}
|
||||
|
||||
export function startProbe(
|
||||
export async function startProbe(
|
||||
authMode: 'NONE' | 'BASIC_AUTH' | 'UI_LOGIN' = 'NONE',
|
||||
user = '',
|
||||
pass = '',
|
||||
initialDelay = 100,
|
||||
) {
|
||||
): Promise<void> {
|
||||
const gen = ++probeGeneration
|
||||
boot.failed = false
|
||||
boot.loginRequired = false
|
||||
boot.skipped = false
|
||||
boot.serverProbeOk = 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)
|
||||
|
||||
if (appState.platform === 'web') {
|
||||
boot.failed = true
|
||||
boot.failed = true
|
||||
appState.status = 'error'
|
||||
startBackgroundProbe(gen, authMode, user, pass)
|
||||
return
|
||||
|
||||
+25
-11
@@ -34,19 +34,32 @@
|
||||
|
||||
const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
|
||||
|
||||
let splashVisible = $state(true)
|
||||
let bypassed = $state(false)
|
||||
let themeEditorOpen = $state(false)
|
||||
let themeEditorId = $state<string | null>(null)
|
||||
let _splashDismissed = $state(false)
|
||||
let bypassed = $state(false)
|
||||
let themeEditorOpen = $state(false)
|
||||
let themeEditorId = $state<string | null>(null)
|
||||
|
||||
const splashVisible = $derived(
|
||||
!_splashDismissed ||
|
||||
appState.status === 'booting' ||
|
||||
appState.status === 'locked' ||
|
||||
appState.status === 'error' ||
|
||||
appState.status === 'auth'
|
||||
)
|
||||
|
||||
const ringFull = $derived(appState.status === 'ready')
|
||||
|
||||
const showApp = $derived(
|
||||
appState.status === 'ready' ||
|
||||
appState.status === 'auth' ||
|
||||
bypassed
|
||||
!splashVisible && (
|
||||
appState.status === 'ready' ||
|
||||
bypassed
|
||||
)
|
||||
)
|
||||
|
||||
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 readerContainerized = $derived(settingsState.settings.readerContainerized ?? false)
|
||||
const strippedLayout = $derived(isReaderRoute && !readerContainerized)
|
||||
@@ -127,10 +140,11 @@
|
||||
)
|
||||
})
|
||||
|
||||
function onSplashReady() { splashVisible = false }
|
||||
function onSplashUnlock() { appState.status = 'ready'; splashVisible = false }
|
||||
function onSplashBypass() { bypassed = true; splashVisible = false }
|
||||
function onIdleDismiss() { appState.idleSplash = false }
|
||||
$effect(() => {
|
||||
if (appState.status === 'booting') _splashDismissed = false
|
||||
})
|
||||
|
||||
function onIdleDismiss() { appState.idleSplash = false }
|
||||
|
||||
function onSplashRetry() {
|
||||
import('$lib/state/boot.svelte').then(({ retryBoot }) => {
|
||||
|
||||
Reference in New Issue
Block a user