2025-08-26 16:17:36 +00:00
|
|
|
|
import { App, TFile, Notice } from 'obsidian';
|
2025-11-04 13:14:26 +00:00
|
|
|
|
import { showNameInputModal } from '../modal/folderRenameModal';
|
|
|
|
|
|
import { t } from '../translations';
|
2025-08-26 16:17:36 +00:00
|
|
|
|
|
2026-05-15 09:52:33 +00:00
|
|
|
|
interface ShortcutFrontmatter {
|
|
|
|
|
|
type?: 'mode' | 'folder' | 'file' | 'search' | 'uri';
|
|
|
|
|
|
redirect?: string;
|
|
|
|
|
|
searchCurrentLocationOnly?: boolean;
|
|
|
|
|
|
searchFilesNameOnly?: boolean;
|
|
|
|
|
|
searchMediaFiles?: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-26 16:17:36 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 創建新筆記的共用函數
|
|
|
|
|
|
* @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) {
|
2025-09-04 16:27:45 +00:00
|
|
|
|
// 預設名稱
|
|
|
|
|
|
const defaultName = `${t('untitled')}`;
|
|
|
|
|
|
|
|
|
|
|
|
// 先彈出名稱輸入框
|
|
|
|
|
|
showNameInputModal(app, {
|
|
|
|
|
|
title: t('new_folder'),
|
|
|
|
|
|
description: t('enter_new_folder_name'),
|
|
|
|
|
|
defaultValue: defaultName,
|
2026-05-13 12:57:18 +00:00
|
|
|
|
onSubmit: (inputName: string) => {
|
|
|
|
|
|
void (async () => {
|
|
|
|
|
|
let baseName = (inputName && inputName.trim().length > 0) ? inputName.trim() : defaultName;
|
|
|
|
|
|
let newFolderName = baseName;
|
|
|
|
|
|
let newFolderPath = !folderPath || folderPath === '/' ? newFolderName : `${folderPath}/${newFolderName}`;
|
2025-09-04 16:27:45 +00:00
|
|
|
|
|
2026-05-13 12:57:18 +00:00
|
|
|
|
// 檢查資料夾是否已存在,如果存在則遞增編號
|
|
|
|
|
|
let counter = 1;
|
|
|
|
|
|
while (app.vault.getAbstractFileByPath(newFolderPath)) {
|
|
|
|
|
|
newFolderName = `${baseName} ${counter}`;
|
|
|
|
|
|
newFolderPath = !folderPath || folderPath === '/' ? newFolderName : `${folderPath}/${newFolderName}`;
|
|
|
|
|
|
counter++;
|
|
|
|
|
|
}
|
2025-09-04 16:27:45 +00:00
|
|
|
|
|
2026-05-13 12:57:18 +00:00
|
|
|
|
try {
|
|
|
|
|
|
// 建立新資料夾
|
|
|
|
|
|
await app.vault.createFolder(newFolderPath);
|
|
|
|
|
|
// 執行成功回調
|
|
|
|
|
|
if (onSuccess) {
|
|
|
|
|
|
onSuccess();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('An error occurred while creating a new folder:', error);
|
2025-09-04 16:27:45 +00:00
|
|
|
|
}
|
2026-05-13 12:57:18 +00:00
|
|
|
|
})();
|
2025-09-04 16:27:45 +00:00
|
|
|
|
},
|
|
|
|
|
|
});
|
2025-08-26 16:17:36 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 創建新畫布的共用函數
|
|
|
|
|
|
* @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
|
2026-05-15 09:52:33 +00:00
|
|
|
|
await app.fileManager.processFrontMatter(newFile, (frontmatter: ShortcutFrontmatter) => {
|
2025-08-26 16:17:36 +00:00
|
|
|
|
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') {
|
2026-05-15 09:52:33 +00:00
|
|
|
|
const targetFile = app.vault.getAbstractFileByPath(option.value);
|
|
|
|
|
|
if (!(targetFile instanceof TFile)) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const link = app.fileManager.generateMarkdownLink(targetFile, "");
|
2025-08-26 16:17:36 +00:00
|
|
|
|
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}`;
|
|
|
|
|
|
|
2026-05-15 09:52:33 +00:00
|
|
|
|
} catch {
|
2025-08-26 16:17:36 +00:00
|
|
|
|
// 如果解析失敗,使用安全的預設名稱
|
|
|
|
|
|
const cleanUri = uri.replace(/[<>:"/\\|?*]/g, '_').substring(0, 30);
|
|
|
|
|
|
return `🌐 ${cleanUri}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|