cursedbelt 5.97.0 → 5.99.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/dist/react/media/FocusViewer.d.ts.map +1 -1
- package/dist/react/media/FocusViewer.js +3 -2
- package/dist/react/media/FocusViewer.js.map +1 -1
- package/dist/react/media/ImageOriginalToggle.d.ts +22 -0
- package/dist/react/media/ImageOriginalToggle.d.ts.map +1 -0
- package/dist/react/media/ImageOriginalToggle.js +16 -0
- package/dist/react/media/ImageOriginalToggle.js.map +1 -0
- package/dist/react/media/VideoPlayer.d.ts.map +1 -1
- package/dist/react/media/VideoPlayer.js +9 -3
- package/dist/react/media/VideoPlayer.js.map +1 -1
- package/dist/react/media/VideoQualitySelect.d.ts +24 -0
- package/dist/react/media/VideoQualitySelect.d.ts.map +1 -0
- package/dist/react/media/VideoQualitySelect.js +34 -0
- package/dist/react/media/VideoQualitySelect.js.map +1 -0
- package/dist/react/media/hlsSource.d.ts +14 -2
- package/dist/react/media/hlsSource.d.ts.map +1 -1
- package/dist/react/media/hlsSource.js +27 -6
- package/dist/react/media/hlsSource.js.map +1 -1
- package/dist/react/media/imageOriginal.d.ts +69 -0
- package/dist/react/media/imageOriginal.d.ts.map +1 -0
- package/dist/react/media/imageOriginal.js +98 -0
- package/dist/react/media/imageOriginal.js.map +1 -0
- package/dist/react/media/imageOriginalEntry.d.ts +9 -0
- package/dist/react/media/imageOriginalEntry.d.ts.map +1 -0
- package/dist/react/media/imageOriginalEntry.js +9 -0
- package/dist/react/media/imageOriginalEntry.js.map +1 -0
- package/dist/react/media/index.d.ts +5 -0
- package/dist/react/media/index.d.ts.map +1 -1
- package/dist/react/media/index.js +6 -0
- package/dist/react/media/index.js.map +1 -1
- package/dist/react/media/useImageOriginal.d.ts +21 -0
- package/dist/react/media/useImageOriginal.d.ts.map +1 -0
- package/dist/react/media/useImageOriginal.js +63 -0
- package/dist/react/media/useImageOriginal.js.map +1 -0
- package/dist/react/media/videoPlayer.css +23 -0
- package/dist/react/media/videoQuality.d.ts +113 -0
- package/dist/react/media/videoQuality.d.ts.map +1 -0
- package/dist/react/media/videoQuality.js +185 -0
- package/dist/react/media/videoQuality.js.map +1 -0
- package/dist/react/media/vidstackQuality.d.ts +27 -0
- package/dist/react/media/vidstackQuality.d.ts.map +1 -0
- package/dist/react/media/vidstackQuality.js +51 -0
- package/dist/react/media/vidstackQuality.js.map +1 -0
- package/dist/react/media-gallery/MediaGallery.d.ts.map +1 -1
- package/dist/react/media-gallery/MediaGallery.js +42 -10
- package/dist/react/media-gallery/MediaGallery.js.map +1 -1
- package/dist/react/media-gallery/mediaGallery.css +42 -0
- package/dist/react/media-gallery/mediaGalleryModel.d.ts.map +1 -1
- package/dist/react/media-gallery/mediaGalleryModel.js +5 -2
- package/dist/react/media-gallery/mediaGalleryModel.js.map +1 -1
- package/dist/react/media-gallery/types.d.ts +10 -0
- package/dist/react/media-gallery/types.d.ts.map +1 -1
- package/dist/react/sharing/SharePanel.d.ts.map +1 -1
- package/dist/react/sharing/SharePanel.js +2 -1
- package/dist/react/sharing/SharePanel.js.map +1 -1
- package/package.json +8 -2
- package/scripts/paths.ts +1 -1
- package/scripts/styleAreas.ts +2 -1
- package/src/react/media/FocusViewer.tsx +9 -1
- package/src/react/media/ImageOriginalToggle.tsx +51 -0
- package/src/react/media/VideoPlayer.spec.tsx +44 -1
- package/src/react/media/VideoPlayer.tsx +17 -3
- package/src/react/media/VideoQualitySelect.spec.tsx +47 -0
- package/src/react/media/VideoQualitySelect.tsx +81 -0
- package/src/react/media/hlsSource.spec.ts +10 -0
- package/src/react/media/hlsSource.ts +41 -7
- package/src/react/media/imageOriginal.spec.tsx +183 -0
- package/src/react/media/imageOriginal.ts +114 -0
- package/src/react/media/imageOriginalEntry.ts +8 -0
- package/src/react/media/index.ts +9 -0
- package/src/react/media/useImageOriginal.ts +97 -0
- package/src/react/media/videoPlayer.css +23 -0
- package/src/react/media/videoQuality.spec.ts +135 -0
- package/src/react/media/videoQuality.ts +248 -0
- package/src/react/media/vidstackQuality.tsx +65 -0
- package/src/react/media-gallery/MediaGallery.spec.tsx +26 -0
- package/src/react/media-gallery/MediaGallery.tsx +57 -17
- package/src/react/media-gallery/mediaGallery.css +42 -0
- package/src/react/media-gallery/mediaGalleryModel.ts +4 -2
- package/src/react/media-gallery/types.ts +10 -0
- package/src/react/sharing/SharePanel.tsx +2 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The quality menu on cursedbelt's Vidstack player (forge task 3868) — kept out of
|
|
3
|
+
* `VideoPlayer.tsx` so the `./react/media/video-player` subpath exports what it always did.
|
|
4
|
+
*/
|
|
5
|
+
import { isHLSProvider, type MediaProviderAdapter } from '@vidstack/react';
|
|
6
|
+
import type { DefaultVideoLayoutSlots } from '@vidstack/react/player/layouts/default';
|
|
7
|
+
import { useSyncExternalStore } from 'react';
|
|
8
|
+
import {
|
|
9
|
+
bindHlsQuality,
|
|
10
|
+
type HlsQualityEngine,
|
|
11
|
+
NATIVE_HLS_REASON,
|
|
12
|
+
NO_LADDER_REASON,
|
|
13
|
+
readVideoQuality,
|
|
14
|
+
unavailableQuality,
|
|
15
|
+
type VideoQualityStore,
|
|
16
|
+
} from './videoQuality.js';
|
|
17
|
+
import { VideoQualitySelect } from './VideoQualitySelect.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The viewer's quality choice on Vidstack's hls.js (forge task 3868) — the same choice, read from
|
|
21
|
+
* the same key, as every `hlsSource.ts` surface (`videoQuality.ts` has what each one means).
|
|
22
|
+
*
|
|
23
|
+
* 🔴 Auto is capped at the player's size HERE TOO. Vidstack's hls.js ran with the library's
|
|
24
|
+
* defaults, so this stage alone fetched the top rung into any size of player; with that rung now
|
|
25
|
+
* the ORIGINAL (a 4K film's is ~20 Mbps), Auto means the same thing on every surface, and Max is
|
|
26
|
+
* the one remembered choice that lifts the cap.
|
|
27
|
+
*
|
|
28
|
+
* Safari's own HLS (Vidstack's plain `VideoProvider` playing a ladder) and the progressive copy
|
|
29
|
+
* can choose nothing, and say why.
|
|
30
|
+
*/
|
|
31
|
+
export function bindVidstackQuality(
|
|
32
|
+
provider: MediaProviderAdapter | null,
|
|
33
|
+
store: VideoQualityStore,
|
|
34
|
+
ladder: boolean,
|
|
35
|
+
): void {
|
|
36
|
+
if (!provider) {
|
|
37
|
+
store.set(null);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (isHLSProvider(provider)) {
|
|
41
|
+
store.set(null);
|
|
42
|
+
provider.config = { ...provider.config, capLevelToPlayerSize: readVideoQuality() === 'auto' };
|
|
43
|
+
provider.onInstance((hls) => {
|
|
44
|
+
bindHlsQuality(hls as unknown as HlsQualityEngine, store.set);
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
store.set(unavailableQuality(ladder ? NATIVE_HLS_REASON : NO_LADDER_REASON));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The quality menu in the bar, re-rendered by the store alone — see `VideoQualityStore`. */
|
|
52
|
+
function QualitySlot({ store }: { store: VideoQualityStore }) {
|
|
53
|
+
const control = useSyncExternalStore(store.subscribe, store.get, store.get);
|
|
54
|
+
return <VideoQualitySelect className='cbvp-quality' control={control} fieldStyle={false} />;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Quality beside the settings cog, where YouTube keeps it. It REPLACES Vidstack's own quality
|
|
59
|
+
* submenu (hidden in `videoPlayer.css`): that one has no Max, remembers nothing, and two menus
|
|
60
|
+
* choosing one thing would disagree.
|
|
61
|
+
*/
|
|
62
|
+
export function qualitySlots(store: VideoQualityStore): DefaultVideoLayoutSlots {
|
|
63
|
+
const pair = { beforeSettingsMenu: <QualitySlot store={store} /> };
|
|
64
|
+
return { ...pair, smallLayout: { ...pair }, largeLayout: { ...pair } };
|
|
65
|
+
}
|
|
@@ -364,6 +364,32 @@ describe('MediaGallery detail view', () => {
|
|
|
364
364
|
expect(container.querySelector('.cbgd-root')).not.toBeNull();
|
|
365
365
|
});
|
|
366
366
|
|
|
367
|
+
test('🔴 a photo with an original draws the web copy and an Original toggle; one without draws none (forge task 3873)', () => {
|
|
368
|
+
const withOriginal: MediaGalleryItem[] = [
|
|
369
|
+
{ ...(ITEMS[0] as MediaGalleryItem), url: '/files/img1.web.jpg', originalUrl: '/files/img1' },
|
|
370
|
+
];
|
|
371
|
+
const first = render(<MediaGallery items={withOriginal} />);
|
|
372
|
+
fireEvent.click(first.getByRole('button', { name: 'Open beach.jpg' }));
|
|
373
|
+
expect(first.container.querySelector('.cbgd-stage-img img')?.getAttribute('src')).toBe('/files/img1.web.jpg');
|
|
374
|
+
const toggle = first.getByRole('button', { name: 'Original' });
|
|
375
|
+
expect(toggle.getAttribute('aria-pressed')).toBe('false');
|
|
376
|
+
expect(toggle.getAttribute('aria-disabled')).not.toBe('true');
|
|
377
|
+
cleanup();
|
|
378
|
+
|
|
379
|
+
const heic: MediaGalleryItem[] = [
|
|
380
|
+
{ ...(ITEMS[0] as MediaGalleryItem), name: 'IMG_1.HEIC', mime: 'image/heic', url: '/files/img1.web.jpg', originalUrl: '/files/img1' },
|
|
381
|
+
];
|
|
382
|
+
const second = render(<MediaGallery items={heic} />);
|
|
383
|
+
fireEvent.click(second.getByRole('button', { name: 'Open IMG_1.HEIC' }));
|
|
384
|
+
// Refused, never hidden: the HEIC original is exactly what the web copy exists for.
|
|
385
|
+
expect(second.getByRole('button', { name: /Original/ }).getAttribute('aria-disabled')).toBe('true');
|
|
386
|
+
cleanup();
|
|
387
|
+
|
|
388
|
+
const plain = render(<MediaGallery items={ITEMS} />);
|
|
389
|
+
fireEvent.click(plain.getByRole('button', { name: 'Open beach.jpg' }));
|
|
390
|
+
expect(plain.queryByRole('button', { name: 'Original' })).toBeNull();
|
|
391
|
+
});
|
|
392
|
+
|
|
367
393
|
test('onDelete renders a confirm-gated delete and closes the detail after it runs', async () => {
|
|
368
394
|
let deleted: string | null = null;
|
|
369
395
|
const { getByRole, findByRole, container } = render(
|
|
@@ -80,6 +80,9 @@ import { DenseGalleryDetail, DenseGalleryShell } from '../media/DenseGalleryShel
|
|
|
80
80
|
import { CBGD_BARE_TILE_HEIGHT } from '../media/VirtualMediaGrid.js';
|
|
81
81
|
import { useGalleryReturn } from './galleryReturn.js';
|
|
82
82
|
import { useHlsSource } from '../media/hlsSource.js';
|
|
83
|
+
import { VideoQualitySelect } from '../media/VideoQualitySelect.js';
|
|
84
|
+
import { ImageOriginalToggle } from '../media/ImageOriginalToggle.js';
|
|
85
|
+
import { useImageOriginal } from '../media/useImageOriginal.js';
|
|
83
86
|
import {
|
|
84
87
|
isRecoverableFault,
|
|
85
88
|
probeSourceFault,
|
|
@@ -1197,22 +1200,27 @@ function NativeVideoStage({
|
|
|
1197
1200
|
onFault?: (fault: SourceFault) => void;
|
|
1198
1201
|
}) {
|
|
1199
1202
|
const ref = React.useRef<HTMLVideoElement>(null);
|
|
1200
|
-
useHlsSource(ref, { hlsUrl, streamUrl }, { ...(onFault ? { onFault } : {}) });
|
|
1203
|
+
const quality = useHlsSource(ref, { hlsUrl, streamUrl }, { ...(onFault ? { onFault } : {}) });
|
|
1201
1204
|
useStageVolume(ref);
|
|
1202
1205
|
return (
|
|
1203
|
-
//
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1206
|
+
// The box exists for the quality menu (task 3868): native `controls` take no extra control,
|
|
1207
|
+
// so the menu sits in the picture's top corner and shows when the controls do.
|
|
1208
|
+
<div className='cbmg-stage-native'>
|
|
1209
|
+
{/* biome-ignore lint/a11y/useMediaCaption: arbitrary user-stored files carry no captions */}
|
|
1210
|
+
<video
|
|
1211
|
+
ref={ref}
|
|
1212
|
+
className='cbmg-stage-video'
|
|
1213
|
+
poster={poster}
|
|
1214
|
+
controls
|
|
1215
|
+
autoPlay
|
|
1216
|
+
playsInline
|
|
1217
|
+
loop={repeat}
|
|
1218
|
+
// A looping element never fires `ended`, so passing both would silently disable
|
|
1219
|
+
// auto-advance rather than combining the two. See `VideoPlayerProps.loop`.
|
|
1220
|
+
onEnded={repeat ? undefined : onEnded}
|
|
1221
|
+
/>
|
|
1222
|
+
<VideoQualitySelect className='cbmg-stage-quality' control={quality} />
|
|
1223
|
+
</div>
|
|
1216
1224
|
);
|
|
1217
1225
|
}
|
|
1218
1226
|
|
|
@@ -1337,13 +1345,36 @@ function NativeVideoSurface({
|
|
|
1337
1345
|
function ImageSurface({
|
|
1338
1346
|
item,
|
|
1339
1347
|
url,
|
|
1348
|
+
originalUrl,
|
|
1340
1349
|
reAsk,
|
|
1341
1350
|
}: {
|
|
1342
1351
|
item: MediaGalleryItem;
|
|
1343
1352
|
url: string;
|
|
1353
|
+
originalUrl?: string | undefined;
|
|
1344
1354
|
reAsk: () => boolean;
|
|
1345
1355
|
}) {
|
|
1346
1356
|
const { fault, report } = useSourceFault(reAsk);
|
|
1357
|
+
// How wide the picture is DRAWN, and how wide the web copy is — the two numbers that decide
|
|
1358
|
+
// whether the original is needed (`useImageOriginal`). The `<img>` is `contain`ed with max
|
|
1359
|
+
// sizes, so its own box IS the drawn picture.
|
|
1360
|
+
const [drawnWidth, setDrawnWidth] = useState(0);
|
|
1361
|
+
const [webWidth, setWebWidth] = useState(0);
|
|
1362
|
+
const observer = useRef<ResizeObserver | null>(null);
|
|
1363
|
+
const measure = useCallback((img: HTMLImageElement | null) => {
|
|
1364
|
+
observer.current?.disconnect();
|
|
1365
|
+
observer.current = null;
|
|
1366
|
+
if (!img || typeof ResizeObserver === 'undefined') return;
|
|
1367
|
+
observer.current = new ResizeObserver(() => setDrawnWidth(img.clientWidth));
|
|
1368
|
+
observer.current.observe(img);
|
|
1369
|
+
}, []);
|
|
1370
|
+
const original = useImageOriginal({
|
|
1371
|
+
src: url,
|
|
1372
|
+
originalSrc: originalUrl,
|
|
1373
|
+
mime: item.mime,
|
|
1374
|
+
name: item.name,
|
|
1375
|
+
drawnWidth,
|
|
1376
|
+
webWidth,
|
|
1377
|
+
});
|
|
1347
1378
|
if (fault) {
|
|
1348
1379
|
return <StageSourceFault item={item} category='image' fault={fault} subject='This picture' />;
|
|
1349
1380
|
}
|
|
@@ -1354,13 +1385,22 @@ function ImageSurface({
|
|
|
1354
1385
|
constraint: a quarter-turned picture is laid out in its ORIGINAL box, so without it
|
|
1355
1386
|
a portrait scan turned upright overflows the frame it is centered in. */}
|
|
1356
1387
|
<img
|
|
1357
|
-
|
|
1388
|
+
ref={measure}
|
|
1389
|
+
src={original.src}
|
|
1358
1390
|
alt={item.name}
|
|
1359
1391
|
style={orientationStyle(item)}
|
|
1392
|
+
onLoad={(event) => {
|
|
1393
|
+
// Only the WEB copy's size is the threshold — the original's is what it is past.
|
|
1394
|
+
if (original.src === url) setWebWidth(event.currentTarget.naturalWidth);
|
|
1395
|
+
setDrawnWidth(event.currentTarget.clientWidth);
|
|
1396
|
+
}}
|
|
1360
1397
|
onError={() => {
|
|
1361
|
-
|
|
1398
|
+
// The original failing is `useImageOriginal`'s own case (it decodes before the swap);
|
|
1399
|
+
// a fault here is the web copy's.
|
|
1400
|
+
void probeSourceFault(original.src).then(report);
|
|
1362
1401
|
}}
|
|
1363
1402
|
/>
|
|
1403
|
+
<ImageOriginalToggle className='cbgd-stage-original' control={original.control} />
|
|
1364
1404
|
</div>
|
|
1365
1405
|
);
|
|
1366
1406
|
}
|
|
@@ -1484,7 +1524,7 @@ function GalleryStage({
|
|
|
1484
1524
|
if (!media.url) return <StageLoading label={resolving ? 'Loading…' : 'Image unavailable.'} />;
|
|
1485
1525
|
// `key`ed on the URL for the reason the video stage is: a new URL — including the one a
|
|
1486
1526
|
// re-ask mints — is a new attempt and must not inherit the previous one's fault.
|
|
1487
|
-
return <ImageSurface key={media.url} item={item} url={media.url} reAsk={refresh} />;
|
|
1527
|
+
return <ImageSurface key={media.url} item={item} url={media.url} originalUrl={media.originalUrl} reAsk={refresh} />;
|
|
1488
1528
|
}
|
|
1489
1529
|
|
|
1490
1530
|
if (category === 'audio') {
|
|
@@ -135,6 +135,35 @@
|
|
|
135
135
|
max-height: 100%;
|
|
136
136
|
height: 100%;
|
|
137
137
|
}
|
|
138
|
+
/* The native stage's box: exactly the video's, so the quality menu (task 3868) can sit in its
|
|
139
|
+
corner. It shows with the native controls — on hover or focus — and always on touch, where
|
|
140
|
+
there is no hover to reveal it. */
|
|
141
|
+
.cbmg-stage-native {
|
|
142
|
+
position: relative;
|
|
143
|
+
width: 100%;
|
|
144
|
+
height: 100%;
|
|
145
|
+
}
|
|
146
|
+
.cbmg-stage-quality {
|
|
147
|
+
position: absolute;
|
|
148
|
+
top: 8px;
|
|
149
|
+
right: 8px;
|
|
150
|
+
opacity: 0;
|
|
151
|
+
transition: opacity 150ms ease;
|
|
152
|
+
}
|
|
153
|
+
.cbmg-stage-native:hover .cbmg-stage-quality,
|
|
154
|
+
.cbmg-stage-quality:focus-within {
|
|
155
|
+
opacity: 1;
|
|
156
|
+
}
|
|
157
|
+
@media (hover: none) {
|
|
158
|
+
.cbmg-stage-quality {
|
|
159
|
+
opacity: 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
@media (pointer: coarse) {
|
|
163
|
+
.cbmg-stage-quality button {
|
|
164
|
+
min-height: 44px;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
138
167
|
|
|
139
168
|
/* ── A TURNED clip (`MediaGalleryItem.orientation` on a video). The owner, 2026-08-14:
|
|
140
169
|
*"a video showing in portrait mode that is actually supposed to be widescreen to fill all
|
|
@@ -227,6 +256,7 @@
|
|
|
227
256
|
|
|
228
257
|
/* Inside the turned frame the stage element must stop claiming its own 70vh, or the flex
|
|
229
258
|
centering above has nothing left to center. */
|
|
259
|
+
.cbmg-stage-turn[data-turned='true'] .cbmg-stage-native,
|
|
230
260
|
.cbmg-stage-turn[data-turned='true'] .cbmg-stage-video,
|
|
231
261
|
.cbmg-stage-turn[data-turned='true'] [data-testid='video-player'] {
|
|
232
262
|
height: auto;
|
|
@@ -1067,3 +1097,15 @@
|
|
|
1067
1097
|
font-size: 13px;
|
|
1068
1098
|
color: var(--muted-foreground);
|
|
1069
1099
|
}
|
|
1100
|
+
|
|
1101
|
+
/* The "Original" toggle on a picture's stage (forge task 3873, `ImageOriginalToggle`): over the
|
|
1102
|
+
letterbox's bottom-right corner, where a viewer's eye is not, and above the picture it swaps.
|
|
1103
|
+
`.cbgd-stage-img` is a flex box, so it anchors the absolute corner. */
|
|
1104
|
+
.cbgd-stage-img {
|
|
1105
|
+
position: relative;
|
|
1106
|
+
}
|
|
1107
|
+
.cbgd-stage-original {
|
|
1108
|
+
position: absolute;
|
|
1109
|
+
right: 12px;
|
|
1110
|
+
bottom: 12px;
|
|
1111
|
+
}
|
|
@@ -93,8 +93,8 @@ export function galleryDateHaystack(createdAt: number | undefined): string {
|
|
|
93
93
|
const date = new Date(createdAt);
|
|
94
94
|
if (Number.isNaN(date.getTime())) return '';
|
|
95
95
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
96
|
-
const
|
|
97
|
-
const parts = [
|
|
96
|
+
const day = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
97
|
+
const parts = [day, day.slice(0, 7), day.slice(0, 4), date.toLocaleDateString()];
|
|
98
98
|
for (const month of ['short', 'long'] as const) {
|
|
99
99
|
parts.push(date.toLocaleDateString(undefined, { day: 'numeric', month, year: 'numeric' }));
|
|
100
100
|
parts.push(date.toLocaleDateString(undefined, { month, year: 'numeric' }));
|
|
@@ -205,6 +205,8 @@ export function staticItemMedia(item: MediaGalleryItem): MediaGalleryItemMedia {
|
|
|
205
205
|
const media: MediaGalleryItemMedia = {};
|
|
206
206
|
if (item.url) media.url = item.url;
|
|
207
207
|
if (item.thumbnailUrl) media.thumbnailUrl = item.thumbnailUrl;
|
|
208
|
+
// A still's original past its web copy — see `MediaGalleryItemMedia.originalUrl`.
|
|
209
|
+
if (item.originalUrl) media.originalUrl = item.originalUrl;
|
|
208
210
|
if (item.posterUrl) media.posterUrl = item.posterUrl;
|
|
209
211
|
if (item.streamUrl) media.streamUrl = item.streamUrl;
|
|
210
212
|
// Additional to `streamUrl`, never instead of it — see `MediaGalleryItemMedia.hlsUrl`.
|
|
@@ -33,6 +33,16 @@ export interface MediaGalleryItemMedia {
|
|
|
33
33
|
url?: string;
|
|
34
34
|
/** Smaller grid derivative; tiles fall back to `url` when absent. */
|
|
35
35
|
thumbnailUrl?: string;
|
|
36
|
+
/**
|
|
37
|
+
* A still's ORIGINAL, signed inline, when `url` is its web copy (forge task 3873).
|
|
38
|
+
*
|
|
39
|
+
* The stage draws `url` and swaps in this one once the picture is drawn with more device
|
|
40
|
+
* pixels than the web copy holds, or when the viewer pressed "Original" — and only after it
|
|
41
|
+
* has decoded. Absent (an app whose `url` already IS the original) draws no control. An
|
|
42
|
+
* original no browser can draw (a HEIC, a RAW) is still worth handing over: the control
|
|
43
|
+
* then says why it is refused. See `imageOriginal.ts`.
|
|
44
|
+
*/
|
|
45
|
+
originalUrl?: string;
|
|
36
46
|
/** Video poster frame for the idle tile. */
|
|
37
47
|
posterUrl?: string;
|
|
38
48
|
/** Range-streamable video source when it differs from `url` (signed stream endpoints). */
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* no controls rather than nothing at all: a relative who cannot change sharing
|
|
28
28
|
* still benefits from knowing the record is shared with their cousin.
|
|
29
29
|
*/
|
|
30
|
+
import { dayKeyOf } from "cwip/calendar";
|
|
30
31
|
import { useState } from "react";
|
|
31
32
|
import {
|
|
32
33
|
describeAccess,
|
|
@@ -243,7 +244,7 @@ export function SharePanel({
|
|
|
243
244
|
<ol className="mt-1 flex flex-col gap-1 text-[var(--muted-foreground)] text-sm">
|
|
244
245
|
{events.map((event) => (
|
|
245
246
|
<li key={event.id}>
|
|
246
|
-
<span className="tabular-nums">{event.at
|
|
247
|
+
<span className="tabular-nums">{dayKeyOf(Date.parse(event.at))}</span>{" "}
|
|
247
248
|
{describeShareEvent(event, labelFor(event.subject))}
|
|
248
249
|
</li>
|
|
249
250
|
))}
|