This commit is contained in:
Devon22 2025-08-02 01:44:27 +08:00
parent 63ca91d3bd
commit 23dfa0f20b
12 changed files with 1833 additions and 1785 deletions

View file

@ -1,7 +1,7 @@
{
"id": "gridexplorer",
"name": "GridExplorer",
"version": "2.7.11",
"version": "2.7.12",
"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.7.11",
"version": "2.7.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gridexplorer",
"version": "2.7.11",
"version": "2.7.12",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",

View file

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

View file

@ -1,4 +1,4 @@
import { App, TFile, Notice } from 'obsidian';
import { App, TFile } from 'obsidian';
import GridExplorerPlugin from './main';
import { GridView } from './GridView';
import { isDocumentFile, isMediaFile } from './fileUtils';
@ -160,16 +160,6 @@ export class FileWatcher {
}
})
);
// 僅當切換到本 GridView最近檔案模式時才重新渲染
// this.plugin.registerEvent(
// this.app.workspace.on('active-leaf-change', (leaf) => {
// // 若切換到的 Leaf 正是此 GridView 的 Leaf且模式為 recent-files
// if (leaf === this.gridView.leaf && this.gridView.sourceMode === 'recent-files') {
// this.scheduleRender(2000);
// }
// })
// );
}
// 以 200ms 去抖動的方式排程 render避免短時間內大量重繪

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
import { TFile, TFolder, getFrontMatterInfo, App, Notice } from 'obsidian';
import { TFile, TFolder, getFrontMatterInfo, Notice } from 'obsidian';
import { GridView } from './GridView';
import { type GallerySettings } from './settings';
import { t } from './translations';

260
src/renderFiles.ts Normal file
View file

