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

View file

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

View file

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

4
package-lock.json generated
View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { TFile, TFolder, WorkspaceLeaf, Menu, setIcon, Platform, normalizePath, ItemView, EventRef, FuzzySuggestModal, parseLinktext } from 'obsidian';
import { TFile, TFolder, WorkspaceLeaf, Menu, setIcon, Platform, normalizePath, ItemView, EventRef, FuzzySuggestModal, parseLinktext, ViewStateResult } from 'obsidian';
import GridExplorerPlugin from './main';
import { 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<string, unknown> {
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<void> {
async setState(state: unknown, result: ViewStateResult): Promise<void> {
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;
}
}

View file

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

View file

@ -32,6 +32,130 @@ interface FileRenderParams {
}
// 定義網格視圖
type GridViewLeaf = WorkspaceLeaf & {
pinned?: boolean;
updateHeader?: () => void;
};
interface DataviewEventSource {
on(name: 'dataview:index-ready', callback: () => void): ReturnType<GridExplorerPlugin['app']['metadataCache']['on']>;
}
interface WorkspaceEventTrigger {
trigger?: (name: 'ge-grid-source-changed', data: { mode: string; path: string }) => void;
}
interface InternalPlugins {
plugins: {
bookmarks?: {
enabled?: boolean;
};
};
getPluginById?: (id: string) => {
instance?: {
openGlobalSearch?: (query: string) => void;
};
} | undefined;
}
interface AppWithInternalPlugins {
internalPlugins?: InternalPlugins;
}
interface CommandManager {
executeCommandById?: (id: string) => void;
}
interface AppWithCommands {
commands?: CommandManager;
}
interface AppWithDataviewPlugin {
plugins?: {
plugins?: {
dataview?: {
api?: unknown;
};
};
};
}
interface MenuItemWithWarning {
setWarning: (warning: boolean) => void;
}
interface ExplorerViewActions {
addToStash: (filePaths: string[]) => void;
refresh: () => void;
}
interface WorkspaceSplitWithChildren {
children?: unknown[];
}
interface NoteViewContainerWithKeydownHandler extends HTMLElement {
keydownHandler?: (e: KeyboardEvent) => void;
}
interface GridViewStateData {
sourceMode?: string;
sourcePath?: string;
baseSortType?: string;
sortType?: string;
searchQuery?: string;
searchCurrentLocationOnly?: boolean;
searchFilesNameOnly?: boolean;
searchMediaFiles?: boolean;
fileNameFilterQuery?: string;
includeMedia?: boolean;
minMode?: boolean;
showIgnoredItems?: boolean;
baseCardLayout?: 'horizontal' | 'vertical';
cardLayout?: 'horizontal' | 'vertical';
hideHeaderElements?: boolean;
showFileNameFilter?: boolean;
showDateDividers?: boolean;
showNoteTags?: boolean;
recentSources?: string[];
futureSources?: string[];
bookmarkGroupId?: string;
}
interface GridViewState {
state?: GridViewStateData;
}
interface ShortcutFrontmatter extends FrontMatterCache {
type?: unknown;
redirect?: unknown;
cardLayout?: unknown;
searchCurrentLocationOnly?: unknown;
searchFilesNameOnly?: unknown;
searchMediaFiles?: unknown;
}
function getFrontmatterValue(frontmatter: FrontMatterCache | undefined, key: string): unknown {
return frontmatter ? (frontmatter as Record<string, unknown>)[key] : undefined;
}
function formatFrontmatterValue(value: unknown): string {
if (typeof value === 'number') return value.toLocaleString();
if (typeof value === 'string') return value;
if (typeof value === 'boolean') return value ? 'true' : 'false';
if (typeof value === 'bigint') return value.toString();
if (typeof value === 'symbol' || typeof value === 'function') return '';
if (Array.isArray(value)) return value.map(item => formatFrontmatterValue(item)).join(', ');
if (value === null || value === undefined) return '';
if (typeof value === 'object') {
try {
return JSON.stringify(value);
} catch {
return '';
}
}
return '';
}
export class GridView extends ItemView {
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<HTMLInputElement>('.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(/<!--[\s\S]*?-->/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<string>();
// 從 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<void> {
async setState(state: GridViewState): Promise<void> {
if (state?.state) {
this.sourceMode = state.state.sourceMode || 'folder';
this.sourcePath = state.state.sourcePath || '/';

View file

@ -1,4 +1,4 @@
import { Plugin, TFolder, TFile, Menu, WorkspaceLeaf } from 'obsidian';
import { EventRef, Menu, MenuItem, Plugin, TFile, TFolder, WorkspaceLeaf } from 'obsidian';
import { GridView } from './GridView';
import { 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<void>;
}
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<GallerySettings> | 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();
}
});
}
}
}
}

