This commit is contained in:
Devon22 2025-08-17 02:10:23 +08:00
parent 9b1906af0f
commit c757476d84
10 changed files with 381 additions and 144 deletions

View file

@ -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",

4
package-lock.json generated
View file

@ -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",

View file

@ -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": {

View file

@ -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<boolean> {
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;

View file

@ -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;
}

71
src/dragUtils.ts Normal file
View file

@ -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<string[]> {
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<void>(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));
}

View file

@ -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();
};

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -1,3 +1,3 @@
{
"2.9.4": "1.1.0"
"2.9.5": "1.1.0"
}