@ -0,0 +1,260 @@
import { TFile } from 'obsidian';
import { GridView } from './GridView';
import { isDocumentFile, isMediaFile, sortFiles, ignoredFiles, getFiles } from './fileUtils';
import { t } from './translations';
export async function renderFiles(gridView: GridView, container: HTMLElement) {
let loadingDiv: HTMLElement | null = null;
if (gridView.searchQuery || gridView.sourceMode === 'tasks') {
// 顯示搜尋中的提示
loadingDiv = container.createDiv({ text: t('searching'), cls: 'ge-loading-indicator' });
}
let files: TFile[] = [];
// 使用 Map 來記錄原始順序
let fileIndexMap = new Map<TFile, number>();
if (gridView.searchQuery) {
// 取得 vault 中所有支援的檔案
let allFiles: TFile[] = [];
if (gridView.searchAllFiles) {
// 全部檔案
allFiles = gridView.app.vault.getFiles().filter(file =>
isDocumentFile(file) || (isMediaFile(file) && gridView.searchMediaFiles)
);
} else {
// 當前位置檔案
allFiles = await getFiles(gridView, gridView.searchMediaFiles);
if (gridView.sourceMode === 'bookmarks') {
allFiles = allFiles.filter(file =>
isDocumentFile(file) || (isMediaFile(file) && gridView.searchMediaFiles)
);
// 使用 Map 來記錄原始順序
allFiles.forEach((file, index) => {
fileIndexMap.set(file, index);
});
} else if (gridView.sourceMode === 'search') {
allFiles = allFiles.filter(file =>
isDocumentFile(file) || (isMediaFile(file) && gridView.searchMediaFiles)
);
} else if (gridView.sourceMode === 'recent-files') {
// 搜尋"最近檔案"的當前位置時先作忽略檔案和只取前n筆
allFiles = ignoredFiles(allFiles, gridView).slice(0, gridView.plugin.settings.recentFilesCount);
} else if (gridView.sourceMode.startsWith('custom-')) {
// 使用 Map 來記錄原始順序
allFiles.forEach((file, index) => {
fileIndexMap.set(file, index);
});
}
}
// 根據搜尋關鍵字進行過濾(不分大小寫)
const searchTerms = gridView.searchQuery.toLowerCase().split(/\s+/).filter(term => term.trim() !== '');
// 分離標籤搜尋、一般搜尋和連結搜尋
const tagTerms: string[] = [];
const linkTerms: string[] = [];
const normalTerms: string[] = [];
for (const term of searchTerms) {
if (term.startsWith('#')) {
tagTerms.push(term.substring(1));
} else if (term.startsWith('[[') && term.endsWith(']]')) {
linkTerms.push(term.slice(2, -2)); // 移除 [[ 和 ]]
} else {
normalTerms.push(term);
}
}
// 檢查是否有 Obsidian 連結格式 [[ ]]
let linkTargetFileSets: Set<string>[] = [];
const hasLinkSearch = linkTerms.length > 0;
if (hasLinkSearch) {
for (const linkTarget of linkTerms) {
// 尋找目標檔案
let targetFile = gridView.app.vault.getAbstractFileByPath(linkTarget + '.md');
if (!targetFile) {
targetFile = gridView.app.metadataCache.getFirstLinkpathDest(linkTarget, '');
}
if (targetFile && 'extension' in targetFile) {
// 使用 Obsidian 的 getBacklinksForFile API
const backlinks = (gridView.app.metadataCache as any).getBacklinksForFile(targetFile);
const currentLinkFiles = new Set<string>();
if (backlinks) {
// 收集所有反向連結的檔案路徑
for (const [filePath, links] of backlinks.data.entries()) {
currentLinkFiles.add(filePath);
}
}
linkTargetFileSets.push(currentLinkFiles);
} else {
// 如果找不到目標檔案,加入空集合
linkTargetFileSets.push(new Set<string>());
}
}
}
// 使用 Promise.all 來非同步地讀取所有檔案內容,順序可能會跟之前不同
await Promise.all(
allFiles.map(async file => {
// 檢查連結搜尋條件:檔案必須在所有連結的反向連結中
if (hasLinkSearch) {
const isInAllLinkTargets = linkTargetFileSets.every(linkSet => linkSet.has(file.path));
if (!isInAllLinkTargets) {
return; // 如果檔案不在所有連結的反向連結中,直接跳過
}
}
const fileName = file.name.toLowerCase();
let matchesNormalTerms = true;
let matchesTags = true;
// 檢查一般搜尋詞
if (normalTerms.length > 0) {
if (gridView.searchFilesNameOnly) {
// 僅檢查檔名
matchesNormalTerms = normalTerms.every(term => fileName.includes(term));
} else {
// 檢查每個一般搜尋字串是否存在於檔名或(若為 Markdown檔案內容中
matchesNormalTerms = true;
let contentLower: string | null = null;
for (const term of normalTerms) {
if (fileName.includes(term)) {
continue; // 此關鍵字已在檔名中找到
}
if (file.extension === 'md') {
// 延遲讀取內容,避免不必要的 IO
if (contentLower === null) {
contentLower = (await gridView.app.vault.cachedRead(file)).toLowerCase();
}
if (contentLower.includes(term)) {
continue; // 此關鍵字在內容中找到
}
}
// 若此關鍵字既不在檔名也不在內容中,則不符合
matchesNormalTerms = false;
break;
}
}
}
// 檢查標籤搜尋詞
if (tagTerms.length > 0) {
if (file.extension !== 'md') {
matchesTags = false; // 非 Markdown 檔案不能匹配標籤
} else {
const fileCache = gridView.app.metadataCache.getFileCache(file);
matchesTags = false;
if (fileCache) {
const collectedTags: string[] = [];
// 內文標籤
if (Array.isArray(fileCache.tags)) {
for (const t of fileCache.tags) {
if (t && t.tag) {
const clean = t.tag.toLowerCase().replace(/^#/, '');
collectedTags.push(...clean.split(/\s+/).filter(st => st.trim() !== ''));
}
}
}
// frontmatter 標籤
if (fileCache.frontmatter && fileCache.frontmatter.tags) {
const fmTags = fileCache.frontmatter.tags;
if (typeof fmTags === 'string') {
collectedTags.push(
...fmTags.split(/[,\s]+/)
.map(t => t.toLowerCase().replace(/^#/, ''))
.filter(t => t.trim() !== '')
);
} else if (Array.isArray(fmTags)) {
for (const t of fmTags) {
if (typeof t === 'string') {
const clean = t.toLowerCase().replace(/^#/, '');
collectedTags.push(...clean.split(/\s+/).filter(st => st.trim() !== ''));
}
}
}
}
matchesTags = tagTerms.every(tag => collectedTags.includes(tag));
}
}
}
// 只有當所有條件都符合時才加入結果
if (matchesNormalTerms && matchesTags) {
files.push(file);
}
})
);
// 排序檔案
if (gridView.searchAllFiles) {
// 搜尋所有檔案時
files = sortFiles(files, gridView);
} else {
// 非搜尋所有檔案時
if (gridView.sourceMode === 'bookmarks') {
// 保持原始順序
files.sort((a, b) => {
const indexA = fileIndexMap.get(a) ?? Number.MAX_SAFE_INTEGER;
const indexB = fileIndexMap.get(b) ?? Number.MAX_SAFE_INTEGER;
return indexA - indexB;
});
} else if (gridView.sourceMode === 'recent-files') {
// 臨時的排序類型
const sortType = gridView.sortType;
gridView.sortType = 'mtime-desc';
files = sortFiles(files, gridView);
gridView.sortType = sortType;
} else if (gridView.sourceMode === 'random-note') {
// 臨時的排序類型
const sortType = gridView.sortType;
gridView.sortType = 'random';
files = sortFiles(files, gridView);
gridView.sortType = sortType;
} else if (gridView.sourceMode.startsWith('custom-')) {
// 保持原始順序
files.sort((a, b) => {
const indexA = fileIndexMap.get(a) ?? Number.MAX_SAFE_INTEGER;
const indexB = fileIndexMap.get(b) ?? Number.MAX_SAFE_INTEGER;
return indexA - indexB;
});
} else {
files = sortFiles(files, gridView);
}
}
// 忽略檔案
files = ignoredFiles(files, gridView);
} else {
// 無搜尋關鍵字的情況
files = await getFiles(gridView, gridView.randomNoteIncludeMedia);
// 忽略檔案
files = ignoredFiles(files, gridView)
// 最近檔案模式只取前n筆
if (gridView.sourceMode === 'recent-files') {
files = files.slice(0, gridView.plugin.settings.recentFilesCount);
}
// 隨機筆記模式只取前n筆
if (gridView.sourceMode === 'random-note') {
files = files.slice(0, gridView.plugin.settings.randomNoteCount);
}
}
if (loadingDiv) {
loadingDiv.remove();
}
return files;
}

441
src/renderFolder.ts Normal file
View file

@ -0,0 +1,441 @@
import { TFolder, TFile, Menu, Platform, setIcon, normalizePath, setTooltip } from 'obsidian';
import { GridView } from './GridView';
import { isFolderIgnored } from './fileUtils';
import { showFolderNoteSettingsModal } from './modal/folderNoteSettingsModal';
import { showFolderRenameModal } from './modal/folderRenameModal';
import { moveFolderSuggestModal } from './modal/moveFolderSuggestModal';
import { t } from './translations';
export async function renderFolder(gridView: GridView, container: HTMLElement) {
// 如果是資料夾模式,先顯示所有子資料夾
if (gridView.sourceMode === 'folder' && gridView.searchQuery === '') {
const currentFolder = gridView.app.vault.getAbstractFileByPath(gridView.sourcePath || '/');
if (currentFolder instanceof TFolder) {
// 為網格容器添加拖曳目標功能(當前資料夾)
if(Platform.isDesktop) {
container.addEventListener('dragover', (event) => {
// 如果拖曳目標是資料夾項目,則不處理
if ((event.target as HTMLElement).closest('.ge-folder-item')) {
return;
}
// 防止預設行為以允許放置
event.preventDefault();
// 設定拖曳效果為移動
(event as any).dataTransfer!.dropEffect = 'move';
// 顯示可放置的視覺提示
container.addClass('ge-dragover');
}, true); // 使用捕獲階段
container.addEventListener('dragleave', (event) => {
// 如果移入的是子元素,則不處理
if (container.contains(event.relatedTarget as Node)) {
return;
}
// 移除視覺提示
container.removeClass('ge-dragover');
});
container.addEventListener('drop', async (event) => {
// 如果拖曳目標是資料夾項目,則不處理
if ((event.target as HTMLElement).closest('.ge-folder-item')) {
return;
}
// 防止預設行為
event.preventDefault();
// 移除視覺提示
container.removeClass('ge-dragover');
// 獲取拖曳的檔案路徑列表
const filesDataString = (event as any).dataTransfer?.getData('application/obsidian-grid-explorer-files');
if (filesDataString) {
try {
// 解析檔案路徑列表
const filePaths = JSON.parse(filesDataString);
// 獲取當前資料夾路徑
const folderPath = currentFolder.path;
if (!folderPath) return;
// 移動檔案
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;
}
// 移動檔案
await gridView.app.fileManager.renameFile(file, newPath);
} catch (error) {
console.error(`An error occurred while moving the file ${file.path}:`, error);
}
}
}
return;
} catch (error) {
console.error('Error parsing dragged files data:', error);
}
}
// 如果沒有檔案路徑列表,則使用檔案路徑
const filePath = (event as any).dataTransfer?.getData('text/plain');
if (!filePath) return;
const cleanedFilePath = filePath.replace(/!?\[\[(.*?)\]\]/, '$1');
// 獲取檔案和資料夾物件
const file = gridView.app.vault.getAbstractFileByPath(cleanedFilePath);
if (file instanceof TFile) {
try {
// 計算新的檔案路徑
const newPath = normalizePath(`${currentFolder.path}/${file.name}`);
// 如果來源路徑和目標路徑相同,則不執行移動
if (file.path !== newPath) {
// 移動檔案
await gridView.app.fileManager.renameFile(file, newPath);
}
} catch (error) {
console.error('An error occurred while moving the file:', error);
}
}
});
}
// 顯示子資料夾
const subfolders = currentFolder.children
.filter(child => {
// 如果不是資料夾,則不顯示
if (!(child instanceof TFolder)) return false;
// 使用 isFolderIgnored 函數檢查是否應該忽略此資料夾
return !isFolderIgnored(
child,
gridView.plugin.settings.ignoredFolders,
gridView.plugin.settings.ignoredFolderPatterns,
gridView.showIgnoredFolders
);
})
.sort((a, b) => a.name.localeCompare(b.name));
for (const folder of subfolders) {
const folderEl = container.createDiv('ge-grid-item ge-folder-item');
gridView.gridItems.push(folderEl); // 添加到網格項目數組
// 設置資料夾路徑屬性,用於拖曳功能
folderEl.dataset.folderPath = folder.path;
const contentArea = folderEl.createDiv('ge-content-area');
const titleContainer = contentArea.createDiv('ge-title-container');
const customFolderIcon = gridView.plugin.settings.customFolderIcon;
titleContainer.createEl('span', { cls: 'ge-title', text: `${customFolderIcon} ${folder.name}`.trim() });
setTooltip(folderEl, folder.name,{ placement: gridView.cardLayout === 'vertical' ? 'bottom' : 'right' });
// 檢查同名筆記是否存在
const notePath = `${folder.path}/${folder.name}.md`;
const noteFile = gridView.app.vault.getAbstractFileByPath(notePath);
if (noteFile instanceof TFile) {
// 使用 span 代替 button只顯示圖示
const noteIcon = titleContainer.createEl('span', {
cls: 'ge-foldernote-button'
});
setIcon(noteIcon, 'panel-left-open');
// 點擊圖示時開啟同名筆記
noteIcon.addEventListener('click', (e) => {
e.stopPropagation(); // 防止觸發資料夾的點擊事件
gridView.app.workspace.getLeaf().openFile(noteFile);
});
// 根據同名筆記設置背景色
const metadata = gridView.app.metadataCache.getFileCache(noteFile)?.frontmatter;
const colorValue = metadata?.color;
if (colorValue) {
// 依顏色名稱加入對應的樣式類別
folderEl.addClass(`ge-folder-color-${colorValue}`);
}
const iconValue = metadata?.icon;
if (iconValue) {
// 修改原本的title文字
const title = folderEl.querySelector('.ge-title');
if (title) {
title.textContent = `${iconValue} ${folder.name}`;
}
}
}
// 點擊時進入子資料夾
folderEl.addEventListener('click', (event) => {
if (event.ctrlKey || event.metaKey) {
event.preventDefault();
event.stopPropagation();
openFolderInNewView(gridView, folder.path);
} else {
gridView.setSource('folder', folder.path, true);
gridView.clearSelection();
}
});
// 添加右鍵選單
folderEl.addEventListener('contextmenu', (event) => {
event.preventDefault();
const menu = new Menu();
//在新網格視圖開啟
menu.addItem((item) => {
item
.setTitle(t('open_in_new_grid_view'))
.setIcon('grid')
.onClick(() => {
openFolderInNewView(gridView, folder.path);
});
});
menu.addSeparator();
// 檢查同名筆記是否存在
const notePath = `${folder.path}/${folder.name}.md`;
let noteFile = gridView.app.vault.getAbstractFileByPath(notePath);
if (noteFile instanceof TFile) {
//打開資料夾筆記
menu.addItem((item) => {
item
.setTitle(t('open_folder_note'))
.setIcon('panel-left-open')
.onClick(() => {
gridView.app.workspace.getLeaf().openFile(noteFile);
});
});
//編輯資料夾筆記設定
menu.addItem((item) => {
item
.setTitle(t('edit_folder_note_settings'))
.setIcon('settings-2')
.onClick(() => {
if (folder instanceof TFolder) {
showFolderNoteSettingsModal(gridView.app, gridView.plugin, folder, gridView);
}
});
});
//刪除資料夾筆記
menu.addItem((item) => {
item
.setTitle(t('delete_folder_note'))
.setIcon('folder-x')
.onClick(() => {
gridView.app.fileManager.trashFile(noteFile as TFile);
});
});
} else {
//建立Folder note
menu.addItem((item) => {
item
.setTitle(t('create_folder_note'))
.setIcon('file-cog')
.onClick(() => {
if (folder instanceof TFolder) {
showFolderNoteSettingsModal(gridView.app, gridView.plugin, folder, gridView);
}
});
});
}
menu.addSeparator();
if (!gridView.plugin.settings.ignoredFolders.includes(folder.path)) {
//加入"忽略此資料夾"選項
menu.addItem((item) => {
item
.setTitle(t('ignore_folder'))
.setIcon('folder-x')
.onClick(() => {
gridView.plugin.settings.ignoredFolders.push(folder.path);
gridView.plugin.saveSettings();
});
});
} else {
//加入"取消忽略此資料夾"選項
menu.addItem((item) => {
item
.setTitle(t('unignore_folder'))
.setIcon('folder-up')
.onClick(() => {
gridView.plugin.settings.ignoredFolders = gridView.plugin.settings.ignoredFolders.filter((path) => path !== folder.path);
gridView.plugin.saveSettings();
});
});
}
// 搬移資料夾
menu.addItem((item) => {
item
.setTitle(t('move_folder'))
.setIcon('folder-cog')
.onClick(() => {
if (folder instanceof TFolder) {
new moveFolderSuggestModal(gridView.plugin, folder, gridView).open();
}
});
});
// 重新命名資料夾
menu.addItem((item) => {
item
.setTitle(t('rename_folder'))
.setIcon('file-cog')
.onClick(() => {
if (folder instanceof TFolder) {
showFolderRenameModal(gridView.app, gridView.plugin, folder, gridView);
}
});
});
// 刪除資料夾
menu.addItem((item) => {
(item as any).setWarning(true);
item
.setTitle(t('delete_folder'))
.setIcon('trash')
.onClick(async () => {
if (folder instanceof TFolder) {
await gridView.app.fileManager.trashFile(folder);
// 重新渲染視圖
setTimeout(() => {
gridView.render();
}, 100);
}
});
});
menu.showAtMouseEvent(event);
});
}
// 資料夾渲染完插入 break僅當有資料夾
if (subfolders.length > 0) {
container.createDiv('ge-break');
}
}
}
// 為資料夾項目添加拖曳目標功能
if(Platform.isDesktop) {
const folderItems = gridView.containerEl.querySelectorAll('.ge-folder-item');
folderItems.forEach(folderItem => {
folderItem.addEventListener('dragover', (event) => {
// 防止預設行為以允許放置
event.preventDefault();
// 設定拖曳效果為移動
(event as any).dataTransfer!.dropEffect = 'move';
// 顯示可放置的視覺提示
folderItem.addClass('ge-dragover');
});
folderItem.addEventListener('dragleave', () => {
// 移除視覺提示
folderItem.removeClass('ge-dragover');
});
folderItem.addEventListener('drop', async (event) => {
// 防止預設行為
event.preventDefault();
// 移除視覺提示
folderItem.removeClass('ge-dragover');
// 獲取拖曳的檔案路徑列表
const filesDataString = (event as any).dataTransfer?.getData('application/obsidian-grid-explorer-files');
if (filesDataString) {
try {
// 解析檔案路徑列表
const filePaths = JSON.parse(filesDataString);
// 獲取目標資料夾路徑
const folderPath = (folderItem as any).dataset.folderPath;
if (!folderPath) return;
// 獲取資料夾物件
const folder = gridView.app.vault.getAbstractFileByPath(folderPath);
if (!(folder instanceof TFolder)) return;
// 移動檔案
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);
}
}
}
return;
} catch (error) {
console.error('Error parsing dragged files data:', error);
}
}
// 如果沒有檔案路徑列表,則使用檔案路徑
const filePath = (event as any).dataTransfer?.getData('text/plain');
if (!filePath) return;
const cleanedFilePath = filePath.replace(/!?\[\[(.*?)\]\]/, '$1');
// 獲取目標資料夾路徑
const folderPath = (folderItem as any).dataset.folderPath;
if (!folderPath) return;
// 獲取檔案和資料夾物件
const file = gridView.app.vault.getAbstractFileByPath(cleanedFilePath);
const folder = gridView.app.vault.getAbstractFileByPath(folderPath);
if (file instanceof TFile && folder instanceof TFolder) {
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:', error);
}
}
});
});
}
}
// 在新視窗中開啟資料夾
function openFolderInNewView(gridview: GridView, folderPath: string) {
const { workspace } = gridview.app;
let leaf = null;
workspace.getLeavesOfType('grid-view');
switch (gridview.plugin.settings.defaultOpenLocation) {
case 'left':
leaf = workspace.getLeftLeaf(false);
break;
case 'right':
leaf = workspace.getRightLeaf(false);
break;
case 'tab':
default:
leaf = workspace.getLeaf('tab');
break;
}
if (!leaf) {
// 如果無法獲取指定位置的 leaf則回退到新分頁
leaf = workspace.getLeaf('tab');
}
leaf.setViewState({ type: 'grid-view', active: true });
// 設定資料來源
if (leaf.view instanceof GridView) {
leaf.view.setSource('folder', folderPath);
}
// 確保視圖是活躍的
workspace.revealLeaf(leaf);
}

