Files
FunConnect/client/src/main/index.ts
FunMC 9649519745 feat: v1.1.0 迭代更新
Server:
- 添加房间密码验证 POST /rooms/:id/join
- 添加玩家踢出 POST /rooms/:id/kick/:playerId
- 添加房间过期自动清理(30分钟无活动)
- 添加流量统计 GET /traffic
- 添加token认证中间件保护写操作API
- 房间详情返回玩家列表

Client:
- 添加设置持久化(electron-store)
- 添加设置页面(玩家名、本地端口、自动重连、托盘最小化)
- 添加系统托盘支持(最小化到托盘、右键菜单)
- 添加最近连接服务器记录
- 连接成功自动保存服务器地址
- 加入房间自动填充默认端口
2026-02-22 23:38:41 +08:00

248 lines
7.3 KiB
TypeScript

import { app, BrowserWindow, ipcMain, dialog, Tray, Menu, nativeImage } from 'electron';
import * as path from 'path';
import Store from 'electron-store';
import { RelayClient } from './relay-client';
import { LocalProxy } from './local-proxy';
import { ApiClient } from './api-client';
const store = new Store({
defaults: {
serverUrl: 'http://localhost:3000',
playerName: 'Player',
localPort: 25566,
recentServers: [] as string[],
autoReconnect: true,
minimizeToTray: true,
}
});
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
let relayClient: RelayClient | null = null;
let localProxy: LocalProxy | null = null;
let apiClient: ApiClient | null = null;
function createWindow() {
mainWindow = new BrowserWindow({
width: 900,
height: 650,
minWidth: 800,
minHeight: 600,
title: 'FunConnect - Minecraft 联机客户端',
frame: false,
titleBarStyle: 'hidden',
backgroundColor: '#1a1a2e',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
mainWindow.loadFile(path.join(__dirname, '../../renderer/index.html'));
mainWindow.on('close', (e) => {
if (store.get('minimizeToTray') && tray) {
e.preventDefault();
mainWindow?.hide();
}
});
mainWindow.on('closed', () => {
mainWindow = null;
cleanup();
});
}
function createTray() {
const icon = nativeImage.createEmpty();
tray = new Tray(icon);
tray.setToolTip('FunConnect - Minecraft 联机客户端');
tray.setContextMenu(Menu.buildFromTemplate([
{ label: '显示主窗口', click: () => { mainWindow?.show(); mainWindow?.focus(); } },
{ type: 'separator' },
{ label: '断开连接', click: () => cleanup() },
{ type: 'separator' },
{ label: '退出', click: () => { cleanup(); app.quit(); } },
]));
tray.on('double-click', () => { mainWindow?.show(); mainWindow?.focus(); });
}
function cleanup() {
if (localProxy) {
localProxy.stop();
localProxy = null;
}
if (relayClient) {
relayClient.disconnect();
relayClient = null;
}
}
app.whenReady().then(() => {
createWindow();
createTray();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
else mainWindow?.show();
});
});
app.on('window-all-closed', () => {
cleanup();
if (process.platform !== 'darwin') app.quit();
});
// ===== IPC Handlers =====
// Window controls
ipcMain.handle('window:minimize', () => mainWindow?.minimize());
ipcMain.handle('window:maximize', () => {
if (mainWindow?.isMaximized()) mainWindow.unmaximize();
else mainWindow?.maximize();
});
ipcMain.handle('window:close', () => mainWindow?.close());
// Server connection
ipcMain.handle('server:connect', async (_event, serverUrl: string) => {
try {
apiClient = new ApiClient(serverUrl);
const health = await apiClient.getHealth();
return { success: true, data: health };
} catch (err: any) {
return { success: false, error: err.message };
}
});
// Room operations
ipcMain.handle('rooms:list', async () => {
if (!apiClient) return { success: false, error: '未连接服务器' };
try {
const data = await apiClient.getRooms();
return { success: true, data };
} catch (err: any) {
return { success: false, error: err.message };
}
});
ipcMain.handle('rooms:create', async (_event, roomData: any) => {
if (!apiClient) return { success: false, error: '未连接服务器' };
try {
const data = await apiClient.createRoom(roomData);
return { success: true, data };
} catch (err: any) {
return { success: false, error: err.response?.data?.error || err.message };
}
});
ipcMain.handle('rooms:delete', async (_event, roomId: string) => {
if (!apiClient) return { success: false, error: '未连接服务器' };
try {
await apiClient.deleteRoom(roomId);
return { success: true };
} catch (err: any) {
return { success: false, error: err.message };
}
});
// Join room - start local proxy and relay connection
ipcMain.handle('rooms:join', async (_event, opts: { serverHost: string; serverPort: number; roomId: string; localPort: number }) => {
try {
cleanup();
relayClient = new RelayClient(opts.serverHost, opts.serverPort, opts.roomId);
localProxy = new LocalProxy(opts.localPort, relayClient);
await localProxy.start();
relayClient.on('connected', () => {
mainWindow?.webContents.send('relay:status', { status: 'connected' });
});
relayClient.on('disconnected', () => {
mainWindow?.webContents.send('relay:status', { status: 'disconnected' });
});
relayClient.on('error', (err: string) => {
mainWindow?.webContents.send('relay:status', { status: 'error', error: err });
});
return { success: true, localPort: opts.localPort };
} catch (err: any) {
return { success: false, error: err.message };
}
});
// Host room - start relay and local proxy from local MC server
ipcMain.handle('rooms:host', async (_event, opts: { serverUrl: string; roomName: string; localMcPort: number; gameVersion: string; gameEdition: string; maxPlayers: number; password?: string }) => {
if (!apiClient) return { success: false, error: '未连接服务器' };
try {
const result = await apiClient.createRoom({
name: opts.roomName,
hostName: 'FunConnect',
hostPort: opts.localMcPort,
gameVersion: opts.gameVersion,
gameEdition: opts.gameEdition,
maxPlayers: opts.maxPlayers,
password: opts.password,
});
return { success: true, data: result };
} catch (err: any) {
return { success: false, error: err.response?.data?.error || err.message };
}
});
// Disconnect
ipcMain.handle('relay:disconnect', () => {
cleanup();
return { success: true };
});
// Stats
ipcMain.handle('server:stats', async () => {
if (!apiClient) return { success: false, error: '未连接服务器' };
try {
const data = await apiClient.getStats();
return { success: true, data };
} catch (err: any) {
return { success: false, error: err.message };
}
});
// ===== Settings =====
ipcMain.handle('settings:get', () => {
return {
serverUrl: store.get('serverUrl'),
playerName: store.get('playerName'),
localPort: store.get('localPort'),
recentServers: store.get('recentServers'),
autoReconnect: store.get('autoReconnect'),
minimizeToTray: store.get('minimizeToTray'),
};
});
ipcMain.handle('settings:set', (_event, settings: Record<string, any>) => {
for (const [key, value] of Object.entries(settings)) {
store.set(key, value);
}
return { success: true };
});
ipcMain.handle('settings:addRecentServer', (_event, url: string) => {
const recent = store.get('recentServers') as string[];
const filtered = recent.filter((s: string) => s !== url);
filtered.unshift(url);
store.set('recentServers', filtered.slice(0, 10));
return { success: true };
});
// ===== Room password verification =====
ipcMain.handle('rooms:verify', async (_event, opts: { roomId: string; password?: string }) => {
if (!apiClient) return { success: false, error: '未连接服务器' };
try {
const data = await apiClient.joinRoom(opts.roomId, opts.password);
return { success: true, data };
} catch (err: any) {
return { success: false, error: err.response?.data?.error || err.message };
}
});