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