From eb78e913ad27c96ce033b4da0b1fd840bdc0d79d Mon Sep 17 00:00:00 2001 From: Devon22 Date: Wed, 27 Aug 2025 00:17:36 +0800 Subject: [PATCH] 2.9.12 --- manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/ExplorerView.ts | 99 ++++++++++++- src/GridView.ts | 85 ++++++++++- src/createItemUtils.ts | 286 ++++++++++++++++++++++++++++++++++++++ src/renderFolder.ts | 48 ++++++- src/renderHeaderButton.ts | 245 +------------------------------- src/translations.ts | 4 + styles.css | 32 +++++ versions.json | 2 +- 11 files changed, 553 insertions(+), 256 deletions(-) create mode 100644 src/createItemUtils.ts diff --git a/manifest.json b/manifest.json index c99fbbd..d6e2750 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "gridexplorer", "name": "GridExplorer", - "version": "2.9.11", + "version": "2.9.12", "minAppVersion": "1.1.0", "description": "Browse note files in a grid view.", "author": "Devon22", diff --git a/package-lock.json b/package-lock.json index 0e8baa6..e9c74b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gridexplorer", - "version": "2.9.11", + "version": "2.9.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gridexplorer", - "version": "2.9.11", + "version": "2.9.12", "license": "MIT", "devDependencies": { "@types/node": "^16.11.6", diff --git a/package.json b/package.json index cd6aebe..cb9ea8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gridexplorer", - "version": "2.9.11", + "version": "2.9.12", "description": "Browse note files in a grid view.", "main": "main.js", "scripts": { diff --git a/src/ExplorerView.ts b/src/ExplorerView.ts index 624204b..9228e9d 100644 --- a/src/ExplorerView.ts +++ b/src/ExplorerView.ts @@ -1,4 +1,4 @@ -import { TFile, TFolder, WorkspaceLeaf, Menu, setIcon, Platform, normalizePath, ItemView, EventRef, FuzzySuggestModal, parseLinktext } from 'obsidian'; +import { TFile, TFolder, WorkspaceLeaf, Menu, setIcon, Platform, normalizePath, ItemView, EventRef, FuzzySuggestModal, parseLinktext, Notice } from 'obsidian'; import GridExplorerPlugin from './main'; import { GridView } from './GridView'; import { isFolderIgnored, isImageFile, isVideoFile, isAudioFile, isMediaFile } from './fileUtils'; @@ -9,6 +9,8 @@ import { showFolderRenameModal } from './modal/folderRenameModal'; import { showFolderMoveModal } from './modal/folderMoveModal'; import { FloatingAudioPlayer } from './floatingAudioPlayer'; import { MediaModal } from './modal/mediaModal'; +import { ShortcutSelectionModal } from './modal/shortcutSelectionModal'; +import { createNewNote, createNewFolder, createNewCanvas, createNewBase, createShortcut } from './createItemUtils'; import { t } from './translations'; // 探索器視圖類型常數 @@ -847,12 +849,15 @@ export class ExplorerView extends ItemView { if (!foldersExpanded) foldersChildren.addClass('is-collapsed'); foldersHeader.addEventListener('click', async (evt) => { + // 判斷點擊的是切換箭頭、資料夾名稱還是空白區域 if ((evt.target as HTMLElement).closest('.ge-explorer-folder-toggle')) { + // 點擊箭頭:只展開/收合 const newExpanded = !this.isExpanded(foldersGroupKey); this.setExpanded(foldersGroupKey, newExpanded); setIcon(foldersToggle, newExpanded ? 'chevron-down' : 'chevron-right'); foldersChildren.toggleClass('is-collapsed', !newExpanded); - } else { + } else if ((evt.target as HTMLElement).closest('.ge-explorer-folder-name')) { + // 點擊資料夾名稱:可能展開/收合,也可能開啟 GridView // Ctrl/Meta + 點擊:在新的 GridView 分頁中開啟 if (evt.ctrlKey || evt.metaKey) { this.openFolderInNewView(rootPath); @@ -877,6 +882,12 @@ export class ExplorerView extends ItemView { const root = this.app.vault.getRoot(); const view = await this.plugin.activateView(); if (view instanceof GridView) await view.setSource('folder', (root as any).path ?? '/'); + } else { + // 點擊空白區域:展開/收合 + const newExpanded = !this.isExpanded(foldersGroupKey); + this.setExpanded(foldersGroupKey, newExpanded); + setIcon(foldersToggle, newExpanded ? 'chevron-down' : 'chevron-right'); + foldersChildren.toggleClass('is-collapsed', !newExpanded); } }); @@ -965,6 +976,11 @@ export class ExplorerView extends ItemView { this.highlightActiveFolder(folder, header); this.attachDropTarget(header, folder.path); + // 如果資料夾沒有可見的子目錄,添加 ge-no-children 類別 + if (!this.hasVisibleChildren(folder)) { + header.addClass('ge-no-children'); + } + return header; } @@ -1050,7 +1066,7 @@ export class ExplorerView extends ItemView { /** * 處理資料夾的點擊事件 - * 區分點擊切換箭頭和點擊資料夾名稱的不同行為 + * 區分點擊切換箭頭、資料夾名稱和空白區域的不同行為 * @param evt 點擊事件 * @param folder 被點擊的資料夾 * @param header 資料夾標頭元素 @@ -1059,13 +1075,24 @@ export class ExplorerView extends ItemView { private handleFolderClick(evt: Event, folder: TFolder, header: HTMLElement, childrenContainer: HTMLElement) { const toggle = header.querySelector('.ge-explorer-folder-toggle') as HTMLElement; - // 判斷點擊的是切換箭頭還是資料夾名稱 + // 判斷點擊的是切換箭頭、資料夾名稱還是空白區域 if ((evt.target as HTMLElement).closest('.ge-explorer-folder-toggle')) { // 點擊箭頭:只展開/收合,不開啟 GridView this.handleToggleClick(folder, toggle, childrenContainer); - } else { + } else if ((evt.target as HTMLElement).closest('.ge-explorer-folder-name')) { // 點擊資料夾名稱:可能展開/收合,也可能開啟 GridView this.handleFolderNameClick(evt as MouseEvent, folder, header, toggle, childrenContainer); + } else { + // 點擊空白區域:如果沒有子資料夾且未選取則開啟 GridView,已選取則無作用;有子資料夾則展開/收合 + const isActive = header.hasClass('is-active'); + if (!this.hasVisibleChildren(folder)) { + if (!isActive) { + this.openFolderInGrid(folder.path); + } + // 如果已選取且沒有子資料夾,則不做任何動作 + } else { + this.handleToggleClick(folder, toggle, childrenContainer); + } } } @@ -1782,11 +1809,73 @@ export class ExplorerView extends ItemView { }); menu.addSeparator(); + // 添加新增筆記相關選項 + this.addNewItemMenuItems(menu, folder); + menu.addSeparator(); + this.addFolderNoteMenuItems(menu, folder); menu.addSeparator(); this.addFolderManagementMenuItems(menu, folder); } + /** + * 添加新增項目的右鍵選單項目 + * @param menu 右鍵選單 + * @param folder 資料夾 + */ + private addNewItemMenuItems(menu: Menu, folder: TFolder) { + // 新增筆記 + menu.addItem((item) => { + item.setTitle(t('new_note')) + .setIcon('square-pen') + .onClick(async () => { + await createNewNote(this.app, folder.path); + }); + }); + + // 新增資料夾 + menu.addItem((item) => { + item.setTitle(t('new_folder')) + .setIcon('folder') + .onClick(async () => { + await createNewFolder(this.app, folder.path, () => { + this.scheduleRender(); + }); + }); + }); + + // 新增畫布 + menu.addItem((item) => { + item.setTitle(t('new_canvas')) + .setIcon('layout-dashboard') + .onClick(async () => { + await createNewCanvas(this.app, folder.path); + }); + }); + + // 新增 base + menu.addItem((item) => { + item.setTitle(t('new_base')) + .setIcon('layout-dashboard') + .onClick(async () => { + await createNewBase(this.app, folder.path); + }); + }); + + // 新增捷徑 + menu.addItem((item) => { + item.setTitle(t('new_shortcut')) + .setIcon('shuffle') + .onClick(async () => { + const modal = new ShortcutSelectionModal(this.app, this.plugin, async (option) => { + await createShortcut(this.app, folder.path, option); + }); + modal.open(); + }); + }); + } + + /** * 添加資料夾筆記的右鍵選單項目 * @param menu 右鍵選單 diff --git a/src/GridView.ts b/src/GridView.ts index 7f113eb..84651fe 100644 --- a/src/GridView.ts +++ b/src/GridView.ts @@ -61,6 +61,7 @@ export class GridView extends ItemView { renderToken: number = 0; // 用於取消尚未完成之批次排程的遞增令牌 isShowingNote: boolean = false; // 是否正在顯示筆記 noteViewContainer: HTMLElement | null = null; // 筆記檢視容器 + eventCleanupFunctions: (() => void)[] = []; // 存儲事件清理函數 constructor(leaf: WorkspaceLeaf, plugin: GridExplorerPlugin) { super(leaf); @@ -305,6 +306,23 @@ export class GridView extends ItemView { await this.render(); } + // 清理事件監聽器 + onunload() { + // 清理所有事件監聽器 + this.eventCleanupFunctions.forEach(cleanup => { + try { + cleanup(); + } catch (error) { + console.warn('GridExplorer: Error during event cleanup:', error); + } + }); + this.eventCleanupFunctions = []; + + // 清理檔案監聽器(FileWatcher 的事件監聽器會在插件卸載時自動清理) + + super.onunload(); + } + // 渲染網格 async render() { @@ -315,6 +333,16 @@ export class GridView extends ItemView { selectedFilePath = selectedItem.dataset.filePath || null; } + // 清理之前的事件監聽器 + this.eventCleanupFunctions.forEach(cleanup => { + try { + cleanup(); + } catch (error) { + console.warn('GridExplorer: Error during event cleanup:', error); + } + }); + this.eventCleanupFunctions = []; + // 清空整個容器 this.containerEl.empty(); @@ -1207,9 +1235,11 @@ export class GridView extends ItemView { if (Platform.isDesktop && file.extension === 'md' && !this.plugin.settings.showNoteInGrid) { let triggeredInHover = false; let isHovering = false; + let isMouseDown = false; // 追蹤滑鼠按下狀態 + let keydownListener: ((e: KeyboardEvent) => void) | null = null; const trigger = () => { - if (triggeredInHover) return; + if (triggeredInHover || isMouseDown) return; // 如果滑鼠按下則不觸發 triggeredInHover = true; if (!this.openShortcutFile(file)) { this.showNoteInGrid(file); @@ -1218,25 +1248,66 @@ export class GridView extends ItemView { const onKeyDown = (e: KeyboardEvent) => { // 只有在滑鼠確實懸停在此項目上且按下 Ctrl 時才觸發 - if (isHovering && e.ctrlKey) { - trigger(); - e.preventDefault(); - e.stopPropagation(); + // 且滑鼠沒有按下(避免干擾 Ctrl+click) + if (isHovering && e.ctrlKey && !isMouseDown) { + // 短暫延遲以確保不是 Ctrl+click 操作 + setTimeout(() => { + if (isHovering && !triggeredInHover && !isMouseDown) { + trigger(); + } + }, 300); } }; + const onMouseDown = () => { + isMouseDown = true; + triggeredInHover = true; // 防止在點擊過程中觸發 hover 功能 + }; + + const onMouseUp = () => { + isMouseDown = false; + // 重置觸發狀態,但稍微延遲以避免立即重新觸發 + setTimeout(() => { + if (isHovering) { + triggeredInHover = false; + } + }, 50); + }; + const onMouseEnter = () => { triggeredInHover = false; isHovering = true; - document.addEventListener('keydown', onKeyDown, { capture: true }); + if (!keydownListener) { + keydownListener = onKeyDown; + document.addEventListener('keydown', keydownListener, { capture: true }); + } }; + const onMouseLeave = () => { isHovering = false; - document.removeEventListener('keydown', onKeyDown, { capture: true } as any); + isMouseDown = false; + if (keydownListener) { + document.removeEventListener('keydown', keydownListener, { capture: true }); + keydownListener = null; + } triggeredInHover = false; }; + fileEl.addEventListener('mouseenter', onMouseEnter); fileEl.addEventListener('mouseleave', onMouseLeave); + fileEl.addEventListener('mousedown', onMouseDown); + fileEl.addEventListener('mouseup', onMouseUp); + + // 添加清理函數到數組中 + this.eventCleanupFunctions.push(() => { + if (keydownListener) { + document.removeEventListener('keydown', keydownListener, { capture: true }); + } + fileEl.removeEventListener('mouseenter', onMouseEnter); + fileEl.removeEventListener('mouseleave', onMouseLeave); + fileEl.removeEventListener('mousedown', onMouseDown); + fileEl.removeEventListener('mouseup', onMouseUp); + }); } // 點擊時開啟檔案 diff --git a/src/createItemUtils.ts b/src/createItemUtils.ts new file mode 100644 index 0000000..6b32e75 --- /dev/null +++ b/src/createItemUtils.ts @@ -0,0 +1,286 @@ +import { App, TFile, Notice } from 'obsidian'; +import { t } from './translations'; + +/** + * 創建新筆記的共用函數 + * @param app Obsidian App 實例 + * @param folderPath 目標資料夾路徑 + */ +export async function createNewNote(app: App, folderPath: string) { + let newFileName = `${t('untitled')}.md`; + let newFilePath = !folderPath || folderPath === '/' ? newFileName : `${folderPath}/${newFileName}`; + + // 檢查檔案是否已存在,如果存在則遞增編號 + let counter = 1; + while (app.vault.getAbstractFileByPath(newFilePath)) { + newFileName = `${t('untitled')} ${counter}.md`; + newFilePath = !folderPath || folderPath === '/' ? newFileName : `${folderPath}/${newFileName}`; + counter++; + } + + try { + // 建立新筆記 + const newFile = await app.vault.create(newFilePath, ''); + // 開啟新筆記 + await app.workspace.getLeaf().openFile(newFile); + } catch (error) { + console.error('An error occurred while creating a new note:', error); + } +} + +/** + * 創建新資料夾的共用函數 + * @param app Obsidian App 實例 + * @param folderPath 目標資料夾路徑 + * @param onSuccess 成功後的回調函數 + */ +export async function createNewFolder(app: App, folderPath: string, onSuccess?: () => void) { + let newFolderName = `${t('untitled')}`; + let newFolderPath = !folderPath || folderPath === '/' ? newFolderName : `${folderPath}/${newFolderName}`; + + // 檢查資料夾是否已存在,如果存在則遞增編號 + let counter = 1; + while (app.vault.getAbstractFileByPath(newFolderPath)) { + newFolderName = `${t('untitled')} ${counter}`; + newFolderPath = !folderPath || folderPath === '/' ? newFolderName : `${folderPath}/${newFolderName}`; + counter++; + } + + try { + // 建立新資料夾 + await app.vault.createFolder(newFolderPath); + // 執行成功回調 + if (onSuccess) { + onSuccess(); + } + } catch (error) { + console.error('An error occurred while creating a new folder:', error); + } +} + +/** + * 創建新畫布的共用函數 + * @param app Obsidian App 實例 + * @param folderPath 目標資料夾路徑 + */ +export async function createNewCanvas(app: App, folderPath: string) { + let newFileName = `${t('untitled')}.canvas`; + let newFilePath = !folderPath || folderPath === '/' ? newFileName : `${folderPath}/${newFileName}`; + + // 檢查檔案是否已存在,如果存在則遞增編號 + let counter = 1; + while (app.vault.getAbstractFileByPath(newFilePath)) { + newFileName = `${t('untitled')} ${counter}.canvas`; + newFilePath = !folderPath || folderPath === '/' ? newFileName : `${folderPath}/${newFileName}`; + counter++; + } + + try { + // 建立新畫布 + const newFile = await app.vault.create(newFilePath, ''); + // 開啟新畫布 + await app.workspace.getLeaf().openFile(newFile); + } catch (error) { + console.error('An error occurred while creating a new canvas:', error); + } +} + +/** + * 創建新 base 檔案的共用函數 + * @param app Obsidian App 實例 + * @param folderPath 目標資料夾路徑 + */ +export async function createNewBase(app: App, folderPath: string) { + let newFileName = `${t('untitled')}.base`; + let newFilePath = !folderPath || folderPath === '/' ? newFileName : `${folderPath}/${newFileName}`; + + // 檢查檔案是否已存在,如果存在則遞增編號 + let counter = 1; + while (app.vault.getAbstractFileByPath(newFilePath)) { + newFileName = `${t('untitled')} ${counter}.base`; + newFilePath = !folderPath || folderPath === '/' ? newFileName : `${folderPath}/${newFileName}`; + counter++; + } + + try { + // 建立新 base 檔案 + const newFile = await app.vault.create(newFilePath, ''); + // 開啟新檔案 + await app.workspace.getLeaf().openFile(newFile); + } catch (error) { + console.error('An error occurred while creating a new base:', error); + } +} + +/** + * 創建捷徑檔案的共用函數 + * @param app Obsidian App 實例 + * @param folderPath 目標資料夾路徑 + * @param option 捷徑選項 + */ +export async function createShortcut( + app: App, + folderPath: string, + option: { + type: 'mode' | 'folder' | 'file' | 'search' | 'uri'; + value: string; + display: string; + searchOptions?: { + searchCurrentLocationOnly: boolean; + searchFilesNameOnly: boolean; + searchMediaFiles: boolean; + }; + } +) { + try { + // 生成不重複的檔案名稱 + let counter = 0; + let shortcutName: string; + + // 對於 URI 類型,使用特殊的檔名生成邏輯 + if (option.type === 'uri') { + shortcutName = generateFilenameFromUri(option.value); + } else { + shortcutName = `${option.display}`; + } + + let newName = `${shortcutName}.md`; + let newPath = !folderPath || folderPath === '/' ? newName : `${folderPath}/${newName}`; + while (app.vault.getAbstractFileByPath(newPath)) { + counter++; + const baseName = option.type === 'uri' ? generateFilenameFromUri(option.value) : option.display; + shortcutName = `${baseName} ${counter}`; + newName = `${shortcutName}.md`; + newPath = !folderPath || folderPath === '/' ? newName : `${folderPath}/${newName}`; + } + + // 創建新檔案 + const newFile = await app.vault.create(newPath, ''); + + // 使用 processFrontMatter 來更新 frontmatter + await app.fileManager.processFrontMatter(newFile, (frontmatter: any) => { + if (option.type === 'mode') { + frontmatter.type = 'mode'; + frontmatter.redirect = option.value; + } else if (option.type === 'folder') { + frontmatter.type = 'folder'; + frontmatter.redirect = option.value; + } else if (option.type === 'file') { + const link = app.fileManager.generateMarkdownLink( + app.vault.getAbstractFileByPath(option.value) as TFile, + "" + ); + frontmatter.type = "file"; + frontmatter.redirect = link; + } else if (option.type === 'search') { + frontmatter.type = 'search'; + frontmatter.redirect = option.value; + // 添加搜尋選項到 frontmatter + if (option.searchOptions) { + frontmatter.searchCurrentLocationOnly = option.searchOptions.searchCurrentLocationOnly; + frontmatter.searchFilesNameOnly = option.searchOptions.searchFilesNameOnly; + frontmatter.searchMediaFiles = option.searchOptions.searchMediaFiles; + } + } else if (option.type === 'uri') { + frontmatter.type = 'uri'; + frontmatter.redirect = option.value; + } + }); + + new Notice(`${t('shortcut_created')}: ${shortcutName}`); + + } catch (error) { + console.error('Create shortcut error', error); + new Notice(t('failed_to_create_shortcut')); + } +} + +/** + * 從 URI 生成檔案名稱 + * @param uri URI 字串 + * @returns 檔案名稱 + */ +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}`; + } +} diff --git a/src/renderFolder.ts b/src/renderFolder.ts index 805d58d..6902d4f 100644 --- a/src/renderFolder.ts +++ b/src/renderFolder.ts @@ -3,9 +3,11 @@ import { GridView } from './GridView'; import { isFolderIgnored } from './fileUtils'; import { extractObsidianPathsFromDT } from './dragUtils'; import { showFolderNoteSettingsModal } from './modal/folderNoteSettingsModal'; -import { showFolderRenameModal } from './modal/folderRenameModal'; import { showFolderMoveModal } from './modal/folderMoveModal'; +import { showFolderRenameModal } from './modal/folderRenameModal'; import { t } from './translations'; +import { createNewNote, createNewFolder, createNewCanvas, createNewBase, createShortcut } from './createItemUtils'; +import { ShortcutSelectionModal } from './modal/shortcutSelectionModal'; export async function renderFolder(gridView: GridView, container: HTMLElement) { @@ -243,6 +245,50 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) { }); menu.addSeparator(); + // 新增筆記相關選項 + menu.addItem((item) => { + item.setTitle(t('new_note')) + .setIcon('square-pen') + .onClick(async () => { + await createNewNote(gridView.app, folder.path); + }); + }); + menu.addItem((item) => { + item.setTitle(t('new_folder')) + .setIcon('folder-plus') + .onClick(async () => { + await createNewFolder(gridView.app, folder.path); + // 重新渲染視圖 + requestAnimationFrame(() => { + gridView.render(); + }); + }); + }); + menu.addItem((item) => { + item.setTitle(t('new_canvas')) + .setIcon('layout-dashboard') + .onClick(async () => { + await createNewCanvas(gridView.app, folder.path); + }); + }); + menu.addItem((item) => { + item.setTitle(t('new_base')) + .setIcon('database') + .onClick(async () => { + await createNewBase(gridView.app, folder.path); + }); + }); + menu.addItem((item) => { + item.setTitle(t('new_shortcut')) + .setIcon('link') + .onClick(async () => { + new ShortcutSelectionModal(gridView.app, gridView.plugin, async (option) => { + await createShortcut(gridView.app, folder.path, option); + }).open(); + }); + }); + menu.addSeparator(); + // 檢查同名筆記是否存在 const notePath = `${folder.path}/${folder.name}.md`; let noteFile = gridView.app.vault.getAbstractFileByPath(notePath); diff --git a/src/renderHeaderButton.ts b/src/renderHeaderButton.ts index 7e42a7f..e70f982 100644 --- a/src/renderHeaderButton.ts +++ b/src/renderHeaderButton.ts @@ -4,6 +4,7 @@ import { EXPLORER_VIEW_TYPE } from './ExplorerView'; import { showFolderSelectionModal } from './modal/folderSelectionModal'; import { showSearchModal } from './modal/searchModal'; import { ShortcutSelectionModal } from './modal/shortcutSelectionModal'; +import { createNewNote, createNewFolder, createNewCanvas, createNewBase, createShortcut as createShortcutUtil } from './createItemUtils'; import { t } from './translations'; export function renderHeaderButton(gridView: GridView) { @@ -389,25 +390,7 @@ export function renderHeaderButton(gridView: GridView) { .setTitle(t('new_note')) .setIcon('square-pen') .onClick(async () => { - let newFileName = `${t('untitled')}.md`; - let newFilePath = !gridView.sourcePath || gridView.sourcePath === '/' ? newFileName : `${gridView.sourcePath}/${newFileName}`; - - // 檢查檔案是否已存在,如果存在則遞增編號 - let counter = 1; - while (gridView.app.vault.getAbstractFileByPath(newFilePath)) { - newFileName = `${t('untitled')} ${counter}.md`; - newFilePath = !gridView.sourcePath || gridView.sourcePath === '/' ? newFileName : `${gridView.sourcePath}/${newFileName}`; - counter++; - } - - try { - // 建立新筆記 - const newFile = await gridView.app.vault.create(newFilePath, ''); - // 開啟新筆記 - await gridView.app.workspace.getLeaf().openFile(newFile); - } catch (error) { - console.error('An error occurred while creating a new note:', error); - } + await createNewNote(gridView.app, gridView.sourcePath); }); }); // 新增資料夾 @@ -415,27 +398,11 @@ export function renderHeaderButton(gridView: GridView) { item.setTitle(t('new_folder')) .setIcon('folder') .onClick(async () => { - let newFolderName = `${t('untitled')}`; - let newFolderPath = !gridView.sourcePath || gridView.sourcePath === '/' ? newFolderName : `${gridView.sourcePath}/${newFolderName}`; - - // 檢查資料夾是否已存在,如果存在則遞增編號 - let counter = 1; - while (gridView.app.vault.getAbstractFileByPath(newFolderPath)) { - newFolderName = `${t('untitled')} ${counter}`; - newFolderPath = !gridView.sourcePath || gridView.sourcePath === '/' ? newFolderName : `${gridView.sourcePath}/${newFolderName}`; - counter++; - } - - try { - // 建立新資料夾 - await gridView.app.vault.createFolder(newFolderPath); - // 重新渲染視圖 + await createNewFolder(gridView.app, gridView.sourcePath, () => { requestAnimationFrame(() => { gridView.render(); }); - } catch (error) { - console.error('An error occurred while creating a new folder:', error); - } + }); }); }); // 新增畫布 @@ -443,25 +410,7 @@ export function renderHeaderButton(gridView: GridView) { item.setTitle(t('new_canvas')) .setIcon('layout-dashboard') .onClick(async () => { - let newFileName = `${t('untitled')}.canvas`; - let newFilePath = !gridView.sourcePath || gridView.sourcePath === '/' ? newFileName : `${gridView.sourcePath}/${newFileName}`; - - // 檢查檔案是否已存在,如果存在則遞增編號 - let counter = 1; - while (gridView.app.vault.getAbstractFileByPath(newFilePath)) { - newFileName = `${t('untitled')} ${counter}.canvas`; - newFilePath = !gridView.sourcePath || gridView.sourcePath === '/' ? newFileName : `${gridView.sourcePath}/${newFileName}`; - counter++; - } - - try { - // 建立新筆記 - const newFile = await gridView.app.vault.create(newFilePath, ''); - // 開啟新筆記 - await gridView.app.workspace.getLeaf().openFile(newFile); - } catch (error) { - console.error('An error occurred while creating a new canvas:', error); - } + await createNewCanvas(gridView.app, gridView.sourcePath); }); }); // 新增 base @@ -469,25 +418,7 @@ export function renderHeaderButton(gridView: GridView) { item.setTitle(t('new_base')) .setIcon('layout-dashboard') .onClick(async () => { - let newFileName = `${t('untitled')}.base`; - let newFilePath = !gridView.sourcePath || gridView.sourcePath === '/' ? newFileName : `${gridView.sourcePath}/${newFileName}`; - - // 檢查檔案是否已存在,如果存在則遞增編號 - let counter = 1; - while (gridView.app.vault.getAbstractFileByPath(newFilePath)) { - newFileName = `${t('untitled')} ${counter}.base`; - newFilePath = !gridView.sourcePath || gridView.sourcePath === '/' ? newFileName : `${gridView.sourcePath}/${newFileName}`; - counter++; - } - - try { - // 建立新筆記 - const newFile = await gridView.app.vault.create(newFilePath, ''); - // 開啟新筆記 - await gridView.app.workspace.getLeaf().openFile(newFile); - } catch (error) { - console.error('An error occurred while creating a new base:', error); - } + await createNewBase(gridView.app, gridView.sourcePath); }); }); // 新增捷徑 @@ -496,7 +427,7 @@ export function renderHeaderButton(gridView: GridView) { .setIcon('shuffle') .onClick(async () => { const modal = new ShortcutSelectionModal(gridView.app, gridView.plugin, async (option) => { - await createShortcut(gridView, option); + await createShortcutUtil(gridView.app, gridView.sourcePath, option); }); modal.open(); }); @@ -720,165 +651,3 @@ 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' | 'search' | 'uri'; - value: string; - display: string; - searchOptions?: { - searchCurrentLocationOnly: boolean; - searchFilesNameOnly: boolean; - searchMediaFiles: boolean; - }; - }) { - try { - // 生成不重複的檔案名稱 - let counter = 0; - let shortcutName: string; - - // 對於 URI 類型,使用特殊的檔名生成邏輯 - if (option.type === 'uri') { - shortcutName = generateFilenameFromUri(option.value); - } else { - shortcutName = `${option.display}`; - } - - let newName = `${shortcutName}.md`; - let newPath = !gridView.sourcePath || gridView.sourcePath === '/' ? newName : `${gridView.sourcePath}/${newName}`; - while (gridView.app.vault.getAbstractFileByPath(newPath)) { - counter++; - const baseName = option.type === 'uri' ? generateFilenameFromUri(option.value) : option.display; - shortcutName = `${baseName} ${counter}`; - newName = `${shortcutName}.md`; - newPath = !gridView.sourcePath || gridView.sourcePath === '/' ? newName : `${gridView.sourcePath}/${newName}`; - } - - // 創建新檔案 - const newFile = await gridView.app.vault.create(newPath, ''); - - // 使用 processFrontMatter 來更新 frontmatter - await gridView.app.fileManager.processFrontMatter(newFile, (frontmatter: any) => { - if (option.type === 'mode') { - frontmatter.type = 'mode'; - frontmatter.redirect = option.value; - } else if (option.type === 'folder') { - frontmatter.type = 'folder'; - frontmatter.redirect = option.value; - } else if (option.type === 'file') { - const link = gridView.app.fileManager.generateMarkdownLink( - gridView.app.vault.getAbstractFileByPath(option.value) as TFile, - "" - ); - frontmatter.type = "file"; - frontmatter.redirect = link; - } else if (option.type === 'search') { - frontmatter.type = 'search'; - frontmatter.redirect = option.value; - // 添加搜尋選項到 frontmatter - if (option.searchOptions) { - frontmatter.searchCurrentLocationOnly = option.searchOptions.searchCurrentLocationOnly; - frontmatter.searchFilesNameOnly = option.searchOptions.searchFilesNameOnly; - frontmatter.searchMediaFiles = option.searchOptions.searchMediaFiles; - } - } else if (option.type === 'uri') { - frontmatter.type = 'uri'; - frontmatter.redirect = option.value; - } - }); - - new Notice(`${t('shortcut_created')}: ${shortcutName}`); - - } catch (error) { - console.error('Create shortcut error', error); - new Notice(t('failed_to_create_shortcut')); - } -} \ No newline at end of file diff --git a/src/translations.ts b/src/translations.ts index 3924d36..1580bf8 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -647,6 +647,7 @@ export const TRANSLATIONS: Translations = { 'new_base': '新建 base', 'new_shortcut': '新建快捷方式', 'delete_folder': '删除文件夹', + 'move_folder': '移动文件夹', 'untitled': '未命名', 'files': '个文件', 'add': '添加', @@ -947,6 +948,7 @@ export const TRANSLATIONS: Translations = { 'new_base': '新規 base', 'new_shortcut': '新規ショートカット', 'delete_folder': '削除フォルダ', + 'move_folder': '移動フォルダ', 'untitled': '無題', 'files': 'ファイル', 'add': '追加', @@ -1247,6 +1249,7 @@ export const TRANSLATIONS: Translations = { 'new_base': 'Новый base', 'new_shortcut': 'Новый ярлык', 'delete_folder': 'Удалить папку', + 'move_folder': 'Переместить папку', 'untitled': 'Без названия', 'files': 'файлы', 'add': 'Добавить', @@ -1548,6 +1551,7 @@ export const TRANSLATIONS: Translations = { 'new_base': 'Новий base', 'new_shortcut': 'Новий ярлик', 'delete_folder': 'Видалити папку', + 'move_folder': 'Перемістити папку', 'untitled': 'Без назви', 'files': 'файли', 'add': 'Додати', diff --git a/styles.css b/styles.css index b10dd37..d009e31 100644 --- a/styles.css +++ b/styles.css @@ -2202,6 +2202,38 @@ a.ge-current-folder:hover { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + cursor: pointer; + padding: 2px 4px 2px 0; + border-radius: var(--radius-s); + transition: background-color 0.15s ease; +} + +/* 只有有子目錄的資料夾名稱才顯示懸停效果 */ +.ge-explorer-folder-header:not(.ge-no-children) .ge-explorer-folder-name:hover { + background-color: var(--background-modifier-hover); + color: var(--text-accent); +} + +/* 沒有子目錄的資料夾名稱不顯示特殊游標和懸停效果 */ +.ge-explorer-folder-header.ge-no-children .ge-explorer-folder-name { + padding: 0; +} + +.ge-explorer-folder-header.ge-no-children .ge-explorer-folder-name:hover { + background-color: transparent !important; + color: var(--text-normal) !important; +} + +/* 模式項目和暫存區項目的名稱也不顯示特殊懸停效果 */ +.ge-explorer-mode-item .ge-explorer-folder-name, +.ge-explorer-stash-item .ge-explorer-folder-name { + padding: 0; +} + +.ge-explorer-mode-item .ge-explorer-folder-name:hover, +.ge-explorer-stash-item .ge-explorer-folder-name:hover { + background-color: transparent !important; + color: var(--text-normal) !important; } .ge-explorer-folder-children { diff --git a/versions.json b/versions.json index 33c36cc..396f9ba 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "2.9.11": "1.1.0" + "2.9.12": "1.1.0" } \ No newline at end of file