generated from tpl/obsidian-sample-plugin
			Add reading log store
This commit is contained in:
		
							parent
							
								
									eada608003
								
							
						
					
					
						commit
						d9cfb3df36
					
				| 
						 | 
				
			
			@ -45,6 +45,7 @@
 | 
			
		|||
	"dependencies": {
 | 
			
		||||
		"chart.js": "^4.5.0",
 | 
			
		||||
		"chroma-js": "^3.1.2",
 | 
			
		||||
		"uuid": "^11.1.0",
 | 
			
		||||
		"yaml": "^2.8.0",
 | 
			
		||||
		"zod": "^3.25.67"
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,9 @@ importers:
 | 
			
		|||
      chroma-js:
 | 
			
		||||
        specifier: ^3.1.2
 | 
			
		||||
        version: 3.1.2
 | 
			
		||||
      uuid:
 | 
			
		||||
        specifier: ^11.1.0
 | 
			
		||||
        version: 11.1.0
 | 
			
		||||
      yaml:
 | 
			
		||||
        specifier: ^2.8.0
 | 
			
		||||
        version: 2.8.0
 | 
			
		||||
| 
						 | 
				
			
			@ -1592,6 +1595,10 @@ packages:
 | 
			
		|||
  uri-js@4.4.1:
 | 
			
		||||
    resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
 | 
			
		||||
 | 
			
		||||
  uuid@11.1.0:
 | 
			
		||||
    resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
 | 
			
		||||
  validate-npm-package-license@3.0.4:
 | 
			
		||||
    resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -3189,6 +3196,8 @@ snapshots:
 | 
			
		|||
    dependencies:
 | 
			
		||||
      punycode: 2.3.1
 | 
			
		||||
 | 
			
		||||
  uuid@11.1.0: {}
 | 
			
		||||
 | 
			
		||||
  validate-npm-package-license@3.0.4:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      spdx-correct: 3.2.0
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,7 +58,7 @@ export class LogReadingFinishedCommand extends EditorCheckCommand {
 | 
			
		|||
		);
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			await this.readingLog.addEntry(fileName, pageCount, pageCount);
 | 
			
		||||
			await this.readingLog.createEntry(fileName, pageCount, pageCount);
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			new Notice(
 | 
			
		||||
				`Failed to log reading progress for ${fileName}: ${error}`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,7 +59,7 @@ export class LogReadingProgressCommand extends EditorCheckCommand {
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			await this.readingLog.addEntry(fileName, pageNumber, pageCount);
 | 
			
		||||
			await this.readingLog.createEntry(fileName, pageNumber, pageCount);
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			new Notice(
 | 
			
		||||
				`Failed to log reading progress for ${fileName}: ${error}`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,7 +43,7 @@ export class ResetReadingStatusCommand extends EditorCheckCommand {
 | 
			
		|||
			frontMatter[this.settings.endDateProperty] = "";
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.readingLog.removeEntries(file.basename);
 | 
			
		||||
		this.readingLog.removeEntriesForBook(file.basename);
 | 
			
		||||
 | 
			
		||||
		new Notice("Reading status reset for " + file.basename);
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,8 +3,9 @@
 | 
			
		|||
	import { Edit, Trash, Plus } from "lucide-svelte";
 | 
			
		||||
	import { ReadingLogEntryEditModal } from "@ui/modals";
 | 
			
		||||
	import type BookTrackerPlugin from "@src/main";
 | 
			
		||||
 | 
			
		||||
	const ALL_TIME = "ALL_TIME";
 | 
			
		||||
	import { createReadingLog } from "@ui/stores/reading-log.svelte";
 | 
			
		||||
	import { ALL_TIME } from "@ui/stores/date-filter.svelte";
 | 
			
		||||
	import { onDestroy } from "svelte";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		plugin: BookTrackerPlugin;
 | 
			
		||||
| 
						 | 
				
			
			@ -18,112 +19,60 @@
 | 
			
		|||
		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)
 | 
			
		||||
			);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
	const store = createReadingLog(plugin.readingLog);
 | 
			
		||||
	onDestroy(() => store.destroy());
 | 
			
		||||
 | 
			
		||||
	function createEntry() {
 | 
			
		||||
		const modal = new ReadingLogEntryEditModal(plugin, async (entry) => {
 | 
			
		||||
			modal.close();
 | 
			
		||||
			await plugin.readingLog.addRawEntry(entry);
 | 
			
		||||
			reload();
 | 
			
		||||
			await store.addEntry(entry);
 | 
			
		||||
		});
 | 
			
		||||
		modal.open();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function editEntry(i: number, entry: ReadingLogEntry) {
 | 
			
		||||
	function editEntry(entry: ReadingLogEntry) {
 | 
			
		||||
		const modal = new ReadingLogEntryEditModal(
 | 
			
		||||
			plugin,
 | 
			
		||||
			async (entry) => {
 | 
			
		||||
				modal.close();
 | 
			
		||||
				await plugin.readingLog.updateEntry(i, entry);
 | 
			
		||||
				reload();
 | 
			
		||||
				await store.updateEntry(entry);
 | 
			
		||||
			},
 | 
			
		||||
			entry,
 | 
			
		||||
		);
 | 
			
		||||
		modal.open();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async function deleteEntry(i: number) {
 | 
			
		||||
		await plugin.readingLog.spliceEntry(i);
 | 
			
		||||
		reload();
 | 
			
		||||
	async function removeEntry(entry: ReadingLogEntry) {
 | 
			
		||||
		await store.removeEntry(entry);
 | 
			
		||||
	}
 | 
			
		||||
</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>
 | 
			
		||||
			<select class="year-filter" bind:value={store.filterYear}>
 | 
			
		||||
				{#each store.filterYears as year}
 | 
			
		||||
					<option value={year}>{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>
 | 
			
		||||
				<option value={ALL_TIME}>All Time</option>
 | 
			
		||||
			</select>
 | 
			
		||||
			{#if store.filterYear !== ALL_TIME}
 | 
			
		||||
				<select class="month-filter" bind:value={store.filterMonth}>
 | 
			
		||||
					<option value={ALL_TIME}>Select Month</option>
 | 
			
		||||
					<option value={1}>January</option>
 | 
			
		||||
					<option value={2}>February</option>
 | 
			
		||||
					<option value={3}>March</option>
 | 
			
		||||
					<option value={4}>April</option>
 | 
			
		||||
					<option value={5}>May</option>
 | 
			
		||||
					<option value={6}>June</option>
 | 
			
		||||
					<option value={7}>July</option>
 | 
			
		||||
					<option value={8}>August</option>
 | 
			
		||||
					<option value={9}>September</option>
 | 
			
		||||
					<option value={10}>October</option>
 | 
			
		||||
					<option value={11}>November</option>
 | 
			
		||||
					<option value={12}>December</option>
 | 
			
		||||
				</select>
 | 
			
		||||
			{/if}
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="right">
 | 
			
		||||
			<button onclick={createEntry} class="create-entry" type="button">
 | 
			
		||||
| 
						 | 
				
			
			@ -144,7 +93,7 @@
 | 
			
		|||
			</tr>
 | 
			
		||||
		</thead>
 | 
			
		||||
		<tbody>
 | 
			
		||||
			{#each filteredEntries as entry}
 | 
			
		||||
			{#each store.entries as entry}
 | 
			
		||||
				<tr>
 | 
			
		||||
					<td class="date">{entry.createdAt.format("YYYY-MM-DD")}</td>
 | 
			
		||||
					<td class="book"
 | 
			
		||||
| 
						 | 
				
			
			@ -159,11 +108,11 @@
 | 
			
		|||
						)}%
 | 
			
		||||
					</td>
 | 
			
		||||
					<td class="actions">
 | 
			
		||||
						<button onclick={() => editEntry(entry.id, entry)}>
 | 
			
		||||
						<button onclick={() => editEntry(entry)}>
 | 
			
		||||
							<Edit />
 | 
			
		||||
							<span>Edit</span>
 | 
			
		||||
						</button>
 | 
			
		||||
						<button onclick={() => deleteEntry(entry.id)}>
 | 
			
		||||
						<button onclick={() => removeEntry(entry)}>
 | 
			
		||||
							<Trash />
 | 
			
		||||
							<span>Delete</span>
 | 
			
		||||
						</button>
 | 
			
		||||
| 
						 | 
				
			
			@ -171,7 +120,7 @@
 | 
			
		|||
				</tr>
 | 
			
		||||
			{:else}
 | 
			
		||||
				<tr>
 | 
			
		||||
					<td colspan="5">No entries found</td>
 | 
			
		||||
					<td colspan="5" class="no-entries">No entries found</td>
 | 
			
		||||
				</tr>
 | 
			
		||||
			{/each}
 | 
			
		||||
		</tbody>
 | 
			
		||||
| 
						 | 
				
			
			@ -204,10 +153,6 @@
 | 
			
		|||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.year-filter:has(> option.all-time:checked) + .month-filter {
 | 
			
		||||
			display: none;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		table {
 | 
			
		||||
			width: 100%;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -228,6 +173,14 @@
 | 
			
		|||
			td.actions span {
 | 
			
		||||
				@include utils.visually-hidden;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			td.no-entries {
 | 
			
		||||
				text-align: center;
 | 
			
		||||
				font-size: 1.5rem;
 | 
			
		||||
				font-weight: var(--bold-weight);
 | 
			
		||||
				font-style: italic;
 | 
			
		||||
				padding: var(--size-4-6);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,6 @@
 | 
			
		|||
	import CountStat from "@ui/components/stats/CountStat.svelte";
 | 
			
		||||
	import TotalStat from "@ui/components/stats/TotalStat.svelte";
 | 
			
		||||
	import {
 | 
			
		||||
		ALL_TIME,
 | 
			
		||||
		createMetadata,
 | 
			
		||||
		setMetadataContext,
 | 
			
		||||
	} from "@ui/stores/metadata.svelte";
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +22,7 @@
 | 
			
		|||
	} from "@ui/stores/settings.svelte";
 | 
			
		||||
	import type BookTrackerPlugin from "@src/main";
 | 
			
		||||
	import BookCountStat from "@ui/components/stats/BookCountStat.svelte";
 | 
			
		||||
	import { ALL_TIME } from "@ui/stores/date-filter.svelte";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		plugin: BookTrackerPlugin;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { chart } from "@ui/directives/chart";
 | 
			
		||||
	import { ALL_TIME, getMetadataContext } from "@ui/stores/metadata.svelte";
 | 
			
		||||
	import { ALL_TIME } from "@ui/stores/date-filter.svelte";
 | 
			
		||||
	import { getMetadataContext } from "@ui/stores/metadata.svelte";
 | 
			
		||||
	import { getSettingsContext } from "@ui/stores/settings.svelte";
 | 
			
		||||
	import { Color } from "@utils/color";
 | 
			
		||||
	import type { ChartConfiguration } from "chart.js";
 | 
			
		||||
| 
						 | 
				
			
			@ -34,9 +35,11 @@
 | 
			
		|||
 | 
			
		||||
		const labels = Array.from(books.keys())
 | 
			
		||||
			.sort((a, b) => a - b)
 | 
			
		||||
			.map((m) =>
 | 
			
		||||
				// @ts-expect-error Moment is provided by Obsidian
 | 
			
		||||
				moment().month(m).format("MMM"),
 | 
			
		||||
			.map((key) =>
 | 
			
		||||
				store.filterYear === ALL_TIME
 | 
			
		||||
					? key
 | 
			
		||||
					: // @ts-expect-error Moment is provided by Obsidian
 | 
			
		||||
						moment().month(key).format("MMM"),
 | 
			
		||||
			);
 | 
			
		||||
		const sortedBooks = Array.from(books.entries())
 | 
			
		||||
			.sort((a, b) => a[0] - b[0])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,7 +26,7 @@
 | 
			
		|||
 | 
			
		||||
		.label {
 | 
			
		||||
			text-transform: capitalize;
 | 
			
		||||
			font-weight: 600;
 | 
			
		||||
			font-weight: var(--bold-weight);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
	import type BookTrackerPlugin from "@src/main";
 | 
			
		||||
	import type { ReadingLogEntry } from "@utils/ReadingLog";
 | 
			
		||||
	import FileSuggest from "@ui/components/suggesters/FileSuggest.svelte";
 | 
			
		||||
	import { v4 as uuidv4 } from "uuid";
 | 
			
		||||
 | 
			
		||||
	interface Props {
 | 
			
		||||
		plugin: BookTrackerPlugin;
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +17,7 @@
 | 
			
		|||
	let pagesRead = $state(entry?.pagesRead ?? 0);
 | 
			
		||||
	let pagesReadTotal = $state(entry?.pagesReadTotal ?? 0);
 | 
			
		||||
	let pagesRemaining = $state(entry?.pagesRemaining ?? 0);
 | 
			
		||||
	let createdAt = $state(entry?.createdAt ?? new Date());
 | 
			
		||||
	let createdAt = $state(entry?.createdAt?.toDate() ?? new Date());
 | 
			
		||||
 | 
			
		||||
	// Source: https://github.com/sveltejs/svelte/discussions/14220#discussioncomment-11188219
 | 
			
		||||
	function watch<T>(
 | 
			
		||||
| 
						 | 
				
			
			@ -48,11 +49,13 @@
 | 
			
		|||
	function onsubmit(ev: SubmitEvent) {
 | 
			
		||||
		ev.preventDefault();
 | 
			
		||||
		onSubmit?.({
 | 
			
		||||
			id: entry?.id ?? uuidv4(),
 | 
			
		||||
			book,
 | 
			
		||||
			pagesRead,
 | 
			
		||||
			pagesReadTotal,
 | 
			
		||||
			pagesRemaining,
 | 
			
		||||
			createdAt: new Date(createdAt),
 | 
			
		||||
			// @ts-expect-error Moment is provided by Obsidian
 | 
			
		||||
			createdAt: moment(createdAt),
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,97 @@
 | 
			
		|||
import type { Moment } from "moment";
 | 
			
		||||
 | 
			
		||||
export const ALL_TIME = "ALL_TIME";
 | 
			
		||||
 | 
			
		||||
export interface DateFilterStore {
 | 
			
		||||
	filterYear: number | typeof ALL_TIME;
 | 
			
		||||
	get filterYears(): number[];
 | 
			
		||||
	filterMonth: number | typeof ALL_TIME;
 | 
			
		||||
	get filterMonths(): { label: string; value: number }[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createDateFilter<T>(
 | 
			
		||||
	data: () => T[],
 | 
			
		||||
	selector: (item: T) => Moment,
 | 
			
		||||
	initialMonth?: boolean
 | 
			
		||||
): DateFilterStore & {
 | 
			
		||||
	filteredData: T[];
 | 
			
		||||
} {
 | 
			
		||||
	const today = new Date();
 | 
			
		||||
	let filterYear: number | typeof ALL_TIME = $state(today.getFullYear());
 | 
			
		||||
	let filterMonth: number | typeof ALL_TIME = $state(
 | 
			
		||||
		initialMonth ? today.getMonth() + 1 : ALL_TIME
 | 
			
		||||
	);
 | 
			
		||||
	const filteredData = $derived.by(() => {
 | 
			
		||||
		return data().filter((item) => {
 | 
			
		||||
			const date = selector(item);
 | 
			
		||||
			if (filterYear !== ALL_TIME) {
 | 
			
		||||
				if (date.year() !== filterYear) {
 | 
			
		||||
					return false;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (filterMonth !== ALL_TIME) {
 | 
			
		||||
				if (date.month() !== filterMonth - 1) {
 | 
			
		||||
					return false;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			return true;
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const filterYears = $derived.by(() => {
 | 
			
		||||
		const years = new Set<number>();
 | 
			
		||||
		data().forEach((item) => {
 | 
			
		||||
			years.add(selector(item).year());
 | 
			
		||||
		});
 | 
			
		||||
		return Array.from(years).sort((a, b) => a - b);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const filterMonths = $derived.by(() => {
 | 
			
		||||
		if (filterYear === ALL_TIME) {
 | 
			
		||||
			return [];
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const months: { label: string; value: number }[] = [];
 | 
			
		||||
		data().forEach((item) => {
 | 
			
		||||
			const date = selector(item);
 | 
			
		||||
			if (date.year() === filterYear) {
 | 
			
		||||
				months.push({
 | 
			
		||||
					label: date.format("MMMM"),
 | 
			
		||||
					value: date.month() + 1,
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return months
 | 
			
		||||
			.filter(
 | 
			
		||||
				(month, idx, self) =>
 | 
			
		||||
					idx === self.findIndex((m) => m.value === month.value)
 | 
			
		||||
			)
 | 
			
		||||
			.sort((a, b) => a.value - b.value);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
		get filterYear() {
 | 
			
		||||
			return filterYear;
 | 
			
		||||
		},
 | 
			
		||||
		set filterYear(value) {
 | 
			
		||||
			filterYear = value;
 | 
			
		||||
		},
 | 
			
		||||
		get filterYears() {
 | 
			
		||||
			return filterYears;
 | 
			
		||||
		},
 | 
			
		||||
		get filterMonth() {
 | 
			
		||||
			return filterMonth;
 | 
			
		||||
		},
 | 
			
		||||
		set filterMonth(value) {
 | 
			
		||||
			filterMonth = value;
 | 
			
		||||
		},
 | 
			
		||||
		get filterMonths() {
 | 
			
		||||
			return filterMonths;
 | 
			
		||||
		},
 | 
			
		||||
		get filteredData() {
 | 
			
		||||
			return filteredData;
 | 
			
		||||
		},
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ import type { CachedMetadata, TFile } from "obsidian";
 | 
			
		|||
import { getContext, setContext } from "svelte";
 | 
			
		||||
import { getSettingsContext } from "./settings.svelte";
 | 
			
		||||
import type BookTrackerPlugin from "@src/main";
 | 
			
		||||
import { createDateFilter, type DateFilterStore } from "./date-filter.svelte";
 | 
			
		||||
 | 
			
		||||
export type FileMetadata = {
 | 
			
		||||
	file: TFile;
 | 
			
		||||
| 
						 | 
				
			
			@ -14,14 +15,8 @@ export type FileProperty = {
 | 
			
		|||
	value: any;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ALL_TIME = "ALL_TIME";
 | 
			
		||||
 | 
			
		||||
interface MetadataStore {
 | 
			
		||||
interface MetadataStore extends DateFilterStore {
 | 
			
		||||
	get metadata(): FileMetadata[];
 | 
			
		||||
	filterYear: number | typeof ALL_TIME;
 | 
			
		||||
	filterMonth: number | typeof ALL_TIME;
 | 
			
		||||
	get filterYears(): number[];
 | 
			
		||||
	get filterMonths(): { label: string; value: number }[];
 | 
			
		||||
 | 
			
		||||
	destroy(): void;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -41,68 +36,7 @@ export function createMetadata(plugin: BookTrackerPlugin): MetadataStore {
 | 
			
		|||
		initialMetadata.push({ file, frontmatter });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const thisYear = new Date().getFullYear();
 | 
			
		||||
 | 
			
		||||
	let metadata = $state(initialMetadata);
 | 
			
		||||
	let filterYear: number | typeof ALL_TIME = $state(thisYear);
 | 
			
		||||
	let filterMonth: number | typeof ALL_TIME = $state(ALL_TIME);
 | 
			
		||||
	const filteredMetadata = $derived.by(() => {
 | 
			
		||||
		console.log("here");
 | 
			
		||||
		return metadata.filter((f) => {
 | 
			
		||||
			// @ts-expect-error Moment is provided by Obsidian
 | 
			
		||||
			const endDate = moment(f.frontmatter[settings.endDateProperty]);
 | 
			
		||||
			if (filterYear !== ALL_TIME) {
 | 
			
		||||
				if (endDate.year() !== filterYear) {
 | 
			
		||||
					return false;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (filterMonth !== ALL_TIME) {
 | 
			
		||||
				if (endDate.month() !== filterMonth - 1) {
 | 
			
		||||
					return false;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			return true;
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const filterYears = $derived.by(() => {
 | 
			
		||||
		const years = new Set<number>();
 | 
			
		||||
		for (const f of metadata) {
 | 
			
		||||
			// @ts-expect-error Moment is provided by Obsidian
 | 
			
		||||
			const endDate = moment(f.frontmatter[settings.endDateProperty]);
 | 
			
		||||
			years.add(endDate.year());
 | 
			
		||||
		}
 | 
			
		||||
		return Array.from(years).sort((a, b) => a - b);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const filterMonths = $derived.by(() => {
 | 
			
		||||
		if (filterYear === ALL_TIME) {
 | 
			
		||||
			return [];
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const months = [];
 | 
			
		||||
		for (const f of metadata) {
 | 
			
		||||
			// @ts-expect-error Moment is provided by Obsidian
 | 
			
		||||
			const endDate = moment(f.frontmatter[settings.endDateProperty]);
 | 
			
		||||
 | 
			
		||||
			if (endDate.year() !== filterYear) {
 | 
			
		||||
				continue;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			months.push({
 | 
			
		||||
				label: endDate.format("MMMM"),
 | 
			
		||||
				value: endDate.month() + 1,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return months
 | 
			
		||||
			.filter(
 | 
			
		||||
				(month, idx, self) =>
 | 
			
		||||
					idx === self.findIndex((m) => m.value === month.value)
 | 
			
		||||
			)
 | 
			
		||||
			.sort((a, b) => a.value - b.value);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	function onChanged(file: TFile, _data: string, cache: CachedMetadata) {
 | 
			
		||||
		metadata = metadata.map((f) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -123,27 +57,35 @@ export function createMetadata(plugin: BookTrackerPlugin): MetadataStore {
 | 
			
		|||
	}
 | 
			
		||||
	plugin.registerEvent(plugin.app.metadataCache.on("deleted", onDeleted));
 | 
			
		||||
 | 
			
		||||
	const dateFilter = createDateFilter(
 | 
			
		||||
		() => metadata,
 | 
			
		||||
		(f) => {
 | 
			
		||||
			// @ts-expect-error Moment is provided by Obsidian
 | 
			
		||||
			return moment(f.frontmatter[settings.endDateProperty]);
 | 
			
		||||
		}
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
		get metadata() {
 | 
			
		||||
			return filteredMetadata;
 | 
			
		||||
			return dateFilter.filteredData;
 | 
			
		||||
		},
 | 
			
		||||
		get filterYear() {
 | 
			
		||||
			return filterYear;
 | 
			
		||||
			return dateFilter.filterYear;
 | 
			
		||||
		},
 | 
			
		||||
		set filterYear(value) {
 | 
			
		||||
			filterYear = value;
 | 
			
		||||
			dateFilter.filterYear = value;
 | 
			
		||||
		},
 | 
			
		||||
		get filterMonth() {
 | 
			
		||||
			return filterMonth;
 | 
			
		||||
			return dateFilter.filterMonth;
 | 
			
		||||
		},
 | 
			
		||||
		set filterMonth(value) {
 | 
			
		||||
			filterMonth = value;
 | 
			
		||||
			dateFilter.filterMonth = value;
 | 
			
		||||
		},
 | 
			
		||||
		get filterYears() {
 | 
			
		||||
			return filterYears;
 | 
			
		||||
			return dateFilter.filterYears;
 | 
			
		||||
		},
 | 
			
		||||
		get filterMonths() {
 | 
			
		||||
			return filterMonths;
 | 
			
		||||
			return dateFilter.filterMonths;
 | 
			
		||||
		},
 | 
			
		||||
		destroy() {
 | 
			
		||||
			plugin.app.metadataCache.off("changed", onChanged);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,111 @@
 | 
			
		|||
import type { ReadingLog, ReadingLogEntry } from "@utils/ReadingLog";
 | 
			
		||||
import { getContext, setContext } from "svelte";
 | 
			
		||||
import { createDateFilter, type DateFilterStore } from "./date-filter.svelte";
 | 
			
		||||
 | 
			
		||||
export interface ReadingLogStore extends DateFilterStore {
 | 
			
		||||
	get entries(): ReadingLogEntry[];
 | 
			
		||||
 | 
			
		||||
	addEntry(entry: ReadingLogEntry): Promise<void>;
 | 
			
		||||
	updateEntry(entry: ReadingLogEntry): Promise<void>;
 | 
			
		||||
	removeEntry(entry: ReadingLogEntry): Promise<void>;
 | 
			
		||||
	destroy(): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createReadingLog(readingLog: ReadingLog): ReadingLogStore {
 | 
			
		||||
	let entries: ReadingLogEntry[] = $state(readingLog.getEntries());
 | 
			
		||||
 | 
			
		||||
	const dateFilter = createDateFilter(
 | 
			
		||||
		() => entries,
 | 
			
		||||
		(entry) => entry.createdAt,
 | 
			
		||||
		true
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	async function addEntry(entry: ReadingLogEntry): Promise<void> {
 | 
			
		||||
		await readingLog.createEntry(
 | 
			
		||||
			entry.book,
 | 
			
		||||
			entry.pagesRead,
 | 
			
		||||
			entry.pagesReadTotal
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async function updateEntry(entry: ReadingLogEntry): Promise<void> {
 | 
			
		||||
		await readingLog.updateEntry(entry);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async function removeEntry(entry: ReadingLogEntry): Promise<void> {
 | 
			
		||||
		await readingLog.removeEntry(entry);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const loadHandler = readingLog.on("load", (payload) => {
 | 
			
		||||
		entries = payload.entries;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const createdHandler = readingLog.on("created", (payload) => {
 | 
			
		||||
		entries.push(payload.entry);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const updatedHandler = readingLog.on("updated", (payload) => {
 | 
			
		||||
		const index = entries.findIndex(
 | 
			
		||||
			(entry) => entry.id === payload.entry.id
 | 
			
		||||
		);
 | 
			
		||||
		if (index !== -1) {
 | 
			
		||||
			entries[index] = payload.entry;
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const removedHandler = readingLog.on("removed", (payload) => {
 | 
			
		||||
		const index = entries.findIndex(
 | 
			
		||||
			(entry) => entry.id === payload.entry.id
 | 
			
		||||
		);
 | 
			
		||||
		if (index !== -1) {
 | 
			
		||||
			entries.splice(index, 1);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const sortedData = $derived(
 | 
			
		||||
		dateFilter.filteredData.sort((a, b) => a.createdAt.diff(b.createdAt))
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
		get entries() {
 | 
			
		||||
			return sortedData;
 | 
			
		||||
		},
 | 
			
		||||
		get filterYear() {
 | 
			
		||||
			return dateFilter.filterYear;
 | 
			
		||||
		},
 | 
			
		||||
		set filterYear(value) {
 | 
			
		||||
			dateFilter.filterYear = value;
 | 
			
		||||
		},
 | 
			
		||||
		get filterMonth() {
 | 
			
		||||
			return dateFilter.filterMonth;
 | 
			
		||||
		},
 | 
			
		||||
		set filterMonth(value) {
 | 
			
		||||
			dateFilter.filterMonth = value;
 | 
			
		||||
		},
 | 
			
		||||
		get filterYears() {
 | 
			
		||||
			return dateFilter.filterYears;
 | 
			
		||||
		},
 | 
			
		||||
		get filterMonths() {
 | 
			
		||||
			return dateFilter.filterMonths;
 | 
			
		||||
		},
 | 
			
		||||
		addEntry,
 | 
			
		||||
		updateEntry,
 | 
			
		||||
		removeEntry,
 | 
			
		||||
		destroy() {
 | 
			
		||||
			loadHandler.off();
 | 
			
		||||
			createdHandler.off();
 | 
			
		||||
			updatedHandler.off();
 | 
			
		||||
			removedHandler.off();
 | 
			
		||||
		},
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const READING_LOG_KEY = Symbol("readingLog");
 | 
			
		||||
 | 
			
		||||
export function setReadingLogContext(state: ReadingLogStore) {
 | 
			
		||||
	setContext(READING_LOG_KEY, state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getReadingLogContext(): ReadingLogStore {
 | 
			
		||||
	return getContext(READING_LOG_KEY);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,10 @@
 | 
			
		|||
import type { Storage } from "./Storage";
 | 
			
		||||
import type { Moment } from "moment";
 | 
			
		||||
import { v4 as uuidv4 } from "uuid";
 | 
			
		||||
import { EventEmitter } from "./event";
 | 
			
		||||
 | 
			
		||||
export interface ReadingLogEntry {
 | 
			
		||||
	id: string;
 | 
			
		||||
	book: string;
 | 
			
		||||
	pagesRead: number;
 | 
			
		||||
	pagesReadTotal: number;
 | 
			
		||||
| 
						 | 
				
			
			@ -9,10 +12,19 @@ export interface ReadingLogEntry {
 | 
			
		|||
	createdAt: Moment;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ReadingLog {
 | 
			
		||||
interface ReadingLogEventMap {
 | 
			
		||||
	load: { entries: ReadingLogEntry[] };
 | 
			
		||||
	created: { entry: ReadingLogEntry };
 | 
			
		||||
	updated: { entry: ReadingLogEntry };
 | 
			
		||||
	removed: { entry: ReadingLogEntry };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ReadingLog extends EventEmitter<ReadingLogEventMap> {
 | 
			
		||||
	private entries: ReadingLogEntry[] = [];
 | 
			
		||||
 | 
			
		||||
	public constructor(private readonly storage: Storage) {
 | 
			
		||||
		super();
 | 
			
		||||
 | 
			
		||||
		this.load().catch((error) => {
 | 
			
		||||
			console.error("Failed to load reading log entries:", error);
 | 
			
		||||
		});
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +40,7 @@ export class ReadingLog {
 | 
			
		|||
				// @ts-expect-error Moment is provided by Obsidian
 | 
			
		||||
				createdAt: moment(entry.createdAt),
 | 
			
		||||
			}));
 | 
			
		||||
			this.emit("load", { entries: this.entries });
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +66,7 @@ export class ReadingLog {
 | 
			
		|||
		return this.entries;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public getLastEntry(book: string): ReadingLogEntry | null {
 | 
			
		||||
	public getLastEntryForBook(book: string): ReadingLogEntry | null {
 | 
			
		||||
		const entriesForBook = this.entries.filter(
 | 
			
		||||
			(entry) => entry.book === book
 | 
			
		||||
		);
 | 
			
		||||
| 
						 | 
				
			
			@ -63,12 +76,12 @@ export class ReadingLog {
 | 
			
		|||
			: null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public async addEntry(
 | 
			
		||||
	public async createEntry(
 | 
			
		||||
		book: string,
 | 
			
		||||
		pageEnded: number,
 | 
			
		||||
		pageCount: number
 | 
			
		||||
	): Promise<void> {
 | 
			
		||||
		const lastEntry = this.getLastEntry(book);
 | 
			
		||||
		const lastEntry = this.getLastEntryForBook(book);
 | 
			
		||||
 | 
			
		||||
		if (lastEntry && lastEntry.pagesReadTotal >= pageEnded) {
 | 
			
		||||
			throw new Error(
 | 
			
		||||
| 
						 | 
				
			
			@ -77,6 +90,7 @@ export class ReadingLog {
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		const newEntry: ReadingLogEntry = {
 | 
			
		||||
			id: uuidv4(),
 | 
			
		||||
			book,
 | 
			
		||||
			pagesRead: lastEntry
 | 
			
		||||
				? pageEnded - lastEntry.pagesReadTotal
 | 
			
		||||
| 
						 | 
				
			
			@ -91,8 +105,9 @@ export class ReadingLog {
 | 
			
		|||
			lastEntry &&
 | 
			
		||||
			lastEntry.createdAt.isSame(newEntry.createdAt, "day")
 | 
			
		||||
		) {
 | 
			
		||||
			newEntry.id = lastEntry.id;
 | 
			
		||||
			newEntry.pagesRead += lastEntry.pagesRead;
 | 
			
		||||
			await this.updateEntry(this.entries.indexOf(lastEntry), newEntry);
 | 
			
		||||
			await this.updateEntry(newEntry);
 | 
			
		||||
		} else {
 | 
			
		||||
			await this.addRawEntry(newEntry);
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -101,35 +116,31 @@ export class ReadingLog {
 | 
			
		|||
	public async addRawEntry(entry: ReadingLogEntry) {
 | 
			
		||||
		this.entries.push(entry);
 | 
			
		||||
		await this.save();
 | 
			
		||||
		this.emit("created", { entry });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public async removeEntries(book: string): Promise<void> {
 | 
			
		||||
		this.entries = this.entries.filter((entry) => entry.book !== book);
 | 
			
		||||
	public async updateEntry(entry: ReadingLogEntry): Promise<void> {
 | 
			
		||||
		this.entries[this.entries.findIndex((other) => other.id === entry.id)] =
 | 
			
		||||
			entry;
 | 
			
		||||
		await this.save();
 | 
			
		||||
		this.emit("updated", { entry });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public async removeLastEntry(book: string): Promise<void> {
 | 
			
		||||
		const latestEntryIndex = this.entries.findLastIndex(
 | 
			
		||||
			(entry) => entry.book === book
 | 
			
		||||
		);
 | 
			
		||||
	public async removeEntry(entry: ReadingLogEntry): Promise<void> {
 | 
			
		||||
		const index = this.entries.findIndex((other) => other.id === entry.id);
 | 
			
		||||
 | 
			
		||||
		if (latestEntryIndex !== -1) {
 | 
			
		||||
			this.entries.splice(latestEntryIndex, 1);
 | 
			
		||||
		if (index !== -1) {
 | 
			
		||||
			const removed = this.entries.splice(index, 1);
 | 
			
		||||
			await this.save();
 | 
			
		||||
			this.emit("removed", { entry: removed[0] });
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public async updateEntry(i: number, entry: ReadingLogEntry): Promise<void> {
 | 
			
		||||
		this.entries[i] = entry;
 | 
			
		||||
	public async removeEntriesForBook(book: string): Promise<void> {
 | 
			
		||||
		this.entries = this.entries.filter((entry) => entry.book !== book);
 | 
			
		||||
		await this.save();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public async spliceEntry(i: number): Promise<ReadingLogEntry> {
 | 
			
		||||
		const entry = this.entries.splice(i, 1);
 | 
			
		||||
		await this.save();
 | 
			
		||||
		return entry[0];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public async clearEntries(): Promise<void> {
 | 
			
		||||
		this.entries = [];
 | 
			
		||||
		await this.save();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,59 @@
 | 
			
		|||
export type EventHandler<T> = (payload: T) => void;
 | 
			
		||||
 | 
			
		||||
export type ListenerRef = {
 | 
			
		||||
	off(): void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type EventHandlerMap<TEventMap, T extends keyof TEventMap> = Record<
 | 
			
		||||
	T,
 | 
			
		||||
	EventHandler<TEventMap[T]>[]
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export class EventEmitter<TEventMap> {
 | 
			
		||||
	private readonly listeners: EventHandlerMap<TEventMap, keyof TEventMap> =
 | 
			
		||||
		{} as EventHandlerMap<TEventMap, keyof TEventMap>;
 | 
			
		||||
 | 
			
		||||
	public on<T extends keyof TEventMap>(
 | 
			
		||||
		type: T,
 | 
			
		||||
		handler: EventHandler<TEventMap[T]>
 | 
			
		||||
	): ListenerRef {
 | 
			
		||||
		if (!this.listeners[type]) {
 | 
			
		||||
			this.listeners[type] = [];
 | 
			
		||||
		}
 | 
			
		||||
		this.listeners[type].push(handler);
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			off() {
 | 
			
		||||
				this.off(type, handler);
 | 
			
		||||
			},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public off<T extends keyof TEventMap>(
 | 
			
		||||
		type: T,
 | 
			
		||||
		handler: EventHandler<TEventMap[T]>
 | 
			
		||||
	) {
 | 
			
		||||
		if (this.listeners[type]) {
 | 
			
		||||
			this.listeners[type] = this.listeners[type].filter(
 | 
			
		||||
				(h) => h !== handler
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public once<T extends keyof TEventMap>(
 | 
			
		||||
		type: T,
 | 
			
		||||
		handler: EventHandler<TEventMap[T]>
 | 
			
		||||
	) {
 | 
			
		||||
		const wrappedHandler = (payload: TEventMap[T]) => {
 | 
			
		||||
			handler(payload);
 | 
			
		||||
			this.off(type, wrappedHandler);
 | 
			
		||||
		};
 | 
			
		||||
		this.on(type, wrappedHandler);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public emit<T extends keyof TEventMap>(type: T, payload: TEventMap[T]) {
 | 
			
		||||
		if (this.listeners[type]) {
 | 
			
		||||
			this.listeners[type].forEach((handler) => handler(payload));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue