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();
 | 
							await this.loadSettings();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.templater = new Templater(this.app);
 | 
							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.readingLog = new ReadingLog(this.storage);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.addCommand(
 | 
							this.addCommand(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@
 | 
				
			||||||
	import type BookTrackerPlugin from "@src/main";
 | 
						import type BookTrackerPlugin from "@src/main";
 | 
				
			||||||
	import { createReadingLog } from "@ui/stores/reading-log.svelte";
 | 
						import { createReadingLog } from "@ui/stores/reading-log.svelte";
 | 
				
			||||||
	import { ALL_TIME } from "@ui/stores/date-filter.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 OpenFileLink from "@ui/components/OpenFileLink.svelte";
 | 
				
			||||||
	import { setAppContext } from "@ui/stores/app";
 | 
						import { setAppContext } from "@ui/stores/app";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,6 +17,7 @@
 | 
				
			||||||
	setAppContext(plugin.app);
 | 
						setAppContext(plugin.app);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const store = createReadingLog(plugin.readingLog);
 | 
						const store = createReadingLog(plugin.readingLog);
 | 
				
			||||||
 | 
						onMount(() => store.load());
 | 
				
			||||||
	onDestroy(() => store.destroy());
 | 
						onDestroy(() => store.destroy());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function createEntry() {
 | 
						function createEntry() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,12 @@
 | 
				
			||||||
	import type { ReadingLogEntry } from "@utils/ReadingLog";
 | 
						import type { ReadingLogEntry } from "@utils/ReadingLog";
 | 
				
			||||||
	import FileSuggest from "@ui/components/suggesters/FileSuggest.svelte";
 | 
						import FileSuggest from "@ui/components/suggesters/FileSuggest.svelte";
 | 
				
			||||||
	import { v4 as uuidv4 } from "uuid";
 | 
						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";
 | 
						const INPUT_DATETIME_FORMAT = "YYYY-MM-DDTHH:mm";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,9 +20,18 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let { plugin, entry, onSubmit }: Props = $props();
 | 
						let { plugin, entry, onSubmit }: Props = $props();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const settingsStore = createSettings(plugin);
 | 
				
			||||||
 | 
						setSettingsContext(settingsStore);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const metadataStore = createMetadata(plugin, null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let editMode = $derived(entry !== undefined);
 | 
						let editMode = $derived(entry !== undefined);
 | 
				
			||||||
	let book = $state(entry?.book ?? "");
 | 
						let book = $state(entry?.book ?? "");
 | 
				
			||||||
 | 
						let bookMetadata = $derived(
 | 
				
			||||||
 | 
							metadataStore.metadata.find((m) => m.file.basename === book),
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
	let pagesRead = $state(entry?.pagesRead ?? 0);
 | 
						let pagesRead = $state(entry?.pagesRead ?? 0);
 | 
				
			||||||
 | 
						let pagesReadPrev = createPrevious(() => pagesRead);
 | 
				
			||||||
	let pagesReadTotal = $state(entry?.pagesReadTotal ?? 0);
 | 
						let pagesReadTotal = $state(entry?.pagesReadTotal ?? 0);
 | 
				
			||||||
	let pagesRemaining = $state(entry?.pagesRemaining ?? 0);
 | 
						let pagesRemaining = $state(entry?.pagesRemaining ?? 0);
 | 
				
			||||||
	let createdAt = $state(
 | 
						let createdAt = $state(
 | 
				
			||||||
| 
						 | 
					@ -25,32 +40,26 @@
 | 
				
			||||||
			moment().format(INPUT_DATETIME_FORMAT),
 | 
								moment().format(INPUT_DATETIME_FORMAT),
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Source: https://github.com/sveltejs/svelte/discussions/14220#discussioncomment-11188219
 | 
						$effect(() => {
 | 
				
			||||||
	function watch<T>(
 | 
							const diff = pagesRead - (pagesReadPrev.value ?? 0);
 | 
				
			||||||
		getter: () => T,
 | 
							pagesRead = pagesRead;
 | 
				
			||||||
		effectCallback: (t: T | undefined) => void,
 | 
							pagesReadTotal = pagesReadTotal + diff;
 | 
				
			||||||
	) {
 | 
						});
 | 
				
			||||||
		let previous: T | undefined = undefined;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	$effect(() => {
 | 
						$effect(() => {
 | 
				
			||||||
			const current = getter(); // add $state.snapshot for deep reactivity
 | 
							pagesRemaining =
 | 
				
			||||||
			const cleanup = effectCallback(previous);
 | 
								(bookMetadata?.frontmatter?.[
 | 
				
			||||||
			previous = current;
 | 
									settingsStore.settings.pageCountProperty
 | 
				
			||||||
 | 
								] ?? 0) - pagesReadTotal;
 | 
				
			||||||
			return cleanup;
 | 
					 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	watch(
 | 
						$effect(() => {
 | 
				
			||||||
		() => pagesRead,
 | 
							pagesReadTotal = Math.max(pagesReadTotal, pagesRead);
 | 
				
			||||||
		(prev) => {
 | 
						});
 | 
				
			||||||
			if (prev !== pagesRead && prev !== undefined) {
 | 
					
 | 
				
			||||||
				const diff = pagesRead - prev;
 | 
						$effect(() => {
 | 
				
			||||||
				pagesReadTotal = pagesReadTotal + diff;
 | 
							pagesRemaining = Math.max(pagesRemaining, 0);
 | 
				
			||||||
				pagesRemaining = pagesRemaining - diff;
 | 
						});
 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function onsubmit(ev: SubmitEvent) {
 | 
						function onsubmit(ev: SubmitEvent) {
 | 
				
			||||||
		ev.preventDefault();
 | 
							ev.preventDefault();
 | 
				
			||||||
| 
						 | 
					@ -95,7 +104,9 @@
 | 
				
			||||||
			/>
 | 
								/>
 | 
				
			||||||
			<label for="pagesRemaining">Pages Remaining</label>
 | 
								<label for="pagesRemaining">Pages Remaining</label>
 | 
				
			||||||
			<input
 | 
								<input
 | 
				
			||||||
				type="number"
 | 
									type="text"
 | 
				
			||||||
 | 
									inputmode="numeric"
 | 
				
			||||||
 | 
									pattern="[0-9]*"
 | 
				
			||||||
				name="pagesRemaining"
 | 
									name="pagesRemaining"
 | 
				
			||||||
				id="pagesRemaining"
 | 
									id="pagesRemaining"
 | 
				
			||||||
				bind:value={pagesRemaining}
 | 
									bind:value={pagesRemaining}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,11 @@
 | 
				
			||||||
import { STATUS_READ } from "@src/const";
 | 
					import { STATUS_READ } from "@src/const";
 | 
				
			||||||
import type { CachedMetadata, TFile } from "obsidian";
 | 
					import type { CachedMetadata, TFile } from "obsidian";
 | 
				
			||||||
import { getContext, setContext } from "svelte";
 | 
					import { getContext, setContext } from "svelte";
 | 
				
			||||||
import { getSettingsContext } from "./settings.svelte";
 | 
					import {
 | 
				
			||||||
 | 
						createSettings,
 | 
				
			||||||
 | 
						getSettingsContext,
 | 
				
			||||||
 | 
						setSettingsContext,
 | 
				
			||||||
 | 
					} from "./settings.svelte";
 | 
				
			||||||
import type BookTrackerPlugin from "@src/main";
 | 
					import type BookTrackerPlugin from "@src/main";
 | 
				
			||||||
import { createDateFilter, type DateFilterStore } from "./date-filter.svelte";
 | 
					import { createDateFilter, type DateFilterStore } from "./date-filter.svelte";
 | 
				
			||||||
import type { ReadingState } from "@src/types";
 | 
					import type { ReadingState } from "@src/types";
 | 
				
			||||||
| 
						 | 
					@ -26,14 +30,17 @@ export interface MetadataStore extends DateFilterStore {
 | 
				
			||||||
function getMetadata(
 | 
					function getMetadata(
 | 
				
			||||||
	plugin: BookTrackerPlugin,
 | 
						plugin: BookTrackerPlugin,
 | 
				
			||||||
	settings: BookTrackerPluginSettings,
 | 
						settings: BookTrackerPluginSettings,
 | 
				
			||||||
	state: ReadingState
 | 
						state: ReadingState | null
 | 
				
			||||||
): FileMetadata[] {
 | 
					): FileMetadata[] {
 | 
				
			||||||
	const metadata: FileMetadata[] = [];
 | 
						const metadata: FileMetadata[] = [];
 | 
				
			||||||
	for (const file of plugin.app.vault.getMarkdownFiles()) {
 | 
						for (const file of plugin.app.vault.getMarkdownFiles()) {
 | 
				
			||||||
		const frontmatter =
 | 
							const frontmatter =
 | 
				
			||||||
			plugin.app.metadataCache.getFileCache(file)?.frontmatter ?? {};
 | 
								plugin.app.metadataCache.getFileCache(file)?.frontmatter ?? {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (frontmatter[settings.statusProperty] !== state) {
 | 
							if (
 | 
				
			||||||
 | 
								!(settings.statusProperty in frontmatter) ||
 | 
				
			||||||
 | 
								(state !== null && frontmatter[settings.statusProperty] !== state)
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
			continue;
 | 
								continue;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,10 +51,15 @@ function getMetadata(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function createMetadata(
 | 
					export function createMetadata(
 | 
				
			||||||
	plugin: BookTrackerPlugin,
 | 
						plugin: BookTrackerPlugin,
 | 
				
			||||||
	statusFilter: ReadingState = STATUS_READ,
 | 
						statusFilter: ReadingState | null = STATUS_READ,
 | 
				
			||||||
	initialMonth?: boolean
 | 
						initialMonth?: boolean
 | 
				
			||||||
): MetadataStore {
 | 
					): MetadataStore {
 | 
				
			||||||
	const settingsStore = getSettingsContext();
 | 
						let settingsStore = getSettingsContext();
 | 
				
			||||||
 | 
						if (!settingsStore) {
 | 
				
			||||||
 | 
							settingsStore = createSettings(plugin);
 | 
				
			||||||
 | 
							setSettingsContext(settingsStore);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const initialMetadata = getMetadata(
 | 
						const initialMetadata = getMetadata(
 | 
				
			||||||
		plugin,
 | 
							plugin,
 | 
				
			||||||
		settingsStore.settings,
 | 
							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>;
 | 
						addEntry(entry: ReadingLogEntry): Promise<void>;
 | 
				
			||||||
	updateEntry(entry: ReadingLogEntry): Promise<void>;
 | 
						updateEntry(entry: ReadingLogEntry): Promise<void>;
 | 
				
			||||||
	removeEntry(entry: ReadingLogEntry): Promise<void>;
 | 
						removeEntry(entry: ReadingLogEntry): Promise<void>;
 | 
				
			||||||
 | 
						load(): Promise<void>;
 | 
				
			||||||
	destroy(): void;
 | 
						destroy(): void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -91,6 +92,9 @@ export function createReadingLog(readingLog: ReadingLog): ReadingLogStore {
 | 
				
			||||||
		addEntry,
 | 
							addEntry,
 | 
				
			||||||
		updateEntry,
 | 
							updateEntry,
 | 
				
			||||||
		removeEntry,
 | 
							removeEntry,
 | 
				
			||||||
 | 
							async load() {
 | 
				
			||||||
 | 
								await readingLog.load();
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		destroy() {
 | 
							destroy() {
 | 
				
			||||||
			loadHandler.off();
 | 
								loadHandler.off();
 | 
				
			||||||
			createdHandler.off();
 | 
								createdHandler.off();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,8 @@ interface ReadingLogEventMap {
 | 
				
			||||||
	removed: { entry: ReadingLogEntry };
 | 
						removed: { entry: ReadingLogEntry };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DEFAULT_FILENAME = "reading-log.json";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ReadingLog extends EventEmitter<ReadingLogEventMap> {
 | 
					export class ReadingLog extends EventEmitter<ReadingLogEventMap> {
 | 
				
			||||||
	private entries: ReadingLogEntry[] = [];
 | 
						private entries: ReadingLogEntry[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,9 +30,17 @@ export class ReadingLog extends EventEmitter<ReadingLogEventMap> {
 | 
				
			||||||
		this.load().catch((error) => {
 | 
							this.load().catch((error) => {
 | 
				
			||||||
			console.error("Failed to load reading log entries:", 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[]>(
 | 
							const entries = await this.storage.readJSON<ReadingLogEntry[]>(
 | 
				
			||||||
			filename
 | 
								filename
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
| 
						 | 
					@ -50,7 +60,7 @@ export class ReadingLog extends EventEmitter<ReadingLogEventMap> {
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async save(filename = "reading-log.json") {
 | 
						async save(filename = DEFAULT_FILENAME) {
 | 
				
			||||||
		this.sortEntries();
 | 
							this.sortEntries();
 | 
				
			||||||
		await this.storage.writeJSON(
 | 
							await this.storage.writeJSON(
 | 
				
			||||||
			filename,
 | 
								filename,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,23 @@
 | 
				
			||||||
import BookTrackerPlugin from "@src/main";
 | 
					import BookTrackerPlugin from "@src/main";
 | 
				
			||||||
import { App, normalizePath } from "obsidian";
 | 
					import { App, normalizePath } from "obsidian";
 | 
				
			||||||
 | 
					import { EventEmitter } from "./event";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class Storage {
 | 
					interface StorageEventMap {
 | 
				
			||||||
	public constructor(
 | 
						change: { path: string };
 | 
				
			||||||
		private readonly app: App,
 | 
					}
 | 
				
			||||||
		private readonly plugin: BookTrackerPlugin
 | 
					 | 
				
			||||||
	) {}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Storage extends EventEmitter<StorageEventMap> {
 | 
				
			||||||
 | 
						private readonly app: App = this.plugin.app;
 | 
				
			||||||
	private readonly baseDir = this.plugin.manifest.dir!;
 | 
						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 {
 | 
						private getFilePath(filename: string): string {
 | 
				
			||||||
		return normalizePath(`${this.baseDir}/${filename}`.replace(/\/$/, ""));
 | 
							return normalizePath(`${this.baseDir}/${filename}`.replace(/\/$/, ""));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -44,4 +54,12 @@ export class Storage {
 | 
				
			||||||
		const files = await this.app.vault.adapter.list(this.baseDir);
 | 
							const files = await this.app.vault.adapter.list(this.baseDir);
 | 
				
			||||||
		return files.folders;
 | 
							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