473
src/renderHeaderButton.ts Normal file
View file

@ -0,0 +1,473 @@
import { Menu, setIcon, TFile, Notice } from 'obsidian';
import { GridView } from './GridView';
import { showFolderSelectionModal } from './modal/folderSelectionModal';
import { showSearchModal } from './modal/searchModal';
import { ShortcutSelectionModal } from './modal/shortcutSelectionModal';
import { t } from './translations';
export function renderHeaderButton(gridView: GridView) {
// 創建頂部按鈕區域
const headerButtonsDiv = gridView.containerEl.createDiv('ge-header-buttons');
// 為頂部按鈕區域添加點擊事件,點擊後網格容器捲動到最頂部
headerButtonsDiv.addEventListener('click', (event: MouseEvent) => {
// 只有當點擊的是頂部按鈕區域本身(而不是其中的按鈕)時才觸發捲動
if (event.target === headerButtonsDiv) {
event.preventDefault();
// 取得網格容器
const gridContainer = gridView.containerEl.querySelector('.ge-grid-container');
if (gridContainer) {
gridContainer.scrollTo({
top: 0,
behavior: 'smooth'
});
}
}
});
// 添加回上一步按鈕
const backButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('back') } });
setIcon(backButton, 'arrow-left');
backButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
if (gridView.searchQuery !== '') {
gridView.searchQuery = '';
gridView.app.workspace.requestSaveLayout();
gridView.render();
return;
}
// 如果有歷史記錄
if (gridView.recentSources.length > 0) {
// 取得最近一筆歷史記錄
const lastSource = JSON.parse(gridView.recentSources[0]);
gridView.recentSources.shift(); // 從歷史記錄中移除
// 設定來源(不記錄到歷史)
gridView.setSource(
lastSource.mode,
lastSource.path || '',
true, // 重設捲動位置
false // 不記錄到歷史
);
}
});
// 添加右鍵選單支援
backButton.addEventListener('contextmenu', (event) => {
// 只有在有歷史記錄時才顯示右鍵選單
if (gridView.recentSources.length > 0) {
event.preventDefault();
const menu = new Menu();
// 添加歷史記錄
gridView.recentSources.forEach((sourceInfoStr, index) => {
try {
const sourceInfo = JSON.parse(sourceInfoStr);
const { mode, path } = sourceInfo;
// 根據模式顯示圖示和文字
let displayText = '';
let icon = '';
switch (mode) {
case 'folder':
displayText = path || '/';
icon = 'folder';
break;
case 'bookmarks':
displayText = t('bookmarks_mode');
icon = 'bookmark';
break;
case 'search':
displayText = t('search_results');
icon = 'search';
break;
case 'backlinks':
displayText = t('backlinks_mode');
icon = 'links-coming-in';
break;
case 'outgoinglinks':
displayText = t('outgoinglinks_mode');
icon = 'links-going-out';
break;
case 'all-files':
displayText = t('all_files_mode');
icon = 'book-text';
break;
case 'recent-files':
displayText = t('recent_files_mode');
icon = 'calendar-days';
break;
case 'random-note':
displayText = t('random_note_mode');
icon = 'dice';
break;
case 'tasks':
displayText = t('tasks_mode');
icon = 'square-check-big';
break;
default:
if (mode.startsWith('custom-')) {
const customMode = gridView.plugin.settings.customModes.find(m => m.internalName === mode);
displayText = customMode ? customMode.displayName : t('custom_mode');
icon = 'puzzle';
} else {
displayText = mode;
icon = 'grid';
}
}
// 添加歷史記錄到選單
menu.addItem((item) => {
item
.setTitle(`${displayText}`)
.setIcon(`${icon}`)
.onClick(() => {
// 找出當前點擊的紀錄索引
const clickedIndex = gridView.recentSources.findIndex(source => {
const parsed = JSON.parse(source);
return parsed.mode === mode && parsed.path === path;
});
// 如果找到點擊的紀錄,清除它之上的紀錄
if (clickedIndex !== -1) {
gridView.recentSources = gridView.recentSources.slice(clickedIndex + 1);
}
gridView.setSource(mode, path, true, false);
});
});
} catch (error) {
console.error('Failed to parse source info:', error);
}
});
// 顯示歷史選單
menu.showAtMouseEvent(event);
}
});
// 添加新增筆記按鈕
const newNoteButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('new_note') } });
setIcon(newNoteButton, 'square-pen');
newNoteButton.addEventListener('click', (event) => {
event.preventDefault();
const menu = new Menu();
// 新增筆記
menu.addItem((item) => {
item
.setTitle(t('new_note'))
.setIcon('square-pen')
.onClick(async () => {
let newFileName = `${t('untitled')}.md`;
let newFilePath = !gridView.sourcePath || gridView.sourcePath === '/' ? newFileName : `${gridView.sourcePath}/${newFileName}`;
// 檢查檔案是否已存在,如果存在則遞增編號
let counter = 1;
while (gridView.app.vault.getAbstractFileByPath(newFilePath)) {
newFileName = `${t('untitled')} ${counter}.md`;
newFilePath = !gridView.sourcePath || gridView.sourcePath === '/' ? newFileName : `${gridView.sourcePath}/${newFileName}`;
counter++;
}
try {
// 建立新筆記
const newFile = await gridView.app.vault.create(newFilePath, '');
// 開啟新筆記
await gridView.app.workspace.getLeaf().openFile(newFile);
} catch (error) {
console.error('An error occurred while creating a new note:', error);
}
});
});
// 新增資料夾
menu.addItem((item) => {
item.setTitle(t('new_folder'))
.setIcon('folder')
.onClick(async () => {
let newFolderName = `${t('untitled')}`;
let newFolderPath = !gridView.sourcePath || gridView.sourcePath === '/' ? newFolderName : `${gridView.sourcePath}/${newFolderName}`;
// 檢查資料夾是否已存在,如果存在則遞增編號
let counter = 1;
while (gridView.app.vault.getAbstractFileByPath(newFolderPath)) {
newFolderName = `${t('untitled')} ${counter}`;
newFolderPath = !gridView.sourcePath || gridView.sourcePath === '/' ? newFolderName : `${gridView.sourcePath}/${newFolderName}`;
counter++;
}
try {
// 建立新資料夾
await gridView.app.vault.createFolder(newFolderPath);
gridView.render(false);
} catch (error) {
console.error('An error occurred while creating a new folder:', error);
}
});
});
// 新增畫布
menu.addItem((item) => {
item.setTitle(t('new_canvas'))
.setIcon('layout-dashboard')
.onClick(async () => {
let newFileName = `${t('untitled')}.canvas`;
let newFilePath = !gridView.sourcePath || gridView.sourcePath === '/' ? newFileName : `${gridView.sourcePath}/${newFileName}`;
// 檢查檔案是否已存在,如果存在則遞增編號
let counter = 1;
while (gridView.app.vault.getAbstractFileByPath(newFilePath)) {
newFileName = `${t('untitled')} ${counter}.canvas`;
newFilePath = !gridView.sourcePath || gridView.sourcePath === '/' ? newFileName : `${gridView.sourcePath}/${newFileName}`;
counter++;
}
try {
// 建立新筆記
const newFile = await gridView.app.vault.create(newFilePath, '');
// 開啟新筆記
await gridView.app.workspace.getLeaf().openFile(newFile);
} catch (error) {
console.error('An error occurred while creating a new canvas:', error);
}
});
});
// 新增捷徑
menu.addItem((item) => {
item.setTitle(t('new_shortcut'))
.setIcon('shuffle')
.onClick(async () => {
const modal = new ShortcutSelectionModal(gridView.app, gridView.plugin, async (option) => {
await createShortcut(gridView, option);
});
modal.open();
});
});
menu.showAtMouseEvent(event);
});
// 添加重新選擇資料夾按鈕
const reselectButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('reselect') } });
reselectButton.addEventListener('click', () => {
showFolderSelectionModal(gridView.app, gridView.plugin, gridView, reselectButton);
});
setIcon(reselectButton, "grid");
// 添加重新整理按鈕
const refreshButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('refresh') } });
refreshButton.addEventListener('click', () => {
if (gridView.sortType === 'random') {
gridView.clearSelection();
}
gridView.render();
});
setIcon(refreshButton, 'refresh-ccw');
// 添加搜尋按鈕
const searchButtonContainer = headerButtonsDiv.createDiv('ge-search-button-container');
const searchButton = searchButtonContainer.createEl('button', {
cls: 'search-button',
attr: { 'aria-label': t('search') }
});
setIcon(searchButton, 'search');
searchButton.addEventListener('click', () => {
showSearchModal(gridView.app, gridView, '', searchButton);
});
// 如果有搜尋關鍵字,顯示搜尋文字和取消按鈕
if (gridView.searchQuery) {
searchButton.style.display = 'none';
const searchTextContainer = searchButtonContainer.createDiv('ge-search-text-container');
// 創建搜尋文字
const searchText = searchTextContainer.createEl('span', { cls: 'ge-search-text', text: gridView.searchQuery });
// 讓搜尋文字可點選
searchText.style.cursor = 'pointer';
searchText.addEventListener('click', () => {
showSearchModal(gridView.app, gridView, gridView.searchQuery, searchText);
});
// 創建取消按鈕
const clearButton = searchTextContainer.createDiv('ge-clear-button');
setIcon(clearButton, 'x');
clearButton.addEventListener('click', (e) => {
e.stopPropagation(); // 防止觸發搜尋文字的點擊事件
gridView.searchQuery = '';
gridView.clearSelection();
gridView.app.workspace.requestSaveLayout();
gridView.render();
});
}
// 更多選項按鈕
const menu = new Menu();
menu.addItem((item) => {
item
.setTitle(t('open_new_grid_view'))
.setIcon('grid')
.onClick(() => {
const { workspace } = gridView.app;
let leaf = null;
workspace.getLeavesOfType('grid-view');
switch (gridView.plugin.settings.defaultOpenLocation) {
case 'left':
leaf = workspace.getLeftLeaf(false);
break;
case 'right':
leaf = workspace.getRightLeaf(false);
break;
case 'tab':
default:
leaf = workspace.getLeaf('tab');
break;
}
if (!leaf) {
// 如果無法獲取指定位置的 leaf則回退到新分頁
leaf = workspace.getLeaf('tab');
}
leaf.setViewState({ type: 'grid-view', active: true });
// 設定資料來源
if (leaf.view instanceof GridView) {
leaf.view.setSource('folder', '/');
}
// 確保視圖是活躍的
workspace.revealLeaf(leaf);
});
});
menu.addSeparator();
// 直向卡片切換
menu.addItem((item) => {
item.setTitle(t('vertical_card'))
.setIcon('layout')
.setChecked(gridView.baseCardLayout === 'vertical')
.onClick(() => {
gridView.baseCardLayout = gridView.baseCardLayout === 'vertical' ? 'horizontal' : 'vertical';
gridView.cardLayout = gridView.baseCardLayout;
gridView.app.workspace.requestSaveLayout();
gridView.render();
});
});
// 最小化模式選項
menu.addItem((item) => {
item
.setTitle(t('min_mode'))
.setIcon('minimize-2')
.setChecked(gridView.minMode)
.onClick(() => {
gridView.minMode = !gridView.minMode;
gridView.app.workspace.requestSaveLayout();
gridView.render();
});
});
// 顯示日期分隔器
if (gridView.plugin.settings.dateDividerMode !== 'none') {
menu.addItem((item) => {
item
.setTitle(t('show_date_dividers'))
.setIcon('calendar')
.setChecked(gridView.showDateDividers)
.onClick(() => {
gridView.showDateDividers = !gridView.showDateDividers;
gridView.app.workspace.requestSaveLayout();
gridView.render();
});
});
}
// 顯示筆記標籤
menu.addItem((item) => {
item
.setTitle(t('show_note_tags'))
.setIcon('tag')
.setChecked(gridView.showNoteTags)
.onClick(() => {
gridView.showNoteTags = !gridView.showNoteTags;
gridView.app.workspace.requestSaveLayout();
gridView.render();
});
});
// 顯示忽略資料夾選項
menu.addItem((item) => {
item
.setTitle(t('show_ignored_folders'))
.setIcon('folder-open-dot')
.setChecked(gridView.showIgnoredFolders)
.onClick(() => {
gridView.showIgnoredFolders = !gridView.showIgnoredFolders;
gridView.app.workspace.requestSaveLayout();
gridView.render();
});
});
menu.addSeparator();
menu.addItem((item) => {
item
.setTitle(t('open_settings'))
.setIcon('settings')
.onClick(() => {
// 打開插件設定頁面
(gridView.app as any).setting.open();
(gridView.app as any).setting.openTabById(gridView.plugin.manifest.id);
});
});
if (gridView.searchQuery === '') {
const moreOptionsButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('more_options') } });
setIcon(moreOptionsButton, 'ellipsis-vertical');
moreOptionsButton.addEventListener('click', (event) => {
menu.showAtMouseEvent(event);
});
}
headerButtonsDiv.addEventListener('contextmenu', (event) => {
if (event.target === headerButtonsDiv) {
event.preventDefault();
menu.showAtMouseEvent(event);
}
});
}
// 創建捷徑檔案
async function createShortcut(gridView: GridView, option: { type: 'mode' | 'folder' | 'file'; value: string; display: string; }) {
try {
// 生成不重複的檔案名稱
let counter = 0;
let shortcutName = `${option.display}`;
let newPath = `${shortcutName}.md`;
while (gridView.app.vault.getAbstractFileByPath(newPath)) {
counter++;
shortcutName = `${option.display} ${counter}`;
newPath = `${shortcutName}.md`;
}
// 創建新檔案
const newFile = await gridView.app.vault.create(newPath, '');
// 使用 processFrontMatter 來更新 frontmatter
await gridView.app.fileManager.processFrontMatter(newFile, (frontmatter: any) => {
if (option.type === 'mode') {
frontmatter.type = 'mode';
frontmatter.redirect = option.value;
} else if (option.type === 'folder') {
frontmatter.type = 'folder';
frontmatter.redirect = option.value;
} else if (option.type === 'file') {
const link = gridView.app.fileManager.generateMarkdownLink(
gridView.app.vault.getAbstractFileByPath(option.value) as TFile,
""
);
frontmatter.type = "file";
frontmatter.redirect = link;
}
});
new Notice(`${t('shortcut_created')}: ${shortcutName}`);
} catch (error) {
console.error('Create shortcut error', error);
new Notice(t('Failed to create shortcut'));
}
}

