Files
Termix/electron/main.cjs

670 lines
18 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 { app, BrowserWindow, shell, ipcMain, dialog } = require("electron");
const path = require("path");
const fs = require("fs");
const os = require("os");
let mainWindow = null;
const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
console.log("Another instance is already running, quitting...");
app.quit();
process.exit(0);
} else {
app.on("second-instance", (event, commandLine, workingDirectory) => {
console.log("Second instance detected, focusing existing window...");
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
mainWindow.show();
}
});
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
title: "Termix",
icon: isDev
? path.join(__dirname, "..", "public", "icon.png")
: path.join(process.resourcesPath, "public", "icon.png"),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
webSecurity: !isDev,
preload: path.join(__dirname, "preload.js"),
},
show: false,
});
if (process.platform !== "darwin") {
mainWindow.setMenuBarVisibility(false);
}
if (isDev) {
mainWindow.loadURL("http://localhost:5173");
mainWindow.webContents.openDevTools();
} else {
const indexPath = path.join(__dirname, "..", "dist", "index.html");
console.log("Loading frontend from:", indexPath);
mainWindow.loadFile(indexPath);
}
mainWindow.once("ready-to-show", () => {
console.log("Window ready to show");
mainWindow.show();
});
mainWindow.webContents.on(
"did-fail-load",
(event, errorCode, errorDescription, validatedURL) => {
console.error(
"Failed to load:",
errorCode,
errorDescription,
validatedURL,
);
},
);
mainWindow.webContents.on("did-finish-load", () => {
console.log("Frontend loaded successfully");
});
mainWindow.on("close", (event) => {
if (process.platform === "darwin") {
event.preventDefault();
mainWindow.hide();
}
});
mainWindow.on("closed", () => {
mainWindow = null;
});
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: "deny" };
});
}
ipcMain.handle("get-app-version", () => {
return app.getVersion();
});
// GitHub API service for version checking
const GITHUB_API_BASE = "https://api.github.com";
const REPO_OWNER = "LukeGus";
const REPO_NAME = "Termix";
// Simple cache for GitHub API responses
const githubCache = new Map();
const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes
async function fetchGitHubAPI(endpoint, cacheKey) {
// Check cache first
const cached = githubCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
return {
data: cached.data,
cached: true,
cache_age: Date.now() - cached.timestamp,
};
}
try {
let fetch;
try {
fetch = globalThis.fetch || require("node-fetch");
} catch (e) {
const https = require("https");
const http = require("http");
const { URL } = require("url");
fetch = (url, options = {}) => {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const isHttps = urlObj.protocol === "https:";
const client = isHttps ? https : http;
const req = client.request(
url,
{
method: options.method || "GET",
headers: options.headers || {},
timeout: options.timeout || 10000,
},
(res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
resolve({
ok: res.statusCode >= 200 && res.statusCode < 300,
status: res.statusCode,
text: () => Promise.resolve(data),
json: () => Promise.resolve(JSON.parse(data)),
});
});
},
);
req.on("error", reject);
req.on("timeout", () => {
req.destroy();
reject(new Error("Request timeout"));
});
if (options.body) {
req.write(options.body);
}
req.end();
});
};
}
const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, {
headers: {
Accept: "application/vnd.github+json",
"User-Agent": "TermixElectronUpdateChecker/1.0",
"X-GitHub-Api-Version": "2022-11-28",
},
timeout: 10000,
});
if (!response.ok) {
throw new Error(
`GitHub API error: ${response.status} ${response.statusText}`,
);
}
const data = await response.json();
// Cache the response
githubCache.set(cacheKey, {
data,
timestamp: Date.now(),
});
return {
data: data,
cached: false,
};
} catch (error) {
console.error("Failed to fetch from GitHub API:", error);
throw error;
}
}
// Check for Electron app updates
ipcMain.handle("check-electron-update", async () => {
try {
const localVersion = app.getVersion();
console.log(`Checking for updates. Local version: ${localVersion}`);
const releaseData = await fetchGitHubAPI(
`/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`,
"latest_release_electron"
);
const rawTag = releaseData.data.tag_name || releaseData.data.name || "";
const remoteVersionMatch = rawTag.match(/(\d+\.\d+(\.\d+)?)/);
const remoteVersion = remoteVersionMatch ? remoteVersionMatch[1] : null;
if (!remoteVersion) {
console.warn("Remote version not found in GitHub response:", rawTag);
return {
success: false,
error: "Remote version not found",
localVersion,
};
}
const isUpToDate = localVersion === remoteVersion;
const result = {
success: true,
status: isUpToDate ? "up_to_date" : "requires_update",
localVersion: localVersion,
remoteVersion: remoteVersion,
latest_release: {
tag_name: releaseData.data.tag_name,
name: releaseData.data.name,
published_at: releaseData.data.published_at,
html_url: releaseData.data.html_url,
body: releaseData.data.body,
},
cached: releaseData.cached,
cache_age: releaseData.cache_age,
};
console.log(`Version check result: ${result.status}`);
return result;
} catch (error) {
console.error("Version check failed:", error);
return {
success: false,
error: error.message,
localVersion: app.getVersion(),
};
}
});
ipcMain.handle("get-platform", () => {
return process.platform;
});
ipcMain.handle("get-server-config", () => {
try {
const userDataPath = app.getPath("userData");
const configPath = path.join(userDataPath, "server-config.json");
if (fs.existsSync(configPath)) {
const configData = fs.readFileSync(configPath, "utf8");
return JSON.parse(configData);
}
return null;
} catch (error) {
console.error("Error reading server config:", error);
return null;
}
});
ipcMain.handle("save-server-config", (event, config) => {
try {
const userDataPath = app.getPath("userData");
const configPath = path.join(userDataPath, "server-config.json");
if (!fs.existsSync(userDataPath)) {
fs.mkdirSync(userDataPath, { recursive: true });
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
return { success: true };
} catch (error) {
console.error("Error saving server config:", error);
return { success: false, error: error.message };
}
});
ipcMain.handle("test-server-connection", async (event, serverUrl) => {
try {
let fetch;
try {
fetch = globalThis.fetch || require("node-fetch");
} catch (e) {
const https = require("https");
const http = require("http");
const { URL } = require("url");
fetch = (url, options = {}) => {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const isHttps = urlObj.protocol === "https:";
const client = isHttps ? https : http;
const req = client.request(
url,
{
method: options.method || "GET",
headers: options.headers || {},
timeout: options.timeout || 5000,
},
(res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
resolve({
ok: res.statusCode >= 200 && res.statusCode < 300,
status: res.statusCode,
text: () => Promise.resolve(data),
json: () => Promise.resolve(JSON.parse(data)),
});
});
},
);
req.on("error", reject);
req.on("timeout", () => {
req.destroy();
reject(new Error("Request timeout"));
});
if (options.body) {
req.write(options.body);
}
req.end();
});
};
}
const normalizedServerUrl = serverUrl.replace(/\/$/, "");
const healthUrl = `${normalizedServerUrl}/health`;
try {
const response = await fetch(healthUrl, {
method: "GET",
timeout: 5000,
});
if (response.ok) {
const data = await response.text();
if (
data.includes("<html") ||
data.includes("<!DOCTYPE") ||
data.includes("<head>") ||
data.includes("<body>")
) {
console.log(
"Health endpoint returned HTML instead of JSON - not a Termix server",
);
return {
success: false,
error:
"Server returned HTML instead of JSON. This does not appear to be a Termix server.",
};
}
try {
const healthData = JSON.parse(data);
if (
healthData &&
(healthData.status === "ok" ||
healthData.status === "healthy" ||
healthData.healthy === true ||
healthData.database === "connected")
) {
return {
success: true,
status: response.status,
testedUrl: healthUrl,
};
}
} catch (parseError) {
console.log("Health endpoint did not return valid JSON");
}
}
} catch (urlError) {
console.error("Health check failed:", urlError);
}
try {
const versionUrl = `${normalizedServerUrl}/version`;
const response = await fetch(versionUrl, {
method: "GET",
timeout: 5000,
});
if (response.ok) {
const data = await response.text();
if (
data.includes("<html") ||
data.includes("<!DOCTYPE") ||
data.includes("<head>") ||
data.includes("<body>")
) {
console.log(
"Version endpoint returned HTML instead of JSON - not a Termix server",
);
return {
success: false,
error:
"Server returned HTML instead of JSON. This does not appear to be a Termix server.",
};
}
try {
const versionData = JSON.parse(data);
if (
versionData &&
(versionData.status === "up_to_date" ||
versionData.status === "requires_update" ||
(versionData.localVersion &&
versionData.version &&
versionData.latest_release))
) {
return {
success: true,
status: response.status,
testedUrl: versionUrl,
warning:
"Health endpoint not available, but server appears to be running",
};
}
} catch (parseError) {
console.log("Version endpoint did not return valid JSON");
}
}
} catch (versionError) {
console.error("Version check failed:", versionError);
}
return {
success: false,
error:
"Server is not responding or does not appear to be a valid Termix server. Please ensure the server is running and accessible.",
};
} catch (error) {
return { success: false, error: error.message };
}
});
app.whenReady().then(() => {
createWindow();
console.log("Termix started successfully");
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
} else if (mainWindow) {
mainWindow.show();
}
});
// ================== 拖拽功能实现 ==================
// 临时文件管理
const tempFiles = new Map(); // 存储临时文件路径映射
// 创建临时文件
ipcMain.handle("create-temp-file", async (event, fileData) => {
try {
const { fileName, content, encoding = "base64" } = fileData;
// 创建临时目录
const tempDir = path.join(os.tmpdir(), "termix-drag-files");
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// 生成临时文件路径
const tempId = Date.now() + "-" + Math.random().toString(36).substr(2, 9);
const tempFilePath = path.join(tempDir, `${tempId}-${fileName}`);
// 写入文件内容
if (encoding === "base64") {
const buffer = Buffer.from(content, "base64");
fs.writeFileSync(tempFilePath, buffer);
} else {
fs.writeFileSync(tempFilePath, content, "utf8");
}
// 记录临时文件
tempFiles.set(tempId, {
path: tempFilePath,
fileName: fileName,
createdAt: Date.now(),
});
console.log(`Created temp file: ${tempFilePath}`);
return { success: true, tempId, path: tempFilePath };
} catch (error) {
console.error("Error creating temp file:", error);
return { success: false, error: error.message };
}
});
// 开始拖拽到桌面
ipcMain.handle("start-drag-to-desktop", async (event, { tempId, fileName }) => {
try {
const tempFile = tempFiles.get(tempId);
if (!tempFile) {
throw new Error("Temporary file not found");
}
// 使用Electron的startDrag API
const iconPath = path.join(__dirname, "..", "public", "icon.png");
const iconExists = fs.existsSync(iconPath);
mainWindow.webContents.startDrag({
file: tempFile.path,
icon: iconExists ? iconPath : undefined,
});
console.log(`Started drag for: ${tempFile.path}`);
return { success: true };
} catch (error) {
console.error("Error starting drag:", error);
return { success: false, error: error.message };
}
});
// 清理临时文件
ipcMain.handle("cleanup-temp-file", async (event, tempId) => {
try {
const tempFile = tempFiles.get(tempId);
if (tempFile && fs.existsSync(tempFile.path)) {
fs.unlinkSync(tempFile.path);
tempFiles.delete(tempId);
console.log(`Cleaned up temp file: ${tempFile.path}`);
}
return { success: true };
} catch (error) {
console.error("Error cleaning up temp file:", error);
return { success: false, error: error.message };
}
});
// 批量清理过期临时文件5分钟过期
const cleanupExpiredTempFiles = () => {
const now = Date.now();
const maxAge = 5 * 60 * 1000; // 5分钟
for (const [tempId, tempFile] of tempFiles.entries()) {
if (now - tempFile.createdAt > maxAge) {
try {
if (fs.existsSync(tempFile.path)) {
fs.unlinkSync(tempFile.path);
}
tempFiles.delete(tempId);
console.log(`Auto-cleaned expired temp file: ${tempFile.path}`);
} catch (error) {
console.error("Error auto-cleaning temp file:", error);
}
}
}
};
// 每分钟清理一次过期临时文件
setInterval(cleanupExpiredTempFiles, 60 * 1000);
// 创建临时文件夹拖拽支持
ipcMain.handle("create-temp-folder", async (event, folderData) => {
try {
const { folderName, files } = folderData;
// 创建临时目录
const tempDir = path.join(os.tmpdir(), "termix-drag-folders");
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
const tempId = Date.now() + "-" + Math.random().toString(36).substr(2, 9);
const tempFolderPath = path.join(tempDir, `${tempId}-${folderName}`);
// 递归创建文件夹结构
const createFolderStructure = (basePath, fileList) => {
for (const file of fileList) {
const fullPath = path.join(basePath, file.relativePath);
const dirPath = path.dirname(fullPath);
// 确保目录存在
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
// 写入文件
if (file.encoding === "base64") {
const buffer = Buffer.from(file.content, "base64");
fs.writeFileSync(fullPath, buffer);
} else {
fs.writeFileSync(fullPath, file.content, "utf8");
}
}
};
fs.mkdirSync(tempFolderPath, { recursive: true });
createFolderStructure(tempFolderPath, files);
// 记录临时文件夹
tempFiles.set(tempId, {
path: tempFolderPath,
fileName: folderName,
createdAt: Date.now(),
isFolder: true,
});
console.log(`Created temp folder: ${tempFolderPath}`);
return { success: true, tempId, path: tempFolderPath };
} catch (error) {
console.error("Error creating temp folder:", error);
return { success: false, error: error.message };
}
});
app.on("before-quit", () => {
console.log("App is quitting...");
// 清理所有临时文件
for (const [tempId, tempFile] of tempFiles.entries()) {
try {
if (fs.existsSync(tempFile.path)) {
if (tempFile.isFolder) {
fs.rmSync(tempFile.path, { recursive: true, force: true });
} else {
fs.unlinkSync(tempFile.path);
}
}
} catch (error) {
console.error("Error cleaning up temp file on quit:", error);
}
}
tempFiles.clear();
});
app.on("will-quit", () => {
console.log("App will quit...");
});
process.on("uncaughtException", (error) => {
console.error("Uncaught Exception:", error);
});
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
});