DNDGamePlayer: Electron редактор сцен, презентация, упаковка electron-builder
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function readCreateWindows(): string {
|
||||
return fs.readFileSync(path.join(here, 'createWindows.ts'), 'utf8');
|
||||
}
|
||||
|
||||
void test('createWindows: закрытие редактора завершает приложение', () => {
|
||||
const src = readCreateWindows();
|
||||
assert.match(src, /kind === 'editor'/);
|
||||
assert.match(src, /win\.on\(\s*['"]close['"]/);
|
||||
assert.ok(src.includes('appQuitting'));
|
||||
assert.ok(src.includes('e.preventDefault()'));
|
||||
assert.ok(src.includes('quitAppFromEditorClose') || src.includes('app.quit()'));
|
||||
assert.ok(src.includes('markAppQuitting'));
|
||||
});
|
||||
|
||||
void test('createWindows: иконка окна (PNG приоритетно, затем SVG)', () => {
|
||||
const src = readCreateWindows();
|
||||
assert.ok(src.includes('resolveWindowIconPath'));
|
||||
assert.ok(src.includes('app-window-icon.png'));
|
||||
assert.ok(src.includes('app-logo.svg'));
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user