const { app, BrowserWindow, Menu, Tray, ipcMain, Notification, nativeImage, shell } = require('electron') const fs = require('fs') const path = require('path') const isDev = !!process.env.FABRIC_DESKTOP_URL const DEFAULT_DEV_URL = 'http://localhost:5173' const DEFAULT_PROD_ENTRY = path.join(__dirname, 'offline.html') let mainWindow = null let tray = null let isQuitting = false function configPath() { return path.join(app.getPath('userData'), 'fabric-desktop.config.json') } function readConfig() { try { const raw = fs.readFileSync(configPath(), 'utf8') return JSON.parse(raw) } catch { return { centerApiBase: 'http://localhost:7001/api', guildApiBase: 'http://localhost:7002/api', guildSocketBase: 'http://localhost:7002/realtime', apiKey: '', } } } function writeConfig(next) { fs.mkdirSync(path.dirname(configPath()), { recursive: true }) fs.writeFileSync(configPath(), JSON.stringify(next, null, 2), 'utf8') return next } function createMenu() { const template = [ { label: 'Fabric', submenu: [ { role: 'reload', accelerator: 'CmdOrCtrl+R' }, { role: 'toggleDevTools', accelerator: 'CmdOrCtrl+Shift+I' }, { type: 'separator' }, { role: 'quit', accelerator: 'CmdOrCtrl+Q' }, ], }, { label: 'Edit', submenu: [{ role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'copy' }, { role: 'paste' }], }, ] Menu.setApplicationMenu(Menu.buildFromTemplate(template)) } function createWindow() { const win = new BrowserWindow({ width: 1280, height: 840, minWidth: 1024, minHeight: 700, title: 'Fabric Desktop', icon: path.join(__dirname, 'assets/icon.png'), autoHideMenuBar: false, webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true, preload: path.join(__dirname, 'preload.js'), }, }) win.webContents.setWindowOpenHandler(() => ({ action: 'deny' })) win.webContents.on('will-navigate', (event, url) => { const allowedDev = isDev && url.startsWith((process.env.FABRIC_DESKTOP_URL || DEFAULT_DEV_URL)) const allowedFile = url.startsWith('file://') if (!allowedDev && !allowedFile) { event.preventDefault() shell.openExternal(url) } }) const devUrl = process.env.FABRIC_DESKTOP_URL || DEFAULT_DEV_URL if (isDev) { win.loadURL(devUrl) } else { win.loadFile(DEFAULT_PROD_ENTRY) } win.on('close', (event) => { if (!isQuitting) { event.preventDefault() win.hide() } }) return win } function createTrayIcon() { if (tray) return tray const icon = nativeImage.createFromPath(path.join(__dirname, 'assets/tray.png')) tray = new Tray(icon) tray.setToolTip('Fabric Desktop') const buildTrayMenu = () => Menu.buildFromTemplate([ { label: mainWindow?.isVisible() ? '隐藏窗口' : '显示窗口', click: () => { if (!mainWindow) { mainWindow = createWindow() return } if (mainWindow.isVisible()) mainWindow.hide() else { mainWindow.show() mainWindow.focus() } tray.setContextMenu(buildTrayMenu()) }, }, { label: '退出', click: () => { isQuitting = true app.quit() }, }, ]) tray.setContextMenu(buildTrayMenu()) tray.on('double-click', () => { if (!mainWindow) { mainWindow = createWindow() return } mainWindow.show() mainWindow.focus() tray.setContextMenu(buildTrayMenu()) }) return tray } app.whenReady().then(() => { createMenu() mainWindow = createWindow() createTrayIcon() ipcMain.handle('fabric:config:get', () => readConfig()) ipcMain.handle('fabric:config:set', (_evt, next) => writeConfig(next || {})) ipcMain.handle('fabric:notify', (_evt, payload) => { const title = payload?.title || 'Fabric' const body = payload?.body || '' if (Notification.isSupported()) { new Notification({ title, body }).show() return { ok: true } } return { ok: false } }) app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) mainWindow = createWindow() else if (mainWindow && !mainWindow.isVisible()) mainWindow.show() }) app.on('before-quit', () => { isQuitting = true }) }) app.on('window-all-closed', () => { // 保持后台驻留,由托盘控制退出 })