This commit is contained in:
Devon22 2025-07-17 00:41:13 +08:00
parent 65a95102af
commit ccd430421e
9 changed files with 311 additions and 189 deletions

View file

@ -1,7 +1,7 @@
{
"id": "gridexplorer",
"name": "GridExplorer",
"version": "2.6.5",
"version": "2.6.6",
"minAppVersion": "1.1.0",
"description": "Browse note files in a grid view.",
"author": "Devon22",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "gridexplorer",
"version": "2.6.5",
"version": "2.6.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gridexplorer",
"version": "2.6.5",
"version": "2.6.6",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",

View file

@ -1,6 +1,6 @@
{
"name": "gridexplorer",
"version": "2.6.5",
"version": "2.6.6",
"description": "Browse note files in a grid view.",
"main": "main.js",
"scripts": {

View file

@ -159,13 +159,17 @@ export class GridView extends ItemView {
this.recentSources.splice(existingIndex, 1);
}
this.recentSources.unshift(key);
const limit = 10;
const limit = 15;
if (this.recentSources.length > limit) {
this.recentSources.length = limit;
}
}
async setSource(mode: string, path = '', resetScroll = false, recordHistory = true) {
// 如果新的狀態與當前狀態相同,則不進行任何操作
if (this.sourceMode === mode && this.sourcePath === path) {
return;
}
// 記錄之前的狀態到歷史記錄中(如果有)
if (this.sourceMode && recordHistory) {
@ -252,6 +256,127 @@ export class GridView extends ItemView {
}
});
// 添加回上一步按鈕
const backButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('back') } });
setIcon(backButton, 'arrow-left');
backButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
// 如果有歷史記錄
if (this.recentSources.length > 0) {
// 取得最近一筆歷史記錄
const lastSource = JSON.parse(this.recentSources[0]);
this.recentSources.shift(); // 從歷史記錄中移除
// 設定來源(不記錄到歷史)
this.setSource(
lastSource.mode,
lastSource.path || '',
true, // 重設捲動位置
false // 不記錄到歷史
);
}
});
// 添加右鍵選單支援
backButton.addEventListener('contextmenu', (event) => {
// 只有在有歷史記錄時才顯示右鍵選單
if (this.recentSources.length > 0) {
event.preventDefault();
const menu = new Menu();
// 添加歷史記錄
this.recentSources.forEach((sourceInfoStr, index) => {
try {
const sourceInfo = JSON.parse(sourceInfoStr);
const { mode, path } = sourceInfo;
// 根據模式顯示圖示和文字
let displayText = '';
let icon = '';
switch (mode) {
case 'folder':
displayText = path || '/';
icon = 'folder';
break;
case 'bookmarks':
displayText = t('bookmarks_mode');
icon = 'bookmark';
break;
case 'search':
displayText = t('search_results');
icon = 'search';
break;
case 'backlinks':
displayText = t('backlinks_mode');
icon = 'links-coming-in';
break;
case 'outgoinglinks':
displayText = t('outgoinglinks_mode');
icon = 'links-going-out';
break;
case 'all-files':
displayText = t('all_files_mode');
icon = 'book-text';
break;
case 'recent-files':
displayText = t('recent_files_mode');
icon = 'calendar-days';
break;
case 'random-note':
displayText = t('random_note_mode');
icon = 'dice';
break;
case 'tasks':
displayText = t('tasks_mode');
icon = 'square-check-big';
break;
default:
if (mode.startsWith('custom-')) {
const customMode = this.plugin.settings.customModes.find(m => m.internalName === mode);
displayText = customMode ? customMode.displayName : t('custom_mode');
icon = 'puzzle';
} else {
displayText = mode;
icon = 'grid';
}
}
// 添加歷史記錄到選單
menu.addItem((item) => {
item
.setTitle(`${displayText}`)
.setIcon(`${icon}`)
.onClick(() => {
// 找出當前點擊的紀錄索引
const clickedIndex = this.recentSources.findIndex(source => {
const parsed = JSON.parse(source);
return parsed.mode === mode && parsed.path === path;
});
// 如果找到點擊的紀錄,清除它之上的紀錄
if (clickedIndex !== -1) {
this.recentSources = this.recentSources.slice(clickedIndex + 1);
}
this.setSource(mode, path, true, false);
});
});
} catch (error) {
console.error('Failed to parse source info:', error);
}
});
// 顯示歷史選單
menu.showAtMouseEvent(event);
}
});
// 添加新增筆記按鈕
const newNoteButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('new_note') } });
setIcon(newNoteButton, 'square-pen');
@ -346,104 +471,6 @@ export class GridView extends ItemView {
});
setIcon(reselectButton, "grid");
// 添加右鍵選單支援
reselectButton.addEventListener('contextmenu', (event) => {
// 只有在有歷史記錄時才顯示右鍵選單
if (this.recentSources.length > 0) {
event.preventDefault();
const menu = new Menu();
// 添加歷史記錄
this.recentSources.forEach((sourceInfoStr, index) => {
try {
const sourceInfo = JSON.parse(sourceInfoStr);
const { mode, path } = sourceInfo;
// 根據模式顯示圖示和文字
let displayText = '';
let icon = '';
switch (mode) {
case 'folder':
displayText = path || '/';
icon = 'folder';
break;
case 'bookmarks':
displayText = t('bookmarks_mode');
icon = 'bookmark';
break;
case 'search':
displayText = t('search_results');
icon = 'search';
break;
case 'backlinks':
displayText = t('backlinks_mode');
icon = 'links-coming-in';
break;
case 'outgoinglinks':
displayText = t('outgoinglinks_mode');
icon = 'links-going-out';
break;
case 'all-files':
displayText = t('all_files_mode');
icon = 'book-text';
break;
case 'recent-files':
displayText = t('recent_files_mode');
icon = 'calendar-days';
break;
case 'random-note':
displayText = t('random_note_mode');
icon = 'dice';
break;
case 'tasks':
displayText = t('tasks_mode');
icon = 'square-check-big';
break;
default:
if (mode.startsWith('custom-')) {
const customMode = this.plugin.settings.customModes.find(m => m.internalName === mode);
displayText = customMode ? customMode.displayName : t('custom_mode');
icon = 'puzzle';
} else {
displayText = mode;
icon = 'grid';
}
}
// 添加歷史記錄到選單
menu.addItem((item) => {
item
.setTitle(`${displayText}`)
.setIcon(`${icon}`)
.onClick(() => {
// 找出當前點擊的紀錄索引
const clickedIndex = this.recentSources.findIndex(source => {
const parsed = JSON.parse(source);
return parsed.mode === mode && parsed.path === path;
});
// 如果找到點擊的紀錄,清除它之上的紀錄
if (clickedIndex !== -1) {
this.recentSources = this.recentSources.slice(clickedIndex + 1);
}
this.setSource(mode, path, true, false);
});
});
} catch (error) {
console.error('Failed to parse source info:', error);
}
});
// 顯示歷史選單
menu.showAtMouseEvent(event);
}
});
// 添加重新整理按鈕
const refreshButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('refresh') } });
refreshButton.addEventListener('click', () => {
@ -454,42 +481,7 @@ export class GridView extends ItemView {
});
setIcon(refreshButton, 'refresh-ccw');
// 添加排序按鈕
if (this.sourceMode !== 'bookmarks' &&
this.sourceMode !== 'recent-files' &&
this.sourceMode !== 'random-note') {
const sortButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('sorting') } });
sortButton.addEventListener('click', (evt) => {
const menu = new Menu();
const sortOptions = [
{ value: 'name-asc', label: t('sort_name_asc'), icon: 'a-arrow-up' },
{ value: 'name-desc', label: t('sort_name_desc'), icon: 'a-arrow-down' },
{ value: 'mtime-desc', label: t('sort_mtime_desc'), icon: 'clock' },
{ value: 'mtime-asc', label: t('sort_mtime_asc'), icon: 'clock' },
{ value: 'ctime-desc', label: t('sort_ctime_desc'), icon: 'calendar' },
{ value: 'ctime-asc', label: t('sort_ctime_asc'), icon: 'calendar' },
{ value: 'random', label: t('sort_random'), icon: 'dice' },
];
sortOptions.forEach(option => {
menu.addItem((item) => {
item
.setTitle(option.label)
.setIcon(option.icon)
.setChecked((this.folderSortType || this.sortType) === option.value)
.onClick(() => {
this.sortType = option.value;
this.folderSortType = '';
this.render();
// 通知 Obsidian 保存視圖狀態
this.app.workspace.requestSaveLayout();
});
});
});
menu.showAtMouseEvent(evt);
});
setIcon(sortButton, 'arrow-up-narrow-wide');
}
// 排序按鈕已移動到模式名稱區域
// 添加搜尋按鈕
const searchButtonContainer = headerButtonsDiv.createDiv('ge-search-button-container');
@ -647,8 +639,60 @@ export class GridView extends ItemView {
});
}
// 創建目前模式名稱的容器
let modenameContainer = this.containerEl.createDiv('ge-modename-content');
// 創建模式名稱和排序按鈕的容器
const modeHeaderContainer = this.containerEl.createDiv('ge-mode-header-container');
// 左側:模式名稱
const modenameContainer = modeHeaderContainer.createDiv('ge-modename-content');
// 右側:排序按鈕
const rightActions = modeHeaderContainer.createDiv('ge-right-actions');
// 添加排序按鈕
if (this.sourceMode !== 'bookmarks' &&
this.sourceMode !== 'recent-files' &&
this.sourceMode !== 'random-note') {
const sortButton = rightActions.createEl('a', {
cls: 'ge-sort-button',
attr: {
'aria-label': t('sorting'),
'href': '#'
}
});
setIcon(sortButton, 'arrow-up-narrow-wide');
sortButton.addEventListener('click', (evt) => {
evt.preventDefault();
evt.stopPropagation();
const menu = new Menu();
const sortOptions = [
{ value: 'name-asc', label: t('sort_name_asc'), icon: 'a-arrow-up' },
{ value: 'name-desc', label: t('sort_name_desc'), icon: 'a-arrow-down' },
{ value: 'mtime-desc', label: t('sort_mtime_desc'), icon: 'clock' },
{ value: 'mtime-asc', label: t('sort_mtime_asc'), icon: 'clock' },
{ value: 'ctime-desc', label: t('sort_ctime_desc'), icon: 'calendar' },
{ value: 'ctime-asc', label: t('sort_ctime_asc'), icon: 'calendar' },
{ value: 'random', label: t('sort_random'), icon: 'dice' },
];
sortOptions.forEach(option => {
menu.addItem((item) => {
item
.setTitle(option.label)
.setIcon(option.icon)
.setChecked((this.folderSortType || this.sortType) === option.value)
.onClick(() => {
this.sortType = option.value;
this.folderSortType = '';
this.render();
// 通知 Obsidian 保存視圖狀態
this.app.workspace.requestSaveLayout();
});
});
});
menu.showAtMouseEvent(evt);
});
}
// 為區域添加點擊事件,點擊後網格容器捲動到最頂部
modenameContainer.addEventListener('click', (event: MouseEvent) => {
@ -1208,10 +1252,10 @@ export class GridView extends ItemView {
// 隱藏頂部元素
const displayValue = this.hideHeaderElements ? 'none' : 'flex';
const headerButtons = this.containerEl.querySelector('.ge-header-buttons') as HTMLElement;
const modenameContainer = this.containerEl.querySelector('.ge-modename-content') as HTMLElement;
const modeHeaderContainer = this.containerEl.querySelector('.ge-mode-header-container') as HTMLElement;
if (headerButtons) headerButtons.style.display = displayValue;
if (modenameContainer) modenameContainer.style.display = displayValue;
if (modeHeaderContainer) modeHeaderContainer.style.display = displayValue;
// 根據設定決定是否啟用卡片模式
if (this.cardLayout === 'vertical') {
@ -2374,15 +2418,22 @@ export class GridView extends ItemView {
if (file.extension === 'md') {
const metadata = this.app.metadataCache.getFileCache(file);
if (metadata?.frontmatter) {
const fieldName = isModifiedTime
const fieldSetting = isModifiedTime
? this.plugin.settings.modifiedDateField
: this.plugin.settings.createdDateField;
if (fieldName && metadata.frontmatter[fieldName]) {
const fieldNames = fieldSetting
? fieldSetting.split(',').map(f => f.trim()).filter(Boolean)
: [];
for (const fieldName of fieldNames) {
const dateStr = metadata.frontmatter[fieldName];
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
frontMatterDate = date;
if (dateStr) {
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
frontMatterDate = date;
break; // 已找到有效日期
}
}
}
}

View file

@ -110,12 +110,17 @@ export function sortFiles(files: TFile[], gridView: GridView): TFile[] {
file,
mDate: (() => {
if (metadata?.frontmatter) {
const fieldName = settings.modifiedDateField;
const dateStr = metadata.frontmatter[fieldName];
if (dateStr) {
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
return date.getTime();
// 支援多個欄位名稱(用逗號隔開)
const fieldNames = settings.modifiedDateField
? settings.modifiedDateField.split(',').map(f => f.trim()).filter(Boolean)
: [];
for (const fieldName of fieldNames) {
const dateStr = metadata.frontmatter[fieldName];
if (dateStr) {
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
return date.getTime();
}
}
}
}
@ -123,12 +128,17 @@ export function sortFiles(files: TFile[], gridView: GridView): TFile[] {
})(),
cDate: (() => {
if (metadata?.frontmatter) {
const fieldName = settings.createdDateField;
const dateStr = metadata.frontmatter[fieldName];
if (dateStr) {
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
return date.getTime();
// 支援多個欄位名稱(用逗號隔開)
const fieldNames = settings.createdDateField
? settings.createdDateField.split(',').map(f => f.trim()).filter(Boolean)
: [];
for (const fieldName of fieldNames) {
const dateStr = metadata.frontmatter[fieldName];
if (dateStr) {
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
return date.getTime();
}
}
}
}
@ -534,7 +544,8 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean):
const files = new Set<TFile>();
for (const page of dvPages) {
if (page.file?.path) {
// 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);

View file

@ -98,16 +98,10 @@ export class CustomModeModal extends Modal {
const renderOptions = () => {
optionsContainer.empty();
options.forEach((opt, idx) => {
const optSetting = new Setting(optionsContainer);
// 使標題與描述佔據一行,輸入區域佔據了下一行
optSetting.settingEl.style.flexDirection = 'column';
optSetting.settingEl.style.alignItems = 'stretch';
optSetting.settingEl.style.gap = '0.5rem';
// 讓 Text 與 TextArea 在 control 區域各佔一行
optSetting.controlEl.style.display = 'flex';
optSetting.controlEl.style.flexDirection = 'column';
optSetting.controlEl.style.alignItems = 'stretch';
optSetting.controlEl.style.gap = '0.5rem';
// 為每個選項創建容器
const optionContainer = optionsContainer.createDiv('ge-custommode-option-container');
const optSetting = new Setting(optionContainer);
optSetting
.addText(text => {
text.setPlaceholder(t('option_name'))

View file

@ -101,9 +101,9 @@ export const TRANSLATIONS: Translations = {
'note_summary_field': '筆記摘要欄位名稱',
'note_summary_field_desc': '指定 frontmatter 中用於筆記摘要的欄位名稱',
'modified_date_field': '"修改時間"欄位名稱',
'modified_date_field_desc': '指定 frontmatter 中用於筆記修改時間的欄位名稱',
'modified_date_field_desc': '指定 frontmatter 中用於筆記修改時間的欄位名稱 (支援多個欄位名稱,用逗號分隔)',
'created_date_field': '"建立時間"欄位名稱',
'created_date_field_desc': '指定 frontmatter 中用於筆記建立時間的欄位名稱',
'created_date_field_desc': '指定 frontmatter 中用於筆記建立時間的欄位名稱 (支援多個欄位名稱,用逗號分隔)',
'grid_item_width': '網格項目寬度',
'grid_item_width_desc': '設定網格項目的寬度',
'grid_item_height': '網格項目高度',
@ -365,9 +365,9 @@ export const TRANSLATIONS: Translations = {
'note_summary_field': 'Note summary field name',
'note_summary_field_desc': 'Set the field name in frontmatter to use for the note summary',
'modified_date_field': '"Modified date" field name',
'modified_date_field_desc': 'Set the field name in frontmatter to use for the modified date',
'modified_date_field_desc': 'Set the field name in frontmatter to use for the modified date (support multiple field names, separated by commas)',
'created_date_field': '"Created date" field name',
'created_date_field_desc': 'Set the field name in frontmatter to use for the created date',
'created_date_field_desc': 'Set the field name in frontmatter to use for the created date (support multiple field names, separated by commas)',
'grid_item_width': 'Grid item width',
'grid_item_width_desc': 'Set the width of grid items',
'grid_item_height': 'Grid item height',
@ -629,9 +629,9 @@ export const TRANSLATIONS: Translations = {
'note_summary_field': '笔记摘要字段名称',
'note_summary_field_desc': '设置 frontmatter 中用于笔记摘要的字段名称',
'modified_date_field': '"修改时间"字段名称',
'modified_date_field_desc': '设置 frontmatter 中用于笔记修改时间的字段名称',
'modified_date_field_desc': '设置 frontmatter 中用于笔记修改时间的字段名称 (支持多个字段名称,用逗号分隔)',
'created_date_field': '"创建时间"字段名称',
'created_date_field_desc': '设置 frontmatter 中用于笔记创建时间的字段名称',
'created_date_field_desc': '设置 frontmatter 中用于笔记创建时间的字段名称 (支持多个字段名称,用逗号分隔)',
'grid_item_width': '网格项目宽度',
'grid_item_width_desc': '设置网格项目的宽度',
'grid_item_height': '网格项目高度',
@ -893,9 +893,9 @@ export const TRANSLATIONS: Translations = {
'note_summary_field': 'ノート要約フィールド名',
'note_summary_field_desc': 'frontmatterでート要約として使用するフィールド名を設定',
'modified_date_field': '"更新日"フィールド名',
'modified_date_field_desc': 'frontmatterで更新日として使用するフィールド名を設定',
'modified_date_field_desc': 'frontmatterで更新日として使用するフィールド名を設定 (複数のフィールド名をカンマ区切りで指定可能)',
'created_date_field': '"作成日"フィールド名',
'created_date_field_desc': 'frontmatterで作成日として使用するフィールド名を設定',
'created_date_field_desc': 'frontmatterで作成日として使用するフィールド名を設定 (複数のフィールド名をカンマ区切りで指定可能)',
'grid_item_width': 'グリッドアイテムの幅',
'grid_item_width_desc': 'グリッドアイテムの幅を設定',
'grid_item_height': 'グリッドアイテムの高さ',
@ -1158,9 +1158,9 @@ export const TRANSLATIONS: Translations = {
'note_summary_field': 'Имя поля "Краткое описание"',
'note_summary_field_desc': 'Укажите имя поля в метаданных для использования в качестве краткого описания',
'modified_date_field': 'Имя поля "Дата изменения"',
'modified_date_field_desc': 'Укажите имя поля в метаданных для использования в качестве даты изменения',
'modified_date_field_desc': 'Укажите имя поля в метаданных для использования в качестве даты изменения (поддерживаются несколько полей, разделенных запятыми)',
'created_date_field': 'Имя поля "Дата создания"',
'created_date_field_desc': 'Укажите имя поля в метаданных для использования в качестве даты создания',
'created_date_field_desc': 'Укажите имя поля в метаданных для использования в качестве даты создания (поддерживаются несколько полей, разделенных запятыми)',
'grid_item_width': 'Ширина элемента сетки',
'grid_item_width_desc': 'Установите ширину элементов сетки',
'grid_item_height': 'Высота элемента сетки',
@ -1422,9 +1422,9 @@ export const TRANSLATIONS: Translations = {
'note_summary_field': 'Назва поля "Краткий опис"',
'note_summary_field_desc': 'Вкажіть назву поля в метаданих для використання як краткого опису',
'modified_date_field': 'Назва поля "Дата зміни"',
'modified_date_field_desc': 'Вкажіть назву поля в метаданих для використання як дати зміни',
'modified_date_field_desc': 'Вкажіть назву поля в метаданих для використання як дати зміни (підтримується кілька назв полів, розділених комами)',
'created_date_field': 'Назва поля "Дата створення"',
'created_date_field_desc': 'Вкажіть назву поля в метаданих для використання як дати створення',
'created_field_desc': 'Вкажіть назву поля в метаданих для використання як дати створення (підтримується кілька назв полів, розділених комами)',
'grid_item_width': 'Ширина елемента сітки',
'grid_item_width_desc': 'Встановіть ширину елементів сітки',
'grid_item_height': 'Висота елемента сітки',

View file

@ -355,6 +355,11 @@
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* 直向卡片模式下的資料夾項目補正 */
.ge-vertical-card .ge-folder-item .ge-content-area {
padding: 7px !important;
}
.ge-grid-item.ge-folder-item .ge-icon-container {
color: var(--interactive-accent);
}
@ -586,18 +591,56 @@
/* --------------------------------------------------------------------------- */
/* Mode名稱區域樣式 Mode Name Content */
.ge-modename-content {
justify-content: flex-start;
padding: 7px 12px;
/* 模式名稱容器 */
.ge-mode-header-container {
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--background-secondary);
border-bottom: 1px solid var(--background-modifier-border);
}
/* 模式名稱區域 */
.ge-modename-content {
flex: 1;
padding: 7px 12px;
font-size: var(--font-ui-medium);
display: flex;
align-items: center;
overflow: hidden;
}
/* 右側操作按鈕區域 */
.ge-right-actions {
display: flex;
align-items: center;
padding-right: 8px;
}
/* 排序按鈕樣式 */
.ge-sort-button {
background: none;
border: none;
color: var(--text-muted);
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.2s ease;
}
.ge-sort-button:hover {
opacity: 1;
background-color: var(--background-modifier-hover);
}
.ge-sort-button svg {
width: 16px;
height: 16px;
}
.ge-mode-title {
text-decoration: none;
padding: 3px 8px;
@ -695,6 +738,7 @@ a.ge-mode-title:hover {
}
a.ge-current-folder {
text-decoration: none;
padding: 3px 8px !important;
border: 1px solid var(--background-modifier-hover);
}
@ -1381,3 +1425,25 @@ a.ge-current-folder:hover {
text-overflow: ellipsis;
}
/* Custom Mode Options */
.ge-custommode-option-container {
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
padding: 0 12px;
margin-bottom: 12px;
background-color: var(--background-secondary);
}
.ge-custommode-option-container .setting-item {
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
width: 100%;
}
.ge-custommode-option-container .setting-item-control {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}

View file

@ -1,3 +1,3 @@
{
"2.6.5": "1.1.0"
"2.6.6": "1.1.0"
}