A few small changes

This commit is contained in:
Evan Fiordeliso 2025-07-14 21:16:32 -04:00
parent 891041c965
commit 19d56652eb
6 changed files with 117 additions and 86 deletions

View File

@ -43,7 +43,7 @@ export class ResetReadingStatusCommand extends EditorCheckCommand {
frontMatter[this.settings.endDateProperty] = ""; frontMatter[this.settings.endDateProperty] = "";
}); });
this.readingLog.removeEntriesForBook(file.basename); this.readingLog.deleteEntriesForBook(file.basename);
new Notice("Reading status reset for " + file.basename); new Notice("Reading status reset for " + file.basename);
} }

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { parseYaml } from "obsidian"; import { parseYaml, TFile } from "obsidian";
import { ReadingCalendarSettingsSchema } from "./ReadingCalendarCodeBlock"; import { ReadingCalendarSettingsSchema } from "./ReadingCalendarCodeBlock";
import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer"; import type { SvelteCodeBlockProps } from "./SvelteCodeBlockRenderer";
import { import {
@ -15,7 +15,8 @@
setReadingLogContext, setReadingLogContext,
} from "@ui/stores/reading-log.svelte"; } from "@ui/stores/reading-log.svelte";
import { ArrowLeft, ArrowRight } from "lucide-svelte"; import { ArrowLeft, ArrowRight } from "lucide-svelte";
import { onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
import OpenFileLink from "@ui/components/OpenFileLink.svelte";
const { plugin, source }: SvelteCodeBlockProps = $props(); const { plugin, source }: SvelteCodeBlockProps = $props();
@ -85,6 +86,11 @@
updateToday(); updateToday();
}); });
onDestroy(() => {
metadataStore.destroy();
readingLog.destroy();
});
const weeks = $derived.by(() => { const weeks = $derived.by(() => {
// @ts-expect-error Moment is provided by Obsidian // @ts-expect-error Moment is provided by Obsidian
const firstDay = moment() const firstDay = moment()
@ -114,40 +120,53 @@
return weeks; return weeks;
}); });
interface CoverData { interface BookData {
src: string; coverSrc: string;
alt: string; coverAlt: string;
file: TFile;
} }
interface BookMapItem { interface BookMapItem {
totalPagesRead: number; totalPagesRead: number;
covers: CoverData[]; books: BookData[];
} }
const bookMap = $derived.by(() => { const bookMap = $derived(
const bookMap = new Map<number, BookMapItem>(); readingLog.entries.reduce(
for (const item of readingLog.entries) { (acc, entry) => {
const key = item.createdAt.date(); const key = entry.createdAt.date();
let coverPath = metadataStore.metadata.find( const metadata = metadataStore.metadata.find(
(entry) => entry.file.basename === item.book, (other) => other.file.basename === entry.book,
)?.frontmatter?.[settings.coverProperty]; );
coverPath = plugin.app.vault.getFileByPath(coverPath); if (!metadata) {
return acc;
}
if (!coverPath) { const coverPath = metadata.frontmatter?.[
continue; settings.coverProperty
} ] as string;
const coverFile = plugin.app.vault.getFileByPath(coverPath);
let coverSrc = "";
if (coverFile) {
coverSrc = plugin.app.vault.getResourcePath(coverFile);
}
coverPath = plugin.app.vault.getResourcePath(coverPath); const value = acc[key] ?? { totalPagesRead: 0, books: [] };
value.totalPagesRead += entry.pagesRead;
value.books.push({
coverSrc,
coverAlt: entry.book,
file: metadata.file,
});
acc[key] = value;
const value = bookMap.get(key) ?? { totalPagesRead: 0, covers: [] }; return acc;
value.totalPagesRead += item.pagesRead; },
value.covers.push({ src: coverPath, alt: item.book }); {} as Record<number, BookMapItem>,
bookMap.set(key, value); ),
} );
return bookMap;
});
</script> </script>
<div class="reading-calendar"> <div class="reading-calendar">
@ -207,6 +226,7 @@
<tr> <tr>
{#each week as day} {#each week as day}
{@const isThisMonth = day.month() === month} {@const isThisMonth = day.month() === month}
{@const date = day.date()}
<td <td
class:is-today={day.isSame(today, "day")} class:is-today={day.isSame(today, "day")}
class:is-weekend={day.day() === 0 || class:is-weekend={day.day() === 0 ||
@ -218,23 +238,25 @@
> >
<div class="header"> <div class="header">
<span>{day.date()}</span> <span>{day.date()}</span>
{#if isThisMonth && bookMap.has(day.date())} {#if isThisMonth && date in bookMap}
{@const data = bookMap.get(day.date())!} {@const data = bookMap[date]}
<span class="total-pages-read"> <span class="total-pages-read">
Pages: {data.totalPagesRead} Pages: {data.totalPagesRead}
</span> </span>
{/if} {/if}
</div> </div>
<div class="covers"> <div class="covers">
{#if isThisMonth && bookMap.has(day.date())} {#if isThisMonth && date in bookMap}
{@const data = bookMap.get(day.date())!} {@const data = bookMap[date]}
{#each data.covers as cover} {#each data.books as book}
{#if cover} <div class="cover">
<img <OpenFileLink file={book.file}>
src={cover.src} <img
alt={cover.alt} src={book.coverSrc}
/> alt={book.coverAlt}
{/if} />
</OpenFileLink>
</div>
{/each} {/each}
{/if} {/if}
</div> </div>
@ -312,59 +334,46 @@
width: 100%; width: 100%;
aspect-ratio: 2 / 3; aspect-ratio: 2 / 3;
img { .cover {
border-radius: var(--radius-l); border-radius: var(--radius-l);
} position: absolute;
height: 100%;
width: 100%;
&:has(img:first-child:nth-last-child(1)) {
img { img {
position: absolute; border-radius: var(--radius-l);
height: 100%;
width: 100%; width: 100%;
height: 100%;
object-fit: cover;
} }
} }
&:has(img:first-child:nth-last-child(2)) { &:has(.cover:first-child:nth-last-child(2)) {
img:first-child { .cover:first-child {
position: absolute;
height: 100%;
width: 100%;
clip-path: polygon(0 0, 100% 0, 0 100%); clip-path: polygon(0 0, 100% 0, 0 100%);
} }
img:last-child { .cover:last-child {
position: absolute;
height: 100%;
width: 100%;
clip-path: polygon(100% 100%, 100% 0, 0 100%); clip-path: polygon(100% 100%, 100% 0, 0 100%);
} }
} }
&:has(img:first-child:nth-last-child(3)) { &:has(.cover:first-child:nth-last-child(3)) {
img:first-child { .cover:first-child {
position: absolute;
height: 100%;
width: 100%;
clip-path: polygon(0 0, 100% 0, 100% 20%, 0 50%); clip-path: polygon(0 0, 100% 0, 100% 20%, 0 50%);
} }
img:nth-child(2) { .cover:nth-child(2) {
position: absolute;
height: 100%;
width: 100%;
clip-path: polygon(100% 80%, 100% 20%, 0 50%); clip-path: polygon(100% 80%, 100% 20%, 0 50%);
} }
img:last-child { .cover:last-child {
position: absolute;
height: 100%;
width: 100%;
clip-path: polygon(0 50%, 100% 80%, 100% 100%, 0% 100%); clip-path: polygon(0 50%, 100% 80%, 100% 100%, 0% 100%);
} }
} }
&:has(img:first-child:nth-last-child(4)) { &:has(.cover:first-child:nth-last-child(4)) {
img:first-child { .cover:first-child {
position: absolute; position: absolute;
height: 50%; height: 50%;
width: 50%; width: 50%;
@ -372,7 +381,7 @@
left: 0; left: 0;
} }
img:nth-child(2) { .cover:nth-child(2) {
position: absolute; position: absolute;
height: 50%; height: 50%;
width: 50%; width: 50%;
@ -380,7 +389,7 @@
right: 0; right: 0;
} }
img:nth-child(3) { .cover:nth-child(3) {
position: absolute; position: absolute;
height: 50%; height: 50%;
width: 50%; width: 50%;
@ -388,7 +397,7 @@
left: 0; left: 0;
} }
img:last-child { .cover:last-child {
position: absolute; position: absolute;
height: 50%; height: 50%;
width: 50%; width: 50%;

View File

@ -16,8 +16,14 @@
const href = $derived( const href = $derived(
makeUri(file instanceof TFile ? file.path : getLinkpath(file)), makeUri(file instanceof TFile ? file.path : getLinkpath(file)),
); );
const title = $derived(
file instanceof TFile
? file.basename
: file.split("/").pop()?.replace(".md", ""),
);
</script> </script>
<a {href}> <a {href} {title}>
{@render children?.()} {@render children?.()}
</a> </a>

View File

@ -111,12 +111,13 @@
type: "linear", type: "linear",
display: true, display: true,
position: "left", position: "left",
ticks: { beginAtZero: true }, beginAtZero: true,
}, },
y1: { y1: {
type: "linear", type: "linear",
display: !isMonthly, display: !isMonthly,
position: "right", position: "right",
beginAtZero: true,
grid: { drawOnChartArea: false }, grid: { drawOnChartArea: false },
}, },
}, },

View File

@ -1,6 +1,10 @@
import type { ReadingLog, ReadingLogEntry } from "@utils/ReadingLog"; import type { ReadingLog, ReadingLogEntry } from "@utils/ReadingLog";
import { getContext, setContext } from "svelte"; import { getContext, setContext } from "svelte";
import { createDateFilter, type DateFilterStore } from "./date-filter.svelte"; import {
createDateFilter,
type DateFilterStore,
type DateFilterStoreOptions,
} from "./date-filter.svelte";
export interface ReadingLogStore extends DateFilterStore { export interface ReadingLogStore extends DateFilterStore {
get entries(): ReadingLogEntry[]; get entries(): ReadingLogEntry[];
@ -12,13 +16,16 @@ export interface ReadingLogStore extends DateFilterStore {
destroy(): void; destroy(): void;
} }
export function createReadingLog(readingLog: ReadingLog): ReadingLogStore { export function createReadingLog(
readingLog: ReadingLog,
{ initialMonth = true, ...otherOpts }: DateFilterStoreOptions = {}
): ReadingLogStore {
let entries: ReadingLogEntry[] = $state(readingLog.getEntries()); let entries: ReadingLogEntry[] = $state(readingLog.getEntries());
const dateFilter = createDateFilter( const dateFilter = createDateFilter(
() => entries, () => entries,
(entry) => entry.createdAt, (entry) => entry.createdAt,
{ initialMonth: true } { initialMonth, ...otherOpts }
); );
async function addEntry(entry: ReadingLogEntry): Promise<void> { async function addEntry(entry: ReadingLogEntry): Promise<void> {
@ -34,18 +41,24 @@ export function createReadingLog(readingLog: ReadingLog): ReadingLogStore {
} }
async function removeEntry(entry: ReadingLogEntry): Promise<void> { async function removeEntry(entry: ReadingLogEntry): Promise<void> {
await readingLog.removeEntry(entry); await readingLog.deleteEntry(entry);
} }
const loadHandler = readingLog.on("load", (payload) => { const loadHandler = readingLog.on("load", (payload) => {
console.info("Reading log loaded");
console.debug("Reading log entries:", payload.entries);
entries = payload.entries; entries = payload.entries;
}); });
const createdHandler = readingLog.on("created", (payload) => { const createdHandler = readingLog.on("create", (payload) => {
console.info("Reading log entry created");
console.debug("Reading log entry:", payload.entry);
entries.push(payload.entry); entries.push(payload.entry);
}); });
const updatedHandler = readingLog.on("updated", (payload) => { const updatedHandler = readingLog.on("update", (payload) => {
console.info("Reading log entry updated");
console.debug("Reading log entry:", payload.entry);
const index = entries.findIndex( const index = entries.findIndex(
(entry) => entry.id === payload.entry.id (entry) => entry.id === payload.entry.id
); );
@ -54,7 +67,9 @@ export function createReadingLog(readingLog: ReadingLog): ReadingLogStore {
} }
}); });
const removedHandler = readingLog.on("removed", (payload) => { const removedHandler = readingLog.on("delete", (payload) => {
console.info("Reading log entry deleted");
console.debug("Reading log entry:", payload.entry);
const index = entries.findIndex( const index = entries.findIndex(
(entry) => entry.id === payload.entry.id (entry) => entry.id === payload.entry.id
); );

View File

@ -14,9 +14,9 @@ export interface ReadingLogEntry {
interface ReadingLogEventMap { interface ReadingLogEventMap {
load: { entries: ReadingLogEntry[] }; load: { entries: ReadingLogEntry[] };
created: { entry: ReadingLogEntry }; create: { entry: ReadingLogEntry };
updated: { entry: ReadingLogEntry }; update: { entry: ReadingLogEntry };
removed: { entry: ReadingLogEntry }; delete: { entry: ReadingLogEntry };
} }
const DEFAULT_FILENAME = "reading-log.json"; const DEFAULT_FILENAME = "reading-log.json";
@ -126,7 +126,7 @@ export class ReadingLog extends EventEmitter<ReadingLogEventMap> {
public async addRawEntry(entry: ReadingLogEntry) { public async addRawEntry(entry: ReadingLogEntry) {
this.entries.push(entry); this.entries.push(entry);
await this.save(); await this.save();
this.emit("created", { entry }); this.emit("create", { entry });
} }
public async updateEntry(entry: ReadingLogEntry): Promise<void> { public async updateEntry(entry: ReadingLogEntry): Promise<void> {
@ -138,20 +138,20 @@ export class ReadingLog extends EventEmitter<ReadingLogEventMap> {
this.entries[index] = entry; this.entries[index] = entry;
await this.save(); await this.save();
this.emit("updated", { entry }); this.emit("update", { entry });
} }
public async removeEntry(entry: ReadingLogEntry): Promise<void> { public async deleteEntry(entry: ReadingLogEntry): Promise<void> {
const index = this.entries.findIndex((other) => other.id === entry.id); const index = this.entries.findIndex((other) => other.id === entry.id);
if (index !== -1) { if (index !== -1) {
const removed = this.entries.splice(index, 1); const removed = this.entries.splice(index, 1);
await this.save(); await this.save();
this.emit("removed", { entry: removed[0] }); this.emit("delete", { entry: removed[0] });
} }
} }
public async removeEntriesForBook(book: string): Promise<void> { public async deleteEntriesForBook(book: string): Promise<void> {
this.entries = this.entries.filter((entry) => entry.book !== book); this.entries = this.entries.filter((entry) => entry.book !== book);
await this.save(); await this.save();
} }