This commit is contained in:
Devon22 2025-04-12 00:08:46 +08:00
parent bfb847d266
commit 0d78177e79
11 changed files with 566 additions and 439 deletions

34
main.ts
View file

@ -6,7 +6,6 @@ import { GallerySettings, DEFAULT_SETTINGS, GridExplorerSettingTab } from './src
import { t } from './src/translations';
import { updateCustomDocumentExtensions } from './src/fileUtils';
// 插件類型定義
export default class GridExplorerPlugin extends Plugin {
settings: GallerySettings;
statusBarItem: HTMLElement;
@ -152,6 +151,39 @@ export default class GridExplorerPlugin extends Plugin {
}
})
);
// 註冊編輯器選單
this.registerEvent(
this.app.workspace.on('editor-menu', (menu: Menu, editor) => {
if (editor.somethingSelected()) {
const selectedText = editor.getSelection();
// 截斷過長的文字,最多顯示 20 個字元
const truncatedText = selectedText.length > 20 ? selectedText.substring(0, 20) + '...' : selectedText;
const menuItemTitle = t('search_selection_in_grid_view').replace('...', `${truncatedText}`); // 假設翻譯中有...代表要替換的部分,或者直接格式化
menu.addItem(item => {
item
.setTitle(menuItemTitle)
.setIcon('search')
.setSection?.("view")
.onClick(async () => {
const selectedText = editor.getSelection();
// 取得或啟用 GridView
const view = await this.activateView('folder','/');
if (view instanceof GridView) {
// 設定搜尋模式和關鍵字
view.searchQuery = selectedText;
// 設定搜尋範圍 (預設搜尋所有檔案,不含媒體)
view.searchAllFiles = true;
view.searchMediaFiles = false;
// 重新渲染視圖
view.render(true); // resetScroll = true
}
});
});
}
})
);
}
async openNoteInRecentFiles(file: TFile) {

View file

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

4
package-lock.json generated
View file

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

212
src/FloatingAudioPlayer.ts Normal file
View file

@ -0,0 +1,212 @@
import { App, TFile, setIcon } from 'obsidian';
export class FloatingAudioPlayer {
private app: App;
private currentFile: TFile;
private containerEl: HTMLElement;
private audioEl: HTMLAudioElement;
private titleEl: HTMLElement;
private closeButtonEl: HTMLElement;
private handleEl: HTMLElement;
private isDragging = false;
private offsetX = 0;
private offsetY = 0;
private isTouchEvent = false;
// 使用靜態 Map 來追蹤已開啟的播放器實例 (以檔案路徑為 key)
private static players: Map<string, FloatingAudioPlayer> = new Map();
// --- Private Event Handlers (Bound) ---
private boundHandleDragStartMouse: (e: MouseEvent) => void;
private boundHandleDragStartTouch: (e: TouchEvent) => void;
private boundHandleDragMoveMouse: (e: MouseEvent) => void;
private boundHandleDragMoveTouch: (e: TouchEvent) => void;
private boundHandleDragEndMouse: () => void;
private boundHandleDragEndTouch: () => void;
private boundClose: () => void;
private constructor(app: App, file: TFile) {
this.app = app;
this.currentFile = file;
// 綁定事件處理器
this.boundHandleDragStartMouse = this.handleDragStartMouse.bind(this);
this.boundHandleDragStartTouch = this.handleDragStartTouch.bind(this);
this.boundHandleDragMoveMouse = this.handleDragMoveMouse.bind(this);
this.boundHandleDragMoveTouch = this.handleDragMoveTouch.bind(this);
this.boundHandleDragEndMouse = this.handleDragEndMouse.bind(this);
this.boundHandleDragEndTouch = this.handleDragEndTouch.bind(this);
this.boundClose = this.close.bind(this);
this.buildUI();
this.setupDragEvents();
}
// --- 靜態方法:開啟或取得播放器 ---
public static open(app: App, file: TFile): FloatingAudioPlayer {
// 檢查是否已有相同檔案的播放器
if (FloatingAudioPlayer.players.has(file.path)) {
const existingPlayer = FloatingAudioPlayer.players.get(file.path)!;
existingPlayer.focus(); // 聚焦到現有播放器
return existingPlayer;
}
// 檢查是否有其他播放器存在
if (FloatingAudioPlayer.players.size > 0) {
// 更新第一個找到的播放器
const firstPlayer = FloatingAudioPlayer.players.values().next().value as FloatingAudioPlayer;
firstPlayer.updatePlayer(file);
firstPlayer.focus();
return firstPlayer;
}
// 創建新的播放器
const newPlayer = new FloatingAudioPlayer(app, file);
FloatingAudioPlayer.players.set(file.path, newPlayer);
newPlayer.show(); // 顯示並播放
return newPlayer;
}
// --- Private UI 和事件設定方法 ---
private buildUI(): void {
this.containerEl = document.createElement('div');
this.containerEl.className = 'ge-floating-audio-player';
this.containerEl.setAttribute('data-file', this.currentFile.path);
this.audioEl = document.createElement('audio');
this.audioEl.controls = true;
this.audioEl.src = this.app.vault.getResourcePath(this.currentFile);
this.titleEl = document.createElement('div');
this.titleEl.className = 'ge-audio-title';
this.titleEl.textContent = this.currentFile.basename;
this.closeButtonEl = document.createElement('div');
this.closeButtonEl.className = 'ge-audio-close-button';
setIcon(this.closeButtonEl, 'x');
this.closeButtonEl.addEventListener('click', this.boundClose);
this.handleEl = document.createElement('div');
this.handleEl.className = 'ge-audio-handle';
this.containerEl.appendChild(this.handleEl);
this.containerEl.appendChild(this.titleEl);
this.containerEl.appendChild(this.audioEl);
this.containerEl.appendChild(this.closeButtonEl);
}
private setupDragEvents(): void {
this.handleEl.addEventListener('mousedown', this.boundHandleDragStartMouse);
this.handleEl.addEventListener('touchstart', this.boundHandleDragStartTouch, { passive: true });
document.addEventListener('mousemove', this.boundHandleDragMoveMouse);
document.addEventListener('touchmove', this.boundHandleDragMoveTouch, { passive: false });
document.addEventListener('mouseup', this.boundHandleDragEndMouse);
document.addEventListener('touchend', this.boundHandleDragEndTouch);
}
private removeDragEvents(): void {
this.handleEl.removeEventListener('mousedown', this.boundHandleDragStartMouse);
this.handleEl.removeEventListener('touchstart', this.boundHandleDragStartTouch);
document.removeEventListener('mousemove', this.boundHandleDragMoveMouse);
document.removeEventListener('touchmove', this.boundHandleDragMoveTouch);
document.removeEventListener('mouseup', this.boundHandleDragEndMouse);
document.removeEventListener('touchend', this.boundHandleDragEndTouch);
}
// --- Private 事件處理器 ---
private handleDragStartMouse(e: MouseEvent): void {
if (this.isTouchEvent) return;
this.isDragging = true;
this.offsetX = e.clientX - this.containerEl.getBoundingClientRect().left;
this.offsetY = e.clientY - this.containerEl.getBoundingClientRect().top;
this.containerEl.classList.add('ge-audio-dragging');
}
private handleDragStartTouch(e: TouchEvent): void {
this.isTouchEvent = true;
this.isDragging = true;
const touch = e.touches[0];
this.offsetX = touch.clientX - this.containerEl.getBoundingClientRect().left;
this.offsetY = touch.clientY - this.containerEl.getBoundingClientRect().top;
this.containerEl.classList.add('ge-audio-dragging');
}
private handleDragMoveMouse(e: MouseEvent): void {
if (!this.isDragging || this.isTouchEvent) return;
this.movePlayer(e.clientX, e.clientY);
}
private handleDragMoveTouch(e: TouchEvent): void {
if (!this.isDragging) return;
const touch = e.touches[0];
this.movePlayer(touch.clientX, touch.clientY);
e.preventDefault(); // 防止頁面滾動
}
private handleDragEndMouse(): void {
if (this.isTouchEvent) return;
this.isDragging = false;
this.containerEl.classList.remove('ge-audio-dragging');
}
private handleDragEndTouch(): void {
this.isDragging = false;
this.isTouchEvent = false; // 重置觸控標記
this.containerEl.classList.remove('ge-audio-dragging');
}
private movePlayer(clientX: number, clientY: number): void {
const x = clientX - this.offsetX;
const y = clientY - this.offsetY;
this.containerEl.style.left = `${x}px`;
this.containerEl.style.top = `${y}px`;
}
// --- Public 方法 ---
public show(): void {
document.body.appendChild(this.containerEl);
// 設定初始位置(右下角)
const rect = this.containerEl.getBoundingClientRect();
this.containerEl.style.left = `${window.innerWidth - rect.width - 20}px`;
this.containerEl.style.top = `${window.innerHeight - rect.height - 20}px`;
this.audioEl.play();
}
public close(): void {
this.removeDragEvents(); // 清理事件監聽器
this.containerEl.remove(); // 從 DOM 移除
FloatingAudioPlayer.players.delete(this.currentFile.path); // 從靜態 Map 移除
}
public focus(): void {
this.containerEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
// 可以考慮添加一些視覺提示,例如短暫閃爍邊框
this.containerEl.style.transition = 'box-shadow 0.1s ease-in-out';
this.containerEl.style.boxShadow = '0 0 10px 2px var(--interactive-accent)';
setTimeout(() => {
this.containerEl.style.boxShadow = '';
}, 300);
}
// 更新播放器以播放新檔案
public updatePlayer(newFile: TFile): void {
// 從 Map 中移除舊的 key
FloatingAudioPlayer.players.delete(this.currentFile.path);
// 更新檔案引用和 Map 中的新 key
this.currentFile = newFile;
FloatingAudioPlayer.players.set(this.currentFile.path, this);
// 更新 UI
this.containerEl.setAttribute('data-file', this.currentFile.path);
this.audioEl.src = this.app.vault.getResourcePath(this.currentFile);
this.titleEl.textContent = this.currentFile.basename;
this.audioEl.play();
}
}

View file

@ -8,7 +8,8 @@ import { showNoteColorSettingsModal } from './NoteColorSettingsModal';
import { showFolderRenameModal } from './FolderRenameModal';
import { showSearchModal } from './SearchModal';
import { FileWatcher } from './FileWatcher';
import { isDocumentFile, isMediaFile, isImageFile, isVideoFile, isAudioFile } from './fileUtils';
import { isDocumentFile, isMediaFile, isImageFile, isVideoFile, isAudioFile, sortFiles, ignoredFiles, getFiles } from './fileUtils';
import { FloatingAudioPlayer } from './FloatingAudioPlayer';
import { t } from './translations';
import GridExplorerPlugin from '../main';
@ -136,265 +137,6 @@ export class GridView extends ItemView {
this.app.workspace.requestSaveLayout();
}
async getFiles(): Promise<TFile[]> {
if (this.sourceMode === 'folder' && this.sourcePath) {
// 獲取指定資料夾內的所有 Markdown、圖片和影片檔案
const folder = this.app.vault.getAbstractFileByPath(this.sourcePath);
if (folder instanceof TFolder) {
// 只取得當前資料夾中的支援檔案,不包含子資料夾
const files = folder.children.filter((file): file is TFile => {
if (!(file instanceof TFile)) return false;
// 如果是 Markdown 檔案,直接包含
if (isDocumentFile(file) ||
(this.plugin.settings.showMediaFiles && isMediaFile(file))) {
return true;
}
return false;
});
return this.sortFiles(files);
}
return [];
} else if (this.sourceMode === 'search') {
// 搜尋模式:使用 Obsidian 的搜尋功能
const globalSearchPlugin = (this.app as any).internalPlugins.getPluginById('global-search');
if (globalSearchPlugin?.instance) {
const searchLeaf = (this.app as any).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 is TFile => file instanceof TFile);
return this.sortFiles(files);
}
}
}
return [];
} else if (this.sourceMode === 'backlinks') {
if(this.searchQuery !== '') {
return [];
}
// 反向連結模式:找出所有引用當前筆記的檔案
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) as TFile;
if (sourceFile) {
backlinks.add(sourceFile);
}
}
}
return this.sortFiles(Array.from(backlinks) as TFile[]);
} else if(this.sourceMode === 'bookmarks') {
// 書籤模式
const bookmarksPlugin = (this.app as any).internalPlugins.plugins.bookmarks;
if (!bookmarksPlugin?.enabled) {
return [];
}
const bookmarks = bookmarksPlugin.instance.items;
const bookmarkedFiles = new Set();
const processBookmarkItem = (item: any) => {
if (item.type === 'file') {
const file = this.app.vault.getAbstractFileByPath(item.path);
if (file instanceof TFile) {
// 根據設定決定是否包含媒體檔案
if (isDocumentFile(file) ||
(this.plugin.settings.showMediaFiles && isMediaFile(file))) {
bookmarkedFiles.add(file);
}
}
} else if (item.type === 'group' && item.items) {
item.items.forEach(processBookmarkItem);
}
};
bookmarks.forEach(processBookmarkItem);
return Array.from(bookmarkedFiles) as TFile[];
} else if (this.sourceMode === 'all-files') {
// 所有筆記模式
const recentFiles = this.app.vault.getFiles().filter(file => {
// 根據設定決定是否包含媒體檔案
if (isDocumentFile(file) ||
(this.plugin.settings.showMediaFiles && this.randomNoteIncludeMedia && isMediaFile(file))) {
return true;
}
return false;
});
return this.sortFiles(recentFiles);
} else if (this.sourceMode === 'recent-files') {
// 最近檔案模式
const recentFiles = this.app.vault.getFiles().filter(file => {
// 根據設定決定是否包含媒體檔案
if (isDocumentFile(file) ||
(this.plugin.settings.showMediaFiles && this.randomNoteIncludeMedia && isMediaFile(file))) {
return true;
}
return false;
});
//臨時的排序類型
const sortType = this.sortType;
this.sortType = 'mtime-desc';
const sortedFiles = this.sortFiles(recentFiles);
this.sortType = sortType;
return sortedFiles;
} else if (this.sourceMode === 'random-note') {
// 隨機筆記模式從所有筆記中隨機選取10筆
const recentFiles = this.app.vault.getFiles().filter(file => {
// 根據設定決定是否包含媒體檔案
if (isDocumentFile(file) ||
(this.plugin.settings.showMediaFiles && this.randomNoteIncludeMedia && isMediaFile(file))) {
return true;
}
return false;
}).sort(() => Math.random() - 0.5);
return recentFiles;
} else {
return [];
}
}
//排序檔案
sortFiles(files: TFile[]) {
const sortType = this.folderSortType ? this.folderSortType : this.sortType;
// 檢查排序類型是否為非日期相關
const isNonDateSort = ['name-asc', 'name-desc', 'random'].includes(sortType);
// 檢查是否有任何日期欄位的設定
const hasModifiedField = !!this.plugin.settings.modifiedDateField;
const hasCreatedField = !!this.plugin.settings.createdDateField;
const hasAnyDateField = hasModifiedField || hasCreatedField;
// 符合以下任一條件就使用簡單排序:
// 1. 非日期排序類型 (name-asc, name-desc, random)
// 2. 沒有設定任何日期欄位
const shouldUseSimpleSort = isNonDateSort || !hasAnyDateField;
if (shouldUseSimpleSort) {
if (sortType === 'name-asc') {
return files.sort((a, b) => a.basename.localeCompare(b.basename));
} else if (sortType === 'name-desc') {
return files.sort((a, b) => b.basename.localeCompare(a.basename));
} else if (sortType === 'mtime-desc') {
return files.sort((a, b) => b.stat.mtime - a.stat.mtime);
} else if (sortType === 'mtime-asc') {
return files.sort((a, b) => a.stat.mtime - b.stat.mtime);
} else if (sortType === 'ctime-desc') {
return files.sort((a, b) => b.stat.ctime - a.stat.ctime);
} else if (sortType === 'ctime-asc') {
return files.sort((a, b) => a.stat.ctime - b.stat.ctime);
} else if (sortType === 'random') {
return files.sort(() => Math.random() - 0.5);
} else {
return files;
}
}
// 處理需要讀取metadata的日期排序情況
// 只有在以下條件都成立時才會執行:
// 1. 是日期排序類型 (mtime-desc, mtime-asc, ctime-desc, ctime-asc)
// 2. 至少設定了一個日期欄位 (modifiedDateField 或 createdDateField)
const filesWithDates = files.map(file => {
// 只對 .md 檔案讀取 metadata
const shouldReadMetadata = file.extension === 'md';
const metadata = shouldReadMetadata ? this.app.metadataCache.getFileCache(file) : null;
return {
file,
mDate: (() => {
if (metadata?.frontmatter) {
const fieldName = this.plugin.settings.modifiedDateField;
const dateStr = metadata.frontmatter[fieldName];
if (dateStr) {
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
return date.getTime();
}
}
}
return file.stat.mtime;
})(),
cDate: (() => {
if (metadata?.frontmatter) {
const fieldName = this.plugin.settings.createdDateField;
const dateStr = metadata.frontmatter[fieldName];
if (dateStr) {
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
return date.getTime();
}
}
}
return file.stat.ctime;
})()
};
});
if (sortType === 'mtime-desc') {
return filesWithDates.sort((a, b) => b.mDate - a.mDate).map(item => item.file);
} else if (sortType === 'mtime-asc') {
return filesWithDates.sort((a, b) => a.mDate - b.mDate).map(item => item.file);
} else if (sortType === 'ctime-desc') {
return filesWithDates.sort((a, b) => b.cDate - a.cDate).map(item => item.file);
} else if (sortType === 'ctime-asc') {
return filesWithDates.sort((a, b) => a.cDate - b.cDate).map(item => item.file);
} else {
return files;
}
}
//忽略檔案
ignoredFiles(files: TFile[]) {
return files.filter(file => {
// 檢查是否在忽略的資料夾中
const isInIgnoredFolder = this.plugin.settings.ignoredFolders.some(folder =>
file.path.startsWith(`${folder}/`)
);
if (isInIgnoredFolder) {
return false;
}
// 檢查資料夾是否符合忽略的模式
if (this.plugin.settings.ignoredFolderPatterns && this.plugin.settings.ignoredFolderPatterns.length > 0) {
const matchesIgnoredPattern = this.plugin.settings.ignoredFolderPatterns.some(pattern => {
try {
// 嘗試將模式作為正則表達式處理
// 如果模式包含特殊字符,使用正則表達式處理
if (/[\^\$\*\+\?\(\)\[\]\{\}\|\\]/.test(pattern)) {
const regex = new RegExp(pattern);
// 檢查檔案路徑是否符合正則表達式
return regex.test(file.path);
} else {
// 如果模式不包含特殊字符,直接檢查檔案路徑
return file.path.toLowerCase().includes(pattern.toLowerCase())
}
} catch (error) {
// 如果正則表達式無效,直接檢查檔案路徑
return file.path.toLowerCase().includes(pattern.toLowerCase())
}
});
// 如果符合任何忽略模式,則忽略此檔案
return !matchesIgnoredPattern;
}
return true;
});
}
async render(resetScroll = false) {
// 儲存當前捲動位置
const scrollContainer = this.containerEl.children[1] as HTMLElement;
@ -500,7 +242,7 @@ export class GridView extends ItemView {
// 建立新筆記
const newFile = await this.app.vault.create(newFilePath, '');
// 開啟新筆記
await this.app.workspace.getLeaf().openFile(newFile);
await this.app.workspace.getLeaf().openFile(newFile);
} catch (error) {
console.error('An error occurred while creating a new note:', error);
}
@ -799,7 +541,7 @@ export class GridView extends ItemView {
});
setIcon(searchButton, 'search');
searchButton.addEventListener('click', () => {
this.showSearchModal();
showSearchModal(this.app, this, '');
});
// 如果有搜尋關鍵字,顯示搜尋文字和取消按鈕
@ -812,7 +554,7 @@ export class GridView extends ItemView {
// 讓搜尋文字可點選
searchText.style.cursor = 'pointer';
searchText.addEventListener('click', () => {
this.showSearchModal(this.searchQuery);
showSearchModal(this.app, this, this.searchQuery);
});
// 創建取消按鈕
@ -1025,6 +767,7 @@ export class GridView extends ItemView {
const contentArea = folderEl.createDiv('ge-content-area');
const titleContainer = contentArea.createDiv('ge-title-container');
titleContainer.createEl('span', { cls: 'ge-title', text: `📁 ${folder.name}` });
titleContainer.setAttribute('title', folder.name);
// 檢查同名筆記是否存在
const notePath = `${folder.path}/${folder.name}.md`;
@ -1168,7 +911,7 @@ export class GridView extends ItemView {
if (this.searchAllFiles) {
allFiles = this.app.vault.getFiles();
} else {
allFiles = await this.getFiles();
allFiles = await getFiles(this);
}
// 根據設定過濾檔案
@ -1203,17 +946,17 @@ export class GridView extends ItemView {
);
// 根據設定的排序方式排序檔案
files = this.sortFiles(files);
files = sortFiles(files, this);
// 移除搜尋中的提示
loadingDiv.remove();
} else {
// 獲取檔案列表並根據搜尋關鍵字過濾
files = await this.getFiles();
files = await getFiles(this);
}
//忽略檔案
files = this.ignoredFiles(files)
files = ignoredFiles(files, this)
//最近檔案模式只取前30筆
if (this.sourceMode === 'recent-files') {
@ -1228,7 +971,11 @@ export class GridView extends ItemView {
// 如果沒有檔案,顯示提示訊息
if (files.length === 0) {
const noFilesDiv = container.createDiv('ge-no-files');
noFilesDiv.setText(t('no_files'));
if (this.sourceMode !== 'backlinks') {
noFilesDiv.setText(t('no_files'));
} else {
noFilesDiv.setText(t('no_backlinks'));
}
if (this.plugin.statusBarItem) {
this.plugin.statusBarItem.setText('');
}
@ -1406,13 +1153,27 @@ export class GridView extends ItemView {
// 處理多選邏輯
if (event.ctrlKey || event.metaKey) {
// Ctrl/Cmd 鍵:切換選中狀態
this.selectItem(index, true);
this.hasKeyboardFocus = true;
if (this.selectedItemIndex !== -1) {
// 如果已有選中狀態,則繼續選中
this.selectItem(index, true);
this.hasKeyboardFocus = true;
} else {
// 沒有選中狀態則開啟新分頁
if (isMediaFile(file)) {
// 開啟媒體檔案
if (isAudioFile(file)) {
FloatingAudioPlayer.open(this.app, file);
} else {
this.openMediaFile(file, files);
}
} else {
// 開啟文件檔案
this.app.workspace.getLeaf(true).openFile(file);
}
}
event.preventDefault();
return;
} else if (event.shiftKey) {
//if (event.shiftKey) {
// Shift 鍵:範圍選擇
this.handleRangeSelection(index);
this.hasKeyboardFocus = true;
@ -1427,17 +1188,13 @@ export class GridView extends ItemView {
if (isMediaFile(file)) {
// 開啟媒體檔案
if (isAudioFile(file)) {
this.openAudioFile(file);
FloatingAudioPlayer.open(this.app, file);
} else {
this.openMediaFile(file, files);
}
} else {
// 開啟文件檔案
if (event.ctrlKey || event.metaKey) {
this.app.workspace.getLeaf(true).openFile(file);
} else {
this.app.workspace.getLeaf().openFile(file);
}
this.app.workspace.getLeaf().openFile(file);
}
}
});
@ -1902,7 +1659,7 @@ export class GridView extends ItemView {
// 如果沒有傳入媒體檔案列表,則獲取
const getMediaFilesPromise = mediaFiles
? Promise.resolve(mediaFiles.filter(f => isMediaFile(f)))
: this.getFiles().then(allFiles => allFiles.filter(f => isMediaFile(f)));
: getFiles(this).then(allFiles => allFiles.filter(f => isMediaFile(f)));
getMediaFilesPromise.then(filteredMediaFiles => {
// 找到當前檔案在媒體檔案列表中的索引
@ -1914,154 +1671,6 @@ export class GridView extends ItemView {
mediaModal.open();
});
}
// 開啟音樂檔案
openAudioFile(file: TFile) {
// 檢查是否已有相同檔案的播放器
const existingPlayer = document.querySelector(`.ge-floating-audio-player[data-file="${file.path}"]`);
// 查詢所有已經存在的音樂播放器
const existingPlayers = document.querySelectorAll('.ge-floating-audio-player');
if (existingPlayer) {
// 如果已有相同檔案播放器,則聚焦到該播放器
existingPlayer.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
} else if (existingPlayers.length > 0) {
// 如果有其他音樂播放器,則聚焦到第一個播放器並更新它
const firstPlayer = existingPlayers[0] as HTMLElement;
// 聚焦到播放器
firstPlayer.scrollIntoView({ behavior: 'smooth', block: 'center' });
// 更新播放器的檔案屬性
firstPlayer.setAttribute('data-file', file.path);
// 更新音訊來源
const audioElement = firstPlayer.querySelector('audio');
if (audioElement) {
audioElement.src = this.app.vault.getResourcePath(file);
audioElement.play();
}
// 更新標題
const titleElement = firstPlayer.querySelector('.ge-audio-title');
if (titleElement) {
titleElement.textContent = file.basename;
}
return;
}
// 創建音樂播放器容器
const audioPlayerContainer = document.createElement('div');
audioPlayerContainer.className = 'ge-floating-audio-player';
audioPlayerContainer.setAttribute('data-file', file.path);
// 創建音樂元素
const audio = document.createElement('audio');
audio.controls = true;
audio.src = this.app.vault.getResourcePath(file);
// 創建標題元素
const titleElement = document.createElement('div');
titleElement.className = 'ge-audio-title';
titleElement.textContent = file.basename;
// 創建關閉按鈕
const closeButton = document.createElement('div');
closeButton.className = 'ge-audio-close-button';
setIcon(closeButton, 'x');
closeButton.addEventListener('click', () => {
// 移除音樂播放器
audioPlayerContainer.remove();
});
// 創建拖曳控制元素
const handleElement = document.createElement('div');
handleElement.className = 'ge-audio-handle';
// 將元素添加到容器中
audioPlayerContainer.appendChild(handleElement);
audioPlayerContainer.appendChild(titleElement);
audioPlayerContainer.appendChild(audio);
audioPlayerContainer.appendChild(closeButton);
// 設定拖曳事件
let isDragging = false;
let offsetX = 0;
let offsetY = 0;
let isTouchEvent = false;
handleElement.addEventListener('mousedown', (e) => {
if (isTouchEvent) return; // 如果是觸控事件觸發的,則忽略滑鼠事件
isDragging = true;
offsetX = e.clientX - audioPlayerContainer.getBoundingClientRect().left;
offsetY = e.clientY - audioPlayerContainer.getBoundingClientRect().top;
audioPlayerContainer.classList.add('ge-audio-dragging');
});
handleElement.addEventListener('touchstart', (e) => {
isTouchEvent = true;
isDragging = true;
const touch = e.touches[0];
offsetX = touch.clientX - audioPlayerContainer.getBoundingClientRect().left;
offsetY = touch.clientY - audioPlayerContainer.getBoundingClientRect().top;
audioPlayerContainer.classList.add('ge-audio-dragging');
}, { passive: true });
document.addEventListener('mousemove', (e) => {
if (!isDragging || isTouchEvent) return;
const x = e.clientX - offsetX;
const y = e.clientY - offsetY;
audioPlayerContainer.style.left = `${x}px`;
audioPlayerContainer.style.top = `${y}px`;
});
document.addEventListener('touchmove', (e) => {
if (!isDragging) return;
const touch = e.touches[0];
const x = touch.clientX - offsetX;
const y = touch.clientY - offsetY;
audioPlayerContainer.style.left = `${x}px`;
audioPlayerContainer.style.top = `${y}px`;
// 防止頁面滾動
e.preventDefault();
}, { passive: false });
document.addEventListener('mouseup', () => {
if (isTouchEvent) return;
isDragging = false;
audioPlayerContainer.classList.remove('ge-audio-dragging');
});
document.addEventListener('touchend', () => {
isDragging = false;
isTouchEvent = false;
audioPlayerContainer.classList.remove('ge-audio-dragging');
});
// 將音樂播放器添加到文檔中
document.body.appendChild(audioPlayerContainer);
// 設定初始位置
const rect = audioPlayerContainer.getBoundingClientRect();
audioPlayerContainer.style.left = `${window.innerWidth - rect.width - 20}px`;
audioPlayerContainer.style.top = `${window.innerHeight - rect.height - 20}px`;
// 播放音樂
audio.play();
}
// 顯示搜尋 modal
showSearchModal(defaultQuery = '') {
showSearchModal(this.app, this, defaultQuery);
}
// 保存視圖狀態
getState() {

View file

@ -58,7 +58,7 @@ export class SearchModal extends Modal {
cls: 'ge-search-scope-checkbox'
});
searchScopeCheckbox.checked = !this.gridView.searchAllFiles;
const searchScopeLabel = searchScopeContainer.createEl('span', {
searchScopeContainer.createEl('span', {
text: t('search_current_location_only'),
cls: 'ge-search-scope-label'
});
@ -70,7 +70,7 @@ export class SearchModal extends Modal {
cls: 'ge-search-media-files-checkbox'
});
searchMediaFilesCheckbox.checked = this.gridView.searchMediaFiles;
const searchMediaFilesLabel = searchMediaFilesContainer.createEl('span', {
searchMediaFilesContainer.createEl('span', {
text: t('search_media_files'),
cls: 'ge-search-media-files-label'
});

View file

@ -1,5 +1,6 @@
import { TFile } from 'obsidian';
import { TFile, TFolder } from 'obsidian';
import { type GallerySettings } from './settings';
import { GridView } from './GridView';
let customDocumentExtensions: string[] = [];
@ -45,3 +46,272 @@ export function isAudioFile(file: TFile): boolean {
export function isMediaFile(file: TFile): boolean {
return isImageFile(file) || isVideoFile(file) || isAudioFile(file);
}
//排序檔案
export function sortFiles(files: TFile[], gridView: GridView): TFile[] {
const app = gridView.app;
const settings = gridView.plugin.settings;
const sortType = gridView.folderSortType ? gridView.folderSortType : gridView.sortType;
// 檢查排序類型是否為非日期相關
const isNonDateSort = ['name-asc', 'name-desc', 'random'].includes(sortType);
// 檢查是否有任何日期欄位的設定
const hasModifiedField = !!settings.modifiedDateField;
const hasCreatedField = !!settings.createdDateField;
const hasAnyDateField = hasModifiedField || hasCreatedField;
// 符合以下任一條件就使用簡單排序:
// 1. 非日期排序類型 (name-asc, name-desc, random)
// 2. 沒有設定任何日期欄位
const shouldUseSimpleSort = isNonDateSort || !hasAnyDateField;
if (shouldUseSimpleSort) {
if (sortType === 'name-asc') {
return files.sort((a, b) => a.basename.localeCompare(b.basename));
} else if (sortType === 'name-desc') {
return files.sort((a, b) => b.basename.localeCompare(a.basename));
} else if (sortType === 'mtime-desc') {
return files.sort((a, b) => b.stat.mtime - a.stat.mtime);
} else if (sortType === 'mtime-asc') {
return files.sort((a, b) => a.stat.mtime - b.stat.mtime);
} else if (sortType === 'ctime-desc') {
return files.sort((a, b) => b.stat.ctime - a.stat.ctime);
} else if (sortType === 'ctime-asc') {
return files.sort((a, b) => a.stat.ctime - b.stat.ctime);
} else if (sortType === 'random') {
return files.sort(() => Math.random() - 0.5);
} else {
return files;
}
}
// 處理需要讀取metadata的日期排序情況
// 只有在以下條件都成立時才會執行:
// 1. 是日期排序類型 (mtime-desc, mtime-asc, ctime-desc, ctime-asc)
// 2. 至少設定了一個日期欄位 (modifiedDateField 或 createdDateField)
const filesWithDates = files.map(file => {
// 只對 .md 檔案讀取 metadata
const shouldReadMetadata = file.extension === 'md';
const metadata = shouldReadMetadata ? app.metadataCache.getFileCache(file) : null;
return {
file,
mDate: (() => {
if (metadata?.frontmatter) {
const fieldName = settings.modifiedDateField;
const dateStr = metadata.frontmatter[fieldName];
if (dateStr) {
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
return date.getTime();
}
}
}
return file.stat.mtime;
})(),
cDate: (() => {
if (metadata?.frontmatter) {
const fieldName = settings.createdDateField;
const dateStr = metadata.frontmatter[fieldName];
if (dateStr) {
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
return date.getTime();
}
}
}
return file.stat.ctime;
})()
};
});
if (sortType === 'mtime-desc') {
return filesWithDates.sort((a, b) => b.mDate - a.mDate).map(item => item.file);
} else if (sortType === 'mtime-asc') {
return filesWithDates.sort((a, b) => a.mDate - b.mDate).map(item => item.file);
} else if (sortType === 'ctime-desc') {
return filesWithDates.sort((a, b) => b.cDate - a.cDate).map(item => item.file);
} else if (sortType === 'ctime-asc') {
return filesWithDates.sort((a, b) => a.cDate - b.cDate).map(item => item.file);
} else {
return files;
}
}
//忽略檔案
export function ignoredFiles(files: TFile[], gridView: GridView): TFile[] {
const settings = gridView.plugin.settings;
return files.filter(file => {
// 檢查是否在忽略的資料夾中
const isInIgnoredFolder = settings.ignoredFolders.some(folder =>
file.path.startsWith(`${folder}/`)
);
if (isInIgnoredFolder) {
return false;
}
// 檢查資料夾是否符合忽略的模式
if (settings.ignoredFolderPatterns && settings.ignoredFolderPatterns.length > 0) {
const matchesIgnoredPattern = settings.ignoredFolderPatterns.some(pattern => {
try {
// 嘗試將模式作為正則表達式處理
// 如果模式包含特殊字符,使用正則表達式處理
if (/[\^\$\*\+\?\(\)\[\]\{\}\|\\]/.test(pattern)) {
const regex = new RegExp(pattern);
// 檢查檔案路徑是否符合正則表達式
return regex.test(file.path);
} else {
// 如果模式不包含特殊字符,直接檢查檔案路徑
return file.path.toLowerCase().includes(pattern.toLowerCase())
}
} catch (error) {
// 如果正則表達式無效,直接檢查檔案路徑
return file.path.toLowerCase().includes(pattern.toLowerCase())
}
});
// 如果符合任何忽略模式,則忽略此檔案
return !matchesIgnoredPattern;
}
return true;
});
}
// 獲取檔案
export async function getFiles(gridView: GridView): Promise<TFile[]> {
const app = gridView.app;
const settings = gridView.plugin.settings;
const sourceMode = gridView.sourceMode;
const sourcePath = gridView.sourcePath;
const searchQuery = gridView.searchQuery;
const randomNoteIncludeMedia = gridView.randomNoteIncludeMedia;
if (sourceMode === 'folder' && sourcePath) {
// 獲取指定資料夾內的所有 Markdown、圖片和影片檔案
const folder = app.vault.getAbstractFileByPath(sourcePath);
if (folder instanceof TFolder) {
// 只取得當前資料夾中的支援檔案,不包含子資料夾
const files = folder.children.filter((file): file is TFile => {
if (!(file instanceof TFile)) return false;
// 如果是 Markdown 檔案,直接包含
if (isDocumentFile(file) ||
(settings.showMediaFiles && isMediaFile(file))) {
return true;
}
return false;
});
return sortFiles(files, gridView);
}
return [];
} else if (sourceMode === 'search') {
// 搜尋模式:使用 Obsidian 的搜尋功能
const globalSearchPlugin = (app as any).internalPlugins.getPluginById('global-search');
if (globalSearchPlugin?.instance) {
const searchLeaf = (app as any).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 is TFile => file instanceof TFile);
return sortFiles(files, gridView);
}
}
}
return [];
} else if (sourceMode === 'backlinks') {
if(searchQuery !== '') {
return [];
}
// 反向連結模式:找出所有引用當前筆記的檔案
const activeFile = app.workspace.getActiveFile();
if (!activeFile) {
return [];
}
const backlinks = new Set();
// 使用 resolvedLinks 來找出反向連結
const resolvedLinks = app.metadataCache.resolvedLinks;
for (const [sourcePath, links] of Object.entries(resolvedLinks)) {
if (Object.keys(links).includes(activeFile.path)) {
const sourceFile = app.vault.getAbstractFileByPath(sourcePath) as TFile;
if (sourceFile) {
backlinks.add(sourceFile);
}
}
}
return sortFiles(Array.from(backlinks) as TFile[], gridView);
} else if(sourceMode === 'bookmarks') {
// 書籤模式
const bookmarksPlugin = (app as any).internalPlugins.plugins.bookmarks;
if (!bookmarksPlugin?.enabled) {
return [];
}
const bookmarks = bookmarksPlugin.instance.items;
const bookmarkedFiles = new Set();
const processBookmarkItem = (item: any) => {
if (item.type === 'file') {
const file = app.vault.getAbstractFileByPath(item.path);
if (file instanceof TFile) {
// 根據設定決定是否包含媒體檔案
if (isDocumentFile(file) ||
(settings.showMediaFiles && isMediaFile(file))) {
bookmarkedFiles.add(file);
}
}
} else if (item.type === 'group' && item.items) {
item.items.forEach(processBookmarkItem);
}
};
bookmarks.forEach(processBookmarkItem);
return Array.from(bookmarkedFiles) as TFile[];
} else if (sourceMode === 'all-files') {
// 所有筆記模式
const allVaultFiles = app.vault.getFiles().filter(file => {
// 根據設定決定是否包含媒體檔案
if (isDocumentFile(file) ||
(settings.showMediaFiles && randomNoteIncludeMedia && isMediaFile(file))) {
return true;
}
return false;
});
return sortFiles(allVaultFiles, gridView);
} else if (sourceMode === 'recent-files') {
// 最近檔案模式
const recentFiles = app.vault.getFiles().filter(file => {
// 根據設定決定是否包含媒體檔案
if (isDocumentFile(file) ||
(settings.showMediaFiles && randomNoteIncludeMedia && isMediaFile(file))) {
return true;
}
return false;
});
//臨時的排序類型
const sortType = gridView.sortType;
gridView.sortType = 'mtime-desc';
const sortedFiles = sortFiles(recentFiles, gridView);
gridView.sortType = sortType;
return sortedFiles;
} else if (sourceMode === 'random-note') {
// 隨機筆記模式,從所有筆記中隨機選取
const randomFiles = app.vault.getFiles().filter(file => {
// 根據設定決定是否包含媒體檔案
if (isDocumentFile(file) ||
(settings.showMediaFiles && randomNoteIncludeMedia && isMediaFile(file))) {
return true;
}
return false;
}).sort(() => Math.random() - 0.5);
return randomFiles; // 隨機模式不需要額外排序
} else {
return [];
}
}

View file

@ -160,7 +160,7 @@ export class GridExplorerSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.recentFilesCount.toString())
.onChange(async (value) => {
this.plugin.settings.recentFilesCount = parseInt(value);
await this.plugin.saveSettings();
await this.plugin.saveSettings(false);
});
});
@ -184,7 +184,7 @@ export class GridExplorerSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.randomNoteCount.toString())
.onChange(async (value) => {
this.plugin.settings.randomNoteCount = parseInt(value);
await this.plugin.saveSettings();
await this.plugin.saveSettings(false);
});
});

