import { writable, get } from "svelte/store"; import type { Manga, Chapter, Source } from "../lib/types"; import { DEFAULT_KEYBINDS, type Keybinds } from "../lib/keybinds"; export type PageStyle = "single" | "double" | "longstrip"; export type FitMode = "width" | "height" | "screen" | "original"; export type LibraryFilter = "all" | "library" | "downloaded" | string; export type NavPage = "library" | "sources" | "explore" | "downloads" | "extensions" | "history" | "search"; export type ReadingDirection = "ltr" | "rtl"; export type ChapterSortDir = "desc" | "asc"; export type Theme = "dark" | "high-contrast" | "light" | "light-contrast" | "midnight" | "warm"; export interface HistoryEntry { mangaId: number; mangaTitle: string; thumbnailUrl: string; chapterId: number; chapterName: string; pageNumber: number; readAt: number; } export interface Toast { id: string; kind: "success" | "error" | "info" | "download"; title: string; body?: string; duration?: number; } export interface ActiveDownload { chapterId: number; mangaId: number; progress: number } export interface Folder { id: string; name: string; mangaIds: number[]; showTab: boolean } export interface Settings { pageStyle: PageStyle; readingDirection: ReadingDirection; fitMode: FitMode; maxPageWidth: number; pageGap: boolean; optimizeContrast: boolean; offsetDoubleSpreads: boolean; preloadPages: number; autoMarkRead: boolean; autoNextChapter: boolean; libraryCropCovers: boolean; libraryPageSize: number; showNsfw: boolean; chapterSortDir: ChapterSortDir; chapterPageSize: number; uiScale: number; compactSidebar: boolean; gpuAcceleration: boolean; serverUrl: string; serverBinary: string; autoStartServer: boolean; preferredExtensionLang: string; keybinds: Keybinds; idleTimeoutMin?: number; splashCards?: boolean; storageLimitGb: number | null; folders: Folder[]; markReadOnNext: boolean; readerDebounceMs: number; theme: Theme; } export const DEFAULT_SETTINGS: Settings = { pageStyle: "longstrip", readingDirection: "ltr", fitMode: "width", maxPageWidth: 900, pageGap: true, optimizeContrast: false, offsetDoubleSpreads: false, preloadPages: 3, autoMarkRead: true, autoNextChapter: true, libraryCropCovers: true, libraryPageSize: 48, showNsfw: false, chapterSortDir: "desc", chapterPageSize: 25, uiScale: 100, compactSidebar: false, gpuAcceleration: true, serverUrl: "http://localhost:4567", serverBinary: "tachidesk-server", autoStartServer: true, preferredExtensionLang: "en", keybinds: DEFAULT_KEYBINDS, idleTimeoutMin: 5, splashCards: true, storageLimitGb: null, folders: [], markReadOnNext: true, readerDebounceMs: 120, theme: "dark", }; function loadPersisted() { try { const raw = localStorage.getItem("moku-store"); if (!raw) return null; return JSON.parse(raw); } catch { return null; } } function persist(key: string, value: unknown) { try { localStorage.setItem(key, JSON.stringify(value)); } catch {} } const saved = loadPersisted(); function mergeSettings(saved: any): Settings { return { ...DEFAULT_SETTINGS, ...saved?.settings, folders: saved?.settings?.folders ?? [], keybinds: { ...DEFAULT_KEYBINDS, ...saved?.settings?.keybinds }, }; } export const navPage = writable(saved?.navPage ?? "library"); export const libraryFilter = writable(saved?.libraryFilter ?? "library"); export const history = writable(saved?.history ?? []); export const settings = writable(mergeSettings(saved)); export const genreFilter = writable(""); export const searchPrefill = writable(""); export const activeManga = writable(null); export const previewManga = writable(null); export const activeSource = writable(null); export const pageUrls = writable([]); export const pageNumber = writable(1); export const libraryTagFilter = writable([]); export const settingsOpen = writable(false); export const activeDownloads = writable([]); export const toasts = writable([]); export const activeChapter = writable(null); export const activeChapterList = writable([]); export function openReader(chapter: Chapter, chapterList: Chapter[]) { activeChapter.set(chapter); activeChapterList.set(chapterList); pageUrls.set([]); pageNumber.set(1); } export function closeReader() { activeChapter.set(null); activeChapterList.set([]); pageUrls.set([]); pageNumber.set(1); } export function addHistory(entry: HistoryEntry) { history.update((h) => { if (h[0]?.chapterId === entry.chapterId) { const updated = [...h]; updated[0] = { ...updated[0], pageNumber: entry.pageNumber, readAt: entry.readAt }; return updated; } return [entry, ...h.filter((x) => x.chapterId !== entry.chapterId)].slice(0, 300); }); } export function addToast(toast: Omit) { toasts.update((t) => [...t, { ...toast, id: Math.random().toString(36).slice(2) }].slice(-5)); } export function dismissToast(id: string) { toasts.update((t) => t.filter((x) => x.id !== id)); } export function updateSettings(patch: Partial) { settings.update((s) => ({ ...s, ...patch })); } export function resetKeybinds() { settings.update((s) => ({ ...s, keybinds: DEFAULT_KEYBINDS })); } const genId = () => Math.random().toString(36).slice(2, 10); export function addFolder(name: string): string { const id = genId(); settings.update((s) => ({ ...s, folders: [...s.folders, { id, name: name.trim(), mangaIds: [], showTab: false }] })); return id; } export function removeFolder(id: string) { settings.update((s) => ({ ...s, folders: s.folders.filter((f) => f.id !== id) })); } export function renameFolder(id: string, name: string) { settings.update((s) => ({ ...s, folders: s.folders.map((f) => f.id === id ? { ...f, name: name.trim() } : f) })); } export function toggleFolderTab(id: string) { settings.update((s) => ({ ...s, folders: s.folders.map((f) => f.id === id ? { ...f, showTab: !f.showTab } : f) })); } export function assignMangaToFolder(folderId: string, mangaId: number) { settings.update((s) => ({ ...s, folders: s.folders.map((f) => f.id === folderId && !f.mangaIds.includes(mangaId) ? { ...f, mangaIds: [...f.mangaIds, mangaId] } : f ), })); } export function removeMangaFromFolder(folderId: string, mangaId: number) { settings.update((s) => ({ ...s, folders: s.folders.map((f) => f.id === folderId ? { ...f, mangaIds: f.mangaIds.filter((id) => id !== mangaId) } : f ), })); } export function getMangaFolders(mangaId: number): Folder[] { return get(settings).folders.filter((f) => f.mangaIds.includes(mangaId)); } navPage.subscribe((v) => persist("moku-store", { ...loadPersisted(), navPage: v })); libraryFilter.subscribe((v) => persist("moku-store", { ...loadPersisted(), libraryFilter: v })); history.subscribe((v) => persist("moku-store", { ...loadPersisted(), history: v })); settings.subscribe((v) => persist("moku-store", { ...loadPersisted(), settings: v }));