@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.
@@ -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
- * The height is automatically calculated to preserve the aspect ratio.
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
  */
@@ -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 heightRef = React.useRef(0);
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
- if (!streamingRef.current) {
55
- const { videoHeight, videoWidth } = video;
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, width]);
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
- canvas.width = width;
80
- canvas.height = heightRef.current;
81
- context.drawImage(video, 0, 0, width, heightRef.current);
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 isSupported = typeof screen !== "undefined" && screen.orientation !== void 0;
20
- const [type, setType] = React.useState(isSupported ? screen.orientation.type : null);
21
- const [angle, setAngle] = React.useState(isSupported ? screen.orientation.angle : null);
22
- const lock = React.useCallback(async (orientation) => {
23
- if (!isSupported) return;
24
- await screen.orientation.lock(orientation);
25
- }, [isSupported]);
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
- screen.orientation.unlock();
29
- }, [isSupported]);
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 (!isSupported) return;
40
+ if (!orientation) return;
32
41
  const handleChange = () => {
33
- setType(screen.orientation.type);
34
- setAngle(screen.orientation.angle);
42
+ setType(orientation.type);
43
+ setAngle(orientation.angle);
35
44
  };
36
- screen.orientation.addEventListener("change", handleChange);
45
+ orientation.addEventListener("change", handleChange);
37
46
  return () => {
38
- screen.orientation.removeEventListener("change", handleChange);
47
+ orientation.removeEventListener("change", handleChange);
39
48
  };
40
- }, [isSupported]);
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.5",
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",