mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 14:22:28 +08:00
* feat(electron): add desktop application support with electron - implement complete Electron main process architecture with window management, app menu, IPC handlers, and settings window - integrate Next.js server for production builds with embedded standalone server - add configuration management with persistent storage and env file support - create preload scripts with secure context bridge for renderer communication - set up electron-builder configuration for multi-platform packaging (macOS, Windows, Linux) - add GitHub Actions workflow for automated release builds - include development scripts for hot-reload during Electron development * feat(electron): enhance security and stability - encrypt API keys using Electron safeStorage API before persisting to disk - add error handling and rollback for preset switching failures - extract inline styles to external CSS file and remove unsafe-inline from CSP - implement dynamic port allocation with automatic fallback for production builds * fix(electron): add maintainer field for Linux .deb package - add maintainer email to linux configuration in electron-builder.yml - required for building .deb packages * fix(electron): use shx for cross-platform file copying - replace Unix-only cp -r with npx shx cp -r - add shx as devDependency for Windows compatibility * fix(electron): fix runtime icon path for all platforms - use icon.png directly instead of platform-specific formats - electron-builder handles icon conversion during packaging - macOS uses embedded icon from app bundle, no explicit path needed - add icon.png to extraResources for Windows/Linux runtime access * fix(electron): add security warning for plaintext API key storage - warn user when safeStorage is unavailable (Linux without keyring) - fail secure: throw error if encryption fails instead of storing plaintext - prevent duplicate warnings with hasWarnedAboutPlaintext flag * fix(electron): add remaining review fixes - Add Windows ARM64 architecture support - Add IPC input validation with config key whitelist - Add server.js existence check before starting Next.js server - Make afterPack throw error on missing directories - Add workflow permissions for release job --------- Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
242 lines
7.1 KiB
TypeScript
242 lines
7.1 KiB
TypeScript
import {
|
|
app,
|
|
BrowserWindow,
|
|
dialog,
|
|
Menu,
|
|
type MenuItemConstructorOptions,
|
|
shell,
|
|
} from "electron"
|
|
import {
|
|
applyPresetToEnv,
|
|
getAllPresets,
|
|
getCurrentPresetId,
|
|
setCurrentPreset,
|
|
} from "./config-manager"
|
|
import { restartNextServer } from "./next-server"
|
|
import { showSettingsWindow } from "./settings-window"
|
|
|
|
/**
|
|
* Build and set the application menu
|
|
*/
|
|
export function buildAppMenu(): void {
|
|
const template = getMenuTemplate()
|
|
const menu = Menu.buildFromTemplate(template)
|
|
Menu.setApplicationMenu(menu)
|
|
}
|
|
|
|
/**
|
|
* Rebuild the menu (call this when presets change)
|
|
*/
|
|
export function rebuildAppMenu(): void {
|
|
buildAppMenu()
|
|
}
|
|
|
|
/**
|
|
* Get the menu template
|
|
*/
|
|
function getMenuTemplate(): MenuItemConstructorOptions[] {
|
|
const isMac = process.platform === "darwin"
|
|
|
|
const template: MenuItemConstructorOptions[] = []
|
|
|
|
// macOS app menu
|
|
if (isMac) {
|
|
template.push({
|
|
label: app.name,
|
|
submenu: [
|
|
{ role: "about" },
|
|
{ type: "separator" },
|
|
{
|
|
label: "Settings...",
|
|
accelerator: "CmdOrCtrl+,",
|
|
click: () => {
|
|
const win = BrowserWindow.getFocusedWindow()
|
|
showSettingsWindow(win || undefined)
|
|
},
|
|
},
|
|
{ type: "separator" },
|
|
{ role: "services" },
|
|
{ type: "separator" },
|
|
{ role: "hide" },
|
|
{ role: "hideOthers" },
|
|
{ role: "unhide" },
|
|
{ type: "separator" },
|
|
{ role: "quit" },
|
|
],
|
|
})
|
|
}
|
|
|
|
// File menu
|
|
template.push({
|
|
label: "File",
|
|
submenu: [
|
|
...(isMac
|
|
? []
|
|
: [
|
|
{
|
|
label: "Settings",
|
|
accelerator: "CmdOrCtrl+,",
|
|
click: () => {
|
|
const win = BrowserWindow.getFocusedWindow()
|
|
showSettingsWindow(win || undefined)
|
|
},
|
|
},
|
|
{ type: "separator" } as MenuItemConstructorOptions,
|
|
]),
|
|
isMac ? { role: "close" } : { role: "quit" },
|
|
],
|
|
})
|
|
|
|
// Edit menu
|
|
template.push({
|
|
label: "Edit",
|
|
submenu: [
|
|
{ role: "undo" },
|
|
{ role: "redo" },
|
|
{ type: "separator" },
|
|
{ role: "cut" },
|
|
{ role: "copy" },
|
|
{ role: "paste" },
|
|
...(isMac
|
|
? [
|
|
{
|
|
role: "pasteAndMatchStyle",
|
|
} as MenuItemConstructorOptions,
|
|
{ role: "delete" } as MenuItemConstructorOptions,
|
|
{ role: "selectAll" } as MenuItemConstructorOptions,
|
|
]
|
|
: [
|
|
{ role: "delete" } as MenuItemConstructorOptions,
|
|
{ type: "separator" } as MenuItemConstructorOptions,
|
|
{ role: "selectAll" } as MenuItemConstructorOptions,
|
|
]),
|
|
],
|
|
})
|
|
|
|
// View menu
|
|
template.push({
|
|
label: "View",
|
|
submenu: [
|
|
{ role: "reload" },
|
|
{ role: "forceReload" },
|
|
{ role: "toggleDevTools" },
|
|
{ type: "separator" },
|
|
{ role: "resetZoom" },
|
|
{ role: "zoomIn" },
|
|
{ role: "zoomOut" },
|
|
{ type: "separator" },
|
|
{ role: "togglefullscreen" },
|
|
],
|
|
})
|
|
|
|
// Configuration menu with presets
|
|
template.push(buildConfigMenu())
|
|
|
|
// Window menu
|
|
template.push({
|
|
label: "Window",
|
|
submenu: [
|
|
{ role: "minimize" },
|
|
{ role: "zoom" },
|
|
...(isMac
|
|
? [
|
|
{ type: "separator" } as MenuItemConstructorOptions,
|
|
{ role: "front" } as MenuItemConstructorOptions,
|
|
]
|
|
: [{ role: "close" } as MenuItemConstructorOptions]),
|
|
],
|
|
})
|
|
|
|
// Help menu
|
|
template.push({
|
|
label: "Help",
|
|
submenu: [
|
|
{
|
|
label: "Documentation",
|
|
click: async () => {
|
|
await shell.openExternal(
|
|
"https://github.com/dayuanjiang/next-ai-draw-io",
|
|
)
|
|
},
|
|
},
|
|
{
|
|
label: "Report Issue",
|
|
click: async () => {
|
|
await shell.openExternal(
|
|
"https://github.com/dayuanjiang/next-ai-draw-io/issues",
|
|
)
|
|
},
|
|
},
|
|
],
|
|
})
|
|
|
|
return template
|
|
}
|
|
|
|
/**
|
|
* Build the Configuration menu with presets
|
|
*/
|
|
function buildConfigMenu(): MenuItemConstructorOptions {
|
|
const presets = getAllPresets()
|
|
const currentPresetId = getCurrentPresetId()
|
|
|
|
const presetItems: MenuItemConstructorOptions[] = presets.map((preset) => ({
|
|
label: preset.name,
|
|
type: "radio",
|
|
checked: preset.id === currentPresetId,
|
|
click: async () => {
|
|
const previousPresetId = getCurrentPresetId()
|
|
const env = applyPresetToEnv(preset.id)
|
|
|
|
if (env) {
|
|
try {
|
|
await restartNextServer()
|
|
rebuildAppMenu() // Rebuild menu to update checkmarks
|
|
} catch (error) {
|
|
console.error("Failed to restart server:", error)
|
|
|
|
// Revert to previous preset on failure
|
|
if (previousPresetId) {
|
|
applyPresetToEnv(previousPresetId)
|
|
} else {
|
|
setCurrentPreset(null)
|
|
}
|
|
|
|
// Rebuild menu to restore previous checkmark state
|
|
rebuildAppMenu()
|
|
|
|
// Show error dialog to notify user
|
|
dialog.showErrorBox(
|
|
"Configuration Error",
|
|
`Failed to apply preset "${preset.name}". The server could not be restarted.\n\nThe previous configuration has been restored.\n\nError: ${error instanceof Error ? error.message : String(error)}`,
|
|
)
|
|
}
|
|
}
|
|
},
|
|
}))
|
|
|
|
return {
|
|
label: "Configuration",
|
|
submenu: [
|
|
...(presetItems.length > 0
|
|
? [
|
|
{ label: "Switch Preset", enabled: false },
|
|
{ type: "separator" } as MenuItemConstructorOptions,
|
|
...presetItems,
|
|
{ type: "separator" } as MenuItemConstructorOptions,
|
|
]
|
|
: []),
|
|
{
|
|
label:
|
|
presetItems.length > 0
|
|
? "Manage Presets..."
|
|
: "Add Configuration Preset...",
|
|
click: () => {
|
|
const win = BrowserWindow.getFocusedWindow()
|
|
showSettingsWindow(win || undefined)
|
|
},
|
|
},
|
|
],
|
|
}
|
|
}
|