This commit is contained in:
Devon22 2026-06-17 17:32:56 +08:00
parent 9580562830
commit 970f27fe00
15 changed files with 257 additions and 23 deletions

View file

@ -1,7 +1,7 @@
{
"id": "gridexplorer",
"name": "GridExplorer",
"version": "3.4.6",
"version": "3.4.7",
"minAppVersion": "1.8.7",
"description": "Browse note files in a grid view.",
"author": "Devon22",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "gridexplorer",
"version": "3.4.6",
"version": "3.4.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gridexplorer",
"version": "3.4.6",
"version": "3.4.7",
"license": "MIT",
"dependencies": {
"jszip": "^3.10.1"

View file

@ -1,6 +1,6 @@
{
"name": "gridexplorer",
"version": "3.4.6",
"version": "3.4.7",
"description": "Browse note files in a grid view.",
"main": "main.js",
"scripts": {

View file

@ -27,7 +27,9 @@ export class FileWatcher {
this.app.vault.on('modify', (file) => {
if (file instanceof TFile) {
if (this.gridView.sourceMode === 'recent-files') {
if(isDocumentFile(file) || (isMediaFile(file) && this.gridView.includeMedia)) {
const isDocAllowed = isDocumentFile(file) && this.gridView.includeMedia !== 'media-only';
const isMediaAllowed = isMediaFile(file) && !!this.gridView.includeMedia;
if (isDocAllowed || isMediaAllowed) {
this.scheduleRender(5000);
}
}
@ -46,7 +48,9 @@ export class FileWatcher {
if(this.gridView.sourceMode === 'random-note') {
return;
} else if (this.gridView.sourceMode === 'recent-files') {
if(isDocumentFile(file) || (isMediaFile(file) && this.gridView.includeMedia)) {
const isDocAllowed = isDocumentFile(file) && this.gridView.includeMedia !== 'media-only';
const isMediaAllowed = isMediaFile(file) && !!this.gridView.includeMedia;
if (isDocAllowed || isMediaAllowed) {
this.scheduleRender(2000);
}
} else if (this.gridView.sourceMode === 'folder') {

View file

@ -95,7 +95,7 @@ interface GridViewStateData {
searchFilesNameOnly?: boolean;
searchMediaFiles?: boolean;
fileNameFilterQuery?: string;
includeMedia?: boolean;
includeMedia?: boolean | 'media-only';
minMode?: boolean;
showIgnoredItems?: boolean;
baseCardLayout?: 'horizontal' | 'vertical';
@ -155,7 +155,7 @@ export class GridView extends ItemView {
searchFilesNameOnly: boolean = false; // 是否只搜尋筆記名稱
searchMediaFiles: boolean = false; // 是否搜尋媒體檔案
fileNameFilterQuery: string = ''; // 目前列表的檔名篩選關鍵字
includeMedia: boolean = false; // 是否包含媒體檔案
includeMedia: boolean | 'media-only' = false; // 是否包含媒體檔案
selectedItemIndex: number = -1; // 當前選中的項目索引
selectedItems: Set<number> = new Set(); // 存儲多選的項目索引
gridItems: HTMLElement[] = []; // 存儲所有網格項目的引用

View file

@ -78,6 +78,16 @@ export class MediaModal extends Modal {
}
onOpen() {
const appWithPlugins = this.app as {
plugins?: {
plugins?: Record<string, { activeMediaModal?: unknown }>;
};
};
const plugin = appWithPlugins.plugins?.plugins?.['obsidian-gridexplorer'];
if (plugin) {
plugin.activeMediaModal = this;
}
const { contentEl } = this;
// 設置 modal 樣式為全螢幕
@ -183,6 +193,16 @@ export class MediaModal extends Modal {
}
onClose() {
const appWithPlugins = this.app as {
plugins?: {
plugins?: Record<string, { activeMediaModal?: unknown }>;
};
};
const plugin = appWithPlugins.plugins?.plugins?.['obsidian-gridexplorer'];
if (plugin && plugin.activeMediaModal === this) {
plugin.activeMediaModal = null;
}
const { contentEl } = this;
contentEl.empty();
@ -266,13 +286,179 @@ export class MediaModal extends Modal {
mediaContainer.appendChild(img);
// 圖片點擊事件(放大/縮小)
if (Platform.isMobile) {
img.addEventListener('dblclick', (event) => {
event.stopPropagation();
this.toggleImageZoom(img, event);
});
} else {
// 取得與更新圖片初始大小並綁定手勢事件
let initialWidth = 0;
let initialHeight = 0;
let currentScale = 1;
let isPinching = false;
let pinchStartDistance = 0;
let pinchStartScale = 1;
let pinchStartCenterX = 0;
let pinchStartCenterY = 0;
let pinchStartScrollLeft = 0;
let pinchStartScrollTop = 0;
let lastTapTime = 0;
let pinchRatioX = 0.5;
let pinchRatioY = 0.5;
const initImageDimensions = () => {
initialWidth = img.offsetWidth || img.clientWidth;
initialHeight = img.offsetHeight || img.clientHeight;
currentScale = 1;
};
img.onload = () => {
// 移除舊 of 媒體元素
if (this.currentMediaElement) {
this.currentMediaElement.remove();
}
this.currentMediaElement = img;
// 設置圖片樣式,預設滿屏顯示
this.resetImageStyles(img);
// 顯示新圖片
img.removeClass('ge-hidden');
initImageDimensions();
};
if (img.complete) {
initImageDimensions();
}
// 行動裝置手勢支援 (Double Tap & Pinch Zoom)
img.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
// 行動裝置雙擊 (Double Tap) 偵測
const now = Date.now();
if (now - lastTapTime < 300) {
e.preventDefault();
e.stopPropagation();
this.toggleImageZoom(img);
lastTapTime = 0;
return;
}
lastTapTime = now;
} else if (e.touches.length === 2) {
// 雙指捏合縮放 (Pinch Zoom) 開始
e.preventDefault();
isPinching = true;
const t1 = e.touches[0];
const t2 = e.touches[1];
pinchStartDistance = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY);
pinchStartCenterX = (t1.clientX + t2.clientX) / 2;
pinchStartCenterY = (t1.clientY + t2.clientY) / 2;
if (initialWidth > 0) {
pinchStartScale = img.offsetWidth / initialWidth;
} else {
pinchStartScale = 1;
}
const mediaView = this.contentEl.querySelector<HTMLElement>('.ge-media-view');
if (mediaView) {
pinchStartScrollLeft = mediaView.scrollLeft;
pinchStartScrollTop = mediaView.scrollTop;
const viewRect = mediaView.getBoundingClientRect();
const relativeCenterX = pinchStartCenterX - viewRect.left;
const relativeCenterY = pinchStartCenterY - viewRect.top;
const startWidth = img.offsetWidth || initialWidth;
const startHeight = img.offsetHeight || initialHeight;
const imgRect = img.getBoundingClientRect();
const imgLeftInView = imgRect.left - viewRect.left + pinchStartScrollLeft;
const imgTopInView = imgRect.top - viewRect.top + pinchStartScrollTop;
pinchRatioX = startWidth > 0 ? (pinchStartScrollLeft + relativeCenterX - imgLeftInView) / startWidth : 0.5;
pinchRatioY = startHeight > 0 ? (pinchStartScrollTop + relativeCenterY - imgTopInView) / startHeight : 0.5;
}
}
}, { passive: false });
img.addEventListener('touchmove', (e) => {
if (isPinching && e.touches.length === 2) {
e.preventDefault();
const t1 = e.touches[0];
const t2 = e.touches[1];
const dist = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY);
if (pinchStartDistance === 0) return;
const centerScale = dist / pinchStartDistance;
let newScale = pinchStartScale * centerScale;
// 最大放大至原始尺寸의 4 倍
if (newScale > 4) newScale = 4;
currentScale = newScale;
const mediaView = this.contentEl.querySelector<HTMLElement>('.ge-media-view');
if (mediaView && initialWidth > 0 && initialHeight > 0) {
if (newScale > 1) {
this.isZoomed = true;
this.contentEl.addClass('is-zoomed');
mediaView.setCssStyles({
overflowX: 'scroll',
overflowY: 'scroll',
});
const newWidth = initialWidth * newScale;
const newHeight = initialHeight * newScale;
img.setCssStyles({
width: `${newWidth}px`,
height: `${newHeight}px`,
maxWidth: 'none',
maxHeight: 'none',
position: 'relative',
left: '0',
top: '0',
margin: 'auto',
transform: 'none',
cursor: 'zoom-out',
});
// 以雙指位置為中心縮放與拖曳
const viewRect = mediaView.getBoundingClientRect();
const currentCenterX = (t1.clientX + t2.clientX) / 2;
const currentCenterY = (t1.clientY + t2.clientY) / 2;
const relativeCurrentCenterX = currentCenterX - viewRect.left;
const relativeCurrentCenterY = currentCenterY - viewRect.top;
const imgLeftInView = newWidth < viewRect.width ? (viewRect.width - newWidth) / 2 : 0;
const imgTopInView = newHeight < viewRect.height ? (viewRect.height - newHeight) / 2 : 0;
mediaView.scrollLeft = (pinchRatioX * newWidth) - relativeCurrentCenterX + imgLeftInView;
mediaView.scrollTop = (pinchRatioY * newHeight) - relativeCurrentCenterY + imgTopInView;
} else {
// 自動復原:縮小至小於或等於初始大小,自動重置為預設填滿模式
this.resetImageStyles(img);
this.isZoomed = false;
this.contentEl.removeClass('is-zoomed');
currentScale = 1;
}
}
}
}, { passive: false });
img.addEventListener('touchend', (e) => {
if (isPinching) {
if (e.touches.length < 2) {
isPinching = false;
if (currentScale <= 1) {
this.resetImageStyles(img);
this.isZoomed = false;
this.contentEl.removeClass('is-zoomed');
currentScale = 1;
}
}
}
});
// 桌面端點擊事件(放大/縮小)
if (!Platform.isMobile) {
img.addEventListener('click', (event) => {
event.stopPropagation();
this.toggleImageZoom(img, event);
@ -468,13 +654,21 @@ export class MediaModal extends Modal {
// 處理媒體右鍵選單
private onMediaContextMenu(event: MouseEvent, file: MediaFile) {
if ('isVirtual' in file && file.isVirtual) {
return;
}
event.preventDefault();
const menu = new Menu();
if (file instanceof TFile) {
this.app.workspace.trigger('file-menu', menu, file, 'media-viewer');
try {
this.app.workspace.trigger('file-menu', menu, file, 'media-viewer');
} catch (err) {
console.error('Error triggering file-menu event:', err);
}
} else if ('isVirtual' in file && file.isVirtual) {
// 觸發自定義事件,讓其他外掛專案可以針對虛擬檔案(如 zip 中的圖片)自定義右鍵選單
try {
this.app.workspace.trigger('gridexplorer:virtual-file-menu', menu, file, 'media-viewer');
} catch (err) {
console.error('Error triggering gridexplorer:virtual-file-menu event:', err);
}
}
menu.showAtMouseEvent(event);
}
@ -560,4 +754,8 @@ export class MediaModal extends Modal {
this.isDragging = false;
}, { passive: true });
}
public getActiveFile(): MediaFile {
return this.mediaFiles[this.currentIndex];
}
}

View file

@ -822,14 +822,21 @@ export function renderModePath(gridView: GridView) {
case 'all-files': {
if (gridView.plugin.settings.showMediaFiles && gridView.searchQuery === '') {
// "顯示類型"選項
const showTypeName = gridView.includeMedia ? t('random_note_include_media_files') : t('random_note_notes_only');
let showTypeName = '';
if (gridView.includeMedia === 'media-only') {
showTypeName = t('random_note_media_only');
} else if (gridView.includeMedia === true) {
showTypeName = t('random_note_include_media_files');
} else {
showTypeName = t('random_note_notes_only');
}
const showTypeSpan = modenameContainer.createEl('a', { text: showTypeName, cls: 'ge-sub-option' });
showTypeSpan.addEventListener('click', (evt) => {
const menu = new Menu();
menu.addItem((item) => {
item.setTitle(t('random_note_notes_only'))
.setIcon('file-text')
.setChecked(!gridView.includeMedia)
.setChecked(gridView.includeMedia === false)
.onClick(() => {
gridView.includeMedia = false;
void gridView.render();
@ -838,12 +845,21 @@ export function renderModePath(gridView: GridView) {
menu.addItem((item) => {
item.setTitle(t('random_note_include_media_files'))
.setIcon('file-image')
.setChecked(gridView.includeMedia)
.setChecked(gridView.includeMedia === true)
.onClick(() => {
gridView.includeMedia = true;
void gridView.render();
});
});
menu.addItem((item) => {
item.setTitle(t('random_note_media_only'))
.setIcon('image')
.setChecked(gridView.includeMedia === 'media-only')
.onClick(() => {
gridView.includeMedia = 'media-only';
void gridView.render();
});
});
menu.showAtMouseEvent(evt);
});
}

View file

@ -176,6 +176,7 @@ export default {
'random_note_count': 'Random note count',
'random_note_notes_only': 'Notes Only',
'random_note_include_media_files': 'Include Media Files',
'random_note_media_only': 'Media Only',
'show_tasks_mode': 'Show tasks mode',
'task_filter': 'Task filter',
'uncompleted': 'Uncompleted',

View file

@ -175,6 +175,7 @@ export default {
'random_note_count': 'ランダムノートモード表示筆数',
'random_note_notes_only': 'ノートのみ',
'random_note_include_media_files': 'メディアファイルを含む',
'random_note_media_only': 'メディアファイルのみ',
'show_tasks_mode': 'タスクモードを表示',
'task_filter': 'タスクフィルタ',
'uncompleted': '未完了',

View file

@ -176,6 +176,7 @@ export default {
'random_note_count': '랜덤 노트 개수',
'random_note_notes_only': '노트만',
'random_note_include_media_files': '미디어 파일 포함',
'random_note_media_only': '미디어 파일만',
'show_tasks_mode': '작업 모드 표시',
'task_filter': '작업 필터',
'uncompleted': '미완료',

View file

@ -175,6 +175,7 @@ export default {
'random_note_count': 'Количество случайных заметок',
'random_note_notes_only': 'Только заметки',
'random_note_include_media_files': 'Включить медиафайлы',
'random_note_media_only': 'Только медиафайлы',
'show_tasks_mode': 'Показывать режим задач',
'task_filter': 'Фильтр задач',
'uncompleted': 'Незавершенные',

View file

@ -175,6 +175,7 @@ export default {
'random_note_count': 'Кількість випадкових нотаток',
'random_note_notes_only': 'Тільки нотатки',
'random_note_include_media_files': 'Включити медіафайли',
'random_note_media_only': 'Тільки медіафайли',
'show_tasks_mode': 'Показувати режим завдань',
'task_filter': 'Фільтр завдань',
'uncompleted': 'Незавершені',

View file

@ -176,6 +176,7 @@ export default {
'random_note_count': '隨機筆記模式顯示筆數',
'random_note_notes_only': '僅筆記',
'random_note_include_media_files': '包含媒體檔案',
'random_note_media_only': '僅媒體檔案',
'show_tasks_mode': '顯示任務模式',
'task_filter': '任務分類',
'uncompleted': '未完成',

View file

@ -175,6 +175,7 @@ export default {
'random_note_count': '随机笔记模式显示笔数',
'random_note_notes_only': '仅笔记',
'random_note_include_media_files': '包含媒体文件',
'random_note_media_only': '仅媒体文件',
'show_tasks_mode': '显示任务模式',
'task_filter': '任务分类',
'uncompleted': '未完成',

View file

@ -321,7 +321,7 @@ export function ignoredFiles(files: TFile[], gridView: GridView): TFile[] {
}
// 獲取檔案
export async function getFiles(gridView: GridView, includeMediaFiles: boolean): Promise<TFile[]> {
export async function getFiles(gridView: GridView, includeMediaFiles: boolean | 'media-only'): Promise<TFile[]> {
const app = gridView.app;
const settings = gridView.plugin.settings;
const sourceMode = gridView.sourceMode;
@ -579,6 +579,9 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean):
} else if (sourceMode === 'all-files') {
// 所有筆記模式
const allVaultFiles = app.vault.getFiles().filter(file => {
if (includeMediaFiles === 'media-only') {
return settings.showMediaFiles && isMediaFile(file);
}
// 根據設定決定是否包含媒體檔案
if (isDocumentFile(file) ||
(settings.showMediaFiles && includeMediaFiles && isMediaFile(file))) {
@ -590,6 +593,9 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean):
} else if (sourceMode === 'recent-files') {
// 最近檔案模式
const recentFiles = app.vault.getFiles().filter(file => {
if (includeMediaFiles === 'media-only') {
return settings.showMediaFiles && isMediaFile(file);
}
// 根據設定決定是否包含媒體檔案
if (isDocumentFile(file) ||
(settings.showMediaFiles && includeMediaFiles && isMediaFile(file))) {
@ -602,6 +608,9 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean):
} else if (sourceMode === 'random-note') {
// 隨機筆記模式,從所有筆記中隨機選取
const randomFiles = app.vault.getFiles().filter(file => {
if (includeMediaFiles === 'media-only') {
return settings.showMediaFiles && isMediaFile(file);
}
// 根據設定決定是否包含媒體檔案
if (isDocumentFile(file) ||
(settings.showMediaFiles && includeMediaFiles && isMediaFile(file))) {