diff --git a/manifest.json b/manifest.json index 5809fa9..c6978e9 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "gridexplorer", "name": "GridExplorer", - "version": "2.8.1", + "version": "2.8.2", "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 37b72d9..89eff4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gridexplorer", - "version": "2.8.1", + "version": "2.8.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gridexplorer", - "version": "2.8.1", + "version": "2.8.2", "license": "MIT", "devDependencies": { "@types/node": "^16.11.6", diff --git a/package.json b/package.json index faeac6f..e05e0d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gridexplorer", - "version": "2.8.1", + "version": "2.8.2", "description": "Browse note files in a grid view.", "main": "main.js", "scripts": { diff --git a/src/GridView.ts b/src/GridView.ts index 33843d2..a5f4401 100644 --- a/src/GridView.ts +++ b/src/GridView.ts @@ -190,20 +190,27 @@ export class GridView extends ItemView { const limit = 15; if (this.recentSources.length > limit) { this.recentSources.length = limit; - } + } } - // resetScroll 為 true 時,會將捲動位置重置到最頂部 - // recordHistory 為 false 時,不會將當前狀態加入歷史記錄 - async setSource(mode: string, path = '', resetScroll = false, recordHistory = true) { + // 設定來源模式 + async setSource( + mode: string, + path = '', + recordHistory = true, // 是否將當前狀態加入歷史記錄 + searchQuery?: string, + searchAllFiles?: boolean, + searchFilesNameOnly?: boolean, + searchMediaFiles?: boolean + ) { // 如果新的狀態與當前狀態相同,則不進行任何操作 - if (this.sourceMode === mode && + if (this.sourceMode === mode && this.sourcePath === path && - this.searchQuery === this.searchQuery && - this.searchAllFiles === this.searchAllFiles && - this.searchFilesNameOnly === this.searchFilesNameOnly && - this.searchMediaFiles === this.searchMediaFiles) { + this.searchQuery === searchQuery && + this.searchAllFiles === searchAllFiles && + this.searchFilesNameOnly === searchFilesNameOnly && + this.searchMediaFiles === searchMediaFiles) { return; } @@ -223,13 +230,13 @@ export class GridView extends ItemView { if (this.searchQuery !== '' && this.searchAllFiles) { this.searchQuery = ''; } - + // 更新來源模式和路徑 - if(mode !== '') this.sourceMode = mode; - if(path !== '') this.sourcePath = path; - if(this.sourceMode === '') this.sourceMode = 'folder'; - if(this.sourcePath === '') this.sourcePath = '/'; - + if (mode !== '') this.sourceMode = mode; + if (path !== '') this.sourcePath = path; + if (this.sourceMode === '') this.sourceMode = 'folder'; + if (this.sourcePath === '') this.sourcePath = '/'; + // 讀取Folder設定 this.folderSortType = ''; this.pinnedList = []; @@ -253,22 +260,34 @@ export class GridView extends ItemView { this.sourcePath = '/'; // 強制設定路徑為根目錄 (創建筆記用) // 切換到自訂模式時重設選項索引並重設排序方式 - if(this.sourceMode.startsWith('custom-')) { + if (this.sourceMode.startsWith('custom-')) { this.customOptionIndex = -1; this.folderSortType = 'none'; } } + // 設定搜尋相關狀態 + if (searchQuery !== undefined) { + this.searchQuery = searchQuery; + } + if (searchAllFiles !== undefined) { + this.searchAllFiles = searchAllFiles; + } + if (searchFilesNameOnly !== undefined) { + this.searchFilesNameOnly = searchFilesNameOnly; + } + if (searchMediaFiles !== undefined) { + this.searchMediaFiles = searchMediaFiles; + } + // 通知 Obsidian 保存視圖狀態 this.app.workspace.requestSaveLayout(); - await this.render(resetScroll); + await this.render(); } - async render(resetScroll = false) { - // 儲存當前捲動位置 - const scrollContainer = this.containerEl.children[1] as HTMLElement; - const scrollTop = resetScroll ? 0 : (scrollContainer ? scrollContainer.scrollTop : 0); + // 渲染網格 + async render() { // 保存選中項目的檔案路徑(如果有) let selectedFilePath: string | null = null; @@ -282,7 +301,7 @@ export class GridView extends ItemView { // 添加頂部按鈕 renderHeaderButton(this); - + // 顯示路徑 / 模式名稱 renderModePath(this); @@ -317,14 +336,10 @@ export class GridView extends ItemView { } }; - // 重新渲染內容 + // 渲染網格內容 await this.grid_render(); (this.leaf as any).updateHeader(); - // 恢復捲動位置 - if (scrollContainer && !resetScroll) { - contentEl.scrollTop = scrollTop; - } // 如果有之前選中的檔案路徑,嘗試恢復選中狀態 if (selectedFilePath && this.hasKeyboardFocus) { @@ -333,8 +348,11 @@ export class GridView extends ItemView { this.selectItem(newIndex); } } + + // new Notice('GridExplorer: ' + this.sourceMode + ' ' + this.sourcePath); } + // 渲染網格內容 async grid_render() { const container = this.containerEl.querySelector('.view-content') as HTMLElement; container.empty(); @@ -344,7 +362,7 @@ export class GridView extends ItemView { const displayValue = this.hideHeaderElements ? 'none' : 'flex'; const headerButtons = this.containerEl.querySelector('.ge-header-buttons') as HTMLElement; const modeHeaderContainer = this.containerEl.querySelector('.ge-mode-header-container') as HTMLElement; - + if (headerButtons) headerButtons.style.display = displayValue; if (modeHeaderContainer) modeHeaderContainer.style.display = displayValue; @@ -417,7 +435,7 @@ export class GridView extends ItemView { } else if (modeClasses.includes(this.sourceMode)) { this.containerEl.addClass(`ge-mode-${this.sourceMode}`); } - + // 重置網格項目數組 this.gridItems = []; @@ -439,7 +457,7 @@ export class GridView extends ItemView { // 顯示資料夾 renderFolder(this, container); - + // 顯示檔案 let files = await renderFiles(this, container); @@ -465,7 +483,7 @@ export class GridView extends ItemView { const otherFiles = files.filter(f => !this.pinnedList.includes(f.name)); files = [...pinnedFiles, ...otherFiles]; } - + // 如果資料夾筆記設定為隱藏,則隱藏資料夾筆記 if (this.sourceMode === 'folder' && this.sourcePath !== '/') { if (this.plugin.settings.folderNoteDisplaySettings === 'hidden') { @@ -481,7 +499,7 @@ export class GridView extends ItemView { const observer = new IntersectionObserver((entries, observer) => { entries.forEach(async entry => { if (entry.isIntersecting) { - const fileEl = entry.target as HTMLElement; + const fileEl = entry.target as HTMLElement; const filePath = fileEl.dataset.filePath; if (!filePath) return; @@ -525,13 +543,13 @@ export class GridView extends ItemView { const option = mode.options[this.customOptionIndex]; fields = option.fields || ''; } - + // 如果 fields 不為空,則使用它來顯示摘要 if (fields) { // 將 fields 以逗號分隔成陣列,並過濾掉空值 const fieldList = fields.split(',').map(f => f.trim()).filter(Boolean); const fieldValues: string[] = []; - + // 收集所有欄位值,並處理別名("原始欄位|別名") fieldList.forEach(fieldEntry => { // 解析欄位 (fieldKey)、別名 (labelName)、運算式 (calcExpr) @@ -540,7 +558,7 @@ export class GridView extends ItemView { // birthday {{ Math.floor(...) }} // birthday|年齡 // birthday - + let raw = fieldEntry.trim(); let calcExpr: string | null = null; // 先取出運算區塊 {{ ... }} @@ -560,7 +578,7 @@ export class GridView extends ItemView { fieldKey = raw; labelName = fieldKey; } - + if (metadata?.[fieldKey] !== undefined && metadata?.[fieldKey] !== '' && metadata?.[fieldKey] !== null) { // 如果是數字,則加入千位分隔符號 if (typeof metadata[fieldKey] === 'number') { @@ -586,13 +604,13 @@ export class GridView extends ItemView { fieldValues.push(`${labelName}: ${outputValue}`); } }); - + // 如果有找到任何欄位值,則組合起來 if (fieldValues.length > 0) { summaryValue = fieldValues.join('\n'); // 使用 | 分隔不同欄位 } } - } + } } if (summaryValue) { if (!this.sourceMode.startsWith('custom-')) { @@ -600,7 +618,7 @@ export class GridView extends ItemView { pEl = contentArea.createEl('p', { text: summaryValue.trim() }); } else { // custom mode 有設定顯示欄位值 - pEl = contentArea.createEl('p', { text: summaryValue.trim() , cls: 'ge-content-area-p-field' }); + pEl = contentArea.createEl('p', { text: summaryValue.trim(), cls: 'ge-content-area-p-field' }); } } else { // Frontmatter 沒有設定摘要值,則使用內文 @@ -619,7 +637,7 @@ export class GridView extends ItemView { // 刪除 code block contentWithoutMediaLinks = contentWithoutFrontmatter .replace(/```[\s\S]*?```\n/g, '') - .replace(/```[\s\S]*$/,''); + .replace(/```[\s\S]*$/, ''); } // 刪除註解及連結 @@ -628,29 +646,29 @@ export class GridView extends ItemView { .replace(/!?\[([^\]]*)\]\([^)]+\)|!?\[\[([^\]]+)\]\]/g, (match, p1, p2) => { const linkText = p1 || p2 || ''; if (!linkText) return ''; - + // 獲取副檔名並檢查是否為圖片或影片 const extension = linkText.split('.').pop()?.toLowerCase() || ''; return (IMAGE_EXTENSIONS.has(extension) || VIDEO_EXTENSIONS.has(extension)) ? '' : linkText; - }); + }); //把開頭的標題整行刪除 if (contentWithoutMediaLinks.startsWith('# ') || contentWithoutMediaLinks.startsWith('## ') || contentWithoutMediaLinks.startsWith('### ')) { contentWithoutMediaLinks = contentWithoutMediaLinks.split('\n').slice(1).join('\n'); } - + if (!this.plugin.settings.showCodeBlocksInSummary) { // 不刪除code block的情況下,包含這些特殊符號 - contentWithoutMediaLinks = contentWithoutMediaLinks.replace(/[>|\-#*]/g,'').trim(); + contentWithoutMediaLinks = contentWithoutMediaLinks.replace(/[>|\-#*]/g, '').trim(); } // 只取前 summaryLength 個字符作為預覽 const preview = contentWithoutMediaLinks.slice(0, summaryLength) + (contentWithoutMediaLinks.length > summaryLength ? '...' : ''); - + // 創建預覽內容 pEl = contentArea.createEl('p', { text: preview.trim() }); } - } + } //將預覽文字設定到標題的 title 屬性中 const titleEl = fileEl.querySelector('.ge-title'); @@ -663,7 +681,7 @@ export class GridView extends ItemView { if (colorValue) { // 使用 CSS 類別來設置顏色 fileEl.addClass(`ge-note-color-${colorValue}`); - + // 設置預覽內容文字顏色 if (pEl) { pEl.addClass(`ge-note-color-${colorValue}-text`); @@ -676,7 +694,7 @@ export class GridView extends ItemView { if (titleEl) { titleEl.textContent = titleValue; } - } + } const displayValue = metadata?.display; if (displayValue === 'minimized') { @@ -709,9 +727,9 @@ export class GridView extends ItemView { contentArea.createEl('p', { text: file.extension.toUpperCase() }); } - setTooltip(fileEl as HTMLElement, `${file.name}`,{ delay:2000 }) + setTooltip(fileEl as HTMLElement, `${file.name}`, { delay: 2000 }) } - + // 顯示標籤(僅限 Markdown 檔案) if (file.extension === 'md' && this.showNoteTags && !this.minMode) { const fileCache = this.app.metadataCache.getFileCache(file); @@ -721,10 +739,10 @@ export class GridView extends ItemView { if (displaySetting !== 'minimized') { const allTags = new Set(); - + // 從 frontmatter 獲取標籤 let frontmatterTags = fileCache?.frontmatter?.tags || []; - + // 處理不同的標籤格式 if (typeof frontmatterTags === 'string') { // 如果是字符串,按逗號或空格分割 @@ -745,27 +763,27 @@ export class GridView extends ItemView { } }); } - + // 從檔案 cache 中獲取內文標籤 const cacheTags = fileCache?.tags || []; cacheTags.forEach(tagObj => { const tag = tagObj.tag.startsWith('#') ? tagObj.tag.substring(1) : tagObj.tag; allTags.add(tag); }); - + if (allTags.size > 0) { // 創建標籤容器 const tagsContainer = contentArea.createDiv('ge-tags-container'); - + // 取得所有標籤 const displayTags = Array.from(allTags); - + displayTags.forEach(tag => { - const tagEl = tagsContainer.createEl('span', { + const tagEl = tagsContainer.createEl('span', { cls: 'ge-tag', text: tag.startsWith('#') ? tag : `#${tag}` }); - + //添加右鍵選單事件,點擊後開啟選單,點擊選單中的選項後追加標籤到搜尋關鍵字內並重新渲染 tagEl.addEventListener('contextmenu', (e) => { e.preventDefault(); @@ -778,8 +796,8 @@ export class GridView extends ItemView { .setTitle(t('add_tag_to_search')) .setIcon('circle-plus') .onClick(() => { - this.searchQuery += ` ${tagText}`; - this.render(true); + const searchQuery = this.searchQuery + ` ${tagText}`; + this.setSource('', '', true, searchQuery); return false; }) ); @@ -790,8 +808,8 @@ export class GridView extends ItemView { .setTitle(t('remove_tag_from_search')) .setIcon('circle-minus') .onClick(() => { - this.searchQuery = this.searchQuery.replace(tagText, ''); - this.render(true); + const searchQuery = this.searchQuery.replace(tagText, ''); + this.setSource('', '', true, searchQuery); return false; }) ); @@ -807,21 +825,19 @@ export class GridView extends ItemView { e.preventDefault(); e.stopPropagation(); // 防止事件冒泡到卡片 const tagText = tag.startsWith('#') ? tag : `#${tag}`; - if (this.searchQuery === tagText) { - return; + if (this.searchQuery !== tagText) { + this.setSource('', '', true, tagText); } - this.searchQuery = tagText; - this.render(true); return false; }); }); } } } - + contentArea.setAttribute('data-loaded', 'true'); } - + // 載入圖片預覽 if (!this.minMode) { const imageArea = fileEl.querySelector('.ge-image-area'); @@ -860,7 +876,7 @@ export class GridView extends ItemView { } } } - + // 一旦載入完成,就不需要再觀察這個元素 observer.unobserve(fileEl); } @@ -870,13 +886,13 @@ export class GridView extends ItemView { rootMargin: '50px', // 預先載入視窗外 50px 的內容 threshold: 0.1 }); - + // 顯示檔案 if (files.length > 0) { // 檢查是否應該顯示日期分隔器 const dateDividerMode = this.plugin.settings.dateDividerMode || 'none'; const sortType = this.folderSortType ? this.folderSortType : this.sortType; - const shouldShowDateDividers = dateDividerMode !== 'none' && + const shouldShowDateDividers = dateDividerMode !== 'none' && (sortType.startsWith('mtime-') || sortType.startsWith('ctime-')) && this.sourceMode !== 'random-note' && this.sourceMode !== 'bookmarks' && @@ -959,23 +975,23 @@ export class GridView extends ItemView { container.createDiv('ge-break'); state.blankDividerAdded = true; } - + // 日期分隔器 if (shouldShowDateDividers && !this.pinnedList.includes(file.name)) { let timestamp = 0; - + // 根據排序類型獲取日期時間戳 if (sortType.startsWith('mtime-') || sortType.startsWith('ctime-')) { // 判斷是否以修改日期排序,最近檔案模式使用修改日期排序 const isModifiedTime = sortType.startsWith('mtime-') || this.sourceMode === 'recent-files'; - + // 檢查是否是 Markdown 文件,且有設定對應的 frontmatter 字段 let frontMatterDate = null; if (file.extension === 'md') { const metadata = this.app.metadataCache.getFileCache(file); if (metadata?.frontmatter) { - const fieldSetting = isModifiedTime - ? this.plugin.settings.modifiedDateField + const fieldSetting = isModifiedTime + ? this.plugin.settings.modifiedDateField : this.plugin.settings.createdDateField; const fieldNames = fieldSetting @@ -994,7 +1010,7 @@ export class GridView extends ItemView { } } } - + // 使用 frontmatter 中的日期或檔案的狀態日期 if (frontMatterDate) { timestamp = frontMatterDate.getTime(); @@ -1025,11 +1041,11 @@ export class GridView extends ItemView { // 如果日期不同於上一個檔案的日期,添加分隔器 if (currentDateString !== state.lastDateString) { state.lastDateString = currentDateString; - + // 創建日期分隔器 const dateDivider = container.createDiv('ge-date-divider'); dateDivider.textContent = currentDateString; - + // 針對 iOS 設備進行特殊處理 if (Platform.isIosApp) { dateDivider.style.width = 'calc(100% - 16px)'; @@ -1052,16 +1068,16 @@ export class GridView extends ItemView { if (this.pinnedList.includes(file.name)) { fileEl.addClass('ge-pinned'); } - + // 創建左側內容區,包含圖示和標題 const contentArea = fileEl.createDiv('ge-content-area'); - + // 創建標題容器 const titleContainer = contentArea.createDiv('ge-title-container'); const extension = file.extension.toLowerCase(); // 檢查是否為媒體檔案,如果是則添加 ge-media-card 類別 - if (this.cardLayout === 'vertical' && + if (this.cardLayout === 'vertical' && (isImageFile(file) || isVideoFile(file)) && !this.minMode) { fileEl.addClass('ge-media-card'); @@ -1104,7 +1120,7 @@ export class GridView extends ItemView { if (!this.minMode) { fileEl.createDiv('ge-image-area'); } - + // 開始觀察這個元素 observer.observe(fileEl); @@ -1146,7 +1162,7 @@ export class GridView extends ItemView { // Alt 鍵或設定為預設時:在 grid container 中顯示筆記 this.selectItem(index); this.hasKeyboardFocus = true; - + if (isMediaFile(file)) { // 媒體檔案:正常開啟 if (isAudioFile(file)) { @@ -1183,7 +1199,7 @@ export class GridView extends ItemView { if (redirectType && typeof redirectPath === 'string' && redirectPath.trim() !== '') { let target; - + if (redirectType === 'file') { if (redirectPath.startsWith('[[') && redirectPath.endsWith(']]')) { const noteName = redirectPath.slice(2, -2); @@ -1191,7 +1207,7 @@ export class GridView extends ItemView { } else { target = this.app.vault.getAbstractFileByPath(normalizePath(redirectPath)); } - + if (target instanceof TFile) { this.app.workspace.getLeaf().openFile(target); } else { @@ -1201,20 +1217,23 @@ export class GridView extends ItemView { else if (redirectType === 'folder') { // 判斷redirectPath是否為資料夾 if (this.app.vault.getAbstractFileByPath(normalizePath(redirectPath)) instanceof TFolder) { - this.setSource('folder', redirectPath, true); + this.setSource('folder', redirectPath); this.clearSelection(); } else { new Notice(`${t('target_not_found')}: ${redirectPath}`); } } else if (redirectType === 'mode') { // 判斷redirectPath是否為模式 - this.setSource(redirectPath, '', true); + this.setSource(redirectPath); + this.clearSelection(); + } else if (redirectType === 'search') { + this.setSource('', '', true, redirectPath); this.clearSelection(); } else if (redirectType === 'uri') { // 檢查是否為 http/https 或 obsidian:// 協議 - if (redirectPath.startsWith('http://') || - redirectPath.startsWith('https://') || - redirectPath.startsWith('obsidian://') || + if (redirectPath.startsWith('http://') || + redirectPath.startsWith('https://') || + redirectPath.startsWith('obsidian://') || redirectPath.startsWith('file://')) { // 使用 window.open 打開網址或 obsidian 協議 window.open(redirectPath, '_blank'); @@ -1236,7 +1255,7 @@ export class GridView extends ItemView { // 1. 先嘗試完整比對(不分大小寫) idx = lowerContent.indexOf(searchQuery.toLowerCase()); - + // 2. 若找不到,嘗試拆開關鍵字搜尋 if (idx === -1 && searchQuery.includes(' ')) { const keywords = searchQuery.split(/\s+/).filter(k => k.trim() !== ''); @@ -1286,7 +1305,7 @@ export class GridView extends ItemView { } }); - if(Platform.isDesktop) { + if (Platform.isDesktop) { // 添加拖曳功能 fileEl.setAttribute('draggable', 'true'); fileEl.addEventListener('dragstart', (event) => { @@ -1302,7 +1321,7 @@ export class GridView extends ItemView { // 獲取選中的檔案 const selectedFiles = this.getSelectedFiles(); let drag_filename = ''; - + // 添加拖曳資料 if (selectedFiles.length > 1) { // 如果多個檔案被選中,使用 files-menu @@ -1311,11 +1330,11 @@ export class GridView extends ItemView { return isMedia ? `![[${f.path}]]` : `[[${f.path}]]`; }).join('\n'); event.dataTransfer?.setData('text/plain', fileList); - + // 添加檔案路徑列表 - event.dataTransfer?.setData('application/obsidian-grid-explorer-files', + event.dataTransfer?.setData('application/obsidian-grid-explorer-files', JSON.stringify(selectedFiles.map(f => f.path))); - + drag_filename = `${selectedFiles.length} ${t('files')}`; } else { // 如果只有單個檔案被選中,使用檔案路徑 @@ -1326,21 +1345,21 @@ export class GridView extends ItemView { // 添加拖曳資料 event.dataTransfer?.setData('text/plain', mdLink); - + // 添加檔案路徑列表 - event.dataTransfer?.setData('application/obsidian-grid-explorer-files', + event.dataTransfer?.setData('application/obsidian-grid-explorer-files', JSON.stringify([file.path])); drag_filename = file.basename; } - + const dragImage = document.createElement('div'); dragImage.className = 'ge-custom-drag-preview'; dragImage.textContent = drag_filename; - + // 將元素暫時加入 DOM document.body.appendChild(dragImage); - + // 設定拖曳圖示 event.dataTransfer!.setDragImage(dragImage, 20, 20); @@ -1354,18 +1373,18 @@ export class GridView extends ItemView { // 添加拖曳中的視覺效果 fileEl.addClass('ge-dragging'); }); - + fileEl.addEventListener('dragend', () => { // 移除拖曳中的視覺效果 fileEl.removeClass('ge-dragging'); }); } - + // 添加右鍵選單 fileEl.addEventListener('contextmenu', (event) => { event.preventDefault(); const menu = new Menu(); - + // 獲取項目索引 const index = this.gridItems.indexOf(fileEl); if (index >= 0) { @@ -1377,11 +1396,11 @@ export class GridView extends ItemView { // 獲取選中的檔案 const selectedFiles = this.getSelectedFiles(); - + if (selectedFiles.length > 1) { // 多個檔案被選中,使用 files-menu this.app.workspace.trigger('files-menu', menu, selectedFiles); - + // 檢查是否所有選中的檔案都是 md 檔案 const allMdFiles = selectedFiles.every(file => file.extension === 'md'); if (allMdFiles) { @@ -1449,8 +1468,8 @@ export class GridView extends ItemView { .onClick(() => { this.hideHeaderElements = !this.hideHeaderElements; this.app.workspace.requestSaveLayout(); - this.render(true); - }); + this.render(); + }); }); menu.addItem((item) => { item @@ -1465,7 +1484,7 @@ export class GridView extends ItemView { .setTitle(t('refresh')) .setIcon("refresh-cw") .onClick(() => { - this.render(true); + this.render(); }); }); } @@ -1493,7 +1512,7 @@ export class GridView extends ItemView { if (index >= 0 && index < this.gridItems.length) { this.selectedItemIndex = index; const selectedItem = this.gridItems[index]; - + // 如果是多選模式且項目已被選中,則取消選中 if (multiSelect && this.selectedItems.has(index)) { selectedItem.removeClass('ge-selected-item'); @@ -1510,7 +1529,7 @@ export class GridView extends ItemView { selectedItem.addClass('ge-selected-item'); this.selectedItems.add(index); } - + // 確保選中的項目在視圖中可見 selectedItem.scrollIntoView({ block: 'nearest' }); } @@ -1563,10 +1582,10 @@ export class GridView extends ItemView { // 開啟媒體檔案 openMediaFile(file: TFile, mediaFiles?: TFile[]) { // 如果沒有傳入媒體檔案列表,則獲取 - const getMediaFilesPromise = mediaFiles + const getMediaFilesPromise = mediaFiles ? Promise.resolve(mediaFiles.filter(f => isMediaFile(f))) : getFiles(this, this.randomNoteIncludeMedia).then(allFiles => allFiles.filter(f => isMediaFile(f))); - + getMediaFilesPromise.then(filteredMediaFiles => { // 找到當前檔案在媒體檔案列表中的索引 const currentIndex = filteredMediaFiles.findIndex(f => f.path === file.path); @@ -1586,44 +1605,8 @@ export class GridView extends ItemView { const redirectPath = fileCache?.frontmatter?.redirect; if (redirectType && typeof redirectPath === 'string' && redirectPath.trim() !== '') { - let target; - - if (redirectType === 'file') { - if (redirectPath.startsWith('[[') && redirectPath.endsWith(']]')) { - const noteName = redirectPath.slice(2, -2); - target = this.app.metadataCache.getFirstLinkpathDest(noteName, file.path); - } else { - target = this.app.vault.getAbstractFileByPath(normalizePath(redirectPath)); - } - - if (target instanceof TFile) { - // 遞迴處理目標檔案 - this.showNoteInGrid(target); - return; - } else { - new Notice(`${t('target_not_found')}: ${redirectPath}`); - return; - } - } - else if (redirectType === 'folder') { - // 判斷redirectPath是否為資料夾 - if (this.app.vault.getAbstractFileByPath(normalizePath(redirectPath)) instanceof TFolder) { - this.setSource('folder', redirectPath, true); - this.clearSelection(); - return; - } else { - new Notice(`${t('target_not_found')}: ${redirectPath}`); - return; - } - } else if (redirectType === 'mode') { - // 判斷redirectPath是否為模式 - this.setSource(redirectPath, '', true); - this.clearSelection(); - return; - } else { - new Notice(`${t('target_not_found')}: ${redirectPath}`); - return; - } + this.app.workspace.getLeaf().openFile(file); + return; } // 關閉之前的筆記顯示 @@ -1668,8 +1651,8 @@ export class GridView extends ItemView { const scrollContainer = this.noteViewContainer.createDiv('ge-note-scroll-container'); // 假設在視圖側邊欄則把字型調小 - const isInSidebar = this.leaf.getRoot() === this.app.workspace.leftSplit || - this.leaf.getRoot() === this.app.workspace.rightSplit; + const isInSidebar = this.leaf.getRoot() === this.app.workspace.leftSplit || + this.leaf.getRoot() === this.app.workspace.rightSplit; if (isInSidebar) { scrollContainer.style.fontSize = '1em'; scrollContainer.style.backgroundColor = 'var(--background-secondary)'; @@ -1680,7 +1663,7 @@ export class GridView extends ItemView { if (isInSidebar) { noteContent.style.padding = '15px'; } - + // 創建筆記內容區域 const noteContentArea = noteContent.createDiv('ge-note-content'); @@ -1696,7 +1679,7 @@ export class GridView extends ItemView { file.path, this ); - + // 加上自訂屬性 data-source-path noteContentArea .querySelectorAll('img') @@ -1709,7 +1692,7 @@ export class GridView extends ItemView { if (link) { e.preventDefault(); e.stopPropagation(); - + const href = link.getAttribute('href'); if (href) { const linkText = link.getAttribute('data-href') || href; @@ -1738,9 +1721,9 @@ export class GridView extends ItemView { e.preventDefault(); } }; - + document.addEventListener('keydown', handleKeyDown); - + // 儲存事件監聽器以便後續移除 (this.noteViewContainer as any).keydownHandler = handleKeyDown; } @@ -1760,7 +1743,7 @@ export class GridView extends ItemView { if (keydownHandler) { document.removeEventListener('keydown', keydownHandler); } - + this.noteViewContainer.remove(); this.noteViewContainer = null; } @@ -1794,7 +1777,7 @@ export class GridView extends ItemView { } // 讀取視圖狀態 - async setState(state: any): Promise { + async setState(state: any): Promise { if (state.state) { this.sourceMode = state.state.sourceMode || 'folder'; this.sourcePath = state.state.sourcePath || '/'; diff --git a/src/handleKeyDown.ts b/src/handleKeyDown.ts index 02cbb84..6c03cf0 100644 --- a/src/handleKeyDown.ts +++ b/src/handleKeyDown.ts @@ -36,7 +36,7 @@ export function handleKeyDown(gridView: GridView, event: KeyboardEvent) { if (gridView.sourceMode === 'folder' && gridView.sourcePath && gridView.sourcePath !== '/') { // 獲取上一層資料夾路徑 const parentPath = gridView.sourcePath.split('/').slice(0, -1).join('/') || '/'; - gridView.setSource('folder', parentPath, true); + gridView.setSource('folder', parentPath); gridView.clearSelection(); event.preventDefault(); } @@ -100,7 +100,7 @@ export function handleKeyDown(gridView: GridView, event: KeyboardEvent) { if (gridView.sourceMode === 'folder' && gridView.sourcePath && gridView.sourcePath !== '/') { // 獲取上一層資料夾路徑 const parentPath = gridView.sourcePath.split('/').slice(0, -1).join('/') || '/'; - gridView.setSource('folder', parentPath, true); + gridView.setSource('folder', parentPath); gridView.clearSelection(); event.preventDefault(); } @@ -176,7 +176,7 @@ export function handleKeyDown(gridView: GridView, event: KeyboardEvent) { if (gridView.sourceMode === 'folder' && gridView.sourcePath && gridView.sourcePath !== '/') { // 獲取上一層資料夾路徑 const parentPath = gridView.sourcePath.split('/').slice(0, -1).join('/') || '/'; - gridView.setSource('folder', parentPath, true); + gridView.setSource('folder', parentPath); gridView.clearSelection(); event.preventDefault(); } diff --git a/src/main.ts b/src/main.ts index 9430c02..a67eb37 100644 --- a/src/main.ts +++ b/src/main.ts @@ -51,10 +51,13 @@ export default class GridExplorerPlugin extends Plugin { this.addCommand({ id: 'view-backlinks-in-grid-view', name: t('open_backlinks_in_grid_view'), - callback: () => { + callback: async () => { const activeFile = this.app.workspace.getActiveFile(); if (activeFile) { - this.activateView('backlinks'); + const view = await this.activateView(); + if (view instanceof GridView) { + await view.setSource('backlinks'); + } } else { // 如果沒有當前筆記,則打開根目錄 this.openNoteInFolder(this.app.vault.getRoot()); @@ -66,10 +69,13 @@ export default class GridExplorerPlugin extends Plugin { this.addCommand({ id: 'view-outgoinglinks-in-grid-view', name: t('open_outgoinglinks_in_grid_view'), - callback: () => { + callback: async () => { const activeFile = this.app.workspace.getActiveFile(); if (activeFile) { - this.activateView('outgoinglinks'); + const view = await this.activateView(); + if (view instanceof GridView) { + await view.setSource('outgoinglinks'); + } } else { // 如果沒有當前筆記,則打開根目錄 this.openNoteInFolder(this.app.vault.getRoot()); @@ -117,7 +123,10 @@ export default class GridExplorerPlugin extends Plugin { id: 'open-quick-access-mode', name: t('open_quick_access_mode'), callback: async () => { - this.activateView(this.settings.quickAccessModeType); + const view = await this.activateView(); + if (view instanceof GridView) { + await view.setSource(this.settings.quickAccessModeType); + } } }); @@ -162,22 +171,24 @@ export default class GridExplorerPlugin extends Plugin { item .setTitle(t('open_backlinks_in_grid_view')) .setIcon('links-coming-in') - .onClick(() => { + .onClick(async () => { this.app.workspace.getLeaf().openFile(file); - setTimeout(() => { - this.activateView('backlinks'); - }, 100); + const view = await this.activateView(); + if (view instanceof GridView) { + await view.setSource('backlinks'); + } }); }); ogSubmenu.addItem((item) => { item .setTitle(t('open_outgoinglinks_in_grid_view')) .setIcon('links-going-out') - .onClick(() => { + .onClick(async () => { this.app.workspace.getLeaf().openFile(file); - setTimeout(() => { - this.activateView('outgoinglinks'); - }, 100); + const view = await this.activateView(); + if (view instanceof GridView) { + await view.setSource('outgoinglinks'); + } }); }); if (this.settings.showRecentFilesMode && file instanceof TFile) { @@ -202,12 +213,9 @@ export default class GridExplorerPlugin extends Plugin { .setSection?.("view") .onClick(async () => { // 取得或啟用 GridView - const view = await this.activateView('',''); + const view = await this.activateView(); if (view instanceof GridView) { - // 設定搜尋模式和關鍵字 - view.searchQuery = link; - // 重新渲染視圖 - view.render(true); // resetScroll = true + await view.setSource('', '', true, link); } }); }); @@ -241,12 +249,9 @@ export default class GridExplorerPlugin extends Plugin { .onClick(async () => { const selectedText = editor.getSelection(); // 取得或啟用 GridView - const view = await this.activateView('',''); + const view = await this.activateView(); if (view instanceof GridView) { - // 設定搜尋模式和關鍵字 - view.searchQuery = selectedText; - // 重新渲染視圖 - view.render(true); // resetScroll = true + await view.setSource('', '', true, selectedText); } }); }); @@ -268,12 +273,9 @@ export default class GridExplorerPlugin extends Plugin { .setSection?.("view") .onClick(async () => { // 取得或啟用 GridView - const view = await this.activateView('',''); + const view = await this.activateView(); if (view instanceof GridView) { - // 設定搜尋模式和關鍵字 - view.searchQuery = `#${tagName}`; - // 重新渲染視圖 - view.render(true); // resetScroll = true + await view.setSource('', '', true, `#${tagName}`); } }); }); @@ -337,10 +339,9 @@ export default class GridExplorerPlugin extends Plugin { evt.stopPropagation(); // 叫出 GridView,並把搜尋字串設成這個 tag - const view = await this.activateView('', ''); + const view = await this.activateView(); if (view instanceof GridView) { - view.searchQuery = `#${tagName}`; - await view.render(true); // resetScroll + await view.setSource('', '', true, `#${tagName}`); } }, true); // 用 capture,可在其他 listener 前先吃到 @@ -381,10 +382,9 @@ export default class GridExplorerPlugin extends Plugin { const folder = this.app.vault.getAbstractFileByPath(folderPath); if (folder instanceof TFolder) { - const view = await this.activateView('folder', folderPath); + const view = await this.activateView(); if (view instanceof GridView) { - view.searchQuery = ''; // 清空搜尋字串 - view.render(true); // resetScroll + await view.setSource('folder', folderPath, true, ''); } } }, true); // 使用 capture 階段以確保優先處理 @@ -548,29 +548,32 @@ export default class GridExplorerPlugin extends Plugin { // 打開最近文件 async openNoteInRecentFiles(file: TFile) { - const view = await this.activateView('recent-files') as GridView; - // 如果是文件,等待視圖渲染完成後捲動到該文件位置 - if (file instanceof TFile) { - // 等待下一個事件循環以確保視圖已完全渲染 - setTimeout(() => { - const gridContainer = view.containerEl.querySelector('.ge-grid-container') as HTMLElement; - if (!gridContainer) return; - - // 找到對應的網格項目 - const gridItem = Array.from(gridContainer.querySelectorAll('.ge-grid-item')).find( - item => (item as HTMLElement).dataset.filePath === file.path - ) as HTMLElement; - - if (gridItem) { - // 捲動到該項目的位置 - gridItem.scrollIntoView({ block: 'nearest' }); - // 選中該項目 - const itemIndex = view.gridItems.indexOf(gridItem); - if (itemIndex >= 0) { - view.selectItem(itemIndex); + const view = await this.activateView(); + if (view instanceof GridView) { + await view.setSource('recent-files'); + // 如果是文件,等待視圖渲染完成後捲動到該文件位置 + if (file instanceof TFile) { + // 等待下一個事件循環以確保視圖已完全渲染 + requestAnimationFrame(() => { + const gridContainer = view.containerEl.querySelector('.ge-grid-container') as HTMLElement; + if (!gridContainer) return; + + // 找到對應的網格項目 + const gridItem = Array.from(gridContainer.querySelectorAll('.ge-grid-item')).find( + item => (item as HTMLElement).dataset.filePath === file.path + ) as HTMLElement; + + if (gridItem) { + // 捲動到該項目的位置 + gridItem.scrollIntoView({ block: 'nearest' }); + // 選中該項目 + const itemIndex = view.gridItems.indexOf(gridItem); + if (itemIndex >= 0) { + view.selectItem(itemIndex); + } } - } - }, 100); + }); + } } } @@ -578,35 +581,37 @@ export default class GridExplorerPlugin extends Plugin { async openNoteInFolder(file: TFile | TFolder = this.app.vault.getRoot()) { // 如果是文件,使用其父資料夾路徑 const folderPath = file ? (file instanceof TFile ? file.parent?.path : file.path) : "/"; - const view = await this.activateView('folder', folderPath) as GridView; - - // 如果是文件,等待視圖渲染完成後捲動到該文件位置 - if (file instanceof TFile) { - // 等待下一個事件循環以確保視圖已完全渲染 - setTimeout(() => { - const gridContainer = view.containerEl.querySelector('.ge-grid-container') as HTMLElement; - if (!gridContainer) return; - - // 找到對應的網格項目 - const gridItem = Array.from(gridContainer.querySelectorAll('.ge-grid-item')).find( - item => (item as HTMLElement).dataset.filePath === file.path - ) as HTMLElement; - - if (gridItem) { - // 捲動到該項目的位置 - gridItem.scrollIntoView({ block: 'nearest' }); - // 選中該項目 - const itemIndex = view.gridItems.indexOf(gridItem); - if (itemIndex >= 0) { - view.selectItem(itemIndex); + const view = await this.activateView(); + if (view instanceof GridView) { + await view.setSource('folder', folderPath); + // 如果是文件,等待視圖渲染完成後捲動到該文件位置 + if (file instanceof TFile) { + // 等待下一個事件循環以確保視圖已完全渲染 + requestAnimationFrame(() => { + const gridContainer = view.containerEl.querySelector('.ge-grid-container') as HTMLElement; + if (!gridContainer) return; + + // 找到對應的網格項目 + const gridItem = Array.from(gridContainer.querySelectorAll('.ge-grid-item')).find( + item => (item as HTMLElement).dataset.filePath === file.path + ) as HTMLElement; + + if (gridItem) { + // 捲動到該項目的位置 + gridItem.scrollIntoView({ block: 'nearest' }); + // 選中該項目 + const itemIndex = view.gridItems.indexOf(gridItem); + if (itemIndex >= 0) { + view.selectItem(itemIndex); + } } - } - }, 100); + }); + } } } // 激活視圖 - async activateView(mode = 'folder', path = '') { + async activateView() { const { workspace } = this.app; let leaf = null; @@ -639,11 +644,6 @@ export default class GridExplorerPlugin extends Plugin { await leaf.setViewState({ type: 'grid-view', active: true }); - // 設定資料來源 - if (leaf.view instanceof GridView) { - await leaf.view.setSource(mode, path); - } - // 確保視圖是活躍的 workspace.revealLeaf(leaf); return leaf.view; diff --git a/src/modal/folderMoveModal.ts b/src/modal/folderMoveModal.ts index 5e9f997..8b08035 100644 --- a/src/modal/folderMoveModal.ts +++ b/src/modal/folderMoveModal.ts @@ -42,7 +42,7 @@ export class showFolderMoveModal extends SuggestModal { // 給檔案系統一點時間處理,然後重新整理視圖 setTimeout(() => { if (!this.plugin.settings.showFolder) { - this.gridView.setSource('folder', newPath || '/', true); + this.gridView.setSource('folder', newPath || '/'); } else { this.gridView.render(); } diff --git a/src/modal/folderRenameModal.ts b/src/modal/folderRenameModal.ts index 9f272bf..f474111 100644 --- a/src/modal/folderRenameModal.ts +++ b/src/modal/folderRenameModal.ts @@ -63,7 +63,7 @@ export class FolderRenameModal extends Modal { // 重新渲染視圖 setTimeout(() => { if (!this.plugin.settings.showFolder) { - this.gridView.setSource('folder', newPath || '/', true); + this.gridView.setSource('folder', newPath || '/'); } else { this.gridView.render(); } diff --git a/src/modal/folderSelectionModal.ts b/src/modal/folderSelectionModal.ts index 63dc8d8..fd9a2fb 100644 --- a/src/modal/folderSelectionModal.ts +++ b/src/modal/folderSelectionModal.ts @@ -72,11 +72,14 @@ export class FolderSelectionModal extends Modal { text: `${mode.icon} ${mode.displayName}` }); - customOption.addEventListener('click', () => { + customOption.addEventListener('click', async () => { if (this.activeView) { - this.activeView.setSource(mode.internalName, '', true); + await this.activeView.setSource(mode.internalName); } else { - this.plugin.activateView(mode.internalName); + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + await view.setSource(mode.internalName); + } } this.close(); }); @@ -93,11 +96,14 @@ export class FolderSelectionModal extends Modal { text: `📑 ${t('bookmarks_mode')}` }); - bookmarkOption.addEventListener('click', () => { + bookmarkOption.addEventListener('click', async () => { if (this.activeView) { - this.activeView.setSource('bookmarks', '', true); + await this.activeView.setSource('bookmarks'); } else { - this.plugin.activateView('bookmarks'); + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + await view.setSource('bookmarks'); + } } this.close(); }); @@ -118,11 +124,14 @@ export class FolderSelectionModal extends Modal { text: `🔍 ${t('search_results')}: ${searchInputEl.value}` }); - searchOption.addEventListener('click', () => { + searchOption.addEventListener('click', async () => { if (this.activeView) { - this.activeView.setSource('search', '', true); + await this.activeView.setSource('search'); } else { - this.plugin.activateView('search'); + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + await view.setSource('search'); + } } this.close(); }); @@ -142,11 +151,14 @@ export class FolderSelectionModal extends Modal { text: `🔗 ${t('backlinks_mode')}${activeFileName}` }); - backlinksOption.addEventListener('click', () => { + backlinksOption.addEventListener('click', async () => { if (this.activeView) { - this.activeView.setSource('backlinks', '', true); + await this.activeView.setSource('backlinks'); } else { - this.plugin.activateView('backlinks'); + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + await view.setSource('backlinks'); + } } this.close(); }); @@ -164,11 +176,14 @@ export class FolderSelectionModal extends Modal { text: `🔗 ${t('outgoinglinks_mode')}${activeFileName}` }); - outgoinglinksOption.addEventListener('click', () => { + outgoinglinksOption.addEventListener('click', async () => { if (this.activeView) { - this.activeView.setSource('outgoinglinks', '', true); + await this.activeView.setSource('outgoinglinks'); } else { - this.plugin.activateView('outgoinglinks'); + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + await view.setSource('outgoinglinks'); + } } this.close(); }); @@ -183,11 +198,14 @@ export class FolderSelectionModal extends Modal { text: `📅 ${t('recent_files_mode')}` }); - recentFilesOption.addEventListener('click', () => { + recentFilesOption.addEventListener('click', async () => { if (this.activeView) { - this.activeView.setSource('recent-files', '', true); + await this.activeView.setSource('recent-files'); } else { - this.plugin.activateView('recent-files'); + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + await view.setSource('recent-files'); + } } this.close(); }); @@ -201,11 +219,14 @@ export class FolderSelectionModal extends Modal { text: `📔 ${t('all_files_mode')}` }); - allFilesOption.addEventListener('click', () => { + allFilesOption.addEventListener('click', async () => { if (this.activeView) { - this.activeView.setSource('all-files', '', true); + await this.activeView.setSource('all-files'); } else { - this.plugin.activateView('all-files'); + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + await view.setSource('all-files'); + } } this.close(); }); @@ -219,11 +240,14 @@ export class FolderSelectionModal extends Modal { text: `🎲 ${t('random_note_mode')}` }); - randomNoteOption.addEventListener('click', () => { + randomNoteOption.addEventListener('click', async () => { if (this.activeView) { - this.activeView.setSource('random-note', '', true); + await this.activeView.setSource('random-note'); } else { - this.plugin.activateView('random-note'); + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + await view.setSource('random-note'); + } } this.close(); }); @@ -237,11 +261,14 @@ export class FolderSelectionModal extends Modal { text: `☑️ ${t('tasks_mode')}` }); - tasksOption.addEventListener('click', () => { + tasksOption.addEventListener('click', async () => { if (this.activeView) { - this.activeView.setSource('tasks', '', true); + await this.activeView.setSource('tasks'); } else { - this.plugin.activateView('tasks'); + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + await view.setSource('tasks'); + } } this.close(); }); @@ -255,11 +282,14 @@ export class FolderSelectionModal extends Modal { text: `${customFolderIcon} /` }); - rootFolderOption.addEventListener('click', () => { + rootFolderOption.addEventListener('click', async () => { if (this.activeView) { - this.activeView.setSource('folder', '/', true); + await this.activeView.setSource('folder', '/'); } else { - this.plugin.activateView('folder', '/'); + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + await view.setSource('folder', '/'); + } } this.close(); }); @@ -307,11 +337,14 @@ export class FolderSelectionModal extends Modal { nameSpan.textContent = displayName; folderOption.appendChild(nameSpan); - folderOption.addEventListener('click', () => { + folderOption.addEventListener('click', async () => { if (this.activeView) { - this.activeView.setSource('folder', folder.path, true); + await this.activeView.setSource('folder', folder.path); } else { - this.plugin.activateView('folder', folder.path); + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + await view.setSource('folder', folder.path); + } } this.close(); }); diff --git a/src/modal/inputModal.ts b/src/modal/inputModal.ts new file mode 100644 index 0000000..e18f2a2 --- /dev/null +++ b/src/modal/inputModal.ts @@ -0,0 +1,102 @@ +import { App, Modal } from 'obsidian'; +import { t } from '../translations'; + +interface InputModalOptions { + title: string; + placeholder: string; + defaultValue?: string; + inputType?: 'text' | 'url'; + onSubmit: (value: string) => void; +} + +export class InputModal extends Modal { + options: InputModalOptions; + + constructor(app: App, options: InputModalOptions) { + super(app); + this.options = options; + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + + // 添加標題 + contentEl.createEl('h2', { text: this.options.title }); + + // 創建輸入框容器 + const inputContainer = contentEl.createDiv('ge-input-field-container'); + + // 創建輸入框 + const input = inputContainer.createEl('input', { + type: this.options.inputType || 'text', + value: this.options.defaultValue || '', + placeholder: this.options.placeholder, + cls: 'ge-input-field' + }); + + // 創建按鈕容器 + const buttonContainer = contentEl.createDiv('ge-button-container'); + + // 創建確認按鈕 + const submitButton = buttonContainer.createEl('button', { + text: t('confirm'), + cls: 'mod-cta' + }); + + // 創建取消按鈕 + const cancelButton = buttonContainer.createEl('button', { + text: t('cancel') + }); + + // 執行提交的函數 + const performSubmit = () => { + const value = input.value.trim(); + if (value) { + this.options.onSubmit(value); + this.close(); + } + }; + + // 綁定事件 + submitButton.addEventListener('click', performSubmit); + + input.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + performSubmit(); + } + }); + + cancelButton.addEventListener('click', () => { + this.close(); + }); + + // 自動聚焦到輸入框 + input.focus(); + input.select(); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} + +// 便利函數:顯示搜尋輸入 modal +export function showSearchInputModal(app: App, onSubmit: (searchText: string) => void) { + new InputModal(app, { + title: t('search_text'), + placeholder: t('enter_search_text'), + onSubmit + }).open(); +} + +// 便利函數:顯示 URI 輸入 modal +export function showUriInputModal(app: App, onSubmit: (uri: string) => void) { + new InputModal(app, { + title: t('enter_uri'), + placeholder: t('enter_uri_placeholder'), + inputType: 'url', + onSubmit + }).open(); +} \ No newline at end of file diff --git a/src/modal/searchModal.ts b/src/modal/searchModal.ts index ee47d0e..e6876c0 100644 --- a/src/modal/searchModal.ts +++ b/src/modal/searchModal.ts @@ -339,7 +339,7 @@ export class SearchModal extends Modal { this.gridView.searchMediaFiles = searchMediaFilesCheckbox.checked; this.gridView.clearSelection(); this.gridView.app.workspace.requestSaveLayout(); - this.gridView.render(true); + this.gridView.render(); this.close(); }; diff --git a/src/modal/shortcutSelectionModal.ts b/src/modal/shortcutSelectionModal.ts index 194e7fd..5572486 100644 --- a/src/modal/shortcutSelectionModal.ts +++ b/src/modal/shortcutSelectionModal.ts @@ -1,9 +1,10 @@ import { App, Modal, TFolder, TFile, FuzzySuggestModal } from 'obsidian'; import GridExplorerPlugin from '../main'; import { t } from '../translations'; +import { showSearchInputModal, showUriInputModal } from './inputModal'; interface ShortcutOption { - type: 'mode' | 'folder' | 'file'; + type: 'mode' | 'folder' | 'file' | 'search' | 'uri'; value: string; display: string; } @@ -27,7 +28,7 @@ export class ShortcutSelectionModal extends Modal { // 資料夾選擇按鈕 const folderButton = contentEl.createDiv('shortcut-option-button'); - folderButton.createSpan({ text: `📂 ${t('select_folder')}`}); + folderButton.createSpan({ text: `📂 ${t('select_folder')}` }); // 點擊資料夾按鈕時打開資料夾選擇模態框 folderButton.addEventListener('click', () => { @@ -44,7 +45,7 @@ export class ShortcutSelectionModal extends Modal { // 檔案選擇按鈕 const fileButton = contentEl.createDiv('shortcut-option-button'); - fileButton.createSpan({ text: `📄 ${t('select_file')}`}); + fileButton.createSpan({ text: `📄 ${t('select_file')}` }); // 點擊檔案按鈕時打開檔案選擇模態框 fileButton.addEventListener('click', () => { @@ -59,6 +60,73 @@ export class ShortcutSelectionModal extends Modal { }).open(); }); + // 搜尋文字按鈕 + const searchButton = contentEl.createDiv('shortcut-option-button'); + searchButton.createSpan({ text: `🔎 ${t('search_text')}` }); + + // 點擊搜尋按鈕時打開搜尋輸入模態框 + searchButton.addEventListener('click', () => { + showSearchInputModal(this.app, (searchText) => { + this.onSubmit({ + type: 'search', + value: searchText, + display: `🔎 ${searchText}` + }); + this.close(); + }); + }); + + // URI 按鈕 + const uriButton = contentEl.createDiv('shortcut-option-button'); + uriButton.createSpan({ text: `🌐 ${t('enter_uri')}` }); + + // 點擊 URI 按鈕時打開 URI 輸入模態框 + uriButton.addEventListener('click', () => { + showUriInputModal(this.app, (uri) => { + // 為顯示生成友好的名稱 + let displayName: string; + try { + if (uri.startsWith('obsidian://')) { + // 嘗試提取 vault 參數 + const vaultMatch = uri.match(/[?&]vault=([^&]+)/); + if (vaultMatch) { + const vaultName = decodeURIComponent(vaultMatch[1]); + displayName = `🌐 Obsidian Link (${vaultName})`; + } else { + displayName = '🌐 Obsidian Link'; + } + } else if (uri.startsWith('http://') || uri.startsWith('https://')) { + const url = new URL(uri); + let domain = url.hostname; + if (domain.startsWith('www.')) { + domain = domain.substring(4); + } + displayName = `🌐 ${domain}`; + } else if (uri.startsWith('file://')) { + displayName = '🌐 Local File'; + } else { + const protocolMatch = uri.match(/^([^:]+):/); + if (protocolMatch) { + displayName = `🌐 ${protocolMatch[1].toUpperCase()} Link`; + } else { + displayName = `🌐 ${uri.substring(0, 20)}${uri.length > 20 ? '...' : ''}`; + } + } + } catch (error) { + displayName = `🌐 ${uri.substring(0, 20)}${uri.length > 20 ? '...' : ''}`; + } + + this.onSubmit({ + type: 'uri', + value: uri, + display: displayName + }); + this.close(); + }); + }); + + contentEl.createEl('p'); + // 初始化模式選項,先添加自定義模式 const modeOptions: ShortcutOption[] = []; @@ -74,7 +142,7 @@ export class ShortcutSelectionModal extends Modal { // 添加內建模式 modeOptions.push( { type: 'mode', value: 'bookmarks', display: `📑 ${t('bookmarks_mode')}` }, - { type: 'mode', value: 'search', display: `🔎 ${t('search_results')}` }, + { type: 'mode', value: 'search', display: `🔍 ${t('search_results')}` }, { type: 'mode', value: 'backlinks', display: `🔗 ${t('backlinks_mode')}` }, { type: 'mode', value: 'outgoinglinks', display: `🔗 ${t('outgoinglinks_mode')}` }, { type: 'mode', value: 'all-files', display: `📔 ${t('all_files_mode')}` }, diff --git a/src/renderFolder.ts b/src/renderFolder.ts index 60ad9ba..babfbe2 100644 --- a/src/renderFolder.ts +++ b/src/renderFolder.ts @@ -181,7 +181,7 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { event.stopPropagation(); openFolderInNewView(gridView, folder.path); } else { - gridView.setSource('folder', folder.path, true); + gridView.setSource('folder', folder.path); gridView.clearSelection(); } }); @@ -305,9 +305,9 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { if (folder instanceof TFolder) { await gridView.app.fileManager.trashFile(folder); // 重新渲染視圖 - setTimeout(() => { + requestAnimationFrame(() => { gridView.render(); - }, 100); + }); } }); }); diff --git a/src/renderHeaderButton.ts b/src/renderHeaderButton.ts index b7a67cc..3ca6c2f 100644 --- a/src/renderHeaderButton.ts +++ b/src/renderHeaderButton.ts @@ -39,20 +39,16 @@ export function renderHeaderButton(gridView: GridView) { const lastSource = JSON.parse(gridView.recentSources[0]); gridView.recentSources.shift(); // 從歷史記錄中移除 - // 設定來源(不記錄到歷史) + // 設定來源及搜尋狀態(不記錄到歷史) await gridView.setSource( lastSource.mode, lastSource.path || '', - true, // 重設捲動位置 - false // 不記錄到歷史 + false, // 不記錄到歷史 + lastSource.searchQuery || '', + lastSource.searchAllFiles ?? true, + lastSource.searchFilesNameOnly ?? false, + lastSource.searchMediaFiles ?? false ); - // 還原搜尋相關狀態 - gridView.searchQuery = lastSource.searchQuery || ''; - gridView.searchAllFiles = lastSource.searchAllFiles ?? true; - gridView.searchFilesNameOnly = lastSource.searchFilesNameOnly ?? false; - gridView.searchMediaFiles = lastSource.searchMediaFiles ?? false; - // 重新渲染以應用搜尋狀態 - await gridView.render(); } }); @@ -138,7 +134,7 @@ export function renderHeaderButton(gridView: GridView) { item .setTitle(`${displayText}`) .setIcon(`${icon}`) - .onClick(() => { + .onClick(async () => { // 找出當前點擊的紀錄索引 const clickedIndex = gridView.recentSources.findIndex(source => { const parsed = JSON.parse(source); @@ -150,12 +146,16 @@ export function renderHeaderButton(gridView: GridView) { gridView.recentSources = gridView.recentSources.slice(clickedIndex + 1); } - gridView.setSource(mode, path, true, false); - gridView.searchQuery = sourceInfo.searchQuery || ''; - gridView.searchAllFiles = sourceInfo.searchAllFiles ?? true; - gridView.searchFilesNameOnly = sourceInfo.searchFilesNameOnly ?? false; - gridView.searchMediaFiles = sourceInfo.searchMediaFiles ?? false; - gridView.render(); + // 設定來源及搜尋狀態(不記錄到歷史) + await gridView.setSource( + mode, + path, + false, // 不記錄到歷史 + sourceInfo.searchQuery || '', + sourceInfo.searchAllFiles ?? true, + sourceInfo.searchFilesNameOnly ?? false, + sourceInfo.searchMediaFiles ?? false + ); }); }); } catch (error) { @@ -220,7 +220,10 @@ export function renderHeaderButton(gridView: GridView) { try { // 建立新資料夾 await gridView.app.vault.createFolder(newFolderPath); - gridView.render(false); + // 重新渲染視圖 + requestAnimationFrame(() => { + gridView.render(); + }); } catch (error) { console.error('An error occurred while creating a new folder:', error); } @@ -446,16 +449,115 @@ export function renderHeaderButton(gridView: GridView) { }); } +// 將 URI 轉換為合適的檔名 +function generateFilenameFromUri(uri: string): string { + try { + // 處理 obsidian:// 協議 + if (uri.startsWith('obsidian://')) { + const match = uri.match(/obsidian:\/\/([^?]+)/); + let vaultName = ''; + + // 嘗試提取 vault 參數 + const vaultMatch = uri.match(/[?&]vault=([^&]+)/); + if (vaultMatch) { + vaultName = decodeURIComponent(vaultMatch[1]); + // 清理 vault 名稱,移除不適合檔名的字符 + vaultName = vaultName.replace(/[<>:"/\\|?*]/g, '_'); + } + + if (match) { + const action = match[1]; + const vaultSuffix = vaultName ? ` (${vaultName})` : ''; + + // 根據不同的 obsidian 動作生成檔名 + switch (action) { + case 'open': + return `🌐 Obsidian Open${vaultSuffix}`; + case 'new': + return `🌐 Obsidian New${vaultSuffix}`; + case 'search': + return `🌐 Obsidian Search${vaultSuffix}`; + case 'hook-get-address': + return `🌐 Obsidian Hook${vaultSuffix}`; + default: + return `🌐 Obsidian ${action}${vaultSuffix}`; + } + } + return vaultName ? `🌐 Obsidian Link (${vaultName})` : '🌐 Obsidian Link'; + } + + // 處理 file:// 協議 + if (uri.startsWith('file://')) { + const filename = uri.split('/').pop() || 'Local File'; + return `🌐 ${filename}`; + } + + // 處理 http/https 協議 + if (uri.startsWith('http://') || uri.startsWith('https://')) { + const url = new URL(uri); + let domain = url.hostname; + + // 移除 www. 前綴 + if (domain.startsWith('www.')) { + domain = domain.substring(4); + } + + // 如果有路徑,嘗試提取有意義的部分 + if (url.pathname && url.pathname !== '/') { + const pathParts = url.pathname.split('/').filter(part => part.length > 0); + if (pathParts.length > 0) { + const lastPart = pathParts[pathParts.length - 1]; + // 如果最後一部分看起來像檔名或有意義的標識符 + if (lastPart.length < 50 && !lastPart.includes('?')) { + return `🌐 ${domain} - ${lastPart}`; + } + } + } + + return `🌐 ${domain}`; + } + + // 其他協議的處理 + const protocolMatch = uri.match(/^([^:]+):/); + if (protocolMatch) { + const protocol = protocolMatch[1].toUpperCase(); + return `🌐 ${protocol} Link`; + } + + // 如果不是標準 URI,直接使用前 30 個字符 + const cleanUri = uri.replace(/[<>:"/\\|?*]/g, '_').substring(0, 30); + return `🌐 ${cleanUri}`; + + } catch (error) { + // 如果解析失敗,使用安全的預設名稱 + const cleanUri = uri.replace(/[<>:"/\\|?*]/g, '_').substring(0, 30); + return `🌐 ${cleanUri}`; + } +} + // 創建捷徑檔案 -async function createShortcut(gridView: GridView, option: { type: 'mode' | 'folder' | 'file'; value: string; display: string; }) { +async function createShortcut( + gridView: GridView, + option: { type: 'mode' | 'folder' | 'file' | 'search' | 'uri'; + value: string; + display: string; }) { try { // 生成不重複的檔案名稱 let counter = 0; - let shortcutName = `${option.display}`; + let shortcutName: string; + + // 對於 URI 類型,使用特殊的檔名生成邏輯 + if (option.type === 'uri') { + shortcutName = generateFilenameFromUri(option.value); + } else { + shortcutName = `${option.display}`; + } + let newPath = `${shortcutName}.md`; while (gridView.app.vault.getAbstractFileByPath(newPath)) { counter++; - shortcutName = `${option.display} ${counter}`; + const baseName = option.type === 'uri' ? generateFilenameFromUri(option.value) : option.display; + shortcutName = `${baseName} ${counter}`; newPath = `${shortcutName}.md`; } @@ -477,6 +579,12 @@ async function createShortcut(gridView: GridView, option: { type: 'mode' | 'fold ); frontmatter.type = "file"; frontmatter.redirect = link; + } else if (option.type === 'search') { + frontmatter.type = 'search'; + frontmatter.redirect = option.value; + } else if (option.type === 'uri') { + frontmatter.type = 'uri'; + frontmatter.redirect = option.value; } }); @@ -484,6 +592,6 @@ async function createShortcut(gridView: GridView, option: { type: 'mode' | 'fold } catch (error) { console.error('Create shortcut error', error); - new Notice(t('Failed to create shortcut')); + new Notice(t('failed_to_create_shortcut')); } } \ No newline at end of file diff --git a/src/renderModePath.ts b/src/renderModePath.ts index be939f1..247d771 100644 --- a/src/renderModePath.ts +++ b/src/renderModePath.ts @@ -173,7 +173,7 @@ export function renderModePath(gridView: GridView) { menu.addItem((item) => { item.setTitle(`${customFolderIcon} ${path.name}`) .onClick(() => { - gridView.setSource('folder', path.path, true); + gridView.setSource('folder', path.path); gridView.clearSelection(); }); }); @@ -203,7 +203,7 @@ export function renderModePath(gridView: GridView) { item.setTitle(`${customFolderIcon} ${folder.name}`) .setIcon('corner-down-right') .onClick(() => { - gridView.setSource('folder', folder.path, true); + gridView.setSource('folder', folder.path); gridView.clearSelection(); }); }); @@ -213,7 +213,7 @@ export function renderModePath(gridView: GridView) { menu.showAtMouseEvent(event as MouseEvent); } else { - gridView.setSource('folder', path.path, true); + gridView.setSource('folder', path.path); gridView.clearSelection(); } }); @@ -330,7 +330,7 @@ export function renderModePath(gridView: GridView) { item.setTitle(`${customFolderIcon} ${sf.name}`) .setIcon('corner-down-right') .onClick(() => { - gridView.setSource('folder', sf.path, true); + gridView.setSource('folder', sf.path); gridView.clearSelection(); }); }); @@ -401,10 +401,10 @@ export function renderModePath(gridView: GridView) { .onClick(() => { gridView.plugin.settings.ignoredFolders.push(folder.path); gridView.plugin.saveSettings(); - setTimeout(() => { + requestAnimationFrame(() => { // 回上層資料夾 - gridView.setSource('folder', parentFolder?.path || '/', true); - }, 100); + gridView.setSource('folder', parentFolder?.path || '/'); + }); }); }); } else { @@ -416,10 +416,10 @@ export function renderModePath(gridView: GridView) { .onClick(() => { gridView.plugin.settings.ignoredFolders = gridView.plugin.settings.ignoredFolders.filter((path) => path !== folder.path); gridView.plugin.saveSettings(); - setTimeout(() => { + requestAnimationFrame(() => { // 回上層資料夾 - gridView.setSource('folder', parentFolder?.path || '/', true); - }, 100); + gridView.setSource('folder', parentFolder?.path || '/'); + }); }); }); } @@ -454,10 +454,10 @@ export function renderModePath(gridView: GridView) { .onClick(async () => { if (folder instanceof TFolder) { await gridView.app.fileManager.trashFile(folder); - setTimeout(() => { + requestAnimationFrame(() => { // 回上層資料夾 - gridView.setSource('folder', parentFolder?.path || '/', true); - }, 100); + gridView.setSource('folder', parentFolder?.path || '/'); + }); } }); }); @@ -562,7 +562,7 @@ export function renderModePath(gridView: GridView) { .setChecked(m.internalName === gridView.sourceMode) .onClick(() => { // 切換至選取的自訂模式並重新渲染 - gridView.setSource(m.internalName, '', true); + gridView.setSource(m.internalName); }); }); }); @@ -676,7 +676,7 @@ export function renderModePath(gridView: GridView) { .setChecked(gridView.customOptionIndex === -1) .onClick(() => { gridView.customOptionIndex = -1; - gridView.render(true); + gridView.render(); }); }); mode.options!.forEach((opt, idx) => { @@ -686,7 +686,7 @@ export function renderModePath(gridView: GridView) { .setChecked(idx === gridView.customOptionIndex) .onClick(() => { gridView.customOptionIndex = idx; - gridView.render(true); + gridView.render(); }); }); }); @@ -703,7 +703,7 @@ export function renderModePath(gridView: GridView) { new CustomModeModal(gridView.app, gridView.plugin, gridView.plugin.settings.customModes[modeIndex], (result) => { gridView.plugin.settings.customModes[modeIndex] = result; gridView.plugin.saveSettings(); - gridView.render(true); + gridView.render(); }).open(); }); } @@ -718,11 +718,3 @@ export function renderModePath(gridView: GridView) { }); } } - -// ---------------------------------------------------- -// 搬移建議: -// 1. 先整段複製原始碼貼到此函式底下,並把所有 `gridView.` 前綴改成 `gridView.`。 -// 2. 逐步補上缺少的 import 與型別。 -// 3. GridView.render 內原本的程式替換為呼叫 renderModePath()。 -// 4. 若 render() 需要重新渲染,可在 GridView 內保留 render() 呼叫不變。 -// ---------------------------------------------------- diff --git a/src/translations.ts b/src/translations.ts index ea14d71..0f7dca8 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -257,6 +257,10 @@ export const TRANSLATIONS: Translations = { 'color_purple': '紫色', 'color_pink': '粉色', 'confirm': '確認', + 'search_text': '搜尋文字', + 'enter_search_text': '輸入搜尋文字', + 'enter_uri': '輸入網址', + 'enter_uri_placeholder': '輸入網址或 obsidian:// 協議', 'note_attribute_settings': '筆記屬性設定', 'note_title': '筆記標題', 'note_title_desc': '設定此筆記的顯示標題', @@ -535,6 +539,10 @@ export const TRANSLATIONS: Translations = { 'color_purple': 'Purple', 'color_pink': 'Pink', 'confirm': 'Confirm', + 'search_text': 'Search Text', + 'enter_search_text': 'Enter search text', + 'enter_uri': 'Enter URI', + 'enter_uri_placeholder': 'Enter URL or obsidian:// protocol', 'note_attribute_settings': 'Note attribute settings', 'note_title': 'Note title', 'note_title_desc': 'Set the display title for this note', @@ -811,6 +819,10 @@ export const TRANSLATIONS: Translations = { 'color_purple': '紫色', 'color_pink': '粉色', 'confirm': '确认', + 'search_text': '搜索文字', + 'enter_search_text': '输入搜索文字', + 'enter_uri': '输入网址', + 'enter_uri_placeholder': '输入网址或 obsidian:// 协议', 'note_attribute_settings': '笔记属性设置', 'note_title': '笔记标题', 'note_title_desc': '设置此笔记的显示标题', @@ -1087,6 +1099,10 @@ export const TRANSLATIONS: Translations = { 'color_purple': '紫', 'color_pink': 'ピンク', 'confirm': '確認', + 'search_text': '検索テキスト', + 'enter_search_text': '検索テキストを入力', + 'enter_uri': 'URIを入力', + 'enter_uri_placeholder': 'URLまたはobsidian://プロトコルを入力', 'note_attribute_settings': 'ノート属性設定', 'note_title': 'ノートタイトル', 'note_title_desc': 'このノートの表示タイトルを設定', @@ -1364,6 +1380,10 @@ export const TRANSLATIONS: Translations = { 'color_purple': 'Фиолетовый', 'color_pink': 'Розовый', 'confirm': 'Подтвердить', + 'search_text': 'Поисковый текст', + 'enter_search_text': 'Введите поисковый текст', + 'enter_uri': 'Введите URI', + 'enter_uri_placeholder': 'Введите URL или протокол obsidian://', 'note_attribute_settings': 'Настройки атрибутов заметки', 'note_title': 'Название заметки', 'note_title_desc': 'Установите отображаемое название для этой заметки', @@ -1640,6 +1660,10 @@ export const TRANSLATIONS: Translations = { 'color_purple': 'Фіолетовий', 'color_pink': 'Рожевий', 'confirm': 'Підтвердити', + 'search_text': 'Пошуковий текст', + 'enter_search_text': 'Введіть пошуковий текст', + 'enter_uri': 'Введіть URI', + 'enter_uri_placeholder': 'Введіть URL або протокол obsidian://', 'note_attribute_settings': 'Налаштування атрибутів нотатки', 'note_title': 'Назва нотатки', 'note_title_desc': 'Встановіть назву відображення для цієї нотатки', diff --git a/styles.css b/styles.css index d33ff0e..734bd7f 100644 --- a/styles.css +++ b/styles.css @@ -1929,3 +1929,51 @@ a.ge-current-folder:hover { height: 16px; } +/* --------------------------------------------------------------------------- */ + +/* 通用輸入框樣式 Universal Input Field Styles */ +.ge-input-field { + width: 100% !important; + padding: 8px 12px !important; + border: 1px solid var(--background-modifier-border) !important; + border-radius: var(--input-radius) !important; + background-color: var(--background-primary) !important; + color: var(--text-normal) !important; + font-size: 14px !important; + font-family: var(--font-interface) !important; + transition: border-color 0.2s ease !important; + box-sizing: border-box !important; +} + +.ge-input-field:focus { + outline: none !important; +} + +.ge-input-field::placeholder { + color: var(--text-muted) !important; + opacity: 1 !important; +} + +/* 確保不同 input type 有一致的外觀 */ +.ge-input-field[type="text"], +.ge-input-field[type="url"], +.ge-input-field[type="search"], +.ge-input-field[type="email"] { + -webkit-appearance: none !important; + -moz-appearance: none !important; + appearance: none !important; +} + +/* 輸入框容器樣式 */ +.ge-input-field-container { + position: relative; + width: 100%; + display: flex; + align-items: center; +} + +/* Modal 中的輸入框容器 */ +.modal .ge-input-field-container { + margin-bottom: 16px; + display: block; +} \ No newline at end of file diff --git a/versions.json b/versions.json index 08db244..343048d 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "2.8.1": "1.1.0" + "2.8.2": "1.1.0" } \ No newline at end of file