@ai-matrx/capture 0.1.2 → 0.2.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 +19 -0
- package/README.md +61 -0
- package/dist/index.d.cts +0 -2
- package/dist/index.d.ts +0 -2
- package/dist/react.cjs +1334 -416
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +149 -5
- package/dist/react.d.ts +149 -5
- package/dist/react.js +1339 -409
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
package/dist/react.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/components/CameraCapture.tsx
|
|
4
|
-
import { useCallback as
|
|
4
|
+
import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef3, useState as useState5 } from "react";
|
|
5
5
|
import {
|
|
6
6
|
Grip,
|
|
7
7
|
Grid3x3,
|
|
8
8
|
Proportions,
|
|
9
9
|
SunMedium,
|
|
10
10
|
Timer,
|
|
11
|
-
X as
|
|
11
|
+
X as X4,
|
|
12
12
|
Zap,
|
|
13
13
|
ZapOff,
|
|
14
14
|
RefreshCw
|
|
@@ -20,6 +20,20 @@ function cn(...inputs) {
|
|
|
20
20
|
return twMerge(inputs.filter(Boolean).join(" "));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// src/safe-area.ts
|
|
24
|
+
var safeTop = {
|
|
25
|
+
paddingTop: "env(safe-area-inset-top, 0px)"
|
|
26
|
+
};
|
|
27
|
+
var safeBottom = {
|
|
28
|
+
paddingBottom: "env(safe-area-inset-bottom, 0px)"
|
|
29
|
+
};
|
|
30
|
+
var safeMarginTop = {
|
|
31
|
+
marginTop: "env(safe-area-inset-top, 0px)"
|
|
32
|
+
};
|
|
33
|
+
var safeMarginBottom = {
|
|
34
|
+
marginBottom: "env(safe-area-inset-bottom, 0px)"
|
|
35
|
+
};
|
|
36
|
+
|
|
23
37
|
// src/hooks/useTrackControls.ts
|
|
24
38
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
25
39
|
var ZOOM_LADDER = [0.5, 1, 2, 4, 8];
|
|
@@ -35,20 +49,31 @@ function useTrackControls(stream) {
|
|
|
35
49
|
setCaps(null);
|
|
36
50
|
return;
|
|
37
51
|
}
|
|
52
|
+
const timers = [];
|
|
38
53
|
const read = () => {
|
|
39
54
|
try {
|
|
40
|
-
|
|
55
|
+
const next = track.getCapabilities();
|
|
56
|
+
setCaps(next);
|
|
41
57
|
const settings = track.getSettings();
|
|
42
58
|
if (typeof settings.zoom === "number") setZoomState(settings.zoom);
|
|
43
59
|
if (typeof settings.exposureCompensation === "number")
|
|
44
60
|
setExposureState(settings.exposureCompensation);
|
|
61
|
+
return next.torch === true || next.zoom !== void 0 || next.exposureCompensation !== void 0;
|
|
45
62
|
} catch {
|
|
46
63
|
setCaps(null);
|
|
64
|
+
return false;
|
|
47
65
|
}
|
|
48
66
|
};
|
|
49
|
-
read()
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
if (!read()) {
|
|
68
|
+
for (const delay of [500, 1200, 2500, 5e3]) {
|
|
69
|
+
timers.push(
|
|
70
|
+
window.setTimeout(() => {
|
|
71
|
+
if (read()) timers.forEach((t) => window.clearTimeout(t));
|
|
72
|
+
}, delay)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return () => timers.forEach((t) => window.clearTimeout(t));
|
|
52
77
|
}, [track]);
|
|
53
78
|
const torchSupported = caps?.torch === true;
|
|
54
79
|
const toggleTorch = useCallback(() => {
|
|
@@ -295,25 +320,32 @@ function OptionsGridPanel({
|
|
|
295
320
|
className: "absolute inset-0 bg-black/70"
|
|
296
321
|
}
|
|
297
322
|
),
|
|
298
|
-
/* @__PURE__ */ jsx4(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
323
|
+
/* @__PURE__ */ jsx4(
|
|
324
|
+
"div",
|
|
325
|
+
{
|
|
326
|
+
className: "absolute inset-x-3 bottom-3 rounded-[2.5rem] bg-[#3a3a3c]/95 px-6 py-8 shadow-2xl",
|
|
327
|
+
style: { marginBottom: "env(safe-area-inset-bottom, 0px)" },
|
|
328
|
+
children: /* @__PURE__ */ jsx4("div", { className: "grid grid-cols-3 gap-x-4 gap-y-7", children: tiles.map((tile) => /* @__PURE__ */ jsxs3("div", { className: "flex flex-col items-center gap-2.5", children: [
|
|
329
|
+
/* @__PURE__ */ jsx4(
|
|
330
|
+
"button",
|
|
331
|
+
{
|
|
332
|
+
type: "button",
|
|
333
|
+
onClick: tile.onPress,
|
|
334
|
+
disabled: tile.disabled,
|
|
335
|
+
"aria-label": tile.label,
|
|
336
|
+
"aria-pressed": tile.active === true,
|
|
337
|
+
className: cn(
|
|
338
|
+
"flex h-16 w-16 touch-manipulation items-center justify-center rounded-full bg-[#2c2c2e] transition-colors",
|
|
339
|
+
tile.active ? "text-[#FFCC00]" : "text-white",
|
|
340
|
+
tile.disabled && "opacity-40"
|
|
341
|
+
),
|
|
342
|
+
children: tile.icon
|
|
343
|
+
}
|
|
311
344
|
),
|
|
312
|
-
children: tile.
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
] }, tile.id)) }) })
|
|
345
|
+
/* @__PURE__ */ jsx4("span", { className: "text-[13px] font-semibold uppercase tracking-[0.14em] text-white", children: tile.valueLabel ? `${tile.label} ${tile.valueLabel}` : tile.label })
|
|
346
|
+
] }, tile.id)) })
|
|
347
|
+
}
|
|
348
|
+
)
|
|
317
349
|
] });
|
|
318
350
|
}
|
|
319
351
|
|
|
@@ -341,39 +373,46 @@ function CaptureSheet({
|
|
|
341
373
|
className: "absolute inset-0 bg-black/30"
|
|
342
374
|
}
|
|
343
375
|
),
|
|
344
|
-
/* @__PURE__ */ jsxs4(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
"
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
className:
|
|
368
|
-
"
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
376
|
+
/* @__PURE__ */ jsxs4(
|
|
377
|
+
"div",
|
|
378
|
+
{
|
|
379
|
+
className: "relative mx-2 mb-2 rounded-[2rem] bg-[#f2f2f7] px-6 pb-6 pt-5 text-black shadow-2xl",
|
|
380
|
+
style: { marginBottom: "calc(0.5rem + env(safe-area-inset-bottom, 0px))" },
|
|
381
|
+
children: [
|
|
382
|
+
/* @__PURE__ */ jsx5(
|
|
383
|
+
"button",
|
|
384
|
+
{
|
|
385
|
+
type: "button",
|
|
386
|
+
onClick: onClose,
|
|
387
|
+
"aria-label": "Close",
|
|
388
|
+
className: "absolute right-4 top-4 flex h-9 w-9 items-center justify-center rounded-full bg-black/5 text-black/70 transition-colors hover:bg-black/10",
|
|
389
|
+
children: /* @__PURE__ */ jsx5(X, { className: "h-5 w-5", strokeWidth: 2.5 })
|
|
390
|
+
}
|
|
391
|
+
),
|
|
392
|
+
variant === "busy" ? /* @__PURE__ */ jsxs4("div", { className: "flex min-h-[220px] items-center justify-center gap-2.5", children: [
|
|
393
|
+
/* @__PURE__ */ jsx5(Loader2, { className: "h-5 w-5 animate-spin text-black/50" }),
|
|
394
|
+
/* @__PURE__ */ jsx5("span", { className: "text-[17px] font-medium text-black/80", children: busyLabel })
|
|
395
|
+
] }) : /* @__PURE__ */ jsxs4("div", { className: "pt-4", children: [
|
|
396
|
+
icon && /* @__PURE__ */ jsx5("div", { className: "mb-5 text-[#0a84ff]", children: icon }),
|
|
397
|
+
title && /* @__PURE__ */ jsx5("h2", { className: "mb-2 text-[26px] font-bold leading-tight", children: title }),
|
|
398
|
+
body && /* @__PURE__ */ jsx5("div", { className: "text-[17px] leading-snug text-black/85", children: body }),
|
|
399
|
+
actions.length > 0 && /* @__PURE__ */ jsx5("div", { className: "mt-7 flex flex-col gap-3", children: actions.map((action) => /* @__PURE__ */ jsx5(
|
|
400
|
+
"button",
|
|
401
|
+
{
|
|
402
|
+
type: "button",
|
|
403
|
+
onClick: action.onPress,
|
|
404
|
+
className: cn(
|
|
405
|
+
"h-[50px] w-full touch-manipulation rounded-full text-[17px] font-semibold transition-transform active:scale-[0.98]",
|
|
406
|
+
(action.kind ?? "primary") === "primary" ? "bg-[#0a84ff] text-white" : "bg-black/[0.06] text-[#0a84ff]"
|
|
407
|
+
),
|
|
408
|
+
children: action.label
|
|
409
|
+
},
|
|
410
|
+
action.label
|
|
411
|
+
)) })
|
|
412
|
+
] })
|
|
413
|
+
]
|
|
414
|
+
}
|
|
415
|
+
)
|
|
377
416
|
] });
|
|
378
417
|
}
|
|
379
418
|
|
|
@@ -403,308 +442,509 @@ function CountdownOverlay({ seconds }) {
|
|
|
403
442
|
) });
|
|
404
443
|
}
|
|
405
444
|
|
|
406
|
-
// src/
|
|
445
|
+
// src/media/media-cache.ts
|
|
446
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
447
|
+
var MAX_ENTRIES = 80;
|
|
448
|
+
var cache = /* @__PURE__ */ new Map();
|
|
449
|
+
var listeners = /* @__PURE__ */ new Map();
|
|
450
|
+
function notify(key) {
|
|
451
|
+
listeners.get(key)?.forEach((fn) => fn());
|
|
452
|
+
}
|
|
453
|
+
function evictIfNeeded() {
|
|
454
|
+
while (cache.size > MAX_ENTRIES) {
|
|
455
|
+
const oldest = cache.keys().next().value;
|
|
456
|
+
if (oldest === void 0) return;
|
|
457
|
+
const entry = cache.get(oldest);
|
|
458
|
+
cache.delete(oldest);
|
|
459
|
+
if (entry?.revoke && entry.url) URL.revokeObjectURL(entry.url);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function touch(key, entry) {
|
|
463
|
+
cache.delete(key);
|
|
464
|
+
cache.set(key, entry);
|
|
465
|
+
}
|
|
466
|
+
function getMediaUrl(item) {
|
|
467
|
+
if (item.src) return item.src;
|
|
468
|
+
if (!item.resolve) return null;
|
|
469
|
+
const existing = cache.get(item.key);
|
|
470
|
+
if (existing) {
|
|
471
|
+
touch(item.key, existing);
|
|
472
|
+
if (existing.url || existing.error) return existing.url;
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
const entry = { url: null, revoke: false, promise: null, error: false };
|
|
476
|
+
cache.set(item.key, entry);
|
|
477
|
+
evictIfNeeded();
|
|
478
|
+
entry.promise = item.resolve().then((resolved) => {
|
|
479
|
+
const url = typeof resolved === "string" ? resolved : resolved.url;
|
|
480
|
+
entry.revoke = typeof resolved === "object" && resolved.revoke === true;
|
|
481
|
+
entry.url = url;
|
|
482
|
+
entry.promise = null;
|
|
483
|
+
notify(item.key);
|
|
484
|
+
return url;
|
|
485
|
+
}).catch(() => {
|
|
486
|
+
entry.error = true;
|
|
487
|
+
entry.promise = null;
|
|
488
|
+
notify(item.key);
|
|
489
|
+
return null;
|
|
490
|
+
});
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
function invalidateMedia(key) {
|
|
494
|
+
const entry = cache.get(key);
|
|
495
|
+
cache.delete(key);
|
|
496
|
+
if (entry?.revoke && entry.url) URL.revokeObjectURL(entry.url);
|
|
497
|
+
notify(key);
|
|
498
|
+
}
|
|
499
|
+
function useMediaUrl(item) {
|
|
500
|
+
const [, bump] = useState2(0);
|
|
501
|
+
const key = item?.key ?? null;
|
|
502
|
+
useEffect2(() => {
|
|
503
|
+
if (!key) return;
|
|
504
|
+
const set = listeners.get(key) ?? /* @__PURE__ */ new Set();
|
|
505
|
+
const fn = () => bump((n) => n + 1);
|
|
506
|
+
set.add(fn);
|
|
507
|
+
listeners.set(key, set);
|
|
508
|
+
return () => {
|
|
509
|
+
set.delete(fn);
|
|
510
|
+
if (set.size === 0) listeners.delete(key);
|
|
511
|
+
};
|
|
512
|
+
}, [key]);
|
|
513
|
+
if (!item) return null;
|
|
514
|
+
return getMediaUrl(item);
|
|
515
|
+
}
|
|
516
|
+
function primeMedia(items) {
|
|
517
|
+
for (const item of items) {
|
|
518
|
+
if (item) void getMediaUrl(item);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/components/CaptureFilmstrip.tsx
|
|
407
523
|
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
408
|
-
function
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
524
|
+
function CaptureFilmstrip({
|
|
525
|
+
items,
|
|
526
|
+
onOpen,
|
|
527
|
+
limit = 12
|
|
528
|
+
}) {
|
|
529
|
+
if (items.length === 0) return null;
|
|
530
|
+
return /* @__PURE__ */ jsx8("div", { className: "flex items-center gap-1.5 overflow-x-auto py-1", children: items.slice(-limit).map((item) => /* @__PURE__ */ jsxs6(
|
|
531
|
+
"button",
|
|
532
|
+
{
|
|
533
|
+
type: "button",
|
|
534
|
+
onClick: () => onOpen(item),
|
|
535
|
+
"aria-label": "View capture",
|
|
536
|
+
className: cn(
|
|
537
|
+
"relative h-12 w-9 shrink-0 touch-manipulation overflow-hidden rounded bg-white/10",
|
|
538
|
+
item.accent && "ring-2 ring-inset ring-amber-400"
|
|
539
|
+
),
|
|
540
|
+
children: [
|
|
541
|
+
/* @__PURE__ */ jsx8(FilmstripThumb, { item }),
|
|
542
|
+
item.status === "uploading" && /* @__PURE__ */ jsx8("span", { className: "absolute inset-0 flex items-center justify-center bg-black/40", children: /* @__PURE__ */ jsx8("span", { className: "h-3.5 w-3.5 animate-spin rounded-full border-2 border-white/40 border-t-white" }) }),
|
|
543
|
+
item.status === "error" && /* @__PURE__ */ jsx8("span", { className: "absolute inset-0 rounded ring-2 ring-inset ring-red-500" })
|
|
544
|
+
]
|
|
545
|
+
},
|
|
546
|
+
item.key
|
|
547
|
+
)) });
|
|
412
548
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
549
|
+
function FilmstripThumb({ item }) {
|
|
550
|
+
const url = useMediaUrl(item);
|
|
551
|
+
if (item.kind === "audio") {
|
|
552
|
+
return /* @__PURE__ */ jsx8("span", { className: "flex h-full w-full items-center justify-center", children: /* @__PURE__ */ jsxs6(
|
|
553
|
+
"svg",
|
|
554
|
+
{
|
|
555
|
+
viewBox: "0 0 24 24",
|
|
556
|
+
className: "h-4 w-4 stroke-white/80",
|
|
557
|
+
fill: "none",
|
|
558
|
+
strokeWidth: 2,
|
|
559
|
+
strokeLinecap: "round",
|
|
560
|
+
"aria-hidden": true,
|
|
561
|
+
children: [
|
|
562
|
+
/* @__PURE__ */ jsx8("path", { d: "M12 3v10" }),
|
|
563
|
+
/* @__PURE__ */ jsx8("path", { d: "M8 7v4M16 7v4M4 9v1M20 9v1" }),
|
|
564
|
+
/* @__PURE__ */ jsx8("circle", { cx: "12", cy: "17", r: "3" })
|
|
565
|
+
]
|
|
566
|
+
}
|
|
567
|
+
) });
|
|
568
|
+
}
|
|
569
|
+
if (!url) return /* @__PURE__ */ jsx8("span", { className: "block h-full w-full bg-white/5" });
|
|
570
|
+
if (item.kind === "video") {
|
|
571
|
+
return /* @__PURE__ */ jsxs6("span", { className: "relative block h-full w-full", children: [
|
|
572
|
+
/* @__PURE__ */ jsx8(
|
|
573
|
+
"video",
|
|
574
|
+
{
|
|
575
|
+
src: url,
|
|
576
|
+
muted: true,
|
|
577
|
+
playsInline: true,
|
|
578
|
+
preload: "metadata",
|
|
579
|
+
className: "h-full w-full object-cover"
|
|
580
|
+
}
|
|
581
|
+
),
|
|
582
|
+
/* @__PURE__ */ jsx8(
|
|
583
|
+
"svg",
|
|
584
|
+
{
|
|
585
|
+
viewBox: "0 0 24 24",
|
|
586
|
+
className: "absolute inset-0 m-auto h-4 w-4 fill-white drop-shadow",
|
|
587
|
+
"aria-hidden": true,
|
|
588
|
+
children: /* @__PURE__ */ jsx8("path", { d: "M8 5v14l11-7z" })
|
|
589
|
+
}
|
|
590
|
+
)
|
|
591
|
+
] });
|
|
592
|
+
}
|
|
593
|
+
return /* @__PURE__ */ jsx8("img", { src: url, alt: "", className: "h-full w-full object-cover" });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/components/MediaViewer.tsx
|
|
597
|
+
import {
|
|
598
|
+
useCallback as useCallback2,
|
|
599
|
+
useEffect as useEffect3,
|
|
600
|
+
useMemo as useMemo2,
|
|
601
|
+
useRef,
|
|
602
|
+
useState as useState3
|
|
603
|
+
} from "react";
|
|
604
|
+
import { ChevronLeft, ChevronRight, Pencil, Trash2, X as X2 } from "lucide-react";
|
|
605
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
606
|
+
var SWIPE_TRIGGER_PX = 40;
|
|
607
|
+
var SWIPE_TRIGGER_VELOCITY = 0.25;
|
|
608
|
+
var DISMISS_TRIGGER_PX = 110;
|
|
609
|
+
var DIRECTION_LOCK_PX = 8;
|
|
610
|
+
function MediaViewer({
|
|
611
|
+
items,
|
|
612
|
+
initialKey,
|
|
420
613
|
onClose,
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
614
|
+
onDelete,
|
|
615
|
+
onEdit,
|
|
616
|
+
deleteConsequence = "It is removed from this capture for everyone.",
|
|
617
|
+
keysDisabled = false
|
|
425
618
|
}) {
|
|
426
|
-
const
|
|
427
|
-
const [
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
const [
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
return;
|
|
619
|
+
const count = items.length;
|
|
620
|
+
const [index, setIndex] = useState3(() => {
|
|
621
|
+
const i = items.findIndex((m) => m.key === initialKey);
|
|
622
|
+
return i >= 0 ? i : 0;
|
|
623
|
+
});
|
|
624
|
+
const clamped = Math.min(index, count - 1);
|
|
625
|
+
const current = count > 0 ? items[clamped] : void 0;
|
|
626
|
+
const [confirmingDelete, setConfirmingDelete] = useState3(false);
|
|
627
|
+
const dragRef = useRef(null);
|
|
628
|
+
const [drag, setDrag] = useState3(null);
|
|
629
|
+
const [settling, setSettling] = useState3(false);
|
|
630
|
+
const go = useCallback2(
|
|
631
|
+
(dir) => {
|
|
632
|
+
setIndex((i) => Math.max(0, Math.min(count - 1, i + dir)));
|
|
633
|
+
setDrag(null);
|
|
634
|
+
setSettling(true);
|
|
635
|
+
},
|
|
636
|
+
[count]
|
|
637
|
+
);
|
|
638
|
+
useEffect3(() => {
|
|
639
|
+
if (count === 0) {
|
|
640
|
+
const t = setTimeout(onClose, 0);
|
|
641
|
+
return () => clearTimeout(t);
|
|
450
642
|
}
|
|
451
|
-
if (
|
|
452
|
-
|
|
453
|
-
|
|
643
|
+
if (index >= count) setIndex(count - 1);
|
|
644
|
+
}, [count, index, onClose]);
|
|
645
|
+
useEffect3(() => {
|
|
646
|
+
setConfirmingDelete(false);
|
|
647
|
+
}, [clamped]);
|
|
648
|
+
useEffect3(() => {
|
|
649
|
+
primeMedia([
|
|
650
|
+
items[clamped - 2],
|
|
651
|
+
items[clamped - 1],
|
|
652
|
+
items[clamped + 1],
|
|
653
|
+
items[clamped + 2]
|
|
654
|
+
]);
|
|
655
|
+
}, [items, clamped]);
|
|
656
|
+
useEffect3(() => {
|
|
657
|
+
if (keysDisabled) return;
|
|
658
|
+
const onKey = (e) => {
|
|
659
|
+
if (e.key === "ArrowRight") go(1);
|
|
660
|
+
else if (e.key === "ArrowLeft") go(-1);
|
|
661
|
+
else if (e.key === "Escape") onClose();
|
|
662
|
+
};
|
|
663
|
+
window.addEventListener("keydown", onKey);
|
|
664
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
665
|
+
}, [go, onClose, keysDisabled]);
|
|
666
|
+
const onPointerDown = useCallback2((e) => {
|
|
667
|
+
if (e.target.closest("video, audio")) return;
|
|
668
|
+
dragRef.current = {
|
|
669
|
+
x0: e.clientX,
|
|
670
|
+
y0: e.clientY,
|
|
671
|
+
t0: performance.now(),
|
|
672
|
+
axis: null,
|
|
673
|
+
pointerId: e.pointerId
|
|
674
|
+
};
|
|
675
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
676
|
+
setSettling(false);
|
|
677
|
+
}, []);
|
|
678
|
+
const onPointerMove = useCallback2((e) => {
|
|
679
|
+
const d = dragRef.current;
|
|
680
|
+
if (!d || e.pointerId !== d.pointerId) return;
|
|
681
|
+
const dx = e.clientX - d.x0;
|
|
682
|
+
const dy = e.clientY - d.y0;
|
|
683
|
+
if (d.axis === null) {
|
|
684
|
+
if (Math.abs(dx) < DIRECTION_LOCK_PX && Math.abs(dy) < DIRECTION_LOCK_PX)
|
|
685
|
+
return;
|
|
686
|
+
d.axis = Math.abs(dx) >= Math.abs(dy) ? "x" : "y";
|
|
454
687
|
}
|
|
455
|
-
|
|
456
|
-
setCountdown(remaining);
|
|
457
|
-
countdownRef.current = setInterval(() => {
|
|
458
|
-
remaining -= 1;
|
|
459
|
-
if (remaining <= 0) {
|
|
460
|
-
clearCountdown();
|
|
461
|
-
engine.onCapturePhoto({ aspect });
|
|
462
|
-
} else {
|
|
463
|
-
setCountdown(remaining);
|
|
464
|
-
}
|
|
465
|
-
}, 1e3);
|
|
466
|
-
}, [mode, engine, countdown, timerSetting, aspect, clearCountdown]);
|
|
467
|
-
const cycleTimer = useCallback2(() => {
|
|
468
|
-
setTimerSetting((t) => t === 0 ? 3 : t === 3 ? 10 : 0);
|
|
688
|
+
setDrag(d.axis === "x" ? { dx, dy: 0 } : { dx: 0, dy });
|
|
469
689
|
}, []);
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
690
|
+
const endDrag = useCallback2(
|
|
691
|
+
(e) => {
|
|
692
|
+
const d = dragRef.current;
|
|
693
|
+
if (!d || e.pointerId !== d.pointerId) return;
|
|
694
|
+
dragRef.current = null;
|
|
695
|
+
const dx = e.clientX - d.x0;
|
|
696
|
+
const dy = e.clientY - d.y0;
|
|
697
|
+
const dt = Math.max(1, performance.now() - d.t0);
|
|
698
|
+
const vx = dx / dt;
|
|
699
|
+
if (d.axis === "y" && Math.abs(dy) > DISMISS_TRIGGER_PX) {
|
|
700
|
+
onClose();
|
|
701
|
+
return;
|
|
478
702
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
id: "grid",
|
|
490
|
-
label: "Grid",
|
|
491
|
-
icon: /* @__PURE__ */ jsx8(Grid3x3, { className: "h-6 w-6" }),
|
|
492
|
-
active: gridOn,
|
|
493
|
-
onPress: () => setGridOn((g) => !g)
|
|
494
|
-
},
|
|
495
|
-
// Aspect applies to PHOTO output (center-cropped from the full sensor).
|
|
496
|
-
{
|
|
497
|
-
id: "aspect",
|
|
498
|
-
label: "Aspect",
|
|
499
|
-
icon: /* @__PURE__ */ jsx8(Proportions, { className: "h-6 w-6" }),
|
|
500
|
-
active: aspect !== "full",
|
|
501
|
-
valueLabel: aspect === "full" ? void 0 : aspect,
|
|
502
|
-
onPress: () => setAspect(
|
|
503
|
-
(a) => ASPECT_CYCLE[(ASPECT_CYCLE.indexOf(a) + 1) % ASPECT_CYCLE.length] ?? "full"
|
|
504
|
-
)
|
|
505
|
-
},
|
|
506
|
-
...controls.exposureSupported ? [
|
|
507
|
-
{
|
|
508
|
-
id: "exposure",
|
|
509
|
-
label: "Exposure",
|
|
510
|
-
icon: /* @__PURE__ */ jsx8(SunMedium, { className: "h-6 w-6" }),
|
|
511
|
-
active: controls.exposure !== 0 || exposureOpen,
|
|
512
|
-
valueLabel: controls.exposure === 0 ? void 0 : `${controls.exposure > 0 ? "+" : ""}${controls.exposure}`,
|
|
513
|
-
onPress: () => setExposureOpen((o) => !o)
|
|
703
|
+
if (d.axis === "x") {
|
|
704
|
+
const flick = Math.abs(dx) > 10 && Math.abs(vx) > SWIPE_TRIGGER_VELOCITY;
|
|
705
|
+
if ((dx < -SWIPE_TRIGGER_PX || flick && dx < 0) && clamped < count - 1) {
|
|
706
|
+
go(1);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if ((dx > SWIPE_TRIGGER_PX || flick && dx > 0) && clamped > 0) {
|
|
710
|
+
go(-1);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
514
713
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
714
|
+
setDrag(null);
|
|
715
|
+
setSettling(true);
|
|
716
|
+
},
|
|
717
|
+
[clamped, count, go, onClose]
|
|
718
|
+
);
|
|
719
|
+
const neighborKeys = useMemo2(() => {
|
|
720
|
+
const wanted = [];
|
|
721
|
+
for (const i of [clamped - 1, clamped + 1]) {
|
|
722
|
+
const m = items[i];
|
|
723
|
+
if (m) wanted.push(m);
|
|
724
|
+
}
|
|
725
|
+
return wanted;
|
|
726
|
+
}, [items, clamped]);
|
|
727
|
+
if (!current) return null;
|
|
728
|
+
const canEdit = onEdit && current.kind === "photo";
|
|
729
|
+
return /* @__PURE__ */ jsxs7("div", { className: "absolute inset-0 z-40 flex flex-col bg-black", children: [
|
|
730
|
+
/* @__PURE__ */ jsxs7(
|
|
523
731
|
"div",
|
|
524
732
|
{
|
|
525
|
-
"
|
|
526
|
-
|
|
527
|
-
children:
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
maxHeight: "100%"
|
|
733
|
+
className: "absolute inset-x-0 top-0 z-20 flex items-center justify-between bg-gradient-to-b from-black/70 to-transparent p-3",
|
|
734
|
+
style: safeTop,
|
|
735
|
+
children: [
|
|
736
|
+
/* @__PURE__ */ jsx9(
|
|
737
|
+
"button",
|
|
738
|
+
{
|
|
739
|
+
type: "button",
|
|
740
|
+
onClick: onClose,
|
|
741
|
+
"aria-label": "Close viewer",
|
|
742
|
+
className: "flex h-10 w-10 touch-manipulation items-center justify-center rounded-full text-white transition-colors hover:bg-white/10",
|
|
743
|
+
children: /* @__PURE__ */ jsx9(X2, { className: "h-5 w-5" })
|
|
537
744
|
}
|
|
538
|
-
|
|
539
|
-
|
|
745
|
+
),
|
|
746
|
+
/* @__PURE__ */ jsxs7("span", { className: "rounded-full bg-black/50 px-3 py-1 text-sm tabular-nums text-white/90", children: [
|
|
747
|
+
clamped + 1,
|
|
748
|
+
" / ",
|
|
749
|
+
count
|
|
750
|
+
] }),
|
|
751
|
+
/* @__PURE__ */ jsxs7("span", { className: "flex items-center gap-1", children: [
|
|
752
|
+
canEdit ? /* @__PURE__ */ jsx9(
|
|
753
|
+
"button",
|
|
754
|
+
{
|
|
755
|
+
type: "button",
|
|
756
|
+
onClick: () => onEdit(current),
|
|
757
|
+
"aria-label": "Edit this photo",
|
|
758
|
+
className: "flex h-10 w-10 touch-manipulation items-center justify-center rounded-full text-white transition-colors hover:bg-white/10",
|
|
759
|
+
children: /* @__PURE__ */ jsx9(Pencil, { className: "h-5 w-5" })
|
|
760
|
+
}
|
|
761
|
+
) : null,
|
|
762
|
+
onDelete ? /* @__PURE__ */ jsx9(
|
|
763
|
+
"button",
|
|
764
|
+
{
|
|
765
|
+
type: "button",
|
|
766
|
+
onClick: () => setConfirmingDelete(true),
|
|
767
|
+
"aria-label": "Delete this file",
|
|
768
|
+
className: "flex h-10 w-10 touch-manipulation items-center justify-center rounded-full text-white transition-colors hover:bg-white/10",
|
|
769
|
+
children: /* @__PURE__ */ jsx9(Trash2, { className: "h-5 w-5" })
|
|
770
|
+
}
|
|
771
|
+
) : /* @__PURE__ */ jsx9("span", { className: "h-10 w-10", "aria-hidden": true })
|
|
772
|
+
] })
|
|
773
|
+
]
|
|
540
774
|
}
|
|
541
775
|
),
|
|
542
|
-
/* @__PURE__ */
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
"button",
|
|
776
|
+
/* @__PURE__ */ jsxs7("div", { className: "relative min-h-0 flex-1 overflow-hidden", children: [
|
|
777
|
+
/* @__PURE__ */ jsx9(
|
|
778
|
+
"div",
|
|
546
779
|
{
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
780
|
+
className: "absolute inset-0 touch-none",
|
|
781
|
+
style: {
|
|
782
|
+
transform: drag ? `translate(${drag.dx}px, ${drag.dy}px)` : "translate(0px, 0px)",
|
|
783
|
+
transition: settling ? "transform 220ms cubic-bezier(0.2, 0.8, 0.3, 1)" : "none",
|
|
784
|
+
opacity: drag?.dy ? Math.max(0.4, 1 - Math.abs(drag.dy) / 400) : 1
|
|
785
|
+
},
|
|
786
|
+
onPointerDown,
|
|
787
|
+
onPointerMove,
|
|
788
|
+
onPointerUp: endDrag,
|
|
789
|
+
onPointerCancel: endDrag,
|
|
790
|
+
children: /* @__PURE__ */ jsx9(ViewerSlide, { item: current, active: true }, current.key)
|
|
791
|
+
}
|
|
792
|
+
),
|
|
793
|
+
/* @__PURE__ */ jsx9(
|
|
794
|
+
"div",
|
|
795
|
+
{
|
|
796
|
+
"aria-hidden": true,
|
|
797
|
+
className: "pointer-events-none absolute inset-0 opacity-0",
|
|
798
|
+
children: neighborKeys.map((m) => /* @__PURE__ */ jsx9("div", { className: "absolute inset-0", children: /* @__PURE__ */ jsx9(ViewerSlide, { item: m, active: false }) }, m.key))
|
|
552
799
|
}
|
|
553
|
-
)
|
|
554
|
-
/* @__PURE__ */
|
|
555
|
-
controls.torchSupported && /* @__PURE__ */ jsx8(
|
|
800
|
+
),
|
|
801
|
+
clamped > 0 && /* @__PURE__ */ jsx9(
|
|
556
802
|
"button",
|
|
557
803
|
{
|
|
558
804
|
type: "button",
|
|
559
|
-
onClick:
|
|
560
|
-
"aria-label":
|
|
561
|
-
"
|
|
562
|
-
className:
|
|
563
|
-
"flex h-10 w-10 shrink-0 touch-manipulation items-center justify-center rounded-full transition-colors",
|
|
564
|
-
controls.torchOn ? "text-[#FFCC00]" : "text-white hover:bg-white/10"
|
|
565
|
-
),
|
|
566
|
-
children: /* @__PURE__ */ jsx8(
|
|
567
|
-
Zap,
|
|
568
|
-
{
|
|
569
|
-
className: "h-5 w-5",
|
|
570
|
-
fill: controls.torchOn ? "currentColor" : "none"
|
|
571
|
-
}
|
|
572
|
-
)
|
|
805
|
+
onClick: () => go(-1),
|
|
806
|
+
"aria-label": "Previous file",
|
|
807
|
+
className: "absolute left-2 top-1/2 z-20 hidden h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full bg-black/40 text-white hover:bg-black/60 sm:flex",
|
|
808
|
+
children: /* @__PURE__ */ jsx9(ChevronLeft, { className: "h-6 w-6" })
|
|
573
809
|
}
|
|
574
810
|
),
|
|
575
|
-
|
|
576
|
-
/* @__PURE__ */ jsx8(
|
|
811
|
+
clamped < count - 1 && /* @__PURE__ */ jsx9(
|
|
577
812
|
"button",
|
|
578
813
|
{
|
|
579
814
|
type: "button",
|
|
580
|
-
onClick: () =>
|
|
581
|
-
"aria-label": "
|
|
582
|
-
"
|
|
583
|
-
className:
|
|
584
|
-
"flex h-10 w-10 shrink-0 touch-manipulation items-center justify-center rounded-full transition-colors",
|
|
585
|
-
optionsOpen ? "bg-white/20 text-white" : "text-white hover:bg-white/10"
|
|
586
|
-
),
|
|
587
|
-
children: /* @__PURE__ */ jsx8(Grip, { className: "h-5 w-5" })
|
|
815
|
+
onClick: () => go(1),
|
|
816
|
+
"aria-label": "Next file",
|
|
817
|
+
className: "absolute right-2 top-1/2 z-20 hidden h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full bg-black/40 text-white hover:bg-black/60 sm:flex",
|
|
818
|
+
children: /* @__PURE__ */ jsx9(ChevronRight, { className: "h-6 w-6" })
|
|
588
819
|
}
|
|
589
820
|
)
|
|
590
|
-
] }) }),
|
|
591
|
-
/* @__PURE__ */ jsxs6("div", { className: "pointer-events-none absolute inset-x-0 top-[50px] z-20 mt-safe flex flex-col items-center gap-1.5", children: [
|
|
592
|
-
engine.recording && /* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-2 rounded-full bg-black/60 px-3 py-1.5 text-sm font-medium text-white", children: [
|
|
593
|
-
/* @__PURE__ */ jsx8("span", { className: "h-2.5 w-2.5 animate-pulse rounded-full bg-[#FF3B30]" }),
|
|
594
|
-
formatElapsed(engine.recordElapsedSeconds)
|
|
595
|
-
] }),
|
|
596
|
-
slots.statusChips
|
|
597
821
|
] }),
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
822
|
+
confirmingDelete && onDelete && /* @__PURE__ */ jsxs7(
|
|
823
|
+
"div",
|
|
824
|
+
{
|
|
825
|
+
className: "absolute inset-x-0 bottom-0 z-30 bg-black/85 px-4 pt-4 backdrop-blur-sm",
|
|
826
|
+
style: {
|
|
827
|
+
paddingBottom: "calc(1rem + env(safe-area-inset-bottom, 0px))"
|
|
828
|
+
},
|
|
829
|
+
children: [
|
|
830
|
+
/* @__PURE__ */ jsxs7("p", { className: "pb-3 text-center text-sm text-white/85", children: [
|
|
831
|
+
"Delete this ",
|
|
832
|
+
current.kind === "video" ? "video" : current.kind === "audio" ? "voice note" : "photo",
|
|
833
|
+
"? ",
|
|
834
|
+
deleteConsequence
|
|
835
|
+
] }),
|
|
836
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-center gap-3", children: [
|
|
837
|
+
/* @__PURE__ */ jsx9(
|
|
838
|
+
"button",
|
|
839
|
+
{
|
|
840
|
+
type: "button",
|
|
841
|
+
onClick: () => setConfirmingDelete(false),
|
|
842
|
+
className: "h-11 touch-manipulation rounded-full bg-white/15 px-6 text-sm font-medium text-white",
|
|
843
|
+
children: "Keep it"
|
|
844
|
+
}
|
|
845
|
+
),
|
|
846
|
+
/* @__PURE__ */ jsx9(
|
|
847
|
+
"button",
|
|
848
|
+
{
|
|
849
|
+
type: "button",
|
|
850
|
+
onClick: () => {
|
|
851
|
+
setConfirmingDelete(false);
|
|
852
|
+
onDelete(current);
|
|
853
|
+
},
|
|
854
|
+
className: "h-11 touch-manipulation rounded-full bg-[#FF3B30] px-6 text-sm font-semibold text-white",
|
|
855
|
+
children: "Delete"
|
|
856
|
+
}
|
|
857
|
+
)
|
|
858
|
+
] })
|
|
859
|
+
]
|
|
860
|
+
}
|
|
861
|
+
),
|
|
862
|
+
count > 1 && count <= 12 && /* @__PURE__ */ jsx9(
|
|
863
|
+
"div",
|
|
864
|
+
{
|
|
865
|
+
className: "pointer-events-none absolute inset-x-0 bottom-4 z-20 flex justify-center gap-1.5",
|
|
866
|
+
style: safeMarginBottom,
|
|
867
|
+
children: items.map((m, i) => /* @__PURE__ */ jsx9(
|
|
868
|
+
"span",
|
|
869
|
+
{
|
|
870
|
+
className: cn(
|
|
871
|
+
"h-1.5 w-1.5 rounded-full transition-colors",
|
|
872
|
+
i === clamped ? "bg-white" : "bg-white/35"
|
|
873
|
+
)
|
|
874
|
+
},
|
|
875
|
+
m.key
|
|
876
|
+
))
|
|
877
|
+
}
|
|
878
|
+
)
|
|
879
|
+
] });
|
|
880
|
+
}
|
|
881
|
+
function ViewerSlide({
|
|
882
|
+
item,
|
|
883
|
+
active
|
|
884
|
+
}) {
|
|
885
|
+
const url = useMediaUrl(item);
|
|
886
|
+
if (!url) {
|
|
887
|
+
return active ? /* @__PURE__ */ jsx9("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsx9("span", { className: "h-8 w-8 animate-spin rounded-full border-2 border-white/30 border-t-white" }) }) : null;
|
|
888
|
+
}
|
|
889
|
+
if (item.kind === "audio") {
|
|
890
|
+
if (!active) return null;
|
|
891
|
+
return /* @__PURE__ */ jsxs7("div", { className: "absolute inset-0 flex flex-col items-center justify-center gap-6 px-8", children: [
|
|
892
|
+
/* @__PURE__ */ jsxs7(
|
|
893
|
+
"svg",
|
|
601
894
|
{
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
895
|
+
viewBox: "0 0 24 24",
|
|
896
|
+
className: "h-12 w-12 stroke-white/80",
|
|
897
|
+
fill: "none",
|
|
898
|
+
strokeWidth: 1.5,
|
|
899
|
+
strokeLinecap: "round",
|
|
900
|
+
"aria-hidden": true,
|
|
901
|
+
children: [
|
|
902
|
+
/* @__PURE__ */ jsx9("path", { d: "M12 2v12" }),
|
|
903
|
+
/* @__PURE__ */ jsx9("path", { d: "M7 6v6M17 6v6M2 9v2M22 9v2" }),
|
|
904
|
+
/* @__PURE__ */ jsx9("circle", { cx: "12", cy: "19", r: "3" })
|
|
905
|
+
]
|
|
605
906
|
}
|
|
606
|
-
)
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
type: "range",
|
|
614
|
-
min: controls.exposureRange.min,
|
|
615
|
-
max: controls.exposureRange.max,
|
|
616
|
-
step: controls.exposureRange.step,
|
|
617
|
-
value: controls.exposure,
|
|
618
|
-
onChange: (e) => controls.setExposure(Number(e.target.value)),
|
|
619
|
-
"aria-label": "Exposure compensation",
|
|
620
|
-
className: "w-full accent-[#FFCC00]"
|
|
621
|
-
}
|
|
622
|
-
),
|
|
623
|
-
/* @__PURE__ */ jsxs6("span", { className: "w-8 shrink-0 text-right text-xs tabular-nums text-white", children: [
|
|
624
|
-
controls.exposure > 0 ? "+" : "",
|
|
625
|
-
controls.exposure
|
|
626
|
-
] })
|
|
627
|
-
] }),
|
|
628
|
-
/* @__PURE__ */ jsxs6("div", { className: "bg-black/65 px-3 pb-safe backdrop-blur-[2px]", children: [
|
|
629
|
-
slots.aboveModeSelector,
|
|
630
|
-
/* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-center gap-2 py-1.5", children: [
|
|
631
|
-
/* @__PURE__ */ jsx8(
|
|
632
|
-
ModeSelector,
|
|
633
|
-
{
|
|
634
|
-
mode,
|
|
635
|
-
onModeChange,
|
|
636
|
-
onUpload: engine.onUpload,
|
|
637
|
-
modeDisabled: engine.recording,
|
|
638
|
-
uploadDisabled: shutterDisabled && !blocked,
|
|
639
|
-
extraModes: slots.extraModes
|
|
640
|
-
}
|
|
641
|
-
),
|
|
642
|
-
slots.modeRowTrailing && /* @__PURE__ */ jsx8("div", { className: "shrink-0", children: slots.modeRowTrailing })
|
|
643
|
-
] }),
|
|
644
|
-
/* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between px-2 pb-1.5 pt-0.5", children: [
|
|
645
|
-
/* @__PURE__ */ jsx8("div", { className: "flex w-14 justify-start", children: /* @__PURE__ */ jsx8(
|
|
646
|
-
"button",
|
|
647
|
-
{
|
|
648
|
-
type: "button",
|
|
649
|
-
onClick: cloud.onOpenLibrary,
|
|
650
|
-
"aria-label": "Open your media library",
|
|
651
|
-
className: "h-11 w-11 touch-manipulation overflow-hidden rounded-xl bg-white/10 ring-1 ring-white/25 transition-transform active:scale-95",
|
|
652
|
-
children: cloud.recentsThumb ?? /* @__PURE__ */ jsx8("span", { className: "block h-full w-full bg-white/5" })
|
|
653
|
-
}
|
|
654
|
-
) }),
|
|
655
|
-
/* @__PURE__ */ jsx8(
|
|
656
|
-
ShutterButton,
|
|
657
|
-
{
|
|
658
|
-
mode,
|
|
659
|
-
recording: engine.recording,
|
|
660
|
-
disabled: shutterDisabled || blocked,
|
|
661
|
-
onPress: onShutter
|
|
662
|
-
}
|
|
663
|
-
),
|
|
664
|
-
/* @__PURE__ */ jsx8("div", { className: "flex w-14 justify-end", children: engine.onFlipCamera ? /* @__PURE__ */ jsx8(
|
|
665
|
-
"button",
|
|
666
|
-
{
|
|
667
|
-
type: "button",
|
|
668
|
-
onClick: engine.onFlipCamera,
|
|
669
|
-
"aria-label": "Switch camera",
|
|
670
|
-
className: "flex h-11 w-11 touch-manipulation items-center justify-center rounded-full bg-white/15 text-white transition-transform active:rotate-180 active:scale-95 duration-300",
|
|
671
|
-
children: /* @__PURE__ */ jsx8(RefreshCw, { className: "h-5 w-5" })
|
|
672
|
-
}
|
|
673
|
-
) : /* @__PURE__ */ jsx8("span", { className: "h-11 w-11" }) })
|
|
674
|
-
] })
|
|
675
|
-
] })
|
|
676
|
-
] }),
|
|
677
|
-
/* @__PURE__ */ jsx8(
|
|
678
|
-
OptionsGridPanel,
|
|
679
|
-
{
|
|
680
|
-
open: optionsOpen && !controlsHidden,
|
|
681
|
-
onClose: () => setOptionsOpen(false),
|
|
682
|
-
tiles
|
|
683
|
-
}
|
|
684
|
-
),
|
|
685
|
-
blocked && blockedSheet && !blockedDismissed && /* @__PURE__ */ jsx8(
|
|
686
|
-
CaptureSheet,
|
|
907
|
+
),
|
|
908
|
+
/* @__PURE__ */ jsx9("audio", { src: url, controls: true, className: "w-full max-w-sm" })
|
|
909
|
+
] });
|
|
910
|
+
}
|
|
911
|
+
if (item.kind === "video") {
|
|
912
|
+
return /* @__PURE__ */ jsx9(
|
|
913
|
+
"video",
|
|
687
914
|
{
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
915
|
+
src: url,
|
|
916
|
+
controls: active,
|
|
917
|
+
playsInline: true,
|
|
918
|
+
preload: active ? "auto" : "metadata",
|
|
919
|
+
muted: !active,
|
|
920
|
+
className: cn(
|
|
921
|
+
"absolute inset-0 h-full w-full object-contain",
|
|
922
|
+
active ? "pointer-events-auto" : "pointer-events-none"
|
|
923
|
+
)
|
|
693
924
|
}
|
|
694
|
-
)
|
|
695
|
-
|
|
696
|
-
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
return /* @__PURE__ */ jsx9(
|
|
928
|
+
"img",
|
|
929
|
+
{
|
|
930
|
+
src: url,
|
|
931
|
+
alt: "",
|
|
932
|
+
draggable: false,
|
|
933
|
+
decoding: active ? "auto" : "async",
|
|
934
|
+
className: "absolute inset-0 h-full w-full select-none object-contain"
|
|
935
|
+
}
|
|
936
|
+
);
|
|
697
937
|
}
|
|
698
938
|
|
|
699
939
|
// src/components/ImageEditSheet.tsx
|
|
700
940
|
import {
|
|
701
941
|
useCallback as useCallback3,
|
|
702
|
-
useEffect as
|
|
942
|
+
useEffect as useEffect4,
|
|
703
943
|
useRef as useRef2,
|
|
704
|
-
useState as
|
|
944
|
+
useState as useState4
|
|
705
945
|
} from "react";
|
|
706
946
|
import { Check, FlipHorizontal2, RotateCcw, X as X3 } from "lucide-react";
|
|
707
|
-
import { jsx as
|
|
947
|
+
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
708
948
|
var ASPECTS = [
|
|
709
949
|
{ id: "free", label: "Free", ratio: null },
|
|
710
950
|
{ id: "1:1", label: "Square", ratio: 1 },
|
|
@@ -719,42 +959,82 @@ function ImageEditSheet({
|
|
|
719
959
|
onClose,
|
|
720
960
|
onSave
|
|
721
961
|
}) {
|
|
722
|
-
const [
|
|
723
|
-
const [
|
|
724
|
-
const [
|
|
725
|
-
const [
|
|
726
|
-
const [
|
|
962
|
+
const [workingSrc, setWorkingSrc] = useState4(null);
|
|
963
|
+
const [aspect, setAspect] = useState4("free");
|
|
964
|
+
const [crop, setCrop] = useState4(FULL_CROP);
|
|
965
|
+
const [saving, setSaving] = useState4(false);
|
|
966
|
+
const [editError, setEditError] = useState4(null);
|
|
727
967
|
const stageRef = useRef2(null);
|
|
728
968
|
const imgRef = useRef2(null);
|
|
969
|
+
const ownedUrlsRef = useRef2([]);
|
|
729
970
|
const dragRef = useRef2(null);
|
|
730
|
-
|
|
971
|
+
const revokeOwned = useCallback3(() => {
|
|
972
|
+
for (const url of ownedUrlsRef.current) URL.revokeObjectURL(url);
|
|
973
|
+
ownedUrlsRef.current = [];
|
|
974
|
+
}, []);
|
|
975
|
+
useEffect4(() => {
|
|
731
976
|
if (open) {
|
|
732
|
-
|
|
733
|
-
setFlipped(false);
|
|
977
|
+
setWorkingSrc(null);
|
|
734
978
|
setAspect("free");
|
|
735
979
|
setCrop(FULL_CROP);
|
|
736
980
|
setSaving(false);
|
|
981
|
+
setEditError(null);
|
|
737
982
|
}
|
|
738
|
-
|
|
983
|
+
return revokeOwned;
|
|
984
|
+
}, [open, src, revokeOwned]);
|
|
985
|
+
const bake = useCallback3(
|
|
986
|
+
(op) => {
|
|
987
|
+
const img = imgRef.current;
|
|
988
|
+
if (!img || img.naturalWidth === 0 || saving) return;
|
|
989
|
+
try {
|
|
990
|
+
const swap = op === "rotate-left";
|
|
991
|
+
const canvas = document.createElement("canvas");
|
|
992
|
+
canvas.width = swap ? img.naturalHeight : img.naturalWidth;
|
|
993
|
+
canvas.height = swap ? img.naturalWidth : img.naturalHeight;
|
|
994
|
+
const ctx = canvas.getContext("2d");
|
|
995
|
+
if (!ctx) throw new Error("no 2d context");
|
|
996
|
+
ctx.translate(canvas.width / 2, canvas.height / 2);
|
|
997
|
+
if (swap) ctx.rotate(-Math.PI / 2);
|
|
998
|
+
else ctx.scale(-1, 1);
|
|
999
|
+
ctx.drawImage(img, -img.naturalWidth / 2, -img.naturalHeight / 2);
|
|
1000
|
+
canvas.toBlob(
|
|
1001
|
+
(blob) => {
|
|
1002
|
+
if (!blob) {
|
|
1003
|
+
setEditError(
|
|
1004
|
+
"This image is too large to process on this device."
|
|
1005
|
+
);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
revokeOwned();
|
|
1009
|
+
const url = URL.createObjectURL(blob);
|
|
1010
|
+
ownedUrlsRef.current.push(url);
|
|
1011
|
+
setWorkingSrc(url);
|
|
1012
|
+
setCrop(FULL_CROP);
|
|
1013
|
+
setAspect("free");
|
|
1014
|
+
setEditError(null);
|
|
1015
|
+
},
|
|
1016
|
+
"image/jpeg",
|
|
1017
|
+
0.95
|
|
1018
|
+
);
|
|
1019
|
+
} catch {
|
|
1020
|
+
setEditError("This image could not be edited on this device.");
|
|
1021
|
+
}
|
|
1022
|
+
},
|
|
1023
|
+
[saving, revokeOwned]
|
|
1024
|
+
);
|
|
739
1025
|
const applyAspect = useCallback3((preset) => {
|
|
740
1026
|
setAspect(preset);
|
|
741
1027
|
const ratio = ASPECTS.find((a) => a.id === preset)?.ratio ?? null;
|
|
742
1028
|
if (ratio === null) return;
|
|
743
1029
|
const img = imgRef.current;
|
|
744
1030
|
if (!img || img.naturalWidth === 0) return;
|
|
745
|
-
const
|
|
746
|
-
const iw = rotated ? img.naturalHeight : img.naturalWidth;
|
|
747
|
-
const ih = rotated ? img.naturalWidth : img.naturalHeight;
|
|
748
|
-
const imageRatio = iw / ih;
|
|
1031
|
+
const imageRatio = img.naturalWidth / img.naturalHeight;
|
|
749
1032
|
let w = 1;
|
|
750
1033
|
let h = 1;
|
|
751
1034
|
if (ratio > imageRatio) h = imageRatio / ratio;
|
|
752
1035
|
else w = ratio / imageRatio;
|
|
753
1036
|
setCrop({ x: (1 - w) / 2, y: (1 - h) / 2, w, h });
|
|
754
|
-
|
|
755
|
-
return rotation % 180 !== 0;
|
|
756
|
-
}
|
|
757
|
-
}, [rotation]);
|
|
1037
|
+
}, []);
|
|
758
1038
|
const onPointerDown = useCallback3(
|
|
759
1039
|
(kind) => (e) => {
|
|
760
1040
|
e.preventDefault();
|
|
@@ -814,28 +1094,24 @@ function ImageEditSheet({
|
|
|
814
1094
|
const img = imgRef.current;
|
|
815
1095
|
if (!img || img.naturalWidth === 0) return;
|
|
816
1096
|
setSaving(true);
|
|
1097
|
+
setEditError(null);
|
|
817
1098
|
try {
|
|
818
|
-
const swap = rotation % 180 !== 0;
|
|
819
|
-
const outW = Math.round((swap ? img.naturalHeight : img.naturalWidth) * crop.w);
|
|
820
|
-
const outH = Math.round((swap ? img.naturalWidth : img.naturalHeight) * crop.h);
|
|
821
1099
|
const canvas = document.createElement("canvas");
|
|
822
|
-
canvas.width = Math.max(1,
|
|
823
|
-
canvas.height = Math.max(1,
|
|
1100
|
+
canvas.width = Math.max(1, Math.round(img.naturalWidth * crop.w));
|
|
1101
|
+
canvas.height = Math.max(1, Math.round(img.naturalHeight * crop.h));
|
|
824
1102
|
const ctx = canvas.getContext("2d");
|
|
825
1103
|
if (!ctx) throw new Error("no 2d context");
|
|
826
|
-
|
|
827
|
-
const rh = swap ? img.naturalWidth : img.naturalHeight;
|
|
828
|
-
ctx.translate(-crop.x * rw, -crop.y * rh);
|
|
829
|
-
ctx.translate(rw / 2, rh / 2);
|
|
830
|
-
ctx.rotate(rotation * Math.PI / 180);
|
|
831
|
-
if (flipped) ctx.scale(-1, 1);
|
|
832
|
-
ctx.drawImage(img, -img.naturalWidth / 2, -img.naturalHeight / 2);
|
|
1104
|
+
ctx.drawImage(img, -crop.x * img.naturalWidth, -crop.y * img.naturalHeight);
|
|
833
1105
|
canvas.toBlob(
|
|
834
1106
|
(blob) => {
|
|
835
1107
|
setSaving(false);
|
|
836
1108
|
if (blob) {
|
|
837
1109
|
onSave(blob);
|
|
838
1110
|
onClose();
|
|
1111
|
+
} else {
|
|
1112
|
+
setEditError(
|
|
1113
|
+
"Saving failed on this device \u2014 try a smaller crop."
|
|
1114
|
+
);
|
|
839
1115
|
}
|
|
840
1116
|
},
|
|
841
1117
|
"image/jpeg",
|
|
@@ -844,41 +1120,49 @@ function ImageEditSheet({
|
|
|
844
1120
|
} catch (err) {
|
|
845
1121
|
console.error("[capture-camera] edit save failed", err);
|
|
846
1122
|
setSaving(false);
|
|
1123
|
+
setEditError("Saving failed on this device \u2014 try again.");
|
|
847
1124
|
}
|
|
848
|
-
}, [
|
|
1125
|
+
}, [crop, onSave, onClose]);
|
|
849
1126
|
if (!open || !src) return null;
|
|
850
|
-
return /* @__PURE__ */
|
|
851
|
-
/* @__PURE__ */
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1127
|
+
return /* @__PURE__ */ jsxs8("div", { className: "absolute inset-0 z-50 flex flex-col bg-black", children: [
|
|
1128
|
+
/* @__PURE__ */ jsxs8(
|
|
1129
|
+
"div",
|
|
1130
|
+
{
|
|
1131
|
+
className: "flex shrink-0 items-center justify-between px-4",
|
|
1132
|
+
style: { paddingTop: "env(safe-area-inset-top, 0px)" },
|
|
1133
|
+
children: [
|
|
1134
|
+
/* @__PURE__ */ jsxs8(
|
|
1135
|
+
"button",
|
|
1136
|
+
{
|
|
1137
|
+
type: "button",
|
|
1138
|
+
onClick: onClose,
|
|
1139
|
+
"aria-label": "Cancel editing",
|
|
1140
|
+
className: "flex h-11 items-center gap-1.5 rounded-full px-3 text-[15px] font-medium text-white",
|
|
1141
|
+
children: [
|
|
1142
|
+
/* @__PURE__ */ jsx10(X3, { className: "h-5 w-5" }),
|
|
1143
|
+
"Cancel"
|
|
1144
|
+
]
|
|
1145
|
+
}
|
|
1146
|
+
),
|
|
1147
|
+
/* @__PURE__ */ jsx10("span", { className: "text-[15px] font-semibold text-white/90", children: "Edit" }),
|
|
1148
|
+
/* @__PURE__ */ jsxs8(
|
|
1149
|
+
"button",
|
|
1150
|
+
{
|
|
1151
|
+
type: "button",
|
|
1152
|
+
onClick: save,
|
|
1153
|
+
disabled: saving,
|
|
1154
|
+
"aria-label": "Save edited image",
|
|
1155
|
+
className: "flex h-11 items-center gap-1.5 rounded-full px-3 text-[15px] font-semibold text-[#FFCC00] disabled:opacity-50",
|
|
1156
|
+
children: [
|
|
1157
|
+
/* @__PURE__ */ jsx10(Check, { className: "h-5 w-5" }),
|
|
1158
|
+
"Save"
|
|
1159
|
+
]
|
|
1160
|
+
}
|
|
1161
|
+
)
|
|
1162
|
+
]
|
|
1163
|
+
}
|
|
1164
|
+
),
|
|
1165
|
+
/* @__PURE__ */ jsx10(
|
|
882
1166
|
"div",
|
|
883
1167
|
{
|
|
884
1168
|
ref: stageRef,
|
|
@@ -886,21 +1170,18 @@ function ImageEditSheet({
|
|
|
886
1170
|
onPointerMove,
|
|
887
1171
|
onPointerUp,
|
|
888
1172
|
onPointerCancel: onPointerUp,
|
|
889
|
-
children: /* @__PURE__ */
|
|
890
|
-
/* @__PURE__ */
|
|
1173
|
+
children: /* @__PURE__ */ jsxs8("div", { className: "relative max-h-full max-w-full", children: [
|
|
1174
|
+
/* @__PURE__ */ jsx10(
|
|
891
1175
|
"img",
|
|
892
1176
|
{
|
|
893
1177
|
ref: imgRef,
|
|
894
|
-
src,
|
|
1178
|
+
src: workingSrc ?? src,
|
|
895
1179
|
alt: "Image being edited",
|
|
896
1180
|
draggable: false,
|
|
897
|
-
className: "max-h-[62dvh] max-w-full select-none object-contain"
|
|
898
|
-
style: {
|
|
899
|
-
transform: `rotate(${rotation}deg) ${flipped ? "scaleX(-1)" : ""}`
|
|
900
|
-
}
|
|
1181
|
+
className: "max-h-[62dvh] max-w-full select-none object-contain"
|
|
901
1182
|
}
|
|
902
1183
|
),
|
|
903
|
-
/* @__PURE__ */
|
|
1184
|
+
/* @__PURE__ */ jsx10(
|
|
904
1185
|
"div",
|
|
905
1186
|
{
|
|
906
1187
|
role: "presentation",
|
|
@@ -912,7 +1193,7 @@ function ImageEditSheet({
|
|
|
912
1193
|
width: `${crop.w * 100}%`,
|
|
913
1194
|
height: `${crop.h * 100}%`
|
|
914
1195
|
},
|
|
915
|
-
children: ["nw", "ne", "sw", "se"].map((corner) => /* @__PURE__ */
|
|
1196
|
+
children: ["nw", "ne", "sw", "se"].map((corner) => /* @__PURE__ */ jsx10(
|
|
916
1197
|
"span",
|
|
917
1198
|
{
|
|
918
1199
|
role: "presentation",
|
|
@@ -933,8 +1214,9 @@ function ImageEditSheet({
|
|
|
933
1214
|
] })
|
|
934
1215
|
}
|
|
935
1216
|
),
|
|
936
|
-
/* @__PURE__ */
|
|
937
|
-
/* @__PURE__ */
|
|
1217
|
+
/* @__PURE__ */ jsxs8("div", { className: "shrink-0", style: { paddingBottom: "env(safe-area-inset-bottom, 0px)" }, children: [
|
|
1218
|
+
editError && /* @__PURE__ */ jsx10("p", { className: "px-6 pb-2 text-center text-sm text-[#FF6961]", children: editError }),
|
|
1219
|
+
/* @__PURE__ */ jsx10("div", { className: "flex items-center justify-center gap-2 pb-2", children: ASPECTS.map((a) => /* @__PURE__ */ jsx10(
|
|
938
1220
|
"button",
|
|
939
1221
|
{
|
|
940
1222
|
type: "button",
|
|
@@ -948,49 +1230,697 @@ function ImageEditSheet({
|
|
|
948
1230
|
},
|
|
949
1231
|
a.id
|
|
950
1232
|
)) }),
|
|
951
|
-
/* @__PURE__ */
|
|
952
|
-
/* @__PURE__ */
|
|
1233
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-center gap-6 pb-4", children: [
|
|
1234
|
+
/* @__PURE__ */ jsx10(
|
|
953
1235
|
"button",
|
|
954
1236
|
{
|
|
955
1237
|
type: "button",
|
|
956
|
-
onClick: () =>
|
|
957
|
-
setRotation((r) => (r + 270) % 360);
|
|
958
|
-
setCrop(FULL_CROP);
|
|
959
|
-
setAspect("free");
|
|
960
|
-
},
|
|
1238
|
+
onClick: () => bake("rotate-left"),
|
|
961
1239
|
"aria-label": "Rotate left",
|
|
962
1240
|
className: "flex h-12 w-12 touch-manipulation items-center justify-center rounded-full bg-white/10 text-white",
|
|
963
|
-
children: /* @__PURE__ */
|
|
1241
|
+
children: /* @__PURE__ */ jsx10(RotateCcw, { className: "h-5 w-5" })
|
|
964
1242
|
}
|
|
965
1243
|
),
|
|
966
|
-
/* @__PURE__ */
|
|
1244
|
+
/* @__PURE__ */ jsx10(
|
|
967
1245
|
"button",
|
|
968
1246
|
{
|
|
969
1247
|
type: "button",
|
|
970
|
-
onClick: () =>
|
|
1248
|
+
onClick: () => bake("flip"),
|
|
971
1249
|
"aria-label": "Flip horizontally",
|
|
972
|
-
"
|
|
973
|
-
className:
|
|
974
|
-
"flex h-12 w-12 touch-manipulation items-center justify-center rounded-full bg-white/10",
|
|
975
|
-
flipped ? "text-[#FFCC00]" : "text-white"
|
|
976
|
-
),
|
|
977
|
-
children: /* @__PURE__ */ jsx9(FlipHorizontal2, { className: "h-5 w-5" })
|
|
1250
|
+
className: "flex h-12 w-12 touch-manipulation items-center justify-center rounded-full bg-white/10 text-white",
|
|
1251
|
+
children: /* @__PURE__ */ jsx10(FlipHorizontal2, { className: "h-5 w-5" })
|
|
978
1252
|
}
|
|
979
1253
|
)
|
|
980
1254
|
] })
|
|
981
1255
|
] })
|
|
982
1256
|
] });
|
|
983
1257
|
}
|
|
1258
|
+
|
|
1259
|
+
// src/components/CameraCapture.tsx
|
|
1260
|
+
import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1261
|
+
function formatElapsed(totalSeconds) {
|
|
1262
|
+
const m = Math.floor(totalSeconds / 60);
|
|
1263
|
+
const s = String(totalSeconds % 60).padStart(2, "0");
|
|
1264
|
+
return `${m}:${s}`;
|
|
1265
|
+
}
|
|
1266
|
+
var ASPECT_CYCLE = ["full", "4:3", "1:1", "16:9"];
|
|
1267
|
+
function CameraCapture({
|
|
1268
|
+
engine,
|
|
1269
|
+
cloud,
|
|
1270
|
+
mode,
|
|
1271
|
+
onModeChange,
|
|
1272
|
+
preview,
|
|
1273
|
+
media,
|
|
1274
|
+
onClose,
|
|
1275
|
+
blockedSheet,
|
|
1276
|
+
controlsHidden = false,
|
|
1277
|
+
shutterDisabled = false,
|
|
1278
|
+
slots = {}
|
|
1279
|
+
}) {
|
|
1280
|
+
const [optionsOpen, setOptionsOpen] = useState5(false);
|
|
1281
|
+
const [viewerKey, setViewerKey] = useState5(null);
|
|
1282
|
+
const [editTarget, setEditTarget] = useState5(null);
|
|
1283
|
+
const [gridOn, setGridOn] = useState5(false);
|
|
1284
|
+
const [timerSetting, setTimerSetting] = useState5(0);
|
|
1285
|
+
const [aspect, setAspect] = useState5("full");
|
|
1286
|
+
const [exposureOpen, setExposureOpen] = useState5(false);
|
|
1287
|
+
const [countdown, setCountdown] = useState5(null);
|
|
1288
|
+
const [blockedDismissed, setBlockedDismissed] = useState5(false);
|
|
1289
|
+
const countdownRef = useRef3(null);
|
|
1290
|
+
const controls = useTrackControls(engine.stream);
|
|
1291
|
+
const clearCountdown = useCallback4(() => {
|
|
1292
|
+
if (countdownRef.current) clearInterval(countdownRef.current);
|
|
1293
|
+
countdownRef.current = null;
|
|
1294
|
+
setCountdown(null);
|
|
1295
|
+
}, []);
|
|
1296
|
+
useEffect5(() => clearCountdown, [clearCountdown]);
|
|
1297
|
+
useEffect5(() => {
|
|
1298
|
+
if (mode !== "photo") clearCountdown();
|
|
1299
|
+
}, [mode, clearCountdown]);
|
|
1300
|
+
const onShutter = useCallback4(() => {
|
|
1301
|
+
if (mode === "video") {
|
|
1302
|
+
if (engine.recording) engine.onStopRecording();
|
|
1303
|
+
else engine.onStartRecording();
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
if (countdown !== null) {
|
|
1307
|
+
clearCountdown();
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
if (timerSetting === 0) {
|
|
1311
|
+
engine.onCapturePhoto({ aspect });
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
let remaining = timerSetting;
|
|
1315
|
+
setCountdown(remaining);
|
|
1316
|
+
countdownRef.current = setInterval(() => {
|
|
1317
|
+
remaining -= 1;
|
|
1318
|
+
if (remaining <= 0) {
|
|
1319
|
+
clearCountdown();
|
|
1320
|
+
engine.onCapturePhoto({ aspect });
|
|
1321
|
+
} else {
|
|
1322
|
+
setCountdown(remaining);
|
|
1323
|
+
}
|
|
1324
|
+
}, 1e3);
|
|
1325
|
+
}, [mode, engine, countdown, timerSetting, aspect, clearCountdown]);
|
|
1326
|
+
const cycleTimer = useCallback4(() => {
|
|
1327
|
+
setTimerSetting((t) => t === 0 ? 3 : t === 3 ? 10 : 0);
|
|
1328
|
+
}, []);
|
|
1329
|
+
const coreTiles = [
|
|
1330
|
+
...controls.torchSupported ? [
|
|
1331
|
+
{
|
|
1332
|
+
id: "flash",
|
|
1333
|
+
label: "Flash",
|
|
1334
|
+
icon: controls.torchOn ? /* @__PURE__ */ jsx11(Zap, { className: "h-6 w-6", fill: "currentColor" }) : /* @__PURE__ */ jsx11(ZapOff, { className: "h-6 w-6" }),
|
|
1335
|
+
active: controls.torchOn,
|
|
1336
|
+
onPress: controls.toggleTorch
|
|
1337
|
+
}
|
|
1338
|
+
] : [],
|
|
1339
|
+
{
|
|
1340
|
+
id: "timer",
|
|
1341
|
+
label: "Timer",
|
|
1342
|
+
icon: /* @__PURE__ */ jsx11(Timer, { className: "h-6 w-6" }),
|
|
1343
|
+
active: timerSetting !== 0,
|
|
1344
|
+
valueLabel: timerSetting === 0 ? void 0 : `${timerSetting}s`,
|
|
1345
|
+
onPress: cycleTimer
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
id: "grid",
|
|
1349
|
+
label: "Grid",
|
|
1350
|
+
icon: /* @__PURE__ */ jsx11(Grid3x3, { className: "h-6 w-6" }),
|
|
1351
|
+
active: gridOn,
|
|
1352
|
+
onPress: () => setGridOn((g) => !g)
|
|
1353
|
+
},
|
|
1354
|
+
// Aspect applies to PHOTO output (center-cropped from the full sensor).
|
|
1355
|
+
{
|
|
1356
|
+
id: "aspect",
|
|
1357
|
+
label: "Aspect",
|
|
1358
|
+
icon: /* @__PURE__ */ jsx11(Proportions, { className: "h-6 w-6" }),
|
|
1359
|
+
active: aspect !== "full",
|
|
1360
|
+
valueLabel: aspect === "full" ? void 0 : aspect,
|
|
1361
|
+
onPress: () => setAspect(
|
|
1362
|
+
(a) => ASPECT_CYCLE[(ASPECT_CYCLE.indexOf(a) + 1) % ASPECT_CYCLE.length] ?? "full"
|
|
1363
|
+
)
|
|
1364
|
+
},
|
|
1365
|
+
...controls.exposureSupported ? [
|
|
1366
|
+
{
|
|
1367
|
+
id: "exposure",
|
|
1368
|
+
label: "Exposure",
|
|
1369
|
+
icon: /* @__PURE__ */ jsx11(SunMedium, { className: "h-6 w-6" }),
|
|
1370
|
+
active: controls.exposure !== 0 || exposureOpen,
|
|
1371
|
+
valueLabel: controls.exposure === 0 ? void 0 : `${controls.exposure > 0 ? "+" : ""}${controls.exposure}`,
|
|
1372
|
+
onPress: () => setExposureOpen((o) => !o)
|
|
1373
|
+
}
|
|
1374
|
+
] : []
|
|
1375
|
+
];
|
|
1376
|
+
const tiles = [...coreTiles, ...slots.optionTiles ?? []];
|
|
1377
|
+
const blocked = engine.blocked !== null;
|
|
1378
|
+
return /* @__PURE__ */ jsxs9("div", { className: "absolute inset-0 select-none overflow-hidden bg-black", children: [
|
|
1379
|
+
/* @__PURE__ */ jsx11("div", { className: "absolute inset-0", children: preview }),
|
|
1380
|
+
/* @__PURE__ */ jsx11(GridOverlay, { visible: gridOn && !blocked }),
|
|
1381
|
+
mode === "photo" && aspect !== "full" && !blocked && /* @__PURE__ */ jsx11(
|
|
1382
|
+
"div",
|
|
1383
|
+
{
|
|
1384
|
+
"aria-hidden": true,
|
|
1385
|
+
className: "pointer-events-none absolute inset-0 z-10 flex items-center justify-center",
|
|
1386
|
+
children: /* @__PURE__ */ jsx11(
|
|
1387
|
+
"div",
|
|
1388
|
+
{
|
|
1389
|
+
className: "shadow-[0_0_0_9999px_rgba(0,0,0,0.45)]",
|
|
1390
|
+
style: {
|
|
1391
|
+
aspectRatio: aspect === "1:1" ? "1 / 1" : aspect === "4:3" ? "3 / 4" : "9 / 16",
|
|
1392
|
+
width: aspect === "16:9" ? "100%" : void 0,
|
|
1393
|
+
height: aspect === "16:9" ? void 0 : "70%",
|
|
1394
|
+
maxWidth: "100%",
|
|
1395
|
+
maxHeight: "100%"
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
)
|
|
1399
|
+
}
|
|
1400
|
+
),
|
|
1401
|
+
/* @__PURE__ */ jsx11(CountdownOverlay, { seconds: countdown }),
|
|
1402
|
+
!controlsHidden && /* @__PURE__ */ jsx11(
|
|
1403
|
+
"div",
|
|
1404
|
+
{
|
|
1405
|
+
className: "absolute inset-x-0 top-0 z-20 bg-black/65 backdrop-blur-[2px]",
|
|
1406
|
+
style: safeTop,
|
|
1407
|
+
children: /* @__PURE__ */ jsxs9("div", { className: "flex h-11 items-center gap-0.5 px-1.5", children: [
|
|
1408
|
+
onClose ? /* @__PURE__ */ jsx11(
|
|
1409
|
+
"button",
|
|
1410
|
+
{
|
|
1411
|
+
type: "button",
|
|
1412
|
+
onClick: onClose,
|
|
1413
|
+
"aria-label": "Close camera",
|
|
1414
|
+
className: "flex h-10 w-10 shrink-0 touch-manipulation items-center justify-center rounded-full text-white transition-colors hover:bg-white/10",
|
|
1415
|
+
children: /* @__PURE__ */ jsx11(X4, { className: "h-5 w-5" })
|
|
1416
|
+
}
|
|
1417
|
+
) : /* @__PURE__ */ jsx11("span", { className: "w-10 shrink-0" }),
|
|
1418
|
+
/* @__PURE__ */ jsx11("div", { className: "min-w-0 flex-1", children: slots.topBarCenter }),
|
|
1419
|
+
controls.torchSupported && /* @__PURE__ */ jsx11(
|
|
1420
|
+
"button",
|
|
1421
|
+
{
|
|
1422
|
+
type: "button",
|
|
1423
|
+
onClick: controls.toggleTorch,
|
|
1424
|
+
"aria-label": controls.torchOn ? "Turn flash off" : "Turn flash on",
|
|
1425
|
+
"aria-pressed": controls.torchOn,
|
|
1426
|
+
className: cn(
|
|
1427
|
+
"flex h-10 w-10 shrink-0 touch-manipulation items-center justify-center rounded-full transition-colors",
|
|
1428
|
+
controls.torchOn ? "text-[#FFCC00]" : "text-white hover:bg-white/10"
|
|
1429
|
+
),
|
|
1430
|
+
children: /* @__PURE__ */ jsx11(
|
|
1431
|
+
Zap,
|
|
1432
|
+
{
|
|
1433
|
+
className: "h-5 w-5",
|
|
1434
|
+
fill: controls.torchOn ? "currentColor" : "none"
|
|
1435
|
+
}
|
|
1436
|
+
)
|
|
1437
|
+
}
|
|
1438
|
+
),
|
|
1439
|
+
slots.topBarTrailing,
|
|
1440
|
+
/* @__PURE__ */ jsx11(
|
|
1441
|
+
"button",
|
|
1442
|
+
{
|
|
1443
|
+
type: "button",
|
|
1444
|
+
onClick: () => setOptionsOpen((o) => !o),
|
|
1445
|
+
"aria-label": "More camera options",
|
|
1446
|
+
"aria-expanded": optionsOpen,
|
|
1447
|
+
className: cn(
|
|
1448
|
+
"flex h-10 w-10 shrink-0 touch-manipulation items-center justify-center rounded-full transition-colors",
|
|
1449
|
+
optionsOpen ? "bg-white/20 text-white" : "text-white hover:bg-white/10"
|
|
1450
|
+
),
|
|
1451
|
+
children: /* @__PURE__ */ jsx11(Grip, { className: "h-5 w-5" })
|
|
1452
|
+
}
|
|
1453
|
+
)
|
|
1454
|
+
] })
|
|
1455
|
+
}
|
|
1456
|
+
),
|
|
1457
|
+
/* @__PURE__ */ jsxs9(
|
|
1458
|
+
"div",
|
|
1459
|
+
{
|
|
1460
|
+
className: "pointer-events-none absolute inset-x-0 top-[50px] z-20 flex flex-col items-center gap-1.5",
|
|
1461
|
+
style: safeMarginTop,
|
|
1462
|
+
children: [
|
|
1463
|
+
engine.recording && /* @__PURE__ */ jsxs9("span", { className: "flex items-center gap-2 rounded-full bg-black/60 px-3 py-1.5 text-sm font-medium text-white", children: [
|
|
1464
|
+
/* @__PURE__ */ jsx11("span", { className: "h-2.5 w-2.5 animate-pulse rounded-full bg-[#FF3B30]" }),
|
|
1465
|
+
formatElapsed(engine.recordElapsedSeconds)
|
|
1466
|
+
] }),
|
|
1467
|
+
slots.statusChips
|
|
1468
|
+
]
|
|
1469
|
+
}
|
|
1470
|
+
),
|
|
1471
|
+
!controlsHidden && /* @__PURE__ */ jsxs9("div", { className: "absolute inset-x-0 bottom-0 z-20", children: [
|
|
1472
|
+
!blocked && controls.zoomOptions.length >= 2 && /* @__PURE__ */ jsx11("div", { className: "mb-2", children: /* @__PURE__ */ jsx11(
|
|
1473
|
+
ZoomRow,
|
|
1474
|
+
{
|
|
1475
|
+
options: controls.zoomOptions,
|
|
1476
|
+
value: controls.zoom,
|
|
1477
|
+
onSelect: controls.setZoom
|
|
1478
|
+
}
|
|
1479
|
+
) }),
|
|
1480
|
+
media && media.items.length > 0 && /* @__PURE__ */ jsx11("div", { className: "mb-1.5 px-2", children: /* @__PURE__ */ jsx11(
|
|
1481
|
+
CaptureFilmstrip,
|
|
1482
|
+
{
|
|
1483
|
+
items: media.items,
|
|
1484
|
+
onOpen: (item) => setViewerKey(item.key)
|
|
1485
|
+
}
|
|
1486
|
+
) }),
|
|
1487
|
+
slots.aboveBar && /* @__PURE__ */ jsx11("div", { className: "mb-1.5 px-2", children: slots.aboveBar }),
|
|
1488
|
+
exposureOpen && controls.exposureSupported && controls.exposureRange && /* @__PURE__ */ jsxs9("div", { className: "mx-auto mb-3 flex w-64 items-center gap-3 rounded-full bg-black/55 px-4 py-2", children: [
|
|
1489
|
+
/* @__PURE__ */ jsx11(SunMedium, { className: "h-4 w-4 shrink-0 text-[#FFCC00]" }),
|
|
1490
|
+
/* @__PURE__ */ jsx11(
|
|
1491
|
+
"input",
|
|
1492
|
+
{
|
|
1493
|
+
type: "range",
|
|
1494
|
+
min: controls.exposureRange.min,
|
|
1495
|
+
max: controls.exposureRange.max,
|
|
1496
|
+
step: controls.exposureRange.step,
|
|
1497
|
+
value: controls.exposure,
|
|
1498
|
+
onChange: (e) => controls.setExposure(Number(e.target.value)),
|
|
1499
|
+
"aria-label": "Exposure compensation",
|
|
1500
|
+
className: "w-full accent-[#FFCC00]"
|
|
1501
|
+
}
|
|
1502
|
+
),
|
|
1503
|
+
/* @__PURE__ */ jsxs9("span", { className: "w-8 shrink-0 text-right text-xs tabular-nums text-white", children: [
|
|
1504
|
+
controls.exposure > 0 ? "+" : "",
|
|
1505
|
+
controls.exposure
|
|
1506
|
+
] })
|
|
1507
|
+
] }),
|
|
1508
|
+
/* @__PURE__ */ jsxs9(
|
|
1509
|
+
"div",
|
|
1510
|
+
{
|
|
1511
|
+
className: "bg-black/65 px-3 backdrop-blur-[2px]",
|
|
1512
|
+
style: safeBottom,
|
|
1513
|
+
children: [
|
|
1514
|
+
slots.aboveModeSelector,
|
|
1515
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-center gap-2 py-1.5", children: [
|
|
1516
|
+
/* @__PURE__ */ jsx11(
|
|
1517
|
+
ModeSelector,
|
|
1518
|
+
{
|
|
1519
|
+
mode,
|
|
1520
|
+
onModeChange,
|
|
1521
|
+
onUpload: engine.onUpload,
|
|
1522
|
+
modeDisabled: engine.recording,
|
|
1523
|
+
uploadDisabled: shutterDisabled && !blocked,
|
|
1524
|
+
extraModes: slots.extraModes
|
|
1525
|
+
}
|
|
1526
|
+
),
|
|
1527
|
+
slots.modeRowTrailing && /* @__PURE__ */ jsx11("div", { className: "shrink-0", children: slots.modeRowTrailing })
|
|
1528
|
+
] }),
|
|
1529
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between px-2 pb-1.5 pt-0.5", children: [
|
|
1530
|
+
/* @__PURE__ */ jsx11("div", { className: "flex w-14 justify-start", children: /* @__PURE__ */ jsx11(
|
|
1531
|
+
"button",
|
|
1532
|
+
{
|
|
1533
|
+
type: "button",
|
|
1534
|
+
onClick: cloud.onOpenLibrary,
|
|
1535
|
+
"aria-label": "Open your media library",
|
|
1536
|
+
className: "h-11 w-11 touch-manipulation overflow-hidden rounded-xl bg-white/10 ring-1 ring-white/25 transition-transform active:scale-95",
|
|
1537
|
+
children: cloud.recentsThumb ?? /* @__PURE__ */ jsx11("span", { className: "block h-full w-full bg-white/5" })
|
|
1538
|
+
}
|
|
1539
|
+
) }),
|
|
1540
|
+
/* @__PURE__ */ jsx11(
|
|
1541
|
+
ShutterButton,
|
|
1542
|
+
{
|
|
1543
|
+
mode,
|
|
1544
|
+
recording: engine.recording,
|
|
1545
|
+
disabled: shutterDisabled || blocked,
|
|
1546
|
+
onPress: onShutter
|
|
1547
|
+
}
|
|
1548
|
+
),
|
|
1549
|
+
/* @__PURE__ */ jsx11("div", { className: "flex w-14 justify-end", children: engine.onFlipCamera ? /* @__PURE__ */ jsx11(
|
|
1550
|
+
"button",
|
|
1551
|
+
{
|
|
1552
|
+
type: "button",
|
|
1553
|
+
onClick: engine.onFlipCamera,
|
|
1554
|
+
"aria-label": "Switch camera",
|
|
1555
|
+
className: "flex h-11 w-11 touch-manipulation items-center justify-center rounded-full bg-white/15 text-white transition-transform active:rotate-180 active:scale-95 duration-300",
|
|
1556
|
+
children: /* @__PURE__ */ jsx11(RefreshCw, { className: "h-5 w-5" })
|
|
1557
|
+
}
|
|
1558
|
+
) : /* @__PURE__ */ jsx11("span", { className: "h-11 w-11" }) })
|
|
1559
|
+
] })
|
|
1560
|
+
]
|
|
1561
|
+
}
|
|
1562
|
+
)
|
|
1563
|
+
] }),
|
|
1564
|
+
/* @__PURE__ */ jsx11(
|
|
1565
|
+
OptionsGridPanel,
|
|
1566
|
+
{
|
|
1567
|
+
open: optionsOpen && !controlsHidden,
|
|
1568
|
+
onClose: () => setOptionsOpen(false),
|
|
1569
|
+
tiles
|
|
1570
|
+
}
|
|
1571
|
+
),
|
|
1572
|
+
blocked && blockedSheet && !blockedDismissed && /* @__PURE__ */ jsx11(
|
|
1573
|
+
CaptureSheet,
|
|
1574
|
+
{
|
|
1575
|
+
open: true,
|
|
1576
|
+
onClose: () => setBlockedDismissed(true),
|
|
1577
|
+
body: blockedSheet.body,
|
|
1578
|
+
title: "Camera unavailable",
|
|
1579
|
+
actions: blockedSheet.actions
|
|
1580
|
+
}
|
|
1581
|
+
),
|
|
1582
|
+
media && viewerKey !== null && /* @__PURE__ */ jsx11(
|
|
1583
|
+
MediaViewer,
|
|
1584
|
+
{
|
|
1585
|
+
items: media.items,
|
|
1586
|
+
initialKey: viewerKey,
|
|
1587
|
+
keysDisabled: editTarget !== null,
|
|
1588
|
+
onClose: () => setViewerKey(null),
|
|
1589
|
+
onDelete: media.onDelete ? (item) => {
|
|
1590
|
+
media.onDelete?.(item.key);
|
|
1591
|
+
invalidateMedia(item.key);
|
|
1592
|
+
} : void 0,
|
|
1593
|
+
onEdit: media.onReplacePhoto ? (item) => {
|
|
1594
|
+
const url = getMediaUrl(item);
|
|
1595
|
+
if (url) setEditTarget({ key: item.key, url });
|
|
1596
|
+
} : void 0
|
|
1597
|
+
}
|
|
1598
|
+
),
|
|
1599
|
+
media?.onReplacePhoto && /* @__PURE__ */ jsx11(
|
|
1600
|
+
ImageEditSheet,
|
|
1601
|
+
{
|
|
1602
|
+
open: editTarget !== null,
|
|
1603
|
+
src: editTarget?.url ?? null,
|
|
1604
|
+
onClose: () => setEditTarget(null),
|
|
1605
|
+
onSave: (blob) => {
|
|
1606
|
+
const target = editTarget;
|
|
1607
|
+
setEditTarget(null);
|
|
1608
|
+
setViewerKey(null);
|
|
1609
|
+
if (target) {
|
|
1610
|
+
media.onReplacePhoto?.(target.key, blob);
|
|
1611
|
+
invalidateMedia(target.key);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
),
|
|
1616
|
+
slots.overlays
|
|
1617
|
+
] });
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/components/CameraFeed.tsx
|
|
1621
|
+
import { useEffect as useEffect6 } from "react";
|
|
1622
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
1623
|
+
function CameraFeed({
|
|
1624
|
+
engine,
|
|
1625
|
+
mirror = false
|
|
1626
|
+
}) {
|
|
1627
|
+
const { stream, videoRef } = engine;
|
|
1628
|
+
useEffect6(() => {
|
|
1629
|
+
const video = videoRef.current;
|
|
1630
|
+
if (!video) return;
|
|
1631
|
+
if (video.srcObject !== stream) video.srcObject = stream;
|
|
1632
|
+
return () => {
|
|
1633
|
+
video.srcObject = null;
|
|
1634
|
+
};
|
|
1635
|
+
}, [stream, videoRef]);
|
|
1636
|
+
return /* @__PURE__ */ jsx12(
|
|
1637
|
+
"video",
|
|
1638
|
+
{
|
|
1639
|
+
ref: videoRef,
|
|
1640
|
+
style: {
|
|
1641
|
+
width: "100%",
|
|
1642
|
+
height: "100%",
|
|
1643
|
+
objectFit: "cover",
|
|
1644
|
+
...mirror ? { transform: "scaleX(-1)" } : {}
|
|
1645
|
+
},
|
|
1646
|
+
muted: true,
|
|
1647
|
+
autoPlay: true,
|
|
1648
|
+
playsInline: true
|
|
1649
|
+
}
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// src/engine/useDefaultEngine.ts
|
|
1654
|
+
import {
|
|
1655
|
+
useCallback as useCallback5,
|
|
1656
|
+
useEffect as useEffect7,
|
|
1657
|
+
useMemo as useMemo3,
|
|
1658
|
+
useRef as useRef4,
|
|
1659
|
+
useState as useState6
|
|
1660
|
+
} from "react";
|
|
1661
|
+
var RECORDING_MIME_LADDER = [
|
|
1662
|
+
'video/mp4;codecs="avc1.42E01E,mp4a.40.2"',
|
|
1663
|
+
"video/mp4",
|
|
1664
|
+
'video/webm;codecs="vp9,opus"',
|
|
1665
|
+
'video/webm;codecs="vp8,opus"',
|
|
1666
|
+
"video/webm"
|
|
1667
|
+
];
|
|
1668
|
+
function cropRect(w, h, aspect) {
|
|
1669
|
+
if (aspect === "full") return { x: 0, y: 0, w, h };
|
|
1670
|
+
const [aw, ah] = aspect === "1:1" ? [1, 1] : aspect === "4:3" ? [3, 4] : [9, 16];
|
|
1671
|
+
const [rw, rh] = w >= h ? [ah, aw] : [aw, ah];
|
|
1672
|
+
const target = rw / rh;
|
|
1673
|
+
let cw = w;
|
|
1674
|
+
let ch = w / target;
|
|
1675
|
+
if (ch > h) {
|
|
1676
|
+
ch = h;
|
|
1677
|
+
cw = h * target;
|
|
1678
|
+
}
|
|
1679
|
+
return { x: (w - cw) / 2, y: (h - ch) / 2, w: cw, h: ch };
|
|
1680
|
+
}
|
|
1681
|
+
function useDefaultCaptureEngine(options) {
|
|
1682
|
+
const {
|
|
1683
|
+
onPhoto,
|
|
1684
|
+
onVideo,
|
|
1685
|
+
onFiles,
|
|
1686
|
+
withAudio = true,
|
|
1687
|
+
facingMode: initialFacing = "environment",
|
|
1688
|
+
photoQuality = 0.92
|
|
1689
|
+
} = options;
|
|
1690
|
+
const [stream, setStream] = useState6(null);
|
|
1691
|
+
const [blocked, setBlocked] = useState6(null);
|
|
1692
|
+
const [facing, setFacing] = useState6(initialFacing);
|
|
1693
|
+
const [multipleCameras, setMultipleCameras] = useState6(false);
|
|
1694
|
+
const [recording, setRecording] = useState6(false);
|
|
1695
|
+
const [recordElapsedSeconds, setRecordElapsedSeconds] = useState6(0);
|
|
1696
|
+
const videoRef = useRef4(null);
|
|
1697
|
+
const streamRef = useRef4(null);
|
|
1698
|
+
const recorderRef = useRef4(null);
|
|
1699
|
+
const callbacksRef = useRef4({ onPhoto, onVideo, onFiles });
|
|
1700
|
+
callbacksRef.current = { onPhoto, onVideo, onFiles };
|
|
1701
|
+
useEffect7(() => {
|
|
1702
|
+
let cancelled = false;
|
|
1703
|
+
let acquired = null;
|
|
1704
|
+
if (typeof navigator === "undefined" || !navigator.mediaDevices) {
|
|
1705
|
+
setBlocked({ reason: "not-supported" });
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
navigator.mediaDevices.getUserMedia({
|
|
1709
|
+
video: {
|
|
1710
|
+
facingMode: facing,
|
|
1711
|
+
width: { ideal: 4096 },
|
|
1712
|
+
height: { ideal: 4096 }
|
|
1713
|
+
},
|
|
1714
|
+
audio: false
|
|
1715
|
+
}).then((s) => {
|
|
1716
|
+
if (cancelled) {
|
|
1717
|
+
s.getTracks().forEach((t) => t.stop());
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
acquired = s;
|
|
1721
|
+
streamRef.current = s;
|
|
1722
|
+
setStream(s);
|
|
1723
|
+
setBlocked(null);
|
|
1724
|
+
return navigator.mediaDevices.enumerateDevices().then((devices) => {
|
|
1725
|
+
if (!cancelled)
|
|
1726
|
+
setMultipleCameras(
|
|
1727
|
+
devices.filter((d) => d.kind === "videoinput").length > 1
|
|
1728
|
+
);
|
|
1729
|
+
});
|
|
1730
|
+
}).catch((err) => {
|
|
1731
|
+
if (cancelled) return;
|
|
1732
|
+
const name = err && typeof err === "object" && "name" in err ? String(err.name) : "";
|
|
1733
|
+
setBlocked({
|
|
1734
|
+
reason: name === "NotAllowedError" || name === "SecurityError" ? "permission-denied" : "not-supported"
|
|
1735
|
+
});
|
|
1736
|
+
});
|
|
1737
|
+
return () => {
|
|
1738
|
+
cancelled = true;
|
|
1739
|
+
acquired?.getTracks().forEach((t) => t.stop());
|
|
1740
|
+
if (streamRef.current === acquired) streamRef.current = null;
|
|
1741
|
+
setStream(null);
|
|
1742
|
+
};
|
|
1743
|
+
}, [facing]);
|
|
1744
|
+
useEffect7(() => {
|
|
1745
|
+
return () => {
|
|
1746
|
+
const r = recorderRef.current;
|
|
1747
|
+
if (r) {
|
|
1748
|
+
clearInterval(r.timer);
|
|
1749
|
+
if (r.recorder.state !== "inactive") r.recorder.stop();
|
|
1750
|
+
r.micTracks.forEach((t) => t.stop());
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
}, []);
|
|
1754
|
+
const onCapturePhoto = useCallback5(
|
|
1755
|
+
(opts) => {
|
|
1756
|
+
const video = videoRef.current;
|
|
1757
|
+
if (!video || video.videoWidth === 0) return;
|
|
1758
|
+
const rect = cropRect(
|
|
1759
|
+
video.videoWidth,
|
|
1760
|
+
video.videoHeight,
|
|
1761
|
+
opts?.aspect ?? "full"
|
|
1762
|
+
);
|
|
1763
|
+
const canvas = document.createElement("canvas");
|
|
1764
|
+
canvas.width = Math.round(rect.w);
|
|
1765
|
+
canvas.height = Math.round(rect.h);
|
|
1766
|
+
const ctx = canvas.getContext("2d");
|
|
1767
|
+
if (!ctx) return;
|
|
1768
|
+
ctx.drawImage(
|
|
1769
|
+
video,
|
|
1770
|
+
rect.x,
|
|
1771
|
+
rect.y,
|
|
1772
|
+
rect.w,
|
|
1773
|
+
rect.h,
|
|
1774
|
+
0,
|
|
1775
|
+
0,
|
|
1776
|
+
canvas.width,
|
|
1777
|
+
canvas.height
|
|
1778
|
+
);
|
|
1779
|
+
canvas.toBlob(
|
|
1780
|
+
(blob) => {
|
|
1781
|
+
if (!blob) return;
|
|
1782
|
+
callbacksRef.current.onPhoto(
|
|
1783
|
+
new File([blob], `capture-${(/* @__PURE__ */ new Date()).toISOString()}.jpg`, {
|
|
1784
|
+
type: "image/jpeg"
|
|
1785
|
+
})
|
|
1786
|
+
);
|
|
1787
|
+
},
|
|
1788
|
+
"image/jpeg",
|
|
1789
|
+
photoQuality
|
|
1790
|
+
);
|
|
1791
|
+
},
|
|
1792
|
+
[photoQuality]
|
|
1793
|
+
);
|
|
1794
|
+
const onStartRecording = useCallback5(() => {
|
|
1795
|
+
const base = streamRef.current;
|
|
1796
|
+
if (!base || recorderRef.current) return;
|
|
1797
|
+
void (async () => {
|
|
1798
|
+
let micTracks = [];
|
|
1799
|
+
if (withAudio) {
|
|
1800
|
+
try {
|
|
1801
|
+
const mic = await navigator.mediaDevices.getUserMedia({
|
|
1802
|
+
audio: true
|
|
1803
|
+
});
|
|
1804
|
+
micTracks = mic.getAudioTracks();
|
|
1805
|
+
} catch {
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
const composed = new MediaStream([
|
|
1809
|
+
...base.getVideoTracks(),
|
|
1810
|
+
...micTracks
|
|
1811
|
+
]);
|
|
1812
|
+
const mime = RECORDING_MIME_LADDER.find(
|
|
1813
|
+
(m) => typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported(m)
|
|
1814
|
+
);
|
|
1815
|
+
let recorder;
|
|
1816
|
+
try {
|
|
1817
|
+
recorder = new MediaRecorder(
|
|
1818
|
+
composed,
|
|
1819
|
+
mime ? { mimeType: mime } : void 0
|
|
1820
|
+
);
|
|
1821
|
+
} catch {
|
|
1822
|
+
micTracks.forEach((t) => t.stop());
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
const entry = {
|
|
1826
|
+
recorder,
|
|
1827
|
+
chunks: [],
|
|
1828
|
+
startedAt: performance.now(),
|
|
1829
|
+
micTracks,
|
|
1830
|
+
timer: setInterval(() => {
|
|
1831
|
+
setRecordElapsedSeconds(
|
|
1832
|
+
Math.floor((performance.now() - entry.startedAt) / 1e3)
|
|
1833
|
+
);
|
|
1834
|
+
}, 250)
|
|
1835
|
+
};
|
|
1836
|
+
recorderRef.current = entry;
|
|
1837
|
+
recorder.ondataavailable = (e) => {
|
|
1838
|
+
if (e.data.size > 0) entry.chunks.push(e.data);
|
|
1839
|
+
};
|
|
1840
|
+
recorder.onstop = () => {
|
|
1841
|
+
clearInterval(entry.timer);
|
|
1842
|
+
entry.micTracks.forEach((t) => t.stop());
|
|
1843
|
+
recorderRef.current = null;
|
|
1844
|
+
setRecording(false);
|
|
1845
|
+
const durationMs = Math.round(performance.now() - entry.startedAt);
|
|
1846
|
+
const type = recorder.mimeType || entry.chunks[0]?.type || "video/webm";
|
|
1847
|
+
const blob = new Blob(entry.chunks, { type });
|
|
1848
|
+
const ext = type.includes("mp4") ? "mp4" : "webm";
|
|
1849
|
+
callbacksRef.current.onVideo(
|
|
1850
|
+
new File([blob], `capture-${(/* @__PURE__ */ new Date()).toISOString()}.${ext}`, {
|
|
1851
|
+
type
|
|
1852
|
+
}),
|
|
1853
|
+
durationMs
|
|
1854
|
+
);
|
|
1855
|
+
};
|
|
1856
|
+
recorder.start(1e3);
|
|
1857
|
+
setRecordElapsedSeconds(0);
|
|
1858
|
+
setRecording(true);
|
|
1859
|
+
})();
|
|
1860
|
+
}, [withAudio]);
|
|
1861
|
+
const onStopRecording = useCallback5(() => {
|
|
1862
|
+
const r = recorderRef.current;
|
|
1863
|
+
if (r && r.recorder.state !== "inactive") r.recorder.stop();
|
|
1864
|
+
}, []);
|
|
1865
|
+
const onUpload = useCallback5(() => {
|
|
1866
|
+
const input = document.createElement("input");
|
|
1867
|
+
input.type = "file";
|
|
1868
|
+
input.accept = "image/*,video/*";
|
|
1869
|
+
input.multiple = true;
|
|
1870
|
+
input.onchange = () => {
|
|
1871
|
+
if (input.files && input.files.length > 0)
|
|
1872
|
+
callbacksRef.current.onFiles(input.files);
|
|
1873
|
+
};
|
|
1874
|
+
input.click();
|
|
1875
|
+
}, []);
|
|
1876
|
+
const onFlipCamera = useCallback5(() => {
|
|
1877
|
+
setFacing((f) => f === "environment" ? "user" : "environment");
|
|
1878
|
+
}, []);
|
|
1879
|
+
return useMemo3(
|
|
1880
|
+
() => ({
|
|
1881
|
+
stream,
|
|
1882
|
+
videoRef,
|
|
1883
|
+
blocked,
|
|
1884
|
+
onCapturePhoto,
|
|
1885
|
+
onStartRecording,
|
|
1886
|
+
onStopRecording,
|
|
1887
|
+
recording,
|
|
1888
|
+
recordElapsedSeconds,
|
|
1889
|
+
onUpload,
|
|
1890
|
+
onFlipCamera: multipleCameras ? onFlipCamera : null
|
|
1891
|
+
}),
|
|
1892
|
+
[
|
|
1893
|
+
stream,
|
|
1894
|
+
blocked,
|
|
1895
|
+
onCapturePhoto,
|
|
1896
|
+
onStartRecording,
|
|
1897
|
+
onStopRecording,
|
|
1898
|
+
recording,
|
|
1899
|
+
recordElapsedSeconds,
|
|
1900
|
+
onUpload,
|
|
1901
|
+
onFlipCamera,
|
|
1902
|
+
multipleCameras
|
|
1903
|
+
]
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
984
1906
|
export {
|
|
985
1907
|
CameraCapture,
|
|
1908
|
+
CameraFeed,
|
|
1909
|
+
CaptureFilmstrip,
|
|
986
1910
|
CaptureSheet,
|
|
987
1911
|
CountdownOverlay,
|
|
988
1912
|
GridOverlay,
|
|
989
1913
|
ImageEditSheet,
|
|
1914
|
+
MediaViewer,
|
|
990
1915
|
ModeSelector,
|
|
991
1916
|
OptionsGridPanel,
|
|
992
1917
|
ShutterButton,
|
|
993
1918
|
ZoomRow,
|
|
1919
|
+
getMediaUrl,
|
|
1920
|
+
invalidateMedia,
|
|
1921
|
+
primeMedia,
|
|
1922
|
+
useDefaultCaptureEngine,
|
|
1923
|
+
useMediaUrl,
|
|
994
1924
|
useTrackControls
|
|
995
1925
|
};
|
|
996
1926
|
//# sourceMappingURL=react.js.map
|