This commit is contained in:
Devon22 2025-08-27 00:17:36 +08:00
parent 6415d57234
commit eb78e913ad
11 changed files with 553 additions and 256 deletions

View file

@ -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",

4
package-lock.json generated
View file

@ -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",

View file

@ -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": {

View file

@ -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

View file

@ -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);
});
}
// 點擊時開啟檔案

286
src/createItemUtils.ts Normal file
View file

@ -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}`;
}
}

View file

@ -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);

View file

@ -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'));
}
}

View file

@ -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': 'Додати',

View file

@ -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 {

View file

@ -1,3 +1,3 @@
{
"2.9.11": "1.1.0"
"2.9.12": "1.1.0"
}