View file

@ -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<HTMLElement>('.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 = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
setIcon(dragHandle, 'grip-vertical');
// 選項名稱
const nameEl = headerEl.createDiv('ge-custommode-option-name');
@ -181,11 +175,11 @@ export class CustomModeModal extends Modal {
// 展開/摺疊按鈕
const toggleEl = headerEl.createDiv('ge-custommode-option-toggle');
toggleEl.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9,18 15,12 9,6"></polyline></svg>`;
setIcon(toggleEl, 'chevron-right');
// 刪除按鈕
const deleteEl = headerEl.createDiv('ge-custommode-option-delete');
deleteEl.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3,6 5,6 21,6"></polyline><path d="m19,6v14a2,2 0 0,1 -2,2H7a2,2 0 0,1 -2,-2V6m3,0V4a2,2 0 0,1 2,-2h4a2,2 0 0,1 2,2v2"></path></svg>`;
setIcon(deleteEl, 'trash-2');
// 內容區域
const contentEl = optionContainer.createDiv('ge-custommode-option-content');
@ -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);

View file

@ -1,4 +1,4 @@
import { App, Modal, Setting, TFolder, TFile } from 'obsidian';
import { App, Modal, Setting, TFile, TFolder, TextComponent } from 'obsidian';
import GridExplorerPlugin from '../main';
import { 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;
}
}
});

View file

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

View file

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

View file

@ -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<HTMLElement>('.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<HTMLElement>('.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<HTMLElement>('.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 { // 縮小

View file

@ -1,4 +1,4 @@
import { App, Modal, Setting, TFile } from 'obsidian';
import { App, Modal, Setting, TFile, TextComponent, ToggleComponent } from 'obsidian';
import GridExplorerPlugin from '../main';
import { 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;
}
});
}

View file

@ -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<string, unknown>;
}
function setHidden(el: HTMLElement, hidden: boolean) {
el.toggleClass('ge-hidden', hidden);
}
function isHidden(el: HTMLElement): boolean {
return el.hasClass('ge-hidden');
}
export class SearchModal extends Modal {
gridView: GridView;
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',
});
}
}

View file

@ -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 ? '...' : ''}`;
}

View file

@ -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<string>();
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]+/)

View file

@ -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<HTMLElement>('.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<HTMLElement>('.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;
}
}

View file

@ -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<string, unknown>;
return typeof source.mode === 'string' &&
typeof source.path === 'string' &&
(source.searchQuery === undefined || typeof source.searchQuery === 'string') &&
(source.searchCurrentLocationOnly === undefined || typeof source.searchCurrentLocationOnly === 'boolean') &&
(source.searchFilesNameOnly === undefined || typeof source.searchFilesNameOnly === 'boolean') &&
(source.searchMediaFiles === undefined || typeof source.searchMediaFiles === 'boolean');
}
function parseSourceInfo(sourceInfoStr: string): SourceInfo | null {
const parsed: unknown = JSON.parse(sourceInfoStr);
return isSourceInfo(parsed) ? parsed : null;
}
export function renderHeaderButton(gridView: GridView) {
// 創建頂部按鈕區域
@ -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);
});
});

View file

@ -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', () => {
// 以文字元素作為定位點開啟搜尋 modalpopup 樣式)
showSearchModal(gridView.app, gridView, gridView.searchQuery, searchText);

View file

@ -2,6 +2,14 @@ import { App, TFile, Notice } from 'obsidian';
import { showNameInputModal } from '../modal/folderRenameModal';
import { 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}`;

View file

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

View file

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

View file

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

View file

@ -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 一致的縮排 */