From 3716f41308f836c20b4d7649148f869f267d45e5 Mon Sep 17 00:00:00 2001 From: Dominic H Date: Fri, 19 Jul 2024 21:28:24 +0200 Subject: [PATCH 1/7] WIP: Torch/Flashlight support --- src/Components/QrScanner/QrScannerPlugin.tsx | 45 +++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/Components/QrScanner/QrScannerPlugin.tsx b/src/Components/QrScanner/QrScannerPlugin.tsx index d676179..279eef2 100644 --- a/src/Components/QrScanner/QrScannerPlugin.tsx +++ b/src/Components/QrScanner/QrScannerPlugin.tsx @@ -1,6 +1,6 @@ // file = QrScannerPlugin.jsx -import {MutableRefObject, useEffect, useRef} from 'react'; -import {ArrowUpTrayIcon} from '@heroicons/react/24/solid'; +import {MutableRefObject, useEffect, useRef, useState} from 'react'; +import {ArrowUpTrayIcon, BoltIcon, BoltSlashIcon} from '@heroicons/react/24/solid'; import {Html5Qrcode, Html5QrcodeScannerState, Html5QrcodeSupportedFormats} from 'html5-qrcode'; import {checkCameraPermissions} from '../../utils/media'; import classes from './QrScanner.module.css'; @@ -51,6 +51,8 @@ export default function QrScannerPlugin({ }: QrProps) { const aspectRatio = calcAspectRatio(); const html5CustomScanner: MutableRefObject = useRef(null); + const torchStateRef = useRef(false); + const [, setRender] = useState(false); // TODO: Find a better way to force a re-render without using the state (this causes the camera to reload) useEffect(() => { const showQRCode = async () => { @@ -103,11 +105,27 @@ export default function QrScannerPlugin({ onPermRefused, ]); + const toggleTorch = async () => { + try { + const track = html5CustomScanner.current?.getRunningTrackCameraCapabilities(); + if (track && track.torchFeature().isSupported()) { + torchStateRef.current = !torchStateRef.current; + await track.torchFeature().apply(torchStateRef.current); + setRender(prev => !prev); // TODO: Hacky + } + } catch (error) { + console.warn('Error toggling torch:', error); + } + }; + return ( -
- -
-
+ <> +
+ +
+
+ + ); } @@ -136,3 +154,18 @@ export function FileUploadScanner({
); } + +export function ToggleTorchButton({onClick, toggled}: {onClick: () => void; toggled: boolean}) { + // TODO: Hide away the button when the torch is not supported + return ( +
+
+ {toggled ? ( + + ) : ( + + )} +
+
+ ); +} From ed2bb17e7fd53dde2ba5de364b6a051e2e4bd815 Mon Sep 17 00:00:00 2001 From: Dominic H Date: Thu, 25 Jul 2024 12:30:42 +0100 Subject: [PATCH 2/7] Refactor torch into seperate component Fixes issues with state updates breaking the camera --- src/Components/QrScanner/QrScannerPlugin.tsx | 37 ++-------------- src/Components/QrScanner/TorchButton.tsx | 46 ++++++++++++++++++++ 2 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 src/Components/QrScanner/TorchButton.tsx diff --git a/src/Components/QrScanner/QrScannerPlugin.tsx b/src/Components/QrScanner/QrScannerPlugin.tsx index 279eef2..bdf524e 100644 --- a/src/Components/QrScanner/QrScannerPlugin.tsx +++ b/src/Components/QrScanner/QrScannerPlugin.tsx @@ -1,8 +1,9 @@ // file = QrScannerPlugin.jsx -import {MutableRefObject, useEffect, useRef, useState} from 'react'; -import {ArrowUpTrayIcon, BoltIcon, BoltSlashIcon} from '@heroicons/react/24/solid'; +import {MutableRefObject, useEffect, useRef} from 'react'; +import {ArrowUpTrayIcon} from '@heroicons/react/24/solid'; import {Html5Qrcode, Html5QrcodeScannerState, Html5QrcodeSupportedFormats} from 'html5-qrcode'; import {checkCameraPermissions} from '../../utils/media'; +import {TorchButton} from './TorchButton'; import classes from './QrScanner.module.css'; // Id of the HTML element used by the Html5QrcodeScanner. @@ -51,8 +52,6 @@ export default function QrScannerPlugin({ }: QrProps) { const aspectRatio = calcAspectRatio(); const html5CustomScanner: MutableRefObject = useRef(null); - const torchStateRef = useRef(false); - const [, setRender] = useState(false); // TODO: Find a better way to force a re-render without using the state (this causes the camera to reload) useEffect(() => { const showQRCode = async () => { @@ -105,26 +104,13 @@ export default function QrScannerPlugin({ onPermRefused, ]); - const toggleTorch = async () => { - try { - const track = html5CustomScanner.current?.getRunningTrackCameraCapabilities(); - if (track && track.torchFeature().isSupported()) { - torchStateRef.current = !torchStateRef.current; - await track.torchFeature().apply(torchStateRef.current); - setRender(prev => !prev); // TODO: Hacky - } - } catch (error) { - console.warn('Error toggling torch:', error); - } - }; - return ( <>
- + ); } @@ -154,18 +140,3 @@ export function FileUploadScanner({
); } - -export function ToggleTorchButton({onClick, toggled}: {onClick: () => void; toggled: boolean}) { - // TODO: Hide away the button when the torch is not supported - return ( -
-
- {toggled ? ( - - ) : ( - - )} -
-
- ); -} diff --git a/src/Components/QrScanner/TorchButton.tsx b/src/Components/QrScanner/TorchButton.tsx new file mode 100644 index 0000000..c510bcb --- /dev/null +++ b/src/Components/QrScanner/TorchButton.tsx @@ -0,0 +1,46 @@ +import {MutableRefObject, useState, useEffect} from 'react'; +import {BoltIcon, BoltSlashIcon} from '@heroicons/react/24/solid'; +import {Html5Qrcode} from 'html5-qrcode'; +import PropTypes from 'prop-types'; + +interface TorchButtonProps { + html5CustomScanner: MutableRefObject; +} + +export function TorchButton({html5CustomScanner}: TorchButtonProps) { + const [torchOn, setTorchOn] = useState(false); + + useEffect(() => { + const toggleTorch = async () => { + try { + const track = html5CustomScanner?.current?.getRunningTrackCameraCapabilities(); + if (track && track.torchFeature().isSupported()) { + await track.torchFeature().apply(torchOn); + } + } catch (error) { + console.warn('Failed to toggle torch:', error); + } + }; + + toggleTorch(); + }, [torchOn, html5CustomScanner]); + + return ( +
+
setTorchOn(prev => !prev)} + className="inline-flex cursor-pointer rounded-full bg-primary text-white" + > + {torchOn ? ( + + ) : ( + + )} +
+
+ ); +} + +TorchButton.propTypes = { + html5CustomScanner: PropTypes.object.isRequired, +}; From 582a818dfc6c96391ebe5e060fafa55ab1c68163 Mon Sep 17 00:00:00 2001 From: Dominic H Date: Mon, 29 Jul 2024 14:25:11 +0100 Subject: [PATCH 3/7] Hide away button, add permission checks --- src/Components/QrScanner/QrScannerPlugin.tsx | 6 ++-- src/Components/QrScanner/TorchButton.tsx | 33 ++++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/Components/QrScanner/QrScannerPlugin.tsx b/src/Components/QrScanner/QrScannerPlugin.tsx index bdf524e..3c4cf56 100644 --- a/src/Components/QrScanner/QrScannerPlugin.tsx +++ b/src/Components/QrScanner/QrScannerPlugin.tsx @@ -1,5 +1,5 @@ // file = QrScannerPlugin.jsx -import {MutableRefObject, useEffect, useRef} from 'react'; +import {MutableRefObject, useEffect, useRef, useState} from 'react'; import {ArrowUpTrayIcon} from '@heroicons/react/24/solid'; import {Html5Qrcode, Html5QrcodeScannerState, Html5QrcodeSupportedFormats} from 'html5-qrcode'; import {checkCameraPermissions} from '../../utils/media'; @@ -52,12 +52,14 @@ export default function QrScannerPlugin({ }: QrProps) { const aspectRatio = calcAspectRatio(); const html5CustomScanner: MutableRefObject = useRef(null); + const [canUseCamera, setCanUseCamera] = useState(true); useEffect(() => { const showQRCode = async () => { const hasCamPerm: boolean = await checkCameraPermissions(); if (!hasCamPerm) { onPermRefused(); + setCanUseCamera(false); return; } @@ -110,7 +112,7 @@ export default function QrScannerPlugin({
- + ); } diff --git a/src/Components/QrScanner/TorchButton.tsx b/src/Components/QrScanner/TorchButton.tsx index c510bcb..34a6ae2 100644 --- a/src/Components/QrScanner/TorchButton.tsx +++ b/src/Components/QrScanner/TorchButton.tsx @@ -1,14 +1,16 @@ import {MutableRefObject, useState, useEffect} from 'react'; -import {BoltIcon, BoltSlashIcon} from '@heroicons/react/24/solid'; +import {BoltIcon, BoltSlashIcon, ExclamationCircleIcon} from '@heroicons/react/24/solid'; import {Html5Qrcode} from 'html5-qrcode'; import PropTypes from 'prop-types'; interface TorchButtonProps { html5CustomScanner: MutableRefObject; + canUseCamera: boolean; } -export function TorchButton({html5CustomScanner}: TorchButtonProps) { +export function TorchButton({html5CustomScanner, canUseCamera}: TorchButtonProps) { const [torchOn, setTorchOn] = useState(false); + const [torchUnavailable, setTorchUnavailable] = useState(false); useEffect(() => { const toggleTorch = async () => { @@ -16,8 +18,12 @@ export function TorchButton({html5CustomScanner}: TorchButtonProps) { const track = html5CustomScanner?.current?.getRunningTrackCameraCapabilities(); if (track && track.torchFeature().isSupported()) { await track.torchFeature().apply(torchOn); + } else if (track && !track.torchFeature().isSupported()) { + setTorchUnavailable(true); + console.warn('Torch feature is not supported on this device.'); } } catch (error) { + setTorchUnavailable(true); console.warn('Failed to toggle torch:', error); } }; @@ -25,6 +31,28 @@ export function TorchButton({html5CustomScanner}: TorchButtonProps) { toggleTorch(); }, [torchOn, html5CustomScanner]); + if (!canUseCamera) { + return null; + } + + if (torchUnavailable) { + return ( + <> +
+ + + Your device's torch is unavailable + +
+
+
+ +
+
+ + ); + } + return (
Date: Mon, 29 Jul 2024 14:25:11 +0100 Subject: [PATCH 4/7] Fix incorrect torch toggle state icons --- src/Components/QrScanner/TorchButton.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Components/QrScanner/TorchButton.tsx b/src/Components/QrScanner/TorchButton.tsx index 34a6ae2..5953e69 100644 --- a/src/Components/QrScanner/TorchButton.tsx +++ b/src/Components/QrScanner/TorchButton.tsx @@ -32,7 +32,7 @@ export function TorchButton({html5CustomScanner, canUseCamera}: TorchButtonProps }, [torchOn, html5CustomScanner]); if (!canUseCamera) { - return null; + return; } if (torchUnavailable) { @@ -60,9 +60,9 @@ export function TorchButton({html5CustomScanner, canUseCamera}: TorchButtonProps className="inline-flex cursor-pointer rounded-full bg-primary text-white" > {torchOn ? ( - - ) : ( + ) : ( + )}
From 7c15462ff36054c09064c850b44d5e5c40df63a1 Mon Sep 17 00:00:00 2001 From: Dominic H Date: Wed, 31 Jul 2024 13:15:15 +0100 Subject: [PATCH 5/7] Turn off the torch when navigating away from the scan page --- src/Components/QrScanner/QrScannerPlugin.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Components/QrScanner/QrScannerPlugin.tsx b/src/Components/QrScanner/QrScannerPlugin.tsx index 3c4cf56..8e9c72e 100644 --- a/src/Components/QrScanner/QrScannerPlugin.tsx +++ b/src/Components/QrScanner/QrScannerPlugin.tsx @@ -54,6 +54,18 @@ export default function QrScannerPlugin({ const html5CustomScanner: MutableRefObject = useRef(null); const [canUseCamera, setCanUseCamera] = useState(true); + // Turn off the torch (if it is on) when navigating away from the scan page + async function switchOffTorch(html5CustomScanner: MutableRefObject) { + try { + const track = html5CustomScanner?.current?.getRunningTrackCameraCapabilities(); + if (track && track.torchFeature().value()) { + await track.torchFeature().apply(false); + } + } catch (error) { + console.warn('Failed to disable torch:', error); + } + } + useEffect(() => { const showQRCode = async () => { const hasCamPerm: boolean = await checkCameraPermissions(); @@ -86,6 +98,7 @@ export default function QrScannerPlugin({ return () => { const stopQrScanner = async () => { if (html5CustomScanner.current?.isScanning) { + switchOffTorch(html5CustomScanner); await html5CustomScanner.current.stop(); } html5CustomScanner.current?.clear(); From 11aa451c45ff0904eeff5c4cc420f231b6183a22 Mon Sep 17 00:00:00 2001 From: Dominic H Date: Tue, 20 Aug 2024 10:08:46 +0200 Subject: [PATCH 6/7] Redesign torch button --- src/Components/QrScanner/TorchButton.tsx | 27 ++++++++++++------------ 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Components/QrScanner/TorchButton.tsx b/src/Components/QrScanner/TorchButton.tsx index 5953e69..405efe0 100644 --- a/src/Components/QrScanner/TorchButton.tsx +++ b/src/Components/QrScanner/TorchButton.tsx @@ -44,27 +44,28 @@ export function TorchButton({html5CustomScanner, canUseCamera}: TorchButtonProps Your device's torch is unavailable
-
-
- -
-
); } return ( -
-
setTorchOn(prev => !prev)} - className="inline-flex cursor-pointer rounded-full bg-primary text-white" - > +
setTorchOn(prev => !prev)} + className="fit-content flex justify-center gap-1 bg-primary py-3 text-center text-white" + > + {torchOn ? ( - + <> + + Turn torch off + ) : ( - + <> + + Turn torch on + )} -
+
); } From 560120b0ebac9352bc695960e5e00d2bf19cccad Mon Sep 17 00:00:00 2001 From: Dominic H Date: Tue, 20 Aug 2024 14:54:17 +0200 Subject: [PATCH 7/7] Address feedback * Improve error handling * Improve torch switch-off handling * Remove unnecessary react fragements * Add visual feedback to torch button interactions --- src/Components/QrScanner/QrScannerPlugin.tsx | 30 +++++++++++++------- src/Components/QrScanner/TorchButton.tsx | 21 +++++++------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/Components/QrScanner/QrScannerPlugin.tsx b/src/Components/QrScanner/QrScannerPlugin.tsx index 8e9c72e..49ddc56 100644 --- a/src/Components/QrScanner/QrScannerPlugin.tsx +++ b/src/Components/QrScanner/QrScannerPlugin.tsx @@ -1,7 +1,8 @@ // file = QrScannerPlugin.jsx -import {MutableRefObject, useEffect, useRef, useState} from 'react'; +import {MutableRefObject, useEffect, useRef, useState, useCallback} from 'react'; import {ArrowUpTrayIcon} from '@heroicons/react/24/solid'; import {Html5Qrcode, Html5QrcodeScannerState, Html5QrcodeSupportedFormats} from 'html5-qrcode'; +import {useLogError} from '../../hooks/useError'; import {checkCameraPermissions} from '../../utils/media'; import {TorchButton} from './TorchButton'; import classes from './QrScanner.module.css'; @@ -53,18 +54,24 @@ export default function QrScannerPlugin({ const aspectRatio = calcAspectRatio(); const html5CustomScanner: MutableRefObject = useRef(null); const [canUseCamera, setCanUseCamera] = useState(true); + const logError = useLogError(); // Turn off the torch (if it is on) when navigating away from the scan page - async function switchOffTorch(html5CustomScanner: MutableRefObject) { - try { - const track = html5CustomScanner?.current?.getRunningTrackCameraCapabilities(); - if (track && track.torchFeature().value()) { - await track.torchFeature().apply(false); + const switchOffTorch = useCallback( + async function switchOffTorch(html5CustomScanner: MutableRefObject) { + try { + const track = html5CustomScanner?.current?.getRunningTrackCameraCapabilities(); + if (track && track.torchFeature().value()) { + await track.torchFeature().apply(false); + } + } catch (error) { + // This raises an error about invalid tracks - we have to catch it! (blame the library) + console.warn('Failed to disable torch:', error); + logError(`Failed to disable torch: ${error}`); } - } catch (error) { - console.warn('Failed to disable torch:', error); - } - } + }, + [logError] + ); useEffect(() => { const showQRCode = async () => { @@ -97,8 +104,8 @@ export default function QrScannerPlugin({ return () => { const stopQrScanner = async () => { + await switchOffTorch(html5CustomScanner); if (html5CustomScanner.current?.isScanning) { - switchOffTorch(html5CustomScanner); await html5CustomScanner.current.stop(); } html5CustomScanner.current?.clear(); @@ -117,6 +124,7 @@ export default function QrScannerPlugin({ qrCodeSuccessCallback, qrCodeErrorCallback, onPermRefused, + switchOffTorch, ]); return ( diff --git a/src/Components/QrScanner/TorchButton.tsx b/src/Components/QrScanner/TorchButton.tsx index 405efe0..e0bd7ca 100644 --- a/src/Components/QrScanner/TorchButton.tsx +++ b/src/Components/QrScanner/TorchButton.tsx @@ -2,6 +2,7 @@ import {MutableRefObject, useState, useEffect} from 'react'; import {BoltIcon, BoltSlashIcon, ExclamationCircleIcon} from '@heroicons/react/24/solid'; import {Html5Qrcode} from 'html5-qrcode'; import PropTypes from 'prop-types'; +import {useLogError} from '../../hooks/useError'; interface TorchButtonProps { html5CustomScanner: MutableRefObject; @@ -11,6 +12,7 @@ interface TorchButtonProps { export function TorchButton({html5CustomScanner, canUseCamera}: TorchButtonProps) { const [torchOn, setTorchOn] = useState(false); const [torchUnavailable, setTorchUnavailable] = useState(false); + const logError = useLogError(); useEffect(() => { const toggleTorch = async () => { @@ -25,11 +27,12 @@ export function TorchButton({html5CustomScanner, canUseCamera}: TorchButtonProps } catch (error) { setTorchUnavailable(true); console.warn('Failed to toggle torch:', error); + logError(`Failed to toggle torch: ${error}`); } }; toggleTorch(); - }, [torchOn, html5CustomScanner]); + }, [torchOn, html5CustomScanner, logError]); if (!canUseCamera) { return; @@ -37,21 +40,19 @@ export function TorchButton({html5CustomScanner, canUseCamera}: TorchButtonProps if (torchUnavailable) { return ( - <> -
- - - Your device's torch is unavailable - -
- +
+ + + Your device's torch is unavailable + +
); } return (
setTorchOn(prev => !prev)} - className="fit-content flex justify-center gap-1 bg-primary py-3 text-center text-white" + className="fit-content flex justify-center gap-1 bg-primary py-3 text-center text-white active:bg-blue-800 dark:bg-blue-600 dark:active:bg-blue-700" > {torchOn ? (