feat: окно инструкций, свёрнутая история и обновление спонсоров

Инструкции вынесены в отдельное окно Electron; токены игроков не пропадают при смене сцены; предпросмотр и плейлисты адаптированы; история свёрнута по умолчанию.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-09-17 06:21:42 +08:00
parent 712086ddca
commit a29c72b307
24 changed files with 506 additions and 290 deletions
+77 -2
View File
@@ -27,7 +27,8 @@ export type WindowKind =
| 'npcsEditor'
| 'sceneEditor'
| 'tokenPathEditor'
| 'npcs';
| 'npcs'
| 'instructions';
/** Окна, которые реально слушают session.stateChanged (редактор синхронизируется через invoke). */
export const SESSION_STATE_WINDOW_KINDS: readonly WindowKind[] = [
@@ -53,6 +54,7 @@ let appQuitting = false;
/** Защита от каскада close(control) ↔ close(presentation). */
let closingPlaySession = false;
let pendingSceneDescriptionHtml = '';
let pendingInstructionsSection = 'overview';
/** Окно материалов — только колонка списка. */
const MATERIALS_WINDOW_WIDTH = 300;
@@ -60,6 +62,7 @@ const MATERIALS_WINDOW_HEIGHT = 720;
/** Окно плейлистов на пульте. */
const PLAYLISTS_WINDOW_WIDTH = 720;
const PLAYLISTS_WINDOW_MIN_WIDTH = 640;
const PLAYLISTS_WINDOW_HEIGHT = 800;
/** Компактное окно таймера сессии. */
@@ -81,6 +84,10 @@ const INITIATIVE_WINDOW_HEIGHT = 420;
const DICE_WINDOW_WIDTH = 380;
const DICE_WINDOW_HEIGHT = 460;
/** Окно инструкции из редактора (не модалка). */
const INSTRUCTIONS_WINDOW_WIDTH = 960;
const INSTRUCTIONS_WINDOW_HEIGHT = 720;
/** Колбэк при закрытии окна кубиков (Esc / крестик / programmatic). */
let diceWindowClosedListener: (() => void) | null = null;
@@ -147,6 +154,15 @@ function sendSceneDescriptionContent(win: BrowserWindow, html: string): void {
}
}
function sendInstructionsSection(win: BrowserWindow, section: string): void {
if (win.isDestroyed() || win.webContents.isDestroyed()) return;
try {
win.webContents.send(ipcChannels.windows.instructionsSection, { section });
} catch {
/* ignore */
}
}
/** Разрешает реальное закрытие окна редактора (выход из приложения). */
export function markAppQuitting(): void {
appQuitting = true;
@@ -265,6 +281,8 @@ function pageNameForKind(kind: WindowKind): string {
return 'tokenPathEditor.html';
case 'npcs':
return 'npcs.html';
case 'instructions':
return 'instructions.html';
}
}
@@ -340,6 +358,7 @@ function windowSizeForKind(kind: WindowKind): { width: number; height: number }
if (kind === 'sceneEditor') return { width: 1280, height: 800 };
if (kind === 'tokenPathEditor') return { width: 1100, height: 760 };
if (kind === 'npcs') return { width: NPCS_WINDOW_WIDTH, height: NPCS_WINDOW_HEIGHT };
if (kind === 'instructions') return { width: INSTRUCTIONS_WINDOW_WIDTH, height: INSTRUCTIONS_WINDOW_HEIGHT };
return { width: 1280, height: 800 };
}
@@ -366,6 +385,14 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
minHeight: 480,
}
: {}),
...(kind === 'playlists'
? {
width: PLAYLISTS_WINDOW_WIDTH,
height: PLAYLISTS_WINDOW_HEIGHT,
minWidth: PLAYLISTS_WINDOW_MIN_WIDTH,
minHeight: 480,
}
: {}),
...(kind === 'npcsEditor'
? {
width: NPCS_EDITOR_WINDOW_WIDTH,
@@ -399,6 +426,14 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
minHeight: 560,
}
: {}),
...(kind === 'instructions'
? {
width: INSTRUCTIONS_WINDOW_WIDTH,
height: INSTRUCTIONS_WINDOW_HEIGHT,
minWidth: 640,
minHeight: 480,
}
: {}),
show: false,
backgroundColor: '#09090B',
...(icon ? { icon } : {}),
@@ -458,7 +493,8 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
kind === 'npcsEditor' ||
kind === 'sceneEditor' ||
kind === 'tokenPathEditor' ||
kind === 'npcs'
kind === 'npcs' ||
kind === 'instructions'
) {
win.setMenuBarVisibility(false);
}
@@ -780,6 +816,44 @@ export function getSceneDescriptionContent(): string {
return pendingSceneDescriptionHtml;
}
export function getInstructionsSection(): string {
return pendingInstructionsSection;
}
/** Окно инструкции из редактора: переиспользовать существующее или создать новое. */
export function openInstructionsWindow(section?: string): void {
pendingInstructionsSection = String(section ?? '').trim() || 'overview';
const existing = windows.get('instructions');
if (existing && !existing.isDestroyed()) {
if (existing.isMinimized()) existing.restore();
existing.show();
existing.focus();
existing.moveTop();
sendInstructionsSection(existing, pendingInstructionsSection);
return;
}
const parent = windows.get('editor');
const win = createWindow('instructions', 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) / 2)),
y: Math.round(y + Math.max(0, (dh - height) / 2)),
width,
height,
});
win.webContents.once('did-finish-load', () => {
sendInstructionsSection(win, pendingInstructionsSection);
if (!win.isDestroyed()) {
win.show();
win.focus();
win.moveTop();
}
});
}
/** Одно окно описания: переиспользовать существующее или создать новое. */
export function openSceneDescriptionWindow(html: string): void {
pendingSceneDescriptionHtml = html;
@@ -856,6 +930,7 @@ export function openMaterialsWindow(): void {
export function openPlaylistsWindow(): void {
const existing = windows.get('playlists');
if (existing && !existing.isDestroyed()) {
existing.setMinimumSize(PLAYLISTS_WINDOW_MIN_WIDTH, 480);
if (existing.isMinimized()) existing.restore();
existing.show();
existing.focus();