Skip to content

Commit

Permalink
reworking scanner
Browse files Browse the repository at this point in the history
- drop state and store selected device id in cookie and validate on the server
- implement a separate hook to request permissions and component that initiates zXing
- Implement view that shows while permissions are pending
- implement a camera switching view
  • Loading branch information
DonKoko committed Mar 26, 2024
1 parent 4b8d5dc commit fb25a85
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 112 deletions.
106 changes: 106 additions & 0 deletions app/components/zxing-scanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useFetcher, useLoaderData, useNavigate } from "@remix-run/react";
import { useZxing } from "react-zxing";
import { useClientNotification } from "~/hooks/use-client-notification";
import type { loader } from "~/routes/_layout+/scanner";
import { isFormProcessing } from "~/utils";
import { ShelfError } from "~/utils/error";
import { Spinner } from "./shared/spinner";

export const ZXingScanner = ({
videoMediaDevices,
}: {
videoMediaDevices: MediaDeviceInfo[] | undefined;
}) => {
const [sendNotification] = useClientNotification();
const navigate = useNavigate();
const fetcher = useFetcher();
const { scannerCameraId } = useLoaderData<typeof loader>();
const isProcessing = isFormProcessing(fetcher.state);

// Function to decode the QR code
const decodeQRCodes = (result: string) => {
if (result != null) {
const regex = /^(https?:\/\/)([^/:]+)(:\d+)?\/qr\/([a-zA-Z0-9]+)$/;
/** We make sure the value of the QR code matches the structure of Shelf qr codes */
const match = result.match(regex);
if (!match) {
/** If the QR code does not match the structure of Shelf qr codes, we show an error message */
sendNotification({
title: "QR Code Not Valid",
message: "Please Scan valid asset QR",
icon: { name: "trash", variant: "error" },
});
return;
}

sendNotification({
title: "Shelf's QR Code detected",
message: "Redirecting to mapped asset",
icon: { name: "success", variant: "success" },
});
const qrId = match[4]; // Get the last segment of the URL as the QR id
navigate(`/qr/${qrId}`);
}
};

const { ref } = useZxing({
deviceId: scannerCameraId,
constraints: { video: true, audio: false },
onDecodeResult(result) {
decodeQRCodes(result.getText());
},
onError(cause) {
throw new ShelfError({
message: "Unable to access media devices permission",
status: 403,
label: "Scanner",
cause,
});
},
});

return (
<div className="relative size-full min-h-[400px]">
{isProcessing ? (
<div className="mt-4 flex flex-col items-center justify-center">
<Spinner /> Switching cameras...
</div>
) : (
<>
<video
ref={ref}
width="100%"
autoPlay={true}
controls={false}
muted={true}
playsInline={true}
className={`pointer-events-none size-full object-cover object-center`}
/>
<fetcher.Form
method="post"
action="/api/user/prefs/scanner-camera"
className="relative"
onChange={(e) => {
const form = e.currentTarget;
fetcher.submit(form);
}}
>
{videoMediaDevices && videoMediaDevices?.length > 0 ? (
<select
className="absolute bottom-3 left-3 z-10 w-[calc(100%-24px)] rounded border-0 md:left-auto md:right-3 md:w-auto"
name="scannerCameraId"
defaultValue={scannerCameraId}
>
{videoMediaDevices.map((device, index) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label ? device.label : `Camera ${index + 1}`}
</option>
))}
</select>
) : null}
</fetcher.Form>
</>
)}
</div>
);
};
71 changes: 2 additions & 69 deletions app/hooks/use-qr-scanner.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,8 @@
import { useEffect, useState } from "react";
import { useNavigate } from "@remix-run/react";
import { useMediaDevices } from "react-media-devices";
import { useZxing } from "react-zxing";
import { ShelfError } from "~/utils";
import { useClientNotification } from "./use-client-notification";

