diff --git a/README.md b/README.md index 7ed7117..205e52c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,16 @@ Electron desktop shell for Fabric.Frontend. +## 功能(当前) +- BrowserWindow 基础配置(尺寸/最小尺寸/标题) +- Dev/Prod 加载策略(dev server / 本地 offline.html) +- 基础菜单与快捷键(刷新、开发者工具、退出) +- 安全基线:`contextIsolation` + `sandbox` + 禁止任意新窗口/导航 +- `preload + IPC` 白名单: + - `fabric:config:get` + - `fabric:config:set` + - `fabric:notify` + ## Dev ```bash diff --git a/main.js b/main.js index f867d81..b5e110b 100644 --- a/main.js +++ b/main.js @@ -1,20 +1,104 @@ -const { app, BrowserWindow } = require('electron') +const { app, BrowserWindow, Menu, ipcMain, Notification, 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') + +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', + autoHideMenuBar: false, webPreferences: { contextIsolation: true, + nodeIntegration: false, + sandbox: true, + preload: path.join(__dirname, 'preload.js'), }, }) - const url = process.env.FABRIC_DESKTOP_URL || 'file://' + __dirname + '/offline.html' - win.loadURL(url) + 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) + } } app.whenReady().then(() => { + createMenu() createWindow() + + 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) createWindow() }) diff --git a/preload.js b/preload.js new file mode 100644 index 0000000..2206661 --- /dev/null +++ b/preload.js @@ -0,0 +1,7 @@ +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('fabricDesktop', { + getConfig: () => ipcRenderer.invoke('fabric:config:get'), + setConfig: (next) => ipcRenderer.invoke('fabric:config:set', next), + notify: (title, body) => ipcRenderer.invoke('fabric:notify', { title, body }), +})