Feat: Implement Storage-based (JSON) Settings & Data-Storage (WIP) (#56)

This commit is contained in:
Youwes09
2026-04-29 00:18:09 -05:00
parent c009bd71fc
commit 170493aa71
23 changed files with 493 additions and 164 deletions
+2 -1
View File
@@ -6,7 +6,7 @@
import { platform } from "@tauri-apps/plugin-os";
import { store, setActiveDownloads } from "@store/state.svelte";
import { downloadStore } from "@features/downloads/store/downloadState.svelte";
import { boot, startProbe, stopProbe, retryBoot, bypassBoot } from "@store/boot.svelte";
import { boot, initStore, startProbe, stopProbe, retryBoot, bypassBoot } from "@store/boot.svelte";
import { initRpc, setIdle, clearReading, destroyRpc } from "@store/discord";
import { applyTheme } from "@core/theme";
import { applyZoom, mountZoomKey, mountIdleDetection } from "@core/ui";
@@ -100,6 +100,7 @@
});
}
await initStore();
startProbe();
const unlistenDownload = await listen<{ chapterId: number; mangaId: number; progress: number }[]>(
+2
View File
@@ -0,0 +1,2 @@
export { loadAllStores, persistSettings, persistLibrary, persistUpdates } from "./persist";
export type { PersistedData } from "./persist";
+135
View File
@@ -0,0 +1,135 @@
import { LazyStore } from "@tauri-apps/plugin-store";
const settingsStore = new LazyStore("settings.json", { autoSave: false });
const libraryStore = new LazyStore("library.json", { autoSave: false });
const updatesStore = new LazyStore("updates.json", { autoSave: false });
export interface PersistedData {
settings: any;
storeVersion: number | null;
history: any[];
bookmarks: any[];
markers: any[];
readLog: any[];
readingStats: any | null;
dailyReadCounts: Record<string, number>;
libraryUpdates: any[];
lastLibraryRefresh: number;
acknowledgedUpdateIds: number[];
}
export async function loadAllStores(): Promise<PersistedData> {
const migrated = await migrateFromLocalStorage();
if (migrated) return migrated;
const [sv, s, hist, bk, mk, rl, rs, dc, lu, llr, au] = await Promise.all([
settingsStore.get<number>("storeVersion"),
settingsStore.get<any>("settings"),
libraryStore.get<any[]>("history"),
libraryStore.get<any[]>("bookmarks"),
libraryStore.get<any[]>("markers"),
libraryStore.get<any[]>("readLog"),
libraryStore.get<any>("readingStats"),
libraryStore.get<Record<string, number>>("dailyReadCounts"),
updatesStore.get<any[]>("libraryUpdates"),
updatesStore.get<number>("lastLibraryRefresh"),
updatesStore.get<number[]>("acknowledgedUpdateIds"),
]);
return {
storeVersion: sv ?? null,
settings: s ?? null,
history: hist ?? [],
bookmarks: bk ?? [],
markers: mk ?? [],
readLog: rl ?? [],
readingStats: rs ?? null,
dailyReadCounts: dc ?? {},
libraryUpdates: lu ?? [],
lastLibraryRefresh: llr ?? 0,
acknowledgedUpdateIds: au ?? [],
};
}
async function migrateFromLocalStorage(): Promise<PersistedData | null> {
try {
const raw = localStorage.getItem("moku-store");
if (!raw) return null;
const data = JSON.parse(raw);
await Promise.all([
persistSettings({ settings: data.settings ?? null, storeVersion: data.storeVersion ?? 1 }),
persistLibrary({
history: data.history ?? [],
bookmarks: data.bookmarks ?? [],
markers: data.markers ?? [],
readLog: data.readLog ?? [],
readingStats: data.readingStats ?? null,
dailyReadCounts: data.dailyReadCounts ?? {},
}),
persistUpdates({
libraryUpdates: data.libraryUpdates ?? [],
lastLibraryRefresh: data.lastLibraryRefresh ?? 0,
acknowledgedUpdateIds: data.acknowledgedUpdateIds ?? [],
}),
]);
localStorage.removeItem("moku-store");
return {
storeVersion: data.storeVersion ?? null,
settings: data.settings ?? null,
history: data.history ?? [],
bookmarks: data.bookmarks ?? [],
markers: data.markers ?? [],
readLog: data.readLog ?? [],
readingStats: data.readingStats ?? null,
dailyReadCounts: data.dailyReadCounts ?? {},
libraryUpdates: data.libraryUpdates ?? [],
lastLibraryRefresh: data.lastLibraryRefresh ?? 0,
acknowledgedUpdateIds: data.acknowledgedUpdateIds ?? [],
};
} catch {
return null;
}
}
export async function persistSettings(data: { settings: any; storeVersion: number }) {
await Promise.all([
settingsStore.set("settings", data.settings),
settingsStore.set("storeVersion", data.storeVersion),
]);
await settingsStore.save();
}
export async function persistLibrary(data: {
history: any[];
bookmarks: any[];
markers: any[];
readLog: any[];
readingStats: any;
dailyReadCounts: Record<string, number>;
}) {
await Promise.all([
libraryStore.set("history", data.history),
libraryStore.set("bookmarks", data.bookmarks),
libraryStore.set("markers", data.markers),
libraryStore.set("readLog", data.readLog),
libraryStore.set("readingStats", data.readingStats),
libraryStore.set("dailyReadCounts", data.dailyReadCounts),
]);
await libraryStore.save();
}
export async function persistUpdates(data: {
libraryUpdates: any[];
lastLibraryRefresh: number;
acknowledgedUpdateIds: number[];
}) {
await Promise.all([
updatesStore.set("libraryUpdates", data.libraryUpdates),
updatesStore.set("lastLibraryRefresh", data.lastLibraryRefresh),
updatesStore.set("acknowledgedUpdateIds", data.acknowledgedUpdateIds),
]);
await updatesStore.save();
}
+10 -4
View File
@@ -1,6 +1,7 @@
import { store } from "@store/state.svelte";
import { probeServer, loginBasic } from "@core/auth";
import { trackingState } from "@features/tracking/store/trackingState.svelte";
import { store } from "@store/state.svelte";
import { probeServer, loginBasic } from "@core/auth";
import { trackingState } from "@features/tracking/store/trackingState.svelte";
import { loadAllStores } from "@core/persistence/persist";
const MAX_ATTEMPTS = 40;
@@ -18,6 +19,11 @@ export const boot = $state({
let probeGeneration = 0;
export async function initStore() {
const saved = await loadAllStores();
store.hydrate(saved);
}
export function startProbe() {
const gen = ++probeGeneration;
boot.failed = false;
@@ -109,4 +115,4 @@ export function bypassBoot(onReady: () => void) {
boot.loginRequired = false;
boot.unsupportedMode = false;
onReady();
}
}
+58 -44
View File
@@ -8,6 +8,8 @@ import { DEFAULT_SETTINGS } from "../t
import { DEFAULT_READING_STATS } from "../types/history";
import { notifications } from "./notifications.svelte";
import { app } from "./app.svelte";
import { persistSettings, persistLibrary, persistUpdates } from "../core/persistence/persist";
import type { PersistedData } from "../core/persistence/persist";
export type { NavPage } from "./app.svelte";
export type { Toast, ActiveDownload } from "./notifications.svelte";
@@ -27,29 +29,6 @@ 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<string, unknown>) {
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<Settings> = {};
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,
@@ -73,7 +52,7 @@ function mergeSettings(saved: any): Settings {
}
class Store {
settings: Settings = $state(mergeSettings(saved));
settings: Settings = $state(mergeSettings(null));
activeManga: Manga | null = $state(null);
previewManga: Manga | null = $state(null);
activeChapter: Chapter | null = $state(null);
@@ -84,19 +63,22 @@ class Store {
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<string, number> = $state(saved?.dailyReadCounts ?? {});
history: HistoryEntry[] = $state([]);
bookmarks: BookmarkEntry[]= $state([]);
markers: MarkerEntry[] = $state([]);
readLog: ReadLogEntry[] = $state([]);
readingStats: ReadingStats = $state({ ...DEFAULT_READING_STATS });
dailyReadCounts: Record<string, number> = $state({});
searchCache: Map<string, any> = $state(new Map());
searchLibraryIds: Set<number> = $state(new Set());
searchSrcOffset: number = $state(0);
readerSessionId: number = $state(0);
libraryUpdates: LibraryUpdateEntry[] = $state(saved?.libraryUpdates ?? []);
lastLibraryRefresh: number = $state(saved?.lastLibraryRefresh ?? 0);
acknowledgedUpdates: Set<number> = $state(new Set(saved?.acknowledgedUpdateIds ?? []));
libraryUpdates: LibraryUpdateEntry[] = $state([]);
lastLibraryRefresh: number = $state(0);
acknowledgedUpdates: Set<number> = $state(new Set());
isFullscreen: boolean = $state(false);
#ready = false;
get toasts() { return notifications.toasts; }
get activeDownloads() { return notifications.activeDownloads; }
@@ -109,19 +91,51 @@ class Store {
get genreFilter() { return app.genreFilter; }
set genreFilter(v) { app.setGenreFilter(v); }
constructor() {
hydrate(saved: PersistedData) {
if (this.#ready) return;
if ((saved.storeVersion ?? 1) < STORE_VERSION && saved.settings) {
for (const key of RESET_ON_UPGRADE)
(saved.settings as any)[key] = (DEFAULT_SETTINGS as any)[key];
}
this.settings = mergeSettings(saved);
this.history = saved.history ?? [];
this.bookmarks = saved.bookmarks ?? [];
this.markers = saved.markers ?? [];
this.readLog = saved.readLog ?? [];
this.readingStats = saved.readingStats ?? { ...DEFAULT_READING_STATS };
this.dailyReadCounts = saved.dailyReadCounts ?? {};
this.libraryUpdates = saved.libraryUpdates ?? [];
this.lastLibraryRefresh = saved.lastLibraryRefresh ?? 0;
this.acknowledgedUpdates = new Set(saved.acknowledgedUpdateIds ?? []);
this.#ready = true;
$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,
});
const s = this.settings;
if (!this.#ready) return;
persistSettings({ settings: s, storeVersion: STORE_VERSION });
});
$effect(() => {
const h = this.history;
const bk = this.bookmarks;
const mk = this.markers;
const rl = this.readLog;
const rs = this.readingStats;
const dc = this.dailyReadCounts;
if (!this.#ready) return;
persistLibrary({ history: h, bookmarks: bk, markers: mk, readLog: rl, readingStats: rs, dailyReadCounts: dc });
});
$effect(() => {
const lu = this.libraryUpdates;
const llr = this.lastLibraryRefresh;
const au = this.acknowledgedUpdates;
if (!this.#ready) return;
persistUpdates({ libraryUpdates: lu, lastLibraryRefresh: llr, acknowledgedUpdateIds: [...au] });
});
});
}