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