v1.6.0 #221
No files matched your search
@@ -1,3 +1,3 @@
|
|||||||
VERSION=1.6.0
|
VERSION=1.6.0
|
||||||
VITE_API_HOST=localhost
|
VITE_API_HOST=localhost
|
||||||
CREDENTIAL_ENCRYPTION_KEY=98fbfabe84b125db7cbbb5168eb584aaecc2f3779a2aaa955c57bdd305071a84
|
CREDENTIAL_ENCRYPTION_KEY=98fbfabe84b125db7cbbb5168eb584aaecc2f3779a2aaa955c57bdd305071a84
|
||||||
@@ -23,8 +23,7 @@ Run the following commands:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run dev
|
npm run dev
|
||||||
npx tsc -p tsconfig.node.json
|
npm run dev:backend
|
||||||
node ./dist/backend/starter.js
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start the backend and the frontend Vite server. You can access Termix by going to `http://localhost:5174/`.
|
This will start the backend and the frontend Vite server. You can access Termix by going to `http://localhost:5174/`.
|
||||||
@@ -40,11 +39,11 @@ This will start the backend and the frontend Vite server. You can access Termix
|
|||||||
3. **Make your changes**: Implement your feature, fix, or improvement.
|
3. **Make your changes**: Implement your feature, fix, or improvement.
|
||||||
4. **Commit your changes**:
|
4. **Commit your changes**:
|
||||||
```sh
|
```sh
|
||||||
git commit -m "Add feature: my new feature"
|
git commit -m "Feature request my new feature"
|
||||||
```
|
```
|
||||||
5. **Push to your fork**:
|
5. **Push to your fork**:
|
||||||
```sh
|
```sh
|
||||||
git push origin feature/my-new-feature
|
git push origin feature/my-feature-request
|
||||||
```
|
```
|
||||||
6. **Open a pull request**: Go to the original repository and create a PR with a clear description.
|
6. **Open a pull request**: Go to the original repository and create a PR with a clear description.
|
||||||
|
|
||||||
@@ -53,4 +52,9 @@ This will start the backend and the frontend Vite server. You can access Termix
|
|||||||
- Follow the existing code style. Use Tailwind CSS with shadcn components.
|
- Follow the existing code style. Use Tailwind CSS with shadcn components.
|
||||||
- Place all API routes in the `main-axios.ts` file. Updating the `openapi.json` is unneeded.
|
- Place all API routes in the `main-axios.ts` file. Updating the `openapi.json` is unneeded.
|
||||||
- Include meaningful commit messages.
|
- Include meaningful commit messages.
|
||||||
- Link related issues when applicable.
|
- Link related issues when applicable.
|
||||||
|
- `MobileApp.tsx` renders when the users screen width is less than 768px, otherwise it loads the usual `DesktopApp.tsx`.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you need help with Termix, you can join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support channel. You can also open an issue or open a pull request on the [GitHub](https://github.com/LukeGus/Termix/issues) repo.
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { execSync } from 'child_process';
|
|
||||||
import path from 'path';
|
|
||||||
import fs from 'fs';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
// Check if we're in a path with spaces
|
|
||||||
const currentPath = process.cwd();
|
|
||||||
if (currentPath.includes(' ')) {
|
|
||||||
console.log('⚠️ Warning: Project path contains spaces which may cause issues with native modules.');
|
|
||||||
console.log('Current path:', currentPath);
|
|
||||||
console.log('Consider moving the project to a path without spaces for better compatibility.');
|
|
||||||
console.log('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set environment variables to help with native module compilation
|
|
||||||
process.env.npm_config_cache = path.join(process.cwd(), 'node_modules', '.cache');
|
|
||||||
process.env.npm_config_tmp = path.join(process.cwd(), 'node_modules', '.tmp');
|
|
||||||
|
|
||||||
console.log('Building Electron application...');
|
|
||||||
|
|
||||||
// Skip better-sqlite3 rebuild due to path issues
|
|
||||||
console.log('Skipping better-sqlite3 rebuild due to path space issues...');
|
|
||||||
console.log('Note: Using existing better-sqlite3 installation.');
|
|
||||||
|
|
||||||
// Run the electron-builder
|
|
||||||
try {
|
|
||||||
execSync('npx electron-builder', { stdio: 'inherit' });
|
|
||||||
console.log('✅ Electron build completed successfully!');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Electron build failed:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
@@ -16,40 +16,28 @@
|
|||||||
"!eslint.config.js"
|
"!eslint.config.js"
|
||||||
],
|
],
|
||||||
"extraMetadata": {
|
"extraMetadata": {
|
||||||
"main": "electron/main-simple.cjs"
|
"main": "electron/main.cjs"
|
||||||
},
|
},
|
||||||
"buildDependenciesFromSource": false,
|
"buildDependenciesFromSource": false,
|
||||||
"nodeGypRebuild": false,
|
"nodeGypRebuild": false,
|
||||||
"npmRebuild": false,
|
"npmRebuild": false,
|
||||||
"mac": {
|
|
||||||
"category": "public.app-category.developer-tools",
|
|
||||||
"target": [
|
|
||||||
{
|
|
||||||
"target": "dmg",
|
|
||||||
"arch": ["x64", "arm64"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"target": "zip",
|
|
||||||
"arch": ["x64", "arm64"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"win": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
"forceCodeSigning": false
|
"icon": "public/icon.ico",
|
||||||
|
"executableName": "Termix"
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
"allowToChangeInstallationDirectory": true,
|
"allowToChangeInstallationDirectory": true,
|
||||||
"artifactName": "${productName}-Setup-${version}.${ext}"
|
"artifactName": "${productName}-Setup-${version}.${ext}",
|
||||||
|
"createDesktopShortcut": true,
|
||||||
|
"createStartMenuShortcut": true,
|
||||||
|
"shortcutName": "Termix",
|
||||||
|
"uninstallDisplayName": "Termix"
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"category": "Development",
|
"target": "AppImage",
|
||||||
"target": [
|
"icon": "public/icon.png",
|
||||||
{
|
"category": "Development"
|
||||||
"target": "AppImage",
|
|
||||||
"arch": ["x64"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
const { app, BrowserWindow, Menu, shell, ipcMain } = require('electron');
|
|
||||||
const path = require('path');
|
|
||||||
const { spawn } = require('child_process');
|
|
||||||
|
|
||||||
// 全局变量
|
|
||||||
let mainWindow = null;
|
|
||||||
let backendProcess = null;
|
|
||||||
|
|
||||||
// 开发环境检测
|
|
||||||
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
|
|
||||||
|
|
||||||
// 启动后端服务
|
|
||||||
function startBackendServer() {
|
|
||||||
if (backendProcess) {
|
|
||||||
console.log('Backend server already running');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在打包环境中,后端文件在 resources/app/dist/backend/backend/ 目录下
|
|
||||||
const backendPath = isDev
|
|
||||||
? path.join(__dirname, '../dist/backend/starter.js')
|
|
||||||
: path.join(process.resourcesPath, 'app', 'dist', 'backend', 'backend', 'starter.js');
|
|
||||||
|
|
||||||
console.log('Starting backend server from:', backendPath);
|
|
||||||
console.log('Working directory:', process.cwd());
|
|
||||||
|
|
||||||
// 设置环境变量
|
|
||||||
const env = {
|
|
||||||
...process.env,
|
|
||||||
NODE_ENV: 'production',
|
|
||||||
DATA_PATH: app.getPath('userData'),
|
|
||||||
DB_PATH: path.join(app.getPath('userData'), 'database.db'),
|
|
||||||
VERSION: app.getVersion()
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检查文件是否存在
|
|
||||||
const fs = require('fs');
|
|
||||||
if (!fs.existsSync(backendPath)) {
|
|
||||||
console.error('Backend file not found at:', backendPath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Backend file exists, starting process...');
|
|
||||||
console.log('Environment variables:', env);
|
|
||||||
|
|
||||||
backendProcess = spawn('node', [backendPath], {
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
|
||||||
detached: false,
|
|
||||||
cwd: isDev ? path.join(__dirname, '..') : process.resourcesPath,
|
|
||||||
env: env
|
|
||||||
});
|
|
||||||
|
|
||||||
backendProcess.stdout.on('data', (data) => {
|
|
||||||
console.log('Backend stdout:', data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
backendProcess.stderr.on('data', (data) => {
|
|
||||||
console.error('Backend stderr:', data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
backendProcess.on('close', (code) => {
|
|
||||||
console.log(`Backend process exited with code ${code}`);
|
|
||||||
backendProcess = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
backendProcess.on('error', (error) => {
|
|
||||||
console.error('Failed to start backend process:', error);
|
|
||||||
console.error('Error details:', error.message);
|
|
||||||
console.error('Error code:', error.code);
|
|
||||||
backendProcess = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 等待一下看看进程是否启动成功
|
|
||||||
setTimeout(() => {
|
|
||||||
if (backendProcess && !backendProcess.killed) {
|
|
||||||
console.log('Backend process appears to be running');
|
|
||||||
} else {
|
|
||||||
console.error('Backend process failed to start or died immediately');
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 停止后端服务
|
|
||||||
function stopBackendServer() {
|
|
||||||
if (backendProcess) {
|
|
||||||
console.log('Stopping backend server...');
|
|
||||||
backendProcess.kill();
|
|
||||||
backendProcess = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 防止多开
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
|
||||||
|
|
||||||
if (!gotTheLock) {
|
|
||||||
app.quit();
|
|
||||||
} else {
|
|
||||||
app.on('second-instance', () => {
|
|
||||||
if (mainWindow) {
|
|
||||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
|
||||||
mainWindow.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建主窗口
|
|
||||||
function createWindow() {
|
|
||||||
mainWindow = new BrowserWindow({
|
|
||||||
width: 1200,
|
|
||||||
height: 800,
|
|
||||||
minWidth: 800,
|
|
||||||
minHeight: 600,
|
|
||||||
title: 'Termix',
|
|
||||||
icon: path.join(__dirname, '..', 'public', 'icon.png'),
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: false,
|
|
||||||
contextIsolation: true,
|
|
||||||
preload: path.join(__dirname, 'preload-simple.cjs'),
|
|
||||||
webSecurity: !isDev
|
|
||||||
},
|
|
||||||
show: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建应用菜单(包含开发者工具快捷键)
|
|
||||||
const template = [
|
|
||||||
{
|
|
||||||
label: 'View',
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: 'Toggle Developer Tools',
|
|
||||||
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
|
||||||
click: () => {
|
|
||||||
mainWindow.webContents.toggleDevTools();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Reload',
|
|
||||||
accelerator: 'CmdOrCtrl+R',
|
|
||||||
click: () => {
|
|
||||||
mainWindow.webContents.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate(template);
|
|
||||||
Menu.setApplicationMenu(menu);
|
|
||||||
|
|
||||||
// 加载应用
|
|
||||||
if (isDev) {
|
|
||||||
// 开发环境:连接到 Vite 开发服务器
|
|
||||||
mainWindow.loadURL('http://localhost:5173');
|
|
||||||
mainWindow.webContents.openDevTools();
|
|
||||||
} else {
|
|
||||||
// 生产环境:加载构建后的文件
|
|
||||||
mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html'));
|
|
||||||
// 生产环境也启用开发者工具以便调试
|
|
||||||
mainWindow.webContents.openDevTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 窗口准备好后显示
|
|
||||||
mainWindow.once('ready-to-show', () => {
|
|
||||||
mainWindow.show();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理窗口关闭事件
|
|
||||||
mainWindow.on('closed', () => {
|
|
||||||
mainWindow = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理外部链接
|
|
||||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
||||||
shell.openExternal(url);
|
|
||||||
return { action: 'deny' };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPC 通信处理
|
|
||||||
ipcMain.handle('get-app-version', () => {
|
|
||||||
return app.getVersion();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('get-platform', () => {
|
|
||||||
return process.platform;
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('get-backend-port', () => {
|
|
||||||
return 8081; // 后端服务端口
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('restart-backend', async () => {
|
|
||||||
try {
|
|
||||||
stopBackendServer();
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
|
|
||||||
startBackendServer();
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 应用事件处理
|
|
||||||
app.whenReady().then(async () => {
|
|
||||||
// 启动后端服务
|
|
||||||
startBackendServer();
|
|
||||||
|
|
||||||
// 等待后端服务启动
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
createWindow();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
stopBackendServer();
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createWindow();
|
|
||||||
} else if (mainWindow) {
|
|
||||||
mainWindow.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理未捕获的异常
|
|
||||||
process.on('uncaughtException', (error) => {
|
|
||||||
console.error('Uncaught Exception:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
|
||||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
||||||
});
|
|
||||||
@@ -1,52 +1,24 @@
|
|||||||
const { app, BrowserWindow, Menu, Tray, shell, ipcMain, dialog } = require('electron');
|
const { app, BrowserWindow, Menu, shell, ipcMain } = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { spawn } = require('child_process');
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
// 动态导入可能有 ESM 问题的模块
|
// Global variables
|
||||||
let portfinder;
|
|
||||||
let Store;
|
|
||||||
let autoUpdater;
|
|
||||||
|
|
||||||
try {
|
|
||||||
portfinder = require('portfinder');
|
|
||||||
Store = require('electron-store');
|
|
||||||
const updaterModule = require('electron-updater');
|
|
||||||
autoUpdater = updaterModule.autoUpdater;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading modules:', error);
|
|
||||||
// 提供后备方案
|
|
||||||
portfinder = {
|
|
||||||
getPortPromise: async () => 18080 + Math.floor(Math.random() * 100)
|
|
||||||
};
|
|
||||||
Store = class {
|
|
||||||
constructor() { this.data = {}; }
|
|
||||||
get(key, defaultValue) { return this.data[key] || defaultValue; }
|
|
||||||
set(key, value) { this.data[key] = value; }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化配置存储
|
|
||||||
const store = new Store();
|
|
||||||
|
|
||||||
// 全局变量
|
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
let backendProcess = null;
|
|
||||||
let tray = null;
|
|
||||||
let backendPort = null;
|
let backendPort = null;
|
||||||
let isQuitting = false;
|
|
||||||
|
|
||||||
// 开发环境检测
|
// Development environment detection
|
||||||
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
|
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
|
||||||
|
|
||||||
// 防止多开
|
const BACKEND_CONFIG = {
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
port: 18080
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prevent multiple instances
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
if (!gotTheLock) {
|
if (!gotTheLock) {
|
||||||
app.quit();
|
app.quit();
|
||||||
} else {
|
} else {
|
||||||
app.on('second-instance', () => {
|
app.on('second-instance', () => {
|
||||||
// 如果用户试图运行第二个实例,我们应该聚焦我们的窗口
|
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
mainWindow.focus();
|
mainWindow.focus();
|
||||||
@@ -54,217 +26,43 @@ if (!gotTheLock) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 后端进程管理类
|
// Simple backend management
|
||||||
class BackendManager {
|
async function startBackend() {
|
||||||
constructor() {
|
try {
|
||||||
this.process = null;
|
console.log('Starting backend...');
|
||||||
this.port = null;
|
|
||||||
this.retryCount = 0;
|
// Set up environment
|
||||||
this.maxRetries = 3;
|
process.env.NODE_ENV = 'production';
|
||||||
this.isStarting = false;
|
process.env.PORT = BACKEND_CONFIG.port.toString();
|
||||||
this.healthCheckInterval = null;
|
process.env.DATA_PATH = app.getPath('userData');
|
||||||
}
|
process.env.DB_PATH = path.join(app.getPath('userData'), 'database.db');
|
||||||
|
process.env.VERSION = app.getVersion();
|
||||||
|
|
||||||
async findAvailablePort() {
|
// Get backend path
|
||||||
portfinder.basePort = store.get('backend.port', 18080);
|
const backendPath = isDev
|
||||||
try {
|
? path.join(__dirname, '..', 'dist', 'backend', 'starter.js')
|
||||||
const port = await portfinder.getPortPromise();
|
: path.join(process.resourcesPath, 'dist', 'backend', 'starter.js');
|
||||||
this.port = port;
|
|
||||||
return port;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error finding available port:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async start() {
|
console.log('Loading backend from:', backendPath);
|
||||||
if (this.isStarting || this.process) {
|
|
||||||
console.log('Backend already starting or running');
|
if (!fs.existsSync(backendPath)) {
|
||||||
return;
|
console.error('Backend file not found at:', backendPath);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isStarting = true;
|
// Load and start the backend
|
||||||
|
const { startServer } = await import(backendPath);
|
||||||
|
backendPort = await startServer();
|
||||||
|
console.log('Backend started successfully on port:', backendPort);
|
||||||
|
return backendPort;
|
||||||
|
|
||||||
try {
|
} catch (error) {
|
||||||
// 查找可用端口
|
console.error('Failed to start backend:', error);
|
||||||
await this.findAvailablePort();
|
return null;
|
||||||
console.log(`Starting backend on port ${this.port}`);
|
|
||||||
|
|
||||||
// 确定后端可执行文件路径
|
|
||||||
let backendPath;
|
|
||||||
if (isDev) {
|
|
||||||
// 开发环境:使用 node 运行构建后的 JS
|
|
||||||
backendPath = path.join(__dirname, '..', 'dist', 'backend', 'starter.js');
|
|
||||||
} else {
|
|
||||||
// 生产环境:使用打包后的后端
|
|
||||||
backendPath = path.join(process.resourcesPath, 'backend', 'starter.js');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保后端文件存在
|
|
||||||
if (!fs.existsSync(backendPath)) {
|
|
||||||
throw new Error(`Backend file not found at ${backendPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置环境变量
|
|
||||||
const env = {
|
|
||||||
...process.env,
|
|
||||||
PORT: this.port.toString(),
|
|
||||||
NODE_ENV: isDev ? 'development' : 'production',
|
|
||||||
DATA_PATH: app.getPath('userData'),
|
|
||||||
DB_PATH: path.join(app.getPath('userData'), 'database.db'),
|
|
||||||
};
|
|
||||||
|
|
||||||
// 启动后端进程
|
|
||||||
if (isDev) {
|
|
||||||
this.process = spawn('node', [backendPath], {
|
|
||||||
env,
|
|
||||||
cwd: path.join(__dirname, '..'),
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe']
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.process = spawn('node', [backendPath], {
|
|
||||||
env,
|
|
||||||
cwd: process.resourcesPath,
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听后端输出
|
|
||||||
this.process.stdout.on('data', (data) => {
|
|
||||||
console.log(`Backend stdout: ${data}`);
|
|
||||||
// 向渲染进程发送日志
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.webContents.send('backend-log', data.toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.process.stderr.on('data', (data) => {
|
|
||||||
console.error(`Backend stderr: ${data}`);
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.webContents.send('backend-error', data.toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听后端进程退出
|
|
||||||
this.process.on('exit', (code) => {
|
|
||||||
console.log(`Backend process exited with code ${code}`);
|
|
||||||
this.process = null;
|
|
||||||
this.isStarting = false;
|
|
||||||
|
|
||||||
// 如果不是正在退出且退出码不为0,尝试重启
|
|
||||||
if (!isQuitting && code !== 0 && this.retryCount < this.maxRetries) {
|
|
||||||
this.retryCount++;
|
|
||||||
console.log(`Attempting to restart backend (retry ${this.retryCount}/${this.maxRetries})`);
|
|
||||||
setTimeout(() => this.start(), 2000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 等待后端启动
|
|
||||||
await this.waitForBackend();
|
|
||||||
|
|
||||||
// 启动健康检查
|
|
||||||
this.startHealthCheck();
|
|
||||||
|
|
||||||
// 更新全局端口变量
|
|
||||||
backendPort = this.port;
|
|
||||||
|
|
||||||
// 通知渲染进程
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.webContents.send('backend-started', { port: this.port });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isStarting = false;
|
|
||||||
this.retryCount = 0;
|
|
||||||
|
|
||||||
return this.port;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to start backend:', error);
|
|
||||||
this.isStarting = false;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForBackend() {
|
|
||||||
const maxWaitTime = 30000; // 30秒
|
|
||||||
const checkInterval = 500; // 每500ms检查一次
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
while (Date.now() - startTime < maxWaitTime) {
|
|
||||||
try {
|
|
||||||
// 尝试连接后端健康检查端点
|
|
||||||
const response = await fetch(`http://127.0.0.1:${this.port}/health`);
|
|
||||||
if (response.ok) {
|
|
||||||
console.log('Backend is ready');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// 继续等待
|
|
||||||
}
|
|
||||||
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Backend failed to start within timeout period');
|
|
||||||
}
|
|
||||||
|
|
||||||
startHealthCheck() {
|
|
||||||
if (this.healthCheckInterval) {
|
|
||||||
clearInterval(this.healthCheckInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.healthCheckInterval = setInterval(async () => {
|
|
||||||
if (!this.process) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`http://127.0.0.1:${this.port}/health`);
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error('Backend health check failed');
|
|
||||||
// 可以在这里触发重启逻辑
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Backend health check error:', error);
|
|
||||||
}
|
|
||||||
}, 10000); // 每10秒检查一次
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (this.healthCheckInterval) {
|
|
||||||
clearInterval(this.healthCheckInterval);
|
|
||||||
this.healthCheckInterval = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.process) {
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Stopping backend process...');
|
|
||||||
|
|
||||||
// 设置超时强制杀死
|
|
||||||
const killTimeout = setTimeout(() => {
|
|
||||||
if (this.process) {
|
|
||||||
console.log('Force killing backend process');
|
|
||||||
this.process.kill('SIGKILL');
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
this.process.on('exit', () => {
|
|
||||||
clearTimeout(killTimeout);
|
|
||||||
this.process = null;
|
|
||||||
console.log('Backend process stopped');
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 优雅关闭
|
|
||||||
this.process.kill('SIGTERM');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建后端管理器实例
|
// Create main window
|
||||||
const backendManager = new BackendManager();
|
|
||||||
|
|
||||||
// 创建主窗口
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
@@ -272,44 +70,51 @@ function createWindow() {
|
|||||||
minWidth: 800,
|
minWidth: 800,
|
||||||
minHeight: 600,
|
minHeight: 600,
|
||||||
title: 'Termix',
|
title: 'Termix',
|
||||||
icon: path.join(__dirname, '..', 'public', 'icon.png'),
|
icon: isDev
|
||||||
|
? path.join(__dirname, '..', 'public', 'icon.png')
|
||||||
|
: path.join(process.resourcesPath, 'public', 'icon.png'),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
preload: path.join(__dirname, 'preload.cjs'),
|
|
||||||
webSecurity: !isDev
|
webSecurity: !isDev
|
||||||
},
|
},
|
||||||
show: false, // 先不显示,等加载完成
|
show: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// 移除默认菜单栏(Windows/Linux)
|
// Remove menu bar on Windows/Linux
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
mainWindow.setMenuBarVisibility(false);
|
mainWindow.setMenuBarVisibility(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载应用
|
// Load application
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
// 开发环境:连接到 Vite 开发服务器
|
|
||||||
mainWindow.loadURL('http://localhost:5173');
|
mainWindow.loadURL('http://localhost:5173');
|
||||||
mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools();
|
||||||
} else {
|
} else {
|
||||||
// 生产环境:加载构建后的文件
|
// Load from dist folder
|
||||||
mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html'));
|
const indexPath = path.join(__dirname, '..', 'dist', 'index.html');
|
||||||
|
console.log('Loading frontend from:', indexPath);
|
||||||
|
mainWindow.loadFile(indexPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 窗口准备好后显示
|
// Show window when ready
|
||||||
mainWindow.once('ready-to-show', () => {
|
mainWindow.once('ready-to-show', () => {
|
||||||
|
console.log('Window ready to show');
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理窗口关闭事件
|
// Handle load errors
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle window close
|
||||||
mainWindow.on('close', (event) => {
|
mainWindow.on('close', (event) => {
|
||||||
if (!isQuitting && process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
// macOS:隐藏窗口而不是退出
|
|
||||||
event.preventDefault();
|
|
||||||
mainWindow.hide();
|
|
||||||
} else if (!isQuitting && store.get('minimizeToTray', true)) {
|
|
||||||
// Windows/Linux:最小化到托盘
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
mainWindow.hide();
|
mainWindow.hide();
|
||||||
}
|
}
|
||||||
@@ -319,53 +124,14 @@ function createWindow() {
|
|||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理外部链接
|
// Handle external links
|
||||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
shell.openExternal(url);
|
shell.openExternal(url);
|
||||||
return { action: 'deny' };
|
return { action: 'deny' };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建系统托盘
|
// IPC handlers
|
||||||
function createTray() {
|
|
||||||
if (process.platform === 'darwin') return; // macOS 不需要托盘
|
|
||||||
|
|
||||||
const iconPath = path.join(__dirname, '..', 'public', 'icon.png');
|
|
||||||
tray = new Tray(iconPath);
|
|
||||||
|
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
|
||||||
{
|
|
||||||
label: '显示',
|
|
||||||
click: () => {
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.show();
|
|
||||||
mainWindow.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: '退出',
|
|
||||||
click: () => {
|
|
||||||
isQuitting = true;
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
tray.setToolTip('Termix');
|
|
||||||
tray.setContextMenu(contextMenu);
|
|
||||||
|
|
||||||
// 双击托盘图标显示窗口
|
|
||||||
tray.on('double-click', () => {
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.show();
|
|
||||||
mainWindow.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPC 通信处理
|
|
||||||
ipcMain.handle('get-backend-port', () => {
|
ipcMain.handle('get-backend-port', () => {
|
||||||
return backendPort;
|
return backendPort;
|
||||||
});
|
});
|
||||||
@@ -380,61 +146,29 @@ ipcMain.handle('get-platform', () => {
|
|||||||
|
|
||||||
ipcMain.handle('restart-backend', async () => {
|
ipcMain.handle('restart-backend', async () => {
|
||||||
try {
|
try {
|
||||||
await backendManager.stop();
|
backendPort = await startBackend();
|
||||||
await backendManager.start();
|
return { success: true, port: backendPort };
|
||||||
return { success: true, port: backendManager.port };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('show-save-dialog', async (event, options) => {
|
// App event handlers
|
||||||
const result = await dialog.showSaveDialog(mainWindow, options);
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('show-open-dialog', async (event, options) => {
|
|
||||||
const result = await dialog.showOpenDialog(mainWindow, options);
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 自动更新
|
|
||||||
if (!isDev && autoUpdater) {
|
|
||||||
try {
|
|
||||||
autoUpdater.checkForUpdatesAndNotify();
|
|
||||||
|
|
||||||
autoUpdater.on('update-available', () => {
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.webContents.send('update-available');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
autoUpdater.on('update-downloaded', () => {
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.webContents.send('update-downloaded');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Auto-updater not available:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用事件处理
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
|
// Create window immediately for fast startup
|
||||||
|
createWindow();
|
||||||
|
|
||||||
|
// Start backend in background (non-blocking)
|
||||||
try {
|
try {
|
||||||
// 启动后端
|
backendPort = await startBackend();
|
||||||
await backendManager.start();
|
if (backendPort) {
|
||||||
|
console.log('Backend started successfully on port:', backendPort);
|
||||||
// 创建窗口
|
}
|
||||||
createWindow();
|
|
||||||
|
|
||||||
// 创建托盘
|
|
||||||
createTray();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize application:', error);
|
console.error('Backend failed to start:', error);
|
||||||
dialog.showErrorBox('启动失败', `无法启动应用: ${error.message}`);
|
|
||||||
app.quit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Termix started successfully');
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
@@ -451,17 +185,15 @@ app.on('activate', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('before-quit', async () => {
|
app.on('before-quit', () => {
|
||||||
isQuitting = true;
|
console.log('App is quitting...');
|
||||||
await backendManager.stop();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理未捕获的异常
|
// Error handling
|
||||||
process.on('uncaughtException', (error) => {
|
process.on('uncaughtException', (error) => {
|
||||||
console.error('Uncaught Exception:', error);
|
console.error('Uncaught Exception:', error);
|
||||||
dialog.showErrorBox('未捕获的异常', error.message);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||||
});
|
});
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron');
|
|
||||||
|
|
||||||
// 暴露简化的 API 给渲染进程
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
|
||||||
// 获取应用版本
|
|
||||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
|
||||||
|
|
||||||
// 获取平台信息
|
|
||||||
getPlatform: () => ipcRenderer.invoke('get-platform'),
|
|
||||||
|
|
||||||
// 获取后端端口
|
|
||||||
getBackendPort: () => ipcRenderer.invoke('get-backend-port'),
|
|
||||||
|
|
||||||
// 重启后端服务
|
|
||||||
restartBackend: () => ipcRenderer.invoke('restart-backend'),
|
|
||||||
|
|
||||||
// 环境检测
|
|
||||||
isElectron: true,
|
|
||||||
isDev: process.env.NODE_ENV === 'development',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加一个标识,让渲染进程知道这是 Electron 环境
|
|
||||||
// 在上下文隔离环境中,使用 contextBridge 暴露
|
|
||||||
contextBridge.exposeInMainWorld('IS_ELECTRON', true);
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron');
|
|
||||||
|
|
||||||
// 暴露安全的 API 给渲染进程
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
|
||||||
// 获取后端端口
|
|
||||||
getBackendPort: () => ipcRenderer.invoke('get-backend-port'),
|
|
||||||
|
|
||||||
// 获取应用版本
|
|
||||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
|
||||||
|
|
||||||
// 获取平台信息
|
|
||||||
getPlatform: () => ipcRenderer.invoke('get-platform'),
|
|
||||||
|
|
||||||
// 重启后端
|
|
||||||
restartBackend: () => ipcRenderer.invoke('restart-backend'),
|
|
||||||
|
|
||||||
// 文件对话框
|
|
||||||
showSaveDialog: (options) => ipcRenderer.invoke('show-save-dialog', options),
|
|
||||||
showOpenDialog: (options) => ipcRenderer.invoke('show-open-dialog', options),
|
|
||||||
|
|
||||||
// 监听后端事件
|
|
||||||
onBackendStarted: (callback) => {
|
|
||||||
ipcRenderer.on('backend-started', (event, data) => callback(data));
|
|
||||||
},
|
|
||||||
|
|
||||||
onBackendLog: (callback) => {
|
|
||||||
ipcRenderer.on('backend-log', (event, data) => callback(data));
|
|
||||||
},
|
|
||||||
|
|
||||||
onBackendError: (callback) => {
|
|
||||||
ipcRenderer.on('backend-error', (event, data) => callback(data));
|
|
||||||
},
|
|
||||||
|
|
||||||
// 监听更新事件
|
|
||||||
onUpdateAvailable: (callback) => {
|
|
||||||
ipcRenderer.on('update-available', () => callback());
|
|
||||||
},
|
|
||||||
|
|
||||||
onUpdateDownloaded: (callback) => {
|
|
||||||
ipcRenderer.on('update-downloaded', () => callback());
|
|
||||||
},
|
|
||||||
|
|
||||||
// 移除事件监听器
|
|
||||||
removeAllListeners: (channel) => {
|
|
||||||
ipcRenderer.removeAllListeners(channel);
|
|
||||||
},
|
|
||||||
|
|
||||||
// 环境检测
|
|
||||||
isElectron: true,
|
|
||||||
isDev: process.env.NODE_ENV === 'development',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加一个标识,让渲染进程知道这是 Electron 环境
|
|
||||||
window.IS_ELECTRON = true;
|
|
||||||
@@ -4,20 +4,19 @@
|
|||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
|
"description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities",
|
||||||
"author": "Karmaa",
|
"author": "Karmaa",
|
||||||
"main": "electron/main-simple.cjs",
|
"main": "electron/main.cjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build && tsc -p tsconfig.node.json",
|
||||||
"build:backend": "tsc -p tsconfig.node.json",
|
|
||||||
"dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/backend/starter.js",
|
"dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/backend/starter.js",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"electron": "electron .",
|
"electron": "electron .",
|
||||||
"electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:5173 && electron .\"",
|
"electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:5173 && electron .\"",
|
||||||
"electron:build": "npm run build && npm run build:backend && electron-builder --dir",
|
"build:win-portable": "npm run build && electron-builder --win --dir",
|
||||||
"electron:build-win": "npm run build && npm run build:backend && electron-builder --win --dir",
|
"build:win-installer": "npm run build && electron-builder --win --publish=never",
|
||||||
"electron:pack": "npm run build && npm run build:backend && electron-packager . Termix --platform=all --arch=x64,arm64 --out=release --overwrite"
|
"build:linux-portable": "npm run build && electron-builder --linux --dir"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
@@ -108,6 +107,7 @@
|
|||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"electron": "^38.0.0",
|
"electron": "^38.0.0",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
|
"electron-icon-builder": "^2.0.1",
|
||||||
"electron-packager": "^17.1.2",
|
"electron-packager": "^17.1.2",
|
||||||
"eslint": "^9.34.0",
|
"eslint": "^9.34.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
Before Width: | Height: | Size: 105 B After Width: | Height: | Size: 16 KiB |
@@ -1,80 +1 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" zoomAndPan="magnify" viewBox="0 0 375 374.999991" height="500" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><g/><clipPath id="1761bbaa89"><path d="M 0.101562 0 L 375 0 L 375 375 L 0.101562 375 Z M 0.101562 0 " clip-rule="nonzero"/></clipPath><clipPath id="3db8e9a507"><path d="M 57.101562 0 L 318 0 C 333.117188 0 347.617188 6.003906 358.304688 16.695312 C 368.996094 27.382812 375 41.882812 375 57 L 375 318 C 375 333.117188 368.996094 347.617188 358.304688 358.304688 C 347.617188 368.996094 333.117188 375 318 375 L 57.101562 375 C 41.984375 375 27.484375 368.996094 16.796875 358.304688 C 6.105469 347.617188 0.101562 333.117188 0.101562 318 L 0.101562 57 C 0.101562 41.882812 6.105469 27.382812 16.796875 16.695312 C 27.484375 6.003906 41.984375 0 57.101562 0 Z M 57.101562 0 " clip-rule="nonzero"/></clipPath><clipPath id="fb4435a1fb"><path d="M 0.101562 0 L 375 0 L 375 375 L 0.101562 375 Z M 0.101562 0 " clip-rule="nonzero"/></clipPath><clipPath id="573455ae23"><path d="M 57.101562 0 L 318 0 C 333.117188 0 347.617188 6.003906 358.304688 16.695312 C 368.996094 27.382812 375 41.882812 375 57 L 375 318 C 375 333.117188 368.996094 347.617188 358.304688 358.304688 C 347.617188 368.996094 333.117188 375 318 375 L 57.101562 375 C 41.984375 375 27.484375 368.996094 16.796875 358.304688 C 6.105469 347.617188 0.101562 333.117188 0.101562 318 L 0.101562 57 C 0.101562 41.882812 6.105469 27.382812 16.796875 16.695312 C 27.484375 6.003906 41.984375 0 57.101562 0 Z M 57.101562 0 " clip-rule="nonzero"/></clipPath><clipPath id="66023f0a94"><rect x="0" width="375" y="0" height="375"/></clipPath><clipPath id="096103623d"><path d="M 0.101562 0.00390625 L 375 0.00390625 L 375 375 L 0.101562 375 Z M 0.101562 0.00390625 " clip-rule="nonzero"/></clipPath><clipPath id="38e0707f78"><path d="M 57.101562 0.00390625 L 317.996094 0.00390625 C 333.113281 0.00390625 347.613281 6.007812 358.300781 16.695312 C 368.992188 27.386719 374.996094 41.882812 374.996094 57 L 374.996094 318 C 374.996094 333.117188 368.992188 347.613281 358.300781 358.304688 C 347.613281 368.992188 333.113281 374.996094 317.996094 374.996094 L 57.101562 374.996094 C 41.984375 374.996094 27.484375 368.992188 16.796875 358.304688 C 6.105469 347.613281 0.101562 333.117188 0.101562 318 L 0.101562 57 C 0.101562 41.882812 6.105469 27.386719 16.796875 16.695312 C 27.484375 6.007812 41.984375 0.00390625 57.101562 0.00390625 Z M 57.101562 0.00390625 " clip-rule="nonzero"/></clipPath><clipPath id="d42a783983"><rect x="0" width="131" y="0" height="224"/></clipPath><clipPath id="a8ddcf3d5e"><rect x="0" width="115" y="0" height="222"/></clipPath></defs><g clip-path="url(#1761bbaa89)"><g clip-path="url(#3db8e9a507)"><g transform="matrix(1, 0, 0, 1, 0, 0)"><g clip-path="url(#66023f0a94)"><g clip-path="url(#fb4435a1fb)"><g clip-path="url(#573455ae23)"><path fill="#18181b" d="M 0.101562 0 L 375 0 L 375 374.898438 L 0.101562 374.898438 Z M 0.101562 0 " fill-opacity="1" fill-rule="nonzero"/></g></g></g></g></g></g><g clip-path="url(#096103623d)"><g clip-path="url(#38e0707f78)"><path stroke-linecap="butt" transform="matrix(0.75, 0, 0, 0.75, 0.101056, 0.00221852)" fill="none" stroke-linejoin="miter" d="M 76.000681 0.0022503 L 423.860079 0.0022503 C 444.01633 0.0022503 463.349665 8.007459 477.599666 22.25746 C 491.854875 36.512669 499.860084 55.840796 499.860084 75.997047 L 499.860084 423.99707 C 499.860084 444.153321 491.854875 463.481447 477.599666 477.736657 C 463.349665 491.986658 444.01633 499.991866 423.860079 499.991866 L 76.000681 499.991866 C 55.84443 499.991866 36.511095 491.986658 22.261094 477.736657 C 8.005885 463.481447 0.000675892 444.153321 0.000675892 423.99707 L 0.000675892 75.997047 C 0.000675892 55.840796 8.005885 36.512669 22.261094 22.25746 C 36.511095 8.007459 55.84443 0.0022503 76.000681 0.0022503 Z M 76.000681 0.0022503 " stroke="#2e2e30" stroke-width="28" stroke-opacity="1" stroke-miterlimit="4"/></g></g><path stroke-linecap="round" transform="matrix(0.75, -0.000000000000000093, 0.000000000000000093, 0.75, 51.908154, 43.885444)" fill="none" stroke-linejoin="miter" d="M 27.997463 28.001701 L 340.638109 28.001701 " stroke="#e6e6e6" stroke-width="56" stroke-opacity="1" stroke-miterlimit="4"/><path stroke-linecap="round" transform="matrix(0, 0.75, -0.75, 0, 208.892597, 64.889403)" fill="none" stroke-linejoin="miter" d="M 25.001631 24.997423 L 330.152693 24.997423 " stroke="#e6e6e6" stroke-width="50" stroke-opacity="1" stroke-miterlimit="4"/><g transform="matrix(1, 0, 0, 1, 69, 101)"><g clip-path="url(#d42a783983)"><g fill="#14191f" fill-opacity="1"><g transform="translate(6.947029, 160.304978)"><g><path d="M 4.691406 -62.90625 L 68.734375 -87.59375 L 73.878906 -74.238281 L 68.667969 -60.910156 L -4.472656 -89.511719 L -4.472656 -145.9375 L 122 -94.609375 L 122 -54.226562 L -4.472656 -2.894531 L -4.472656 -59.371094 Z M 14.984375 -36.199219 L 9.839844 -49.550781 L 24.Line truncated
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="201.47884mm"
|
|
||||||
height="168.06284mm"
|
|
||||||
viewBox="0 0 201.47884 168.06284"
|
|
||||||
version="1.1"
|
|
||||||
id="svg5"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs2" />
|
|
||||||
<g
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-99.862555,-88.982953)">
|
|
||||||
<g
|
|
||||||
id="g2"
|
|
||||||
transform="matrix(1.238566,0,0,1.238566,-497.97829,-269.39983)">
|
|
||||||
<path
|
|
||||||
id="path68"
|
|
||||||
style="fill:#3f3f45;fill-opacity:1;stroke:none;stroke-width:3.175;stroke-linecap:square;stroke-miterlimit:8;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="m 493.79462,290.70621 h 140.28116 c 5.3225,0 9.6074,4.28488 9.6074,9.60736 v 113.30061 c 0,5.32248 -4.2849,9.60737 -9.6074,9.60737 H 493.79462 c -5.3224,0 -9.6073,-4.28489 -9.6073,-9.60737 V 300.31357 c 0,-5.32248 4.2849,-9.60736 9.6073,-9.60736 z" />
|
|
||||||
<path
|
|
||||||
id="path69"
|
|
||||||
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#090909;fill-opacity:1;stroke-linecap:square;stroke-miterlimit:8;enable-background:accumulate;stop-color:#000000"
|
|
||||||
d="m 493.88312,289.353 c -6.1745,0 -11.1952,5.02069 -11.1952,11.19518 v 113.30058 c 0,6.1745 5.0207,11.1957 11.1952,11.1957 h 140.28066 c 6.1745,0 11.1952,-5.0212 11.1952,-11.1957 V 300.54818 c 0,-6.1745 -5.0207,-11.19518 -11.1952,-11.19518 z m 0,3.17552 h 140.28066 c 4.4705,0 8.0197,3.5492 8.0197,8.01966 V 385.0717 H 485.86132 v -84.52352 c 0,-4.47046 3.5513,-8.01966 8.0218,-8.01966 z m -8.0218,95.71818 h 156.32216 v 25.60206 c 0,4.47047 -3.5492,8.01966 -8.0197,8.01966 H 493.88312 c -4.4705,0 -8.0218,-3.54919 -8.0218,-8.01966 z" />
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-weight:800;font-size:25.1593px;line-height:1.25;font-family:'Argentum Sans';-inkscape-font-specification:'Argentum Sans, Ultra-Bold';letter-spacing:0px;word-spacing:0px;fill:#e4e4e4;fill-opacity:1;stroke-width:0.336953"
|
|
||||||
x="497.09778"
|
|
||||||
y="410.97794"
|
|
||||||
id="text69"><tspan
|
|
||||||
id="tspan69"
|
|
||||||
style="fill:#e4e4e4;fill-opacity:1;stroke-width:0.336953"
|
|
||||||
x="497.09778"
|
|
||||||
y="410.97794">>_</tspan></text>
|
|
||||||
<path
|
|
||||||
style="fill:#00ff00;fill-opacity:1;stroke:none;stroke-width:6.11001;stroke-linecap:square;stroke-miterlimit:8;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="m 503.38876,299.01072 c -1.3765,0 -2.4846,1.10862 -2.4846,2.48512 v 14.07976 c 0,1.37651 1.1081,2.48461 2.4846,2.48461 h 115.66187 c 1.3765,0 2.4851,-1.1081 2.4851,-2.48461 v -14.07976 c 0,-1.3765 -1.1086,-2.48512 -2.4851,-2.48512 z"
|
|
||||||
id="rect31" />
|
|
||||||
<rect
|
|
||||||
style="fill:#00ff00;fill-opacity:1;stroke:none;stroke-width:0.170087;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4.4"
|
|
||||||
id="rect2"
|
|
||||||
width="87.026749"
|
|
||||||
height="16.21431"
|
|
||||||
x="500.90417"
|
|
||||||
y="321.50845"
|
|
||||||
ry="2.1688685" />
|
|
||||||
<rect
|
|
||||||
style="fill:#00ff00;fill-opacity:1;stroke:none;stroke-width:0.170086;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4.4"
|
|
||||||
id="rect3"
|
|
||||||
width="113.76796"
|
|
||||||
height="10.175971"
|
|
||||||
x="500.90417"
|
|
||||||
y="341.17099"
|
|
||||||
ry="1.3611646" />
|
|
||||||
<rect
|
|
||||||
style="fill:#00ff00;fill-opacity:1;stroke:none;stroke-width:0.170086;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4.4"
|
|
||||||
id="rect4"
|
|
||||||
width="101.98421"
|
|
||||||
height="10.175971"
|
|
||||||
x="500.90417"
|
|
||||||
y="354.79523"
|
|
||||||
ry="1.3611646" />
|
|
||||||
<rect
|
|
||||||
style="fill:#00ff00;fill-opacity:1;stroke:none;stroke-width:0.170086;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4.4"
|
|
||||||
id="rect5"
|
|
||||||
width="52.736389"
|
|
||||||
height="10.175971"
|
|
||||||
x="500.90417"
|
|
||||||
y="368.41943"
|
|
||||||
ry="1.3611646" />
|
|
||||||
<path
|
|
||||||
d="m 519.16076,303.77439 h -5.40281 l -3.07739,12.02691 h -3.35346 l 3.07738,-12.02691 h -5.32 l 0.63032,-2.46336 h 14.07626 z m 11.15437,11.98551 h -7.05883 q -1.63534,0 -2.95578,-0.7452 -1.42877,-0.80732 -1.84795,-2.56685 -0.3985,-1.75954 0.14176,-3.87096 0.53497,-2.09077 1.79731,-3.78819 1.38223,-1.84233 3.21402,-2.69105 1.00665,-0.45541 1.86162,-0.6417 0.8757,-0.18631 1.93142,-0.18631 h 6.54132 l -0.65679,2.56684 h -6.04451 q -0.80731,0 -1.42449,0.22771 -0.59119,0.207 -1.2724,0.76592 -0.68123,0.5589 -1.02594,1.09712 -0.33942,0.51751 -0.5407,1.30411 h 9.52219 l -0.65679,2.56688 h -9.48078 q -0.1801,0.70379 -0.13152,1.24201 0.0539,0.51751 0.38701,1.07642 0.35379,0.55892 0.79715,0.84872 0.46405,0.28979 1.18856,0.28979 h 6.35503 z m 16.95364,0 h -4.07798 l -2.30077,-5.65121 h -3.89167 l -1.446,5.65121 h -3.47767 l 3.7077,-14.49027 h 10.5986 q 1.80093,0 2.62068,1.40764 0.82503,1.38692 0.33776,3.29136 -0.38668,1.51112 -1.67114,2.56684 -1.28444,1.05574 -2.90582,1.32482 z m -2.07376,-9.93617 q 0.25953,-1.01434 -0.36867,-1.55254 -0.6022,-0.55891 -1.88565,-0.55891 h -4.34706 l -1.03815,4.05729 h 4.45057 q 0.97291,0 1.85007,-0.43472 1.08995,-0.53821 1.33889,-1.51112 z m 20.97321,9.93617 h -3.12576 l 2.83373,-11.0747 -6.93241,11.0747 h -3.72607 l -1.32558,-11.32311 -2.89729,11.32311 h -3.02227 l 3.70769,-14.49027 h 4.84389 l 1.30922,11.46801 7.01234,-11.46801 h 5.0302 z m 5.98242,0 h -3.87097 l 3.70769,-14.49027 h 3.87098 z m 17.84375,0 h -4.22289 l -2.46594,-4.84389 -4.68629,4.8853 h -4.32637 l 7.49976,-7.22442 -3.65727,-7.30727 h 4.34708 l 2.07794,4.82318 4.77388,-4.82318 h 4.22289 l -7.42224,7.24515 z"
|
|
||||||
id="path45"
|
|
||||||
style="font-size:19.7556px;line-height:1.25;font-family:Denmark;-inkscape-font-specification:'Denmark, Normal';letter-spacing:0px;word-spacing:0px;fill:#26262b;fill-opacity:1;stroke-width:0.264583" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 483 B |
|
After Width: | Height: | Size: 749 B |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 986 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 353 KiB |
@@ -2,11 +2,13 @@
|
|||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next';
|
||||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
import HttpApi from 'i18next-http-backend';
|
|
||||||
|
// Import translation files directly
|
||||||
|
import enTranslation from '../locales/en/translation.json';
|
||||||
|
import zhTranslation from '../locales/zh/translation.json';
|
||||||
|
|
||||||
// Initialize i18n
|
// Initialize i18n
|
||||||
i18n
|
i18n
|
||||||
.use(HttpApi) // Load translations using http
|
|
||||||
.use(LanguageDetector) // Detect user language
|
.use(LanguageDetector) // Detect user language
|
||||||
.use(initReactI18next) // Pass i18n instance to react-i18next
|
.use(initReactI18next) // Pass i18n instance to react-i18next
|
||||||
.init({
|
.init({
|
||||||
@@ -23,9 +25,14 @@ i18n
|
|||||||
checkWhitelist: true,
|
checkWhitelist: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Backend options
|
// Resources - load translations directly
|
||||||
backend: {
|
resources: {
|
||||||
loadPath: './locales/{{lng}}/translation.json',
|
en: {
|
||||||
|
translation: enTranslation
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
translation: zhTranslation
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input.tsx";
|
||||||
import { FormControl, FormItem, FormLabel } from "@/components/ui/form";
|
import { FormControl, FormItem, FormLabel } from "@/components/ui/form.tsx";
|
||||||
import { getCredentials } from '@/ui/main-axios';
|
import { getCredentials } from '@/ui/main-axios.ts';
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { Credential } from '../types/index.js';
|
import type { Credential } from '../../../../types';
|
||||||
|
|
||||||
interface CredentialSelectorProps {
|
interface CredentialSelectorProps {
|
||||||
value?: number | null;
|
value?: number | null;
|
||||||
@@ -23,7 +23,7 @@ import {Alert, AlertDescription} from "@/components/ui/alert.tsx";
|
|||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {createSSHHost, updateSSHHost, getSSHHosts, getCredentials} from '@/ui/main-axios.ts';
|
import {createSSHHost, updateSSHHost, getSSHHosts, getCredentials} from '@/ui/main-axios.ts';
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import {CredentialSelector} from "@/components/CredentialSelector.tsx";
|
import {CredentialSelector} from "@/ui/Desktop/Apps/Credentials/CredentialSelector.tsx";
|
||||||
|
|
||||||
interface SSHHost {
|
interface SSHHost {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {PasswordInput} from "@/components/ui/password-input.tsx";
|
|||||||
import {Label} from "@/components/ui/label.tsx";
|
import {Label} from "@/components/ui/label.tsx";
|
||||||
import {Alert, AlertTitle, AlertDescription} from "@/components/ui/alert.tsx";
|
import {Alert, AlertTitle, AlertDescription} from "@/components/ui/alert.tsx";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import {LanguageSwitcher} from "@/components/LanguageSwitcher.tsx";
|
import {LanguageSwitcher} from "@/ui/Desktop/User/LanguageSwitcher.tsx";
|
||||||
import {
|
import {
|
||||||
registerUser,
|
registerUser,
|
||||||
loginUser,
|
loginUser,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select.tsx';
|
||||||
import { Globe } from 'lucide-react';
|
import { Globe } from 'lucide-react';
|
||||||
|
|
||||||
const languages = [
|
const languages = [
|
||||||
@@ -13,7 +13,7 @@ import {getVersionInfo} from "@/ui/main-axios.ts";
|
|||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {PasswordReset} from "@/ui/Desktop/User/PasswordReset.tsx";
|
import {PasswordReset} from "@/ui/Desktop/User/PasswordReset.tsx";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import {LanguageSwitcher} from "@/components/LanguageSwitcher.tsx";
|
import {LanguageSwitcher} from "@/ui/Desktop/User/LanguageSwitcher.tsx";
|
||||||
import {useSidebar} from "@/components/ui/sidebar.tsx";
|
import {useSidebar} from "@/components/ui/sidebar.tsx";
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {Input} from "@/components/ui/input.tsx";
|
|||||||
import {Label} from "@/components/ui/label.tsx";
|
import {Label} from "@/components/ui/label.tsx";
|
||||||
import {Alert, AlertTitle, AlertDescription} from "@/components/ui/alert.tsx";
|
import {Alert, AlertTitle, AlertDescription} from "@/components/ui/alert.tsx";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import {LanguageSwitcher} from "@/components/LanguageSwitcher.tsx";
|
import {LanguageSwitcher} from "@/ui/Desktop/User/LanguageSwitcher.tsx";
|
||||||
import {
|
import {
|
||||||
registerUser,
|
registerUser,
|
||||||
loginUser,
|
loginUser,
|
||||||
|
|||||||