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 type ShelfView = (typeof SHELF_VIEWS)[number];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ShelfSettingsSchema = z.object({
 | 
					export const ShelfSettingsSchema = z.object({
 | 
				
			||||||
| 
						 | 
					@ -27,10 +27,13 @@ export const ShelfSettingsSchema = z.object({
 | 
				
			||||||
	titleProperty: z.string(),
 | 
						titleProperty: z.string(),
 | 
				
			||||||
	subtitleProperty: z.optional(z.string()),
 | 
						subtitleProperty: z.optional(z.string()),
 | 
				
			||||||
	authorsProperty: z.string(),
 | 
						authorsProperty: z.string(),
 | 
				
			||||||
 | 
						descriptionProperty: z.optional(z.string()),
 | 
				
			||||||
	seriesTitleProperty: z.optional(z.string()),
 | 
						seriesTitleProperty: z.optional(z.string()),
 | 
				
			||||||
	seriesNumberProperty: z.optional(z.string()),
 | 
						seriesNumberProperty: z.optional(z.string()),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ShelfSettings = z.infer<typeof ShelfSettingsSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ShelfCodeBlockRenderer extends SvelteCodeBlockRenderer<
 | 
					export class ShelfCodeBlockRenderer extends SvelteCodeBlockRenderer<
 | 
				
			||||||
	typeof ShelfCodeBockView
 | 
						typeof ShelfCodeBockView
 | 
				
			||||||
> {
 | 
					> {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,50 +1,31 @@
 | 
				
			||||||
<script lang="ts">
 | 
					<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 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 {
 | 
						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, type ColorName } from "@utils/color";
 | 
					 | 
				
			||||||
	import { randomElement, randomFloat } from "@utils/rand";
 | 
					 | 
				
			||||||
	import { onDestroy } from "svelte";
 | 
						import { onDestroy } from "svelte";
 | 
				
			||||||
	import { ShelfSettingsSchema } from "./ShelfCodeBlock";
 | 
						import { ShelfSettingsSchema } from "./ShelfCodeBlock";
 | 
				
			||||||
	import { getLinkpath, parseYaml, TFile } from "obsidian";
 | 
						import { parseYaml } from "obsidian";
 | 
				
			||||||
	import DateFilter from "@ui/components/DateFilter.svelte";
 | 
						import DateFilter from "@ui/components/DateFilter.svelte";
 | 
				
			||||||
	import Rating from "@ui/components/Rating.svelte";
 | 
						import BookshelfView from "@ui/components/BookshelfView.svelte";
 | 
				
			||||||
	import { v4 as uuidv4 } from "uuid";
 | 
						import TableView from "@ui/components/TableView.svelte";
 | 
				
			||||||
	import memoize from "just-memoize";
 | 
						import DetailsView from "@ui/components/DetailsView.svelte";
 | 
				
			||||||
 | 
						import {
 | 
				
			||||||
 | 
							createReadingLog,
 | 
				
			||||||
 | 
							setReadingLogContext,
 | 
				
			||||||
 | 
						} from "@ui/stores/reading-log.svelte";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	interface Props {
 | 
						interface Props {
 | 
				
			||||||
		plugin: BookTrackerPlugin;
 | 
							plugin: BookTrackerPlugin;
 | 
				
			||||||
		source: string;
 | 
							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 { plugin, source }: Props = $props();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const settings = ShelfSettingsSchema.parse(parseYaml(source));
 | 
						const settings = ShelfSettingsSchema.parse(parseYaml(source));
 | 
				
			||||||
| 
						 | 
					@ -55,91 +36,7 @@
 | 
				
			||||||
	const metadataStore = createMetadata(plugin, settings.statusFilter, true);
 | 
						const metadataStore = createMetadata(plugin, settings.statusFilter, true);
 | 
				
			||||||
	setMetadataContext(metadataStore);
 | 
						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 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());
 | 
						onDestroy(() => metadataStore.destroy());
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -148,139 +45,24 @@
 | 
				
			||||||
	class="shelf-code-block"
 | 
						class="shelf-code-block"
 | 
				
			||||||
	class:table-view={view === "table"}
 | 
						class:table-view={view === "table"}
 | 
				
			||||||
	class:bookshelf-view={view === "bookshelf"}
 | 
						class:bookshelf-view={view === "bookshelf"}
 | 
				
			||||||
 | 
						class:details-view={view === "details"}
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	<div class="controls">
 | 
						<div class="controls">
 | 
				
			||||||
		<select bind:value={view}>
 | 
							<select bind:value={view}>
 | 
				
			||||||
			<option value="table">Table</option>
 | 
								<option value="table">Table</option>
 | 
				
			||||||
			<option value="bookshelf">Bookshelf</option>
 | 
								<option value="bookshelf">Bookshelf</option>
 | 
				
			||||||
 | 
								<option value="details">Details</option>
 | 
				
			||||||
		</select>
 | 
							</select>
 | 
				
			||||||
		{#if settings.statusFilter === STATUS_READ}
 | 
							{#if settings.statusFilter === STATUS_READ}
 | 
				
			||||||
			<DateFilter store={metadataStore} />
 | 
								<DateFilter store={metadataStore} />
 | 
				
			||||||
		{/if}
 | 
							{/if}
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	{#if view === "bookshelf"}
 | 
						{#if view === "bookshelf"}
 | 
				
			||||||
		<Bookshelf>
 | 
							<BookshelfView {plugin} {settings} />
 | 
				
			||||||
			{#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>
 | 
					 | 
				
			||||||
	{:else if view === "table"}
 | 
						{:else if view === "table"}
 | 
				
			||||||
		<table>
 | 
							<TableView {plugin} {settings} />
 | 
				
			||||||
			<thead>
 | 
						{:else if view === "details"}
 | 
				
			||||||
				<tr>
 | 
							<DetailsView {plugin} {settings} />
 | 
				
			||||||
					<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>
 | 
					 | 
				
			||||||
	{/if}
 | 
						{/if}
 | 
				
			||||||
</div>
 | 
					</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;
 | 
							name?: string;
 | 
				
			||||||
		max?: number;
 | 
							max?: number;
 | 
				
			||||||
		half?: boolean;
 | 
							half?: boolean;
 | 
				
			||||||
 | 
							iconSize?: number;
 | 
				
			||||||
 | 
							disabled?: boolean;
 | 
				
			||||||
		inactive?: Snippet;
 | 
							inactive?: Snippet;
 | 
				
			||||||
		active?: Snippet;
 | 
							active?: Snippet;
 | 
				
			||||||
		partial?: Snippet;
 | 
							partial?: Snippet;
 | 
				
			||||||
| 
						 | 
					@ -14,9 +16,10 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let {
 | 
						let {
 | 
				
			||||||
		value = $bindable(),
 | 
							value = $bindable(),
 | 
				
			||||||
		name = "",
 | 
					 | 
				
			||||||
		max = 5,
 | 
							max = 5,
 | 
				
			||||||
		half = false,
 | 
							half = false,
 | 
				
			||||||
 | 
							iconSize = 64,
 | 
				
			||||||
 | 
							disabled,
 | 
				
			||||||
		inactive,
 | 
							inactive,
 | 
				
			||||||
		active,
 | 
							active,
 | 
				
			||||||
		partial,
 | 
							partial,
 | 
				
			||||||
| 
						 | 
					@ -36,7 +39,9 @@
 | 
				
			||||||
	let hovering = $state(false);
 | 
						let hovering = $state(false);
 | 
				
			||||||
	let valueHover = $state(0);
 | 
						let valueHover = $state(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let displayVal = $derived(hovering ? valueHover : (value ?? 0));
 | 
						let displayVal = $derived(
 | 
				
			||||||
 | 
							hovering && !disabled ? valueHover : (value ?? 0),
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
	let items = $derived.by(() => {
 | 
						let items = $derived.by(() => {
 | 
				
			||||||
		const full = Number.isInteger(displayVal);
 | 
							const full = Number.isInteger(displayVal);
 | 
				
			||||||
		return Array.from({ length: max }, (_, i) => i + 1).map((index) => ({
 | 
							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_click_events_have_key_events -->
 | 
				
			||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
 | 
					<!-- svelte-ignore a11y_no_static_element_interactions -->
 | 
				
			||||||
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
 | 
					<!-- 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 -->
 | 
						<!-- svelte-ignore a11y_no_static_element_interactions -->
 | 
				
			||||||
	<div class="ctrl" {onmousemove} bind:this={ctrl}></div>
 | 
						<div class="ctrl" {onmousemove} bind:this={ctrl}></div>
 | 
				
			||||||
	<div class="cont m-1" bind:clientWidth={w} bind:clientHeight={h}>
 | 
						<div class="cont m-1" bind:clientWidth={w} bind:clientHeight={h}>
 | 
				
			||||||
| 
						 | 
					@ -132,14 +142,14 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		:global(svg) {
 | 
							:global(svg) {
 | 
				
			||||||
			position: absolute;
 | 
								position: absolute;
 | 
				
			||||||
			width: 100%;
 | 
								width: var(--icon-size);
 | 
				
			||||||
			height: 100%;
 | 
								height: var(--icon-size);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		.rating-item {
 | 
							.rating-item {
 | 
				
			||||||
			position: relative;
 | 
								position: relative;
 | 
				
			||||||
			width: var(--size-4-16);
 | 
								width: var(--icon-size);
 | 
				
			||||||
			height: var(--size-4-16);
 | 
								height: var(--icon-size);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
</style>
 | 
					</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