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;
|
$effect(() => {
|
||||||
});
|
pagesReadTotal = Math.max(pagesReadTotal, pagesRead);
|
||||||
}
|
});
|
||||||
|
|
||||||
watch(
|
$effect(() => {
|
||||||
() => pagesRead,
|
pagesRemaining = Math.max(pagesRemaining, 0);
|
||||||
(prev) => {
|
});
|
||||||
if (prev !== pagesRead && prev !== undefined) {
|
|
||||||
const diff = pagesRead - prev;
|
|
||||||
pagesReadTotal = pagesReadTotal + diff;
|
|
||||||
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