@ai-matrx/capture 0.4.2 → 0.5.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 +47 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/react.cjs +675 -99
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +265 -4
- package/dist/react.d.ts +265 -4
- package/dist/react.js +676 -106
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
package/dist/react.cjs
CHANGED
|
@@ -28,6 +28,7 @@ __export(react_exports, {
|
|
|
28
28
|
CaptureFilmstrip: () => CaptureFilmstrip,
|
|
29
29
|
CaptureRail: () => CaptureRail,
|
|
30
30
|
CaptureSheet: () => CaptureSheet,
|
|
31
|
+
CloudLibrarySheet: () => CloudLibrarySheet,
|
|
31
32
|
CountdownOverlay: () => CountdownOverlay,
|
|
32
33
|
GridOverlay: () => GridOverlay,
|
|
33
34
|
HoldShutter: () => HoldShutter,
|
|
@@ -35,14 +36,33 @@ __export(react_exports, {
|
|
|
35
36
|
MediaViewer: () => MediaViewer,
|
|
36
37
|
ModeSelector: () => ModeSelector,
|
|
37
38
|
OptionsGridPanel: () => OptionsGridPanel,
|
|
39
|
+
PHOTO_JPEG_QUALITY: () => PHOTO_JPEG_QUALITY,
|
|
38
40
|
ShutterButton: () => ShutterButton,
|
|
39
41
|
ZoomRow: () => ZoomRow,
|
|
42
|
+
acquireWarmMic: () => acquireWarmMic,
|
|
43
|
+
adoptWarmAudioStream: () => adoptWarmAudioStream,
|
|
44
|
+
buildWarmMicConstraints: () => buildWarmMicConstraints,
|
|
45
|
+
classifyCameraBlockReason: () => classifyCameraBlockReason,
|
|
46
|
+
cropBlobToAspect: () => cropBlobToAspect,
|
|
40
47
|
getMediaUrl: () => getMediaUrl,
|
|
48
|
+
getWarmMicState: () => getWarmMicState,
|
|
49
|
+
hardStopWarmMic: () => hardStopWarmMic,
|
|
41
50
|
invalidateMedia: () => invalidateMedia,
|
|
51
|
+
isMediaDenialError: () => isMediaDenialError,
|
|
52
|
+
nextCameraDevice: () => nextCameraDevice,
|
|
53
|
+
notifyMicPermissionRevoked: () => notifyMicPermissionRevoked,
|
|
42
54
|
primeMedia: () => primeMedia,
|
|
55
|
+
queryCameraPermission: () => queryCameraPermission,
|
|
56
|
+
queryMicPermission: () => queryMicPermission,
|
|
57
|
+
releaseWarmMic: () => releaseWarmMic,
|
|
58
|
+
setPreferredMicDeviceId: () => setPreferredMicDeviceId,
|
|
59
|
+
shouldCombineMicPrompt: () => shouldCombineMicPrompt,
|
|
60
|
+
subscribeWarmMic: () => subscribeWarmMic,
|
|
61
|
+
subscribeWarmMicInterruption: () => subscribeWarmMicInterruption,
|
|
43
62
|
useDefaultCaptureEngine: () => useDefaultCaptureEngine,
|
|
44
63
|
useMediaUrl: () => useMediaUrl,
|
|
45
|
-
useTrackControls: () => useTrackControls
|
|
64
|
+
useTrackControls: () => useTrackControls,
|
|
65
|
+
warmMicDebug: () => warmMicDebug
|
|
46
66
|
});
|
|
47
67
|
module.exports = __toCommonJS(react_exports);
|
|
48
68
|
|
|
@@ -118,6 +138,23 @@ function ImagesIcon(props) {
|
|
|
118
138
|
function Loader2Icon(props) {
|
|
119
139
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { ...svgProps(props), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) });
|
|
120
140
|
}
|
|
141
|
+
function UploadIcon(props) {
|
|
142
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { ...svgProps(props), children: [
|
|
143
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 3v12" }),
|
|
144
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "m17 8-5-5-5 5" }),
|
|
145
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" })
|
|
146
|
+
] });
|
|
147
|
+
}
|
|
148
|
+
function ImageOffIcon(props) {
|
|
149
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { ...svgProps(props), children: [
|
|
150
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "2", x2: "22", y1: "2", y2: "22" }),
|
|
151
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M10.41 10.41a2 2 0 1 1-2.83-2.83" }),
|
|
152
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "13.5", x2: "6", y1: "13.5", y2: "21" }),
|
|
153
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "18", x2: "21", y1: "12", y2: "15" }),
|
|
154
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M3.59 3.59A1.99 1.99 0 0 0 3 5v14a2 2 0 0 0 2 2h14c.55 0 1.052-.22 1.41-.59" }),
|
|
155
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 15V5a2 2 0 0 0-2-2H9" })
|
|
156
|
+
] });
|
|
157
|
+
}
|
|
121
158
|
function LockIcon(props) {
|
|
122
159
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { ...svgProps(props), children: [
|
|
123
160
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { width: "18", height: "11", x: "3", y: "11", rx: "2", ry: "2" }),
|
|
@@ -766,7 +803,7 @@ function FilmstripThumb({ item }) {
|
|
|
766
803
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
767
804
|
"video",
|
|
768
805
|
{
|
|
769
|
-
src: url
|
|
806
|
+
src: `${url}#t=0.01`,
|
|
770
807
|
muted: true,
|
|
771
808
|
playsInline: true,
|
|
772
809
|
preload: "metadata",
|
|
@@ -2572,6 +2609,349 @@ function CameraFeed({
|
|
|
2572
2609
|
|
|
2573
2610
|
// src/engine/useDefaultEngine.ts
|
|
2574
2611
|
var import_react11 = require("react");
|
|
2612
|
+
|
|
2613
|
+
// src/engine/crop.ts
|
|
2614
|
+
var PHOTO_JPEG_QUALITY = 0.92;
|
|
2615
|
+
async function cropBlobToAspect(blob, aspect, quality = PHOTO_JPEG_QUALITY) {
|
|
2616
|
+
if (aspect === "full") return blob;
|
|
2617
|
+
const [wRatio, hRatio] = aspect === "1:1" ? [1, 1] : aspect === "4:3" ? [4, 3] : [16, 9];
|
|
2618
|
+
const bitmap = await createImageBitmap(blob);
|
|
2619
|
+
try {
|
|
2620
|
+
const srcW = bitmap.width;
|
|
2621
|
+
const srcH = bitmap.height;
|
|
2622
|
+
const target = srcW >= srcH ? wRatio / hRatio : hRatio / wRatio;
|
|
2623
|
+
let cropW = srcW;
|
|
2624
|
+
let cropH = Math.round(srcW / target);
|
|
2625
|
+
if (cropH > srcH) {
|
|
2626
|
+
cropH = srcH;
|
|
2627
|
+
cropW = Math.round(srcH * target);
|
|
2628
|
+
}
|
|
2629
|
+
if (cropW === srcW && cropH === srcH) return blob;
|
|
2630
|
+
const canvas = document.createElement("canvas");
|
|
2631
|
+
canvas.width = cropW;
|
|
2632
|
+
canvas.height = cropH;
|
|
2633
|
+
const ctx = canvas.getContext("2d");
|
|
2634
|
+
if (!ctx) return blob;
|
|
2635
|
+
ctx.drawImage(
|
|
2636
|
+
bitmap,
|
|
2637
|
+
Math.round((srcW - cropW) / 2),
|
|
2638
|
+
Math.round((srcH - cropH) / 2),
|
|
2639
|
+
cropW,
|
|
2640
|
+
cropH,
|
|
2641
|
+
0,
|
|
2642
|
+
0,
|
|
2643
|
+
cropW,
|
|
2644
|
+
cropH
|
|
2645
|
+
);
|
|
2646
|
+
return await new Promise(
|
|
2647
|
+
(resolve) => canvas.toBlob((out) => resolve(out ?? blob), "image/jpeg", quality)
|
|
2648
|
+
);
|
|
2649
|
+
} finally {
|
|
2650
|
+
bitmap.close();
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
function nextCameraDevice(cameras, currentDeviceId) {
|
|
2654
|
+
if (cameras.length < 2) return null;
|
|
2655
|
+
const currentIdx = currentDeviceId ? cameras.findIndex((c) => c.deviceId === currentDeviceId) : -1;
|
|
2656
|
+
return cameras[(Math.max(currentIdx, 0) + 1) % cameras.length] ?? null;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
// src/engine/permissions.ts
|
|
2660
|
+
async function queryPermission(name) {
|
|
2661
|
+
if (typeof navigator === "undefined" || !navigator.permissions) {
|
|
2662
|
+
return "unknown";
|
|
2663
|
+
}
|
|
2664
|
+
try {
|
|
2665
|
+
const status = await navigator.permissions.query({
|
|
2666
|
+
name
|
|
2667
|
+
});
|
|
2668
|
+
if (status.state === "granted") return "granted";
|
|
2669
|
+
if (status.state === "denied") return "denied";
|
|
2670
|
+
return "prompt";
|
|
2671
|
+
} catch {
|
|
2672
|
+
return "unknown";
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
function queryCameraPermission() {
|
|
2676
|
+
return queryPermission("camera");
|
|
2677
|
+
}
|
|
2678
|
+
function queryMicPermission() {
|
|
2679
|
+
return queryPermission("microphone");
|
|
2680
|
+
}
|
|
2681
|
+
function shouldCombineMicPrompt(micPermissionState) {
|
|
2682
|
+
return micPermissionState === "prompt" || micPermissionState === "unknown";
|
|
2683
|
+
}
|
|
2684
|
+
function isMediaDenialError(err) {
|
|
2685
|
+
const name = err && typeof err === "object" && "name" in err ? String(err.name) : "";
|
|
2686
|
+
return name === "NotAllowedError" || name === "SecurityError";
|
|
2687
|
+
}
|
|
2688
|
+
function classifyCameraBlockReason(err) {
|
|
2689
|
+
return isMediaDenialError(err) ? "permission-denied" : "not-supported";
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
// src/engine/warm-mic.ts
|
|
2693
|
+
var DEFAULT_KEEPALIVE_MS = 6e3;
|
|
2694
|
+
var STATE_SLOT2 = /* @__PURE__ */ Symbol.for("ai-matrx.capture.warm-mic-state");
|
|
2695
|
+
function getManager() {
|
|
2696
|
+
const holder = globalThis;
|
|
2697
|
+
let m = holder[STATE_SLOT2];
|
|
2698
|
+
if (!m) {
|
|
2699
|
+
m = {
|
|
2700
|
+
stream: null,
|
|
2701
|
+
inFlight: null,
|
|
2702
|
+
refCount: 0,
|
|
2703
|
+
releaseTimer: null,
|
|
2704
|
+
state: "idle",
|
|
2705
|
+
keepAliveMs: DEFAULT_KEEPALIVE_MS,
|
|
2706
|
+
preferredInputDeviceId: null,
|
|
2707
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
2708
|
+
interruptionListeners: /* @__PURE__ */ new Set(),
|
|
2709
|
+
pageLifecycleWatched: false,
|
|
2710
|
+
permissionWatched: false
|
|
2711
|
+
};
|
|
2712
|
+
holder[STATE_SLOT2] = m;
|
|
2713
|
+
}
|
|
2714
|
+
return m;
|
|
2715
|
+
}
|
|
2716
|
+
function emitInterruption(reason) {
|
|
2717
|
+
for (const l of getManager().interruptionListeners) {
|
|
2718
|
+
try {
|
|
2719
|
+
l(reason);
|
|
2720
|
+
} catch {
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
function subscribeWarmMicInterruption(listener) {
|
|
2725
|
+
const m = getManager();
|
|
2726
|
+
m.interruptionListeners.add(listener);
|
|
2727
|
+
return () => {
|
|
2728
|
+
m.interruptionListeners.delete(listener);
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
function setState(next) {
|
|
2732
|
+
const m = getManager();
|
|
2733
|
+
m.state = next;
|
|
2734
|
+
for (const l of m.listeners) {
|
|
2735
|
+
try {
|
|
2736
|
+
l(next);
|
|
2737
|
+
} catch {
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
function clearReleaseTimer() {
|
|
2742
|
+
const m = getManager();
|
|
2743
|
+
if (m.releaseTimer) {
|
|
2744
|
+
clearTimeout(m.releaseTimer);
|
|
2745
|
+
m.releaseTimer = null;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
function attachTrackHealth(stream) {
|
|
2749
|
+
const m = getManager();
|
|
2750
|
+
for (const track of stream.getAudioTracks()) {
|
|
2751
|
+
track.onended = () => {
|
|
2752
|
+
console.error(
|
|
2753
|
+
"[capture:warm-mic] mic track ENDED (OS interruption / device change). The warm grant is gone; the next recording will re-prompt."
|
|
2754
|
+
);
|
|
2755
|
+
if (m.stream === stream) {
|
|
2756
|
+
m.stream = null;
|
|
2757
|
+
setState("error");
|
|
2758
|
+
}
|
|
2759
|
+
emitInterruption("ended");
|
|
2760
|
+
};
|
|
2761
|
+
track.onmute = () => {
|
|
2762
|
+
console.warn(
|
|
2763
|
+
"[capture:warm-mic] mic track MUTED (transient interruption \u2014 e.g. a call). The grant survives; it should unmute when the interruption ends."
|
|
2764
|
+
);
|
|
2765
|
+
emitInterruption("muted");
|
|
2766
|
+
};
|
|
2767
|
+
track.onunmute = () => {
|
|
2768
|
+
emitInterruption("unmuted");
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
function notifyMicPermissionRevoked() {
|
|
2773
|
+
console.error("[capture:warm-mic] microphone permission REVOKED.");
|
|
2774
|
+
hardStopWarmMic();
|
|
2775
|
+
emitInterruption("permission-revoked");
|
|
2776
|
+
}
|
|
2777
|
+
function watchMicPermission() {
|
|
2778
|
+
const m = getManager();
|
|
2779
|
+
if (m.permissionWatched) return;
|
|
2780
|
+
if (typeof navigator === "undefined" || !navigator.permissions) return;
|
|
2781
|
+
m.permissionWatched = true;
|
|
2782
|
+
void (async () => {
|
|
2783
|
+
try {
|
|
2784
|
+
const status = await navigator.permissions.query({
|
|
2785
|
+
name: "microphone"
|
|
2786
|
+
});
|
|
2787
|
+
status.onchange = () => {
|
|
2788
|
+
if (status.state === "denied") notifyMicPermissionRevoked();
|
|
2789
|
+
};
|
|
2790
|
+
} catch {
|
|
2791
|
+
}
|
|
2792
|
+
})();
|
|
2793
|
+
}
|
|
2794
|
+
function watchPageLifecycle() {
|
|
2795
|
+
const m = getManager();
|
|
2796
|
+
if (m.pageLifecycleWatched) return;
|
|
2797
|
+
if (typeof window === "undefined") return;
|
|
2798
|
+
m.pageLifecycleWatched = true;
|
|
2799
|
+
window.addEventListener("pagehide", () => {
|
|
2800
|
+
if (m.stream || m.refCount > 0) {
|
|
2801
|
+
if (m.refCount > 0) {
|
|
2802
|
+
console.error(
|
|
2803
|
+
`[capture:warm-mic] page hiding with ${m.refCount} unreleased mic holder(s) \u2014 forcing release. A surface leaked acquireWarmMic without a matching releaseWarmMic on unmount.`
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2806
|
+
hardStopWarmMic();
|
|
2807
|
+
}
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
function setPreferredMicDeviceId(id) {
|
|
2811
|
+
getManager().preferredInputDeviceId = id;
|
|
2812
|
+
}
|
|
2813
|
+
function streamIsLive(stream) {
|
|
2814
|
+
if (!stream) return false;
|
|
2815
|
+
const tracks = stream.getAudioTracks();
|
|
2816
|
+
return tracks.length > 0 && tracks.every((t) => t.readyState === "live");
|
|
2817
|
+
}
|
|
2818
|
+
function buildWarmMicConstraints() {
|
|
2819
|
+
const m = getManager();
|
|
2820
|
+
const audio = {
|
|
2821
|
+
echoCancellation: true,
|
|
2822
|
+
noiseSuppression: true,
|
|
2823
|
+
autoGainControl: true
|
|
2824
|
+
};
|
|
2825
|
+
if (m.preferredInputDeviceId) {
|
|
2826
|
+
audio.deviceId = { ideal: m.preferredInputDeviceId };
|
|
2827
|
+
}
|
|
2828
|
+
return audio;
|
|
2829
|
+
}
|
|
2830
|
+
function adoptWarmAudioStream(stream) {
|
|
2831
|
+
const m = getManager();
|
|
2832
|
+
if (streamIsLive(m.stream) || m.inFlight) {
|
|
2833
|
+
for (const t of stream.getTracks()) {
|
|
2834
|
+
try {
|
|
2835
|
+
t.stop();
|
|
2836
|
+
} catch {
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
if (!streamIsLive(stream)) return;
|
|
2842
|
+
clearReleaseTimer();
|
|
2843
|
+
m.stream = stream;
|
|
2844
|
+
attachTrackHealth(stream);
|
|
2845
|
+
watchPageLifecycle();
|
|
2846
|
+
watchMicPermission();
|
|
2847
|
+
if (m.refCount > 0) {
|
|
2848
|
+
setState("active");
|
|
2849
|
+
} else {
|
|
2850
|
+
armKeepaliveStop();
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
async function acquireWarmMic(constraints) {
|
|
2854
|
+
const m = getManager();
|
|
2855
|
+
clearReleaseTimer();
|
|
2856
|
+
m.refCount += 1;
|
|
2857
|
+
if (streamIsLive(m.stream)) {
|
|
2858
|
+
setState("active");
|
|
2859
|
+
return m.stream;
|
|
2860
|
+
}
|
|
2861
|
+
m.stream = null;
|
|
2862
|
+
if (m.inFlight) {
|
|
2863
|
+
return m.inFlight;
|
|
2864
|
+
}
|
|
2865
|
+
setState("acquiring");
|
|
2866
|
+
const audio = constraints ?? buildWarmMicConstraints();
|
|
2867
|
+
if (m.preferredInputDeviceId && audio.deviceId == null) {
|
|
2868
|
+
audio.deviceId = { ideal: m.preferredInputDeviceId };
|
|
2869
|
+
}
|
|
2870
|
+
m.inFlight = (async () => {
|
|
2871
|
+
try {
|
|
2872
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio });
|
|
2873
|
+
m.stream = stream;
|
|
2874
|
+
attachTrackHealth(stream);
|
|
2875
|
+
watchPageLifecycle();
|
|
2876
|
+
watchMicPermission();
|
|
2877
|
+
if (m.refCount === 0) {
|
|
2878
|
+
armKeepaliveStop();
|
|
2879
|
+
return stream;
|
|
2880
|
+
}
|
|
2881
|
+
setState("active");
|
|
2882
|
+
return stream;
|
|
2883
|
+
} catch (err) {
|
|
2884
|
+
m.refCount = Math.max(0, m.refCount - 1);
|
|
2885
|
+
setState("error");
|
|
2886
|
+
throw err;
|
|
2887
|
+
} finally {
|
|
2888
|
+
m.inFlight = null;
|
|
2889
|
+
}
|
|
2890
|
+
})();
|
|
2891
|
+
return m.inFlight;
|
|
2892
|
+
}
|
|
2893
|
+
function releaseWarmMic() {
|
|
2894
|
+
const m = getManager();
|
|
2895
|
+
if (m.refCount === 0) {
|
|
2896
|
+
console.error(
|
|
2897
|
+
"[capture:warm-mic] releaseWarmMic() with zero holders \u2014 unbalanced release. Some caller released twice (or released a hold it never acquired)."
|
|
2898
|
+
);
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
m.refCount -= 1;
|
|
2902
|
+
if (m.refCount > 0) return;
|
|
2903
|
+
if (!m.stream) {
|
|
2904
|
+
setState(m.inFlight ? "keepalive" : "idle");
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
armKeepaliveStop();
|
|
2908
|
+
}
|
|
2909
|
+
function armKeepaliveStop() {
|
|
2910
|
+
const m = getManager();
|
|
2911
|
+
clearReleaseTimer();
|
|
2912
|
+
setState("keepalive");
|
|
2913
|
+
m.releaseTimer = setTimeout(() => {
|
|
2914
|
+
m.releaseTimer = null;
|
|
2915
|
+
if (m.refCount > 0) return;
|
|
2916
|
+
hardStopWarmMic();
|
|
2917
|
+
}, m.keepAliveMs);
|
|
2918
|
+
}
|
|
2919
|
+
function hardStopWarmMic() {
|
|
2920
|
+
const m = getManager();
|
|
2921
|
+
clearReleaseTimer();
|
|
2922
|
+
if (m.stream) {
|
|
2923
|
+
for (const t of m.stream.getTracks()) {
|
|
2924
|
+
try {
|
|
2925
|
+
t.stop();
|
|
2926
|
+
} catch {
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
m.stream = null;
|
|
2930
|
+
}
|
|
2931
|
+
m.refCount = 0;
|
|
2932
|
+
setState("idle");
|
|
2933
|
+
}
|
|
2934
|
+
function getWarmMicState() {
|
|
2935
|
+
return getManager().state;
|
|
2936
|
+
}
|
|
2937
|
+
function subscribeWarmMic(listener) {
|
|
2938
|
+
const m = getManager();
|
|
2939
|
+
m.listeners.add(listener);
|
|
2940
|
+
return () => {
|
|
2941
|
+
m.listeners.delete(listener);
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
function warmMicDebug() {
|
|
2945
|
+
const m = getManager();
|
|
2946
|
+
return {
|
|
2947
|
+
state: m.state,
|
|
2948
|
+
refCount: m.refCount,
|
|
2949
|
+
live: streamIsLive(m.stream),
|
|
2950
|
+
keepAliveMs: m.keepAliveMs
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
// src/engine/useDefaultEngine.ts
|
|
2575
2955
|
var RECORDING_MIME_LADDER = [
|
|
2576
2956
|
'video/mp4;codecs="avc1.42E01E,mp4a.40.2"',
|
|
2577
2957
|
"video/mp4",
|
|
@@ -2579,39 +2959,38 @@ var RECORDING_MIME_LADDER = [
|
|
|
2579
2959
|
'video/webm;codecs="vp8,opus"',
|
|
2580
2960
|
"video/webm"
|
|
2581
2961
|
];
|
|
2582
|
-
function cropRect(w, h, aspect) {
|
|
2583
|
-
if (aspect === "full") return { x: 0, y: 0, w, h };
|
|
2584
|
-
const [aw, ah] = aspect === "1:1" ? [1, 1] : aspect === "4:3" ? [3, 4] : [9, 16];
|
|
2585
|
-
const [rw, rh] = w >= h ? [ah, aw] : [aw, ah];
|
|
2586
|
-
const target = rw / rh;
|
|
2587
|
-
let cw = w;
|
|
2588
|
-
let ch = w / target;
|
|
2589
|
-
if (ch > h) {
|
|
2590
|
-
ch = h;
|
|
2591
|
-
cw = h * target;
|
|
2592
|
-
}
|
|
2593
|
-
return { x: (w - cw) / 2, y: (h - ch) / 2, w: cw, h: ch };
|
|
2594
|
-
}
|
|
2595
2962
|
function useDefaultCaptureEngine(options) {
|
|
2596
2963
|
const {
|
|
2597
2964
|
onPhoto,
|
|
2598
2965
|
onVideo,
|
|
2599
2966
|
onFiles,
|
|
2600
2967
|
withAudio = true,
|
|
2968
|
+
mode,
|
|
2601
2969
|
facingMode: initialFacing = "environment",
|
|
2602
|
-
photoQuality = 0.92
|
|
2970
|
+
photoQuality = 0.92,
|
|
2971
|
+
fileNamePrefix = "capture",
|
|
2972
|
+
onError
|
|
2603
2973
|
} = options;
|
|
2604
2974
|
const [stream, setStream] = (0, import_react11.useState)(null);
|
|
2605
2975
|
const [blocked, setBlocked] = (0, import_react11.useState)(null);
|
|
2606
|
-
const [
|
|
2607
|
-
const [
|
|
2976
|
+
const [deviceId, setDeviceId] = (0, import_react11.useState)(null);
|
|
2977
|
+
const [cameras, setCameras] = (0, import_react11.useState)([]);
|
|
2608
2978
|
const [recording, setRecording] = (0, import_react11.useState)(false);
|
|
2609
2979
|
const [recordElapsedSeconds, setRecordElapsedSeconds] = (0, import_react11.useState)(0);
|
|
2980
|
+
const [flash, setFlash] = (0, import_react11.useState)(false);
|
|
2981
|
+
const [acquireToken, setAcquireToken] = (0, import_react11.useState)(0);
|
|
2610
2982
|
const videoRef = (0, import_react11.useRef)(null);
|
|
2611
2983
|
const streamRef = (0, import_react11.useRef)(null);
|
|
2612
2984
|
const recorderRef = (0, import_react11.useRef)(null);
|
|
2613
|
-
const
|
|
2614
|
-
|
|
2985
|
+
const captureBusyRef = (0, import_react11.useRef)(false);
|
|
2986
|
+
const reacquiredOnceRef = (0, import_react11.useRef)(false);
|
|
2987
|
+
const callbacksRef = (0, import_react11.useRef)({ onPhoto, onVideo, onFiles, onError });
|
|
2988
|
+
callbacksRef.current = { onPhoto, onVideo, onFiles, onError };
|
|
2989
|
+
const reportError = (0, import_react11.useCallback)((message, err) => {
|
|
2990
|
+
const sink = callbacksRef.current.onError;
|
|
2991
|
+
if (sink) sink(message, err);
|
|
2992
|
+
else console.error(`[capture:engine] ${message}`, err);
|
|
2993
|
+
}, []);
|
|
2615
2994
|
(0, import_react11.useEffect)(() => {
|
|
2616
2995
|
let cancelled = false;
|
|
2617
2996
|
let acquired = null;
|
|
@@ -2619,110 +2998,176 @@ function useDefaultCaptureEngine(options) {
|
|
|
2619
2998
|
setBlocked({ reason: "not-supported" });
|
|
2620
2999
|
return;
|
|
2621
3000
|
}
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
},
|
|
2628
|
-
audio: false
|
|
2629
|
-
}).then((s) => {
|
|
2630
|
-
if (cancelled) {
|
|
2631
|
-
s.getTracks().forEach((t) => t.stop());
|
|
3001
|
+
void (async () => {
|
|
3002
|
+
const known = await queryCameraPermission();
|
|
3003
|
+
if (cancelled) return;
|
|
3004
|
+
if (known === "denied") {
|
|
3005
|
+
setBlocked({ reason: "permission-denied" });
|
|
2632
3006
|
return;
|
|
2633
3007
|
}
|
|
2634
|
-
|
|
2635
|
-
streamRef.current = s;
|
|
2636
|
-
setStream(s);
|
|
2637
|
-
setBlocked(null);
|
|
2638
|
-
return navigator.mediaDevices.enumerateDevices().then((devices) => {
|
|
2639
|
-
if (!cancelled)
|
|
2640
|
-
setMultipleCameras(
|
|
2641
|
-
devices.filter((d) => d.kind === "videoinput").length > 1
|
|
2642
|
-
);
|
|
2643
|
-
});
|
|
2644
|
-
}).catch((err) => {
|
|
3008
|
+
const includeMic = withAudio && shouldCombineMicPrompt(await queryMicPermission());
|
|
2645
3009
|
if (cancelled) return;
|
|
2646
|
-
const
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
3010
|
+
const video = {
|
|
3011
|
+
width: { ideal: 4096 },
|
|
3012
|
+
height: { ideal: 4096 },
|
|
3013
|
+
...deviceId ? { deviceId: { exact: deviceId } } : { facingMode: initialFacing }
|
|
3014
|
+
};
|
|
3015
|
+
const acquire = async (mic) => {
|
|
3016
|
+
try {
|
|
3017
|
+
return await navigator.mediaDevices.getUserMedia(
|
|
3018
|
+
mic ? { video, audio: buildWarmMicConstraints() } : { video, audio: false }
|
|
3019
|
+
);
|
|
3020
|
+
} catch (err) {
|
|
3021
|
+
if (mic && classifyCameraBlockReason(err) === "permission-denied") {
|
|
3022
|
+
return navigator.mediaDevices.getUserMedia({ video, audio: false });
|
|
3023
|
+
}
|
|
3024
|
+
throw err;
|
|
3025
|
+
}
|
|
3026
|
+
};
|
|
3027
|
+
try {
|
|
3028
|
+
const s = await acquire(includeMic);
|
|
3029
|
+
if (cancelled) {
|
|
3030
|
+
s.getTracks().forEach((t) => t.stop());
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
const audioTracks = s.getAudioTracks();
|
|
3034
|
+
if (audioTracks.length > 0) {
|
|
3035
|
+
for (const t of audioTracks) s.removeTrack(t);
|
|
3036
|
+
adoptWarmAudioStream(new MediaStream(audioTracks));
|
|
3037
|
+
}
|
|
3038
|
+
acquired = s;
|
|
3039
|
+
streamRef.current = s;
|
|
3040
|
+
setStream(s);
|
|
3041
|
+
setBlocked(null);
|
|
3042
|
+
reacquiredOnceRef.current = false;
|
|
3043
|
+
for (const track of s.getVideoTracks()) {
|
|
3044
|
+
track.onended = () => {
|
|
3045
|
+
if (cancelled || streamRef.current !== s) return;
|
|
3046
|
+
console.error(
|
|
3047
|
+
"[capture:engine] camera track ENDED (OS interruption / device change) \u2014 reacquiring."
|
|
3048
|
+
);
|
|
3049
|
+
if (reacquiredOnceRef.current) {
|
|
3050
|
+
setBlocked({ reason: "not-supported" });
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
reacquiredOnceRef.current = true;
|
|
3054
|
+
setAcquireToken((n) => n + 1);
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
3058
|
+
if (!cancelled) {
|
|
3059
|
+
setCameras(devices.filter((d) => d.kind === "videoinput"));
|
|
3060
|
+
}
|
|
3061
|
+
} catch (err) {
|
|
3062
|
+
if (cancelled) return;
|
|
3063
|
+
setBlocked({ reason: classifyCameraBlockReason(err) });
|
|
3064
|
+
}
|
|
3065
|
+
})();
|
|
2651
3066
|
return () => {
|
|
2652
3067
|
cancelled = true;
|
|
2653
3068
|
acquired?.getTracks().forEach((t) => t.stop());
|
|
2654
3069
|
if (streamRef.current === acquired) streamRef.current = null;
|
|
2655
3070
|
setStream(null);
|
|
2656
3071
|
};
|
|
2657
|
-
}, [
|
|
3072
|
+
}, [deviceId, initialFacing, withAudio, acquireToken]);
|
|
3073
|
+
const cameraBlocked = blocked !== null;
|
|
3074
|
+
(0, import_react11.useEffect)(() => {
|
|
3075
|
+
if (mode !== "video" || cameraBlocked || !withAudio) return;
|
|
3076
|
+
let cancelled = false;
|
|
3077
|
+
let held = false;
|
|
3078
|
+
void (async () => {
|
|
3079
|
+
if (await queryMicPermission() === "denied") return;
|
|
3080
|
+
if (cancelled) return;
|
|
3081
|
+
acquireWarmMic().then(() => {
|
|
3082
|
+
if (cancelled) {
|
|
3083
|
+
releaseWarmMic();
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
3086
|
+
held = true;
|
|
3087
|
+
}).catch(() => {
|
|
3088
|
+
});
|
|
3089
|
+
})();
|
|
3090
|
+
return () => {
|
|
3091
|
+
cancelled = true;
|
|
3092
|
+
if (held) releaseWarmMic();
|
|
3093
|
+
};
|
|
3094
|
+
}, [mode, cameraBlocked, withAudio]);
|
|
2658
3095
|
(0, import_react11.useEffect)(() => {
|
|
2659
3096
|
return () => {
|
|
2660
3097
|
const r = recorderRef.current;
|
|
2661
3098
|
if (r) {
|
|
2662
3099
|
clearInterval(r.timer);
|
|
2663
3100
|
if (r.recorder.state !== "inactive") r.recorder.stop();
|
|
2664
|
-
r.micTracks.forEach((t) => t.stop());
|
|
2665
3101
|
}
|
|
2666
3102
|
};
|
|
2667
3103
|
}, []);
|
|
2668
|
-
const
|
|
3104
|
+
const capturePhotoWith = (0, import_react11.useCallback)(
|
|
2669
3105
|
(opts) => {
|
|
2670
3106
|
const video = videoRef.current;
|
|
2671
3107
|
if (!video || video.videoWidth === 0) return;
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
(blob) => {
|
|
2695
|
-
if (!blob) return;
|
|
3108
|
+
if (captureBusyRef.current) return;
|
|
3109
|
+
captureBusyRef.current = true;
|
|
3110
|
+
void (async () => {
|
|
3111
|
+
try {
|
|
3112
|
+
const canvas = document.createElement("canvas");
|
|
3113
|
+
canvas.width = video.videoWidth;
|
|
3114
|
+
canvas.height = video.videoHeight;
|
|
3115
|
+
const ctx = canvas.getContext("2d");
|
|
3116
|
+
if (!ctx) throw new Error("2d canvas context unavailable");
|
|
3117
|
+
ctx.drawImage(video, 0, 0);
|
|
3118
|
+
const full = await new Promise(
|
|
3119
|
+
(resolve, reject) => canvas.toBlob(
|
|
3120
|
+
(b) => b ? resolve(b) : reject(new Error("toBlob returned null")),
|
|
3121
|
+
"image/jpeg",
|
|
3122
|
+
photoQuality
|
|
3123
|
+
)
|
|
3124
|
+
);
|
|
3125
|
+
const blob = await cropBlobToAspect(
|
|
3126
|
+
full,
|
|
3127
|
+
opts.aspect ?? "full",
|
|
3128
|
+
photoQuality
|
|
3129
|
+
);
|
|
2696
3130
|
callbacksRef.current.onPhoto(
|
|
2697
|
-
new File(
|
|
2698
|
-
|
|
2699
|
-
|
|
3131
|
+
new File(
|
|
3132
|
+
[blob],
|
|
3133
|
+
`${opts.fileNamePrefix}-${(/* @__PURE__ */ new Date()).toISOString()}.jpg`,
|
|
3134
|
+
{ type: "image/jpeg" }
|
|
3135
|
+
)
|
|
2700
3136
|
);
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
3137
|
+
setFlash(true);
|
|
3138
|
+
window.setTimeout(() => setFlash(false), 120);
|
|
3139
|
+
} catch (err) {
|
|
3140
|
+
reportError("The photo could not be captured \u2014 try again.", err);
|
|
3141
|
+
} finally {
|
|
3142
|
+
captureBusyRef.current = false;
|
|
3143
|
+
}
|
|
3144
|
+
})();
|
|
3145
|
+
},
|
|
3146
|
+
[photoQuality, reportError]
|
|
3147
|
+
);
|
|
3148
|
+
const onCapturePhoto = (0, import_react11.useCallback)(
|
|
3149
|
+
(opts) => {
|
|
3150
|
+
capturePhotoWith(
|
|
3151
|
+
opts?.aspect ? { fileNamePrefix, aspect: opts.aspect } : { fileNamePrefix }
|
|
2704
3152
|
);
|
|
2705
3153
|
},
|
|
2706
|
-
[
|
|
3154
|
+
[capturePhotoWith, fileNamePrefix]
|
|
2707
3155
|
);
|
|
2708
3156
|
const onStartRecording = (0, import_react11.useCallback)(() => {
|
|
2709
3157
|
const base = streamRef.current;
|
|
2710
3158
|
if (!base || recorderRef.current) return;
|
|
2711
3159
|
void (async () => {
|
|
2712
|
-
let
|
|
3160
|
+
let micClones = [];
|
|
3161
|
+
let micHeld = false;
|
|
2713
3162
|
if (withAudio) {
|
|
2714
3163
|
try {
|
|
2715
|
-
const mic = await
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
micTracks = mic.getAudioTracks();
|
|
3164
|
+
const mic = await acquireWarmMic();
|
|
3165
|
+
micHeld = true;
|
|
3166
|
+
micClones = mic.getAudioTracks().map((t) => t.clone());
|
|
2719
3167
|
} catch {
|
|
2720
3168
|
}
|
|
2721
3169
|
}
|
|
2722
|
-
const composed = new MediaStream([
|
|
2723
|
-
...base.getVideoTracks(),
|
|
2724
|
-
...micTracks
|
|
2725
|
-
]);
|
|
3170
|
+
const composed = new MediaStream([...base.getVideoTracks(), ...micClones]);
|
|
2726
3171
|
const mime = RECORDING_MIME_LADDER.find(
|
|
2727
3172
|
(m) => typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported(m)
|
|
2728
3173
|
);
|
|
@@ -2732,15 +3177,18 @@ function useDefaultCaptureEngine(options) {
|
|
|
2732
3177
|
composed,
|
|
2733
3178
|
mime ? { mimeType: mime } : void 0
|
|
2734
3179
|
);
|
|
2735
|
-
} catch {
|
|
2736
|
-
|
|
3180
|
+
} catch (err) {
|
|
3181
|
+
micClones.forEach((t) => t.stop());
|
|
3182
|
+
if (micHeld) releaseWarmMic();
|
|
3183
|
+
reportError("Could not start the video recording.", err);
|
|
2737
3184
|
return;
|
|
2738
3185
|
}
|
|
2739
3186
|
const entry = {
|
|
2740
3187
|
recorder,
|
|
2741
3188
|
chunks: [],
|
|
2742
3189
|
startedAt: performance.now(),
|
|
2743
|
-
|
|
3190
|
+
micClones,
|
|
3191
|
+
micHeld,
|
|
2744
3192
|
timer: setInterval(() => {
|
|
2745
3193
|
setRecordElapsedSeconds(
|
|
2746
3194
|
Math.floor((performance.now() - entry.startedAt) / 1e3)
|
|
@@ -2753,7 +3201,8 @@ function useDefaultCaptureEngine(options) {
|
|
|
2753
3201
|
};
|
|
2754
3202
|
recorder.onstop = () => {
|
|
2755
3203
|
clearInterval(entry.timer);
|
|
2756
|
-
entry.
|
|
3204
|
+
entry.micClones.forEach((t) => t.stop());
|
|
3205
|
+
if (entry.micHeld) releaseWarmMic();
|
|
2757
3206
|
recorderRef.current = null;
|
|
2758
3207
|
setRecording(false);
|
|
2759
3208
|
const durationMs = Math.round(performance.now() - entry.startedAt);
|
|
@@ -2761,7 +3210,7 @@ function useDefaultCaptureEngine(options) {
|
|
|
2761
3210
|
const blob = new Blob(entry.chunks, { type });
|
|
2762
3211
|
const ext = type.includes("mp4") ? "mp4" : "webm";
|
|
2763
3212
|
callbacksRef.current.onVideo(
|
|
2764
|
-
new File([blob],
|
|
3213
|
+
new File([blob], `${fileNamePrefix}-video-${Date.now()}.${ext}`, {
|
|
2765
3214
|
type
|
|
2766
3215
|
}),
|
|
2767
3216
|
durationMs
|
|
@@ -2771,7 +3220,7 @@ function useDefaultCaptureEngine(options) {
|
|
|
2771
3220
|
setRecordElapsedSeconds(0);
|
|
2772
3221
|
setRecording(true);
|
|
2773
3222
|
})();
|
|
2774
|
-
}, [withAudio]);
|
|
3223
|
+
}, [withAudio, fileNamePrefix, reportError]);
|
|
2775
3224
|
const onStopRecording = (0, import_react11.useCallback)(() => {
|
|
2776
3225
|
const r = recorderRef.current;
|
|
2777
3226
|
if (r && r.recorder.state !== "inactive") r.recorder.stop();
|
|
@@ -2788,8 +3237,12 @@ function useDefaultCaptureEngine(options) {
|
|
|
2788
3237
|
input.click();
|
|
2789
3238
|
}, []);
|
|
2790
3239
|
const onFlipCamera = (0, import_react11.useCallback)(() => {
|
|
2791
|
-
|
|
2792
|
-
|
|
3240
|
+
if (recorderRef.current) return;
|
|
3241
|
+
const current = deviceId ?? streamRef.current?.getVideoTracks()[0]?.getSettings().deviceId ?? null;
|
|
3242
|
+
const next = nextCameraDevice(cameras, current);
|
|
3243
|
+
if (next) setDeviceId(next.deviceId);
|
|
3244
|
+
}, [cameras, deviceId]);
|
|
3245
|
+
const canFlip = cameras.length > 1 && !cameraBlocked && !recording;
|
|
2793
3246
|
return (0, import_react11.useMemo)(
|
|
2794
3247
|
() => ({
|
|
2795
3248
|
stream,
|
|
@@ -2801,7 +3254,9 @@ function useDefaultCaptureEngine(options) {
|
|
|
2801
3254
|
recording,
|
|
2802
3255
|
recordElapsedSeconds,
|
|
2803
3256
|
onUpload,
|
|
2804
|
-
onFlipCamera:
|
|
3257
|
+
onFlipCamera: canFlip ? onFlipCamera : null,
|
|
3258
|
+
flash,
|
|
3259
|
+
capturePhotoWith
|
|
2805
3260
|
}),
|
|
2806
3261
|
[
|
|
2807
3262
|
stream,
|
|
@@ -2813,8 +3268,129 @@ function useDefaultCaptureEngine(options) {
|
|
|
2813
3268
|
recordElapsedSeconds,
|
|
2814
3269
|
onUpload,
|
|
2815
3270
|
onFlipCamera,
|
|
2816
|
-
|
|
3271
|
+
canFlip,
|
|
3272
|
+
flash,
|
|
3273
|
+
capturePhotoWith
|
|
2817
3274
|
]
|
|
2818
3275
|
);
|
|
2819
3276
|
}
|
|
3277
|
+
|
|
3278
|
+
// src/components/CloudLibrarySheet.tsx
|
|
3279
|
+
var import_react12 = require("react");
|
|
3280
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
3281
|
+
var PAGE_SIZE = 60;
|
|
3282
|
+
function CloudLibrarySheet({
|
|
3283
|
+
open,
|
|
3284
|
+
onClose,
|
|
3285
|
+
items,
|
|
3286
|
+
loading,
|
|
3287
|
+
busy = false,
|
|
3288
|
+
onOpenItem,
|
|
3289
|
+
onUpload,
|
|
3290
|
+
title = "Your media"
|
|
3291
|
+
}) {
|
|
3292
|
+
const [visibleCount, setVisibleCount] = (0, import_react12.useState)(PAGE_SIZE);
|
|
3293
|
+
(0, import_react12.useEffect)(() => {
|
|
3294
|
+
if (open) setVisibleCount(PAGE_SIZE);
|
|
3295
|
+
}, [open]);
|
|
3296
|
+
if (!open) return null;
|
|
3297
|
+
const uploadThenClose = onUpload ? () => {
|
|
3298
|
+
onClose();
|
|
3299
|
+
onUpload();
|
|
3300
|
+
} : null;
|
|
3301
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "absolute inset-0 z-50 flex flex-col bg-black", children: [
|
|
3302
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
|
|
3303
|
+
"div",
|
|
3304
|
+
{
|
|
3305
|
+
className: "flex shrink-0 items-center justify-between bg-black/80 px-4",
|
|
3306
|
+
style: safeTop,
|
|
3307
|
+
children: [
|
|
3308
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h2", { className: "py-4 text-[17px] font-semibold text-white", children: title }),
|
|
3309
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
3310
|
+
uploadThenClose ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
|
|
3311
|
+
"button",
|
|
3312
|
+
{
|
|
3313
|
+
type: "button",
|
|
3314
|
+
onClick: uploadThenClose,
|
|
3315
|
+
className: "flex h-10 touch-manipulation items-center gap-1.5 rounded-full bg-white/10 px-3.5 text-[13px] font-medium text-white transition-colors hover:bg-white/20",
|
|
3316
|
+
children: [
|
|
3317
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(UploadIcon, { className: "h-4 w-4" }),
|
|
3318
|
+
"Upload"
|
|
3319
|
+
]
|
|
3320
|
+
}
|
|
3321
|
+
) : null,
|
|
3322
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3323
|
+
"button",
|
|
3324
|
+
{
|
|
3325
|
+
type: "button",
|
|
3326
|
+
onClick: onClose,
|
|
3327
|
+
"aria-label": "Close library",
|
|
3328
|
+
className: "flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20",
|
|
3329
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(XIcon, { className: "h-5 w-5" })
|
|
3330
|
+
}
|
|
3331
|
+
)
|
|
3332
|
+
] })
|
|
3333
|
+
]
|
|
3334
|
+
}
|
|
3335
|
+
),
|
|
3336
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
|
|
3337
|
+
"div",
|
|
3338
|
+
{
|
|
3339
|
+
className: "min-h-0 flex-1 overflow-y-auto overscroll-contain px-1",
|
|
3340
|
+
style: safeBottom,
|
|
3341
|
+
children: [
|
|
3342
|
+
loading ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Loader2Icon, { className: "h-6 w-6 animate-spin text-white/60" }) }) : items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex h-full flex-col items-center justify-center gap-3 text-white/60", children: [
|
|
3343
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ImageOffIcon, { className: "h-8 w-8" }),
|
|
3344
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-sm", children: "No photos or videos in your cloud yet." }),
|
|
3345
|
+
uploadThenClose ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
|
|
3346
|
+
"button",
|
|
3347
|
+
{
|
|
3348
|
+
type: "button",
|
|
3349
|
+
onClick: uploadThenClose,
|
|
3350
|
+
className: "flex h-11 touch-manipulation items-center gap-2 rounded-full bg-white/10 px-5 text-sm font-medium text-white transition-colors hover:bg-white/20",
|
|
3351
|
+
children: [
|
|
3352
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(UploadIcon, { className: "h-4 w-4" }),
|
|
3353
|
+
"Upload from this device"
|
|
3354
|
+
]
|
|
3355
|
+
}
|
|
3356
|
+
) : null
|
|
3357
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "grid grid-cols-3 gap-0.5 sm:grid-cols-5 md:grid-cols-7", children: items.slice(0, visibleCount).map((item) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
|
|
3358
|
+
"button",
|
|
3359
|
+
{
|
|
3360
|
+
type: "button",
|
|
3361
|
+
onClick: () => onOpenItem(item.id),
|
|
3362
|
+
"aria-label": `Open ${item.fileName}`,
|
|
3363
|
+
className: "relative aspect-square overflow-hidden bg-white/5 transition-opacity active:opacity-70",
|
|
3364
|
+
children: [
|
|
3365
|
+
item.thumbnail,
|
|
3366
|
+
item.kind === "video" && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
3367
|
+
PlayIcon,
|
|
3368
|
+
{
|
|
3369
|
+
className: "absolute bottom-1.5 left-1.5 h-4 w-4 text-white drop-shadow",
|
|
3370
|
+
fill: "currentColor"
|
|
3371
|
+
}
|
|
3372
|
+
)
|
|
3373
|
+
]
|
|
3374
|
+
},
|
|
3375
|
+
item.id
|
|
3376
|
+
)) }),
|
|
3377
|
+
!loading && items.length > visibleCount && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "flex justify-center py-4", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
|
|
3378
|
+
"button",
|
|
3379
|
+
{
|
|
3380
|
+
type: "button",
|
|
3381
|
+
onClick: () => setVisibleCount((n) => n + PAGE_SIZE),
|
|
3382
|
+
className: "h-11 touch-manipulation rounded-full bg-white/10 px-6 text-sm font-medium text-white",
|
|
3383
|
+
children: [
|
|
3384
|
+
"Show more (",
|
|
3385
|
+
items.length - visibleCount,
|
|
3386
|
+
" left)"
|
|
3387
|
+
]
|
|
3388
|
+
}
|
|
3389
|
+
) }),
|
|
3390
|
+
busy && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-black/40", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Loader2Icon, { className: "h-6 w-6 animate-spin text-white" }) })
|
|
3391
|
+
]
|
|
3392
|
+
}
|
|
3393
|
+
)
|
|
3394
|
+
] });
|
|
3395
|
+
}
|
|
2820
3396
|
//# sourceMappingURL=react.cjs.map
|