@rightkit/video-ui 0.0.0-stage → 0.1.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/CHANGELOG.md +5 -0
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +47 -2
- package/dist/appName.d.ts +2 -0
- package/dist/appName.js +8 -0
- package/dist/components/DesktopWorkspace.d.ts +4 -0
- package/dist/components/DesktopWorkspace.js +527 -0
- package/dist/components/Player.d.ts +49 -0
- package/dist/components/Player.js +335 -0
- package/dist/components/RangePreview.d.ts +9 -0
- package/dist/components/RangePreview.js +22 -0
- package/dist/components/Workspace.css +15 -0
- package/dist/demo/DemoSettings.d.ts +1 -0
- package/dist/demo/DemoSettings.js +43 -0
- package/dist/demo/bindings.d.ts +121 -0
- package/dist/demo/bindings.js +57 -0
- package/dist/demo/demo.css +124 -0
- package/dist/demo/useDemo.d.ts +8 -0
- package/dist/demo/useDemo.js +37 -0
- package/dist/editor/CaptionInspector.d.ts +8 -0
- package/dist/editor/CaptionInspector.js +33 -0
- package/dist/editor/ChangesPanel.d.ts +13 -0
- package/dist/editor/ChangesPanel.js +87 -0
- package/dist/editor/ColourInspector.d.ts +5 -0
- package/dist/editor/ColourInspector.js +341 -0
- package/dist/editor/Inspector.d.ts +15 -0
- package/dist/editor/Inspector.js +202 -0
- package/dist/editor/Timeline.d.ts +39 -0
- package/dist/editor/Timeline.js +506 -0
- package/dist/editor/TimelineEditorView.d.ts +9 -0
- package/dist/editor/TimelineEditorView.js +237 -0
- package/dist/editor/TranscriptPanel.d.ts +20 -0
- package/dist/editor/TranscriptPanel.js +180 -0
- package/dist/editor/actions.d.ts +63 -0
- package/dist/editor/actions.js +124 -0
- package/dist/editor/adapter.d.ts +76 -0
- package/dist/editor/adapter.js +181 -0
- package/dist/editor/capabilities.d.ts +37 -0
- package/dist/editor/capabilities.js +33 -0
- package/dist/editor/gesture.d.ts +29 -0
- package/dist/editor/gesture.js +119 -0
- package/dist/editor/index.d.ts +10 -0
- package/dist/editor/index.js +7 -0
- package/dist/editor/keys.d.ts +15 -0
- package/dist/editor/keys.js +42 -0
- package/dist/editor/model.d.ts +88 -0
- package/dist/editor/model.js +87 -0
- package/dist/editor/transcript.d.ts +27 -0
- package/dist/editor/transcript.js +59 -0
- package/dist/editor/vendor/hyperframes/PlayheadIndicator.d.ts +9 -0
- package/dist/editor/vendor/hyperframes/PlayheadIndicator.js +38 -0
- package/dist/editor/vendor/hyperframes/audioWaveformPeaks.d.ts +7 -0
- package/dist/editor/vendor/hyperframes/audioWaveformPeaks.js +47 -0
- package/dist/editor/vendor/hyperframes/rulerGeometry.d.ts +13 -0
- package/dist/editor/vendor/hyperframes/rulerGeometry.js +133 -0
- package/dist/editor/vendor/hyperframes/useInspectorGestureTransaction.d.ts +33 -0
- package/dist/editor/vendor/hyperframes/useInspectorGestureTransaction.js +126 -0
- package/dist/env.d.ts +66 -0
- package/dist/hooks/useEngine.d.ts +10 -0
- package/dist/hooks/useEngine.js +68 -0
- package/dist/hooks/useHdrToSdr.d.ts +11 -0
- package/dist/hooks/useHdrToSdr.js +19 -0
- package/dist/hooks/useProgress.d.ts +10 -0
- package/dist/hooks/useProgress.js +32 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +57 -0
- package/dist/legal/legalConfig.d.ts +38 -0
- package/dist/legal/legalConfig.js +77 -0
- package/dist/lib/assemble.d.ts +100 -0
- package/dist/lib/assemble.js +72 -0
- package/dist/lib/asset.d.ts +2 -0
- package/dist/lib/asset.js +3 -0
- package/dist/lib/dialog.d.ts +4 -0
- package/dist/lib/dialog.js +44 -0
- package/dist/lib/engine.d.ts +44 -0
- package/dist/lib/engine.js +51 -0
- package/dist/lib/genright.d.ts +90 -0
- package/dist/lib/genright.js +160 -0
- package/dist/lib/media.d.ts +1045 -0
- package/dist/lib/media.js +171 -0
- package/dist/lib/mediaAdapters.d.ts +18 -0
- package/dist/lib/mediaAdapters.js +46 -0
- package/dist/lib/mic.d.ts +38 -0
- package/dist/lib/mic.js +14 -0
- package/dist/lib/models.d.ts +53 -0
- package/dist/lib/models.js +72 -0
- package/dist/lib/modelsBackend.d.ts +3 -0
- package/dist/lib/modelsBackend.js +68 -0
- package/dist/lib/progress.d.ts +77 -0
- package/dist/lib/progress.js +192 -0
- package/dist/lib/project.d.ts +138 -0
- package/dist/lib/project.js +48 -0
- package/dist/lib/services.d.ts +24 -0
- package/dist/lib/services.js +38 -0
- package/dist/lib/settingsBackend.d.ts +2 -0
- package/dist/lib/settingsBackend.js +57 -0
- package/dist/lib/shell.d.ts +171 -0
- package/dist/lib/shell.js +32 -0
- package/dist/styles.css +16 -0
- package/dist/ui/icons.d.ts +7 -0
- package/dist/ui/icons.js +62 -0
- package/dist/ui/index.d.ts +45 -0
- package/dist/ui/index.js +43 -0
- package/dist/ui/ui.css +104 -0
- package/dist/views/Assets.css +99 -0
- package/dist/views/AssetsView.d.ts +6 -0
- package/dist/views/AssetsView.js +371 -0
- package/dist/views/AutoEditView.d.ts +16 -0
- package/dist/views/AutoEditView.js +452 -0
- package/dist/views/BatchDelivery.d.ts +3 -0
- package/dist/views/BatchDelivery.js +331 -0
- package/dist/views/BundleExport.d.ts +3 -0
- package/dist/views/BundleExport.js +81 -0
- package/dist/views/Exports.css +117 -0
- package/dist/views/ExportsView.d.ts +12 -0
- package/dist/views/ExportsView.js +354 -0
- package/dist/views/FinishedEdit.css +546 -0
- package/dist/views/Home.css +97 -0
- package/dist/views/Home.d.ts +14 -0
- package/dist/views/Home.js +192 -0
- package/dist/views/Media.css +65 -0
- package/dist/views/OnboardingGate.d.ts +10 -0
- package/dist/views/OnboardingGate.js +54 -0
- package/dist/views/Phrases.css +45 -0
- package/dist/views/PhrasesView.d.ts +4 -0
- package/dist/views/PhrasesView.js +104 -0
- package/dist/views/ProgressView.d.ts +19 -0
- package/dist/views/ProgressView.js +36 -0
- package/dist/views/Prompt.css +84 -0
- package/dist/views/PromptPanel.d.ts +8 -0
- package/dist/views/PromptPanel.js +169 -0
- package/dist/views/Recipe.css +84 -0
- package/dist/views/RecipeLibrary.d.ts +7 -0
- package/dist/views/RecipeLibrary.js +104 -0
- package/dist/views/Retention.css +49 -0
- package/dist/views/RetentionView.d.ts +7 -0
- package/dist/views/RetentionView.js +145 -0
- package/dist/views/SettingsView.d.ts +9 -0
- package/dist/views/SettingsView.js +235 -0
- package/dist/views/Shorts.css +112 -0
- package/dist/views/ShortsView.d.ts +4 -0
- package/dist/views/ShortsView.js +205 -0
- package/dist/views/SourcesView.d.ts +6 -0
- package/dist/views/SourcesView.js +417 -0
- package/dist/views/TimelineView.d.ts +20 -0
- package/dist/views/TimelineView.js +12 -0
- package/package.json +70 -3
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, IconButton, Select, Slider } from "@rightkit/react-shell/react";
|
|
3
|
+
import { invoke } from "@tauri-apps/api/core";
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { errorMessage } from "../lib/engine.js";
|
|
6
|
+
import { playbackClose, playbackControl, playbackFrame, playbackOpen, PLAYBACK_REFRESHED, } from "../lib/media.js";
|
|
7
|
+
import { formatSeconds } from "../lib/project.js";
|
|
8
|
+
import { Icon, timecode } from "../ui/index.js";
|
|
9
|
+
/**
|
|
10
|
+
* Preview-only output gain (0.0..=1.0, 0 mutes). Drives the native transport's
|
|
11
|
+
* audio callback; export/render code never reads it.
|
|
12
|
+
*/
|
|
13
|
+
const playbackSetGain = (value) => invoke("playback_set_gain", { value });
|
|
14
|
+
/** Draw one binary frame reply onto the canvas; returns the decoded header. */
|
|
15
|
+
function drawFrame(canvas, buf) {
|
|
16
|
+
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
17
|
+
if (bytes.length < 24)
|
|
18
|
+
return null;
|
|
19
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
20
|
+
const w = view.getUint32(0, true);
|
|
21
|
+
const h = view.getUint32(4, true);
|
|
22
|
+
const frame = view.getUint32(8, true);
|
|
23
|
+
const total = view.getUint32(12, true);
|
|
24
|
+
const pts = view.getFloat64(16, true);
|
|
25
|
+
const rgba = new Uint8ClampedArray(w * h * 4);
|
|
26
|
+
rgba.set(bytes.subarray(24, 24 + w * h * 4));
|
|
27
|
+
if (canvas.width !== w)
|
|
28
|
+
canvas.width = w;
|
|
29
|
+
if (canvas.height !== h)
|
|
30
|
+
canvas.height = h;
|
|
31
|
+
canvas.getContext("2d")?.putImageData(new ImageData(rgba, w, h), 0, 0);
|
|
32
|
+
return { frame, total, pts };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Native playback surface: frames are decoded and composited in Rust (FFmpeg +
|
|
36
|
+
* wgpu) and delivered here as RGBA; the browser never decodes the timeline.
|
|
37
|
+
* `dir` plays the canonical project revision.
|
|
38
|
+
*/
|
|
39
|
+
export function NativePlayer({ dir, label, seekRequest, onStatus, onError, }) {
|
|
40
|
+
const canvasRef = useRef(null);
|
|
41
|
+
const [meta, setMeta] = useState(null);
|
|
42
|
+
const [state, setState] = useState("loading");
|
|
43
|
+
const [error, setError] = useState(null);
|
|
44
|
+
const drawing = useRef(false);
|
|
45
|
+
const apply = useCallback((m) => {
|
|
46
|
+
setMeta(m);
|
|
47
|
+
onStatus?.(m);
|
|
48
|
+
}, [onStatus]);
|
|
49
|
+
const draw = useCallback(async () => {
|
|
50
|
+
if (drawing.current)
|
|
51
|
+
return;
|
|
52
|
+
drawing.current = true;
|
|
53
|
+
try {
|
|
54
|
+
const buf = await playbackFrame();
|
|
55
|
+
const canvas = canvasRef.current;
|
|
56
|
+
if (canvas)
|
|
57
|
+
drawFrame(canvas, buf);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// transient (session replaced mid-frame); the status loop surfaces errors
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
drawing.current = false;
|
|
64
|
+
}
|
|
65
|
+
}, []);
|
|
66
|
+
// Open/replace the session for this source.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
let cancelled = false;
|
|
69
|
+
setState("loading");
|
|
70
|
+
setError(null);
|
|
71
|
+
(async () => {
|
|
72
|
+
try {
|
|
73
|
+
const m = await playbackOpen({ dir });
|
|
74
|
+
if (cancelled)
|
|
75
|
+
return;
|
|
76
|
+
apply(m);
|
|
77
|
+
setState("ready");
|
|
78
|
+
await draw();
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
if (!cancelled) {
|
|
82
|
+
const msg = errorMessage(e);
|
|
83
|
+
setError(msg);
|
|
84
|
+
setState("error");
|
|
85
|
+
onError?.(msg);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
})();
|
|
89
|
+
return () => {
|
|
90
|
+
cancelled = true;
|
|
91
|
+
void playbackClose().catch(() => undefined);
|
|
92
|
+
};
|
|
93
|
+
}, [dir, apply, draw, onError]);
|
|
94
|
+
// External seek requests (e.g. timeline scrub).
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!seekRequest || state !== "ready")
|
|
97
|
+
return;
|
|
98
|
+
void playbackControl("seek", seekRequest.ms)
|
|
99
|
+
.then(apply)
|
|
100
|
+
.then(() => draw())
|
|
101
|
+
.catch(() => undefined);
|
|
102
|
+
}, [seekRequest, state, apply, draw]);
|
|
103
|
+
// The session was rebuilt under a paused player (a colour grade changed): repaint.
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (state !== "ready")
|
|
106
|
+
return;
|
|
107
|
+
const repaint = () => void draw().catch((e) => setError(errorMessage(e)));
|
|
108
|
+
window.addEventListener(PLAYBACK_REFRESHED, repaint);
|
|
109
|
+
return () => window.removeEventListener(PLAYBACK_REFRESHED, repaint);
|
|
110
|
+
}, [state, draw]);
|
|
111
|
+
// Frame pump while playing.
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (state !== "ready" || !meta?.playing)
|
|
114
|
+
return;
|
|
115
|
+
let live = true;
|
|
116
|
+
const pump = async () => {
|
|
117
|
+
const { playbackStatus } = await import("../lib/media.js");
|
|
118
|
+
while (live) {
|
|
119
|
+
await draw();
|
|
120
|
+
const still = await playbackStatus().catch(() => null);
|
|
121
|
+
if (!still || !still.playing) {
|
|
122
|
+
if (still)
|
|
123
|
+
apply(still);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
apply(still);
|
|
127
|
+
if (!live)
|
|
128
|
+
return;
|
|
129
|
+
await new Promise((r) => setTimeout(r, 8));
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
void pump();
|
|
133
|
+
return () => {
|
|
134
|
+
live = false;
|
|
135
|
+
};
|
|
136
|
+
}, [state, meta?.playing, draw, apply]);
|
|
137
|
+
const control = (cmd, value, value2) => playbackControl(cmd, value, value2)
|
|
138
|
+
.then(apply)
|
|
139
|
+
.then(() => draw())
|
|
140
|
+
.catch((e) => setError(errorMessage(e)));
|
|
141
|
+
if (state === "error") {
|
|
142
|
+
return (_jsxs("p", { role: "alert", className: "cr-muted m-0 text-sm", children: [label ?? dir, ": native playback failed \u2014 ", error] }));
|
|
143
|
+
}
|
|
144
|
+
const pos = (meta?.position_ms ?? 0) / 1000;
|
|
145
|
+
const dur = (meta?.duration_ms ?? 0) / 1000;
|
|
146
|
+
return (_jsxs("div", { className: "cr-player", children: [_jsx("canvas", { ref: canvasRef, style: {
|
|
147
|
+
display: "block",
|
|
148
|
+
width: "100%",
|
|
149
|
+
maxHeight: "420px",
|
|
150
|
+
background: "black",
|
|
151
|
+
opacity: state === "loading" ? 0.3 : 1,
|
|
152
|
+
} }), state === "loading" && (_jsx("p", { role: "status", className: "cr-muted m-0 text-sm", children: "Preparing native playback\u2026" })), _jsxs("div", { className: "seek-row cr-dim cr-mono", style: { alignItems: "center", gap: 8 }, children: [_jsx(Button, { type: "button", disabled: state !== "ready", onClick: () => void control("toggle"), children: meta?.playing ? "Pause" : "Play" }), _jsx(Button, { type: "button", disabled: state !== "ready", onClick: () => void control("step", -1), children: "-1f" }), _jsx(Button, { type: "button", disabled: state !== "ready", onClick: () => void control("step", 1), children: "+1f" }), _jsx(Slider, { label: "Seek", min: 0, max: Math.max(dur, 0.001), step: 0.001, value: pos, disabled: state !== "ready", onChange: (value) => void control("seek", value * 1000), className: "flex-1" }), _jsx(Select, { label: "Speed", value: String(meta?.speed ?? 1), disabled: state !== "ready", onChange: (value) => void control("speed", Number(value)), options: [0.5, 1, 1.5, 2, 4].map((speed) => ({
|
|
153
|
+
value: String(speed),
|
|
154
|
+
label: `${speed}x`,
|
|
155
|
+
})) }), _jsx(Button, { type: "button", disabled: state !== "ready", "aria-pressed": !!meta?.loop, onClick: () => void control("loop", meta?.loop ? -1 : 0, meta?.loop ? 0 : (meta?.duration_ms ?? 0)), children: "Loop" })] }), _jsxs("div", { className: "seek-row cr-dim cr-mono", children: [_jsx("span", { className: "truncate", title: dir, children: label ?? "Timeline preview" }), _jsxs("span", { children: [formatSeconds(pos), " / ", formatSeconds(dur), meta ? ` · f${meta.frame}/${meta.total_frames}` : "", meta && meta.audio !== "on" && meta.kind === "timeline" ? " · muted" : ""] })] })] }));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Preview host for the editor: revision-bound native playback of the project
|
|
159
|
+
* timeline. `seek(revisionCursor, timeS)` scrubs to `timeS`.
|
|
160
|
+
*/
|
|
161
|
+
export function useTimelinePreview(dir, revision) {
|
|
162
|
+
const [seekRequest, setSeekRequest] = useState(null);
|
|
163
|
+
const host = {
|
|
164
|
+
seek: (_revisionCursor, timeS) => setSeekRequest({ ms: timeS * 1000, n: Date.now() }),
|
|
165
|
+
slot: dir ? (_jsx(NativePlayer, { dir: dir, label: "Timeline preview", seekRequest: seekRequest }, `${dir}:${revision}`)) : null,
|
|
166
|
+
};
|
|
167
|
+
return host;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Stage player for the Finished edit screen (§3.4): a letterboxed canvas fed
|
|
171
|
+
* by the Rust compositor playback session for the CURRENT project revision,
|
|
172
|
+
* plus the spec transport (play circle, ±1f, mono timecode, speed, loop,
|
|
173
|
+
* scrubber). While the session opens — or if playback fails — it shows the
|
|
174
|
+
* caller-provided fallback frame (`media.thumbnail`), never a black box.
|
|
175
|
+
*/
|
|
176
|
+
export function StagePlayer({ dir, revision, seekRequest, captionsOn, caption, aspect, fallback, onPosition, onToggleCaptions, }) {
|
|
177
|
+
const canvasRef = useRef(null);
|
|
178
|
+
const [meta, setMeta] = useState(null);
|
|
179
|
+
const [state, setState] = useState("loading");
|
|
180
|
+
const [error, setError] = useState(null);
|
|
181
|
+
const [volume, setVolume] = useState(1);
|
|
182
|
+
const [muted, setMuted] = useState(false);
|
|
183
|
+
/** Bumped by the Retry control to reopen the playback session for the same revision. */
|
|
184
|
+
const [attempt, setAttempt] = useState(0);
|
|
185
|
+
const drawing = useRef(false);
|
|
186
|
+
const draw = useCallback(async () => {
|
|
187
|
+
if (drawing.current)
|
|
188
|
+
return;
|
|
189
|
+
drawing.current = true;
|
|
190
|
+
try {
|
|
191
|
+
const buf = await playbackFrame();
|
|
192
|
+
const canvas = canvasRef.current;
|
|
193
|
+
if (canvas)
|
|
194
|
+
drawFrame(canvas, buf);
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
drawing.current = false;
|
|
198
|
+
}
|
|
199
|
+
}, []);
|
|
200
|
+
const apply = useCallback((m) => {
|
|
201
|
+
setMeta(m);
|
|
202
|
+
onPosition?.(m.position_ms);
|
|
203
|
+
}, [onPosition]);
|
|
204
|
+
// Gain is process-wide in the native transport, so it survives session reopen.
|
|
205
|
+
// A failed call leaves the previous gain in place; the stage keeps playing.
|
|
206
|
+
const setGain = (gain) => {
|
|
207
|
+
void playbackSetGain(gain).catch(() => undefined);
|
|
208
|
+
};
|
|
209
|
+
const onVolume = (v) => {
|
|
210
|
+
setVolume(v);
|
|
211
|
+
setMuted(false);
|
|
212
|
+
setGain(v);
|
|
213
|
+
};
|
|
214
|
+
const toggleMute = () => {
|
|
215
|
+
if (muted) {
|
|
216
|
+
const restored = volume > 0 ? volume : 1;
|
|
217
|
+
setVolume(restored);
|
|
218
|
+
setMuted(false);
|
|
219
|
+
setGain(restored);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
setMuted(true);
|
|
223
|
+
setGain(0);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
// One session per (dir, revision, attempt): an applied edit changes the programme,
|
|
227
|
+
// and Retry reopens the session after a playback error.
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
let cancelled = false;
|
|
230
|
+
setState("loading");
|
|
231
|
+
setError(null);
|
|
232
|
+
(async () => {
|
|
233
|
+
try {
|
|
234
|
+
const m = await playbackOpen({ dir, height: 720 });
|
|
235
|
+
if (cancelled)
|
|
236
|
+
return;
|
|
237
|
+
apply(m);
|
|
238
|
+
setState("ready");
|
|
239
|
+
await draw();
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
if (!cancelled) {
|
|
243
|
+
setError(errorMessage(e));
|
|
244
|
+
setState("error");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
})();
|
|
248
|
+
return () => {
|
|
249
|
+
cancelled = true;
|
|
250
|
+
void playbackClose().catch(() => undefined);
|
|
251
|
+
};
|
|
252
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
253
|
+
}, [dir, revision, attempt]);
|
|
254
|
+
// Seeks run in request order, and only the newest one's reply moves the playhead:
|
|
255
|
+
// a late reply from an earlier keystroke must not snap it back.
|
|
256
|
+
const seekChain = useRef(Promise.resolve());
|
|
257
|
+
const latestSeek = useRef(null);
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
if (!seekRequest || state !== "ready")
|
|
260
|
+
return;
|
|
261
|
+
latestSeek.current = seekRequest;
|
|
262
|
+
seekChain.current = seekChain.current
|
|
263
|
+
.catch(() => undefined)
|
|
264
|
+
.then(() => playbackControl("seek", seekRequest.ms))
|
|
265
|
+
.then(async (m) => {
|
|
266
|
+
if (latestSeek.current !== seekRequest)
|
|
267
|
+
return;
|
|
268
|
+
apply(m);
|
|
269
|
+
await draw();
|
|
270
|
+
})
|
|
271
|
+
.catch((e) => setError(errorMessage(e)));
|
|
272
|
+
}, [seekRequest, state, apply, draw]);
|
|
273
|
+
// The session was rebuilt under a paused player (a colour grade changed): repaint.
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (state !== "ready")
|
|
276
|
+
return;
|
|
277
|
+
const repaint = () => void draw().catch((e) => setError(errorMessage(e)));
|
|
278
|
+
window.addEventListener(PLAYBACK_REFRESHED, repaint);
|
|
279
|
+
return () => window.removeEventListener(PLAYBACK_REFRESHED, repaint);
|
|
280
|
+
}, [state, draw]);
|
|
281
|
+
// Frame pump while playing.
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
if (state !== "ready" || !meta?.playing)
|
|
284
|
+
return;
|
|
285
|
+
let live = true;
|
|
286
|
+
const pump = async () => {
|
|
287
|
+
const { playbackStatus } = await import("../lib/media.js");
|
|
288
|
+
while (live) {
|
|
289
|
+
try {
|
|
290
|
+
await draw();
|
|
291
|
+
}
|
|
292
|
+
catch (e) {
|
|
293
|
+
// A persistent decode failure (e.g. missing media) is honest UI:
|
|
294
|
+
// surface it rather than leaving a black canvas.
|
|
295
|
+
if (live) {
|
|
296
|
+
setError(errorMessage(e));
|
|
297
|
+
setState("error");
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const still = await playbackStatus().catch(() => null);
|
|
302
|
+
if (!still || !still.playing) {
|
|
303
|
+
if (still)
|
|
304
|
+
apply(still);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
apply(still);
|
|
308
|
+
if (!live)
|
|
309
|
+
return;
|
|
310
|
+
await new Promise((r) => setTimeout(r, 8));
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
void pump();
|
|
314
|
+
return () => {
|
|
315
|
+
live = false;
|
|
316
|
+
};
|
|
317
|
+
}, [state, meta?.playing, draw, apply]);
|
|
318
|
+
const control = (cmd, value, value2) => playbackControl(cmd, value, value2)
|
|
319
|
+
.then(apply)
|
|
320
|
+
.then(() => draw())
|
|
321
|
+
.catch((e) => setError(errorMessage(e)));
|
|
322
|
+
const pos = (meta?.position_ms ?? 0) / 1000;
|
|
323
|
+
const dur = (meta?.duration_ms ?? 0) / 1000;
|
|
324
|
+
const ready = state === "ready";
|
|
325
|
+
const tidyError = (e) => {
|
|
326
|
+
if (!e)
|
|
327
|
+
return "unknown error";
|
|
328
|
+
const last = e.split("\n").filter(Boolean).pop() ?? e;
|
|
329
|
+
return last.replace(/\S*\/([^\s/]+)/g, "$1");
|
|
330
|
+
};
|
|
331
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "fe-stage", "data-preview-state": state, "data-preview-error": error ?? undefined, children: [fallback && state !== "ready" && (_jsx("img", { className: "fe-stage-fallback", src: fallback, alt: "", draggable: false })), _jsx("canvas", { ref: canvasRef, style: { display: ready ? "block" : fallback ? "none" : "block" } }), state === "loading" && !fallback && (_jsx("div", { className: "fe-stage__loading", children: _jsx("span", { role: "status", className: "cr-muted", children: "Loading preview\u2026" }) })), state === "error" && (_jsxs("div", { className: "fe-stage__loading", role: "alert", title: error ?? undefined, children: ["Preview failed \u2014 ", tidyError(error)] })), state === "error" && (_jsx(Button, { type: "button", style: { position: "absolute", left: "50%", bottom: 12, transform: "translateX(-50%)" }, onClick: () => setAttempt((n) => n + 1), children: "Retry" })), _jsx("span", { className: "fe-stage__tag", children: "Final edit" }), _jsxs("span", { className: "fe-stage__chips", children: [_jsxs("button", { type: "button", className: "rk-badge", "aria-pressed": captionsOn, onClick: () => onToggleCaptions(!captionsOn), children: ["Captions ", captionsOn ? "on" : "off"] }), aspect && _jsx("span", { className: "rk-badge", children: aspect })] }), captionsOn && caption && _jsx("div", { className: "fe-stage__caption", children: caption })] }), _jsxs("div", { className: "fe-transport", children: [_jsxs("div", { className: "fe-transport__row", children: [_jsx(IconButton, { label: "-1 frame", disabled: !ready, onClick: () => void control("step", -1), children: _jsx(Icon, { name: "step-back" }) }), _jsx("button", { type: "button", className: "fe-play", "aria-label": meta?.playing ? "Pause" : "Play", disabled: !ready, onClick: () => void control("toggle"), children: _jsx(Icon, { name: meta?.playing ? "pause" : "play" }) }), _jsx(IconButton, { label: "+1 frame", disabled: !ready, onClick: () => void control("step", 1), children: _jsx(Icon, { name: "step-forward" }) }), _jsxs("span", { className: "fe-transport__tc", children: [_jsx("strong", { children: timecode(meta?.position_ms ?? 0) }), " / ", timecode(meta?.duration_ms ?? 0)] }), _jsxs("span", { className: "fe-transport__right", children: [_jsx(Select, { className: "w-auto h-[26px]!", label: "Playback speed", value: String(meta?.speed ?? 1), disabled: !ready, onChange: (value) => void control("speed", Number(value)), options: [0.5, 1, 1.5, 2].map((speed) => ({
|
|
332
|
+
value: String(speed),
|
|
333
|
+
label: `${speed}×`,
|
|
334
|
+
})) }), _jsx(IconButton, { label: "Loop", pressed: !!meta?.loop, disabled: !ready, onClick: () => void control("loop", meta?.loop ? -1 : 0, meta?.loop ? 0 : meta?.duration_ms ?? 0), children: _jsx(Icon, { name: "repeat" }) }), _jsx(IconButton, { label: "Mute", pressed: muted, onClick: toggleMute, children: _jsx(Icon, { name: muted || volume === 0 ? "volume-x" : "volume" }) }), _jsx(Slider, { label: "Volume", min: 0, max: 1, step: 0.01, value: muted ? 0 : volume, onChange: onVolume, className: "w-[72px]!" })] })] }), _jsx(Slider, { label: "Scrub", min: 0, max: Math.max(dur, 0.001), step: 0.001, value: pos, disabled: !ready, onChange: (value) => void control("seek", value * 1000) })] })] }));
|
|
335
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** @rightkit/media-ui player confined to [startMs, endMs] of a real file: starts
|
|
2
|
+
* at the range start and either loops it (shorts) or stops at its end (phrases). */
|
|
3
|
+
export declare function RangePreview({ path, startMs, endMs, loop, title, }: {
|
|
4
|
+
path: string;
|
|
5
|
+
startMs: number;
|
|
6
|
+
endMs: number;
|
|
7
|
+
loop?: boolean;
|
|
8
|
+
title?: string;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import { MediaPlayer } from "@rightkit/media-ui";
|
|
4
|
+
import { mediaKind, useMediaPlaybackId } from "../lib/mediaAdapters.js";
|
|
5
|
+
/** @rightkit/media-ui player confined to [startMs, endMs] of a real file: starts
|
|
6
|
+
* at the range start and either loops it (shorts) or stops at its end (phrases). */
|
|
7
|
+
export function RangePreview({ path, startMs, endMs, loop, title, }) {
|
|
8
|
+
const el = useRef(null);
|
|
9
|
+
const playbackId = useMediaPlaybackId(path);
|
|
10
|
+
return (_jsx(MediaPlayer, { src: path, playbackId: playbackId, kind: mediaKind(path), title: title, autoPlay: true, compact: true, mediaRef: el, onReady: () => {
|
|
11
|
+
if (el.current)
|
|
12
|
+
el.current.currentTime = startMs / 1000;
|
|
13
|
+
}, onTimeChange: (s) => {
|
|
14
|
+
const m = el.current;
|
|
15
|
+
if (!m || s * 1000 < endMs)
|
|
16
|
+
return;
|
|
17
|
+
if (loop)
|
|
18
|
+
m.currentTime = startMs / 1000;
|
|
19
|
+
else
|
|
20
|
+
m.pause();
|
|
21
|
+
} }, `${path}:${startMs}:${playbackId ?? ""}`));
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* Workspace pieces inside @rightkit/react-shell's frame. */
|
|
2
|
+
.ws-plane { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
3
|
+
.ws-crumb { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; color: var(--rk-ink-2); }
|
|
4
|
+
.ws-crumb strong { font-weight: 500; color: var(--rk-ink); }
|
|
5
|
+
.ws-crumb__link { border: 0; background: none; padding: 0; color: inherit; font: inherit; cursor: pointer; }
|
|
6
|
+
.ws-crumb__link:hover { color: var(--rk-ink); }
|
|
7
|
+
.ws-count { font: 500 11px/1 var(--rk-font-body); color: var(--rk-ink-2); padding: 2px 6px; border-radius: 6px; background: var(--rk-fill-2); }
|
|
8
|
+
.ws-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--rk-ink-3); margin: 0 4px; }
|
|
9
|
+
.ws-dot--cur { background: var(--rk-accent); }
|
|
10
|
+
.ws-dot--missing { background: var(--rk-warn-dot); }
|
|
11
|
+
.ws-wordmark { font-family: var(--rk-wordmark-font); font-size: 24px; line-height: 1; color: var(--rk-ink); }
|
|
12
|
+
.ws-wordmark span { color: var(--rk-accent); }
|
|
13
|
+
.ws-foot-note { margin: 0 0 var(--s2); font-size: 11px; line-height: 1.4; color: var(--rk-ink-2); }
|
|
14
|
+
.ws-status { border: 0; background: none; padding: 0; width: 100%; text-align: left; cursor: pointer; color: inherit; font: inherit; }
|
|
15
|
+
.ws-side-top { padding: 4px 10px 8px; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function DemoSettings(): import("react").JSX.Element;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Select, Toggle } from "@rightkit/react-shell/react";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { SettingRow, SettingsGroup } from "@rightkit/settings-ui";
|
|
5
|
+
import { demoBeginPerson, demoBundleDiscard, demoBundleReveal, demoCatalog, demoErrorMessage, demoPrepare, demoSetEnabled, phaseLabel, } from "./bindings.js";
|
|
6
|
+
import { useDemo } from "./useDemo.js";
|
|
7
|
+
import { hostAppName } from "../appName.js";
|
|
8
|
+
export function DemoSettings() {
|
|
9
|
+
const { status, state, refresh } = useDemo();
|
|
10
|
+
const [catalog, setCatalog] = useState([]);
|
|
11
|
+
const [app, setApp] = useState("");
|
|
12
|
+
const [prep, setPrep] = useState(null);
|
|
13
|
+
const [busy, setBusy] = useState(false);
|
|
14
|
+
const [error, setError] = useState(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
demoCatalog()
|
|
17
|
+
.then((c) => {
|
|
18
|
+
setCatalog(c);
|
|
19
|
+
setApp((cur) => cur || c[0]?.id || "");
|
|
20
|
+
})
|
|
21
|
+
.catch(() => setCatalog([]));
|
|
22
|
+
}, []);
|
|
23
|
+
if (!status) {
|
|
24
|
+
return (_jsx(SettingsGroup, { children: _jsx(SettingRow, { id: "demo-unavailable", label: "Agent demos", description: "Not part of this build.", children: _jsx("span", {}) }) }));
|
|
25
|
+
}
|
|
26
|
+
const run = async (f) => {
|
|
27
|
+
setBusy(true);
|
|
28
|
+
setError(null);
|
|
29
|
+
try {
|
|
30
|
+
await f();
|
|
31
|
+
await refresh();
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
setError(demoErrorMessage(e));
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
setBusy(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const lastBundle = state.bundle?.name;
|
|
41
|
+
const active = state.phase === "armed" || state.phase === "recording" || state.phase === "stopping" || state.phase === "awaiting_consent";
|
|
42
|
+
return (_jsxs(_Fragment, { children: [_jsxs(SettingsGroup, { title: "Agent demos", children: [_jsx(SettingRow, { id: "demo-enabled", label: "Allow agent demos", description: `Off by default. When on, an agent you run on this computer can ask to drive one app while ${hostAppName()} records that app's window. You approve every session. Nothing leaves this computer.`, children: _jsx(Toggle, { label: "Allow agent demos", checked: status.enabled, onChange: (v) => void run(() => demoSetEnabled(v)) }) }), status.enabled && (_jsx(SettingRow, { id: "demo-kill", label: "Kill control", description: "Stops the agent and the recording at once, from anywhere.", children: _jsx("kbd", { children: status.hotkey }) }))] }), status.enabled && (_jsxs(SettingsGroup, { title: "Permissions", children: [_jsx(SettingRow, { id: "demo-permissions", label: "Accessibility and Screen Recording", description: `${hostAppName()} needs both to drive and record another app. It never asks by itself.`, children: _jsx(Button, { size: "sm", variant: "secondary", disabled: busy, onClick: () => void run(async () => setPrep(await demoPrepare())), children: "Check" }) }), prep && (_jsx(SettingRow, { id: "demo-permissions-result", label: "Result", description: `${prep.recorder}${prep.notes.length ? `. ${prep.notes.join(" ")}` : ""}`, children: _jsxs("span", { children: ["Accessibility: ", prep.accessibility ? "granted" : "missing", ". Screen Recording: ", prep.screen_recording ? "granted" : "missing", "."] }) }))] })), status.enabled && (_jsx(SettingsGroup, { title: "Record myself", children: _jsxs(SettingRow, { id: "demo-person", label: "Record an app", description: "Records one app's window with the badge and the kill control, without an agent.", children: [_jsx(Select, { label: "App to record", value: app, onChange: setApp, disabled: busy || active, options: catalog.map((c) => ({ value: c.id, label: c.name })) }), _jsx(Button, { size: "sm", disabled: busy || active || !app, onClick: () => void run(() => demoBeginPerson(app)), children: "Start recording" })] }) })), status.enabled && (_jsx(SettingsGroup, { title: "Current state", children: _jsx(SettingRow, { id: "demo-state", label: phaseLabel(state.phase), description: state.message ?? undefined, children: lastBundle ? (_jsxs("span", { className: "cr-demo-inline", children: [_jsx(Button, { size: "sm", variant: "secondary", disabled: busy, onClick: () => void run(() => demoBundleReveal(lastBundle)), children: "Show last recording" }), _jsx(Button, { size: "sm", variant: "danger", disabled: busy || active, onClick: () => void run(() => demoBundleDiscard(lastBundle)), children: "Delete it" })] })) : (_jsx("span", {})) }) })), error && (_jsx("p", { role: "alert", className: "cr-demo-error", children: error }))] }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { type UnlistenFn } from "@tauri-apps/api/event";
|
|
2
|
+
export type DemoErrorCode = "demo_disabled" | "consent_denied" | "invalid_token" | "session_ended" | "session_killed" | "unknown_app" | "not_armed" | "permission_missing" | "target_blocked" | "capture_unsafe" | "invalid_arguments" | "service_unavailable" | "malformed_response";
|
|
3
|
+
export type ElementOp = "click" | "type" | "press";
|
|
4
|
+
export type WireStep = {
|
|
5
|
+
ref: string;
|
|
6
|
+
op: ElementOp;
|
|
7
|
+
text?: string;
|
|
8
|
+
key?: string;
|
|
9
|
+
};
|
|
10
|
+
export type WireApp = {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
};
|
|
14
|
+
export type WireSnapshotElement = {
|
|
15
|
+
ref: string;
|
|
16
|
+
role: string;
|
|
17
|
+
label: string;
|
|
18
|
+
ops: ElementOp[];
|
|
19
|
+
};
|
|
20
|
+
export type WireSnapshotResult = {
|
|
21
|
+
capture_state: "safe";
|
|
22
|
+
elements: WireSnapshotElement[];
|
|
23
|
+
};
|
|
24
|
+
export type WireBatchResult = {
|
|
25
|
+
completed: number;
|
|
26
|
+
results: {
|
|
27
|
+
receipt_id: string;
|
|
28
|
+
}[];
|
|
29
|
+
failed_step: number | null;
|
|
30
|
+
error_code: DemoErrorCode | null;
|
|
31
|
+
};
|
|
32
|
+
export type WireEndResult = {
|
|
33
|
+
ended: boolean;
|
|
34
|
+
bundle_id: string;
|
|
35
|
+
bundle_state: "complete" | "killed";
|
|
36
|
+
};
|
|
37
|
+
export declare const DEMO_SERVICE = "rightkit.agent.demo";
|
|
38
|
+
export declare const MAX_BATCH_STEPS = 20;
|
|
39
|
+
export type Phase = "idle" | "awaiting_consent" | "armed" | "recording" | "stopping" | "sealed" | "killed";
|
|
40
|
+
export type DemoSession = {
|
|
41
|
+
id: string;
|
|
42
|
+
kind: "agent" | "person";
|
|
43
|
+
app: string;
|
|
44
|
+
app_name: string;
|
|
45
|
+
agent_name: string | null;
|
|
46
|
+
started_ms: number | null;
|
|
47
|
+
};
|
|
48
|
+
export type DemoBundle = {
|
|
49
|
+
/** Folder name; what reveal and discard take. */
|
|
50
|
+
name: string;
|
|
51
|
+
id: string;
|
|
52
|
+
dir: string;
|
|
53
|
+
state: "complete" | "killed" | "quarantined" | "invalid";
|
|
54
|
+
verified: boolean;
|
|
55
|
+
problems: string[];
|
|
56
|
+
reason: string | null;
|
|
57
|
+
};
|
|
58
|
+
export type DemoState = {
|
|
59
|
+
phase: Phase;
|
|
60
|
+
session: DemoSession | null;
|
|
61
|
+
bundle: DemoBundle | null;
|
|
62
|
+
message: string | null;
|
|
63
|
+
hotkey: string;
|
|
64
|
+
};
|
|
65
|
+
/** What the consent dialog shows. `agent_exe` and `agent_pid` come from the OS; `agent_name` is a claim. */
|
|
66
|
+
export type DemoConsent = {
|
|
67
|
+
session: string;
|
|
68
|
+
app: string;
|
|
69
|
+
app_name: string;
|
|
70
|
+
agent_name: string;
|
|
71
|
+
agent_exe: string | null;
|
|
72
|
+
agent_pid: number | null;
|
|
73
|
+
will_do: string;
|
|
74
|
+
};
|
|
75
|
+
/** Content-free: no typed text, no element label, no value. */
|
|
76
|
+
export type DemoReceipt = {
|
|
77
|
+
receipt_id: string;
|
|
78
|
+
session: string;
|
|
79
|
+
action: string;
|
|
80
|
+
result: "done" | "blocked" | "failed";
|
|
81
|
+
denied_by: string | null;
|
|
82
|
+
tier: string | null;
|
|
83
|
+
ts_ms: number;
|
|
84
|
+
};
|
|
85
|
+
export type DemoStatus = {
|
|
86
|
+
compiled: boolean;
|
|
87
|
+
enabled: boolean;
|
|
88
|
+
hotkey: string;
|
|
89
|
+
state: DemoState;
|
|
90
|
+
pending_consent: DemoConsent | null;
|
|
91
|
+
};
|
|
92
|
+
export type DemoPrepare = {
|
|
93
|
+
accessibility: boolean;
|
|
94
|
+
screen_recording: boolean;
|
|
95
|
+
recorder: string;
|
|
96
|
+
notes: string[];
|
|
97
|
+
};
|
|
98
|
+
export type DemoCatalogItem = {
|
|
99
|
+
id: string;
|
|
100
|
+
name: string;
|
|
101
|
+
};
|
|
102
|
+
export declare const EVENT_STATE = "demo://state";
|
|
103
|
+
export declare const EVENT_CONSENT = "demo://consent";
|
|
104
|
+
export declare const EVENT_RECEIPT = "demo://receipt";
|
|
105
|
+
export declare const demoStatus: () => Promise<DemoStatus>;
|
|
106
|
+
export declare const demoSetEnabled: (enabled: boolean) => Promise<DemoStatus>;
|
|
107
|
+
export declare const demoCatalog: () => Promise<DemoCatalogItem[]>;
|
|
108
|
+
export declare const demoPrepare: () => Promise<DemoPrepare>;
|
|
109
|
+
export declare const demoConsentRespond: (session: string, approve: boolean) => Promise<void>;
|
|
110
|
+
export declare const demoBeginPerson: (app: string) => Promise<void>;
|
|
111
|
+
export declare const demoStop: () => Promise<DemoBundle | null>;
|
|
112
|
+
export declare const demoKill: () => Promise<void>;
|
|
113
|
+
export declare const demoBundleReveal: (name: string) => Promise<void>;
|
|
114
|
+
export declare const demoBundleDiscard: (name: string) => Promise<void>;
|
|
115
|
+
export declare const onDemoState: (f: (s: DemoState) => void) => Promise<UnlistenFn>;
|
|
116
|
+
export declare const onDemoConsent: (f: (c: DemoConsent) => void) => Promise<UnlistenFn>;
|
|
117
|
+
export declare const onDemoReceipt: (f: (r: DemoReceipt) => void) => Promise<UnlistenFn>;
|
|
118
|
+
/** Tauri rejects with the command's `{code, message}`, or a string for a missing command. */
|
|
119
|
+
export declare function demoErrorMessage(e: unknown): string;
|
|
120
|
+
/** One-line wording for a phase, shared by the badge and the settings page. */
|
|
121
|
+
export declare function phaseLabel(p: Phase): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Agent demo mode: TypeScript bindings.
|
|
2
|
+
//
|
|
3
|
+
// Two layers, both derived from Rust:
|
|
4
|
+
// 1. The agent wire contract (`demo.*` tools). The source of truth is rightkit-agent-tools
|
|
5
|
+
// (`src/backends/demo.rs`, `docs/demo-contract.md`). These types mirror it field for field; the UI
|
|
6
|
+
// never sends them, but the shapes are kept here so the contract has one TypeScript home.
|
|
7
|
+
// 2. The Tauri commands and events of the the host app host (`src-tauri/src/demo/`).
|
|
8
|
+
//
|
|
9
|
+
// Nothing here carries the session token: it never leaves the host.
|
|
10
|
+
import { invoke } from "@tauri-apps/api/core";
|
|
11
|
+
import { listen } from "@tauri-apps/api/event";
|
|
12
|
+
export const DEMO_SERVICE = "rightkit.agent.demo";
|
|
13
|
+
export const MAX_BATCH_STEPS = 20;
|
|
14
|
+
export const EVENT_STATE = "demo://state";
|
|
15
|
+
export const EVENT_CONSENT = "demo://consent";
|
|
16
|
+
export const EVENT_RECEIPT = "demo://receipt";
|
|
17
|
+
// ---------------------------------------------------------------- commands
|
|
18
|
+
export const demoStatus = () => invoke("demo_status");
|
|
19
|
+
export const demoSetEnabled = (enabled) => invoke("demo_set_enabled", { enabled });
|
|
20
|
+
export const demoCatalog = () => invoke("demo_catalog");
|
|
21
|
+
export const demoPrepare = () => invoke("demo_prepare");
|
|
22
|
+
export const demoConsentRespond = (session, approve) => invoke("demo_consent_respond", { session, approve });
|
|
23
|
+
export const demoBeginPerson = (app) => invoke("demo_begin_person", { app });
|
|
24
|
+
export const demoStop = () => invoke("demo_stop");
|
|
25
|
+
export const demoKill = () => invoke("demo_kill");
|
|
26
|
+
export const demoBundleReveal = (name) => invoke("demo_bundle_reveal", { name });
|
|
27
|
+
export const demoBundleDiscard = (name) => invoke("demo_bundle_discard", { name });
|
|
28
|
+
export const onDemoState = (f) => listen(EVENT_STATE, (e) => f(e.payload));
|
|
29
|
+
export const onDemoConsent = (f) => listen(EVENT_CONSENT, (e) => f(e.payload));
|
|
30
|
+
export const onDemoReceipt = (f) => listen(EVENT_RECEIPT, (e) => f(e.payload));
|
|
31
|
+
/** Tauri rejects with the command's `{code, message}`, or a string for a missing command. */
|
|
32
|
+
export function demoErrorMessage(e) {
|
|
33
|
+
if (typeof e === "string")
|
|
34
|
+
return e;
|
|
35
|
+
if (e && typeof e === "object" && "message" in e)
|
|
36
|
+
return String(e.message);
|
|
37
|
+
return "Something went wrong.";
|
|
38
|
+
}
|
|
39
|
+
/** One-line wording for a phase, shared by the badge and the settings page. */
|
|
40
|
+
export function phaseLabel(p) {
|
|
41
|
+
switch (p) {
|
|
42
|
+
case "awaiting_consent":
|
|
43
|
+
return "Waiting for your answer";
|
|
44
|
+
case "armed":
|
|
45
|
+
return "Ready, not recording yet";
|
|
46
|
+
case "recording":
|
|
47
|
+
return "Recording";
|
|
48
|
+
case "stopping":
|
|
49
|
+
return "Saving the recording";
|
|
50
|
+
case "sealed":
|
|
51
|
+
return "Recording saved";
|
|
52
|
+
case "killed":
|
|
53
|
+
return "Stopped by the kill control";
|
|
54
|
+
default:
|
|
55
|
+
return "Off";
|
|
56
|
+
}
|
|
57
|
+
}
|