@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,235 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Badge, Button, Dialog, SegmentedControl, Input, Select, Toggle, useShell, useToast } from "@rightkit/react-shell/react";
|
|
3
|
+
import { Suspense, lazy, useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { getVersion } from "@tauri-apps/api/app";
|
|
5
|
+
import { SettingRow, SettingsGroup, SettingsPanel, aboutSection, appearanceSection, diagnosticsSection, formatBytes, legalSection, licenseSection, mergeSections, modelsSection, updatesSection, } from "@rightkit/settings-ui";
|
|
6
|
+
import { Kbd } from "@rightkit/react-shell/react";
|
|
7
|
+
import { engineCall, errorMessage } from "../lib/engine.js";
|
|
8
|
+
import { CAPTION_STYLES, DEFAULT_CAPTION_STYLE, storageClear, storageStatus } from "../lib/media.js";
|
|
9
|
+
import { genRightCallFailed, interpretHost } from "../lib/genright.js";
|
|
10
|
+
import { pickFile, pickFolder } from "../lib/dialog.js";
|
|
11
|
+
import { createModelsBackend } from "../lib/modelsBackend.js";
|
|
12
|
+
import { createHostSettingsBackend } from "../lib/settingsBackend.js";
|
|
13
|
+
import { themeStore } from "../lib/shell.js";
|
|
14
|
+
import { licenseClient, logs, restartAfterUpdate, updates } from "../lib/services.js";
|
|
15
|
+
import { LEGAL_GATE_DOCUMENTS, legalDocumentText } from "../legal/legalConfig.js";
|
|
16
|
+
import { LegalDocumentBody } from "@rightkit/legal-ui";
|
|
17
|
+
import { usePlatform } from "@rightkit/platform-ui/react";
|
|
18
|
+
import { HDR_COLOR_PRESETS, HDR_UNAVAILABLE_NOTICE, useHdrToSdr } from "../hooks/useHdrToSdr.js";
|
|
19
|
+
import { hostAppName } from "../appName.js";
|
|
20
|
+
const COLOR_PRESETS = [
|
|
21
|
+
["neutral", "Neutral Rec.709"],
|
|
22
|
+
["hdr-natural", "HDR natural"],
|
|
23
|
+
["hdr-soft", "HDR soft"],
|
|
24
|
+
["apple-log-natural", "Apple Log natural"],
|
|
25
|
+
["talking-head-auto", "Talking head (auto)"],
|
|
26
|
+
["warm", "Warm"],
|
|
27
|
+
["cool", "Cool"],
|
|
28
|
+
["lift", "Lift"],
|
|
29
|
+
];
|
|
30
|
+
const AUDIO_PRESETS = [
|
|
31
|
+
["", "None"],
|
|
32
|
+
["talking-head", "Talking head"],
|
|
33
|
+
["podcast", "Podcast"],
|
|
34
|
+
["noisy-room", "Noisy room"],
|
|
35
|
+
];
|
|
36
|
+
/** Plain-language rows for the engine's readiness checks. An optional check never reads as "engine not ready". */
|
|
37
|
+
const DIAGNOSTICS = [
|
|
38
|
+
{
|
|
39
|
+
id: "data_dir",
|
|
40
|
+
label: "App data folder",
|
|
41
|
+
sentence: {
|
|
42
|
+
ok: `Writable — ${hostAppName()} can save projects, settings and caches here.`,
|
|
43
|
+
failed: `Not writable — ${hostAppName()} cannot save projects until this is fixed.`,
|
|
44
|
+
missing: "Not found on this machine.",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "resource_dir",
|
|
49
|
+
label: "App resources",
|
|
50
|
+
sentence: {
|
|
51
|
+
ok: "Found — bundled tools are available.",
|
|
52
|
+
failed: "Not readable — bundled tools are unavailable.",
|
|
53
|
+
missing: "Not running from an app bundle; bundled tools are unavailable.",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "editing_ffmpeg",
|
|
58
|
+
label: "Video engine (FFmpeg)",
|
|
59
|
+
sentence: {
|
|
60
|
+
ok: "Found — probing, thumbnails and rendering work.",
|
|
61
|
+
failed: "Present but unusable — media features will fail.",
|
|
62
|
+
missing: "Not found — probing, thumbnails and rendering will fail.",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "hdr_to_sdr",
|
|
67
|
+
label: "HDR to SDR conversion",
|
|
68
|
+
optional: true,
|
|
69
|
+
sentence: {
|
|
70
|
+
ok: "Available — HDR (PQ or HLG) footage can be finished to SDR here.",
|
|
71
|
+
failed: "Not available — HDR (PQ or HLG) footage cannot be finished on this computer.",
|
|
72
|
+
missing: "This engine version does not report HDR to SDR conversion.",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
/** Colour finish options; with HDR to SDR unavailable the HDR presets stay selectable but say so. */
|
|
77
|
+
function colourOptions(hdr) {
|
|
78
|
+
return COLOR_PRESETS.map(([v, l]) => hdr === false && HDR_COLOR_PRESETS.includes(v) ? [v, `${l} — not available on this computer`] : [v, l]);
|
|
79
|
+
}
|
|
80
|
+
const str = (v, fallback = "") => (v === undefined || v === null ? fallback : String(v));
|
|
81
|
+
function EditingSection({ ctx }) {
|
|
82
|
+
const formats = ctx.values.default_formats ?? ["16:9"];
|
|
83
|
+
return (_jsxs(SettingsGroup, { children: [_jsx(SettingRow, { id: "cut-style", label: "Cut style", description: "Seeds the edit recipe for new projects.", children: _jsx(SegmentedControl, { label: "Cut style", value: str(ctx.values.cut_preset, "natural"), options: [
|
|
84
|
+
{ value: "natural", label: "Natural" },
|
|
85
|
+
{ value: "tight", label: "Tight" },
|
|
86
|
+
{ value: "punchy", label: "Punchy" },
|
|
87
|
+
], onChange: (v) => void ctx.set("cut_preset", v) }) }), _jsx(SettingRow, { id: "formats", label: "Formats", description: "Formats the recipe produces by default.", children: _jsx("span", { className: "cr-settings-chips", children: ["16:9", "9:16", "1:1"].map((f) => (_jsx(Button, { variant: "ghost", size: "sm", className: "cr-chip-btn", onClick: () => void ctx.set("default_formats", formats.includes(f) ? formats.filter((x) => x !== f) : [...formats, f]), children: _jsx(Badge, { tone: formats.includes(f) ? "accent" : "neutral", children: f }) }, f))) }) })] }));
|
|
88
|
+
}
|
|
89
|
+
function FinishingSection({ ctx }) {
|
|
90
|
+
const lut = str(ctx.values.lut_path);
|
|
91
|
+
const hdr = useHdrToSdr();
|
|
92
|
+
const preset = str(ctx.values.color_preset, "neutral");
|
|
93
|
+
// A saved HDR preset that cannot run is kept, and the notice is an alert because it is in use.
|
|
94
|
+
const hdrInUse = HDR_COLOR_PRESETS.includes(preset);
|
|
95
|
+
return (_jsxs(_Fragment, { children: [_jsxs(SettingsGroup, { title: "Colour", children: [_jsx(SettingRow, { id: "color-preset", label: "Colour finish", description: "Applied to every render.", children: _jsxs("span", { className: "cr-settings-field", children: [_jsx(Select, { label: "Colour finish", value: preset, options: colourOptions(hdr).map(([value, label]) => ({ value, label })), onChange: (v) => void ctx.set("color_preset", v) }), hdr === false && (_jsx("p", { className: "cr-settings-note", role: hdrInUse ? "alert" : undefined, children: HDR_UNAVAILABLE_NOTICE }))] }) }), _jsx(SettingRow, { id: "custom-lut", label: "Custom LUT", description: lut ? lut.split(/[\\/]/).pop() : "A .cube LUT. It is the default for the Custom finish only; other finishes ignore it.", children: _jsxs("span", { className: "cr-settings-chips", children: [_jsx(Button, { size: "sm", variant: "secondary", onClick: () => void pickFile("Choose a LUT file", ["cube", "3dl"]).then((f) => {
|
|
96
|
+
if (f)
|
|
97
|
+
void ctx.set("lut_path", f);
|
|
98
|
+
}), children: "Choose\u2026" }), lut && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => void ctx.set("lut_path", null), children: "Clear" }))] }) }), lut && (_jsx(SettingRow, { id: "lut-intensity", label: "LUT intensity", description: "How strongly the LUT is applied (0\u2013100%).", children: _jsx(Input, { className: "cr-settings-num", "aria-label": "LUT intensity (percent)", type: "number", min: 0, max: 100, step: 5, value: str(ctx.values.lut_intensity, "100"), onChange: (e) => void ctx.set("lut_intensity", Number(e.target.value)) }) })), _jsx(SettingRow, { id: "match-shots", label: "Match shots", description: "Match colour between sources before grading.", children: _jsx(Toggle, { label: "Match shots", checked: ctx.values.match_shots !== false, onChange: (v) => void ctx.set("match_shots", v) }) })] }), _jsx(SettingsGroup, { title: "Framing", children: _jsx(SettingRow, { id: "framing-approval", label: "Approve framing automatically", description: "Approve analysed framing using saved policy; turn off to review each plan.", children: _jsx(Toggle, { label: "Approve framing automatically", checked: ctx.values.framing_approval === "auto", onChange: (v) => void ctx.set("framing_approval", v ? "auto" : "review") }) }) }), _jsx(SettingsGroup, { title: "Audio", children: _jsx(SettingRow, { id: "audio-preset", label: "Audio finish", description: "Cleanup and loudness target.", children: _jsx(Select, { label: "Audio finish", value: str(ctx.values.audio_preset, "talking-head"), options: AUDIO_PRESETS.map(([value, label]) => ({ value, label })), onChange: (v) => void ctx.set("audio_preset", v) }) }) }), _jsxs(SettingsGroup, { title: "Captions", children: [_jsx(SettingRow, { id: "burn-captions", label: "Burn captions", description: "Captions are drawn into finished edits.", children: _jsx(Toggle, { label: "Burn captions", checked: ctx.values.burn_captions !== false, onChange: (v) => void ctx.set("burn_captions", v) }) }), _jsx(SettingRow, { id: "caption-style", label: "Caption style", children: _jsx(Select, { label: "Caption style", value: str(ctx.values.caption_style, DEFAULT_CAPTION_STYLE), options: CAPTION_STYLES.map(([value]) => ({ value, label: value.replace(/-/g, " ") })), onChange: (v) => void ctx.set("caption_style", v) }) })] })] }));
|
|
99
|
+
}
|
|
100
|
+
/** Encoder choices per platform; ids must match the engine's encoder names. */
|
|
101
|
+
const ENCODER_OPTIONS_MAC = [
|
|
102
|
+
["auto", "Auto"],
|
|
103
|
+
["videotoolbox", "VideoToolbox"],
|
|
104
|
+
];
|
|
105
|
+
const ENCODER_OPTIONS_WINDOWS = [
|
|
106
|
+
["auto", "Auto"],
|
|
107
|
+
["nvenc", "NVIDIA NVENC"],
|
|
108
|
+
["mf", "Media Foundation"],
|
|
109
|
+
];
|
|
110
|
+
function OutputSection({ ctx }) {
|
|
111
|
+
const encoderOptions = usePlatform() === "macos" ? ENCODER_OPTIONS_MAC : ENCODER_OPTIONS_WINDOWS;
|
|
112
|
+
const savedEncoder = str(ctx.values.encoder, "auto");
|
|
113
|
+
// A saved value that this platform does not offer (e.g. legacy "libx264") shows as Auto.
|
|
114
|
+
const encoderValue = encoderOptions.some(([v]) => v === savedEncoder) ? savedEncoder : "auto";
|
|
115
|
+
return (_jsxs(SettingsGroup, { children: [_jsx(SettingRow, { id: "encoder", label: "Encoder", description: "The system H.264 encoder for exports.", children: _jsx(Select, { label: "Encoder", value: encoderValue, options: encoderOptions.map(([value, label]) => ({ value, label })), onChange: (v) => void ctx.set("encoder", v) }) }), _jsx(SettingRow, { id: "export-dir", label: "Export folder", description: str(ctx.values.export_dir, "Renders stay inside the project folder."), children: _jsx(Button, { size: "sm", variant: "secondary", onClick: () => void pickFolder("Default export folder").then((d) => {
|
|
116
|
+
if (d)
|
|
117
|
+
void ctx.set("export_dir", d);
|
|
118
|
+
}), children: "Choose\u2026" }) })] }));
|
|
119
|
+
}
|
|
120
|
+
function StorageSection({ ctx, dir }) {
|
|
121
|
+
const toast = useToast();
|
|
122
|
+
const [storage, setStorage] = useState(null);
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
storageStatus(dir ?? undefined).then(setStorage).catch(() => setStorage(null));
|
|
125
|
+
}, [dir]);
|
|
126
|
+
return (_jsxs(SettingsGroup, { children: [_jsx(SettingRow, { id: "project-folder", label: "Project folder", description: `Dropped footage creates a new project here: ${str(ctx.values.project_root, storage?.default_project_root ?? "")}`, children: _jsx(Button, { size: "sm", variant: "secondary", onClick: () => void pickFolder("Project folder").then((d) => {
|
|
127
|
+
if (d)
|
|
128
|
+
void ctx.set("project_root", d);
|
|
129
|
+
}), children: "Choose\u2026" }) }), _jsx(SettingRow, { id: "cache", label: "Cache", description: "Thumbnails and waveforms are rebuilt on demand; clearing is safe.", children: _jsxs("span", { className: "cr-settings-chips", children: [_jsx("span", { className: "cr-mono", children: storage ? formatBytes(storage.cache_bytes) : "…" }), _jsx(Button, { size: "sm", variant: "secondary", disabled: !storage || storage.cache_bytes === 0, onClick: () => void storageClear(dir ?? undefined)
|
|
130
|
+
.then((r) => {
|
|
131
|
+
setStorage((s) => (s ? { ...s, cache_bytes: 0 } : s));
|
|
132
|
+
toast(`Cleared ${formatBytes(r.freed_bytes)}`, { kind: "success" });
|
|
133
|
+
})
|
|
134
|
+
.catch((e) => toast(errorMessage(e), { kind: "error", ttl: 0 })), children: "Clear" })] }) })] }));
|
|
135
|
+
}
|
|
136
|
+
function HostSection({ ctx }) {
|
|
137
|
+
const [gr, setGr] = useState({ state: "checking" });
|
|
138
|
+
const [path, setPath] = useState(str(ctx.values.genright_engine_path));
|
|
139
|
+
const check = () => void engineCall("genright.status", {})
|
|
140
|
+
.then((r) => setGr(interpretHost(r)))
|
|
141
|
+
.catch((e) => setGr(genRightCallFailed(errorMessage(e))));
|
|
142
|
+
useEffect(check, []);
|
|
143
|
+
return (_jsxs(SettingsGroup, { children: [_jsx(SettingRow, { id: "genright-path", label: "Engine location", description: `Path to the ${hostAppName()} engine; empty means auto-detect.`, stacked: true, children: _jsxs("span", { className: "cr-settings-chips", children: [_jsx(Input, { className: "cr-settings-path", "aria-label": `${hostAppName()} engine path`, value: path, placeholder: "Auto-detect", onChange: (e) => setPath(e.target.value), onBlur: () => void ctx.set("genright_engine_path", path.trim() === "" ? null : path.trim()) }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => void pickFile(`Locate the ${hostAppName()} engine`, null).then((f) => {
|
|
144
|
+
if (f) {
|
|
145
|
+
setPath(f);
|
|
146
|
+
void ctx.set("genright_engine_path", f);
|
|
147
|
+
}
|
|
148
|
+
}), children: "Locate\u2026" })] }) }), _jsx(SettingRow, { id: "genright-status", label: "Status", description: gr.detail ?? `Media generation runs through the ${hostAppName()} app.`, children: _jsxs("span", { className: "cr-settings-chips", children: [_jsx(Badge, { tone: gr.state === "ready" ? "ok" : gr.state === "checking" ? "neutral" : "warn", children: gr.state === "ready" ? `${hostAppName()} ready` : gr.state === "checking" ? "Checking…" : "Not available" }), _jsx(Button, { size: "sm", variant: "secondary", onClick: check, children: "Re-check" })] }) }), _jsx(SettingRow, { id: "genright-budget", label: "Budget", description: "Optional spend cap (USD) for generation jobs.", children: _jsx(Input, { className: "cr-settings-num", "aria-label": `${hostAppName()} budget (USD)`, type: "number", min: 0, step: 1, value: str(ctx.values.genright_budget_usd), placeholder: "No cap", onChange: (e) => void ctx.set("genright_budget_usd", e.target.value === "" ? null : Number(e.target.value)) }) })] }));
|
|
149
|
+
}
|
|
150
|
+
function EngineSection({ doctor, error }) {
|
|
151
|
+
const checks = doctor?.readiness?.checks ?? [];
|
|
152
|
+
return (_jsx(SettingsGroup, { description: error ?? undefined, children: DIAGNOSTICS.map((d) => {
|
|
153
|
+
const c = checks.find((x) => x.id === d.id);
|
|
154
|
+
const st = (c?.status ?? "").toLowerCase();
|
|
155
|
+
// An optional check that is absent from a loaded report is reported as unavailable, not still checking.
|
|
156
|
+
const waiting = !c && !(d.optional && doctor);
|
|
157
|
+
const ok = st === "ok";
|
|
158
|
+
const tone = ok ? "ok" : waiting || d.optional ? "neutral" : "warn";
|
|
159
|
+
const label = ok ? "OK" : waiting ? "Checking…" : d.optional ? "Unavailable" : "Needs attention";
|
|
160
|
+
const sentence = waiting ? "Waiting for the engine check." : !c ? d.sentence.missing : (d.sentence[st] ?? d.sentence.failed);
|
|
161
|
+
return (_jsx(SettingRow, { id: `diag-${d.id}`, label: d.label, description: _jsxs(_Fragment, { children: [sentence, c?.detail && (_jsxs("details", { className: "cr-settings-detail", children: [_jsx("summary", { children: "Details" }), _jsx("p", { children: c.detail })] }))] }), children: _jsx(Badge, { tone: tone, children: label }) }, d.id));
|
|
162
|
+
}) }));
|
|
163
|
+
}
|
|
164
|
+
/** Every binding on the shell's shortcut registry, as the help overlay lists them (hidden ones omitted). */
|
|
165
|
+
// Agent demo settings: a lazy chunk that exists only when `__RIGHTKIT_DEMO__` (vite.config.ts), so a
|
|
166
|
+
// release bundle neither imports nor contains src/demo.
|
|
167
|
+
const DEMO_BUILD = typeof __RIGHTKIT_DEMO__ === "boolean" && __RIGHTKIT_DEMO__;
|
|
168
|
+
const DemoSettingsLazy = DEMO_BUILD
|
|
169
|
+
? lazy(() => import("../demo/DemoSettings.js").then((m) => ({ default: m.DemoSettings })))
|
|
170
|
+
: null;
|
|
171
|
+
function ShortcutsSection() {
|
|
172
|
+
const { registry } = useShell();
|
|
173
|
+
useSyncExternalStore(registry.subscribe, registry.version);
|
|
174
|
+
return (_jsx(_Fragment, { children: registry.groups().map((group) => (_jsx(SettingsGroup, { title: group.title, children: group.items.map((info) => (_jsx(SettingRow, { id: `key-${info.id}`, label: info.label, children: info.chords.map((chord) => (_jsx(Kbd, { chord: chord }, chord))) }, info.id))) }, group.title))) }));
|
|
175
|
+
}
|
|
176
|
+
export function SettingsView({ dir, doctor, error, section, }) {
|
|
177
|
+
const backend = useMemo(() => createHostSettingsBackend(), []);
|
|
178
|
+
const models = useMemo(() => createModelsBackend(), []);
|
|
179
|
+
const [version, setVersion] = useState("");
|
|
180
|
+
const [active, setActive] = useState(section);
|
|
181
|
+
const [reading, setReading] = useState(null);
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
getVersion().then(setVersion).catch(() => setVersion(""));
|
|
184
|
+
}, []);
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (section)
|
|
187
|
+
setActive(section === "models" ? "engine" : section);
|
|
188
|
+
}, [section]);
|
|
189
|
+
const sections = mergeSections([
|
|
190
|
+
aboutSection({ appName: hostAppName(), version: version || "dev", copyright: "Orthic Labs" }),
|
|
191
|
+
appearanceSection({ themeStore }),
|
|
192
|
+
modelsSection({ models }),
|
|
193
|
+
licenseSection({ client: licenseClient }),
|
|
194
|
+
updatesSection({ client: updates, currentVersion: version || "dev", onRestart: restartAfterUpdate }),
|
|
195
|
+
diagnosticsSection({ logs }),
|
|
196
|
+
legalSection({
|
|
197
|
+
documents: LEGAL_GATE_DOCUMENTS,
|
|
198
|
+
// legalSection's per-document callback (settings-ui sections.tsx legalSection `openExternal`): the
|
|
199
|
+
// bundled documents open in the owner Dialog + LegalDocumentBody below; no document is opened
|
|
200
|
+
// externally and legal-ui has no reader component to adopt instead (its exports are LegalGate,
|
|
201
|
+
// policyGateDocuments and LegalDocumentBody).
|
|
202
|
+
openExternal: (url) => {
|
|
203
|
+
const document = LEGAL_GATE_DOCUMENTS.find((entry) => entry.url === url);
|
|
204
|
+
if (document)
|
|
205
|
+
setReading({ title: document.title, text: legalDocumentText(document.path) });
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
], [
|
|
209
|
+
{ id: "editing", label: "Editing defaults", group: "Editing", order: 300, keywords: ["cut", "format"], render: (ctx) => _jsx(EditingSection, { ctx: ctx }) },
|
|
210
|
+
{
|
|
211
|
+
id: "finishing",
|
|
212
|
+
label: "Finishing",
|
|
213
|
+
group: "Editing",
|
|
214
|
+
order: 310,
|
|
215
|
+
keywords: ["colour", "color", "lut", "audio", "captions", "loudness"],
|
|
216
|
+
entries: [
|
|
217
|
+
{ id: "color-preset", label: "Colour finish" },
|
|
218
|
+
{ id: "custom-lut", label: "Custom LUT" },
|
|
219
|
+
{ id: "match-shots", label: "Match shots" },
|
|
220
|
+
{ id: "audio-preset", label: "Audio finish" },
|
|
221
|
+
{ id: "burn-captions", label: "Burn captions" },
|
|
222
|
+
{ id: "caption-style", label: "Caption style" },
|
|
223
|
+
],
|
|
224
|
+
render: (ctx) => _jsx(FinishingSection, { ctx: ctx }),
|
|
225
|
+
},
|
|
226
|
+
{ id: "output", label: "Output", group: "Editing", order: 320, keywords: ["encoder", "export"], render: (ctx) => _jsx(OutputSection, { ctx: ctx }) },
|
|
227
|
+
{ id: "storage", label: "Storage", group: "App", order: 400, keywords: ["cache", "project folder"], render: (ctx) => _jsx(StorageSection, { ctx: ctx, dir: dir }) },
|
|
228
|
+
{ id: "genright", label: "Media generation", group: "App", order: 410, keywords: ["generate", "assets", "budget"], render: (ctx) => _jsx(HostSection, { ctx: ctx }) },
|
|
229
|
+
{ id: "engine", label: "Engine", group: "App", order: 420, keywords: ["diagnostics", "ffmpeg", "doctor"], render: () => _jsx(EngineSection, { doctor: doctor, error: error }) },
|
|
230
|
+
{ id: "shortcuts", label: "Shortcuts", group: "App", order: 430, keywords: ["keys", "keyboard"], render: () => _jsx(ShortcutsSection, {}) },
|
|
231
|
+
// Agent demo mode: present only in builds with the demo feature (vite.config.ts), off by default.
|
|
232
|
+
...(DEMO_BUILD ? [{ id: "agent-demos", label: "Agent demos", group: "App", order: 440, keywords: ["agent", "demo", "record", "automation", "consent"], render: () => DemoSettingsLazy ? _jsx(Suspense, { fallback: null, children: _jsx(DemoSettingsLazy, {}) }) : null }] : []),
|
|
233
|
+
]);
|
|
234
|
+
return (_jsxs(_Fragment, { children: [_jsx(SettingsPanel, { backend: backend, sections: sections, title: "Settings", activeId: active, onActiveChange: setActive, defaultActiveId: "appearance" }), reading && (_jsx(Dialog, { title: reading.title, ariaLabel: reading.title, width: 760, onClose: () => setReading(null), dismissible: false, actions: _jsx(Button, { onClick: () => setReading(null), children: "Close" }), children: _jsx(LegalDocumentBody, { content: reading.text, format: "markdown", title: reading.title }) }))] }));
|
|
235
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* Shorts — docs/design/app-ui.md §3.6 */
|
|
2
|
+
|
|
3
|
+
.shorts-body { display: flex; flex-direction: column; gap: var(--s4); padding-bottom: 72px; }
|
|
4
|
+
|
|
5
|
+
.shorts-grid {
|
|
6
|
+
display: grid;
|
|
7
|
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
8
|
+
gap: var(--s4);
|
|
9
|
+
}
|
|
10
|
+
.shorts-card {
|
|
11
|
+
background: var(--rk-elevated);
|
|
12
|
+
border: 1px solid var(--rk-line);
|
|
13
|
+
border-radius: var(--r-lg);
|
|
14
|
+
padding: var(--s3);
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
gap: var(--s2);
|
|
18
|
+
min-width: 0;
|
|
19
|
+
}
|
|
20
|
+
.shorts-card--sel { border-color: var(--rk-accent); box-shadow: 0 0 0 1px var(--rk-accent); }
|
|
21
|
+
.shorts-card__thumb { position: relative; padding: 0; border: 0; background: none; cursor: pointer; }
|
|
22
|
+
.shorts-card__rank {
|
|
23
|
+
position: absolute;
|
|
24
|
+
left: 6px;
|
|
25
|
+
top: 6px;
|
|
26
|
+
padding: 2px 6px;
|
|
27
|
+
border-radius: var(--r-sm);
|
|
28
|
+
background: rgb(0 0 0 / 0.6);
|
|
29
|
+
color: #fff;
|
|
30
|
+
font-size: 11px;
|
|
31
|
+
z-index: 2;
|
|
32
|
+
}
|
|
33
|
+
.shorts-card__meta { display: flex; align-items: center; gap: var(--s2); font-size: 12px; }
|
|
34
|
+
.shorts-card__hook { font-size: 12px; color: var(--rk-ink); line-height: 1.4; min-height: 2.6em; }
|
|
35
|
+
.shorts-card__pick {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: var(--s2);
|
|
39
|
+
font-size: 12px;
|
|
40
|
+
color: var(--rk-ink-2);
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
}
|
|
43
|
+
.shorts-card__pick input { accent-color: var(--rk-accent); }
|
|
44
|
+
|
|
45
|
+
.shorts-bar {
|
|
46
|
+
position: sticky;
|
|
47
|
+
bottom: var(--s4);
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: var(--s3);
|
|
51
|
+
padding: var(--s3) var(--s4);
|
|
52
|
+
background: var(--rk-elevated);
|
|
53
|
+
border: 1px solid var(--rk-line);
|
|
54
|
+
border-radius: var(--r-lg);
|
|
55
|
+
box-shadow: var(--rk-pop);
|
|
56
|
+
}
|
|
57
|
+
.shorts-bar span { flex: 1; font-size: 13px; }
|
|
58
|
+
|
|
59
|
+
.shorts-range-player {
|
|
60
|
+
width: 100%;
|
|
61
|
+
aspect-ratio: 9 / 16;
|
|
62
|
+
max-height: 50vh;
|
|
63
|
+
background: var(--cr-stage);
|
|
64
|
+
border-radius: var(--r-sm);
|
|
65
|
+
display: block;
|
|
66
|
+
object-fit: contain;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.shorts-trim { display: flex; align-items: center; gap: var(--s2); }
|
|
70
|
+
.shorts-trim__time { font-size: 10px; color: var(--rk-ink-2); flex: none; }
|
|
71
|
+
.shorts-trim__track {
|
|
72
|
+
position: relative;
|
|
73
|
+
flex: 1;
|
|
74
|
+
height: 24px;
|
|
75
|
+
background: var(--rk-fill);
|
|
76
|
+
border-radius: var(--r-sm);
|
|
77
|
+
touch-action: none;
|
|
78
|
+
}
|
|
79
|
+
.shorts-trim__sel {
|
|
80
|
+
position: absolute;
|
|
81
|
+
top: 0;
|
|
82
|
+
bottom: 0;
|
|
83
|
+
background: color-mix(in srgb, var(--rk-accent) 22%, transparent);
|
|
84
|
+
border-radius: var(--r-sm);
|
|
85
|
+
}
|
|
86
|
+
.shorts-trim__handle {
|
|
87
|
+
position: absolute;
|
|
88
|
+
top: 0;
|
|
89
|
+
bottom: 0;
|
|
90
|
+
width: 10px;
|
|
91
|
+
margin-left: -5px;
|
|
92
|
+
border: 0;
|
|
93
|
+
border-radius: 3px;
|
|
94
|
+
background: var(--rk-accent);
|
|
95
|
+
cursor: ew-resize;
|
|
96
|
+
padding: 0;
|
|
97
|
+
}
|
|
98
|
+
.shorts-drawer__rationale { font-size: 12px; line-height: 1.5; margin: 0; }
|
|
99
|
+
.shorts-card__platforms { display: flex; flex-wrap: wrap; gap: var(--s1); margin-top: var(--s1); }
|
|
100
|
+
|
|
101
|
+
.shorts-framing {
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
gap: 12px;
|
|
105
|
+
flex-wrap: wrap;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.shorts-drawer__error {
|
|
109
|
+
margin: 0;
|
|
110
|
+
color: var(--rk-bad);
|
|
111
|
+
font-size: 13px;
|
|
112
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Shorts — docs/design/app-ui.md §3.6.
|
|
3
|
+
import { Badge, Button, Checkbox, EmptyState, Field, PageShell, useToast } from "@rightkit/react-shell/react";
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { errorMessage } from "../lib/engine.js";
|
|
6
|
+
import { framingApproval, framingLabel, settingsGet, reframeShow, shortsAccept, shortsPropose, sourceList, thumbnails, } from "../lib/media.js";
|
|
7
|
+
import { assetUrl } from "../lib/asset.js";
|
|
8
|
+
import { appendClip, projectShow } from "../lib/project.js";
|
|
9
|
+
import { FramingDrawer } from "./AutoEditView.js";
|
|
10
|
+
import { RangePreview } from "../components/RangePreview.js";
|
|
11
|
+
import { Drawer, ErrorPanel, ProgressRow, Thumbnail, timecode, Icon } from "../ui/index.js";
|
|
12
|
+
import { hostAppName } from "../appName.js";
|
|
13
|
+
/** Shorts render vertical: the engine requires an approved plan for this aspect. */
|
|
14
|
+
const SHORT_ASPECT = "9:16";
|
|
15
|
+
/** Mirrors the engine's rule: a source within 5% of 9:16 renders full-frame without a plan. */
|
|
16
|
+
function needsFraming(show, source) {
|
|
17
|
+
const v = show?.timeline.sources.find((t) => t.id === source)?.video;
|
|
18
|
+
if (!v)
|
|
19
|
+
return false;
|
|
20
|
+
return Math.abs(v.width / v.height / (9 / 16) - 1) >= 0.05;
|
|
21
|
+
}
|
|
22
|
+
/** Two-handle boundary trim over a ±5s context window around the proposal. */
|
|
23
|
+
function BoundaryTrim({ proposal, startMs, endMs, onChange, }) {
|
|
24
|
+
const track = useRef(null);
|
|
25
|
+
const lo = Math.max(0, proposal.start_ms - 5000);
|
|
26
|
+
const hi = proposal.end_ms + 5000;
|
|
27
|
+
const span = hi - lo;
|
|
28
|
+
const pct = (ms) => `${(((ms - lo) / span) * 100).toFixed(2)}%`;
|
|
29
|
+
const drag = (which) => (e) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
const el = track.current;
|
|
32
|
+
if (!el)
|
|
33
|
+
return;
|
|
34
|
+
const rect = el.getBoundingClientRect();
|
|
35
|
+
const move = (ev) => {
|
|
36
|
+
const frac = Math.max(0, Math.min(1, (ev.clientX - rect.left) / rect.width));
|
|
37
|
+
const ms = lo + frac * span;
|
|
38
|
+
if (which === "in")
|
|
39
|
+
onChange(Math.min(ms, endMs - 250), endMs);
|
|
40
|
+
else
|
|
41
|
+
onChange(startMs, Math.max(ms, startMs + 250));
|
|
42
|
+
};
|
|
43
|
+
const up = () => {
|
|
44
|
+
window.removeEventListener("pointermove", move);
|
|
45
|
+
window.removeEventListener("pointerup", up);
|
|
46
|
+
};
|
|
47
|
+
window.addEventListener("pointermove", move);
|
|
48
|
+
window.addEventListener("pointerup", up);
|
|
49
|
+
};
|
|
50
|
+
return (_jsxs("div", { className: "shorts-trim", ref: track, children: [_jsx("span", { className: "shorts-trim__time cr-mono", children: timecode(lo) }), _jsxs("div", { className: "shorts-trim__track", children: [_jsx("div", { className: "shorts-trim__sel", style: { left: pct(startMs), right: `calc(100% - ${pct(endMs)})` } }), _jsx("button", { type: "button", className: "shorts-trim__handle", style: { left: pct(startMs) }, "aria-label": "Trim start", onPointerDown: drag("in") }), _jsx("button", { type: "button", className: "shorts-trim__handle", style: { left: pct(endMs) }, "aria-label": "Trim end", onPointerDown: drag("out") })] }), _jsx("span", { className: "shorts-trim__time cr-mono", children: timecode(hi) })] }));
|
|
51
|
+
}
|
|
52
|
+
function ShortThumb({ dir, source, atMs, videoPath }) {
|
|
53
|
+
const [src, setSrc] = useState();
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
let cancelled = false;
|
|
56
|
+
setSrc(undefined);
|
|
57
|
+
thumbnails(dir, { source }, { at_ms: atMs, width: 360 })
|
|
58
|
+
.then((r) => !cancelled && setSrc(r.frames[0] ? assetUrl(r.frames[0].path) : undefined))
|
|
59
|
+
.catch(() => undefined);
|
|
60
|
+
return () => {
|
|
61
|
+
cancelled = true;
|
|
62
|
+
};
|
|
63
|
+
}, [dir, source, atMs]);
|
|
64
|
+
return _jsx(Thumbnail, { src: src, aspect: "9 / 16", video: videoPath ? `${assetUrl(videoPath)}#t=${atMs / 1000}` : undefined });
|
|
65
|
+
}
|
|
66
|
+
/** Engine platform ids → spec labels (R6): Shorts / Reels / TikTok. */
|
|
67
|
+
function platformLabel(id) {
|
|
68
|
+
const s = id.toLowerCase();
|
|
69
|
+
if (s.includes("short"))
|
|
70
|
+
return "Shorts";
|
|
71
|
+
if (s.includes("reel"))
|
|
72
|
+
return "Reels";
|
|
73
|
+
if (s.includes("tiktok"))
|
|
74
|
+
return "TikTok";
|
|
75
|
+
return id.charAt(0).toUpperCase() + id.slice(1);
|
|
76
|
+
}
|
|
77
|
+
export function ShortsView({ dir, onChanged }) {
|
|
78
|
+
const toast = useToast();
|
|
79
|
+
const [sources, setSources] = useState([]);
|
|
80
|
+
const [source, setSource] = useState("");
|
|
81
|
+
const [busy, setBusy] = useState(null);
|
|
82
|
+
// Which operation `busy` (a short id) stands for, so each button shows its own busy text.
|
|
83
|
+
const [busyOp, setBusyOp] = useState(null);
|
|
84
|
+
const [error, setError] = useState(null);
|
|
85
|
+
const [shorts, setShorts] = useState(null);
|
|
86
|
+
const [selected, setSelected] = useState(new Set());
|
|
87
|
+
const [open, setOpen] = useState(null);
|
|
88
|
+
const [trim, setTrim] = useState(null);
|
|
89
|
+
const [searched, setSearched] = useState(false);
|
|
90
|
+
const [show, setShow] = useState(null);
|
|
91
|
+
const [plan, setPlan] = useState(undefined);
|
|
92
|
+
const [autoFraming, setAutoFraming] = useState(false);
|
|
93
|
+
const [framing, setFraming] = useState(false);
|
|
94
|
+
const refresh = useCallback(async () => {
|
|
95
|
+
const [s, p, settings] = await Promise.all([sourceList(dir), projectShow(dir), settingsGet(dir)]);
|
|
96
|
+
setAutoFraming(framingApproval(settings) === "auto");
|
|
97
|
+
const rows = s.sources;
|
|
98
|
+
setSources(rows);
|
|
99
|
+
setShow(p);
|
|
100
|
+
setSource((cur) => cur || rows.find((x) => x.has_transcript)?.id || rows[0]?.id || "");
|
|
101
|
+
}, [dir]);
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
refresh().catch((e) => setError(errorMessage(e)));
|
|
104
|
+
}, [refresh]);
|
|
105
|
+
const selectedSource = sources.find((s) => s.id === source);
|
|
106
|
+
const framingNeeded = needsFraming(show, source);
|
|
107
|
+
// The vertical plan for the chosen source; "none" until one is analysed.
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
setPlan(undefined);
|
|
110
|
+
if (!source || !framingNeeded)
|
|
111
|
+
return;
|
|
112
|
+
let live = true;
|
|
113
|
+
reframeShow(dir, source, SHORT_ASPECT)
|
|
114
|
+
.then((r) => live && setPlan(r))
|
|
115
|
+
.catch(() => live && setPlan("none"));
|
|
116
|
+
return () => {
|
|
117
|
+
live = false;
|
|
118
|
+
};
|
|
119
|
+
}, [dir, source, framingNeeded]);
|
|
120
|
+
const framingApproved = !framingNeeded || (plan !== undefined && plan !== "none" && plan.approved);
|
|
121
|
+
const framingAllowed = autoFraming || framingApproved;
|
|
122
|
+
const framingBlock = framingAllowed ? undefined : "Approve this source's 9:16 framing first";
|
|
123
|
+
const propose = async () => {
|
|
124
|
+
setBusy("propose");
|
|
125
|
+
setError(null);
|
|
126
|
+
try {
|
|
127
|
+
const r = await shortsPropose(dir, source, 6);
|
|
128
|
+
setShorts(r);
|
|
129
|
+
setSelected(new Set());
|
|
130
|
+
setSearched(true);
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
setError(errorMessage(e));
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
setBusy(null);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const exportShort = async (p, bounds) => {
|
|
140
|
+
setBusy(p.id);
|
|
141
|
+
setBusyOp("export");
|
|
142
|
+
setError(null);
|
|
143
|
+
try {
|
|
144
|
+
const trimmed = bounds && (bounds.start !== p.start_ms || bounds.end !== p.end_ms)
|
|
145
|
+
? { start_ms: bounds.start, end_ms: bounds.end }
|
|
146
|
+
: undefined;
|
|
147
|
+
const r = await shortsAccept(dir, source, p.id, trimmed);
|
|
148
|
+
toast(`Short ${r.variant} exported`, { kind: "success" });
|
|
149
|
+
if (framingNeeded)
|
|
150
|
+
setPlan(await reframeShow(dir, source, SHORT_ASPECT));
|
|
151
|
+
await refresh();
|
|
152
|
+
await onChanged();
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
setError(errorMessage(e));
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
setBusy(null);
|
|
161
|
+
setBusyOp(null);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const exportSelected = async () => {
|
|
165
|
+
const list = (shorts?.proposals ?? []).filter((p) => selected.has(p.id));
|
|
166
|
+
for (const p of list) {
|
|
167
|
+
// eslint-disable-next-line no-await-in-loop -- sequential: one render worker
|
|
168
|
+
if (!(await exportShort(p)))
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const addToTimeline = async (p, startMs, endMs) => {
|
|
173
|
+
setBusy(p.id);
|
|
174
|
+
setBusyOp("add");
|
|
175
|
+
setError(null);
|
|
176
|
+
try {
|
|
177
|
+
await appendClip(dir, source, startMs, endMs);
|
|
178
|
+
toast(`Added “${p.title}” to the timeline`, { kind: "success" });
|
|
179
|
+
await onChanged();
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
setError(errorMessage(e));
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
setBusy(null);
|
|
186
|
+
setBusyOp(null);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const openDrawer = (p) => {
|
|
190
|
+
setOpen(p);
|
|
191
|
+
setTrim({ start: p.start_ms, end: p.end_ms });
|
|
192
|
+
};
|
|
193
|
+
return (_jsxs(PageShell, { title: "Shorts", actions: _jsxs(_Fragment, { children: [_jsx(Field, { label: "Source", children: (ids) => (_jsx("select", { ...ids, className: "rk-input cr-select", value: source, onChange: (e) => setSource(e.target.value), children: sources.map((s) => (_jsxs("option", { value: s.id, children: [s.name, s.has_transcript ? "" : " (not transcribed)"] }, s.id))) })) }), _jsxs(Button, { variant: "primary", title: selectedSource?.has_transcript ? undefined : "Transcribe this source first", onClick: () => void propose(), disabled: (!selectedSource?.has_transcript) || (busy === "propose"), "aria-busy": (busy === "propose") || undefined, children: [!(busy === "propose") && _jsx(Icon, { name: "scissors" }), busy === "propose" ? "Finding…" : "Find shorts"] })] }), maxWidth: 0, subtitle: sources.length ? `${sources.length} source${sources.length === 1 ? "" : "s"}` : undefined, children: [_jsxs("div", { className: "shorts-body", children: [busy === "propose" && _jsx(ProgressRow, { label: "Ranking clip candidates", status: "Indexing", fraction: undefined }), error && _jsx(ErrorPanel, { title: "Shorts failed", detail: error }), framingNeeded && (autoFraming || plan !== undefined) && (_jsxs("div", { className: "shorts-framing", children: [_jsx(Badge, { tone: framingAllowed ? "ok" : "warn", children: framingLabel(plan && plan !== "none" ? plan : undefined, autoFraming) }), _jsx("span", { className: "cr-muted", children: autoFraming ? "Shorts render 9:16; saved policy approves tracked framing during export." : "Shorts render 9:16; approve how this source is cropped before exporting." }), _jsx(Button, { variant: "secondary", onClick: () => setFraming(true), children: "Review framing" })] })), searched && shorts && shorts.proposals.length === 0 && (_jsx(EmptyState, { title: "No shorts found", icon: _jsx(Icon, { name: "scissors", width: 28, height: 28 }), children: "No strong clip candidates in this source. Try another clip." })), !searched && !busy && !error && (_jsxs(EmptyState, { title: "Find the best moments", icon: _jsx(Icon, { name: "scissors", width: 28, height: 28 }), children: ["Pick a transcribed source and ", hostAppName(), " ranks clip candidates for vertical platforms."] })), shorts && shorts.proposals.length > 0 && (_jsxs(_Fragment, { children: [shorts.ranking && (_jsxs("div", { className: "shorts-ranking", children: [shorts.ranking.method === "llm" ? (_jsxs(Badge, { tone: "accent", children: ["Ranked by ", shorts.ranking.model ?? "LLM"] })) : (_jsx("span", { title: shorts.ranking.fallback_reason ?? undefined, children: _jsx(Badge, { tone: "warn", children: "Heuristic ranking" }) })), shorts.ranking.method !== "llm" && shorts.ranking.fallback_reason && (_jsxs("span", { className: "cr-muted", children: ["Why: ", shorts.ranking.fallback_reason] })), shorts.ranking.prompt_budget &&
|
|
194
|
+
shorts.ranking.prompt_budget.sentences_sent <
|
|
195
|
+
shorts.ranking.prompt_budget.sentences_offered && (_jsxs("span", { className: "cr-muted", children: ["Prompt budget sent ", shorts.ranking.prompt_budget.sentences_sent, " of", " ", shorts.ranking.prompt_budget.sentences_offered, " sentences"] }))] })), _jsx("div", { className: "shorts-grid", children: shorts.proposals.map((p) => (_jsxs("div", { className: `shorts-card ${selected.has(p.id) ? "shorts-card--sel" : ""}`, children: [_jsxs("button", { type: "button", className: "shorts-card__thumb", onClick: () => openDrawer(p), children: [_jsx(ShortThumb, { dir: dir, source: source, atMs: p.start_ms, videoPath: selectedSource?.path }), _jsxs("span", { className: "shorts-card__rank cr-mono", children: ["#", p.rank] })] }), _jsxs("div", { className: "shorts-card__meta", children: [_jsx("span", { className: "cr-mono cr-muted", children: timecode(p.duration_ms) }), p.scores?.composite != null && (_jsxs(Badge, { tone: "accent", children: ["Score ", Math.round(p.scores.composite * 100)] }))] }), (p.platform_fit ?? []).some((f) => f.fits) && (_jsx("div", { className: "shorts-card__platforms", children: (p.platform_fit ?? [])
|
|
196
|
+
.filter((f) => f.fits)
|
|
197
|
+
.map((f) => (_jsx(Badge, { children: platformLabel(f.platform) }, f.platform))) })), _jsx("div", { className: "shorts-card__hook cr-clamp-2", children: p.title }), _jsx(Checkbox, { className: "shorts-card__pick", label: "Select", checked: selected.has(p.id), onChange: (checked) => setSelected((s) => {
|
|
198
|
+
const n = new Set(s);
|
|
199
|
+
if (checked)
|
|
200
|
+
n.add(p.id);
|
|
201
|
+
else
|
|
202
|
+
n.delete(p.id);
|
|
203
|
+
return n;
|
|
204
|
+
}) })] }, p.id))) })] }))] }), selected.size > 0 && (_jsxs("div", { className: "shorts-bar", children: [_jsxs("span", { children: [selected.size, " selected"] }), _jsxs(Button, { variant: "primary", title: framingBlock, onClick: () => void exportSelected(), disabled: (!framingAllowed) || (busy !== null), "aria-busy": (busy !== null) || undefined, children: [!(busy !== null) && _jsx(Icon, { name: "download" }), busyOp === "export" ? "Exporting…" : "Export shorts"] })] })), _jsx(Drawer, { title: open?.title ?? "Short", open: open !== null && !framing, onClose: () => setOpen(null), footer: open && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "secondary", onClick: () => void addToTimeline(open, trim?.start ?? open.start_ms, trim?.end ?? open.end_ms), disabled: (busy === open.id), "aria-busy": (busy === open.id) || undefined, children: busyOp === "add" && busy === open.id ? "Adding…" : "Add to timeline" }), !framingAllowed && (_jsx(Button, { variant: "secondary", onClick: () => setFraming(true), children: "Review framing" })), _jsx(Button, { variant: "primary", onClick: () => void exportShort(open, trim ? { start: trim.start, end: trim.end } : undefined).then((ok) => ok && setOpen(null)), title: framingBlock ?? "Renders the trimmed range (bounds snap to word edges)", disabled: (!framingAllowed) || (busy === open.id), "aria-busy": (busy === open.id) || undefined, children: busyOp === "export" && busy === open.id ? "Exporting…" : "Export" })] })), children: open && selectedSource && (_jsxs(_Fragment, { children: [_jsx(RangePreview, { loop: true, path: selectedSource.path, startMs: trim?.start ?? open.start_ms, endMs: trim?.end ?? open.end_ms }), _jsx(Field, { label: "Boundaries", note: `${timecode(trim?.start ?? open.start_ms)} – ${timecode(trim?.end ?? open.end_ms)}`, children: () => (_jsx(BoundaryTrim, { proposal: open, startMs: trim?.start ?? open.start_ms, endMs: trim?.end ?? open.end_ms, onChange: (start, end) => setTrim({ start, end }) })) }), _jsx("p", { className: "cr-muted shorts-drawer__rationale", children: open.rationale }), error && (_jsx("p", { role: "alert", className: "shorts-drawer__error", children: error }))] })) }), framing && selectedSource && (_jsx(FramingDrawer, { dir: dir, source: selectedSource, aspect: SHORT_ASPECT, plan: plan, autoFraming: autoFraming, onClose: () => setFraming(false), onChanged: setPlan }))] }));
|
|
205
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** §3.2 Media: source cards, drag-drop import, per-card pipeline state, detail drawer. */
|
|
2
|
+
export declare function SourcesView({ dir, onChanged, onNavigate, }: {
|
|
3
|
+
dir: string;
|
|
4
|
+
onChanged: () => Promise<void>;
|
|
5
|
+
onNavigate?: (to: "recipe" | "finished") => void;
|
|
6
|
+
}): import("react").JSX.Element;
|