feat(playlists): библиотека плейлистов приложения
CRUD в редакторе, read-only окно на пульте, отдельный аудиоканал, экспорт/импорт и маппинг из Foundry. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
} from '../../shared/graph/storylineExportImport';
|
||||
import type { Project, ProjectId, SceneId } from '../../shared/types';
|
||||
import { Button, Select } from '../shared/ui/controls';
|
||||
import { useAppPlaylists } from '../shared/playlists/useAppPlaylists';
|
||||
|
||||
import styles from './EditorApp.module.css';
|
||||
import { useEditorI18n } from './i18n/EditorI18nContext';
|
||||
@@ -37,6 +38,7 @@ type ExportProjectModalProps = {
|
||||
projectId: ProjectId,
|
||||
selections: StorylineSelection[],
|
||||
npcIds: string[],
|
||||
playlistIds: string[],
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -50,12 +52,14 @@ export function ExportProjectModal({
|
||||
onExport,
|
||||
}: ExportProjectModalProps) {
|
||||
const { t } = useEditorI18n();
|
||||
const playlistsState = useAppPlaylists();
|
||||
const [step, setStep] = useState<'storylines' | 'npcs'>('storylines');
|
||||
const [projectId, setProjectId] = useState<ProjectId | null>(initialProjectId);
|
||||
const [storylines, setStorylines] = useState<StorylineListItem[]>([]);
|
||||
const [npcs, setNpcs] = useState<ExportNpcOption[]>([]);
|
||||
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());
|
||||
const [selectedNpcIds, setSelectedNpcIds] = useState<Set<string>>(new Set());
|
||||
const [selectedPlaylistIds, setSelectedPlaylistIds] = useState<Set<string>>(new Set());
|
||||
const [loadingStorylines, setLoadingStorylines] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -67,6 +71,7 @@ export function ExportProjectModal({
|
||||
setError(null);
|
||||
setSelectedKeys(new Set());
|
||||
setSelectedNpcIds(new Set());
|
||||
setSelectedPlaylistIds(new Set());
|
||||
setStep('storylines');
|
||||
setNpcs([]);
|
||||
}, [initialProjectId, open]);
|
||||
@@ -156,7 +161,7 @@ export function ExportProjectModal({
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
await onExport(projectId, selectedSelections, npcIds);
|
||||
await onExport(projectId, selectedSelections, npcIds, [...selectedPlaylistIds]);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
@@ -168,7 +173,7 @@ export function ExportProjectModal({
|
||||
|
||||
const goNextFromStorylines = () => {
|
||||
if (!canContinueStorylines) return;
|
||||
if (npcs.length === 0) {
|
||||
if (npcs.length === 0 && playlistsState.playlists.length === 0) {
|
||||
runExport([]);
|
||||
return;
|
||||
}
|
||||
@@ -248,26 +253,58 @@ export function ExportProjectModal({
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.fieldGrid}>
|
||||
<div className={styles.muted}>{t('export.npcsHint')}</div>
|
||||
<div className={styles.storylineChecklist}>
|
||||
{npcs.map((n) => (
|
||||
<label key={n.id} className={styles.storylineCheck}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedNpcIds.has(n.id)}
|
||||
disabled={saving}
|
||||
onChange={() => toggleNpc(n.id)}
|
||||
/>
|
||||
<span>{n.name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
disabled={saving || npcs.length === 0}
|
||||
onClick={() => setSelectedNpcIds(new Set(npcs.map((n) => n.id)))}
|
||||
>
|
||||
{t('export.selectAllNpcs')}
|
||||
</Button>
|
||||
{npcs.length > 0 ? (
|
||||
<>
|
||||
<div className={styles.muted}>{t('export.npcsHint')}</div>
|
||||
<div className={styles.storylineChecklist}>
|
||||
{npcs.map((n) => (
|
||||
<label key={n.id} className={styles.storylineCheck}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedNpcIds.has(n.id)}
|
||||
disabled={saving}
|
||||
onChange={() => toggleNpc(n.id)}
|
||||
/>
|
||||
<span>{n.name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
disabled={saving || npcs.length === 0}
|
||||
onClick={() => setSelectedNpcIds(new Set(npcs.map((n) => n.id)))}
|
||||
>
|
||||
{t('export.selectAllNpcs')}
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
{playlistsState.playlists.length > 0 ? (
|
||||
<>
|
||||
<div className={styles.fieldLabel}>{t('export.playlists')}</div>
|
||||
<div className={styles.muted}>{t('export.playlistsHint')}</div>
|
||||
<div className={styles.storylineChecklist}>
|
||||
{playlistsState.playlists.map((pl) => (
|
||||
<label key={pl.id} className={styles.storylineCheck}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPlaylistIds.has(pl.id)}
|
||||
disabled={saving}
|
||||
onChange={() => {
|
||||
setSelectedPlaylistIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(pl.id)) next.delete(pl.id);
|
||||
else next.add(pl.id);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
{pl.name} ({pl.tracks.length})
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -289,7 +326,7 @@ export function ExportProjectModal({
|
||||
disabled={!canContinueStorylines || saving}
|
||||
onClick={goNextFromStorylines}
|
||||
>
|
||||
{npcs.length > 0 ? t('export.next') : t('export.saveAs')}
|
||||
{npcs.length > 0 || playlistsState.playlists.length > 0 ? t('export.next') : t('export.saveAs')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user