Put actual books on bookshelf view

This commit is contained in:
Evan Fiordeliso 2025-07-05 10:42:25 -04:00
parent 658713fbec
commit b9f146f922
23 changed files with 265 additions and 208 deletions

View File

@ -10,7 +10,7 @@ import { EditorCheckCommand } from "./Command";
import type { BookTrackerPluginSettings } from "@ui/settings"; import type { BookTrackerPluginSettings } from "@ui/settings";
import { RatingModal } from "@ui/modals"; import { RatingModal } from "@ui/modals";
import type { ReadingLog } from "@utils/ReadingLog"; import type { ReadingLog } from "@utils/ReadingLog";
import { READ_STATE } from "@src/const"; import { STATUS_READ } from "@src/const";
import { mkdirRecursive, dirname } from "@utils/fs"; import { mkdirRecursive, dirname } from "@utils/fs";
export class LogReadingFinishedCommand extends EditorCheckCommand { export class LogReadingFinishedCommand extends EditorCheckCommand {
@ -70,7 +70,7 @@ export class LogReadingFinishedCommand extends EditorCheckCommand {
const endDate = moment().format("YYYY-MM-DD"); const endDate = moment().format("YYYY-MM-DD");
this.app.fileManager.processFrontMatter(file, (frontMatter) => { 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.endDateProperty] = endDate;
frontMatter[this.settings.ratingProperty] = ratings.rating; frontMatter[this.settings.ratingProperty] = ratings.rating;
if (this.settings.spiceProperty !== "") { if (this.settings.spiceProperty !== "") {

View File

@ -7,7 +7,7 @@ import {
} from "obsidian"; } from "obsidian";
import { EditorCheckCommand } from "./Command"; import { EditorCheckCommand } from "./Command";
import type { BookTrackerPluginSettings } from "@ui/settings"; 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 { export class LogReadingStartedCommand extends EditorCheckCommand {
constructor( constructor(
@ -38,7 +38,7 @@ export class LogReadingStartedCommand extends EditorCheckCommand {
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, (frontMatter) => {
frontMatter[this.settings.statusProperty] = IN_PROGRESS_STATE; frontMatter[this.settings.statusProperty] = STATUS_IN_PROGRESS;
frontMatter[this.settings.startDateProperty] = startDate; frontMatter[this.settings.startDateProperty] = startDate;
}); });

View File

@ -8,7 +8,7 @@ import {
import { EditorCheckCommand } from "./Command"; import { EditorCheckCommand } from "./Command";
import type { BookTrackerPluginSettings } from "@ui/settings"; import type { BookTrackerPluginSettings } from "@ui/settings";
import type { ReadingLog } from "@utils/ReadingLog"; 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 { export class ResetReadingStatusCommand extends EditorCheckCommand {
constructor( constructor(
@ -38,7 +38,7 @@ export class ResetReadingStatusCommand extends EditorCheckCommand {
const file = ctx.file!; const file = ctx.file!;
this.app.fileManager.processFrontMatter(file, (frontMatter) => { 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.startDateProperty] = "";
frontMatter[this.settings.endDateProperty] = ""; frontMatter[this.settings.endDateProperty] = "";
}); });

View File

@ -1,6 +1,6 @@
export const TO_BE_READ_STATE = "To Be Read"; export const STATUS_TO_BE_READ = "To Be Read";
export const IN_PROGRESS_STATE = "Currently Reading"; export const STATUS_IN_PROGRESS = "Currently Reading";
export const READ_STATE = "Read"; export const STATUS_READ = "Read";
export const CONTENT_TYPE_EXTENSIONS: Record<string, string> = { export const CONTENT_TYPE_EXTENSIONS: Record<string, string> = {
"image/jpeg": "jpg", "image/jpeg": "jpg",

View File

@ -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 { export interface Author {
name: string; name: string;
@ -24,8 +28,8 @@ export interface Book {
isbn13: string; isbn13: string;
} }
export type ToBeReadState = typeof TO_BE_READ_STATE; export type ToBeReadState = typeof STATUS_TO_BE_READ;
export type InProgressState = typeof IN_PROGRESS_STATE; export type InProgressState = typeof STATUS_IN_PROGRESS;
export type ReadState = typeof READ_STATE; export type ReadState = typeof STATUS_READ;
export type ReadingState = ToBeReadState | InProgressState | ReadState; export type ReadingState = ToBeReadState | InProgressState | ReadState;

View File

@ -2,6 +2,7 @@ import { registerCodeBlockRenderer } from ".";
import { SvelteCodeBlockRenderer } from "./SvelteCodeBlockRenderer"; import { SvelteCodeBlockRenderer } from "./SvelteCodeBlockRenderer";
import BookshelfCodeBlockView from "./BookshelfCodeBlockView.svelte"; import BookshelfCodeBlockView from "./BookshelfCodeBlockView.svelte";
import type BookTrackerPlugin from "@src/main"; import type BookTrackerPlugin from "@src/main";
import z from "zod/v4";
export function registerBookshelfCodeBlockProcessor( export function registerBookshelfCodeBlockProcessor(
plugin: BookTrackerPlugin 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< export class BookshelfCodeBlockRenderer extends SvelteCodeBlockRenderer<
typeof BookshelfCodeBlockView typeof BookshelfCodeBlockView
> { > {

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { READ_STATE } from "@src/const"; import { STATUS_TO_BE_READ } from "@src/const";
import type BookTrackerPlugin from "@src/main"; import type BookTrackerPlugin from "@src/main";
import type { ReadingState } from "@src/types"; import type { ReadingState } from "@src/types";
import Book from "@ui/components/bookshelf/Book.svelte"; import Book from "@ui/components/bookshelf/Book.svelte";
@ -9,26 +9,42 @@
import { import {
createMetadata, createMetadata,
setMetadataContext, setMetadataContext,
type FileMetadata,
} from "@ui/stores/metadata.svelte"; } from "@ui/stores/metadata.svelte";
import { import {
createSettings, createSettings,
setSettingsContext, setSettingsContext,
} from "@ui/stores/settings.svelte"; } from "@ui/stores/settings.svelte";
import { COLOR_NAMES } from "@utils/color"; import { COLOR_NAMES, type ColorName } from "@utils/color";
import { randomElement } from "@utils/rand"; import { randomElement, randomFloat, randomInt } from "@utils/rand";
import { onDestroy } from "svelte"; import { onDestroy } from "svelte";
import { BookshelfSettingsSchema } from "./BookshelfCodeBlock";
import { parseYaml, TFile } from "obsidian";
interface Props { interface Props {
plugin: BookTrackerPlugin; plugin: BookTrackerPlugin;
source: string; source: string;
} }
const { plugin }: Props = $props(); interface BookData {
title: string;
subtitle?: string;
author: string;
width: number;
color: ColorName;
design: (typeof designs)[number];
orientation: undefined | "tilted" | "on-display";
file: TFile;
}
const { plugin, source }: Props = $props();
const settings = $derived(BookshelfSettingsSchema.parse(parseYaml(source)));
const settingsStore = createSettings(plugin); const settingsStore = createSettings(plugin);
setSettingsContext(settingsStore); setSettingsContext(settingsStore);
let stateFilter: ReadingState = $state(READ_STATE); let stateFilter: ReadingState = $state(STATUS_TO_BE_READ);
const metadataStore = createMetadata(plugin, stateFilter); const metadataStore = createMetadata(plugin, stateFilter);
setMetadataContext(metadataStore); setMetadataContext(metadataStore);
@ -40,155 +56,113 @@
"split-bands", "split-bands",
] as const; ] as const;
function randomDesign() { const randomDesign = () => randomElement(designs);
return randomElement(designs); const randomColor = () => randomElement(COLOR_NAMES);
function randomOrientation() {
const n = randomFloat();
if (n < 0.55) {
return undefined;
} else if (n < 0.8) {
return "tilted";
} else {
return "on-display";
}
}
const randomStackChance = () => randomFloat() > 0.9;
function randomStackSize() {
const n = randomFloat();
if (n < 0.2) {
return 5;
} else if (n < 0.5) {
return 4;
} else if (n < 0.8) {
return 3;
} else if (n < 0.98) {
return 2;
} else {
return 1;
}
}
const calculateWidth = (pageCount: number) =>
Math.max(10, Math.min(1000, 200 + pageCount * 10));
function getBookData(metadata: FileMetadata): BookData {
return {
title: metadata.frontmatter[settings.titleProperty],
subtitle: settings.subtitleProperty
? metadata.frontmatter[settings.subtitleProperty]
: undefined,
author: metadata.frontmatter[settings.authorsProperty].join(", "),
width: calculateWidth(
metadata.frontmatter[settingsStore.settings.pageCountProperty],
),
color: randomColor(),
design: randomDesign(),
orientation: randomOrientation(),
file: metadata.file,
};
} }
function randomColor() { const books = $derived.by(() => {
return randomElement(COLOR_NAMES); let books: (BookData | BookData[])[] = [];
}
for (let i = 0; i < metadataStore.metadata.length; i++) {
if (randomStackChance()) {
const booksRemaining = metadataStore.metadata.length - i;
const stackSize = randomInt(
1,
Math.min(booksRemaining, randomStackSize()),
);
books.push(
metadataStore.metadata
.slice(i, i + stackSize)
.map(getBookData),
);
i += stackSize - 1;
} else {
books.push(getBookData(metadataStore.metadata[i]));
}
}
return books;
});
onDestroy(() => metadataStore.destroy()); onDestroy(() => metadataStore.destroy());
</script> </script>
<Bookshelf> <Bookshelf>
<Book {#each books as book}
title="Hello World" {#if Array.isArray(book)}
width={1120} <BookStack totalChildren={book.length}>
color={randomColor()} {#each book as bookData}
design={randomDesign()} <BookStackElement
/> title={bookData.title}
<Book subtitle={bookData.subtitle}
title="White Space" color={bookData.color}
width={700} design={bookData.design}
color={randomColor()} onClick={() =>
design={randomDesign()} plugin.app.workspace.openLinkText(
/> bookData.file.path,
<Book "",
title="The Art of Computer Programming Vol 1" true,
width={1156} )}
color={randomColor()} />
design={randomDesign()} {/each}
/> </BookStack>
<Book {:else}
title="Cascading Style Sheets" <Book
subtitle="Guide to Design" title={book.title}
width={560} subtitle={book.subtitle}
color={randomColor()} author={book.author}
design={randomDesign()} width={book.width}
orientation="tilted" color={book.color}
/> design={book.design}
<Book orientation={book.orientation}
title="HTML5" onClick={() =>
subtitle="Welcome to the Web" plugin.app.workspace.openLinkText(book.file.path, "", true)}
width={1350} />
color={randomColor()} {/if}
design={randomDesign()} {/each}
/>
<BookStack totalChildren={2}>
<BookStackElement
title="Coding for Dummies"
subtitle="JS tutorial"
color={randomColor()}
design={randomDesign()}
/>
<BookStackElement
title="Coding for Dummies"
subtitle="C# tutorial"
color={randomColor()}
design={randomDesign()}
/>
</BookStack>
<Book
title="CoffeeScript"
subtitle="The JS Alternative"
author="The Dev Guy"
color={randomColor()}
design={randomDesign()}
orientation="on-display"
/>
<Book
title="Cheat Sheet"
subtitle="Guide to Design"
width={870}
color={randomColor()}
design={randomDesign()}
/>
<Book
title="Psychology of Colors"
width={540}
color={randomColor()}
design={randomDesign()}
/>
<Book
title="TypeScript"
subtitle="Intro JS to type checking"
width={1130}
color={randomColor()}
design={randomDesign()}
/>
<Book
title="Testing"
width={10}
color={randomColor()}
design={randomDesign()}
/>
<Book
title="JavaScript"
subtitle="The Definitive Guide"
author="David Flanagan"
color={randomColor()}
design={randomDesign()}
orientation="on-display"
/>
<Book
title="Pragmatic Programmer"
color={randomColor()}
design={randomDesign()}
/>
<Book title="White Space" color={randomColor()} design={randomDesign()} />
<Book
title="W3 Schools"
subtitle="The best around"
color={randomColor()}
design={randomDesign()}
orientation="tilted"
/>
<Book
title="UI/UX"
subtitle="Guide to Mobile Development"
author="John Doe"
color={randomColor()}
design={randomDesign()}
orientation="on-display"
/>
<Book
title="Clean Code"
color={randomColor()}
design={randomDesign()}
orientation="tilted"
/>
<Book title="Docs for Devs" color={randomColor()} design={randomDesign()} />
<BookStack totalChildren={4}>
<BookStackElement
title="The Art of Computer Programming Vol 1"
color={randomColor()}
design={randomDesign()}
/>
<BookStackElement
title="The Art of Computer Programming Vol 2"
color={randomColor()}
design={randomDesign()}
/>
<BookStackElement
title="The Art of Computer Programming Vol 3"
color={randomColor()}
design={randomDesign()}
/>
<BookStackElement
title="The Art of Computer Programming Vol 4a"
color={randomColor()}
design={randomDesign()}
/>
</BookStack>
</Bookshelf> </Bookshelf>

View File

@ -6,6 +6,7 @@
import { createReadingLog } from "@ui/stores/reading-log.svelte"; import { createReadingLog } from "@ui/stores/reading-log.svelte";
import { ALL_TIME } from "@ui/stores/date-filter.svelte"; import { ALL_TIME } from "@ui/stores/date-filter.svelte";
import { onDestroy } from "svelte"; import { onDestroy } from "svelte";
import { getLinkpath } from "obsidian";
interface Props { interface Props {
plugin: BookTrackerPlugin; plugin: BookTrackerPlugin;
@ -14,9 +15,7 @@
const { plugin }: Props = $props(); const { plugin }: Props = $props();
function bookUri(book: string) { function bookUri(book: string) {
const v = encodeURIComponent(plugin.app.vault.getName()); return getLinkpath(book + ".md");
const f = encodeURIComponent(book + ".md");
return `obsidian://open?vault=${v}&file=${f}`;
} }
const store = createReadingLog(plugin.readingLog); const store = createReadingLog(plugin.readingLog);

View File

@ -26,6 +26,7 @@
orientation?: "tilted" | "on-display"; orientation?: "tilted" | "on-display";
height?: number; height?: number;
width?: number; width?: number;
onClick?: () => void;
} }
let { let {
@ -37,6 +38,7 @@
orientation, orientation,
height, height,
width, width,
onClick,
}: BookProps = $props(); }: BookProps = $props();
function widthCheck(input: number | undefined) { function widthCheck(input: number | undefined) {
@ -69,10 +71,11 @@
{design} {design}
{height} {height}
{width} {width}
{onClick}
/> />
</BookTiltedDefault> </BookTiltedDefault>
{:else if orientation === "on-display"} {:else if orientation === "on-display"}
<BookOnDisplay color={color.hex}> <BookOnDisplay color={color.hex} {onClick}>
<div <div
class="book-display-crease" class="book-display-crease"
style:--book-color={color.hex} style:--book-color={color.hex}
@ -84,19 +87,19 @@
</BookOnDisplay> </BookOnDisplay>
{/if} {/if}
{:else if design === "split-bands"} {:else if design === "split-bands"}
<BookSplitBands color={color.hex} width={verifiedWidth}> <BookSplitBands color={color.hex} width={verifiedWidth} {onClick}>
<BookText {title} {subtitle} /> <BookText {title} {subtitle} />
</BookSplitBands> </BookSplitBands>
{:else if design === "dual-top-bands"} {:else if design === "dual-top-bands"}
<BookDualTopBands color={color.hex} width={verifiedWidth}> <BookDualTopBands color={color.hex} width={verifiedWidth} {onClick}>
<BookText {title} {subtitle} /> <BookText {title} {subtitle} />
</BookDualTopBands> </BookDualTopBands>
{:else if design === "colored-spine"} {:else if design === "colored-spine"}
<BookColoredSpine color={color.hex} width={verifiedWidth}> <BookColoredSpine color={color.hex} width={verifiedWidth} {onClick}>
<BookText {title} {subtitle} /> <BookText {title} {subtitle} />
</BookColoredSpine> </BookColoredSpine>
{:else} {:else}
<BookDefault color={color.hex} width={verifiedWidth}> <BookDefault color={color.hex} width={verifiedWidth} {onClick}>
<BookText {title} {subtitle} /> <BookText {title} {subtitle} />
</BookDefault> </BookDefault>
{/if} {/if}

View File

@ -11,6 +11,7 @@
subtitle?: string; subtitle?: string;
color?: ColorName | string; color?: ColorName | string;
design?: "default" | "split-bands" | "dual-top-bands" | "colored-spine"; design?: "default" | "split-bands" | "dual-top-bands" | "colored-spine";
onClick?: () => void;
} }
let { let {
@ -18,6 +19,7 @@
subtitle, subtitle,
color: colorRaw = "green", color: colorRaw = "green",
design, design,
onClick,
}: Props = $props(); }: Props = $props();
const color = $derived( const color = $derived(
@ -29,19 +31,19 @@
<li class="bookshelf__bookstack-elem"> <li class="bookshelf__bookstack-elem">
{#if design === "split-bands"} {#if design === "split-bands"}
<BookStackSplitBands color={color.hex}> <BookStackSplitBands color={color.hex} {onClick}>
<BookText {title} {subtitle} /> <BookText {title} {subtitle} />
</BookStackSplitBands> </BookStackSplitBands>
{:else if design === "dual-top-bands"} {:else if design === "dual-top-bands"}
<BookStackDualTopBands color={color.hex}> <BookStackDualTopBands color={color.hex} {onClick}>
<BookText {title} {subtitle} /> <BookText {title} {subtitle} />
</BookStackDualTopBands> </BookStackDualTopBands>
{:else if design === "colored-spine"} {:else if design === "colored-spine"}
<BookStackColoredSpine color={color.hex}> <BookStackColoredSpine color={color.hex} {onClick}>
<BookText {title} {subtitle} /> <BookText {title} {subtitle} />
</BookStackColoredSpine> </BookStackColoredSpine>
{:else} {:else}
<BookStackDefault color={color.hex}> <BookStackDefault color={color.hex} {onClick}>
<BookText {title} {subtitle} /> <BookText {title} {subtitle} />
</BookStackDefault> </BookStackDefault>
{/if} {/if}

View File

@ -19,6 +19,7 @@ $bookEdge: 2px;
display: inline-flex; display: inline-flex;
flex-flow: column nowrap; flex-flow: column nowrap;
list-style: none; list-style: none;
float: left;
margin: 0; margin: 0;
padding: 0; padding: 0;

View File

@ -7,9 +7,10 @@
children?: Snippet; children?: Snippet;
color?: string; color?: string;
width?: number; 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 borderLeftColor = $derived(chroma(color).mix("white", 0.04));
const borderRightColor = $derived(chroma(color).mix("black", 0.04)); const borderRightColor = $derived(chroma(color).mix("black", 0.04));
@ -26,6 +27,10 @@
style:--book-width={width + "px"} style:--book-width={width + "px"}
style:width={width + "px"} style:width={width + "px"}
style:color={textColor} style:color={textColor}
onclick={onClick}
onkeydown={(ev) => ev.key === "Enter" && onClick?.()}
role="link"
tabindex="0"
> >
{@render children?.()} {@render children?.()}
</div> </div>

View File

@ -7,9 +7,10 @@
children?: Snippet; children?: Snippet;
color?: string; color?: string;
width?: number; 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 backgroundColor = $derived(chroma(color));
const borderLeftColor = $derived(chroma(color).mix("white", 0.04)); const borderLeftColor = $derived(chroma(color).mix("white", 0.04));
@ -25,6 +26,10 @@
style:--book-width={width + "px"} style:--book-width={width + "px"}
style:width={width + "px"} style:width={width + "px"}
style:color={textColor} style:color={textColor}
onclick={onClick}
onkeydown={(ev) => ev.key === "Enter" && onClick?.()}
role="link"
tabindex="0"
> >
{@render children?.()} {@render children?.()}
</div> </div>

View File

@ -7,9 +7,10 @@
children?: Snippet; children?: Snippet;
color?: string; color?: string;
width?: number; 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 backgroundColor = $derived(chroma(color));
const borderLeftColor = $derived(chroma(color).mix("white", 0.04)); const borderLeftColor = $derived(chroma(color).mix("white", 0.04));
@ -27,6 +28,10 @@
style:--book-width={width + "px"} style:--book-width={width + "px"}
style:width={width + "px"} style:width={width + "px"}
style:color={textColor} style:color={textColor}
onclick={onClick}
onkeydown={(ev) => ev.key === "Enter" && onClick?.()}
role="link"
tabindex="0"
> >
{@render children?.()} {@render children?.()}
</div> </div>

View File

@ -4,14 +4,19 @@
interface Props { interface Props {
children?: Snippet; children?: Snippet;
color?: string; color?: string;
onClick?: () => void;
} }
let { children, color = "green" }: Props = $props(); let { children, color = "green", onClick }: Props = $props();
</script> </script>
<div <div
class="bookshelf__book-wrapper bookshelf__book-onDisplay" class="bookshelf__book-wrapper bookshelf__book-onDisplay"
style:--book-color={color} style:--book-color={color}
onclick={onClick}
onkeydown={(ev) => ev.key === "Enter" && onClick?.()}
role="link"
tabindex="0"
> >
{@render children?.()} {@render children?.()}
</div> </div>

View File

@ -7,9 +7,10 @@
children?: Snippet; children?: Snippet;
color?: string; color?: string;
width?: number; 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 backgroundColor = $derived(chroma(color));
const borderLeftColor = $derived(chroma(color).mix("white", 0.04)); const borderLeftColor = $derived(chroma(color).mix("white", 0.04));
@ -27,6 +28,10 @@
style:--book-width={width + "px"} style:--book-width={width + "px"}
style:width={width + "px"} style:width={width + "px"}
style:color={textColor} style:color={textColor}
onclick={onClick}
onkeydown={(ev) => ev.key === "Enter" && onClick?.()}
role="link"
tabindex="0"
> >
{@render children?.()} {@render children?.()}
</div> </div>

View File

@ -6,9 +6,10 @@
interface Props { interface Props {
children?: Snippet; children?: Snippet;
color?: string; 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 borderLeftColor = $derived(chroma(color).mix("white", 0.04));
const borderRightColor = $derived(chroma(color).mix("black", 0.04)); const borderRightColor = $derived(chroma(color).mix("black", 0.04));
@ -22,6 +23,10 @@
style:--book-border-right-color={borderRightColor.css()} style:--book-border-right-color={borderRightColor.css()}
style:--book-background-color={backgroundColor.css()} style:--book-background-color={backgroundColor.css()}
style:color={textColor} style:color={textColor}
onclick={onClick}
onkeydown={(ev) => ev.key === "Enter" && onClick?.()}
role="link"
tabindex="0"
> >
{@render children?.()} {@render children?.()}
</div> </div>

View File

@ -6,9 +6,10 @@
interface Props { interface Props {
children?: Snippet; children?: Snippet;
color?: string; color?: string;
onClick?: () => void;
} }
let { children, color = "green" }: Props = $props(); let { children, color = "green", onClick }: Props = $props();
const backgroundColor = $derived(chroma(color)); const backgroundColor = $derived(chroma(color));
const borderLeftColor = $derived(chroma(color).mix("white", 0.04)); const borderLeftColor = $derived(chroma(color).mix("white", 0.04));
@ -22,6 +23,10 @@
style:--book-border-left-color={borderLeftColor.css()} style:--book-border-left-color={borderLeftColor.css()}
style:--book-border-right-color={borderRightColor.css()} style:--book-border-right-color={borderRightColor.css()}
style:color={textColor} style:color={textColor}
onclick={onClick}
onkeydown={(ev) => ev.key === "Enter" && onClick?.()}
role="link"
tabindex="0"
> >
{@render children?.()} {@render children?.()}
</div> </div>

View File

@ -6,9 +6,10 @@
interface Props { interface Props {
children?: Snippet; children?: Snippet;
color?: string; color?: string;
onClick?: () => void;
} }
let { children, color = "green" }: Props = $props(); let { children, color = "green", onClick }: Props = $props();
const backgroundColor = $derived(chroma(color)); const backgroundColor = $derived(chroma(color));
const borderLeftColor = $derived(chroma(color).mix("white", 0.04)); const borderLeftColor = $derived(chroma(color).mix("white", 0.04));
@ -24,6 +25,10 @@
style:--book-border-right-color={borderRightColor.css()} style:--book-border-right-color={borderRightColor.css()}
style:--book-band-color={bandColor.css()} style:--book-band-color={bandColor.css()}
style:color={textColor} style:color={textColor}
onclick={onClick}
onkeydown={(ev) => ev.key === "Enter" && onClick?.()}
role="link"
tabindex="0"
> >
{@render children?.()} {@render children?.()}
</div> </div>

View File

@ -6,9 +6,10 @@
interface Props { interface Props {
children?: Snippet; children?: Snippet;
color?: string; color?: string;
onClick?: () => void;
} }
let { children, color = "green" }: Props = $props(); let { children, color = "green", onClick }: Props = $props();
const backgroundColor = $derived(chroma(color)); const backgroundColor = $derived(chroma(color));
const borderLeftColor = $derived(chroma(color).mix("white", 0.04)); const borderLeftColor = $derived(chroma(color).mix("white", 0.04));
@ -24,6 +25,10 @@
style:--book-border-right-color={borderRightColor.css()} style:--book-border-right-color={borderRightColor.css()}
style:--book-band-color={bandColor.css()} style:--book-band-color={bandColor.css()}
style:color={textColor} style:color={textColor}
onclick={onClick}
onkeydown={(ev) => ev.key === "Enter" && onClick?.()}
role="link"
tabindex="0"
> >
{@render children?.()} {@render children?.()}
</div> </div>

View File

@ -1,10 +1,11 @@
import { READ_STATE } 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 { getSettingsContext } from "./settings.svelte"; import { getSettingsContext } from "./settings.svelte";
import type BookTrackerPlugin from "@src/main"; import type BookTrackerPlugin from "@src/main";
import { createDateFilter, type DateFilterStore } from "./date-filter.svelte"; import { createDateFilter, type DateFilterStore } from "./date-filter.svelte";
import type { ReadingState } from "@src/types"; import type { ReadingState } from "@src/types";
import type { BookTrackerPluginSettings } from "@ui/settings";
export type FileMetadata = { export type FileMetadata = {
file: TFile; file: TFile;
@ -16,34 +17,45 @@ export type FileProperty = {
value: any; value: any;
}; };
interface MetadataStore extends DateFilterStore { export interface MetadataStore extends DateFilterStore {
get metadata(): FileMetadata[]; get metadata(): FileMetadata[];
destroy(): void; destroy(): void;
} }
export function createMetadata(
function getMetadata(
plugin: BookTrackerPlugin, plugin: BookTrackerPlugin,
state: ReadingState = READ_STATE settings: BookTrackerPluginSettings,
): MetadataStore { state: ReadingState
const settingsStore = getSettingsContext(); ): FileMetadata[] {
const metadata: FileMetadata[] = [];
for (const file of plugin.app.vault.getMarkdownFiles()) {
const frontmatter =
plugin.app.metadataCache.getFileCache(file)?.frontmatter ?? {};
let metadata: FileMetadata[] = $state([]); if (frontmatter[settings.statusProperty] !== state) {
continue;
$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 });
} }
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) { function onChanged(file: TFile, _data: string, cache: CachedMetadata) {
@ -57,7 +69,6 @@ export function createMetadata(
return f; return f;
}); });
} }
plugin.registerEvent(plugin.app.metadataCache.on("changed", onChanged)); plugin.registerEvent(plugin.app.metadataCache.on("changed", onChanged));
function onDeleted(file: TFile) { function onDeleted(file: TFile) {
@ -69,13 +80,17 @@ export function createMetadata(
() => metadata, () => metadata,
(f) => { (f) => {
// @ts-expect-error Moment is provided by Obsidian // @ts-expect-error Moment is provided by Obsidian
return moment(f.frontmatter[settings.endDateProperty]); return moment(
f.frontmatter[settingsStore.settings.endDateProperty]
);
} }
); );
return { return {
get metadata() { get metadata() {
return dateFilter.filteredData; return statusFilter === STATUS_READ
? dateFilter.filteredData
: metadata;
}, },
get filterYear() { get filterYear() {
return dateFilter.filterYear; return dateFilter.filterYear;

View File

@ -2,7 +2,7 @@ import type BookTrackerPlugin from "@src/main";
import { type BookTrackerSettings, DEFAULT_SETTINGS } from "../settings/types"; import { type BookTrackerSettings, DEFAULT_SETTINGS } from "../settings/types";
import { getContext, setContext } from "svelte"; import { getContext, setContext } from "svelte";
interface SettingsStore { export interface SettingsStore {
settings: BookTrackerSettings; settings: BookTrackerSettings;
load(): Promise<void>; load(): Promise<void>;
} }

View File

@ -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) { export function randomInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min; return Math.floor(Math.random() * (max - min + 1)) + min;
} }