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;
|
||||
|
||||
return cleanup;
|
||||
pagesRemaining =
|
||||
(bookMetadata?.frontmatter?.[
|
||||
settingsStore.settings.pageCountProperty
|
||||
] ?? 0) - pagesReadTotal;
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => pagesRead,
|
||||
(prev) => {
|
||||
if (prev !== pagesRead && prev !== undefined) {
|
||||
const diff = pagesRead - prev;
|
||||
pagesReadTotal = pagesReadTotal + diff;
|
||||
pagesRemaining = pagesRemaining - diff;
|
||||
}
|
||||
},
|
||||
);
|
||||
$effect(() => {
|
||||
pagesReadTotal = Math.max(pagesReadTotal, pagesRead);
|
||||
});
|
||||
|
||||
$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