generated from tpl/obsidian-sample-plugin
			Extract book properties into object
This commit is contained in:
		
							parent
							
								
									19d56652eb
								
							
						
					
					
						commit
						8356b6649f
					
				| 
						 | 
					@ -1,5 +1,6 @@
 | 
				
			||||||
import type { ReadingLog } from "@utils/ReadingLog";
 | 
					import type { ReadingLog } from "@utils/ReadingLog";
 | 
				
			||||||
import { Command } from "./Command";
 | 
					import { Command } from "./Command";
 | 
				
			||||||
 | 
					import moment from "@external/moment";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class BackupReadingLogCommand extends Command {
 | 
					export class BackupReadingLogCommand extends Command {
 | 
				
			||||||
	constructor(private readonly readingLog: ReadingLog) {
 | 
						constructor(private readonly readingLog: ReadingLog) {
 | 
				
			||||||
| 
						 | 
					@ -7,7 +8,6 @@ export class BackupReadingLogCommand extends Command {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async callback() {
 | 
						async callback() {
 | 
				
			||||||
		// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
		const timestamp = moment().format("YYYY-MM-DD_HH-mm-ss");
 | 
							const timestamp = moment().format("YYYY-MM-DD_HH-mm-ss");
 | 
				
			||||||
		const backupFilename = `reading-log-backup_${timestamp}.json`;
 | 
							const backupFilename = `reading-log-backup_${timestamp}.json`;
 | 
				
			||||||
		await this.readingLog.save(backupFilename);
 | 
							await this.readingLog.save(backupFilename);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,32 +2,23 @@ import {
 | 
				
			||||||
	type Editor,
 | 
						type Editor,
 | 
				
			||||||
	type MarkdownView,
 | 
						type MarkdownView,
 | 
				
			||||||
	type MarkdownFileInfo,
 | 
						type MarkdownFileInfo,
 | 
				
			||||||
	type App,
 | 
					 | 
				
			||||||
	Notice,
 | 
						Notice,
 | 
				
			||||||
	TFile,
 | 
						TFile,
 | 
				
			||||||
} from "obsidian";
 | 
					} from "obsidian";
 | 
				
			||||||
import { EditorCheckCommand } from "./Command";
 | 
					import { EditorCheckCommand } from "./Command";
 | 
				
			||||||
import type { BookTrackerPluginSettings } from "@ui/settings";
 | 
					 | 
				
			||||||
import { RatingModal } from "@ui/modals";
 | 
					import { RatingModal } from "@ui/modals";
 | 
				
			||||||
import type { ReadingLog } from "@utils/ReadingLog";
 | 
					 | 
				
			||||||
import { STATUS_READ } from "@src/const";
 | 
					import { STATUS_READ } from "@src/const";
 | 
				
			||||||
import { mkdirRecursive, dirname } from "@utils/fs";
 | 
					import { mkdirRecursive, dirname } from "@utils/fs";
 | 
				
			||||||
 | 
					import moment from "@external/moment";
 | 
				
			||||||
 | 
					import type BookTrackerPlugin from "@src/main";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class LogReadingFinishedCommand extends EditorCheckCommand {
 | 
					export class LogReadingFinishedCommand extends EditorCheckCommand {
 | 
				
			||||||
	constructor(
 | 
						constructor(private readonly plugin: BookTrackerPlugin) {
 | 
				
			||||||
		private readonly app: App,
 | 
					 | 
				
			||||||
		private readonly readingLog: ReadingLog,
 | 
					 | 
				
			||||||
		private readonly settings: BookTrackerPluginSettings
 | 
					 | 
				
			||||||
	) {
 | 
					 | 
				
			||||||
		super("log-reading-finished", "Log Reading Finished");
 | 
							super("log-reading-finished", "Log Reading Finished");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private getPageCount(file: TFile): number {
 | 
						private getPageCount(file: TFile): number {
 | 
				
			||||||
		return (
 | 
							return this.plugin.getBookMetadata(file)?.pageCount ?? 0;
 | 
				
			||||||
			(this.app.metadataCache.getFileCache(file)?.frontmatter?.[
 | 
					 | 
				
			||||||
				this.settings.pageCountProperty
 | 
					 | 
				
			||||||
			] as number | undefined) ?? 0
 | 
					 | 
				
			||||||
		);
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	protected check(
 | 
						protected check(
 | 
				
			||||||
| 
						 | 
					@ -36,10 +27,10 @@ export class LogReadingFinishedCommand extends EditorCheckCommand {
 | 
				
			||||||
	): boolean {
 | 
						): boolean {
 | 
				
			||||||
		return !(
 | 
							return !(
 | 
				
			||||||
			ctx.file === null ||
 | 
								ctx.file === null ||
 | 
				
			||||||
			this.settings.statusProperty === "" ||
 | 
								this.plugin.settings.statusProperty === "" ||
 | 
				
			||||||
			this.settings.endDateProperty === "" ||
 | 
								this.plugin.settings.endDateProperty === "" ||
 | 
				
			||||||
			this.settings.ratingProperty === "" ||
 | 
								this.plugin.settings.ratingProperty === "" ||
 | 
				
			||||||
			this.settings.pageCountProperty === "" ||
 | 
								this.plugin.settings.pageCountProperty === "" ||
 | 
				
			||||||
			this.getPageCount(ctx.file) <= 0
 | 
								this.getPageCount(ctx.file) <= 0
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -53,12 +44,16 @@ export class LogReadingFinishedCommand extends EditorCheckCommand {
 | 
				
			||||||
		const pageCount = this.getPageCount(file);
 | 
							const pageCount = this.getPageCount(file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const ratings = await RatingModal.createAndOpen(
 | 
							const ratings = await RatingModal.createAndOpen(
 | 
				
			||||||
			this.app,
 | 
								this.plugin.app,
 | 
				
			||||||
			this.settings.spiceProperty !== ""
 | 
								this.plugin.settings.spiceProperty !== ""
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			await this.readingLog.createEntry(fileName, pageCount, pageCount);
 | 
								await this.plugin.readingLog.createEntry(
 | 
				
			||||||
 | 
									fileName,
 | 
				
			||||||
 | 
									pageCount,
 | 
				
			||||||
 | 
									pageCount
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
		} catch (error) {
 | 
							} catch (error) {
 | 
				
			||||||
			new Notice(
 | 
								new Notice(
 | 
				
			||||||
				`Failed to log reading progress for ${fileName}: ${error}`
 | 
									`Failed to log reading progress for ${fileName}: ${error}`
 | 
				
			||||||
| 
						 | 
					@ -66,25 +61,23 @@ export class LogReadingFinishedCommand extends EditorCheckCommand {
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
		const endDate = moment().format("YYYY-MM-DD");
 | 
							const endDate = moment().format("YYYY-MM-DD");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.app.fileManager.processFrontMatter(file, (frontMatter) => {
 | 
							this.plugin.app.fileManager.processFrontMatter(file, (fm) => {
 | 
				
			||||||
			frontMatter[this.settings.statusProperty] = STATUS_READ;
 | 
								fm[this.plugin.settings.statusProperty] = STATUS_READ;
 | 
				
			||||||
			frontMatter[this.settings.endDateProperty] = endDate;
 | 
								fm[this.plugin.settings.endDateProperty] = endDate;
 | 
				
			||||||
			frontMatter[this.settings.ratingProperty] = ratings.rating;
 | 
								fm[this.plugin.settings.ratingProperty] = ratings.rating;
 | 
				
			||||||
			if (this.settings.spiceProperty !== "") {
 | 
								if (this.plugin.settings.spiceProperty !== "") {
 | 
				
			||||||
				frontMatter[this.settings.spiceProperty] = ratings.spice;
 | 
									fm[this.plugin.settings.spiceProperty] = ratings.spice;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (this.settings.organizeReadBooks) {
 | 
							if (this.plugin.settings.organizeReadBooks) {
 | 
				
			||||||
			// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
			const datePath = moment().format("YYYY/MMMM");
 | 
								const datePath = moment().format("YYYY/MMMM");
 | 
				
			||||||
			const newPath = `${this.settings.readBooksFolder}/${datePath}/${file.name}`;
 | 
								const newPath = `${this.plugin.settings.readBooksFolder}/${datePath}/${file.name}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			await mkdirRecursive(this.app.vault, dirname(newPath));
 | 
								await mkdirRecursive(this.plugin.app.vault, dirname(newPath));
 | 
				
			||||||
			await this.app.vault.rename(file, newPath);
 | 
								await this.plugin.app.vault.rename(file, newPath);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		new Notice("Reading finished for " + fileName);
 | 
							new Notice("Reading finished for " + fileName);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,30 +2,20 @@ import {
 | 
				
			||||||
	type Editor,
 | 
						type Editor,
 | 
				
			||||||
	type MarkdownView,
 | 
						type MarkdownView,
 | 
				
			||||||
	type MarkdownFileInfo,
 | 
						type MarkdownFileInfo,
 | 
				
			||||||
	type App,
 | 
					 | 
				
			||||||
	Notice,
 | 
						Notice,
 | 
				
			||||||
	TFile,
 | 
						TFile,
 | 
				
			||||||
} from "obsidian";
 | 
					} from "obsidian";
 | 
				
			||||||
import { EditorCheckCommand } from "./Command";
 | 
					import { EditorCheckCommand } from "./Command";
 | 
				
			||||||
import type { BookTrackerPluginSettings } from "@ui/settings";
 | 
					 | 
				
			||||||
import { ReadingProgressModal } from "@ui/modals";
 | 
					import { ReadingProgressModal } from "@ui/modals";
 | 
				
			||||||
import type { ReadingLog } from "@utils/ReadingLog";
 | 
					import type BookTrackerPlugin from "@src/main";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class LogReadingProgressCommand extends EditorCheckCommand {
 | 
					export class LogReadingProgressCommand extends EditorCheckCommand {
 | 
				
			||||||
	constructor(
 | 
						constructor(private readonly plugin: BookTrackerPlugin) {
 | 
				
			||||||
		private readonly app: App,
 | 
					 | 
				
			||||||
		private readonly readingLog: ReadingLog,
 | 
					 | 
				
			||||||
		private readonly settings: BookTrackerPluginSettings
 | 
					 | 
				
			||||||
	) {
 | 
					 | 
				
			||||||
		super("log-reading-progress", "Log Reading Progress");
 | 
							super("log-reading-progress", "Log Reading Progress");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private getPageCount(file: TFile): number {
 | 
						private getPageCount(file: TFile): number {
 | 
				
			||||||
		return (
 | 
							return this.plugin.getBookMetadata(file)?.pageCount ?? 0;
 | 
				
			||||||
			(this.app.metadataCache.getFileCache(file)?.frontmatter?.[
 | 
					 | 
				
			||||||
				this.settings.pageCountProperty
 | 
					 | 
				
			||||||
			] as number | undefined) ?? 0
 | 
					 | 
				
			||||||
		);
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	protected check(
 | 
						protected check(
 | 
				
			||||||
| 
						 | 
					@ -34,7 +24,7 @@ export class LogReadingProgressCommand extends EditorCheckCommand {
 | 
				
			||||||
	): boolean {
 | 
						): boolean {
 | 
				
			||||||
		return !(
 | 
							return !(
 | 
				
			||||||
			ctx.file === null ||
 | 
								ctx.file === null ||
 | 
				
			||||||
			this.settings.pageCountProperty === "" ||
 | 
								this.plugin.settings.pageCountProperty === "" ||
 | 
				
			||||||
			this.getPageCount(ctx.file) <= 0
 | 
								this.getPageCount(ctx.file) <= 0
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -47,7 +37,7 @@ export class LogReadingProgressCommand extends EditorCheckCommand {
 | 
				
			||||||
		const fileName = file.basename;
 | 
							const fileName = file.basename;
 | 
				
			||||||
		const pageCount = this.getPageCount(file);
 | 
							const pageCount = this.getPageCount(file);
 | 
				
			||||||
		const pageNumber = await ReadingProgressModal.createAndOpen(
 | 
							const pageNumber = await ReadingProgressModal.createAndOpen(
 | 
				
			||||||
			this.app,
 | 
								this.plugin.app,
 | 
				
			||||||
			pageCount
 | 
								pageCount
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,7 +49,11 @@ export class LogReadingProgressCommand extends EditorCheckCommand {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			await this.readingLog.createEntry(fileName, pageNumber, pageCount);
 | 
								await this.plugin.readingLog.createEntry(
 | 
				
			||||||
 | 
									fileName,
 | 
				
			||||||
 | 
									pageNumber,
 | 
				
			||||||
 | 
									pageCount
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
		} catch (error) {
 | 
							} catch (error) {
 | 
				
			||||||
			new Notice(
 | 
								new Notice(
 | 
				
			||||||
				`Failed to log reading progress for ${fileName}: ${error}`
 | 
									`Failed to log reading progress for ${fileName}: ${error}`
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,7 @@ import {
 | 
				
			||||||
import { EditorCheckCommand } from "./Command";
 | 
					import { EditorCheckCommand } from "./Command";
 | 
				
			||||||
import type { BookTrackerPluginSettings } from "@ui/settings";
 | 
					import type { BookTrackerPluginSettings } from "@ui/settings";
 | 
				
			||||||
import { STATUS_IN_PROGRESS } from "@src/const";
 | 
					import { STATUS_IN_PROGRESS } from "@src/const";
 | 
				
			||||||
 | 
					import moment from "@external/moment";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class LogReadingStartedCommand extends EditorCheckCommand {
 | 
					export class LogReadingStartedCommand extends EditorCheckCommand {
 | 
				
			||||||
	constructor(
 | 
						constructor(
 | 
				
			||||||
| 
						 | 
					@ -34,12 +35,11 @@ export class LogReadingStartedCommand extends EditorCheckCommand {
 | 
				
			||||||
	): Promise<void> {
 | 
						): Promise<void> {
 | 
				
			||||||
		const file = ctx.file!;
 | 
							const file = ctx.file!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
		const startDate = moment().format("YYYY-MM-DD");
 | 
							const startDate = moment().format("YYYY-MM-DD");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.app.fileManager.processFrontMatter(file, (frontMatter) => {
 | 
							this.app.fileManager.processFrontMatter(file, (fm) => {
 | 
				
			||||||
			frontMatter[this.settings.statusProperty] = STATUS_IN_PROGRESS;
 | 
								fm[this.settings.statusProperty] = STATUS_IN_PROGRESS;
 | 
				
			||||||
			frontMatter[this.settings.startDateProperty] = startDate;
 | 
								fm[this.settings.startDateProperty] = startDate;
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		new Notice("Reading started for " + file.basename);
 | 
							new Notice("Reading started for " + file.basename);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,10 +37,10 @@ export class ResetReadingStatusCommand extends EditorCheckCommand {
 | 
				
			||||||
	): void | Promise<void> {
 | 
						): void | Promise<void> {
 | 
				
			||||||
		const file = ctx.file!;
 | 
							const file = ctx.file!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.app.fileManager.processFrontMatter(file, (frontMatter) => {
 | 
							this.app.fileManager.processFrontMatter(file, (fm) => {
 | 
				
			||||||
			frontMatter[this.settings.statusProperty] = STATUS_TO_BE_READ;
 | 
								fm[this.settings.statusProperty] = STATUS_TO_BE_READ;
 | 
				
			||||||
			frontMatter[this.settings.startDateProperty] = "";
 | 
								fm[this.settings.startDateProperty] = "";
 | 
				
			||||||
			frontMatter[this.settings.endDateProperty] = "";
 | 
								fm[this.settings.endDateProperty] = "";
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.readingLog.deleteEntriesForBook(file.basename);
 | 
							this.readingLog.deleteEntriesForBook(file.basename);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					export type { Moment } from "moment";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// @ts-expect-error Moment is provided by Obsidian
 | 
				
			||||||
 | 
					export default moment;
 | 
				
			||||||
							
								
								
									
										95
									
								
								src/main.ts
								
								
								
								
							
							
						
						
									
										95
									
								
								src/main.ts
								
								
								
								
							| 
						 | 
					@ -1,4 +1,10 @@
 | 
				
			||||||
import { Notice, Plugin, requestUrl, TFile } from "obsidian";
 | 
					import {
 | 
				
			||||||
 | 
						Notice,
 | 
				
			||||||
 | 
						Plugin,
 | 
				
			||||||
 | 
						requestUrl,
 | 
				
			||||||
 | 
						TFile,
 | 
				
			||||||
 | 
						type FrontMatterCache,
 | 
				
			||||||
 | 
					} from "obsidian";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
	type BookTrackerPluginSettings,
 | 
						type BookTrackerPluginSettings,
 | 
				
			||||||
	DEFAULT_SETTINGS,
 | 
						DEFAULT_SETTINGS,
 | 
				
			||||||
| 
						 | 
					@ -12,7 +18,7 @@ import {
 | 
				
			||||||
	registerReadingLogCodeBlockProcessor,
 | 
						registerReadingLogCodeBlockProcessor,
 | 
				
			||||||
	registerReadingStatsCodeBlockProcessor,
 | 
						registerReadingStatsCodeBlockProcessor,
 | 
				
			||||||
} from "@ui/code-blocks";
 | 
					} from "@ui/code-blocks";
 | 
				
			||||||
import type { Book } from "./types";
 | 
					import type { Book, BookMetadata, ReadingState } from "./types";
 | 
				
			||||||
import { SearchGoodreadsCommand } from "@commands/SearchGoodreadsCommand";
 | 
					import { SearchGoodreadsCommand } from "@commands/SearchGoodreadsCommand";
 | 
				
			||||||
import { LogReadingStartedCommand } from "@commands/LogReadingStartedCommand";
 | 
					import { LogReadingStartedCommand } from "@commands/LogReadingStartedCommand";
 | 
				
			||||||
import { LogReadingProgressCommand } from "@commands/LogReadingProgressCommand";
 | 
					import { LogReadingProgressCommand } from "@commands/LogReadingProgressCommand";
 | 
				
			||||||
| 
						 | 
					@ -26,6 +32,7 @@ import { registerShelfCodeBlockProcessor } from "@ui/code-blocks/ShelfCodeBlock"
 | 
				
			||||||
import { ReloadReadingLogCommand } from "@commands/ReloadReadingLogCommand";
 | 
					import { ReloadReadingLogCommand } from "@commands/ReloadReadingLogCommand";
 | 
				
			||||||
import { registerReadingCalendarCodeBlockProcessor } from "@ui/code-blocks/ReadingCalendarCodeBlock";
 | 
					import { registerReadingCalendarCodeBlockProcessor } from "@ui/code-blocks/ReadingCalendarCodeBlock";
 | 
				
			||||||
import { registerAToZChallengeCodeBlockProcessor } from "@ui/code-blocks/AToZChallengeCodeBlock";
 | 
					import { registerAToZChallengeCodeBlockProcessor } from "@ui/code-blocks/AToZChallengeCodeBlock";
 | 
				
			||||||
 | 
					import moment from "@external/moment";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class BookTrackerPlugin extends Plugin {
 | 
					export default class BookTrackerPlugin extends Plugin {
 | 
				
			||||||
	public settings: BookTrackerPluginSettings;
 | 
						public settings: BookTrackerPluginSettings;
 | 
				
			||||||
| 
						 | 
					@ -49,20 +56,8 @@ export default class BookTrackerPlugin extends Plugin {
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
		this.addCommand(new LogReadingStartedCommand(this.app, this.settings));
 | 
							this.addCommand(new LogReadingStartedCommand(this.app, this.settings));
 | 
				
			||||||
		this.addCommand(
 | 
							this.addCommand(new LogReadingProgressCommand(this));
 | 
				
			||||||
			new LogReadingProgressCommand(
 | 
							this.addCommand(new LogReadingFinishedCommand(this));
 | 
				
			||||||
				this.app,
 | 
					 | 
				
			||||||
				this.readingLog,
 | 
					 | 
				
			||||||
				this.settings
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
		);
 | 
					 | 
				
			||||||
		this.addCommand(
 | 
					 | 
				
			||||||
			new LogReadingFinishedCommand(
 | 
					 | 
				
			||||||
				this.app,
 | 
					 | 
				
			||||||
				this.readingLog,
 | 
					 | 
				
			||||||
				this.settings
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
		);
 | 
					 | 
				
			||||||
		this.addCommand(
 | 
							this.addCommand(
 | 
				
			||||||
			new ResetReadingStatusCommand(
 | 
								new ResetReadingStatusCommand(
 | 
				
			||||||
				this.app,
 | 
									this.app,
 | 
				
			||||||
| 
						 | 
					@ -178,4 +173,72 @@ export default class BookTrackerPlugin extends Plugin {
 | 
				
			||||||
		const file = await this.app.vault.create(filePath, renderedContent);
 | 
							const file = await this.app.vault.create(filePath, renderedContent);
 | 
				
			||||||
		await this.app.workspace.getLeaf().openFile(file);
 | 
							await this.app.workspace.getLeaf().openFile(file);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						getBookMetadata(file: TFile): BookMetadata | null {
 | 
				
			||||||
 | 
							const metadata = this.app.metadataCache.getFileCache(file);
 | 
				
			||||||
 | 
							if (!metadata) {
 | 
				
			||||||
 | 
								return null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return this.frontmatterToMetadata(metadata.frontmatter);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						frontmatterToMetadata(fm: FrontMatterCache | undefined): BookMetadata {
 | 
				
			||||||
 | 
							const getString = (key: string) => {
 | 
				
			||||||
 | 
								const value = fm?.[key];
 | 
				
			||||||
 | 
								if (typeof value === "string") {
 | 
				
			||||||
 | 
									return value;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return "";
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const getStringArray = (key: string) => {
 | 
				
			||||||
 | 
								const value = fm?.[key];
 | 
				
			||||||
 | 
								if (Array.isArray(value)) {
 | 
				
			||||||
 | 
									return value as string[];
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return [];
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const getNumber = (key: string) => {
 | 
				
			||||||
 | 
								const value = fm?.[key];
 | 
				
			||||||
 | 
								if (typeof value === "number") {
 | 
				
			||||||
 | 
									return value;
 | 
				
			||||||
 | 
								} else if (typeof value === "string") {
 | 
				
			||||||
 | 
									return parseFloat(value);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return 0;
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const getDate = (key: string) => {
 | 
				
			||||||
 | 
								const value = fm?.[key];
 | 
				
			||||||
 | 
								if (typeof value === "string" || value instanceof Date) {
 | 
				
			||||||
 | 
									return moment(value);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return null;
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								title: getString(this.settings.titleProperty),
 | 
				
			||||||
 | 
								subtitle: getString(this.settings.subtitleProperty),
 | 
				
			||||||
 | 
								description: getString(this.settings.descriptionProperty),
 | 
				
			||||||
 | 
								authors: getStringArray(this.settings.authorsProperty),
 | 
				
			||||||
 | 
								seriesTitle: getString(this.settings.seriesTitleProperty),
 | 
				
			||||||
 | 
								seriesPosition: getNumber(this.settings.seriesPositionProperty),
 | 
				
			||||||
 | 
								startDate: getDate(this.settings.startDateProperty)!,
 | 
				
			||||||
 | 
								endDate: getDate(this.settings.endDateProperty)!,
 | 
				
			||||||
 | 
								status: getString(this.settings.statusProperty) as ReadingState,
 | 
				
			||||||
 | 
								rating: getNumber(this.settings.ratingProperty),
 | 
				
			||||||
 | 
								spice: getNumber(this.settings.spiceProperty),
 | 
				
			||||||
 | 
								format: getString(this.settings.formatProperty),
 | 
				
			||||||
 | 
								source: getStringArray(this.settings.sourceProperty),
 | 
				
			||||||
 | 
								categories: getStringArray(this.settings.categoriesProperty),
 | 
				
			||||||
 | 
								publisher: getString(this.settings.publisherProperty),
 | 
				
			||||||
 | 
								publishDate: getDate(this.settings.publishDateProperty)!,
 | 
				
			||||||
 | 
								pageCount: getNumber(this.settings.pageCountProperty),
 | 
				
			||||||
 | 
								isbn: getString(this.settings.isbnProperty),
 | 
				
			||||||
 | 
								coverImageUrl: getString(this.settings.coverImageUrlProperty),
 | 
				
			||||||
 | 
								localCoverPath: getString(this.settings.localCoverPathProperty),
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										35
									
								
								src/types.ts
								
								
								
								
							
							
						
						
									
										35
									
								
								src/types.ts
								
								
								
								
							| 
						 | 
					@ -1,12 +1,10 @@
 | 
				
			||||||
import type {
 | 
					import moment from "@external/moment";
 | 
				
			||||||
	STATUS_IN_PROGRESS,
 | 
					import { STATUS_IN_PROGRESS, STATUS_READ, STATUS_TO_BE_READ } from "./const";
 | 
				
			||||||
	STATUS_READ,
 | 
					import z from "zod/v4";
 | 
				
			||||||
	STATUS_TO_BE_READ,
 | 
					 | 
				
			||||||
} from "./const";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Author {
 | 
					export interface Author {
 | 
				
			||||||
	name: string;
 | 
						name: string;
 | 
				
			||||||
	description: string;
 | 
						description?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Series {
 | 
					export interface Series {
 | 
				
			||||||
| 
						 | 
					@ -33,3 +31,28 @@ export type InProgressState = typeof STATUS_IN_PROGRESS;
 | 
				
			||||||
export type ReadState = typeof STATUS_READ;
 | 
					export type ReadState = typeof STATUS_READ;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ReadingState = ToBeReadState | InProgressState | ReadState;
 | 
					export type ReadingState = ToBeReadState | InProgressState | ReadState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const BookMetadataSchema = z.object({
 | 
				
			||||||
 | 
						title: z.string(),
 | 
				
			||||||
 | 
						subtitle: z.string(),
 | 
				
			||||||
 | 
						description: z.string(),
 | 
				
			||||||
 | 
						authors: z.array(z.string()),
 | 
				
			||||||
 | 
						seriesTitle: z.string(),
 | 
				
			||||||
 | 
						seriesPosition: z.number(),
 | 
				
			||||||
 | 
						startDate: z.date().transform((date) => moment(date)),
 | 
				
			||||||
 | 
						endDate: z.date().transform((date) => moment(date)),
 | 
				
			||||||
 | 
						status: z.enum([STATUS_TO_BE_READ, STATUS_IN_PROGRESS, STATUS_READ]),
 | 
				
			||||||
 | 
						rating: z.number(),
 | 
				
			||||||
 | 
						spice: z.number(),
 | 
				
			||||||
 | 
						format: z.string(),
 | 
				
			||||||
 | 
						source: z.array(z.string()),
 | 
				
			||||||
 | 
						categories: z.array(z.string()),
 | 
				
			||||||
 | 
						publisher: z.string(),
 | 
				
			||||||
 | 
						publishDate: z.date().transform((date) => moment(date)),
 | 
				
			||||||
 | 
						pageCount: z.number(),
 | 
				
			||||||
 | 
						isbn: z.string(),
 | 
				
			||||||
 | 
						coverImageUrl: z.string(),
 | 
				
			||||||
 | 
						localCoverPath: z.string(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type BookMetadata = z.infer<typeof BookMetadataSchema>;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,6 @@ import { registerCodeBlockRenderer } from ".";
 | 
				
			||||||
import { SvelteCodeBlockRenderer } from "./SvelteCodeBlockRenderer";
 | 
					import { SvelteCodeBlockRenderer } from "./SvelteCodeBlockRenderer";
 | 
				
			||||||
import AToZChallengeCodeBlockView from "./AToZChallengeCodeBlockView.svelte";
 | 
					import AToZChallengeCodeBlockView from "./AToZChallengeCodeBlockView.svelte";
 | 
				
			||||||
import type BookTrackerPlugin from "@src/main";
 | 
					import type BookTrackerPlugin from "@src/main";
 | 
				
			||||||
import z from "zod/v4";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function registerAToZChallengeCodeBlockProcessor(
 | 
					export function registerAToZChallengeCodeBlockProcessor(
 | 
				
			||||||
	plugin: BookTrackerPlugin
 | 
						plugin: BookTrackerPlugin
 | 
				
			||||||
| 
						 | 
					@ -19,8 +18,3 @@ export function registerAToZChallengeCodeBlockProcessor(
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const AToZChallengeSettingsSchema = z.object({
 | 
					 | 
				
			||||||
	coverProperty: z.string(),
 | 
					 | 
				
			||||||
	titleProperty: z.string(),
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,17 +4,16 @@
 | 
				
			||||||
		setSettingsContext,
 | 
							setSettingsContext,
 | 
				
			||||||
	} from "@ui/stores/settings.svelte";
 | 
						} from "@ui/stores/settings.svelte";
 | 
				
			||||||
	import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer";
 | 
						import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer";
 | 
				
			||||||
	import { createMetadata } from "@ui/stores/metadata.svelte";
 | 
						import {
 | 
				
			||||||
 | 
							createMetadata,
 | 
				
			||||||
 | 
							type FileMetadata,
 | 
				
			||||||
 | 
						} from "@ui/stores/metadata.svelte";
 | 
				
			||||||
	import { STATUS_READ } from "@src/const";
 | 
						import { STATUS_READ } from "@src/const";
 | 
				
			||||||
	import { ALL_TIME } from "@ui/stores/date-filter.svelte";
 | 
					 | 
				
			||||||
	import { AToZChallengeSettingsSchema } from "./AToZChallengeCodeBlock";
 | 
					 | 
				
			||||||
	import { parseYaml, TFile } from "obsidian";
 | 
					 | 
				
			||||||
	import OpenFileLink from "@ui/components/OpenFileLink.svelte";
 | 
						import OpenFileLink from "@ui/components/OpenFileLink.svelte";
 | 
				
			||||||
	import type { Moment } from "moment";
 | 
						import DateFilter from "@ui/components/DateFilter.svelte";
 | 
				
			||||||
 | 
						import BookCover from "@ui/components/BookCover.svelte";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { plugin, source }: SvelteCodeBlockProps = $props();
 | 
						const { plugin }: SvelteCodeBlockProps = $props();
 | 
				
			||||||
 | 
					 | 
				
			||||||
	const settings = AToZChallengeSettingsSchema.parse(parseYaml(source));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const settingsStore = createSettings(plugin);
 | 
						const settingsStore = createSettings(plugin);
 | 
				
			||||||
	setSettingsContext(settingsStore);
 | 
						setSettingsContext(settingsStore);
 | 
				
			||||||
| 
						 | 
					@ -39,10 +38,10 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
 | 
						const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const items = $derived(
 | 
						const metadata = $derived(
 | 
				
			||||||
		metadataStore.metadata.reduce(
 | 
							metadataStore.metadata.reduce(
 | 
				
			||||||
			(acc, item) => {
 | 
								(acc, meta) => {
 | 
				
			||||||
				const title = item.frontmatter[settings.titleProperty];
 | 
									const title = meta.book.title;
 | 
				
			||||||
				const firstLetter = getSortValue(title).charAt(0).toUpperCase();
 | 
									const firstLetter = getSortValue(title).charAt(0).toUpperCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (!firstLetter.match(/[A-Z]/)) {
 | 
									if (!firstLetter.match(/[A-Z]/)) {
 | 
				
			||||||
| 
						 | 
					@ -50,60 +49,23 @@
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (!acc[firstLetter]) {
 | 
									if (!acc[firstLetter]) {
 | 
				
			||||||
					const coverPath = item.frontmatter[
 | 
										acc[firstLetter] = meta;
 | 
				
			||||||
						settings.coverProperty
 | 
					 | 
				
			||||||
					] as string;
 | 
					 | 
				
			||||||
					const coverFile = plugin.app.vault.getFileByPath(coverPath);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					let coverSrc: string = "";
 | 
					 | 
				
			||||||
					if (coverFile) {
 | 
					 | 
				
			||||||
						coverSrc = plugin.app.vault.getResourcePath(coverFile);
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					const coverAlt = item.frontmatter[settings.titleProperty];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					acc[firstLetter] = {
 | 
					 | 
				
			||||||
						file: item.file,
 | 
					 | 
				
			||||||
						// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
						startDate: moment(
 | 
					 | 
				
			||||||
							item.frontmatter[
 | 
					 | 
				
			||||||
								settingsStore.settings.startDateProperty
 | 
					 | 
				
			||||||
							],
 | 
					 | 
				
			||||||
						),
 | 
					 | 
				
			||||||
						// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
						endDate: moment(
 | 
					 | 
				
			||||||
							item.frontmatter[
 | 
					 | 
				
			||||||
								settingsStore.settings.endDateProperty
 | 
					 | 
				
			||||||
							],
 | 
					 | 
				
			||||||
						),
 | 
					 | 
				
			||||||
						coverSrc,
 | 
					 | 
				
			||||||
						coverAlt,
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				return acc;
 | 
									return acc;
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			{} as Record<
 | 
								{} as Record<string, FileMetadata>,
 | 
				
			||||||
				string,
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					file: TFile;
 | 
					 | 
				
			||||||
					startDate: Moment;
 | 
					 | 
				
			||||||
					endDate: Moment;
 | 
					 | 
				
			||||||
					coverSrc: string;
 | 
					 | 
				
			||||||
					coverAlt: string;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			>,
 | 
					 | 
				
			||||||
		),
 | 
							),
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const startDate = $derived(
 | 
						const startDate = $derived(
 | 
				
			||||||
		Object.values(items)
 | 
							Object.values(metadata)
 | 
				
			||||||
			.map((item) => item.startDate)
 | 
								.map((meta) => meta.book.startDate)
 | 
				
			||||||
			.sort((a, b) => a.diff(b))[0],
 | 
								.sort((a, b) => a.diff(b))[0],
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const endDate = $derived.by(() => {
 | 
						const endDate = $derived.by(() => {
 | 
				
			||||||
		const dates = Object.values(items)
 | 
							const dates = Object.values(metadata)
 | 
				
			||||||
			.map((item) => item.endDate)
 | 
								.map((meta) => meta.book.endDate)
 | 
				
			||||||
			.sort((a, b) => b.diff(a));
 | 
								.sort((a, b) => b.diff(a));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (dates.length !== 26) {
 | 
							if (dates.length !== 26) {
 | 
				
			||||||
| 
						 | 
					@ -116,21 +78,17 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="reading-bingo">
 | 
					<div class="reading-bingo">
 | 
				
			||||||
	<div class="top-info">
 | 
						<div class="top-info">
 | 
				
			||||||
		<select class="year-filter" bind:value={metadataStore.filterYear}>
 | 
							<DateFilter store={metadataStore} disableMonthFilter disableAllTime />
 | 
				
			||||||
			{#each metadataStore.filterYears as year}
 | 
					 | 
				
			||||||
				<option value={year}>{year}</option>
 | 
					 | 
				
			||||||
			{/each}
 | 
					 | 
				
			||||||
		</select>
 | 
					 | 
				
			||||||
		<p>Started: {startDate.format("YYYY-MM-DD")}</p>
 | 
							<p>Started: {startDate.format("YYYY-MM-DD")}</p>
 | 
				
			||||||
		<p>Ended: {endDate?.format("YYYY-MM-DD") ?? "N/A"}</p>
 | 
							<p>Ended: {endDate?.format("YYYY-MM-DD") ?? "N/A"}</p>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<div class="bingo">
 | 
						<div class="bingo">
 | 
				
			||||||
		{#each alphabet as letter}
 | 
							{#each alphabet as letter}
 | 
				
			||||||
			<div class="bingo-item">
 | 
								<div class="bingo-item">
 | 
				
			||||||
				{#if items[letter]}
 | 
									{#if metadata[letter]}
 | 
				
			||||||
					{@const item = items[letter]}
 | 
										{@const meta = metadata[letter]}
 | 
				
			||||||
					<OpenFileLink file={item.file}>
 | 
										<OpenFileLink file={meta.file}>
 | 
				
			||||||
						<img src={item.coverSrc} alt={item.coverAlt} />
 | 
											<BookCover app={plugin.app} book={meta.book} />
 | 
				
			||||||
					</OpenFileLink>
 | 
										</OpenFileLink>
 | 
				
			||||||
				{:else}
 | 
									{:else}
 | 
				
			||||||
					<div class="placeholder">{letter}</div>
 | 
										<div class="placeholder">{letter}</div>
 | 
				
			||||||
| 
						 | 
					@ -182,7 +140,7 @@
 | 
				
			||||||
					font-style: italic;
 | 
										font-style: italic;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				img {
 | 
									:global(img) {
 | 
				
			||||||
					width: 100%;
 | 
										width: 100%;
 | 
				
			||||||
					height: 100%;
 | 
										height: 100%;
 | 
				
			||||||
					object-fit: cover;
 | 
										object-fit: cover;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,6 @@ import { registerCodeBlockRenderer } from ".";
 | 
				
			||||||
import { SvelteCodeBlockRenderer } from "./SvelteCodeBlockRenderer";
 | 
					import { SvelteCodeBlockRenderer } from "./SvelteCodeBlockRenderer";
 | 
				
			||||||
import ReadingCalendarCodeBlockView from "./ReadingCalendarCodeBlockView.svelte";
 | 
					import ReadingCalendarCodeBlockView from "./ReadingCalendarCodeBlockView.svelte";
 | 
				
			||||||
import type BookTrackerPlugin from "@src/main";
 | 
					import type BookTrackerPlugin from "@src/main";
 | 
				
			||||||
import z from "zod/v4";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function registerReadingCalendarCodeBlockProcessor(
 | 
					export function registerReadingCalendarCodeBlockProcessor(
 | 
				
			||||||
	plugin: BookTrackerPlugin
 | 
						plugin: BookTrackerPlugin
 | 
				
			||||||
| 
						 | 
					@ -19,7 +18,3 @@ export function registerReadingCalendarCodeBlockProcessor(
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const ReadingCalendarSettingsSchema = z.object({
 | 
					 | 
				
			||||||
	coverProperty: z.string(),
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,5 @@
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
	import { parseYaml, TFile } from "obsidian";
 | 
						import { TFile } from "obsidian";
 | 
				
			||||||
	import { ReadingCalendarSettingsSchema } from "./ReadingCalendarCodeBlock";
 | 
					 | 
				
			||||||
	import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer";
 | 
						import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer";
 | 
				
			||||||
	import {
 | 
						import {
 | 
				
			||||||
		createSettings,
 | 
							createSettings,
 | 
				
			||||||
| 
						 | 
					@ -9,6 +8,7 @@
 | 
				
			||||||
	import {
 | 
						import {
 | 
				
			||||||
		createMetadata,
 | 
							createMetadata,
 | 
				
			||||||
		setMetadataContext,
 | 
							setMetadataContext,
 | 
				
			||||||
 | 
							type FileMetadata,
 | 
				
			||||||
	} from "@ui/stores/metadata.svelte";
 | 
						} from "@ui/stores/metadata.svelte";
 | 
				
			||||||
	import {
 | 
						import {
 | 
				
			||||||
		createReadingLog,
 | 
							createReadingLog,
 | 
				
			||||||
| 
						 | 
					@ -17,10 +17,10 @@
 | 
				
			||||||
	import { ArrowLeft, ArrowRight } from "lucide-svelte";
 | 
						import { ArrowLeft, ArrowRight } from "lucide-svelte";
 | 
				
			||||||
	import { onDestroy, onMount } from "svelte";
 | 
						import { onDestroy, onMount } from "svelte";
 | 
				
			||||||
	import OpenFileLink from "@ui/components/OpenFileLink.svelte";
 | 
						import OpenFileLink from "@ui/components/OpenFileLink.svelte";
 | 
				
			||||||
 | 
						import moment from "@external/moment";
 | 
				
			||||||
 | 
						import BookCover from "@ui/components/BookCover.svelte";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { plugin, source }: SvelteCodeBlockProps = $props();
 | 
						const { plugin }: SvelteCodeBlockProps = $props();
 | 
				
			||||||
 | 
					 | 
				
			||||||
	const settings = ReadingCalendarSettingsSchema.parse(parseYaml(source));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const settingsStore = createSettings(plugin);
 | 
						const settingsStore = createSettings(plugin);
 | 
				
			||||||
	setSettingsContext(settingsStore);
 | 
						setSettingsContext(settingsStore);
 | 
				
			||||||
| 
						 | 
					@ -66,17 +66,14 @@
 | 
				
			||||||
		"Saturday",
 | 
							"Saturday",
 | 
				
			||||||
	];
 | 
						];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
	let today = $state(moment());
 | 
						let today = $state(moment());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function msUntilMidnight() {
 | 
						function msUntilMidnight() {
 | 
				
			||||||
		// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
		return moment().endOf("day").diff(today, "milliseconds");
 | 
							return moment().endOf("day").diff(today, "milliseconds");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function updateToday() {
 | 
						function updateToday() {
 | 
				
			||||||
		setTimeout(() => {
 | 
							setTimeout(() => {
 | 
				
			||||||
			// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
			today = moment();
 | 
								today = moment();
 | 
				
			||||||
			updateToday();
 | 
								updateToday();
 | 
				
			||||||
		}, msUntilMidnight() + 1000);
 | 
							}, msUntilMidnight() + 1000);
 | 
				
			||||||
| 
						 | 
					@ -92,14 +89,12 @@
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const weeks = $derived.by(() => {
 | 
						const weeks = $derived.by(() => {
 | 
				
			||||||
		// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
		const firstDay = moment()
 | 
							const firstDay = moment()
 | 
				
			||||||
			.year(year)
 | 
								.year(year)
 | 
				
			||||||
			.month(month)
 | 
								.month(month)
 | 
				
			||||||
			.startOf("month")
 | 
								.startOf("month")
 | 
				
			||||||
			.startOf("week");
 | 
								.startOf("week");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
		const lastDay = moment()
 | 
							const lastDay = moment()
 | 
				
			||||||
			.year(year)
 | 
								.year(year)
 | 
				
			||||||
			.month(month)
 | 
								.month(month)
 | 
				
			||||||
| 
						 | 
					@ -120,15 +115,9 @@
 | 
				
			||||||
		return weeks;
 | 
							return weeks;
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	interface BookData {
 | 
					 | 
				
			||||||
		coverSrc: string;
 | 
					 | 
				
			||||||
		coverAlt: string;
 | 
					 | 
				
			||||||
		file: TFile;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	interface BookMapItem {
 | 
						interface BookMapItem {
 | 
				
			||||||
		totalPagesRead: number;
 | 
							totalPagesRead: number;
 | 
				
			||||||
		books: BookData[];
 | 
							metadata: FileMetadata[];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const bookMap = $derived(
 | 
						const bookMap = $derived(
 | 
				
			||||||
| 
						 | 
					@ -136,30 +125,17 @@
 | 
				
			||||||
			(acc, entry) => {
 | 
								(acc, entry) => {
 | 
				
			||||||
				const key = entry.createdAt.date();
 | 
									const key = entry.createdAt.date();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const metadata = metadataStore.metadata.find(
 | 
									const meta = metadataStore.metadata.find(
 | 
				
			||||||
					(other) => other.file.basename === entry.book,
 | 
										(other) => other.file.basename === entry.book,
 | 
				
			||||||
				);
 | 
									);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (!metadata) {
 | 
									if (!meta) {
 | 
				
			||||||
					return acc;
 | 
										return acc;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const coverPath = metadata.frontmatter?.[
 | 
									const value = acc[key] ?? { totalPagesRead: 0, metadata: [] };
 | 
				
			||||||
					settings.coverProperty
 | 
					 | 
				
			||||||
				] as string;
 | 
					 | 
				
			||||||
				const coverFile = plugin.app.vault.getFileByPath(coverPath);
 | 
					 | 
				
			||||||
				let coverSrc = "";
 | 
					 | 
				
			||||||
				if (coverFile) {
 | 
					 | 
				
			||||||
					coverSrc = plugin.app.vault.getResourcePath(coverFile);
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				const value = acc[key] ?? { totalPagesRead: 0, books: [] };
 | 
					 | 
				
			||||||
				value.totalPagesRead += entry.pagesRead;
 | 
									value.totalPagesRead += entry.pagesRead;
 | 
				
			||||||
				value.books.push({
 | 
									value.metadata.push(meta);
 | 
				
			||||||
					coverSrc,
 | 
					 | 
				
			||||||
					coverAlt: entry.book,
 | 
					 | 
				
			||||||
					file: metadata.file,
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
				acc[key] = value;
 | 
									acc[key] = value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				return acc;
 | 
									return acc;
 | 
				
			||||||
| 
						 | 
					@ -248,12 +224,12 @@
 | 
				
			||||||
							<div class="covers">
 | 
												<div class="covers">
 | 
				
			||||||
								{#if isThisMonth && date in bookMap}
 | 
													{#if isThisMonth && date in bookMap}
 | 
				
			||||||
									{@const data = bookMap[date]}
 | 
														{@const data = bookMap[date]}
 | 
				
			||||||
									{#each data.books as book}
 | 
														{#each data.metadata as meta}
 | 
				
			||||||
										<div class="cover">
 | 
															<div class="cover">
 | 
				
			||||||
											<OpenFileLink file={book.file}>
 | 
																<OpenFileLink file={meta.file}>
 | 
				
			||||||
												<img
 | 
																	<BookCover
 | 
				
			||||||
													src={book.coverSrc}
 | 
																		app={plugin.app}
 | 
				
			||||||
													alt={book.coverAlt}
 | 
																		book={meta.book}
 | 
				
			||||||
												/>
 | 
																	/>
 | 
				
			||||||
											</OpenFileLink>
 | 
																</OpenFileLink>
 | 
				
			||||||
										</div>
 | 
															</div>
 | 
				
			||||||
| 
						 | 
					@ -340,7 +316,7 @@
 | 
				
			||||||
					height: 100%;
 | 
										height: 100%;
 | 
				
			||||||
					width: 100%;
 | 
										width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					img {
 | 
										:global(img) {
 | 
				
			||||||
						border-radius: var(--radius-l);
 | 
											border-radius: var(--radius-l);
 | 
				
			||||||
						width: 100%;
 | 
											width: 100%;
 | 
				
			||||||
						height: 100%;
 | 
											height: 100%;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ import ReadingStatsCodeBlockView from "./ReadingStatsCodeBlockView.svelte";
 | 
				
			||||||
import type BookTrackerPlugin from "@src/main";
 | 
					import type BookTrackerPlugin from "@src/main";
 | 
				
			||||||
import * as z from "zod/v4";
 | 
					import * as z from "zod/v4";
 | 
				
			||||||
import { COLOR_NAMES } from "@utils/color";
 | 
					import { COLOR_NAMES } from "@utils/color";
 | 
				
			||||||
 | 
					import { BookMetadataSchema } from "@src/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function registerReadingStatsCodeBlockProcessor(
 | 
					export function registerReadingStatsCodeBlockProcessor(
 | 
				
			||||||
	plugin: BookTrackerPlugin
 | 
						plugin: BookTrackerPlugin
 | 
				
			||||||
| 
						 | 
					@ -41,7 +42,7 @@ export type PieChartColor = z.infer<typeof PieChartColorSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const PieChart = z.object({
 | 
					const PieChart = z.object({
 | 
				
			||||||
	type: z.literal("pie"),
 | 
						type: z.literal("pie"),
 | 
				
			||||||
	property: z.string(),
 | 
						property: z.keyof(BookMetadataSchema),
 | 
				
			||||||
	unit: z.optional(z.string()),
 | 
						unit: z.optional(z.string()),
 | 
				
			||||||
	unitPlural: z.optional(z.string()),
 | 
						unitPlural: z.optional(z.string()),
 | 
				
			||||||
	groups: z.optional(z.array(PieGroupingSchema)),
 | 
						groups: z.optional(z.array(PieGroupingSchema)),
 | 
				
			||||||
| 
						 | 
					@ -51,7 +52,7 @@ const PieChart = z.object({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const BarChart = z.object({
 | 
					const BarChart = z.object({
 | 
				
			||||||
	type: z.literal("bar"),
 | 
						type: z.literal("bar"),
 | 
				
			||||||
	property: z.string(),
 | 
						property: z.keyof(BookMetadataSchema),
 | 
				
			||||||
	horizontal: z.optional(z.boolean()),
 | 
						horizontal: z.optional(z.boolean()),
 | 
				
			||||||
	sortByLabel: z.optional(z.boolean()),
 | 
						sortByLabel: z.optional(z.boolean()),
 | 
				
			||||||
	topN: z.optional(z.number()),
 | 
						topN: z.optional(z.number()),
 | 
				
			||||||
| 
						 | 
					@ -63,10 +64,10 @@ const BarChart = z.object({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const LineChart = z.object({
 | 
					const LineChart = z.object({
 | 
				
			||||||
	type: z.literal("line"),
 | 
						type: z.literal("line"),
 | 
				
			||||||
	property: z.string(),
 | 
						property: z.keyof(BookMetadataSchema),
 | 
				
			||||||
	unit: z.optional(z.string()),
 | 
						unit: z.optional(z.string()),
 | 
				
			||||||
	unitPlural: z.optional(z.string()),
 | 
						unitPlural: z.optional(z.string()),
 | 
				
			||||||
	secondProperty: z.optional(z.string()),
 | 
						secondProperty: z.optional(z.keyof(BookMetadataSchema)),
 | 
				
			||||||
	secondUnit: z.optional(z.string()),
 | 
						secondUnit: z.optional(z.string()),
 | 
				
			||||||
	secondUnitPlural: z.optional(z.string()),
 | 
						secondUnitPlural: z.optional(z.string()),
 | 
				
			||||||
	responsive: z.optional(z.boolean()),
 | 
						responsive: z.optional(z.boolean()),
 | 
				
			||||||
| 
						 | 
					@ -84,7 +85,7 @@ export const ReadingStatsSectionSchema = z.object({
 | 
				
			||||||
				z.object({
 | 
									z.object({
 | 
				
			||||||
					type: z.enum(["count", "average", "total"]),
 | 
										type: z.enum(["count", "average", "total"]),
 | 
				
			||||||
					label: z.string(),
 | 
										label: z.string(),
 | 
				
			||||||
					property: z.string(),
 | 
										property: z.keyof(BookMetadataSchema),
 | 
				
			||||||
				}),
 | 
									}),
 | 
				
			||||||
				z.object({
 | 
									z.object({
 | 
				
			||||||
					type: z.literal("book-count"),
 | 
										type: z.literal("book-count"),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,13 +24,6 @@ export const ShelfSettingsSchema = z.object({
 | 
				
			||||||
		.enum([STATUS_TO_BE_READ, STATUS_IN_PROGRESS, STATUS_READ])
 | 
							.enum([STATUS_TO_BE_READ, STATUS_IN_PROGRESS, STATUS_READ])
 | 
				
			||||||
		.default(STATUS_TO_BE_READ),
 | 
							.default(STATUS_TO_BE_READ),
 | 
				
			||||||
	defaultView: z.enum(SHELF_VIEWS).default("table"),
 | 
						defaultView: z.enum(SHELF_VIEWS).default("table"),
 | 
				
			||||||
	coverProperty: z.string(),
 | 
					 | 
				
			||||||
	titleProperty: z.string(),
 | 
					 | 
				
			||||||
	subtitleProperty: z.optional(z.string()),
 | 
					 | 
				
			||||||
	authorsProperty: z.string(),
 | 
					 | 
				
			||||||
	descriptionProperty: z.optional(z.string()),
 | 
					 | 
				
			||||||
	seriesTitleProperty: z.optional(z.string()),
 | 
					 | 
				
			||||||
	seriesNumberProperty: z.optional(z.string()),
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ShelfSettings = z.infer<typeof ShelfSettingsSchema>;
 | 
					export type ShelfSettings = z.infer<typeof ShelfSettingsSchema>;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -54,7 +54,7 @@
 | 
				
			||||||
		{/if}
 | 
							{/if}
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	{#if view === "bookshelf"}
 | 
						{#if view === "bookshelf"}
 | 
				
			||||||
		<BookshelfView {plugin} {settings} />
 | 
							<BookshelfView {plugin} />
 | 
				
			||||||
	{:else if view === "table"}
 | 
						{:else if view === "table"}
 | 
				
			||||||
		<TableView {plugin} {settings} />
 | 
							<TableView {plugin} {settings} />
 | 
				
			||||||
	{:else if view === "details"}
 | 
						{:else if view === "details"}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,27 @@
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import type { BookMetadata } from "@src/types";
 | 
				
			||||||
 | 
						import type { App } from "obsidian";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						interface Props {
 | 
				
			||||||
 | 
							app: App;
 | 
				
			||||||
 | 
							book: BookMetadata;
 | 
				
			||||||
 | 
							size?: number;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const { app, book, size }: Props = $props();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const coverPath = $derived(book.localCoverPath);
 | 
				
			||||||
 | 
						const coverFile = $derived(app.vault.getFileByPath(coverPath));
 | 
				
			||||||
 | 
						const coverSrc = $derived(
 | 
				
			||||||
 | 
							coverFile ? app.vault.getResourcePath(coverFile) : "",
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
						const coverAlt = $derived(book.title);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<img src={coverSrc} alt={coverAlt} width={size} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss">
 | 
				
			||||||
 | 
						img {
 | 
				
			||||||
 | 
							border-radius: var(--radius-l);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -7,17 +7,14 @@
 | 
				
			||||||
		getMetadataContext,
 | 
							getMetadataContext,
 | 
				
			||||||
		type FileMetadata,
 | 
							type FileMetadata,
 | 
				
			||||||
	} from "@ui/stores/metadata.svelte";
 | 
						} from "@ui/stores/metadata.svelte";
 | 
				
			||||||
	import { getSettingsContext } from "@ui/stores/settings.svelte";
 | 
					 | 
				
			||||||
	import { COLOR_NAMES, type ColorName } from "@utils/color";
 | 
						import { COLOR_NAMES, type ColorName } from "@utils/color";
 | 
				
			||||||
	import { randomElement, randomFloat } from "@utils/rand";
 | 
						import { randomElement, randomFloat } from "@utils/rand";
 | 
				
			||||||
	import { v4 as uuidv4 } from "uuid";
 | 
						import { v4 as uuidv4 } from "uuid";
 | 
				
			||||||
	import memoize from "just-memoize";
 | 
						import memoize from "just-memoize";
 | 
				
			||||||
	import type { ShelfSettings } from "@ui/code-blocks/ShelfCodeBlock";
 | 
					 | 
				
			||||||
	import type { TFile } from "obsidian";
 | 
						import type { TFile } from "obsidian";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	interface Props {
 | 
						interface Props {
 | 
				
			||||||
		plugin: BookTrackerPlugin;
 | 
							plugin: BookTrackerPlugin;
 | 
				
			||||||
		settings: ShelfSettings;
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	interface BookData {
 | 
						interface BookData {
 | 
				
			||||||
| 
						 | 
					@ -37,9 +34,8 @@
 | 
				
			||||||
		books: BookData[];
 | 
							books: BookData[];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { plugin, settings }: Props = $props();
 | 
						const { plugin }: Props = $props();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const settingsStore = getSettingsContext();
 | 
					 | 
				
			||||||
	const metadataStore = getMetadataContext();
 | 
						const metadataStore = getMetadataContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const designs = ["default", "dual-top-bands", "split-bands"] as const;
 | 
						const designs = ["default", "dual-top-bands", "split-bands"] as const;
 | 
				
			||||||
| 
						 | 
					@ -74,29 +70,20 @@
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const getBookData = memoize(
 | 
						const getBookData = memoize(
 | 
				
			||||||
		(metadata: FileMetadata): BookData => {
 | 
							(meta: FileMetadata): BookData => {
 | 
				
			||||||
			const orientation = randomOrientation();
 | 
								const orientation = randomOrientation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return {
 | 
								return {
 | 
				
			||||||
				id: metadata.file.path,
 | 
									id: meta.file.path,
 | 
				
			||||||
				title: metadata.frontmatter[settings.titleProperty],
 | 
									title: meta.book.title,
 | 
				
			||||||
				subtitle: settings.subtitleProperty
 | 
									subtitle:
 | 
				
			||||||
					? metadata.frontmatter[settings.subtitleProperty]
 | 
										meta.book.subtitle === "" ? undefined : meta.book.subtitle,
 | 
				
			||||||
					: undefined,
 | 
									authors: meta.book.authors,
 | 
				
			||||||
				authors: metadata.frontmatter[settings.authorsProperty],
 | 
									width: Math.min(Math.max(20, meta.book.pageCount / 10), 100),
 | 
				
			||||||
				width: Math.min(
 | 
					 | 
				
			||||||
					Math.max(
 | 
					 | 
				
			||||||
						20,
 | 
					 | 
				
			||||||
						metadata.frontmatter[
 | 
					 | 
				
			||||||
							settingsStore.settings.pageCountProperty
 | 
					 | 
				
			||||||
						] / 10,
 | 
					 | 
				
			||||||
					),
 | 
					 | 
				
			||||||
					100,
 | 
					 | 
				
			||||||
				),
 | 
					 | 
				
			||||||
				color: randomColor(),
 | 
									color: randomColor(),
 | 
				
			||||||
				design: orientation === "front" ? "default" : randomDesign(),
 | 
									design: orientation === "front" ? "default" : randomDesign(),
 | 
				
			||||||
				orientation: randomOrientation(),
 | 
									orientation: randomOrientation(),
 | 
				
			||||||
				file: metadata.file,
 | 
									file: meta.file,
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		(metadata: FileMetadata) => metadata.file.path,
 | 
							(metadata: FileMetadata) => metadata.file.path,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,20 +10,27 @@
 | 
				
			||||||
			"filterYear" | "filterYears" | "filterMonth" | "filterMonths"
 | 
								"filterYear" | "filterYears" | "filterMonth" | "filterMonths"
 | 
				
			||||||
		>;
 | 
							>;
 | 
				
			||||||
		showAllMonths?: boolean;
 | 
							showAllMonths?: boolean;
 | 
				
			||||||
 | 
							disableMonthFilter?: boolean;
 | 
				
			||||||
 | 
							disableAllTime?: boolean;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { store, showAllMonths }: Props = $props();
 | 
						const { store, showAllMonths, disableMonthFilter, disableAllTime }: Props =
 | 
				
			||||||
 | 
							$props();
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<select class="year-filter" bind:value={store.filterYear}>
 | 
					<select class="year-filter" bind:value={store.filterYear}>
 | 
				
			||||||
	{#each store.filterYears as year}
 | 
						{#each store.filterYears as year}
 | 
				
			||||||
		<option value={year}>{year}</option>
 | 
							<option value={year}>{year}</option>
 | 
				
			||||||
	{/each}
 | 
						{/each}
 | 
				
			||||||
	<option value={ALL_TIME}>All Time</option>
 | 
						{#if !disableAllTime}
 | 
				
			||||||
 | 
							<option value={ALL_TIME}>All Time</option>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
</select>
 | 
					</select>
 | 
				
			||||||
{#if store.filterYear !== ALL_TIME}
 | 
					{#if store.filterYear !== ALL_TIME && !disableMonthFilter}
 | 
				
			||||||
	<select class="month-filter" bind:value={store.filterMonth}>
 | 
						<select class="month-filter" bind:value={store.filterMonth}>
 | 
				
			||||||
		<option value={ALL_TIME}>Select Month</option>
 | 
							{#if disableAllTime}
 | 
				
			||||||
 | 
								<option value={ALL_TIME}>Select Month</option>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
		{#if showAllMonths}
 | 
							{#if showAllMonths}
 | 
				
			||||||
			<option value={1}>January</option>
 | 
								<option value={1}>January</option>
 | 
				
			||||||
			<option value={2}>February</option>
 | 
								<option value={2}>February</option>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,12 +2,11 @@
 | 
				
			||||||
	import { STATUS_IN_PROGRESS, STATUS_READ } from "@src/const";
 | 
						import { STATUS_IN_PROGRESS, STATUS_READ } from "@src/const";
 | 
				
			||||||
	import type BookTrackerPlugin from "@src/main";
 | 
						import type BookTrackerPlugin from "@src/main";
 | 
				
			||||||
	import { getMetadataContext } from "@ui/stores/metadata.svelte";
 | 
						import { getMetadataContext } from "@ui/stores/metadata.svelte";
 | 
				
			||||||
	import { getSettingsContext } from "@ui/stores/settings.svelte";
 | 
					 | 
				
			||||||
	import { getLinkpath } from "obsidian";
 | 
					 | 
				
			||||||
	import type { ShelfSettings } from "@ui/code-blocks/ShelfCodeBlock";
 | 
						import type { ShelfSettings } from "@ui/code-blocks/ShelfCodeBlock";
 | 
				
			||||||
	import { Dot, Flame, Star, StarHalf } from "lucide-svelte";
 | 
						import { Dot, Flame, Star, StarHalf } from "lucide-svelte";
 | 
				
			||||||
	import RatingInput from "./RatingInput.svelte";
 | 
						import RatingInput from "./RatingInput.svelte";
 | 
				
			||||||
	import OpenFileLink from "./OpenFileLink.svelte";
 | 
						import OpenFileLink from "./OpenFileLink.svelte";
 | 
				
			||||||
 | 
						import BookCover from "./BookCover.svelte";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	interface Props {
 | 
						interface Props {
 | 
				
			||||||
		plugin: BookTrackerPlugin;
 | 
							plugin: BookTrackerPlugin;
 | 
				
			||||||
| 
						 | 
					@ -16,79 +15,54 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { plugin, settings }: Props = $props();
 | 
						const { plugin, settings }: Props = $props();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const settingsStore = getSettingsContext();
 | 
					 | 
				
			||||||
	const metadataStore = getMetadataContext();
 | 
						const metadataStore = getMetadataContext();
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="book-details-list">
 | 
					<div class="book-details-list">
 | 
				
			||||||
	{#each metadataStore.metadata as book}
 | 
						{#each metadataStore.metadata as meta}
 | 
				
			||||||
		{@const coverPath = book.frontmatter[settings.coverProperty]}
 | 
					 | 
				
			||||||
		{@const title = book.frontmatter[settings.titleProperty]}
 | 
					 | 
				
			||||||
		{@const subtitle = settings.subtitleProperty
 | 
					 | 
				
			||||||
			? book.frontmatter[settings.subtitleProperty]
 | 
					 | 
				
			||||||
			: undefined}
 | 
					 | 
				
			||||||
		{@const authors = book.frontmatter[settings.authorsProperty]}
 | 
					 | 
				
			||||||
		{@const description = settings.descriptionProperty
 | 
					 | 
				
			||||||
			? book.frontmatter[settings.descriptionProperty]
 | 
					 | 
				
			||||||
			: undefined}
 | 
					 | 
				
			||||||
		{@const seriesTitle = settings.seriesTitleProperty
 | 
					 | 
				
			||||||
			? book.frontmatter[settings.seriesTitleProperty]
 | 
					 | 
				
			||||||
			: undefined}
 | 
					 | 
				
			||||||
		{@const seriesNumber = settings.seriesNumberProperty
 | 
					 | 
				
			||||||
			? book.frontmatter[settings.seriesNumberProperty]
 | 
					 | 
				
			||||||
			: undefined}
 | 
					 | 
				
			||||||
		{@const startDate =
 | 
					 | 
				
			||||||
			book.frontmatter[settingsStore.settings.startDateProperty]}
 | 
					 | 
				
			||||||
		{@const endDate =
 | 
					 | 
				
			||||||
			book.frontmatter[settingsStore.settings.endDateProperty]}
 | 
					 | 
				
			||||||
		{@const rating =
 | 
					 | 
				
			||||||
			book.frontmatter[settingsStore.settings.ratingProperty] ?? 0}
 | 
					 | 
				
			||||||
		{@const spice =
 | 
					 | 
				
			||||||
			book.frontmatter[settingsStore.settings.spiceProperty] ?? 0}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		<div class="book-details">
 | 
							<div class="book-details">
 | 
				
			||||||
			<img
 | 
								<BookCover app={plugin.app} book={meta.book} />
 | 
				
			||||||
				src={plugin.app.vault.getResourcePath(
 | 
					 | 
				
			||||||
					plugin.app.vault.getFileByPath(coverPath)!,
 | 
					 | 
				
			||||||
				)}
 | 
					 | 
				
			||||||
				alt={title}
 | 
					 | 
				
			||||||
			/>
 | 
					 | 
				
			||||||
			<div class="book-info">
 | 
								<div class="book-info">
 | 
				
			||||||
				<OpenFileLink file={book.file}>
 | 
									<OpenFileLink file={meta.file}>
 | 
				
			||||||
					<h2 class="book-title">
 | 
										<h2 class="book-title">
 | 
				
			||||||
						{title}
 | 
											{meta.book.title}
 | 
				
			||||||
					</h2>
 | 
										</h2>
 | 
				
			||||||
				</OpenFileLink>
 | 
									</OpenFileLink>
 | 
				
			||||||
				{#if subtitle}
 | 
									{#if meta.book.subtitle !== ""}
 | 
				
			||||||
					<p class="subtitle">{subtitle}</p>
 | 
										<p class="subtitle">{meta.book.subtitle}</p>
 | 
				
			||||||
				{/if}
 | 
									{/if}
 | 
				
			||||||
				<p class="authors">By: {authors.join(", ")}</p>
 | 
									<p class="authors">By: {meta.book.authors.join(", ")}</p>
 | 
				
			||||||
				{#if seriesTitle}
 | 
									{#if meta.book.seriesTitle != ""}
 | 
				
			||||||
					<p class="series">
 | 
										<p class="series">
 | 
				
			||||||
						<span class="series-title">{seriesTitle}</span>
 | 
											<span class="series-title">{meta.book.seriesTitle}</span
 | 
				
			||||||
						{#if seriesNumber}
 | 
											>
 | 
				
			||||||
							<span class="series-number">#{seriesNumber}</span>
 | 
											<span class="series-number">
 | 
				
			||||||
						{/if}
 | 
												#{meta.book.seriesPosition}
 | 
				
			||||||
 | 
											</span>
 | 
				
			||||||
					</p>
 | 
										</p>
 | 
				
			||||||
				{/if}
 | 
									{/if}
 | 
				
			||||||
				{#if description}
 | 
									{#if meta.book.description != ""}
 | 
				
			||||||
					<hr />
 | 
										<hr />
 | 
				
			||||||
					<p class="description">{@html description}</p>
 | 
										<p class="description">{@html meta.book.description}</p>
 | 
				
			||||||
					<hr />
 | 
										<hr />
 | 
				
			||||||
				{/if}
 | 
									{/if}
 | 
				
			||||||
				<div class="footer">
 | 
									<div class="footer">
 | 
				
			||||||
					{#if settings.statusFilter === STATUS_IN_PROGRESS || settings.statusFilter === STATUS_READ}
 | 
										{#if settings.statusFilter === STATUS_IN_PROGRESS || settings.statusFilter === STATUS_READ}
 | 
				
			||||||
						<p class="start-date">
 | 
											<p class="start-date">
 | 
				
			||||||
							Started:
 | 
												Started:
 | 
				
			||||||
							<datetime datetime={startDate}>{startDate}</datetime
 | 
												<datetime
 | 
				
			||||||
 | 
													datetime={meta.book.startDate.format("LLLL")}
 | 
				
			||||||
 | 
													title={meta.book.startDate.format("LLLL")}
 | 
				
			||||||
							>
 | 
												>
 | 
				
			||||||
 | 
													{meta.book.startDate.format("l")}
 | 
				
			||||||
 | 
												</datetime>
 | 
				
			||||||
						</p>
 | 
											</p>
 | 
				
			||||||
					{/if}
 | 
										{/if}
 | 
				
			||||||
					{#if settings.statusFilter === STATUS_IN_PROGRESS}
 | 
										{#if settings.statusFilter === STATUS_IN_PROGRESS}
 | 
				
			||||||
						<Dot color="var(--text-muted)" />
 | 
											<Dot color="var(--text-muted)" />
 | 
				
			||||||
						<p class="current-page">
 | 
											<p class="current-page">
 | 
				
			||||||
							Current Page: {plugin.readingLog.getLastEntryForBook(
 | 
												Current Page: {plugin.readingLog.getLastEntryForBook(
 | 
				
			||||||
								book.file.basename,
 | 
													meta.file.basename,
 | 
				
			||||||
							)?.pagesReadTotal ?? 0}
 | 
												)?.pagesReadTotal ?? 0}
 | 
				
			||||||
						</p>
 | 
											</p>
 | 
				
			||||||
					{/if}
 | 
										{/if}
 | 
				
			||||||
| 
						 | 
					@ -97,10 +71,19 @@
 | 
				
			||||||
						<Dot color="var(--text-muted)" />
 | 
											<Dot color="var(--text-muted)" />
 | 
				
			||||||
						<p class="end-date">
 | 
											<p class="end-date">
 | 
				
			||||||
							Finished:
 | 
												Finished:
 | 
				
			||||||
							<datetime datetime={endDate}>{endDate}</datetime>
 | 
												<datetime
 | 
				
			||||||
 | 
													datetime={meta.book.endDate.format("LLLL")}
 | 
				
			||||||
 | 
													title={meta.book.endDate.format("LLLL")}
 | 
				
			||||||
 | 
												>
 | 
				
			||||||
 | 
													{meta.book.endDate.format("l")}
 | 
				
			||||||
 | 
												</datetime>
 | 
				
			||||||
						</p>
 | 
											</p>
 | 
				
			||||||
						<Dot color="var(--text-muted)" />
 | 
											<Dot color="var(--text-muted)" />
 | 
				
			||||||
						<RatingInput value={rating} disabled {iconSize}>
 | 
											<RatingInput
 | 
				
			||||||
 | 
												value={meta.book.rating}
 | 
				
			||||||
 | 
												disabled
 | 
				
			||||||
 | 
												{iconSize}
 | 
				
			||||||
 | 
											>
 | 
				
			||||||
							{#snippet inactive()}
 | 
												{#snippet inactive()}
 | 
				
			||||||
								<Star
 | 
													<Star
 | 
				
			||||||
									color="var(--background-modifier-border)"
 | 
														color="var(--background-modifier-border)"
 | 
				
			||||||
| 
						 | 
					@ -122,7 +105,11 @@
 | 
				
			||||||
								/>
 | 
													/>
 | 
				
			||||||
							{/snippet}
 | 
												{/snippet}
 | 
				
			||||||
						</RatingInput>
 | 
											</RatingInput>
 | 
				
			||||||
						<RatingInput value={spice} disabled {iconSize}>
 | 
											<RatingInput
 | 
				
			||||||
 | 
												value={meta.book.spice}
 | 
				
			||||||
 | 
												disabled
 | 
				
			||||||
 | 
												{iconSize}
 | 
				
			||||||
 | 
											>
 | 
				
			||||||
							{#snippet inactive()}
 | 
												{#snippet inactive()}
 | 
				
			||||||
								<Flame
 | 
													<Flame
 | 
				
			||||||
									color="var(--background-modifier-border)"
 | 
														color="var(--background-modifier-border)"
 | 
				
			||||||
| 
						 | 
					@ -157,7 +144,7 @@
 | 
				
			||||||
			background-color: var(--background-secondary);
 | 
								background-color: var(--background-secondary);
 | 
				
			||||||
			border-radius: var(--radius-l);
 | 
								border-radius: var(--radius-l);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			img {
 | 
								:global(img) {
 | 
				
			||||||
				border-radius: var(--radius-l);
 | 
									border-radius: var(--radius-l);
 | 
				
			||||||
				max-width: 30%;
 | 
									max-width: 30%;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
| 
						 | 
					@ -166,7 +153,7 @@
 | 
				
			||||||
				flex-direction: column;
 | 
									flex-direction: column;
 | 
				
			||||||
				align-items: center;
 | 
									align-items: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				img {
 | 
									:global(img) {
 | 
				
			||||||
					max-height: 30rem;
 | 
										max-height: 30rem;
 | 
				
			||||||
					max-width: 100%;
 | 
										max-width: 100%;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,11 +2,10 @@
 | 
				
			||||||
	import { STATUS_IN_PROGRESS, STATUS_READ } from "@src/const";
 | 
						import { STATUS_IN_PROGRESS, STATUS_READ } from "@src/const";
 | 
				
			||||||
	import type BookTrackerPlugin from "@src/main";
 | 
						import type BookTrackerPlugin from "@src/main";
 | 
				
			||||||
	import { getMetadataContext } from "@ui/stores/metadata.svelte";
 | 
						import { getMetadataContext } from "@ui/stores/metadata.svelte";
 | 
				
			||||||
	import { getSettingsContext } from "@ui/stores/settings.svelte";
 | 
					 | 
				
			||||||
	import { getLinkpath } from "obsidian";
 | 
					 | 
				
			||||||
	import Rating from "@ui/components/Rating.svelte";
 | 
						import Rating from "@ui/components/Rating.svelte";
 | 
				
			||||||
	import type { ShelfSettings } from "@ui/code-blocks/ShelfCodeBlock";
 | 
						import type { ShelfSettings } from "@ui/code-blocks/ShelfCodeBlock";
 | 
				
			||||||
	import OpenFileLink from "./OpenFileLink.svelte";
 | 
						import OpenFileLink from "./OpenFileLink.svelte";
 | 
				
			||||||
 | 
						import BookCover from "./BookCover.svelte";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	interface Props {
 | 
						interface Props {
 | 
				
			||||||
		plugin: BookTrackerPlugin;
 | 
							plugin: BookTrackerPlugin;
 | 
				
			||||||
| 
						 | 
					@ -15,7 +14,6 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { plugin, settings }: Props = $props();
 | 
						const { plugin, settings }: Props = $props();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const settingsStore = getSettingsContext();
 | 
					 | 
				
			||||||
	const metadataStore = getMetadataContext();
 | 
						const metadataStore = getMetadataContext();
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,12 +23,8 @@
 | 
				
			||||||
			<th>Cover</th>
 | 
								<th>Cover</th>
 | 
				
			||||||
			<th>Title</th>
 | 
								<th>Title</th>
 | 
				
			||||||
			<th>Authors</th>
 | 
								<th>Authors</th>
 | 
				
			||||||
			{#if settings.seriesTitleProperty}
 | 
								<th>Series</th>
 | 
				
			||||||
				<th>Series</th>
 | 
								<th>#</th>
 | 
				
			||||||
			{/if}
 | 
					 | 
				
			||||||
			{#if settings.seriesNumberProperty}
 | 
					 | 
				
			||||||
				<th>#</th>
 | 
					 | 
				
			||||||
			{/if}
 | 
					 | 
				
			||||||
			{#if settings.statusFilter === STATUS_IN_PROGRESS || settings.statusFilter === STATUS_READ}
 | 
								{#if settings.statusFilter === STATUS_IN_PROGRESS || settings.statusFilter === STATUS_READ}
 | 
				
			||||||
				<th>Start Date</th>
 | 
									<th>Start Date</th>
 | 
				
			||||||
			{/if}
 | 
								{/if}
 | 
				
			||||||
| 
						 | 
					@ -41,64 +35,46 @@
 | 
				
			||||||
		</tr>
 | 
							</tr>
 | 
				
			||||||
	</thead>
 | 
						</thead>
 | 
				
			||||||
	<tbody>
 | 
						<tbody>
 | 
				
			||||||
		{#each metadataStore.metadata as book}
 | 
							{#each metadataStore.metadata as meta}
 | 
				
			||||||
			{@const coverPath = book.frontmatter[settings.coverProperty]}
 | 
					 | 
				
			||||||
			{@const title = book.frontmatter[settings.titleProperty]}
 | 
					 | 
				
			||||||
			{@const authors = book.frontmatter[settings.authorsProperty]}
 | 
					 | 
				
			||||||
			{@const seriesTitle = settings.seriesTitleProperty
 | 
					 | 
				
			||||||
				? book.frontmatter[settings.seriesTitleProperty]
 | 
					 | 
				
			||||||
				: undefined}
 | 
					 | 
				
			||||||
			{@const seriesNumber = settings.seriesNumberProperty
 | 
					 | 
				
			||||||
				? book.frontmatter[settings.seriesNumberProperty]
 | 
					 | 
				
			||||||
				: undefined}
 | 
					 | 
				
			||||||
			{@const startDate =
 | 
					 | 
				
			||||||
				book.frontmatter[settingsStore.settings.startDateProperty]}
 | 
					 | 
				
			||||||
			{@const endDate =
 | 
					 | 
				
			||||||
				book.frontmatter[settingsStore.settings.endDateProperty]}
 | 
					 | 
				
			||||||
			{@const rating =
 | 
					 | 
				
			||||||
				book.frontmatter[settingsStore.settings.ratingProperty] ?? 0}
 | 
					 | 
				
			||||||
			{@const spice =
 | 
					 | 
				
			||||||
				book.frontmatter[settingsStore.settings.spiceProperty] ?? 0}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			<tr>
 | 
								<tr>
 | 
				
			||||||
				<td>
 | 
									<td class="cover">
 | 
				
			||||||
					<img
 | 
										<BookCover app={plugin.app} book={meta.book} size={50} />
 | 
				
			||||||
						src={plugin.app.vault.getResourcePath(
 | 
					 | 
				
			||||||
							plugin.app.vault.getFileByPath(coverPath)!,
 | 
					 | 
				
			||||||
						)}
 | 
					 | 
				
			||||||
						alt={title}
 | 
					 | 
				
			||||||
						width="50"
 | 
					 | 
				
			||||||
					/>
 | 
					 | 
				
			||||||
				</td>
 | 
									</td>
 | 
				
			||||||
				<td>
 | 
									<td>
 | 
				
			||||||
					<OpenFileLink file={book.file}>
 | 
										<OpenFileLink file={meta.file}>
 | 
				
			||||||
						{title}
 | 
											{meta.book.title}
 | 
				
			||||||
					</OpenFileLink>
 | 
										</OpenFileLink>
 | 
				
			||||||
				</td>
 | 
									</td>
 | 
				
			||||||
				<td>
 | 
									<td>
 | 
				
			||||||
					{authors.join(", ")}
 | 
										{meta.book.authors.join(", ")}
 | 
				
			||||||
 | 
									</td>
 | 
				
			||||||
 | 
									<td>
 | 
				
			||||||
 | 
										{meta.book.seriesTitle}
 | 
				
			||||||
 | 
									</td>
 | 
				
			||||||
 | 
									<td>
 | 
				
			||||||
 | 
										{meta.book.seriesPosition}
 | 
				
			||||||
				</td>
 | 
									</td>
 | 
				
			||||||
				{#if settings.seriesTitleProperty}
 | 
					 | 
				
			||||||
					<td>
 | 
					 | 
				
			||||||
						{#if seriesTitle}{seriesTitle}{/if}
 | 
					 | 
				
			||||||
					</td>
 | 
					 | 
				
			||||||
				{/if}
 | 
					 | 
				
			||||||
				{#if settings.seriesNumberProperty}
 | 
					 | 
				
			||||||
					<td>
 | 
					 | 
				
			||||||
						{#if seriesNumber}{seriesNumber}{/if}
 | 
					 | 
				
			||||||
					</td>
 | 
					 | 
				
			||||||
				{/if}
 | 
					 | 
				
			||||||
				{#if settings.statusFilter === STATUS_IN_PROGRESS || settings.statusFilter === STATUS_READ}
 | 
									{#if settings.statusFilter === STATUS_IN_PROGRESS || settings.statusFilter === STATUS_READ}
 | 
				
			||||||
					<td>
 | 
										<td>
 | 
				
			||||||
						<datetime datetime={startDate}>{startDate}</datetime>
 | 
											<datetime
 | 
				
			||||||
 | 
												datetime={meta.book.startDate.format("LLLL")}
 | 
				
			||||||
 | 
												title={meta.book.startDate.format("LLLL")}
 | 
				
			||||||
 | 
											>
 | 
				
			||||||
 | 
												{meta.book.startDate.format("ll")}
 | 
				
			||||||
 | 
											</datetime>
 | 
				
			||||||
					</td>
 | 
										</td>
 | 
				
			||||||
				{/if}
 | 
									{/if}
 | 
				
			||||||
				{#if settings.statusFilter === STATUS_READ}
 | 
									{#if settings.statusFilter === STATUS_READ}
 | 
				
			||||||
					<td>
 | 
										<td>
 | 
				
			||||||
						<datetime datetime={endDate}>{endDate}</datetime>
 | 
											<datetime
 | 
				
			||||||
 | 
												datetime={meta.book.endDate.format("LLLL")}
 | 
				
			||||||
 | 
												title={meta.book.endDate.format("LLLL")}
 | 
				
			||||||
 | 
											>
 | 
				
			||||||
 | 
												{meta.book.endDate.format("ll")}
 | 
				
			||||||
 | 
											</datetime>
 | 
				
			||||||
					</td>
 | 
										</td>
 | 
				
			||||||
					<td>
 | 
										<td>
 | 
				
			||||||
						<Rating {rating} />
 | 
											<Rating rating={meta.book.rating} />
 | 
				
			||||||
					</td>
 | 
										</td>
 | 
				
			||||||
				{/if}
 | 
									{/if}
 | 
				
			||||||
			</tr>
 | 
								</tr>
 | 
				
			||||||
| 
						 | 
					@ -110,5 +86,15 @@
 | 
				
			||||||
	table {
 | 
						table {
 | 
				
			||||||
		border-collapse: collapse;
 | 
							border-collapse: collapse;
 | 
				
			||||||
		width: 100%;
 | 
							width: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							td {
 | 
				
			||||||
 | 
								text-align: center;
 | 
				
			||||||
 | 
								vertical-align: middle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								&.cover {
 | 
				
			||||||
 | 
									padding: 0;
 | 
				
			||||||
 | 
									margin: 0;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,12 @@
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import type { BookMetadata } from "@src/types";
 | 
				
			||||||
	import { chart } from "@ui/directives/chart";
 | 
						import { chart } from "@ui/directives/chart";
 | 
				
			||||||
	import { createPropertyStore } from "@ui/stores/metadata.svelte";
 | 
						import { createPropertyStore } from "@ui/stores/metadata.svelte";
 | 
				
			||||||
	import { Color, type ColorName } from "@utils/color";
 | 
						import { Color, type ColorName } from "@utils/color";
 | 
				
			||||||
	import type { ChartConfiguration } from "chart.js";
 | 
						import type { ChartConfiguration } from "chart.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	type Props = {
 | 
						type Props = {
 | 
				
			||||||
		property: string;
 | 
							property: keyof BookMetadata;
 | 
				
			||||||
		horizontal?: boolean;
 | 
							horizontal?: boolean;
 | 
				
			||||||
		sortByLabel?: boolean;
 | 
							sortByLabel?: boolean;
 | 
				
			||||||
		topN?: number;
 | 
							topN?: number;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,11 +3,10 @@
 | 
				
			||||||
	import { ALL_TIME } from "@ui/stores/date-filter.svelte";
 | 
						import { ALL_TIME } from "@ui/stores/date-filter.svelte";
 | 
				
			||||||
	import { getMetadataContext } from "@ui/stores/metadata.svelte";
 | 
						import { getMetadataContext } from "@ui/stores/metadata.svelte";
 | 
				
			||||||
	import { getReadingLogContext } from "@ui/stores/reading-log.svelte";
 | 
						import { getReadingLogContext } from "@ui/stores/reading-log.svelte";
 | 
				
			||||||
	import { getSettingsContext } from "@ui/stores/settings.svelte";
 | 
					 | 
				
			||||||
	import { Color } from "@utils/color";
 | 
						import { Color } from "@utils/color";
 | 
				
			||||||
	import type { ChartConfiguration } from "chart.js";
 | 
						import type { ChartConfiguration } from "chart.js";
 | 
				
			||||||
 | 
						import moment from "@external/moment";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const settingsStore = getSettingsContext();
 | 
					 | 
				
			||||||
	const store = getMetadataContext();
 | 
						const store = getMetadataContext();
 | 
				
			||||||
	const readingLog = getReadingLogContext();
 | 
						const readingLog = getReadingLogContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,12 +21,8 @@
 | 
				
			||||||
					date: entry.createdAt,
 | 
										date: entry.createdAt,
 | 
				
			||||||
				}))
 | 
									}))
 | 
				
			||||||
			: store.metadata.map((f) => ({
 | 
								: store.metadata.map((f) => ({
 | 
				
			||||||
					pageCount:
 | 
										pageCount: f.book.pageCount,
 | 
				
			||||||
						f.frontmatter[settingsStore.settings.pageCountProperty],
 | 
										date: f.book.endDate,
 | 
				
			||||||
					// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
					date: moment(
 | 
					 | 
				
			||||||
						f.frontmatter[settingsStore.settings.endDateProperty],
 | 
					 | 
				
			||||||
					),
 | 
					 | 
				
			||||||
				})),
 | 
									})),
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,7 +47,6 @@
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (isMonthly && typeof store.filterMonth === "number") {
 | 
							if (isMonthly && typeof store.filterMonth === "number") {
 | 
				
			||||||
			// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
			const daysInMonth = moment()
 | 
								const daysInMonth = moment()
 | 
				
			||||||
				.month(store.filterMonth - 1)
 | 
									.month(store.filterMonth - 1)
 | 
				
			||||||
				.daysInMonth();
 | 
									.daysInMonth();
 | 
				
			||||||
| 
						 | 
					@ -70,8 +64,7 @@
 | 
				
			||||||
			.map((key) =>
 | 
								.map((key) =>
 | 
				
			||||||
				store.filterYear === ALL_TIME || isMonthly
 | 
									store.filterYear === ALL_TIME || isMonthly
 | 
				
			||||||
					? key
 | 
										? key
 | 
				
			||||||
					: // @ts-expect-error Moment is provided by Obsidian
 | 
										: moment().month(key).format("MMM"),
 | 
				
			||||||
						moment().month(key).format("MMM"),
 | 
					 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
		const sortedBooks = Array.from(books.entries())
 | 
							const sortedBooks = Array.from(books.entries())
 | 
				
			||||||
			.sort((a, b) => a[0] - b[0])
 | 
								.sort((a, b) => a[0] - b[0])
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import type { BookMetadata } from "@src/types";
 | 
				
			||||||
	import type {
 | 
						import type {
 | 
				
			||||||
		PieChartColor,
 | 
							PieChartColor,
 | 
				
			||||||
		PieGrouping,
 | 
							PieGrouping,
 | 
				
			||||||
| 
						 | 
					@ -9,7 +10,7 @@
 | 
				
			||||||
	import type { ChartConfiguration } from "chart.js";
 | 
						import type { ChartConfiguration } from "chart.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	type Props = {
 | 
						type Props = {
 | 
				
			||||||
		property: string;
 | 
							property: keyof BookMetadata;
 | 
				
			||||||
		groups?: PieGrouping[];
 | 
							groups?: PieGrouping[];
 | 
				
			||||||
		unit?: string;
 | 
							unit?: string;
 | 
				
			||||||
		unitPlural?: string;
 | 
							unitPlural?: string;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
	import type { ComponentProps } from "svelte";
 | 
						import type { ComponentProps } from "svelte";
 | 
				
			||||||
	import Item from "./Item.svelte";
 | 
						import Item from "./Item.svelte";
 | 
				
			||||||
	import type { App } from "obsidian";
 | 
						import type { App } from "obsidian";
 | 
				
			||||||
	import FieldSuggest from "../suggesters/FieldSuggest.svelte";
 | 
						import PropertySuggest from "../suggesters/PropertySuggest.svelte";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	type Props = Omit<ComponentProps<typeof Item>, "control"> & {
 | 
						type Props = Omit<ComponentProps<typeof Item>, "control"> & {
 | 
				
			||||||
		app: App;
 | 
							app: App;
 | 
				
			||||||
| 
						 | 
					@ -12,9 +12,9 @@
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let {
 | 
						let {
 | 
				
			||||||
 | 
							app,
 | 
				
			||||||
		name,
 | 
							name,
 | 
				
			||||||
		description,
 | 
							description,
 | 
				
			||||||
		app,
 | 
					 | 
				
			||||||
		id,
 | 
							id,
 | 
				
			||||||
		value = $bindable(),
 | 
							value = $bindable(),
 | 
				
			||||||
		accepts,
 | 
							accepts,
 | 
				
			||||||
| 
						 | 
					@ -23,6 +23,6 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<Item {name} {description}>
 | 
					<Item {name} {description}>
 | 
				
			||||||
	{#snippet control()}
 | 
						{#snippet control()}
 | 
				
			||||||
		<FieldSuggest {id} {app} asString bind:value {accepts} />
 | 
							<PropertySuggest {app} {id} asString bind:value {accepts} />
 | 
				
			||||||
	{/snippet}
 | 
						{/snippet}
 | 
				
			||||||
</Item>
 | 
					</Item>
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,11 @@
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
	import { createPropertyStore } from "@ui/stores/metadata.svelte";
 | 
						import { createPropertyStore } from "@ui/stores/metadata.svelte";
 | 
				
			||||||
	import Stat from "./Stat.svelte";
 | 
						import Stat from "./Stat.svelte";
 | 
				
			||||||
 | 
						import type { BookMetadata } from "@src/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	type Props = {
 | 
						type Props = {
 | 
				
			||||||
		label: string;
 | 
							label: string;
 | 
				
			||||||
		property: string;
 | 
							property: keyof BookMetadata;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { label, property }: Props = $props();
 | 
						const { label, property }: Props = $props();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,11 @@
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
	import { createPropertyStore } from "@ui/stores/metadata.svelte";
 | 
						import { createPropertyStore } from "@ui/stores/metadata.svelte";
 | 
				
			||||||
	import Stat from "./Stat.svelte";
 | 
						import Stat from "./Stat.svelte";
 | 
				
			||||||
 | 
						import type { BookMetadata } from "@src/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	type Props = {
 | 
						type Props = {
 | 
				
			||||||
		label: string;
 | 
							label: string;
 | 
				
			||||||
		property: string;
 | 
							property: keyof BookMetadata;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { label, property }: Props = $props();
 | 
						const { label, property }: Props = $props();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,11 @@
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
	import { createPropertyStore } from "@ui/stores/metadata.svelte";
 | 
						import { createPropertyStore } from "@ui/stores/metadata.svelte";
 | 
				
			||||||
	import Stat from "./Stat.svelte";
 | 
						import Stat from "./Stat.svelte";
 | 
				
			||||||
 | 
						import type { BookMetadata } from "@src/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	type Props = {
 | 
						type Props = {
 | 
				
			||||||
		label: string;
 | 
							label: string;
 | 
				
			||||||
		property: string;
 | 
							property: keyof BookMetadata;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { label, property }: Props = $props();
 | 
						const { label, property }: Props = $props();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,8 +32,8 @@
 | 
				
			||||||
	let items: Item<Field | string>[] = $state([]);
 | 
						let items: Item<Field | string>[] = $state([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async function handleChange(query: string) {
 | 
						async function handleChange(query: string) {
 | 
				
			||||||
		const typesContent = await this.app.vault.adapter.read(
 | 
							const typesContent = await app.vault.adapter.read(
 | 
				
			||||||
			this.app.vault.configDir + "/types.json",
 | 
								app.vault.configDir + "/types.json",
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
		const types = JSON.parse(typesContent).types as Record<string, string>;
 | 
							const types = JSON.parse(typesContent).types as Record<string, string>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,7 @@
 | 
				
			||||||
		createSettings,
 | 
							createSettings,
 | 
				
			||||||
		setSettingsContext,
 | 
							setSettingsContext,
 | 
				
			||||||
	} from "@ui/stores/settings.svelte";
 | 
						} from "@ui/stores/settings.svelte";
 | 
				
			||||||
 | 
						import moment from "@external/moment";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const INPUT_DATETIME_FORMAT = "YYYY-MM-DDTHH:mm";
 | 
						const INPUT_DATETIME_FORMAT = "YYYY-MM-DDTHH:mm";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,7 +39,6 @@
 | 
				
			||||||
	let pagesRemaining = $state(entry?.pagesRemaining ?? 0);
 | 
						let pagesRemaining = $state(entry?.pagesRemaining ?? 0);
 | 
				
			||||||
	let createdAt = $state(
 | 
						let createdAt = $state(
 | 
				
			||||||
		entry?.createdAt?.format(INPUT_DATETIME_FORMAT) ??
 | 
							entry?.createdAt?.format(INPUT_DATETIME_FORMAT) ??
 | 
				
			||||||
			// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
			moment().format(INPUT_DATETIME_FORMAT),
 | 
								moment().format(INPUT_DATETIME_FORMAT),
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,10 +49,7 @@
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	$effect(() => {
 | 
						$effect(() => {
 | 
				
			||||||
		pagesRemaining =
 | 
							pagesRemaining = bookMetadata?.book.pageCount ?? 0 - pagesReadTotal;
 | 
				
			||||||
			(bookMetadata?.frontmatter?.[
 | 
					 | 
				
			||||||
				settingsStore.settings.pageCountProperty
 | 
					 | 
				
			||||||
			] ?? 0) - pagesReadTotal;
 | 
					 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	$effect(() => {
 | 
						$effect(() => {
 | 
				
			||||||
| 
						 | 
					@ -71,7 +68,6 @@
 | 
				
			||||||
			pagesRead,
 | 
								pagesRead,
 | 
				
			||||||
			pagesReadTotal,
 | 
								pagesReadTotal,
 | 
				
			||||||
			pagesRemaining,
 | 
								pagesRemaining,
 | 
				
			||||||
			// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
			createdAt: moment(createdAt),
 | 
								createdAt: moment(createdAt),
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,11 +3,12 @@
 | 
				
			||||||
	import FileSuggestItem from "@ui/components/setting/FileSuggestItem.svelte";
 | 
						import FileSuggestItem from "@ui/components/setting/FileSuggestItem.svelte";
 | 
				
			||||||
	import FolderSuggestItem from "@ui/components/setting/FolderSuggestItem.svelte";
 | 
						import FolderSuggestItem from "@ui/components/setting/FolderSuggestItem.svelte";
 | 
				
			||||||
	import TextInputItem from "@ui/components/setting/TextInputItem.svelte";
 | 
						import TextInputItem from "@ui/components/setting/TextInputItem.svelte";
 | 
				
			||||||
	import FieldSuggestItem from "@ui/components/setting/FieldSuggestItem.svelte";
 | 
						import PropertySuggestItem from "@ui/components/setting/PropertySuggestItem.svelte";
 | 
				
			||||||
	import ToggleItem from "@ui/components/setting/ToggleItem.svelte";
 | 
						import ToggleItem from "@ui/components/setting/ToggleItem.svelte";
 | 
				
			||||||
	import type BookTrackerPlugin from "@src/main";
 | 
						import type BookTrackerPlugin from "@src/main";
 | 
				
			||||||
	import { createSettings } from "@ui/stores/settings.svelte";
 | 
						import { createSettings } from "@ui/stores/settings.svelte";
 | 
				
			||||||
	import { onMount } from "svelte";
 | 
						import { onMount } from "svelte";
 | 
				
			||||||
 | 
						import type { BookTrackerSettings } from "./types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	type Props = {
 | 
						type Props = {
 | 
				
			||||||
		plugin: BookTrackerPlugin;
 | 
							plugin: BookTrackerPlugin;
 | 
				
			||||||
| 
						 | 
					@ -19,6 +20,149 @@
 | 
				
			||||||
	const settingsStore = createSettings(plugin);
 | 
						const settingsStore = createSettings(plugin);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	onMount(async () => settingsStore.load());
 | 
						onMount(async () => settingsStore.load());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						interface Property {
 | 
				
			||||||
 | 
							label: string;
 | 
				
			||||||
 | 
							description: string;
 | 
				
			||||||
 | 
							key: keyof BookTrackerSettings;
 | 
				
			||||||
 | 
							type: "text" | "multitext" | "number" | "date";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const properties: Property[] = [
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Title",
 | 
				
			||||||
 | 
								description: "The property which contains the book's title.",
 | 
				
			||||||
 | 
								key: "titleProperty",
 | 
				
			||||||
 | 
								type: "text",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Subtitle",
 | 
				
			||||||
 | 
								description: "The property which contains the book's subtitle.",
 | 
				
			||||||
 | 
								key: "subtitleProperty",
 | 
				
			||||||
 | 
								type: "text",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Description",
 | 
				
			||||||
 | 
								description:
 | 
				
			||||||
 | 
									"The property which contains the description/blurb of the book.",
 | 
				
			||||||
 | 
								key: "descriptionProperty",
 | 
				
			||||||
 | 
								type: "text",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Authors",
 | 
				
			||||||
 | 
								description:
 | 
				
			||||||
 | 
									"The property which contains the list of the book's author names.",
 | 
				
			||||||
 | 
								key: "authorsProperty",
 | 
				
			||||||
 | 
								type: "multitext",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Series Title",
 | 
				
			||||||
 | 
								description:
 | 
				
			||||||
 | 
									"The property which contains the title of the series the book belongs to.",
 | 
				
			||||||
 | 
								key: "seriesTitleProperty",
 | 
				
			||||||
 | 
								type: "text",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Series Position",
 | 
				
			||||||
 | 
								description:
 | 
				
			||||||
 | 
									"The property which contains the position of the book in the series.",
 | 
				
			||||||
 | 
								key: "seriesPositionProperty",
 | 
				
			||||||
 | 
								type: "number",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Start Date",
 | 
				
			||||||
 | 
								description:
 | 
				
			||||||
 | 
									"The property where the book's start date will be stored.",
 | 
				
			||||||
 | 
								key: "startDateProperty",
 | 
				
			||||||
 | 
								type: "date",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "End Date",
 | 
				
			||||||
 | 
								description:
 | 
				
			||||||
 | 
									"The property where the book's end date will be stored.",
 | 
				
			||||||
 | 
								key: "endDateProperty",
 | 
				
			||||||
 | 
								type: "date",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Status",
 | 
				
			||||||
 | 
								description:
 | 
				
			||||||
 | 
									"The property which contains the book's reading status.",
 | 
				
			||||||
 | 
								key: "statusProperty",
 | 
				
			||||||
 | 
								type: "text",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Rating",
 | 
				
			||||||
 | 
								description:
 | 
				
			||||||
 | 
									"The property where your rating of the book will be stored.",
 | 
				
			||||||
 | 
								key: "ratingProperty",
 | 
				
			||||||
 | 
								type: "number",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Spice",
 | 
				
			||||||
 | 
								description: `The property where your spice rating of the book will be stored.
 | 
				
			||||||
 | 
								Set to empty to if you're not interested in this feature.`,
 | 
				
			||||||
 | 
								key: "spiceProperty",
 | 
				
			||||||
 | 
								type: "number",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Format",
 | 
				
			||||||
 | 
								description: `The property which contains the book's format.
 | 
				
			||||||
 | 
								(e.g. E-Book, Audiobook, Physical, etc.)`,
 | 
				
			||||||
 | 
								key: "formatProperty",
 | 
				
			||||||
 | 
								type: "text",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Source",
 | 
				
			||||||
 | 
								description: `The property which contains the where you obtained the book.
 | 
				
			||||||
 | 
								(e.g. Amazon, Library, Bookstore, etc.)`,
 | 
				
			||||||
 | 
								key: "sourceProperty",
 | 
				
			||||||
 | 
								type: "multitext",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Categories",
 | 
				
			||||||
 | 
								description: "The property which contains the book's categories.",
 | 
				
			||||||
 | 
								key: "categoriesProperty",
 | 
				
			||||||
 | 
								type: "multitext",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Publisher",
 | 
				
			||||||
 | 
								description: "The property which contains the book's publisher.",
 | 
				
			||||||
 | 
								key: "publisherProperty",
 | 
				
			||||||
 | 
								type: "text",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Publish Date",
 | 
				
			||||||
 | 
								description: "The property which contains the book's publish date.",
 | 
				
			||||||
 | 
								key: "publishDateProperty",
 | 
				
			||||||
 | 
								type: "date",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Page Count",
 | 
				
			||||||
 | 
								description: "The property which contains the book's page count.",
 | 
				
			||||||
 | 
								key: "pageCountProperty",
 | 
				
			||||||
 | 
								type: "number",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "ISBN",
 | 
				
			||||||
 | 
								description: "The property which contains the book's ISBN.",
 | 
				
			||||||
 | 
								key: "isbnProperty",
 | 
				
			||||||
 | 
								type: "text",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Cover Image URL",
 | 
				
			||||||
 | 
								description:
 | 
				
			||||||
 | 
									"The property which contains the book's cover image URL.",
 | 
				
			||||||
 | 
								key: "coverImageUrlProperty",
 | 
				
			||||||
 | 
								type: "text",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								label: "Local Cover Path",
 | 
				
			||||||
 | 
								description:
 | 
				
			||||||
 | 
									"The property which contains the book's local cover path.",
 | 
				
			||||||
 | 
								key: "localCoverPathProperty",
 | 
				
			||||||
 | 
								type: "text",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						];
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="obt-settings">
 | 
					<div class="obt-settings">
 | 
				
			||||||
| 
						 | 
					@ -93,54 +237,15 @@
 | 
				
			||||||
		bind:checked={settingsStore.settings.overwriteExistingCovers}
 | 
							bind:checked={settingsStore.settings.overwriteExistingCovers}
 | 
				
			||||||
	/>
 | 
						/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<Header title="Reading Log" />
 | 
						<Header title="Book Properties" />
 | 
				
			||||||
	<FieldSuggestItem
 | 
						{#each properties as property}
 | 
				
			||||||
		{app}
 | 
							<PropertySuggestItem
 | 
				
			||||||
		id="status-field"
 | 
								{app}
 | 
				
			||||||
		name="Status Field"
 | 
								id={property.key}
 | 
				
			||||||
		description="Select the field to use for reading status."
 | 
								name={`${property.label} Property`}
 | 
				
			||||||
		bind:value={settingsStore.settings.statusProperty}
 | 
								description={property.description}
 | 
				
			||||||
		accepts={["text"]}
 | 
								bind:value={settingsStore.settings[property.key] as string}
 | 
				
			||||||
	/>
 | 
								accepts={[property.type]}
 | 
				
			||||||
	<FieldSuggestItem
 | 
							/>
 | 
				
			||||||
		{app}
 | 
						{/each}
 | 
				
			||||||
		id="start-date-field"
 | 
					 | 
				
			||||||
		name="Start Date Field"
 | 
					 | 
				
			||||||
		description="Select the field to use for start date."
 | 
					 | 
				
			||||||
		bind:value={settingsStore.settings.startDateProperty}
 | 
					 | 
				
			||||||
		accepts={["date"]}
 | 
					 | 
				
			||||||
	/>
 | 
					 | 
				
			||||||
	<FieldSuggestItem
 | 
					 | 
				
			||||||
		{app}
 | 
					 | 
				
			||||||
		id="end-date-field"
 | 
					 | 
				
			||||||
		name="End Date Field"
 | 
					 | 
				
			||||||
		description="Select the field to use for end date."
 | 
					 | 
				
			||||||
		bind:value={settingsStore.settings.endDateProperty}
 | 
					 | 
				
			||||||
		accepts={["date"]}
 | 
					 | 
				
			||||||
	/>
 | 
					 | 
				
			||||||
	<FieldSuggestItem
 | 
					 | 
				
			||||||
		{app}
 | 
					 | 
				
			||||||
		id="rating-field"
 | 
					 | 
				
			||||||
		name="Rating Field"
 | 
					 | 
				
			||||||
		description="Select the field to use for rating."
 | 
					 | 
				
			||||||
		bind:value={settingsStore.settings.ratingProperty}
 | 
					 | 
				
			||||||
		accepts={["number"]}
 | 
					 | 
				
			||||||
	/>
 | 
					 | 
				
			||||||
	<FieldSuggestItem
 | 
					 | 
				
			||||||
		{app}
 | 
					 | 
				
			||||||
		id="spice-field"
 | 
					 | 
				
			||||||
		name="Spice Field"
 | 
					 | 
				
			||||||
		description={`Select the field to use for spice rating.
 | 
					 | 
				
			||||||
		Set to empty to disable.`}
 | 
					 | 
				
			||||||
		bind:value={settingsStore.settings.spiceProperty}
 | 
					 | 
				
			||||||
		accepts={["number"]}
 | 
					 | 
				
			||||||
	/>
 | 
					 | 
				
			||||||
	<FieldSuggestItem
 | 
					 | 
				
			||||||
		{app}
 | 
					 | 
				
			||||||
		id="page-count-field"
 | 
					 | 
				
			||||||
		name="Page Count Field"
 | 
					 | 
				
			||||||
		description="Select the field to use for page count."
 | 
					 | 
				
			||||||
		bind:value={settingsStore.settings.pageCountProperty}
 | 
					 | 
				
			||||||
		accepts={["number"]}
 | 
					 | 
				
			||||||
	/>
 | 
					 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,12 +9,26 @@ export interface BookTrackerSettings {
 | 
				
			||||||
	coverFolder: string;
 | 
						coverFolder: string;
 | 
				
			||||||
	groupCoversByFirstLetter: boolean;
 | 
						groupCoversByFirstLetter: boolean;
 | 
				
			||||||
	overwriteExistingCovers: boolean;
 | 
						overwriteExistingCovers: boolean;
 | 
				
			||||||
	statusProperty: string;
 | 
						titleProperty: string;
 | 
				
			||||||
 | 
						subtitleProperty: string;
 | 
				
			||||||
 | 
						descriptionProperty: string;
 | 
				
			||||||
 | 
						authorsProperty: string;
 | 
				
			||||||
 | 
						seriesTitleProperty: string;
 | 
				
			||||||
 | 
						seriesPositionProperty: string;
 | 
				
			||||||
	startDateProperty: string;
 | 
						startDateProperty: string;
 | 
				
			||||||
	endDateProperty: string;
 | 
						endDateProperty: string;
 | 
				
			||||||
 | 
						statusProperty: string;
 | 
				
			||||||
	ratingProperty: string;
 | 
						ratingProperty: string;
 | 
				
			||||||
	spiceProperty: string;
 | 
						spiceProperty: string;
 | 
				
			||||||
 | 
						formatProperty: string;
 | 
				
			||||||
 | 
						sourceProperty: string;
 | 
				
			||||||
 | 
						categoriesProperty: string;
 | 
				
			||||||
 | 
						publisherProperty: string;
 | 
				
			||||||
 | 
						publishDateProperty: string;
 | 
				
			||||||
	pageCountProperty: string;
 | 
						pageCountProperty: string;
 | 
				
			||||||
 | 
						isbnProperty: string;
 | 
				
			||||||
 | 
						coverImageUrlProperty: string;
 | 
				
			||||||
 | 
						localCoverPathProperty: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DEFAULT_SETTINGS: BookTrackerSettings = {
 | 
					export const DEFAULT_SETTINGS: BookTrackerSettings = {
 | 
				
			||||||
| 
						 | 
					@ -28,10 +42,24 @@ export const DEFAULT_SETTINGS: BookTrackerSettings = {
 | 
				
			||||||
	coverFolder: "images/covers",
 | 
						coverFolder: "images/covers",
 | 
				
			||||||
	groupCoversByFirstLetter: true,
 | 
						groupCoversByFirstLetter: true,
 | 
				
			||||||
	overwriteExistingCovers: false,
 | 
						overwriteExistingCovers: false,
 | 
				
			||||||
	statusProperty: "status",
 | 
						titleProperty: "title",
 | 
				
			||||||
 | 
						subtitleProperty: "subtitle",
 | 
				
			||||||
 | 
						descriptionProperty: "description",
 | 
				
			||||||
 | 
						authorsProperty: "authors",
 | 
				
			||||||
 | 
						seriesTitleProperty: "seriesTitle",
 | 
				
			||||||
 | 
						seriesPositionProperty: "seriesPosition",
 | 
				
			||||||
	startDateProperty: "startDate",
 | 
						startDateProperty: "startDate",
 | 
				
			||||||
	endDateProperty: "endDate",
 | 
						endDateProperty: "endDate",
 | 
				
			||||||
 | 
						statusProperty: "status",
 | 
				
			||||||
	ratingProperty: "rating",
 | 
						ratingProperty: "rating",
 | 
				
			||||||
	spiceProperty: "",
 | 
						spiceProperty: "",
 | 
				
			||||||
 | 
						formatProperty: "type",
 | 
				
			||||||
 | 
						sourceProperty: "source",
 | 
				
			||||||
 | 
						categoriesProperty: "categories",
 | 
				
			||||||
 | 
						publisherProperty: "publisher",
 | 
				
			||||||
 | 
						publishDateProperty: "publishDate",
 | 
				
			||||||
	pageCountProperty: "pageCount",
 | 
						pageCountProperty: "pageCount",
 | 
				
			||||||
 | 
						isbnProperty: "isbn",
 | 
				
			||||||
 | 
						coverImageUrlProperty: "coverImageUrl",
 | 
				
			||||||
 | 
						localCoverPathProperty: "localCoverPath",
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import type { Moment } from "moment";
 | 
					import type { Moment } from "@external/moment";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ALL_TIME = "ALL_TIME";
 | 
					export const ALL_TIME = "ALL_TIME";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,23 +1,17 @@
 | 
				
			||||||
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 {
 | 
					 | 
				
			||||||
	createSettings,
 | 
					 | 
				
			||||||
	getSettingsContext,
 | 
					 | 
				
			||||||
	setSettingsContext,
 | 
					 | 
				
			||||||
} from "./settings.svelte";
 | 
					 | 
				
			||||||
import type BookTrackerPlugin from "@src/main";
 | 
					import type BookTrackerPlugin from "@src/main";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
	createDateFilter,
 | 
						createDateFilter,
 | 
				
			||||||
	type DateFilterStore,
 | 
						type DateFilterStore,
 | 
				
			||||||
	type DateFilterStoreOptions,
 | 
						type DateFilterStoreOptions,
 | 
				
			||||||
} from "./date-filter.svelte";
 | 
					} from "./date-filter.svelte";
 | 
				
			||||||
import type { ReadingState } from "@src/types";
 | 
					import type { BookMetadata, ReadingState } from "@src/types";
 | 
				
			||||||
import type { BookTrackerPluginSettings } from "@ui/settings";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type FileMetadata = {
 | 
					export type FileMetadata = {
 | 
				
			||||||
	file: TFile;
 | 
						file: TFile;
 | 
				
			||||||
	frontmatter: Record<string, any>;
 | 
						book: BookMetadata;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type FileProperty = {
 | 
					export type FileProperty = {
 | 
				
			||||||
| 
						 | 
					@ -33,22 +27,20 @@ export interface MetadataStore extends DateFilterStore {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getMetadata(
 | 
					function getMetadata(
 | 
				
			||||||
	plugin: BookTrackerPlugin,
 | 
						plugin: BookTrackerPlugin,
 | 
				
			||||||
	settings: BookTrackerPluginSettings,
 | 
					 | 
				
			||||||
	state: ReadingState | null
 | 
						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 book = plugin.getBookMetadata(file);
 | 
				
			||||||
			plugin.app.metadataCache.getFileCache(file)?.frontmatter ?? {};
 | 
							if (!book) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (
 | 
					 | 
				
			||||||
			!(settings.statusProperty in frontmatter) ||
 | 
					 | 
				
			||||||
			(state !== null && frontmatter[settings.statusProperty] !== state)
 | 
					 | 
				
			||||||
		) {
 | 
					 | 
				
			||||||
			continue;
 | 
								continue;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		metadata.push({ file, frontmatter });
 | 
							if (state && book.status !== state) {
 | 
				
			||||||
 | 
								continue;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							metadata.push({ file, book });
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return metadata;
 | 
						return metadata;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -64,29 +56,19 @@ export function createMetadata(
 | 
				
			||||||
	plugin: BookTrackerPlugin,
 | 
						plugin: BookTrackerPlugin,
 | 
				
			||||||
	{ statusFilter = STATUS_READ, ...dateFilterOpts }: MetadataStoreOptions = {}
 | 
						{ statusFilter = STATUS_READ, ...dateFilterOpts }: MetadataStoreOptions = {}
 | 
				
			||||||
): MetadataStore {
 | 
					): MetadataStore {
 | 
				
			||||||
	let settingsStore = getSettingsContext();
 | 
						const initialMetadata = getMetadata(plugin, statusFilter);
 | 
				
			||||||
	if (!settingsStore) {
 | 
					 | 
				
			||||||
		settingsStore = createSettings(plugin);
 | 
					 | 
				
			||||||
		setSettingsContext(settingsStore);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const initialMetadata = getMetadata(
 | 
					 | 
				
			||||||
		plugin,
 | 
					 | 
				
			||||||
		settingsStore.settings,
 | 
					 | 
				
			||||||
		statusFilter
 | 
					 | 
				
			||||||
	);
 | 
					 | 
				
			||||||
	let metadata: FileMetadata[] = $state(initialMetadata);
 | 
						let metadata: FileMetadata[] = $state(initialMetadata);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	$effect(() => {
 | 
						$effect(() => {
 | 
				
			||||||
		metadata = getMetadata(plugin, settingsStore.settings, statusFilter);
 | 
							metadata = getMetadata(plugin, statusFilter);
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function onChanged(file: TFile, _data: string, cache: CachedMetadata) {
 | 
						function onChanged(file: TFile, _data: string, cache: CachedMetadata) {
 | 
				
			||||||
		metadata = metadata.map((f) => {
 | 
							metadata = metadata.map((f) => {
 | 
				
			||||||
			if (f.file.path === file.path) {
 | 
								if (f.file.path === file.path) {
 | 
				
			||||||
				return {
 | 
									return {
 | 
				
			||||||
					...f,
 | 
										file: f.file,
 | 
				
			||||||
					frontmatter: cache.frontmatter ?? {},
 | 
										book: plugin.frontmatterToMetadata(cache.frontmatter),
 | 
				
			||||||
				};
 | 
									};
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return f;
 | 
								return f;
 | 
				
			||||||
| 
						 | 
					@ -101,12 +83,7 @@ export function createMetadata(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const dateFilter = createDateFilter(
 | 
						const dateFilter = createDateFilter(
 | 
				
			||||||
		() => metadata,
 | 
							() => metadata,
 | 
				
			||||||
		(f) => {
 | 
							(f) => f.book.endDate,
 | 
				
			||||||
			// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
			return moment(
 | 
					 | 
				
			||||||
				f.frontmatter[settingsStore.settings.endDateProperty]
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		dateFilterOpts
 | 
							dateFilterOpts
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -159,15 +136,15 @@ interface PropertyStore {
 | 
				
			||||||
	get propertyData(): FileProperty[];
 | 
						get propertyData(): FileProperty[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function createPropertyStore(
 | 
					export function createPropertyStore<T extends keyof BookMetadata>(
 | 
				
			||||||
	property: string,
 | 
						property: T,
 | 
				
			||||||
	filter: (value: any) => boolean = notEmpty
 | 
						filter: (value: BookMetadata[T]) => boolean = notEmpty
 | 
				
			||||||
): PropertyStore {
 | 
					): PropertyStore {
 | 
				
			||||||
	const store = getMetadataContext();
 | 
						const store = getMetadataContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const propertyData = $derived(
 | 
						const propertyData = $derived(
 | 
				
			||||||
		store.metadata
 | 
							store.metadata
 | 
				
			||||||
			.map((f) => ({ ...f, value: f.frontmatter[property] }))
 | 
								.map((f) => ({ file: f.file, value: f.book[property] }))
 | 
				
			||||||
			.filter((f) => (filter ? filter(f.value) : true))
 | 
								.filter((f) => (filter ? filter(f.value) : true))
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import type { Storage } from "./Storage";
 | 
					import type { Storage } from "./Storage";
 | 
				
			||||||
import type { Moment } from "moment";
 | 
					import moment, { type Moment } from "@external/moment";
 | 
				
			||||||
import { v4 as uuidv4 } from "uuid";
 | 
					import { v4 as uuidv4 } from "uuid";
 | 
				
			||||||
import { EventEmitter } from "./event";
 | 
					import { EventEmitter } from "./event";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -47,7 +47,6 @@ export class ReadingLog extends EventEmitter<ReadingLogEventMap> {
 | 
				
			||||||
		if (entries) {
 | 
							if (entries) {
 | 
				
			||||||
			this.entries = entries.map((entry) => ({
 | 
								this.entries = entries.map((entry) => ({
 | 
				
			||||||
				...entry,
 | 
									...entry,
 | 
				
			||||||
				// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
				createdAt: moment(entry.createdAt),
 | 
									createdAt: moment(entry.createdAt),
 | 
				
			||||||
			}));
 | 
								}));
 | 
				
			||||||
			this.emit("load", { entries: this.entries });
 | 
								this.emit("load", { entries: this.entries });
 | 
				
			||||||
| 
						 | 
					@ -66,7 +65,6 @@ export class ReadingLog extends EventEmitter<ReadingLogEventMap> {
 | 
				
			||||||
			filename,
 | 
								filename,
 | 
				
			||||||
			this.entries.map((entry) => ({
 | 
								this.entries.map((entry) => ({
 | 
				
			||||||
				...entry,
 | 
									...entry,
 | 
				
			||||||
				// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
				createdAt: moment(entry.createdAt).toISOString(true),
 | 
									createdAt: moment(entry.createdAt).toISOString(true),
 | 
				
			||||||
			}))
 | 
								}))
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
| 
						 | 
					@ -107,7 +105,6 @@ export class ReadingLog extends EventEmitter<ReadingLogEventMap> {
 | 
				
			||||||
				: pageEnded,
 | 
									: pageEnded,
 | 
				
			||||||
			pagesReadTotal: pageEnded,
 | 
								pagesReadTotal: pageEnded,
 | 
				
			||||||
			pagesRemaining: pageCount - pageEnded,
 | 
								pagesRemaining: pageCount - pageEnded,
 | 
				
			||||||
			// @ts-expect-error Moment is provided by Obsidian
 | 
					 | 
				
			||||||
			createdAt: moment(),
 | 
								createdAt: moment(),
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,6 +23,7 @@
 | 
				
			||||||
    "paths": {
 | 
					    "paths": {
 | 
				
			||||||
      "@commands/*": ["src/commands/*"],
 | 
					      "@commands/*": ["src/commands/*"],
 | 
				
			||||||
      "@data-sources/*": ["src/data-sources/*"],
 | 
					      "@data-sources/*": ["src/data-sources/*"],
 | 
				
			||||||
 | 
					      "@external/*": ["src/external/*"],
 | 
				
			||||||
      "@ui/*": ["src/ui/*"],
 | 
					      "@ui/*": ["src/ui/*"],
 | 
				
			||||||
      "@utils/*": ["src/utils/*"],
 | 
					      "@utils/*": ["src/utils/*"],
 | 
				
			||||||
      "@src/*": ["src/*"]
 | 
					      "@src/*": ["src/*"]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue