From 9bf25cabaa03826b2af1c128db986e24d1cdfb8d Mon Sep 17 00:00:00 2001 From: Devon22 Date: Wed, 10 Jun 2026 23:34:07 +0800 Subject: [PATCH] 3.4.0 --- docs/custom-mode-example.md | 51 ------------ docs/custom-mode-fields.md | 134 ------------------------------ docs/custom-mode-fields_ja.md | 134 ------------------------------ docs/custom-mode-fields_zhTW.md | 140 -------------------------------- manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/FileWatcher.ts | 10 ++- src/GridView.ts | 84 ++----------------- src/main.ts | 22 +++++ src/modal/customModeModal.ts | 27 +++--- src/renderFolder.ts | 10 ++- src/settings.ts | 14 ++-- src/translations/en.ts | 2 +- src/translations/ja.ts | 4 +- src/translations/ko.ts | 2 +- src/translations/ru.ts | 4 +- src/translations/uk.ts | 4 +- src/translations/zh-TW.ts | 4 +- src/translations/zh.ts | 4 +- src/utils/fileUtils.ts | 65 ++++++++++----- styles.css | 106 +++++++++++++----------- 22 files changed, 180 insertions(+), 649 deletions(-) delete mode 100644 docs/custom-mode-example.md delete mode 100644 docs/custom-mode-fields.md delete mode 100644 docs/custom-mode-fields_ja.md delete mode 100644 docs/custom-mode-fields_zhTW.md diff --git a/docs/custom-mode-example.md b/docs/custom-mode-example.md deleted file mode 100644 index e93803b..0000000 --- a/docs/custom-mode-example.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -date: 2025-06-26 ---- -1. My Favorites -``` -return dv.pages("#Favorites").sort(page => page.modified_date ? dv.date(page.modified_date) : page.file.mtime, 'desc'); -``` - -2. On This Day in History -``` -const today = new Date(); -return dv.pages().where(p => { - const pageDate = p.modified_date ? new Date(p.modified_date): new Date(p.file.mtime); - return pageDate.getYear() !== today.getYear() && - pageDate.getMonth() === today.getMonth() && - pageDate.getDate() === today.getDate(); -}); -``` - -3. Backlinks -``` -const currentPage = dv.current(); -if (!currentPage) return []; -return dv.pages().where(p => p.file.outlinks.includes(currentPage.file.link) && p.file.path.split('/').length > 1).sort(page => page.modified_date ? dv.date(page.modified_date) : page.file.mtime, 'desc'); -``` - -4. Random Images -``` -const files = app.vault.getFiles(); -const imageFiles = files.filter(file => - file.path.startsWith("resources/") && - /\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i.test(file.name) -); -return imageFiles - .sort(() => 0.5 - Math.random()) - .slice(0, 10) - .map(f => ({ file: f })); -``` - -5. Random Videos -``` -const files = app.vault.getFiles(); -const videoFiles = files.filter(file => - file.path.startsWith("resources/") && - /\.(mp4|webm|mov|avi|mkv|ogv)$/i.test(file.name) -); -return videoFiles - .sort(() => 0.5 - Math.random()) - .slice(0, 10) - .map(f => ({ file: f })); -``` \ No newline at end of file diff --git a/docs/custom-mode-fields.md b/docs/custom-mode-fields.md deleted file mode 100644 index 7fe573b..0000000 --- a/docs/custom-mode-fields.md +++ /dev/null @@ -1,134 +0,0 @@ -# Grid Explorer Custom Mode Fields Guide - -## Overview - -Grid Explorer supports setting field aliases and performing real-time computations in custom modes. This allows for more flexible display and calculation of note metadata. - -## Basic Syntax - -### Field Aliases - -Use the `|` symbol to set a display name for a field: - -``` -original_field|Display Name -``` - -**Examples:** -- `birthday|Birthday` - Displays the `birthday` field as "Birthday" -- `status|Status` - Displays the `status` field as "Status" - -### Computation Expressions - -Use `{{ ... }}` to wrap JavaScript expressions for computation: - -``` -field_name {{ expression }} -``` - -**Available Variables:** - -| Variable | Description | Example | -|----------|-------------|---------| -| `value` | Current field's raw value | `value` or `this` | -| `metadata` | The complete frontmatter metadata object | `metadata.tags` | -| `app` | Obsidian app instance | `app.vault.getMarkdownFiles()` | -| `dv` | Dataview API instance (requires Dataview plugin) | `dv.pages('#tag')` | - -## Advanced Usage - -### Combining Aliases and Computations - -``` -field_name|Display Name {{ expression }} -``` - -**Basic Example:** -``` -age|Age {{ Math.floor((Date.now() - new Date(value).getTime()) / (1000 * 60 * 60 * 24 * 365.25)) }} -``` - -**With Dataview Example:** -``` -tags|Related Notes {{ dv.pages(value).length }} -``` - -### Multi-field Combinations - -``` -full_name|Full Name {{ (metadata.first_name || '') + ' ' + (metadata.last_name || '') }} -``` - -## Practical Examples - -### 1. Basic Computations - -#### Calculate Age -``` -birthday|Age {{ Math.floor((Date.now() - new Date(value).getTime()) / (1000 * 60 * 60 * 24 * 365.25)) }} -``` - -#### Format Date -``` -date|Date {{ new Date(value).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) }} -``` - -### 2. Advanced Dataview Usage - -#### Count Related Notes -``` -tags|Related Notes {{ dv.pages(value).length }} -``` - -#### Show Incomplete Tasks -``` -file.folder|To-dos {{ dv.pages(`"${value}"`).file.tasks.where(t => !t.completed).length }} -``` - -#### Calculate Tag Frequency -``` -tags|Tag Stats {{ value.map(tag => `${tag}(${dv.pages(tag).length})`).join(', ') }} -``` - -#### Show Last Modified Time -``` -file.mtime|Last Modified {{ dv.date(value).toFormat('yyyy-MM-dd HH:mm') }} -``` - -### 3. Conditional Display - -``` -status|Status {{ value === 'active' ? 'Active' : 'Inactive' }} -``` - -### 4. Progress Calculation - -``` -progress|Progress {{ Math.round((metadata.completed / metadata.total) * 100) + '%' }} -``` - -## Notes - -1. JavaScript code in computation expressions is executed dynamically when displayed -2. If a computation fails, it falls back to the original value -3. For security, avoid using potentially harmful code in expressions -4. Keep expressions simple; use Dataview plugin for complex calculations - -## Troubleshooting - -If computations don't work as expected: - -1. Check expression syntax -2. Use `console.log(value, metadata)` for debugging -3. Verify field names and case sensitivity -4. Check for extra spaces or special characters - -## Advanced Tips - -- Use ternary operators for conditional rendering -- Combine multiple fields in computations -- Use JavaScript built-in functions (like `toLocaleString()`, `toFixed()`) for formatting -- Access Obsidian API using the `app` variable ---- - -This feature allows for flexible display and computation of note data without writing complex plugin code. diff --git a/docs/custom-mode-fields_ja.md b/docs/custom-mode-fields_ja.md deleted file mode 100644 index 4089e69..0000000 --- a/docs/custom-mode-fields_ja.md +++ /dev/null @@ -1,134 +0,0 @@ -# Grid Explorer カスタムモード フィールドガイド - -## 概要 - -Grid Explorer は、カスタムモードでフィールドのエイリアスを設定し、リアルタイムで計算を実行する機能をサポートしています。これにより、メタデータを柔軟に表示・計算できます。 - -## 基本構文 - -### フィールドエイリアス - -`|` 記号を使用して、フィールドに表示名を設定します: - -``` -元のフィールド名|表示名 -``` - -**例:** -- `birthday|誕生日` - `birthday` フィールドを「誕生日」として表示 -- `status|状態` - `status` フィールドを「状態」として表示 - -### 計算式 - -`{{ ... }}` で JavaScript の式を囲んで計算を実行します: - -``` -フィールド名 {{ 式 }} -``` - -**使用可能な変数:** - -| 変数 | 説明 | 例 | -|------|------|-----| -| `value` | 現在のフィールドの値 | `value` または `this` | -| `metadata` | フロントマターのメタデータオブジェクト | `metadata.tags` | -| `app` | Obsidian アプリケーションインスタンス | `app.vault.getMarkdownFiles()` | -| `dv` | Dataview API インスタンス(Dataview プラグインが必要) | `dv.pages('#tag')` | - -## 高度な使い方 - -### エイリアスと計算の組み合わせ - -``` -フィールド名|表示名 {{ 式 }} -``` - -**基本例:** -``` -birthday|年齢 {{ Math.floor((Date.now() - new Date(value).getTime()) / (1000 * 60 * 60 * 24 * 365.25)) }} -``` - -**Dataview 使用例:** -``` -tags|関連ノート {{ dv.pages(value).length }} -``` - -### 複数フィールドの組み合わせ - -``` -full_name|フルネーム {{ (metadata.first_name || '') + ' ' + (metadata.last_name || '') }} -``` - -## 実践的な例 - -### 1. 基本的な計算 - -#### 年齢の計算 -``` -birthday|年齢 {{ Math.floor((Date.now() - new Date(value).getTime()) / (1000 * 60 * 60 * 24 * 365.25)) }} -``` - -#### 日付のフォーマット -``` -date|日付 {{ new Date(value).toLocaleDateString('ja-JP', { year: 'numeric', month: 'long', day: 'numeric' }) }} -``` - -### 2. Dataview の高度な機能 - -#### 関連ノートのカウント -``` -tags|関連ノート {{ dv.pages(value).length }} -``` - -#### 未完了タスクの表示 -``` -file.folder|ToDo {{ dv.pages(`"${value}"`).file.tasks.where(t => !t.completed).length }} -``` - -#### タグの使用頻度 -``` -tags|タグ統計 {{ value.map(tag => `${tag}(${dv.pages(tag).length})`).join(', ') }} -``` - -#### 最終更新日時の表示 -``` -file.mtime|最終更新 {{ dv.date(value).toFormat('yyyy-MM-dd HH:mm') }} -``` - -### 3. 条件付き表示 - -``` -status|状態 {{ value === 'active' ? '有効' : '無効' }} -``` - -### 4. 進捗率の計算 - -``` -progress|進捗 {{ Math.round((metadata.completed / metadata.total) * 100) + '%' }} -``` - -## 注意事項 - -1. 計算式内の JavaScript コードは表示時に動的に実行されます -2. 計算に失敗した場合は元の値が表示されます -3. セキュリティのため、危険なコードは使用しないでください -4. 複雑な計算には Dataview プラグインの使用を推奨します - -## トラブルシューティング - -計算が期待通りに動作しない場合: - -1. 式の構文を確認してください -2. `console.log(value, metadata)` でデバッグできます -3. フィールド名と大文字小文字を確認してください -4. 余分なスペースや特殊文字を確認してください - -## 高度なヒント - -- 条件付き表示には三項演算子を使用します -- 複数のフィールドを組み合わせて計算します -- 表示のフォーマットには `toLocaleString()` や `toFixed()` などの JavaScript 組み込み関数を使用します -- `app` 変数で Obsidian API にアクセスできます ---- - -この機能を使用すると、複雑なプラグインコードを書かずに、ノートデータを柔軟に表示・計算できます。 diff --git a/docs/custom-mode-fields_zhTW.md b/docs/custom-mode-fields_zhTW.md deleted file mode 100644 index ad3576e..0000000 --- a/docs/custom-mode-fields_zhTW.md +++ /dev/null @@ -1,140 +0,0 @@ -# Grid Explorer 自訂模式欄位說明 - -## 功能概述 - -Grid Explorer 現在支援在自訂模式中為欄位設定別名,並在顯示時進行即時計算。這讓您能夠以更靈活的方式顯示和計算筆記中的元數據。 - -## 基本語法 - -### 欄位別名 - -使用 `|` 符號為欄位設定顯示名稱: - -``` -原始欄位名稱|顯示名稱 -``` - -**範例:** -- `birthday|生日` - 將 `birthday` 欄位顯示為「生日」 -- `status|狀態` - 將 `status` 欄位顯示為「狀態」 - -### 計算表達式 - -使用 `{{ ... }}` 包裝 JavaScript 表達式來進行計算: - -``` -欄位名稱 {{ 表達式 }} -``` - -**可用變數:** - -在計算表達式中,您可以使用以下變數: - -| 變數 | 說明 | 範例 | -|------|------|------| -| `value` | 當前欄位的原始值 | `value` 或 `this` | -| `metadata` | 整個 frontmatter 元數據對象 | `metadata.tags` | -| `app` | Obsidian 應用程式實例 | `app.vault.getMarkdownFiles()` | -| `dv` | Dataview API 實例(需安裝 Dataview 插件) | `dv.pages('#tag')` | - -## 進階用法 - -### 結合別名與計算 - -您可以同時使用別名和計算表達式: - -``` -欄位名稱|顯示名稱 {{ 表達式 }} -``` - -**基本範例:** -``` -birthday|年齡 {{ Math.floor((Date.now() - new Date(value).getTime()) / (1000 * 60 * 60 * 24 * 365.25)) }} -``` - -**結合 Dataview 範例:** -``` -tags|相關筆記 {{ dv.pages(value).length }} -``` - -### 多欄位組合 - -您可以在計算表達式中引用其他欄位: - -``` -full_name|全名 {{ (metadata.first_name || '') + ' ' + (metadata.last_name || '') }} -``` - -## 實際應用範例 - -### 1. 基本計算 - -#### 計算年齡 -``` -birthday|年齡 {{ Math.floor((Date.now() - new Date(value).getTime()) / (1000 * 60 * 60 * 24 * 365.25)) }} -``` - -#### 格式化日期 -``` -date|日期 {{ new Date(value).toLocaleDateString('zh-TW', { year: 'numeric', month: 'long', day: 'numeric' }) }} -``` - -### 2. 使用 Dataview 的進階功能 - -#### 計算相關筆記數量 -``` -tags|相關筆記 {{ dv.pages(value).length }} -``` - -#### 顯示未完成任務數 -``` -file.folder|待辦事項 {{ dv.pages(`"${value}"`).file.tasks.where(t => !t.completed).length }} -``` - -#### 計算標籤使用頻率 -``` -tags|標籤統計 {{ value.map(tag => `${tag}(${dv.pages(tag).length})`).join(', ') }} -``` - -#### 顯示最近修改時間 -``` -file.mtime|最後修改 {{ dv.date(value).toFormat('yyyy-MM-dd HH:mm') }} -``` - -### 3. 條件顯示 - -``` -status|狀態 {{ value === 'active' ? '啟用' : '停用' }} -``` - -### 4. 計算進度 - -``` -progress|進度 {{ Math.round((metadata.completed / metadata.total) * 100) + '%' }} -``` - -## 注意事項 - -1. 計算表達式中的 JavaScript 代碼會在顯示時動態執行 -2. 如果計算表達式出錯,會自動回退顯示原始值 -3. 為確保安全性,不建議在表達式中使用可能有害的代碼 -4. 表達式應保持簡潔,複雜的計算建議使用 Dataview 插件 - -## 疑難排解 - -如果計算結果不符合預期: - -1. 檢查表達式語法是否正確 -2. 使用 `console.log(value, metadata)` 調試表達式 -3. 確認欄位名稱和大小寫是否正確 -4. 檢查是否有額外的空格或特殊字符 - -## 進階提示 - -- 使用三元運算符進行條件渲染 -- 結合多個欄位進行計算 -- 使用 JavaScript 的內建函數(如 `toLocaleString()`、`toFixed()` 等)格式化輸出 -- 在表達式中使用 `app` 變數訪問 Obsidian API ---- - -這個功能讓您能夠更靈活地呈現和計算筆記中的數據,無需編寫複雜的插件代碼即可實現強大的顯示效果。 diff --git a/manifest.json b/manifest.json index 2267bc6..b5cc9a9 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "gridexplorer", "name": "GridExplorer", - "version": "3.3.1", + "version": "3.4.0", "minAppVersion": "1.8.7", "description": "Browse note files in a grid view.", "author": "Devon22", diff --git a/package-lock.json b/package-lock.json index e463e0d..b2c639f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gridexplorer", - "version": "3.3.1", + "version": "3.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gridexplorer", - "version": "3.3.1", + "version": "3.4.0", "license": "MIT", "dependencies": { "jszip": "^3.10.1" diff --git a/package.json b/package.json index 15af1d1..263088d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gridexplorer", - "version": "3.3.1", + "version": "3.4.0", "description": "Browse note files in a grid view.", "main": "main.js", "scripts": { diff --git a/src/FileWatcher.ts b/src/FileWatcher.ts index a7b743b..dd08158 100644 --- a/src/FileWatcher.ts +++ b/src/FileWatcher.ts @@ -153,11 +153,15 @@ export class FileWatcher { return; } - // 處理自訂模式,僅當腳本包含 dv.current 時才觸發 + // 處理自訂模式,僅當查詢包含 this.file 時才觸發 if (sourceMode.startsWith('custom-')) { const mode = this.plugin.settings.customModes.find(m => m.internalName === sourceMode); - if (mode && mode.dataviewCode.includes('dv.current')) { - this.scheduleRender(); + if (mode) { + const hasThisFile = mode.dataviewQuery.includes('this.file') || + (mode.options && mode.options.some(opt => opt.dataviewQuery.includes('this.file'))); + if (hasThisFile) { + this.scheduleRender(); + } } } } diff --git a/src/GridView.ts b/src/GridView.ts index 3dc2ab0..be2eb6b 100644 --- a/src/GridView.ts +++ b/src/GridView.ts @@ -72,16 +72,6 @@ interface AppWithCommands { commands?: CommandManager; } -interface AppWithDataviewPlugin { - plugins?: { - plugins?: { - dataview?: { - api?: unknown; - }; - }; - }; -} - interface MenuItemWithWarning { setWarning: (warning: boolean) => void; } @@ -913,59 +903,13 @@ export class GridView extends ItemView { // 如果 fields 不為空,則使用它來顯示摘要 if (fields) { - // 以逗號拆分,但忽略 {{ ... }} 內的逗號 - const fieldList: string[] = (() => { - const parts: string[] = []; - let buf = ''; - let depth = 0; // 在 {{...}} 內時 depth > 0 - for (let i = 0; i < fields.length; i++) { - const ch = fields[i]; - const next = fields[i + 1]; - // 偵測 '{{' - if (ch === '{' && next === '{') { - depth++; - buf += '{{'; - i++; - continue; - } - // 偵測 '}}' - if (ch === '}' && next === '}') { - if (depth > 0) depth--; - buf += '}}'; - i++; - continue; - } - // 只有在不在 {{...}} 內時,逗號才作為分隔符 - if (ch === ',' && depth === 0) { - if (buf.trim()) parts.push(buf.trim()); - buf = ''; - continue; - } - buf += ch; - } - if (buf.trim()) parts.push(buf.trim()); - return parts; - })(); + const fieldList = fields.split(',').map(f => f.trim()).filter(Boolean); const fieldValues: string[] = []; // 收集所有欄位值,並處理別名("原始欄位|別名") fieldList.forEach(fieldEntry => { - // 解析欄位 (fieldKey)、別名 (labelName)、運算式 (calcExpr) - // 格式示例: - // birthday|年齡 {{ Math.floor(...) }} - // birthday {{ Math.floor(...) }} - // birthday|年齡 - // birthday - - let raw = fieldEntry.trim(); - let calcExpr: string | null = null; - // 先取出運算區塊 {{ ... }} - const calcMatch = raw.match(/\{\{(.*?)\}\}/); - if (calcMatch) { - calcExpr = calcMatch[1]; - raw = raw.substring(0, calcMatch.index).trim(); // 去掉運算部分 - } - // 再處理 alias + const raw = fieldEntry.trim(); + // 處理 alias const aliasIdx = raw.lastIndexOf('|'); let fieldKey: string; let labelName: string; @@ -979,32 +923,14 @@ export class GridView extends ItemView { const rawValue = getFrontmatterValue(metadata, fieldKey); if (rawValue !== undefined && rawValue !== '' && rawValue !== null) { - const value = formatFrontmatterValue(rawValue); - - // 如果是數字,則加入千位分隔符號 - // 如果是陣列,則轉換為字串 - - let outputValue: unknown = value; - if (calcExpr) { - try { - const fn = new Function('value', 'metadata', 'app', 'dv', `return (${calcExpr});`) as (value: string, metadata: FrontMatterCache | undefined, app: typeof this.app, dv: unknown) => unknown; - - // 獲取 Dataview API - const dvApi = (this.app as AppWithDataviewPlugin).plugins?.plugins?.dataview?.api; - - outputValue = fn(value, metadata, this.app, dvApi); - } catch (error) { - console.error('GridExplorer: evaluate displayName error', error); - } - } - fieldValues.push(`${labelName}: ${formatFrontmatterValue(outputValue)}`); + fieldValues.push(`${labelName}: ${value}`); } }); // 如果有找到任何欄位值,則組合起來 if (fieldValues.length > 0) { - summaryValue = fieldValues.join('\n'); // 使用 | 分隔不同欄位 + summaryValue = fieldValues.join('\n'); } } } diff --git a/src/main.ts b/src/main.ts index 98377dd..690ee98 100644 --- a/src/main.ts +++ b/src/main.ts @@ -795,6 +795,7 @@ export default class GridExplorerPlugin extends Plugin { async loadSettings() { const loadedSettings = await this.loadData() as Partial | null; this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedSettings ?? {}); + migrateDataviewCodeToQuery(this.settings); updateCustomDocumentExtensions(this.settings); } @@ -821,3 +822,24 @@ export default class GridExplorerPlugin extends Plugin { } } } + +function migrateDataviewCodeToQuery(settings: GallerySettings): void { + if (!settings.customModes) return; + for (const mode of settings.customModes) { + if ('dataviewCode' in mode && !('dataviewQuery' in mode)) { + const oldCode = (mode as unknown as { dataviewCode: string }).dataviewCode; + (mode as Record).dataviewQuery = oldCode; + // 刪除舊欄位 + delete (mode as unknown as Record).dataviewCode; + } + if (mode.options) { + for (const opt of mode.options) { + if ('dataviewCode' in opt && !('dataviewQuery' in opt)) { + const oldCode = (opt as unknown as { dataviewCode: string }).dataviewCode; + (opt as Record).dataviewQuery = oldCode; + delete (opt as unknown as Record).dataviewCode; + } + } + } + } +} diff --git a/src/modal/customModeModal.ts b/src/modal/customModeModal.ts index 640d930..8f42e4c 100644 --- a/src/modal/customModeModal.ts +++ b/src/modal/customModeModal.ts @@ -27,8 +27,8 @@ export class CustomModeModal extends Modal { let name = this.mode?.name || t('default'); // 支援多個子選項,每個選項包含名稱與 Dataview 程式碼 const options: CustomModeOption[] = this.mode?.options ? this.mode.options.map(opt => ({ ...opt })) : []; // 其他選項(不含 Default) - // 向下相容:使用第一個選項作為主要 dataviewCode - let dataviewCode = this.mode ? this.mode.dataviewCode : ''; + // 向下相容:使用第一個選項作為主要 dataviewQuery + let dataviewQuery = this.mode ? this.mode.dataviewQuery : ''; let enabled = this.mode ? (this.mode.enabled ?? true) : true; let fields = this.mode ? this.mode.fields : ''; @@ -63,14 +63,17 @@ export class CustomModeModal extends Modal { .onChange(v => name = v); }); + // 示範查詢 + const dataviewExampleQuery = 'LIST FROM #tag\nTABLE status FROM "folder" SORT file.mtime DESC' + dvSetting.addTextArea(text => { - text.setValue(dataviewCode) + text.setValue(dataviewQuery) .onChange(value => { - dataviewCode = value; + dataviewQuery = value; }) - .setPlaceholder('Dataview js code'); + .setPlaceholder(dataviewExampleQuery); // 給TextArea有足夠的垂直空間和完整的寬度 - text.inputEl.setAttr('rows', 6); + text.inputEl.setAttr('rows', 4); text.inputEl.addClass('ge-custommode-code-input'); }); @@ -195,12 +198,12 @@ export class CustomModeModal extends Modal { }); }) .addTextArea(text => { - text.setPlaceholder('Dataview js code') - .setValue(opt.dataviewCode) + text.setPlaceholder(dataviewExampleQuery) + .setValue(opt.dataviewQuery) .onChange(val => { - opt.dataviewCode = val; + opt.dataviewQuery = val; }); - text.inputEl.setAttr('rows', 6); + text.inputEl.setAttr('rows', 4); text.inputEl.addClass('ge-custommode-code-input'); }) .addText(text => { @@ -314,7 +317,7 @@ export class CustomModeModal extends Modal { .addButton(btn => { btn.setButtonText(t('add_option')) .onClick(() => { - options.push({ name: `${t('option')} ${options.length + 1}`, dataviewCode: '' }); + options.push({ name: `${t('option')} ${options.length + 1}`, dataviewQuery: '' }); renderOptions(); }); }); @@ -335,7 +338,7 @@ export class CustomModeModal extends Modal { icon, displayName, name, - dataviewCode, + dataviewQuery, options: options, enabled, fields diff --git a/src/renderFolder.ts b/src/renderFolder.ts index 80a0a82..49ccd32 100644 --- a/src/renderFolder.ts +++ b/src/renderFolder.ts @@ -177,12 +177,14 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { if (visibleSubfolders.length > 0) { const isCompact = folderStyle === 'compact'; - const foldersContainer = isCompact ? container.createDiv('ge-folders-container') : null; + const foldersContainer = isCompact + ? container.createDiv('ge-folders-container') + : container.createDiv('ge-folders-grid-container'); for (const folder of visibleSubfolders) { - const folderEl = isCompact && foldersContainer - ? foldersContainer.createDiv('ge-folder-button') - : container.createDiv('ge-grid-item ge-folder-item'); + const folderEl = foldersContainer.createDiv( + isCompact ? 'ge-folder-button' : 'ge-grid-item ge-folder-item' + ); if (gridView.showIgnoredItems && isFolderActuallyIgnored(folder)) { folderEl.addClass('ge-folder-ignored'); diff --git a/src/settings.ts b/src/settings.ts index b374836..1f17c51 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -5,7 +5,7 @@ import { t } from './translations'; export interface CustomModeOption { name: string; - dataviewCode: string; + dataviewQuery: string; fields?: string; } @@ -14,7 +14,7 @@ export interface CustomMode { icon: string; displayName: string; name: string; - dataviewCode: string; + dataviewQuery: string; options?: CustomModeOption[]; enabled?: boolean; // 是否顯示此自訂模式,預設為 true fields?: string; @@ -31,9 +31,9 @@ function isCustomMode(value: unknown): value is CustomMode { 'displayName' in value && typeof value.displayName === 'string' && value.displayName.length > 0 && - 'dataviewCode' in value && - typeof value.dataviewCode === 'string' && - value.dataviewCode.length > 0 + 'dataviewQuery' in value && + typeof value.dataviewQuery === 'string' && + value.dataviewQuery.length > 0 ); } @@ -116,7 +116,7 @@ export const DEFAULT_SETTINGS: GallerySettings = { folderDisplayStyle: 'show', // 預設直接顯示資料夾 showMediaFiles: true, // 預設顯示圖片和影片 showVideoThumbnails: true, // 預設顯示影片縮圖 - defaultOpenLocation: 'left', // 預設開啟位置:左側邊欄 + defaultOpenLocation: Platform.isMobile ? 'tab' : 'left', // 行動裝置預設開啟位置:新分頁,非行動裝置預設開啟位置:左側邊欄 reuseExistingLeaf: true, // 預設重用現有的網格視圖 showBookmarksMode: true, // 預設顯示書籤模式 showSearchMode: true, // 預設顯示搜尋結果模式 @@ -149,7 +149,7 @@ export const DEFAULT_SETTINGS: GallerySettings = { icon: '🧩', displayName: 'My Books (Sample)', name: 'Default', - dataviewCode: 'return dv.pages("#Book");', + dataviewQuery: 'LIST FROM #Book', } ], // 自訂模式 quickAccessCommandPath: '', // Path used by "Open quick access folder" command diff --git a/src/translations/en.ts b/src/translations/en.ts index 76c3874..5b8dda7 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -161,7 +161,7 @@ export default { 'custom_mode_display_name': 'Display Name', 'custom_mode_display_name_desc': 'The name displayed in the mode menu', 'custom_mode_dataview_code': 'Dataview Query', - 'custom_mode_dataview_code_desc': 'Enter a Dataview query to get the list of files', + 'custom_mode_dataview_code_desc': 'Enter a Dataview query to get the list of files, e.g., LIST FROM #tag', 'custom_mode_sub_options': 'Custom Mode Sub Options', 'custom_mode_fields_placeholder': 'Enter frontmatter field names, separated by commas (e.g. date,category,status) (optional)', 'dataview_plugin_not_enabled': 'Dataview plugin is not enabled', diff --git a/src/translations/ja.ts b/src/translations/ja.ts index d8ea6d6..96d479f 100644 --- a/src/translations/ja.ts +++ b/src/translations/ja.ts @@ -159,8 +159,8 @@ export default { 'custom_mode': 'カスタムモード', 'custom_mode_display_name': '表示名', 'custom_mode_display_name_desc': 'モードメニューで表示する名前', - 'custom_mode_dataview_code': 'Dataviewjs コード', - 'custom_mode_dataview_code_desc': 'ファイルリストを取得する Dataviewjs コードを入力', + 'custom_mode_dataview_code': 'Dataview クエリ', + 'custom_mode_dataview_code_desc': 'ファイルリストを取得するための Dataview クエリを入力します(例:LIST FROM #tag)', 'custom_mode_sub_options': 'カスタムモード子選択肢', 'custom_mode_fields_placeholder': 'frontmatterのフィールド名をカンマ区切りで入力 (例: date,category,status) (任意)', 'dataview_plugin_not_enabled': 'Dataview プラグインが有効になっていません', diff --git a/src/translations/ko.ts b/src/translations/ko.ts index 0b1f9ea..0f22ee0 100644 --- a/src/translations/ko.ts +++ b/src/translations/ko.ts @@ -161,7 +161,7 @@ export default { 'custom_mode_display_name': '표시 이름', 'custom_mode_display_name_desc': '모드 메뉴에 표시되는 이름', 'custom_mode_dataview_code': 'Dataview 쿼리', - 'custom_mode_dataview_code_desc': 'Dataview 쿼리를 입력하여 파일 목록 가져오기', + 'custom_mode_dataview_code_desc': '파일 목록을 가져올 Dataview 쿼리를 입력하세요 (예: LIST FROM #tag)', 'custom_mode_sub_options': '사용자 정의 모드 하위 옵션', 'custom_mode_fields_placeholder': '프론트매터 필드 이름을 쉼표로 구분하여 입력 (예: date,category,status) (선택사항)', 'dataview_plugin_not_enabled': 'Dataview 플러그인이 활성화되지 않았습니다', diff --git a/src/translations/ru.ts b/src/translations/ru.ts index b338070..c588b33 100644 --- a/src/translations/ru.ts +++ b/src/translations/ru.ts @@ -159,8 +159,8 @@ export default { 'custom_mode': 'Режим отображения', 'custom_mode_display_name': 'Отображаемое имя', 'custom_mode_display_name_desc': 'Отображаемое имя в меню режимов', - 'custom_mode_dataview_code': 'Код Dataview', - 'custom_mode_dataview_code_desc': 'Введите код Dataview для получения списка файлов', + 'custom_mode_dataview_code': 'Запрос Dataview', + 'custom_mode_dataview_code_desc': 'Введите запрос Dataview для получения списка файлов (например, LIST FROM #tag)', 'custom_mode_sub_options': 'Подопции пользовательского режима', 'custom_mode_fields_placeholder': 'Введите названия полей frontmatter через запятую (напр.: date,category,status) (необязательно)', 'dataview_plugin_not_enabled': 'Dataview плагин не включен', diff --git a/src/translations/uk.ts b/src/translations/uk.ts index 5a6c64a..3716cfc 100644 --- a/src/translations/uk.ts +++ b/src/translations/uk.ts @@ -159,8 +159,8 @@ export default { 'custom_mode': 'Користувацький режим', 'custom_mode_display_name': 'Відображувана назва', 'custom_mode_display_name_desc': 'Назва, що відображається в меню режимів', - 'custom_mode_dataview_code': 'Код Dataview', - 'custom_mode_dataview_code_desc': 'Введіть запит Dataview для отримання списку файлів', + 'custom_mode_dataview_code': 'Запит Dataview', + 'custom_mode_dataview_code_desc': 'Введіть запит Dataview для отримання списку файлів (наприклад, LIST FROM #tag)', 'custom_mode_sub_options': 'Опції користувацького режиму', 'custom_mode_fields_placeholder': 'Введіть назви полів frontmatter, розділені комами (напр. date,category,status) (необов\'язково)', 'dataview_plugin_not_enabled': 'Плагін Dataview не увімкнено', diff --git a/src/translations/zh-TW.ts b/src/translations/zh-TW.ts index 5ec495a..4df16a9 100644 --- a/src/translations/zh-TW.ts +++ b/src/translations/zh-TW.ts @@ -160,8 +160,8 @@ export default { 'custom_mode': '自訂模式', 'custom_mode_display_name': '顯示名稱', 'custom_mode_display_name_desc': '在模式選單中顯示的名稱', - 'custom_mode_dataview_code': 'Dataviewjs 代碼', - 'custom_mode_dataview_code_desc': '輸入 Dataviewjs 代碼以取得檔案列表', + 'custom_mode_dataview_code': 'Dataview 查詢', + 'custom_mode_dataview_code_desc': '輸入 Dataview 查詢語法以取得檔案列表,例如:LIST FROM #tag', 'custom_mode_sub_options': '自訂模式子選項', 'custom_mode_fields_placeholder': '輸入 frontmatter 欄位名稱,用逗號分隔 (如: date,category,status) (可選)', 'dataview_plugin_not_enabled': 'Dataview 外掛未啟用', diff --git a/src/translations/zh.ts b/src/translations/zh.ts index 48d4f83..0f15444 100644 --- a/src/translations/zh.ts +++ b/src/translations/zh.ts @@ -159,8 +159,8 @@ export default { 'custom_mode': '自定义模式', 'custom_mode_display_name': '显示名称', 'custom_mode_display_name_desc': '在模式菜单中显示的名称', - 'custom_mode_dataview_code': 'Dataviewjs 代码', - 'custom_mode_dataview_code_desc': '输入 Dataviewjs 代码以获取文件列表', + 'custom_mode_dataview_code': 'Dataview 查询', + 'custom_mode_dataview_code_desc': '输入 Dataview 查询语法以获取文件列表,例如:LIST FROM #tag', 'custom_mode_sub_options': '自定义模式子选项', 'custom_mode_fields_placeholder': '输入 frontmatter 栏位名称,用逗号分隔 (如: date,category,status) (可选)', 'dataview_plugin_not_enabled': 'Dataview 外掛未啟用', diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts index dc76ab3..03cae4d 100644 --- a/src/utils/fileUtils.ts +++ b/src/utils/fileUtils.ts @@ -1,4 +1,4 @@ -import { TFile, TFolder, getFrontMatterInfo, Notice, View, EventRef } from 'obsidian'; +import { App, TFile, TFolder, getFrontMatterInfo, Notice, View, EventRef } from 'obsidian'; import { GridView } from '../GridView'; import { type GallerySettings } from '../settings'; import { t } from '../translations'; @@ -21,6 +21,14 @@ interface DataviewApi { }; page: (path: string) => Record; current?: () => Record; + query: (query: string, originFile?: string) => Promise<{ + successful: boolean; + value: { + type: string; + values: unknown[]; + }; + error?: string; + }>; } interface TasksPlugin { @@ -615,43 +623,44 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean): return []; } - // 依據 GridView 目前選擇的選項決定要使用哪段 Dataview 程式碼 - let dvCode: string | undefined = mode?.dataviewCode; + let dvQuery: string | undefined = mode?.dataviewQuery; if (mode && mode.options && mode.options.length > 0) { const idx = gridView.customOptionIndex ?? -1; if (idx >= 0 && idx < mode.options.length) { - dvCode = mode.options[idx].dataviewCode; + dvQuery = mode.options[idx].dataviewQuery; } } + if (!dvQuery) return []; + try { const activeFile = app.workspace.getActiveFile(); - if (activeFile) { - // 暫時添加 .current 到 dvApi 物件內 - dvApi.current = () => dvApi.page(activeFile.path); - } + const originPath = activeFile?.path ?? ''; - const func = new Function('app', 'dv', dvCode); - const dvPagesResult = func(app, dvApi) as unknown; - const dvPages = Array.isArray(dvPagesResult) - ? (dvPagesResult as unknown[]) - : Array.from((dvPagesResult as Iterable | null | undefined) || []); + const result = await dvApi.query(dvQuery, originPath); - if (!dvPages || dvPages.length === 0) { + if (!result.successful) { + console.error('Grid Explorer: Dataview query failed.', result.error); return []; } const files = new Set(); + const queryResult = result.value; - for (const page of dvPages as Array<{ file?: { path?: string } } | null | undefined>) { - // Add null checks for page and page.file - if (page?.file?.path) { - const file = app.vault.getAbstractFileByPath(page.file.path); - if (file instanceof TFile) { - files.add(file); - } + if (queryResult.type === 'list') { + // LIST 查詢:values 是 Literal[],每個元素是 Link + for (const item of queryResult.values) { + const path = extractPath(item); + if (path) addFileByPath(app, files, path); + } + } else if (queryResult.type === 'table') { + // TABLE 查詢:values 是 Literal[][],第一欄是 file link + for (const row of queryResult.values) { + const path = extractPath((row as unknown[])[0]); + if (path) addFileByPath(app, files, path); } } + return sortFiles(Array.from(files), gridView); } catch (error) { console.error('Grid Explorer: Error executing Dataview query.', error); @@ -661,3 +670,17 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean): return []; } } + +function extractPath(literal: unknown): string | null { + // Dataview 的 Link 物件有 .path 屬性 + if (literal && typeof literal === 'object' && 'path' in literal) { + const path = (literal as Record).path; + if (typeof path === 'string') return path; + } + return null; +} + +function addFileByPath(app: App, files: Set, path: string): void { + const file = app.vault.getAbstractFileByPath(path); + if (file instanceof TFile) files.add(file); +} diff --git a/styles.css b/styles.css index a66b6a2..8d84181 100644 --- a/styles.css +++ b/styles.css @@ -180,8 +180,8 @@ height: 14px; } -.ge-multiline-title { - white-space: normal !important; +.ge-title.ge-multiline-title { + white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; @@ -208,12 +208,11 @@ margin: 0; } -.ge-content-area-p-field, -.ge-content-area .ge-content-area-p-field { +.ge-content-area p.ge-content-area-p-field { display: block; - -webkit-line-clamp: unset !important; - line-clamp: unset !important; - -webkit-box-orient: unset !important; + -webkit-line-clamp: unset; + line-clamp: unset; + -webkit-box-orient: unset; white-space: pre-wrap; overflow: visible; height: auto; @@ -2171,11 +2170,10 @@ a.ge-current-folder:hover { /* --------------------------------------------------------------------------- */ -/* Popup Modal Styles */ -.ge-popup-modal { - background: transparent !important; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15) !important; - border: none !important; +.modal-container .modal.ge-popup-modal { + background: transparent; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + border: none; } .ge-popup-modal-reset { @@ -2333,7 +2331,7 @@ a.ge-current-folder:hover { .is-tablet .ge-note-close-button:not(.clickable-icon), .is-tablet .ge-note-edit-button:not(.clickable-icon), .is-tablet .ge-note-info-button:not(.clickable-icon) { - padding: 6px 12px !important; + padding: 6px 12px; } .ge-note-close-button:hover, @@ -2353,8 +2351,8 @@ a.ge-current-folder:hover { .is-mobile .ge-note-info-button:focus, .is-mobile .ge-note-info-button:focus-visible { outline: none; - background-color: var(--interactive-normal) !important; - transform: none !important; + background-color: var(--interactive-normal); + transform: none; } .ge-note-content-container { @@ -2593,36 +2591,36 @@ a.ge-current-folder:hover { /* --------------------------------------------------------------------------- */ /* 通用輸入框樣式 Universal Input Field Styles */ -.ge-input-field { - width: 100% !important; - padding: 8px 12px !important; - border: 1px solid var(--background-modifier-border) !important; - border-radius: var(--input-radius) !important; - background-color: var(--background-primary) !important; - color: var(--text-normal) !important; - font-size: 14px !important; - font-family: var(--font-interface) !important; - transition: border-color 0.2s ease !important; - box-sizing: border-box !important; +.modal-container .modal input.ge-input-field { + width: 100%; + padding: 8px 12px; + border: 1px solid var(--background-modifier-border); + border-radius: var(--input-radius); + background-color: var(--background-primary); + color: var(--text-normal); + font-size: 14px; + font-family: var(--font-interface); + transition: border-color 0.2s ease; + box-sizing: border-box; } -.ge-input-field:focus { - outline: none !important; +.modal-container .modal input.ge-input-field:focus { + outline: none; } -.ge-input-field::placeholder { - color: var(--text-muted) !important; - opacity: 1 !important; +.modal-container .modal input.ge-input-field::placeholder { + color: var(--text-muted); + opacity: 1; } /* 確保不同 input type 有一致的外觀 */ -.ge-input-field[type="text"], -.ge-input-field[type="url"], -.ge-input-field[type="search"], -.ge-input-field[type="email"] { - -webkit-appearance: none !important; - -moz-appearance: none !important; - appearance: none !important; +.modal-container .modal input.ge-input-field[type="text"], +.modal-container .modal input.ge-input-field[type="url"], +.modal-container .modal input.ge-input-field[type="search"], +.modal-container .modal input.ge-input-field[type="email"] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; } /* 輸入框容器樣式 */ @@ -2725,7 +2723,6 @@ a.ge-current-folder:hover { /* 只有有子目錄的資料夾名稱才顯示懸停效果 */ .ge-explorer-folder-header:not(.ge-no-children) .ge-explorer-folder-name:hover { background-color: var(--background-modifier-hover); - /* color: var(--text-accent); */ } /* 有子目錄的資料夾整個 header 懸停時不顯示效果 */ @@ -3076,9 +3073,9 @@ a.ge-current-folder:hover { /* =========================================================================== */ /* 移除 view-content 的內距,使 zip viewer 滿版 */ -.workspace-leaf .zip-viewer-container { - padding: 0 !important; - overflow: hidden !important; +.workspace-leaf .view-content.zip-viewer-container { + padding: 0; + overflow: hidden; } .zip-viewer-container { @@ -3266,11 +3263,11 @@ a.ge-current-folder:hover { /* 確保網格滾動最底部時不被底部 Home Indicator 遮擋 */ .is-phone .zip-viewer-grid-container { - padding-bottom: calc(var(--view-bottom-spacing) + var(--size-4-3)) !important; + padding-bottom: calc(var(--view-bottom-spacing) + var(--size-4-3)); } .is-tablet .zip-viewer-grid-container { - padding-bottom: calc(var(--safe-area-inset-bottom) + var(--size-4-3)) !important; + padding-bottom: calc(var(--safe-area-inset-bottom) + var(--size-4-3)); } /* =========================================================================== */ @@ -3366,11 +3363,11 @@ a.ge-current-folder:hover { } .is-phone .ge-zip-content-container { - padding-bottom: var(--view-bottom-spacing) !important; + padding-bottom: var(--view-bottom-spacing); } .is-tablet .ge-zip-content-container { - padding-bottom: var(--safe-area-inset-bottom) !important; + padding-bottom: var(--safe-area-inset-bottom); } .ge-zip-sidebar-content-container { @@ -3386,8 +3383,8 @@ a.ge-current-folder:hover { } .is-mobile .ge-zip-view-container::-webkit-scrollbar { - display: none !important; - width: 0 !important; + display: none; + width: 0; } /* =========================================================================== */ @@ -3401,6 +3398,19 @@ a.ge-current-folder:hover { gap: 10px; background: transparent; width: 100%; + margin-top: 3px; + margin-bottom: 6px; +} + +.ge-folders-grid-container { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(var(--grid-item-width, 300px), 1fr)); + gap: 10px; + background: transparent; + width: 100%; + margin-top: 3px; + margin-bottom: 6px; } .ge-folder-button {