generated from tpl/obsidian-sample-plugin
			Improve reading log entry modal by grabbing metadata of selected book and updating pages remaining
This commit is contained in:
		
							parent
							
								
									c603475f69
								
							
						
					
					
						commit
						ac10cf646f
					
				| 
						 | 
				
			
			@ -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(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
		},
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 });
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue