Skip to content

Commit

Permalink
Merge pull request #39 from cereallarceny/mobile-and-safari
Browse files Browse the repository at this point in the history
Dramatically improving performance, bundle size, and browser support
  • Loading branch information
cereallarceny authored Sep 12, 2024
2 parents 3e1610e + 2d0b8b1 commit 0f9b34d
Show file tree
Hide file tree
Showing 48 changed files with 2,239 additions and 1,368 deletions.
8 changes: 8 additions & 0 deletions .changeset/selfish-eggs-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@flipbookqr/reader': minor
'@flipbookqr/shared': minor
'@flipbookqr/writer': minor
'web': minor
---

Dramatically improving performance, browser support, and bundle size
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
}
],
"css.customData": [".vscode/tailwind.json"],
"files.autoSave": "afterDelay"
"files.autoSave": "off"
}
25 changes: 12 additions & 13 deletions apps/web/app/benchmark/page.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
'use client';

import Image from 'next/image';
import { useState, useCallback } from 'react';
import type { FormEventHandler, JSX } from 'react';
import { Writer } from '@flipbookqr/writer';
import { Reader, FileProcessor } from '@flipbookqr/reader';
import Image from 'next/image';

export default function File(): JSX.Element {
const [decoded, setDecoded] = useState<string | null>(null);
const [isDecoding, setIsDecoding] = useState(false);
const [text, setText] = useState('');
const [qr, setQr] = useState('');
const [src, setSrc] = useState('');

const generate = useCallback(async () => {
setQr('');
const writer = new Writer();
const qrs = await writer.write(text);
const result = await writer.compose(qrs);
const qrs = writer.write(text);

const blob = writer.toGif(qrs);
const url = URL.createObjectURL(blob);

setQr(result);
setSrc(url);
}, [text]);

const handleSubmit = async (event: Event): Promise<void> => {
Expand All @@ -30,10 +31,10 @@ export default function File(): JSX.Element {
const file = formData.get('inputFile') as File;

const reader = new Reader({
frameProcessor: new FileProcessor(file),
frameProcessor: new FileProcessor(),
});

const decodedData = await reader.read();
const decodedData = await reader.read(file);

setDecoded(decodedData);
} catch (error) {
Expand All @@ -59,11 +60,9 @@ export default function File(): JSX.Element {
Generate Flipbook
</button>
<br />
{qr ? (
<div>
<Image alt="QR Code" height={512} id="image" src={qr} width={512} />
</div>
) : null}
{src && (
<Image alt="QR code" id="image" src={src} width={200} height={200} />
)}

<hr style={{ marginTop: 20, marginBottom: 20 }} />

Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/components/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default function DialogBox({
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>

<div className="fixed inset-0 z-10 w-screen overflow-hidden">
<div className="fixed inset-0 z-10 w-screen overflow-hidden overflow-y-auto">
<div
className={twMerge(
'min-h-full w-full mx-auto px-6 sm:px-0',
Expand All @@ -69,7 +69,7 @@ export default function DialogBox({
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg p-8 my-8 bg-white shadow-xl transition-all">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg px-4 py-8 my-8 bg-white shadow-xl transition-all">
{children}
</Dialog.Panel>
</Transition.Child>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ const getDisclosureClasses = (isActive: boolean): string => {
};

const leftLinks = [
{ href: '/', label: 'Home' },
{ href: '/writer', label: 'Writer' },
{ href: '/', label: 'Read' },
{ href: '/writer', label: 'Write' },
];

const rightLinks = [
Expand Down
22 changes: 22 additions & 0 deletions apps/web/app/components/support.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useEffect, useState } from 'react';

export default function useNavigatorSupport() {
const [userMediaSupport, setUserMediaSupport] = useState<boolean>(false);
const [displayMediaSupport, setDisplayMediaSupport] =
useState<boolean>(false);

useEffect(() => {
if (navigator.mediaDevices && !!navigator.mediaDevices.getUserMedia) {
setUserMediaSupport(true);
}

if (navigator.mediaDevices && !!navigator.mediaDevices.getDisplayMedia) {
setDisplayMediaSupport(true);
}
}, []);

return {
getUserMedia: userMediaSupport,
getDisplayMedia: displayMediaSupport,
};
}
2 changes: 1 addition & 1 deletion apps/web/app/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const meta = {
};

export const homepage = {
title: 'Scan a Flipbook QR Code',
title: 'Read a Flipbook QR Code',
description:
'"Flipbooks" are a superset of QR codes in the form of an animated GIF. This allows for digital information of any size to be transferred without the need for an internet connection. Flipbooks can be used to download apps, music, movies, rich-text, and more.',
version: `Beta Release v${pkg.version}`,
Expand Down
16 changes: 8 additions & 8 deletions apps/web/app/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ export default function Hero(): JSX.Element {
return (
<div className="px-6 lg:px-8">
<div className="mx-auto max-w-3xl py-24 sm:py-36 lg:py-48">
<iframe
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
className="w-full aspect-video max-w-3xl mx-auto mb-8 lg:mb-12"
referrerPolicy="strict-origin-when-cross-origin"
src="https://www.youtube.com/embed/D4QD9DaISEs?si=-0S6GmPbqu6t9GGh&amp;controls=0"
title="Flipbook Video"
/>
<div className="hidden sm:mb-8 sm:flex sm:justify-center">
<div className="relative rounded-full px-3 py-1 text-sm leading-6 text-gray-600 ring-1 ring-gray-900/10 hover:ring-gray-900/20">
{homepage.version}
Expand Down Expand Up @@ -64,6 +56,14 @@ export default function Hero(): JSX.Element {
</div>
</div>
) : null}
<iframe
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
className="w-full aspect-video max-w-3xl mx-auto mt-8 lg:mt-12"
referrerPolicy="strict-origin-when-cross-origin"
src="https://www.youtube.com/embed/D4QD9DaISEs?si=-0S6GmPbqu6t9GGh&amp;controls=0"
title="Flipbook Video"
/>
</div>
</div>
</div>
Expand Down
23 changes: 20 additions & 3 deletions apps/web/app/method-buttons.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
'use client';

import { useCallback, useEffect, useState } from 'react';
import { Fragment, useCallback, useEffect, useState } from 'react';
import { FileProcessor, Reader, WebRTCProcessor } from '@flipbookqr/reader';
import { Button } from './components/button';
import DialogBox from './components/dialog';
import useNavigatorSupport from './components/support';

interface MethodButtonProps {
setResults: (results: string) => void;
Expand All @@ -14,6 +15,9 @@ export function CameraScan({
setResults,
children,
}: MethodButtonProps): JSX.Element {
// Get the navigator support
const supports = useNavigatorSupport();

// Store the reader
const [reader, setReader] = useState<Reader>();

Expand Down Expand Up @@ -54,6 +58,11 @@ export function CameraScan({
[reader, setResults]
);

// If the browser does not support user media, return an empty fragment
if (!supports.getUserMedia) {
return <Fragment />;
}

return (
<>
<Button onClick={onGetCameraTracks}>{children}</Button>
Expand Down Expand Up @@ -109,10 +118,10 @@ export function Upload({
if (!f) return;

const reader = new Reader({
frameProcessor: new FileProcessor(f),
frameProcessor: new FileProcessor(),
});

setResults(await reader.read());
setResults(await reader.read(f));
};

try {
Expand All @@ -129,11 +138,19 @@ export function ScreenScan({
setResults,
children,
}: MethodButtonProps): JSX.Element {
// Get the navigator support
const supports = useNavigatorSupport();

// When clicking the button, we want to trigger a screen capture
const onClick = useCallback(async () => {
const reader = new Reader();
setResults(await reader.read());
}, [setResults]);

// If the browser does not support display media, return an empty fragment
if (!supports.getDisplayMedia) {
return <Fragment />;
}

return <Button onClick={onClick}>{children}</Button>;
}
50 changes: 47 additions & 3 deletions apps/web/app/writer/components/configuration-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Button } from '../../components/button';
import InputGroup from '../../components/input-group';
import NumberInput from '../../components/number-input';
import SelectInput from '../../components/select-input';
import { ErrorCorrectionLevel } from '@nuintun/qrcode';
import { useMemo } from 'react';

interface ConfigurationFormProps {
defaultValues: Partial<WriterProps>;
Expand All @@ -16,15 +18,57 @@ export default function ConfigurationForm({
}: ConfigurationFormProps): JSX.Element {
const { register, handleSubmit } = useForm({ defaultValues });

const errorLevels = useMemo(() => {
const keys = Object.keys(ErrorCorrectionLevel);

return keys.reduce((acc, key) => {
if (!isNaN(Number(key))) {
return acc;
}

return {
...acc,
[key]: ErrorCorrectionLevel[key as keyof typeof ErrorCorrectionLevel],
};
}, {});
}, []);

return (
<form onSubmit={(...args) => void handleSubmit(onSubmit)(...args)}>
<p className="font-bold text-xl mb-4">Configuration</p>
<div className="flex gap-4 flex-col mb-4">
<InputGroup label="Size (in pixels)">
<NumberInput {...register('size')} />
<InputGroup label="Error Correction Level">
<SelectInput {...register('errorCorrectionLevel')}>
{Object.keys(errorLevels).map((key) => (
<option
key={key}
value={errorLevels[key as keyof typeof errorLevels]}
>
{key}
</option>
))}
</SelectInput>
</InputGroup>
<InputGroup label="Encoding Hint">
<SelectInput {...register('encodingHint')}>
<option value="true">Yes</option>
<option value="false">No</option>
</SelectInput>
</InputGroup>
<InputGroup label="Version">
<NumberInput {...register('version')} min={1} max={40} />
</InputGroup>
<InputGroup label="Module Size">
<NumberInput {...register('moduleSize')} min={1} />
</InputGroup>
<InputGroup label="Margin">
<NumberInput {...register('margin')} min={0} />
</InputGroup>
<InputGroup label="Delay (in ms)">
<NumberInput {...register('gifOptions.delay')} />
<NumberInput {...register('delay')} />
</InputGroup>
<InputGroup label="Split Length">
<NumberInput {...register('splitLength')} min={1} />
</InputGroup>
<InputGroup label="Log Level">
<SelectInput {...register('logLevel')}>
Expand Down
27 changes: 3 additions & 24 deletions apps/web/app/writer/components/generate.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,26 @@
'use client';

import { useCallback, useState } from 'react';
import { Writer, type WriterProps } from '@flipbookqr/writer';
import { type WriterProps } from '@flipbookqr/writer';
import { CogIcon } from '@heroicons/react/24/solid';
import { Button, IconButton } from '../../components/button';
import DialogBox from '../../components/dialog';
import ConfigurationForm from './configuration-form';

interface GenerateProps {
code: string;
setQR: (qr: string) => void;
configuration: Partial<WriterProps>;
setConfiguration: (config: Partial<WriterProps>) => void;
createQR: () => void;
}

export default function Generate({
code,
setQR,
configuration,
setConfiguration,
createQR,
}: GenerateProps): JSX.Element {
// State whether the dialog is open
const [isOpen, setIsOpen] = useState(false);

// A function to create the QR code
const createQR = useCallback(async (): Promise<void> => {
try {
// Resetting whatever QR code was there before
setQR('');

// Write the QR code
const writer = new Writer(configuration);
const qrs = await writer.write(code);
const result = await writer.compose(qrs);

// Set the QR code
setQR(result);
} catch (e) {
// eslint-disable-next-line no-console -- Intentional
console.error(e);
}
}, [code, setQR, configuration]);

// A function to launch the configuration dialog
const launchDialog = useCallback(() => {
setIsOpen(!isOpen);
Expand Down
Loading

0 comments on commit 0f9b34d

Please sign in to comment.