From b9f146f9226ce347e87912b7ffca4b702664722f Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Sat, 5 Jul 2025 10:42:25 -0400 Subject: [PATCH] Put actual books on bookshelf view --- src/commands/LogReadingFinishedCommand.ts | 4 +- src/commands/LogReadingStartedCommand.ts | 4 +- src/commands/ResetReadingStatusCommand.ts | 4 +- src/const.ts | 6 +- src/types.ts | 12 +- src/ui/code-blocks/BookshelfCodeBlock.ts | 7 + .../code-blocks/BookshelfCodeBlockView.svelte | 272 ++++++++---------- .../ReadingLogCodeBlockView.svelte | 5 +- src/ui/components/bookshelf/Book.svelte | 13 +- .../bookshelf/BookStackElement.svelte | 10 +- src/ui/components/bookshelf/bookshelf.scss | 1 + .../designs/book/BookColoredSpine.svelte | 7 +- .../bookshelf/designs/book/BookDefault.svelte | 7 +- .../designs/book/BookDualTopBands.svelte | 7 +- .../designs/book/BookOnDisplay.svelte | 7 +- .../designs/book/BookSplitBands.svelte | 7 +- .../stack/BookStackColoredSpine.svelte | 7 +- .../designs/stack/BookStackDefault.svelte | 7 +- .../stack/BookStackDualTopBands.svelte | 7 +- .../designs/stack/BookStackSplitBands.svelte | 7 +- src/ui/stores/metadata.svelte.ts | 63 ++-- src/ui/stores/settings.svelte.ts | 2 +- src/utils/rand.ts | 7 + 23 files changed, 265 insertions(+), 208 deletions(-) diff --git a/src/commands/LogReadingFinishedCommand.ts b/src/commands/LogReadingFinishedCommand.ts index 394b3dd..4824c8d 100644 --- a/src/commands/LogReadingFinishedCommand.ts +++ b/src/commands/LogReadingFinishedCommand.ts @@ -10,7 +10,7 @@ import { EditorCheckCommand } from "./Command"; import type { BookTrackerPluginSettings } from "@ui/settings"; import { RatingModal } from "@ui/modals"; import type { ReadingLog } from "@utils/ReadingLog"; -import { READ_STATE } from "@src/const"; +import { STATUS_READ } from "@src/const"; import { mkdirRecursive, dirname } from "@utils/fs"; export class LogReadingFinishedCommand extends EditorCheckCommand { @@ -70,7 +70,7 @@ export class LogReadingFinishedCommand extends EditorCheckCommand { const endDate = moment().format("YYYY-MM-DD"); this.app.fileManager.processFrontMatter(file, (frontMatter) => { - frontMatter[this.settings.statusProperty] = READ_STATE; + frontMatter[this.settings.statusProperty] = STATUS_READ; frontMatter[this.settings.endDateProperty] = endDate; frontMatter[this.settings.ratingProperty] = ratings.rating; if (this.settings.spiceProperty !== "") { diff --git a/src/commands/LogReadingStartedCommand.ts b/src/commands/LogReadingStartedCommand.ts index 93938dd..1b55467 100644 --- a/src/commands/LogReadingStartedCommand.ts +++ b/src/commands/LogReadingStartedCommand.ts @@ -7,7 +7,7 @@ import { } from "obsidian"; import { EditorCheckCommand } from "./Command"; import type { BookTrackerPluginSettings } from "@ui/settings"; -import { IN_PROGRESS_STATE } from "@src/const"; +import { STATUS_IN_PROGRESS } from "@src/const"; export class LogReadingStartedCommand extends EditorCheckCommand { constructor( @@ -38,7 +38,7 @@ export class LogReadingStartedCommand extends EditorCheckCommand { const startDate = moment().format("YYYY-MM-DD"); this.app.fileManager.processFrontMatter(file, (frontMatter) => { - frontMatter[this.settings.statusProperty] = IN_PROGRESS_STATE; + frontMatter[this.settings.statusProperty] = STATUS_IN_PROGRESS; frontMatter[this.settings.startDateProperty] = startDate; }); diff --git a/src/commands/ResetReadingStatusCommand.ts b/src/commands/ResetReadingStatusCommand.ts index 27ae0b8..de5c083 100644 --- a/src/commands/ResetReadingStatusCommand.ts +++ b/src/commands/ResetReadingStatusCommand.ts @@ -8,7 +8,7 @@ import { import { EditorCheckCommand } from "./Command"; import type { BookTrackerPluginSettings } from "@ui/settings"; import type { ReadingLog } from "@utils/ReadingLog"; -import { TO_BE_READ_STATE } from "@src/const"; +import { STATUS_TO_BE_READ } from "@src/const"; export class ResetReadingStatusCommand extends EditorCheckCommand { constructor( @@ -38,7 +38,7 @@ export class ResetReadingStatusCommand extends EditorCheckCommand { const file = ctx.file!; this.app.fileManager.processFrontMatter(file, (frontMatter) => { - frontMatter[this.settings.statusProperty] = TO_BE_READ_STATE; + frontMatter[this.settings.statusProperty] = STATUS_TO_BE_READ; frontMatter[this.settings.startDateProperty] = ""; frontMatter[this.settings.endDateProperty] = ""; }); diff --git a/src/const.ts b/src/const.ts index 43e92cb..16e1a51 100644 --- a/src/const.ts +++ b/src/const.ts @@ -1,6 +1,6 @@ -export const TO_BE_READ_STATE = "To Be Read"; -export const IN_PROGRESS_STATE = "Currently Reading"; -export const READ_STATE = "Read"; +export const STATUS_TO_BE_READ = "To Be Read"; +export const STATUS_IN_PROGRESS = "Currently Reading"; +export const STATUS_READ = "Read"; export const CONTENT_TYPE_EXTENSIONS: Record = { "image/jpeg": "jpg", diff --git a/src/types.ts b/src/types.ts index 27dc91a..9a69b8f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,8 @@ -import type { IN_PROGRESS_STATE, READ_STATE, TO_BE_READ_STATE } from "./const"; +import type { + STATUS_IN_PROGRESS, + STATUS_READ, + STATUS_TO_BE_READ, +} from "./const"; export interface Author { name: string; @@ -24,8 +28,8 @@ export interface Book { isbn13: string; } -export type ToBeReadState = typeof TO_BE_READ_STATE; -export type InProgressState = typeof IN_PROGRESS_STATE; -export type ReadState = typeof READ_STATE; +export type ToBeReadState = typeof STATUS_TO_BE_READ; +export type InProgressState = typeof STATUS_IN_PROGRESS; +export type ReadState = typeof STATUS_READ; export type ReadingState = ToBeReadState | InProgressState | ReadState; diff --git a/src/ui/code-blocks/BookshelfCodeBlock.ts b/src/ui/code-blocks/BookshelfCodeBlock.ts index 0caa87d..35cc4fd 100644 --- a/src/ui/code-blocks/BookshelfCodeBlock.ts +++ b/src/ui/code-blocks/BookshelfCodeBlock.ts @@ -2,6 +2,7 @@ import { registerCodeBlockRenderer } from "."; import { SvelteCodeBlockRenderer } from "./SvelteCodeBlockRenderer"; import BookshelfCodeBlockView from "./BookshelfCodeBlockView.svelte"; import type BookTrackerPlugin from "@src/main"; +import z from "zod/v4"; export function registerBookshelfCodeBlockProcessor( plugin: BookTrackerPlugin @@ -13,6 +14,12 @@ export function registerBookshelfCodeBlockProcessor( ); } +export const BookshelfSettingsSchema = z.object({ + titleProperty: z.string(), + subtitleProperty: z.optional(z.string()), + authorsProperty: z.string(), +}); + export class BookshelfCodeBlockRenderer extends SvelteCodeBlockRenderer< typeof BookshelfCodeBlockView > { diff --git a/src/ui/code-blocks/BookshelfCodeBlockView.svelte b/src/ui/code-blocks/BookshelfCodeBlockView.svelte index b6a982d..cc2e81e 100644 --- a/src/ui/code-blocks/BookshelfCodeBlockView.svelte +++ b/src/ui/code-blocks/BookshelfCodeBlockView.svelte @@ -1,5 +1,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + {#each books as book} + {#if Array.isArray(book)} + + {#each book as bookData} + + plugin.app.workspace.openLinkText( + bookData.file.path, + "", + true, + )} + /> + {/each} + + {:else} + + plugin.app.workspace.openLinkText(book.file.path, "", true)} + /> + {/if} + {/each} diff --git a/src/ui/code-blocks/ReadingLogCodeBlockView.svelte b/src/ui/code-blocks/ReadingLogCodeBlockView.svelte index e746c92..7c720de 100644 --- a/src/ui/code-blocks/ReadingLogCodeBlockView.svelte +++ b/src/ui/code-blocks/ReadingLogCodeBlockView.svelte @@ -6,6 +6,7 @@ import { createReadingLog } from "@ui/stores/reading-log.svelte"; import { ALL_TIME } from "@ui/stores/date-filter.svelte"; import { onDestroy } from "svelte"; + import { getLinkpath } from "obsidian"; interface Props { plugin: BookTrackerPlugin; @@ -14,9 +15,7 @@ const { plugin }: Props = $props(); function bookUri(book: string) { - const v = encodeURIComponent(plugin.app.vault.getName()); - const f = encodeURIComponent(book + ".md"); - return `obsidian://open?vault=${v}&file=${f}`; + return getLinkpath(book + ".md"); } const store = createReadingLog(plugin.readingLog); diff --git a/src/ui/components/bookshelf/Book.svelte b/src/ui/components/bookshelf/Book.svelte index cb0cacf..71bbd9e 100644 --- a/src/ui/components/bookshelf/Book.svelte +++ b/src/ui/components/bookshelf/Book.svelte @@ -26,6 +26,7 @@ orientation?: "tilted" | "on-display"; height?: number; width?: number; + onClick?: () => void; } let { @@ -37,6 +38,7 @@ orientation, height, width, + onClick, }: BookProps = $props(); function widthCheck(input: number | undefined) { @@ -69,10 +71,11 @@ {design} {height} {width} + {onClick} /> {:else if orientation === "on-display"} - +
{/if} {:else if design === "split-bands"} - + {:else if design === "dual-top-bands"} - + {:else if design === "colored-spine"} - + {:else} - + {/if} diff --git a/src/ui/components/bookshelf/BookStackElement.svelte b/src/ui/components/bookshelf/BookStackElement.svelte index 3bdcad6..d8be958 100644 --- a/src/ui/components/bookshelf/BookStackElement.svelte +++ b/src/ui/components/bookshelf/BookStackElement.svelte @@ -11,6 +11,7 @@ subtitle?: string; color?: ColorName | string; design?: "default" | "split-bands" | "dual-top-bands" | "colored-spine"; + onClick?: () => void; } let { @@ -18,6 +19,7 @@ subtitle, color: colorRaw = "green", design, + onClick, }: Props = $props(); const color = $derived( @@ -29,19 +31,19 @@
  • {#if design === "split-bands"} - + {:else if design === "dual-top-bands"} - + {:else if design === "colored-spine"} - + {:else} - + {/if} diff --git a/src/ui/components/bookshelf/bookshelf.scss b/src/ui/components/bookshelf/bookshelf.scss index 97553cc..5e1813c 100644 --- a/src/ui/components/bookshelf/bookshelf.scss +++ b/src/ui/components/bookshelf/bookshelf.scss @@ -19,6 +19,7 @@ $bookEdge: 2px; display: inline-flex; flex-flow: column nowrap; list-style: none; + float: left; margin: 0; padding: 0; diff --git a/src/ui/components/bookshelf/designs/book/BookColoredSpine.svelte b/src/ui/components/bookshelf/designs/book/BookColoredSpine.svelte index c8c7b80..2d0ea28 100644 --- a/src/ui/components/bookshelf/designs/book/BookColoredSpine.svelte +++ b/src/ui/components/bookshelf/designs/book/BookColoredSpine.svelte @@ -7,9 +7,10 @@ children?: Snippet; color?: string; width?: number; + onClick?: () => void; } - let { children, color = "green", width = 40 }: Props = $props(); + let { children, color = "green", width = 40, onClick }: Props = $props(); const borderLeftColor = $derived(chroma(color).mix("white", 0.04)); const borderRightColor = $derived(chroma(color).mix("black", 0.04)); @@ -26,6 +27,10 @@ style:--book-width={width + "px"} style:width={width + "px"} style:color={textColor} + onclick={onClick} + onkeydown={(ev) => ev.key === "Enter" && onClick?.()} + role="link" + tabindex="0" > {@render children?.()}
  • diff --git a/src/ui/components/bookshelf/designs/book/BookDefault.svelte b/src/ui/components/bookshelf/designs/book/BookDefault.svelte index 83ccc83..1b2c41f 100644 --- a/src/ui/components/bookshelf/designs/book/BookDefault.svelte +++ b/src/ui/components/bookshelf/designs/book/BookDefault.svelte @@ -7,9 +7,10 @@ children?: Snippet; color?: string; width?: number; + onClick?: () => void; } - let { children, color = "green", width = 40 }: Props = $props(); + let { children, color = "green", width = 40, onClick }: Props = $props(); const backgroundColor = $derived(chroma(color)); const borderLeftColor = $derived(chroma(color).mix("white", 0.04)); @@ -25,6 +26,10 @@ style:--book-width={width + "px"} style:width={width + "px"} style:color={textColor} + onclick={onClick} + onkeydown={(ev) => ev.key === "Enter" && onClick?.()} + role="link" + tabindex="0" > {@render children?.()} diff --git a/src/ui/components/bookshelf/designs/book/BookDualTopBands.svelte b/src/ui/components/bookshelf/designs/book/BookDualTopBands.svelte index 5da2c7a..6b233e7 100644 --- a/src/ui/components/bookshelf/designs/book/BookDualTopBands.svelte +++ b/src/ui/components/bookshelf/designs/book/BookDualTopBands.svelte @@ -7,9 +7,10 @@ children?: Snippet; color?: string; width?: number; + onClick?: () => void; } - let { children, color = "green", width = 40 }: Props = $props(); + let { children, color = "green", width = 40, onClick }: Props = $props(); const backgroundColor = $derived(chroma(color)); const borderLeftColor = $derived(chroma(color).mix("white", 0.04)); @@ -27,6 +28,10 @@ style:--book-width={width + "px"} style:width={width + "px"} style:color={textColor} + onclick={onClick} + onkeydown={(ev) => ev.key === "Enter" && onClick?.()} + role="link" + tabindex="0" > {@render children?.()} diff --git a/src/ui/components/bookshelf/designs/book/BookOnDisplay.svelte b/src/ui/components/bookshelf/designs/book/BookOnDisplay.svelte index b0afa80..72e1760 100644 --- a/src/ui/components/bookshelf/designs/book/BookOnDisplay.svelte +++ b/src/ui/components/bookshelf/designs/book/BookOnDisplay.svelte @@ -4,14 +4,19 @@ interface Props { children?: Snippet; color?: string; + onClick?: () => void; } - let { children, color = "green" }: Props = $props(); + let { children, color = "green", onClick }: Props = $props();
    ev.key === "Enter" && onClick?.()} + role="link" + tabindex="0" > {@render children?.()}
    diff --git a/src/ui/components/bookshelf/designs/book/BookSplitBands.svelte b/src/ui/components/bookshelf/designs/book/BookSplitBands.svelte index 5da2c7a..6b233e7 100644 --- a/src/ui/components/bookshelf/designs/book/BookSplitBands.svelte +++ b/src/ui/components/bookshelf/designs/book/BookSplitBands.svelte @@ -7,9 +7,10 @@ children?: Snippet; color?: string; width?: number; + onClick?: () => void; } - let { children, color = "green", width = 40 }: Props = $props(); + let { children, color = "green", width = 40, onClick }: Props = $props(); const backgroundColor = $derived(chroma(color)); const borderLeftColor = $derived(chroma(color).mix("white", 0.04)); @@ -27,6 +28,10 @@ style:--book-width={width + "px"} style:width={width + "px"} style:color={textColor} + onclick={onClick} + onkeydown={(ev) => ev.key === "Enter" && onClick?.()} + role="link" + tabindex="0" > {@render children?.()} diff --git a/src/ui/components/bookshelf/designs/stack/BookStackColoredSpine.svelte b/src/ui/components/bookshelf/designs/stack/BookStackColoredSpine.svelte index 9e567fd..4f140a5 100644 --- a/src/ui/components/bookshelf/designs/stack/BookStackColoredSpine.svelte +++ b/src/ui/components/bookshelf/designs/stack/BookStackColoredSpine.svelte @@ -6,9 +6,10 @@ interface Props { children?: Snippet; color?: string; + onClick?: () => void; } - let { children, color = "green" }: Props = $props(); + let { children, color = "green", onClick }: Props = $props(); const borderLeftColor = $derived(chroma(color).mix("white", 0.04)); const borderRightColor = $derived(chroma(color).mix("black", 0.04)); @@ -22,6 +23,10 @@ style:--book-border-right-color={borderRightColor.css()} style:--book-background-color={backgroundColor.css()} style:color={textColor} + onclick={onClick} + onkeydown={(ev) => ev.key === "Enter" && onClick?.()} + role="link" + tabindex="0" > {@render children?.()} diff --git a/src/ui/components/bookshelf/designs/stack/BookStackDefault.svelte b/src/ui/components/bookshelf/designs/stack/BookStackDefault.svelte index ac92761..33b2212 100644 --- a/src/ui/components/bookshelf/designs/stack/BookStackDefault.svelte +++ b/src/ui/components/bookshelf/designs/stack/BookStackDefault.svelte @@ -6,9 +6,10 @@ interface Props { children?: Snippet; color?: string; + onClick?: () => void; } - let { children, color = "green" }: Props = $props(); + let { children, color = "green", onClick }: Props = $props(); const backgroundColor = $derived(chroma(color)); const borderLeftColor = $derived(chroma(color).mix("white", 0.04)); @@ -22,6 +23,10 @@ style:--book-border-left-color={borderLeftColor.css()} style:--book-border-right-color={borderRightColor.css()} style:color={textColor} + onclick={onClick} + onkeydown={(ev) => ev.key === "Enter" && onClick?.()} + role="link" + tabindex="0" > {@render children?.()} diff --git a/src/ui/components/bookshelf/designs/stack/BookStackDualTopBands.svelte b/src/ui/components/bookshelf/designs/stack/BookStackDualTopBands.svelte index 8b54671..4a3e81f 100644 --- a/src/ui/components/bookshelf/designs/stack/BookStackDualTopBands.svelte +++ b/src/ui/components/bookshelf/designs/stack/BookStackDualTopBands.svelte @@ -6,9 +6,10 @@ interface Props { children?: Snippet; color?: string; + onClick?: () => void; } - let { children, color = "green" }: Props = $props(); + let { children, color = "green", onClick }: Props = $props(); const backgroundColor = $derived(chroma(color)); const borderLeftColor = $derived(chroma(color).mix("white", 0.04)); @@ -24,6 +25,10 @@ style:--book-border-right-color={borderRightColor.css()} style:--book-band-color={bandColor.css()} style:color={textColor} + onclick={onClick} + onkeydown={(ev) => ev.key === "Enter" && onClick?.()} + role="link" + tabindex="0" > {@render children?.()} diff --git a/src/ui/components/bookshelf/designs/stack/BookStackSplitBands.svelte b/src/ui/components/bookshelf/designs/stack/BookStackSplitBands.svelte index 8835c4e..877d40e 100644 --- a/src/ui/components/bookshelf/designs/stack/BookStackSplitBands.svelte +++ b/src/ui/components/bookshelf/designs/stack/BookStackSplitBands.svelte @@ -6,9 +6,10 @@ interface Props { children?: Snippet; color?: string; + onClick?: () => void; } - let { children, color = "green" }: Props = $props(); + let { children, color = "green", onClick }: Props = $props(); const backgroundColor = $derived(chroma(color)); const borderLeftColor = $derived(chroma(color).mix("white", 0.04)); @@ -24,6 +25,10 @@ style:--book-border-right-color={borderRightColor.css()} style:--book-band-color={bandColor.css()} style:color={textColor} + onclick={onClick} + onkeydown={(ev) => ev.key === "Enter" && onClick?.()} + role="link" + tabindex="0" > {@render children?.()} diff --git a/src/ui/stores/metadata.svelte.ts b/src/ui/stores/metadata.svelte.ts index 97c5897..a125e13 100644 --- a/src/ui/stores/metadata.svelte.ts +++ b/src/ui/stores/metadata.svelte.ts @@ -1,10 +1,11 @@ -import { READ_STATE } from "@src/const"; +import { STATUS_READ } from "@src/const"; import type { CachedMetadata, TFile } from "obsidian"; import { getContext, setContext } from "svelte"; import { getSettingsContext } from "./settings.svelte"; import type BookTrackerPlugin from "@src/main"; import { createDateFilter, type DateFilterStore } from "./date-filter.svelte"; import type { ReadingState } from "@src/types"; +import type { BookTrackerPluginSettings } from "@ui/settings"; export type FileMetadata = { file: TFile; @@ -16,34 +17,45 @@ export type FileProperty = { value: any; }; -interface MetadataStore extends DateFilterStore { +export interface MetadataStore extends DateFilterStore { get metadata(): FileMetadata[]; destroy(): void; } -export function createMetadata( + +function getMetadata( plugin: BookTrackerPlugin, - state: ReadingState = READ_STATE -): MetadataStore { - const settingsStore = getSettingsContext(); + settings: BookTrackerPluginSettings, + state: ReadingState +): FileMetadata[] { + const metadata: FileMetadata[] = []; + for (const file of plugin.app.vault.getMarkdownFiles()) { + const frontmatter = + plugin.app.metadataCache.getFileCache(file)?.frontmatter ?? {}; - let metadata: FileMetadata[] = $state([]); - - $effect(() => { - const newMetadata: FileMetadata[] = []; - - for (const file of plugin.app.vault.getMarkdownFiles()) { - const frontmatter = - plugin.app.metadataCache.getFileCache(file)?.frontmatter ?? {}; - - if (frontmatter[settingsStore.settings.statusProperty] !== state) { - continue; - } - - newMetadata.push({ file, frontmatter }); + if (frontmatter[settings.statusProperty] !== state) { + continue; } - metadata = newMetadata; + metadata.push({ file, frontmatter }); + } + return metadata; +} + +export function createMetadata( + plugin: BookTrackerPlugin, + statusFilter: ReadingState = STATUS_READ +): MetadataStore { + const settingsStore = getSettingsContext(); + const initialMetadata = getMetadata( + plugin, + settingsStore.settings, + statusFilter + ); + let metadata: FileMetadata[] = $state(initialMetadata); + + $effect(() => { + metadata = getMetadata(plugin, settingsStore.settings, statusFilter); }); function onChanged(file: TFile, _data: string, cache: CachedMetadata) { @@ -57,7 +69,6 @@ export function createMetadata( return f; }); } - plugin.registerEvent(plugin.app.metadataCache.on("changed", onChanged)); function onDeleted(file: TFile) { @@ -69,13 +80,17 @@ export function createMetadata( () => metadata, (f) => { // @ts-expect-error Moment is provided by Obsidian - return moment(f.frontmatter[settings.endDateProperty]); + return moment( + f.frontmatter[settingsStore.settings.endDateProperty] + ); } ); return { get metadata() { - return dateFilter.filteredData; + return statusFilter === STATUS_READ + ? dateFilter.filteredData + : metadata; }, get filterYear() { return dateFilter.filterYear; diff --git a/src/ui/stores/settings.svelte.ts b/src/ui/stores/settings.svelte.ts index 102adfd..0ea3190 100644 --- a/src/ui/stores/settings.svelte.ts +++ b/src/ui/stores/settings.svelte.ts @@ -2,7 +2,7 @@ import type BookTrackerPlugin from "@src/main"; import { type BookTrackerSettings, DEFAULT_SETTINGS } from "../settings/types"; import { getContext, setContext } from "svelte"; -interface SettingsStore { +export interface SettingsStore { settings: BookTrackerSettings; load(): Promise; } diff --git a/src/utils/rand.ts b/src/utils/rand.ts index ad30349..c940448 100644 --- a/src/utils/rand.ts +++ b/src/utils/rand.ts @@ -1,3 +1,10 @@ +export function randomFloat(min?: number, max?: number) { + const minVal = min === undefined && max === undefined ? 0 : min ?? 0; + const maxVal = max === undefined ? (min === undefined ? 1 : min) : max; + + return Math.random() * (maxVal - minVal) + minVal; +} + export function randomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; }