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