generated from tpl/obsidian-sample-plugin
Add actions to reading log viewer
This commit is contained in:
parent
a2767f4595
commit
8ebed95fda
|
@ -3,6 +3,14 @@ import process from "process";
|
|||
import builtins from "builtin-modules";
|
||||
import esbuildSvelte from "esbuild-svelte";
|
||||
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 = `/*
|
||||
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";
|
||||
|
||||
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({
|
||||
banner: {
|
||||
js: banner,
|
||||
|
@ -21,11 +44,32 @@ const context = await esbuild.context({
|
|||
plugins: [
|
||||
esbuildSvelte({
|
||||
preprocess: sveltePreprocess(),
|
||||
compilerOptions: {
|
||||
css: "injected",
|
||||
dev: !prod,
|
||||
},
|
||||
compilerOptions: { 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: [
|
||||
"obsidian",
|
||||
|
@ -48,8 +92,8 @@ const context = await esbuild.context({
|
|||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
minify: prod,
|
||||
outdir: outDir,
|
||||
minify: prod
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
|
|
11
package.json
11
package.json
|
@ -4,8 +4,8 @@
|
|||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch --mode=development",
|
||||
"build": "vite build",
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||
"svelte-check": "svelte-check --tsconfig tsconfig.json"
|
||||
},
|
||||
|
@ -13,13 +13,10 @@
|
|||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@guanghechen/rollup-plugin-copy": "^6.0.7",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.1.0",
|
||||
"@types/node": "^24.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"bits-ui": "^2.8.10",
|
||||
"builtin-modules": "3.3.0",
|
||||
"dotenv": "^17.0.0",
|
||||
"dotenv-expand": "^12.0.2",
|
||||
|
@ -29,12 +26,12 @@
|
|||
"lucide-svelte": "^0.525.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"obsidian": "latest",
|
||||
"runed": "^0.29.1",
|
||||
"sass": "^1.89.2",
|
||||
"svelte": "^5.34.8",
|
||||
"svelte-check": "^4.2.2",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "5.0.4",
|
||||
"vite": "^6.0.0"
|
||||
"typescript": "5.0.4"
|
||||
}
|
||||
}
|
||||
|
|
738
pnpm-lock.yaml
738
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -2,14 +2,11 @@
|
|||
import { searchBooks, type SearchResult } from "@data-sources/goodreads";
|
||||
|
||||
interface Props {
|
||||
onSearch: (results: SearchResult[]) => void;
|
||||
onSearch: (query: string, results: SearchResult[]) => void;
|
||||
onError: (error: Error) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
onSearch,
|
||||
onError
|
||||
}: Props = $props();
|
||||
let { onSearch, onError }: Props = $props();
|
||||
|
||||
let query = $state("");
|
||||
|
||||
|
@ -22,7 +19,7 @@
|
|||
onError(new Error("No results found."));
|
||||
return;
|
||||
}
|
||||
onSearch(results);
|
||||
onSearch(query, results);
|
||||
} catch (error) {
|
||||
onError(error as Error);
|
||||
}
|
||||
|
@ -32,7 +29,12 @@
|
|||
|
||||
<div class="obt-goodreads-search">
|
||||
<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>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { unstable_RatingGroup as RatingGroup } from "bits-ui";
|
||||
import { Star, StarHalf } from "lucide-svelte";
|
||||
|
||||
interface Props {
|
||||
|
@ -7,21 +6,71 @@
|
|||
name?: string;
|
||||
}
|
||||
|
||||
const maxV = 5;
|
||||
|
||||
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>
|
||||
|
||||
<div class="rating-input">
|
||||
<RatingGroup.Root
|
||||
{name}
|
||||
bind:value
|
||||
min={0}
|
||||
max={5}
|
||||
allowHalf
|
||||
class="rating-group"
|
||||
>
|
||||
{#snippet children({ items })}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
||||
<div class="rating-input" {onclick} {onmouseout}>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="ctrl" {onmousemove} bind:this={ctrl}></div>
|
||||
<div class="cont m-1" bind:clientWidth={w} bind:clientHeight={h}>
|
||||
{#each items as item (item.index)}
|
||||
<RatingGroup.Item index={item.index} class="rating-item">
|
||||
<div class="rating-item">
|
||||
{#if item.state === "inactive"}
|
||||
<Star fill="var(--interactive-normal)" />
|
||||
{:else if item.state === "active"}
|
||||
|
@ -30,30 +79,31 @@
|
|||
<Star fill="var(--interactive-normal)" />
|
||||
<StarHalf fill="var(--interactive-accent)" />
|
||||
{/if}
|
||||
</RatingGroup.Item>
|
||||
</div>
|
||||
{/each}
|
||||
{/snippet}
|
||||
</RatingGroup.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.rating-input {
|
||||
cursor: pointer;
|
||||
|
||||
.ctrl {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.cont {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
:global(svg) {
|
||||
position: absolute;
|
||||
width: var(--size-4-16);
|
||||
height: var(--size-4-16);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(.rating-group) {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
:global(.rating-item) {
|
||||
.rating-item {
|
||||
position: relative;
|
||||
width: var(--size-4-16);
|
||||
height: var(--size-4-16);
|
||||
|
|
|
@ -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>
|
|
@ -1,6 +1,9 @@
|
|||
<script lang="ts">
|
||||
import type { ReadingLog } from "@utils/storage";
|
||||
import type { ReadingLogEntry } from "@src/types";
|
||||
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";
|
||||
|
||||
|
@ -22,10 +25,23 @@
|
|||
return moment(date).format("YYYY-MM-DD");
|
||||
}
|
||||
|
||||
const entries = readingLog.getEntries();
|
||||
const years = [
|
||||
let entries = $state(
|
||||
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())),
|
||||
];
|
||||
]);
|
||||
|
||||
let selectedYear = $state(new Date().getFullYear().toString());
|
||||
const filterYear = $derived(
|
||||
|
@ -51,6 +67,22 @@
|
|||
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>
|
||||
|
||||
<div class="obt-reading-log-viewer">
|
||||
|
@ -85,6 +117,7 @@
|
|||
<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>
|
||||
|
@ -102,6 +135,16 @@
|
|||
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>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
@ -109,6 +152,8 @@
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use "../styles/utils";
|
||||
|
||||
.obt-reading-log-viewer {
|
||||
.year-filter:has(> option.all-time:checked) + .month-filter {
|
||||
display: none;
|
||||
|
@ -130,6 +175,10 @@
|
|||
td.percent-complete {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td.actions span {
|
||||
@include utils.visually-hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -21,3 +21,11 @@ export interface Book {
|
|||
isbn: string;
|
||||
isbn13: string;
|
||||
}
|
||||
|
||||
export interface ReadingLogEntry {
|
||||
book: string;
|
||||
pagesRead: number;
|
||||
pagesReadTotal: number;
|
||||
pagesRemaining: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import BookTrackerPlugin from "@src/main";
|
||||
import type { ReadingLogEntry } from "@src/types";
|
||||
import { App } from "obsidian";
|
||||
|
||||
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 {
|
||||
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> {
|
||||
this.entries = [];
|
||||
await this.storeEntries();
|
||||
|
|
|
@ -1,29 +1,44 @@
|
|||
import GoodreadsSearch from "@components/GoodreadsSearch.svelte";
|
||||
import { type SearchResult } from "@data-sources/goodreads";
|
||||
import { Event, EventEmitter } from "@utils/event";
|
||||
import { App, Modal, Notice } from "obsidian";
|
||||
import { mount, unmount } from "svelte";
|
||||
|
||||
export class GoodreadsSearchModal extends Modal {
|
||||
private component: ReturnType<typeof GoodreadsSearch> | undefined;
|
||||
|
||||
export class SearchEvent extends Event {
|
||||
constructor(
|
||||
app: App,
|
||||
private readonly onSearch: (error: any, results: SearchResult[]) => void
|
||||
public readonly query: string,
|
||||
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() {
|
||||
this.component = mount(GoodreadsSearch, {
|
||||
target: this.contentEl,
|
||||
props: {
|
||||
onError(error) {
|
||||
this.onSearch(error, []);
|
||||
this.close();
|
||||
onError: (error: Error) => {
|
||||
this.emit("error", new ErrorEvent(error));
|
||||
},
|
||||
onSearch: (results: SearchResult[]) => {
|
||||
this.onSearch(null, results);
|
||||
this.close();
|
||||
onSearch: (query: string, results: SearchResult[]) => {
|
||||
this.emit("search", new SearchEvent(query, results));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -38,13 +53,14 @@ export class GoodreadsSearchModal extends Modal {
|
|||
|
||||
static createAndOpen(app: App): Promise<SearchResult[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const modal = new GoodreadsSearchModal(app, (error, results) => {
|
||||
if (error) {
|
||||
new Notice(`Error: ${error.message}`);
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(results);
|
||||
}
|
||||
const modal = new GoodreadsSearchModal(app);
|
||||
modal.once("search", (event: SearchEvent) => {
|
||||
modal.close();
|
||||
resolve(event.results);
|
||||
});
|
||||
modal.once("error", (event: ErrorEvent) => {
|
||||
modal.close();
|
||||
reject(event.error);
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
|
|
|
@ -1,22 +1,30 @@
|
|||
import Rating from "@components/Rating.svelte";
|
||||
import { Event, EventEmitter } from "@utils/event";
|
||||
import { App, Modal } from "obsidian";
|
||||
import { mount, unmount } from "svelte";
|
||||
|
||||
export class RatingModal extends Modal {
|
||||
private component: ReturnType<typeof Rating> | undefined;
|
||||
|
||||
constructor(app: App, private readonly onSubmit: (rating: number) => void) {
|
||||
super(app);
|
||||
class SubmitEvent extends Event {
|
||||
constructor(public readonly rating: number) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
interface RatingModalEventMap {
|
||||
submit: SubmitEvent;
|
||||
}
|
||||
|
||||
export class RatingModal extends EventEmitter<
|
||||
RatingModalEventMap,
|
||||
typeof Modal
|
||||
>(Modal) {
|
||||
private component: ReturnType<typeof Rating> | undefined;
|
||||
|
||||
onOpen(): void {
|
||||
this.component = mount(Rating, {
|
||||
target: this.contentEl,
|
||||
props: {
|
||||
onSubmit: (rating) => {
|
||||
this.onSubmit(rating);
|
||||
this.close();
|
||||
},
|
||||
onSubmit: (rating: number) =>
|
||||
this.emit("submit", new SubmitEvent(rating)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -30,8 +38,10 @@ export class RatingModal extends Modal {
|
|||
|
||||
static createAndOpen(app: App): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new RatingModal(app, (rating) => {
|
||||
resolve(rating);
|
||||
const modal = new RatingModal(app);
|
||||
modal.once("submit", (event: SubmitEvent) => {
|
||||
modal.close();
|
||||
resolve(event.rating);
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,25 @@
|
|||
import ReadingProgress from "@components/ReadingProgress.svelte";
|
||||
import { Event, EventEmitter } from "@utils/event";
|
||||
import { App, Modal } from "obsidian";
|
||||
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;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
private readonly pageLength: number,
|
||||
private readonly onSubmit: (pageNumber: number) => void
|
||||
) {
|
||||
constructor(app: App, private readonly pageLength: number) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
|
@ -18,9 +28,8 @@ export class ReadingProgressModal extends Modal {
|
|||
target: this.contentEl,
|
||||
props: {
|
||||
pageLength: this.pageLength,
|
||||
onSubmit: (pageNumber) => {
|
||||
this.onSubmit(pageNumber);
|
||||
this.close();
|
||||
onSubmit: (pageNumber: number) => {
|
||||
this.emit("submit", new SubmitEvent(pageNumber));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -35,13 +44,11 @@ export class ReadingProgressModal extends Modal {
|
|||
|
||||
static createAndOpen(app: App, pageLength: number): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new ReadingProgressModal(
|
||||
app,
|
||||
pageLength,
|
||||
(pageNumber) => {
|
||||
resolve(pageNumber);
|
||||
}
|
||||
);
|
||||
const modal = new ReadingProgressModal(app, pageLength);
|
||||
modal.once("submit", (event: SubmitEvent) => {
|
||||
modal.close();
|
||||
resolve(event.pageNumber);
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
Loading…
Reference in New Issue