mirror of
https://github.com/devon22/obsidian-gridexplorer.git
synced 2026-07-22 05:38:16 +00:00
2.9.5
This commit is contained in:
parent
9b1906af0f
commit
c757476d84
10 changed files with 381 additions and 144 deletions
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
71
src/dragUtils.ts
Normal 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));
|
||||
}
|
||||
54
src/main.ts
54
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();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"2.9.4": "1.1.0"
|
||||
"2.9.5": "1.1.0"
|
||||
}
|
||||
Loading…
Reference in a new issue