generated from tpl/obsidian-sample-plugin
Add reading calendar
This commit is contained in:
parent
ac10cf646f
commit
ffb8cc8d9c
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "obsidian-book-tracker",
|
"id": "obsidian-book-tracker",
|
||||||
"name": "Book Tracker",
|
"name": "Book Tracker",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
|
"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
|
||||||
"author": "FiFiTiDo",
|
"author": "FiFiTiDo",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "obsidian-book-tracker",
|
"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.",
|
"description": "Simplifies tracking your reading progress and managing your book collection in Obsidian.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { Goodreads } from "@data-sources/Goodreads";
|
||||||
import { CreateBookFromGoodreadsUrlCommand } from "@commands/CreateBookFromGoodreadsUrlCommand";
|
import { CreateBookFromGoodreadsUrlCommand } from "@commands/CreateBookFromGoodreadsUrlCommand";
|
||||||
import { registerShelfCodeBlockProcessor } from "@ui/code-blocks/ShelfCodeBlock";
|
import { registerShelfCodeBlockProcessor } from "@ui/code-blocks/ShelfCodeBlock";
|
||||||
import { ReloadReadingLogCommand } from "@commands/ReloadReadingLogCommand";
|
import { ReloadReadingLogCommand } from "@commands/ReloadReadingLogCommand";
|
||||||
|
import { registerReadingCalendarCodeBlockProcessor } from "@ui/code-blocks/ReadingCalendarCodeBlock";
|
||||||
|
|
||||||
export default class BookTrackerPlugin extends Plugin {
|
export default class BookTrackerPlugin extends Plugin {
|
||||||
public settings: BookTrackerPluginSettings;
|
public settings: BookTrackerPluginSettings;
|
||||||
|
@ -89,6 +90,7 @@ export default class BookTrackerPlugin extends Plugin {
|
||||||
registerReadingLogCodeBlockProcessor(this);
|
registerReadingLogCodeBlockProcessor(this);
|
||||||
registerReadingStatsCodeBlockProcessor(this);
|
registerReadingStatsCodeBlockProcessor(this);
|
||||||
registerShelfCodeBlockProcessor(this);
|
registerShelfCodeBlockProcessor(this);
|
||||||
|
registerReadingCalendarCodeBlockProcessor(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {}
|
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(
|
registerCodeBlockRenderer(
|
||||||
plugin,
|
plugin,
|
||||||
"readinglog",
|
"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 type { ReadingLogEntry } from "@utils/ReadingLog";
|
||||||
import { Edit, Trash, Plus } from "lucide-svelte";
|
import { Edit, Trash, Plus } from "lucide-svelte";
|
||||||
import { ReadingLogEntryEditModal } from "@ui/modals";
|
import { ReadingLogEntryEditModal } from "@ui/modals";
|
||||||
import type BookTrackerPlugin from "@src/main";
|
|
||||||
import { createReadingLog } from "@ui/stores/reading-log.svelte";
|
import { createReadingLog } from "@ui/stores/reading-log.svelte";
|
||||||
import { ALL_TIME } from "@ui/stores/date-filter.svelte";
|
import { ALL_TIME } from "@ui/stores/date-filter.svelte";
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import OpenFileLink from "@ui/components/OpenFileLink.svelte";
|
import OpenFileLink from "@ui/components/OpenFileLink.svelte";
|
||||||
import { setAppContext } from "@ui/stores/app";
|
import { setAppContext } from "@ui/stores/app";
|
||||||
|
import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer";
|
||||||
|
|
||||||
interface Props {
|
const { plugin }: SvelteCodeBlockProps = $props();
|
||||||
plugin: BookTrackerPlugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { plugin }: Props = $props();
|
|
||||||
setAppContext(plugin.app);
|
setAppContext(plugin.app);
|
||||||
|
|
||||||
const store = createReadingLog(plugin.readingLog);
|
const store = createReadingLog(plugin.readingLog);
|
||||||
|
@ -32,7 +28,6 @@
|
||||||
const modal = new ReadingLogEntryEditModal(
|
const modal = new ReadingLogEntryEditModal(
|
||||||
plugin,
|
plugin,
|
||||||
async (entry) => {
|
async (entry) => {
|
||||||
modal.close();
|
|
||||||
await store.updateEntry(entry);
|
await store.updateEntry(entry);
|
||||||
},
|
},
|
||||||
entry,
|
entry,
|
||||||
|
|
|
@ -11,7 +11,13 @@ export function registerReadingStatsCodeBlockProcessor(
|
||||||
registerCodeBlockRenderer(
|
registerCodeBlockRenderer(
|
||||||
plugin,
|
plugin,
|
||||||
"readingstats",
|
"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 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,
|
createSettings,
|
||||||
setSettingsContext,
|
setSettingsContext,
|
||||||
} from "@ui/stores/settings.svelte";
|
} from "@ui/stores/settings.svelte";
|
||||||
import type BookTrackerPlugin from "@src/main";
|
|
||||||
import BookCountStat from "@ui/components/stats/BookCountStat.svelte";
|
import BookCountStat from "@ui/components/stats/BookCountStat.svelte";
|
||||||
import { ALL_TIME } from "@ui/stores/date-filter.svelte";
|
import { ALL_TIME } from "@ui/stores/date-filter.svelte";
|
||||||
import {
|
import {
|
||||||
|
@ -28,13 +27,9 @@
|
||||||
setReadingLogContext,
|
setReadingLogContext,
|
||||||
} from "@ui/stores/reading-log.svelte";
|
} from "@ui/stores/reading-log.svelte";
|
||||||
import { setAppContext } from "@ui/stores/app";
|
import { setAppContext } from "@ui/stores/app";
|
||||||
|
import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer";
|
||||||
|
|
||||||
interface Props {
|
const { plugin, source }: SvelteCodeBlockProps = $props();
|
||||||
plugin: BookTrackerPlugin;
|
|
||||||
source: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { plugin, source }: Props = $props();
|
|
||||||
setAppContext(plugin.app);
|
setAppContext(plugin.app);
|
||||||
|
|
||||||
const settingsStore = createSettings(plugin);
|
const settingsStore = createSettings(plugin);
|
||||||
|
|
|
@ -11,7 +11,8 @@ export function registerShelfCodeBlockProcessor(
|
||||||
registerCodeBlockRenderer(
|
registerCodeBlockRenderer(
|
||||||
plugin,
|
plugin,
|
||||||
"shelf",
|
"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 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">
|
<script lang="ts">
|
||||||
import { STATUS_READ } from "@src/const";
|
import { STATUS_READ } from "@src/const";
|
||||||
import type BookTrackerPlugin from "@src/main";
|
|
||||||
import {
|
import {
|
||||||
createMetadata,
|
createMetadata,
|
||||||
setMetadataContext,
|
setMetadataContext,
|
||||||
|
@ -17,13 +16,9 @@
|
||||||
import TableView from "@ui/components/TableView.svelte";
|
import TableView from "@ui/components/TableView.svelte";
|
||||||
import DetailsView from "@ui/components/DetailsView.svelte";
|
import DetailsView from "@ui/components/DetailsView.svelte";
|
||||||
import { setAppContext } from "@ui/stores/app";
|
import { setAppContext } from "@ui/stores/app";
|
||||||
|
import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer";
|
||||||
|
|
||||||
interface Props {
|
const { plugin, source }: SvelteCodeBlockProps = $props();
|
||||||
plugin: BookTrackerPlugin;
|
|
||||||
source: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { plugin, source }: Props = $props();
|
|
||||||
setAppContext(plugin.app);
|
setAppContext(plugin.app);
|
||||||
|
|
||||||
const settings = ShelfSettingsSchema.parse(parseYaml(source));
|
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 { MarkdownRenderChild } from "obsidian";
|
||||||
|
import type BookTrackerPlugin from "@src/main";
|
||||||
|
|
||||||
|
export interface SvelteCodeBlockProps {
|
||||||
|
plugin: BookTrackerPlugin;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class SvelteCodeBlockRenderer<
|
export class SvelteCodeBlockRenderer<
|
||||||
TComponent extends Component<TProps, TExports, TBindings>,
|
TComponent extends Component<SvelteCodeBlockProps, TExports>,
|
||||||
TProps extends Record<string, any> = {},
|
TExports extends Record<string, any> = {}
|
||||||
TExports extends Record<string, any> = {},
|
|
||||||
TBindings extends keyof TProps | "" = string
|
|
||||||
> extends MarkdownRenderChild {
|
> extends MarkdownRenderChild {
|
||||||
protected component: TExports | undefined;
|
protected component: TExports | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly contentEl: HTMLElement,
|
|
||||||
private readonly componentCtor: TComponent,
|
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);
|
super(contentEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
onload(): void {
|
onload(): void {
|
||||||
this.component = mount(this.componentCtor, {
|
this.component = mount(this.componentCtor, {
|
||||||
...this.mountOpts,
|
|
||||||
target: this.contentEl,
|
target: this.contentEl,
|
||||||
|
props: {
|
||||||
|
plugin: this.plugin,
|
||||||
|
source: this.source,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,7 @@ export class GoodreadsSearchModal extends SvelteModal<
|
||||||
goodreads: Goodreads,
|
goodreads: Goodreads,
|
||||||
onSearch: (error: any, results: SearchResult[]) => void = () => {}
|
onSearch: (error: any, results: SearchResult[]) => void = () => {}
|
||||||
) {
|
) {
|
||||||
super(app, GoodreadsSearchModalView, {
|
super(app, GoodreadsSearchModalView, { goodreads, onSearch });
|
||||||
props: { goodreads, onSearch },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static createAndOpen(
|
static createAndOpen(
|
||||||
|
|
|
@ -8,7 +8,7 @@ export class RatingModal extends SvelteModal<typeof RatingModalView> {
|
||||||
spiceConfigured: boolean,
|
spiceConfigured: boolean,
|
||||||
onSubmit: (rating: number, spice: number) => void = () => {}
|
onSubmit: (rating: number, spice: number) => void = () => {}
|
||||||
) {
|
) {
|
||||||
super(app, RatingModalView, { props: { spiceConfigured, onSubmit } });
|
super(app, RatingModalView, { spiceConfigured, onSubmit });
|
||||||
}
|
}
|
||||||
|
|
||||||
static createAndOpen(
|
static createAndOpen(
|
||||||
|
|
|
@ -8,11 +8,16 @@ export class ReadingLogEntryEditModal extends SvelteModal<
|
||||||
> {
|
> {
|
||||||
constructor(
|
constructor(
|
||||||
plugin: BookTrackerPlugin,
|
plugin: BookTrackerPlugin,
|
||||||
onSubmit?: (entry: ReadingLogEntry) => void,
|
onSubmit: (entry: ReadingLogEntry) => void,
|
||||||
entry?: ReadingLogEntry
|
entry?: ReadingLogEntry
|
||||||
) {
|
) {
|
||||||
super(plugin.app, ReadingLogEntryEditModalView, {
|
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,
|
pageCount: number,
|
||||||
onSubmit: (pageNumber: number) => void = () => {}
|
onSubmit: (pageNumber: number) => void = () => {}
|
||||||
) {
|
) {
|
||||||
super(app, ReadingProgressModalView, {
|
super(app, ReadingProgressModalView, { pageCount, onSubmit });
|
||||||
props: { pageCount, onSubmit },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static createAndOpen(app: App, pageCount: number): Promise<number> {
|
static createAndOpen(app: App, pageCount: number): Promise<number> {
|
||||||
|
|
|
@ -1,26 +1,23 @@
|
||||||
import { App, Modal } from "obsidian";
|
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<
|
export class SvelteModal<
|
||||||
TComponent extends Component<TProps, TExports, TBindings>,
|
TComponent extends Component<Record<string, any>, any, any>
|
||||||
TProps extends Record<string, any> = {},
|
|
||||||
TExports extends Record<string, any> = {},
|
|
||||||
TBindings extends keyof TProps | "" = string
|
|
||||||
> extends Modal {
|
> extends Modal {
|
||||||
protected component: TExports | undefined;
|
protected component: TComponent | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
app: App,
|
app: App,
|
||||||
private readonly componentCtor: TComponent,
|
private readonly componentCtor: TComponent,
|
||||||
private readonly mountOpts: Omit<MountOptions<TProps>, "target">
|
private readonly props: ComponentProps<TComponent>
|
||||||
) {
|
) {
|
||||||
super(app);
|
super(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen(): void {
|
onOpen(): void {
|
||||||
this.component = mount(this.componentCtor, {
|
this.component = mount(this.componentCtor, {
|
||||||
...this.mountOpts,
|
|
||||||
target: this.contentEl,
|
target: this.contentEl,
|
||||||
|
props: this.props,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -130,8 +130,13 @@ export class ReadingLog extends EventEmitter<ReadingLogEventMap> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateEntry(entry: ReadingLogEntry): Promise<void> {
|
public async updateEntry(entry: ReadingLogEntry): Promise<void> {
|
||||||
this.entries[this.entries.findIndex((other) => other.id === entry.id)] =
|
const index = this.entries.findIndex((other) => other.id === entry.id);
|
||||||
entry;
|
|
||||||
|
if (index === -1) {
|
||||||
|
throw new Error("Entry not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.entries[index] = entry;
|
||||||
await this.save();
|
await this.save();
|
||||||
this.emit("updated", { entry });
|
this.emit("updated", { entry });
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ export class EventEmitter<TEventMap> {
|
||||||
this.listeners[type].push(handler);
|
this.listeners[type].push(handler);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
off() {
|
off: () => {
|
||||||
this.off(type, handler);
|
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