// Custom hook to handle video devices
export const useQrScanner = (scannerCameraId: string) => {
const navigate = useNavigate();
/** Get the default id from the loader
* This is the default camera that the user has selected on previous use of the scanner
* It comes from a cookie
*/
const [sendNotification] = useClientNotification();
// const [hasPermission, setHasPermission] = useState<boolean | null>(null);

export const useQrScanner = () => {
const { devices } = useMediaDevices({
constraints: {
video: true,
Expand All @@ -25,7 +13,7 @@ export const useQrScanner = (scannerCameraId: string) => {
// Initialize videoMediaDevices as undefined. This will be used to store the video devices once they have loaded.
const [videoMediaDevices, setVideoMediaDevices] = useState<
MediaDeviceInfo[] | undefined
>();
>(undefined);

useEffect(() => {
if (devices) {
Expand All @@ -38,65 +26,10 @@ export const useQrScanner = (scannerCameraId: string) => {
}

setVideoMediaDevices(videoDevices);

// Set hasPermission to true as devices are available
// setHasPermission(true);
} else {
// Set hasPermission to false as devices are not available
// setHasPermission(false);
}
}, [devices]);

// Use the useZxing hook to access the camera and scan for QR codes
const { ref } = useZxing({
deviceId: scannerCameraId,
constraints: { video: true, audio: false },
onDecodeResult(result) {
decodeQRCodes(result.getText());
},

onError(cause) {
/** This is not idea an kinda useless actually
* We are simply showing the message to the user based on hasPermission so if they deny permission we show a message
*/
throw new ShelfError({
message: "Unable to access media devices permission",
status: 403,
label: "Scanner",
cause,
});
},
});

// Function to decode the QR code
const decodeQRCodes = (result: string) => {
if (result != null) {
const regex = /^(https?:\/\/)([^/:]+)(:\d+)?\/qr\/([a-zA-Z0-9]+)$/;
/** We make sure the value of the QR code matches the structure of Shelf qr codes */
const match = result.match(regex);
if (!match) {
/** If the QR code does not match the structure of Shelf qr codes, we show an error message */
sendNotification({
title: "QR Code Not Valid",
message: "Please Scan valid asset QR",
icon: { name: "trash", variant: "error" },
});
return;
}

sendNotification({
title: "Shelf's QR Code detected",
message: "Redirecting to mapped asset",
icon: { name: "success", variant: "success" },
});
const qrId = match[4]; // Get the last segment of the URL as the QR id
navigate(`/qr/${qrId}`);
}
};

return {
ref,
videoMediaDevices,
// hasPermission,
};
};
48 changes: 7 additions & 41 deletions app/routes/_layout+/scanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { Link, useFetcher, useLoaderData } from "@remix-run/react";
import { ErrorContent } from "~/components/errors";
import Header from "~/components/layout/header";
import type { HeaderData } from "~/components/layout/header/types";
import { Spinner } from "~/components/shared/spinner";
import { ZXingScanner } from "~/components/zxing-scanner";
import { useQrScanner } from "~/hooks/use-qr-scanner";
import scannerCss from "~/styles/scanner.css";
import { appendToMetaTitle } from "~/utils/append-to-meta-title";
Expand Down Expand Up @@ -45,55 +47,19 @@ export const meta: MetaFunction<typeof loader> = () => [
];

const QRScanner = () => {
const fetcher = useFetcher();
const { scannerCameraId } = useLoaderData<typeof loader>();

const { ref, videoMediaDevices } = useQrScanner(scannerCameraId);
const { videoMediaDevices } = useQrScanner();

return (
<>
<Header title="QR code scanner" />
<div className=" -mx-4 flex h-[calc(100vh-167px)] flex-col md:h-[calc(100vh-132px)]">
{/* {!videoMediaDevices || videoMediaDevices.length === 0 ? (
{videoMediaDevices && videoMediaDevices.length > 0 ? (
<ZXingScanner videoMediaDevices={videoMediaDevices} />
) : (
<div className="mt-4 flex flex-col items-center justify-center">
<Spinner /> Waiting for permission to access camera.
</div>
) : ( */}
<div className="relative size-full min-h-[400px]">
<video
ref={ref}
width="100%"
autoPlay={true}
controls={false}
muted={true}
playsInline={true}
className={`pointer-events-none size-full object-cover object-center`}
/>
<fetcher.Form
method="post"
action="/api/user/prefs/scanner-camera"
className="relative"
onChange={(e) => {
const form = e.currentTarget;
fetcher.submit(form);
}}
>
{videoMediaDevices && videoMediaDevices?.length > 0 ? (
<select
className="absolute bottom-3 left-3 z-10 w-[calc(100%-24px)] rounded border-0 md:left-auto md:right-3 md:w-auto"
name="scannerCameraId"
defaultValue={scannerCameraId}
>
{videoMediaDevices.map((device, index) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label ? device.label : `Camera ${index + 1}`}
</option>
))}
</select>
) : null}
</fetcher.Form>
</div>
{/* )} */}
)}
</div>
</>
);
Expand Down
5 changes: 3 additions & 2 deletions docs/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ This will make sure you have a DATABASE that you are ready to connect to.
--restart unless-stopped \
ghcr.io/shelf-nu/shelf.nu:latest
```
> [!NOTE]
> `DATABASE_URL` and `DIRECT_URL` are mandatory when using Supabase Cloud. Learn more in [Get Started > Development](./get-started.md#development) section.
> [!NOTE] > `DATABASE_URL` and `DIRECT_URL` are mandatory when using Supabase Cloud. Learn more in [Get Started > Development](./get-started.md#development) section.
3. Run the following command to seed the database (create initial user), **only once after the first deployment**:
```sh
docker exec -it shelf npm run setup:seed
Expand All @@ -47,6 +46,7 @@ This will make sure you have a DATABASE that you are ready to connect to.

> [!CAUTION]
> During development involving Dockerfile changes, make sure to **address the correct Dockerfile** in your builds:
>
> - Fly.io will be built via `Dockerfile`
> - ghcr.io will be built via `Dockerfile.image`
Expand Down Expand Up @@ -75,6 +75,7 @@ docker run -d \
You can also run shelf on ARM64 processors.

1. Linux / Pine A64

```sh
root@DietPi:~#
docker run -it --rm --entrypoint /usr/bin/uname ghcr.io/shelf-nu/shelf.nu:latest -a
Expand Down
1 change: 1 addition & 0 deletions docs/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ The database seed script creates a new user with some data you can use to get st

> [!CAUTION]
> During development involving Dockerfile changes, make sure to **address the correct file** in your builds:
>
> - Fly.io will be built via `Dockerfile`
> - ghcr.io will be built via `Dockerfile.image`
Expand Down

0 comments on commit fb25a85

Please sign in to comment.