Skip to content

Commit

Permalink
Reduce code duplication for QR code scan (#8004)
Browse files Browse the repository at this point in the history
* Add Link/Unlink Barcode action
Fixes #7920

* remove unneeded imports

* remove duplication

* simplify

* add testing

* refactor type

* wait for reload to add coverage

* Add warning if custom barcode is used

* Add Image based assign

* fix action button size

* fix selection to prevent wrapping

* use left section for button

* Refactor to seperate Input

* Add comment when not scanning

* Fix punctuation

* factor scan area out

* fix readonly arg

* make BarcodeInput more generic

* make button optional

* reduce code duplication by using BarcodeInput

* remove unneeded abstraction
  • Loading branch information
matmair authored Aug 27, 2024
1 parent 313cb47 commit 450abcd
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 211 deletions.
59 changes: 59 additions & 0 deletions src/frontend/src/components/items/BarcodeInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { t } from '@lingui/macro';
import { ActionIcon, Box, Button, Divider, TextInput } from '@mantine/core';
import { IconQrcode } from '@tabler/icons-react';
import React, { useState } from 'react';

import { InputImageBarcode } from '../../pages/Index/Scan';

type BarcodeInputProps = {
onScan: (decodedText: string) => void;
value?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onAction?: () => void;
placeholder?: string;
label?: string;
actionText?: string;
};

export function BarcodeInput({
onScan,
value,
onChange,
onAction,
placeholder = t`Scan barcode data here using barcode scanner`,
label = t`Barcode`,
actionText = t`Scan`
}: Readonly<BarcodeInputProps>) {
const [isScanning, setIsScanning] = useState(false);

return (
<Box>
{isScanning && (
<>
<InputImageBarcode action={onScan} />
<Divider mt={'sm'} />
</>
)}
<TextInput
label={label}
value={value}
onChange={onChange}
placeholder={placeholder}
leftSection={
<ActionIcon
variant={isScanning ? 'filled' : 'subtle'}
onClick={() => setIsScanning(!isScanning)}
>
<IconQrcode />
</ActionIcon>
}
w="100%"
/>
{onAction ? (
<Button color="green" onClick={onAction} mt="lg" fullWidth>
{actionText}
</Button>
) : null}
</Box>
);
}
53 changes: 16 additions & 37 deletions src/frontend/src/components/items/QRCode.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,29 @@
import { Trans, t } from '@lingui/macro';
import {
ActionIcon,
Alert,
Box,
Button,
Code,
Divider,
Flex,
Group,
Image,
Select,
Skeleton,
Stack,
Text,
TextInput
Text
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { modals } from '@mantine/modals';
import { IconQrcode } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import QR from 'qrcode';
import { useEffect, useMemo, useState } from 'react';
import { set } from 'react-hook-form';

import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { InputImageBarcode, ScanItem } from '../../pages/Index/Scan';
import { apiUrl } from '../../states/ApiState';
import { useGlobalSettingsState } from '../../states/SettingsState';
import { CopyButton } from '../buttons/CopyButton';
import { QrCodeType } from './ActionDropdown';
import { BarcodeInput } from './BarcodeInput';

type QRCodeProps = {
ecl?: 'L' | 'M' | 'Q' | 'H';
Expand Down Expand Up @@ -162,37 +156,22 @@ export const QRCodeLink = ({ mdl_prop }: { mdl_prop: QrCodeType }) => {
location.reload();
});
}
const actionSubmit = (data: ScanItem[]) => {
linkBarcode(data[0].data);
const actionSubmit = (decodedText: string) => {
linkBarcode(decodedText);
};

const handleLinkBarcode = () => {
linkBarcode(barcode);
};

return (
<Box>
{isScanning ? (
<>
<InputImageBarcode action={actionSubmit} />
<Divider />
</>
) : null}
<TextInput
label={t`Barcode`}
value={barcode}
onChange={(event) => setBarcode(event.currentTarget.value)}
placeholder={t`Scan barcode data here using barcode scanner`}
leftSection={
<ActionIcon
variant="subtle"
onClick={toggleIsScanning.toggle}
size="input-sm"
>
<IconQrcode />
</ActionIcon>
}
w="100%"
/>
<Button color="green" onClick={() => linkBarcode()} mt="lg" fullWidth>
<Trans>Link</Trans>
</Button>
</Box>
<BarcodeInput
value={barcode}
onChange={(event) => setBarcode(event.currentTarget.value)}
onScan={actionSubmit}
onAction={handleLinkBarcode}
actionText={t`Link`}
/>
);
};

Expand Down
178 changes: 17 additions & 161 deletions src/frontend/src/components/modals/QrCodeModal.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,21 @@
import { Trans, t } from '@lingui/macro';
import {
Badge,
Button,
Container,
Group,
ScrollArea,
Space,
Stack,
Text
} from '@mantine/core';
import {
useDocumentVisibility,
useListState,
useLocalStorage
} from '@mantine/hooks';
import { Button, ScrollArea, Stack, Text } from '@mantine/core';
import { useListState } from '@mantine/hooks';
import { ContextModalProps } from '@mantine/modals';
import { showNotification } from '@mantine/notifications';
import { IconX } from '@tabler/icons-react';
import { Html5Qrcode } from 'html5-qrcode';
import { CameraDevice } from 'html5-qrcode/camera/core';
import { Html5QrcodeResult } from 'html5-qrcode/core';
import { useEffect, useState } from 'react';

import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState';
import { BarcodeInput } from '../items/BarcodeInput';

export function QrCodeModal({
context,
id
}: ContextModalProps<{ modalBody: string }>) {
const [qrCodeScanner, setQrCodeScanner] = useState<Html5Qrcode | null>(null);
const [camId, setCamId] = useLocalStorage<CameraDevice | null>({
key: 'camId',
defaultValue: null
});
const [scanningEnabled, setScanningEnabled] = useState<boolean>(false);
const [wasAutoPaused, setWasAutoPaused] = useState<boolean>(false);
const documentState = useDocumentVisibility();

}: Readonly<ContextModalProps<{ modalBody: string }>>) {
const [values, handlers] = useListState<string>([]);

// Mount QR code once we are loaded
useEffect(() => {
setQrCodeScanner(new Html5Qrcode('reader'));
}, []);

// Stop/star when leaving or reentering page
useEffect(() => {
if (scanningEnabled && documentState === 'hidden') {
stopScanning();
setWasAutoPaused(true);
} else if (wasAutoPaused && documentState === 'visible') {
startScanning();
setWasAutoPaused(false);
}
}, [documentState]);

// Scanner functions
function onScanSuccess(
decodedText: string,
decodedResult: Html5QrcodeResult
) {
qrCodeScanner?.pause();

function onScanAction(decodedText: string) {
handlers.append(decodedText);
api
.post(apiUrl(ApiEndpoints.barcode), { barcode: decodedText })
Expand All @@ -77,124 +29,28 @@ export function QrCodeModal({
window.location.href = response.data.url;
}
});

qrCodeScanner?.resume();
}

function onScanFailure(error: string) {
if (
error !=
'QR code parse error, error = NotFoundException: No MultiFormat Readers were able to detect the code.'
) {
console.warn(`Code scan error = ${error}`);
}
}

function selectCamera() {
Html5Qrcode.getCameras()
.then((devices) => {
if (devices?.length) {
setCamId(devices[0]);
}
})
.catch((err) => {
showNotification({
title: t`Error while getting camera`,
message: err,
color: 'red',
icon: <IconX />
});
});
}

function startScanning() {
if (camId && qrCodeScanner) {
qrCodeScanner
.start(
camId.id,
{ fps: 10, qrbox: { width: 250, height: 250 } },
(decodedText, decodedResult) => {
onScanSuccess(decodedText, decodedResult);
},
(errorMessage) => {
onScanFailure(errorMessage);
}
)
.catch((err: string) => {
showNotification({
title: t`Error while scanning`,
message: err,
color: 'red',
icon: <IconX />
});
});
setScanningEnabled(true);
}
}

function stopScanning() {
if (qrCodeScanner && scanningEnabled) {
qrCodeScanner.stop().catch((err: string) => {
showNotification({
title: t`Error while stopping`,
message: err,
color: 'red',
icon: <IconX />
});
});
setScanningEnabled(false);
}
}

return (
<Stack>
<Group>
<Text size="sm">{camId?.label}</Text>
<Space style={{ flex: 1 }} />
<Badge>{scanningEnabled ? t`Scanning` : t`Not scanning`}</Badge>
</Group>
<Container px={0} id="reader" w={'100%'} mih="300px" />
{!camId ? (
<Button onClick={() => selectCamera()}>
<Trans>Select Camera</Trans>
</Button>
<Stack gap="xs">
<BarcodeInput onScan={onScanAction} />
{values.length == 0 ? (
<Text c={'grey'}>
<Trans>No scans yet!</Trans>
</Text>
) : (
<>
<Group>
<Button
style={{ flex: 1 }}
onClick={() => startScanning()}
disabled={camId != undefined && scanningEnabled}
>
<Trans>Start scanning</Trans>
</Button>
<Button
style={{ flex: 1 }}
onClick={() => stopScanning()}
disabled={!scanningEnabled}
>
<Trans>Stop scanning</Trans>
</Button>
</Group>
{values.length == 0 ? (
<Text c={'grey'}>
<Trans>No scans yet!</Trans>
</Text>
) : (
<ScrollArea style={{ height: 200 }} type="auto" offsetScrollbars>
{values.map((value, index) => (
<div key={index}>{value}</div>
))}
</ScrollArea>
)}
</>
<ScrollArea style={{ height: 200 }} type="auto" offsetScrollbars>
{values.map((value, index) => (
<div key={`${index}-${value}`}>{value}</div>
))}
</ScrollArea>
)}
<Button
fullWidth
mt="md"
color="red"
onClick={() => {
stopScanning();
// stopScanning();
context.closeModal(id);
}}
>
Expand Down
Loading

0 comments on commit 450abcd

Please sign in to comment.