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