generated from tpl/obsidian-sample-plugin
			Add details view to shelf code block
This commit is contained in:
		
							parent
							
								
									f4c2aabf1f
								
							
						
					
					
						commit
						67930eb1fd
					
				| 
						 | 
				
			
			@ -15,7 +15,7 @@ export function registerShelfCodeBlockProcessor(
 | 
			
		|||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SHELF_VIEWS = ["table", "bookshelf"] as const;
 | 
			
		||||
export const SHELF_VIEWS = ["table", "bookshelf", "details"] as const;
 | 
			
		||||
export type ShelfView = (typeof SHELF_VIEWS)[number];
 | 
			
		||||
 | 
			
		||||
export const ShelfSettingsSchema = z.object({
 | 
			
		||||
| 
						 | 
				
			
			@ -27,10 +27,13 @@ export const ShelfSettingsSchema = z.object({
 | 
			
		|||
	titleProperty: z.string(),
 | 
			
		||||
	subtitleProperty: z.optional(z.string()),
 | 
			
		||||
	authorsProperty: z.string(),
 | 
			
		||||
	descriptionProperty: z.optional(z.string()),
 | 
			
		||||
	seriesTitleProperty: z.optional(z.string()),
 | 
			
		||||
	seriesNumberProperty: z.optional(z.string()),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export type ShelfSettings = z.infer<typeof ShelfSettingsSchema>;
 | 
			
		||||
 | 
			
		||||
export class ShelfCodeBlockRenderer extends SvelteCodeBlockRenderer<
 | 
			
		||||
	typeof ShelfCodeBockView
 | 
			
		||||
> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,50 +1,31 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { STATUS_IN_PROGRESS, STATUS_READ } from "@src/const";
 | 
			
		||||
	import { 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 {
 | 
			
		||||
		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 } from "@utils/rand";
 | 
			
		||||
	import { onDestroy } from "svelte";
 | 
			
		||||
	import { ShelfSettingsSchema } from "./ShelfCodeBlock";
 | 
			
		||||
	import { getLinkpath, parseYaml, TFile } from "obsidian";
 | 
			
		||||
	import { parseYaml } from "obsidian";
 | 
			
		||||
	import DateFilter from "@ui/components/DateFilter.svelte";
 | 
			
		||||
	import Rating from "@ui/components/Rating.svelte";
 | 
			
		||||
	import { v4 as uuidv4 } from "uuid";
 | 
			
		||||
	import memoize from "just-memoize";
 | 
			
		||||
	import BookshelfView from "@ui/components/BookshelfView.svelte";
 | 
			
		||||
	import TableView from "@ui/components/TableView.svelte";
 | 
			
		||||
	import DetailsView from "@ui/components/DetailsView.svelte";
 | 
			
		||||
	import {
 | 
			
		||||
		createReadingLog,
 | 
			
		||||
		setReadingLogContext,
 | 
			
		||||
	} from "@ui/stores/reading-log.svelte";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		plugin: BookTrackerPlugin;
 | 
			
		||||
		source: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	interface BookData {
 | 
			
		||||
		id: string;
 | 
			
		||||
		title: string;
 | 
			
		||||
		subtitle?: string;
 | 
			
		||||
		authors: string[];
 | 
			
		||||
		width: number;
 | 
			
		||||
		color: ColorName;
 | 
			
		||||
		design: (typeof designs)[number];
 | 
			
		||||
		orientation: "default" | "tilted" | "front";
 | 
			
		||||
		file: TFile;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	interface BookStackData {
 | 
			
		||||
		id: string;
 | 
			
		||||
		books: BookData[];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const { plugin, source }: Props = $props();
 | 
			
		||||
 | 
			
		||||
	const settings = ShelfSettingsSchema.parse(parseYaml(source));
 | 
			
		||||
| 
						 | 
				
			
			@ -55,91 +36,7 @@
 | 
			
		|||
	const metadataStore = createMetadata(plugin, settings.statusFilter, true);
 | 
			
		||||
	setMetadataContext(metadataStore);
 | 
			
		||||
 | 
			
		||||
	const designs = ["default", "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 "default";
 | 
			
		||||
		} else if (n < 0.8) {
 | 
			
		||||
			return "tilted";
 | 
			
		||||
		} else {
 | 
			
		||||
			return "front";
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	const randomStackChance = () => randomFloat() > 0.9;
 | 
			
		||||
	function randomStackSize() {
 | 
			
		||||
		const n = randomFloat();
 | 
			
		||||
		if (n < 0.15) {
 | 
			
		||||
			return 5;
 | 
			
		||||
		} else if (n < 0.3) {
 | 
			
		||||
			return 4;
 | 
			
		||||
		} else if (n < 0.5) {
 | 
			
		||||
			return 3;
 | 
			
		||||
		} else if (n < 0.8) {
 | 
			
		||||
			return 2;
 | 
			
		||||
		} else {
 | 
			
		||||
			return 1;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const getBookData = memoize(
 | 
			
		||||
		(metadata: 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,
 | 
			
		||||
				),
 | 
			
		||||
				color: randomColor(),
 | 
			
		||||
				design: orientation === "front" ? "default" : randomDesign(),
 | 
			
		||||
				orientation: randomOrientation(),
 | 
			
		||||
				file: metadata.file,
 | 
			
		||||
			};
 | 
			
		||||
		},
 | 
			
		||||
		(metadata: FileMetadata) => metadata.file.path,
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	let view = $state(settings.defaultView);
 | 
			
		||||
	let books: (BookData | BookStackData)[] = $state([]);
 | 
			
		||||
 | 
			
		||||
	$effect(() => {
 | 
			
		||||
		let newBooks: (BookData | BookStackData)[] = [];
 | 
			
		||||
 | 
			
		||||
		for (let i = 0; i < metadataStore.metadata.length; i++) {
 | 
			
		||||
			if (randomStackChance()) {
 | 
			
		||||
				const booksRemaining = metadataStore.metadata.length - i;
 | 
			
		||||
				const stackSize = Math.min(booksRemaining, randomStackSize());
 | 
			
		||||
 | 
			
		||||
				newBooks.push({
 | 
			
		||||
					id: uuidv4(),
 | 
			
		||||
					books: metadataStore.metadata
 | 
			
		||||
						.slice(i, i + stackSize)
 | 
			
		||||
						.map(getBookData),
 | 
			
		||||
				});
 | 
			
		||||
				i += stackSize - 1;
 | 
			
		||||
			} else {
 | 
			
		||||
				newBooks.push(getBookData(metadataStore.metadata[i]));
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		books = newBooks;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	onDestroy(() => metadataStore.destroy());
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -148,139 +45,24 @@
 | 
			
		|||
	class="shelf-code-block"
 | 
			
		||||
	class:table-view={view === "table"}
 | 
			
		||||
	class:bookshelf-view={view === "bookshelf"}
 | 
			
		||||
	class:details-view={view === "details"}
 | 
			
		||||
>
 | 
			
		||||
	<div class="controls">
 | 
			
		||||
		<select bind:value={view}>
 | 
			
		||||
			<option value="table">Table</option>
 | 
			
		||||
			<option value="bookshelf">Bookshelf</option>
 | 
			
		||||
			<option value="details">Details</option>
 | 
			
		||||
		</select>
 | 
			
		||||
		{#if settings.statusFilter === STATUS_READ}
 | 
			
		||||
			<DateFilter store={metadataStore} />
 | 
			
		||||
		{/if}
 | 
			
		||||
	</div>
 | 
			
		||||
	{#if view === "bookshelf"}
 | 
			
		||||
		<Bookshelf>
 | 
			
		||||
			{#each books as book (book.id)}
 | 
			
		||||
				{#if "books" in book}
 | 
			
		||||
					<BookStack>
 | 
			
		||||
						{#each book.books as bookData (bookData.id)}
 | 
			
		||||
							<Book
 | 
			
		||||
								title={bookData.title}
 | 
			
		||||
								subtitle={bookData.subtitle}
 | 
			
		||||
								authors={bookData.authors}
 | 
			
		||||
								width={bookData.width}
 | 
			
		||||
								color={bookData.color}
 | 
			
		||||
								design={bookData.design}
 | 
			
		||||
								orientation="flat"
 | 
			
		||||
								onClick={() =>
 | 
			
		||||
									plugin.app.workspace
 | 
			
		||||
										.getLeaf("tab")
 | 
			
		||||
										.openFile(bookData.file)}
 | 
			
		||||
							/>
 | 
			
		||||
						{/each}
 | 
			
		||||
					</BookStack>
 | 
			
		||||
				{:else}
 | 
			
		||||
					<Book
 | 
			
		||||
						title={book.title}
 | 
			
		||||
						subtitle={book.subtitle}
 | 
			
		||||
						authors={book.authors}
 | 
			
		||||
						width={book.width}
 | 
			
		||||
						color={book.color}
 | 
			
		||||
						design={book.design}
 | 
			
		||||
						orientation={book.orientation}
 | 
			
		||||
						onClick={() =>
 | 
			
		||||
							plugin.app.workspace
 | 
			
		||||
								.getLeaf("tab")
 | 
			
		||||
								.openFile(book.file)}
 | 
			
		||||
					/>
 | 
			
		||||
				{/if}
 | 
			
		||||
			{/each}
 | 
			
		||||
		</Bookshelf>
 | 
			
		||||
		<BookshelfView {plugin} {settings} />
 | 
			
		||||
	{: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>
 | 
			
		||||
							<a href={getLinkpath(book.file.path)}>
 | 
			
		||||
								{book.frontmatter[settings.titleProperty]}
 | 
			
		||||
							</a>
 | 
			
		||||
						</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>
 | 
			
		||||
		<TableView {plugin} {settings} />
 | 
			
		||||
	{:else if view === "details"}
 | 
			
		||||
		<DetailsView {plugin} {settings} />
 | 
			
		||||
	{/if}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,164 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	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 {
 | 
			
		||||
		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 {
 | 
			
		||||
		id: string;
 | 
			
		||||
		title: string;
 | 
			
		||||
		subtitle?: string;
 | 
			
		||||
		authors: string[];
 | 
			
		||||
		width: number;
 | 
			
		||||
		color: ColorName;
 | 
			
		||||
		design: (typeof designs)[number];
 | 
			
		||||
		orientation: "default" | "tilted" | "front";
 | 
			
		||||
		file: TFile;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	interface BookStackData {
 | 
			
		||||
		id: string;
 | 
			
		||||
		books: BookData[];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const { plugin, settings }: Props = $props();
 | 
			
		||||
 | 
			
		||||
	const settingsStore = getSettingsContext();
 | 
			
		||||
	const metadataStore = getMetadataContext();
 | 
			
		||||
 | 
			
		||||
	const designs = ["default", "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 "default";
 | 
			
		||||
		} else if (n < 0.8) {
 | 
			
		||||
			return "tilted";
 | 
			
		||||
		} else {
 | 
			
		||||
			return "front";
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	const randomStackChance = () => randomFloat() > 0.9;
 | 
			
		||||
	function randomStackSize() {
 | 
			
		||||
		const n = randomFloat();
 | 
			
		||||
		if (n < 0.15) {
 | 
			
		||||
			return 5;
 | 
			
		||||
		} else if (n < 0.3) {
 | 
			
		||||
			return 4;
 | 
			
		||||
		} else if (n < 0.5) {
 | 
			
		||||
			return 3;
 | 
			
		||||
		} else if (n < 0.8) {
 | 
			
		||||
			return 2;
 | 
			
		||||
		} else {
 | 
			
		||||
			return 1;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const getBookData = memoize(
 | 
			
		||||
		(metadata: 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,
 | 
			
		||||
				),
 | 
			
		||||
				color: randomColor(),
 | 
			
		||||
				design: orientation === "front" ? "default" : randomDesign(),
 | 
			
		||||
				orientation: randomOrientation(),
 | 
			
		||||
				file: metadata.file,
 | 
			
		||||
			};
 | 
			
		||||
		},
 | 
			
		||||
		(metadata: FileMetadata) => metadata.file.path,
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	let books: (BookData | BookStackData)[] = $state([]);
 | 
			
		||||
	$effect(() => {
 | 
			
		||||
		let newBooks: (BookData | BookStackData)[] = [];
 | 
			
		||||
 | 
			
		||||
		for (let i = 0; i < metadataStore.metadata.length; i++) {
 | 
			
		||||
			if (randomStackChance()) {
 | 
			
		||||
				const booksRemaining = metadataStore.metadata.length - i;
 | 
			
		||||
				const stackSize = Math.min(booksRemaining, randomStackSize());
 | 
			
		||||
 | 
			
		||||
				newBooks.push({
 | 
			
		||||
					id: uuidv4(),
 | 
			
		||||
					books: metadataStore.metadata
 | 
			
		||||
						.slice(i, i + stackSize)
 | 
			
		||||
						.map(getBookData),
 | 
			
		||||
				});
 | 
			
		||||
				i += stackSize - 1;
 | 
			
		||||
			} else {
 | 
			
		||||
				newBooks.push(getBookData(metadataStore.metadata[i]));
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		books = newBooks;
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Bookshelf>
 | 
			
		||||
	{#each books as book (book.id)}
 | 
			
		||||
		{#if "books" in book}
 | 
			
		||||
			<BookStack>
 | 
			
		||||
				{#each book.books as bookData (bookData.id)}
 | 
			
		||||
					<Book
 | 
			
		||||
						title={bookData.title}
 | 
			
		||||
						subtitle={bookData.subtitle}
 | 
			
		||||
						authors={bookData.authors}
 | 
			
		||||
						width={bookData.width}
 | 
			
		||||
						color={bookData.color}
 | 
			
		||||
						design={bookData.design}
 | 
			
		||||
						orientation="flat"
 | 
			
		||||
						onClick={() =>
 | 
			
		||||
							plugin.app.workspace
 | 
			
		||||
								.getLeaf("tab")
 | 
			
		||||
								.openFile(bookData.file)}
 | 
			
		||||
					/>
 | 
			
		||||
				{/each}
 | 
			
		||||
			</BookStack>
 | 
			
		||||
		{:else}
 | 
			
		||||
			<Book
 | 
			
		||||
				title={book.title}
 | 
			
		||||
				subtitle={book.subtitle}
 | 
			
		||||
				authors={book.authors}
 | 
			
		||||
				width={book.width}
 | 
			
		||||
				color={book.color}
 | 
			
		||||
				design={book.design}
 | 
			
		||||
				orientation={book.orientation}
 | 
			
		||||
				onClick={() =>
 | 
			
		||||
					plugin.app.workspace.getLeaf("tab").openFile(book.file)}
 | 
			
		||||
			/>
 | 
			
		||||
		{/if}
 | 
			
		||||
	{/each}
 | 
			
		||||
</Bookshelf>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,199 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	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";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		plugin: BookTrackerPlugin;
 | 
			
		||||
		settings: ShelfSettings;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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}
 | 
			
		||||
 | 
			
		||||
		<div class="book-details">
 | 
			
		||||
			<img
 | 
			
		||||
				src={plugin.app.vault.getResourcePath(
 | 
			
		||||
					plugin.app.vault.getFileByPath(coverPath)!,
 | 
			
		||||
				)}
 | 
			
		||||
				alt={title}
 | 
			
		||||
			/>
 | 
			
		||||
			<div class="book-info">
 | 
			
		||||
				<a href={getLinkpath(book.file.path)}>
 | 
			
		||||
					<h2 class="book-title">
 | 
			
		||||
						{title}
 | 
			
		||||
					</h2>
 | 
			
		||||
				</a>
 | 
			
		||||
				{#if subtitle}
 | 
			
		||||
					<p class="subtitle">{subtitle}</p>
 | 
			
		||||
				{/if}
 | 
			
		||||
				<p class="authors">By: {authors.join(", ")}</p>
 | 
			
		||||
				{#if seriesTitle}
 | 
			
		||||
					<p class="series">
 | 
			
		||||
						<span class="series-title">{seriesTitle}</span>
 | 
			
		||||
						{#if seriesNumber}
 | 
			
		||||
							<span class="series-number">#{seriesNumber}</span>
 | 
			
		||||
						{/if}
 | 
			
		||||
					</p>
 | 
			
		||||
				{/if}
 | 
			
		||||
				{#if description}
 | 
			
		||||
					<hr />
 | 
			
		||||
					<p class="description">{@html 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
 | 
			
		||||
							>
 | 
			
		||||
						</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,
 | 
			
		||||
							)?.pagesReadTotal ?? 0}
 | 
			
		||||
						</p>
 | 
			
		||||
					{/if}
 | 
			
		||||
					{#if settings.statusFilter === STATUS_READ}
 | 
			
		||||
						{@const iconSize = 18}
 | 
			
		||||
						<Dot color="var(--text-muted)" />
 | 
			
		||||
						<p class="end-date">
 | 
			
		||||
							Finished:
 | 
			
		||||
							<datetime datetime={endDate}>{endDate}</datetime>
 | 
			
		||||
						</p>
 | 
			
		||||
						<Dot color="var(--text-muted)" />
 | 
			
		||||
						<RatingInput value={rating} disabled {iconSize}>
 | 
			
		||||
							{#snippet inactive()}
 | 
			
		||||
								<Star
 | 
			
		||||
									color="var(--background-modifier-border)"
 | 
			
		||||
								/>
 | 
			
		||||
							{/snippet}
 | 
			
		||||
							{#snippet active()}
 | 
			
		||||
								<Star
 | 
			
		||||
									color="var(--color-yellow)"
 | 
			
		||||
									fill="rgba(var(--color-yellow-rgb), 0.2)"
 | 
			
		||||
								/>
 | 
			
		||||
							{/snippet}
 | 
			
		||||
							{#snippet partial()}
 | 
			
		||||
								<Star
 | 
			
		||||
									color="var(--background-modifier-border)"
 | 
			
		||||
								/>
 | 
			
		||||
								<StarHalf
 | 
			
		||||
									color="var(--color-yellow)"
 | 
			
		||||
									fill="rgba(var(--color-yellow-rgb), 0.2)"
 | 
			
		||||
								/>
 | 
			
		||||
							{/snippet}
 | 
			
		||||
						</RatingInput>
 | 
			
		||||
						<RatingInput value={spice} disabled {iconSize}>
 | 
			
		||||
							{#snippet inactive()}
 | 
			
		||||
								<Flame
 | 
			
		||||
									color="var(--background-modifier-border)"
 | 
			
		||||
								/>
 | 
			
		||||
							{/snippet}
 | 
			
		||||
							{#snippet active()}
 | 
			
		||||
								<Flame
 | 
			
		||||
									color="var(--color-red)"
 | 
			
		||||
									fill="rgba(var(--color-red-rgb), 0.2)"
 | 
			
		||||
								/>
 | 
			
		||||
							{/snippet}
 | 
			
		||||
						</RatingInput>
 | 
			
		||||
					{/if}
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	{/each}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
	.book-details-list {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		gap: var(--size-4-6);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.book-details {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		align-items: start;
 | 
			
		||||
		gap: 1rem;
 | 
			
		||||
		background-color: var(--background-secondary);
 | 
			
		||||
		border-radius: var(--radius-l);
 | 
			
		||||
 | 
			
		||||
		img {
 | 
			
		||||
			border-radius: var(--radius-l);
 | 
			
		||||
			max-width: 30%;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.book-info {
 | 
			
		||||
			display: flex;
 | 
			
		||||
			flex-direction: column;
 | 
			
		||||
			gap: var(--size-4-2);
 | 
			
		||||
			padding: var(--size-4-4);
 | 
			
		||||
 | 
			
		||||
			h2,
 | 
			
		||||
			p {
 | 
			
		||||
				margin: 0;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			hr {
 | 
			
		||||
				margin: var(--size-4-2) 0;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			.authors,
 | 
			
		||||
			.series {
 | 
			
		||||
				font-size: var(--font-small);
 | 
			
		||||
				color: var(--text-muted);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			.description {
 | 
			
		||||
				max-height: 30rem;
 | 
			
		||||
				overflow-y: auto;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			.footer {
 | 
			
		||||
				font-size: var(--font-smaller);
 | 
			
		||||
				color: var(--text-muted);
 | 
			
		||||
				display: flex;
 | 
			
		||||
				gap: var(--size-2-2);
 | 
			
		||||
				align-items: center;
 | 
			
		||||
				flex-wrap: wrap;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -7,6 +7,8 @@
 | 
			
		|||
		name?: string;
 | 
			
		||||
		max?: number;
 | 
			
		||||
		half?: boolean;
 | 
			
		||||
		iconSize?: number;
 | 
			
		||||
		disabled?: boolean;
 | 
			
		||||
		inactive?: Snippet;
 | 
			
		||||
		active?: Snippet;
 | 
			
		||||
		partial?: Snippet;
 | 
			
		||||
| 
						 | 
				
			
			@ -14,9 +16,10 @@
 | 
			
		|||
 | 
			
		||||
	let {
 | 
			
		||||
		value = $bindable(),
 | 
			
		||||
		name = "",
 | 
			
		||||
		max = 5,
 | 
			
		||||
		half = false,
 | 
			
		||||
		iconSize = 64,
 | 
			
		||||
		disabled,
 | 
			
		||||
		inactive,
 | 
			
		||||
		active,
 | 
			
		||||
		partial,
 | 
			
		||||
| 
						 | 
				
			
			@ -36,7 +39,9 @@
 | 
			
		|||
	let hovering = $state(false);
 | 
			
		||||
	let valueHover = $state(0);
 | 
			
		||||
 | 
			
		||||
	let displayVal = $derived(hovering ? valueHover : (value ?? 0));
 | 
			
		||||
	let displayVal = $derived(
 | 
			
		||||
		hovering && !disabled ? valueHover : (value ?? 0),
 | 
			
		||||
	);
 | 
			
		||||
	let items = $derived.by(() => {
 | 
			
		||||
		const full = Number.isInteger(displayVal);
 | 
			
		||||
		return Array.from({ length: max }, (_, i) => i + 1).map((index) => ({
 | 
			
		||||
| 
						 | 
				
			
			@ -80,7 +85,12 @@
 | 
			
		|||
<!-- svelte-ignore a11y_click_events_have_key_events -->
 | 
			
		||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
 | 
			
		||||
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
 | 
			
		||||
<div class="rating-input" {onclick} {onmouseout}>
 | 
			
		||||
<div
 | 
			
		||||
	class="rating-input"
 | 
			
		||||
	{onclick}
 | 
			
		||||
	{onmouseout}
 | 
			
		||||
	style:--icon-size="{iconSize}px"
 | 
			
		||||
>
 | 
			
		||||
	<!-- svelte-ignore a11y_no_static_element_interactions -->
 | 
			
		||||
	<div class="ctrl" {onmousemove} bind:this={ctrl}></div>
 | 
			
		||||
	<div class="cont m-1" bind:clientWidth={w} bind:clientHeight={h}>
 | 
			
		||||
| 
						 | 
				
			
			@ -132,14 +142,14 @@
 | 
			
		|||
 | 
			
		||||
		:global(svg) {
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			width: 100%;
 | 
			
		||||
			height: 100%;
 | 
			
		||||
			width: var(--icon-size);
 | 
			
		||||
			height: var(--icon-size);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.rating-item {
 | 
			
		||||
			position: relative;
 | 
			
		||||
			width: var(--size-4-16);
 | 
			
		||||
			height: var(--size-4-16);
 | 
			
		||||
			width: var(--icon-size);
 | 
			
		||||
			height: var(--icon-size);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,113 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	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";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		plugin: BookTrackerPlugin;
 | 
			
		||||
		settings: ShelfSettings;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const { plugin, settings }: Props = $props();
 | 
			
		||||
 | 
			
		||||
	const settingsStore = getSettingsContext();
 | 
			
		||||
	const metadataStore = getMetadataContext();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<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}
 | 
			
		||||
			{@const coverPath = book.frontmatter[settings.coverProperty]}
 | 
			
		||||
			{@const title = book.frontmatter[settings.titleProperty]}
 | 
			
		||||
			{@const authors = book.frontmatter[settings.authorsProperty]}
 | 
			
		||||
			{@const seriesTitle = settings.seriesTitleProperty
 | 
			
		||||
				? book.frontmatter[settings.seriesTitleProperty]
 | 
			
		||||
				: undefined}
 | 
			
		||||
			{@const seriesNumber = settings.seriesNumberProperty
 | 
			
		||||
				? book.frontmatter[settings.seriesNumberProperty]
 | 
			
		||||
				: undefined}
 | 
			
		||||
			{@const startDate =
 | 
			
		||||
				book.frontmatter[settingsStore.settings.startDateProperty]}
 | 
			
		||||
			{@const endDate =
 | 
			
		||||
				book.frontmatter[settingsStore.settings.endDateProperty]}
 | 
			
		||||
			{@const rating =
 | 
			
		||||
				book.frontmatter[settingsStore.settings.ratingProperty] ?? 0}
 | 
			
		||||
			{@const spice =
 | 
			
		||||
				book.frontmatter[settingsStore.settings.spiceProperty] ?? 0}
 | 
			
		||||
 | 
			
		||||
			<tr>
 | 
			
		||||
				<td>
 | 
			
		||||
					<img
 | 
			
		||||
						src={plugin.app.vault.getResourcePath(
 | 
			
		||||
							plugin.app.vault.getFileByPath(coverPath)!,
 | 
			
		||||
						)}
 | 
			
		||||
						alt={title}
 | 
			
		||||
						width="50"
 | 
			
		||||
					/>
 | 
			
		||||
				</td>
 | 
			
		||||
				<td>
 | 
			
		||||
					<a href={getLinkpath(book.file.path)}>
 | 
			
		||||
						{title}
 | 
			
		||||
					</a>
 | 
			
		||||
				</td>
 | 
			
		||||
				<td>
 | 
			
		||||
					{authors.join(", ")}
 | 
			
		||||
				</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>
 | 
			
		||||
					</td>
 | 
			
		||||
				{/if}
 | 
			
		||||
				{#if settings.statusFilter === STATUS_READ}
 | 
			
		||||
					<td>
 | 
			
		||||
						<datetime datetime={endDate}>{endDate}</datetime>
 | 
			
		||||
					</td>
 | 
			
		||||
					<td>
 | 
			
		||||
						<Rating {rating} />
 | 
			
		||||
					</td>
 | 
			
		||||
				{/if}
 | 
			
		||||
			</tr>
 | 
			
		||||
		{/each}
 | 
			
		||||
	</tbody>
 | 
			
		||||
</table>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
	table {
 | 
			
		||||
		border-collapse: collapse;
 | 
			
		||||
		width: 100%;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
		Loading…
	
		Reference in New Issue