@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.mjs
CHANGED
|
@@ -1343,6 +1343,7 @@ var LevelMeter = ({
|
|
|
1343
1343
|
|
|
1344
1344
|
// src/hooks/useTrackLevels.ts
|
|
1345
1345
|
import { useEffect as useEffect2, useRef as useRef3, useState as useState3 } from "react";
|
|
1346
|
+
var meterDiagRLast = /* @__PURE__ */ new Map();
|
|
1346
1347
|
var POLL_INTERVAL_MS = 33;
|
|
1347
1348
|
var HIDDEN_RECHECK_MS = 250;
|
|
1348
1349
|
var METER_FLOOR_DB = -120;
|
|
@@ -1474,6 +1475,11 @@ function useTrackMeter(handle, trackId) {
|
|
|
1474
1475
|
}
|
|
1475
1476
|
const update = () => {
|
|
1476
1477
|
const level = handle.getLevel(trackId);
|
|
1478
|
+
const dNow = Date.now();
|
|
1479
|
+
if ((meterDiagRLast.get(trackId) ?? 0) < dNow - 3e3) {
|
|
1480
|
+
meterDiagRLast.set(trackId, dNow);
|
|
1481
|
+
console.log(`[MeterDiagR] lookup trackId=${trackId} \u2192 ${level === null ? "MISS (no level for this id)" : "hit"}`);
|
|
1482
|
+
}
|
|
1477
1483
|
const now = performance.now();
|
|
1478
1484
|
const dtSec = lastTickRef.current ? Math.max(0, (now - lastTickRef.current) / 1e3) : 0;
|
|
1479
1485
|
lastTickRef.current = now;
|
|
@@ -2175,8 +2181,7 @@ function TrackRow({
|
|
|
2175
2181
|
{
|
|
2176
2182
|
"data-testid": "sdk-mute-button",
|
|
2177
2183
|
onClick: onMuteToggle,
|
|
2178
|
-
|
|
2179
|
-
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"}`,
|
|
2184
|
+
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"}`,
|
|
2180
2185
|
title: isMuted ? "Unmute track" : "Mute track",
|
|
2181
2186
|
children: "M"
|
|
2182
2187
|
}
|
|
@@ -2427,6 +2432,18 @@ function CrossfadeTrackRow({
|
|
|
2427
2432
|
}
|
|
2428
2433
|
|
|
2429
2434
|
// src/crossfade-meta.ts
|
|
2435
|
+
function hashString(s) {
|
|
2436
|
+
let h = 5381;
|
|
2437
|
+
for (let i = 0; i < s.length; i++) h = (h << 5) + h ^ s.charCodeAt(i) | 0;
|
|
2438
|
+
return (h >>> 0).toString(36);
|
|
2439
|
+
}
|
|
2440
|
+
function soundIdentity(snap) {
|
|
2441
|
+
if (!snap) return "";
|
|
2442
|
+
if (snap.kind === "preset") return `p:${hashString(snap.state)}`;
|
|
2443
|
+
if (snap.kind === "sample") return `s:${snap.samplePath}`;
|
|
2444
|
+
if (snap.kind === "instrument") return `i:${snap.instrumentId ?? ""}:${hashString(JSON.stringify(snap.zones))}`;
|
|
2445
|
+
return "";
|
|
2446
|
+
}
|
|
2430
2447
|
var EQUAL_POWER_GAIN = 0.707;
|
|
2431
2448
|
function asCrossfadeMeta(val) {
|
|
2432
2449
|
if (!val || typeof val !== "object") return null;
|
|
@@ -2570,9 +2587,452 @@ function buildCrossfadeInpaintPrompt(input) {
|
|
|
2570
2587
|
return lines.join("\n");
|
|
2571
2588
|
}
|
|
2572
2589
|
|
|
2590
|
+
// src/fade-meta.ts
|
|
2591
|
+
function asFadeMeta(val) {
|
|
2592
|
+
if (!val || typeof val !== "object") return null;
|
|
2593
|
+
const m = val;
|
|
2594
|
+
if (m.direction !== "in" && m.direction !== "out") return null;
|
|
2595
|
+
if (m.gesture !== "volume" && m.gesture !== "build") return null;
|
|
2596
|
+
const effect = m.effect === "stutter" || m.effect === "chopped" || m.effect === "delay" || m.effect === "fade" ? m.effect : void 0;
|
|
2597
|
+
return {
|
|
2598
|
+
direction: m.direction,
|
|
2599
|
+
gesture: m.gesture,
|
|
2600
|
+
effect,
|
|
2601
|
+
sourceTrackDbId: typeof m.sourceTrackDbId === "string" ? m.sourceTrackDbId : "",
|
|
2602
|
+
sourceSceneId: typeof m.sourceSceneId === "string" ? m.sourceSceneId : "",
|
|
2603
|
+
sourceName: typeof m.sourceName === "string" ? m.sourceName : "",
|
|
2604
|
+
soundLabel: typeof m.soundLabel === "string" ? m.soundLabel : "",
|
|
2605
|
+
sliderPos: typeof m.sliderPos === "number" ? m.sliderPos : 0.5
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
function parseFades(sceneData) {
|
|
2609
|
+
const out = [];
|
|
2610
|
+
for (const [key, val] of Object.entries(sceneData)) {
|
|
2611
|
+
const match = /^track:(.+):fade$/.exec(key);
|
|
2612
|
+
if (!match) continue;
|
|
2613
|
+
const meta = asFadeMeta(val);
|
|
2614
|
+
if (!meta) continue;
|
|
2615
|
+
out.push({ dbId: match[1], meta });
|
|
2616
|
+
}
|
|
2617
|
+
return out;
|
|
2618
|
+
}
|
|
2619
|
+
function buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture, steps = 32) {
|
|
2620
|
+
const durationSeconds = bars * 4 * 60 / Math.max(1, bpm);
|
|
2621
|
+
if (gesture === "build") {
|
|
2622
|
+
return [
|
|
2623
|
+
{ time: 0, db: 0 },
|
|
2624
|
+
{ time: Math.round(durationSeconds * 1e3) / 1e3, db: 0 }
|
|
2625
|
+
];
|
|
2626
|
+
}
|
|
2627
|
+
const s = Math.min(0.98, Math.max(0.02, sliderPos));
|
|
2628
|
+
const round = (n) => Math.round(n * 1e3) / 1e3;
|
|
2629
|
+
const points = [];
|
|
2630
|
+
for (let i = 0; i <= steps; i++) {
|
|
2631
|
+
const x = i / steps;
|
|
2632
|
+
const time = round(x * durationSeconds);
|
|
2633
|
+
const theta = x <= s ? x / s * (Math.PI / 4) : Math.PI / 4 + (x - s) / (1 - s) * (Math.PI / 4);
|
|
2634
|
+
const gain = direction === "out" ? Math.cos(theta) : Math.sin(theta);
|
|
2635
|
+
points.push({ time, db: Math.round(gainToDb(gain) * 100) / 100 });
|
|
2636
|
+
}
|
|
2637
|
+
return points;
|
|
2638
|
+
}
|
|
2639
|
+
var TEXTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
2640
|
+
"pads",
|
|
2641
|
+
"pad",
|
|
2642
|
+
"strings",
|
|
2643
|
+
"atmospheres",
|
|
2644
|
+
"atmosphere",
|
|
2645
|
+
"atmos",
|
|
2646
|
+
"drones",
|
|
2647
|
+
"drone",
|
|
2648
|
+
"soundscapes",
|
|
2649
|
+
"soundscape"
|
|
2650
|
+
]);
|
|
2651
|
+
function defaultFadeGesture(role) {
|
|
2652
|
+
if (!role) return "build";
|
|
2653
|
+
const norm = role.toLowerCase().replace(/[\s_-]+/g, " ").trim();
|
|
2654
|
+
if (TEXTURAL_ROLES.has(norm)) return "volume";
|
|
2655
|
+
for (const token of norm.split(" ")) {
|
|
2656
|
+
if (TEXTURAL_ROLES.has(token)) return "volume";
|
|
2657
|
+
}
|
|
2658
|
+
return "build";
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
// src/components/FadeTrackRow.tsx
|
|
2662
|
+
import React10 from "react";
|
|
2663
|
+
import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2664
|
+
function FadeCaption({
|
|
2665
|
+
layer,
|
|
2666
|
+
direction,
|
|
2667
|
+
gesture
|
|
2668
|
+
}) {
|
|
2669
|
+
const tag = direction === "in" ? "Fade in" : "Fade out";
|
|
2670
|
+
return /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
|
|
2671
|
+
/* @__PURE__ */ jsx13("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: tag }),
|
|
2672
|
+
/* @__PURE__ */ jsx13("span", { className: "text-[11px] text-sas-text truncate", title: layer.sourceName ?? layer.name, children: layer.sourceName ?? layer.name }),
|
|
2673
|
+
layer.soundLabel && /* @__PURE__ */ jsxs9("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: layer.soundLabel, children: [
|
|
2674
|
+
"\xB7 ",
|
|
2675
|
+
layer.soundLabel
|
|
2676
|
+
] }),
|
|
2677
|
+
/* @__PURE__ */ jsxs9("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", title: `Fade gesture: ${gesture}`, children: [
|
|
2678
|
+
"\xB7 ",
|
|
2679
|
+
gesture
|
|
2680
|
+
] })
|
|
2681
|
+
] });
|
|
2682
|
+
}
|
|
2683
|
+
function FadeTrackRow({
|
|
2684
|
+
layer,
|
|
2685
|
+
direction,
|
|
2686
|
+
gesture,
|
|
2687
|
+
effect,
|
|
2688
|
+
sliderPos = 0.5,
|
|
2689
|
+
onMuteToggle,
|
|
2690
|
+
onSoloToggle,
|
|
2691
|
+
onVolumeChange,
|
|
2692
|
+
onPanChange,
|
|
2693
|
+
onDelete,
|
|
2694
|
+
onSliderChange,
|
|
2695
|
+
levels,
|
|
2696
|
+
accentColor = "#9333EA"
|
|
2697
|
+
}) {
|
|
2698
|
+
const [confirmDelete, setConfirmDelete] = React10.useState(false);
|
|
2699
|
+
const leftLabel = direction === "in" ? "(silent)" : layer.sourceName ?? layer.name;
|
|
2700
|
+
const rightLabel = direction === "in" ? layer.sourceName ?? layer.name : "(silent)";
|
|
2701
|
+
const verb = effect && effect !== "fade" ? effect.charAt(0).toUpperCase() + effect.slice(1) : "Fade";
|
|
2702
|
+
const badge = direction === "in" ? `\u2197 ${verb} in` : `\u2198 ${verb} out`;
|
|
2703
|
+
return /* @__PURE__ */ jsxs9(
|
|
2704
|
+
"div",
|
|
2705
|
+
{
|
|
2706
|
+
"data-testid": "fade-track-row",
|
|
2707
|
+
className: "w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden",
|
|
2708
|
+
style: { borderLeftColor: accentColor, borderLeftWidth: "3px" },
|
|
2709
|
+
children: [
|
|
2710
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60", children: [
|
|
2711
|
+
/* @__PURE__ */ jsx13(
|
|
2712
|
+
"span",
|
|
2713
|
+
{
|
|
2714
|
+
"data-testid": "fade-direction-badge",
|
|
2715
|
+
className: "text-[10px] font-bold uppercase tracking-wide",
|
|
2716
|
+
style: { color: accentColor },
|
|
2717
|
+
children: badge
|
|
2718
|
+
}
|
|
2719
|
+
),
|
|
2720
|
+
/* @__PURE__ */ jsx13(
|
|
2721
|
+
"button",
|
|
2722
|
+
{
|
|
2723
|
+
"data-testid": "fade-delete-button",
|
|
2724
|
+
onClick: () => setConfirmDelete(true),
|
|
2725
|
+
className: "text-sas-danger/70 hover:text-sas-danger px-1 transition-colors text-sm",
|
|
2726
|
+
title: "Delete fade",
|
|
2727
|
+
"aria-label": "Delete fade",
|
|
2728
|
+
children: "x"
|
|
2729
|
+
}
|
|
2730
|
+
)
|
|
2731
|
+
] }),
|
|
2732
|
+
/* @__PURE__ */ jsx13(
|
|
2733
|
+
TrackRow,
|
|
2734
|
+
{
|
|
2735
|
+
track: { id: layer.trackId, name: "", role: layer.role },
|
|
2736
|
+
runtimeState: layer.runtimeState,
|
|
2737
|
+
fxDetailState: EMPTY_FX_DETAIL_STATE,
|
|
2738
|
+
drawerOpen: false,
|
|
2739
|
+
drawerTab: "fx",
|
|
2740
|
+
levels,
|
|
2741
|
+
accentColor,
|
|
2742
|
+
contentSlot: /* @__PURE__ */ jsx13(FadeCaption, { layer, direction, gesture }),
|
|
2743
|
+
onMuteToggle,
|
|
2744
|
+
onSoloToggle,
|
|
2745
|
+
onVolumeChange,
|
|
2746
|
+
onPanChange
|
|
2747
|
+
}
|
|
2748
|
+
),
|
|
2749
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "fade-slider-row", children: [
|
|
2750
|
+
/* @__PURE__ */ jsx13(
|
|
2751
|
+
"span",
|
|
2752
|
+
{
|
|
2753
|
+
className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0",
|
|
2754
|
+
title: leftLabel,
|
|
2755
|
+
children: leftLabel
|
|
2756
|
+
}
|
|
2757
|
+
),
|
|
2758
|
+
/* @__PURE__ */ jsx13(
|
|
2759
|
+
"input",
|
|
2760
|
+
{
|
|
2761
|
+
type: "range",
|
|
2762
|
+
"data-testid": "fade-slider",
|
|
2763
|
+
min: 0,
|
|
2764
|
+
max: 1,
|
|
2765
|
+
step: 0.01,
|
|
2766
|
+
value: sliderPos,
|
|
2767
|
+
disabled: !onSliderChange,
|
|
2768
|
+
onChange: onSliderChange ? (e) => onSliderChange(Number(e.target.value)) : void 0,
|
|
2769
|
+
style: { accentColor },
|
|
2770
|
+
className: "flex-1 disabled:opacity-60 disabled:cursor-not-allowed",
|
|
2771
|
+
"aria-label": "Fade position"
|
|
2772
|
+
}
|
|
2773
|
+
),
|
|
2774
|
+
/* @__PURE__ */ jsx13(
|
|
2775
|
+
"span",
|
|
2776
|
+
{
|
|
2777
|
+
className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0",
|
|
2778
|
+
title: rightLabel,
|
|
2779
|
+
children: rightLabel
|
|
2780
|
+
}
|
|
2781
|
+
)
|
|
2782
|
+
] }),
|
|
2783
|
+
/* @__PURE__ */ jsx13(
|
|
2784
|
+
ConfirmDialog,
|
|
2785
|
+
{
|
|
2786
|
+
open: confirmDelete,
|
|
2787
|
+
title: "Delete fade?",
|
|
2788
|
+
message: /* @__PURE__ */ jsx13(Fragment3, { children: "This fade track will be permanently removed from this scene. This cannot be undone." }),
|
|
2789
|
+
confirmLabel: "Delete",
|
|
2790
|
+
onConfirm: () => {
|
|
2791
|
+
setConfirmDelete(false);
|
|
2792
|
+
onDelete();
|
|
2793
|
+
},
|
|
2794
|
+
onCancel: () => setConfirmDelete(false),
|
|
2795
|
+
testIdPrefix: "fade-delete-confirm"
|
|
2796
|
+
}
|
|
2797
|
+
)
|
|
2798
|
+
]
|
|
2799
|
+
}
|
|
2800
|
+
);
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
// src/components/FadeModal.tsx
|
|
2804
|
+
import { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef7, useState as useState7 } from "react";
|
|
2805
|
+
import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2806
|
+
function shortId(dbId) {
|
|
2807
|
+
return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
|
|
2808
|
+
}
|
|
2809
|
+
var normRole = (r) => (r ?? "").toLowerCase().trim();
|
|
2810
|
+
function computeOrphans(from, to, excludeSet) {
|
|
2811
|
+
const bucket = (list) => {
|
|
2812
|
+
const m = /* @__PURE__ */ new Map();
|
|
2813
|
+
for (const t of list) {
|
|
2814
|
+
const k = normRole(t.role);
|
|
2815
|
+
const arr = m.get(k);
|
|
2816
|
+
if (arr) arr.push(t);
|
|
2817
|
+
else m.set(k, [t]);
|
|
2818
|
+
}
|
|
2819
|
+
return m;
|
|
2820
|
+
};
|
|
2821
|
+
const fromByRole = bucket(from);
|
|
2822
|
+
const toByRole = bucket(to);
|
|
2823
|
+
const roles = /* @__PURE__ */ new Set([...fromByRole.keys(), ...toByRole.keys()]);
|
|
2824
|
+
const fadeOut = [];
|
|
2825
|
+
const fadeIn = [];
|
|
2826
|
+
for (const role of roles) {
|
|
2827
|
+
const f = fromByRole.get(role) ?? [];
|
|
2828
|
+
const t = toByRole.get(role) ?? [];
|
|
2829
|
+
const shared = Math.min(f.length, t.length);
|
|
2830
|
+
fadeOut.push(...f.slice(shared));
|
|
2831
|
+
fadeIn.push(...t.slice(shared));
|
|
2832
|
+
}
|
|
2833
|
+
return {
|
|
2834
|
+
fadeOut: fadeOut.filter((x) => !excludeSet.has(x.dbId)),
|
|
2835
|
+
fadeIn: fadeIn.filter((x) => !excludeSet.has(x.dbId))
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
function OrphanRow({
|
|
2839
|
+
track,
|
|
2840
|
+
gesture,
|
|
2841
|
+
selected,
|
|
2842
|
+
disabled,
|
|
2843
|
+
onSelect,
|
|
2844
|
+
testId
|
|
2845
|
+
}) {
|
|
2846
|
+
const primary = track.prompt?.trim() || track.name;
|
|
2847
|
+
const meta = [track.role, shortId(track.dbId), gesture].filter(Boolean).join(" \xB7 ");
|
|
2848
|
+
return /* @__PURE__ */ jsxs10(
|
|
2849
|
+
"button",
|
|
2850
|
+
{
|
|
2851
|
+
type: "button",
|
|
2852
|
+
role: "radio",
|
|
2853
|
+
"aria-checked": selected,
|
|
2854
|
+
"data-testid": testId,
|
|
2855
|
+
"data-value": track.dbId,
|
|
2856
|
+
onClick: onSelect,
|
|
2857
|
+
disabled,
|
|
2858
|
+
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"}`,
|
|
2859
|
+
children: [
|
|
2860
|
+
/* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
|
|
2861
|
+
meta && /* @__PURE__ */ jsx14("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
|
|
2862
|
+
]
|
|
2863
|
+
}
|
|
2864
|
+
);
|
|
2865
|
+
}
|
|
2866
|
+
function FadeModal({
|
|
2867
|
+
host,
|
|
2868
|
+
open,
|
|
2869
|
+
fromSceneId,
|
|
2870
|
+
toSceneId,
|
|
2871
|
+
fromSceneName,
|
|
2872
|
+
toSceneName,
|
|
2873
|
+
excludeSourceDbIds,
|
|
2874
|
+
onClose,
|
|
2875
|
+
onCreate,
|
|
2876
|
+
testIdPrefix = "fade-modal"
|
|
2877
|
+
}) {
|
|
2878
|
+
const [load, setLoad] = useState7({ status: "loading" });
|
|
2879
|
+
const [selectedDbId, setSelectedDbId] = useState7("");
|
|
2880
|
+
const [isCreating, setIsCreating] = useState7(false);
|
|
2881
|
+
const [error, setError] = useState7(null);
|
|
2882
|
+
const [fromName, setFromName] = useState7(null);
|
|
2883
|
+
const [toName, setToName] = useState7(null);
|
|
2884
|
+
const cancelRef = useRef7(null);
|
|
2885
|
+
const refresh = useCallback4(async () => {
|
|
2886
|
+
if (!host.listSceneFamilyTracks) {
|
|
2887
|
+
setLoad({ status: "error", message: "This host does not support fades." });
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
setLoad({ status: "loading" });
|
|
2891
|
+
try {
|
|
2892
|
+
const [from, to, fName, tName] = await Promise.all([
|
|
2893
|
+
host.listSceneFamilyTracks(fromSceneId),
|
|
2894
|
+
host.listSceneFamilyTracks(toSceneId),
|
|
2895
|
+
host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),
|
|
2896
|
+
host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null)
|
|
2897
|
+
]);
|
|
2898
|
+
setFromName(fName);
|
|
2899
|
+
setToName(tName);
|
|
2900
|
+
setLoad({ status: "ready", from, to });
|
|
2901
|
+
} catch (err) {
|
|
2902
|
+
setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
|
|
2903
|
+
}
|
|
2904
|
+
}, [host, fromSceneId, toSceneId]);
|
|
2905
|
+
useEffect6(() => {
|
|
2906
|
+
if (open) {
|
|
2907
|
+
setError(null);
|
|
2908
|
+
setIsCreating(false);
|
|
2909
|
+
setSelectedDbId("");
|
|
2910
|
+
void refresh();
|
|
2911
|
+
}
|
|
2912
|
+
}, [open, refresh]);
|
|
2913
|
+
const excludeSet = useMemo3(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
|
|
2914
|
+
const { fadeOut, fadeIn } = useMemo3(
|
|
2915
|
+
() => load.status === "ready" ? computeOrphans(load.from, load.to, excludeSet) : { fadeOut: [], fadeIn: [] },
|
|
2916
|
+
[load, excludeSet]
|
|
2917
|
+
);
|
|
2918
|
+
const allOrphans = useMemo3(
|
|
2919
|
+
() => [
|
|
2920
|
+
...fadeOut.map((t) => ({ track: t, direction: "out" })),
|
|
2921
|
+
...fadeIn.map((t) => ({ track: t, direction: "in" }))
|
|
2922
|
+
],
|
|
2923
|
+
[fadeOut, fadeIn]
|
|
2924
|
+
);
|
|
2925
|
+
useEffect6(() => {
|
|
2926
|
+
if (!allOrphans.some((o) => o.track.dbId === selectedDbId)) {
|
|
2927
|
+
setSelectedDbId(allOrphans[0]?.track.dbId ?? "");
|
|
2928
|
+
}
|
|
2929
|
+
}, [allOrphans, selectedDbId]);
|
|
2930
|
+
const selected = allOrphans.find((o) => o.track.dbId === selectedDbId) ?? null;
|
|
2931
|
+
const canCreate = !isCreating && !!selected;
|
|
2932
|
+
const handleClose = useCallback4(() => {
|
|
2933
|
+
if (!isCreating) onClose();
|
|
2934
|
+
}, [isCreating, onClose]);
|
|
2935
|
+
const handleCreate = useCallback4(async () => {
|
|
2936
|
+
if (!selected) return;
|
|
2937
|
+
setIsCreating(true);
|
|
2938
|
+
setError(null);
|
|
2939
|
+
try {
|
|
2940
|
+
await onCreate(
|
|
2941
|
+
{ dbId: selected.track.dbId, name: selected.track.name, role: selected.track.role },
|
|
2942
|
+
selected.direction,
|
|
2943
|
+
defaultFadeGesture(selected.track.role)
|
|
2944
|
+
);
|
|
2945
|
+
onClose();
|
|
2946
|
+
} catch (err) {
|
|
2947
|
+
setError(err instanceof Error ? err.message : "Failed to create fade.");
|
|
2948
|
+
setIsCreating(false);
|
|
2949
|
+
}
|
|
2950
|
+
}, [selected, onCreate, onClose]);
|
|
2951
|
+
const fromLabel = fromName ?? fromSceneName ?? null;
|
|
2952
|
+
const toLabel = toName ?? toSceneName ?? null;
|
|
2953
|
+
if (!open) return null;
|
|
2954
|
+
const renderSection = (heading, list, section) => {
|
|
2955
|
+
if (list.length === 0) return null;
|
|
2956
|
+
return /* @__PURE__ */ jsxs10("div", { className: "block", children: [
|
|
2957
|
+
/* @__PURE__ */ jsx14("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: heading }),
|
|
2958
|
+
/* @__PURE__ */ jsx14(
|
|
2959
|
+
"div",
|
|
2960
|
+
{
|
|
2961
|
+
role: "radiogroup",
|
|
2962
|
+
"aria-label": heading,
|
|
2963
|
+
"data-testid": `${testIdPrefix}-${section === "out" ? "fade-out" : "fade-in"}-list`,
|
|
2964
|
+
className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
|
|
2965
|
+
children: list.map((t) => /* @__PURE__ */ jsx14(
|
|
2966
|
+
OrphanRow,
|
|
2967
|
+
{
|
|
2968
|
+
track: t,
|
|
2969
|
+
gesture: defaultFadeGesture(t.role),
|
|
2970
|
+
selected: t.dbId === selectedDbId,
|
|
2971
|
+
disabled: isCreating,
|
|
2972
|
+
onSelect: () => setSelectedDbId(t.dbId),
|
|
2973
|
+
testId: `${testIdPrefix}-option-${t.dbId}`
|
|
2974
|
+
},
|
|
2975
|
+
t.dbId
|
|
2976
|
+
))
|
|
2977
|
+
}
|
|
2978
|
+
)
|
|
2979
|
+
] });
|
|
2980
|
+
};
|
|
2981
|
+
return /* @__PURE__ */ jsx14(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs10(
|
|
2982
|
+
"div",
|
|
2983
|
+
{
|
|
2984
|
+
className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
|
|
2985
|
+
onClick: (e) => e.stopPropagation(),
|
|
2986
|
+
"data-testid": `${testIdPrefix}-box`,
|
|
2987
|
+
children: [
|
|
2988
|
+
/* @__PURE__ */ jsx14("h3", { className: "text-sm font-bold text-sas-text", children: "Add fade" }),
|
|
2989
|
+
/* @__PURE__ */ jsxs10("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
|
|
2990
|
+
"Tracks with no counterpart between",
|
|
2991
|
+
" ",
|
|
2992
|
+
/* @__PURE__ */ jsx14("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
|
|
2993
|
+
" and",
|
|
2994
|
+
" ",
|
|
2995
|
+
/* @__PURE__ */ jsx14("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
|
|
2996
|
+
" can gracefully fade out (leaving) or fade in (entering) across this transition."
|
|
2997
|
+
] }),
|
|
2998
|
+
load.status === "loading" && /* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
|
|
2999
|
+
load.status === "error" && /* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
|
|
3000
|
+
load.status === "ready" && (allOrphans.length === 0 ? /* @__PURE__ */ jsx14("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__ */ jsxs10(Fragment4, { children: [
|
|
3001
|
+
renderSection(`Fade out${fromLabel ? ` (from ${fromLabel})` : ""}`, fadeOut, "out"),
|
|
3002
|
+
renderSection(`Fade in${toLabel ? ` (to ${toLabel})` : ""}`, fadeIn, "in")
|
|
3003
|
+
] })),
|
|
3004
|
+
error && /* @__PURE__ */ jsx14("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
|
|
3005
|
+
/* @__PURE__ */ jsxs10("div", { className: "flex justify-end gap-2 pt-1", children: [
|
|
3006
|
+
/* @__PURE__ */ jsx14(
|
|
3007
|
+
"button",
|
|
3008
|
+
{
|
|
3009
|
+
ref: cancelRef,
|
|
3010
|
+
"data-testid": `${testIdPrefix}-cancel`,
|
|
3011
|
+
onClick: onClose,
|
|
3012
|
+
disabled: isCreating,
|
|
3013
|
+
className: "px-3 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-text disabled:opacity-50",
|
|
3014
|
+
children: "Cancel"
|
|
3015
|
+
}
|
|
3016
|
+
),
|
|
3017
|
+
/* @__PURE__ */ jsx14(
|
|
3018
|
+
"button",
|
|
3019
|
+
{
|
|
3020
|
+
"data-testid": `${testIdPrefix}-confirm`,
|
|
3021
|
+
onClick: handleCreate,
|
|
3022
|
+
disabled: !canCreate,
|
|
3023
|
+
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"}`,
|
|
3024
|
+
children: isCreating ? "Generating fade\u2026" : "Create fade"
|
|
3025
|
+
}
|
|
3026
|
+
)
|
|
3027
|
+
] })
|
|
3028
|
+
]
|
|
3029
|
+
}
|
|
3030
|
+
) });
|
|
3031
|
+
}
|
|
3032
|
+
|
|
2573
3033
|
// src/components/ImportTrackModal.tsx
|
|
2574
|
-
import { useCallback as
|
|
2575
|
-
import { jsx as
|
|
3034
|
+
import { useCallback as useCallback5, useEffect as useEffect7, useState as useState8 } from "react";
|
|
3035
|
+
import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2576
3036
|
function ImportTrackModal({
|
|
2577
3037
|
host,
|
|
2578
3038
|
open,
|
|
@@ -2584,10 +3044,10 @@ function ImportTrackModal({
|
|
|
2584
3044
|
onPick,
|
|
2585
3045
|
onPortTrack
|
|
2586
3046
|
}) {
|
|
2587
|
-
const [load, setLoad] =
|
|
2588
|
-
const [selectedSceneId, setSelectedSceneId] =
|
|
2589
|
-
const [importingTrackId, setImportingTrackId] =
|
|
2590
|
-
const refresh =
|
|
3047
|
+
const [load, setLoad] = useState8({ status: "loading" });
|
|
3048
|
+
const [selectedSceneId, setSelectedSceneId] = useState8(null);
|
|
3049
|
+
const [importingTrackId, setImportingTrackId] = useState8(null);
|
|
3050
|
+
const refresh = useCallback5(async () => {
|
|
2591
3051
|
if (!host.listImportableTracks) {
|
|
2592
3052
|
setLoad({ status: "error", message: "This host does not support importing tracks." });
|
|
2593
3053
|
return;
|
|
@@ -2603,14 +3063,14 @@ function ImportTrackModal({
|
|
|
2603
3063
|
setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load scenes." });
|
|
2604
3064
|
}
|
|
2605
3065
|
}, [host, mode, onPortTrack]);
|
|
2606
|
-
|
|
3066
|
+
useEffect7(() => {
|
|
2607
3067
|
if (open) {
|
|
2608
3068
|
setSelectedSceneId(null);
|
|
2609
3069
|
setImportingTrackId(null);
|
|
2610
3070
|
void refresh();
|
|
2611
3071
|
}
|
|
2612
3072
|
}, [open, refresh]);
|
|
2613
|
-
const handleImport =
|
|
3073
|
+
const handleImport = useCallback5(
|
|
2614
3074
|
async (track, sourceSceneId, sceneName, isSameScene) => {
|
|
2615
3075
|
if (isSameScene && onPortTrack) {
|
|
2616
3076
|
if (!track.importable) return;
|
|
@@ -2651,16 +3111,16 @@ function ImportTrackModal({
|
|
|
2651
3111
|
if (!open) return null;
|
|
2652
3112
|
const scenes = load.status === "ready" ? load.scenes : [];
|
|
2653
3113
|
const selectedScene = scenes.find((s) => s.sceneId === selectedSceneId) ?? null;
|
|
2654
|
-
return /* @__PURE__ */
|
|
3114
|
+
return /* @__PURE__ */ jsx15(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ jsxs11(
|
|
2655
3115
|
"div",
|
|
2656
3116
|
{
|
|
2657
3117
|
className: "w-[420px] max-h-[70vh] overflow-hidden flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl",
|
|
2658
3118
|
onClick: (e) => e.stopPropagation(),
|
|
2659
3119
|
"data-testid": `${testIdPrefix}-modal`,
|
|
2660
3120
|
children: [
|
|
2661
|
-
/* @__PURE__ */
|
|
2662
|
-
/* @__PURE__ */
|
|
2663
|
-
selectedScene && /* @__PURE__ */
|
|
3121
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
|
|
3122
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2", children: [
|
|
3123
|
+
selectedScene && /* @__PURE__ */ jsx15(
|
|
2664
3124
|
"button",
|
|
2665
3125
|
{
|
|
2666
3126
|
className: "text-sas-muted hover:text-sas-accent text-xs",
|
|
@@ -2669,9 +3129,9 @@ function ImportTrackModal({
|
|
|
2669
3129
|
children: "\u2190"
|
|
2670
3130
|
}
|
|
2671
3131
|
),
|
|
2672
|
-
/* @__PURE__ */
|
|
3132
|
+
/* @__PURE__ */ jsx15("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
|
|
2673
3133
|
] }),
|
|
2674
|
-
/* @__PURE__ */
|
|
3134
|
+
/* @__PURE__ */ jsx15(
|
|
2675
3135
|
"button",
|
|
2676
3136
|
{
|
|
2677
3137
|
className: "text-sas-muted hover:text-sas-accent text-sm",
|
|
@@ -2681,30 +3141,30 @@ function ImportTrackModal({
|
|
|
2681
3141
|
}
|
|
2682
3142
|
)
|
|
2683
3143
|
] }),
|
|
2684
|
-
/* @__PURE__ */
|
|
2685
|
-
load.status === "loading" && /* @__PURE__ */
|
|
2686
|
-
load.status === "error" && /* @__PURE__ */
|
|
2687
|
-
load.status === "ready" && scenes.length === 0 && /* @__PURE__ */
|
|
2688
|
-
load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */
|
|
3144
|
+
/* @__PURE__ */ jsxs11("div", { className: "overflow-y-auto p-2 flex-1", children: [
|
|
3145
|
+
load.status === "loading" && /* @__PURE__ */ jsx15("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
|
|
3146
|
+
load.status === "error" && /* @__PURE__ */ jsx15("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
|
|
3147
|
+
load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ jsx15("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." }),
|
|
3148
|
+
load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ jsx15("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ jsx15("li", { children: /* @__PURE__ */ jsxs11(
|
|
2689
3149
|
"button",
|
|
2690
3150
|
{
|
|
2691
3151
|
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",
|
|
2692
3152
|
onClick: () => setSelectedSceneId(scene.sceneId),
|
|
2693
3153
|
"data-testid": `${testIdPrefix}-scene`,
|
|
2694
3154
|
children: [
|
|
2695
|
-
/* @__PURE__ */
|
|
2696
|
-
/* @__PURE__ */
|
|
3155
|
+
/* @__PURE__ */ jsx15("span", { className: "truncate", children: scene.sceneName }),
|
|
3156
|
+
/* @__PURE__ */ jsxs11("span", { className: "text-sas-muted", children: [
|
|
2697
3157
|
scene.tracks.length,
|
|
2698
3158
|
" \u2192"
|
|
2699
3159
|
] })
|
|
2700
3160
|
]
|
|
2701
3161
|
}
|
|
2702
3162
|
) }, scene.sceneId)) }),
|
|
2703
|
-
selectedScene && /* @__PURE__ */
|
|
3163
|
+
selectedScene && /* @__PURE__ */ jsx15("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
|
|
2704
3164
|
const busy = importingTrackId === track.trackId;
|
|
2705
3165
|
const gated = mode === "track" && !track.importable;
|
|
2706
3166
|
const disabled = gated || busy;
|
|
2707
|
-
return /* @__PURE__ */
|
|
3167
|
+
return /* @__PURE__ */ jsx15("li", { children: /* @__PURE__ */ jsxs11(
|
|
2708
3168
|
"button",
|
|
2709
3169
|
{
|
|
2710
3170
|
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"}`,
|
|
@@ -2714,14 +3174,14 @@ function ImportTrackModal({
|
|
|
2714
3174
|
"data-testid": `${testIdPrefix}-track`,
|
|
2715
3175
|
"data-importable": mode === "sound" || track.importable ? "true" : "false",
|
|
2716
3176
|
children: [
|
|
2717
|
-
/* @__PURE__ */
|
|
3177
|
+
/* @__PURE__ */ jsxs11("span", { className: "truncate", children: [
|
|
2718
3178
|
track.name,
|
|
2719
|
-
track.role ? /* @__PURE__ */
|
|
3179
|
+
track.role ? /* @__PURE__ */ jsxs11("span", { className: "text-sas-muted", children: [
|
|
2720
3180
|
" \xB7 ",
|
|
2721
3181
|
track.role
|
|
2722
3182
|
] }) : null
|
|
2723
3183
|
] }),
|
|
2724
|
-
busy ? /* @__PURE__ */
|
|
3184
|
+
busy ? /* @__PURE__ */ jsx15("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ jsx15("span", { className: "text-sas-muted", children: "\u2298" }) : null
|
|
2725
3185
|
]
|
|
2726
3186
|
}
|
|
2727
3187
|
) }, track.dbId);
|
|
@@ -2733,29 +3193,59 @@ function ImportTrackModal({
|
|
|
2733
3193
|
}
|
|
2734
3194
|
|
|
2735
3195
|
// src/components/CrossfadeModal.tsx
|
|
2736
|
-
import { useCallback as
|
|
2737
|
-
import { Fragment as
|
|
2738
|
-
function
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
3196
|
+
import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo4, useRef as useRef8, useState as useState9 } from "react";
|
|
3197
|
+
import { Fragment as Fragment5, jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3198
|
+
function shortId2(dbId) {
|
|
3199
|
+
return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
|
|
3200
|
+
}
|
|
3201
|
+
function CandidateRow({
|
|
3202
|
+
track,
|
|
3203
|
+
selected,
|
|
3204
|
+
disabled,
|
|
3205
|
+
onSelect,
|
|
3206
|
+
testId
|
|
3207
|
+
}) {
|
|
3208
|
+
const primary = track.prompt?.trim() || track.name;
|
|
3209
|
+
const meta = [track.role, shortId2(track.dbId)].filter(Boolean).join(" \xB7 ");
|
|
3210
|
+
return /* @__PURE__ */ jsxs12(
|
|
3211
|
+
"button",
|
|
3212
|
+
{
|
|
3213
|
+
type: "button",
|
|
3214
|
+
role: "radio",
|
|
3215
|
+
"aria-checked": selected,
|
|
3216
|
+
"data-testid": testId,
|
|
3217
|
+
"data-value": track.dbId,
|
|
3218
|
+
onClick: onSelect,
|
|
3219
|
+
disabled,
|
|
3220
|
+
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"}`,
|
|
3221
|
+
children: [
|
|
3222
|
+
/* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
|
|
3223
|
+
meta && /* @__PURE__ */ jsx16("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
|
|
3224
|
+
]
|
|
3225
|
+
}
|
|
3226
|
+
);
|
|
3227
|
+
}
|
|
3228
|
+
function CrossfadeModal({
|
|
3229
|
+
host,
|
|
3230
|
+
open,
|
|
3231
|
+
fromSceneId,
|
|
3232
|
+
toSceneId,
|
|
3233
|
+
fromSceneName,
|
|
2744
3234
|
toSceneName,
|
|
2745
3235
|
excludeSourceDbIds,
|
|
2746
3236
|
onClose,
|
|
2747
3237
|
onCreate,
|
|
2748
3238
|
testIdPrefix = "crossfade-modal"
|
|
2749
3239
|
}) {
|
|
2750
|
-
const [load, setLoad] =
|
|
2751
|
-
const [originDbId, setOriginDbId] =
|
|
2752
|
-
const [targetDbId, setTargetDbId] =
|
|
2753
|
-
const [isCreating, setIsCreating] =
|
|
2754
|
-
const [error, setError] =
|
|
2755
|
-
const [fromName, setFromName] =
|
|
2756
|
-
const [toName, setToName] =
|
|
2757
|
-
const cancelRef =
|
|
2758
|
-
const refresh =
|
|
3240
|
+
const [load, setLoad] = useState9({ status: "loading" });
|
|
3241
|
+
const [originDbId, setOriginDbId] = useState9("");
|
|
3242
|
+
const [targetDbId, setTargetDbId] = useState9("");
|
|
3243
|
+
const [isCreating, setIsCreating] = useState9(false);
|
|
3244
|
+
const [error, setError] = useState9(null);
|
|
3245
|
+
const [fromName, setFromName] = useState9(null);
|
|
3246
|
+
const [toName, setToName] = useState9(null);
|
|
3247
|
+
const cancelRef = useRef8(null);
|
|
3248
|
+
const refresh = useCallback6(async () => {
|
|
2759
3249
|
if (!host.listSceneFamilyTracks) {
|
|
2760
3250
|
setLoad({ status: "error", message: "This host does not support crossfade tracks." });
|
|
2761
3251
|
return;
|
|
@@ -2775,7 +3265,7 @@ function CrossfadeModal({
|
|
|
2775
3265
|
setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
|
|
2776
3266
|
}
|
|
2777
3267
|
}, [host, fromSceneId, toSceneId]);
|
|
2778
|
-
|
|
3268
|
+
useEffect8(() => {
|
|
2779
3269
|
if (open) {
|
|
2780
3270
|
setError(null);
|
|
2781
3271
|
setIsCreating(false);
|
|
@@ -2784,21 +3274,21 @@ function CrossfadeModal({
|
|
|
2784
3274
|
void refresh();
|
|
2785
3275
|
}
|
|
2786
3276
|
}, [open, refresh]);
|
|
2787
|
-
const excludeSet =
|
|
2788
|
-
const originCandidates =
|
|
3277
|
+
const excludeSet = useMemo4(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
|
|
3278
|
+
const originCandidates = useMemo4(
|
|
2789
3279
|
() => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
2790
3280
|
[load, excludeSet]
|
|
2791
3281
|
);
|
|
2792
|
-
const targetCandidates =
|
|
3282
|
+
const targetCandidates = useMemo4(
|
|
2793
3283
|
() => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
2794
3284
|
[load, excludeSet]
|
|
2795
3285
|
);
|
|
2796
|
-
|
|
3286
|
+
useEffect8(() => {
|
|
2797
3287
|
if (!originCandidates.some((t) => t.dbId === originDbId)) {
|
|
2798
3288
|
setOriginDbId(originCandidates[0]?.dbId ?? "");
|
|
2799
3289
|
}
|
|
2800
3290
|
}, [originCandidates, originDbId]);
|
|
2801
|
-
|
|
3291
|
+
useEffect8(() => {
|
|
2802
3292
|
if (!targetCandidates.some((t) => t.dbId === targetDbId)) {
|
|
2803
3293
|
setTargetDbId(targetCandidates[0]?.dbId ?? "");
|
|
2804
3294
|
}
|
|
@@ -2806,10 +3296,10 @@ function CrossfadeModal({
|
|
|
2806
3296
|
const originTrack = originCandidates.find((t) => t.dbId === originDbId) ?? null;
|
|
2807
3297
|
const targetTrack = targetCandidates.find((t) => t.dbId === targetDbId) ?? null;
|
|
2808
3298
|
const canCreate = !isCreating && !!originTrack && !!targetTrack;
|
|
2809
|
-
const handleClose =
|
|
3299
|
+
const handleClose = useCallback6(() => {
|
|
2810
3300
|
if (!isCreating) onClose();
|
|
2811
3301
|
}, [isCreating, onClose]);
|
|
2812
|
-
const handleCreate =
|
|
3302
|
+
const handleCreate = useCallback6(async () => {
|
|
2813
3303
|
if (!originTrack || !targetTrack) return;
|
|
2814
3304
|
setIsCreating(true);
|
|
2815
3305
|
setError(null);
|
|
@@ -2827,26 +3317,26 @@ function CrossfadeModal({
|
|
|
2827
3317
|
const fromLabel = fromName ?? fromSceneName ?? null;
|
|
2828
3318
|
const toLabel = toName ?? toSceneName ?? null;
|
|
2829
3319
|
if (!open) return null;
|
|
2830
|
-
return /* @__PURE__ */
|
|
3320
|
+
return /* @__PURE__ */ jsx16(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ jsxs12(
|
|
2831
3321
|
"div",
|
|
2832
3322
|
{
|
|
2833
3323
|
className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
|
|
2834
3324
|
onClick: (e) => e.stopPropagation(),
|
|
2835
3325
|
"data-testid": `${testIdPrefix}-box`,
|
|
2836
3326
|
children: [
|
|
2837
|
-
/* @__PURE__ */
|
|
2838
|
-
/* @__PURE__ */
|
|
3327
|
+
/* @__PURE__ */ jsx16("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
|
|
3328
|
+
/* @__PURE__ */ jsxs12("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
|
|
2839
3329
|
"Bridge a track from",
|
|
2840
3330
|
" ",
|
|
2841
|
-
/* @__PURE__ */
|
|
3331
|
+
/* @__PURE__ */ jsx16("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
|
|
2842
3332
|
" into one from",
|
|
2843
3333
|
" ",
|
|
2844
|
-
/* @__PURE__ */
|
|
3334
|
+
/* @__PURE__ */ jsx16("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
|
|
2845
3335
|
". Both layers share one generated part; each keeps its own preset."
|
|
2846
3336
|
] }),
|
|
2847
|
-
load.status === "loading" && /* @__PURE__ */
|
|
2848
|
-
load.status === "error" && /* @__PURE__ */
|
|
2849
|
-
load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */
|
|
3337
|
+
load.status === "loading" && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
|
|
3338
|
+
load.status === "error" && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
|
|
3339
|
+
load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ jsxs12(
|
|
2850
3340
|
"div",
|
|
2851
3341
|
{
|
|
2852
3342
|
className: "text-xs text-sas-muted py-4 text-center",
|
|
@@ -2857,55 +3347,67 @@ function CrossfadeModal({
|
|
|
2857
3347
|
". Add one (or free one from another crossfade) first."
|
|
2858
3348
|
]
|
|
2859
3349
|
}
|
|
2860
|
-
) : /* @__PURE__ */
|
|
2861
|
-
/* @__PURE__ */
|
|
2862
|
-
/* @__PURE__ */
|
|
3350
|
+
) : /* @__PURE__ */ jsxs12(Fragment5, { children: [
|
|
3351
|
+
/* @__PURE__ */ jsxs12("div", { className: "block", children: [
|
|
3352
|
+
/* @__PURE__ */ jsxs12("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
|
|
2863
3353
|
"Origin ",
|
|
2864
3354
|
fromLabel ? `(${fromLabel})` : "(top)"
|
|
2865
3355
|
] }),
|
|
2866
|
-
/* @__PURE__ */
|
|
2867
|
-
"
|
|
3356
|
+
/* @__PURE__ */ jsx16(
|
|
3357
|
+
"div",
|
|
2868
3358
|
{
|
|
2869
|
-
"
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
3359
|
+
role: "radiogroup",
|
|
3360
|
+
"aria-label": "Origin track",
|
|
3361
|
+
"data-testid": `${testIdPrefix}-origin-list`,
|
|
3362
|
+
className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
|
|
3363
|
+
children: originCandidates.map((t) => /* @__PURE__ */ jsx16(
|
|
3364
|
+
CandidateRow,
|
|
3365
|
+
{
|
|
3366
|
+
track: t,
|
|
3367
|
+
selected: t.dbId === originDbId,
|
|
3368
|
+
disabled: isCreating,
|
|
3369
|
+
onSelect: () => setOriginDbId(t.dbId),
|
|
3370
|
+
testId: `${testIdPrefix}-origin-option-${t.dbId}`
|
|
3371
|
+
},
|
|
3372
|
+
t.dbId
|
|
3373
|
+
))
|
|
2878
3374
|
}
|
|
2879
3375
|
)
|
|
2880
3376
|
] }),
|
|
2881
|
-
/* @__PURE__ */
|
|
2882
|
-
/* @__PURE__ */
|
|
3377
|
+
/* @__PURE__ */ jsxs12("div", { className: "block", children: [
|
|
3378
|
+
/* @__PURE__ */ jsxs12("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
|
|
2883
3379
|
"Target ",
|
|
2884
3380
|
toLabel ? `(${toLabel})` : "(bottom)"
|
|
2885
3381
|
] }),
|
|
2886
|
-
targetCandidates.length === 0 ? /* @__PURE__ */
|
|
3382
|
+
targetCandidates.length === 0 ? /* @__PURE__ */ jsxs12("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
|
|
2887
3383
|
"No available tracks in ",
|
|
2888
3384
|
toLabel ?? "the target scene",
|
|
2889
3385
|
" to crossfade into."
|
|
2890
|
-
] }) : /* @__PURE__ */
|
|
2891
|
-
"
|
|
3386
|
+
] }) : /* @__PURE__ */ jsx16(
|
|
3387
|
+
"div",
|
|
2892
3388
|
{
|
|
2893
|
-
"
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
3389
|
+
role: "radiogroup",
|
|
3390
|
+
"aria-label": "Target track",
|
|
3391
|
+
"data-testid": `${testIdPrefix}-target-list`,
|
|
3392
|
+
className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
|
|
3393
|
+
children: targetCandidates.map((t) => /* @__PURE__ */ jsx16(
|
|
3394
|
+
CandidateRow,
|
|
3395
|
+
{
|
|
3396
|
+
track: t,
|
|
3397
|
+
selected: t.dbId === targetDbId,
|
|
3398
|
+
disabled: isCreating,
|
|
3399
|
+
onSelect: () => setTargetDbId(t.dbId),
|
|
3400
|
+
testId: `${testIdPrefix}-target-option-${t.dbId}`
|
|
3401
|
+
},
|
|
3402
|
+
t.dbId
|
|
3403
|
+
))
|
|
2902
3404
|
}
|
|
2903
3405
|
)
|
|
2904
3406
|
] })
|
|
2905
3407
|
] })),
|
|
2906
|
-
error && /* @__PURE__ */
|
|
2907
|
-
/* @__PURE__ */
|
|
2908
|
-
/* @__PURE__ */
|
|
3408
|
+
error && /* @__PURE__ */ jsx16("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
|
|
3409
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex justify-end gap-2 pt-1", children: [
|
|
3410
|
+
/* @__PURE__ */ jsx16(
|
|
2909
3411
|
"button",
|
|
2910
3412
|
{
|
|
2911
3413
|
ref: cancelRef,
|
|
@@ -2916,7 +3418,7 @@ function CrossfadeModal({
|
|
|
2916
3418
|
children: "Cancel"
|
|
2917
3419
|
}
|
|
2918
3420
|
),
|
|
2919
|
-
/* @__PURE__ */
|
|
3421
|
+
/* @__PURE__ */ jsx16(
|
|
2920
3422
|
"button",
|
|
2921
3423
|
{
|
|
2922
3424
|
"data-testid": `${testIdPrefix}-confirm`,
|
|
@@ -2932,9 +3434,701 @@ function CrossfadeModal({
|
|
|
2932
3434
|
) });
|
|
2933
3435
|
}
|
|
2934
3436
|
|
|
3437
|
+
// src/components/TransitionDesigner.tsx
|
|
3438
|
+
import { useCallback as useCallback8, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef10, useState as useState11 } from "react";
|
|
3439
|
+
|
|
3440
|
+
// src/hooks/useTrackReorder.ts
|
|
3441
|
+
import { useCallback as useCallback7, useRef as useRef9, useState as useState10 } from "react";
|
|
3442
|
+
function moveItem(arr, from, to) {
|
|
3443
|
+
const next = arr.slice();
|
|
3444
|
+
if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
|
|
3445
|
+
return next;
|
|
3446
|
+
}
|
|
3447
|
+
const [moved] = next.splice(from, 1);
|
|
3448
|
+
next.splice(to, 0, moved);
|
|
3449
|
+
return next;
|
|
3450
|
+
}
|
|
3451
|
+
function useTrackReorder({
|
|
3452
|
+
host,
|
|
3453
|
+
items,
|
|
3454
|
+
setItems,
|
|
3455
|
+
getId,
|
|
3456
|
+
onError
|
|
3457
|
+
}) {
|
|
3458
|
+
const [draggingIndex, setDraggingIndex] = useState10(null);
|
|
3459
|
+
const [dragOverIndex, setDragOverIndex] = useState10(null);
|
|
3460
|
+
const fromRef = useRef9(null);
|
|
3461
|
+
const itemsRef = useRef9(items);
|
|
3462
|
+
itemsRef.current = items;
|
|
3463
|
+
const dragPropsFor = useCallback7(
|
|
3464
|
+
(index) => ({
|
|
3465
|
+
handleProps: {
|
|
3466
|
+
draggable: true,
|
|
3467
|
+
onDragStart: (e) => {
|
|
3468
|
+
fromRef.current = index;
|
|
3469
|
+
setDraggingIndex(index);
|
|
3470
|
+
if (e.dataTransfer) {
|
|
3471
|
+
e.dataTransfer.effectAllowed = "move";
|
|
3472
|
+
try {
|
|
3473
|
+
e.dataTransfer.setData("text/plain", String(index));
|
|
3474
|
+
} catch {
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
},
|
|
3478
|
+
onDragEnd: () => {
|
|
3479
|
+
fromRef.current = null;
|
|
3480
|
+
setDraggingIndex(null);
|
|
3481
|
+
setDragOverIndex(null);
|
|
3482
|
+
}
|
|
3483
|
+
},
|
|
3484
|
+
rowProps: {
|
|
3485
|
+
onDragEnter: (e) => {
|
|
3486
|
+
if (fromRef.current === null) return;
|
|
3487
|
+
e.preventDefault();
|
|
3488
|
+
setDragOverIndex(index);
|
|
3489
|
+
},
|
|
3490
|
+
onDragOver: (e) => {
|
|
3491
|
+
if (fromRef.current === null) return;
|
|
3492
|
+
e.preventDefault();
|
|
3493
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
3494
|
+
setDragOverIndex((cur) => cur === index ? cur : index);
|
|
3495
|
+
},
|
|
3496
|
+
onDragLeave: () => {
|
|
3497
|
+
setDragOverIndex((cur) => cur === index ? null : cur);
|
|
3498
|
+
},
|
|
3499
|
+
onDrop: (e) => {
|
|
3500
|
+
e.preventDefault();
|
|
3501
|
+
const from = fromRef.current;
|
|
3502
|
+
fromRef.current = null;
|
|
3503
|
+
setDraggingIndex(null);
|
|
3504
|
+
setDragOverIndex(null);
|
|
3505
|
+
if (from === null || from === index) return;
|
|
3506
|
+
const prev = itemsRef.current;
|
|
3507
|
+
const next = moveItem(prev, from, index);
|
|
3508
|
+
setItems(next);
|
|
3509
|
+
const ids = next.map(getId);
|
|
3510
|
+
Promise.resolve(host.reorderTracks(ids)).catch((err) => {
|
|
3511
|
+
setItems(prev);
|
|
3512
|
+
onError?.(err);
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
},
|
|
3516
|
+
isDragging: draggingIndex === index,
|
|
3517
|
+
isDragTarget: dragOverIndex === index && draggingIndex !== index
|
|
3518
|
+
}),
|
|
3519
|
+
[host, setItems, getId, onError, draggingIndex, dragOverIndex]
|
|
3520
|
+
);
|
|
3521
|
+
return { dragPropsFor, draggingIndex, dragOverIndex };
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
// src/transition-designer-meta.ts
|
|
3525
|
+
var TRANSITION_DESIGNER_DRAFT_KEY = "transitionDesigner:draft";
|
|
3526
|
+
var AUDIO_EFFECTS = ["fade", "stutter", "chopped", "delay"];
|
|
3527
|
+
var AUDIO_EFFECT_LABEL = {
|
|
3528
|
+
fade: "Fade",
|
|
3529
|
+
stutter: "Stutter",
|
|
3530
|
+
chopped: "Chopped",
|
|
3531
|
+
delay: "Delay"
|
|
3532
|
+
};
|
|
3533
|
+
function asAudioEffect(v) {
|
|
3534
|
+
return v === "fade" || v === "stutter" || v === "chopped" || v === "delay" ? v : null;
|
|
3535
|
+
}
|
|
3536
|
+
function rowType(hasOrigin, hasTarget) {
|
|
3537
|
+
if (hasOrigin && hasTarget) return "crossfade";
|
|
3538
|
+
if (hasOrigin) return "fade-out";
|
|
3539
|
+
if (hasTarget) return "fade-in";
|
|
3540
|
+
return null;
|
|
3541
|
+
}
|
|
3542
|
+
function asTransitionDesignerDraft(val) {
|
|
3543
|
+
if (!val || typeof val !== "object") return null;
|
|
3544
|
+
const d = val;
|
|
3545
|
+
const clean = (a) => Array.isArray(a) ? a.filter((x) => x === null || typeof x === "string") : [];
|
|
3546
|
+
const cleanEffects = (e) => {
|
|
3547
|
+
const out = {};
|
|
3548
|
+
if (e && typeof e === "object") {
|
|
3549
|
+
for (const [k, v] of Object.entries(e)) {
|
|
3550
|
+
const eff = asAudioEffect(v);
|
|
3551
|
+
if (eff) out[k] = eff;
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
return out;
|
|
3555
|
+
};
|
|
3556
|
+
return {
|
|
3557
|
+
originOrder: clean(d.originOrder),
|
|
3558
|
+
targetOrder: clean(d.targetOrder),
|
|
3559
|
+
rowEffects: cleanEffects(d.rowEffects)
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
function reconcileSlots(saved, poolIds) {
|
|
3563
|
+
const pool = new Set(poolIds);
|
|
3564
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3565
|
+
const out = [];
|
|
3566
|
+
for (const slot of saved ?? []) {
|
|
3567
|
+
if (slot === null) {
|
|
3568
|
+
out.push(null);
|
|
3569
|
+
continue;
|
|
3570
|
+
}
|
|
3571
|
+
if (pool.has(slot) && !seen.has(slot)) {
|
|
3572
|
+
out.push(slot);
|
|
3573
|
+
seen.add(slot);
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
for (const id of poolIds) {
|
|
3577
|
+
if (!seen.has(id)) {
|
|
3578
|
+
out.push(id);
|
|
3579
|
+
seen.add(id);
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
return out;
|
|
3583
|
+
}
|
|
3584
|
+
function buildRowSlots(originSlots, targetSlots) {
|
|
3585
|
+
const n = Math.max(originSlots.length, targetSlots.length);
|
|
3586
|
+
const rows = [];
|
|
3587
|
+
for (let i = 0; i < n; i++) {
|
|
3588
|
+
const originId = originSlots[i] ?? null;
|
|
3589
|
+
const targetId = targetSlots[i] ?? null;
|
|
3590
|
+
rows.push({ originId, targetId, type: rowType(originId !== null, targetId !== null) });
|
|
3591
|
+
}
|
|
3592
|
+
return rows;
|
|
3593
|
+
}
|
|
3594
|
+
function normalizeSlots(originSlots, targetSlots) {
|
|
3595
|
+
const rows = buildRowSlots(originSlots, targetSlots).filter(
|
|
3596
|
+
(r) => r.originId !== null || r.targetId !== null
|
|
3597
|
+
);
|
|
3598
|
+
const trimTrailing = (a) => {
|
|
3599
|
+
let end = a.length;
|
|
3600
|
+
while (end > 0 && a[end - 1] === null) end--;
|
|
3601
|
+
return a.slice(0, end);
|
|
3602
|
+
};
|
|
3603
|
+
return {
|
|
3604
|
+
originOrder: trimTrailing(rows.map((r) => r.originId)),
|
|
3605
|
+
targetOrder: trimTrailing(rows.map((r) => r.targetId))
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3608
|
+
function padSlots(slots, n) {
|
|
3609
|
+
if (slots.length >= n) return slots.slice();
|
|
3610
|
+
return [...slots, ...new Array(n - slots.length).fill(null)];
|
|
3611
|
+
}
|
|
3612
|
+
function padPair(originSlots, targetSlots) {
|
|
3613
|
+
const n = Math.max(originSlots.length, targetSlots.length);
|
|
3614
|
+
return [padSlots(originSlots, n), padSlots(targetSlots, n)];
|
|
3615
|
+
}
|
|
3616
|
+
function slotsEqual(a, b) {
|
|
3617
|
+
if (a.length !== b.length) return false;
|
|
3618
|
+
for (let i = 0; i < a.length; i++) {
|
|
3619
|
+
if (a[i] !== b[i]) return false;
|
|
3620
|
+
}
|
|
3621
|
+
return true;
|
|
3622
|
+
}
|
|
3623
|
+
function rowKey(row) {
|
|
3624
|
+
if (row.type === "crossfade") return `xf:${row.originId}|${row.targetId}`;
|
|
3625
|
+
if (row.type === "fade-out") return `fo:${row.originId}`;
|
|
3626
|
+
if (row.type === "fade-in") return `fi:${row.targetId}`;
|
|
3627
|
+
return null;
|
|
3628
|
+
}
|
|
3629
|
+
function dbIdsFromKeys(keys) {
|
|
3630
|
+
const out = /* @__PURE__ */ new Set();
|
|
3631
|
+
for (const k of keys) {
|
|
3632
|
+
const body = k.slice(3);
|
|
3633
|
+
if (k.startsWith("xf:")) {
|
|
3634
|
+
const sep = body.indexOf("|");
|
|
3635
|
+
out.add(body.slice(0, sep));
|
|
3636
|
+
out.add(body.slice(sep + 1));
|
|
3637
|
+
} else {
|
|
3638
|
+
out.add(body);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
return out;
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
// src/components/TransitionDesigner.tsx
|
|
3645
|
+
import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3646
|
+
var CROSSFADE_ESTIMATE_MS = 15e3;
|
|
3647
|
+
var FADE_ESTIMATE_MS = 11e3;
|
|
3648
|
+
var CREATE_ALL_CONCURRENCY = 5;
|
|
3649
|
+
var TYPE_LABEL = {
|
|
3650
|
+
crossfade: "Crossfade",
|
|
3651
|
+
"fade-out": "Fade out",
|
|
3652
|
+
"fade-in": "Fade in"
|
|
3653
|
+
};
|
|
3654
|
+
function shortId3(dbId) {
|
|
3655
|
+
return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
|
|
3656
|
+
}
|
|
3657
|
+
function TransitionDesigner({
|
|
3658
|
+
host,
|
|
3659
|
+
fromSceneId,
|
|
3660
|
+
toSceneId,
|
|
3661
|
+
transitionSceneId,
|
|
3662
|
+
excludeSourceDbIds,
|
|
3663
|
+
onCreateCrossfade,
|
|
3664
|
+
onCreateFade,
|
|
3665
|
+
onCreateAudioTransition,
|
|
3666
|
+
familyLabel,
|
|
3667
|
+
testIdPrefix = "transition-designer"
|
|
3668
|
+
}) {
|
|
3669
|
+
const [load, setLoad] = useState11({ status: "loading" });
|
|
3670
|
+
const [fromName, setFromName] = useState11(null);
|
|
3671
|
+
const [toName, setToName] = useState11(null);
|
|
3672
|
+
const [originSlots, setOriginSlots] = useState11([]);
|
|
3673
|
+
const [targetSlots, setTargetSlots] = useState11([]);
|
|
3674
|
+
const [creatingKeys, setCreatingKeys] = useState11(() => /* @__PURE__ */ new Set());
|
|
3675
|
+
const [rowErrors, setRowErrors] = useState11({});
|
|
3676
|
+
const [rowEffects, setRowEffects] = useState11({});
|
|
3677
|
+
const rowEffectsRef = useRef10(rowEffects);
|
|
3678
|
+
rowEffectsRef.current = rowEffects;
|
|
3679
|
+
const audioEffectsEnabled = !!onCreateAudioTransition;
|
|
3680
|
+
const excludeRef = useRef10(excludeSourceDbIds);
|
|
3681
|
+
excludeRef.current = excludeSourceDbIds;
|
|
3682
|
+
const originSlotsRef = useRef10(originSlots);
|
|
3683
|
+
originSlotsRef.current = originSlots;
|
|
3684
|
+
const targetSlotsRef = useRef10(targetSlots);
|
|
3685
|
+
targetSlotsRef.current = targetSlots;
|
|
3686
|
+
const creatingKeysRef = useRef10(creatingKeys);
|
|
3687
|
+
creatingKeysRef.current = creatingKeys;
|
|
3688
|
+
const dragRef = useRef10(null);
|
|
3689
|
+
const [dragging, setDragging] = useState11(null);
|
|
3690
|
+
const [dragOver, setDragOver] = useState11(null);
|
|
3691
|
+
const excludeSet = useMemo5(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
|
|
3692
|
+
const originPool = useMemo5(
|
|
3693
|
+
() => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
3694
|
+
[load, excludeSet]
|
|
3695
|
+
);
|
|
3696
|
+
const targetPool = useMemo5(
|
|
3697
|
+
() => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
|
|
3698
|
+
[load, excludeSet]
|
|
3699
|
+
);
|
|
3700
|
+
const originById = useMemo5(() => new Map(originPool.map((t) => [t.dbId, t])), [originPool]);
|
|
3701
|
+
const targetById = useMemo5(() => new Map(targetPool.map((t) => [t.dbId, t])), [targetPool]);
|
|
3702
|
+
const originByIdRef = useRef10(originById);
|
|
3703
|
+
originByIdRef.current = originById;
|
|
3704
|
+
const targetByIdRef = useRef10(targetById);
|
|
3705
|
+
targetByIdRef.current = targetById;
|
|
3706
|
+
const refresh = useCallback8(async () => {
|
|
3707
|
+
if (!host.listSceneFamilyTracks) {
|
|
3708
|
+
setLoad({ status: "error", message: "This host does not support transition tracks." });
|
|
3709
|
+
return;
|
|
3710
|
+
}
|
|
3711
|
+
setLoad({ status: "loading" });
|
|
3712
|
+
try {
|
|
3713
|
+
const [origin, target, fName, tName, draftRaw] = await Promise.all([
|
|
3714
|
+
host.listSceneFamilyTracks(fromSceneId),
|
|
3715
|
+
host.listSceneFamilyTracks(toSceneId),
|
|
3716
|
+
host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),
|
|
3717
|
+
host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null),
|
|
3718
|
+
host.getSceneData ? host.getSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY) : Promise.resolve(null)
|
|
3719
|
+
]);
|
|
3720
|
+
const draft = asTransitionDesignerDraft(draftRaw);
|
|
3721
|
+
const exSet = new Set(excludeRef.current ?? []);
|
|
3722
|
+
const originIds = origin.filter((t) => !exSet.has(t.dbId)).map((t) => t.dbId);
|
|
3723
|
+
const targetIds = target.filter((t) => !exSet.has(t.dbId)).map((t) => t.dbId);
|
|
3724
|
+
const [po, pt] = padPair(
|
|
3725
|
+
reconcileSlots(draft?.originOrder, originIds),
|
|
3726
|
+
reconcileSlots(draft?.targetOrder, targetIds)
|
|
3727
|
+
);
|
|
3728
|
+
setOriginSlots(po);
|
|
3729
|
+
setTargetSlots(pt);
|
|
3730
|
+
setRowEffects(draft?.rowEffects ?? {});
|
|
3731
|
+
setFromName(fName);
|
|
3732
|
+
setToName(tName);
|
|
3733
|
+
setLoad({ status: "ready", origin, target });
|
|
3734
|
+
} catch (err) {
|
|
3735
|
+
setLoad({
|
|
3736
|
+
status: "error",
|
|
3737
|
+
message: err instanceof Error ? err.message : "Failed to load tracks."
|
|
3738
|
+
});
|
|
3739
|
+
}
|
|
3740
|
+
}, [host, fromSceneId, toSceneId, transitionSceneId]);
|
|
3741
|
+
useEffect9(() => {
|
|
3742
|
+
void refresh();
|
|
3743
|
+
}, [refresh]);
|
|
3744
|
+
useEffect9(() => {
|
|
3745
|
+
if (load.status !== "ready") return;
|
|
3746
|
+
const [po, pt] = padPair(
|
|
3747
|
+
reconcileSlots(originSlotsRef.current, originPool.map((t) => t.dbId)),
|
|
3748
|
+
reconcileSlots(targetSlotsRef.current, targetPool.map((t) => t.dbId))
|
|
3749
|
+
);
|
|
3750
|
+
if (!slotsEqual(po, originSlotsRef.current)) setOriginSlots(po);
|
|
3751
|
+
if (!slotsEqual(pt, targetSlotsRef.current)) setTargetSlots(pt);
|
|
3752
|
+
}, [originPool, targetPool, load.status]);
|
|
3753
|
+
const mutate = useCallback8(
|
|
3754
|
+
(nextOrigin, nextTarget) => {
|
|
3755
|
+
const norm = normalizeSlots(nextOrigin, nextTarget);
|
|
3756
|
+
const [po, pt] = padPair(norm.originOrder, norm.targetOrder);
|
|
3757
|
+
setOriginSlots(po);
|
|
3758
|
+
setTargetSlots(pt);
|
|
3759
|
+
if (host.setSceneData) {
|
|
3760
|
+
host.setSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY, { ...norm, rowEffects: rowEffectsRef.current }).catch(() => {
|
|
3761
|
+
});
|
|
3762
|
+
}
|
|
3763
|
+
},
|
|
3764
|
+
[host, transitionSceneId]
|
|
3765
|
+
);
|
|
3766
|
+
const setRowEffect = useCallback8(
|
|
3767
|
+
(sourceDbId, effect) => {
|
|
3768
|
+
setRowEffects((prev) => {
|
|
3769
|
+
const next = { ...prev, [sourceDbId]: effect };
|
|
3770
|
+
if (host.setSceneData) {
|
|
3771
|
+
const norm = normalizeSlots(originSlotsRef.current, targetSlotsRef.current);
|
|
3772
|
+
host.setSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY, { ...norm, rowEffects: next }).catch(() => {
|
|
3773
|
+
});
|
|
3774
|
+
}
|
|
3775
|
+
return next;
|
|
3776
|
+
});
|
|
3777
|
+
},
|
|
3778
|
+
[host, transitionSceneId]
|
|
3779
|
+
);
|
|
3780
|
+
const insertGapAbove = useCallback8(
|
|
3781
|
+
(col, index) => {
|
|
3782
|
+
const slots = col === "origin" ? originSlots : targetSlots;
|
|
3783
|
+
const next = [...slots.slice(0, index), null, ...slots.slice(index)];
|
|
3784
|
+
if (col === "origin") mutate(next, targetSlots);
|
|
3785
|
+
else mutate(originSlots, next);
|
|
3786
|
+
},
|
|
3787
|
+
[originSlots, targetSlots, mutate]
|
|
3788
|
+
);
|
|
3789
|
+
const removeGap = useCallback8(
|
|
3790
|
+
(col, index) => {
|
|
3791
|
+
const slots = col === "origin" ? originSlots : targetSlots;
|
|
3792
|
+
const next = slots.filter((_, i) => i !== index);
|
|
3793
|
+
if (col === "origin") mutate(next, targetSlots);
|
|
3794
|
+
else mutate(originSlots, next);
|
|
3795
|
+
},
|
|
3796
|
+
[originSlots, targetSlots, mutate]
|
|
3797
|
+
);
|
|
3798
|
+
const handleDrop = useCallback8(
|
|
3799
|
+
(col, to) => {
|
|
3800
|
+
const from = dragRef.current;
|
|
3801
|
+
dragRef.current = null;
|
|
3802
|
+
setDragging(null);
|
|
3803
|
+
setDragOver(null);
|
|
3804
|
+
if (!from || from.col !== col || from.index === to) return;
|
|
3805
|
+
if (col === "origin") mutate(moveItem(originSlots, from.index, to), targetSlots);
|
|
3806
|
+
else mutate(originSlots, moveItem(targetSlots, from.index, to));
|
|
3807
|
+
},
|
|
3808
|
+
[originSlots, targetSlots, mutate]
|
|
3809
|
+
);
|
|
3810
|
+
const rows = useMemo5(() => buildRowSlots(originSlots, targetSlots), [originSlots, targetSlots]);
|
|
3811
|
+
const creatingDbIds = useMemo5(() => dbIdsFromKeys(creatingKeys), [creatingKeys]);
|
|
3812
|
+
const eligibleCount = useMemo5(
|
|
3813
|
+
() => rows.filter((r) => {
|
|
3814
|
+
const k = rowKey(r);
|
|
3815
|
+
return k !== null && !creatingKeys.has(k);
|
|
3816
|
+
}).length,
|
|
3817
|
+
[rows, creatingKeys]
|
|
3818
|
+
);
|
|
3819
|
+
const createRow = useCallback8(
|
|
3820
|
+
async (row) => {
|
|
3821
|
+
const key = rowKey(row);
|
|
3822
|
+
if (!key || !row.type || creatingKeysRef.current.has(key)) return;
|
|
3823
|
+
setCreatingKeys((prev) => new Set(prev).add(key));
|
|
3824
|
+
setRowErrors((prev) => {
|
|
3825
|
+
if (!(key in prev)) return prev;
|
|
3826
|
+
const next = { ...prev };
|
|
3827
|
+
delete next[key];
|
|
3828
|
+
return next;
|
|
3829
|
+
});
|
|
3830
|
+
try {
|
|
3831
|
+
if (row.type === "crossfade") {
|
|
3832
|
+
const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
|
|
3833
|
+
const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
|
|
3834
|
+
if (!o || !t) throw new Error("Track is no longer available \u2014 refresh and retry.");
|
|
3835
|
+
await onCreateCrossfade(
|
|
3836
|
+
{ dbId: o.dbId, name: o.name, role: o.role },
|
|
3837
|
+
{ dbId: t.dbId, name: t.name, role: t.role }
|
|
3838
|
+
);
|
|
3839
|
+
} else if (row.type === "fade-out") {
|
|
3840
|
+
const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
|
|
3841
|
+
if (!o) throw new Error("Track is no longer available \u2014 refresh and retry.");
|
|
3842
|
+
const eff = rowEffectsRef.current[o.dbId] ?? "fade";
|
|
3843
|
+
if (eff !== "fade" && onCreateAudioTransition) {
|
|
3844
|
+
await onCreateAudioTransition({ dbId: o.dbId, name: o.name, role: o.role }, "out", eff);
|
|
3845
|
+
} else {
|
|
3846
|
+
await onCreateFade({ dbId: o.dbId, name: o.name, role: o.role }, "out", defaultFadeGesture(o.role));
|
|
3847
|
+
}
|
|
3848
|
+
} else {
|
|
3849
|
+
const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
|
|
3850
|
+
if (!t) throw new Error("Track is no longer available \u2014 refresh and retry.");
|
|
3851
|
+
const eff = rowEffectsRef.current[t.dbId] ?? "fade";
|
|
3852
|
+
if (eff !== "fade" && onCreateAudioTransition) {
|
|
3853
|
+
await onCreateAudioTransition({ dbId: t.dbId, name: t.name, role: t.role }, "in", eff);
|
|
3854
|
+
} else {
|
|
3855
|
+
await onCreateFade({ dbId: t.dbId, name: t.name, role: t.role }, "in", defaultFadeGesture(t.role));
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
} catch (err) {
|
|
3859
|
+
setRowErrors((prev) => ({
|
|
3860
|
+
...prev,
|
|
3861
|
+
[key]: err instanceof Error ? err.message : "Failed to create transition."
|
|
3862
|
+
}));
|
|
3863
|
+
} finally {
|
|
3864
|
+
setCreatingKeys((prev) => {
|
|
3865
|
+
const next = new Set(prev);
|
|
3866
|
+
next.delete(key);
|
|
3867
|
+
return next;
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
3870
|
+
},
|
|
3871
|
+
[onCreateCrossfade, onCreateFade, onCreateAudioTransition]
|
|
3872
|
+
);
|
|
3873
|
+
const createAll = useCallback8(async () => {
|
|
3874
|
+
const eligible = buildRowSlots(originSlotsRef.current, targetSlotsRef.current).filter((r) => {
|
|
3875
|
+
const k = rowKey(r);
|
|
3876
|
+
return k !== null && !creatingKeysRef.current.has(k);
|
|
3877
|
+
});
|
|
3878
|
+
if (eligible.length === 0) return;
|
|
3879
|
+
let cursor = 0;
|
|
3880
|
+
const worker = async () => {
|
|
3881
|
+
while (cursor < eligible.length) {
|
|
3882
|
+
const row = eligible[cursor];
|
|
3883
|
+
cursor += 1;
|
|
3884
|
+
await createRow(row);
|
|
3885
|
+
}
|
|
3886
|
+
};
|
|
3887
|
+
await Promise.all(
|
|
3888
|
+
Array.from({ length: Math.min(CREATE_ALL_CONCURRENCY, eligible.length) }, () => worker())
|
|
3889
|
+
);
|
|
3890
|
+
}, [createRow]);
|
|
3891
|
+
const fromLabel = fromName ?? "origin";
|
|
3892
|
+
const toLabel = toName ?? "target";
|
|
3893
|
+
const cellDragProps = (col, index, locked) => ({
|
|
3894
|
+
draggable: !locked,
|
|
3895
|
+
onDragStart: (e) => {
|
|
3896
|
+
if (locked) return;
|
|
3897
|
+
dragRef.current = { col, index };
|
|
3898
|
+
setDragging({ col, index });
|
|
3899
|
+
if (e.dataTransfer) {
|
|
3900
|
+
e.dataTransfer.effectAllowed = "move";
|
|
3901
|
+
try {
|
|
3902
|
+
e.dataTransfer.setData("text/plain", String(index));
|
|
3903
|
+
} catch {
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
},
|
|
3907
|
+
onDragEnd: () => {
|
|
3908
|
+
dragRef.current = null;
|
|
3909
|
+
setDragging(null);
|
|
3910
|
+
setDragOver(null);
|
|
3911
|
+
},
|
|
3912
|
+
onDragEnter: (e) => {
|
|
3913
|
+
const d = dragRef.current;
|
|
3914
|
+
if (!d || d.col !== col) return;
|
|
3915
|
+
e.preventDefault();
|
|
3916
|
+
setDragOver({ col, index });
|
|
3917
|
+
},
|
|
3918
|
+
onDragOver: (e) => {
|
|
3919
|
+
const d = dragRef.current;
|
|
3920
|
+
if (!d || d.col !== col) return;
|
|
3921
|
+
e.preventDefault();
|
|
3922
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
3923
|
+
},
|
|
3924
|
+
onDragLeave: () => {
|
|
3925
|
+
setDragOver((cur) => cur && cur.col === col && cur.index === index ? null : cur);
|
|
3926
|
+
},
|
|
3927
|
+
onDrop: (e) => {
|
|
3928
|
+
e.preventDefault();
|
|
3929
|
+
handleDrop(col, index);
|
|
3930
|
+
}
|
|
3931
|
+
});
|
|
3932
|
+
const renderCell = (col, index, slotId) => {
|
|
3933
|
+
const byId = col === "origin" ? originById : targetById;
|
|
3934
|
+
const track = slotId ? byId.get(slotId) : void 0;
|
|
3935
|
+
const locked = slotId !== null && creatingDbIds.has(slotId);
|
|
3936
|
+
const isDragging = dragging?.col === col && dragging.index === index;
|
|
3937
|
+
const isDragTarget = dragOver?.col === col && dragOver.index === index && !isDragging;
|
|
3938
|
+
const base = "group relative rounded-sm border px-2 py-1.5 text-left transition-colors select-none";
|
|
3939
|
+
const tone = isDragTarget ? "border-sas-accent bg-sas-accent/10" : "border-sas-border bg-sas-panel";
|
|
3940
|
+
if (slotId === null) {
|
|
3941
|
+
return /* @__PURE__ */ jsxs13(
|
|
3942
|
+
"div",
|
|
3943
|
+
{
|
|
3944
|
+
...cellDragProps(col, index, false),
|
|
3945
|
+
"data-testid": `${testIdPrefix}-${col}-gap-${index}`,
|
|
3946
|
+
className: `${base} ${tone} border-dashed flex items-center justify-between ${isDragging ? "opacity-40" : "opacity-70"}`,
|
|
3947
|
+
children: [
|
|
3948
|
+
/* @__PURE__ */ jsx17("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
|
|
3949
|
+
/* @__PURE__ */ jsx17(
|
|
3950
|
+
"button",
|
|
3951
|
+
{
|
|
3952
|
+
type: "button",
|
|
3953
|
+
"data-testid": `${testIdPrefix}-${col}-remove-gap-${index}`,
|
|
3954
|
+
onClick: () => removeGap(col, index),
|
|
3955
|
+
title: "Remove gap",
|
|
3956
|
+
className: "text-[10px] text-sas-muted hover:text-sas-danger",
|
|
3957
|
+
children: "\u2715"
|
|
3958
|
+
}
|
|
3959
|
+
)
|
|
3960
|
+
]
|
|
3961
|
+
}
|
|
3962
|
+
);
|
|
3963
|
+
}
|
|
3964
|
+
const primary = track ? track.prompt?.trim() || track.name : slotId;
|
|
3965
|
+
const meta = track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing";
|
|
3966
|
+
return /* @__PURE__ */ jsx17(
|
|
3967
|
+
"div",
|
|
3968
|
+
{
|
|
3969
|
+
...cellDragProps(col, index, locked),
|
|
3970
|
+
"data-testid": `${testIdPrefix}-${col}-cell-${slotId}`,
|
|
3971
|
+
"data-value": slotId,
|
|
3972
|
+
className: `${base} ${tone} ${isDragging ? "opacity-40" : ""} ${locked ? "opacity-60" : "cursor-grab active:cursor-grabbing"}`,
|
|
3973
|
+
title: track ? track.dbId : "Track no longer available",
|
|
3974
|
+
children: /* @__PURE__ */ jsxs13("div", { className: "flex items-start gap-1", children: [
|
|
3975
|
+
/* @__PURE__ */ jsx17("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
|
|
3976
|
+
/* @__PURE__ */ jsxs13("div", { className: "min-w-0 flex-1", children: [
|
|
3977
|
+
/* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-text truncate", children: primary }),
|
|
3978
|
+
meta && /* @__PURE__ */ jsx17("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
|
|
3979
|
+
] }),
|
|
3980
|
+
/* @__PURE__ */ jsx17(
|
|
3981
|
+
"button",
|
|
3982
|
+
{
|
|
3983
|
+
type: "button",
|
|
3984
|
+
"data-testid": `${testIdPrefix}-${col}-insert-gap-${index}`,
|
|
3985
|
+
onClick: () => insertGapAbove(col, index),
|
|
3986
|
+
disabled: locked,
|
|
3987
|
+
title: "Insert a gap above (make this a fade)",
|
|
3988
|
+
className: "text-[10px] text-sas-muted opacity-0 group-hover:opacity-100 hover:text-sas-accent disabled:opacity-30",
|
|
3989
|
+
children: "+gap"
|
|
3990
|
+
}
|
|
3991
|
+
)
|
|
3992
|
+
] })
|
|
3993
|
+
}
|
|
3994
|
+
);
|
|
3995
|
+
};
|
|
3996
|
+
return /* @__PURE__ */ jsxs13("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
|
|
3997
|
+
/* @__PURE__ */ jsxs13("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
|
|
3998
|
+
/* @__PURE__ */ jsxs13("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
|
|
3999
|
+
/* @__PURE__ */ jsx17("span", { className: "text-sas-text", children: fromLabel }),
|
|
4000
|
+
" \u2192",
|
|
4001
|
+
" ",
|
|
4002
|
+
/* @__PURE__ */ jsx17("span", { className: "text-sas-text", children: toLabel }),
|
|
4003
|
+
familyLabel ? ` \xB7 ${familyLabel}` : "",
|
|
4004
|
+
" \xB7 line up a track on each side to crossfade; leave one blank (or insert a gap) to fade."
|
|
4005
|
+
] }),
|
|
4006
|
+
/* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
4007
|
+
creatingKeys.size > 0 && /* @__PURE__ */ jsxs13("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
|
|
4008
|
+
creatingKeys.size,
|
|
4009
|
+
" creating\u2026"
|
|
4010
|
+
] }),
|
|
4011
|
+
/* @__PURE__ */ jsxs13(
|
|
4012
|
+
"button",
|
|
4013
|
+
{
|
|
4014
|
+
type: "button",
|
|
4015
|
+
"data-testid": `${testIdPrefix}-create-all`,
|
|
4016
|
+
onClick: createAll,
|
|
4017
|
+
disabled: eligibleCount === 0,
|
|
4018
|
+
title: "Create every staged transition at once (runs several concurrently)",
|
|
4019
|
+
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"}`,
|
|
4020
|
+
children: [
|
|
4021
|
+
"Create all",
|
|
4022
|
+
eligibleCount > 0 ? ` (${eligibleCount})` : ""
|
|
4023
|
+
]
|
|
4024
|
+
}
|
|
4025
|
+
)
|
|
4026
|
+
] })
|
|
4027
|
+
] }),
|
|
4028
|
+
/* @__PURE__ */ jsxs13("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
|
|
4029
|
+
/* @__PURE__ */ jsxs13("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
|
|
4030
|
+
"Origin (",
|
|
4031
|
+
fromLabel,
|
|
4032
|
+
")"
|
|
4033
|
+
] }),
|
|
4034
|
+
/* @__PURE__ */ jsx17("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
|
|
4035
|
+
/* @__PURE__ */ jsxs13("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
|
|
4036
|
+
"Target (",
|
|
4037
|
+
toLabel,
|
|
4038
|
+
")"
|
|
4039
|
+
] })
|
|
4040
|
+
] }),
|
|
4041
|
+
load.status === "loading" && /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
|
|
4042
|
+
load.status === "error" && /* @__PURE__ */ jsx17("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
|
|
4043
|
+
load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ jsxs13("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
|
|
4044
|
+
"No tracks to arrange in this panel for either scene. Add tracks to ",
|
|
4045
|
+
fromLabel,
|
|
4046
|
+
" or ",
|
|
4047
|
+
toLabel,
|
|
4048
|
+
" ",
|
|
4049
|
+
"first (or free one by deleting an existing crossfade/fade)."
|
|
4050
|
+
] }) : /* @__PURE__ */ jsx17("div", { className: "space-y-2", children: rows.map((row, i) => {
|
|
4051
|
+
const key = rowKey(row);
|
|
4052
|
+
const isCreatingThis = key !== null && creatingKeys.has(key);
|
|
4053
|
+
const errMsg = key !== null ? rowErrors[key] : void 0;
|
|
4054
|
+
return /* @__PURE__ */ jsxs13(
|
|
4055
|
+
"div",
|
|
4056
|
+
{
|
|
4057
|
+
"data-testid": `${testIdPrefix}-row-${i}`,
|
|
4058
|
+
className: "grid grid-cols-[1fr_auto_1fr] gap-2 items-center",
|
|
4059
|
+
children: [
|
|
4060
|
+
renderCell("origin", i, row.originId),
|
|
4061
|
+
/* @__PURE__ */ jsxs13("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
|
|
4062
|
+
!row.type ? /* @__PURE__ */ jsx17("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ jsx17(
|
|
4063
|
+
"span",
|
|
4064
|
+
{
|
|
4065
|
+
"data-testid": `${testIdPrefix}-type-${i}`,
|
|
4066
|
+
className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-accent/50 text-sas-accent",
|
|
4067
|
+
children: TYPE_LABEL[row.type]
|
|
4068
|
+
}
|
|
4069
|
+
) : audioEffectsEnabled ? /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
|
|
4070
|
+
/* @__PURE__ */ jsx17(
|
|
4071
|
+
"select",
|
|
4072
|
+
{
|
|
4073
|
+
"data-testid": `${testIdPrefix}-effect-${i}`,
|
|
4074
|
+
value: rowEffects[row.originId ?? row.targetId] ?? "fade",
|
|
4075
|
+
onChange: (e) => {
|
|
4076
|
+
const id = row.originId ?? row.targetId;
|
|
4077
|
+
if (id) setRowEffect(id, e.target.value);
|
|
4078
|
+
},
|
|
4079
|
+
className: "text-[10px] bg-sas-panel border border-sas-border rounded-sm px-1 py-0.5 text-sas-text",
|
|
4080
|
+
children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ jsx17("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
|
|
4081
|
+
}
|
|
4082
|
+
),
|
|
4083
|
+
/* @__PURE__ */ jsx17("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
|
|
4084
|
+
] }) : /* @__PURE__ */ jsx17(
|
|
4085
|
+
"span",
|
|
4086
|
+
{
|
|
4087
|
+
"data-testid": `${testIdPrefix}-type-${i}`,
|
|
4088
|
+
className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-border text-sas-muted",
|
|
4089
|
+
children: TYPE_LABEL[row.type]
|
|
4090
|
+
}
|
|
4091
|
+
),
|
|
4092
|
+
isCreatingThis ? /* @__PURE__ */ jsx17("div", { className: "w-full", children: /* @__PURE__ */ jsx17(
|
|
4093
|
+
SorceryProgressBar,
|
|
4094
|
+
{
|
|
4095
|
+
isLoading: true,
|
|
4096
|
+
heightClass: "h-5",
|
|
4097
|
+
statusText: "CREATING",
|
|
4098
|
+
estimatedDurationMs: row.type === "crossfade" ? CROSSFADE_ESTIMATE_MS : FADE_ESTIMATE_MS
|
|
4099
|
+
}
|
|
4100
|
+
) }) : /* @__PURE__ */ jsx17(
|
|
4101
|
+
"button",
|
|
4102
|
+
{
|
|
4103
|
+
type: "button",
|
|
4104
|
+
"data-testid": `${testIdPrefix}-create-${i}`,
|
|
4105
|
+
onClick: () => createRow(row),
|
|
4106
|
+
disabled: !row.type,
|
|
4107
|
+
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"}`,
|
|
4108
|
+
children: "Create"
|
|
4109
|
+
}
|
|
4110
|
+
),
|
|
4111
|
+
errMsg && /* @__PURE__ */ jsx17(
|
|
4112
|
+
"span",
|
|
4113
|
+
{
|
|
4114
|
+
"data-testid": `${testIdPrefix}-row-error-${i}`,
|
|
4115
|
+
className: "text-[10px] text-sas-danger text-center leading-tight",
|
|
4116
|
+
children: errMsg
|
|
4117
|
+
}
|
|
4118
|
+
)
|
|
4119
|
+
] }),
|
|
4120
|
+
renderCell("target", i, row.targetId)
|
|
4121
|
+
]
|
|
4122
|
+
},
|
|
4123
|
+
i
|
|
4124
|
+
);
|
|
4125
|
+
}) }))
|
|
4126
|
+
] });
|
|
4127
|
+
}
|
|
4128
|
+
|
|
2935
4129
|
// src/components/DownloadPackButton.tsx
|
|
2936
|
-
import { useCallback as
|
|
2937
|
-
import { jsx as
|
|
4130
|
+
import { useCallback as useCallback9, useEffect as useEffect10, useState as useState12 } from "react";
|
|
4131
|
+
import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2938
4132
|
function formatSize(bytes) {
|
|
2939
4133
|
if (!bytes || bytes <= 0) return "";
|
|
2940
4134
|
const gb = bytes / 1024 ** 3;
|
|
@@ -2950,10 +4144,10 @@ var DownloadPackButton = ({
|
|
|
2950
4144
|
variant = "compact",
|
|
2951
4145
|
onDownloadComplete
|
|
2952
4146
|
}) => {
|
|
2953
|
-
const [status, setStatus] =
|
|
2954
|
-
const [progress, setProgress] =
|
|
2955
|
-
const [errorMessage, setErrorMessage] =
|
|
2956
|
-
|
|
4147
|
+
const [status, setStatus] = useState12("idle");
|
|
4148
|
+
const [progress, setProgress] = useState12(0);
|
|
4149
|
+
const [errorMessage, setErrorMessage] = useState12(null);
|
|
4150
|
+
useEffect10(() => {
|
|
2957
4151
|
const unsub = host.onSamplePackProgress(packId, (p) => {
|
|
2958
4152
|
setStatus(p.status);
|
|
2959
4153
|
setProgress(p.progress);
|
|
@@ -2968,7 +4162,7 @@ var DownloadPackButton = ({
|
|
|
2968
4162
|
});
|
|
2969
4163
|
return unsub;
|
|
2970
4164
|
}, [host, packId, onDownloadComplete]);
|
|
2971
|
-
const handleClick =
|
|
4165
|
+
const handleClick = useCallback9(async () => {
|
|
2972
4166
|
if (status !== "idle" && status !== "error") return;
|
|
2973
4167
|
try {
|
|
2974
4168
|
setStatus("downloading");
|
|
@@ -3022,8 +4216,8 @@ var DownloadPackButton = ({
|
|
|
3022
4216
|
} else {
|
|
3023
4217
|
className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
|
|
3024
4218
|
}
|
|
3025
|
-
return /* @__PURE__ */
|
|
3026
|
-
/* @__PURE__ */
|
|
4219
|
+
return /* @__PURE__ */ jsxs14("div", { children: [
|
|
4220
|
+
/* @__PURE__ */ jsx18(
|
|
3027
4221
|
"button",
|
|
3028
4222
|
{
|
|
3029
4223
|
"data-testid": `download-pack-button-${packId}`,
|
|
@@ -3034,12 +4228,12 @@ var DownloadPackButton = ({
|
|
|
3034
4228
|
children: buttonLabel
|
|
3035
4229
|
}
|
|
3036
4230
|
),
|
|
3037
|
-
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */
|
|
4231
|
+
variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
|
|
3038
4232
|
] });
|
|
3039
4233
|
};
|
|
3040
4234
|
|
|
3041
4235
|
// src/components/SamplePackCTACard.tsx
|
|
3042
|
-
import { jsx as
|
|
4236
|
+
import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
3043
4237
|
var SamplePackCTACard = ({
|
|
3044
4238
|
host,
|
|
3045
4239
|
pack,
|
|
@@ -3047,7 +4241,7 @@ var SamplePackCTACard = ({
|
|
|
3047
4241
|
onDownloadComplete
|
|
3048
4242
|
}) => {
|
|
3049
4243
|
if (status === "checking") {
|
|
3050
|
-
return /* @__PURE__ */
|
|
4244
|
+
return /* @__PURE__ */ jsx19(
|
|
3051
4245
|
"div",
|
|
3052
4246
|
{
|
|
3053
4247
|
"data-testid": `sample-pack-cta-checking-${pack.packId}`,
|
|
@@ -3058,16 +4252,16 @@ var SamplePackCTACard = ({
|
|
|
3058
4252
|
}
|
|
3059
4253
|
const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
|
|
3060
4254
|
const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
|
|
3061
|
-
return /* @__PURE__ */
|
|
4255
|
+
return /* @__PURE__ */ jsxs15(
|
|
3062
4256
|
"div",
|
|
3063
4257
|
{
|
|
3064
4258
|
"data-testid": `sample-pack-cta-${pack.packId}`,
|
|
3065
4259
|
className: "flex flex-col items-center justify-center py-12 px-6 text-center",
|
|
3066
4260
|
children: [
|
|
3067
|
-
/* @__PURE__ */
|
|
3068
|
-
/* @__PURE__ */
|
|
3069
|
-
/* @__PURE__ */
|
|
3070
|
-
/* @__PURE__ */
|
|
4261
|
+
/* @__PURE__ */ jsx19("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
|
|
4262
|
+
/* @__PURE__ */ jsx19("div", { className: "text-base text-sas-text mb-1", children: headline }),
|
|
4263
|
+
/* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
|
|
4264
|
+
/* @__PURE__ */ jsx19(
|
|
3071
4265
|
DownloadPackButton,
|
|
3072
4266
|
{
|
|
3073
4267
|
host,
|
|
@@ -3084,7 +4278,7 @@ var SamplePackCTACard = ({
|
|
|
3084
4278
|
};
|
|
3085
4279
|
|
|
3086
4280
|
// src/components/WaveformView.tsx
|
|
3087
|
-
import { useEffect as
|
|
4281
|
+
import { useEffect as useEffect11, useRef as useRef11, useState as useState13 } from "react";
|
|
3088
4282
|
|
|
3089
4283
|
// src/components/waveform.ts
|
|
3090
4284
|
function computePeaks(audioBuffer, bins, targetSamples) {
|
|
@@ -3147,7 +4341,7 @@ function drawWaveform(canvas, peaks, options = {}) {
|
|
|
3147
4341
|
}
|
|
3148
4342
|
|
|
3149
4343
|
// src/components/WaveformView.tsx
|
|
3150
|
-
import { jsx as
|
|
4344
|
+
import { jsx as jsx20 } from "react/jsx-runtime";
|
|
3151
4345
|
var WaveformView = ({
|
|
3152
4346
|
host,
|
|
3153
4347
|
filePath,
|
|
@@ -3156,9 +4350,9 @@ var WaveformView = ({
|
|
|
3156
4350
|
fillStyle,
|
|
3157
4351
|
targetSamples
|
|
3158
4352
|
}) => {
|
|
3159
|
-
const canvasRef =
|
|
3160
|
-
const [peaks, setPeaks] =
|
|
3161
|
-
|
|
4353
|
+
const canvasRef = useRef11(null);
|
|
4354
|
+
const [peaks, setPeaks] = useState13(null);
|
|
4355
|
+
useEffect11(() => {
|
|
3162
4356
|
let cancelled = false;
|
|
3163
4357
|
let audioContext = null;
|
|
3164
4358
|
(async () => {
|
|
@@ -3184,7 +4378,7 @@ var WaveformView = ({
|
|
|
3184
4378
|
cancelled = true;
|
|
3185
4379
|
};
|
|
3186
4380
|
}, [host, filePath, bins, targetSamples]);
|
|
3187
|
-
|
|
4381
|
+
useEffect11(() => {
|
|
3188
4382
|
if (!peaks) return;
|
|
3189
4383
|
const canvas = canvasRef.current;
|
|
3190
4384
|
if (!canvas) return;
|
|
@@ -3195,7 +4389,7 @@ var WaveformView = ({
|
|
|
3195
4389
|
observer.observe(canvas);
|
|
3196
4390
|
return () => observer.disconnect();
|
|
3197
4391
|
}, [peaks, fillStyle]);
|
|
3198
|
-
return /* @__PURE__ */
|
|
4392
|
+
return /* @__PURE__ */ jsx20(
|
|
3199
4393
|
"canvas",
|
|
3200
4394
|
{
|
|
3201
4395
|
ref: canvasRef,
|
|
@@ -3206,8 +4400,8 @@ var WaveformView = ({
|
|
|
3206
4400
|
};
|
|
3207
4401
|
|
|
3208
4402
|
// src/components/ScrollingWaveform.tsx
|
|
3209
|
-
import { useEffect as
|
|
3210
|
-
import { jsx as
|
|
4403
|
+
import { useEffect as useEffect12, useRef as useRef12 } from "react";
|
|
4404
|
+
import { jsx as jsx21 } from "react/jsx-runtime";
|
|
3211
4405
|
var ScrollingWaveform = ({
|
|
3212
4406
|
getPeakDb,
|
|
3213
4407
|
active,
|
|
@@ -3215,11 +4409,11 @@ var ScrollingWaveform = ({
|
|
|
3215
4409
|
className,
|
|
3216
4410
|
fillStyle
|
|
3217
4411
|
}) => {
|
|
3218
|
-
const canvasRef =
|
|
3219
|
-
const ringRef =
|
|
3220
|
-
const writeIdxRef =
|
|
3221
|
-
const rafRef =
|
|
3222
|
-
|
|
4412
|
+
const canvasRef = useRef12(null);
|
|
4413
|
+
const ringRef = useRef12(new Float32Array(columns));
|
|
4414
|
+
const writeIdxRef = useRef12(0);
|
|
4415
|
+
const rafRef = useRef12(null);
|
|
4416
|
+
useEffect12(() => {
|
|
3223
4417
|
if (ringRef.current.length !== columns) {
|
|
3224
4418
|
const next = new Float32Array(columns);
|
|
3225
4419
|
const prev = ringRef.current;
|
|
@@ -3231,7 +4425,7 @@ var ScrollingWaveform = ({
|
|
|
3231
4425
|
writeIdxRef.current = writeIdxRef.current % columns;
|
|
3232
4426
|
}
|
|
3233
4427
|
}, [columns]);
|
|
3234
|
-
|
|
4428
|
+
useEffect12(() => {
|
|
3235
4429
|
if (!active) {
|
|
3236
4430
|
if (rafRef.current !== null) {
|
|
3237
4431
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -3283,7 +4477,7 @@ var ScrollingWaveform = ({
|
|
|
3283
4477
|
}
|
|
3284
4478
|
};
|
|
3285
4479
|
}, [active, getPeakDb, fillStyle]);
|
|
3286
|
-
return /* @__PURE__ */
|
|
4480
|
+
return /* @__PURE__ */ jsx21(
|
|
3287
4481
|
"canvas",
|
|
3288
4482
|
{
|
|
3289
4483
|
ref: canvasRef,
|
|
@@ -3294,8 +4488,8 @@ var ScrollingWaveform = ({
|
|
|
3294
4488
|
};
|
|
3295
4489
|
|
|
3296
4490
|
// src/components/OffsetScrubber.tsx
|
|
3297
|
-
import { useCallback as
|
|
3298
|
-
import { jsx as
|
|
4491
|
+
import { useCallback as useCallback10, useEffect as useEffect13, useMemo as useMemo6, useRef as useRef13, useState as useState14 } from "react";
|
|
4492
|
+
import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
3299
4493
|
var SLIDER_HEIGHT_PX = 28;
|
|
3300
4494
|
var TICK_HEIGHT_PX = 14;
|
|
3301
4495
|
var DOWNBEAT_TICK_HEIGHT_PX = 22;
|
|
@@ -3308,40 +4502,40 @@ function OffsetScrubber({
|
|
|
3308
4502
|
onChange,
|
|
3309
4503
|
disabled = false
|
|
3310
4504
|
}) {
|
|
3311
|
-
const trackRef =
|
|
3312
|
-
const [draftOffset, setDraftOffset] =
|
|
3313
|
-
const [isDragging, setIsDragging] =
|
|
3314
|
-
|
|
4505
|
+
const trackRef = useRef13(null);
|
|
4506
|
+
const [draftOffset, setDraftOffset] = useState14(offsetSamples);
|
|
4507
|
+
const [isDragging, setIsDragging] = useState14(false);
|
|
4508
|
+
useEffect13(() => {
|
|
3315
4509
|
if (!isDragging) setDraftOffset(offsetSamples);
|
|
3316
4510
|
}, [offsetSamples, isDragging]);
|
|
3317
4511
|
const sampleRate = cuePoints?.sample_rate ?? 44100;
|
|
3318
4512
|
const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
|
|
3319
|
-
const beatsForRange =
|
|
4513
|
+
const beatsForRange = useMemo6(() => {
|
|
3320
4514
|
return Math.round(60 / projectBpm * sampleRate);
|
|
3321
4515
|
}, [projectBpm, sampleRate]);
|
|
3322
4516
|
const rangeSamples = beatsForRange * meter;
|
|
3323
|
-
const sampleToFraction =
|
|
4517
|
+
const sampleToFraction = useCallback10(
|
|
3324
4518
|
(sample) => {
|
|
3325
4519
|
const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
|
|
3326
4520
|
return (clamped + rangeSamples) / (2 * rangeSamples);
|
|
3327
4521
|
},
|
|
3328
4522
|
[rangeSamples]
|
|
3329
4523
|
);
|
|
3330
|
-
const fractionToSample =
|
|
4524
|
+
const fractionToSample = useCallback10(
|
|
3331
4525
|
(fraction) => {
|
|
3332
4526
|
const clamped = Math.max(0, Math.min(1, fraction));
|
|
3333
4527
|
return Math.round(clamped * 2 * rangeSamples - rangeSamples);
|
|
3334
4528
|
},
|
|
3335
4529
|
[rangeSamples]
|
|
3336
4530
|
);
|
|
3337
|
-
const snapTargets =
|
|
4531
|
+
const snapTargets = useMemo6(() => {
|
|
3338
4532
|
if (!cuePoints || cuePoints.beats.length === 0) return [];
|
|
3339
4533
|
const downbeat = cuePoints.beats[0];
|
|
3340
4534
|
const positives = cuePoints.beats.map((b) => b - downbeat);
|
|
3341
4535
|
const negatives = positives.slice(1).map((p) => -p);
|
|
3342
4536
|
return [...negatives, ...positives].sort((a, b) => a - b);
|
|
3343
4537
|
}, [cuePoints]);
|
|
3344
|
-
const snapToBeat =
|
|
4538
|
+
const snapToBeat = useCallback10(
|
|
3345
4539
|
(sample) => {
|
|
3346
4540
|
if (snapTargets.length === 0) return sample;
|
|
3347
4541
|
let best = snapTargets[0];
|
|
@@ -3357,7 +4551,7 @@ function OffsetScrubber({
|
|
|
3357
4551
|
},
|
|
3358
4552
|
[snapTargets]
|
|
3359
4553
|
);
|
|
3360
|
-
const handlePointerDown =
|
|
4554
|
+
const handlePointerDown = useCallback10(
|
|
3361
4555
|
(e) => {
|
|
3362
4556
|
if (disabled || !cuePoints) return;
|
|
3363
4557
|
e.preventDefault();
|
|
@@ -3391,7 +4585,7 @@ function OffsetScrubber({
|
|
|
3391
4585
|
},
|
|
3392
4586
|
[disabled, cuePoints, fractionToSample, onChange, snapToBeat]
|
|
3393
4587
|
);
|
|
3394
|
-
const handleResetToZero =
|
|
4588
|
+
const handleResetToZero = useCallback10(() => {
|
|
3395
4589
|
if (disabled) return;
|
|
3396
4590
|
setDraftOffset(0);
|
|
3397
4591
|
onChange(0);
|
|
@@ -3399,7 +4593,7 @@ function OffsetScrubber({
|
|
|
3399
4593
|
const thumbFraction = sampleToFraction(draftOffset);
|
|
3400
4594
|
const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;
|
|
3401
4595
|
const bpmMismatch = cuePoints?.detected_bpm != null && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;
|
|
3402
|
-
const ticks =
|
|
4596
|
+
const ticks = useMemo6(() => {
|
|
3403
4597
|
if (!cuePoints) return [];
|
|
3404
4598
|
const downbeat = cuePoints.beats[0] ?? 0;
|
|
3405
4599
|
return cuePoints.beats.map((b, i) => {
|
|
@@ -3410,9 +4604,9 @@ function OffsetScrubber({
|
|
|
3410
4604
|
});
|
|
3411
4605
|
}, [cuePoints, sampleToFraction]);
|
|
3412
4606
|
const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
|
|
3413
|
-
return /* @__PURE__ */
|
|
3414
|
-
/* @__PURE__ */
|
|
3415
|
-
/* @__PURE__ */
|
|
4607
|
+
return /* @__PURE__ */ jsxs16("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
|
|
4608
|
+
/* @__PURE__ */ jsx22("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
|
|
4609
|
+
/* @__PURE__ */ jsxs16(
|
|
3416
4610
|
"div",
|
|
3417
4611
|
{
|
|
3418
4612
|
ref: trackRef,
|
|
@@ -3428,7 +4622,7 @@ function OffsetScrubber({
|
|
|
3428
4622
|
"aria-valuenow": draftOffset,
|
|
3429
4623
|
"aria-disabled": isDisabled,
|
|
3430
4624
|
children: [
|
|
3431
|
-
/* @__PURE__ */
|
|
4625
|
+
/* @__PURE__ */ jsx22(
|
|
3432
4626
|
"div",
|
|
3433
4627
|
{
|
|
3434
4628
|
"aria-hidden": "true",
|
|
@@ -3436,7 +4630,7 @@ function OffsetScrubber({
|
|
|
3436
4630
|
style: { left: "50%" }
|
|
3437
4631
|
}
|
|
3438
4632
|
),
|
|
3439
|
-
ticks.map((t) => /* @__PURE__ */
|
|
4633
|
+
ticks.map((t) => /* @__PURE__ */ jsx22(
|
|
3440
4634
|
"div",
|
|
3441
4635
|
{
|
|
3442
4636
|
"data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
|
|
@@ -3451,7 +4645,7 @@ function OffsetScrubber({
|
|
|
3451
4645
|
},
|
|
3452
4646
|
t.i
|
|
3453
4647
|
)),
|
|
3454
|
-
/* @__PURE__ */
|
|
4648
|
+
/* @__PURE__ */ jsx22(
|
|
3455
4649
|
"div",
|
|
3456
4650
|
{
|
|
3457
4651
|
"data-testid": "offset-scrubber-thumb",
|
|
@@ -3468,7 +4662,7 @@ function OffsetScrubber({
|
|
|
3468
4662
|
]
|
|
3469
4663
|
}
|
|
3470
4664
|
),
|
|
3471
|
-
/* @__PURE__ */
|
|
4665
|
+
/* @__PURE__ */ jsx22(
|
|
3472
4666
|
"span",
|
|
3473
4667
|
{
|
|
3474
4668
|
"data-testid": "offset-scrubber-readout",
|
|
@@ -3476,7 +4670,7 @@ function OffsetScrubber({
|
|
|
3476
4670
|
children: formatOffset(draftOffset, sampleRate)
|
|
3477
4671
|
}
|
|
3478
4672
|
),
|
|
3479
|
-
/* @__PURE__ */
|
|
4673
|
+
/* @__PURE__ */ jsx22(
|
|
3480
4674
|
"button",
|
|
3481
4675
|
{
|
|
3482
4676
|
type: "button",
|
|
@@ -3488,7 +4682,7 @@ function OffsetScrubber({
|
|
|
3488
4682
|
children: "\u2316"
|
|
3489
4683
|
}
|
|
3490
4684
|
),
|
|
3491
|
-
bpmMismatch && /* @__PURE__ */
|
|
4685
|
+
bpmMismatch && /* @__PURE__ */ jsx22(
|
|
3492
4686
|
"span",
|
|
3493
4687
|
{
|
|
3494
4688
|
"data-testid": "offset-bpm-mismatch",
|
|
@@ -3560,13 +4754,13 @@ function synthesizeCuePoints({
|
|
|
3560
4754
|
}
|
|
3561
4755
|
|
|
3562
4756
|
// src/hooks/useSceneState.ts
|
|
3563
|
-
import { useState as
|
|
4757
|
+
import { useState as useState15, useCallback as useCallback11, useRef as useRef14 } from "react";
|
|
3564
4758
|
function useSceneState(activeSceneId, initialValue) {
|
|
3565
|
-
const [stateMap, setStateMap] =
|
|
3566
|
-
const activeSceneIdRef =
|
|
4759
|
+
const [stateMap, setStateMap] = useState15(() => /* @__PURE__ */ new Map());
|
|
4760
|
+
const activeSceneIdRef = useRef14(activeSceneId);
|
|
3567
4761
|
activeSceneIdRef.current = activeSceneId;
|
|
3568
4762
|
const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
|
|
3569
|
-
const setForCurrentScene =
|
|
4763
|
+
const setForCurrentScene = useCallback11((value) => {
|
|
3570
4764
|
const sid = activeSceneIdRef.current;
|
|
3571
4765
|
if (sid === null) return;
|
|
3572
4766
|
setStateMap((prev) => {
|
|
@@ -3577,7 +4771,7 @@ function useSceneState(activeSceneId, initialValue) {
|
|
|
3577
4771
|
return newMap;
|
|
3578
4772
|
});
|
|
3579
4773
|
}, [initialValue]);
|
|
3580
|
-
const setForScene =
|
|
4774
|
+
const setForScene = useCallback11((sceneId, value) => {
|
|
3581
4775
|
setStateMap((prev) => {
|
|
3582
4776
|
const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
|
|
3583
4777
|
const next = typeof value === "function" ? value(current) : value;
|
|
@@ -3590,10 +4784,10 @@ function useSceneState(activeSceneId, initialValue) {
|
|
|
3590
4784
|
}
|
|
3591
4785
|
|
|
3592
4786
|
// src/hooks/useAnySolo.ts
|
|
3593
|
-
import { useEffect as
|
|
4787
|
+
import { useEffect as useEffect14, useState as useState16 } from "react";
|
|
3594
4788
|
function useAnySolo(host) {
|
|
3595
|
-
const [anySolo, setAnySolo] =
|
|
3596
|
-
|
|
4789
|
+
const [anySolo, setAnySolo] = useState16(false);
|
|
4790
|
+
useEffect14(() => {
|
|
3597
4791
|
let active = true;
|
|
3598
4792
|
const refresh = () => {
|
|
3599
4793
|
host.isAnySoloActive().then((v) => {
|
|
@@ -3612,7 +4806,7 @@ function useAnySolo(host) {
|
|
|
3612
4806
|
}
|
|
3613
4807
|
|
|
3614
4808
|
// src/hooks/useSoundHistory.ts
|
|
3615
|
-
import { useCallback as
|
|
4809
|
+
import { useCallback as useCallback12, useMemo as useMemo7, useRef as useRef15, useState as useState17 } from "react";
|
|
3616
4810
|
var EMPTY = { entries: [], cursor: -1 };
|
|
3617
4811
|
function sameDescriptor(a, b) {
|
|
3618
4812
|
if (a === b) return true;
|
|
@@ -3624,14 +4818,14 @@ function sameDescriptor(a, b) {
|
|
|
3624
4818
|
}
|
|
3625
4819
|
function useSoundHistory(applySound, opts = {}) {
|
|
3626
4820
|
const max = Math.max(2, opts.max ?? 24);
|
|
3627
|
-
const applyRef =
|
|
4821
|
+
const applyRef = useRef15(applySound);
|
|
3628
4822
|
applyRef.current = applySound;
|
|
3629
|
-
const onChangeRef =
|
|
4823
|
+
const onChangeRef = useRef15(opts.onChange);
|
|
3630
4824
|
onChangeRef.current = opts.onChange;
|
|
3631
|
-
const dataRef =
|
|
3632
|
-
const [, setVersion] =
|
|
3633
|
-
const bump =
|
|
3634
|
-
const commit =
|
|
4825
|
+
const dataRef = useRef15({});
|
|
4826
|
+
const [, setVersion] = useState17(0);
|
|
4827
|
+
const bump = useCallback12(() => setVersion((v) => v + 1), []);
|
|
4828
|
+
const commit = useCallback12(
|
|
3635
4829
|
(trackId, next, notify) => {
|
|
3636
4830
|
dataRef.current = { ...dataRef.current, [trackId]: next };
|
|
3637
4831
|
bump();
|
|
@@ -3639,7 +4833,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3639
4833
|
},
|
|
3640
4834
|
[bump]
|
|
3641
4835
|
);
|
|
3642
|
-
const record =
|
|
4836
|
+
const record = useCallback12(
|
|
3643
4837
|
(trackId, descriptor, label) => {
|
|
3644
4838
|
const h = dataRef.current[trackId];
|
|
3645
4839
|
const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
|
|
@@ -3654,7 +4848,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3654
4848
|
},
|
|
3655
4849
|
[max, commit]
|
|
3656
4850
|
);
|
|
3657
|
-
const restoreTo =
|
|
4851
|
+
const restoreTo = useCallback12(
|
|
3658
4852
|
async (trackId, index) => {
|
|
3659
4853
|
const h = dataRef.current[trackId];
|
|
3660
4854
|
if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
|
|
@@ -3664,7 +4858,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3664
4858
|
},
|
|
3665
4859
|
[commit]
|
|
3666
4860
|
);
|
|
3667
|
-
const undo =
|
|
4861
|
+
const undo = useCallback12(
|
|
3668
4862
|
(trackId) => {
|
|
3669
4863
|
const h = dataRef.current[trackId];
|
|
3670
4864
|
if (!h || h.cursor <= 0) return Promise.resolve(false);
|
|
@@ -3672,7 +4866,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3672
4866
|
},
|
|
3673
4867
|
[restoreTo]
|
|
3674
4868
|
);
|
|
3675
|
-
const toggleFavorite =
|
|
4869
|
+
const toggleFavorite = useCallback12(
|
|
3676
4870
|
(trackId, index) => {
|
|
3677
4871
|
const h = dataRef.current[trackId];
|
|
3678
4872
|
if (!h || index < 0 || index >= h.entries.length) return;
|
|
@@ -3681,7 +4875,7 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3681
4875
|
},
|
|
3682
4876
|
[commit]
|
|
3683
4877
|
);
|
|
3684
|
-
const restore =
|
|
4878
|
+
const restore = useCallback12(
|
|
3685
4879
|
(trackId, state) => {
|
|
3686
4880
|
const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
|
|
3687
4881
|
const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
|
|
@@ -3690,15 +4884,15 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3690
4884
|
},
|
|
3691
4885
|
[commit]
|
|
3692
4886
|
);
|
|
3693
|
-
const list =
|
|
4887
|
+
const list = useCallback12(
|
|
3694
4888
|
(trackId) => dataRef.current[trackId] ?? EMPTY,
|
|
3695
4889
|
[]
|
|
3696
4890
|
);
|
|
3697
|
-
const canUndo =
|
|
4891
|
+
const canUndo = useCallback12((trackId) => {
|
|
3698
4892
|
const h = dataRef.current[trackId];
|
|
3699
4893
|
return !!h && h.cursor > 0;
|
|
3700
4894
|
}, []);
|
|
3701
|
-
const clear =
|
|
4895
|
+
const clear = useCallback12(
|
|
3702
4896
|
(trackId) => {
|
|
3703
4897
|
if (dataRef.current[trackId]) {
|
|
3704
4898
|
const next = { ...dataRef.current };
|
|
@@ -3710,102 +4904,18 @@ function useSoundHistory(applySound, opts = {}) {
|
|
|
3710
4904
|
},
|
|
3711
4905
|
[bump]
|
|
3712
4906
|
);
|
|
3713
|
-
const reset =
|
|
4907
|
+
const reset = useCallback12(() => {
|
|
3714
4908
|
dataRef.current = {};
|
|
3715
4909
|
bump();
|
|
3716
4910
|
}, [bump]);
|
|
3717
|
-
return
|
|
4911
|
+
return useMemo7(
|
|
3718
4912
|
() => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),
|
|
3719
4913
|
[record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite]
|
|
3720
4914
|
);
|
|
3721
4915
|
}
|
|
3722
4916
|
|
|
3723
|
-
// src/hooks/useTrackReorder.ts
|
|
3724
|
-
import { useCallback as useCallback10, useRef as useRef13, useState as useState15 } from "react";
|
|
3725
|
-
function moveItem(arr, from, to) {
|
|
3726
|
-
const next = arr.slice();
|
|
3727
|
-
if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
|
|
3728
|
-
return next;
|
|
3729
|
-
}
|
|
3730
|
-
const [moved] = next.splice(from, 1);
|
|
3731
|
-
next.splice(to, 0, moved);
|
|
3732
|
-
return next;
|
|
3733
|
-
}
|
|
3734
|
-
function useTrackReorder({
|
|
3735
|
-
host,
|
|
3736
|
-
items,
|
|
3737
|
-
setItems,
|
|
3738
|
-
getId,
|
|
3739
|
-
onError
|
|
3740
|
-
}) {
|
|
3741
|
-
const [draggingIndex, setDraggingIndex] = useState15(null);
|
|
3742
|
-
const [dragOverIndex, setDragOverIndex] = useState15(null);
|
|
3743
|
-
const fromRef = useRef13(null);
|
|
3744
|
-
const itemsRef = useRef13(items);
|
|
3745
|
-
itemsRef.current = items;
|
|
3746
|
-
const dragPropsFor = useCallback10(
|
|
3747
|
-
(index) => ({
|
|
3748
|
-
handleProps: {
|
|
3749
|
-
draggable: true,
|
|
3750
|
-
onDragStart: (e) => {
|
|
3751
|
-
fromRef.current = index;
|
|
3752
|
-
setDraggingIndex(index);
|
|
3753
|
-
if (e.dataTransfer) {
|
|
3754
|
-
e.dataTransfer.effectAllowed = "move";
|
|
3755
|
-
try {
|
|
3756
|
-
e.dataTransfer.setData("text/plain", String(index));
|
|
3757
|
-
} catch {
|
|
3758
|
-
}
|
|
3759
|
-
}
|
|
3760
|
-
},
|
|
3761
|
-
onDragEnd: () => {
|
|
3762
|
-
fromRef.current = null;
|
|
3763
|
-
setDraggingIndex(null);
|
|
3764
|
-
setDragOverIndex(null);
|
|
3765
|
-
}
|
|
3766
|
-
},
|
|
3767
|
-
rowProps: {
|
|
3768
|
-
onDragEnter: (e) => {
|
|
3769
|
-
if (fromRef.current === null) return;
|
|
3770
|
-
e.preventDefault();
|
|
3771
|
-
setDragOverIndex(index);
|
|
3772
|
-
},
|
|
3773
|
-
onDragOver: (e) => {
|
|
3774
|
-
if (fromRef.current === null) return;
|
|
3775
|
-
e.preventDefault();
|
|
3776
|
-
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
3777
|
-
setDragOverIndex((cur) => cur === index ? cur : index);
|
|
3778
|
-
},
|
|
3779
|
-
onDragLeave: () => {
|
|
3780
|
-
setDragOverIndex((cur) => cur === index ? null : cur);
|
|
3781
|
-
},
|
|
3782
|
-
onDrop: (e) => {
|
|
3783
|
-
e.preventDefault();
|
|
3784
|
-
const from = fromRef.current;
|
|
3785
|
-
fromRef.current = null;
|
|
3786
|
-
setDraggingIndex(null);
|
|
3787
|
-
setDragOverIndex(null);
|
|
3788
|
-
if (from === null || from === index) return;
|
|
3789
|
-
const prev = itemsRef.current;
|
|
3790
|
-
const next = moveItem(prev, from, index);
|
|
3791
|
-
setItems(next);
|
|
3792
|
-
const ids = next.map(getId);
|
|
3793
|
-
Promise.resolve(host.reorderTracks(ids)).catch((err) => {
|
|
3794
|
-
setItems(prev);
|
|
3795
|
-
onError?.(err);
|
|
3796
|
-
});
|
|
3797
|
-
}
|
|
3798
|
-
},
|
|
3799
|
-
isDragging: draggingIndex === index,
|
|
3800
|
-
isDragTarget: dragOverIndex === index && draggingIndex !== index
|
|
3801
|
-
}),
|
|
3802
|
-
[host, setItems, getId, onError, draggingIndex, dragOverIndex]
|
|
3803
|
-
);
|
|
3804
|
-
return { dragPropsFor, draggingIndex, dragOverIndex };
|
|
3805
|
-
}
|
|
3806
|
-
|
|
3807
4917
|
// src/constants/sdk-version.ts
|
|
3808
|
-
var PLUGIN_SDK_VERSION = "2.
|
|
4918
|
+
var PLUGIN_SDK_VERSION = "2.34.0";
|
|
3809
4919
|
|
|
3810
4920
|
// src/utils/format-concurrent-tracks.ts
|
|
3811
4921
|
function formatConcurrentTracks(ctx) {
|
|
@@ -3948,6 +5058,8 @@ function pickTopKWeighted(scored, options = {}) {
|
|
|
3948
5058
|
return top[top.length - 1].item;
|
|
3949
5059
|
}
|
|
3950
5060
|
export {
|
|
5061
|
+
AUDIO_EFFECTS,
|
|
5062
|
+
AUDIO_EFFECT_LABEL,
|
|
3951
5063
|
ConfirmDialog,
|
|
3952
5064
|
CrossfadeModal,
|
|
3953
5065
|
CrossfadeTrackRow,
|
|
@@ -3965,6 +5077,8 @@ export {
|
|
|
3965
5077
|
FX_DISPLAY_LABELS,
|
|
3966
5078
|
FX_ENGINE_PLUGIN_NAMES,
|
|
3967
5079
|
FX_PRESET_CONFIGS,
|
|
5080
|
+
FadeModal,
|
|
5081
|
+
FadeTrackRow,
|
|
3968
5082
|
FxToggleBar,
|
|
3969
5083
|
GUTTER_W,
|
|
3970
5084
|
ImportTrackModal,
|
|
@@ -3983,30 +5097,50 @@ export {
|
|
|
3983
5097
|
SamplePackCTACard,
|
|
3984
5098
|
ScrollingWaveform,
|
|
3985
5099
|
SorceryProgressBar,
|
|
5100
|
+
TEXTURAL_ROLES,
|
|
5101
|
+
TRANSITION_DESIGNER_DRAFT_KEY,
|
|
3986
5102
|
TrackDrawer,
|
|
3987
5103
|
TrackMeterStrip,
|
|
3988
5104
|
TrackRow,
|
|
5105
|
+
TransitionDesigner,
|
|
3989
5106
|
VolumeSlider,
|
|
3990
5107
|
WaveformView,
|
|
3991
5108
|
analyzeWavPeak,
|
|
5109
|
+
asAudioEffect,
|
|
3992
5110
|
asCrossfadeMeta,
|
|
5111
|
+
asFadeMeta,
|
|
5112
|
+
asTransitionDesignerDraft,
|
|
3993
5113
|
buildCrossfadeInpaintPrompt,
|
|
3994
5114
|
buildCrossfadeVolumeCurves,
|
|
5115
|
+
buildFadeVolumeCurve,
|
|
5116
|
+
buildRowSlots,
|
|
3995
5117
|
calculateTimeBasedTarget,
|
|
3996
5118
|
cellToPx,
|
|
3997
5119
|
centerScrollTop,
|
|
3998
5120
|
computePeaks,
|
|
5121
|
+
dbIdsFromKeys,
|
|
3999
5122
|
dbToSlider,
|
|
5123
|
+
defaultFadeGesture,
|
|
4000
5124
|
drawWaveform,
|
|
4001
5125
|
formatConcurrentTracks,
|
|
5126
|
+
hashString,
|
|
4002
5127
|
moveItem,
|
|
5128
|
+
normalizeSlots,
|
|
5129
|
+
padPair,
|
|
5130
|
+
padSlots,
|
|
4003
5131
|
parseCrossfadePairs,
|
|
5132
|
+
parseFades,
|
|
4004
5133
|
pickTopKWeighted,
|
|
4005
5134
|
pitchToName,
|
|
4006
5135
|
pxToCell,
|
|
5136
|
+
reconcileSlots,
|
|
4007
5137
|
resizeNoteDuration,
|
|
5138
|
+
rowKey,
|
|
5139
|
+
rowType,
|
|
4008
5140
|
scorePromptMatch,
|
|
4009
5141
|
sliderToDb,
|
|
5142
|
+
slotsEqual,
|
|
5143
|
+
soundIdentity,
|
|
4010
5144
|
synthesizeCuePoints,
|
|
4011
5145
|
tokenizePrompt,
|
|
4012
5146
|
transposeNotes,
|