feat(playlists): библиотека плейлистов приложения
CRUD в редакторе, read-only окно на пульте, отдельный аудиоканал, экспорт/импорт и маппинг из Foundry. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
/** App-local библиотека плейлистов (userData, не в project zip по умолчанию). */
|
||||
|
||||
import {
|
||||
asPlaylistAudioId,
|
||||
asPlaylistId,
|
||||
asPlaylistTrackId,
|
||||
type PlaylistAudioId,
|
||||
type PlaylistId,
|
||||
type PlaylistTrackId,
|
||||
} from './ids';
|
||||
|
||||
export type { PlaylistAudioId, PlaylistId, PlaylistTrackId };
|
||||
export { asPlaylistAudioId, asPlaylistId, asPlaylistTrackId };
|
||||
|
||||
/** Физический аудиофайл в библиотеке (один файл — много вхождений в плейлистах). */
|
||||
export type PlaylistAudio = {
|
||||
id: PlaylistAudioId;
|
||||
/** Отображаемое имя (общее для всех вхождений). */
|
||||
title: string;
|
||||
originalName: string;
|
||||
/** Относительный путь внутри `userData/playlists/`. */
|
||||
relPath: string;
|
||||
sha256: string;
|
||||
mime: string;
|
||||
sizeBytes: number;
|
||||
};
|
||||
|
||||
/** Вхождение трека в конкретный плейлист. */
|
||||
export type PlaylistTrack = {
|
||||
id: PlaylistTrackId;
|
||||
audioId: PlaylistAudioId;
|
||||
};
|
||||
|
||||
export type AppPlaylist = {
|
||||
id: PlaylistId;
|
||||
name: string;
|
||||
tracks: PlaylistTrack[];
|
||||
};
|
||||
|
||||
export type PlaylistsState = {
|
||||
playlists: AppPlaylist[];
|
||||
audios: PlaylistAudio[];
|
||||
};
|
||||
|
||||
/** Снимок для упаковки в экспорт проекта / импорта. */
|
||||
export type PlaylistsExportBundle = {
|
||||
version: 1;
|
||||
playlists: AppPlaylist[];
|
||||
audios: PlaylistAudio[];
|
||||
/** Имя файла ассета → байты читаются отдельно в zip. */
|
||||
};
|
||||
|
||||
export function normalizePlaylistName(name: string): string {
|
||||
return name.trim() || 'Playlist';
|
||||
}
|
||||
|
||||
export function titleFromAudioFileName(fileName: string): string {
|
||||
const base = fileName.replace(/\.[^.]+$/u, '').trim();
|
||||
return base || fileName.trim() || 'Track';
|
||||
}
|
||||
|
||||
export function filterPlaylistsByQuery(
|
||||
state: PlaylistsState,
|
||||
query: string,
|
||||
): { playlists: AppPlaylist[]; matchedAudioIds: Set<PlaylistAudioId> } {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) {
|
||||
return { playlists: state.playlists, matchedAudioIds: new Set() };
|
||||
}
|
||||
const audioById = new Map(state.audios.map((a) => [a.id, a]));
|
||||
const matchedAudioIds = new Set<PlaylistAudioId>();
|
||||
const playlists: AppPlaylist[] = [];
|
||||
for (const pl of state.playlists) {
|
||||
const nameHit = pl.name.toLowerCase().includes(q);
|
||||
let trackHit = false;
|
||||
for (const tr of pl.tracks) {
|
||||
const audio = audioById.get(tr.audioId);
|
||||
if (!audio) continue;
|
||||
if (audio.title.toLowerCase().includes(q) || audio.originalName.toLowerCase().includes(q)) {
|
||||
matchedAudioIds.add(audio.id);
|
||||
trackHit = true;
|
||||
}
|
||||
}
|
||||
if (nameHit || trackHit) playlists.push(pl);
|
||||
}
|
||||
return { playlists, matchedAudioIds };
|
||||
}
|
||||
@@ -13,6 +13,9 @@ export type SceneTokenId = Brand<string, 'SceneTokenId'>;
|
||||
export type PlayerId = Brand<string, 'PlayerId'>;
|
||||
export type PlayerTeamId = Brand<string, 'PlayerTeamId'>;
|
||||
export type SceneNpcTokenId = Brand<string, 'SceneNpcTokenId'>;
|
||||
export type PlaylistId = Brand<string, 'PlaylistId'>;
|
||||
export type PlaylistAudioId = Brand<string, 'PlaylistAudioId'>;
|
||||
export type PlaylistTrackId = Brand<string, 'PlaylistTrackId'>;
|
||||
|
||||
export function asProjectId(value: string): ProjectId {
|
||||
return value as ProjectId;
|
||||
@@ -65,3 +68,15 @@ export function asPlayerTeamId(value: string): PlayerTeamId {
|
||||
export function asSceneNpcTokenId(value: string): SceneNpcTokenId {
|
||||
return value as SceneNpcTokenId;
|
||||
}
|
||||
|
||||
export function asPlaylistId(value: string): PlaylistId {
|
||||
return value as PlaylistId;
|
||||
}
|
||||
|
||||
export function asPlaylistAudioId(value: string): PlaylistAudioId {
|
||||
return value as PlaylistAudioId;
|
||||
}
|
||||
|
||||
export function asPlaylistTrackId(value: string): PlaylistTrackId {
|
||||
return value as PlaylistTrackId;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './appPlayers';
|
||||
export * from './appPlaylists';
|
||||
export * from './appTokens';
|
||||
export * from './domain';
|
||||
export * from './effects';
|
||||
|
||||
Reference in New Issue
Block a user