diff --git a/src/main.ts b/src/main.ts index 52a99d1..1b280f6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,9 +5,13 @@ import { DEFAULT_SETTINGS, } from "./settings/settings"; import { getBookByLegacyId, type SearchResult } from "@data-sources/goodreads"; -import { Templater } from "./utils/templater"; -import { GoodreadsSearchModal } from "@views/goodreads-search-modal"; -import { GoodreadsSearchSuggestModal } from "@views/goodreads-search-suggest-modal"; +import { Templater } from "@utils/templater"; +import { + GoodreadsSearchModal, + GoodreadsSearchSuggestModal, + ReadingProgressModal, + RatingModal, +} from "@ui/modals"; import { CONTENT_TYPE_EXTENSIONS, IN_PROGRESS_STATE, @@ -15,10 +19,7 @@ import { TO_BE_READ_STATE, } from "./const"; import { ReadingLog, Storage } from "@utils/storage"; -import { ReadingProgressModal } from "@views/reading-progress-modal"; -import { RatingModal } from "@views/rating-modal"; -import { renderCodeBlockProcessor } from "@utils/svelte"; -import ReadingLogViewer from "@components/ReadingLogViewer.svelte"; +import { registerReadingLogCodeBlockProcessor } from "@ui/code-blocks"; export default class BookTrackerPlugin extends Plugin { settings: BookTrackerPluginSettings; @@ -65,13 +66,7 @@ export default class BookTrackerPlugin extends Plugin { this.addSettingTab(new BookTrackerSettingTab(this.app, this)); - this.registerMarkdownCodeBlockProcessor( - "readinglog", - renderCodeBlockProcessor(ReadingLogViewer, { - app: this.app, - readingLog: this.readingLog, - }) - ); + registerReadingLogCodeBlockProcessor(this); } onunload() {} diff --git a/src/ui/code-blocks/ReadingLogCodeBlock.ts b/src/ui/code-blocks/ReadingLogCodeBlock.ts new file mode 100644 index 0000000..cb099ae --- /dev/null +++ b/src/ui/code-blocks/ReadingLogCodeBlock.ts @@ -0,0 +1,29 @@ +import { registerCodeBlockRenderer } from "."; +import { SvelteCodeBlockRenderer } from "./SvelteCodeBlockRenderer"; +import ReadingLogCodeBlockView from "./ReadingLogCodeBlockView.svelte"; +import type BookTrackerPlugin from "@src/main"; + +export function registerReadingLogCodeBlockProcessor( + plugin: BookTrackerPlugin +): void { + registerCodeBlockRenderer( + plugin, + "readinglog", + (_source, el) => new ReadingLogCodeBlockRenderer(el, plugin) + ); +} + +export class ReadingLogCodeBlockRenderer extends SvelteCodeBlockRenderer< + typeof ReadingLogCodeBlockView +> { + constructor(contentEl: HTMLElement, plugin: BookTrackerPlugin) { + super(contentEl, ReadingLogCodeBlockView, { + props: { + app: plugin.app, + readingLog: plugin.readingLog, + }, + }); + } + + onunload() {} +} diff --git a/src/components/ReadingLogViewer.svelte b/src/ui/code-blocks/ReadingLogCodeBlockView.svelte similarity index 90% rename from src/components/ReadingLogViewer.svelte rename to src/ui/code-blocks/ReadingLogCodeBlockView.svelte index e6a1ec1..228f2dc 100644 --- a/src/components/ReadingLogViewer.svelte +++ b/src/ui/code-blocks/ReadingLogCodeBlockView.svelte @@ -3,8 +3,7 @@ import type { ReadingLogEntry } from "@src/types"; import type { App } from "obsidian"; import { Edit, Trash, Plus } from "lucide-svelte"; - import { ReadingLogEntryEditModal } from "@views/reading-log-entry-edit-modal"; - import { ReadingLogNewEntryModal } from "@views/reading-log-new-entry-modal"; + import { ReadingLogEntryEditModal } from "@ui/modals"; const ALL_TIME = "ALL_TIME"; @@ -70,23 +69,24 @@ ); function createEntry() { - const modal = new ReadingLogNewEntryModal(app); - modal.once("submit", async (event) => { + const modal = new ReadingLogEntryEditModal(app, async (entry) => { modal.close(); - await readingLog.addRawEntry(event.entry); + await readingLog.addRawEntry(entry); reload(); }); modal.open(); } 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(); - }); + const modal = new ReadingLogEntryEditModal( + app, + async (entry) => { + modal.close(); + await readingLog.updateEntry(i, entry); + reload(); + }, + entry, + ); modal.open(); } diff --git a/src/ui/code-blocks/SvelteCodeBlockRenderer.ts b/src/ui/code-blocks/SvelteCodeBlockRenderer.ts new file mode 100644 index 0000000..48ccbc2 --- /dev/null +++ b/src/ui/code-blocks/SvelteCodeBlockRenderer.ts @@ -0,0 +1,33 @@ +import { mount, unmount, type Component, type MountOptions } from "svelte"; +import { MarkdownRenderChild } from "obsidian"; + +export class SvelteCodeBlockRenderer< + TComponent extends Component, + TProps extends Record = {}, + TExports extends Record = {}, + TBindings extends keyof TProps | "" = string +> extends MarkdownRenderChild { + protected component: TExports | undefined; + + constructor( + private readonly contentEl: HTMLElement, + private readonly componentCtor: TComponent, + private readonly mountOpts: Omit, "target"> + ) { + super(contentEl); + } + + onload(): void { + this.component = mount(this.componentCtor, { + ...this.mountOpts, + target: this.contentEl, + }); + } + + onunload(): void { + if (this.component) { + unmount(this.component); + this.component = undefined; + } + } +} diff --git a/src/ui/code-blocks/index.ts b/src/ui/code-blocks/index.ts new file mode 100644 index 0000000..72caada --- /dev/null +++ b/src/ui/code-blocks/index.ts @@ -0,0 +1,46 @@ +import type { Plugin } from "obsidian"; +import { mount, unmount, type Component, type MountOptions } from "svelte"; +import { MarkdownRenderChild } from "obsidian"; + +export function registerCodeBlockRenderer( + plugin: Plugin, + name: string, + renderer: (source: string, el: HTMLElement) => MarkdownRenderChild +): void { + plugin.registerMarkdownCodeBlockProcessor(name, (source, el, ctx) => { + ctx.addChild(renderer(source, el)); + }); +} + +export class SvelteCodeBlockRenderer< + TComponent extends Component, + TProps extends Record = {}, + TExports extends Record = {}, + TBindings extends keyof TProps | "" = string +> extends MarkdownRenderChild { + protected component: TExports | undefined; + + constructor( + private readonly contentEl: HTMLElement, + private readonly componentCtor: TComponent, + private readonly mountOpts: Omit, "target"> + ) { + super(contentEl); + } + + onload(): void { + this.component = mount(this.componentCtor, { + ...this.mountOpts, + target: this.contentEl, + }); + } + + onunload(): void { + if (this.component) { + unmount(this.component); + this.component = undefined; + } + } +} + +export { registerReadingLogCodeBlockProcessor } from "./ReadingLogCodeBlock"; diff --git a/src/components/RatingInput.svelte b/src/ui/components/RatingInput.svelte similarity index 100% rename from src/components/RatingInput.svelte rename to src/ui/components/RatingInput.svelte diff --git a/src/ui/modals/GoodreadsSearchModal.ts b/src/ui/modals/GoodreadsSearchModal.ts new file mode 100644 index 0000000..433376b --- /dev/null +++ b/src/ui/modals/GoodreadsSearchModal.ts @@ -0,0 +1,31 @@ +import GoodreadsSearchModalView from "./GoodreadsSearchModalView.svelte"; +import { type SearchResult } from "@data-sources/goodreads"; +import { App } from "obsidian"; +import { SvelteModal } from "./SvelteModal"; + +export class GoodreadsSearchModal extends SvelteModal< + typeof GoodreadsSearchModalView +> { + constructor( + app: App, + onSearch: (error: any, results: SearchResult[]) => void = () => {} + ) { + super(app, GoodreadsSearchModalView, { props: { onSearch } }); + } + + static createAndOpen(app: App): Promise { + return new Promise((resolve, reject) => { + const modal = new GoodreadsSearchModal(app, (error, results) => { + modal.close(); + + if (error) { + reject(error); + return; + } + + resolve(results); + }); + modal.open(); + }); + } +} diff --git a/src/components/GoodreadsSearch.svelte b/src/ui/modals/GoodreadsSearchModalView.svelte similarity index 73% rename from src/components/GoodreadsSearch.svelte rename to src/ui/modals/GoodreadsSearchModalView.svelte index dfbdac2..72d41b0 100644 --- a/src/components/GoodreadsSearch.svelte +++ b/src/ui/modals/GoodreadsSearchModalView.svelte @@ -2,11 +2,10 @@ import { searchBooks, type SearchResult } from "@data-sources/goodreads"; interface Props { - onSearch: (query: string, results: SearchResult[]) => void; - onError: (error: Error) => void; + onSearch: (error: any, results?: SearchResult[]) => void; } - let { onSearch, onError }: Props = $props(); + let { onSearch }: Props = $props(); let query = $state(""); @@ -16,12 +15,12 @@ try { const results = await searchBooks(query); if (results.length === 0) { - onError(new Error("No results found.")); + onSearch(new Error("No results found.")); return; } - onSearch(query, results); + onSearch(null, results); } catch (error) { - onError(error as Error); + onSearch(error); } } } diff --git a/src/views/goodreads-search-suggest-modal.ts b/src/ui/modals/GoodreadsSearchSuggestModal.ts similarity index 87% rename from src/views/goodreads-search-suggest-modal.ts rename to src/ui/modals/GoodreadsSearchSuggestModal.ts index 9cf54eb..95d3c1b 100644 --- a/src/views/goodreads-search-suggest-modal.ts +++ b/src/ui/modals/GoodreadsSearchSuggestModal.ts @@ -1,4 +1,4 @@ -import GoodreadsSearchSuggestion from "@components/GoodreadsSearchSuggestion.svelte"; +import GoodreadsSearchSuggestion from "./GoodreadsSearchSuggestion.svelte"; import { type SearchResult } from "@data-sources/goodreads"; import { App, Notice, SuggestModal } from "obsidian"; import { mount } from "svelte"; @@ -12,7 +12,7 @@ export class GoodreadsSearchSuggestModal extends SuggestModal { super(app); } - getSuggestions(query: string): SearchResult[] | Promise { + getSuggestions(_query: string): SearchResult[] | Promise { return this.results; } diff --git a/src/components/GoodreadsSearchSuggestion.svelte b/src/ui/modals/GoodreadsSearchSuggestion.svelte similarity index 100% rename from src/components/GoodreadsSearchSuggestion.svelte rename to src/ui/modals/GoodreadsSearchSuggestion.svelte diff --git a/src/ui/modals/RatingModal.ts b/src/ui/modals/RatingModal.ts new file mode 100644 index 0000000..3e1e242 --- /dev/null +++ b/src/ui/modals/RatingModal.ts @@ -0,0 +1,19 @@ +import RatingModalView from "./RatingModalView.svelte"; +import { App } from "obsidian"; +import { SvelteModal } from "./SvelteModal"; + +export class RatingModal extends SvelteModal { + constructor(app: App, onSubmit: (rating: number) => void = () => {}) { + super(app, RatingModalView, { props: { onSubmit } }); + } + + static createAndOpen(app: App): Promise { + return new Promise((resolve) => { + const modal = new RatingModal(app, (rating) => { + modal.close(); + resolve(rating); + }); + modal.open(); + }); + } +} diff --git a/src/components/Rating.svelte b/src/ui/modals/RatingModalView.svelte similarity index 93% rename from src/components/Rating.svelte rename to src/ui/modals/RatingModalView.svelte index 4a712a9..f668b26 100644 --- a/src/components/Rating.svelte +++ b/src/ui/modals/RatingModalView.svelte @@ -1,5 +1,5 @@