Skip to content

Commit

Permalink
Add Torch/Flashlight support (#65)
Browse files Browse the repository at this point in the history
* WIP: Torch/Flashlight support

* Refactor torch into seperate component

Fixes issues with state updates breaking the camera

* Hide away button, add permission checks

* Fix incorrect torch toggle state icons

* Turn off the torch when navigating away from the scan page

* Redesign torch button

* Address feedback

* Improve error handling
* Improve torch switch-off handling
* Remove unnecessary react fragements
* Add visual feedback to torch button interactions
  • Loading branch information
GovernmentPlates authored Aug 20, 2024
1 parent 6355ee7 commit df30933
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 5 deletions.
37 changes: 32 additions & 5 deletions src/Components/QrScanner/QrScannerPlugin.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// file = QrScannerPlugin.jsx
import {MutableRefObject, useEffect, useRef} 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';

// Id of the HTML element used by the Html5QrcodeScanner.
Expand Down Expand Up @@ -51,12 +53,32 @@ export default function QrScannerPlugin({
}: QrProps) {
const aspectRatio = calcAspectRatio();
const html5CustomScanner: MutableRefObject<Html5Qrcode | null> = 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
const switchOffTorch = useCallback(
async function switchOffTorch(html5CustomScanner: MutableRefObject<Html5Qrcode | null>) {
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}`);
}
},
[logError]
);

useEffect(() => {
const showQRCode = async () => {
const hasCamPerm: boolean = await checkCameraPermissions();
if (!hasCamPerm) {
onPermRefused();
setCanUseCamera(false);
return;
}

Expand All @@ -82,6 +104,7 @@ export default function QrScannerPlugin({

return () => {
const stopQrScanner = async () => {
await switchOffTorch(html5CustomScanner);
if (html5CustomScanner.current?.isScanning) {
await html5CustomScanner.current.stop();
}
Expand All @@ -101,13 +124,17 @@ export default function QrScannerPlugin({
qrCodeSuccessCallback,
qrCodeErrorCallback,
onPermRefused,
switchOffTorch,
]);

return (
<div className={classes.wrapper}>
<ShadedRegion size={qrbox} />
<div id={qrcodeRegionId} />
</div>
<>
<div className={classes.wrapper}>
<ShadedRegion size={qrbox}></ShadedRegion>
<div id={qrcodeRegionId} />
</div>
<TorchButton html5CustomScanner={html5CustomScanner} canUseCamera={canUseCamera} />
</>
);
}

Expand Down
77 changes: 77 additions & 0 deletions src/Components/QrScanner/TorchButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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<Html5Qrcode | null>;
canUseCamera: boolean;
}

export function TorchButton({html5CustomScanner, canUseCamera}: TorchButtonProps) {
const [torchOn, setTorchOn] = useState(false);
const [torchUnavailable, setTorchUnavailable] = useState(false);
const logError = useLogError();

useEffect(() => {
const toggleTorch = async () => {
try {
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);
logError(`Failed to toggle torch: ${error}`);
}
};

toggleTorch();
}, [torchOn, html5CustomScanner, logError]);

if (!canUseCamera) {
return;
}

if (torchUnavailable) {
return (
<div className="fit-content flex justify-center gap-1 bg-yellow-500 py-3 text-center text-amber-900">
<span className="flex items-center">
<ExclamationCircleIcon className="mr-1 h-6 w-6" />
Your device's torch is unavailable
</span>
</div>
);
}

return (
<div
onClick={() => setTorchOn(prev => !prev)}
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"
>
<span className="flex items-center">
{torchOn ? (
<>
<BoltSlashIcon className="mr-1 h-6 w-6" />
Turn torch off
</>
) : (
<>
<BoltIcon className="mr-1 h-6 w-6" />
Turn torch on
</>
)}
</span>
</div>
);
}

TorchButton.propTypes = {
html5CustomScanner: PropTypes.object.isRequired,
canUseCamera: PropTypes.bool.isRequired,
};

0 comments on commit df30933

Please sign in to comment.