Chore: Remove Old Directory (Prepare for Patches)

This commit is contained in:
Youwes09
2026-06-02 20:04:01 -05:00
parent 18027baee1
commit db8a984270
223 changed files with 595 additions and 34696 deletions
+18 -39
View File
@@ -1,11 +1,12 @@
import { initRequestManager } from '$lib/request-manager'
import { initPlatformService } from '$lib/platform-service'
import { appState } from '$lib/state/app.svelte'
import { configureAuth, probeServer } from '$lib/core/auth'
import { loadSettings, loadLibrary, loadUpdates } from '$lib/core/persistence/persist'
import { loadSettingsIntoState } from '$lib/state/settings.svelte'
import { historyState } from '$lib/state/history.svelte'
import { readerState } from '$lib/state/reader.svelte'
import { detectAdapter } from '$lib/platform-adapters'
import { initPlatformService } from '$lib/platform-service'
import { initRequestManager } from '$lib/request-manager'
import { appState } from '$lib/state/app.svelte'
import { configureAuth, probeServer } from '$lib/core/auth'
import { loadSettings, loadLibrary, loadUpdates } from '$lib/core/persistence/persist'
import { loadSettingsIntoState } from '$lib/state/settings.svelte'
import { historyState } from '$lib/state/history.svelte'
import { readerState } from '$lib/state/reader.svelte'
const KEY_URL = 'moku_server_url'
const KEY_AUTH = 'moku_auth_config'
@@ -16,28 +17,6 @@ interface SavedAuth {
pass?: string
}
function isTauri(): boolean { return '__TAURI_INTERNALS__' in window }
function isCapacitor(): boolean { return 'Capacitor' in window }
function detectPlatform(): 'tauri' | 'capacitor' | 'web' {
if (isTauri()) return 'tauri'
if (isCapacitor()) return 'capacitor'
return 'web'
}
async function resolvePlatformAdapter() {
if (isTauri()) {
const { TauriAdapter } = await import('$lib/platform-adapters/tauri')
return new TauriAdapter()
}
if (isCapacitor()) {
const { CapacitorAdapter } = await import('$lib/platform-adapters/capacitor')
return new CapacitorAdapter()
}
const { WebAdapter } = await import('$lib/platform-adapters/web')
return new WebAdapter()
}
async function resolveServerAdapter() {
const { SuwayomiAdapter } = await import('$lib/server-adapters/suwayomi')
return new SuwayomiAdapter()
@@ -45,18 +24,18 @@ async function resolveServerAdapter() {
async function boot() {
try {
const [serverAdapter, platformAdapter] = await Promise.all([
resolveServerAdapter(),
resolvePlatformAdapter(),
])
const platformAdapter = detectAdapter()
await platformAdapter.init()
const serverAdapter = await resolveServerAdapter()
initRequestManager(serverAdapter)
initPlatformService(platformAdapter)
initRequestManager(serverAdapter)
appState.platform = detectPlatform()
appState.platform = platformAdapter.platform
appState.version = await platformAdapter.getVersion()
const [settingsData, libraryData, _updatesData] = await Promise.all([
const [settingsData, libraryData] = await Promise.all([
loadSettings(),
loadLibrary(),
loadUpdates(),
@@ -87,8 +66,8 @@ async function boot() {
const probe = await probeServer()
if (probe === 'auth_required') { appState.status = 'auth'; return }
if (probe === 'unreachable') {
if (probe === 'auth_required') { appState.status = 'auth'; return }
if (probe === 'unreachable') {
appState.error = `Could not reach server at ${savedUrl}`
appState.status = 'error'
return
+5 -12
View File
@@ -38,25 +38,19 @@
let allRecs: RecommendedManga[] = $state([])
let loading = $state(false)
let _ctrl: AbortController | null = null
$effect(() => {
const _history = history
const _library = libraryManga
if (!_history.length || !_library.length) { allRecs = []; return }
_ctrl?.abort()
if (!history.length || !libraryManga.length) { allRecs = []; return }
const ctrl = new AbortController()
_ctrl = ctrl
loading = true
fetchRecommendations(_history, _library, ctrl.signal)
fetchRecommendations(history, libraryManga, ctrl.signal)
.then(r => { if (!ctrl.signal.aborted) { allRecs = r; loading = false } })
.catch(() => { if (!ctrl.signal.aborted) loading = false })
return () => ctrl.abort()
})
const genres = $derived(topGenres(history, libraryManga))
let genreIdx = $state(0)
const genres = $derived(topGenres(history, libraryManga))
let genreIdx = $state(0)
const activeGenre = $derived(genres[genreIdx] ?? null)
const visibleRecs = $derived(
@@ -233,7 +227,6 @@
overflow: hidden;
text-overflow: ellipsis;
}
.empty-msg {
font-family: var(--font-ui);
font-size: var(--text-sm);
+21 -12
View File
@@ -13,6 +13,7 @@ const TARGET_PER_GENRE = 20
export function topGenres(history: ReadSession[], libraryManga: Manga[]): string[] {
const byId = new Map(libraryManga.map(m => [m.id, m]))
const tally = new Map<string, { count: number; original: string }>()
for (const session of history) {
const manga = byId.get(session.mangaId)
if (!manga?.genre?.length) continue
@@ -23,6 +24,7 @@ export function topGenres(history: ReadSession[], libraryManga: Manga[]): string
else tally.set(key, { count: 1, original: g })
}
}
return [...tally.values()]
.sort((a, b) => b.count - a.count)
.slice(0, TOP_GENRES)
@@ -35,25 +37,32 @@ export async function fetchRecommendations(
signal?: AbortSignal,
): Promise<RecommendedManga[]> {
if (!history.length || !libraryManga.length) return []
const genres = topGenres(history, libraryManga)
if (!genres.length) return []
const adapter = getAdapter()
const globalSeen = new Set<number>(libraryManga.map(m => m.id))
const merged: Manga[] = []
for (const genre of genres) {
if (signal?.aborted) break
try {
const results = await adapter.getMangaByGenre(genre, { excludeInLibrary: true }, signal)
for (const m of results) {
if (globalSeen.has(m.id)) continue
globalSeen.add(m.id)
merged.push(m)
if (merged.length >= genres.length * TARGET_PER_GENRE) break
const perGenre = await Promise.all(
genres.map(async genre => {
if (signal?.aborted) return []
try {
const { items } = await adapter.getMangaList({ tags: [genre], inLibrary: false })
return items
} catch {
return []
}
} catch {
continue
})
)
const merged: Manga[] = []
for (const items of perGenre) {
for (const m of items) {
if (globalSeen.has(m.id)) continue
globalSeen.add(m.id)
merged.push(m)
if (merged.length >= genres.length * TARGET_PER_GENRE) break
}
}
+69 -59
View File
@@ -1,88 +1,98 @@
const VAULT_KEY = "moku-credential-vault";
const SALT_ITERATIONS = 200_000;
const KEY_USAGE: KeyUsage[] = ["encrypt", "decrypt"];
import { platformService } from '$lib/platform-service'
const VAULT_STORE_KEY = 'moku-vault'
const SALT_ITERATIONS = 200_000
const KEY_USAGE: KeyUsage[] = ['encrypt', 'decrypt']
export interface VaultPayload {
refreshToken?: string;
basicUser?: string;
basicPass?: string;
authMode: "UI_LOGIN" | "BASIC_AUTH" | "NONE";
refreshToken?: string
basicUser?: string
basicPass?: string
authMode: 'UI_LOGIN' | 'BASIC_AUTH' | 'NONE'
}
interface StoredVault {
salt: string;
iv: string;
data: string;
salt: string
iv: string
data: string
}
function toB64(buf: ArrayBuffer): string {
return btoa(String.fromCharCode(...new Uint8Array(buf)));
return btoa(String.fromCharCode(...new Uint8Array(buf)))
}
function fromB64(s: string): Uint8Array {
return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
return Uint8Array.from(atob(s), c => c.charCodeAt(0))
}
async function deriveKey(pin: string, salt: Uint8Array): Promise<CryptoKey> {
const enc = new TextEncoder();
const keyMat = await crypto.subtle.importKey("raw", enc.encode(pin), "PBKDF2", false, ["deriveKey"]);
const enc = new TextEncoder()
const keyMat = await crypto.subtle.importKey('raw', enc.encode(pin), 'PBKDF2', false, ['deriveKey'])
return crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: SALT_ITERATIONS, hash: "SHA-256" },
{ name: 'PBKDF2', salt, iterations: SALT_ITERATIONS, hash: 'SHA-256' },
keyMat,
{ name: "AES-GCM", length: 256 },
{ name: 'AES-GCM', length: 256 },
false,
KEY_USAGE,
);
)
}
export function vaultExists(): boolean {
return !!localStorage.getItem(VAULT_KEY);
}
export async function lockVault(pin: string, payload: VaultPayload): Promise<void> {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await deriveKey(pin, salt);
const enc = new TextEncoder();
const cipher = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
enc.encode(JSON.stringify(payload)),
);
localStorage.setItem(VAULT_KEY, JSON.stringify({
salt: toB64(salt),
iv: toB64(iv),
data: toB64(cipher),
} satisfies StoredVault));
}
export async function unlockVault(pin: string): Promise<VaultPayload | null> {
const raw = localStorage.getItem(VAULT_KEY);
if (!raw) return null;
async function readRaw(): Promise<StoredVault | null> {
try {
const stored = JSON.parse(raw) as StoredVault;
const key = await deriveKey(pin, fromB64(stored.salt));
const plain = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: fromB64(stored.iv) },
key,
fromB64(stored.data),
);
return JSON.parse(new TextDecoder().decode(plain)) as VaultPayload;
const raw = await platformService.getCredential(VAULT_STORE_KEY)
return raw ? JSON.parse(raw) as StoredVault : null
} catch {
return null;
return null
}
}
export function clearVault(): void {
localStorage.removeItem(VAULT_KEY);
async function writeRaw(vault: StoredVault): Promise<void> {
await platformService.storeCredential(VAULT_STORE_KEY, JSON.stringify(vault))
}
export async function vaultExists(): Promise<boolean> {
return (await readRaw()) !== null
}
export async function lockVault(pin: string, payload: VaultPayload): Promise<void> {
const salt = crypto.getRandomValues(new Uint8Array(16))
const iv = crypto.getRandomValues(new Uint8Array(12))
const key = await deriveKey(pin, salt)
const enc = new TextEncoder()
const cipher = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
enc.encode(JSON.stringify(payload)),
)
await writeRaw({ salt: toB64(salt), iv: toB64(iv), data: toB64(cipher) })
}
export async function unlockVault(pin: string): Promise<VaultPayload | null> {
const stored = await readRaw()
if (!stored) return null
try {
const key = await deriveKey(pin, fromB64(stored.salt))
const plain = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: fromB64(stored.iv) },
key,
fromB64(stored.data),
)
return JSON.parse(new TextDecoder().decode(plain)) as VaultPayload
} catch {
return null
}
}
export async function clearVault(): Promise<void> {
await platformService.storeCredential(VAULT_STORE_KEY, '')
}
export async function rekeyVault(oldPin: string, newPin: string): Promise<boolean> {
const payload = await unlockVault(oldPin);
if (!payload) return false;
await lockVault(newPin, payload);
return true;
const payload = await unlockVault(oldPin)
if (!payload) return false
await lockVault(newPin, payload)
return true
}
+21 -4
View File
@@ -1,5 +1,22 @@
export { loadAllStores, persistSettings, persistLibrary, persistUpdates } from "./persist";
export type { PersistedData } from "./persist";
export {
loadSettings, saveSettings,
loadLibrary, saveLibrary,
loadUpdates, saveUpdates,
loadBackups, saveBackups,
} from './persist'
export { vaultExists, lockVault, unlockVault, clearVault, rekeyVault } from "./credentialVault";
export type { VaultPayload } from "./credentialVault";
export type {
PersistedSettings,
PersistedLibrary,
PersistedUpdates,
} from './persist'
export {
vaultExists,
lockVault,
unlockVault,
clearVault,
rekeyVault,
} from './credentialVault'
export type { VaultPayload } from './credentialVault'
+49 -49
View File
@@ -1,6 +1,6 @@
import { platformService } from '$lib/platform-service'
import type { ReadSession } from '$lib/types/history'
import type { BookmarkEntry, MarkerEntry } from '$lib/types/history'
import type { ReadSession } from '$lib/types/history'
import type { BookmarkEntry, MarkerEntry } from '$lib/types/history'
const STORE_VERSION = 2
@@ -22,10 +22,6 @@ export interface PersistedUpdates {
acknowledgedUpdateIds: number[]
}
export interface PersistedBackups {
backupList: { url: string; name: string }[]
}
function migrateLibrary(raw: unknown, fromVersion: number): PersistedLibrary {
const data = (raw ?? {}) as Record<string, unknown>
@@ -36,27 +32,25 @@ function migrateLibrary(raw: unknown, fromVersion: number): PersistedLibrary {
pageNumber?: number; readAt: number
}>
const sessions: ReadSession[] = oldHistory.map(e => ({
id: crypto.randomUUID(),
mangaId: e.mangaId,
mangaTitle: e.mangaTitle,
thumbnailUrl: e.thumbnailUrl,
startChapterId: e.chapterId,
startChapterName: e.chapterName,
endChapterId: e.chapterId,
endChapterName: e.chapterName,
startPage: 1,
endPage: e.pageNumber ?? 1,
startedAt: e.readAt,
endedAt: e.readAt,
durationMs: 0,
chaptersSpanned: 1,
}))
return {
sessions,
bookmarks: (data.bookmarks ?? []) as BookmarkEntry[],
markers: (data.markers ?? []) as MarkerEntry[],
sessions: oldHistory.map(e => ({
id: crypto.randomUUID(),
mangaId: e.mangaId,
mangaTitle: e.mangaTitle,
thumbnailUrl: e.thumbnailUrl,
startChapterId: e.chapterId,
startChapterName: e.chapterName,
endChapterId: e.chapterId,
endChapterName: e.chapterName,
startPage: 1,
endPage: e.pageNumber ?? 1,
startedAt: e.readAt,
endedAt: e.readAt,
durationMs: 0,
chaptersSpanned: 1,
})),
bookmarks: (data.bookmarks ?? []) as BookmarkEntry[],
markers: (data.markers ?? []) as MarkerEntry[],
dailyReadCounts: (data.dailyReadCounts ?? {}) as Record<string, number>,
}
}
@@ -69,19 +63,30 @@ function migrateLibrary(raw: unknown, fromVersion: number): PersistedLibrary {
}
}
function evacuateLocalStorage(key: string): unknown | null {
if (typeof window === 'undefined') return null
try {
const raw = localStorage.getItem(key)
if (!raw) return null
const parsed = JSON.parse(raw)
localStorage.removeItem(key)
return parsed
} catch {
return null
}
}
export async function loadSettings(): Promise<PersistedSettings> {
const raw = await platformService.loadStore('settings')
const raw = await platformService.loadStore('settings')
const data = (raw ?? {}) as Record<string, unknown>
const legacyRaw = typeof window !== 'undefined' ? localStorage.getItem('moku_settings') : null
if (legacyRaw && !data.settings) {
try {
const legacySettings = JSON.parse(legacyRaw)
localStorage.removeItem('moku_settings')
const result: PersistedSettings = { storeVersion: STORE_VERSION, settings: legacySettings }
if (!data.settings) {
const legacy = evacuateLocalStorage('moku_settings')
if (legacy) {
const result: PersistedSettings = { storeVersion: STORE_VERSION, settings: legacy }
await saveSettings(result)
return result
} catch {}
}
}
return {
@@ -99,15 +104,13 @@ export async function loadLibrary(): Promise<PersistedLibrary> {
const data = (raw ?? {}) as Record<string, unknown>
const version = (data.storeVersion as number) ?? 1
const legacyRaw = typeof window !== 'undefined' ? localStorage.getItem('moku-store') : null
if (legacyRaw && !(data.sessions || data.history)) {
try {
const legacy = JSON.parse(legacyRaw)
if (!data.sessions && !data.history) {
const legacy = evacuateLocalStorage('moku-store')
if (legacy) {
const migrated = migrateLibrary(legacy, 1)
localStorage.removeItem('moku-store')
await saveLibrary(migrated)
return migrated
} catch {}
}
}
return migrateLibrary(raw, version)
@@ -136,15 +139,12 @@ export async function loadBackups(): Promise<{ url: string; name: string }[]> {
const data = (raw ?? {}) as Record<string, unknown>
if (!data.backupList) {
try {
const legacyRaw = typeof window !== 'undefined' ? localStorage.getItem('moku_backups') : null
if (legacyRaw) {
const list = JSON.parse(legacyRaw) as { url: string; name: string }[]
localStorage.removeItem('moku_backups')
await saveBackups(list)
return list
}
} catch {}
const legacy = evacuateLocalStorage('moku_backups')
if (legacy) {
const list = legacy as { url: string; name: string }[]
await saveBackups(list)
return list
}
return []
}
@@ -1,19 +1,27 @@
import type {
PlatformAdapter,
PlatformFeature,
ServerLaunchConfig,
DiscordPresence,
AppUpdateInfo,
PlatformAdapter, PlatformFeature, Platform,
ServerLaunchConfig, DiscordPresence,
AppUpdateInfo, StorageInfo, ReleaseInfo,
UpdateProgress, MigrateProgress,
} from '$lib/platform-adapters/types'
export class CapacitorAdapter implements PlatformAdapter {
async init() {}
readonly platform: Platform = 'capacitor'
async init(): Promise<void> {}
async destroy(): Promise<void> {}
isSupported(feature: PlatformFeature): boolean {
const supported: PlatformFeature[] = ['biometric-auth', 'filesystem']
return supported.includes(feature)
}
async getAppDir(): Promise<string> {
const { Filesystem, Directory } = await import('@capacitor/filesystem')
const result = await Filesystem.getUri({ path: '', directory: Directory.Data })
return result.uri
}
async loadStore(key: string): Promise<unknown> {
try {
const { Preferences } = await import('@capacitor/preferences')
@@ -31,39 +39,6 @@ export class CapacitorAdapter implements PlatformAdapter {
} catch {}
}
async launchServer(_config: ServerLaunchConfig) {}
async stopServer() {}
async getServerStatus(): Promise<'running' | 'stopped' | 'error'> { return 'stopped' }
async readFile(path: string): Promise<Uint8Array> {
const { Filesystem, Directory } = await import('@capacitor/filesystem')
const result = await Filesystem.readFile({ path, directory: Directory.Data })
const base64 = result.data as string
const binary = atob(base64)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
return bytes
}
async writeFile(path: string, data: Uint8Array): Promise<void> {
const { Filesystem, Directory } = await import('@capacitor/filesystem')
const binary = String.fromCharCode(...data)
const base64 = btoa(binary)
await Filesystem.writeFile({ path, data: base64, directory: Directory.Data })
}
async pickFolder(): Promise<string | null> { return null }
async authenticateBiometric(): Promise<boolean> {
try {
const { NativeBiometric } = await import('capacitor-native-biometric')
await NativeBiometric.verifyIdentity({ reason: 'Authenticate to access Moku', title: 'Biometric Auth' })
return true
} catch {
return false
}
}
async storeCredential(key: string, value: string): Promise<void> {
const { NativeBiometric } = await import('capacitor-native-biometric')
await NativeBiometric.setCredentials({ username: key, password: value, server: 'moku' })
@@ -79,14 +54,57 @@ export class CapacitorAdapter implements PlatformAdapter {
}
}
async setTitle(_title: string) {}
async minimize() {}
async maximize() {}
async close() {}
async toggleFullscreen() {}
async authenticateBiometric(): Promise<boolean> {
try {
const { NativeBiometric } = await import('capacitor-native-biometric')
await NativeBiometric.verifyIdentity({ reason: 'Authenticate to access Moku', title: 'Biometric Auth' })
return true
} catch {
return false
}
}
async setDiscordPresence(_presence: DiscordPresence) {}
async clearDiscordPresence() {}
async readFile(path: string): Promise<Uint8Array> {
const { Filesystem, Directory } = await import('@capacitor/filesystem')
const result = await Filesystem.readFile({ path, directory: Directory.Data })
const binary = atob(result.data as string)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
return bytes
}
async writeFile(path: string, data: Uint8Array): Promise<void> {
const { Filesystem, Directory } = await import('@capacitor/filesystem')
await Filesystem.writeFile({
path,
data: btoa(String.fromCharCode(...data)),
directory: Directory.Data,
})
}
async pickFolder(): Promise<string | null> { return null }
async checkPathExists(_path: string): Promise<boolean> { return false }
async createDirectory(_path: string): Promise<void> {}
async openPath(_path: string): Promise<void> {}
async getDefaultDownloadsPath(): Promise<string> { return '' }
async getStorageInfo(_downloadsPath: string): Promise<StorageInfo> {
return { manga_bytes: 0, total_bytes: 0, free_bytes: 0, path: '' }
}
async migrateDownloads(_src: string, _dst: string): Promise<void> {}
async getAutoBackupDir(): Promise<string> { return '' }
async launchServer(_config: ServerLaunchConfig): Promise<void> {}
async stopServer(): Promise<void> {}
async getServerStatus(): Promise<'running' | 'stopped' | 'error'> { return 'stopped' }
async setTitle(_title: string): Promise<void> {}
async minimize(): Promise<void> {}
async maximize(): Promise<void> {}
async close(): Promise<void> {}
async toggleFullscreen(): Promise<void> {}
async setDiscordPresence(_presence: DiscordPresence): Promise<void> {}
async clearDiscordPresence(): Promise<void> {}
async getVersion(): Promise<string> {
const { App } = await import('@capacitor/app')
@@ -100,26 +118,16 @@ export class CapacitorAdapter implements PlatformAdapter {
}
async checkForAppUpdate(): Promise<AppUpdateInfo | null> { return null }
async installAppUpdate(): Promise<void> {}
async installAppUpdate(_tag: string): Promise<void> {}
async restartApp(): Promise<void> {}
async getDefaultDownloadsPath(): Promise<string> { return '' }
async getStorageInfo(): Promise<{ manga_bytes: number; total_bytes: number; free_bytes: number; path: string }> {
return { manga_bytes: 0, total_bytes: 0, free_bytes: 0, path: '' }
}
async checkPathExists(_path: string): Promise<boolean> { return false }
async createDirectory(_path: string): Promise<void> {}
async openPath(_path: string): Promise<void> {}
async getAutoBackupDir(): Promise<string> { return '' }
async exitApp(): Promise<void> {}
async listReleases(): Promise<ReleaseInfo[]> { return [] }
async clearMokuCache(): Promise<void> {}
async clearSuwayomiCache(): Promise<void> {}
async resetSuwayomiData(): Promise<void> {}
async exitApp(): Promise<void> {}
async listReleases() { return [] }
async onUpdateProgress(_cb: (p: { downloaded: number; total: number | null }) => void): Promise<() => void> { return () => {} }
async onUpdateProgress(_cb: (p: UpdateProgress) => void): Promise<() => void> { return () => {} }
async onUpdateLaunching(_cb: () => void): Promise<() => void> { return () => {} }
async onMigrateProgress(_cb: (p: { done: number; total: number; current: string }) => void): Promise<() => void> { return () => {} }
async migrateDownloads(_src: string, _dst: string): Promise<void> {}
async onMigrateProgress(_cb: (p: MigrateProgress) => void): Promise<() => void> { return () => {} }
}
+24
View File
@@ -0,0 +1,24 @@
export type { PlatformAdapter, PlatformFeature, Platform } from './types'
export type {
ServerLaunchConfig, DiscordPresence, AppUpdateInfo,
StorageInfo, MigrateProgress, UpdateProgress, ReleaseInfo,
} from './types'
export { TauriAdapter } from './tauri/adapter'
export { WebAdapter } from './web/adapter'
export { CapacitorAdapter } from './capacitor/adapter'
import type { PlatformAdapter } from './types'
import { TauriAdapter } from './tauri/adapter'
import { CapacitorAdapter } from './capacitor/adapter'
import { WebAdapter } from './web/adapter'
export function detectAdapter(): PlatformAdapter {
if (typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window) {
return new TauriAdapter()
}
if (typeof window !== 'undefined' && 'Capacitor' in window) {
return new CapacitorAdapter()
}
return new WebAdapter()
}
@@ -5,34 +5,21 @@ import { open } from '@tauri-apps/plu
import { readFile, writeFile } from '@tauri-apps/plugin-fs'
import { open as openUrl } from '@tauri-apps/plugin-shell'
import { getVersion } from '@tauri-apps/api/app'
import { LazyStore } from '@tauri-apps/plugin-store'
import { connect, disconnect, setActivity, clearActivity } from 'tauri-plugin-discord-rpc-api'
import type {
PlatformAdapter,
PlatformFeature,
ServerLaunchConfig,
DiscordPresence,
AppUpdateInfo,
StorageInfo,
ReleaseInfo,
UpdateProgress,
MigrateProgress,
PlatformAdapter, PlatformFeature, Platform,
ServerLaunchConfig, DiscordPresence,
AppUpdateInfo, StorageInfo, ReleaseInfo,
UpdateProgress, MigrateProgress,
} from '$lib/platform-adapters/types'
const APP_ID = '1487894643613106298'
const storeCache = new Map<string, LazyStore>()
function getStore(key: string): LazyStore {
if (!storeCache.has(key)) {
storeCache.set(key, new LazyStore(`${key}.json`, { autoSave: false }))
}
return storeCache.get(key)!
}
const DISCORD_APP_ID = '1487894643613106298'
export class TauriAdapter implements PlatformAdapter {
readonly platform: Platform = 'tauri'
async init() {
await connect(APP_ID).catch(() => {})
await connect(DISCORD_APP_ID).catch(() => {})
}
async destroy() {
@@ -41,47 +28,54 @@ export class TauriAdapter implements PlatformAdapter {
isSupported(feature: PlatformFeature): boolean {
const supported: PlatformFeature[] = [
'server-management',
'biometric-auth',
'native-window',
'filesystem',
'app-updates',
'discord-rpc',
'server-management', 'biometric-auth',
'native-window', 'filesystem', 'app-updates', 'discord-rpc',
]
return supported.includes(feature)
}
async getAppDir(): Promise<string> {
return invoke<string>('get_app_dir')
}
async loadStore(key: string): Promise<unknown> {
return getStore(key).get<unknown>(key) ?? null
try {
return await invoke<unknown>('load_store', { key })
} catch {
return null
}
}
async saveStore(key: string, value: unknown): Promise<void> {
const store = getStore(key)
await store.set(key, value)
await store.save()
await invoke('save_store', { key, value: JSON.stringify(value) })
}
async launchServer(config: ServerLaunchConfig) {
await invoke('spawn_server', {
binary: config.binary ?? '',
binaryArgs: config.binaryArgs ?? null,
webUiEnabled: config.webUiEnabled ?? false,
})
async storeCredential(key: string, value: string): Promise<void> {
await invoke('store_credential', { key, value })
}
async stopServer() {
await invoke('kill_server')
async getCredential(key: string): Promise<string | null> {
try {
return await invoke<string | null>('get_credential', { key })
} catch {
return null
}
}
async getServerStatus(): Promise<'running' | 'stopped' | 'error'> {
return 'stopped'
async authenticateBiometric(): Promise<boolean> {
try {
await invoke('windows_hello_authenticate', { reason: 'Authenticate to access Moku' })
return true
} catch {
return false
}
}
async readFile(path: string): Promise<Uint8Array> {
return readFile(path)
}
async writeFile(path: string, data: Uint8Array) {
async writeFile(path: string, data: Uint8Array): Promise<void> {
await writeFile(path, data)
}
@@ -94,11 +88,11 @@ export class TauriAdapter implements PlatformAdapter {
return invoke('check_path_exists', { path })
}
async createDirectory(path: string) {
async createDirectory(path: string): Promise<void> {
await invoke('create_directory', { path })
}
async openPath(path: string) {
async openPath(path: string): Promise<void> {
await invoke('open_path', { path })
}
@@ -110,46 +104,57 @@ export class TauriAdapter implements PlatformAdapter {
return invoke('get_storage_info', { downloadsPath })
}
async migrateDownloads(src: string, dst: string) {
async migrateDownloads(src: string, dst: string): Promise<void> {
await invoke('migrate_downloads', { src, dst })
}
async authenticateBiometric(): Promise<boolean> {
try {
await invoke('windows_hello_authenticate', { reason: 'Authenticate to access Moku' })
return true
} catch {
return false
}
async getAutoBackupDir(): Promise<string> {
return invoke('get_auto_backup_dir')
}
async setTitle(title: string) {
async launchServer(config: ServerLaunchConfig): Promise<void> {
await invoke('spawn_server', {
binary: config.binary ?? '',
binaryArgs: config.binaryArgs ?? null,
webUiEnabled: config.webUiEnabled ?? false,
})
}
async stopServer(): Promise<void> {
await invoke('kill_server')
}
async getServerStatus(): Promise<'running' | 'stopped' | 'error'> {
return 'stopped'
}
async setTitle(title: string): Promise<void> {
await getCurrentWindow().setTitle(title)
}
async minimize() {
async minimize(): Promise<void> {
await getCurrentWindow().minimize()
}
async maximize() {
async maximize(): Promise<void> {
const win = getCurrentWindow()
await (await win.isMaximized() ? win.unmaximize() : win.maximize())
}
async close() {
async close(): Promise<void> {
await getCurrentWindow().close()
}
async toggleFullscreen() {
async toggleFullscreen(): Promise<void> {
const win = getCurrentWindow()
await win.setFullscreen(!await win.isFullscreen())
}
async setDiscordPresence(presence: DiscordPresence) {
async setDiscordPresence(presence: DiscordPresence): Promise<void> {
await setActivity(presence).catch(() => {})
}
async clearDiscordPresence() {
async clearDiscordPresence(): Promise<void> {
await clearActivity().catch(() => {})
}
@@ -157,42 +162,57 @@ export class TauriAdapter implements PlatformAdapter {
return getVersion()
}
async openExternal(url: string) {
async openExternal(url: string): Promise<void> {
await openUrl(url)
}
async restartApp() {
await invoke('restart_app')
}
async exitApp() {
await invoke('exit_app')
}
async checkForAppUpdate(): Promise<AppUpdateInfo | null> {
const releases = await invoke<Array<{ tag_name: string; html_url: string; body: string }>>('list_releases')
const current = await getVersion()
const valid = releases.filter(r => r.tag_name?.trim())
if (!valid.length) return null
const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number)
const latest = valid.map(r => r.tag_name).sort((a, b) => {
const pa = parse(a), pb = parse(b)
for (let i = 0; i < 3; i++) if ((pb[i] ?? 0) !== (pa[i] ?? 0)) return (pb[i] ?? 0) - (pa[i] ?? 0)
return 0
})[0]
const pa = parse(latest), pb = parse(current)
if (!pa.some((n, i) => n > (pb[i] ?? 0))) return null
const rel = valid.find(r => r.tag_name === latest)!
return { version: latest.replace(/^v/, ''), url: rel.html_url, notes: rel.body }
}
async installAppUpdate(tag: string): Promise<void> {
await invoke('download_and_install_update', { tag })
}
async restartApp(): Promise<void> {
await invoke('restart_app')
}
async exitApp(): Promise<void> {
await invoke('exit_app')
}
async listReleases(): Promise<ReleaseInfo[]> {
const all = await invoke<ReleaseInfo[]>('list_releases')
return all.filter(r => typeof r.tag_name === 'string' && r.tag_name.trim())
}
async installAppUpdate(tag: string) {
await invoke('download_and_install_update', { tag })
async clearMokuCache(): Promise<void> {
await invoke('clear_moku_cache')
}
async clearSuwayomiCache(): Promise<void> {
await invoke('clear_suwayomi_cache')
}
async resetSuwayomiData(): Promise<void> {
await invoke('reset_suwayomi_data')
}
async onUpdateProgress(cb: (p: UpdateProgress) => void): Promise<() => void> {
@@ -203,22 +223,6 @@ export class TauriAdapter implements PlatformAdapter {
return listen('update-launching', cb)
}
async getAutoBackupDir(): Promise<string> {
return invoke('get_auto_backup_dir')
}
async clearMokuCache() {
await invoke('clear_moku_cache')
}
async clearSuwayomiCache() {
await invoke('clear_suwayomi_cache')
}
async resetSuwayomiData() {
await invoke('reset_suwayomi_data')
}
async onMigrateProgress(cb: (p: MigrateProgress) => void): Promise<() => void> {
return listen<MigrateProgress>('migrate_progress', e => cb(e.payload))
}
+14 -10
View File
@@ -6,32 +6,36 @@ function parse(tag: string): number[] {
return tag.replace(/^v/, '').split('.').map(Number)
}
function compare(a: number[], b: number[]): number {
function isNewer(candidate: number[], current: number[]): boolean {
for (let i = 0; i < 3; i++) {
if ((a[i] ?? 0) !== (b[i] ?? 0)) return (b[i] ?? 0) - (a[i] ?? 0)
if ((candidate[i] ?? 0) > (current[i] ?? 0)) return true
if ((candidate[i] ?? 0) < (current[i] ?? 0)) return false
}
return 0
return false
}
export async function checkForUpdateSilently(): Promise<void> {
try {
const [currentVersion, releases] = await Promise.all([
getVersion(),
invoke<Array<{ tag_name: string; html_url: string }>>('list_releases'),
invoke<Array<{ tag_name: string }>>('list_releases'),
])
const valid = releases.filter(r => typeof r.tag_name === 'string' && r.tag_name.trim())
const valid = releases.filter(r => r.tag_name?.trim())
if (!valid.length) return
const latestTag = valid
const latest = valid
.map(r => r.tag_name)
.sort((a, b) => compare(parse(a), parse(b)))[0]
.replace(/^v/, '')
.sort((a, b) => {
const pa = parse(a), pb = parse(b)
for (let i = 0; i < 3; i++) if ((pb[i] ?? 0) !== (pa[i] ?? 0)) return (pb[i] ?? 0) - (pa[i] ?? 0)
return 0
})[0]
if (compare(parse(latestTag), parse(currentVersion)) < 0) {
if (isNewer(parse(latest), parse(currentVersion))) {
toast({
kind: 'info',
message: `Update available — v${latestTag}`,
message: `Update available — ${latest}`,
detail: 'Open Settings → About to install.',
})
}
+38 -44
View File
@@ -6,9 +6,12 @@ export type PlatformFeature =
| 'app-updates'
| 'discord-rpc'
export type Platform = 'tauri' | 'capacitor' | 'web'
export interface ServerLaunchConfig {
port?: number
[key: string]: unknown
binary?: string
binaryArgs?: string
webUiEnabled?: boolean
}
export interface DiscordAssets {
@@ -23,24 +26,12 @@ export interface DiscordButton {
url: string
}
export interface DiscordParty {
id?: string
currentSize?: number
maxSize?: number
}
export interface DiscordTimestamps {
start?: number
end?: number
}
export interface DiscordPresence {
state?: string
details?: string
assets?: DiscordAssets
buttons?: DiscordButton[]
party?: DiscordParty
timestamps?: DiscordTimestamps
timestamps?: { start?: number; end?: number }
}
export interface AppUpdateInfo {
@@ -76,25 +67,36 @@ export interface ReleaseInfo {
}
export interface PlatformAdapter {
readonly platform: Platform
init(): Promise<void>
destroy(): Promise<void>
isSupported(feature: PlatformFeature): boolean
getAppDir(): Promise<string>
loadStore(key: string): Promise<unknown>
saveStore(key: string, value: unknown): Promise<void>
storeCredential(key: string, value: string): Promise<void>
getCredential(key: string): Promise<string | null>
authenticateBiometric(): Promise<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>
launchServer(config: ServerLaunchConfig): Promise<void>
stopServer(): Promise<void>
getServerStatus(): Promise<'running' | 'stopped' | 'error'>
readFile(path: string): Promise<Uint8Array>
writeFile(path: string, data: Uint8Array): Promise<void>
pickFolder(): Promise<string | null>
authenticateBiometric(): Promise<boolean>
storeCredential(key: string, value: string): Promise<void>
getCredential(key: string): Promise<string | null>
loadStore(key: string): Promise<unknown>
saveStore(key: string, value: unknown): Promise<void>
setTitle(title: string): Promise<void>
minimize(): Promise<void>
maximize(): Promise<void>
@@ -104,27 +106,19 @@ export interface PlatformAdapter {
setDiscordPresence(presence: DiscordPresence): Promise<void>
clearDiscordPresence(): Promise<void>
getVersion(): Promise<string>
openExternal(url: string): Promise<void>
checkForAppUpdate(): Promise<AppUpdateInfo | null>
installAppUpdate(tag: string): Promise<void>
restartApp(): 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[]>
getDefaultDownloadsPath(): Promise<string>
getStorageInfo(downloadsPath: string): Promise<StorageInfo>
checkPathExists(path: string): Promise<boolean>
createDirectory(path: string): Promise<void>
openPath(path: string): Promise<void>
getAutoBackupDir(): Promise<string>
clearMokuCache(): Promise<void>
clearSuwayomiCache(): Promise<void>
resetSuwayomiData(): Promise<void>
exitApp(): Promise<void>
clearMokuCache(): Promise<void>
clearSuwayomiCache(): Promise<void>
resetSuwayomiData(): Promise<void>
onUpdateProgress(cb: (p: UpdateProgress) => void): Promise<() => void>
onUpdateLaunching(cb: () => void): Promise<() => void>
listReleases(): Promise<ReleaseInfo[]>
onMigrateProgress(cb: (p: MigrateProgress) => void): Promise<() => void>
migrateDownloads(src: string, dst: string): Promise<void>
}
@@ -1,22 +1,26 @@
import type {
PlatformAdapter,
PlatformFeature,
ServerLaunchConfig,
DiscordPresence,
AppUpdateInfo,
StorageInfo,
ReleaseInfo,
UpdateProgress,
MigrateProgress,
PlatformAdapter, PlatformFeature, Platform,
ServerLaunchConfig, DiscordPresence,
AppUpdateInfo, StorageInfo, ReleaseInfo,
UpdateProgress, MigrateProgress,
} from '$lib/platform-adapters/types'
declare const __APP_VERSION__: string
export class WebAdapter implements PlatformAdapter {
async init() {}
readonly platform: Platform = 'web'
async init(): Promise<void> {}
async destroy(): Promise<void> {}
isSupported(_feature: PlatformFeature): boolean {
return false
}
async getAppDir(): Promise<string> {
return ''
}
async loadStore(key: string): Promise<unknown> {
try {
const raw = localStorage.getItem(`moku:${key}`)
@@ -32,24 +36,40 @@ export class WebAdapter implements PlatformAdapter {
} catch {}
}
async launchServer(_config: ServerLaunchConfig) {}
async stopServer() {}
async getServerStatus(): Promise<'running' | 'stopped' | 'error'> { return 'stopped' }
async storeCredential(key: string, value: string): Promise<void> {
localStorage.setItem(`moku:cred:${key}`, value)
}
async getCredential(key: string): Promise<string | null> {
return localStorage.getItem(`moku:cred:${key}`)
}
async authenticateBiometric(): Promise<boolean> {
return false
}
async readFile(_path: string): Promise<Uint8Array> { return new Uint8Array() }
async writeFile(_path: string, _data: Uint8Array) {}
async writeFile(_path: string, _data: Uint8Array): Promise<void> {}
async pickFolder(): Promise<string | null> { return null }
async checkPathExists(_path: string): Promise<boolean> { return false }
async createDirectory(_path: string): Promise<void> {}
async openPath(_path: string): Promise<void> {}
async getDefaultDownloadsPath(): Promise<string> { return '' }
async getStorageInfo(_downloadsPath: string): Promise<StorageInfo> {
return { manga_bytes: 0, total_bytes: 0, free_bytes: 0, path: '' }
}
async migrateDownloads(_src: string, _dst: string): Promise<void> {}
async getAutoBackupDir(): Promise<string> { return '' }
async authenticateBiometric(): Promise<boolean> { return false }
async storeCredential(_key: string, _value: string) {}
async getCredential(_key: string): Promise<string | null> { return null }
async launchServer(_config: ServerLaunchConfig): Promise<void> {}
async stopServer(): Promise<void> {}
async getServerStatus(): Promise<'running' | 'stopped' | 'error'> { return 'stopped' }
async setTitle(title: string) { document.title = title }
async minimize() {}
async maximize() {}
async close() {}
async toggleFullscreen() {
async setTitle(title: string): Promise<void> { document.title = title }
async minimize(): Promise<void> {}
async maximize(): Promise<void> {}
async close(): Promise<void> {}
async toggleFullscreen(): Promise<void> {
if (!document.fullscreenElement) {
await document.documentElement.requestFullscreen().catch(() => {})
} else {
@@ -57,37 +77,24 @@ export class WebAdapter implements PlatformAdapter {
}
}
async setDiscordPresence(_presence: DiscordPresence) {}
async clearDiscordPresence() {}
async setDiscordPresence(_presence: DiscordPresence): Promise<void> {}
async clearDiscordPresence(): Promise<void> {}
async getVersion(): Promise<string> { return __APP_VERSION__ }
async openExternal(url: string) {
async openExternal(url: string): Promise<void> {
window.open(url, '_blank', 'noopener,noreferrer')
}
async checkForAppUpdate(): Promise<AppUpdateInfo | null> { return null }
async installAppUpdate(_tag: string) {}
async restartApp() {}
async getDefaultDownloadsPath(): Promise<string> { return '' }
async getStorageInfo(_downloadsPath: string): Promise<StorageInfo> {
return { manga_bytes: 0, total_bytes: 0, free_bytes: 0, path: '' }
}
async checkPathExists(_path: string): Promise<boolean> { return false }
async createDirectory(_path: string) {}
async openPath(_path: string) {}
async getAutoBackupDir(): Promise<string> { return '' }
async clearMokuCache() {}
async clearSuwayomiCache() {}
async resetSuwayomiData() {}
async exitApp() {}
async installAppUpdate(_tag: string): Promise<void> {}
async restartApp(): Promise<void> {}
async exitApp(): Promise<void> {}
async listReleases(): Promise<ReleaseInfo[]> { return [] }
async clearMokuCache(): Promise<void> {}
async clearSuwayomiCache(): Promise<void> {}
async resetSuwayomiData(): Promise<void> {}
async onUpdateProgress(_cb: (p: UpdateProgress) => void): Promise<() => void> { return () => {} }
async onUpdateLaunching(_cb: () => void): Promise<() => void> { return () => {} }
async onMigrateProgress(_cb: (p: MigrateProgress) => void): Promise<() => void> { return () => {} }
async migrateDownloads(_src: string, _dst: string) {}
}
+49 -53
View File
@@ -1,18 +1,12 @@
import type { PlatformAdapter } from '$lib/platform-adapters/types'
import type {
PlatformAdapter,
PlatformFeature,
ServerLaunchConfig,
DiscordPresence,
AppUpdateInfo,
StorageInfo,
ReleaseInfo,
UpdateProgress,
MigrateProgress,
PlatformFeature, ServerLaunchConfig, DiscordPresence,
AppUpdateInfo, StorageInfo, ReleaseInfo, UpdateProgress, MigrateProgress,
} from '$lib/platform-adapters/types'
let adapter: PlatformAdapter
let adapter: PlatformAdapter | null = null
export function initPlatformService(a: PlatformAdapter) {
export function initPlatformService(a: PlatformAdapter): void {
adapter = a
}
@@ -22,56 +16,58 @@ function get(): PlatformAdapter {
}
export const platformService = {
isSupported: (f: PlatformFeature) => get().isSupported(f),
init: () => get().init(),
destroy: () => get().destroy(),
get platform() { return get().platform },
launchServer: (c: ServerLaunchConfig) => get().launchServer(c),
stopServer: () => get().stopServer(),
getServerStatus: () => get().getServerStatus(),
isSupported: (f: PlatformFeature) => get().isSupported(f),
init: () => get().init(),
destroy: () => get().destroy(),
readFile: (path: string) => get().readFile(path),
writeFile: (path: string, data: Uint8Array) => get().writeFile(path, data),
pickFolder: () => get().pickFolder(),
getAppDir: () => get().getAppDir(),
authenticateBiometric: () => get().authenticateBiometric(),
storeCredential: (k: string, v: string) => get().storeCredential(k, v),
getCredential: (k: string) => get().getCredential(k),
loadStore: (key: string) => get().loadStore(key),
saveStore: (key: string, value: unknown) => get().saveStore(key, value),
loadStore: (key: string) => get().loadStore(key),
saveStore: (key: string, value: unknown) => get().saveStore(key, value),
storeCredential: (k: string, v: string) => get().storeCredential(k, v),
getCredential: (k: string) => get().getCredential(k),
authenticateBiometric: () => get().authenticateBiometric(),
setTitle: (title: string) => get().setTitle(title),
minimize: () => get().minimize(),
maximize: () => get().maximize(),
close: () => get().close(),
toggleFullscreen: () => get().toggleFullscreen(),
readFile: (path: string) => get().readFile(path),
writeFile: (path: string, data: Uint8Array) => get().writeFile(path, data),
pickFolder: () => get().pickFolder(),
checkPathExists: (path: string) => get().checkPathExists(path),
createDirectory: (path: string) => get().createDirectory(path),
openPath: (path: string) => get().openPath(path),
getDefaultDownloadsPath: () => get().getDefaultDownloadsPath(),
getStorageInfo: (downloadsPath: string) => get().getStorageInfo(downloadsPath),
migrateDownloads:(src: string, dst: string) => get().migrateDownloads(src, dst),
getAutoBackupDir:() => get().getAutoBackupDir(),
setDiscordPresence: (p: DiscordPresence) => get().setDiscordPresence(p),
launchServer: (c: ServerLaunchConfig) => get().launchServer(c),
stopServer: () => get().stopServer(),
getServerStatus: () => get().getServerStatus(),
setTitle: (title: string) => get().setTitle(title),
minimize: () => get().minimize(),
maximize: () => get().maximize(),
close: () => get().close(),
toggleFullscreen: () => get().toggleFullscreen(),
setDiscordPresence: (p: DiscordPresence) => get().setDiscordPresence(p),
clearDiscordPresence: () => get().clearDiscordPresence(),
getVersion: () => get().getVersion(),
openExternal: (url: string) => get().openExternal(url),
checkForAppUpdate: () => get().checkForAppUpdate(),
installAppUpdate: (tag: string) => get().installAppUpdate(tag),
restartApp: () => get().restartApp(),
getVersion: () => get().getVersion(),
openExternal: (url: string) => get().openExternal(url),
checkForAppUpdate: () => get().checkForAppUpdate(),
installAppUpdate: (tag: string) => get().installAppUpdate(tag),
restartApp: () => get().restartApp(),
exitApp: () => get().exitApp(),
listReleases: () => get().listReleases(),
getDefaultDownloadsPath: () => get().getDefaultDownloadsPath(),
getStorageInfo: (downloadsPath: string) => get().getStorageInfo(downloadsPath),
checkPathExists: (path: string) => get().checkPathExists(path),
createDirectory: (path: string) => get().createDirectory(path),
openPath: (path: string) => get().openPath(path),
getAutoBackupDir: () => get().getAutoBackupDir(),
clearMokuCache: () => get().clearMokuCache(),
clearSuwayomiCache: () => get().clearSuwayomiCache(),
resetSuwayomiData: () => get().resetSuwayomiData(),
clearMokuCache: () => get().clearMokuCache(),
clearSuwayomiCache: () => get().clearSuwayomiCache(),
resetSuwayomiData: () => get().resetSuwayomiData(),
exitApp: () => get().exitApp(),
listReleases: () => get().listReleases(),
onUpdateProgress: (cb: (p: UpdateProgress) => void) => get().onUpdateProgress(cb),
onUpdateLaunching: (cb: () => void) => get().onUpdateLaunching(cb),
onMigrateProgress: (cb: (p: MigrateProgress) => void) => get().onMigrateProgress(cb),
migrateDownloads: (src: string, dst: string) => get().migrateDownloads(src, dst),
onUpdateProgress: (cb: (p: UpdateProgress) => void) => get().onUpdateProgress(cb),
onUpdateLaunching: (cb: () => void) => get().onUpdateLaunching(cb),
onMigrateProgress: (cb: (p: MigrateProgress) => void) => get().onMigrateProgress(cb),
}
+15 -11
View File
@@ -1,9 +1,11 @@
import type { Platform } from '$lib/platform-adapters/types'
export type AppStatus = 'booting' | 'not-configured' | 'auth' | 'ready' | 'error'
class AppStore {
settingsOpen: boolean = $state(false)
navPage: string = $state('')
scrollPositions: Map<string, number> = $state(new Map())
settingsOpen: boolean = $state(false)
navPage: string = $state('')
scrollPositions: Map<string, number> = $state(new Map())
setSettingsOpen(next: boolean) { this.settingsOpen = next }
setNavPage(next: string) { this.navPage = next }
@@ -13,6 +15,7 @@ class AppStore {
m.set(key, top)
this.scrollPositions = m
}
getScroll(key: string): number { return this.scrollPositions.get(key) ?? 0 }
}
@@ -20,21 +23,22 @@ export const app = new AppStore()
export const appState = $state({
status: 'booting' as AppStatus,
error: null as string | null,
error: null as string | null,
serverUrl: '',
authenticated: false,
authMode: 'NONE' as 'NONE' | 'BASIC_AUTH' | 'UI_LOGIN',
platform: 'web' as 'web' | 'tauri' | 'capacitor',
authMode: 'NONE' as 'NONE' | 'BASIC_AUTH' | 'UI_LOGIN',
platform: 'web' as Platform,
version: '',
libraryFilter: '',
navPage: '',
categories: [] as { id: number; name: string }[],
history: [] as unknown[],
toasts: [] as unknown[],
appDir: '',
})
export function setSettingsOpen(next: boolean) { app.setSettingsOpen(next) }
export function saveScroll(key: string, top: number) { app.saveScroll(key, top) }
export function getScroll(key: string): number { return app.getScroll(key) }
export function setGenreFilter(genre: string) { appState.libraryFilter = genre }
export function setNavPage(page: string) { app.setNavPage(page); appState.navPage = page }
export function setSettingsOpen(next: boolean) { app.setSettingsOpen(next) }
export function saveScroll(key: string, top: number) { app.saveScroll(key, top) }
export function getScroll(key: string): number { return app.getScroll(key) }
export function setGenreFilter(genre: string) { appState.libraryFilter = genre }
export function setNavPage(page: string) { app.setNavPage(page); appState.navPage = page }
+22 -5
View File
@@ -1,5 +1,8 @@
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 { appState } from '$lib/state/app.svelte'
import { appState } from '$lib/state/app.svelte'
const MAX_ATTEMPTS = 15
const BG_MAX_ATTEMPTS = 60
@@ -19,6 +22,15 @@ export const boot = $state({
let probeGeneration = 0
export async function initPlatform(): Promise<void> {
const adapter = detectAdapter()
initPlatformService(adapter)
await adapter.init()
appState.platform = adapter.platform
appState.version = await platformService.getVersion()
appState.appDir = await platformService.getAppDir()
}
function handleProbeSuccess(gen: number) {
if (gen !== probeGeneration) return
boot.failed = false
@@ -28,7 +40,12 @@ function handleProbeSuccess(gen: number) {
appState.status = 'ready'
}
function handleAuthRequired(gen: number, authMode: 'NONE' | 'BASIC_AUTH' | 'UI_LOGIN', user: string, pass: string) {
function handleAuthRequired(
gen: number,
authMode: 'NONE' | 'BASIC_AUTH' | 'UI_LOGIN',
user: string,
pass: string,
) {
if (gen !== probeGeneration) return
boot.failed = false
@@ -79,10 +96,10 @@ export function startProbe(
}
function startBackgroundProbe(
gen: number,
gen: number,
authMode: 'NONE' | 'BASIC_AUTH' | 'UI_LOGIN',
user: string,
pass: string,
user: string,
pass: string,
) {
let bgTries = 0
+12 -33
View File
@@ -1,14 +1,12 @@
<script lang="ts">
import { onMount } from 'svelte'
import { page } from '$app/stores'
import { appState, app } from '$lib/state/app.svelte'
import { notifications } from '$lib/state/notifications.svelte'
import { onMount } from 'svelte'
import { page } from '$app/stores'
import { appState, app } from '$lib/state/app.svelte'
import { notifications } from '$lib/state/notifications.svelte'
import { settingsState, loadSettingsIntoState, updateSettings } from '$lib/state/settings.svelte'
import { loadSettings } from '$lib/core/persistence/persist'
import { applyTheme, mountSystemThemeSync } from '$lib/core/theme'
import { initPlatformService, platformService } from '$lib/platform-service'
import { startProbe } from '$lib/state/boot.svelte'
import * as discord from '$lib/core/discord'
import { applyTheme, mountSystemThemeSync } from '$lib/core/theme'
import { platformService } from '$lib/platform-service'
import * as discord from '$lib/core/discord'
import SplashScreen from '$lib/components/chrome/SplashScreen.svelte'
import AuthGate from '$lib/components/chrome/AuthGate.svelte'
import Sidebar from '$lib/components/chrome/Sidebar.svelte'
@@ -52,27 +50,14 @@
const strippedLayout = $derived(isReaderRoute && !readerContainerized)
onMount(async () => {
if (isTauri) {
const { TauriAdapter } = await import('$lib/platform-adapters/tauri')
initPlatformService(new TauriAdapter())
} else {
const { WebAdapter } = await import('$lib/platform-adapters/web')
initPlatformService(new WebAdapter())
}
await platformService.init()
const persisted = await loadSettings()
await loadSettingsIntoState(persisted.settings)
appState.platform = isTauri ? 'tauri' : 'web'
appState.version = await platformService.getVersion().catch(() => '')
// hooks.client.ts already ran detectAdapter(), initPlatformService(),
// loadSettingsIntoState(), and startProbe() — nothing to re-initialize here.
if (isTauri && settingsState.settings.autoStartServer) {
platformService.launchServer({
binary: settingsState.settings.serverBinary,
binary_args: settingsState.settings.serverBinaryArgs,
web_ui_enabled: settingsState.settings.suwayomiWebUI,
binary: settingsState.settings.serverBinary,
binaryArgs: settingsState.settings.serverBinaryArgs,
webUiEnabled: settingsState.settings.suwayomiWebUI,
}).catch(() => {})
}
@@ -81,12 +66,6 @@
await discord.setIdle()
}
startProbe(
settingsState.settings.serverAuthMode,
settingsState.settings.serverAuthUser,
settingsState.settings.serverAuthPass,
)
polling = true
pollLoop()