rvlib-shipkit 2.7.0 → 2.9.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 +12 -0
- package/package.json +11 -3
- package/src/assets/gen/comfyts-gen.ts +56 -25
- package/src/assets/kits/identity-kit.ts +23 -4
- package/src/checks/SK005-rvlib-versions.check.ts +17 -11
- package/src/cli/cmd_Fleet.ts +8 -5
- package/src/core/update-policy.ts +6 -0
- package/src/fleet/commit-bump.ts +123 -0
- package/src/fleet/push-fleet.ts +9 -3
- package/src/fleet/update-fleet.ts +38 -9
- package/src/serve/hub-app/auto-text.tsx +52 -0
- package/src/serve/hub-app/hub.css +2 -0
- package/src/serve/hub-app/media-view.tsx +3 -11
- package/src/serve/hub-app/review/review-answer.tsx +8 -7
- package/src/serve/hub-app/review/review-page.tsx +28 -12
- package/src/serve/hub-app/review/review-st.ts +39 -8
- package/src/serve/hub-app/review/review-views.tsx +104 -35
- package/src/serve/hub-app/review/trailer-page.tsx +5 -4
- package/src/serve/hub-app/review.css +20 -2
- package/src/trailer/sound/comfy.ts +1 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// the review page's middle column: every view of the open card stacked, each under its heading with the owner's reaction and
|
|
2
2
|
// its zoom; a view is an image, a clip, a pair (side by side, the curtain, the onion skin, the after alone) or a
|
|
3
|
-
// gallery of tiles. A drag on an image draws a rectangle, on a clip it pauses and keeps the frame's time
|
|
4
|
-
// views that each play alone, show as
|
|
3
|
+
// gallery of tiles. A drag on an image draws a rectangle, on a clip it pauses and keeps the frame's time; the picked
|
|
4
|
+
// rectangle's note box opens beside it. A choice, or views that each play alone, show as option rows, each image and
|
|
5
|
+
// clip shown full under its row; the card's header sits on top when the page reads in one column
|
|
5
6
|
import { observer } from 'mobx-react-lite'
|
|
6
7
|
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
7
8
|
import { isGallery, isPair, onFrame, STARS, thumbFileOf, verdictOf, viewName, type GalleryView, type MediaView, type Rect, type ReviewClip, type ReviewItem, type ReviewMark, type ReviewView, type Side } from '../../../review/model.ts'
|
|
9
|
+
import { AutoText } from '../auto-text.tsx'
|
|
8
10
|
import { blurOnEscape, Empty, FIT_ROOM, keepFocus, PAIR_NAMES, REACTIONS, secs } from './kit.tsx'
|
|
9
11
|
import { CardHead } from './review-answer.tsx'
|
|
10
12
|
import type { ClipSt } from './clip-st.ts'
|
|
@@ -20,11 +22,13 @@ export const ReviewImage = observer(({ rv }: { rv: ReviewSt }) => {
|
|
|
20
22
|
const views = r.views
|
|
21
23
|
const drawable = views.some((v) => !isGallery(v) && !v.clip && !v.audio)
|
|
22
24
|
const rows = rv.rows
|
|
25
|
+
// a draft card's history is no view of its own: its frames cycle in place over the live one
|
|
26
|
+
const history = r.wip ? views.find(isGallery) : undefined
|
|
23
27
|
return (
|
|
24
28
|
<div className="rv-views">
|
|
25
29
|
{rv.headInViews ? <CardHead rv={rv} x={x} /> : null}
|
|
26
30
|
<div className="rv-viewsbar">
|
|
27
|
-
{views.length > 1 && !rows ? <JumpList rv={rv} /> : <span />}
|
|
31
|
+
{views.length > 1 && !rows && !history ? <JumpList rv={rv} /> : <span />}
|
|
28
32
|
<span className="rv-viewsctl">
|
|
29
33
|
{asOptionsCard(rv) ? (
|
|
30
34
|
<span className="seg" title="the options as thin rows, one below the other, or every view full">
|
|
@@ -49,7 +53,9 @@ export const ReviewImage = observer(({ rv }: { rv: ReviewSt }) => {
|
|
|
49
53
|
{rows ? (
|
|
50
54
|
<div className="rv-opts">{views.map((v, i) => <OptionRow key={`${r.id}#${i}`} rv={rv} x={x} v={v} i={i} />)}</div>
|
|
51
55
|
) : (
|
|
52
|
-
views.map((v, i) =>
|
|
56
|
+
views.map((v, i) => (v === history ? null : (
|
|
57
|
+
<ViewBlock key={`${r.id}#${i}`} rv={rv} id={r.id} v={v} i={i} many={views.length > (history ? 2 : 1)} {...(i === 0 && history && isGallery(history) ? { frames: history } : {})} />
|
|
58
|
+
)))
|
|
53
59
|
)}
|
|
54
60
|
{views.length === 0 ? <TextOnly x={x} /> : null}
|
|
55
61
|
</div>
|
|
@@ -59,8 +65,9 @@ export const ReviewImage = observer(({ rv }: { rv: ReviewSt }) => {
|
|
|
59
65
|
/** the open card may show as rows: a choice, or views that each play alone (the switch shows while it shows full) */
|
|
60
66
|
const asOptionsCard = (rv: ReviewSt): boolean => rv.rows || rv.full[rv.open?.item.id ?? ''] === true
|
|
61
67
|
|
|
62
|
-
/** one option of a choice, or one view of a group, as a
|
|
63
|
-
* scrubs it, its stars and « pick » (a choice) or OK, fix, no (a group), ✎ his word, ⤢ the full view
|
|
68
|
+
/** one option of a choice, or one view of a group, as a row: its number, ▶ and the clock, its label, a strip that
|
|
69
|
+
* scrubs it, its stars and « pick » (a choice) or OK, fix, no (a group), ✎ his word, ⤢ folds or unfolds the full view
|
|
70
|
+
* under it (an image or a clip shows it from the start) */
|
|
64
71
|
const OptionRow = observer(({ rv, x, v, i }: { rv: ReviewSt; x: ReviewItem; v: ReviewView; i: number }) => {
|
|
65
72
|
const [word, setWord] = useState(false)
|
|
66
73
|
const id = x.item.id
|
|
@@ -71,7 +78,7 @@ const OptionRow = observer(({ rv, x, v, i }: { rv: ReviewSt; x: ReviewItem; v: R
|
|
|
71
78
|
const choice = rv.choice
|
|
72
79
|
const picked = rv.draft.pick === i
|
|
73
80
|
const sent = verdictOf(x)?.pick === i
|
|
74
|
-
const unfolded = rv.
|
|
81
|
+
const unfolded = rv.rowOpen(i)
|
|
75
82
|
const file = playable ? ('video' in playable.files ? playable.files.video : 'audio' in playable.files ? playable.files.audio : '') : ''
|
|
76
83
|
const duration = playable?.clip?.duration ?? playable?.audio?.duration ?? 0
|
|
77
84
|
const showWord = word || (reaction?.comment ?? '') !== ''
|
|
@@ -92,7 +99,7 @@ const OptionRow = observer(({ rv, x, v, i }: { rv: ReviewSt; x: ReviewItem; v: R
|
|
|
92
99
|
) : null}
|
|
93
100
|
{c && playable ? (
|
|
94
101
|
<Strip clip={c} duration={duration} wave={playable.audio ? rv.fileUrl(id, playable.audio.wave) : undefined} />
|
|
95
|
-
) : (
|
|
102
|
+
) : unfolded ? null : (
|
|
96
103
|
<span className="rv-optthumb img" onClick={() => rv.toggleRow(i)} title="the full view"><img src={rv.thumbUrl(x, thumbFileOf(v))} alt="" draggable={false} /></span>
|
|
97
104
|
)}
|
|
98
105
|
{c ? <span className="rv-mono rv-opttime">{clock(c.time)} / {clock(duration)}</span> : null}
|
|
@@ -112,8 +119,8 @@ const OptionRow = observer(({ rv, x, v, i }: { rv: ReviewSt; x: ReviewItem; v: R
|
|
|
112
119
|
</div>
|
|
113
120
|
{media?.note ? <div className="rv-optnote">{media.note}</div> : null}
|
|
114
121
|
{showWord ? (
|
|
115
|
-
<
|
|
116
|
-
onKeyDown={blurOnEscape}
|
|
122
|
+
<AutoText className="rv-word" placeholder="a word on this one" value={reaction?.comment ?? ''} autoFocus={word && !reaction?.comment}
|
|
123
|
+
onKeyDown={blurOnEscape} onText={(t) => rv.setReactionComment(i, undefined, t)} />
|
|
117
124
|
) : null}
|
|
118
125
|
{c && playable?.audio ? <SoundEl clip={c} src={rv.fileUrl(id, file)} /> : null}
|
|
119
126
|
{unfolded ? (
|
|
@@ -223,8 +230,11 @@ const JumpList = observer(({ rv }: { rv: ReviewSt }) => {
|
|
|
223
230
|
)
|
|
224
231
|
})
|
|
225
232
|
|
|
226
|
-
/** one view: its label, its reaction and zoom, its image, pair, clip or gallery, then his word on it
|
|
227
|
-
|
|
233
|
+
/** one view: its label, its reaction and zoom, its image, pair, clip or gallery, then his word on it; a draft card's
|
|
234
|
+
* live view carries its history's frames, one shown in its place */
|
|
235
|
+
const ViewBlock = observer(({ rv, id, v: live, i, many, frames }: { rv: ReviewSt; id: string; v: ReviewView; i: number; many: boolean; frames?: GalleryView }) => {
|
|
236
|
+
const past = frames && rv.frame !== null ? frames.gallery[rv.frame] : undefined
|
|
237
|
+
const v: ReviewView = past && !isGallery(live) ? { ...live, files: { image: past.file }, size: past.size } : live
|
|
228
238
|
const reaction = rv.reactionOf(i)
|
|
229
239
|
const media = isGallery(v) ? null : v
|
|
230
240
|
const zoom = rv.zoomOf(i)
|
|
@@ -243,9 +253,25 @@ const ViewBlock = observer(({ rv, id, v, i, many }: { rv: ReviewSt; id: string;
|
|
|
243
253
|
onClick={() => rv.setViewZoom(i, zoom === 'fit' ? '1' : 'fit')}>{zoom === 'fit' ? '100 %' : 'Fit'}</button>
|
|
244
254
|
) : null}
|
|
245
255
|
</div>
|
|
256
|
+
{frames ? <FrameSlider rv={rv} frames={frames} /> : null}
|
|
246
257
|
{isGallery(v) ? <Gallery rv={rv} id={id} v={v} view={i} /> : <MediaBody rv={rv} id={id} v={v} i={i} zoom={zoom} />}
|
|
247
|
-
<
|
|
248
|
-
onKeyDown={blurOnEscape}
|
|
258
|
+
<AutoText className="rv-word" placeholder={isGallery(v) ? 'a word on these images' : 'a word on this image'} value={reaction?.comment ?? ''}
|
|
259
|
+
onKeyDown={blurOnEscape} onText={(t) => rv.setReactionComment(i, undefined, t)} />
|
|
260
|
+
</div>
|
|
261
|
+
)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
/** a draft card's frames over its live view: a slider and two arrows cycle them in place, the live one at the right end */
|
|
265
|
+
const FrameSlider = observer(({ rv, frames }: { rv: ReviewSt; frames: GalleryView }) => {
|
|
266
|
+
const last = frames.gallery.length
|
|
267
|
+
const at = rv.frame ?? last
|
|
268
|
+
const go = (f: number): void => rv.setFrame(f >= last ? null : Math.max(0, f))
|
|
269
|
+
return (
|
|
270
|
+
<div className="rv-frames">
|
|
271
|
+
<button title="the frame before" disabled={at === 0} onMouseDown={keepFocus} onClick={() => go(at - 1)}>‹</button>
|
|
272
|
+
<input type="range" min={0} max={last} step={1} value={at} aria-label="the draft's frames" onChange={(e) => go(Number(e.currentTarget.value))} onMouseUp={(e) => e.currentTarget.blur()} />
|
|
273
|
+
<button title="the frame after" disabled={at === last} onMouseDown={keepFocus} onClick={() => go(at + 1)}>›</button>
|
|
274
|
+
<span className="rv-dim rv-small">{at === last ? 'now' : (frames.gallery[at]?.label ?? `frame ${at + 1}`)}</span>
|
|
249
275
|
</div>
|
|
250
276
|
)
|
|
251
277
|
})
|
|
@@ -285,8 +311,8 @@ const Gallery = observer(({ rv, id, v, view }: { rv: ReviewSt; id: string; v: Ga
|
|
|
285
311
|
</div>
|
|
286
312
|
<div className="rv-tilelabel" title={t.label}>{t.label}</div>
|
|
287
313
|
<ReactionToggles rv={rv} view={view} tile={k} small />
|
|
288
|
-
<
|
|
289
|
-
|
|
314
|
+
<AutoText className="rv-word" placeholder="a word" value={r?.comment ?? ''} onKeyDown={blurOnEscape}
|
|
315
|
+
onText={(t) => rv.setReactionComment(view, k, t)} />
|
|
290
316
|
</div>
|
|
291
317
|
)
|
|
292
318
|
})}
|
|
@@ -309,7 +335,8 @@ const MediaBody = observer(({ rv, id, v, i, zoom }: { rv: ReviewSt; id: string;
|
|
|
309
335
|
return (
|
|
310
336
|
<div style={scroll}>
|
|
311
337
|
<div style={{ width: scale ? `${v.size.w * scale}px` : `min(100%, calc((100vh - ${room}px) * ${ratio}))`, margin: '0 auto' }}>
|
|
312
|
-
<Compare c={rv.compare} base={rv.fileUrl(id, name)} mod={undefined} aspect={aspect} draw={(s) => <Marks rv={rv} src={s} side={undefined} view={i} />}
|
|
338
|
+
<Compare c={rv.compare} base={rv.fileUrl(id, name)} mod={undefined} aspect={aspect} draw={(s) => <Marks rv={rv} src={s} side={undefined} view={i} />}
|
|
339
|
+
over={() => <RectNote rv={rv} view={i} sides={[undefined]} />} />
|
|
313
340
|
{bar}
|
|
314
341
|
<Downloads rv={rv} id={id} files={[{ file: name }]} />
|
|
315
342
|
</div>
|
|
@@ -334,7 +361,8 @@ const MediaBody = observer(({ rv, id, v, i, zoom }: { rv: ReviewSt; id: string;
|
|
|
334
361
|
<div style={scroll}>
|
|
335
362
|
<div className="rv-pairgrid" style={{ width: scale ? `${v.size.w * scale * cols + 8 * (cols - 1)}px` : fitW, gridTemplateColumns: `repeat(${cols}, 1fr)` }}>
|
|
336
363
|
<Compare c={c} base={before} mod={after} names={PAIR_NAMES} aspect={aspect}
|
|
337
|
-
draw={(s) => <Marks rv={rv} src={s} side={drawnSide(c.mode, s === before ? 'before' : 'after')} view={i} />}
|
|
364
|
+
draw={(s) => <Marks rv={rv} src={s} side={drawnSide(c.mode, s === before ? 'before' : 'after')} view={i} />}
|
|
365
|
+
over={(srcs) => <RectNote rv={rv} view={i} sides={srcs.map((s) => drawnSide(c.mode, s === before ? 'before' : 'after'))} />} />
|
|
338
366
|
</div>
|
|
339
367
|
</div>
|
|
340
368
|
{bar ? <div style={{ width: fitW, margin: '0 auto' }}>{bar}</div> : null}
|
|
@@ -351,7 +379,7 @@ const SoundBody = ({ rv, id, v }: { rv: ReviewSt; id: string; v: MediaView }) =>
|
|
|
351
379
|
if (!a) return null
|
|
352
380
|
const rows = isPair(f) ? [{ label: 'before', file: f.before, wave: a.waveBefore ?? a.wave }, { label: 'after', file: f.after, wave: a.wave }] : [{ label: '', file: 'audio' in f ? f.audio : '', wave: a.wave }]
|
|
353
381
|
return (
|
|
354
|
-
<div className="rv-
|
|
382
|
+
<div className="rv-soundview">
|
|
355
383
|
{rows.map((r) => (
|
|
356
384
|
<div key={r.file} className="rv-soundrow">
|
|
357
385
|
{r.label ? <div className="rv-dim">{r.label}</div> : null}
|
|
@@ -412,9 +440,11 @@ const PhonePair = observer(({ rv, view, bar, before, after, aspect, size, scale
|
|
|
412
440
|
<div style={scale ? { overflow: 'auto', maxHeight: '70vh' } : undefined}>
|
|
413
441
|
<div style={{ width: scale ? `${size.w * scale}px` : '100%' }}>
|
|
414
442
|
{side === 'swipe' ? (
|
|
415
|
-
<Compare c={c} base={before} mod={after} names={PAIR_NAMES} aspect={aspect} draw={(s) => <Marks rv={rv} src={s} side={s === before ? 'before' : 'after'} view={view} />}
|
|
443
|
+
<Compare c={c} base={before} mod={after} names={PAIR_NAMES} aspect={aspect} draw={(s) => <Marks rv={rv} src={s} side={s === before ? 'before' : 'after'} view={view} />}
|
|
444
|
+
over={(srcs) => <RectNote rv={rv} view={view} sides={srcs.map((s) => (s === before ? 'before' : 'after'))} />} />
|
|
416
445
|
) : (
|
|
417
|
-
<Compare key={side} c={c} base={one} mod={undefined} aspect={aspect} draw={(s) => <Marks rv={rv} src={s} side={side} view={view} />}
|
|
446
|
+
<Compare key={side} c={c} base={one} mod={undefined} aspect={aspect} draw={(s) => <Marks rv={rv} src={s} side={side} view={view} />}
|
|
447
|
+
over={() => <RectNote rv={rv} view={view} sides={[side]} />} />
|
|
418
448
|
)}
|
|
419
449
|
{bar}
|
|
420
450
|
</div>
|
|
@@ -458,27 +488,28 @@ function track(move: (e: PointerEvent) => void): void {
|
|
|
458
488
|
}
|
|
459
489
|
|
|
460
490
|
/** THE pair: side by side, the curtain clipping both images, the after over the before as an onion skin, or the
|
|
461
|
-
* after alone (the before while the pointer is over it). One image alone when `mod` is undefined
|
|
462
|
-
|
|
491
|
+
* after alone (the before while the pointer is over it). One image alone when `mod` is undefined. `over` draws what
|
|
492
|
+
* sits over a frame unclipped (the picked rectangle's note box), given the images that frame shows */
|
|
493
|
+
const Compare = observer(({ c, base, mod, names, aspect, draw, over }: { c: CompareSt; base: string; mod: string | undefined; names?: { base: string; mod: string }; aspect: string; draw: (src: string) => ReactNode; over?: (srcs: string[]) => ReactNode }) => {
|
|
463
494
|
const [peek, setPeek] = useState(false)
|
|
464
|
-
if (mod === undefined) return <Cell aspect={aspect} caption={null}><div className="rv-fill">{draw(base)}</div></Cell>
|
|
495
|
+
if (mod === undefined) return <Cell aspect={aspect} caption={null} over={over?.([base])}><div className="rv-fill">{draw(base)}</div></Cell>
|
|
465
496
|
const n = names ?? PAIR_NAMES
|
|
466
497
|
if (c.mode === 'side')
|
|
467
498
|
return (
|
|
468
499
|
<>
|
|
469
|
-
<Cell aspect={aspect} caption={n.base}><div className="rv-fill">{draw(base)}</div></Cell>
|
|
470
|
-
<Cell aspect={aspect} caption={n.mod} modCap><div className="rv-fill">{draw(mod)}</div></Cell>
|
|
500
|
+
<Cell aspect={aspect} caption={n.base} over={over?.([base])}><div className="rv-fill">{draw(base)}</div></Cell>
|
|
501
|
+
<Cell aspect={aspect} caption={n.mod} modCap over={over?.([mod])}><div className="rv-fill">{draw(mod)}</div></Cell>
|
|
471
502
|
</>
|
|
472
503
|
)
|
|
473
504
|
if (c.mode === 'mod')
|
|
474
505
|
return (
|
|
475
|
-
<Cell aspect={aspect} caption={peek ? n.base : n.mod} modCap={!peek} onPeek={setPeek}>
|
|
506
|
+
<Cell aspect={aspect} caption={peek ? n.base : n.mod} modCap={!peek} onPeek={setPeek} over={over?.([peek ? base : mod])}>
|
|
476
507
|
<div className="rv-fill">{draw(peek ? base : mod)}</div>
|
|
477
508
|
</Cell>
|
|
478
509
|
)
|
|
479
510
|
const pct = Math.round(c.split * 100)
|
|
480
511
|
return (
|
|
481
|
-
<Cell aspect={aspect} caption={<>{n.base} {c.mode === 'swipe' ? '◀ ▶' : '+'} <span className="rv-modcap">{n.mod}</span></>} swipe={c.mode === 'swipe' ? c : undefined}>
|
|
512
|
+
<Cell aspect={aspect} caption={<>{n.base} {c.mode === 'swipe' ? '◀ ▶' : '+'} <span className="rv-modcap">{n.mod}</span></>} swipe={c.mode === 'swipe' ? c : undefined} over={over?.([base, mod])}>
|
|
482
513
|
<div className="rv-fill" style={c.mode === 'swipe' ? { clipPath: `inset(0 ${100 - pct}% 0 0)` } : undefined}>{draw(base)}</div>
|
|
483
514
|
<div className="rv-fill" style={c.mode === 'swipe' ? { clipPath: `inset(0 0 0 ${pct}%)` } : { opacity: c.opacity }}>{draw(mod)}</div>
|
|
484
515
|
{c.mode === 'swipe' ? <div className="rv-curtain" style={{ left: `${pct}%` }} /> : null}
|
|
@@ -486,8 +517,8 @@ const Compare = observer(({ c, base, mod, names, aspect, draw }: { c: CompareSt;
|
|
|
486
517
|
)
|
|
487
518
|
})
|
|
488
519
|
|
|
489
|
-
/** one frame and its caption; the curtain's line follows the pointer */
|
|
490
|
-
const Cell = ({ aspect, caption, modCap, swipe, onPeek, children }: { aspect: string; caption: ReactNode; modCap?: boolean; swipe?: CompareSt | undefined; onPeek?: (on: boolean) => void; children: ReactNode }) => {
|
|
520
|
+
/** one frame and its caption; the curtain's line follows the pointer; `over` sits on the frame, free of its clip */
|
|
521
|
+
const Cell = ({ aspect, caption, modCap, swipe, onPeek, over, children }: { aspect: string; caption: ReactNode; modCap?: boolean; swipe?: CompareSt | undefined; onPeek?: (on: boolean) => void; over?: ReactNode; children: ReactNode }) => {
|
|
491
522
|
const frame = useRef<HTMLDivElement>(null)
|
|
492
523
|
const down = (e: React.PointerEvent): void => {
|
|
493
524
|
const el = frame.current
|
|
@@ -502,9 +533,12 @@ const Cell = ({ aspect, caption, modCap, swipe, onPeek, children }: { aspect: st
|
|
|
502
533
|
}
|
|
503
534
|
return (
|
|
504
535
|
<div className="rv-cell">
|
|
505
|
-
<div
|
|
506
|
-
|
|
507
|
-
|
|
536
|
+
<div className="rv-framebox">
|
|
537
|
+
<div ref={frame} className={`rv-frame${swipe ? ' swipe' : ''}`} style={{ aspectRatio: aspect }} onPointerDown={down}
|
|
538
|
+
onMouseEnter={() => onPeek?.(true)} onMouseLeave={() => onPeek?.(false)}>
|
|
539
|
+
{children}
|
|
540
|
+
</div>
|
|
541
|
+
{over ? <div className="rv-over">{over}</div> : null}
|
|
508
542
|
</div>
|
|
509
543
|
{caption != null ? <div className={`rv-caption${modCap ? ' mod' : ''}`}>{caption}</div> : null}
|
|
510
544
|
</div>
|
|
@@ -578,7 +612,7 @@ const Marks = observer(({ rv, src, side, view }: { rv: ReviewSt; src: string; si
|
|
|
578
612
|
{clip ? <ClipVideo key={src} clip={c} src={src} /> : <img src={src} alt="" draggable={false} className="rv-markimg" />}
|
|
579
613
|
{shown && rings.length > 0 ? <Rings marks={rings} w={shown.size.w} h={shown.size.h} /> : null}
|
|
580
614
|
{rects.map(([r, i]) => (
|
|
581
|
-
<RectMark key={i} r={r} n={i + 1} on={rv.rect === i} faint={!onIts(r)} at={clip && r.t !== undefined ? secs(r.t) : undefined} pick={() => rv.pickRect(i)} />
|
|
615
|
+
<RectMark key={i} r={r} n={i + 1} on={rv.rect === i} faint={!onIts(r)} at={clip && r.t !== undefined ? secs(r.t) : undefined} pick={() => rv.pickRect(i, true)} />
|
|
582
616
|
))}
|
|
583
617
|
{drag ? <div className="rv-drag" style={pct(box(drag))} /> : null}
|
|
584
618
|
</div>
|
|
@@ -600,11 +634,46 @@ const Rings = ({ marks, w, h }: { marks: readonly ReviewMark[]; w: number; h: nu
|
|
|
600
634
|
/** a rectangle of his; on a clip, faint and dashed away from its frame, its time on its number */
|
|
601
635
|
const RectMark = ({ r, n, on, faint, at, pick }: { r: Rect; n: number; on: boolean; faint: boolean; at: string | undefined; pick: () => void }) => (
|
|
602
636
|
<div className={`rv-rect${on ? ' on' : ''}${faint ? ' faint' : ''}`} style={pct(r)} title={r.comment || `rectangle ${n}`}
|
|
603
|
-
onPointerDown={(e) => { e.stopPropagation(); pick() }}>
|
|
637
|
+
onPointerDown={(e) => { e.stopPropagation(); pick() }} onMouseDown={keepFocus}>
|
|
604
638
|
<span className="rv-rectn">{n}{at ? ` · ${at}` : ''}</span>
|
|
605
639
|
</div>
|
|
606
640
|
)
|
|
607
641
|
|
|
642
|
+
/** the picked rectangle's note box, beside it on the image: under it, or over it when it sits low, focused once drawn
|
|
643
|
+
* or clicked. Enter or Escape closes it (the rectangle stays), Shift Enter is a new line; `sides` are the sides the
|
|
644
|
+
* frame shows */
|
|
645
|
+
const RectNote = observer(({ rv, view, sides }: { rv: ReviewSt; view: number; sides: readonly (Side | undefined)[] }) => {
|
|
646
|
+
const box = useRef<HTMLTextAreaElement | null>(null)
|
|
647
|
+
const i = rv.rect
|
|
648
|
+
const r = i === null ? undefined : rv.draft.rects[i]
|
|
649
|
+
const here = i !== null && r !== undefined && (r.view ?? 0) === view && sides.includes(r.side)
|
|
650
|
+
const focus = here && rv.noteFocus === i
|
|
651
|
+
useEffect(() => {
|
|
652
|
+
if (!focus || box.current === null) return
|
|
653
|
+
box.current.focus()
|
|
654
|
+
rv.noteFocused()
|
|
655
|
+
}, [focus, rv])
|
|
656
|
+
if (!here) return null
|
|
657
|
+
const below = r.y + r.h <= NOTE_LOW || r.y < 1 - NOTE_LOW
|
|
658
|
+
const place = below ? { top: `${(r.y + r.h) * 100}%` } : { bottom: `${(1 - r.y) * 100}%` }
|
|
659
|
+
return (
|
|
660
|
+
<div className={`rv-note${below ? ' below' : ' above'}`} style={{ ...place, ['--nx' as string]: `${r.x * 100}%` }} onPointerDown={(e) => e.stopPropagation()}>
|
|
661
|
+
<span className="rv-noten">{i + 1}{r.t !== undefined ? ` · ${secs(r.t)}` : ''}</span>
|
|
662
|
+
<AutoText el={(t) => (box.current = t)} placeholder="what is wrong here · Enter closes" value={r.comment}
|
|
663
|
+
onText={(t) => rv.setRectComment(i, t)}
|
|
664
|
+
onKeyDown={(e) => {
|
|
665
|
+
if ((e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) || e.key === 'Escape') {
|
|
666
|
+
e.preventDefault()
|
|
667
|
+
rv.pickRect(null)
|
|
668
|
+
}
|
|
669
|
+
}} />
|
|
670
|
+
<button className="ghost rv-x" title="delete this rectangle" aria-label="delete this rectangle" onMouseDown={keepFocus} onClick={() => rv.removeRect(i)}>✕</button>
|
|
671
|
+
</div>
|
|
672
|
+
)
|
|
673
|
+
})
|
|
674
|
+
/** a rectangle whose bottom is lower than this share of the image opens its note over it, when there is room above */
|
|
675
|
+
const NOTE_LOW = 0.78
|
|
676
|
+
|
|
608
677
|
// ── a clip ──
|
|
609
678
|
|
|
610
679
|
/** one video of the open view, driven by the view's one clock; muted until the sound button, inline; looping, or at its
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { observer } from 'mobx-react-lite'
|
|
5
5
|
import { Fragment, useEffect, useRef, useState, type ReactNode } from 'react'
|
|
6
6
|
import { Group, Panel, Separator } from 'react-resizable-panels'
|
|
7
|
+
import { AutoText } from '../auto-text.tsx'
|
|
7
8
|
import { clock, cutsByRecent, shotAt, type CutInfo, type TrailerNote } from '../../../review/trailer-model.ts'
|
|
8
9
|
import { editorHref } from '../check-words.ts'
|
|
9
10
|
import { renderMd } from '../md.ts'
|
|
@@ -271,7 +272,7 @@ const DraftBox = observer(({ tr }: { tr: TrailerSt }) => {
|
|
|
271
272
|
<span className="tr-dim tr-small">click the image to pin a point</span>
|
|
272
273
|
)}
|
|
273
274
|
</div>
|
|
274
|
-
<
|
|
275
|
+
<AutoText autoFocus rows={3} placeholder="What is wrong, what you want instead…" value={d.text} onText={(t) => tr.setDraftText(t)}
|
|
275
276
|
onKeyDown={(e) => {
|
|
276
277
|
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
277
278
|
e.preventDefault()
|
|
@@ -362,7 +363,7 @@ const NoteCard = observer(({ tr, n: p }: { tr: TrailerSt; n: PlacedNote }) => {
|
|
|
362
363
|
{on ? <Still tr={tr} n={n} w="100%" /> : null}
|
|
363
364
|
{editing ? (
|
|
364
365
|
<div className="tr-edit">
|
|
365
|
-
<
|
|
366
|
+
<AutoText autoFocus rows={3} value={editing.text} onText={(t) => tr.setEditText(t)} onKeyDown={enterSaves(() => void tr.saveEdit(), () => tr.cancelEdit())} />
|
|
366
367
|
<div className="tr-dim tr-small">Enter saves · Escape drops it · on its frame, a click moves the pin</div>
|
|
367
368
|
</div>
|
|
368
369
|
) : null}
|
|
@@ -374,8 +375,8 @@ const NoteCard = observer(({ tr, n: p }: { tr: TrailerSt; n: PlacedNote }) => {
|
|
|
374
375
|
) : null}
|
|
375
376
|
{closing ? (
|
|
376
377
|
<div className="tr-edit">
|
|
377
|
-
<
|
|
378
|
-
|
|
378
|
+
<AutoText autoFocus rows={2} placeholder={closing.state === 'done' ? 'The commit, or what was done (optional)' : 'Why (optional)'} value={closing.reply}
|
|
379
|
+
onText={(t) => tr.setReply(t)} onKeyDown={enterSaves(() => void tr.confirmClose(), () => tr.cancelClose())} />
|
|
379
380
|
<div className="tr-draftbtns">
|
|
380
381
|
<button onClick={() => tr.cancelClose()}>Cancel</button>
|
|
381
382
|
<button className="primary" onClick={() => void tr.confirmClose()}>{closing.state === 'done' ? 'Mark done' : 'Not doing it'}</button>
|
|
@@ -120,6 +120,9 @@
|
|
|
120
120
|
.rv-raisedimg{display:block;max-width:100%;max-height:320px;margin-top:var(--s2);border-radius:4px}
|
|
121
121
|
.rv-pairgrid{display:grid;gap:var(--s2)}
|
|
122
122
|
.rv-cell{display:flex;flex-direction:column;align-items:center;gap:var(--s0);min-width:0}
|
|
123
|
+
/* the frame's box: what sits over the frame free of its clip (the note box) shares its size */
|
|
124
|
+
.rv-framebox{position:relative;width:100%}
|
|
125
|
+
.rv-over{position:absolute;inset:0;z-index:4;pointer-events:none}
|
|
123
126
|
.rv-frame{position:relative;width:100%;max-width:100%;border-radius:4px;border:1px solid var(--line);overflow:hidden;
|
|
124
127
|
background:repeating-conic-gradient(var(--chk1) 0 25%,var(--chk2) 0 50%) 0 0/16px 16px}
|
|
125
128
|
.rv-frame.swipe{cursor:ew-resize;touch-action:none}
|
|
@@ -142,6 +145,13 @@
|
|
|
142
145
|
.rv-rectn{position:absolute;left:-2px;top:-2px;transform:translateY(-100%);padding:0 var(--s1);border-radius:3px;background:var(--warn);color:var(--bg);font-size:11px;font-weight:800;line-height:16px;white-space:nowrap}
|
|
143
146
|
.rv-rect.on .rv-rectn{background:var(--blue)}
|
|
144
147
|
.rv-drag{position:absolute;border:2px dashed var(--warn)}
|
|
148
|
+
/* the picked rectangle's note box, in the picked rectangle's blue: under it, or over it (clearing its number) */
|
|
149
|
+
.rv-note{position:absolute;left:clamp(0px,var(--nx),calc(100% - min(300px,100%)));width:min(300px,100%);pointer-events:auto;display:flex;gap:var(--s1);align-items:flex-start;
|
|
150
|
+
padding:var(--s1);border-radius:6px;border:2px solid var(--blue);background:var(--panel);box-shadow:0 2px 10px var(--shadow)}
|
|
151
|
+
.rv-note.below{margin-top:var(--s1)}
|
|
152
|
+
.rv-note.above{margin-bottom:var(--s4)}
|
|
153
|
+
.rv-noten{flex:none;padding:0 var(--s1);border-radius:3px;background:var(--blue);color:var(--bg);font-size:11px;font-weight:800;line-height:18px;white-space:nowrap}
|
|
154
|
+
.rv-note textarea{flex:1;min-width:0}
|
|
145
155
|
.rv-downloads{display:flex;gap:var(--s2);justify-content:flex-end;margin-top:var(--s1)}
|
|
146
156
|
.rv-dl{font-size:11.5px;text-decoration:none}
|
|
147
157
|
.rv-gallery{display:flex;flex-wrap:wrap;gap:var(--s2);align-items:flex-start}
|
|
@@ -153,8 +163,12 @@
|
|
|
153
163
|
.rv-tileimg img{display:block;width:100%;height:100%;object-fit:contain}
|
|
154
164
|
.rv-tilelabel{font-size:11px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
155
165
|
.rv-clipbar{display:flex;gap:var(--s1);align-items:center;margin-top:var(--s2)}
|
|
166
|
+
.rv-frames{display:flex;gap:var(--s1);align-items:center;margin-bottom:var(--s2)}
|
|
167
|
+
.rv-frames input{flex:1;min-width:60px}
|
|
168
|
+
.rv-frames button{padding:0 var(--s2);line-height:22px}
|
|
156
169
|
.rv-clipbar button{padding:0 var(--s2);line-height:22px}
|
|
157
170
|
.rv-scrub{flex:1;min-width:60px}
|
|
171
|
+
.rv-clipbar .rv-mono{flex:none;white-space:nowrap}
|
|
158
172
|
.rv-sound{display:inline-flex;align-items:center;gap:var(--s1)}
|
|
159
173
|
.rv-sound button{padding:0 var(--s1);line-height:22px}
|
|
160
174
|
.rv-sound input{width:72px}
|
|
@@ -223,7 +237,7 @@
|
|
|
223
237
|
.rv-linkedtitle{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}
|
|
224
238
|
.rv-outgoing,.rv-messages{display:flex;flex-direction:column;gap:var(--s1)}
|
|
225
239
|
.rv-outrow{display:flex;gap:var(--s1);align-items:flex-start}
|
|
226
|
-
.rv-outrow textarea{flex:1
|
|
240
|
+
.rv-outrow textarea{flex:1}
|
|
227
241
|
.rv-outbadge{flex:none;max-width:170px;margin-top:var(--s0);font-size:11px;padding:0 var(--s2);line-height:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
|
228
242
|
color:var(--tone,var(--ink));border-color:color-mix(in srgb,var(--tone,var(--line)) 50%,var(--line))}
|
|
229
243
|
.rv-outbadge.rect{--tone:var(--warn)}
|
|
@@ -251,6 +265,8 @@
|
|
|
251
265
|
.rv-verdictbar .rv-verdict{flex:1;text-align:center;font-size:14px;padding:var(--s2) var(--s1);color:var(--tone)}
|
|
252
266
|
.rv-verdictbar .rv-verdict.chosen{color:var(--bg)}
|
|
253
267
|
.rv-phone .rv-views{gap:var(--s3)}
|
|
268
|
+
/* the views' labels take the phone's whole width, the zoom under them */
|
|
269
|
+
.rv-phone .rv-jumps{flex:1 1 100%}
|
|
254
270
|
.rv-phone .rv-toast{top:var(--s4);bottom:auto}
|
|
255
271
|
/* a row on a phone: ▶, the label and the time on one line, the strip and the acts under them */
|
|
256
272
|
.rv-phone .rv-optrow{flex-wrap:wrap;row-gap:var(--s1)}
|
|
@@ -261,6 +277,8 @@
|
|
|
261
277
|
.rv-phone .rv-stars button{font-size:22px;padding:0 var(--s1)}
|
|
262
278
|
.rv-phone .rv-optplay{width:40px;height:34px}
|
|
263
279
|
.rv-phone .rv-optnote,.rv-phone .rv-opt .rv-word{margin-left:0;width:100%}
|
|
280
|
+
/* a phone zooms the page into a field whose text is under 16px: the field takes 16px instead */
|
|
281
|
+
.rv-phone textarea,.tr-phone textarea{font-size:16px}
|
|
264
282
|
|
|
265
283
|
/* ── the trailer page ── */
|
|
266
284
|
.tr-cuts{display:flex;flex-direction:column;gap:var(--s2);padding:var(--s3)}
|
|
@@ -385,5 +403,5 @@
|
|
|
385
403
|
.rv-parttext{font-size:13px;line-height:1.45;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
386
404
|
.rv-anim select{font-size:12px}
|
|
387
405
|
/* a sound view (review-views.tsx SoundBody): its waveform over the player, a row a side on a pair */
|
|
388
|
-
.rv-
|
|
406
|
+
.rv-soundview{display:flex;flex-direction:column;gap:var(--s2)}
|
|
389
407
|
.rv-soundrow{display:flex;flex-direction:column;gap:var(--s1);background:var(--panel);border:1px solid var(--line);border-radius:4px;padding:var(--s2)}
|
|
@@ -12,6 +12,7 @@ export function comfyModule(comfy: string | undefined, name: string): string {
|
|
|
12
12
|
const COMFY_TIMEOUT_S = 15 * 60
|
|
13
13
|
|
|
14
14
|
export async function comfyRun(module: string, vars: Record<string, string | number | boolean | undefined>, out: string): Promise<void> {
|
|
15
|
+
if (Bun.which('comfy-ts') == null) throw new Error(`comfy-ts is not on PATH, and ${module} is a comfy-ts module. Install it: bun add -g comfy-ts`)
|
|
15
16
|
const args = ['comfy-ts', 'run', module]
|
|
16
17
|
for (const [k, v] of Object.entries(vars)) if (v !== undefined) args.push(`--${k}`, String(v))
|
|
17
18
|
args.push('--out', out, '--json')
|