Initial commit: any%
This commit is contained in:
		
						commit
						c938303a07
					
				
					 13 changed files with 4313 additions and 0 deletions
				
			
		
							
								
								
									
										3
									
								
								.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | { | ||||||
|  |   "extends": "next/core-web-vitals" | ||||||
|  | } | ||||||
							
								
								
									
										36
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
							
								
								
									
										11
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
|  | ``` | ||||||
							
								
								
									
										155
									
								
								lib/gluejar.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								lib/gluejar.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||||
|  | } | ||||||
							
								
								
									
										344
									
								
								lib/qr.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										344
									
								
								lib/qr.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||||
							
								
								
									
										6
									
								
								next.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								next.config.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | /** @type {import('next').NextConfig} */ | ||||||
|  | const nextConfig = { | ||||||
|  |   reactStrictMode: true, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = nextConfig | ||||||
							
								
								
									
										3569
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3569
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										26
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							|  | @ -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" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										6
									
								
								src/pages/_app.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/pages/_app.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | import '@/styles/globals.css' | ||||||
|  | import type { AppProps } from 'next/app' | ||||||
|  | 
 | ||||||
|  | export default function App({ Component, pageProps }: AppProps) { | ||||||
|  |   return <Component {...pageProps} /> | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								src/pages/_document.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/pages/_document.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||||
|  |   ) | ||||||
|  | } | ||||||
							
								
								
									
										67
									
								
								src/pages/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/pages/index.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||||
|  |     </> | ||||||
|  |   ) | ||||||
|  | } | ||||||
							
								
								
									
										53
									
								
								src/styles/globals.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/styles/globals.css
									
									
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 a new issue