@ai-matrx/capture 0.2.0 → 0.3.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 +17 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -1
- package/dist/index.d.ts +49 -1
- package/dist/react.cjs +681 -22
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +228 -2
- package/dist/react.d.ts +228 -2
- package/dist/react.js +694 -25
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
package/dist/react.js
CHANGED
|
@@ -1271,6 +1271,7 @@ function CameraCapture({
|
|
|
1271
1271
|
onModeChange,
|
|
1272
1272
|
preview,
|
|
1273
1273
|
media,
|
|
1274
|
+
onReviewOpenChange,
|
|
1274
1275
|
onClose,
|
|
1275
1276
|
blockedSheet,
|
|
1276
1277
|
controlsHidden = false,
|
|
@@ -1288,6 +1289,10 @@ function CameraCapture({
|
|
|
1288
1289
|
const [blockedDismissed, setBlockedDismissed] = useState5(false);
|
|
1289
1290
|
const countdownRef = useRef3(null);
|
|
1290
1291
|
const controls = useTrackControls(engine.stream);
|
|
1292
|
+
const reviewOpen = viewerKey !== null || editTarget !== null;
|
|
1293
|
+
useEffect5(() => {
|
|
1294
|
+
onReviewOpenChange?.(reviewOpen);
|
|
1295
|
+
}, [reviewOpen, onReviewOpenChange]);
|
|
1291
1296
|
const clearCountdown = useCallback4(() => {
|
|
1292
1297
|
if (countdownRef.current) clearInterval(countdownRef.current);
|
|
1293
1298
|
countdownRef.current = null;
|
|
@@ -1617,15 +1622,675 @@ function CameraCapture({
|
|
|
1617
1622
|
] });
|
|
1618
1623
|
}
|
|
1619
1624
|
|
|
1625
|
+
// src/components/CameraCaptureV3.tsx
|
|
1626
|
+
import { useCallback as useCallback6, useEffect as useEffect7, useRef as useRef5, useState as useState8 } from "react";
|
|
1627
|
+
import {
|
|
1628
|
+
Grid3x3 as Grid3x32,
|
|
1629
|
+
Images,
|
|
1630
|
+
Proportions as Proportions2,
|
|
1631
|
+
RefreshCw as RefreshCw2,
|
|
1632
|
+
SunMedium as SunMedium2,
|
|
1633
|
+
Timer as Timer2,
|
|
1634
|
+
X as X5,
|
|
1635
|
+
Zap as Zap2,
|
|
1636
|
+
ZapOff as ZapOff2
|
|
1637
|
+
} from "lucide-react";
|
|
1638
|
+
|
|
1639
|
+
// src/components/CaptureRail.tsx
|
|
1640
|
+
import { useState as useState6 } from "react";
|
|
1641
|
+
import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1642
|
+
function CaptureRail({
|
|
1643
|
+
actions,
|
|
1644
|
+
defaultExpanded = false,
|
|
1645
|
+
className
|
|
1646
|
+
}) {
|
|
1647
|
+
const [expanded, setExpanded] = useState6(defaultExpanded);
|
|
1648
|
+
const primary = actions.filter((a) => a.primary);
|
|
1649
|
+
const extra = actions.filter((a) => !a.primary);
|
|
1650
|
+
const shown = primary.length === 0 ? actions : expanded ? actions : primary;
|
|
1651
|
+
const collapsible = primary.length > 0 && extra.length > 0;
|
|
1652
|
+
if (actions.length === 0) return null;
|
|
1653
|
+
return /* @__PURE__ */ jsxs10(
|
|
1654
|
+
"div",
|
|
1655
|
+
{
|
|
1656
|
+
className: cn(
|
|
1657
|
+
"pointer-events-auto flex flex-col items-center gap-1",
|
|
1658
|
+
className
|
|
1659
|
+
),
|
|
1660
|
+
children: [
|
|
1661
|
+
shown.map((action) => /* @__PURE__ */ jsx12(RailButton, { action }, action.id)),
|
|
1662
|
+
collapsible ? /* @__PURE__ */ jsx12(
|
|
1663
|
+
"button",
|
|
1664
|
+
{
|
|
1665
|
+
type: "button",
|
|
1666
|
+
onClick: () => setExpanded((open) => !open),
|
|
1667
|
+
"aria-expanded": expanded,
|
|
1668
|
+
"aria-label": expanded ? "Fewer options" : "More options",
|
|
1669
|
+
className: cn(
|
|
1670
|
+
"mt-0.5 flex h-9 w-9 touch-manipulation items-center justify-center",
|
|
1671
|
+
"rounded-full text-white/90 transition-colors hover:bg-white/10"
|
|
1672
|
+
),
|
|
1673
|
+
children: /* @__PURE__ */ jsx12(
|
|
1674
|
+
Chevron,
|
|
1675
|
+
{
|
|
1676
|
+
className: cn(
|
|
1677
|
+
"h-5 w-5 transition-transform duration-200",
|
|
1678
|
+
expanded && "rotate-180"
|
|
1679
|
+
)
|
|
1680
|
+
}
|
|
1681
|
+
)
|
|
1682
|
+
}
|
|
1683
|
+
) : null
|
|
1684
|
+
]
|
|
1685
|
+
}
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
function RailButton({ action }) {
|
|
1689
|
+
return /* @__PURE__ */ jsxs10(
|
|
1690
|
+
"button",
|
|
1691
|
+
{
|
|
1692
|
+
type: "button",
|
|
1693
|
+
onClick: action.onPress,
|
|
1694
|
+
disabled: action.disabled,
|
|
1695
|
+
"aria-label": action.label,
|
|
1696
|
+
"aria-pressed": action.active ?? void 0,
|
|
1697
|
+
className: cn(
|
|
1698
|
+
"flex w-12 shrink-0 touch-manipulation flex-col items-center justify-center gap-0.5",
|
|
1699
|
+
"rounded-2xl py-1.5 transition-colors",
|
|
1700
|
+
action.active ? "text-[#FFCC00]" : "text-white",
|
|
1701
|
+
action.disabled ? "opacity-40" : "hover:bg-white/10 active:bg-white/15"
|
|
1702
|
+
),
|
|
1703
|
+
children: [
|
|
1704
|
+
/* @__PURE__ */ jsx12("span", { className: "drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)]", children: action.icon }),
|
|
1705
|
+
action.valueLabel ? /* @__PURE__ */ jsx12("span", { className: "text-[10px] font-semibold leading-none tabular-nums drop-shadow-[0_1px_2px_rgba(0,0,0,0.7)]", children: action.valueLabel }) : null
|
|
1706
|
+
]
|
|
1707
|
+
}
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
function Chevron({ className }) {
|
|
1711
|
+
return /* @__PURE__ */ jsx12(
|
|
1712
|
+
"svg",
|
|
1713
|
+
{
|
|
1714
|
+
viewBox: "0 0 24 24",
|
|
1715
|
+
fill: "none",
|
|
1716
|
+
stroke: "currentColor",
|
|
1717
|
+
strokeWidth: "2.5",
|
|
1718
|
+
strokeLinecap: "round",
|
|
1719
|
+
strokeLinejoin: "round",
|
|
1720
|
+
className,
|
|
1721
|
+
"aria-hidden": "true",
|
|
1722
|
+
children: /* @__PURE__ */ jsx12("path", { d: "m6 9 6 6 6-6" })
|
|
1723
|
+
}
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// src/components/HoldShutter.tsx
|
|
1728
|
+
import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef4, useState as useState7 } from "react";
|
|
1729
|
+
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1730
|
+
var HOLD_MS = 320;
|
|
1731
|
+
var LOCK_AFTER_MS = 1200;
|
|
1732
|
+
function HoldShutter({
|
|
1733
|
+
recording,
|
|
1734
|
+
elapsedSeconds,
|
|
1735
|
+
maxSeconds,
|
|
1736
|
+
disabled = false,
|
|
1737
|
+
onPressStart,
|
|
1738
|
+
onPhoto,
|
|
1739
|
+
onStartRecording,
|
|
1740
|
+
onStopRecording
|
|
1741
|
+
}) {
|
|
1742
|
+
const holdTimer = useRef4(null);
|
|
1743
|
+
const startedRecording = useRef4(false);
|
|
1744
|
+
const [pressed, setPressed] = useState7(false);
|
|
1745
|
+
const [locked, setLocked] = useState7(false);
|
|
1746
|
+
const clearHold = useCallback5(() => {
|
|
1747
|
+
if (holdTimer.current !== null) {
|
|
1748
|
+
clearTimeout(holdTimer.current);
|
|
1749
|
+
holdTimer.current = null;
|
|
1750
|
+
}
|
|
1751
|
+
}, []);
|
|
1752
|
+
useEffect6(() => {
|
|
1753
|
+
if (!recording) {
|
|
1754
|
+
setLocked(false);
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
if (elapsedSeconds * 1e3 >= LOCK_AFTER_MS) setLocked(true);
|
|
1758
|
+
}, [recording, elapsedSeconds]);
|
|
1759
|
+
useEffect6(() => clearHold, [clearHold]);
|
|
1760
|
+
const onPointerDown = useCallback5(
|
|
1761
|
+
(event) => {
|
|
1762
|
+
if (disabled) return;
|
|
1763
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
1764
|
+
setPressed(true);
|
|
1765
|
+
onPressStart?.();
|
|
1766
|
+
if (recording && locked) return;
|
|
1767
|
+
startedRecording.current = false;
|
|
1768
|
+
holdTimer.current = setTimeout(() => {
|
|
1769
|
+
holdTimer.current = null;
|
|
1770
|
+
startedRecording.current = true;
|
|
1771
|
+
onStartRecording();
|
|
1772
|
+
}, HOLD_MS);
|
|
1773
|
+
},
|
|
1774
|
+
[disabled, locked, onPressStart, onStartRecording, recording]
|
|
1775
|
+
);
|
|
1776
|
+
const endPress = useCallback5(() => {
|
|
1777
|
+
if (!pressed) return;
|
|
1778
|
+
setPressed(false);
|
|
1779
|
+
const wasHold = startedRecording.current;
|
|
1780
|
+
clearHold();
|
|
1781
|
+
startedRecording.current = false;
|
|
1782
|
+
if (recording && locked && !wasHold) {
|
|
1783
|
+
onStopRecording();
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
if (wasHold) {
|
|
1787
|
+
if (!locked) onStopRecording();
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
if (!recording) onPhoto();
|
|
1791
|
+
}, [clearHold, locked, onPhoto, onStopRecording, pressed, recording]);
|
|
1792
|
+
const progress = maxSeconds && maxSeconds > 0 ? Math.min(1, elapsedSeconds / maxSeconds) : 0;
|
|
1793
|
+
const R = 39;
|
|
1794
|
+
const C = 2 * Math.PI * R;
|
|
1795
|
+
return /* @__PURE__ */ jsxs11("div", { className: "relative flex h-[86px] w-[86px] shrink-0 items-center justify-center", children: [
|
|
1796
|
+
recording && maxSeconds ? /* @__PURE__ */ jsxs11(
|
|
1797
|
+
"svg",
|
|
1798
|
+
{
|
|
1799
|
+
className: "pointer-events-none absolute inset-0 -rotate-90",
|
|
1800
|
+
viewBox: "0 0 86 86",
|
|
1801
|
+
"aria-hidden": "true",
|
|
1802
|
+
children: [
|
|
1803
|
+
/* @__PURE__ */ jsx13(
|
|
1804
|
+
"circle",
|
|
1805
|
+
{
|
|
1806
|
+
cx: "43",
|
|
1807
|
+
cy: "43",
|
|
1808
|
+
r: R,
|
|
1809
|
+
fill: "none",
|
|
1810
|
+
stroke: "rgba(255,255,255,0.25)",
|
|
1811
|
+
strokeWidth: "4"
|
|
1812
|
+
}
|
|
1813
|
+
),
|
|
1814
|
+
/* @__PURE__ */ jsx13(
|
|
1815
|
+
"circle",
|
|
1816
|
+
{
|
|
1817
|
+
cx: "43",
|
|
1818
|
+
cy: "43",
|
|
1819
|
+
r: R,
|
|
1820
|
+
fill: "none",
|
|
1821
|
+
stroke: "#FF3B30",
|
|
1822
|
+
strokeWidth: "4",
|
|
1823
|
+
strokeLinecap: "round",
|
|
1824
|
+
strokeDasharray: C,
|
|
1825
|
+
strokeDashoffset: C * (1 - progress)
|
|
1826
|
+
}
|
|
1827
|
+
)
|
|
1828
|
+
]
|
|
1829
|
+
}
|
|
1830
|
+
) : null,
|
|
1831
|
+
/* @__PURE__ */ jsx13(
|
|
1832
|
+
"button",
|
|
1833
|
+
{
|
|
1834
|
+
type: "button",
|
|
1835
|
+
disabled,
|
|
1836
|
+
onPointerDown,
|
|
1837
|
+
onPointerUp: endPress,
|
|
1838
|
+
onPointerCancel: endPress,
|
|
1839
|
+
onLostPointerCapture: endPress,
|
|
1840
|
+
onContextMenu: (e) => e.preventDefault(),
|
|
1841
|
+
"aria-label": recording ? locked ? "Stop recording" : "Recording \u2014 release to stop" : "Tap for a photo, hold to record",
|
|
1842
|
+
className: cn(
|
|
1843
|
+
"group flex h-[74px] w-[74px] shrink-0 touch-none select-none items-center justify-center",
|
|
1844
|
+
"rounded-full border-[3.5px] border-white transition-opacity",
|
|
1845
|
+
disabled && "opacity-30"
|
|
1846
|
+
),
|
|
1847
|
+
children: /* @__PURE__ */ jsx13(
|
|
1848
|
+
"span",
|
|
1849
|
+
{
|
|
1850
|
+
className: cn(
|
|
1851
|
+
"block transition-all duration-200 ease-out",
|
|
1852
|
+
recording ? "h-7 w-7 rounded-[6px] bg-[#FF3B30]" : cn(
|
|
1853
|
+
"h-[62px] w-[62px] rounded-full bg-white",
|
|
1854
|
+
pressed && "scale-90"
|
|
1855
|
+
)
|
|
1856
|
+
)
|
|
1857
|
+
}
|
|
1858
|
+
)
|
|
1859
|
+
}
|
|
1860
|
+
),
|
|
1861
|
+
recording ? /* @__PURE__ */ jsxs11(
|
|
1862
|
+
"span",
|
|
1863
|
+
{
|
|
1864
|
+
className: cn(
|
|
1865
|
+
"pointer-events-none absolute -bottom-1 left-1/2 -translate-x-1/2 translate-y-full",
|
|
1866
|
+
"rounded-full bg-black/60 px-2 py-0.5 text-[11px] font-medium tabular-nums text-white"
|
|
1867
|
+
),
|
|
1868
|
+
children: [
|
|
1869
|
+
formatElapsed2(elapsedSeconds),
|
|
1870
|
+
locked ? " \xB7 tap to stop" : ""
|
|
1871
|
+
]
|
|
1872
|
+
}
|
|
1873
|
+
) : null
|
|
1874
|
+
] });
|
|
1875
|
+
}
|
|
1876
|
+
function formatElapsed2(totalSeconds) {
|
|
1877
|
+
const m = Math.floor(totalSeconds / 60);
|
|
1878
|
+
const s = String(Math.floor(totalSeconds % 60)).padStart(2, "0");
|
|
1879
|
+
return `${m}:${s}`;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// src/components/CameraCaptureV3.tsx
|
|
1883
|
+
import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1884
|
+
var ASPECT_CYCLE2 = ["full", "4:3", "1:1", "16:9"];
|
|
1885
|
+
function CameraCaptureV3({
|
|
1886
|
+
engine,
|
|
1887
|
+
cloud,
|
|
1888
|
+
preview,
|
|
1889
|
+
media,
|
|
1890
|
+
onRecordIntent,
|
|
1891
|
+
maxRecordSeconds,
|
|
1892
|
+
onReviewOpenChange,
|
|
1893
|
+
onClose,
|
|
1894
|
+
blockedSheet,
|
|
1895
|
+
controlsHidden = false,
|
|
1896
|
+
shutterDisabled = false,
|
|
1897
|
+
slots = {}
|
|
1898
|
+
}) {
|
|
1899
|
+
const [viewerKey, setViewerKey] = useState8(null);
|
|
1900
|
+
const [editTarget, setEditTarget] = useState8(
|
|
1901
|
+
null
|
|
1902
|
+
);
|
|
1903
|
+
const [gridOn, setGridOn] = useState8(false);
|
|
1904
|
+
const [timerSetting, setTimerSetting] = useState8(0);
|
|
1905
|
+
const [aspect, setAspect] = useState8("full");
|
|
1906
|
+
const [countdown, setCountdown] = useState8(null);
|
|
1907
|
+
const [blockedDismissed, setBlockedDismissed] = useState8(false);
|
|
1908
|
+
const countdownRef = useRef5(null);
|
|
1909
|
+
const controls = useTrackControls(engine.stream);
|
|
1910
|
+
const reviewOpen = viewerKey !== null || editTarget !== null;
|
|
1911
|
+
useEffect7(() => {
|
|
1912
|
+
onReviewOpenChange?.(reviewOpen);
|
|
1913
|
+
}, [reviewOpen, onReviewOpenChange]);
|
|
1914
|
+
const clearCountdown = useCallback6(() => {
|
|
1915
|
+
if (countdownRef.current) clearInterval(countdownRef.current);
|
|
1916
|
+
countdownRef.current = null;
|
|
1917
|
+
setCountdown(null);
|
|
1918
|
+
}, []);
|
|
1919
|
+
useEffect7(() => clearCountdown, [clearCountdown]);
|
|
1920
|
+
useEffect7(() => {
|
|
1921
|
+
if (engine.recording) clearCountdown();
|
|
1922
|
+
}, [engine.recording, clearCountdown]);
|
|
1923
|
+
const onPhoto = useCallback6(() => {
|
|
1924
|
+
if (countdown !== null) {
|
|
1925
|
+
clearCountdown();
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
if (timerSetting === 0) {
|
|
1929
|
+
engine.onCapturePhoto({ aspect });
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
let remaining = timerSetting;
|
|
1933
|
+
setCountdown(remaining);
|
|
1934
|
+
countdownRef.current = setInterval(() => {
|
|
1935
|
+
remaining -= 1;
|
|
1936
|
+
if (remaining <= 0) {
|
|
1937
|
+
clearCountdown();
|
|
1938
|
+
engine.onCapturePhoto({ aspect });
|
|
1939
|
+
} else {
|
|
1940
|
+
setCountdown(remaining);
|
|
1941
|
+
}
|
|
1942
|
+
}, 1e3);
|
|
1943
|
+
}, [aspect, clearCountdown, countdown, engine, timerSetting]);
|
|
1944
|
+
const blocked = engine.blocked !== null;
|
|
1945
|
+
const coreRail = [
|
|
1946
|
+
...engine.onFlipCamera ? [
|
|
1947
|
+
{
|
|
1948
|
+
id: "flip",
|
|
1949
|
+
label: "Switch camera",
|
|
1950
|
+
icon: /* @__PURE__ */ jsx14(RefreshCw2, { className: "h-[22px] w-[22px]" }),
|
|
1951
|
+
primary: true,
|
|
1952
|
+
onPress: engine.onFlipCamera
|
|
1953
|
+
}
|
|
1954
|
+
] : [],
|
|
1955
|
+
...controls.torchSupported ? [
|
|
1956
|
+
{
|
|
1957
|
+
id: "flash",
|
|
1958
|
+
label: "Flash",
|
|
1959
|
+
icon: controls.torchOn ? /* @__PURE__ */ jsx14(Zap2, { className: "h-[22px] w-[22px]", fill: "currentColor" }) : /* @__PURE__ */ jsx14(ZapOff2, { className: "h-[22px] w-[22px]" }),
|
|
1960
|
+
active: controls.torchOn,
|
|
1961
|
+
primary: true,
|
|
1962
|
+
onPress: controls.toggleTorch
|
|
1963
|
+
}
|
|
1964
|
+
] : [],
|
|
1965
|
+
{
|
|
1966
|
+
id: "timer",
|
|
1967
|
+
label: "Timer",
|
|
1968
|
+
icon: /* @__PURE__ */ jsx14(Timer2, { className: "h-[22px] w-[22px]" }),
|
|
1969
|
+
active: timerSetting !== 0,
|
|
1970
|
+
valueLabel: timerSetting === 0 ? void 0 : `${timerSetting}s`,
|
|
1971
|
+
onPress: () => setTimerSetting((t) => t === 0 ? 3 : t === 3 ? 10 : 0)
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
id: "grid",
|
|
1975
|
+
label: "Grid",
|
|
1976
|
+
icon: /* @__PURE__ */ jsx14(Grid3x32, { className: "h-[22px] w-[22px]" }),
|
|
1977
|
+
active: gridOn,
|
|
1978
|
+
onPress: () => setGridOn((g) => !g)
|
|
1979
|
+
},
|
|
1980
|
+
{
|
|
1981
|
+
id: "aspect",
|
|
1982
|
+
label: "Aspect",
|
|
1983
|
+
icon: /* @__PURE__ */ jsx14(Proportions2, { className: "h-[22px] w-[22px]" }),
|
|
1984
|
+
active: aspect !== "full",
|
|
1985
|
+
valueLabel: aspect === "full" ? void 0 : aspect,
|
|
1986
|
+
onPress: () => setAspect(
|
|
1987
|
+
(a) => ASPECT_CYCLE2[(ASPECT_CYCLE2.indexOf(a) + 1) % ASPECT_CYCLE2.length] ?? "full"
|
|
1988
|
+
)
|
|
1989
|
+
},
|
|
1990
|
+
...controls.exposureSupported ? [
|
|
1991
|
+
{
|
|
1992
|
+
id: "exposure",
|
|
1993
|
+
label: "Exposure",
|
|
1994
|
+
icon: /* @__PURE__ */ jsx14(SunMedium2, { className: "h-[22px] w-[22px]" }),
|
|
1995
|
+
active: controls.exposure !== 0,
|
|
1996
|
+
valueLabel: controls.exposure === 0 ? void 0 : `${controls.exposure > 0 ? "+" : ""}${controls.exposure}`,
|
|
1997
|
+
onPress: () => controls.setExposure(controls.exposure === 0 ? 1 : 0)
|
|
1998
|
+
}
|
|
1999
|
+
] : []
|
|
2000
|
+
];
|
|
2001
|
+
const railActions = [...coreRail, ...slots.railActions ?? []];
|
|
2002
|
+
return /* @__PURE__ */ jsxs12("div", { className: "absolute inset-0 select-none overflow-hidden bg-black", children: [
|
|
2003
|
+
/* @__PURE__ */ jsx14("div", { className: "absolute inset-0", children: preview }),
|
|
2004
|
+
/* @__PURE__ */ jsx14(GridOverlay, { visible: gridOn && !blocked }),
|
|
2005
|
+
aspect !== "full" && !blocked && !engine.recording && /* @__PURE__ */ jsx14(
|
|
2006
|
+
"div",
|
|
2007
|
+
{
|
|
2008
|
+
"aria-hidden": true,
|
|
2009
|
+
className: "pointer-events-none absolute inset-0 z-10 flex items-center justify-center",
|
|
2010
|
+
children: /* @__PURE__ */ jsx14(
|
|
2011
|
+
"div",
|
|
2012
|
+
{
|
|
2013
|
+
className: "shadow-[0_0_0_9999px_rgba(0,0,0,0.45)]",
|
|
2014
|
+
style: {
|
|
2015
|
+
aspectRatio: aspect === "1:1" ? "1 / 1" : aspect === "4:3" ? "3 / 4" : "9 / 16",
|
|
2016
|
+
width: aspect === "16:9" ? "100%" : void 0,
|
|
2017
|
+
height: aspect === "16:9" ? void 0 : "70%",
|
|
2018
|
+
maxWidth: "100%",
|
|
2019
|
+
maxHeight: "100%"
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
)
|
|
2023
|
+
}
|
|
2024
|
+
),
|
|
2025
|
+
/* @__PURE__ */ jsx14(CountdownOverlay, { seconds: countdown }),
|
|
2026
|
+
!controlsHidden && /* @__PURE__ */ jsxs12(
|
|
2027
|
+
"div",
|
|
2028
|
+
{
|
|
2029
|
+
className: "pointer-events-none absolute inset-x-0 top-0 z-20 px-2",
|
|
2030
|
+
style: safeTop,
|
|
2031
|
+
children: [
|
|
2032
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex min-h-[44px] items-center gap-2 py-1.5", children: [
|
|
2033
|
+
onClose ? /* @__PURE__ */ jsx14(
|
|
2034
|
+
"button",
|
|
2035
|
+
{
|
|
2036
|
+
type: "button",
|
|
2037
|
+
onClick: onClose,
|
|
2038
|
+
"aria-label": "Close camera",
|
|
2039
|
+
className: "pointer-events-auto flex h-10 w-10 shrink-0 touch-manipulation items-center justify-center rounded-full text-white drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)] transition-colors hover:bg-white/10",
|
|
2040
|
+
children: /* @__PURE__ */ jsx14(X5, { className: "h-6 w-6" })
|
|
2041
|
+
}
|
|
2042
|
+
) : /* @__PURE__ */ jsx14("span", { className: "h-10 w-10 shrink-0" }),
|
|
2043
|
+
/* @__PURE__ */ jsx14("div", { className: "pointer-events-auto flex min-w-0 flex-1 justify-center", children: slots.topEntry }),
|
|
2044
|
+
/* @__PURE__ */ jsx14("span", { className: "h-10 w-10 shrink-0" })
|
|
2045
|
+
] }),
|
|
2046
|
+
slots.topCenter ? /* @__PURE__ */ jsx14("div", { className: "pointer-events-none pb-1 text-center", children: slots.topCenter }) : null
|
|
2047
|
+
]
|
|
2048
|
+
}
|
|
2049
|
+
),
|
|
2050
|
+
slots.statusChips ? /* @__PURE__ */ jsx14(
|
|
2051
|
+
"div",
|
|
2052
|
+
{
|
|
2053
|
+
className: "pointer-events-none absolute inset-x-0 z-20 flex flex-col items-center gap-1.5 px-3",
|
|
2054
|
+
style: {
|
|
2055
|
+
top: `calc(env(safe-area-inset-top, 0px) + ${controlsHidden ? 12 : 64}px)`
|
|
2056
|
+
},
|
|
2057
|
+
children: slots.statusChips
|
|
2058
|
+
}
|
|
2059
|
+
) : null,
|
|
2060
|
+
!controlsHidden && !blocked && /* @__PURE__ */ jsx14(
|
|
2061
|
+
"div",
|
|
2062
|
+
{
|
|
2063
|
+
className: "pointer-events-none absolute right-1 z-20 flex justify-end",
|
|
2064
|
+
style: { top: `calc(env(safe-area-inset-top, 0px) + 64px)` },
|
|
2065
|
+
children: /* @__PURE__ */ jsx14(CaptureRail, { actions: railActions })
|
|
2066
|
+
}
|
|
2067
|
+
),
|
|
2068
|
+
!controlsHidden && /* @__PURE__ */ jsxs12("div", { className: "absolute inset-x-0 bottom-0 z-20", style: safeBottom, children: [
|
|
2069
|
+
media && media.items.length > 0 ? /* @__PURE__ */ jsx14("div", { className: "px-2 pb-1.5", children: /* @__PURE__ */ jsx14(
|
|
2070
|
+
CaptureFilmstrip,
|
|
2071
|
+
{
|
|
2072
|
+
items: media.items,
|
|
2073
|
+
onOpen: (item) => setViewerKey(item.key)
|
|
2074
|
+
}
|
|
2075
|
+
) }) : null,
|
|
2076
|
+
slots.aboveShutter ? /* @__PURE__ */ jsx14("div", { className: "px-2 pb-1.5", children: slots.aboveShutter }) : null,
|
|
2077
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-between px-3 pb-2", children: [
|
|
2078
|
+
/* @__PURE__ */ jsx14("div", { className: "flex w-[86px] justify-start", children: /* @__PURE__ */ jsx14(
|
|
2079
|
+
"button",
|
|
2080
|
+
{
|
|
2081
|
+
type: "button",
|
|
2082
|
+
onClick: cloud.onOpenLibrary,
|
|
2083
|
+
"aria-label": "Your media \u2014 library and upload",
|
|
2084
|
+
className: "relative h-12 w-12 touch-manipulation overflow-hidden rounded-xl bg-black/40 ring-1 ring-white/30 backdrop-blur-md transition-transform active:scale-95",
|
|
2085
|
+
children: cloud.recentsThumb ?? /* @__PURE__ */ jsx14("span", { className: "flex h-full w-full items-center justify-center", children: /* @__PURE__ */ jsx14(Images, { className: "h-5 w-5 text-white/80" }) })
|
|
2086
|
+
}
|
|
2087
|
+
) }),
|
|
2088
|
+
/* @__PURE__ */ jsx14(
|
|
2089
|
+
HoldShutter,
|
|
2090
|
+
{
|
|
2091
|
+
recording: engine.recording,
|
|
2092
|
+
elapsedSeconds: engine.recordElapsedSeconds,
|
|
2093
|
+
...maxRecordSeconds !== void 0 ? { maxSeconds: maxRecordSeconds } : {},
|
|
2094
|
+
disabled: shutterDisabled || blocked,
|
|
2095
|
+
...onRecordIntent ? { onPressStart: onRecordIntent } : {},
|
|
2096
|
+
onPhoto,
|
|
2097
|
+
onStartRecording: engine.onStartRecording,
|
|
2098
|
+
onStopRecording: engine.onStopRecording
|
|
2099
|
+
}
|
|
2100
|
+
),
|
|
2101
|
+
/* @__PURE__ */ jsx14("div", { className: "flex w-[86px] justify-end", children: slots.shutterTrailing })
|
|
2102
|
+
] }),
|
|
2103
|
+
!engine.recording && !blocked ? /* @__PURE__ */ jsx14("p", { className: "pb-1.5 text-center text-[11px] text-white/60 drop-shadow-[0_1px_2px_rgba(0,0,0,0.8)]", children: "Tap for a photo \xB7 hold to record" }) : null
|
|
2104
|
+
] }),
|
|
2105
|
+
blocked && blockedSheet && !blockedDismissed && /* @__PURE__ */ jsx14(
|
|
2106
|
+
CaptureSheet,
|
|
2107
|
+
{
|
|
2108
|
+
open: true,
|
|
2109
|
+
onClose: () => setBlockedDismissed(true),
|
|
2110
|
+
body: blockedSheet.body,
|
|
2111
|
+
title: "Camera unavailable",
|
|
2112
|
+
actions: blockedSheet.actions
|
|
2113
|
+
}
|
|
2114
|
+
),
|
|
2115
|
+
media && viewerKey !== null && /* @__PURE__ */ jsx14(
|
|
2116
|
+
MediaViewer,
|
|
2117
|
+
{
|
|
2118
|
+
items: media.items,
|
|
2119
|
+
initialKey: viewerKey,
|
|
2120
|
+
keysDisabled: editTarget !== null,
|
|
2121
|
+
onClose: () => setViewerKey(null),
|
|
2122
|
+
onDelete: media.onDelete ? (item) => {
|
|
2123
|
+
media.onDelete?.(item.key);
|
|
2124
|
+
invalidateMedia(item.key);
|
|
2125
|
+
} : void 0,
|
|
2126
|
+
onEdit: media.onReplacePhoto ? (item) => {
|
|
2127
|
+
const url = getMediaUrl(item);
|
|
2128
|
+
if (url) setEditTarget({ key: item.key, url });
|
|
2129
|
+
} : void 0
|
|
2130
|
+
}
|
|
2131
|
+
),
|
|
2132
|
+
media?.onReplacePhoto && /* @__PURE__ */ jsx14(
|
|
2133
|
+
ImageEditSheet,
|
|
2134
|
+
{
|
|
2135
|
+
open: editTarget !== null,
|
|
2136
|
+
src: editTarget?.url ?? null,
|
|
2137
|
+
onClose: () => setEditTarget(null),
|
|
2138
|
+
onSave: (blob) => {
|
|
2139
|
+
const target = editTarget;
|
|
2140
|
+
setEditTarget(null);
|
|
2141
|
+
setViewerKey(null);
|
|
2142
|
+
if (target) {
|
|
2143
|
+
media.onReplacePhoto?.(target.key, blob);
|
|
2144
|
+
invalidateMedia(target.key);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
),
|
|
2149
|
+
slots.overlays
|
|
2150
|
+
] });
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// src/components/CaptureExpandingField.tsx
|
|
2154
|
+
import { useCallback as useCallback7, useEffect as useEffect8, useRef as useRef6, useState as useState9 } from "react";
|
|
2155
|
+
import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2156
|
+
function CaptureExpandingField({
|
|
2157
|
+
label,
|
|
2158
|
+
placeholder,
|
|
2159
|
+
icon,
|
|
2160
|
+
onCommit,
|
|
2161
|
+
defaultExpanded = false,
|
|
2162
|
+
disabled = false,
|
|
2163
|
+
currentValue = null,
|
|
2164
|
+
className
|
|
2165
|
+
}) {
|
|
2166
|
+
const [expanded, setExpanded] = useState9(defaultExpanded);
|
|
2167
|
+
const [draft, setDraft] = useState9("");
|
|
2168
|
+
const inputRef = useRef6(null);
|
|
2169
|
+
const draftRef = useRef6(draft);
|
|
2170
|
+
draftRef.current = draft;
|
|
2171
|
+
const onCommitRef = useRef6(onCommit);
|
|
2172
|
+
onCommitRef.current = onCommit;
|
|
2173
|
+
useEffect8(() => {
|
|
2174
|
+
return () => {
|
|
2175
|
+
const trimmed = draftRef.current.trim();
|
|
2176
|
+
if (trimmed) onCommitRef.current(trimmed);
|
|
2177
|
+
};
|
|
2178
|
+
}, []);
|
|
2179
|
+
const commit = useCallback7(() => {
|
|
2180
|
+
const trimmed = draftRef.current.trim();
|
|
2181
|
+
if (trimmed) onCommitRef.current(trimmed);
|
|
2182
|
+
setDraft("");
|
|
2183
|
+
}, []);
|
|
2184
|
+
const open = useCallback7(() => {
|
|
2185
|
+
setExpanded(true);
|
|
2186
|
+
requestAnimationFrame(() => inputRef.current?.focus());
|
|
2187
|
+
}, []);
|
|
2188
|
+
const close = useCallback7(() => {
|
|
2189
|
+
commit();
|
|
2190
|
+
setExpanded(false);
|
|
2191
|
+
}, [commit]);
|
|
2192
|
+
if (!expanded) {
|
|
2193
|
+
return /* @__PURE__ */ jsxs13(
|
|
2194
|
+
"button",
|
|
2195
|
+
{
|
|
2196
|
+
type: "button",
|
|
2197
|
+
onClick: open,
|
|
2198
|
+
disabled,
|
|
2199
|
+
"aria-label": label,
|
|
2200
|
+
"aria-expanded": false,
|
|
2201
|
+
className: cn(
|
|
2202
|
+
"flex h-9 max-w-[62vw] items-center gap-1.5 rounded-full px-3.5",
|
|
2203
|
+
"bg-black/45 text-[13px] font-semibold text-white backdrop-blur-md",
|
|
2204
|
+
"transition-colors hover:bg-black/60 active:bg-black/70",
|
|
2205
|
+
disabled && "opacity-40",
|
|
2206
|
+
className
|
|
2207
|
+
),
|
|
2208
|
+
children: [
|
|
2209
|
+
icon ? /* @__PURE__ */ jsx15("span", { className: "shrink-0", children: icon }) : null,
|
|
2210
|
+
/* @__PURE__ */ jsx15("span", { className: "truncate", children: currentValue?.trim() || label })
|
|
2211
|
+
]
|
|
2212
|
+
}
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
return /* @__PURE__ */ jsxs13(
|
|
2216
|
+
"div",
|
|
2217
|
+
{
|
|
2218
|
+
className: cn(
|
|
2219
|
+
"flex h-9 w-[min(78vw,320px)] items-center gap-1.5 rounded-full px-3",
|
|
2220
|
+
"bg-black/60 backdrop-blur-md",
|
|
2221
|
+
className
|
|
2222
|
+
),
|
|
2223
|
+
children: [
|
|
2224
|
+
icon ? /* @__PURE__ */ jsx15("span", { className: "shrink-0 text-white/70", children: icon }) : null,
|
|
2225
|
+
/* @__PURE__ */ jsx15(
|
|
2226
|
+
"input",
|
|
2227
|
+
{
|
|
2228
|
+
ref: inputRef,
|
|
2229
|
+
value: draft,
|
|
2230
|
+
onChange: (e) => setDraft(e.target.value),
|
|
2231
|
+
onBlur: close,
|
|
2232
|
+
onKeyDown: (e) => {
|
|
2233
|
+
if (e.key === "Enter") {
|
|
2234
|
+
commit();
|
|
2235
|
+
e.target.blur();
|
|
2236
|
+
}
|
|
2237
|
+
if (e.key === "Escape") {
|
|
2238
|
+
setDraft("");
|
|
2239
|
+
setExpanded(false);
|
|
2240
|
+
}
|
|
2241
|
+
},
|
|
2242
|
+
placeholder: placeholder ?? label,
|
|
2243
|
+
"aria-label": label,
|
|
2244
|
+
enterKeyHint: "done",
|
|
2245
|
+
autoCapitalize: "characters",
|
|
2246
|
+
autoCorrect: "off",
|
|
2247
|
+
spellCheck: false,
|
|
2248
|
+
className: cn(
|
|
2249
|
+
"min-w-0 flex-1 bg-transparent text-base text-white",
|
|
2250
|
+
"placeholder:text-white/40 focus:outline-none"
|
|
2251
|
+
)
|
|
2252
|
+
}
|
|
2253
|
+
),
|
|
2254
|
+
/* @__PURE__ */ jsx15(
|
|
2255
|
+
"button",
|
|
2256
|
+
{
|
|
2257
|
+
type: "button",
|
|
2258
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
2259
|
+
onClick: () => {
|
|
2260
|
+
setDraft("");
|
|
2261
|
+
setExpanded(false);
|
|
2262
|
+
},
|
|
2263
|
+
"aria-label": `Close ${label}`,
|
|
2264
|
+
className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-white/70 hover:bg-white/10 hover:text-white",
|
|
2265
|
+
children: /* @__PURE__ */ jsx15(
|
|
2266
|
+
"svg",
|
|
2267
|
+
{
|
|
2268
|
+
viewBox: "0 0 24 24",
|
|
2269
|
+
fill: "none",
|
|
2270
|
+
stroke: "currentColor",
|
|
2271
|
+
strokeWidth: "2.5",
|
|
2272
|
+
strokeLinecap: "round",
|
|
2273
|
+
className: "h-3.5 w-3.5",
|
|
2274
|
+
"aria-hidden": "true",
|
|
2275
|
+
children: /* @__PURE__ */ jsx15("path", { d: "M18 6 6 18M6 6l12 12" })
|
|
2276
|
+
}
|
|
2277
|
+
)
|
|
2278
|
+
}
|
|
2279
|
+
)
|
|
2280
|
+
]
|
|
2281
|
+
}
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
|
|
1620
2285
|
// src/components/CameraFeed.tsx
|
|
1621
|
-
import { useEffect as
|
|
1622
|
-
import { jsx as
|
|
2286
|
+
import { useEffect as useEffect9 } from "react";
|
|
2287
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
1623
2288
|
function CameraFeed({
|
|
1624
2289
|
engine,
|
|
1625
2290
|
mirror = false
|
|
1626
2291
|
}) {
|
|
1627
2292
|
const { stream, videoRef } = engine;
|
|
1628
|
-
|
|
2293
|
+
useEffect9(() => {
|
|
1629
2294
|
const video = videoRef.current;
|
|
1630
2295
|
if (!video) return;
|
|
1631
2296
|
if (video.srcObject !== stream) video.srcObject = stream;
|
|
@@ -1633,7 +2298,7 @@ function CameraFeed({
|
|
|
1633
2298
|
video.srcObject = null;
|
|
1634
2299
|
};
|
|
1635
2300
|
}, [stream, videoRef]);
|
|
1636
|
-
return /* @__PURE__ */
|
|
2301
|
+
return /* @__PURE__ */ jsx16(
|
|
1637
2302
|
"video",
|
|
1638
2303
|
{
|
|
1639
2304
|
ref: videoRef,
|
|
@@ -1652,11 +2317,11 @@ function CameraFeed({
|
|
|
1652
2317
|
|
|
1653
2318
|
// src/engine/useDefaultEngine.ts
|
|
1654
2319
|
import {
|
|
1655
|
-
useCallback as
|
|
1656
|
-
useEffect as
|
|
2320
|
+
useCallback as useCallback8,
|
|
2321
|
+
useEffect as useEffect10,
|
|
1657
2322
|
useMemo as useMemo3,
|
|
1658
|
-
useRef as
|
|
1659
|
-
useState as
|
|
2323
|
+
useRef as useRef7,
|
|
2324
|
+
useState as useState10
|
|
1660
2325
|
} from "react";
|
|
1661
2326
|
var RECORDING_MIME_LADDER = [
|
|
1662
2327
|
'video/mp4;codecs="avc1.42E01E,mp4a.40.2"',
|
|
@@ -1687,18 +2352,18 @@ function useDefaultCaptureEngine(options) {
|
|
|
1687
2352
|
facingMode: initialFacing = "environment",
|
|
1688
2353
|
photoQuality = 0.92
|
|
1689
2354
|
} = options;
|
|
1690
|
-
const [stream, setStream] =
|
|
1691
|
-
const [blocked, setBlocked] =
|
|
1692
|
-
const [facing, setFacing] =
|
|
1693
|
-
const [multipleCameras, setMultipleCameras] =
|
|
1694
|
-
const [recording, setRecording] =
|
|
1695
|
-
const [recordElapsedSeconds, setRecordElapsedSeconds] =
|
|
1696
|
-
const videoRef =
|
|
1697
|
-
const streamRef =
|
|
1698
|
-
const recorderRef =
|
|
1699
|
-
const callbacksRef =
|
|
2355
|
+
const [stream, setStream] = useState10(null);
|
|
2356
|
+
const [blocked, setBlocked] = useState10(null);
|
|
2357
|
+
const [facing, setFacing] = useState10(initialFacing);
|
|
2358
|
+
const [multipleCameras, setMultipleCameras] = useState10(false);
|
|
2359
|
+
const [recording, setRecording] = useState10(false);
|
|
2360
|
+
const [recordElapsedSeconds, setRecordElapsedSeconds] = useState10(0);
|
|
2361
|
+
const videoRef = useRef7(null);
|
|
2362
|
+
const streamRef = useRef7(null);
|
|
2363
|
+
const recorderRef = useRef7(null);
|
|
2364
|
+
const callbacksRef = useRef7({ onPhoto, onVideo, onFiles });
|
|
1700
2365
|
callbacksRef.current = { onPhoto, onVideo, onFiles };
|
|
1701
|
-
|
|
2366
|
+
useEffect10(() => {
|
|
1702
2367
|
let cancelled = false;
|
|
1703
2368
|
let acquired = null;
|
|
1704
2369
|
if (typeof navigator === "undefined" || !navigator.mediaDevices) {
|
|
@@ -1741,7 +2406,7 @@ function useDefaultCaptureEngine(options) {
|
|
|
1741
2406
|
setStream(null);
|
|
1742
2407
|
};
|
|
1743
2408
|
}, [facing]);
|
|
1744
|
-
|
|
2409
|
+
useEffect10(() => {
|
|
1745
2410
|
return () => {
|
|
1746
2411
|
const r = recorderRef.current;
|
|
1747
2412
|
if (r) {
|
|
@@ -1751,7 +2416,7 @@ function useDefaultCaptureEngine(options) {
|
|
|
1751
2416
|
}
|
|
1752
2417
|
};
|
|
1753
2418
|
}, []);
|
|
1754
|
-
const onCapturePhoto =
|
|
2419
|
+
const onCapturePhoto = useCallback8(
|
|
1755
2420
|
(opts) => {
|
|
1756
2421
|
const video = videoRef.current;
|
|
1757
2422
|
if (!video || video.videoWidth === 0) return;
|
|
@@ -1791,7 +2456,7 @@ function useDefaultCaptureEngine(options) {
|
|
|
1791
2456
|
},
|
|
1792
2457
|
[photoQuality]
|
|
1793
2458
|
);
|
|
1794
|
-
const onStartRecording =
|
|
2459
|
+
const onStartRecording = useCallback8(() => {
|
|
1795
2460
|
const base = streamRef.current;
|
|
1796
2461
|
if (!base || recorderRef.current) return;
|
|
1797
2462
|
void (async () => {
|
|
@@ -1858,11 +2523,11 @@ function useDefaultCaptureEngine(options) {
|
|
|
1858
2523
|
setRecording(true);
|
|
1859
2524
|
})();
|
|
1860
2525
|
}, [withAudio]);
|
|
1861
|
-
const onStopRecording =
|
|
2526
|
+
const onStopRecording = useCallback8(() => {
|
|
1862
2527
|
const r = recorderRef.current;
|
|
1863
2528
|
if (r && r.recorder.state !== "inactive") r.recorder.stop();
|
|
1864
2529
|
}, []);
|
|
1865
|
-
const onUpload =
|
|
2530
|
+
const onUpload = useCallback8(() => {
|
|
1866
2531
|
const input = document.createElement("input");
|
|
1867
2532
|
input.type = "file";
|
|
1868
2533
|
input.accept = "image/*,video/*";
|
|
@@ -1873,7 +2538,7 @@ function useDefaultCaptureEngine(options) {
|
|
|
1873
2538
|
};
|
|
1874
2539
|
input.click();
|
|
1875
2540
|
}, []);
|
|
1876
|
-
const onFlipCamera =
|
|
2541
|
+
const onFlipCamera = useCallback8(() => {
|
|
1877
2542
|
setFacing((f) => f === "environment" ? "user" : "environment");
|
|
1878
2543
|
}, []);
|
|
1879
2544
|
return useMemo3(
|
|
@@ -1905,11 +2570,15 @@ function useDefaultCaptureEngine(options) {
|
|
|
1905
2570
|
}
|
|
1906
2571
|
export {
|
|
1907
2572
|
CameraCapture,
|
|
2573
|
+
CameraCaptureV3,
|
|
1908
2574
|
CameraFeed,
|
|
2575
|
+
CaptureExpandingField,
|
|
1909
2576
|
CaptureFilmstrip,
|
|
2577
|
+
CaptureRail,
|
|
1910
2578
|
CaptureSheet,
|
|
1911
2579
|
CountdownOverlay,
|
|
1912
2580
|
GridOverlay,
|
|
2581
|
+
HoldShutter,
|
|
1913
2582
|
ImageEditSheet,
|
|
1914
2583
|
MediaViewer,
|
|
1915
2584
|
ModeSelector,
|