@crievo/omni 0.1.3 → 0.1.5
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/omnichat.mjs +19 -6
- package/tools/sound.mjs +67 -3
|
@@ -114,3 +114,26 @@
|
|
|
114
114
|
@import "./pages/agent.css";
|
|
115
115
|
@import "./components/omni.css";
|
|
116
116
|
@import "./components/omni-cards.css";
|
|
117
|
+
|
|
118
|
+
/* Bencho ports (the /bencho gallery): its own palette, the page, then one file per component */
|
|
119
|
+
@import "./components/bencho/tokens.css";
|
|
120
|
+
@import "./components/bencho/scope.css";
|
|
121
|
+
@import "./components/bencho/gallery.css";
|
|
122
|
+
@import "./components/bencho/pull.css";
|
|
123
|
+
@import "./components/bencho/liq-toggle.css";
|
|
124
|
+
@import "./components/bencho/checklist.css";
|
|
125
|
+
@import "./components/bencho/sound.css";
|
|
126
|
+
@import "./components/bencho/dock.css";
|
|
127
|
+
@import "./components/bencho/slosh.css";
|
|
128
|
+
@import "./components/bencho/liq-create.css";
|
|
129
|
+
@import "./components/bencho/picker.css";
|
|
130
|
+
@import "./components/bencho/slide-confirm.css";
|
|
131
|
+
@import "./components/bencho/image-accordion.css";
|
|
132
|
+
@import "./components/bencho/action-node.css";
|
|
133
|
+
@import "./components/bencho/generate.css";
|
|
134
|
+
@import "./components/bencho/one-time-code.css";
|
|
135
|
+
@import "./components/bencho/upload-dropzone.css";
|
|
136
|
+
@import "./components/bencho/voice-note.css";
|
|
137
|
+
@import "./components/bencho/toast-stack.css";
|
|
138
|
+
@import "./components/toaster.css";
|
|
139
|
+
@import "./components/bencho/stacked-bars.css";
|
|
@@ -41,10 +41,7 @@
|
|
|
41
41
|
padding: 4px 24px;
|
|
42
42
|
background: none;
|
|
43
43
|
border-bottom: 1px solid var(--border);
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
.rail .stepper {
|
|
47
|
-
min-width: 900px;
|
|
44
|
+
/* no scroll box: the stage cards' fans hang below the rail and a scroller would clip them; the cards wrap instead */
|
|
48
45
|
}
|
|
49
46
|
/* sentinel at the rail's natural position (takes no space): when it leaves through the top, the rail
|
|
50
47
|
is stuck and gets the panel background */
|
|
@@ -244,6 +244,9 @@
|
|
|
244
244
|
position: absolute;
|
|
245
245
|
left: 6px;
|
|
246
246
|
top: 4px;
|
|
247
|
+
display: flex;
|
|
248
|
+
align-items: center;
|
|
249
|
+
gap: 6px;
|
|
247
250
|
}
|
|
248
251
|
.sound-header {
|
|
249
252
|
position: absolute;
|
|
@@ -670,3 +673,8 @@
|
|
|
670
673
|
border-top-color: transparent;
|
|
671
674
|
animation: spin 0.8s linear infinite;
|
|
672
675
|
}
|
|
676
|
+
.sound-add-buttons {
|
|
677
|
+
display: flex;
|
|
678
|
+
flex-wrap: wrap;
|
|
679
|
+
gap: 6px;
|
|
680
|
+
}
|
|
@@ -89,44 +89,28 @@ main.with-sidebar {
|
|
|
89
89
|
background: var(--el-nav-active-bg);
|
|
90
90
|
color: var(--el-nav-active-text);
|
|
91
91
|
}
|
|
92
|
-
/* the "Criar
|
|
92
|
+
/* the "Criar" pill (components/bencho/CreateMenu) inside the Conteúdos row: it sits over the link at the right edge, as a
|
|
93
|
+
sibling (a button inside <a> is not valid HTML), and its menu hangs from the pill's top right corner. Out of flow,
|
|
94
|
+
centred on the row by margin rather than a transform: a transformed box would be a stacking context of its own. */
|
|
93
95
|
.menu .menu-item-with-action {
|
|
94
96
|
position: relative;
|
|
95
97
|
display: flex;
|
|
96
98
|
flex-direction: column;
|
|
97
99
|
}
|
|
98
100
|
.menu .menu-item-with-action > a {
|
|
99
|
-
padding-right:
|
|
101
|
+
padding-right: 100px;
|
|
100
102
|
}
|
|
101
|
-
.menu .menu-
|
|
103
|
+
.menu .menu-create {
|
|
102
104
|
position: absolute;
|
|
103
105
|
top: 50%;
|
|
104
|
-
right:
|
|
105
|
-
|
|
106
|
-
display: inline-flex;
|
|
107
|
-
align-items: center;
|
|
108
|
-
gap: 4px;
|
|
109
|
-
height: 24px;
|
|
110
|
-
padding: 0 8px 0 6px;
|
|
111
|
-
font: 500 12px / 1 var(--sans);
|
|
112
|
-
border: 0;
|
|
113
|
-
border-radius: var(--el-radius-nav);
|
|
114
|
-
background: transparent;
|
|
115
|
-
color: var(--text-2);
|
|
116
|
-
cursor: pointer;
|
|
117
|
-
}
|
|
118
|
-
.menu .menu-action svg {
|
|
119
|
-
width: 12px;
|
|
120
|
-
height: 12px;
|
|
121
|
-
}
|
|
122
|
-
.menu .menu-action:hover,
|
|
123
|
-
.menu .menu-action:focus-visible {
|
|
124
|
-
background: var(--surface-2);
|
|
125
|
-
color: var(--text);
|
|
106
|
+
right: 3px;
|
|
107
|
+
margin-top: calc(var(--pill-h) / -2);
|
|
126
108
|
}
|
|
127
109
|
@media (max-width: 900px) {
|
|
128
110
|
.menu {
|
|
129
111
|
width: auto;
|
|
112
|
+
/* the bar has no fixed height to scroll in, and the create menu hangs below it */
|
|
113
|
+
overflow: visible;
|
|
130
114
|
}
|
|
131
115
|
.brand {
|
|
132
116
|
margin-bottom: 0;
|
|
@@ -213,7 +197,7 @@ main.with-sidebar {
|
|
|
213
197
|
}
|
|
214
198
|
.menu-collapsed .brand-omni,
|
|
215
199
|
.menu-collapsed .menu-label,
|
|
216
|
-
.menu-collapsed .menu-
|
|
200
|
+
.menu-collapsed .menu-create {
|
|
217
201
|
display: none;
|
|
218
202
|
}
|
|
219
203
|
.menu-collapsed .menu-group-name {
|
package/tools/omnichat.mjs
CHANGED
|
@@ -1033,10 +1033,16 @@ async function status() {
|
|
|
1033
1033
|
const pause = readJson(PAUSE_FILE);
|
|
1034
1034
|
if (isPaused(pause, Date.now())) s.pause = pause;
|
|
1035
1035
|
if (cloudOwns()) {
|
|
1036
|
-
|
|
1037
|
-
|
|
1036
|
+
/* the cloud answers: its pause and its token are the ones that count; its posts' updated_at say
|
|
1037
|
+
which records changed since the mirror, and only those are pulled */
|
|
1038
1038
|
try {
|
|
1039
1039
|
const c = await cloudApi("GET", "/api/status");
|
|
1040
|
+
const at = new Map((c.posts ?? []).map(p => [String(p.media_id), p.updated_at]));
|
|
1041
|
+
await Promise.all(
|
|
1042
|
+
videosWithRule()
|
|
1043
|
+
.filter(d => at.has(String(mediaOf(d))))
|
|
1044
|
+
.map(d => pullRecord(d, at.get(String(mediaOf(d)))).catch(() => {})),
|
|
1045
|
+
);
|
|
1040
1046
|
s.cloud = { ready: true, active: !!c.active, expires: c.account?.expires ?? null };
|
|
1041
1047
|
if (!c.active) s.cloud.reason = "a cloud não está respondendo (cloud push)";
|
|
1042
1048
|
if (c.account?.paused_until && Date.parse(c.account.paused_until) > Date.now())
|
|
@@ -1167,19 +1173,26 @@ function syncRule(dir) {
|
|
|
1167
1173
|
}
|
|
1168
1174
|
const flushCloud = () => Promise.all(pendingCloud.splice(0));
|
|
1169
1175
|
|
|
1170
|
-
/*
|
|
1171
|
-
|
|
1176
|
+
/* The cloud's record mirrored into omnichat-record.json: the studio and the log read the file.
|
|
1177
|
+
`cloudAt`: the post's updated_at from /api/status; when the mirror already has that version, nothing is
|
|
1178
|
+
asked (the studio's status runs every few minutes, and each record that leaves Neon counts against the
|
|
1179
|
+
free plan's monthly transfer). */
|
|
1180
|
+
async function pullRecord(dir, cloudAt = null) {
|
|
1172
1181
|
if (!cloudOwns()) return;
|
|
1173
1182
|
const media = mediaOf(dir);
|
|
1174
1183
|
if (!media) return;
|
|
1184
|
+
const file = recordFile(dir);
|
|
1185
|
+
if (cloudAt && readJson(file)?.cloud_updated_at === cloudAt) return;
|
|
1175
1186
|
try {
|
|
1176
1187
|
const out = await cloudApi("GET", `/api/posts/${media}`);
|
|
1177
|
-
const file = recordFile(dir);
|
|
1178
1188
|
const all = readJson(file) ?? {};
|
|
1179
1189
|
/* only a change is written: the write is a studio notice, and the notice makes the page read (and
|
|
1180
1190
|
pull) again, so an unchanged record written anyway loops against the cloud */
|
|
1181
|
-
|
|
1191
|
+
const at = out.updated_at ?? null;
|
|
1192
|
+
if (JSON.stringify(all.instagram ?? null) === JSON.stringify(out.record ?? null) && all.cloud_updated_at === at)
|
|
1193
|
+
return;
|
|
1182
1194
|
all.instagram = out.record;
|
|
1195
|
+
all.cloud_updated_at = at;
|
|
1183
1196
|
delete all.cloud;
|
|
1184
1197
|
writeJson(file, all);
|
|
1185
1198
|
} catch (e) {
|
package/tools/sound.mjs
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
node tools/sound.mjs kit [role|text] lists the global kit by role
|
|
9
9
|
node tools/sound.mjs kit adopt videos/<slug> <name> --role <role> [--as <name-in-kit>]
|
|
10
10
|
moves an effect captured for the video into the kit
|
|
11
|
+
node tools/sound.mjs import videos/<slug> <file> [name] [--title <text>]
|
|
12
|
+
a track the user sent (the editor's upload), as an MP3 of the video's sfx/
|
|
11
13
|
node tools/sound.mjs effects videos/<slug> [text] lists the effects captured for the video
|
|
12
14
|
node tools/sound.mjs videos/<slug> prepares the video's sound/
|
|
13
15
|
|
|
@@ -16,7 +18,9 @@
|
|
|
16
18
|
by every video, each effect with a role (click, pop, tick, typing, confirm,
|
|
17
19
|
whoosh, transition, riser, impact, glitch, bed, texture); and the video's
|
|
18
20
|
sfx/ (sfx/sources.json), for what the kit lacks. No other audio saved on the
|
|
19
|
-
computer gets in
|
|
21
|
+
computer gets in, except a track the user sends on purpose through the
|
|
22
|
+
editor's upload (`import`): it goes to the video's sfx/ as an MP3 with
|
|
23
|
+
source "upload", only for that video, never into the kit. With a search open in Chrome (sound.source.search in the
|
|
20
24
|
config), the `capture` code plays the previews and returns each one's URL
|
|
21
25
|
(audiocdn.epidemicsound.com/lqmp3/<id>.mp3); `download` writes the MP3 to
|
|
22
26
|
the kit or the video's sfx/ and records the origin. An effect outside both
|
|
@@ -79,7 +83,7 @@
|
|
|
79
83
|
queue turn (tools/job-queue.mjs).
|
|
80
84
|
============================================================================ */
|
|
81
85
|
|
|
82
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
86
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, statSync } from "node:fs";
|
|
83
87
|
import { spawnSync } from "node:child_process";
|
|
84
88
|
import { join, resolve, basename, relative } from "node:path";
|
|
85
89
|
import { fileURLToPath } from "node:url";
|
|
@@ -406,6 +410,62 @@ async function download(videoDir, url, name, search, role) {
|
|
|
406
410
|
);
|
|
407
411
|
}
|
|
408
412
|
|
|
413
|
+
/* A track the user sent from the editor (the upload): any audio file becomes an MP3 in the video's sfx/,
|
|
414
|
+
registered with the origin "upload". It is the user's own file, so it is theirs to license; it stays in
|
|
415
|
+
this video (adopt refuses it) and never reaches the kit. */
|
|
416
|
+
function importUpload(videoDir, file, wanted, title) {
|
|
417
|
+
const dir = resolve(videoDir ?? "");
|
|
418
|
+
if (!existsSync(join(dir, "scenes.json"))) throw new Error(`${dir} não é a pasta de um vídeo (falta scenes.json)`);
|
|
419
|
+
if (!file || !existsSync(file)) throw new Error(`o arquivo “${file}” não existe`);
|
|
420
|
+
const base = (wanted || basename(file)).replace(/\.[A-Za-z0-9]{1,5}$/, "");
|
|
421
|
+
const slug =
|
|
422
|
+
base
|
|
423
|
+
.normalize("NFD")
|
|
424
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
425
|
+
.toLowerCase()
|
|
426
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
427
|
+
.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, "") || "musica";
|
|
428
|
+
const taken = n =>
|
|
429
|
+
!!sources(dir)[`${n}.mp3`] || !!kitSources()[`${n}.mp3`] || existsSync(join(effectsDir(dir), `${n}.mp3`));
|
|
430
|
+
let n = slug;
|
|
431
|
+
for (let i = 2; taken(n); i++) n = `${slug}-${i}`;
|
|
432
|
+
const name = `${n}.mp3`;
|
|
433
|
+
mkdirSync(effectsDir(dir), { recursive: true });
|
|
434
|
+
const out = join(effectsDir(dir), name);
|
|
435
|
+
/* an MP3 is kept as it is (without cover art or tags); anything else is encoded */
|
|
436
|
+
const isMp3 = /\.mp3$/i.test(file);
|
|
437
|
+
try {
|
|
438
|
+
run("ffmpeg", [
|
|
439
|
+
"-y",
|
|
440
|
+
"-v",
|
|
441
|
+
"error",
|
|
442
|
+
"-i",
|
|
443
|
+
file,
|
|
444
|
+
"-vn",
|
|
445
|
+
"-map_metadata",
|
|
446
|
+
"-1",
|
|
447
|
+
...(isMp3 ? ["-c:a", "copy"] : ["-c:a", "libmp3lame", "-q:a", "2"]),
|
|
448
|
+
out,
|
|
449
|
+
]);
|
|
450
|
+
const med = measureEffect(out);
|
|
451
|
+
if (!(med.duration > 0)) throw new Error("o arquivo não tem áudio");
|
|
452
|
+
} catch (e) {
|
|
453
|
+
rmSync(out, { force: true });
|
|
454
|
+
throw new Error(`não consegui ler “${basename(file)}” como áudio: ${String(e.message).slice(0, 200)}`);
|
|
455
|
+
}
|
|
456
|
+
cache.delete(out);
|
|
457
|
+
const list = sources(dir);
|
|
458
|
+
list[name] = {
|
|
459
|
+
source: "upload",
|
|
460
|
+
title: title || basename(file).replace(/\.[A-Za-z0-9]{1,5}$/, ""),
|
|
461
|
+
uploaded_at: new Date().toISOString().slice(0, 10),
|
|
462
|
+
};
|
|
463
|
+
writeFileSync(join(effectsDir(dir), "sources.json"), JSON.stringify(list, null, 1) + "\n");
|
|
464
|
+
const m = measureEffect(out);
|
|
465
|
+
console.log(`${name} ← ${list[name].title} · enviado pelo usuário`);
|
|
466
|
+
console.log(m.music ? ` ${m.duration.toFixed(0)}s · trilha (só música de fundo)` : ` ${describe(m)}`);
|
|
467
|
+
}
|
|
468
|
+
|
|
409
469
|
const describe = m =>
|
|
410
470
|
`${m.duration.toFixed(2)}s · pico em ${m.peak_s}s (${m.peak_db} dB) · útil até ${m.useful_end_s}s · volume ${m.volume_lufs} LUFS`;
|
|
411
471
|
|
|
@@ -437,6 +497,8 @@ function adopt(videoDir, name, role, as) {
|
|
|
437
497
|
if (!ROLES[role]) throw new Error(`--role é obrigatório: ${Object.keys(ROLES).join(", ")}`);
|
|
438
498
|
const source = sources(dir)[name];
|
|
439
499
|
if (!source) throw new Error(`“${name}” não está em ${join(effectsDir(dir), "sources.json")}`);
|
|
500
|
+
if (source.source === "upload")
|
|
501
|
+
throw new Error(`“${name}” foi enviado pelo usuário para este vídeo: o kit só recebe áudio da Epidemic`);
|
|
440
502
|
const newName = `${(as ?? name).replace(/\.mp3$/i, "")}.mp3`;
|
|
441
503
|
if (!NAME.test(newName)) throw new Error(`nome “${newName}”: letras, números, ponto, hífen e sublinhado`);
|
|
442
504
|
const list = kitSources();
|
|
@@ -1295,9 +1357,11 @@ if (isMainModule(import.meta.url)) {
|
|
|
1295
1357
|
try {
|
|
1296
1358
|
const option = n => (i => (i < 0 ? undefined : rest.splice(i, 2)[1]))(rest.indexOf(`--${n}`));
|
|
1297
1359
|
const search = option("search"),
|
|
1298
|
-
role = option("role")
|
|
1360
|
+
role = option("role"),
|
|
1361
|
+
title = option("title");
|
|
1299
1362
|
if (cmd === "capture") capture(+(rest[0] ?? 8));
|
|
1300
1363
|
else if (cmd === "download") await download(rest[0], rest[1], rest[2], search, role);
|
|
1364
|
+
else if (cmd === "import") importUpload(rest[0], rest[1], rest[2], title);
|
|
1301
1365
|
else if (cmd === "effects") effects(rest[0], rest[1]);
|
|
1302
1366
|
else if (cmd === "kit" && rest[0] === "adopt") adopt(rest[1], rest[2], role, option("as") ?? rest[3]);
|
|
1303
1367
|
else if (cmd === "kit") kit(rest[0]);
|