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