f851d8b8f2
Electron desktop application for Neverwinter Nights module development. Clone, edit, build, and run a complete Layonara NWNX server with only Docker required. - React 19 + Vite frontend with Monaco editor and NWScript LSP - Node.js + Express backend managing Docker sibling containers - Electron shell with Docker availability check and auto-setup - Builder image auto-builds on first use from bundled Dockerfile - Cross-platform: Windows (.exe), macOS (.dmg), Linux (.AppImage) - Gitea Actions CI for automated release builds
132 lines
3.3 KiB
TypeScript
132 lines
3.3 KiB
TypeScript
import { app, BrowserWindow, shell, dialog } from "electron";
|
|
import path from "path";
|
|
import { checkDocker, dockerDownloadUrl } from "./docker-check";
|
|
import { defaultWorkspacePath, detectNwnHome } from "./paths";
|
|
|
|
let mainWindow: BrowserWindow | null = null;
|
|
let serverPort: number;
|
|
|
|
function setEnvironment(): void {
|
|
if (!process.env.WORKSPACE_PATH) {
|
|
process.env.WORKSPACE_PATH = defaultWorkspacePath();
|
|
}
|
|
if (!process.env.NWN_HOME_PATH) {
|
|
const detected = detectNwnHome();
|
|
if (detected) process.env.NWN_HOME_PATH = detected;
|
|
}
|
|
if (!process.env.GIT_PROVIDER_URL) {
|
|
process.env.GIT_PROVIDER_URL = "https://gitea.layonara.com";
|
|
}
|
|
process.env.ELECTRON = "1";
|
|
}
|
|
|
|
async function startBackend(): Promise<number> {
|
|
const { startServer } = await import(
|
|
path.join(__dirname, "../packages/backend/dist/index.js")
|
|
);
|
|
const port = await findFreePort();
|
|
await startServer(port);
|
|
return port;
|
|
}
|
|
|
|
function findFreePort(): Promise<number> {
|
|
return new Promise((resolve, reject) => {
|
|
const net = require("net");
|
|
const srv = net.createServer();
|
|
srv.listen(0, () => {
|
|
const port = srv.address().port;
|
|
srv.close(() => resolve(port));
|
|
});
|
|
srv.on("error", reject);
|
|
});
|
|
}
|
|
|
|
function createWindow(port: number): BrowserWindow {
|
|
const win = new BrowserWindow({
|
|
width: 1280,
|
|
height: 900,
|
|
minWidth: 960,
|
|
minHeight: 600,
|
|
title: "Layonara Forge",
|
|
icon: path.join(__dirname, "../assets/icon.png"),
|
|
webPreferences: {
|
|
preload: path.join(__dirname, "preload.js"),
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
},
|
|
});
|
|
|
|
win.loadURL(`http://localhost:${port}`);
|
|
|
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
if (url.startsWith("http")) shell.openExternal(url);
|
|
return { action: "deny" };
|
|
});
|
|
|
|
win.on("closed", () => {
|
|
mainWindow = null;
|
|
});
|
|
|
|
return win;
|
|
}
|
|
|
|
async function showDockerMissing(): Promise<boolean> {
|
|
const url = dockerDownloadUrl();
|
|
const { response } = await dialog.showMessageBox({
|
|
type: "warning",
|
|
title: "Docker Required",
|
|
message: "Layonara Forge requires Docker to build modules and run the NWN server.",
|
|
detail: "Docker Desktop must be installed and running before using the Forge.\n\nClick \"Download Docker\" to open the download page, or \"Check Again\" after installing.",
|
|
buttons: ["Download Docker", "Check Again", "Quit"],
|
|
defaultId: 0,
|
|
cancelId: 2,
|
|
});
|
|
|
|
if (response === 0) {
|
|
shell.openExternal(url);
|
|
return showDockerMissing();
|
|
}
|
|
if (response === 1) {
|
|
const status = await checkDocker();
|
|
if (status.available) return true;
|
|
return showDockerMissing();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
app.whenReady().then(async () => {
|
|
setEnvironment();
|
|
|
|
const docker = await checkDocker();
|
|
if (!docker.available) {
|
|
const installed = await showDockerMissing();
|
|
if (!installed) {
|
|
app.quit();
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
serverPort = await startBackend();
|
|
} catch (err: any) {
|
|
dialog.showErrorBox(
|
|
"Forge Startup Error",
|
|
`Failed to start the backend server:\n\n${err.message}`,
|
|
);
|
|
app.quit();
|
|
return;
|
|
}
|
|
|
|
mainWindow = createWindow(serverPort);
|
|
});
|
|
|
|
app.on("window-all-closed", () => {
|
|
app.quit();
|
|
});
|
|
|
|
app.on("activate", () => {
|
|
if (mainWindow === null && serverPort) {
|
|
mainWindow = createWindow(serverPort);
|
|
}
|
|
});
|