diff --git a/manifest.json b/manifest.json index 2009f68..c9b83c8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "gridexplorer", "name": "GridExplorer", - "version": "2.6.6", + "version": "2.7.0", "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 f0ab08c..f834ffb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gridexplorer", - "version": "2.6.6", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gridexplorer", - "version": "2.6.6", + "version": "2.7.0", "license": "MIT", "devDependencies": { "@types/node": "^16.11.6", diff --git a/package.json b/package.json index 6f43f8c..cbb2c29 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gridexplorer", - "version": "2.6.6", + "version": "2.7.0", "description": "Browse note files in a grid view.", "main": "main.js", "scripts": { diff --git a/src/GridView.ts b/src/GridView.ts index b79893e..7143de5 100644 --- a/src/GridView.ts +++ b/src/GridView.ts @@ -9,7 +9,9 @@ import { MediaModal } from './modal/mediaModal'; import { showFolderNoteSettingsModal } from './modal/folderNoteSettingsModal'; import { showNoteSettingsModal } from './modal/noteSettingsModal'; import { showFolderRenameModal } from './modal/folderRenameModal'; +import { moveFolderSuggestModal } from './modal/moveFolderSuggestModal'; import { showSearchModal } from './modal/searchModal'; +import { CustomModeModal } from './modal/customModeModal'; import { FloatingAudioPlayer } from './floatingAudioPlayer'; import { t } from './translations'; @@ -46,6 +48,8 @@ export class GridView extends ItemView { recentSources: string[] = []; // 歷史記錄 minMode: boolean = false; // 最小模式 showIgnoredFolders: boolean = false; // 顯示忽略資料夾 + showDateDividers: boolean = false; // 顯示日期分隔器 + showNoteTags: boolean = false; // 顯示筆記標籤 pinnedList: string[] = []; // 置頂清單 taskFilter: string = 'uncompleted'; // 任務分類 hideHeaderElements: boolean = false; // 是否隱藏標題列元素(模式名稱和按鈕) @@ -64,6 +68,8 @@ export class GridView extends ItemView { this.sortType = this.plugin.settings.defaultSortType; // 使用設定中的預設排序模式 this.baseCardLayout = this.plugin.settings.cardLayout; this.cardLayout = this.baseCardLayout; + this.showDateDividers = this.plugin.settings.dateDividerMode !== 'none'; + this.showNoteTags = this.plugin.settings.showNoteTags; // 根據設定決定是否註冊檔案變更監聽器 if (this.plugin.settings.enableFileWatcher) { @@ -467,7 +473,7 @@ export class GridView extends ItemView { // 添加重新選擇資料夾按鈕 const reselectButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('reselect') } }); reselectButton.addEventListener('click', () => { - showFolderSelectionModal(this.app, this.plugin, this); + showFolderSelectionModal(this.app, this.plugin, this, reselectButton); }); setIcon(reselectButton, "grid"); @@ -481,8 +487,6 @@ export class GridView extends ItemView { }); setIcon(refreshButton, 'refresh-ccw'); - // 排序按鈕已移動到模式名稱區域 - // 添加搜尋按鈕 const searchButtonContainer = headerButtonsDiv.createDiv('ge-search-button-container'); const searchButton = searchButtonContainer.createEl('button', { @@ -491,7 +495,7 @@ export class GridView extends ItemView { }); setIcon(searchButton, 'search'); searchButton.addEventListener('click', () => { - showSearchModal(this.app, this, ''); + showSearchModal(this.app, this, '', searchButton); }); // 如果有搜尋關鍵字,顯示搜尋文字和取消按鈕 @@ -504,7 +508,7 @@ export class GridView extends ItemView { // 讓搜尋文字可點選 searchText.style.cursor = 'pointer'; searchText.addEventListener('click', () => { - showSearchModal(this.app, this, this.searchQuery); + showSearchModal(this.app, this, this.searchQuery, searchText); }); // 創建取消按鈕 @@ -521,123 +525,154 @@ export class GridView extends ItemView { } // 添加更多選項按鈕 + + const menu = new Menu(); + menu.addItem((item) => { + item + .setTitle(t('open_new_grid_view')) + .setIcon('grid') + .onClick(() => { + const { workspace } = this.app; + let leaf = null; + workspace.getLeavesOfType('grid-view'); + switch (this.plugin.settings.defaultOpenLocation) { + case 'left': + leaf = workspace.getLeftLeaf(false); + break; + case 'right': + leaf = workspace.getRightLeaf(false); + break; + case 'tab': + default: + leaf = workspace.getLeaf('tab'); + break; + } + if (!leaf) { + // 如果無法獲取指定位置的 leaf,則回退到新分頁 + leaf = workspace.getLeaf('tab'); + } + leaf.setViewState({ type: 'grid-view', active: true }); + // 設定資料來源 + if (leaf.view instanceof GridView) { + leaf.view.setSource('folder', '/'); + } + // 確保視圖是活躍的 + workspace.revealLeaf(leaf); + }); + }); + menu.addSeparator(); + + // 建立隨機筆記、最近筆記、全部筆記是否包含圖片和影片的設定按鈕 + if ((this.sourceMode === 'all-files' || this.sourceMode === 'recent-files' || this.sourceMode === 'random-note') && + this.plugin.settings.showMediaFiles && this.searchQuery === '') { + menu.addItem((item) => { + item.setTitle(t('random_note_notes_only')) + .setIcon('file-text') + .setChecked(!this.randomNoteIncludeMedia) + .onClick(() => { + this.randomNoteIncludeMedia = false; + this.render(); + }); + }); + menu.addItem((item) => { + item.setTitle(t('random_note_include_media_files')) + .setIcon('file-image') + .setChecked(this.randomNoteIncludeMedia) + .onClick(() => { + this.randomNoteIncludeMedia = true; + this.render(); + }); + }); + menu.addSeparator(); + } + // 直向卡片切換 + menu.addItem((item) => { + item.setTitle(t('vertical_card')) + .setIcon('layout') + .setChecked(this.baseCardLayout === 'vertical') + .onClick(() => { + this.baseCardLayout = this.baseCardLayout === 'vertical' ? 'horizontal' : 'vertical'; + this.cardLayout = this.baseCardLayout; + this.render(); + this.app.workspace.requestSaveLayout(); + }); + }); + // 最小化模式選項 + menu.addItem((item) => { + item + .setTitle(t('min_mode')) + .setIcon('minimize-2') + .setChecked(this.minMode) + .onClick(() => { + this.minMode = !this.minMode; + this.app.workspace.requestSaveLayout(); + this.render(); + }); + }); + // 顯示日期分隔器 + if (this.plugin.settings.dateDividerMode !== 'none') { + menu.addItem((item) => { + item + .setTitle(t('show_date_dividers')) + .setIcon('calendar') + .setChecked(this.showDateDividers) + .onClick(() => { + this.showDateDividers = !this.showDateDividers; + this.app.workspace.requestSaveLayout(); + this.render(); + }); + }); + } + // 顯示筆記標籤 + menu.addItem((item) => { + item + .setTitle(t('show_note_tags')) + .setIcon('tag') + .setChecked(this.showNoteTags) + .onClick(() => { + this.showNoteTags = !this.showNoteTags; + this.app.workspace.requestSaveLayout(); + this.render(); + }); + }); + // 顯示忽略資料夾選項 + menu.addItem((item) => { + item + .setTitle(t('show_ignored_folders')) + .setIcon('folder-open-dot') + .setChecked(this.showIgnoredFolders) + .onClick(() => { + this.showIgnoredFolders = !this.showIgnoredFolders; + this.app.workspace.requestSaveLayout(); + this.render(); + }); + }); + menu.addSeparator(); + menu.addItem((item) => { + item + .setTitle(t('open_settings')) + .setIcon('settings') + .onClick(() => { + // 打開插件設定頁面 + (this.app as any).setting.open(); + (this.app as any).setting.openTabById(this.plugin.manifest.id); + }); + }); + if (this.searchQuery === '') { const moreOptionsButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('more_options') } }); setIcon(moreOptionsButton, 'ellipsis-vertical'); - const menu = new Menu(); - menu.addItem((item) => { - item - .setTitle(t('open_new_grid_view')) - .setIcon('grid') - .onClick(() => { - const { workspace } = this.app; - let leaf = null; - workspace.getLeavesOfType('grid-view'); - switch (this.plugin.settings.defaultOpenLocation) { - case 'left': - leaf = workspace.getLeftLeaf(false); - break; - case 'right': - leaf = workspace.getRightLeaf(false); - break; - case 'tab': - default: - leaf = workspace.getLeaf('tab'); - break; - } - if (!leaf) { - // 如果無法獲取指定位置的 leaf,則回退到新分頁 - leaf = workspace.getLeaf('tab'); - } - leaf.setViewState({ type: 'grid-view', active: true }); - // 設定資料來源 - if (leaf.view instanceof GridView) { - leaf.view.setSource('folder', '/'); - } - // 確保視圖是活躍的 - workspace.revealLeaf(leaf); - }); - }); - menu.addSeparator(); - - - // 建立隨機筆記、最近筆記、全部筆記是否包含圖片和影片的設定按鈕 - if ((this.sourceMode === 'all-files' || this.sourceMode === 'recent-files' || this.sourceMode === 'random-note') && - this.plugin.settings.showMediaFiles && this.searchQuery === '') { - menu.addItem((item) => { - item.setTitle(t('random_note_notes_only')) - .setIcon('file-text') - .setChecked(!this.randomNoteIncludeMedia) - .onClick(() => { - this.randomNoteIncludeMedia = false; - this.render(); - }); - }); - menu.addItem((item) => { - item.setTitle(t('random_note_include_media_files')) - .setIcon('file-image') - .setChecked(this.randomNoteIncludeMedia) - .onClick(() => { - this.randomNoteIncludeMedia = true; - this.render(); - }); - }); - menu.addSeparator(); - } - - // 直向卡片切換 - menu.addItem((item) => { - item.setTitle(t('vertical_card')) - .setIcon('layout') - .setChecked(this.baseCardLayout === 'vertical') - .onClick(() => { - this.baseCardLayout = this.baseCardLayout === 'vertical' ? 'horizontal' : 'vertical'; - this.cardLayout = this.baseCardLayout; - this.render(); - this.app.workspace.requestSaveLayout(); - }); - }); - - // 最小化模式選項 - menu.addItem((item) => { - item - .setTitle(t('min_mode')) - .setIcon('minimize-2') - .setChecked(this.minMode) - .onClick(() => { - this.minMode = !this.minMode; - this.app.workspace.requestSaveLayout(); - this.render(); - }); - }); - // 顯示忽略資料夾選項 - menu.addItem((item) => { - item - .setTitle(t('show_ignored_folders')) - .setIcon('folder-open-dot') - .setChecked(this.showIgnoredFolders) - .onClick(() => { - this.showIgnoredFolders = !this.showIgnoredFolders; - this.app.workspace.requestSaveLayout(); - this.render(); - }); - }); - menu.addSeparator(); - menu.addItem((item) => { - item - .setTitle(t('open_settings')) - .setIcon('settings') - .onClick(() => { - // 打開插件設定頁面 - (this.app as any).setting.open(); - (this.app as any).setting.openTabById(this.plugin.manifest.id); - }); - }); - moreOptionsButton.addEventListener('click', (event) => { menu.showAtMouseEvent(event); }); - } + } + + headerButtonsDiv.addEventListener('contextmenu', (event) => { + if (event.target === headerButtonsDiv) { + event.preventDefault(); + menu.showAtMouseEvent(event); + } + }); // 創建模式名稱和排序按鈕的容器 const modeHeaderContainer = this.containerEl.createDiv('ge-mode-header-container'); @@ -1143,46 +1178,80 @@ export class GridView extends ItemView { // 取得當前自訂模式 const mode = this.plugin.settings.customModes.find(m => m.internalName === this.sourceMode); - if (mode && mode.options && mode.options.length > 0) { - if (this.customOptionIndex >= mode.options.length || this.customOptionIndex < -1) { - this.customOptionIndex = -1; - } + if (mode) { + const hasOptions = mode.options && mode.options.length > 0; + + if (hasOptions && mode.options) { + if (this.customOptionIndex >= mode.options.length || this.customOptionIndex < -1) { + this.customOptionIndex = -1; + } - let subName: string | undefined; - if (this.customOptionIndex === -1) { - subName = (mode as any).name?.trim() || t('default'); - } else if (this.customOptionIndex >= 0 && this.customOptionIndex < mode.options.length) { - const opt = mode.options![this.customOptionIndex]; - subName = opt.name?.trim() || `${t('option')} ${this.customOptionIndex + 1}`; - } + let subName: string | undefined; + if (this.customOptionIndex === -1) { + subName = (mode as any).name?.trim() || t('default'); + } else if (this.customOptionIndex >= 0 && this.customOptionIndex < mode.options.length) { + const opt = mode.options[this.customOptionIndex]; + subName = opt.name?.trim() || `${t('option')} ${this.customOptionIndex + 1}`; + } - const subSpan = modenameContainer.createEl('a', { text: subName ?? '-', cls: 'ge-sub-option' }); - subSpan.addEventListener('click', (evt) => { - const menu = new Menu(); - // 預設選項 - const defaultName = (mode as any).name?.trim() || t('default'); - menu.addItem(item => { - item.setTitle(defaultName) - .setIcon('puzzle') - .setChecked(this.customOptionIndex === -1) - .onClick(() => { - this.customOptionIndex = -1; - this.render(true); - }); - }); - mode.options!.forEach((opt, idx) => { + const subSpan = modenameContainer.createEl('a', { text: subName ?? '-', cls: 'ge-sub-option' }); + subSpan.addEventListener('click', (evt) => { + const menu = new Menu(); + // 預設選項 + const defaultName = (mode as any).name?.trim() || t('default'); menu.addItem(item => { - item.setTitle(opt.name?.trim() || t('option') + ' ' + (idx + 1)) + item.setTitle(defaultName) .setIcon('puzzle') - .setChecked(idx === this.customOptionIndex) + .setChecked(this.customOptionIndex === -1) .onClick(() => { - this.customOptionIndex = idx; + this.customOptionIndex = -1; this.render(true); }); }); + mode.options!.forEach((opt, idx) => { + menu.addItem(item => { + item.setTitle(opt.name?.trim() || t('option') + ' ' + (idx + 1)) + .setIcon('puzzle') + .setChecked(idx === this.customOptionIndex) + .onClick(() => { + this.customOptionIndex = idx; + this.render(true); + }); + }); + }); + + // 設定選項 + menu.addSeparator(); + menu.addItem(item => { + item.setTitle(t('edit')) + .setIcon('settings') + .onClick(() => { + const modeIndex = this.plugin.settings.customModes.findIndex(m => m.internalName === mode.internalName); + if (modeIndex === -1) return; + new CustomModeModal(this.app, this.plugin, this.plugin.settings.customModes[modeIndex], (result) => { + this.plugin.settings.customModes[modeIndex] = result; + this.plugin.saveSettings(); + this.render(true); + }).open(); + }); + }); + + menu.showAtMouseEvent(evt); }); - menu.showAtMouseEvent(evt); - }); + } else { + // 只有預設選項時,只顯示齒輪圖示 + const gearIcon = modenameContainer.createEl('a', { cls: 'ge-sub-option' }); + setIcon(gearIcon, 'settings'); + gearIcon.addEventListener('click', () => { + const modeIndex = this.plugin.settings.customModes.findIndex(m => m.internalName === mode.internalName); + if (modeIndex === -1) return; + new CustomModeModal(this.app, this.plugin, this.plugin.settings.customModes[modeIndex], (result) => { + this.plugin.settings.customModes[modeIndex] = result; + this.plugin.saveSettings(); + this.render(true); + }).open(); + }); + } } } break; @@ -1608,6 +1677,17 @@ export class GridView extends ItemView { }); }); } + // 搬移資料夾 + menu.addItem((item) => { + item + .setTitle(t('move_folder')) + .setIcon('folder-cog') + .onClick(() => { + if (folder instanceof TFolder) { + new moveFolderSuggestModal(this.plugin, folder, this).open(); + } + }); + }); // 重新命名資料夾 menu.addItem((item) => { item @@ -1619,7 +1699,7 @@ export class GridView extends ItemView { } }); }); - //刪除資料夾 + // 刪除資料夾 menu.addItem((item) => { (item as any).setWarning(true); item @@ -2147,11 +2227,11 @@ export class GridView extends ItemView { contentArea.createEl('p', { text: file.extension.toUpperCase() }); } - setTooltip(fileEl as HTMLElement, `${file.name}`) + setTooltip(fileEl as HTMLElement, `${file.name}`,{ delay:2000 }) } // 顯示標籤(僅限 Markdown 檔案) - if (file.extension === 'md' && this.plugin.settings.showNoteTags && !this.minMode) { + if (file.extension === 'md' && this.showNoteTags && !this.minMode) { const fileCache = this.app.metadataCache.getFileCache(file); const displaySetting = fileCache?.frontmatter?.display; @@ -2322,7 +2402,8 @@ export class GridView extends ItemView { const shouldShowDateDividers = dateDividerMode !== 'none' && (sortType.startsWith('mtime-') || sortType.startsWith('ctime-')) && this.sourceMode !== 'random-note' && - this.sourceMode !== 'bookmarks'; + this.sourceMode !== 'bookmarks' && + this.showDateDividers; let lastDateString = ''; let pinDividerAdded = false; @@ -2965,6 +3046,8 @@ export class GridView extends ItemView { baseCardLayout: this.baseCardLayout, cardLayout: this.cardLayout, hideHeaderElements: this.hideHeaderElements, + showDateDividers: this.showDateDividers, + showNoteTags: this.showNoteTags, } }; } @@ -2985,6 +3068,8 @@ export class GridView extends ItemView { this.baseCardLayout = state.state.baseCardLayout ?? 'horizontal'; this.cardLayout = state.state.cardLayout ?? this.baseCardLayout; // 同步 baseCardLayout 的卡片樣式,以便 render() 使用正確的 cardLayout this.hideHeaderElements = state.state.hideHeaderElements ?? false; + this.showDateDividers = state.state.showDateDividers ?? this.plugin.settings.dateDividerMode !== 'none'; + this.showNoteTags = state.state.showNoteTags ?? this.plugin.settings.showNoteTags; this.render(); } } diff --git a/src/fileUtils.ts b/src/fileUtils.ts index 92c326b..5402b40 100644 --- a/src/fileUtils.ts +++ b/src/fileUtils.ts @@ -513,7 +513,6 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean): const mode = settings.customModes.find(m => m.internalName === sourceMode); if (!mode) { - new Notice(`Custom mode ${sourceMode} not found.`); return []; } diff --git a/src/modal/customModeModal.ts b/src/modal/customModeModal.ts index ae401c2..4254ee7 100644 --- a/src/modal/customModeModal.ts +++ b/src/modal/customModeModal.ts @@ -128,16 +128,13 @@ export class CustomModeModal extends Modal { }); }); - // 移除按鈕(至少保留一個) + // 移除按鈕 if (options.length > 0) { optSetting.addExtraButton(btn => { btn.setIcon('trash') .setTooltip(t('remove')) .onClick(() => { options.splice(idx, 1); - if (idx === 0 && options.length > 0) { - dataviewCode = options[0].dataviewCode; - } renderOptions(); }); }); diff --git a/src/modal/folderSelectionModal.ts b/src/modal/folderSelectionModal.ts index f0f3523..64cafbd 100644 --- a/src/modal/folderSelectionModal.ts +++ b/src/modal/folderSelectionModal.ts @@ -4,8 +4,8 @@ import { GridView } from '../GridView'; import { t } from '../translations'; // 顯示資料夾選擇 modal -export function showFolderSelectionModal(app: App, plugin: GridExplorerPlugin, activeView?: GridView) { - new FolderSelectionModal(app, plugin, activeView).open(); +export function showFolderSelectionModal(app: App, plugin: GridExplorerPlugin, activeView?: GridView, buttonElement?: HTMLElement) { + new FolderSelectionModal(app, plugin, activeView, buttonElement).open(); } export class FolderSelectionModal extends Modal { @@ -15,17 +15,25 @@ export class FolderSelectionModal extends Modal { folderOptions: HTMLElement[] = []; selectedIndex: number = -1; // 當前選中的選項索引 searchInput: HTMLInputElement; + buttonElement: HTMLElement | undefined; - constructor(app: App, plugin: GridExplorerPlugin, activeView?: GridView) { + constructor(app: App, plugin: GridExplorerPlugin, activeView?: GridView, buttonElement?: HTMLElement) { super(app); this.plugin = plugin; this.activeView = activeView; + this.buttonElement = buttonElement; } onOpen() { const { contentEl } = this; contentEl.empty(); + // 如果有按鈕元素,設置為 popup 樣式 + if (this.buttonElement) { + this.modalEl.addClass('ge-popup-modal'); + this.positionAsPopup(); + } + // 添加搜尋輸入框 const searchContainer = contentEl.createEl('div', { cls: 'ge-folder-search-container' @@ -240,9 +248,10 @@ export class FolderSelectionModal extends Modal { } // 建立根目錄選項 + const customFolderIcon = this.plugin.settings.customFolderIcon; const rootFolderOption = this.folderOptionsContainer.createEl('div', { cls: 'ge-grid-view-folder-option', - text: `📁 /` + text: `${customFolderIcon} /` }); rootFolderOption.addEventListener('click', () => { @@ -287,7 +296,7 @@ export class FolderSelectionModal extends Modal { // 資料夾圖示與名稱 const icon = document.createElement('span'); - icon.textContent = '📁 '; + icon.textContent = `${customFolderIcon} `; folderOption.appendChild(icon); const nameSpan = document.createElement('span'); @@ -423,6 +432,48 @@ export class FolderSelectionModal extends Modal { } } + // 將 modal 定位到按鈕下方,類似 Chrome popup 樣式 + positionAsPopup() { + if (!this.buttonElement) return; + + const buttonRect = this.buttonElement.getBoundingClientRect(); + const modalEl = this.modalEl; + const contentEl = this.contentEl; + + // 設置 modal 的基本樣式 + modalEl.addClass('ge-popup-modal-reset'); + + // 添加 popup 內容樣式類別 + contentEl.addClass('ge-popup-content'); + + // 計算位置 + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // 預設位置:按鈕下方中心對齊 + let left = buttonRect.left + (buttonRect.width / 2) - 150; // 150 是 modal 寬度的一半 + let top = buttonRect.bottom + 8; // 8px 間距 + + // 檢查右側邊界 + if (left + 300 > viewportWidth) { + left = viewportWidth - 300 - 16; + } + + // 檢查左側邊界 + if (left < 16) { + left = 16; + } + + // 檢查下方空間,如果不夠則顯示在上方 + if (top + 400 > viewportHeight && buttonRect.top - 400 > 0) { + top = buttonRect.top - 8; + } + + // 應用位置 + modalEl.style.left = `${left}px`; + modalEl.style.top = `${top}px`; + } + onClose() { const { contentEl } = this; contentEl.empty(); diff --git a/src/modal/moveFolderSuggestModal.ts b/src/modal/moveFolderSuggestModal.ts new file mode 100644 index 0000000..c7676d8 --- /dev/null +++ b/src/modal/moveFolderSuggestModal.ts @@ -0,0 +1,48 @@ +import { SuggestModal, TFolder, normalizePath } from 'obsidian'; +import GridExplorerPlugin from '../main'; +import { GridView } from '../GridView'; + +export class moveFolderSuggestModal extends SuggestModal { + private readonly allPaths: string[]; + + constructor( + private readonly plugin: GridExplorerPlugin, + private readonly folder: TFolder, + private readonly view: GridView + ) { + super(plugin.app); + // 預先快取所有資料夾路徑以加速篩選 + this.allPaths = this.app.vault.getAllFolders().map(f => f.path).sort((a, b) => a.localeCompare(b)); + this.setPlaceholder('/'); + // 自動聚焦輸入框,讓使用者可以直接打字 + this.inputEl.focus(); + } + + getSuggestions(query: string): string[] { + const lower = query.toLowerCase(); + const filtered = this.allPaths.filter(p => p.toLowerCase().includes(lower)); + if ('/'.includes(lower) && !filtered.includes('/')) { + // 始終允許根目錄 + filtered.unshift('/'); + } + return filtered; + } + + renderSuggestion(value: string, el: HTMLElement): void { + el.setText(value); + } + + async onChooseSuggestion(value: string): Promise { + try { + const dest = value === '/' ? '' : value.replace(/\/$/, ''); + const newPath = normalizePath(dest ? `${dest}/${this.folder.name}` : this.folder.name); + if (newPath === this.folder.path) return; + + await this.app.fileManager.renameFile(this.folder, newPath); + // 給檔案系統一點時間處理,然後重新整理視圖 + setTimeout(() => this.view.render(), 100); + } catch (err) { + console.error('Failed to move folder', err); + } + } +} diff --git a/src/modal/searchModal.ts b/src/modal/searchModal.ts index 7586163..9213f5d 100644 --- a/src/modal/searchModal.ts +++ b/src/modal/searchModal.ts @@ -5,16 +5,23 @@ import { t } from '../translations'; export class SearchModal extends Modal { gridView: GridView; defaultQuery: string; - constructor(app: App, gridView: GridView, defaultQuery: string) { + buttonElement: HTMLElement | undefined; + constructor(app: App, gridView: GridView, defaultQuery: string, buttonElement?: HTMLElement) { super(app); this.gridView = gridView; this.defaultQuery = defaultQuery; + this.buttonElement = buttonElement; } onOpen() { const { contentEl } = this; contentEl.empty(); - new Setting(contentEl).setName(t('search')).setHeading(); + + // 如果有提供按鈕元素,則設置為 popup 樣式 + if (this.buttonElement) { + this.modalEl.addClass('ge-popup-modal'); + this.positionAsPopup(); + } // 創建搜尋輸入框容器 const searchContainer = contentEl.createDiv('ge-search-container'); @@ -326,9 +333,53 @@ export class SearchModal extends Modal { const { contentEl } = this; contentEl.empty(); } + + private positionAsPopup() { + if (!this.buttonElement) return; + + const modalEl = this.modalEl; + const contentEl = this.contentEl; + + // 設置 modal 的基本樣式 + modalEl.addClass('ge-popup-modal-reset'); + + // 添加 popup 內容樣式類別 + contentEl.addClass('ge-popup-content'); + + // 計算按鈕位置 + const buttonRect = this.buttonElement.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // 設置初始位置(按鈕下方) + let top = buttonRect.bottom + 4; + let left = buttonRect.left + (buttonRect.width / 2) - 150; // 300px 寬度的一半 + + // 檢查右側邊界 + if (left + 300 > viewportWidth) { + left = viewportWidth - 300 - 10; + } + + // 檢查左側邊界 + if (left < 10) { + left = 10; + } + + // 檢查下方空間,如果不夠則顯示在上方 + const estimatedHeight = 400; // 預估高度 + if (top + estimatedHeight > viewportHeight && buttonRect.top - estimatedHeight > 0) { + top = buttonRect.top - estimatedHeight - 4; + } + + // 應用位置 + modalEl.style.position = 'fixed'; + modalEl.style.top = `${top}px`; + modalEl.style.left = `${left}px`; + modalEl.style.transform = 'none'; + } } // 顯示搜尋 modal -export function showSearchModal(app:App, gridView: GridView, defaultQuery = '') { - new SearchModal(app, gridView, defaultQuery).open(); +export function showSearchModal(app:App, gridView: GridView, defaultQuery = '', buttonElement?: HTMLElement) { + new SearchModal(app, gridView, defaultQuery, buttonElement).open(); } diff --git a/src/settings.ts b/src/settings.ts index efbf193..712330d 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1126,20 +1126,75 @@ export class GridExplorerSettingTab extends PluginSettingTab { containerEl.appendChild(ignoredFolderPatternsContainer); - containerEl.createEl('h3', { text: t('reset_to_default'), attr: { style: 'margin-top: 40px;' } }); + // 設定檔管理 (Reset / Export / Import) + containerEl.createEl('h3', { text: t('config_management'), attr: { style: 'margin-top: 40px;' } }); - // 回復預設值按鈕 new Setting(containerEl) - .setName(t('reset_to_default')) - .setDesc(t('reset_to_default_desc')) + .setName(t('config_management')) + .setDesc(t('config_management_desc')) + // Reset to default .addButton(button => button - .setButtonText(t('reset')) + .setButtonText(t('reset_to_default')) .setWarning() .onClick(async () => { this.plugin.settings = { ...DEFAULT_SETTINGS }; await this.plugin.saveSettings(); this.display(); new Notice(t('settings_reset_notice')); + })) + // Export settings to JSON file + .addButton(button => button + .setButtonText(t('export')) + .setTooltip(t('export')) + .onClick(() => { + const data = JSON.stringify(this.plugin.settings, null, 2); + const blob = new Blob([data], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'grid-explorer-settings.json'; + a.click(); + URL.revokeObjectURL(url); + })) + // Import settings from JSON file + .addButton(button => button + .setButtonText(t('import')) + .setTooltip(t('import')) + .onClick(() => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = async (e) => { + const files = (e.target as HTMLInputElement).files; + if (!files || files.length === 0) { + return; + } + const file = files[0]; + const reader = new FileReader(); + reader.onload = async (evt) => { + if (!evt.target || typeof evt.target.result !== 'string') { + new Notice(t('import_failed')); + return; + } + try { + const content = evt.target.result; + const importedSettings = JSON.parse(content); + if (importedSettings && typeof importedSettings === 'object') { + this.plugin.settings = { ...DEFAULT_SETTINGS, ...importedSettings } as typeof DEFAULT_SETTINGS; + await this.plugin.saveSettings(); + this.display(); + new Notice(t('import_success')); + } else { + new Notice(t('import_failed')); + } + } catch (error) { + console.error('Grid Explorer: Error importing settings', error); + new Notice(t('import_failed')); + } + }; + reader.readAsText(file); + }; + input.click(); })); } diff --git a/src/translations.ts b/src/translations.ts index 889bd1a..de5f982 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -39,6 +39,8 @@ export const TRANSLATIONS: Translations = { 'new_folder': '新增資料夾', 'new_canvas': '新增畫布', 'delete_folder': '刪除資料夾', + 'move_folder': '搬移資料夾', + 'select_destination_folder': '選擇目標資料夾', 'untitled': '未命名', 'files': '個檔案', 'add': '新增', @@ -136,8 +138,9 @@ export const TRANSLATIONS: Translations = { 'intercept_breadcrumb_clicks': '攔截筆記導航列點擊事件', 'intercept_breadcrumb_clicks_desc': '啟用後會攔截筆記導航列點擊事件,並在網格視圖中打開路徑', 'reset_to_default' : '重置為預設值', - 'reset_to_default_desc': '將所有設定重置為預設值', 'settings_reset_notice': '設定值已重置為預設值', + 'config_management': '設定檔管理', + 'config_management_desc': '管理設定檔的重置、匯出和匯入', 'ignored_folders_settings': '忽略資料夾設定', 'display_mode_settings': '顯示模式設定', 'custom_mode_settings': '自訂模式設定', @@ -145,7 +148,7 @@ export const TRANSLATIONS: Translations = { 'export': '匯出', 'import': '匯入', 'no_custom_modes_to_export': '沒有可匯出的自訂模式', - 'import_success': '自訂模式匯入成功', + 'import_success': '匯入成功', 'import_error': '匯入失敗:無效的檔案格式', 'edit_custom_mode': '編輯自訂模式', 'custom_mode': '自訂模式', @@ -302,7 +305,9 @@ export const TRANSLATIONS: Translations = { 'new_note': 'New note', 'new_folder': 'New folder', 'new_canvas': 'New canvas', - 'delete_folder': 'Delete folder', + 'delete_folder': 'Delete Folder', + 'move_folder': 'Move Folder', + 'select_destination_folder': 'Select destination folder', 'untitled': 'Untitled', 'files': 'files', 'add': 'Add', @@ -400,8 +405,9 @@ export const TRANSLATIONS: Translations = { 'intercept_breadcrumb_clicks': 'Intercept breadcrumb clicks', 'intercept_breadcrumb_clicks_desc': 'When enabled, breadcrumb clicks will be intercepted and the path will be opened in the grid view', 'reset_to_default': 'Reset to default', - 'reset_to_default_desc': 'Reset all settings to default values', 'settings_reset_notice': 'Settings have been reset to default values', + 'config_management': 'Config management', + 'config_management_desc': 'Manage config reset, export and import', 'ignored_folders_settings': 'Ignore folders settings', 'display_mode_settings': 'Display mode settings', 'custom_mode_settings': 'Custom Mode Settings', @@ -409,7 +415,7 @@ export const TRANSLATIONS: Translations = { 'export': 'Export', 'import': 'Import', 'no_custom_modes_to_export': 'No custom modes to export', - 'import_success': 'Custom modes imported successfully', + 'import_success': 'Import success', 'import_error': 'Import failed: Invalid file format', 'edit_custom_mode': 'Edit Custom Mode', 'custom_mode': 'Custom Mode', @@ -664,8 +670,9 @@ export const TRANSLATIONS: Translations = { 'intercept_breadcrumb_clicks': '拦截笔记导航栏点击事件', 'intercept_breadcrumb_clicks_desc': '启用后会拦截笔记导航栏点击事件,并在网格视图中打开路径', 'reset_to_default': '重置为默认值', - 'reset_to_default_desc': '将所有设置重置为默认值', 'settings_reset_notice': '设置值已重置为默认值', + 'config_management': '配置管理', + 'config_management_desc': '管理配置的重置、导出和导入', 'ignored_folders_settings': '忽略文件夹设置', 'display_mode_settings': '显示模式设置', 'custom_mode_settings': '自定义模式设置', @@ -673,7 +680,7 @@ export const TRANSLATIONS: Translations = { 'export': '导出', 'import': '导入', 'no_custom_modes_to_export': '没有可导出的自定义模式', - 'import_success': '自定义模式导入成功', + 'import_success': '导入成功', 'import_error': '导入失败:无效的文件格式', 'edit_custom_mode': '编辑自定义模式', 'custom_mode': '自定义模式', @@ -928,8 +935,9 @@ export const TRANSLATIONS: Translations = { 'intercept_breadcrumb_clicks': 'パンくずリストのクリックをインターセプト', 'intercept_breadcrumb_clicks_desc': '有効にすると、パンくずリストのクリックがインターセプトされ、パスがグリッドビューで開かれます', 'reset_to_default': 'デフォルトに戻す', - 'reset_to_default_desc': 'すべての設定をデフォルト値に戻', 'settings_reset_notice': '設定値がデフォルト値にリセットされました', + 'config_management': '設定管理', + 'config_management_desc': '管理設定のリセット、エクスポート、インポート', 'ignored_folders_settings': '無視するフォルダ設定', 'display_mode_settings': '表示モード設定', 'custom_mode_settings': 'カスタムモード設定', @@ -937,7 +945,7 @@ export const TRANSLATIONS: Translations = { 'export': 'エクスポート', 'import': 'インポート', 'no_custom_modes_to_export': 'エクスポートするカスタムモードがありません', - 'import_success': 'カスタムモードをインポートしました', + 'import_success': 'インポート成功', 'import_error': 'インポート失敗:無効なファイル形式', 'edit_custom_mode': 'カスタムモードを編集', 'custom_mode': 'カスタムモード', @@ -1193,8 +1201,9 @@ export const TRANSLATIONS: Translations = { 'intercept_breadcrumb_clicks': 'Перехватывать клики по навигационной цепочке', 'intercept_breadcrumb_clicks_desc': 'При включении клики по навигационной цепочке будут перехватываться, и путь будет открываться в виде сетки', 'reset_to_default': 'Сбросить на значения по умолчанию', - 'reset_to_default_desc': 'Сбросить все настройки на значения по умолчанию', 'settings_reset_notice': 'Настройки сброшены на значения по умолчанию', + 'config_management': 'Конфигурация управления', + 'config_management_desc': 'Управление конфигурацией (сброс, экспорт, импорт)', 'ignored_folders_settings': 'Настройки игнорируемых папок', 'display_mode_settings': 'Настройки режима отображения', 'custom_mode_settings': 'Настройки режима отображения', @@ -1202,7 +1211,7 @@ export const TRANSLATIONS: Translations = { 'export': 'Экспорт', 'import': 'Импорт', 'no_custom_modes_to_export': 'Нет режимов отображения для экспорта', - 'import_success': 'Режимы отображения успешно импортированы', + 'import_success': 'Импорт успешно', 'import_error': 'Ошибка импорта: недопустимый формат файла', 'edit_custom_mode': 'Редактировать режим отображения', 'custom_mode': 'Режим отображения', @@ -1457,8 +1466,9 @@ export const TRANSLATIONS: Translations = { 'intercept_breadcrumb_clicks': 'Перехоплювати кліки за навігаційним ланцюжком', 'intercept_breadcrumb_clicks_desc': 'При ввімкненні кліки за навігаційним ланцюжком будуть перехоплюватися, і шлях буде відкриватися у вигляді сітки', 'reset_to_default': 'Скинути до стандартних значень', - 'reset_to_default_desc': 'Скинути всі налаштування до стандартних значень', 'settings_reset_notice': 'Налаштування скинуто до стандартних значень', + 'config_management': 'Конфігурація управління', + 'config_management_desc': 'Управління конфігурацією (скидання, експорт, імпорт)', 'ignored_folders_settings': 'Налаштування ігнорованих папок', 'display_mode_settings': 'Налаштування режиму відображення', 'custom_mode_settings': 'Налаштування режиму відображення', @@ -1466,7 +1476,7 @@ export const TRANSLATIONS: Translations = { 'export': 'Експорт', 'import': 'Імпорт', 'no_custom_modes_to_export': 'Немає режимів відображення для експорту', - 'import_success': 'Режими відображення успішно імпортовані', + 'import_success': 'Імпорт успішний', 'import_error': 'Помилка імпорту: недопустимий формат файлу', 'edit_custom_mode': 'Редагувати режим відображення', 'custom_mode': 'Режим відображення', diff --git a/styles.css b/styles.css index c5ad11b..44ca290 100644 --- a/styles.css +++ b/styles.css @@ -823,6 +823,7 @@ a.ge-current-folder:hover { padding-right: 24px; height: 24px; border: none; + font-size: var(--font-ui-small); } .ge-search-tag-delete-button { @@ -863,6 +864,7 @@ a.ge-current-folder:hover { overflow-y: auto; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); z-index: 1000; + font-size: var(--font-ui-small); } .ge-search-tag-suggestion-item { @@ -884,7 +886,7 @@ a.ge-current-folder:hover { border-radius: 4px; background-color: var(--background-primary); color: var(--text-normal); - font-size: 14px; + font-size: var(--font-ui-medium); outline: none; transition: border-color 0.2s; padding-right: 25px; @@ -901,8 +903,9 @@ a.ge-current-folder:hover { /* 搜尋選項容器 Search Options Container */ .ge-search-options-container { display: flex; - gap: 16px; - margin: 4px; + flex-direction: column; + gap: 2px; + margin: 4px 0; } /* 搜尋範圍設定樣式 Search Scope Container */ @@ -910,7 +913,6 @@ a.ge-current-folder:hover { display: flex; align-items: center; cursor: pointer; - padding: 4px; } .ge-search-scope-checkbox { @@ -921,6 +923,7 @@ a.ge-current-folder:hover { cursor: pointer; user-select: none; color: var(--text-normal); + font-size: var(--font-ui-small); } /* 搜尋媒體檔案設定樣式 Search Media Files Container */ @@ -928,7 +931,6 @@ a.ge-current-folder:hover { display: flex; align-items: center; cursor: pointer; - padding: 4px; } .ge-search-media-files-checkbox { @@ -939,6 +941,7 @@ a.ge-current-folder:hover { cursor: pointer; user-select: none; color: var(--text-normal); + font-size: var(--font-ui-small); } .ge-button-container { @@ -1446,4 +1449,160 @@ a.ge-current-folder:hover { flex-direction: column; align-items: stretch; gap: 0.5rem; +} + +/* --------------------------------------------------------------------------- */ + +/* Popup Modal Styles */ +.ge-popup-modal { + background: transparent !important; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15) !important; + border: none !important; +} + +.ge-popup-modal-reset { + position: fixed; + top: auto; + left: auto; + right: auto; + bottom: auto; + transform: none; + max-width: 300px; + width: auto; + background-color: transparent; + box-shadow: none; + border: none; + border-radius: 0; + padding: 0; +} + +.ge-popup-modal-reset .modal-close-button { + display: none; +} + +.ge-popup-modal-reset .modal-header { + display: none; +} + +.is-phone .ge-popup-modal-reset.modal { + max-width: fit-content; + max-height: fit-content; +} + +.ge-popup-content { + background-color: var(--background-primary); + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + padding: 10px 8px; + position: relative; + max-height: 400px; + overflow-y: auto; + min-width: 250px; + max-width: 300px; +} + +/* Popup modal 中的資料夾選項樣式 */ +.ge-popup-content .ge-grid-view-folder-option { + margin: 2px 0; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; + font-size: var(--font-ui-medium); + line-height: 1.4; +} + +.ge-popup-content .ge-grid-view-folder-option:hover { + background-color: var(--background-modifier-hover); +} + +.ge-popup-content .ge-grid-view-folder-option.selected { + background-color: var(--background-modifier-active); +} + +/* Popup modal 中的搜尋框樣式 */ +.ge-popup-content .ge-folder-search-container { + margin-bottom: 8px; +} + +.ge-popup-content .ge-folder-search-input { + width: 100%; + padding: 6px 8px; + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + background-color: var(--background-primary); + color: var(--text-normal); + font-size: var(--font-ui-medium); +} + +.ge-popup-content .ge-folder-search-input:focus { + outline: none; + border-color: var(--interactive-accent); + box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2); +} + +.ge-popup-content .ge-folder-options-container { + max-height: 300px; + overflow-y: auto; +} + +/* 搜尋 modal popup 樣式 */ +.ge-popup-content .ge-search-container { + margin-top: 0; +} + +.ge-popup-content .ge-search-input-wrapper { + position: relative; +} + +.ge-popup-content .ge-search-input { + width: 100%; + padding: 8px 12px; + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + background-color: var(--background-primary); + color: var(--text-normal); + font-size: var(--font-ui-medium); +} + +.ge-popup-content .ge-search-input:focus { + outline: none; + border-color: var(--interactive-accent); + box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2); +} + +.ge-popup-content .ge-search-clear-button { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + width: 16px; + height: 16px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; + color: var(--text-muted); +} + +.ge-popup-content .ge-search-clear-button:hover { + background-color: var(--background-modifier-hover); + color: var(--text-normal); +} + +.ge-popup-content .ge-search-tag-suggestions { + margin-top: 8px; + max-height: 200px; + overflow-y: auto; +} + +.ge-popup-content .setting-item { + border: none; + padding: 8px 0; +} + +.ge-popup-content .setting-item-name { + font-size: var(--font-ui-medium); + font-weight: 500; } \ No newline at end of file diff --git a/versions.json b/versions.json index 41753c6..31f50b6 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "2.6.6": "1.1.0" + "2.7.0": "1.1.0" } \ No newline at end of file