diff --git a/src/main.ts b/src/main.ts index 397aa88..917bba1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -36,7 +36,7 @@ export default class BookTrackerPlugin extends Plugin { await this.loadSettings(); this.templater = new Templater(this.app); - this.storage = new Storage(this.app, this); + this.storage = new Storage(this); this.readingLog = new ReadingLog(this.storage); this.addCommand( diff --git a/src/ui/code-blocks/ReadingLogCodeBlockView.svelte b/src/ui/code-blocks/ReadingLogCodeBlockView.svelte index fd75f4c..4228865 100644 --- a/src/ui/code-blocks/ReadingLogCodeBlockView.svelte +++ b/src/ui/code-blocks/ReadingLogCodeBlockView.svelte @@ -5,7 +5,7 @@ import type BookTrackerPlugin from "@src/main"; import { createReadingLog } from "@ui/stores/reading-log.svelte"; import { ALL_TIME } from "@ui/stores/date-filter.svelte"; - import { onDestroy } from "svelte"; + import { onDestroy, onMount } from "svelte"; import OpenFileLink from "@ui/components/OpenFileLink.svelte"; import { setAppContext } from "@ui/stores/app"; @@ -17,6 +17,7 @@ setAppContext(plugin.app); const store = createReadingLog(plugin.readingLog); + onMount(() => store.load()); onDestroy(() => store.destroy()); function createEntry() { diff --git a/src/ui/modals/ReadingLogEntryEditModalView.svelte b/src/ui/modals/ReadingLogEntryEditModalView.svelte index b1a5df5..6b8adf5 100644 --- a/src/ui/modals/ReadingLogEntryEditModalView.svelte +++ b/src/ui/modals/ReadingLogEntryEditModalView.svelte @@ -3,6 +3,12 @@ import type { ReadingLogEntry } from "@utils/ReadingLog"; import FileSuggest from "@ui/components/suggesters/FileSuggest.svelte"; import { v4 as uuidv4 } from "uuid"; + import { createPrevious } from "@ui/stores/previous.svelte"; + import { createMetadata } from "@ui/stores/metadata.svelte"; + import { + createSettings, + setSettingsContext, + } from "@ui/stores/settings.svelte"; const INPUT_DATETIME_FORMAT = "YYYY-MM-DDTHH:mm"; @@ -14,9 +20,18 @@ let { plugin, entry, onSubmit }: Props = $props(); + const settingsStore = createSettings(plugin); + setSettingsContext(settingsStore); + + const metadataStore = createMetadata(plugin, null); + let editMode = $derived(entry !== undefined); let book = $state(entry?.book ?? ""); + let bookMetadata = $derived( + metadataStore.metadata.find((m) => m.file.basename === book), + ); let pagesRead = $state(entry?.pagesRead ?? 0); + let pagesReadPrev = createPrevious(() => pagesRead); let pagesReadTotal = $state(entry?.pagesReadTotal ?? 0); let pagesRemaining = $state(entry?.pagesRemaining ?? 0); let createdAt = $state( @@ -25,32 +40,26 @@ moment().format(INPUT_DATETIME_FORMAT), ); - // Source: https://github.com/sveltejs/svelte/discussions/14220#discussioncomment-11188219 - function watch( - getter: () => T, - effectCallback: (t: T | undefined) => void, - ) { - let previous: T | undefined = undefined; + $effect(() => { + const diff = pagesRead - (pagesReadPrev.value ?? 0); + pagesRead = pagesRead; + pagesReadTotal = pagesReadTotal + diff; + }); - $effect(() => { - const current = getter(); // add $state.snapshot for deep reactivity - const cleanup = effectCallback(previous); - previous = current; + $effect(() => { + pagesRemaining = + (bookMetadata?.frontmatter?.[ + settingsStore.settings.pageCountProperty + ] ?? 0) - pagesReadTotal; + }); - return cleanup; - }); - } + $effect(() => { + pagesReadTotal = Math.max(pagesReadTotal, pagesRead); + }); - watch( - () => pagesRead, - (prev) => { - if (prev !== pagesRead && prev !== undefined) { - const diff = pagesRead - prev; - pagesReadTotal = pagesReadTotal + diff; - pagesRemaining = pagesRemaining - diff; - } - }, - ); + $effect(() => { + pagesRemaining = Math.max(pagesRemaining, 0); + }); function onsubmit(ev: SubmitEvent) { ev.preventDefault(); @@ -95,7 +104,9 @@ /> { + get value(): T | undefined; +} + +export function createPrevious(getter: () => T): PreviousState { + let previous: T | undefined = $state(); + let current: T = $state(getter()); + + $effect(() => { + const newValue = getter(); + previous = current; + current = newValue; + }); + + return { + get value() { + return previous; + }, + }; +} diff --git a/src/ui/stores/reading-log.svelte.ts b/src/ui/stores/reading-log.svelte.ts index 5f5c980..dc95da5 100644 --- a/src/ui/stores/reading-log.svelte.ts +++ b/src/ui/stores/reading-log.svelte.ts @@ -8,6 +8,7 @@ export interface ReadingLogStore extends DateFilterStore { addEntry(entry: ReadingLogEntry): Promise; updateEntry(entry: ReadingLogEntry): Promise; removeEntry(entry: ReadingLogEntry): Promise; + load(): Promise; destroy(): void; } @@ -91,6 +92,9 @@ export function createReadingLog(readingLog: ReadingLog): ReadingLogStore { addEntry, updateEntry, removeEntry, + async load() { + await readingLog.load(); + }, destroy() { loadHandler.off(); createdHandler.off(); diff --git a/src/utils/ReadingLog.ts b/src/utils/ReadingLog.ts index 90bff0b..50f164c 100644 --- a/src/utils/ReadingLog.ts +++ b/src/utils/ReadingLog.ts @@ -19,6 +19,8 @@ interface ReadingLogEventMap { removed: { entry: ReadingLogEntry }; } +const DEFAULT_FILENAME = "reading-log.json"; + export class ReadingLog extends EventEmitter { private entries: ReadingLogEntry[] = []; @@ -28,9 +30,17 @@ export class ReadingLog extends EventEmitter { this.load().catch((error) => { console.error("Failed to load reading log entries:", error); }); + + storage.on("change", ({ path }) => { + if (path === DEFAULT_FILENAME) { + this.load().catch((error) => { + console.error("Failed to load reading log entries:", error); + }); + } + }); } - async load(filename = "reading-log.json") { + async load(filename = DEFAULT_FILENAME) { const entries = await this.storage.readJSON( filename ); @@ -50,7 +60,7 @@ export class ReadingLog extends EventEmitter { ); } - async save(filename = "reading-log.json") { + async save(filename = DEFAULT_FILENAME) { this.sortEntries(); await this.storage.writeJSON( filename, diff --git a/src/utils/Storage.ts b/src/utils/Storage.ts index 36219ea..cb87b2f 100644 --- a/src/utils/Storage.ts +++ b/src/utils/Storage.ts @@ -1,13 +1,23 @@ import BookTrackerPlugin from "@src/main"; import { App, normalizePath } from "obsidian"; +import { EventEmitter } from "./event"; -export class Storage { - public constructor( - private readonly app: App, - private readonly plugin: BookTrackerPlugin - ) {} +interface StorageEventMap { + change: { path: string }; +} +export class Storage extends EventEmitter { + private readonly app: App = this.plugin.app; private readonly baseDir = this.plugin.manifest.dir!; + + public constructor(private readonly plugin: BookTrackerPlugin) { + super(); + plugin.registerEvent( + // @ts-expect-error "raw" event is an internal api + this.app.vault.on("raw", this.fileChangeHandler.bind(this)) + ); + } + private getFilePath(filename: string): string { return normalizePath(`${this.baseDir}/${filename}`.replace(/\/$/, "")); } @@ -44,4 +54,12 @@ export class Storage { const files = await this.app.vault.adapter.list(this.baseDir); return files.folders; } + + private fileChangeHandler(path: string) { + if (!path.startsWith(this.baseDir)) return; + + path = path.replace(this.baseDir + "/", ""); + + this.emit("change", { path }); + } }