mirror of
https://github.com/lossless-group/image-gin.git
synced 2026-07-22 06:49:33 +00:00
ObsidianReviewBot flagged 49 sentence-case findings on commit cfbceba2; an audit of every cited line classified ~47 of them as brand-name false positives (Recraft, Magnific, Freepik, Ideogram, ImageKit, Imgur, WebP — all product names that our local eslint.config.mjs allowlists for `obsidianmd/ui/sentence-case`, but the server-side bot runs the rule with the default DEFAULT_BRANDS list which doesn't know our integrations). Two strings were genuine violations of sentence case and are fixed here: - main.ts:153 — command name `'Drop gate: Reset session-remembered destination'` → lowercase `reset` after the colon, since "Reset" is not a proper noun and after-colon capitalization isn't standard sentence case in Obsidian's UI conventions. - settings/settings.ts:915 — heading `"Drag-Drop / paste confirmation gate"` → lowercase `drop` after the hyphen; "Drag-drop" is one compound word, not two proper nouns. The remaining brand-name flags are addressed via a `/skip` comment on PR #12524 once the bot rescans this push. Plan and `/skip` draft text live at: context-v/plans/2026-05-10_Final-ObsidianReviewBot-Cleanup-Round.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
184 lines
No EOL
7.9 KiB
TypeScript
184 lines
No EOL
7.9 KiB
TypeScript
import { Plugin, Notice } from 'obsidian';
|
|
// Import modal classes
|
|
import { CurrentFileModal } from './src/modals/CurrentFileModal';
|
|
import { ConvertLocalImagesForCurrentFile } from './src/modals/ConvertLocalImagesForCurrentFile';
|
|
import { BatchDirectoryConvertLocalToRemote } from './src/modals/BatchDirectoryConvertLocalToRemote';
|
|
import { MagnificModal } from './src/modals/MagnificModal';
|
|
import { IdeogramModal } from './src/modals/IdeogramModal';
|
|
import type { ImageGinSettings} from './src/settings/settings';
|
|
import { ImageGinSettingTab, DEFAULT_SETTINGS } from './src/settings/settings';
|
|
import { logger } from './src/utils/logger';
|
|
import { DropGateHandlers } from './src/handlers/DropGateHandlers';
|
|
import type { DropGateDestination } from './src/destinations/types';
|
|
import { VaultDestination } from './src/destinations/VaultDestination';
|
|
import { ImageKitDestination } from './src/destinations/ImageKitDestination';
|
|
import { ImgurDestination } from './src/destinations/ImgurDestination';
|
|
|
|
// Deep-merge `loaded` over `base` for plain objects only. Arrays are
|
|
// replaced wholesale (so user-edited `imageSizes` overrides defaults
|
|
// rather than being elementwise-merged). Required so partial nested
|
|
// provider blocks in data.json (e.g. `ideogram: { enabled, apiKey }`)
|
|
// don't drop the rest of the provider's defaults.
|
|
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
}
|
|
|
|
function deepMergeSettings<T>(base: T, loaded: unknown): T {
|
|
if (!isPlainObject(loaded) || !isPlainObject(base)) {
|
|
return (loaded === undefined ? base : loaded) as T;
|
|
}
|
|
const out: Record<string, unknown> = { ...base };
|
|
for (const key of Object.keys(loaded)) {
|
|
const baseVal = (base as Record<string, unknown>)[key];
|
|
const loadedVal = loaded[key];
|
|
if (isPlainObject(baseVal) && isPlainObject(loadedVal)) {
|
|
out[key] = deepMergeSettings(baseVal, loadedVal);
|
|
} else {
|
|
out[key] = loadedVal;
|
|
}
|
|
}
|
|
return out as T;
|
|
}
|
|
|
|
export default class ImageGinPlugin extends Plugin {
|
|
settings: ImageGinSettings = { ...DEFAULT_SETTINGS };
|
|
private dropGateHandlers: DropGateHandlers | null = null;
|
|
|
|
async loadSettings(): Promise<void> {
|
|
const raw: unknown = (await this.loadData()) ?? {};
|
|
const loadedSettings: Record<string, unknown> = (typeof raw === 'object' && raw !== null)
|
|
? (raw as Record<string, unknown>)
|
|
: {};
|
|
// One-shot migration: Freepik rebranded to Magnific. Move legacy
|
|
// `freepik` config into `magnific` so the user's saved key/enabled
|
|
// flag survive the schema rename. Persists via saveSettings below.
|
|
if (loadedSettings.freepik && !loadedSettings.magnific) {
|
|
loadedSettings.magnific = loadedSettings.freepik;
|
|
delete loadedSettings.freepik;
|
|
}
|
|
this.settings = deepMergeSettings(DEFAULT_SETTINGS, loadedSettings);
|
|
await this.saveSettings();
|
|
}
|
|
|
|
async saveSettings(): Promise<void> {
|
|
await this.saveData(this.settings);
|
|
}
|
|
|
|
async onload(): Promise<void> {
|
|
await this.loadSettings();
|
|
|
|
// Wire up the file logger so console.* calls in services/modals
|
|
// also persist to .obsidian/plugins/image-gin/log.json.
|
|
logger.initialize(this.app.vault);
|
|
|
|
this.addSettingTab(new ImageGinSettingTab(this.app, this));
|
|
|
|
// Register commands directly in onload
|
|
this.addCommand({
|
|
id: 'generate-images-for-current-file',
|
|
name: 'Generate images for current file',
|
|
callback: () => {
|
|
new CurrentFileModal(this.app, this).open();
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'convert-local-images-to-remote',
|
|
name: 'Convert local images to remote images',
|
|
callback: () => {
|
|
new ConvertLocalImagesForCurrentFile(this.app, this).open();
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'batch-convert-directory-images',
|
|
name: 'Batch convert directory images to remote',
|
|
callback: () => {
|
|
new BatchDirectoryConvertLocalToRemote(this.app, this).open();
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'search-magnific-images',
|
|
name: 'Search Magnific images',
|
|
callback: () => {
|
|
if (this.settings.magnific.enabled) {
|
|
new MagnificModal(this.app, this).open();
|
|
} else {
|
|
new Notice('Magnific integration is not enabled. Please enable it in settings.');
|
|
}
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'generate-images-ideogram',
|
|
name: 'Generate images (Ideogram)',
|
|
callback: () => {
|
|
if (this.settings.ideogram.enabled) {
|
|
new IdeogramModal(this.app, this).open();
|
|
} else {
|
|
new Notice('Ideogram integration is not enabled. Please enable it in settings.');
|
|
}
|
|
}
|
|
});
|
|
|
|
// ─── Drop / paste confirmation gate ────────────────────────
|
|
// Intercepts every image dropped or pasted into a markdown view and
|
|
// pops a destination-picker modal before anything reaches disk or
|
|
// the network. See src/handlers/DropGateHandlers.ts.
|
|
const destinations: DropGateDestination[] = [
|
|
new VaultDestination(this.app),
|
|
new ImageKitDestination(() => this.settings),
|
|
new ImgurDestination(() => this.settings),
|
|
];
|
|
this.dropGateHandlers = new DropGateHandlers(this, destinations);
|
|
|
|
this.registerEvent(this.app.workspace.on('editor-drop', this.dropGateHandlers.onDrop));
|
|
this.registerEvent(this.app.workspace.on('editor-paste', this.dropGateHandlers.onPaste));
|
|
this.registerEvent(
|
|
this.app.workspace.on('active-leaf-change', () => this.dropGateHandlers?.resetSession())
|
|
);
|
|
|
|
// Detect a conflicting third-party imgur plugin. Both plugins register
|
|
// editor-drop handlers and Obsidian fires every registered handler in
|
|
// turn — preventDefault() only blocks the browser default, not other
|
|
// plugins. So if obsidian-imgur-plugin is enabled, the user gets two
|
|
// modals: ours, then theirs. Warn once.
|
|
if (this.settings.dropGate.enabled) {
|
|
this.warnAboutConflictingImgurPlugin();
|
|
}
|
|
|
|
this.addCommand({
|
|
id: 'reset-drop-gate-session',
|
|
name: 'Drop gate: reset session-remembered destination',
|
|
callback: () => {
|
|
this.dropGateHandlers?.resetSession();
|
|
new Notice('Image gin: drop-gate session reset.');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* If obsidian-imgur-plugin is enabled, both plugins will fire on every
|
|
* image drop and the user gets two modals back-to-back. Surface a one-
|
|
* time, persistent Notice telling them to disable the other.
|
|
*/
|
|
private warnAboutConflictingImgurPlugin(): void {
|
|
const pluginsApi = (this.app as unknown as {
|
|
plugins?: { enabledPlugins?: Set<string>; manifests?: Record<string, unknown> };
|
|
}).plugins;
|
|
|
|
const enabled = pluginsApi?.enabledPlugins;
|
|
if (!enabled) return;
|
|
if (!enabled.has('obsidian-imgur-plugin')) return;
|
|
|
|
new Notice(
|
|
'Image Gin Drop Gate: the "Imgur" community plugin is also enabled. ' +
|
|
'Both plugins handle image drops, so you will see two modals. ' +
|
|
'Disable the Imgur community plugin in Settings → Community Plugins, ' +
|
|
'then enable Image Gin\'s Imgur destination if you want Imgur uploads from inside the gate.',
|
|
0
|
|
);
|
|
console.warn('[image-gin/drop-gate] obsidian-imgur-plugin is enabled — drop events will fire twice.');
|
|
}
|
|
} |