miriad-viz 0.7.2 → 0.8.0
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/{from-pacing-chain-ZP4XGDJ7.js → from-pacing-chain-ENCSRNXM.js} +16 -0
- package/dist-cli/index.js +157 -46
- package/dist-lib/viewer/exports.cjs +36 -35
- package/dist-lib/viewer/exports.cjs.map +1 -1
- package/dist-lib/viewer/exports.js +36 -35
- package/dist-lib/viewer/exports.js.map +1 -1
- package/package.json +1 -1
|
@@ -84,6 +84,22 @@ function generateTiming(script, pacing) {
|
|
|
84
84
|
phase: line.phase
|
|
85
85
|
};
|
|
86
86
|
});
|
|
87
|
+
const validationErrors = [];
|
|
88
|
+
for (const line of timedLines) {
|
|
89
|
+
const expectedRange = totalProjectTime > 0 ? line.durationSec / totalProjectTime : 0;
|
|
90
|
+
const actualRange = line.progressEnd - line.progressStart;
|
|
91
|
+
if (Math.abs(actualRange - expectedRange) > 1e-3) {
|
|
92
|
+
validationErrors.push(
|
|
93
|
+
`"${line.id}": progress range ${actualRange.toFixed(4)} != expected ${expectedRange.toFixed(4)}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (validationErrors.length > 0) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Timing validation failed (${validationErrors.length} clip${validationErrors.length > 1 ? "s" : ""}):
|
|
100
|
+
${validationErrors.join("\n")}`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
87
103
|
return {
|
|
88
104
|
version: 1,
|
|
89
105
|
totalDurationSec,
|
package/dist-cli/index.js
CHANGED
|
@@ -216,18 +216,6 @@ function classifyIntensity(total) {
|
|
|
216
216
|
if (total <= INTENSITY_THRESHOLDS.busy) return "busy";
|
|
217
217
|
return "intense";
|
|
218
218
|
}
|
|
219
|
-
function getSuggestion(intensity) {
|
|
220
|
-
switch (intensity) {
|
|
221
|
-
case "quiet":
|
|
222
|
-
return "high gapVizSpeed (skip through quiet period)";
|
|
223
|
-
case "moderate":
|
|
224
|
-
return "default vizSpeed (normal pacing)";
|
|
225
|
-
case "busy":
|
|
226
|
-
return "low vizSpeed (let viewer see the activity)";
|
|
227
|
-
case "intense":
|
|
228
|
-
return "low vizSpeed (let viewer see the frenzy)";
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
219
|
function countInWindow(sortedTimestamps, start, end) {
|
|
232
220
|
let count = 0;
|
|
233
221
|
for (const t of sortedTimestamps) {
|
|
@@ -243,18 +231,23 @@ function computeEventDensity(input, lines, projectStart, projectEnd) {
|
|
|
243
231
|
if (Number.isNaN(startMs) || Number.isNaN(endMs) || endMs <= startMs) {
|
|
244
232
|
return [];
|
|
245
233
|
}
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
const
|
|
234
|
+
const toSortedMs = (dates) => dates.map((d) => new Date(d).getTime()).filter((t) => !Number.isNaN(t)).sort((a, b) => a - b);
|
|
235
|
+
const commitMs = toSortedMs(input.commitDates);
|
|
236
|
+
const prMs = toSortedMs(input.prMergeDates);
|
|
237
|
+
const msgMs = toSortedMs(input.messageDates);
|
|
238
|
+
const pillMs = toSortedMs(input.pillDates);
|
|
239
|
+
const fileMs = toSortedMs(input.fileDates);
|
|
249
240
|
const totalDuration = endMs - startMs;
|
|
250
241
|
const sliceDuration = totalDuration / lines.length;
|
|
251
242
|
return lines.map((line, i) => {
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
const commits = countInWindow(commitMs,
|
|
255
|
-
const prs = countInWindow(prMs,
|
|
256
|
-
const messages = countInWindow(msgMs,
|
|
257
|
-
const
|
|
243
|
+
const wStart = startMs + i * sliceDuration;
|
|
244
|
+
const wEnd = startMs + (i + 1) * sliceDuration;
|
|
245
|
+
const commits = countInWindow(commitMs, wStart, wEnd);
|
|
246
|
+
const prs = countInWindow(prMs, wStart, wEnd);
|
|
247
|
+
const messages = countInWindow(msgMs, wStart, wEnd);
|
|
248
|
+
const pills = countInWindow(pillMs, wStart, wEnd);
|
|
249
|
+
const files = countInWindow(fileMs, wStart, wEnd);
|
|
250
|
+
const total = commits + prs + messages + pills + files;
|
|
258
251
|
const intensity = classifyIntensity(total);
|
|
259
252
|
return {
|
|
260
253
|
lineId: line.id,
|
|
@@ -263,22 +256,23 @@ function computeEventDensity(input, lines, projectStart, projectEnd) {
|
|
|
263
256
|
commits,
|
|
264
257
|
prs,
|
|
265
258
|
messages,
|
|
259
|
+
pills,
|
|
260
|
+
files,
|
|
266
261
|
total,
|
|
267
262
|
intensity,
|
|
268
|
-
|
|
263
|
+
windowStart: new Date(wStart).toISOString(),
|
|
264
|
+
windowEnd: new Date(wEnd).toISOString()
|
|
269
265
|
};
|
|
270
266
|
});
|
|
271
267
|
}
|
|
272
|
-
function formatDensityReport(reports, projectStart, projectEnd) {
|
|
268
|
+
function formatDensityReport(reports, projectStart, projectEnd, timingLines) {
|
|
273
269
|
if (reports.length === 0) return [];
|
|
274
|
-
const lines = [];
|
|
275
270
|
const startMs = new Date(projectStart).getTime();
|
|
276
271
|
const endMs = new Date(projectEnd).getTime();
|
|
277
272
|
const totalDuration = endMs - startMs;
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const d = new Date(ms);
|
|
273
|
+
const lines = [];
|
|
274
|
+
const formatTime = (iso) => {
|
|
275
|
+
const d = new Date(iso);
|
|
282
276
|
const months = [
|
|
283
277
|
"Jan",
|
|
284
278
|
"Feb",
|
|
@@ -295,22 +289,50 @@ function formatDensityReport(reports, projectStart, projectEnd) {
|
|
|
295
289
|
];
|
|
296
290
|
return `${months[d.getUTCMonth()]} ${d.getUTCDate()} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
|
|
297
291
|
};
|
|
292
|
+
lines.push(" \u{1F4CA} Activity density per narration line:");
|
|
298
293
|
lines.push(
|
|
299
|
-
` Your script covers ${formatTime(
|
|
294
|
+
` Your script covers ${formatTime(projectStart)} \u2192 ${formatTime(projectEnd)} (${Math.round(totalDuration / 36e5)}h)`
|
|
300
295
|
);
|
|
301
296
|
lines.push("");
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
const windowEnd = startMs + (i + 1) * sliceDuration;
|
|
306
|
-
const intensityLabel = r.intensity === "intense" ? "INTENSE" : r.intensity;
|
|
307
|
-
lines.push(` ${r.lineId} "${r.text}"`);
|
|
308
|
-
lines.push(
|
|
309
|
-
` covers: ${formatTime(windowStart)}-${formatTime(windowEnd)} | ${r.prs} PRs, ${r.messages} messages (${intensityLabel})`
|
|
310
|
-
);
|
|
311
|
-
lines.push(` \u2192 suggestion: ${r.suggestion}`);
|
|
312
|
-
lines.push("");
|
|
297
|
+
const timingMap = /* @__PURE__ */ new Map();
|
|
298
|
+
if (timingLines) {
|
|
299
|
+
for (const tl of timingLines) timingMap.set(tl.id, tl);
|
|
313
300
|
}
|
|
301
|
+
const hasTiming = timingMap.size > 0;
|
|
302
|
+
const idW = Math.max(7, ...reports.map((r) => r.lineId.length));
|
|
303
|
+
const pad = (s, w) => s.padEnd(w);
|
|
304
|
+
const rpad = (s, w) => s.padStart(w);
|
|
305
|
+
let header = ` ${pad("Line ID", idW)}`;
|
|
306
|
+
if (hasTiming) header += ` | ${rpad("Dur", 6)} | ${rpad("Speed", 5)}`;
|
|
307
|
+
header += ` | ${rpad("Pills", 5)} | ${rpad("Msgs", 5)} | ${rpad("Cmts", 5)} | ${rpad("PRs", 4)} | ${rpad("Files", 5)} | Time Range`;
|
|
308
|
+
lines.push(header);
|
|
309
|
+
let sep = ` ${"\u2500".repeat(idW)}`;
|
|
310
|
+
if (hasTiming) sep += `\u2500\u253C${"\u2500".repeat(8)}\u2500\u253C${"\u2500".repeat(7)}`;
|
|
311
|
+
sep += `\u2500\u253C${"\u2500".repeat(7)}\u2500\u253C${"\u2500".repeat(7)}\u2500\u253C${"\u2500".repeat(7)}\u2500\u253C${"\u2500".repeat(6)}\u2500\u253C${"\u2500".repeat(7)}\u2500\u253C${"\u2500".repeat(20)}`;
|
|
312
|
+
lines.push(sep);
|
|
313
|
+
for (const r of reports) {
|
|
314
|
+
const tl = timingMap.get(r.lineId);
|
|
315
|
+
let row = ` ${pad(r.lineId, idW)}`;
|
|
316
|
+
if (hasTiming) {
|
|
317
|
+
const dur = tl ? `${tl.durationSec.toFixed(1)}s` : "\u2014";
|
|
318
|
+
const spd = tl ? `${tl.vizSpeed.toFixed(1)}x` : "\u2014";
|
|
319
|
+
row += ` | ${rpad(dur, 6)} | ${rpad(spd, 5)}`;
|
|
320
|
+
}
|
|
321
|
+
row += ` | ${rpad(String(r.pills), 5)} | ${rpad(String(r.messages), 5)} | ${rpad(String(r.commits), 5)} | ${rpad(String(r.prs), 4)} | ${rpad(String(r.files), 5)}`;
|
|
322
|
+
row += ` | ${formatTime(r.windowStart)}-${formatTime(r.windowEnd)}`;
|
|
323
|
+
lines.push(row);
|
|
324
|
+
}
|
|
325
|
+
lines.push("");
|
|
326
|
+
lines.push(" \u{1F4CA} Pacing guidance:");
|
|
327
|
+
lines.push(" - High pill count \u2192 lower vizSpeed so pills appear one-by-one like dialog");
|
|
328
|
+
lines.push(" - Zero activity \u2192 raise vizSpeed to skip dead time");
|
|
329
|
+
lines.push(
|
|
330
|
+
" - Messages \u2260 pills: messages = raw activity density, pills = curated selections that actually render"
|
|
331
|
+
);
|
|
332
|
+
lines.push(
|
|
333
|
+
" - Commits/PRs/Files are visual events too \u2014 a section can be busy even if chat is quiet"
|
|
334
|
+
);
|
|
335
|
+
lines.push(" - Goal: narration should match what the viewer shows on screen");
|
|
314
336
|
return lines;
|
|
315
337
|
}
|
|
316
338
|
|
|
@@ -329,14 +351,16 @@ function buildVizTimingPending(_progress) {
|
|
|
329
351
|
output.push(" (once pacing.json exists, you'll get the iterate loop instructions)");
|
|
330
352
|
return output;
|
|
331
353
|
}
|
|
332
|
-
function buildVizTimingInProgress(_progress, densityInput, scriptLines, projectStart, projectEnd) {
|
|
354
|
+
function buildVizTimingInProgress(_progress, densityInput, scriptLines, projectStart, projectEnd, timingLines) {
|
|
333
355
|
const output = [];
|
|
334
356
|
output.push("", "\u23F1\uFE0F Viz Timing \u2014 Iterate Loop");
|
|
335
357
|
if (densityInput && scriptLines && scriptLines.length > 0 && projectStart && projectEnd) {
|
|
336
358
|
const reports = computeEventDensity(densityInput, scriptLines, projectStart, projectEnd);
|
|
337
359
|
if (reports.length > 0) {
|
|
338
360
|
output.push("");
|
|
339
|
-
output.push(
|
|
361
|
+
output.push(
|
|
362
|
+
...formatDensityReport(reports, projectStart, projectEnd, timingLines ?? void 0)
|
|
363
|
+
);
|
|
340
364
|
}
|
|
341
365
|
}
|
|
342
366
|
output.push("");
|
|
@@ -458,7 +482,7 @@ function buildCommand(step, progress, flags) {
|
|
|
458
482
|
}
|
|
459
483
|
return parts.join(" ");
|
|
460
484
|
}
|
|
461
|
-
function computeNext(progress, existingFiles, logTails, flags = {}, dataSummary, env = {}) {
|
|
485
|
+
function computeNext(progress, existingFiles, logTails, flags = {}, dataSummary, env = {}, densityData) {
|
|
462
486
|
const fileSet = new Set(existingFiles);
|
|
463
487
|
const step = getNextStep(progress);
|
|
464
488
|
if (!step) {
|
|
@@ -845,7 +869,16 @@ function computeNext(progress, existingFiles, logTails, flags = {}, dataSummary,
|
|
|
845
869
|
};
|
|
846
870
|
}
|
|
847
871
|
if (state.status === "in_progress") {
|
|
848
|
-
output.push(
|
|
872
|
+
output.push(
|
|
873
|
+
...buildVizTimingInProgress(
|
|
874
|
+
progress,
|
|
875
|
+
densityData?.densityInput ?? null,
|
|
876
|
+
densityData?.scriptLines ?? null,
|
|
877
|
+
densityData?.projectStart ?? null,
|
|
878
|
+
densityData?.projectEnd ?? null,
|
|
879
|
+
densityData?.timingLines ?? null
|
|
880
|
+
)
|
|
881
|
+
);
|
|
849
882
|
return { action: "waiting_for_edit", step, progress, output };
|
|
850
883
|
}
|
|
851
884
|
output.push(...buildVizTimingPending(progress));
|
|
@@ -1378,6 +1411,75 @@ function gatherDataSummary(projectDir, dataDir) {
|
|
|
1378
1411
|
data.chatActivity = tryRead("chat-activity.json");
|
|
1379
1412
|
return buildDataSummary(data);
|
|
1380
1413
|
}
|
|
1414
|
+
function gatherDensityData(projectDir, dataDir) {
|
|
1415
|
+
const tryRead = (filename) => {
|
|
1416
|
+
const path = resolve3(projectDir, dataDir, filename);
|
|
1417
|
+
if (!existsSync3(path)) return null;
|
|
1418
|
+
try {
|
|
1419
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
1420
|
+
} catch {
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
const script = tryRead("script.json");
|
|
1425
|
+
if (!script?.lines || script.lines.length === 0) return null;
|
|
1426
|
+
const vizData = tryRead("viz-data.json");
|
|
1427
|
+
const messagesFull = tryRead("messages-full.json");
|
|
1428
|
+
const timing = tryRead("timing.json");
|
|
1429
|
+
const input = {
|
|
1430
|
+
commitDates: [],
|
|
1431
|
+
prMergeDates: [],
|
|
1432
|
+
messageDates: [],
|
|
1433
|
+
pillDates: [],
|
|
1434
|
+
fileDates: []
|
|
1435
|
+
};
|
|
1436
|
+
if (vizData?.events) {
|
|
1437
|
+
for (const evt of vizData.events) {
|
|
1438
|
+
const iso = new Date(evt.timestamp).toISOString();
|
|
1439
|
+
switch (evt.type) {
|
|
1440
|
+
case "commit":
|
|
1441
|
+
input.commitDates.push(iso);
|
|
1442
|
+
break;
|
|
1443
|
+
case "pr_created":
|
|
1444
|
+
case "pr_merged":
|
|
1445
|
+
input.prMergeDates.push(iso);
|
|
1446
|
+
break;
|
|
1447
|
+
case "message":
|
|
1448
|
+
input.pillDates.push(iso);
|
|
1449
|
+
break;
|
|
1450
|
+
case "artifact":
|
|
1451
|
+
input.fileDates.push(iso);
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
if (Array.isArray(messagesFull)) {
|
|
1457
|
+
for (const msg of messagesFull) {
|
|
1458
|
+
if (msg.timestamp) input.messageDates.push(msg.timestamp);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
let projectStart = null;
|
|
1462
|
+
let projectEnd = null;
|
|
1463
|
+
if (vizData?.timeRange) {
|
|
1464
|
+
projectStart = new Date(vizData.timeRange.start).toISOString();
|
|
1465
|
+
projectEnd = new Date(vizData.timeRange.end).toISOString();
|
|
1466
|
+
}
|
|
1467
|
+
let timingLines = null;
|
|
1468
|
+
if (timing?.lines) {
|
|
1469
|
+
timingLines = timing.lines.map((l) => ({
|
|
1470
|
+
id: l.id,
|
|
1471
|
+
durationSec: l.durationSec,
|
|
1472
|
+
vizSpeed: l.vizSpeed
|
|
1473
|
+
}));
|
|
1474
|
+
}
|
|
1475
|
+
return {
|
|
1476
|
+
densityInput: input,
|
|
1477
|
+
scriptLines: script.lines,
|
|
1478
|
+
projectStart,
|
|
1479
|
+
projectEnd,
|
|
1480
|
+
timingLines
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1381
1483
|
async function runNext(flags) {
|
|
1382
1484
|
const { projectDir, progress } = requireProject();
|
|
1383
1485
|
const dataDir = resolve3(projectDir, progress.project.dataDir);
|
|
@@ -1437,7 +1539,16 @@ async function runNext(flags) {
|
|
|
1437
1539
|
const existingFiles = [...gatherExistingFiles(projectDir), ...gatherAudioFiles(projectDir)];
|
|
1438
1540
|
const logTails = gatherLogTails(projectDir);
|
|
1439
1541
|
const dataSummary = gatherDataSummary(projectDir, progress.project.dataDir);
|
|
1440
|
-
const
|
|
1542
|
+
const densityData = gatherDensityData(projectDir, progress.project.dataDir);
|
|
1543
|
+
const result = computeNext(
|
|
1544
|
+
progress,
|
|
1545
|
+
existingFiles,
|
|
1546
|
+
logTails,
|
|
1547
|
+
nextFlags,
|
|
1548
|
+
dataSummary,
|
|
1549
|
+
env,
|
|
1550
|
+
densityData
|
|
1551
|
+
);
|
|
1441
1552
|
for (const line of result.output) {
|
|
1442
1553
|
console.log(line);
|
|
1443
1554
|
}
|
|
@@ -1530,7 +1641,7 @@ async function main() {
|
|
|
1530
1641
|
const port = typeof flags.port === "string" ? Number.parseInt(flags.port, 10) : void 0;
|
|
1531
1642
|
if (flags["from-pacing"] === true) {
|
|
1532
1643
|
const dataDir = resolve3(projectDir, progress.project.dataDir);
|
|
1533
|
-
const { runFromPacingChain } = await import("./from-pacing-chain-
|
|
1644
|
+
const { runFromPacingChain } = await import("./from-pacing-chain-ENCSRNXM.js");
|
|
1534
1645
|
console.log("\n\u{1F517} Running from-pacing chain: timing \u2192 transform \u2192 preview\n");
|
|
1535
1646
|
const chainResult = runFromPacingChain(dataDir);
|
|
1536
1647
|
if (!chainResult.success) {
|
|
@@ -1883,40 +1883,6 @@ function Footer({
|
|
|
1883
1883
|
(e) => {
|
|
1884
1884
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
|
1885
1885
|
switch (e.code) {
|
|
1886
|
-
case "Space": {
|
|
1887
|
-
e.preventDefault();
|
|
1888
|
-
toggle();
|
|
1889
|
-
break;
|
|
1890
|
-
}
|
|
1891
|
-
case "ArrowRight": {
|
|
1892
|
-
e.preventDefault();
|
|
1893
|
-
if (isAuto) {
|
|
1894
|
-
onToggleMute?.();
|
|
1895
|
-
lastManualSpeed.current = 1;
|
|
1896
|
-
setSpeed(1);
|
|
1897
|
-
} else {
|
|
1898
|
-
const idx = MANUAL_SPEEDS.indexOf(speed);
|
|
1899
|
-
if (idx < MANUAL_SPEEDS.length - 1) {
|
|
1900
|
-
const next = MANUAL_SPEEDS[idx + 1];
|
|
1901
|
-
lastManualSpeed.current = next;
|
|
1902
|
-
setSpeed(next);
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
break;
|
|
1906
|
-
}
|
|
1907
|
-
case "ArrowLeft": {
|
|
1908
|
-
e.preventDefault();
|
|
1909
|
-
if (isAuto) break;
|
|
1910
|
-
const idx = MANUAL_SPEEDS.indexOf(speed);
|
|
1911
|
-
if (idx === 0 && hasTimingFile) {
|
|
1912
|
-
onToggleMute?.();
|
|
1913
|
-
} else if (idx > 0) {
|
|
1914
|
-
const prev = MANUAL_SPEEDS[idx - 1];
|
|
1915
|
-
lastManualSpeed.current = prev;
|
|
1916
|
-
setSpeed(prev);
|
|
1917
|
-
}
|
|
1918
|
-
break;
|
|
1919
|
-
}
|
|
1920
1886
|
case "Home": {
|
|
1921
1887
|
e.preventDefault();
|
|
1922
1888
|
scrub(0);
|
|
@@ -1944,7 +1910,7 @@ function Footer({
|
|
|
1944
1910
|
}
|
|
1945
1911
|
}
|
|
1946
1912
|
},
|
|
1947
|
-
[
|
|
1913
|
+
[scrub, setSpeed, onToggleMute, onToggleDev, hasTimingFile, isAuto]
|
|
1948
1914
|
);
|
|
1949
1915
|
react.useEffect(() => {
|
|
1950
1916
|
window.addEventListener("keydown", handleKeyDown);
|
|
@@ -2737,6 +2703,41 @@ function usePlayback(durationSeconds = FALLBACK_DURATION, timingFile = null) {
|
|
|
2737
2703
|
progressRef.current = clamped;
|
|
2738
2704
|
setProgress(clamped);
|
|
2739
2705
|
}, []);
|
|
2706
|
+
react.useEffect(() => {
|
|
2707
|
+
const SKIP_SECONDS = 5;
|
|
2708
|
+
const handleKeyDown = (e) => {
|
|
2709
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
|
2710
|
+
if (e.key === " " || e.key === "Space") {
|
|
2711
|
+
e.preventDefault();
|
|
2712
|
+
if (playingRef.current) {
|
|
2713
|
+
setPlaying(false);
|
|
2714
|
+
} else {
|
|
2715
|
+
if (progressRef.current >= 1) {
|
|
2716
|
+
progressRef.current = 0;
|
|
2717
|
+
setProgress(0);
|
|
2718
|
+
}
|
|
2719
|
+
lastTimeRef.current = null;
|
|
2720
|
+
setPlaying(true);
|
|
2721
|
+
}
|
|
2722
|
+
} else if (e.key === "ArrowLeft") {
|
|
2723
|
+
e.preventDefault();
|
|
2724
|
+
const denominator = totalProjectTimeRef.current ?? durationSeconds;
|
|
2725
|
+
const delta = SKIP_SECONDS / denominator;
|
|
2726
|
+
const next = Math.max(0, progressRef.current - delta);
|
|
2727
|
+
progressRef.current = next;
|
|
2728
|
+
setProgress(next);
|
|
2729
|
+
} else if (e.key === "ArrowRight") {
|
|
2730
|
+
e.preventDefault();
|
|
2731
|
+
const denominator = totalProjectTimeRef.current ?? durationSeconds;
|
|
2732
|
+
const delta = SKIP_SECONDS / denominator;
|
|
2733
|
+
const next = Math.min(1, progressRef.current + delta);
|
|
2734
|
+
progressRef.current = next;
|
|
2735
|
+
setProgress(next);
|
|
2736
|
+
}
|
|
2737
|
+
};
|
|
2738
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
2739
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
2740
|
+
}, [durationSeconds]);
|
|
2740
2741
|
return { progress, playing, speed, play, pause, toggle, setSpeed, scrub };
|
|
2741
2742
|
}
|
|
2742
2743
|
function parseViewportString(raw) {
|