From c757476d843986bc1ea6922ce96050fd766c19f2 Mon Sep 17 00:00:00 2001 From: Devon22 Date: Sun, 17 Aug 2025 02:10:23 +0800 Subject: [PATCH] 2.9.5 --- manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/ExplorerView.ts | 162 ++++++++++++++++++++++++++++++++---------- src/GridView.ts | 49 ++++++++----- src/dragUtils.ts | 71 ++++++++++++++++++ src/main.ts | 54 ++++++++------ src/renderFolder.ts | 139 ++++++++++++++++++++++-------------- src/renderModePath.ts | 40 ++++++++--- versions.json | 2 +- 10 files changed, 381 insertions(+), 144 deletions(-) create mode 100644 src/dragUtils.ts diff --git a/manifest.json b/manifest.json index 6f53fe0..5edd1d3 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "gridexplorer", "name": "GridExplorer", - "version": "2.9.4", + "version": "2.9.5", "minAppVersion": "1.1.0", "description": "Browse note files in a grid view.", "author": "Devon22", diff --git a/package-lock.json b/package-lock.json index 28c19d8..83cd73e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gridexplorer", - "version": "2.9.4", + "version": "2.9.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gridexplorer", - "version": "2.9.4", + "version": "2.9.5", "license": "MIT", "devDependencies": { "@types/node": "^16.11.6", diff --git a/package.json b/package.json index 10aa6d1..b672d82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gridexplorer", - "version": "2.9.4", + "version": "2.9.5", "description": "Browse note files in a grid view.", "main": "main.js", "scripts": { diff --git a/src/ExplorerView.ts b/src/ExplorerView.ts index 2503a51..56e3185 100644 --- a/src/ExplorerView.ts +++ b/src/ExplorerView.ts @@ -1,11 +1,12 @@ -import { ItemView, WorkspaceLeaf, TFolder, TFile, Menu, setIcon, EventRef, Platform, normalizePath, FuzzySuggestModal } from 'obsidian'; +import { TFile, TFolder, WorkspaceLeaf, Menu, setIcon, Platform, normalizePath, ItemView, EventRef, FuzzySuggestModal } from 'obsidian'; import GridExplorerPlugin from './main'; import { GridView } from './GridView'; +import { isFolderIgnored, isImageFile, isVideoFile, isAudioFile, isMediaFile } from './fileUtils'; +import { extractObsidianPathsFromDT } from './dragUtils'; import { CustomModeModal } from './modal/customModeModal'; import { showFolderNoteSettingsModal } from './modal/folderNoteSettingsModal'; import { showFolderRenameModal } from './modal/folderRenameModal'; import { showFolderMoveModal } from './modal/folderMoveModal'; -import { isFolderIgnored, isImageFile, isVideoFile, isAudioFile } from './fileUtils'; import { FloatingAudioPlayer } from './floatingAudioPlayer'; import { MediaModal } from './modal/mediaModal'; import { t } from './translations'; @@ -168,9 +169,27 @@ export class ExplorerView extends ItemView { ); this.registerCustomEvent('ge-grid-source-changed', (payload: any) => { - this.lastMode = payload?.mode ?? this.lastMode; - this.lastPath = payload?.path ?? this.lastPath; - schedule(); + // 檢查模式或路徑是否真的改變了 + const newMode = payload?.mode ?? this.lastMode; + const newPath = payload?.path ?? this.lastPath; + + // 只有在模式或路徑真正改變時才更新狀態和觸發重繪 + if (newMode !== this.lastMode || newPath !== this.lastPath) { + this.lastMode = newMode; + this.lastPath = newPath; + + // 當模式切換時,清理搜尋狀態以避免資料夾被強制展開 + if (this.searchQuery.trim()) { + this.searchQuery = ''; + if (this.searchInputEl) { + this.searchInputEl.value = ''; + } + // 同步搜尋輸入框狀態 + this.syncSearchInput(); + } + + schedule(); + } }); this.registerCustomEvent('grid-explorer:folder-note-updated', schedule); @@ -479,10 +498,16 @@ export class ExplorerView extends ItemView { // 嘗試獲取當前活躍的 GridView const activeGrid = this.app.workspace.getActiveViewOfType(GridView); - // 如果有活躍的 GridView,更新快取的狀態 + // 如果有活躍的 GridView,檢查是否需要更新快取的狀態 if (activeGrid) { - this.lastMode = activeGrid.sourceMode; - this.lastPath = activeGrid.sourcePath; + const currentMode = activeGrid.sourceMode; + const currentPath = activeGrid.sourcePath; + + // 只有在狀態真正改變時才更新快取 + if (currentMode !== this.lastMode || currentPath !== this.lastPath) { + this.lastMode = currentMode; + this.lastPath = currentPath; + } } // 返回當前狀態,優先使用活躍 GridView 的狀態,否則使用快取 @@ -533,6 +558,25 @@ export class ExplorerView extends ItemView { return topLevelFolders.some((f) => this.shouldShowFolder(f)); } + // 檢查資料夾是否有符合搜尋條件的子項目 + private hasMatchingChildren(folder: TFolder): boolean { + if (!this.isFiltering()) return false; + + // 檢查資料夾名稱本身是否符合搜尋條件 + if (this.matchesQuery(folder.name)) return true; + + // 檢查子資料夾是否有符合條件的 + const settings = this.plugin.settings; + const activeGrid = this.app.workspace.getActiveViewOfType(GridView); + const showIgnoredFolders = activeGrid?.showIgnoredFolders ?? false; + + const childFolders = folder.children + .filter((f): f is TFolder => f instanceof TFolder) + .filter((f) => !isFolderIgnored(f, settings.ignoredFolders, settings.ignoredFolderPatterns, showIgnoredFolders)); + + return childFolders.some((child) => this.hasMatchingChildren(child)); + } + // 渲染模式群組 private renderModeGroups(contentEl: HTMLElement, currentMode: string) { const settings = this.plugin.settings; @@ -616,7 +660,12 @@ export class ExplorerView extends ItemView { const nodeEl = contentEl.createDiv({ cls: 'ge-explorer-folder-node' }); const header = nodeEl.createDiv({ cls: 'ge-explorer-folder-header' }); const toggle = header.createSpan({ cls: 'ge-explorer-folder-toggle' }); - const expanded = this.isExpanded(groupKey); + let expanded = this.isExpanded(groupKey); + // 如果正在搜尋且有符合的項目,自動展開群組 + if (this.isFiltering() && items.length > 0) { + expanded = true; + this.setExpanded(groupKey, expanded); + } setIcon(toggle, expanded ? 'chevron-down' : 'chevron-right'); @@ -731,7 +780,7 @@ export class ExplorerView extends ItemView { const foldersGroupKey = '__folders__root'; const foldersNode = contentEl.createDiv({ cls: 'ge-explorer-folder-node' }); - const { foldersChildren } = this.createFoldersGroupHeader(foldersNode, foldersGroupKey, currentMode, currentPath); + const { foldersChildren } = this.createFoldersGroupHeader(foldersNode, foldersGroupKey, currentMode, currentPath, showIgnoredFolders); // 若目前在資料夾模式,預先展開對應的父路徑,確保可見 this.expandCurrentFolderPath(currentMode, currentPath); @@ -756,14 +805,27 @@ export class ExplorerView extends ItemView { } // 創建資料夾群組標頭 - private createFoldersGroupHeader(foldersNode: HTMLElement, foldersGroupKey: string, currentMode: string, currentPath: string) { + private createFoldersGroupHeader(foldersNode: HTMLElement, foldersGroupKey: string, currentMode: string, currentPath: string, showIgnoredFolders: boolean) { const foldersHeader = foldersNode.createDiv({ cls: 'ge-explorer-folder-header' }); const foldersToggle = foldersHeader.createSpan({ cls: 'ge-explorer-folder-toggle' }); + // 檢查是否已經記錄過展開狀態,如果沒有則預設為收合狀態 let foldersExpanded = this.isExpanded(foldersGroupKey); if (!this.expandedPaths.has(foldersGroupKey)) { - foldersExpanded = true; // 預設第一次載入展開 - this.setExpanded(foldersGroupKey, true); + // 如果正在搜尋且有符合的資料夾,自動展開根選項 + if (this.isFiltering() && this.hasVisibleTopLevelFolders(showIgnoredFolders)) { + foldersExpanded = true; + } else { + // 只有在第一次載入且沒有其他展開記錄時才預設展開 + // 避免在模式切換時自動展開 + const hasAnyExpandedPaths = this.expandedPaths.size > 0; + foldersExpanded = !hasAnyExpandedPaths; // 如果有其他展開路徑,預設收合;否則預設展開 + } + this.setExpanded(foldersGroupKey, foldersExpanded); + } else if (this.isFiltering() && this.hasVisibleTopLevelFolders(showIgnoredFolders)) { + // 如果正在搜尋且有符合的資料夾,即使之前是收合狀態也要展開 + foldersExpanded = true; + this.setExpanded(foldersGroupKey, foldersExpanded); } setIcon(foldersToggle, foldersExpanded ? 'chevron-down' : 'chevron-right'); @@ -851,8 +913,16 @@ export class ExplorerView extends ItemView { topLevelFolders.sort((a, b) => a.name.localeCompare(b.name)); for (const child of topLevelFolders) { + // 在搜尋時,只展開有符合搜尋條件的子資料夾的資料夾 + // 而不是強制展開所有資料夾 + let expanded = this.isExpanded(child.path); + if (this.isFiltering()) { + // 檢查是否有符合搜尋條件的子項目 + const hasMatchingChildren = this.hasMatchingChildren(child); + expanded = hasMatchingChildren || expanded; + } + // depth=2 -> 28px,與 .ge-explorer-mode-item 的 28px 縮排保持一致 - const expanded = this.isFiltering() ? true : this.isExpanded(child.path); this.renderFolderNode(child, foldersChildren, expanded, 2); } } @@ -1094,7 +1164,15 @@ export class ExplorerView extends ItemView { const nodeEl = contentEl.createDiv({ cls: 'ge-explorer-folder-node ge-explorer-stash-node' }); const header = nodeEl.createDiv({ cls: 'ge-explorer-folder-header' }); const toggle = header.createSpan({ cls: 'ge-explorer-folder-toggle' }); - const expanded = this.isExpanded(groupKey); + let expanded = this.isExpanded(groupKey); + // 如果正在搜尋且有符合的暫存檔案,自動展開群組 + if (this.isFiltering()) { + const visibleFiles = this.getVisibleStashFiles(); + if (visibleFiles.length > 0) { + expanded = true; + this.setExpanded(groupKey, expanded); + } + } setIcon(toggle, expanded ? 'chevron-down' : 'chevron-right'); @@ -1229,17 +1307,17 @@ export class ExplorerView extends ItemView { // 產生內容(以路徑為連結) const lines = files.map(f => { const ext = f.extension.toLowerCase(); - if (isImageFile(f)) { + if (isMediaFile(f)) { // 圖片檔要加上副檔名,且前面加上 ! - return `- ![[${f.path}]]`; + return `![[${f.path}]]`; } if (ext === 'md') { // Markdown 檔的連結不帶 .md 副檔名 const withoutExt = f.path.replace(/\.md$/i, ''); - return `- [[${withoutExt}]]`; + return `[[${withoutExt}]]`; } // 其他檔案維持原樣 - return `- [[${f.path}]]`; + return `[[${f.path}]]`; }); const content = lines.join('\n') + '\n'; @@ -1384,11 +1462,12 @@ export class ExplorerView extends ItemView { itemEl.addEventListener('dragstart', (event: DragEvent) => { if (!event.dataTransfer) return; - const isMedia = isImageFile(file) || isVideoFile(file) || isAudioFile(file); - const mdLink = isMedia ? `![[${file.path}]]` : `[[${file.path}]]`; - - event.dataTransfer.setData('text/plain', mdLink); - event.dataTransfer.setData('application/obsidian-grid-explorer-files', JSON.stringify([file.path])); + // 使用 Obsidian 內建的拖曳格式(obsidian:// URI) + const vaultName = this.app.vault.getName(); + const obsidianUri = `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodeURIComponent(file.path)}`; + + event.dataTransfer.setData('text/uri-list', obsidianUri); + event.dataTransfer.setData('text/plain', obsidianUri); event.dataTransfer.setData('application/obsidian-ge-stash', file.path); event.dataTransfer.effectAllowed = 'all'; @@ -1459,17 +1538,17 @@ export class ExplorerView extends ItemView { this.scheduleRender(); } + // 處理暫存區拖放 private async handleStashDrop(event: DragEvent) { try { const dt = event.dataTransfer; if (!dt) return; - // 自訂多檔案格式 - const filesDataString = dt.getData('application/obsidian-grid-explorer-files'); - if (filesDataString) { - const filePaths: string[] = JSON.parse(filesDataString); - this.addToStash(filePaths); + // 處理 obsidian:// URI 格式(單檔/多檔) + const obsidianPaths = await extractObsidianPathsFromDT(dt); + if (obsidianPaths.length > 0) { + this.addToStash(obsidianPaths); return; } @@ -1784,12 +1863,16 @@ export class ExplorerView extends ItemView { private handleDragOver(event: DragEvent) { event.preventDefault(); event.dataTransfer!.dropEffect = 'move'; - (event.target as HTMLElement).addClass('ge-dragover'); + // 使用 currentTarget 確保樣式加在 header 本身,而非內部子元素 + const header = event.currentTarget as HTMLElement; + if (header) header.addClass('ge-dragover'); } // 處理拖拽離開 private handleDragLeave(event: DragEvent) { - (event.target as HTMLElement).removeClass('ge-dragover'); + // 使用 currentTarget 確保從 header 本身移除樣式 + const header = event.currentTarget as HTMLElement; + if (header) header.removeClass('ge-dragover'); } // 處理拖放事件 @@ -1799,9 +1882,10 @@ export class ExplorerView extends ItemView { if (typeof (event as any).preventDefault === 'function') { event.preventDefault(); } - const targetEl = (event.target as HTMLElement | null); - if (targetEl && typeof (targetEl as any).removeClass === 'function') { - targetEl.removeClass('ge-dragover'); + // 確保從 header 本身移除樣式,而非子元素 + const header = event.currentTarget as HTMLElement | null; + if (header && typeof (header as any).removeClass === 'function') { + header.removeClass('ge-dragover'); } // 無有效 dataTransfer 則忽略 @@ -1816,15 +1900,17 @@ export class ExplorerView extends ItemView { // 處理多檔案拖放 private async handleMultiFileDrop(event: DragEvent, folderPath: string): Promise { - const filesDataString = event.dataTransfer?.getData('application/obsidian-grid-explorer-files'); - if (!filesDataString) return false; + if (!event.dataTransfer) return false; + + // 處理 obsidian:// URI 格式(單檔/多檔) + const obsidianPaths = await extractObsidianPathsFromDT(event.dataTransfer); + if (obsidianPaths.length === 0) return false; try { - const filePaths: string[] = JSON.parse(filesDataString); const destFolder = this.app.vault.getAbstractFileByPath(folderPath); if (!(destFolder instanceof TFolder)) return false; - for (const path of filePaths) { + for (const path of obsidianPaths) { await this.moveFileToFolder(path, folderPath); } return true; diff --git a/src/GridView.ts b/src/GridView.ts index d3456a1..6323d00 100644 --- a/src/GridView.ts +++ b/src/GridView.ts @@ -900,6 +900,7 @@ export class GridView extends ItemView { // 直接顯示圖片 const img = imageArea.createEl('img'); img.src = this.app.vault.getResourcePath(file); + img.draggable = false; imageArea.setAttribute('data-loaded', 'true'); } else if (isVideoFile(file)) { // 根據設定決定是否顯示影片縮圖 @@ -918,6 +919,7 @@ export class GridView extends ItemView { if (imageUrl) { const img = imageArea.createEl('img'); img.src = imageUrl; + img.draggable = false; imageArea.setAttribute('data-loaded', 'true'); } else { // 如果沒有圖片,移除圖片區域 @@ -1328,33 +1330,42 @@ export class GridView extends ItemView { const selectedFiles = this.getSelectedFiles(); let drag_filename = ''; - // 添加拖曳資料 + // 使用 Obsidian 內建的拖曳格式(obsidian:// URI) + const vaultName = this.app.vault.getName(); + if (selectedFiles.length > 1) { - // 如果多個檔案被選中,使用 files-menu - const fileList = selectedFiles.map(f => { - const isMedia = isMediaFile(f); - return isMedia ? `![[${f.path}]]` : `[[${f.path}]]`; - }).join('\n'); - event.dataTransfer?.setData('text/plain', fileList); + // 多檔案:建立多個 obsidian://open URI + const obsidianUris = selectedFiles.map(f => + `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodeURIComponent(f.path)}` + ); + + // 設定 text/uri-list + event.dataTransfer?.setData('text/uri-list', obsidianUris.join('\n')); + console.log(obsidianUris.join('\n')); - // 添加檔案路徑列表 - event.dataTransfer?.setData('application/obsidian-grid-explorer-files', - JSON.stringify(selectedFiles.map(f => f.path))); + // 設定 text/plain + const mdList = selectedFiles + .map(f => isMediaFile(f) ? `![[${f.path}]]` : `[[${f.path}]]`) + .join('\n'); + event.dataTransfer?.setData('text/plain', mdList); + + // 兼容舊版:提供 markdown 連結與舊自定義 MIME,供 main.ts 使用 + event.dataTransfer?.setData('application/obsidian-grid-explorer-files', JSON.stringify(selectedFiles.map(f => f.path))); drag_filename = `${selectedFiles.length} ${t('files')}`; } else { - // 如果只有單個檔案被選中,使用檔案路徑 - const isMedia = isMediaFile(file); - const mdLink = isMedia - ? `![[${file.path}]]` // 媒體檔案使用 ![[]] 格式 - : `[[${file.path}]]`; // 一般檔案使用 [[]] 格式 + // 單檔案:建立單一 obsidian://open URI + const obsidianUri = `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodeURIComponent(file.path)}`; + + // 設定為 text/uri-list + event.dataTransfer?.setData('text/uri-list', obsidianUri); - // 添加拖曳資料 + // 設定 text/plain + const mdLink = isMediaFile(file) ? `![[${file.path}]]` : `[[${file.path}]]`; event.dataTransfer?.setData('text/plain', mdLink); - // 添加檔案路徑列表 - event.dataTransfer?.setData('application/obsidian-grid-explorer-files', - JSON.stringify([file.path])); + // 兼容舊版:提供 markdown 連結與舊自定義 MIME,供 main.ts 使用 + event.dataTransfer?.setData('application/obsidian-grid-explorer-files', JSON.stringify([file.path])); drag_filename = file.basename; } diff --git a/src/dragUtils.ts b/src/dragUtils.ts new file mode 100644 index 0000000..2a27308 --- /dev/null +++ b/src/dragUtils.ts @@ -0,0 +1,71 @@ +/** + * 拖曳相關的工具函數 + * 用於處理 Obsidian URI 格式的拖曳操作 + */ + +// 從 obsidian://open?vault=...&file=... 解析出 vault 名稱與檔案路徑 +export function parseObsidianOpenUris(input: string): Array<{ vault?: string; file?: string }> { + const results: Array<{ vault?: string; file?: string }> = []; + if (!input) return results; + + // 可能是多行或連續字串,先嘗試按換行切分 + const chunks = input.split(/\r?\n/).filter(Boolean); + console.log(chunks); + const toScan = chunks.length ? chunks : [input]; + + for (const part of toScan) { + // 支援無分隔連續的多筆 obsidian://open + const re = /obsidian:\/\/open\?([\s\S]*?)(?=obsidian:\/\/open\?|$)/g; + let m: RegExpExecArray | null; + while ((m = re.exec(part)) !== null) { + const q = m[1]; + try { + const usp = new URLSearchParams(q); + const vault = usp.get('vault') || undefined; + const file = usp.get('file') ? decodeURIComponent(usp.get('file') as string) : undefined; + results.push({ vault, file }); + } catch { /* ignore */ } + } + } + return results; +} + +// 從 DataTransfer 嘗試萃取 obsidian://open URI 的 file 路徑(支援多筆) +export async function extractObsidianPathsFromDT(dt: DataTransfer | null): Promise { + if (!dt) return []; + + const texts: string[] = []; + // 1) items (getAsString) + if (dt.items) { + const items = Array.from(dt.items).filter(i => i.kind === 'string'); + await Promise.all(items.map(i => new Promise(resolve => { + try { + i.getAsString((s) => { if (s) texts.push(s); resolve(); }); + } catch { resolve(); } + }))); + } + // 2) text/uri-list + try { + const uriList = dt.getData('text/uri-list'); + if (uriList) texts.push(uriList); + } catch {} + // 3) text/plain + try { + const plain = dt.getData('text/plain'); + if (plain && plain.startsWith('obsidian://')) texts.push(plain); + } catch {} + + const vaultName = (window as any).app?.vault?.getName?.() as string | undefined; + const paths: string[] = []; + for (const t of texts) { + const entries = parseObsidianOpenUris(t); + for (const e of entries) { + if (!e.file) continue; + // 僅接受相同 vault(若有提供 vault 參數) + if (e.vault && vaultName && e.vault !== vaultName) continue; + paths.push(e.file); + } + } + // 去重 + return Array.from(new Set(paths)); +} diff --git a/src/main.ts b/src/main.ts index 399152b..3c5f349 100644 --- a/src/main.ts +++ b/src/main.ts @@ -522,7 +522,8 @@ export default class GridExplorerPlugin extends Plugin { canvasView.gridExplorerDropHandler = true; const canvasEl = canvasView.containerEl; - + if (!canvasEl) return; + const dragoverHandler = (evt: DragEvent) => { // 只處理來自 Grid Explorer 的檔案拖曳,以顯示正確的游標 if (evt.dataTransfer?.types.includes('application/obsidian-grid-explorer-files')) { @@ -541,27 +542,22 @@ export default class GridExplorerPlugin extends Plugin { evt.preventDefault(); evt.stopPropagation(); - let filePath: string | undefined; - + // 嘗試解析多檔路徑 const data = evt.dataTransfer.getData('application/obsidian-grid-explorer-files'); + let filePaths: string[] = []; try { - const paths = JSON.parse(data); - // 目前我們只支援單一檔案拖放 - if (Array.isArray(paths) && paths.length === 1) { - filePath = paths[0]; + const parsed = JSON.parse(data); + if (Array.isArray(parsed)) { + filePaths = parsed.filter((p: any) => typeof p === 'string'); + } else if (typeof parsed === 'string') { + filePaths = [parsed]; } } catch (e) { console.error("GridExplorer: Failed to parse drop data from GridExplorer.", e); } // 如果無法取得檔案路徑,則中止操作 - if (!filePath) { - return; - } - - const tfile = this.app.vault.getAbstractFileByPath(filePath); - if (!(tfile instanceof TFile)) { - console.warn('GridExplorer: Dropped item is not a TFile or could not be found.', filePath); + if (filePaths.length === 0) { return; } @@ -571,15 +567,31 @@ export default class GridExplorerPlugin extends Plugin { } const pos = canvas.posFromEvt(evt); + let currentPos = { ...pos } as { x: number; y: number }; - const newNode = canvas.createFileNode({ - file: tfile, - pos: pos, - size: { width: 400, height: 400 }, // 預設大小 - focus: true, // 建立後自動對焦 - }); + // 逐一建立節點,若多檔則位置做些微偏移 + for (let i = 0; i < filePaths.length; i++) { + const filePath = filePaths[i]; + const tfile = this.app.vault.getAbstractFileByPath(filePath); + if (!(tfile instanceof TFile)) { + console.warn('GridExplorer: Dropped item is not a TFile or could not be found.', filePath); + continue; + } + + const newNode = canvas.createFileNode({ + file: tfile, + pos: currentPos, + size: { width: 400, height: 400 }, // 預設大小 + focus: filePaths.length === 1 || i === filePaths.length - 1, // 單檔或最後一個檔案才自動對焦 + }); + canvas.addNode(newNode); + + // 如果有多個檔案,下一個節點位置向右下偏移 + if (filePaths.length > 1) { + currentPos = { x: currentPos.x + 50, y: currentPos.y + 50 }; + } + } - canvas.addNode(newNode); await canvas.requestSave(); }; diff --git a/src/renderFolder.ts b/src/renderFolder.ts index 549fc6e..c0d6141 100644 --- a/src/renderFolder.ts +++ b/src/renderFolder.ts @@ -1,6 +1,7 @@ -import { TFolder, TFile, Menu, Platform, setIcon, normalizePath, setTooltip } from 'obsidian'; +import { TFolder, TFile, normalizePath, Platform, setIcon, setTooltip, Menu } from 'obsidian'; import { GridView } from './GridView'; import { isFolderIgnored } from './fileUtils'; +import { extractObsidianPathsFromDT } from './dragUtils'; import { showFolderNoteSettingsModal } from './modal/folderNoteSettingsModal'; import { showFolderRenameModal } from './modal/folderRenameModal'; import { showFolderMoveModal } from './modal/folderMoveModal'; @@ -48,38 +49,53 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { // 移除視覺提示 container.removeClass('ge-dragover'); - // 獲取拖曳的檔案路徑列表 - const filesDataString = (event as any).dataTransfer?.getData('application/obsidian-grid-explorer-files'); - if (filesDataString) { + + // 處理從 Obsidian 檔案總管拖來的 obsidian://open URI(單檔/多檔) + const dt = (event as DragEvent).dataTransfer; + if (dt) { try { - // 解析檔案路徑列表 - const filePaths = JSON.parse(filesDataString); + const obsidianPaths = await extractObsidianPathsFromDT(dt); + if (obsidianPaths.length) { + for (const p of obsidianPaths) { + let resolved: TFile | null = null; - // 獲取當前資料夾路徑 - const folderPath = currentFolder.path; - if (!folderPath) return; + // 1) 直接以路徑查找 + const direct = gridView.app.vault.getAbstractFileByPath(p); + if (direct instanceof TFile) { + resolved = direct; + } - // 移動檔案 - for (const path of filePaths) { - const file = gridView.app.vault.getAbstractFileByPath(path); - if (file instanceof TFile) { - try { - // 計算新的檔案路徑 - const newPath = normalizePath(`${folderPath}/${file.name}`); - // 如果來源路徑和目標路徑相同,則跳過 - if (path === newPath) { - continue; + // 2) 若沒有副檔名,嘗試補 .md + if (!resolved && !p.includes('.')) { + const tryMd = normalizePath(`${p}.md`); + const f2 = gridView.app.vault.getAbstractFileByPath(tryMd); + if (f2 instanceof TFile) resolved = f2; + } + + // 3) 使用 Obsidian 的連結解析(支援不含副檔名與子資料夾) + if (!resolved) { + const srcPath = currentFolder.path || '/'; + const dest = (gridView.app.metadataCache as any).getFirstLinkpathDest?.(p, srcPath); + if (dest instanceof TFile) resolved = dest; + } + + if (resolved instanceof TFile) { + try { + const newPath = normalizePath(`${currentFolder.path}/${resolved.name}`); + if (resolved.path !== newPath) { + await gridView.app.fileManager.renameFile(resolved, newPath); + } + } catch (error) { + console.error(`An error occurred while moving the file ${p}:`, error); } - // 移動檔案 - await gridView.app.fileManager.renameFile(file, newPath); - } catch (error) { - console.error(`An error occurred while moving the file ${file.path}:`, error); + } else { + console.warn('[GridExplorer] Unable to resolve dragged obsidian file (container drop):', p); } } + return; } - return; - } catch (error) { - console.error('Error parsing dragged files data:', error); + } catch (e) { + console.error('Error handling obsidian:// URIs (container drop):', e); } } @@ -355,40 +371,57 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { // 移除視覺提示 folderItem.removeClass('ge-dragover'); - // 獲取拖曳的檔案路徑列表 - const filesDataString = (event as any).dataTransfer?.getData('application/obsidian-grid-explorer-files'); - if (filesDataString) { + // 處理從 Obsidian 檔案總管拖來的 obsidian://open URI(單檔/多檔) + const dt = (event as DragEvent).dataTransfer; + if (dt) { try { - // 解析檔案路徑列表 - const filePaths = JSON.parse(filesDataString); + const obsidianPaths = await extractObsidianPathsFromDT(dt); + if (obsidianPaths.length) { + const folderPath = (folderItem as any).dataset.folderPath; + if (!folderPath) return; + const folder = gridView.app.vault.getAbstractFileByPath(folderPath); + if (!(folder instanceof TFolder)) return; - // 獲取目標資料夾路徑 - const folderPath = (folderItem as any).dataset.folderPath; - if (!folderPath) return; + for (const p of obsidianPaths) { + let resolved: TFile | null = null; - // 獲取資料夾物件 - const folder = gridView.app.vault.getAbstractFileByPath(folderPath); - if (!(folder instanceof TFolder)) return; + // 1) 直接以路徑查找 + const direct = gridView.app.vault.getAbstractFileByPath(p); + if (direct instanceof TFile) { + resolved = direct; + } - // 移動檔案 - for (const path of filePaths) { - const file = gridView.app.vault.getAbstractFileByPath(path); - if (file instanceof TFile) { - try { - // 計算新的檔案路徑 - const newPath = normalizePath(`${folderPath}/${file.name}`); - // 移動檔案 - await gridView.app.fileManager.renameFile(file, newPath); - } catch (error) { - console.error(`An error occurred while moving the file ${file.path}:`, error); + // 2) 若沒有副檔名,嘗試補 .md + if (!resolved && !p.includes('.') ) { + const tryMd = normalizePath(`${p}.md`); + const f2 = gridView.app.vault.getAbstractFileByPath(tryMd); + if (f2 instanceof TFile) resolved = f2; + } + + // 3) 使用 Obsidian 的連結解析(支援不含副檔名與子資料夾) + if (!resolved) { + const srcPath = (folderItem as any).dataset.folderPath || '/'; + const dest = (gridView.app.metadataCache as any).getFirstLinkpathDest?.(p, srcPath); + if (dest instanceof TFile) resolved = dest; + } + + if (resolved instanceof TFile) { + try { + const newPath = normalizePath(`${folderPath}/${resolved.name}`); + if (resolved.path !== newPath) { + await gridView.app.fileManager.renameFile(resolved, newPath); + } + } catch (error) { + console.error(`An error occurred while moving the file ${p}:`, error); + } + } else { + console.warn('[GridExplorer] Unable to resolve dragged obsidian file:', p); } } + return; } - - return; - - } catch (error) { - console.error('Error parsing dragged files data:', error); + } catch (e) { + console.error('Error handling obsidian:// URIs:', e); } } diff --git a/src/renderModePath.ts b/src/renderModePath.ts index 3b9a49f..f58fc59 100644 --- a/src/renderModePath.ts +++ b/src/renderModePath.ts @@ -6,6 +6,8 @@ import { showFolderRenameModal } from './modal/folderRenameModal'; import { showFolderMoveModal } from './modal/folderMoveModal'; import { CustomModeModal } from './modal/customModeModal'; import { t } from './translations'; +import { parseObsidianOpenUris, extractObsidianPathsFromDT } from './dragUtils'; + export function renderModePath(gridView: GridView) { @@ -240,15 +242,37 @@ export function renderModePath(gridView: GridView) { const folder = gridView.app.vault.getAbstractFileByPath(path.path); if (!(folder instanceof TFolder)) return; - const filesData = event.dataTransfer?.getData('application/obsidian-grid-explorer-files'); - if (filesData) { + // 處理 obsidian:// URI 格式(單檔/多檔) + const obsidianPaths = await extractObsidianPathsFromDT(event.dataTransfer); + if (obsidianPaths.length > 0) { try { - const filePaths = JSON.parse(filesData); - for (const filePath of filePaths) { - const file = gridView.app.vault.getAbstractFileByPath(filePath); - if (file instanceof TFile) { - const newPath = normalizePath(`${path.path}/${file.name}`); - await gridView.app.fileManager.renameFile(file, newPath); + for (const filePath of obsidianPaths) { + let resolved: TFile | null = null; + + // 1) 直接以路徑查找 + const direct = gridView.app.vault.getAbstractFileByPath(filePath); + if (direct instanceof TFile) { + resolved = direct; + } + + // 2) 若沒有副檔名,嘗試補 .md + if (!resolved && !filePath.includes('.')) { + const tryMd = normalizePath(`${filePath}.md`); + const f2 = gridView.app.vault.getAbstractFileByPath(tryMd); + if (f2 instanceof TFile) resolved = f2; + } + + // 3) 使用 Obsidian 的連結解析 + if (!resolved) { + const dest = (gridView.app.metadataCache as any).getFirstLinkpathDest?.(filePath, path.path); + if (dest instanceof TFile) resolved = dest; + } + + if (resolved instanceof TFile) { + const newPath = normalizePath(`${path.path}/${resolved.name}`); + if (resolved.path !== newPath) { + await gridView.app.fileManager.renameFile(resolved, newPath); + } } } } catch (error) { diff --git a/versions.json b/versions.json index 09c2d0e..e62e57f 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "2.9.4": "1.1.0" + "2.9.5": "1.1.0" } \ No newline at end of file