From 23dfa0f20b42bc32931595bbcee5cffd3f8a95ec Mon Sep 17 00:00:00 2001 From: Devon22 Date: Sat, 2 Aug 2025 01:44:27 +0800 Subject: [PATCH] 2.7.12 --- manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/FileWatcher.ts | 12 +- src/GridView.ts | 1784 +------------------------------------ src/fileUtils.ts | 2 +- src/renderFiles.ts | 260 ++++++ src/renderFolder.ts | 441 +++++++++ src/renderHeaderButton.ts | 473 ++++++++++ src/renderModePath.ts | 620 +++++++++++++ styles.css | 16 + versions.json | 2 +- 12 files changed, 1833 insertions(+), 1785 deletions(-) create mode 100644 src/renderFiles.ts create mode 100644 src/renderFolder.ts create mode 100644 src/renderHeaderButton.ts create mode 100644 src/renderModePath.ts diff --git a/manifest.json b/manifest.json index 94521cc..7af6499 100644 --- a/manifest.json +++ b/manifest.json @@ -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", diff --git a/package-lock.json b/package-lock.json index cbb724b..260991d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 12a54ab..c528e44 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/FileWatcher.ts b/src/FileWatcher.ts index c9d7873..4a25cee 100644 --- a/src/FileWatcher.ts +++ b/src/FileWatcher.ts @@ -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,避免短時間內大量重繪 diff --git a/src/GridView.ts b/src/GridView.ts index 9c73050..e34b6ef 100644 --- a/src/GridView.ts +++ b/src/GridView.ts @@ -1,18 +1,16 @@ -import { WorkspaceLeaf, ItemView, TFolder, TFile, Menu, Notice, Platform, setIcon, getFrontMatterInfo, FrontMatterCache, normalizePath, setTooltip, MarkdownRenderer, Component } from 'obsidian'; +import { WorkspaceLeaf, ItemView, TFolder, TFile, Menu, Notice, Platform, setIcon, getFrontMatterInfo, FrontMatterCache, normalizePath, setTooltip, MarkdownRenderer } from 'obsidian'; import GridExplorerPlugin from './main'; +import { renderHeaderButton } from './renderHeaderButton'; +import { renderModePath } from './renderModePath'; +import { renderFolder } from './renderFolder'; +import { renderFiles } from './renderFiles'; import { handleKeyDown } from './handleKeyDown'; -import { isDocumentFile, isMediaFile, isImageFile, isVideoFile, isAudioFile, sortFiles, ignoredFiles, getFiles, IMAGE_EXTENSIONS, VIDEO_EXTENSIONS, isFolderIgnored } from './fileUtils'; +import { isDocumentFile, isMediaFile, isImageFile, isVideoFile, isAudioFile, getFiles, IMAGE_EXTENSIONS, VIDEO_EXTENSIONS } from './fileUtils'; import { FileWatcher } from './fileWatcher'; import { findFirstImageInNote } from './mediaUtils'; import { showFolderSelectionModal } from './modal/folderSelectionModal'; import { MediaModal } from './modal/mediaModal'; -import { showFolderNoteSettingsModal } from './modal/folderNoteSettingsModal'; import { showNoteSettingsModal } from './modal/noteSettingsModal'; -import { showFolderRenameModal } from './modal/folderRenameModal'; -import { moveFolderSuggestModal } from './modal/moveFolderSuggestModal'; -import { showSearchModal } from './modal/searchModal'; -import { CustomModeModal } from './modal/customModeModal'; -import { ShortcutSelectionModal } from './modal/shortcutSelectionModal'; import { FloatingAudioPlayer } from './floatingAudioPlayer'; import { t } from './translations'; @@ -46,7 +44,7 @@ export class GridView extends ItemView { selectedItems: Set = new Set(); // 存儲多選的項目索引 gridItems: HTMLElement[] = []; // 存儲所有網格項目的引用 hasKeyboardFocus: boolean = false; // 是否有鍵盤焦點 - fileWatcher: FileWatcher; + fileWatcher: FileWatcher; // 檔案監聽器 recentSources: string[] = []; // 歷史記錄 minMode: boolean = false; // 最小模式 showIgnoredFolders: boolean = false; // 顯示忽略資料夾 @@ -255,1036 +253,11 @@ export class GridView extends ItemView { // 清空整個容器 this.containerEl.empty(); - // 創建頂部按鈕區域 - const headerButtonsDiv = this.containerEl.createDiv('ge-header-buttons'); - - // 為頂部按鈕區域添加點擊事件,點擊後網格容器捲動到最頂部 - headerButtonsDiv.addEventListener('click', (event: MouseEvent) => { - // 只有當點擊的是頂部按鈕區域本身(而不是其中的按鈕)時才觸發捲動 - if (event.target === headerButtonsDiv) { - event.preventDefault(); - // 取得網格容器 - const gridContainer = this.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 (this.searchQuery !== '') { - this.searchQuery = ''; - this.app.workspace.requestSaveLayout(); - this.render(); - return; - } - - // 如果有歷史記錄 - if (this.recentSources.length > 0) { - // 取得最近一筆歷史記錄 - const lastSource = JSON.parse(this.recentSources[0]); - this.recentSources.shift(); // 從歷史記錄中移除 - - // 設定來源(不記錄到歷史) - this.setSource( - lastSource.mode, - lastSource.path || '', - true, // 重設捲動位置 - false // 不記錄到歷史 - ); - } - }); - - // 添加右鍵選單支援 - backButton.addEventListener('contextmenu', (event) => { - // 只有在有歷史記錄時才顯示右鍵選單 - if (this.recentSources.length > 0) { - event.preventDefault(); - - const menu = new Menu(); - - // 添加歷史記錄 - this.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 = this.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 = this.recentSources.findIndex(source => { - const parsed = JSON.parse(source); - return parsed.mode === mode && parsed.path === path; - }); - - // 如果找到點擊的紀錄,清除它之上的紀錄 - if (clickedIndex !== -1) { - this.recentSources = this.recentSources.slice(clickedIndex + 1); - } - - this.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 = !this.sourcePath || this.sourcePath === '/' ? newFileName : `${this.sourcePath}/${newFileName}`; - - // 檢查檔案是否已存在,如果存在則遞增編號 - let counter = 1; - while (this.app.vault.getAbstractFileByPath(newFilePath)) { - newFileName = `${t('untitled')} ${counter}.md`; - newFilePath = !this.sourcePath || this.sourcePath === '/' ? newFileName : `${this.sourcePath}/${newFileName}`; - counter++; - } - - try { - // 建立新筆記 - const newFile = await this.app.vault.create(newFilePath, ''); - // 開啟新筆記 - await this.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 = !this.sourcePath || this.sourcePath === '/' ? newFolderName : `${this.sourcePath}/${newFolderName}`; - - // 檢查資料夾是否已存在,如果存在則遞增編號 - let counter = 1; - while (this.app.vault.getAbstractFileByPath(newFolderPath)) { - newFolderName = `${t('untitled')} ${counter}`; - newFolderPath = !this.sourcePath || this.sourcePath === '/' ? newFolderName : `${this.sourcePath}/${newFolderName}`; - counter++; - } - - try { - // 建立新資料夾 - await this.app.vault.createFolder(newFolderPath); - this.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 = !this.sourcePath || this.sourcePath === '/' ? newFileName : `${this.sourcePath}/${newFileName}`; - - // 檢查檔案是否已存在,如果存在則遞增編號 - let counter = 1; - while (this.app.vault.getAbstractFileByPath(newFilePath)) { - newFileName = `${t('untitled')} ${counter}.canvas`; - newFilePath = !this.sourcePath || this.sourcePath === '/' ? newFileName : `${this.sourcePath}/${newFileName}`; - counter++; - } - - try { - // 建立新筆記 - const newFile = await this.app.vault.create(newFilePath, ''); - // 開啟新筆記 - await this.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 () => { - this.showShortcutSelectionModal(); - }); - }); - menu.showAtMouseEvent(event); - }); - - // 添加重新選擇資料夾按鈕 - const reselectButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('reselect') } }); - reselectButton.addEventListener('click', () => { - showFolderSelectionModal(this.app, this.plugin, this, reselectButton); - }); - setIcon(reselectButton, "grid"); - - // 添加重新整理按鈕 - const refreshButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('refresh') } }); - refreshButton.addEventListener('click', () => { - if (this.sortType === 'random') { - this.clearSelection(); - } - this.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(this.app, this, '', searchButton); - }); - - // 如果有搜尋關鍵字,顯示搜尋文字和取消按鈕 - if (this.searchQuery) { - searchButton.style.display = 'none'; - const searchTextContainer = searchButtonContainer.createDiv('ge-search-text-container'); - - // 創建搜尋文字 - const searchText = searchTextContainer.createEl('span', { cls: 'ge-search-text', text: this.searchQuery }); - // 讓搜尋文字可點選 - searchText.style.cursor = 'pointer'; - searchText.addEventListener('click', () => { - showSearchModal(this.app, this, this.searchQuery, searchText); - }); - - // 創建取消按鈕 - const clearButton = searchTextContainer.createDiv('ge-clear-button'); - setIcon(clearButton, 'x'); - clearButton.addEventListener('click', (e) => { - e.stopPropagation(); // 防止觸發搜尋文字的點擊事件 - this.searchQuery = ''; - this.clearSelection(); - this.app.workspace.requestSaveLayout(); - this.render(); - }); - } - - // 添加更多選項按鈕 - const menu = new Menu(); - menu.addItem((item) => { - item - .setTitle(t('open_new_grid_view')) - .setIcon('grid') - .onClick(() => { - const { workspace } = this.app; - let leaf = null; - workspace.getLeavesOfType('grid-view'); - switch (this.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(); - - // 建立隨機筆記、最近筆記、全部筆記是否包含圖片和影片的設定按鈕 - if ((this.sourceMode === 'all-files' || this.sourceMode === 'recent-files' || this.sourceMode === 'random-note') && - this.plugin.settings.showMediaFiles && this.searchQuery === '') { - menu.addItem((item) => { - item.setTitle(t('random_note_notes_only')) - .setIcon('file-text') - .setChecked(!this.randomNoteIncludeMedia) - .onClick(() => { - this.randomNoteIncludeMedia = false; - this.render(); - }); - }); - menu.addItem((item) => { - item.setTitle(t('random_note_include_media_files')) - .setIcon('file-image') - .setChecked(this.randomNoteIncludeMedia) - .onClick(() => { - this.randomNoteIncludeMedia = true; - this.render(); - }); - }); - menu.addSeparator(); - } - // 直向卡片切換 - menu.addItem((item) => { - item.setTitle(t('vertical_card')) - .setIcon('layout') - .setChecked(this.baseCardLayout === 'vertical') - .onClick(() => { - this.baseCardLayout = this.baseCardLayout === 'vertical' ? 'horizontal' : 'vertical'; - this.cardLayout = this.baseCardLayout; - this.app.workspace.requestSaveLayout(); - this.render(); - }); - }); - // 最小化模式選項 - menu.addItem((item) => { - item - .setTitle(t('min_mode')) - .setIcon('minimize-2') - .setChecked(this.minMode) - .onClick(() => { - this.minMode = !this.minMode; - this.app.workspace.requestSaveLayout(); - this.render(); - }); - }); - // 顯示日期分隔器 - if (this.plugin.settings.dateDividerMode !== 'none') { - menu.addItem((item) => { - item - .setTitle(t('show_date_dividers')) - .setIcon('calendar') - .setChecked(this.showDateDividers) - .onClick(() => { - this.showDateDividers = !this.showDateDividers; - this.app.workspace.requestSaveLayout(); - this.render(); - }); - }); - } - // 顯示筆記標籤 - menu.addItem((item) => { - item - .setTitle(t('show_note_tags')) - .setIcon('tag') - .setChecked(this.showNoteTags) - .onClick(() => { - this.showNoteTags = !this.showNoteTags; - this.app.workspace.requestSaveLayout(); - this.render(); - }); - }); - // 顯示忽略資料夾選項 - menu.addItem((item) => { - item - .setTitle(t('show_ignored_folders')) - .setIcon('folder-open-dot') - .setChecked(this.showIgnoredFolders) - .onClick(() => { - this.showIgnoredFolders = !this.showIgnoredFolders; - this.app.workspace.requestSaveLayout(); - this.render(); - }); - }); - menu.addSeparator(); - menu.addItem((item) => { - item - .setTitle(t('open_settings')) - .setIcon('settings') - .onClick(() => { - // 打開插件設定頁面 - (this.app as any).setting.open(); - (this.app as any).setting.openTabById(this.plugin.manifest.id); - }); - }); + // 添加頂部按鈕 + renderHeaderButton(this); - if (this.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); - } - }); - - // 創建模式名稱和排序按鈕的容器 - const modeHeaderContainer = this.containerEl.createDiv('ge-mode-header-container'); - - // 左側:模式名稱 - const modenameContainer = modeHeaderContainer.createDiv('ge-modename-content'); - - // 右側:排序按鈕 - const rightActions = modeHeaderContainer.createDiv('ge-right-actions'); - - // 添加排序按鈕 - if (this.sourceMode !== 'bookmarks' && - this.sourceMode !== 'recent-files' && - this.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((this.folderSortType || this.sortType) === option.value) - .onClick(() => { - this.sortType = option.value; - this.folderSortType = ''; - this.app.workspace.requestSaveLayout(); - this.render(); - }); - }); - }); - menu.showAtMouseEvent(evt); - }); - } - - // 為區域添加點擊事件,點擊後網格容器捲動到最頂部 - modenameContainer.addEventListener('click', (event: MouseEvent) => { - // 只有當點擊的是頂部按鈕區域本身(而不是其中的按鈕)時才觸發捲動 - if (event.target === modenameContainer) { - event.preventDefault(); - // 取得網格容器 - const gridContainer = this.containerEl.querySelector('.ge-grid-container'); - if (gridContainer) { - gridContainer.scrollTo({ - top: 0, - behavior: 'smooth' - }); - } - } - }); - - // 顯示目前資料夾及完整路徑 - if (this.sourceMode === 'folder' && - (this.searchQuery === '' || (this.searchQuery && !this.searchAllFiles)) && - this.sourcePath !== '/') { - const pathParts = this.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 = this.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(); - this.setSource('folder', path.path, true); - this.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(() => { - this.setSource('folder', path.path, true); - this.clearSelection(); - }); - }); - - // 2. 獲取並添加當前目錄下的所有子目錄 - const currentFolder = this.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, - this.plugin.settings.ignoredFolders, - this.plugin.settings.ignoredFolderPatterns, - this.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(() => { - this.setSource('folder', folder.path, true); - this.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(() => { - this.setSource('folder', p.path, true); - this.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 = this.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 = this.app.vault.getAbstractFileByPath(filePath); - if (file instanceof TFile) { - const newPath = normalizePath(`${path.path}/${file.name}`); - await this.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 = this.app.vault.getAbstractFileByPath(cleanedFilePath); - - if (file instanceof TFile) { - try { - const newPath = normalizePath(`${path.path}/${file.name}`); - await this.app.fileManager.renameFile(file, newPath); - this.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 = this.app.vault.getAbstractFileByPath(this.sourcePath); - const folderName = this.sourcePath.split('/').pop() || ''; - const notePath = `${this.sourcePath}/${folderName}.md`; - const noteFile = this.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(() => { - this.app.workspace.getLeaf().openFile(noteFile); - }); - }); - // 編輯資料夾筆記設定選項 - menu.addItem((item) => { - item - .setTitle(t('edit_folder_note_settings')) - .setIcon('settings-2') - .onClick(() => { - if (folder instanceof TFolder) { - showFolderNoteSettingsModal(this.app, this.plugin, folder, this); - } - }); - }); - // 刪除資料夾筆記選項 - menu.addItem((item) => { - item - .setTitle(t('delete_folder_note')) - .setIcon('folder-x') - .onClick(() => { - this.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(this.app, this.plugin, folder, this); - } - }); - }); - } - menu.showAtMouseEvent(event); - }; - - // 左鍵與右鍵都呼叫相同的選單 - el.addEventListener('click', showFolderMenu); - el.addEventListener('contextmenu', showFolderMenu); - } - } - } else if (!(this.searchQuery !== '' && this.searchAllFiles)) { - // 顯示目前模式名稱 - - let modeName = ''; - let modeIcon = ''; - - // 根據目前模式設定對應的圖示和名稱 - switch (this.sourceMode) { - case 'bookmarks': - modeIcon = '📑'; - modeName = t('bookmarks_mode'); - break; - case 'search': - modeIcon = '🔍'; - modeName = t('search_results'); - const searchLeaf = (this.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 (this.searchQuery) { - modeName += `: ${this.searchQuery}`; - } - } - break; - case 'backlinks': - modeIcon = '🔗'; - modeName = t('backlinks_mode'); - const activeFile = this.app.workspace.getActiveFile(); - if (activeFile) { - modeName += `: ${activeFile.basename}`; - } - break; - case 'outgoinglinks': - modeIcon = '🔗'; - modeName = t('outgoinglinks_mode'); - const currentFile = this.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 (this.sourceMode.startsWith('custom-')) { - const mode = this.plugin.settings.customModes.find(m => m.internalName === this.sourceMode); - modeIcon = mode ? mode.icon : '🧩'; - modeName = mode ? mode.displayName : t('custom_mode'); - } else { // folder mode - modeIcon = '📁'; - if (this.sourcePath && this.sourcePath !== '/') { - modeName = this.sourcePath.split('/').pop() || this.sourcePath; - } else { - modeName = t('root'); - } - } - } - - // 顯示模式名稱 (若為自訂模式則提供點擊選單以快速切換) - let modeTitleEl: HTMLElement; - if (this.sourceMode.startsWith('custom-')) { - // 使用可點擊的 元素 - modeTitleEl = modenameContainer.createEl('a', { - text: `${modeIcon} ${modeName}`.trim(), - cls: 'ge-mode-title' - }); - - // 點擊時顯示所有自訂模式選單 - modeTitleEl.addEventListener('click', (evt) => { - const menu = new Menu(); - this.plugin.settings.customModes - .filter(m => m.enabled ?? true) // 僅顯示啟用的自訂模式 - .forEach((m) => { - menu.addItem(item => { - item.setTitle(`${m.icon || '🧩'} ${m.displayName}`) - .setChecked(m.internalName === this.sourceMode) - .onClick(() => { - // 切換至選取的自訂模式並重新渲染 - this.setSource(m.internalName, '', true); - }); - }); - }); - menu.showAtMouseEvent(evt); - }); - } else { - // 其他模式維持原本的 span - modeTitleEl = modenameContainer.createEl('span', { - text: `${modeIcon} ${modeName}`.trim(), - cls: 'ge-mode-title' - }); - } - - switch (this.sourceMode) { - case 'tasks': - const taskFilterSpan = modenameContainer.createEl('a', { text: t(`${this.taskFilter}`), cls: 'ge-sub-option' }); - taskFilterSpan.addEventListener('click', (evt) => { - const menu = new Menu(); - menu.addItem((item) => { - item.setTitle(t('uncompleted')) - .setChecked(this.taskFilter === 'uncompleted') - .setIcon('square') - .onClick(() => { - this.taskFilter = 'uncompleted'; - this.render(); - }); - }); - menu.addItem((item) => { - item.setTitle(t('completed')) - .setChecked(this.taskFilter === 'completed') - .setIcon('square-check-big') - .onClick(() => { - this.taskFilter = 'completed'; - this.render(); - }); - }); - menu.addItem((item) => { - item.setTitle(t('all')) - .setChecked(this.taskFilter === 'all') - .setIcon('square-asterisk') - .onClick(() => { - this.taskFilter = 'all'; - this.render(); - }); - }); - menu.addSeparator(); - menu.showAtMouseEvent(evt); - }); - break; - default: - if (this.sourceMode.startsWith('custom-')) { - // 把 modenameContainer 加上所有自訂模式選項的選單 - - // 取得當前自訂模式 - const mode = this.plugin.settings.customModes.find(m => m.internalName === this.sourceMode); - if (mode) { - const hasOptions = mode.options && mode.options.length > 0; - - if (hasOptions && mode.options) { - if (this.customOptionIndex >= mode.options.length || this.customOptionIndex < -1) { - this.customOptionIndex = -1; - } - - let subName: string | undefined; - if (this.customOptionIndex === -1) { - subName = (mode as any).name?.trim() || t('default'); - } else if (this.customOptionIndex >= 0 && this.customOptionIndex < mode.options.length) { - const opt = mode.options[this.customOptionIndex]; - subName = opt.name?.trim() || `${t('option')} ${this.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(this.customOptionIndex === -1) - .onClick(() => { - this.customOptionIndex = -1; - this.render(true); - }); - }); - mode.options!.forEach((opt, idx) => { - menu.addItem(item => { - item.setTitle(opt.name?.trim() || t('option') + ' ' + (idx + 1)) - .setIcon('puzzle') - .setChecked(idx === this.customOptionIndex) - .onClick(() => { - this.customOptionIndex = idx; - this.render(true); - }); - }); - }); - - // 設定選項 - menu.addSeparator(); - menu.addItem(item => { - item.setTitle(t('edit')) - .setIcon('settings') - .onClick(() => { - const modeIndex = this.plugin.settings.customModes.findIndex(m => m.internalName === mode.internalName); - if (modeIndex === -1) return; - new CustomModeModal(this.app, this.plugin, this.plugin.settings.customModes[modeIndex], (result) => { - this.plugin.settings.customModes[modeIndex] = result; - this.plugin.saveSettings(); - this.render(true); - }).open(); - }); - }); - - menu.showAtMouseEvent(evt); - }); - } else { - // 只有預設選項時,只顯示齒輪圖示 - const gearIcon = modenameContainer.createEl('a', { cls: 'ge-sub-option' }); - setIcon(gearIcon, 'settings'); - gearIcon.addEventListener('click', () => { - const modeIndex = this.plugin.settings.customModes.findIndex(m => m.internalName === mode.internalName); - if (modeIndex === -1) return; - new CustomModeModal(this.app, this.plugin, this.plugin.settings.customModes[modeIndex], (result) => { - this.plugin.settings.customModes[modeIndex] = result; - this.plugin.saveSettings(); - this.render(true); - }).open(); - }); - } - } - } - break; - } - } else if (this.searchQuery !== '' && this.searchAllFiles) { - // 顯示全域搜尋名稱 - modenameContainer.createEl('span', { - text: `🔍 ${t('global_search')}`, - cls: 'ge-mode-title' - }); - } + // 顯示路徑 / 模式名稱 + renderModePath(this); // 創建內容區域 const contentEl = this.containerEl.createDiv('view-content'); @@ -1437,657 +410,11 @@ export class GridView extends ItemView { return; } - // 如果是資料夾模式,先顯示所有子資料夾 - if (this.sourceMode === 'folder' && this.searchQuery === '') { - const currentFolder = this.app.vault.getAbstractFileByPath(this.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 = this.app.vault.getAbstractFileByPath(path); - if (file instanceof TFile) { - try { - // 計算新的檔案路徑 - const newPath = normalizePath(`${folderPath}/${file.name}`); - // 如果來源路徑和目標路徑相同,則跳過 - if (path === newPath) { - continue; - } - // 移動檔案 - await this.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 = this.app.vault.getAbstractFileByPath(cleanedFilePath); - - if (file instanceof TFile) { - try { - // 計算新的檔案路徑 - const newPath = normalizePath(`${currentFolder.path}/${file.name}`); - // 如果來源路徑和目標路徑相同,則不執行移動 - if (file.path !== newPath) { - // 移動檔案 - await this.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, - this.plugin.settings.ignoredFolders, - this.plugin.settings.ignoredFolderPatterns, - this.showIgnoredFolders - ); - }) - .sort((a, b) => a.name.localeCompare(b.name)); - for (const folder of subfolders) { - const folderEl = container.createDiv('ge-grid-item ge-folder-item'); - this.gridItems.push(folderEl); // 添加到網格項目數組 - - // 設置資料夾路徑屬性,用於拖曳功能 - folderEl.dataset.folderPath = folder.path; - - const contentArea = folderEl.createDiv('ge-content-area'); - const titleContainer = contentArea.createDiv('ge-title-container'); - const customFolderIcon = this.plugin.settings.customFolderIcon; - titleContainer.createEl('span', { cls: 'ge-title', text: `${customFolderIcon} ${folder.name}`.trim() }); - setTooltip(folderEl, folder.name,{ placement: this.cardLayout === 'vertical' ? 'bottom' : 'right' }); - - // 檢查同名筆記是否存在 - const notePath = `${folder.path}/${folder.name}.md`; - const noteFile = this.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(); // 防止觸發資料夾的點擊事件 - this.app.workspace.getLeaf().openFile(noteFile); - }); - - // 根據同名筆記設置背景色 - const metadata = this.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(); - this.openFolderInNewView(folder.path); - } else { - this.setSource('folder', folder.path, true); - this.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(() => { - this.openFolderInNewView(folder.path); - }); - }); - menu.addSeparator(); - - // 檢查同名筆記是否存在 - const notePath = `${folder.path}/${folder.name}.md`; - let noteFile = this.app.vault.getAbstractFileByPath(notePath); - if (noteFile instanceof TFile) { - //打開資料夾筆記 - menu.addItem((item) => { - item - .setTitle(t('open_folder_note')) - .setIcon('panel-left-open') - .onClick(() => { - this.app.workspace.getLeaf().openFile(noteFile); - }); - }); - //編輯資料夾筆記設定 - menu.addItem((item) => { - item - .setTitle(t('edit_folder_note_settings')) - .setIcon('settings-2') - .onClick(() => { - if (folder instanceof TFolder) { - showFolderNoteSettingsModal(this.app, this.plugin, folder, this); - } - }); - }); - //刪除資料夾筆記 - menu.addItem((item) => { - item - .setTitle(t('delete_folder_note')) - .setIcon('folder-x') - .onClick(() => { - this.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(this.app, this.plugin, folder, this); - } - }); - }); - } - menu.addSeparator(); - - if (!this.plugin.settings.ignoredFolders.includes(folder.path)) { - //加入"忽略此資料夾"選項 - menu.addItem((item) => { - item - .setTitle(t('ignore_folder')) - .setIcon('folder-x') - .onClick(() => { - this.plugin.settings.ignoredFolders.push(folder.path); - this.plugin.saveSettings(); - }); - }); - } else { - //加入"取消忽略此資料夾"選項 - menu.addItem((item) => { - item - .setTitle(t('unignore_folder')) - .setIcon('folder-up') - .onClick(() => { - this.plugin.settings.ignoredFolders = this.plugin.settings.ignoredFolders.filter((path) => path !== folder.path); - this.plugin.saveSettings(); - }); - }); - } - // 搬移資料夾 - menu.addItem((item) => { - item - .setTitle(t('move_folder')) - .setIcon('folder-cog') - .onClick(() => { - if (folder instanceof TFolder) { - new moveFolderSuggestModal(this.plugin, folder, this).open(); - } - }); - }); - // 重新命名資料夾 - menu.addItem((item) => { - item - .setTitle(t('rename_folder')) - .setIcon('file-cog') - .onClick(() => { - if (folder instanceof TFolder) { - showFolderRenameModal(this.app, this.plugin, folder, this); - } - }); - }); - // 刪除資料夾 - menu.addItem((item) => { - (item as any).setWarning(true); - item - .setTitle(t('delete_folder')) - .setIcon('trash') - .onClick(async () => { - if (folder instanceof TFolder) { - await this.app.fileManager.trashFile(folder); - // 重新渲染視圖 - setTimeout(() => { - this.render(); - }, 100); - } - }); - }); - menu.showAtMouseEvent(event); - }); - } - - // 資料夾渲染完插入 break(僅當有資料夾) - if (subfolders.length > 0) { - container.createDiv('ge-break'); - } - } - } - - // 為資料夾項目添加拖曳目標功能 - if(Platform.isDesktop) { - const folderItems = this.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 = this.app.vault.getAbstractFileByPath(folderPath); - if (!(folder instanceof TFolder)) return; - - // 移動檔案 - for (const path of filePaths) { - const file = this.app.vault.getAbstractFileByPath(path); - if (file instanceof TFile) { - try { - // 計算新的檔案路徑 - const newPath = normalizePath(`${folderPath}/${file.name}`); - // 移動檔案 - await this.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 = this.app.vault.getAbstractFileByPath(cleanedFilePath); - const folder = this.app.vault.getAbstractFileByPath(folderPath); - - if (file instanceof TFile && folder instanceof TFolder) { - try { - // 計算新的檔案路徑 - const newPath = normalizePath(`${folderPath}/${file.name}`); - // 移動檔案 - await this.app.fileManager.renameFile(file, newPath); - - } catch (error) { - console.error('An error occurred while moving the file:', error); - } - } - }); - }); - } + // 顯示資料夾 + renderFolder(this, container); - let loadingDiv: HTMLElement | null = null; - if (this.searchQuery || this.sourceMode === 'tasks') { - // 顯示搜尋中的提示 - loadingDiv = container.createDiv({ text: t('searching'), cls: 'ge-loading-indicator' }); - } - - let files: TFile[] = []; - // 使用 Map 來記錄原始順序 - let fileIndexMap = new Map(); - if (this.searchQuery) { - // 取得 vault 中所有支援的檔案 - let allFiles: TFile[] = []; - if (this.searchAllFiles) { - // 全部檔案 - allFiles = this.app.vault.getFiles().filter(file => - isDocumentFile(file) || (isMediaFile(file) && this.searchMediaFiles) - ); - } else { - // 當前位置檔案 - allFiles = await getFiles(this, this.searchMediaFiles); - - if (this.sourceMode === 'bookmarks') { - allFiles = allFiles.filter(file => - isDocumentFile(file) || (isMediaFile(file) && this.searchMediaFiles) - ); - // 使用 Map 來記錄原始順序 - allFiles.forEach((file, index) => { - fileIndexMap.set(file, index); - }); - } else if (this.sourceMode === 'search') { - allFiles = allFiles.filter(file => - isDocumentFile(file) || (isMediaFile(file) && this.searchMediaFiles) - ); - } else if (this.sourceMode === 'recent-files') { - // 搜尋"最近檔案"的當前位置時,先作忽略檔案和只取前n筆 - allFiles = ignoredFiles(allFiles, this).slice(0, this.plugin.settings.recentFilesCount); - } else if (this.sourceMode.startsWith('custom-')) { - // 使用 Map 來記錄原始順序 - allFiles.forEach((file, index) => { - fileIndexMap.set(file, index); - }); - } - } - - // 根據搜尋關鍵字進行過濾(不分大小寫) - const searchTerms = this.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[] = []; - const hasLinkSearch = linkTerms.length > 0; - - if (hasLinkSearch) { - for (const linkTarget of linkTerms) { - // 尋找目標檔案 - let targetFile = this.app.vault.getAbstractFileByPath(linkTarget + '.md'); - if (!targetFile) { - targetFile = this.app.metadataCache.getFirstLinkpathDest(linkTarget, ''); - } - - if (targetFile && 'extension' in targetFile) { - // 使用 Obsidian 的 getBacklinksForFile API - const backlinks = (this.app.metadataCache as any).getBacklinksForFile(targetFile); - - const currentLinkFiles = new Set(); - if (backlinks) { - // 收集所有反向連結的檔案路徑 - for (const [filePath, links] of backlinks.data.entries()) { - currentLinkFiles.add(filePath); - } - } - linkTargetFileSets.push(currentLinkFiles); - } else { - // 如果找不到目標檔案,加入空集合 - linkTargetFileSets.push(new Set()); - } - } - } - - // 使用 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 (this.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 this.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 = this.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 (this.searchAllFiles) { - // 搜尋所有檔案時 - files = sortFiles(files, this); - } else { - // 非搜尋所有檔案時 - if (this.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 (this.sourceMode === 'recent-files') { - // 臨時的排序類型 - const sortType = this.sortType; - this.sortType = 'mtime-desc'; - files = sortFiles(files, this); - this.sortType = sortType; - } else if (this.sourceMode === 'random-note') { - // 臨時的排序類型 - const sortType = this.sortType; - this.sortType = 'random'; - files = sortFiles(files, this); - this.sortType = sortType; - } else if (this.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, this); - } - } - - // 忽略檔案 - files = ignoredFiles(files, this); - } else { - // 無搜尋關鍵字的情況 - files = await getFiles(this, this.randomNoteIncludeMedia); - - // 忽略檔案 - files = ignoredFiles(files, this) - - // 最近檔案模式,只取前n筆 - if (this.sourceMode === 'recent-files') { - files = files.slice(0, this.plugin.settings.recentFilesCount); - } - - // 隨機筆記模式,只取前n筆 - if (this.sourceMode === 'random-note') { - files = files.slice(0, this.plugin.settings.randomNoteCount); - } - } - - if (loadingDiv) { - loadingDiv.remove(); - } + // 顯示檔案 + let files = await renderFiles(this, container); // 如果沒有檔案,顯示提示訊息 if (files.length === 0) { @@ -2112,6 +439,7 @@ export class GridView extends ItemView { files = [...pinnedFiles, ...otherFiles]; } + // 如果資料夾筆記設定為隱藏,則隱藏資料夾筆記 if (this.sourceMode === 'folder' && this.sourcePath !== '/') { if (this.plugin.settings.folderNoteDisplaySettings === 'hidden') { const currentFolder = this.app.vault.getAbstractFileByPath(this.sourcePath); @@ -3112,36 +1440,6 @@ export class GridView extends ItemView { }); } - // 在新視窗中開啟資料夾 - private openFolderInNewView(folderPath: string) { - const { workspace } = this.app; - let leaf = null; - workspace.getLeavesOfType('grid-view'); - switch (this.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); - } - // 清除選中狀態 clearSelection() { this.gridItems.forEach(item => { @@ -3440,56 +1738,6 @@ export class GridView extends ItemView { this.isShowingNote = false; } - // 顯示捷徑選擇 Modal - showShortcutSelectionModal() { - const modal = new ShortcutSelectionModal(this.app, this.plugin, async (option) => { - await this.createShortcut(option); - }); - modal.open(); - } - - // 創建捷徑檔案 - private async createShortcut(option: { type: 'mode' | 'folder' | 'file'; value: string; display: string; }) { - try { - // 生成不重複的檔案名稱 - let counter = 0; - let shortcutName = `${option.display}`; - let newPath = `${shortcutName}.md`; - while (this.app.vault.getAbstractFileByPath(newPath)) { - counter++; - shortcutName = `${option.display} ${counter}`; - newPath = `${shortcutName}.md`; - } - - // 創建新檔案 - const newFile = await this.app.vault.create(newPath, ''); - - // 使用 processFrontMatter 來更新 frontmatter - await this.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 = this.app.fileManager.generateMarkdownLink( - this.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')); - } - } - // 保存視圖狀態 getState() { return { diff --git a/src/fileUtils.ts b/src/fileUtils.ts index 08115ba..bfda7ea 100644 --- a/src/fileUtils.ts +++ b/src/fileUtils.ts @@ -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'; diff --git a/src/renderFiles.ts b/src/renderFiles.ts new file mode 100644 index 0000000..1a45559 --- /dev/null +++ b/src/renderFiles.ts @@ -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(); + 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[] = []; + 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(); + if (backlinks) { + // 收集所有反向連結的檔案路徑 + for (const [filePath, links] of backlinks.data.entries()) { + currentLinkFiles.add(filePath); + } + } + linkTargetFileSets.push(currentLinkFiles); + } else { + // 如果找不到目標檔案,加入空集合 + linkTargetFileSets.push(new Set()); + } + } + } + + // 使用 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; +} diff --git a/src/renderFolder.ts b/src/renderFolder.ts new file mode 100644 index 0000000..4fded0a --- /dev/null +++ b/src/renderFolder.ts @@ -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); +} \ No newline at end of file diff --git a/src/renderHeaderButton.ts b/src/renderHeaderButton.ts new file mode 100644 index 0000000..7344194 --- /dev/null +++ b/src/renderHeaderButton.ts @@ -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')); + } +} \ No newline at end of file diff --git a/src/renderModePath.ts b/src/renderModePath.ts new file mode 100644 index 0000000..42f55b4 --- /dev/null +++ b/src/renderModePath.ts @@ -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-')) { + // 使用可點擊的 元素 + 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() 呼叫不變。 +// ---------------------------------------------------- diff --git a/styles.css b/styles.css index 2b07738..c4594fe 100644 --- a/styles.css +++ b/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; diff --git a/versions.json b/versions.json index d294f2a..4924c65 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "2.7.11": "1.1.0" + "2.7.12": "1.1.0" } \ No newline at end of file