This commit is contained in:
Devon22 2025-08-05 23:39:53 +08:00
parent 05dc7122e3
commit b46998f5d0
18 changed files with 702 additions and 344 deletions

View file

@ -1,7 +1,7 @@
{
"id": "gridexplorer",
"name": "GridExplorer",
"version": "2.8.1",
"version": "2.8.2",
"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.8.1",
"version": "2.8.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gridexplorer",
"version": "2.8.1",
"version": "2.8.2",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",

View file

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

View file

@ -190,20 +190,27 @@ export class GridView extends ItemView {
const limit = 15;
if (this.recentSources.length > limit) {
this.recentSources.length = limit;
}
}
}
// resetScroll 為 true 時,會將捲動位置重置到最頂部
// recordHistory 為 false 時,不會將當前狀態加入歷史記錄
async setSource(mode: string, path = '', resetScroll = false, recordHistory = true) {
// 設定來源模式
async setSource(
mode: string,
path = '',
recordHistory = true, // 是否將當前狀態加入歷史記錄
searchQuery?: string,
searchAllFiles?: boolean,
searchFilesNameOnly?: boolean,
searchMediaFiles?: boolean
) {
// 如果新的狀態與當前狀態相同,則不進行任何操作
if (this.sourceMode === mode &&
if (this.sourceMode === mode &&
this.sourcePath === path &&
this.searchQuery === this.searchQuery &&
this.searchAllFiles === this.searchAllFiles &&
this.searchFilesNameOnly === this.searchFilesNameOnly &&
this.searchMediaFiles === this.searchMediaFiles) {
this.searchQuery === searchQuery &&
this.searchAllFiles === searchAllFiles &&
this.searchFilesNameOnly === searchFilesNameOnly &&
this.searchMediaFiles === searchMediaFiles) {
return;
}
@ -223,13 +230,13 @@ export class GridView extends ItemView {
if (this.searchQuery !== '' && this.searchAllFiles) {
this.searchQuery = '';
}
// 更新來源模式和路徑
if(mode !== '') this.sourceMode = mode;
if(path !== '') this.sourcePath = path;
if(this.sourceMode === '') this.sourceMode = 'folder';
if(this.sourcePath === '') this.sourcePath = '/';
if (mode !== '') this.sourceMode = mode;
if (path !== '') this.sourcePath = path;
if (this.sourceMode === '') this.sourceMode = 'folder';
if (this.sourcePath === '') this.sourcePath = '/';
// 讀取Folder設定
this.folderSortType = '';
this.pinnedList = [];
@ -253,22 +260,34 @@ export class GridView extends ItemView {
this.sourcePath = '/'; // 強制設定路徑為根目錄 (創建筆記用)
// 切換到自訂模式時重設選項索引並重設排序方式
if(this.sourceMode.startsWith('custom-')) {
if (this.sourceMode.startsWith('custom-')) {
this.customOptionIndex = -1;
this.folderSortType = 'none';
}
}
// 設定搜尋相關狀態
if (searchQuery !== undefined) {
this.searchQuery = searchQuery;
}
if (searchAllFiles !== undefined) {
this.searchAllFiles = searchAllFiles;
}
if (searchFilesNameOnly !== undefined) {
this.searchFilesNameOnly = searchFilesNameOnly;
}
if (searchMediaFiles !== undefined) {
this.searchMediaFiles = searchMediaFiles;
}
// 通知 Obsidian 保存視圖狀態
this.app.workspace.requestSaveLayout();
await this.render(resetScroll);
await this.render();
}
async render(resetScroll = false) {
// 儲存當前捲動位置
const scrollContainer = this.containerEl.children[1] as HTMLElement;
const scrollTop = resetScroll ? 0 : (scrollContainer ? scrollContainer.scrollTop : 0);
// 渲染網格
async render() {
// 保存選中項目的檔案路徑(如果有)
let selectedFilePath: string | null = null;
@ -282,7 +301,7 @@ export class GridView extends ItemView {
// 添加頂部按鈕
renderHeaderButton(this);
// 顯示路徑 / 模式名稱
renderModePath(this);
@ -317,14 +336,10 @@ export class GridView extends ItemView {
}
};
// 重新渲染內容
// 渲染網格內容
await this.grid_render();
(this.leaf as any).updateHeader();
// 恢復捲動位置
if (scrollContainer && !resetScroll) {
contentEl.scrollTop = scrollTop;
}
// 如果有之前選中的檔案路徑,嘗試恢復選中狀態
if (selectedFilePath && this.hasKeyboardFocus) {
@ -333,8 +348,11 @@ export class GridView extends ItemView {
this.selectItem(newIndex);
}
}
// new Notice('GridExplorer: ' + this.sourceMode + ' ' + this.sourcePath);
}
// 渲染網格內容
async grid_render() {
const container = this.containerEl.querySelector('.view-content') as HTMLElement;
container.empty();
@ -344,7 +362,7 @@ export class GridView extends ItemView {
const displayValue = this.hideHeaderElements ? 'none' : 'flex';
const headerButtons = this.containerEl.querySelector('.ge-header-buttons') as HTMLElement;
const modeHeaderContainer = this.containerEl.querySelector('.ge-mode-header-container') as HTMLElement;
if (headerButtons) headerButtons.style.display = displayValue;
if (modeHeaderContainer) modeHeaderContainer.style.display = displayValue;
@ -417,7 +435,7 @@ export class GridView extends ItemView {
} else if (modeClasses.includes(this.sourceMode)) {
this.containerEl.addClass(`ge-mode-${this.sourceMode}`);
}
// 重置網格項目數組
this.gridItems = [];
@ -439,7 +457,7 @@ export class GridView extends ItemView {
// 顯示資料夾
renderFolder(this, container);
// 顯示檔案
let files = await renderFiles(this, container);
@ -465,7 +483,7 @@ export class GridView extends ItemView {
const otherFiles = files.filter(f => !this.pinnedList.includes(f.name));
files = [...pinnedFiles, ...otherFiles];
}
// 如果資料夾筆記設定為隱藏,則隱藏資料夾筆記
if (this.sourceMode === 'folder' && this.sourcePath !== '/') {
if (this.plugin.settings.folderNoteDisplaySettings === 'hidden') {
@ -481,7 +499,7 @@ export class GridView extends ItemView {
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(async entry => {
if (entry.isIntersecting) {
const fileEl = entry.target as HTMLElement;
const fileEl = entry.target as HTMLElement;
const filePath = fileEl.dataset.filePath;
if (!filePath) return;
@ -525,13 +543,13 @@ export class GridView extends ItemView {
const option = mode.options[this.customOptionIndex];
fields = option.fields || '';
}
// 如果 fields 不為空,則使用它來顯示摘要
if (fields) {
// 將 fields 以逗號分隔成陣列,並過濾掉空值
const fieldList = fields.split(',').map(f => f.trim()).filter(Boolean);
const fieldValues: string[] = [];
// 收集所有欄位值,並處理別名("原始欄位|別名"
fieldList.forEach(fieldEntry => {
// 解析欄位 (fieldKey)、別名 (labelName)、運算式 (calcExpr)
@ -540,7 +558,7 @@ export class GridView extends ItemView {
// birthday {{ Math.floor(...) }}
// birthday|年齡
// birthday
let raw = fieldEntry.trim();
let calcExpr: string | null = null;
// 先取出運算區塊 {{ ... }}
@ -560,7 +578,7 @@ export class GridView extends ItemView {
fieldKey = raw;
labelName = fieldKey;
}
if (metadata?.[fieldKey] !== undefined && metadata?.[fieldKey] !== '' && metadata?.[fieldKey] !== null) {
// 如果是數字,則加入千位分隔符號
if (typeof metadata[fieldKey] === 'number') {
@ -586,13 +604,13 @@ export class GridView extends ItemView {
fieldValues.push(`${labelName}: ${outputValue}`);
}
});
// 如果有找到任何欄位值,則組合起來
if (fieldValues.length > 0) {
summaryValue = fieldValues.join('\n'); // 使用 | 分隔不同欄位
}
}
}
}
}
if (summaryValue) {
if (!this.sourceMode.startsWith('custom-')) {
@ -600,7 +618,7 @@ export class GridView extends ItemView {
pEl = contentArea.createEl('p', { text: summaryValue.trim() });
} else {
// custom mode 有設定顯示欄位值
pEl = contentArea.createEl('p', { text: summaryValue.trim() , cls: 'ge-content-area-p-field' });
pEl = contentArea.createEl('p', { text: summaryValue.trim(), cls: 'ge-content-area-p-field' });
}
} else {
// Frontmatter 沒有設定摘要值,則使用內文
@ -619,7 +637,7 @@ export class GridView extends ItemView {
// 刪除 code block
contentWithoutMediaLinks = contentWithoutFrontmatter
.replace(/```[\s\S]*?```\n/g, '')
.replace(/```[\s\S]*$/,'');
.replace(/```[\s\S]*$/, '');
}
// 刪除註解及連結
@ -628,29 +646,29 @@ export class GridView extends ItemView {
.replace(/!?\[([^\]]*)\]\([^)]+\)|!?\[\[([^\]]+)\]\]/g, (match, p1, p2) => {
const linkText = p1 || p2 || '';
if (!linkText) return '';
// 獲取副檔名並檢查是否為圖片或影片
const extension = linkText.split('.').pop()?.toLowerCase() || '';
return (IMAGE_EXTENSIONS.has(extension) || VIDEO_EXTENSIONS.has(extension)) ? '' : linkText;
});
});
//把開頭的標題整行刪除
if (contentWithoutMediaLinks.startsWith('# ') || contentWithoutMediaLinks.startsWith('## ') || contentWithoutMediaLinks.startsWith('### ')) {
contentWithoutMediaLinks = contentWithoutMediaLinks.split('\n').slice(1).join('\n');
}
if (!this.plugin.settings.showCodeBlocksInSummary) {
// 不刪除code block的情況下包含這些特殊符號
contentWithoutMediaLinks = contentWithoutMediaLinks.replace(/[>|\-#*]/g,'').trim();
contentWithoutMediaLinks = contentWithoutMediaLinks.replace(/[>|\-#*]/g, '').trim();
}
// 只取前 summaryLength 個字符作為預覽
const preview = contentWithoutMediaLinks.slice(0, summaryLength) + (contentWithoutMediaLinks.length > summaryLength ? '...' : '');
// 創建預覽內容
pEl = contentArea.createEl('p', { text: preview.trim() });
}
}
}
//將預覽文字設定到標題的 title 屬性中
const titleEl = fileEl.querySelector('.ge-title');
@ -663,7 +681,7 @@ export class GridView extends ItemView {
if (colorValue) {
// 使用 CSS 類別來設置顏色
fileEl.addClass(`ge-note-color-${colorValue}`);
// 設置預覽內容文字顏色
if (pEl) {
pEl.addClass(`ge-note-color-${colorValue}-text`);
@ -676,7 +694,7 @@ export class GridView extends ItemView {
if (titleEl) {
titleEl.textContent = titleValue;
}
}
}
const displayValue = metadata?.display;
if (displayValue === 'minimized') {
@ -709,9 +727,9 @@ export class GridView extends ItemView {
contentArea.createEl('p', { text: file.extension.toUpperCase() });
}
setTooltip(fileEl as HTMLElement, `${file.name}`,{ delay:2000 })
setTooltip(fileEl as HTMLElement, `${file.name}`, { delay: 2000 })
}
// 顯示標籤(僅限 Markdown 檔案)
if (file.extension === 'md' && this.showNoteTags && !this.minMode) {
const fileCache = this.app.metadataCache.getFileCache(file);
@ -721,10 +739,10 @@ export class GridView extends ItemView {
if (displaySetting !== 'minimized') {
const allTags = new Set<string>();
// 從 frontmatter 獲取標籤
let frontmatterTags = fileCache?.frontmatter?.tags || [];
// 處理不同的標籤格式
if (typeof frontmatterTags === 'string') {
// 如果是字符串,按逗號或空格分割
@ -745,27 +763,27 @@ export class GridView extends ItemView {
}
});
}
// 從檔案 cache 中獲取內文標籤
const cacheTags = fileCache?.tags || [];
cacheTags.forEach(tagObj => {
const tag = tagObj.tag.startsWith('#') ? tagObj.tag.substring(1) : tagObj.tag;
allTags.add(tag);
});
if (allTags.size > 0) {
// 創建標籤容器
const tagsContainer = contentArea.createDiv('ge-tags-container');
// 取得所有標籤
const displayTags = Array.from(allTags);
displayTags.forEach(tag => {
const tagEl = tagsContainer.createEl('span', {
const tagEl = tagsContainer.createEl('span', {
cls: 'ge-tag',
text: tag.startsWith('#') ? tag : `#${tag}`
});
//添加右鍵選單事件,點擊後開啟選單,點擊選單中的選項後追加標籤到搜尋關鍵字內並重新渲染
tagEl.addEventListener('contextmenu', (e) => {
e.preventDefault();
@ -778,8 +796,8 @@ export class GridView extends ItemView {
.setTitle(t('add_tag_to_search'))
.setIcon('circle-plus')
.onClick(() => {
this.searchQuery += ` ${tagText}`;
this.render(true);
const searchQuery = this.searchQuery + ` ${tagText}`;
this.setSource('', '', true, searchQuery);
return false;
})
);
@ -790,8 +808,8 @@ export class GridView extends ItemView {
.setTitle(t('remove_tag_from_search'))
.setIcon('circle-minus')
.onClick(() => {
this.searchQuery = this.searchQuery.replace(tagText, '');
this.render(true);
const searchQuery = this.searchQuery.replace(tagText, '');
this.setSource('', '', true, searchQuery);
return false;
})
);
@ -807,21 +825,19 @@ export class GridView extends ItemView {
e.preventDefault();
e.stopPropagation(); // 防止事件冒泡到卡片
const tagText = tag.startsWith('#') ? tag : `#${tag}`;
if (this.searchQuery === tagText) {
return;
if (this.searchQuery !== tagText) {
this.setSource('', '', true, tagText);
}
this.searchQuery = tagText;
this.render(true);
return false;
});
});
}
}
}
contentArea.setAttribute('data-loaded', 'true');
}
// 載入圖片預覽
if (!this.minMode) {
const imageArea = fileEl.querySelector('.ge-image-area');
@ -860,7 +876,7 @@ export class GridView extends ItemView {
}
}
}
// 一旦載入完成,就不需要再觀察這個元素
observer.unobserve(fileEl);
}
@ -870,13 +886,13 @@ export class GridView extends ItemView {
rootMargin: '50px', // 預先載入視窗外 50px 的內容
threshold: 0.1
});
// 顯示檔案
if (files.length > 0) {
// 檢查是否應該顯示日期分隔器
const dateDividerMode = this.plugin.settings.dateDividerMode || 'none';
const sortType = this.folderSortType ? this.folderSortType : this.sortType;
const shouldShowDateDividers = dateDividerMode !== 'none' &&
const shouldShowDateDividers = dateDividerMode !== 'none' &&
(sortType.startsWith('mtime-') || sortType.startsWith('ctime-')) &&
this.sourceMode !== 'random-note' &&
this.sourceMode !== 'bookmarks' &&
@ -959,23 +975,23 @@ export class GridView extends ItemView {
container.createDiv('ge-break');
state.blankDividerAdded = true;
}
// 日期分隔器
if (shouldShowDateDividers && !this.pinnedList.includes(file.name)) {
let timestamp = 0;
// 根據排序類型獲取日期時間戳
if (sortType.startsWith('mtime-') || sortType.startsWith('ctime-')) {
// 判斷是否以修改日期排序,最近檔案模式使用修改日期排序
const isModifiedTime = sortType.startsWith('mtime-') || this.sourceMode === 'recent-files';
// 檢查是否是 Markdown 文件,且有設定對應的 frontmatter 字段
let frontMatterDate = null;
if (file.extension === 'md') {
const metadata = this.app.metadataCache.getFileCache(file);
if (metadata?.frontmatter) {
const fieldSetting = isModifiedTime
? this.plugin.settings.modifiedDateField
const fieldSetting = isModifiedTime
? this.plugin.settings.modifiedDateField
: this.plugin.settings.createdDateField;
const fieldNames = fieldSetting
@ -994,7 +1010,7 @@ export class GridView extends ItemView {
}
}
}
// 使用 frontmatter 中的日期或檔案的狀態日期
if (frontMatterDate) {
timestamp = frontMatterDate.getTime();
@ -1025,11 +1041,11 @@ export class GridView extends ItemView {
// 如果日期不同於上一個檔案的日期,添加分隔器
if (currentDateString !== state.lastDateString) {
state.lastDateString = currentDateString;
// 創建日期分隔器
const dateDivider = container.createDiv('ge-date-divider');
dateDivider.textContent = currentDateString;
// 針對 iOS 設備進行特殊處理
if (Platform.isIosApp) {
dateDivider.style.width = 'calc(100% - 16px)';
@ -1052,16 +1068,16 @@ export class GridView extends ItemView {
if (this.pinnedList.includes(file.name)) {
fileEl.addClass('ge-pinned');
}
// 創建左側內容區,包含圖示和標題
const contentArea = fileEl.createDiv('ge-content-area');
// 創建標題容器
const titleContainer = contentArea.createDiv('ge-title-container');
const extension = file.extension.toLowerCase();
// 檢查是否為媒體檔案,如果是則添加 ge-media-card 類別
if (this.cardLayout === 'vertical' &&
if (this.cardLayout === 'vertical' &&
(isImageFile(file) || isVideoFile(file)) &&
!this.minMode) {
fileEl.addClass('ge-media-card');
@ -1104,7 +1120,7 @@ export class GridView extends ItemView {
if (!this.minMode) {
fileEl.createDiv('ge-image-area');
}
// 開始觀察這個元素
observer.observe(fileEl);
@ -1146,7 +1162,7 @@ export class GridView extends ItemView {
// Alt 鍵或設定為預設時:在 grid container 中顯示筆記
this.selectItem(index);
this.hasKeyboardFocus = true;
if (isMediaFile(file)) {
// 媒體檔案:正常開啟
if (isAudioFile(file)) {
@ -1183,7 +1199,7 @@ export class GridView extends ItemView {
if (redirectType && typeof redirectPath === 'string' && redirectPath.trim() !== '') {
let target;
if (redirectType === 'file') {
if (redirectPath.startsWith('[[') && redirectPath.endsWith(']]')) {
const noteName = redirectPath.slice(2, -2);
@ -1191,7 +1207,7 @@ export class GridView extends ItemView {
} else {
target = this.app.vault.getAbstractFileByPath(normalizePath(redirectPath));
}
if (target instanceof TFile) {
this.app.workspace.getLeaf().openFile(target);
} else {
@ -1201,20 +1217,23 @@ export class GridView extends ItemView {
else if (redirectType === 'folder') {
// 判斷redirectPath是否為資料夾
if (this.app.vault.getAbstractFileByPath(normalizePath(redirectPath)) instanceof TFolder) {
this.setSource('folder', redirectPath, true);
this.setSource('folder', redirectPath);
this.clearSelection();
} else {
new Notice(`${t('target_not_found')}: ${redirectPath}`);
}
} else if (redirectType === 'mode') {
// 判斷redirectPath是否為模式
this.setSource(redirectPath, '', true);
this.setSource(redirectPath);
this.clearSelection();
} else if (redirectType === 'search') {
this.setSource('', '', true, redirectPath);
this.clearSelection();
} else if (redirectType === 'uri') {
// 檢查是否為 http/https 或 obsidian:// 協議
if (redirectPath.startsWith('http://') ||
redirectPath.startsWith('https://') ||
redirectPath.startsWith('obsidian://') ||
if (redirectPath.startsWith('http://') ||
redirectPath.startsWith('https://') ||
redirectPath.startsWith('obsidian://') ||
redirectPath.startsWith('file://')) {
// 使用 window.open 打開網址或 obsidian 協議
window.open(redirectPath, '_blank');
@ -1236,7 +1255,7 @@ export class GridView extends ItemView {
// 1. 先嘗試完整比對(不分大小寫)
idx = lowerContent.indexOf(searchQuery.toLowerCase());
// 2. 若找不到,嘗試拆開關鍵字搜尋
if (idx === -1 && searchQuery.includes(' ')) {
const keywords = searchQuery.split(/\s+/).filter(k => k.trim() !== '');
@ -1286,7 +1305,7 @@ export class GridView extends ItemView {
}
});
if(Platform.isDesktop) {
if (Platform.isDesktop) {
// 添加拖曳功能
fileEl.setAttribute('draggable', 'true');
fileEl.addEventListener('dragstart', (event) => {
@ -1302,7 +1321,7 @@ export class GridView extends ItemView {
// 獲取選中的檔案
const selectedFiles = this.getSelectedFiles();
let drag_filename = '';
// 添加拖曳資料
if (selectedFiles.length > 1) {
// 如果多個檔案被選中,使用 files-menu
@ -1311,11 +1330,11 @@ export class GridView extends ItemView {
return isMedia ? `![[${f.path}]]` : `[[${f.path}]]`;
}).join('\n');
event.dataTransfer?.setData('text/plain', fileList);
// 添加檔案路徑列表
event.dataTransfer?.setData('application/obsidian-grid-explorer-files',
event.dataTransfer?.setData('application/obsidian-grid-explorer-files',
JSON.stringify(selectedFiles.map(f => f.path)));
drag_filename = `${selectedFiles.length} ${t('files')}`;
} else {
// 如果只有單個檔案被選中,使用檔案路徑
@ -1326,21 +1345,21 @@ export class GridView extends ItemView {
// 添加拖曳資料
event.dataTransfer?.setData('text/plain', mdLink);
// 添加檔案路徑列表
event.dataTransfer?.setData('application/obsidian-grid-explorer-files',
event.dataTransfer?.setData('application/obsidian-grid-explorer-files',
JSON.stringify([file.path]));
drag_filename = file.basename;
}
const dragImage = document.createElement('div');
dragImage.className = 'ge-custom-drag-preview';
dragImage.textContent = drag_filename;
// 將元素暫時加入 DOM
document.body.appendChild(dragImage);
// 設定拖曳圖示
event.dataTransfer!.setDragImage(dragImage, 20, 20);
@ -1354,18 +1373,18 @@ export class GridView extends ItemView {
// 添加拖曳中的視覺效果
fileEl.addClass('ge-dragging');
});
fileEl.addEventListener('dragend', () => {
// 移除拖曳中的視覺效果
fileEl.removeClass('ge-dragging');
});
}
// 添加右鍵選單
fileEl.addEventListener('contextmenu', (event) => {
event.preventDefault();
const menu = new Menu();
// 獲取項目索引
const index = this.gridItems.indexOf(fileEl);
if (index >= 0) {
@ -1377,11 +1396,11 @@ export class GridView extends ItemView {
// 獲取選中的檔案
const selectedFiles = this.getSelectedFiles();
if (selectedFiles.length > 1) {
// 多個檔案被選中,使用 files-menu
this.app.workspace.trigger('files-menu', menu, selectedFiles);
// 檢查是否所有選中的檔案都是 md 檔案
const allMdFiles = selectedFiles.every(file => file.extension === 'md');
if (allMdFiles) {
@ -1449,8 +1468,8 @@ export class GridView extends ItemView {
.onClick(() => {
this.hideHeaderElements = !this.hideHeaderElements;
this.app.workspace.requestSaveLayout();
this.render(true);
});
this.render();
});
});
menu.addItem((item) => {
item
@ -1465,7 +1484,7 @@ export class GridView extends ItemView {
.setTitle(t('refresh'))
.setIcon("refresh-cw")
.onClick(() => {
this.render(true);
this.render();
});
});
}
@ -1493,7 +1512,7 @@ export class GridView extends ItemView {
if (index >= 0 && index < this.gridItems.length) {
this.selectedItemIndex = index;
const selectedItem = this.gridItems[index];
// 如果是多選模式且項目已被選中,則取消選中
if (multiSelect && this.selectedItems.has(index)) {
selectedItem.removeClass('ge-selected-item');
@ -1510,7 +1529,7 @@ export class GridView extends ItemView {
selectedItem.addClass('ge-selected-item');
this.selectedItems.add(index);
}
// 確保選中的項目在視圖中可見
selectedItem.scrollIntoView({ block: 'nearest' });
}
@ -1563,10 +1582,10 @@ export class GridView extends ItemView {
// 開啟媒體檔案
openMediaFile(file: TFile, mediaFiles?: TFile[]) {
// 如果沒有傳入媒體檔案列表,則獲取
const getMediaFilesPromise = mediaFiles
const getMediaFilesPromise = mediaFiles
? Promise.resolve(mediaFiles.filter(f => isMediaFile(f)))
: getFiles(this, this.randomNoteIncludeMedia).then(allFiles => allFiles.filter(f => isMediaFile(f)));
getMediaFilesPromise.then(filteredMediaFiles => {
// 找到當前檔案在媒體檔案列表中的索引
const currentIndex = filteredMediaFiles.findIndex(f => f.path === file.path);
@ -1586,44 +1605,8 @@ export class GridView extends ItemView {
const redirectPath = fileCache?.frontmatter?.redirect;
if (redirectType && typeof redirectPath === 'string' && redirectPath.trim() !== '') {
let target;
if (redirectType === 'file') {
if (redirectPath.startsWith('[[') && redirectPath.endsWith(']]')) {
const noteName = redirectPath.slice(2, -2);
target = this.app.metadataCache.getFirstLinkpathDest(noteName, file.path);
} else {
target = this.app.vault.getAbstractFileByPath(normalizePath(redirectPath));
}
if (target instanceof TFile) {
// 遞迴處理目標檔案
this.showNoteInGrid(target);
return;
} else {
new Notice(`${t('target_not_found')}: ${redirectPath}`);
return;
}
}
else if (redirectType === 'folder') {
// 判斷redirectPath是否為資料夾
if (this.app.vault.getAbstractFileByPath(normalizePath(redirectPath)) instanceof TFolder) {
this.setSource('folder', redirectPath, true);
this.clearSelection();
return;
} else {
new Notice(`${t('target_not_found')}: ${redirectPath}`);
return;
}
} else if (redirectType === 'mode') {
// 判斷redirectPath是否為模式
this.setSource(redirectPath, '', true);
this.clearSelection();
return;
} else {
new Notice(`${t('target_not_found')}: ${redirectPath}`);
return;
}
this.app.workspace.getLeaf().openFile(file);
return;
}
// 關閉之前的筆記顯示
@ -1668,8 +1651,8 @@ export class GridView extends ItemView {
const scrollContainer = this.noteViewContainer.createDiv('ge-note-scroll-container');
// 假設在視圖側邊欄則把字型調小
const isInSidebar = this.leaf.getRoot() === this.app.workspace.leftSplit ||
this.leaf.getRoot() === this.app.workspace.rightSplit;
const isInSidebar = this.leaf.getRoot() === this.app.workspace.leftSplit ||
this.leaf.getRoot() === this.app.workspace.rightSplit;
if (isInSidebar) {
scrollContainer.style.fontSize = '1em';
scrollContainer.style.backgroundColor = 'var(--background-secondary)';
@ -1680,7 +1663,7 @@ export class GridView extends ItemView {
if (isInSidebar) {
noteContent.style.padding = '15px';
}
// 創建筆記內容區域
const noteContentArea = noteContent.createDiv('ge-note-content');
@ -1696,7 +1679,7 @@ export class GridView extends ItemView {
file.path,
this
);
// 加上自訂屬性 data-source-path
noteContentArea
.querySelectorAll<HTMLImageElement>('img')
@ -1709,7 +1692,7 @@ export class GridView extends ItemView {
if (link) {
e.preventDefault();
e.stopPropagation();
const href = link.getAttribute('href');
if (href) {
const linkText = link.getAttribute('data-href') || href;
@ -1738,9 +1721,9 @@ export class GridView extends ItemView {
e.preventDefault();
}
};
document.addEventListener('keydown', handleKeyDown);
// 儲存事件監聽器以便後續移除
(this.noteViewContainer as any).keydownHandler = handleKeyDown;
}
@ -1760,7 +1743,7 @@ export class GridView extends ItemView {
if (keydownHandler) {
document.removeEventListener('keydown', keydownHandler);
}
this.noteViewContainer.remove();
this.noteViewContainer = null;
}
@ -1794,7 +1777,7 @@ export class GridView extends ItemView {
}
// 讀取視圖狀態
async setState(state: any): Promise<void> {
async setState(state: any): Promise<void> {
if (state.state) {
this.sourceMode = state.state.sourceMode || 'folder';
this.sourcePath = state.state.sourcePath || '/';

View file

@ -36,7 +36,7 @@ export function handleKeyDown(gridView: GridView, event: KeyboardEvent) {
if (gridView.sourceMode === 'folder' && gridView.sourcePath && gridView.sourcePath !== '/') {
// 獲取上一層資料夾路徑
const parentPath = gridView.sourcePath.split('/').slice(0, -1).join('/') || '/';
gridView.setSource('folder', parentPath, true);
gridView.setSource('folder', parentPath);
gridView.clearSelection();
event.preventDefault();
}
@ -100,7 +100,7 @@ export function handleKeyDown(gridView: GridView, event: KeyboardEvent) {
if (gridView.sourceMode === 'folder' && gridView.sourcePath && gridView.sourcePath !== '/') {
// 獲取上一層資料夾路徑
const parentPath = gridView.sourcePath.split('/').slice(0, -1).join('/') || '/';
gridView.setSource('folder', parentPath, true);
gridView.setSource('folder', parentPath);
gridView.clearSelection();
event.preventDefault();
}
@ -176,7 +176,7 @@ export function handleKeyDown(gridView: GridView, event: KeyboardEvent) {
if (gridView.sourceMode === 'folder' && gridView.sourcePath && gridView.sourcePath !== '/') {
// 獲取上一層資料夾路徑
const parentPath = gridView.sourcePath.split('/').slice(0, -1).join('/') || '/';
gridView.setSource('folder', parentPath, true);
gridView.setSource('folder', parentPath);
gridView.clearSelection();
event.preventDefault();
}

View file

@ -51,10 +51,13 @@ export default class GridExplorerPlugin extends Plugin {
this.addCommand({
id: 'view-backlinks-in-grid-view',
name: t('open_backlinks_in_grid_view'),
callback: () => {
callback: async () => {
const activeFile = this.app.workspace.getActiveFile();
if (activeFile) {
this.activateView('backlinks');
const view = await this.activateView();
if (view instanceof GridView) {
await view.setSource('backlinks');
}
} else {
// 如果沒有當前筆記,則打開根目錄
this.openNoteInFolder(this.app.vault.getRoot());
@ -66,10 +69,13 @@ export default class GridExplorerPlugin extends Plugin {
this.addCommand({
id: 'view-outgoinglinks-in-grid-view',
name: t('open_outgoinglinks_in_grid_view'),
callback: () => {
callback: async () => {
const activeFile = this.app.workspace.getActiveFile();
if (activeFile) {
this.activateView('outgoinglinks');
const view = await this.activateView();
if (view instanceof GridView) {
await view.setSource('outgoinglinks');
}
} else {
// 如果沒有當前筆記,則打開根目錄
this.openNoteInFolder(this.app.vault.getRoot());
@ -117,7 +123,10 @@ export default class GridExplorerPlugin extends Plugin {
id: 'open-quick-access-mode',
name: t('open_quick_access_mode'),
callback: async () => {
this.activateView(this.settings.quickAccessModeType);
const view = await this.activateView();
if (view instanceof GridView) {
await view.setSource(this.settings.quickAccessModeType);
}
}
});
@ -162,22 +171,24 @@ export default class GridExplorerPlugin extends Plugin {
item
.setTitle(t('open_backlinks_in_grid_view'))
.setIcon('links-coming-in')
.onClick(() => {
.onClick(async () => {
this.app.workspace.getLeaf().openFile(file);
setTimeout(() => {
this.activateView('backlinks');
}, 100);
const view = await this.activateView();
if (view instanceof GridView) {
await view.setSource('backlinks');
}
});
});
ogSubmenu.addItem((item) => {
item
.setTitle(t('open_outgoinglinks_in_grid_view'))
.setIcon('links-going-out')
.onClick(() => {
.onClick(async () => {
this.app.workspace.getLeaf().openFile(file);
setTimeout(() => {
this.activateView('outgoinglinks');
}, 100);
const view = await this.activateView();
if (view instanceof GridView) {
await view.setSource('outgoinglinks');
}
});
});
if (this.settings.showRecentFilesMode && file instanceof TFile) {
@ -202,12 +213,9 @@ export default class GridExplorerPlugin extends Plugin {
.setSection?.("view")
.onClick(async () => {
// 取得或啟用 GridView
const view = await this.activateView('','');
const view = await this.activateView();
if (view instanceof GridView) {
// 設定搜尋模式和關鍵字
view.searchQuery = link;
// 重新渲染視圖
view.render(true); // resetScroll = true
await view.setSource('', '', true, link);
}
});
});
@ -241,12 +249,9 @@ export default class GridExplorerPlugin extends Plugin {
.onClick(async () => {
const selectedText = editor.getSelection();
// 取得或啟用 GridView
const view = await this.activateView('','');
const view = await this.activateView();
if (view instanceof GridView) {
// 設定搜尋模式和關鍵字
view.searchQuery = selectedText;
// 重新渲染視圖
view.render(true); // resetScroll = true
await view.setSource('', '', true, selectedText);
}
});
});
@ -268,12 +273,9 @@ export default class GridExplorerPlugin extends Plugin {
.setSection?.("view")
.onClick(async () => {
// 取得或啟用 GridView
const view = await this.activateView('','');
const view = await this.activateView();
if (view instanceof GridView) {
// 設定搜尋模式和關鍵字
view.searchQuery = `#${tagName}`;
// 重新渲染視圖
view.render(true); // resetScroll = true
await view.setSource('', '', true, `#${tagName}`);
}
});
});
@ -337,10 +339,9 @@ export default class GridExplorerPlugin extends Plugin {
evt.stopPropagation();
// 叫出 GridView並把搜尋字串設成這個 tag
const view = await this.activateView('', '');
const view = await this.activateView();
if (view instanceof GridView) {
view.searchQuery = `#${tagName}`;
await view.render(true); // resetScroll
await view.setSource('', '', true, `#${tagName}`);
}
}, true); // 用 capture可在其他 listener 前先吃到
@ -381,10 +382,9 @@ export default class GridExplorerPlugin extends Plugin {
const folder = this.app.vault.getAbstractFileByPath(folderPath);
if (folder instanceof TFolder) {
const view = await this.activateView('folder', folderPath);
const view = await this.activateView();
if (view instanceof GridView) {
view.searchQuery = ''; // 清空搜尋字串
view.render(true); // resetScroll
await view.setSource('folder', folderPath, true, '');
}
}
}, true); // 使用 capture 階段以確保優先處理
@ -548,29 +548,32 @@ export default class GridExplorerPlugin extends Plugin {
// 打開最近文件
async openNoteInRecentFiles(file: TFile) {
const view = await this.activateView('recent-files') as GridView;
// 如果是文件,等待視圖渲染完成後捲動到該文件位置
if (file instanceof TFile) {
// 等待下一個事件循環以確保視圖已完全渲染
setTimeout(() => {
const gridContainer = view.containerEl.querySelector('.ge-grid-container') as HTMLElement;
if (!gridContainer) return;
// 找到對應的網格項目
const gridItem = Array.from(gridContainer.querySelectorAll('.ge-grid-item')).find(
item => (item as HTMLElement).dataset.filePath === file.path
) as HTMLElement;
if (gridItem) {
// 捲動到該項目的位置
gridItem.scrollIntoView({ block: 'nearest' });
// 選中該項目
const itemIndex = view.gridItems.indexOf(gridItem);
if (itemIndex >= 0) {
view.selectItem(itemIndex);
const view = await this.activateView();
if (view instanceof GridView) {
await view.setSource('recent-files');
// 如果是文件,等待視圖渲染完成後捲動到該文件位置
if (file instanceof TFile) {
// 等待下一個事件循環以確保視圖已完全渲染
requestAnimationFrame(() => {
const gridContainer = view.containerEl.querySelector('.ge-grid-container') as HTMLElement;
if (!gridContainer) return;
// 找到對應的網格項目
const gridItem = Array.from(gridContainer.querySelectorAll('.ge-grid-item')).find(
item => (item as HTMLElement).dataset.filePath === file.path
) as HTMLElement;
if (gridItem) {
// 捲動到該項目的位置
gridItem.scrollIntoView({ block: 'nearest' });
// 選中該項目
const itemIndex = view.gridItems.indexOf(gridItem);
if (itemIndex >= 0) {
view.selectItem(itemIndex);
}
}
}
}, 100);
});
}
}
}
@ -578,35 +581,37 @@ export default class GridExplorerPlugin extends Plugin {
async openNoteInFolder(file: TFile | TFolder = this.app.vault.getRoot()) {
// 如果是文件,使用其父資料夾路徑
const folderPath = file ? (file instanceof TFile ? file.parent?.path : file.path) : "/";
const view = await this.activateView('folder', folderPath) as GridView;
// 如果是文件,等待視圖渲染完成後捲動到該文件位置
if (file instanceof TFile) {
// 等待下一個事件循環以確保視圖已完全渲染
setTimeout(() => {
const gridContainer = view.containerEl.querySelector('.ge-grid-container') as HTMLElement;
if (!gridContainer) return;
// 找到對應的網格項目
const gridItem = Array.from(gridContainer.querySelectorAll('.ge-grid-item')).find(
item => (item as HTMLElement).dataset.filePath === file.path
) as HTMLElement;
if (gridItem) {
// 捲動到該項目的位置
gridItem.scrollIntoView({ block: 'nearest' });
// 選中該項目
const itemIndex = view.gridItems.indexOf(gridItem);
if (itemIndex >= 0) {
view.selectItem(itemIndex);
const view = await this.activateView();
if (view instanceof GridView) {
await view.setSource('folder', folderPath);
// 如果是文件,等待視圖渲染完成後捲動到該文件位置
if (file instanceof TFile) {
// 等待下一個事件循環以確保視圖已完全渲染
requestAnimationFrame(() => {
const gridContainer = view.containerEl.querySelector('.ge-grid-container') as HTMLElement;
if (!gridContainer) return;
// 找到對應的網格項目
const gridItem = Array.from(gridContainer.querySelectorAll('.ge-grid-item')).find(
item => (item as HTMLElement).dataset.filePath === file.path
) as HTMLElement;
if (gridItem) {
// 捲動到該項目的位置
gridItem.scrollIntoView({ block: 'nearest' });
// 選中該項目
const itemIndex = view.gridItems.indexOf(gridItem);
if (itemIndex >= 0) {
view.selectItem(itemIndex);
}
}
}
}, 100);
});
}
}
}
// 激活視圖
async activateView(mode = 'folder', path = '') {
async activateView() {
const { workspace } = this.app;
let leaf = null;
@ -639,11 +644,6 @@ export default class GridExplorerPlugin extends Plugin {
await leaf.setViewState({ type: 'grid-view', active: true });
// 設定資料來源
if (leaf.view instanceof GridView) {
await leaf.view.setSource(mode, path);
}
// 確保視圖是活躍的
workspace.revealLeaf(leaf);
return leaf.view;

View file

@ -42,7 +42,7 @@ export class showFolderMoveModal extends SuggestModal<string> {
// 給檔案系統一點時間處理,然後重新整理視圖
setTimeout(() => {
if (!this.plugin.settings.showFolder) {
this.gridView.setSource('folder', newPath || '/', true);
this.gridView.setSource('folder', newPath || '/');
} else {
this.gridView.render();
}

View file

@ -63,7 +63,7 @@ export class FolderRenameModal extends Modal {
// 重新渲染視圖
setTimeout(() => {
if (!this.plugin.settings.showFolder) {
this.gridView.setSource('folder', newPath || '/', true);
this.gridView.setSource('folder', newPath || '/');
} else {
this.gridView.render();
}

View file

@ -72,11 +72,14 @@ export class FolderSelectionModal extends Modal {
text: `${mode.icon} ${mode.displayName}`
});
customOption.addEventListener('click', () => {
customOption.addEventListener('click', async () => {
if (this.activeView) {
this.activeView.setSource(mode.internalName, '', true);
await this.activeView.setSource(mode.internalName);
} else {
this.plugin.activateView(mode.internalName);
const view = await this.plugin.activateView();
if (view instanceof GridView) {
await view.setSource(mode.internalName);
}
}
this.close();
});
@ -93,11 +96,14 @@ export class FolderSelectionModal extends Modal {
text: `📑 ${t('bookmarks_mode')}`
});
bookmarkOption.addEventListener('click', () => {
bookmarkOption.addEventListener('click', async () => {
if (this.activeView) {
this.activeView.setSource('bookmarks', '', true);
await this.activeView.setSource('bookmarks');
} else {
this.plugin.activateView('bookmarks');
const view = await this.plugin.activateView();
if (view instanceof GridView) {
await view.setSource('bookmarks');
}
}
this.close();
});
@ -118,11 +124,14 @@ export class FolderSelectionModal extends Modal {
text: `🔍 ${t('search_results')}: ${searchInputEl.value}`
});
searchOption.addEventListener('click', () => {
searchOption.addEventListener('click', async () => {
if (this.activeView) {
this.activeView.setSource('search', '', true);
await this.activeView.setSource('search');
} else {
this.plugin.activateView('search');
const view = await this.plugin.activateView();
if (view instanceof GridView) {
await view.setSource('search');
}
}
this.close();
});
@ -142,11 +151,14 @@ export class FolderSelectionModal extends Modal {
text: `🔗 ${t('backlinks_mode')}${activeFileName}`
});
backlinksOption.addEventListener('click', () => {
backlinksOption.addEventListener('click', async () => {
if (this.activeView) {
this.activeView.setSource('backlinks', '', true);
await this.activeView.setSource('backlinks');
} else {
this.plugin.activateView('backlinks');
const view = await this.plugin.activateView();
if (view instanceof GridView) {
await view.setSource('backlinks');
}
}
this.close();
});
@ -164,11 +176,14 @@ export class FolderSelectionModal extends Modal {
text: `🔗 ${t('outgoinglinks_mode')}${activeFileName}`
});
outgoinglinksOption.addEventListener('click', () => {
outgoinglinksOption.addEventListener('click', async () => {
if (this.activeView) {
this.activeView.setSource('outgoinglinks', '', true);
await this.activeView.setSource('outgoinglinks');
} else {
this.plugin.activateView('outgoinglinks');
const view = await this.plugin.activateView();
if (view instanceof GridView) {
await view.setSource('outgoinglinks');
}
}
this.close();
});
@ -183,11 +198,14 @@ export class FolderSelectionModal extends Modal {
text: `📅 ${t('recent_files_mode')}`
});
recentFilesOption.addEventListener('click', () => {
recentFilesOption.addEventListener('click', async () => {
if (this.activeView) {
this.activeView.setSource('recent-files', '', true);
await this.activeView.setSource('recent-files');
} else {
this.plugin.activateView('recent-files');
const view = await this.plugin.activateView();
if (view instanceof GridView) {
await view.setSource('recent-files');
}
}
this.close();
});
@ -201,11 +219,14 @@ export class FolderSelectionModal extends Modal {
text: `📔 ${t('all_files_mode')}`
});
allFilesOption.addEventListener('click', () => {
allFilesOption.addEventListener('click', async () => {
if (this.activeView) {
this.activeView.setSource('all-files', '', true);
await this.activeView.setSource('all-files');
} else {
this.plugin.activateView('all-files');
const view = await this.plugin.activateView();
if (view instanceof GridView) {
await view.setSource('all-files');
}
}
this.close();
});
@ -219,11 +240,14 @@ export class FolderSelectionModal extends Modal {
text: `🎲 ${t('random_note_mode')}`
});
randomNoteOption.addEventListener('click', () => {
randomNoteOption.addEventListener('click', async () => {
if (this.activeView) {
this.activeView.setSource('random-note', '', true);
await this.activeView.setSource('random-note');
} else {
this.plugin.activateView('random-note');
const view = await this.plugin.activateView();
if (view instanceof GridView) {
await view.setSource('random-note');
}
}
this.close();
});
@ -237,11 +261,14 @@ export class FolderSelectionModal extends Modal {
text: `☑️ ${t('tasks_mode')}`
});
tasksOption.addEventListener('click', () => {
tasksOption.addEventListener('click', async () => {
if (this.activeView) {
this.activeView.setSource('tasks', '', true);
await this.activeView.setSource('tasks');
} else {
this.plugin.activateView('tasks');
const view = await this.plugin.activateView();
if (view instanceof GridView) {
await view.setSource('tasks');
}
}
this.close();
});
@ -255,11 +282,14 @@ export class FolderSelectionModal extends Modal {
text: `${customFolderIcon} /`
});
rootFolderOption.addEventListener('click', () => {
rootFolderOption.addEventListener('click', async () => {
if (this.activeView) {
this.activeView.setSource('folder', '/', true);
await this.activeView.setSource('folder', '/');
} else {
this.plugin.activateView('folder', '/');
const view = await this.plugin.activateView();
if (view instanceof GridView) {
await view.setSource('folder', '/');
}
}
this.close();
});
@ -307,11 +337,14 @@ export class FolderSelectionModal extends Modal {
nameSpan.textContent = displayName;
folderOption.appendChild(nameSpan);
folderOption.addEventListener('click', () => {
folderOption.addEventListener('click', async () => {
if (this.activeView) {
this.activeView.setSource('folder', folder.path, true);
await this.activeView.setSource('folder', folder.path);
} else {
this.plugin.activateView('folder', folder.path);
const view = await this.plugin.activateView();
if (view instanceof GridView) {
await view.setSource('folder', folder.path);
}
}
this.close();
});

102
src/modal/inputModal.ts Normal file
View file

@ -0,0 +1,102 @@
import { App, Modal } from 'obsidian';
import { t } from '../translations';
interface InputModalOptions {
title: string;
placeholder: string;
defaultValue?: string;
inputType?: 'text' | 'url';
onSubmit: (value: string) => void;
}
export class InputModal extends Modal {
options: InputModalOptions;
constructor(app: App, options: InputModalOptions) {
super(app);
this.options = options;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
// 添加標題
contentEl.createEl('h2', { text: this.options.title });
// 創建輸入框容器
const inputContainer = contentEl.createDiv('ge-input-field-container');
// 創建輸入框
const input = inputContainer.createEl('input', {
type: this.options.inputType || 'text',
value: this.options.defaultValue || '',
placeholder: this.options.placeholder,
cls: 'ge-input-field'
});
// 創建按鈕容器
const buttonContainer = contentEl.createDiv('ge-button-container');
// 創建確認按鈕
const submitButton = buttonContainer.createEl('button', {
text: t('confirm'),
cls: 'mod-cta'
});
// 創建取消按鈕
const cancelButton = buttonContainer.createEl('button', {
text: t('cancel')
});
// 執行提交的函數
const performSubmit = () => {
const value = input.value.trim();
if (value) {
this.options.onSubmit(value);
this.close();
}
};
// 綁定事件
submitButton.addEventListener('click', performSubmit);
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
performSubmit();
}
});
cancelButton.addEventListener('click', () => {
this.close();
});
// 自動聚焦到輸入框
input.focus();
input.select();
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
// 便利函數:顯示搜尋輸入 modal
export function showSearchInputModal(app: App, onSubmit: (searchText: string) => void) {
new InputModal(app, {
title: t('search_text'),
placeholder: t('enter_search_text'),
onSubmit
}).open();
}
// 便利函數:顯示 URI 輸入 modal
export function showUriInputModal(app: App, onSubmit: (uri: string) => void) {
new InputModal(app, {
title: t('enter_uri'),
placeholder: t('enter_uri_placeholder'),
inputType: 'url',
onSubmit
}).open();
}

View file

@ -339,7 +339,7 @@ export class SearchModal extends Modal {
this.gridView.searchMediaFiles = searchMediaFilesCheckbox.checked;
this.gridView.clearSelection();
this.gridView.app.workspace.requestSaveLayout();
this.gridView.render(true);
this.gridView.render();
this.close();
};

View file

@ -1,9 +1,10 @@
import { App, Modal, TFolder, TFile, FuzzySuggestModal } from 'obsidian';
import GridExplorerPlugin from '../main';
import { t } from '../translations';
import { showSearchInputModal, showUriInputModal } from './inputModal';
interface ShortcutOption {
type: 'mode' | 'folder' | 'file';
type: 'mode' | 'folder' | 'file' | 'search' | 'uri';
value: string;
display: string;
}
@ -27,7 +28,7 @@ export class ShortcutSelectionModal extends Modal {
// 資料夾選擇按鈕
const folderButton = contentEl.createDiv('shortcut-option-button');
folderButton.createSpan({ text: `📂 ${t('select_folder')}`});
folderButton.createSpan({ text: `📂 ${t('select_folder')}` });
// 點擊資料夾按鈕時打開資料夾選擇模態框
folderButton.addEventListener('click', () => {
@ -44,7 +45,7 @@ export class ShortcutSelectionModal extends Modal {
// 檔案選擇按鈕
const fileButton = contentEl.createDiv('shortcut-option-button');
fileButton.createSpan({ text: `📄 ${t('select_file')}`});
fileButton.createSpan({ text: `📄 ${t('select_file')}` });
// 點擊檔案按鈕時打開檔案選擇模態框
fileButton.addEventListener('click', () => {
@ -59,6 +60,73 @@ export class ShortcutSelectionModal extends Modal {
}).open();
});
// 搜尋文字按鈕
const searchButton = contentEl.createDiv('shortcut-option-button');
searchButton.createSpan({ text: `🔎 ${t('search_text')}` });
// 點擊搜尋按鈕時打開搜尋輸入模態框
searchButton.addEventListener('click', () => {
showSearchInputModal(this.app, (searchText) => {
this.onSubmit({
type: 'search',
value: searchText,
display: `🔎 ${searchText}`
});
this.close();
});
});
// URI 按鈕
const uriButton = contentEl.createDiv('shortcut-option-button');
uriButton.createSpan({ text: `🌐 ${t('enter_uri')}` });
// 點擊 URI 按鈕時打開 URI 輸入模態框
uriButton.addEventListener('click', () => {
showUriInputModal(this.app, (uri) => {
// 為顯示生成友好的名稱
let displayName: string;
try {
if (uri.startsWith('obsidian://')) {
// 嘗試提取 vault 參數
const vaultMatch = uri.match(/[?&]vault=([^&]+)/);
if (vaultMatch) {
const vaultName = decodeURIComponent(vaultMatch[1]);
displayName = `🌐 Obsidian Link (${vaultName})`;
} else {
displayName = '🌐 Obsidian Link';
}
} else if (uri.startsWith('http://') || uri.startsWith('https://')) {
const url = new URL(uri);
let domain = url.hostname;
if (domain.startsWith('www.')) {
domain = domain.substring(4);
}
displayName = `🌐 ${domain}`;
} else if (uri.startsWith('file://')) {
displayName = '🌐 Local File';
} else {
const protocolMatch = uri.match(/^([^:]+):/);
if (protocolMatch) {
displayName = `🌐 ${protocolMatch[1].toUpperCase()} Link`;
} else {
displayName = `🌐 ${uri.substring(0, 20)}${uri.length > 20 ? '...' : ''}`;
}
}
} catch (error) {
displayName = `🌐 ${uri.substring(0, 20)}${uri.length > 20 ? '...' : ''}`;
}
this.onSubmit({
type: 'uri',
value: uri,
display: displayName
});
this.close();
});
});
contentEl.createEl('p');
// 初始化模式選項,先添加自定義模式
const modeOptions: ShortcutOption[] = [];
@ -74,7 +142,7 @@ export class ShortcutSelectionModal extends Modal {
// 添加內建模式
modeOptions.push(
{ type: 'mode', value: 'bookmarks', display: `📑 ${t('bookmarks_mode')}` },
{ type: 'mode', value: 'search', display: `🔎 ${t('search_results')}` },
{ type: 'mode', value: 'search', display: `🔍 ${t('search_results')}` },
{ type: 'mode', value: 'backlinks', display: `🔗 ${t('backlinks_mode')}` },
{ type: 'mode', value: 'outgoinglinks', display: `🔗 ${t('outgoinglinks_mode')}` },
{ type: 'mode', value: 'all-files', display: `📔 ${t('all_files_mode')}` },

View file

@ -181,7 +181,7 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
event.stopPropagation();
openFolderInNewView(gridView, folder.path);
} else {
gridView.setSource('folder', folder.path, true);
gridView.setSource('folder', folder.path);
gridView.clearSelection();
}
});
@ -305,9 +305,9 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
if (folder instanceof TFolder) {
await gridView.app.fileManager.trashFile(folder);
// 重新渲染視圖
setTimeout(() => {
requestAnimationFrame(() => {
gridView.render();
}, 100);
});
}
});
});

View file

@ -39,20 +39,16 @@ export function renderHeaderButton(gridView: GridView) {
const lastSource = JSON.parse(gridView.recentSources[0]);
gridView.recentSources.shift(); // 從歷史記錄中移除
// 設定來源(不記錄到歷史)
// 設定來源及搜尋狀態(不記錄到歷史)
await gridView.setSource(
lastSource.mode,
lastSource.path || '',
true, // 重設捲動位置
false // 不記錄到歷史
false, // 不記錄到歷史
lastSource.searchQuery || '',
lastSource.searchAllFiles ?? true,
lastSource.searchFilesNameOnly ?? false,
lastSource.searchMediaFiles ?? false
);
// 還原搜尋相關狀態
gridView.searchQuery = lastSource.searchQuery || '';
gridView.searchAllFiles = lastSource.searchAllFiles ?? true;
gridView.searchFilesNameOnly = lastSource.searchFilesNameOnly ?? false;
gridView.searchMediaFiles = lastSource.searchMediaFiles ?? false;
// 重新渲染以應用搜尋狀態
await gridView.render();
}
});
@ -138,7 +134,7 @@ export function renderHeaderButton(gridView: GridView) {
item
.setTitle(`${displayText}`)
.setIcon(`${icon}`)
.onClick(() => {
.onClick(async () => {
// 找出當前點擊的紀錄索引
const clickedIndex = gridView.recentSources.findIndex(source => {
const parsed = JSON.parse(source);
@ -150,12 +146,16 @@ export function renderHeaderButton(gridView: GridView) {
gridView.recentSources = gridView.recentSources.slice(clickedIndex + 1);
}
gridView.setSource(mode, path, true, false);
gridView.searchQuery = sourceInfo.searchQuery || '';
gridView.searchAllFiles = sourceInfo.searchAllFiles ?? true;
gridView.searchFilesNameOnly = sourceInfo.searchFilesNameOnly ?? false;
gridView.searchMediaFiles = sourceInfo.searchMediaFiles ?? false;
gridView.render();
// 設定來源及搜尋狀態(不記錄到歷史)
await gridView.setSource(
mode,
path,
false, // 不記錄到歷史
sourceInfo.searchQuery || '',
sourceInfo.searchAllFiles ?? true,
sourceInfo.searchFilesNameOnly ?? false,
sourceInfo.searchMediaFiles ?? false
);
});
});
} catch (error) {
@ -220,7 +220,10 @@ export function renderHeaderButton(gridView: GridView) {
try {
// 建立新資料夾
await gridView.app.vault.createFolder(newFolderPath);
gridView.render(false);
// 重新渲染視圖
requestAnimationFrame(() => {
gridView.render();
});
} catch (error) {
console.error('An error occurred while creating a new folder:', error);
}
@ -446,16 +449,115 @@ export function renderHeaderButton(gridView: GridView) {
});
}
// 將 URI 轉換為合適的檔名
function generateFilenameFromUri(uri: string): string {
try {
// 處理 obsidian:// 協議
if (uri.startsWith('obsidian://')) {
const match = uri.match(/obsidian:\/\/([^?]+)/);
let vaultName = '';
// 嘗試提取 vault 參數
const vaultMatch = uri.match(/[?&]vault=([^&]+)/);
if (vaultMatch) {
vaultName = decodeURIComponent(vaultMatch[1]);
// 清理 vault 名稱,移除不適合檔名的字符
vaultName = vaultName.replace(/[<>:"/\\|?*]/g, '_');
}
if (match) {
const action = match[1];
const vaultSuffix = vaultName ? ` (${vaultName})` : '';
// 根據不同的 obsidian 動作生成檔名
switch (action) {
case 'open':
return `🌐 Obsidian Open${vaultSuffix}`;
case 'new':
return `🌐 Obsidian New${vaultSuffix}`;
case 'search':
return `🌐 Obsidian Search${vaultSuffix}`;
case 'hook-get-address':
return `🌐 Obsidian Hook${vaultSuffix}`;
default:
return `🌐 Obsidian ${action}${vaultSuffix}`;
}
}
return vaultName ? `🌐 Obsidian Link (${vaultName})` : '🌐 Obsidian Link';
}
// 處理 file:// 協議
if (uri.startsWith('file://')) {
const filename = uri.split('/').pop() || 'Local File';
return `🌐 ${filename}`;
}
// 處理 http/https 協議
if (uri.startsWith('http://') || uri.startsWith('https://')) {
const url = new URL(uri);
let domain = url.hostname;
// 移除 www. 前綴
if (domain.startsWith('www.')) {
domain = domain.substring(4);
}
// 如果有路徑,嘗試提取有意義的部分
if (url.pathname && url.pathname !== '/') {
const pathParts = url.pathname.split('/').filter(part => part.length > 0);
if (pathParts.length > 0) {
const lastPart = pathParts[pathParts.length - 1];
// 如果最後一部分看起來像檔名或有意義的標識符
if (lastPart.length < 50 && !lastPart.includes('?')) {
return `🌐 ${domain} - ${lastPart}`;
}
}
}
return `🌐 ${domain}`;
}
// 其他協議的處理
const protocolMatch = uri.match(/^([^:]+):/);
if (protocolMatch) {
const protocol = protocolMatch[1].toUpperCase();
return `🌐 ${protocol} Link`;
}
// 如果不是標準 URI直接使用前 30 個字符
const cleanUri = uri.replace(/[<>:"/\\|?*]/g, '_').substring(0, 30);
return `🌐 ${cleanUri}`;
} catch (error) {
// 如果解析失敗,使用安全的預設名稱
const cleanUri = uri.replace(/[<>:"/\\|?*]/g, '_').substring(0, 30);
return `🌐 ${cleanUri}`;
}
}
// 創建捷徑檔案
async function createShortcut(gridView: GridView, option: { type: 'mode' | 'folder' | 'file'; value: string; display: string; }) {
async function createShortcut(
gridView: GridView,
option: { type: 'mode' | 'folder' | 'file' | 'search' | 'uri';
value: string;
display: string; }) {
try {
// 生成不重複的檔案名稱
let counter = 0;
let shortcutName = `${option.display}`;
let shortcutName: string;
// 對於 URI 類型,使用特殊的檔名生成邏輯
if (option.type === 'uri') {
shortcutName = generateFilenameFromUri(option.value);
} else {
shortcutName = `${option.display}`;
}
let newPath = `${shortcutName}.md`;
while (gridView.app.vault.getAbstractFileByPath(newPath)) {
counter++;
shortcutName = `${option.display} ${counter}`;
const baseName = option.type === 'uri' ? generateFilenameFromUri(option.value) : option.display;
shortcutName = `${baseName} ${counter}`;
newPath = `${shortcutName}.md`;
}
@ -477,6 +579,12 @@ async function createShortcut(gridView: GridView, option: { type: 'mode' | 'fold
);
frontmatter.type = "file";
frontmatter.redirect = link;
} else if (option.type === 'search') {
frontmatter.type = 'search';
frontmatter.redirect = option.value;
} else if (option.type === 'uri') {
frontmatter.type = 'uri';
frontmatter.redirect = option.value;
}
});
@ -484,6 +592,6 @@ async function createShortcut(gridView: GridView, option: { type: 'mode' | 'fold
} catch (error) {
console.error('Create shortcut error', error);
new Notice(t('Failed to create shortcut'));
new Notice(t('failed_to_create_shortcut'));
}
}

View file

@ -173,7 +173,7 @@ export function renderModePath(gridView: GridView) {
menu.addItem((item) => {
item.setTitle(`${customFolderIcon} ${path.name}`)
.onClick(() => {
gridView.setSource('folder', path.path, true);
gridView.setSource('folder', path.path);
gridView.clearSelection();
});
});
@ -203,7 +203,7 @@ export function renderModePath(gridView: GridView) {
item.setTitle(`${customFolderIcon} ${folder.name}`)
.setIcon('corner-down-right')
.onClick(() => {
gridView.setSource('folder', folder.path, true);
gridView.setSource('folder', folder.path);
gridView.clearSelection();
});
});
@ -213,7 +213,7 @@ export function renderModePath(gridView: GridView) {
menu.showAtMouseEvent(event as MouseEvent);
} else {
gridView.setSource('folder', path.path, true);
gridView.setSource('folder', path.path);
gridView.clearSelection();
}
});
@ -330,7 +330,7 @@ export function renderModePath(gridView: GridView) {
item.setTitle(`${customFolderIcon} ${sf.name}`)
.setIcon('corner-down-right')
.onClick(() => {
gridView.setSource('folder', sf.path, true);
gridView.setSource('folder', sf.path);
gridView.clearSelection();
});
});
@ -401,10 +401,10 @@ export function renderModePath(gridView: GridView) {
.onClick(() => {
gridView.plugin.settings.ignoredFolders.push(folder.path);
gridView.plugin.saveSettings();
setTimeout(() => {
requestAnimationFrame(() => {
// 回上層資料夾
gridView.setSource('folder', parentFolder?.path || '/', true);
}, 100);
gridView.setSource('folder', parentFolder?.path || '/');
});
});
});
} else {
@ -416,10 +416,10 @@ export function renderModePath(gridView: GridView) {
.onClick(() => {
gridView.plugin.settings.ignoredFolders = gridView.plugin.settings.ignoredFolders.filter((path) => path !== folder.path);
gridView.plugin.saveSettings();
setTimeout(() => {
requestAnimationFrame(() => {
// 回上層資料夾
gridView.setSource('folder', parentFolder?.path || '/', true);
}, 100);
gridView.setSource('folder', parentFolder?.path || '/');
});
});
});
}
@ -454,10 +454,10 @@ export function renderModePath(gridView: GridView) {
.onClick(async () => {
if (folder instanceof TFolder) {
await gridView.app.fileManager.trashFile(folder);
setTimeout(() => {
requestAnimationFrame(() => {
// 回上層資料夾
gridView.setSource('folder', parentFolder?.path || '/', true);
}, 100);
gridView.setSource('folder', parentFolder?.path || '/');
});
}
});
});
@ -562,7 +562,7 @@ export function renderModePath(gridView: GridView) {
.setChecked(m.internalName === gridView.sourceMode)
.onClick(() => {
// 切換至選取的自訂模式並重新渲染
gridView.setSource(m.internalName, '', true);
gridView.setSource(m.internalName);
});
});
});
@ -676,7 +676,7 @@ export function renderModePath(gridView: GridView) {
.setChecked(gridView.customOptionIndex === -1)
.onClick(() => {
gridView.customOptionIndex = -1;
gridView.render(true);
gridView.render();
});
});
mode.options!.forEach((opt, idx) => {
@ -686,7 +686,7 @@ export function renderModePath(gridView: GridView) {
.setChecked(idx === gridView.customOptionIndex)
.onClick(() => {
gridView.customOptionIndex = idx;
gridView.render(true);
gridView.render();
});
});
});
@ -703,7 +703,7 @@ export function renderModePath(gridView: GridView) {
new CustomModeModal(gridView.app, gridView.plugin, gridView.plugin.settings.customModes[modeIndex], (result) => {
gridView.plugin.settings.customModes[modeIndex] = result;
gridView.plugin.saveSettings();
gridView.render(true);
gridView.render();
}).open();
});
}
@ -718,11 +718,3 @@ export function renderModePath(gridView: GridView) {
});
}
}
// ----------------------------------------------------
// 搬移建議:
// 1. 先整段複製原始碼貼到此函式底下,並把所有 `gridView.` 前綴改成 `gridView.`。
// 2. 逐步補上缺少的 import 與型別。
// 3. GridView.render 內原本的程式替換為呼叫 renderModePath()。
// 4. 若 render() 需要重新渲染,可在 GridView 內保留 render() 呼叫不變。
// ----------------------------------------------------

View file

@ -257,6 +257,10 @@ export const TRANSLATIONS: Translations = {
'color_purple': '紫色',
'color_pink': '粉色',
'confirm': '確認',
'search_text': '搜尋文字',
'enter_search_text': '輸入搜尋文字',
'enter_uri': '輸入網址',
'enter_uri_placeholder': '輸入網址或 obsidian:// 協議',
'note_attribute_settings': '筆記屬性設定',
'note_title': '筆記標題',
'note_title_desc': '設定此筆記的顯示標題',
@ -535,6 +539,10 @@ export const TRANSLATIONS: Translations = {
'color_purple': 'Purple',
'color_pink': 'Pink',
'confirm': 'Confirm',
'search_text': 'Search Text',
'enter_search_text': 'Enter search text',
'enter_uri': 'Enter URI',
'enter_uri_placeholder': 'Enter URL or obsidian:// protocol',
'note_attribute_settings': 'Note attribute settings',
'note_title': 'Note title',
'note_title_desc': 'Set the display title for this note',
@ -811,6 +819,10 @@ export const TRANSLATIONS: Translations = {
'color_purple': '紫色',
'color_pink': '粉色',
'confirm': '确认',
'search_text': '搜索文字',
'enter_search_text': '输入搜索文字',
'enter_uri': '输入网址',
'enter_uri_placeholder': '输入网址或 obsidian:// 协议',
'note_attribute_settings': '笔记属性设置',
'note_title': '笔记标题',
'note_title_desc': '设置此笔记的显示标题',
@ -1087,6 +1099,10 @@ export const TRANSLATIONS: Translations = {
'color_purple': '紫',
'color_pink': 'ピンク',
'confirm': '確認',
'search_text': '検索テキスト',
'enter_search_text': '検索テキストを入力',
'enter_uri': 'URIを入力',
'enter_uri_placeholder': 'URLまたはobsidian://プロトコルを入力',
'note_attribute_settings': 'ノート属性設定',
'note_title': 'ノートタイトル',
'note_title_desc': 'このノートの表示タイトルを設定',
@ -1364,6 +1380,10 @@ export const TRANSLATIONS: Translations = {
'color_purple': 'Фиолетовый',
'color_pink': 'Розовый',
'confirm': 'Подтвердить',
'search_text': 'Поисковый текст',
'enter_search_text': 'Введите поисковый текст',
'enter_uri': 'Введите URI',
'enter_uri_placeholder': 'Введите URL или протокол obsidian://',
'note_attribute_settings': 'Настройки атрибутов заметки',
'note_title': 'Название заметки',
'note_title_desc': 'Установите отображаемое название для этой заметки',
@ -1640,6 +1660,10 @@ export const TRANSLATIONS: Translations = {
'color_purple': 'Фіолетовий',
'color_pink': 'Рожевий',
'confirm': 'Підтвердити',
'search_text': 'Пошуковий текст',
'enter_search_text': 'Введіть пошуковий текст',
'enter_uri': 'Введіть URI',
'enter_uri_placeholder': 'Введіть URL або протокол obsidian://',
'note_attribute_settings': 'Налаштування атрибутів нотатки',
'note_title': 'Назва нотатки',
'note_title_desc': 'Встановіть назву відображення для цієї нотатки',

View file

@ -1929,3 +1929,51 @@ a.ge-current-folder:hover {
height: 16px;
}
/* --------------------------------------------------------------------------- */
/* 通用輸入框樣式 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;
}
.ge-input-field:focus {
outline: none !important;
}
.ge-input-field::placeholder {
color: var(--text-muted) !important;
opacity: 1 !important;
}
/* 確保不同 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;
}
/* 輸入框容器樣式 */
.ge-input-field-container {
position: relative;
width: 100%;
display: flex;
align-items: center;
}
/* Modal 中的輸入框容器 */
.modal .ge-input-field-container {
margin-bottom: 16px;
display: block;
}

View file

@ -1,3 +1,3 @@
{
"2.8.1": "1.1.0"
"2.8.2": "1.1.0"
}