mirror of
https://github.com/devon22/obsidian-gridexplorer.git
synced 2026-07-22 12:00:25 +00:00
3.4.7
This commit is contained in:
parent
9580562830
commit
970f27fe00
15 changed files with 257 additions and 23 deletions
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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[] = []; // 存儲所有網格項目的引用
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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': '未完了',
|
||||
|
|
|
|||
|
|
@ -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': '미완료',
|
||||
|
|
|
|||
|
|
@ -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': 'Незавершенные',
|
||||
|
|
|
|||
|
|
@ -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': 'Незавершені',
|
||||
|
|
|
|||
|
|
@ -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': '未完成',
|
||||
|
|
|
|||
|
|
@ -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': '未完成',
|
||||
|
|
|
|||
|
|
@ -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))) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue