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 { ReadingProgressModal } from "@views/reading-progress-modal";
|
||||
import { RatingModal } from "@views/rating-modal";
|
||||
import { renderCodeBlockProcessor } from "@utils/svelte";
|
||||
import ReadingLogViewer from "@components/ReadingLogViewer.svelte";
|
||||
|
||||
export default class BookTrackerPlugin extends Plugin {
|
||||
settings: BookTrackerPluginSettings;
|
||||
|
@ -62,6 +64,14 @@ export default class BookTrackerPlugin extends Plugin {
|
|||
});
|
||||
|
||||
this.addSettingTab(new BookTrackerSettingTab(this.app, this));
|
||||
|
||||
this.registerMarkdownCodeBlockProcessor(
|
||||
"readinglog",
|
||||
renderCodeBlockProcessor(ReadingLogViewer, {
|
||||
app: this.app,
|
||||
readingLog: this.readingLog,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onunload() {}
|
||||
|
@ -252,7 +262,7 @@ export default class BookTrackerPlugin extends Plugin {
|
|||
return;
|
||||
}
|
||||
|
||||
await this.readingLog.addEntry(fileName, pageNumber);
|
||||
await this.readingLog.addEntry(fileName, pageNumber, pageLength);
|
||||
new Notice(
|
||||
`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);
|
||||
|
||||
await this.readingLog.addEntry(activeFile.basename, pageLength);
|
||||
await this.readingLog.addEntry(
|
||||
activeFile.basename,
|
||||
pageLength,
|
||||
pageLength
|
||||
);
|
||||
|
||||
// @ts-expect-error Moment is provided by Obsidian
|
||||
const endDate = moment().format("YYYY-MM-DD");
|
||||
|
|
|
@ -37,6 +37,7 @@ interface ReadingLogEntry {
|
|||
readonly book: string;
|
||||
readonly pagesRead: number;
|
||||
readonly pagesReadTotal: number;
|
||||
readonly pagesRemaining: number;
|
||||
readonly createdAt: Date;
|
||||
}
|
||||
|
||||
|
@ -57,7 +58,10 @@ export class ReadingLog {
|
|||
"reading-log.json"
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
public getEntries(): ReadingLogEntry[] {
|
||||
return this.entries;
|
||||
}
|
||||
|
||||
public getLatestEntry(book: string): ReadingLogEntry | null {
|
||||
const entriesForBook = this.entries.filter(
|
||||
(entry) => entry.book === book
|
||||
|
@ -75,7 +83,11 @@ export class ReadingLog {
|
|||
: 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 newEntry: ReadingLogEntry = {
|
||||
|
@ -84,6 +96,7 @@ export class ReadingLog {
|
|||
? pageEnded - latestEntry.pagesReadTotal
|
||||
: pageEnded,
|
||||
pagesReadTotal: pageEnded,
|
||||
pagesRemaining: pageLength - pageEnded,
|
||||
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