a6cbcc273e
Made-with: Cursor
160 lines
4.4 KiB
TypeScript
160 lines
4.4 KiB
TypeScript
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();
|
|
}
|