- build:renderer builds Fabric.Frontend with relative base and copies the static bundle into renderer/; dist/pack scripts run it first. - main.js: production loads renderer/index.html when present (falls back to offline.html); packaged app no longer needs a separate frontend server (still talks to Center/Guild backends, which the login screen points at — file:// origin is CORS-allowed). - package files += renderer/**; gitignore renderer/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
183 lines
4.6 KiB
JavaScript
183 lines
4.6 KiB
JavaScript
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'
|
|
// Self-contained: load the bundled frontend if present, else the placeholder.
|
|
const BUNDLED_ENTRY = path.join(__dirname, 'renderer', 'index.html')
|
|
const DEFAULT_PROD_ENTRY = fs.existsSync(BUNDLED_ENTRY)
|
|
? BUNDLED_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', () => {
|
|
// 保持后台驻留,由托盘控制退出
|
|
})
|