markdown/public/preload/services.js

352 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const fs = require("node:fs");
const path = require("node:path");
const { exec, spawn } = require("node:child_process");
const treeKill = require("tree-kill");
// 辅助函数:递归获取所有文件
const getAllFiles = (dirPath, arrayOfFiles) => {
const files = fs.readdirSync(dirPath);
arrayOfFiles = arrayOfFiles || [];
files.forEach((file) => {
if (fs.statSync(dirPath + "/" + file).isDirectory()) {
arrayOfFiles = getAllFiles(dirPath + "/" + file, arrayOfFiles);
} else {
arrayOfFiles.push(path.join(dirPath, "/", file));
}
});
return arrayOfFiles;
};
window.services = {
// --- File Service ---
// 选择目录
selectDirectory() {
try {
const result = window.utools.showOpenDialog({
title: "选择项目目录",
properties: ["openDirectory"],
});
return result;
} catch (error) {
console.error("选择目录失败:", error);
throw error;
}
},
// 读取目录结构 (单层,用于懒加载)
async readDirectory(dirPath) {
try {
if (!fs.existsSync(dirPath)) {
return [];
}
const items = await fs.promises.readdir(dirPath, { withFileTypes: true });
return items.map((item) => ({
id: path.join(dirPath, item.name), // 使用路径作为 ID
name: item.name,
path: path.join(dirPath, item.name),
type: item.isDirectory() ? "directory" : "file",
// children 不设置或设为 undefined表示未加载如果设为 [],组件会认为已加载且为空
children: undefined,
}));
} catch (error) {
console.error("读取目录失败:", error);
throw error;
}
},
// 读取文件内容
async readFile(filePath) {
return await fs.promises.readFile(filePath, { encoding: "utf-8" });
},
// 写入文件内容
async writeFile(filePath, content) {
await fs.promises.writeFile(filePath, content, { encoding: "utf-8" });
return true;
},
// 创建文件夹
async createDirectory(dirPath) {
if (!fs.existsSync(dirPath)) {
await fs.promises.mkdir(dirPath, { recursive: true });
}
return true;
},
// 删除文件/文件夹
async deleteItem(itemPath) {
await fs.promises.rm(itemPath, { recursive: true, force: true });
return true;
},
// 重命名
async renameItem(oldPath, newPath) {
await fs.promises.rename(oldPath, newPath);
return true;
},
// 复制文件/文件夹
async copyItem(sourcePath, destPath) {
await fs.promises.cp(sourcePath, destPath, { recursive: true });
return true;
},
// 检查路径是否存在
pathExists(itemPath) {
return fs.existsSync(itemPath);
},
// 打开所在目录
openItemLocation(itemPath) {
window.utools.shellShowItemInFolder(itemPath);
},
// --- Image Service ---
// 保存图片
// imageData: base64 字符串
// mdFilePath: 当前 Markdown 文件的路径
async saveImage(imageData, mdFilePath) {
try {
const mdDir = path.dirname(mdFilePath);
const mdName = path.basename(mdFilePath, path.extname(mdFilePath));
const assetsDir = path.join(mdDir, mdName);
// 创建图片目录
if (!fs.existsSync(assetsDir)) {
await fs.promises.mkdir(assetsDir, { recursive: true });
}
// 生成文件名 (时间戳)
const timestamp = Date.now();
// 简单判断图片类型,默认 png
let ext = "png";
if (imageData.startsWith("data:image/jpeg")) ext = "jpg";
else if (imageData.startsWith("data:image/gif")) ext = "gif";
else if (imageData.startsWith("data:image/webp")) ext = "webp";
const imageName = `${timestamp}.${ext}`;
const imagePath = path.join(assetsDir, imageName);
// 写入文件 (去掉 base64 头部)
const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
await fs.promises.writeFile(imagePath, base64Data, { encoding: "base64" });
// 返回相对路径,用于 Markdown 引用
// 使用 forward slashes for markdown compatibility
return `./${mdName}/${imageName}`;
} catch (error) {
console.error("保存图片失败:", error);
throw error;
}
},
// --- Git Service ---
// 执行 Git 命令
async execGit(command, cwd) {
return new Promise((resolve, reject) => {
exec(`git ${command}`, { cwd }, (error, stdout, stderr) => {
if (error) {
// git status 即使有变动有时也会返回非0需要特殊处理但通常 error 是命令执行失败
// 这里简单返回
resolve({ success: false, error: error.message, stderr });
} else {
resolve({ success: true, stdout });
}
});
});
},
async isGitRepository(dirPath) {
const gitDir = path.join(dirPath, ".git");
return fs.existsSync(gitDir);
},
async gitStatus(dirPath) {
return this.execGit("status --short", dirPath);
},
async gitAdd(dirPath, files = ".") {
return this.execGit(`add ${files}`, dirPath);
},
async gitCommit(dirPath, message) {
// 简单处理 message 中的引号
const safeMessage = message.replace(/"/g, '\\"');
return this.execGit(`commit -m "${safeMessage}"`, dirPath);
},
async gitPush(dirPath) {
return this.execGit("push", dirPath);
},
async gitPull(dirPath) {
return this.execGit("pull", dirPath);
},
// --- Search Service ---
// 搜索文件 (文件名)
async searchFileName(rootDir, keyword) {
try {
const allFiles = [];
const traverse = async (currentDir) => {
const items = await fs.promises.readdir(currentDir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(currentDir, item.name);
if (item.isDirectory()) {
if (item.name === ".git" || item.name === "node_modules") continue; // 忽略特定目录
await traverse(fullPath);
} else {
if (item.name.toLowerCase().includes(keyword.toLowerCase())) {
allFiles.push({
name: item.name,
path: fullPath,
type: "file",
});
}
}
}
};
await traverse(rootDir);
return allFiles;
} catch (error) {
console.error("搜索文件名失败:", error);
return [];
}
},
// 搜索内容
async searchContent(rootDir, keyword, useRegex = false) {
try {
const results = [];
const traverse = async (currentDir) => {
const items = await fs.promises.readdir(currentDir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(currentDir, item.name);
if (item.isDirectory()) {
if (item.name === ".git" || item.name === "node_modules") continue;
await traverse(fullPath);
} else if (item.isFile() && item.name.endsWith(".md")) { // 仅搜索 markdown 文件
const content = await fs.promises.readFile(fullPath, "utf-8");
let match = false;
let matches = [];
if (useRegex) {
try {
const regex = new RegExp(keyword, "g");
let m;
while ((m = regex.exec(content)) !== null) {
match = true;
matches.push({ index: m.index, match: m[0] });
if (matches.length > 10) break; // 限制每个文件的匹配数
}
} catch (e) {
// 正则错误,忽略
}
} else {
if (content.includes(keyword)) {
match = true;
// 简单定位第一个匹配
const index = content.indexOf(keyword);
matches.push({ index, match: keyword });
}
}
if (match) {
results.push({
name: item.name,
path: fullPath,
matches,
preview: content.substring(0, 200) + "..." // 简单预览
});
}
}
}
};
await traverse(rootDir);
return results;
} catch (error) {
console.error("搜索内容失败:", error);
return [];
}
},
// --- Config Service ---
// 读取配置文件 (.md-helper.json)
async loadConfig(rootDir) {
const configPath = path.join(rootDir, ".md-helper.json");
try {
if (fs.existsSync(configPath)) {
const content = await fs.promises.readFile(configPath, "utf-8");
return JSON.parse(content);
}
} catch (e) {
console.error("读取配置失败", e);
}
return null;
},
// 保存配置文件
async saveConfig(rootDir, config) {
const configPath = path.join(rootDir, ".md-helper.json");
try {
await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
return true;
} catch (e) {
console.error("保存配置失败", e);
return false;
}
},
// --- Global Data Service ---
// 保存全局数据(如上次打开的目录、历史记录、主题等)
// 存储在 uTools 插件数据目录下的 global-data.json
getGlobalDataPath() {
// 优先使用 uTools 提供的路径,或者 fallback 到 userData
// 注意window.utools.getPath('userData') 可能在 preload 中不可直接用,需谨慎
// 但通常 preload 中可以使用 window.utools
// 为了稳妥,我们可以尝试使用 utools.dbStorage 如果只是简单 KV但用户要求 JSON 文件。
// 我们这里使用 electron 的 userData 目录
const userDataPath = window.utools.getPath('userData');
return path.join(userDataPath, 'md-helper-global.json');
},
async loadGlobalData() {
try {
const dataPath = this.getGlobalDataPath();
if (fs.existsSync(dataPath)) {
const content = await fs.promises.readFile(dataPath, 'utf-8');
return JSON.parse(content);
}
} catch (e) {
console.error("读取全局数据失败", e);
}
return {
lastOpenedDir: '',
historyDirs: [], // string[]
theme: {
primaryColor: '#1890ff',
isDark: false
}
};
},
async saveGlobalData(data) {
try {
const dataPath = this.getGlobalDataPath();
await fs.promises.writeFile(dataPath, JSON.stringify(data, null, 2), 'utf-8');
return true;
} catch (e) {
console.error("保存全局数据失败", e);
return false;
}
}
};