mirror of
https://github.com/devon22/obsidian-gridexplorer.git
synced 2026-07-22 12:00:25 +00:00
2.8.2
This commit is contained in:
parent
05dc7122e3
commit
b46998f5d0
18 changed files with 702 additions and 344 deletions
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
305
src/GridView.ts
305
src/GridView.ts
|
|
@ -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 || '/';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
170
src/main.ts
170
src/main.ts
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
102
src/modal/inputModal.ts
Normal 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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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')}` },
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
@ -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() 呼叫不變。
|
||||
// ----------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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': 'Встановіть назву відображення для цієї нотатки',
|
||||
|
|
|
|||
48
styles.css
48
styles.css
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"2.8.1": "1.1.0"
|
||||
"2.8.2": "1.1.0"
|
||||
}
|
||||
Loading…
Reference in a new issue