devon22_obsidian-gridexplorer/main_src.js
2025-02-11 21:38:42 +08:00

1160 lines
No EOL
46 KiB
JavaScript

const { Plugin, ItemView, TFolder, TFile, Setting, Menu, setIcon } = require('obsidian');
// 語系檔案
const TRANSLATIONS = {
'zh-TW': {
// 通知訊息
'BOOKMARKS_PLUGIN_DISABLED': '請先啟用書籤插件',
// 按鈕和標籤
'SORTING': '排序方式',
'REFRESH': '重新整理',
'RESELECT_FOLDER': '重新選擇位置',
'GO_UP': '回上層資料夾',
'NO_BACKLINKS': '沒有反向連結',
'SEARCH': '搜尋',
'SEARCH_PLACEHOLDER': '搜尋關鍵字',
'CANCEL': '取消',
'CLEAR_SEARCH': '清除搜尋',
// 視圖標題
'GRID_VIEW_TITLE': '網格視圖',
'BOOKMARKS_MODE': '書籤',
'FOLDER_MODE': '資料夾',
'SEARCH_RESULTS': '搜尋結果',
'BACKLINKS_MODE': '反向連結',
// 排序選項
'SORT_NAME_ASC': '名稱 (A → Z)',
'SORT_NAME_DESC': '名稱 (Z → A)',
'SORT_MTIME_DESC': '修改時間 (新 → 舊)',
'SORT_MTIME_ASC': '修改時間 (舊 → 新)',
'SORT_CTIME_DESC': '建立時間 (新 → 舊)',
'SORT_CTIME_ASC': '建立時間 (舊 → 新)',
'SORT_RANDOM': '隨機排序',
// 設定
'GRID_VIEW_SETTINGS': '網格視圖設定',
'IGNORED_FOLDERS': '忽略的資料夾',
'IGNORED_FOLDERS_DESC': '在這裡設定不要出現在資料夾選擇器中的資料夾(每行一個資料夾路徑)',
'IGNORED_FOLDERS_PLACEHOLDER': '範例:\n.obsidian\nTemplates',
'DEFAULT_SORT_TYPE': '預設排序模式',
'DEFAULT_SORT_TYPE_DESC': '設定開啟網格視圖時的預設排序方式',
'GRID_ITEM_WIDTH': '網格項目寬度',
'GRID_ITEM_WIDTH_DESC': '設定網格項目的寬度',
'IMAGE_AREA_WIDTH': '圖片區域寬度',
'IMAGE_AREA_WIDTH_DESC': '設定圖片預覽區域的寬度',
'IMAGE_AREA_HEIGHT': '圖片區域高度',
'IMAGE_AREA_HEIGHT_DESC': '設定圖片預覽區域的高度',
// 選擇資料夾對話框
'SELECT_FOLDERS': '選擇資料夾',
'OPEN_GRID_VIEW': '開啟網格視圖',
'OPEN_IN_GRID_VIEW': '在網格視圖中開啟',
'DELETE_NOTE': '刪除筆記',
},
'en': {
// Notifications
'BOOKMARKS_PLUGIN_DISABLED': 'Please enable the Bookmarks plugin first',
// Buttons and Labels
'SORTING': 'Sort by',
'REFRESH': 'Refresh',
'RESELECT_FOLDER': 'Reselect Folder',
'GO_UP': 'Go Up',
'NO_BACKLINKS': 'No Backlinks',
'SEARCH': 'Search',
'SEARCH_PLACEHOLDER': 'Search keyword',
'CANCEL': 'Cancel',
'CLEAR_SEARCH': 'Clear Search',
// View Titles
'GRID_VIEW_TITLE': 'Grid View',
'BOOKMARKS_MODE': 'Bookmarks',
'FOLDER_MODE': 'Folder',
'SEARCH_RESULTS': 'Search Results',
'BACKLINKS_MODE': 'Backlinks',
// Sort Options
'SORT_NAME_ASC': 'Name (A → Z)',
'SORT_NAME_DESC': 'Name (Z → A)',
'SORT_MTIME_DESC': 'Modified Time (New → Old)',
'SORT_MTIME_ASC': 'Modified Time (Old → New)',
'SORT_CTIME_DESC': 'Created Time (New → Old)',
'SORT_CTIME_ASC': 'Created Time (Old → New)',
'SORT_RANDOM': 'Random',
// Settings
'GRID_VIEW_SETTINGS': 'Grid View Settings',
'IGNORED_FOLDERS': 'Ignored Folders',
'IGNORED_FOLDERS_DESC': 'Set folders to be excluded from the folder selector (one path per line)',
'IGNORED_FOLDERS_PLACEHOLDER': 'Example:\n.obsidian\nTemplates',
'DEFAULT_SORT_TYPE': 'Default Sort Type',
'DEFAULT_SORT_TYPE_DESC': 'Set the default sorting method when opening Grid View',
'GRID_ITEM_WIDTH': 'Grid Item Width',
'GRID_ITEM_WIDTH_DESC': 'Set the width of grid items',
'IMAGE_AREA_WIDTH': 'Image Area Width',
'IMAGE_AREA_WIDTH_DESC': 'Set the width of the image preview area',
'IMAGE_AREA_HEIGHT': 'Image Area Height',
'IMAGE_AREA_HEIGHT_DESC': 'Set the height of the image preview area',
// Folder Selection Dialog
'SELECT_FOLDERS': 'Select Folder',
'OPEN_GRID_VIEW': 'Open Grid View',
'OPEN_IN_GRID_VIEW': 'Open in Grid View',
'DELETE_NOTE': 'Delete Note',
},
'zh': {
// 通知信息
'BOOKMARKS_PLUGIN_DISABLED': '请先启用书签插件',
// 按钮和标签
'SORTING': '排序方式',
'REFRESH': '刷新',
'RESELECT_FOLDER': '重新选择位置',
'GO_UP': '回上层文件夹',
'NO_BACKLINKS': '没有反向链接',
'SEARCH': '搜索',
'SEARCH_PLACEHOLDER': '搜索关键字',
'CANCEL': '取消',
'CLEAR_SEARCH': '清除搜索',
// 视图标题
'GRID_VIEW_TITLE': '网格视图',
'BOOKMARKS_MODE': '书签',
'FOLDER_MODE': '文件夹',
'SEARCH_RESULTS': '搜索结果',
'BACKLINKS_MODE': '反向链接',
// 排序选项
'SORT_NAME_ASC': '名称 (A → Z)',
'SORT_NAME_DESC': '名称 (Z → A)',
'SORT_MTIME_DESC': '修改时间 (新 → 旧)',
'SORT_MTIME_ASC': '修改时间 (旧 → 新)',
'SORT_CTIME_DESC': '建立时间 (新 → 旧)',
'SORT_CTIME_ASC': '建立时间 (旧 → 新)',
'SORT_RANDOM': '随机排序',
// 设置
'GRID_VIEW_SETTINGS': '网格视图设置',
'IGNORED_FOLDERS': '忽略的文件夹',
'IGNORED_FOLDERS_DESC': '在這裡设置不要出现在文件夹选择器中的文件夹(每行一个文件夹路径)',
'IGNORED_FOLDERS_PLACEHOLDER': '範例:\n.obsidian\nTemplates',
'DEFAULT_SORT_TYPE': '預设排序模式',
'DEFAULT_SORT_TYPE_DESC': '设置开启网格视图时的預设排序方式',
'GRID_ITEM_WIDTH': '网格项目宽度',
'GRID_ITEM_WIDTH_DESC': '设置网格项目的宽度',
'IMAGE_AREA_WIDTH': '圖片區域寬度',
'IMAGE_AREA_WIDTH_DESC': '设置圖片預覽區域的寬度',
'IMAGE_AREA_HEIGHT': '圖片區域高度',
'IMAGE_AREA_HEIGHT_DESC': '设置圖片預覽區域的高度',
// 选择资料夹对话框
'SELECT_FOLDERS': '选择文件夹',
'OPEN_GRID_VIEW': '开启网格视图',
'OPEN_IN_GRID_VIEW': '在网格视图中开启',
'DELETE_NOTE': '删除笔记',
},
'ja': {
// 通知メッジ
'BOOKMARKS_PLUGIN_DISABLED': 'ブックマークプラグインを有効にしてください',
// ボタンとラベル
'SORTING': 'ソート',
'REFRESH': 'リフレッシュ',
'RESELECT_FOLDER': 'フォルダを再選択',
'GO_UP': '上へ',
'NO_BACKLINKS': 'バックリンクはありません',
'SEARCH': '検索',
'SEARCH_PLACEHOLDER': '検索キーワード',
'CANCEL': 'キャンセル',
'CLEAR_SEARCH': '検索をクリア',
// ビュータイトル
'GRID_VIEW_TITLE': 'グリッドビュー',
'BOOKMARKS_MODE': 'ブックマーク',
'FOLDER_MODE': 'フォルダ',
'SEARCH_RESULTS': '検索結果',
'BACKLINKS_MODE': 'バックリンク',
// ソートオプション
'SORT_NAME_ASC': '名前 (A → Z)',
'SORT_NAME_DESC': '名前 (Z → A)',
'SORT_MTIME_DESC': '変更時間 (新 → 旧)',
'SORT_MTIME_ASC': '変更時間 (旧 → 新)',
'SORT_CTIME_DESC': '作成時間 (新 → 旧)',
'SORT_CTIME_ASC': '作成時間 (旧 → 新)',
'SORT_RANDOM': 'ランダム',
// 設定
'GRID_VIEW_SETTINGS': 'グリッドビューセッティング',
'IGNORED_FOLDERS': '無視するフォルダ',
'IGNORED_FOLDERS_DESC': 'フォルダ選択器から除外するフォルダを設定します (1 行に 1 つのフォルダパス)',
'IGNORED_FOLDERS_PLACEHOLDER': '例:\n.obsidian\nTemplates',
'DEFAULT_SORT_TYPE': 'デフォルトのソートタイプ',
'DEFAULT_SORT_TYPE_DESC': 'グリッドビューを開くときのデフォルトのソート方法を設定します',
'GRID_ITEM_WIDTH': 'グリッドアイテムの幅',
'GRID_ITEM_WIDTH_DESC': 'グリッドアイテムの幅を設定します',
'IMAGE_AREA_WIDTH': '画像エリア幅',
'IMAGE_AREA_WIDTH_DESC': '画像プレビュー領域の幅を設定します',
'IMAGE_AREA_HEIGHT': '画像エリア高さ',
'IMAGE_AREA_HEIGHT_DESC': '画像プレビュー領域の高さを設定します',
// フォルダ選択ダイアログ
'SELECT_FOLDERS': 'フォルダを選択',
'OPEN_GRID_VIEW': 'グリッドビューを開く',
'OPEN_IN_GRID_VIEW': 'グリッドビューで開く',
'DELETE_NOTE': 'ノートを削除',
},
};
// 全域翻譯函式
function t(key) {
const langSetting = window.localStorage.getItem('language');
const lang = TRANSLATIONS[langSetting] || TRANSLATIONS['en'];
return lang[key] || key;
}
// 處理媒體連結
function processMediaLink(app, linkText) {
// 處理 Obsidian 內部連結 ![[file]]
const internalMatch = linkText.match(/!\[\[(.*?)\]\]/);
if (internalMatch) {
const file = app.metadataCache.getFirstLinkpathDest(internalMatch[1], '');
if (file) {
return app.vault.getResourcePath(file);
}
}
// 處理標準 Markdown 連結 ![alt](path)
const markdownMatch = linkText.match(/!\[(.*?)\]\((.*?)\)/);
if (markdownMatch) {
const url = markdownMatch[2];
if (url.startsWith('http')) {
return url;
} else {
const file = app.metadataCache.getFirstLinkpathDest(url, '');
if (!file) {
const fileByPath = app.vault.getAbstractFileByPath(url);
if (fileByPath) {
return app.vault.getResourcePath(fileByPath);
}
} else {
return app.vault.getResourcePath(file);
}
}
}
return null;
}
// 尋找筆記中的第一張圖片
async function findFirstImageInNote(app, file) {
try {
const content = await app.vault.read(file);
let firstImage = null;
let firstImagePosition = Infinity;
// 檢查 ![[image.jpg]] 格式
const internalMatch = content.match(/!\[\[(.*?\.(?:jpg|jpeg|png|gif|webp))\]\]/i);
if (internalMatch) {
firstImage = internalMatch[0];
firstImagePosition = content.indexOf(internalMatch[0]);
}
// 檢查 ![alt](path) 格式,支援一般路徑和帶 format 參數的 URL
const markdownMatch = content.match(/!\[.*?\]\((.*?(?:\.(?:jpg|jpeg|png|gif|webp)|format=(?:jpg|jpeg|png|gif|webp)).*?)\)/i);
if (markdownMatch) {
const markdownPosition = content.indexOf(markdownMatch[0]);
if (markdownPosition < firstImagePosition) {
firstImage = markdownMatch[0];
firstImagePosition = markdownPosition;
}
}
if (firstImage) {
return processMediaLink(app, firstImage);
}
return null;
} catch (error) {
console.error('Error finding image in note:', error);
return null;
}
}
// 定義網格視圖
class GridView extends ItemView {
constructor(leaf, plugin) {
super(leaf);
this.plugin = plugin;
this.containerEl.addClass('grid-view-container');
this.sourceMode = ''; // 預設為書籤模式
this.sourcePath = null; // 用於資料夾模式的路徑
this.sortType = this.plugin.settings.defaultSortType; // 使用設定中的預設排序模式
this.searchQuery = ''; // 搜尋關鍵字
// 註冊檔案變更監聽器
this.registerFileWatcher();
}
getViewType() {
return 'grid-view';
}
getIcon() {
return 'grid';
}
getDisplayText() {
if (this.sourceMode === '') {
return t('GRID_VIEW_TITLE');
} else if (this.sourceMode === 'bookmarks') {
return t('BOOKMARKS_MODE');
} else if (this.sourceMode === 'folder') {
return this.sourcePath;
} else if (this.sourceMode === 'search') {
return t('SEARCH_RESULTS');
} else if (this.sourceMode === 'backlinks') {
return t('BACKLINKS_MODE');
}
}
async getFiles() {
if (this.sourceMode === 'folder' && this.sourcePath) {
// 獲取指定資料夾內的所有 Markdown 檔案
const folder = this.app.vault.getAbstractFileByPath(this.sourcePath);
if (folder instanceof TFolder) {
// 只取得當前資料夾中的 Markdown 檔案,不包含子資料夾
const files = folder.children.filter(file => file.extension === 'md');
return this.sortFiles(files);
}
return [];
} else if (this.sourceMode === 'search') {
// 搜尋模式:使用 Obsidian 的搜尋功能
const globalSearchPlugin = this.app.internalPlugins.getPluginById('global-search');
if (globalSearchPlugin?.instance) {
const searchLeaf = this.app.workspace.getLeavesOfType('search')[0];
if (searchLeaf && searchLeaf.view && searchLeaf.view.dom) {
const resultDomLookup = searchLeaf.view.dom.resultDomLookup;
if (resultDomLookup) {
const files = Array.from(resultDomLookup.keys())
.filter(file => file.extension === 'md');
return this.sortFiles(files);
}
}
}
return [];
} else if (this.sourceMode === 'backlinks') {
// 反向連結模式:找出所有引用當前筆記的檔案
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) {
return [];
}
const backlinks = new Set();
// 使用 resolvedLinks 來找出反向連結
const resolvedLinks = this.app.metadataCache.resolvedLinks;
for (const [sourcePath, links] of Object.entries(resolvedLinks)) {
if (Object.keys(links).includes(activeFile.path)) {
const sourceFile = this.app.vault.getAbstractFileByPath(sourcePath);
if (sourceFile && sourceFile.extension === 'md') {
backlinks.add(sourceFile);
}
}
}
return this.sortFiles(Array.from(backlinks));
} else {
// 書籤模式
const bookmarksPlugin = this.app.internalPlugins.plugins.bookmarks;
if (!bookmarksPlugin?.enabled) {
return [];
}
const bookmarks = bookmarksPlugin.instance.items;
const bookmarkedFiles = new Set();
const processBookmarkItem = (item) => {
if (item.type === 'file') {
const file = this.app.vault.getAbstractFileByPath(item.path);
if (file && file.extension === 'md') {
bookmarkedFiles.add(file);
}
} else if (item.type === 'group' && item.items) {
item.items.forEach(processBookmarkItem);
}
};
bookmarks.forEach(processBookmarkItem);
return Array.from(bookmarkedFiles);
}
}
sortFiles(files) {
if (this.sortType === 'name-asc') {
return files.sort((a, b) => a.basename.localeCompare(b.basename));
} else if (this.sortType === 'name-desc') {
return files.sort((a, b) => b.basename.localeCompare(a.basename));
} else if (this.sortType === 'mtime-desc') {
return files.sort((a, b) => b.stat.mtime - a.stat.mtime);
} else if (this.sortType === 'mtime-asc') {
return files.sort((a, b) => a.stat.mtime - b.stat.mtime);
} else if (this.sortType === 'ctime-desc') {
return files.sort((a, b) => b.stat.ctime - a.stat.ctime);
} else if (this.sortType === 'ctime-asc') {
return files.sort((a, b) => a.stat.ctime - b.stat.ctime);
} else if (this.sortType === 'random') {
return files.sort(() => Math.random() - 0.5);
}
return files;
}
setSource(mode, path = null) {
this.sourceMode = mode;
this.sourcePath = path;
this.render();
}
async render() {
// 儲存當前捲動位置
const scrollContainer = this.containerEl.querySelector('.view-content');
const scrollTop = scrollContainer ? scrollContainer.scrollTop : 0;
// 清空整個容器
this.containerEl.empty();
// 創建頂部按鈕區域
const headerButtonsDiv = this.containerEl.createDiv('header-buttons');
// 添加回上層按鈕(僅在資料夾模式且不在根目錄時顯示)
if (this.sourceMode === 'folder' && this.sourcePath !== '/') {
const upButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('GO_UP') } });
upButton.addEventListener('click', () => {
const parentPath = this.sourcePath.split('/').slice(0, -1).join('/') || '/';
this.setSource('folder', parentPath);
});
setIcon(upButton, 'arrow-up');
}
// 添加重新整理按鈕
const refreshButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('REFRESH') } });
refreshButton.addEventListener('click', () => {
this.render();
});
setIcon(refreshButton, 'refresh-ccw');
// 添加重新選擇資料夾按鈕
const reselectButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('RESELECT_FOLDER') } });
reselectButton.addEventListener('click', () => {
showFolderSelectionModal(this.app, this.plugin, this);
});
setIcon(reselectButton, "folder");
// 添加排序按鈕
if (this.sourceMode !== 'bookmarks') {
const sortButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('SORTING') } });
sortButton.addEventListener('click', (evt) => {
const menu = new Menu();
const sortOptions = [
{ value: 'name-asc', label: t('SORT_NAME_ASC'), icon: 'a-arrow-up' },
{ value: 'name-desc', label: t('SORT_NAME_DESC'), icon: 'a-arrow-down' },
{ value: 'mtime-desc', label: t('SORT_MTIME_DESC'), icon: 'clock' },
{ value: 'mtime-asc', label: t('SORT_MTIME_ASC'), icon: 'clock' },
{ value: 'ctime-desc', label: t('SORT_CTIME_DESC'), icon: 'calendar' },
{ value: 'ctime-asc', label: t('SORT_CTIME_ASC'), icon: 'calendar' },
{ value: 'random', label: t('SORT_RANDOM'), icon: 'dice' },
];
sortOptions.forEach(option => {
menu.addItem((item) => {
item
.setTitle(option.label)
.setIcon(option.icon)
.setChecked(this.sortType === option.value)
.onClick(() => {
this.sortType = option.value;
this.render();
});
});
});
menu.showAtMouseEvent(evt);
});
setIcon(sortButton, 'arrow-down-up');
}
// 添加搜尋按鈕
const searchButtonContainer = headerButtonsDiv.createDiv('search-button-container');
// 搜尋按鈕
const searchButton = searchButtonContainer.createEl('button', {
attr: {
'aria-label': this.searchQuery ? t('CLEAR_SEARCH') : t('SEARCH'),
'class': this.searchQuery ? 'active clear-search' : ''
}
});
searchButton.addEventListener('click', () => {
if (this.searchQuery) {
// 如果已經有搜尋關鍵字,則清除搜尋
this.searchQuery = '';
this.render();
} else {
// 如果沒有搜尋關鍵字,則顯示搜尋對話框
this.showSearchModal();
}
});
setIcon(searchButton, this.searchQuery ? 'x' : 'search');
// 如果有搜尋關鍵字,顯示搜尋文字
if (this.searchQuery) {
const searchText = searchButtonContainer.createSpan('search-text');
searchText.textContent = this.searchQuery;
}
// 創建內容區域
const contentEl = this.containerEl.createDiv('view-content');
// 重新渲染內容
await this.grid_render();
this.leaf.updateHeader()
// 恢復捲動位置
if (scrollContainer) {
contentEl.scrollTop = scrollTop;
}
}
async grid_render() {
const container = this.containerEl.children[1];
container.empty();
container.addClass('grid-container');
container.style.setProperty('--grid-item-width', this.plugin.settings.gridItemWidth + 'px');
container.style.setProperty('--image-area-width', this.plugin.settings.imageAreaWidth + 'px');
container.style.setProperty('--image-area-height', this.plugin.settings.imageAreaHeight + 'px');
// 如果是書籤模式且書籤插件未啟用,顯示提示
if (this.sourceMode === 'bookmarks' && !this.app.internalPlugins.plugins.bookmarks?.enabled) {
new Notice(t('BOOKMARKS_PLUGIN_DISABLED'));
return;
}
// 如果是反向連結模式,但沒有活動中的檔案
if (this.sourceMode === 'backlinks' && !this.app.workspace.getActiveFile()) {
new Notice(t('NO_BACKLINKS'));
return;
}
// 如果是資料夾模式,先顯示所有子資料夾
if (this.sourceMode === 'folder') {
const currentFolder = this.app.vault.getAbstractFileByPath(this.sourcePath || '/');
if (currentFolder instanceof TFolder) {
// 只取得當前資料夾中的 Markdown 檔案,不包含子資料夾
const subfolders = currentFolder.children
.filter(child => {
if (!(child instanceof TFolder)) return false;
// 檢查資料夾是否在忽略清單中
return !this.plugin.settings.ignoredFolders.some(
ignoredPath => child.path === ignoredPath || child.path.startsWith(ignoredPath + '/')
);
})
.sort((a, b) => a.name.localeCompare(b.name));
// 如果有搜尋關鍵字,過濾資料夾
const filteredSubfolders = this.searchQuery
? subfolders.filter(folder =>
folder.name.toLowerCase().includes(this.searchQuery.toLowerCase()))
: subfolders;
for (const folder of filteredSubfolders) {
const folderEl = container.createDiv('grid-item folder-item');
const contentArea = folderEl.createDiv('content-area');
const titleEl = contentArea.createEl('h3');
titleEl.textContent = `📁 ${folder.name}`;
// 點擊時進入子資料夾
folderEl.addEventListener('click', () => {
this.setSource('folder', folder.path);
});
}
}
}
let files = [];
if (this.searchQuery) {
// 取得 vault 中所有的 Markdown 檔案
const allMarkdownFiles = this.app.vault.getFiles().filter(file => file.extension === 'md');
// 根據搜尋關鍵字進行過濾(不分大小寫)
const lowerCaseSearchQuery = this.searchQuery.toLowerCase();
// 使用 Promise.all 來非同步地讀取所有檔案內容
await Promise.all(
allMarkdownFiles.map(async file => {
const fileName = file.name.toLowerCase();
const content = (await this.app.vault.cachedRead(file)).toLowerCase();
if (fileName.includes(lowerCaseSearchQuery) || content.includes(lowerCaseSearchQuery)) {
files.push(file);
}
})
);
// 根據設定的排序方式排序檔案
files = this.sortFiles(files);
} else {
// 獲取檔案列表並根據搜尋關鍵字過濾
files = await this.getFiles();
}
// 創建 Intersection Observer
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(async entry => {
if (entry.isIntersecting) {
const noteEl = entry.target;
const file = noteEl.file;
// 載入預覽內容
const contentArea = noteEl.querySelector('.content-area');
if (!contentArea.hasAttribute('data-loaded')) {
const content = await this.app.vault.cachedRead(file);
// 移除 frontmatter 區域,並移除內部連結和圖片連結
const contentWithoutFrontmatter = content.replace(/^---\n[\s\S]*?\n---\n|`{3}[\s\S]*?`{3}|<!--[\s\S]*?-->|(!?\[([^\]]*)\]\(([^)]+)\))|!?\[\[([^\]]+)\]\]/g, '');
// 只取前100個字符作為預覽
const preview = contentWithoutFrontmatter.slice(0, 100) + (contentWithoutFrontmatter.length > 100 ? '...' : '');
// 創建預覽內容
const contentEl = contentArea.createEl('p', { text: preview.trim() });
contentArea.setAttribute('data-loaded', 'true');
}
// 載入圖片
const imageArea = noteEl.querySelector('.image-area');
if (!imageArea.hasAttribute('data-loaded')) {
const imageUrl = await findFirstImageInNote(this.app, file);
if (imageUrl) {
const img = imageArea.createEl('img');
img.src = imageUrl;
imageArea.setAttribute('data-loaded', 'true');
} else {
// 如果沒有圖片,移除圖片區域
imageArea.remove();
}
}
// 一旦載入完成,就不需要再觀察這個元素
observer.unobserve(noteEl);
}
});
}, {
root: container,
rootMargin: '50px', // 預先載入視窗外 50px 的內容
threshold: 0.1
});
// 顯示筆記
for (const file of files) {
const noteEl = container.createDiv('grid-item');
noteEl.file = file; // 儲存檔案引用以供後續使用
// 創建左側內容區,但先只放標題
const contentArea = noteEl.createDiv('content-area');
// 創建標題(立即載入)
const titleEl = contentArea.createEl('h3', { text: file.basename });
// 創建圖片區域,但先不載入圖片
const imageArea = noteEl.createDiv('image-area');
// 開始觀察這個筆記元素
observer.observe(noteEl);
// 點擊時開啟筆記
noteEl.addEventListener('click', () => {
this.app.workspace.getLeaf().openFile(file);
});
// 添加右鍵選單
noteEl.addEventListener('contextmenu', (event) => {
event.preventDefault();
const menu = new Menu();
menu.addItem((item) => {
item
.setTitle(t('DELETE_NOTE'))
.setIcon('trash')
.onClick(async () => {
await this.app.vault.trash(file, false);
});
});
menu.showAtMouseEvent(event);
});
}
}
// 顯示搜尋 modal
showSearchModal() {
const { Modal } = require('obsidian');
class SearchModal extends Modal {
constructor(app, gridView) {
super(app);
this.gridView = gridView;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl('h2', { text: t('SEARCH') });
// 創建搜尋輸入框
const searchContainer = contentEl.createDiv('search-container');
const searchInput = searchContainer.createEl('input', {
type: 'text',
value: '',
placeholder: t('SEARCH_PLACEHOLDER')
});
// 創建按鈕容器
const buttonContainer = contentEl.createDiv('button-container');
// 創建搜尋按鈕
const searchButton = buttonContainer.createEl('button', {
text: t('SEARCH')
});
// 創建取消按鈕
const cancelButton = buttonContainer.createEl('button', {
text: t('CANCEL')
});
// 綁定搜尋事件
const performSearch = () => {
this.gridView.searchQuery = searchInput.value;
this.gridView.render();
this.close();
};
searchButton.addEventListener('click', performSearch);
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
performSearch();
}
});
cancelButton.addEventListener('click', () => {
this.close();
});
// 自動聚焦到搜尋輸入框
searchInput.focus();
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
new SearchModal(this.app, this).open();
}
// 保存視圖狀態
getState() {
return {
type: 'grid-view',
state: {
sourceMode: this.sourceMode,
sourcePath: this.sourcePath,
sortType: this.sortType,
searchQuery: this.searchQuery
}
};
}
// 讀取視圖狀態
setState(state) {
if (state.state) {
this.sourceMode = state.state.sourceMode || '';
this.sourcePath = state.state.sourcePath || null;
this.sortType = state.state.sortType || 'mtime-desc';
this.searchQuery = state.state.searchQuery || '';
this.render();
}
}
// 註冊檔案監聽器
registerFileWatcher() {
['create', 'modify', 'delete'].forEach(eventName => {
this.registerEvent(
this.app.vault.on(eventName, (file) => {
if (this.sourceMode === 'folder' && this.sourcePath && this.searchQuery === '') {
const fileDirPath = file.path.split('/').slice(0, -1).join('/') || '/';
if (fileDirPath === this.sourcePath) {
this.render();
}
} else {
this.render();
}
})
);
});
//更名及檔案移動
this.registerEvent(
this.app.vault.on('rename', (file, oldPath) => {
if (this.sourceMode === 'folder' && this.sourcePath && this.searchQuery === '') {
const fileDirPath = file.path.split('/').slice(0, -1).join('/') || '/';
const oldDirPath = oldPath.split('/').slice(0, -1).join('/') || '/';
if (fileDirPath === this.sourcePath || oldDirPath === this.sourcePath) {
this.render();
}
} else {
this.render();
}
})
);
// 處理書籤變更
this.registerEvent(
this.app.internalPlugins.plugins.bookmarks.instance.on('changed', () => {
if (this.sourceMode === 'bookmarks') {
this.render();
}
})
);
}
}
// 預設設定
const DEFAULT_SETTINGS = {
ignoredFolders: [],
defaultSortType: 'mtime-desc', // 預設排序模式:修改時間倒序
gridItemWidth: 300, // 網格項目寬度,預設 300
imageAreaWidth: 100, // 圖片區域寬度,預設 100
imageAreaHeight: 100 // 圖片區域高度,預設 100
};
// 設定頁面類別
class GridExplorerSettingTab extends require('obsidian').PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: t('GRID_VIEW_SETTINGS') });
// 忽略的資料夾設定
new Setting(containerEl)
.setName(t('IGNORED_FOLDERS'))
.setDesc(t('IGNORED_FOLDERS_DESC'))
.addTextArea(text => text
.setPlaceholder(t('IGNORED_FOLDERS_PLACEHOLDER'))
.setValue(this.plugin.settings.ignoredFolders.join('\n'))
.onChange(async (value) => {
// 將文字區域的內容轉換為陣列,並過濾掉空行
this.plugin.settings.ignoredFolders = value
.split('\n')
.map(folder => folder.trim())
.filter(folder => folder.length > 0);
await this.plugin.saveSettings();
}));
// 預設排序模式設定
new Setting(containerEl)
.setName(t('DEFAULT_SORT_TYPE'))
.setDesc(t('DEFAULT_SORT_TYPE_DESC'))
.addDropdown(dropdown => dropdown
.addOption('name-asc', t('SORT_NAME_ASC'))
.addOption('name-desc', t('SORT_NAME_DESC'))
.addOption('mtime-desc', t('SORT_MTIME_DESC'))
.addOption('mtime-asc', t('SORT_MTIME_ASC'))
.addOption('ctime-desc', t('SORT_CTIME_DESC'))
.addOption('ctime-asc', t('SORT_CTIME_ASC'))
.addOption('random', t('SORT_RANDOM'))
.setValue(this.plugin.settings.defaultSortType)
.onChange(async (value) => {
this.plugin.settings.defaultSortType = value;
await this.plugin.saveSettings();
}));
// 網格項目寬度設定
new Setting(containerEl)
.setName(t('GRID_ITEM_WIDTH'))
.setDesc(t('GRID_ITEM_WIDTH_DESC'))
.addSlider(slider => slider
.setLimits(200, 600, 50)
.setValue(this.plugin.settings.gridItemWidth)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.gridItemWidth = value;
await this.plugin.saveSettings();
}));
// 圖片區域寬度設定
new Setting(containerEl)
.setName(t('IMAGE_AREA_WIDTH'))
.setDesc(t('IMAGE_AREA_WIDTH_DESC'))
.addSlider(slider => slider
.setLimits(50, 300, 10)
.setValue(this.plugin.settings.imageAreaWidth)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.imageAreaWidth = value;
await this.plugin.saveSettings();
}));
// 圖片區域高度設定
new Setting(containerEl)
.setName(t('IMAGE_AREA_HEIGHT'))
.setDesc(t('IMAGE_AREA_HEIGHT_DESC'))
.addSlider(slider => slider
.setLimits(50, 300, 10)
.setValue(this.plugin.settings.imageAreaHeight)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.imageAreaHeight = value;
await this.plugin.saveSettings();
}));
}
}
// 顯示資料夾選擇 modal
async function showFolderSelectionModal(app, plugin, activeView = null) {
const { Modal, TFolder } = require('obsidian');
class FolderSelectionModal extends Modal {
constructor(app, plugin, activeView) {
super(app);
this.plugin = plugin;
this.activeView = activeView;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl('h2', { text: t('SELECT_FOLDERS') });
// 建立書籤選項
const bookmarksPlugin = this.app.internalPlugins.plugins.bookmarks;
if (bookmarksPlugin?.enabled) {
const bookmarkOption = contentEl.createEl('div', {
cls: 'grid-view-folder-option',
text: `📑 ${t('BOOKMARKS_MODE')}`
});
bookmarkOption.style.cursor = 'pointer';
bookmarkOption.style.padding = '8px';
bookmarkOption.style.marginBottom = '8px';
bookmarkOption.style.border = '1px solid var(--background-modifier-border)';
bookmarkOption.style.borderRadius = '4px';
bookmarkOption.addEventListener('click', () => {
if (this.activeView) {
this.activeView.setSource('bookmarks');
} else {
this.plugin.activateView('bookmarks');
}
this.close();
});
bookmarkOption.addEventListener('mouseenter', () => {
bookmarkOption.style.backgroundColor = 'var(--background-modifier-hover)';
});
bookmarkOption.addEventListener('mouseleave', () => {
bookmarkOption.style.backgroundColor = '';
});
}
// 建立搜尋結果選項
const searchLeaf = this.app.workspace.getLeavesOfType('search')[0];
if (searchLeaf) {
const searchView = searchLeaf.view;
const searchInput = searchView.searchComponent ? searchView.searchComponent.inputEl : null;
if(searchInput) {
if (searchInput.value.trim().length > 0) {
const searchOption = contentEl.createEl('div', {
cls: 'grid-view-folder-option',
text: `🔍 ${t('SEARCH_RESULTS')}: ${searchInput.value}`
});
searchOption.style.cursor = 'pointer';
searchOption.style.padding = '8px';
searchOption.style.marginBottom = '8px';
searchOption.style.border = '1px solid var(--background-modifier-border)';
searchOption.style.borderRadius = '4px';
searchOption.addEventListener('click', () => {
if (this.activeView) {
this.activeView.setSource('search');
} else {
this.plugin.activateView('search');
}
this.close();
});
searchOption.addEventListener('mouseenter', () => {
searchOption.style.backgroundColor = 'var(--background-modifier-hover)';
});
searchOption.addEventListener('mouseleave', () => {
searchOption.style.backgroundColor = '';
});
}
}
}
// 建立反向連結選項
const activeFile = this.app.workspace.getActiveFile();
if (activeFile) {
const activeFileName = activeFile ? `: ${activeFile.basename}` : '';
const backlinksOption = contentEl.createEl('div', {
cls: 'grid-view-folder-option',
text: `🔗 ${t('BACKLINKS_MODE')}${activeFileName}`
});
backlinksOption.style.cursor = 'pointer';
backlinksOption.style.padding = '8px';
backlinksOption.style.marginBottom = '8px';
backlinksOption.style.border = '1px solid var(--background-modifier-border)';
backlinksOption.style.borderRadius = '4px';
backlinksOption.addEventListener('click', () => {
if (this.activeView) {
this.activeView.setSource('backlinks');
} else {
this.plugin.activateView('backlinks');
}
this.close();
});
backlinksOption.addEventListener('mouseenter', () => {
backlinksOption.style.backgroundColor = 'var(--background-modifier-hover)';
});
backlinksOption.addEventListener('mouseleave', () => {
backlinksOption.style.backgroundColor = '';
});
}
// 取得所有資料夾(排除被忽略的資料夾)
const folders = app.vault.getAllLoadedFiles()
.filter(file => {
if (!(file instanceof TFolder)) return false;
// 檢查資料夾是否在忽略清單中
return !this.plugin.settings.ignoredFolders.some(
ignoredPath => file.path === ignoredPath || file.path.startsWith(ignoredPath + '/')
);
})
.sort((a, b) => a.path.localeCompare(b.path));
// 建立資料夾選項
folders.forEach(folder => {
const folderOption = contentEl.createEl('div', {
cls: 'grid-view-folder-option',
text: `📁 ${folder.path || '/'}`
});
folderOption.style.cursor = 'pointer';
folderOption.style.padding = '8px';
folderOption.style.marginBottom = '8px';
folderOption.style.border = '1px solid var(--background-modifier-border)';
folderOption.style.borderRadius = '4px';
folderOption.addEventListener('click', () => {
if (this.activeView) {
this.activeView.setSource('folder', folder.path);
} else {
this.plugin.activateView('folder', folder.path);
}
this.close();
});
folderOption.addEventListener('mouseenter', () => {
folderOption.style.backgroundColor = 'var(--background-modifier-hover)';
});
folderOption.addEventListener('mouseleave', () => {
folderOption.style.backgroundColor = '';
});
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
new FolderSelectionModal(app, plugin, activeView).open();
}
// 主插件類別
module.exports = class GridExplorerPlugin extends Plugin {
async onload() {
// 載入設定
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
// 註冊視圖類型
this.registerView(
'grid-view',
(leaf) => new GridView(leaf, this)
);
// 添加命令以開啟網格視圖選擇 modal
this.addCommand({
id: 'open-grid-view',
name: t('OPEN_GRID_VIEW'),
callback: () => {
showFolderSelectionModal(this.app, this);
}
});
this.addRibbonIcon('grid', t('OPEN_GRID_VIEW'), () => {
showFolderSelectionModal(this.app, this);
});
// 註冊設定頁面
this.addSettingTab(new GridExplorerSettingTab(this.app, this));
// 註冊資料夾的右鍵選單
this.registerEvent(
this.app.workspace.on('file-menu', (menu, file) => {
if (file instanceof TFolder) {
menu.addItem((item) => {
item
.setTitle(t('OPEN_IN_GRID_VIEW'))
.setIcon('grid')
.onClick(() => {
this.activateView('folder', file.path);
});
});
}
})
);
}
// 保存設定
async saveSettings() {
await this.saveData(this.settings);
}
async onunload() {
this.app.workspace.detachLeavesOfType('grid-view');
}
async activateView(mode = 'bookmarks', path = null) {
const { workspace } = this.app;
let leaf = null;
const leaves = workspace.getLeavesOfType('grid-view');
// 在新分頁中開啟
leaf = workspace.getLeaf('tab');
leaf.setViewState({ type: 'grid-view', active: true });
// 設定資料來源
if (leaf.view instanceof GridView) {
leaf.view.setSource(mode, path);
}
// 確保視圖是活躍的
workspace.revealLeaf(leaf);
}
}