@crievo/omni 0.1.3 → 0.1.4
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/package.json +2 -1
- package/reference/learnings.md +12 -0
- package/studio/api/app.ts +2 -2
- package/studio/api/routes/sound.ts +22 -3
- package/studio/api/services/sound.ts +45 -0
- package/studio/studio.ts +1 -0
- package/studio/web/components/AppShell.tsx +3 -1
- package/studio/web/components/CloneVoiceDialog/RecordStep.tsx +3 -0
- package/studio/web/components/CloneVoiceDialog/RecorderBar.tsx +37 -20
- package/studio/web/components/CloneVoiceDialog/useCloneVoice.ts +39 -12
- package/studio/web/components/MissingDmsPanel/BatchSend.tsx +13 -4
- package/studio/web/components/Omni/OmniWidget.tsx +14 -5
- package/studio/web/components/Omni/PendingActions.tsx +25 -6
- package/studio/web/components/OmnichatRulePanel/RuleConfig.tsx +13 -3
- package/studio/web/components/OmnichatRulePanel/RuleEnableConfirm.tsx +14 -4
- package/studio/web/components/ResultMessage.tsx +12 -3
- package/studio/web/components/Sidebar.tsx +63 -24
- package/studio/web/components/StageStepper.tsx +105 -34
- package/studio/web/components/Toaster.tsx +25 -0
- package/studio/web/components/VoiceCarousel.tsx +19 -16
- package/studio/web/components/VoiceProfilesSettings.tsx +8 -3
- package/studio/web/components/bencho/ActionNode.tsx +456 -0
- package/studio/web/components/bencho/AssigneePicker.tsx +246 -0
- package/studio/web/components/bencho/BenchoScope.tsx +8 -0
- package/studio/web/components/bencho/Checklist.tsx +421 -0
- package/studio/web/components/bencho/CreateMenu.tsx +258 -0
- package/studio/web/components/bencho/GenerateImage.tsx +290 -0
- package/studio/web/components/bencho/ImageAccordion.tsx +273 -0
- package/studio/web/components/bencho/LiquidToggle.tsx +289 -0
- package/studio/web/components/bencho/MagnifyDock.tsx +200 -0
- package/studio/web/components/bencho/NowPlaying.tsx +403 -0
- package/studio/web/components/bencho/OneTimeCode.tsx +374 -0
- package/studio/web/components/bencho/PullToRefresh.tsx +527 -0
- package/studio/web/components/bencho/SlideToConfirm.tsx +385 -0
- package/studio/web/components/bencho/SloshSlider.tsx +394 -0
- package/studio/web/components/bencho/StackedBars.tsx +342 -0
- package/studio/web/components/bencho/ToastStack.tsx +263 -0
- package/studio/web/components/bencho/UploadDropzone.tsx +394 -0
- package/studio/web/components/bencho/VoiceNote.tsx +705 -0
- package/studio/web/components/icons.tsx +27 -0
- package/studio/web/components/ui/slider.tsx +34 -1
- package/studio/web/components/ui/switch.tsx +18 -5
- package/studio/web/index.html +1 -1
- package/studio/web/lib/omni-panel.ts +4 -1
- package/studio/web/lib/toast.ts +196 -0
- package/studio/web/lib/upload.ts +24 -0
- package/studio/web/pages/BenchoPage/index.tsx +74 -0
- package/studio/web/pages/BenchoPage/registry.ts +174 -0
- package/studio/web/pages/NewVideoPage/index.tsx +6 -2
- package/studio/web/pages/ReferencesPage/index.tsx +4 -1
- package/studio/web/pages/VideoPage/components/ChangedVoices.tsx +14 -17
- package/studio/web/pages/VideoPage/components/LineTextEditor.tsx +1 -1
- package/studio/web/pages/VideoPage/components/PublishForm.tsx +24 -17
- package/studio/web/pages/VideoPage/components/ScheduledInfo.tsx +2 -1
- package/studio/web/pages/VideoPage/components/SoundTimeline/AddSound.tsx +12 -14
- package/studio/web/pages/VideoPage/components/SoundTimeline/BedRow.tsx +27 -11
- package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionSaveBar.tsx +22 -18
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineFooter.tsx +3 -4
- package/studio/web/pages/VideoPage/components/SoundTimeline/UploadTrack.tsx +59 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundTimeline.ts +1 -0
- package/studio/web/pages/VideoPage/components/VoicePauseBanner.tsx +4 -6
- package/studio/web/pages/VideoPage/index.tsx +4 -0
- package/studio/web/pages/VideosPage/components/VideoRow.tsx +2 -4
- package/studio/web/router.tsx +8 -0
- package/studio/web/styles/components/bencho/action-node.css +227 -0
- package/studio/web/styles/components/bencho/checklist.css +182 -0
- package/studio/web/styles/components/bencho/dock.css +188 -0
- package/studio/web/styles/components/bencho/gallery.css +109 -0
- package/studio/web/styles/components/bencho/generate.css +151 -0
- package/studio/web/styles/components/bencho/image-accordion.css +37 -0
- package/studio/web/styles/components/bencho/liq-create.css +279 -0
- package/studio/web/styles/components/bencho/liq-toggle.css +106 -0
- package/studio/web/styles/components/bencho/one-time-code.css +148 -0
- package/studio/web/styles/components/bencho/picker.css +204 -0
- package/studio/web/styles/components/bencho/pull.css +313 -0
- package/studio/web/styles/components/bencho/scope.css +42 -0
- package/studio/web/styles/components/bencho/slide-confirm.css +139 -0
- package/studio/web/styles/components/bencho/slosh.css +137 -0
- package/studio/web/styles/components/bencho/sound.css +260 -0
- package/studio/web/styles/components/bencho/stacked-bars.css +334 -0
- package/studio/web/styles/components/bencho/toast-stack.css +192 -0
- package/studio/web/styles/components/bencho/tokens.css +243 -0
- package/studio/web/styles/components/bencho/upload-dropzone.css +352 -0
- package/studio/web/styles/components/bencho/voice-note.css +337 -0
- package/studio/web/styles/components/clone-voice.css +12 -2
- package/studio/web/styles/components/omni.css +6 -1
- package/studio/web/styles/components/stepper.css +123 -7
- package/studio/web/styles/components/toaster.css +10 -0
- package/studio/web/styles/components/ui/slider.css +6 -16
- package/studio/web/styles/components/ui/switch.css +2 -41
- package/studio/web/styles/components/voice-carousel.css +46 -4
- package/studio/web/styles/index.css +23 -0
- package/studio/web/styles/pages/video/layout.css +1 -4
- package/studio/web/styles/pages/video/sound-timeline.css +8 -0
- package/studio/web/styles/theme/identity/shell.css +10 -26
- package/tools/sound.mjs +67 -3
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import type { ReactNode } from "react";
|
|
10
10
|
import { Link, useMatchRoute, useNavigate } from "@tanstack/react-router";
|
|
11
11
|
import { useAdmin } from "../hooks/useAdmin";
|
|
12
|
+
import { BenchoScope } from "./bencho/BenchoScope";
|
|
13
|
+
import { CreateMenu, type CreateMenuItem } from "./bencho/CreateMenu";
|
|
12
14
|
import { useRunner } from "../hooks/useRunner";
|
|
13
15
|
import { omniLevel, toggleOmniPanel, useOmniPanel } from "../lib/omni-panel";
|
|
14
16
|
import { OmniGlow } from "./Omni/OmniGlow";
|
|
@@ -22,9 +24,10 @@ import {
|
|
|
22
24
|
Dashboard,
|
|
23
25
|
Gear,
|
|
24
26
|
ArrowExpand,
|
|
27
|
+
GridFour,
|
|
25
28
|
Minus,
|
|
26
29
|
Plug,
|
|
27
|
-
|
|
30
|
+
Play,
|
|
28
31
|
Power,
|
|
29
32
|
ShieldCheck,
|
|
30
33
|
SidebarLeft,
|
|
@@ -152,6 +155,42 @@ function NavLink({
|
|
|
152
155
|
);
|
|
153
156
|
}
|
|
154
157
|
|
|
158
|
+
declare module "@tanstack/history" {
|
|
159
|
+
interface HistoryState {
|
|
160
|
+
/* /new: which deliverables come checked */
|
|
161
|
+
deliverables?: ("video" | "carousel")[];
|
|
162
|
+
/* /styles: the new-style form comes open */
|
|
163
|
+
creating?: boolean;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* The Criar menu: what the studio can start. The video and the carousel are the same form (/new, the carousel with its
|
|
168
|
+
deliverable already checked); a new visual style opens the form at the top of Estilos visuais. The destination reads
|
|
169
|
+
the router's history state, so no route needs a search schema. */
|
|
170
|
+
function useCreateItems(): CreateMenuItem[] {
|
|
171
|
+
const navigate = useNavigate();
|
|
172
|
+
return [
|
|
173
|
+
{
|
|
174
|
+
id: "video",
|
|
175
|
+
label: "Vídeo",
|
|
176
|
+
icon: Play,
|
|
177
|
+
onSelect: () => navigate({ to: "/new", state: { deliverables: ["video"] } }),
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "carousel",
|
|
181
|
+
label: "Carrossel",
|
|
182
|
+
icon: GridFour,
|
|
183
|
+
onSelect: () => navigate({ to: "/new", state: { deliverables: ["carousel"] } }),
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "style",
|
|
187
|
+
label: "Estilo visual",
|
|
188
|
+
icon: Sparkle,
|
|
189
|
+
onSelect: () => navigate({ to: "/styles", state: { creating: true } }),
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
|
|
155
194
|
/* `missing`: the required setup steps still missing (Primeiros passos shows while there are any) */
|
|
156
195
|
export function Sidebar({
|
|
157
196
|
collapsed,
|
|
@@ -165,7 +204,7 @@ export function Sidebar({
|
|
|
165
204
|
showGettingStarted: boolean;
|
|
166
205
|
}) {
|
|
167
206
|
const isActive = useIsActive();
|
|
168
|
-
const
|
|
207
|
+
const createItems = useCreateItems();
|
|
169
208
|
const { hosted } = useRunner();
|
|
170
209
|
const { isAdmin } = useAdmin();
|
|
171
210
|
const omni = useOmniPanel();
|
|
@@ -194,24 +233,19 @@ export function Sidebar({
|
|
|
194
233
|
<span className="menu-group-name">{g.name}</span>
|
|
195
234
|
{g.items.map(item =>
|
|
196
235
|
item.id === "videos" ? (
|
|
197
|
-
/* "Criar
|
|
198
|
-
(a button inside <a> is not valid HTML), and its
|
|
236
|
+
/* "Criar" lives inside the Conteúdos row: the pill sits over the link, as a sibling
|
|
237
|
+
(a button inside <a> is not valid HTML), and its menu hangs from its corner */
|
|
199
238
|
<div key={item.id} className="menu-item-with-action">
|
|
200
239
|
<NavLink item={item} collapsed={collapsed} active={isActive(item)} />
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}}
|
|
211
|
-
>
|
|
212
|
-
<Plus weight="bold" aria-hidden="true" />
|
|
213
|
-
<span>Criar</span>
|
|
214
|
-
</button>
|
|
240
|
+
<BenchoScope>
|
|
241
|
+
<CreateMenu
|
|
242
|
+
className="menu-create"
|
|
243
|
+
label="Criar"
|
|
244
|
+
items={createItems}
|
|
245
|
+
anchor="top-end"
|
|
246
|
+
pillHeight={28}
|
|
247
|
+
/>
|
|
248
|
+
</BenchoScope>
|
|
215
249
|
</div>
|
|
216
250
|
) : (
|
|
217
251
|
<NavLink key={item.id} item={item} collapsed={collapsed} active={isActive(item)} />
|
|
@@ -249,18 +283,23 @@ export function Sidebar({
|
|
|
249
283
|
className={`menu-omni ${omni.ring}${omni.open ? " open" : ""}`}
|
|
250
284
|
onClick={toggleOmniPanel}
|
|
251
285
|
aria-expanded={omni.open}
|
|
252
|
-
aria-label={omni.open ? "Minimizar o Omni" : "Falar com Omni"}
|
|
253
|
-
title={
|
|
286
|
+
aria-label={omni.open ? "Minimizar o Omni" : omni.active ? "Abrir o Omni" : "Falar com Omni"}
|
|
287
|
+
title={
|
|
288
|
+
omni.open ? "Minimizar o Omni" : omni.active ? "Abrir o Omni" : collapsed ? "Falar com Omni" : undefined
|
|
289
|
+
}
|
|
254
290
|
>
|
|
255
291
|
<VoiceAvatar seed={omni.face ?? OMNI_AVATAR} size={40} />
|
|
256
292
|
{/* minimized in a call, the button says what Omni is doing */}
|
|
257
293
|
<span className="menu-label">
|
|
258
294
|
{omni.open ? "Falar com Omni" : (GLOW_LABEL[omni.glow] ?? "Falar com Omni")}
|
|
259
295
|
</span>
|
|
260
|
-
{/* the
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
296
|
+
{/* the first click activates the voice (minimized in the card); only then the icon shows, and the
|
|
297
|
+
next click opens the dock (with it open, the icon says it minimizes) */}
|
|
298
|
+
{(omni.active || omni.open) && (
|
|
299
|
+
<span className="menu-omni-expand" aria-hidden="true">
|
|
300
|
+
{omni.open ? <Minus /> : <ArrowExpand />}
|
|
301
|
+
</span>
|
|
302
|
+
)}
|
|
264
303
|
</button>
|
|
265
304
|
</OmniGlow>
|
|
266
305
|
)}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
/* The pipeline stepper: each stage with its state now. On the overview each
|
|
2
|
-
stage on the video page; on the page, it picks the stage.
|
|
1
|
+
/* The pipeline stepper: each stage as a Bencho action node card with its state now. On the overview each card links
|
|
2
|
+
to the stage on the video page; on the page, it picks the stage. The corner buttons are the actions the studio
|
|
3
|
+
already has for the stage (approve, build the export again, open its process); a stage with none shows none. */
|
|
3
4
|
|
|
5
|
+
import { type CSSProperties, useState } from "react";
|
|
4
6
|
import { Link } from "@tanstack/react-router";
|
|
5
7
|
import { useNow } from "../hooks/useNow";
|
|
8
|
+
import { soundRoutes } from "../hooks/useSound";
|
|
9
|
+
import { api, enc, send, type ActionResult } from "../lib/api";
|
|
6
10
|
import {
|
|
7
11
|
STAGES,
|
|
8
12
|
getStageState,
|
|
@@ -12,7 +16,10 @@ import {
|
|
|
12
16
|
type StageState,
|
|
13
17
|
} from "../lib/stages";
|
|
14
18
|
import type { Queue, Ticket } from "../types/api";
|
|
15
|
-
import {
|
|
19
|
+
import { ActionNode, type NodeAction } from "./bencho/ActionNode";
|
|
20
|
+
import { BenchoScope } from "./bencho/BenchoScope";
|
|
21
|
+
import { ResultMessage } from "./ResultMessage";
|
|
22
|
+
import { ArrowClockwise, Check, SidebarRight, X } from "./icons";
|
|
16
23
|
|
|
17
24
|
function stepMark(s: StageState, stage: StageDef) {
|
|
18
25
|
switch (s.kind) {
|
|
@@ -33,13 +40,15 @@ function stepMark(s: StageState, stage: StageDef) {
|
|
|
33
40
|
}
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
/* `stages`: the pipeline shown (the video's by default; the carousel's in PIPELINES)
|
|
43
|
+
/* `stages`: the pipeline shown (the video's by default; the carousel's in PIPELINES).
|
|
44
|
+
`onProcess`: opens the stage's process (the page's side panel); without it that action is not offered. */
|
|
37
45
|
export function StageStepper({
|
|
38
46
|
video,
|
|
39
47
|
queue,
|
|
40
48
|
runs,
|
|
41
49
|
selected,
|
|
42
50
|
onSelect,
|
|
51
|
+
onProcess,
|
|
43
52
|
compact = false,
|
|
44
53
|
stages = STAGES,
|
|
45
54
|
}: {
|
|
@@ -49,39 +58,101 @@ export function StageStepper({
|
|
|
49
58
|
runs?: Ticket[];
|
|
50
59
|
selected?: string;
|
|
51
60
|
onSelect?: (id: string) => void;
|
|
61
|
+
onProcess?: (id: string) => void;
|
|
52
62
|
compact?: boolean;
|
|
53
63
|
}) {
|
|
54
64
|
const now = useNow();
|
|
65
|
+
const [busy, setBusy] = useState<string | null>(null);
|
|
66
|
+
const [note, setNote] = useState<ActionResult>(null);
|
|
67
|
+
const param = { slug: enc(video.slug) };
|
|
68
|
+
const run = async (key: string, request: () => Promise<NonNullable<ActionResult>>) => {
|
|
69
|
+
setBusy(key);
|
|
70
|
+
setNote(null);
|
|
71
|
+
const r = await request();
|
|
72
|
+
setBusy(null);
|
|
73
|
+
setNote(r);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/* the buttons a stage's card carries: only what the studio already does, in the order of how often it is wanted */
|
|
77
|
+
const actionsOf = (stage: StageDef, s: StageState): NodeAction[] => {
|
|
78
|
+
const list: NodeAction[] = [];
|
|
79
|
+
if (s.kind === "user" && stage.id === "script")
|
|
80
|
+
list.push({
|
|
81
|
+
id: "approve",
|
|
82
|
+
label: "Aprovar o roteiro",
|
|
83
|
+
icon: Check,
|
|
84
|
+
disabled: !!busy,
|
|
85
|
+
onClick: () =>
|
|
86
|
+
run("approve", () => send(api.videos[":slug"].script.decide.$post({ param, json: { decision: "approve" } }))),
|
|
87
|
+
});
|
|
88
|
+
if (s.kind === "user" && stage.id === "motion_script")
|
|
89
|
+
list.push({
|
|
90
|
+
id: "approve",
|
|
91
|
+
label: "Aprovar todas as cenas",
|
|
92
|
+
icon: Check,
|
|
93
|
+
disabled: !!busy,
|
|
94
|
+
onClick: () => run("approve", () => send(api.videos[":slug"].scenes["approve-all"].$post({ param, json: {} }))),
|
|
95
|
+
});
|
|
96
|
+
/* the editor's "Renderizar" is the same call; it is offered here only when the export is out of date */
|
|
97
|
+
if (s.kind === "warning" && stage.id === "render" && video.status?.stages.find(x => x.id === "export")?.outdated)
|
|
98
|
+
list.push({
|
|
99
|
+
id: "reassemble",
|
|
100
|
+
label: "Montar o export de novo",
|
|
101
|
+
icon: ArrowClockwise,
|
|
102
|
+
disabled: !!busy,
|
|
103
|
+
onClick: () => run("reassemble", () => soundRoutes(video.slug).reassemble()),
|
|
104
|
+
});
|
|
105
|
+
if (onProcess && ["running", "queued", "redoing", "error", "warning"].includes(s.kind))
|
|
106
|
+
list.push({
|
|
107
|
+
id: "process",
|
|
108
|
+
label: "Abrir o processamento",
|
|
109
|
+
icon: SidebarRight,
|
|
110
|
+
onClick: () => onProcess(stage.id),
|
|
111
|
+
});
|
|
112
|
+
return list;
|
|
113
|
+
};
|
|
114
|
+
|
|
55
115
|
return (
|
|
56
|
-
|
|
57
|
-
{stages.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
116
|
+
<>
|
|
117
|
+
<ol className={`stepper cards${compact ? " compact" : ""}`} style={{ "--steps": stages.length } as CSSProperties}>
|
|
118
|
+
{stages.map(stage => {
|
|
119
|
+
const s = getStageState(stage, video, queue, runs);
|
|
120
|
+
const hint = `${stage.num} · ${stage.title}${s.kind === "warning" ? `: ${s.reason}` : ""}`;
|
|
121
|
+
return (
|
|
122
|
+
<li key={stage.id} className={`step ${s.kind}${selected === stage.id ? " chosen" : ""}`}>
|
|
123
|
+
<BenchoScope>
|
|
124
|
+
<ActionNode
|
|
125
|
+
title={stage.name}
|
|
126
|
+
description={stageStateDetail(s, now) || "Ainda não feita"}
|
|
127
|
+
hint={hint}
|
|
128
|
+
mark={<span className="step-mark">{stepMark(s, stage)}</span>}
|
|
129
|
+
actions={actionsOf(stage, s)}
|
|
130
|
+
people={[]}
|
|
131
|
+
selected={selected === stage.id}
|
|
132
|
+
width="100%"
|
|
133
|
+
dense
|
|
134
|
+
corner={compact ? 12 : 14}
|
|
135
|
+
air={0}
|
|
136
|
+
fan="down"
|
|
137
|
+
render={props =>
|
|
138
|
+
onSelect ? (
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
{...props}
|
|
142
|
+
onClick={() => onSelect(stage.id)}
|
|
143
|
+
aria-current={selected === stage.id ? "step" : undefined}
|
|
144
|
+
/>
|
|
145
|
+
) : (
|
|
146
|
+
<Link to="/videos/$slug/$stage" params={{ slug: video.slug, stage: stage.id }} {...props} />
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
/>
|
|
150
|
+
</BenchoScope>
|
|
151
|
+
</li>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</ol>
|
|
155
|
+
<ResultMessage result={note} />
|
|
156
|
+
</>
|
|
86
157
|
);
|
|
87
158
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* The studio's toasts (lib/toast.ts), drawn once in the app shell: Bencho's stack in the bottom-right corner. */
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { toast, useToasts } from "../lib/toast";
|
|
5
|
+
import { BenchoScope } from "./bencho/BenchoScope";
|
|
6
|
+
import { ToastStack } from "./bencho/ToastStack";
|
|
7
|
+
|
|
8
|
+
export function Toaster() {
|
|
9
|
+
const toasts = useToasts();
|
|
10
|
+
/* a stack open when the shell goes away must not leave every timer held */
|
|
11
|
+
useEffect(() => () => toast.resume(), []);
|
|
12
|
+
return (
|
|
13
|
+
<BenchoScope>
|
|
14
|
+
<div className="toaster">
|
|
15
|
+
<ToastStack
|
|
16
|
+
toasts={toasts}
|
|
17
|
+
onDismiss={toast.dismiss}
|
|
18
|
+
onOpenChange={open => (open ? toast.pause() : toast.resume())}
|
|
19
|
+
align="end"
|
|
20
|
+
floating
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
</BenchoScope>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/* The carousel (Configurações → Voz). The center voice is the selected one; the neighbors are
|
|
2
|
-
smaller and lighter. From there it becomes the active voice, or opens to edit. The
|
|
3
|
-
|
|
2
|
+
smaller and lighter. From there it becomes the active voice, or opens to edit. The add ball is
|
|
3
|
+
not a carousel step: it stays in the slot before the center (centered while there is no voice),
|
|
4
|
+
and the voices that go past the center slide under it. */
|
|
4
5
|
|
|
5
|
-
import { useEffect, useRef, useState } from "react";
|
|
6
|
+
import { type CSSProperties, useEffect, useRef, useState } from "react";
|
|
6
7
|
import { useAvatarAudioReactive } from "../hooks/useAvatarAudioReactive";
|
|
7
8
|
import { useVoiceProfilePreview } from "../hooks/useVoiceProfilePreview";
|
|
8
9
|
import type { ActionResult } from "../lib/api";
|
|
@@ -26,7 +27,7 @@ export function VoiceCarousel({
|
|
|
26
27
|
onEdit: (id: string) => void;
|
|
27
28
|
onAdd: () => void;
|
|
28
29
|
}) {
|
|
29
|
-
const total = profiles.length;
|
|
30
|
+
const total = profiles.length;
|
|
30
31
|
const last = Math.max(0, total - 1);
|
|
31
32
|
const initial = Math.max(
|
|
32
33
|
0,
|
|
@@ -97,7 +98,8 @@ export function VoiceCarousel({
|
|
|
97
98
|
<button
|
|
98
99
|
key={p.id}
|
|
99
100
|
type="button"
|
|
100
|
-
className={`carousel-item d${Math.min(Math.abs(i - index), 3)}${p.id === defaultId ? " active" : ""}`}
|
|
101
|
+
className={`carousel-item d${Math.min(Math.abs(i - index), 3)}${p.id === defaultId ? " active" : ""}${i < index - 1 ? " under" : ""}`}
|
|
102
|
+
style={{ "--shift": `calc(${Math.max(0, index - 1 - i)} * var(--slot))` } as CSSProperties}
|
|
101
103
|
onClick={() => go(i)}
|
|
102
104
|
aria-label={p.name}
|
|
103
105
|
aria-current={i === index}
|
|
@@ -106,18 +108,19 @@ export function VoiceCarousel({
|
|
|
106
108
|
<VoiceAvatar seed={p.avatar || p.id} size={160} />
|
|
107
109
|
</button>
|
|
108
110
|
))}
|
|
109
|
-
<button
|
|
110
|
-
type="button"
|
|
111
|
-
className={`carousel-item more d${Math.min(Math.abs(total - index), 3)}`}
|
|
112
|
-
onClick={onAdd}
|
|
113
|
-
aria-label="Adicionar voz"
|
|
114
|
-
title="Adicionar voz"
|
|
115
|
-
>
|
|
116
|
-
<span className="avatar-more large">
|
|
117
|
-
<Plus />
|
|
118
|
-
</span>
|
|
119
|
-
</button>
|
|
120
111
|
</div>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
className={`carousel-add ${total ? "d1" : "d0"}${index > 0 ? " covering" : ""}`}
|
|
115
|
+
style={{ "--add-x": total ? -1.5 : -0.5 } as CSSProperties}
|
|
116
|
+
onClick={onAdd}
|
|
117
|
+
aria-label="Adicionar voz"
|
|
118
|
+
title="Adicionar voz"
|
|
119
|
+
>
|
|
120
|
+
<span className="avatar-more large">
|
|
121
|
+
<Plus />
|
|
122
|
+
</span>
|
|
123
|
+
</button>
|
|
121
124
|
</div>
|
|
122
125
|
{profile && (
|
|
123
126
|
<div className="carousel-actions">
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { useEffect, useState, type ReactNode } from "react";
|
|
15
15
|
import { api, send } from "../lib/api";
|
|
16
|
+
import { toastResult } from "../lib/toast";
|
|
16
17
|
import type { Settings } from "../types/api";
|
|
17
18
|
import { CloneVoiceDialog } from "./CloneVoiceDialog";
|
|
18
19
|
import { EditVoiceDialog } from "./EditVoiceDialog";
|
|
@@ -36,8 +37,12 @@ export function VoiceProfilesSettings({
|
|
|
36
37
|
/* the id of the voice being redone: opens the clone modal with its recording attached */
|
|
37
38
|
const [redoing, setRedoing] = useState<string | null>(null);
|
|
38
39
|
const [focus, setFocus] = useState<string | null>(null);
|
|
39
|
-
/* what the removal did on each provider; stays under the carousel
|
|
40
|
+
/* what the removal did on each provider: a toast when it is one short line; a longer text stays under the carousel
|
|
41
|
+
until the next action */
|
|
40
42
|
const [message, setMessage] = useState<string | null>(null);
|
|
43
|
+
const report = (output: string) => {
|
|
44
|
+
if (!toastResult({ ok: true, output })) setMessage(output);
|
|
45
|
+
};
|
|
41
46
|
/* the clone creates the voice by itself: when the new profile shows up, it comes to the center.
|
|
42
47
|
If some provider failed, the voice opens in edit with the warning, to create again only there */
|
|
43
48
|
const [cloned, setCloned] = useState<{ before: string[]; warnings: string[] } | null>(null);
|
|
@@ -108,7 +113,7 @@ export function VoiceProfilesSettings({
|
|
|
108
113
|
onRedone={output => {
|
|
109
114
|
setRedoing(null);
|
|
110
115
|
setFocus(profile.id);
|
|
111
|
-
|
|
116
|
+
report(output);
|
|
112
117
|
reload();
|
|
113
118
|
}}
|
|
114
119
|
/>
|
|
@@ -140,7 +145,7 @@ export function VoiceProfilesSettings({
|
|
|
140
145
|
}}
|
|
141
146
|
onRemove={output => {
|
|
142
147
|
setEditing(null);
|
|
143
|
-
|
|
148
|
+
report(output);
|
|
144
149
|
}}
|
|
145
150
|
/>
|
|
146
151
|
);
|