generated from tpl/obsidian-sample-plugin
Add redownload cover command and move image functions to services
This commit is contained in:
parent
210ba33297
commit
2f89a703d3
|
|
@ -0,0 +1,34 @@
|
||||||
|
import type { BookTrackerPluginSettings } from "@ui/settings";
|
||||||
|
import { Notice, type Editor, type MarkdownFileInfo, type MarkdownView, type MetadataCache } from "obsidian";
|
||||||
|
import { CoverImageDownloaderService } from "../services/CoverImageDownloaderService";
|
||||||
|
import { EditorCheckCommand } from "./Command";
|
||||||
|
|
||||||
|
export class RedownloadCoverCommand extends EditorCheckCommand {
|
||||||
|
constructor(
|
||||||
|
private readonly metadataCache: MetadataCache,
|
||||||
|
private readonly settings: BookTrackerPluginSettings,
|
||||||
|
private readonly downloader: CoverImageDownloaderService,
|
||||||
|
) {
|
||||||
|
super("redownload-cover", "Redownload Cover from Current Image URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected check(_editor: Editor, ctx: MarkdownView | MarkdownFileInfo): boolean {
|
||||||
|
return ctx.file != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async run(_editor: Editor, ctx: MarkdownView | MarkdownFileInfo): Promise<void> {
|
||||||
|
const file = ctx.file!;
|
||||||
|
const fm = this.metadataCache.getFileCache(file)?.frontmatter!;
|
||||||
|
const url = fm[this.settings.coverImageUrlProperty];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.downloader.download(url, file.basename, true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to download cover image:", error);
|
||||||
|
new Notice("Failed to download cover image. Check console for details.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new Notice("Fetched newest cover image.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { type Editor, type MarkdownView, type MarkdownFileInfo, type App, type TFile, Notice } from "obsidian";
|
import type { CoverImageDownloaderService } from "@services/CoverImageDownloaderService";
|
||||||
import { EditorCheckCommand } from "./Command";
|
|
||||||
import type { BookTrackerPluginSettings } from "@ui/settings";
|
import type { BookTrackerPluginSettings } from "@ui/settings";
|
||||||
|
import { Notice, type Editor, type FileManager, type MarkdownFileInfo, type MarkdownView, type TFile } from "obsidian";
|
||||||
|
import { EditorCheckCommand } from "./Command";
|
||||||
|
|
||||||
export class UpdateCoverFromURLCommand extends EditorCheckCommand {
|
export class UpdateCoverFromURLCommand extends EditorCheckCommand {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly app: App,
|
private readonly fileManager: FileManager,
|
||||||
private readonly settings: BookTrackerPluginSettings,
|
private readonly settings: BookTrackerPluginSettings,
|
||||||
private readonly downloadCoverImage: (url: string, fileName: string, overwrite?: boolean) => Promise<TFile>,
|
private readonly downloader: CoverImageDownloaderService,
|
||||||
) {
|
) {
|
||||||
super("update-cover-from-url", "Update Cover from URL");
|
super("update-cover-from-url", "Update Cover from URL");
|
||||||
}
|
}
|
||||||
|
|
@ -21,14 +22,14 @@ export class UpdateCoverFromURLCommand extends EditorCheckCommand {
|
||||||
|
|
||||||
let coverFile: TFile;
|
let coverFile: TFile;
|
||||||
try {
|
try {
|
||||||
coverFile = await this.downloadCoverImage(url, file.basename, true);
|
coverFile = await this.downloader.download(url, file.basename, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to download cover image:", error);
|
console.error("Failed to download cover image:", error);
|
||||||
new Notice("Failed to download cover image. Check console for details.");
|
new Notice("Failed to download cover image. Check console for details.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.app.fileManager.processFrontMatter(file, (fm) => {
|
this.fileManager.processFrontMatter(file, (fm) => {
|
||||||
fm[this.settings.coverImageUrlProperty] = url;
|
fm[this.settings.coverImageUrlProperty] = url;
|
||||||
});
|
});
|
||||||
new Notice("Updated cover image.")
|
new Notice("Updated cover image.")
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,34 @@ export interface SearchResult {
|
||||||
export class Goodreads {
|
export class Goodreads {
|
||||||
async getNextData(legacyId: number): Promise<NextData> {
|
async getNextData(legacyId: number): Promise<NextData> {
|
||||||
const url = "https://www.goodreads.com/book/show/" + legacyId;
|
const url = "https://www.goodreads.com/book/show/" + legacyId;
|
||||||
const res = await requestUrl({ url });
|
const res = await requestUrl({
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
"Accept": "text/html, application/json",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Device-Memory": "8",
|
||||||
|
"Downlink": "10",
|
||||||
|
"Dpr": "2",
|
||||||
|
"Ect": "4g",
|
||||||
|
"Origin": "https://www.amazon.com",
|
||||||
|
"Priority": "u=1, i",
|
||||||
|
"Rtt": "50",
|
||||||
|
"Sec-Ch-Device-Memory": "8",
|
||||||
|
"Sec-Ch-Dpr": "2",
|
||||||
|
"Sec-Ch-Ua": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
|
||||||
|
"Sec-Ch-Ua-Mobile": "?0",
|
||||||
|
"Sec-Ch-Ua-Platform": "\"macOS\"",
|
||||||
|
"Sec-Ch-Viewport-Width": "1170",
|
||||||
|
"Sec-Fetch-Dest": "empty",
|
||||||
|
"Sec-Fetch-Mode": "cors",
|
||||||
|
"Sec-Fetch-Site": "same-origin",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||||
|
"Viewport-Width": "1170",
|
||||||
|
"X-Amz-Amabot-Click-Attributes": "disable",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
},
|
||||||
|
});
|
||||||
const doc = new DOMParser().parseFromString(res.text, "text/html");
|
const doc = new DOMParser().parseFromString(res.text, "text/html");
|
||||||
const nextDataRaw = doc.getElementById("__NEXT_DATA__")?.textContent;
|
const nextDataRaw = doc.getElementById("__NEXT_DATA__")?.textContent;
|
||||||
if (typeof nextDataRaw !== "string") {
|
if (typeof nextDataRaw !== "string") {
|
||||||
|
|
|
||||||
132
src/main.ts
132
src/main.ts
|
|
@ -1,41 +1,39 @@
|
||||||
import {
|
import { CreateBookFromGoodreadsUrlCommand } from "@commands/CreateBookFromGoodreadsUrlCommand";
|
||||||
Notice,
|
import { BackupReadingLogCommand } from "@commands/CreateReadingLogBackupCommand";
|
||||||
Plugin,
|
import { LogReadingFinishedCommand } from "@commands/LogReadingFinishedCommand";
|
||||||
requestUrl,
|
import { LogReadingProgressCommand } from "@commands/LogReadingProgressCommand";
|
||||||
TFile,
|
import { LogReadingStartedCommand } from "@commands/LogReadingStartedCommand";
|
||||||
type FrontMatterCache,
|
import { RedownloadCoverCommand } from "@commands/RedownloadCoverCommand";
|
||||||
} from "obsidian";
|
import { ReloadReadingLogCommand } from "@commands/ReloadReadingLogCommand";
|
||||||
import {
|
import { ResetReadingStatusCommand } from "@commands/ResetReadingStatusCommand";
|
||||||
type BookTrackerPluginSettings,
|
import { RestoreReadingLogBackupCommand } from "@commands/RestoreReadingLogBackupCommand";
|
||||||
DEFAULT_SETTINGS,
|
import { SearchGoodreadsCommand } from "@commands/SearchGoodreadsCommand";
|
||||||
BookTrackerSettingTab,
|
import { UpdateCoverFromURLCommand } from "@commands/UpdateCoverFromURLCommand";
|
||||||
} from "@ui/settings";
|
import { Goodreads } from "@data-sources/Goodreads";
|
||||||
import { safeString, Templater } from "@utils/Templater";
|
import moment from "@external/moment";
|
||||||
import { CONTENT_TYPE_EXTENSIONS } from "./const";
|
|
||||||
import { Storage } from "@utils/Storage";
|
|
||||||
import { ReadingLog } from "@utils/ReadingLog";
|
|
||||||
import {
|
import {
|
||||||
registerReadingLogCodeBlockProcessor,
|
registerReadingLogCodeBlockProcessor,
|
||||||
registerReadingStatsCodeBlockProcessor,
|
registerReadingStatsCodeBlockProcessor,
|
||||||
} from "@ui/code-blocks";
|
} from "@ui/code-blocks";
|
||||||
import type { Book, BookMetadata, ReadingState } 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";
|
import { registerAToZChallengeCodeBlockProcessor } from "@ui/code-blocks/AToZChallengeCodeBlock";
|
||||||
import moment from "@external/moment";
|
import { registerReadingCalendarCodeBlockProcessor } from "@ui/code-blocks/ReadingCalendarCodeBlock";
|
||||||
import { compressImage } from "@utils/image";
|
import { registerShelfCodeBlockProcessor } from "@ui/code-blocks/ShelfCodeBlock";
|
||||||
import { titleSortValue } from "@utils/text";
|
import {
|
||||||
import { UpdateCoverFromURLCommand } from "@commands/UpdateCoverFromURLCommand";
|
BookTrackerSettingTab,
|
||||||
|
DEFAULT_SETTINGS,
|
||||||
|
type BookTrackerPluginSettings,
|
||||||
|
} from "@ui/settings";
|
||||||
|
import { ReadingLog } from "@utils/ReadingLog";
|
||||||
|
import { Storage } from "@utils/Storage";
|
||||||
|
import { Templater, safeString } from "@utils/Templater";
|
||||||
|
import {
|
||||||
|
Plugin,
|
||||||
|
TFile,
|
||||||
|
type FrontMatterCache,
|
||||||
|
} from "obsidian";
|
||||||
|
import { CoverImageDownloaderService } from "./services/CoverImageDownloaderService";
|
||||||
|
import { ImageCompressorService } from "./services/ImageCompressorService";
|
||||||
|
import type { Book, BookMetadata, ReadingState } from "./types";
|
||||||
|
|
||||||
export default class BookTrackerPlugin extends Plugin {
|
export default class BookTrackerPlugin extends Plugin {
|
||||||
public settings: BookTrackerPluginSettings;
|
public settings: BookTrackerPluginSettings;
|
||||||
|
|
@ -43,6 +41,8 @@ export default class BookTrackerPlugin extends Plugin {
|
||||||
public storage: Storage;
|
public storage: Storage;
|
||||||
public readingLog: ReadingLog;
|
public readingLog: ReadingLog;
|
||||||
public goodreads: Goodreads = new Goodreads();
|
public goodreads: Goodreads = new Goodreads();
|
||||||
|
public imageCompressor: ImageCompressorService;
|
||||||
|
public coverImageDownloader: CoverImageDownloaderService;
|
||||||
|
|
||||||
private onSettingsSavedHandlers: ((
|
private onSettingsSavedHandlers: ((
|
||||||
settings: BookTrackerPluginSettings
|
settings: BookTrackerPluginSettings
|
||||||
|
|
@ -58,6 +58,8 @@ export default class BookTrackerPlugin extends Plugin {
|
||||||
this.templater = new Templater(this.app);
|
this.templater = new Templater(this.app);
|
||||||
this.storage = new Storage(this);
|
this.storage = new Storage(this);
|
||||||
this.readingLog = new ReadingLog(this.storage);
|
this.readingLog = new ReadingLog(this.storage);
|
||||||
|
this.imageCompressor = new ImageCompressorService(this.app.vault);
|
||||||
|
this.coverImageDownloader = new CoverImageDownloaderService(this.app.vault, this.app.fileManager, this.imageCompressor, this.settings);
|
||||||
|
|
||||||
this.addCommand(
|
this.addCommand(
|
||||||
new SearchGoodreadsCommand(
|
new SearchGoodreadsCommand(
|
||||||
|
|
@ -91,7 +93,8 @@ export default class BookTrackerPlugin extends Plugin {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
this.addCommand(new ReloadReadingLogCommand(this.readingLog));
|
this.addCommand(new ReloadReadingLogCommand(this.readingLog));
|
||||||
this.addCommand(new UpdateCoverFromURLCommand(this.app, this.settings, this.downloadCoverImage.bind(this)));
|
this.addCommand(new UpdateCoverFromURLCommand(this.app.fileManager, this.settings, this.coverImageDownloader));
|
||||||
|
this.addCommand(new RedownloadCoverCommand(this.app.metadataCache, this.settings, this.coverImageDownloader));
|
||||||
|
|
||||||
this.addSettingTab(new BookTrackerSettingTab(this));
|
this.addSettingTab(new BookTrackerSettingTab(this));
|
||||||
|
|
||||||
|
|
@ -145,65 +148,6 @@ export default class BookTrackerPlugin extends Plugin {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadCoverImage(
|
|
||||||
coverImageUrl: string,
|
|
||||||
fileName: string,
|
|
||||||
overwrite?: boolean
|
|
||||||
): Promise<TFile> {
|
|
||||||
const response = await requestUrl(coverImageUrl);
|
|
||||||
const header = Object.keys(response.headers).find(k => k.toLowerCase() === "content-type") ?? "Content-Type";
|
|
||||||
const contentType = response.headers[header] ?? "application/octet-stream";
|
|
||||||
|
|
||||||
if (!contentType.startsWith("image/") && contentType !== "application/octet-stream") {
|
|
||||||
throw new Error("Unexpected content type: " + contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlExtension = coverImageUrl.split(".").pop();
|
|
||||||
const extension =
|
|
||||||
CONTENT_TYPE_EXTENSIONS[contentType || ""] ?? urlExtension ?? "jpg";
|
|
||||||
|
|
||||||
let filePath = this.settings.coverFolder + "/";
|
|
||||||
if (this.settings.groupCoversByFirstLetter) {
|
|
||||||
let groupName = titleSortValue(fileName).charAt(0).toUpperCase();
|
|
||||||
if (!/^[A-Z]$/.test(groupName)) {
|
|
||||||
groupName = "#";
|
|
||||||
}
|
|
||||||
|
|
||||||
filePath += groupName + "/";
|
|
||||||
}
|
|
||||||
filePath += fileName + "." + extension;
|
|
||||||
|
|
||||||
let file = this.app.vault.getFileByPath(filePath);
|
|
||||||
if (file) {
|
|
||||||
if (this.settings.overwriteExistingCovers || overwrite) {
|
|
||||||
await this.app.vault.modifyBinary(file, response.arrayBuffer);
|
|
||||||
} else {
|
|
||||||
new Notice("Cover image already exists: " + filePath);
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
file = await this.app.vault.createBinary(
|
|
||||||
filePath,
|
|
||||||
response.arrayBuffer
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await compressImage(this.app, file, {
|
|
||||||
height: 400,
|
|
||||||
quality: 0.8,
|
|
||||||
maintainAspectRatio: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (file.extension !== "jpg") {
|
|
||||||
await this.app.fileManager.renameFile(
|
|
||||||
file,
|
|
||||||
file.path.replace(/\.[^.]+$/, ".jpg")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createEntry(book: Book): Promise<void> {
|
async createEntry(book: Book): Promise<void> {
|
||||||
const fileName = this.templater
|
const fileName = this.templater
|
||||||
.renderTemplate(this.settings.fileNameFormat, {
|
.renderTemplate(this.settings.fileNameFormat, {
|
||||||
|
|
@ -239,7 +183,7 @@ export default class BookTrackerPlugin extends Plugin {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.settings.downloadCovers && book.coverImageUrl) {
|
if (this.settings.downloadCovers && book.coverImageUrl) {
|
||||||
const coverImageFile = await this.downloadCoverImage(
|
const coverImageFile = await this.coverImageDownloader.download(
|
||||||
book.coverImageUrl,
|
book.coverImageUrl,
|
||||||
fileName
|
fileName
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { CONTENT_TYPE_EXTENSIONS } from "@src/const";
|
||||||
|
import type { BookTrackerPluginSettings } from "@ui/settings";
|
||||||
|
import { titleSortValue } from "@utils/text";
|
||||||
|
import { Notice, requestUrl, type FileManager, type TFile, type Vault } from "obsidian";
|
||||||
|
import type { ImageCompressorService } from "./ImageCompressorService";
|
||||||
|
|
||||||
|
export class CoverImageDownloaderService {
|
||||||
|
constructor(
|
||||||
|
private readonly vault: Vault,
|
||||||
|
private readonly fileManager: FileManager,
|
||||||
|
private readonly compressor: ImageCompressorService,
|
||||||
|
private readonly settings: BookTrackerPluginSettings,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async download(
|
||||||
|
coverImageUrl: string,
|
||||||
|
fileName: string,
|
||||||
|
overwrite?: boolean
|
||||||
|
): Promise<TFile> {
|
||||||
|
const response = await requestUrl(coverImageUrl);
|
||||||
|
const header = Object.keys(response.headers).find(k => k.toLowerCase() === "content-type") ?? "Content-Type";
|
||||||
|
const contentType = response.headers[header] ?? "application/octet-stream";
|
||||||
|
|
||||||
|
if (!contentType.startsWith("image/") && contentType !== "application/octet-stream") {
|
||||||
|
throw new Error("Unexpected content type: " + contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlExtension = coverImageUrl.split(".").pop();
|
||||||
|
const extension =
|
||||||
|
CONTENT_TYPE_EXTENSIONS[contentType || ""] ?? urlExtension ?? "jpg";
|
||||||
|
|
||||||
|
let filePath = this.settings.coverFolder + "/";
|
||||||
|
if (this.settings.groupCoversByFirstLetter) {
|
||||||
|
let groupName = titleSortValue(fileName).charAt(0).toUpperCase();
|
||||||
|
if (!/^[A-Z]$/.test(groupName)) {
|
||||||
|
groupName = "#";
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath += groupName + "/";
|
||||||
|
}
|
||||||
|
filePath += fileName + "." + extension;
|
||||||
|
|
||||||
|
let file = this.vault.getFileByPath(filePath);
|
||||||
|
if (file) {
|
||||||
|
if (this.settings.overwriteExistingCovers || overwrite) {
|
||||||
|
await this.vault.modifyBinary(file, response.arrayBuffer);
|
||||||
|
} else {
|
||||||
|
new Notice("Cover image already exists: " + filePath);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
file = await this.vault.createBinary(
|
||||||
|
filePath,
|
||||||
|
response.arrayBuffer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.compressor.compress(file, {
|
||||||
|
height: 400,
|
||||||
|
quality: 0.8,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (file.extension !== "jpg") {
|
||||||
|
await this.fileManager.renameFile(
|
||||||
|
file,
|
||||||
|
file.path.replace(/\.[^.]+$/, ".jpg")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import type { TFile, Vault } from "obsidian";
|
||||||
|
|
||||||
|
interface CompressOptions {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
maintainAspectRatio?: boolean;
|
||||||
|
quality?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImageCompressorService {
|
||||||
|
|
||||||
|
constructor(private readonly vault: Vault) {
|
||||||
|
}
|
||||||
|
|
||||||
|
async compress(
|
||||||
|
file: TFile,
|
||||||
|
options: CompressOptions
|
||||||
|
) {
|
||||||
|
const img = await new Promise<HTMLImageElement>((resolve) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = this.vault.getResourcePath(file);
|
||||||
|
img.onload = () => {
|
||||||
|
resolve(img);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
|
||||||
|
const quality = options.quality ?? 1;
|
||||||
|
let height = options.height;
|
||||||
|
let width = options.width;
|
||||||
|
|
||||||
|
if (!width && !height) {
|
||||||
|
width = img.width;
|
||||||
|
height = img.height;
|
||||||
|
} else if (!width && height) {
|
||||||
|
width = height * (img.width / img.height);
|
||||||
|
} else if (!height && width) {
|
||||||
|
height = width * (img.height / img.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.maintainAspectRatio) {
|
||||||
|
const aspectRatio = img.width / img.height;
|
||||||
|
|
||||||
|
if (options.height)
|
||||||
|
if (width! > height!) {
|
||||||
|
height = width! / aspectRatio;
|
||||||
|
} else {
|
||||||
|
width = height! * aspectRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = width!;
|
||||||
|
canvas.height = height!;
|
||||||
|
|
||||||
|
ctx.drawImage(img, 0, 0, width!, height!);
|
||||||
|
|
||||||
|
const blob = await new Promise<Blob>((resolve, reject) => {
|
||||||
|
canvas.toBlob(
|
||||||
|
(blob) => {
|
||||||
|
if (blob) {
|
||||||
|
resolve(blob);
|
||||||
|
} else {
|
||||||
|
reject(new Error("Failed to compress image"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"image/jpeg",
|
||||||
|
quality
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.vault.modifyBinary(file, await blob.arrayBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
import { type App, type TFile } from "obsidian";
|
|
||||||
|
|
||||||
interface CompressOptions {
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
maintainAspectRatio?: boolean;
|
|
||||||
quality?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function compressImage(
|
|
||||||
app: App,
|
|
||||||
file: TFile,
|
|
||||||
options: CompressOptions
|
|
||||||
) {
|
|
||||||
const img = await new Promise<HTMLImageElement>((resolve) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.src = app.vault.getResourcePath(file);
|
|
||||||
img.onload = () => {
|
|
||||||
resolve(img);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
const ctx = canvas.getContext("2d")!;
|
|
||||||
|
|
||||||
const quality = options.quality ?? 1;
|
|
||||||
let height = options.height;
|
|
||||||
let width = options.width;
|
|
||||||
|
|
||||||
if (!width && !height) {
|
|
||||||
width = img.width;
|
|
||||||
height = img.height;
|
|
||||||
} else if (!width && height) {
|
|
||||||
width = height * (img.width / img.height);
|
|
||||||
} else if (!height && width) {
|
|
||||||
height = width * (img.height / img.width);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.maintainAspectRatio) {
|
|
||||||
const aspectRatio = img.width / img.height;
|
|
||||||
|
|
||||||
if (options.height)
|
|
||||||
if (width! > height!) {
|
|
||||||
height = width! / aspectRatio;
|
|
||||||
} else {
|
|
||||||
width = height! * aspectRatio;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.width = width!;
|
|
||||||
canvas.height = height!;
|
|
||||||
|
|
||||||
ctx.drawImage(img, 0, 0, width!, height!);
|
|
||||||
|
|
||||||
const blob = await new Promise<Blob>((resolve, reject) => {
|
|
||||||
canvas.toBlob(
|
|
||||||
(blob) => {
|
|
||||||
if (blob) {
|
|
||||||
resolve(blob);
|
|
||||||
} else {
|
|
||||||
reject(new Error("Failed to compress image"));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"image/jpeg",
|
|
||||||
quality
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return app.vault.modifyBinary(file, await blob.arrayBuffer());
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +1,51 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"inlineSourceMap": true,
|
"inlineSourceMap": true,
|
||||||
"inlineSources": true,
|
"inlineSources": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"DOM",
|
"DOM",
|
||||||
"ES5",
|
"ES5",
|
||||||
"ES6",
|
"ES6",
|
||||||
"ES7"
|
"ES7"
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@commands/*": ["src/commands/*"],
|
"@commands/*": [
|
||||||
"@data-sources/*": ["src/data-sources/*"],
|
"src/commands/*"
|
||||||
"@external/*": ["src/external/*"],
|
],
|
||||||
"@ui/*": ["src/ui/*"],
|
"@data-sources/*": [
|
||||||
"@utils/*": ["src/utils/*"],
|
"src/data-sources/*"
|
||||||
"@src/*": ["src/*"]
|
],
|
||||||
}
|
"@external/*": [
|
||||||
},
|
"src/external/*"
|
||||||
"include": [
|
],
|
||||||
"**/*.ts",
|
"@services/*": [
|
||||||
"**/*.svelte"
|
"src/services/*"
|
||||||
]
|
],
|
||||||
|
"@ui/*": [
|
||||||
|
"src/ui/*"
|
||||||
|
],
|
||||||
|
"@utils/*": [
|
||||||
|
"src/utils/*"
|
||||||
|
],
|
||||||
|
"@src/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.svelte"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue