From 5a492f558cabecb1590b8342e28b7859c470685f Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Mon, 30 Jun 2025 16:09:43 -0400 Subject: [PATCH] Move commands into classes --- src/commands/Command.ts | 99 ++++++++ src/commands/LogReadingFinishedCommand.ts | 75 ++++++ src/commands/LogReadingProgressCommand.ts | 66 +++++ src/commands/LogReadingStartedCommand.ts | 47 ++++ src/commands/ResetReadingStatusCommand.ts | 50 ++++ src/commands/SearchGoodreadsCommand.ts | 47 ++++ src/main.ts | 283 +++------------------- tsconfig.json | 1 + 8 files changed, 418 insertions(+), 250 deletions(-) create mode 100644 src/commands/Command.ts create mode 100644 src/commands/LogReadingFinishedCommand.ts create mode 100644 src/commands/LogReadingProgressCommand.ts create mode 100644 src/commands/LogReadingStartedCommand.ts create mode 100644 src/commands/ResetReadingStatusCommand.ts create mode 100644 src/commands/SearchGoodreadsCommand.ts diff --git a/src/commands/Command.ts b/src/commands/Command.ts new file mode 100644 index 0000000..1d3d69a --- /dev/null +++ b/src/commands/Command.ts @@ -0,0 +1,99 @@ +import type { + Editor, + Hotkey, + MarkdownFileInfo, + MarkdownView, + Command as ObsidianCommand, +} from "obsidian"; + +export abstract class Command implements ObsidianCommand { + private _icon?: string; + public get icon(): string | undefined { + return this._icon; + } + protected setIcon(icon: string) { + this._icon = icon; + } + + private _mobileOnly?: boolean; + public get mobileOnly(): boolean | undefined { + return this._mobileOnly; + } + protected setMobileOnly(mobileOnly: boolean) { + this._mobileOnly = mobileOnly; + } + + private _repeatable?: boolean; + public get repeatable(): boolean | undefined { + return this._repeatable; + } + protected setRepeatable(repeatable: boolean) { + this._repeatable = repeatable; + } + + private _hotkeys: Hotkey[] = []; + public get hotkeys(): Hotkey[] { + return this._hotkeys; + } + protected addHotkey(hotkey: Hotkey) { + this._hotkeys.push(hotkey); + } + protected removeHotkey(hotkey: Hotkey) { + this._hotkeys = this._hotkeys.filter( + (h) => h.key !== hotkey.key && h.modifiers !== h.modifiers + ); + } + protected clearHotkeys() { + this._hotkeys = []; + } + + constructor(public id: string, public name: string) {} + + callback?(): any; + checkCallback?(checking: boolean): boolean; + editorCallback?( + editor: Editor, + ctx: MarkdownView | MarkdownFileInfo + ): boolean; + editorCheckCallback?( + checking: boolean, + editor: Editor, + ctx: MarkdownView | MarkdownFileInfo + ): boolean | void; +} + +export abstract class CheckCommand extends Command { + checkCallback(checking: boolean): boolean { + if (!this.check()) return false; + if (!checking) { + this.run(); + } + return true; + } + + protected abstract check(): boolean; + protected abstract run(): void | Promise; +} + +export abstract class EditorCheckCommand extends Command { + editorCheckCallback( + checking: boolean, + editor: Editor, + ctx: MarkdownView | MarkdownFileInfo + ): boolean | void { + if (!this.check(editor, ctx)) return false; + if (!checking) { + this.run(editor, ctx); + } + return true; + } + + protected abstract check( + editor: Editor, + ctx: MarkdownView | MarkdownFileInfo + ): boolean; + protected abstract run( + editor: Editor, + ctx: MarkdownView | MarkdownFileInfo + ): void | Promise; +} diff --git a/src/commands/LogReadingFinishedCommand.ts b/src/commands/LogReadingFinishedCommand.ts new file mode 100644 index 0000000..e9a9663 --- /dev/null +++ b/src/commands/LogReadingFinishedCommand.ts @@ -0,0 +1,75 @@ +import { + type Editor, + type MarkdownView, + type MarkdownFileInfo, + type App, + Notice, + TFile, +} from "obsidian"; +import { EditorCheckCommand } from "./Command"; +import type { BookTrackerPluginSettings } from "@ui/settings"; +import { RatingModal } from "@ui/modals"; +import type { ReadingLog } from "@utils/ReadingLog"; +import { READ_STATE } from "@src/const"; + +export class LogReadingFinishedCommand extends EditorCheckCommand { + constructor( + private readonly app: App, + private readonly readingLog: ReadingLog, + private readonly settings: BookTrackerPluginSettings + ) { + super("log-reading-finished", "Log Reading Finished"); + } + + private getPageCount(file: TFile): number { + return ( + (this.app.metadataCache.getFileCache(file)?.frontmatter?.[ + this.settings.pageCountProperty + ] as number | undefined) ?? 0 + ); + } + + protected check( + _editor: Editor, + ctx: MarkdownView | MarkdownFileInfo + ): boolean { + return !( + ctx.file === null || + this.settings.statusProperty === "" || + this.settings.endDateProperty === "" || + this.settings.ratingProperty === "" || + this.settings.pageCountProperty === "" || + this.getPageCount(ctx.file) <= 0 + ); + } + + protected async run( + _editor: Editor, + ctx: MarkdownView | MarkdownFileInfo + ): Promise { + const file = ctx.file!; + const fileName = file.basename; + const pageCount = this.getPageCount(file); + + const rating = await RatingModal.createAndOpen( + this.app, + this.settings.spiceProperty !== "" + ); + + await this.readingLog.addEntry(fileName, pageCount, pageCount); + + // @ts-expect-error Moment is provided by Obsidian + const endDate = moment().format("YYYY-MM-DD"); + + this.app.fileManager.processFrontMatter(file, (frontMatter) => { + frontMatter[this.settings.statusProperty] = READ_STATE; + frontMatter[this.settings.endDateProperty] = endDate; + frontMatter[this.settings.ratingProperty] = rating; + if (this.settings.spiceProperty !== "") { + frontMatter[this.settings.spiceProperty] = rating; + } + }); + + new Notice("Reading finished for " + fileName); + } +} diff --git a/src/commands/LogReadingProgressCommand.ts b/src/commands/LogReadingProgressCommand.ts new file mode 100644 index 0000000..6b8f833 --- /dev/null +++ b/src/commands/LogReadingProgressCommand.ts @@ -0,0 +1,66 @@ +import { + type Editor, + type MarkdownView, + type MarkdownFileInfo, + type App, + Notice, + TFile, +} from "obsidian"; +import { Command, EditorCheckCommand } from "./Command"; +import type { BookTrackerPluginSettings } from "@ui/settings"; +import { ReadingProgressModal } from "@ui/modals"; +import type { ReadingLog } from "@utils/ReadingLog"; + +export class LogReadingProgressCommand extends EditorCheckCommand { + constructor( + private readonly app: App, + private readonly readingLog: ReadingLog, + private readonly settings: BookTrackerPluginSettings + ) { + super("log-reading-progress", "Log Reading Progress"); + } + + private getPageCount(file: TFile): number { + return ( + (this.app.metadataCache.getFileCache(file)?.frontmatter?.[ + this.settings.pageCountProperty + ] as number | undefined) ?? 0 + ); + } + + protected check( + _editor: Editor, + ctx: MarkdownView | MarkdownFileInfo + ): boolean { + return !( + ctx.file === null || + this.settings.pageCountProperty === "" || + this.getPageCount(ctx.file) <= 0 + ); + } + + protected async run( + _editor: Editor, + ctx: MarkdownView | MarkdownFileInfo + ): Promise { + const file = ctx.file!; + const fileName = file.basename; + const pageCount = this.getPageCount(file); + const pageNumber = await ReadingProgressModal.createAndOpen( + this.app, + pageCount + ); + + if (pageNumber <= 0 || pageNumber > pageCount) { + new Notice( + `Invalid page number: ${pageNumber}. It must be between 1 and ${pageCount}.` + ); + return; + } + + await this.readingLog.addEntry(fileName, pageNumber, pageCount); + new Notice( + `Logged reading progress for ${fileName}: Page ${pageNumber} of ${pageCount}.` + ); + } +} diff --git a/src/commands/LogReadingStartedCommand.ts b/src/commands/LogReadingStartedCommand.ts new file mode 100644 index 0000000..93938dd --- /dev/null +++ b/src/commands/LogReadingStartedCommand.ts @@ -0,0 +1,47 @@ +import { + type Editor, + type MarkdownView, + type MarkdownFileInfo, + type App, + Notice, +} from "obsidian"; +import { EditorCheckCommand } from "./Command"; +import type { BookTrackerPluginSettings } from "@ui/settings"; +import { IN_PROGRESS_STATE } from "@src/const"; + +export class LogReadingStartedCommand extends EditorCheckCommand { + constructor( + private readonly app: App, + private readonly settings: BookTrackerPluginSettings + ) { + super("log-reading-started", "Log Reading Started"); + } + + protected check( + _editor: Editor, + ctx: MarkdownView | MarkdownFileInfo + ): boolean { + return !( + ctx.file === null || + this.settings.statusProperty === "" || + this.settings.startDateProperty === "" + ); + } + + protected async run( + _editor: Editor, + ctx: MarkdownView | MarkdownFileInfo + ): Promise { + const file = ctx.file!; + + // @ts-expect-error Moment is provided by Obsidian + const startDate = moment().format("YYYY-MM-DD"); + + this.app.fileManager.processFrontMatter(file, (frontMatter) => { + frontMatter[this.settings.statusProperty] = IN_PROGRESS_STATE; + frontMatter[this.settings.startDateProperty] = startDate; + }); + + new Notice("Reading started for " + file.basename); + } +} diff --git a/src/commands/ResetReadingStatusCommand.ts b/src/commands/ResetReadingStatusCommand.ts new file mode 100644 index 0000000..4a0ab5f --- /dev/null +++ b/src/commands/ResetReadingStatusCommand.ts @@ -0,0 +1,50 @@ +import { + Notice, + type App, + type Editor, + type MarkdownFileInfo, + type MarkdownView, +} from "obsidian"; +import { Command, EditorCheckCommand } from "./Command"; +import type { BookTrackerPluginSettings } from "@ui/settings"; +import type { ReadingLog } from "@utils/ReadingLog"; +import { TO_BE_READ_STATE } from "@src/const"; + +export class ResetReadingStatusCommand extends EditorCheckCommand { + constructor( + private readonly app: App, + private readonly readingLog: ReadingLog, + private readonly settings: BookTrackerPluginSettings + ) { + super("reset-reading-status", "Reset Reading Status"); + } + + protected check( + _editor: Editor, + ctx: MarkdownView | MarkdownFileInfo + ): boolean { + return !( + ctx.file === null || + this.settings.statusProperty === "" || + this.settings.startDateProperty === "" || + this.settings.endDateProperty === "" + ); + } + + protected run( + _editor: Editor, + ctx: MarkdownView | MarkdownFileInfo + ): void | Promise { + const file = ctx.file!; + + this.app.fileManager.processFrontMatter(file, (frontMatter) => { + frontMatter[this.settings.statusProperty] = TO_BE_READ_STATE; + frontMatter[this.settings.startDateProperty] = ""; + frontMatter[this.settings.endDateProperty] = ""; + }); + + this.readingLog.removeEntries(file.basename); + + new Notice("Reading status reset for " + file.basename); + } +} diff --git a/src/commands/SearchGoodreadsCommand.ts b/src/commands/SearchGoodreadsCommand.ts new file mode 100644 index 0000000..c15378e --- /dev/null +++ b/src/commands/SearchGoodreadsCommand.ts @@ -0,0 +1,47 @@ +import { getBookByLegacyId, type SearchResult } from "@data-sources/goodreads"; +import { GoodreadsSearchModal, GoodreadsSearchSuggestModal } from "@ui/modals"; +import { App, Notice } from "obsidian"; +import { Command } from "./Command"; +import type { Book } from "@src/types"; + +export class SearchGoodreadsCommand extends Command { + constructor( + private readonly app: App, + private readonly cb: (book: Book) => void + ) { + super("search-goodreads", "Search Goodreads"); + } + + async callback() { + let results: SearchResult[]; + try { + results = await GoodreadsSearchModal.createAndOpen(this.app); + } catch (error) { + console.error("Failed to search Goodreads:", error); + new Notice( + "Failed to search Goodreads. Check console for details." + ); + return; + } + + const selectedResult = await GoodreadsSearchSuggestModal.createAndOpen( + this.app, + results + ); + if (!selectedResult) { + new Notice("No book selected."); + return; + } + + let book: Book; + try { + book = await getBookByLegacyId(selectedResult.legacyId); + } catch (error) { + console.error("Failed to get book:", error); + new Notice("Failed to get book. Check console for details."); + return; + } + + this.cb(book); + } +} diff --git a/src/main.ts b/src/main.ts index eac44d1..02c5086 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,23 +4,17 @@ import { DEFAULT_SETTINGS, BookTrackerSettingTab, } from "@ui/settings"; -import { getBookByLegacyId, type SearchResult } from "@data-sources/goodreads"; import { Templater } from "@utils/Templater"; -import { - GoodreadsSearchModal, - GoodreadsSearchSuggestModal, - ReadingProgressModal, - RatingModal, -} from "@ui/modals"; -import { - CONTENT_TYPE_EXTENSIONS, - IN_PROGRESS_STATE, - READ_STATE, - TO_BE_READ_STATE, -} from "./const"; +import { CONTENT_TYPE_EXTENSIONS } from "./const"; import { Storage } from "@utils/Storage"; import { ReadingLog } from "@utils/ReadingLog"; import { registerReadingLogCodeBlockProcessor } from "@ui/code-blocks"; +import type { Book } from "./types"; +import { SearchGoodreadsCommand } from "@commands/SearchGoodreadsCommand"; +import { LogReadingStartedCommand } from "@commands/LogReadingStartedCommand"; +import { LogReadingProgressCommand } from "@commands/LogReadingProgressCommand"; +import { LogReadingFinishedCommand } from "@commands/LogReadingFinishedCommand"; +import { ResetReadingStatusCommand } from "@commands/ResetReadingStatusCommand"; export default class BookTrackerPlugin extends Plugin { settings: BookTrackerPluginSettings; @@ -35,35 +29,31 @@ export default class BookTrackerPlugin extends Plugin { this.storage = new Storage(this.app, this); this.readingLog = new ReadingLog(this.storage); - this.addCommand({ - id: "search-goodreads", - name: "Search Goodreads", - callback: () => this.searchGoodreads(), - }); - - this.addCommand({ - id: "log-reading-started", - name: "Log Reading Started", - callback: () => this.logReadingStarted(), - }); - - this.addCommand({ - id: "log-reading-progress", - name: "Log Reading Progress", - callback: () => this.logReadingProgress(), - }); - - this.addCommand({ - id: "log-reading-completed", - name: "Log Reading Completed", - callback: () => this.logReadingFinished(), - }); - - this.addCommand({ - id: "reset-reading-status", - name: "Reset Reading Status", - callback: () => this.resetReadingStatus(), - }); + this.addCommand( + new SearchGoodreadsCommand(this.app, this.createEntry.bind(this)) + ); + this.addCommand(new LogReadingStartedCommand(this.app, this.settings)); + this.addCommand( + new LogReadingProgressCommand( + this.app, + this.readingLog, + this.settings + ) + ); + this.addCommand( + new LogReadingFinishedCommand( + this.app, + this.readingLog, + this.settings + ) + ); + this.addCommand( + new ResetReadingStatusCommand( + this.app, + this.readingLog, + this.settings + ) + ); this.addSettingTab(new BookTrackerSettingTab(this)); @@ -124,10 +114,8 @@ export default class BookTrackerPlugin extends Plugin { return filePath; } - async createEntryFromGoodreads(legacyId: number): Promise { + async createEntry(book: Book): Promise { try { - const book = await getBookByLegacyId(legacyId); - const fileName = this.templater .renderTemplate(this.settings.fileNameFormat, { title: book.title, @@ -159,209 +147,4 @@ export default class BookTrackerPlugin extends Plugin { console.error("Failed to create book entry:", error); } } - - async searchGoodreads(): Promise { - let results: SearchResult[]; - try { - results = await GoodreadsSearchModal.createAndOpen(this.app); - } catch (error) { - console.error("Failed to search Goodreads:", error); - new Notice( - "Failed to search Goodreads. Check console for details." - ); - return; - } - const selectedBook = await GoodreadsSearchSuggestModal.createAndOpen( - this.app, - results - ); - - if (selectedBook) { - await this.createEntryFromGoodreads(selectedBook.legacyId); - } else { - new Notice("No book selected."); - } - } - - logReadingStarted(): void { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) { - new Notice("No active file to mark as currently reading."); - return; - } - - if (activeFile.extension !== "md") { - new Notice("Active file is not a markdown file."); - return; - } - - if (!this.settings.statusProperty) { - new Notice("Status property is not set in settings."); - return; - } - - if (!this.settings.startDateProperty) { - new Notice("Start date property is not set in settings."); - return; - } - - // @ts-expect-error Moment is provided by Obsidian - const startDate = moment().format("YYYY-MM-DD"); - - this.app.fileManager.processFrontMatter(activeFile, (frontMatter) => { - frontMatter[this.settings.statusProperty] = IN_PROGRESS_STATE; - frontMatter[this.settings.startDateProperty] = startDate; - }); - - new Notice("Reading started for " + activeFile.name); - } - - async logReadingProgress() { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) { - new Notice("No active file to log reading progress."); - return; - } - - if (activeFile.extension !== "md") { - new Notice("Active file is not a markdown file."); - return; - } - - const fileName = activeFile.basename; - if (!this.settings.pageCountProperty) { - new Notice("Page count property is not set in settings."); - return; - } - - const pageCount = - (this.app.metadataCache.getFileCache(activeFile)?.frontmatter?.[ - this.settings.pageCountProperty - ] as number | undefined) ?? 0; - - if (pageCount <= 0) { - new Notice( - "Page length property is not set or is invalid in the active file." - ); - return; - } - - const pageNumber = await ReadingProgressModal.createAndOpen( - this.app, - pageCount - ); - - if (pageNumber <= 0 || pageNumber > pageCount) { - new Notice( - `Invalid page number: ${pageNumber}. It must be between 1 and ${pageCount}.` - ); - return; - } - - await this.readingLog.addEntry(fileName, pageNumber, pageCount); - new Notice( - `Logged reading progress for ${fileName}: Page ${pageNumber} of ${pageCount}.` - ); - } - - async logReadingFinished() { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) { - new Notice("No active file to mark as finished reading."); - return; - } - - if (activeFile.extension !== "md") { - new Notice("Active file is not a markdown file."); - return; - } - - if (!this.settings.statusProperty) { - new Notice("Status property is not set in settings."); - return; - } - - if (!this.settings.endDateProperty) { - new Notice("End date property is not set in settings."); - return; - } - - if (!this.settings.ratingProperty) { - new Notice("Rating property is not set in settings."); - return; - } - - const pageCount = - (this.app.metadataCache.getFileCache(activeFile)?.frontmatter?.[ - this.settings.pageCountProperty - ] as number | undefined) ?? 0; - - if (pageCount <= 0) { - new Notice( - "Page count property is not set or is invalid in the active file." - ); - return; - } - - const rating = await RatingModal.createAndOpen( - this.app, - this.settings.spiceProperty !== "" - ); - - await this.readingLog.addEntry( - activeFile.basename, - pageCount, - pageCount - ); - - // @ts-expect-error Moment is provided by Obsidian - const endDate = moment().format("YYYY-MM-DD"); - - this.app.fileManager.processFrontMatter(activeFile, (frontMatter) => { - frontMatter[this.settings.statusProperty] = READ_STATE; - frontMatter[this.settings.endDateProperty] = endDate; - frontMatter[this.settings.ratingProperty] = rating; - if (this.settings.spiceProperty !== "") { - frontMatter[this.settings.spiceProperty] = rating; - } - }); - - new Notice("Reading finished for " + activeFile.name); - } - - resetReadingStatus(): any { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) { - new Notice("No active file to reset reading status."); - return; - } - - if (activeFile.extension !== "md") { - new Notice("Active file is not a markdown file."); - return; - } - - if (!this.settings.statusProperty) { - new Notice("Status property is not set in settings."); - return; - } - - if (!this.settings.startDateProperty) { - new Notice("Start date property is not set in settings."); - return; - } - - if (!this.settings.endDateProperty) { - new Notice("End date property is not set in settings."); - return; - } - - this.app.fileManager.processFrontMatter(activeFile, (frontMatter) => { - frontMatter[this.settings.statusProperty] = TO_BE_READ_STATE; - frontMatter[this.settings.startDateProperty] = ""; - frontMatter[this.settings.endDateProperty] = ""; - }); - - new Notice("Reading status reset for " + activeFile.name); - } } diff --git a/tsconfig.json b/tsconfig.json index decc67a..b8cec7a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,7 @@ "ES7" ], "paths": { + "@commands/*": ["src/commands/*"], "@data-sources/*": ["src/data-sources/*"], "@ui/*": ["src/ui/*"], "@utils/*": ["src/utils/*"],