@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,331 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Batch delivery — one project's timelines x presets as a resumable batch (batch.* engine methods).
|
|
3
|
+
import { Badge, Button, Checkbox, Field, Input, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, useToast } from "@rightkit/react-shell/react";
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { assetUrl } from "../lib/asset.js";
|
|
6
|
+
import { errorCode, errorMessage, onEngineEvent } from "../lib/engine.js";
|
|
7
|
+
import { batchCover, batchCreate, batchList, batchRun, batchStatus, recipeCancel, SETTINGS_CHANGED, variantsList, } from "../lib/media.js";
|
|
8
|
+
import { Icon } from "../ui/index.js";
|
|
9
|
+
const PRESETS = [
|
|
10
|
+
{ value: "youtube", label: "YouTube 16:9", shape: "wide" },
|
|
11
|
+
{ value: "reels", label: "Reels 9:16", shape: "tall" },
|
|
12
|
+
{ value: "tiktok", label: "TikTok 9:16", shape: "tall" },
|
|
13
|
+
{ value: "square", label: "Square 1:1", shape: "square" },
|
|
14
|
+
];
|
|
15
|
+
const PRESET_BY_ID = Object.fromEntries(PRESETS.map((p) => [p.value, p]));
|
|
16
|
+
const STATE_LABEL = {
|
|
17
|
+
pending: "Pending",
|
|
18
|
+
rendered: "Rendered",
|
|
19
|
+
qualified: "Qualified",
|
|
20
|
+
failed: "Failed",
|
|
21
|
+
};
|
|
22
|
+
const STATE_TONE = {
|
|
23
|
+
pending: "neutral",
|
|
24
|
+
rendered: "accent",
|
|
25
|
+
qualified: "ok",
|
|
26
|
+
failed: "bad",
|
|
27
|
+
};
|
|
28
|
+
/** Timeline key for the main timeline; saved variants use their own names. */
|
|
29
|
+
const MAIN = "";
|
|
30
|
+
/** Ties the changed-settings notice to the Run button that a run will be refused by. */
|
|
31
|
+
const DRIFT_NOTICE_ID = "batch-drift-notice";
|
|
32
|
+
/** Ties the all-delivered reason to the Run button that is disabled by it. */
|
|
33
|
+
const DELIVERED_REASON_ID = "batch-delivered-reason";
|
|
34
|
+
const DELIVERED_REASON = "Every output is delivered and the finishing settings are unchanged, so there is nothing to run.";
|
|
35
|
+
function isBatchState(v) {
|
|
36
|
+
return v === "pending" || v === "rendered" || v === "qualified" || v === "failed";
|
|
37
|
+
}
|
|
38
|
+
/** Engine names that do not read as words once their underscores are spaces. */
|
|
39
|
+
const SETTING_WORDS = {
|
|
40
|
+
lut_blake3: "LUT file contents",
|
|
41
|
+
lut_path: "LUT file",
|
|
42
|
+
caption_style_in_force: "caption style in use",
|
|
43
|
+
burn_in_force: "burned captions",
|
|
44
|
+
grade_by_source: "per-source grade",
|
|
45
|
+
};
|
|
46
|
+
const settingWords = (key) => SETTING_WORDS[key] ?? key.replace(/_/g, " ");
|
|
47
|
+
/** The drift a status reply reports; null when the batch matches or was made before pinning. */
|
|
48
|
+
function driftOf(status) {
|
|
49
|
+
return status.finishing_matches === false ? { id: status.id, differs: status.finishing_differs } : null;
|
|
50
|
+
}
|
|
51
|
+
function asCover(v) {
|
|
52
|
+
if (!v || typeof v !== "object")
|
|
53
|
+
return null;
|
|
54
|
+
const c = v;
|
|
55
|
+
return typeof c.path === "string" && typeof c.at_ms === "number" && typeof c.blake3 === "string"
|
|
56
|
+
? { at_ms: c.at_ms, path: c.path, blake3: c.blake3 }
|
|
57
|
+
: null;
|
|
58
|
+
}
|
|
59
|
+
function withItem(set, item, on) {
|
|
60
|
+
const next = new Set(set);
|
|
61
|
+
if (on)
|
|
62
|
+
next.add(item);
|
|
63
|
+
else
|
|
64
|
+
next.delete(item);
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
67
|
+
function patchRow(doc, key, patch) {
|
|
68
|
+
return { ...doc, outputs: doc.outputs.map((o) => (o.key === key ? { ...o, ...patch } : o)) };
|
|
69
|
+
}
|
|
70
|
+
function applyRunOutputs(doc, outs) {
|
|
71
|
+
return outs.reduce((acc, o) => patchRow(acc, o.key, { state: o.state, output: o.output, cover: o.cover, error: o.error }), doc);
|
|
72
|
+
}
|
|
73
|
+
/** Batch panel on the Exports screen: build the output list, run it, resume it, and pick covers. */
|
|
74
|
+
/** A project-relative path as an absolute one, in the project folder's own separator style. */
|
|
75
|
+
function projectFile(dir, relative) {
|
|
76
|
+
const sep = dir.includes("\\") ? "\\" : "/";
|
|
77
|
+
return `${dir.replace(/[\\/]+$/, "")}${sep}${relative.split("/").join(sep)}`;
|
|
78
|
+
}
|
|
79
|
+
export function BatchDelivery({ dir }) {
|
|
80
|
+
const toast = useToast();
|
|
81
|
+
const [variants, setVariants] = useState([]);
|
|
82
|
+
const [presets, setPresets] = useState(new Set(["youtube"]));
|
|
83
|
+
const [timelines, setTimelines] = useState(new Set([MAIN]));
|
|
84
|
+
const [batches, setBatches] = useState([]);
|
|
85
|
+
const [batchId, setBatchId] = useState("");
|
|
86
|
+
const [doc, setDoc] = useState(null);
|
|
87
|
+
const [coverAt, setCoverAt] = useState({});
|
|
88
|
+
const [creating, setCreating] = useState(false);
|
|
89
|
+
const [running, setRunning] = useState(null); // job id
|
|
90
|
+
const [summary, setSummary] = useState(null);
|
|
91
|
+
const [error, setError] = useState(null);
|
|
92
|
+
const [drift, setDrift] = useState(null);
|
|
93
|
+
const [compareError, setCompareError] = useState(null);
|
|
94
|
+
const [statusReads, setStatusReads] = useState(0);
|
|
95
|
+
const runTag = useRef(null);
|
|
96
|
+
const selectedId = useRef("");
|
|
97
|
+
// Each read takes a number; only the newest read may write the panel, so a slow older reply
|
|
98
|
+
// cannot clear a drift or compare notice that a newer read has raised.
|
|
99
|
+
const readSeq = useRef(0);
|
|
100
|
+
const refreshList = useCallback(async () => {
|
|
101
|
+
const [list, vars] = await Promise.all([batchList(dir), variantsList(dir)]);
|
|
102
|
+
setBatches(list.batches);
|
|
103
|
+
setVariants(vars.variants.filter((v) => !v.error).map((v) => v.name));
|
|
104
|
+
}, [dir]);
|
|
105
|
+
// Re-reads a batch's status; a reply for a batch that is no longer the selected one is dropped.
|
|
106
|
+
// While a read is in flight the finishing settings are unverified, so Run waits for it. A read
|
|
107
|
+
// that fails leaves them unverified too: the batch's comparison is then reported as unavailable
|
|
108
|
+
// and Run stays disabled until a later read succeeds.
|
|
109
|
+
const loadStatus = useCallback((id) => {
|
|
110
|
+
const seq = ++readSeq.current;
|
|
111
|
+
setStatusReads((n) => n + 1);
|
|
112
|
+
return batchStatus(dir, id)
|
|
113
|
+
.then((d) => {
|
|
114
|
+
if (selectedId.current !== id || seq !== readSeq.current)
|
|
115
|
+
return;
|
|
116
|
+
setDoc(d);
|
|
117
|
+
setDrift(driftOf(d));
|
|
118
|
+
setCompareError(d.finishing_error ? { id: d.id, message: d.finishing_error } : null);
|
|
119
|
+
}, (e) => {
|
|
120
|
+
if (selectedId.current === id && seq === readSeq.current) {
|
|
121
|
+
setDrift(null);
|
|
122
|
+
setCompareError({ id, message: `the batch could not be re-read (${errorMessage(e)})` });
|
|
123
|
+
}
|
|
124
|
+
throw e;
|
|
125
|
+
})
|
|
126
|
+
.finally(() => setStatusReads((n) => n - 1));
|
|
127
|
+
}, [dir]);
|
|
128
|
+
// A different project starts with no batch selected.
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
selectedId.current = "";
|
|
131
|
+
setBatchId("");
|
|
132
|
+
setDoc(null);
|
|
133
|
+
setSummary(null);
|
|
134
|
+
setError(null);
|
|
135
|
+
setDrift(null);
|
|
136
|
+
setCompareError(null);
|
|
137
|
+
}, [dir]);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
refreshList().catch((e) => setError(errorMessage(e)));
|
|
140
|
+
}, [refreshList]);
|
|
141
|
+
// Finishing settings can change elsewhere in the app, so the selected batch's status is re-read when the panel is visible again.
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const refresh = () => {
|
|
144
|
+
if (selectedId.current)
|
|
145
|
+
void loadStatus(selectedId.current).catch(() => undefined);
|
|
146
|
+
};
|
|
147
|
+
const onVisibility = () => {
|
|
148
|
+
if (document.visibilityState === "visible")
|
|
149
|
+
refresh();
|
|
150
|
+
};
|
|
151
|
+
window.addEventListener("focus", refresh);
|
|
152
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
153
|
+
refresh(); // a batch already selected when the panel mounts
|
|
154
|
+
return () => {
|
|
155
|
+
window.removeEventListener("focus", refresh);
|
|
156
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
157
|
+
};
|
|
158
|
+
}, [loadStatus]);
|
|
159
|
+
// Finishing settings saved in another view (Settings, Auto edit, the colour inspector) change
|
|
160
|
+
// what this batch compares against. The re-read raises statusReads, so Run stays disabled
|
|
161
|
+
// until it lands. A save to a different project is ignored.
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
const onSettingsChanged = (e) => {
|
|
164
|
+
const savedDir = e.detail?.dir;
|
|
165
|
+
if (savedDir !== undefined && savedDir !== dir)
|
|
166
|
+
return;
|
|
167
|
+
if (selectedId.current)
|
|
168
|
+
void loadStatus(selectedId.current).catch(() => undefined);
|
|
169
|
+
};
|
|
170
|
+
window.addEventListener(SETTINGS_CHANGED, onSettingsChanged);
|
|
171
|
+
return () => window.removeEventListener(SETTINGS_CHANGED, onSettingsChanged);
|
|
172
|
+
}, [dir, loadStatus]);
|
|
173
|
+
// batch.output progress for this panel's own run only (frames are matched by the run's tag).
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
let off;
|
|
176
|
+
let dead = false;
|
|
177
|
+
void onEngineEvent((frame) => {
|
|
178
|
+
const tag = runTag.current;
|
|
179
|
+
if (!tag || frame.tag !== tag)
|
|
180
|
+
return;
|
|
181
|
+
const ev = frame.event;
|
|
182
|
+
if (ev.event !== "batch.output")
|
|
183
|
+
return;
|
|
184
|
+
const key = typeof ev.key === "string" ? ev.key : undefined;
|
|
185
|
+
const state = ev.state;
|
|
186
|
+
if (key === undefined || !isBatchState(state))
|
|
187
|
+
return;
|
|
188
|
+
const patch = { state };
|
|
189
|
+
if (ev.output !== undefined)
|
|
190
|
+
patch.output = typeof ev.output === "string" ? ev.output : null;
|
|
191
|
+
if (ev.cover !== undefined)
|
|
192
|
+
patch.cover = asCover(ev.cover);
|
|
193
|
+
if (ev.error !== undefined)
|
|
194
|
+
patch.error = typeof ev.error === "string" ? ev.error : null;
|
|
195
|
+
if (typeof ev.attempts === "number")
|
|
196
|
+
patch.attempts = ev.attempts;
|
|
197
|
+
setDoc((d) => (d ? patchRow(d, key, patch) : d));
|
|
198
|
+
}).then((fn) => {
|
|
199
|
+
if (dead)
|
|
200
|
+
fn();
|
|
201
|
+
else
|
|
202
|
+
off = fn;
|
|
203
|
+
});
|
|
204
|
+
return () => {
|
|
205
|
+
dead = true;
|
|
206
|
+
off?.();
|
|
207
|
+
};
|
|
208
|
+
}, []);
|
|
209
|
+
// The cross product of the chosen timelines and presets is the output list.
|
|
210
|
+
const timelineOptions = [MAIN, ...variants];
|
|
211
|
+
const planned = timelineOptions
|
|
212
|
+
.filter((t) => timelines.has(t))
|
|
213
|
+
.flatMap((t) => PRESETS.filter((p) => presets.has(p.value)).map((p) => ({
|
|
214
|
+
variant: t === MAIN ? undefined : t,
|
|
215
|
+
preset: p.value,
|
|
216
|
+
label: `${t === MAIN ? "Main timeline" : t} · ${p.label}`,
|
|
217
|
+
})));
|
|
218
|
+
const create = () => {
|
|
219
|
+
setCreating(true);
|
|
220
|
+
setError(null);
|
|
221
|
+
setSummary(null);
|
|
222
|
+
void batchCreate(dir, planned.map(({ variant, preset }) => ({ variant, preset })))
|
|
223
|
+
.then(async (created) => {
|
|
224
|
+
selectedId.current = created.id;
|
|
225
|
+
setDoc(created);
|
|
226
|
+
setDrift(null);
|
|
227
|
+
setCompareError(null);
|
|
228
|
+
setBatchId(created.id);
|
|
229
|
+
toast(`Batch ${created.id} created`, { kind: "success" });
|
|
230
|
+
await refreshList();
|
|
231
|
+
})
|
|
232
|
+
.catch((e) => setError(errorMessage(e)))
|
|
233
|
+
.finally(() => setCreating(false));
|
|
234
|
+
};
|
|
235
|
+
const choose = (id) => {
|
|
236
|
+
selectedId.current = id;
|
|
237
|
+
setBatchId(id);
|
|
238
|
+
setDoc(null);
|
|
239
|
+
setSummary(null);
|
|
240
|
+
setError(null);
|
|
241
|
+
setDrift(null);
|
|
242
|
+
setCompareError(null);
|
|
243
|
+
if (!id)
|
|
244
|
+
return;
|
|
245
|
+
void loadStatus(id).catch((e) => setError(errorMessage(e)));
|
|
246
|
+
};
|
|
247
|
+
const outputs = doc?.outputs ?? [];
|
|
248
|
+
const driftShown = drift !== null && drift.id === doc?.id;
|
|
249
|
+
const qualified = outputs.filter((o) => o.state === "qualified").length;
|
|
250
|
+
const left = outputs.length - qualified;
|
|
251
|
+
// A fully delivered batch whose settings changed is not "All delivered": its way forward is the re-render.
|
|
252
|
+
const fullyDelivered = outputs.length > 0 && left === 0;
|
|
253
|
+
const runLabel = doc === null
|
|
254
|
+
? "Run"
|
|
255
|
+
: fullyDelivered
|
|
256
|
+
? driftShown
|
|
257
|
+
? "Run"
|
|
258
|
+
: "All delivered"
|
|
259
|
+
: qualified === 0
|
|
260
|
+
? "Run"
|
|
261
|
+
: `Resume (${left} left)`;
|
|
262
|
+
// The engine refuses a plain run while the settings differ, so Run waits for the re-render button.
|
|
263
|
+
// Run also waits while a settings read is in flight, and while this batch's settings cannot be compared.
|
|
264
|
+
const compareShown = compareError !== null && compareError.id === doc?.id;
|
|
265
|
+
const canRun = doc !== null && left > 0 && running === null && !driftShown && statusReads === 0 && !compareShown;
|
|
266
|
+
// Run is disabled with a stated reason when every output is delivered and the settings match.
|
|
267
|
+
const deliveredReason = fullyDelivered && !driftShown && !compareShown ? DELIVERED_REASON : null;
|
|
268
|
+
// `finishing: "current"` re-pins the batch to the current settings and renders every output again.
|
|
269
|
+
const run = (finishing) => {
|
|
270
|
+
if (!doc)
|
|
271
|
+
return;
|
|
272
|
+
const id = doc.id;
|
|
273
|
+
const job = `batch-ui-${Date.now().toString(36)}`;
|
|
274
|
+
const tag = `batch-${job}`;
|
|
275
|
+
runTag.current = tag;
|
|
276
|
+
setRunning(job);
|
|
277
|
+
setError(null);
|
|
278
|
+
setSummary(null);
|
|
279
|
+
void batchRun(dir, id, job, tag, finishing)
|
|
280
|
+
.then((result) => {
|
|
281
|
+
setDoc((d) => (d && d.id === id ? applyRunOutputs(d, result.outputs) : d));
|
|
282
|
+
const pin = result.finishing?.action === "repinned"
|
|
283
|
+
? " · re-pinned to current settings"
|
|
284
|
+
: result.finishing?.action === "pinned"
|
|
285
|
+
? " · pinned to current settings"
|
|
286
|
+
: "";
|
|
287
|
+
setSummary(`Rendered ${result.ran.length}, kept ${result.skipped.length}${result.status === "partial" ? " · partial, run again to finish" : ""}${pin}`);
|
|
288
|
+
})
|
|
289
|
+
.catch((e) => {
|
|
290
|
+
const code = errorCode(e);
|
|
291
|
+
setError(code === "cancelled" || code === "render_cancelled"
|
|
292
|
+
? "Cancelled — run again to resume where it stopped."
|
|
293
|
+
: errorMessage(e));
|
|
294
|
+
})
|
|
295
|
+
.finally(() => {
|
|
296
|
+
runTag.current = null;
|
|
297
|
+
setRunning(null);
|
|
298
|
+
// A refused run (settings changed) leaves nothing rendered; this fresh status is what shows the notice.
|
|
299
|
+
void loadStatus(id).catch(() => undefined);
|
|
300
|
+
void refreshList().catch(() => undefined);
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
const cancel = () => {
|
|
304
|
+
if (running === null)
|
|
305
|
+
return;
|
|
306
|
+
void recipeCancel(running).catch((e) => setError(errorMessage(e)));
|
|
307
|
+
};
|
|
308
|
+
const setCover = (row) => {
|
|
309
|
+
if (!doc)
|
|
310
|
+
return;
|
|
311
|
+
const raw = (coverAt[row.key] ?? "0").trim();
|
|
312
|
+
const sec = Number(raw);
|
|
313
|
+
if (raw === "" || !Number.isFinite(sec) || sec < 0) {
|
|
314
|
+
setError(`Enter a cover time in seconds (0 or more) for ${row.key}.`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const id = doc.id;
|
|
318
|
+
setError(null);
|
|
319
|
+
void batchCover(dir, id, row.key, Math.round(sec * 1000))
|
|
320
|
+
.then((res) => setDoc((d) => (d && d.id === id ? patchRow(d, row.key, res) : d)))
|
|
321
|
+
.catch((e) => setError(errorMessage(e)));
|
|
322
|
+
};
|
|
323
|
+
return (_jsxs("section", { className: "batch-delivery", "data-testid": "batch-delivery", "aria-label": "Batch delivery", children: [_jsx("h2", { className: "panel-title", children: "Batch delivery" }), _jsxs("div", { className: "panel-stack", children: [_jsx("span", { className: "cr-overline", children: "Presets" }), _jsx("div", { className: "batch-checks", role: "group", "aria-label": "Presets", children: PRESETS.map((p) => (_jsx(Checkbox, { checked: presets.has(p.value), onChange: (checked) => setPresets((s) => withItem(s, p.value, checked)), label: p.label }, p.value))) }), _jsx("span", { className: "cr-overline", children: "Timelines" }), _jsx("div", { className: "batch-checks", role: "group", "aria-label": "Timelines", children: timelineOptions.map((t) => (_jsx(Checkbox, { checked: timelines.has(t), onChange: (checked) => setTimelines((s) => withItem(s, t, checked)), label: t === MAIN ? "Main" : t }, t || "main"))) }), planned.length > 0 ? (_jsx("ul", { className: "batch-plan", "aria-label": "Outputs to create", children: planned.map((p) => (_jsx("li", { children: p.label }, p.label))) })) : (_jsx("p", { className: "panel-note", children: "Pick at least one preset and one timeline." })), _jsx("div", { className: "panel-row", children: _jsx(Button, { variant: "secondary", size: "sm", "data-testid": "batch-create", onClick: create, disabled: (planned.length === 0 || creating || running !== null) || (creating), "aria-busy": (creating) || undefined, children: creating ? "Creating…" : "Create batch" }) })] }), _jsxs("div", { className: "panel-stack", children: [_jsx(Field, { label: "Batch", children: (ids) => (_jsxs("select", { ...ids, className: "rk-input cr-select", "data-testid": "batch-select", value: batchId, onChange: (e) => choose(e.target.value), children: [_jsx("option", { value: "", children: batches.length === 0 ? "No batches yet" : "Choose a batch…" }), batches.map((b) => (_jsx("option", { value: b.id, children: b.id }, b.id)))] })) }), compareError && compareError.id === doc?.id && (_jsxs("p", { className: "panel-note", role: "status", "data-testid": "batch-compare-error", children: ["Could not compare this batch with the current finishing settings: ", compareError.message] })), driftShown && drift && (_jsxs("div", { role: "alert", id: DRIFT_NOTICE_ID, className: "panel-stack", "data-testid": "batch-drift", children: [_jsxs("p", { className: "panel-error", children: ["Finishing settings changed since this batch was created:", " ", drift.differs.length > 0
|
|
324
|
+
? drift.differs.map(settingWords).join(", ")
|
|
325
|
+
: "an unreported setting", "."] }), _jsx("p", { className: "panel-note", children: "Outputs already delivered would no longer match the rest. Restore the settings, or re-render every output with the current ones." }), _jsx("div", { className: "panel-row", children: _jsx(Button, { variant: "secondary", size: "sm", "data-testid": "batch-rerender", disabled: outputs.length === 0 || running !== null, onClick: () => run("current"), children: "Re-render all with current settings" }) })] })), _jsxs("div", { className: "panel-row", children: [_jsxs(Button, { variant: "primary", size: "sm", "data-testid": "batch-run", "aria-describedby": driftShown ? DRIFT_NOTICE_ID : deliveredReason ? DELIVERED_REASON_ID : undefined, title: driftShown
|
|
326
|
+
? "Finishing settings changed: re-render all with current settings instead"
|
|
327
|
+
: (deliveredReason ?? undefined), onClick: () => run(), disabled: (!canRun) || (running !== null), "aria-busy": (running !== null) || undefined, children: [!(running !== null) && _jsx(Icon, { name: "play" }), running !== null ? "Running…" : runLabel] }), running !== null && (_jsx(Button, { variant: "secondary", size: "sm", "data-testid": "batch-cancel", onClick: cancel, children: "Cancel" }))] }), deliveredReason && (_jsx("p", { className: "panel-note", id: DELIVERED_REASON_ID, "data-testid": "batch-delivered-reason", children: deliveredReason })), summary && (_jsx("p", { className: "panel-note", role: "status", "data-testid": "batch-summary", children: summary })), error && (_jsx("p", { role: "alert", className: "panel-error", "data-testid": "batch-error", children: error })), doc === null ? (_jsx("p", { className: "panel-note", children: batchId ? "Loading batch…" : "Create a batch or choose one to see its outputs." })) : outputs.length === 0 ? (_jsx("p", { className: "panel-note", children: "This batch has no outputs." })) : (_jsx("div", { className: "batch-table-wrap", children: _jsxs(Table, { className: "batch-table", label: "Batch outputs", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableHeaderCell, { children: "Output" }), _jsx(TableHeaderCell, { children: "Preset" }), _jsx(TableHeaderCell, { children: "State" }), _jsx(TableHeaderCell, { children: "Attempts" }), _jsx(TableHeaderCell, { children: "Cover" }), _jsx(TableHeaderCell, { children: "Choose cover" })] }) }), _jsx(TableBody, { children: outputs.map((row) => {
|
|
328
|
+
const shape = PRESET_BY_ID[row.preset]?.shape ?? "wide";
|
|
329
|
+
return (_jsxs(TableRow, { "data-testid": `batch-row-${row.key}`, children: [_jsx(TableCell, { children: _jsx("span", { className: "cr-mono", children: row.key }) }), _jsx(TableCell, { children: PRESET_BY_ID[row.preset]?.label ?? row.preset }), _jsxs(TableCell, { children: [_jsx("span", { "data-testid": `batch-state-${row.key}`, children: _jsx(Badge, { tone: STATE_TONE[row.state], children: STATE_LABEL[row.state] }) }), row.error && _jsx("p", { className: "batch-table__error", children: row.error })] }), _jsx(TableCell, { className: "cr-mono", children: row.attempts }), _jsx(TableCell, { children: row.cover ? (_jsx("img", { className: `batch-cover batch-cover--${shape}`, src: assetUrl(projectFile(dir, row.cover.path)) + `?v=${row.cover.blake3}`, alt: `Cover for ${row.key}`, "data-testid": `batch-cover-${row.key}`, loading: "lazy", draggable: false })) : (_jsx("span", { className: "panel-note", children: "None yet" })) }), _jsx(TableCell, { children: _jsxs("div", { className: "batch-cover-set", children: [_jsx(Input, { type: "number", className: "batch-cover-input", min: 0, step: 0.1, "aria-label": `Cover time for ${row.key} in seconds`, "data-testid": `batch-cover-input-${row.key}`, value: coverAt[row.key] ?? "0", disabled: !row.output, onChange: (e) => setCoverAt((m) => ({ ...m, [row.key]: e.target.value })) }), _jsx(Button, { size: "sm", variant: "ghost", "data-testid": `batch-cover-set-${row.key}`, disabled: !row.output || running !== null, onClick: () => setCover(row), children: "Set cover" })] }) })] }, row.key));
|
|
330
|
+
}) })] }) }))] })] }));
|
|
331
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Portable bundle — write this project to a folder that can be imported elsewhere (bundle.export).
|
|
3
|
+
import { Button, useToast } from "@rightkit/react-shell/react";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { pickFolder } from "../lib/dialog.js";
|
|
6
|
+
import { errorMessage, onEngineEvent, revealPath } from "../lib/engine.js";
|
|
7
|
+
import { bundleExport } from "../lib/media.js";
|
|
8
|
+
import { baseName } from "../lib/project.js";
|
|
9
|
+
import { ProgressRow, fileSize, Icon } from "../ui/index.js";
|
|
10
|
+
const TAG = "bundle-export";
|
|
11
|
+
const MEDIA_CHOICES = [
|
|
12
|
+
{ value: "collected", label: "Include media", note: "Copies every source file into the bundle." },
|
|
13
|
+
{
|
|
14
|
+
value: "referenced",
|
|
15
|
+
label: "Reference media in place",
|
|
16
|
+
note: "Lists each source by its current path; nothing is copied.",
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
export function BundleExport({ dir }) {
|
|
20
|
+
const toast = useToast();
|
|
21
|
+
const [media, setMedia] = useState("collected");
|
|
22
|
+
const [folder, setFolder] = useState(null);
|
|
23
|
+
const [busy, setBusy] = useState(false);
|
|
24
|
+
const [progress, setProgress] = useState(null);
|
|
25
|
+
const [error, setError] = useState(null);
|
|
26
|
+
const [result, setResult] = useState(null);
|
|
27
|
+
// Write-stage progress for this call only (frames carry the caller's tag).
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let off;
|
|
30
|
+
let dead = false;
|
|
31
|
+
void onEngineEvent((frame) => {
|
|
32
|
+
if (frame.tag !== TAG)
|
|
33
|
+
return;
|
|
34
|
+
const ev = frame.event;
|
|
35
|
+
if (ev.event === "bundle.progress" && typeof ev.completed === "number" && typeof ev.total === "number") {
|
|
36
|
+
setProgress({ completed: ev.completed, total: ev.total });
|
|
37
|
+
}
|
|
38
|
+
}).then((fn) => {
|
|
39
|
+
if (dead)
|
|
40
|
+
fn();
|
|
41
|
+
else
|
|
42
|
+
off = fn;
|
|
43
|
+
});
|
|
44
|
+
return () => {
|
|
45
|
+
dead = true;
|
|
46
|
+
off?.();
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
const choose = () => {
|
|
50
|
+
// The engine writes the bundle into this folder itself, so it must be empty or new.
|
|
51
|
+
pickFolder("Choose an empty folder for the bundle")
|
|
52
|
+
.then((picked) => {
|
|
53
|
+
if (!picked)
|
|
54
|
+
return;
|
|
55
|
+
setFolder(picked);
|
|
56
|
+
setResult(null);
|
|
57
|
+
setError(null);
|
|
58
|
+
})
|
|
59
|
+
.catch((e) => setError(errorMessage(e)));
|
|
60
|
+
};
|
|
61
|
+
const run = () => {
|
|
62
|
+
if (!folder)
|
|
63
|
+
return;
|
|
64
|
+
setBusy(true);
|
|
65
|
+
setError(null);
|
|
66
|
+
setResult(null);
|
|
67
|
+
setProgress(null);
|
|
68
|
+
void bundleExport(dir, { out: folder, media }, TAG)
|
|
69
|
+
.then(setResult)
|
|
70
|
+
.catch((e) => setError(errorMessage(e)))
|
|
71
|
+
.finally(() => {
|
|
72
|
+
setBusy(false);
|
|
73
|
+
setProgress(null);
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
const choice = MEDIA_CHOICES.find((c) => c.value === media) ?? MEDIA_CHOICES[0];
|
|
77
|
+
return (_jsxs("section", { className: "bundle-export", "aria-label": "Portable bundle", children: [_jsx("h2", { className: "panel-title", children: "Portable bundle" }), _jsx("p", { className: "panel-note", children: "Write this project to a folder you can import on another computer." }), _jsxs("div", { className: "panel-stack", children: [_jsx("div", { className: "batch-checks", role: "radiogroup", "aria-label": "Media", children: MEDIA_CHOICES.map((c) => (_jsxs("label", { children: [_jsx("input", { type: "radio", name: "bundle-media", value: c.value, checked: media === c.value, disabled: busy, onChange: () => {
|
|
78
|
+
setMedia(c.value);
|
|
79
|
+
setResult(null);
|
|
80
|
+
} }), c.label] }, c.value))) }), _jsx("p", { className: "panel-note", children: choice.note }), _jsxs("div", { className: "panel-row", children: [_jsxs(Button, { variant: "secondary", size: "sm", onClick: choose, disabled: busy, children: [_jsx(Icon, { name: "folder-open" }), "Choose folder\u2026"] }), folder && (_jsx("span", { className: "cr-mono cr-clamp-1 bundle-export__path", title: folder, children: folder }))] }), _jsx("div", { className: "panel-row", children: _jsx(Button, { variant: "primary", size: "sm", onClick: run, disabled: (!folder || busy) || (busy), "aria-busy": (busy) || undefined, children: busy ? "Exporting…" : "Export bundle" }) }), busy && (_jsx(ProgressRow, { label: "Bundle", status: progress ? `${progress.completed} of ${progress.total} files` : "Preparing", fraction: progress && progress.total > 0 ? progress.completed / progress.total : undefined })), error && (_jsx("p", { role: "alert", className: "bundle-export__error", children: error })), result && (_jsxs("div", { className: "bundle-export__result", children: [_jsxs("p", { role: "status", className: "panel-note", children: ["Bundle written to ", _jsx("span", { className: "cr-mono bundle-export__path", children: result.out })] }), _jsxs("p", { className: "panel-note", children: [result.members, " files, ", fileSize(result.bytes), ", plus bundle.json"] }), result.external.length > 0 && (_jsxs("div", { className: "bundle-export__external", children: [_jsx("span", { className: "cr-overline", children: "Not in the bundle" }), _jsx("ul", { children: result.external.map((e) => (_jsxs("li", { title: e.path, children: [e.name ?? baseName(e.path), _jsx("span", { className: "cr-muted", children: e.kind === "asset" ? " · missing font or LUT" : " · referenced in place" })] }, `${e.kind}:${e.path}`))) })] })), _jsx("div", { className: "panel-row", children: _jsxs(Button, { size: "sm", variant: "ghost", onClick: () => void revealPath(result.out).catch((e) => toast(errorMessage(e), { kind: "error", ttl: 0 })), children: [_jsx(Icon, { name: "folder-open" }), "Reveal bundle"] }) })] }))] })] }));
|
|
81
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/* Exports — docs/design/app-ui.md §3.5 */
|
|
2
|
+
|
|
3
|
+
.exports-body { display: flex; flex-direction: column; gap: var(--s5); }
|
|
4
|
+
|
|
5
|
+
.exports-queue {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: var(--s3);
|
|
9
|
+
padding: var(--s4);
|
|
10
|
+
background: var(--rk-elevated);
|
|
11
|
+
border: 1px solid var(--rk-line);
|
|
12
|
+
border-radius: var(--r-lg);
|
|
13
|
+
}
|
|
14
|
+
.exports-queue__row {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: var(--s3);
|
|
18
|
+
}
|
|
19
|
+
.exports-queue__row .cr-progress { flex: 1; min-width: 0; }
|
|
20
|
+
|
|
21
|
+
.exports-preview {
|
|
22
|
+
background: var(--rk-elevated);
|
|
23
|
+
border: 1px solid var(--rk-line);
|
|
24
|
+
border-radius: var(--r-lg);
|
|
25
|
+
padding: var(--s4);
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
gap: var(--s3);
|
|
29
|
+
}
|
|
30
|
+
.exports-preview__head { display: flex; align-items: center; justify-content: space-between; }
|
|
31
|
+
|
|
32
|
+
.exports-grid {
|
|
33
|
+
display: grid;
|
|
34
|
+
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
35
|
+
gap: var(--s4);
|
|
36
|
+
}
|
|
37
|
+
.exports-card {
|
|
38
|
+
background: var(--rk-elevated);
|
|
39
|
+
border: 1px solid var(--rk-line);
|
|
40
|
+
border-radius: var(--r-lg);
|
|
41
|
+
padding: var(--s3);
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
gap: var(--s2);
|
|
45
|
+
min-width: 0;
|
|
46
|
+
}
|
|
47
|
+
.exports-card__thumb { padding: 0; border: 0; background: none; cursor: pointer; text-align: left; }
|
|
48
|
+
.exports-card__name { font: 500 13px/1.3 var(--rk-font-body); }
|
|
49
|
+
.exports-card__meta { font-size: 12px; }
|
|
50
|
+
.exports-card__row { display: flex; align-items: center; gap: var(--s2); }
|
|
51
|
+
.exports-card__spacer { flex: 1; }
|
|
52
|
+
.exports-lufs { font: 11px/1 var(--rk-font-mono); color: var(--rk-ink-2); font-variant-numeric: tabular-nums; }
|
|
53
|
+
|
|
54
|
+
.exports-docs {
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
gap: var(--s2);
|
|
58
|
+
}
|
|
59
|
+
.exports-doc {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: var(--s3);
|
|
63
|
+
padding: var(--s2) var(--s3);
|
|
64
|
+
background: var(--rk-elevated);
|
|
65
|
+
border: 1px solid var(--rk-line);
|
|
66
|
+
border-radius: var(--r-md);
|
|
67
|
+
font-size: 13px;
|
|
68
|
+
}
|
|
69
|
+
.exports-doc > span:first-of-type { flex: 1; min-width: 0; }
|
|
70
|
+
.exports-doc svg { color: var(--rk-ink-2); flex: none; }
|
|
71
|
+
|
|
72
|
+
/* Export drawer */
|
|
73
|
+
.exports-format {
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: flex-start;
|
|
76
|
+
gap: var(--s3);
|
|
77
|
+
padding: var(--s3);
|
|
78
|
+
border: 1px solid var(--rk-line);
|
|
79
|
+
border-radius: var(--r-md);
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
}
|
|
82
|
+
.exports-format:hover { border-color: var(--rk-ink-3); }
|
|
83
|
+
.exports-format--off { opacity: 0.6; cursor: default; }
|
|
84
|
+
.exports-format__body { display: flex; flex-direction: column; gap: var(--s1); min-width: 0; }
|
|
85
|
+
.exports-format__title { font: 500 13px/1.3 var(--rk-font-body); }
|
|
86
|
+
.exports-format__title .cr-muted { font-weight: 400; font-size: 12px; }
|
|
87
|
+
|
|
88
|
+
/* Package form (package.build) */
|
|
89
|
+
.exports-package {
|
|
90
|
+
display: flex;
|
|
91
|
+
flex-direction: column;
|
|
92
|
+
gap: var(--s3);
|
|
93
|
+
padding: var(--s3);
|
|
94
|
+
border: 1px solid var(--rk-line);
|
|
95
|
+
border-radius: var(--r-md);
|
|
96
|
+
}
|
|
97
|
+
.exports-package__head { display: flex; align-items: center; justify-content: space-between; }
|
|
98
|
+
.exports-package__actions { display: flex; align-items: center; gap: var(--s2); }
|
|
99
|
+
.exports-package__result { margin: 0; font-size: 12px; overflow-wrap: anywhere; }
|
|
100
|
+
.exports-package__error { margin: 0; font-size: 13px; color: var(--rk-bad); }
|
|
101
|
+
|
|
102
|
+
/* Portable bundle (bundle.export) — panel matches .batch-delivery */
|
|
103
|
+
.bundle-export {
|
|
104
|
+
display: flex;
|
|
105
|
+
flex-direction: column;
|
|
106
|
+
gap: var(--s4);
|
|
107
|
+
min-width: 0;
|
|
108
|
+
padding: var(--s4);
|
|
109
|
+
background: var(--rk-elevated);
|
|
110
|
+
border: 1px solid var(--rk-line);
|
|
111
|
+
border-radius: var(--r-lg);
|
|
112
|
+
}
|
|
113
|
+
.bundle-export__path { min-width: 0; max-width: 100%; font-size: 12px; overflow-wrap: anywhere; }
|
|
114
|
+
.bundle-export__result { display: flex; flex-direction: column; gap: var(--s2); min-width: 0; }
|
|
115
|
+
.bundle-export__result .panel-note { overflow-wrap: anywhere; }
|
|
116
|
+
.bundle-export__external ul { margin: var(--s1) 0 0; padding: 0; list-style: none; display: grid; gap: var(--s1); font-size: 13px; }
|
|
117
|
+
.bundle-export__error { margin: 0; font-size: 13px; color: var(--rk-bad); }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProgressState } from "../hooks/useProgress.js";
|
|
2
|
+
type Preset = "youtube" | "reels" | "tiktok" | "square";
|
|
3
|
+
export declare function ExportsView({ dir, progress, rendering, onRender, onChanged, onImportRetention, }: {
|
|
4
|
+
dir: string;
|
|
5
|
+
progress?: ProgressState;
|
|
6
|
+
rendering?: boolean;
|
|
7
|
+
outputPath?: string | null;
|
|
8
|
+
onRender?: (preset: Preset) => void;
|
|
9
|
+
onChanged: () => Promise<void>;
|
|
10
|
+
onImportRetention?: (render: string) => void;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
export {};
|