generated from tpl/obsidian-sample-plugin
Add reading log viewer
This commit is contained in:
parent
93da69910c
commit
a2767f4595
|
@ -0,0 +1,135 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { ReadingLog } from "@utils/storage";
|
||||||
|
import type { App } from "obsidian";
|
||||||
|
|
||||||
|
const ALL_TIME = "ALL_TIME";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
app: App;
|
||||||
|
readingLog: ReadingLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { app, readingLog }: Props = $props();
|
||||||
|
|
||||||
|
function bookUri(book: string) {
|
||||||
|
const v = encodeURIComponent(app.vault.getName());
|
||||||
|
const f = encodeURIComponent(book + ".md");
|
||||||
|
return `obsidian://open?vault=${v}&file=${f}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date: Date) {
|
||||||
|
// @ts-expect-error Moment is provided by Obsidian
|
||||||
|
return moment(date).format("YYYY-MM-DD");
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = readingLog.getEntries();
|
||||||
|
const years = [
|
||||||
|
...new Set(entries.map((entry) => entry.createdAt.getFullYear())),
|
||||||
|
];
|
||||||
|
|
||||||
|
let selectedYear = $state(new Date().getFullYear().toString());
|
||||||
|
const filterYear = $derived(
|
||||||
|
selectedYear === ALL_TIME ? ALL_TIME : parseInt(selectedYear, 10),
|
||||||
|
);
|
||||||
|
|
||||||
|
let selectedMonth = $state(
|
||||||
|
(new Date().getMonth() + 1).toLocaleString("en-US", {
|
||||||
|
minimumIntegerDigits: 2,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const filterMonth = $derived(
|
||||||
|
selectedMonth === "" ? undefined : parseInt(selectedMonth, 10),
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredEntries = $derived(
|
||||||
|
filterYear === ALL_TIME
|
||||||
|
? entries
|
||||||
|
: entries.filter(
|
||||||
|
(entry) =>
|
||||||
|
entry.createdAt.getFullYear() === filterYear &&
|
||||||
|
(filterMonth === undefined ||
|
||||||
|
entry.createdAt.getMonth() === filterMonth - 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="obt-reading-log-viewer">
|
||||||
|
<div class="filters">
|
||||||
|
<select class="year-filter" bind:value={selectedYear}>
|
||||||
|
{#each years as year}
|
||||||
|
<option value={year.toString()}>{year}</option>
|
||||||
|
{/each}
|
||||||
|
<option class="all-time" value={ALL_TIME}>All Time</option>
|
||||||
|
</select>
|
||||||
|
<select class="month-filter" bind:value={selectedMonth}>
|
||||||
|
<option value="">Select Month</option>
|
||||||
|
<option value="01">January</option>
|
||||||
|
<option value="02">February</option>
|
||||||
|
<option value="03">March</option>
|
||||||
|
<option value="04">April</option>
|
||||||
|
<option value="05">May</option>
|
||||||
|
<option value="06">June</option>
|
||||||
|
<option value="07">July</option>
|
||||||
|
<option value="08">August</option>
|
||||||
|
<option value="09">September</option>
|
||||||
|
<option value="10">October</option>
|
||||||
|
<option value="11">November</option>
|
||||||
|
<option value="12">December</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="date">Date</th>
|
||||||
|
<th class="book">Book</th>
|
||||||
|
<th class="pages-read">Pages Read</th>
|
||||||
|
<th class="percent-complete">Percent Complete</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each filteredEntries as entry}
|
||||||
|
<tr>
|
||||||
|
<td class="date">{formatDate(entry.createdAt)}</td>
|
||||||
|
<td class="book"
|
||||||
|
><a href={bookUri(entry.book)}>{entry.book}</a></td
|
||||||
|
>
|
||||||
|
<td class="pages-read">{entry.pagesRead}</td>
|
||||||
|
<td class="percent-complete">
|
||||||
|
{Math.round(
|
||||||
|
(entry.pagesReadTotal /
|
||||||
|
(entry.pagesReadTotal + entry.pagesRemaining)) *
|
||||||
|
100,
|
||||||
|
)}%
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.obt-reading-log-viewer {
|
||||||
|
.year-filter:has(> option.all-time:checked) + .month-filter {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
td.book {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td:not(.book) {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.pages-read,
|
||||||
|
td.percent-complete {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
18
src/main.ts
18
src/main.ts
|
@ -17,6 +17,8 @@ import {
|
||||||
import { ReadingLog, Storage } from "@utils/storage";
|
import { ReadingLog, Storage } from "@utils/storage";
|
||||||
import { ReadingProgressModal } from "@views/reading-progress-modal";
|
import { ReadingProgressModal } from "@views/reading-progress-modal";
|
||||||
import { RatingModal } from "@views/rating-modal";
|
import { RatingModal } from "@views/rating-modal";
|
||||||
|
import { renderCodeBlockProcessor } from "@utils/svelte";
|
||||||
|
import ReadingLogViewer from "@components/ReadingLogViewer.svelte";
|
||||||
|
|
||||||
export default class BookTrackerPlugin extends Plugin {
|
export default class BookTrackerPlugin extends Plugin {
|
||||||
settings: BookTrackerPluginSettings;
|
settings: BookTrackerPluginSettings;
|
||||||
|
@ -62,6 +64,14 @@ export default class BookTrackerPlugin extends Plugin {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addSettingTab(new BookTrackerSettingTab(this.app, this));
|
this.addSettingTab(new BookTrackerSettingTab(this.app, this));
|
||||||
|
|
||||||
|
this.registerMarkdownCodeBlockProcessor(
|
||||||
|
"readinglog",
|
||||||
|
renderCodeBlockProcessor(ReadingLogViewer, {
|
||||||
|
app: this.app,
|
||||||
|
readingLog: this.readingLog,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {}
|
onunload() {}
|
||||||
|
@ -252,7 +262,7 @@ export default class BookTrackerPlugin extends Plugin {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.readingLog.addEntry(fileName, pageNumber);
|
await this.readingLog.addEntry(fileName, pageNumber, pageLength);
|
||||||
new Notice(
|
new Notice(
|
||||||
`Logged reading progress for ${fileName}: Page ${pageNumber} of ${pageLength}.`
|
`Logged reading progress for ${fileName}: Page ${pageNumber} of ${pageLength}.`
|
||||||
);
|
);
|
||||||
|
@ -299,7 +309,11 @@ export default class BookTrackerPlugin extends Plugin {
|
||||||
|
|
||||||
const rating = await RatingModal.createAndOpen(this.app);
|
const rating = await RatingModal.createAndOpen(this.app);
|
||||||
|
|
||||||
await this.readingLog.addEntry(activeFile.basename, pageLength);
|
await this.readingLog.addEntry(
|
||||||
|
activeFile.basename,
|
||||||
|
pageLength,
|
||||||
|
pageLength
|
||||||
|
);
|
||||||
|
|
||||||
// @ts-expect-error Moment is provided by Obsidian
|
// @ts-expect-error Moment is provided by Obsidian
|
||||||
const endDate = moment().format("YYYY-MM-DD");
|
const endDate = moment().format("YYYY-MM-DD");
|
||||||
|
|
|
@ -37,6 +37,7 @@ interface ReadingLogEntry {
|
||||||
readonly book: string;
|
readonly book: string;
|
||||||
readonly pagesRead: number;
|
readonly pagesRead: number;
|
||||||
readonly pagesReadTotal: number;
|
readonly pagesReadTotal: number;
|
||||||
|
readonly pagesRemaining: number;
|
||||||
readonly createdAt: Date;
|
readonly createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +58,10 @@ export class ReadingLog {
|
||||||
"reading-log.json"
|
"reading-log.json"
|
||||||
);
|
);
|
||||||
if (entries) {
|
if (entries) {
|
||||||
this.entries = entries;
|
this.entries = entries.map((entry) => ({
|
||||||
|
...entry,
|
||||||
|
createdAt: new Date(entry.createdAt),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,6 +69,10 @@ export class ReadingLog {
|
||||||
await this.plugin.storage.writeJSON("reading-log.json", this.entries);
|
await this.plugin.storage.writeJSON("reading-log.json", this.entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getEntries(): ReadingLogEntry[] {
|
||||||
|
return this.entries;
|
||||||
|
}
|
||||||
|
|
||||||
public getLatestEntry(book: string): ReadingLogEntry | null {
|
public getLatestEntry(book: string): ReadingLogEntry | null {
|
||||||
const entriesForBook = this.entries.filter(
|
const entriesForBook = this.entries.filter(
|
||||||
(entry) => entry.book === book
|
(entry) => entry.book === book
|
||||||
|
@ -75,7 +83,11 @@ export class ReadingLog {
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addEntry(book: string, pageEnded: number): Promise<void> {
|
public async addEntry(
|
||||||
|
book: string,
|
||||||
|
pageEnded: number,
|
||||||
|
pageLength: number
|
||||||
|
): Promise<void> {
|
||||||
const latestEntry = this.getLatestEntry(book);
|
const latestEntry = this.getLatestEntry(book);
|
||||||
|
|
||||||
const newEntry: ReadingLogEntry = {
|
const newEntry: ReadingLogEntry = {
|
||||||
|
@ -84,6 +96,7 @@ export class ReadingLog {
|
||||||
? pageEnded - latestEntry.pagesReadTotal
|
? pageEnded - latestEntry.pagesReadTotal
|
||||||
: pageEnded,
|
: pageEnded,
|
||||||
pagesReadTotal: pageEnded,
|
pagesReadTotal: pageEnded,
|
||||||
|
pagesRemaining: pageLength - pageEnded,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import type { MarkdownPostProcessorContext } from "obsidian";
|
||||||
|
import { mount, unmount, type Component, type ComponentProps } from "svelte";
|
||||||
|
import { MarkdownRenderChild } from "obsidian";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a svelte component as a code block processor.
|
||||||
|
* @param component the svelte component to render.
|
||||||
|
* @param props properties forwarded to the component.
|
||||||
|
* @param stateProvider an optional provider that handles state & state updates of the code block processor.
|
||||||
|
*/
|
||||||
|
export function renderCodeBlockProcessor<C extends Component>(
|
||||||
|
component: C,
|
||||||
|
props: ComponentProps<C>
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
source: string,
|
||||||
|
containerEl: HTMLElement,
|
||||||
|
ctx: MarkdownPostProcessorContext
|
||||||
|
) => {
|
||||||
|
const svelteComponent = mount(component, {
|
||||||
|
target: containerEl,
|
||||||
|
props,
|
||||||
|
});
|
||||||
|
|
||||||
|
class UnloadSvelteComponent extends MarkdownRenderChild {
|
||||||
|
onunload() {
|
||||||
|
unmount(svelteComponent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.addChild(new UnloadSvelteComponent(containerEl));
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue