This commit is contained in:
Devon22 2026-06-10 23:34:07 +08:00
parent 7cfda20845
commit 9bf25cabaa
22 changed files with 180 additions and 649 deletions

View file

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

View file

@ -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.

View file

@ -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 にアクセスできます
---
この機能を使用すると、複雑なプラグインコードを書かずに、ノートデータを柔軟に表示・計算できます。

View file

@ -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
---
這個功能讓您能夠更靈活地呈現和計算筆記中的數據,無需編寫複雜的插件代碼即可實現強大的顯示效果。

View file

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

4
package-lock.json generated
View file

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

View file

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

View file

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

View file

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

View file

@ -795,6 +795,7 @@ export default class GridExplorerPlugin extends Plugin {
async loadSettings() {
const loadedSettings = await this.loadData() as Partial<GallerySettings> | 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<string, unknown>).dataviewQuery = oldCode;
// 刪除舊欄位
delete (mode as unknown as Record<string, unknown>).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<string, unknown>).dataviewQuery = oldCode;
delete (opt as unknown as Record<string, unknown>).dataviewCode;
}
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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 プラグインが有効になっていません',

View file

@ -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 플러그인이 활성화되지 않았습니다',

View file

@ -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 плагин не включен',

View file

@ -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 не увімкнено',

View file

@ -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 外掛未啟用',

View file

@ -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 外掛未啟用',

View file

@ -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<string, unknown>;
current?: () => Record<string, unknown>;
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<unknown> | 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<TFile>();
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<string, unknown>).path;
if (typeof path === 'string') return path;
}
return null;
}
function addFileByPath(app: App, files: Set<TFile>, path: string): void {
const file = app.vault.getAbstractFileByPath(path);
if (file instanceof TFile) files.add(file);
}

View file

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