Add actions to reading log viewer

This commit is contained in:
Evan Fiordeliso 2025-06-29 08:11:52 -04:00
parent a2767f4595
commit 8ebed95fda
16 changed files with 566 additions and 959 deletions

View File

@ -3,6 +3,14 @@ import process from "process";
import builtins from "builtin-modules"; import builtins from "builtin-modules";
import esbuildSvelte from "esbuild-svelte"; import esbuildSvelte from "esbuild-svelte";
import { sveltePreprocess } from "svelte-preprocess"; import { sveltePreprocess } from "svelte-preprocess";
import dotenv from "dotenv";
import dotenvExpand from "dotenv-expand";
import manifest from "./manifest.json" with { type: "json" };
import fs from "fs/promises";
import path from "path";
const env = dotenv.config();
dotenvExpand.expand(env);
const banner = `/* const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
@ -12,6 +20,21 @@ if you want to view the source, please visit the github repository of this plugi
const prod = process.argv[2] === "production"; const prod = process.argv[2] === "production";
let outDir = "dist";
if (!prod) {
if (!process.env.OBSIDIAN_PLUGIN_DIR) {
console.log(
"Set OBSIDIAN_PLUGIN_DIR in the .env file to plugin directory to copy files to."
);
} else {
outDir = process.env.OBSIDIAN_PLUGIN_DIR + "/" + manifest.id + "-dev";
}
}
if (outDir[0] === "~") {
outDir = path.join(process.env.HOME, outDir.slice(1));
}
const context = await esbuild.context({ const context = await esbuild.context({
banner: { banner: {
js: banner, js: banner,
@ -21,11 +44,32 @@ const context = await esbuild.context({
plugins: [ plugins: [
esbuildSvelte({ esbuildSvelte({
preprocess: sveltePreprocess(), preprocess: sveltePreprocess(),
compilerOptions: { compilerOptions: { dev: !prod },
css: "injected",
dev: !prod,
},
}), }),
{
name: 'copy-plugin',
setup(build) {
build.onEnd(async () => {
try {
await fs.copyFile(new URL('./manifest.json', import.meta.url), path.resolve(outDir, 'manifest.json'));
} catch (e) {
console.error('Failed to rename file:', e);
}
});
},
},
{
name: 'rename-plugin',
setup(build) {
build.onEnd(async () => {
try {
await fs.rename(path.resolve(outDir, 'main.css'), path.resolve(outDir, 'styles.css'));
} catch (e) {
console.error('Failed to rename file:', e);
}
});
},
}
], ],
external: [ external: [
"obsidian", "obsidian",
@ -48,8 +92,8 @@ const context = await esbuild.context({
logLevel: "info", logLevel: "info",
sourcemap: prod ? false : "inline", sourcemap: prod ? false : "inline",
treeShaking: true, treeShaking: true,
outfile: "main.js", outdir: outDir,
minify: prod, minify: prod
}); });
if (prod) { if (prod) {

View File

@ -4,8 +4,8 @@
"description": "This is a sample plugin for Obsidian (https://obsidian.md)", "description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"dev": "vite build --watch --mode=development", "dev": "node esbuild.config.mjs",
"build": "vite build", "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json", "version": "node version-bump.mjs && git add manifest.json versions.json",
"svelte-check": "svelte-check --tsconfig tsconfig.json" "svelte-check": "svelte-check --tsconfig tsconfig.json"
}, },
@ -13,13 +13,10 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@guanghechen/rollup-plugin-copy": "^6.0.7",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@sveltejs/vite-plugin-svelte": "^5.1.0",
"@types/node": "^24.0.6", "@types/node": "^24.0.6",
"@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0", "@typescript-eslint/parser": "5.29.0",
"bits-ui": "^2.8.10",
"builtin-modules": "3.3.0", "builtin-modules": "3.3.0",
"dotenv": "^17.0.0", "dotenv": "^17.0.0",
"dotenv-expand": "^12.0.2", "dotenv-expand": "^12.0.2",
@ -29,12 +26,12 @@
"lucide-svelte": "^0.525.0", "lucide-svelte": "^0.525.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"obsidian": "latest", "obsidian": "latest",
"runed": "^0.29.1",
"sass": "^1.89.2", "sass": "^1.89.2",
"svelte": "^5.34.8", "svelte": "^5.34.8",
"svelte-check": "^4.2.2", "svelte-check": "^4.2.2",
"svelte-preprocess": "^6.0.3", "svelte-preprocess": "^6.0.3",
"tslib": "2.4.0", "tslib": "2.4.0",
"typescript": "5.0.4", "typescript": "5.0.4"
"vite": "^6.0.0"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -2,14 +2,11 @@
import { searchBooks, type SearchResult } from "@data-sources/goodreads"; import { searchBooks, type SearchResult } from "@data-sources/goodreads";
interface Props { interface Props {
onSearch: (results: SearchResult[]) => void; onSearch: (query: string, results: SearchResult[]) => void;
onError: (error: Error) => void; onError: (error: Error) => void;
} }
let { let { onSearch, onError }: Props = $props();
onSearch,
onError
}: Props = $props();
let query = $state(""); let query = $state("");
@ -22,7 +19,7 @@
onError(new Error("No results found.")); onError(new Error("No results found."));
return; return;
} }
onSearch(results); onSearch(query, results);
} catch (error) { } catch (error) {
onError(error as Error); onError(error as Error);
} }
@ -32,7 +29,12 @@
<div class="obt-goodreads-search"> <div class="obt-goodreads-search">
<h2>Goodreads Search</h2> <h2>Goodreads Search</h2>
<input type="text" placeholder="Search for books..." bind:value={query} {onkeydown} /> <input
type="text"
placeholder="Search for books..."
bind:value={query}
{onkeydown}
/>
</div> </div>
<style lang="scss"> <style lang="scss">

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { unstable_RatingGroup as RatingGroup } from "bits-ui";
import { Star, StarHalf } from "lucide-svelte"; import { Star, StarHalf } from "lucide-svelte";
interface Props { interface Props {
@ -7,21 +6,71 @@
name?: string; name?: string;
} }
const maxV = 5;
let { value = $bindable(), name }: Props = $props(); let { value = $bindable(), name }: Props = $props();
let ctrl: HTMLElement | null = $state(null);
let w = $state(0);
let h = $state(0);
$effect(() => {
if (ctrl) {
ctrl.style.width = `${w}px`;
ctrl.style.height = `${h}px`;
}
});
let hovering = $state(false);
let valueHover = $state(0);
let displayVal = $derived(hovering ? valueHover : (value ?? 0));
let items = $derived.by(() => {
const full = Number.isInteger(displayVal);
return Array.from({ length: maxV }, (_, i) => i + 1).map((index) => ({
index,
state:
index <= Math.ceil(displayVal)
? full || index != Math.ceil(displayVal)
? "active"
: "partial"
: "inactive",
}));
});
$effect(() => {
console.log(items);
});
function calcSliderPos(e: MouseEvent) {
return (e.offsetX / (e.target as HTMLElement).clientWidth) * maxV;
}
function onclick() {
value = valueHover;
}
function onmouseout() {
hovering = false;
valueHover = 0;
}
function onmousemove(e: MouseEvent) {
hovering = true;
valueHover = Math.ceil(calcSliderPos(e) * 2) / 2;
}
</script> </script>
<div class="rating-input"> <!-- svelte-ignore a11y_no_static_element_interactions -->
<RatingGroup.Root <!-- svelte-ignore a11y_click_events_have_key_events -->
{name} <!-- svelte-ignore a11y_no_static_element_interactions -->
bind:value <!-- svelte-ignore a11y_mouse_events_have_key_events -->
min={0} <div class="rating-input" {onclick} {onmouseout}>
max={5} <!-- svelte-ignore a11y_no_static_element_interactions -->
allowHalf <div class="ctrl" {onmousemove} bind:this={ctrl}></div>
class="rating-group" <div class="cont m-1" bind:clientWidth={w} bind:clientHeight={h}>
>
{#snippet children({ items })}
{#each items as item (item.index)} {#each items as item (item.index)}
<RatingGroup.Item index={item.index} class="rating-item"> <div class="rating-item">
{#if item.state === "inactive"} {#if item.state === "inactive"}
<Star fill="var(--interactive-normal)" /> <Star fill="var(--interactive-normal)" />
{:else if item.state === "active"} {:else if item.state === "active"}
@ -30,30 +79,31 @@
<Star fill="var(--interactive-normal)" /> <Star fill="var(--interactive-normal)" />
<StarHalf fill="var(--interactive-accent)" /> <StarHalf fill="var(--interactive-accent)" />
{/if} {/if}
</RatingGroup.Item> </div>
{/each} {/each}
{/snippet} </div>
</RatingGroup.Root>
</div> </div>
<style lang="scss"> <style lang="scss">
.rating-input { .rating-input {
cursor: pointer;
.ctrl {
position: absolute;
z-index: 2;
}
.cont {
display: flex; display: flex;
align-items: center; }
gap: 0.5rem;
:global(svg) { :global(svg) {
position: absolute; position: absolute;
width: var(--size-4-16); width: 100%;
height: var(--size-4-16); height: 100%;
} }
:global(.rating-group) { .rating-item {
display: flex;
gap: 0.25rem;
}
:global(.rating-item) {
position: relative; position: relative;
width: var(--size-4-16); width: var(--size-4-16);
height: var(--size-4-16); height: var(--size-4-16);

View File

@ -0,0 +1,102 @@
<script lang="ts">
import type { ReadingLogEntry } from "@src/types";
interface Props {
entry: ReadingLogEntry;
onSubmit: (entry: ReadingLogEntry) => void;
}
let { entry, onSubmit }: Props = $props();
let pagesRead = $state(entry.pagesRead);
let pagesReadTotal = $state(entry.pagesReadTotal);
let pagesRemaining = $state(entry.pagesRemaining);
// Source: https://github.com/sveltejs/svelte/discussions/14220#discussioncomment-11188219
function watch<T>(
getter: () => T,
effectCallback: (t: T | undefined) => void,
) {
let previous: T | undefined = undefined;
$effect(() => {
const current = getter(); // add $state.snapshot for deep reactivity
const cleanup = effectCallback(previous);
previous = current;
return cleanup;
});
}
watch(
() => pagesRead,
(prev) => {
if (prev !== pagesRead && prev !== undefined) {
const diff = pagesRead - prev;
pagesReadTotal = pagesReadTotal + diff;
pagesRemaining = pagesRemaining - diff;
}
},
);
function onsubmit(ev: SubmitEvent) {
ev.preventDefault();
onSubmit({
...entry,
pagesRead,
pagesReadTotal,
pagesRemaining,
});
}
</script>
<div class="obt-reading-log-entry-editor">
<h2>Edit Reading Log Entry</h2>
<form {onsubmit}>
<div class="fields">
<label for="pagesRead">Pages Read</label>
<input
type="number"
name="pagesRead"
id="pagesRead"
bind:value={pagesRead}
/>
<label for="pagesReadTotal">Pages Read Total</label>
<input
type="number"
name="pagesReadTotal"
id="pagesReadTotal"
bind:value={pagesReadTotal}
/>
<label for="pagesRemaining">Pages Remaining</label>
<input
type="number"
name="pagesRemaining"
id="pagesRemaining"
bind:value={pagesRemaining}
/>
</div>
<button type="submit">Submit</button>
</form>
</div>
<style lang="scss">
.obt-reading-log-entry-editor {
margin-bottom: var(--size-4-4);
h2 {
margin-bottom: var(--size-4-6);
}
form {
display: flex;
flex-direction: column;
gap: var(--size-4-4);
.fields {
display: grid;
grid-template-columns: max-content 1fr;
gap: var(--size-4-2);
}
}
}
</style>

View File

@ -1,6 +1,9 @@
<script lang="ts"> <script lang="ts">
import type { ReadingLog } from "@utils/storage"; import type { ReadingLog } from "@utils/storage";
import type { ReadingLogEntry } from "@src/types";
import type { App } from "obsidian"; import type { App } from "obsidian";
import { Edit, Trash } from "lucide-svelte";
import { ReadingLogEntryEditModal } from "@views/reading-log-entry-edit-modal";
const ALL_TIME = "ALL_TIME"; const ALL_TIME = "ALL_TIME";
@ -22,10 +25,23 @@
return moment(date).format("YYYY-MM-DD"); return moment(date).format("YYYY-MM-DD");
} }
const entries = readingLog.getEntries(); let entries = $state(
const years = [ readingLog.getEntries().map((entry, id) => ({
...entry,
id,
})),
);
function reload() {
entries = readingLog.getEntries().map((entry, id) => ({
...entry,
id,
}));
}
const years = $derived([
...new Set(entries.map((entry) => entry.createdAt.getFullYear())), ...new Set(entries.map((entry) => entry.createdAt.getFullYear())),
]; ]);
let selectedYear = $state(new Date().getFullYear().toString()); let selectedYear = $state(new Date().getFullYear().toString());
const filterYear = $derived( const filterYear = $derived(
@ -51,6 +67,22 @@
entry.createdAt.getMonth() === filterMonth - 1), entry.createdAt.getMonth() === filterMonth - 1),
), ),
); );
function editEntry(i: number, entry: ReadingLogEntry) {
const modal = new ReadingLogEntryEditModal(app, entry);
modal.once("submit", async (event) => {
console.log(i, event);
modal.close();
await readingLog.updateEntry(i, event.entry);
reload();
});
modal.open();
}
async function deleteEntry(i: number) {
await readingLog.spliceEntry(i);
reload();
}
</script> </script>
<div class="obt-reading-log-viewer"> <div class="obt-reading-log-viewer">
@ -85,6 +117,7 @@
<th class="book">Book</th> <th class="book">Book</th>
<th class="pages-read">Pages Read</th> <th class="pages-read">Pages Read</th>
<th class="percent-complete">Percent Complete</th> <th class="percent-complete">Percent Complete</th>
<th class="actions">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -102,6 +135,16 @@
100, 100,
)}% )}%
</td> </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> </tr>
{/each} {/each}
</tbody> </tbody>
@ -109,6 +152,8 @@
</div> </div>
<style lang="scss"> <style lang="scss">
@use "../styles/utils";
.obt-reading-log-viewer { .obt-reading-log-viewer {
.year-filter:has(> option.all-time:checked) + .month-filter { .year-filter:has(> option.all-time:checked) + .month-filter {
display: none; display: none;
@ -130,6 +175,10 @@
td.percent-complete { td.percent-complete {
text-align: center; text-align: center;
} }
td.actions span {
@include utils.visually-hidden;
}
} }
} }
</style> </style>

26
src/styles/_utils.scss Normal file
View File

@ -0,0 +1,26 @@
// Source: https://gist.github.com/ffoodd/000b59f431e3e64e4ce1a24d5bb36034
@mixin visually-hidden {
border: 0 !important;
clip-path: inset(50%) !important; /* 2 */
height: 1px !important;
margin: -1px !important;
overflow: hidden !important;
padding: 0 !important;
width: 1px !important;
white-space: nowrap !important; /* 3 */
&:not(caption) {
position: absolute !important;
}
* {
overflow: hidden !important;
}
}
@mixin visually-hidden-focusable {
&:not(:focus, :focus-within) {
@include visually-hidden;
}
}

View File

@ -21,3 +21,11 @@ export interface Book {
isbn: string; isbn: string;
isbn13: string; isbn13: string;
} }
export interface ReadingLogEntry {
book: string;
pagesRead: number;
pagesReadTotal: number;
pagesRemaining: number;
createdAt: Date;
}

59
src/utils/event.ts Normal file
View File

@ -0,0 +1,59 @@
export abstract class Event {}
export type EventHandler<T> = (event: T) => void;
export type EventHandlerMap<TEventMap, T extends keyof TEventMap> = Record<
T,
EventHandler<TEventMap[T]>[]
>;
type Constructor = new (...args: any[]) => {};
export function EventEmitter<TEventMap, TBase extends Constructor>(
Base: TBase
) {
return class extends Base {
private readonly listeners: EventHandlerMap<
TEventMap,
keyof TEventMap
> = {} as EventHandlerMap<TEventMap, keyof TEventMap>;
public on<T extends keyof TEventMap>(
type: T,
handler: EventHandler<TEventMap[T]>
) {
if (!this.listeners[type]) {
this.listeners[type] = [];
}
this.listeners[type].push(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 = (event: TEventMap[T]) => {
handler(event);
this.off(type, wrappedHandler);
};
this.on(type, wrappedHandler);
}
public emit<T extends keyof TEventMap>(type: T, event: TEventMap[T]) {
if (this.listeners[type]) {
this.listeners[type].forEach((handler) => handler(event));
}
}
};
}

View File

@ -1,4 +1,5 @@
import BookTrackerPlugin from "@src/main"; import BookTrackerPlugin from "@src/main";
import type { ReadingLogEntry } from "@src/types";
import { App } from "obsidian"; import { App } from "obsidian";
export class Storage { export class Storage {
@ -33,14 +34,6 @@ export class Storage {
} }
} }
interface ReadingLogEntry {
readonly book: string;
readonly pagesRead: number;
readonly pagesReadTotal: number;
readonly pagesRemaining: number;
readonly createdAt: Date;
}
export class ReadingLog { export class ReadingLog {
private entries: ReadingLogEntry[] = []; private entries: ReadingLogEntry[] = [];
@ -120,6 +113,17 @@ export class ReadingLog {
} }
} }
public async updateEntry(i: number, entry: ReadingLogEntry): Promise<void> {
this.entries[i] = entry;
await this.storeEntries();
}
public async spliceEntry(i: number): Promise<ReadingLogEntry> {
const entry = this.entries.splice(i, 1);
await this.storeEntries();
return entry[0];
}
public async clearEntries(): Promise<void> { public async clearEntries(): Promise<void> {
this.entries = []; this.entries = [];
await this.storeEntries(); await this.storeEntries();

View File

@ -1,29 +1,44 @@
import GoodreadsSearch from "@components/GoodreadsSearch.svelte"; import GoodreadsSearch from "@components/GoodreadsSearch.svelte";
import { type SearchResult } from "@data-sources/goodreads"; import { type SearchResult } from "@data-sources/goodreads";
import { Event, EventEmitter } from "@utils/event";
import { App, Modal, Notice } from "obsidian"; import { App, Modal, Notice } from "obsidian";
import { mount, unmount } from "svelte"; import { mount, unmount } from "svelte";
export class GoodreadsSearchModal extends Modal { export class SearchEvent extends Event {
private component: ReturnType<typeof GoodreadsSearch> | undefined;
constructor( constructor(
app: App, public readonly query: string,
private readonly onSearch: (error: any, results: SearchResult[]) => void public readonly results: SearchResult[]
) { ) {
super(app); super();
} }
}
export class ErrorEvent extends Event {
constructor(public readonly error: Error) {
super();
}
}
interface GoodreadsSearchModalEventMap {
search: SearchEvent;
error: ErrorEvent;
}
export class GoodreadsSearchModal extends EventEmitter<
GoodreadsSearchModalEventMap,
typeof Modal
>(Modal) {
private component: ReturnType<typeof GoodreadsSearch> | undefined;
onOpen() { onOpen() {
this.component = mount(GoodreadsSearch, { this.component = mount(GoodreadsSearch, {
target: this.contentEl, target: this.contentEl,
props: { props: {
onError(error) { onError: (error: Error) => {
this.onSearch(error, []); this.emit("error", new ErrorEvent(error));
this.close();
}, },
onSearch: (results: SearchResult[]) => { onSearch: (query: string, results: SearchResult[]) => {
this.onSearch(null, results); this.emit("search", new SearchEvent(query, results));
this.close();
}, },
}, },
}); });
@ -38,13 +53,14 @@ export class GoodreadsSearchModal extends Modal {
static createAndOpen(app: App): Promise<SearchResult[]> { static createAndOpen(app: App): Promise<SearchResult[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const modal = new GoodreadsSearchModal(app, (error, results) => { const modal = new GoodreadsSearchModal(app);
if (error) { modal.once("search", (event: SearchEvent) => {
new Notice(`Error: ${error.message}`); modal.close();
reject(error); resolve(event.results);
} else { });
resolve(results); modal.once("error", (event: ErrorEvent) => {
} modal.close();
reject(event.error);
}); });
modal.open(); modal.open();
}); });

View File

@ -1,22 +1,30 @@
import Rating from "@components/Rating.svelte"; import Rating from "@components/Rating.svelte";
import { Event, EventEmitter } from "@utils/event";
import { App, Modal } from "obsidian"; import { App, Modal } from "obsidian";
import { mount, unmount } from "svelte"; import { mount, unmount } from "svelte";
export class RatingModal extends Modal { class SubmitEvent extends Event {
private component: ReturnType<typeof Rating> | undefined; constructor(public readonly rating: number) {
super();
constructor(app: App, private readonly onSubmit: (rating: number) => void) {
super(app);
} }
}
interface RatingModalEventMap {
submit: SubmitEvent;
}
export class RatingModal extends EventEmitter<
RatingModalEventMap,
typeof Modal
>(Modal) {
private component: ReturnType<typeof Rating> | undefined;
onOpen(): void { onOpen(): void {
this.component = mount(Rating, { this.component = mount(Rating, {
target: this.contentEl, target: this.contentEl,
props: { props: {
onSubmit: (rating) => { onSubmit: (rating: number) =>
this.onSubmit(rating); this.emit("submit", new SubmitEvent(rating)),
this.close();
},
}, },
}); });
} }
@ -30,8 +38,10 @@ export class RatingModal extends Modal {
static createAndOpen(app: App): Promise<number> { static createAndOpen(app: App): Promise<number> {
return new Promise((resolve) => { return new Promise((resolve) => {
const modal = new RatingModal(app, (rating) => { const modal = new RatingModal(app);
resolve(rating); modal.once("submit", (event: SubmitEvent) => {
modal.close();
resolve(event.rating);
}); });
modal.open(); modal.open();
}); });

View File

@ -0,0 +1,44 @@
import ReadingLogEntryEditor from "@components/ReadingLogEntryEditor.svelte";
import type { ReadingLogEntry } from "@src/types";
import { Event, EventEmitter } from "@utils/event";
import { App, Modal } from "obsidian";
import { mount, unmount } from "svelte";
class SubmitEvent extends Event {
constructor(public readonly entry: ReadingLogEntry) {
super();
}
}
interface ReadingLogEntryEditModalEventMap {
submit: SubmitEvent;
}
export class ReadingLogEntryEditModal extends EventEmitter<
ReadingLogEntryEditModalEventMap,
typeof Modal
>(Modal) {
private component: ReturnType<typeof ReadingLogEntryEditor> | undefined;
constructor(app: App, private readonly entry: ReadingLogEntry) {
super(app);
}
onOpen(): void {
this.component = mount(ReadingLogEntryEditor, {
target: this.contentEl,
props: {
entry: this.entry,
onSubmit: (entry: ReadingLogEntry) =>
this.emit("submit", new SubmitEvent(entry)),
},
});
}
onClose(): void {
if (this.component) {
unmount(this.component);
this.component = undefined;
}
}
}

View File

@ -1,15 +1,25 @@
import ReadingProgress from "@components/ReadingProgress.svelte"; import ReadingProgress from "@components/ReadingProgress.svelte";
import { Event, EventEmitter } from "@utils/event";
import { App, Modal } from "obsidian"; import { App, Modal } from "obsidian";
import { mount, unmount } from "svelte"; import { mount, unmount } from "svelte";
export class ReadingProgressModal extends Modal { class SubmitEvent extends Event {
constructor(public readonly pageNumber: number) {
super();
}
}
interface ReadingProgressModalEventMap {
submit: SubmitEvent;
}
export class ReadingProgressModal extends EventEmitter<
ReadingProgressModalEventMap,
typeof Modal
>(Modal) {
private component: ReturnType<typeof ReadingProgress> | undefined; private component: ReturnType<typeof ReadingProgress> | undefined;
constructor( constructor(app: App, private readonly pageLength: number) {
app: App,
private readonly pageLength: number,
private readonly onSubmit: (pageNumber: number) => void
) {
super(app); super(app);
} }
@ -18,9 +28,8 @@ export class ReadingProgressModal extends Modal {
target: this.contentEl, target: this.contentEl,
props: { props: {
pageLength: this.pageLength, pageLength: this.pageLength,
onSubmit: (pageNumber) => { onSubmit: (pageNumber: number) => {
this.onSubmit(pageNumber); this.emit("submit", new SubmitEvent(pageNumber));
this.close();
}, },
}, },
}); });
@ -35,13 +44,11 @@ export class ReadingProgressModal extends Modal {
static createAndOpen(app: App, pageLength: number): Promise<number> { static createAndOpen(app: App, pageLength: number): Promise<number> {
return new Promise((resolve) => { return new Promise((resolve) => {
const modal = new ReadingProgressModal( const modal = new ReadingProgressModal(app, pageLength);
app, modal.once("submit", (event: SubmitEvent) => {
pageLength, modal.close();
(pageNumber) => { resolve(event.pageNumber);
resolve(pageNumber); });
}
);
modal.open(); modal.open();
}); });
} }

View File

@ -1,89 +0,0 @@
import { UserConfig, defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import copy from "@guanghechen/rollup-plugin-copy";
import manifest from "./manifest.json";
import path from "path";
import builtins from "builtin-modules";
import dotenv from "dotenv";
import dotenvExpand from "dotenv-expand";
const env = dotenv.config();
dotenvExpand.expand(env);
export default defineConfig(async ({ mode }) => {
const { resolve } = path;
const prod = mode === "production";
let outDir = "dist";
if (!prod) {
if (!process.env.OBSIDIAN_PLUGIN_DIR) {
console.log(
"Set OBSIDIAN_PLUGIN_DIR in the .env file to plugin directory to copy files to."
);
} else {
outDir =
process.env.OBSIDIAN_PLUGIN_DIR + "/" + manifest.id + "-dev";
}
}
if (outDir[0] === "~") {
outDir = path.join(process.env.HOME!, outDir.slice(1));
}
return {
plugins: [
svelte(),
copy({
targets: [{ src: "manifest.json", dest: outDir }],
}),
],
resolve: {
alias: {
"@components": resolve(__dirname, "./src/components"),
"@data-sources": resolve(__dirname, "./src/data-sources"),
"@settings": resolve(__dirname, "./src/settings"),
"@utils": resolve(__dirname, "./src/utils"),
"@views": resolve(__dirname, "./src/views"),
"@src": resolve(__dirname, "./src"),
},
},
build: {
lib: {
entry: resolve(__dirname, "src/main.ts"),
name: "main",
fileName: () => "main.js",
formats: ["cjs"],
},
minify: prod,
sourcemap: prod ? false : "inline",
cssCodeSplit: false,
emptyOutDir: false,
outDir,
rollupOptions: {
input: {
main: resolve(__dirname, "src/main.ts"),
},
output: {
entryFileNames: "main.js",
assetFileNames: "styles.css",
},
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
],
},
},
} as UserConfig;
});