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
+82
View File
@@ -20,6 +20,12 @@ import type {
AppToken,
AppPlayer,
AppPlayerTeam,
AppPlaylist,
PlaylistAudio,
PlaylistAudioId,
PlaylistId,
PlaylistTrackId,
PlaylistsState,
PlayerId,
PlayerTeamId,
PlayerImageOffset,
@@ -149,6 +155,8 @@ export const ipcChannels = {
sceneDescriptionContent: 'windows.sceneDescriptionContent',
openMaterials: 'windows.openMaterials',
closeMaterials: 'windows.closeMaterials',
openPlaylists: 'windows.openPlaylists',
closePlaylists: 'windows.closePlaylists',
openNpcsEditor: 'windows.openNpcsEditor',
closeNpcsEditor: 'windows.closeNpcsEditor',
openNpcs: 'windows.openNpcs',
@@ -243,6 +251,21 @@ export const ipcChannels = {
upsertProgress: 'players.upsertProgress',
stateChanged: 'players.stateChanged',
},
playlists: {
getState: 'playlists.getState',
create: 'playlists.create',
rename: 'playlists.rename',
duplicate: 'playlists.duplicate',
delete: 'playlists.delete',
setOrder: 'playlists.setOrder',
setTracksOrder: 'playlists.setTracksOrder',
addAudioFiles: 'playlists.addAudioFiles',
pickAudioFiles: 'playlists.pickAudioFiles',
removeTrack: 'playlists.removeTrack',
renameAudio: 'playlists.renameAudio',
audioUrl: 'playlists.audioUrl',
stateChanged: 'playlists.stateChanged',
},
sceneNpcTokensSession: {
getState: 'sceneNpcTokensSession.getState',
dispatch: 'sceneNpcTokensSession.dispatch',
@@ -342,6 +365,7 @@ export type IpcEventMap = {
[ipcChannels.tokenGridSnap.stateChanged]: { enabled: boolean };
[ipcChannels.players.stateChanged]: { players: AppPlayer[]; teams: AppPlayerTeam[] };
[ipcChannels.players.upsertProgress]: PlayersUpsertProgressEvent;
[ipcChannels.playlists.stateChanged]: PlaylistsState;
[ipcChannels.sceneNpcTokensSession.stateChanged]: { state: SceneNpcTokensSessionState };
[ipcChannels.npcMapSpawn.stateChanged]: { state: NpcMapSpawnDragState };
[ipcChannels.scenePlayerTokensSession.stateChanged]: { state: ScenePlayerTokensSessionState };
@@ -667,6 +691,7 @@ export type IpcInvokeMap = {
storylineSelections: StorylineSelection[];
npcIds: string[];
labels: StorylineLabels;
playlistIds?: string[];
};
res: { canceled: true } | { canceled: false };
};
@@ -726,6 +751,14 @@ export type IpcInvokeMap = {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.windows.openPlaylists]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.windows.closePlaylists]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.windows.openNpcsEditor]: {
req: Record<string, never>;
res: { ok: true };
@@ -914,6 +947,54 @@ export type IpcInvokeMap = {
req: { id: PlayerId };
res: { url: string | null };
};
[ipcChannels.playlists.getState]: {
req: Record<string, never>;
res: PlaylistsState;
};
[ipcChannels.playlists.create]: {
req: { name?: string };
res: { playlist: AppPlaylist; state: PlaylistsState };
};
[ipcChannels.playlists.rename]: {
req: { id: PlaylistId; name: string };
res: { playlist: AppPlaylist; state: PlaylistsState };
};
[ipcChannels.playlists.duplicate]: {
req: { id: PlaylistId };
res: { playlist: AppPlaylist; state: PlaylistsState };
};
[ipcChannels.playlists.delete]: {
req: { id: PlaylistId };
res: { state: PlaylistsState };
};
[ipcChannels.playlists.setOrder]: {
req: { playlistIds: PlaylistId[] };
res: { state: PlaylistsState };
};
[ipcChannels.playlists.setTracksOrder]: {
req: { playlistId: PlaylistId; trackIds: PlaylistTrackId[] };
res: { playlist: AppPlaylist; state: PlaylistsState };
};
[ipcChannels.playlists.addAudioFiles]: {
req: { playlistId: PlaylistId; filePaths: string[] };
res: { playlist: AppPlaylist; state: PlaylistsState };
};
[ipcChannels.playlists.pickAudioFiles]: {
req: Record<string, never>;
res: { filePaths: string[] };
};
[ipcChannels.playlists.removeTrack]: {
req: { playlistId: PlaylistId; trackId: PlaylistTrackId };
res: { playlist: AppPlaylist; state: PlaylistsState };
};
[ipcChannels.playlists.renameAudio]: {
req: { audioId: PlaylistAudioId; title: string };
res: { audio: PlaylistAudio; state: PlaylistsState };
};
[ipcChannels.playlists.audioUrl]: {
req: { id: PlaylistAudioId };
res: { url: string | null };
};
[ipcChannels.sceneNpcTokensSession.getState]: {
req: Record<string, never>;
res: { state: SceneNpcTokensSessionState };
@@ -996,6 +1077,7 @@ export type LegacyIpcEventMap = {
[ipcChannels.tokenGridSnap.stateChanged]: { enabled: boolean };
[ipcChannels.players.stateChanged]: { players: AppPlayer[]; teams: AppPlayerTeam[] };
[ipcChannels.players.upsertProgress]: PlayersUpsertProgressEvent;
[ipcChannels.playlists.stateChanged]: PlaylistsState;
[ipcChannels.sceneNpcTokensSession.stateChanged]: { state: SceneNpcTokensSessionState };
[ipcChannels.npcMapSpawn.stateChanged]: { state: NpcMapSpawnDragState };
[ipcChannels.scenePlayerTokensSession.stateChanged]: { state: ScenePlayerTokensSessionState };