generated from tpl/obsidian-sample-plugin
			Add reading calendar
This commit is contained in:
		
							parent
							
								
									ac10cf646f
								
							
						
					
					
						commit
						ffb8cc8d9c
					
				| 
						 | 
				
			
			@ -1,9 +1,9 @@
 | 
			
		|||
{
 | 
			
		||||
	"id": "obsidian-book-tracker",
 | 
			
		||||
	"name": "Book Tracker",
 | 
			
		||||
	"version": "1.0.0",
 | 
			
		||||
	"version": "1.1.0",
 | 
			
		||||
	"minAppVersion": "0.15.0",
 | 
			
		||||
	"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
 | 
			
		||||
	"author": "FiFiTiDo",
 | 
			
		||||
	"isDesktopOnly": false
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
{
 | 
			
		||||
	"name": "obsidian-book-tracker",
 | 
			
		||||
	"version": "1.0.0",
 | 
			
		||||
	"version": "1.1.0",
 | 
			
		||||
	"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
 | 
			
		||||
	"main": "main.js",
 | 
			
		||||
	"scripts": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,7 @@ import { Goodreads } from "@data-sources/Goodreads";
 | 
			
		|||
import { CreateBookFromGoodreadsUrlCommand } from "@commands/CreateBookFromGoodreadsUrlCommand";
 | 
			
		||||
import { registerShelfCodeBlockProcessor } from "@ui/code-blocks/ShelfCodeBlock";
 | 
			
		||||
import { ReloadReadingLogCommand } from "@commands/ReloadReadingLogCommand";
 | 
			
		||||
import { registerReadingCalendarCodeBlockProcessor } from "@ui/code-blocks/ReadingCalendarCodeBlock";
 | 
			
		||||
 | 
			
		||||
export default class BookTrackerPlugin extends Plugin {
 | 
			
		||||
	public settings: BookTrackerPluginSettings;
 | 
			
		||||
| 
						 | 
				
			
			@ -89,6 +90,7 @@ export default class BookTrackerPlugin extends Plugin {
 | 
			
		|||
		registerReadingLogCodeBlockProcessor(this);
 | 
			
		||||
		registerReadingStatsCodeBlockProcessor(this);
 | 
			
		||||
		registerShelfCodeBlockProcessor(this);
 | 
			
		||||
		registerReadingCalendarCodeBlockProcessor(this);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onunload() {}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
import { registerCodeBlockRenderer } from ".";
 | 
			
		||||
import { SvelteCodeBlockRenderer } from "./SvelteCodeBlockRenderer";
 | 
			
		||||
import ReadingCalendarCodeBlockView from "./ReadingCalendarCodeBlockView.svelte";
 | 
			
		||||
import type BookTrackerPlugin from "@src/main";
 | 
			
		||||
import z from "zod/v4";
 | 
			
		||||
 | 
			
		||||
export function registerReadingCalendarCodeBlockProcessor(
 | 
			
		||||
	plugin: BookTrackerPlugin
 | 
			
		||||
): void {
 | 
			
		||||
	registerCodeBlockRenderer(
 | 
			
		||||
		plugin,
 | 
			
		||||
		"readingcalendar",
 | 
			
		||||
		(source, el) =>
 | 
			
		||||
			new SvelteCodeBlockRenderer(
 | 
			
		||||
				ReadingCalendarCodeBlockView,
 | 
			
		||||
				plugin,
 | 
			
		||||
				source,
 | 
			
		||||
				el
 | 
			
		||||
			)
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ReadingCalendarSettingsSchema = z.object({
 | 
			
		||||
	coverProperty: z.string(),
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,400 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { parseYaml } from "obsidian";
 | 
			
		||||
	import { ReadingCalendarSettingsSchema } from "./ReadingCalendarCodeBlock";
 | 
			
		||||
	import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer";
 | 
			
		||||
	import {
 | 
			
		||||
		createSettings,
 | 
			
		||||
		setSettingsContext,
 | 
			
		||||
	} from "@ui/stores/settings.svelte";
 | 
			
		||||
	import {
 | 
			
		||||
		createMetadata,
 | 
			
		||||
		setMetadataContext,
 | 
			
		||||
	} from "@ui/stores/metadata.svelte";
 | 
			
		||||
	import {
 | 
			
		||||
		createReadingLog,
 | 
			
		||||
		setReadingLogContext,
 | 
			
		||||
	} from "@ui/stores/reading-log.svelte";
 | 
			
		||||
	import { ArrowLeft, ArrowRight } from "lucide-svelte";
 | 
			
		||||
	import { onMount } from "svelte";
 | 
			
		||||
 | 
			
		||||
	const { plugin, source }: SvelteCodeBlockProps = $props();
 | 
			
		||||
 | 
			
		||||
	const settings = ReadingCalendarSettingsSchema.parse(parseYaml(source));
 | 
			
		||||
 | 
			
		||||
	const settingsStore = createSettings(plugin);
 | 
			
		||||
	setSettingsContext(settingsStore);
 | 
			
		||||
 | 
			
		||||
	const metadataStore = createMetadata(plugin, null);
 | 
			
		||||
	setMetadataContext(metadataStore);
 | 
			
		||||
 | 
			
		||||
	const readingLog = createReadingLog(plugin.readingLog);
 | 
			
		||||
	setReadingLogContext(readingLog);
 | 
			
		||||
 | 
			
		||||
	let year = $state(new Date().getFullYear());
 | 
			
		||||
	let month = $state(new Date().getMonth());
 | 
			
		||||
 | 
			
		||||
	$effect(() => {
 | 
			
		||||
		readingLog.filterYear = year;
 | 
			
		||||
		readingLog.filterMonth = month + 1;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const monthNames = [
 | 
			
		||||
		"January",
 | 
			
		||||
		"February",
 | 
			
		||||
		"March",
 | 
			
		||||
		"April",
 | 
			
		||||
		"May",
 | 
			
		||||
		"June",
 | 
			
		||||
		"July",
 | 
			
		||||
		"August",
 | 
			
		||||
		"September",
 | 
			
		||||
		"October",
 | 
			
		||||
		"November",
 | 
			
		||||
		"December",
 | 
			
		||||
	];
 | 
			
		||||
 | 
			
		||||
	const daysOfWeek = [
 | 
			
		||||
		"Sunday",
 | 
			
		||||
		"Monday",
 | 
			
		||||
		"Tuesday",
 | 
			
		||||
		"Wednesday",
 | 
			
		||||
		"Thursday",
 | 
			
		||||
		"Friday",
 | 
			
		||||
		"Saturday",
 | 
			
		||||
	];
 | 
			
		||||
 | 
			
		||||
	// @ts-expect-error Moment is provided by Obsidian
 | 
			
		||||
	let today = $state(moment());
 | 
			
		||||
 | 
			
		||||
	function msUntilMidnight() {
 | 
			
		||||
		// @ts-expect-error Moment is provided by Obsidian
 | 
			
		||||
		return moment().endOf("day").diff(today, "milliseconds");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function updateToday() {
 | 
			
		||||
		setTimeout(() => {
 | 
			
		||||
			// @ts-expect-error Moment is provided by Obsidian
 | 
			
		||||
			today = moment();
 | 
			
		||||
			updateToday();
 | 
			
		||||
		}, msUntilMidnight() + 1000);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		updateToday();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const weeks = $derived.by(() => {
 | 
			
		||||
		// @ts-expect-error Moment is provided by Obsidian
 | 
			
		||||
		const firstDay = moment()
 | 
			
		||||
			.year(year)
 | 
			
		||||
			.month(month)
 | 
			
		||||
			.startOf("month")
 | 
			
		||||
			.startOf("week");
 | 
			
		||||
 | 
			
		||||
		// @ts-expect-error Moment is provided by Obsidian
 | 
			
		||||
		const lastDay = moment()
 | 
			
		||||
			.year(year)
 | 
			
		||||
			.month(month)
 | 
			
		||||
			.endOf("month")
 | 
			
		||||
			.endOf("week");
 | 
			
		||||
 | 
			
		||||
		const weeks = [];
 | 
			
		||||
		let currentDay = firstDay.clone();
 | 
			
		||||
		while (currentDay.isBefore(lastDay)) {
 | 
			
		||||
			const week = [];
 | 
			
		||||
			for (let i = 0; i < 7; i++) {
 | 
			
		||||
				week.push(currentDay.clone());
 | 
			
		||||
				currentDay.add(1, "day");
 | 
			
		||||
			}
 | 
			
		||||
			weeks.push(week);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return weeks;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	interface CoverData {
 | 
			
		||||
		src: string;
 | 
			
		||||
		alt: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	interface BookMapItem {
 | 
			
		||||
		totalPagesRead: number;
 | 
			
		||||
		covers: CoverData[];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const bookMap = $derived.by(() => {
 | 
			
		||||
		const bookMap = new Map<number, BookMapItem>();
 | 
			
		||||
		for (const item of readingLog.entries) {
 | 
			
		||||
			const key = item.createdAt.date();
 | 
			
		||||
 | 
			
		||||
			let coverPath = metadataStore.metadata.find(
 | 
			
		||||
				(entry) => entry.file.basename === item.book,
 | 
			
		||||
			)?.frontmatter?.[settings.coverProperty];
 | 
			
		||||
 | 
			
		||||
			coverPath = plugin.app.vault.getFileByPath(coverPath);
 | 
			
		||||
 | 
			
		||||
			if (!coverPath) {
 | 
			
		||||
				continue;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			coverPath = plugin.app.vault.getResourcePath(coverPath);
 | 
			
		||||
 | 
			
		||||
			const value = bookMap.get(key) ?? { totalPagesRead: 0, covers: [] };
 | 
			
		||||
			value.totalPagesRead += item.pagesRead;
 | 
			
		||||
			value.covers.push({ src: coverPath, alt: item.book });
 | 
			
		||||
			bookMap.set(key, value);
 | 
			
		||||
		}
 | 
			
		||||
		return bookMap;
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="reading-calendar">
 | 
			
		||||
	<div class="controls">
 | 
			
		||||
		<div class="left">
 | 
			
		||||
			<button
 | 
			
		||||
				class="prev-month"
 | 
			
		||||
				aria-label="Go to previous month"
 | 
			
		||||
				onclick={() => {
 | 
			
		||||
					if (month === 0) {
 | 
			
		||||
						month = 11;
 | 
			
		||||
						year--;
 | 
			
		||||
					} else {
 | 
			
		||||
						month--;
 | 
			
		||||
					}
 | 
			
		||||
				}}
 | 
			
		||||
			>
 | 
			
		||||
				<ArrowLeft />
 | 
			
		||||
			</button>
 | 
			
		||||
			<button
 | 
			
		||||
				class="today"
 | 
			
		||||
				aria-label="Go to the current month"
 | 
			
		||||
				onclick={() => {
 | 
			
		||||
					year = today.year();
 | 
			
		||||
					month = today.month();
 | 
			
		||||
				}}
 | 
			
		||||
			>
 | 
			
		||||
				Today
 | 
			
		||||
			</button>
 | 
			
		||||
		</div>
 | 
			
		||||
		<h2>{monthNames[month]} {year}</h2>
 | 
			
		||||
		<button
 | 
			
		||||
			class="next-month"
 | 
			
		||||
			aria-label="Go to next month"
 | 
			
		||||
			onclick={() => {
 | 
			
		||||
				if (month === 11) {
 | 
			
		||||
					month = 0;
 | 
			
		||||
					year++;
 | 
			
		||||
				} else {
 | 
			
		||||
					month++;
 | 
			
		||||
				}
 | 
			
		||||
			}}
 | 
			
		||||
		>
 | 
			
		||||
			<ArrowRight />
 | 
			
		||||
		</button>
 | 
			
		||||
	</div>
 | 
			
		||||
	<table>
 | 
			
		||||
		<thead>
 | 
			
		||||
			<tr>
 | 
			
		||||
				{#each daysOfWeek as day}
 | 
			
		||||
					<th>{day}</th>
 | 
			
		||||
				{/each}
 | 
			
		||||
			</tr>
 | 
			
		||||
		</thead>
 | 
			
		||||
		<tbody>
 | 
			
		||||
			{#each weeks as week}
 | 
			
		||||
				<tr>
 | 
			
		||||
					{#each week as day}
 | 
			
		||||
						{@const isThisMonth = day.month() === month}
 | 
			
		||||
						<td
 | 
			
		||||
							class:is-today={day.isSame(today, "day")}
 | 
			
		||||
							class:is-weekend={day.day() === 0 ||
 | 
			
		||||
								day.day() === 6}
 | 
			
		||||
							class:is-prev-month={day.month() ===
 | 
			
		||||
								(month === 0 ? 11 : month - 1)}
 | 
			
		||||
							class:is-next-month={day.month() ===
 | 
			
		||||
								(month + 1) % 12}
 | 
			
		||||
						>
 | 
			
		||||
							<div class="header">
 | 
			
		||||
								<span>{day.date()}</span>
 | 
			
		||||
								{#if isThisMonth && bookMap.has(day.date())}
 | 
			
		||||
									{@const data = bookMap.get(day.date())!}
 | 
			
		||||
									<span class="total-pages-read">
 | 
			
		||||
										Pages: {data.totalPagesRead}
 | 
			
		||||
									</span>
 | 
			
		||||
								{/if}
 | 
			
		||||
							</div>
 | 
			
		||||
							<div class="covers">
 | 
			
		||||
								{#if isThisMonth && bookMap.has(day.date())}
 | 
			
		||||
									{@const data = bookMap.get(day.date())!}
 | 
			
		||||
									{#each data.covers as cover}
 | 
			
		||||
										{#if cover}
 | 
			
		||||
											<img
 | 
			
		||||
												src={cover.src}
 | 
			
		||||
												alt={cover.alt}
 | 
			
		||||
											/>
 | 
			
		||||
										{/if}
 | 
			
		||||
									{/each}
 | 
			
		||||
								{/if}
 | 
			
		||||
							</div>
 | 
			
		||||
						</td>
 | 
			
		||||
					{/each}
 | 
			
		||||
				</tr>
 | 
			
		||||
			{/each}
 | 
			
		||||
		</tbody>
 | 
			
		||||
	</table>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
	$cell-padding: var(--size-4-2);
 | 
			
		||||
 | 
			
		||||
	.reading-calendar {
 | 
			
		||||
		display: flex;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		overflow: auto;
 | 
			
		||||
 | 
			
		||||
		.controls {
 | 
			
		||||
			min-width: 800px;
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: max-content 1fr max-content;
 | 
			
		||||
			gap: var(--size-4-4);
 | 
			
		||||
			align-items: center;
 | 
			
		||||
 | 
			
		||||
			h2 {
 | 
			
		||||
				text-align: center;
 | 
			
		||||
				margin: 0;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			.left {
 | 
			
		||||
				display: flex;
 | 
			
		||||
				gap: var(--size-2-2);
 | 
			
		||||
				align-items: center;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	table {
 | 
			
		||||
		table-layout: fixed;
 | 
			
		||||
		border-collapse: collapse;
 | 
			
		||||
		min-width: 800px;
 | 
			
		||||
 | 
			
		||||
		th {
 | 
			
		||||
			padding: $cell-padding;
 | 
			
		||||
			width: calc(100% / 7);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		td {
 | 
			
		||||
			padding: $cell-padding;
 | 
			
		||||
 | 
			
		||||
			&.is-prev-month,
 | 
			
		||||
			&.is-next-month {
 | 
			
		||||
				color: var(--text-faint);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.is-today {
 | 
			
		||||
				color: var(--text-accent);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			.header {
 | 
			
		||||
				display: flex;
 | 
			
		||||
				justify-content: space-between;
 | 
			
		||||
				align-items: center;
 | 
			
		||||
 | 
			
		||||
				.total-pages-read {
 | 
			
		||||
					font-size: var(--font-smallest);
 | 
			
		||||
					color: var(--text-muted);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			.covers {
 | 
			
		||||
				position: relative;
 | 
			
		||||
				width: 100%;
 | 
			
		||||
				aspect-ratio: 2 / 3;
 | 
			
		||||
 | 
			
		||||
				img {
 | 
			
		||||
					border-radius: var(--radius-l);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&:has(img:first-child:nth-last-child(1)) {
 | 
			
		||||
					img {
 | 
			
		||||
						position: absolute;
 | 
			
		||||
						height: 100%;
 | 
			
		||||
						width: 100%;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&:has(img:first-child:nth-last-child(2)) {
 | 
			
		||||
					img:first-child {
 | 
			
		||||
						position: absolute;
 | 
			
		||||
						height: 100%;
 | 
			
		||||
						width: 100%;
 | 
			
		||||
						clip-path: polygon(0 0, 100% 0, 0 100%);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					img:last-child {
 | 
			
		||||
						position: absolute;
 | 
			
		||||
						height: 100%;
 | 
			
		||||
						width: 100%;
 | 
			
		||||
						clip-path: polygon(100% 100%, 100% 0, 0 100%);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&:has(img:first-child:nth-last-child(3)) {
 | 
			
		||||
					img:first-child {
 | 
			
		||||
						position: absolute;
 | 
			
		||||
						height: 100%;
 | 
			
		||||
						width: 100%;
 | 
			
		||||
						clip-path: polygon(0 0, 100% 0, 100% 20%, 0 50%);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					img:nth-child(2) {
 | 
			
		||||
						position: absolute;
 | 
			
		||||
						height: 100%;
 | 
			
		||||
						width: 100%;
 | 
			
		||||
						clip-path: polygon(100% 80%, 100% 20%, 0 50%);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					img:last-child {
 | 
			
		||||
						position: absolute;
 | 
			
		||||
						height: 100%;
 | 
			
		||||
						width: 100%;
 | 
			
		||||
						clip-path: polygon(0 50%, 100% 80%, 100% 100%, 0% 100%);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&:has(img:first-child:nth-last-child(4)) {
 | 
			
		||||
					img:first-child {
 | 
			
		||||
						position: absolute;
 | 
			
		||||
						height: 50%;
 | 
			
		||||
						width: 50%;
 | 
			
		||||
						top: 0;
 | 
			
		||||
						left: 0;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					img:nth-child(2) {
 | 
			
		||||
						position: absolute;
 | 
			
		||||
						height: 50%;
 | 
			
		||||
						width: 50%;
 | 
			
		||||
						top: 0;
 | 
			
		||||
						right: 0;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					img:nth-child(3) {
 | 
			
		||||
						position: absolute;
 | 
			
		||||
						height: 50%;
 | 
			
		||||
						width: 50%;
 | 
			
		||||
						bottom: 0;
 | 
			
		||||
						left: 0;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					img:last-child {
 | 
			
		||||
						position: absolute;
 | 
			
		||||
						height: 50%;
 | 
			
		||||
						width: 50%;
 | 
			
		||||
						bottom: 0;
 | 
			
		||||
						right: 0;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -9,16 +9,12 @@ export function registerReadingLogCodeBlockProcessor(
 | 
			
		|||
	registerCodeBlockRenderer(
 | 
			
		||||
		plugin,
 | 
			
		||||
		"readinglog",
 | 
			
		||||
		(_source, el) => new ReadingLogCodeBlockRenderer(el, plugin)
 | 
			
		||||
		(source, el) =>
 | 
			
		||||
			new SvelteCodeBlockRenderer(
 | 
			
		||||
				ReadingLogCodeBlockView,
 | 
			
		||||
				plugin,
 | 
			
		||||
				source,
 | 
			
		||||
				el
 | 
			
		||||
			)
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ReadingLogCodeBlockRenderer extends SvelteCodeBlockRenderer<
 | 
			
		||||
	typeof ReadingLogCodeBlockView
 | 
			
		||||
> {
 | 
			
		||||
	constructor(contentEl: HTMLElement, plugin: BookTrackerPlugin) {
 | 
			
		||||
		super(contentEl, ReadingLogCodeBlockView, { props: { plugin } });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onunload() {}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,18 +2,14 @@
 | 
			
		|||
	import type { ReadingLogEntry } from "@utils/ReadingLog";
 | 
			
		||||
	import { Edit, Trash, Plus } from "lucide-svelte";
 | 
			
		||||
	import { ReadingLogEntryEditModal } from "@ui/modals";
 | 
			
		||||
	import type BookTrackerPlugin from "@src/main";
 | 
			
		||||
	import { createReadingLog } from "@ui/stores/reading-log.svelte";
 | 
			
		||||
	import { ALL_TIME } from "@ui/stores/date-filter.svelte";
 | 
			
		||||
	import { onDestroy, onMount } from "svelte";
 | 
			
		||||
	import OpenFileLink from "@ui/components/OpenFileLink.svelte";
 | 
			
		||||
	import { setAppContext } from "@ui/stores/app";
 | 
			
		||||
	import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		plugin: BookTrackerPlugin;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const { plugin }: Props = $props();
 | 
			
		||||
	const { plugin }: SvelteCodeBlockProps = $props();
 | 
			
		||||
	setAppContext(plugin.app);
 | 
			
		||||
 | 
			
		||||
	const store = createReadingLog(plugin.readingLog);
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +28,6 @@
 | 
			
		|||
		const modal = new ReadingLogEntryEditModal(
 | 
			
		||||
			plugin,
 | 
			
		||||
			async (entry) => {
 | 
			
		||||
				modal.close();
 | 
			
		||||
				await store.updateEntry(entry);
 | 
			
		||||
			},
 | 
			
		||||
			entry,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,13 @@ export function registerReadingStatsCodeBlockProcessor(
 | 
			
		|||
	registerCodeBlockRenderer(
 | 
			
		||||
		plugin,
 | 
			
		||||
		"readingstats",
 | 
			
		||||
		(source, el) => new ReadingStatsCodeBlockRenderer(source, el, plugin)
 | 
			
		||||
		(source, el) =>
 | 
			
		||||
			new SvelteCodeBlockRenderer(
 | 
			
		||||
				ReadingStatsCodeBlockView,
 | 
			
		||||
				plugin,
 | 
			
		||||
				source,
 | 
			
		||||
				el
 | 
			
		||||
			)
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -98,19 +104,3 @@ export const ReadingStatsSectionSchema = z.object({
 | 
			
		|||
});
 | 
			
		||||
 | 
			
		||||
export type ReadingStatsSection = z.infer<typeof ReadingStatsSectionSchema>;
 | 
			
		||||
 | 
			
		||||
export class ReadingStatsCodeBlockRenderer extends SvelteCodeBlockRenderer<
 | 
			
		||||
	typeof ReadingStatsCodeBlockView
 | 
			
		||||
> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		source: string,
 | 
			
		||||
		contentEl: HTMLElement,
 | 
			
		||||
		plugin: BookTrackerPlugin
 | 
			
		||||
	) {
 | 
			
		||||
		super(contentEl, ReadingStatsCodeBlockView, {
 | 
			
		||||
			props: { plugin, source },
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onunload() {}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,7 +20,6 @@
 | 
			
		|||
		createSettings,
 | 
			
		||||
		setSettingsContext,
 | 
			
		||||
	} from "@ui/stores/settings.svelte";
 | 
			
		||||
	import type BookTrackerPlugin from "@src/main";
 | 
			
		||||
	import BookCountStat from "@ui/components/stats/BookCountStat.svelte";
 | 
			
		||||
	import { ALL_TIME } from "@ui/stores/date-filter.svelte";
 | 
			
		||||
	import {
 | 
			
		||||
| 
						 | 
				
			
			@ -28,13 +27,9 @@
 | 
			
		|||
		setReadingLogContext,
 | 
			
		||||
	} from "@ui/stores/reading-log.svelte";
 | 
			
		||||
	import { setAppContext } from "@ui/stores/app";
 | 
			
		||||
	import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		plugin: BookTrackerPlugin;
 | 
			
		||||
		source: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const { plugin, source }: Props = $props();
 | 
			
		||||
	const { plugin, source }: SvelteCodeBlockProps = $props();
 | 
			
		||||
	setAppContext(plugin.app);
 | 
			
		||||
 | 
			
		||||
	const settingsStore = createSettings(plugin);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,8 @@ export function registerShelfCodeBlockProcessor(
 | 
			
		|||
	registerCodeBlockRenderer(
 | 
			
		||||
		plugin,
 | 
			
		||||
		"shelf",
 | 
			
		||||
		(source, el) => new ShelfCodeBlockRenderer(plugin, source, el)
 | 
			
		||||
		(source, el) =>
 | 
			
		||||
			new SvelteCodeBlockRenderer(ShelfCodeBockView, plugin, source, el)
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -33,17 +34,3 @@ export const ShelfSettingsSchema = z.object({
 | 
			
		|||
});
 | 
			
		||||
 | 
			
		||||
export type ShelfSettings = z.infer<typeof ShelfSettingsSchema>;
 | 
			
		||||
 | 
			
		||||
export class ShelfCodeBlockRenderer extends SvelteCodeBlockRenderer<
 | 
			
		||||
	typeof ShelfCodeBockView
 | 
			
		||||
> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		plugin: BookTrackerPlugin,
 | 
			
		||||
		source: string,
 | 
			
		||||
		contentEl: HTMLElement
 | 
			
		||||
	) {
 | 
			
		||||
		super(contentEl, ShelfCodeBockView, { props: { plugin, source } });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onunload() {}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { STATUS_READ } from "@src/const";
 | 
			
		||||
	import type BookTrackerPlugin from "@src/main";
 | 
			
		||||
	import {
 | 
			
		||||
		createMetadata,
 | 
			
		||||
		setMetadataContext,
 | 
			
		||||
| 
						 | 
				
			
			@ -17,13 +16,9 @@
 | 
			
		|||
	import TableView from "@ui/components/TableView.svelte";
 | 
			
		||||
	import DetailsView from "@ui/components/DetailsView.svelte";
 | 
			
		||||
	import { setAppContext } from "@ui/stores/app";
 | 
			
		||||
	import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		plugin: BookTrackerPlugin;
 | 
			
		||||
		source: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const { plugin, source }: Props = $props();
 | 
			
		||||
	const { plugin, source }: SvelteCodeBlockProps = $props();
 | 
			
		||||
	setAppContext(plugin.app);
 | 
			
		||||
 | 
			
		||||
	const settings = ShelfSettingsSchema.parse(parseYaml(source));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,26 +1,34 @@
 | 
			
		|||
import { mount, unmount, type Component, type MountOptions } from "svelte";
 | 
			
		||||
import { mount, unmount, type Component } from "svelte";
 | 
			
		||||
import { MarkdownRenderChild } from "obsidian";
 | 
			
		||||
import type BookTrackerPlugin from "@src/main";
 | 
			
		||||
 | 
			
		||||
export interface SvelteCodeBlockProps {
 | 
			
		||||
	plugin: BookTrackerPlugin;
 | 
			
		||||
	source: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class SvelteCodeBlockRenderer<
 | 
			
		||||
	TComponent extends Component<TProps, TExports, TBindings>,
 | 
			
		||||
	TProps extends Record<string, any> = {},
 | 
			
		||||
	TExports extends Record<string, any> = {},
 | 
			
		||||
	TBindings extends keyof TProps | "" = string
 | 
			
		||||
	TComponent extends Component<SvelteCodeBlockProps, TExports>,
 | 
			
		||||
	TExports extends Record<string, any> = {}
 | 
			
		||||
> extends MarkdownRenderChild {
 | 
			
		||||
	protected component: TExports | undefined;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private readonly contentEl: HTMLElement,
 | 
			
		||||
		private readonly componentCtor: TComponent,
 | 
			
		||||
		private readonly mountOpts: Omit<MountOptions<TProps>, "target">
 | 
			
		||||
		private readonly plugin: BookTrackerPlugin,
 | 
			
		||||
		private readonly source: string,
 | 
			
		||||
		private readonly contentEl: HTMLElement
 | 
			
		||||
	) {
 | 
			
		||||
		super(contentEl);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onload(): void {
 | 
			
		||||
		this.component = mount(this.componentCtor, {
 | 
			
		||||
			...this.mountOpts,
 | 
			
		||||
			target: this.contentEl,
 | 
			
		||||
			props: {
 | 
			
		||||
				plugin: this.plugin,
 | 
			
		||||
				source: this.source,
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,9 +11,7 @@ export class GoodreadsSearchModal extends SvelteModal<
 | 
			
		|||
		goodreads: Goodreads,
 | 
			
		||||
		onSearch: (error: any, results: SearchResult[]) => void = () => {}
 | 
			
		||||
	) {
 | 
			
		||||
		super(app, GoodreadsSearchModalView, {
 | 
			
		||||
			props: { goodreads, onSearch },
 | 
			
		||||
		});
 | 
			
		||||
		super(app, GoodreadsSearchModalView, { goodreads, onSearch });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static createAndOpen(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ export class RatingModal extends SvelteModal<typeof RatingModalView> {
 | 
			
		|||
		spiceConfigured: boolean,
 | 
			
		||||
		onSubmit: (rating: number, spice: number) => void = () => {}
 | 
			
		||||
	) {
 | 
			
		||||
		super(app, RatingModalView, { props: { spiceConfigured, onSubmit } });
 | 
			
		||||
		super(app, RatingModalView, { spiceConfigured, onSubmit });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static createAndOpen(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,11 +8,16 @@ export class ReadingLogEntryEditModal extends SvelteModal<
 | 
			
		|||
> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		plugin: BookTrackerPlugin,
 | 
			
		||||
		onSubmit?: (entry: ReadingLogEntry) => void,
 | 
			
		||||
		onSubmit: (entry: ReadingLogEntry) => void,
 | 
			
		||||
		entry?: ReadingLogEntry
 | 
			
		||||
	) {
 | 
			
		||||
		super(plugin.app, ReadingLogEntryEditModalView, {
 | 
			
		||||
			props: { plugin, entry, onSubmit },
 | 
			
		||||
			plugin,
 | 
			
		||||
			entry,
 | 
			
		||||
			onSubmit: (entry: ReadingLogEntry) => {
 | 
			
		||||
				onSubmit(entry);
 | 
			
		||||
				this.close();
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,9 +10,7 @@ export class ReadingProgressModal extends SvelteModal<
 | 
			
		|||
		pageCount: number,
 | 
			
		||||
		onSubmit: (pageNumber: number) => void = () => {}
 | 
			
		||||
	) {
 | 
			
		||||
		super(app, ReadingProgressModalView, {
 | 
			
		||||
			props: { pageCount, onSubmit },
 | 
			
		||||
		});
 | 
			
		||||
		super(app, ReadingProgressModalView, { pageCount, onSubmit });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static createAndOpen(app: App, pageCount: number): Promise<number> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,26 +1,23 @@
 | 
			
		|||
import { App, Modal } from "obsidian";
 | 
			
		||||
import { mount, unmount, type Component, type MountOptions } from "svelte";
 | 
			
		||||
import { mount, unmount, type Component, type ComponentProps } from "svelte";
 | 
			
		||||
 | 
			
		||||
export class SvelteModal<
 | 
			
		||||
	TComponent extends Component<TProps, TExports, TBindings>,
 | 
			
		||||
	TProps extends Record<string, any> = {},
 | 
			
		||||
	TExports extends Record<string, any> = {},
 | 
			
		||||
	TBindings extends keyof TProps | "" = string
 | 
			
		||||
	TComponent extends Component<Record<string, any>, any, any>
 | 
			
		||||
> extends Modal {
 | 
			
		||||
	protected component: TExports | undefined;
 | 
			
		||||
	protected component: TComponent | undefined;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		app: App,
 | 
			
		||||
		private readonly componentCtor: TComponent,
 | 
			
		||||
		private readonly mountOpts: Omit<MountOptions<TProps>, "target">
 | 
			
		||||
		private readonly props: ComponentProps<TComponent>
 | 
			
		||||
	) {
 | 
			
		||||
		super(app);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	onOpen(): void {
 | 
			
		||||
		this.component = mount(this.componentCtor, {
 | 
			
		||||
			...this.mountOpts,
 | 
			
		||||
			target: this.contentEl,
 | 
			
		||||
			props: this.props,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -130,8 +130,13 @@ export class ReadingLog extends EventEmitter<ReadingLogEventMap> {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	public async updateEntry(entry: ReadingLogEntry): Promise<void> {
 | 
			
		||||
		this.entries[this.entries.findIndex((other) => other.id === entry.id)] =
 | 
			
		||||
			entry;
 | 
			
		||||
		const index = this.entries.findIndex((other) => other.id === entry.id);
 | 
			
		||||
 | 
			
		||||
		if (index === -1) {
 | 
			
		||||
			throw new Error("Entry not found");
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.entries[index] = entry;
 | 
			
		||||
		await this.save();
 | 
			
		||||
		this.emit("updated", { entry });
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,7 @@ export class EventEmitter<TEventMap> {
 | 
			
		|||
		this.listeners[type].push(handler);
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			off() {
 | 
			
		||||
			off: () => {
 | 
			
		||||
				this.off(type, handler);
 | 
			
		||||
			},
 | 
			
		||||
		};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
{
 | 
			
		||||
	"1.0.0": "0.15.0"
 | 
			
		||||
}
 | 
			
		||||
	"1.0.0": "0.15.0",
 | 
			
		||||
	"1.1.0": "0.15.0"
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue