generated from tpl/obsidian-sample-plugin
366 lines
9.2 KiB
TypeScript
366 lines
9.2 KiB
TypeScript
import { Notice, Plugin, requestUrl } from "obsidian";
|
|
import {
|
|
type BookTrackerPluginSettings,
|
|
BookTrackerSettingTab,
|
|
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 {
|
|
CONTENT_TYPE_EXTENSIONS,
|
|
IN_PROGRESS_STATE,
|
|
READ_STATE,
|
|
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";
|
|
|
|
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.app, this);
|
|
|
|
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.addSettingTab(new BookTrackerSettingTab(this.app, this));
|
|
|
|
this.registerMarkdownCodeBlockProcessor(
|
|
"readinglog",
|
|
renderCodeBlockProcessor(ReadingLogViewer, {
|
|
app: this.app,
|
|
readingLog: this.readingLog,
|
|
})
|
|
);
|
|
}
|
|
|
|
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<string> {
|
|
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.coverDirectory + "/";
|
|
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 createEntryFromGoodreads(legacyId: number): Promise<void> {
|
|
try {
|
|
const book = await getBookByLegacyId(legacyId);
|
|
|
|
const fileName = this.templater
|
|
.renderTemplate(this.settings.fileNameFormat, {
|
|
title: book.title,
|
|
authors: book.authors.map((a) => a.name).join(", "),
|
|
})
|
|
.replace(/[/\:*?<>|""]/g, "");
|
|
|
|
const data: Record<string, unknown> = { 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.tbrDirectory + "/" + fileName + ".md",
|
|
renderedContent
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to create book entry:", error);
|
|
}
|
|
}
|
|
|
|
async searchGoodreads(): Promise<void> {
|
|
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.pageLengthProperty) {
|
|
new Notice("Page length property is not set in settings.");
|
|
return;
|
|
}
|
|
|
|
const pageLength =
|
|
(this.app.metadataCache.getFileCache(activeFile)?.frontmatter?.[
|
|
this.settings.pageLengthProperty
|
|
] as number | undefined) ?? 0;
|
|
|
|
if (pageLength <= 0) {
|
|
new Notice(
|
|
"Page length property is not set or is invalid in the active file."
|
|
);
|
|
return;
|
|
}
|
|
|
|
const pageNumber = await ReadingProgressModal.createAndOpen(
|
|
this.app,
|
|
pageLength
|
|
);
|
|
|
|
if (pageNumber <= 0 || pageNumber > pageLength) {
|
|
new Notice(
|
|
`Invalid page number: ${pageNumber}. It must be between 1 and ${pageLength}.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
await this.readingLog.addEntry(fileName, pageNumber, pageLength);
|
|
new Notice(
|
|
`Logged reading progress for ${fileName}: Page ${pageNumber} of ${pageLength}.`
|
|
);
|
|
}
|
|
|
|
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 pageLength =
|
|
(this.app.metadataCache.getFileCache(activeFile)?.frontmatter?.[
|
|
this.settings.pageLengthProperty
|
|
] as number | undefined) ?? 0;
|
|
|
|
if (pageLength <= 0) {
|
|
new Notice(
|
|
"Page length property is not set or is invalid in the active file."
|
|
);
|
|
return;
|
|
}
|
|
|
|
const rating = await RatingModal.createAndOpen(this.app);
|
|
|
|
await this.readingLog.addEntry(
|
|
activeFile.basename,
|
|
pageLength,
|
|
pageLength
|
|
);
|
|
|
|
// @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;
|
|
});
|
|
|
|
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);
|
|
}
|
|
}
|