Initial commit: any%
commit
c938303a07
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
|
@ -0,0 +1,11 @@
|
||||||
|
# qr.umm.gay
|
||||||
|
|
||||||
|
qr decoder web tool any% (20 minutes)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ npm install # Install dependencies
|
||||||
|
$ npm run dev # Start a local dev server
|
||||||
|
$ npm run build # Generate static bundle in 'out/' directory
|
||||||
|
```
|
|
@ -0,0 +1,155 @@
|
||||||
|
import { useEffect, useCallback, useReducer } from "react";
|
||||||
|
|
||||||
|
/* <PasteContainer
|
||||||
|
onPaste={files => files.length > 0 && this.props.onFileAccepted(files[0], files[0])}
|
||||||
|
/> */
|
||||||
|
|
||||||
|
// NOTE: `onPaste` should return an array of files that were acceptedFiles
|
||||||
|
//
|
||||||
|
// const Image = ({ src }) => <img src={image} alt={`Pasted: ${image}`} />
|
||||||
|
//
|
||||||
|
// <PasteContainer
|
||||||
|
// onPaste={files => this.method(files)}
|
||||||
|
// onError={err => console.error(err)}
|
||||||
|
// >
|
||||||
|
// {({images}) => images.map((image, i) => <Image src={image} key={i} />)}
|
||||||
|
// </PasteContainer>
|
||||||
|
|
||||||
|
const DEFAULT_ACCEPTED_FILES = ["image/gif", "image/png", "image/jpeg", "image/bmp"];
|
||||||
|
|
||||||
|
const _transformImageDataToURL = (
|
||||||
|
data: DataTransfer,
|
||||||
|
acceptedFiles: string[] = DEFAULT_ACCEPTED_FILES
|
||||||
|
): string | undefined => {
|
||||||
|
const isValidFormat = (fileType: string): boolean => acceptedFiles.includes(fileType);
|
||||||
|
// NOTE: This needs to be a for loop, it's a list like object
|
||||||
|
for (let i = 0; i < data.items.length; i++) {
|
||||||
|
if (!isValidFormat(data.items[i].type)) {
|
||||||
|
continue;
|
||||||
|
// throw new Error(`Sorry, that's not a format we support ${data.items[i].type}`);
|
||||||
|
}
|
||||||
|
let blob: BlobLikeFile = data.items[i].getAsFile();
|
||||||
|
|
||||||
|
if (blob) {
|
||||||
|
// We shouldn't fire the callback if we can't create `new Blob()`
|
||||||
|
let file = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type BlobLikeFile = File | null;
|
||||||
|
|
||||||
|
export interface GlueJarOptions<T extends HTMLElement> {
|
||||||
|
acceptedFiles: string[];
|
||||||
|
ref?: React.RefObject<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGlueJarState {
|
||||||
|
pasted: string[];
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum GlueActions {
|
||||||
|
SET_FILE = "SET_FILES",
|
||||||
|
SET_ERROR = "SET_ERROR",
|
||||||
|
}
|
||||||
|
|
||||||
|
type GlueJarActions =
|
||||||
|
| {
|
||||||
|
type: GlueActions.SET_FILE;
|
||||||
|
file: string;
|
||||||
|
}
|
||||||
|
| { type: GlueActions.SET_ERROR; error: string };
|
||||||
|
|
||||||
|
type GlueJarReducer = React.Reducer<IGlueJarState, GlueJarActions>;
|
||||||
|
|
||||||
|
const reducer: GlueJarReducer = (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case GlueActions.SET_FILE:
|
||||||
|
return { ...state, pasted: [action.file, ...state.pasted], error: null };
|
||||||
|
case GlueActions.SET_ERROR:
|
||||||
|
return { ...state, error: action.error };
|
||||||
|
default:
|
||||||
|
throw new Error("Must specify action type");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const useGlueJarReducer = () =>
|
||||||
|
useReducer<GlueJarReducer>(reducer, {
|
||||||
|
pasted: [],
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function usePasteHandler(acceptedFiles: string[] = DEFAULT_ACCEPTED_FILES) {
|
||||||
|
const [state, dispatch] = useGlueJarReducer();
|
||||||
|
|
||||||
|
const transformImageDataToURL = useCallback(
|
||||||
|
(data: DataTransfer): void => {
|
||||||
|
try {
|
||||||
|
const file = _transformImageDataToURL(data, acceptedFiles);
|
||||||
|
dispatch(
|
||||||
|
file
|
||||||
|
? { type: GlueActions.SET_FILE, file }
|
||||||
|
: {
|
||||||
|
type: GlueActions.SET_ERROR,
|
||||||
|
error: "Something went wrong",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
dispatch({ type: GlueActions.SET_ERROR, error: error.message });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, acceptedFiles]
|
||||||
|
);
|
||||||
|
|
||||||
|
const pasteHandler = useCallback(
|
||||||
|
({ clipboardData }: ClipboardEvent): void => {
|
||||||
|
if (clipboardData && clipboardData.items.length > 0) {
|
||||||
|
transformImageDataToURL(clipboardData);
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: GlueActions.SET_ERROR,
|
||||||
|
error: `Sorry, to bother you but there was no image pasted.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, transformImageDataToURL]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [state, pasteHandler] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useGlueJar
|
||||||
|
* if you don't pass a `ref` to the options it will default to use the document
|
||||||
|
* to add an event listener
|
||||||
|
* @param options
|
||||||
|
* @returns `IGlueJarState`
|
||||||
|
*/
|
||||||
|
export function useGlueJar<T extends HTMLElement>(
|
||||||
|
{ ref, ...options }: Partial<GlueJarOptions<T>> = {
|
||||||
|
acceptedFiles: DEFAULT_ACCEPTED_FILES,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const [state, pasteHandler] = usePasteHandler(options.acceptedFiles);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref && ref.current) {
|
||||||
|
ref.current.addEventListener("paste", pasteHandler);
|
||||||
|
} else {
|
||||||
|
document.addEventListener("paste", pasteHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (ref && ref.current) {
|
||||||
|
ref.current.removeEventListener("paste", pasteHandler);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener("paste", pasteHandler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [ref, options]);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
|
@ -0,0 +1,344 @@
|
||||||
|
import { Options } from 'jsqr';
|
||||||
|
import jsQR from 'jsqr';
|
||||||
|
|
||||||
|
const videoSize = {
|
||||||
|
width: { min: 360, ideal: 720, max: 1080 },
|
||||||
|
height: { min: 360, ideal: 720, max: 1080 },
|
||||||
|
};
|
||||||
|
|
||||||
|
class QrcodeDecoder {
|
||||||
|
timerCapture: null | NodeJS.Timeout;
|
||||||
|
canvasElem: null | HTMLCanvasElement;
|
||||||
|
gCtx: null | CanvasRenderingContext2D;
|
||||||
|
stream: null | MediaStream;
|
||||||
|
videoElem: null | HTMLVideoElement;
|
||||||
|
getUserMediaHandler: null;
|
||||||
|
videoConstraints: MediaStreamConstraints;
|
||||||
|
defaultOption: Options;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.timerCapture = null;
|
||||||
|
this.canvasElem = null;
|
||||||
|
this.gCtx = null;
|
||||||
|
this.stream = null;
|
||||||
|
this.videoElem = null;
|
||||||
|
this.getUserMediaHandler = null;
|
||||||
|
this.videoConstraints = {
|
||||||
|
// default use rear camera
|
||||||
|
video: { ...videoSize, facingMode: { exact: 'environment' } },
|
||||||
|
audio: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.defaultOption = { inversionAttempts: 'attemptBoth' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies if canvas element is supported.
|
||||||
|
*/
|
||||||
|
isCanvasSupported() {
|
||||||
|
const elem = document.createElement('canvas');
|
||||||
|
return !!(elem.getContext && elem.getContext('2d'));
|
||||||
|
}
|
||||||
|
|
||||||
|
_createImageData(target: CanvasImageSource, width: number, height: number) {
|
||||||
|
if (!this.canvasElem) {
|
||||||
|
this._prepareCanvas(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gCtx!.clearRect(0, 0, width, height);
|
||||||
|
this.gCtx!.drawImage(target, 0, 0, width, height);
|
||||||
|
|
||||||
|
const imageData = this.gCtx!.getImageData(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
this.canvasElem!.width,
|
||||||
|
this.canvasElem!.height,
|
||||||
|
);
|
||||||
|
|
||||||
|
return imageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares the canvas element (which will
|
||||||
|
* receive the image from the camera and provide
|
||||||
|
* what the algorithm needs for checking for a
|
||||||
|
* QRCode and then decoding it.)
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {DOMElement} canvasElem the canvas
|
||||||
|
* element
|
||||||
|
* @param {number} width The width that
|
||||||
|
* the canvas element
|
||||||
|
* should have
|
||||||
|
* @param {number} height The height that
|
||||||
|
* the canvas element
|
||||||
|
* should have
|
||||||
|
* @return {DOMElement} the canvas
|
||||||
|
* after the resize if width and height
|
||||||
|
* provided.
|
||||||
|
*/
|
||||||
|
_prepareCanvas(width: number, height: number) {
|
||||||
|
if (!this.canvasElem) {
|
||||||
|
this.canvasElem = document.createElement('canvas');
|
||||||
|
this.canvasElem.style.width = `${width}px`;
|
||||||
|
this.canvasElem.style.height = `${height}px`;
|
||||||
|
this.canvasElem.width = width;
|
||||||
|
this.canvasElem.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gCtx = this.canvasElem.getContext('2d');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on the video dimensions and the canvas
|
||||||
|
* that was previously generated captures the
|
||||||
|
* video/image source and then paints into the
|
||||||
|
* canvas so that the decoder is able to work as
|
||||||
|
* it expects.
|
||||||
|
* @param {DOMElement} videoElem <video> dom element
|
||||||
|
* @param {Object} options options (optional) - Additional options.
|
||||||
|
* inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst)
|
||||||
|
* refer to jsqr options: https://github.com/cozmo/jsQR
|
||||||
|
*/
|
||||||
|
async _captureToCanvas(videoElem: HTMLVideoElement, options: Options) {
|
||||||
|
if (this.timerCapture) {
|
||||||
|
clearTimeout(this.timerCapture);
|
||||||
|
}
|
||||||
|
const proms = () => {
|
||||||
|
const p = new Promise(async (resolve) => {
|
||||||
|
let code;
|
||||||
|
if (videoElem.videoWidth && videoElem.videoHeight) {
|
||||||
|
const imageData = this._createImageData(
|
||||||
|
videoElem,
|
||||||
|
videoElem.videoWidth,
|
||||||
|
videoElem.videoHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
code = jsQR(
|
||||||
|
imageData.data,
|
||||||
|
imageData.width,
|
||||||
|
imageData.height,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
resolve(code);
|
||||||
|
} else {
|
||||||
|
this.timerCapture = setTimeout(async () => {
|
||||||
|
code = await this._captureToCanvas(videoElem, options);
|
||||||
|
resolve(code);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.timerCapture = setTimeout(async () => {
|
||||||
|
code = await this._captureToCanvas(videoElem, options);
|
||||||
|
resolve(code);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await proms();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares the video element for receiving
|
||||||
|
* camera's input. Releases a stream if there
|
||||||
|
* was any (resets).
|
||||||
|
*
|
||||||
|
* @param {DOMElement} videoElem <video> dom element
|
||||||
|
* @param {Object} options options (optional) - Additional options.
|
||||||
|
* inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst)
|
||||||
|
* refer to jsqr options: https://github.com/cozmo/jsQR
|
||||||
|
*/
|
||||||
|
async decodeFromCamera(videoElem: HTMLVideoElement, options: any = {}) {
|
||||||
|
const opts = {
|
||||||
|
...this.defaultOption,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.stop();
|
||||||
|
if (!navigator.mediaDevices.getUserMedia) {
|
||||||
|
throw new Error("Couldn't get video from camera");
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream: MediaStream;
|
||||||
|
try {
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia(this.videoConstraints);
|
||||||
|
} catch (e) {
|
||||||
|
if ((e as OverconstrainedError).name === 'OverconstrainedError') {
|
||||||
|
console.log('[OverconstrainedError] Can not use rear camera.');
|
||||||
|
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: {
|
||||||
|
...videoSize,
|
||||||
|
...{
|
||||||
|
width: opts.width,
|
||||||
|
height: opts.height,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
audio: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream) {
|
||||||
|
videoElem.srcObject = stream;
|
||||||
|
// videoElem.src = window.URL.createObjectURL(stream);
|
||||||
|
this.videoElem = videoElem;
|
||||||
|
this.stream = stream;
|
||||||
|
|
||||||
|
const code = await this.decodeFromVideo(videoElem, opts);
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares the video element for video file.
|
||||||
|
*
|
||||||
|
* @param {DOMElement} videoElem <video> dom element
|
||||||
|
* @param {Object} options options (optional) - Additional options.
|
||||||
|
* inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst)
|
||||||
|
* refer to jsqr options: https://github.com/cozmo/jsQR
|
||||||
|
*/
|
||||||
|
async decodeFromVideo(videoElem: HTMLVideoElement, options = {}) {
|
||||||
|
const opts = {
|
||||||
|
...this.defaultOption,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
this.videoElem = videoElem;
|
||||||
|
const code = await this._captureToCanvas(videoElem, opts);
|
||||||
|
return code;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes an image from its src.
|
||||||
|
* @param {DOMElement} imageElem
|
||||||
|
* @param {Object} options options (optional) - Additional options.
|
||||||
|
* inversionAttempts - (attemptBoth (default), dontInvert, onlyInvert, or invertFirst)
|
||||||
|
* refer to jsqr options: https://github.com/cozmo/jsQR
|
||||||
|
*/
|
||||||
|
async decodeFromImage(
|
||||||
|
img: HTMLImageElement | string,
|
||||||
|
options: { crossOrigin?: string } = {},
|
||||||
|
) {
|
||||||
|
let imgDom: HTMLImageElement | null = null;
|
||||||
|
const opts = {
|
||||||
|
...this.defaultOption,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof img === 'string') {
|
||||||
|
imgDom = document.createElement('img');
|
||||||
|
if (options.crossOrigin) {
|
||||||
|
imgDom.crossOrigin = options.crossOrigin;
|
||||||
|
}
|
||||||
|
imgDom.src = img;
|
||||||
|
const proms = () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
imgDom!.onload = () => resolve(true);
|
||||||
|
});
|
||||||
|
await proms();
|
||||||
|
} else if (+img.nodeType > 0) {
|
||||||
|
if (!img.src) {
|
||||||
|
throw new Error('The ImageElement must contain a src');
|
||||||
|
}
|
||||||
|
imgDom = img;
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = null;
|
||||||
|
if (imgDom) {
|
||||||
|
code = this._decodeFromImageElm(imgDom, opts);
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
_decodeFromImageElm(imgObj: HTMLImageElement, options = {}) {
|
||||||
|
const opts: Options = {
|
||||||
|
...this.defaultOption,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
const imageData = this._createImageData(
|
||||||
|
imgObj,
|
||||||
|
imgObj.width,
|
||||||
|
imgObj.height,
|
||||||
|
);
|
||||||
|
|
||||||
|
const code = jsQR(imageData.data, imageData.width, imageData.height, opts);
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases a video stream that was being
|
||||||
|
* captured by prepareToVideo
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
if (this.stream) {
|
||||||
|
const track = this.stream.getTracks()[0];
|
||||||
|
track.stop();
|
||||||
|
this.stream = null;
|
||||||
|
|
||||||
|
// fix: clear black bg after camera capture
|
||||||
|
this.videoElem!.srcObject = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.timerCapture) {
|
||||||
|
clearTimeout(this.timerCapture);
|
||||||
|
this.timerCapture = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the sourceId for the camera to use.
|
||||||
|
*/
|
||||||
|
setGroupId(groupId: string) {
|
||||||
|
if (groupId) {
|
||||||
|
this.videoConstraints.video = {
|
||||||
|
advanced: [{ groupId }],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.videoConstraints.video = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of all available video sources on
|
||||||
|
* the current device.
|
||||||
|
*/
|
||||||
|
async getVideoDevices() {
|
||||||
|
if (navigator.mediaDevices.enumerateDevices) {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
return devices.filter((item: MediaDeviceInfo) => {
|
||||||
|
if (item.kind === 'videoinput') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'Current browser doest not support MediaStreamTrack.getSources',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QrcodeDecoder;
|
|
@ -0,0 +1,6 @@
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "qr-decoder",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build && next export",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@next/font": "13.1.6",
|
||||||
|
"@types/node": "18.13.0",
|
||||||
|
"@types/react": "18.0.28",
|
||||||
|
"@types/react-dom": "18.0.10",
|
||||||
|
"jsqr": "^1.4.0",
|
||||||
|
"next": "13.1.6",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"typescript": "4.9.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "8.34.0",
|
||||||
|
"eslint-config-next": "13.1.6"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import '@/styles/globals.css'
|
||||||
|
import type { AppProps } from 'next/app'
|
||||||
|
|
||||||
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
|
return <Component {...pageProps} />
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Html, Head, Main, NextScript } from 'next/document'
|
||||||
|
|
||||||
|
export default function Document() {
|
||||||
|
return (
|
||||||
|
<Html lang="en">
|
||||||
|
<Head />
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import Head from 'next/head'
|
||||||
|
|
||||||
|
import { useGlueJar } from "../../lib/gluejar";
|
||||||
|
import QrcodeDecoder from '../../lib/qr';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { QRCode } from 'jsqr';
|
||||||
|
|
||||||
|
function QRImage({ image }: { image: string }) {
|
||||||
|
const [data, setData] = useState<QRCode>();
|
||||||
|
const [errored, setErrored] = useState(false);
|
||||||
|
|
||||||
|
const qr = new QrcodeDecoder();
|
||||||
|
qr.decodeFromImage(image).then(data => {
|
||||||
|
if (data != null && typeof data === "object") {
|
||||||
|
setData(data)
|
||||||
|
} else {
|
||||||
|
setErrored(true);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
setData(undefined);
|
||||||
|
setErrored(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<article>
|
||||||
|
<img src={image} alt={"Pasted image: " + image} />
|
||||||
|
{errored
|
||||||
|
? <strong>There was an error decoding this QR code.</strong>
|
||||||
|
: data === undefined
|
||||||
|
? <p>Parsing...</p>
|
||||||
|
: <code>{data.data}</code>}
|
||||||
|
</article>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const { pasted, error } = useGlueJar();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="app">
|
||||||
|
<Head>
|
||||||
|
<title>QR Decoder</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
</Head>
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h1>QR Decoder</h1>
|
||||||
|
<p>{"Paste an image :)"}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{error !== null && <span>{error}</span>}
|
||||||
|
<div>
|
||||||
|
{pasted.length > 0 &&
|
||||||
|
pasted.map((image) => (
|
||||||
|
<QRImage image={image} key={image} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
A web tool by <a href="https://som.codes">Charlotte Som</a>.
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
:root {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
article {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2em;
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.85em;
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
Loading…
Reference in New Issue