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