mirror of
https://github.com/devon22/obsidian-gridexplorer.git
synced 2026-07-22 12:00:25 +00:00
2.7.12
This commit is contained in:
parent
63ca91d3bd
commit
23dfa0f20b
12 changed files with 1833 additions and 1785 deletions
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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,避免短時間內大量重繪
|
||||
|
|
|
|||
1784
src/GridView.ts
1784
src/GridView.ts
File diff suppressed because it is too large
Load diff
|
|
@ -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
260
src/renderFiles.ts
Normal 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
441
src/renderFolder.ts
Normal 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
473
src/renderHeaderButton.ts
Normal 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
620
src/renderModePath.ts
Normal 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() 呼叫不變。
|
||||
// ----------------------------------------------------
|
||||
16
styles.css
16
styles.css
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"2.7.11": "1.1.0"
|
||||
"2.7.12": "1.1.0"
|
||||
}
|
||||
Loading…
Reference in a new issue