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}
|
||||
<option value={ALL_TIME}>All Time</option>
|
||||
{#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}>
|
||||
<option value={ALL_TIME}>Select Month</option>
|
||||
{#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}
|
||||
<th>Series</th>
|
||||
<th>#</th>
|
||||
{#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>
|
||||
<td>
|
||||
{meta.book.seriesTitle}
|
||||
</td>
|
||||
<td>
|
||||
{meta.book.seriesPosition}
|
||||
</td>
|
||||
{#if settings.seriesTitleProperty}
|
||||
<td>
|
||||
{#if seriesTitle}{seriesTitle}{/if}
|
||||
</td>
|
||||
{/if}
|
||||
{#if settings.seriesNumberProperty}
|
||||
<td>
|
||||
{#if seriesNumber}{seriesNumber}{/if}
|
||||
</td>
|
||||
{/if}
|
||||
{#if settings.statusFilter === STATUS_IN_PROGRESS || settings.statusFilter === STATUS_READ}
|
||||
<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
|
||||
{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"]}
|
||||
/>
|
||||
<Header title="Book Properties" />
|
||||
{#each properties as property}
|
||||
<PropertySuggestItem
|
||||
{app}
|
||||
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