mirror of
https://github.com/Pantonius/pantosite-astro.git
synced 2026-04-30 10:34:37 +00:00
4258 lines
562 KiB
JavaScript
4258 lines
562 KiB
JavaScript
|
|
/*
|
||
|
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||
|
|
if you want to view the source, please visit the github repository of this plugin
|
||
|
|
*/
|
||
|
|
|
||
|
|
var __create = Object.create;
|
||
|
|
var __defProp = Object.defineProperty;
|
||
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||
|
|
var __getProtoOf = Object.getPrototypeOf;
|
||
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||
|
|
var __esm = (fn, res) => function __init() {
|
||
|
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
||
|
|
};
|
||
|
|
var __export = (target, all) => {
|
||
|
|
for (var name in all)
|
||
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||
|
|
};
|
||
|
|
var __copyProps = (to, from, except, desc) => {
|
||
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
||
|
|
for (let key of __getOwnPropNames(from))
|
||
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||
|
|
}
|
||
|
|
return to;
|
||
|
|
};
|
||
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
||
|
|
// file that has been converted to a CommonJS file using a Babel-
|
||
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
||
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
||
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||
|
|
mod
|
||
|
|
));
|
||
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||
|
|
|
||
|
|
// src/modals/ConfirmModal.ts
|
||
|
|
var ConfirmModal_exports = {};
|
||
|
|
__export(ConfirmModal_exports, {
|
||
|
|
ConfirmModal: () => ConfirmModal,
|
||
|
|
openConfirmModal: () => openConfirmModal
|
||
|
|
});
|
||
|
|
function openConfirmModal(app, title, message, confirmText = "Confirm", cancelText = "Cancel") {
|
||
|
|
const modal = new ConfirmModal(app, title, message, confirmText, cancelText);
|
||
|
|
return modal.openAndAwaitResult();
|
||
|
|
}
|
||
|
|
var import_obsidian14, ConfirmModal;
|
||
|
|
var init_ConfirmModal = __esm({
|
||
|
|
"src/modals/ConfirmModal.ts"() {
|
||
|
|
import_obsidian14 = require("obsidian");
|
||
|
|
ConfirmModal = class extends import_obsidian14.Modal {
|
||
|
|
constructor(app, title, message, confirmText = "Confirm", cancelText = "Cancel") {
|
||
|
|
super(app);
|
||
|
|
this.title = title;
|
||
|
|
this.message = message;
|
||
|
|
this.confirmText = confirmText;
|
||
|
|
this.cancelText = cancelText;
|
||
|
|
}
|
||
|
|
onOpen() {
|
||
|
|
const { contentEl, titleEl } = this;
|
||
|
|
titleEl.setText(this.title);
|
||
|
|
const messageEl = contentEl.createDiv({ cls: "image-manager-confirm-message" });
|
||
|
|
messageEl.createEl("p", { text: this.message });
|
||
|
|
const buttonContainer = contentEl.createDiv({ cls: "image-manager-confirm-buttons" });
|
||
|
|
buttonContainer.createEl("button", {
|
||
|
|
text: this.confirmText,
|
||
|
|
cls: "mod-cta"
|
||
|
|
}).addEventListener("click", () => {
|
||
|
|
this.resolve({ confirmed: true });
|
||
|
|
this.close();
|
||
|
|
});
|
||
|
|
buttonContainer.createEl("button", {
|
||
|
|
text: this.cancelText
|
||
|
|
}).addEventListener("click", () => {
|
||
|
|
this.resolve({ confirmed: false });
|
||
|
|
this.close();
|
||
|
|
});
|
||
|
|
setTimeout(() => {
|
||
|
|
const confirmButton = buttonContainer.querySelector(".mod-cta");
|
||
|
|
confirmButton == null ? void 0 : confirmButton.focus();
|
||
|
|
}, 50);
|
||
|
|
}
|
||
|
|
onClose() {
|
||
|
|
const { contentEl } = this;
|
||
|
|
contentEl.empty();
|
||
|
|
}
|
||
|
|
openAndAwaitResult() {
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
this.resolve = resolve;
|
||
|
|
this.open();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// src/main.ts
|
||
|
|
var main_exports = {};
|
||
|
|
__export(main_exports, {
|
||
|
|
default: () => ImageManagerPlugin
|
||
|
|
});
|
||
|
|
module.exports = __toCommonJS(main_exports);
|
||
|
|
var import_obsidian15 = require("obsidian");
|
||
|
|
|
||
|
|
// src/settings.ts
|
||
|
|
var import_obsidian = require("obsidian");
|
||
|
|
|
||
|
|
// src/types.ts
|
||
|
|
var DEFAULT_BANNER_DEVICE_SETTINGS = {
|
||
|
|
["desktop" /* Desktop */]: {
|
||
|
|
enabled: true,
|
||
|
|
height: 240,
|
||
|
|
viewOffset: 0,
|
||
|
|
noteOffset: -32,
|
||
|
|
bannerRadiusEnabled: false,
|
||
|
|
borderRadius: [8, 8, 8, 8],
|
||
|
|
padding: 8,
|
||
|
|
fade: true,
|
||
|
|
animation: false,
|
||
|
|
iconEnabled: false,
|
||
|
|
iconSize: 96,
|
||
|
|
iconRadius: 8,
|
||
|
|
iconBackground: true,
|
||
|
|
iconBorder: 2,
|
||
|
|
iconFrame: true,
|
||
|
|
iconAlignmentH: "flex-start",
|
||
|
|
iconAlignmentV: "flex-end",
|
||
|
|
iconOffsetX: 0,
|
||
|
|
iconOffsetY: -24
|
||
|
|
},
|
||
|
|
["tablet" /* Tablet */]: {
|
||
|
|
enabled: true,
|
||
|
|
height: 190,
|
||
|
|
viewOffset: 0,
|
||
|
|
noteOffset: -32,
|
||
|
|
bannerRadiusEnabled: false,
|
||
|
|
borderRadius: [8, 8, 8, 8],
|
||
|
|
padding: 8,
|
||
|
|
fade: true,
|
||
|
|
animation: false,
|
||
|
|
iconEnabled: false,
|
||
|
|
iconSize: 96,
|
||
|
|
iconRadius: 8,
|
||
|
|
iconBackground: true,
|
||
|
|
iconBorder: 2,
|
||
|
|
iconFrame: true,
|
||
|
|
iconAlignmentH: "flex-start",
|
||
|
|
iconAlignmentV: "flex-end",
|
||
|
|
iconOffsetX: 0,
|
||
|
|
iconOffsetY: -24
|
||
|
|
},
|
||
|
|
["phone" /* Phone */]: {
|
||
|
|
enabled: true,
|
||
|
|
height: 160,
|
||
|
|
viewOffset: 0,
|
||
|
|
noteOffset: -32,
|
||
|
|
bannerRadiusEnabled: false,
|
||
|
|
borderRadius: [8, 8, 8, 8],
|
||
|
|
padding: 8,
|
||
|
|
fade: true,
|
||
|
|
animation: false,
|
||
|
|
iconEnabled: false,
|
||
|
|
iconSize: 56,
|
||
|
|
iconRadius: 8,
|
||
|
|
iconBackground: true,
|
||
|
|
iconBorder: 2,
|
||
|
|
iconFrame: true,
|
||
|
|
iconAlignmentH: "flex-start",
|
||
|
|
iconAlignmentV: "flex-end",
|
||
|
|
iconOffsetX: 0,
|
||
|
|
iconOffsetY: -24
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var DEFAULT_BANNER_SETTINGS = {
|
||
|
|
properties: {
|
||
|
|
imageProperty: "banner",
|
||
|
|
iconProperty: "icon",
|
||
|
|
hidePropertyEnabled: false,
|
||
|
|
hideProperty: ""
|
||
|
|
},
|
||
|
|
desktop: { ...DEFAULT_BANNER_DEVICE_SETTINGS["desktop" /* Desktop */] },
|
||
|
|
tablet: { ...DEFAULT_BANNER_DEVICE_SETTINGS["tablet" /* Tablet */] },
|
||
|
|
phone: { ...DEFAULT_BANNER_DEVICE_SETTINGS["phone" /* Phone */] }
|
||
|
|
};
|
||
|
|
var DEFAULT_SETTINGS = {
|
||
|
|
// General Settings
|
||
|
|
enableRenameOnPaste: true,
|
||
|
|
enableRenameOnDrop: true,
|
||
|
|
imageNameTemplate: "",
|
||
|
|
attachmentLocation: "obsidian" /* ObsidianDefault */,
|
||
|
|
customAttachmentPath: "./assets",
|
||
|
|
// Image Services
|
||
|
|
defaultProvider: "unsplash" /* Unsplash */,
|
||
|
|
unsplashProxyServer: "",
|
||
|
|
pexelsApiKey: "",
|
||
|
|
pexelsApiKeySecretId: "",
|
||
|
|
pixabayApiKey: "",
|
||
|
|
pixabayApiKeySecretId: "",
|
||
|
|
defaultOrientation: "any" /* Any */,
|
||
|
|
defaultImageSize: "large" /* Large */,
|
||
|
|
// Property Insertion
|
||
|
|
enablePropertyPaste: true,
|
||
|
|
propertyLinkFormat: "obsidian" /* ObsidianDefault */,
|
||
|
|
customPropertyLinkFormat: "{image-url}",
|
||
|
|
defaultPropertyName: "banner",
|
||
|
|
defaultIconPropertyName: "icon",
|
||
|
|
altTextProperty: "",
|
||
|
|
// Conversion
|
||
|
|
autoConvertRemoteImages: false,
|
||
|
|
convertOnNoteOpen: false,
|
||
|
|
convertOnNoteSave: false,
|
||
|
|
processBackgroundChanges: true,
|
||
|
|
// Rename Options
|
||
|
|
showRenameDialog: true,
|
||
|
|
autoRename: false,
|
||
|
|
dupNumberDelimiter: "-",
|
||
|
|
dupNumberAtStart: false,
|
||
|
|
disableRenameNotice: false,
|
||
|
|
enableDescriptiveImages: false,
|
||
|
|
// Image Insertion Options (remote image attribution options)
|
||
|
|
insertSize: "",
|
||
|
|
// Empty = no size specified
|
||
|
|
insertReferral: true,
|
||
|
|
// Default to true (attribution)
|
||
|
|
insertBackLink: false,
|
||
|
|
// Default to false
|
||
|
|
appendReferral: false,
|
||
|
|
// Default to false
|
||
|
|
// Banner Settings
|
||
|
|
banner: { ...DEFAULT_BANNER_SETTINGS },
|
||
|
|
// Advanced
|
||
|
|
supportedExtensions: ["md", "mdx"],
|
||
|
|
debugMode: false
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/settings.ts
|
||
|
|
var ImageManagerSettingTab = class extends import_obsidian.PluginSettingTab {
|
||
|
|
constructor(app, plugin) {
|
||
|
|
super(app, plugin);
|
||
|
|
this.icon = "lucide-image-down";
|
||
|
|
this.plugin = plugin;
|
||
|
|
}
|
||
|
|
display() {
|
||
|
|
const { containerEl } = this;
|
||
|
|
containerEl.empty();
|
||
|
|
this.renderGeneralSettings(containerEl);
|
||
|
|
this.renderImageServicesSettings(containerEl);
|
||
|
|
this.renderPropertySettings(containerEl);
|
||
|
|
this.renderConversionSettings(containerEl);
|
||
|
|
this.renderRenameSettings(containerEl);
|
||
|
|
this.renderBannerSettings(containerEl);
|
||
|
|
this.renderAdvancedSettings(containerEl);
|
||
|
|
}
|
||
|
|
renderGeneralSettings(containerEl) {
|
||
|
|
const group = new import_obsidian.SettingGroup(containerEl);
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Image name template").setDesc("Template for generated image names. Variables: {{fileName}}, {{dirName}}, {{DATE:YYYY-MM-DD}}, {{TIME:HH-mm-ss}}").addText((text) => {
|
||
|
|
text.setPlaceholder("{{fileName}}").setValue(this.plugin.settings.imageNameTemplate).onChange(async (value) => {
|
||
|
|
this.plugin.settings.imageNameTemplate = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Attachment location").setDesc("Where to save inserted images").addDropdown((dropdown) => {
|
||
|
|
dropdown.addOption("obsidian" /* ObsidianDefault */, "Use Obsidian's settings").addOption("same" /* SameFolder */, "Same folder as note").addOption("subfolder" /* Subfolder */, "Subfolder (configure below)").addOption("vault" /* VaultFolder */, "Vault folder (configure below)").setValue(this.plugin.settings.attachmentLocation).onChange(async (value) => {
|
||
|
|
this.plugin.settings.attachmentLocation = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
const scrollContainer = containerEl.closest(".vertical-tab-content") || containerEl.closest(".settings-content") || containerEl.parentElement;
|
||
|
|
const scrollTop = (scrollContainer == null ? void 0 : scrollContainer.scrollTop) || 0;
|
||
|
|
this.display();
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
if (scrollContainer) {
|
||
|
|
scrollContainer.scrollTop = scrollTop;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
if (this.plugin.settings.attachmentLocation !== "obsidian" /* ObsidianDefault */ && this.plugin.settings.attachmentLocation !== "same" /* SameFolder */) {
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Custom attachment path").setDesc('Path for attachments. Use "./" for relative to note, or "/" for vault root.').addText((text) => {
|
||
|
|
text.setPlaceholder("./assets").setValue(this.plugin.settings.customAttachmentPath).onChange(async (value) => {
|
||
|
|
this.plugin.settings.customAttachmentPath = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
renderImageServicesSettings(containerEl) {
|
||
|
|
const group = new import_obsidian.SettingGroup(containerEl).setHeading("Image services");
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Default provider").setDesc("Default image provider for search").addDropdown((dropdown) => {
|
||
|
|
dropdown.addOption("unsplash" /* Unsplash */, "Unsplash").addOption("pexels" /* Pexels */, "Pexels").addOption("pixabay" /* Pixabay */, "Pixabay").addOption("local" /* Local */, "Local files").setValue(this.plugin.settings.defaultProvider).onChange(async (value) => {
|
||
|
|
this.plugin.settings.defaultProvider = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Default orientation").setDesc("Filter images by orientation").addDropdown((dropdown) => {
|
||
|
|
dropdown.addOption("any" /* Any */, "Any").addOption("landscape" /* Landscape */, "Landscape").addOption("portrait" /* Portrait */, "Portrait").addOption("square" /* Square */, "Square").setValue(this.plugin.settings.defaultOrientation).onChange(async (value) => {
|
||
|
|
this.plugin.settings.defaultOrientation = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Default image size").setDesc("Preferred size when downloading images").addDropdown((dropdown) => {
|
||
|
|
dropdown.addOption("original" /* Original */, "Original").addOption("large" /* Large */, "Large").addOption("medium" /* Medium */, "Medium").addOption("small" /* Small */, "Small").setValue(this.plugin.settings.defaultImageSize).onChange(async (value) => {
|
||
|
|
this.plugin.settings.defaultImageSize = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Unsplash proxy server").setDesc("Optional proxy server (leave empty to use built-in)").addText((text) => {
|
||
|
|
text.setPlaceholder("https://your-proxy.com/").setValue(this.plugin.settings.unsplashProxyServer).onChange(async (value) => {
|
||
|
|
this.plugin.settings.unsplashProxyServer = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Pexels API key");
|
||
|
|
if ((0, import_obsidian.requireApiVersion)("1.11.4")) {
|
||
|
|
setting.setDesc("Choose a secret that contains your Pexels API key.").addComponent((el) => {
|
||
|
|
const obsidian = require("obsidian");
|
||
|
|
const SecretComponent = obsidian.SecretComponent;
|
||
|
|
const component = new SecretComponent(this.app, el);
|
||
|
|
component.setValue(this.plugin.settings.pexelsApiKeySecretId);
|
||
|
|
component.onChange((value) => {
|
||
|
|
void (async () => {
|
||
|
|
this.plugin.settings.pexelsApiKeySecretId = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
})();
|
||
|
|
});
|
||
|
|
return component;
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
setting.setDesc("Get your API key from https://www.pexels.com/api/new/").addText((text) => {
|
||
|
|
text.setPlaceholder("Pexels API key").setValue(this.plugin.settings.pexelsApiKey).onChange(async (value) => {
|
||
|
|
this.plugin.settings.pexelsApiKey = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Pixabay API key");
|
||
|
|
if ((0, import_obsidian.requireApiVersion)("1.11.4")) {
|
||
|
|
setting.setDesc("Choose a secret that contains your Pixabay API key.").addComponent((el) => {
|
||
|
|
const obsidian = require("obsidian");
|
||
|
|
const SecretComponent = obsidian.SecretComponent;
|
||
|
|
const component = new SecretComponent(this.app, el);
|
||
|
|
component.setValue(this.plugin.settings.pixabayApiKeySecretId);
|
||
|
|
component.onChange((value) => {
|
||
|
|
void (async () => {
|
||
|
|
this.plugin.settings.pixabayApiKeySecretId = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
})();
|
||
|
|
});
|
||
|
|
return component;
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
setting.setDesc("Get your API key from https://pixabay.com/api/docs/").addText((text) => {
|
||
|
|
text.setPlaceholder("Pixabay API key").setValue(this.plugin.settings.pixabayApiKey).onChange(async (value) => {
|
||
|
|
this.plugin.settings.pixabayApiKey = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Insert size").setDesc('Set the size of the image when inserting. Format could be only the width "200" or the width and height "200x100". Leave empty for no size.').addText((text) => {
|
||
|
|
text.setPlaceholder("200 or 200x100").setValue(this.plugin.settings.insertSize).onChange(async (value) => {
|
||
|
|
this.plugin.settings.insertSize = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Insert referral").setDesc("Insert the reference text").addToggle((toggle) => {
|
||
|
|
toggle.setValue(this.plugin.settings.insertReferral).onChange(async (value) => {
|
||
|
|
this.plugin.settings.insertReferral = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Insert backlink").setDesc("Insert a backlink in front of the reference text").addToggle((toggle) => {
|
||
|
|
toggle.setValue(this.plugin.settings.insertBackLink).onChange(async (value) => {
|
||
|
|
this.plugin.settings.insertBackLink = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
renderPropertySettings(containerEl) {
|
||
|
|
const group = new import_obsidian.SettingGroup(containerEl).setHeading("Property insertion");
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Enable paste into properties").setDesc("Allow pasting images directly into properties").addToggle((toggle) => {
|
||
|
|
toggle.setValue(this.plugin.settings.enablePropertyPaste).onChange(async (value) => {
|
||
|
|
this.plugin.settings.enablePropertyPaste = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Property link format").setDesc("How to format the image link in properties").addDropdown((dropdown) => {
|
||
|
|
dropdown.addOption("obsidian" /* ObsidianDefault */, "Use Obsidian's settings").addOption("path" /* Path */, "Plain path (path/to/image.jpg)").addOption("relative" /* RelativePath */, "Relative path (./image.jpg)").addOption("wikilink" /* Wikilink */, "Wikilink ([[path/to/image.jpg]])").addOption("markdown" /* Markdown */, "Markdown ()").addOption("custom" /* Custom */, "Custom format").setValue(this.plugin.settings.propertyLinkFormat).onChange(async (value) => {
|
||
|
|
this.plugin.settings.propertyLinkFormat = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
const scrollContainer = containerEl.closest(".vertical-tab-content") || containerEl.closest(".settings-content") || containerEl.parentElement;
|
||
|
|
const scrollTop = (scrollContainer == null ? void 0 : scrollContainer.scrollTop) || 0;
|
||
|
|
this.display();
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
if (scrollContainer) {
|
||
|
|
scrollContainer.scrollTop = scrollTop;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
if (this.plugin.settings.propertyLinkFormat === "custom" /* Custom */) {
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Custom format template").setDesc("Use {image-url} as placeholder for the image path").addText((text) => {
|
||
|
|
text.setPlaceholder("{image-url}").setValue(this.plugin.settings.customPropertyLinkFormat).onChange(async (value) => {
|
||
|
|
this.plugin.settings.customPropertyLinkFormat = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Default property name").setDesc("Default property name when inserting to properties via command").addText((text) => {
|
||
|
|
text.setPlaceholder("Banner").setValue(this.plugin.settings.defaultPropertyName).onChange(async (value) => {
|
||
|
|
this.plugin.settings.defaultPropertyName = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Default icon property name").setDesc("Default property name when inserting to icon property via command").addText((text) => {
|
||
|
|
text.setPlaceholder("Icon").setValue(this.plugin.settings.defaultIconPropertyName).onChange(async (value) => {
|
||
|
|
this.plugin.settings.defaultIconPropertyName = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Alt text property name").setDesc('Property name to use for image alt text (description) when inserting to properties. If "Descriptive images" is enabled, this will be filled with the description you provide. If disabled, it will be filled with the search term for external images.').addText((text) => {
|
||
|
|
text.setPlaceholder("alt").setValue(this.plugin.settings.altTextProperty).onChange(async (value) => {
|
||
|
|
this.plugin.settings.altTextProperty = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
renderConversionSettings(containerEl) {
|
||
|
|
const group = new import_obsidian.SettingGroup(containerEl).setHeading("Remote image conversion");
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Auto-convert remote images").setDesc("Automatically download and replace remote image urls with local files").addToggle((toggle) => {
|
||
|
|
toggle.setValue(this.plugin.settings.autoConvertRemoteImages).onChange(async (value) => {
|
||
|
|
this.plugin.settings.autoConvertRemoteImages = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
const scrollContainer = containerEl.closest(".vertical-tab-content") || containerEl.closest(".settings-content") || containerEl.parentElement;
|
||
|
|
const scrollTop = (scrollContainer == null ? void 0 : scrollContainer.scrollTop) || 0;
|
||
|
|
this.display();
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
if (scrollContainer) {
|
||
|
|
scrollContainer.scrollTop = scrollTop;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
if (this.plugin.settings.autoConvertRemoteImages) {
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Convert on note open").setDesc("Process remote images when opening a note").addToggle((toggle) => {
|
||
|
|
toggle.setValue(this.plugin.settings.convertOnNoteOpen).onChange(async (value) => {
|
||
|
|
this.plugin.settings.convertOnNoteOpen = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Convert on note save").setDesc("Process remote images when saving a note").addToggle((toggle) => {
|
||
|
|
toggle.setValue(this.plugin.settings.convertOnNoteSave).onChange(async (value) => {
|
||
|
|
this.plugin.settings.convertOnNoteSave = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
renderRenameSettings(containerEl) {
|
||
|
|
const group = new import_obsidian.SettingGroup(containerEl).setHeading("Rename options");
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Show image rename dialog automatically").setDesc("Handle and rename images when they are added to the vault via paste or drag and drop").addToggle((toggle) => {
|
||
|
|
toggle.setValue(this.plugin.settings.showRenameDialog).onChange(async (value) => {
|
||
|
|
this.plugin.settings.showRenameDialog = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
this.refreshWithScrollPreserve(containerEl);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
if (this.plugin.settings.showRenameDialog) {
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Rename on paste").setDesc("Handle and rename images when pasting into the editor").addToggle((toggle) => {
|
||
|
|
toggle.setValue(this.plugin.settings.enableRenameOnPaste).onChange(async (value) => {
|
||
|
|
this.plugin.settings.enableRenameOnPaste = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Rename on drag and drop").setDesc("Handle and rename images when dropping into the editor").addToggle((toggle) => {
|
||
|
|
toggle.setValue(this.plugin.settings.enableRenameOnDrop).onChange(async (value) => {
|
||
|
|
this.plugin.settings.enableRenameOnDrop = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Process background file changes").setDesc("Automatically convert and rename remote images when files are changed in the background (by Git or other plugins). Warning: Turning this on may cause the rename modal to appear for images you've already processed on other devices during a sync.").addToggle((toggle) => {
|
||
|
|
toggle.setValue(this.plugin.settings.processBackgroundChanges).onChange(async (value) => {
|
||
|
|
this.plugin.settings.processBackgroundChanges = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Descriptive images").setDesc("Ask for image description, use as display text and kebab-case for file name (applies to note body insertions only, not properties)").addToggle((toggle) => {
|
||
|
|
toggle.setValue(this.plugin.settings.enableDescriptiveImages).onChange(async (value) => {
|
||
|
|
this.plugin.settings.enableDescriptiveImages = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Auto rename").setDesc("Automatically rename without showing dialog (uses template)").addToggle((toggle) => {
|
||
|
|
toggle.setValue(this.plugin.settings.autoRename).onChange(async (value) => {
|
||
|
|
this.plugin.settings.autoRename = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Duplicate number delimiter").setDesc('Character(s) between name and number for duplicates (e.g., "-" gives "image-1")').addText((text) => {
|
||
|
|
text.setPlaceholder("-").setValue(this.plugin.settings.dupNumberDelimiter).onChange(async (value) => {
|
||
|
|
this.plugin.settings.dupNumberDelimiter = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Duplicate number at start").setDesc('Put the duplicate number at the start ("1-image" instead of "image-1")').addToggle((toggle) => {
|
||
|
|
toggle.setValue(this.plugin.settings.dupNumberAtStart).onChange(async (value) => {
|
||
|
|
this.plugin.settings.dupNumberAtStart = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Disable rename notice").setDesc("Do not show a notice after renaming an image").addToggle((toggle) => {
|
||
|
|
toggle.setValue(this.plugin.settings.disableRenameNotice).onChange(async (value) => {
|
||
|
|
this.plugin.settings.disableRenameNotice = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get the current device type
|
||
|
|
*/
|
||
|
|
getCurrentDevice() {
|
||
|
|
if (import_obsidian.Platform.isPhone) {
|
||
|
|
return "phone" /* Phone */;
|
||
|
|
}
|
||
|
|
if (import_obsidian.Platform.isTablet) {
|
||
|
|
return "tablet" /* Tablet */;
|
||
|
|
}
|
||
|
|
return "desktop" /* Desktop */;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Helper to preserve scroll position when re-rendering settings
|
||
|
|
*/
|
||
|
|
refreshWithScrollPreserve(containerEl) {
|
||
|
|
const scrollContainer = containerEl.closest(".vertical-tab-content") || containerEl.closest(".settings-content") || containerEl.parentElement;
|
||
|
|
const scrollTop = (scrollContainer == null ? void 0 : scrollContainer.scrollTop) || 0;
|
||
|
|
this.display();
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
if (scrollContainer) {
|
||
|
|
scrollContainer.scrollTop = scrollTop;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
renderBannerSettings(containerEl) {
|
||
|
|
const group = new import_obsidian.SettingGroup(containerEl).setHeading("Banner images");
|
||
|
|
const currentDevice = this.getCurrentDevice();
|
||
|
|
const deviceSettings = this.plugin.settings.banner[currentDevice];
|
||
|
|
const defaultDeviceSettings = DEFAULT_BANNER_DEVICE_SETTINGS[currentDevice];
|
||
|
|
const propertySettings = this.plugin.settings.banner.properties;
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Show banner").setDesc(`Enable or disable banners on your ${currentDevice} device`).addToggle((toggle) => {
|
||
|
|
toggle.setValue(deviceSettings.enabled).onChange(async (value) => {
|
||
|
|
this.plugin.settings.banner[currentDevice].enabled = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
this.refreshWithScrollPreserve(containerEl);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
if (!deviceSettings.enabled) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Height").setDesc(`Height of the banner on your ${currentDevice} device (in pixels)`).addText((text) => {
|
||
|
|
text.setPlaceholder(String(defaultDeviceSettings.height)).setValue(String(deviceSettings.height)).onChange(async (value) => {
|
||
|
|
const num = parseInt(value, 10);
|
||
|
|
if (!isNaN(num) && num > 0) {
|
||
|
|
this.plugin.settings.banner[currentDevice].height = num;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Padding").setDesc("Padding of the banner from the edges of the note (in pixels)").addText((text) => {
|
||
|
|
text.setPlaceholder(String(defaultDeviceSettings.padding)).setValue(String(deviceSettings.padding)).onChange(async (value) => {
|
||
|
|
const num = parseInt(value, 10);
|
||
|
|
if (!isNaN(num) && num >= 0) {
|
||
|
|
this.plugin.settings.banner[currentDevice].padding = num;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Note offset").setDesc("Move the position of the note content (in pixels)").addText((text) => {
|
||
|
|
text.setPlaceholder(String(defaultDeviceSettings.noteOffset)).setValue(String(deviceSettings.noteOffset)).onChange(async (value) => {
|
||
|
|
const num = parseInt(value, 10);
|
||
|
|
if (!isNaN(num)) {
|
||
|
|
this.plugin.settings.banner[currentDevice].noteOffset = num;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("View offset").setDesc("Move the position of the view content (in pixels)").addText((text) => {
|
||
|
|
text.setPlaceholder(String(defaultDeviceSettings.viewOffset)).setValue(String(deviceSettings.viewOffset)).onChange(async (value) => {
|
||
|
|
const num = parseInt(value, 10);
|
||
|
|
if (!isNaN(num)) {
|
||
|
|
this.plugin.settings.banner[currentDevice].viewOffset = num;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Fade").setDesc("Fade the image out towards the content").addToggle((toggle) => {
|
||
|
|
toggle.setValue(deviceSettings.fade).onChange(async (value) => {
|
||
|
|
this.plugin.settings.banner[currentDevice].fade = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Rounded corners").setDesc("Enable rounded corners for the banner").addToggle((toggle) => {
|
||
|
|
toggle.setValue(deviceSettings.bannerRadiusEnabled).onChange(async (value) => {
|
||
|
|
this.plugin.settings.banner[currentDevice].bannerRadiusEnabled = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Animation").setDesc("Enable banner animation when opening files").addToggle((toggle) => {
|
||
|
|
toggle.setValue(deviceSettings.animation).onChange(async (value) => {
|
||
|
|
this.plugin.settings.banner[currentDevice].animation = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Banner property").setDesc("Name of the banner property this plugin will look for in the properties").addText((text) => {
|
||
|
|
text.setPlaceholder("Banner").setValue(propertySettings.imageProperty).onChange(async (value) => {
|
||
|
|
this.plugin.settings.banner.properties.imageProperty = value || "banner";
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Icon property").setDesc("Name of the icon property this plugin will look for in the properties").addText((text) => {
|
||
|
|
text.setPlaceholder("Icon").setValue(propertySettings.iconProperty).onChange(async (value) => {
|
||
|
|
this.plugin.settings.banner.properties.iconProperty = value || "icon";
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Enable per-note banner hiding").setDesc("Allow disabling banners on a per-note basis using a properties field").addToggle((toggle) => {
|
||
|
|
toggle.setValue(propertySettings.hidePropertyEnabled).onChange(async (value) => {
|
||
|
|
this.plugin.settings.banner.properties.hidePropertyEnabled = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
this.refreshWithScrollPreserve(containerEl);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
if (propertySettings.hidePropertyEnabled) {
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Hide banner property").setDesc("Name of the property that, when set to true, will hide the banner for that note").addText((text) => {
|
||
|
|
text.setPlaceholder("hideBanner").setValue(propertySettings.hideProperty).onChange(async (value) => {
|
||
|
|
this.plugin.settings.banner.properties.hideProperty = value || "";
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Show icon").setDesc("Enable or disable the icon").addToggle((toggle) => {
|
||
|
|
toggle.setValue(deviceSettings.iconEnabled).onChange(async (value) => {
|
||
|
|
this.plugin.settings.banner[currentDevice].iconEnabled = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
this.refreshWithScrollPreserve(containerEl);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
if (deviceSettings.iconEnabled) {
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Icon size").setDesc("Size of the icon (in pixels)").addText((text) => {
|
||
|
|
text.setPlaceholder(String(defaultDeviceSettings.iconSize)).setValue(String(deviceSettings.iconSize)).onChange(async (value) => {
|
||
|
|
const num = parseInt(value, 10);
|
||
|
|
if (!isNaN(num) && num > 0) {
|
||
|
|
this.plugin.settings.banner[currentDevice].iconSize = num;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Icon background").setDesc("Enable or disable the icon background").addToggle((toggle) => {
|
||
|
|
toggle.setValue(deviceSettings.iconBackground).onChange(async (value) => {
|
||
|
|
this.plugin.settings.banner[currentDevice].iconBackground = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Icon frame").setDesc("Show the border/background frame around the icon (disable to display just the icon graphic)").addToggle((toggle) => {
|
||
|
|
toggle.setValue(deviceSettings.iconFrame).onChange(async (value) => {
|
||
|
|
this.plugin.settings.banner[currentDevice].iconFrame = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Icon border size").setDesc("Size of the icon border (in pixels)").addText((text) => {
|
||
|
|
text.setPlaceholder(String(defaultDeviceSettings.iconBorder)).setValue(String(deviceSettings.iconBorder)).onChange(async (value) => {
|
||
|
|
const num = parseInt(value, 10);
|
||
|
|
if (!isNaN(num) && num >= 0) {
|
||
|
|
this.plugin.settings.banner[currentDevice].iconBorder = num;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Icon border radius").setDesc("Size of the icon border radius (in pixels)").addText((text) => {
|
||
|
|
text.setPlaceholder(String(defaultDeviceSettings.iconRadius)).setValue(String(deviceSettings.iconRadius)).onChange(async (value) => {
|
||
|
|
const num = parseInt(value, 10);
|
||
|
|
if (!isNaN(num) && num >= 0) {
|
||
|
|
this.plugin.settings.banner[currentDevice].iconRadius = num;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Icon alignment - horizontal").setDesc("Horizontal alignment of the icon").addDropdown((dropdown) => {
|
||
|
|
dropdown.addOption("flex-start", "Left").addOption("center", "Center").addOption("flex-end", "Right").setValue(deviceSettings.iconAlignmentH).onChange(async (value) => {
|
||
|
|
this.plugin.settings.banner[currentDevice].iconAlignmentH = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Icon alignment - vertical").setDesc("Vertical alignment of the icon").addDropdown((dropdown) => {
|
||
|
|
dropdown.addOption("flex-start", "Top").addOption("center", "Center").addOption("flex-end", "Bottom").setValue(deviceSettings.iconAlignmentV).onChange(async (value) => {
|
||
|
|
this.plugin.settings.banner[currentDevice].iconAlignmentV = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
renderAdvancedSettings(containerEl) {
|
||
|
|
const group = new import_obsidian.SettingGroup(containerEl).setHeading("Advanced");
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Supported file extensions").setDesc("File extensions to process (comma-separated)").addText((text) => {
|
||
|
|
const currentValue = this.plugin.settings.supportedExtensions.length > 0 ? this.plugin.settings.supportedExtensions.join(", ") : "";
|
||
|
|
text.setPlaceholder("File extensions").setValue(currentValue).onChange(async (value) => {
|
||
|
|
const extensions = value.split(",").map((ext) => ext.trim().toLowerCase()).filter((ext) => ext.length > 0);
|
||
|
|
this.plugin.settings.supportedExtensions = extensions.length > 0 ? extensions : ["md"];
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
group.addSetting((setting) => {
|
||
|
|
setting.setName("Debug mode").setDesc("Enable debug logging to console").addToggle((toggle) => {
|
||
|
|
toggle.setValue(this.plugin.settings.debugMode).onChange(async (value) => {
|
||
|
|
this.plugin.settings.debugMode = value;
|
||
|
|
await this.plugin.saveSettings();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/services/StorageManager.ts
|
||
|
|
var import_obsidian2 = require("obsidian");
|
||
|
|
var StorageManager = class {
|
||
|
|
constructor(app, settings, observable) {
|
||
|
|
this.app = app;
|
||
|
|
this.settings = settings;
|
||
|
|
observable == null ? void 0 : observable.subscribe((newSettings) => {
|
||
|
|
this.updateSettings(newSettings);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Update settings reference
|
||
|
|
*/
|
||
|
|
updateSettings(settings) {
|
||
|
|
this.settings = settings;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get the attachment folder path for a given note
|
||
|
|
*/
|
||
|
|
getAttachmentFolder(noteFile) {
|
||
|
|
var _a, _b;
|
||
|
|
const notePath = (_b = (_a = noteFile.parent) == null ? void 0 : _a.path) != null ? _b : "";
|
||
|
|
switch (this.settings.attachmentLocation) {
|
||
|
|
case "same" /* SameFolder */:
|
||
|
|
return notePath;
|
||
|
|
case "subfolder" /* Subfolder */:
|
||
|
|
return (0, import_obsidian2.normalizePath)(this.joinPaths(notePath, this.settings.customAttachmentPath));
|
||
|
|
case "vault" /* VaultFolder */:
|
||
|
|
return (0, import_obsidian2.normalizePath)(this.settings.customAttachmentPath);
|
||
|
|
case "obsidian" /* ObsidianDefault */:
|
||
|
|
default:
|
||
|
|
return this.getObsidianAttachmentFolder(noteFile);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get Obsidian's configured attachment folder
|
||
|
|
*/
|
||
|
|
getObsidianAttachmentFolder(noteFile) {
|
||
|
|
var _a, _b, _c;
|
||
|
|
const vaultConfig = this.app.vault.config;
|
||
|
|
const attachmentFolderPath = (_a = vaultConfig == null ? void 0 : vaultConfig.attachmentFolderPath) != null ? _a : "/";
|
||
|
|
const notePath = (_c = (_b = noteFile.parent) == null ? void 0 : _b.path) != null ? _c : "";
|
||
|
|
if (attachmentFolderPath === "/") {
|
||
|
|
return "";
|
||
|
|
} else if (attachmentFolderPath === "./") {
|
||
|
|
return notePath;
|
||
|
|
} else if (attachmentFolderPath.startsWith("./")) {
|
||
|
|
const relativePath = attachmentFolderPath.slice(2);
|
||
|
|
return (0, import_obsidian2.normalizePath)(this.joinPaths(notePath, relativePath));
|
||
|
|
} else {
|
||
|
|
return (0, import_obsidian2.normalizePath)(attachmentFolderPath);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Join path segments
|
||
|
|
*/
|
||
|
|
joinPaths(...parts) {
|
||
|
|
return parts.filter((p) => p).join("/");
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Ensure a folder exists, creating it if necessary
|
||
|
|
*/
|
||
|
|
async ensureFolderExists(folderPath) {
|
||
|
|
if (!folderPath) return;
|
||
|
|
const normalizedPath = (0, import_obsidian2.normalizePath)(folderPath);
|
||
|
|
const folder = this.app.vault.getAbstractFileByPath(normalizedPath);
|
||
|
|
if (!folder) {
|
||
|
|
await this.app.vault.createFolder(normalizedPath);
|
||
|
|
} else if (!(folder instanceof import_obsidian2.TFolder)) {
|
||
|
|
throw new Error(`Path exists but is not a folder: ${normalizedPath}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Generate a unique file path for an image
|
||
|
|
*/
|
||
|
|
async getAvailablePath(baseName, extension, noteFile) {
|
||
|
|
const folder = this.getAttachmentFolder(noteFile);
|
||
|
|
await this.ensureFolderExists(folder);
|
||
|
|
const sanitizedName = this.sanitizeFileName(baseName);
|
||
|
|
let fileName = `${sanitizedName}.${extension}`;
|
||
|
|
let filePath = folder ? (0, import_obsidian2.normalizePath)(this.joinPaths(folder, fileName)) : (0, import_obsidian2.normalizePath)(fileName);
|
||
|
|
let counter = 1;
|
||
|
|
while (this.app.vault.getAbstractFileByPath(filePath)) {
|
||
|
|
if (this.settings.dupNumberAtStart) {
|
||
|
|
fileName = `${counter}${this.settings.dupNumberDelimiter}${sanitizedName}.${extension}`;
|
||
|
|
} else {
|
||
|
|
fileName = `${sanitizedName}${this.settings.dupNumberDelimiter}${counter}.${extension}`;
|
||
|
|
}
|
||
|
|
filePath = folder ? (0, import_obsidian2.normalizePath)(this.joinPaths(folder, fileName)) : (0, import_obsidian2.normalizePath)(fileName);
|
||
|
|
counter++;
|
||
|
|
}
|
||
|
|
return filePath;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Save binary data as a file
|
||
|
|
*/
|
||
|
|
async saveFile(data, filePath) {
|
||
|
|
const normalizedPath = (0, import_obsidian2.normalizePath)(filePath);
|
||
|
|
const lastSlash = normalizedPath.lastIndexOf("/");
|
||
|
|
const parentPath = lastSlash > 0 ? normalizedPath.slice(0, lastSlash) : "";
|
||
|
|
if (parentPath) {
|
||
|
|
await this.ensureFolderExists(parentPath);
|
||
|
|
}
|
||
|
|
return await this.app.vault.createBinary(normalizedPath, data);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Generate markdown image link for a file
|
||
|
|
* Ensures the link includes '!' for images
|
||
|
|
* @param displayText Optional display text to add after the link (e.g., ![[image.jpg|display text]])
|
||
|
|
* @param insertSize Optional size to add (e.g., "200" or "200x100")
|
||
|
|
*/
|
||
|
|
generateMarkdownLink(file, sourcePath, displayText, insertSize) {
|
||
|
|
const link = this.app.fileManager.generateMarkdownLink(file, sourcePath);
|
||
|
|
let imageLink = link;
|
||
|
|
if (this.isImageFile(file) && !link.startsWith("!")) {
|
||
|
|
imageLink = `!${link}`;
|
||
|
|
}
|
||
|
|
if (this.settings.debugMode) {
|
||
|
|
console.debug("[Image Manager] generateMarkdownLink", {
|
||
|
|
originalLink: link,
|
||
|
|
imageLink,
|
||
|
|
insertSize,
|
||
|
|
displayText,
|
||
|
|
hasSize: !!(insertSize && insertSize.trim())
|
||
|
|
});
|
||
|
|
}
|
||
|
|
if (imageLink.startsWith(") {
|
||
|
|
if (insertSize && insertSize.trim()) {
|
||
|
|
const sizePart = `|${insertSize}`;
|
||
|
|
if (displayText && displayText.trim()) {
|
||
|
|
imageLink = imageLink.replace(/^!\[([^\]]*)\]/, `![${displayText}${sizePart}]`);
|
||
|
|
} else {
|
||
|
|
const altMatch = imageLink.match(/^!\[([^\]]*)\]/);
|
||
|
|
if (altMatch) {
|
||
|
|
const alt = altMatch[1] || "";
|
||
|
|
imageLink = imageLink.replace(/^!\[([^\]]*)\]/, `![${alt}${sizePart}]`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if (displayText && displayText.trim()) {
|
||
|
|
imageLink = imageLink.replace(/^!\[([^\]]*)\]/, `![${displayText}]`);
|
||
|
|
}
|
||
|
|
} else if (imageLink.startsWith("![") && imageLink.includes("]]")) {
|
||
|
|
const parts = [];
|
||
|
|
if (insertSize && insertSize.trim()) {
|
||
|
|
parts.push(insertSize);
|
||
|
|
}
|
||
|
|
if (displayText && displayText.trim()) {
|
||
|
|
parts.push(displayText);
|
||
|
|
}
|
||
|
|
if (parts.length > 0) {
|
||
|
|
imageLink = imageLink.replace(/\]\]$/, `|${parts.join("|")}]]`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (this.settings.debugMode) {
|
||
|
|
console.debug("[Image Manager] generateMarkdownLink result", {
|
||
|
|
finalLink: imageLink
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return imageLink;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get relative path from source file to target file
|
||
|
|
*/
|
||
|
|
getRelativePath(from, to) {
|
||
|
|
var _a, _b, _c, _d;
|
||
|
|
const fromDir = (_b = (_a = from.parent) == null ? void 0 : _a.path) != null ? _b : "";
|
||
|
|
const toPath = to.path;
|
||
|
|
if (!fromDir) {
|
||
|
|
return toPath;
|
||
|
|
}
|
||
|
|
const toDir = (_d = (_c = to.parent) == null ? void 0 : _c.path) != null ? _d : "";
|
||
|
|
if (fromDir === toDir) {
|
||
|
|
return to.name;
|
||
|
|
}
|
||
|
|
return toPath;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Sanitize a file name
|
||
|
|
*/
|
||
|
|
sanitizeFileName(name) {
|
||
|
|
return name.replace(/[\\/:*?"<>|]/g, "-").replace(/\s+/g, "-").replace(/^\.+/, "").replace(/\.+$/, "").trim();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get file extension from MIME type
|
||
|
|
*/
|
||
|
|
getExtensionFromMimeType(mimeType) {
|
||
|
|
var _a;
|
||
|
|
const mimeToExt = {
|
||
|
|
"image/jpeg": "jpg",
|
||
|
|
"image/jpg": "jpg",
|
||
|
|
"image/png": "png",
|
||
|
|
"image/gif": "gif",
|
||
|
|
"image/webp": "webp",
|
||
|
|
"image/svg+xml": "svg",
|
||
|
|
"image/bmp": "bmp",
|
||
|
|
"image/tiff": "tiff",
|
||
|
|
"image/avif": "avif"
|
||
|
|
};
|
||
|
|
return (_a = mimeToExt[mimeType]) != null ? _a : "png";
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if a file is an image based on extension
|
||
|
|
*/
|
||
|
|
isImageFile(file) {
|
||
|
|
const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "tiff", "avif"];
|
||
|
|
return imageExtensions.includes(file.extension.toLowerCase());
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if a URL points to an external image
|
||
|
|
*/
|
||
|
|
isExternalImageUrl(url) {
|
||
|
|
try {
|
||
|
|
const parsed = new URL(url);
|
||
|
|
if (!["http:", "https:"].includes(parsed.protocol)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const pathname = parsed.pathname.toLowerCase();
|
||
|
|
const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp", ".tiff", ".avif"];
|
||
|
|
if (imageExtensions.some((ext) => pathname.endsWith(ext))) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
const imageHosts = [
|
||
|
|
"images.unsplash.com",
|
||
|
|
"images.pexels.com",
|
||
|
|
"pixabay.com",
|
||
|
|
"i.imgur.com",
|
||
|
|
"cdn.discordapp.com"
|
||
|
|
];
|
||
|
|
return imageHosts.some((host) => parsed.hostname.includes(host));
|
||
|
|
} catch (e) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/services/ImageProcessor.ts
|
||
|
|
var import_obsidian5 = require("obsidian");
|
||
|
|
|
||
|
|
// src/utils/template.ts
|
||
|
|
function renderTemplate(template, variables, frontmatter) {
|
||
|
|
var _a, _b;
|
||
|
|
let result = template;
|
||
|
|
result = result.replace(/\{\{fileName\}\}/g, variables.fileName);
|
||
|
|
result = result.replace(/\{\{dirName\}\}/g, variables.dirName);
|
||
|
|
result = result.replace(/\{\{imageNameKey\}\}/g, (_a = variables.imageNameKey) != null ? _a : "");
|
||
|
|
result = result.replace(/\{\{firstHeading\}\}/g, (_b = variables.firstHeading) != null ? _b : "");
|
||
|
|
result = result.replace(/\{\{DATE:([^}]+)\}\}/g, (_, format) => {
|
||
|
|
return formatDate(/* @__PURE__ */ new Date(), format);
|
||
|
|
});
|
||
|
|
result = result.replace(/\{\{TIME:([^}]+)\}\}/g, (_, format) => {
|
||
|
|
return formatTime(/* @__PURE__ */ new Date(), format);
|
||
|
|
});
|
||
|
|
if (frontmatter) {
|
||
|
|
result = result.replace(/\{\{fm:([^}]+)\}\}/g, (_, key) => {
|
||
|
|
const value = frontmatter[key.trim()];
|
||
|
|
if (value == null) return "";
|
||
|
|
if (typeof value === "string") return value;
|
||
|
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
||
|
|
return "";
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
function formatDate(date, format) {
|
||
|
|
const year = date.getFullYear();
|
||
|
|
const month = date.getMonth() + 1;
|
||
|
|
const day = date.getDate();
|
||
|
|
return format.replace("YYYY", String(year)).replace("YY", String(year).slice(-2)).replace("MM", String(month).padStart(2, "0")).replace("DD", String(day).padStart(2, "0")).replace("M", String(month)).replace("D", String(day));
|
||
|
|
}
|
||
|
|
function formatTime(date, format) {
|
||
|
|
const hours = date.getHours();
|
||
|
|
const minutes = date.getMinutes();
|
||
|
|
const seconds = date.getSeconds();
|
||
|
|
return format.replace("HH", String(hours).padStart(2, "0")).replace("mm", String(minutes).padStart(2, "0")).replace("ss", String(seconds).padStart(2, "0")).replace("H", String(hours)).replace("m", String(minutes)).replace("s", String(seconds));
|
||
|
|
}
|
||
|
|
function buildTemplateVariables(app, activeFile) {
|
||
|
|
var _a, _b;
|
||
|
|
const cache = app.metadataCache.getFileCache(activeFile);
|
||
|
|
const frontmatter = cache == null ? void 0 : cache.frontmatter;
|
||
|
|
let firstHeading = "";
|
||
|
|
if (cache == null ? void 0 : cache.headings) {
|
||
|
|
for (const heading of cache.headings) {
|
||
|
|
if (heading.level === 1) {
|
||
|
|
firstHeading = heading.heading;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
fileName: activeFile.basename,
|
||
|
|
dirName: (_b = (_a = activeFile.parent) == null ? void 0 : _a.name) != null ? _b : "",
|
||
|
|
imageNameKey: frontmatter == null ? void 0 : frontmatter.imageNameKey,
|
||
|
|
firstHeading,
|
||
|
|
date: formatDate(/* @__PURE__ */ new Date(), "YYYY-MM-DD"),
|
||
|
|
time: formatTime(/* @__PURE__ */ new Date(), "HH-mm-ss")
|
||
|
|
};
|
||
|
|
}
|
||
|
|
function isTemplateMeaningful(result, delimiter) {
|
||
|
|
const meaninglessRegex = new RegExp(`[${escapeRegExp(delimiter)}\\s]`, "gm");
|
||
|
|
return result.replace(meaninglessRegex, "") !== "";
|
||
|
|
}
|
||
|
|
function escapeRegExp(string) {
|
||
|
|
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/modals/RenameModal.ts
|
||
|
|
var import_obsidian3 = require("obsidian");
|
||
|
|
var RenameModal = class extends import_obsidian3.Modal {
|
||
|
|
constructor(app, imageFile, suggestedName, onSubmit) {
|
||
|
|
super(app);
|
||
|
|
this.nameInput = null;
|
||
|
|
this.previewEl = null;
|
||
|
|
this.errorEl = null;
|
||
|
|
this.imageFile = imageFile;
|
||
|
|
this.suggestedName = suggestedName;
|
||
|
|
this.currentName = suggestedName;
|
||
|
|
this.onSubmit = onSubmit;
|
||
|
|
}
|
||
|
|
onOpen() {
|
||
|
|
const { contentEl, titleEl } = this;
|
||
|
|
this.containerEl.addClass("image-manager-rename-modal");
|
||
|
|
titleEl.setText("Rename image");
|
||
|
|
this.renderImagePreview(contentEl);
|
||
|
|
this.renderFileInfo(contentEl);
|
||
|
|
this.renderNameInput(contentEl);
|
||
|
|
this.errorEl = contentEl.createDiv({ cls: "image-manager-error image-manager-error-hidden" });
|
||
|
|
this.renderButtons(contentEl);
|
||
|
|
setTimeout(() => {
|
||
|
|
if (this.nameInput) {
|
||
|
|
this.nameInput.focus();
|
||
|
|
this.nameInput.select();
|
||
|
|
}
|
||
|
|
}, 50);
|
||
|
|
}
|
||
|
|
renderImagePreview(containerEl) {
|
||
|
|
const previewContainer = containerEl.createDiv({ cls: "image-manager-preview" });
|
||
|
|
const img = previewContainer.createEl("img", {
|
||
|
|
attr: {
|
||
|
|
src: this.app.vault.getResourcePath(this.imageFile),
|
||
|
|
alt: this.imageFile.name
|
||
|
|
}
|
||
|
|
});
|
||
|
|
img.addClass("image-manager-preview-img");
|
||
|
|
}
|
||
|
|
renderFileInfo(containerEl) {
|
||
|
|
const infoContainer = containerEl.createDiv({ cls: "image-manager-info" });
|
||
|
|
const infoList = infoContainer.createEl("ul");
|
||
|
|
const originalItem = infoList.createEl("li");
|
||
|
|
originalItem.createEl("strong", { text: "Original: " });
|
||
|
|
originalItem.createEl("span", { text: this.imageFile.path });
|
||
|
|
const newItem = infoList.createEl("li");
|
||
|
|
newItem.createEl("strong", { text: "New path: " });
|
||
|
|
this.previewEl = newItem.createEl("span", { text: this.getNewPath(this.currentName) });
|
||
|
|
}
|
||
|
|
renderNameInput(containerEl) {
|
||
|
|
new import_obsidian3.Setting(containerEl).setName("New name").setDesc("Enter a new name for the image (without extension)").addText((text) => {
|
||
|
|
this.nameInput = text.inputEl;
|
||
|
|
text.setPlaceholder("Enter name").setValue(this.currentName).onChange((value) => {
|
||
|
|
this.currentName = this.sanitizeName(value);
|
||
|
|
this.updatePreview();
|
||
|
|
});
|
||
|
|
text.inputEl.addEventListener("keydown", (e) => {
|
||
|
|
if (e.key === "Enter" && !e.isComposing) {
|
||
|
|
e.preventDefault();
|
||
|
|
this.submit();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
renderButtons(containerEl) {
|
||
|
|
new import_obsidian3.Setting(containerEl).addButton((btn) => {
|
||
|
|
btn.setButtonText("Rename").setCta().onClick(() => this.submit());
|
||
|
|
}).addButton((btn) => {
|
||
|
|
btn.setButtonText("Skip").onClick(() => this.cancel());
|
||
|
|
});
|
||
|
|
}
|
||
|
|
getNewPath(name) {
|
||
|
|
var _a, _b;
|
||
|
|
const folder = (_b = (_a = this.imageFile.parent) == null ? void 0 : _a.path) != null ? _b : "";
|
||
|
|
const extension = this.imageFile.extension;
|
||
|
|
const fileName = `${name}.${extension}`;
|
||
|
|
return folder ? `${folder}/${fileName}` : fileName;
|
||
|
|
}
|
||
|
|
updatePreview() {
|
||
|
|
if (this.previewEl) {
|
||
|
|
this.previewEl.setText(this.getNewPath(this.currentName));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
sanitizeName(name) {
|
||
|
|
return name.replace(/[\\/:*?"<>|]/g, "-").replace(/\s+/g, "-").trim();
|
||
|
|
}
|
||
|
|
showError(message) {
|
||
|
|
if (this.errorEl) {
|
||
|
|
this.errorEl.setText(message);
|
||
|
|
this.errorEl.addClass("image-manager-error-visible");
|
||
|
|
this.errorEl.removeClass("image-manager-error-hidden");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
hideError() {
|
||
|
|
if (this.errorEl) {
|
||
|
|
this.errorEl.addClass("image-manager-error-hidden");
|
||
|
|
this.errorEl.removeClass("image-manager-error-visible");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
submit() {
|
||
|
|
this.hideError();
|
||
|
|
if (!this.currentName || this.currentName.trim() === "") {
|
||
|
|
this.showError("Name cannot be empty");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.onSubmit({
|
||
|
|
newName: this.currentName,
|
||
|
|
cancelled: false
|
||
|
|
});
|
||
|
|
this.close();
|
||
|
|
}
|
||
|
|
cancel() {
|
||
|
|
this.onSubmit({
|
||
|
|
newName: "",
|
||
|
|
cancelled: true
|
||
|
|
});
|
||
|
|
this.close();
|
||
|
|
}
|
||
|
|
onClose() {
|
||
|
|
const { contentEl } = this;
|
||
|
|
contentEl.empty();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
function openRenameModal(app, imageFile, suggestedName) {
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
const modal = new RenameModal(app, imageFile, suggestedName, resolve);
|
||
|
|
modal.open();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/modals/DescriptiveImageModal.ts
|
||
|
|
var import_obsidian4 = require("obsidian");
|
||
|
|
|
||
|
|
// src/utils/kebab-case.ts
|
||
|
|
function toKebabCase(str) {
|
||
|
|
return str.toLowerCase().replace(/[<>:"/\\|?*]/g, "").replace(/['"]/g, "").replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/modals/DescriptiveImageModal.ts
|
||
|
|
var DescriptiveImageModal = class extends import_obsidian4.Modal {
|
||
|
|
constructor(app, imageFile, onSubmit, suggestedDescription) {
|
||
|
|
super(app);
|
||
|
|
this.description = "";
|
||
|
|
this.descriptionInput = null;
|
||
|
|
this.previewEl = null;
|
||
|
|
this.fileNamePreviewEl = null;
|
||
|
|
this.errorEl = null;
|
||
|
|
this.imageFile = imageFile;
|
||
|
|
this.onSubmit = onSubmit;
|
||
|
|
this.description = suggestedDescription != null ? suggestedDescription : "";
|
||
|
|
}
|
||
|
|
onOpen() {
|
||
|
|
const { contentEl, titleEl } = this;
|
||
|
|
this.containerEl.addClass("image-manager-rename-modal");
|
||
|
|
titleEl.setText("Describe image");
|
||
|
|
this.renderImagePreview(contentEl);
|
||
|
|
new import_obsidian4.Setting(contentEl).setName("Image description").setDesc("Describe this image. This will be used as display text and for the file name.").addText((text) => {
|
||
|
|
this.descriptionInput = text.inputEl;
|
||
|
|
text.setPlaceholder("A beautiful sunset over mountains").setValue(this.description).onChange((value) => {
|
||
|
|
this.description = value;
|
||
|
|
this.updatePreview();
|
||
|
|
});
|
||
|
|
text.inputEl.addEventListener("keydown", (e) => {
|
||
|
|
if (e.key === "Enter" && !e.isComposing) {
|
||
|
|
e.preventDefault();
|
||
|
|
this.submit();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
const previewContainer = contentEl.createDiv({ cls: "image-manager-info" });
|
||
|
|
previewContainer.createEl("p", { text: "Preview:" });
|
||
|
|
const fileNamePreview = previewContainer.createEl("p");
|
||
|
|
fileNamePreview.createEl("strong", { text: "Filename: " });
|
||
|
|
this.fileNamePreviewEl = fileNamePreview.createEl("span");
|
||
|
|
const linkPreview = previewContainer.createEl("p");
|
||
|
|
linkPreview.createEl("strong", { text: "Link: " });
|
||
|
|
this.previewEl = linkPreview.createEl("span", { cls: "code" });
|
||
|
|
this.errorEl = contentEl.createDiv({ cls: "image-manager-error image-manager-error-hidden" });
|
||
|
|
new import_obsidian4.Setting(contentEl).addButton((btn) => {
|
||
|
|
btn.setButtonText("Insert").setCta().onClick(() => this.submit());
|
||
|
|
}).addButton((btn) => {
|
||
|
|
btn.setButtonText("Cancel").onClick(() => this.cancel());
|
||
|
|
});
|
||
|
|
if (this.description) {
|
||
|
|
this.updatePreview();
|
||
|
|
}
|
||
|
|
setTimeout(() => {
|
||
|
|
if (this.descriptionInput) {
|
||
|
|
this.descriptionInput.focus();
|
||
|
|
}
|
||
|
|
}, 50);
|
||
|
|
}
|
||
|
|
renderImagePreview(containerEl) {
|
||
|
|
const previewContainer = containerEl.createDiv({ cls: "image-manager-preview" });
|
||
|
|
const img = previewContainer.createEl("img", {
|
||
|
|
attr: {
|
||
|
|
src: this.app.vault.getResourcePath(this.imageFile),
|
||
|
|
alt: this.imageFile.name
|
||
|
|
}
|
||
|
|
});
|
||
|
|
img.addClass("image-manager-preview-img");
|
||
|
|
}
|
||
|
|
updatePreview() {
|
||
|
|
if (!this.description || this.description.trim() === "") {
|
||
|
|
if (this.fileNamePreviewEl) {
|
||
|
|
this.fileNamePreviewEl.setText("(enter description)");
|
||
|
|
}
|
||
|
|
if (this.previewEl) {
|
||
|
|
this.previewEl.setText("(enter description)");
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const kebabName = toKebabCase(this.description);
|
||
|
|
const extension = this.imageFile.extension;
|
||
|
|
const fileName = `${kebabName}.${extension}`;
|
||
|
|
const displayText = this.description.trim();
|
||
|
|
if (this.fileNamePreviewEl) {
|
||
|
|
this.fileNamePreviewEl.setText(fileName);
|
||
|
|
}
|
||
|
|
if (this.previewEl) {
|
||
|
|
this.previewEl.setText(`![[${fileName}|${displayText}]]`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
showError(message) {
|
||
|
|
if (this.errorEl) {
|
||
|
|
this.errorEl.setText(message);
|
||
|
|
this.errorEl.addClass("image-manager-error-visible");
|
||
|
|
this.errorEl.removeClass("image-manager-error-hidden");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
hideError() {
|
||
|
|
if (this.errorEl) {
|
||
|
|
this.errorEl.addClass("image-manager-error-hidden");
|
||
|
|
this.errorEl.removeClass("image-manager-error-visible");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
submit() {
|
||
|
|
this.hideError();
|
||
|
|
if (!this.description || this.description.trim() === "") {
|
||
|
|
this.showError("Description cannot be empty");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const kebabName = toKebabCase(this.description);
|
||
|
|
if (!kebabName || kebabName === "") {
|
||
|
|
this.showError("Description must contain valid characters");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.onSubmit({
|
||
|
|
description: this.description.trim(),
|
||
|
|
fileName: kebabName,
|
||
|
|
cancelled: false
|
||
|
|
});
|
||
|
|
this.close();
|
||
|
|
}
|
||
|
|
cancel() {
|
||
|
|
this.onSubmit({
|
||
|
|
description: "",
|
||
|
|
fileName: "",
|
||
|
|
cancelled: true
|
||
|
|
});
|
||
|
|
this.close();
|
||
|
|
}
|
||
|
|
onClose() {
|
||
|
|
const { contentEl } = this;
|
||
|
|
contentEl.empty();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
function openDescriptiveImageModal(app, imageFile, suggestedDescription) {
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
const modal = new DescriptiveImageModal(app, imageFile, resolve, suggestedDescription);
|
||
|
|
modal.open();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/services/ImageProcessor.ts
|
||
|
|
var ImageProcessor = class {
|
||
|
|
constructor(app, settings, storageManager, observable) {
|
||
|
|
this.app = app;
|
||
|
|
this.settings = settings;
|
||
|
|
this.storageManager = storageManager;
|
||
|
|
observable == null ? void 0 : observable.subscribe((newSettings) => {
|
||
|
|
this.updateSettings(newSettings);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Update settings reference
|
||
|
|
*/
|
||
|
|
updateSettings(settings) {
|
||
|
|
this.settings = settings;
|
||
|
|
this.storageManager.updateSettings(settings);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Process a pasted/dropped image file
|
||
|
|
* This is called from our event handlers (user-initiated action)
|
||
|
|
* @param isPropertyInsertion - If true, skip descriptive images (only applies to note body)
|
||
|
|
*/
|
||
|
|
async processImageFile(file, activeFile, showRenameModal = true, isPropertyInsertion = false) {
|
||
|
|
try {
|
||
|
|
const arrayBuffer = await file.arrayBuffer();
|
||
|
|
const extension = this.getExtension(file);
|
||
|
|
const suggestedName = this.generateNameWithSuffix(activeFile);
|
||
|
|
let finalName = suggestedName;
|
||
|
|
if (showRenameModal && !this.settings.autoRename) {
|
||
|
|
const tempPath = await this.storageManager.getAvailablePath(
|
||
|
|
`temp-${Date.now()}`,
|
||
|
|
extension,
|
||
|
|
activeFile
|
||
|
|
);
|
||
|
|
const tempFile = await this.storageManager.saveFile(arrayBuffer, tempPath);
|
||
|
|
let finalName2;
|
||
|
|
let displayText;
|
||
|
|
if (this.settings.enableDescriptiveImages) {
|
||
|
|
const descResult = await openDescriptiveImageModal(this.app, tempFile, suggestedName);
|
||
|
|
if (descResult.cancelled) {
|
||
|
|
await this.app.fileManager.trashFile(tempFile);
|
||
|
|
return {
|
||
|
|
file: null,
|
||
|
|
path: "",
|
||
|
|
linkText: "",
|
||
|
|
success: false,
|
||
|
|
error: "Cancelled by user"
|
||
|
|
};
|
||
|
|
}
|
||
|
|
finalName2 = descResult.fileName;
|
||
|
|
displayText = descResult.description;
|
||
|
|
} else {
|
||
|
|
const result = await openRenameModal(this.app, tempFile, suggestedName);
|
||
|
|
if (result.cancelled) {
|
||
|
|
await this.app.fileManager.trashFile(tempFile);
|
||
|
|
return {
|
||
|
|
file: null,
|
||
|
|
path: "",
|
||
|
|
linkText: "",
|
||
|
|
success: false,
|
||
|
|
error: "Cancelled by user"
|
||
|
|
};
|
||
|
|
}
|
||
|
|
finalName2 = result.newName;
|
||
|
|
}
|
||
|
|
const finalPath = await this.getDeduplicatedPath(finalName2, extension, activeFile);
|
||
|
|
await this.app.fileManager.renameFile(tempFile, finalPath);
|
||
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(finalPath);
|
||
|
|
if (!(abstractFile instanceof import_obsidian5.TFile)) {
|
||
|
|
throw new Error("Renamed file not found");
|
||
|
|
}
|
||
|
|
const renamedFile = abstractFile;
|
||
|
|
const linkText = this.storageManager.generateMarkdownLink(
|
||
|
|
renamedFile,
|
||
|
|
activeFile.path,
|
||
|
|
displayText,
|
||
|
|
this.settings.insertSize
|
||
|
|
);
|
||
|
|
if (!this.settings.disableRenameNotice) {
|
||
|
|
new import_obsidian5.Notice(`Image saved as: ${renamedFile.name}`);
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
file: renamedFile,
|
||
|
|
path: finalPath,
|
||
|
|
linkText,
|
||
|
|
description: displayText,
|
||
|
|
success: true
|
||
|
|
};
|
||
|
|
} else {
|
||
|
|
const finalPath = await this.getDeduplicatedPath(finalName, extension, activeFile);
|
||
|
|
const savedFile = await this.storageManager.saveFile(arrayBuffer, finalPath);
|
||
|
|
const linkText = this.storageManager.generateMarkdownLink(
|
||
|
|
savedFile,
|
||
|
|
activeFile.path,
|
||
|
|
void 0,
|
||
|
|
this.settings.insertSize
|
||
|
|
);
|
||
|
|
if (!this.settings.disableRenameNotice) {
|
||
|
|
new import_obsidian5.Notice(`Image saved as: ${savedFile.name}`);
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
file: savedFile,
|
||
|
|
path: finalPath,
|
||
|
|
linkText,
|
||
|
|
description: void 0,
|
||
|
|
success: true
|
||
|
|
};
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Error processing image:", error);
|
||
|
|
return {
|
||
|
|
file: null,
|
||
|
|
path: "",
|
||
|
|
linkText: "",
|
||
|
|
success: false,
|
||
|
|
error: error instanceof Error ? error.message : String(error)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Process an image from a URL (download and save locally)
|
||
|
|
* @param isPropertyInsertion - If true, skip descriptive images (only applies to note body)
|
||
|
|
* @param suggestedNameOverride - Optional override for suggested name (e.g., from search term)
|
||
|
|
*/
|
||
|
|
async processImageUrl(url, activeFile, showRenameModal = true, isPropertyInsertion = false, suggestedNameOverride) {
|
||
|
|
var _a;
|
||
|
|
try {
|
||
|
|
const response = await (0, import_obsidian5.requestUrl)({ url });
|
||
|
|
if (response.status >= 400) {
|
||
|
|
throw new Error(`Failed to download image: ${response.status}`);
|
||
|
|
}
|
||
|
|
const arrayBuffer = response.arrayBuffer;
|
||
|
|
const contentType = (_a = response.headers["content-type"]) != null ? _a : "image/png";
|
||
|
|
const extension = this.storageManager.getExtensionFromMimeType(contentType);
|
||
|
|
const suggestedName = this.generateNameWithSuffix(activeFile, suggestedNameOverride);
|
||
|
|
let finalName = suggestedName;
|
||
|
|
if (showRenameModal && !this.settings.autoRename) {
|
||
|
|
const tempPath = await this.storageManager.getAvailablePath(
|
||
|
|
`temp-${Date.now()}`,
|
||
|
|
extension,
|
||
|
|
activeFile
|
||
|
|
);
|
||
|
|
const tempFile = await this.storageManager.saveFile(arrayBuffer, tempPath);
|
||
|
|
let finalName2;
|
||
|
|
let displayText;
|
||
|
|
const shouldShowDescriptive = this.settings.enableDescriptiveImages && (!isPropertyInsertion || this.settings.altTextProperty !== "");
|
||
|
|
if (shouldShowDescriptive) {
|
||
|
|
const descResult = await openDescriptiveImageModal(this.app, tempFile, suggestedName);
|
||
|
|
if (descResult.cancelled) {
|
||
|
|
await this.app.fileManager.trashFile(tempFile);
|
||
|
|
return {
|
||
|
|
file: null,
|
||
|
|
path: "",
|
||
|
|
linkText: "",
|
||
|
|
success: false,
|
||
|
|
error: "Cancelled by user"
|
||
|
|
};
|
||
|
|
}
|
||
|
|
finalName2 = descResult.fileName;
|
||
|
|
displayText = descResult.description;
|
||
|
|
} else {
|
||
|
|
const result = await openRenameModal(this.app, tempFile, suggestedName);
|
||
|
|
if (result.cancelled) {
|
||
|
|
await this.app.fileManager.trashFile(tempFile);
|
||
|
|
return {
|
||
|
|
file: null,
|
||
|
|
path: "",
|
||
|
|
linkText: "",
|
||
|
|
success: false,
|
||
|
|
error: "Cancelled by user"
|
||
|
|
};
|
||
|
|
}
|
||
|
|
finalName2 = result.newName;
|
||
|
|
}
|
||
|
|
const finalPath = await this.getDeduplicatedPath(finalName2, extension, activeFile);
|
||
|
|
await this.app.fileManager.renameFile(tempFile, finalPath);
|
||
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(finalPath);
|
||
|
|
if (!(abstractFile instanceof import_obsidian5.TFile)) {
|
||
|
|
throw new Error("Renamed file not found");
|
||
|
|
}
|
||
|
|
const renamedFile = abstractFile;
|
||
|
|
const linkText = this.storageManager.generateMarkdownLink(
|
||
|
|
renamedFile,
|
||
|
|
activeFile.path,
|
||
|
|
displayText,
|
||
|
|
this.settings.insertSize
|
||
|
|
);
|
||
|
|
if (!this.settings.disableRenameNotice) {
|
||
|
|
new import_obsidian5.Notice(`Image downloaded and saved as: ${renamedFile.name}`);
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
file: renamedFile,
|
||
|
|
path: finalPath,
|
||
|
|
linkText,
|
||
|
|
description: displayText,
|
||
|
|
success: true
|
||
|
|
};
|
||
|
|
} else {
|
||
|
|
const finalPath = await this.getDeduplicatedPath(finalName, extension, activeFile);
|
||
|
|
const savedFile = await this.storageManager.saveFile(arrayBuffer, finalPath);
|
||
|
|
const linkText = this.storageManager.generateMarkdownLink(
|
||
|
|
savedFile,
|
||
|
|
activeFile.path,
|
||
|
|
void 0,
|
||
|
|
this.settings.insertSize
|
||
|
|
);
|
||
|
|
if (!this.settings.disableRenameNotice) {
|
||
|
|
new import_obsidian5.Notice(`Image downloaded and saved as: ${savedFile.name}`);
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
file: savedFile,
|
||
|
|
path: finalPath,
|
||
|
|
linkText,
|
||
|
|
description: isPropertyInsertion ? suggestedNameOverride : void 0,
|
||
|
|
success: true
|
||
|
|
};
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Error processing image URL:", error);
|
||
|
|
return {
|
||
|
|
file: null,
|
||
|
|
path: "",
|
||
|
|
linkText: "",
|
||
|
|
success: false,
|
||
|
|
error: error instanceof Error ? error.message : String(error)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Generate a suggested name based on the template and optional suffix
|
||
|
|
*/
|
||
|
|
generateNameWithSuffix(activeFile, suffix) {
|
||
|
|
const variables = buildTemplateVariables(this.app, activeFile);
|
||
|
|
const rendered = renderTemplate(this.settings.imageNameTemplate, variables);
|
||
|
|
const isMeaningful = isTemplateMeaningful(rendered, this.settings.dupNumberDelimiter);
|
||
|
|
const base = isMeaningful ? rendered : "";
|
||
|
|
if (base && suffix) {
|
||
|
|
return `${base} - ${suffix}`;
|
||
|
|
} else if (base) {
|
||
|
|
return `${base} - `;
|
||
|
|
} else if (suffix) {
|
||
|
|
return suffix;
|
||
|
|
}
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Generate a suggested name based on the template
|
||
|
|
*/
|
||
|
|
generateSuggestedName(activeFile) {
|
||
|
|
return this.generateNameWithSuffix(activeFile);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get a deduplicated file path
|
||
|
|
*/
|
||
|
|
async getDeduplicatedPath(baseName, extension, activeFile) {
|
||
|
|
return await this.storageManager.getAvailablePath(baseName, extension, activeFile);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get file extension from File object
|
||
|
|
*/
|
||
|
|
getExtension(file) {
|
||
|
|
var _a;
|
||
|
|
const nameParts = file.name.split(".");
|
||
|
|
if (nameParts.length > 1) {
|
||
|
|
const nameExt = (_a = nameParts[nameParts.length - 1]) == null ? void 0 : _a.toLowerCase();
|
||
|
|
if (nameExt) {
|
||
|
|
return nameExt;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return this.storageManager.getExtensionFromMimeType(file.type);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Insert link text at cursor position
|
||
|
|
*/
|
||
|
|
insertLinkAtCursor(linkText) {
|
||
|
|
const view = this.app.workspace.getActiveViewOfType(import_obsidian5.MarkdownView);
|
||
|
|
if (view == null ? void 0 : view.editor) {
|
||
|
|
view.editor.replaceSelection(linkText);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get the active markdown file
|
||
|
|
*/
|
||
|
|
getActiveFile() {
|
||
|
|
var _a;
|
||
|
|
const view = this.app.workspace.getActiveViewOfType(import_obsidian5.MarkdownView);
|
||
|
|
return (_a = view == null ? void 0 : view.file) != null ? _a : null;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Rename an existing image file with optional rename modal
|
||
|
|
* Used by LocalConversionService to rename converted images
|
||
|
|
*/
|
||
|
|
async renameImageFile(imageFile, suggestedName, activeFile) {
|
||
|
|
try {
|
||
|
|
const extension = imageFile.extension;
|
||
|
|
let finalName = suggestedName;
|
||
|
|
let displayText = "";
|
||
|
|
if (this.settings.enableDescriptiveImages) {
|
||
|
|
const descResult = await openDescriptiveImageModal(this.app, imageFile, suggestedName);
|
||
|
|
if (descResult.cancelled) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
displayText = descResult.description;
|
||
|
|
finalName = descResult.fileName;
|
||
|
|
} else if (!this.settings.autoRename) {
|
||
|
|
const result = await openRenameModal(
|
||
|
|
this.app,
|
||
|
|
imageFile,
|
||
|
|
finalName
|
||
|
|
);
|
||
|
|
if (result.cancelled) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
finalName = result.newName;
|
||
|
|
}
|
||
|
|
const finalPath = await this.getDeduplicatedPath(finalName, extension, activeFile);
|
||
|
|
await this.app.fileManager.renameFile(imageFile, finalPath);
|
||
|
|
const abstractFile = this.app.vault.getAbstractFileByPath(finalPath);
|
||
|
|
if (!(abstractFile instanceof import_obsidian5.TFile)) {
|
||
|
|
throw new Error("Renamed file not found");
|
||
|
|
}
|
||
|
|
const renamedFile = abstractFile;
|
||
|
|
const linkText = this.storageManager.generateMarkdownLink(
|
||
|
|
renamedFile,
|
||
|
|
activeFile.path,
|
||
|
|
displayText,
|
||
|
|
this.settings.insertSize
|
||
|
|
);
|
||
|
|
return {
|
||
|
|
file: renamedFile,
|
||
|
|
path: finalPath,
|
||
|
|
linkText,
|
||
|
|
success: true
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Error renaming image file:", error);
|
||
|
|
return {
|
||
|
|
file: null,
|
||
|
|
path: "",
|
||
|
|
linkText: "",
|
||
|
|
success: false,
|
||
|
|
error: error instanceof Error ? error.message : String(error)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Debug logging
|
||
|
|
*/
|
||
|
|
log(...args) {
|
||
|
|
if (this.settings.debugMode) {
|
||
|
|
console.debug("[Image Manager]", ...args);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/services/PropertyHandler.ts
|
||
|
|
var import_obsidian7 = require("obsidian");
|
||
|
|
|
||
|
|
// src/utils/mdx-frontmatter.ts
|
||
|
|
var import_obsidian6 = require("obsidian");
|
||
|
|
function isMdxFile(file) {
|
||
|
|
return file.extension === "mdx";
|
||
|
|
}
|
||
|
|
function isMarkdownFile(file) {
|
||
|
|
return file.extension === "md" || file.extension === "mdx";
|
||
|
|
}
|
||
|
|
function parseMdxFrontmatter(content) {
|
||
|
|
var _a;
|
||
|
|
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/;
|
||
|
|
const match = content.match(frontmatterRegex);
|
||
|
|
if (!match) {
|
||
|
|
return {
|
||
|
|
frontmatter: {},
|
||
|
|
body: content
|
||
|
|
};
|
||
|
|
}
|
||
|
|
const frontmatterText = (_a = match[1]) != null ? _a : "";
|
||
|
|
const bodyContent = content.slice(match[0].length);
|
||
|
|
try {
|
||
|
|
const parsed = (0, import_obsidian6.parseYaml)(frontmatterText);
|
||
|
|
const frontmatter = parsed && typeof parsed === "object" ? parsed : {};
|
||
|
|
return {
|
||
|
|
frontmatter,
|
||
|
|
body: bodyContent
|
||
|
|
};
|
||
|
|
} catch (e) {
|
||
|
|
console.error("Error parsing MDX properties:", e);
|
||
|
|
return {
|
||
|
|
frontmatter: {},
|
||
|
|
body: bodyContent
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async function readMdxFrontmatter(app, file) {
|
||
|
|
if (!isMdxFile(file)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const content = await app.vault.read(file);
|
||
|
|
const parsed = parseMdxFrontmatter(content);
|
||
|
|
return parsed ? parsed.frontmatter : null;
|
||
|
|
} catch (e) {
|
||
|
|
console.error(`Error reading MDX properties from ${file.path}:`, e);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async function processMdxFrontMatter(app, file, callback) {
|
||
|
|
if (!isMdxFile(file)) {
|
||
|
|
throw new Error(`File ${file.path} is not an MDX file`);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const content = await app.vault.read(file);
|
||
|
|
const parsed = parseMdxFrontmatter(content);
|
||
|
|
if (!parsed) {
|
||
|
|
throw new Error("Failed to parse existing frontmatter");
|
||
|
|
}
|
||
|
|
const frontmatter = { ...parsed.frontmatter };
|
||
|
|
callback(frontmatter);
|
||
|
|
const newFrontmatterText = (0, import_obsidian6.stringifyYaml)(frontmatter).trim();
|
||
|
|
const newContent = `---
|
||
|
|
${newFrontmatterText}
|
||
|
|
---
|
||
|
|
${parsed.body}`;
|
||
|
|
await app.vault.modify(file, newContent);
|
||
|
|
} catch (e) {
|
||
|
|
console.error(`Error processing MDX properties for ${file.path}:`, e);
|
||
|
|
throw e;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async function getFrontmatter(app, file) {
|
||
|
|
var _a;
|
||
|
|
if (isMdxFile(file)) {
|
||
|
|
return await readMdxFrontmatter(app, file);
|
||
|
|
}
|
||
|
|
const cache = app.metadataCache.getFileCache(file);
|
||
|
|
return (_a = cache == null ? void 0 : cache.frontmatter) != null ? _a : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/services/PropertyHandler.ts
|
||
|
|
var PropertyHandler = class {
|
||
|
|
constructor(app, settings, storageManager, imageProcessor, remoteService, observable) {
|
||
|
|
this.app = app;
|
||
|
|
this.settings = settings;
|
||
|
|
this.storageManager = storageManager;
|
||
|
|
this.imageProcessor = imageProcessor;
|
||
|
|
this.remoteService = remoteService;
|
||
|
|
observable == null ? void 0 : observable.subscribe((newSettings) => {
|
||
|
|
this.updateSettings(newSettings);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Update settings reference
|
||
|
|
*/
|
||
|
|
updateSettings(settings) {
|
||
|
|
var _a;
|
||
|
|
this.settings = settings;
|
||
|
|
(_a = this.imageProcessor) == null ? void 0 : _a.updateSettings(settings);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set an image property value in frontmatter
|
||
|
|
*/
|
||
|
|
async setPropertyValue(noteFile, propertyName, imageFile, altText) {
|
||
|
|
const linkValue = this.formatPropertyLink(imageFile, noteFile);
|
||
|
|
try {
|
||
|
|
if (isMdxFile(noteFile)) {
|
||
|
|
await this.setMdxProperty(noteFile, propertyName, linkValue, altText);
|
||
|
|
} else {
|
||
|
|
await this.setMdProperty(noteFile, propertyName, linkValue, altText);
|
||
|
|
}
|
||
|
|
new import_obsidian7.Notice(`Image added to property: ${propertyName}`);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to update property:", error);
|
||
|
|
new import_obsidian7.Notice(`Failed to update property: ${error instanceof Error ? error.message : String(error)}`);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set property in MD file using Obsidian's API
|
||
|
|
*/
|
||
|
|
async setMdProperty(file, propertyName, value, altText) {
|
||
|
|
await this.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||
|
|
frontmatter[propertyName] = value;
|
||
|
|
if (altText && this.settings.altTextProperty) {
|
||
|
|
frontmatter[this.settings.altTextProperty] = altText;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set property in MDX file using custom handler
|
||
|
|
*/
|
||
|
|
async setMdxProperty(file, propertyName, value, altText) {
|
||
|
|
await processMdxFrontMatter(this.app, file, (frontmatter) => {
|
||
|
|
frontmatter[propertyName] = value;
|
||
|
|
if (altText && this.settings.altTextProperty) {
|
||
|
|
frontmatter[this.settings.altTextProperty] = altText;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Format the image link according to settings
|
||
|
|
* Public method so PasteHandler can get the formatted value for UI updates
|
||
|
|
*/
|
||
|
|
formatPropertyLink(imageFile, noteFile) {
|
||
|
|
if (this.settings.propertyLinkFormat === "obsidian" /* ObsidianDefault */) {
|
||
|
|
const generatedLink = this.app.fileManager.generateMarkdownLink(imageFile, noteFile.path);
|
||
|
|
if (generatedLink.startsWith("![") && generatedLink.includes("]]")) {
|
||
|
|
return generatedLink.substring(1);
|
||
|
|
} else if (generatedLink.startsWith(") {
|
||
|
|
const match = generatedLink.match(/!\[.*?\]\((.*?)\)/);
|
||
|
|
return match && match[1] ? match[1] : generatedLink;
|
||
|
|
} else if (generatedLink.startsWith("[[") && generatedLink.endsWith("]]")) {
|
||
|
|
return generatedLink;
|
||
|
|
} else if (generatedLink.includes("](")) {
|
||
|
|
const match = generatedLink.match(/\[.*?\]\((.*?)\)/);
|
||
|
|
return match && match[1] ? match[1] : generatedLink;
|
||
|
|
}
|
||
|
|
return generatedLink;
|
||
|
|
}
|
||
|
|
let pathToUse;
|
||
|
|
switch (this.settings.propertyLinkFormat) {
|
||
|
|
case "relative" /* RelativePath */:
|
||
|
|
pathToUse = `./${imageFile.name}`;
|
||
|
|
break;
|
||
|
|
case "custom" /* Custom */:
|
||
|
|
pathToUse = imageFile.name;
|
||
|
|
break;
|
||
|
|
case "path" /* Path */:
|
||
|
|
default:
|
||
|
|
pathToUse = this.getRelativePath(noteFile, imageFile);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
switch (this.settings.propertyLinkFormat) {
|
||
|
|
case "wikilink" /* Wikilink */:
|
||
|
|
return `[[${pathToUse}]]`;
|
||
|
|
case "markdown" /* Markdown */:
|
||
|
|
return `})`;
|
||
|
|
case "custom" /* Custom */:
|
||
|
|
return this.settings.customPropertyLinkFormat.replace(
|
||
|
|
/\{image-url\}/gi,
|
||
|
|
pathToUse
|
||
|
|
);
|
||
|
|
case "relative" /* RelativePath */:
|
||
|
|
case "path" /* Path */:
|
||
|
|
default:
|
||
|
|
return pathToUse;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get relative path from note to image
|
||
|
|
*/
|
||
|
|
getRelativePath(fromFile, toFile) {
|
||
|
|
var _a;
|
||
|
|
const vaultConfig = this.app.vault.config;
|
||
|
|
const useMarkdownLinks = (_a = vaultConfig == null ? void 0 : vaultConfig.useMarkdownLinks) != null ? _a : false;
|
||
|
|
const useWikilinks = !useMarkdownLinks;
|
||
|
|
if (useWikilinks && this.settings.propertyLinkFormat === "wikilink" /* Wikilink */) {
|
||
|
|
return toFile.name;
|
||
|
|
}
|
||
|
|
return this.storageManager.getRelativePath(fromFile, toFile);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get the current value of a property
|
||
|
|
*/
|
||
|
|
getPropertyValue(file, propertyName) {
|
||
|
|
var _a;
|
||
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
||
|
|
return (_a = cache == null ? void 0 : cache.frontmatter) == null ? void 0 : _a[propertyName];
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if a property exists in frontmatter
|
||
|
|
*/
|
||
|
|
hasProperty(file, propertyName) {
|
||
|
|
var _a;
|
||
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
||
|
|
return ((_a = cache == null ? void 0 : cache.frontmatter) == null ? void 0 : _a[propertyName]) !== void 0;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Insert an image from a URL into a property
|
||
|
|
* Downloads the image, saves it locally, and sets the property
|
||
|
|
* @param remoteImage Optional RemoteImage object for generating referral text
|
||
|
|
* @param suggestedNameOverride Optional override for suggested name (e.g., from search term)
|
||
|
|
*/
|
||
|
|
async insertImageFromUrl(imageUrl, noteFile, propertyName, remoteImage, suggestedNameOverride) {
|
||
|
|
const result = await this.imageProcessor.processImageUrl(
|
||
|
|
imageUrl,
|
||
|
|
noteFile,
|
||
|
|
true,
|
||
|
|
// Show rename modal if enabled
|
||
|
|
true,
|
||
|
|
// isPropertyInsertion - skip descriptive images
|
||
|
|
suggestedNameOverride
|
||
|
|
// Pass search term as suggested name
|
||
|
|
);
|
||
|
|
if (!result.success || !result.file) {
|
||
|
|
throw new Error(result.error || "Failed to process image");
|
||
|
|
}
|
||
|
|
await this.setPropertyValue(noteFile, propertyName, result.file, result.description);
|
||
|
|
if (this.settings.appendReferral && remoteImage && this.remoteService) {
|
||
|
|
const referralText = this.remoteService.generateReferralText(remoteImage);
|
||
|
|
if (referralText) {
|
||
|
|
const content = await this.app.vault.read(noteFile);
|
||
|
|
const updatedContent = content + referralText;
|
||
|
|
await this.app.vault.modify(noteFile, updatedContent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/services/PasteHandler.ts
|
||
|
|
var import_obsidian8 = require("obsidian");
|
||
|
|
var PasteHandler = class {
|
||
|
|
constructor(app, settings, imageProcessor, propertyHandler, observable) {
|
||
|
|
this.app = app;
|
||
|
|
this.settings = settings;
|
||
|
|
this.imageProcessor = imageProcessor;
|
||
|
|
this.propertyHandler = propertyHandler;
|
||
|
|
observable == null ? void 0 : observable.subscribe((newSettings) => {
|
||
|
|
this.updateSettings(newSettings);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Update settings reference
|
||
|
|
*/
|
||
|
|
updateSettings(settings) {
|
||
|
|
this.settings = settings;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Handle editor paste event
|
||
|
|
* This is registered via workspace.on('editor-paste')
|
||
|
|
*/
|
||
|
|
async handleEditorPaste(evt, editor, view) {
|
||
|
|
var _a;
|
||
|
|
if (!this.settings.showRenameDialog || !this.settings.enableRenameOnPaste) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const activeEl = document.activeElement;
|
||
|
|
if (activeEl && this.isFrontmatterField(activeEl)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const files = (_a = evt.clipboardData) == null ? void 0 : _a.files;
|
||
|
|
if (!files || files.length === 0) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const imageFiles = [];
|
||
|
|
for (let i = 0; i < files.length; i++) {
|
||
|
|
const file = files.item(i);
|
||
|
|
if (file && file.type.startsWith("image/")) {
|
||
|
|
imageFiles.push(file);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (imageFiles.length === 0) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
evt.preventDefault();
|
||
|
|
const activeFile = view.file;
|
||
|
|
if (!activeFile) {
|
||
|
|
new import_obsidian8.Notice("No active file");
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
for (let i = 0; i < imageFiles.length; i++) {
|
||
|
|
const imageFile = imageFiles[i];
|
||
|
|
if (!imageFile) continue;
|
||
|
|
const result = await this.imageProcessor.processImageFile(
|
||
|
|
imageFile,
|
||
|
|
activeFile,
|
||
|
|
true
|
||
|
|
// Show rename modal
|
||
|
|
);
|
||
|
|
if (result.success && result.linkText) {
|
||
|
|
editor.replaceSelection(result.linkText);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Handle paste into frontmatter property
|
||
|
|
* This is registered via document paste event with property detection
|
||
|
|
*/
|
||
|
|
async handlePropertyPaste(evt) {
|
||
|
|
var _a;
|
||
|
|
if (!this.settings.showRenameDialog || !this.settings.enablePropertyPaste) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const activeEl = document.activeElement;
|
||
|
|
if (!activeEl) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (!this.isFrontmatterField(activeEl)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (this.settings.debugMode) {
|
||
|
|
console.debug("[Image Manager] Property paste detected", {
|
||
|
|
activeElement: activeEl.tagName,
|
||
|
|
classes: activeEl.className,
|
||
|
|
propertyName: this.getPropertyName(activeEl)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const files = (_a = evt.clipboardData) == null ? void 0 : _a.files;
|
||
|
|
if (!files || files.length === 0) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
let imageFile = null;
|
||
|
|
for (let i = 0; i < files.length; i++) {
|
||
|
|
const f = files.item(i);
|
||
|
|
if (f && f.type.startsWith("image/")) {
|
||
|
|
imageFile = f;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!imageFile) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const currentEl = document.activeElement;
|
||
|
|
if (!currentEl || !this.isFrontmatterField(currentEl)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const activeFile = this.app.workspace.getActiveFile();
|
||
|
|
if (!activeFile) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
evt.preventDefault();
|
||
|
|
evt.stopPropagation();
|
||
|
|
evt.stopImmediatePropagation();
|
||
|
|
const propertyName = this.getPropertyName(currentEl);
|
||
|
|
if (!propertyName) {
|
||
|
|
new import_obsidian8.Notice("Could not determine property name");
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
const result = await this.imageProcessor.processImageFile(
|
||
|
|
imageFile,
|
||
|
|
activeFile,
|
||
|
|
true,
|
||
|
|
// Show rename modal for property paste
|
||
|
|
true
|
||
|
|
// isPropertyInsertion - skip descriptive images
|
||
|
|
);
|
||
|
|
if (result.success && result.file) {
|
||
|
|
const linkValue = this.propertyHandler.formatPropertyLink(result.file, activeFile);
|
||
|
|
await this.propertyHandler.setPropertyValue(
|
||
|
|
activeFile,
|
||
|
|
propertyName,
|
||
|
|
result.file,
|
||
|
|
result.description
|
||
|
|
);
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||
|
|
const propertyEl = document.querySelector(
|
||
|
|
`.metadata-property[data-property-key="${propertyName}"]`
|
||
|
|
);
|
||
|
|
if (this.settings.debugMode) {
|
||
|
|
console.debug("[Image Manager] Updating property UI", {
|
||
|
|
propertyName,
|
||
|
|
linkValue,
|
||
|
|
propertyElFound: !!propertyEl
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const inputEl = propertyEl == null ? void 0 : propertyEl.querySelector(
|
||
|
|
".metadata-input-longtext, .metadata-input-text, input.metadata-input, textarea.metadata-input"
|
||
|
|
);
|
||
|
|
if (inputEl) {
|
||
|
|
if (this.settings.debugMode) {
|
||
|
|
const currentValue = inputEl instanceof HTMLInputElement || inputEl instanceof HTMLTextAreaElement ? inputEl.value : inputEl.textContent || inputEl.innerText;
|
||
|
|
console.debug("[Image Manager] Found input field, updating value", {
|
||
|
|
elementType: inputEl.tagName,
|
||
|
|
currentValue,
|
||
|
|
newValue: linkValue
|
||
|
|
});
|
||
|
|
}
|
||
|
|
if (inputEl instanceof HTMLInputElement || inputEl instanceof HTMLTextAreaElement) {
|
||
|
|
inputEl.value = linkValue;
|
||
|
|
} else {
|
||
|
|
inputEl.textContent = linkValue;
|
||
|
|
inputEl.innerText = linkValue;
|
||
|
|
}
|
||
|
|
const inputEvent = new Event("input", { bubbles: true, cancelable: true });
|
||
|
|
const changeEvent = new Event("change", { bubbles: true, cancelable: true });
|
||
|
|
const blurEvent = new Event("blur", { bubbles: true, cancelable: true });
|
||
|
|
inputEl.dispatchEvent(inputEvent);
|
||
|
|
setTimeout(() => {
|
||
|
|
inputEl.dispatchEvent(changeEvent);
|
||
|
|
if (inputEl instanceof HTMLElement) {
|
||
|
|
inputEl.focus();
|
||
|
|
setTimeout(() => {
|
||
|
|
inputEl.blur();
|
||
|
|
inputEl.dispatchEvent(blurEvent);
|
||
|
|
setTimeout(() => {
|
||
|
|
const view = this.app.workspace.getActiveViewOfType(import_obsidian8.MarkdownView);
|
||
|
|
if (view == null ? void 0 : view.editor) {
|
||
|
|
view.editor.focus();
|
||
|
|
}
|
||
|
|
}, 50);
|
||
|
|
}, 50);
|
||
|
|
}
|
||
|
|
}, 50);
|
||
|
|
} else {
|
||
|
|
const view = this.app.workspace.getActiveViewOfType(import_obsidian8.MarkdownView);
|
||
|
|
if (view == null ? void 0 : view.editor) {
|
||
|
|
view.editor.focus();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if an element is a supported frontmatter field
|
||
|
|
* Works for both MD and MDX files
|
||
|
|
*/
|
||
|
|
isFrontmatterField(element) {
|
||
|
|
const propertyEl = element.closest(".metadata-property");
|
||
|
|
if (!propertyEl) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return element.matches(".metadata-input-longtext") || element.matches(".metadata-input-text") || element.matches("input.metadata-input") || element.matches("textarea.metadata-input") || // Also check if the element itself is an input/textarea/div inside a property
|
||
|
|
(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLDivElement && element.classList.contains("metadata-input-longtext")) && propertyEl !== null;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get the property name from a frontmatter field element
|
||
|
|
*/
|
||
|
|
getPropertyName(element) {
|
||
|
|
var _a;
|
||
|
|
const propertyEl = element.closest(".metadata-property");
|
||
|
|
return (_a = propertyEl == null ? void 0 : propertyEl.getAttribute("data-property-key")) != null ? _a : null;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var DropHandler = class {
|
||
|
|
constructor(app, settings, imageProcessor, observable) {
|
||
|
|
this.app = app;
|
||
|
|
this.settings = settings;
|
||
|
|
this.imageProcessor = imageProcessor;
|
||
|
|
observable == null ? void 0 : observable.subscribe((newSettings) => {
|
||
|
|
this.updateSettings(newSettings);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Update settings reference
|
||
|
|
*/
|
||
|
|
updateSettings(settings) {
|
||
|
|
this.settings = settings;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Handle editor drop event
|
||
|
|
*/
|
||
|
|
async handleEditorDrop(evt, editor, view) {
|
||
|
|
var _a;
|
||
|
|
if (!this.settings.showRenameDialog || !this.settings.enableRenameOnDrop) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const files = (_a = evt.dataTransfer) == null ? void 0 : _a.files;
|
||
|
|
if (!files || files.length === 0) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const imageFiles = [];
|
||
|
|
for (let i = 0; i < files.length; i++) {
|
||
|
|
const f = files.item(i);
|
||
|
|
if (f && f.type.startsWith("image/")) {
|
||
|
|
imageFiles.push(f);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (imageFiles.length === 0) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
evt.preventDefault();
|
||
|
|
const activeFile = view.file;
|
||
|
|
if (!activeFile) {
|
||
|
|
new import_obsidian8.Notice("No active file");
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
for (let i = 0; i < imageFiles.length; i++) {
|
||
|
|
const imageFile = imageFiles[i];
|
||
|
|
if (!imageFile) continue;
|
||
|
|
const result = await this.imageProcessor.processImageFile(
|
||
|
|
imageFile,
|
||
|
|
activeFile,
|
||
|
|
true
|
||
|
|
);
|
||
|
|
if (result.success && result.linkText) {
|
||
|
|
editor.replaceSelection(result.linkText);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/services/RemoteImageService.ts
|
||
|
|
var import_obsidian9 = require("obsidian");
|
||
|
|
var UNSPLASH_PROXY = "https://insert-unsplash-image.cloudy9101.com/";
|
||
|
|
var RemoteImageService = class {
|
||
|
|
constructor(app, settings, observable) {
|
||
|
|
this.app = app;
|
||
|
|
this.settings = settings;
|
||
|
|
observable == null ? void 0 : observable.subscribe((newSettings) => {
|
||
|
|
this.updateSettings(newSettings);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Update settings reference
|
||
|
|
*/
|
||
|
|
updateSettings(settings) {
|
||
|
|
this.settings = settings;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Search for images from the specified provider
|
||
|
|
*/
|
||
|
|
async search(query, provider, page = 1) {
|
||
|
|
const targetProvider = provider != null ? provider : this.settings.defaultProvider;
|
||
|
|
switch (targetProvider) {
|
||
|
|
case "unsplash" /* Unsplash */:
|
||
|
|
return await this.searchUnsplash(query, page);
|
||
|
|
case "pexels" /* Pexels */:
|
||
|
|
return await this.searchPexels(query, page);
|
||
|
|
case "pixabay" /* Pixabay */:
|
||
|
|
return await this.searchPixabay(query, page);
|
||
|
|
default:
|
||
|
|
throw new Error(`Unsupported provider: ${targetProvider}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Search Unsplash
|
||
|
|
*/
|
||
|
|
async searchUnsplash(query, page) {
|
||
|
|
var _a;
|
||
|
|
let proxyUrl = this.settings.unsplashProxyServer || UNSPLASH_PROXY;
|
||
|
|
if (!proxyUrl.endsWith("/")) {
|
||
|
|
proxyUrl += "/";
|
||
|
|
}
|
||
|
|
const orientation = this.mapOrientation(this.settings.defaultOrientation);
|
||
|
|
const url = new URL("/search/photos", proxyUrl);
|
||
|
|
url.searchParams.set("query", query);
|
||
|
|
url.searchParams.set("page", String(page));
|
||
|
|
url.searchParams.set("per_page", "20");
|
||
|
|
if (orientation) {
|
||
|
|
url.searchParams.set("orientation", orientation);
|
||
|
|
}
|
||
|
|
const response = await (0, import_obsidian9.requestUrl)({ url: url.toString() });
|
||
|
|
if (response.status >= 400) {
|
||
|
|
console.error("Unsplash API error:", response.status, response.text);
|
||
|
|
throw new Error(`Unsplash search failed: ${response.status} - ${response.text}`);
|
||
|
|
}
|
||
|
|
const data = response.json;
|
||
|
|
if (!data || !data.results) {
|
||
|
|
console.error("Invalid Unsplash response:", data);
|
||
|
|
throw new Error("Invalid response from Unsplash API");
|
||
|
|
}
|
||
|
|
const results = (_a = data.results) != null ? _a : [];
|
||
|
|
return results.map((photo) => this.mapUnsplashPhoto(photo));
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get Pexels API key from SecretStorage or fall back to plaintext
|
||
|
|
*/
|
||
|
|
getPexelsApiKey() {
|
||
|
|
if ((0, import_obsidian9.requireApiVersion)("1.11.4") && this.settings.pexelsApiKeySecretId) {
|
||
|
|
const secretStorage = this.app.secretStorage;
|
||
|
|
if (secretStorage) {
|
||
|
|
const secret = secretStorage.getSecret(this.settings.pexelsApiKeySecretId);
|
||
|
|
if (secret) {
|
||
|
|
return secret;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return this.settings.pexelsApiKey || null;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get Pixabay API key from SecretStorage or fall back to plaintext
|
||
|
|
*/
|
||
|
|
getPixabayApiKey() {
|
||
|
|
if ((0, import_obsidian9.requireApiVersion)("1.11.4") && this.settings.pixabayApiKeySecretId) {
|
||
|
|
const secretStorage = this.app.secretStorage;
|
||
|
|
if (secretStorage) {
|
||
|
|
const secret = secretStorage.getSecret(this.settings.pixabayApiKeySecretId);
|
||
|
|
if (secret) {
|
||
|
|
return secret;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return this.settings.pixabayApiKey || null;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Search Pexels
|
||
|
|
*/
|
||
|
|
async searchPexels(query, page) {
|
||
|
|
var _a;
|
||
|
|
const apiKey = this.getPexelsApiKey();
|
||
|
|
if (!apiKey) {
|
||
|
|
const errorMsg = (0, import_obsidian9.requireApiVersion)("1.11.4") ? "Pexels API key is required. Please configure it in settings (use SecretStorage on Obsidian 1.11.4+ or enter plaintext on older versions)." : "Pexels API key is required. Please configure it in settings.";
|
||
|
|
throw new Error(errorMsg);
|
||
|
|
}
|
||
|
|
const orientation = this.mapOrientation(this.settings.defaultOrientation);
|
||
|
|
const params = new URLSearchParams({
|
||
|
|
query,
|
||
|
|
page: String(page),
|
||
|
|
per_page: "20"
|
||
|
|
});
|
||
|
|
if (orientation) {
|
||
|
|
params.set("orientation", orientation);
|
||
|
|
}
|
||
|
|
const url = `https://api.pexels.com/v1/search?${params.toString()}`;
|
||
|
|
const response = await (0, import_obsidian9.requestUrl)({
|
||
|
|
url,
|
||
|
|
headers: {
|
||
|
|
Authorization: apiKey
|
||
|
|
}
|
||
|
|
});
|
||
|
|
if (response.status >= 400) {
|
||
|
|
throw new Error(`Pexels search failed: ${response.status}`);
|
||
|
|
}
|
||
|
|
const data = response.json;
|
||
|
|
const photos = (_a = data.photos) != null ? _a : [];
|
||
|
|
return photos.map((photo) => this.mapPexelsPhoto(photo));
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Search Pixabay
|
||
|
|
*/
|
||
|
|
async searchPixabay(query, page) {
|
||
|
|
var _a;
|
||
|
|
const apiKey = this.getPixabayApiKey();
|
||
|
|
if (!apiKey) {
|
||
|
|
const errorMsg = (0, import_obsidian9.requireApiVersion)("1.11.4") ? "Pixabay API key is required. Please configure it in settings (use SecretStorage on Obsidian 1.11.4+ or enter plaintext on older versions)." : "Pixabay API key is required. Please configure it in settings.";
|
||
|
|
throw new Error(errorMsg);
|
||
|
|
}
|
||
|
|
const orientation = this.mapPixabayOrientation(this.settings.defaultOrientation);
|
||
|
|
const params = new URLSearchParams({
|
||
|
|
key: apiKey,
|
||
|
|
q: query,
|
||
|
|
page: String(page),
|
||
|
|
per_page: "20",
|
||
|
|
image_type: "photo"
|
||
|
|
});
|
||
|
|
if (orientation) {
|
||
|
|
params.set("orientation", orientation);
|
||
|
|
}
|
||
|
|
const url = `https://pixabay.com/api/?${params.toString()}`;
|
||
|
|
const response = await (0, import_obsidian9.requestUrl)({ url });
|
||
|
|
if (response.status >= 400) {
|
||
|
|
throw new Error(`Pixabay search failed: ${response.status}`);
|
||
|
|
}
|
||
|
|
const data = response.json;
|
||
|
|
const hits = (_a = data.hits) != null ? _a : [];
|
||
|
|
return hits.map((hit) => this.mapPixabayHit(hit));
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get the download URL for an image based on size preference
|
||
|
|
*/
|
||
|
|
getDownloadUrl(image, size) {
|
||
|
|
const targetSize = size != null ? size : this.settings.defaultImageSize;
|
||
|
|
switch (targetSize) {
|
||
|
|
case "original" /* Original */:
|
||
|
|
return image.fullUrl;
|
||
|
|
case "large" /* Large */:
|
||
|
|
return image.regularUrl;
|
||
|
|
case "medium" /* Medium */:
|
||
|
|
return image.regularUrl;
|
||
|
|
case "small" /* Small */:
|
||
|
|
return image.thumbnailUrl;
|
||
|
|
default:
|
||
|
|
return image.regularUrl;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Download an image and return the binary data
|
||
|
|
*/
|
||
|
|
async downloadImage(image) {
|
||
|
|
const url = this.getDownloadUrl(image);
|
||
|
|
const response = await (0, import_obsidian9.requestUrl)({ url });
|
||
|
|
if (response.status >= 400) {
|
||
|
|
throw new Error(`Failed to download image: ${response.status}`);
|
||
|
|
}
|
||
|
|
return response.arrayBuffer;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Generate referral text for an image (attribution)
|
||
|
|
*/
|
||
|
|
generateReferralText(image) {
|
||
|
|
if (!this.settings.insertReferral) {
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
const backlink = this.settings.insertBackLink && image.pageUrl ? `[Backlink](${image.pageUrl}) | ` : "";
|
||
|
|
let referral = "";
|
||
|
|
switch (image.provider) {
|
||
|
|
case "unsplash" /* Unsplash */:
|
||
|
|
if (image.author && image.authorUrl) {
|
||
|
|
const utm = "utm_source=Obsidian%20Image%20Manager&utm_medium=referral";
|
||
|
|
referral = `
|
||
|
|
*${backlink}Photo by [${image.author}](${image.authorUrl}) on [Unsplash](https://unsplash.com/?${utm})*
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case "pexels" /* Pexels */:
|
||
|
|
if (image.author && image.authorUrl) {
|
||
|
|
referral = `
|
||
|
|
*${backlink}Photo by [${image.author}](${image.authorUrl}) on [Pexels](https://www.pexels.com/)*
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case "pixabay" /* Pixabay */:
|
||
|
|
if (image.author && image.authorUrl) {
|
||
|
|
referral = `
|
||
|
|
*${backlink}Image by [${image.author}](${image.authorUrl}) on [Pixabay](https://pixabay.com/)*
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
return referral;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Map orientation setting to API parameter
|
||
|
|
*/
|
||
|
|
mapOrientation(orientation) {
|
||
|
|
switch (orientation) {
|
||
|
|
case "landscape" /* Landscape */:
|
||
|
|
return "landscape";
|
||
|
|
case "portrait" /* Portrait */:
|
||
|
|
return "portrait";
|
||
|
|
case "square" /* Square */:
|
||
|
|
return "squarish";
|
||
|
|
default:
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Map orientation for Pixabay (different values)
|
||
|
|
*/
|
||
|
|
mapPixabayOrientation(orientation) {
|
||
|
|
switch (orientation) {
|
||
|
|
case "landscape" /* Landscape */:
|
||
|
|
return "horizontal";
|
||
|
|
case "portrait" /* Portrait */:
|
||
|
|
return "vertical";
|
||
|
|
default:
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Map Unsplash photo to RemoteImage
|
||
|
|
*/
|
||
|
|
mapUnsplashPhoto(photo) {
|
||
|
|
var _a, _b;
|
||
|
|
return {
|
||
|
|
id: photo.id,
|
||
|
|
provider: "unsplash" /* Unsplash */,
|
||
|
|
thumbnailUrl: photo.urls.thumb,
|
||
|
|
regularUrl: photo.urls.regular,
|
||
|
|
fullUrl: photo.urls.full,
|
||
|
|
downloadUrl: photo.links.download_location || photo.links.download,
|
||
|
|
width: photo.width,
|
||
|
|
height: photo.height,
|
||
|
|
description: (_b = (_a = photo.description) != null ? _a : photo.alt_description) != null ? _b : "",
|
||
|
|
author: photo.user.name,
|
||
|
|
authorUrl: photo.user.links.html,
|
||
|
|
pageUrl: photo.links.html
|
||
|
|
};
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Map Pexels photo to RemoteImage
|
||
|
|
*/
|
||
|
|
mapPexelsPhoto(photo) {
|
||
|
|
var _a;
|
||
|
|
return {
|
||
|
|
id: String(photo.id),
|
||
|
|
provider: "pexels" /* Pexels */,
|
||
|
|
thumbnailUrl: photo.src.tiny,
|
||
|
|
regularUrl: photo.src.large,
|
||
|
|
fullUrl: photo.src.original,
|
||
|
|
downloadUrl: photo.src.original,
|
||
|
|
width: photo.width,
|
||
|
|
height: photo.height,
|
||
|
|
description: (_a = photo.alt) != null ? _a : "",
|
||
|
|
author: photo.photographer,
|
||
|
|
authorUrl: photo.photographer_url,
|
||
|
|
pageUrl: photo.url
|
||
|
|
};
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Map Pixabay hit to RemoteImage
|
||
|
|
*/
|
||
|
|
mapPixabayHit(hit) {
|
||
|
|
return {
|
||
|
|
id: String(hit.id),
|
||
|
|
provider: "pixabay" /* Pixabay */,
|
||
|
|
thumbnailUrl: hit.previewURL,
|
||
|
|
regularUrl: hit.webformatURL,
|
||
|
|
fullUrl: hit.largeImageURL,
|
||
|
|
downloadUrl: hit.largeImageURL,
|
||
|
|
width: hit.imageWidth,
|
||
|
|
height: hit.imageHeight,
|
||
|
|
description: hit.tags,
|
||
|
|
author: hit.user,
|
||
|
|
authorUrl: `https://pixabay.com/users/${hit.user}-${hit.user_id}/`,
|
||
|
|
pageUrl: hit.pageURL
|
||
|
|
};
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/services/LocalConversionService.ts
|
||
|
|
var import_obsidian10 = require("obsidian");
|
||
|
|
var MARKDOWN_IMAGE_REGEX = /!\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)/g;
|
||
|
|
var HTML_IMAGE_REGEX = /<img[^>]+src=["'](https?:\/\/[^"']+)["'][^>]*>/g;
|
||
|
|
var LocalConversionService = class {
|
||
|
|
constructor(app, settings, storageManager, imageProcessor, observable) {
|
||
|
|
this.app = app;
|
||
|
|
this.settings = settings;
|
||
|
|
this.storageManager = storageManager;
|
||
|
|
this.imageProcessor = imageProcessor;
|
||
|
|
observable == null ? void 0 : observable.subscribe((newSettings) => {
|
||
|
|
this.updateSettings(newSettings);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Update settings reference
|
||
|
|
*/
|
||
|
|
updateSettings(settings) {
|
||
|
|
var _a;
|
||
|
|
this.settings = settings;
|
||
|
|
(_a = this.imageProcessor) == null ? void 0 : _a.updateSettings(settings);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Process a file to convert all remote images to local
|
||
|
|
* @param file - The file to process
|
||
|
|
* @param isBackground - If true, skip conversion if user interaction (modal) would be required
|
||
|
|
*/
|
||
|
|
async processFile(file, isBackground = false) {
|
||
|
|
if (!isMarkdownFile(file)) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
const content = await this.app.vault.read(file);
|
||
|
|
const { newContent, count } = await this.processContent(content, file, isBackground);
|
||
|
|
if (count > 0) {
|
||
|
|
await this.app.vault.modify(file, newContent);
|
||
|
|
}
|
||
|
|
return count;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Process content and replace remote images with local
|
||
|
|
*/
|
||
|
|
async processContent(content, sourceFile, isBackground = false) {
|
||
|
|
let newContent = content;
|
||
|
|
let count = 0;
|
||
|
|
const externalImages = await this.findExternalImages(content, sourceFile);
|
||
|
|
for (const image of externalImages) {
|
||
|
|
try {
|
||
|
|
if (isBackground && !this.settings.autoRename) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const tempPath = await this.downloadAndSave(image.url, sourceFile);
|
||
|
|
if (!tempPath) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const tempFile = this.app.vault.getAbstractFileByPath(tempPath);
|
||
|
|
if (!(tempFile instanceof import_obsidian10.TFile)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let finalFile = tempFile;
|
||
|
|
const suggestedName = image.alt ? this.storageManager.sanitizeFileName(image.alt) : tempFile.basename;
|
||
|
|
const result = await this.imageProcessor.renameImageFile(
|
||
|
|
tempFile,
|
||
|
|
suggestedName,
|
||
|
|
sourceFile
|
||
|
|
);
|
||
|
|
if (result && result.file) {
|
||
|
|
finalFile = result.file;
|
||
|
|
} else {
|
||
|
|
await this.app.fileManager.trashFile(tempFile);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
newContent = newContent.replace(image.fullMatch, image.replacement(finalFile.path));
|
||
|
|
count++;
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`Failed to convert image: ${image.url}`, error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return { newContent, count };
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if a position in content is inside a code block
|
||
|
|
*/
|
||
|
|
isInsideCodeBlock(content, position) {
|
||
|
|
const fencedCodeBlockRegex = /```[\s\S]*?```/g;
|
||
|
|
let match;
|
||
|
|
while ((match = fencedCodeBlockRegex.exec(content)) !== null) {
|
||
|
|
const start = match.index;
|
||
|
|
const end = start + match[0].length;
|
||
|
|
if (position >= start && position < end) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const fencedPositions = Array.from({ length: content.length }, () => false);
|
||
|
|
const fencedRegex = /```[\s\S]*?```/g;
|
||
|
|
while ((match = fencedRegex.exec(content)) !== null) {
|
||
|
|
for (let i = match.index; i < match.index + match[0].length; i++) {
|
||
|
|
fencedPositions[i] = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const inlineCodeRegex = /`[^`\n]+`/g;
|
||
|
|
while ((match = inlineCodeRegex.exec(content)) !== null) {
|
||
|
|
if (fencedPositions[match.index]) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const start = match.index;
|
||
|
|
const end = start + match[0].length;
|
||
|
|
if (position >= start && position < end) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Find all external images in content
|
||
|
|
* Verifies each URL with HEAD request to ensure it actually serves an image
|
||
|
|
*/
|
||
|
|
async findExternalImages(content, sourceFile) {
|
||
|
|
var _a;
|
||
|
|
const candidateMatches = [];
|
||
|
|
let match;
|
||
|
|
const mdRegex = new RegExp(MARKDOWN_IMAGE_REGEX.source, "g");
|
||
|
|
while ((match = mdRegex.exec(content)) !== null) {
|
||
|
|
const matchIndex = match.index;
|
||
|
|
if (this.isInsideCodeBlock(content, matchIndex)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const fullMatch = match[0];
|
||
|
|
const alt = (_a = match[1]) != null ? _a : "";
|
||
|
|
const url = match[2];
|
||
|
|
if (url && this.isExternalUrl(url) && this.isImageUrl(url)) {
|
||
|
|
const sourceFileRef = sourceFile;
|
||
|
|
candidateMatches.push({
|
||
|
|
fullMatch,
|
||
|
|
url,
|
||
|
|
alt,
|
||
|
|
replacement: (localPath) => {
|
||
|
|
const savedFile = this.app.vault.getAbstractFileByPath(localPath);
|
||
|
|
if (savedFile instanceof import_obsidian10.TFile) {
|
||
|
|
const link = this.storageManager.generateMarkdownLink(savedFile, sourceFileRef.path);
|
||
|
|
if (alt && link.startsWith("![") && link.includes("]]")) {
|
||
|
|
return link.replace("]]", `|${alt}]]`);
|
||
|
|
}
|
||
|
|
if (link.startsWith(") {
|
||
|
|
const pathMatch = link.match(/\]\(([^)]+)\)/);
|
||
|
|
if (pathMatch) {
|
||
|
|
return ``;
|
||
|
|
}
|
||
|
|
return `})`;
|
||
|
|
}
|
||
|
|
return link;
|
||
|
|
}
|
||
|
|
const localFile = this.app.vault.getAbstractFileByPath(localPath);
|
||
|
|
if (localFile instanceof import_obsidian10.TFile) {
|
||
|
|
const relativePath = this.storageManager.getRelativePath(sourceFileRef, localFile);
|
||
|
|
return `})`;
|
||
|
|
}
|
||
|
|
return `})`;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const htmlRegex = new RegExp(HTML_IMAGE_REGEX.source, "g");
|
||
|
|
while ((match = htmlRegex.exec(content)) !== null) {
|
||
|
|
const matchIndex = match.index;
|
||
|
|
if (this.isInsideCodeBlock(content, matchIndex)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const fullMatch = match[0];
|
||
|
|
const url = match[1];
|
||
|
|
if (url && this.isExternalUrl(url) && this.isImageUrl(url)) {
|
||
|
|
candidateMatches.push({
|
||
|
|
fullMatch,
|
||
|
|
url,
|
||
|
|
replacement: (localPath) => `})`
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const verifiedMatches = [];
|
||
|
|
for (const candidate of candidateMatches) {
|
||
|
|
const isImage = await this.verifyImageUrl(candidate.url);
|
||
|
|
if (isImage) {
|
||
|
|
verifiedMatches.push(candidate);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return verifiedMatches;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if a URL is external
|
||
|
|
*/
|
||
|
|
isExternalUrl(url) {
|
||
|
|
try {
|
||
|
|
const parsed = new URL(url);
|
||
|
|
return ["http:", "https:"].includes(parsed.protocol);
|
||
|
|
} catch (e) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if a URL should be considered for image conversion
|
||
|
|
* Returns false for known non-image embed domains (YouTube, etc.)
|
||
|
|
* This is a preliminary filter - actual image verification happens via HEAD request
|
||
|
|
*/
|
||
|
|
isImageUrl(url) {
|
||
|
|
try {
|
||
|
|
const parsed = new URL(url);
|
||
|
|
const hostname = parsed.hostname.toLowerCase();
|
||
|
|
const nonImageDomains = [
|
||
|
|
"youtube.com",
|
||
|
|
"www.youtube.com",
|
||
|
|
"youtu.be",
|
||
|
|
"m.youtube.com",
|
||
|
|
"youtube-nocookie.com",
|
||
|
|
"www.youtube-nocookie.com",
|
||
|
|
"vimeo.com",
|
||
|
|
"www.vimeo.com",
|
||
|
|
"spotify.com",
|
||
|
|
"open.spotify.com",
|
||
|
|
"soundcloud.com",
|
||
|
|
"www.soundcloud.com"
|
||
|
|
];
|
||
|
|
if (nonImageDomains.some((domain) => hostname === domain || hostname.endsWith("." + domain))) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
} catch (e) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Verify if a URL actually serves an image by checking Content-Type header
|
||
|
|
* Uses HEAD request to avoid downloading non-image content
|
||
|
|
*/
|
||
|
|
async verifyImageUrl(url) {
|
||
|
|
var _a, _b;
|
||
|
|
try {
|
||
|
|
const response = await (0, import_obsidian10.requestUrl)({ url, method: "HEAD" });
|
||
|
|
const contentType = (_b = (_a = response.headers["content-type"]) == null ? void 0 : _a.toLowerCase()) != null ? _b : "";
|
||
|
|
return contentType.startsWith("image/");
|
||
|
|
} catch (e) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Download an image and save it locally
|
||
|
|
* Includes Content-Type validation as a safety net
|
||
|
|
*/
|
||
|
|
async downloadAndSave(url, sourceFile) {
|
||
|
|
var _a, _b, _c;
|
||
|
|
try {
|
||
|
|
const response = await (0, import_obsidian10.requestUrl)({ url });
|
||
|
|
if (response.status >= 400) {
|
||
|
|
throw new Error(`HTTP ${response.status}`);
|
||
|
|
}
|
||
|
|
const contentType = (_a = response.headers["content-type"]) != null ? _a : "";
|
||
|
|
if (!contentType.toLowerCase().startsWith("image/")) {
|
||
|
|
console.warn(`Skipping ${url}: Content-Type is ${contentType}, not an image`);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const extension = this.storageManager.getExtensionFromMimeType(contentType);
|
||
|
|
const arrayBuffer = response.arrayBuffer;
|
||
|
|
const urlPath = new URL(url).pathname;
|
||
|
|
const urlFileName = (_c = (_b = urlPath.split("/").pop()) == null ? void 0 : _b.split(".")[0]) != null ? _c : "image";
|
||
|
|
const baseName = this.storageManager.sanitizeFileName(urlFileName);
|
||
|
|
const filePath = await this.storageManager.getAvailablePath(baseName, extension, sourceFile);
|
||
|
|
await this.storageManager.saveFile(arrayBuffer, filePath);
|
||
|
|
return filePath;
|
||
|
|
} catch (error) {
|
||
|
|
console.error(`Failed to download ${url}:`, error);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Process all files in the vault
|
||
|
|
*/
|
||
|
|
async processAllFiles() {
|
||
|
|
const files = this.app.vault.getMarkdownFiles();
|
||
|
|
let totalCount = 0;
|
||
|
|
for (const file of files) {
|
||
|
|
if (this.settings.supportedExtensions.includes(file.extension)) {
|
||
|
|
const count = await this.processFile(file);
|
||
|
|
totalCount += count;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return totalCount;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Register event handlers for automatic conversion
|
||
|
|
*/
|
||
|
|
registerEventHandlers(onNoteOpen, onNoteSave) {
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/services/BannerService.ts
|
||
|
|
var import_obsidian11 = require("obsidian");
|
||
|
|
var ObsidianModule = __toESM(require("obsidian"), 1);
|
||
|
|
function setCssProperties(element, props) {
|
||
|
|
const obsidian = ObsidianModule;
|
||
|
|
if (typeof obsidian.setCssProperties === "function") {
|
||
|
|
obsidian.setCssProperties(element, props);
|
||
|
|
} else {
|
||
|
|
for (const [key, value] of Object.entries(props)) {
|
||
|
|
element.style.setProperty(key, value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
var CSS_CLASSES = {
|
||
|
|
Main: "image-manager-banner",
|
||
|
|
Content: "banner-content",
|
||
|
|
Icon: "banner-icon",
|
||
|
|
Static: "static"
|
||
|
|
};
|
||
|
|
var PATTERNS = {
|
||
|
|
Wikilink: /^!?\[\[([^\]]+?)(\|([^\]]+?))?\]\]$/,
|
||
|
|
Markdown: /^!?\[([^\]]*)\]\(([^)]+?)\)$/,
|
||
|
|
MarkdownBare: /^!?<([^>]+)>$/,
|
||
|
|
Weblink: /^https?:\/\//i
|
||
|
|
};
|
||
|
|
var bannerDataStore = /* @__PURE__ */ new Map();
|
||
|
|
var BannerService = class {
|
||
|
|
constructor(app, settings, observable) {
|
||
|
|
this.app = app;
|
||
|
|
this.settings = settings;
|
||
|
|
observable == null ? void 0 : observable.subscribe((newSettings) => {
|
||
|
|
this.updateSettings(newSettings);
|
||
|
|
this.applySettings();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Update settings reference
|
||
|
|
*/
|
||
|
|
updateSettings(settings) {
|
||
|
|
this.settings = settings;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get the current device type
|
||
|
|
*/
|
||
|
|
getCurrentDevice() {
|
||
|
|
if (import_obsidian11.Platform.isPhone) {
|
||
|
|
return "phone" /* Phone */;
|
||
|
|
}
|
||
|
|
if (import_obsidian11.Platform.isTablet) {
|
||
|
|
return "tablet" /* Tablet */;
|
||
|
|
}
|
||
|
|
return "desktop" /* Desktop */;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get device-specific settings
|
||
|
|
*/
|
||
|
|
getDeviceSettings() {
|
||
|
|
const device = this.getCurrentDevice();
|
||
|
|
return this.settings.banner[device];
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Process all open markdown views
|
||
|
|
*/
|
||
|
|
processAll(force = false) {
|
||
|
|
const deviceSettings = this.getDeviceSettings();
|
||
|
|
this.app.workspace.iterateRootLeaves((leaf) => {
|
||
|
|
const view = leaf.view;
|
||
|
|
if (view instanceof import_obsidian11.MarkdownView) {
|
||
|
|
if (deviceSettings.enabled) {
|
||
|
|
const file = (view == null ? void 0 : view.file) || null;
|
||
|
|
void this.process(file, view, force);
|
||
|
|
} else {
|
||
|
|
this.remove(view);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Process a single file/view
|
||
|
|
*/
|
||
|
|
async process(file, view, force = false) {
|
||
|
|
const data = await this.compute(file, view);
|
||
|
|
if (!data) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (force) {
|
||
|
|
data.needsUpdate = true;
|
||
|
|
}
|
||
|
|
if (!data.image) {
|
||
|
|
this.remove(view, data);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!data.icon) {
|
||
|
|
data.needsUpdate = true;
|
||
|
|
}
|
||
|
|
const deviceSettings = this.getDeviceSettings();
|
||
|
|
if (deviceSettings.enabled) {
|
||
|
|
await this.render(data, view, force);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Compute banner data from frontmatter
|
||
|
|
*/
|
||
|
|
async compute(file, targetView) {
|
||
|
|
var _a;
|
||
|
|
const view = targetView || this.getActiveView();
|
||
|
|
if (!file || !(view instanceof import_obsidian11.MarkdownView)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const deviceSettings = this.getDeviceSettings();
|
||
|
|
if (!deviceSettings.enabled) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
if (!this.settings.supportedExtensions.includes(file.extension) && file.extension !== "md") {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const leafId = (_a = view == null ? void 0 : view.leaf) == null ? void 0 : _a.id;
|
||
|
|
if (!leafId) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const oldData = bannerDataStore.get(leafId) || this.createDefaultBannerData();
|
||
|
|
const newData = this.createDefaultBannerData(view, oldData.viewMode);
|
||
|
|
if (file.extension === "md") {
|
||
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
||
|
|
if ((cache == null ? void 0 : cache.frontmatter) != null) {
|
||
|
|
const propertySettings2 = this.settings.banner.properties;
|
||
|
|
const imageProp2 = propertySettings2.imageProperty;
|
||
|
|
const iconProp2 = propertySettings2.iconProperty;
|
||
|
|
if (propertySettings2.hidePropertyEnabled && propertySettings2.hideProperty) {
|
||
|
|
const hideProp = propertySettings2.hideProperty;
|
||
|
|
const hideValue = cache.frontmatter[hideProp];
|
||
|
|
if (hideValue === true || hideValue === "true" || hideValue === 1 || hideValue === "1") {
|
||
|
|
return newData;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const hasBannerProperty = cache.frontmatter[imageProp2] != null;
|
||
|
|
const hasIconProperty = deviceSettings.iconEnabled && cache.frontmatter[iconProp2] != null;
|
||
|
|
if (!hasBannerProperty && !hasIconProperty) {
|
||
|
|
return newData;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const frontmatter = await getFrontmatter(this.app, file);
|
||
|
|
if (!frontmatter) {
|
||
|
|
return newData;
|
||
|
|
}
|
||
|
|
const propertySettings = this.settings.banner.properties;
|
||
|
|
if (propertySettings.hidePropertyEnabled && propertySettings.hideProperty) {
|
||
|
|
const hideProp = propertySettings.hideProperty;
|
||
|
|
const hideValue = frontmatter[hideProp];
|
||
|
|
if (hideValue === true || hideValue === "true" || hideValue === 1 || hideValue === "1") {
|
||
|
|
return newData;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const imageProp = propertySettings.imageProperty;
|
||
|
|
const iconProp = propertySettings.iconProperty;
|
||
|
|
const imageValue = frontmatter[imageProp];
|
||
|
|
if (imageValue && typeof imageValue === "string") {
|
||
|
|
newData.image = imageValue;
|
||
|
|
newData.filepath = file.path;
|
||
|
|
if (oldData.filepath !== newData.filepath) {
|
||
|
|
newData.needsUpdate = true;
|
||
|
|
newData.isImageChange = true;
|
||
|
|
} else if (oldData.image !== newData.image) {
|
||
|
|
newData.needsUpdate = true;
|
||
|
|
newData.isImageChange = true;
|
||
|
|
if (await this.isImagePropertiesUpdate(oldData.image, newData.image, view)) {
|
||
|
|
newData.isImagePropsUpdate = true;
|
||
|
|
newData.isImageChange = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (deviceSettings.iconEnabled) {
|
||
|
|
const iconValue = frontmatter[iconProp];
|
||
|
|
if (iconValue && typeof iconValue === "string") {
|
||
|
|
newData.icon = iconValue;
|
||
|
|
if (oldData.icon !== newData.icon) {
|
||
|
|
newData.needsUpdate = true;
|
||
|
|
}
|
||
|
|
} else if (oldData.icon) {
|
||
|
|
newData.icon = null;
|
||
|
|
newData.needsUpdate = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return newData;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Render banner in the view
|
||
|
|
*/
|
||
|
|
async render(data, targetView, force = false) {
|
||
|
|
var _a;
|
||
|
|
const { image, viewMode, lastViewMode } = data;
|
||
|
|
const view = targetView || this.getActiveView();
|
||
|
|
if (!view || !(view instanceof import_obsidian11.MarkdownView)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const container = view.containerEl;
|
||
|
|
if (!container) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const containers = container.querySelectorAll(
|
||
|
|
".cm-scroller, .markdown-reading-view > .markdown-preview-view"
|
||
|
|
);
|
||
|
|
const bannerMissing = !!image && containers.length > 0 && Array.from(containers).some((c) => !c.querySelector(`.${CSS_CLASSES.Main}`));
|
||
|
|
if (bannerMissing) {
|
||
|
|
data.needsUpdate = true;
|
||
|
|
data.isImageChange = true;
|
||
|
|
}
|
||
|
|
if (!force && !data.needsUpdate && lastViewMode === viewMode && !bannerMissing) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (containers.length === 0) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const imageOptions = await this.parseLink(image || "", view);
|
||
|
|
const banners = this.updateBannerElements(data, imageOptions, containers);
|
||
|
|
await this.updateIcons(data, banners, view);
|
||
|
|
if (data.isImageChange) {
|
||
|
|
this.injectBanners(banners, containers);
|
||
|
|
} else {
|
||
|
|
this.replaceBanners(banners);
|
||
|
|
}
|
||
|
|
data.lastViewMode = viewMode;
|
||
|
|
container.dataset.imBanner = "";
|
||
|
|
const leafId = (_a = view == null ? void 0 : view.leaf) == null ? void 0 : _a.id;
|
||
|
|
if (leafId) {
|
||
|
|
bannerDataStore.set(leafId, data);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Update banner DOM elements
|
||
|
|
*/
|
||
|
|
updateBannerElements(data, imgOptions, containers) {
|
||
|
|
const { isImageChange, isImagePropsUpdate } = data;
|
||
|
|
const banners = [];
|
||
|
|
containers.forEach((container) => {
|
||
|
|
var _a;
|
||
|
|
let element = container.querySelector(`.${CSS_CLASSES.Main}`);
|
||
|
|
if (!element) {
|
||
|
|
element = document.createElement("div");
|
||
|
|
element.classList.add(CSS_CLASSES.Main);
|
||
|
|
}
|
||
|
|
let content = element.querySelector(`.${CSS_CLASSES.Content}`);
|
||
|
|
if (!content) {
|
||
|
|
content = document.createElement("div");
|
||
|
|
content.classList.add(CSS_CLASSES.Content);
|
||
|
|
element.appendChild(content);
|
||
|
|
}
|
||
|
|
banners.push(element);
|
||
|
|
if (isImageChange || isImagePropsUpdate) {
|
||
|
|
if (isImageChange) {
|
||
|
|
element.classList.remove(CSS_CLASSES.Static);
|
||
|
|
(_a = content.firstChild) == null ? void 0 : _a.remove();
|
||
|
|
}
|
||
|
|
const cssVars = {
|
||
|
|
"--im-banner-img-x": `${imgOptions.x}px`,
|
||
|
|
"--im-banner-img-y": `${imgOptions.y}px`,
|
||
|
|
"--im-banner-size": imgOptions.repeatable ? "auto" : "cover",
|
||
|
|
"--im-banner-repeat": imgOptions.repeatable ? "repeat" : "no-repeat",
|
||
|
|
"--im-banner-url": "none"
|
||
|
|
};
|
||
|
|
if (imgOptions.type === "video" /* Video */) {
|
||
|
|
const video = document.createElement("video");
|
||
|
|
video.controls = false;
|
||
|
|
video.autoplay = true;
|
||
|
|
video.muted = true;
|
||
|
|
video.loop = true;
|
||
|
|
video.src = imgOptions.url.replace(/^"|"$/g, "");
|
||
|
|
content.appendChild(video);
|
||
|
|
} else {
|
||
|
|
cssVars["--im-banner-url"] = `url(${imgOptions.url})`;
|
||
|
|
}
|
||
|
|
setCssProperties(container, cssVars);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
return banners;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Update icon elements on banners
|
||
|
|
*/
|
||
|
|
async updateIcons(data, banners, view) {
|
||
|
|
var _a;
|
||
|
|
const deviceSettings = this.getDeviceSettings();
|
||
|
|
let calculatedFontSize = null;
|
||
|
|
for (const banner of banners) {
|
||
|
|
const { icon } = data;
|
||
|
|
let iconContainer = banner.querySelector(`.${CSS_CLASSES.Icon}`);
|
||
|
|
const hasContainer = iconContainer !== null;
|
||
|
|
if (hasContainer) {
|
||
|
|
iconContainer == null ? void 0 : iconContainer.classList.add(CSS_CLASSES.Static);
|
||
|
|
}
|
||
|
|
if (deviceSettings.iconEnabled && icon) {
|
||
|
|
if (!hasContainer) {
|
||
|
|
iconContainer = document.createElement("div");
|
||
|
|
iconContainer.classList.add(CSS_CLASSES.Icon);
|
||
|
|
const innerDiv = document.createElement("div");
|
||
|
|
iconContainer.appendChild(innerDiv);
|
||
|
|
banner.prepend(iconContainer);
|
||
|
|
}
|
||
|
|
const iconElement = iconContainer == null ? void 0 : iconContainer.querySelector("div");
|
||
|
|
if (!iconElement) continue;
|
||
|
|
const iconData = await this.parseIcon(icon, view);
|
||
|
|
let value = ((_a = iconData.value) == null ? void 0 : _a.replace(/([#.:[\\]"])/g, "\\$1")) || "";
|
||
|
|
iconElement.dataset.type = iconData.type;
|
||
|
|
if (iconData.type === "link" /* Link */) {
|
||
|
|
setCssProperties(iconElement, {
|
||
|
|
"--im-banner-icon-value": `url(${value})`
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
calculatedFontSize = calculatedFontSize != null ? calculatedFontSize : this.calculateFontSize(value, deviceSettings.iconSize);
|
||
|
|
setCssProperties(iconElement, {
|
||
|
|
"--im-banner-icon-value": `"${value}"`,
|
||
|
|
"--im-banner-icon-fontsize": calculatedFontSize
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} else if (hasContainer && iconContainer) {
|
||
|
|
data.icon = null;
|
||
|
|
iconContainer.remove();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Inject banners into containers
|
||
|
|
*/
|
||
|
|
injectBanners(banners, containers) {
|
||
|
|
const deviceSettings = this.getDeviceSettings();
|
||
|
|
const shouldAnimate = deviceSettings.animation;
|
||
|
|
containers.forEach((container, index) => {
|
||
|
|
const banner = banners[index];
|
||
|
|
if (banner) {
|
||
|
|
banner.classList.remove(CSS_CLASSES.Static);
|
||
|
|
container.prepend(banner);
|
||
|
|
if (shouldAnimate) {
|
||
|
|
void banner.offsetHeight;
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
banner.onanimationend = () => {
|
||
|
|
banner.classList.add(CSS_CLASSES.Static);
|
||
|
|
};
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
banner.classList.add(CSS_CLASSES.Static);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Replace banners (no animation)
|
||
|
|
*/
|
||
|
|
replaceBanners(banners) {
|
||
|
|
banners.forEach((banner) => {
|
||
|
|
banner.classList.add(CSS_CLASSES.Static);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Remove banner from view
|
||
|
|
*/
|
||
|
|
remove(view, data) {
|
||
|
|
var _a;
|
||
|
|
const targetView = view || (data == null ? void 0 : data.filepath) ? this.getActiveView() : null;
|
||
|
|
if (!(targetView instanceof import_obsidian11.MarkdownView)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const container = targetView.containerEl;
|
||
|
|
if (!container) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const targets = container.querySelectorAll(`.${CSS_CLASSES.Main}`);
|
||
|
|
targets.forEach((t) => t.remove());
|
||
|
|
const leafId = (_a = targetView == null ? void 0 : targetView.leaf) == null ? void 0 : _a.id;
|
||
|
|
if (leafId) {
|
||
|
|
bannerDataStore.delete(leafId);
|
||
|
|
}
|
||
|
|
delete container.dataset.imBanner;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Apply current settings to DOM
|
||
|
|
*/
|
||
|
|
applySettings() {
|
||
|
|
var _a;
|
||
|
|
const deviceSettings = this.getDeviceSettings();
|
||
|
|
const height = deviceSettings.height;
|
||
|
|
const noteOffset = deviceSettings.noteOffset;
|
||
|
|
const viewOffset = deviceSettings.viewOffset;
|
||
|
|
const radius = deviceSettings.bannerRadiusEnabled ? deviceSettings.borderRadius : [0, 0, 0, 0];
|
||
|
|
const padding = deviceSettings.padding;
|
||
|
|
const fade = deviceSettings.fade;
|
||
|
|
const cssVars = {
|
||
|
|
"--im-banner-height": `${height}px`,
|
||
|
|
"--im-banner-note-offset": `${noteOffset}px`,
|
||
|
|
"--im-banner-view-offset": `${viewOffset}px`,
|
||
|
|
"--im-banner-radius": `${radius[0]}px ${radius[1]}px ${radius[2]}px ${radius[3]}px`,
|
||
|
|
"--im-banner-padding": `${padding}px`,
|
||
|
|
"--im-banner-mask": fade ? "revert-layer" : "initial",
|
||
|
|
"--im-banner-mask-webkit": fade ? "revert-layer" : "initial"
|
||
|
|
};
|
||
|
|
if (deviceSettings.iconEnabled) {
|
||
|
|
const iconFrame = (_a = deviceSettings.iconFrame) != null ? _a : true;
|
||
|
|
cssVars["--im-banner-icon-size-w"] = `${deviceSettings.iconSize}px`;
|
||
|
|
cssVars["--im-banner-icon-size-h"] = `${deviceSettings.iconSize}px`;
|
||
|
|
cssVars["--im-banner-icon-radius"] = `${deviceSettings.iconRadius}px`;
|
||
|
|
cssVars["--im-banner-icon-align-h"] = deviceSettings.iconAlignmentH;
|
||
|
|
cssVars["--im-banner-icon-align-v"] = deviceSettings.iconAlignmentV;
|
||
|
|
cssVars["--im-banner-icon-offset-x"] = `${deviceSettings.iconOffsetX}px`;
|
||
|
|
cssVars["--im-banner-icon-offset-y"] = `${deviceSettings.iconOffsetY}px`;
|
||
|
|
cssVars["--im-banner-icon-border"] = iconFrame ? `${deviceSettings.iconBorder}px` : "0px";
|
||
|
|
cssVars["--im-banner-icon-background"] = iconFrame && deviceSettings.iconBackground ? "revert-layer" : "transparent";
|
||
|
|
}
|
||
|
|
setCssProperties(document.body, cssVars);
|
||
|
|
this.processAll(true);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Parse image link string into options
|
||
|
|
*/
|
||
|
|
async parseLink(str, view) {
|
||
|
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
|
||
|
|
let url = null;
|
||
|
|
let displayText = null;
|
||
|
|
let external = false;
|
||
|
|
let obsidianUrl = false;
|
||
|
|
let options = { x: 0, y: 0, repeatable: false };
|
||
|
|
const wikilinkMatch = str.match(PATTERNS.Wikilink);
|
||
|
|
if (wikilinkMatch) {
|
||
|
|
url = (_b = (_a = wikilinkMatch[1]) == null ? void 0 : _a.trim()) != null ? _b : null;
|
||
|
|
displayText = (_d = (_c = wikilinkMatch[3]) == null ? void 0 : _c.trim()) != null ? _d : null;
|
||
|
|
}
|
||
|
|
const markdownMatch = str.match(PATTERNS.Markdown);
|
||
|
|
const markdownBareMatch = str.match(PATTERNS.MarkdownBare);
|
||
|
|
if (markdownMatch) {
|
||
|
|
displayText = (_f = (_e = markdownMatch[1]) == null ? void 0 : _e.trim()) != null ? _f : null;
|
||
|
|
url = (_h = (_g = markdownMatch[2]) == null ? void 0 : _g.trim()) != null ? _h : null;
|
||
|
|
} else if (markdownBareMatch) {
|
||
|
|
url = (_j = (_i = markdownBareMatch[1]) == null ? void 0 : _i.trim()) != null ? _j : null;
|
||
|
|
displayText = null;
|
||
|
|
}
|
||
|
|
if (!url) {
|
||
|
|
url = str;
|
||
|
|
displayText = null;
|
||
|
|
}
|
||
|
|
external = PATTERNS.Weblink.test(url);
|
||
|
|
if (this.isObsidianUrl(url)) {
|
||
|
|
const urlStr = url.replace("obsidian://open", "");
|
||
|
|
const params = new URLSearchParams(urlStr);
|
||
|
|
const file = params.get("file");
|
||
|
|
if (file) {
|
||
|
|
url = file;
|
||
|
|
obsidianUrl = true;
|
||
|
|
external = false;
|
||
|
|
displayText = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (url.startsWith("file:")) {
|
||
|
|
url = url.replace(/^file:\/{1,}/, import_obsidian11.Platform.resourcePathPrefix);
|
||
|
|
external = true;
|
||
|
|
}
|
||
|
|
const hashIndex = url.indexOf("#");
|
||
|
|
if ((external || obsidianUrl) && hashIndex !== -1) {
|
||
|
|
options = this.parseImageProperties(url.substring(hashIndex + 1));
|
||
|
|
url = url.replace(/#.*/, "").trim();
|
||
|
|
}
|
||
|
|
if (displayText) {
|
||
|
|
options = this.parseImageProperties(displayText);
|
||
|
|
}
|
||
|
|
if (!external) {
|
||
|
|
const vault = this.app.vault;
|
||
|
|
let file = null;
|
||
|
|
if (url.startsWith("/") && !url.startsWith("//")) {
|
||
|
|
const vaultCms = (_l = (_k = this.app.plugins) == null ? void 0 : _k.plugins) == null ? void 0 : _l["vault-cms"];
|
||
|
|
const resolved = (_m = vaultCms == null ? void 0 : vaultCms.resolvePublicPath) == null ? void 0 : _m.call(vaultCms, url);
|
||
|
|
if (resolved) {
|
||
|
|
url = resolved;
|
||
|
|
external = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!external) {
|
||
|
|
if (view == null ? void 0 : view.file) {
|
||
|
|
const resolvedPath = this.app.metadataCache.getFirstLinkpathDest(url, view.file.path);
|
||
|
|
if (resolvedPath) {
|
||
|
|
file = resolvedPath;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!file) {
|
||
|
|
const files = vault.getFiles().filter((f) => f.path === url || f.name === url);
|
||
|
|
file = files.find((f) => f.path === url) || files.find((f) => f.name === url) || null;
|
||
|
|
}
|
||
|
|
if (file) {
|
||
|
|
url = vault.getResourcePath(file);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let type = null;
|
||
|
|
try {
|
||
|
|
const urlObj = new URL(url);
|
||
|
|
const extension = (_n = urlObj.pathname.split(".").pop()) == null ? void 0 : _n.toLowerCase();
|
||
|
|
const imageExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp"];
|
||
|
|
const videoExtensions = ["mp4", "webm", "ogg", "ogv", "mov"];
|
||
|
|
if (extension && imageExtensions.includes(extension)) {
|
||
|
|
type = "image" /* Image */;
|
||
|
|
} else if (extension && videoExtensions.includes(extension)) {
|
||
|
|
type = "video" /* Video */;
|
||
|
|
}
|
||
|
|
if (!type) {
|
||
|
|
try {
|
||
|
|
const response = await (0, import_obsidian11.requestUrl)({ url, method: "HEAD" });
|
||
|
|
const contentType = (response == null ? void 0 : response.headers["content-type"]) || null;
|
||
|
|
if (contentType) {
|
||
|
|
if (contentType.includes("image")) {
|
||
|
|
type = "image" /* Image */;
|
||
|
|
} else if (contentType.includes("video")) {
|
||
|
|
type = "video" /* Video */;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
url: `"${url.trim().replace(/(["\\])/g, "\\$1")}"`,
|
||
|
|
external,
|
||
|
|
type,
|
||
|
|
...options
|
||
|
|
};
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Parse image properties (offset, repeat) from string
|
||
|
|
*/
|
||
|
|
parseImageProperties(str) {
|
||
|
|
const values = str.toLowerCase();
|
||
|
|
const repeatable = values.includes("repeat");
|
||
|
|
const sizes = str.split(/x|,/);
|
||
|
|
const numbers = sizes.filter((v) => !isNaN(parseInt(v.trim(), 10)));
|
||
|
|
let x = 0;
|
||
|
|
let y = 0;
|
||
|
|
const num0 = numbers[0];
|
||
|
|
const num1 = numbers[1];
|
||
|
|
if (numbers.length === 2 && num0 && num1) {
|
||
|
|
x = parseInt(num0.trim(), 10);
|
||
|
|
y = parseInt(num1.trim(), 10);
|
||
|
|
} else if (numbers.length === 1 && num0) {
|
||
|
|
y = parseInt(num0.trim(), 10);
|
||
|
|
}
|
||
|
|
return { x, y, repeatable };
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Parse icon property
|
||
|
|
*/
|
||
|
|
async parseIcon(icon, view) {
|
||
|
|
const str = icon || "";
|
||
|
|
const result = { value: null, type: "text" /* Text */ };
|
||
|
|
const isExplicitLink = PATTERNS.Wikilink.test(str) || PATTERNS.Markdown.test(str) || PATTERNS.MarkdownBare.test(str) || PATTERNS.Weblink.test(str) || this.isObsidianUrl(str);
|
||
|
|
const imageExtensions = /\.(jpg|jpeg|png|gif|svg|webp|bmp|ico|avif)$/i;
|
||
|
|
const isFilePath = imageExtensions.test(str);
|
||
|
|
if (isExplicitLink || isFilePath) {
|
||
|
|
result.type = "link" /* Link */;
|
||
|
|
const data = await this.parseLink(str, view);
|
||
|
|
result.value = data.url;
|
||
|
|
} else {
|
||
|
|
result.value = str;
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if only image properties changed (not the URL)
|
||
|
|
*/
|
||
|
|
async isImagePropertiesUpdate(oldStr, newStr, view) {
|
||
|
|
if (!oldStr || !newStr) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const oldOpt = await this.parseLink(oldStr, view);
|
||
|
|
const newOpt = await this.parseLink(newStr, view);
|
||
|
|
return oldOpt.url === newOpt.url;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Resolve absolute-from-root image paths (e.g. /images/blog/1.jpg)
|
||
|
|
* Uses the configured project root to find files in the public/ folder.
|
||
|
|
* Returns a file:// URL if found, null otherwise.
|
||
|
|
*/
|
||
|
|
/**
|
||
|
|
* Check if URL is an obsidian:// URL
|
||
|
|
*/
|
||
|
|
isObsidianUrl(url) {
|
||
|
|
return url.startsWith("obsidian://open");
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Calculate font size to fit text in icon
|
||
|
|
* Uses actual DOM measurement for accurate sizing
|
||
|
|
*/
|
||
|
|
calculateFontSize(textContent, iconSize) {
|
||
|
|
const temp = document.createElement("span");
|
||
|
|
temp.addClass("im-measure-temp");
|
||
|
|
setCssProperties(temp, {
|
||
|
|
position: "absolute",
|
||
|
|
visibility: "hidden",
|
||
|
|
"white-space": "nowrap",
|
||
|
|
padding: "0",
|
||
|
|
margin: "0",
|
||
|
|
left: "-9999px"
|
||
|
|
});
|
||
|
|
temp.textContent = textContent.toUpperCase();
|
||
|
|
document.body.appendChild(temp);
|
||
|
|
const checkWidth = iconSize - 16;
|
||
|
|
let fontSize = iconSize;
|
||
|
|
setCssProperties(temp, { "font-size": `${fontSize}px` });
|
||
|
|
while (temp.offsetWidth > checkWidth && fontSize > 1) {
|
||
|
|
fontSize -= 1;
|
||
|
|
setCssProperties(temp, { "font-size": `${fontSize}px` });
|
||
|
|
}
|
||
|
|
document.body.removeChild(temp);
|
||
|
|
return `${fontSize}px`;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get active markdown view
|
||
|
|
*/
|
||
|
|
getActiveView() {
|
||
|
|
return this.app.workspace.getActiveViewOfType(import_obsidian11.MarkdownView);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Create default banner data object
|
||
|
|
*/
|
||
|
|
createDefaultBannerData(view, lastViewMode) {
|
||
|
|
let viewMode = null;
|
||
|
|
if (view) {
|
||
|
|
const mode = view.getMode();
|
||
|
|
viewMode = mode === "preview" ? "preview" : "source";
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
filepath: null,
|
||
|
|
image: null,
|
||
|
|
icon: null,
|
||
|
|
viewMode,
|
||
|
|
lastViewMode: lastViewMode || null,
|
||
|
|
isImagePropsUpdate: false,
|
||
|
|
isImageChange: false,
|
||
|
|
needsUpdate: false
|
||
|
|
};
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Cleanup when plugin unloads
|
||
|
|
*/
|
||
|
|
destroy() {
|
||
|
|
document.querySelectorAll(`.${CSS_CLASSES.Main}`).forEach((el) => el.remove());
|
||
|
|
bannerDataStore.clear();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/modals/FilePickerModal.ts
|
||
|
|
var import_obsidian12 = require("obsidian");
|
||
|
|
var FilePickerModal = class extends import_obsidian12.Modal {
|
||
|
|
constructor(app, imageProcessor, propertyHandler, insertToProperty = false, propertyName) {
|
||
|
|
super(app);
|
||
|
|
this.imageProcessor = imageProcessor;
|
||
|
|
this.propertyHandler = propertyHandler;
|
||
|
|
this.insertToProperty = insertToProperty;
|
||
|
|
this.propertyName = propertyName;
|
||
|
|
}
|
||
|
|
onOpen() {
|
||
|
|
const { contentEl } = this;
|
||
|
|
const input = contentEl.createEl("input", {
|
||
|
|
type: "file",
|
||
|
|
attr: {
|
||
|
|
accept: "image/*",
|
||
|
|
multiple: "true",
|
||
|
|
style: "display: none;"
|
||
|
|
}
|
||
|
|
});
|
||
|
|
input.addEventListener("change", () => {
|
||
|
|
void this.handleFileSelection(input);
|
||
|
|
});
|
||
|
|
input.addEventListener("cancel", () => {
|
||
|
|
this.close();
|
||
|
|
});
|
||
|
|
input.click();
|
||
|
|
}
|
||
|
|
async handleFileSelection(input) {
|
||
|
|
this.close();
|
||
|
|
const files = input.files;
|
||
|
|
if (!files || files.length === 0) {
|
||
|
|
new import_obsidian12.Notice("No files selected");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const activeFile = this.getActiveFile();
|
||
|
|
if (!activeFile) {
|
||
|
|
new import_obsidian12.Notice("No active file");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const view = this.app.workspace.getActiveViewOfType(import_obsidian12.MarkdownView);
|
||
|
|
const editor = view == null ? void 0 : view.editor;
|
||
|
|
for (let i = 0; i < files.length; i++) {
|
||
|
|
const file = files.item(i);
|
||
|
|
if (!file || !file.type.startsWith("image/")) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (this.insertToProperty) {
|
||
|
|
if (!this.propertyName || this.propertyName.trim() === "") {
|
||
|
|
new import_obsidian12.Notice("Please specify a property name in settings");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const result = await this.imageProcessor.processImageFile(
|
||
|
|
file,
|
||
|
|
activeFile,
|
||
|
|
true,
|
||
|
|
// Show rename modal
|
||
|
|
true
|
||
|
|
// isPropertyInsertion - skip descriptive images
|
||
|
|
);
|
||
|
|
if (result.success && result.file) {
|
||
|
|
await this.propertyHandler.setPropertyValue(
|
||
|
|
activeFile,
|
||
|
|
this.propertyName,
|
||
|
|
result.file
|
||
|
|
);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
const result = await this.imageProcessor.processImageFile(
|
||
|
|
file,
|
||
|
|
activeFile,
|
||
|
|
true
|
||
|
|
// Show rename modal
|
||
|
|
);
|
||
|
|
if (result.success && result.linkText && editor) {
|
||
|
|
editor.replaceSelection(result.linkText);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
new import_obsidian12.Notice(`Added ${files.length} image(s)`);
|
||
|
|
}
|
||
|
|
getActiveFile() {
|
||
|
|
var _a;
|
||
|
|
const view = this.app.workspace.getActiveViewOfType(import_obsidian12.MarkdownView);
|
||
|
|
return (_a = view == null ? void 0 : view.file) != null ? _a : null;
|
||
|
|
}
|
||
|
|
onClose() {
|
||
|
|
const { contentEl } = this;
|
||
|
|
contentEl.empty();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
function openFilePicker(app, imageProcessor, propertyHandler, insertToProperty = false, propertyName) {
|
||
|
|
new FilePickerModal(app, imageProcessor, propertyHandler, insertToProperty, propertyName).open();
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/modals/RemoteSearchModal.ts
|
||
|
|
var import_obsidian13 = require("obsidian");
|
||
|
|
var RemoteSearchModal = class extends import_obsidian13.Modal {
|
||
|
|
constructor(app, settings, remoteService, imageProcessor, propertyHandler, options = {}) {
|
||
|
|
super(app);
|
||
|
|
this.container = null;
|
||
|
|
this.queryInput = null;
|
||
|
|
this.providerSelect = null;
|
||
|
|
this.sizeSelect = null;
|
||
|
|
this.scrollArea = null;
|
||
|
|
this.imagesList = null;
|
||
|
|
this.loadingContainer = null;
|
||
|
|
this.currentQuery = "";
|
||
|
|
this.currentPage = 1;
|
||
|
|
this.currentResults = [];
|
||
|
|
this.isLoading = false;
|
||
|
|
this.selectedImage = 0;
|
||
|
|
this.settings = settings;
|
||
|
|
this.remoteService = remoteService;
|
||
|
|
this.imageProcessor = imageProcessor;
|
||
|
|
this.propertyHandler = propertyHandler;
|
||
|
|
this.options = options;
|
||
|
|
this.currentProvider = settings.defaultProvider;
|
||
|
|
this.containerEl.addClass("image-inserter-container");
|
||
|
|
}
|
||
|
|
onOpen() {
|
||
|
|
const { contentEl } = this;
|
||
|
|
this.container = contentEl.createDiv({ cls: "container" });
|
||
|
|
const inputGroup = this.container.createDiv({ cls: "input-group" });
|
||
|
|
this.queryInput = inputGroup.createEl("input", {
|
||
|
|
type: "text",
|
||
|
|
cls: "query-input",
|
||
|
|
attr: { placeholder: "Search images...", autofocus: "true" }
|
||
|
|
});
|
||
|
|
this.providerSelect = inputGroup.createEl("select", { cls: "selector" });
|
||
|
|
this.providerSelect.createEl("option", { text: "Unsplash", value: "unsplash" /* Unsplash */ });
|
||
|
|
this.providerSelect.createEl("option", { text: "Pexels", value: "pexels" /* Pexels */ });
|
||
|
|
this.providerSelect.createEl("option", { text: "Pixabay", value: "pixabay" /* Pixabay */ });
|
||
|
|
this.providerSelect.value = this.currentProvider;
|
||
|
|
this.sizeSelect = inputGroup.createEl("select", { cls: "selector" });
|
||
|
|
this.sizeSelect.createEl("option", { text: "Original", value: "original" /* Original */ });
|
||
|
|
this.sizeSelect.createEl("option", { text: "Large", value: "large" /* Large */ });
|
||
|
|
this.sizeSelect.createEl("option", { text: "Medium", value: "medium" /* Medium */ });
|
||
|
|
this.sizeSelect.createEl("option", { text: "Small", value: "small" /* Small */ });
|
||
|
|
this.sizeSelect.value = this.settings.defaultImageSize;
|
||
|
|
this.loadingContainer = this.container.createDiv({ cls: "loading-container" });
|
||
|
|
const loaderIcon = this.loadingContainer.createDiv({ cls: "loader-icon" });
|
||
|
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||
|
|
svg.setAttribute("width", "24");
|
||
|
|
svg.setAttribute("height", "24");
|
||
|
|
svg.setAttribute("viewBox", "0 0 24 24");
|
||
|
|
svg.setAttribute("fill", "none");
|
||
|
|
svg.setAttribute("stroke", "currentColor");
|
||
|
|
svg.setAttribute("stroke-width", "2");
|
||
|
|
svg.setAttribute("stroke-linecap", "round");
|
||
|
|
svg.setAttribute("stroke-linejoin", "round");
|
||
|
|
svg.classList.add("lucide", "lucide-loader-circle");
|
||
|
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||
|
|
path.setAttribute("d", "M21 12a9 9 0 1 1-6.219-8.56");
|
||
|
|
svg.appendChild(path);
|
||
|
|
loaderIcon.appendChild(svg);
|
||
|
|
this.showLoading(false);
|
||
|
|
this.scrollArea = this.container.createDiv({ cls: "scroll-area" });
|
||
|
|
this.imagesList = this.scrollArea.createDiv({ cls: "images-list" });
|
||
|
|
this.setupEventListeners();
|
||
|
|
setTimeout(() => {
|
||
|
|
var _a;
|
||
|
|
(_a = this.queryInput) == null ? void 0 : _a.focus();
|
||
|
|
}, 50);
|
||
|
|
}
|
||
|
|
setupEventListeners() {
|
||
|
|
var _a, _b, _c, _d;
|
||
|
|
const debouncedSearch = (0, import_obsidian13.debounce)((query) => {
|
||
|
|
if (query.trim()) {
|
||
|
|
void this.performSearch(query, true);
|
||
|
|
} else {
|
||
|
|
this.clearResults();
|
||
|
|
}
|
||
|
|
}, 1e3, true);
|
||
|
|
(_a = this.queryInput) == null ? void 0 : _a.addEventListener("input", (e) => {
|
||
|
|
const query = e.target.value;
|
||
|
|
this.currentQuery = query;
|
||
|
|
this.showLoading(true);
|
||
|
|
debouncedSearch(query);
|
||
|
|
});
|
||
|
|
(_b = this.queryInput) == null ? void 0 : _b.addEventListener("keydown", (e) => {
|
||
|
|
var _a2;
|
||
|
|
if (e.key === "Enter") {
|
||
|
|
e.preventDefault();
|
||
|
|
const query = ((_a2 = this.queryInput) == null ? void 0 : _a2.value.trim()) || "";
|
||
|
|
if (query) {
|
||
|
|
void this.performSearch(query, true);
|
||
|
|
} else if (this.currentResults.length > 0 && this.selectedImage < this.currentResults.length) {
|
||
|
|
const image = this.currentResults[this.selectedImage];
|
||
|
|
if (image) {
|
||
|
|
void this.insertImage(image);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if (e.ctrlKey && e.key === "n") {
|
||
|
|
e.preventDefault();
|
||
|
|
if (this.currentResults.length > 0) {
|
||
|
|
this.selectedImage = (this.selectedImage + 1) % this.currentResults.length;
|
||
|
|
this.renderResults();
|
||
|
|
}
|
||
|
|
} else if (e.ctrlKey && e.key === "p") {
|
||
|
|
e.preventDefault();
|
||
|
|
if (this.currentResults.length > 0) {
|
||
|
|
this.selectedImage = (this.selectedImage - 1 + this.currentResults.length) % this.currentResults.length;
|
||
|
|
this.renderResults();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
(_c = this.providerSelect) == null ? void 0 : _c.addEventListener("change", (e) => {
|
||
|
|
this.currentProvider = e.target.value;
|
||
|
|
if (this.currentQuery) {
|
||
|
|
this.showLoading(true);
|
||
|
|
void this.performSearch(this.currentQuery, true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
(_d = this.sizeSelect) == null ? void 0 : _d.addEventListener("change", (e) => {
|
||
|
|
const size = e.target.value;
|
||
|
|
this.settings.defaultImageSize = size;
|
||
|
|
if (this.currentQuery) {
|
||
|
|
this.showLoading(true);
|
||
|
|
void this.performSearch(this.currentQuery, true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
async performSearch(query, resetPage = false) {
|
||
|
|
if (this.isLoading) return;
|
||
|
|
this.currentQuery = query;
|
||
|
|
if (resetPage) {
|
||
|
|
this.currentPage = 1;
|
||
|
|
}
|
||
|
|
this.isLoading = true;
|
||
|
|
this.showLoading(true);
|
||
|
|
try {
|
||
|
|
this.currentResults = await this.remoteService.search(
|
||
|
|
query,
|
||
|
|
this.currentProvider,
|
||
|
|
this.currentPage
|
||
|
|
);
|
||
|
|
this.selectedImage = 0;
|
||
|
|
this.renderResults();
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Search failed:", error);
|
||
|
|
const errorMsg = error instanceof Error ? error.message : "Search failed";
|
||
|
|
new import_obsidian13.Notice(`Request failed, status ${errorMsg}`);
|
||
|
|
this.renderError(errorMsg);
|
||
|
|
} finally {
|
||
|
|
this.isLoading = false;
|
||
|
|
this.showLoading(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
renderResults() {
|
||
|
|
if (!this.imagesList) return;
|
||
|
|
this.imagesList.empty();
|
||
|
|
if (this.currentResults.length === 0) {
|
||
|
|
const noResult = this.imagesList.createDiv({ cls: "no-result-container" });
|
||
|
|
noResult.setText("No results found");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
for (let i = 0; i < this.currentResults.length; i++) {
|
||
|
|
const image = this.currentResults[i];
|
||
|
|
if (!image) continue;
|
||
|
|
const result = this.imagesList.createDiv({
|
||
|
|
cls: `query-result${i === this.selectedImage ? " is-selected" : ""}`
|
||
|
|
});
|
||
|
|
result.createEl("img", {
|
||
|
|
attr: {
|
||
|
|
src: image.thumbnailUrl,
|
||
|
|
alt: image.description || "Image"
|
||
|
|
}
|
||
|
|
});
|
||
|
|
result.addEventListener("click", () => {
|
||
|
|
void this.insertImage(image);
|
||
|
|
});
|
||
|
|
result.addEventListener("mousemove", () => {
|
||
|
|
this.selectedImage = i;
|
||
|
|
this.renderResults();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
this.renderPagination();
|
||
|
|
}
|
||
|
|
renderPagination() {
|
||
|
|
if (!this.scrollArea) return;
|
||
|
|
const existingPagination = this.scrollArea.querySelector(".pagination");
|
||
|
|
if (existingPagination) {
|
||
|
|
existingPagination.remove();
|
||
|
|
}
|
||
|
|
const hasMore = this.currentResults.length >= 20;
|
||
|
|
if (hasMore || this.currentPage > 1) {
|
||
|
|
const pagination = this.scrollArea.createDiv({ cls: "pagination" });
|
||
|
|
if (this.currentPage > 1) {
|
||
|
|
const prevBtn = pagination.createEl("button", { cls: "btn", text: "Previous" });
|
||
|
|
prevBtn.addEventListener("click", () => {
|
||
|
|
this.currentPage--;
|
||
|
|
this.showLoading(true);
|
||
|
|
void this.performSearch(this.currentQuery);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
if (hasMore) {
|
||
|
|
const nextBtn = pagination.createEl("button", { cls: "btn", text: "Next" });
|
||
|
|
nextBtn.addEventListener("click", () => {
|
||
|
|
this.currentPage++;
|
||
|
|
this.showLoading(true);
|
||
|
|
void this.performSearch(this.currentQuery);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
renderError(message) {
|
||
|
|
if (!this.imagesList) return;
|
||
|
|
this.imagesList.empty();
|
||
|
|
const errorDiv = this.imagesList.createDiv({ cls: "no-result-container error-text" });
|
||
|
|
errorDiv.setText(`Error: ${message}`);
|
||
|
|
}
|
||
|
|
clearResults() {
|
||
|
|
if (this.imagesList) {
|
||
|
|
this.imagesList.empty();
|
||
|
|
}
|
||
|
|
this.currentResults = [];
|
||
|
|
}
|
||
|
|
showLoading(show) {
|
||
|
|
if (this.loadingContainer) {
|
||
|
|
this.loadingContainer.style.display = show ? "flex" : "none";
|
||
|
|
}
|
||
|
|
if (this.scrollArea) {
|
||
|
|
if (show) {
|
||
|
|
this.scrollArea.addClass("loading");
|
||
|
|
} else {
|
||
|
|
this.scrollArea.removeClass("loading");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async insertImage(image) {
|
||
|
|
this.close();
|
||
|
|
const activeFile = this.getActiveFile();
|
||
|
|
if (!activeFile) {
|
||
|
|
new import_obsidian13.Notice("No active file");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const downloadUrl = this.remoteService.getDownloadUrl(image, this.settings.defaultImageSize);
|
||
|
|
if (this.options.insertToProperty) {
|
||
|
|
if (!this.options.propertyName || this.options.propertyName.trim() === "") {
|
||
|
|
new import_obsidian13.Notice("Please specify a property name in settings");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
await this.propertyHandler.insertImageFromUrl(
|
||
|
|
downloadUrl,
|
||
|
|
activeFile,
|
||
|
|
this.options.propertyName,
|
||
|
|
image,
|
||
|
|
// Pass RemoteImage for referral text generation
|
||
|
|
this.currentQuery
|
||
|
|
// Pass search term as suggested name
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
const result = await this.imageProcessor.processImageUrl(
|
||
|
|
downloadUrl,
|
||
|
|
activeFile,
|
||
|
|
true,
|
||
|
|
// Show rename modal
|
||
|
|
false,
|
||
|
|
// Not property insertion
|
||
|
|
this.currentQuery
|
||
|
|
// Pass search term as suggested name
|
||
|
|
);
|
||
|
|
if (result.success && result.linkText) {
|
||
|
|
const referralText = this.remoteService.generateReferralText(image);
|
||
|
|
const fullText = result.linkText + referralText;
|
||
|
|
const view = this.app.workspace.getActiveViewOfType(import_obsidian13.MarkdownView);
|
||
|
|
if (view == null ? void 0 : view.editor) {
|
||
|
|
view.editor.replaceSelection(fullText);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to insert image:", error);
|
||
|
|
new import_obsidian13.Notice(`Failed to insert image: ${error instanceof Error ? error.message : String(error)}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
getActiveFile() {
|
||
|
|
var _a;
|
||
|
|
const view = this.app.workspace.getActiveViewOfType(import_obsidian13.MarkdownView);
|
||
|
|
return (_a = view == null ? void 0 : view.file) != null ? _a : null;
|
||
|
|
}
|
||
|
|
onClose() {
|
||
|
|
const { contentEl } = this;
|
||
|
|
contentEl.empty();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
function openRemoteSearch(app, settings, remoteService, imageProcessor, propertyHandler, options = {}) {
|
||
|
|
new RemoteSearchModal(app, settings, remoteService, imageProcessor, propertyHandler, options).open();
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/main.ts
|
||
|
|
var SettingsObservable = class {
|
||
|
|
constructor() {
|
||
|
|
this.observers = [];
|
||
|
|
}
|
||
|
|
subscribe(fn) {
|
||
|
|
this.observers.push(fn);
|
||
|
|
}
|
||
|
|
notify(settings) {
|
||
|
|
this.observers.forEach((fn) => fn(settings));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var ImageManagerPlugin = class extends import_obsidian15.Plugin {
|
||
|
|
constructor() {
|
||
|
|
super(...arguments);
|
||
|
|
// Settings observer for notifying services of changes
|
||
|
|
this.settingsObservable = new SettingsObservable();
|
||
|
|
}
|
||
|
|
async onload() {
|
||
|
|
await this.loadSettings();
|
||
|
|
await this.migrateApiKeysToSecrets();
|
||
|
|
this.initializeServices();
|
||
|
|
this.registerEventHandlers();
|
||
|
|
this.registerCommands();
|
||
|
|
this.addSettingTab(new ImageManagerSettingTab(this.app, this));
|
||
|
|
this.log("Image Manager plugin loaded");
|
||
|
|
}
|
||
|
|
onunload() {
|
||
|
|
var _a;
|
||
|
|
(_a = this.bannerService) == null ? void 0 : _a.destroy();
|
||
|
|
this.log("Image Manager plugin unloaded");
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Migrate API keys from plaintext to SecretStorage (one-time migration for 1.11.4+)
|
||
|
|
* Only runs if Secrets API is available and secret IDs are not already set
|
||
|
|
*/
|
||
|
|
async migrateApiKeysToSecrets() {
|
||
|
|
if (!(0, import_obsidian15.requireApiVersion)("1.11.4")) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let migrated = false;
|
||
|
|
const failures = [];
|
||
|
|
const secretStorage = this.app.secretStorage;
|
||
|
|
if (!secretStorage) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!this.settings.pexelsApiKeySecretId && this.settings.pexelsApiKey) {
|
||
|
|
const secretId = "image-manager-pexels-api-key";
|
||
|
|
try {
|
||
|
|
secretStorage.setSecret(secretId, this.settings.pexelsApiKey);
|
||
|
|
this.settings.pexelsApiKeySecretId = secretId;
|
||
|
|
migrated = true;
|
||
|
|
console.info("[Image Manager] Successfully migrated Pexels API key to SecretStorage");
|
||
|
|
} catch (error) {
|
||
|
|
console.error("[Image Manager] Failed to migrate Pexels API key to SecretStorage:", error);
|
||
|
|
failures.push("Pexels");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!this.settings.pixabayApiKeySecretId && this.settings.pixabayApiKey) {
|
||
|
|
const secretId = "image-manager-pixabay-api-key";
|
||
|
|
try {
|
||
|
|
secretStorage.setSecret(secretId, this.settings.pixabayApiKey);
|
||
|
|
this.settings.pixabayApiKeySecretId = secretId;
|
||
|
|
migrated = true;
|
||
|
|
console.info("[Image Manager] Successfully migrated Pixabay API key to SecretStorage");
|
||
|
|
} catch (error) {
|
||
|
|
console.error("[Image Manager] Failed to migrate Pixabay API key to SecretStorage:", error);
|
||
|
|
failures.push("Pixabay");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (migrated) {
|
||
|
|
await this.saveSettings();
|
||
|
|
console.info("[Image Manager] API key migration completed");
|
||
|
|
}
|
||
|
|
if (failures.length > 0) {
|
||
|
|
new import_obsidian15.Notice(
|
||
|
|
`Image Manager: Failed to migrate ${failures.join(" and ")} API key(s) to secure storage. Please re-enter your API key(s) in settings.`,
|
||
|
|
1e4
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Initialize all services
|
||
|
|
*/
|
||
|
|
initializeServices() {
|
||
|
|
this.storageManager = new StorageManager(this.app, this.settings, this.settingsObservable);
|
||
|
|
this.remoteService = new RemoteImageService(this.app, this.settings, this.settingsObservable);
|
||
|
|
this.imageProcessor = new ImageProcessor(this.app, this.settings, this.storageManager, this.settingsObservable);
|
||
|
|
this.propertyHandler = new PropertyHandler(this.app, this.settings, this.storageManager, this.imageProcessor, this.remoteService, this.settingsObservable);
|
||
|
|
this.pasteHandler = new PasteHandler(
|
||
|
|
this.app,
|
||
|
|
this.settings,
|
||
|
|
this.imageProcessor,
|
||
|
|
this.propertyHandler,
|
||
|
|
this.settingsObservable
|
||
|
|
);
|
||
|
|
this.dropHandler = new DropHandler(this.app, this.settings, this.imageProcessor, this.settingsObservable);
|
||
|
|
this.conversionService = new LocalConversionService(this.app, this.settings, this.storageManager, this.imageProcessor, this.settingsObservable);
|
||
|
|
this.bannerService = new BannerService(this.app, this.settings, this.settingsObservable);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Register event handlers
|
||
|
|
*/
|
||
|
|
registerEventHandlers() {
|
||
|
|
this.registerEvent(
|
||
|
|
this.app.workspace.on("editor-paste", (evt, editor, view) => {
|
||
|
|
void this.pasteHandler.handleEditorPaste(evt, editor, view);
|
||
|
|
})
|
||
|
|
);
|
||
|
|
this.registerEvent(
|
||
|
|
this.app.workspace.on("editor-drop", (evt, editor, view) => {
|
||
|
|
void this.dropHandler.handleEditorDrop(evt, editor, view);
|
||
|
|
})
|
||
|
|
);
|
||
|
|
this.registerDomEvent(document, "paste", (evt) => {
|
||
|
|
const target = evt.target;
|
||
|
|
if (!target || !target.closest(".workspace")) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
void this.pasteHandler.handlePropertyPaste(evt);
|
||
|
|
}, { capture: true });
|
||
|
|
this.registerEvent(
|
||
|
|
this.app.workspace.on("file-open", (file) => {
|
||
|
|
if (!file) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (this.settings.autoConvertRemoteImages && this.settings.convertOnNoteOpen) {
|
||
|
|
if (this.settings.supportedExtensions.includes(file.extension)) {
|
||
|
|
void (async () => {
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
|
|
const activeFile = this.app.workspace.getActiveFile();
|
||
|
|
const isActiveFile = activeFile && activeFile.path === file.path;
|
||
|
|
if (isActiveFile || this.settings.processBackgroundChanges) {
|
||
|
|
const count = await this.conversionService.processFile(file, !isActiveFile);
|
||
|
|
if (count > 0) {
|
||
|
|
new import_obsidian15.Notice(`Converted ${count} remote image(s) to local`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const deviceSettings = this.bannerService.getDeviceSettings();
|
||
|
|
if (deviceSettings.enabled && (this.settings.supportedExtensions.includes(file.extension) || file.extension === "md")) {
|
||
|
|
const view = this.app.workspace.getActiveViewOfType(import_obsidian15.MarkdownView);
|
||
|
|
if (view instanceof import_obsidian15.MarkdownView) {
|
||
|
|
void this.bannerService.process(file, view);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
);
|
||
|
|
this.registerEvent(
|
||
|
|
this.app.workspace.on("layout-change", () => {
|
||
|
|
const deviceSettings = this.bannerService.getDeviceSettings();
|
||
|
|
if (!deviceSettings.enabled) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const view = this.app.workspace.getActiveViewOfType(import_obsidian15.MarkdownView);
|
||
|
|
if (view && view.file && (this.settings.supportedExtensions.includes(view.file.extension) || view.file.extension === "md")) {
|
||
|
|
void this.bannerService.process(view.file, view);
|
||
|
|
}
|
||
|
|
})
|
||
|
|
);
|
||
|
|
this.registerEvent(
|
||
|
|
this.app.metadataCache.on("changed", (file) => {
|
||
|
|
if (this.settings.autoConvertRemoteImages && this.settings.convertOnNoteSave) {
|
||
|
|
if (this.settings.supportedExtensions.includes(file.extension)) {
|
||
|
|
void (async () => {
|
||
|
|
const activeFile = this.app.workspace.getActiveFile();
|
||
|
|
const isActiveFile = activeFile && activeFile.path === file.path;
|
||
|
|
if (isActiveFile || this.settings.processBackgroundChanges) {
|
||
|
|
const count = await this.conversionService.processFile(file, !isActiveFile);
|
||
|
|
if (count > 0) {
|
||
|
|
new import_obsidian15.Notice(`Converted ${count} remote image(s) to local`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const deviceSettings = this.bannerService.getDeviceSettings();
|
||
|
|
if (!deviceSettings.enabled || !this.settings.supportedExtensions.includes(file.extension) && file.extension !== "md") {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.app.workspace.iterateRootLeaves((leaf) => {
|
||
|
|
const view = leaf.view;
|
||
|
|
if (view instanceof import_obsidian15.MarkdownView && view.file === file) {
|
||
|
|
void this.bannerService.process(file, view);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
})
|
||
|
|
);
|
||
|
|
this.app.workspace.onLayoutReady(() => {
|
||
|
|
this.bannerService.applySettings();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Register commands
|
||
|
|
*/
|
||
|
|
registerCommands() {
|
||
|
|
this.addCommand({
|
||
|
|
id: "insert-image",
|
||
|
|
name: "Insert local image",
|
||
|
|
editorCallback: (editor, view) => {
|
||
|
|
openFilePicker(this.app, this.imageProcessor, this.propertyHandler);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.addCommand({
|
||
|
|
id: "search-image",
|
||
|
|
name: "Insert remote image",
|
||
|
|
editorCallback: (editor, view) => {
|
||
|
|
openRemoteSearch(
|
||
|
|
this.app,
|
||
|
|
this.settings,
|
||
|
|
this.remoteService,
|
||
|
|
this.imageProcessor,
|
||
|
|
this.propertyHandler
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.addCommand({
|
||
|
|
id: "insert-remote-image-to-property",
|
||
|
|
name: "Insert remote image to property",
|
||
|
|
editorCallback: (editor, view) => {
|
||
|
|
openRemoteSearch(
|
||
|
|
this.app,
|
||
|
|
this.settings,
|
||
|
|
this.remoteService,
|
||
|
|
this.imageProcessor,
|
||
|
|
this.propertyHandler,
|
||
|
|
{ insertToProperty: true, propertyName: this.settings.defaultPropertyName }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.addCommand({
|
||
|
|
id: "insert-local-image-to-property",
|
||
|
|
name: "Insert local image to property",
|
||
|
|
editorCallback: (editor, view) => {
|
||
|
|
openFilePicker(
|
||
|
|
this.app,
|
||
|
|
this.imageProcessor,
|
||
|
|
this.propertyHandler,
|
||
|
|
true,
|
||
|
|
// insertToProperty
|
||
|
|
this.settings.defaultPropertyName
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.addCommand({
|
||
|
|
id: "insert-remote-image-to-icon-property",
|
||
|
|
name: "Insert remote image to icon property",
|
||
|
|
editorCallback: (editor, view) => {
|
||
|
|
openRemoteSearch(
|
||
|
|
this.app,
|
||
|
|
this.settings,
|
||
|
|
this.remoteService,
|
||
|
|
this.imageProcessor,
|
||
|
|
this.propertyHandler,
|
||
|
|
{ insertToProperty: true, propertyName: this.settings.defaultIconPropertyName }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.addCommand({
|
||
|
|
id: "insert-local-image-to-icon-property",
|
||
|
|
name: "Insert local image to icon property",
|
||
|
|
editorCallback: (editor, view) => {
|
||
|
|
openFilePicker(
|
||
|
|
this.app,
|
||
|
|
this.imageProcessor,
|
||
|
|
this.propertyHandler,
|
||
|
|
true,
|
||
|
|
// insertToProperty
|
||
|
|
this.settings.defaultIconPropertyName
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.addCommand({
|
||
|
|
id: "convert-remote-images",
|
||
|
|
name: "Convert remote images",
|
||
|
|
editorCallback: async (editor, view) => {
|
||
|
|
const file = view.file;
|
||
|
|
if (!file) {
|
||
|
|
new import_obsidian15.Notice("No active file");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const count = await this.conversionService.processFile(file);
|
||
|
|
if (count > 0) {
|
||
|
|
new import_obsidian15.Notice(`Converted ${count} remote image(s) to local`);
|
||
|
|
} else {
|
||
|
|
new import_obsidian15.Notice("No remote images found");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
this.addCommand({
|
||
|
|
id: "convert-all-remote-images",
|
||
|
|
name: "Convert all remote images",
|
||
|
|
callback: async () => {
|
||
|
|
const { openConfirmModal: openConfirmModal2 } = await Promise.resolve().then(() => (init_ConfirmModal(), ConfirmModal_exports));
|
||
|
|
const result = await openConfirmModal2(
|
||
|
|
this.app,
|
||
|
|
"Convert All Remote Images",
|
||
|
|
"This will scan all files in your vault and convert every remote image URL to a local file. This action cannot be undone.\n\nEach image will be downloaded and you'll be prompted to rename them. This may take a while if you have many images.\n\nAre you sure you want to proceed?",
|
||
|
|
"Yes, convert all images",
|
||
|
|
"Cancel"
|
||
|
|
);
|
||
|
|
if (!result.confirmed) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
new import_obsidian15.Notice("Processing all files... This may take a while.");
|
||
|
|
const count = await this.conversionService.processAllFiles();
|
||
|
|
new import_obsidian15.Notice(`Converted ${count} remote image(s) to local`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Load settings from storage
|
||
|
|
*/
|
||
|
|
async loadSettings() {
|
||
|
|
const data = await this.loadData();
|
||
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, data != null ? data : {});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Save settings to storage
|
||
|
|
*/
|
||
|
|
async saveSettings() {
|
||
|
|
await this.saveData(this.settings);
|
||
|
|
this.settingsObservable.notify(this.settings);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Debug logging
|
||
|
|
*/
|
||
|
|
log(...args) {
|
||
|
|
var _a;
|
||
|
|
if ((_a = this.settings) == null ? void 0 : _a.debugMode) {
|
||
|
|
console.debug("[Image Manager]", ...args);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsic3JjL21vZGFscy9Db25maXJtTW9kYWwudHMiLCAic3JjL21haW4udHMiLCAic3JjL3NldHRpbmdzLnRzIiwgInNyYy90eXBlcy50cyIsICJzcmMvc2VydmljZXMvU3RvcmFnZU1hbmFnZXIudHMiLCAic3JjL3NlcnZpY2VzL0ltYWdlUHJvY2Vzc29yLnRzIiwgInNyYy91dGlscy90ZW1wbGF0ZS50cyIsICJzcmMvbW9kYWxzL1JlbmFtZU1vZGFsLnRzIiwgInNyYy9tb2RhbHMvRGVzY3JpcHRpdmVJbWFnZU1vZGFsLnRzIiwgInNyYy91dGlscy9rZWJhYi1jYXNlLnRzIiwgInNyYy9zZXJ2aWNlcy9Qcm9wZXJ0eUhhbmRsZXIudHMiLCAic3JjL3V0aWxzL21keC1mcm9udG1hdHRlci50cyIsICJzcmMvc2VydmljZXMvUGFzdGVIYW5kbGVyLnRzIiwgInNyYy9zZXJ2aWNlcy9SZW1vdGVJbWFnZVNlcnZpY2UudHMiLCAic3JjL3NlcnZpY2VzL0xvY2FsQ29udmVyc2lvblNlcnZpY2UudHMiLCAic3JjL3NlcnZpY2VzL0Jhbm5lclNlcnZpY2UudHMiLCAic3JjL21vZGFscy9GaWxlUGlja2VyTW9kYWwudHMiLCAic3JjL21vZGFscy9SZW1vdGVTZWFyY2hNb2RhbC50cyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiLyoqXHJcbiAqIENvbmZpcm1hdGlvbiBNb2RhbFxyXG4gKiBTaW1wbGUgbW9kYWwgZm9yIGNvbmZpcm1pbmcgcG90ZW50aWFsbHkgZGVzdHJ1Y3RpdmUgYWN0aW9uc1xyXG4gKi9cclxuXHJcbmltcG9ydCB7IEFwcCwgTW9kYWwgfSBmcm9tICdvYnNpZGlhbic7XHJcblxyXG5leHBvcnQgaW50ZXJmYWNlIENvbmZpcm1SZXN1bHQge1xyXG5cdGNvbmZpcm1lZDogYm9vbGVhbjtcclxufVxyXG5cclxuZXhwb3J0IGNsYXNzIENvbmZpcm1Nb2RhbCBleHRlbmRzIE1vZGFsIHtcclxuXHRwcml2YXRlIHRpdGxlOiBzdHJpbmc7XHJcblx0cHJpdmF0ZSBtZXNzYWdlOiBzdHJpbmc7XHJcblx0cHJpdmF0ZSBjb25maXJtVGV4dDogc3RyaW5nO1xyXG5cdHByaXZhdGUgY2FuY2VsVGV4dDogc3RyaW5nO1xyXG5cdHByaXZhdGUgcmVzb2x2ZTogKHJlc3VsdDogQ29uZmlybVJlc3VsdCkgPT4gdm9pZDtcclxuXHJcblx0Y29uc3RydWN0b3IoXHJcblx0XHRhcHA6IEFwcCxcclxuXHRcdHRpdGxlOiBzdHJpbmcsXHJcblx0XHRtZXNzYWdlOiBzdHJpbmcsXHJcblx0XHRjb25maXJtVGV4dDogc3RyaW5nID0gJ0NvbmZpcm0nLFxyXG5cdFx0Y2FuY2VsVGV4dDogc3RyaW5nID0gJ0NhbmNlbCdcclxuXHQpIHtcclxuXHRcdHN1cGVyKGFwcCk7XHJcblx0XHR0aGlzLnRpdGxlID0gdGl0bGU7XHJcblx0XHR0aGlzLm1lc3NhZ2UgPSBtZXNzYWdlO1xyXG5cdFx0dGhpcy5jb25maXJtVGV4dCA9IGNvbmZpcm1UZXh0O1xyXG5cdFx0dGhpcy5jYW5jZWxUZXh0ID0gY2FuY2VsVGV4dDtcclxuXHR9XHJcblxyXG5cdG9uT3BlbigpOiB2b2lkIHtcclxuXHRcdGNvbnN0IHsgY29udGVudEVsLCB0aXRsZUVsIH0gPSB0aGlzO1xyXG5cdFx0dGl0bGVFbC5zZXRUZXh0KHRoaXMudGl0bGUpO1xyXG5cclxuXHRcdC8vIE1lc3NhZ2VcclxuXHRcdGNvbnN0IG1lc3NhZ2VFbCA9IGNvbnRlbnRFbC5jcmVhdGVEaXYoeyBjbHM6ICdpbWFnZS1tYW5hZ2VyLWNvbmZpcm0tbWVzc2FnZScgfSk7XHJcblx0XHRtZXNzYWdlRWwuY3JlYXRlRWwoJ3AnLCB7IHRleHQ6IHRoaXMubWVzc2FnZSB9KTtcclxuXHJcblx0XHQvLyBCdXR0b25zXHJcblx0XHRjb25zdCBidXR0b25Db250YWluZXIgPSBjb250ZW50RWwuY3JlYXRlRGl2KHsgY2xzOiAnaW1hZ2UtbWFuYWdlci1jb25maXJtLWJ1dHRvbnMnIH0pO1xyXG5cdFx0XHJcblx0XHRidXR0b25Db250YWluZXIuY3JlYXRlRWwoJ2J1dHRvbicsIHtcclxuXHRcdFx0dGV4dDogdGhpcy5jb25maXJtVGV4dCxcclxuXHRcdFx0Y2xzOiAnbW9kLWN0YScsXHJcblx0XHR9KS5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHtcclxuXHRcdFx0dGhpcy5yZXNvbHZlKHsgY29uZmlybWVkOiB0cnVlIH0pO1xyXG5cdFx0XHR0aGlzLmNsb3NlKCk7XHJcblx0XHR9KTtcclxuXHJcblx0XHRidXR0b25Db250YWluZXIuY3JlYXRlRWwoJ2J1dHRvbicsIHtcclxuXHRcdFx0dGV4dDogdGhpcy5jYW5jZWxUZXh0LFxyXG5cdFx0fSkuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB7XHJcblx0XHRcdHRoaXMucmVzb2x2ZSh7IGNvbmZpcm1lZDogZmFsc2UgfSk7XHJcblx0XHRcdHRoaXMuY2xvc2UoKTtcclxuXHRcdH0pO1xyXG5cclxuXHRcdC8vIEZvY3VzIHRoZSBjb25maXJtIGJ1dHRvblxyXG5cdFx0c2V0VGltZW91dCgoKSA9PiB7XHJcblx0XHRcdGNvbnN0IGNvbmZpcm1CdXR0b24gPSBidXR0b25Db250YWluZXIucXVlcnlTZWxlY3RvcignLm1vZC1jdGEnKSBhcyBIVE1MQnV0dG9uRWxlbWVudDtcclxuXHRcdFx0Y29uZmlybUJ1dHRvbj8uZm9jdXMoKTtcclxuXHRcdH0sIDUwKTtcclxuXHR9XHJcblxyXG5cdG9uQ2xvc2UoKTogdm9pZCB7XHJcblx0XHRjb25zdCB7IGNvbnRlbnRFbCB9ID0gdGhpcztcclxuXHRcdGNvbnRlbnRFbC5lbXB0eSgpO1xyXG5cdH1cclxuXHJcblx0cHVibGljIG9wZW5BbmRBd2FpdFJlc3VsdCgpOiBQcm9taXNlPENvbmZpcm1SZXN1bHQ+IHtcclxuXHRcdHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4ge1xyXG5cdFx0XHR0aGlzLnJlc29sdmUgPSByZXNvbHZlO1xyXG5cdFx0XHR0aGlzLm9wZW4oKTtcclxuXHRcdH0pO1xyXG5cdH1cclxufVxyXG5cclxuLyoqXHJcbiAqIE9wZW4gYSBjb25maXJtYXRpb24gbW9kYWwgYW5kIHJldHVybiB0aGUgcmVzdWx0XHJcbiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gb3BlbkNvbmZpcm1Nb2RhbChcclxuXHRhcHA6IEFwcCxcclxuXHR0aXRsZTogc3RyaW5nLFxyXG5cdG1lc3NhZ2U6IHN0cmluZyxcclxuXHRjb25maXJtVGV4dDogc3RyaW5nID0gJ0NvbmZpcm0nLFxyXG5cdGNhbmNlbFRleHQ6IHN0cmluZyA9ICdDYW5jZWwnXHJcbik6IFByb21pc2U8Q29uZmlybVJlc3VsdD4ge1xyXG5cdGNvbnN0IG1vZGFsID0gbmV3IENvbmZpcm1Nb2RhbChhcHAsIHRpdGxlLCBtZXNzYWdlLCBjb25maXJtVGV4d
|