This commit is contained in:
Devon22 2026-05-15 17:52:33 +08:00
parent fd6046db67
commit b1887f3ad7
25 changed files with 1018 additions and 587 deletions

View file

@ -30,5 +30,6 @@ export default tseslint.config(
"version-bump.mjs", "version-bump.mjs",
"versions.json", "versions.json",
"main.js", "main.js",
"package.json"
]), ]),
); );

View file

@ -1,7 +1,7 @@
{ {
"id": "gridexplorer", "id": "gridexplorer",
"name": "GridExplorer", "name": "GridExplorer",
"version": "3.2.1", "version": "3.2.2",
"minAppVersion": "1.8.7", "minAppVersion": "1.8.7",
"description": "Browse note files in a grid view.", "description": "Browse note files in a grid view.",
"author": "Devon22", "author": "Devon22",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "gridexplorer", "name": "gridexplorer",
"version": "3.2.1", "version": "3.2.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "gridexplorer", "name": "gridexplorer",
"version": "3.2.1", "version": "3.2.2",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^16.11.6", "@types/node": "^16.11.6",

View file

@ -1,6 +1,6 @@
{ {
"name": "gridexplorer", "name": "gridexplorer",
"version": "3.2.1", "version": "3.2.2",
"description": "Browse note files in a grid view.", "description": "Browse note files in a grid view.",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View file

@ -1,4 +1,4 @@
import { TFile, TFolder, WorkspaceLeaf, Menu, setIcon, Platform, normalizePath, ItemView, EventRef, FuzzySuggestModal, parseLinktext } from 'obsidian'; import { TFile, TFolder, WorkspaceLeaf, Menu, setIcon, Platform, normalizePath, ItemView, EventRef, FuzzySuggestModal, parseLinktext, ViewStateResult } from 'obsidian';
import GridExplorerPlugin from './main'; import GridExplorerPlugin from './main';
import { GridView } from './GridView'; import { GridView } from './GridView';
import { isFolderIgnored, isImageFile, isVideoFile, isAudioFile, isMediaFile } from './utils/fileUtils'; import { isFolderIgnored, isImageFile, isVideoFile, isAudioFile, isMediaFile } from './utils/fileUtils';
@ -12,10 +12,21 @@ import { MediaModal } from './modal/mediaModal';
import { ShortcutSelectionModal } from './modal/shortcutSelectionModal'; import { ShortcutSelectionModal } from './modal/shortcutSelectionModal';
import { FloatingAudioPlayer } from './FloatingAudioPlayer'; import { FloatingAudioPlayer } from './FloatingAudioPlayer';
import { t } from './translations'; import { t } from './translations';
import { CustomMode, GallerySettings } from './settings';
// 探索器視圖類型常數 // 探索器視圖類型常數
export const EXPLORER_VIEW_TYPE = 'explorer-view'; export const EXPLORER_VIEW_TYPE = 'explorer-view';
type ModeItem = { key?: string; label: string; icon: string; onClick: () => void };
type CustomEventPayload = { mode?: string; path?: string };
interface WorkspaceWithCustomEvents {
on(name: string, callback: (...args: unknown[]) => void): EventRef;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
export class ExplorerView extends ItemView { export class ExplorerView extends ItemView {
plugin: GridExplorerPlugin; plugin: GridExplorerPlugin;
@ -104,7 +115,7 @@ export class ExplorerView extends ItemView {
} }
// 恢復視圖狀態 // 恢復視圖狀態
async setState(state: any, result?: any): Promise<void> { async setState(state: unknown, result: ViewStateResult): Promise<void> {
await super.setState(state, result); await super.setState(state, result);
this.restoreExpandedPaths(state); this.restoreExpandedPaths(state);
@ -117,8 +128,8 @@ export class ExplorerView extends ItemView {
} }
// 恢復展開路徑狀態 // 恢復展開路徑狀態
private restoreExpandedPaths(state: any) { private restoreExpandedPaths(state: unknown) {
if (state?.expandedPaths && Array.isArray(state.expandedPaths)) { if (isRecord(state) && Array.isArray(state.expandedPaths)) {
this.expandedPaths = new Set( this.expandedPaths = new Set(
state.expandedPaths.filter((p: unknown) => typeof p === 'string') state.expandedPaths.filter((p: unknown) => typeof p === 'string')
); );
@ -128,19 +139,16 @@ export class ExplorerView extends ItemView {
} }
// 恢復搜尋查詢 // 恢復搜尋查詢
private restoreSearchQuery(state: any) { private restoreSearchQuery(state: unknown) {
this.searchQuery = (state?.searchQuery && typeof state.searchQuery === 'string') this.searchQuery = (isRecord(state) && typeof state.searchQuery === 'string')
? state.searchQuery ? state.searchQuery
: ''; : '';
} }
// 恢復暫存檔案列表 // 恢復暫存檔案列表
private restoreStashFiles(state: any) { private restoreStashFiles(state: unknown) {
if (state?.stashFilePaths && Array.isArray(state.stashFilePaths)) { if (isRecord(state) && Array.isArray(state.stashFilePaths)) {
const validPaths = state.stashFilePaths this.stashFilePaths = this.getValidStashPaths(state.stashFilePaths as unknown[]);
.filter((p: unknown) => typeof p === 'string' && p)
.filter((p: string) => this.app.vault.getAbstractFileByPath(p) instanceof TFile);
this.stashFilePaths = Array.from(new Set(validPaths));
// 將還原的暫存區同步回設定,確保下次新建視圖也能取得 // 將還原的暫存區同步回設定,確保下次新建視圖也能取得
this.persistStashToSettings(); this.persistStashToSettings();
} }
@ -150,22 +158,27 @@ export class ExplorerView extends ItemView {
private loadStashFromSettingsIfNeeded() { private loadStashFromSettingsIfNeeded() {
try { try {
if (this.stashFilePaths.length > 0) return; if (this.stashFilePaths.length > 0) return;
const paths = (this.plugin.settings as any)?.explorerStashPaths as unknown; const paths: unknown = this.plugin.settings.explorerStashPaths;
if (!Array.isArray(paths)) return; if (!Array.isArray(paths)) return;
const validPaths = paths this.stashFilePaths = this.getValidStashPaths(paths as unknown[]);
.filter((p: unknown) => typeof p === 'string' && p)
.filter((p: string) => this.app.vault.getAbstractFileByPath(p) instanceof TFile);
this.stashFilePaths = Array.from(new Set(validPaths));
} catch (error) { } catch (error) {
console.error('ExplorerView: Failed to load stash from settings', error); console.error('ExplorerView: Failed to load stash from settings', error);
} }
} }
// 將目前的暫存清單寫回設定(去重) // 將目前的暫存清單寫回設定(去重)
private getValidStashPaths(paths: unknown[]): string[] {
const validPaths = paths
.filter((p): p is string => typeof p === 'string' && p.length > 0)
.filter((p) => this.app.vault.getAbstractFileByPath(p) instanceof TFile);
return Array.from(new Set(validPaths));
}
private persistStashToSettings() { private persistStashToSettings() {
try { try {
const unique = Array.from(new Set(this.stashFilePaths)); const unique = Array.from(new Set(this.stashFilePaths));
(this.plugin.settings as any).explorerStashPaths = unique; this.plugin.settings.explorerStashPaths = unique;
// 保存但不觸發整體視圖更新,避免頻繁重繪 // 保存但不觸發整體視圖更新,避免頻繁重繪
void this.plugin.saveSettings(false); void this.plugin.saveSettings(false);
} catch (error) { } catch (error) {
@ -197,14 +210,18 @@ export class ExplorerView extends ItemView {
this.eventRefs.push( this.eventRefs.push(
vault.on('create', schedule), vault.on('create', schedule),
vault.on('delete', (file: any) => this.handleFileDelete(file, schedule)), vault.on('delete', (file: unknown) => this.handleFileDelete(file, schedule)),
vault.on('rename', (file: any, oldPath: string) => this.handleFileRename(file, oldPath, schedule)) vault.on('rename', (file: unknown, oldPath: string) => this.handleFileRename(file, oldPath, schedule))
); );
this.registerCustomEvent('ge-grid-source-changed', (payload: any) => { this.registerCustomEvent('ge-grid-source-changed', (payload: unknown) => {
// 檢查模式或路徑是否真的改變了 // 檢查模式或路徑是否真的改變了
const newMode = payload?.mode ?? this.lastMode; const sourceChangedPayload: CustomEventPayload = isRecord(payload) ? {
const newPath = payload?.path ?? this.lastPath; mode: typeof payload.mode === 'string' ? payload.mode : undefined,
path: typeof payload.path === 'string' ? payload.path : undefined,
} : {};
const newMode = sourceChangedPayload.mode ?? this.lastMode;
const newPath = sourceChangedPayload.path ?? this.lastPath;
// 只有在模式或路徑真正改變時才更新狀態和觸發重繪 // 只有在模式或路徑真正改變時才更新狀態和觸發重繪
if (newMode !== this.lastMode || newPath !== this.lastPath) { if (newMode !== this.lastMode || newPath !== this.lastPath) {
@ -229,13 +246,13 @@ export class ExplorerView extends ItemView {
} }
// 處理檔案刪除事件 // 處理檔案刪除事件
private handleFileDelete(file: any, schedule: () => void) { private handleFileDelete(file: unknown, schedule: () => void) {
const path = file?.path as string | undefined; const path = isRecord(file) && typeof file.path === 'string' ? file.path : undefined;
if (path) { if (path) {
this.removeExpandedPrefix(path); this.removeExpandedPrefix(path);
if (file instanceof TFile) { if (file instanceof TFile) {
this.stashFilePaths = this.stashFilePaths.filter(p => p !== path); this.stashFilePaths = this.stashFilePaths.filter(p => p !== path);
this.app.workspace.requestSaveLayout(); void this.app.workspace.requestSaveLayout();
this.persistStashToSettings(); this.persistStashToSettings();
} }
} }
@ -243,13 +260,13 @@ export class ExplorerView extends ItemView {
} }
// 處理檔案重新命名事件 // 處理檔案重新命名事件
private handleFileRename(file: any, oldPath: string, schedule: () => void) { private handleFileRename(file: unknown, oldPath: string, schedule: () => void) {
const newPath = file?.path as string || ''; const newPath = isRecord(file) && typeof file.path === 'string' ? file.path : '';
if (oldPath && newPath) { if (oldPath && newPath) {
this.renameExpandedPrefix(oldPath, newPath); this.renameExpandedPrefix(oldPath, newPath);
if (file instanceof TFile) { if (file instanceof TFile) {
this.stashFilePaths = this.stashFilePaths.map(p => p === oldPath ? newPath : p); this.stashFilePaths = this.stashFilePaths.map(p => p === oldPath ? newPath : p);
this.app.workspace.requestSaveLayout(); void this.app.workspace.requestSaveLayout();
this.persistStashToSettings(); this.persistStashToSettings();
} }
} }
@ -257,9 +274,9 @@ export class ExplorerView extends ItemView {
} }
// 註冊自定義事件 // 註冊自定義事件
private registerCustomEvent(eventName: string, callback: (...args: any[]) => void) { private registerCustomEvent(eventName: string, callback: (...args: unknown[]) => void) {
try { try {
const ref = (this.app.workspace as any).on?.(eventName, callback); const ref = (this.app.workspace as WorkspaceWithCustomEvents).on(eventName, callback);
if (ref) this.eventRefs.push(ref); if (ref) this.eventRefs.push(ref);
} catch (error) { } catch (error) {
console.warn(`Failed to register event ${eventName}:`, error); console.warn(`Failed to register event ${eventName}:`, error);
@ -309,7 +326,7 @@ export class ExplorerView extends ItemView {
// 在新視圖中開啟資料夾 // 在新視圖中開啟資料夾
private openFolderInNewView(folderPath: string) { private openFolderInNewView(folderPath: string) {
const { workspace } = this.app; const { workspace } = this.app;
let leaf: any = null; let leaf: WorkspaceLeaf | null = null;
switch (this.plugin.settings.defaultOpenLocation) { switch (this.plugin.settings.defaultOpenLocation) {
case 'left': case 'left':
leaf = workspace.getLeftLeaf(false); leaf = workspace.getLeftLeaf(false);
@ -323,9 +340,9 @@ export class ExplorerView extends ItemView {
break; break;
} }
if (!leaf) leaf = workspace.getLeaf('tab'); if (!leaf) leaf = workspace.getLeaf('tab');
leaf.setViewState({ type: 'grid-view', active: true }); void leaf.setViewState({ type: 'grid-view', active: true });
if (leaf.view instanceof GridView) { if (leaf.view instanceof GridView) {
leaf.view.setSource('folder', folderPath); void leaf.view.setSource('folder', folderPath);
} }
void workspace.revealLeaf(leaf); void workspace.revealLeaf(leaf);
} }
@ -333,7 +350,7 @@ export class ExplorerView extends ItemView {
// 在新視圖中開啟模式 // 在新視圖中開啟模式
private openModeInNewView(mode: string) { private openModeInNewView(mode: string) {
const { workspace } = this.app; const { workspace } = this.app;
let leaf: any = null; let leaf: WorkspaceLeaf | null = null;
switch (this.plugin.settings.defaultOpenLocation) { switch (this.plugin.settings.defaultOpenLocation) {
case 'left': case 'left':
leaf = workspace.getLeftLeaf(false); leaf = workspace.getLeftLeaf(false);
@ -347,9 +364,9 @@ export class ExplorerView extends ItemView {
break; break;
} }
if (!leaf) leaf = workspace.getLeaf('tab'); if (!leaf) leaf = workspace.getLeaf('tab');
leaf.setViewState({ type: 'grid-view', active: true }); void leaf.setViewState({ type: 'grid-view', active: true });
if (leaf.view instanceof GridView) { if (leaf.view instanceof GridView) {
leaf.view.setSource(mode); void leaf.view.setSource(mode);
} }
void workspace.revealLeaf(leaf); void workspace.revealLeaf(leaf);
} }
@ -410,7 +427,7 @@ export class ExplorerView extends ItemView {
// 創建搜尋容器 // 創建搜尋容器
private createSearchContainer(contentEl: HTMLElement) { private createSearchContainer(contentEl: HTMLElement) {
this.searchContainerEl = contentEl.createDiv({ cls: 'ge-explorer-search-container' }); this.searchContainerEl = contentEl.createDiv({ cls: 'ge-explorer-search-container' });
this.searchInputEl = this.searchContainerEl.createEl('input', { type: 'text' }) as HTMLInputElement; this.searchInputEl = this.searchContainerEl.createEl('input', { type: 'text' });
this.searchInputEl.addClass('ge-explorer-search-input'); this.searchInputEl.addClass('ge-explorer-search-input');
this.searchInputEl.placeholder = t?.('search') || 'Search'; this.searchInputEl.placeholder = t?.('search') || 'Search';
this.searchInputEl.value = this.searchQuery; this.searchInputEl.value = this.searchQuery;
@ -458,7 +475,7 @@ export class ExplorerView extends ItemView {
if (!this.isComposing) { if (!this.isComposing) {
this.keepSearchFocus = true; this.keepSearchFocus = true;
this.scheduleRender(); this.scheduleRender();
this.app.workspace.requestSaveLayout(); void this.app.workspace.requestSaveLayout();
} }
} }
@ -483,7 +500,7 @@ export class ExplorerView extends ItemView {
if (this.searchInputEl) this.searchInputEl.value = ''; if (this.searchInputEl) this.searchInputEl.value = '';
clearBtn.removeClass('show'); clearBtn.removeClass('show');
this.scheduleRender(); this.scheduleRender();
this.app.workspace.requestSaveLayout(); void this.app.workspace.requestSaveLayout();
window.setTimeout(() => this.searchInputEl?.focus(), 0); window.setTimeout(() => this.searchInputEl?.focus(), 0);
} }
@ -503,15 +520,15 @@ export class ExplorerView extends ItemView {
setIcon(searchIcon, 'search'); setIcon(searchIcon, 'search');
const searchName = searchItem.createSpan({ cls: 'ge-explorer-folder-name' }); const searchName = searchItem.createSpan({ cls: 'ge-explorer-folder-name' });
searchName.textContent = `${t ? t('search_for') : 'Search for'} "${trimmed}"`; searchName.textContent = `${t ? t('search_for') : 'Search for'} "${trimmed}"`;
searchItem.addEventListener('click', async (evt) => { searchItem.addEventListener('click', (evt) => {
evt.stopPropagation(); evt.stopPropagation();
await this.openFolderSearch(trimmed); void this.openFolderSearch(trimmed);
}); });
// 鍵盤支援Enter 觸發、ArrowUp 返回輸入框、Escape 清空並返回 // 鍵盤支援Enter 觸發、ArrowUp 返回輸入框、Escape 清空並返回
searchItem.addEventListener('keydown', async (evt: KeyboardEvent) => { searchItem.addEventListener('keydown', (evt: KeyboardEvent) => {
if (evt.key === 'Enter') { if (evt.key === 'Enter') {
evt.preventDefault(); evt.preventDefault();
await this.openFolderSearch(trimmed); void this.openFolderSearch(trimmed);
} else if (evt.key === 'ArrowUp') { } else if (evt.key === 'ArrowUp') {
evt.preventDefault(); evt.preventDefault();
this.searchInputEl?.focus(); this.searchInputEl?.focus();
@ -523,7 +540,7 @@ export class ExplorerView extends ItemView {
clearBtn?.removeClass('show'); clearBtn?.removeClass('show');
this.scheduleRender(); this.scheduleRender();
// 通知 Obsidian 保存視圖狀態 // 通知 Obsidian 保存視圖狀態
this.app.workspace.requestSaveLayout(); void this.app.workspace.requestSaveLayout();
window.setTimeout(() => this.searchInputEl?.focus(), 0); window.setTimeout(() => this.searchInputEl?.focus(), 0);
} }
}); });
@ -630,16 +647,16 @@ export class ExplorerView extends ItemView {
} }
// 獲取自定義模式項目 // 獲取自定義模式項目
private getCustomModeItems(settings: any) { private getCustomModeItems(settings: GallerySettings): ModeItem[] {
const customModes = (settings?.customModes ?? []).filter((cm: any) => cm?.enabled !== false); const customModes = settings.customModes.filter((cm: CustomMode) => cm.enabled !== false);
return customModes return customModes
.filter((cm: any) => { .filter((cm: CustomMode) => {
if (!this.isFiltering()) return true; if (!this.isFiltering()) return true;
const baseLabel = cm.displayName || cm.internalName || 'Custom'; const baseLabel = cm.displayName || cm.internalName || 'Custom';
return this.matchesQuery(baseLabel); return this.matchesQuery(baseLabel);
}) })
.map((cm: any) => { .map((cm: CustomMode) => {
const baseLabel = cm.displayName || cm.internalName || 'Custom'; const baseLabel = cm.displayName || cm.internalName || 'Custom';
const internalName = cm.internalName || `custom-${baseLabel}`; const internalName = cm.internalName || `custom-${baseLabel}`;
const textIcon = cm.icon ? `${cm.icon} ` : ''; const textIcon = cm.icon ? `${cm.icon} ` : '';
@ -647,13 +664,13 @@ export class ExplorerView extends ItemView {
key: internalName, key: internalName,
label: `${textIcon}${baseLabel}`, label: `${textIcon}${baseLabel}`,
icon: '', icon: '',
onClick: () => this.openMode(internalName) onClick: () => { void this.openMode(internalName); }
}; };
}); });
} }
// 獲取內建模式項目 // 獲取內建模式項目
private getBuiltinModeItems(settings: any) { private getBuiltinModeItems(settings: GallerySettings): ModeItem[] {
const builtInModes = this.getEnabledBuiltInModes(settings); const builtInModes = this.getEnabledBuiltInModes(settings);
return builtInModes return builtInModes
@ -661,12 +678,12 @@ export class ExplorerView extends ItemView {
.map(m => { .map(m => {
const emoji = this.BUILTIN_MODE_EMOJIS[m.key] || ''; const emoji = this.BUILTIN_MODE_EMOJIS[m.key] || '';
const label = emoji ? `${emoji} ${m.label}` : m.label; const label = emoji ? `${emoji} ${m.label}` : m.label;
return { key: m.key, label, icon: '', onClick: () => this.openMode(m.key) }; return { key: m.key, label, icon: '', onClick: () => { void this.openMode(m.key); } };
}); });
} }
// 獲取已啟用的內建模式 // 獲取已啟用的內建模式
private getEnabledBuiltInModes(settings: any) { private getEnabledBuiltInModes(settings: GallerySettings) {
const builtInCandidates = [ const builtInCandidates = [
{ key: 'bookmarks', label: t?.('bookmarks_mode') || 'Bookmarks', icon: 'bookmark', enabled: !!settings?.showBookmarksMode }, { key: 'bookmarks', label: t?.('bookmarks_mode') || 'Bookmarks', icon: 'bookmark', enabled: !!settings?.showBookmarksMode },
{ key: 'search', label: t?.('search_results') || 'Search', icon: 'search', enabled: !!settings?.showSearchMode }, { key: 'search', label: t?.('search_results') || 'Search', icon: 'search', enabled: !!settings?.showSearchMode },
@ -689,13 +706,13 @@ export class ExplorerView extends ItemView {
// 恢復滾動位置 // 恢復滾動位置
private restoreScrollPosition(contentEl: HTMLElement, prevScrollTop: number) { private restoreScrollPosition(contentEl: HTMLElement, prevScrollTop: number) {
contentEl.scrollTop = prevScrollTop; contentEl.scrollTop = prevScrollTop;
requestAnimationFrame(() => { window.requestAnimationFrame(() => {
contentEl.scrollTop = prevScrollTop; contentEl.scrollTop = prevScrollTop;
}); });
} }
// 渲染模式群組 // 渲染模式群組
private renderModesGroup(contentEl: HTMLElement, groupKey: string, title: string, iconName: string, items: Array<{ key?: string; label: string; icon: string; onClick: () => void }>) { private renderModesGroup(contentEl: HTMLElement, groupKey: string, title: string, iconName: string, items: ModeItem[]) {
if (items.length === 0) return; if (items.length === 0) return;
const nodeEl = contentEl.createDiv({ cls: 'ge-explorer-folder-node' }); const nodeEl = contentEl.createDiv({ cls: 'ge-explorer-folder-node' });
@ -730,7 +747,7 @@ export class ExplorerView extends ItemView {
} }
// 渲染模式項目 // 渲染模式項目
private renderModeItems(children: HTMLElement, items: Array<{ key?: string; label: string; icon: string; onClick: () => void }>, groupKey: string) { private renderModeItems(children: HTMLElement, items: ModeItem[], groupKey: string) {
const { currentMode } = this.getCurrentGridState(); const { currentMode } = this.getCurrentGridState();
items.forEach(({ key, label, icon, onClick }) => { items.forEach(({ key, label, icon, onClick }) => {
@ -795,16 +812,16 @@ export class ExplorerView extends ItemView {
item.setTitle(t?.('edit_custom_mode') || 'Edit Custom Mode') item.setTitle(t?.('edit_custom_mode') || 'Edit Custom Mode')
.setIcon('settings') .setIcon('settings')
.onClick(() => { .onClick(() => {
const modeIndex = this.plugin.settings.customModes.findIndex((m: any) => m.internalName === key); const modeIndex = this.plugin.settings.customModes.findIndex((m: CustomMode) => m.internalName === key);
if (modeIndex === -1) return; if (modeIndex === -1) return;
new CustomModeModal(this.app, this.plugin, this.plugin.settings.customModes[modeIndex], (result: any) => { new CustomModeModal(this.app, this.plugin, this.plugin.settings.customModes[modeIndex], (result: CustomMode) => {
this.plugin.settings.customModes[modeIndex] = result; this.plugin.settings.customModes[modeIndex] = result;
void this.plugin.saveSettings(); void this.plugin.saveSettings();
this.scheduleRender(); this.scheduleRender();
}).open(); }).open();
}); });
}); });
menu.showAtMouseEvent(evt as MouseEvent); menu.showAtMouseEvent(evt);
}); });
} }
@ -840,7 +857,7 @@ export class ExplorerView extends ItemView {
const view = await this.plugin.activateView(); const view = await this.plugin.activateView();
if (view instanceof GridView) { if (view instanceof GridView) {
await view.clearSelection(); view.clearSelection();
await view.setSource('', '', true, searchTerm); await view.setSource('', '', true, searchTerm);
} }
} }
@ -877,7 +894,7 @@ export class ExplorerView extends ItemView {
// 高亮 vault 根(當前為 folder 模式且路徑為根) // 高亮 vault 根(當前為 folder 模式且路徑為根)
const vaultRoot = this.app.vault.getRoot(); const vaultRoot = this.app.vault.getRoot();
const rootPath = (vaultRoot as any).path ?? '/'; const rootPath = vaultRoot.path || '/';
if (currentMode === 'folder' && (currentPath === rootPath || currentPath === '' || currentPath === '/')) { if (currentMode === 'folder' && (currentPath === rootPath || currentPath === '' || currentPath === '/')) {
foldersHeader.addClass('is-active'); foldersHeader.addClass('is-active');
} }
@ -921,7 +938,7 @@ export class ExplorerView extends ItemView {
// 開啟 Vault 根目錄 // 開啟 Vault 根目錄
const root = this.app.vault.getRoot(); const root = this.app.vault.getRoot();
const view = await this.plugin.activateView(); const view = await this.plugin.activateView();
if (view instanceof GridView) await view.setSource('folder', (root as any).path ?? '/'); if (view instanceof GridView) await view.setSource('folder', root.path || '/');
} else { } else {
// 點擊空白區域:展開/收合 // 點擊空白區域:展開/收合
const newExpanded = !this.isExpanded(foldersGroupKey); const newExpanded = !this.isExpanded(foldersGroupKey);
@ -1010,6 +1027,7 @@ export class ExplorerView extends ItemView {
const toggle = header.createSpan({ cls: 'ge-explorer-folder-toggle' }); const toggle = header.createSpan({ cls: 'ge-explorer-folder-toggle' });
const icon = header.createSpan({ cls: 'ge-explorer-folder-icon' }); const icon = header.createSpan({ cls: 'ge-explorer-folder-icon' });
setIcon(icon, 'folder');
const name = header.createSpan({ cls: 'ge-explorer-folder-name' }); const name = header.createSpan({ cls: 'ge-explorer-folder-name' });
name.textContent = folder.name || '/'; name.textContent = folder.name || '/';
@ -1049,12 +1067,12 @@ export class ExplorerView extends ItemView {
const noteFile = this.app.vault.getAbstractFileByPath(notePath); const noteFile = this.app.vault.getAbstractFileByPath(notePath);
if (noteFile instanceof TFile) { if (noteFile instanceof TFile) {
const metadata = this.app.metadataCache.getFileCache(noteFile)?.frontmatter; const metadata = this.app.metadataCache.getFileCache(noteFile)?.frontmatter;
const colorValue = (metadata as any)?.color as string | undefined; const colorValue: unknown = metadata?.color;
if (colorValue) { if (typeof colorValue === 'string' && colorValue) {
name.parentElement?.addClass(`ge-folder-color-${colorValue}`); name.parentElement?.addClass(`ge-folder-color-${colorValue}`);
} }
const iconValue = (metadata as any)?.icon as string | undefined; const iconValue: unknown = metadata?.icon;
if (iconValue && name) { if (typeof iconValue === 'string' && iconValue && name) {
name.textContent = `${iconValue} ${folder.name || '/'}`; name.textContent = `${iconValue} ${folder.name || '/'}`;
return true; return true;
} }
@ -1206,7 +1224,7 @@ export class ExplorerView extends ItemView {
} }
// 其他情況:直接開啟該資料夾的 GridView // 其他情況:直接開啟該資料夾的 GridView
this.openFolderInGrid(folder.path); void this.openFolderInGrid(folder.path);
} }
// 渲染暫存區群組 // 渲染暫存區群組
@ -1278,9 +1296,9 @@ export class ExplorerView extends ItemView {
menu.addItem((item) => { menu.addItem((item) => {
item.setTitle(t('save_stash_as_markdown')) item.setTitle(t('save_stash_as_markdown'))
.setIcon('file-plus') .setIcon('file-plus')
.onClick(() => this.saveStashAsMarkdown()); .onClick(() => { void this.saveStashAsMarkdown(); });
}); });
menu.showAtMouseEvent(evt as MouseEvent); menu.showAtMouseEvent(evt);
}); });
this.setupStashDropTarget(nodeEl); this.setupStashDropTarget(nodeEl);
@ -1290,8 +1308,6 @@ export class ExplorerView extends ItemView {
private setupStashDropTarget(nodeEl: HTMLElement) { private setupStashDropTarget(nodeEl: HTMLElement) {
if (!Platform.isDesktop) return; if (!Platform.isDesktop) return;
let currentDropTarget: HTMLElement | null = null;
let insertPosition: 'before' | 'after' | 'end' = 'end';
let targetIndex = -1; let targetIndex = -1;
nodeEl.addEventListener('dragover', (e: DragEvent) => { nodeEl.addEventListener('dragover', (e: DragEvent) => {
@ -1324,8 +1340,6 @@ export class ExplorerView extends ItemView {
}); });
if (closestItem && closestDistance < 50) { // 50px 容忍範圍 if (closestItem && closestDistance < 50) { // 50px 容忍範圍
currentDropTarget = closestItem;
insertPosition = insertBefore ? 'before' : 'after';
// 顯示插入指示器 // 顯示插入指示器
if (insertBefore) { if (insertBefore) {
@ -1335,8 +1349,6 @@ export class ExplorerView extends ItemView {
} }
} else { } else {
// 沒有找到合適的項目,插入到最後 // 沒有找到合適的項目,插入到最後
currentDropTarget = null;
insertPosition = 'end';
targetIndex = stashItems.length; targetIndex = stashItems.length;
nodeEl.addClass('ge-dragover'); nodeEl.addClass('ge-dragover');
} }
@ -1351,22 +1363,20 @@ export class ExplorerView extends ItemView {
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
this.clearStashInsertIndicators(nodeEl); this.clearStashInsertIndicators(nodeEl);
nodeEl.removeClass('ge-dragover'); nodeEl.removeClass('ge-dragover');
currentDropTarget = null;
} }
}); });
nodeEl.addEventListener('drop', async (e: DragEvent) => { nodeEl.addEventListener('drop', (e: DragEvent) => {
if (e.dataTransfer?.types?.includes('application/obsidian-ge-stash')) return; void (async () => {
e.preventDefault(); if (e.dataTransfer?.types?.includes('application/obsidian-ge-stash')) return;
e.preventDefault();
this.clearStashInsertIndicators(nodeEl);
nodeEl.removeClass('ge-dragover'); this.clearStashInsertIndicators(nodeEl);
nodeEl.removeClass('ge-dragover');
await this.handleStashDrop(e, targetIndex);
await this.handleStashDrop(e, targetIndex);
currentDropTarget = null; targetIndex = -1;
insertPosition = 'end'; })();
targetIndex = -1;
}); });
} }
@ -1404,6 +1414,7 @@ export class ExplorerView extends ItemView {
// 渲染暫存拖放區 // 渲染暫存拖放區
private renderStashDropzone(children: HTMLElement) { private renderStashDropzone(children: HTMLElement) {
const dropzone = children.createDiv({ cls: 'ge-explorer-folder-header ge-explorer-mode-item ge-explorer-stash-dropzone' }); const dropzone = children.createDiv({ cls: 'ge-explorer-folder-header ge-explorer-mode-item ge-explorer-stash-dropzone' });
dropzone.addClass('ge-explorer-stash-dropzone-clickable');
const dzIcon = dropzone.createSpan({ cls: 'ge-explorer-folder-icon' }); const dzIcon = dropzone.createSpan({ cls: 'ge-explorer-folder-icon' });
setIcon(dzIcon, 'plus'); setIcon(dzIcon, 'plus');
const dzName = dropzone.createSpan({ cls: 'ge-explorer-folder-name' }); const dzName = dropzone.createSpan({ cls: 'ge-explorer-folder-name' });
@ -1411,8 +1422,6 @@ export class ExplorerView extends ItemView {
// 讓 dropzone 可點擊以選擇檔案加入 // 讓 dropzone 可點擊以選擇檔案加入
// CSS 對 .ge-explorer-stash-dropzone 設了 pointer-events: none; 這裡強制開啟 // CSS 對 .ge-explorer-stash-dropzone 設了 pointer-events: none; 這裡強制開啟
(dropzone as HTMLElement).style.pointerEvents = 'auto';
(dropzone as HTMLElement).style.cursor = 'pointer';
dropzone.setAttr('role', 'button'); dropzone.setAttr('role', 'button');
dropzone.setAttr('tabindex', '0'); dropzone.setAttr('tabindex', '0');
dropzone.addEventListener('click', (evt) => { dropzone.addEventListener('click', (evt) => {
@ -1568,7 +1577,7 @@ export class ExplorerView extends ItemView {
// 捷徑檔:啟用 GridView 並嘗試以捷徑邏輯開啟 // 捷徑檔:啟用 GridView 並嘗試以捷徑邏輯開啟
const view = await this.plugin.activateView(); const view = await this.plugin.activateView();
if (view instanceof GridView) { if (view instanceof GridView) {
if (!(view as any).openShortcutFile || !(view as any).openShortcutFile(file)) { if (!view.openShortcutFile(file)) {
void this.app.workspace.getLeaf().openFile(file); void this.app.workspace.getLeaf().openFile(file);
} }
} else { } else {
@ -1591,7 +1600,7 @@ export class ExplorerView extends ItemView {
.setIcon('x') .setIcon('x')
.onClick(() => this.removeFromStash(file.path)); .onClick(() => this.removeFromStash(file.path));
}); });
menu.showAtMouseEvent(evt as MouseEvent); menu.showAtMouseEvent(evt);
}); });
} }
@ -1683,7 +1692,7 @@ export class ExplorerView extends ItemView {
newList.splice(insertIndex, 0, sourcePath); newList.splice(insertIndex, 0, sourcePath);
this.stashFilePaths = newList; this.stashFilePaths = newList;
this.app.workspace.requestSaveLayout(); void this.app.workspace.requestSaveLayout();
this.persistStashToSettings(); this.persistStashToSettings();
this.scheduleRender(); this.scheduleRender();
} }
@ -1707,7 +1716,7 @@ export class ExplorerView extends ItemView {
// 與一般文字處理一致:先嘗試絕對路徑,再用 linkpath 解析 // 與一般文字處理一致:先嘗試絕對路徑,再用 linkpath 解析
let file = this.app.vault.getAbstractFileByPath(p); let file = this.app.vault.getAbstractFileByPath(p);
if (!(file instanceof TFile)) { if (!(file instanceof TFile)) {
const alt = (this.app.metadataCache as any).getFirstLinkpathDest?.(p, srcPath); const alt = this.app.metadataCache.getFirstLinkpathDest(p, srcPath);
if (alt instanceof TFile) file = alt; if (alt instanceof TFile) file = alt;
} }
if (file instanceof TFile) resolved.push(file.path); if (file instanceof TFile) resolved.push(file.path);
@ -1740,14 +1749,14 @@ export class ExplorerView extends ItemView {
if (line.startsWith('[[') && line.endsWith(']]')) { if (line.startsWith('[[') && line.endsWith(']]')) {
const inner = line.slice(2, -2); const inner = line.slice(2, -2);
const parsed = parseLinktext(inner); const parsed = parseLinktext(inner);
const dest = (this.app.metadataCache as any).getFirstLinkpathDest?.(parsed.path, srcPath); const dest = this.app.metadataCache.getFirstLinkpathDest(parsed.path, srcPath);
if (dest instanceof TFile) resolved = dest; if (dest instanceof TFile) resolved = dest;
} else { } else {
const direct = this.app.vault.getAbstractFileByPath(line); const direct = this.app.vault.getAbstractFileByPath(line);
if (direct instanceof TFile) { if (direct instanceof TFile) {
resolved = direct; resolved = direct;
} else { } else {
const dest = (this.app.metadataCache as any).getFirstLinkpathDest?.(line, srcPath); const dest = this.app.metadataCache.getFirstLinkpathDest(line, srcPath);
if (dest instanceof TFile) resolved = dest; if (dest instanceof TFile) resolved = dest;
} }
} }
@ -1798,7 +1807,7 @@ export class ExplorerView extends ItemView {
this.stashFilePaths = [...this.stashFilePaths, ...validPaths]; this.stashFilePaths = [...this.stashFilePaths, ...validPaths];
} }
this.app.workspace.requestSaveLayout(); void this.app.workspace.requestSaveLayout();
this.persistStashToSettings(); this.persistStashToSettings();
this.scheduleRender(); this.scheduleRender();
} }
@ -1806,7 +1815,7 @@ export class ExplorerView extends ItemView {
// 從暫存區移除 // 從暫存區移除
private removeFromStash(path: string) { private removeFromStash(path: string) {
this.stashFilePaths = this.stashFilePaths.filter(p => p !== path); this.stashFilePaths = this.stashFilePaths.filter(p => p !== path);
this.app.workspace.requestSaveLayout(); void this.app.workspace.requestSaveLayout();
this.persistStashToSettings(); this.persistStashToSettings();
this.scheduleRender(); this.scheduleRender();
} }
@ -1814,7 +1823,7 @@ export class ExplorerView extends ItemView {
// 清空暫存區 // 清空暫存區
private clearStash() { private clearStash() {
this.stashFilePaths = []; this.stashFilePaths = [];
this.app.workspace.requestSaveLayout(); void this.app.workspace.requestSaveLayout();
this.persistStashToSettings(); this.persistStashToSettings();
this.scheduleRender(); this.scheduleRender();
} }
@ -1885,8 +1894,8 @@ export class ExplorerView extends ItemView {
menu.addItem((item) => { menu.addItem((item) => {
item.setTitle(t('new_note')) item.setTitle(t('new_note'))
.setIcon('square-pen') .setIcon('square-pen')
.onClick(async () => { .onClick(() => {
await createNewNote(this.app, folder.path); void createNewNote(this.app, folder.path);
}); });
}); });
@ -1894,8 +1903,8 @@ export class ExplorerView extends ItemView {
menu.addItem((item) => { menu.addItem((item) => {
item.setTitle(t('new_folder')) item.setTitle(t('new_folder'))
.setIcon('folder') .setIcon('folder')
.onClick(async () => { .onClick(() => {
await createNewFolder(this.app, folder.path, () => { void createNewFolder(this.app, folder.path, () => {
this.scheduleRender(); this.scheduleRender();
}); });
}); });
@ -1905,8 +1914,8 @@ export class ExplorerView extends ItemView {
menu.addItem((item) => { menu.addItem((item) => {
item.setTitle(t('new_canvas')) item.setTitle(t('new_canvas'))
.setIcon('layout-dashboard') .setIcon('layout-dashboard')
.onClick(async () => { .onClick(() => {
await createNewCanvas(this.app, folder.path); void createNewCanvas(this.app, folder.path);
}); });
}); });
@ -1914,8 +1923,8 @@ export class ExplorerView extends ItemView {
menu.addItem((item) => { menu.addItem((item) => {
item.setTitle(t('new_base')) item.setTitle(t('new_base'))
.setIcon('layout-dashboard') .setIcon('layout-dashboard')
.onClick(async () => { .onClick(() => {
await createNewBase(this.app, folder.path); void createNewBase(this.app, folder.path);
}); });
}); });
@ -1923,9 +1932,9 @@ export class ExplorerView extends ItemView {
menu.addItem((item) => { menu.addItem((item) => {
item.setTitle(t('new_shortcut')) item.setTitle(t('new_shortcut'))
.setIcon('shuffle') .setIcon('shuffle')
.onClick(async () => { .onClick(() => {
const modal = new ShortcutSelectionModal(this.app, this.plugin, async (option) => { const modal = new ShortcutSelectionModal(this.app, this.plugin, (option) => {
await createShortcut(this.app, folder.path, option); void createShortcut(this.app, folder.path, option);
}); });
modal.open(); modal.open();
}); });
@ -1962,15 +1971,17 @@ export class ExplorerView extends ItemView {
menu.addItem((item) => { menu.addItem((item) => {
item.setTitle(t('open_folder_note')) item.setTitle(t('open_folder_note'))
.setIcon('panel-left-open') .setIcon('panel-left-open')
.onClick(async () => { .onClick(() => {
const view = await this.plugin.activateView(); void (async () => {
if (view instanceof GridView) { const view = await this.plugin.activateView();
if (!view.openShortcutFile(noteFile)) { if (view instanceof GridView) {
if (!view.openShortcutFile(noteFile)) {
void this.app.workspace.getLeaf().openFile(noteFile);
}
} else {
void this.app.workspace.getLeaf().openFile(noteFile); void this.app.workspace.getLeaf().openFile(noteFile);
} }
} else { })();
void this.app.workspace.getLeaf().openFile(noteFile);
}
}); });
}); });
@ -1978,11 +1989,13 @@ export class ExplorerView extends ItemView {
menu.addItem((item) => { menu.addItem((item) => {
item.setTitle(t('edit_folder_note_settings')) item.setTitle(t('edit_folder_note_settings'))
.setIcon('settings-2') .setIcon('settings-2')
.onClick(async () => { .onClick(() => {
const view = await this.plugin.activateView(); void (async () => {
if (view instanceof GridView) { const view = await this.plugin.activateView();
showFolderNoteSettingsModal(this.app, this.plugin, folder, view); if (view instanceof GridView) {
} showFolderNoteSettingsModal(this.app, this.plugin, folder, view);
}
})();
}); });
}); });
@ -2006,11 +2019,13 @@ export class ExplorerView extends ItemView {
menu.addItem((item) => { menu.addItem((item) => {
item.setTitle(t('create_folder_note')) item.setTitle(t('create_folder_note'))
.setIcon('file-cog') .setIcon('file-cog')
.onClick(async () => { .onClick(() => {
const view = await this.plugin.activateView(); void (async () => {
if (view instanceof GridView) { const view = await this.plugin.activateView();
showFolderNoteSettingsModal(this.app, this.plugin, folder, view); if (view instanceof GridView) {
} showFolderNoteSettingsModal(this.app, this.plugin, folder, view);
}
})();
}); });
}); });
} }
@ -2027,13 +2042,15 @@ export class ExplorerView extends ItemView {
menu.addItem((item) => { menu.addItem((item) => {
item.setTitle(isIgnored ? t('unignore_folder') : t('ignore_folder')) item.setTitle(isIgnored ? t('unignore_folder') : t('ignore_folder'))
.setIcon(isIgnored ? 'folder-up' : 'folder-x') .setIcon(isIgnored ? 'folder-up' : 'folder-x')
.onClick(async () => { .onClick(() => {
if (isIgnored) { void (async () => {
this.plugin.settings.ignoredFolders = this.plugin.settings.ignoredFolders.filter(p => p !== folder.path); if (isIgnored) {
} else { this.plugin.settings.ignoredFolders = this.plugin.settings.ignoredFolders.filter(p => p !== folder.path);
this.plugin.settings.ignoredFolders.push(folder.path); } else {
} this.plugin.settings.ignoredFolders.push(folder.path);
await this.plugin.saveSettings(); }
await this.plugin.saveSettings();
})();
}); });
}); });
@ -2041,11 +2058,13 @@ export class ExplorerView extends ItemView {
menu.addItem((item) => { menu.addItem((item) => {
item.setTitle(t('move_folder')) item.setTitle(t('move_folder'))
.setIcon('folder-cog') .setIcon('folder-cog')
.onClick(async () => { .onClick(() => {
const view = await this.plugin.activateView(); void (async () => {
if (view instanceof GridView) { const view = await this.plugin.activateView();
new showFolderMoveModal(this.plugin, folder, view).open(); if (view instanceof GridView) {
} new showFolderMoveModal(this.plugin, folder, view).open();
}
})();
}); });
}); });
@ -2053,21 +2072,23 @@ export class ExplorerView extends ItemView {
menu.addItem((item) => { menu.addItem((item) => {
item.setTitle(t('rename_folder')) item.setTitle(t('rename_folder'))
.setIcon('file-cog') .setIcon('file-cog')
.onClick(async () => { .onClick(() => {
const view = await this.plugin.activateView(); void (async () => {
if (view instanceof GridView) { const view = await this.plugin.activateView();
showFolderRenameModal(this.app, this.plugin, folder, view); if (view instanceof GridView) {
} showFolderRenameModal(this.app, this.plugin, folder, view);
}
})();
}); });
}); });
// 刪除資料夾 // 刪除資料夾
menu.addItem((item) => { menu.addItem((item) => {
(item as any).setWarning(true); item.setWarning(true)
item.setTitle(t('delete_folder')) .setTitle(t('delete_folder'))
.setIcon('trash') .setIcon('trash')
.onClick(async () => { .onClick(() => {
await this.app.fileManager.trashFile(folder); void this.app.fileManager.trashFile(folder);
}); });
}); });
} }
@ -2134,7 +2155,7 @@ export class ExplorerView extends ItemView {
// 如果狀態確實發生變化,請求保存版面配置 // 如果狀態確實發生變化,請求保存版面配置
// 這樣下次開啟時可以恢復展開狀態 // 這樣下次開啟時可以恢復展開狀態
if (before !== after) { if (before !== after) {
this.app.workspace.requestSaveLayout(); void this.app.workspace.requestSaveLayout();
} }
} }
@ -2146,7 +2167,9 @@ export class ExplorerView extends ItemView {
headerEl.addEventListener('dragover', this.handleDragOver.bind(this)); headerEl.addEventListener('dragover', this.handleDragOver.bind(this));
headerEl.addEventListener('dragleave', this.handleDragLeave.bind(this)); headerEl.addEventListener('dragleave', this.handleDragLeave.bind(this));
headerEl.addEventListener('drop', this.handleDrop.bind(this, folderPath)); headerEl.addEventListener('drop', (event) => {
void this.handleDrop(folderPath, event);
});
} }
// 處理拖拽懸停 // 處理拖拽懸停
@ -2169,12 +2192,12 @@ export class ExplorerView extends ItemView {
private async handleDrop(folderPath: string, evt: Event) { private async handleDrop(folderPath: string, evt: Event) {
const event = evt as DragEvent; const event = evt as DragEvent;
// 某些情況下事件非 DragEvent需防禦處理 // 某些情況下事件非 DragEvent需防禦處理
if (typeof (event as any).preventDefault === 'function') { if (typeof event.preventDefault === 'function') {
event.preventDefault(); event.preventDefault();
} }
// 確保從 header 本身移除樣式,而非子元素 // 確保從 header 本身移除樣式,而非子元素
const header = event.currentTarget as HTMLElement | null; const header = event.currentTarget as HTMLElement | null;
if (header && typeof (header as any).removeClass === 'function') { if (header && typeof header.removeClass === 'function') {
header.removeClass('ge-dragover'); header.removeClass('ge-dragover');
} }
@ -2209,14 +2232,14 @@ export class ExplorerView extends ItemView {
if (text.startsWith('[[') && text.endsWith(']]')) { if (text.startsWith('[[') && text.endsWith(']]')) {
const inner = text.slice(2, -2); const inner = text.slice(2, -2);
const parsed = parseLinktext(inner); const parsed = parseLinktext(inner);
const dest = (this.app.metadataCache as any).getFirstLinkpathDest?.(parsed.path, srcPath); const dest = this.app.metadataCache.getFirstLinkpathDest(parsed.path, srcPath);
if (dest instanceof TFile) resolvedFile = dest; if (dest instanceof TFile) resolvedFile = dest;
} else { } else {
const direct = this.app.vault.getAbstractFileByPath(text); const direct = this.app.vault.getAbstractFileByPath(text);
if (direct instanceof TFile) { if (direct instanceof TFile) {
resolvedFile = direct; resolvedFile = direct;
} else { } else {
const dest = (this.app.metadataCache as any).getFirstLinkpathDest?.(text, srcPath); const dest = this.app.metadataCache.getFirstLinkpathDest(text, srcPath);
if (dest instanceof TFile) resolvedFile = dest; if (dest instanceof TFile) resolvedFile = dest;
} }
} }

View file

@ -188,10 +188,9 @@ export class FloatingAudioPlayer {
public focus(): void { public focus(): void {
this.containerEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); this.containerEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
// 可以考慮添加一些視覺提示,例如短暫閃爍邊框 // 可以考慮添加一些視覺提示,例如短暫閃爍邊框
this.containerEl.style.transition = 'box-shadow 0.1s ease-in-out'; this.containerEl.addClass('ge-audio-focus-highlight');
this.containerEl.style.boxShadow = '0 0 10px 2px var(--interactive-accent)';
window.setTimeout(() => { window.setTimeout(() => {
this.containerEl.style.boxShadow = ''; this.containerEl.removeClass('ge-audio-focus-highlight');
}, 300); }, 300);
} }

View file

@ -32,6 +32,130 @@ interface FileRenderParams {
} }
// 定義網格視圖 // 定義網格視圖
type GridViewLeaf = WorkspaceLeaf & {
pinned?: boolean;
updateHeader?: () => void;
};
interface DataviewEventSource {
on(name: 'dataview:index-ready', callback: () => void): ReturnType<GridExplorerPlugin['app']['metadataCache']['on']>;
}
interface WorkspaceEventTrigger {
trigger?: (name: 'ge-grid-source-changed', data: { mode: string; path: string }) => void;
}
interface InternalPlugins {
plugins: {
bookmarks?: {
enabled?: boolean;
};
};
getPluginById?: (id: string) => {
instance?: {
openGlobalSearch?: (query: string) => void;
};
} | undefined;
}
interface AppWithInternalPlugins {
internalPlugins?: InternalPlugins;
}
interface CommandManager {
executeCommandById?: (id: string) => void;
}
interface AppWithCommands {
commands?: CommandManager;
}
interface AppWithDataviewPlugin {
plugins?: {
plugins?: {
dataview?: {
api?: unknown;
};
};
};
}
interface MenuItemWithWarning {
setWarning: (warning: boolean) => void;
}
interface ExplorerViewActions {
addToStash: (filePaths: string[]) => void;
refresh: () => void;
}
interface WorkspaceSplitWithChildren {
children?: unknown[];
}
interface NoteViewContainerWithKeydownHandler extends HTMLElement {
keydownHandler?: (e: KeyboardEvent) => void;
}
interface GridViewStateData {
sourceMode?: string;
sourcePath?: string;
baseSortType?: string;
sortType?: string;
searchQuery?: string;
searchCurrentLocationOnly?: boolean;
searchFilesNameOnly?: boolean;
searchMediaFiles?: boolean;
fileNameFilterQuery?: string;
includeMedia?: boolean;
minMode?: boolean;
showIgnoredItems?: boolean;
baseCardLayout?: 'horizontal' | 'vertical';
cardLayout?: 'horizontal' | 'vertical';
hideHeaderElements?: boolean;
showFileNameFilter?: boolean;
showDateDividers?: boolean;
showNoteTags?: boolean;
recentSources?: string[];
futureSources?: string[];
bookmarkGroupId?: string;
}
interface GridViewState {
state?: GridViewStateData;
}
interface ShortcutFrontmatter extends FrontMatterCache {
type?: unknown;
redirect?: unknown;
cardLayout?: unknown;
searchCurrentLocationOnly?: unknown;
searchFilesNameOnly?: unknown;
searchMediaFiles?: unknown;
}
function getFrontmatterValue(frontmatter: FrontMatterCache | undefined, key: string): unknown {
return frontmatter ? (frontmatter as Record<string, unknown>)[key] : undefined;
}
function formatFrontmatterValue(value: unknown): string {
if (typeof value === 'number') return value.toLocaleString();
if (typeof value === 'string') return value;
if (typeof value === 'boolean') return value ? 'true' : 'false';
if (typeof value === 'bigint') return value.toString();
if (typeof value === 'symbol' || typeof value === 'function') return '';
if (Array.isArray(value)) return value.map(item => formatFrontmatterValue(item)).join(', ');
if (value === null || value === undefined) return '';
if (typeof value === 'object') {
try {
return JSON.stringify(value);
} catch {
return '';
}
}
return '';
}
export class GridView extends ItemView { export class GridView extends ItemView {
plugin: GridExplorerPlugin; plugin: GridExplorerPlugin;
sourceMode: string = ''; // 模式選擇 sourceMode: string = ''; // 模式選擇
@ -103,11 +227,7 @@ export class GridView extends ItemView {
// 阻止內建快捷鍵與其他監聽器 // 阻止內建快捷鍵與其他監聽器
event.preventDefault(); event.preventDefault();
// 停止後續所有監聽器(包含 Obsidian 內建 hotkey // 停止後續所有監聽器(包含 Obsidian 內建 hotkey
if (typeof (event as any).stopImmediatePropagation === 'function') { event.stopImmediatePropagation();
(event as any).stopImmediatePropagation();
} else {
event.stopPropagation();
}
const parentPath = this.sourcePath.split('/').slice(0, -1).join('/') || '/'; const parentPath = this.sourcePath.split('/').slice(0, -1).join('/') || '/';
void this.setSource('folder', parentPath); void this.setSource('folder', parentPath);
this.clearSelection(); this.clearSelection();
@ -122,18 +242,14 @@ export class GridView extends ItemView {
if (this.selectedItemIndex >= 0 && this.selectedItemIndex < this.gridItems.length) { if (this.selectedItemIndex >= 0 && this.selectedItemIndex < this.gridItems.length) {
// 阻止可能的其他快捷鍵或後續處理,避免重複觸發 // 阻止可能的其他快捷鍵或後續處理,避免重複觸發
event.preventDefault(); event.preventDefault();
if (typeof (event as any).stopImmediatePropagation === 'function') { event.stopImmediatePropagation();
(event as any).stopImmediatePropagation();
} else {
event.stopPropagation();
}
this.gridItems[this.selectedItemIndex].click(); this.gridItems[this.selectedItemIndex].click();
} }
} }
}, true); }, true);
// 註冊鍵盤事件處理 // 註冊鍵盤事件處理
this.registerDomEvent(document, 'keydown', (event: KeyboardEvent) => { this.registerDomEvent(activeDocument, 'keydown', (event: KeyboardEvent) => {
// 只有當 GridView 是活動視圖時才處理鍵盤事件 // 只有當 GridView 是活動視圖時才處理鍵盤事件
if (this.app.workspace.getActiveViewOfType(GridView) === this) { if (this.app.workspace.getActiveViewOfType(GridView) === this) {
return handleKeyDown(this, event); return handleKeyDown(this, event);
@ -142,7 +258,7 @@ export class GridView extends ItemView {
// 監聽 dataview:index-ready // 監聽 dataview:index-ready
this.registerEvent( this.registerEvent(
(this.app.metadataCache as any).on('dataview:index-ready', () => { (this.app.metadataCache as unknown as DataviewEventSource).on('dataview:index-ready', () => {
if (this.sourceMode.startsWith('custom-')) { if (this.sourceMode.startsWith('custom-')) {
void this.render(); void this.render();
} }
@ -214,7 +330,7 @@ export class GridView extends ItemView {
// 判斷當前Leaf是否被釘選 // 判斷當前Leaf是否被釘選
isPinned(): boolean { isPinned(): boolean {
return (this.leaf as any)?.pinned ?? false; return (this.leaf as GridViewLeaf).pinned ?? false;
} }
// 將來源加入歷史記錄 // 將來源加入歷史記錄
@ -304,7 +420,7 @@ export class GridView extends ItemView {
let folderSort: string | undefined; let folderSort: string | undefined;
if (mdFile instanceof TFile) { if (mdFile instanceof TFile) {
const metadata = this.app.metadataCache.getFileCache(mdFile)?.frontmatter; const metadata = this.app.metadataCache.getFileCache(mdFile)?.frontmatter;
folderSort = metadata?.sort; folderSort = typeof metadata?.sort === 'string' ? metadata.sort : undefined;
if (metadata?.cardLayout === 'horizontal' || metadata?.cardLayout === 'vertical') { if (metadata?.cardLayout === 'horizontal' || metadata?.cardLayout === 'vertical') {
tempLayout = metadata.cardLayout as 'horizontal' | 'vertical'; tempLayout = metadata.cardLayout as 'horizontal' | 'vertical';
} }
@ -351,7 +467,7 @@ export class GridView extends ItemView {
// 發送自訂事件,通知 ExplorerView 目前來源已變更 // 發送自訂事件,通知 ExplorerView 目前來源已變更
try { try {
(this.app.workspace as any).trigger?.('ge-grid-source-changed', { (this.app.workspace as WorkspaceEventTrigger).trigger?.('ge-grid-source-changed', {
mode: this.sourceMode, mode: this.sourceMode,
path: this.sourcePath, path: this.sourcePath,
}); });
@ -414,7 +530,7 @@ export class GridView extends ItemView {
} }
// 創建內容區域 // 創建內容區域
const contentEl = this.containerEl.createDiv('view-content'); this.containerEl.createDiv('view-content');
// 取得置頂清單 // 取得置頂清單
if (this.sourceMode === 'folder' && this.sourcePath !== '/') { if (this.sourceMode === 'folder' && this.sourcePath !== '/') {
@ -430,11 +546,11 @@ export class GridView extends ItemView {
if (Array.isArray(metadata['pinned'])) { if (Array.isArray(metadata['pinned'])) {
if (this.plugin.settings.folderNoteDisplaySettings === 'pinned') { if (this.plugin.settings.folderNoteDisplaySettings === 'pinned') {
// 先過濾掉所有重複的資料夾筆記 // 先過濾掉所有重複的資料夾筆記
this.pinnedList = metadata['pinned'].filter((name: string) => name !== `${folderName}.md`); this.pinnedList = (metadata['pinned'] as string[]).filter((name: string) => name !== `${folderName}.md`);
// 將資料夾筆記添加到最前面 // 將資料夾筆記添加到最前面
this.pinnedList.unshift(`${folderName}.md`); this.pinnedList.unshift(`${folderName}.md`);
} else { } else {
this.pinnedList = metadata['pinned']; this.pinnedList = metadata['pinned'] as string[];
} }
} else if (this.plugin.settings.folderNoteDisplaySettings === 'pinned') { } else if (this.plugin.settings.folderNoteDisplaySettings === 'pinned') {
// 如果沒有置頂清單,則建立一個僅包含資料夾筆記的清單 // 如果沒有置頂清單,則建立一個僅包含資料夾筆記的清單
@ -446,7 +562,7 @@ export class GridView extends ItemView {
// 渲染網格內容 // 渲染網格內容
await this.grid_render(); await this.grid_render();
(this.leaf as any).updateHeader(); (this.leaf as GridViewLeaf).updateHeader?.();
// 如果有之前選中的檔案路徑,嘗試恢復選中狀態 // 如果有之前選中的檔案路徑,嘗試恢復選中狀態
@ -474,7 +590,7 @@ export class GridView extends ItemView {
attr: { attr: {
placeholder: t('file_name_filter_placeholder') placeholder: t('file_name_filter_placeholder')
} }
}) as HTMLInputElement; });
const searchButton = inputWrapper.createEl('button', { const searchButton = inputWrapper.createEl('button', {
cls: 'ge-file-filter-search clickable-icon', cls: 'ge-file-filter-search clickable-icon',
attr: { attr: {
@ -524,8 +640,8 @@ export class GridView extends ItemView {
updateClearButton(); updateClearButton();
scheduleFilteredRender(); scheduleFilteredRender();
}); });
filterInput.addEventListener('input', (event) => { filterInput.addEventListener('input', (event: InputEvent) => {
if (isComposingFilterText || (event as InputEvent).isComposing) return; if (isComposingFilterText || event.isComposing) return;
this.fileNameFilterQuery = filterInput.value; this.fileNameFilterQuery = filterInput.value;
updateClearButton(); updateClearButton();
scheduleFilteredRender(); scheduleFilteredRender();
@ -577,7 +693,7 @@ export class GridView extends ItemView {
if (fileNameFilterContainer) { if (fileNameFilterContainer) {
fileNameFilterContainer.style.display = this.hideHeaderElements || !show ? 'none' : 'block'; fileNameFilterContainer.style.display = this.hideHeaderElements || !show ? 'none' : 'block';
if (show && !this.hideHeaderElements) { if (show && !this.hideHeaderElements) {
const filterInput = fileNameFilterContainer.querySelector('.ge-file-filter-input') as HTMLInputElement | null; const filterInput = fileNameFilterContainer.querySelector<HTMLInputElement>('.ge-file-filter-input');
filterInput?.focus(); filterInput?.focus();
} }
} }
@ -622,15 +738,13 @@ export class GridView extends ItemView {
const imageAreaWidth = settings.imageAreaWidth; const imageAreaWidth = settings.imageAreaWidth;
const imageAreaHeight = this.cardLayout === 'vertical' ? settings.verticalImageAreaHeight : settings.imageAreaHeight; const imageAreaHeight = this.cardLayout === 'vertical' ? settings.verticalImageAreaHeight : settings.imageAreaHeight;
container.style.setProperty('--grid-item-width', gridItemWidth + 'px'); container.setCssProps({
if (gridItemHeight === 0 || this.minMode) { '--grid-item-width': `${gridItemWidth}px`,
container.style.setProperty('--grid-item-height', '100%'); '--grid-item-height': gridItemHeight === 0 || this.minMode ? '100%' : `${gridItemHeight}px`,
} else { '--image-area-width': `${imageAreaWidth}px`,
container.style.setProperty('--grid-item-height', gridItemHeight + 'px'); '--image-area-height': `${imageAreaHeight}px`,
} '--title-font-size': `${settings.titleFontSize}em`,
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 (this.cardLayout === 'vertical') {
@ -673,7 +787,7 @@ export class GridView extends ItemView {
this.gridItems = []; this.gridItems = [];
// 如果是書籤模式且書籤插件未啟用,顯示提示 // 如果是書籤模式且書籤插件未啟用,顯示提示
if (this.sourceMode === 'bookmarks' && !(this.app as any).internalPlugins.plugins.bookmarks?.enabled) { if (this.sourceMode === 'bookmarks' && !(this.app as AppWithInternalPlugins).internalPlugins?.plugins.bookmarks?.enabled) {
new Notice(t('bookmarks_plugin_disabled')); new Notice(t('bookmarks_plugin_disabled'));
return; return;
} }
@ -689,7 +803,7 @@ export class GridView extends ItemView {
} }
// 顯示資料夾 // 顯示資料夾
renderFolder(this, container); void renderFolder(this, container);
// 顯示檔案 // 顯示檔案
let files = await renderFiles(this, container); let files = await renderFiles(this, container);
@ -776,7 +890,7 @@ export class GridView extends ItemView {
let pEl: HTMLElement | null = null; let pEl: HTMLElement | null = null;
if (!this.minMode) { if (!this.minMode) {
let summaryField = this.plugin.settings.noteSummaryField || 'summary'; let summaryField = this.plugin.settings.noteSummaryField || 'summary';
let summaryValue = metadata?.[summaryField]; let summaryValue = formatFrontmatterValue(getFrontmatterValue(metadata, summaryField));
if (this.sourceMode.startsWith('custom-')) { if (this.sourceMode.startsWith('custom-')) {
// 自訂模式下,使用自訂的 fields 來顯示摘要 // 自訂模式下,使用自訂的 fields 來顯示摘要
const mode = this.plugin.settings.customModes.find(m => m.internalName === this.sourceMode); const mode = this.plugin.settings.customModes.find(m => m.internalName === this.sourceMode);
@ -856,33 +970,29 @@ export class GridView extends ItemView {
labelName = fieldKey; labelName = fieldKey;
} }
if (metadata?.[fieldKey] !== undefined && metadata?.[fieldKey] !== '' && metadata?.[fieldKey] !== null) { const rawValue = getFrontmatterValue(metadata, fieldKey);
if (rawValue !== undefined && rawValue !== '' && rawValue !== null) {
let value = metadata[fieldKey]; const value = formatFrontmatterValue(rawValue);
// 如果是數字,則加入千位分隔符號 // 如果是數字,則加入千位分隔符號
if (typeof metadata[fieldKey] === 'number') {
value = metadata[fieldKey].toLocaleString();
}
// 如果是陣列,則轉換為字串 // 如果是陣列,則轉換為字串
if (Array.isArray(metadata[fieldKey])) {
value = metadata[fieldKey].join(', ');
}
let outputValue: string | number | null = value; let outputValue: unknown = value;
if (calcExpr) { if (calcExpr) {
try { try {
const fn = new Function('value', 'metadata', 'app', 'dv', `return (${calcExpr});`); // eslint-disable-next-line @typescript-eslint/no-implied-eval
const fn = new Function('value', 'metadata', 'app', 'dv', `return (${calcExpr});`) as (value: string, metadata: FrontMatterCache | undefined, app: typeof this.app, dv: unknown) => unknown;
// 獲取 Dataview API // 獲取 Dataview API
const dvApi = this.app.plugins.plugins.dataview?.api; const dvApi = (this.app as AppWithDataviewPlugin).plugins?.plugins?.dataview?.api;
outputValue = fn(value, metadata, this.app, dvApi); outputValue = fn(value, metadata, this.app, dvApi);
} catch (error) { } catch (error) {
console.error('GridExplorer: evaluate displayName error', error); console.error('GridExplorer: evaluate displayName error', error);
} }
} }
fieldValues.push(`${labelName}: ${outputValue}`); fieldValues.push(`${labelName}: ${formatFrontmatterValue(outputValue)}`);
} }
}); });
@ -924,7 +1034,7 @@ export class GridView extends ItemView {
// 刪除註解及連結 // 刪除註解及連結
contentWithoutMediaLinks = contentWithoutMediaLinks contentWithoutMediaLinks = contentWithoutMediaLinks
.replace(/<!--[\s\S]*?-->/g, '') .replace(/<!--[\s\S]*?-->/g, '')
.replace(/!?\[([^\]]*)\]\([^)]+\)|!?\[\[([^\]]+)\]\]/g, (match, p1, p2) => { .replace(/!?\[([^\]]*)\]\([^)]+\)|!?\[\[([^\]]+)\]\]/g, (_match: string, p1?: string, p2?: string) => {
const linkText = p1 || p2 || ''; const linkText = p1 || p2 || '';
if (!linkText) return ''; if (!linkText) return '';
@ -958,21 +1068,25 @@ export class GridView extends ItemView {
} }
if (frontMatterInfo.exists) { if (frontMatterInfo.exists) {
const colorValue = metadata?.color; const colorValue = getFrontmatterValue(metadata, 'color');
if (colorValue) { if (colorValue) {
// 檢查是否為 HEX 色值 // 檢查是否為 HEX 色值
if (isHexColor(colorValue)) { if (typeof colorValue === 'string' && isHexColor(colorValue)) {
// 使用自訂 CSS 變數來設置 HEX 顏色 // 使用自訂 CSS 變數來設置 HEX 顏色
fileEl.addClass('ge-note-color-custom'); fileEl.addClass('ge-note-color-custom');
fileEl.style.setProperty('--ge-note-color-bg', hexToRgba(colorValue, 0.2)); fileEl.setCssProps({
fileEl.style.setProperty('--ge-note-color-border', hexToRgba(colorValue, 0.5)); '--ge-note-color-bg': hexToRgba(colorValue, 0.2),
'--ge-note-color-border': hexToRgba(colorValue, 0.5),
});
// 設置預覽內容文字顏色 // 設置預覽內容文字顏色
if (pEl) { if (pEl) {
pEl.addClass('ge-note-color-custom-text'); pEl.addClass('ge-note-color-custom-text');
pEl.style.setProperty('--ge-note-color-text', hexToRgba(colorValue, 0.7)); pEl.setCssProps({
'--ge-note-color-text': hexToRgba(colorValue, 0.7),
});
} }
} else { } else if (typeof colorValue === 'string') {
// 使用預設的 CSS 類別來設置顏色 // 使用預設的 CSS 類別來設置顏色
fileEl.addClass(`ge-note-color-${colorValue}`); fileEl.addClass(`ge-note-color-${colorValue}`);
@ -983,7 +1097,7 @@ export class GridView extends ItemView {
} }
} }
const titleField = this.plugin.settings.noteTitleField || 'title'; const titleField = this.plugin.settings.noteTitleField || 'title';
const titleValue = metadata?.[titleField]; const titleValue = formatFrontmatterValue(getFrontmatterValue(metadata, titleField));
if (titleValue) { if (titleValue) {
// 將標題文字設為 frontmatter 的 title // 將標題文字設為 frontmatter 的 title
if (titleEl) { if (titleEl) {
@ -991,7 +1105,7 @@ export class GridView extends ItemView {
} }
} }
const displayValue = metadata?.display; const displayValue = getFrontmatterValue(metadata, 'display');
if (displayValue === 'minimized') { if (displayValue === 'minimized') {
// 移除已建立的預覽段落 // 移除已建立的預覽段落
if (pEl) { if (pEl) {
@ -1002,15 +1116,15 @@ export class GridView extends ItemView {
if (imageAreaEl) { if (imageAreaEl) {
imageAreaEl.remove(); imageAreaEl.remove();
} }
fileEl.style.height = '100%'; fileEl.addClass('ge-grid-item-full-height');
} else if (displayValue === 'hidden') { } else if (displayValue === 'hidden') {
// 為隱藏的筆記添加特殊樣式類別 // 為隱藏的筆記添加特殊樣式類別
fileEl.addClass('ge-note-hidden'); fileEl.addClass('ge-note-hidden');
} }
// 如果 frontmatter 同時存在 type 與非空的 redirect視為捷徑檔將圖示設為 shuffle // 如果 frontmatter 同時存在 type 與非空的 redirect視為捷徑檔將圖示設為 shuffle
const redirectType = metadata?.type; const redirectType = getFrontmatterValue(metadata, 'type');
const redirectPath = metadata?.redirect; const redirectPath = getFrontmatterValue(metadata, 'redirect');
if (redirectType && typeof redirectPath === 'string' && redirectPath.trim() !== '') { if (redirectType && typeof redirectPath === 'string' && redirectPath.trim() !== '') {
const iconContainer = fileEl.querySelector('.ge-icon-container'); const iconContainer = fileEl.querySelector('.ge-icon-container');
if (iconContainer) { if (iconContainer) {
@ -1026,13 +1140,13 @@ export class GridView extends ItemView {
contentArea.createEl('p', { text: file.extension.toUpperCase() }); contentArea.createEl('p', { text: file.extension.toUpperCase() });
} }
setTooltip(fileEl as HTMLElement, `${file.name}`, { delay: 2000 }) setTooltip(fileEl, `${file.name}`, { delay: 2000 })
} }
// 顯示標籤(僅限 Markdown 檔案) // 顯示標籤(僅限 Markdown 檔案)
if (file.extension === 'md' && this.showNoteTags && !this.minMode) { if (file.extension === 'md' && this.showNoteTags && !this.minMode) {
const fileCache = this.app.metadataCache.getFileCache(file); const fileCache = this.app.metadataCache.getFileCache(file);
const displaySetting = fileCache?.frontmatter?.display; const displaySetting = getFrontmatterValue(fileCache?.frontmatter, 'display');
// 如果筆記是最小化就直接跳過標籤邏輯 // 如果筆記是最小化就直接跳過標籤邏輯
if (displaySetting !== 'minimized') { if (displaySetting !== 'minimized') {
@ -1040,7 +1154,7 @@ export class GridView extends ItemView {
const allTags = new Set<string>(); const allTags = new Set<string>();
// 從 frontmatter 獲取標籤 // 從 frontmatter 獲取標籤
let frontmatterTags = fileCache?.frontmatter?.tags || []; const frontmatterTags = getFrontmatterValue(fileCache?.frontmatter, 'tags');
// 處理不同的標籤格式 // 處理不同的標籤格式
if (typeof frontmatterTags === 'string') { if (typeof frontmatterTags === 'string') {
@ -1217,9 +1331,6 @@ export class GridView extends ItemView {
// 建立哨兵元素,用於觸發後續載入 // 建立哨兵元素,用於觸發後續載入
const sentinel = container.createDiv('ge-load-more-sentinel'); const sentinel = container.createDiv('ge-load-more-sentinel');
sentinel.style.height = '1px';
sentinel.style.width = '100%';
sentinel.style.flexShrink = '0';
let isLoading = false; let isLoading = false;
@ -1327,7 +1438,7 @@ export class GridView extends ItemView {
// 針對 iOS 設備進行特殊處理 // 針對 iOS 設備進行特殊處理
if (Platform.isIosApp) { if (Platform.isIosApp) {
pinDivider.style.width = 'calc(100% - 16px)'; pinDivider.addClass('ge-ios-divider');
} }
} }
@ -1360,8 +1471,9 @@ export class GridView extends ItemView {
: []; : [];
for (const fieldName of fieldNames) { for (const fieldName of fieldNames) {
const dateStr = metadata.frontmatter[fieldName]; const dateStr = getFrontmatterValue(metadata.frontmatter, fieldName);
if (dateStr) { if (dateStr) {
if (typeof dateStr !== 'string' && typeof dateStr !== 'number' && !(dateStr instanceof Date)) continue;
const date = new Date(dateStr); const date = new Date(dateStr);
if (!isNaN(date.getTime())) { if (!isNaN(date.getTime())) {
frontMatterDate = date; frontMatterDate = date;
@ -1409,7 +1521,7 @@ export class GridView extends ItemView {
// 針對 iOS 設備進行特殊處理 // 針對 iOS 設備進行特殊處理
if (Platform.isIosApp) { if (Platform.isIosApp) {
dateDivider.style.width = 'calc(100% - 16px)'; dateDivider.addClass('ge-ios-divider');
} }
} }
} }
@ -1696,7 +1808,7 @@ export class GridView extends ItemView {
if (idx !== -1) { if (idx !== -1) {
lineNumber = content.substring(0, idx).split('\n').length - 1; lineNumber = content.substring(0, idx).split('\n').length - 1;
await leaf.openFile(file, { eState: { line: lineNumber } }); await leaf.openFile(file, { eState: { line: lineNumber } });
(this.app as any)?.commands?.executeCommandById?.('editor:focus'); (this.app as AppWithCommands).commands?.executeCommandById?.('editor:focus');
return; return;
} }
// 若都找不到關鍵字,直接開檔 // 若都找不到關鍵字,直接開檔
@ -1866,7 +1978,7 @@ export class GridView extends ItemView {
// 刪除選項 // 刪除選項
menu.addItem((item) => { menu.addItem((item) => {
(item as any).setWarning(true); (item as MenuItemWithWarning).setWarning(true);
item item
.setTitle(t('delete_note')) .setTitle(t('delete_note'))
.setIcon("trash") .setIcon("trash")
@ -1896,7 +2008,7 @@ export class GridView extends ItemView {
const explorerLeaves = this.app.workspace.getLeavesOfType(EXPLORER_VIEW_TYPE); const explorerLeaves = this.app.workspace.getLeavesOfType(EXPLORER_VIEW_TYPE);
if (explorerLeaves.length === 0) { if (explorerLeaves.length === 0) {
new Notice('找不到 Explorer 視圖'); new Notice('找不到 explorer 視圖');
return; return;
} }
@ -1904,7 +2016,7 @@ export class GridView extends ItemView {
const explorerView = explorerLeaves[0].view as ExplorerView; const explorerView = explorerLeaves[0].view as ExplorerView;
if (!explorerView) { if (!explorerView) {
new Notice('無法訪問 Explorer 視圖'); new Notice('無法訪問 explorer 視圖');
return; return;
} }
@ -1912,10 +2024,10 @@ export class GridView extends ItemView {
const filePaths = files.map(file => file.path); const filePaths = files.map(file => file.path);
// 調用 ExplorerView 的 addToStash 方法 // 調用 ExplorerView 的 addToStash 方法
(explorerView as any).addToStash(filePaths); (explorerView as unknown as ExplorerViewActions).addToStash(filePaths);
// 強制立即重新渲染 ExplorerView 以確保畫面更新 // 強制立即重新渲染 ExplorerView 以確保畫面更新
(explorerView as any).refresh(); (explorerView as unknown as ExplorerViewActions).refresh();
// 顯示成功通知 // 顯示成功通知
new Notice(t('added_to_stash')); new Notice(t('added_to_stash'));
@ -2048,7 +2160,7 @@ export class GridView extends ItemView {
? Promise.resolve(mediaFiles.filter(f => isMediaFile(f))) ? Promise.resolve(mediaFiles.filter(f => isMediaFile(f)))
: getFiles(this, this.includeMedia).then(allFiles => allFiles.filter(f => isMediaFile(f))); : getFiles(this, this.includeMedia).then(allFiles => allFiles.filter(f => isMediaFile(f)));
getMediaFilesPromise.then(filteredMediaFiles => { void getMediaFilesPromise.then(filteredMediaFiles => {
// 找到當前檔案在媒體檔案列表中的索引 // 找到當前檔案在媒體檔案列表中的索引
const currentIndex = filteredMediaFiles.findIndex(f => f.path === file.path); const currentIndex = filteredMediaFiles.findIndex(f => f.path === file.path);
if (currentIndex === -1) return; if (currentIndex === -1) return;
@ -2083,16 +2195,17 @@ export class GridView extends ItemView {
} }
// 沒有找到已開啟的分頁,開新分頁 // 沒有找到已開啟的分頁,開新分頁
return this.app.workspace.getLeaf('tab'); return this.app.workspace.getLeaf('tab');
case 'split': case 'split': {
// 檢查是否已經有 split 視圖存在 // 檢查是否已經有 split 視圖存在
const mainEntry = this.app.workspace.rootSplit; const mainEntry = this.app.workspace.rootSplit as WorkspaceSplitWithChildren | undefined;
if (mainEntry && (mainEntry as any)?.children?.length > 1) { if ((mainEntry?.children?.length ?? 0) > 1) {
// 如果已經有 split直接在現有的 split 中開啟 // 如果已經有 split直接在現有的 split 中開啟
return this.app.workspace.getLeaf(false); return this.app.workspace.getLeaf(false);
} else { } else {
// 如果沒有 split創建新的 split // 如果沒有 split創建新的 split
return this.app.workspace.getLeaf('split'); return this.app.workspace.getLeaf('split');
} }
}
case 'newWindow': case 'newWindow':
return this.app.workspace.getLeaf('window'); return this.app.workspace.getLeaf('window');
case 'default': case 'default':
@ -2104,9 +2217,14 @@ export class GridView extends ItemView {
// 開啟捷徑檔案 // 開啟捷徑檔案
openShortcutFile(file: TFile): boolean { openShortcutFile(file: TFile): boolean {
const fileCache = this.app.metadataCache.getFileCache(file); const fileCache = this.app.metadataCache.getFileCache(file);
if (!fileCache?.frontmatter) return false; const frontmatter: ShortcutFrontmatter | undefined = fileCache?.frontmatter;
const redirectType = fileCache?.frontmatter?.type; if (!frontmatter) return false;
const redirectPath = fileCache?.frontmatter?.redirect;
const redirectType = typeof frontmatter.type === 'string' ? frontmatter.type : '';
const redirectPath = typeof frontmatter.redirect === 'string' ? frontmatter.redirect : '';
const cardLayout = frontmatter.cardLayout === 'horizontal' || frontmatter.cardLayout === 'vertical'
? frontmatter.cardLayout
: undefined;
if (redirectType && typeof redirectPath === 'string' && redirectPath.trim() !== '') { if (redirectType && typeof redirectPath === 'string' && redirectPath.trim() !== '') {
let target; let target;
@ -2142,7 +2260,6 @@ export class GridView extends ItemView {
void this.setSource('folder', redirectPath); void this.setSource('folder', redirectPath);
this.clearSelection(); this.clearSelection();
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
const cardLayout = fileCache?.frontmatter?.cardLayout;
if (cardLayout && cardLayout !== this.cardLayout) { if (cardLayout && cardLayout !== this.cardLayout) {
this.cardLayout = cardLayout; this.cardLayout = cardLayout;
void this.render(); void this.render();
@ -2157,7 +2274,6 @@ export class GridView extends ItemView {
void this.setSource(redirectPath); void this.setSource(redirectPath);
this.clearSelection(); this.clearSelection();
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
const cardLayout = fileCache?.frontmatter?.cardLayout;
if (cardLayout && cardLayout !== this.cardLayout) { if (cardLayout && cardLayout !== this.cardLayout) {
this.cardLayout = cardLayout; this.cardLayout = cardLayout;
void this.render(); void this.render();
@ -2165,9 +2281,9 @@ export class GridView extends ItemView {
}); });
return true; return true;
} else if (redirectType === 'search') { } else if (redirectType === 'search') {
const searchCurrentLocationOnly = fileCache?.frontmatter?.searchCurrentLocationOnly || false; const searchCurrentLocationOnly = frontmatter.searchCurrentLocationOnly === true;
const searchFilesNameOnly = fileCache?.frontmatter?.searchFilesNameOnly || false; const searchFilesNameOnly = frontmatter.searchFilesNameOnly === true;
const searchMediaFiles = fileCache?.frontmatter?.searchMediaFiles || false; const searchMediaFiles = frontmatter.searchMediaFiles === true;
void this.setSource('', '', true, redirectPath, searchCurrentLocationOnly, searchFilesNameOnly, searchMediaFiles); void this.setSource('', '', true, redirectPath, searchCurrentLocationOnly, searchFilesNameOnly, searchMediaFiles);
this.clearSelection(); this.clearSelection();
return true; return true;
@ -2241,38 +2357,39 @@ export class GridView extends ItemView {
const isInSidebar = this.leaf.getRoot() === this.app.workspace.leftSplit || const isInSidebar = this.leaf.getRoot() === this.app.workspace.leftSplit ||
this.leaf.getRoot() === this.app.workspace.rightSplit; this.leaf.getRoot() === this.app.workspace.rightSplit;
if (isInSidebar) { if (isInSidebar) {
scrollContainer.style.fontSize = '1em'; scrollContainer.addClass('ge-note-sidebar-scroll-container');
scrollContainer.style.backgroundColor = 'var(--background-secondary)';
} }
// 創建筆記內容容器 // 創建筆記內容容器
const noteContent = scrollContainer.createDiv('ge-note-content-container'); const noteContent = scrollContainer.createDiv('ge-note-content-container');
if (isInSidebar) { if (isInSidebar) {
noteContent.style.padding = '15px'; noteContent.addClass('ge-note-sidebar-content-container');
} }
// 在移動端添加滾動監聽,根據滾動方向控制導航欄顯示/隱藏 // 在移動端添加滾動監聽,根據滾動方向控制導航欄顯示/隱藏
if (Platform.isPhone) { if (Platform.isPhone) {
let lastScrollTop = 0; let lastScrollTop = 0;
const handleScroll = () => { const handleScroll = () => {
const mobileNavbar = document.querySelector('.mobile-navbar') as HTMLElement; const mobileNavbar = activeDocument.querySelector('.mobile-navbar') as HTMLElement;
if (!mobileNavbar) return; if (!mobileNavbar) return;
const currentScrollTop = scrollContainer.scrollTop; const currentScrollTop = scrollContainer.scrollTop;
// 往上捲(滾動位置增加)時隱藏導航欄 // 往上捲(滾動位置增加)時隱藏導航欄
if (currentScrollTop > lastScrollTop && currentScrollTop > 50) { if (currentScrollTop > lastScrollTop && currentScrollTop > 50) {
if (!document.body.classList.contains('is-floating-nav')) { if (!activeDocument.body.classList.contains('is-floating-nav')) {
mobileNavbar.style.transform = 'translateY(100%)'; mobileNavbar.setCssProps({ transform: 'translateY(100%)' });
} else { } else {
mobileNavbar.style.transform = 'translateY(200%)'; mobileNavbar.setCssProps({ transform: 'translateY(200%)' });
} }
mobileNavbar.style.transition = 'transform 0.3s ease-out'; mobileNavbar.setCssProps({ transition: 'transform 0.3s ease-out' });
} }
// 往下捲(滾動位置減少)時顯示導航欄 // 往下捲(滾動位置減少)時顯示導航欄
else if (currentScrollTop < lastScrollTop) { else if (currentScrollTop < lastScrollTop) {
mobileNavbar.style.transform = 'translateY(0)'; mobileNavbar.setCssProps({
mobileNavbar.style.transition = 'transform 0.3s ease-in'; transform: 'translateY(0)',
transition: 'transform 0.3s ease-in',
});
} }
lastScrollTop = currentScrollTop; lastScrollTop = currentScrollTop;
@ -2285,10 +2402,12 @@ export class GridView extends ItemView {
const activeView = this.app.workspace.getActiveViewOfType(GridView); const activeView = this.app.workspace.getActiveViewOfType(GridView);
// 如果當前活動視圖不是這個 GridView 實例,或者不在顯示筆記狀態 // 如果當前活動視圖不是這個 GridView 實例,或者不在顯示筆記狀態
if (activeView !== this || !this.isShowingNote) { if (activeView !== this || !this.isShowingNote) {
const navbar = document.querySelector('.mobile-navbar') as HTMLElement; const navbar = activeDocument.querySelector('.mobile-navbar') as HTMLElement;
if (navbar) { if (navbar) {
navbar.style.transform = 'translateY(0)'; navbar.setCssProps({
navbar.style.transition = 'transform 0.3s ease-in'; transform: 'translateY(0)',
transition: 'transform 0.3s ease-in',
});
} }
} }
}; };
@ -2324,7 +2443,7 @@ export class GridView extends ItemView {
for (const key of keys) { for (const key of keys) {
const item = metadataContent.createDiv('ge-note-metadata-item'); const item = metadataContent.createDiv('ge-note-metadata-item');
item.createSpan({ cls: 'ge-note-metadata-key', text: `${key}: ` }); item.createSpan({ cls: 'ge-note-metadata-key', text: `${key}: ` });
const value = frontmatter[key]; const value: unknown = frontmatter[key] as unknown;
const valueSpan = item.createSpan({ cls: 'ge-note-metadata-value' }); const valueSpan = item.createSpan({ cls: 'ge-note-metadata-value' });
const values = Array.isArray(value) ? value : [value]; const values = Array.isArray(value) ? value : [value];
@ -2393,7 +2512,7 @@ export class GridView extends ItemView {
}); });
tagEl.addEventListener('click', (e) => { tagEl.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
(this.app as any).internalPlugins?.getPluginById('global-search')?.instance?.openGlobalSearch('tag:#' + valStr); (this.app as AppWithInternalPlugins).internalPlugins?.getPluginById?.('global-search')?.instance?.openGlobalSearch?.('tag:#' + valStr);
}); });
} else { } else {
tagRegex.lastIndex = 0; tagRegex.lastIndex = 0;
@ -2411,7 +2530,7 @@ export class GridView extends ItemView {
}); });
tagEl.addEventListener('click', (e) => { tagEl.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
(this.app as any).internalPlugins?.getPluginById('global-search')?.instance?.openGlobalSearch('tag:#' + tagName); (this.app as AppWithInternalPlugins).internalPlugins?.getPluginById?.('global-search')?.instance?.openGlobalSearch?.('tag:#' + tagName);
}); });
lastIndex = tagRegex.lastIndex; lastIndex = tagRegex.lastIndex;
} }
@ -2538,7 +2657,7 @@ export class GridView extends ItemView {
// 當同時符合(例如內容很短)且往上拉時,視為上拉 // 當同時符合(例如內容很短)且往上拉時,視為上拉
isPullingUp = canPullUp && deltaY < 0; isPullingUp = canPullUp && deltaY < 0;
if (this.noteViewContainer) { if (this.noteViewContainer) {
this.noteViewContainer.style.transition = 'none'; this.noteViewContainer.setCssProps({ transition: 'none' });
} }
} else if ((isAtTop && !isAtBottom && deltaY < 0) || (isAtBottom && !isAtTop && deltaY > 0)) { } else if ((isAtTop && !isAtBottom && deltaY < 0) || (isAtBottom && !isAtTop && deltaY > 0)) {
// 往反方向正常滑動閱讀時,取消攔截 // 往反方向正常滑動閱讀時,取消攔截
@ -2555,7 +2674,7 @@ export class GridView extends ItemView {
} }
const resistance = 0.5; const resistance = 0.5;
const translateY = deltaY * resistance; const translateY = deltaY * resistance;
this.noteViewContainer.style.transform = `translateY(${translateY}px)`; this.noteViewContainer.setCssProps({ transform: `translateY(${translateY}px)` });
} }
}; };
@ -2573,23 +2692,31 @@ export class GridView extends ItemView {
if ((!isPullingUp && deltaY > 80) || (isPullingUp && deltaY < -80)) { if ((!isPullingUp && deltaY > 80) || (isPullingUp && deltaY < -80)) {
// 關閉筆記 // 關閉筆記
const targetY = isPullingUp ? '-100vh' : '100vh'; const targetY = isPullingUp ? '-100vh' : '100vh';
this.noteViewContainer.style.transition = 'transform 0.2s ease-out'; this.noteViewContainer.setCssProps({
this.noteViewContainer.style.transform = `translateY(${targetY})`; transition: 'transform 0.2s ease-out',
transform: `translateY(${targetY})`,
});
window.setTimeout(() => { window.setTimeout(() => {
this.hideNoteInGrid(); this.hideNoteInGrid();
if (this.noteViewContainer) { if (this.noteViewContainer) {
this.noteViewContainer.style.transform = ''; this.noteViewContainer.setCssProps({
this.noteViewContainer.style.transition = ''; transform: '',
transition: '',
});
} }
}, 200); }, 200);
} else { } else {
// 距離不夠,彈回原位 // 距離不夠,彈回原位
this.noteViewContainer.style.transition = 'transform 0.3s ease-out'; this.noteViewContainer.setCssProps({
this.noteViewContainer.style.transform = `translateY(0)`; transition: 'transform 0.3s ease-out',
transform: 'translateY(0)',
});
window.setTimeout(() => { window.setTimeout(() => {
if (this.noteViewContainer) { if (this.noteViewContainer) {
this.noteViewContainer.style.transform = ''; this.noteViewContainer.setCssProps({
this.noteViewContainer.style.transition = ''; transform: '',
transition: '',
});
} }
}, 300); }, 300);
} }
@ -2614,7 +2741,7 @@ export class GridView extends ItemView {
activeDocument.addEventListener('keydown', handleKeyDown); activeDocument.addEventListener('keydown', handleKeyDown);
// 儲存事件監聽器以便後續移除 // 儲存事件監聽器以便後續移除
(this.noteViewContainer as any).keydownHandler = handleKeyDown; (this.noteViewContainer as NoteViewContainerWithKeydownHandler).keydownHandler = handleKeyDown;
} }
// 隱藏筆記顯示 // 隱藏筆記顯示
@ -2625,14 +2752,16 @@ export class GridView extends ItemView {
if (Platform.isPhone) { if (Platform.isPhone) {
const mobileNavbar = activeDocument.querySelector('.mobile-navbar') as HTMLElement; const mobileNavbar = activeDocument.querySelector('.mobile-navbar') as HTMLElement;
if (mobileNavbar) { if (mobileNavbar) {
mobileNavbar.style.transform = 'translateY(0)'; mobileNavbar.setCssProps({
mobileNavbar.style.transition = 'transform 0.3s ease-in'; transform: 'translateY(0)',
transition: 'transform 0.3s ease-in',
});
} }
} }
if (this.noteViewContainer) { if (this.noteViewContainer) {
// 移除鍵盤事件監聽器 // 移除鍵盤事件監聽器
const keydownHandler = (this.noteViewContainer as any).keydownHandler; const keydownHandler = (this.noteViewContainer as NoteViewContainerWithKeydownHandler).keydownHandler;
if (keydownHandler) { if (keydownHandler) {
activeDocument.removeEventListener('keydown', keydownHandler); activeDocument.removeEventListener('keydown', keydownHandler);
} }
@ -2675,7 +2804,7 @@ export class GridView extends ItemView {
} }
// 讀取視圖狀態 // 讀取視圖狀態
async setState(state: any): Promise<void> { async setState(state: GridViewState): Promise<void> {
if (state?.state) { if (state?.state) {
this.sourceMode = state.state.sourceMode || 'folder'; this.sourceMode = state.state.sourceMode || 'folder';
this.sourcePath = state.state.sourcePath || '/'; this.sourcePath = state.state.sourcePath || '/';

View file

@ -1,4 +1,4 @@
import { Plugin, TFolder, TFile, Menu, WorkspaceLeaf } from 'obsidian'; import { EventRef, Menu, MenuItem, Plugin, TFile, TFolder, WorkspaceLeaf } from 'obsidian';
import { GridView } from './GridView'; import { GridView } from './GridView';
import { ExplorerView, EXPLORER_VIEW_TYPE } from './ExplorerView'; import { ExplorerView, EXPLORER_VIEW_TYPE } from './ExplorerView';
import { updateCustomDocumentExtensions, isMediaFile } from './utils/fileUtils'; import { updateCustomDocumentExtensions, isMediaFile } from './utils/fileUtils';
@ -7,6 +7,37 @@ import { showNoteSettingsModal } from './modal/noteSettingsModal';
import { GallerySettings, DEFAULT_SETTINGS, GridExplorerSettingTab } from './settings'; import { GallerySettings, DEFAULT_SETTINGS, GridExplorerSettingTab } from './settings';
import { t } from './translations'; import { t } from './translations';
interface MenuItemWithSubmenu extends MenuItem {
setSubmenu(): Menu;
}
type WorkspaceEventHandler = (name: string, callback: (...data: unknown[]) => unknown) => EventRef;
interface CanvasPosition {
x: number;
y: number;
}
interface CanvasDropNode {
file: TFile;
pos: CanvasPosition;
size: { width: number; height: number };
focus: boolean;
}
interface CanvasDropApi {
posFromEvt(evt: DragEvent): CanvasPosition;
createFileNode(node: CanvasDropNode): unknown;
addNode(node: unknown): void;
requestSave(): Promise<void>;
}
interface CanvasDropView {
containerEl?: HTMLElement;
canvas?: CanvasDropApi;
gridExplorerDropHandler?: boolean;
}
export default class GridExplorerPlugin extends Plugin { export default class GridExplorerPlugin extends Plugin {
settings: GallerySettings = DEFAULT_SETTINGS; settings: GallerySettings = DEFAULT_SETTINGS;
statusBarItem: HTMLElement | null = null; statusBarItem: HTMLElement | null = null;
@ -185,7 +216,7 @@ export default class GridExplorerPlugin extends Plugin {
item.setTitle(t('open_in_grid_view')); item.setTitle(t('open_in_grid_view'));
item.setIcon('grid'); item.setIcon('grid');
item.setSection?.("open"); item.setSection?.("open");
const ogSubmenu: Menu = (item as any).setSubmenu(); const ogSubmenu = (item as MenuItemWithSubmenu).setSubmenu();
ogSubmenu.addItem((item) => { ogSubmenu.addItem((item) => {
item item
.setTitle(t('show_note_in_grid_view')) .setTitle(t('show_note_in_grid_view'))
@ -306,8 +337,14 @@ export default class GridExplorerPlugin extends Plugin {
); );
// 註冊tag-wrangler右鍵選單 // 註冊tag-wrangler右鍵選單
const onWorkspaceEvent = this.app.workspace.on.bind(this.app.workspace) as WorkspaceEventHandler;
this.registerEvent( this.registerEvent(
(this.app.workspace as any).on('tag-wrangler:contextmenu', (menu: Menu, tagName: string) => { onWorkspaceEvent('tag-wrangler:contextmenu', (...data: unknown[]) => {
const [menu, tagName] = data;
if (!(menu instanceof Menu) || typeof tagName !== 'string') {
return;
}
// 截斷過長的文字,最多顯示 15 個字元 // 截斷過長的文字,最多顯示 15 個字元
const truncatedText = tagName.length > 15 ? tagName.substring(0, 15) + '...' : tagName; const truncatedText = tagName.length > 15 ? tagName.substring(0, 15) + '...' : tagName;
const menuItemTitle = t('search_selection_in_grid_view').replace('...', `「#${truncatedText}`); // 假設翻譯中有...代表要替換的部分,或者直接格式化 const menuItemTitle = t('search_selection_in_grid_view').replace('...', `「#${truncatedText}`); // 假設翻譯中有...代表要替換的部分,或者直接格式化
@ -344,7 +381,7 @@ export default class GridExplorerPlugin extends Plugin {
'.tag-pane-tag, /* 標籤面板中的 tag */\n' + '.tag-pane-tag, /* 標籤面板中的 tag */\n' +
'span.cm-hashtag, /* 編輯器中的 tag */\n' + 'span.cm-hashtag, /* 編輯器中的 tag */\n' +
'.metadata-property[data-property-key="tags"] .multi-select-pill /* 屬性面板中的 tag */' '.metadata-property[data-property-key="tags"] .multi-select-pill /* 屬性面板中的 tag */'
) as HTMLElement | null; );
if (!el) return; // 不是 tag 點擊就跳過 if (!el) return; // 不是 tag 點擊就跳過
// 取出 tag 名稱(去掉前導 # // 取出 tag 名稱(去掉前導 #
@ -352,23 +389,23 @@ export default class GridExplorerPlugin extends Plugin {
if (el.matches('span.cm-hashtag')) { if (el.matches('span.cm-hashtag')) {
// 編輯器模式:可能被拆成多個 span需組合 // 編輯器模式:可能被拆成多個 span需組合
const collect = [] as string[]; const collect = [] as string[];
let curr: HTMLElement | null = el; let curr: Element | null = el;
// 向左收集 // 向左收集
while (curr && curr.matches('span.cm-hashtag') && !curr.classList.contains('cm-formatting')) { while (curr && curr.matches('span.cm-hashtag') && !curr.classList.contains('cm-formatting')) {
collect.unshift(curr.textContent ?? ''); collect.unshift(curr.textContent ?? '');
curr = curr.previousElementSibling as HTMLElement | null; curr = curr.previousElementSibling;
if (curr && (!curr.matches('span.cm-hashtag') || curr.classList.contains('cm-formatting'))) break; if (curr && (!curr.matches('span.cm-hashtag') || curr.classList.contains('cm-formatting'))) break;
} }
// 向右收集 // 向右收集
curr = el.nextElementSibling as HTMLElement | null; curr = el.nextElementSibling;
while (curr && curr.matches('span.cm-hashtag') && !curr.classList.contains('cm-formatting')) { while (curr && curr.matches('span.cm-hashtag') && !curr.classList.contains('cm-formatting')) {
collect.push(curr.textContent ?? ''); collect.push(curr.textContent ?? '');
curr = curr.nextElementSibling as HTMLElement | null; curr = curr.nextElementSibling;
} }
tagName = collect.join('').trim(); tagName = collect.join('').trim();
} else if (el.classList.contains('tag-pane-tag')) { } else if (el.classList.contains('tag-pane-tag')) {
// Tag Pane 中的元素,需避開計數器 // Tag Pane 中的元素,需避開計數器
const inner = el.querySelector('.tag-pane-tag-text, .tree-item-inner-text') as HTMLElement | null; const inner = el.querySelector('.tag-pane-tag-text, .tree-item-inner-text');
tagName = inner?.textContent?.trim() ?? ''; tagName = inner?.textContent?.trim() ?? '';
} else if (el.matches('.metadata-property[data-property-key="tags"] .multi-select-pill')) { } else if (el.matches('.metadata-property[data-property-key="tags"] .multi-select-pill')) {
// Frontmatter/Properties view // Frontmatter/Properties view
@ -414,12 +451,12 @@ export default class GridExplorerPlugin extends Plugin {
// 使用索引對映實際路徑,避免依賴顯示文字(可能被其他外掛修改) // 使用索引對映實際路徑,避免依賴顯示文字(可能被其他外掛修改)
const parentEl = breadcrumbEl.parentElement; const parentEl = breadcrumbEl.parentElement;
// 僅取麵包屑元素,建立清單以取得被點擊元素的索引 // 僅取麵包屑元素,建立清單以取得被點擊元素的索引
const crumbs = Array.from(parentEl.querySelectorAll('.view-header-breadcrumb')) as HTMLElement[]; const crumbs = Array.from(parentEl.querySelectorAll('.view-header-breadcrumb'));
const clickedIndex = crumbs.indexOf(breadcrumbEl as HTMLElement); const clickedIndex = crumbs.indexOf(breadcrumbEl);
if (clickedIndex < 0) return; if (clickedIndex < 0) return;
// 嘗試取得目前活躍檔案 // 嘗試取得目前活躍檔案
let activeFile = this.app.workspace.getActiveFile(); const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) { if (!activeFile) {
// 若無活躍檔案,無法可靠對映麵包屑,結束處理 // 若無活躍檔案,無法可靠對映麵包屑,結束處理
return; return;
@ -427,7 +464,7 @@ export default class GridExplorerPlugin extends Plugin {
// 蒐集從根到當前檔案之父層的所有資料夾 // 蒐集從根到當前檔案之父層的所有資料夾
const folders: TFolder[] = []; const folders: TFolder[] = [];
let currFolder: TFolder | null = (activeFile instanceof TFile) ? activeFile.parent : (activeFile as unknown as TFolder); let currFolder: TFolder | null = activeFile.parent;
while (currFolder) { while (currFolder) {
folders.push(currFolder); folders.push(currFolder);
currFolder = currFolder.parent; currFolder = currFolder.parent;
@ -436,7 +473,7 @@ export default class GridExplorerPlugin extends Plugin {
// 以「去除根目錄('/')」後的資料夾清單,直接用索引對應麵包屑 // 以「去除根目錄('/')」後的資料夾清單,直接用索引對應麵包屑
const foldersWithoutRoot = folders.filter(f => f.path !== '/'); const foldersWithoutRoot = folders.filter(f => f.path !== '/');
const isClickingFileCrumb = (activeFile instanceof TFile) && (clickedIndex === crumbs.length - 1); const isClickingFileCrumb = clickedIndex === crumbs.length - 1;
let targetFolder: TFolder | undefined; let targetFolder: TFolder | undefined;
if (isClickingFileCrumb) { if (isClickingFileCrumb) {
@ -535,7 +572,7 @@ export default class GridExplorerPlugin extends Plugin {
private setupCanvasDropHandlers() { private setupCanvasDropHandlers() {
const setup = () => { const setup = () => {
this.app.workspace.getLeavesOfType('canvas').forEach(leaf => { this.app.workspace.getLeavesOfType('canvas').forEach(leaf => {
const canvasView = leaf.view as any; const canvasView = leaf.view as CanvasDropView;
if (canvasView.gridExplorerDropHandler) { if (canvasView.gridExplorerDropHandler) {
return; return;
} }
@ -566,9 +603,9 @@ export default class GridExplorerPlugin extends Plugin {
const data = evt.dataTransfer.getData('application/obsidian-grid-explorer-files'); const data = evt.dataTransfer.getData('application/obsidian-grid-explorer-files');
let filePaths: string[] = []; let filePaths: string[] = [];
try { try {
const parsed = JSON.parse(data); const parsed: unknown = JSON.parse(data);
if (Array.isArray(parsed)) { if (Array.isArray(parsed)) {
filePaths = parsed.filter((p: any) => typeof p === 'string'); filePaths = parsed.filter((p): p is string => typeof p === 'string');
} else if (typeof parsed === 'string') { } else if (typeof parsed === 'string') {
filePaths = [parsed]; filePaths = [parsed];
} }
@ -587,7 +624,7 @@ export default class GridExplorerPlugin extends Plugin {
} }
const pos = canvas.posFromEvt(evt); const pos = canvas.posFromEvt(evt);
let currentPos = { ...pos } as { x: number; y: number }; let currentPos = { ...pos };
// 逐一建立節點,若多檔則位置做些微偏移 // 逐一建立節點,若多檔則位置做些微偏移
for (let i = 0; i < filePaths.length; i++) { for (let i = 0; i < filePaths.length; i++) {
@ -748,7 +785,8 @@ export default class GridExplorerPlugin extends Plugin {
} }
async loadSettings() { async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); const loadedSettings = await this.loadData() as Partial<GallerySettings> | null;
this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedSettings ?? {});
updateCustomDocumentExtensions(this.settings); updateCustomDocumentExtensions(this.settings);
} }
@ -769,9 +807,9 @@ export default class GridExplorerPlugin extends Plugin {
const explorerLeaves = this.app.workspace.getLeavesOfType(EXPLORER_VIEW_TYPE); const explorerLeaves = this.app.workspace.getLeavesOfType(EXPLORER_VIEW_TYPE);
explorerLeaves.forEach(leaf => { explorerLeaves.forEach(leaf => {
if (leaf.view instanceof ExplorerView) { if (leaf.view instanceof ExplorerView) {
void (leaf.view as ExplorerView).refresh(); void (leaf.view).refresh();
} }
}); });
} }
} }
} }

View file

@ -1,6 +1,6 @@
import { App, Modal, Setting } from 'obsidian'; import { App, Modal, Setting, setIcon } from 'obsidian';
import GridExplorerPlugin from '../main'; import GridExplorerPlugin from '../main';
import { CustomMode } from '../settings'; import { CustomMode, CustomModeOption } from '../settings';
import { t } from '../translations'; import { t } from '../translations';
export class CustomModeModal extends Modal { export class CustomModeModal extends Modal {
@ -24,9 +24,9 @@ export class CustomModeModal extends Modal {
let icon = this.mode ? this.mode.icon : '🧩'; let icon = this.mode ? this.mode.icon : '🧩';
let displayName = this.mode ? this.mode.displayName : ''; let displayName = this.mode ? this.mode.displayName : '';
// 新增名稱欄位,用於原始 dataviewCode 的選項名稱 // 新增名稱欄位,用於原始 dataviewCode 的選項名稱
let name = this.mode && (this.mode as any).name ? (this.mode as any).name : t('default'); let name = this.mode?.name || t('default');
// 支援多個子選項,每個選項包含名稱與 Dataview 程式碼 // 支援多個子選項,每個選項包含名稱與 Dataview 程式碼
let options = this.mode?.options ? this.mode.options.map(opt => ({ ...opt })) : []; // 其他選項(不含 Default const options: CustomModeOption[] = this.mode?.options ? this.mode.options.map(opt => ({ ...opt })) : []; // 其他選項(不含 Default
// 向下相容:使用第一個選項作為主要 dataviewCode // 向下相容:使用第一個選項作為主要 dataviewCode
let dataviewCode = this.mode ? this.mode.dataviewCode : ''; let dataviewCode = this.mode ? this.mode.dataviewCode : '';
let enabled = this.mode ? (this.mode.enabled ?? true) : true; let enabled = this.mode ? (this.mode.enabled ?? true) : true;
@ -41,8 +41,7 @@ export class CustomModeModal extends Modal {
icon = value || '🧩'; icon = value || '🧩';
}); });
// 設置固定寬度,適合單個圖示 // 設置固定寬度,適合單個圖示
text.inputEl.style.width = '3em'; text.inputEl.addClass('ge-custommode-icon-input');
text.inputEl.style.minWidth = '3em';
}) })
.addText(text => { .addText(text => {
text.setValue(displayName) text.setValue(displayName)
@ -56,9 +55,7 @@ export class CustomModeModal extends Modal {
.setDesc(t('custom_mode_dataview_code_desc')); .setDesc(t('custom_mode_dataview_code_desc'));
// 使標題與描述佔據一行,輸入區域佔據了下一行 // 使標題與描述佔據一行,輸入區域佔據了下一行
dvSetting.settingEl.style.flexDirection = 'column'; dvSetting.settingEl.addClass('ge-custommode-dataview-setting');
dvSetting.settingEl.style.alignItems = 'stretch';
dvSetting.settingEl.style.gap = '0.5rem';
dvSetting.addText(text => { dvSetting.addText(text => {
text.setValue(name) text.setValue(name)
@ -71,10 +68,10 @@ export class CustomModeModal extends Modal {
.onChange(value => { .onChange(value => {
dataviewCode = value; dataviewCode = value;
}) })
.setPlaceholder('Dataview JS code'); .setPlaceholder('Dataview js code');
// 給TextArea有足夠的垂直空間和完整的寬度 // 給TextArea有足夠的垂直空間和完整的寬度
text.inputEl.setAttr('rows', 6); text.inputEl.setAttr('rows', 6);
text.inputEl.style.width = '100%'; text.inputEl.addClass('ge-custommode-code-input');
}); });
dvSetting.addText(text => { dvSetting.addText(text => {
@ -86,10 +83,7 @@ export class CustomModeModal extends Modal {
}); });
// 讓 Text 與 TextArea 在 control 區域各佔一行 // 讓 Text 與 TextArea 在 control 區域各佔一行
dvSetting.controlEl.style.display = 'flex'; dvSetting.controlEl.addClass('ge-custommode-dataview-controls');
dvSetting.controlEl.style.flexDirection = 'column';
dvSetting.controlEl.style.alignItems = 'stretch';
dvSetting.controlEl.style.gap = '0.5rem';
// ===== 選項管理區域 ===== // ===== 選項管理區域 =====
contentEl.createEl('h3', { text: t('custom_mode_sub_options') }); contentEl.createEl('h3', { text: t('custom_mode_sub_options') });
@ -126,11 +120,11 @@ export class CustomModeModal extends Modal {
}; };
const getDropIndex = (e: DragEvent): number => { const getDropIndex = (e: DragEvent): number => {
const containers = Array.from(optionsContainer.querySelectorAll('.ge-custommode-option-container')); const containers = Array.from(optionsContainer.querySelectorAll<HTMLElement>('.ge-custommode-option-container'));
const y = e.clientY; const y = e.clientY;
for (let i = 0; i < containers.length; i++) { for (let i = 0; i < containers.length; i++) {
const container = containers[i] as HTMLElement; const container = containers[i];
const rect = container.getBoundingClientRect(); const rect = container.getBoundingClientRect();
const midY = rect.top + rect.height / 2; const midY = rect.top + rect.height / 2;
@ -173,7 +167,7 @@ export class CustomModeModal extends Modal {
// 拖曳手柄 // 拖曳手柄
const dragHandle = headerEl.createDiv('ge-custommode-option-drag-handle'); const dragHandle = headerEl.createDiv('ge-custommode-option-drag-handle');
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/></svg>`; setIcon(dragHandle, 'grip-vertical');
// 選項名稱 // 選項名稱
const nameEl = headerEl.createDiv('ge-custommode-option-name'); const nameEl = headerEl.createDiv('ge-custommode-option-name');
@ -181,11 +175,11 @@ export class CustomModeModal extends Modal {
// 展開/摺疊按鈕 // 展開/摺疊按鈕
const toggleEl = headerEl.createDiv('ge-custommode-option-toggle'); const toggleEl = headerEl.createDiv('ge-custommode-option-toggle');
toggleEl.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9,18 15,12 9,6"></polyline></svg>`; setIcon(toggleEl, 'chevron-right');
// 刪除按鈕 // 刪除按鈕
const deleteEl = headerEl.createDiv('ge-custommode-option-delete'); const deleteEl = headerEl.createDiv('ge-custommode-option-delete');
deleteEl.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3,6 5,6 21,6"></polyline><path d="m19,6v14a2,2 0 0,1 -2,2H7a2,2 0 0,1 -2,-2V6m3,0V4a2,2 0 0,1 2,-2h4a2,2 0 0,1 2,2v2"></path></svg>`; setIcon(deleteEl, 'trash-2');
// 內容區域 // 內容區域
const contentEl = optionContainer.createDiv('ge-custommode-option-content'); const contentEl = optionContainer.createDiv('ge-custommode-option-content');
@ -201,13 +195,13 @@ export class CustomModeModal extends Modal {
}); });
}) })
.addTextArea(text => { .addTextArea(text => {
text.setPlaceholder('Dataview JS code') text.setPlaceholder('Dataview js code')
.setValue(opt.dataviewCode) .setValue(opt.dataviewCode)
.onChange(val => { .onChange(val => {
opt.dataviewCode = val; opt.dataviewCode = val;
}); });
text.inputEl.setAttr('rows', 6); text.inputEl.setAttr('rows', 6);
text.inputEl.style.width = '100%'; text.inputEl.addClass('ge-custommode-code-input');
}) })
.addText(text => { .addText(text => {
text.setPlaceholder(t('custom_mode_fields_placeholder')) text.setPlaceholder(t('custom_mode_fields_placeholder'))
@ -251,7 +245,7 @@ export class CustomModeModal extends Modal {
optionContainer.classList.add('dragging'); optionContainer.classList.add('dragging');
if (e.dataTransfer) { if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', optionContainer.outerHTML); e.dataTransfer.setData('text/plain', idx.toString());
} }
}); });
@ -293,6 +287,9 @@ export class CustomModeModal extends Modal {
// 保存展開狀態 // 保存展開狀態
const draggedExpanded = expandedStates[draggedIndex]; const draggedExpanded = expandedStates[draggedIndex];
const draggedOption = options[draggedIndex]; const draggedOption = options[draggedIndex];
if (!draggedOption) {
return;
}
// 從原位置移除 // 從原位置移除
options.splice(draggedIndex, 1); options.splice(draggedIndex, 1);

View file

@ -1,4 +1,4 @@
import { App, Modal, Setting, TFolder, TFile } from 'obsidian'; import { App, Modal, Setting, TFile, TFolder, TextComponent } from 'obsidian';
import GridExplorerPlugin from '../main'; import GridExplorerPlugin from '../main';
import { GridView } from '../GridView'; import { GridView } from '../GridView';
import { t } from '../translations'; import { t } from '../translations';
@ -11,6 +11,25 @@ export interface FolderNoteSettings {
cardLayout: '' | 'horizontal' | 'vertical'; cardLayout: '' | 'horizontal' | 'vertical';
} }
interface FolderNoteFrontmatter {
sort?: unknown;
color?: unknown;
icon?: unknown;
cardLayout?: unknown;
pinned?: unknown;
}
function getStringValue(value: unknown, fallback = ''): string {
return typeof value === 'string' ? value : fallback;
}
function getPinnedName(value: unknown): string {
if (typeof value === 'string' || typeof value === 'number') {
return value.toString();
}
return '';
}
export function showFolderNoteSettingsModal(app: App, plugin: GridExplorerPlugin, folder: TFolder, gridView: GridView) { export function showFolderNoteSettingsModal(app: App, plugin: GridExplorerPlugin, folder: TFolder, gridView: GridView) {
new FolderNoteSettingsModal(app, plugin, folder, gridView).open(); new FolderNoteSettingsModal(app, plugin, folder, gridView).open();
} }
@ -106,13 +125,13 @@ export class FolderNoteSettingsModal extends Modal {
}); });
// HEX 自訂顏色輸入欄位 // HEX 自訂顏色輸入欄位
let hexInput: any; let hexInput: TextComponent | null = null;
const hexSetting = new Setting(contentEl) new Setting(contentEl)
.setName(t('custom_hex_color')) .setName(t('custom_hex_color'))
.setDesc(t('custom_hex_color_desc')) .setDesc(t('custom_hex_color_desc'))
.addText(text => { .addText(text => {
hexInput = text; hexInput = text;
text.setPlaceholder('#FF8800 or #F80') text.setPlaceholder('#Ff8800 or #f80')
.setValue(isCurrentColorHex ? this.settings.color : '') .setValue(isCurrentColorHex ? this.settings.color : '')
.onChange(value => { .onChange(value => {
// 驗證 HEX 格式 // 驗證 HEX 格式
@ -196,36 +215,24 @@ export class FolderNoteSettingsModal extends Modal {
try { try {
// 使用 metadataCache 讀取筆記的 frontmatter // 使用 metadataCache 讀取筆記的 frontmatter
const fileCache = this.app.metadataCache.getFileCache(this.existingFile); const fileCache = this.app.metadataCache.getFileCache(this.existingFile);
if (fileCache && fileCache.frontmatter) { if (fileCache?.frontmatter) {
// 讀取排序設定 const frontmatter = fileCache.frontmatter as FolderNoteFrontmatter;
if ('sort' in fileCache.frontmatter) {
this.settings.sort = fileCache.frontmatter.sort || ''; this.settings.sort = getStringValue(frontmatter.sort);
} this.settings.color = getStringValue(frontmatter.color);
this.settings.icon = getStringValue(frontmatter.icon, '📁');
// 讀取顏色設定
if ('color' in fileCache.frontmatter) { const cardLayout = getStringValue(frontmatter.cardLayout);
this.settings.color = fileCache.frontmatter.color || ''; this.settings.cardLayout =
} cardLayout === 'horizontal' || cardLayout === 'vertical' ? cardLayout : '';
// 讀取圖示設定 if (Array.isArray(frontmatter.pinned)) {
if ('icon' in fileCache.frontmatter) { this.settings.isPinned = frontmatter.pinned.some((item: unknown) => {
this.settings.icon = fileCache.frontmatter.icon || '📁'; const pinnedName = getPinnedName(item);
}
// 讀取卡片樣式設定
if ('cardLayout' in fileCache.frontmatter) {
this.settings.cardLayout = fileCache.frontmatter.cardLayout || 'default';
}
// 讀取置頂設定
if (fileCache.frontmatter?.pinned && Array.isArray(fileCache.frontmatter.pinned)) {
this.settings.isPinned = fileCache.frontmatter.pinned.some((item: any) => {
if (!item) return false;
const pinnedName = item.toString();
const pinnedNameWithoutExt = pinnedName.replace(/\.\w+$/, ''); const pinnedNameWithoutExt = pinnedName.replace(/\.\w+$/, '');
return pinnedNameWithoutExt === this.folder.name; return pinnedNameWithoutExt === this.folder.name;
}); });
} }
} }
} catch (error) { } catch (error) {
console.error('無法讀取資料夾筆記設定', error); console.error('無法讀取資料夾筆記設定', error);
@ -252,51 +259,53 @@ export class FolderNoteSettingsModal extends Modal {
} }
// 使用 fileManager.processFrontMatter 更新 frontmatter // 使用 fileManager.processFrontMatter 更新 frontmatter
await this.app.fileManager.processFrontMatter(file, (frontmatter) => { await this.app.fileManager.processFrontMatter(file, (frontmatter: FolderNoteFrontmatter) => {
if (this.settings.sort) { if (this.settings.sort) {
frontmatter['sort'] = this.settings.sort; frontmatter.sort = this.settings.sort;
} else { } else {
delete frontmatter['sort']; delete frontmatter.sort;
} }
if (this.settings.color) { if (this.settings.color) {
frontmatter['color'] = this.settings.color; frontmatter.color = this.settings.color;
} else { } else {
delete frontmatter['color']; delete frontmatter.color;
} }
if (this.settings.icon && this.settings.icon !== '📁') { if (this.settings.icon && this.settings.icon !== '📁') {
frontmatter['icon'] = this.settings.icon; frontmatter.icon = this.settings.icon;
} else { } else {
delete frontmatter['icon']; delete frontmatter.icon;
} }
if (this.settings.cardLayout) { if (this.settings.cardLayout) {
frontmatter['cardLayout'] = this.settings.cardLayout; frontmatter.cardLayout = this.settings.cardLayout;
} else { } else {
delete frontmatter['cardLayout']; delete frontmatter.cardLayout;
} }
const folderName = `${this.folder.name}.md`; const folderName = `${this.folder.name}.md`;
if (this.settings.isPinned) { if (this.settings.isPinned) {
// 如果原本就有 pinned 陣列,則添加或更新 // 如果原本就有 pinned 陣列,則添加或更新
if (Array.isArray(frontmatter['pinned'])) { if (Array.isArray(frontmatter.pinned)) {
if (!frontmatter['pinned'].includes(folderName)) { const pinned = frontmatter.pinned.map(getPinnedName).filter(Boolean);
frontmatter['pinned'] = [folderName, ...frontmatter['pinned']]; if (!pinned.includes(folderName)) {
frontmatter.pinned = [folderName, ...pinned];
} }
} else { } else {
// 如果沒有 pinned 陣列,則創建一個新的 // 如果沒有 pinned 陣列,則創建一個新的
frontmatter['pinned'] = [folderName]; frontmatter.pinned = [folderName];
} }
} else if (Array.isArray(frontmatter['pinned'])) { } else if (Array.isArray(frontmatter.pinned)) {
// 如果取消置頂,則從陣列中移除 // 如果取消置頂,則從陣列中移除
frontmatter['pinned'] = frontmatter['pinned'].filter( const pinned = frontmatter.pinned
(item: any) => item !== folderName .map(getPinnedName)
); .filter(item => item !== folderName);
frontmatter.pinned = pinned;
// 如果陣列為空,則刪除該欄位 // 如果陣列為空,則刪除該欄位
if (frontmatter['pinned'].length === 0) { if (pinned.length === 0) {
delete frontmatter['pinned']; delete frontmatter.pinned;
} }
} }
}); });

View file

@ -4,6 +4,22 @@ import { GridView } from '../GridView';
import { t } from '../translations'; import { t } from '../translations';
import { isFolderIgnored } from '../utils/fileUtils'; import { isFolderIgnored } from '../utils/fileUtils';
type AppWithInternalPlugins = App & {
internalPlugins?: {
plugins?: {
bookmarks?: {
enabled?: boolean;
};
};
};
};
type SearchViewWithComponent = {
searchComponent?: {
inputEl?: HTMLInputElement;
};
};
// 顯示資料夾選擇 modal // 顯示資料夾選擇 modal
export function showFolderSelectionModal(app: App, plugin: GridExplorerPlugin, activeView?: GridView, buttonElement?: HTMLElement) { export function showFolderSelectionModal(app: App, plugin: GridExplorerPlugin, activeView?: GridView, buttonElement?: HTMLElement) {
new FolderSelectionModal(app, plugin, activeView, buttonElement).open(); new FolderSelectionModal(app, plugin, activeView, buttonElement).open();
@ -94,7 +110,7 @@ export class FolderSelectionModal extends Modal {
// 建立書籤選項 // 建立書籤選項
if (this.plugin.settings.showBookmarksMode) { if (this.plugin.settings.showBookmarksMode) {
const bookmarksPlugin = (this.app as any).internalPlugins.plugins.bookmarks; const bookmarksPlugin = (this.app as AppWithInternalPlugins).internalPlugins?.plugins?.bookmarks;
if (bookmarksPlugin?.enabled) { if (bookmarksPlugin?.enabled) {
const bookmarkOption = this.folderOptionsContainer.createEl('div', { const bookmarkOption = this.folderOptionsContainer.createEl('div', {
cls: 'ge-grid-view-folder-option', cls: 'ge-grid-view-folder-option',
@ -120,10 +136,10 @@ export class FolderSelectionModal extends Modal {
// 建立搜尋結果選項 // 建立搜尋結果選項
if (this.plugin.settings.showSearchMode) { if (this.plugin.settings.showSearchMode) {
const searchLeaf = (this.app as any).workspace.getLeavesOfType('search')[0]; const searchLeaf = this.app.workspace.getLeavesOfType('search')[0];
if (searchLeaf) { if (searchLeaf) {
const searchView = searchLeaf.view; const searchView = searchLeaf.view as SearchViewWithComponent;
const searchInputEl = searchView.searchComponent ? searchView.searchComponent.inputEl : null; const searchInputEl = searchView.searchComponent?.inputEl ?? null;
if (searchInputEl) { if (searchInputEl) {
if (searchInputEl.value.trim().length > 0) { if (searchInputEl.value.trim().length > 0) {
const searchOption = this.folderOptionsContainer.createEl('div', { const searchOption = this.folderOptionsContainer.createEl('div', {
@ -446,7 +462,7 @@ export class FolderSelectionModal extends Modal {
event.preventDefault(); event.preventDefault();
if (this.selectedIndex >= 0) { if (this.selectedIndex >= 0) {
const selectedOption = this.folderOptions[this.selectedIndex]; const selectedOption = this.folderOptions[this.selectedIndex];
if (selectedOption && selectedOption.style.display !== 'none') { if (selectedOption && !selectedOption.hasClass('ge-hidden')) {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
selectedOption.click(); selectedOption.click();
}); });
@ -508,7 +524,7 @@ export class FolderSelectionModal extends Modal {
// 獲取當前可見的選項 // 獲取當前可見的選項
getVisibleOptions(): HTMLElement[] { getVisibleOptions(): HTMLElement[] {
return this.folderOptions.filter(option => return this.folderOptions.filter(option =>
option.style.display !== 'none' !option.hasClass('ge-hidden')
); );
} }
@ -526,7 +542,7 @@ export class FolderSelectionModal extends Modal {
const dataPath = option.getAttribute('data-path'); const dataPath = option.getAttribute('data-path');
if (dataPath) { if (dataPath) {
// 這是一個資料夾選項 // 這是一個資料夾選項
const nameSpan = option.querySelector('span:last-child') as HTMLSpanElement | null; const nameSpan = option.querySelector<HTMLSpanElement>('span:last-child');
if (nameSpan) { if (nameSpan) {
if (searchTerm !== '') { if (searchTerm !== '') {
// 顯示完整路徑 // 顯示完整路徑
@ -538,7 +554,7 @@ export class FolderSelectionModal extends Modal {
} }
// 根據搜尋狀態調整前綴縮排ascii tree // 根據搜尋狀態調整前綴縮排ascii tree
const prefixSpan = option.querySelector('.ge-folder-tree-prefix') as HTMLSpanElement | null; const prefixSpan = option.querySelector<HTMLSpanElement>('.ge-folder-tree-prefix');
if (prefixSpan) { if (prefixSpan) {
if (searchTerm !== '') { if (searchTerm !== '') {
// 搜尋時移除縮排 // 搜尋時移除縮排
@ -555,10 +571,10 @@ export class FolderSelectionModal extends Modal {
const text = option.textContent?.toLowerCase() || ''; const text = option.textContent?.toLowerCase() || '';
const fullPath = option.getAttribute('data-path')?.toLowerCase() || ''; const fullPath = option.getAttribute('data-path')?.toLowerCase() || '';
if (searchTerm === '' || text.includes(searchTerm) || fullPath.includes(searchTerm)) { if (searchTerm === '' || text.includes(searchTerm) || fullPath.includes(searchTerm)) {
option.style.display = 'block'; option.removeClass('ge-hidden');
hasVisibleOptions = true; hasVisibleOptions = true;
} else { } else {
option.style.display = 'none'; option.addClass('ge-hidden');
} }
}); });

View file

@ -59,7 +59,7 @@ export class InputModal extends Modal {
attr: { attr: {
id: 'searchCurrentLocationOnly' id: 'searchCurrentLocationOnly'
} }
}) as HTMLInputElement; });
if (searchOptions.searchCurrentLocationOnly) { if (searchOptions.searchCurrentLocationOnly) {
searchScopeCheckbox.checked = true; searchScopeCheckbox.checked = true;
} }
@ -73,7 +73,7 @@ export class InputModal extends Modal {
attr: { attr: {
id: 'searchFilesNameOnly' id: 'searchFilesNameOnly'
} }
}) as HTMLInputElement; });
if (searchOptions.searchFilesNameOnly) { if (searchOptions.searchFilesNameOnly) {
searchNameCheckbox.checked = true; searchNameCheckbox.checked = true;
} }
@ -87,7 +87,7 @@ export class InputModal extends Modal {
attr: { attr: {
id: 'searchMediaFiles' id: 'searchMediaFiles'
} }
}) as HTMLInputElement; });
if (searchOptions.searchMediaFiles) { if (searchOptions.searchMediaFiles) {
searchMediaFilesCheckbox.checked = true; searchMediaFilesCheckbox.checked = true;
} }
@ -178,4 +178,4 @@ export function showUriInputModal(app: App, onSubmit: (uri: string) => void) {
inputType: 'url', inputType: 'url',
onSubmit onSubmit
}).open(); }).open();
} }

View file

@ -1,6 +1,12 @@
import { App, Modal, TFile, Menu, setIcon } from 'obsidian'; import { App, Modal, TFile, Menu, setIcon } from 'obsidian';
import { isImageFile, isVideoFile, isAudioFile } from '../utils/fileUtils'; import { isImageFile, isVideoFile, isAudioFile } from '../utils/fileUtils';
interface GridViewFocusTarget {
gridItems: HTMLElement[];
hasKeyboardFocus: boolean;
selectItem(index: number): void;
}
export class MediaModal extends Modal { export class MediaModal extends Modal {
private file: TFile; private file: TFile;
private mediaFiles: TFile[]; private mediaFiles: TFile[];
@ -8,7 +14,7 @@ export class MediaModal extends Modal {
private currentMediaElement: HTMLElement | null = null; private currentMediaElement: HTMLElement | null = null;
private isZoomed = false; private isZoomed = false;
private handleWheel: EventListener | null = null; private handleWheel: EventListener | null = null;
private gridView: any; // 儲存 GridView 實例的引用 private gridView?: GridViewFocusTarget; // 儲存 GridView 實例的引用
// 觸控拖曳相關屬性 // 觸控拖曳相關屬性
private touchStartX = 0; private touchStartX = 0;
@ -18,7 +24,7 @@ export class MediaModal extends Modal {
private minSwipeDistance = 50; // 最小滑動距離 private minSwipeDistance = 50; // 最小滑動距離
private maxSwipeTime = 300; // 最大滑動時間(毫秒) private maxSwipeTime = 300; // 最大滑動時間(毫秒)
constructor(app: App, file: TFile, mediaFiles: TFile[], gridView?: any) { constructor(app: App, file: TFile, mediaFiles: TFile[], gridView?: GridViewFocusTarget) {
super(app); super(app);
this.file = file; this.file = file;
this.mediaFiles = mediaFiles; this.mediaFiles = mediaFiles;
@ -34,8 +40,6 @@ export class MediaModal extends Modal {
// 設置 modal 樣式為全螢幕 // 設置 modal 樣式為全螢幕
contentEl.empty(); contentEl.empty();
contentEl.style.width = '100%';
contentEl.style.height = '100%';
contentEl.addClass('ge-media-modal-content'); contentEl.addClass('ge-media-modal-content');
// 創建媒體顯示區域 // 創建媒體顯示區域
@ -120,7 +124,7 @@ export class MediaModal extends Modal {
// 如果存在之前的滾輪事件處理程序,先移除它 // 如果存在之前的滾輪事件處理程序,先移除它
if (this.handleWheel) { if (this.handleWheel) {
const mediaView = contentEl.querySelector('.ge-media-view') as HTMLElement | null; const mediaView = contentEl.querySelector<HTMLElement>('.ge-media-view');
if (mediaView) { if (mediaView) {
mediaView.removeEventListener('wheel', this.handleWheel); mediaView.removeEventListener('wheel', this.handleWheel);
} }
@ -131,6 +135,7 @@ export class MediaModal extends Modal {
if (this.gridView) { if (this.gridView) {
// 找到當前媒體檔案在 GridView 中的索引 // 找到當前媒體檔案在 GridView 中的索引
const currentFile = this.mediaFiles[this.currentIndex]; const currentFile = this.mediaFiles[this.currentIndex];
if (!currentFile) return;
const gridItemIndex = this.gridView.gridItems.findIndex((item: HTMLElement) => const gridItemIndex = this.gridView.gridItems.findIndex((item: HTMLElement) =>
item.dataset.filePath === currentFile.path item.dataset.filePath === currentFile.path
); );
@ -170,9 +175,9 @@ export class MediaModal extends Modal {
if (isImageFile(mediaFile)) { if (isImageFile(mediaFile)) {
// 創建圖片元素 // 創建圖片元素
const img = document.createElement('img'); const img = activeDocument.createElement('img');
img.className = 'ge-fullscreen-image'; img.className = 'ge-fullscreen-image';
img.style.display = 'none'; // 先隱藏新圖片 img.addClass('ge-hidden'); // 先隱藏新圖片
img.src = this.app.vault.getResourcePath(mediaFile); img.src = this.app.vault.getResourcePath(mediaFile);
// 等待新圖片載入完成 // 等待新圖片載入完成
@ -185,7 +190,7 @@ export class MediaModal extends Modal {
// 設置圖片樣式,預設滿屏顯示 // 設置圖片樣式,預設滿屏顯示
this.resetImageStyles(img); this.resetImageStyles(img);
// 顯示新圖片 // 顯示新圖片
img.style.display = ''; img.removeClass('ge-hidden');
}; };
mediaContainer.appendChild(img); mediaContainer.appendChild(img);
@ -201,7 +206,7 @@ export class MediaModal extends Modal {
if (this.currentMediaElement) { if (this.currentMediaElement) {
this.currentMediaElement.remove(); this.currentMediaElement.remove();
} }
const video = document.createElement('video'); const video = activeDocument.createElement('video');
video.className = 'ge-fullscreen-video'; video.className = 'ge-fullscreen-video';
video.controls = true; video.controls = true;
video.autoplay = true; video.autoplay = true;
@ -219,7 +224,7 @@ export class MediaModal extends Modal {
if (isAudioFile(mediaFile)) { if (isAudioFile(mediaFile)) {
//顯示檔案名稱 //顯示檔案名稱
const fileName = mediaFile.name; const fileName = mediaFile.name;
const fileNameElement = document.createElement('div'); const fileNameElement = activeDocument.createElement('div');
fileNameElement.className = 'ge-fullscreen-file-name'; fileNameElement.className = 'ge-fullscreen-file-name';
fileNameElement.textContent = fileName; fileNameElement.textContent = fileName;
mediaContainer.appendChild(fileNameElement); mediaContainer.appendChild(fileNameElement);
@ -240,31 +245,35 @@ export class MediaModal extends Modal {
// 重設圖片樣式 // 重設圖片樣式
resetImageStyles(img: HTMLImageElement) { resetImageStyles(img: HTMLImageElement) {
const mediaView = this.contentEl.querySelector('.ge-media-view') as HTMLElement | null; const mediaView = this.contentEl.querySelector<HTMLElement>('.ge-media-view');
if (!mediaView) return; if (!mediaView) return;
img.style.width = 'auto'; img.setCssStyles({
img.style.height = 'auto'; width: 'auto',
img.style.maxWidth = '100vw'; height: 'auto',
img.style.maxHeight = '100vh'; maxWidth: '100vw',
img.style.position = 'absolute'; maxHeight: '100vh',
img.style.left = '50%'; position: 'absolute',
img.style.top = '50%'; left: '50%',
img.style.transform = 'translate(-50%, -50%)'; top: '50%',
img.style.cursor = 'zoom-in'; transform: 'translate(-50%, -50%)',
cursor: 'zoom-in',
});
(mediaView as HTMLElement).style.overflowX = 'hidden'; mediaView.setCssStyles({
(mediaView as HTMLElement).style.overflowY = 'hidden'; overflowX: 'hidden',
overflowY: 'hidden',
});
// 等待圖片載入完成後調整大小 // 等待圖片載入完成後調整大小
img.onload = () => { img.onload = () => {
if (mediaView.clientWidth > mediaView.clientHeight) { if (mediaView.clientWidth > mediaView.clientHeight) {
if (img.naturalHeight < mediaView.clientHeight) { if (img.naturalHeight < mediaView.clientHeight) {
img.style.height = '100%'; img.setCssStyles({ height: '100%' });
} }
} else { } else {
if (img.naturalWidth < mediaView.clientWidth) { if (img.naturalWidth < mediaView.clientWidth) {
img.style.width = '100%'; img.setCssStyles({ width: '100%' });
} }
} }
}; };
@ -273,11 +282,11 @@ export class MediaModal extends Modal {
if (img.complete) { if (img.complete) {
if (mediaView.clientWidth > mediaView.clientHeight) { if (mediaView.clientWidth > mediaView.clientHeight) {
if (img.naturalHeight < mediaView.clientHeight) { if (img.naturalHeight < mediaView.clientHeight) {
img.style.height = '100%'; img.setCssStyles({ height: '100%' });
} }
} else { } else {
if (img.naturalWidth < mediaView.clientWidth) { if (img.naturalWidth < mediaView.clientWidth) {
img.style.width = '100%'; img.setCssStyles({ width: '100%' });
} }
} }
} }
@ -285,7 +294,7 @@ export class MediaModal extends Modal {
// 切換圖片縮放 // 切換圖片縮放
toggleImageZoom(img: HTMLImageElement, event?: MouseEvent) { toggleImageZoom(img: HTMLImageElement, event?: MouseEvent) {
const mediaView = this.contentEl.querySelector('.ge-media-view'); const mediaView = this.contentEl.querySelector<HTMLElement>('.ge-media-view');
if (!mediaView) return; if (!mediaView) return;
if (!this.isZoomed) { // 放大 if (!this.isZoomed) { // 放大
@ -306,25 +315,33 @@ export class MediaModal extends Modal {
// 如果圖片比視窗更"細長" (Aspect Ratio 較小),則寬度填滿 (Fit Width),垂直捲動 // 如果圖片比視窗更"細長" (Aspect Ratio 較小),則寬度填滿 (Fit Width),垂直捲動
// 如果圖片比視窗更"扁平" (Aspect Ratio 較大),則高度填滿 (Fit Height),水平捲動 // 如果圖片比視窗更"扁平" (Aspect Ratio 較大),則高度填滿 (Fit Height),水平捲動
if (imageAspect < screenAspect) { if (imageAspect < screenAspect) {
img.style.width = '100vw'; img.setCssStyles({
img.style.height = 'auto'; width: '100vw',
(mediaView as HTMLElement).style.overflowX = 'hidden'; height: 'auto',
(mediaView as HTMLElement).style.overflowY = 'scroll'; });
mediaView.setCssStyles({
overflowX: 'hidden',
overflowY: 'scroll',
});
// 計算滾動位置 // 計算滾動位置
requestAnimationFrame(() => { window.requestAnimationFrame(() => {
const newHeight = img.offsetHeight; const newHeight = img.offsetHeight;
const scrollY = Math.max(0, (newHeight * clickY) - (mediaView.clientHeight / 2)); const scrollY = Math.max(0, (newHeight * clickY) - (mediaView.clientHeight / 2));
mediaView.scrollTop = scrollY; mediaView.scrollTop = scrollY;
}); });
} else { } else {
img.style.width = 'auto'; img.setCssStyles({
img.style.height = '100vh'; width: 'auto',
(mediaView as HTMLElement).style.overflowX = 'scroll'; height: '100vh',
(mediaView as HTMLElement).style.overflowY = 'hidden'; });
mediaView.setCssStyles({
overflowX: 'scroll',
overflowY: 'hidden',
});
// 計算滾動位置 // 計算滾動位置
requestAnimationFrame(() => { window.requestAnimationFrame(() => {
const newWidth = img.offsetWidth; const newWidth = img.offsetWidth;
const scrollX = Math.max(0, (newWidth * clickX) - (mediaView.clientWidth / 2)); const scrollX = Math.max(0, (newWidth * clickX) - (mediaView.clientWidth / 2));
mediaView.scrollLeft = scrollX; mediaView.scrollLeft = scrollX;
@ -339,14 +356,16 @@ export class MediaModal extends Modal {
mediaView.addEventListener('wheel', this.handleWheel); mediaView.addEventListener('wheel', this.handleWheel);
} }
img.style.maxWidth = 'none'; img.setCssStyles({
img.style.maxHeight = 'none'; maxWidth: 'none',
img.style.position = 'relative'; maxHeight: 'none',
img.style.left = '0'; position: 'relative',
img.style.top = '0'; left: '0',
img.style.margin = 'auto'; top: '0',
img.style.transform = 'none'; margin: 'auto',
img.style.cursor = 'zoom-out'; transform: 'none',
cursor: 'zoom-out',
});
this.isZoomed = true; this.isZoomed = true;
this.contentEl.addClass('is-zoomed'); this.contentEl.addClass('is-zoomed');
} else { // 縮小 } else { // 縮小

View file

@ -1,4 +1,4 @@
import { App, Modal, Setting, TFile } from 'obsidian'; import { App, Modal, Setting, TFile, TextComponent, ToggleComponent } from 'obsidian';
import GridExplorerPlugin from '../main'; import GridExplorerPlugin from '../main';
import { GridView } from '../GridView'; import { GridView } from '../GridView';
import { t } from '../translations'; import { t } from '../translations';
@ -12,6 +12,26 @@ export interface NoteSettings {
isHidden: boolean; isHidden: boolean;
} }
interface NoteFrontmatter {
[key: string]: unknown;
color?: unknown;
display?: unknown;
pinned?: unknown;
type?: unknown;
redirect?: unknown;
}
function getStringValue(value: unknown, fallback = ''): string {
return typeof value === 'string' ? value : fallback;
}
function getStringArray(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
return value.filter((item): item is string => typeof item === 'string');
}
export function showNoteSettingsModal(app: App, plugin: GridExplorerPlugin, file: TFile | TFile[]) { export function showNoteSettingsModal(app: App, plugin: GridExplorerPlugin, file: TFile | TFile[]) {
new NoteSettingsModal(app, plugin, file).open(); new NoteSettingsModal(app, plugin, file).open();
} }
@ -105,13 +125,13 @@ export class NoteSettingsModal extends Modal {
}); });
// HEX 自訂顏色輸入欄位 // HEX 自訂顏色輸入欄位
let hexInput: any; let hexInput: TextComponent | null = null;
const hexSetting = new Setting(contentEl) new Setting(contentEl)
.setName(t('custom_hex_color')) .setName(t('custom_hex_color'))
.setDesc(t('custom_hex_color_desc')) .setDesc(t('custom_hex_color_desc'))
.addText(text => { .addText(text => {
hexInput = text; hexInput = text;
text.setPlaceholder('#FF8800 or #F80') text.setPlaceholder('#Ff8800 or #f80')
.setValue(isCurrentColorHex ? this.settings.color : '') .setValue(isCurrentColorHex ? this.settings.color : '')
.onChange(value => { .onChange(value => {
// 驗證 HEX 格式 // 驗證 HEX 格式
@ -148,8 +168,8 @@ export class NoteSettingsModal extends Modal {
} }
// 最小化顯示切換 // 最小化顯示切換
let minimizedToggle: any; let minimizedToggle: ToggleComponent | null = null;
let hiddenToggle: any; let hiddenToggle: ToggleComponent | null = null;
if (this.files[0].extension === "md") { if (this.files[0].extension === "md") {
new Setting(contentEl) new Setting(contentEl)
@ -233,7 +253,7 @@ export class NoteSettingsModal extends Modal {
const newFile = await this.app.vault.create(newPath, ''); const newFile = await this.app.vault.create(newPath, '');
// 使用 processFrontMatter 來更新 frontmatter // 使用 processFrontMatter 來更新 frontmatter
await this.app.fileManager.processFrontMatter(newFile, (frontmatter: any) => { await this.app.fileManager.processFrontMatter(newFile, (frontmatter: NoteFrontmatter) => {
// 設置 redirect 和 summary // 設置 redirect 和 summary
const link = this.app.fileManager.generateMarkdownLink(originalFile, ""); const link = this.app.fileManager.generateMarkdownLink(originalFile, "");
frontmatter.type = "file"; frontmatter.type = "file";
@ -258,14 +278,15 @@ export class NoteSettingsModal extends Modal {
// 讀取自訂標題設定 // 讀取自訂標題設定
if (this.files.length === 1) { if (this.files.length === 1) {
const fileCache = this.app.metadataCache.getFileCache(this.files[0]); const fileCache = this.app.metadataCache.getFileCache(this.files[0]);
if (fileCache && fileCache.frontmatter) { if (fileCache?.frontmatter) {
const frontmatter = fileCache.frontmatter as NoteFrontmatter;
const titleField = this.plugin.settings.noteTitleField || 'title'; const titleField = this.plugin.settings.noteTitleField || 'title';
if (fileCache.frontmatter[titleField]) { if (frontmatter[titleField]) {
this.settings.title = fileCache.frontmatter[titleField] || ''; this.settings.title = getStringValue(frontmatter[titleField]);
} }
const summaryField = this.plugin.settings.noteSummaryField || 'summary'; const summaryField = this.plugin.settings.noteSummaryField || 'summary';
if (fileCache.frontmatter[summaryField]) { if (frontmatter[summaryField]) {
this.settings.summary = fileCache.frontmatter[summaryField] || ''; this.settings.summary = getStringValue(frontmatter[summaryField]);
} }
} }
} }
@ -273,17 +294,18 @@ export class NoteSettingsModal extends Modal {
// 如果有多個檔案,只讀取第一個檔案的設定作為預設值 // 如果有多個檔案,只讀取第一個檔案的設定作為預設值
if (this.files.length > 0) { if (this.files.length > 0) {
const fileCache = this.app.metadataCache.getFileCache(this.files[0]); const fileCache = this.app.metadataCache.getFileCache(this.files[0]);
if (fileCache && fileCache.frontmatter) { if (fileCache?.frontmatter) {
const frontmatter = fileCache.frontmatter as NoteFrontmatter;
// 讀取顏色設定 // 讀取顏色設定
if ('color' in fileCache.frontmatter) { if ('color' in fileCache.frontmatter) {
this.settings.color = fileCache.frontmatter.color || ''; this.settings.color = getStringValue(frontmatter.color);
} }
// 讀取最小化設定 // 讀取最小化設定
if (fileCache.frontmatter.display === 'minimized') { if (frontmatter.display === 'minimized') {
this.settings.isMinimized = true; this.settings.isMinimized = true;
} }
// 讀取隱藏設定 // 讀取隱藏設定
if (fileCache.frontmatter.display === 'hidden') { if (frontmatter.display === 'hidden') {
this.settings.isHidden = true; this.settings.isHidden = true;
} }
} }
@ -296,8 +318,9 @@ export class NoteSettingsModal extends Modal {
const noteFile = this.app.vault.getAbstractFileByPath(notePath); const noteFile = this.app.vault.getAbstractFileByPath(notePath);
if (noteFile instanceof TFile) { if (noteFile instanceof TFile) {
const fm = this.app.metadataCache.getFileCache(noteFile)?.frontmatter; const fm = this.app.metadataCache.getFileCache(noteFile)?.frontmatter;
if (fm && Array.isArray(fm['pinned'])) { const pinned = getStringArray(fm?.pinned);
this.settings.isPinned = fm['pinned'].includes(this.files[0].name); if (pinned.length > 0) {
this.settings.isPinned = pinned.includes(this.files[0].name);
} }
} }
// 保存初始的 isPinned 狀態 // 保存初始的 isPinned 狀態
@ -313,7 +336,7 @@ export class NoteSettingsModal extends Modal {
try { try {
if (this.files.length === 1 && this.files[0].extension === "md") { if (this.files.length === 1 && this.files[0].extension === "md") {
// 使用 fileManager.processFrontMatter 更新 frontmatter // 使用 fileManager.processFrontMatter 更新 frontmatter
await this.app.fileManager.processFrontMatter(this.files[0], (frontmatter) => { await this.app.fileManager.processFrontMatter(this.files[0], (frontmatter: NoteFrontmatter) => {
const titleField = this.plugin.settings.noteTitleField || 'title'; const titleField = this.plugin.settings.noteTitleField || 'title';
if (this.settings.title) { if (this.settings.title) {
frontmatter[titleField] = this.settings.title; frontmatter[titleField] = this.settings.title;
@ -327,16 +350,16 @@ export class NoteSettingsModal extends Modal {
delete frontmatter[summaryField]; delete frontmatter[summaryField];
} }
if (this.settings.color) { if (this.settings.color) {
frontmatter['color'] = this.settings.color; frontmatter.color = this.settings.color;
} else { } else {
delete frontmatter['color']; delete frontmatter.color;
} }
if (this.settings.isHidden) { if (this.settings.isHidden) {
frontmatter['display'] = 'hidden'; frontmatter.display = 'hidden';
} else if (this.settings.isMinimized) { } else if (this.settings.isMinimized) {
frontmatter['display'] = 'minimized'; frontmatter.display = 'minimized';
} else { } else {
if (frontmatter['display'] === 'minimized' || frontmatter['display'] === 'hidden') delete frontmatter['display']; if (frontmatter.display === 'minimized' || frontmatter.display === 'hidden') delete frontmatter.display;
} }
}); });
} }
@ -346,18 +369,18 @@ export class NoteSettingsModal extends Modal {
for (const file of this.files) { for (const file of this.files) {
if (file.extension === "md") { if (file.extension === "md") {
// 使用 fileManager.processFrontMatter 更新 frontmatter // 使用 fileManager.processFrontMatter 更新 frontmatter
await this.app.fileManager.processFrontMatter(file, (frontmatter) => { await this.app.fileManager.processFrontMatter(file, (frontmatter: NoteFrontmatter) => {
if (this.settings.color) { if (this.settings.color) {
frontmatter['color'] = this.settings.color; frontmatter.color = this.settings.color;
} else { } else {
delete frontmatter['color']; delete frontmatter.color;
} }
if (this.settings.isHidden) { if (this.settings.isHidden) {
frontmatter['display'] = 'hidden'; frontmatter.display = 'hidden';
} else if (this.settings.isMinimized) { } else if (this.settings.isMinimized) {
frontmatter['display'] = 'minimized'; frontmatter.display = 'minimized';
} else { } else {
if (frontmatter['display'] === 'minimized' || frontmatter['display'] === 'hidden') delete frontmatter['display']; if (frontmatter.display === 'minimized' || frontmatter.display === 'hidden') delete frontmatter.display;
} }
}); });
} }
@ -383,17 +406,21 @@ export class NoteSettingsModal extends Modal {
} }
// 此時 noteFile 一定是 TFile // 此時 noteFile 一定是 TFile
await this.app.fileManager.processFrontMatter(noteFile as TFile, (frontmatter) => { if (!(noteFile instanceof TFile)) {
let list: string[] = Array.isArray(frontmatter['pinned']) ? frontmatter['pinned'] : []; continue;
}
await this.app.fileManager.processFrontMatter(noteFile, (frontmatter: NoteFrontmatter) => {
let list = getStringArray(frontmatter.pinned);
if (this.settings.isPinned) { if (this.settings.isPinned) {
if (!list.includes(file.name)) list.push(file.name); if (!list.includes(file.name)) list.push(file.name);
} else { } else {
list = list.filter(n => n !== file.name); list = list.filter(n => n !== file.name);
} }
if (list.length > 0) { if (list.length > 0) {
frontmatter['pinned'] = list; frontmatter.pinned = list;
} else { } else {
delete frontmatter['pinned']; delete frontmatter.pinned;
} }
}); });
} }

View file

@ -1,8 +1,20 @@
import { App, Modal, Setting, setIcon } from 'obsidian'; import { App, Modal, setIcon } from 'obsidian';
import type { GridView } from '../GridView'; import type { GridView } from '../GridView';
import { t } from '../translations'; import { t } from '../translations';
export class SearchModal extends Modal { interface MetadataCacheWithTags {
getTags?: () => Record<string, unknown>;
}
function setHidden(el: HTMLElement, hidden: boolean) {
el.toggleClass('ge-hidden', hidden);
}
function isHidden(el: HTMLElement): boolean {
return el.hasClass('ge-hidden');
}
export class SearchModal extends Modal {
gridView: GridView; gridView: GridView;
defaultQuery: string; defaultQuery: string;
buttonElement: HTMLElement | undefined; buttonElement: HTMLElement | undefined;
@ -92,20 +104,21 @@ export class SearchModal extends Modal {
// 創建清空按鈕 // 創建清空按鈕
const clearButton = inputContainer.createDiv('ge-search-clear-button'); const clearButton = inputContainer.createDiv('ge-search-clear-button');
clearButton.style.display = searchTerms.length > 0 ? 'flex' : 'none'; setHidden(clearButton, searchTerms.length === 0);
setIcon(clearButton, 'x'); setIcon(clearButton, 'x');
const updateClearButton = () => { const updateClearButton = () => {
const hasContent = searchTerms.length > 0 || searchInput.value.trim().length > 0; const hasContent = searchTerms.length > 0 || searchInput.value.trim().length > 0;
clearButton.style.display = hasContent ? 'flex' : 'none'; setHidden(clearButton, !hasContent);
}; };
// 建立標籤建議容器 // 建立標籤建議容器
const tagSuggestionContainer = contentEl.createDiv('ge-search-tag-suggestions'); const tagSuggestionContainer = contentEl.createDiv('ge-search-tag-suggestions');
tagSuggestionContainer.style.display = 'none'; setHidden(tagSuggestionContainer, true);
// 取得並快取所有標籤 (移除 # 前綴) // 取得並快取所有標籤 (移除 # 前綴)
const allTagsArr: string[] = Object.keys(((this.app.metadataCache as any).getTags?.() || {})).map((t) => t.substring(1)); const tagMap = (this.app.metadataCache as unknown as MetadataCacheWithTags).getTags?.() ?? {};
const allTagsArr = Object.keys(tagMap).map((tag) => tag.substring(1));
let tagSuggestions: string[] = []; let tagSuggestions: string[] = [];
let selectedSuggestionIndex = -1; let selectedSuggestionIndex = -1;
@ -113,7 +126,7 @@ export class SearchModal extends Modal {
const updateTagSuggestions = () => { const updateTagSuggestions = () => {
const match = searchInput.value.substring(0, searchInput.selectionStart || 0).match(/#([^#\s]*)$/); const match = searchInput.value.substring(0, searchInput.selectionStart || 0).match(/#([^#\s]*)$/);
if (!match) { if (!match) {
tagSuggestionContainer.style.display = 'none'; setHidden(tagSuggestionContainer, true);
tagSuggestionContainer.empty(); tagSuggestionContainer.empty();
selectedSuggestionIndex = -1; selectedSuggestionIndex = -1;
return; return;
@ -134,7 +147,7 @@ export class SearchModal extends Modal {
}).slice(0, 10); }).slice(0, 10);
if (tagSuggestions.length === 0) { if (tagSuggestions.length === 0) {
tagSuggestionContainer.style.display = 'none'; setHidden(tagSuggestionContainer, true);
selectedSuggestionIndex = -1; selectedSuggestionIndex = -1;
return; return;
} }
@ -149,7 +162,7 @@ export class SearchModal extends Modal {
applySuggestion(idx); applySuggestion(idx);
}); });
}); });
tagSuggestionContainer.style.display = 'block'; setHidden(tagSuggestionContainer, false);
}; };
const applySuggestion = (index: number) => { const applySuggestion = (index: number) => {
@ -165,7 +178,7 @@ export class SearchModal extends Modal {
flushInput(false); flushInput(false);
tagSuggestionContainer.style.display = 'none'; setHidden(tagSuggestionContainer, true);
tagSuggestionContainer.empty(); tagSuggestionContainer.empty();
selectedSuggestionIndex = -1; selectedSuggestionIndex = -1;
@ -189,7 +202,7 @@ export class SearchModal extends Modal {
}); });
// 隨機筆記模式下,搜尋範圍設定不顯示 // 隨機筆記模式下,搜尋範圍設定不顯示
if (this.gridView.sourceMode === 'random-note') { if (this.gridView.sourceMode === 'random-note') {
searchScopeContainer.style.display = 'none'; setHidden(searchScopeContainer, true);
searchScopeCheckbox.checked = false; searchScopeCheckbox.checked = false;
} }
@ -218,7 +231,7 @@ export class SearchModal extends Modal {
}); });
// 如果設定中的顯示媒體檔案為false或在反向連結模式下則隱藏搜尋媒體檔案設定 // 如果設定中的顯示媒體檔案為false或在反向連結模式下則隱藏搜尋媒體檔案設定
if (!this.gridView.plugin.settings.showMediaFiles || this.gridView.sourceMode === 'backlinks') { if (!this.gridView.plugin.settings.showMediaFiles || this.gridView.sourceMode === 'backlinks') {
searchMediaFilesContainer.style.display = 'none'; setHidden(searchMediaFilesContainer, true);
searchMediaFilesCheckbox.checked = false; searchMediaFilesCheckbox.checked = false;
this.gridView.searchMediaFiles = false; this.gridView.searchMediaFiles = false;
} }
@ -295,7 +308,7 @@ export class SearchModal extends Modal {
return; return;
} }
if (tagSuggestionContainer.style.display !== 'none') { if (!isHidden(tagSuggestionContainer)) {
if (e.key === 'ArrowDown') { if (e.key === 'ArrowDown') {
e.preventDefault(); e.preventDefault();
selectedSuggestionIndex = (selectedSuggestionIndex + 1) % tagSuggestions.length; selectedSuggestionIndex = (selectedSuggestionIndex + 1) % tagSuggestions.length;
@ -312,7 +325,7 @@ export class SearchModal extends Modal {
} }
} else { } else {
if (e.key === 'ArrowDown') { if (e.key === 'ArrowDown') {
const firstVisible = optionsList.find(o => o.style.display !== 'none'); const firstVisible = optionsList.find(o => !isHidden(o));
if (firstVisible) { if (firstVisible) {
e.preventDefault(); e.preventDefault();
firstVisible.focus(); firstVisible.focus();
@ -331,7 +344,7 @@ export class SearchModal extends Modal {
currentInputIndex = 0; currentInputIndex = 0;
updateClearButton(); updateClearButton();
renderTagButtons(); renderTagButtons();
tagSuggestionContainer.style.display = 'none'; setHidden(tagSuggestionContainer, true);
searchInput.focus(); searchInput.focus();
}); });
optionsList.forEach((container, index) => { optionsList.forEach((container, index) => {
@ -343,7 +356,7 @@ export class SearchModal extends Modal {
} else if (e.key === 'ArrowDown') { } else if (e.key === 'ArrowDown') {
e.preventDefault(); e.preventDefault();
for (let i = index + 1; i < optionsList.length; i++) { for (let i = index + 1; i < optionsList.length; i++) {
if (optionsList[i].style.display !== 'none') { if (!isHidden(optionsList[i])) {
optionsList[i].focus(); optionsList[i].focus();
return; return;
} }
@ -351,7 +364,7 @@ export class SearchModal extends Modal {
} else if (e.key === 'ArrowUp') { } else if (e.key === 'ArrowUp') {
e.preventDefault(); e.preventDefault();
for (let i = index - 1; i >= 0; i--) { for (let i = index - 1; i >= 0; i--) {
if (optionsList[i].style.display !== 'none') { if (!isHidden(optionsList[i])) {
optionsList[i].focus(); optionsList[i].focus();
return; return;
} }
@ -571,10 +584,12 @@ export class SearchModal extends Modal {
} }
// 應用位置 // 應用位置
modalEl.style.position = 'fixed'; modalEl.setCssProps({
modalEl.style.top = `${top}px`; position: 'fixed',
modalEl.style.left = `${left}px`; top: `${top}px`,
modalEl.style.transform = 'none'; left: `${left}px`,
transform: 'none',
});
} }
} }

View file

@ -115,7 +115,7 @@ export class ShortcutSelectionModal extends Modal {
displayName = `🌐 ${uri.substring(0, 20)}${uri.length > 20 ? '...' : ''}`; displayName = `🌐 ${uri.substring(0, 20)}${uri.length > 20 ? '...' : ''}`;
} }
} }
} catch (error) { } catch {
displayName = `🌐 ${uri.substring(0, 20)}${uri.length > 20 ? '...' : ''}`; displayName = `🌐 ${uri.substring(0, 20)}${uri.length > 20 ? '...' : ''}`;
} }

View file

@ -79,14 +79,11 @@ export async function renderFiles(gridView: GridView, container: HTMLElement) {
targetFile = gridView.app.metadataCache.getFirstLinkpathDest(linkTarget, ''); targetFile = gridView.app.metadataCache.getFirstLinkpathDest(linkTarget, '');
} }
if (targetFile && 'extension' in targetFile) { if (targetFile instanceof TFile) {
// 使用 Obsidian 的 getBacklinksForFile API
const backlinks = (gridView.app.metadataCache as any).getBacklinksForFile(targetFile);
const currentLinkFiles = new Set<string>(); const currentLinkFiles = new Set<string>();
if (backlinks) { // 收集所有反向連結的檔案路徑
// 收集所有反向連結的檔案路徑 for (const [filePath, links] of Object.entries(gridView.app.metadataCache.resolvedLinks)) {
for (const [filePath, links] of backlinks.data.entries()) { if (links[targetFile.path]) {
currentLinkFiles.add(filePath); currentLinkFiles.add(filePath);
} }
} }
@ -166,7 +163,7 @@ export async function renderFiles(gridView: GridView, container: HTMLElement) {
// frontmatter 標籤 // frontmatter 標籤
if (fileCache.frontmatter && fileCache.frontmatter.tags) { if (fileCache.frontmatter && fileCache.frontmatter.tags) {
const fmTags = fileCache.frontmatter.tags; const fmTags: unknown = fileCache.frontmatter.tags;
if (typeof fmTags === 'string') { if (typeof fmTags === 'string') {
collectedTags.push( collectedTags.push(
...fmTags.split(/[,\s]+/) ...fmTags.split(/[,\s]+/)

View file

@ -10,6 +10,10 @@ import { showFolderRenameModal } from './modal/folderRenameModal';
import { ShortcutSelectionModal } from './modal/shortcutSelectionModal'; import { ShortcutSelectionModal } from './modal/shortcutSelectionModal';
import { t } from './translations'; import { t } from './translations';
interface AppWithShowInFolder {
showInFolder?: (path: string) => void;
}
export async function renderFolder(gridView: GridView, container: HTMLElement) { export async function renderFolder(gridView: GridView, container: HTMLElement) {
// 如果是資料夾模式,先顯示所有子資料夾 // 如果是資料夾模式,先顯示所有子資料夾
@ -27,7 +31,7 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
// 防止預設行為以允許放置 // 防止預設行為以允許放置
event.preventDefault(); event.preventDefault();
// 設定拖曳效果為移動 // 設定拖曳效果為移動
(event as any).dataTransfer!.dropEffect = 'move'; event.dataTransfer!.dropEffect = 'move';
// 顯示可放置的視覺提示 // 顯示可放置的視覺提示
container.addClass('ge-dragover'); container.addClass('ge-dragover');
}, true); // 使用捕獲階段 }, true); // 使用捕獲階段
@ -54,8 +58,7 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
container.removeClass('ge-dragover'); container.removeClass('ge-dragover');
// 處理從 Obsidian 檔案總管拖來的 obsidian://open URI單檔/多檔) // 處理從 Obsidian 檔案總管拖來的 obsidian://open URI單檔/多檔)
const dt = (event as DragEvent).dataTransfer; const dt = event.dataTransfer;
console.log(dt);
if (dt) { if (dt) {
try { try {
const obsidianPaths = await extractObsidianPathsFromDT(dt); const obsidianPaths = await extractObsidianPathsFromDT(dt);
@ -79,7 +82,7 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
// 3) 使用 Obsidian 的連結解析(支援不含副檔名與子資料夾) // 3) 使用 Obsidian 的連結解析(支援不含副檔名與子資料夾)
if (!resolved) { if (!resolved) {
const srcPath = currentFolder.path || '/'; const srcPath = currentFolder.path || '/';
const dest = (gridView.app.metadataCache as any).getFirstLinkpathDest?.(p, srcPath); const dest = gridView.app.metadataCache.getFirstLinkpathDest(p, srcPath);
if (dest instanceof TFile) resolved = dest; if (dest instanceof TFile) resolved = dest;
} }
@ -104,7 +107,7 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
} }
// 如果沒有檔案路徑列表,則使用檔案路徑(支援多行多檔) // 如果沒有檔案路徑列表,則使用檔案路徑(支援多行多檔)
const filePath = (event as any).dataTransfer?.getData('text/plain'); const filePath = event.dataTransfer?.getData('text/plain');
if (!filePath) return; if (!filePath) return;
const srcPath = currentFolder.path || '/'; const srcPath = currentFolder.path || '/';
@ -122,14 +125,14 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
if (text.startsWith('[[') && text.endsWith(']]')) { if (text.startsWith('[[') && text.endsWith(']]')) {
const inner = text.slice(2, -2); const inner = text.slice(2, -2);
const parsed = parseLinktext(inner); const parsed = parseLinktext(inner);
const dest = (gridView.app.metadataCache as any).getFirstLinkpathDest?.(parsed.path, srcPath); const dest = gridView.app.metadataCache.getFirstLinkpathDest(parsed.path, srcPath);
if (dest instanceof TFile) resolvedFile = dest; if (dest instanceof TFile) resolvedFile = dest;
} else { } else {
const direct = gridView.app.vault.getAbstractFileByPath(text); const direct = gridView.app.vault.getAbstractFileByPath(text);
if (direct instanceof TFile) { if (direct instanceof TFile) {
resolvedFile = direct; resolvedFile = direct;
} else { } else {
const dest = (gridView.app.metadataCache as any).getFirstLinkpathDest?.(text, srcPath); const dest = gridView.app.metadataCache.getFirstLinkpathDest(text, srcPath);
if (dest instanceof TFile) resolvedFile = dest; if (dest instanceof TFile) resolvedFile = dest;
} }
} }
@ -204,8 +207,8 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
// 根據同名筆記設置背景色 // 根據同名筆記設置背景色
const metadata = gridView.app.metadataCache.getFileCache(noteFile)?.frontmatter; const metadata = gridView.app.metadataCache.getFileCache(noteFile)?.frontmatter;
const colorValue = metadata?.color; const colorValue: unknown = metadata?.color;
if (colorValue) { if (typeof colorValue === 'string' && colorValue) {
// 檢查是否為 HEX 色值 // 檢查是否為 HEX 色值
if (isHexColor(colorValue)) { if (isHexColor(colorValue)) {
// 使用自訂 CSS 變數來設置 HEX 顏色 // 使用自訂 CSS 變數來設置 HEX 顏色
@ -217,10 +220,10 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
folderEl.addClass(`ge-folder-color-${colorValue}`); folderEl.addClass(`ge-folder-color-${colorValue}`);
} }
} }
const iconValue = metadata?.icon; const iconValue: unknown = metadata?.icon;
if (iconValue) { if (typeof iconValue === 'string' && iconValue) {
// 修改原本的title文字 // 修改原本的title文字
const title = folderEl.querySelector('.ge-title'); const title = folderEl.querySelector<HTMLElement>('.ge-title');
if (title) { if (title) {
title.textContent = `${iconValue} ${folder.name}`; title.textContent = `${iconValue} ${folder.name}`;
} }
@ -261,7 +264,7 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
.setTitle(t('open_in_file_explorer')) .setTitle(t('open_in_file_explorer'))
.setIcon('arrow-up-right') .setIcon('arrow-up-right')
.onClick(() => { .onClick(() => {
(gridView.app as any).showInFolder(folder.path); (gridView.app as AppWithShowInFolder).showInFolder?.(folder.path);
}); });
}); });
} }
@ -347,7 +350,9 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
.setTitle(t('delete_folder_note')) .setTitle(t('delete_folder_note'))
.setIcon('folder-x') .setIcon('folder-x')
.onClick(() => { .onClick(() => {
void gridView.app.fileManager.trashFile(noteFile as TFile); if (noteFile instanceof TFile) {
void gridView.app.fileManager.trashFile(noteFile);
}
}); });
}); });
} else { } else {
@ -412,8 +417,8 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
}); });
// 刪除資料夾 // 刪除資料夾
menu.addItem((item) => { menu.addItem((item) => {
(item as any).setWarning(true);
item item
.setWarning(true)
.setTitle(t('delete_folder')) .setTitle(t('delete_folder'))
.setIcon('trash') .setIcon('trash')
.onClick(() => { .onClick(() => {
@ -441,13 +446,13 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
// 為資料夾項目添加拖曳目標功能 // 為資料夾項目添加拖曳目標功能
if (Platform.isDesktop) { if (Platform.isDesktop) {
const folderItems = gridView.containerEl.querySelectorAll('.ge-folder-item'); const folderItems = gridView.containerEl.querySelectorAll<HTMLElement>('.ge-folder-item');
folderItems.forEach(folderItem => { folderItems.forEach(folderItem => {
folderItem.addEventListener('dragover', (event) => { folderItem.addEventListener('dragover', (event) => {
// 防止預設行為以允許放置 // 防止預設行為以允許放置
event.preventDefault(); event.preventDefault();
// 設定拖曳效果為移動 // 設定拖曳效果為移動
(event as any).dataTransfer!.dropEffect = 'move'; event.dataTransfer!.dropEffect = 'move';
// 顯示可放置的視覺提示 // 顯示可放置的視覺提示
folderItem.addClass('ge-dragover'); folderItem.addClass('ge-dragover');
}); });
@ -465,12 +470,12 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
folderItem.removeClass('ge-dragover'); folderItem.removeClass('ge-dragover');
// 處理從 Obsidian 檔案總管拖來的 obsidian://open URI單檔/多檔) // 處理從 Obsidian 檔案總管拖來的 obsidian://open URI單檔/多檔)
const dt = (event as DragEvent).dataTransfer; const dt = event.dataTransfer;
if (dt) { if (dt) {
try { try {
const obsidianPaths = await extractObsidianPathsFromDT(dt); const obsidianPaths = await extractObsidianPathsFromDT(dt);
if (obsidianPaths.length) { if (obsidianPaths.length) {
const folderPath = (folderItem as any).dataset.folderPath; const folderPath = folderItem.dataset.folderPath;
if (!folderPath) return; if (!folderPath) return;
const folder = gridView.app.vault.getAbstractFileByPath(folderPath); const folder = gridView.app.vault.getAbstractFileByPath(folderPath);
if (!(folder instanceof TFolder)) return; if (!(folder instanceof TFolder)) return;
@ -493,8 +498,8 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
// 3) 使用 Obsidian 的連結解析(支援不含副檔名與子資料夾) // 3) 使用 Obsidian 的連結解析(支援不含副檔名與子資料夾)
if (!resolved) { if (!resolved) {
const srcPath = (folderItem as any).dataset.folderPath || '/'; const srcPath = folderItem.dataset.folderPath || '/';
const dest = (gridView.app.metadataCache as any).getFirstLinkpathDest?.(p, srcPath); const dest = gridView.app.metadataCache.getFirstLinkpathDest(p, srcPath);
if (dest instanceof TFile) resolved = dest; if (dest instanceof TFile) resolved = dest;
} }
@ -519,17 +524,17 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
} }
// 如果沒有檔案路徑列表,則使用檔案路徑(支援多行多檔) // 如果沒有檔案路徑列表,則使用檔案路徑(支援多行多檔)
const filePath = (event as any).dataTransfer?.getData('text/plain'); const filePath = event.dataTransfer?.getData('text/plain');
if (!filePath) return; if (!filePath) return;
// 獲取目標資料夾路徑 // 獲取目標資料夾路徑
const folderPath = (folderItem as any).dataset.folderPath; const folderPath = folderItem.dataset.folderPath;
if (!folderPath) return; if (!folderPath) return;
const folder = gridView.app.vault.getAbstractFileByPath(folderPath); const folder = gridView.app.vault.getAbstractFileByPath(folderPath);
if (!(folder instanceof TFolder)) return; if (!(folder instanceof TFolder)) return;
// 使用 Obsidian API 解析每一行的連結或路徑 // 使用 Obsidian API 解析每一行的連結或路徑
const srcPath = (folderItem as any).dataset.folderPath || '/'; const srcPath = folderItem.dataset.folderPath || '/';
const lines = filePath const lines = filePath
.split(/\r?\n/) .split(/\r?\n/)
.map((s: string) => s.trim()) .map((s: string) => s.trim())
@ -543,14 +548,14 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
if (text.startsWith('[[') && text.endsWith(']]')) { if (text.startsWith('[[') && text.endsWith(']]')) {
const inner = text.slice(2, -2); const inner = text.slice(2, -2);
const parsed = parseLinktext(inner); const parsed = parseLinktext(inner);
const dest = (gridView.app.metadataCache as any).getFirstLinkpathDest?.(parsed.path, srcPath); const dest = gridView.app.metadataCache.getFirstLinkpathDest(parsed.path, srcPath);
if (dest instanceof TFile) resolvedFile = dest; if (dest instanceof TFile) resolvedFile = dest;
} else { } else {
const direct = gridView.app.vault.getAbstractFileByPath(text); const direct = gridView.app.vault.getAbstractFileByPath(text);
if (direct instanceof TFile) { if (direct instanceof TFile) {
resolvedFile = direct; resolvedFile = direct;
} else { } else {
const dest = (gridView.app.metadataCache as any).getFirstLinkpathDest?.(text, srcPath); const dest = gridView.app.metadataCache.getFirstLinkpathDest(text, srcPath);
if (dest instanceof TFile) resolvedFile = dest; if (dest instanceof TFile) resolvedFile = dest;
} }
} }

View file

@ -7,6 +7,41 @@ import { ShortcutSelectionModal } from './modal/shortcutSelectionModal';
import { createNewNote, createNewFolder, createNewCanvas, createNewBase, createShortcut as createShortcutUtil } from './utils/createItemUtils'; import { createNewNote, createNewFolder, createNewCanvas, createNewBase, createShortcut as createShortcutUtil } from './utils/createItemUtils';
import { t } from './translations'; import { t } from './translations';
interface SourceInfo {
mode: string;
path: string;
searchQuery?: string;
searchCurrentLocationOnly?: boolean;
searchFilesNameOnly?: boolean;
searchMediaFiles?: boolean;
}
interface AppWithSettings {
setting?: {
open: () => void;
openTabById: (id: string) => void;
};
}
function isSourceInfo(value: unknown): value is SourceInfo {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return false;
}
const source = value as Record<string, unknown>;
return typeof source.mode === 'string' &&
typeof source.path === 'string' &&
(source.searchQuery === undefined || typeof source.searchQuery === 'string') &&
(source.searchCurrentLocationOnly === undefined || typeof source.searchCurrentLocationOnly === 'boolean') &&
(source.searchFilesNameOnly === undefined || typeof source.searchFilesNameOnly === 'boolean') &&
(source.searchMediaFiles === undefined || typeof source.searchMediaFiles === 'boolean');
}
function parseSourceInfo(sourceInfoStr: string): SourceInfo | null {
const parsed: unknown = JSON.parse(sourceInfoStr);
return isSourceInfo(parsed) ? parsed : null;
}
export function renderHeaderButton(gridView: GridView) { export function renderHeaderButton(gridView: GridView) {
// 創建頂部按鈕區域 // 創建頂部按鈕區域
@ -53,7 +88,8 @@ export function renderHeaderButton(gridView: GridView) {
} }
// 取得最近一筆歷史記錄 // 取得最近一筆歷史記錄
const lastSource = JSON.parse(gridView.recentSources[0]); const lastSource = parseSourceInfo(gridView.recentSources[0]);
if (!lastSource) return;
gridView.recentSources.shift(); // 從歷史記錄中移除 gridView.recentSources.shift(); // 從歷史記錄中移除
// 設定來源及搜尋狀態(不記錄到歷史) // 設定來源及搜尋狀態(不記錄到歷史)
@ -100,7 +136,8 @@ export function renderHeaderButton(gridView: GridView) {
} }
// 取得下一筆未來紀錄 // 取得下一筆未來紀錄
const nextSource = JSON.parse(gridView.futureSources[0]); const nextSource = parseSourceInfo(gridView.futureSources[0]);
if (!nextSource) return;
gridView.futureSources.shift(); // 從未來紀錄中移除 gridView.futureSources.shift(); // 從未來紀錄中移除
// 設定來源及搜尋狀態(不記錄到歷史) // 設定來源及搜尋狀態(不記錄到歷史)
@ -131,7 +168,8 @@ export function renderHeaderButton(gridView: GridView) {
// 添加歷史記錄 // 添加歷史記錄
gridView.recentSources.forEach((sourceInfoStr, index) => { gridView.recentSources.forEach((sourceInfoStr, index) => {
try { try {
const sourceInfo = JSON.parse(sourceInfoStr); const sourceInfo = parseSourceInfo(sourceInfoStr);
if (!sourceInfo) return;
const { mode, path } = sourceInfo; const { mode, path } = sourceInfo;
// 根據模式顯示圖示和文字 // 根據模式顯示圖示和文字
@ -260,7 +298,8 @@ export function renderHeaderButton(gridView: GridView) {
// 添加未來記錄 // 添加未來記錄
gridView.futureSources.forEach((sourceInfoStr, index) => { gridView.futureSources.forEach((sourceInfoStr, index) => {
try { try {
const sourceInfo = JSON.parse(sourceInfoStr); const sourceInfo = parseSourceInfo(sourceInfoStr);
if (!sourceInfo) return;
const { mode, path } = sourceInfo; const { mode, path } = sourceInfo;
// 根據模式顯示圖示和文字 // 根據模式顯示圖示和文字
@ -319,7 +358,7 @@ export function renderHeaderButton(gridView: GridView) {
if (!sourceInfo.searchCurrentLocationOnly) { if (!sourceInfo.searchCurrentLocationOnly) {
displayText = '"' + (sourceInfo.searchQuery || t('search_results')) + '"'; displayText = '"' + (sourceInfo.searchQuery || t('search_results')) + '"';
} else { } else {
displayText += `: \"${sourceInfo.searchQuery}\"`; displayText += `: "${sourceInfo.searchQuery}"`;
} }
} }
@ -599,8 +638,9 @@ export function renderHeaderButton(gridView: GridView) {
.setIcon('settings') .setIcon('settings')
.onClick(() => { .onClick(() => {
// 打開插件設定頁面 // 打開插件設定頁面
(gridView.app as any).setting.open(); const setting = (gridView.app as AppWithSettings).setting;
(gridView.app as any).setting.openTabById(gridView.plugin.manifest.id); setting?.open();
setting?.openTabById(gridView.plugin.manifest.id);
}); });
}); });

View file

@ -8,6 +8,35 @@ import { showFolderMoveModal } from './modal/folderMoveModal';
import { CustomModeModal } from './modal/customModeModal'; import { CustomModeModal } from './modal/customModeModal';
import { showSearchModal } from './modal/searchModal'; import { showSearchModal } from './modal/searchModal';
import { t } from './translations'; import { t } from './translations';
interface SearchLeaf {
view?: {
searchComponent?: {
inputEl?: HTMLInputElement;
};
};
}
interface BookmarksPluginItem {
type?: string;
title?: string;
items?: BookmarksPluginItem[];
}
interface BookmarksPlugin {
enabled?: boolean;
instance?: {
items?: BookmarksPluginItem[];
};
}
interface AppWithInternalPlugins {
showInFolder?: (path: string) => void;
internalPlugins?: {
plugins?: {
bookmarks?: BookmarksPlugin;
};
};
}
export function renderModePath(gridView: GridView) { export function renderModePath(gridView: GridView) {
@ -225,7 +254,7 @@ export function renderModePath(gridView: GridView) {
.setDisabled(true) .setDisabled(true)
); );
subFolders.forEach((folder: any) => { subFolders.forEach((folder) => {
menu.addItem((item) => { menu.addItem((item) => {
item.setTitle(`${customFolderIcon} ${folder.name}`) item.setTitle(`${customFolderIcon} ${folder.name}`)
.setIcon('corner-down-right') .setIcon('corner-down-right')
@ -238,7 +267,7 @@ export function renderModePath(gridView: GridView) {
} }
} }
menu.showAtMouseEvent(event as MouseEvent); menu.showAtMouseEvent(event);
} else { } else {
void gridView.setSource('folder', path.path); void gridView.setSource('folder', path.path);
gridView.clearSelection(); gridView.clearSelection();
@ -291,7 +320,7 @@ export function renderModePath(gridView: GridView) {
// 3) 使用 Obsidian 的連結解析 // 3) 使用 Obsidian 的連結解析
if (!resolved) { if (!resolved) {
const dest = (gridView.app.metadataCache as any).getFirstLinkpathDest?.(filePath, path.path); const dest = gridView.app.metadataCache.getFirstLinkpathDest(filePath, path.path);
if (dest instanceof TFile) resolved = dest; if (dest instanceof TFile) resolved = dest;
} }
@ -327,14 +356,14 @@ export function renderModePath(gridView: GridView) {
if (text.startsWith('[[') && text.endsWith(']]')) { if (text.startsWith('[[') && text.endsWith(']]')) {
const inner = text.slice(2, -2); const inner = text.slice(2, -2);
const parsed = parseLinktext(inner); const parsed = parseLinktext(inner);
const dest = (gridView.app.metadataCache as any).getFirstLinkpathDest?.(parsed.path, srcPath); const dest = gridView.app.metadataCache.getFirstLinkpathDest(parsed.path, srcPath);
if (dest instanceof TFile) resolvedFile = dest; if (dest instanceof TFile) resolvedFile = dest;
} else { } else {
const direct = gridView.app.vault.getAbstractFileByPath(text); const direct = gridView.app.vault.getAbstractFileByPath(text);
if (direct instanceof TFile) { if (direct instanceof TFile) {
resolvedFile = direct; resolvedFile = direct;
} else { } else {
const dest = (gridView.app.metadataCache as any).getFirstLinkpathDest?.(text, srcPath); const dest = gridView.app.metadataCache.getFirstLinkpathDest(text, srcPath);
if (dest instanceof TFile) resolvedFile = dest; if (dest instanceof TFile) resolvedFile = dest;
} }
} }
@ -356,7 +385,7 @@ export function renderModePath(gridView: GridView) {
} }
} }
if (el.className === 'ge-current-folder' || (el instanceof HTMLAnchorElement && el.className.includes('ge-current-folder'))) { if (el.className === 'ge-current-folder' || (el.instanceOf(HTMLAnchorElement) && el.className.includes('ge-current-folder'))) {
const showCurrentFolderMenu = (event: MouseEvent) => { const showCurrentFolderMenu = (event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -484,7 +513,7 @@ export function renderModePath(gridView: GridView) {
.setIcon('arrow-up-right') .setIcon('arrow-up-right')
.onClick(() => { .onClick(() => {
if (folder instanceof TFolder) { if (folder instanceof TFolder) {
(gridView.app as any).showInFolder(folder.path); (gridView.app as AppWithInternalPlugins).showInFolder?.(folder.path);
} }
}); });
}); });
@ -548,8 +577,8 @@ export function renderModePath(gridView: GridView) {
}); });
// 刪除資料夾 // 刪除資料夾
menu.addItem((item) => { menu.addItem((item) => {
(item as any).setWarning(true);
item item
.setWarning(true)
.setTitle(t('delete_folder')) .setTitle(t('delete_folder'))
.setIcon('trash') .setIcon('trash')
.onClick(async () => { .onClick(async () => {
@ -606,17 +635,18 @@ export function renderModePath(gridView: GridView) {
// 根據目前模式設定對應的圖示和名稱 // 根據目前模式設定對應的圖示和名稱
switch (gridView.sourceMode) { switch (gridView.sourceMode) {
case 'bookmarks': case 'bookmarks': {
modeIcon = '📑'; modeIcon = '📑';
modeName = t('bookmarks_mode'); modeName = t('bookmarks_mode');
break; break;
case 'search': }
case 'search': {
modeIcon = '🔍'; modeIcon = '🔍';
modeName = t('search_results'); modeName = t('search_results');
const searchLeaf = (gridView.app as any).workspace.getLeavesOfType('search')[0]; const searchLeaf = gridView.app.workspace.getLeavesOfType('search')[0] as SearchLeaf | undefined;
if (searchLeaf) { if (searchLeaf) {
const searchView: any = searchLeaf.view; const searchView = searchLeaf.view;
const searchInputEl: HTMLInputElement | null = searchView.searchComponent ? searchView.searchComponent.inputEl : null; const searchInputEl: HTMLInputElement | null = searchView?.searchComponent?.inputEl ?? null;
const currentQuery = searchInputEl?.value.trim(); const currentQuery = searchInputEl?.value.trim();
if (currentQuery && currentQuery.length > 0) { if (currentQuery && currentQuery.length > 0) {
modeName += `: ${currentQuery}`; modeName += `: ${currentQuery}`;
@ -625,7 +655,8 @@ export function renderModePath(gridView: GridView) {
} }
} }
break; break;
case 'backlinks': }
case 'backlinks': {
modeIcon = '🔗'; modeIcon = '🔗';
modeName = t('backlinks_mode'); modeName = t('backlinks_mode');
const activeFile = gridView.app.workspace.getActiveFile(); const activeFile = gridView.app.workspace.getActiveFile();
@ -633,7 +664,8 @@ export function renderModePath(gridView: GridView) {
modeName += `: ${activeFile.basename}`; modeName += `: ${activeFile.basename}`;
} }
break; break;
case 'outgoinglinks': }
case 'outgoinglinks': {
modeIcon = '🔗'; modeIcon = '🔗';
modeName = t('outgoinglinks_mode'); modeName = t('outgoinglinks_mode');
const currentFile = gridView.app.workspace.getActiveFile(); const currentFile = gridView.app.workspace.getActiveFile();
@ -641,6 +673,7 @@ export function renderModePath(gridView: GridView) {
modeName += `: ${currentFile.basename}`; modeName += `: ${currentFile.basename}`;
} }
break; break;
}
case 'recent-files': case 'recent-files':
modeIcon = '📅'; modeIcon = '📅';
modeName = t('recent_files_mode'); modeName = t('recent_files_mode');
@ -707,7 +740,7 @@ export function renderModePath(gridView: GridView) {
} }
switch (gridView.sourceMode) { switch (gridView.sourceMode) {
case 'bookmarks': case 'bookmarks': {
let bookmarkGroupName = ''; let bookmarkGroupName = '';
if (gridView.bookmarkGroupId === 'all') { if (gridView.bookmarkGroupId === 'all') {
bookmarkGroupName = t('all_bookmarks'); bookmarkGroupName = t('all_bookmarks');
@ -744,14 +777,14 @@ export function renderModePath(gridView: GridView) {
menu.addSeparator(); menu.addSeparator();
// 取得所有書籤群組 // 取得所有書籤群組
const bookmarksPlugin = (gridView.app as any).internalPlugins.plugins.bookmarks; const bookmarksPlugin = (gridView.app as AppWithInternalPlugins).internalPlugins?.plugins?.bookmarks;
if (bookmarksPlugin?.enabled) { if (bookmarksPlugin?.enabled) {
const bookmarks = bookmarksPlugin.instance.items; const bookmarks = bookmarksPlugin.instance?.items ?? [];
const groups: string[] = []; const groups: string[] = [];
const findGroups = (items: any[]) => { const findGroups = (items: BookmarksPluginItem[]) => {
items.forEach(item => { items.forEach(item => {
if (item.type === 'group') { if (item.type === 'group') {
groups.push(item.title); groups.push(item.title ?? '');
if (item.items) findGroups(item.items); if (item.items) findGroups(item.items);
} }
}); });
@ -772,9 +805,10 @@ export function renderModePath(gridView: GridView) {
menu.showAtMouseEvent(evt); menu.showAtMouseEvent(evt);
}); });
break; break;
}
case 'random-note': case 'random-note':
case 'recent-files': case 'recent-files':
case 'all-files': case 'all-files': {
if (gridView.plugin.settings.showMediaFiles && gridView.searchQuery === '') { if (gridView.plugin.settings.showMediaFiles && gridView.searchQuery === '') {
// "顯示類型"選項 // "顯示類型"選項
const showTypeName = gridView.includeMedia ? t('random_note_include_media_files') : t('random_note_notes_only'); const showTypeName = gridView.includeMedia ? t('random_note_include_media_files') : t('random_note_notes_only');
@ -803,7 +837,8 @@ export function renderModePath(gridView: GridView) {
}); });
} }
break; break;
case 'tasks': }
case 'tasks': {
const taskFilterSpan = modenameContainer.createEl('a', { text: t(`${gridView.taskFilter}`), cls: 'ge-sub-option' }); const taskFilterSpan = modenameContainer.createEl('a', { text: t(`${gridView.taskFilter}`), cls: 'ge-sub-option' });
taskFilterSpan.addEventListener('click', (evt) => { taskFilterSpan.addEventListener('click', (evt) => {
const menu = new Menu(); const menu = new Menu();
@ -838,7 +873,8 @@ export function renderModePath(gridView: GridView) {
menu.showAtMouseEvent(evt); menu.showAtMouseEvent(evt);
}); });
break; break;
default: }
default: {
if (gridView.sourceMode.startsWith('custom-')) { if (gridView.sourceMode.startsWith('custom-')) {
// 把 modenameContainer 加上所有自訂模式選項的選單 // 把 modenameContainer 加上所有自訂模式選項的選單
@ -854,7 +890,7 @@ export function renderModePath(gridView: GridView) {
let subName: string | undefined; let subName: string | undefined;
if (gridView.customOptionIndex === -1) { if (gridView.customOptionIndex === -1) {
subName = (mode as any).name?.trim() || t('default'); subName = mode.name?.trim() || t('default');
} else if (gridView.customOptionIndex >= 0 && gridView.customOptionIndex < mode.options.length) { } else if (gridView.customOptionIndex >= 0 && gridView.customOptionIndex < mode.options.length) {
const opt = mode.options[gridView.customOptionIndex]; const opt = mode.options[gridView.customOptionIndex];
subName = opt.name?.trim() || `${t('option')} ${gridView.customOptionIndex + 1}`; subName = opt.name?.trim() || `${t('option')} ${gridView.customOptionIndex + 1}`;
@ -864,7 +900,7 @@ export function renderModePath(gridView: GridView) {
subSpan.addEventListener('click', (evt) => { subSpan.addEventListener('click', (evt) => {
const menu = new Menu(); const menu = new Menu();
// 預設選項 // 預設選項
const defaultName = (mode as any).name?.trim() || t('default'); const defaultName = mode.name?.trim() || t('default');
menu.addItem(item => { menu.addItem(item => {
item.setTitle(defaultName) item.setTitle(defaultName)
.setIcon('puzzle') .setIcon('puzzle')
@ -904,6 +940,7 @@ export function renderModePath(gridView: GridView) {
} }
} }
break; break;
}
} }
} else if (gridView.searchQuery && !gridView.searchCurrentLocationOnly) { } else if (gridView.searchQuery && !gridView.searchCurrentLocationOnly) {
// 顯示全域搜尋名稱 // 顯示全域搜尋名稱
@ -921,7 +958,6 @@ export function renderModePath(gridView: GridView) {
// 建立可點選的搜尋文字 // 建立可點選的搜尋文字
const searchText = searchTextContainer.createEl('span', { cls: 'ge-search-text', text: gridView.searchQuery }); const searchText = searchTextContainer.createEl('span', { cls: 'ge-search-text', text: gridView.searchQuery });
searchText.style.cursor = 'pointer';
searchText.addEventListener('click', () => { searchText.addEventListener('click', () => {
// 以文字元素作為定位點開啟搜尋 modalpopup 樣式) // 以文字元素作為定位點開啟搜尋 modalpopup 樣式)
showSearchModal(gridView.app, gridView, gridView.searchQuery, searchText); showSearchModal(gridView.app, gridView, gridView.searchQuery, searchText);

View file

@ -2,6 +2,14 @@ import { App, TFile, Notice } from 'obsidian';
import { showNameInputModal } from '../modal/folderRenameModal'; import { showNameInputModal } from '../modal/folderRenameModal';
import { t } from '../translations'; import { t } from '../translations';
interface ShortcutFrontmatter {
type?: 'mode' | 'folder' | 'file' | 'search' | 'uri';
redirect?: string;
searchCurrentLocationOnly?: boolean;
searchFilesNameOnly?: boolean;
searchMediaFiles?: boolean;
}
/** /**
* *
* @param app Obsidian App * @param app Obsidian App
@ -173,7 +181,7 @@ export async function createShortcut(
const newFile = await app.vault.create(newPath, ''); const newFile = await app.vault.create(newPath, '');
// 使用 processFrontMatter 來更新 frontmatter // 使用 processFrontMatter 來更新 frontmatter
await app.fileManager.processFrontMatter(newFile, (frontmatter: any) => { await app.fileManager.processFrontMatter(newFile, (frontmatter: ShortcutFrontmatter) => {
if (option.type === 'mode') { if (option.type === 'mode') {
frontmatter.type = 'mode'; frontmatter.type = 'mode';
frontmatter.redirect = option.value; frontmatter.redirect = option.value;
@ -181,10 +189,12 @@ export async function createShortcut(
frontmatter.type = 'folder'; frontmatter.type = 'folder';
frontmatter.redirect = option.value; frontmatter.redirect = option.value;
} else if (option.type === 'file') { } else if (option.type === 'file') {
const link = app.fileManager.generateMarkdownLink( const targetFile = app.vault.getAbstractFileByPath(option.value);
app.vault.getAbstractFileByPath(option.value) as TFile, if (!(targetFile instanceof TFile)) {
"" return;
); }
const link = app.fileManager.generateMarkdownLink(targetFile, "");
frontmatter.type = "file"; frontmatter.type = "file";
frontmatter.redirect = link; frontmatter.redirect = link;
} else if (option.type === 'search') { } else if (option.type === 'search') {
@ -293,7 +303,7 @@ function generateFilenameFromUri(uri: string): string {
const cleanUri = uri.replace(/[<>:"/\\|?*]/g, '_').substring(0, 30); const cleanUri = uri.replace(/[<>:"/\\|?*]/g, '_').substring(0, 30);
return `🌐 ${cleanUri}`; return `🌐 ${cleanUri}`;
} catch (error) { } catch {
// 如果解析失敗,使用安全的預設名稱 // 如果解析失敗,使用安全的預設名稱
const cleanUri = uri.replace(/[<>:"/\\|?*]/g, '_').substring(0, 30); const cleanUri = uri.replace(/[<>:"/\\|?*]/g, '_').substring(0, 30);
return `🌐 ${cleanUri}`; return `🌐 ${cleanUri}`;

View file

@ -4,6 +4,14 @@
*/ */
// 從 obsidian://open?vault=...&file=... 解析出 vault 名稱與檔案路徑 // 從 obsidian://open?vault=...&file=... 解析出 vault 名稱與檔案路徑
interface ObsidianWindow extends Window {
app?: {
vault?: {
getName?: () => string;
};
};
}
export function parseObsidianOpenUris(input: string): Array<{ vault?: string; file?: string }> { export function parseObsidianOpenUris(input: string): Array<{ vault?: string; file?: string }> {
const results: Array<{ vault?: string; file?: string }> = []; const results: Array<{ vault?: string; file?: string }> = [];
if (!input) return results; if (!input) return results;
@ -44,7 +52,7 @@ export async function extractObsidianPathsFromDT(dt: DataTransfer | null): Promi
console.error('GridExplorer: Error getting data from DataTransfer', error); console.error('GridExplorer: Error getting data from DataTransfer', error);
} }
const vaultName = (window as any).app?.vault?.getName?.() as string | undefined; const vaultName = (window as ObsidianWindow).app?.vault?.getName?.();
const paths: string[] = []; const paths: string[] = [];
for (const t of texts) { for (const t of texts) {
const entries = parseObsidianOpenUris(t); const entries = parseObsidianOpenUris(t);

View file

@ -110,15 +110,16 @@ export function sortFiles(files: TFile[], gridView: GridView, overrideSortType?:
return { return {
file, file,
mDate: (() => { mDate: (() => {
if (metadata?.frontmatter) { const frontmatter = metadata?.frontmatter;
if (frontmatter) {
// 支援多個欄位名稱(用逗號隔開) // 支援多個欄位名稱(用逗號隔開)
const fieldNames = settings.modifiedDateField const fieldNames = settings.modifiedDateField
? settings.modifiedDateField.split(',').map(f => f.trim()).filter(Boolean) ? settings.modifiedDateField.split(',').map(f => f.trim()).filter(Boolean)
: []; : [];
for (const fieldName of fieldNames) { for (const fieldName of fieldNames) {
const dateStr = metadata.frontmatter[fieldName]; const dateValue: unknown = frontmatter[fieldName];
if (dateStr) { if (typeof dateValue === 'string' || typeof dateValue === 'number' || dateValue instanceof Date) {
const date = new Date(dateStr); const date = new Date(dateValue);
if (!isNaN(date.getTime())) { if (!isNaN(date.getTime())) {
return date.getTime(); return date.getTime();
} }
@ -128,15 +129,16 @@ export function sortFiles(files: TFile[], gridView: GridView, overrideSortType?:
return file.stat.mtime; return file.stat.mtime;
})(), })(),
cDate: (() => { cDate: (() => {
if (metadata?.frontmatter) { const frontmatter = metadata?.frontmatter;
if (frontmatter) {
// 支援多個欄位名稱(用逗號隔開) // 支援多個欄位名稱(用逗號隔開)
const fieldNames = settings.createdDateField const fieldNames = settings.createdDateField
? settings.createdDateField.split(',').map(f => f.trim()).filter(Boolean) ? settings.createdDateField.split(',').map(f => f.trim()).filter(Boolean)
: []; : [];
for (const fieldName of fieldNames) { for (const fieldName of fieldNames) {
const dateStr = metadata.frontmatter[fieldName]; const dateValue: unknown = frontmatter[fieldName];
if (dateStr) { if (typeof dateValue === 'string' || typeof dateValue === 'number' || dateValue instanceof Date) {
const date = new Date(dateStr); const date = new Date(dateValue);
if (!isNaN(date.getTime())) { if (!isNaN(date.getTime())) {
return date.getTime(); return date.getTime();
} }
@ -186,7 +188,7 @@ export function isFolderIgnored(folder: TFolder, ignoredFolders: string[], ignor
// 檢查資料夾名稱是否包含模式字串(不區分大小寫) // 檢查資料夾名稱是否包含模式字串(不區分大小寫)
return folder.name.toLowerCase().includes(pattern.toLowerCase()); return folder.name.toLowerCase().includes(pattern.toLowerCase());
} }
} catch (error) { } catch {
// 如果正則表達式無效,直接檢查資料夾名稱 // 如果正則表達式無效,直接檢查資料夾名稱
return folder.name.toLowerCase().includes(pattern.toLowerCase()); return folder.name.toLowerCase().includes(pattern.toLowerCase());
} }
@ -214,7 +216,7 @@ export function ignoredFiles(files: TFile[], gridView: GridView): TFile[] {
// 檢查檔案的 metadata 是否有 hidden: true // 檢查檔案的 metadata 是否有 hidden: true
if (file.extension === 'md') { if (file.extension === 'md') {
const metadata = gridView.app.metadataCache.getFileCache(file)?.frontmatter; const metadata = gridView.app.metadataCache.getFileCache(file)?.frontmatter;
const displayValue = metadata?.display; const displayValue: unknown = metadata?.display;
if (displayValue === 'hidden') { if (displayValue === 'hidden') {
return false; return false;
} }
@ -303,8 +305,8 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean):
const resolvedLinks = app.metadataCache.resolvedLinks; const resolvedLinks = app.metadataCache.resolvedLinks;
for (const [sourcePath, links] of Object.entries(resolvedLinks)) { for (const [sourcePath, links] of Object.entries(resolvedLinks)) {
if (Object.keys(links).includes(activeFile.path)) { if (Object.keys(links).includes(activeFile.path)) {
const sourceFile = app.vault.getAbstractFileByPath(sourcePath) as TFile; const sourceFile = app.vault.getAbstractFileByPath(sourcePath);
if (sourceFile) { if (sourceFile instanceof TFile) {
backlinks.add(sourceFile); backlinks.add(sourceFile);
} }
} }
@ -325,8 +327,8 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean):
if (fileLinks) { if (fileLinks) {
for (const targetPath of Object.keys(fileLinks)) { for (const targetPath of Object.keys(fileLinks)) {
const targetFile = app.vault.getAbstractFileByPath(targetPath) as TFile; const targetFile = app.vault.getAbstractFileByPath(targetPath);
if (targetFile && (isDocumentFile(targetFile) || if (targetFile instanceof TFile && (isDocumentFile(targetFile) ||
(settings.showMediaFiles && isMediaFile(targetFile)))) { (settings.showMediaFiles && isMediaFile(targetFile)))) {
outgoingLinks.add(targetFile); outgoingLinks.add(targetFile);
} }
@ -356,7 +358,7 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean):
} }
} }
} }
} catch (_e) { } catch {
// 忽略讀取錯誤 // 忽略讀取錯誤
} }
} }
@ -481,16 +483,16 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean):
// 用 gridView.taskFilter 匹配 uncompleted、completed、all 任務 // 用 gridView.taskFilter 匹配 uncompleted、completed、all 任務
if (gridView.taskFilter === 'uncompleted') { if (gridView.taskFilter === 'uncompleted') {
// 只匹配未完成的任務: - [ ] 或 * [ ] // 只匹配未完成的任務: - [ ] 或 * [ ]
shouldAdd = /^[\s]*[-*]\s*\[\s*\](?![^\[]*\[\s*[^\s\]]+\]).*$/m.test(content); shouldAdd = /^[\s]*[-*]\s*\[\s*\](?![^[]*\[\s*[^\s\]]+\]).*$/m.test(content);
} else if (gridView.taskFilter === 'completed') { } else if (gridView.taskFilter === 'completed') {
// 只匹配所有任務均已完成的檔案(至少有一個已完成且沒有未完成) // 只匹配所有任務均已完成的檔案(至少有一個已完成且沒有未完成)
const hasCompleted = /^[\s]*[-*]\s*\[x\](?![^\[]*\[\s*[^\s\]]+\]).*$/m.test(content); const hasCompleted = /^[\s]*[-*]\s*\[x\](?![^[]*\[\s*[^\s\]]+\]).*$/m.test(content);
const hasIncomplete = /^[\s]*[-*]\s*\[\s*\](?![^\[]*\[\s*[^\s\]]+\]).*$/m.test(content); const hasIncomplete = /^[\s]*[-*]\s*\[\s*\](?![^[]*\[\s*[^\s\]]+\]).*$/m.test(content);
shouldAdd = hasCompleted && !hasIncomplete; shouldAdd = hasCompleted && !hasIncomplete;
} else if (gridView.taskFilter === 'all') { } else if (gridView.taskFilter === 'all') {
// 匹配任何任務(已完成或未完成皆可) // 匹配任何任務(已完成或未完成皆可)
const hasIncomplete = /^[\s]*[-*]\s*\[\s*\](?![^\[]*\[\s*[^\s\]]+\]).*$/m.test(content); const hasIncomplete = /^[\s]*[-*]\s*\[\s*\](?![^[]*\[\s*[^\s\]]+\]).*$/m.test(content);
const hasCompleted = /^[\s]*[-*]\s*\[x\](?![^\[]*\[\s*[^\s\]]+\]).*$/m.test(content); const hasCompleted = /^[\s]*[-*]\s*\[x\](?![^[]*\[\s*[^\s\]]+\]).*$/m.test(content);
shouldAdd = hasIncomplete || hasCompleted; shouldAdd = hasIncomplete || hasCompleted;
} }
@ -567,7 +569,7 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean):
dvApi.current = () => dvApi.page(activeFile.path); dvApi.current = () => dvApi.page(activeFile.path);
} }
const func = new Function('app', 'dv', dvCode!); const func = new Function('app', 'dv', dvCode);
const dvPagesResult = func(app, dvApi); const dvPagesResult = func(app, dvApi);
const dvPages = Array.isArray(dvPagesResult) ? dvPagesResult : Array.from(dvPagesResult || []); const dvPages = Array.isArray(dvPagesResult) ? dvPagesResult : Array.from(dvPagesResult || []);
@ -586,7 +588,7 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean):
} }
} }
} }
return sortFiles(Array.from(files) as TFile[], gridView); return sortFiles(Array.from(files), gridView);
} catch (error) { } catch (error) {
console.error('Grid Explorer: Error executing Dataview query.', error); console.error('Grid Explorer: Error executing Dataview query.', error);
return []; return [];

View file

@ -115,7 +115,7 @@ async function validateRemoteImage(url: string): Promise<string | null> {
return url; return url;
} }
} }
} catch (error) { } catch {
try { try {
const getResponse = await requestUrl({ url }); const getResponse = await requestUrl({ url });
if (isSuccessfulStatus(getResponse)) { if (isSuccessfulStatus(getResponse)) {

View file

@ -56,6 +56,10 @@
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
.ge-grid-item-full-height {
height: 100%;
}
/* 右上角懸浮開啟筆記按鈕 Hover Open-in-Grid Button */ /* 右上角懸浮開啟筆記按鈕 Hover Open-in-Grid Button */
/* .ge-grid-item { position: relative; } /* .ge-grid-item { position: relative; }
.ge-hover-open-note { .ge-hover-open-note {
@ -619,6 +623,16 @@
margin-bottom: -2px; margin-bottom: -2px;
} }
.ge-ios-divider {
width: calc(100% - 16px);
}
.ge-load-more-sentinel {
height: 1px;
width: 100%;
flex-shrink: 0;
}
/* 置頂分隔器圖示 */ /* 置頂分隔器圖示 */
.ge-pin-divider::before { .ge-pin-divider::before {
content: "📌"; content: "📌";
@ -1372,6 +1386,7 @@ a.ge-current-folder:hover {
.ge-search-text { .ge-search-text {
font-size: 14px; font-size: 14px;
color: var(--text-normal); color: var(--text-normal);
cursor: pointer;
flex-grow: 1; flex-grow: 1;
/* 讓文字填滿剩餘空間 */ /* 讓文字填滿剩餘空間 */
min-width: 0; min-width: 0;
@ -1632,6 +1647,11 @@ a.ge-current-folder:hover {
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
} }
.ge-floating-audio-player.ge-audio-focus-highlight {
box-shadow: 0 0 10px 2px var(--interactive-accent);
transition: box-shadow 0.1s ease-in-out;
}
.ge-audio-title { .ge-audio-title {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
@ -1779,6 +1799,32 @@ a.ge-current-folder:hover {
} }
/* Custom Mode Options */ /* Custom Mode Options */
.ge-hidden {
display: none !important;
}
.ge-custommode-icon-input {
width: 3em;
min-width: 3em;
}
.ge-custommode-dataview-setting {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.ge-custommode-dataview-controls {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.ge-custommode-code-input {
width: 100%;
}
.ge-custommode-option-container { .ge-custommode-option-container {
border: 1px solid var(--background-modifier-border); border: 1px solid var(--background-modifier-border);
border-radius: 6px; border-radius: 6px;
@ -2129,6 +2175,15 @@ a.ge-current-folder:hover {
font-size: var(--font-text-size); font-size: var(--font-text-size);
} }
.ge-note-sidebar-scroll-container {
background-color: var(--background-secondary);
font-size: 1em;
}
.ge-note-sidebar-content-container {
padding: 15px;
}
.ge-note-close-button, .ge-note-close-button,
.ge-note-edit-button, .ge-note-edit-button,
.ge-note-info-button { .ge-note-info-button {
@ -2803,6 +2858,11 @@ a.ge-current-folder:hover {
pointer-events: none; pointer-events: none;
} }
.ge-explorer-stash-dropzone-clickable {
pointer-events: auto;
cursor: pointer;
}
.ge-explorer-stash-item { .ge-explorer-stash-item {
padding-left: 28px; padding-left: 28px;
/* 與 .ge-explorer-mode-item 一致的縮排 */ /* 與 .ge-explorer-mode-item 一致的縮排 */