import { RemoveClipCommand, RenameClipCommand, SetClipPropsCommand } from '../../core/commands/clipCommands'; import type { SoundAsset, SoundClip } from '../../core/model/Asset'; import { findAssetOf, type Project } from '../../core/model/Project'; import { clipsOf } from '../../core/model/sound'; import { CLIP_ROWS } from '../../core/properties/sounds'; import { Button } from '../../ui'; import { createFieldRow } from './fieldFactory'; import { addGap, addNameField, addNote, addTitle, type InspectorHost } from './host'; import { previewingHandlers } from './previewHandlers'; /** Structure-defining facts of a clip selection: none – only the clip disappearing rebuilds. */ export function clipSignature(project: Project, assetId: string, clipId: string): string { return currentClip(project, assetId, clipId) ? 'clip' : 'missing'; } /** Name, the stretch of the file it is and how it is played, and the way to delete it (#357). */ export function buildClipSection(host: InspectorHost, assetId: string, clipId: string): void { const asset = currentSound(host.state.project, assetId); const clip = asset && currentClip(host.state.project, assetId, clipId); if (!asset || !clip) return; addTitle(host, 'SOUND CLIP'); addNameField(host, clip.name, { onChange: (name) => { // The command rejects a blank name but cannot put the old text back into the field. if (name.trim()) host.state.execute(new RenameClipCommand(assetId, clipId, name.trim())); else host.nameField?.setValue(clip.name); }, }); addNote(host, `Clip of the sound "${asset.name}". Its volume, loop, bus and sound FX are the sound's.`); for (const def of CLIP_ROWS) { const handlers = previewingHandlers(host, { target: () => currentClip(host.state.project, assetId, clipId), read: (target) => target.props[def.key], write: (target, value) => (target.props[def.key] = value), remove: (target) => delete target.props[def.key], commit: (value) => new SetClipPropsCommand(assetId, clipId, { [def.key]: value }, `Set clip ${def.label}`), }); const row = createFieldRow(host, def, clip.props[def.key] ?? def.default, handlers); host.fields.set(def.key, row); host.body.add(row.row); } addGap(host); host.body.add( new Button('Delete clip', { variant: 'danger', onClick: () => host.state.execute(new RemoveClipCommand(assetId, clipId)), }), ); } /** Pushes the clip's current name and values into the rows. False when it is gone (rebuild). */ export function refreshClipSection(host: InspectorHost, assetId: string, clipId: string): boolean { const clip = currentClip(host.state.project, assetId, clipId); if (!clip) return false; host.nameField?.setValue(clip.name); for (const def of CLIP_ROWS) host.fields.get(def.key)?.setValue(clip.props[def.key] ?? def.default); return true; } /** Looked up inside the sound, so a selection left over from a deleted sound edits nothing. */ function currentClip(project: Project, assetId: string, clipId: string): SoundClip | undefined { const asset = currentSound(project, assetId); return asset && clipsOf(asset).find((c) => c.id === clipId); } function currentSound(project: Project, assetId: string): SoundAsset | undefined { return findAssetOf(project, assetId, 'sound'); }