From b1887f3ad76a43bb91cf3a0b3aabcdcbcae86764 Mon Sep 17 00:00:00 2001 From: Devon22 Date: Fri, 15 May 2026 17:52:33 +0800 Subject: [PATCH] 3.2.2 --- eslint.config.mjs | 1 + manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/ExplorerView.ts | 331 ++++++++++++------------ src/FloatingAudioPlayer.ts | 5 +- src/GridView.ts | 363 ++++++++++++++++++--------- src/main.ts | 80 ++++-- src/modal/customModeModal.ts | 43 ++-- src/modal/folderNoteSettingsModal.ts | 109 ++++---- src/modal/folderSelectionModal.ts | 36 ++- src/modal/inputModal.ts | 8 +- src/modal/mediaModal.ts | 109 ++++---- src/modal/noteSettingsModal.ts | 95 ++++--- src/modal/searchModal.ts | 63 +++-- src/modal/shortcutSelectionModal.ts | 2 +- src/renderFiles.ts | 13 +- src/renderFolder.ts | 57 +++-- src/renderHeaderButton.ts | 54 +++- src/renderModePath.ts | 88 +++++-- src/utils/createItemUtils.ts | 22 +- src/utils/dragUtils.ts | 10 +- src/utils/fileUtils.ts | 46 ++-- src/utils/mediaUtils.ts | 2 +- styles.css | 60 +++++ 25 files changed, 1018 insertions(+), 587 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index c586a51..a12af7d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -30,5 +30,6 @@ export default tseslint.config( "version-bump.mjs", "versions.json", "main.js", + "package.json" ]), ); \ No newline at end of file diff --git a/manifest.json b/manifest.json index 12b6eb5..9ee9731 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "gridexplorer", "name": "GridExplorer", - "version": "3.2.1", + "version": "3.2.2", "minAppVersion": "1.8.7", "description": "Browse note files in a grid view.", "author": "Devon22", diff --git a/package-lock.json b/package-lock.json index 965c265..a4e7464 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gridexplorer", - "version": "3.2.1", + "version": "3.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gridexplorer", - "version": "3.2.1", + "version": "3.2.2", "license": "MIT", "devDependencies": { "@types/node": "^16.11.6", diff --git a/package.json b/package.json index deb81dc..650b41b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gridexplorer", - "version": "3.2.1", + "version": "3.2.2", "description": "Browse note files in a grid view.", "main": "main.js", "scripts": { diff --git a/src/ExplorerView.ts b/src/ExplorerView.ts index 0e124c5..e2c9009 100644 --- a/src/ExplorerView.ts +++ b/src/ExplorerView.ts @@ -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 { GridView } from './GridView'; import { isFolderIgnored, isImageFile, isVideoFile, isAudioFile, isMediaFile } from './utils/fileUtils'; @@ -12,10 +12,21 @@ import { MediaModal } from './modal/mediaModal'; import { ShortcutSelectionModal } from './modal/shortcutSelectionModal'; import { FloatingAudioPlayer } from './FloatingAudioPlayer'; import { t } from './translations'; +import { CustomMode, GallerySettings } from './settings'; // 探索器視圖類型常數 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 { + return typeof value === 'object' && value !== null; +} + export class ExplorerView extends ItemView { plugin: GridExplorerPlugin; @@ -104,7 +115,7 @@ export class ExplorerView extends ItemView { } // 恢復視圖狀態 - async setState(state: any, result?: any): Promise { + async setState(state: unknown, result: ViewStateResult): Promise { await super.setState(state, result); this.restoreExpandedPaths(state); @@ -117,8 +128,8 @@ export class ExplorerView extends ItemView { } // 恢復展開路徑狀態 - private restoreExpandedPaths(state: any) { - if (state?.expandedPaths && Array.isArray(state.expandedPaths)) { + private restoreExpandedPaths(state: unknown) { + if (isRecord(state) && Array.isArray(state.expandedPaths)) { this.expandedPaths = new Set( state.expandedPaths.filter((p: unknown) => typeof p === 'string') ); @@ -128,19 +139,16 @@ export class ExplorerView extends ItemView { } // 恢復搜尋查詢 - private restoreSearchQuery(state: any) { - this.searchQuery = (state?.searchQuery && typeof state.searchQuery === 'string') + private restoreSearchQuery(state: unknown) { + this.searchQuery = (isRecord(state) && typeof state.searchQuery === 'string') ? state.searchQuery : ''; } // 恢復暫存檔案列表 - private restoreStashFiles(state: any) { - if (state?.stashFilePaths && Array.isArray(state.stashFilePaths)) { - const validPaths = state.stashFilePaths - .filter((p: unknown) => typeof p === 'string' && p) - .filter((p: string) => this.app.vault.getAbstractFileByPath(p) instanceof TFile); - this.stashFilePaths = Array.from(new Set(validPaths)); + private restoreStashFiles(state: unknown) { + if (isRecord(state) && Array.isArray(state.stashFilePaths)) { + this.stashFilePaths = this.getValidStashPaths(state.stashFilePaths as unknown[]); // 將還原的暫存區同步回設定,確保下次新建視圖也能取得 this.persistStashToSettings(); } @@ -150,22 +158,27 @@ export class ExplorerView extends ItemView { private loadStashFromSettingsIfNeeded() { try { 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; - const validPaths = paths - .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.stashFilePaths = this.getValidStashPaths(paths as unknown[]); } catch (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() { try { 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); } catch (error) { @@ -197,14 +210,18 @@ export class ExplorerView extends ItemView { this.eventRefs.push( vault.on('create', schedule), - vault.on('delete', (file: any) => this.handleFileDelete(file, schedule)), - vault.on('rename', (file: any, oldPath: string) => this.handleFileRename(file, oldPath, schedule)) + vault.on('delete', (file: unknown) => this.handleFileDelete(file, 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 newPath = payload?.path ?? this.lastPath; + const sourceChangedPayload: CustomEventPayload = isRecord(payload) ? { + 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) { @@ -229,13 +246,13 @@ export class ExplorerView extends ItemView { } // 處理檔案刪除事件 - private handleFileDelete(file: any, schedule: () => void) { - const path = file?.path as string | undefined; + private handleFileDelete(file: unknown, schedule: () => void) { + const path = isRecord(file) && typeof file.path === 'string' ? file.path : undefined; if (path) { this.removeExpandedPrefix(path); if (file instanceof TFile) { this.stashFilePaths = this.stashFilePaths.filter(p => p !== path); - this.app.workspace.requestSaveLayout(); + void this.app.workspace.requestSaveLayout(); this.persistStashToSettings(); } } @@ -243,13 +260,13 @@ export class ExplorerView extends ItemView { } // 處理檔案重新命名事件 - private handleFileRename(file: any, oldPath: string, schedule: () => void) { - const newPath = file?.path as string || ''; + private handleFileRename(file: unknown, oldPath: string, schedule: () => void) { + const newPath = isRecord(file) && typeof file.path === 'string' ? file.path : ''; if (oldPath && newPath) { this.renameExpandedPrefix(oldPath, newPath); if (file instanceof TFile) { this.stashFilePaths = this.stashFilePaths.map(p => p === oldPath ? newPath : p); - this.app.workspace.requestSaveLayout(); + void this.app.workspace.requestSaveLayout(); 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 { - 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); } catch (error) { console.warn(`Failed to register event ${eventName}:`, error); @@ -309,7 +326,7 @@ export class ExplorerView extends ItemView { // 在新視圖中開啟資料夾 private openFolderInNewView(folderPath: string) { const { workspace } = this.app; - let leaf: any = null; + let leaf: WorkspaceLeaf | null = null; switch (this.plugin.settings.defaultOpenLocation) { case 'left': leaf = workspace.getLeftLeaf(false); @@ -323,9 +340,9 @@ export class ExplorerView extends ItemView { break; } 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) { - leaf.view.setSource('folder', folderPath); + void leaf.view.setSource('folder', folderPath); } void workspace.revealLeaf(leaf); } @@ -333,7 +350,7 @@ export class ExplorerView extends ItemView { // 在新視圖中開啟模式 private openModeInNewView(mode: string) { const { workspace } = this.app; - let leaf: any = null; + let leaf: WorkspaceLeaf | null = null; switch (this.plugin.settings.defaultOpenLocation) { case 'left': leaf = workspace.getLeftLeaf(false); @@ -347,9 +364,9 @@ export class ExplorerView extends ItemView { break; } 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) { - leaf.view.setSource(mode); + void leaf.view.setSource(mode); } void workspace.revealLeaf(leaf); } @@ -410,7 +427,7 @@ export class ExplorerView extends ItemView { // 創建搜尋容器 private createSearchContainer(contentEl: HTMLElement) { 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.placeholder = t?.('search') || 'Search'; this.searchInputEl.value = this.searchQuery; @@ -458,7 +475,7 @@ export class ExplorerView extends ItemView { if (!this.isComposing) { this.keepSearchFocus = true; 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 = ''; clearBtn.removeClass('show'); this.scheduleRender(); - this.app.workspace.requestSaveLayout(); + void this.app.workspace.requestSaveLayout(); window.setTimeout(() => this.searchInputEl?.focus(), 0); } @@ -503,15 +520,15 @@ export class ExplorerView extends ItemView { setIcon(searchIcon, 'search'); const searchName = searchItem.createSpan({ cls: 'ge-explorer-folder-name' }); searchName.textContent = `${t ? t('search_for') : 'Search for'} "${trimmed}"`; - searchItem.addEventListener('click', async (evt) => { + searchItem.addEventListener('click', (evt) => { evt.stopPropagation(); - await this.openFolderSearch(trimmed); + void this.openFolderSearch(trimmed); }); // 鍵盤支援:Enter 觸發、ArrowUp 返回輸入框、Escape 清空並返回 - searchItem.addEventListener('keydown', async (evt: KeyboardEvent) => { + searchItem.addEventListener('keydown', (evt: KeyboardEvent) => { if (evt.key === 'Enter') { evt.preventDefault(); - await this.openFolderSearch(trimmed); + void this.openFolderSearch(trimmed); } else if (evt.key === 'ArrowUp') { evt.preventDefault(); this.searchInputEl?.focus(); @@ -523,7 +540,7 @@ export class ExplorerView extends ItemView { clearBtn?.removeClass('show'); this.scheduleRender(); // 通知 Obsidian 保存視圖狀態 - this.app.workspace.requestSaveLayout(); + void this.app.workspace.requestSaveLayout(); window.setTimeout(() => this.searchInputEl?.focus(), 0); } }); @@ -630,16 +647,16 @@ export class ExplorerView extends ItemView { } // 獲取自定義模式項目 - private getCustomModeItems(settings: any) { - const customModes = (settings?.customModes ?? []).filter((cm: any) => cm?.enabled !== false); + private getCustomModeItems(settings: GallerySettings): ModeItem[] { + const customModes = settings.customModes.filter((cm: CustomMode) => cm.enabled !== false); return customModes - .filter((cm: any) => { + .filter((cm: CustomMode) => { if (!this.isFiltering()) return true; const baseLabel = cm.displayName || cm.internalName || 'Custom'; return this.matchesQuery(baseLabel); }) - .map((cm: any) => { + .map((cm: CustomMode) => { const baseLabel = cm.displayName || cm.internalName || 'Custom'; const internalName = cm.internalName || `custom-${baseLabel}`; const textIcon = cm.icon ? `${cm.icon} ` : ''; @@ -647,13 +664,13 @@ export class ExplorerView extends ItemView { key: internalName, label: `${textIcon}${baseLabel}`, icon: '', - onClick: () => this.openMode(internalName) + onClick: () => { void this.openMode(internalName); } }; }); } // 獲取內建模式項目 - private getBuiltinModeItems(settings: any) { + private getBuiltinModeItems(settings: GallerySettings): ModeItem[] { const builtInModes = this.getEnabledBuiltInModes(settings); return builtInModes @@ -661,12 +678,12 @@ export class ExplorerView extends ItemView { .map(m => { const emoji = this.BUILTIN_MODE_EMOJIS[m.key] || ''; 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 = [ { key: 'bookmarks', label: t?.('bookmarks_mode') || 'Bookmarks', icon: 'bookmark', enabled: !!settings?.showBookmarksMode }, { 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) { contentEl.scrollTop = prevScrollTop; - requestAnimationFrame(() => { + window.requestAnimationFrame(() => { 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; 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(); items.forEach(({ key, label, icon, onClick }) => { @@ -795,16 +812,16 @@ export class ExplorerView extends ItemView { item.setTitle(t?.('edit_custom_mode') || 'Edit Custom Mode') .setIcon('settings') .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; - 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; void this.plugin.saveSettings(); this.scheduleRender(); }).open(); }); }); - menu.showAtMouseEvent(evt as MouseEvent); + menu.showAtMouseEvent(evt); }); } @@ -840,7 +857,7 @@ export class ExplorerView extends ItemView { const view = await this.plugin.activateView(); if (view instanceof GridView) { - await view.clearSelection(); + view.clearSelection(); await view.setSource('', '', true, searchTerm); } } @@ -877,7 +894,7 @@ export class ExplorerView extends ItemView { // 高亮 vault 根(當前為 folder 模式且路徑為根) const vaultRoot = this.app.vault.getRoot(); - const rootPath = (vaultRoot as any).path ?? '/'; + const rootPath = vaultRoot.path || '/'; if (currentMode === 'folder' && (currentPath === rootPath || currentPath === '' || currentPath === '/')) { foldersHeader.addClass('is-active'); } @@ -921,7 +938,7 @@ export class ExplorerView extends ItemView { // 開啟 Vault 根目錄 const root = this.app.vault.getRoot(); 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 { // 點擊空白區域:展開/收合 const newExpanded = !this.isExpanded(foldersGroupKey); @@ -1010,6 +1027,7 @@ export class ExplorerView extends ItemView { const toggle = header.createSpan({ cls: 'ge-explorer-folder-toggle' }); const icon = header.createSpan({ cls: 'ge-explorer-folder-icon' }); + setIcon(icon, 'folder'); const name = header.createSpan({ cls: 'ge-explorer-folder-name' }); name.textContent = folder.name || '/'; @@ -1049,12 +1067,12 @@ export class ExplorerView extends ItemView { const noteFile = this.app.vault.getAbstractFileByPath(notePath); if (noteFile instanceof TFile) { const metadata = this.app.metadataCache.getFileCache(noteFile)?.frontmatter; - const colorValue = (metadata as any)?.color as string | undefined; - if (colorValue) { + const colorValue: unknown = metadata?.color; + if (typeof colorValue === 'string' && colorValue) { name.parentElement?.addClass(`ge-folder-color-${colorValue}`); } - const iconValue = (metadata as any)?.icon as string | undefined; - if (iconValue && name) { + const iconValue: unknown = metadata?.icon; + if (typeof iconValue === 'string' && iconValue && name) { name.textContent = `${iconValue} ${folder.name || '/'}`; return true; } @@ -1206,7 +1224,7 @@ export class ExplorerView extends ItemView { } // 其他情況:直接開啟該資料夾的 GridView - this.openFolderInGrid(folder.path); + void this.openFolderInGrid(folder.path); } // 渲染暫存區群組 @@ -1278,9 +1296,9 @@ export class ExplorerView extends ItemView { menu.addItem((item) => { item.setTitle(t('save_stash_as_markdown')) .setIcon('file-plus') - .onClick(() => this.saveStashAsMarkdown()); + .onClick(() => { void this.saveStashAsMarkdown(); }); }); - menu.showAtMouseEvent(evt as MouseEvent); + menu.showAtMouseEvent(evt); }); this.setupStashDropTarget(nodeEl); @@ -1290,8 +1308,6 @@ export class ExplorerView extends ItemView { private setupStashDropTarget(nodeEl: HTMLElement) { if (!Platform.isDesktop) return; - let currentDropTarget: HTMLElement | null = null; - let insertPosition: 'before' | 'after' | 'end' = 'end'; let targetIndex = -1; nodeEl.addEventListener('dragover', (e: DragEvent) => { @@ -1324,8 +1340,6 @@ export class ExplorerView extends ItemView { }); if (closestItem && closestDistance < 50) { // 50px 容忍範圍 - currentDropTarget = closestItem; - insertPosition = insertBefore ? 'before' : 'after'; // 顯示插入指示器 if (insertBefore) { @@ -1335,8 +1349,6 @@ export class ExplorerView extends ItemView { } } else { // 沒有找到合適的項目,插入到最後 - currentDropTarget = null; - insertPosition = 'end'; targetIndex = stashItems.length; 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) { this.clearStashInsertIndicators(nodeEl); nodeEl.removeClass('ge-dragover'); - currentDropTarget = null; } }); - nodeEl.addEventListener('drop', async (e: DragEvent) => { - if (e.dataTransfer?.types?.includes('application/obsidian-ge-stash')) return; - e.preventDefault(); - - this.clearStashInsertIndicators(nodeEl); - nodeEl.removeClass('ge-dragover'); - - await this.handleStashDrop(e, targetIndex); - - currentDropTarget = null; - insertPosition = 'end'; - targetIndex = -1; + nodeEl.addEventListener('drop', (e: DragEvent) => { + void (async () => { + if (e.dataTransfer?.types?.includes('application/obsidian-ge-stash')) return; + e.preventDefault(); + + this.clearStashInsertIndicators(nodeEl); + nodeEl.removeClass('ge-dragover'); + + await this.handleStashDrop(e, targetIndex); + targetIndex = -1; + })(); }); } @@ -1404,6 +1414,7 @@ export class ExplorerView extends ItemView { // 渲染暫存拖放區 private renderStashDropzone(children: HTMLElement) { 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' }); setIcon(dzIcon, 'plus'); const dzName = dropzone.createSpan({ cls: 'ge-explorer-folder-name' }); @@ -1411,8 +1422,6 @@ export class ExplorerView extends ItemView { // 讓 dropzone 可點擊以選擇檔案加入 // 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('tabindex', '0'); dropzone.addEventListener('click', (evt) => { @@ -1568,7 +1577,7 @@ export class ExplorerView extends ItemView { // 捷徑檔:啟用 GridView 並嘗試以捷徑邏輯開啟 const view = await this.plugin.activateView(); 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); } } else { @@ -1591,7 +1600,7 @@ export class ExplorerView extends ItemView { .setIcon('x') .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); this.stashFilePaths = newList; - this.app.workspace.requestSaveLayout(); + void this.app.workspace.requestSaveLayout(); this.persistStashToSettings(); this.scheduleRender(); } @@ -1707,7 +1716,7 @@ export class ExplorerView extends ItemView { // 與一般文字處理一致:先嘗試絕對路徑,再用 linkpath 解析 let file = this.app.vault.getAbstractFileByPath(p); 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 (file instanceof TFile) resolved.push(file.path); @@ -1740,14 +1749,14 @@ export class ExplorerView extends ItemView { if (line.startsWith('[[') && line.endsWith(']]')) { const inner = line.slice(2, -2); 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; } else { const direct = this.app.vault.getAbstractFileByPath(line); if (direct instanceof TFile) { resolved = direct; } 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; } } @@ -1798,7 +1807,7 @@ export class ExplorerView extends ItemView { this.stashFilePaths = [...this.stashFilePaths, ...validPaths]; } - this.app.workspace.requestSaveLayout(); + void this.app.workspace.requestSaveLayout(); this.persistStashToSettings(); this.scheduleRender(); } @@ -1806,7 +1815,7 @@ export class ExplorerView extends ItemView { // 從暫存區移除 private removeFromStash(path: string) { this.stashFilePaths = this.stashFilePaths.filter(p => p !== path); - this.app.workspace.requestSaveLayout(); + void this.app.workspace.requestSaveLayout(); this.persistStashToSettings(); this.scheduleRender(); } @@ -1814,7 +1823,7 @@ export class ExplorerView extends ItemView { // 清空暫存區 private clearStash() { this.stashFilePaths = []; - this.app.workspace.requestSaveLayout(); + void this.app.workspace.requestSaveLayout(); this.persistStashToSettings(); this.scheduleRender(); } @@ -1885,8 +1894,8 @@ export class ExplorerView extends ItemView { menu.addItem((item) => { item.setTitle(t('new_note')) .setIcon('square-pen') - .onClick(async () => { - await createNewNote(this.app, folder.path); + .onClick(() => { + void createNewNote(this.app, folder.path); }); }); @@ -1894,8 +1903,8 @@ export class ExplorerView extends ItemView { menu.addItem((item) => { item.setTitle(t('new_folder')) .setIcon('folder') - .onClick(async () => { - await createNewFolder(this.app, folder.path, () => { + .onClick(() => { + void createNewFolder(this.app, folder.path, () => { this.scheduleRender(); }); }); @@ -1905,8 +1914,8 @@ export class ExplorerView extends ItemView { menu.addItem((item) => { item.setTitle(t('new_canvas')) .setIcon('layout-dashboard') - .onClick(async () => { - await createNewCanvas(this.app, folder.path); + .onClick(() => { + void createNewCanvas(this.app, folder.path); }); }); @@ -1914,8 +1923,8 @@ export class ExplorerView extends ItemView { menu.addItem((item) => { item.setTitle(t('new_base')) .setIcon('layout-dashboard') - .onClick(async () => { - await createNewBase(this.app, folder.path); + .onClick(() => { + void createNewBase(this.app, folder.path); }); }); @@ -1923,9 +1932,9 @@ export class ExplorerView extends ItemView { menu.addItem((item) => { item.setTitle(t('new_shortcut')) .setIcon('shuffle') - .onClick(async () => { - const modal = new ShortcutSelectionModal(this.app, this.plugin, async (option) => { - await createShortcut(this.app, folder.path, option); + .onClick(() => { + const modal = new ShortcutSelectionModal(this.app, this.plugin, (option) => { + void createShortcut(this.app, folder.path, option); }); modal.open(); }); @@ -1962,15 +1971,17 @@ export class ExplorerView extends ItemView { menu.addItem((item) => { item.setTitle(t('open_folder_note')) .setIcon('panel-left-open') - .onClick(async () => { - const view = await this.plugin.activateView(); - if (view instanceof GridView) { - if (!view.openShortcutFile(noteFile)) { + .onClick(() => { + void (async () => { + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + if (!view.openShortcutFile(noteFile)) { + void this.app.workspace.getLeaf().openFile(noteFile); + } + } else { 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) => { item.setTitle(t('edit_folder_note_settings')) .setIcon('settings-2') - .onClick(async () => { - const view = await this.plugin.activateView(); - if (view instanceof GridView) { - showFolderNoteSettingsModal(this.app, this.plugin, folder, view); - } + .onClick(() => { + void (async () => { + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + showFolderNoteSettingsModal(this.app, this.plugin, folder, view); + } + })(); }); }); @@ -2006,11 +2019,13 @@ export class ExplorerView extends ItemView { menu.addItem((item) => { item.setTitle(t('create_folder_note')) .setIcon('file-cog') - .onClick(async () => { - const view = await this.plugin.activateView(); - if (view instanceof GridView) { - showFolderNoteSettingsModal(this.app, this.plugin, folder, view); - } + .onClick(() => { + void (async () => { + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + showFolderNoteSettingsModal(this.app, this.plugin, folder, view); + } + })(); }); }); } @@ -2027,13 +2042,15 @@ export class ExplorerView extends ItemView { menu.addItem((item) => { item.setTitle(isIgnored ? t('unignore_folder') : t('ignore_folder')) .setIcon(isIgnored ? 'folder-up' : 'folder-x') - .onClick(async () => { - if (isIgnored) { - this.plugin.settings.ignoredFolders = this.plugin.settings.ignoredFolders.filter(p => p !== folder.path); - } else { - this.plugin.settings.ignoredFolders.push(folder.path); - } - await this.plugin.saveSettings(); + .onClick(() => { + void (async () => { + if (isIgnored) { + this.plugin.settings.ignoredFolders = this.plugin.settings.ignoredFolders.filter(p => p !== folder.path); + } else { + this.plugin.settings.ignoredFolders.push(folder.path); + } + await this.plugin.saveSettings(); + })(); }); }); @@ -2041,11 +2058,13 @@ export class ExplorerView extends ItemView { menu.addItem((item) => { item.setTitle(t('move_folder')) .setIcon('folder-cog') - .onClick(async () => { - const view = await this.plugin.activateView(); - if (view instanceof GridView) { - new showFolderMoveModal(this.plugin, folder, view).open(); - } + .onClick(() => { + void (async () => { + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + new showFolderMoveModal(this.plugin, folder, view).open(); + } + })(); }); }); @@ -2053,21 +2072,23 @@ export class ExplorerView extends ItemView { menu.addItem((item) => { item.setTitle(t('rename_folder')) .setIcon('file-cog') - .onClick(async () => { - const view = await this.plugin.activateView(); - if (view instanceof GridView) { - showFolderRenameModal(this.app, this.plugin, folder, view); - } + .onClick(() => { + void (async () => { + const view = await this.plugin.activateView(); + if (view instanceof GridView) { + showFolderRenameModal(this.app, this.plugin, folder, view); + } + })(); }); }); // 刪除資料夾 menu.addItem((item) => { - (item as any).setWarning(true); - item.setTitle(t('delete_folder')) + item.setWarning(true) + .setTitle(t('delete_folder')) .setIcon('trash') - .onClick(async () => { - await this.app.fileManager.trashFile(folder); + .onClick(() => { + void this.app.fileManager.trashFile(folder); }); }); } @@ -2134,7 +2155,7 @@ export class ExplorerView extends ItemView { // 如果狀態確實發生變化,請求保存版面配置 // 這樣下次開啟時可以恢復展開狀態 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('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) { const event = evt as DragEvent; // 某些情況下事件非 DragEvent,需防禦處理 - if (typeof (event as any).preventDefault === 'function') { + if (typeof event.preventDefault === 'function') { event.preventDefault(); } // 確保從 header 本身移除樣式,而非子元素 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'); } @@ -2209,14 +2232,14 @@ export class ExplorerView extends ItemView { if (text.startsWith('[[') && text.endsWith(']]')) { const inner = text.slice(2, -2); 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; } else { const direct = this.app.vault.getAbstractFileByPath(text); if (direct instanceof TFile) { resolvedFile = direct; } 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; } } diff --git a/src/FloatingAudioPlayer.ts b/src/FloatingAudioPlayer.ts index 82ad942..4c6d95d 100644 --- a/src/FloatingAudioPlayer.ts +++ b/src/FloatingAudioPlayer.ts @@ -188,10 +188,9 @@ export class FloatingAudioPlayer { public focus(): void { this.containerEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); // 可以考慮添加一些視覺提示,例如短暫閃爍邊框 - this.containerEl.style.transition = 'box-shadow 0.1s ease-in-out'; - this.containerEl.style.boxShadow = '0 0 10px 2px var(--interactive-accent)'; + this.containerEl.addClass('ge-audio-focus-highlight'); window.setTimeout(() => { - this.containerEl.style.boxShadow = ''; + this.containerEl.removeClass('ge-audio-focus-highlight'); }, 300); } diff --git a/src/GridView.ts b/src/GridView.ts index 90c0880..9aa3a04 100644 --- a/src/GridView.ts +++ b/src/GridView.ts @@ -32,6 +32,130 @@ interface FileRenderParams { } // 定義網格視圖 +type GridViewLeaf = WorkspaceLeaf & { + pinned?: boolean; + updateHeader?: () => void; +}; + +interface DataviewEventSource { + on(name: 'dataview:index-ready', callback: () => void): ReturnType; +} + +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)[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 { plugin: GridExplorerPlugin; sourceMode: string = ''; // 模式選擇 @@ -103,11 +227,7 @@ export class GridView extends ItemView { // 阻止內建快捷鍵與其他監聽器 event.preventDefault(); // 停止後續所有監聽器(包含 Obsidian 內建 hotkey) - if (typeof (event as any).stopImmediatePropagation === 'function') { - (event as any).stopImmediatePropagation(); - } else { - event.stopPropagation(); - } + event.stopImmediatePropagation(); const parentPath = this.sourcePath.split('/').slice(0, -1).join('/') || '/'; void this.setSource('folder', parentPath); this.clearSelection(); @@ -122,18 +242,14 @@ export class GridView extends ItemView { if (this.selectedItemIndex >= 0 && this.selectedItemIndex < this.gridItems.length) { // 阻止可能的其他快捷鍵或後續處理,避免重複觸發 event.preventDefault(); - if (typeof (event as any).stopImmediatePropagation === 'function') { - (event as any).stopImmediatePropagation(); - } else { - event.stopPropagation(); - } + event.stopImmediatePropagation(); this.gridItems[this.selectedItemIndex].click(); } } }, true); // 註冊鍵盤事件處理 - this.registerDomEvent(document, 'keydown', (event: KeyboardEvent) => { + this.registerDomEvent(activeDocument, 'keydown', (event: KeyboardEvent) => { // 只有當 GridView 是活動視圖時才處理鍵盤事件 if (this.app.workspace.getActiveViewOfType(GridView) === this) { return handleKeyDown(this, event); @@ -142,7 +258,7 @@ export class GridView extends ItemView { // 監聽 dataview:index-ready 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-')) { void this.render(); } @@ -214,7 +330,7 @@ export class GridView extends ItemView { // 判斷當前Leaf是否被釘選 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; if (mdFile instanceof TFile) { 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') { tempLayout = metadata.cardLayout as 'horizontal' | 'vertical'; } @@ -351,7 +467,7 @@ export class GridView extends ItemView { // 發送自訂事件,通知 ExplorerView 目前來源已變更 try { - (this.app.workspace as any).trigger?.('ge-grid-source-changed', { + (this.app.workspace as WorkspaceEventTrigger).trigger?.('ge-grid-source-changed', { mode: this.sourceMode, 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 !== '/') { @@ -430,11 +546,11 @@ export class GridView extends ItemView { if (Array.isArray(metadata['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`); } else { - this.pinnedList = metadata['pinned']; + this.pinnedList = metadata['pinned'] as string[]; } } else if (this.plugin.settings.folderNoteDisplaySettings === 'pinned') { // 如果沒有置頂清單,則建立一個僅包含資料夾筆記的清單 @@ -446,7 +562,7 @@ export class GridView extends ItemView { // 渲染網格內容 await this.grid_render(); - (this.leaf as any).updateHeader(); + (this.leaf as GridViewLeaf).updateHeader?.(); // 如果有之前選中的檔案路徑,嘗試恢復選中狀態 @@ -474,7 +590,7 @@ export class GridView extends ItemView { attr: { placeholder: t('file_name_filter_placeholder') } - }) as HTMLInputElement; + }); const searchButton = inputWrapper.createEl('button', { cls: 'ge-file-filter-search clickable-icon', attr: { @@ -524,8 +640,8 @@ export class GridView extends ItemView { updateClearButton(); scheduleFilteredRender(); }); - filterInput.addEventListener('input', (event) => { - if (isComposingFilterText || (event as InputEvent).isComposing) return; + filterInput.addEventListener('input', (event: InputEvent) => { + if (isComposingFilterText || event.isComposing) return; this.fileNameFilterQuery = filterInput.value; updateClearButton(); scheduleFilteredRender(); @@ -577,7 +693,7 @@ export class GridView extends ItemView { if (fileNameFilterContainer) { fileNameFilterContainer.style.display = this.hideHeaderElements || !show ? 'none' : 'block'; if (show && !this.hideHeaderElements) { - const filterInput = fileNameFilterContainer.querySelector('.ge-file-filter-input') as HTMLInputElement | null; + const filterInput = fileNameFilterContainer.querySelector('.ge-file-filter-input'); filterInput?.focus(); } } @@ -622,15 +738,13 @@ export class GridView extends ItemView { const imageAreaWidth = settings.imageAreaWidth; const imageAreaHeight = this.cardLayout === 'vertical' ? settings.verticalImageAreaHeight : settings.imageAreaHeight; - container.style.setProperty('--grid-item-width', gridItemWidth + 'px'); - if (gridItemHeight === 0 || this.minMode) { - container.style.setProperty('--grid-item-height', '100%'); - } else { - container.style.setProperty('--grid-item-height', gridItemHeight + 'px'); - } - container.style.setProperty('--image-area-width', imageAreaWidth + 'px'); - container.style.setProperty('--image-area-height', imageAreaHeight + 'px'); - container.style.setProperty('--title-font-size', settings.titleFontSize + 'em'); + container.setCssProps({ + '--grid-item-width': `${gridItemWidth}px`, + '--grid-item-height': gridItemHeight === 0 || this.minMode ? '100%' : `${gridItemHeight}px`, + '--image-area-width': `${imageAreaWidth}px`, + '--image-area-height': `${imageAreaHeight}px`, + '--title-font-size': `${settings.titleFontSize}em`, + }); // 依圖片位置設定切換樣式類別 if (this.cardLayout === 'vertical') { @@ -673,7 +787,7 @@ export class GridView extends ItemView { 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')); return; } @@ -689,7 +803,7 @@ export class GridView extends ItemView { } // 顯示資料夾 - renderFolder(this, container); + void renderFolder(this, container); // 顯示檔案 let files = await renderFiles(this, container); @@ -776,7 +890,7 @@ export class GridView extends ItemView { let pEl: HTMLElement | null = null; if (!this.minMode) { let summaryField = this.plugin.settings.noteSummaryField || 'summary'; - let summaryValue = metadata?.[summaryField]; + let summaryValue = formatFrontmatterValue(getFrontmatterValue(metadata, summaryField)); if (this.sourceMode.startsWith('custom-')) { // 自訂模式下,使用自訂的 fields 來顯示摘要 const mode = this.plugin.settings.customModes.find(m => m.internalName === this.sourceMode); @@ -856,33 +970,29 @@ export class GridView extends ItemView { 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) { 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 - 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); } catch (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 .replace(//g, '') - .replace(/!?\[([^\]]*)\]\([^)]+\)|!?\[\[([^\]]+)\]\]/g, (match, p1, p2) => { + .replace(/!?\[([^\]]*)\]\([^)]+\)|!?\[\[([^\]]+)\]\]/g, (_match: string, p1?: string, p2?: string) => { const linkText = p1 || p2 || ''; if (!linkText) return ''; @@ -958,21 +1068,25 @@ export class GridView extends ItemView { } if (frontMatterInfo.exists) { - const colorValue = metadata?.color; + const colorValue = getFrontmatterValue(metadata, 'color'); if (colorValue) { // 檢查是否為 HEX 色值 - if (isHexColor(colorValue)) { + if (typeof colorValue === 'string' && isHexColor(colorValue)) { // 使用自訂 CSS 變數來設置 HEX 顏色 fileEl.addClass('ge-note-color-custom'); - fileEl.style.setProperty('--ge-note-color-bg', hexToRgba(colorValue, 0.2)); - fileEl.style.setProperty('--ge-note-color-border', hexToRgba(colorValue, 0.5)); + fileEl.setCssProps({ + '--ge-note-color-bg': hexToRgba(colorValue, 0.2), + '--ge-note-color-border': hexToRgba(colorValue, 0.5), + }); // 設置預覽內容文字顏色 if (pEl) { 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 類別來設置顏色 fileEl.addClass(`ge-note-color-${colorValue}`); @@ -983,7 +1097,7 @@ export class GridView extends ItemView { } } const titleField = this.plugin.settings.noteTitleField || 'title'; - const titleValue = metadata?.[titleField]; + const titleValue = formatFrontmatterValue(getFrontmatterValue(metadata, titleField)); if (titleValue) { // 將標題文字設為 frontmatter 的 title if (titleEl) { @@ -991,7 +1105,7 @@ export class GridView extends ItemView { } } - const displayValue = metadata?.display; + const displayValue = getFrontmatterValue(metadata, 'display'); if (displayValue === 'minimized') { // 移除已建立的預覽段落 if (pEl) { @@ -1002,15 +1116,15 @@ export class GridView extends ItemView { if (imageAreaEl) { imageAreaEl.remove(); } - fileEl.style.height = '100%'; + fileEl.addClass('ge-grid-item-full-height'); } else if (displayValue === 'hidden') { // 為隱藏的筆記添加特殊樣式類別 fileEl.addClass('ge-note-hidden'); } // 如果 frontmatter 同時存在 type 與非空的 redirect(視為捷徑檔),將圖示設為 shuffle - const redirectType = metadata?.type; - const redirectPath = metadata?.redirect; + const redirectType = getFrontmatterValue(metadata, 'type'); + const redirectPath = getFrontmatterValue(metadata, 'redirect'); if (redirectType && typeof redirectPath === 'string' && redirectPath.trim() !== '') { const iconContainer = fileEl.querySelector('.ge-icon-container'); if (iconContainer) { @@ -1026,13 +1140,13 @@ export class GridView extends ItemView { contentArea.createEl('p', { text: file.extension.toUpperCase() }); } - setTooltip(fileEl as HTMLElement, `${file.name}`, { delay: 2000 }) + setTooltip(fileEl, `${file.name}`, { delay: 2000 }) } // 顯示標籤(僅限 Markdown 檔案) if (file.extension === 'md' && this.showNoteTags && !this.minMode) { const fileCache = this.app.metadataCache.getFileCache(file); - const displaySetting = fileCache?.frontmatter?.display; + const displaySetting = getFrontmatterValue(fileCache?.frontmatter, 'display'); // 如果筆記是最小化就直接跳過標籤邏輯 if (displaySetting !== 'minimized') { @@ -1040,7 +1154,7 @@ export class GridView extends ItemView { const allTags = new Set(); // 從 frontmatter 獲取標籤 - let frontmatterTags = fileCache?.frontmatter?.tags || []; + const frontmatterTags = getFrontmatterValue(fileCache?.frontmatter, 'tags'); // 處理不同的標籤格式 if (typeof frontmatterTags === 'string') { @@ -1217,9 +1331,6 @@ export class GridView extends ItemView { // 建立哨兵元素,用於觸發後續載入 const sentinel = container.createDiv('ge-load-more-sentinel'); - sentinel.style.height = '1px'; - sentinel.style.width = '100%'; - sentinel.style.flexShrink = '0'; let isLoading = false; @@ -1327,7 +1438,7 @@ export class GridView extends ItemView { // 針對 iOS 設備進行特殊處理 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) { - const dateStr = metadata.frontmatter[fieldName]; + const dateStr = getFrontmatterValue(metadata.frontmatter, fieldName); if (dateStr) { + if (typeof dateStr !== 'string' && typeof dateStr !== 'number' && !(dateStr instanceof Date)) continue; const date = new Date(dateStr); if (!isNaN(date.getTime())) { frontMatterDate = date; @@ -1409,7 +1521,7 @@ export class GridView extends ItemView { // 針對 iOS 設備進行特殊處理 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) { lineNumber = content.substring(0, idx).split('\n').length - 1; await leaf.openFile(file, { eState: { line: lineNumber } }); - (this.app as any)?.commands?.executeCommandById?.('editor:focus'); + (this.app as AppWithCommands).commands?.executeCommandById?.('editor:focus'); return; } // 若都找不到關鍵字,直接開檔 @@ -1866,7 +1978,7 @@ export class GridView extends ItemView { // 刪除選項 menu.addItem((item) => { - (item as any).setWarning(true); + (item as MenuItemWithWarning).setWarning(true); item .setTitle(t('delete_note')) .setIcon("trash") @@ -1896,7 +2008,7 @@ export class GridView extends ItemView { const explorerLeaves = this.app.workspace.getLeavesOfType(EXPLORER_VIEW_TYPE); if (explorerLeaves.length === 0) { - new Notice('找不到 Explorer 視圖'); + new Notice('找不到 explorer 視圖'); return; } @@ -1904,7 +2016,7 @@ export class GridView extends ItemView { const explorerView = explorerLeaves[0].view as ExplorerView; if (!explorerView) { - new Notice('無法訪問 Explorer 視圖'); + new Notice('無法訪問 explorer 視圖'); return; } @@ -1912,10 +2024,10 @@ export class GridView extends ItemView { const filePaths = files.map(file => file.path); // 調用 ExplorerView 的 addToStash 方法 - (explorerView as any).addToStash(filePaths); + (explorerView as unknown as ExplorerViewActions).addToStash(filePaths); // 強制立即重新渲染 ExplorerView 以確保畫面更新 - (explorerView as any).refresh(); + (explorerView as unknown as ExplorerViewActions).refresh(); // 顯示成功通知 new Notice(t('added_to_stash')); @@ -2048,7 +2160,7 @@ export class GridView extends ItemView { ? Promise.resolve(mediaFiles.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); if (currentIndex === -1) return; @@ -2083,16 +2195,17 @@ export class GridView extends ItemView { } // 沒有找到已開啟的分頁,開新分頁 return this.app.workspace.getLeaf('tab'); - case 'split': + case 'split': { // 檢查是否已經有 split 視圖存在 - const mainEntry = this.app.workspace.rootSplit; - if (mainEntry && (mainEntry as any)?.children?.length > 1) { + const mainEntry = this.app.workspace.rootSplit as WorkspaceSplitWithChildren | undefined; + if ((mainEntry?.children?.length ?? 0) > 1) { // 如果已經有 split,直接在現有的 split 中開啟 return this.app.workspace.getLeaf(false); } else { // 如果沒有 split,創建新的 split return this.app.workspace.getLeaf('split'); } + } case 'newWindow': return this.app.workspace.getLeaf('window'); case 'default': @@ -2104,9 +2217,14 @@ export class GridView extends ItemView { // 開啟捷徑檔案 openShortcutFile(file: TFile): boolean { const fileCache = this.app.metadataCache.getFileCache(file); - if (!fileCache?.frontmatter) return false; - const redirectType = fileCache?.frontmatter?.type; - const redirectPath = fileCache?.frontmatter?.redirect; + const frontmatter: ShortcutFrontmatter | undefined = fileCache?.frontmatter; + if (!frontmatter) return false; + + 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() !== '') { let target; @@ -2142,7 +2260,6 @@ export class GridView extends ItemView { void this.setSource('folder', redirectPath); this.clearSelection(); window.requestAnimationFrame(() => { - const cardLayout = fileCache?.frontmatter?.cardLayout; if (cardLayout && cardLayout !== this.cardLayout) { this.cardLayout = cardLayout; void this.render(); @@ -2157,7 +2274,6 @@ export class GridView extends ItemView { void this.setSource(redirectPath); this.clearSelection(); window.requestAnimationFrame(() => { - const cardLayout = fileCache?.frontmatter?.cardLayout; if (cardLayout && cardLayout !== this.cardLayout) { this.cardLayout = cardLayout; void this.render(); @@ -2165,9 +2281,9 @@ export class GridView extends ItemView { }); return true; } else if (redirectType === 'search') { - const searchCurrentLocationOnly = fileCache?.frontmatter?.searchCurrentLocationOnly || false; - const searchFilesNameOnly = fileCache?.frontmatter?.searchFilesNameOnly || false; - const searchMediaFiles = fileCache?.frontmatter?.searchMediaFiles || false; + const searchCurrentLocationOnly = frontmatter.searchCurrentLocationOnly === true; + const searchFilesNameOnly = frontmatter.searchFilesNameOnly === true; + const searchMediaFiles = frontmatter.searchMediaFiles === true; void this.setSource('', '', true, redirectPath, searchCurrentLocationOnly, searchFilesNameOnly, searchMediaFiles); this.clearSelection(); return true; @@ -2241,38 +2357,39 @@ export class GridView extends ItemView { const isInSidebar = this.leaf.getRoot() === this.app.workspace.leftSplit || this.leaf.getRoot() === this.app.workspace.rightSplit; if (isInSidebar) { - scrollContainer.style.fontSize = '1em'; - scrollContainer.style.backgroundColor = 'var(--background-secondary)'; + scrollContainer.addClass('ge-note-sidebar-scroll-container'); } // 創建筆記內容容器 const noteContent = scrollContainer.createDiv('ge-note-content-container'); if (isInSidebar) { - noteContent.style.padding = '15px'; + noteContent.addClass('ge-note-sidebar-content-container'); } // 在移動端添加滾動監聽,根據滾動方向控制導航欄顯示/隱藏 if (Platform.isPhone) { let lastScrollTop = 0; const handleScroll = () => { - const mobileNavbar = document.querySelector('.mobile-navbar') as HTMLElement; + const mobileNavbar = activeDocument.querySelector('.mobile-navbar') as HTMLElement; if (!mobileNavbar) return; const currentScrollTop = scrollContainer.scrollTop; // 往上捲(滾動位置增加)時隱藏導航欄 if (currentScrollTop > lastScrollTop && currentScrollTop > 50) { - if (!document.body.classList.contains('is-floating-nav')) { - mobileNavbar.style.transform = 'translateY(100%)'; + if (!activeDocument.body.classList.contains('is-floating-nav')) { + mobileNavbar.setCssProps({ transform: 'translateY(100%)' }); } 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) { - mobileNavbar.style.transform = 'translateY(0)'; - mobileNavbar.style.transition = 'transform 0.3s ease-in'; + mobileNavbar.setCssProps({ + transform: 'translateY(0)', + transition: 'transform 0.3s ease-in', + }); } lastScrollTop = currentScrollTop; @@ -2285,10 +2402,12 @@ export class GridView extends ItemView { const activeView = this.app.workspace.getActiveViewOfType(GridView); // 如果當前活動視圖不是這個 GridView 實例,或者不在顯示筆記狀態 if (activeView !== this || !this.isShowingNote) { - const navbar = document.querySelector('.mobile-navbar') as HTMLElement; + const navbar = activeDocument.querySelector('.mobile-navbar') as HTMLElement; if (navbar) { - navbar.style.transform = 'translateY(0)'; - navbar.style.transition = 'transform 0.3s ease-in'; + navbar.setCssProps({ + transform: 'translateY(0)', + transition: 'transform 0.3s ease-in', + }); } } }; @@ -2324,7 +2443,7 @@ export class GridView extends ItemView { for (const key of keys) { const item = metadataContent.createDiv('ge-note-metadata-item'); 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 values = Array.isArray(value) ? value : [value]; @@ -2393,7 +2512,7 @@ export class GridView extends ItemView { }); tagEl.addEventListener('click', (e) => { 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 { tagRegex.lastIndex = 0; @@ -2411,7 +2530,7 @@ export class GridView extends ItemView { }); tagEl.addEventListener('click', (e) => { 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; } @@ -2538,7 +2657,7 @@ export class GridView extends ItemView { // 當同時符合(例如內容很短)且往上拉時,視為上拉 isPullingUp = canPullUp && deltaY < 0; if (this.noteViewContainer) { - this.noteViewContainer.style.transition = 'none'; + this.noteViewContainer.setCssProps({ transition: 'none' }); } } else if ((isAtTop && !isAtBottom && deltaY < 0) || (isAtBottom && !isAtTop && deltaY > 0)) { // 往反方向正常滑動閱讀時,取消攔截 @@ -2555,7 +2674,7 @@ export class GridView extends ItemView { } const resistance = 0.5; 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)) { // 關閉筆記 const targetY = isPullingUp ? '-100vh' : '100vh'; - this.noteViewContainer.style.transition = 'transform 0.2s ease-out'; - this.noteViewContainer.style.transform = `translateY(${targetY})`; + this.noteViewContainer.setCssProps({ + transition: 'transform 0.2s ease-out', + transform: `translateY(${targetY})`, + }); window.setTimeout(() => { this.hideNoteInGrid(); if (this.noteViewContainer) { - this.noteViewContainer.style.transform = ''; - this.noteViewContainer.style.transition = ''; + this.noteViewContainer.setCssProps({ + transform: '', + transition: '', + }); } }, 200); } else { // 距離不夠,彈回原位 - this.noteViewContainer.style.transition = 'transform 0.3s ease-out'; - this.noteViewContainer.style.transform = `translateY(0)`; + this.noteViewContainer.setCssProps({ + transition: 'transform 0.3s ease-out', + transform: 'translateY(0)', + }); window.setTimeout(() => { if (this.noteViewContainer) { - this.noteViewContainer.style.transform = ''; - this.noteViewContainer.style.transition = ''; + this.noteViewContainer.setCssProps({ + transform: '', + transition: '', + }); } }, 300); } @@ -2614,7 +2741,7 @@ export class GridView extends ItemView { 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) { const mobileNavbar = activeDocument.querySelector('.mobile-navbar') as HTMLElement; if (mobileNavbar) { - mobileNavbar.style.transform = 'translateY(0)'; - mobileNavbar.style.transition = 'transform 0.3s ease-in'; + mobileNavbar.setCssProps({ + transform: 'translateY(0)', + transition: 'transform 0.3s ease-in', + }); } } if (this.noteViewContainer) { // 移除鍵盤事件監聽器 - const keydownHandler = (this.noteViewContainer as any).keydownHandler; + const keydownHandler = (this.noteViewContainer as NoteViewContainerWithKeydownHandler).keydownHandler; if (keydownHandler) { activeDocument.removeEventListener('keydown', keydownHandler); } @@ -2675,7 +2804,7 @@ export class GridView extends ItemView { } // 讀取視圖狀態 - async setState(state: any): Promise { + async setState(state: GridViewState): Promise { if (state?.state) { this.sourceMode = state.state.sourceMode || 'folder'; this.sourcePath = state.state.sourcePath || '/'; diff --git a/src/main.ts b/src/main.ts index 02a18a2..c8c27f7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 { ExplorerView, EXPLORER_VIEW_TYPE } from './ExplorerView'; import { updateCustomDocumentExtensions, isMediaFile } from './utils/fileUtils'; @@ -7,6 +7,37 @@ import { showNoteSettingsModal } from './modal/noteSettingsModal'; import { GallerySettings, DEFAULT_SETTINGS, GridExplorerSettingTab } from './settings'; 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; +} + +interface CanvasDropView { + containerEl?: HTMLElement; + canvas?: CanvasDropApi; + gridExplorerDropHandler?: boolean; +} + export default class GridExplorerPlugin extends Plugin { settings: GallerySettings = DEFAULT_SETTINGS; statusBarItem: HTMLElement | null = null; @@ -185,7 +216,7 @@ export default class GridExplorerPlugin extends Plugin { item.setTitle(t('open_in_grid_view')); item.setIcon('grid'); item.setSection?.("open"); - const ogSubmenu: Menu = (item as any).setSubmenu(); + const ogSubmenu = (item as MenuItemWithSubmenu).setSubmenu(); ogSubmenu.addItem((item) => { item .setTitle(t('show_note_in_grid_view')) @@ -306,8 +337,14 @@ export default class GridExplorerPlugin extends Plugin { ); // 註冊tag-wrangler右鍵選單 + const onWorkspaceEvent = this.app.workspace.on.bind(this.app.workspace) as WorkspaceEventHandler; 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 個字元 const truncatedText = tagName.length > 15 ? tagName.substring(0, 15) + '...' : tagName; 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' + 'span.cm-hashtag, /* 編輯器中的 tag */\n' + '.metadata-property[data-property-key="tags"] .multi-select-pill /* 屬性面板中的 tag */' - ) as HTMLElement | null; + ); if (!el) return; // 不是 tag 點擊就跳過 // 取出 tag 名稱(去掉前導 #) @@ -352,23 +389,23 @@ export default class GridExplorerPlugin extends Plugin { if (el.matches('span.cm-hashtag')) { // 編輯器模式:可能被拆成多個 span,需組合 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')) { 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; } // 向右收集 - curr = el.nextElementSibling as HTMLElement | null; + curr = el.nextElementSibling; while (curr && curr.matches('span.cm-hashtag') && !curr.classList.contains('cm-formatting')) { collect.push(curr.textContent ?? ''); - curr = curr.nextElementSibling as HTMLElement | null; + curr = curr.nextElementSibling; } tagName = collect.join('').trim(); } else if (el.classList.contains('tag-pane-tag')) { // 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() ?? ''; } else if (el.matches('.metadata-property[data-property-key="tags"] .multi-select-pill')) { // Frontmatter/Properties view @@ -414,12 +451,12 @@ export default class GridExplorerPlugin extends Plugin { // 使用索引對映實際路徑,避免依賴顯示文字(可能被其他外掛修改) const parentEl = breadcrumbEl.parentElement; // 僅取麵包屑元素,建立清單以取得被點擊元素的索引 - const crumbs = Array.from(parentEl.querySelectorAll('.view-header-breadcrumb')) as HTMLElement[]; - const clickedIndex = crumbs.indexOf(breadcrumbEl as HTMLElement); + const crumbs = Array.from(parentEl.querySelectorAll('.view-header-breadcrumb')); + const clickedIndex = crumbs.indexOf(breadcrumbEl); if (clickedIndex < 0) return; // 嘗試取得目前活躍檔案 - let activeFile = this.app.workspace.getActiveFile(); + const activeFile = this.app.workspace.getActiveFile(); if (!activeFile) { // 若無活躍檔案,無法可靠對映麵包屑,結束處理 return; @@ -427,7 +464,7 @@ export default class GridExplorerPlugin extends Plugin { // 蒐集從根到當前檔案之父層的所有資料夾 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) { folders.push(currFolder); currFolder = currFolder.parent; @@ -436,7 +473,7 @@ export default class GridExplorerPlugin extends Plugin { // 以「去除根目錄('/')」後的資料夾清單,直接用索引對應麵包屑 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; if (isClickingFileCrumb) { @@ -535,7 +572,7 @@ export default class GridExplorerPlugin extends Plugin { private setupCanvasDropHandlers() { const setup = () => { this.app.workspace.getLeavesOfType('canvas').forEach(leaf => { - const canvasView = leaf.view as any; + const canvasView = leaf.view as CanvasDropView; if (canvasView.gridExplorerDropHandler) { return; } @@ -566,9 +603,9 @@ export default class GridExplorerPlugin extends Plugin { const data = evt.dataTransfer.getData('application/obsidian-grid-explorer-files'); let filePaths: string[] = []; try { - const parsed = JSON.parse(data); + const parsed: unknown = JSON.parse(data); 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') { filePaths = [parsed]; } @@ -587,7 +624,7 @@ export default class GridExplorerPlugin extends Plugin { } const pos = canvas.posFromEvt(evt); - let currentPos = { ...pos } as { x: number; y: number }; + let currentPos = { ...pos }; // 逐一建立節點,若多檔則位置做些微偏移 for (let i = 0; i < filePaths.length; i++) { @@ -748,7 +785,8 @@ export default class GridExplorerPlugin extends Plugin { } async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + const loadedSettings = await this.loadData() as Partial | null; + this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedSettings ?? {}); updateCustomDocumentExtensions(this.settings); } @@ -769,9 +807,9 @@ export default class GridExplorerPlugin extends Plugin { const explorerLeaves = this.app.workspace.getLeavesOfType(EXPLORER_VIEW_TYPE); explorerLeaves.forEach(leaf => { if (leaf.view instanceof ExplorerView) { - void (leaf.view as ExplorerView).refresh(); + void (leaf.view).refresh(); } }); } } -} \ No newline at end of file +} diff --git a/src/modal/customModeModal.ts b/src/modal/customModeModal.ts index 32c594f..640d930 100644 --- a/src/modal/customModeModal.ts +++ b/src/modal/customModeModal.ts @@ -1,6 +1,6 @@ -import { App, Modal, Setting } from 'obsidian'; +import { App, Modal, Setting, setIcon } from 'obsidian'; import GridExplorerPlugin from '../main'; -import { CustomMode } from '../settings'; +import { CustomMode, CustomModeOption } from '../settings'; import { t } from '../translations'; export class CustomModeModal extends Modal { @@ -24,9 +24,9 @@ export class CustomModeModal extends Modal { let icon = this.mode ? this.mode.icon : '🧩'; let displayName = this.mode ? this.mode.displayName : ''; // 新增名稱欄位,用於原始 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 程式碼 - 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 let dataviewCode = this.mode ? this.mode.dataviewCode : ''; let enabled = this.mode ? (this.mode.enabled ?? true) : true; @@ -41,8 +41,7 @@ export class CustomModeModal extends Modal { icon = value || '🧩'; }); // 設置固定寬度,適合單個圖示 - text.inputEl.style.width = '3em'; - text.inputEl.style.minWidth = '3em'; + text.inputEl.addClass('ge-custommode-icon-input'); }) .addText(text => { text.setValue(displayName) @@ -56,9 +55,7 @@ export class CustomModeModal extends Modal { .setDesc(t('custom_mode_dataview_code_desc')); // 使標題與描述佔據一行,輸入區域佔據了下一行 - dvSetting.settingEl.style.flexDirection = 'column'; - dvSetting.settingEl.style.alignItems = 'stretch'; - dvSetting.settingEl.style.gap = '0.5rem'; + dvSetting.settingEl.addClass('ge-custommode-dataview-setting'); dvSetting.addText(text => { text.setValue(name) @@ -71,10 +68,10 @@ export class CustomModeModal extends Modal { .onChange(value => { dataviewCode = value; }) - .setPlaceholder('Dataview JS code'); + .setPlaceholder('Dataview js code'); // 給TextArea有足夠的垂直空間和完整的寬度 text.inputEl.setAttr('rows', 6); - text.inputEl.style.width = '100%'; + text.inputEl.addClass('ge-custommode-code-input'); }); dvSetting.addText(text => { @@ -86,10 +83,7 @@ export class CustomModeModal extends Modal { }); // 讓 Text 與 TextArea 在 control 區域各佔一行 - dvSetting.controlEl.style.display = 'flex'; - dvSetting.controlEl.style.flexDirection = 'column'; - dvSetting.controlEl.style.alignItems = 'stretch'; - dvSetting.controlEl.style.gap = '0.5rem'; + dvSetting.controlEl.addClass('ge-custommode-dataview-controls'); // ===== 選項管理區域 ===== contentEl.createEl('h3', { text: t('custom_mode_sub_options') }); @@ -126,11 +120,11 @@ export class CustomModeModal extends Modal { }; const getDropIndex = (e: DragEvent): number => { - const containers = Array.from(optionsContainer.querySelectorAll('.ge-custommode-option-container')); + const containers = Array.from(optionsContainer.querySelectorAll('.ge-custommode-option-container')); const y = e.clientY; for (let i = 0; i < containers.length; i++) { - const container = containers[i] as HTMLElement; + const container = containers[i]; const rect = container.getBoundingClientRect(); 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'); - dragHandle.innerHTML = ``; + setIcon(dragHandle, 'grip-vertical'); // 選項名稱 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'); - toggleEl.innerHTML = ``; + setIcon(toggleEl, 'chevron-right'); // 刪除按鈕 const deleteEl = headerEl.createDiv('ge-custommode-option-delete'); - deleteEl.innerHTML = ``; + setIcon(deleteEl, 'trash-2'); // 內容區域 const contentEl = optionContainer.createDiv('ge-custommode-option-content'); @@ -201,13 +195,13 @@ export class CustomModeModal extends Modal { }); }) .addTextArea(text => { - text.setPlaceholder('Dataview JS code') + text.setPlaceholder('Dataview js code') .setValue(opt.dataviewCode) .onChange(val => { opt.dataviewCode = val; }); text.inputEl.setAttr('rows', 6); - text.inputEl.style.width = '100%'; + text.inputEl.addClass('ge-custommode-code-input'); }) .addText(text => { text.setPlaceholder(t('custom_mode_fields_placeholder')) @@ -251,7 +245,7 @@ export class CustomModeModal extends Modal { optionContainer.classList.add('dragging'); if (e.dataTransfer) { 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 draggedOption = options[draggedIndex]; + if (!draggedOption) { + return; + } // 從原位置移除 options.splice(draggedIndex, 1); diff --git a/src/modal/folderNoteSettingsModal.ts b/src/modal/folderNoteSettingsModal.ts index a000e4e..f4c0d71 100644 --- a/src/modal/folderNoteSettingsModal.ts +++ b/src/modal/folderNoteSettingsModal.ts @@ -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 { GridView } from '../GridView'; import { t } from '../translations'; @@ -11,6 +11,25 @@ export interface FolderNoteSettings { 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) { new FolderNoteSettingsModal(app, plugin, folder, gridView).open(); } @@ -106,13 +125,13 @@ export class FolderNoteSettingsModal extends Modal { }); // HEX 自訂顏色輸入欄位 - let hexInput: any; - const hexSetting = new Setting(contentEl) + let hexInput: TextComponent | null = null; + new Setting(contentEl) .setName(t('custom_hex_color')) .setDesc(t('custom_hex_color_desc')) .addText(text => { hexInput = text; - text.setPlaceholder('#FF8800 or #F80') + text.setPlaceholder('#Ff8800 or #f80') .setValue(isCurrentColorHex ? this.settings.color : '') .onChange(value => { // 驗證 HEX 格式 @@ -196,36 +215,24 @@ export class FolderNoteSettingsModal extends Modal { try { // 使用 metadataCache 讀取筆記的 frontmatter const fileCache = this.app.metadataCache.getFileCache(this.existingFile); - if (fileCache && fileCache.frontmatter) { - // 讀取排序設定 - if ('sort' in fileCache.frontmatter) { - this.settings.sort = fileCache.frontmatter.sort || ''; - } - - // 讀取顏色設定 - if ('color' in fileCache.frontmatter) { - this.settings.color = fileCache.frontmatter.color || ''; - } - - // 讀取圖示設定 - if ('icon' in fileCache.frontmatter) { - this.settings.icon = fileCache.frontmatter.icon || '📁'; - } - - // 讀取卡片樣式設定 - 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(); + if (fileCache?.frontmatter) { + const frontmatter = fileCache.frontmatter as FolderNoteFrontmatter; + + this.settings.sort = getStringValue(frontmatter.sort); + this.settings.color = getStringValue(frontmatter.color); + this.settings.icon = getStringValue(frontmatter.icon, '📁'); + + const cardLayout = getStringValue(frontmatter.cardLayout); + this.settings.cardLayout = + cardLayout === 'horizontal' || cardLayout === 'vertical' ? cardLayout : ''; + + if (Array.isArray(frontmatter.pinned)) { + this.settings.isPinned = frontmatter.pinned.some((item: unknown) => { + const pinnedName = getPinnedName(item); const pinnedNameWithoutExt = pinnedName.replace(/\.\w+$/, ''); return pinnedNameWithoutExt === this.folder.name; }); - } + } } } catch (error) { console.error('無法讀取資料夾筆記設定', error); @@ -252,51 +259,53 @@ export class FolderNoteSettingsModal extends Modal { } // 使用 fileManager.processFrontMatter 更新 frontmatter - await this.app.fileManager.processFrontMatter(file, (frontmatter) => { + await this.app.fileManager.processFrontMatter(file, (frontmatter: FolderNoteFrontmatter) => { if (this.settings.sort) { - frontmatter['sort'] = this.settings.sort; + frontmatter.sort = this.settings.sort; } else { - delete frontmatter['sort']; + delete frontmatter.sort; } if (this.settings.color) { - frontmatter['color'] = this.settings.color; + frontmatter.color = this.settings.color; } else { - delete frontmatter['color']; + delete frontmatter.color; } if (this.settings.icon && this.settings.icon !== '📁') { - frontmatter['icon'] = this.settings.icon; + frontmatter.icon = this.settings.icon; } else { - delete frontmatter['icon']; + delete frontmatter.icon; } if (this.settings.cardLayout) { - frontmatter['cardLayout'] = this.settings.cardLayout; + frontmatter.cardLayout = this.settings.cardLayout; } else { - delete frontmatter['cardLayout']; + delete frontmatter.cardLayout; } const folderName = `${this.folder.name}.md`; if (this.settings.isPinned) { // 如果原本就有 pinned 陣列,則添加或更新 - if (Array.isArray(frontmatter['pinned'])) { - if (!frontmatter['pinned'].includes(folderName)) { - frontmatter['pinned'] = [folderName, ...frontmatter['pinned']]; + if (Array.isArray(frontmatter.pinned)) { + const pinned = frontmatter.pinned.map(getPinnedName).filter(Boolean); + if (!pinned.includes(folderName)) { + frontmatter.pinned = [folderName, ...pinned]; } } else { // 如果沒有 pinned 陣列,則創建一個新的 - frontmatter['pinned'] = [folderName]; + frontmatter.pinned = [folderName]; } - } else if (Array.isArray(frontmatter['pinned'])) { + } else if (Array.isArray(frontmatter.pinned)) { // 如果取消置頂,則從陣列中移除 - frontmatter['pinned'] = frontmatter['pinned'].filter( - (item: any) => item !== folderName - ); + const pinned = frontmatter.pinned + .map(getPinnedName) + .filter(item => item !== folderName); + frontmatter.pinned = pinned; // 如果陣列為空,則刪除該欄位 - if (frontmatter['pinned'].length === 0) { - delete frontmatter['pinned']; + if (pinned.length === 0) { + delete frontmatter.pinned; } } }); diff --git a/src/modal/folderSelectionModal.ts b/src/modal/folderSelectionModal.ts index 1fbc89a..b38bfbd 100644 --- a/src/modal/folderSelectionModal.ts +++ b/src/modal/folderSelectionModal.ts @@ -4,6 +4,22 @@ import { GridView } from '../GridView'; import { t } from '../translations'; import { isFolderIgnored } from '../utils/fileUtils'; +type AppWithInternalPlugins = App & { + internalPlugins?: { + plugins?: { + bookmarks?: { + enabled?: boolean; + }; + }; + }; +}; + +type SearchViewWithComponent = { + searchComponent?: { + inputEl?: HTMLInputElement; + }; +}; + // 顯示資料夾選擇 modal export function showFolderSelectionModal(app: App, plugin: GridExplorerPlugin, activeView?: GridView, buttonElement?: HTMLElement) { new FolderSelectionModal(app, plugin, activeView, buttonElement).open(); @@ -94,7 +110,7 @@ export class FolderSelectionModal extends Modal { // 建立書籤選項 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) { const bookmarkOption = this.folderOptionsContainer.createEl('div', { cls: 'ge-grid-view-folder-option', @@ -120,10 +136,10 @@ export class FolderSelectionModal extends Modal { // 建立搜尋結果選項 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) { - const searchView = searchLeaf.view; - const searchInputEl = searchView.searchComponent ? searchView.searchComponent.inputEl : null; + const searchView = searchLeaf.view as SearchViewWithComponent; + const searchInputEl = searchView.searchComponent?.inputEl ?? null; if (searchInputEl) { if (searchInputEl.value.trim().length > 0) { const searchOption = this.folderOptionsContainer.createEl('div', { @@ -446,7 +462,7 @@ export class FolderSelectionModal extends Modal { event.preventDefault(); if (this.selectedIndex >= 0) { const selectedOption = this.folderOptions[this.selectedIndex]; - if (selectedOption && selectedOption.style.display !== 'none') { + if (selectedOption && !selectedOption.hasClass('ge-hidden')) { window.requestAnimationFrame(() => { selectedOption.click(); }); @@ -508,7 +524,7 @@ export class FolderSelectionModal extends Modal { // 獲取當前可見的選項 getVisibleOptions(): HTMLElement[] { 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'); if (dataPath) { // 這是一個資料夾選項 - const nameSpan = option.querySelector('span:last-child') as HTMLSpanElement | null; + const nameSpan = option.querySelector('span:last-child'); if (nameSpan) { if (searchTerm !== '') { // 顯示完整路徑 @@ -538,7 +554,7 @@ export class FolderSelectionModal extends Modal { } // 根據搜尋狀態調整前綴縮排(ascii tree) - const prefixSpan = option.querySelector('.ge-folder-tree-prefix') as HTMLSpanElement | null; + const prefixSpan = option.querySelector('.ge-folder-tree-prefix'); if (prefixSpan) { if (searchTerm !== '') { // 搜尋時移除縮排 @@ -555,10 +571,10 @@ export class FolderSelectionModal extends Modal { const text = option.textContent?.toLowerCase() || ''; const fullPath = option.getAttribute('data-path')?.toLowerCase() || ''; if (searchTerm === '' || text.includes(searchTerm) || fullPath.includes(searchTerm)) { - option.style.display = 'block'; + option.removeClass('ge-hidden'); hasVisibleOptions = true; } else { - option.style.display = 'none'; + option.addClass('ge-hidden'); } }); diff --git a/src/modal/inputModal.ts b/src/modal/inputModal.ts index 19f36f3..376c21f 100644 --- a/src/modal/inputModal.ts +++ b/src/modal/inputModal.ts @@ -59,7 +59,7 @@ export class InputModal extends Modal { attr: { id: 'searchCurrentLocationOnly' } - }) as HTMLInputElement; + }); if (searchOptions.searchCurrentLocationOnly) { searchScopeCheckbox.checked = true; } @@ -73,7 +73,7 @@ export class InputModal extends Modal { attr: { id: 'searchFilesNameOnly' } - }) as HTMLInputElement; + }); if (searchOptions.searchFilesNameOnly) { searchNameCheckbox.checked = true; } @@ -87,7 +87,7 @@ export class InputModal extends Modal { attr: { id: 'searchMediaFiles' } - }) as HTMLInputElement; + }); if (searchOptions.searchMediaFiles) { searchMediaFilesCheckbox.checked = true; } @@ -178,4 +178,4 @@ export function showUriInputModal(app: App, onSubmit: (uri: string) => void) { inputType: 'url', onSubmit }).open(); -} \ No newline at end of file +} diff --git a/src/modal/mediaModal.ts b/src/modal/mediaModal.ts index 702a733..e68dd79 100644 --- a/src/modal/mediaModal.ts +++ b/src/modal/mediaModal.ts @@ -1,6 +1,12 @@ import { App, Modal, TFile, Menu, setIcon } from 'obsidian'; import { isImageFile, isVideoFile, isAudioFile } from '../utils/fileUtils'; +interface GridViewFocusTarget { + gridItems: HTMLElement[]; + hasKeyboardFocus: boolean; + selectItem(index: number): void; +} + export class MediaModal extends Modal { private file: TFile; private mediaFiles: TFile[]; @@ -8,7 +14,7 @@ export class MediaModal extends Modal { private currentMediaElement: HTMLElement | null = null; private isZoomed = false; private handleWheel: EventListener | null = null; - private gridView: any; // 儲存 GridView 實例的引用 + private gridView?: GridViewFocusTarget; // 儲存 GridView 實例的引用 // 觸控拖曳相關屬性 private touchStartX = 0; @@ -18,7 +24,7 @@ export class MediaModal extends Modal { private minSwipeDistance = 50; // 最小滑動距離 private maxSwipeTime = 300; // 最大滑動時間(毫秒) - constructor(app: App, file: TFile, mediaFiles: TFile[], gridView?: any) { + constructor(app: App, file: TFile, mediaFiles: TFile[], gridView?: GridViewFocusTarget) { super(app); this.file = file; this.mediaFiles = mediaFiles; @@ -34,8 +40,6 @@ export class MediaModal extends Modal { // 設置 modal 樣式為全螢幕 contentEl.empty(); - contentEl.style.width = '100%'; - contentEl.style.height = '100%'; contentEl.addClass('ge-media-modal-content'); // 創建媒體顯示區域 @@ -120,7 +124,7 @@ export class MediaModal extends Modal { // 如果存在之前的滾輪事件處理程序,先移除它 if (this.handleWheel) { - const mediaView = contentEl.querySelector('.ge-media-view') as HTMLElement | null; + const mediaView = contentEl.querySelector('.ge-media-view'); if (mediaView) { mediaView.removeEventListener('wheel', this.handleWheel); } @@ -131,6 +135,7 @@ export class MediaModal extends Modal { if (this.gridView) { // 找到當前媒體檔案在 GridView 中的索引 const currentFile = this.mediaFiles[this.currentIndex]; + if (!currentFile) return; const gridItemIndex = this.gridView.gridItems.findIndex((item: HTMLElement) => item.dataset.filePath === currentFile.path ); @@ -170,9 +175,9 @@ export class MediaModal extends Modal { if (isImageFile(mediaFile)) { // 創建圖片元素 - const img = document.createElement('img'); + const img = activeDocument.createElement('img'); img.className = 'ge-fullscreen-image'; - img.style.display = 'none'; // 先隱藏新圖片 + img.addClass('ge-hidden'); // 先隱藏新圖片 img.src = this.app.vault.getResourcePath(mediaFile); // 等待新圖片載入完成 @@ -185,7 +190,7 @@ export class MediaModal extends Modal { // 設置圖片樣式,預設滿屏顯示 this.resetImageStyles(img); // 顯示新圖片 - img.style.display = ''; + img.removeClass('ge-hidden'); }; mediaContainer.appendChild(img); @@ -201,7 +206,7 @@ export class MediaModal extends Modal { if (this.currentMediaElement) { this.currentMediaElement.remove(); } - const video = document.createElement('video'); + const video = activeDocument.createElement('video'); video.className = 'ge-fullscreen-video'; video.controls = true; video.autoplay = true; @@ -219,7 +224,7 @@ export class MediaModal extends Modal { if (isAudioFile(mediaFile)) { //顯示檔案名稱 const fileName = mediaFile.name; - const fileNameElement = document.createElement('div'); + const fileNameElement = activeDocument.createElement('div'); fileNameElement.className = 'ge-fullscreen-file-name'; fileNameElement.textContent = fileName; mediaContainer.appendChild(fileNameElement); @@ -240,31 +245,35 @@ export class MediaModal extends Modal { // 重設圖片樣式 resetImageStyles(img: HTMLImageElement) { - const mediaView = this.contentEl.querySelector('.ge-media-view') as HTMLElement | null; + const mediaView = this.contentEl.querySelector('.ge-media-view'); if (!mediaView) return; - img.style.width = 'auto'; - img.style.height = 'auto'; - img.style.maxWidth = '100vw'; - img.style.maxHeight = '100vh'; - img.style.position = 'absolute'; - img.style.left = '50%'; - img.style.top = '50%'; - img.style.transform = 'translate(-50%, -50%)'; - img.style.cursor = 'zoom-in'; + img.setCssStyles({ + width: 'auto', + height: 'auto', + maxWidth: '100vw', + maxHeight: '100vh', + position: 'absolute', + left: '50%', + top: '50%', + transform: 'translate(-50%, -50%)', + cursor: 'zoom-in', + }); - (mediaView as HTMLElement).style.overflowX = 'hidden'; - (mediaView as HTMLElement).style.overflowY = 'hidden'; + mediaView.setCssStyles({ + overflowX: 'hidden', + overflowY: 'hidden', + }); // 等待圖片載入完成後調整大小 img.onload = () => { if (mediaView.clientWidth > mediaView.clientHeight) { if (img.naturalHeight < mediaView.clientHeight) { - img.style.height = '100%'; + img.setCssStyles({ height: '100%' }); } } else { 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 (mediaView.clientWidth > mediaView.clientHeight) { if (img.naturalHeight < mediaView.clientHeight) { - img.style.height = '100%'; + img.setCssStyles({ height: '100%' }); } } else { 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) { - const mediaView = this.contentEl.querySelector('.ge-media-view'); + const mediaView = this.contentEl.querySelector('.ge-media-view'); if (!mediaView) return; if (!this.isZoomed) { // 放大 @@ -306,25 +315,33 @@ export class MediaModal extends Modal { // 如果圖片比視窗更"細長" (Aspect Ratio 較小),則寬度填滿 (Fit Width),垂直捲動 // 如果圖片比視窗更"扁平" (Aspect Ratio 較大),則高度填滿 (Fit Height),水平捲動 if (imageAspect < screenAspect) { - img.style.width = '100vw'; - img.style.height = 'auto'; - (mediaView as HTMLElement).style.overflowX = 'hidden'; - (mediaView as HTMLElement).style.overflowY = 'scroll'; + img.setCssStyles({ + width: '100vw', + height: 'auto', + }); + mediaView.setCssStyles({ + overflowX: 'hidden', + overflowY: 'scroll', + }); // 計算滾動位置 - requestAnimationFrame(() => { + window.requestAnimationFrame(() => { const newHeight = img.offsetHeight; const scrollY = Math.max(0, (newHeight * clickY) - (mediaView.clientHeight / 2)); mediaView.scrollTop = scrollY; }); } else { - img.style.width = 'auto'; - img.style.height = '100vh'; - (mediaView as HTMLElement).style.overflowX = 'scroll'; - (mediaView as HTMLElement).style.overflowY = 'hidden'; + img.setCssStyles({ + width: 'auto', + height: '100vh', + }); + mediaView.setCssStyles({ + overflowX: 'scroll', + overflowY: 'hidden', + }); // 計算滾動位置 - requestAnimationFrame(() => { + window.requestAnimationFrame(() => { const newWidth = img.offsetWidth; const scrollX = Math.max(0, (newWidth * clickX) - (mediaView.clientWidth / 2)); mediaView.scrollLeft = scrollX; @@ -339,14 +356,16 @@ export class MediaModal extends Modal { mediaView.addEventListener('wheel', this.handleWheel); } - img.style.maxWidth = 'none'; - img.style.maxHeight = 'none'; - img.style.position = 'relative'; - img.style.left = '0'; - img.style.top = '0'; - img.style.margin = 'auto'; - img.style.transform = 'none'; - img.style.cursor = 'zoom-out'; + img.setCssStyles({ + maxWidth: 'none', + maxHeight: 'none', + position: 'relative', + left: '0', + top: '0', + margin: 'auto', + transform: 'none', + cursor: 'zoom-out', + }); this.isZoomed = true; this.contentEl.addClass('is-zoomed'); } else { // 縮小 diff --git a/src/modal/noteSettingsModal.ts b/src/modal/noteSettingsModal.ts index 57196c9..3eac63c 100644 --- a/src/modal/noteSettingsModal.ts +++ b/src/modal/noteSettingsModal.ts @@ -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 { GridView } from '../GridView'; import { t } from '../translations'; @@ -12,6 +12,26 @@ export interface NoteSettings { 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[]) { new NoteSettingsModal(app, plugin, file).open(); } @@ -105,13 +125,13 @@ export class NoteSettingsModal extends Modal { }); // HEX 自訂顏色輸入欄位 - let hexInput: any; - const hexSetting = new Setting(contentEl) + let hexInput: TextComponent | null = null; + new Setting(contentEl) .setName(t('custom_hex_color')) .setDesc(t('custom_hex_color_desc')) .addText(text => { hexInput = text; - text.setPlaceholder('#FF8800 or #F80') + text.setPlaceholder('#Ff8800 or #f80') .setValue(isCurrentColorHex ? this.settings.color : '') .onChange(value => { // 驗證 HEX 格式 @@ -148,8 +168,8 @@ export class NoteSettingsModal extends Modal { } // 最小化顯示切換 - let minimizedToggle: any; - let hiddenToggle: any; + let minimizedToggle: ToggleComponent | null = null; + let hiddenToggle: ToggleComponent | null = null; if (this.files[0].extension === "md") { new Setting(contentEl) @@ -233,7 +253,7 @@ export class NoteSettingsModal extends Modal { const newFile = await this.app.vault.create(newPath, ''); // 使用 processFrontMatter 來更新 frontmatter - await this.app.fileManager.processFrontMatter(newFile, (frontmatter: any) => { + await this.app.fileManager.processFrontMatter(newFile, (frontmatter: NoteFrontmatter) => { // 設置 redirect 和 summary const link = this.app.fileManager.generateMarkdownLink(originalFile, ""); frontmatter.type = "file"; @@ -258,14 +278,15 @@ export class NoteSettingsModal extends Modal { // 讀取自訂標題設定 if (this.files.length === 1) { 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'; - if (fileCache.frontmatter[titleField]) { - this.settings.title = fileCache.frontmatter[titleField] || ''; + if (frontmatter[titleField]) { + this.settings.title = getStringValue(frontmatter[titleField]); } const summaryField = this.plugin.settings.noteSummaryField || 'summary'; - if (fileCache.frontmatter[summaryField]) { - this.settings.summary = fileCache.frontmatter[summaryField] || ''; + if (frontmatter[summaryField]) { + this.settings.summary = getStringValue(frontmatter[summaryField]); } } } @@ -273,17 +294,18 @@ export class NoteSettingsModal extends Modal { // 如果有多個檔案,只讀取第一個檔案的設定作為預設值 if (this.files.length > 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) { - 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; } // 讀取隱藏設定 - if (fileCache.frontmatter.display === 'hidden') { + if (frontmatter.display === 'hidden') { this.settings.isHidden = true; } } @@ -296,8 +318,9 @@ export class NoteSettingsModal extends Modal { const noteFile = this.app.vault.getAbstractFileByPath(notePath); if (noteFile instanceof TFile) { const fm = this.app.metadataCache.getFileCache(noteFile)?.frontmatter; - if (fm && Array.isArray(fm['pinned'])) { - this.settings.isPinned = fm['pinned'].includes(this.files[0].name); + const pinned = getStringArray(fm?.pinned); + if (pinned.length > 0) { + this.settings.isPinned = pinned.includes(this.files[0].name); } } // 保存初始的 isPinned 狀態 @@ -313,7 +336,7 @@ export class NoteSettingsModal extends Modal { try { if (this.files.length === 1 && this.files[0].extension === "md") { // 使用 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'; if (this.settings.title) { frontmatter[titleField] = this.settings.title; @@ -327,16 +350,16 @@ export class NoteSettingsModal extends Modal { delete frontmatter[summaryField]; } if (this.settings.color) { - frontmatter['color'] = this.settings.color; + frontmatter.color = this.settings.color; } else { - delete frontmatter['color']; + delete frontmatter.color; } if (this.settings.isHidden) { - frontmatter['display'] = 'hidden'; + frontmatter.display = 'hidden'; } else if (this.settings.isMinimized) { - frontmatter['display'] = 'minimized'; + frontmatter.display = 'minimized'; } 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) { if (file.extension === "md") { // 使用 fileManager.processFrontMatter 更新 frontmatter - await this.app.fileManager.processFrontMatter(file, (frontmatter) => { + await this.app.fileManager.processFrontMatter(file, (frontmatter: NoteFrontmatter) => { if (this.settings.color) { - frontmatter['color'] = this.settings.color; + frontmatter.color = this.settings.color; } else { - delete frontmatter['color']; + delete frontmatter.color; } if (this.settings.isHidden) { - frontmatter['display'] = 'hidden'; + frontmatter.display = 'hidden'; } else if (this.settings.isMinimized) { - frontmatter['display'] = 'minimized'; + frontmatter.display = 'minimized'; } 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 - await this.app.fileManager.processFrontMatter(noteFile as TFile, (frontmatter) => { - let list: string[] = Array.isArray(frontmatter['pinned']) ? frontmatter['pinned'] : []; + if (!(noteFile instanceof TFile)) { + continue; + } + + await this.app.fileManager.processFrontMatter(noteFile, (frontmatter: NoteFrontmatter) => { + let list = getStringArray(frontmatter.pinned); if (this.settings.isPinned) { if (!list.includes(file.name)) list.push(file.name); } else { list = list.filter(n => n !== file.name); } if (list.length > 0) { - frontmatter['pinned'] = list; + frontmatter.pinned = list; } else { - delete frontmatter['pinned']; + delete frontmatter.pinned; } }); } diff --git a/src/modal/searchModal.ts b/src/modal/searchModal.ts index a826d15..bad3d80 100644 --- a/src/modal/searchModal.ts +++ b/src/modal/searchModal.ts @@ -1,8 +1,20 @@ -import { App, Modal, Setting, setIcon } from 'obsidian'; -import type { GridView } from '../GridView'; -import { t } from '../translations'; - -export class SearchModal extends Modal { +import { App, Modal, setIcon } from 'obsidian'; +import type { GridView } from '../GridView'; +import { t } from '../translations'; + +interface MetadataCacheWithTags { + getTags?: () => Record; +} + +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; defaultQuery: string; buttonElement: HTMLElement | undefined; @@ -92,20 +104,21 @@ export class SearchModal extends Modal { // 創建清空按鈕 const clearButton = inputContainer.createDiv('ge-search-clear-button'); - clearButton.style.display = searchTerms.length > 0 ? 'flex' : 'none'; + setHidden(clearButton, searchTerms.length === 0); setIcon(clearButton, 'x'); const updateClearButton = () => { 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'); - 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 selectedSuggestionIndex = -1; @@ -113,7 +126,7 @@ export class SearchModal extends Modal { const updateTagSuggestions = () => { const match = searchInput.value.substring(0, searchInput.selectionStart || 0).match(/#([^#\s]*)$/); if (!match) { - tagSuggestionContainer.style.display = 'none'; + setHidden(tagSuggestionContainer, true); tagSuggestionContainer.empty(); selectedSuggestionIndex = -1; return; @@ -134,7 +147,7 @@ export class SearchModal extends Modal { }).slice(0, 10); if (tagSuggestions.length === 0) { - tagSuggestionContainer.style.display = 'none'; + setHidden(tagSuggestionContainer, true); selectedSuggestionIndex = -1; return; } @@ -149,7 +162,7 @@ export class SearchModal extends Modal { applySuggestion(idx); }); }); - tagSuggestionContainer.style.display = 'block'; + setHidden(tagSuggestionContainer, false); }; const applySuggestion = (index: number) => { @@ -165,7 +178,7 @@ export class SearchModal extends Modal { flushInput(false); - tagSuggestionContainer.style.display = 'none'; + setHidden(tagSuggestionContainer, true); tagSuggestionContainer.empty(); selectedSuggestionIndex = -1; @@ -189,7 +202,7 @@ export class SearchModal extends Modal { }); // 隨機筆記模式下,搜尋範圍設定不顯示 if (this.gridView.sourceMode === 'random-note') { - searchScopeContainer.style.display = 'none'; + setHidden(searchScopeContainer, true); searchScopeCheckbox.checked = false; } @@ -218,7 +231,7 @@ export class SearchModal extends Modal { }); // 如果設定中的顯示媒體檔案為false,或在反向連結模式下,則隱藏搜尋媒體檔案設定 if (!this.gridView.plugin.settings.showMediaFiles || this.gridView.sourceMode === 'backlinks') { - searchMediaFilesContainer.style.display = 'none'; + setHidden(searchMediaFilesContainer, true); searchMediaFilesCheckbox.checked = false; this.gridView.searchMediaFiles = false; } @@ -295,7 +308,7 @@ export class SearchModal extends Modal { return; } - if (tagSuggestionContainer.style.display !== 'none') { + if (!isHidden(tagSuggestionContainer)) { if (e.key === 'ArrowDown') { e.preventDefault(); selectedSuggestionIndex = (selectedSuggestionIndex + 1) % tagSuggestions.length; @@ -312,7 +325,7 @@ export class SearchModal extends Modal { } } else { if (e.key === 'ArrowDown') { - const firstVisible = optionsList.find(o => o.style.display !== 'none'); + const firstVisible = optionsList.find(o => !isHidden(o)); if (firstVisible) { e.preventDefault(); firstVisible.focus(); @@ -331,7 +344,7 @@ export class SearchModal extends Modal { currentInputIndex = 0; updateClearButton(); renderTagButtons(); - tagSuggestionContainer.style.display = 'none'; + setHidden(tagSuggestionContainer, true); searchInput.focus(); }); optionsList.forEach((container, index) => { @@ -343,7 +356,7 @@ export class SearchModal extends Modal { } else if (e.key === 'ArrowDown') { e.preventDefault(); for (let i = index + 1; i < optionsList.length; i++) { - if (optionsList[i].style.display !== 'none') { + if (!isHidden(optionsList[i])) { optionsList[i].focus(); return; } @@ -351,7 +364,7 @@ export class SearchModal extends Modal { } else if (e.key === 'ArrowUp') { e.preventDefault(); for (let i = index - 1; i >= 0; i--) { - if (optionsList[i].style.display !== 'none') { + if (!isHidden(optionsList[i])) { optionsList[i].focus(); return; } @@ -571,10 +584,12 @@ export class SearchModal extends Modal { } // 應用位置 - modalEl.style.position = 'fixed'; - modalEl.style.top = `${top}px`; - modalEl.style.left = `${left}px`; - modalEl.style.transform = 'none'; + modalEl.setCssProps({ + position: 'fixed', + top: `${top}px`, + left: `${left}px`, + transform: 'none', + }); } } diff --git a/src/modal/shortcutSelectionModal.ts b/src/modal/shortcutSelectionModal.ts index c075c35..ff94384 100644 --- a/src/modal/shortcutSelectionModal.ts +++ b/src/modal/shortcutSelectionModal.ts @@ -115,7 +115,7 @@ export class ShortcutSelectionModal extends Modal { displayName = `🌐 ${uri.substring(0, 20)}${uri.length > 20 ? '...' : ''}`; } } - } catch (error) { + } catch { displayName = `🌐 ${uri.substring(0, 20)}${uri.length > 20 ? '...' : ''}`; } diff --git a/src/renderFiles.ts b/src/renderFiles.ts index 0dbad01..b399a4a 100644 --- a/src/renderFiles.ts +++ b/src/renderFiles.ts @@ -79,14 +79,11 @@ export async function renderFiles(gridView: GridView, container: HTMLElement) { targetFile = gridView.app.metadataCache.getFirstLinkpathDest(linkTarget, ''); } - if (targetFile && 'extension' in targetFile) { - // 使用 Obsidian 的 getBacklinksForFile API - const backlinks = (gridView.app.metadataCache as any).getBacklinksForFile(targetFile); - + if (targetFile instanceof TFile) { const currentLinkFiles = new Set(); - if (backlinks) { - // 收集所有反向連結的檔案路徑 - for (const [filePath, links] of backlinks.data.entries()) { + // 收集所有反向連結的檔案路徑 + for (const [filePath, links] of Object.entries(gridView.app.metadataCache.resolvedLinks)) { + if (links[targetFile.path]) { currentLinkFiles.add(filePath); } } @@ -166,7 +163,7 @@ export async function renderFiles(gridView: GridView, container: HTMLElement) { // frontmatter 標籤 if (fileCache.frontmatter && fileCache.frontmatter.tags) { - const fmTags = fileCache.frontmatter.tags; + const fmTags: unknown = fileCache.frontmatter.tags; if (typeof fmTags === 'string') { collectedTags.push( ...fmTags.split(/[,\s]+/) diff --git a/src/renderFolder.ts b/src/renderFolder.ts index 47d3bc6..6a2e6c5 100644 --- a/src/renderFolder.ts +++ b/src/renderFolder.ts @@ -10,6 +10,10 @@ import { showFolderRenameModal } from './modal/folderRenameModal'; import { ShortcutSelectionModal } from './modal/shortcutSelectionModal'; import { t } from './translations'; +interface AppWithShowInFolder { + showInFolder?: (path: string) => void; +} + export async function renderFolder(gridView: GridView, container: HTMLElement) { // 如果是資料夾模式,先顯示所有子資料夾 @@ -27,7 +31,7 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { // 防止預設行為以允許放置 event.preventDefault(); // 設定拖曳效果為移動 - (event as any).dataTransfer!.dropEffect = 'move'; + event.dataTransfer!.dropEffect = 'move'; // 顯示可放置的視覺提示 container.addClass('ge-dragover'); }, true); // 使用捕獲階段 @@ -54,8 +58,7 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { container.removeClass('ge-dragover'); // 處理從 Obsidian 檔案總管拖來的 obsidian://open URI(單檔/多檔) - const dt = (event as DragEvent).dataTransfer; - console.log(dt); + const dt = event.dataTransfer; if (dt) { try { const obsidianPaths = await extractObsidianPathsFromDT(dt); @@ -79,7 +82,7 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { // 3) 使用 Obsidian 的連結解析(支援不含副檔名與子資料夾) if (!resolved) { 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; } @@ -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; const srcPath = currentFolder.path || '/'; @@ -122,14 +125,14 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { if (text.startsWith('[[') && text.endsWith(']]')) { const inner = text.slice(2, -2); 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; } else { const direct = gridView.app.vault.getAbstractFileByPath(text); if (direct instanceof TFile) { resolvedFile = direct; } 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; } } @@ -204,8 +207,8 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { // 根據同名筆記設置背景色 const metadata = gridView.app.metadataCache.getFileCache(noteFile)?.frontmatter; - const colorValue = metadata?.color; - if (colorValue) { + const colorValue: unknown = metadata?.color; + if (typeof colorValue === 'string' && colorValue) { // 檢查是否為 HEX 色值 if (isHexColor(colorValue)) { // 使用自訂 CSS 變數來設置 HEX 顏色 @@ -217,10 +220,10 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { folderEl.addClass(`ge-folder-color-${colorValue}`); } } - const iconValue = metadata?.icon; - if (iconValue) { + const iconValue: unknown = metadata?.icon; + if (typeof iconValue === 'string' && iconValue) { // 修改原本的title文字 - const title = folderEl.querySelector('.ge-title'); + const title = folderEl.querySelector('.ge-title'); if (title) { title.textContent = `${iconValue} ${folder.name}`; } @@ -261,7 +264,7 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { .setTitle(t('open_in_file_explorer')) .setIcon('arrow-up-right') .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')) .setIcon('folder-x') .onClick(() => { - void gridView.app.fileManager.trashFile(noteFile as TFile); + if (noteFile instanceof TFile) { + void gridView.app.fileManager.trashFile(noteFile); + } }); }); } else { @@ -412,8 +417,8 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { }); // 刪除資料夾 menu.addItem((item) => { - (item as any).setWarning(true); item + .setWarning(true) .setTitle(t('delete_folder')) .setIcon('trash') .onClick(() => { @@ -441,13 +446,13 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { // 為資料夾項目添加拖曳目標功能 if (Platform.isDesktop) { - const folderItems = gridView.containerEl.querySelectorAll('.ge-folder-item'); + const folderItems = gridView.containerEl.querySelectorAll('.ge-folder-item'); folderItems.forEach(folderItem => { folderItem.addEventListener('dragover', (event) => { // 防止預設行為以允許放置 event.preventDefault(); // 設定拖曳效果為移動 - (event as any).dataTransfer!.dropEffect = 'move'; + event.dataTransfer!.dropEffect = 'move'; // 顯示可放置的視覺提示 folderItem.addClass('ge-dragover'); }); @@ -465,12 +470,12 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { folderItem.removeClass('ge-dragover'); // 處理從 Obsidian 檔案總管拖來的 obsidian://open URI(單檔/多檔) - const dt = (event as DragEvent).dataTransfer; + const dt = event.dataTransfer; if (dt) { try { const obsidianPaths = await extractObsidianPathsFromDT(dt); if (obsidianPaths.length) { - const folderPath = (folderItem as any).dataset.folderPath; + const folderPath = folderItem.dataset.folderPath; if (!folderPath) return; const folder = gridView.app.vault.getAbstractFileByPath(folderPath); if (!(folder instanceof TFolder)) return; @@ -493,8 +498,8 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { // 3) 使用 Obsidian 的連結解析(支援不含副檔名與子資料夾) if (!resolved) { - const srcPath = (folderItem as any).dataset.folderPath || '/'; - const dest = (gridView.app.metadataCache as any).getFirstLinkpathDest?.(p, srcPath); + const srcPath = folderItem.dataset.folderPath || '/'; + const dest = gridView.app.metadataCache.getFirstLinkpathDest(p, srcPath); 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; // 獲取目標資料夾路徑 - const folderPath = (folderItem as any).dataset.folderPath; + const folderPath = folderItem.dataset.folderPath; if (!folderPath) return; const folder = gridView.app.vault.getAbstractFileByPath(folderPath); if (!(folder instanceof TFolder)) return; // 使用 Obsidian API 解析每一行的連結或路徑 - const srcPath = (folderItem as any).dataset.folderPath || '/'; + const srcPath = folderItem.dataset.folderPath || '/'; const lines = filePath .split(/\r?\n/) .map((s: string) => s.trim()) @@ -543,14 +548,14 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { if (text.startsWith('[[') && text.endsWith(']]')) { const inner = text.slice(2, -2); 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; } else { const direct = gridView.app.vault.getAbstractFileByPath(text); if (direct instanceof TFile) { resolvedFile = direct; } 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; } } diff --git a/src/renderHeaderButton.ts b/src/renderHeaderButton.ts index e7f5a6a..1bee125 100644 --- a/src/renderHeaderButton.ts +++ b/src/renderHeaderButton.ts @@ -7,6 +7,41 @@ import { ShortcutSelectionModal } from './modal/shortcutSelectionModal'; import { createNewNote, createNewFolder, createNewCanvas, createNewBase, createShortcut as createShortcutUtil } from './utils/createItemUtils'; 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; + 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) { // 創建頂部按鈕區域 @@ -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(); // 從歷史記錄中移除 // 設定來源及搜尋狀態(不記錄到歷史) @@ -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(); // 從未來紀錄中移除 // 設定來源及搜尋狀態(不記錄到歷史) @@ -131,7 +168,8 @@ export function renderHeaderButton(gridView: GridView) { // 添加歷史記錄 gridView.recentSources.forEach((sourceInfoStr, index) => { try { - const sourceInfo = JSON.parse(sourceInfoStr); + const sourceInfo = parseSourceInfo(sourceInfoStr); + if (!sourceInfo) return; const { mode, path } = sourceInfo; // 根據模式顯示圖示和文字 @@ -260,7 +298,8 @@ export function renderHeaderButton(gridView: GridView) { // 添加未來記錄 gridView.futureSources.forEach((sourceInfoStr, index) => { try { - const sourceInfo = JSON.parse(sourceInfoStr); + const sourceInfo = parseSourceInfo(sourceInfoStr); + if (!sourceInfo) return; const { mode, path } = sourceInfo; // 根據模式顯示圖示和文字 @@ -319,7 +358,7 @@ export function renderHeaderButton(gridView: GridView) { if (!sourceInfo.searchCurrentLocationOnly) { displayText = '"' + (sourceInfo.searchQuery || t('search_results')) + '"'; } else { - displayText += `: \"${sourceInfo.searchQuery}\"`; + displayText += `: "${sourceInfo.searchQuery}"`; } } @@ -599,8 +638,9 @@ export function renderHeaderButton(gridView: GridView) { .setIcon('settings') .onClick(() => { // 打開插件設定頁面 - (gridView.app as any).setting.open(); - (gridView.app as any).setting.openTabById(gridView.plugin.manifest.id); + const setting = (gridView.app as AppWithSettings).setting; + setting?.open(); + setting?.openTabById(gridView.plugin.manifest.id); }); }); diff --git a/src/renderModePath.ts b/src/renderModePath.ts index 94051ba..32c745b 100644 --- a/src/renderModePath.ts +++ b/src/renderModePath.ts @@ -8,6 +8,35 @@ import { showFolderMoveModal } from './modal/folderMoveModal'; import { CustomModeModal } from './modal/customModeModal'; import { showSearchModal } from './modal/searchModal'; 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) { @@ -225,7 +254,7 @@ export function renderModePath(gridView: GridView) { .setDisabled(true) ); - subFolders.forEach((folder: any) => { + subFolders.forEach((folder) => { menu.addItem((item) => { item.setTitle(`${customFolderIcon} ${folder.name}`) .setIcon('corner-down-right') @@ -238,7 +267,7 @@ export function renderModePath(gridView: GridView) { } } - menu.showAtMouseEvent(event as MouseEvent); + menu.showAtMouseEvent(event); } else { void gridView.setSource('folder', path.path); gridView.clearSelection(); @@ -291,7 +320,7 @@ export function renderModePath(gridView: GridView) { // 3) 使用 Obsidian 的連結解析 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; } @@ -327,14 +356,14 @@ export function renderModePath(gridView: GridView) { if (text.startsWith('[[') && text.endsWith(']]')) { const inner = text.slice(2, -2); 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; } else { const direct = gridView.app.vault.getAbstractFileByPath(text); if (direct instanceof TFile) { resolvedFile = direct; } 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; } } @@ -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) => { event.preventDefault(); event.stopPropagation(); @@ -484,7 +513,7 @@ export function renderModePath(gridView: GridView) { .setIcon('arrow-up-right') .onClick(() => { 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) => { - (item as any).setWarning(true); item + .setWarning(true) .setTitle(t('delete_folder')) .setIcon('trash') .onClick(async () => { @@ -606,17 +635,18 @@ export function renderModePath(gridView: GridView) { // 根據目前模式設定對應的圖示和名稱 switch (gridView.sourceMode) { - case 'bookmarks': + case 'bookmarks': { modeIcon = '📑'; modeName = t('bookmarks_mode'); break; - case 'search': + } + case 'search': { modeIcon = '🔍'; 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) { - const searchView: any = searchLeaf.view; - const searchInputEl: HTMLInputElement | null = searchView.searchComponent ? searchView.searchComponent.inputEl : null; + const searchView = searchLeaf.view; + const searchInputEl: HTMLInputElement | null = searchView?.searchComponent?.inputEl ?? null; const currentQuery = searchInputEl?.value.trim(); if (currentQuery && currentQuery.length > 0) { modeName += `: ${currentQuery}`; @@ -625,7 +655,8 @@ export function renderModePath(gridView: GridView) { } } break; - case 'backlinks': + } + case 'backlinks': { modeIcon = '🔗'; modeName = t('backlinks_mode'); const activeFile = gridView.app.workspace.getActiveFile(); @@ -633,7 +664,8 @@ export function renderModePath(gridView: GridView) { modeName += `: ${activeFile.basename}`; } break; - case 'outgoinglinks': + } + case 'outgoinglinks': { modeIcon = '🔗'; modeName = t('outgoinglinks_mode'); const currentFile = gridView.app.workspace.getActiveFile(); @@ -641,6 +673,7 @@ export function renderModePath(gridView: GridView) { modeName += `: ${currentFile.basename}`; } break; + } case 'recent-files': modeIcon = '📅'; modeName = t('recent_files_mode'); @@ -707,7 +740,7 @@ export function renderModePath(gridView: GridView) { } switch (gridView.sourceMode) { - case 'bookmarks': + case 'bookmarks': { let bookmarkGroupName = ''; if (gridView.bookmarkGroupId === 'all') { bookmarkGroupName = t('all_bookmarks'); @@ -744,14 +777,14 @@ export function renderModePath(gridView: GridView) { menu.addSeparator(); // 取得所有書籤群組 - const bookmarksPlugin = (gridView.app as any).internalPlugins.plugins.bookmarks; + const bookmarksPlugin = (gridView.app as AppWithInternalPlugins).internalPlugins?.plugins?.bookmarks; if (bookmarksPlugin?.enabled) { - const bookmarks = bookmarksPlugin.instance.items; + const bookmarks = bookmarksPlugin.instance?.items ?? []; const groups: string[] = []; - const findGroups = (items: any[]) => { + const findGroups = (items: BookmarksPluginItem[]) => { items.forEach(item => { if (item.type === 'group') { - groups.push(item.title); + groups.push(item.title ?? ''); if (item.items) findGroups(item.items); } }); @@ -772,9 +805,10 @@ export function renderModePath(gridView: GridView) { menu.showAtMouseEvent(evt); }); break; + } case 'random-note': case 'recent-files': - case 'all-files': + case 'all-files': { if (gridView.plugin.settings.showMediaFiles && gridView.searchQuery === '') { // "顯示類型"選項 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; - case 'tasks': + } + case 'tasks': { const taskFilterSpan = modenameContainer.createEl('a', { text: t(`${gridView.taskFilter}`), cls: 'ge-sub-option' }); taskFilterSpan.addEventListener('click', (evt) => { const menu = new Menu(); @@ -838,7 +873,8 @@ export function renderModePath(gridView: GridView) { menu.showAtMouseEvent(evt); }); break; - default: + } + default: { if (gridView.sourceMode.startsWith('custom-')) { // 把 modenameContainer 加上所有自訂模式選項的選單 @@ -854,7 +890,7 @@ export function renderModePath(gridView: GridView) { let subName: string | undefined; 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) { const opt = mode.options[gridView.customOptionIndex]; subName = opt.name?.trim() || `${t('option')} ${gridView.customOptionIndex + 1}`; @@ -864,7 +900,7 @@ export function renderModePath(gridView: GridView) { subSpan.addEventListener('click', (evt) => { const menu = new Menu(); // 預設選項 - const defaultName = (mode as any).name?.trim() || t('default'); + const defaultName = mode.name?.trim() || t('default'); menu.addItem(item => { item.setTitle(defaultName) .setIcon('puzzle') @@ -904,6 +940,7 @@ export function renderModePath(gridView: GridView) { } } break; + } } } 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 }); - searchText.style.cursor = 'pointer'; searchText.addEventListener('click', () => { // 以文字元素作為定位點開啟搜尋 modal(popup 樣式) showSearchModal(gridView.app, gridView, gridView.searchQuery, searchText); diff --git a/src/utils/createItemUtils.ts b/src/utils/createItemUtils.ts index 64e17d6..851a28e 100644 --- a/src/utils/createItemUtils.ts +++ b/src/utils/createItemUtils.ts @@ -2,6 +2,14 @@ import { App, TFile, Notice } from 'obsidian'; import { showNameInputModal } from '../modal/folderRenameModal'; import { t } from '../translations'; +interface ShortcutFrontmatter { + type?: 'mode' | 'folder' | 'file' | 'search' | 'uri'; + redirect?: string; + searchCurrentLocationOnly?: boolean; + searchFilesNameOnly?: boolean; + searchMediaFiles?: boolean; +} + /** * 創建新筆記的共用函數 * @param app Obsidian App 實例 @@ -173,7 +181,7 @@ export async function createShortcut( const newFile = await app.vault.create(newPath, ''); // 使用 processFrontMatter 來更新 frontmatter - await app.fileManager.processFrontMatter(newFile, (frontmatter: any) => { + await app.fileManager.processFrontMatter(newFile, (frontmatter: ShortcutFrontmatter) => { if (option.type === 'mode') { frontmatter.type = 'mode'; frontmatter.redirect = option.value; @@ -181,10 +189,12 @@ export async function createShortcut( frontmatter.type = 'folder'; frontmatter.redirect = option.value; } else if (option.type === 'file') { - const link = app.fileManager.generateMarkdownLink( - app.vault.getAbstractFileByPath(option.value) as TFile, - "" - ); + const targetFile = app.vault.getAbstractFileByPath(option.value); + if (!(targetFile instanceof TFile)) { + return; + } + + const link = app.fileManager.generateMarkdownLink(targetFile, ""); frontmatter.type = "file"; frontmatter.redirect = link; } else if (option.type === 'search') { @@ -293,7 +303,7 @@ function generateFilenameFromUri(uri: string): string { const cleanUri = uri.replace(/[<>:"/\\|?*]/g, '_').substring(0, 30); return `🌐 ${cleanUri}`; - } catch (error) { + } catch { // 如果解析失敗,使用安全的預設名稱 const cleanUri = uri.replace(/[<>:"/\\|?*]/g, '_').substring(0, 30); return `🌐 ${cleanUri}`; diff --git a/src/utils/dragUtils.ts b/src/utils/dragUtils.ts index 4321f36..4f7b492 100644 --- a/src/utils/dragUtils.ts +++ b/src/utils/dragUtils.ts @@ -4,6 +4,14 @@ */ // 從 obsidian://open?vault=...&file=... 解析出 vault 名稱與檔案路徑 +interface ObsidianWindow extends Window { + app?: { + vault?: { + getName?: () => string; + }; + }; +} + export function parseObsidianOpenUris(input: string): Array<{ vault?: string; file?: string }> { const results: Array<{ vault?: string; file?: string }> = []; 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); } - const vaultName = (window as any).app?.vault?.getName?.() as string | undefined; + const vaultName = (window as ObsidianWindow).app?.vault?.getName?.(); const paths: string[] = []; for (const t of texts) { const entries = parseObsidianOpenUris(t); diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts index eec3c9a..0f41811 100644 --- a/src/utils/fileUtils.ts +++ b/src/utils/fileUtils.ts @@ -110,15 +110,16 @@ export function sortFiles(files: TFile[], gridView: GridView, overrideSortType?: return { file, mDate: (() => { - if (metadata?.frontmatter) { + const frontmatter = metadata?.frontmatter; + if (frontmatter) { // 支援多個欄位名稱(用逗號隔開) const fieldNames = settings.modifiedDateField ? settings.modifiedDateField.split(',').map(f => f.trim()).filter(Boolean) : []; for (const fieldName of fieldNames) { - const dateStr = metadata.frontmatter[fieldName]; - if (dateStr) { - const date = new Date(dateStr); + const dateValue: unknown = frontmatter[fieldName]; + if (typeof dateValue === 'string' || typeof dateValue === 'number' || dateValue instanceof Date) { + const date = new Date(dateValue); if (!isNaN(date.getTime())) { return date.getTime(); } @@ -128,15 +129,16 @@ export function sortFiles(files: TFile[], gridView: GridView, overrideSortType?: return file.stat.mtime; })(), cDate: (() => { - if (metadata?.frontmatter) { + const frontmatter = metadata?.frontmatter; + if (frontmatter) { // 支援多個欄位名稱(用逗號隔開) const fieldNames = settings.createdDateField ? settings.createdDateField.split(',').map(f => f.trim()).filter(Boolean) : []; for (const fieldName of fieldNames) { - const dateStr = metadata.frontmatter[fieldName]; - if (dateStr) { - const date = new Date(dateStr); + const dateValue: unknown = frontmatter[fieldName]; + if (typeof dateValue === 'string' || typeof dateValue === 'number' || dateValue instanceof Date) { + const date = new Date(dateValue); if (!isNaN(date.getTime())) { return date.getTime(); } @@ -186,7 +188,7 @@ export function isFolderIgnored(folder: TFolder, ignoredFolders: string[], ignor // 檢查資料夾名稱是否包含模式字串(不區分大小寫) return folder.name.toLowerCase().includes(pattern.toLowerCase()); } - } catch (error) { + } catch { // 如果正則表達式無效,直接檢查資料夾名稱 return folder.name.toLowerCase().includes(pattern.toLowerCase()); } @@ -214,7 +216,7 @@ export function ignoredFiles(files: TFile[], gridView: GridView): TFile[] { // 檢查檔案的 metadata 是否有 hidden: true if (file.extension === 'md') { const metadata = gridView.app.metadataCache.getFileCache(file)?.frontmatter; - const displayValue = metadata?.display; + const displayValue: unknown = metadata?.display; if (displayValue === 'hidden') { return false; } @@ -303,8 +305,8 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean): const resolvedLinks = app.metadataCache.resolvedLinks; for (const [sourcePath, links] of Object.entries(resolvedLinks)) { if (Object.keys(links).includes(activeFile.path)) { - const sourceFile = app.vault.getAbstractFileByPath(sourcePath) as TFile; - if (sourceFile) { + const sourceFile = app.vault.getAbstractFileByPath(sourcePath); + if (sourceFile instanceof TFile) { backlinks.add(sourceFile); } } @@ -325,8 +327,8 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean): if (fileLinks) { for (const targetPath of Object.keys(fileLinks)) { - const targetFile = app.vault.getAbstractFileByPath(targetPath) as TFile; - if (targetFile && (isDocumentFile(targetFile) || + const targetFile = app.vault.getAbstractFileByPath(targetPath); + if (targetFile instanceof TFile && (isDocumentFile(targetFile) || (settings.showMediaFiles && isMediaFile(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 任務 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') { // 只匹配所有任務均已完成的檔案(至少有一個已完成且沒有未完成) - const hasCompleted = /^[\s]*[-*]\s*\[x\](?![^\[]*\[\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 hasIncomplete = /^[\s]*[-*]\s*\[\s*\](?![^[]*\[\s*[^\s\]]+\]).*$/m.test(content); shouldAdd = hasCompleted && !hasIncomplete; } else if (gridView.taskFilter === 'all') { // 匹配任何任務(已完成或未完成皆可) - const hasIncomplete = /^[\s]*[-*]\s*\[\s*\](?![^\[]*\[\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 hasCompleted = /^[\s]*[-*]\s*\[x\](?![^[]*\[\s*[^\s\]]+\]).*$/m.test(content); shouldAdd = hasIncomplete || hasCompleted; } @@ -567,7 +569,7 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean): 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 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) { console.error('Grid Explorer: Error executing Dataview query.', error); return []; diff --git a/src/utils/mediaUtils.ts b/src/utils/mediaUtils.ts index 9f0621a..9d7bc85 100644 --- a/src/utils/mediaUtils.ts +++ b/src/utils/mediaUtils.ts @@ -115,7 +115,7 @@ async function validateRemoteImage(url: string): Promise { return url; } } - } catch (error) { + } catch { try { const getResponse = await requestUrl({ url }); if (isSuccessfulStatus(getResponse)) { diff --git a/styles.css b/styles.css index f77e2cf..510102a 100644 --- a/styles.css +++ b/styles.css @@ -56,6 +56,10 @@ -webkit-tap-highlight-color: transparent; } +.ge-grid-item-full-height { + height: 100%; +} + /* 右上角懸浮開啟筆記按鈕 Hover Open-in-Grid Button */ /* .ge-grid-item { position: relative; } .ge-hover-open-note { @@ -619,6 +623,16 @@ 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 { content: "📌"; @@ -1372,6 +1386,7 @@ a.ge-current-folder:hover { .ge-search-text { font-size: 14px; color: var(--text-normal); + cursor: pointer; flex-grow: 1; /* 讓文字填滿剩餘空間 */ min-width: 0; @@ -1632,6 +1647,11 @@ a.ge-current-folder:hover { 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 { font-size: 14px; font-weight: 500; @@ -1779,6 +1799,32 @@ a.ge-current-folder:hover { } /* 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 { border: 1px solid var(--background-modifier-border); border-radius: 6px; @@ -2129,6 +2175,15 @@ a.ge-current-folder:hover { 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-edit-button, .ge-note-info-button { @@ -2803,6 +2858,11 @@ a.ge-current-folder:hover { pointer-events: none; } +.ge-explorer-stash-dropzone-clickable { + pointer-events: auto; + cursor: pointer; +} + .ge-explorer-stash-item { padding-left: 28px; /* 與 .ge-explorer-mode-item 一致的縮排 */