diff --git a/main.ts b/main.ts index bd57a76..c81b55d 100644 --- a/main.ts +++ b/main.ts @@ -6,7 +6,6 @@ import { GallerySettings, DEFAULT_SETTINGS, GridExplorerSettingTab } from './src import { t } from './src/translations'; import { updateCustomDocumentExtensions } from './src/fileUtils'; -// 插件類型定義 export default class GridExplorerPlugin extends Plugin { settings: GallerySettings; statusBarItem: HTMLElement; @@ -152,6 +151,39 @@ export default class GridExplorerPlugin extends Plugin { } }) ); + + // 註冊編輯器選單 + this.registerEvent( + this.app.workspace.on('editor-menu', (menu: Menu, editor) => { + if (editor.somethingSelected()) { + const selectedText = editor.getSelection(); + // 截斷過長的文字,最多顯示 20 個字元 + const truncatedText = selectedText.length > 20 ? selectedText.substring(0, 20) + '...' : selectedText; + const menuItemTitle = t('search_selection_in_grid_view').replace('...', `「${truncatedText}」`); // 假設翻譯中有...代表要替換的部分,或者直接格式化 + + menu.addItem(item => { + item + .setTitle(menuItemTitle) + .setIcon('search') + .setSection?.("view") + .onClick(async () => { + const selectedText = editor.getSelection(); + // 取得或啟用 GridView + const view = await this.activateView('folder','/'); + if (view instanceof GridView) { + // 設定搜尋模式和關鍵字 + view.searchQuery = selectedText; + // 設定搜尋範圍 (預設搜尋所有檔案,不含媒體) + view.searchAllFiles = true; + view.searchMediaFiles = false; + // 重新渲染視圖 + view.render(true); // resetScroll = true + } + }); + }); + } + }) + ); } async openNoteInRecentFiles(file: TFile) { diff --git a/manifest.json b/manifest.json index f2ad784..6397e8a 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "gridexplorer", "name": "GridExplorer", - "version": "1.9.10", + "version": "1.9.11", "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 3786015..111dc1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gridexplorer", - "version": "1.9.10", + "version": "1.9.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gridexplorer", - "version": "1.9.10", + "version": "1.9.11", "license": "MIT", "devDependencies": { "@types/node": "^16.11.6", diff --git a/src/FloatingAudioPlayer.ts b/src/FloatingAudioPlayer.ts new file mode 100644 index 0000000..7033c98 --- /dev/null +++ b/src/FloatingAudioPlayer.ts @@ -0,0 +1,212 @@ +import { App, TFile, setIcon } from 'obsidian'; + +export class FloatingAudioPlayer { + private app: App; + private currentFile: TFile; + private containerEl: HTMLElement; + private audioEl: HTMLAudioElement; + private titleEl: HTMLElement; + private closeButtonEl: HTMLElement; + private handleEl: HTMLElement; + + private isDragging = false; + private offsetX = 0; + private offsetY = 0; + private isTouchEvent = false; + + // 使用靜態 Map 來追蹤已開啟的播放器實例 (以檔案路徑為 key) + private static players: Map = new Map(); + + // --- Private Event Handlers (Bound) --- + private boundHandleDragStartMouse: (e: MouseEvent) => void; + private boundHandleDragStartTouch: (e: TouchEvent) => void; + private boundHandleDragMoveMouse: (e: MouseEvent) => void; + private boundHandleDragMoveTouch: (e: TouchEvent) => void; + private boundHandleDragEndMouse: () => void; + private boundHandleDragEndTouch: () => void; + private boundClose: () => void; + + private constructor(app: App, file: TFile) { + this.app = app; + this.currentFile = file; + + // 綁定事件處理器 + this.boundHandleDragStartMouse = this.handleDragStartMouse.bind(this); + this.boundHandleDragStartTouch = this.handleDragStartTouch.bind(this); + this.boundHandleDragMoveMouse = this.handleDragMoveMouse.bind(this); + this.boundHandleDragMoveTouch = this.handleDragMoveTouch.bind(this); + this.boundHandleDragEndMouse = this.handleDragEndMouse.bind(this); + this.boundHandleDragEndTouch = this.handleDragEndTouch.bind(this); + this.boundClose = this.close.bind(this); + + this.buildUI(); + this.setupDragEvents(); + } + + // --- 靜態方法:開啟或取得播放器 --- + public static open(app: App, file: TFile): FloatingAudioPlayer { + // 檢查是否已有相同檔案的播放器 + if (FloatingAudioPlayer.players.has(file.path)) { + const existingPlayer = FloatingAudioPlayer.players.get(file.path)!; + existingPlayer.focus(); // 聚焦到現有播放器 + return existingPlayer; + } + + // 檢查是否有其他播放器存在 + if (FloatingAudioPlayer.players.size > 0) { + // 更新第一個找到的播放器 + const firstPlayer = FloatingAudioPlayer.players.values().next().value as FloatingAudioPlayer; + firstPlayer.updatePlayer(file); + firstPlayer.focus(); + return firstPlayer; + } + + // 創建新的播放器 + const newPlayer = new FloatingAudioPlayer(app, file); + FloatingAudioPlayer.players.set(file.path, newPlayer); + newPlayer.show(); // 顯示並播放 + return newPlayer; + } + + // --- Private UI 和事件設定方法 --- + private buildUI(): void { + this.containerEl = document.createElement('div'); + this.containerEl.className = 'ge-floating-audio-player'; + this.containerEl.setAttribute('data-file', this.currentFile.path); + + this.audioEl = document.createElement('audio'); + this.audioEl.controls = true; + this.audioEl.src = this.app.vault.getResourcePath(this.currentFile); + + this.titleEl = document.createElement('div'); + this.titleEl.className = 'ge-audio-title'; + this.titleEl.textContent = this.currentFile.basename; + + this.closeButtonEl = document.createElement('div'); + this.closeButtonEl.className = 'ge-audio-close-button'; + setIcon(this.closeButtonEl, 'x'); + this.closeButtonEl.addEventListener('click', this.boundClose); + + this.handleEl = document.createElement('div'); + this.handleEl.className = 'ge-audio-handle'; + + this.containerEl.appendChild(this.handleEl); + this.containerEl.appendChild(this.titleEl); + this.containerEl.appendChild(this.audioEl); + this.containerEl.appendChild(this.closeButtonEl); + } + + private setupDragEvents(): void { + this.handleEl.addEventListener('mousedown', this.boundHandleDragStartMouse); + this.handleEl.addEventListener('touchstart', this.boundHandleDragStartTouch, { passive: true }); + + document.addEventListener('mousemove', this.boundHandleDragMoveMouse); + document.addEventListener('touchmove', this.boundHandleDragMoveTouch, { passive: false }); + + document.addEventListener('mouseup', this.boundHandleDragEndMouse); + document.addEventListener('touchend', this.boundHandleDragEndTouch); + } + + private removeDragEvents(): void { + this.handleEl.removeEventListener('mousedown', this.boundHandleDragStartMouse); + this.handleEl.removeEventListener('touchstart', this.boundHandleDragStartTouch); + + document.removeEventListener('mousemove', this.boundHandleDragMoveMouse); + document.removeEventListener('touchmove', this.boundHandleDragMoveTouch); + + document.removeEventListener('mouseup', this.boundHandleDragEndMouse); + document.removeEventListener('touchend', this.boundHandleDragEndTouch); + } + + // --- Private 事件處理器 --- + private handleDragStartMouse(e: MouseEvent): void { + if (this.isTouchEvent) return; + this.isDragging = true; + this.offsetX = e.clientX - this.containerEl.getBoundingClientRect().left; + this.offsetY = e.clientY - this.containerEl.getBoundingClientRect().top; + this.containerEl.classList.add('ge-audio-dragging'); + } + + private handleDragStartTouch(e: TouchEvent): void { + this.isTouchEvent = true; + this.isDragging = true; + const touch = e.touches[0]; + this.offsetX = touch.clientX - this.containerEl.getBoundingClientRect().left; + this.offsetY = touch.clientY - this.containerEl.getBoundingClientRect().top; + this.containerEl.classList.add('ge-audio-dragging'); + } + + private handleDragMoveMouse(e: MouseEvent): void { + if (!this.isDragging || this.isTouchEvent) return; + this.movePlayer(e.clientX, e.clientY); + } + + private handleDragMoveTouch(e: TouchEvent): void { + if (!this.isDragging) return; + const touch = e.touches[0]; + this.movePlayer(touch.clientX, touch.clientY); + e.preventDefault(); // 防止頁面滾動 + } + + private handleDragEndMouse(): void { + if (this.isTouchEvent) return; + this.isDragging = false; + this.containerEl.classList.remove('ge-audio-dragging'); + } + + private handleDragEndTouch(): void { + this.isDragging = false; + this.isTouchEvent = false; // 重置觸控標記 + this.containerEl.classList.remove('ge-audio-dragging'); + } + + private movePlayer(clientX: number, clientY: number): void { + const x = clientX - this.offsetX; + const y = clientY - this.offsetY; + this.containerEl.style.left = `${x}px`; + this.containerEl.style.top = `${y}px`; + } + + // --- Public 方法 --- + public show(): void { + document.body.appendChild(this.containerEl); + + // 設定初始位置(右下角) + const rect = this.containerEl.getBoundingClientRect(); + this.containerEl.style.left = `${window.innerWidth - rect.width - 20}px`; + this.containerEl.style.top = `${window.innerHeight - rect.height - 20}px`; + + this.audioEl.play(); + } + + public close(): void { + this.removeDragEvents(); // 清理事件監聽器 + this.containerEl.remove(); // 從 DOM 移除 + FloatingAudioPlayer.players.delete(this.currentFile.path); // 從靜態 Map 移除 + } + + public focus(): void { + this.containerEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); + // 可以考慮添加一些視覺提示,例如短暫閃爍邊框 + this.containerEl.style.transition = 'box-shadow 0.1s ease-in-out'; + this.containerEl.style.boxShadow = '0 0 10px 2px var(--interactive-accent)'; + setTimeout(() => { + this.containerEl.style.boxShadow = ''; + }, 300); + } + + // 更新播放器以播放新檔案 + public updatePlayer(newFile: TFile): void { + // 從 Map 中移除舊的 key + FloatingAudioPlayer.players.delete(this.currentFile.path); + // 更新檔案引用和 Map 中的新 key + this.currentFile = newFile; + FloatingAudioPlayer.players.set(this.currentFile.path, this); + + // 更新 UI + this.containerEl.setAttribute('data-file', this.currentFile.path); + this.audioEl.src = this.app.vault.getResourcePath(this.currentFile); + this.titleEl.textContent = this.currentFile.basename; + this.audioEl.play(); + } +} diff --git a/src/GridView.ts b/src/GridView.ts index b87d8cd..c3ede5d 100644 --- a/src/GridView.ts +++ b/src/GridView.ts @@ -8,7 +8,8 @@ import { showNoteColorSettingsModal } from './NoteColorSettingsModal'; import { showFolderRenameModal } from './FolderRenameModal'; import { showSearchModal } from './SearchModal'; import { FileWatcher } from './FileWatcher'; -import { isDocumentFile, isMediaFile, isImageFile, isVideoFile, isAudioFile } from './fileUtils'; +import { isDocumentFile, isMediaFile, isImageFile, isVideoFile, isAudioFile, sortFiles, ignoredFiles, getFiles } from './fileUtils'; +import { FloatingAudioPlayer } from './FloatingAudioPlayer'; import { t } from './translations'; import GridExplorerPlugin from '../main'; @@ -136,265 +137,6 @@ export class GridView extends ItemView { this.app.workspace.requestSaveLayout(); } - async getFiles(): Promise { - if (this.sourceMode === 'folder' && this.sourcePath) { - // 獲取指定資料夾內的所有 Markdown、圖片和影片檔案 - const folder = this.app.vault.getAbstractFileByPath(this.sourcePath); - if (folder instanceof TFolder) { - // 只取得當前資料夾中的支援檔案,不包含子資料夾 - const files = folder.children.filter((file): file is TFile => { - if (!(file instanceof TFile)) return false; - - // 如果是 Markdown 檔案,直接包含 - if (isDocumentFile(file) || - (this.plugin.settings.showMediaFiles && isMediaFile(file))) { - return true; - } - - return false; - }); - - return this.sortFiles(files); - } - return []; - } else if (this.sourceMode === 'search') { - // 搜尋模式:使用 Obsidian 的搜尋功能 - const globalSearchPlugin = (this.app as any).internalPlugins.getPluginById('global-search'); - if (globalSearchPlugin?.instance) { - const searchLeaf = (this.app as any).workspace.getLeavesOfType('search')[0]; - if (searchLeaf && searchLeaf.view && searchLeaf.view.dom) { - const resultDomLookup = searchLeaf.view.dom.resultDomLookup; - if (resultDomLookup) { - const files = Array.from(resultDomLookup.keys()) - .filter((file): file is TFile => file instanceof TFile); - return this.sortFiles(files); - } - } - } - return []; - } else if (this.sourceMode === 'backlinks') { - - if(this.searchQuery !== '') { - return []; - } - - // 反向連結模式:找出所有引用當前筆記的檔案 - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) { - return []; - } - - const backlinks = new Set(); - // 使用 resolvedLinks 來找出反向連結 - const resolvedLinks = this.app.metadataCache.resolvedLinks; - for (const [sourcePath, links] of Object.entries(resolvedLinks)) { - if (Object.keys(links).includes(activeFile.path)) { - const sourceFile = this.app.vault.getAbstractFileByPath(sourcePath) as TFile; - if (sourceFile) { - backlinks.add(sourceFile); - } - } - } - - return this.sortFiles(Array.from(backlinks) as TFile[]); - } else if(this.sourceMode === 'bookmarks') { - // 書籤模式 - const bookmarksPlugin = (this.app as any).internalPlugins.plugins.bookmarks; - if (!bookmarksPlugin?.enabled) { - return []; - } - - const bookmarks = bookmarksPlugin.instance.items; - const bookmarkedFiles = new Set(); - - const processBookmarkItem = (item: any) => { - if (item.type === 'file') { - const file = this.app.vault.getAbstractFileByPath(item.path); - if (file instanceof TFile) { - // 根據設定決定是否包含媒體檔案 - if (isDocumentFile(file) || - (this.plugin.settings.showMediaFiles && isMediaFile(file))) { - bookmarkedFiles.add(file); - } - } - } else if (item.type === 'group' && item.items) { - item.items.forEach(processBookmarkItem); - } - }; - - bookmarks.forEach(processBookmarkItem); - return Array.from(bookmarkedFiles) as TFile[]; - } else if (this.sourceMode === 'all-files') { - // 所有筆記模式 - const recentFiles = this.app.vault.getFiles().filter(file => { - // 根據設定決定是否包含媒體檔案 - if (isDocumentFile(file) || - (this.plugin.settings.showMediaFiles && this.randomNoteIncludeMedia && isMediaFile(file))) { - return true; - } - return false; - }); - return this.sortFiles(recentFiles); - } else if (this.sourceMode === 'recent-files') { - // 最近檔案模式 - const recentFiles = this.app.vault.getFiles().filter(file => { - // 根據設定決定是否包含媒體檔案 - if (isDocumentFile(file) || - (this.plugin.settings.showMediaFiles && this.randomNoteIncludeMedia && isMediaFile(file))) { - return true; - } - return false; - }); - //臨時的排序類型 - const sortType = this.sortType; - this.sortType = 'mtime-desc'; - const sortedFiles = this.sortFiles(recentFiles); - this.sortType = sortType; - return sortedFiles; - } else if (this.sourceMode === 'random-note') { - // 隨機筆記模式,從所有筆記中隨機選取10筆 - const recentFiles = this.app.vault.getFiles().filter(file => { - // 根據設定決定是否包含媒體檔案 - if (isDocumentFile(file) || - (this.plugin.settings.showMediaFiles && this.randomNoteIncludeMedia && isMediaFile(file))) { - return true; - } - return false; - }).sort(() => Math.random() - 0.5); - return recentFiles; - } else { - return []; - } - } - - //排序檔案 - sortFiles(files: TFile[]) { - const sortType = this.folderSortType ? this.folderSortType : this.sortType; - - // 檢查排序類型是否為非日期相關 - const isNonDateSort = ['name-asc', 'name-desc', 'random'].includes(sortType); - - // 檢查是否有任何日期欄位的設定 - const hasModifiedField = !!this.plugin.settings.modifiedDateField; - const hasCreatedField = !!this.plugin.settings.createdDateField; - const hasAnyDateField = hasModifiedField || hasCreatedField; - - // 符合以下任一條件就使用簡單排序: - // 1. 非日期排序類型 (name-asc, name-desc, random) - // 2. 沒有設定任何日期欄位 - const shouldUseSimpleSort = isNonDateSort || !hasAnyDateField; - if (shouldUseSimpleSort) { - if (sortType === 'name-asc') { - return files.sort((a, b) => a.basename.localeCompare(b.basename)); - } else if (sortType === 'name-desc') { - return files.sort((a, b) => b.basename.localeCompare(a.basename)); - } else if (sortType === 'mtime-desc') { - return files.sort((a, b) => b.stat.mtime - a.stat.mtime); - } else if (sortType === 'mtime-asc') { - return files.sort((a, b) => a.stat.mtime - b.stat.mtime); - } else if (sortType === 'ctime-desc') { - return files.sort((a, b) => b.stat.ctime - a.stat.ctime); - } else if (sortType === 'ctime-asc') { - return files.sort((a, b) => a.stat.ctime - b.stat.ctime); - } else if (sortType === 'random') { - return files.sort(() => Math.random() - 0.5); - } else { - return files; - } - } - - // 處理需要讀取metadata的日期排序情況 - // 只有在以下條件都成立時才會執行: - // 1. 是日期排序類型 (mtime-desc, mtime-asc, ctime-desc, ctime-asc) - // 2. 至少設定了一個日期欄位 (modifiedDateField 或 createdDateField) - const filesWithDates = files.map(file => { - // 只對 .md 檔案讀取 metadata - const shouldReadMetadata = file.extension === 'md'; - const metadata = shouldReadMetadata ? this.app.metadataCache.getFileCache(file) : null; - - return { - file, - mDate: (() => { - if (metadata?.frontmatter) { - const fieldName = this.plugin.settings.modifiedDateField; - const dateStr = metadata.frontmatter[fieldName]; - if (dateStr) { - const date = new Date(dateStr); - if (!isNaN(date.getTime())) { - return date.getTime(); - } - } - } - return file.stat.mtime; - })(), - cDate: (() => { - if (metadata?.frontmatter) { - const fieldName = this.plugin.settings.createdDateField; - const dateStr = metadata.frontmatter[fieldName]; - if (dateStr) { - const date = new Date(dateStr); - if (!isNaN(date.getTime())) { - return date.getTime(); - } - } - } - return file.stat.ctime; - })() - }; - }); - - if (sortType === 'mtime-desc') { - return filesWithDates.sort((a, b) => b.mDate - a.mDate).map(item => item.file); - } else if (sortType === 'mtime-asc') { - return filesWithDates.sort((a, b) => a.mDate - b.mDate).map(item => item.file); - } else if (sortType === 'ctime-desc') { - return filesWithDates.sort((a, b) => b.cDate - a.cDate).map(item => item.file); - } else if (sortType === 'ctime-asc') { - return filesWithDates.sort((a, b) => a.cDate - b.cDate).map(item => item.file); - } else { - return files; - } - } - - //忽略檔案 - ignoredFiles(files: TFile[]) { - return files.filter(file => { - // 檢查是否在忽略的資料夾中 - const isInIgnoredFolder = this.plugin.settings.ignoredFolders.some(folder => - file.path.startsWith(`${folder}/`) - ); - - if (isInIgnoredFolder) { - return false; - } - - // 檢查資料夾是否符合忽略的模式 - if (this.plugin.settings.ignoredFolderPatterns && this.plugin.settings.ignoredFolderPatterns.length > 0) { - const matchesIgnoredPattern = this.plugin.settings.ignoredFolderPatterns.some(pattern => { - try { - // 嘗試將模式作為正則表達式處理 - // 如果模式包含特殊字符,使用正則表達式處理 - if (/[\^\$\*\+\?\(\)\[\]\{\}\|\\]/.test(pattern)) { - const regex = new RegExp(pattern); - // 檢查檔案路徑是否符合正則表達式 - return regex.test(file.path); - } else { - // 如果模式不包含特殊字符,直接檢查檔案路徑 - return file.path.toLowerCase().includes(pattern.toLowerCase()) - } - } catch (error) { - // 如果正則表達式無效,直接檢查檔案路徑 - return file.path.toLowerCase().includes(pattern.toLowerCase()) - } - }); - // 如果符合任何忽略模式,則忽略此檔案 - return !matchesIgnoredPattern; - } - - return true; - }); - } - async render(resetScroll = false) { // 儲存當前捲動位置 const scrollContainer = this.containerEl.children[1] as HTMLElement; @@ -500,7 +242,7 @@ export class GridView extends ItemView { // 建立新筆記 const newFile = await this.app.vault.create(newFilePath, ''); // 開啟新筆記 - await this.app.workspace.getLeaf().openFile(newFile); + await this.app.workspace.getLeaf().openFile(newFile); } catch (error) { console.error('An error occurred while creating a new note:', error); } @@ -799,7 +541,7 @@ export class GridView extends ItemView { }); setIcon(searchButton, 'search'); searchButton.addEventListener('click', () => { - this.showSearchModal(); + showSearchModal(this.app, this, ''); }); // 如果有搜尋關鍵字,顯示搜尋文字和取消按鈕 @@ -812,7 +554,7 @@ export class GridView extends ItemView { // 讓搜尋文字可點選 searchText.style.cursor = 'pointer'; searchText.addEventListener('click', () => { - this.showSearchModal(this.searchQuery); + showSearchModal(this.app, this, this.searchQuery); }); // 創建取消按鈕 @@ -1025,6 +767,7 @@ export class GridView extends ItemView { const contentArea = folderEl.createDiv('ge-content-area'); const titleContainer = contentArea.createDiv('ge-title-container'); titleContainer.createEl('span', { cls: 'ge-title', text: `📁 ${folder.name}` }); + titleContainer.setAttribute('title', folder.name); // 檢查同名筆記是否存在 const notePath = `${folder.path}/${folder.name}.md`; @@ -1168,7 +911,7 @@ export class GridView extends ItemView { if (this.searchAllFiles) { allFiles = this.app.vault.getFiles(); } else { - allFiles = await this.getFiles(); + allFiles = await getFiles(this); } // 根據設定過濾檔案 @@ -1203,17 +946,17 @@ export class GridView extends ItemView { ); // 根據設定的排序方式排序檔案 - files = this.sortFiles(files); + files = sortFiles(files, this); // 移除搜尋中的提示 loadingDiv.remove(); } else { // 獲取檔案列表並根據搜尋關鍵字過濾 - files = await this.getFiles(); + files = await getFiles(this); } //忽略檔案 - files = this.ignoredFiles(files) + files = ignoredFiles(files, this) //最近檔案模式,只取前30筆 if (this.sourceMode === 'recent-files') { @@ -1228,7 +971,11 @@ export class GridView extends ItemView { // 如果沒有檔案,顯示提示訊息 if (files.length === 0) { const noFilesDiv = container.createDiv('ge-no-files'); - noFilesDiv.setText(t('no_files')); + if (this.sourceMode !== 'backlinks') { + noFilesDiv.setText(t('no_files')); + } else { + noFilesDiv.setText(t('no_backlinks')); + } if (this.plugin.statusBarItem) { this.plugin.statusBarItem.setText(''); } @@ -1406,13 +1153,27 @@ export class GridView extends ItemView { // 處理多選邏輯 if (event.ctrlKey || event.metaKey) { - // Ctrl/Cmd 鍵:切換選中狀態 - this.selectItem(index, true); - this.hasKeyboardFocus = true; + if (this.selectedItemIndex !== -1) { + // 如果已有選中狀態,則繼續選中 + this.selectItem(index, true); + this.hasKeyboardFocus = true; + } else { + // 沒有選中狀態則開啟新分頁 + if (isMediaFile(file)) { + // 開啟媒體檔案 + if (isAudioFile(file)) { + FloatingAudioPlayer.open(this.app, file); + } else { + this.openMediaFile(file, files); + } + } else { + // 開啟文件檔案 + this.app.workspace.getLeaf(true).openFile(file); + } + } event.preventDefault(); return; } else if (event.shiftKey) { - //if (event.shiftKey) { // Shift 鍵:範圍選擇 this.handleRangeSelection(index); this.hasKeyboardFocus = true; @@ -1427,17 +1188,13 @@ export class GridView extends ItemView { if (isMediaFile(file)) { // 開啟媒體檔案 if (isAudioFile(file)) { - this.openAudioFile(file); + FloatingAudioPlayer.open(this.app, file); } else { this.openMediaFile(file, files); } } else { // 開啟文件檔案 - if (event.ctrlKey || event.metaKey) { - this.app.workspace.getLeaf(true).openFile(file); - } else { - this.app.workspace.getLeaf().openFile(file); - } + this.app.workspace.getLeaf().openFile(file); } } }); @@ -1902,7 +1659,7 @@ export class GridView extends ItemView { // 如果沒有傳入媒體檔案列表,則獲取 const getMediaFilesPromise = mediaFiles ? Promise.resolve(mediaFiles.filter(f => isMediaFile(f))) - : this.getFiles().then(allFiles => allFiles.filter(f => isMediaFile(f))); + : getFiles(this).then(allFiles => allFiles.filter(f => isMediaFile(f))); getMediaFilesPromise.then(filteredMediaFiles => { // 找到當前檔案在媒體檔案列表中的索引 @@ -1914,154 +1671,6 @@ export class GridView extends ItemView { mediaModal.open(); }); } - - // 開啟音樂檔案 - openAudioFile(file: TFile) { - // 檢查是否已有相同檔案的播放器 - const existingPlayer = document.querySelector(`.ge-floating-audio-player[data-file="${file.path}"]`); - - // 查詢所有已經存在的音樂播放器 - const existingPlayers = document.querySelectorAll('.ge-floating-audio-player'); - - if (existingPlayer) { - // 如果已有相同檔案播放器,則聚焦到該播放器 - existingPlayer.scrollIntoView({ behavior: 'smooth', block: 'center' }); - return; - } else if (existingPlayers.length > 0) { - // 如果有其他音樂播放器,則聚焦到第一個播放器並更新它 - const firstPlayer = existingPlayers[0] as HTMLElement; - - // 聚焦到播放器 - firstPlayer.scrollIntoView({ behavior: 'smooth', block: 'center' }); - - // 更新播放器的檔案屬性 - firstPlayer.setAttribute('data-file', file.path); - - // 更新音訊來源 - const audioElement = firstPlayer.querySelector('audio'); - if (audioElement) { - audioElement.src = this.app.vault.getResourcePath(file); - audioElement.play(); - } - - // 更新標題 - const titleElement = firstPlayer.querySelector('.ge-audio-title'); - if (titleElement) { - titleElement.textContent = file.basename; - } - - return; - } - - // 創建音樂播放器容器 - const audioPlayerContainer = document.createElement('div'); - audioPlayerContainer.className = 'ge-floating-audio-player'; - audioPlayerContainer.setAttribute('data-file', file.path); - - // 創建音樂元素 - const audio = document.createElement('audio'); - audio.controls = true; - audio.src = this.app.vault.getResourcePath(file); - - // 創建標題元素 - const titleElement = document.createElement('div'); - titleElement.className = 'ge-audio-title'; - titleElement.textContent = file.basename; - - // 創建關閉按鈕 - const closeButton = document.createElement('div'); - closeButton.className = 'ge-audio-close-button'; - setIcon(closeButton, 'x'); - closeButton.addEventListener('click', () => { - // 移除音樂播放器 - audioPlayerContainer.remove(); - }); - - // 創建拖曳控制元素 - const handleElement = document.createElement('div'); - handleElement.className = 'ge-audio-handle'; - - // 將元素添加到容器中 - audioPlayerContainer.appendChild(handleElement); - audioPlayerContainer.appendChild(titleElement); - audioPlayerContainer.appendChild(audio); - audioPlayerContainer.appendChild(closeButton); - - // 設定拖曳事件 - let isDragging = false; - let offsetX = 0; - let offsetY = 0; - let isTouchEvent = false; - - handleElement.addEventListener('mousedown', (e) => { - if (isTouchEvent) return; // 如果是觸控事件觸發的,則忽略滑鼠事件 - isDragging = true; - offsetX = e.clientX - audioPlayerContainer.getBoundingClientRect().left; - offsetY = e.clientY - audioPlayerContainer.getBoundingClientRect().top; - audioPlayerContainer.classList.add('ge-audio-dragging'); - }); - - handleElement.addEventListener('touchstart', (e) => { - isTouchEvent = true; - isDragging = true; - const touch = e.touches[0]; - offsetX = touch.clientX - audioPlayerContainer.getBoundingClientRect().left; - offsetY = touch.clientY - audioPlayerContainer.getBoundingClientRect().top; - audioPlayerContainer.classList.add('ge-audio-dragging'); - }, { passive: true }); - - document.addEventListener('mousemove', (e) => { - if (!isDragging || isTouchEvent) return; - - const x = e.clientX - offsetX; - const y = e.clientY - offsetY; - - audioPlayerContainer.style.left = `${x}px`; - audioPlayerContainer.style.top = `${y}px`; - }); - - document.addEventListener('touchmove', (e) => { - if (!isDragging) return; - - const touch = e.touches[0]; - const x = touch.clientX - offsetX; - const y = touch.clientY - offsetY; - - audioPlayerContainer.style.left = `${x}px`; - audioPlayerContainer.style.top = `${y}px`; - - // 防止頁面滾動 - e.preventDefault(); - }, { passive: false }); - - document.addEventListener('mouseup', () => { - if (isTouchEvent) return; - isDragging = false; - audioPlayerContainer.classList.remove('ge-audio-dragging'); - }); - - document.addEventListener('touchend', () => { - isDragging = false; - isTouchEvent = false; - audioPlayerContainer.classList.remove('ge-audio-dragging'); - }); - - // 將音樂播放器添加到文檔中 - document.body.appendChild(audioPlayerContainer); - - // 設定初始位置 - const rect = audioPlayerContainer.getBoundingClientRect(); - audioPlayerContainer.style.left = `${window.innerWidth - rect.width - 20}px`; - audioPlayerContainer.style.top = `${window.innerHeight - rect.height - 20}px`; - - // 播放音樂 - audio.play(); - } - - // 顯示搜尋 modal - showSearchModal(defaultQuery = '') { - showSearchModal(this.app, this, defaultQuery); - } // 保存視圖狀態 getState() { diff --git a/src/SearchModal.ts b/src/SearchModal.ts index 94d4f2a..04985f3 100644 --- a/src/SearchModal.ts +++ b/src/SearchModal.ts @@ -58,7 +58,7 @@ export class SearchModal extends Modal { cls: 'ge-search-scope-checkbox' }); searchScopeCheckbox.checked = !this.gridView.searchAllFiles; - const searchScopeLabel = searchScopeContainer.createEl('span', { + searchScopeContainer.createEl('span', { text: t('search_current_location_only'), cls: 'ge-search-scope-label' }); @@ -70,7 +70,7 @@ export class SearchModal extends Modal { cls: 'ge-search-media-files-checkbox' }); searchMediaFilesCheckbox.checked = this.gridView.searchMediaFiles; - const searchMediaFilesLabel = searchMediaFilesContainer.createEl('span', { + searchMediaFilesContainer.createEl('span', { text: t('search_media_files'), cls: 'ge-search-media-files-label' }); diff --git a/src/fileUtils.ts b/src/fileUtils.ts index ddd4ccd..f92941f 100644 --- a/src/fileUtils.ts +++ b/src/fileUtils.ts @@ -1,5 +1,6 @@ -import { TFile } from 'obsidian'; +import { TFile, TFolder } from 'obsidian'; import { type GallerySettings } from './settings'; +import { GridView } from './GridView'; let customDocumentExtensions: string[] = []; @@ -45,3 +46,272 @@ export function isAudioFile(file: TFile): boolean { export function isMediaFile(file: TFile): boolean { return isImageFile(file) || isVideoFile(file) || isAudioFile(file); } + +//排序檔案 +export function sortFiles(files: TFile[], gridView: GridView): TFile[] { + const app = gridView.app; + const settings = gridView.plugin.settings; + const sortType = gridView.folderSortType ? gridView.folderSortType : gridView.sortType; + + // 檢查排序類型是否為非日期相關 + const isNonDateSort = ['name-asc', 'name-desc', 'random'].includes(sortType); + + // 檢查是否有任何日期欄位的設定 + const hasModifiedField = !!settings.modifiedDateField; + const hasCreatedField = !!settings.createdDateField; + const hasAnyDateField = hasModifiedField || hasCreatedField; + + // 符合以下任一條件就使用簡單排序: + // 1. 非日期排序類型 (name-asc, name-desc, random) + // 2. 沒有設定任何日期欄位 + const shouldUseSimpleSort = isNonDateSort || !hasAnyDateField; + if (shouldUseSimpleSort) { + if (sortType === 'name-asc') { + return files.sort((a, b) => a.basename.localeCompare(b.basename)); + } else if (sortType === 'name-desc') { + return files.sort((a, b) => b.basename.localeCompare(a.basename)); + } else if (sortType === 'mtime-desc') { + return files.sort((a, b) => b.stat.mtime - a.stat.mtime); + } else if (sortType === 'mtime-asc') { + return files.sort((a, b) => a.stat.mtime - b.stat.mtime); + } else if (sortType === 'ctime-desc') { + return files.sort((a, b) => b.stat.ctime - a.stat.ctime); + } else if (sortType === 'ctime-asc') { + return files.sort((a, b) => a.stat.ctime - b.stat.ctime); + } else if (sortType === 'random') { + return files.sort(() => Math.random() - 0.5); + } else { + return files; + } + } + + // 處理需要讀取metadata的日期排序情況 + // 只有在以下條件都成立時才會執行: + // 1. 是日期排序類型 (mtime-desc, mtime-asc, ctime-desc, ctime-asc) + // 2. 至少設定了一個日期欄位 (modifiedDateField 或 createdDateField) + const filesWithDates = files.map(file => { + // 只對 .md 檔案讀取 metadata + const shouldReadMetadata = file.extension === 'md'; + const metadata = shouldReadMetadata ? app.metadataCache.getFileCache(file) : null; + + return { + file, + mDate: (() => { + if (metadata?.frontmatter) { + const fieldName = settings.modifiedDateField; + const dateStr = metadata.frontmatter[fieldName]; + if (dateStr) { + const date = new Date(dateStr); + if (!isNaN(date.getTime())) { + return date.getTime(); + } + } + } + return file.stat.mtime; + })(), + cDate: (() => { + if (metadata?.frontmatter) { + const fieldName = settings.createdDateField; + const dateStr = metadata.frontmatter[fieldName]; + if (dateStr) { + const date = new Date(dateStr); + if (!isNaN(date.getTime())) { + return date.getTime(); + } + } + } + return file.stat.ctime; + })() + }; + }); + + if (sortType === 'mtime-desc') { + return filesWithDates.sort((a, b) => b.mDate - a.mDate).map(item => item.file); + } else if (sortType === 'mtime-asc') { + return filesWithDates.sort((a, b) => a.mDate - b.mDate).map(item => item.file); + } else if (sortType === 'ctime-desc') { + return filesWithDates.sort((a, b) => b.cDate - a.cDate).map(item => item.file); + } else if (sortType === 'ctime-asc') { + return filesWithDates.sort((a, b) => a.cDate - b.cDate).map(item => item.file); + } else { + return files; + } +} + +//忽略檔案 +export function ignoredFiles(files: TFile[], gridView: GridView): TFile[] { + const settings = gridView.plugin.settings; + return files.filter(file => { + // 檢查是否在忽略的資料夾中 + const isInIgnoredFolder = settings.ignoredFolders.some(folder => + file.path.startsWith(`${folder}/`) + ); + + if (isInIgnoredFolder) { + return false; + } + + // 檢查資料夾是否符合忽略的模式 + if (settings.ignoredFolderPatterns && settings.ignoredFolderPatterns.length > 0) { + const matchesIgnoredPattern = settings.ignoredFolderPatterns.some(pattern => { + try { + // 嘗試將模式作為正則表達式處理 + // 如果模式包含特殊字符,使用正則表達式處理 + if (/[\^\$\*\+\?\(\)\[\]\{\}\|\\]/.test(pattern)) { + const regex = new RegExp(pattern); + // 檢查檔案路徑是否符合正則表達式 + return regex.test(file.path); + } else { + // 如果模式不包含特殊字符,直接檢查檔案路徑 + return file.path.toLowerCase().includes(pattern.toLowerCase()) + } + } catch (error) { + // 如果正則表達式無效,直接檢查檔案路徑 + return file.path.toLowerCase().includes(pattern.toLowerCase()) + } + }); + // 如果符合任何忽略模式,則忽略此檔案 + return !matchesIgnoredPattern; + } + + return true; + }); +} + +// 獲取檔案 +export async function getFiles(gridView: GridView): Promise { + const app = gridView.app; + const settings = gridView.plugin.settings; + const sourceMode = gridView.sourceMode; + const sourcePath = gridView.sourcePath; + const searchQuery = gridView.searchQuery; + const randomNoteIncludeMedia = gridView.randomNoteIncludeMedia; + + if (sourceMode === 'folder' && sourcePath) { + // 獲取指定資料夾內的所有 Markdown、圖片和影片檔案 + const folder = app.vault.getAbstractFileByPath(sourcePath); + if (folder instanceof TFolder) { + // 只取得當前資料夾中的支援檔案,不包含子資料夾 + const files = folder.children.filter((file): file is TFile => { + if (!(file instanceof TFile)) return false; + + // 如果是 Markdown 檔案,直接包含 + if (isDocumentFile(file) || + (settings.showMediaFiles && isMediaFile(file))) { + return true; + } + + return false; + }); + + return sortFiles(files, gridView); + } + return []; + } else if (sourceMode === 'search') { + // 搜尋模式:使用 Obsidian 的搜尋功能 + const globalSearchPlugin = (app as any).internalPlugins.getPluginById('global-search'); + if (globalSearchPlugin?.instance) { + const searchLeaf = (app as any).workspace.getLeavesOfType('search')[0]; + if (searchLeaf && searchLeaf.view && searchLeaf.view.dom) { + const resultDomLookup = searchLeaf.view.dom.resultDomLookup; + if (resultDomLookup) { + const files = Array.from(resultDomLookup.keys()) + .filter((file): file is TFile => file instanceof TFile); + return sortFiles(files, gridView); + } + } + } + return []; + } else if (sourceMode === 'backlinks') { + if(searchQuery !== '') { + return []; + } + + // 反向連結模式:找出所有引用當前筆記的檔案 + const activeFile = app.workspace.getActiveFile(); + if (!activeFile) { + return []; + } + + const backlinks = new Set(); + // 使用 resolvedLinks 來找出反向連結 + const resolvedLinks = app.metadataCache.resolvedLinks; + for (const [sourcePath, links] of Object.entries(resolvedLinks)) { + if (Object.keys(links).includes(activeFile.path)) { + const sourceFile = app.vault.getAbstractFileByPath(sourcePath) as TFile; + if (sourceFile) { + backlinks.add(sourceFile); + } + } + } + + return sortFiles(Array.from(backlinks) as TFile[], gridView); + } else if(sourceMode === 'bookmarks') { + // 書籤模式 + const bookmarksPlugin = (app as any).internalPlugins.plugins.bookmarks; + if (!bookmarksPlugin?.enabled) { + return []; + } + + const bookmarks = bookmarksPlugin.instance.items; + const bookmarkedFiles = new Set(); + + const processBookmarkItem = (item: any) => { + if (item.type === 'file') { + const file = app.vault.getAbstractFileByPath(item.path); + if (file instanceof TFile) { + // 根據設定決定是否包含媒體檔案 + if (isDocumentFile(file) || + (settings.showMediaFiles && isMediaFile(file))) { + bookmarkedFiles.add(file); + } + } + } else if (item.type === 'group' && item.items) { + item.items.forEach(processBookmarkItem); + } + }; + + bookmarks.forEach(processBookmarkItem); + return Array.from(bookmarkedFiles) as TFile[]; + } else if (sourceMode === 'all-files') { + // 所有筆記模式 + const allVaultFiles = app.vault.getFiles().filter(file => { + // 根據設定決定是否包含媒體檔案 + if (isDocumentFile(file) || + (settings.showMediaFiles && randomNoteIncludeMedia && isMediaFile(file))) { + return true; + } + return false; + }); + return sortFiles(allVaultFiles, gridView); + } else if (sourceMode === 'recent-files') { + // 最近檔案模式 + const recentFiles = app.vault.getFiles().filter(file => { + // 根據設定決定是否包含媒體檔案 + if (isDocumentFile(file) || + (settings.showMediaFiles && randomNoteIncludeMedia && isMediaFile(file))) { + return true; + } + return false; + }); + //臨時的排序類型 + const sortType = gridView.sortType; + gridView.sortType = 'mtime-desc'; + const sortedFiles = sortFiles(recentFiles, gridView); + gridView.sortType = sortType; + return sortedFiles; + } else if (sourceMode === 'random-note') { + // 隨機筆記模式,從所有筆記中隨機選取 + const randomFiles = app.vault.getFiles().filter(file => { + // 根據設定決定是否包含媒體檔案 + if (isDocumentFile(file) || + (settings.showMediaFiles && randomNoteIncludeMedia && isMediaFile(file))) { + return true; + } + return false; + }).sort(() => Math.random() - 0.5); + return randomFiles; // 隨機模式不需要額外排序 + } else { + return []; + } +} diff --git a/src/settings.ts b/src/settings.ts index 2798a7f..1152a99 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -160,7 +160,7 @@ export class GridExplorerSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.recentFilesCount.toString()) .onChange(async (value) => { this.plugin.settings.recentFilesCount = parseInt(value); - await this.plugin.saveSettings(); + await this.plugin.saveSettings(false); }); }); @@ -184,7 +184,7 @@ export class GridExplorerSettingTab extends PluginSettingTab { .setValue(this.plugin.settings.randomNoteCount.toString()) .onChange(async (value) => { this.plugin.settings.randomNoteCount = parseInt(value); - await this.plugin.saveSettings(); + await this.plugin.saveSettings(false); }); }); diff --git a/src/translations.ts b/src/translations.ts index 4f89f42..dfe4ee4 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -173,6 +173,7 @@ export const TRANSLATIONS: Translations = { 'set_note_color': '設定筆記顏色', 'rename_folder': '重新命名資料夾', 'enter_new_folder_name': '輸入新資料夾名稱', + 'search_selection_in_grid_view': '在網格視圖中搜尋...', }, 'en': { // Notifications @@ -328,6 +329,7 @@ export const TRANSLATIONS: Translations = { 'set_note_color': 'Set note color', 'rename_folder': 'Rename folder', 'enter_new_folder_name': 'Enter new folder name', + 'search_selection_in_grid_view': 'Search ... in grid view', }, 'zh': { // 通知信息 @@ -483,6 +485,7 @@ export const TRANSLATIONS: Translations = { 'set_note_color': '设置笔记颜色', 'rename_folder': '重新命名文件夹', 'enter_new_folder_name': '输入新文件夹名称', + 'search_selection_in_grid_view': '在网格视图中搜寻...', }, 'ja': { // 通知メッジ @@ -638,5 +641,6 @@ export const TRANSLATIONS: Translations = { 'set_note_color': 'ノート色を設定', 'rename_folder': 'フォルダを再命名', 'enter_new_folder_name': '新しいフォルダ名を入力', + 'search_selection_in_grid_view': '... をグリッドビューで検索', } } diff --git a/styles.css b/styles.css index 145db15..e87eb8e 100644 --- a/styles.css +++ b/styles.css @@ -38,7 +38,7 @@ background-color: var(--background-primary); border: 1px solid var(--background-modifier-border); border-radius: var(--button-radius); - padding: 12px; + padding: 10px; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; display: flex; @@ -132,7 +132,7 @@ } .ge-content-area p { - margin: 8px 0 0 0; + margin: 5px 0 0 0; color: var(--text-faint); font-size: 0.85em; line-height: 1.4; diff --git a/versions.json b/versions.json index d0dd728..70003c2 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "1.9.10": "1.1.0" + "1.9.11": "1.1.0" } \ No newline at end of file