import type { Manga, Chapter, Category, Source } from "../types"; import { DEFAULT_KEYBINDS, type Keybinds } from "../core/keybinds/defaultBinds"; import { notifications } from "./notifications.svelte"; import { app } from "./app.svelte"; export type { NavPage } from "./app.svelte"; export type { Toast, ActiveDownload } from "./notifications.svelte"; export type PageStyle = "single" | "double" | "longstrip"; export type FitMode = "width" | "height" | "screen" | "original"; export type LibraryFilter = "all" | "library" | "downloaded" | string; export type ReadingDirection = "ltr" | "rtl"; export type ChapterSortDir = "desc" | "asc"; export type ChapterSortMode = "source" | "chapterNumber" | "uploadDate"; export type LibrarySortMode = | "az" | "unreadCount" | "totalChapters" | "recentlyAdded" | "recentlyRead" | "latestFetched" | "latestUploaded"; export type LibrarySortDir = "asc" | "desc"; export type LibraryStatusFilter = "ALL" | "ONGOING" | "COMPLETED" | "CANCELLED" | "HIATUS" | "UNKNOWN"; export type LibraryContentFilter = "unread" | "started" | "downloaded" | "bookmarked" | "marked"; export type BuiltinTheme = "dark" | "high-contrast" | "light" | "light-contrast" | "midnight" | "warm"; export type Theme = BuiltinTheme | string; export interface ThemeTokens { "bg-void": string; "bg-base": string; "bg-surface": string; "bg-raised": string; "bg-overlay": string; "bg-subtle": string; "border-dim": string; "border-base": string; "border-strong": string; "border-focus": string; "text-primary": string; "text-secondary": string; "text-muted": string; "text-faint": string; "text-disabled": string; "accent": string; "accent-dim": string; "accent-muted": string; "accent-fg": string; "accent-bright": string; "color-error": string; "color-error-bg": string; "color-success": string; "color-info": string; "color-info-bg": string; } export interface CustomTheme { id: string; name: string; tokens: ThemeTokens; } export const DEFAULT_THEME_TOKENS: ThemeTokens = { "bg-void": "#080808", "bg-base": "#0c0c0c", "bg-surface": "#101010", "bg-raised": "#151515", "bg-overlay": "#1a1a1a", "bg-subtle": "#202020", "border-dim": "#1c1c1c", "border-base": "#242424", "border-strong": "#2e2e2e", "border-focus": "#4a5c4a", "text-primary": "#f0efec", "text-secondary": "#c8c6c0", "text-muted": "#8a8880", "text-faint": "#4e4d4a", "text-disabled": "#2a2a28", "accent": "#6b8f6b", "accent-dim": "#2a3d2a", "accent-muted": "#1a251a", "accent-fg": "#a8c4a8", "accent-bright": "#8fb88f", "color-error": "#c47a7a", "color-error-bg": "#1f1212", "color-success": "#7aab7a", "color-info": "#7a9ec4", "color-info-bg": "#121a1f", }; export interface HistoryEntry { mangaId: number; mangaTitle: string; thumbnailUrl: string; chapterId: number; chapterName: string; readAt: number; } export interface BookmarkEntry { mangaId: number; mangaTitle: string; thumbnailUrl: string; chapterId: number; chapterName: string; pageNumber: number; savedAt: number; label?: string; } export type MarkerColor = "yellow" | "red" | "blue" | "green" | "purple"; export interface MarkerEntry { id: string; mangaId: number; mangaTitle: string; thumbnailUrl: string; chapterId: number; chapterName: string; pageNumber: number; note: string; color: MarkerColor; createdAt: number; updatedAt?: number; } export interface ReadLogEntry { mangaId: number; chapterId: number; readAt: number; minutes: number; } export interface ReadingStats { totalChaptersRead: number; totalMangaRead: number; totalMinutesRead: number; firstReadAt: number; lastReadAt: number; currentStreakDays: number; longestStreakDays: number; lastStreakDate: string; } export interface LibraryUpdateEntry { mangaId: number; mangaTitle: string; thumbnailUrl: string; newChapters: number; checkedAt: number; } export interface MangaPrefs { autoDownload: boolean; downloadAhead: number; deleteOnRead: boolean; deleteDelayHours: number; maxKeepChapters: number; pauseUpdates: boolean; refreshInterval: "global" | "daily" | "weekly" | "manual"; preferredScanlator: string; scanlatorFilter: string[]; } export const DEFAULT_MANGA_PREFS: MangaPrefs = { autoDownload: false, downloadAhead: 0, deleteOnRead: false, deleteDelayHours: 0, maxKeepChapters: 0, pauseUpdates: false, refreshInterval: "global", preferredScanlator: "", scanlatorFilter: [], }; export interface Settings { pageStyle: PageStyle; readingDirection: ReadingDirection; fitMode: FitMode; readerZoom: number; pageGap: boolean; optimizeContrast: boolean; offsetDoubleSpreads: boolean; preloadPages: number; autoMarkRead: boolean; autoNextChapter: boolean; libraryCropCovers: boolean; libraryPageSize: number; showNsfw: boolean; discordRpc: boolean; chapterSortDir: ChapterSortDir; chapterSortMode: ChapterSortMode; chapterPageSize: number; uiZoom: number; compactSidebar: boolean; gpuAcceleration: boolean; serverUrl: string; serverBinary: string; autoStartServer: boolean; preferredExtensionLang: string; keybinds: Keybinds; idleTimeoutMin?: number; splashCards?: boolean; storageLimitGb: number | null; markReadOnNext: boolean; readerDebounceMs: number; autoBookmark: boolean; theme: Theme; libraryBranches: boolean; renderLimit: number; heroSlots: (number | null)[]; mangaLinks: Record; mangaPrefs: Record>; serverAuthUser: string; serverAuthPass: string; serverAuthMode: "NONE" | "BASIC_AUTH" | "SIMPLE_LOGIN" | "UI_LOGIN"; socksProxyEnabled: boolean; socksProxyHost: string; socksProxyPort: string; socksProxyVersion: number; socksProxyUsername: string; socksProxyPassword: string; flareSolverrEnabled: boolean; flareSolverrUrl: string; flareSolverrTimeout: number; flareSolverrSessionName: string; flareSolverrSessionTtl: number; flareSolverrFallback: boolean; appLockEnabled: boolean; appLockPin: string; customThemes: CustomTheme[]; hiddenCategoryIds: number[]; defaultLibraryCategoryId: number | null; savedIsDefaultCategory: boolean; nsfwFilteredTags: string[]; nsfwAllowedSourceIds: string[]; nsfwBlockedSourceIds: string[]; libraryTabSort: Record; libraryTabStatus: Record; libraryTabFilters: Record>>; maxPageWidth?: number; uiScale?: number; extraScanDirs: string[]; serverDownloadsPath: string; serverLocalSourcePath: string; qolAnimations: boolean; } export const DEFAULT_READING_STATS: ReadingStats = { totalChaptersRead: 0, totalMangaRead: 0, totalMinutesRead: 0, firstReadAt: 0, lastReadAt: 0, currentStreakDays: 0, longestStreakDays: 0, lastStreakDate: "", }; export const DEFAULT_SETTINGS: Settings = { pageStyle: "longstrip", readingDirection: "ltr", fitMode: "width", readerZoom: 1.0, pageGap: true, optimizeContrast: false, offsetDoubleSpreads: false, preloadPages: 3, autoMarkRead: true, autoNextChapter: true, libraryCropCovers: true, libraryPageSize: 48, showNsfw: false, discordRpc: false, chapterSortDir: "desc", chapterSortMode: "source", chapterPageSize: 25, uiZoom: 1.0, compactSidebar: false, gpuAcceleration: true, serverUrl: "http://localhost:4567", serverBinary: "", autoStartServer: true, preferredExtensionLang: "en", keybinds: DEFAULT_KEYBINDS, idleTimeoutMin: 5, splashCards: true, storageLimitGb: null, markReadOnNext: true, readerDebounceMs: 120, autoBookmark: true, theme: "dark", libraryBranches: true, renderLimit: 48, heroSlots: [null, null, null, null], mangaLinks: {}, mangaPrefs: {}, serverAuthUser: "", serverAuthPass: "", serverAuthMode: "NONE", socksProxyEnabled: false, socksProxyHost: "", socksProxyPort: "1080", socksProxyVersion: 5, socksProxyUsername: "", socksProxyPassword: "", flareSolverrEnabled: false, flareSolverrUrl: "http://localhost:8191", flareSolverrTimeout: 60, flareSolverrSessionName: "moku", flareSolverrSessionTtl: 15, flareSolverrFallback: false, appLockEnabled: false, appLockPin: "", customThemes: [], hiddenCategoryIds: [], defaultLibraryCategoryId: null, savedIsDefaultCategory: false, nsfwFilteredTags: ["adult", "mature", "hentai", "ecchi", "erotic", "pornograph", "18+", "smut", "lemon", "explicit", "sexual violence"], nsfwAllowedSourceIds: [], nsfwBlockedSourceIds: [], libraryTabSort: {}, libraryTabStatus: {}, libraryTabFilters: {}, extraScanDirs: [], serverDownloadsPath: "", serverLocalSourcePath: "", qolAnimations: true, }; const STORE_VERSION = 3; const AVG_MIN_PER_CHAPTER = 5; const RESET_ON_UPGRADE: (keyof Settings)[] = ["serverBinary", "readerZoom", "uiZoom"]; function loadPersisted(): any { try { const raw = localStorage.getItem("moku-store"); return raw ? JSON.parse(raw) : null; } catch { return null; } } function persist(patch: Record) { try { localStorage.setItem("moku-store", JSON.stringify({ ...loadPersisted() ?? {}, ...patch })); } catch {} } const saved = (() => { const data = loadPersisted(); if (!data) return null; if ((data.storeVersion ?? 1) < STORE_VERSION) { const resetPatch: Partial = {}; for (const key of RESET_ON_UPGRADE) (resetPatch as any)[key] = (DEFAULT_SETTINGS as any)[key]; const migrated = { ...data, storeVersion: STORE_VERSION, settings: { ...data.settings, ...resetPatch } }; try { localStorage.setItem("moku-store", JSON.stringify(migrated)); } catch {} return migrated; } return data; })(); function mergeSettings(saved: any): Settings { return { ...DEFAULT_SETTINGS, ...saved?.settings, keybinds: { ...DEFAULT_KEYBINDS, ...saved?.settings?.keybinds }, heroSlots: saved?.settings?.heroSlots ?? [null, null, null, null], mangaLinks: saved?.settings?.mangaLinks ?? {}, mangaPrefs: saved?.settings?.mangaPrefs ?? {}, customThemes: saved?.settings?.customThemes ?? [], hiddenCategoryIds: saved?.settings?.hiddenCategoryIds ?? [], nsfwFilteredTags: saved?.settings?.nsfwFilteredTags ?? DEFAULT_SETTINGS.nsfwFilteredTags, nsfwAllowedSourceIds: saved?.settings?.nsfwAllowedSourceIds ?? [], nsfwBlockedSourceIds: saved?.settings?.nsfwBlockedSourceIds ?? [], libraryTabSort: saved?.settings?.libraryTabSort ?? {}, libraryTabStatus: saved?.settings?.libraryTabStatus ?? {}, libraryTabFilters: saved?.settings?.libraryTabFilters ?? {}, extraScanDirs: saved?.settings?.extraScanDirs ?? [], }; } class Store { settings: Settings = $state(mergeSettings(saved)); activeManga: Manga | null = $state(null); previewManga: Manga | null = $state(null); activeChapter: Chapter | null = $state(null); activeChapterList: Chapter[] = $state([]); pageUrls: string[] = $state([]); pageNumber: number = $state(1); libraryFilter: LibraryFilter = $state("all"); categories: Category[] = $state([]); activeSource: Source | null = $state(null); libraryTagFilter: string[] = $state([]); history: HistoryEntry[] = $state(saved?.history ?? []); bookmarks: BookmarkEntry[]= $state(saved?.bookmarks ?? []); markers: MarkerEntry[] = $state(saved?.markers ?? []); readLog: ReadLogEntry[] = $state(saved?.readLog ?? []); readingStats: ReadingStats = $state(saved?.readingStats ?? { ...DEFAULT_READING_STATS }); dailyReadCounts: Record = $state(saved?.dailyReadCounts ?? {}); searchCache: Map = $state(new Map()); searchLibraryIds: Set = $state(new Set()); searchSrcOffset: number = $state(0); readerSessionId: number = $state(0); libraryUpdates: LibraryUpdateEntry[] = $state(saved?.libraryUpdates ?? []); lastLibraryRefresh: number = $state(saved?.lastLibraryRefresh ?? 0); acknowledgedUpdates: Set = $state(new Set(saved?.acknowledgedUpdateIds ?? [])); get toasts() { return notifications.toasts; } get activeDownloads() { return notifications.activeDownloads; } get navPage() { return app.navPage; } set navPage(v) { app.setNavPage(v); } get settingsOpen() { return app.settingsOpen; } set settingsOpen(v) { app.setSettingsOpen(v); } get searchPrefill() { return app.searchPrefill; } set searchPrefill(v) { app.setSearchPrefill(v); } get genreFilter() { return app.genreFilter; } set genreFilter(v) { app.setGenreFilter(v); } constructor() { $effect.root(() => { $effect(() => { persist({ settings: this.settings, history: this.history, bookmarks: this.bookmarks, markers: this.markers, readLog: this.readLog, readingStats: this.readingStats, dailyReadCounts: this.dailyReadCounts, libraryUpdates: this.libraryUpdates, lastLibraryRefresh: this.lastLibraryRefresh, acknowledgedUpdateIds: [...this.acknowledgedUpdates], storeVersion: STORE_VERSION, }); }); }); } openReader(chapter: Chapter, chapterList: Chapter[], manga?: Manga | null) { this.activeChapter = chapter; this.activeChapterList = chapterList; if (manga !== undefined) this.activeManga = manga; } closeReader() { this.activeChapter = null; this.activeChapterList = []; this.pageUrls = []; this.pageNumber = 1; } addHistory(entry: HistoryEntry, completed = false, minutes?: number) { this.history = [entry, ...this.history.filter(h => h.chapterId !== entry.chapterId)].slice(0, 500); if (completed && !this.readLog.find(e => e.chapterId === entry.chapterId)) { this.readLog = [...this.readLog, { mangaId: entry.mangaId, chapterId: entry.chapterId, readAt: entry.readAt, minutes: minutes ?? AVG_MIN_PER_CHAPTER }]; const uniqueChapters = new Set(this.readLog.map(e => e.chapterId)); const uniqueManga = new Set(this.readLog.map(e => e.mangaId)); const totalMinutes = this.readLog.reduce((sum, e) => sum + e.minutes, 0); const todayStr = new Date().toISOString().slice(0, 10); const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); const yesterdayStr = yesterday.toISOString().slice(0, 10); const lastDate = this.readingStats.lastStreakDate; const streak = lastDate === todayStr ? this.readingStats.currentStreakDays : lastDate === yesterdayStr ? this.readingStats.currentStreakDays + 1 : 1; this.readingStats = { totalChaptersRead: uniqueChapters.size, totalMangaRead: uniqueManga.size, totalMinutesRead: totalMinutes, firstReadAt: this.readingStats.firstReadAt || entry.readAt, lastReadAt: entry.readAt, currentStreakDays: streak, longestStreakDays: Math.max(this.readingStats.longestStreakDays, streak), lastStreakDate: todayStr, }; const dayKey = new Date().toISOString().slice(0, 10); this.dailyReadCounts = { ...this.dailyReadCounts, [dayKey]: (this.dailyReadCounts[dayKey] ?? 0) + 1 }; } } addBookmark(entry: Omit, label?: string) { this.bookmarks = [{ ...entry, savedAt: Date.now(), label }, ...this.bookmarks.filter(b => b.chapterId !== entry.chapterId)].slice(0, 200); } removeBookmark(chapterId: number) { this.bookmarks = this.bookmarks.filter(b => b.chapterId !== chapterId); } clearBookmarks() { this.bookmarks = []; } getBookmark(chapterId: number) { return this.bookmarks.find(b => b.chapterId === chapterId); } addMarker(entry: Omit): string { const id = Math.random().toString(36).slice(2); this.markers = [...this.markers, { ...entry, id, createdAt: Date.now() }]; return id; } updateMarker(id: string, patch: Partial>) { this.markers = this.markers.map(m => m.id === id ? { ...m, ...patch, updatedAt: Date.now() } : m); } removeMarker(id: string) { this.markers = this.markers.filter(m => m.id !== id); } getMarkersForPage(chapterId: number, page: number) { return this.markers.filter(m => m.chapterId === chapterId && m.pageNumber === page); } getMarkersForChapter(chapterId: number) { return this.markers.filter(m => m.chapterId === chapterId); } getMarkersForManga(mangaId: number) { return this.markers.filter(m => m.mangaId === mangaId); } clearMarkersForManga(mangaId: number) { this.markers = this.markers.filter(m => m.mangaId !== mangaId); } clearHistory() { this.history = []; this.readLog = []; this.dailyReadCounts = {}; } clearHistoryForManga(mangaId: number) { this.history = this.history.filter(x => x.mangaId !== mangaId); this.readLog = this.readLog.filter(x => x.mangaId !== mangaId); this.readingStats = { ...this.readingStats, totalChaptersRead: new Set(this.readLog.map(e => e.chapterId)).size, totalMangaRead: new Set(this.readLog.map(e => e.mangaId)).size, totalMinutesRead: this.readLog.reduce((sum, e) => sum + e.minutes, 0), }; } wipeAllData() { this.history = []; this.readLog = []; this.markers = []; this.dailyReadCounts = {}; this.readingStats = { ...DEFAULT_READING_STATS }; this.settings = { ...this.settings, heroSlots: [null, null, null, null], mangaLinks: {} }; } linkManga(idA: number, idB: number) { if (idA === idB) return; const links = { ...this.settings.mangaLinks }; links[idA] = [...new Set([...(links[idA] ?? []), idB])]; links[idB] = [...new Set([...(links[idB] ?? []), idA])]; this.settings = { ...this.settings, mangaLinks: links }; } unlinkManga(idA: number, idB: number) { const links = { ...this.settings.mangaLinks }; links[idA] = (links[idA] ?? []).filter(id => id !== idB); links[idB] = (links[idB] ?? []).filter(id => id !== idA); if (!links[idA].length) delete links[idA]; if (!links[idB].length) delete links[idB]; this.settings = { ...this.settings, mangaLinks: links }; } getLinkedMangaIds(mangaId: number) { return this.settings.mangaLinks[mangaId] ?? []; } setHeroSlot(index: 1 | 2 | 3, mangaId: number | null) { const slots = [...(this.settings.heroSlots ?? [null, null, null, null])]; slots[index] = mangaId; this.settings = { ...this.settings, heroSlots: slots }; } saveCustomTheme(theme: CustomTheme) { const i = this.settings.customThemes.findIndex(t => t.id === theme.id); this.settings = { ...this.settings, customThemes: i >= 0 ? this.settings.customThemes.map((t, j) => j === i ? theme : t) : [...this.settings.customThemes, theme] }; } deleteCustomTheme(id: string) { this.settings = { ...this.settings, customThemes: this.settings.customThemes.filter(t => t.id !== id), theme: this.settings.theme === id ? "dark" : this.settings.theme }; } toggleHiddenCategory(id: number) { const ids = this.settings.hiddenCategoryIds ?? []; this.settings = { ...this.settings, hiddenCategoryIds: ids.includes(id) ? ids.filter(x => x !== id) : [...ids, id] }; } clearSearchCache() { this.searchCache = new Map(); this.searchLibraryIds = new Set(); this.searchSrcOffset++; } bumpReaderSession() { this.readerSessionId++; } setLibraryUpdates(entries: LibraryUpdateEntry[]) { this.libraryUpdates = entries; this.lastLibraryRefresh = Date.now(); } clearLibraryUpdates() { this.libraryUpdates = []; this.lastLibraryRefresh = 0; this.acknowledgedUpdates = new Set(); } acknowledgeUpdate(mangaId: number) { if (this.acknowledgedUpdates.has(mangaId)) return; this.acknowledgedUpdates = new Set([...this.acknowledgedUpdates, mangaId]); } async checkAndMarkCompleted( mangaId: number, chaps: Chapter[], categories: Category[], gqlFn: (query: string, vars: Record) => Promise, UPDATE_MANGA_CATEGORIES: string, UPDATE_MANGA?: string, mangaStatus?: string, ): Promise { if (!chaps.length || mangaStatus === "ONGOING") return; const completed = categories.find(c => c.name === "Completed"); if (!completed) return; const allRead = chaps.every(c => c.isRead); if (allRead) { await gqlFn(UPDATE_MANGA_CATEGORIES, { mangaId, addTo: [completed.id], removeFrom: [] }).catch(console.error); if (UPDATE_MANGA) await gqlFn(UPDATE_MANGA, { id: mangaId, inLibrary: true }).catch(console.error); } else { await gqlFn(UPDATE_MANGA_CATEGORIES, { mangaId, addTo: [], removeFrom: [completed.id] }).catch(console.error); } } setCategories(cats: Category[]) { this.categories = cats; } setActiveManga(next: Manga | null) { this.activeManga = next; } setPreviewManga(next: Manga | null) { this.previewManga = next; } setActiveSource(next: Source | null) { this.activeSource = next; } setPageUrls(next: string[]) { this.pageUrls = next; } setPageNumber(next: number) { this.pageNumber = next; } setLibraryFilter(next: LibraryFilter) { this.libraryFilter = next; } setLibraryTagFilter(next: string[]) { this.libraryTagFilter = next; } updateSettings(patch: Partial) { this.settings = { ...this.settings, ...patch }; } resetKeybinds() { this.settings = { ...this.settings, keybinds: DEFAULT_KEYBINDS }; } } export const store = new Store(); export function openReader(chapter: Chapter, chapterList: Chapter[], manga?: Manga | null) { store.openReader(chapter, chapterList, manga); } export function closeReader() { store.closeReader(); } export function addHistory(entry: HistoryEntry, completed?: boolean, minutes?: number) { store.addHistory(entry, completed, minutes); } export function clearHistory() { store.clearHistory(); } export function clearHistoryForManga(mangaId: number) { store.clearHistoryForManga(mangaId); } export function wipeAllData() { store.wipeAllData(); } export function linkManga(idA: number, idB: number) { store.linkManga(idA, idB); } export function unlinkManga(idA: number, idB: number) { store.unlinkManga(idA, idB); } export function getLinkedMangaIds(mangaId: number) { return store.getLinkedMangaIds(mangaId); } export function setHeroSlot(i: 1|2|3, mangaId: number | null) { store.setHeroSlot(i, mangaId); } export function setCategories(cats: Category[]) { store.setCategories(cats); } export function setActiveManga(next: Manga | null) { store.setActiveManga(next); } export function setPreviewManga(next: Manga | null) { store.setPreviewManga(next); } export function setActiveSource(next: Source | null) { store.setActiveSource(next); } export function setPageUrls(next: string[]) { store.setPageUrls(next); } export function setPageNumber(next: number) { store.setPageNumber(next); } export function setLibraryFilter(next: LibraryFilter) { store.setLibraryFilter(next); } export function setLibraryTagFilter(next: string[]) { store.setLibraryTagFilter(next); } export function updateSettings(patch: Partial) { store.updateSettings(patch); } export function resetKeybinds() { store.resetKeybinds(); } export function clearSearchCache() { store.clearSearchCache(); } export function setLibraryUpdates(entries: LibraryUpdateEntry[]) { store.setLibraryUpdates(entries); } export function clearLibraryUpdates() { store.clearLibraryUpdates(); } export function acknowledgeUpdate(mangaId: number) { store.acknowledgeUpdate(mangaId); } export function bumpReaderSession() { store.bumpReaderSession(); } export function addBookmark(entry: Omit, label?: string) { store.addBookmark(entry, label); } export function removeBookmark(chapterId: number) { store.removeBookmark(chapterId); } export function clearBookmarks() { store.clearBookmarks(); } export function getBookmark(chapterId: number) { return store.getBookmark(chapterId); } export function addMarker(entry: Omit): string { return store.addMarker(entry); } export function updateMarker(id: string, patch: Partial>) { store.updateMarker(id, patch); } export function removeMarker(id: string) { store.removeMarker(id); } export function getMarkersForPage(chapterId: number, page: number) { return store.getMarkersForPage(chapterId, page); } export function getMarkersForChapter(chapterId: number) { return store.getMarkersForChapter(chapterId); } export function getMarkersForManga(mangaId: number) { return store.getMarkersForManga(mangaId); } export function clearMarkersForManga(mangaId: number) { store.clearMarkersForManga(mangaId); } export function toggleHiddenCategory(id: number) { store.toggleHiddenCategory(id); } export function saveCustomTheme(theme: CustomTheme) { store.saveCustomTheme(theme); } export function deleteCustomTheme(id: string) { store.deleteCustomTheme(id); } export async function checkAndMarkCompleted( mangaId: number, chaps: Chapter[], categories: Category[], gqlFn: (query: string, vars: Record) => Promise, UPDATE_MANGA_CATEGORIES: string, UPDATE_MANGA?: string, mangaStatus?: string, ): Promise { return store.checkAndMarkCompleted(mangaId, chaps, categories, gqlFn, UPDATE_MANGA_CATEGORIES, UPDATE_MANGA, mangaStatus); } export { addToast, dismissToast, setActiveDownloads } from "./notifications.svelte"; export { setNavPage, setSettingsOpen, setSearchPrefill, setGenreFilter } from "./app.svelte";