@signalsandsorcery/plugin-sdk 2.26.1 → 2.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +446 -2
- package/dist/index.d.ts +446 -2
- package/dist/index.js +1411 -253
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1393 -259
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -30,6 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
AUDIO_EFFECTS: () => AUDIO_EFFECTS,
|
|
34
|
+
AUDIO_EFFECT_LABEL: () => AUDIO_EFFECT_LABEL,
|
|
33
35
|
ConfirmDialog: () => ConfirmDialog,
|
|
34
36
|
CrossfadeModal: () => CrossfadeModal,
|
|
35
37
|
CrossfadeTrackRow: () => CrossfadeTrackRow,
|
|
@@ -47,6 +49,8 @@ __export(index_exports, {
|
|
|
47
49
|
FX_DISPLAY_LABELS: () => FX_DISPLAY_LABELS,
|
|
48
50
|
FX_ENGINE_PLUGIN_NAMES: () => FX_ENGINE_PLUGIN_NAMES,
|
|
49
51
|
FX_PRESET_CONFIGS: () => FX_PRESET_CONFIGS,
|
|
52
|
+
FadeModal: () => FadeModal,
|
|
53
|
+
FadeTrackRow: () => FadeTrackRow,
|
|
50
54
|
FxToggleBar: () => FxToggleBar,
|
|
51
55
|
GUTTER_W: () => GUTTER_W,
|
|
52
56
|
ImportTrackModal: () => ImportTrackModal,
|
|
@@ -65,30 +69,50 @@ __export(index_exports, {
|
|
|
65
69
|
SamplePackCTACard: () => SamplePackCTACard,
|
|
66
70
|
ScrollingWaveform: () => ScrollingWaveform,
|
|
67
71
|
SorceryProgressBar: () => SorceryProgressBar,
|
|
72
|
+
TEXTURAL_ROLES: () => TEXTURAL_ROLES,
|
|
73
|
+
TRANSITION_DESIGNER_DRAFT_KEY: () => TRANSITION_DESIGNER_DRAFT_KEY,
|
|
68
74
|
TrackDrawer: () => TrackDrawer,
|
|
69
75
|
TrackMeterStrip: () => TrackMeterStrip,
|
|
70
76
|
TrackRow: () => TrackRow,
|
|
77
|
+
TransitionDesigner: () => TransitionDesigner,
|
|
71
78
|
VolumeSlider: () => VolumeSlider,
|
|
72
79
|
WaveformView: () => WaveformView,
|
|
73
80
|
analyzeWavPeak: () => analyzeWavPeak,
|
|
81
|
+
asAudioEffect: () => asAudioEffect,
|
|
74
82
|
asCrossfadeMeta: () => asCrossfadeMeta,
|
|
83
|
+
asFadeMeta: () => asFadeMeta,
|
|
84
|
+
asTransitionDesignerDraft: () => asTransitionDesignerDraft,
|
|
75
85
|
buildCrossfadeInpaintPrompt: () => buildCrossfadeInpaintPrompt,
|
|
76
86
|
buildCrossfadeVolumeCurves: () => buildCrossfadeVolumeCurves,
|
|
87
|
+
buildFadeVolumeCurve: () => buildFadeVolumeCurve,
|
|
88
|
+
buildRowSlots: () => buildRowSlots,
|
|
77
89
|
calculateTimeBasedTarget: () => calculateTimeBasedTarget,
|
|
78
90
|
cellToPx: () => cellToPx,
|
|
79
91
|
centerScrollTop: () => centerScrollTop,
|
|
80
92
|
computePeaks: () => computePeaks,
|
|
93
|
+
dbIdsFromKeys: () => dbIdsFromKeys,
|
|
81
94
|
dbToSlider: () => dbToSlider,
|
|
95
|
+
defaultFadeGesture: () => defaultFadeGesture,
|
|
82
96
|
drawWaveform: () => drawWaveform,
|
|
83
97
|
formatConcurrentTracks: () => formatConcurrentTracks,
|
|
98
|
+
hashString: () => hashString,
|
|
84
99
|
moveItem: () => moveItem,
|
|
100
|
+
normalizeSlots: () => normalizeSlots,
|
|
101
|
+
padPair: () => padPair,
|
|
102
|
+
padSlots: () => padSlots,
|
|
85
103
|
parseCrossfadePairs: () => parseCrossfadePairs,
|
|
104
|
+
parseFades: () => parseFades,
|
|
86
105
|
pickTopKWeighted: () => pickTopKWeighted,
|
|
87
106
|
pitchToName: () => pitchToName,
|
|
88
107
|
pxToCell: () => pxToCell,
|
|
108
|
+
reconcileSlots: () => reconcileSlots,
|
|
89
109
|
resizeNoteDuration: () => resizeNoteDuration,
|
|
110
|
+
rowKey: () => rowKey,
|
|
111
|
+
rowType: () => rowType,
|
|
90
112
|
scorePromptMatch: () => scorePromptMatch,
|
|
91
113
|
sliderToDb: () => sliderToDb,
|
|
114
|
+
slotsEqual: () => slotsEqual,
|
|
115
|
+
soundIdentity: () => soundIdentity,
|
|
92
116
|
synthesizeCuePoints: () => synthesizeCuePoints,
|
|
93
117
|
tokenizePrompt: () => tokenizePrompt,
|
|
94
118
|
transposeNotes: () => transposeNotes,
|
|
@@ -1448,6 +1472,7 @@ var LevelMeter = ({
|
|
|
1448
1472
|
|
|
1449
1473
|
// src/hooks/useTrackLevels.ts
|
|
1450
1474
|
var import_react5 = require("react");
|
|
1475
|
+
var meterDiagRLast = /* @__PURE__ */ new Map();
|
|
1451
1476
|
var POLL_INTERVAL_MS = 33;
|
|
1452
1477
|
var HIDDEN_RECHECK_MS = 250;
|
|
1453
1478
|
var METER_FLOOR_DB = -120;
|
|
@@ -1579,6 +1604,11 @@ function useTrackMeter(handle, trackId) {
|
|
|
1579
1604
|
}
|
|
1580
1605
|
const update = () => {
|
|
1581
1606
|
const level = handle.getLevel(trackId);
|
|
1607
|
+
const dNow = Date.now();
|
|
1608
|
+
if ((meterDiagRLast.get(trackId) ?? 0) < dNow - 3e3) {
|
|
1609
|
+
meterDiagRLast.set(trackId, dNow);
|
|
1610
|
+
console.log(`[MeterDiagR] lookup trackId=${trackId} \u2192 ${level === null ? "MISS (no level for this id)" : "hit"}`);
|
|
1611
|
+
}
|
|
1582
1612
|
const now = performance.now();
|
|
1583
1613
|
const dtSec = lastTickRef.current ? Math.max(0, (now - lastTickRef.current) / 1e3) : 0;
|
|
1584
1614
|
lastTickRef.current = now;
|
|
@@ -2280,8 +2310,7 @@ function TrackRow({
|
|
|
2280
2310
|
{
|
|
2281
2311
|
"data-testid": "sdk-mute-button",
|
|
2282
2312
|
onClick: onMuteToggle,
|
|
2283
|
-
|
|
2284
|
-
className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isGenerating ? "bg-sas-panel text-sas-muted/50 cursor-not-allowed" : isMuted ? "bg-red-600 text-white" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
|
|
2313
|
+
className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${isMuted ? "bg-red-600 text-white" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"}`,
|
|
2285
2314
|
title: isMuted ? "Unmute track" : "Mute track",
|
|
2286
2315
|
children: "M"
|
|
2287
2316
|
}
|
|
@@ -2532,6 +2561,18 @@ function CrossfadeTrackRow({
|
|
|
2532
2561
|
}
|
|
2533
2562
|
|
|
2534
2563
|
// src/crossfade-meta.ts
|
|
2564
|
+
function hashString(s) {
|
|
2565
|
+
let h = 5381;
|
|
2566
|
+
for (let i = 0; i < s.length; i++) h = (h << 5) + h ^ s.charCodeAt(i) | 0;
|
|
2567
|
+
return (h >>> 0).toString(36);
|
|
2568
|
+
}
|
|
2569
|
+
function soundIdentity(snap) {
|
|
2570
|
+
if (!snap) return "";
|
|
2571
|
+
if (snap.kind === "preset") return `p:${hashString(snap.state)}`;
|
|
2572
|
+
if (snap.kind === "sample") return `s:${snap.samplePath}`;
|
|
2573
|
+
if (snap.kind === "instrument") return `i:${snap.instrumentId ?? ""}:${hashString(JSON.stringify(snap.zones))}`;
|
|
2574
|
+
return "";
|
|
2575
|
+
}
|
|
2535
2576
|
var EQUAL_POWER_GAIN = 0.707;
|
|
2536
2577
|
function asCrossfadeMeta(val) {
|
|
2537
2578
|
if (!val || typeof val !== "object") return null;
|
|
@@ -2675,9 +2716,452 @@ function buildCrossfadeInpaintPrompt(input) {
|
|
|
2675
2716
|
return lines.join("\n");
|
|
2676
2717
|
}
|
|
2677
2718
|
|
|
2678
|
-
// src/
|
|
2679
|
-
|
|
2719
|
+
// src/fade-meta.ts
|
|
2720
|
+
function asFadeMeta(val) {
|
|
2721
|
+
if (!val || typeof val !== "object") return null;
|
|
2722
|
+
const m = val;
|
|
2723
|
+
if (m.direction !== "in" && m.direction !== "out") return null;
|
|
2724
|
+
if (m.gesture !== "volume" && m.gesture !== "build") return null;
|
|
2725
|
+
const effect = m.effect === "stutter" || m.effect === "chopped" || m.effect === "delay" || m.effect === "fade" ? m.effect : void 0;
|
|
2726
|
+
return {
|
|
2727
|
+
direction: m.direction,
|
|
2728
|
+
gesture: m.gesture,
|
|
2729
|
+
effect,
|
|
2730
|
+
sourceTrackDbId: typeof m.sourceTrackDbId === "string" ? m.sourceTrackDbId : "",
|
|
2731
|
+
sourceSceneId: typeof m.sourceSceneId === "string" ? m.sourceSceneId : "",
|
|
2732
|
+
sourceName: typeof m.sourceName === "string" ? m.sourceName : "",
|
|
2733
|
+
soundLabel: typeof m.soundLabel === "string" ? m.soundLabel : "",
|
|
2734
|
+
sliderPos: typeof m.sliderPos === "number" ? m.sliderPos : 0.5
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
function parseFades(sceneData) {
|
|
2738
|
+
const out = [];
|
|
2739
|
+
for (const [key, val] of Object.entries(sceneData)) {
|
|
2740
|
+
const match = /^track:(.+):fade$/.exec(key);
|
|
2741
|
+
if (!match) continue;
|
|
2742
|
+
const meta = asFadeMeta(val);
|
|
2743
|
+
if (!meta) continue;
|
|
2744
|
+
out.push({ dbId: match[1], meta });
|
|
2745
|
+
}
|
|
2746
|
+
return out;
|
|
2747
|
+
}
|
|
2748
|
+
function buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture, steps = 32) {
|
|
2749
|
+
const durationSeconds = bars * 4 * 60 / Math.max(1, bpm);
|
|
2750
|
+
if (gesture === "build") {
|
|
2751
|
+
return [
|
|
2752
|
+
{ time: 0, db: 0 },
|
|
2753
|
+
{ time: Math.round(durationSeconds * 1e3) / 1e3, db: 0 }
|
|
2754
|
+
];
|
|
2755
|
+
}
|
|
2756
|
+
const s = Math.min(0.98, Math.max(0.02, sliderPos));
|
|
2757
|
+
const round = (n) => Math.round(n * 1e3) / 1e3;
|
|
2758
|
+
const points = [];
|
|
2759
|
+
for (let i = 0; i <= steps; i++) {
|
|
2760
|
+
const x = i / steps;
|
|
2761
|
+
const time = round(x * durationSeconds);
|
|
2762
|
+
const theta = x <= s ? x / s * (Math.PI / 4) : Math.PI / 4 + (x - s) / (1 - s) * (Math.PI / 4);
|
|
2763
|
+
const gain = direction === "out" ? Math.cos(theta) : Math.sin(theta);
|
|
2764
|
+
points.push({ time, db: Math.round(gainToDb(gain) * 100) / 100 });
|
|
2765
|
+
}
|
|
2766
|
+
return points;
|
|
2767
|
+
}
|
|
2768
|
+
var TEXTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
2769
|
+
"pads",
|
|
2770
|
+
"pad",
|
|
2771
|
+
"strings",
|
|
2772
|
+
"atmospheres",
|
|
2773
|
+
"atmosphere",
|
|
2774
|
+
"atmos",
|
|
2775
|
+
"drones",
|
|
2776
|
+
"drone",
|
|
2777
|
+
"soundscapes",
|
|
2778
|
+
"soundscape"
|
|
2779
|
+
]);
|
|
2780
|
+
function defaultFadeGesture(role) {
|
|
2781
|
+
if (!role) return "build";
|
|
2782
|
+
const norm = role.toLowerCase().replace(/[\s_-]+/g, " ").trim();
|
|
2783
|
+
if (TEXTURAL_ROLES.has(norm)) return "volume";
|
|
2784
|
+
for (const token of norm.split(" ")) {
|
|
2785
|
+
if (TEXTURAL_ROLES.has(token)) return "volume";
|
|
2786
|
+
}
|
|
2787
|
+
return "build";
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// src/components/FadeTrackRow.tsx
|
|
2791
|
+
var import_react11 = __toESM(require("react"));
|
|
2680
2792
|
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
2793
|
+
function FadeCaption({
|
|
2794
|
+
layer,
|
|
2795
|
+
direction,
|
|
2796
|
+
gesture
|
|
2797
|
+
}) {
|
|
2798
|
+
const tag = direction === "in" ? "Fade in" : "Fade out";
|
|
2799
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
|
|
2800
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: tag }),
|
|
2801
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-[11px] text-sas-text truncate", title: layer.sourceName ?? layer.name, children: layer.sourceName ?? layer.name }),
|
|
2802
|
+
layer.soundLabel && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: layer.soundLabel, children: [
|
|
2803
|
+
"\xB7 ",
|
|
2804
|
+
layer.soundLabel
|
|
2805
|
+
] }),
|
|
2806
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", title: `Fade gesture: ${gesture}`, children: [
|
|
2807
|
+
"\xB7 ",
|
|
2808
|
+
gesture
|
|
2809
|
+
] })
|
|
2810
|
+
] });
|
|
2811
|
+
}
|
|
2812
|
+
function FadeTrackRow({
|
|
2813
|
+
layer,
|
|
2814
|
+
direction,
|
|
2815
|
+
gesture,
|
|
2816
|
+
effect,
|
|
2817
|
+
sliderPos = 0.5,
|
|
2818
|
+
onMuteToggle,
|
|
2819
|
+
onSoloToggle,
|
|
2820
|
+
onVolumeChange,
|
|
2821
|
+
onPanChange,
|
|
2822
|
+
onDelete,
|
|
2823
|
+
onSliderChange,
|
|
2824
|
+
levels,
|
|
2825
|
+
accentColor = "#9333EA"
|
|
2826
|
+
}) {
|
|
2827
|
+
const [confirmDelete, setConfirmDelete] = import_react11.default.useState(false);
|
|
2828
|
+
const leftLabel = direction === "in" ? "(silent)" : layer.sourceName ?? layer.name;
|
|
2829
|
+
const rightLabel = direction === "in" ? layer.sourceName ?? layer.name : "(silent)";
|
|
2830
|
+
const verb = effect && effect !== "fade" ? effect.charAt(0).toUpperCase() + effect.slice(1) : "Fade";
|
|
2831
|
+
const badge = direction === "in" ? `\u2197 ${verb} in` : `\u2198 ${verb} out`;
|
|
2832
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
|
|
2833
|
+
"div",
|
|
2834
|
+
{
|
|
2835
|
+
"data-testid": "fade-track-row",
|
|
2836
|
+
className: "w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden",
|
|
2837
|
+
style: { borderLeftColor: accentColor, borderLeftWidth: "3px" },
|
|
2838
|
+
children: [
|
|
2839
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60", children: [
|
|
2840
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2841
|
+
"span",
|
|
2842
|
+
{
|
|
2843
|
+
"data-testid": "fade-direction-badge",
|
|
2844
|
+
className: "text-[10px] font-bold uppercase tracking-wide",
|
|
2845
|
+
style: { color: accentColor },
|
|
2846
|
+
children: badge
|
|
2847
|
+
}
|
|
2848
|
+
),
|
|
2849
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2850
|
+
"button",
|
|
2851
|
+
{
|
|
2852
|
+
"data-testid": "fade-delete-button",
|
|
2853
|
+
onClick: () => setConfirmDelete(true),
|
|
2854
|
+
className: "text-sas-danger/70 hover:text-sas-danger px-1 transition-colors text-sm",
|
|
2855
|
+
title: "Delete fade",
|
|
2856
|
+
"aria-label": "Delete fade",
|
|
2857
|
+
children: "x"
|
|
2858
|
+
}
|
|
2859
|
+
)
|
|
2860
|
+
] }),
|
|
2861
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2862
|
+
TrackRow,
|
|
2863
|
+
{
|
|
2864
|
+
track: { id: layer.trackId, name: "", role: layer.role },
|
|
2865
|
+
runtimeState: layer.runtimeState,
|
|
2866
|
+
fxDetailState: EMPTY_FX_DETAIL_STATE,
|
|
2867
|
+
drawerOpen: false,
|
|
2868
|
+
drawerTab: "fx",
|
|
2869
|
+
levels,
|
|
2870
|
+
accentColor,
|
|
2871
|
+
contentSlot: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(FadeCaption, { layer, direction, gesture }),
|
|
2872
|
+
onMuteToggle,
|
|
2873
|
+
onSoloToggle,
|
|
2874
|
+
onVolumeChange,
|
|
2875
|
+
onPanChange
|
|
2876
|
+
}
|
|
2877
|
+
),
|
|
2878
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "fade-slider-row", children: [
|
|
2879
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2880
|
+
"span",
|
|
2881
|
+
{
|
|
2882
|
+
className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0",
|
|
2883
|
+
title: leftLabel,
|
|
2884
|
+
children: leftLabel
|
|
2885
|
+
}
|
|
2886
|
+
),
|
|
2887
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2888
|
+
"input",
|
|
2889
|
+
{
|
|
2890
|
+
type: "range",
|
|
2891
|
+
"data-testid": "fade-slider",
|
|
2892
|
+
min: 0,
|
|
2893
|
+
max: 1,
|
|
2894
|
+
step: 0.01,
|
|
2895
|
+
value: sliderPos,
|
|
2896
|
+
disabled: !onSliderChange,
|
|
2897
|
+
onChange: onSliderChange ? (e) => onSliderChange(Number(e.target.value)) : void 0,
|
|
2898
|
+
style: { accentColor },
|
|
2899
|
+
className: "flex-1 disabled:opacity-60 disabled:cursor-not-allowed",
|
|
2900
|
+
"aria-label": "Fade position"
|
|
2901
|
+
}
|
|
2902
|
+
),
|
|
2903
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2904
|
+
"span",
|
|
2905
|
+
{
|
|
2906
|
+
className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0",
|
|
2907
|
+
title: rightLabel,
|
|
2908
|
+
children: rightLabel
|
|
2909
|
+
}
|
|
2910
|
+
)
|
|
2911
|
+
] }),
|
|
2912
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
2913
|
+
ConfirmDialog,
|
|
2914
|
+
{
|
|
2915
|
+
open: confirmDelete,
|
|
2916
|
+
title: "Delete fade?",
|
|
2917
|
+
message: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_jsx_runtime13.Fragment, { children: "This fade track will be permanently removed from this scene. This cannot be undone." }),
|
|
2918
|
+
confirmLabel: "Delete",
|
|
2919
|
+
onConfirm: () => {
|
|
2920
|
+
setConfirmDelete(false);
|
|
2921
|
+
onDelete();
|
|
2922
|
+
},
|
|
2923
|
+
onCancel: () => setConfirmDelete(false),
|
|
2924
|
+
testIdPrefix: "fade-delete-confirm"
|
|
2925
|
+
}
|
|
2926
|
+
)
|
|
2927
|
+
]
|
|
2928
|
+
}
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
// src/components/FadeModal.tsx
|
|
2933
|
+
var import_react12 = require("react");
|
|
2934
|
+
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
2935
|
+
function shortId(dbId) {
|
|
2936
|
+
return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
|
|
2937
|
+
}
|
|
2938
|
+
var normRole = (r) => (r ?? "").toLowerCase().trim();
|
|
2939
|
+
function computeOrphans(from, to, excludeSet) {
|
|
2940
|
+
const bucket = (list) => {
|
|
2941
|
+
const m = /* @__PURE__ */ new Map();
|
|
2942
|
+
for (const t of list) {
|
|
2943
|
+
const k = normRole(t.role);
|
|
2944
|
+
const arr = m.get(k);
|
|
2945
|
+
if (arr) arr.push(t);
|
|
2946
|
+
else m.set(k, [t]);
|
|
2947
|
+
}
|
|
2948
|
+
return m;
|
|
2949
|
+
};
|
|
2950
|
+
const fromByRole = bucket(from);
|
|
2951
|
+
const toByRole = bucket(to);
|
|
2952
|
+
const roles = /* @__PURE__ */ new Set([...fromByRole.keys(), ...toByRole.keys()]);
|
|
2953
|
+
const fadeOut = [];
|
|
2954
|
+
const fadeIn = [];
|
|
2955
|
+
for (const role of roles) {
|
|
2956
|
+
const f = fromByRole.get(role) ?? [];
|
|
2957
|
+
const t = toByRole.get(role) ?? [];
|
|
2958
|
+
const shared = Math.min(f.length, t.length);
|
|
2959
|
+
fadeOut.push(...f.slice(shared));
|
|
2960
|
+
fadeIn.push(...t.slice(shared));
|
|
2961
|
+
}
|
|
2962
|
+
return {
|
|
2963
|
+
fadeOut: fadeOut.filter((x) => !excludeSet.has(x.dbId)),
|
|
2964
|
+
fadeIn: fadeIn.filter((x) => !excludeSet.has(x.dbId))
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
function OrphanRow({
|
|
2968
|
+
track,
|
|
2969
|
+
gesture,
|
|
2970
|
+
selected,
|
|
2971
|
+
disabled,
|
|
2972
|
+
onSelect,
|
|
2973
|
+
testId
|
|
2974
|
+
}) {
|
|
2975
|
+
const primary = track.prompt?.trim() || track.name;
|
|
2976
|
+
const meta = [track.role, shortId(track.dbId), gesture].filter(Boolean).join(" \xB7 ");
|
|
2977
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
2978
|
+
"button",
|
|
2979
|
+
{
|
|
2980
|
+
type: "button",
|
|
2981
|
+
role: "radio",
|
|
2982
|
+
"aria-checked": selected,
|
|
2983
|
+
"data-testid": testId,
|
|
2984
|
+
"data-value": track.dbId,
|
|
2985
|
+
onClick: onSelect,
|
|
2986
|
+
disabled,
|
|
2987
|
+
className: `w-full text-left px-2 py-1.5 rounded-sm border transition-colors disabled:opacity-50 ${selected ? "bg-sas-accent/15 border-sas-accent" : "bg-sas-panel border-sas-border hover:border-sas-accent/50"}`,
|
|
2988
|
+
children: [
|
|
2989
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
|
|
2990
|
+
meta && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
|
|
2991
|
+
]
|
|
2992
|
+
}
|
|
2993
|
+
);
|
|
2994
|
+
}
|
|
2995
|
+
function FadeModal({
|
|
2996
|
+
host,
|
|
2997
|
+
open,
|
|
2998
|
+
fromSceneId,
|
|
2999
|
+
toSceneId,
|
|
3000
|
+
fromSceneName,
|
|
3001
|
+
toSceneName,
|
|
3002
|
+
excludeSourceDbIds,
|
|
3003
|
+
onClose,
|
|
3004
|
+
onCreate,
|
|
3005
|
+
testIdPrefix = "fade-modal"
|
|
3006
|
+
}) {
|
|
3007
|
+
const [load, setLoad] = (0, import_react12.useState)({ status: "loading" });
|
|
3008
|
+
const [selectedDbId, setSelectedDbId] = (0, import_react12.useState)("");
|
|
3009
|
+
const [isCreating, setIsCreating] = (0, import_react12.useState)(false);
|
|
3010
|
+
const [error, setError] = (0, import_react12.useState)(null);
|
|
3011
|
+
const [fromName, setFromName] = (0, import_react12.useState)(null);
|
|
3012
|
+
const [toName, setToName] = (0, import_react12.useState)(null);
|
|
3013
|
+
const cancelRef = (0, import_react12.useRef)(null);
|
|
3014
|
+
const refresh = (0, import_react12.useCallback)(async () => {
|
|
3015
|
+
if (!host.listSceneFamilyTracks) {
|
|
3016
|
+
setLoad({ status: "error", message: "This host does not support fades." });
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
setLoad({ status: "loading" });
|
|
3020
|
+
try {
|
|
3021
|
+
const [from, to, fName, tName] = await Promise.all([
|
|
3022
|
+
host.listSceneFamilyTracks(fromSceneId),
|
|
3023
|
+
host.listSceneFamilyTracks(toSceneId),
|
|
3024
|
+
host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),
|
|
3025
|
+
host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null)
|
|
3026
|
+
]);
|
|
3027
|
+
setFromName(fName);
|
|
3028
|
+
setToName(tName);
|
|
3029
|
+
setLoad({ status: "ready", from, to });
|
|
3030
|
+
} catch (err) {
|
|
3031
|
+
setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
|
|
3032
|
+
}
|
|
3033
|
+
}, [host, fromSceneId, toSceneId]);
|
|
3034
|
+
(0, import_react12.useEffect)(() => {
|
|
3035
|
+
if (open) {
|
|
3036
|
+
setError(null);
|
|
3037
|
+
setIsCreating(false);
|
|
3038
|
+
setSelectedDbId("");
|
|
3039
|
+
void refresh();
|
|
3040
|
+
}
|
|
3041
|
+
}, [open, refresh]);
|
|
3042
|
+
const excludeSet = (0, import_react12.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
|
|
3043
|
+
const { fadeOut, fadeIn } = (0, import_react12.useMemo)(
|
|
3044
|
+
() => load.status === "ready" ? computeOrphans(load.from, load.to, excludeSet) : { fadeOut: [], fadeIn: [] },
|
|
3045
|
+
[load, excludeSet]
|
|
3046
|
+
);
|
|
3047
|
+
const allOrphans = (0, import_react12.useMemo)(
|
|
3048
|
+
() => [
|
|
3049
|
+
...fadeOut.map((t) => ({ track: t, direction: "out" })),
|
|
3050
|
+
...fadeIn.map((t) => ({ track: t, direction: "in" }))
|
|
3051
|
+
],
|
|
3052
|
+
[fadeOut, fadeIn]
|
|
3053
|
+
);
|
|
3054
|
+
(0, import_react12.useEffect)(() => {
|
|
3055
|
+
if (!allOrphans.some((o) => o.track.dbId === selectedDbId)) {
|
|
3056
|
+
setSelectedDbId(allOrphans[0]?.track.dbId ?? "");
|
|
3057
|
+
}
|
|
3058
|
+
}, [allOrphans, selectedDbId]);
|
|
3059
|
+
const selected = allOrphans.find((o) => o.track.dbId === selectedDbId) ?? null;
|
|
3060
|
+
const canCreate = !isCreating && !!selected;
|
|
3061
|
+
const handleClose = (0, import_react12.useCallback)(() => {
|
|
3062
|
+
if (!isCreating) onClose();
|
|
3063
|
+
}, [isCreating, onClose]);
|
|
3064
|
+
const handleCreate = (0, import_react12.useCallback)(async () => {
|
|
3065
|
+
if (!selected) return;
|
|
3066
|
+
setIsCreating(true);
|
|
3067
|
+
setError(null);
|
|
3068
|
+
try {
|
|
3069
|
+
await onCreate(
|
|
3070
|
+
{ dbId: selected.track.dbId, name: selected.track.name, role: selected.track.role },
|
|
3071
|
+
selected.direction,
|
|
3072
|
+
defaultFadeGesture(selected.track.role)
|
|
3073
|
+
);
|
|
3074
|
+
onClose();
|
|
3075
|
+
} catch (err) {
|
|
3076
|
+
setError(err instanceof Error ? err.message : "Failed to create fade.");
|
|
3077
|
+
setIsCreating(false);
|
|
3078
|
+
}
|
|
3079
|
+
}, [selected, onCreate, onClose]);
|
|
3080
|
+
const fromLabel = fromName ?? fromSceneName ?? null;
|
|
3081
|
+
const toLabel = toName ?? toSceneName ?? null;
|
|
3082
|
+
if (!open) return null;
|
|
3083
|
+
const renderSection = (heading, list, section) => {
|
|
3084
|
+
if (list.length === 0) return null;
|
|
3085
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "block", children: [
|
|
3086
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: heading }),
|
|
3087
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
3088
|
+
"div",
|
|
3089
|
+
{
|
|
3090
|
+
role: "radiogroup",
|
|
3091
|
+
"aria-label": heading,
|
|
3092
|
+
"data-testid": `${testIdPrefix}-${section === "out" ? "fade-out" : "fade-in"}-list`,
|
|
3093
|
+
className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
|
|
3094
|
+
children: list.map((t) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
3095
|
+
OrphanRow,
|
|
3096
|
+
{
|
|
3097
|
+
track: t,
|
|
3098
|
+
gesture: defaultFadeGesture(t.role),
|
|
3099
|
+
selected: t.dbId === selectedDbId,
|
|
3100
|
+
disabled: isCreating,
|
|
3101
|
+
onSelect: () => setSelectedDbId(t.dbId),
|
|
3102
|
+
testId: `${testIdPrefix}-option-${t.dbId}`
|
|
3103
|
+
},
|
|
3104
|
+
t.dbId
|
|
3105
|
+
))
|
|
3106
|
+
}
|
|
3107
|
+
)
|
|
3108
|
+
] });
|
|
3109
|
+
};
|
|
3110
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
3111
|
+
"div",
|
|
3112
|
+
{
|
|
3113
|
+
className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
|
|
3114
|
+
onClick: (e) => e.stopPropagation(),
|
|
3115
|
+
"data-testid": `${testIdPrefix}-box`,
|
|
3116
|
+
children: [
|
|
3117
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h3", { className: "text-sm font-bold text-sas-text", children: "Add fade" }),
|
|
3118
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
|
|
3119
|
+
"Tracks with no counterpart between",
|
|
3120
|
+
" ",
|
|
3121
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
|
|
3122
|
+
" and",
|
|
3123
|
+
" ",
|
|
3124
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
|
|
3125
|
+
" can gracefully fade out (leaving) or fade in (entering) across this transition."
|
|
3126
|
+
] }),
|
|
3127
|
+
load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
|
|
3128
|
+
load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
|
|
3129
|
+
load.status === "ready" && (allOrphans.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", "data-testid": `${testIdPrefix}-empty`, children: "Every track has a counterpart in the other scene \u2014 nothing to fade. Use \u201C+ Crossfade\u201D to bridge matching tracks." }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
|
|
3130
|
+
renderSection(`Fade out${fromLabel ? ` (from ${fromLabel})` : ""}`, fadeOut, "out"),
|
|
3131
|
+
renderSection(`Fade in${toLabel ? ` (to ${toLabel})` : ""}`, fadeIn, "in")
|
|
3132
|
+
] })),
|
|
3133
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
|
|
3134
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex justify-end gap-2 pt-1", children: [
|
|
3135
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
3136
|
+
"button",
|
|
3137
|
+
{
|
|
3138
|
+
ref: cancelRef,
|
|
3139
|
+
"data-testid": `${testIdPrefix}-cancel`,
|
|
3140
|
+
onClick: onClose,
|
|
3141
|
+
disabled: isCreating,
|
|
3142
|
+
className: "px-3 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-text disabled:opacity-50",
|
|
3143
|
+
children: "Cancel"
|
|
3144
|
+
}
|
|
3145
|
+
),
|
|
3146
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
3147
|
+
"button",
|
|
3148
|
+
{
|
|
3149
|
+
"data-testid": `${testIdPrefix}-confirm`,
|
|
3150
|
+
onClick: handleCreate,
|
|
3151
|
+
disabled: !canCreate,
|
|
3152
|
+
className: `px-3 py-1 text-xs rounded-sm border transition-colors ${canCreate ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
|
|
3153
|
+
children: isCreating ? "Generating fade\u2026" : "Create fade"
|
|
3154
|
+
}
|
|
3155
|
+
)
|
|
3156
|
+
] })
|
|
3157
|
+
]
|
|
3158
|
+
}
|
|
3159
|
+
) });
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
// src/components/ImportTrackModal.tsx
|
|
3163
|
+
var import_react13 = require("react");
|
|
3164
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
2681
3165
|
function ImportTrackModal({
|
|
2682
3166
|
host,
|
|
2683
3167
|
open,
|
|
@@ -2689,10 +3173,10 @@ function ImportTrackModal({
|
|
|
2689
3173
|
onPick,
|
|
2690
3174
|
onPortTrack
|
|
2691
3175
|
}) {
|
|
2692
|
-
const [load, setLoad] = (0,
|
|
2693
|
-
const [selectedSceneId, setSelectedSceneId] = (0,
|
|
2694
|
-
const [importingTrackId, setImportingTrackId] = (0,
|
|
2695
|
-
const refresh = (0,
|
|
3176
|
+
const [load, setLoad] = (0, import_react13.useState)({ status: "loading" });
|
|
3177
|
+
const [selectedSceneId, setSelectedSceneId] = (0, import_react13.useState)(null);
|
|
3178
|
+
const [importingTrackId, setImportingTrackId] = (0, import_react13.useState)(null);
|
|
3179
|
+
const refresh = (0, import_react13.useCallback)(async () => {
|
|
2696
3180
|
if (!host.listImportableTracks) {
|
|
2697
3181
|
setLoad({ status: "error", message: "This host does not support importing tracks." });
|
|
2698
3182
|
return;
|
|
@@ -2708,14 +3192,14 @@ function ImportTrackModal({
|
|
|
2708
3192
|
setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load scenes." });
|
|
2709
3193
|
}
|
|
2710
3194
|
}, [host, mode, onPortTrack]);
|
|
2711
|
-
(0,
|
|
3195
|
+
(0, import_react13.useEffect)(() => {
|
|
2712
3196
|
if (open) {
|
|
2713
3197
|
setSelectedSceneId(null);
|
|
2714
3198
|
setImportingTrackId(null);
|
|
2715
3199
|
void refresh();
|
|
2716
3200
|
}
|
|
2717
3201
|
}, [open, refresh]);
|
|
2718
|
-
const handleImport = (0,
|
|
3202
|
+
const handleImport = (0, import_react13.useCallback)(
|
|
2719
3203
|
async (track, sourceSceneId, sceneName, isSameScene) => {
|
|
2720
3204
|
if (isSameScene && onPortTrack) {
|
|
2721
3205
|
if (!track.importable) return;
|
|
@@ -2756,16 +3240,16 @@ function ImportTrackModal({
|
|
|
2756
3240
|
if (!open) return null;
|
|
2757
3241
|
const scenes = load.status === "ready" ? load.scenes : [];
|
|
2758
3242
|
const selectedScene = scenes.find((s) => s.sceneId === selectedSceneId) ?? null;
|
|
2759
|
-
return /* @__PURE__ */ (0,
|
|
3243
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
2760
3244
|
"div",
|
|
2761
3245
|
{
|
|
2762
3246
|
className: "w-[420px] max-h-[70vh] overflow-hidden flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl",
|
|
2763
3247
|
onClick: (e) => e.stopPropagation(),
|
|
2764
3248
|
"data-testid": `${testIdPrefix}-modal`,
|
|
2765
3249
|
children: [
|
|
2766
|
-
/* @__PURE__ */ (0,
|
|
2767
|
-
/* @__PURE__ */ (0,
|
|
2768
|
-
selectedScene && /* @__PURE__ */ (0,
|
|
3250
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
|
|
3251
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
3252
|
+
selectedScene && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2769
3253
|
"button",
|
|
2770
3254
|
{
|
|
2771
3255
|
className: "text-sas-muted hover:text-sas-accent text-xs",
|
|
@@ -2774,9 +3258,9 @@ function ImportTrackModal({
|
|
|
2774
3258
|
children: "\u2190"
|
|
2775
3259
|
}
|
|
2776
3260
|
),
|
|
2777
|
-
/* @__PURE__ */ (0,
|
|
3261
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
|
|
2778
3262
|
] }),
|
|
2779
|
-
/* @__PURE__ */ (0,
|
|
3263
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2780
3264
|
"button",
|
|
2781
3265
|
{
|
|
2782
3266
|
className: "text-sas-muted hover:text-sas-accent text-sm",
|
|
@@ -2786,30 +3270,30 @@ function ImportTrackModal({
|
|
|
2786
3270
|
}
|
|
2787
3271
|
)
|
|
2788
3272
|
] }),
|
|
2789
|
-
/* @__PURE__ */ (0,
|
|
2790
|
-
load.status === "loading" && /* @__PURE__ */ (0,
|
|
2791
|
-
load.status === "error" && /* @__PURE__ */ (0,
|
|
2792
|
-
load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ (0,
|
|
2793
|
-
load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ (0,
|
|
3273
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "overflow-y-auto p-2 flex-1", children: [
|
|
3274
|
+
load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
|
|
3275
|
+
load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
|
|
3276
|
+
load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-empty`, children: mode === "sound" ? "No other scenes have a sound to import." : "No other scenes have a compatible track to import." }),
|
|
3277
|
+
load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
2794
3278
|
"button",
|
|
2795
3279
|
{
|
|
2796
3280
|
className: "w-full flex items-center justify-between px-2 py-1.5 rounded-sm border border-sas-border bg-sas-panel-alt text-left text-xs text-sas-text hover:border-sas-accent hover:text-sas-accent transition-colors",
|
|
2797
3281
|
onClick: () => setSelectedSceneId(scene.sceneId),
|
|
2798
3282
|
"data-testid": `${testIdPrefix}-scene`,
|
|
2799
3283
|
children: [
|
|
2800
|
-
/* @__PURE__ */ (0,
|
|
2801
|
-
/* @__PURE__ */ (0,
|
|
3284
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "truncate", children: scene.sceneName }),
|
|
3285
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "text-sas-muted", children: [
|
|
2802
3286
|
scene.tracks.length,
|
|
2803
3287
|
" \u2192"
|
|
2804
3288
|
] })
|
|
2805
3289
|
]
|
|
2806
3290
|
}
|
|
2807
3291
|
) }, scene.sceneId)) }),
|
|
2808
|
-
selectedScene && /* @__PURE__ */ (0,
|
|
3292
|
+
selectedScene && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
|
|
2809
3293
|
const busy = importingTrackId === track.trackId;
|
|
2810
3294
|
const gated = mode === "track" && !track.importable;
|
|
2811
3295
|
const disabled = gated || busy;
|
|
2812
|
-
return /* @__PURE__ */ (0,
|
|
3296
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
2813
3297
|
"button",
|
|
2814
3298
|
{
|
|
2815
3299
|
className: `w-full flex items-center justify-between px-2 py-1.5 rounded-sm border text-left text-xs transition-colors ${disabled ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-text hover:border-sas-accent hover:text-sas-accent"}`,
|
|
@@ -2819,14 +3303,14 @@ function ImportTrackModal({
|
|
|
2819
3303
|
"data-testid": `${testIdPrefix}-track`,
|
|
2820
3304
|
"data-importable": mode === "sound" || track.importable ? "true" : "false",
|
|
2821
3305
|
children: [
|
|
2822
|
-
/* @__PURE__ */ (0,
|
|
3306
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "truncate", children: [
|
|
2823
3307
|
track.name,
|
|
2824
|
-
track.role ? /* @__PURE__ */ (0,
|
|
3308
|
+
track.role ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "text-sas-muted", children: [
|
|
2825
3309
|
" \xB7 ",
|
|
2826
3310
|
track.role
|
|
2827
3311
|
] }) : null
|
|
2828
3312
|
] }),
|
|
2829
|
-
busy ? /* @__PURE__ */ (0,
|
|
3313
|
+
busy ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-sas-muted", children: "\u2298" }) : null
|
|
2830
3314
|
]
|
|
2831
3315
|
}
|
|
2832
3316
|
) }, track.dbId);
|
|
@@ -2838,8 +3322,38 @@ function ImportTrackModal({
|
|
|
2838
3322
|
}
|
|
2839
3323
|
|
|
2840
3324
|
// src/components/CrossfadeModal.tsx
|
|
2841
|
-
var
|
|
2842
|
-
var
|
|
3325
|
+
var import_react14 = require("react");
|
|
3326
|
+
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
3327
|
+
function shortId2(dbId) {
|
|
3328
|
+
return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
|
|
3329
|
+
}
|
|
3330
|
+
function CandidateRow({
|
|
3331
|
+
track,
|
|
3332
|
+
selected,
|
|
3333
|
+
disabled,
|
|
3334
|
+
onSelect,
|
|
3335
|
+
testId
|
|
3336
|
+
}) {
|
|
3337
|
+
const primary = track.prompt?.trim() || track.name;
|
|
3338
|
+
const meta = [track.role, shortId2(track.dbId)].filter(Boolean).join(" \xB7 ");
|
|
3339
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
3340
|
+
"button",
|
|
3341
|
+
{
|
|
3342
|
+
type: "button",
|
|
3343
|
+
role: "radio",
|
|
3344
|
+
"aria-checked": selected,
|
|
3345
|
+
"data-testid": testId,
|
|
3346
|
+
"data-value": track.dbId,
|
|
3347
|
+
onClick: onSelect,
|
|
3348
|
+
disabled,
|
|
3349
|
+
className: `w-full text-left px-2 py-1.5 rounded-sm border transition-colors disabled:opacity-50 ${selected ? "bg-sas-accent/15 border-sas-accent" : "bg-sas-panel border-sas-border hover:border-sas-accent/50"}`,
|
|
3350
|
+
children: [
|
|
3351
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
|
|
3352
|
+
meta && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
|
|
3353
|
+
]
|
|
3354
|
+
}
|
|
3355
|
+
);
|
|
3356
|
+
}
|
|
2843
3357
|
function CrossfadeModal({
|
|
2844
3358
|
host,
|
|
2845
3359
|
open,
|
|
@@ -2852,15 +3366,15 @@ function CrossfadeModal({
|
|
|
2852
3366
|
onCreate,
|
|
2853
3367
|
testIdPrefix = "crossfade-modal"
|
|
2854
3368
|
}) {
|
|
2855
|
-
const [load, setLoad] = (0,
|
|
2856
|
-
const [originDbId, setOriginDbId] = (0,
|
|
2857
|
-
const [targetDbId, setTargetDbId] = (0,
|
|
2858
|
-
const [isCreating, setIsCreating] = (0,
|
|
2859
|
-
const [error, setError] = (0,
|
|
2860
|
-
const [fromName, setFromName] = (0,
|
|
2861
|
-
const [toName, setToName] = (0,
|
|
2862
|
-
const cancelRef = (0,
|
|
2863
|
-
const refresh = (0,
|
|
3369
|
+
const [load, setLoad] = (0, import_react14.useState)({ status: "loading" });
|
|
3370
|
+
const [originDbId, setOriginDbId] = (0, import_react14.useState)("");
|
|
3371
|
+
const [targetDbId, setTargetDbId] = (0, import_react14.useState)("");
|
|
3372
|
+
const [isCreating, setIsCreating] = (0, import_react14.useState)(false);
|
|
3373
|
+
const [error, setError] = (0, import_react14.useState)(null);
|
|
3374
|
+
const [fromName, setFromName] = (0, import_react14.useState)(null);
|
|
3375
|
+
const [toName, setToName] = (0, import_react14.useState)(null);
|
|
3376
|
+
const cancelRef = (0, import_react14.useRef)(null);
|
|
3377
|
+
const refresh = (0, import_react14.useCallback)(async () => {
|
|
2864
3378
|
if (!host.listSceneFamilyTracks) {
|
|
2865
3379
|
setLoad({ status: "error", message: "This host does not support crossfade tracks." });
|
|
2866
3380
|
return;
|
|
@@ -2880,7 +3394,7 @@ function CrossfadeModal({
|
|
|
2880
3394
|
setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
|
|
2881
3395
|
}
|
|
2882
3396
|
}, [host, fromSceneId, toSceneId]);
|
|
2883
|
-
(0,
|
|
3397
|
+
(0, import_react14.useEffect)(() => {
|
|
2884
3398
|
if (open) {
|
|
2885
3399
|
setError(null);
|
|
2886
3400
|
setIsCreating(false);
|
|
@@ -2889,21 +3403,21 @@ function CrossfadeModal({
|
|
|
2889
3403
|
void refresh();
|
|
2890
3404
|
}
|
|
2891
3405
|
}, [open, refresh]);
|
|
2892
|
-
const excludeSet = (0,
|
|
2893
|
-
const originCandidates = (0,
|
|
3406
|
+
const excludeSet = (0, import_react14.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
|
|
3407
|
+
const originCandidates = (0, import_react14.useMemo)(
|
|
2894
3408
|
() => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
2895
3409
|
[load, excludeSet]
|
|
2896
3410
|
);
|
|
2897
|
-
const targetCandidates = (0,
|
|
3411
|
+
const targetCandidates = (0, import_react14.useMemo)(
|
|
2898
3412
|
() => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
2899
3413
|
[load, excludeSet]
|
|
2900
3414
|
);
|
|
2901
|
-
(0,
|
|
3415
|
+
(0, import_react14.useEffect)(() => {
|
|
2902
3416
|
if (!originCandidates.some((t) => t.dbId === originDbId)) {
|
|
2903
3417
|
setOriginDbId(originCandidates[0]?.dbId ?? "");
|
|
2904
3418
|
}
|
|
2905
3419
|
}, [originCandidates, originDbId]);
|
|
2906
|
-
(0,
|
|
3420
|
+
(0, import_react14.useEffect)(() => {
|
|
2907
3421
|
if (!targetCandidates.some((t) => t.dbId === targetDbId)) {
|
|
2908
3422
|
setTargetDbId(targetCandidates[0]?.dbId ?? "");
|
|
2909
3423
|
}
|
|
@@ -2911,10 +3425,10 @@ function CrossfadeModal({
|
|
|
2911
3425
|
const originTrack = originCandidates.find((t) => t.dbId === originDbId) ?? null;
|
|
2912
3426
|
const targetTrack = targetCandidates.find((t) => t.dbId === targetDbId) ?? null;
|
|
2913
3427
|
const canCreate = !isCreating && !!originTrack && !!targetTrack;
|
|
2914
|
-
const handleClose = (0,
|
|
3428
|
+
const handleClose = (0, import_react14.useCallback)(() => {
|
|
2915
3429
|
if (!isCreating) onClose();
|
|
2916
3430
|
}, [isCreating, onClose]);
|
|
2917
|
-
const handleCreate = (0,
|
|
3431
|
+
const handleCreate = (0, import_react14.useCallback)(async () => {
|
|
2918
3432
|
if (!originTrack || !targetTrack) return;
|
|
2919
3433
|
setIsCreating(true);
|
|
2920
3434
|
setError(null);
|
|
@@ -2932,26 +3446,26 @@ function CrossfadeModal({
|
|
|
2932
3446
|
const fromLabel = fromName ?? fromSceneName ?? null;
|
|
2933
3447
|
const toLabel = toName ?? toSceneName ?? null;
|
|
2934
3448
|
if (!open) return null;
|
|
2935
|
-
return /* @__PURE__ */ (0,
|
|
3449
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
2936
3450
|
"div",
|
|
2937
3451
|
{
|
|
2938
3452
|
className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
|
|
2939
3453
|
onClick: (e) => e.stopPropagation(),
|
|
2940
3454
|
"data-testid": `${testIdPrefix}-box`,
|
|
2941
3455
|
children: [
|
|
2942
|
-
/* @__PURE__ */ (0,
|
|
2943
|
-
/* @__PURE__ */ (0,
|
|
3456
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
|
|
3457
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
|
|
2944
3458
|
"Bridge a track from",
|
|
2945
3459
|
" ",
|
|
2946
|
-
/* @__PURE__ */ (0,
|
|
3460
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
|
|
2947
3461
|
" into one from",
|
|
2948
3462
|
" ",
|
|
2949
|
-
/* @__PURE__ */ (0,
|
|
3463
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
|
|
2950
3464
|
". Both layers share one generated part; each keeps its own preset."
|
|
2951
3465
|
] }),
|
|
2952
|
-
load.status === "loading" && /* @__PURE__ */ (0,
|
|
2953
|
-
load.status === "error" && /* @__PURE__ */ (0,
|
|
2954
|
-
load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ (0,
|
|
3466
|
+
load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
|
|
3467
|
+
load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
|
|
3468
|
+
load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
2955
3469
|
"div",
|
|
2956
3470
|
{
|
|
2957
3471
|
className: "text-xs text-sas-muted py-4 text-center",
|
|
@@ -2962,55 +3476,67 @@ function CrossfadeModal({
|
|
|
2962
3476
|
". Add one (or free one from another crossfade) first."
|
|
2963
3477
|
]
|
|
2964
3478
|
}
|
|
2965
|
-
) : /* @__PURE__ */ (0,
|
|
2966
|
-
/* @__PURE__ */ (0,
|
|
2967
|
-
/* @__PURE__ */ (0,
|
|
3479
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
|
|
3480
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "block", children: [
|
|
3481
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
|
|
2968
3482
|
"Origin ",
|
|
2969
3483
|
fromLabel ? `(${fromLabel})` : "(top)"
|
|
2970
3484
|
] }),
|
|
2971
|
-
/* @__PURE__ */ (0,
|
|
2972
|
-
"
|
|
3485
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3486
|
+
"div",
|
|
2973
3487
|
{
|
|
2974
|
-
"
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3488
|
+
role: "radiogroup",
|
|
3489
|
+
"aria-label": "Origin track",
|
|
3490
|
+
"data-testid": `${testIdPrefix}-origin-list`,
|
|
3491
|
+
className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
|
|
3492
|
+
children: originCandidates.map((t) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3493
|
+
CandidateRow,
|
|
3494
|
+
{
|
|
3495
|
+
track: t,
|
|
3496
|
+
selected: t.dbId === originDbId,
|
|
3497
|
+
disabled: isCreating,
|
|
3498
|
+
onSelect: () => setOriginDbId(t.dbId),
|
|
3499
|
+
testId: `${testIdPrefix}-origin-option-${t.dbId}`
|
|
3500
|
+
},
|
|
3501
|
+
t.dbId
|
|
3502
|
+
))
|
|
2983
3503
|
}
|
|
2984
3504
|
)
|
|
2985
3505
|
] }),
|
|
2986
|
-
/* @__PURE__ */ (0,
|
|
2987
|
-
/* @__PURE__ */ (0,
|
|
3506
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "block", children: [
|
|
3507
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
|
|
2988
3508
|
"Target ",
|
|
2989
3509
|
toLabel ? `(${toLabel})` : "(bottom)"
|
|
2990
3510
|
] }),
|
|
2991
|
-
targetCandidates.length === 0 ? /* @__PURE__ */ (0,
|
|
3511
|
+
targetCandidates.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
|
|
2992
3512
|
"No available tracks in ",
|
|
2993
3513
|
toLabel ?? "the target scene",
|
|
2994
3514
|
" to crossfade into."
|
|
2995
|
-
] }) : /* @__PURE__ */ (0,
|
|
2996
|
-
"
|
|
3515
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3516
|
+
"div",
|
|
2997
3517
|
{
|
|
2998
|
-
"
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3518
|
+
role: "radiogroup",
|
|
3519
|
+
"aria-label": "Target track",
|
|
3520
|
+
"data-testid": `${testIdPrefix}-target-list`,
|
|
3521
|
+
className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
|
|
3522
|
+
children: targetCandidates.map((t) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3523
|
+
CandidateRow,
|
|
3524
|
+
{
|
|
3525
|
+
track: t,
|
|
3526
|
+
selected: t.dbId === targetDbId,
|
|
3527
|
+
disabled: isCreating,
|
|
3528
|
+
onSelect: () => setTargetDbId(t.dbId),
|
|
3529
|
+
testId: `${testIdPrefix}-target-option-${t.dbId}`
|
|
3530
|
+
},
|
|
3531
|
+
t.dbId
|
|
3532
|
+
))
|
|
3007
3533
|
}
|
|
3008
3534
|
)
|
|
3009
3535
|
] })
|
|
3010
3536
|
] })),
|
|
3011
|
-
error && /* @__PURE__ */ (0,
|
|
3012
|
-
/* @__PURE__ */ (0,
|
|
3013
|
-
/* @__PURE__ */ (0,
|
|
3537
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
|
|
3538
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex justify-end gap-2 pt-1", children: [
|
|
3539
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3014
3540
|
"button",
|
|
3015
3541
|
{
|
|
3016
3542
|
ref: cancelRef,
|
|
@@ -3021,7 +3547,7 @@ function CrossfadeModal({
|
|
|
3021
3547
|
children: "Cancel"
|
|
3022
3548
|
}
|
|
3023
3549
|
),
|
|
3024
|
-
/* @__PURE__ */ (0,
|
|
3550
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3025
3551
|
"button",
|
|
3026
3552
|
{
|
|
3027
3553
|
"data-testid": `${testIdPrefix}-confirm`,
|
|
@@ -3037,9 +3563,701 @@ function CrossfadeModal({
|
|
|
3037
3563
|
) });
|
|
3038
3564
|
}
|
|
3039
3565
|
|
|
3566
|
+
// src/components/TransitionDesigner.tsx
|
|
3567
|
+
var import_react16 = require("react");
|
|
3568
|
+
|
|
3569
|
+
// src/hooks/useTrackReorder.ts
|
|
3570
|
+
var import_react15 = require("react");
|
|
3571
|
+
function moveItem(arr, from, to) {
|
|
3572
|
+
const next = arr.slice();
|
|
3573
|
+
if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
|
|
3574
|
+
return next;
|
|
3575
|
+
}
|
|
3576
|
+
const [moved] = next.splice(from, 1);
|
|
3577
|
+
next.splice(to, 0, moved);
|
|
3578
|
+
return next;
|
|
3579
|
+
}
|
|
3580
|
+
function useTrackReorder({
|
|
3581
|
+
host,
|
|
3582
|
+
items,
|
|
3583
|
+
setItems,
|
|
3584
|
+
getId,
|
|
3585
|
+
onError
|
|
3586
|
+
}) {
|
|
3587
|
+
const [draggingIndex, setDraggingIndex] = (0, import_react15.useState)(null);
|
|
3588
|
+
const [dragOverIndex, setDragOverIndex] = (0, import_react15.useState)(null);
|
|
3589
|
+
const fromRef = (0, import_react15.useRef)(null);
|
|
3590
|
+
const itemsRef = (0, import_react15.useRef)(items);
|
|
3591
|
+
itemsRef.current = items;
|
|
3592
|
+
const dragPropsFor = (0, import_react15.useCallback)(
|
|
3593
|
+
(index) => ({
|
|
3594
|
+
handleProps: {
|
|
3595
|
+
draggable: true,
|
|
3596
|
+
onDragStart: (e) => {
|
|
3597
|
+
fromRef.current = index;
|
|
3598
|
+
setDraggingIndex(index);
|
|
3599
|
+
if (e.dataTransfer) {
|
|
3600
|
+
e.dataTransfer.effectAllowed = "move";
|
|
3601
|
+
try {
|
|
3602
|
+
e.dataTransfer.setData("text/plain", String(index));
|
|
3603
|
+
} catch {
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
},
|
|
3607
|
+
onDragEnd: () => {
|
|
3608
|
+
fromRef.current = null;
|
|
3609
|
+
setDraggingIndex(null);
|
|
3610
|
+
setDragOverIndex(null);
|
|
3611
|
+
}
|
|
3612
|
+
},
|
|
3613
|
+
rowProps: {
|
|
3614
|
+
onDragEnter: (e) => {
|
|
3615
|
+
if (fromRef.current === null) return;
|
|
3616
|
+
e.preventDefault();
|
|
3617
|
+
setDragOverIndex(index);
|
|
3618
|
+
},
|
|
3619
|
+
onDragOver: (e) => {
|
|
3620
|
+
if (fromRef.current === null) return;
|
|
3621
|
+
e.preventDefault();
|
|
3622
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
3623
|
+
setDragOverIndex((cur) => cur === index ? cur : index);
|
|
3624
|
+
},
|
|
3625
|
+
onDragLeave: () => {
|
|
3626
|
+
setDragOverIndex((cur) => cur === index ? null : cur);
|
|
3627
|
+
},
|
|
3628
|
+
onDrop: (e) => {
|
|
3629
|
+
e.preventDefault();
|
|
3630
|
+
const from = fromRef.current;
|
|
3631
|
+
fromRef.current = null;
|
|
3632
|
+
setDraggingIndex(null);
|
|
3633
|
+
setDragOverIndex(null);
|
|
3634
|
+
if (from === null || from === index) return;
|
|
3635
|
+
const prev = itemsRef.current;
|
|
3636
|
+
const next = moveItem(prev, from, index);
|
|
3637
|
+
setItems(next);
|
|
3638
|
+
const ids = next.map(getId);
|
|
3639
|
+
Promise.resolve(host.reorderTracks(ids)).catch((err) => {
|
|
3640
|
+
setItems(prev);
|
|
3641
|
+
onError?.(err);
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
},
|
|
3645
|
+
isDragging: draggingIndex === index,
|
|
3646
|
+
isDragTarget: dragOverIndex === index && draggingIndex !== index
|
|
3647
|
+
}),
|
|
3648
|
+
[host, setItems, getId, onError, draggingIndex, dragOverIndex]
|
|
3649
|
+
);
|
|
3650
|
+
return { dragPropsFor, draggingIndex, dragOverIndex };
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
// src/transition-designer-meta.ts
|
|
3654
|
+
var TRANSITION_DESIGNER_DRAFT_KEY = "transitionDesigner:draft";
|
|
3655
|
+
var AUDIO_EFFECTS = ["fade", "stutter", "chopped", "delay"];
|
|
3656
|
+
var AUDIO_EFFECT_LABEL = {
|
|
3657
|
+
fade: "Fade",
|
|
3658
|
+
stutter: "Stutter",
|
|
3659
|
+
chopped: "Chopped",
|
|
3660
|
+
delay: "Delay"
|
|
3661
|
+
};
|
|
3662
|
+
function asAudioEffect(v) {
|
|
3663
|
+
return v === "fade" || v === "stutter" || v === "chopped" || v === "delay" ? v : null;
|
|
3664
|
+
}
|
|
3665
|
+
function rowType(hasOrigin, hasTarget) {
|
|
3666
|
+
if (hasOrigin && hasTarget) return "crossfade";
|
|
3667
|
+
if (hasOrigin) return "fade-out";
|
|
3668
|
+
if (hasTarget) return "fade-in";
|
|
3669
|
+
return null;
|
|
3670
|
+
}
|
|
3671
|
+
function asTransitionDesignerDraft(val) {
|
|
3672
|
+
if (!val || typeof val !== "object") return null;
|
|
3673
|
+
const d = val;
|
|
3674
|
+
const clean = (a) => Array.isArray(a) ? a.filter((x) => x === null || typeof x === "string") : [];
|
|
3675
|
+
const cleanEffects = (e) => {
|
|
3676
|
+
const out = {};
|
|
3677
|
+
if (e && typeof e === "object") {
|
|
3678
|
+
for (const [k, v] of Object.entries(e)) {
|
|
3679
|
+
const eff = asAudioEffect(v);
|
|
3680
|
+
if (eff) out[k] = eff;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
return out;
|
|
3684
|
+
};
|
|
3685
|
+
return {
|
|
3686
|
+
originOrder: clean(d.originOrder),
|
|
3687
|
+
targetOrder: clean(d.targetOrder),
|
|
3688
|
+
rowEffects: cleanEffects(d.rowEffects)
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
function reconcileSlots(saved, poolIds) {
|
|
3692
|
+
const pool = new Set(poolIds);
|
|
3693
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3694
|
+
const out = [];
|
|
3695
|
+
for (const slot of saved ?? []) {
|
|
3696
|
+
if (slot === null) {
|
|
3697
|
+
out.push(null);
|
|
3698
|
+
continue;
|
|
3699
|
+
}
|
|
3700
|
+
if (pool.has(slot) && !seen.has(slot)) {
|
|
3701
|
+
out.push(slot);
|
|
3702
|
+
seen.add(slot);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
for (const id of poolIds) {
|
|
3706
|
+
if (!seen.has(id)) {
|
|
3707
|
+
out.push(id);
|
|
3708
|
+
seen.add(id);
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
return out;
|
|
3712
|
+
}
|
|
3713
|
+
function buildRowSlots(originSlots, targetSlots) {
|
|
3714
|
+
const n = Math.max(originSlots.length, targetSlots.length);
|
|
3715
|
+
const rows = [];
|
|
3716
|
+
for (let i = 0; i < n; i++) {
|
|
3717
|
+
const originId = originSlots[i] ?? null;
|
|
3718
|
+
const targetId = targetSlots[i] ?? null;
|
|
3719
|
+
rows.push({ originId, targetId, type: rowType(originId !== null, targetId !== null) });
|
|
3720
|
+
}
|
|
3721
|
+
return rows;
|
|
3722
|
+
}
|
|
3723
|
+
function normalizeSlots(originSlots, targetSlots) {
|
|
3724
|
+
const rows = buildRowSlots(originSlots, targetSlots).filter(
|
|
3725
|
+
(r) => r.originId !== null || r.targetId !== null
|
|
3726
|
+
);
|
|
3727
|
+
const trimTrailing = (a) => {
|
|
3728
|
+
let end = a.length;
|
|
3729
|
+
while (end > 0 && a[end - 1] === null) end--;
|
|
3730
|
+
return a.slice(0, end);
|
|
3731
|
+
};
|
|
3732
|
+
return {
|
|
3733
|
+
originOrder: trimTrailing(rows.map((r) => r.originId)),
|
|
3734
|
+
targetOrder: trimTrailing(rows.map((r) => r.targetId))
|
|
3735
|
+
};
|
|
3736
|
+
}
|
|
3737
|
+
function padSlots(slots, n) {
|
|
3738
|
+
if (slots.length >= n) return slots.slice();
|
|
3739
|
+
return [...slots, ...new Array(n - slots.length).fill(null)];
|
|
3740
|
+
}
|
|
3741
|
+
function padPair(originSlots, targetSlots) {
|
|
3742
|
+
const n = Math.max(originSlots.length, targetSlots.length);
|
|
3743
|
+
return [padSlots(originSlots, n), padSlots(targetSlots, n)];
|
|
3744
|
+
}
|
|
3745
|
+
function slotsEqual(a, b) {
|
|
3746
|
+
if (a.length !== b.length) return false;
|
|
3747
|
+
for (let i = 0; i < a.length; i++) {
|
|
3748
|
+
if (a[i] !== b[i]) return false;
|
|
3749
|
+
}
|
|
3750
|
+
return true;
|
|
3751
|
+
}
|
|
3752
|
+
function rowKey(row) {
|
|
3753
|
+
if (row.type === "crossfade") return `xf:${row.originId}|${row.targetId}`;
|
|
3754
|
+
if (row.type === "fade-out") return `fo:${row.originId}`;
|
|
3755
|
+
if (row.type === "fade-in") return `fi:${row.targetId}`;
|
|
3756
|
+
return null;
|
|
3757
|
+
}
|
|
3758
|
+
function dbIdsFromKeys(keys) {
|
|
3759
|
+
const out = /* @__PURE__ */ new Set();
|
|
3760
|
+
for (const k of keys) {
|
|
3761
|
+
const body = k.slice(3);
|
|
3762
|
+
if (k.startsWith("xf:")) {
|
|
3763
|
+
const sep = body.indexOf("|");
|
|
3764
|
+
out.add(body.slice(0, sep));
|
|
3765
|
+
out.add(body.slice(sep + 1));
|
|
3766
|
+
} else {
|
|
3767
|
+
out.add(body);
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
return out;
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
// src/components/TransitionDesigner.tsx
|
|
3774
|
+
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
3775
|
+
var CROSSFADE_ESTIMATE_MS = 15e3;
|
|
3776
|
+
var FADE_ESTIMATE_MS = 11e3;
|
|
3777
|
+
var CREATE_ALL_CONCURRENCY = 5;
|
|
3778
|
+
var TYPE_LABEL = {
|
|
3779
|
+
crossfade: "Crossfade",
|
|
3780
|
+
"fade-out": "Fade out",
|
|
3781
|
+
"fade-in": "Fade in"
|
|
3782
|
+
};
|
|
3783
|
+
function shortId3(dbId) {
|
|
3784
|
+
return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
|
|
3785
|
+
}
|
|
3786
|
+
function TransitionDesigner({
|
|
3787
|
+
host,
|
|
3788
|
+
fromSceneId,
|
|
3789
|
+
toSceneId,
|
|
3790
|
+
transitionSceneId,
|
|
3791
|
+
excludeSourceDbIds,
|
|
3792
|
+
onCreateCrossfade,
|
|
3793
|
+
onCreateFade,
|
|
3794
|
+
onCreateAudioTransition,
|
|
3795
|
+
familyLabel,
|
|
3796
|
+
testIdPrefix = "transition-designer"
|
|
3797
|
+
}) {
|
|
3798
|
+
const [load, setLoad] = (0, import_react16.useState)({ status: "loading" });
|
|
3799
|
+
const [fromName, setFromName] = (0, import_react16.useState)(null);
|
|
3800
|
+
const [toName, setToName] = (0, import_react16.useState)(null);
|
|
3801
|
+
const [originSlots, setOriginSlots] = (0, import_react16.useState)([]);
|
|
3802
|
+
const [targetSlots, setTargetSlots] = (0, import_react16.useState)([]);
|
|
3803
|
+
const [creatingKeys, setCreatingKeys] = (0, import_react16.useState)(() => /* @__PURE__ */ new Set());
|
|
3804
|
+
const [rowErrors, setRowErrors] = (0, import_react16.useState)({});
|
|
3805
|
+
const [rowEffects, setRowEffects] = (0, import_react16.useState)({});
|
|
3806
|
+
const rowEffectsRef = (0, import_react16.useRef)(rowEffects);
|
|
3807
|
+
rowEffectsRef.current = rowEffects;
|
|
3808
|
+
const audioEffectsEnabled = !!onCreateAudioTransition;
|
|
3809
|
+
const excludeRef = (0, import_react16.useRef)(excludeSourceDbIds);
|
|
3810
|
+
excludeRef.current = excludeSourceDbIds;
|
|
3811
|
+
const originSlotsRef = (0, import_react16.useRef)(originSlots);
|
|
3812
|
+
originSlotsRef.current = originSlots;
|
|
3813
|
+
const targetSlotsRef = (0, import_react16.useRef)(targetSlots);
|
|
3814
|
+
targetSlotsRef.current = targetSlots;
|
|
3815
|
+
const creatingKeysRef = (0, import_react16.useRef)(creatingKeys);
|
|
3816
|
+
creatingKeysRef.current = creatingKeys;
|
|
3817
|
+
const dragRef = (0, import_react16.useRef)(null);
|
|
3818
|
+
const [dragging, setDragging] = (0, import_react16.useState)(null);
|
|
3819
|
+
const [dragOver, setDragOver] = (0, import_react16.useState)(null);
|
|
3820
|
+
const excludeSet = (0, import_react16.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
|
|
3821
|
+
const originPool = (0, import_react16.useMemo)(
|
|
3822
|
+
() => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
3823
|
+
[load, excludeSet]
|
|
3824
|
+
);
|
|
3825
|
+
const targetPool = (0, import_react16.useMemo)(
|
|
3826
|
+
() => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
3827
|
+
[load, excludeSet]
|
|
3828
|
+
);
|
|
3829
|
+
const originById = (0, import_react16.useMemo)(() => new Map(originPool.map((t) => [t.dbId, t])), [originPool]);
|
|
3830
|
+
const targetById = (0, import_react16.useMemo)(() => new Map(targetPool.map((t) => [t.dbId, t])), [targetPool]);
|
|
3831
|
+
const originByIdRef = (0, import_react16.useRef)(originById);
|
|
3832
|
+
originByIdRef.current = originById;
|
|
3833
|
+
const targetByIdRef = (0, import_react16.useRef)(targetById);
|
|
3834
|
+
targetByIdRef.current = targetById;
|
|
3835
|
+
const refresh = (0, import_react16.useCallback)(async () => {
|
|
3836
|
+
if (!host.listSceneFamilyTracks) {
|
|
3837
|
+
setLoad({ status: "error", message: "This host does not support transition tracks." });
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
setLoad({ status: "loading" });
|
|
3841
|
+
try {
|
|
3842
|
+
const [origin, target, fName, tName, draftRaw] = await Promise.all([
|
|
3843
|
+
host.listSceneFamilyTracks(fromSceneId),
|
|
3844
|
+
host.listSceneFamilyTracks(toSceneId),
|
|
3845
|
+
host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),
|
|
3846
|
+
host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null),
|
|
3847
|
+
host.getSceneData ? host.getSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY) : Promise.resolve(null)
|
|
3848
|
+
]);
|
|
3849
|
+
const draft = asTransitionDesignerDraft(draftRaw);
|
|
3850
|
+
const exSet = new Set(excludeRef.current ?? []);
|
|
3851
|
+
const originIds = origin.filter((t) => !exSet.has(t.dbId)).map((t) => t.dbId);
|
|
3852
|
+
const targetIds = target.filter((t) => !exSet.has(t.dbId)).map((t) => t.dbId);
|
|
3853
|
+
const [po, pt] = padPair(
|
|
3854
|
+
reconcileSlots(draft?.originOrder, originIds),
|
|
3855
|
+
reconcileSlots(draft?.targetOrder, targetIds)
|
|
3856
|
+
);
|
|
3857
|
+
setOriginSlots(po);
|
|
3858
|
+
setTargetSlots(pt);
|
|
3859
|
+
setRowEffects(draft?.rowEffects ?? {});
|
|
3860
|
+
setFromName(fName);
|
|
3861
|
+
setToName(tName);
|
|
3862
|
+
setLoad({ status: "ready", origin, target });
|
|
3863
|
+
} catch (err) {
|
|
3864
|
+
setLoad({
|
|
3865
|
+
status: "error",
|
|
3866
|
+
message: err instanceof Error ? err.message : "Failed to load tracks."
|
|
3867
|
+
});
|
|
3868
|
+
}
|
|
3869
|
+
}, [host, fromSceneId, toSceneId, transitionSceneId]);
|
|
3870
|
+
(0, import_react16.useEffect)(() => {
|
|
3871
|
+
void refresh();
|
|
3872
|
+
}, [refresh]);
|
|
3873
|
+
(0, import_react16.useEffect)(() => {
|
|
3874
|
+
if (load.status !== "ready") return;
|
|
3875
|
+
const [po, pt] = padPair(
|
|
3876
|
+
reconcileSlots(originSlotsRef.current, originPool.map((t) => t.dbId)),
|
|
3877
|
+
reconcileSlots(targetSlotsRef.current, targetPool.map((t) => t.dbId))
|
|
3878
|
+
);
|
|
3879
|
+
if (!slotsEqual(po, originSlotsRef.current)) setOriginSlots(po);
|
|
3880
|
+
if (!slotsEqual(pt, targetSlotsRef.current)) setTargetSlots(pt);
|
|
3881
|
+
}, [originPool, targetPool, load.status]);
|
|
3882
|
+
const mutate = (0, import_react16.useCallback)(
|
|
3883
|
+
(nextOrigin, nextTarget) => {
|
|
3884
|
+
const norm = normalizeSlots(nextOrigin, nextTarget);
|
|
3885
|
+
const [po, pt] = padPair(norm.originOrder, norm.targetOrder);
|
|
3886
|
+
setOriginSlots(po);
|
|
3887
|
+
setTargetSlots(pt);
|
|
3888
|
+
if (host.setSceneData) {
|
|
3889
|
+
host.setSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY, { ...norm, rowEffects: rowEffectsRef.current }).catch(() => {
|
|
3890
|
+
});
|
|
3891
|
+
}
|
|
3892
|
+
},
|
|
3893
|
+
[host, transitionSceneId]
|
|
3894
|
+
);
|
|
3895
|
+
const setRowEffect = (0, import_react16.useCallback)(
|
|
3896
|
+
(sourceDbId, effect) => {
|
|
3897
|
+
setRowEffects((prev) => {
|
|
3898
|
+
const next = { ...prev, [sourceDbId]: effect };
|
|
3899
|
+
if (host.setSceneData) {
|
|
3900
|
+
const norm = normalizeSlots(originSlotsRef.current, targetSlotsRef.current);
|
|
3901
|
+
host.setSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY, { ...norm, rowEffects: next }).catch(() => {
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3904
|
+
return next;
|
|
3905
|
+
});
|
|
3906
|
+
},
|
|
3907
|
+
[host, transitionSceneId]
|
|
3908
|
+
);
|
|
3909
|
+
const insertGapAbove = (0, import_react16.useCallback)(
|
|
3910
|
+
(col, index) => {
|
|
3911
|
+
const slots = col === "origin" ? originSlots : targetSlots;
|
|
3912
|
+
const next = [...slots.slice(0, index), null, ...slots.slice(index)];
|
|
3913
|
+
if (col === "origin") mutate(next, targetSlots);
|
|
3914
|
+
else mutate(originSlots, next);
|
|
3915
|
+
},
|
|
3916
|
+
[originSlots, targetSlots, mutate]
|
|
3917
|
+
);
|
|
3918
|
+
const removeGap = (0, import_react16.useCallback)(
|
|
3919
|
+
(col, index) => {
|
|
3920
|
+
const slots = col === "origin" ? originSlots : targetSlots;
|
|
3921
|
+
const next = slots.filter((_, i) => i !== index);
|
|
3922
|
+
if (col === "origin") mutate(next, targetSlots);
|
|
3923
|
+
else mutate(originSlots, next);
|
|
3924
|
+
},
|
|
3925
|
+
[originSlots, targetSlots, mutate]
|
|
3926
|
+
);
|
|
3927
|
+
const handleDrop = (0, import_react16.useCallback)(
|
|
3928
|
+
(col, to) => {
|
|
3929
|
+
const from = dragRef.current;
|
|
3930
|
+
dragRef.current = null;
|
|
3931
|
+
setDragging(null);
|
|
3932
|
+
setDragOver(null);
|
|
3933
|
+
if (!from || from.col !== col || from.index === to) return;
|
|
3934
|
+
if (col === "origin") mutate(moveItem(originSlots, from.index, to), targetSlots);
|
|
3935
|
+
else mutate(originSlots, moveItem(targetSlots, from.index, to));
|
|
3936
|
+
},
|
|
3937
|
+
[originSlots, targetSlots, mutate]
|
|
3938
|
+
);
|
|
3939
|
+
const rows = (0, import_react16.useMemo)(() => buildRowSlots(originSlots, targetSlots), [originSlots, targetSlots]);
|
|
3940
|
+
const creatingDbIds = (0, import_react16.useMemo)(() => dbIdsFromKeys(creatingKeys), [creatingKeys]);
|
|
3941
|
+
const eligibleCount = (0, import_react16.useMemo)(
|
|
3942
|
+
() => rows.filter((r) => {
|
|
3943
|
+
const k = rowKey(r);
|
|
3944
|
+
return k !== null && !creatingKeys.has(k);
|
|
3945
|
+
}).length,
|
|
3946
|
+
[rows, creatingKeys]
|
|
3947
|
+
);
|
|
3948
|
+
const createRow = (0, import_react16.useCallback)(
|
|
3949
|
+
async (row) => {
|
|
3950
|
+
const key = rowKey(row);
|
|
3951
|
+
if (!key || !row.type || creatingKeysRef.current.has(key)) return;
|
|
3952
|
+
setCreatingKeys((prev) => new Set(prev).add(key));
|
|
3953
|
+
setRowErrors((prev) => {
|
|
3954
|
+
if (!(key in prev)) return prev;
|
|
3955
|
+
const next = { ...prev };
|
|
3956
|
+
delete next[key];
|
|
3957
|
+
return next;
|
|
3958
|
+
});
|
|
3959
|
+
try {
|
|
3960
|
+
if (row.type === "crossfade") {
|
|
3961
|
+
const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
|
|
3962
|
+
const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
|
|
3963
|
+
if (!o || !t) throw new Error("Track is no longer available \u2014 refresh and retry.");
|
|
3964
|
+
await onCreateCrossfade(
|
|
3965
|
+
{ dbId: o.dbId, name: o.name, role: o.role },
|
|
3966
|
+
{ dbId: t.dbId, name: t.name, role: t.role }
|
|
3967
|
+
);
|
|
3968
|
+
} else if (row.type === "fade-out") {
|
|
3969
|
+
const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
|
|
3970
|
+
if (!o) throw new Error("Track is no longer available \u2014 refresh and retry.");
|
|
3971
|
+
const eff = rowEffectsRef.current[o.dbId] ?? "fade";
|
|
3972
|
+
if (eff !== "fade" && onCreateAudioTransition) {
|
|
3973
|
+
await onCreateAudioTransition({ dbId: o.dbId, name: o.name, role: o.role }, "out", eff);
|
|
3974
|
+
} else {
|
|
3975
|
+
await onCreateFade({ dbId: o.dbId, name: o.name, role: o.role }, "out", defaultFadeGesture(o.role));
|
|
3976
|
+
}
|
|
3977
|
+
} else {
|
|
3978
|
+
const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
|
|
3979
|
+
if (!t) throw new Error("Track is no longer available \u2014 refresh and retry.");
|
|
3980
|
+
const eff = rowEffectsRef.current[t.dbId] ?? "fade";
|
|
3981
|
+
if (eff !== "fade" && onCreateAudioTransition) {
|
|
3982
|
+
await onCreateAudioTransition({ dbId: t.dbId, name: t.name, role: t.role }, "in", eff);
|
|
3983
|
+
} else {
|
|
3984
|
+
await onCreateFade({ dbId: t.dbId, name: t.name, role: t.role }, "in", defaultFadeGesture(t.role));
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
} catch (err) {
|
|
3988
|
+
setRowErrors((prev) => ({
|
|
3989
|
+
...prev,
|
|
3990
|
+
[key]: err instanceof Error ? err.message : "Failed to create transition."
|
|
3991
|
+
}));
|
|
3992
|
+
} finally {
|
|
3993
|
+
setCreatingKeys((prev) => {
|
|
3994
|
+
const next = new Set(prev);
|
|
3995
|
+
next.delete(key);
|
|
3996
|
+
return next;
|
|
3997
|
+
});
|
|
3998
|
+
}
|
|
3999
|
+
},
|
|
4000
|
+
[onCreateCrossfade, onCreateFade, onCreateAudioTransition]
|
|
4001
|
+
);
|
|
4002
|
+
const createAll = (0, import_react16.useCallback)(async () => {
|
|
4003
|
+
const eligible = buildRowSlots(originSlotsRef.current, targetSlotsRef.current).filter((r) => {
|
|
4004
|
+
const k = rowKey(r);
|
|
4005
|
+
return k !== null && !creatingKeysRef.current.has(k);
|
|
4006
|
+
});
|
|
4007
|
+
if (eligible.length === 0) return;
|
|
4008
|
+
let cursor = 0;
|
|
4009
|
+
const worker = async () => {
|
|
4010
|
+
while (cursor < eligible.length) {
|
|
4011
|
+
const row = eligible[cursor];
|
|
4012
|
+
cursor += 1;
|
|
4013
|
+
await createRow(row);
|
|
4014
|
+
}
|
|
4015
|
+
};
|
|
4016
|
+
await Promise.all(
|
|
4017
|
+
Array.from({ length: Math.min(CREATE_ALL_CONCURRENCY, eligible.length) }, () => worker())
|
|
4018
|
+
);
|
|
4019
|
+
}, [createRow]);
|
|
4020
|
+
const fromLabel = fromName ?? "origin";
|
|
4021
|
+
const toLabel = toName ?? "target";
|
|
4022
|
+
const cellDragProps = (col, index, locked) => ({
|
|
4023
|
+
draggable: !locked,
|
|
4024
|
+
onDragStart: (e) => {
|
|
4025
|
+
if (locked) return;
|
|
4026
|
+
dragRef.current = { col, index };
|
|
4027
|
+
setDragging({ col, index });
|
|
4028
|
+
if (e.dataTransfer) {
|
|
4029
|
+
e.dataTransfer.effectAllowed = "move";
|
|
4030
|
+
try {
|
|
4031
|
+
e.dataTransfer.setData("text/plain", String(index));
|
|
4032
|
+
} catch {
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
},
|
|
4036
|
+
onDragEnd: () => {
|
|
4037
|
+
dragRef.current = null;
|
|
4038
|
+
setDragging(null);
|
|
4039
|
+
setDragOver(null);
|
|
4040
|
+
},
|
|
4041
|
+
onDragEnter: (e) => {
|
|
4042
|
+
const d = dragRef.current;
|
|
4043
|
+
if (!d || d.col !== col) return;
|
|
4044
|
+
e.preventDefault();
|
|
4045
|
+
setDragOver({ col, index });
|
|
4046
|
+
},
|
|
4047
|
+
onDragOver: (e) => {
|
|
4048
|
+
const d = dragRef.current;
|
|
4049
|
+
if (!d || d.col !== col) return;
|
|
4050
|
+
e.preventDefault();
|
|
4051
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
4052
|
+
},
|
|
4053
|
+
onDragLeave: () => {
|
|
4054
|
+
setDragOver((cur) => cur && cur.col === col && cur.index === index ? null : cur);
|
|
4055
|
+
},
|
|
4056
|
+
onDrop: (e) => {
|
|
4057
|
+
e.preventDefault();
|
|
4058
|
+
handleDrop(col, index);
|
|
4059
|
+
}
|
|
4060
|
+
});
|
|
4061
|
+
const renderCell = (col, index, slotId) => {
|
|
4062
|
+
const byId = col === "origin" ? originById : targetById;
|
|
4063
|
+
const track = slotId ? byId.get(slotId) : void 0;
|
|
4064
|
+
const locked = slotId !== null && creatingDbIds.has(slotId);
|
|
4065
|
+
const isDragging = dragging?.col === col && dragging.index === index;
|
|
4066
|
+
const isDragTarget = dragOver?.col === col && dragOver.index === index && !isDragging;
|
|
4067
|
+
const base = "group relative rounded-sm border px-2 py-1.5 text-left transition-colors select-none";
|
|
4068
|
+
const tone = isDragTarget ? "border-sas-accent bg-sas-accent/10" : "border-sas-border bg-sas-panel";
|
|
4069
|
+
if (slotId === null) {
|
|
4070
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
|
|
4071
|
+
"div",
|
|
4072
|
+
{
|
|
4073
|
+
...cellDragProps(col, index, false),
|
|
4074
|
+
"data-testid": `${testIdPrefix}-${col}-gap-${index}`,
|
|
4075
|
+
className: `${base} ${tone} border-dashed flex items-center justify-between ${isDragging ? "opacity-40" : "opacity-70"}`,
|
|
4076
|
+
children: [
|
|
4077
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
|
|
4078
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4079
|
+
"button",
|
|
4080
|
+
{
|
|
4081
|
+
type: "button",
|
|
4082
|
+
"data-testid": `${testIdPrefix}-${col}-remove-gap-${index}`,
|
|
4083
|
+
onClick: () => removeGap(col, index),
|
|
4084
|
+
title: "Remove gap",
|
|
4085
|
+
className: "text-[10px] text-sas-muted hover:text-sas-danger",
|
|
4086
|
+
children: "\u2715"
|
|
4087
|
+
}
|
|
4088
|
+
)
|
|
4089
|
+
]
|
|
4090
|
+
}
|
|
4091
|
+
);
|
|
4092
|
+
}
|
|
4093
|
+
const primary = track ? track.prompt?.trim() || track.name : slotId;
|
|
4094
|
+
const meta = track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing";
|
|
4095
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4096
|
+
"div",
|
|
4097
|
+
{
|
|
4098
|
+
...cellDragProps(col, index, locked),
|
|
4099
|
+
"data-testid": `${testIdPrefix}-${col}-cell-${slotId}`,
|
|
4100
|
+
"data-value": slotId,
|
|
4101
|
+
className: `${base} ${tone} ${isDragging ? "opacity-40" : ""} ${locked ? "opacity-60" : "cursor-grab active:cursor-grabbing"}`,
|
|
4102
|
+
title: track ? track.dbId : "Track no longer available",
|
|
4103
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-start gap-1", children: [
|
|
4104
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
|
|
4105
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
4106
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-text truncate", children: primary }),
|
|
4107
|
+
meta && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
|
|
4108
|
+
] }),
|
|
4109
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4110
|
+
"button",
|
|
4111
|
+
{
|
|
4112
|
+
type: "button",
|
|
4113
|
+
"data-testid": `${testIdPrefix}-${col}-insert-gap-${index}`,
|
|
4114
|
+
onClick: () => insertGapAbove(col, index),
|
|
4115
|
+
disabled: locked,
|
|
4116
|
+
title: "Insert a gap above (make this a fade)",
|
|
4117
|
+
className: "text-[10px] text-sas-muted opacity-0 group-hover:opacity-100 hover:text-sas-accent disabled:opacity-30",
|
|
4118
|
+
children: "+gap"
|
|
4119
|
+
}
|
|
4120
|
+
)
|
|
4121
|
+
] })
|
|
4122
|
+
}
|
|
4123
|
+
);
|
|
4124
|
+
};
|
|
4125
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
|
|
4126
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
|
|
4127
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
|
|
4128
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-text", children: fromLabel }),
|
|
4129
|
+
" \u2192",
|
|
4130
|
+
" ",
|
|
4131
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-text", children: toLabel }),
|
|
4132
|
+
familyLabel ? ` \xB7 ${familyLabel}` : "",
|
|
4133
|
+
" \xB7 line up a track on each side to crossfade; leave one blank (or insert a gap) to fade."
|
|
4134
|
+
] }),
|
|
4135
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
4136
|
+
creatingKeys.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
|
|
4137
|
+
creatingKeys.size,
|
|
4138
|
+
" creating\u2026"
|
|
4139
|
+
] }),
|
|
4140
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
|
|
4141
|
+
"button",
|
|
4142
|
+
{
|
|
4143
|
+
type: "button",
|
|
4144
|
+
"data-testid": `${testIdPrefix}-create-all`,
|
|
4145
|
+
onClick: createAll,
|
|
4146
|
+
disabled: eligibleCount === 0,
|
|
4147
|
+
title: "Create every staged transition at once (runs several concurrently)",
|
|
4148
|
+
className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors whitespace-nowrap ${eligibleCount > 0 ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
|
|
4149
|
+
children: [
|
|
4150
|
+
"Create all",
|
|
4151
|
+
eligibleCount > 0 ? ` (${eligibleCount})` : ""
|
|
4152
|
+
]
|
|
4153
|
+
}
|
|
4154
|
+
)
|
|
4155
|
+
] })
|
|
4156
|
+
] }),
|
|
4157
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
|
|
4158
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
|
|
4159
|
+
"Origin (",
|
|
4160
|
+
fromLabel,
|
|
4161
|
+
")"
|
|
4162
|
+
] }),
|
|
4163
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
|
|
4164
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
|
|
4165
|
+
"Target (",
|
|
4166
|
+
toLabel,
|
|
4167
|
+
")"
|
|
4168
|
+
] })
|
|
4169
|
+
] }),
|
|
4170
|
+
load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
|
|
4171
|
+
load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
|
|
4172
|
+
load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
|
|
4173
|
+
"No tracks to arrange in this panel for either scene. Add tracks to ",
|
|
4174
|
+
fromLabel,
|
|
4175
|
+
" or ",
|
|
4176
|
+
toLabel,
|
|
4177
|
+
" ",
|
|
4178
|
+
"first (or free one by deleting an existing crossfade/fade)."
|
|
4179
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "space-y-2", children: rows.map((row, i) => {
|
|
4180
|
+
const key = rowKey(row);
|
|
4181
|
+
const isCreatingThis = key !== null && creatingKeys.has(key);
|
|
4182
|
+
const errMsg = key !== null ? rowErrors[key] : void 0;
|
|
4183
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
|
|
4184
|
+
"div",
|
|
4185
|
+
{
|
|
4186
|
+
"data-testid": `${testIdPrefix}-row-${i}`,
|
|
4187
|
+
className: "grid grid-cols-[1fr_auto_1fr] gap-2 items-center",
|
|
4188
|
+
children: [
|
|
4189
|
+
renderCell("origin", i, row.originId),
|
|
4190
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
|
|
4191
|
+
!row.type ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4192
|
+
"span",
|
|
4193
|
+
{
|
|
4194
|
+
"data-testid": `${testIdPrefix}-type-${i}`,
|
|
4195
|
+
className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-accent/50 text-sas-accent",
|
|
4196
|
+
children: TYPE_LABEL[row.type]
|
|
4197
|
+
}
|
|
4198
|
+
) : audioEffectsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
|
|
4199
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4200
|
+
"select",
|
|
4201
|
+
{
|
|
4202
|
+
"data-testid": `${testIdPrefix}-effect-${i}`,
|
|
4203
|
+
value: rowEffects[row.originId ?? row.targetId] ?? "fade",
|
|
4204
|
+
onChange: (e) => {
|
|
4205
|
+
const id = row.originId ?? row.targetId;
|
|
4206
|
+
if (id) setRowEffect(id, e.target.value);
|
|
4207
|
+
},
|
|
4208
|
+
className: "text-[10px] bg-sas-panel border border-sas-border rounded-sm px-1 py-0.5 text-sas-text",
|
|
4209
|
+
children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
|
|
4210
|
+
}
|
|
4211
|
+
),
|
|
4212
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
|
|
4213
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4214
|
+
"span",
|
|
4215
|
+
{
|
|
4216
|
+
"data-testid": `${testIdPrefix}-type-${i}`,
|
|
4217
|
+
className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-border text-sas-muted",
|
|
4218
|
+
children: TYPE_LABEL[row.type]
|
|
4219
|
+
}
|
|
4220
|
+
),
|
|
4221
|
+
isCreatingThis ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4222
|
+
SorceryProgressBar,
|
|
4223
|
+
{
|
|
4224
|
+
isLoading: true,
|
|
4225
|
+
heightClass: "h-5",
|
|
4226
|
+
statusText: "CREATING",
|
|
4227
|
+
estimatedDurationMs: row.type === "crossfade" ? CROSSFADE_ESTIMATE_MS : FADE_ESTIMATE_MS
|
|
4228
|
+
}
|
|
4229
|
+
) }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4230
|
+
"button",
|
|
4231
|
+
{
|
|
4232
|
+
type: "button",
|
|
4233
|
+
"data-testid": `${testIdPrefix}-create-${i}`,
|
|
4234
|
+
onClick: () => createRow(row),
|
|
4235
|
+
disabled: !row.type,
|
|
4236
|
+
className: `w-full px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors ${row.type ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
|
|
4237
|
+
children: "Create"
|
|
4238
|
+
}
|
|
4239
|
+
),
|
|
4240
|
+
errMsg && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
4241
|
+
"span",
|
|
4242
|
+
{
|
|
4243
|
+
"data-testid": `${testIdPrefix}-row-error-${i}`,
|
|
4244
|
+
className: "text-[10px] text-sas-danger text-center leading-tight",
|
|
4245
|
+
children: errMsg
|
|
4246
|
+
}
|
|
4247
|
+
)
|
|
4248
|
+
] }),
|
|
4249
|
+
renderCell("target", i, row.targetId)
|
|
4250
|
+
]
|
|
4251
|
+
},
|
|
4252
|
+
i
|
|
4253
|
+
);
|
|
4254
|
+
}) }))
|
|
4255
|
+
] });
|
|
4256
|
+
}
|
|
4257
|
+
|
|
3040
4258
|
// src/components/DownloadPackButton.tsx
|
|
3041
|
-
var
|
|
3042
|
-
var
|
|
4259
|
+
var import_react17 = require("react");
|
|
4260
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
3043
4261
|
function formatSize(bytes) {
|
|
3044
4262
|
if (!bytes || bytes <= 0) return "";
|
|
3045
4263
|
const gb = bytes / 1024 ** 3;
|
|
@@ -3055,10 +4273,10 @@ var DownloadPackButton = ({
|
|
|
3055
4273
|
variant = "compact",
|
|
3056
4274
|
onDownloadComplete
|
|
3057
4275
|
}) => {
|
|
3058
|
-
const [status, setStatus] = (0,
|
|
3059
|
-
const [progress, setProgress] = (0,
|
|
3060
|
-
const [errorMessage, setErrorMessage] = (0,
|
|
3061
|
-
(0,
|
|
4276
|
+
const [status, setStatus] = (0, import_react17.useState)("idle");
|
|
4277
|
+
const [progress, setProgress] = (0, import_react17.useState)(0);
|
|
4278
|
+
const [errorMessage, setErrorMessage] = (0, import_react17.useState)(null);
|
|
4279
|
+
(0, import_react17.useEffect)(() => {
|
|
3062
4280
|
const unsub = host.onSamplePackProgress(packId, (p) => {
|
|
3063
4281
|
setStatus(p.status);
|
|
3064
4282
|
setProgress(p.progress);
|
|
@@ -3073,7 +4291,7 @@ var DownloadPackButton = ({
|
|
|
3073
4291
|
});
|
|
3074
4292
|
return unsub;
|
|
3075
4293
|
}, [host, packId, onDownloadComplete]);
|
|
3076
|
-
const handleClick = (0,
|
|
4294
|
+
const handleClick = (0, import_react17.useCallback)(async () => {
|
|
3077
4295
|
if (status !== "idle" && status !== "error") return;
|
|
3078
4296
|
try {
|
|
3079
4297
|
setStatus("downloading");
|
|
@@ -3127,8 +4345,8 @@ var DownloadPackButton = ({
|
|
|
3127
4345
|
} else {
|
|
3128
4346
|
className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
|
|
3129
4347
|
}
|
|
3130
|
-
return /* @__PURE__ */ (0,
|
|
3131
|
-
/* @__PURE__ */ (0,
|
|
4348
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
|
|
4349
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3132
4350
|
"button",
|
|
3133
4351
|
{
|
|
3134
4352
|
"data-testid": `download-pack-button-${packId}`,
|
|
@@ -3139,12 +4357,12 @@ var DownloadPackButton = ({
|
|
|
3139
4357
|
children: buttonLabel
|
|
3140
4358
|
}
|
|
3141
4359
|
),
|
|
3142
|
-
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ (0,
|
|
4360
|
+
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
|
|
3143
4361
|
] });
|
|
3144
4362
|
};
|
|
3145
4363
|
|
|
3146
4364
|
// src/components/SamplePackCTACard.tsx
|
|
3147
|
-
var
|
|
4365
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
3148
4366
|
var SamplePackCTACard = ({
|
|
3149
4367
|
host,
|
|
3150
4368
|
pack,
|
|
@@ -3152,7 +4370,7 @@ var SamplePackCTACard = ({
|
|
|
3152
4370
|
onDownloadComplete
|
|
3153
4371
|
}) => {
|
|
3154
4372
|
if (status === "checking") {
|
|
3155
|
-
return /* @__PURE__ */ (0,
|
|
4373
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3156
4374
|
"div",
|
|
3157
4375
|
{
|
|
3158
4376
|
"data-testid": `sample-pack-cta-checking-${pack.packId}`,
|
|
@@ -3163,16 +4381,16 @@ var SamplePackCTACard = ({
|
|
|
3163
4381
|
}
|
|
3164
4382
|
const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
|
|
3165
4383
|
const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
|
|
3166
|
-
return /* @__PURE__ */ (0,
|
|
4384
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
3167
4385
|
"div",
|
|
3168
4386
|
{
|
|
3169
4387
|
"data-testid": `sample-pack-cta-${pack.packId}`,
|
|
3170
4388
|
className: "flex flex-col items-center justify-center py-12 px-6 text-center",
|
|
3171
4389
|
children: [
|
|
3172
|
-
/* @__PURE__ */ (0,
|
|
3173
|
-
/* @__PURE__ */ (0,
|
|
3174
|
-
/* @__PURE__ */ (0,
|
|
3175
|
-
/* @__PURE__ */ (0,
|
|
4390
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
|
|
4391
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-base text-sas-text mb-1", children: headline }),
|
|
4392
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
|
|
4393
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3176
4394
|
DownloadPackButton,
|
|
3177
4395
|
{
|
|
3178
4396
|
host,
|
|
@@ -3189,7 +4407,7 @@ var SamplePackCTACard = ({
|
|
|
3189
4407
|
};
|
|
3190
4408
|
|
|
3191
4409
|
// src/components/WaveformView.tsx
|
|
3192
|
-
var
|
|
4410
|
+
var import_react18 = require("react");
|
|
3193
4411
|
|
|
3194
4412
|
// src/components/waveform.ts
|
|
3195
4413
|
function computePeaks(audioBuffer, bins, targetSamples) {
|
|
@@ -3252,7 +4470,7 @@ function drawWaveform(canvas, peaks, options = {}) {
|
|
|
3252
4470
|
}
|
|
3253
4471
|
|
|
3254
4472
|
// src/components/WaveformView.tsx
|
|
3255
|
-
var
|
|
4473
|
+
var import_jsx_runtime20 = require("react/jsx-runtime");
|
|
3256
4474
|
var WaveformView = ({
|
|
3257
4475
|
host,
|
|
3258
4476
|
filePath,
|
|
@@ -3261,9 +4479,9 @@ var WaveformView = ({
|
|
|
3261
4479
|
fillStyle,
|
|
3262
4480
|
targetSamples
|
|
3263
4481
|
}) => {
|
|
3264
|
-
const canvasRef = (0,
|
|
3265
|
-
const [peaks, setPeaks] = (0,
|
|
3266
|
-
(0,
|
|
4482
|
+
const canvasRef = (0, import_react18.useRef)(null);
|
|
4483
|
+
const [peaks, setPeaks] = (0, import_react18.useState)(null);
|
|
4484
|
+
(0, import_react18.useEffect)(() => {
|
|
3267
4485
|
let cancelled = false;
|
|
3268
4486
|
let audioContext = null;
|
|
3269
4487
|
(async () => {
|
|
@@ -3289,7 +4507,7 @@ var WaveformView = ({
|
|
|
3289
4507
|
cancelled = true;
|
|
3290
4508
|
};
|
|
3291
4509
|
}, [host, filePath, bins, targetSamples]);
|
|
3292
|
-
(0,
|
|
4510
|
+
(0, import_react18.useEffect)(() => {
|
|
3293
4511
|
if (!peaks) return;
|
|
3294
4512
|
const canvas = canvasRef.current;
|
|
3295
4513
|
if (!canvas) return;
|
|
@@ -3300,7 +4518,7 @@ var WaveformView = ({
|
|
|
3300
4518
|
observer.observe(canvas);
|
|
3301
4519
|
return () => observer.disconnect();
|
|
3302
4520
|
}, [peaks, fillStyle]);
|
|
3303
|
-
return /* @__PURE__ */ (0,
|
|
4521
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
3304
4522
|
"canvas",
|
|
3305
4523
|
{
|
|
3306
4524
|
ref: canvasRef,
|
|
@@ -3311,8 +4529,8 @@ var WaveformView = ({
|
|
|
3311
4529
|
};
|
|
3312
4530
|
|
|
3313
4531
|
// src/components/ScrollingWaveform.tsx
|
|
3314
|
-
var
|
|
3315
|
-
var
|
|
4532
|
+
var import_react19 = require("react");
|
|
4533
|
+
var import_jsx_runtime21 = require("react/jsx-runtime");
|
|
3316
4534
|
var ScrollingWaveform = ({
|
|
3317
4535
|
getPeakDb,
|
|
3318
4536
|
active,
|
|
@@ -3320,11 +4538,11 @@ var ScrollingWaveform = ({
|
|
|
3320
4538
|
className,
|
|
3321
4539
|
fillStyle
|
|
3322
4540
|
}) => {
|
|
3323
|
-
const canvasRef = (0,
|
|
3324
|
-
const ringRef = (0,
|
|
3325
|
-
const writeIdxRef = (0,
|
|
3326
|
-
const rafRef = (0,
|
|
3327
|
-
(0,
|
|
4541
|
+
const canvasRef = (0, import_react19.useRef)(null);
|
|
4542
|
+
const ringRef = (0, import_react19.useRef)(new Float32Array(columns));
|
|
4543
|
+
const writeIdxRef = (0, import_react19.useRef)(0);
|
|
4544
|
+
const rafRef = (0, import_react19.useRef)(null);
|
|
4545
|
+
(0, import_react19.useEffect)(() => {
|
|
3328
4546
|
if (ringRef.current.length !== columns) {
|
|
3329
4547
|
const next = new Float32Array(columns);
|
|
3330
4548
|
const prev = ringRef.current;
|
|
@@ -3336,7 +4554,7 @@ var ScrollingWaveform = ({
|
|
|
3336
4554
|
writeIdxRef.current = writeIdxRef.current % columns;
|
|
3337
4555
|
}
|
|
3338
4556
|
}, [columns]);
|
|
3339
|
-
(0,
|
|
4557
|
+
(0, import_react19.useEffect)(() => {
|
|
3340
4558
|
if (!active) {
|
|
3341
4559
|
if (rafRef.current !== null) {
|
|
3342
4560
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -3388,7 +4606,7 @@ var ScrollingWaveform = ({
|
|
|
3388
4606
|
}
|
|
3389
4607
|
};
|
|
3390
4608
|
}, [active, getPeakDb, fillStyle]);
|
|
3391
|
-
return /* @__PURE__ */ (0,
|
|
4609
|
+
return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
|
|
3392
4610
|
"canvas",
|
|
3393
4611
|
{
|
|
3394
4612
|
ref: canvasRef,
|
|
@@ -3399,8 +4617,8 @@ var ScrollingWaveform = ({
|
|
|
3399
4617
|
};
|
|
3400
4618
|
|
|
3401
4619
|
// src/components/OffsetScrubber.tsx
|
|
3402
|
-
var
|
|
3403
|
-
var
|
|
4620
|
+
var import_react20 = require("react");
|
|
4621
|
+
var import_jsx_runtime22 = require("react/jsx-runtime");
|
|
3404
4622
|
var SLIDER_HEIGHT_PX = 28;
|
|
3405
4623
|
var TICK_HEIGHT_PX = 14;
|
|
3406
4624
|
var DOWNBEAT_TICK_HEIGHT_PX = 22;
|
|
@@ -3413,40 +4631,40 @@ function OffsetScrubber({
|
|
|
3413
4631
|
onChange,
|
|
3414
4632
|
disabled = false
|
|
3415
4633
|
}) {
|
|
3416
|
-
const trackRef = (0,
|
|
3417
|
-
const [draftOffset, setDraftOffset] = (0,
|
|
3418
|
-
const [isDragging, setIsDragging] = (0,
|
|
3419
|
-
(0,
|
|
4634
|
+
const trackRef = (0, import_react20.useRef)(null);
|
|
4635
|
+
const [draftOffset, setDraftOffset] = (0, import_react20.useState)(offsetSamples);
|
|
4636
|
+
const [isDragging, setIsDragging] = (0, import_react20.useState)(false);
|
|
4637
|
+
(0, import_react20.useEffect)(() => {
|
|
3420
4638
|
if (!isDragging) setDraftOffset(offsetSamples);
|
|
3421
4639
|
}, [offsetSamples, isDragging]);
|
|
3422
4640
|
const sampleRate = cuePoints?.sample_rate ?? 44100;
|
|
3423
4641
|
const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
|
|
3424
|
-
const beatsForRange = (0,
|
|
4642
|
+
const beatsForRange = (0, import_react20.useMemo)(() => {
|
|
3425
4643
|
return Math.round(60 / projectBpm * sampleRate);
|
|
3426
4644
|
}, [projectBpm, sampleRate]);
|
|
3427
4645
|
const rangeSamples = beatsForRange * meter;
|
|
3428
|
-
const sampleToFraction = (0,
|
|
4646
|
+
const sampleToFraction = (0, import_react20.useCallback)(
|
|
3429
4647
|
(sample) => {
|
|
3430
4648
|
const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
|
|
3431
4649
|
return (clamped + rangeSamples) / (2 * rangeSamples);
|
|
3432
4650
|
},
|
|
3433
4651
|
[rangeSamples]
|
|
3434
4652
|
);
|
|
3435
|
-
const fractionToSample = (0,
|
|
4653
|
+
const fractionToSample = (0, import_react20.useCallback)(
|
|
3436
4654
|
(fraction) => {
|
|
3437
4655
|
const clamped = Math.max(0, Math.min(1, fraction));
|
|
3438
4656
|
return Math.round(clamped * 2 * rangeSamples - rangeSamples);
|
|
3439
4657
|
},
|
|
3440
4658
|
[rangeSamples]
|
|
3441
4659
|
);
|
|
3442
|
-
const snapTargets = (0,
|
|
4660
|
+
const snapTargets = (0, import_react20.useMemo)(() => {
|
|
3443
4661
|
if (!cuePoints || cuePoints.beats.length === 0) return [];
|
|
3444
4662
|
const downbeat = cuePoints.beats[0];
|
|
3445
4663
|
const positives = cuePoints.beats.map((b) => b - downbeat);
|
|
3446
4664
|
const negatives = positives.slice(1).map((p) => -p);
|
|
3447
4665
|
return [...negatives, ...positives].sort((a, b) => a - b);
|
|
3448
4666
|
}, [cuePoints]);
|
|
3449
|
-
const snapToBeat = (0,
|
|
4667
|
+
const snapToBeat = (0, import_react20.useCallback)(
|
|
3450
4668
|
(sample) => {
|
|
3451
4669
|
if (snapTargets.length === 0) return sample;
|
|
3452
4670
|
let best = snapTargets[0];
|
|
@@ -3462,7 +4680,7 @@ function OffsetScrubber({
|
|
|
3462
4680
|
},
|
|
3463
4681
|
[snapTargets]
|
|
3464
4682
|
);
|
|
3465
|
-
const handlePointerDown = (0,
|
|
4683
|
+
const handlePointerDown = (0, import_react20.useCallback)(
|
|
3466
4684
|
(e) => {
|
|
3467
4685
|
if (disabled || !cuePoints) return;
|
|
3468
4686
|
e.preventDefault();
|
|
@@ -3496,7 +4714,7 @@ function OffsetScrubber({
|
|
|
3496
4714
|
},
|
|
3497
4715
|
[disabled, cuePoints, fractionToSample, onChange, snapToBeat]
|
|
3498
4716
|
);
|
|
3499
|
-
const handleResetToZero = (0,
|
|
4717
|
+
const handleResetToZero = (0, import_react20.useCallback)(() => {
|
|
3500
4718
|
if (disabled) return;
|
|
3501
4719
|
setDraftOffset(0);
|
|
3502
4720
|
onChange(0);
|
|
@@ -3504,7 +4722,7 @@ function OffsetScrubber({
|
|
|
3504
4722
|
const thumbFraction = sampleToFraction(draftOffset);
|
|
3505
4723
|
const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;
|
|
3506
4724
|
const bpmMismatch = cuePoints?.detected_bpm != null && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;
|
|
3507
|
-
const ticks = (0,
|
|
4725
|
+
const ticks = (0, import_react20.useMemo)(() => {
|
|
3508
4726
|
if (!cuePoints) return [];
|
|
3509
4727
|
const downbeat = cuePoints.beats[0] ?? 0;
|
|
3510
4728
|
return cuePoints.beats.map((b, i) => {
|
|
@@ -3515,9 +4733,9 @@ function OffsetScrubber({
|
|
|
3515
4733
|
});
|
|
3516
4734
|
}, [cuePoints, sampleToFraction]);
|
|
3517
4735
|
const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
|
|
3518
|
-
return /* @__PURE__ */ (0,
|
|
3519
|
-
/* @__PURE__ */ (0,
|
|
3520
|
-
/* @__PURE__ */ (0,
|
|
4736
|
+
return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
|
|
4737
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
|
|
4738
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
|
|
3521
4739
|
"div",
|
|
3522
4740
|
{
|
|
3523
4741
|
ref: trackRef,
|
|
@@ -3533,7 +4751,7 @@ function OffsetScrubber({
|
|
|
3533
4751
|
"aria-valuenow": draftOffset,
|
|
3534
4752
|
"aria-disabled": isDisabled,
|
|
3535
4753
|
children: [
|
|
3536
|
-
/* @__PURE__ */ (0,
|
|
4754
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
3537
4755
|
"div",
|
|
3538
4756
|
{
|
|
3539
4757
|
"aria-hidden": "true",
|
|
@@ -3541,7 +4759,7 @@ function OffsetScrubber({
|
|
|
3541
4759
|
style: { left: "50%" }
|
|
3542
4760
|
}
|
|
3543
4761
|
),
|
|
3544
|
-
ticks.map((t) => /* @__PURE__ */ (0,
|
|
4762
|
+
ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
3545
4763
|
"div",
|
|
3546
4764
|
{
|
|
3547
4765
|
"data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
|
|
@@ -3556,7 +4774,7 @@ function OffsetScrubber({
|
|
|
3556
4774
|
},
|
|
3557
4775
|
t.i
|
|
3558
4776
|
)),
|
|
3559
|
-
/* @__PURE__ */ (0,
|
|
4777
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
3560
4778
|
"div",
|
|
3561
4779
|
{
|
|
3562
4780
|
"data-testid": "offset-scrubber-thumb",
|
|
@@ -3573,7 +4791,7 @@ function OffsetScrubber({
|
|
|
3573
4791
|
]
|
|
3574
4792
|
}
|
|
3575
4793
|
),
|
|
3576
|
-
/* @__PURE__ */ (0,
|
|
4794
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
3577
4795
|
"span",
|
|
3578
4796
|
{
|
|
3579
4797
|
"data-testid": "offset-scrubber-readout",
|
|
@@ -3581,7 +4799,7 @@ function OffsetScrubber({
|
|
|
3581
4799
|
children: formatOffset(draftOffset, sampleRate)
|
|
3582
4800
|
}
|
|
3583
4801
|
),
|
|
3584
|
-
/* @__PURE__ */ (0,
|
|
4802
|
+
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
3585
4803
|
"button",
|
|
3586
4804
|
{
|
|
3587
4805
|
type: "button",
|
|
@@ -3593,7 +4811,7 @@ function OffsetScrubber({
|
|
|
3593
4811
|
children: "\u2316"
|
|
3594
4812
|
}
|
|
3595
4813
|
),
|
|
3596
|
-
bpmMismatch && /* @__PURE__ */ (0,
|
|
4814
|
+
bpmMismatch && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
3597
4815
|
"span",
|
|
3598
4816
|
{
|
|
3599
4817
|
"data-testid": "offset-bpm-mismatch",
|
|
@@ -3665,13 +4883,13 @@ function synthesizeCuePoints({
|
|
|
3665
4883
|
}
|
|
3666
4884
|
|
|
3667
4885
|
// src/hooks/useSceneState.ts
|
|
3668
|
-
var
|
|
4886
|
+
var import_react21 = require("react");
|
|
3669
4887
|
function useSceneState(activeSceneId, initialValue) {
|
|
3670
|
-
const [stateMap, setStateMap] = (0,
|
|
3671
|
-
const activeSceneIdRef = (0,
|
|
4888
|
+
const [stateMap, setStateMap] = (0, import_react21.useState)(() => /* @__PURE__ */ new Map());
|
|
4889
|
+
const activeSceneIdRef = (0, import_react21.useRef)(activeSceneId);
|
|
3672
4890
|
activeSceneIdRef.current = activeSceneId;
|
|
3673
4891
|
const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
|
|
3674
|
-
const setForCurrentScene = (0,
|
|
4892
|
+
const setForCurrentScene = (0, import_react21.useCallback)((value) => {
|
|
3675
4893
|
const sid = activeSceneIdRef.current;
|
|
3676
4894
|
if (sid === null) return;
|
|
3677
4895
|
setStateMap((prev) => {
|
|
@@ -3682,7 +4900,7 @@ function useSceneState(activeSceneId, initialValue) {
|
|
|
3682
4900
|
return newMap;
|
|
3683
4901
|
});
|
|
3684
4902
|
}, [initialValue]);
|
|
3685
|
-
const setForScene = (0,
|
|
4903
|
+
const setForScene = (0, import_react21.useCallback)((sceneId, value) => {
|
|
3686
4904
|
setStateMap((prev) => {
|
|
3687
4905
|
const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
|
|
3688
4906
|
const next = typeof value === "function" ? value(current) : value;
|
|
@@ -3695,10 +4913,10 @@ function useSceneState(activeSceneId, initialValue) {
|
|
|
3695
4913
|
}
|
|
3696
4914
|
|
|
3697
4915
|
// src/hooks/useAnySolo.ts
|
|
3698
|
-
var
|
|
4916
|
+
var import_react22 = require("react");
|
|
3699
4917
|
function useAnySolo(host) {
|
|
3700
|
-
const [anySolo, setAnySolo] = (0,
|
|
3701
|
-
(0,
|
|
4918
|
+
const [anySolo, setAnySolo] = (0, import_react22.useState)(false);
|
|
4919
|
+
(0, import_react22.useEffect)(() => {
|
|
3702
4920
|
let active = true;
|
|
3703
4921
|
const refresh = () => {
|
|
3704
4922
|
host.isAnySoloActive().then((v) => {
|
|
@@ -3717,7 +4935,7 @@ function useAnySolo(host) {
|
|
|
3717
4935
|
}
|
|
3718
4936
|
|
|
3719
4937
|
// src/hooks/useSoundHistory.ts
|
|
3720
|
-
var
|
|
4938
|
+
var import_react23 = require("react");
|
|
3721
4939
|
var EMPTY = { entries: [], cursor: -1 };
|
|
3722
4940
|
function sameDescriptor(a, b) {
|
|
3723
4941
|
if (a === b) return true;
|
|
@@ -3729,14 +4947,14 @@ function sameDescriptor(a, b) {
|
|
|
3729
4947
|
}
|
|
3730
4948
|
function useSoundHistory(applySound, opts = {}) {
|
|
3731
4949
|
const max = Math.max(2, opts.max ?? 24);
|
|
3732
|
-
const applyRef = (0,
|
|
4950
|
+
const applyRef = (0, import_react23.useRef)(applySound);
|
|
3733
4951
|
applyRef.current = applySound;
|
|
3734
|
-
const onChangeRef = (0,
|
|
4952
|
+
const onChangeRef = (0, import_react23.useRef)(opts.onChange);
|
|
3735
4953
|
onChangeRef.current = opts.onChange;
|
|
3736
|
-
const dataRef = (0,
|
|
3737
|
-
const [, setVersion] = (0,
|
|
3738
|
-
const bump = (0,
|
|
3739
|
-
const commit = (0,
|
|
4954
|
+
const dataRef = (0, import_react23.useRef)({});
|
|
4955
|
+
const [, setVersion] = (0, import_react23.useState)(0);
|
|
4956
|
+
const bump = (0, import_react23.useCallback)(() => setVersion((v) => v + 1), []);
|
|
4957
|
+
const commit = (0, import_react23.useCallback)(
|
|
3740
4958
|
(trackId, next, notify) => {
|
|
3741
4959
|
dataRef.current = { ...dataRef.current, [trackId]: next };
|
|
3742
4960
|
bump();
|
|
@@ -3744,7 +4962,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3744
4962
|
},
|
|
3745
4963
|
[bump]
|
|
3746
4964
|
);
|
|
3747
|
-
const record = (0,
|
|
4965
|
+
const record = (0, import_react23.useCallback)(
|
|
3748
4966
|
(trackId, descriptor, label) => {
|
|
3749
4967
|
const h = dataRef.current[trackId];
|
|
3750
4968
|
const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
|
|
@@ -3759,7 +4977,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3759
4977
|
},
|
|
3760
4978
|
[max, commit]
|
|
3761
4979
|
);
|
|
3762
|
-
const restoreTo = (0,
|
|
4980
|
+
const restoreTo = (0, import_react23.useCallback)(
|
|
3763
4981
|
async (trackId, index) => {
|
|
3764
4982
|
const h = dataRef.current[trackId];
|
|
3765
4983
|
if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
|
|
@@ -3769,7 +4987,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3769
4987
|
},
|
|
3770
4988
|
[commit]
|
|
3771
4989
|
);
|
|
3772
|
-
const undo = (0,
|
|
4990
|
+
const undo = (0, import_react23.useCallback)(
|
|
3773
4991
|
(trackId) => {
|
|
3774
4992
|
const h = dataRef.current[trackId];
|
|
3775
4993
|
if (!h || h.cursor <= 0) return Promise.resolve(false);
|
|
@@ -3777,7 +4995,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3777
4995
|
},
|
|
3778
4996
|
[restoreTo]
|
|
3779
4997
|
);
|
|
3780
|
-
const toggleFavorite = (0,
|
|
4998
|
+
const toggleFavorite = (0, import_react23.useCallback)(
|
|
3781
4999
|
(trackId, index) => {
|
|
3782
5000
|
const h = dataRef.current[trackId];
|
|
3783
5001
|
if (!h || index < 0 || index >= h.entries.length) return;
|
|
@@ -3786,7 +5004,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3786
5004
|
},
|
|
3787
5005
|
[commit]
|
|
3788
5006
|
);
|
|
3789
|
-
const restore = (0,
|
|
5007
|
+
const restore = (0, import_react23.useCallback)(
|
|
3790
5008
|
(trackId, state) => {
|
|
3791
5009
|
const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
|
|
3792
5010
|
const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
|
|
@@ -3795,15 +5013,15 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3795
5013
|
},
|
|
3796
5014
|
[commit]
|
|
3797
5015
|
);
|
|
3798
|
-
const list = (0,
|
|
5016
|
+
const list = (0, import_react23.useCallback)(
|
|
3799
5017
|
(trackId) => dataRef.current[trackId] ?? EMPTY,
|
|
3800
5018
|
[]
|
|
3801
5019
|
);
|
|
3802
|
-
const canUndo = (0,
|
|
5020
|
+
const canUndo = (0, import_react23.useCallback)((trackId) => {
|
|
3803
5021
|
const h = dataRef.current[trackId];
|
|
3804
5022
|
return !!h && h.cursor > 0;
|
|
3805
5023
|
}, []);
|
|
3806
|
-
const clear = (0,
|
|
5024
|
+
const clear = (0, import_react23.useCallback)(
|
|
3807
5025
|
(trackId) => {
|
|
3808
5026
|
if (dataRef.current[trackId]) {
|
|
3809
5027
|
const next = { ...dataRef.current };
|
|
@@ -3815,102 +5033,18 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3815
5033
|
},
|
|
3816
5034
|
[bump]
|
|
3817
5035
|
);
|
|
3818
|
-
const reset = (0,
|
|
5036
|
+
const reset = (0, import_react23.useCallback)(() => {
|
|
3819
5037
|
dataRef.current = {};
|
|
3820
5038
|
bump();
|
|
3821
5039
|
}, [bump]);
|
|
3822
|
-
return (0,
|
|
5040
|
+
return (0, import_react23.useMemo)(
|
|
3823
5041
|
() => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),
|
|
3824
5042
|
[record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite]
|
|
3825
5043
|
);
|
|
3826
5044
|
}
|
|
3827
5045
|
|
|
3828
|
-
// src/hooks/useTrackReorder.ts
|
|
3829
|
-
var import_react20 = require("react");
|
|
3830
|
-
function moveItem(arr, from, to) {
|
|
3831
|
-
const next = arr.slice();
|
|
3832
|
-
if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
|
|
3833
|
-
return next;
|
|
3834
|
-
}
|
|
3835
|
-
const [moved] = next.splice(from, 1);
|
|
3836
|
-
next.splice(to, 0, moved);
|
|
3837
|
-
return next;
|
|
3838
|
-
}
|
|
3839
|
-
function useTrackReorder({
|
|
3840
|
-
host,
|
|
3841
|
-
items,
|
|
3842
|
-
setItems,
|
|
3843
|
-
getId,
|
|
3844
|
-
onError
|
|
3845
|
-
}) {
|
|
3846
|
-
const [draggingIndex, setDraggingIndex] = (0, import_react20.useState)(null);
|
|
3847
|
-
const [dragOverIndex, setDragOverIndex] = (0, import_react20.useState)(null);
|
|
3848
|
-
const fromRef = (0, import_react20.useRef)(null);
|
|
3849
|
-
const itemsRef = (0, import_react20.useRef)(items);
|
|
3850
|
-
itemsRef.current = items;
|
|
3851
|
-
const dragPropsFor = (0, import_react20.useCallback)(
|
|
3852
|
-
(index) => ({
|
|
3853
|
-
handleProps: {
|
|
3854
|
-
draggable: true,
|
|
3855
|
-
onDragStart: (e) => {
|
|
3856
|
-
fromRef.current = index;
|
|
3857
|
-
setDraggingIndex(index);
|
|
3858
|
-
if (e.dataTransfer) {
|
|
3859
|
-
e.dataTransfer.effectAllowed = "move";
|
|
3860
|
-
try {
|
|
3861
|
-
e.dataTransfer.setData("text/plain", String(index));
|
|
3862
|
-
} catch {
|
|
3863
|
-
}
|
|
3864
|
-
}
|
|
3865
|
-
},
|
|
3866
|
-
onDragEnd: () => {
|
|
3867
|
-
fromRef.current = null;
|
|
3868
|
-
setDraggingIndex(null);
|
|
3869
|
-
setDragOverIndex(null);
|
|
3870
|
-
}
|
|
3871
|
-
},
|
|
3872
|
-
rowProps: {
|
|
3873
|
-
onDragEnter: (e) => {
|
|
3874
|
-
if (fromRef.current === null) return;
|
|
3875
|
-
e.preventDefault();
|
|
3876
|
-
setDragOverIndex(index);
|
|
3877
|
-
},
|
|
3878
|
-
onDragOver: (e) => {
|
|
3879
|
-
if (fromRef.current === null) return;
|
|
3880
|
-
e.preventDefault();
|
|
3881
|
-
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
3882
|
-
setDragOverIndex((cur) => cur === index ? cur : index);
|
|
3883
|
-
},
|
|
3884
|
-
onDragLeave: () => {
|
|
3885
|
-
setDragOverIndex((cur) => cur === index ? null : cur);
|
|
3886
|
-
},
|
|
3887
|
-
onDrop: (e) => {
|
|
3888
|
-
e.preventDefault();
|
|
3889
|
-
const from = fromRef.current;
|
|
3890
|
-
fromRef.current = null;
|
|
3891
|
-
setDraggingIndex(null);
|
|
3892
|
-
setDragOverIndex(null);
|
|
3893
|
-
if (from === null || from === index) return;
|
|
3894
|
-
const prev = itemsRef.current;
|
|
3895
|
-
const next = moveItem(prev, from, index);
|
|
3896
|
-
setItems(next);
|
|
3897
|
-
const ids = next.map(getId);
|
|
3898
|
-
Promise.resolve(host.reorderTracks(ids)).catch((err) => {
|
|
3899
|
-
setItems(prev);
|
|
3900
|
-
onError?.(err);
|
|
3901
|
-
});
|
|
3902
|
-
}
|
|
3903
|
-
},
|
|
3904
|
-
isDragging: draggingIndex === index,
|
|
3905
|
-
isDragTarget: dragOverIndex === index && draggingIndex !== index
|
|
3906
|
-
}),
|
|
3907
|
-
[host, setItems, getId, onError, draggingIndex, dragOverIndex]
|
|
3908
|
-
);
|
|
3909
|
-
return { dragPropsFor, draggingIndex, dragOverIndex };
|
|
3910
|
-
}
|
|
3911
|
-
|
|
3912
5046
|
// src/constants/sdk-version.ts
|
|
3913
|
-
var PLUGIN_SDK_VERSION = "2.
|
|
5047
|
+
var PLUGIN_SDK_VERSION = "2.34.0";
|
|
3914
5048
|
|
|
3915
5049
|
// src/utils/format-concurrent-tracks.ts
|
|
3916
5050
|
function formatConcurrentTracks(ctx) {
|
|
@@ -4054,6 +5188,8 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
4054
5188
|
}
|
|
4055
5189
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4056
5190
|
0 && (module.exports = {
|
|
5191
|
+
AUDIO_EFFECTS,
|
|
5192
|
+
AUDIO_EFFECT_LABEL,
|
|
4057
5193
|
ConfirmDialog,
|
|
4058
5194
|
CrossfadeModal,
|
|
4059
5195
|
CrossfadeTrackRow,
|
|
@@ -4071,6 +5207,8 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
4071
5207
|
FX_DISPLAY_LABELS,
|
|
4072
5208
|
FX_ENGINE_PLUGIN_NAMES,
|
|
4073
5209
|
FX_PRESET_CONFIGS,
|
|
5210
|
+
FadeModal,
|
|
5211
|
+
FadeTrackRow,
|
|
4074
5212
|
FxToggleBar,
|
|
4075
5213
|
GUTTER_W,
|
|
4076
5214
|
ImportTrackModal,
|
|
@@ -4089,30 +5227,50 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
4089
5227
|
SamplePackCTACard,
|
|
4090
5228
|
ScrollingWaveform,
|
|
4091
5229
|
SorceryProgressBar,
|
|
5230
|
+
TEXTURAL_ROLES,
|
|
5231
|
+
TRANSITION_DESIGNER_DRAFT_KEY,
|
|
4092
5232
|
TrackDrawer,
|
|
4093
5233
|
TrackMeterStrip,
|
|
4094
5234
|
TrackRow,
|
|
5235
|
+
TransitionDesigner,
|
|
4095
5236
|
VolumeSlider,
|
|
4096
5237
|
WaveformView,
|
|
4097
5238
|
analyzeWavPeak,
|
|
5239
|
+
asAudioEffect,
|
|
4098
5240
|
asCrossfadeMeta,
|
|
5241
|
+
asFadeMeta,
|
|
5242
|
+
asTransitionDesignerDraft,
|
|
4099
5243
|
buildCrossfadeInpaintPrompt,
|
|
4100
5244
|
buildCrossfadeVolumeCurves,
|
|
5245
|
+
buildFadeVolumeCurve,
|
|
5246
|
+
buildRowSlots,
|
|
4101
5247
|
calculateTimeBasedTarget,
|
|
4102
5248
|
cellToPx,
|
|
4103
5249
|
centerScrollTop,
|
|
4104
5250
|
computePeaks,
|
|
5251
|
+
dbIdsFromKeys,
|
|
4105
5252
|
dbToSlider,
|
|
5253
|
+
defaultFadeGesture,
|
|
4106
5254
|
drawWaveform,
|
|
4107
5255
|
formatConcurrentTracks,
|
|
5256
|
+
hashString,
|
|
4108
5257
|
moveItem,
|
|
5258
|
+
normalizeSlots,
|
|
5259
|
+
padPair,
|
|
5260
|
+
padSlots,
|
|
4109
5261
|
parseCrossfadePairs,
|
|
5262
|
+
parseFades,
|
|
4110
5263
|
pickTopKWeighted,
|
|
4111
5264
|
pitchToName,
|
|
4112
5265
|
pxToCell,
|
|
5266
|
+
reconcileSlots,
|
|
4113
5267
|
resizeNoteDuration,
|
|
5268
|
+
rowKey,
|
|
5269
|
+
rowType,
|
|
4114
5270
|
scorePromptMatch,
|
|
4115
5271
|
sliderToDb,
|
|
5272
|
+
slotsEqual,
|
|
5273
|
+
soundIdentity,
|
|
4116
5274
|
synthesizeCuePoints,
|
|
4117
5275
|
tokenizePrompt,
|
|
4118
5276
|
transposeNotes,
|