feat: окно инструкций, свёрнутая история и обновление спонсоров
Инструкции вынесены в отдельное окно Electron; токены игроков не пропадают при смене сцены; предпросмотр и плейлисты адаптированы; история свёрнута по умолчанию. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -83,6 +83,8 @@ import {
|
||||
openTokenPathEditorWindow,
|
||||
openNpcsWindow,
|
||||
openSceneDescriptionWindow,
|
||||
openInstructionsWindow,
|
||||
getInstructionsSection,
|
||||
closeMaterialsWindow,
|
||||
closePlaylistsWindow,
|
||||
closeTimerWindow,
|
||||
@@ -783,6 +785,13 @@ async function main() {
|
||||
openSceneDescriptionWindow(html);
|
||||
return { ok: true };
|
||||
});
|
||||
registerHandler(ipcChannels.windows.openInstructions, ({ section }) => {
|
||||
openInstructionsWindow(section);
|
||||
return { ok: true };
|
||||
});
|
||||
registerHandler(ipcChannels.windows.getInstructionsSection, () => {
|
||||
return { section: getInstructionsSection() };
|
||||
});
|
||||
registerHandler(ipcChannels.windows.closeSceneDescription, () => {
|
||||
closeSceneDescriptionWindow();
|
||||
return { ok: true };
|
||||
|
||||
@@ -52,6 +52,12 @@ void test('createWindows: окно материалов закрывается
|
||||
assert.match(src, /export function closeMultiWindow[\s\S]*closeMaterialsWindow/);
|
||||
});
|
||||
|
||||
void test('createWindows: окно плейлистов — minWidth 640', () => {
|
||||
const src = readCreateWindows();
|
||||
assert.ok(src.includes('PLAYLISTS_WINDOW_MIN_WIDTH = 640'));
|
||||
assert.match(src, /kind === 'playlists'[\s\S]*?minWidth:\s*PLAYLISTS_WINDOW_MIN_WIDTH/);
|
||||
});
|
||||
|
||||
void test('createWindows: окно таймера закрывается с multi-window', () => {
|
||||
const src = readCreateWindows();
|
||||
assert.ok(src.includes('openTimerWindow'));
|
||||
@@ -76,6 +82,16 @@ void test('createWindows: окно кубиков закрывается с mult
|
||||
assert.match(src, /closePlaySessionAuxiliaryWindows[\s\S]*closeDiceWindow/);
|
||||
});
|
||||
|
||||
void test('createWindows: окно инструкции из редактора, не закрывается с сессией', () => {
|
||||
const src = readCreateWindows();
|
||||
assert.ok(src.includes('openInstructionsWindow'));
|
||||
assert.ok(src.includes("createWindow('instructions'"));
|
||||
assert.ok(src.includes("return 'instructions.html'"));
|
||||
const aux = /function closePlaySessionAuxiliaryWindows\(\): void \{([\s\S]*?)\n\}/.exec(src);
|
||||
assert.ok(aux, 'closePlaySessionAuxiliaryWindows');
|
||||
assert.doesNotMatch(aux[1] ?? '', /instructions/i);
|
||||
});
|
||||
|
||||
void test('createWindows: окно НПС закрывается с multi-window', () => {
|
||||
const src = readCreateWindows();
|
||||
assert.ok(src.includes('openNpcsWindow'));
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user