This commit is contained in:
Devon22 2025-07-18 02:06:44 +08:00
parent ccd430421e
commit ffb5d74752
13 changed files with 648 additions and 193 deletions

View file

@ -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",

4
package-lock.json generated
View file

@ -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",

View file

@ -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": {

View file

@ -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();
}
}

View file

@ -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 [];
}

View file

@ -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();
});
});

View file

@ -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();

View file

@ -0,0 +1,48 @@
import { SuggestModal, TFolder, normalizePath } from 'obsidian';
import GridExplorerPlugin from '../main';
import { GridView } from '../GridView';
export class moveFolderSuggestModal extends SuggestModal<string> {
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<void> {
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);
}
}
}

View file

@ -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();
}

View file

@ -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();
}));
}

View file

@ -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': 'Режим відображення',

View file

@ -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;
}

View file

@ -1,3 +1,3 @@
{
"2.6.6": "1.1.0"
"2.7.0": "1.1.0"
}