import { Notice, Plugin, requestUrl } from "obsidian"; import { type BookTrackerPluginSettings, DEFAULT_SETTINGS, BookTrackerSettingTab, } from "@ui/settings"; import { Templater } from "@utils/Templater"; 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; templater: Templater; storage: Storage; readingLog: ReadingLog; async onload() { await this.loadSettings(); this.templater = new Templater(this.app); this.storage = new Storage(this.app, this); this.readingLog = new ReadingLog(this.storage); 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)); registerReadingLogCodeBlockProcessor(this); } onunload() {} async loadSettings() { this.settings = Object.assign( {}, DEFAULT_SETTINGS, await this.loadData() ); } async saveSettings() { await this.saveData(this.settings); } async downloadCoverImage( coverImageUrl: string, fileName: string ): Promise { const response = await requestUrl(coverImageUrl); const contentType = response.headers["content-type"]; const extension = CONTENT_TYPE_EXTENSIONS[contentType || ""] || ""; if (extension === "") { throw new Error("Unsupported content type: " + contentType); } let filePath = this.settings.coverFolder + "/"; if (this.settings.groupCoversByFirstLetter) { let groupName = fileName.charAt(0).toUpperCase(); if (!/^[A-Z]$/.test(groupName)) { groupName = "#"; } filePath += groupName + "/"; } filePath += fileName + "." + extension; const existingFile = this.app.vault.getFileByPath(filePath); if (existingFile) { if (this.settings.overwriteExistingCovers) { await this.app.vault.modifyBinary( existingFile, response.arrayBuffer ); } else { new Notice("Cover image already exists: " + filePath); return filePath; } } await this.app.vault.createBinary(filePath, response.arrayBuffer); return filePath; } async createEntry(book: Book): Promise { try { const fileName = this.templater .renderTemplate(this.settings.fileNameFormat, { title: book.title, authors: book.authors.map((a) => a.name).join(", "), }) .replace(/[/\:*?<>|""]/g, ""); const data: Record = { book }; if (this.settings.downloadCovers && book.coverImageUrl) { data.coverImagePath = await this.downloadCoverImage( book.coverImageUrl, fileName ); } const renderedContent = await this.templater.renderTemplateFile( this.settings.templateFile, data ); if (renderedContent) { await this.app.vault.create( this.settings.tbrFolder + "/" + fileName + ".md", renderedContent ); } } catch (error) { console.error("Failed to create book entry:", error); } } }