import { Notice, Plugin, requestUrl, TFile } 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, registerReadingStatsCodeBlockProcessor, } 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"; import { BackupReadingLogCommand } from "@commands/CreateReadingLogBackupCommand"; import { RestoreReadingLogBackupCommand } from "@commands/RestoreReadingLogBackupCommand"; import { Goodreads } from "@data-sources/Goodreads"; import { CreateBookFromGoodreadsUrlCommand } from "@commands/CreateBookFromGoodreadsUrlCommand"; import { registerShelfCodeBlockProcessor } from "@ui/code-blocks/ShelfCodeBlock"; import { ReloadReadingLogCommand } from "@commands/ReloadReadingLogCommand"; import { registerReadingCalendarCodeBlockProcessor } from "@ui/code-blocks/ReadingCalendarCodeBlock"; import { registerAToZChallengeCodeBlockProcessor } from "@ui/code-blocks/AToZChallengeCodeBlock"; export default class BookTrackerPlugin extends Plugin { public settings: BookTrackerPluginSettings; public templater: Templater; public storage: Storage; public readingLog: ReadingLog; public goodreads: Goodreads = new Goodreads(); async onload() { await this.loadSettings(); this.templater = new Templater(this.app); this.storage = new Storage(this); this.readingLog = new ReadingLog(this.storage); this.addCommand( new SearchGoodreadsCommand( this.app, this.goodreads, 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.addCommand(new BackupReadingLogCommand(this.readingLog)); this.addCommand( new RestoreReadingLogBackupCommand( this.app, this.storage, this.readingLog ) ); this.addCommand( new CreateBookFromGoodreadsUrlCommand( this.goodreads, this.createEntry.bind(this) ) ); this.addCommand(new ReloadReadingLogCommand(this.readingLog)); this.addSettingTab(new BookTrackerSettingTab(this)); registerReadingLogCodeBlockProcessor(this); registerReadingStatsCodeBlockProcessor(this); registerShelfCodeBlockProcessor(this); registerReadingCalendarCodeBlockProcessor(this); registerAToZChallengeCodeBlockProcessor(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, overwrite?: boolean ): 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 || overwrite) { await this.app.vault.modifyBinary( existingFile, response.arrayBuffer ); } else { new Notice("Cover image already exists: " + filePath); } return existingFile; } return await this.app.vault.createBinary( filePath, response.arrayBuffer ); } async createEntry(book: Book): Promise { 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) { const coverImageFile = await this.downloadCoverImage( book.coverImageUrl, fileName ); data.coverImagePath = coverImageFile.path; } const renderedContent = await this.templater.renderTemplateFile( this.settings.templateFile, data ); const filePath = this.settings.tbrFolder + "/" + fileName + ".md"; const file = await this.app.vault.create(filePath, renderedContent); await this.app.workspace.getLeaf().openFile(file); } }