2026-02-22 23:38:41 +08:00
|
|
|
import { app, BrowserWindow, ipcMain, dialog, Tray, Menu, nativeImage } from 'electron';
|
2026-02-22 23:33:00 +08:00
|
|
|
import * as path from 'path';
|
2026-02-22 23:38:41 +08:00
|
|
|
import Store from 'electron-store';
|
2026-02-22 23:33:00 +08:00
|
|
|
import { RelayClient } from './relay-client';
|
|
|
|
|
import { LocalProxy } from './local-proxy';
|
|
|
|
|
import { ApiClient } from './api-client';
|
|
|
|
|
|
2026-02-22 23:38:41 +08:00
|
|
|
const store = new Store({
|
|
|
|
|
defaults: {
|
|
|
|
|
serverUrl: 'http://localhost:3000',
|
|
|
|
|
playerName: 'Player',
|
|
|
|
|
localPort: 25566,
|
|
|
|
|
recentServers: [] as string[],
|
|
|
|
|
autoReconnect: true,
|
|
|
|
|
minimizeToTray: true,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-22 23:33:00 +08:00
|
|
|
let mainWindow: BrowserWindow | null = null;
|
2026-02-22 23:38:41 +08:00
|
|
|
let tray: Tray | null = null;
|
2026-02-22 23:33:00 +08:00
|
|
|
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'));
|
|
|
|
|
|
2026-02-22 23:38:41 +08:00
|
|
|
mainWindow.on('close', (e) => {
|
|
|
|
|
if (store.get('minimizeToTray') && tray) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
mainWindow?.hide();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-22 23:33:00 +08:00
|
|
|
mainWindow.on('closed', () => {
|
|
|
|
|
mainWindow = null;
|
|
|
|
|
cleanup();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 23:38:41 +08:00
|
|
|
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(); });
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 23:33:00 +08:00
|
|
|
function cleanup() {
|
|
|
|
|
if (localProxy) {
|
|
|
|
|
localProxy.stop();
|
|
|
|
|
localProxy = null;
|
|
|
|
|
}
|
|
|
|
|
if (relayClient) {
|
|
|
|
|
relayClient.disconnect();
|
|
|
|
|
relayClient = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app.whenReady().then(() => {
|
|
|
|
|
createWindow();
|
2026-02-22 23:38:41 +08:00
|
|
|
createTray();
|
2026-02-22 23:33:00 +08:00
|
|
|
|
|
|
|
|
app.on('activate', () => {
|
|
|
|
|
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
2026-02-22 23:38:41 +08:00
|
|
|
else mainWindow?.show();
|
2026-02-22 23:33:00 +08:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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 };
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-02-22 23:38:41 +08:00
|
|
|
|
|
|
|
|
// ===== 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 };
|
|
|
|
|
}
|
|
|
|
|
});
|