View file

@ -173,6 +173,7 @@ export const TRANSLATIONS: Translations = {
'set_note_color': '設定筆記顏色',
'rename_folder': '重新命名資料夾',
'enter_new_folder_name': '輸入新資料夾名稱',
'search_selection_in_grid_view': '在網格視圖中搜尋...',
},
'en': {
// Notifications
@ -328,6 +329,7 @@ export const TRANSLATIONS: Translations = {
'set_note_color': 'Set note color',
'rename_folder': 'Rename folder',
'enter_new_folder_name': 'Enter new folder name',
'search_selection_in_grid_view': 'Search ... in grid view',
},
'zh': {
// 通知信息
@ -483,6 +485,7 @@ export const TRANSLATIONS: Translations = {
'set_note_color': '设置笔记颜色',
'rename_folder': '重新命名文件夹',
'enter_new_folder_name': '输入新文件夹名称',
'search_selection_in_grid_view': '在网格视图中搜寻...',
},
'ja': {
// 通知メッジ
@ -638,5 +641,6 @@ export const TRANSLATIONS: Translations = {
'set_note_color': 'ノート色を設定',
'rename_folder': 'フォルダを再命名',
'enter_new_folder_name': '新しいフォルダ名を入力',
'search_selection_in_grid_view': '... をグリッドビューで検索',
}
}

View file

@ -38,7 +38,7 @@
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: var(--button-radius);
padding: 12px;
padding: 10px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
@ -132,7 +132,7 @@
}
.ge-content-area p {
margin: 8px 0 0 0;
margin: 5px 0 0 0;
color: var(--text-faint);
font-size: 0.85em;
line-height: 1.4;

View file

@ -1,3 +1,3 @@
{
"1.9.10": "1.1.0"
"1.9.11": "1.1.0"
}