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}
|
||||||
|
{#if !disableAllTime}
|
||||||
<option value={ALL_TIME}>All Time</option>
|
<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}>
|
||||||
|
{#if disableAllTime}
|
||||||
<option value={ALL_TIME}>Select Month</option>
|
<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>
|
||||||
{/if}
|
|
||||||
{#if settings.seriesNumberProperty}
|
|
||||||
<th>#</th>
|
<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>
|
||||||
{#if settings.seriesTitleProperty}
|
|
||||||
<td>
|
<td>
|
||||||
{#if seriesTitle}{seriesTitle}{/if}
|
{meta.book.seriesTitle}
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
|
||||||
{#if settings.seriesNumberProperty}
|
|
||||||
<td>
|
<td>
|
||||||
{#if seriesNumber}{seriesNumber}{/if}
|
{meta.book.seriesPosition}
|
||||||
</td>
|
</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}
|
||||||
|
<PropertySuggestItem
|
||||||
{app}
|
{app}
|
||||||
id="status-field"
|
id={property.key}
|
||||||
name="Status Field"
|
name={`${property.label} Property`}
|
||||||
description="Select the field to use for reading status."
|
description={property.description}
|
||||||
bind:value={settingsStore.settings.statusProperty}
|
bind:value={settingsStore.settings[property.key] as string}
|
||||||
accepts={["text"]}
|
accepts={[property.type]}
|
||||||
/>
|
|
||||||
<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"]}
|
|
||||||
/>
|
/>
|
||||||
|
{/each}
|
||||||
</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