feat(playlists): библиотека плейлистов приложения

CRUD в редакторе, read-only окно на пульте, отдельный аудиоканал, экспорт/импорт и маппинг из Foundry.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-09-03 10:37:21 +08:00
parent cfb6e62331
commit 278eb78637
46 changed files with 2438 additions and 41 deletions
+47
View File
@@ -20,6 +20,7 @@ export type WindowKind =
| 'control'
| 'sceneDescription'
| 'materials'
| 'playlists'
| 'npcsEditor'
| 'sceneEditor'
| 'tokenPathEditor'
@@ -54,6 +55,10 @@ let pendingSceneDescriptionHtml = '';
const MATERIALS_WINDOW_WIDTH = 300;
const MATERIALS_WINDOW_HEIGHT = 720;
/** Окно плейлистов на пульте. */
const PLAYLISTS_WINDOW_WIDTH = 720;
const PLAYLISTS_WINDOW_HEIGHT = 800;
/** Редактор НПС — как основной редактор, шире инспектор. */
const NPCS_EDITOR_WINDOW_WIDTH = 1400;
const NPCS_EDITOR_WINDOW_HEIGHT = 860;
@@ -163,6 +168,7 @@ function bindWindowChromeTitle(win: BrowserWindow, kind: WindowKind): void {
function closePlaySessionAuxiliaryWindows(): void {
closeSceneDescriptionWindow();
closeMaterialsWindow();
closePlaylistsWindow();
closeNpcsWindow();
}
@@ -211,6 +217,8 @@ function pageNameForKind(kind: WindowKind): string {
return 'sceneDescription.html';
case 'materials':
return 'materials.html';
case 'playlists':
return 'playlists.html';
case 'npcsEditor':
return 'npcsEditor.html';
case 'sceneEditor':
@@ -286,6 +294,7 @@ function windowSizeForKind(kind: WindowKind): { width: number; height: number }
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 };
if (kind === 'playlists') return { width: PLAYLISTS_WINDOW_WIDTH, height: PLAYLISTS_WINDOW_HEIGHT };
if (kind === 'npcsEditor') return { width: NPCS_EDITOR_WINDOW_WIDTH, height: NPCS_EDITOR_WINDOW_HEIGHT };
if (kind === 'sceneEditor') return { width: 1280, height: 800 };
if (kind === 'tokenPathEditor') return { width: 1100, height: 760 };
@@ -396,6 +405,7 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
if (
kind === 'sceneDescription' ||
kind === 'materials' ||
kind === 'playlists' ||
kind === 'npcsEditor' ||
kind === 'sceneEditor' ||
kind === 'tokenPathEditor' ||
@@ -586,6 +596,13 @@ export function closeMaterialsWindow(): void {
}
}
export function closePlaylistsWindow(): void {
const win = windows.get('playlists');
if (win && !win.isDestroyed()) {
win.close();
}
}
export function closeNpcsEditorWindow(): void {
const win = windows.get('npcsEditor');
if (win && !win.isDestroyed()) {
@@ -745,6 +762,36 @@ export function openMaterialsWindow(): void {
});
}
export function openPlaylistsWindow(): void {
const existing = windows.get('playlists');
if (existing && !existing.isDestroyed()) {
if (existing.isMinimized()) existing.restore();
existing.show();
existing.focus();
existing.moveTop();
return;
}
const parent = windows.get('control') ?? windows.get('presentation');
const win = createWindow('playlists', 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', () => {
if (!win.isDestroyed()) {
win.show();
win.focus();
win.moveTop();
}
});
}
function positionNpcsEditorWindow(win: BrowserWindow): void {
const parent = windows.get('editor');
const { width, height } = win.getBounds();