devon22_obsidian-gridexplorer/src/GridView.ts
2025-07-22 00:05:49 +08:00

3229 lines
No EOL
154 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { WorkspaceLeaf, ItemView, TFolder, TFile, Menu, Notice, Platform, setIcon, getFrontMatterInfo, FrontMatterCache, normalizePath, setTooltip, MarkdownRenderer, Component } from 'obsidian';
import GridExplorerPlugin from './main';
import { handleKeyDown } from './handleKeyDown';
import { isDocumentFile, isMediaFile, isImageFile, isVideoFile, isAudioFile, sortFiles, ignoredFiles, getFiles, IMAGE_EXTENSIONS, VIDEO_EXTENSIONS, isFolderIgnored } from './fileUtils';
import { FileWatcher } from './fileWatcher';
import { findFirstImageInNote } from './mediaUtils';
import { showFolderSelectionModal } from './modal/folderSelectionModal';
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';
// 定義分隔器狀態
interface DividerState { lastDateString: string; pinDividerAdded: boolean; blankDividerAdded: boolean; }
// 定義檔案渲染參數
interface FileRenderParams {
container: HTMLElement;
observer: IntersectionObserver;
files: TFile[];
dateDividerMode: string;
sortType: string;
shouldShowDateDividers: boolean;
state: DividerState;
}
// 定義網格視圖
export class GridView extends ItemView {
plugin: GridExplorerPlugin;
sourceMode: string = ''; // 模式選擇
sourcePath: string = ''; // 用於資料夾模式的路徑
sortType: string; // 排序模式
folderSortType: string = ''; // 資料夾排序模式
searchQuery: string = ''; // 搜尋關鍵字
searchAllFiles: boolean = true; // 是否搜尋所有筆記
searchMediaFiles: boolean = false; // 是否搜尋媒體檔案
randomNoteIncludeMedia: boolean = false; // 隨機筆記是否包含圖片和影片
selectedItemIndex: number = -1; // 當前選中的項目索引
selectedItems: Set<number> = new Set(); // 存儲多選的項目索引
gridItems: HTMLElement[] = []; // 存儲所有網格項目的引用
hasKeyboardFocus: boolean = false; // 是否有鍵盤焦點
fileWatcher: FileWatcher;
recentSources: string[] = []; // 歷史記錄
minMode: boolean = false; // 最小模式
showIgnoredFolders: boolean = false; // 顯示忽略資料夾
showDateDividers: boolean = false; // 顯示日期分隔器
showNoteTags: boolean = false; // 顯示筆記標籤
pinnedList: string[] = []; // 置頂清單
taskFilter: string = 'uncompleted'; // 任務分類
hideHeaderElements: boolean = false; // 是否隱藏標題列元素(模式名稱和按鈕)
customOptionIndex: number = -1; // 自訂模式選項索引
baseCardLayout: 'horizontal' | 'vertical' = 'horizontal'; // 使用者在設定或 UI 中選擇的基礎卡片樣式(不受資料夾臨時覆蓋影響)
cardLayout: 'horizontal' | 'vertical' = 'horizontal'; // 目前實際使用的卡片樣式(可能被資料夾 metadata 臨時覆蓋)
private renderToken: number = 0; // 用於取消尚未完成之批次排程的遞增令牌
// 筆記檢視相關
private isShowingNote: boolean = false;
private noteViewContainer: HTMLElement | null = null;
constructor(leaf: WorkspaceLeaf, plugin: GridExplorerPlugin) {
super(leaf);
this.plugin = plugin;
this.containerEl.addClass('ge-grid-view-container');
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) {
this.fileWatcher = new FileWatcher(plugin, this);
this.fileWatcher.registerFileWatcher();
}
// 註冊鍵盤事件處理
this.registerDomEvent(document, 'keydown', (event: KeyboardEvent) => {
// 只有當 GridView 是活動視圖時才處理鍵盤事件
if (this.app.workspace.getActiveViewOfType(GridView) === this) {
return handleKeyDown(this, event);
}
});
}
getViewType() {
return 'grid-view';
}
getIcon() {
if (this.sourceMode.startsWith('custom-')) {
return 'puzzle';
} else if (this.sourceMode === 'bookmarks') {
return 'bookmark';
} else if (this.sourceMode === 'search') {
return 'search';
} else if (this.sourceMode === 'backlinks') {
return 'links-coming-in';
} else if (this.sourceMode === 'outgoinglinks') {
return 'links-going-out';
} else if (this.sourceMode === 'all-files') {
return 'book-text';
} else if (this.sourceMode === 'recent-files') {
return 'calendar-days';
} else if (this.sourceMode === 'random-note') {
return 'dice';
} else if (this.sourceMode === 'tasks') {
return 'square-check-big';
} else if (this.sourceMode === 'folder') {
return 'folder';
} else {
return 'grid';
}
}
getDisplayText() {
if (this.sourceMode.startsWith('custom-')) {
const mode = this.plugin.settings.customModes.find(m => m.internalName === this.sourceMode);
return mode ? mode.displayName : t('custom_mode');
} else if (this.sourceMode === '') {
return t('grid_view_title');
} else if (this.sourceMode === 'bookmarks') {
return t('bookmarks_mode');
} else if (this.sourceMode === 'search') {
return t('search_results');
} else if (this.sourceMode === 'backlinks') {
return t('backlinks_mode');
} else if (this.sourceMode === 'outgoinglinks') {
return t('outgoinglinks_mode');
} else if (this.sourceMode === 'all-files') {
return t('all_files_mode');
} else if (this.sourceMode === 'recent-files') {
return t('recent_files_mode');
} else if (this.sourceMode === 'random-note') {
return t('random_note_mode');
} else if (this.sourceMode === 'tasks') {
return t('tasks_mode');
} else if (this.sourceMode === 'folder') {
if (this.sourcePath === '/') {
return t('root');
}
return this.sourcePath;
} else {
return '';
}
}
// 判斷當前Leaf是否被釘選
isPinned(): boolean {
return (this.leaf as any)?.pinned ?? false;
}
// 將來源加入歷史記錄LRU 去重)
// 1. 若已有相同紀錄先移除,確保唯一
// 2. 插入到陣列開頭,代表最新使用
// 3. 超過上限時裁切
private pushHistory(mode: string, path: string | null) {
const sanitizedPath = path ?? '';
const key = JSON.stringify({ mode, path: sanitizedPath });
const existingIndex = this.recentSources.indexOf(key);
if (existingIndex !== -1) {
this.recentSources.splice(existingIndex, 1);
}
this.recentSources.unshift(key);
const limit = 15;
if (this.recentSources.length > limit) {
this.recentSources.length = limit;
}
}
// 當 resetScroll 為 true 時,會將捲動位置重置到最頂部
async setSource(mode: string, path = '', resetScroll = false, recordHistory = true) {
// 如果新的狀態與當前狀態相同,則不進行任何操作
if (this.sourceMode === mode && this.sourcePath === path) {
return;
}
// 記錄之前的狀態到歷史記錄中(如果有)
if (this.sourceMode && recordHistory) {
this.pushHistory(this.sourceMode, this.sourcePath);
}
// 全域搜尋時切換路徑則清空搜尋
if (this.searchQuery !== '' && this.searchAllFiles) {
this.searchQuery = '';
}
this.folderSortType = '';
this.pinnedList = [];
if (mode === 'folder') {
// 檢查是否有與資料夾同名的 md 檔案
const folderName = path.split('/').pop() || '';
const mdFilePath = `${path}/${folderName}.md`;
const mdFile = this.app.vault.getAbstractFileByPath(mdFilePath);
let tempLayout: 'horizontal' | 'vertical' = this.baseCardLayout;
if (mdFile instanceof TFile) {
const metadata = this.app.metadataCache.getFileCache(mdFile)?.frontmatter;
this.folderSortType = metadata?.sort;
if (metadata?.cardLayout === 'horizontal' || metadata?.cardLayout === 'vertical') {
tempLayout = metadata.cardLayout as 'horizontal' | 'vertical';
}
}
this.cardLayout = tempLayout;
} else {
// 非資料夾來源時,回復基礎卡片排列
this.cardLayout = this.baseCardLayout;
}
if(mode.startsWith('custom-')) {
this.customOptionIndex = -1; // 切換自訂模式時重設選項索引
this.folderSortType = 'none';
}
if(mode !== '') this.sourceMode = mode;
if(path !== '') this.sourcePath = path;
if(this.sourceMode === '') this.sourceMode = 'folder';
if(this.sourcePath === '') this.sourcePath = '/';
// 非資料夾模式時,強制路徑為根目錄
if(this.sourceMode !== 'folder') {
this.sourcePath = '/';
}
// 通知 Obsidian 保存視圖狀態
this.app.workspace.requestSaveLayout();
this.render(resetScroll);
}
async render(resetScroll = false) {
// 儲存當前捲動位置
const scrollContainer = this.containerEl.children[1] as HTMLElement;
const scrollTop = resetScroll ? 0 : (scrollContainer ? scrollContainer.scrollTop : 0);
// 保存選中項目的檔案路徑(如果有)
let selectedFilePath: string | null = null;
if (this.selectedItemIndex >= 0 && this.selectedItemIndex < this.gridItems.length) {
const selectedItem = this.gridItems[this.selectedItemIndex];
selectedFilePath = selectedItem.dataset.filePath || null;
}
// 清空整個容器
this.containerEl.empty();
// 創建頂部按鈕區域
const headerButtonsDiv = this.containerEl.createDiv('ge-header-buttons');
// 為頂部按鈕區域添加點擊事件,點擊後網格容器捲動到最頂部
headerButtonsDiv.addEventListener('click', (event: MouseEvent) => {
// 只有當點擊的是頂部按鈕區域本身(而不是其中的按鈕)時才觸發捲動
if (event.target === headerButtonsDiv) {
event.preventDefault();
// 取得網格容器
const gridContainer = this.containerEl.querySelector('.ge-grid-container');
if (gridContainer) {
gridContainer.scrollTo({
top: 0,
behavior: 'smooth'
});
}
}
});
// 添加回上一步按鈕
const backButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('back') } });
setIcon(backButton, 'arrow-left');
backButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
if (this.searchQuery !== '') {
this.searchQuery = '';
this.app.workspace.requestSaveLayout();
this.render();
return;
}
// 如果有歷史記錄
if (this.recentSources.length > 0) {
// 取得最近一筆歷史記錄
const lastSource = JSON.parse(this.recentSources[0]);
this.recentSources.shift(); // 從歷史記錄中移除
// 設定來源(不記錄到歷史)
this.setSource(
lastSource.mode,
lastSource.path || '',
true, // 重設捲動位置
false // 不記錄到歷史
);
}
});
// 添加右鍵選單支援
backButton.addEventListener('contextmenu', (event) => {
// 只有在有歷史記錄時才顯示右鍵選單
if (this.recentSources.length > 0) {
event.preventDefault();
const menu = new Menu();
// 添加歷史記錄
this.recentSources.forEach((sourceInfoStr, index) => {
try {
const sourceInfo = JSON.parse(sourceInfoStr);
const { mode, path } = sourceInfo;
// 根據模式顯示圖示和文字
let displayText = '';
let icon = '';
switch (mode) {
case 'folder':
displayText = path || '/';
icon = 'folder';
break;
case 'bookmarks':
displayText = t('bookmarks_mode');
icon = 'bookmark';
break;
case 'search':
displayText = t('search_results');
icon = 'search';
break;
case 'backlinks':
displayText = t('backlinks_mode');
icon = 'links-coming-in';
break;
case 'outgoinglinks':
displayText = t('outgoinglinks_mode');
icon = 'links-going-out';
break;
case 'all-files':
displayText = t('all_files_mode');
icon = 'book-text';
break;
case 'recent-files':
displayText = t('recent_files_mode');
icon = 'calendar-days';
break;
case 'random-note':
displayText = t('random_note_mode');
icon = 'dice';
break;
case 'tasks':
displayText = t('tasks_mode');
icon = 'square-check-big';
break;
default:
if (mode.startsWith('custom-')) {
const customMode = this.plugin.settings.customModes.find(m => m.internalName === mode);
displayText = customMode ? customMode.displayName : t('custom_mode');
icon = 'puzzle';
} else {
displayText = mode;
icon = 'grid';
}
}
// 添加歷史記錄到選單
menu.addItem((item) => {
item
.setTitle(`${displayText}`)
.setIcon(`${icon}`)
.onClick(() => {
// 找出當前點擊的紀錄索引
const clickedIndex = this.recentSources.findIndex(source => {
const parsed = JSON.parse(source);
return parsed.mode === mode && parsed.path === path;
});
// 如果找到點擊的紀錄,清除它之上的紀錄
if (clickedIndex !== -1) {
this.recentSources = this.recentSources.slice(clickedIndex + 1);
}
this.setSource(mode, path, true, false);
});
});
} catch (error) {
console.error('Failed to parse source info:', error);
}
});
// 顯示歷史選單
menu.showAtMouseEvent(event);
}
});
// 添加新增筆記按鈕
const newNoteButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('new_note') } });
setIcon(newNoteButton, 'square-pen');
newNoteButton.addEventListener('click', (event) => {
event.preventDefault();
const menu = new Menu();
// 新增筆記
menu.addItem((item) => {
item
.setTitle(t('new_note'))
.setIcon('square-pen')
.onClick(async () => {
let newFileName = `${t('untitled')}.md`;
let newFilePath = !this.sourcePath || this.sourcePath === '/' ? newFileName : `${this.sourcePath}/${newFileName}`;
// 檢查檔案是否已存在,如果存在則遞增編號
let counter = 1;
while (this.app.vault.getAbstractFileByPath(newFilePath)) {
newFileName = `${t('untitled')} ${counter}.md`;
newFilePath = !this.sourcePath || this.sourcePath === '/' ? newFileName : `${this.sourcePath}/${newFileName}`;
counter++;
}
try {
// 建立新筆記
const newFile = await this.app.vault.create(newFilePath, '');
// 開啟新筆記
await this.app.workspace.getLeaf().openFile(newFile);
} catch (error) {
console.error('An error occurred while creating a new note:', error);
}
});
});
// 新增資料夾
menu.addItem((item) => {
item.setTitle(t('new_folder'))
.setIcon('folder')
.onClick(async () => {
let newFolderName = `${t('untitled')}`;
let newFolderPath = !this.sourcePath || this.sourcePath === '/' ? newFolderName : `${this.sourcePath}/${newFolderName}`;
// 檢查資料夾是否已存在,如果存在則遞增編號
let counter = 1;
while (this.app.vault.getAbstractFileByPath(newFolderPath)) {
newFolderName = `${t('untitled')} ${counter}`;
newFolderPath = !this.sourcePath || this.sourcePath === '/' ? newFolderName : `${this.sourcePath}/${newFolderName}`;
counter++;
}
try {
// 建立新資料夾
await this.app.vault.createFolder(newFolderPath);
this.render(false);
} catch (error) {
console.error('An error occurred while creating a new folder:', error);
}
});
});
// 新增畫布
menu.addItem((item) => {
item.setTitle(t('new_canvas'))
.setIcon('layout-dashboard')
.onClick(async () => {
let newFileName = `${t('untitled')}.canvas`;
let newFilePath = !this.sourcePath || this.sourcePath === '/' ? newFileName : `${this.sourcePath}/${newFileName}`;
// 檢查檔案是否已存在,如果存在則遞增編號
let counter = 1;
while (this.app.vault.getAbstractFileByPath(newFilePath)) {
newFileName = `${t('untitled')} ${counter}.canvas`;
newFilePath = !this.sourcePath || this.sourcePath === '/' ? newFileName : `${this.sourcePath}/${newFileName}`;
counter++;
}
try {
// 建立新筆記
const newFile = await this.app.vault.create(newFilePath, '');
// 開啟新筆記
await this.app.workspace.getLeaf().openFile(newFile);
} catch (error) {
console.error('An error occurred while creating a new canvas:', error);
}
});
});
menu.showAtMouseEvent(event);
});
// 添加重新選擇資料夾按鈕
const reselectButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('reselect') } });
reselectButton.addEventListener('click', () => {
showFolderSelectionModal(this.app, this.plugin, this, reselectButton);
});
setIcon(reselectButton, "grid");
// 添加重新整理按鈕
const refreshButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('refresh') } });
refreshButton.addEventListener('click', () => {
if (this.sortType === 'random') {
this.clearSelection();
}
this.render();
});
setIcon(refreshButton, 'refresh-ccw');
// 添加搜尋按鈕
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, '', searchButton);
});
// 如果有搜尋關鍵字,顯示搜尋文字和取消按鈕
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, searchText);
});
// 創建取消按鈕
const clearButton = searchTextContainer.createDiv('ge-clear-button');
setIcon(clearButton, 'x');
clearButton.addEventListener('click', (e) => {
e.stopPropagation(); // 防止觸發搜尋文字的點擊事件
this.searchQuery = '';
this.clearSelection();
this.app.workspace.requestSaveLayout();
this.render();
});
}
// 添加更多選項按鈕
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.app.workspace.requestSaveLayout();
this.render();
});
});
// 最小化模式選項
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');
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');
// 左側:模式名稱
const modenameContainer = modeHeaderContainer.createDiv('ge-modename-content');
// 右側:排序按鈕
const rightActions = modeHeaderContainer.createDiv('ge-right-actions');
// 添加排序按鈕
if (this.sourceMode !== 'bookmarks' &&
this.sourceMode !== 'recent-files' &&
this.sourceMode !== 'random-note') {
const sortButton = rightActions.createEl('a', {
cls: 'ge-sort-button',
attr: {
'aria-label': t('sorting'),
'href': '#'
}
});
setIcon(sortButton, 'arrow-up-narrow-wide');
sortButton.addEventListener('click', (evt) => {
evt.preventDefault();
evt.stopPropagation();
const menu = new Menu();
const sortOptions = [
{ value: 'name-asc', label: t('sort_name_asc'), icon: 'a-arrow-up' },
{ value: 'name-desc', label: t('sort_name_desc'), icon: 'a-arrow-down' },
{ value: 'mtime-desc', label: t('sort_mtime_desc'), icon: 'clock' },
{ value: 'mtime-asc', label: t('sort_mtime_asc'), icon: 'clock' },
{ value: 'ctime-desc', label: t('sort_ctime_desc'), icon: 'calendar' },
{ value: 'ctime-asc', label: t('sort_ctime_asc'), icon: 'calendar' },
{ value: 'random', label: t('sort_random'), icon: 'dice' },
];
sortOptions.forEach(option => {
menu.addItem((item) => {
item
.setTitle(option.label)
.setIcon(option.icon)
.setChecked((this.folderSortType || this.sortType) === option.value)
.onClick(() => {
this.sortType = option.value;
this.folderSortType = '';
this.app.workspace.requestSaveLayout();
this.render();
});
});
});
menu.showAtMouseEvent(evt);
});
}
// 為區域添加點擊事件,點擊後網格容器捲動到最頂部
modenameContainer.addEventListener('click', (event: MouseEvent) => {
// 只有當點擊的是頂部按鈕區域本身(而不是其中的按鈕)時才觸發捲動
if (event.target === modenameContainer) {
event.preventDefault();
// 取得網格容器
const gridContainer = this.containerEl.querySelector('.ge-grid-container');
if (gridContainer) {
gridContainer.scrollTo({
top: 0,
behavior: 'smooth'
});
}
}
});
// 顯示目前資料夾及完整路徑
if (this.sourceMode === 'folder' &&
(this.searchQuery === '' || (this.searchQuery && !this.searchAllFiles)) &&
this.sourcePath !== '/') {
const pathParts = this.sourcePath.split('/').filter(part => part.trim() !== '');
// 建立路徑項目的資料結構
interface PathItem {
name: string;
path: string;
isLast: boolean;
}
const paths: PathItem[] = [];
let pathAccumulator = '';
// 添加根目錄
paths.push({
name: t('root'),
path: '/',
isLast: pathParts.length === 0
});
// 建立所有路徑
pathParts.forEach((part, index) => {
pathAccumulator = pathAccumulator ? `${pathAccumulator}/${part}` : part;
paths.push({
name: part,
path: pathAccumulator,
isLast: index === pathParts.length - 1
});
});
// 創建一個容器來測量寬度
const pathContainer = modenameContainer.createDiv({cls: 'ge-path-container'});
const customFolderIcon = this.plugin.settings.customFolderIcon;
// 計算可用寬度
const pathElements: HTMLElement[] = [];
// 建立所有路徑元素
paths.forEach((path, index) => {
const isLast = index === paths.length - 1;
let pathEl;
if (isLast) {
// 當前資料夾使用 span 元素
pathEl = modenameContainer.createEl('a', {
text: `${customFolderIcon} ${path.name}`.trim(),
cls: 'ge-current-folder'
});
} else {
// 上層資料夾使用 a 元素(可點擊)
pathEl = modenameContainer.createEl('a', {
text: path.name,
cls: 'ge-parent-folder-link'
});
}
setTooltip(pathEl, path.name);
pathElements.push(pathEl);
});
// 添加路徑元素
for (let i = 0; i < pathElements.length; i++) {
const el = pathElements[i];
pathContainer.appendChild(el);
// 為路徑元素添加點擊事件
if (el.className === 'ge-parent-folder-link') {
const pathIndex = i; // 直接使用索引,因為不再有分隔符
if (pathIndex < paths.length) {
const path = paths[pathIndex];
el.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
this.setSource('folder', path.path, true);
this.clearSelection();
});
// 為路徑元素添加右鍵選單,顯示路徑層級和同層級目錄
el.addEventListener('contextmenu', async (event) => {
event.preventDefault();
event.stopPropagation();
const menu = new Menu();
// 1. 添加當前點擊的目錄
menu.addItem((item) => {
item.setTitle(path.name)
.setIcon('folder')
.onClick(() => {
this.setSource('folder', path.path, true);
this.clearSelection();
});
});
// 2. 獲取並添加當前目錄下的所有子目錄
const currentFolder = this.app.vault.getAbstractFileByPath(path.path);
if (currentFolder && currentFolder instanceof TFolder) {
const subFolders = currentFolder.children
.filter(child => {
// 如果不是資料夾,則不顯示
if (!(child instanceof TFolder)) return false;
// 使用 isFolderIgnored 函數檢查是否應該忽略此資料夾
return !isFolderIgnored(
child,
this.plugin.settings.ignoredFolders,
this.plugin.settings.ignoredFolderPatterns,
this.showIgnoredFolders
);
})
.sort((a, b) => a.name.localeCompare(b.name));
if (subFolders.length > 0) {
menu.addSeparator();
menu.addItem((item) =>
item.setTitle(t('sub_folders'))
.setIcon('folder-symlink')
.setDisabled(true)
);
subFolders.forEach(folder => {
menu.addItem((item) => {
item.setTitle(folder.name)
.setIcon('folder')
.onClick(() => {
this.setSource('folder', folder.path, true);
this.clearSelection();
});
});
});
}
}
// 3. 添加上層路徑
if (pathIndex > 0) {
menu.addSeparator();
menu.addItem((item) =>
item.setTitle(t('parent_folders'))
.setIcon('arrow-up')
.setDisabled(true)
);
for (let i = pathIndex - 1; i >= 0; i--) {
const p = paths[i];
menu.addItem((item) => {
item.setTitle(p.name)
.setIcon(p.path === '/' ? 'folder-root' : 'folder')
.onClick(() => {
this.setSource('folder', p.path, true);
this.clearSelection();
});
});
}
}
menu.showAtMouseEvent(event);
});
// 為最後一個路徑以外的路徑添加拖曳功能
if (!path.isLast && Platform.isDesktop) {
// 為路徑元素添加拖曳目標功能
el.addEventListener('dragover', (event) => {
event.preventDefault();
event.dataTransfer!.dropEffect = 'move';
el.addClass('ge-dragover');
});
el.addEventListener('dragleave', () => {
el.removeClass('ge-dragover');
});
el.addEventListener('drop', async (event) => {
event.preventDefault();
el.removeClass('ge-dragover');
if (!path.path) return;
const folder = this.app.vault.getAbstractFileByPath(path.path);
if (!(folder instanceof TFolder)) return;
const filesData = event.dataTransfer?.getData('application/obsidian-grid-explorer-files');
if (filesData) {
try {
const filePaths = JSON.parse(filesData);
for (const filePath of filePaths) {
const file = this.app.vault.getAbstractFileByPath(filePath);
if (file instanceof TFile) {
const newPath = normalizePath(`${path.path}/${file.name}`);
await this.app.fileManager.renameFile(file, newPath);
}
}
} catch (error) {
console.error('An error occurred while moving multiple files to folder:', error);
}
return;
}
const filePath = event.dataTransfer?.getData('text/plain');
if (!filePath) return;
const cleanedFilePath = filePath.replace(/!?\[\[(.*?)\]\]/, '$1');
const file = this.app.vault.getAbstractFileByPath(cleanedFilePath);
if (file instanceof TFile) {
try {
const newPath = normalizePath(`${path.path}/${file.name}`);
await this.app.fileManager.renameFile(file, newPath);
this.render();
} catch (error) {
console.error('An error occurred while moving the file to folder:', error);
}
}
});
}
}
}
if (el.className === 'ge-current-folder') {
// 將選單邏輯抽出,以同時支援 click 與 contextmenu
const showFolderMenu = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
const folder = this.app.vault.getAbstractFileByPath(this.sourcePath);
const folderName = this.sourcePath.split('/').pop() || '';
const notePath = `${this.sourcePath}/${folderName}.md`;
const noteFile = this.app.vault.getAbstractFileByPath(notePath);
const menu = new Menu();
if (noteFile instanceof TFile) {
// 打開資料夾筆記選項
menu.addItem((item) => {
item
.setTitle(t('open_folder_note'))
.setIcon('panel-left-open')
.onClick(() => {
this.app.workspace.getLeaf().openFile(noteFile);
});
});
// 編輯資料夾筆記設定選項
menu.addItem((item) => {
item
.setTitle(t('edit_folder_note_settings'))
.setIcon('settings-2')
.onClick(() => {
if (folder instanceof TFolder) {
showFolderNoteSettingsModal(this.app, this.plugin, folder, this);
}
});
});
// 刪除資料夾筆記選項
menu.addItem((item) => {
item
.setTitle(t('delete_folder_note'))
.setIcon('folder-x')
.onClick(() => {
this.app.fileManager.trashFile(noteFile as TFile);
});
});
} else {
// 建立 Folder note
menu.addItem((item) => {
item
.setTitle(t('create_folder_note'))
.setIcon('file-cog')
.onClick(() => {
if (folder instanceof TFolder) {
showFolderNoteSettingsModal(this.app, this.plugin, folder, this);
}
});
});
}
menu.showAtMouseEvent(event);
};
// 左鍵與右鍵都呼叫相同的選單
el.addEventListener('click', showFolderMenu);
el.addEventListener('contextmenu', showFolderMenu);
}
}
} else if (!(this.searchQuery !== '' && this.searchAllFiles)) {
// 顯示目前模式名稱
let modeName = '';
let modeIcon = '';
// 根據目前模式設定對應的圖示和名稱
switch (this.sourceMode) {
case 'bookmarks':
modeIcon = '📑';
modeName = t('bookmarks_mode');
break;
case 'search':
modeIcon = '🔍';
modeName = t('search_results');
const searchLeaf = (this.app as any).workspace.getLeavesOfType('search')[0];
if (searchLeaf) {
const searchView: any = searchLeaf.view;
const searchInputEl: HTMLInputElement | null = searchView.searchComponent ? searchView.searchComponent.inputEl : null;
const currentQuery = searchInputEl?.value.trim();
if (currentQuery && currentQuery.length > 0) {
modeName += `: ${currentQuery}`;
} else if (this.searchQuery) {
modeName += `: ${this.searchQuery}`;
}
}
break;
case 'backlinks':
modeIcon = '🔗';
modeName = t('backlinks_mode');
const activeFile = this.app.workspace.getActiveFile();
if (activeFile) {
modeName += `: ${activeFile.basename}`;
}
break;
case 'outgoinglinks':
modeIcon = '🔗';
modeName = t('outgoinglinks_mode');
const currentFile = this.app.workspace.getActiveFile();
if (currentFile) {
modeName += `: ${currentFile.basename}`;
}
break;
case 'recent-files':
modeIcon = '📅';
modeName = t('recent_files_mode');
break;
case 'all-files':
modeIcon = '📔';
modeName = t('all_files_mode');
break;
case 'random-note':
modeIcon = '🎲';
modeName = t('random_note_mode');
break;
case 'tasks':
modeIcon = '☑️';
modeName = t('tasks_mode');
break;
default:
if (this.sourceMode.startsWith('custom-')) {
const mode = this.plugin.settings.customModes.find(m => m.internalName === this.sourceMode);
modeIcon = mode ? mode.icon : '🧩';
modeName = mode ? mode.displayName : t('custom_mode');
} else { // folder mode
modeIcon = '📁';
if (this.sourcePath && this.sourcePath !== '/') {
modeName = this.sourcePath.split('/').pop() || this.sourcePath;
} else {
modeName = t('root');
}
}
}
// 顯示模式名稱 (若為自訂模式則提供點擊選單以快速切換)
let modeTitleEl: HTMLElement;
if (this.sourceMode.startsWith('custom-')) {
// 使用可點擊的 <a> 元素
modeTitleEl = modenameContainer.createEl('a', {
text: `${modeIcon} ${modeName}`.trim(),
cls: 'ge-mode-title'
});
// 點擊時顯示所有自訂模式選單
modeTitleEl.addEventListener('click', (evt) => {
const menu = new Menu();
this.plugin.settings.customModes
.filter(m => m.enabled ?? true) // 僅顯示啟用的自訂模式
.forEach((m) => {
menu.addItem(item => {
item.setTitle(`${m.icon || '🧩'} ${m.displayName}`)
.setChecked(m.internalName === this.sourceMode)
.onClick(() => {
// 切換至選取的自訂模式並重新渲染
this.setSource(m.internalName, '', true);
});
});
});
menu.showAtMouseEvent(evt);
});
} else {
// 其他模式維持原本的 span
modeTitleEl = modenameContainer.createEl('span', {
text: `${modeIcon} ${modeName}`.trim(),
cls: 'ge-mode-title'
});
}
switch (this.sourceMode) {
case 'tasks':
const taskFilterSpan = modenameContainer.createEl('a', { text: t(`${this.taskFilter}`), cls: 'ge-sub-option' });
taskFilterSpan.addEventListener('click', (evt) => {
const menu = new Menu();
menu.addItem((item) => {
item.setTitle(t('uncompleted'))
.setChecked(this.taskFilter === 'uncompleted')
.setIcon('square')
.onClick(() => {
this.taskFilter = 'uncompleted';
this.render();
});
});
menu.addItem((item) => {
item.setTitle(t('completed'))
.setChecked(this.taskFilter === 'completed')
.setIcon('square-check-big')
.onClick(() => {
this.taskFilter = 'completed';
this.render();
});
});
menu.addItem((item) => {
item.setTitle(t('all'))
.setChecked(this.taskFilter === 'all')
.setIcon('square-asterisk')
.onClick(() => {
this.taskFilter = 'all';
this.render();
});
});
menu.addSeparator();
menu.showAtMouseEvent(evt);
});
break;
default:
if (this.sourceMode.startsWith('custom-')) {
// 把 modenameContainer 加上所有自訂模式選項的選單
// 取得當前自訂模式
const mode = this.plugin.settings.customModes.find(m => m.internalName === this.sourceMode);
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}`;
}
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) => {
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);
});
} 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;
}
} else if (this.searchQuery !== '' && this.searchAllFiles) {
// 顯示全域搜尋名稱
modenameContainer.createEl('span', {
text: `🔍 ${t('global_search')}`,
cls: 'ge-mode-title'
});
}
// 創建內容區域
const contentEl = this.containerEl.createDiv('view-content');
// 取得置頂清單
if (this.sourceMode === 'folder' && this.sourcePath !== '/') {
this.pinnedList = [];
const folderPath = this.sourcePath;
if (!folderPath || folderPath === '/') return;
const folderName = folderPath.split('/').pop() || '';
const notePath = `${folderPath}/${folderName}.md`;
const noteFile = this.app.vault.getAbstractFileByPath(notePath);
if (noteFile instanceof TFile) {
const metadata = this.app.metadataCache.getFileCache(noteFile)?.frontmatter;
if (metadata) {
if (Array.isArray(metadata['pinned'])) {
if (this.plugin.settings.folderNoteDisplaySettings === 'pinned') {
// 先過濾掉所有重複的資料夾筆記
this.pinnedList = metadata['pinned'].filter((name: string) => name !== `${folderName}.md`);
// 將資料夾筆記添加到最前面
this.pinnedList.unshift(`${folderName}.md`);
} else {
this.pinnedList = metadata['pinned'];
}
} else if (this.plugin.settings.folderNoteDisplaySettings === 'pinned') {
// 如果沒有置頂清單,則建立一個僅包含資料夾筆記的清單
this.pinnedList = [`${folderName}.md`];
}
}
}
};
// 重新渲染內容
await this.grid_render();
(this.leaf as any).updateHeader();
// 恢復捲動位置
if (scrollContainer && !resetScroll) {
contentEl.scrollTop = scrollTop;
}
// 如果有之前選中的檔案路徑,嘗試恢復選中狀態
if (selectedFilePath && this.hasKeyboardFocus) {
const newIndex = this.gridItems.findIndex(item => item.dataset.filePath === selectedFilePath);
if (newIndex >= 0) {
this.selectItem(newIndex);
}
}
}
async grid_render() {
const container = this.containerEl.querySelector('.view-content') as HTMLElement;
container.empty();
container.addClass('ge-grid-container');
// 隱藏頂部元素
const displayValue = this.hideHeaderElements ? 'none' : 'flex';
const headerButtons = this.containerEl.querySelector('.ge-header-buttons') as HTMLElement;
const modeHeaderContainer = this.containerEl.querySelector('.ge-mode-header-container') as HTMLElement;
if (headerButtons) headerButtons.style.display = displayValue;
if (modeHeaderContainer) modeHeaderContainer.style.display = displayValue;
// 根據設定決定是否啟用卡片模式
if (this.cardLayout === 'vertical') {
container.addClass('ge-vertical-card');
} else {
container.removeClass('ge-vertical-card');
}
// 添加點擊空白處取消選中的事件處理器
container.addEventListener('click', (event) => {
// 只有當點擊的是容器本身,而不是其子元素時才清除選中
if (event.target === container) {
this.clearSelection();
this.hasKeyboardFocus = false;
}
});
// 設定網格項目寬度和高度等設定
const settings = this.plugin.settings;
const gridItemWidth = this.cardLayout === 'vertical' ? settings.verticalGridItemWidth : settings.gridItemWidth;
const gridItemHeight = this.cardLayout === 'vertical' ? settings.verticalGridItemHeight : settings.gridItemHeight;
const imageAreaWidth = settings.imageAreaWidth;
const imageAreaHeight = this.cardLayout === 'vertical' ? settings.verticalImageAreaHeight : settings.imageAreaHeight;
container.style.setProperty('--grid-item-width', gridItemWidth + 'px');
if (gridItemHeight === 0 || this.minMode) {
container.style.setProperty('--grid-item-height', '100%');
} else {
container.style.setProperty('--grid-item-height', gridItemHeight + 'px');
}
container.style.setProperty('--image-area-width', imageAreaWidth + 'px');
container.style.setProperty('--image-area-height', imageAreaHeight + 'px');
container.style.setProperty('--title-font-size', settings.titleFontSize + 'em');
// 依圖片位置設定切換樣式類別
if (this.cardLayout === 'vertical') {
if (settings.verticalCardImagePosition === 'top') {
container.addClass('ge-image-top');
container.removeClass('ge-image-bottom');
} else {
container.addClass('ge-image-bottom');
container.removeClass('ge-image-top');
}
}
// 定義所有可能的模式(不包括 custom-
const modeClasses = [
'bookmarks',
'search',
'backlinks',
'outgoinglinks',
'all-files',
'recent-files',
'random-note',
'tasks',
'folder'
];
// 先移除所有模式相關的 class
this.containerEl.removeClass('ge-mode-custom'); // 特別處理 custom 類別
modeClasses.forEach(mode => {
this.containerEl.removeClass(`ge-mode-${mode}`);
});
// 添加當前模式的 class
if (this.sourceMode.startsWith('custom-')) {
this.containerEl.addClass('ge-mode-custom');
} else if (modeClasses.includes(this.sourceMode)) {
this.containerEl.addClass(`ge-mode-${this.sourceMode}`);
}
// 重置網格項目數組
this.gridItems = [];
// 如果是書籤模式且書籤插件未啟用,顯示提示
if (this.sourceMode === 'bookmarks' && !(this.app as any).internalPlugins.plugins.bookmarks?.enabled) {
new Notice(t('bookmarks_plugin_disabled'));
return;
}
// 如果是反向連結模式,但沒有活動中的檔案
if (this.sourceMode === 'backlinks' && !this.app.workspace.getActiveFile()) {
const noFilesDiv = container.createDiv('ge-no-files');
noFilesDiv.setText(t('no_backlinks'));
if (this.plugin.statusBarItem) {
this.plugin.statusBarItem.setText('');
}
return;
}
// 如果是資料夾模式,先顯示所有子資料夾
if (this.sourceMode === 'folder' && this.searchQuery === '') {
const currentFolder = this.app.vault.getAbstractFileByPath(this.sourcePath || '/');
if (currentFolder instanceof TFolder) {
// 為網格容器添加拖曳目標功能(當前資料夾)
if(Platform.isDesktop) {
container.addEventListener('dragover', (event) => {
// 如果拖曳目標是資料夾項目,則不處理
if ((event.target as HTMLElement).closest('.ge-folder-item')) {
return;
}
// 防止預設行為以允許放置
event.preventDefault();
// 設定拖曳效果為移動
(event as any).dataTransfer!.dropEffect = 'move';
// 顯示可放置的視覺提示
container.addClass('ge-dragover');
}, true); // 使用捕獲階段
container.addEventListener('dragleave', (event) => {
// 如果移入的是子元素,則不處理
if (container.contains(event.relatedTarget as Node)) {
return;
}
// 移除視覺提示
container.removeClass('ge-dragover');
});
container.addEventListener('drop', async (event) => {
// 如果拖曳目標是資料夾項目,則不處理
if ((event.target as HTMLElement).closest('.ge-folder-item')) {
return;
}
// 防止預設行為
event.preventDefault();
// 移除視覺提示
container.removeClass('ge-dragover');
// 獲取拖曳的檔案路徑列表
const filesDataString = (event as any).dataTransfer?.getData('application/obsidian-grid-explorer-files');
if (filesDataString) {
try {
// 解析檔案路徑列表
const filePaths = JSON.parse(filesDataString);
// 獲取當前資料夾路徑
const folderPath = currentFolder.path;
if (!folderPath) return;
// 移動檔案
for (const path of filePaths) {
const file = this.app.vault.getAbstractFileByPath(path);
if (file instanceof TFile) {
try {
// 計算新的檔案路徑
const newPath = normalizePath(`${folderPath}/${file.name}`);
// 如果來源路徑和目標路徑相同,則跳過
if (path === newPath) {
continue;
}
// 移動檔案
await this.app.fileManager.renameFile(file, newPath);
} catch (error) {
console.error(`An error occurred while moving the file ${file.path}:`, error);
}
}
}
return;
} catch (error) {
console.error('Error parsing dragged files data:', error);
}
}
// 如果沒有檔案路徑列表,則使用檔案路徑
const filePath = (event as any).dataTransfer?.getData('text/plain');
if (!filePath) return;
const cleanedFilePath = filePath.replace(/!?\[\[(.*?)\]\]/, '$1');
// 獲取檔案和資料夾物件
const file = this.app.vault.getAbstractFileByPath(cleanedFilePath);
if (file instanceof TFile) {
try {
// 計算新的檔案路徑
const newPath = normalizePath(`${currentFolder.path}/${file.name}`);
// 如果來源路徑和目標路徑相同,則不執行移動
if (file.path !== newPath) {
// 移動檔案
await this.app.fileManager.renameFile(file, newPath);
}
} catch (error) {
console.error('An error occurred while moving the file:', error);
}
}
});
}
// 顯示子資料夾
const subfolders = currentFolder.children
.filter(child => {
// 如果不是資料夾,則不顯示
if (!(child instanceof TFolder)) return false;
// 使用 isFolderIgnored 函數檢查是否應該忽略此資料夾
return !isFolderIgnored(
child,
this.plugin.settings.ignoredFolders,
this.plugin.settings.ignoredFolderPatterns,
this.showIgnoredFolders
);
})
.sort((a, b) => a.name.localeCompare(b.name));
for (const folder of subfolders) {
const folderEl = container.createDiv('ge-grid-item ge-folder-item');
this.gridItems.push(folderEl); // 添加到網格項目數組
// 設置資料夾路徑屬性,用於拖曳功能
folderEl.dataset.folderPath = folder.path;
const contentArea = folderEl.createDiv('ge-content-area');
const titleContainer = contentArea.createDiv('ge-title-container');
const customFolderIcon = this.plugin.settings.customFolderIcon;
titleContainer.createEl('span', { cls: 'ge-title', text: `${customFolderIcon} ${folder.name}`.trim() });
setTooltip(folderEl, folder.name,{ placement: this.cardLayout === 'vertical' ? 'bottom' : 'right' });
// 檢查同名筆記是否存在
const notePath = `${folder.path}/${folder.name}.md`;
const noteFile = this.app.vault.getAbstractFileByPath(notePath);
if (noteFile instanceof TFile) {
// 使用 span 代替 button只顯示圖示
const noteIcon = titleContainer.createEl('span', {
cls: 'ge-foldernote-button'
});
setIcon(noteIcon, 'panel-left-open');
// 點擊圖示時開啟同名筆記
noteIcon.addEventListener('click', (e) => {
e.stopPropagation(); // 防止觸發資料夾的點擊事件
this.app.workspace.getLeaf().openFile(noteFile);
});
// 根據同名筆記設置背景色
const metadata = this.app.metadataCache.getFileCache(noteFile)?.frontmatter;
const colorValue = metadata?.color;
if (colorValue) {
// 依顏色名稱加入對應的樣式類別
folderEl.addClass(`ge-folder-color-${colorValue}`);
}
const iconValue = metadata?.icon;
if (iconValue) {
// 修改原本的title文字
const title = folderEl.querySelector('.ge-title');
if (title) {
title.textContent = `${iconValue} ${folder.name}`;
}
}
}
// 點擊時進入子資料夾
folderEl.addEventListener('click', (event) => {
if (event.ctrlKey || event.metaKey) {
event.preventDefault();
event.stopPropagation();
this.openFolderInNewView(folder.path);
} else {
this.setSource('folder', folder.path, true);
this.clearSelection();
}
});
// 添加右鍵選單
folderEl.addEventListener('contextmenu', (event) => {
event.preventDefault();
const menu = new Menu();
//在新網格視圖開啟
menu.addItem((item) => {
item
.setTitle(t('open_in_new_grid_view'))
.setIcon('grid')
.onClick(() => {
this.openFolderInNewView(folder.path);
});
});
menu.addSeparator();
// 檢查同名筆記是否存在
const notePath = `${folder.path}/${folder.name}.md`;
let noteFile = this.app.vault.getAbstractFileByPath(notePath);
if (noteFile instanceof TFile) {
//打開資料夾筆記
menu.addItem((item) => {
item
.setTitle(t('open_folder_note'))
.setIcon('panel-left-open')
.onClick(() => {
this.app.workspace.getLeaf().openFile(noteFile);
});
});
//編輯資料夾筆記設定
menu.addItem((item) => {
item
.setTitle(t('edit_folder_note_settings'))
.setIcon('settings-2')
.onClick(() => {
if (folder instanceof TFolder) {
showFolderNoteSettingsModal(this.app, this.plugin, folder, this);
}
});
});
//刪除資料夾筆記
menu.addItem((item) => {
item
.setTitle(t('delete_folder_note'))
.setIcon('folder-x')
.onClick(() => {
this.app.fileManager.trashFile(noteFile as TFile);
});
});
} else {
//建立Folder note
menu.addItem((item) => {
item
.setTitle(t('create_folder_note'))
.setIcon('file-cog')
.onClick(() => {
if (folder instanceof TFolder) {
showFolderNoteSettingsModal(this.app, this.plugin, folder, this);
}
});
});
}
menu.addSeparator();
if (!this.plugin.settings.ignoredFolders.includes(folder.path)) {
//加入"忽略此資料夾"選項
menu.addItem((item) => {
item
.setTitle(t('ignore_folder'))
.setIcon('folder-x')
.onClick(() => {
this.plugin.settings.ignoredFolders.push(folder.path);
this.plugin.saveSettings();
});
});
} else {
//加入"取消忽略此資料夾"選項
menu.addItem((item) => {
item
.setTitle(t('unignore_folder'))
.setIcon('folder-up')
.onClick(() => {
this.plugin.settings.ignoredFolders = this.plugin.settings.ignoredFolders.filter((path) => path !== folder.path);
this.plugin.saveSettings();
});
});
}
// 搬移資料夾
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
.setTitle(t('rename_folder'))
.setIcon('file-cog')
.onClick(() => {
if (folder instanceof TFolder) {
showFolderRenameModal(this.app, this.plugin, folder, this);
}
});
});
// 刪除資料夾
menu.addItem((item) => {
(item as any).setWarning(true);
item
.setTitle(t('delete_folder'))
.setIcon('trash')
.onClick(async () => {
if (folder instanceof TFolder) {
await this.app.fileManager.trashFile(folder);
// 重新渲染視圖
setTimeout(() => {
this.render();
}, 100);
}
});
});
menu.showAtMouseEvent(event);
});
}
// 資料夾渲染完插入 break僅當有資料夾
if (subfolders.length > 0) {
container.createDiv('ge-break');
}
}
}
// 為資料夾項目添加拖曳目標功能
if(Platform.isDesktop) {
const folderItems = this.containerEl.querySelectorAll('.ge-folder-item');
folderItems.forEach(folderItem => {
folderItem.addEventListener('dragover', (event) => {
// 防止預設行為以允許放置
event.preventDefault();
// 設定拖曳效果為移動
(event as any).dataTransfer!.dropEffect = 'move';
// 顯示可放置的視覺提示
folderItem.addClass('ge-dragover');
});
folderItem.addEventListener('dragleave', () => {
// 移除視覺提示
folderItem.removeClass('ge-dragover');
});
folderItem.addEventListener('drop', async (event) => {
// 防止預設行為
event.preventDefault();
// 移除視覺提示
folderItem.removeClass('ge-dragover');
// 獲取拖曳的檔案路徑列表
const filesDataString = (event as any).dataTransfer?.getData('application/obsidian-grid-explorer-files');
if (filesDataString) {
try {
// 解析檔案路徑列表
const filePaths = JSON.parse(filesDataString);
// 獲取目標資料夾路徑
const folderPath = (folderItem as any).dataset.folderPath;
if (!folderPath) return;
// 獲取資料夾物件
const folder = this.app.vault.getAbstractFileByPath(folderPath);
if (!(folder instanceof TFolder)) return;
// 移動檔案
for (const path of filePaths) {
const file = this.app.vault.getAbstractFileByPath(path);
if (file instanceof TFile) {
try {
// 計算新的檔案路徑
const newPath = normalizePath(`${folderPath}/${file.name}`);
// 移動檔案
await this.app.fileManager.renameFile(file, newPath);
} catch (error) {
console.error(`An error occurred while moving the file ${file.path}:`, error);
}
}
}
return;
} catch (error) {
console.error('Error parsing dragged files data:', error);
}
}
// 如果沒有檔案路徑列表,則使用檔案路徑
const filePath = (event as any).dataTransfer?.getData('text/plain');
if (!filePath) return;
const cleanedFilePath = filePath.replace(/!?\[\[(.*?)\]\]/, '$1');
// 獲取目標資料夾路徑
const folderPath = (folderItem as any).dataset.folderPath;
if (!folderPath) return;
// 獲取檔案和資料夾物件
const file = this.app.vault.getAbstractFileByPath(cleanedFilePath);
const folder = this.app.vault.getAbstractFileByPath(folderPath);
if (file instanceof TFile && folder instanceof TFolder) {
try {
// 計算新的檔案路徑
const newPath = normalizePath(`${folderPath}/${file.name}`);
// 移動檔案
await this.app.fileManager.renameFile(file, newPath);
} catch (error) {
console.error('An error occurred while moving the file:', error);
}
}
});
});
}
let loadingDiv: HTMLElement | null = null;
if (this.searchQuery || this.sourceMode === 'tasks') {
// 顯示搜尋中的提示
loadingDiv = container.createDiv({ text: t('searching'), cls: 'ge-loading-indicator' });
}
let files: TFile[] = [];
// 使用 Map 來記錄原始順序
let fileIndexMap = new Map<TFile, number>();
if (this.searchQuery) {
// 取得 vault 中所有支援的檔案
let allFiles: TFile[] = [];
if (this.searchAllFiles) {
// 全部檔案
allFiles = this.app.vault.getFiles().filter(file =>
isDocumentFile(file) || (isMediaFile(file) && this.searchMediaFiles)
);
} else {
// 當前位置檔案
allFiles = await getFiles(this, this.searchMediaFiles);
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)
);
} else if (this.sourceMode === 'recent-files') {
// 搜尋"最近檔案"的當前位置時先作忽略檔案和只取前n筆
allFiles = ignoredFiles(allFiles, this).slice(0, this.plugin.settings.recentFilesCount);
} else if (this.sourceMode.startsWith('custom-')) {
// 使用 Map 來記錄原始順序
allFiles.forEach((file, index) => {
fileIndexMap.set(file, index);
});
}
}
// 根據搜尋關鍵字進行過濾(不分大小寫)
const searchTerms = this.searchQuery.toLowerCase().split(/\s+/).filter(term => term.trim() !== '');
// 分離標籤搜尋和一般搜尋
const tagTerms = searchTerms.filter(term => term.startsWith('#')).map(term => term.substring(1));
const normalTerms = searchTerms.filter(term => !term.startsWith('#'));
// 使用 Promise.all 來非同步地讀取所有檔案內容,順序可能會跟之前不同
await Promise.all(
allFiles.map(async file => {
const fileName = file.name.toLowerCase();
// 檢查檔案名稱是否包含所有一般搜尋字串
const matchesFileName = normalTerms.length === 0 || normalTerms.every(term => fileName.includes(term));
// 如果只有標籤搜尋詞且不是 Markdown 檔案,直接跳過(因為標籤只存在於 Markdown 檔案中)
if (tagTerms.length > 0 && normalTerms.length === 0 && file.extension !== 'md') {
return;
}
// 如果沒有標籤搜尋詞,只有一般搜尋詞
if (tagTerms.length === 0) {
if (matchesFileName) {
files.push(file);
} else if (file.extension === 'md') {
// 只對 Markdown 檔案進行內容搜尋
const content = (await this.app.vault.cachedRead(file)).toLowerCase();
// 檢查檔案內容是否包含所有一般搜尋字串
const matchesContent = normalTerms.every(term => content.includes(term));
if (matchesContent) {
files.push(file);
}
}
return;
}
// 處理標籤搜尋
if (file.extension === 'md') {
// 檢查檔案是否包含所有標籤
const fileCache = this.app.metadataCache.getFileCache(file);
let matchesTags = false;
if (fileCache) {
const collectedTags: string[] = [];
// 內文標籤
if (Array.isArray(fileCache.tags)) {
for (const t of fileCache.tags) {
if (t && t.tag) {
const clean = t.tag.toLowerCase().replace(/^#/, '');
collectedTags.push(...clean.split(/\s+/).filter(st => st.trim() !== ''));
}
}
}
// frontmatter 標籤
if (fileCache.frontmatter && fileCache.frontmatter.tags) {
const fmTags = fileCache.frontmatter.tags;
if (typeof fmTags === 'string') {
collectedTags.push(
...fmTags.split(/[,\s]+/)
.map(t => t.toLowerCase().replace(/^#/, ''))
.filter(t => t.trim() !== '')
);
} else if (Array.isArray(fmTags)) {
for (const t of fmTags) {
if (typeof t === 'string') {
const clean = t.toLowerCase().replace(/^#/, '');
collectedTags.push(...clean.split(/\s+/).filter(st => st.trim() !== ''));
}
}
}
}
matchesTags = tagTerms.every(tag => collectedTags.includes(tag));
}
// 如果標籤匹配,且檔名或內容也匹配(如果有一般搜尋詞的話),則加入結果
if (matchesTags) {
if (matchesFileName) {
files.push(file);
} else if (normalTerms.length > 0) {
// 如果有一般搜尋詞,還需檢查內容
const content = (await this.app.vault.cachedRead(file)).toLowerCase();
const matchesContent = normalTerms.every(term => content.includes(term));
if (matchesContent) {
files.push(file);
}
} else {
// 如果只有標籤搜尋詞,且標籤匹配,則加入結果
files.push(file);
}
}
}
})
);
// 排序檔案
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 === 'recent-files') {
// 臨時的排序類型
const sortType = this.sortType;
this.sortType = 'mtime-desc';
files = sortFiles(files, this);
this.sortType = sortType;
} else if (this.sourceMode === 'random-note') {
// 臨時的排序類型
const sortType = this.sortType;
this.sortType = 'random';
files = sortFiles(files, this);
this.sortType = sortType;
} else if (this.sourceMode.startsWith('custom-')) {
// 保持原始順序
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 {
files = sortFiles(files, this);
}
// 忽略檔案
files = ignoredFiles(files, this);
} else {
// 無搜尋關鍵字的情況
files = await getFiles(this, this.randomNoteIncludeMedia);
// 忽略檔案
files = ignoredFiles(files, this)
// 最近檔案模式只取前n筆
if (this.sourceMode === 'recent-files') {
files = files.slice(0, this.plugin.settings.recentFilesCount);
}
// 隨機筆記模式只取前n筆
if (this.sourceMode === 'random-note') {
files = files.slice(0, this.plugin.settings.randomNoteCount);
}
}
if (loadingDiv) {
loadingDiv.remove();
}
// 如果沒有檔案,顯示提示訊息
if (files.length === 0) {
const noFilesDiv = container.createDiv('ge-no-files');
if (this.sourceMode !== 'backlinks') {
noFilesDiv.setText(t('no_files'));
} else {
noFilesDiv.setText(t('no_backlinks'));
}
if (this.plugin.statusBarItem) {
this.plugin.statusBarItem.setText('');
}
return;
}
// 若有置頂清單且目前為資料夾模式,將置頂檔案移到最前面並維持其在清單中的順序
if (this.pinnedList.length > 0 && this.sourceMode === 'folder') {
const pinnedFiles = files.filter(f => this.pinnedList.includes(f.name));
// 依照 pinnedList 順序排序
pinnedFiles.sort((a, b) => this.pinnedList.indexOf(a.name) - this.pinnedList.indexOf(b.name));
const otherFiles = files.filter(f => !this.pinnedList.includes(f.name));
files = [...pinnedFiles, ...otherFiles];
}
if (this.sourceMode === 'folder' && this.sourcePath !== '/') {
if (this.plugin.settings.folderNoteDisplaySettings === 'hidden') {
const currentFolder = this.app.vault.getAbstractFileByPath(this.sourcePath);
if (currentFolder instanceof TFolder) {
const folderName = currentFolder.name;
files = files.filter(f => f.name !== `${folderName}.md`);
}
}
}
// 創建 Intersection Observer
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(async entry => {
if (entry.isIntersecting) {
const fileEl = entry.target as HTMLElement;
const filePath = fileEl.dataset.filePath;
if (!filePath) return;
const file = this.app.vault.getAbstractFileByPath(filePath);
if (!(file instanceof TFile)) return;
// 載入預覽內容
let imageUrl: string | null = '';
const contentArea = fileEl.querySelector('.ge-content-area') as Element;
if (!contentArea.hasAttribute('data-loaded')) {
// 根據檔案類型處理
if (file.extension === 'md') {
let summaryLength = this.plugin.settings.summaryLength;
if (summaryLength < 50) {
summaryLength = 100;
this.plugin.settings.summaryLength = 100;
this.plugin.saveSettings();
}
// Markdown 檔案顯示內容預覽
const content = await this.app.vault.cachedRead(file);
const frontMatterInfo = getFrontMatterInfo(content);
let metadata: FrontMatterCache | undefined = undefined;
if (frontMatterInfo.exists) {
metadata = this.app.metadataCache.getFileCache(file)?.frontmatter;
}
let pEl: HTMLElement | null = null;
if (!this.minMode) {
let summaryField = this.plugin.settings.noteSummaryField || 'summary';
let summaryValue = metadata?.[summaryField];
if (this.sourceMode.startsWith('custom-')) {
// 自訂模式下,使用自訂的 fields 來顯示摘要
const mode = this.plugin.settings.customModes.find(m => m.internalName === this.sourceMode);
if (mode) {
let fields = mode?.fields || '';
// 當有選到子選項 (index >= 0) 而且 options 陣列確實存在
if (this.customOptionIndex >= 0 &&
mode.options &&
this.customOptionIndex < mode.options.length) {
const option = mode.options[this.customOptionIndex];
fields = option.fields || '';
}
// 如果 fields 不為空,則使用它來顯示摘要
if (fields) {
// 將 fields 以逗號分隔成陣列,並過濾掉空值
const fieldList = fields.split(',').map(f => f.trim()).filter(Boolean);
const fieldValues: string[] = [];
// 收集所有欄位值
fieldList.forEach(field => {
if (metadata?.[field] !== undefined && metadata?.[field] !== '' && metadata?.[field] !== null) {
// 如果是數字,則加入千位分隔符號
if (typeof metadata[field] === 'number') {
metadata[field] = metadata[field].toLocaleString();
}
// 如果是陣列,則轉換為字串
if (Array.isArray(metadata[field])) {
metadata[field] = metadata[field].join(', ');
}
fieldValues.push(`${field}: ${metadata[field]}`);
}
});
// 如果有找到任何欄位值,則組合起來
if (fieldValues.length > 0) {
summaryValue = fieldValues.join('\n'); // 使用 | 分隔不同欄位
}
}
}
}
if (summaryValue) {
if (!this.sourceMode.startsWith('custom-')) {
// Frontmatter 有設定摘要值
pEl = contentArea.createEl('p', { text: summaryValue.trim() });
} else {
// custom mode 有設定顯示欄位值
pEl = contentArea.createEl('p', { text: summaryValue.trim() , cls: 'ge-content-area-p-field' });
}
} else {
// Frontmatter 沒有設定摘要值,則使用內文
let contentWithoutFrontmatter = '';
if (summaryLength < 500) {
contentWithoutFrontmatter = content.substring(frontMatterInfo.contentStart).slice(0, 500);
} else {
contentWithoutFrontmatter = content.substring(frontMatterInfo.contentStart).slice(0, summaryLength + summaryLength);
}
let contentWithoutMediaLinks = '';
if (this.plugin.settings.showCodeBlocksInSummary) {
contentWithoutMediaLinks = contentWithoutFrontmatter;
} else {
// 刪除 code block
contentWithoutMediaLinks = contentWithoutFrontmatter
.replace(/```[\s\S]*?```\n/g, '')
.replace(/```[\s\S]*$/,'');
}
// 刪除註解及連結
contentWithoutMediaLinks = contentWithoutMediaLinks
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/!?\[([^\]]*)\]\([^)]+\)|!?\[\[([^\]]+)\]\]/g, (match, p1, p2) => {
const linkText = p1 || p2 || '';
if (!linkText) return '';
// 獲取副檔名並檢查是否為圖片或影片
const extension = linkText.split('.').pop()?.toLowerCase() || '';
return (IMAGE_EXTENSIONS.has(extension) || VIDEO_EXTENSIONS.has(extension)) ? '' : linkText;
});
//把開頭的標題整行刪除
if (contentWithoutMediaLinks.startsWith('# ') || contentWithoutMediaLinks.startsWith('## ') || contentWithoutMediaLinks.startsWith('### ')) {
contentWithoutMediaLinks = contentWithoutMediaLinks.split('\n').slice(1).join('\n');
}
if (!this.plugin.settings.showCodeBlocksInSummary) {
// 不刪除code block的情況下包含這些特殊符號
contentWithoutMediaLinks = contentWithoutMediaLinks.replace(/[>|\-#*]/g,'').trim();
}
// 只取前 summaryLength 個字符作為預覽
const preview = contentWithoutMediaLinks.slice(0, summaryLength) + (contentWithoutMediaLinks.length > summaryLength ? '...' : '');
// 創建預覽內容
pEl = contentArea.createEl('p', { text: preview.trim() });
}
}
//將預覽文字設定到標題的 title 屬性中
const titleEl = fileEl.querySelector('.ge-title');
if (titleEl) {
setTooltip(contentArea as HTMLElement, `${titleEl.textContent}`)
}
if (frontMatterInfo.exists) {
const colorValue = metadata?.color;
if (colorValue) {
// 使用 CSS 類別來設置顏色
fileEl.addClass(`ge-note-color-${colorValue}`);
// 設置預覽內容文字顏色
if (pEl) {
pEl.addClass(`ge-note-color-${colorValue}-text`);
}
}
const titleField = this.plugin.settings.noteTitleField || 'title';
const titleValue = metadata?.[titleField];
if (titleValue) {
// 將標題文字設為 frontmatter 的 title
if (titleEl) {
titleEl.textContent = titleValue;
}
}
const displayValue = metadata?.display;
if (displayValue === 'minimized') {
// 移除已建立的預覽段落
if (pEl) {
pEl.remove();
}
// 移除圖片區域(若已存在)
const imageAreaEl = fileEl.querySelector('.ge-image-area');
if (imageAreaEl) {
imageAreaEl.remove();
}
fileEl.style.height = '100%';
}
}
imageUrl = await findFirstImageInNote(this.app, content);
} else {
// 其他檔案顯示副檔名
if (!this.minMode) {
contentArea.createEl('p', { text: file.extension.toUpperCase() });
}
setTooltip(fileEl as HTMLElement, `${file.name}`,{ delay:2000 })
}
// 顯示標籤(僅限 Markdown 檔案)
if (file.extension === 'md' && this.showNoteTags && !this.minMode) {
const fileCache = this.app.metadataCache.getFileCache(file);
const displaySetting = fileCache?.frontmatter?.display;
// 如果筆記是最小化就直接跳過標籤邏輯
if (displaySetting !== 'minimized') {
const allTags = new Set<string>();
// 從 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('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
const tagText = tag.startsWith('#') ? tag : `#${tag}`;
const menu = new Menu();
//添加加入搜尋選項
if (!this.searchQuery.includes(tagText)) {
menu.addItem(item => item
.setTitle(t('add_tag_to_search'))
.setIcon('circle-plus')
.onClick(() => {
this.searchQuery += ` ${tagText}`;
this.render(true);
return false;
})
);
}
// 添加刪除標籤選項
if (this.searchQuery.includes(tagText)) {
menu.addItem(item => item
.setTitle(t('remove_tag_from_search'))
.setIcon('circle-minus')
.onClick(() => {
this.searchQuery = this.searchQuery.replace(tagText, '');
this.render(true);
return false;
})
);
}
menu.showAtPosition({
x: e.clientX,
y: e.clientY
});
});
// 添加點擊事件,點擊後設置搜尋關鍵字並重新渲染
tagEl.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation(); // 防止事件冒泡到卡片
const tagText = tag.startsWith('#') ? tag : `#${tag}`;
if (this.searchQuery === tagText) {
return;
}
this.searchQuery = tagText;
this.render(true);
return false;
});
});
}
}
}
contentArea.setAttribute('data-loaded', 'true');
}
// 載入圖片預覽
if (!this.minMode) {
const imageArea = fileEl.querySelector('.ge-image-area');
if (imageArea && !imageArea.hasAttribute('data-loaded')) {
// 根據檔案類型處理
if (isImageFile(file)) {
// 直接顯示圖片
const img = imageArea.createEl('img');
img.src = this.app.vault.getResourcePath(file);
imageArea.setAttribute('data-loaded', 'true');
} else if (isVideoFile(file)) {
// 根據設定決定是否顯示影片縮圖
if (this.plugin.settings.showVideoThumbnails) {
// 顯示影片縮圖
const video = imageArea.createEl('video');
video.src = this.app.vault.getResourcePath(file);
} else {
// 顯示播放圖示
const videoThumb = imageArea.createDiv('ge-video-thumbnail');
setIcon(videoThumb, 'play-circle');
}
imageArea.setAttribute('data-loaded', 'true');
} else if (file.extension === 'md') {
// Markdown 檔案尋找內部圖片
if (imageUrl) {
const img = imageArea.createEl('img');
img.src = imageUrl;
imageArea.setAttribute('data-loaded', 'true');
} else {
// 如果沒有圖片,移除圖片區域
imageArea.remove();
}
} else {
// 其他檔案類型,移除圖片區域
imageArea.remove();
}
}
}
// 一旦載入完成,就不需要再觀察這個元素
observer.unobserve(fileEl);
}
});
}, {
root: container,
rootMargin: '50px', // 預先載入視窗外 50px 的內容
threshold: 0.1
});
// 顯示檔案
if (files.length > 0) {
// 檢查是否應該顯示日期分隔器
const dateDividerMode = this.plugin.settings.dateDividerMode || 'none';
const sortType = this.folderSortType ? this.folderSortType : this.sortType;
const shouldShowDateDividers = dateDividerMode !== 'none' &&
(sortType.startsWith('mtime-') || sortType.startsWith('ctime-')) &&
this.sourceMode !== 'random-note' &&
this.sourceMode !== 'bookmarks' &&
this.showDateDividers;
let lastDateString = '';
let pinDividerAdded = false;
let blankDividerAdded = false;
// 每次重新渲染時遞增 token以便中斷舊批次
const currentToken = ++this.renderToken;
// 設定批次渲染狀態
const state: DividerState = { lastDateString: lastDateString, pinDividerAdded: pinDividerAdded, blankDividerAdded: blankDividerAdded };
const paramsBase: FileRenderParams = { container, observer, files, dateDividerMode, sortType, shouldShowDateDividers, state };
const selfRef = this;
if (Platform.isIosApp) {
// iOS 專用:以 time-slice 方式分批,避免阻塞點擊事件
const TIME_BUDGET_MS = 6; // 每幀最多執行 6ms
const processChunk = (start: number) => {
if (currentToken !== this.renderToken) return;
const startTime = performance.now();
let i = start;
for (; i < files.length; i++) {
selfRef.processFile(files[i], paramsBase);
if (performance.now() - startTime > TIME_BUDGET_MS) {
break; // 超過時間預算,讓出主執行緒
}
}
if (i < files.length) {
requestAnimationFrame(() => processChunk(i)); // 下一幀繼續
}
};
processChunk(0);
} else {
// 其他平台維持原本固定 batchSize 的邏輯
const batchSize = 50;
const processBatch = (start: number) => {
if (currentToken !== this.renderToken) return;
const end = Math.min(start + batchSize, files.length);
for (let i = start; i < end; i++) {
selfRef.processFile(files[i], paramsBase);
}
if (end < files.length) {
const cb = () => processBatch(end);
if (typeof (window as any).requestIdleCallback === 'function') {
(window as any).requestIdleCallback(cb);
} else {
setTimeout(cb, 0);
}
}
};
processBatch(0);
}
}
if (this.plugin.statusBarItem) {
this.plugin.statusBarItem.setText(`${files.length} ${t('files')}`);
}
}
// 處理單個檔案渲染
private processFile(file: TFile, params: FileRenderParams): void {
const { container, observer, files, dateDividerMode, sortType, shouldShowDateDividers, state } = params;
// 如果需要顯示置頂分隔器,且尚未加入,當前檔案為置頂清單之一時插入
if (!state.pinDividerAdded && this.pinnedList.includes(file.name)) {
const pinDivider = container.createDiv('ge-pin-divider');
pinDivider.textContent = t('pinned');
state.pinDividerAdded = true;
// 針對 iOS 設備進行特殊處理
if (Platform.isIosApp) {
pinDivider.style.width = 'calc(100% - 16px)';
}
}
// 插入空白分隔器:當已加入置頂分隔器且尚未加入空白分隔器,且當前檔案不是置頂檔案
if (state.pinDividerAdded && !state.blankDividerAdded && !this.pinnedList.includes(file.name)) {
container.createDiv('ge-break');
state.blankDividerAdded = true;
}
// 日期分隔器
if (shouldShowDateDividers && !this.pinnedList.includes(file.name)) {
let timestamp = 0;
// 根據排序類型獲取日期時間戳
if (sortType.startsWith('mtime-') || sortType.startsWith('ctime-')) {
// 判斷是否以修改日期排序,最近檔案模式使用修改日期排序
const isModifiedTime = sortType.startsWith('mtime-') || this.sourceMode === 'recent-files';
// 檢查是否是 Markdown 文件,且有設定對應的 frontmatter 字段
let frontMatterDate = null;
if (file.extension === 'md') {
const metadata = this.app.metadataCache.getFileCache(file);
if (metadata?.frontmatter) {
const fieldSetting = isModifiedTime
? this.plugin.settings.modifiedDateField
: this.plugin.settings.createdDateField;
const fieldNames = fieldSetting
? fieldSetting.split(',').map(f => f.trim()).filter(Boolean)
: [];
for (const fieldName of fieldNames) {
const dateStr = metadata.frontmatter[fieldName];
if (dateStr) {
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
frontMatterDate = date;
break; // 已找到有效日期
}
}
}
}
}
// 使用 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 !== state.lastDateString) {
state.lastDateString = currentDateString;
// 創建日期分隔器
const dateDivider = container.createDiv('ge-date-divider');
dateDivider.textContent = currentDateString;
// 針對 iOS 設備進行特殊處理
if (Platform.isIosApp) {
dateDivider.style.width = 'calc(100% - 16px)';
}
}
}
const fileEl = container.createDiv('ge-grid-item');
this.gridItems.push(fileEl); // 添加到網格項目數組
fileEl.dataset.filePath = file.path;
// 如果檔案與父資料夾同名,添加 ge-foldernote 類別
const parentPath = file.parent?.path || '';
const parentName = parentPath.split('/').pop() || '';
if (parentName === file.basename) {
fileEl.addClass('ge-foldernote');
}
//如果檔案是否處於置頂範圍,添加 ge-pinned 類別
if (this.pinnedList.includes(file.name)) {
fileEl.addClass('ge-pinned');
}
// 創建左側內容區,包含圖示和標題
const contentArea = fileEl.createDiv('ge-content-area');
// 創建標題容器
const titleContainer = contentArea.createDiv('ge-title-container');
const extension = file.extension.toLowerCase();
// 檢查是否為媒體檔案,如果是則添加 ge-media-card 類別
if (this.cardLayout === 'vertical' &&
(isImageFile(file) || isVideoFile(file)) &&
!this.minMode) {
fileEl.addClass('ge-media-card');
}
// 添加檔案類型圖示
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 === 'base') {
const iconContainer = titleContainer.createDiv('ge-icon-container ge-base');
setIcon(iconContainer, 'layout-list');
} 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 shouldShowExtension = this.minMode && extension !== 'md';
const displayText = shouldShowExtension ? `${file.basename}.${file.extension}` : file.basename;
const titleEl = titleContainer.createEl('span', { cls: 'ge-title', text: displayText });
if (this.plugin.settings.multiLineTitle) titleEl.addClass('ge-multiline-title');
// 創建圖片區域,但先不載入圖片
if (!this.minMode) {
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.selectedItems.size > 1) {
// 如果已有選中2個以上的狀態則繼續選中
this.selectItem(index, true);
this.hasKeyboardFocus = true;
} else {
// 沒有選中狀態則開啟新分頁
if (isMediaFile(file)) {
// 開啟媒體檔案
if (isAudioFile(file)) {
FloatingAudioPlayer.open(this.app, file);
} else {
this.openMediaFile(file, files);
}
} else {
// 開啟文件檔案到新分頁
this.app.workspace.getLeaf(true).openFile(file);
}
}
event.preventDefault();
return;
} else if (event.shiftKey) {
// Shift 鍵:範圍選擇
this.handleRangeSelection(index);
this.hasKeyboardFocus = true;
event.preventDefault();
return;
} else if (event.altKey || this.plugin.settings.showNoteInGrid) {
// Alt 鍵或設定為預設時:在 grid container 中顯示筆記
this.selectItem(index);
this.hasKeyboardFocus = true;
if (isMediaFile(file)) {
// 媒體檔案:正常開啟
if (isAudioFile(file)) {
FloatingAudioPlayer.open(this.app, file);
} else {
this.openMediaFile(file, files);
}
} else {
// 非媒體檔案:在 grid container 中顯示筆記
this.showNoteInGrid(file);
}
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);
}
} 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);
}
}
});
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);
}
}
// 獲取選中的檔案
const selectedFiles = this.getSelectedFiles();
let drag_filename = '';
// 添加拖曳資料
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)));
drag_filename = `${selectedFiles.length} ${t('files')}`;
} 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]));
drag_filename = file.basename;
}
const dragImage = document.createElement('div');
dragImage.className = 'ge-custom-drag-preview';
dragImage.textContent = drag_filename;
// 將元素暫時加入 DOM
document.body.appendChild(dragImage);
// 設定拖曳圖示
event.dataTransfer!.setDragImage(dragImage, 20, 20);
// 延遲移除元素(讓拖曳圖示正常顯示)
setTimeout(() => {
document.body.removeChild(dragImage);
}, 0);
// 設定拖曳效果
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) {
// 如果項目未被選中,則選中它
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_attribute'))
.setIcon('palette')
.onClick(() => {
showNoteSettingsModal(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);
}
} else {
// 刪除單個檔案
await this.app.fileManager.trashFile(file);
}
// 清除選中狀態
this.clearSelection();
});
});
menu.showAtMouseEvent(event);
});
}
onPaneMenu(menu: Menu, source: string) {
menu.addItem(item => {
item
.setTitle(t('hide_header_elements'))
.setIcon("archive-restore")
.setChecked(this.hideHeaderElements)
.onClick(() => {
this.hideHeaderElements = !this.hideHeaderElements;
this.app.workspace.requestSaveLayout();
this.render(true);
});
});
menu.addItem((item) => {
item
.setTitle(t('reselect'))
.setIcon("grid")
.onClick(() => {
showFolderSelectionModal(this.app, this.plugin, this);
});
});
menu.addItem((item) => {
item
.setTitle(t('refresh'))
.setIcon("refresh-cw")
.onClick(() => {
this.render(true);
});
});
}
// 在新視窗中開啟資料夾
private openFolderInNewView(folderPath: string) {
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', folderPath);
}
// 確保視圖是活躍的
workspace.revealLeaf(leaf);
}
// 清除選中狀態
clearSelection() {
this.gridItems.forEach(item => {
item.removeClass('ge-selected-item');
});
this.selectedItemIndex = -1;
this.selectedItems.clear();
}
// 選中指定索引的項目
selectItem(index: number, multiSelect = false) {
// 如果不是多選模式,先清除所有項目的選中狀態
if (!multiSelect) {
this.gridItems.forEach(item => {
item.removeClass('ge-selected-item');
});
this.selectedItems.clear();
}
// 確保索引在有效範圍內
if (index >= 0 && index < this.gridItems.length) {
this.selectedItemIndex = index;
const selectedItem = this.gridItems[index];
// 如果是多選模式且項目已被選中,則取消選中
if (multiSelect && this.selectedItems.has(index)) {
selectedItem.removeClass('ge-selected-item');
this.selectedItems.delete(index);
// 如果取消選中後沒有選中項目,則設置 selectedItemIndex 為 -1
if (this.selectedItems.size === 0) {
this.selectedItemIndex = -1;
} else {
// 否則設置為最後一個選中項目
this.selectedItemIndex = Array.from(this.selectedItems).pop() || -1;
}
} else {
// 否則選中項目
selectedItem.addClass('ge-selected-item');
this.selectedItems.add(index);
}
// 確保選中的項目在視圖中可見
selectedItem.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
}
// 處理範圍選擇Shift 鍵)
handleRangeSelection(index: number) {
if (this.selectedItemIndex === -1) {
// 如果沒有已選中的項目,直接選中當前項目
this.selectItem(index);
return;
}
// 計算範圍的起始和結束索引
const startIndex = Math.min(this.selectedItemIndex, index);
const endIndex = Math.max(this.selectedItemIndex, index);
// 清除現有選擇
this.gridItems.forEach(item => {
item.removeClass('ge-selected-item');
});
this.selectedItems.clear();
// 選中範圍內的所有項目
for (let i = startIndex; i <= endIndex; i++) {
this.gridItems[i].addClass('ge-selected-item');
this.selectedItems.add(i);
}
// 更新當前選中索引
this.selectedItemIndex = index;
}
// 獲取所有選中項目的檔案
getSelectedFiles(): TFile[] {
const files: TFile[] = [];
this.selectedItems.forEach(index => {
const fileEl = this.gridItems[index];
const filePath = fileEl.dataset.filePath;
if (filePath) {
const file = this.app.vault.getAbstractFileByPath(filePath);
if (file instanceof TFile) {
files.push(file);
}
}
});
return files;
}
// 開啟媒體檔案
openMediaFile(file: TFile, mediaFiles?: TFile[]) {
// 如果沒有傳入媒體檔案列表,則獲取
const getMediaFilesPromise = mediaFiles
? Promise.resolve(mediaFiles.filter(f => isMediaFile(f)))
: getFiles(this, this.randomNoteIncludeMedia).then(allFiles => allFiles.filter(f => isMediaFile(f)));
getMediaFilesPromise.then(filteredMediaFiles => {
// 找到當前檔案在媒體檔案列表中的索引
const currentIndex = filteredMediaFiles.findIndex(f => f.path === file.path);
if (currentIndex === -1) return;
// 使用 MediaModal 開啟媒體檔案,並傳入 this 作為 gridView 參數
const mediaModal = new MediaModal(this.app, file, filteredMediaFiles, this);
mediaModal.open();
});
}
// 在 grid container 中顯示筆記
async showNoteInGrid(file: TFile) {
// 關閉之前的筆記顯示
if (this.isShowingNote) {
this.hideNoteInGrid();
}
const gridContainer = this.containerEl.querySelector('.ge-grid-container');
if (!gridContainer) return;
// 隱藏網格內容
gridContainer.addClass('ge-hidden');
// 創建筆記顯示容器
this.noteViewContainer = this.containerEl.createDiv('ge-note-view-container');
// 頂部列 (左右區塊)
const topBar = this.noteViewContainer.createDiv('ge-note-top-bar');
const leftBar = topBar.createDiv('ge-note-top-left');
const rightBar = topBar.createDiv('ge-note-top-right');
// 筆記標題
const noteTitle = leftBar.createDiv('ge-note-title');
noteTitle.textContent = file.basename;
setTooltip(noteTitle, file.basename);
// 編輯按鈕
const editButton = rightBar.createEl('button', { cls: 'ge-note-edit-button' });
setIcon(editButton, 'pencil');
editButton.addEventListener('click', () => {
this.app.workspace.getLeaf().openFile(file);
});
// 關閉按鈕
const closeButton = rightBar.createEl('button', { cls: 'ge-note-close-button' });
setIcon(closeButton, 'x');
closeButton.addEventListener('click', () => {
this.hideNoteInGrid();
});
// 捲動內容容器
const scrollContainer = this.noteViewContainer.createDiv('ge-note-scroll-container');
// 創建筆記內容容器
const noteContent = scrollContainer.createDiv('ge-note-content-container');
// 創建筆記內容區域
const noteContentArea = noteContent.createDiv('ge-note-content');
try {
// 讀取筆記內容
const content = await this.app.vault.read(file);
// 使用 Obsidian 的 MarkdownRenderer 渲染內容
await MarkdownRenderer.render(
this.app,
content,
noteContentArea,
file.path,
this
);
// 加上自訂屬性 data-source-path
noteContentArea
.querySelectorAll<HTMLImageElement>('img')
.forEach((img) => (img.dataset.sourcePath = file.path));
// 處理內部連結點擊
const handleLinkClick = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const link = target.closest('a.internal-link');
if (link) {
e.preventDefault();
e.stopPropagation();
const href = link.getAttribute('href');
if (href) {
const linkText = link.getAttribute('data-href') || href;
const linkedFile = this.app.metadataCache.getFirstLinkpathDest(linkText, file.path);
if (linkedFile) {
this.app.workspace.getLeaf().openFile(linkedFile);
}
}
}
};
// 使用 registerDomEvent 註冊事件
this.registerDomEvent(noteContentArea, 'click', handleLinkClick);
} catch (error) {
noteContentArea.textContent = '無法載入筆記內容';
console.error('Error loading note content:', error);
}
// 設定狀態
this.isShowingNote = true;
// 註冊鍵盤事件監聽器
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
this.hideNoteInGrid();
e.preventDefault();
}
};
document.addEventListener('keydown', handleKeyDown);
// 儲存事件監聽器以便後續移除
(this.noteViewContainer as any).keydownHandler = handleKeyDown;
}
// 隱藏筆記顯示
hideNoteInGrid() {
if (!this.isShowingNote) return;
const gridContainer = this.containerEl.querySelector('.ge-grid-container');
if (gridContainer) {
gridContainer.removeClass('ge-hidden');
}
if (this.noteViewContainer) {
// 移除鍵盤事件監聽器
const keydownHandler = (this.noteViewContainer as any).keydownHandler;
if (keydownHandler) {
document.removeEventListener('keydown', keydownHandler);
}
this.noteViewContainer.remove();
this.noteViewContainer = null;
}
this.isShowingNote = false;
}
// 保存視圖狀態
getState() {
return {
type: 'grid-view',
state: {
sourceMode: this.sourceMode,
sourcePath: this.sourcePath,
sortType: this.sortType,
folderSortType: this.folderSortType,
searchQuery: this.searchQuery,
searchAllFiles: this.searchAllFiles,
searchMediaFiles: this.searchMediaFiles,
randomNoteIncludeMedia: this.randomNoteIncludeMedia,
minMode: this.minMode,
showIgnoredFolders: this.showIgnoredFolders,
baseCardLayout: this.baseCardLayout,
cardLayout: this.cardLayout,
hideHeaderElements: this.hideHeaderElements,
showDateDividers: this.showDateDividers,
showNoteTags: this.showNoteTags,
}
};
}
// 讀取視圖狀態
async setState(state: any): Promise<void> {
if (state.state) {
this.sourceMode = state.state.sourceMode || 'folder';
this.sourcePath = state.state.sourcePath || '/';
this.sortType = state.state.sortType || this.plugin.settings.defaultSortType;
this.folderSortType = state.state.folderSortType || '';
this.searchQuery = state.state.searchQuery || '';
this.searchAllFiles = state.state.searchAllFiles ?? true;
this.searchMediaFiles = state.state.searchMediaFiles ?? false;
this.randomNoteIncludeMedia = state.state.randomNoteIncludeMedia ?? false;
this.minMode = state.state.minMode ?? false;
this.showIgnoredFolders = state.state.showIgnoredFolders ?? false;
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();
}
}
}