@vibehooks/react 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/useCameraCapture.d.ts +5 -3
- package/dist/useCameraCapture.js +46 -19
- package/dist/useScreenOrientation.js +25 -16
- package/package.json +1 -1
|
@@ -22,9 +22,7 @@ interface UseCameraCaptureOptions {
|
|
|
22
22
|
onCapture?: (dataUrl: string, blob: Blob) => void;
|
|
23
23
|
/**
|
|
24
24
|
* Desired output width for the captured image.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* @default 320
|
|
25
|
+
* If ommited, the image is captured at native camera resolution.
|
|
28
26
|
*/
|
|
29
27
|
width?: number;
|
|
30
28
|
}
|
|
@@ -50,6 +48,10 @@ interface UseCameraCaptureReturn {
|
|
|
50
48
|
* Stops all active media tracks and releases the camera.
|
|
51
49
|
*/
|
|
52
50
|
stop: () => void;
|
|
51
|
+
/**
|
|
52
|
+
* Toggles between front and back camera if available.
|
|
53
|
+
*/
|
|
54
|
+
toggleCamera: () => void;
|
|
53
55
|
/**
|
|
54
56
|
* Indicates whether camera permissions has been granted.
|
|
55
57
|
*/
|
package/dist/useCameraCapture.js
CHANGED
|
@@ -33,17 +33,37 @@ function useCameraCapture(options = {}) {
|
|
|
33
33
|
const imageRef = React.useRef(null);
|
|
34
34
|
const streamRef = React.useRef(null);
|
|
35
35
|
const streamingRef = React.useRef(false);
|
|
36
|
-
const
|
|
36
|
+
const devicesRef = React.useRef([]);
|
|
37
|
+
const currentDeviceIndexRef = React.useRef(0);
|
|
37
38
|
const isBrowser = typeof window !== "undefined";
|
|
38
39
|
const permissionStoreRef = React.useRef(createExternalStore(() => !!streamRef.current));
|
|
39
40
|
const streamingStoreRef = React.useRef(createExternalStore(() => streamingRef.current));
|
|
41
|
+
const isTouchDevice = () => {
|
|
42
|
+
if (!isBrowser) return false;
|
|
43
|
+
return "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
|
44
|
+
};
|
|
45
|
+
const loadVideDevices = React.useCallback(async () => {
|
|
46
|
+
if (!navigator.mediaDevices?.enumerateDevices) return;
|
|
47
|
+
const videoInputs = (await navigator.mediaDevices.enumerateDevices()).filter((device) => device.kind === "videoinput");
|
|
48
|
+
devicesRef.current = videoInputs;
|
|
49
|
+
if (videoInputs.length === 0) return;
|
|
50
|
+
const touch = isTouchDevice();
|
|
51
|
+
const backCameraIndex = videoInputs.findIndex((device) => /back|rear|environment/i.test(device.label));
|
|
52
|
+
if (touch && backCameraIndex !== -1) currentDeviceIndexRef.current = backCameraIndex;
|
|
53
|
+
else currentDeviceIndexRef.current = 0;
|
|
54
|
+
}, [isBrowser]);
|
|
40
55
|
const requestPermission = React.useCallback(async () => {
|
|
41
56
|
if (!isBrowser) return false;
|
|
42
57
|
if (!navigator.mediaDevices?.getUserMedia) return false;
|
|
43
58
|
try {
|
|
59
|
+
if (devicesRef.current.length === 0) {
|
|
60
|
+
(await navigator.mediaDevices.getUserMedia({ video: true })).getTracks().forEach((track) => track.stop());
|
|
61
|
+
await loadVideDevices();
|
|
62
|
+
}
|
|
63
|
+
const device = devicesRef.current[currentDeviceIndexRef.current];
|
|
44
64
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
45
65
|
audio: false,
|
|
46
|
-
video: true
|
|
66
|
+
video: device ? { deviceId: { exact: device.deviceId } } : true
|
|
47
67
|
});
|
|
48
68
|
streamRef.current = stream;
|
|
49
69
|
permissionStoreRef.current.notify();
|
|
@@ -51,24 +71,13 @@ function useCameraCapture(options = {}) {
|
|
|
51
71
|
if (!video) return false;
|
|
52
72
|
video.srcObject = stream;
|
|
53
73
|
await video.play();
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
heightRef.current = videoHeight / (videoWidth / width);
|
|
57
|
-
video.width = width;
|
|
58
|
-
video.height = heightRef.current;
|
|
59
|
-
const canvas = canvasRef.current;
|
|
60
|
-
if (canvas) {
|
|
61
|
-
canvas.width = width;
|
|
62
|
-
canvas.height = heightRef.current;
|
|
63
|
-
}
|
|
64
|
-
streamingRef.current = true;
|
|
65
|
-
streamingStoreRef.current.notify();
|
|
66
|
-
}
|
|
74
|
+
streamingRef.current = true;
|
|
75
|
+
streamingStoreRef.current.notify();
|
|
67
76
|
return true;
|
|
68
77
|
} catch {
|
|
69
78
|
return false;
|
|
70
79
|
}
|
|
71
|
-
}, [isBrowser,
|
|
80
|
+
}, [isBrowser, loadVideDevices]);
|
|
72
81
|
const capture = React.useCallback(() => {
|
|
73
82
|
const video = videoRef.current;
|
|
74
83
|
const canvas = canvasRef.current;
|
|
@@ -76,9 +85,19 @@ function useCameraCapture(options = {}) {
|
|
|
76
85
|
if (!video || !canvas || !streamingRef.current) return null;
|
|
77
86
|
const context = canvas.getContext("2d");
|
|
78
87
|
if (!context) return null;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
const videoWidth = video.videoWidth;
|
|
89
|
+
const videoHeight = video.videoHeight;
|
|
90
|
+
if (!videoWidth || !videoHeight) return null;
|
|
91
|
+
if (width) {
|
|
92
|
+
const scaledHeight = videoHeight / (videoWidth / width);
|
|
93
|
+
canvas.width = width;
|
|
94
|
+
canvas.height = scaledHeight;
|
|
95
|
+
context.drawImage(video, 0, 0, width, scaledHeight);
|
|
96
|
+
} else {
|
|
97
|
+
canvas.width = videoWidth;
|
|
98
|
+
canvas.height = videoHeight;
|
|
99
|
+
context.drawImage(video, 0, 0, videoWidth, videoHeight);
|
|
100
|
+
}
|
|
82
101
|
const dataUrl = canvas.toDataURL(output.type, output.quality);
|
|
83
102
|
image?.setAttribute("src", dataUrl);
|
|
84
103
|
if (onCapture) canvas.toBlob((blob) => {
|
|
@@ -93,11 +112,18 @@ function useCameraCapture(options = {}) {
|
|
|
93
112
|
]);
|
|
94
113
|
const stop = React.useCallback(() => {
|
|
95
114
|
streamRef.current?.getTracks().forEach((track) => track.stop());
|
|
115
|
+
if (videoRef.current) videoRef.current.srcObject = null;
|
|
96
116
|
streamRef.current = null;
|
|
97
117
|
streamingRef.current = false;
|
|
98
118
|
permissionStoreRef.current.notify();
|
|
99
119
|
streamingStoreRef.current.notify();
|
|
100
120
|
}, []);
|
|
121
|
+
const toggleCamera = React.useCallback(async () => {
|
|
122
|
+
if (devicesRef.current.length <= 1) return false;
|
|
123
|
+
stop();
|
|
124
|
+
currentDeviceIndexRef.current = (currentDeviceIndexRef.current + 1) / devicesRef.current.length;
|
|
125
|
+
return await requestPermission();
|
|
126
|
+
}, [requestPermission, stop]);
|
|
101
127
|
const usePermission = () => React.useSyncExternalStore(permissionStoreRef.current.subscribe, permissionStoreRef.current.getSnapshot, () => false);
|
|
102
128
|
const useStreaming = () => React.useSyncExternalStore(streamingStoreRef.current.subscribe, streamingStoreRef.current.getSnapshot, () => false);
|
|
103
129
|
return {
|
|
@@ -106,6 +132,7 @@ function useCameraCapture(options = {}) {
|
|
|
106
132
|
imageRef,
|
|
107
133
|
requestPermission,
|
|
108
134
|
stop,
|
|
135
|
+
toggleCamera,
|
|
109
136
|
usePermission,
|
|
110
137
|
useStreaming,
|
|
111
138
|
videoRef
|
|
@@ -16,28 +16,37 @@ import * as React from "react";
|
|
|
16
16
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/Screen_Orientation_API
|
|
17
17
|
*/
|
|
18
18
|
function useScreenOrientation() {
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const [
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
const orientation = typeof window !== "undefined" && "orientation" in screen ? screen.orientation : null;
|
|
20
|
+
const isSupported = !!orientation && typeof orientation.lock === "function" && typeof orientation.unlock === "function";
|
|
21
|
+
const [type, setType] = React.useState(orientation?.type ?? null);
|
|
22
|
+
const [angle, setAngle] = React.useState(orientation?.angle ?? null);
|
|
23
|
+
const lock = React.useCallback(async (orientationLock) => {
|
|
24
|
+
if (!isSupported || !orientation) return;
|
|
25
|
+
try {
|
|
26
|
+
await orientation.lock(orientationLock);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.warn("Orientation lock failed:", error);
|
|
29
|
+
}
|
|
30
|
+
}, [isSupported, orientation]);
|
|
26
31
|
const unlock = React.useCallback(() => {
|
|
27
|
-
if (!isSupported) return;
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
if (!isSupported || !orientation) return;
|
|
33
|
+
try {
|
|
34
|
+
orientation.unlock();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.warn("Orientation unlock failed:", error);
|
|
37
|
+
}
|
|
38
|
+
}, [isSupported, orientation]);
|
|
30
39
|
React.useEffect(() => {
|
|
31
|
-
if (!
|
|
40
|
+
if (!orientation) return;
|
|
32
41
|
const handleChange = () => {
|
|
33
|
-
setType(
|
|
34
|
-
setAngle(
|
|
42
|
+
setType(orientation.type);
|
|
43
|
+
setAngle(orientation.angle);
|
|
35
44
|
};
|
|
36
|
-
|
|
45
|
+
orientation.addEventListener("change", handleChange);
|
|
37
46
|
return () => {
|
|
38
|
-
|
|
47
|
+
orientation.removeEventListener("change", handleChange);
|
|
39
48
|
};
|
|
40
|
-
}, [
|
|
49
|
+
}, [orientation]);
|
|
41
50
|
return {
|
|
42
51
|
angle,
|
|
43
52
|
isSupported,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibehooks/react",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.7",
|
|
5
5
|
"description": "Modern React and Next.js hooks, unopinionated and focused on developer experience.",
|
|
6
6
|
"author": "Sebastian Marat Urdanegui Bisalaya <sebasurdanegui@gmail.com>",
|
|
7
7
|
"license": "MIT",
|