@odori/cli 0.0.4 → 0.0.6
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/bin/odori.mjs +33 -11
- package/dist/{chunk-NYXWEZU2.js → chunk-6CE7U5KB.js} +124 -39
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1 -1
- package/dist/{registry-snapshot-MSH2EA36.js → registry-snapshot-ADKSTGDR.js} +15 -14
- package/package.json +3 -3
- package/src/cli.ts +26 -4
- package/src/commands/add.ts +12 -1
- package/src/commands/dev.ts +4 -1
- package/src/commands/exportVideo.ts +6 -0
- package/src/commands/init.ts +34 -1
- package/src/commands/test.ts +24 -2
- package/src/commands/update.ts +58 -7
- package/src/jobs.ts +1 -1
- package/src/registry-snapshot.json +15 -15
- package/src/render.ts +8 -1
- package/studio/index.html +26 -0
- package/studio/src/Studio.tsx +6 -22
- package/studio/src/components/CanvasStage.tsx +40 -3
- package/studio/src/components/ExportPanel.tsx +90 -6
- package/studio/src/components/Inspector.tsx +36 -30
- package/studio/src/components/Navigator.tsx +64 -2
- package/studio/src/components/Settings.tsx +109 -0
- package/studio/src/components/Transport.tsx +136 -58
- package/studio/src/components/ui.tsx +20 -1
- package/studio/src/settings.ts +87 -0
- package/studio/src/studio.css +269 -20
- package/studio/src/views/VideosView.tsx +12 -5
package/studio/src/studio.css
CHANGED
|
@@ -201,9 +201,21 @@ a.button {
|
|
|
201
201
|
border-color: var(--border);
|
|
202
202
|
color: var(--foreground);
|
|
203
203
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
/*
|
|
205
|
+
* Hover says a control is live. Selected says what the setting is. They have
|
|
206
|
+
* to differ in kind, not in degree.
|
|
207
|
+
*
|
|
208
|
+
* Hovering an unselected option used to darken its border and fill it, which
|
|
209
|
+
* is what being chosen looks like, so passing the cursor over 2x read as
|
|
210
|
+
* having clicked 2x. And this rule outranked the selected one - a pseudo-class
|
|
211
|
+
* beats an attribute - so hovering the option that *was* chosen replaced its
|
|
212
|
+
* fill with this one and the choice appeared to move under the cursor.
|
|
213
|
+
*
|
|
214
|
+
* Now hover only lifts the surface a step, the border never moves, and
|
|
215
|
+
* selected always wins.
|
|
216
|
+
*/
|
|
217
|
+
.button[data-variant="outline"]:hover:not([data-active="true"]) {
|
|
218
|
+
background: var(--hover);
|
|
207
219
|
}
|
|
208
220
|
.button[data-variant="primary"] {
|
|
209
221
|
background: var(--foreground);
|
|
@@ -217,6 +229,25 @@ a.button {
|
|
|
217
229
|
background: var(--active);
|
|
218
230
|
color: var(--foreground);
|
|
219
231
|
}
|
|
232
|
+
/*
|
|
233
|
+
* A chosen option in a group: filled and outlined, which is how the source
|
|
234
|
+
* tabs and the catalog filter already say the same thing.
|
|
235
|
+
*
|
|
236
|
+
* Fill alone was not enough of a difference. Hover is #f2f2f2 and chosen is
|
|
237
|
+
* #ebebeb, seven levels apart on a #fafafa panel, so the cursor moving across
|
|
238
|
+
* 0.5x / 1x / 2x looked like the choice following it. The border is the part
|
|
239
|
+
* hover never touches, so it is the part that carries the state.
|
|
240
|
+
*
|
|
241
|
+
* Both listed, and last, so the chosen option keeps looking chosen under the
|
|
242
|
+
* cursor: a pseudo-class outranks an attribute, and the plain hover rule was
|
|
243
|
+
* winning and taking the fill away.
|
|
244
|
+
*/
|
|
245
|
+
.button[data-variant="outline"][data-active="true"],
|
|
246
|
+
.button[data-variant="outline"][data-active="true"]:hover {
|
|
247
|
+
background: var(--active);
|
|
248
|
+
border-color: var(--border-strong);
|
|
249
|
+
color: var(--foreground);
|
|
250
|
+
}
|
|
220
251
|
.button:disabled {
|
|
221
252
|
cursor: not-allowed;
|
|
222
253
|
opacity: 0.45;
|
|
@@ -306,6 +337,66 @@ a.button {
|
|
|
306
337
|
font-size: 11px;
|
|
307
338
|
}
|
|
308
339
|
|
|
340
|
+
/* ---------- settings ---------- */
|
|
341
|
+
|
|
342
|
+
.settings {
|
|
343
|
+
position: relative;
|
|
344
|
+
}
|
|
345
|
+
/* Right-aligned: the trigger is the last thing in the header, so a menu
|
|
346
|
+
growing rightward from its left edge would leave the window. */
|
|
347
|
+
.settings-menu {
|
|
348
|
+
left: auto;
|
|
349
|
+
right: 0;
|
|
350
|
+
}
|
|
351
|
+
.menu-heading {
|
|
352
|
+
color: var(--subtle);
|
|
353
|
+
font-size: 11px;
|
|
354
|
+
letter-spacing: 0.04em;
|
|
355
|
+
margin: 6px 0 2px;
|
|
356
|
+
padding: 0 9px;
|
|
357
|
+
text-transform: uppercase;
|
|
358
|
+
}
|
|
359
|
+
.menu-heading:first-child {
|
|
360
|
+
margin-top: 2px;
|
|
361
|
+
}
|
|
362
|
+
.menu-hint {
|
|
363
|
+
color: var(--subtle);
|
|
364
|
+
font-size: 11px;
|
|
365
|
+
}
|
|
366
|
+
/* A tick in a reserved column, so choosing an option never reflows the row
|
|
367
|
+
or nudges the one under the cursor. */
|
|
368
|
+
.menu-item {
|
|
369
|
+
display: grid;
|
|
370
|
+
grid-template-columns: 1fr 14px;
|
|
371
|
+
}
|
|
372
|
+
.menu-item > span:first-child {
|
|
373
|
+
grid-column: 1;
|
|
374
|
+
}
|
|
375
|
+
.menu-hint {
|
|
376
|
+
grid-column: 1;
|
|
377
|
+
}
|
|
378
|
+
.menu-item[data-selected="true"]::after {
|
|
379
|
+
align-self: center;
|
|
380
|
+
color: var(--foreground);
|
|
381
|
+
content: "✓";
|
|
382
|
+
font-size: 11px;
|
|
383
|
+
grid-column: 2;
|
|
384
|
+
grid-row: 1 / span 2;
|
|
385
|
+
justify-self: end;
|
|
386
|
+
}
|
|
387
|
+
/* A stalled bar stops looking like a working one. */
|
|
388
|
+
.progress[data-stalled="true"] > span {
|
|
389
|
+
opacity: 0.5;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.menu-note {
|
|
393
|
+
border-top: 1px solid var(--border);
|
|
394
|
+
color: var(--subtle);
|
|
395
|
+
font-size: 11px;
|
|
396
|
+
margin: 4px 0 0;
|
|
397
|
+
padding: 7px 9px 3px;
|
|
398
|
+
}
|
|
399
|
+
|
|
309
400
|
/* Matches the transport's icon buttons rather than the host select. */
|
|
310
401
|
.rate {
|
|
311
402
|
appearance: none;
|
|
@@ -503,12 +594,77 @@ select.input {
|
|
|
503
594
|
without a rule per combination of what is open. The inspector's width is
|
|
504
595
|
a root variable so its drag handle can resize it without React re-laying
|
|
505
596
|
anything out. */
|
|
506
|
-
|
|
597
|
+
/*
|
|
598
|
+
* The side columns are stated rather than measured from their contents.
|
|
599
|
+
*
|
|
600
|
+
* A track that is `auto` collapses because the pane inside it left the DOM,
|
|
601
|
+
* and that is a used-value change, which no transition fires on: the stage
|
|
602
|
+
* jumped to its new width a frame after the click. Stating both widths, and
|
|
603
|
+
* letting the attributes the panes already set drive them, makes the track
|
|
604
|
+
* list itself change, which is a computed value and animatable. The
|
|
605
|
+
* inspector's is the same variable its drag handle writes.
|
|
606
|
+
*/
|
|
607
|
+
grid-template-columns:
|
|
608
|
+
var(--navigator-column, 232px)
|
|
609
|
+
minmax(0, 1fr)
|
|
610
|
+
clamp(240px, var(--inspector-width, 320px), 50vw);
|
|
507
611
|
overflow: hidden;
|
|
508
612
|
}
|
|
613
|
+
html[data-navigator="collapsed"] .main {
|
|
614
|
+
--navigator-column: 0px;
|
|
615
|
+
}
|
|
616
|
+
/* Zero, not `auto`: the pane is gone and the column has to say so in a length
|
|
617
|
+
the track list can interpolate towards. */
|
|
618
|
+
html[data-inspector="collapsed"] .main {
|
|
619
|
+
--inspector-width: 0px;
|
|
620
|
+
}
|
|
621
|
+
html[data-inspector="collapsed"] .main:not([data-single="true"]) {
|
|
622
|
+
grid-template-columns: var(--navigator-column, 232px) minmax(0, 1fr) 0px;
|
|
623
|
+
}
|
|
509
624
|
.main[data-single="true"] {
|
|
510
625
|
grid-template-columns: 1fr;
|
|
511
626
|
}
|
|
627
|
+
/*
|
|
628
|
+
* Each pane owns its column by number rather than by arriving in order.
|
|
629
|
+
*
|
|
630
|
+
* A pane that is put away renders nothing but its reopen button, and that
|
|
631
|
+
* button is fixed, so it is not a grid item. Auto-placement then slid every
|
|
632
|
+
* remaining pane one column to the left: closing the list handed the stage the
|
|
633
|
+
* sidebar's auto column, which sizes to content rather than to the space, and
|
|
634
|
+
* pushed the inspector into the middle column and its 273px out through the
|
|
635
|
+
* right edge of the grid. Naming the column makes where a pane sits
|
|
636
|
+
* independent of how many panes happen to exist, so any of them can be put
|
|
637
|
+
* away without moving the others.
|
|
638
|
+
*/
|
|
639
|
+
.main:not([data-single="true"]) > .navigator {
|
|
640
|
+
grid-column: 1;
|
|
641
|
+
}
|
|
642
|
+
.main:not([data-single="true"]) > .stage {
|
|
643
|
+
grid-column: 2;
|
|
644
|
+
}
|
|
645
|
+
.main:not([data-single="true"]) > .inspector {
|
|
646
|
+
grid-column: 3;
|
|
647
|
+
}
|
|
648
|
+
/*
|
|
649
|
+
* Putting a pane away is a move, so it should read as one.
|
|
650
|
+
*
|
|
651
|
+
* The columns are what actually change, so they are what animates: the stage
|
|
652
|
+
* grows into the space as the list leaves rather than snapping to a new width
|
|
653
|
+
* a frame later. Only the track list is transitioned, not `all`, because the
|
|
654
|
+
* inspector's drag handle writes its width continuously and easing that would
|
|
655
|
+
* make the pane lag the cursor.
|
|
656
|
+
*/
|
|
657
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
658
|
+
.main:not([data-single="true"]) {
|
|
659
|
+
transition: grid-template-columns 220ms cubic-bezier(0.32, 0.72, 0, 1);
|
|
660
|
+
}
|
|
661
|
+
/* Except while a divider is being dragged, when the column has to track the
|
|
662
|
+
pointer exactly. A pane follows its column instantly and the column would
|
|
663
|
+
not, so the two would part company and show the page between them. */
|
|
664
|
+
html[data-resizing] .main {
|
|
665
|
+
transition: none;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
512
668
|
|
|
513
669
|
/* ---------- navigator ---------- */
|
|
514
670
|
|
|
@@ -518,14 +674,39 @@ select.input {
|
|
|
518
674
|
display: flex;
|
|
519
675
|
flex-direction: column;
|
|
520
676
|
overflow: hidden;
|
|
521
|
-
width
|
|
677
|
+
/* The column carries the width now, so the pane fills whatever it is given
|
|
678
|
+
and the drag has one number to move rather than two to keep in step. */
|
|
679
|
+
min-width: 0;
|
|
680
|
+
position: relative;
|
|
681
|
+
width: 100%;
|
|
682
|
+
}
|
|
683
|
+
.navigator-resize {
|
|
684
|
+
cursor: col-resize;
|
|
685
|
+
/* Fully inside the pane, like the inspector's: the pane clips overflow, and
|
|
686
|
+
a handle hung across the border loses the clipped half of its hit area. */
|
|
687
|
+
inset: 0 0 0 auto;
|
|
688
|
+
position: absolute;
|
|
689
|
+
touch-action: none;
|
|
690
|
+
width: 8px;
|
|
691
|
+
z-index: 2;
|
|
522
692
|
}
|
|
523
|
-
|
|
693
|
+
/*
|
|
694
|
+
* Both pane headers, from one rule, because they are the same thing on either
|
|
695
|
+
* side of the workspace and describing them separately is how they drifted:
|
|
696
|
+
* one sat at a 10px gutter and the other at 16px, so the two sides of the top
|
|
697
|
+
* of the window did not line up with each other.
|
|
698
|
+
*
|
|
699
|
+
* 49px is 28px of control with ten above and below and the rule underneath.
|
|
700
|
+
*/
|
|
701
|
+
.navigator-head,
|
|
702
|
+
.inspector-head {
|
|
524
703
|
align-items: center;
|
|
525
704
|
border-bottom: 1px solid var(--border);
|
|
526
705
|
display: flex;
|
|
527
|
-
|
|
528
|
-
|
|
706
|
+
flex: none;
|
|
707
|
+
gap: 8px;
|
|
708
|
+
min-height: 49px;
|
|
709
|
+
padding: 10px 16px;
|
|
529
710
|
}
|
|
530
711
|
.navigator-search {
|
|
531
712
|
height: 28px;
|
|
@@ -534,7 +715,9 @@ select.input {
|
|
|
534
715
|
}
|
|
535
716
|
.navigator-scroll {
|
|
536
717
|
overflow-y: auto;
|
|
537
|
-
|
|
718
|
+
/* The header's gutter, so a row lines up with the title above it and with
|
|
719
|
+
the inspector's content across the window. */
|
|
720
|
+
padding: 16px;
|
|
538
721
|
scrollbar-gutter: stable;
|
|
539
722
|
}
|
|
540
723
|
.navigator-group {
|
|
@@ -570,11 +753,12 @@ select.input {
|
|
|
570
753
|
background: var(--hover);
|
|
571
754
|
color: var(--foreground);
|
|
572
755
|
}
|
|
573
|
-
/* Away, the way back
|
|
756
|
+
/* Away, the way back sits exactly where the way out was: the header's own
|
|
757
|
+
16px gutter, and its top edge plus 10px of padding. */
|
|
574
758
|
.navigator-reopen {
|
|
575
759
|
background: var(--panel);
|
|
576
760
|
border-color: var(--border);
|
|
577
|
-
left:
|
|
761
|
+
left: 16px;
|
|
578
762
|
position: fixed;
|
|
579
763
|
top: 58px;
|
|
580
764
|
z-index: 5;
|
|
@@ -586,6 +770,8 @@ select.input {
|
|
|
586
770
|
width: clamp(240px, var(--inspector-width, 320px), 50vw);
|
|
587
771
|
container-type: inline-size;
|
|
588
772
|
display: flex;
|
|
773
|
+
/* A header above a scroller, which is what the navigator is. */
|
|
774
|
+
flex-direction: column;
|
|
589
775
|
overflow: hidden;
|
|
590
776
|
position: relative;
|
|
591
777
|
}
|
|
@@ -601,12 +787,19 @@ select.input {
|
|
|
601
787
|
}
|
|
602
788
|
|
|
603
789
|
|
|
790
|
+
/*
|
|
791
|
+
* The pane's header, matched to the navigator's across the workspace.
|
|
792
|
+
*
|
|
793
|
+
* It used to be the first thing inside the scroller, so the title and the tabs
|
|
794
|
+
* slid away as you read and there was no line to say where the pane stopped
|
|
795
|
+
* and its contents started. Out here it stays put and carries the same rule
|
|
796
|
+
* the list's header does. Ten pixels top and bottom is what makes the two the
|
|
797
|
+
* same height, since both hold a 28px control; sixteen at the sides is what
|
|
798
|
+
* puts the title over the content underneath it rather than six pixels to its
|
|
799
|
+
* left.
|
|
800
|
+
*/
|
|
604
801
|
.inspector-head {
|
|
605
|
-
align-items: center;
|
|
606
|
-
display: flex;
|
|
607
|
-
gap: 8px;
|
|
608
802
|
justify-content: space-between;
|
|
609
|
-
margin-bottom: 14px;
|
|
610
803
|
}
|
|
611
804
|
.inspector-title {
|
|
612
805
|
color: var(--foreground);
|
|
@@ -738,6 +931,8 @@ select.input {
|
|
|
738
931
|
white-space: pre;
|
|
739
932
|
}
|
|
740
933
|
|
|
934
|
+
/* 28px, the same as the list's pair: two controls doing the same job either
|
|
935
|
+
side of the workspace should not be different sizes. */
|
|
741
936
|
.inspector-toggle,
|
|
742
937
|
.inspector-reopen {
|
|
743
938
|
align-items: center;
|
|
@@ -748,21 +943,28 @@ select.input {
|
|
|
748
943
|
cursor: pointer;
|
|
749
944
|
display: inline-flex;
|
|
750
945
|
flex: none;
|
|
751
|
-
height:
|
|
946
|
+
height: 28px;
|
|
752
947
|
justify-content: center;
|
|
753
|
-
width:
|
|
948
|
+
width: 28px;
|
|
754
949
|
}
|
|
755
950
|
.inspector-toggle:hover,
|
|
756
951
|
.inspector-reopen:hover {
|
|
757
952
|
background: var(--hover);
|
|
758
953
|
color: var(--foreground);
|
|
759
954
|
}
|
|
760
|
-
/*
|
|
955
|
+
/*
|
|
956
|
+
* Away, the way back sits exactly where the way out was.
|
|
957
|
+
*
|
|
958
|
+
* These are the same control in two states, so it must not jump between them.
|
|
959
|
+
* 16px is the header's own side padding, which is where the toggle sits while
|
|
960
|
+
* the pane is open; it was 10px, and the button hopped six pixels right on
|
|
961
|
+
* every close. 58px is the header's top edge plus its 10px of padding.
|
|
962
|
+
*/
|
|
761
963
|
.inspector-reopen {
|
|
762
964
|
background: var(--panel);
|
|
763
965
|
border-color: var(--border);
|
|
764
966
|
position: fixed;
|
|
765
|
-
right:
|
|
967
|
+
right: 16px;
|
|
766
968
|
top: 58px;
|
|
767
969
|
z-index: 5;
|
|
768
970
|
}
|
|
@@ -951,6 +1153,13 @@ select.input {
|
|
|
951
1153
|
|
|
952
1154
|
.stage {
|
|
953
1155
|
display: grid;
|
|
1156
|
+
/* The column has to be named. An implicit grid column is auto, which means
|
|
1157
|
+
max-content, so the transport's row of controls set the width and the
|
|
1158
|
+
timeline was laid out at its natural size and then clipped: in a pane this
|
|
1159
|
+
narrow the right half of every video sat under the inspector, unclickable,
|
|
1160
|
+
and the playhead walked off the edge on its way there. min-width on .stage
|
|
1161
|
+
does not help, because the overflow is the track inside it. */
|
|
1162
|
+
grid-template-columns: minmax(0, 1fr);
|
|
954
1163
|
grid-template-rows: minmax(0, 1fr) auto;
|
|
955
1164
|
min-width: 0;
|
|
956
1165
|
overflow: hidden;
|
|
@@ -993,13 +1202,19 @@ select.input {
|
|
|
993
1202
|
border-top: 1px solid var(--border);
|
|
994
1203
|
display: grid;
|
|
995
1204
|
gap: 10px;
|
|
1205
|
+
min-width: 0;
|
|
996
1206
|
padding: 10px 14px 14px;
|
|
997
1207
|
}
|
|
998
1208
|
|
|
999
1209
|
.transport-row {
|
|
1000
1210
|
align-items: center;
|
|
1001
1211
|
display: flex;
|
|
1212
|
+
/* Wrapping is what lets the transport be narrower than its controls. Without
|
|
1213
|
+
it the row is a single min-content block and it is the row, not the
|
|
1214
|
+
timeline, that decides how wide the stage has to be. */
|
|
1215
|
+
flex-wrap: wrap;
|
|
1002
1216
|
gap: 8px;
|
|
1217
|
+
min-width: 0;
|
|
1003
1218
|
}
|
|
1004
1219
|
|
|
1005
1220
|
.timecode {
|
|
@@ -1014,8 +1229,13 @@ select.input {
|
|
|
1014
1229
|
}
|
|
1015
1230
|
|
|
1016
1231
|
.timeline {
|
|
1017
|
-
|
|
1232
|
+
/* Only the track is clickable, so only the track shows a pointer. */
|
|
1018
1233
|
display: grid;
|
|
1234
|
+
/* Named for the same reason .stage is: an implicit auto column would take
|
|
1235
|
+
its width from the ruler and the scene labels rather than from the space
|
|
1236
|
+
available, and push the track out from under the cursor. */
|
|
1237
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1238
|
+
min-width: 0;
|
|
1019
1239
|
gap: 4px;
|
|
1020
1240
|
position: relative;
|
|
1021
1241
|
user-select: none;
|
|
@@ -1029,6 +1249,8 @@ select.input {
|
|
|
1029
1249
|
justify-content: space-between;
|
|
1030
1250
|
}
|
|
1031
1251
|
.track {
|
|
1252
|
+
/* The track is a click target, not something to drag. */
|
|
1253
|
+
cursor: pointer;
|
|
1032
1254
|
background: var(--field);
|
|
1033
1255
|
border: 1px solid var(--border);
|
|
1034
1256
|
border-radius: var(--radius-sm);
|
|
@@ -1069,6 +1291,18 @@ select.input {
|
|
|
1069
1291
|
height: 20px;
|
|
1070
1292
|
position: relative;
|
|
1071
1293
|
}
|
|
1294
|
+
/* Empty until the runtime has reported, then labelled. Same height either
|
|
1295
|
+
way, so nothing above it moves. */
|
|
1296
|
+
.audio-track[data-empty="true"] {
|
|
1297
|
+
align-items: center;
|
|
1298
|
+
border: 1px dashed var(--border);
|
|
1299
|
+
border-radius: var(--radius-sm);
|
|
1300
|
+
box-sizing: border-box;
|
|
1301
|
+
color: var(--subtle);
|
|
1302
|
+
display: flex;
|
|
1303
|
+
font-size: 11px;
|
|
1304
|
+
padding: 0 8px;
|
|
1305
|
+
}
|
|
1072
1306
|
.audio-track .cue {
|
|
1073
1307
|
align-items: center;
|
|
1074
1308
|
background: var(--hover);
|
|
@@ -1140,6 +1374,21 @@ select.input {
|
|
|
1140
1374
|
width: 7.5px;
|
|
1141
1375
|
}
|
|
1142
1376
|
|
|
1377
|
+
/* The frame under the cursor, which is a suggestion until it is clicked. It
|
|
1378
|
+
reads as the same instrument as the playhead, turned down. */
|
|
1379
|
+
.playhead[data-preview="true"],
|
|
1380
|
+
.playhead[data-preview="true"]::before {
|
|
1381
|
+
background: var(--subtle);
|
|
1382
|
+
}
|
|
1383
|
+
.playhead[data-preview="true"] {
|
|
1384
|
+
opacity: 0.75;
|
|
1385
|
+
}
|
|
1386
|
+
/* Muted rather than moved: the readout must not change width when it starts
|
|
1387
|
+
reporting the hovered frame, or the whole row shifts under the cursor. */
|
|
1388
|
+
.timecode[data-preview="true"] {
|
|
1389
|
+
color: var(--subtle);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1143
1392
|
/* ---------- inspector ---------- */
|
|
1144
1393
|
|
|
1145
1394
|
.facts {
|
|
@@ -23,6 +23,7 @@ import {Inspector} from "../components/Inspector";
|
|
|
23
23
|
import {Navigator} from "../components/Navigator";
|
|
24
24
|
import {Badge, Empty, Fact, SectionTitle, Separator} from "../components/ui";
|
|
25
25
|
import {measureTrack} from "../lib/mix-loudness";
|
|
26
|
+
import {readStartSound} from "../settings";
|
|
26
27
|
import {useShortcuts} from "../shortcuts";
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -51,9 +52,15 @@ export const VideosView = ({
|
|
|
51
52
|
const [timeline, setTimeline] = useState<CompiledTimeline | null>(null);
|
|
52
53
|
const [track, setTrack] = useState<AudioTrack | null>(null);
|
|
53
54
|
const [loop, setLoop] = useState(true);
|
|
54
|
-
|
|
55
|
+
/* Read once per mount rather than watched: flipping the default while a
|
|
56
|
+
video is open would mute the thing being listened to, and a default is a
|
|
57
|
+
statement about what happens next. */
|
|
58
|
+
const [muted, setMuted] = useState(() => readStartSound() === "off");
|
|
55
59
|
const [soloCue, setSoloCue] = useState<string | null>(null);
|
|
56
|
-
|
|
60
|
+
/* The frame the cursor is over, which the stage shows instead of the
|
|
61
|
+
playhead frame. Preview never moves the playhead and never sounds: it
|
|
62
|
+
answers "what is here" and nothing else. */
|
|
63
|
+
const [preview, setPreview] = useState<number | null>(null);
|
|
57
64
|
const [rate, setRate] = useState(1);
|
|
58
65
|
const [loudness, setLoudness] = useState<number | null>(null);
|
|
59
66
|
const [audioBlocked, setAudioBlocked] = useState(false);
|
|
@@ -108,7 +115,7 @@ export const VideosView = ({
|
|
|
108
115
|
playing: playback.playing,
|
|
109
116
|
muted,
|
|
110
117
|
soloCue,
|
|
111
|
-
scrubbing,
|
|
118
|
+
scrubbing: preview !== null,
|
|
112
119
|
rate,
|
|
113
120
|
onBlocked: setAudioBlocked,
|
|
114
121
|
onFailed: setAudioFailed,
|
|
@@ -238,7 +245,7 @@ export const VideosView = ({
|
|
|
238
245
|
<PreviewBoundary resetKey={selected.metadata.id}>
|
|
239
246
|
<OdoriRuntime
|
|
240
247
|
entry={selected}
|
|
241
|
-
frame={playback.frame}
|
|
248
|
+
frame={preview ?? playback.frame}
|
|
242
249
|
input={input}
|
|
243
250
|
assets={project.assets}
|
|
244
251
|
onTimeline={handleTimeline}
|
|
@@ -258,7 +265,7 @@ export const VideosView = ({
|
|
|
258
265
|
audioBlocked={audioBlocked}
|
|
259
266
|
onEnableAudio={() => setMuted(false)}
|
|
260
267
|
onSoloCue={setSoloCue}
|
|
261
|
-
|
|
268
|
+
onPreview={setPreview}
|
|
262
269
|
rate={rate}
|
|
263
270
|
onRate={setRate}
|
|
264
271
|
onToggleLoop={() => setLoop((value) => !value)}
|