feat(materials): add campaign materials overlay for sessions

Let GMs manage and show images over the scene from the editor and control panel, with zoom tools, help docs, and full ru/en i18n.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-17 11:05:38 +08:00
parent 195d4be086
commit 61875be857
39 changed files with 2521 additions and 22 deletions
+66 -2
View File
@@ -8,13 +8,17 @@ import { ipcChannels } from '../../shared/ipc/contracts';
import { getBootSplashWindow } from './bootWindow';
import { loadBrandingWindowIcon } from './brandingIcon';
type WindowKind = 'editor' | 'presentation' | 'control' | 'sceneDescription';
type WindowKind = 'editor' | 'presentation' | 'control' | 'sceneDescription' | 'materials';
const windows = new Map<WindowKind, BrowserWindow>();
let appQuitting = false;
let pendingSceneDescriptionHtml = '';
/** Окно материалов — только колонка списка. */
const MATERIALS_WINDOW_WIDTH = 300;
const MATERIALS_WINDOW_HEIGHT = 720;
/** Учитываем окна, которые уже уничтожены при каскадном закрытии (родитель → дочернее). */
function broadcastMultiWindowStateChanged(open: boolean): void {
for (const w of BrowserWindow.getAllWindows()) {
@@ -71,6 +75,8 @@ function pageNameForKind(kind: WindowKind): string {
return 'control.html';
case 'sceneDescription':
return 'sceneDescription.html';
case 'materials':
return 'materials.html';
}
}
@@ -137,6 +143,7 @@ function windowSizeForKind(kind: WindowKind): { width: number; height: number }
if (kind === 'editor') return { width: 1280, height: 800 };
if (kind === 'control') return { width: 1200, height: 800 };
if (kind === 'sceneDescription') return { width: 720, height: 640 };
if (kind === 'materials') return { width: MATERIALS_WINDOW_WIDTH, height: MATERIALS_WINDOW_HEIGHT };
return { width: 1280, height: 800 };
}
@@ -154,6 +161,16 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
autoHideMenuBar: true,
}
: {}),
...(kind === 'materials'
? {
width: MATERIALS_WINDOW_WIDTH,
height: MATERIALS_WINDOW_HEIGHT,
minWidth: 260,
maxWidth: 360,
minHeight: 480,
autoHideMenuBar: true,
}
: {}),
show: false,
backgroundColor: '#09090B',
...(icon ? { icon } : {}),
@@ -178,7 +195,7 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
}
win.setTitle(windowChromeTitle(kind, app.getLocale()));
if (kind === 'sceneDescription') {
if (kind === 'sceneDescription' || kind === 'materials') {
win.setMenuBarVisibility(false);
}
@@ -209,6 +226,7 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
const open = windows.has('presentation') || windows.has('control');
if (!open) {
closeSceneDescriptionWindow();
closeMaterialsWindow();
}
broadcastMultiWindowStateChanged(open);
});
@@ -293,6 +311,7 @@ export function openMultiWindow() {
export function closeMultiWindow(): void {
closeSceneDescriptionWindow();
closeMaterialsWindow();
const pres = windows.get('presentation');
const ctrl = windows.get('control');
if (pres) pres.close();
@@ -310,6 +329,13 @@ export function closeSceneDescriptionWindow(): void {
}
}
export function closeMaterialsWindow(): void {
const win = windows.get('materials');
if (win && !win.isDestroyed()) {
win.close();
}
}
export function getSceneDescriptionContent(): string {
return pendingSceneDescriptionHtml;
}
@@ -349,6 +375,44 @@ export function openSceneDescriptionWindow(html: string): void {
});
}
/** Полоса материалов: фиксированная ширина плитки, переиспользование окна. */
export function openMaterialsWindow(): void {
const existing = windows.get('materials');
if (existing && !existing.isDestroyed()) {
if (existing.isMinimized()) existing.restore();
const b = existing.getBounds();
existing.setBounds({
x: b.x,
y: b.y,
width: MATERIALS_WINDOW_WIDTH,
height: MATERIALS_WINDOW_HEIGHT,
});
existing.show();
existing.focus();
existing.moveTop();
return;
}
const parent = windows.get('control') ?? windows.get('presentation');
const win = createWindow('materials', parent ? { parent } : undefined);
const { width, height } = win.getBounds();
const display = screen.getDisplayMatching(parent?.getBounds() ?? win.getBounds());
const { x, y, width: dw, height: dh } = display.workArea;
win.setBounds({
x: Math.round(x + Math.max(0, dw - width - 24)),
y: Math.round(y + (dh - height) / 2),
width,
height,
});
win.webContents.once('did-finish-load', () => {
if (!win.isDestroyed()) {
win.show();
win.focus();
win.moveTop();
}
});
}
export function togglePresentationFullscreen(): boolean {
const pres = windows.get('presentation');
if (!pres) return false;