diff --git a/manifest.json b/manifest.json index 6397e8a..f859a84 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "gridexplorer", "name": "GridExplorer", - "version": "1.9.11", + "version": "1.9.12", "minAppVersion": "1.1.0", "description": "Browse note files in a grid view.", "author": "Devon22", diff --git a/package-lock.json b/package-lock.json index 111dc1d..45e4595 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gridexplorer", - "version": "1.9.11", + "version": "1.9.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gridexplorer", - "version": "1.9.11", + "version": "1.9.12", "license": "MIT", "devDependencies": { "@types/node": "^16.11.6", diff --git a/package.json b/package.json index c3c9432..87290cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gridexplorer", - "version": "1.9.10", + "version": "1.9.12", "description": "Browse note files in a grid view.", "main": "main.js", "scripts": { diff --git a/src/GridView.ts b/src/GridView.ts index c3ede5d..b3bbc10 100644 --- a/src/GridView.ts +++ b/src/GridView.ts @@ -532,43 +532,41 @@ export class GridView extends ItemView { setIcon(sortButton, 'arrow-up-narrow-wide'); } - if (this.sourceMode !== 'random-note') { - // 添加搜尋按鈕 - const searchButtonContainer = headerButtonsDiv.createDiv('ge-search-button-container'); - const searchButton = searchButtonContainer.createEl('button', { - cls: 'search-button', - attr: { 'aria-label': t('search') } - }); - setIcon(searchButton, 'search'); - searchButton.addEventListener('click', () => { - showSearchModal(this.app, this, ''); + // 添加搜尋按鈕 + const searchButtonContainer = headerButtonsDiv.createDiv('ge-search-button-container'); + const searchButton = searchButtonContainer.createEl('button', { + cls: 'search-button', + attr: { 'aria-label': t('search') } + }); + setIcon(searchButton, 'search'); + searchButton.addEventListener('click', () => { + showSearchModal(this.app, this, ''); + }); + + // 如果有搜尋關鍵字,顯示搜尋文字和取消按鈕 + if (this.searchQuery) { + searchButton.style.display = 'none'; + const searchTextContainer = searchButtonContainer.createDiv('ge-search-text-container'); + + // 創建搜尋文字 + const searchText = searchTextContainer.createEl('span', { cls: 'ge-search-text', text: this.searchQuery }); + // 讓搜尋文字可點選 + searchText.style.cursor = 'pointer'; + searchText.addEventListener('click', () => { + showSearchModal(this.app, this, this.searchQuery); }); - // 如果有搜尋關鍵字,顯示搜尋文字和取消按鈕 - if (this.searchQuery) { - searchButton.style.display = 'none'; - const searchTextContainer = searchButtonContainer.createDiv('ge-search-text-container'); - - // 創建搜尋文字 - const searchText = searchTextContainer.createEl('span', { cls: 'ge-search-text', text: this.searchQuery }); - // 讓搜尋文字可點選 - searchText.style.cursor = 'pointer'; - searchText.addEventListener('click', () => { - showSearchModal(this.app, this, this.searchQuery); - }); - - // 創建取消按鈕 - const clearButton = searchTextContainer.createDiv('ge-clear-button'); - setIcon(clearButton, 'x'); - clearButton.addEventListener('click', (e) => { - e.stopPropagation(); // 防止觸發搜尋文字的點擊事件 - this.searchQuery = ''; - this.clearSelection(); - this.render(); - // 通知 Obsidian 保存視圖狀態 - this.app.workspace.requestSaveLayout(); - }); - } + // 創建取消按鈕 + const clearButton = searchTextContainer.createDiv('ge-clear-button'); + setIcon(clearButton, 'x'); + clearButton.addEventListener('click', (e) => { + e.stopPropagation(); // 防止觸發搜尋文字的點擊事件 + this.searchQuery = ''; + this.clearSelection(); + this.render(); + // 通知 Obsidian 保存視圖狀態 + this.app.workspace.requestSaveLayout(); + }); } if ((this.sourceMode === 'all-files' || this.sourceMode === 'recent-files' || this.sourceMode === 'random-note') && @@ -901,7 +899,9 @@ export class GridView extends ItemView { } let files: TFile[] = []; - if (this.searchQuery && this.sourceMode !== 'random-note') { + // 使用 Map 來記錄原始順序 + let fileIndexMap = new Map(); + if (this.searchQuery) { // 顯示搜尋中的提示 const loadingDiv = container.createDiv('ge-loading-indicator'); loadingDiv.setText(t('searching')); @@ -909,63 +909,101 @@ export class GridView extends ItemView { // 取得 vault 中所有支援的檔案 let allFiles: TFile[] = []; if (this.searchAllFiles) { - allFiles = this.app.vault.getFiles(); + // 全部檔案 + allFiles = this.app.vault.getFiles().filter(file => + isDocumentFile(file) || (isMediaFile(file) && this.searchMediaFiles) + ); } else { + // 當前位置檔案 + const randomNoteIncludeMedia = this.randomNoteIncludeMedia; + this.randomNoteIncludeMedia = this.searchMediaFiles; allFiles = await getFiles(this); + this.randomNoteIncludeMedia = randomNoteIncludeMedia; + + if (this.sourceMode === 'recent-files') { + // 搜尋"最近檔案"的當前位置時,先作忽略檔案和只取前n筆 + allFiles = ignoredFiles(allFiles, this).slice(0, this.plugin.settings.recentFilesCount); + } else if (this.sourceMode === 'bookmarks') { + allFiles = allFiles.filter(file => + isDocumentFile(file) || (isMediaFile(file) && this.searchMediaFiles) + ); + // 使用 Map 來記錄原始順序 + allFiles.forEach((file, index) => { + fileIndexMap.set(file, index); + }); + } else if (this.sourceMode === 'search') { + allFiles = allFiles.filter(file => + isDocumentFile(file) || (isMediaFile(file) && this.searchMediaFiles) + ); + } } - // 根據設定過濾檔案 - const filteredFiles = allFiles.filter(file => { - // 文件檔案始終包含 - if (isDocumentFile(file)) { - return true; - } - // 媒體檔案根據 searchMediaFiles 設定決定是否包含 - if (isMediaFile(file)) { - return this.searchMediaFiles; - } - return false; - }); - // 根據搜尋關鍵字進行過濾(不分大小寫) - const lowerCaseSearchQuery = this.searchQuery.toLowerCase(); + const searchTerms = this.searchQuery.toLowerCase().split(/\s+/).filter(term => term.trim() !== ''); // 使用 Promise.all 來非同步地讀取所有檔案內容 await Promise.all( - filteredFiles.map(async file => { + allFiles.map(async file => { const fileName = file.name.toLowerCase(); - if (fileName.includes(lowerCaseSearchQuery)) { + // 檢查檔案名稱是否包含所有搜尋字串 + const matchesFileName = searchTerms.every(term => fileName.includes(term)); + if (matchesFileName) { files.push(file); } else if (file.extension === 'md') { // 只對 Markdown 檔案進行內容搜尋 const content = (await this.app.vault.cachedRead(file)).toLowerCase(); - if (content.includes(lowerCaseSearchQuery)) { + // 檢查檔案內容是否包含所有搜尋字串 + const matchesContent = searchTerms.every(term => content.includes(term)); + if (matchesContent) { files.push(file); } } }) ); - // 根據設定的排序方式排序檔案 - files = sortFiles(files, this); - + // 排序檔案 + if (this.sourceMode === 'recent-files') { + // 臨時的排序類型 + const sortType = this.sortType; + this.sortType = 'mtime-desc'; + files = sortFiles(files, this); + this.sortType = sortType; + } else if (this.sourceMode === 'bookmarks') { + // 保持原始順序 + files.sort((a, b) => { + const indexA = fileIndexMap.get(a) ?? Number.MAX_SAFE_INTEGER; + const indexB = fileIndexMap.get(b) ?? Number.MAX_SAFE_INTEGER; + return indexA - indexB; + }); + } else if (this.sourceMode === 'random-note') { + const sortType = this.sortType; + this.sortType = 'random'; + files = sortFiles(files, this); + this.sortType = sortType; + } else { + files = sortFiles(files, this); + } + + // 忽略檔案 + files = ignoredFiles(files, this); + // 移除搜尋中的提示 loadingDiv.remove(); } else { - // 獲取檔案列表並根據搜尋關鍵字過濾 + // 無搜尋關鍵字的情況 files = await getFiles(this); - } - //忽略檔案 - files = ignoredFiles(files, this) + // 忽略檔案 + files = ignoredFiles(files, this) - //最近檔案模式,只取前30筆 - if (this.sourceMode === 'recent-files') { - files = files.slice(0, this.plugin.settings.recentFilesCount); - } + // 最近檔案模式,只取前n筆 + if (this.sourceMode === 'recent-files') { + files = files.slice(0, this.plugin.settings.recentFilesCount); + } - // 隨機筆記模式,只取前10筆 - if (this.sourceMode === 'random-note') { - files = files.slice(0, this.plugin.settings.randomNoteCount); + // 隨機筆記模式,只取前n筆 + if (this.sourceMode === 'random-note') { + files = files.slice(0, this.plugin.settings.randomNoteCount); + } } // 如果沒有檔案,顯示提示訊息 @@ -1048,6 +1086,78 @@ export class GridView extends ItemView { // 其他檔案顯示副檔名 contentArea.createEl('p', { text: file.extension.toUpperCase() }); } + + // 顯示標籤(僅限 Markdown 檔案) + if (file.extension === 'md' && this.plugin.settings.showNoteTags) { + const fileCache = this.app.metadataCache.getFileCache(file); + const allTags = new Set(); + + // 從 frontmatter 獲取標籤 + let frontmatterTags = fileCache?.frontmatter?.tags || []; + + // 處理不同的標籤格式 + if (typeof frontmatterTags === 'string') { + // 如果是字符串,按逗號或空格分割 + frontmatterTags.split(/[,\s]+/).filter(tag => tag.trim() !== '') + .forEach(tag => allTags.add(tag)); + } else if (Array.isArray(frontmatterTags)) { + frontmatterTags.forEach(tag => { + // 處理陣列中的每個標籤,可能是字符串或包含空格的字符串 + if (typeof tag === 'string') { + // 檢查標籤是否包含空格(可能是未被正確分割的多個標籤) + if (tag.includes(' ')) { + // 按空格分割並添加每個子標籤 + tag.split(/\s+/).filter(subTag => subTag.trim() !== '') + .forEach(subTag => allTags.add(subTag)); + } else { + allTags.add(tag); + } + } + }); + } + + // 從檔案 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 containerWidth = tagsContainer.getBoundingClientRect().width; + const tagWidth = 70; + const maxTags = Math.floor(containerWidth / tagWidth); + + // 取得要顯示的標籤 + const displayTags = Array.from(allTags).slice(0, maxTags); + + displayTags.forEach(tag => { + const tagEl = tagsContainer.createEl('span', { + cls: 'ge-tag', + text: tag.startsWith('#') ? tag : `#${tag}` + }); + + // 添加點擊事件,點擊後設置搜尋關鍵字並重新渲染 + tagEl.addEventListener('click', (e) => { + e.stopPropagation(); // 防止事件冒泡到卡片 + const tagText = tag.startsWith('#') ? tag.substring(1) : tag; + if (this.searchQuery === tagText) { + return; + } + this.searchQuery = tagText; + this.searchAllFiles = true; + this.searchMediaFiles = false; + this.render(true); + return false; + }); + }); + } + } + contentArea.setAttribute('data-loaded', 'true'); } @@ -1099,66 +1209,167 @@ export class GridView extends ItemView { }); // 顯示檔案 - for (const file of files) { - const fileEl = container.createDiv('ge-grid-item'); - this.gridItems.push(fileEl); // 添加到網格項目數組 - fileEl.dataset.filePath = file.path; - - // 創建左側內容區,包含圖示和標題 - const contentArea = fileEl.createDiv('ge-content-area'); - - // 創建標題容器 - const titleContainer = contentArea.createDiv('ge-title-container'); - const extension = file.extension.toLowerCase(); + if (files.length > 0) { + // 檢查是否應該顯示日期分隔器 + const dateDividerMode = this.plugin.settings.dateDividerMode || 'none'; + const sortType = this.folderSortType ? this.folderSortType : this.sortType; + const shouldShowDateDividers = dateDividerMode !== 'none' && + (sortType.startsWith('mtime-') || sortType.startsWith('ctime-')) && + this.sourceMode !== 'random-note' && + this.sourceMode !== 'bookmarks'; - // 添加檔案類型圖示 - if (isImageFile(file)) { - const iconContainer = titleContainer.createDiv('ge-icon-container ge-img'); - setIcon(iconContainer, 'image'); - } else if (isVideoFile(file)) { - const iconContainer = titleContainer.createDiv('ge-icon-container ge-video'); - setIcon(iconContainer, 'play-circle'); - } else if (isAudioFile(file)) { - const iconContainer = titleContainer.createDiv('ge-icon-container ge-audio'); - setIcon(iconContainer, 'music'); - } else if (extension === 'pdf') { - const iconContainer = titleContainer.createDiv('ge-icon-container ge-pdf'); - setIcon(iconContainer, 'paperclip'); - } else if (extension === 'canvas') { - const iconContainer = titleContainer.createDiv('ge-icon-container ge-canvas'); - setIcon(iconContainer, 'layout-dashboard'); - } else if (extension === 'md' || extension === 'txt') { - const iconContainer = titleContainer.createDiv('ge-icon-container'); - setIcon(iconContainer, 'file-text'); - } else { - const iconContainer = titleContainer.createDiv('ge-icon-container'); - setIcon(iconContainer, 'file'); - } + let lastDateString = ''; - // 創建標題(立即載入) - const titleEl = titleContainer.createEl('span', { cls: 'ge-title', text: file.basename }); - titleEl.setAttribute('title', file.basename); - - // 創建圖片區域,但先不載入圖片 - fileEl.createDiv('ge-image-area'); - - // 開始觀察這個元素 - observer.observe(fileEl); - - // 點擊時開啟檔案 - fileEl.addEventListener('click', (event) => { - // 獲取項目索引 - const index = this.gridItems.indexOf(fileEl); - if (index < 0) return; - - // 處理多選邏輯 - if (event.ctrlKey || event.metaKey) { - if (this.selectedItemIndex !== -1) { - // 如果已有選中狀態,則繼續選中 - this.selectItem(index, true); - this.hasKeyboardFocus = true; + for (const file of files) { + // 如果啟用日期分隔器,檢查是否需要添加新的日期分隔器 + if (shouldShowDateDividers) { + let timestamp = 0; + + // 根據排序類型獲取日期時間戳 + if (sortType.startsWith('mtime-') || sortType.startsWith('ctime-')) { + const isModifiedTime = sortType.startsWith('mtime-'); + + // 檢查是否是 Markdown 文件,且有設定對應的 frontmatter 字段 + let frontMatterDate = null; + if (file.extension === 'md') { + const metadata = this.app.metadataCache.getFileCache(file); + if (metadata?.frontmatter) { + const fieldName = isModifiedTime + ? this.plugin.settings.modifiedDateField + : this.plugin.settings.createdDateField; + + if (fieldName && metadata.frontmatter[fieldName]) { + const dateStr = metadata.frontmatter[fieldName]; + const date = new Date(dateStr); + if (!isNaN(date.getTime())) { + frontMatterDate = date; + } + } + } + } + + // 使用 frontmatter 中的日期或檔案的狀態日期 + if (frontMatterDate) { + timestamp = frontMatterDate.getTime(); + } else { + timestamp = isModifiedTime ? file.stat.mtime : file.stat.ctime; + } + } + + // 創建日期物件並格式化 + const fileDate = new Date(timestamp); + + // 根據日期分隔器模式設定格式化 + let currentDateString = ''; + + if (dateDividerMode === 'year') { + // 年分隔器:只顯示年份 + currentDateString = fileDate.getFullYear().toString(); + } else if (dateDividerMode === 'month') { + // 月分隔器:顯示年-月 + const year = fileDate.getFullYear(); + const month = fileDate.getMonth() + 1; // getMonth() 回傳 0-11 + currentDateString = `${year}-${month.toString().padStart(2, '0')}`; } else { - // 沒有選中狀態則開啟新分頁 + // 日分隔器:顯示完整日期(預設行為) + currentDateString = fileDate.toLocaleDateString(); + } + + // 如果日期不同於上一個檔案的日期,添加分隔器 + if (currentDateString !== lastDateString) { + lastDateString = currentDateString; + + // 創建日期分隔器 + const dateDivider = container.createDiv('ge-date-divider'); + dateDivider.textContent = currentDateString; + } + } + + const fileEl = container.createDiv('ge-grid-item'); + this.gridItems.push(fileEl); // 添加到網格項目數組 + fileEl.dataset.filePath = file.path; + + // 創建左側內容區,包含圖示和標題 + const contentArea = fileEl.createDiv('ge-content-area'); + + // 創建標題容器 + const titleContainer = contentArea.createDiv('ge-title-container'); + const extension = file.extension.toLowerCase(); + + // 添加檔案類型圖示 + if (isImageFile(file)) { + const iconContainer = titleContainer.createDiv('ge-icon-container ge-img'); + setIcon(iconContainer, 'image'); + } else if (isVideoFile(file)) { + const iconContainer = titleContainer.createDiv('ge-icon-container ge-video'); + setIcon(iconContainer, 'play-circle'); + } else if (isAudioFile(file)) { + const iconContainer = titleContainer.createDiv('ge-icon-container ge-audio'); + setIcon(iconContainer, 'music'); + } else if (extension === 'pdf') { + const iconContainer = titleContainer.createDiv('ge-icon-container ge-pdf'); + setIcon(iconContainer, 'paperclip'); + } else if (extension === 'canvas') { + const iconContainer = titleContainer.createDiv('ge-icon-container ge-canvas'); + setIcon(iconContainer, 'layout-dashboard'); + } else if (extension === 'md' || extension === 'txt') { + const iconContainer = titleContainer.createDiv('ge-icon-container'); + setIcon(iconContainer, 'file-text'); + } else { + const iconContainer = titleContainer.createDiv('ge-icon-container'); + setIcon(iconContainer, 'file'); + } + + // 創建標題(立即載入) + const titleEl = titleContainer.createEl('span', { cls: 'ge-title', text: file.basename }); + titleEl.setAttribute('title', file.basename); + + // 創建圖片區域,但先不載入圖片 + fileEl.createDiv('ge-image-area'); + + // 開始觀察這個元素 + observer.observe(fileEl); + + // 點擊時開啟檔案 + fileEl.addEventListener('click', (event) => { + // 獲取項目索引 + const index = this.gridItems.indexOf(fileEl); + if (index < 0) return; + + // 處理多選邏輯 + if (event.ctrlKey || event.metaKey) { + 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().openFile(file); + } + } + event.preventDefault(); + return; + } else if (event.shiftKey) { + // Shift 鍵:範圍選擇 + this.handleRangeSelection(index); + this.hasKeyboardFocus = true; + event.preventDefault(); + return; + } else { + // 一般點擊:選中單個項目並開啟 + this.selectItem(index); + this.hasKeyboardFocus = true; + + // 根據檔案類型處理點擊事件 if (isMediaFile(file)) { // 開啟媒體檔案 if (isAudioFile(file)) { @@ -1168,57 +1379,87 @@ export class GridView extends ItemView { } } else { // 開啟文件檔案 + this.app.workspace.getLeaf().openFile(file); + } + } + }); + + // 處理滑鼠中鍵點擊 + fileEl.addEventListener('mousedown', (event) => { + if (event.button === 1) { + event.preventDefault(); + } + }); + + fileEl.addEventListener('mouseup', (event) => { + if (event.button === 1) { + event.preventDefault(); + if (!isMediaFile(file)) { this.app.workspace.getLeaf(true).openFile(file); } } - event.preventDefault(); - return; - } else if (event.shiftKey) { - // Shift 鍵:範圍選擇 - this.handleRangeSelection(index); - this.hasKeyboardFocus = true; - event.preventDefault(); - return; - } else { - // 一般點擊:選中單個項目並開啟 - this.selectItem(index); - this.hasKeyboardFocus = true; + }); - // 根據檔案類型處理點擊事件 - if (isMediaFile(file)) { - // 開啟媒體檔案 - if (isAudioFile(file)) { - FloatingAudioPlayer.open(this.app, file); - } else { - this.openMediaFile(file, files); + if(Platform.isDesktop) { + // 添加拖曳功能 + fileEl.setAttribute('draggable', 'true'); + fileEl.addEventListener('dragstart', (event) => { + // 獲取項目索引 + const index = this.gridItems.indexOf(fileEl); + if (index >= 0) { + // 如果項目未被選中,則選中它 + if (!this.selectedItems.has(index)) { + this.selectItem(index); + } } - } else { - // 開啟文件檔案 - this.app.workspace.getLeaf().openFile(file); - } - } - }); - - // 處理滑鼠中鍵點擊 - fileEl.addEventListener('mousedown', (event) => { - if (event.button === 1) { - event.preventDefault(); - } - }); - fileEl.addEventListener('mouseup', (event) => { - if (event.button === 1) { - event.preventDefault(); - if (!isMediaFile(file)) { - this.app.workspace.getLeaf(true).openFile(file); - } - } - }); + // 獲取選中的檔案 + const selectedFiles = this.getSelectedFiles(); + + // 添加拖曳資料 + if (selectedFiles.length > 1) { + // 如果多個檔案被選中,使用 files-menu + const fileList = selectedFiles.map(f => { + const isMedia = isMediaFile(f); + return isMedia ? `![[${f.path}]]` : `[[${f.path}]]`; + }).join('\n'); + event.dataTransfer?.setData('text/plain', fileList); + + // 添加檔案路徑列表 + event.dataTransfer?.setData('application/obsidian-grid-explorer-files', + JSON.stringify(selectedFiles.map(f => f.path))); + } else { + // 如果只有單個檔案被選中,使用檔案路徑 + const isMedia = isMediaFile(file); + const mdLink = isMedia + ? `![[${file.path}]]` // 媒體檔案使用 ![[]] 格式 + : `[[${file.path}]]`; // 一般檔案使用 [[]] 格式 - if(Platform.isDesktop) { - // 添加拖曳功能 - fileEl.setAttribute('draggable', 'true'); - fileEl.addEventListener('dragstart', (event) => { + // 添加拖曳資料 + event.dataTransfer?.setData('text/plain', mdLink); + + // 添加檔案路徑列表 + event.dataTransfer?.setData('application/obsidian-grid-explorer-files', + JSON.stringify([file.path])); + } + + // 設定拖曳效果 + event.dataTransfer!.effectAllowed = 'all'; + // 添加拖曳中的視覺效果 + 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) { @@ -1231,122 +1472,67 @@ export class GridView extends ItemView { // 獲取選中的檔案 const selectedFiles = this.getSelectedFiles(); - // 添加拖曳資料 if (selectedFiles.length > 1) { - // 如果多個檔案被選中,使用 files-menu - const fileList = selectedFiles.map(f => { - const isMedia = isMediaFile(f); - return isMedia ? `![[${f.path}]]` : `[[${f.path}]]`; - }).join('\n'); - event.dataTransfer?.setData('text/plain', fileList); + // 多個檔案被選中,使用 files-menu + this.app.workspace.trigger('files-menu', menu, selectedFiles); - // 添加檔案路徑列表 - event.dataTransfer?.setData('application/obsidian-grid-explorer-files', - JSON.stringify(selectedFiles.map(f => f.path))); + // 檢查是否所有選中的檔案都是 md 檔案 + const allMdFiles = selectedFiles.every(file => file.extension === 'md'); + if (allMdFiles) { + menu.addItem((item) => { + item + .setTitle(t('set_note_color')) + .setIcon('palette') + .onClick(() => { + showNoteColorSettingsModal(this.app, this.plugin, selectedFiles); + }); + }); + } } else { - // 如果只有單個檔案被選中,使用檔案路徑 - const isMedia = isMediaFile(file); - const mdLink = isMedia - ? `![[${file.path}]]` // 媒體檔案使用 ![[]] 格式 - : `[[${file.path}]]`; // 一般檔案使用 [[]] 格式 - - // 添加拖曳資料 - event.dataTransfer?.setData('text/plain', mdLink); - - // 添加檔案路徑列表 - event.dataTransfer?.setData('application/obsidian-grid-explorer-files', - JSON.stringify([file.path])); + this.app.workspace.trigger('file-menu', menu, file); } - - // 設定拖曳效果 - event.dataTransfer!.effectAllowed = 'all'; - // 添加拖曳中的視覺效果 - fileEl.addClass('ge-dragging'); - }); - - fileEl.addEventListener('dragend', () => { - // 移除拖曳中的視覺效果 - fileEl.removeClass('ge-dragging'); + // 新增在新分頁開啟選項 + menu.addItem((item) => { + item + .setTitle(t('open_in_new_tab')) + .setIcon('external-link') + .setSection?.("open") + .onClick(() => { + if (selectedFiles.length > 1) { + // 如果多個檔案被選中,開啟所有文件檔案 + const documentFiles = selectedFiles.filter(f => isDocumentFile(f)); + for (const docFile of documentFiles) { + this.app.workspace.getLeaf(true).openFile(docFile); + } + } else { + this.app.workspace.getLeaf(true).openFile(file); + } + }); + }); + + // 刪除選項 + menu.addItem((item) => { + (item as any).setWarning(true); + item + .setTitle(t('delete_note')) + .setIcon('trash') + .onClick(async () => { + if (selectedFiles.length > 1) { + // 刪除多個檔案 + for (const f of selectedFiles) { + await this.app.fileManager.trashFile(f); + } + // 清除選中狀態 + this.clearSelection(); + } else { + // 刪除單個檔案 + await this.app.fileManager.trashFile(file); + } + }); + }); + menu.showAtMouseEvent(event); }); } - - // 添加右鍵選單 - fileEl.addEventListener('contextmenu', (event) => { - event.preventDefault(); - const menu = new Menu(); - - // 獲取項目索引 - const index = this.gridItems.indexOf(fileEl); - if (index >= 0) { - // 如果項目未被選中,則選中它 - if (!this.selectedItems.has(index)) { - this.selectItem(index); - } - } - - // 獲取選中的檔案 - 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) { - menu.addItem((item) => { - item - .setTitle(t('set_note_color')) - .setIcon('palette') - .onClick(() => { - showNoteColorSettingsModal(this.app, this.plugin, selectedFiles); - }); - }); - } - } else { - this.app.workspace.trigger('file-menu', menu, file); - } - // 新增在新分頁開啟選項 - menu.addItem((item) => { - item - .setTitle(t('open_in_new_tab')) - .setIcon('external-link') - .setSection?.("open") - .onClick(() => { - if (selectedFiles.length > 1) { - // 如果多個檔案被選中,開啟所有文件檔案 - const documentFiles = selectedFiles.filter(f => isDocumentFile(f)); - for (const docFile of documentFiles) { - this.app.workspace.getLeaf(true).openFile(docFile); - } - } else { - this.app.workspace.getLeaf(true).openFile(file); - } - }); - }); - - // 刪除選項 - menu.addItem((item) => { - (item as any).setWarning(true); - item - .setTitle(t('delete_note')) - .setIcon('trash') - .onClick(async () => { - if (selectedFiles.length > 1) { - // 刪除多個檔案 - for (const f of selectedFiles) { - await this.app.fileManager.trashFile(f); - } - // 清除選中狀態 - this.clearSelection(); - } else { - // 刪除單個檔案 - await this.app.fileManager.trashFile(file); - } - }); - }); - menu.showAtMouseEvent(event); - }); } if(Platform.isDesktop) { diff --git a/src/SearchModal.ts b/src/SearchModal.ts index 04985f3..8feaa56 100644 --- a/src/SearchModal.ts +++ b/src/SearchModal.ts @@ -62,6 +62,11 @@ export class SearchModal extends Modal { text: t('search_current_location_only'), cls: 'ge-search-scope-label' }); + // 隨機筆記模式下,搜尋範圍設定不顯示 + if (this.gridView.sourceMode === 'random-note') { + searchScopeContainer.style.display = 'none'; + searchScopeCheckbox.checked = false; + } // 創建搜尋媒體檔案設定 const searchMediaFilesContainer = searchOptionsContainer.createDiv('ge-search-media-files-container'); @@ -74,6 +79,12 @@ export class SearchModal extends Modal { text: t('search_media_files'), cls: 'ge-search-media-files-label' }); + // 如果設定中的顯示媒體檔案為false,或在反向連結模式下,則隱藏搜尋媒體檔案設定 + if (!this.gridView.plugin.settings.showMediaFiles || this.gridView.sourceMode === 'backlinks') { + searchMediaFilesContainer.style.display = 'none'; + searchMediaFilesCheckbox.checked = false; + this.gridView.searchMediaFiles = false; + } // 點擊容器時切換勾選框狀態 searchScopeContainer.addEventListener('click', (e) => { diff --git a/src/fileUtils.ts b/src/fileUtils.ts index f92941f..9089a20 100644 --- a/src/fileUtils.ts +++ b/src/fileUtils.ts @@ -184,7 +184,6 @@ export async function getFiles(gridView: GridView): Promise { 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) { @@ -200,10 +199,8 @@ export async function getFiles(gridView: GridView): Promise { (settings.showMediaFiles && isMediaFile(file))) { return true; } - return false; }); - return sortFiles(files, gridView); } return []; @@ -222,11 +219,7 @@ export async function getFiles(gridView: GridView): Promise { } } return []; - } else if (sourceMode === 'backlinks') { - if(searchQuery !== '') { - return []; - } - + } else if (sourceMode === 'backlinks') { // 反向連結模式:找出所有引用當前筆記的檔案 const activeFile = app.workspace.getActiveFile(); if (!activeFile) { diff --git a/src/settings.ts b/src/settings.ts index 1152a99..84e817f 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -30,6 +30,8 @@ export interface GallerySettings { createdDateField: string; // 建立時間的欄位名稱 recentFilesCount: number; // 最近筆記模式顯示的筆數 randomNoteCount: number; // 隨機筆記模式顯示的筆數 + showNoteTags: boolean; // 是否顯示筆記標籤 + dateDividerMode: string; // 日期分隔器模式:none, year, month, day } // 預設設定 @@ -60,7 +62,9 @@ export const DEFAULT_SETTINGS: GallerySettings = { customDocumentExtensions: '', // 自訂文件副檔名(用逗號分隔) recentSources: [], // 預設最近的瀏覽記錄 modifiedDateField: '', - createdDateField: '' + createdDateField: '', + showNoteTags: false, // 預設不顯示筆記標籤 + dateDividerMode: 'none', // 預設不使用日期分隔器 }; // 設定頁面類別 @@ -292,6 +296,23 @@ export class GridExplorerSettingTab extends PluginSettingTab { await this.plugin.saveSettings(false); })); + // 日期分隔器模式設定 + new Setting(containerEl) + .setName(t('date_divider_mode')) + .setDesc(t('date_divider_mode_desc')) + .addDropdown(dropdown => { + dropdown + .addOption('none', t('date_divider_mode_none')) + .addOption('year', t('date_divider_mode_year')) + .addOption('month', t('date_divider_mode_month')) + .addOption('day', t('date_divider_mode_day')) + .setValue(this.plugin.settings.dateDividerMode) + .onChange(async (value) => { + this.plugin.settings.dateDividerMode = value; + await this.plugin.saveSettings(); + }); + }); + // 檔案監控功能設定 new Setting(containerEl) .setName(t('enable_file_watcher')) @@ -319,7 +340,7 @@ export class GridExplorerSettingTab extends PluginSettingTab { }); }); - // 顯示"返回上级文件夹"選項設定 + // 顯示"回上層資料夾"選項設定 new Setting(containerEl) .setName(t('show_parent_folder_item')) .setDesc(t('show_parent_folder_item_desc')) @@ -332,6 +353,19 @@ export class GridExplorerSettingTab extends PluginSettingTab { }); }); + // 顯示筆記標籤設定 + new Setting(containerEl) + .setName(t('show_note_tags')) + .setDesc(t('show_note_tags_desc')) + .addToggle(toggle => { + toggle + .setValue(this.plugin.settings.showNoteTags) + .onChange(async (value) => { + this.plugin.settings.showNoteTags = value; + await this.plugin.saveSettings(); + }); + }); + // 網格項目寬度設定 new Setting(containerEl) .setName(t('grid_item_width')) diff --git a/src/translations.ts b/src/translations.ts index dfe4ee4..c0e3ba2 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -63,10 +63,12 @@ export const TRANSLATIONS: Translations = { // 設定 'grid_view_settings': '網格視圖設定', 'media_files_settings': '媒體檔案設定', - 'show_media_files': '顯示圖片和影片', - 'show_media_files_desc': '在網格視圖中顯示圖片和影片檔案', + 'show_media_files': '顯示媒體檔案', + 'show_media_files_desc': '在網格視圖中顯示媒體檔案', 'show_video_thumbnails': '顯示影片縮圖', 'show_video_thumbnails_desc': '在網格視圖中顯示影片的縮圖,關閉時將顯示播放圖示', + 'show_note_tags': '顯示筆記標籤', + 'show_note_tags_desc': '在網格視圖中顯示筆記的標籤', 'ignored_folders': '忽略的資料夾', 'ignored_folders_desc': '在這裡設定要忽略的資料夾', 'add_ignored_folder': '新增忽略資料夾', @@ -111,7 +113,7 @@ export const TRANSLATIONS: Translations = { 'show_random_note_mode': '顯示隨機筆記模式', 'random_note_count': '隨機筆記模式顯示筆數', 'random_note_notes_only': '僅筆記', - 'random_note_include_media_files': '包含圖片影片', + 'random_note_include_media_files': '包含媒體檔案', // 顯示"返回上層資料夾"選項設定 'show_parent_folder_item': '顯示「返回上層資料夾」', @@ -174,6 +176,15 @@ export const TRANSLATIONS: Translations = { 'rename_folder': '重新命名資料夾', 'enter_new_folder_name': '輸入新資料夾名稱', 'search_selection_in_grid_view': '在網格視圖中搜尋...', + 'show_date_dividers': '顯示日期分隔器', + 'show_date_dividers_desc': '在日期相關排序時,在不同天的第一筆之前顯示日期分隔器', + 'date_divider_format': '日期分隔器格式', + 'date_divider_mode': '日期分隔器', + 'date_divider_mode_desc': '選擇日期分隔器的顯示模式', + 'date_divider_mode_none': '不使用', + 'date_divider_mode_year': '年', + 'date_divider_mode_month': '月', + 'date_divider_mode_day': '日', }, 'en': { // Notifications @@ -219,10 +230,12 @@ export const TRANSLATIONS: Translations = { // Settings 'grid_view_settings': 'Grid view settings', 'media_files_settings': 'Media files settings', - 'show_media_files': 'Show images and videos', - 'show_media_files_desc': 'Display image and video files in the grid view', + 'show_media_files': 'Show media files', + 'show_media_files_desc': 'Display media files in the grid view', 'show_video_thumbnails': 'Show video thumbnails', 'show_video_thumbnails_desc': 'Display thumbnails for videos in the grid view, shows a play icon when disabled', + 'show_note_tags': 'Show note tags', + 'show_note_tags_desc': 'Display tags for notes in the grid view', 'ignored_folders': 'Ignored folders', 'ignored_folders_desc': 'Set folders to ignore here', 'add_ignored_folder': 'Add ignored folder', @@ -330,6 +343,15 @@ export const TRANSLATIONS: Translations = { 'rename_folder': 'Rename folder', 'enter_new_folder_name': 'Enter new folder name', 'search_selection_in_grid_view': 'Search ... in grid view', + 'show_date_dividers': 'Show date dividers', + 'show_date_dividers_desc': 'Show date dividers before the first item of each different day when using date-related sorting', + 'date_divider_format': 'Date divider format', + 'date_divider_mode': 'Date divider', + 'date_divider_mode_desc': 'Select the display mode for date dividers', + 'date_divider_mode_none': 'None', + 'date_divider_mode_year': 'Year', + 'date_divider_mode_month': 'Month', + 'date_divider_mode_day': 'Day', }, 'zh': { // 通知信息 @@ -375,10 +397,12 @@ export const TRANSLATIONS: Translations = { // 设置 'grid_view_settings': '网格视图设置', 'media_files_settings': '媒体文件设置', - 'show_media_files': '显示图片和视频', - 'show_media_files_desc': '在网格视图中显示图片和视频文件', + 'show_media_files': '显示媒体文件', + 'show_media_files_desc': '在网格视图中显示媒体文件', 'show_video_thumbnails': '显示视频缩略图', 'show_video_thumbnails_desc': '在网格视图中显示视频的缩略图,关闭时将显示播放图标', + 'show_note_tags': '显示笔记标签', + 'show_note_tags_desc': '在网格视图中显示笔记的标签', 'ignored_folders': '忽略的文件夹', 'ignored_folders_desc': '在这里设置要忽略的文件夹', 'add_ignored_folder': '添加忽略文件夹', @@ -423,7 +447,7 @@ export const TRANSLATIONS: Translations = { 'show_random_note_mode': '显示随机笔记模式', 'random_note_count': '随机笔记模式显示笔数', 'random_note_notes_only': '仅笔记', - 'random_note_include_media_files': '包含图片视频', + 'random_note_include_media_files': '包含媒体文件', // 显示"返回上级文件夹"选项设置 'show_parent_folder_item': '显示「返回上级文件夹」', @@ -486,6 +510,15 @@ export const TRANSLATIONS: Translations = { 'rename_folder': '重新命名文件夹', 'enter_new_folder_name': '输入新文件夹名称', 'search_selection_in_grid_view': '在网格视图中搜寻...', + 'show_date_dividers': '显示日期分隔器', + 'show_date_dividers_desc': '在日期相关排序时,在不同天的第一条之前显示日期分隔器', + 'date_divider_format': '日期分隔器格式', + 'date_divider_mode': '日期分隔器', + 'date_divider_mode_desc': '选择日期分隔器的显示模式', + 'date_divider_mode_none': '不使用', + 'date_divider_mode_year': '年', + 'date_divider_mode_month': '月', + 'date_divider_mode_day': '日', }, 'ja': { // 通知メッジ @@ -531,10 +564,12 @@ export const TRANSLATIONS: Translations = { // 設定 'grid_view_settings': 'グリッドビュー設定', 'media_files_settings': 'メディアファイル設定', - 'show_media_files': '画像と動画を表示', - 'show_media_files_desc': 'グリッドビューで画像と動画ファイルを表示する', + 'show_media_files': 'メディアファイルを表示', + 'show_media_files_desc': 'グリッドビューでメディアファイルを表示する', 'show_video_thumbnails': '動画のサムネイルを表示', 'show_video_thumbnails_desc': 'グリッドビューで動画のサムネイルを表示する、無効の場合は再生アイコンを表示', + 'show_note_tags': 'ノートのタグを表示', + 'show_note_tags_desc': 'グリッドビューでノートのタグを表示する', 'ignored_folders': '無視するフォルダ', 'ignored_folders_desc': '無視するフォルダを設定する', 'add_ignored_folder': '無視するフォルダを追加', @@ -579,9 +614,9 @@ export const TRANSLATIONS: Translations = { 'show_random_note_mode': 'ランダムノートモードを表示', 'random_note_count': 'ランダムノートモード表示筆数', 'random_note_notes_only': 'ノートのみ', - 'random_note_include_media_files': 'メディアを含む', + 'random_note_include_media_files': 'メディアファイルを含む', - // 「親フォルダ」オプション設定を表示 + // "親フォルダ"オプション設定を表示 'show_parent_folder_item': '「親フォルダ」項目を表示', 'show_parent_folder_item_desc': 'グリッドの最初の項目として「親フォルダ」項目を表示します', 'parent_folder': '親フォルダ', @@ -642,5 +677,14 @@ export const TRANSLATIONS: Translations = { 'rename_folder': 'フォルダを再命名', 'enter_new_folder_name': '新しいフォルダ名を入力', 'search_selection_in_grid_view': '... をグリッドビューで検索', + 'show_date_dividers': '日付区切りを表示', + 'show_date_dividers_desc': '日付関連の並び替え時、各日の最初のアイテムの前に日付区切りを表示する', + 'date_divider_format': '日付区切りフォーマット', + 'date_divider_mode': '日付区切り', + 'date_divider_mode_desc': '日付区切りの表示モードを選択', + 'date_divider_mode_none': '使用しない', + 'date_divider_mode_year': '年', + 'date_divider_mode_month': '月', + 'date_divider_mode_day': '日', } } diff --git a/styles.css b/styles.css index e87eb8e..c2b7d7d 100644 --- a/styles.css +++ b/styles.css @@ -143,6 +143,10 @@ height: 100%; } +.ge-grid-item:hover p { + color: var(--text-muted); +} + .ge-content-area p:empty { margin: 0; } @@ -167,6 +171,30 @@ border-radius: 4px; } +/* 標籤樣式 */ +.ge-tags-container { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 8px; + margin-bottom: 2px; +} + +.ge-tag { + display: inline-flex; + align-items: center; + padding: 0 6px; + font-size: 0.8em; + border: 1px solid var(--background-modifier-border); + border-radius: 5px; + background-color: var(--background-secondary-alt); + color: var(--text-accent); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + /* 資料夾項目的特殊樣式 */ .ge-grid-item.ge-folder-item { background-color: var(--background-primary-alt); @@ -210,6 +238,18 @@ align-items: center; } +/* 日期分隔器樣式 */ +.ge-date-divider { + width: 100%; + text-align: left; + color: var(--text-normal); + margin-left: 8px; + grid-column: 1 / -1; + pointer-events: none; + user-select: none; + background-color: var(--background-secondary); +} + /* 頂部按鈕區域樣式 */ .ge-header-buttons { display: flex; diff --git a/versions.json b/versions.json index 70003c2..6ea7ddb 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "1.9.11": "1.1.0" + "1.9.12": "1.1.0" } \ No newline at end of file