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(); 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'); } /** * PNG для иконки окна / дока: тот же растр, что electron-builder берёт из `build/icon.png` * (копия в dist после сборки), затем окно 256px, затем dev-пути. SVG не используем для * nativeImage на Windows — иначе пустая картинка и дефолтная иконка Electron вместо exe. */ function resolveBrandingPngPaths(): string[] { const root = app.getAppPath(); return [ path.join(root, 'dist', 'renderer', 'app-pack-icon.png'), path.join(root, 'dist', 'renderer', 'app-window-icon.png'), path.join(root, 'build', 'icon.png'), path.join(root, 'app', 'renderer', 'public', 'app-window-icon.png'), ]; } function resolveWindowIconPath(): string | undefined { for (const p of resolveBrandingPngPaths()) { try { if (fs.existsSync(p)) return p; } catch { /* ignore */ } } const root = app.getAppPath(); const svgFallback = [ path.join(root, 'dist', 'renderer', 'app-logo.svg'), path.join(root, 'app', 'renderer', 'public', 'app-logo.svg'), ]; for (const p of svgFallback) { try { if (fs.existsSync(p)) return p; } catch { /* ignore */ } } return undefined; } function resolveWindowIcon(): Electron.NativeImage | undefined { const tryPath = (filePath: string): Electron.NativeImage | undefined => { try { const img = nativeImage.createFromPath(filePath); if (!img.isEmpty()) return img; } catch { /* ignore */ } return undefined; }; if (process.platform === 'win32' || process.platform === 'linux') { for (const p of resolveBrandingPngPaths()) { if (!fs.existsSync(p)) continue; const img = tryPath(p); if (img) return img; } return undefined; } const p = resolveWindowIconPath(); if (!p) return undefined; return tryPath(p); } /** macOS: в Dock показываем тот же PNG, что и у упакованного приложения на Windows (иконка exe). */ export function applyDockIconIfNeeded(): void { if (process.platform !== 'darwin' || !app.dock) return; for (const p of resolveBrandingPngPaths()) { if (!fs.existsSync(p)) continue; try { const img = nativeImage.createFromPath(p); if (img.isEmpty()) continue; app.dock.setIcon(img); return; } catch { /* try next */ } } } 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(); }