devon22_obsidian-mediaviewer/src/galleryBlock.ts
2026-07-19 12:45:48 +08:00

2384 lines
105 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { App, TFile, Menu, Notice, Platform, Modal, Setting } from 'obsidian';
import MediaViewPlugin from './main';
import { FullScreenModal } from './fullscreen';
import { MediaViewSettings } from './settings';
import { ImageUploadModal } from './imageUpload';
import { GalleryBlockGenerateModal } from './galleryBlockGenerate';
import { t } from './translations';
import { captureScrollRestore } from './scrollHelper';
// 重新命名對話框
class RenameModal extends Modal {
newName: string;
onConfirm: (newName: string) => void;
currentName: string;
constructor(app: App, currentName: string, onConfirm: (newName: string) => void) {
super(app);
this.currentName = currentName;
this.newName = currentName;
this.onConfirm = onConfirm;
}
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: t('rename') });
const inputSetting = new Setting(contentEl)
.setName(t('new_filename'))
.addText(text => {
text.setValue(this.currentName)
.onChange(value => this.newName = value);
text.inputEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
this.onConfirm(this.newName);
this.close();
}
});
});
inputSetting.settingEl.addClass('mv-settings-input-flex');
new Setting(contentEl)
.addButton(button => button
.setButtonText(t('confirm'))
.setCta()
.onClick(() => {
this.onConfirm(this.newName);
this.close();
}))
.addButton(button => button
.setButtonText(t('cancel') || 'Cancel')
.onClick(() => this.close()));
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
// 編輯 Alt 對話框
class AltModal extends Modal {
altText: string;
onConfirm: (altText: string) => void;
onDelete?: () => void;
currentAlt: string;
isEdit: boolean;
constructor(app: App, currentAlt: string, isEdit: boolean, onConfirm: (altText: string) => void, onDelete?: () => void) {
super(app);
this.currentAlt = currentAlt;
this.altText = currentAlt;
this.isEdit = isEdit;
this.onConfirm = onConfirm;
this.onDelete = onDelete;
}
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: this.isEdit ? t('edit_alt') || 'Edit alt' : t('add_alt') || 'Add alt' });
const inputSetting = new Setting(contentEl)
.setName('Alt')
.addText(text => {
text.setValue(this.currentAlt)
.onChange(value => this.altText = value);
text.inputEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
this.onConfirm(this.altText);
this.close();
}
});
});
inputSetting.settingEl.addClass('mv-settings-input-flex');
const buttonsSetting = new Setting(contentEl);
if (this.isEdit && this.onDelete) {
buttonsSetting.addButton(button => button
.setButtonText(t('delete') || 'Delete')
.setWarning()
.onClick(() => {
if (this.onDelete) {
this.onDelete();
}
this.close();
}));
}
buttonsSetting.addButton(button => button
.setButtonText(t('confirm'))
.setCta()
.onClick(() => {
this.onConfirm(this.altText);
this.close();
}));
buttonsSetting.addButton(button => button
.setButtonText(t('cancel') || 'Cancel')
.onClick(() => this.close()));
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
interface GalleryItem {
type: string;
url?: string;
path?: string;
altText?: string | null;
title?: {
type: string;
url?: string;
text: string;
} | string | null;
linkUrl?: string | null;
file?: TFile | null;
isInternalLink?: boolean;
isExternalLink?: boolean;
thumbnail?: string | null;
originalBlock?: string;
loc?: { start: number; end: number };
}
interface ContainerInfo {
title: {
type: string;
url?: string;
text: string;
} | string | null;
addButtonEnabled: boolean;
gridSize: string;
paginationEnabled: number;
waterfall?: boolean;
}
interface MediaUrlsData {
items: GalleryItem[];
headerLines: string[];
containerInfo: ContainerInfo;
galleryId: string;
sourcePath?: string;
isFiltered?: boolean;
}
const GALLERY_REORDER_MIME = 'application/x-mediaviewer-gallery-item';
interface GalleryReorderPayload {
galleryId: string;
index: number;
}
type DroppedTextItem = {
type: 'uri' | 'text';
getData: () => string;
};
type DroppedFileItem = File;
type DroppedItem = DroppedTextItem | DroppedFileItem;
export class GalleryBlock {
app: App;
plugin: MediaViewPlugin;
static paginationState: Map<string, number> = new Map();
constructor(app: App, plugin: MediaViewPlugin) {
this.app = app;
this.plugin = plugin;
}
// 處理 gallery 區塊
async processGalleryBlock(source: string, el: HTMLElement, sourcePath?: string): Promise<void> {
if (el.querySelector('.mvgb-media-gallery-grid')) {
return;
}
try {
const mediaUrlsData = await this.parseGalleryContent(source, sourcePath);
const galleryDiv = this.createGalleryElement(mediaUrlsData);
el.appendChild(galleryDiv);
} catch (error) {
console.error('Error processing gallery block:', error);
}
}
// 解析 gallery 內容
async parseGalleryContent(content: string, sourcePath?: string, skipFilter: boolean = false): Promise<MediaUrlsData> {
const items: GalleryItem[] = [];
const lines = content.split('\n');
const headerLines: string[] = [];
let pendingItemLines: string[] = [];
let currentTitle = null;
let currentAltText: string | null = null;
let currentLinkUrl = null;
let currentThumbnail = null;
let containerTitle = null;
let addButtonEnabled = false;
let gridSize = this.plugin.settings.galleryGridSize || 'medium';
let paginationEnabled = this.plugin.settings.itemsPerPage || 0; // 設定變數以決定是否開啟分頁功能
let filterKeyword: string | null = null; // 新增搜尋關鍵字
let waterfall = this.plugin.settings.galleryWaterfall || false;
// 產生基於內容的唯一識別碼
const galleryId = 'gallery-' + this.hashString(content.trim());
// 處理縮圖連結的輔助函數
const processMediaLink = (linkText: string) => {
// 處理 Obsidian 內部連結 ![[file]]
const internalMatch = linkText.match(/!\[\[(.*?)\]\]/);
if (internalMatch) {
const file = this.app.metadataCache.getFirstLinkpathDest(internalMatch[1], '');
if (file) {
return this.app.vault.getResourcePath(file);
}
}
// 處理標準 Markdown 連結 ![alt](path)
const markdownMatch = linkText.match(/!\[(.*?)\]\((.*?)\)/);
if (markdownMatch) {
const url = markdownMatch[2].split(' "')[0];
if (url.startsWith('http')) {
return url;
} else {
const file = this.app.metadataCache.getFirstLinkpathDest(url, '');
if (!file) {
const fileByPath = this.app.vault.getAbstractFileByPath(url);
if (fileByPath && fileByPath instanceof TFile) {
return this.app.vault.getResourcePath(fileByPath);
}
} else {
return this.app.vault.getResourcePath(file);
}
}
}
// 處理直接的 URL 或路徑 (僅img參數)
if (linkText.startsWith('http')) {
return linkText;
} else {
const file = this.app.vault.getAbstractFileByPath(linkText);
if (file instanceof TFile) {
return this.app.vault.getResourcePath(file);
}
}
return null;
};
// 新增:處理筆記內容中的第一張圖片
const findFirstImageInNote = async (file: TFile) => {
try {
const content = await this.app.vault.cachedRead(file);
const internalMatch = content.match(/(?:!?\[\[(.*?\.(?:jpg|jpeg|png|gif|webp))(?:\|.*?)?\]\]|!\[(.*?)\]\(\s*(\S+?(?:\.(?:jpg|jpeg|png|gif|webp)|format=(?:jpg|jpeg|png|gif|webp))[^\s)]*)\s*(?:\s+["'][^"']*["'])?\s*\))/i);
if (internalMatch) {
if (internalMatch[1]) {
return `![[${internalMatch[1]}]]`;
}
return internalMatch[0];
} else {
return null;
}
} catch (error) {
console.error('Error reading note content:', error);
return null;
}
};
let tempStartLine = -1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();
if (!trimmedLine) {
if (pendingItemLines.length > 0) {
pendingItemLines.push(line);
} else {
headerLines.push(line);
}
continue;
}
const paginationMatch = trimmedLine.match(/^pagination:\s*(\d+)$/i);
if (paginationMatch) {
headerLines.push(line);
const paginationValue = parseInt(paginationMatch[1]);
paginationEnabled = paginationValue || 0;
continue;
}
// 新增 filter 參數的處理
const filterMatch = trimmedLine.match(/^filter:\s*(.*?)\s*$/i);
if (filterMatch) {
headerLines.push(line);
const keyword = filterMatch[1].trim().toLowerCase();
if (keyword && keyword !== 'null' && keyword !== 'undefined') {
filterKeyword = keyword;
}
continue;
}
// 新增 size 參數的處理
const sizeMatch = trimmedLine.match(/^size:\s*(small|medium|large)$/i);
if (sizeMatch) {
headerLines.push(line);
gridSize = sizeMatch[1].toLowerCase();
continue;
}
// 檢查是否要禁用新增按鈕
const addButtonMatch = trimmedLine.match(/^addbutton:\s*(true|false)$/i);
if (addButtonMatch) {
headerLines.push(line);
addButtonEnabled = addButtonMatch[1].toLowerCase() === 'true';
continue;
}
// 檢查是否為容器標題設定
const containerTitleMatch = trimmedLine.match(/^title:\s*(.+)$/);
if (containerTitleMatch) {
headerLines.push(line);
const titleText = containerTitleMatch[1];
// 檢查是否為內部連結 [[note]]
const internalLinkMatch = titleText.match(/\[\[(.*?)(?:\|.*?)?\]\]/);
if (internalLinkMatch) {
const linktext = internalLinkMatch[1].split('|')[0].split('#')[0];
const file = this.app.metadataCache.getFirstLinkpathDest(linktext, '');
containerTitle = {
text: linktext,
url: file ? file.path : linktext,
type: 'internal'
};
}
// 檢查是否為外部連結 [text](url)
else {
const externalLinkMatch = titleText.match(/\[(.*?)\]\((.*?)\)/);
if (externalLinkMatch) {
containerTitle = {
text: externalLinkMatch[1],
url: externalLinkMatch[2],
type: 'external'
};
} else {
// 純文字
containerTitle = {
text: titleText,
type: 'text'
};
}
}
continue;
}
// 檢查是否為個別圖片的標題設定
const titleMatch = trimmedLine.match(/^alt:\s*(.+)$/);
if (titleMatch) {
if (pendingItemLines.length === 0) tempStartLine = i;
pendingItemLines.push(line);
const titleText = titleMatch[1];
currentAltText = titleText;
// 檢查是否為內部連結
const internalLinkMatch = titleText.match(/\[\[(.*?)(?:\|.*?)?\]\]/);
if (internalLinkMatch) {
const linktext = internalLinkMatch[1].split('|')[0].split('#')[0];
const file = this.app.metadataCache.getFirstLinkpathDest(linktext, '');
currentTitle = {
text: linktext,
url: file ? file.path : linktext,
type: 'internal'
};
}
// 檢查是否為外部連結
else {
const externalLinkMatch = titleText.match(/\[(.*?)\]\((.*?)\)/);
if (externalLinkMatch) {
currentTitle = {
text: externalLinkMatch[1],
url: externalLinkMatch[2],
type: 'external'
};
} else {
// 純文字
currentTitle = {
text: titleText,
type: 'text'
};
}
}
continue;
}
// 檢查是否為縮圖設定
const thumbnailMatch = trimmedLine.match(/^img:\s*(.+)$/);
if (thumbnailMatch) {
if (pendingItemLines.length === 0) tempStartLine = i;
pendingItemLines.push(line);
const imageUrl = thumbnailMatch[1].trim();
const isImageFile = /\.(jpg|jpeg|png|gif|webp|bmp|svg|tiff|tif)$/i.test(imageUrl) || /^https?:\/\/.*\.(jpg|jpeg|png|gif|webp|bmp|svg|tiff|tif)$/i.test(imageUrl);
if (isImageFile) {
currentThumbnail = processMediaLink(`![Image](${imageUrl})`);
} else {
currentThumbnail = processMediaLink(imageUrl);
}
continue;
}
// 處理筆記連結
const internalMatch = line.match(/(!?)\[\[(.*?)(?:\|.*?)?\]\]/);
if (internalMatch) {
if (pendingItemLines.length === 0) tempStartLine = i;
pendingItemLines.push(line);
const originalBlock = pendingItemLines.join('\n');
pendingItemLines = [];
const [, isImage, linktext] = internalMatch;
const actualLinktext = linktext.split('|')[0].split('#')[0];
const file = this.app.metadataCache.getFirstLinkpathDest(actualLinktext, '');
if (file && file.extension.toLowerCase() === 'zip') {
if (!currentThumbnail) {
currentThumbnail = await this.getZipThumbnail(file.path);
}
items.push({
type: 'note',
title: currentTitle || actualLinktext,
altText: currentAltText,
path: file.path,
linkUrl: currentLinkUrl,
file: file,
isInternalLink: true,
thumbnail: currentThumbnail,
originalBlock: originalBlock,
loc: { start: tempStartLine, end: i }
});
currentTitle = null;
currentAltText = null;
currentLinkUrl = null;
currentThumbnail = null;
continue;
}
if (isImage && !currentThumbnail) {
// 處理一般的圖片/媒體連結
if (file) {
const extension = file.extension.toLowerCase();
if (extension.match(/^(jpg|jpeg|png|gif|webp|mp4|mkv|mov|webm|mp3|m4a|flac|ogg|wav|3gp)$/)) {
items.push({
type: extension.match(/^(jpg|jpeg|png|gif|webp)$/) ? 'image' : 'video',
url: this.app.vault.getResourcePath(file),
path: file.path,
file: file,
altText: currentAltText,
title: currentTitle,
linkUrl: currentLinkUrl,
thumbnail: currentThumbnail,
originalBlock: originalBlock,
loc: { start: tempStartLine, end: i }
});
}
}
} else if (isImage && currentThumbnail) {
// 處理帶有 img 參數的影片連結
if (file) {
const extension = file.extension.toLowerCase();
if (extension.match(/^(mp4|mkv|mov|webm|mp3|m4a|flac|ogg|wav|3gp)$/)) {
items.push({
type: 'video',
url: this.app.vault.getResourcePath(file),
path: file.path,
file: file,
altText: currentAltText,
title: currentTitle,
linkUrl: currentLinkUrl,
thumbnail: currentThumbnail,
originalBlock: originalBlock,
loc: { start: tempStartLine, end: i }
});
}
}
} else {
// 處理筆記連結
if (file && !currentThumbnail) {
// 尋找筆記中的第一張圖片
const firstImage = await findFirstImageInNote(file);
if (firstImage) {
currentThumbnail = processMediaLink(firstImage);
}
}
items.push({
type: 'note',
title: currentTitle || actualLinktext,
altText: currentAltText,
path: file ? file.path : actualLinktext,
linkUrl: currentLinkUrl,
file: file,
isInternalLink: true,
thumbnail: currentThumbnail,
originalBlock: originalBlock,
loc: { start: tempStartLine, end: i }
});
}
currentTitle = null;
currentAltText = null;
currentLinkUrl = null;
currentThumbnail = null;
continue;
}
// 處理標準 Markdown 連結
const markdownMatch = line.match(/(!?)\[(.*?)(?:\|.*?)?\]\((.*?)\)/);
if (markdownMatch) {
if (pendingItemLines.length === 0) tempStartLine = i;
pendingItemLines.push(line);
const originalBlock = pendingItemLines.join('\n');
pendingItemLines = [];
const [, isImage, text, url] = markdownMatch;
const cleanUrl = url.split(' "')[0];
const youtubeRegex = /(?:youtube\.com\/(?:[^/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?/ ]{11})/;
const ytMatch = cleanUrl.match(youtubeRegex);
const ytId = ytMatch ? ytMatch[1] : null;
if (ytId && !Platform.isIosApp) {
if (!currentThumbnail) {
currentThumbnail = `https://img.youtube.com/vi/${ytId}/hqdefault.jpg`;
}
items.push({
type: 'youtube',
url: cleanUrl,
path: text || ytId,
altText: currentAltText,
title: currentTitle,
linkUrl: cleanUrl,
thumbnail: currentThumbnail,
originalBlock: originalBlock,
loc: { start: tempStartLine, end: i }
});
currentTitle = null;
currentAltText = null;
currentLinkUrl = null;
currentThumbnail = null;
continue;
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
const file = this.app.metadataCache.getFirstLinkpathDest(url, '');
const abstractFile = this.app.vault.getAbstractFileByPath(url);
const targetFile = file || (abstractFile instanceof TFile ? abstractFile : null);
if (targetFile && targetFile.extension.toLowerCase() === 'zip') {
if (!currentThumbnail) {
currentThumbnail = await this.getZipThumbnail(targetFile.path);
}
items.push({
type: 'note',
title: currentTitle || text || targetFile.basename,
altText: currentAltText,
path: targetFile.path,
linkUrl: currentLinkUrl,
file: targetFile,
isInternalLink: true,
thumbnail: currentThumbnail,
originalBlock: originalBlock,
loc: { start: tempStartLine, end: i }
});
currentTitle = null;
currentAltText = null;
currentLinkUrl = null;
currentThumbnail = null;
continue;
}
}
if (isImage && !currentThumbnail) {
// 處理一般的圖片連結
if (!url.startsWith('http://') && !url.startsWith('https://')) {
// 使用 getFirstLinkpathDest 來解析路徑
const file = this.app.metadataCache.getFirstLinkpathDest(url, '');
if (!file) {
// 如果找不到檔案,再嘗試直接用路徑查找
const fileByPath = this.app.vault.getAbstractFileByPath(url);
if (fileByPath && fileByPath instanceof TFile) {
const extension = fileByPath.extension.toLowerCase();
items.push({
type: extension.match(/\.(mp4|mkv|mov|webm|mp3|m4a|flac|ogg|wav|3gp)$/) ? 'video' : 'image',
url: this.app.vault.getResourcePath(fileByPath),
path: text || fileByPath.path,
file: fileByPath,
altText: currentAltText,
title: currentTitle,
linkUrl: currentLinkUrl,
thumbnail: currentThumbnail,
originalBlock: originalBlock,
loc: { start: tempStartLine, end: i }
});
}
} else {
const extension = file.extension.toLowerCase();
items.push({
type: extension.match(/^(mp4|mkv|mov|webm|mp3|m4a|flac|ogg|wav|3gp)$/) ? 'video' : 'image',
url: this.app.vault.getResourcePath(file),
path: text || file.path,
file: file,
altText: currentAltText,
title: currentTitle,
linkUrl: currentLinkUrl,
thumbnail: currentThumbnail,
originalBlock: originalBlock,
loc: { start: tempStartLine, end: i }
});
}
} else {
const urlForTypeCheck = url.split(' "')[0].split('?')[0].toLowerCase();
const isImageFile = urlForTypeCheck.match(/\.(mp4|mkv|mov|webm|mp3|m4a|flac|ogg|wav|3gp)$/);
items.push({
type: isImageFile ? 'video' : 'image',
url: url.split(' "')[0],
path: text,
altText: currentAltText,
title: currentTitle,
linkUrl: currentLinkUrl,
thumbnail: currentThumbnail,
originalBlock: originalBlock,
loc: { start: tempStartLine, end: i }
});
}
} else if (isImage && currentThumbnail) {
// 處理帶有 img 參數的影片連結
if (!url.startsWith('http://') && !url.startsWith('https://')) {
const file = this.app.metadataCache.getFirstLinkpathDest(url, '');
if (!file) {
const fileByPath = this.app.vault.getAbstractFileByPath(url);
if (fileByPath && fileByPath instanceof TFile) {
const extension = fileByPath.extension.toLowerCase();
if (extension.match(/\.(mp4|mkv|mov|webm|mp3|m4a|flac|ogg|wav|3gp)$/)) {
items.push({
type: 'video',
url: this.app.vault.getResourcePath(fileByPath),
path: text || fileByPath.path,
file: fileByPath,
altText: currentAltText,
title: currentTitle,
linkUrl: currentLinkUrl,
thumbnail: currentThumbnail,
originalBlock: originalBlock,
loc: { start: tempStartLine, end: i }
});
}
}
} else {
const extension = file.extension.toLowerCase();
if (extension.match(/^(mp4|mkv|mov|webm|mp3|m4a|flac|ogg|wav|3gp)$/)) {
items.push({
type: 'video',
url: this.app.vault.getResourcePath(file),
path: text || file.path,
file: file,
altText: currentAltText,
title: currentTitle,
linkUrl: currentLinkUrl,
thumbnail: currentThumbnail,
originalBlock: originalBlock,
loc: { start: tempStartLine, end: i }
});
}
}
} else {
const urlForTypeCheck = url.split(' "')[0].split('?')[0].toLowerCase();
const isVideoFile = urlForTypeCheck.match(/\.(mp4|mkv|mov|webm|mp3|m4a|flac|ogg|wav|3gp)$/);
if (isVideoFile) {
items.push({
type: 'video',
url: url.split(' "')[0],
path: text,
altText: currentAltText,
title: currentTitle,
linkUrl: currentLinkUrl,
thumbnail: currentThumbnail,
originalBlock: originalBlock,
loc: { start: tempStartLine, end: i }
});
}
}
} else {
// 處理一般連結
items.push({
type: 'note',
title: currentTitle || text,
altText: currentAltText,
path: url,
linkUrl: url,
isExternalLink: url.startsWith('http://') || url.startsWith('https://'),
thumbnail: currentThumbnail,
originalBlock: originalBlock,
loc: { start: tempStartLine, end: i }
});
}
currentTitle = null;
currentAltText = null;
currentLinkUrl = null;
currentThumbnail = null;
continue;
}
if (pendingItemLines.length > 0) {
pendingItemLines.push(line);
} else {
headerLines.push(line);
}
}
const shouldFilter = !!filterKeyword && filterKeyword.trim() !== '' && !skipFilter;
const finalItems = shouldFilter ? items.filter(item => {
const titleText = this.getGalleryTitleText(item);
const textToFilter = (titleText || '') + (item.path || '') + (item.url || '');
return textToFilter.toLowerCase().includes(filterKeyword!);
}) : items;
return {
items: finalItems,
headerLines: headerLines,
containerInfo: {
title: containerTitle,
addButtonEnabled: addButtonEnabled,
gridSize: gridSize,
paginationEnabled: paginationEnabled,
waterfall: waterfall
},
galleryId: galleryId,
sourcePath: sourcePath,
isFiltered: shouldFilter
};
}
hashString(str: string) {
const normalized = str.replace(/\r\n/g, '\n');
let hash = 0;
for (let i = 0; i < normalized.length; i++) {
const char = normalized.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash).toString(36);
}
async getZipThumbnail(zipPath: string): Promise<string | null> {
interface ObsidianAppWithPlugins {
plugins?: {
plugins?: Record<string, {
getThumbnail?: (path: string) => Promise<string | null>;
}>;
};
}
const appWithPlugins = this.app as ObsidianAppWithPlugins;
const zipPlugin = appWithPlugins.plugins?.plugins?.['gridexplorer'];
if (!zipPlugin || typeof zipPlugin.getThumbnail !== 'function') {
return null;
}
return await zipPlugin.getThumbnail(zipPath);
}
private isGalleryReorderDrag(e: DragEvent): boolean {
return Array.from(e.dataTransfer?.types || []).includes(GALLERY_REORDER_MIME);
}
private getCurrentGalleryPage(galleryDiv: Element | null): number {
const paginationDiv = galleryDiv?.parentElement?.querySelector('.mvgb-pagination');
if (paginationDiv?.instanceOf(HTMLElement) && paginationDiv.dataset.currentPage) {
return parseInt(paginationDiv.dataset.currentPage);
}
return 1;
}
private getGalleryTitleText(item: GalleryItem): string {
if (typeof item.title === 'string') {
return item.title;
}
return item.title?.text ?? '';
}
private createDragHandleIcon(): SVGSVGElement {
const svg = activeDocument.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
const path = activeDocument.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('fill', 'currentColor');
path.setAttribute('d', 'M13,6.83V11H17.17L15.59,9.41L17,8L21,12L17,16L15.59,14.59L17.17,13H13V17.17L14.59,15.59L16,17L12,21L8,17L9.41,15.59L11,17.17V13H6.83L8.41,14.59L7,16L3,12L7,8L8.41,9.41L6.83,11H11V6.83L9.41,8.41L8,7L12,3L16,7L14.59,8.41L13,6.83Z');
svg.appendChild(path);
return svg;
}
private clearReorderClasses(galleryDiv: HTMLElement) {
galleryDiv.querySelectorAll('.mvgb-reorder-before, .mvgb-reorder-after, .mvgb-reorder-dragging').forEach((el) => {
el.classList.remove('mvgb-reorder-before', 'mvgb-reorder-after', 'mvgb-reorder-dragging');
});
}
private getReorderTarget(e: DragEvent, galleryDiv: HTMLElement) {
const targetEl = (e.target as HTMLElement | null)?.closest('.mv-media-thumbnail-container') as HTMLElement | null;
if (!targetEl || !galleryDiv.contains(targetEl) || targetEl.classList.contains('placeholder') || targetEl.classList.contains('mvgb-add-media-button')) {
return null;
}
const indexAttr = targetEl.dataset.galleryIndex;
if (!indexAttr) {
return null;
}
const targetIndex = parseInt(indexAttr);
if (Number.isNaN(targetIndex)) {
return null;
}
const rect = targetEl.getBoundingClientRect();
return {
el: targetEl,
index: targetIndex,
position: e.clientX < rect.left + rect.width / 2 ? 'before' as const : 'after' as const
};
}
createGalleryElement(mediaUrlsData: MediaUrlsData) {
const { items, containerInfo, galleryId, sourcePath } = mediaUrlsData;
const titleDiv = activeDocument.createElement('div');
const galleryDiv = activeDocument.createElement('div');
galleryDiv.className = 'mvgb-media-gallery-grid';
// 使用從 mediaUrlsData 中取得的 galleryId
galleryDiv.setAttribute('data-gallery-id', galleryId);
// 根據 size 參數添加對應的 class 並設定寬度
galleryDiv.addClass(`size-${containerInfo.gridSize}`);
if (containerInfo.waterfall) {
galleryDiv.addClass('waterfall');
}
const propertyName = `galleryGridSize${containerInfo.gridSize.charAt(0).toUpperCase() + containerInfo.gridSize.slice(1)}` as keyof MediaViewSettings;
const width = this.plugin.settings[propertyName];
galleryDiv.setCssProps({
'--mv-gallery-thumbnail-width': `${width}px`
});
// 處理容器標題
if (typeof containerInfo.title === 'object' && containerInfo.title !== null) {
const containerLinkArea = titleDiv.createEl('div', {
cls: 'mvgb-container-link-area'
});
if (containerInfo.title.type === 'text') {
// 純文字
containerLinkArea.createEl('span', {
text: containerInfo.title.text
});
} else {
// 內部或外部連結
const link = containerLinkArea.createEl('a', {
text: containerInfo.title.text
});
if (containerInfo.title.url) {
if (containerInfo.title.type === 'internal') {
// 內部連結
const file = this.app.vault.getAbstractFileByPath(containerInfo.title.url);
if (file instanceof TFile) {
link.onclick = (e) => {
e.preventDefault();
const isInPreview = link.closest('.ge-note-view-container') !== null;
const isModKey = e.ctrlKey || e.metaKey;
if (isInPreview && !isModKey) {
this.app.workspace.trigger('grid-explorer:preview-file', file.path);
} else {
const leaf = this.app.workspace.getLeaf('tab');
void leaf.openFile(file);
}
};
}
} else {
// 外部連結
link.href = encodeURI(containerInfo.title.url);
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.onclick = (e) => {
e.stopPropagation();
};
}
}
}
galleryDiv.addClass('has-container-title');
}
const container = activeDocument.createElement('div');
container.className = 'mvgb-media-container';
container.appendChild(titleDiv);
container.appendChild(galleryDiv);
// 處理分頁和新增按鈕
const itemsPerPage = containerInfo.paginationEnabled || this.plugin.settings.itemsPerPage || 0;
// 當 itemsPerPage 為 0 或項目數量不足時,顯示所有項目
if (itemsPerPage <= 0 || items.length <= itemsPerPage) {
// 顯示所有項目
const itemElements: HTMLElement[] = [];
items.forEach((item: GalleryItem, index: number) => {
if (item.type === 'note') {
const noteContainer = this.createNoteContainer(item, index, items, galleryId, sourcePath, mediaUrlsData.isFiltered);
itemElements.push(noteContainer);
} else {
const mediaContainer = this.createMediaContainer(item, index, items, galleryId, sourcePath, mediaUrlsData.isFiltered);
itemElements.push(mediaContainer);
}
});
// 不使用分頁時,如果允許新增圖片,則顯示傳統的新增按鈕
if (containerInfo.addButtonEnabled || items.length === 0) {
const addContainer = activeDocument.createElement('div');
addContainer.className = 'mv-media-thumbnail-container mvgb-add-media-button';
const addIcon = addContainer.createDiv('mvgb-add-media-icon');
const svg = activeDocument.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '24');
svg.setAttribute('height', '24');
const path = activeDocument.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('fill', 'currentColor');
path.setAttribute('d', 'M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z');
svg.appendChild(path);
addIcon.appendChild(svg);
const addIconText = activeDocument.createElement('div');
addIconText.className = 'mvgb-add-media-text';
addIconText.textContent = t('add_item');
addIcon.appendChild(addIconText);
addContainer.onclick = () => {
const modal = new ImageUploadModal(this.app, this.plugin, galleryDiv, sourcePath);
modal.open();
};
itemElements.push(addContainer);
}
this.renderGalleryItems(galleryDiv, itemElements);
} else {
// 使用分頁顯示
const totalPages = Math.ceil(items.length / itemsPerPage);
let currentPage = 1;
// 檢查是否有儲存此 Gallery 的狀態
if (GalleryBlock.paginationState.has(galleryId)) {
const savedPage = GalleryBlock.paginationState.get(galleryId);
if (savedPage) {
currentPage = Math.min(savedPage, totalPages);
}
GalleryBlock.paginationState.delete(galleryId);
}
const controlsDiv = activeDocument.createElement('div');
controlsDiv.className = 'mvgb-gallery-controls';
const paginationDiv = activeDocument.createElement('div');
paginationDiv.className = 'mvgb-pagination';
const prevPageButton = paginationDiv.createEl('button', {
cls: 'mvgb-gallery-control-button prev-page-button',
text: t('prev_page')
});
const pageInfoSpan = paginationDiv.createEl('span', {
cls: 'mvgb-page-info',
text: `${currentPage} / ${totalPages}`
});
const nextPageButton = paginationDiv.createEl('button', {
cls: 'mvgb-gallery-control-button next-page-button',
text: t('next_page')
});
// 儲存當前頁碼到按鈕上,以便更新時使用
paginationDiv.dataset.currentPage = currentPage.toString();
prevPageButton.onclick = () => {
const currentPage = parseInt(paginationDiv.dataset.currentPage || '0');
// 如果是第一頁,則跳到最後一頁,否則往前一頁
if (currentPage !== 0) {
const newPage = currentPage === 1 ? totalPages : currentPage - 1;
paginationDiv.dataset.currentPage = newPage.toString();
this.updateGalleryPage(galleryDiv, items, newPage, itemsPerPage, sourcePath);
}
};
nextPageButton.onclick = () => {
const currentPage = parseInt(paginationDiv.dataset.currentPage || '0');
// 如果是最後一頁,則跳到第一頁,否則往後一頁
if (currentPage !== 0) {
const newPage = currentPage === totalPages ? 1 : currentPage + 1;
paginationDiv.dataset.currentPage = newPage.toString();
this.updateGalleryPage(galleryDiv, items, newPage, itemsPerPage, sourcePath);
}
};
// 新增圖片按鈕移到分頁控制項旁邊
if (containerInfo.addButtonEnabled || items.length === 0) {
const addButton = paginationDiv.createEl('button', {
cls: 'mvgb-gallery-control-button mvgb-add-image-button',
text: t('add_item')
});
addButton.onclick = () => {
const modal = new ImageUploadModal(this.app, this.plugin, galleryDiv, sourcePath);
modal.open();
};
}
paginationDiv.appendChild(prevPageButton);
paginationDiv.appendChild(pageInfoSpan);
paginationDiv.appendChild(nextPageButton);
controlsDiv.appendChild(paginationDiv);
container.appendChild(controlsDiv);
// 初始化第一頁
this.updateGalleryPage(galleryDiv, items, currentPage, itemsPerPage, sourcePath, mediaUrlsData.isFiltered);
}
// 加入拖曳事件處理
galleryDiv.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
if (this.isGalleryReorderDrag(e)) {
const dragEvent = e;
if (dragEvent.dataTransfer) {
dragEvent.dataTransfer.dropEffect = 'move';
}
this.clearReorderClasses(galleryDiv);
const reorderTarget = this.getReorderTarget(dragEvent, galleryDiv);
if (reorderTarget) {
reorderTarget.el.classList.add(reorderTarget.position === 'before' ? 'mvgb-reorder-before' : 'mvgb-reorder-after');
}
return;
}
galleryDiv.addClass('drag-over');
// 根據滑鼠位置決定左右區域並加上對應樣式
const rect = galleryDiv.getBoundingClientRect();
const midX = rect.left + rect.width / 2;
const clientX = e.clientX;
if (clientX < midX) {
galleryDiv.addClass('drag-left');
galleryDiv.removeClass('drag-right');
} else {
galleryDiv.addClass('drag-right');
galleryDiv.removeClass('drag-left');
}
});
galleryDiv.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
galleryDiv.removeClass('drag-over');
galleryDiv.removeClass('drag-left');
galleryDiv.removeClass('drag-right');
this.clearReorderClasses(galleryDiv);
});
galleryDiv.addEventListener('drop', (e) => {
void (async () => {
e.preventDefault();
e.stopPropagation();
galleryDiv.removeClass('drag-over');
galleryDiv.removeClass('drag-left');
galleryDiv.removeClass('drag-right');
const dragEvent = e;
if (this.isGalleryReorderDrag(dragEvent)) {
try {
const payload = JSON.parse(dragEvent.dataTransfer?.getData(GALLERY_REORDER_MIME) || '') as GalleryReorderPayload;
const reorderTarget = this.getReorderTarget(dragEvent, galleryDiv);
this.clearReorderClasses(galleryDiv);
if (!reorderTarget || payload.galleryId !== galleryId || payload.index === reorderTarget.index) {
return;
}
const draggedItem = items[payload.index];
const targetItem = items[reorderTarget.index];
if (!draggedItem || !targetItem) {
return;
}
await this.moveGalleryItem(galleryId, draggedItem, targetItem, reorderTarget.position, sourcePath, this.getCurrentGalleryPage(galleryDiv));
} catch (error) {
console.error('Error reordering gallery item:', error);
}
return;
}
// 計算是否落在右側(右側代表加到最後,左側代表加到最前)
const rect = galleryDiv.getBoundingClientRect();
const midX = rect.left + rect.width / 2;
const clientX = e.clientX;
const insertAtEnd = clientX >= midX;
const files: Array<Promise<DroppedItem>> = [];
// 處理所有拖放的項目
for (const item of Array.from(e.dataTransfer?.items ?? [])) {
if (item.kind === 'string') {
if (item.type === 'text/uri-list') {
// 處理 URI 列表
const uriPromise = new Promise<DroppedTextItem>(resolve => {
item.getAsString((string: string) => {
resolve({ type: 'uri', getData: () => string });
});
});
files.push(uriPromise);
} else if (item.type === 'text/plain') {
const textPromise = new Promise<DroppedTextItem>(resolve => {
item.getAsString((string: string) => {
resolve({ type: 'text', getData: () => string });
});
});
files.push(textPromise);
}
} else if (item.kind === 'file') {
// 處理一般檔案
const file = item.getAsFile();
if (file) {
files.push(Promise.resolve(file));
}
}
}
const resolvedFiles = await Promise.all(files);
const modal = new ImageUploadModal(this.app, this.plugin, galleryDiv, sourcePath);
// 依左右區域設定插入位置
modal.insertAtEnd = insertAtEnd;
await modal.handleFiles(resolvedFiles);
})();
});
// 右鍵選單
galleryDiv.addEventListener('contextmenu', (event) => {
if (event.target !== galleryDiv) {
return;
}
event.preventDefault();
const menu = new Menu();
this.addGalleryBlockMenuItems(menu, galleryDiv, galleryId, sourcePath);
menu.showAtMouseEvent(event);
});
return container;
}
createNoteContainer(item: GalleryItem, index: number, allItems: GalleryItem[], galleryId: string, sourcePath?: string, isFiltered?: boolean) {
const container = activeDocument.createElement('div');
container.className = 'mv-media-thumbnail-container mvgb-note-thumbnail';
container.dataset.galleryId = galleryId;
container.dataset.galleryIndex = index.toString();
const notePreview = activeDocument.createElement('div');
notePreview.className = 'mvgb-note-preview';
// 如果有縮圖,使用縮圖
if (item.thumbnail) {
const img = activeDocument.createElement('img');
img.src = item.thumbnail;
img.className = 'mvgb-note-thumbnail-image';
img.toggleClass('is-pointer-disabled', Platform.isMobile);
notePreview.appendChild(img);
} else {
// 否則使用預設圖示
const noteIcon = activeDocument.createElement('div');
noteIcon.className = 'mvgb-note-icon';
if (item.isExternalLink) {
const svg = activeDocument.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '24');
svg.setAttribute('height', '24');
const path = activeDocument.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('fill', 'currentColor');
path.setAttribute('d', 'M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z');
svg.appendChild(path);
noteIcon.appendChild(svg);
noteIcon.classList.add('external-link');
} else {
const svg = activeDocument.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '24');
svg.setAttribute('height', '24');
const path = activeDocument.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('fill', 'currentColor');
path.setAttribute('d', 'M14,17H7V15H14V17M17,13H7V11H17V13M17,9H7V7H17V9M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z');
svg.appendChild(path);
noteIcon.appendChild(svg);
noteIcon.classList.add('internal-link');
}
notePreview.appendChild(noteIcon);
}
const noteTitle = createEl('div', { cls: 'mvgb-note-title', text: this.getGalleryTitleText(item) });
if (item.isInternalLink && !item.file) {
noteTitle.addClass('mvgb-invalid-link');
}
notePreview.appendChild(noteTitle);
container.appendChild(notePreview);
container.onclick = (event) => {
event.stopPropagation();
if (item.isExternalLink) {
// 外部連結:在新分頁開啟
if (item.linkUrl) {
window.open(item.linkUrl, '_blank', 'noopener,noreferrer');
}
} else if (item.isInternalLink && item.file) {
// 內部連結:在新分頁開啟筆記
const isInPreview = container.closest('.ge-note-view-container') !== null;
const isModKey = event.ctrlKey || event.metaKey;
if (isInPreview && !isModKey) {
this.app.workspace.trigger('grid-explorer:preview-file', item.file.path);
} else {
const leaf = this.app.workspace.getLeaf('tab');
void leaf.openFile(item.file);
}
} else {
// 其他連結:嘗試在新分頁開啟
window.open(item.path, '_blank', 'noopener,noreferrer');
}
};
// 新增懸停控制項
if (!isFiltered && !Platform.isMobile) {
const controls = this.createHoverControls(item, index, allItems, galleryId, sourcePath);
container.appendChild(controls);
}
// 右鍵選單
container.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
const menu = new Menu();
if (index > 0) {
menu.addItem((menuItem) => {
menuItem
.setTitle(t('move_left') || 'Move Left')
.setIcon('arrow-left')
.onClick(async () => {
// 從 DOM 獲取當前頁碼
const galleryDiv = container.closest('.mvgb-media-gallery-grid');
let currentPage = 1;
if (galleryDiv) {
const paginationDiv = galleryDiv.parentElement?.querySelector('.mvgb-pagination');
if (paginationDiv?.instanceOf(HTMLElement) && paginationDiv.dataset.currentPage) {
currentPage = parseInt(paginationDiv.dataset.currentPage);
}
}
await this.swapGalleryItems(galleryId, item, allItems[index - 1], sourcePath, currentPage);
});
});
}
if (index < allItems.length - 1) {
menu.addItem((menuItem) => {
menuItem
.setTitle(t('move_right') || 'Move Right')
.setIcon('arrow-right')
.onClick(async () => {
// 從 DOM 獲取當前頁碼
const galleryDiv = container.closest('.mvgb-media-gallery-grid');
let currentPage = 1;
if (galleryDiv) {
const paginationDiv = galleryDiv.parentElement?.querySelector('.mvgb-pagination');
if (paginationDiv?.instanceOf(HTMLElement) && paginationDiv.dataset.currentPage) {
currentPage = parseInt(paginationDiv.dataset.currentPage);
}
}
await this.swapGalleryItems(galleryId, item, allItems[index + 1], sourcePath, currentPage);
});
});
}
this.addCopyAltMenuItem(menu, item);
this.addEditAltMenuItem(menu, item, galleryId, sourcePath, container);
if (item.file) {
menu.addItem((menuItem) => {
menuItem
.setTitle(t('rename'))
.setIcon("pencil")
.onClick(() => {
const galleryDiv = container.closest('.mvgb-media-gallery-grid');
let currentPage = 1;
if (galleryDiv) {
const paginationDiv = galleryDiv.parentElement?.querySelector('.mvgb-pagination');
if (paginationDiv?.instanceOf(HTMLElement) && paginationDiv.dataset.currentPage) {
currentPage = parseInt(paginationDiv.dataset.currentPage);
}
}
void this.renameGalleryItem(galleryId, item, sourcePath, currentPage);
});
});
}
menu.addItem((menuItem) => {
menuItem
.setTitle(t('delete'))
.setIcon("trash")
.onClick(() => {
// 從 DOM 獲取當前頁碼
const galleryDiv = container.closest('.mvgb-media-gallery-grid');
let currentPage = 1;
if (galleryDiv) {
const paginationDiv = galleryDiv.parentElement?.querySelector('.mvgb-pagination');
if (paginationDiv?.instanceOf(HTMLElement) && paginationDiv.dataset.currentPage) {
currentPage = parseInt(paginationDiv.dataset.currentPage);
}
}
void this.deleteGalleryItem(galleryId, item, sourcePath, currentPage);
});
});
const galleryDiv = container.closest('.mvgb-media-gallery-grid');
if (galleryDiv instanceof HTMLElement) {
menu.addSeparator();
this.addGalleryBlockMenuItems(menu, galleryDiv, galleryId, sourcePath);
}
menu.showAtMouseEvent(e);
});
return container;
}
renderGalleryItems(galleryDiv: HTMLElement, itemElements: HTMLElement[]) {
if (galleryDiv.classList.contains('waterfall')) {
// 清理舊的 observer
const observedDiv = galleryDiv as HTMLElement & { _resizeObserver?: ResizeObserver };
if (observedDiv._resizeObserver) {
observedDiv._resizeObserver.disconnect();
}
const styleVal = galleryDiv.style.getPropertyValue('--mv-gallery-thumbnail-width');
const colWidth = parseInt(styleVal) || 150;
const gap = parseFloat(getComputedStyle(galleryDiv).columnGap) || 15;
let currentCols = 0;
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = entry.contentRect.width;
if (width <= 0) continue;
const cols = Math.max(1, Math.floor(width / (colWidth + gap)));
if (cols !== currentCols) {
currentCols = cols;
galleryDiv.replaceChildren();
const colDivs: HTMLDivElement[] = [];
for (let c = 0; c < cols; c++) {
const colDiv = activeDocument.createElement('div');
colDiv.className = 'mvgb-waterfall-column';
galleryDiv.appendChild(colDiv);
colDivs.push(colDiv);
}
itemElements.forEach((el, idx) => {
colDivs[idx % cols].appendChild(el);
});
}
}
});
resizeObserver.observe(galleryDiv);
observedDiv._resizeObserver = resizeObserver;
} else {
galleryDiv.replaceChildren();
itemElements.forEach(el => galleryDiv.appendChild(el));
}
}
updateGalleryPage(galleryDiv: HTMLElement, items: GalleryItem[], page: number, itemsPerPage: number, sourcePath?: string, isFiltered?: boolean) {
const start = (page - 1) * itemsPerPage;
const end = start + itemsPerPage;
const currentPageItems = items.slice(start, end);
const totalPages = Math.ceil(items.length / itemsPerPage);
if (!galleryDiv) return;
// 移除舊的項目
galleryDiv.replaceChildren();
const galleryId = galleryDiv.getAttribute('data-gallery-id') || '';
// 新增新的項目
const itemElements: HTMLElement[] = [];
currentPageItems.forEach((item: GalleryItem, index: number) => {
const absoluteIndex = start + index;
if (item.type === 'note') {
const noteContainer = this.createNoteContainer(item, absoluteIndex, items, galleryId, sourcePath, isFiltered);
itemElements.push(noteContainer);
} else {
const mediaContainer = this.createMediaContainer(item, absoluteIndex, items, galleryId, sourcePath, isFiltered);
itemElements.push(mediaContainer);
}
});
// 如果是最後一頁且項目數量不足,添加空的佔位元素
if (page === totalPages && currentPageItems.length < itemsPerPage) {
const placeholdersNeeded = itemsPerPage - currentPageItems.length;
for (let i = 0; i < placeholdersNeeded; i++) {
const placeholder = activeDocument.createElement('div');
placeholder.className = 'mv-media-thumbnail-container placeholder';
placeholder.addClass('is-placeholder-hidden'); // 隱藏但保持佔位
itemElements.push(placeholder);
}
}
this.renderGalleryItems(galleryDiv, itemElements);
// 更新分頁控制項
if (galleryDiv.parentElement) {
const paginationDiv = galleryDiv.parentElement.querySelector('.mvgb-pagination');
if (paginationDiv) {
const pageInfoSpan = paginationDiv.querySelector('.mvgb-page-info');
if (pageInfoSpan) {
pageInfoSpan.textContent = `${page} / ${totalPages}`;
}
}
}
}
createMediaContainer(media: GalleryItem, index: number, allItems: GalleryItem[], galleryId: string, sourcePath?: string, isFiltered?: boolean) {
const container = activeDocument.createElement('div');
container.className = 'mv-media-thumbnail-container';
container.dataset.galleryId = galleryId;
container.dataset.galleryIndex = index.toString();
if (media.type === 'image') {
const img = activeDocument.createElement('img');
if (media.url) {
img.src = media.url;
img.alt = media.path || '';
img.toggleClass('is-pointer-disabled', Platform.isMobile);
container.appendChild(img);
}
} else if (media.type === 'youtube') {
if (media.thumbnail) {
const img = activeDocument.createElement('img');
img.src = media.thumbnail;
img.alt = media.path || '';
img.className = 'mvgb-video-thumbnail';
img.toggleClass('is-pointer-disabled', Platform.isMobile);
container.appendChild(img);
}
const videoIcon = activeDocument.createElement('div');
videoIcon.className = 'mv-video-indicator';
const svg = activeDocument.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '24');
svg.setAttribute('height', '24');
const path = activeDocument.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M8 5v14l11-7z');
svg.appendChild(path);
videoIcon.appendChild(svg);
container.appendChild(videoIcon);
} else {
if (media.path && media.url) {
// 處理影片檔案
if (media.path.toLowerCase().match(/\.(mp4|mkv|mov|webm)$/)) {
// 如果有自訂縮圖,使用縮圖顯示
if (media.thumbnail) {
const img = activeDocument.createElement('img');
img.src = media.thumbnail;
img.alt = media.path || '';
img.className = 'mvgb-video-thumbnail';
img.toggleClass('is-pointer-disabled', Platform.isMobile);
container.appendChild(img);
} else {
const video = activeDocument.createElement('video');
if (!Platform.isAndroidApp) {
video.src = media.url;
}
video.addClass('is-pointer-disabled');
container.appendChild(video);
}
if (!Platform.isAndroidApp || media.thumbnail) {
const videoIcon = activeDocument.createElement('div');
videoIcon.className = 'mv-video-indicator';
const svg = activeDocument.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '24');
svg.setAttribute('height', '24');
const path = activeDocument.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M8 5v14l11-7z');
svg.appendChild(path);
videoIcon.appendChild(svg);
container.appendChild(videoIcon);
}
} else {
// 處理音樂檔案
container.addClass('mvgb-audio-thumbnail');
// 如果有自訂縮圖,使用縮圖顯示
if (media.thumbnail) {
const img = activeDocument.createElement('img');
img.src = media.thumbnail;
img.alt = media.path || '';
img.className = 'mvgb-video-thumbnail';
img.toggleClass('is-pointer-disabled', Platform.isMobile);
container.appendChild(img);
} else {
const audio = activeDocument.createElement('audio');
audio.src = media.url;
audio.addClass('is-pointer-disabled');
container.appendChild(audio);
}
if (!Platform.isAndroidApp || media.thumbnail) {
const audioIcon = activeDocument.createElement('div');
audioIcon.className = 'mv-audio-indicator';
const svg = activeDocument.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '24');
svg.setAttribute('height', '24');
const path = activeDocument.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('fill', 'currentColor');
path.setAttribute('d', 'M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z');
svg.appendChild(path);
audioIcon.appendChild(svg);
container.appendChild(audioIcon);
}
// 添加檔案名稱顯示
const filenameDiv = activeDocument.createElement('div');
filenameDiv.className = 'mv-audio-filename';
let filename = '';
if (media.path) {
const pathParts = media.path.split('/');
filename = pathParts[pathParts.length - 1];
filename = filename.replace(/\.[^/.]+$/, '');
}
filenameDiv.textContent = filename;
container.appendChild(filenameDiv);
}
}
}
if (typeof media.title === 'object' && media.title !== null) {
const linkArea = activeDocument.createElement('div');
linkArea.className = 'mvgb-media-link-area';
if (media.title.type === 'text') {
// 純文字
const textSpan = activeDocument.createElement('span');
textSpan.textContent = media.title.text;
textSpan.addClass('mvgb-media-link-text');
linkArea.appendChild(textSpan);
} else {
// 內部或外部連結
const link = activeDocument.createElement('a');
link.textContent = media.title.text;
if (media.title.url) {
if (media.title.type === 'internal') {
// 內部連結
const file = this.app.vault.getAbstractFileByPath(media.title.url);
if (file instanceof TFile) {
link.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const isInPreview = link.closest('.ge-note-view-container') !== null;
const isModKey = e.ctrlKey || e.metaKey;
if (isInPreview && !isModKey) {
this.app.workspace.trigger('grid-explorer:preview-file', file.path);
} else {
const leaf = this.app.workspace.getLeaf('tab');
void leaf.openFile(file);
}
};
}
} else {
// 外部連結
link.href = encodeURI(media.title.url);
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.onclick = (e) => {
e.stopPropagation();
};
}
}
linkArea.appendChild(link);
}
container.appendChild(linkArea);
}
// 使媒體縮圖可拖曳為 URI包含影片/圖片/音訊)。在行動裝置上略過。
const mediaUrl = media.url;
if (!Platform.isMobile && mediaUrl && media.type === 'video') {
container.draggable = true;
container.addEventListener('dragstart', (e: DragEvent) => {
if (!e.dataTransfer) return;
// 避免父層或 Obsidian 其他處理器攔截
e.stopPropagation();
try {
// 針對內部檔案輸出 Obsidian URI外部連結則維持原本 URL
const isHttp = /^https?:\/\//i.test(mediaUrl);
let outUri = mediaUrl;
if (!isHttp && media.path) {
const vaultName = this.app.vault.getName();
outUri = `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodeURIComponent(media.path)}`;
}
// 主要提供 URI並以純文字作為備援
e.dataTransfer.setData('text/uri-list', outUri);
e.dataTransfer.setData('text/plain', outUri);
e.dataTransfer.effectAllowed = 'copy';
} catch {
// 忽略 setData 可能的例外
}
});
}
if ((!this.plugin.settings.disableClickToOpenMediaOnGallery && media.type === 'image') || media.type === 'video' || media.type === 'youtube') {
container.onclick = () => {
const modal = new FullScreenModal(this.app, this.plugin, 'thumbnail', sourcePath);
modal.open();
window.setTimeout(() => {
const allUrls = modal.mediaUrls;
const targetIndex = allUrls.findIndex(m => (media.path && m.path === media.path) || m.url === media.url);
if (targetIndex !== -1) {
void modal.showMedia(targetIndex);
}
}, 100);
};
}
// 新增懸停控制項
if (!isFiltered && !Platform.isMobile) {
const controls = this.createHoverControls(media, index, allItems, galleryId, sourcePath);
container.appendChild(controls);
}
// 右鍵選單
container.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
const menu = new Menu();
if (index > 0) {
menu.addItem((menuItem) => {
menuItem
.setTitle(t('move_left') || 'Move Left')
.setIcon('arrow-left')
.onClick(async () => {
const galleryDiv = container.closest('.mvgb-media-gallery-grid');
let currentPage = 1;
if (galleryDiv) {
const paginationDiv = galleryDiv.parentElement?.querySelector('.mvgb-pagination');
if (paginationDiv?.instanceOf(HTMLElement) && paginationDiv.dataset.currentPage) {
currentPage = parseInt(paginationDiv.dataset.currentPage);
}
}
await this.swapGalleryItems(galleryId, media, allItems[index - 1], sourcePath, currentPage);
});
});
}
if (index < allItems.length - 1) {
menu.addItem((menuItem) => {
menuItem
.setTitle(t('move_right') || 'Move Right')
.setIcon('arrow-right')
.onClick(async () => {
const galleryDiv = container.closest('.mvgb-media-gallery-grid');
let currentPage = 1;
if (galleryDiv) {
const paginationDiv = galleryDiv.parentElement?.querySelector('.mvgb-pagination');
if (paginationDiv?.instanceOf(HTMLElement) && paginationDiv.dataset.currentPage) {
currentPage = parseInt(paginationDiv.dataset.currentPage);
}
}
await this.swapGalleryItems(galleryId, media, allItems[index + 1], sourcePath, currentPage);
});
});
}
this.addCopyAltMenuItem(menu, media);
this.addEditAltMenuItem(menu, media, galleryId, sourcePath, container);
if (media.file) {
menu.addItem((menuItem) => {
menuItem
.setTitle(t('rename'))
.setIcon("pencil")
.onClick(() => {
const galleryDiv = container.closest('.mvgb-media-gallery-grid');
let currentPage = 1;
if (galleryDiv) {
const paginationDiv = galleryDiv.parentElement?.querySelector('.mvgb-pagination');
if (paginationDiv?.instanceOf(HTMLElement) && paginationDiv.dataset.currentPage) {
currentPage = parseInt(paginationDiv.dataset.currentPage);
}
}
void this.renameGalleryItem(galleryId, media, sourcePath, currentPage);
});
});
}
menu.addItem((menuItem) => {
menuItem
.setTitle(t('delete'))
.setIcon("trash")
.onClick(() => {
// 從 DOM 獲取當前頁碼
const galleryDiv = container.closest('.mvgb-media-gallery-grid');
let currentPage = 1;
if (galleryDiv) {
const paginationDiv = galleryDiv.parentElement?.querySelector('.mvgb-pagination');
if (paginationDiv?.instanceOf(HTMLElement) && paginationDiv.dataset.currentPage) {
currentPage = parseInt(paginationDiv.dataset.currentPage);
}
}
void this.deleteGalleryItem(galleryId, media, sourcePath, currentPage);
});
});
const galleryDiv = container.closest('.mvgb-media-gallery-grid');
if (galleryDiv instanceof HTMLElement) {
menu.addSeparator();
this.addGalleryBlockMenuItems(menu, galleryDiv, galleryId, sourcePath);
}
menu.showAtMouseEvent(e);
});
return container;
}
private addCopyAltMenuItem(menu: Menu, item: GalleryItem) {
if (!item.altText?.trim()) {
return;
}
menu.addItem((menuItem) => {
menuItem
.setTitle(t('copy_alt') || 'Copy alt')
.setIcon('copy')
.onClick(async () => {
try {
await navigator.clipboard.writeText(item.altText ?? '');
new Notice(t('alt_copied') || 'Alt copied');
} catch (error) {
console.error('Error copying alt text:', error);
new Notice(t('clipboard_error') || 'Clipboard error');
}
});
});
}
private addEditAltMenuItem(menu: Menu, item: GalleryItem, galleryId: string, sourcePath?: string, container?: HTMLElement) {
if (!item.loc) {
return;
}
const hasAlt = !!(item.altText && item.altText.trim() !== '');
menu.addItem((menuItem) => {
menuItem
.setTitle(hasAlt ? (t('edit_alt') || 'Edit alt') : (t('add_alt') || 'Add alt'))
.setIcon("pencil")
.onClick(() => {
let currentPage = 1;
if (container) {
const galleryDiv = container.closest('.mvgb-media-gallery-grid');
if (galleryDiv) {
const paginationDiv = galleryDiv.parentElement?.querySelector('.mvgb-pagination');
if (paginationDiv?.instanceOf(HTMLElement) && paginationDiv.dataset.currentPage) {
currentPage = parseInt(paginationDiv.dataset.currentPage);
}
}
}
const modal = new AltModal(
this.app,
item.altText || '',
hasAlt,
(newAlt) => {
void this.editGalleryItemAlt(galleryId, item, false, newAlt, sourcePath, currentPage);
},
() => {
void this.editGalleryItemAlt(galleryId, item, true, '', sourcePath, currentPage);
}
);
modal.open();
});
});
}
private addGalleryBlockMenuItems(menu: Menu, galleryDiv: HTMLElement, galleryId: string, sourcePath?: string) {
menu.addItem((item) => {
item
.setTitle(t('add_item'))
.setIcon("image")
.onClick(() => {
const modal = new ImageUploadModal(this.app, this.plugin, galleryDiv, sourcePath);
modal.open();
});
});
menu.addItem((item) => {
item
.setTitle(t('setting_gallery'))
.setIcon("settings")
.onClick(() => {
// 獲取當前筆記的文件
// 優先使用來源路徑,若無效則使用當前開啟的檔案
let activeFile: TFile | null = null;
if (sourcePath) {
activeFile = this.app.vault.getAbstractFileByPath(sourcePath) as TFile | null;
}
if (!activeFile) {
activeFile = this.app.workspace.getActiveFile();
if (!activeFile) {
new Notice(t('please_open_note'));
return;
}
}
void (async () => {
// 讀取文件內容
const targetFile = activeFile;
const content = await this.app.vault.read(targetFile);
// 尋找包含當前 gallery ID 的 gallery 區塊
const galleryBlockRegex = /```gallery\r?\n([\s\S]*?)```/g;
let match;
let galleryContent = '';
let matchPosition = { start: 0, end: 0 };
while ((match = galleryBlockRegex.exec(content)) !== null) {
const blockContent = match[1];
const blockId = 'gallery-' + this.hashString(blockContent.trim());
if (blockId === galleryId) {
galleryContent = blockContent;
matchPosition.start = match.index;
matchPosition.end = match.index + match[0].length;
break;
}
}
if (!match || match.length === 0) {
new Notice(t('gallery_not_found'));
return;
}
// 創建並打開設定對話框
const modal = new GalleryBlockGenerateModal(this.app, galleryContent);
// 設置確認後的回調函數,使用 vault.process 修改文件
modal.onConfirm = (newGalleryBlock: string) => {
void this.app.vault.process(targetFile, (fileContent) => {
return fileContent.substring(0, matchPosition.start) +
newGalleryBlock +
fileContent.substring(matchPosition.end);
}).catch((error) => {
console.error('Error updating gallery block:', error);
new Notice(t('gallery_not_found'));
});
};
modal.open();
})().catch((error) => {
console.error('Error opening gallery settings:', error);
new Notice(t('gallery_not_found'));
});
});
});
menu.addItem((item) => {
item
.setTitle(t('ungenerate_gallery'))
.setIcon("eraser")
.onClick(() => {
// 獲取當前筆記的文件
// 優先使用來源路徑,若無效則使用當前開啟的檔案
let activeFile: TFile | null = null;
if (sourcePath) {
activeFile = this.app.vault.getAbstractFileByPath(sourcePath) as TFile | null;
}
if (!activeFile) {
activeFile = this.app.workspace.getActiveFile();
if (!activeFile) {
new Notice(t('please_open_note'));
return;
}
}
void (async () => {
// 讀取文件內容
const targetFile = activeFile;
const content = await this.app.vault.read(targetFile);
// 尋找包含當前 gallery ID 的 gallery 區塊
const galleryBlockRegex = /```gallery\r?\n([\s\S]*?)```/g;
let match;
let matchPosition = { start: 0, end: 0 };
while ((match = galleryBlockRegex.exec(content)) !== null) {
const blockContent = match[1];
const blockId = 'gallery-' + this.hashString(blockContent.trim());
if (blockId === galleryId) {
matchPosition.start = match.index;
matchPosition.end = match.index + match[0].length;
break;
}
}
if (!match || match.length === 0) {
new Notice(t('gallery_not_found'));
return;
}
// 移除 gallery 標記,保留內容
const newContent = content.substring(matchPosition.start + '```gallery\n'.length, matchPosition.end - '```'.length);
// 使用 vault.process 修改文件
await this.app.vault.process(targetFile, (fileContent) => {
return fileContent.substring(0, matchPosition.start) +
newContent +
fileContent.substring(matchPosition.end);
});
})().catch((error) => {
console.error('Error ungenerating gallery block:', error);
new Notice(t('gallery_not_found'));
});
});
});
}
createHoverControls(item: GalleryItem, index: number, allItems: GalleryItem[], galleryId: string, sourcePath?: string) {
const controls = activeDocument.createElement('div');
controls.className = 'mvgb-hover-controls';
if (item.loc) {
const dragHandle = activeDocument.createElement('button');
dragHandle.className = 'mvgb-hover-btn mvgb-btn-drag';
dragHandle.type = 'button';
dragHandle.title = t('drag_reorder');
dragHandle.draggable = true;
dragHandle.appendChild(this.createDragHandleIcon());
dragHandle.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
};
dragHandle.addEventListener('dragstart', (e: DragEvent) => {
if (!e.dataTransfer) return;
e.stopPropagation();
const payload: GalleryReorderPayload = { galleryId, index };
e.dataTransfer.setData(GALLERY_REORDER_MIME, JSON.stringify(payload));
e.dataTransfer.effectAllowed = 'move';
const itemContainer = dragHandle.closest('.mv-media-thumbnail-container');
if (itemContainer) {
itemContainer.classList.add('mvgb-reorder-dragging');
}
});
dragHandle.addEventListener('dragend', (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
const galleryDiv = dragHandle.closest('.mvgb-media-gallery-grid');
if (galleryDiv?.instanceOf(HTMLElement)) {
this.clearReorderClasses(galleryDiv);
}
});
controls.appendChild(dragHandle);
}
return controls;
}
async swapGalleryItems(galleryId: string, item1: GalleryItem, item2: GalleryItem, sourcePath?: string, currentPage?: number) {
// 獲取當前文件
let activeFile: TFile | null = null;
if (sourcePath) {
activeFile = this.app.vault.getAbstractFileByPath(sourcePath) as TFile | null;
}
if (!activeFile) {
activeFile = this.app.workspace.getActiveFile();
if (!activeFile) return;
}
const content = await this.app.vault.read(activeFile);
const galleryBlockRegex = /```gallery\r?\n([\s\S]*?)```/g;
let match;
while ((match = galleryBlockRegex.exec(content)) !== null) {
const blockContent = match[1];
const blockId = 'gallery-' + this.hashString(blockContent.trim());
if (blockId === galleryId) {
// 如果 item1 和 item2 都有效的地理位置資訊,則直接進行區塊交換
if (item1.loc && item2.loc) {
const lines = blockContent.split('\n');
// 取得行號範圍(包含起始與結束)
const range1 = item1.loc;
const range2 = item2.loc;
// 確保交換順序,先處理位置較前的項目
const first = range1.start < range2.start ? range1 : range2;
const second = range1.start < range2.start ? range2 : range1;
// 檢查區塊是否重疊(正常情況不應發生)
if (first.end >= second.start) {
console.error('Overlapping items, cannot swap');
return;
}
// 將區塊內容分割成五段:前段、第一項、中段、第二項、後段
const partBefore = lines.slice(0, first.start);
const partFirst = lines.slice(first.start, first.end + 1);
const partBetween = lines.slice(first.end + 1, second.start);
const partSecond = lines.slice(second.start, second.end + 1);
const partAfter = lines.slice(second.end + 1);
// 重新組裝:前段 + 第二項 + 中段 + 第一項 + 後段
const newLines = [
...partBefore,
...partSecond, // 已交換
...partBetween,
...partFirst, // 已交換
...partAfter
];
const newBlockContent = newLines.join('\n');
const newId = 'gallery-' + this.hashString(newBlockContent.trim());
// 如果提供了當前頁碼,則為重構後的 Gallery ID 儲存分頁狀態
if (currentPage) {
GalleryBlock.paginationState.set(newId, currentPage);
}
const matchStart = match.index;
const matchEnd = match.index + match[0].length;
const newBlock = '```gallery\n' + newBlockContent + '```';
// 更新 Obsidian 文件內容
const restoreScroll = captureScrollRestore(this.app, galleryId);
await this.app.vault.process(activeFile, (fileContent) => {
return fileContent.substring(0, matchStart) +
newBlock +
fileContent.substring(matchEnd);
});
restoreScroll(newId);
}
break;
}
}
}
async moveGalleryItem(galleryId: string, draggedItem: GalleryItem, targetItem: GalleryItem, position: 'before' | 'after', sourcePath?: string, currentPage?: number) {
let activeFile: TFile | null = null;
if (sourcePath) {
activeFile = this.app.vault.getAbstractFileByPath(sourcePath) as TFile | null;
}
if (!activeFile) {
activeFile = this.app.workspace.getActiveFile();
if (!activeFile) return;
}
const content = await this.app.vault.read(activeFile);
const galleryBlockRegex = /```gallery\r?\n([\s\S]*?)```/g;
let match;
while ((match = galleryBlockRegex.exec(content)) !== null) {
const blockContent = match[1];
const blockId = 'gallery-' + this.hashString(blockContent.trim());
if (blockId === galleryId) {
if (!draggedItem.loc || !targetItem.loc) {
return;
}
const dragged = draggedItem.loc;
const target = targetItem.loc;
if (dragged.start === target.start && dragged.end === target.end) {
return;
}
if (dragged.start <= target.end && dragged.end >= target.start) {
console.error('Overlapping items, cannot move');
return;
}
const lines = blockContent.split('\n');
const movedLines = lines.slice(dragged.start, dragged.end + 1);
const remainingLines = [
...lines.slice(0, dragged.start),
...lines.slice(dragged.end + 1)
];
const removedCount = dragged.end - dragged.start + 1;
let targetStart = target.start;
let targetEnd = target.end;
if (target.start > dragged.end) {
targetStart -= removedCount;
targetEnd -= removedCount;
}
const insertIndex = position === 'before' ? targetStart : targetEnd + 1;
remainingLines.splice(insertIndex, 0, ...movedLines);
const newBlockContent = remainingLines.join('\n');
const newId = 'gallery-' + this.hashString(newBlockContent.trim());
if (currentPage) {
GalleryBlock.paginationState.set(newId, currentPage);
}
const matchStart = match.index;
const matchEnd = match.index + match[0].length;
const newBlock = '```gallery\n' + newBlockContent + '```';
const restoreScroll = captureScrollRestore(this.app, galleryId);
await this.app.vault.process(activeFile, (fileContent) => {
return fileContent.substring(0, matchStart) +
newBlock +
fileContent.substring(matchEnd);
});
restoreScroll(newId);
break;
}
}
}
async deleteGalleryItem(galleryId: string, item: GalleryItem, sourcePath?: string, currentPage?: number) {
// 獲取當前文件
let activeFile: TFile | null = null;
if (sourcePath) {
activeFile = this.app.vault.getAbstractFileByPath(sourcePath) as TFile | null;
}
if (!activeFile) {
activeFile = this.app.workspace.getActiveFile();
if (!activeFile) return;
}
const content = await this.app.vault.read(activeFile);
const galleryBlockRegex = /```gallery\r?\n([\s\S]*?)```/g;
let match;
while ((match = galleryBlockRegex.exec(content)) !== null) {
const blockContent = match[1];
const blockId = 'gallery-' + this.hashString(blockContent.trim());
if (blockId === galleryId) {
if (item.loc) {
const lines = blockContent.split('\n');
const start = item.loc.start;
const end = item.loc.end;
// 移除選定範圍內的行
lines.splice(start, end - start + 1);
const newBlockContent = lines.join('\n');
const newId = 'gallery-' + this.hashString(newBlockContent.trim());
// 如果提供了當前頁碼,則為重構後的 Gallery ID 儲存分頁狀態
if (currentPage) {
GalleryBlock.paginationState.set(newId, currentPage);
}
const matchStart = match.index;
const matchEnd = match.index + match[0].length;
const newBlock = '```gallery\n' + newBlockContent + '```';
// 更新 Obsidian 文件內容
const restoreScroll = captureScrollRestore(this.app, galleryId);
await this.app.vault.process(activeFile, (fileContent) => {
return fileContent.substring(0, matchStart) +
newBlock +
fileContent.substring(matchEnd);
});
restoreScroll(newId);
}
break;
}
}
}
async renameGalleryItem(galleryId: string, item: GalleryItem, sourcePath?: string, currentPage?: number) {
const file = item.file;
if (!file) {
new Notice(t('error_renaming_file') || 'File not found');
return;
}
const currentName = file.basename;
const modal = new RenameModal(this.app, currentName, (newName) => {
void (async () => {
if (!newName || newName === currentName) return;
try {
const parentPath = file.parent ? file.parent.path : "";
const newPath = parentPath === "/" || parentPath === ""
? `${newName}.${file.extension}`
: `${parentPath}/${newName}.${file.extension}`;
await this.app.fileManager.renameFile(file, newPath);
// 更新 Gallery 區塊中的檔案名稱連結
let activeFile: TFile | null = null;
if (sourcePath) {
activeFile = this.app.vault.getAbstractFileByPath(sourcePath) as TFile | null;
}
if (!activeFile) {
activeFile = this.app.workspace.getActiveFile();
}
if (activeFile) {
const content = await this.app.vault.read(activeFile);
const galleryBlockRegex = /```gallery\r?\n([\s\S]*?)```/g;
let match;
while ((match = galleryBlockRegex.exec(content)) !== null) {
const blockContent = match[1];
const blockId = 'gallery-' + this.hashString(blockContent.trim());
if (blockId === galleryId) {
if (item.loc) {
const lines = blockContent.split('\n');
const start = item.loc.start;
const end = item.loc.end;
// 將選定範圍內的舊檔名替換為新檔名
for (let i = start; i <= end; i++) {
lines[i] = lines[i].replace(currentName, newName);
}
const newBlockContent = lines.join('\n');
const newId = 'gallery-' + this.hashString(newBlockContent.trim());
if (currentPage) {
GalleryBlock.paginationState.set(newId, currentPage);
}
const matchStart = match.index;
const matchEnd = match.index + match[0].length;
const newBlock = '```gallery\n' + newBlockContent + '```';
const restoreScroll = captureScrollRestore(this.app, galleryId);
await this.app.vault.process(activeFile, (fileContent) => {
return fileContent.substring(0, matchStart) +
newBlock +
fileContent.substring(matchEnd);
});
restoreScroll(newId);
}
break;
}
}
}
new Notice(t('file_renamed') || 'File renamed');
} catch (error) {
console.error('Error renaming file:', error);
new Notice(t('error_renaming_file') || 'Error renaming file');
}
})();
});
modal.open();
}
async editGalleryItemAlt(galleryId: string, item: GalleryItem, isDelete: boolean, newAlt: string, sourcePath?: string, currentPage?: number) {
let activeFile: TFile | null = null;
if (sourcePath) {
activeFile = this.app.vault.getAbstractFileByPath(sourcePath) as TFile | null;
}
if (!activeFile) {
activeFile = this.app.workspace.getActiveFile();
}
if (!activeFile) {
new Notice(t('please_open_note') || 'Please open a note first');
return;
}
try {
const content = await this.app.vault.read(activeFile);
const galleryBlockRegex = /```gallery\r?\n([\s\S]*?)```/g;
let match;
while ((match = galleryBlockRegex.exec(content)) !== null) {
const blockContent = match[1];
const blockId = 'gallery-' + this.hashString(blockContent.trim());
if (blockId === galleryId) {
if (item.loc) {
const lines = blockContent.split('\n');
const start = item.loc.start;
const end = item.loc.end;
let hasAltLine = false;
let altIndex = -1;
for (let i = start; i <= end; i++) {
if (/^\s*alt:\s*(.*)$/i.test(lines[i])) {
hasAltLine = true;
altIndex = i;
break;
}
}
if (isDelete) {
if (hasAltLine && altIndex !== -1) {
lines.splice(altIndex, 1);
}
} else {
if (hasAltLine && altIndex !== -1) {
lines[altIndex] = `alt: ${newAlt}`;
} else {
// 原本沒有 alt在項目的最前面插入一行 alt:
lines.splice(start, 0, `alt: ${newAlt}`);
}
}
const newBlockContent = lines.join('\n');
const newId = 'gallery-' + this.hashString(newBlockContent.trim());
if (currentPage) {
GalleryBlock.paginationState.set(newId, currentPage);
}
const matchStart = match.index;
const matchEnd = match.index + match[0].length;
const newBlock = '```gallery\n' + newBlockContent + '```';
const restoreScroll = captureScrollRestore(this.app, galleryId);
await this.app.vault.process(activeFile, (fileContent) => {
return fileContent.substring(0, matchStart) +
newBlock +
fileContent.substring(matchEnd);
});
restoreScroll(newId);
new Notice(isDelete ? (t('alt_removed') || 'Alt text removed') : (t('alt_updated') || 'Alt text updated'));
}
break;
}
}
} catch (error) {
console.error('Error editing alt:', error);
new Notice(t('error_updating_alt') || 'Error updating alt text');
}
}
}