generated from tpl/obsidian-sample-plugin
			Rename bookshelf code block to shelf and add table view
This commit is contained in:
		
							parent
							
								
									94fe4d5f1c
								
							
						
					
					
						commit
						db732fd8a6
					
				| 
						 | 
				
			
			@ -22,7 +22,7 @@ import { BackupReadingLogCommand } from "@commands/CreateReadingLogBackupCommand
 | 
			
		|||
import { RestoreReadingLogBackupCommand } from "@commands/RestoreReadingLogBackupCommand";
 | 
			
		||||
import { Goodreads } from "@data-sources/Goodreads";
 | 
			
		||||
import { CreateBookFromGoodreadsUrlCommand } from "@commands/CreateBookFromGoodreadsUrlCommand";
 | 
			
		||||
import { registerBookshelfCodeBlockProcessor } from "@ui/code-blocks/BookshelfCodeBlock";
 | 
			
		||||
import { registerShelfCodeBlockProcessor } from "@ui/code-blocks/ShelfCodeBlock";
 | 
			
		||||
 | 
			
		||||
export default class BookTrackerPlugin extends Plugin {
 | 
			
		||||
	public settings: BookTrackerPluginSettings;
 | 
			
		||||
| 
						 | 
				
			
			@ -86,7 +86,7 @@ export default class BookTrackerPlugin extends Plugin {
 | 
			
		|||
 | 
			
		||||
		registerReadingLogCodeBlockProcessor(this);
 | 
			
		||||
		registerReadingStatsCodeBlockProcessor(this);
 | 
			
		||||
		registerBookshelfCodeBlockProcessor(this);
 | 
			
		||||
		registerShelfCodeBlockProcessor(this);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onunload() {}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,35 +0,0 @@
 | 
			
		|||
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
 | 
			
		||||
): void {
 | 
			
		||||
	registerCodeBlockRenderer(
 | 
			
		||||
		plugin,
 | 
			
		||||
		"bookshelf",
 | 
			
		||||
		(source, el) => new BookshelfCodeBlockRenderer(plugin, source, el)
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const BookshelfSettingsSchema = z.object({
 | 
			
		||||
	titleProperty: z.string(),
 | 
			
		||||
	subtitleProperty: z.optional(z.string()),
 | 
			
		||||
	authorsProperty: z.string(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export class BookshelfCodeBlockRenderer extends SvelteCodeBlockRenderer<
 | 
			
		||||
	typeof BookshelfCodeBlockView
 | 
			
		||||
> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		plugin: BookTrackerPlugin,
 | 
			
		||||
		source: string,
 | 
			
		||||
		contentEl: HTMLElement
 | 
			
		||||
	) {
 | 
			
		||||
		super(contentEl, BookshelfCodeBlockView, { props: { plugin, source } });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onunload() {}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,166 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { STATUS_TO_BE_READ } from "@src/const";
 | 
			
		||||
	import type BookTrackerPlugin from "@src/main";
 | 
			
		||||
	import type { ReadingState } from "@src/types";
 | 
			
		||||
	import Book from "@ui/components/bookshelf/Book.svelte";
 | 
			
		||||
	import Bookshelf from "@ui/components/bookshelf/Bookshelf.svelte";
 | 
			
		||||
	import BookStack from "@ui/components/bookshelf/BookStack.svelte";
 | 
			
		||||
	import BookStackElement from "@ui/components/bookshelf/BookStackElement.svelte";
 | 
			
		||||
	import {
 | 
			
		||||
		createMetadata,
 | 
			
		||||
		setMetadataContext,
 | 
			
		||||
		type FileMetadata,
 | 
			
		||||
	} from "@ui/stores/metadata.svelte";
 | 
			
		||||
	import {
 | 
			
		||||
		createSettings,
 | 
			
		||||
		setSettingsContext,
 | 
			
		||||
	} from "@ui/stores/settings.svelte";
 | 
			
		||||
	import { COLOR_NAMES, type ColorName } from "@utils/color";
 | 
			
		||||
	import { randomElement, randomFloat, randomInt } from "@utils/rand";
 | 
			
		||||
	import { onDestroy } from "svelte";
 | 
			
		||||
	import { BookshelfSettingsSchema } from "./BookshelfCodeBlock";
 | 
			
		||||
	import { parseYaml, TFile } from "obsidian";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		plugin: BookTrackerPlugin;
 | 
			
		||||
		source: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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);
 | 
			
		||||
	setSettingsContext(settingsStore);
 | 
			
		||||
 | 
			
		||||
	let stateFilter: ReadingState = $state(STATUS_TO_BE_READ);
 | 
			
		||||
 | 
			
		||||
	const metadataStore = createMetadata(plugin, stateFilter);
 | 
			
		||||
	setMetadataContext(metadataStore);
 | 
			
		||||
 | 
			
		||||
	const designs = [
 | 
			
		||||
		"default",
 | 
			
		||||
		"colored-spine",
 | 
			
		||||
		"dual-top-bands",
 | 
			
		||||
		"split-bands",
 | 
			
		||||
	] as const;
 | 
			
		||||
 | 
			
		||||
	const randomDesign = () => 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;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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: metadata.frontmatter[
 | 
			
		||||
				settingsStore.settings.pageCountProperty
 | 
			
		||||
			],
 | 
			
		||||
			color: randomColor(),
 | 
			
		||||
			design: randomDesign(),
 | 
			
		||||
			orientation: randomOrientation(),
 | 
			
		||||
			file: metadata.file,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const books = $derived.by(() => {
 | 
			
		||||
		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());
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Bookshelf>
 | 
			
		||||
	{#each books as book}
 | 
			
		||||
		{#if Array.isArray(book)}
 | 
			
		||||
			<BookStack totalChildren={book.length}>
 | 
			
		||||
				{#each book as bookData}
 | 
			
		||||
					<BookStackElement
 | 
			
		||||
						title={bookData.title}
 | 
			
		||||
						subtitle={bookData.subtitle}
 | 
			
		||||
						color={bookData.color}
 | 
			
		||||
						design={bookData.design}
 | 
			
		||||
						onClick={() =>
 | 
			
		||||
							plugin.app.workspace.openLinkText(
 | 
			
		||||
								bookData.file.path,
 | 
			
		||||
								"",
 | 
			
		||||
								true,
 | 
			
		||||
							)}
 | 
			
		||||
					/>
 | 
			
		||||
				{/each}
 | 
			
		||||
			</BookStack>
 | 
			
		||||
		{:else}
 | 
			
		||||
			<Book
 | 
			
		||||
				title={book.title}
 | 
			
		||||
				subtitle={book.subtitle}
 | 
			
		||||
				author={book.author}
 | 
			
		||||
				width={book.width}
 | 
			
		||||
				color={book.color}
 | 
			
		||||
				design={book.design}
 | 
			
		||||
				orientation={book.orientation}
 | 
			
		||||
				onClick={() =>
 | 
			
		||||
					plugin.app.workspace.openLinkText(book.file.path, "", true)}
 | 
			
		||||
			/>
 | 
			
		||||
		{/if}
 | 
			
		||||
	{/each}
 | 
			
		||||
</Bookshelf>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,46 @@
 | 
			
		|||
import { registerCodeBlockRenderer } from ".";
 | 
			
		||||
import { SvelteCodeBlockRenderer } from "./SvelteCodeBlockRenderer";
 | 
			
		||||
import ShelfCodeBockView from "./ShelfCodeBlockView.svelte";
 | 
			
		||||
import type BookTrackerPlugin from "@src/main";
 | 
			
		||||
import z from "zod/v4";
 | 
			
		||||
import { STATUS_IN_PROGRESS, STATUS_READ, STATUS_TO_BE_READ } from "@src/const";
 | 
			
		||||
 | 
			
		||||
export function registerShelfCodeBlockProcessor(
 | 
			
		||||
	plugin: BookTrackerPlugin
 | 
			
		||||
): void {
 | 
			
		||||
	registerCodeBlockRenderer(
 | 
			
		||||
		plugin,
 | 
			
		||||
		"shelf",
 | 
			
		||||
		(source, el) => new ShelfCodeBlockRenderer(plugin, source, el)
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SHELF_VIEWS = ["table", "bookshelf"] as const;
 | 
			
		||||
export type ShelfView = (typeof SHELF_VIEWS)[number];
 | 
			
		||||
 | 
			
		||||
export const ShelfSettingsSchema = z.object({
 | 
			
		||||
	statusFilter: z
 | 
			
		||||
		.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(),
 | 
			
		||||
	seriesTitleProperty: z.optional(z.string()),
 | 
			
		||||
	seriesNumberProperty: z.optional(z.string()),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export class ShelfCodeBlockRenderer extends SvelteCodeBlockRenderer<
 | 
			
		||||
	typeof ShelfCodeBockView
 | 
			
		||||
> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		plugin: BookTrackerPlugin,
 | 
			
		||||
		source: string,
 | 
			
		||||
		contentEl: HTMLElement
 | 
			
		||||
	) {
 | 
			
		||||
		super(contentEl, ShelfCodeBockView, { props: { plugin, source } });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onunload() {}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,289 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { STATUS_IN_PROGRESS, STATUS_READ } from "@src/const";
 | 
			
		||||
	import type BookTrackerPlugin from "@src/main";
 | 
			
		||||
	import Book from "@ui/components/bookshelf/Book.svelte";
 | 
			
		||||
	import Bookshelf from "@ui/components/bookshelf/Bookshelf.svelte";
 | 
			
		||||
	import BookStack from "@ui/components/bookshelf/BookStack.svelte";
 | 
			
		||||
	import BookStackElement from "@ui/components/bookshelf/BookStackElement.svelte";
 | 
			
		||||
	import {
 | 
			
		||||
		createMetadata,
 | 
			
		||||
		setMetadataContext,
 | 
			
		||||
		type FileMetadata,
 | 
			
		||||
	} from "@ui/stores/metadata.svelte";
 | 
			
		||||
	import {
 | 
			
		||||
		createSettings,
 | 
			
		||||
		setSettingsContext,
 | 
			
		||||
	} from "@ui/stores/settings.svelte";
 | 
			
		||||
	import { COLOR_NAMES, type ColorName } from "@utils/color";
 | 
			
		||||
	import { randomElement, randomFloat, randomInt } from "@utils/rand";
 | 
			
		||||
	import { onDestroy } from "svelte";
 | 
			
		||||
	import { ShelfSettingsSchema } from "./ShelfCodeBlock";
 | 
			
		||||
	import { parseYaml, TFile } from "obsidian";
 | 
			
		||||
	import DateFilter from "@ui/components/DateFilter.svelte";
 | 
			
		||||
	import Rating from "@ui/components/Rating.svelte";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		plugin: BookTrackerPlugin;
 | 
			
		||||
		source: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 = ShelfSettingsSchema.parse(parseYaml(source));
 | 
			
		||||
 | 
			
		||||
	const settingsStore = createSettings(plugin);
 | 
			
		||||
	setSettingsContext(settingsStore);
 | 
			
		||||
 | 
			
		||||
	const metadataStore = createMetadata(plugin, settings.statusFilter);
 | 
			
		||||
	setMetadataContext(metadataStore);
 | 
			
		||||
 | 
			
		||||
	const designs = [
 | 
			
		||||
		"default",
 | 
			
		||||
		"colored-spine",
 | 
			
		||||
		"dual-top-bands",
 | 
			
		||||
		"split-bands",
 | 
			
		||||
	] as const;
 | 
			
		||||
 | 
			
		||||
	const randomDesign = () => 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;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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: metadata.frontmatter[
 | 
			
		||||
				settingsStore.settings.pageCountProperty
 | 
			
		||||
			],
 | 
			
		||||
			color: randomColor(),
 | 
			
		||||
			design: randomDesign(),
 | 
			
		||||
			orientation: randomOrientation(),
 | 
			
		||||
			file: metadata.file,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let view = $state(settings.defaultView);
 | 
			
		||||
	const books = $derived.by(() => {
 | 
			
		||||
		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());
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
	class="shelf-code-block"
 | 
			
		||||
	class:table-view={view === "table"}
 | 
			
		||||
	class:bookshelf-view={view === "bookshelf"}
 | 
			
		||||
>
 | 
			
		||||
	<div class="controls">
 | 
			
		||||
		<select bind:value={view}>
 | 
			
		||||
			<option value="table">Table</option>
 | 
			
		||||
			<option value="bookshelf">Bookshelf</option>
 | 
			
		||||
		</select>
 | 
			
		||||
		{#if settings.statusFilter === STATUS_READ}
 | 
			
		||||
			<DateFilter store={metadataStore} />
 | 
			
		||||
		{/if}
 | 
			
		||||
	</div>
 | 
			
		||||
	{#if view === "bookshelf"}
 | 
			
		||||
		<Bookshelf>
 | 
			
		||||
			{#each books as book}
 | 
			
		||||
				{#if Array.isArray(book)}
 | 
			
		||||
					<BookStack totalChildren={book.length}>
 | 
			
		||||
						{#each book as bookData}
 | 
			
		||||
							<BookStackElement
 | 
			
		||||
								title={bookData.title}
 | 
			
		||||
								subtitle={bookData.subtitle}
 | 
			
		||||
								color={bookData.color}
 | 
			
		||||
								design={bookData.design}
 | 
			
		||||
								onClick={() =>
 | 
			
		||||
									plugin.app.workspace.openLinkText(
 | 
			
		||||
										bookData.file.path,
 | 
			
		||||
										"",
 | 
			
		||||
										true,
 | 
			
		||||
									)}
 | 
			
		||||
							/>
 | 
			
		||||
						{/each}
 | 
			
		||||
					</BookStack>
 | 
			
		||||
				{:else}
 | 
			
		||||
					<Book
 | 
			
		||||
						title={book.title}
 | 
			
		||||
						subtitle={book.subtitle}
 | 
			
		||||
						author={book.author}
 | 
			
		||||
						width={book.width}
 | 
			
		||||
						color={book.color}
 | 
			
		||||
						design={book.design}
 | 
			
		||||
						orientation={book.orientation}
 | 
			
		||||
						onClick={() =>
 | 
			
		||||
							plugin.app.workspace.openLinkText(
 | 
			
		||||
								book.file.path,
 | 
			
		||||
								"",
 | 
			
		||||
								true,
 | 
			
		||||
							)}
 | 
			
		||||
					/>
 | 
			
		||||
				{/if}
 | 
			
		||||
			{/each}
 | 
			
		||||
		</Bookshelf>
 | 
			
		||||
	{:else if view === "table"}
 | 
			
		||||
		<table>
 | 
			
		||||
			<thead>
 | 
			
		||||
				<tr>
 | 
			
		||||
					<th>Cover</th>
 | 
			
		||||
					<th>Title</th>
 | 
			
		||||
					<th>Authors</th>
 | 
			
		||||
					{#if settings.seriesTitleProperty}
 | 
			
		||||
						<th>Series</th>
 | 
			
		||||
					{/if}
 | 
			
		||||
					{#if settings.seriesNumberProperty}
 | 
			
		||||
						<th>#</th>
 | 
			
		||||
					{/if}
 | 
			
		||||
					{#if settings.statusFilter === STATUS_IN_PROGRESS || settings.statusFilter === STATUS_READ}
 | 
			
		||||
						<th>Start Date</th>
 | 
			
		||||
					{/if}
 | 
			
		||||
					{#if settings.statusFilter === STATUS_READ}
 | 
			
		||||
						<th>End Date</th>
 | 
			
		||||
						<th>Rating</th>
 | 
			
		||||
					{/if}
 | 
			
		||||
				</tr>
 | 
			
		||||
			</thead>
 | 
			
		||||
			<tbody>
 | 
			
		||||
				{#each metadataStore.metadata as book}
 | 
			
		||||
					<tr>
 | 
			
		||||
						<td>
 | 
			
		||||
							<img
 | 
			
		||||
								src={plugin.app.vault.getResourcePath(
 | 
			
		||||
									plugin.app.vault.getFileByPath(
 | 
			
		||||
										book.frontmatter[
 | 
			
		||||
											settings.coverProperty
 | 
			
		||||
										],
 | 
			
		||||
									)!,
 | 
			
		||||
								)}
 | 
			
		||||
								alt={book.frontmatter[settings.titleProperty]}
 | 
			
		||||
								width="50"
 | 
			
		||||
							/>
 | 
			
		||||
						</td>
 | 
			
		||||
						<td>{book.frontmatter[settings.titleProperty]}</td>
 | 
			
		||||
						<td>
 | 
			
		||||
							{book.frontmatter[settings.authorsProperty].join(
 | 
			
		||||
								", ",
 | 
			
		||||
							)}
 | 
			
		||||
						</td>
 | 
			
		||||
						{#if settings.seriesTitleProperty}
 | 
			
		||||
							<td>
 | 
			
		||||
								{book.frontmatter[settings.seriesTitleProperty]}
 | 
			
		||||
							</td>
 | 
			
		||||
						{/if}
 | 
			
		||||
						{#if settings.seriesNumberProperty}
 | 
			
		||||
							<td>
 | 
			
		||||
								{book.frontmatter[
 | 
			
		||||
									settings.seriesNumberProperty
 | 
			
		||||
								]}
 | 
			
		||||
							</td>
 | 
			
		||||
						{/if}
 | 
			
		||||
						{#if settings.statusFilter === STATUS_IN_PROGRESS || settings.statusFilter === STATUS_READ}
 | 
			
		||||
							<td>
 | 
			
		||||
								{book.frontmatter[
 | 
			
		||||
									settingsStore.settings.startDateProperty
 | 
			
		||||
								]}
 | 
			
		||||
							</td>
 | 
			
		||||
						{/if}
 | 
			
		||||
						{#if settings.statusFilter === STATUS_READ}
 | 
			
		||||
							<td>
 | 
			
		||||
								{book.frontmatter[
 | 
			
		||||
									settingsStore.settings.endDateProperty
 | 
			
		||||
								]}
 | 
			
		||||
							</td>
 | 
			
		||||
							<td>
 | 
			
		||||
								<Rating
 | 
			
		||||
									rating={book.frontmatter[
 | 
			
		||||
										settingsStore.settings.ratingProperty
 | 
			
		||||
									]}
 | 
			
		||||
								/>
 | 
			
		||||
							</td>
 | 
			
		||||
						{/if}
 | 
			
		||||
					</tr>
 | 
			
		||||
				{/each}
 | 
			
		||||
			</tbody>
 | 
			
		||||
		</table>
 | 
			
		||||
	{/if}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	.shelf-code-block {
 | 
			
		||||
		.controls {
 | 
			
		||||
			margin-bottom: 1rem;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&.bookshelf-view {
 | 
			
		||||
			.controls {
 | 
			
		||||
				margin-left: auto;
 | 
			
		||||
				margin-right: auto;
 | 
			
		||||
				width: 80%;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		table {
 | 
			
		||||
			border-collapse: collapse;
 | 
			
		||||
			width: 100%;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,46 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import {
 | 
			
		||||
		ALL_TIME,
 | 
			
		||||
		type DateFilterStore,
 | 
			
		||||
	} from "@ui/stores/date-filter.svelte";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		store: Pick<
 | 
			
		||||
			DateFilterStore,
 | 
			
		||||
			"filterYear" | "filterYears" | "filterMonth" | "filterMonths"
 | 
			
		||||
		>;
 | 
			
		||||
		showAllMonths?: boolean;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const { store, showAllMonths }: 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>
 | 
			
		||||
</select>
 | 
			
		||||
{#if store.filterYear !== ALL_TIME}
 | 
			
		||||
	<select class="month-filter" bind:value={store.filterMonth}>
 | 
			
		||||
		<option value={ALL_TIME}>Select Month</option>
 | 
			
		||||
		{#if showAllMonths}
 | 
			
		||||
			<option value={1}>January</option>
 | 
			
		||||
			<option value={2}>February</option>
 | 
			
		||||
			<option value={3}>March</option>
 | 
			
		||||
			<option value={4}>April</option>
 | 
			
		||||
			<option value={5}>May</option>
 | 
			
		||||
			<option value={6}>June</option>
 | 
			
		||||
			<option value={7}>July</option>
 | 
			
		||||
			<option value={8}>August</option>
 | 
			
		||||
			<option value={9}>September</option>
 | 
			
		||||
			<option value={10}>October</option>
 | 
			
		||||
			<option value={11}>November</option>
 | 
			
		||||
			<option value={12}>December</option>
 | 
			
		||||
		{:else}
 | 
			
		||||
			{#each store.filterMonths as month}
 | 
			
		||||
				<option value={month.value}>{month.label}</option>
 | 
			
		||||
			{/each}
 | 
			
		||||
		{/if}
 | 
			
		||||
	</select>
 | 
			
		||||
{/if}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,186 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	interface Props {
 | 
			
		||||
		rating: number;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let { rating }: Props = $props();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<span data-star={rating}>{rating}</span>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	[data-star] {
 | 
			
		||||
		text-align: left;
 | 
			
		||||
		font-style: normal;
 | 
			
		||||
		display: inline-block;
 | 
			
		||||
		position: relative;
 | 
			
		||||
		unicode-bidi: bidi-override;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star]::before {
 | 
			
		||||
		display: block;
 | 
			
		||||
		content: "★★★★★";
 | 
			
		||||
		color: #eee;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star]::after {
 | 
			
		||||
		white-space: nowrap;
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 0;
 | 
			
		||||
		left: 0;
 | 
			
		||||
		content: "★★★★★";
 | 
			
		||||
		width: 0;
 | 
			
		||||
		color: #ff8c00;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
		height: 100%;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	[data-star^="0.1"]::after {
 | 
			
		||||
		width: 2%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="0.2"]::after {
 | 
			
		||||
		width: 4%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="0.3"]::after {
 | 
			
		||||
		width: 6%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="0.4"]::after {
 | 
			
		||||
		width: 8%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="0.5"]::after {
 | 
			
		||||
		width: 10%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="0.6"]::after {
 | 
			
		||||
		width: 12%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="0.7"]::after {
 | 
			
		||||
		width: 14%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="0.8"]::after {
 | 
			
		||||
		width: 16%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="0.9"]::after {
 | 
			
		||||
		width: 18%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="1"]::after {
 | 
			
		||||
		width: 20%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="1.1"]::after {
 | 
			
		||||
		width: 22%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="1.2"]::after {
 | 
			
		||||
		width: 24%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="1.3"]::after {
 | 
			
		||||
		width: 26%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="1.4"]::after {
 | 
			
		||||
		width: 28%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="1.5"]::after {
 | 
			
		||||
		width: 30%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="1.6"]::after {
 | 
			
		||||
		width: 32%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="1.7"]::after {
 | 
			
		||||
		width: 34%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="1.8"]::after {
 | 
			
		||||
		width: 36%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="1.9"]::after {
 | 
			
		||||
		width: 38%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="2"]::after {
 | 
			
		||||
		width: 40%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="2.1"]::after {
 | 
			
		||||
		width: 42%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="2.2"]::after {
 | 
			
		||||
		width: 44%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="2.3"]::after {
 | 
			
		||||
		width: 46%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="2.4"]::after {
 | 
			
		||||
		width: 48%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="2.5"]::after {
 | 
			
		||||
		width: 50%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="2.6"]::after {
 | 
			
		||||
		width: 52%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="2.7"]::after {
 | 
			
		||||
		width: 54%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="2.8"]::after {
 | 
			
		||||
		width: 56%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="2.9"]::after {
 | 
			
		||||
		width: 58%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="3"]::after {
 | 
			
		||||
		width: 60%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="3.1"]::after {
 | 
			
		||||
		width: 62%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="3.2"]::after {
 | 
			
		||||
		width: 64%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="3.3"]::after {
 | 
			
		||||
		width: 66%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="3.4"]::after {
 | 
			
		||||
		width: 68%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="3.5"]::after {
 | 
			
		||||
		width: 70%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="3.6"]::after {
 | 
			
		||||
		width: 72%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="3.7"]::after {
 | 
			
		||||
		width: 74%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="3.8"]::after {
 | 
			
		||||
		width: 76%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="3.9"]::after {
 | 
			
		||||
		width: 78%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="4"]::after {
 | 
			
		||||
		width: 80%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="4.1"]::after {
 | 
			
		||||
		width: 82%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="4.2"]::after {
 | 
			
		||||
		width: 84%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="4.3"]::after {
 | 
			
		||||
		width: 86%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="4.4"]::after {
 | 
			
		||||
		width: 88%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="4.5"]::after {
 | 
			
		||||
		width: 90%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="4.6"]::after {
 | 
			
		||||
		width: 92%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="4.7"]::after {
 | 
			
		||||
		width: 94%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="4.8"]::after {
 | 
			
		||||
		width: 96%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="4.9"]::after {
 | 
			
		||||
		width: 98%;
 | 
			
		||||
	}
 | 
			
		||||
	[data-star^="5"]::after {
 | 
			
		||||
		width: 100%;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -22,21 +22,23 @@ export function createDateFilter<T>(
 | 
			
		|||
		initialMonth ? today.getMonth() + 1 : ALL_TIME
 | 
			
		||||
	);
 | 
			
		||||
	const filteredData = $derived.by(() => {
 | 
			
		||||
		return data().filter((item) => {
 | 
			
		||||
			const date = selector(item);
 | 
			
		||||
			if (filterYear !== ALL_TIME) {
 | 
			
		||||
				if (date.year() !== filterYear) {
 | 
			
		||||
					return false;
 | 
			
		||||
		return data()
 | 
			
		||||
			.filter((item) => {
 | 
			
		||||
				const date = selector(item);
 | 
			
		||||
				if (filterYear !== ALL_TIME) {
 | 
			
		||||
					if (date.year() !== filterYear) {
 | 
			
		||||
						return false;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (filterMonth !== ALL_TIME) {
 | 
			
		||||
				if (date.month() !== filterMonth - 1) {
 | 
			
		||||
					return false;
 | 
			
		||||
				if (filterMonth !== ALL_TIME) {
 | 
			
		||||
					if (date.month() !== filterMonth - 1) {
 | 
			
		||||
						return false;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			return true;
 | 
			
		||||
		});
 | 
			
		||||
				return true;
 | 
			
		||||
			})
 | 
			
		||||
			.sort((a, b) => selector(a).diff(selector(b)));
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const filterYears = $derived.by(() => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue