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
+11 -3
View File
@@ -411,6 +411,10 @@ export class ZipProjectStore {
return this.openProject?.project ?? null;
}
getOpenCacheDir(): string | null {
return this.openProject?.cacheDir ?? null;
}
/** Только указатель сессии в памяти — без записи project.json на диск. */
setSessionPointer(currentGraphNodeId: GraphNodeId | null, currentSceneId: SceneId | null): Project {
const open = this.openProject;
@@ -2039,7 +2043,7 @@ export class ZipProjectStore {
async importProjectFromFoundry(
sourcePath: string,
onProgress?: (p: FoundryImportProgress) => void,
): Promise<Project> {
): Promise<{ project: Project; pendingAppPlaylists: import('../foundry/foundryImport').FoundryPendingAppPlaylist[] }> {
await this.ensureRoots();
const loaded = await loadFoundryDocumentsForImport(sourcePath, onProgress);
try {
@@ -2049,7 +2053,7 @@ export class ZipProjectStore {
await fs.rm(cacheDir, { recursive: true, force: true });
await fs.mkdir(path.join(cacheDir, 'assets'), { recursive: true });
const { project } = await buildProjectFromFoundryDocuments(
const { project, pendingAppPlaylists } = await buildProjectFromFoundryDocuments(
loaded.manifest,
loaded.docs,
cacheDir,
@@ -2070,7 +2074,7 @@ export class ZipProjectStore {
onProgress?.({ stage: 'zip', percent: 90, detail: 'Сборка проекта…' });
await this.enqueuePack(cacheDir, zipPath);
onProgress?.({ stage: 'done', percent: 100, detail: 'Готово' });
return this.openProject.project;
return { project: this.openProject.project, pendingAppPlaylists };
} finally {
await loaded.cleanup();
}
@@ -2184,6 +2188,7 @@ export class ZipProjectStore {
labels: StorylineLabels,
onProgress?: (p: { stage: 'zip' | 'done'; percent: number; detail?: string }) => void,
packAppTokens?: (tokenIds: string[], exportRoot: string) => Promise<void>,
packAppPlaylists?: (exportRoot: string) => Promise<void>,
): Promise<void> {
if (selections.length === 0) throw new Error('Не выбрана ни одна сюжетная линия');
await this.ensureRoots();
@@ -2217,6 +2222,9 @@ export class ZipProjectStore {
const tokenIds = collectTokenIdsFromProject(partial);
await packAppTokens(tokenIds, exportCache);
}
if (packAppPlaylists) {
await packAppPlaylists(exportCache);
}
const now = new Date().toISOString();
const toWrite: Project = {
...partial,