DNDGamePlayer: Electron редактор сцен, презентация, упаковка electron-builder

Made-with: Cursor
This commit is contained in:
Ivan Fontosh
2026-04-19 14:16:54 +08:00
commit a6cbcc273e
82 changed files with 22195 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { app, BrowserWindow, nativeImage, screen } from 'electron';
type WindowKind = 'editor' | 'presentation' | 'control';
const windows = new Map<WindowKind, BrowserWindow>();
let appQuitting = false;
/** Разрешает реальное закрытие окна редактора (выход из приложения). */
export function markAppQuitting(): void {
appQuitting = true;
}
function quitAppFromEditorClose(): void {
markAppQuitting();
app.quit();
}
function isDev() {
return process.env.NODE_ENV === 'development' || process.env.VITE_DEV_SERVER_URL !== undefined;
}
function getRendererUrl(kind: WindowKind): string {
const dev = process.env.VITE_DEV_SERVER_URL;
if (dev) {
const page =
kind === 'editor' ? 'editor.html' : kind === 'presentation' ? 'presentation.html' : 'control.html';
return new URL(page, dev).toString();
}
const filePath = path.join(app.getAppPath(), 'dist', 'renderer', `${kind}.html`);
return pathToFileURL(filePath).toString();
}
function getPreloadPath(): string {
return path.join(app.getAppPath(), 'dist', 'preload', 'index.cjs');
}
/**
* Иконка окна. На Windows `nativeImage` из SVG часто пустой — сначала ищем PNG
* (`app-window-icon.png`), затем SVG из public / dist.
*/
function resolveWindowIconPath(): string | undefined {
const root = app.getAppPath();
const candidates = [
path.join(root, 'dist', 'renderer', 'app-window-icon.png'),
path.join(root, 'app', 'renderer', 'public', 'app-window-icon.png'),
path.join(root, 'dist', 'renderer', 'app-logo.svg'),
path.join(root, 'app', 'renderer', 'public', 'app-logo.svg'),
];
for (const p of candidates) {
try {
if (fs.existsSync(p)) return p;
} catch {
/* ignore */
}
}
return undefined;
}
function resolveWindowIcon(): Electron.NativeImage | undefined {
const p = resolveWindowIconPath();
if (!p) return undefined;
try {
const img = nativeImage.createFromPath(p);
if (!img.isEmpty()) return img;
} catch {
/* ignore */
}
return undefined;
}
function createWindow(kind: WindowKind): BrowserWindow {
const icon = resolveWindowIcon();
const win = new BrowserWindow({
width: kind === 'editor' ? 1280 : kind === 'control' ? 1200 : 1280,
height: 800,
show: false,
backgroundColor: '#09090B',
...(icon ? { icon } : {}),
webPreferences: {
contextIsolation: true,
sandbox: true,
nodeIntegration: false,
devTools: isDev(),
preload: getPreloadPath(),
autoplayPolicy: 'no-user-gesture-required',
},
});
win.webContents.on('preload-error', (_event, preloadPath, error) => {
console.error(`[preload-error] ${preloadPath}:`, error);
});
win.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
console.error(`[did-fail-load] ${String(errorCode)} ${errorDescription} ${validatedURL}`);
});
win.once('ready-to-show', () => win.show());
void win.loadURL(getRendererUrl(kind));
if (kind === 'editor') {
win.on('close', (e) => {
if (appQuitting) return;
e.preventDefault();
quitAppFromEditorClose();
});
}
win.on('closed', () => windows.delete(kind));
windows.set(kind, win);
return win;
}
export function createWindows() {
if (!windows.has('editor')) {
createWindow('editor');
}
}
export function focusEditorWindow(): void {
const win = windows.get('editor');
if (win) {
if (win.isMinimized()) win.restore();
win.show();
win.focus();
} else {
createWindows();
}
}
export function openMultiWindow() {
if (!windows.has('presentation')) {
const display = screen.getPrimaryDisplay();
const { x, y, width, height } = display.bounds;
const win = createWindow('presentation');
win.setBounds({ x, y, width, height });
win.setMenuBarVisibility(false);
win.maximize();
}
if (!windows.has('control')) {
createWindow('control');
}
}
export function closeMultiWindow(): void {
const pres = windows.get('presentation');
const ctrl = windows.get('control');
if (pres) pres.close();
if (ctrl) ctrl.close();
}
export function togglePresentationFullscreen(): boolean {
const pres = windows.get('presentation');
if (!pres) return false;
const next = !pres.isFullScreen();
pres.setFullScreen(next);
return pres.isFullScreen();
}