620
src/renderModePath.ts Normal file
View file

@ -0,0 +1,620 @@
import { TFolder, TFile, Menu, Platform, setIcon, normalizePath, setTooltip } from 'obsidian';
import { GridView } from './GridView';
import { isFolderIgnored } from './fileUtils';
import { showFolderNoteSettingsModal } from './modal/folderNoteSettingsModal';
import { CustomModeModal } from './modal/customModeModal';
import { t } from './translations';
export function renderModePath(gridView: GridView) {
// 創建模式名稱和排序按鈕的容器
const modeHeaderContainer = gridView.containerEl.createDiv('ge-mode-header-container');
// 左側:模式名稱
const modenameContainer = modeHeaderContainer.createDiv('ge-modename-content');
// 右側:排序按鈕
const rightActions = modeHeaderContainer.createDiv('ge-right-actions');
// 添加排序按鈕
if (gridView.sourceMode !== 'bookmarks' &&
gridView.sourceMode !== 'recent-files' &&
gridView.sourceMode !== 'random-note') {
const sortButton = rightActions.createEl('a', {
cls: 'ge-sort-button',
attr: {
'aria-label': t('sorting'),
'href': '#'
}
});
setIcon(sortButton, 'arrow-up-narrow-wide');
sortButton.addEventListener('click', (evt) => {
evt.preventDefault();
evt.stopPropagation();
const menu = new Menu();
const sortOptions = [
{ value: 'name-asc', label: t('sort_name_asc'), icon: 'a-arrow-up' },
{ value: 'name-desc', label: t('sort_name_desc'), icon: 'a-arrow-down' },
{ value: 'mtime-desc', label: t('sort_mtime_desc'), icon: 'clock' },
{ value: 'mtime-asc', label: t('sort_mtime_asc'), icon: 'clock' },
{ value: 'ctime-desc', label: t('sort_ctime_desc'), icon: 'calendar' },
{ value: 'ctime-asc', label: t('sort_ctime_asc'), icon: 'calendar' },
{ value: 'random', label: t('sort_random'), icon: 'dice' },
];
sortOptions.forEach(option => {
menu.addItem((item) => {
item
.setTitle(option.label)
.setIcon(option.icon)
.setChecked((gridView.folderSortType || gridView.sortType) === option.value)
.onClick(() => {
gridView.sortType = option.value;
gridView.folderSortType = '';
gridView.app.workspace.requestSaveLayout();
gridView.render();
});
});
});
menu.showAtMouseEvent(evt);
});
}
// 為區域添加點擊事件,點擊後網格容器捲動到最頂部
modenameContainer.addEventListener('click', (event: MouseEvent) => {
// 只有當點擊的是頂部按鈕區域本身(而不是其中的按鈕)時才觸發捲動
if (event.target === modenameContainer) {
event.preventDefault();
// 取得網格容器
const gridContainer = gridView.containerEl.querySelector('.ge-grid-container');
if (gridContainer) {
gridContainer.scrollTo({
top: 0,
behavior: 'smooth'
});
}
}
});
// 顯示目前資料夾及完整路徑
if (gridView.sourceMode === 'folder' &&
(gridView.searchQuery === '' || (gridView.searchQuery && !gridView.searchAllFiles)) &&
gridView.sourcePath !== '/') {
const pathParts = gridView.sourcePath.split('/').filter(part => part.trim() !== '');
// 建立路徑項目的資料結構
interface PathItem {
name: string;
path: string;
isLast: boolean;
}
const paths: PathItem[] = [];
let pathAccumulator = '';
// 添加根目錄
paths.push({
name: t('root'),
path: '/',
isLast: pathParts.length === 0
});
// 建立所有路徑
pathParts.forEach((part, index) => {
pathAccumulator = pathAccumulator ? `${pathAccumulator}/${part}` : part;
paths.push({
name: part,
path: pathAccumulator,
isLast: index === pathParts.length - 1
});
});
// 創建一個容器來測量寬度
const pathContainer = modenameContainer.createDiv({ cls: 'ge-path-container' });
const customFolderIcon = gridView.plugin.settings.customFolderIcon;
// 計算可用寬度
const pathElements: HTMLElement[] = [];
// 建立所有路徑元素
paths.forEach((path, index) => {
const isLast = index === paths.length - 1;
let pathEl;
if (isLast) {
// 當前資料夾使用 span 元素
pathEl = modenameContainer.createEl('a', {
text: `${customFolderIcon} ${path.name}`.trim(),
cls: 'ge-current-folder'
});
} else {
// 上層資料夾使用 a 元素(可點擊)
pathEl = modenameContainer.createEl('a', {
text: path.name,
cls: 'ge-parent-folder-link'
});
}
setTooltip(pathEl, path.name);
pathElements.push(pathEl);
});
// 添加路徑元素
for (let i = 0; i < pathElements.length; i++) {
const el = pathElements[i];
pathContainer.appendChild(el);
// 為路徑元素添加點擊事件
if (el.className === 'ge-parent-folder-link') {
const pathIndex = i; // 直接使用索引,因為不再有分隔符
if (pathIndex < paths.length) {
const path = paths[pathIndex];
el.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
gridView.setSource('folder', path.path, true);
gridView.clearSelection();
});
// 為路徑元素添加右鍵選單,顯示路徑層級和同層級目錄
el.addEventListener('contextmenu', async (event) => {
event.preventDefault();
event.stopPropagation();
const menu = new Menu();
// 1. 添加當前點擊的目錄
menu.addItem((item) => {
item.setTitle(path.name)
.setIcon('folder')
.onClick(() => {
gridView.setSource('folder', path.path, true);
gridView.clearSelection();
});
});
// 2. 獲取並添加當前目錄下的所有子目錄
const currentFolder = gridView.app.vault.getAbstractFileByPath(path.path);
if (currentFolder && currentFolder instanceof TFolder) {
const subFolders = currentFolder.children
.filter(child => {
// 如果不是資料夾,則不顯示
if (!(child instanceof TFolder)) return false;
// 使用 isFolderIgnored 函數檢查是否應該忽略此資料夾
return !isFolderIgnored(
child,
gridView.plugin.settings.ignoredFolders,
gridView.plugin.settings.ignoredFolderPatterns,
gridView.showIgnoredFolders
);
})
.sort((a, b) => a.name.localeCompare(b.name));
if (subFolders.length > 0) {
menu.addSeparator();
menu.addItem((item) =>
item.setTitle(t('sub_folders'))
.setIcon('folder-symlink')
.setDisabled(true)
);
subFolders.forEach(folder => {
menu.addItem((item) => {
item.setTitle(folder.name)
.setIcon('folder')
.onClick(() => {
gridView.setSource('folder', folder.path, true);
gridView.clearSelection();
});
});
});
}
}
// 3. 添加上層路徑
if (pathIndex > 0) {
menu.addSeparator();
menu.addItem((item) =>
item.setTitle(t('parent_folders'))
.setIcon('arrow-up')
.setDisabled(true)
);
for (let i = pathIndex - 1; i >= 0; i--) {
const p = paths[i];
menu.addItem((item) => {
item.setTitle(p.name)
.setIcon(p.path === '/' ? 'folder-root' : 'folder')
.onClick(() => {
gridView.setSource('folder', p.path, true);
gridView.clearSelection();
});
});
}
}
menu.showAtMouseEvent(event);
});
// 為最後一個路徑以外的路徑添加拖曳功能
if (!path.isLast && Platform.isDesktop) {
// 為路徑元素添加拖曳目標功能
el.addEventListener('dragover', (event) => {
event.preventDefault();
event.dataTransfer!.dropEffect = 'move';
el.addClass('ge-dragover');
});
el.addEventListener('dragleave', () => {
el.removeClass('ge-dragover');
});
el.addEventListener('drop', async (event) => {
event.preventDefault();
el.removeClass('ge-dragover');
if (!path.path) return;
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) {
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);
}
}
} catch (error) {
console.error('An error occurred while moving multiple files to folder:', error);
}
return;
}
const filePath = event.dataTransfer?.getData('text/plain');
if (!filePath) return;
const cleanedFilePath = filePath.replace(/!?\[\[(.*?)\]\]/, '$1');
const file = gridView.app.vault.getAbstractFileByPath(cleanedFilePath);
if (file instanceof TFile) {
try {
const newPath = normalizePath(`${path.path}/${file.name}`);
await gridView.app.fileManager.renameFile(file, newPath);
gridView.render();
} catch (error) {
console.error('An error occurred while moving the file to folder:', error);
}
}
});
}
}
}
if (el.className === 'ge-current-folder') {
// 將選單邏輯抽出,以同時支援 click 與 contextmenu
const showFolderMenu = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
const folder = gridView.app.vault.getAbstractFileByPath(gridView.sourcePath);
const folderName = gridView.sourcePath.split('/').pop() || '';
const notePath = `${gridView.sourcePath}/${folderName}.md`;
const noteFile = gridView.app.vault.getAbstractFileByPath(notePath);
const menu = new Menu();
if (noteFile instanceof TFile) {
// 打開資料夾筆記選項
menu.addItem((item) => {
item
.setTitle(t('open_folder_note'))
.setIcon('panel-left-open')
.onClick(() => {
gridView.app.workspace.getLeaf().openFile(noteFile);
});
});
// 編輯資料夾筆記設定選項
menu.addItem((item) => {
item
.setTitle(t('edit_folder_note_settings'))
.setIcon('settings-2')
.onClick(() => {
if (folder instanceof TFolder) {
showFolderNoteSettingsModal(gridView.app, gridView.plugin, folder, gridView);
}
});
});
// 刪除資料夾筆記選項
menu.addItem((item) => {
item
.setTitle(t('delete_folder_note'))
.setIcon('folder-x')
.onClick(() => {
gridView.app.fileManager.trashFile(noteFile as TFile);
});
});
} else {
// 建立 Folder note
menu.addItem((item) => {
item
.setTitle(t('create_folder_note'))
.setIcon('file-cog')
.onClick(() => {
if (folder instanceof TFolder) {
showFolderNoteSettingsModal(gridView.app, gridView.plugin, folder, gridView);
}
});
});
}
menu.showAtMouseEvent(event);
};
// 左鍵與右鍵都呼叫相同的選單
el.addEventListener('click', showFolderMenu);
el.addEventListener('contextmenu', showFolderMenu);
}
}
} else if (!(gridView.searchQuery !== '' && gridView.searchAllFiles)) {
// 顯示目前模式名稱
let modeName = '';
let modeIcon = '';
// 根據目前模式設定對應的圖示和名稱
switch (gridView.sourceMode) {
case 'bookmarks':
modeIcon = '📑';
modeName = t('bookmarks_mode');
break;
case 'search':
modeIcon = '🔍';
modeName = t('search_results');
const searchLeaf = (gridView.app as any).workspace.getLeavesOfType('search')[0];
if (searchLeaf) {
const searchView: any = searchLeaf.view;
const searchInputEl: HTMLInputElement | null = searchView.searchComponent ? searchView.searchComponent.inputEl : null;
const currentQuery = searchInputEl?.value.trim();
if (currentQuery && currentQuery.length > 0) {
modeName += `: ${currentQuery}`;
} else if (gridView.searchQuery) {
modeName += `: ${gridView.searchQuery}`;
}
}
break;
case 'backlinks':
modeIcon = '🔗';
modeName = t('backlinks_mode');
const activeFile = gridView.app.workspace.getActiveFile();
if (activeFile) {
modeName += `: ${activeFile.basename}`;
}
break;
case 'outgoinglinks':
modeIcon = '🔗';
modeName = t('outgoinglinks_mode');
const currentFile = gridView.app.workspace.getActiveFile();
if (currentFile) {
modeName += `: ${currentFile.basename}`;
}
break;
case 'recent-files':
modeIcon = '📅';
modeName = t('recent_files_mode');
break;
case 'all-files':
modeIcon = '📔';
modeName = t('all_files_mode');
break;
case 'random-note':
modeIcon = '🎲';
modeName = t('random_note_mode');
break;
case 'tasks':
modeIcon = '☑️';
modeName = t('tasks_mode');
break;
default:
if (gridView.sourceMode.startsWith('custom-')) {
const mode = gridView.plugin.settings.customModes.find(m => m.internalName === gridView.sourceMode);
modeIcon = mode ? mode.icon : '🧩';
modeName = mode ? mode.displayName : t('custom_mode');
} else { // folder mode
modeIcon = '📁';
if (gridView.sourcePath && gridView.sourcePath !== '/') {
modeName = gridView.sourcePath.split('/').pop() || gridView.sourcePath;
} else {
modeName = t('root');
}
}
}
// 顯示模式名稱 (若為自訂模式則提供點擊選單以快速切換)
let modeTitleEl: HTMLElement;
if (gridView.sourceMode.startsWith('custom-')) {
// 使用可點擊的 <a> 元素
modeTitleEl = modenameContainer.createEl('a', {
text: `${modeIcon} ${modeName}`.trim(),
cls: 'ge-mode-title'
});
// 點擊時顯示所有自訂模式選單
modeTitleEl.addEventListener('click', (evt) => {
const menu = new Menu();
gridView.plugin.settings.customModes
.filter(m => m.enabled ?? true) // 僅顯示啟用的自訂模式
.forEach((m) => {
menu.addItem(item => {
item.setTitle(`${m.icon || '🧩'} ${m.displayName}`)
.setChecked(m.internalName === gridView.sourceMode)
.onClick(() => {
// 切換至選取的自訂模式並重新渲染
gridView.setSource(m.internalName, '', true);
});
});
});
menu.showAtMouseEvent(evt);
});
} else {
// 其他模式維持原本的 span
modeTitleEl = modenameContainer.createEl('span', {
text: `${modeIcon} ${modeName}`.trim(),
cls: 'ge-mode-title'
});
}
switch (gridView.sourceMode) {
case 'random-note':
case 'recent-files':
case 'all-files':
if (gridView.plugin.settings.showMediaFiles && gridView.searchQuery === '') {
// "顯示類型"選項
const showTypeName = gridView.randomNoteIncludeMedia ? t('random_note_include_media_files') : 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.randomNoteIncludeMedia)
.onClick(() => {
gridView.randomNoteIncludeMedia = false;
gridView.render();
});
});
menu.addItem((item) => {
item.setTitle(t('random_note_include_media_files'))
.setIcon('file-image')
.setChecked(gridView.randomNoteIncludeMedia)
.onClick(() => {
gridView.randomNoteIncludeMedia = true;
gridView.render();
});
});
menu.showAtMouseEvent(evt);
});
}
break;
case 'tasks':
const taskFilterSpan = modenameContainer.createEl('a', { text: t(`${gridView.taskFilter}`), cls: 'ge-sub-option' });
taskFilterSpan.addEventListener('click', (evt) => {
const menu = new Menu();
menu.addItem((item) => {
item.setTitle(t('uncompleted'))
.setChecked(gridView.taskFilter === 'uncompleted')
.setIcon('square')
.onClick(() => {
gridView.taskFilter = 'uncompleted';
gridView.render();
});
});
menu.addItem((item) => {
item.setTitle(t('completed'))
.setChecked(gridView.taskFilter === 'completed')
.setIcon('square-check-big')
.onClick(() => {
gridView.taskFilter = 'completed';
gridView.render();
});
});
menu.addItem((item) => {
item.setTitle(t('all'))
.setChecked(gridView.taskFilter === 'all')
.setIcon('square-asterisk')
.onClick(() => {
gridView.taskFilter = 'all';
gridView.render();
});
});
menu.addSeparator();
menu.showAtMouseEvent(evt);
});
break;
default:
if (gridView.sourceMode.startsWith('custom-')) {
// 把 modenameContainer 加上所有自訂模式選項的選單
// 取得當前自訂模式
const mode = gridView.plugin.settings.customModes.find(m => m.internalName === gridView.sourceMode);
if (mode) {
const hasOptions = mode.options && mode.options.length > 0;
if (hasOptions && mode.options) {
if (gridView.customOptionIndex >= mode.options.length || gridView.customOptionIndex < -1) {
gridView.customOptionIndex = -1;
}
let subName: string | undefined;
if (gridView.customOptionIndex === -1) {
subName = (mode as any).name?.trim() || t('default');
} else if (gridView.customOptionIndex >= 0 && gridView.customOptionIndex < mode.options.length) {
const opt = mode.options[gridView.customOptionIndex];
subName = opt.name?.trim() || `${t('option')} ${gridView.customOptionIndex + 1}`;
}
const subSpan = modenameContainer.createEl('a', { text: subName ?? '-', cls: 'ge-sub-option' });
subSpan.addEventListener('click', (evt) => {
const menu = new Menu();
// 預設選項
const defaultName = (mode as any).name?.trim() || t('default');
menu.addItem(item => {
item.setTitle(defaultName)
.setIcon('puzzle')
.setChecked(gridView.customOptionIndex === -1)
.onClick(() => {
gridView.customOptionIndex = -1;
gridView.render(true);
});
});
mode.options!.forEach((opt, idx) => {
menu.addItem(item => {
item.setTitle(opt.name?.trim() || t('option') + ' ' + (idx + 1))
.setIcon('puzzle')
.setChecked(idx === gridView.customOptionIndex)
.onClick(() => {
gridView.customOptionIndex = idx;
gridView.render(true);
});
});
});
menu.showAtMouseEvent(evt);
});
}
// 總是顯示設定齒輪圖示
const gearIcon = modenameContainer.createEl('a', { cls: 'ge-settings-gear' });
setIcon(gearIcon, 'settings');
gearIcon.addEventListener('click', () => {
const modeIndex = gridView.plugin.settings.customModes.findIndex(m => m.internalName === mode.internalName);
if (modeIndex === -1) return;
new CustomModeModal(gridView.app, gridView.plugin, gridView.plugin.settings.customModes[modeIndex], (result) => {
gridView.plugin.settings.customModes[modeIndex] = result;
gridView.plugin.saveSettings();
gridView.render(true);
}).open();
});
}
}
break;
}
} else if (gridView.searchQuery !== '' && gridView.searchAllFiles) {
// 顯示全域搜尋名稱
modenameContainer.createEl('span', {
text: `🔍 ${t('global_search')}`,
cls: 'ge-mode-title'
});
}
}
// ----------------------------------------------------
// 搬移建議:
// 1. 先整段複製原始碼貼到此函式底下,並把所有 `gridView.` 前綴改成 `gridView.`。
// 2. 逐步補上缺少的 import 與型別。
// 3. GridView.render 內原本的程式替換為呼叫 renderModePath()。
// 4. 若 render() 需要重新渲染,可在 GridView 內保留 render() 呼叫不變。
// ----------------------------------------------------

View file

@ -679,6 +679,7 @@ a.ge-mode-title:hover {
border-radius: 4px;
border: 1px solid var(--background-modifier-hover);
cursor: pointer;
margin-right: 4px;
}
.ge-sub-option:hover {
@ -687,6 +688,21 @@ a.ge-mode-title:hover {
text-decoration: none;
}
.ge-settings-gear {
display: inline-flex;
align-items: center;
padding: 3px 5px;
border-radius: 4px;
color: var(--text-muted);
opacity: 0.7;
cursor: pointer;
}
.ge-settings-gear:hover {
background-color: var(--background-modifier-hover);
opacity: 1;
}
/* 路徑容器樣式 */
.ge-path-container {
display: flex;

View file

@ -1,3 +1,3 @@
{
"2.7.11": "1.1.0"
"2.7.12": "1.1.0"
}