miriad-viz 0.9.0 → 0.9.2
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-cli/chunk-K6N7F6GL.js +189 -0
- package/dist-cli/index.js +31 -2
- package/dist-cli/{transform-SCT5EYPV.js → transform-Z4LA7G44.js} +22 -3
- package/dist-cli/validate-curated-files-ZLNFHBXT.js +12 -0
- package/dist-lib/viewer/exports.cjs +208 -9
- package/dist-lib/viewer/exports.cjs.map +1 -1
- package/dist-lib/viewer/exports.js +209 -10
- package/dist-lib/viewer/exports.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// src/cli/guided/validate-curated-files.ts
|
|
2
|
+
function validateTimelineEvents(data) {
|
|
3
|
+
const errors = [];
|
|
4
|
+
const warnings = [];
|
|
5
|
+
if (data === null || typeof data !== "object" || !Array.isArray(data.events)) {
|
|
6
|
+
errors.push("timeline-events.json: `events` must be an array");
|
|
7
|
+
return { errors, warnings };
|
|
8
|
+
}
|
|
9
|
+
const events = data.events;
|
|
10
|
+
const messageBeanEvents = events.filter((e) => {
|
|
11
|
+
if (e === null || typeof e !== "object") return false;
|
|
12
|
+
const type = e.type;
|
|
13
|
+
return type === "message" || type === "beam";
|
|
14
|
+
});
|
|
15
|
+
if (messageBeanEvents.length === 0) {
|
|
16
|
+
errors.push(
|
|
17
|
+
"0 message/beam events \u2014 no chat pills will render. Add events with type 'message' or 'beam', fields: { type, t, from, to }."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const missingT = events.filter((e) => {
|
|
21
|
+
if (e === null || typeof e !== "object") return true;
|
|
22
|
+
return !e.t;
|
|
23
|
+
});
|
|
24
|
+
if (missingT.length > 0) {
|
|
25
|
+
warnings.push(
|
|
26
|
+
`${missingT.length} event(s) missing 't' field (timestamp) \u2014 events without timestamps may not render correctly`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const messageBeanMissingFields = messageBeanEvents.filter((e) => {
|
|
30
|
+
const ev = e;
|
|
31
|
+
return !ev.from || !ev.to;
|
|
32
|
+
});
|
|
33
|
+
if (messageBeanMissingFields.length > 0) {
|
|
34
|
+
warnings.push(
|
|
35
|
+
`${messageBeanMissingFields.length} message/beam event(s) missing 'from' or 'to' field \u2014 chat pills need both fields to render correctly`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (messageBeanEvents.length === 0 && events.length > 0) {
|
|
39
|
+
const milestoneEvents = events.filter((e) => {
|
|
40
|
+
if (e === null || typeof e !== "object") return false;
|
|
41
|
+
return e.type === "milestone";
|
|
42
|
+
});
|
|
43
|
+
if (milestoneEvents.length > 0) {
|
|
44
|
+
warnings.push(
|
|
45
|
+
"Only milestone events found. Milestones render as markers, NOT chat pills. Add message/beam events for chat pills."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { errors, warnings };
|
|
50
|
+
}
|
|
51
|
+
function validateRetroPageData(data) {
|
|
52
|
+
const errors = [];
|
|
53
|
+
const warnings = [];
|
|
54
|
+
if (data === null || typeof data !== "object") {
|
|
55
|
+
errors.push("retro-page-data.json: `meta` is missing or not an object");
|
|
56
|
+
return { errors, warnings };
|
|
57
|
+
}
|
|
58
|
+
const d = data;
|
|
59
|
+
if (d.meta === null || d.meta === void 0 || typeof d.meta !== "object") {
|
|
60
|
+
errors.push("retro-page-data.json: `meta` is missing or not an object");
|
|
61
|
+
return { errors, warnings };
|
|
62
|
+
}
|
|
63
|
+
const meta = d.meta;
|
|
64
|
+
if (!meta.title) {
|
|
65
|
+
errors.push("meta.title is required");
|
|
66
|
+
}
|
|
67
|
+
if (!meta.subtitle) {
|
|
68
|
+
errors.push("meta.subtitle is required");
|
|
69
|
+
}
|
|
70
|
+
if (!meta.startDate) {
|
|
71
|
+
errors.push(
|
|
72
|
+
"meta.startDate is required \u2014 transform uses this to calculate the project time span"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (!meta.endDate) {
|
|
76
|
+
errors.push(
|
|
77
|
+
"meta.endDate is required \u2014 transform uses this to calculate the project time span"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (!d.teamAssembly || !Array.isArray(d.teamAssembly) || d.teamAssembly.length === 0) {
|
|
81
|
+
warnings.push("No team assembly entries");
|
|
82
|
+
} else {
|
|
83
|
+
const teamAssembly = d.teamAssembly;
|
|
84
|
+
for (let i = 0; i < teamAssembly.length; i++) {
|
|
85
|
+
const entry = teamAssembly[i];
|
|
86
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
87
|
+
const e = entry;
|
|
88
|
+
if (!e.agent) {
|
|
89
|
+
errors.push(`teamAssembly[${i}] missing 'agent' field \u2014 use 'agent', not 'name'`);
|
|
90
|
+
}
|
|
91
|
+
if (!e.time) {
|
|
92
|
+
warnings.push(
|
|
93
|
+
`teamAssembly[${i}] missing 'time' field \u2014 agent join timestamp will be null`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!d.phases || !Array.isArray(d.phases) || d.phases.length === 0) {
|
|
99
|
+
warnings.push("No phases defined \u2014 timeline will have no phase labels");
|
|
100
|
+
} else {
|
|
101
|
+
const phases = d.phases;
|
|
102
|
+
for (let i = 0; i < phases.length; i++) {
|
|
103
|
+
const phase = phases[i];
|
|
104
|
+
if (phase === null || typeof phase !== "object") continue;
|
|
105
|
+
const p = phase;
|
|
106
|
+
if (!p.start || !p.end) {
|
|
107
|
+
warnings.push(`phases[${i}] missing start/end timestamps`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!d.milestones || !Array.isArray(d.milestones) || d.milestones.length === 0) {
|
|
112
|
+
warnings.push("No milestones defined");
|
|
113
|
+
}
|
|
114
|
+
if (!d.trustArc || !Array.isArray(d.trustArc) || d.trustArc.length === 0) {
|
|
115
|
+
warnings.push("No trust arc data");
|
|
116
|
+
}
|
|
117
|
+
if (!d.editorialNarration || !Array.isArray(d.editorialNarration) || d.editorialNarration.length === 0) {
|
|
118
|
+
warnings.push("No editorial narration \u2014 generic phase-based narration will be used");
|
|
119
|
+
}
|
|
120
|
+
return { errors, warnings };
|
|
121
|
+
}
|
|
122
|
+
function validateRetroPageQuotes(data) {
|
|
123
|
+
const errors = [];
|
|
124
|
+
const warnings = [];
|
|
125
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
126
|
+
errors.push("retro-page-quotes.json: must be an object (not an array or primitive)");
|
|
127
|
+
return { errors, warnings };
|
|
128
|
+
}
|
|
129
|
+
const d = data;
|
|
130
|
+
const categories = ["good", "bad", "ugly"];
|
|
131
|
+
const foundCategories = categories.filter(
|
|
132
|
+
(cat) => Array.isArray(d[cat]) && d[cat].length > 0
|
|
133
|
+
);
|
|
134
|
+
if (foundCategories.length === 0) {
|
|
135
|
+
warnings.push("No quote categories found");
|
|
136
|
+
return { errors, warnings };
|
|
137
|
+
}
|
|
138
|
+
for (const cat of foundCategories) {
|
|
139
|
+
const entries = d[cat];
|
|
140
|
+
for (let i = 0; i < entries.length; i++) {
|
|
141
|
+
const entry = entries[i];
|
|
142
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
143
|
+
const e = entry;
|
|
144
|
+
if (e.speaker !== void 0 && e.author === void 0) {
|
|
145
|
+
warnings.push(`quotes[${i}] uses 'speaker' \u2014 should be 'author'`);
|
|
146
|
+
}
|
|
147
|
+
if (!e.text) {
|
|
148
|
+
warnings.push(`quotes[${i}] missing 'text' field`);
|
|
149
|
+
}
|
|
150
|
+
if (!e.timestamp) {
|
|
151
|
+
warnings.push(`quotes[${i}] missing 'timestamp' field`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { errors, warnings };
|
|
156
|
+
}
|
|
157
|
+
function validateCuratedFiles(timelineEvents, retroPageData, retroPageQuotes) {
|
|
158
|
+
const errors = [];
|
|
159
|
+
const warnings = [];
|
|
160
|
+
if (timelineEvents === null) {
|
|
161
|
+
errors.push("timeline-events.json: could not be read or parsed");
|
|
162
|
+
} else {
|
|
163
|
+
const r = validateTimelineEvents(timelineEvents);
|
|
164
|
+
errors.push(...r.errors);
|
|
165
|
+
warnings.push(...r.warnings);
|
|
166
|
+
}
|
|
167
|
+
if (retroPageData === null) {
|
|
168
|
+
errors.push("retro-page-data.json: could not be read or parsed");
|
|
169
|
+
} else {
|
|
170
|
+
const r = validateRetroPageData(retroPageData);
|
|
171
|
+
errors.push(...r.errors);
|
|
172
|
+
warnings.push(...r.warnings);
|
|
173
|
+
}
|
|
174
|
+
if (retroPageQuotes === null) {
|
|
175
|
+
errors.push("retro-page-quotes.json: could not be read or parsed");
|
|
176
|
+
} else {
|
|
177
|
+
const r = validateRetroPageQuotes(retroPageQuotes);
|
|
178
|
+
errors.push(...r.errors);
|
|
179
|
+
warnings.push(...r.warnings);
|
|
180
|
+
}
|
|
181
|
+
return { errors, warnings };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export {
|
|
185
|
+
validateTimelineEvents,
|
|
186
|
+
validateRetroPageData,
|
|
187
|
+
validateRetroPageQuotes,
|
|
188
|
+
validateCuratedFiles
|
|
189
|
+
};
|
package/dist-cli/index.js
CHANGED
|
@@ -1579,6 +1579,7 @@ async function runNext(flags) {
|
|
|
1579
1579
|
"audio-approved": flags["audio-approved"] === true || void 0,
|
|
1580
1580
|
// v2 gate flags
|
|
1581
1581
|
"script-approved": flags["script-approved"] === true || void 0,
|
|
1582
|
+
"curate-approved": flags["curate-approved"] === true || void 0,
|
|
1582
1583
|
"voices-approved": flags["voices-approved"] === true || void 0,
|
|
1583
1584
|
"timing-approved": flags["timing-approved"] === true || void 0,
|
|
1584
1585
|
"sound-approved": flags["sound-approved"] === true || void 0,
|
|
@@ -1597,6 +1598,34 @@ async function runNext(flags) {
|
|
|
1597
1598
|
progress.project.dataDir,
|
|
1598
1599
|
progress.project.outputDir
|
|
1599
1600
|
);
|
|
1601
|
+
if (flags["curate-approved"]) {
|
|
1602
|
+
const { validateCuratedFiles } = await import("./validate-curated-files-ZLNFHBXT.js");
|
|
1603
|
+
const curateDataDir = resolve3(projectDir, progress.project.dataDir);
|
|
1604
|
+
const tryReadJSON = (filename) => {
|
|
1605
|
+
try {
|
|
1606
|
+
return JSON.parse(readFileSync2(resolve3(curateDataDir, filename), "utf-8"));
|
|
1607
|
+
} catch {
|
|
1608
|
+
return null;
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
const validation = validateCuratedFiles(
|
|
1612
|
+
tryReadJSON("timeline-events.json"),
|
|
1613
|
+
tryReadJSON("retro-page-data.json"),
|
|
1614
|
+
tryReadJSON("retro-page-quotes.json")
|
|
1615
|
+
);
|
|
1616
|
+
if (validation.errors.length > 0) {
|
|
1617
|
+
console.log("\n\u2717 Curation validation failed:\n");
|
|
1618
|
+
for (const e of validation.errors) console.log(` \u2717 ${e}`);
|
|
1619
|
+
for (const w of validation.warnings) console.log(` \u26A0 ${w}`);
|
|
1620
|
+
console.log("\nFix the errors above and try again.");
|
|
1621
|
+
process.exit(1);
|
|
1622
|
+
}
|
|
1623
|
+
if (validation.warnings.length > 0) {
|
|
1624
|
+
console.log("\n\u26A0 Curation warnings:\n");
|
|
1625
|
+
for (const w of validation.warnings) console.log(` \u26A0 ${w}`);
|
|
1626
|
+
console.log("");
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1600
1629
|
const result = computeNext(
|
|
1601
1630
|
progress,
|
|
1602
1631
|
existingFiles,
|
|
@@ -1682,7 +1711,7 @@ async function main() {
|
|
|
1682
1711
|
case "transform": {
|
|
1683
1712
|
const { projectDir, progress } = requireProject();
|
|
1684
1713
|
const { parseDuration } = await import("./parse-duration-NVLCEFAF.js");
|
|
1685
|
-
const { runTransform } = await import("./transform-
|
|
1714
|
+
const { runTransform } = await import("./transform-Z4LA7G44.js");
|
|
1686
1715
|
const padding = {};
|
|
1687
1716
|
if (typeof flags["pad-start"] === "string") {
|
|
1688
1717
|
padding.padStartMs = parseDuration(flags["pad-start"]);
|
|
@@ -1710,7 +1739,7 @@ async function main() {
|
|
|
1710
1739
|
console.log("");
|
|
1711
1740
|
console.log(chainResult.previewTable);
|
|
1712
1741
|
}
|
|
1713
|
-
const { runTransform } = await import("./transform-
|
|
1742
|
+
const { runTransform } = await import("./transform-Z4LA7G44.js");
|
|
1714
1743
|
await runTransform({ projectDir, progress });
|
|
1715
1744
|
const { runPreview: runPreview2 } = await import("./preview-UUWVOX5Y.js");
|
|
1716
1745
|
await runPreview2({ projectDir, progress, port, noOpen: flags["no-open"] === true });
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TimingFileSchema
|
|
3
|
+
} from "./chunk-SKRQW7PY.js";
|
|
4
|
+
import {
|
|
5
|
+
validateCuratedFiles
|
|
6
|
+
} from "./chunk-K6N7F6GL.js";
|
|
1
7
|
import {
|
|
2
8
|
buildArtifactData
|
|
3
9
|
} from "./chunk-JITEBF25.js";
|
|
@@ -15,9 +21,6 @@ import {
|
|
|
15
21
|
markInProgress,
|
|
16
22
|
writeProgress
|
|
17
23
|
} from "./chunk-GLABTDMO.js";
|
|
18
|
-
import {
|
|
19
|
-
TimingFileSchema
|
|
20
|
-
} from "./chunk-SKRQW7PY.js";
|
|
21
24
|
|
|
22
25
|
// src/cli/guided/steps/transform.ts
|
|
23
26
|
import { resolve as resolve2 } from "path";
|
|
@@ -659,6 +662,22 @@ function printDataInventory(summary, warnings) {
|
|
|
659
662
|
}
|
|
660
663
|
function runPipeline(dataDir, outDir, options) {
|
|
661
664
|
const { sources, warnings } = loadSources(dataDir);
|
|
665
|
+
const curationValidation = validateCuratedFiles(
|
|
666
|
+
sources.timelineEvents,
|
|
667
|
+
sources.retroPageData,
|
|
668
|
+
sources.retroPageQuotes
|
|
669
|
+
);
|
|
670
|
+
if (curationValidation.errors.length > 0) {
|
|
671
|
+
console.warn("\n\u26A0 Curated file validation issues:\n");
|
|
672
|
+
for (const e of curationValidation.errors) console.warn(` \u2717 ${e}`);
|
|
673
|
+
for (const w of curationValidation.warnings) console.warn(` \u26A0 ${w}`);
|
|
674
|
+
console.warn("\n Transform will continue, but output may be incomplete.");
|
|
675
|
+
console.warn(" Fix the issues above and re-run for best results.\n");
|
|
676
|
+
} else if (curationValidation.warnings.length > 0) {
|
|
677
|
+
console.warn("\n\u26A0 Curated file warnings:\n");
|
|
678
|
+
for (const w of curationValidation.warnings) console.warn(` \u26A0 ${w}`);
|
|
679
|
+
console.warn("");
|
|
680
|
+
}
|
|
662
681
|
const summary = validateSources(sources);
|
|
663
682
|
printDataInventory(summary, warnings);
|
|
664
683
|
const rawData = transformToRawData(sources);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateCuratedFiles,
|
|
3
|
+
validateRetroPageData,
|
|
4
|
+
validateRetroPageQuotes,
|
|
5
|
+
validateTimelineEvents
|
|
6
|
+
} from "./chunk-K6N7F6GL.js";
|
|
7
|
+
export {
|
|
8
|
+
validateCuratedFiles,
|
|
9
|
+
validateRetroPageData,
|
|
10
|
+
validateRetroPageQuotes,
|
|
11
|
+
validateTimelineEvents
|
|
12
|
+
};
|
|
@@ -1795,6 +1795,20 @@ function computeTimeLabelWidth(totalHours) {
|
|
|
1795
1795
|
return `${totalChars}ch`;
|
|
1796
1796
|
}
|
|
1797
1797
|
var MANUAL_SPEEDS = [0.5, 1, 2, 4];
|
|
1798
|
+
var MOBILE_BREAKPOINT = 640;
|
|
1799
|
+
function useIsMobile(breakpoint = MOBILE_BREAKPOINT) {
|
|
1800
|
+
const [isMobile, setIsMobile] = react.useState(
|
|
1801
|
+
() => typeof window !== "undefined" ? window.innerWidth <= breakpoint : false
|
|
1802
|
+
);
|
|
1803
|
+
react.useEffect(() => {
|
|
1804
|
+
const mql = window.matchMedia(`(max-width: ${breakpoint}px)`);
|
|
1805
|
+
const handler = (e) => setIsMobile(e.matches);
|
|
1806
|
+
setIsMobile(mql.matches);
|
|
1807
|
+
mql.addEventListener("change", handler);
|
|
1808
|
+
return () => mql.removeEventListener("change", handler);
|
|
1809
|
+
}, [breakpoint]);
|
|
1810
|
+
return isMobile;
|
|
1811
|
+
}
|
|
1798
1812
|
function PlayIcon({ size = 14 }) {
|
|
1799
1813
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1800
1814
|
"svg",
|
|
@@ -1851,6 +1865,20 @@ function SoundOffIcon({ size = 16 }) {
|
|
|
1851
1865
|
}
|
|
1852
1866
|
);
|
|
1853
1867
|
}
|
|
1868
|
+
function ChevronDownIcon({ size = 10 }) {
|
|
1869
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1870
|
+
"svg",
|
|
1871
|
+
{
|
|
1872
|
+
width: size,
|
|
1873
|
+
height: size,
|
|
1874
|
+
viewBox: "0 0 24 24",
|
|
1875
|
+
fill: "currentColor",
|
|
1876
|
+
role: "img",
|
|
1877
|
+
"aria-label": "Speed menu",
|
|
1878
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7 10l5 5 5-5z" })
|
|
1879
|
+
}
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1854
1882
|
function GearIcon({ size = 14 }) {
|
|
1855
1883
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1856
1884
|
"svg",
|
|
@@ -1878,6 +1906,19 @@ function Footer({
|
|
|
1878
1906
|
}) {
|
|
1879
1907
|
const { progress, playing, speed, toggle, setSpeed, scrub } = playback;
|
|
1880
1908
|
const lastManualSpeed = react.useRef(1);
|
|
1909
|
+
const isMobile = useIsMobile();
|
|
1910
|
+
const [speedMenuOpen, setSpeedMenuOpen] = react.useState(false);
|
|
1911
|
+
const speedMenuRef = react.useRef(null);
|
|
1912
|
+
react.useEffect(() => {
|
|
1913
|
+
if (!speedMenuOpen) return;
|
|
1914
|
+
const handleClickOutside = (e) => {
|
|
1915
|
+
if (speedMenuRef.current && !speedMenuRef.current.contains(e.target)) {
|
|
1916
|
+
setSpeedMenuOpen(false);
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
1920
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
1921
|
+
}, [speedMenuOpen]);
|
|
1881
1922
|
const isAuto = hasTimingFile && !muted;
|
|
1882
1923
|
const handleKeyDown = react.useCallback(
|
|
1883
1924
|
(e) => {
|
|
@@ -1959,10 +2000,69 @@ function Footer({
|
|
|
1959
2000
|
step: 1e-3,
|
|
1960
2001
|
value: progress,
|
|
1961
2002
|
onChange: (e) => scrub(Number(e.target.value)),
|
|
1962
|
-
style: scrubberStyle
|
|
2003
|
+
style: isMobile ? mobileScrubberStyle : scrubberStyle
|
|
1963
2004
|
}
|
|
1964
2005
|
),
|
|
1965
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { style:
|
|
2006
|
+
isMobile ? /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: speedMenuRef, style: speedMenuContainerStyle, children: [
|
|
2007
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2008
|
+
"button",
|
|
2009
|
+
{
|
|
2010
|
+
type: "button",
|
|
2011
|
+
onClick: () => setSpeedMenuOpen((v) => !v),
|
|
2012
|
+
style: {
|
|
2013
|
+
...speedMenuTriggerStyle,
|
|
2014
|
+
color: speedMenuOpen ? "#00e5cc" : "#fff",
|
|
2015
|
+
background: speedMenuOpen ? "rgba(0, 229, 204, 0.12)" : "rgba(255,255,255,0.08)"
|
|
2016
|
+
},
|
|
2017
|
+
title: "Speed",
|
|
2018
|
+
children: [
|
|
2019
|
+
isAuto ? "Auto" : `${speed}\xD7`,
|
|
2020
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChevronDownIcon, {})
|
|
2021
|
+
]
|
|
2022
|
+
}
|
|
2023
|
+
),
|
|
2024
|
+
speedMenuOpen && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: speedMenuDropdownStyle, children: [
|
|
2025
|
+
hasTimingFile && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2026
|
+
"button",
|
|
2027
|
+
{
|
|
2028
|
+
type: "button",
|
|
2029
|
+
onClick: () => {
|
|
2030
|
+
handleAutoClick();
|
|
2031
|
+
setSpeedMenuOpen(false);
|
|
2032
|
+
},
|
|
2033
|
+
style: {
|
|
2034
|
+
...speedMenuItemStyle,
|
|
2035
|
+
color: isAuto ? "#00e5cc" : "#fff",
|
|
2036
|
+
fontWeight: isAuto ? 700 : 400
|
|
2037
|
+
},
|
|
2038
|
+
children: [
|
|
2039
|
+
"Auto ",
|
|
2040
|
+
autoSpeedLabel
|
|
2041
|
+
]
|
|
2042
|
+
}
|
|
2043
|
+
),
|
|
2044
|
+
MANUAL_SPEEDS.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2045
|
+
"button",
|
|
2046
|
+
{
|
|
2047
|
+
type: "button",
|
|
2048
|
+
onClick: () => {
|
|
2049
|
+
handleManualSpeed(s);
|
|
2050
|
+
setSpeedMenuOpen(false);
|
|
2051
|
+
},
|
|
2052
|
+
style: {
|
|
2053
|
+
...speedMenuItemStyle,
|
|
2054
|
+
color: !isAuto && speed === s ? "#00e5cc" : "#fff",
|
|
2055
|
+
fontWeight: !isAuto && speed === s ? 700 : 400
|
|
2056
|
+
},
|
|
2057
|
+
children: [
|
|
2058
|
+
s,
|
|
2059
|
+
"\xD7"
|
|
2060
|
+
]
|
|
2061
|
+
},
|
|
2062
|
+
s
|
|
2063
|
+
))
|
|
2064
|
+
] })
|
|
2065
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: speedGroupStyle, children: [
|
|
1966
2066
|
hasTimingFile && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1967
2067
|
"button",
|
|
1968
2068
|
{
|
|
@@ -2003,7 +2103,7 @@ function Footer({
|
|
|
2003
2103
|
s
|
|
2004
2104
|
))
|
|
2005
2105
|
] }),
|
|
2006
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...timeLabelStyle, width: timeLabelWidth }, children: timeLabel }),
|
|
2106
|
+
!isMobile && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...timeLabelStyle, width: timeLabelWidth }, children: timeLabel }),
|
|
2007
2107
|
hasTimingFile && onToggleMute && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2008
2108
|
"button",
|
|
2009
2109
|
{
|
|
@@ -2102,6 +2202,54 @@ var muteButtonStyle = {
|
|
|
2102
2202
|
minWidth: "32px",
|
|
2103
2203
|
minHeight: "28px"
|
|
2104
2204
|
};
|
|
2205
|
+
var mobileScrubberStyle = {
|
|
2206
|
+
flex: 1,
|
|
2207
|
+
minWidth: 0,
|
|
2208
|
+
cursor: "pointer",
|
|
2209
|
+
accentColor: "#3b82f6",
|
|
2210
|
+
height: "24px"
|
|
2211
|
+
};
|
|
2212
|
+
var speedMenuContainerStyle = {
|
|
2213
|
+
position: "relative",
|
|
2214
|
+
flexShrink: 0
|
|
2215
|
+
};
|
|
2216
|
+
var speedMenuTriggerStyle = {
|
|
2217
|
+
background: "rgba(255,255,255,0.08)",
|
|
2218
|
+
border: "none",
|
|
2219
|
+
borderRadius: "4px",
|
|
2220
|
+
color: "#fff",
|
|
2221
|
+
fontSize: "11px",
|
|
2222
|
+
padding: "4px 6px",
|
|
2223
|
+
cursor: "pointer",
|
|
2224
|
+
display: "flex",
|
|
2225
|
+
alignItems: "center",
|
|
2226
|
+
gap: "2px",
|
|
2227
|
+
minHeight: "28px"
|
|
2228
|
+
};
|
|
2229
|
+
var speedMenuDropdownStyle = {
|
|
2230
|
+
position: "absolute",
|
|
2231
|
+
bottom: "100%",
|
|
2232
|
+
right: 0,
|
|
2233
|
+
marginBottom: "4px",
|
|
2234
|
+
background: "rgba(0, 0, 0, 0.95)",
|
|
2235
|
+
border: "1px solid rgba(255, 255, 255, 0.15)",
|
|
2236
|
+
borderRadius: "6px",
|
|
2237
|
+
padding: "4px 0",
|
|
2238
|
+
display: "flex",
|
|
2239
|
+
flexDirection: "column",
|
|
2240
|
+
minWidth: "80px",
|
|
2241
|
+
zIndex: 100
|
|
2242
|
+
};
|
|
2243
|
+
var speedMenuItemStyle = {
|
|
2244
|
+
background: "none",
|
|
2245
|
+
border: "none",
|
|
2246
|
+
color: "#fff",
|
|
2247
|
+
fontSize: "13px",
|
|
2248
|
+
padding: "8px 12px",
|
|
2249
|
+
cursor: "pointer",
|
|
2250
|
+
textAlign: "left",
|
|
2251
|
+
whiteSpace: "nowrap"
|
|
2252
|
+
};
|
|
2105
2253
|
var devButtonStyle = {
|
|
2106
2254
|
background: "rgba(255,255,255,0.08)",
|
|
2107
2255
|
border: "none",
|
|
@@ -2381,6 +2529,7 @@ function useAudioPlayback({
|
|
|
2381
2529
|
const audioCache = react.useRef(/* @__PURE__ */ new Map());
|
|
2382
2530
|
const activeClipRef = react.useRef(null);
|
|
2383
2531
|
const prevProgressRef = react.useRef(0);
|
|
2532
|
+
const clipAbortRef = react.useRef(null);
|
|
2384
2533
|
const baseUrl = audioBaseUrl.endsWith("/") ? audioBaseUrl : `${audioBaseUrl}/`;
|
|
2385
2534
|
const getAudio = react.useCallback(
|
|
2386
2535
|
(clipId, audioFile) => {
|
|
@@ -2393,21 +2542,67 @@ function useAudioPlayback({
|
|
|
2393
2542
|
},
|
|
2394
2543
|
[baseUrl]
|
|
2395
2544
|
);
|
|
2545
|
+
const abortActiveClip = react.useCallback(() => {
|
|
2546
|
+
if (clipAbortRef.current) {
|
|
2547
|
+
clipAbortRef.current.abort();
|
|
2548
|
+
clipAbortRef.current = null;
|
|
2549
|
+
}
|
|
2550
|
+
}, []);
|
|
2396
2551
|
const stopAll = react.useCallback(() => {
|
|
2552
|
+
abortActiveClip();
|
|
2397
2553
|
for (const audio of audioCache.current.values()) {
|
|
2398
2554
|
audio.pause();
|
|
2399
2555
|
}
|
|
2400
2556
|
activeClipRef.current = null;
|
|
2401
|
-
}, []);
|
|
2557
|
+
}, [abortActiveClip]);
|
|
2558
|
+
const handlePlayError = react.useCallback(
|
|
2559
|
+
(err, audio, signal) => {
|
|
2560
|
+
if (!(err instanceof Error)) {
|
|
2561
|
+
console.warn("Audio play failed:", err);
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
if (err.name === "NotAllowedError") {
|
|
2565
|
+
console.warn("Audio play blocked by autoplay policy \u2014 waiting for user interaction");
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
if (err.name === "NotSupportedError" || err.name === "AbortError") {
|
|
2569
|
+
console.warn(`Audio play failed (${err.name}) \u2014 waiting for canplaythrough to retry`);
|
|
2570
|
+
if (signal.aborted) return;
|
|
2571
|
+
const retryTimeout = setTimeout(() => {
|
|
2572
|
+
cleanup();
|
|
2573
|
+
console.warn("Audio retry timed out after 3s");
|
|
2574
|
+
}, 3e3);
|
|
2575
|
+
const cleanup = () => {
|
|
2576
|
+
clearTimeout(retryTimeout);
|
|
2577
|
+
audio.removeEventListener("canplaythrough", onReady);
|
|
2578
|
+
};
|
|
2579
|
+
const onReady = () => {
|
|
2580
|
+
cleanup();
|
|
2581
|
+
if (signal.aborted) return;
|
|
2582
|
+
audio.play().catch((retryErr) => {
|
|
2583
|
+
console.warn(
|
|
2584
|
+
"Audio retry failed:",
|
|
2585
|
+
retryErr instanceof Error ? retryErr.name : retryErr
|
|
2586
|
+
);
|
|
2587
|
+
});
|
|
2588
|
+
};
|
|
2589
|
+
audio.addEventListener("canplaythrough", onReady, { once: true, signal });
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
console.warn("Audio play failed:", err.name, err.message);
|
|
2593
|
+
},
|
|
2594
|
+
[]
|
|
2595
|
+
);
|
|
2402
2596
|
react.useEffect(() => {
|
|
2403
2597
|
return () => {
|
|
2598
|
+
abortActiveClip();
|
|
2404
2599
|
for (const audio of audioCache.current.values()) {
|
|
2405
2600
|
audio.pause();
|
|
2406
2601
|
audio.src = "";
|
|
2407
2602
|
}
|
|
2408
2603
|
audioCache.current.clear();
|
|
2409
2604
|
};
|
|
2410
|
-
}, []);
|
|
2605
|
+
}, [abortActiveClip]);
|
|
2411
2606
|
react.useEffect(() => {
|
|
2412
2607
|
if (!timingFile) return;
|
|
2413
2608
|
for (const line of timingFile.lines) {
|
|
@@ -2457,9 +2652,10 @@ function useAudioPlayback({
|
|
|
2457
2652
|
if (activeClipRef.current !== clipId) {
|
|
2458
2653
|
stopAll();
|
|
2459
2654
|
activeClipRef.current = clipId;
|
|
2655
|
+
const controller = new AbortController();
|
|
2656
|
+
clipAbortRef.current = controller;
|
|
2460
2657
|
audio.currentTime = offsetSec;
|
|
2461
|
-
audio.play().catch(() =>
|
|
2462
|
-
});
|
|
2658
|
+
audio.play().catch((err) => handlePlayError(err, audio, controller.signal));
|
|
2463
2659
|
} else {
|
|
2464
2660
|
const progressDelta = Math.abs(progress - prevProgressRef.current);
|
|
2465
2661
|
const isLikelyScrub = progressDelta > 0.01;
|
|
@@ -2470,12 +2666,15 @@ function useAudioPlayback({
|
|
|
2470
2666
|
}
|
|
2471
2667
|
}
|
|
2472
2668
|
if (audio.paused) {
|
|
2473
|
-
|
|
2669
|
+
const signal = clipAbortRef.current?.signal;
|
|
2670
|
+
audio.play().catch((err) => {
|
|
2671
|
+
if (signal) handlePlayError(err, audio, signal);
|
|
2672
|
+
else console.warn("Audio play failed:", err instanceof Error ? err.name : err);
|
|
2474
2673
|
});
|
|
2475
2674
|
}
|
|
2476
2675
|
}
|
|
2477
2676
|
prevProgressRef.current = progress;
|
|
2478
|
-
}, [progress, playing, timingFile, getAudio, stopAll]);
|
|
2677
|
+
}, [progress, playing, timingFile, getAudio, stopAll, handlePlayError]);
|
|
2479
2678
|
return { forceLoadAll };
|
|
2480
2679
|
}
|
|
2481
2680
|
var SFX_TRIGGER_WINDOW_SEC = 0.3;
|