Cleanup core utilities and abstractions

This commit is contained in:
Zerebos
2026-05-23 21:47:54 -04:00
parent f91b46cfa5
commit 074147f64f
14 changed files with 310 additions and 161 deletions
+45 -31
View File
@@ -3,7 +3,7 @@
import { onMount } from 'svelte'
import { page } from '$app/stores'
import { applyTheme, mountSystemThemeSync, unmountSystemThemeSync } from '$lib/core/theme'
import { matchesKeybind, toggleFullscreen } from '$lib/core/keybinds/keybindEngine'
import { initKeybindEngine, matchesKeybind, toggleFullscreen } from '$lib/core/keybinds/keybindEngine'
import { mountIdleDetection } from '$lib/core/ui/idle'
import { applyZoom, mountZoomKey } from '$lib/core/ui/zoom'
import { appState } from '$lib/state/app.svelte'
@@ -94,36 +94,6 @@
}
}
let lastPresenceKey = ''
$effect(() => {
const enabled = settingsState.discordRpc && appState.status === 'ready' && !appState.idle && canUseDiscordRpc()
if (!enabled) {
if (lastPresenceKey) {
lastPresenceKey = ''
void clearDiscordPresence().catch(() => {})
}
return
}
const isReaderRoute = pathname === '/reader' || pathname.startsWith('/reader/')
const title = isReaderRoute ? (readerState.manga?.title ?? 'Moku') : 'Moku'
const chapter = isReaderRoute && readerState.chapter
? `Chapter ${readerState.chapter.chapterNumber}`
: 'Browsing library'
const nextKey = `${title}|${chapter}`
if (nextKey === lastPresenceKey) return
lastPresenceKey = nextKey
void setDiscordPresence({
title,
chapter,
startTimestamp: Date.now(),
}).catch(() => {})
})
function onSplashReady() {
splashVisible = false
}
@@ -134,6 +104,9 @@
}
onMount(() => {
// Keep shell startup deterministic: keybinds -> visuals -> idle -> platform listeners -> feature loops.
const stopKeybindEngine = initKeybindEngine()
applyTheme(settingsState.theme, settingsState.customThemes)
applyZoom(settingsState.uiZoom)
mountSystemThemeSync()
@@ -169,6 +142,45 @@
})
}
let lastPresenceKey = ''
const stopDiscordWatch = $effect.root(() => {
$effect(() => {
const enabled = settingsState.discordRpc && appState.status === 'ready' && !appState.idle && canUseDiscordRpc()
if (!enabled) {
if (lastPresenceKey) {
lastPresenceKey = ''
void clearDiscordPresence().catch(() => {})
}
return
}
const isReaderRoute = pathname === '/reader' || pathname.startsWith('/reader/')
const title = isReaderRoute ? (readerState.manga?.title ?? 'Moku') : 'Moku'
const chapter = isReaderRoute && readerState.chapter
? `Chapter ${readerState.chapter.chapterNumber}`
: 'Browsing library'
const nextKey = `${title}|${chapter}`
if (nextKey === lastPresenceKey) return
lastPresenceKey = nextKey
void setDiscordPresence({
title,
chapter,
startTimestamp: Date.now(),
}).catch(() => {})
})
return () => {
if (lastPresenceKey) {
lastPresenceKey = ''
void clearDiscordPresence().catch(() => {})
}
}
})
const DOWNLOAD_POLL_MS = 8_000
let downloadPollId: ReturnType<typeof setInterval> | null = null
@@ -206,7 +218,9 @@
appState.idle = false
stopZoomKey()
stopIdleDetection()
stopKeybindEngine()
stopDownloadPolling()
stopDiscordWatch()
stopStatusWatch()
window.removeEventListener('resize', handleResize)
unmountSystemThemeSync()
+20 -109
View File
@@ -1,63 +1,14 @@
<script lang="ts">
import { onMount } from 'svelte'
import { settingsState } from '$lib/state/settings.svelte'
import { trackingState } from '$lib/state/tracking.svelte'
import { syncTracking } from '$lib/request-manager/tracking'
interface GqlTracker {
id: number
name: string
icon?: string | null
isLoggedIn: boolean
isTokenExpired: boolean
authUrl?: string | null
trackRecords?: {
nodes: Array<{
id: number
manga?: { id: number; title: string; thumbnailUrl: string; inLibrary: boolean } | null
}>
}
}
const GET_TRACKERS = `
query GetTrackers {
trackers {
nodes {
id name icon isLoggedIn isTokenExpired authUrl
trackRecords {
nodes {
id trackerId remoteId title status score displayScore lastChapterRead totalChapters remoteUrl
manga { id title thumbnailUrl inLibrary }
}
}
}
}
}
`
const LOGIN_TRACKER_OAUTH = `
mutation LoginTrackerOAuth($trackerId: Int!, $callbackUrl: String!) {
loginTrackerOAuth(input: { trackerId: $trackerId, callbackUrl: $callbackUrl }) {
isLoggedIn
}
}
`
const LOGIN_TRACKER_CREDENTIALS = `
mutation LoginTrackerCredentials($trackerId: Int!, $username: String!, $password: String!) {
loginTrackerCredentials(input: { trackerId: $trackerId, username: $username, password: $password }) {
isLoggedIn
}
}
`
const LOGOUT_TRACKER = `
mutation LogoutTracker($trackerId: Int!) {
logoutTracker(input: { trackerId: $trackerId }) {
isLoggedIn
}
}
`
import {
loadTrackers,
loginTrackerOAuth,
loginTrackerCredentials,
logoutTracker,
syncTracking,
} from '$lib/request-manager/tracking'
import type { Tracker } from '$lib/types'
let oauthTrackerId = $state<number | null>(null)
let oauthCallback = $state('')
@@ -65,84 +16,44 @@
let credsUsername = $state('')
let credsPassword = $state('')
function endpoint() {
return `${settingsState.serverUrl.replace(/\/$/, '')}/api/graphql`
}
function authHeaders() {
const headers: Record<string, string> = {'Content-Type': 'application/json'}
if (settingsState.serverAuthMode === 'BASIC_AUTH' && settingsState.serverAuthUser) {
headers.Authorization = `Basic ${btoa(`${settingsState.serverAuthUser}:${settingsState.serverAuthPass}`)}`
}
return headers
}
async function gql<T>(query: string, variables?: Record<string, unknown>) {
const response = await fetch(endpoint(), {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({query, variables}),
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const json = await response.json() as { data?: T; errors?: { message: string }[] }
if (json.errors?.length) {
throw new Error(json.errors[0].message)
}
return json.data as T
}
async function refreshTrackers() {
trackingState.loading = true
trackingState.error = null
try {
const data = await gql<{ trackers: { nodes: GqlTracker[] } }>(GET_TRACKERS)
trackingState.trackers = data.trackers.nodes as never
await loadTrackers()
} catch (error) {
trackingState.error = error instanceof Error ? error.message : String(error)
} finally {
trackingState.loading = false
}
}
async function reconnectOAuth() {
if (!oauthTrackerId || !oauthCallback.trim()) return
await gql(LOGIN_TRACKER_OAUTH, {trackerId: oauthTrackerId, callbackUrl: oauthCallback.trim()})
await loginTrackerOAuth(oauthTrackerId, oauthCallback.trim())
oauthTrackerId = null
oauthCallback = ''
await refreshTrackers()
}
async function connectCredentials() {
if (!credsTrackerId || !credsUsername.trim() || !credsPassword.trim()) return
await gql(LOGIN_TRACKER_CREDENTIALS, {
trackerId: credsTrackerId,
username: credsUsername.trim(),
password: credsPassword,
})
await loginTrackerCredentials(credsTrackerId, credsUsername.trim(), credsPassword)
credsTrackerId = null
credsUsername = ''
credsPassword = ''
await refreshTrackers()
}
async function disconnectTracker(trackerId: number) {
await gql(LOGOUT_TRACKER, {trackerId})
await refreshTrackers()
await logoutTracker(trackerId)
}
async function syncAllTrackers() {
trackingState.syncing = true
try {
const mangaIds = new Set<number>()
const mangaIds: number[] = []
for (const tracker of trackingState.trackers) {
for (const record of tracker.trackRecords?.nodes ?? []) {
if (record.manga?.id) mangaIds.add(record.manga.id)
const mangaId = record.manga?.id
if (mangaId && !mangaIds.includes(mangaId)) {
mangaIds.push(mangaId)
}
}
}
@@ -154,14 +65,14 @@
}
}
function openOAuth(tracker: GqlTracker) {
function openOAuth(tracker: Tracker) {
if (tracker.authUrl) window.open(tracker.authUrl, '_blank', 'noopener')
oauthTrackerId = tracker.id
oauthCallback = ''
credsTrackerId = null
}
function openCredentials(tracker: GqlTracker) {
function openCredentials(tracker: Tracker) {
credsTrackerId = tracker.id
credsUsername = ''
credsPassword = ''
@@ -193,7 +104,7 @@
<button class="settings-button" type="button" onclick={() => void refreshTrackers()}>Refresh</button>
</div>
{#each trackingState.trackers as tracker}
{#each trackingState.trackers as tracker (tracker.id)}
<div class="settings-row settings-tracker-row">
<div>
<div class="settings-label">{tracker.name}</div>