generated from tpl/obsidian-sample-plugin
234 lines
5.3 KiB
Svelte
234 lines
5.3 KiB
Svelte
<script lang="ts">
|
|
import type { ReadingLogEntry } from "@utils/ReadingLog";
|
|
import { Edit, Trash, Plus } from "lucide-svelte";
|
|
import { ReadingLogEntryEditModal } from "@ui/modals";
|
|
import type BookTrackerPlugin from "@src/main";
|
|
|
|
const ALL_TIME = "ALL_TIME";
|
|
|
|
interface Props {
|
|
plugin: BookTrackerPlugin;
|
|
}
|
|
|
|
let { plugin }: Props = $props();
|
|
|
|
function bookUri(book: string) {
|
|
const v = encodeURIComponent(plugin.app.vault.getName());
|
|
const f = encodeURIComponent(book + ".md");
|
|
return `obsidian://open?vault=${v}&file=${f}`;
|
|
}
|
|
|
|
let entries = $state(
|
|
plugin.readingLog.getEntries().map((entry, id) => ({
|
|
...entry,
|
|
id,
|
|
})),
|
|
);
|
|
|
|
function reload() {
|
|
entries = plugin.readingLog.getEntries().map((entry, id) => ({
|
|
...entry,
|
|
id,
|
|
}));
|
|
}
|
|
|
|
const years = $derived([
|
|
...new Set(entries.map((entry) => entry.createdAt.year())),
|
|
]);
|
|
|
|
// @ts-expect-error Moment is provided by Obsidian
|
|
let selectedYear = $state(moment().year().toString());
|
|
const filterYear = $derived(
|
|
selectedYear === ALL_TIME ? ALL_TIME : parseInt(selectedYear, 10),
|
|
);
|
|
|
|
// @ts-expect-error Moment is provided by Obsidian
|
|
let selectedMonth = $state(moment().format("MM"));
|
|
const filterMonth = $derived(
|
|
selectedMonth === "" ? undefined : parseInt(selectedMonth, 10),
|
|
);
|
|
|
|
const filteredEntries = $derived.by(() => {
|
|
if (filterYear === ALL_TIME) {
|
|
return entries;
|
|
}
|
|
|
|
// @ts-expect-error Moment is provided by Obsidian
|
|
let startDate = moment().year(filterYear).startOf("year");
|
|
|
|
// @ts-expect-error Moment is provided by Obsidian
|
|
let endDate = moment().year(filterYear).endOf("year");
|
|
|
|
if (filterMonth !== undefined) {
|
|
startDate = startDate.month(filterMonth - 1).startOf("month");
|
|
endDate = endDate.month(filterMonth - 1).endOf("month");
|
|
}
|
|
|
|
return entries.filter((entry) => {
|
|
return (
|
|
entry.createdAt.isSameOrAfter(startDate) &&
|
|
entry.createdAt.isSameOrBefore(endDate)
|
|
);
|
|
});
|
|
});
|
|
|
|
function createEntry() {
|
|
const modal = new ReadingLogEntryEditModal(plugin, async (entry) => {
|
|
modal.close();
|
|
await plugin.readingLog.addRawEntry(entry);
|
|
reload();
|
|
});
|
|
modal.open();
|
|
}
|
|
|
|
function editEntry(i: number, entry: ReadingLogEntry) {
|
|
const modal = new ReadingLogEntryEditModal(
|
|
plugin,
|
|
async (entry) => {
|
|
modal.close();
|
|
await plugin.readingLog.updateEntry(i, entry);
|
|
reload();
|
|
},
|
|
entry,
|
|
);
|
|
modal.open();
|
|
}
|
|
|
|
async function deleteEntry(i: number) {
|
|
await plugin.readingLog.spliceEntry(i);
|
|
reload();
|
|
}
|
|
</script>
|
|
|
|
<div class="obt-reading-log-viewer">
|
|
<div class="controls">
|
|
<div class="left">
|
|
<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>
|
|
<div class="right">
|
|
<button onclick={createEntry} class="create-entry" type="button">
|
|
<Plus />
|
|
Create
|
|
</button>
|
|
</div>
|
|
</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>
|
|
<th class="actions">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each filteredEntries as entry}
|
|
<tr>
|
|
<td class="date">{entry.createdAt.format("YYYY-MM-DD")}</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>
|
|
<td class="actions">
|
|
<button onclick={() => editEntry(entry.id, entry)}>
|
|
<Edit />
|
|
<span>Edit</span>
|
|
</button>
|
|
<button onclick={() => deleteEntry(entry.id)}>
|
|
<Trash />
|
|
<span>Delete</span>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{:else}
|
|
<tr>
|
|
<td colspan="5">No entries found</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- svelte-ignore css_unused_selector -->
|
|
<style lang="scss">
|
|
@use "../styles/utils";
|
|
|
|
.obt-reading-log-viewer {
|
|
.controls {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
align-items: center;
|
|
|
|
.right {
|
|
text-align: right;
|
|
|
|
button.create-entry {
|
|
display: inline-flex;
|
|
gap: var(--size-2-2);
|
|
align-items: center;
|
|
|
|
:global(svg) {
|
|
width: var(--icon-s);
|
|
height: var(--icon-s);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
td.actions span {
|
|
@include utils.visually-hidden;
|
|
}
|
|
}
|
|
}
|
|
</style>
|