Improve reading log entry modal by grabbing metadata of selected book and updating pages remaining

This commit is contained in:
Evan Fiordeliso 2025-07-09 17:43:41 -04:00
parent c603475f69
commit ac10cf646f
8 changed files with 114 additions and 38 deletions

View File

@ -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(

View File

@ -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() {

View File

@ -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<T>(
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 @@
/>
<label for="pagesRemaining">Pages Remaining</label>
<input
type="number"
type="text"
inputmode="numeric"
pattern="[0-9]*"
name="pagesRemaining"
id="pagesRemaining"
bind:value={pagesRemaining}

View File

@ -1,7 +1,11 @@
import { STATUS_READ } from "@src/const";
import type { CachedMetadata, TFile } from "obsidian";
import { getContext, setContext } from "svelte";
import { getSettingsContext } from "./settings.svelte";
import {
createSettings,
getSettingsContext,
setSettingsContext,
} from "./settings.svelte";
import type BookTrackerPlugin from "@src/main";
import { createDateFilter, type DateFilterStore } from "./date-filter.svelte";
import type { ReadingState } from "@src/types";
@ -26,14 +30,17 @@ export interface MetadataStore extends DateFilterStore {
function getMetadata(
plugin: BookTrackerPlugin,
settings: BookTrackerPluginSettings,
state: ReadingState
state: ReadingState | null
): FileMetadata[] {
const metadata: FileMetadata[] = [];
for (const file of plugin.app.vault.getMarkdownFiles()) {
const frontmatter =
plugin.app.metadataCache.getFileCache(file)?.frontmatter ?? {};
if (frontmatter[settings.statusProperty] !== state) {
if (
!(settings.statusProperty in frontmatter) ||
(state !== null && frontmatter[settings.statusProperty] !== state)
) {
continue;
}
@ -44,10 +51,15 @@ function getMetadata(
export function createMetadata(
plugin: BookTrackerPlugin,
statusFilter: ReadingState = STATUS_READ,
statusFilter: ReadingState | null = STATUS_READ,
initialMonth?: boolean
): MetadataStore {
const settingsStore = getSettingsContext();
let settingsStore = getSettingsContext();
if (!settingsStore) {
settingsStore = createSettings(plugin);
setSettingsContext(settingsStore);
}
const initialMetadata = getMetadata(
plugin,
settingsStore.settings,

View File

@ -0,0 +1,20 @@
export interface PreviousState<T> {
get value(): T | undefined;
}
export function createPrevious<T>(getter: () => T): PreviousState<T> {
let previous: T | undefined = $state();
let current: T = $state(getter());
$effect(() => {
const newValue = getter();
previous = current;
current = newValue;
});
return {
get value() {
return previous;
},
};
}

View File

@ -8,6 +8,7 @@ export interface ReadingLogStore extends DateFilterStore {
addEntry(entry: ReadingLogEntry): Promise<void>;
updateEntry(entry: ReadingLogEntry): Promise<void>;
removeEntry(entry: ReadingLogEntry): Promise<void>;
load(): Promise<void>;
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();

View File

@ -19,6 +19,8 @@ interface ReadingLogEventMap {
removed: { entry: ReadingLogEntry };
}
const DEFAULT_FILENAME = "reading-log.json";
export class ReadingLog extends EventEmitter<ReadingLogEventMap> {
private entries: ReadingLogEntry[] = [];
@ -28,9 +30,17 @@ export class ReadingLog extends EventEmitter<ReadingLogEventMap> {
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<ReadingLogEntry[]>(
filename
);
@ -50,7 +60,7 @@ export class ReadingLog extends EventEmitter<ReadingLogEventMap> {
);
}
async save(filename = "reading-log.json") {
async save(filename = DEFAULT_FILENAME) {
this.sortEntries();
await this.storage.writeJSON(
filename,

View File

@ -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<StorageEventMap> {
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 });
}
}