├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── _redirects ├── app ├── CNAME ├── css │ └── styles.css ├── decoder.js ├── images │ ├── photo-camera.svg │ ├── qrcode-scanner.svg │ └── touch │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.jpg │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ └── mstile-150x150.png ├── index.html ├── js │ ├── main.js │ ├── snackbar.js │ └── vendor │ │ └── qrscan.js └── manifest.json ├── logo.png ├── package.json ├── robots.txt └── webpack.config.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | node_modules 6 | .DS_Store 7 | dist 8 | .env 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.json 2 | /dist 3 | app/decoder.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "parser": "babylon" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 code-kotis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # For more active development/deployments, code is moved to [https://github.com/gokulkrishh/qrcodescan.in](https://github.com/gokulkrishh/qrcodescan.in) 3 | 4 | ###

5 | 6 | # [QR Code Scanner](https://qrcodescan.in) 7 | 8 | *QR Code Scanner - a simple, fast and useful progressive web application* 9 | 10 | ### [Live](https://qrcodescan.in) 11 | 12 | ## Features 13 | 14 | - App Shell 15 | - Offline 16 | - Secure via https 17 | - Responsive 18 | - Add to home screen & Splash screen 19 | - Supported Browser (Mobile & Desktop) - Google Chrome, Firefox, Safari, Opera, Microsoft Edge and now supports iOS as well. 20 | 21 | ## Installation 22 | 23 | 1. Clone this repo 24 | 25 | ```bash 26 | git clone https://github.com/code-kotis/qr-code-scanner 27 | ``` 28 | 29 | 2. Installation 30 | 31 | ```bash 32 | npm install 33 | ``` 34 | 35 | 3. Run 36 | 37 | ```bash 38 | npm run start 39 | ``` 40 | 41 | 4. Build 42 | 43 | ```bash 44 | npm run build 45 | ``` 46 | 47 | ### Contributions 48 | 49 | If you find a bug, please file an issue. PR's are most welcome ;) 50 | 51 | #### MIT Licensed 52 | -------------------------------------------------------------------------------- /_redirects: -------------------------------------------------------------------------------- 1 | # Redirect default Netlify subdomain to primary domain 2 | http://qrcodescan.netlify.com/* https://www.qrcodescan.in/:splat 301! 3 | https://qrcodescan/* https://www.qrcodescan/:splat 301! -------------------------------------------------------------------------------- /app/CNAME: -------------------------------------------------------------------------------- 1 | qrcodescan.in 2 | -------------------------------------------------------------------------------- /app/css/styles.css: -------------------------------------------------------------------------------- 1 | /*! minireset.css v0.0.2 | MIT License | github.com/jgthms/minireset.css */ 2 | html,body,p,ol,ul,li,dl,dt,dd,blockquote,figure,fieldset,legend,textarea,pre,iframe,hr,h1,h2,h3,h4,h5,h6{margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}ul{list-style:none}button,input,select,textarea{margin:0}html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}img,embed,object,audio{height:auto;max-width:100%}iframe{border:0}table{border-collapse:collapse;border-spacing:0}td,th{padding:0;text-align:left} 3 | 4 | body { 5 | font-family: Roboto, Helvetica,Arial,sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | } 8 | 9 | .app__layout { 10 | position: absolute; 11 | width: 100%; 12 | height: 100%; 13 | overflow: hidden; 14 | background-color: rgba(0, 0, 0, 0.5); 15 | } 16 | 17 | .app__header { 18 | width: 100%; 19 | height: 56px; 20 | color: #fff; 21 | display: flex; 22 | -webkit-box-align: center; 23 | -ms-flex-align: center; 24 | align-items: center; 25 | position: fixed; 26 | top: 0; 27 | left: 0; 28 | right: 0; 29 | z-index: 10; 30 | } 31 | 32 | .app__header-icon { 33 | width: 35px; 34 | height: 35px; 35 | display: -webkit-box; 36 | display: -ms-flexbox; 37 | display: flex; 38 | -webkit-box-align: center; 39 | -ms-flex-align: center; 40 | align-items: center; 41 | -webkit-box-pack: center; 42 | -ms-flex-pack: center; 43 | justify-content: center; 44 | cursor: pointer; 45 | position: absolute; 46 | right: 20px; 47 | top: 20px; 48 | } 49 | 50 | .app__header-icon:active { 51 | opacity: 0.8; 52 | } 53 | 54 | .app__header-title { 55 | margin-left: 5px; 56 | font-size: 19px; 57 | user-select: none; 58 | } 59 | 60 | .app__layout-content { 61 | height: inherit; 62 | /*margin-top: 56px;*/ 63 | } 64 | 65 | .custom-menu-icon { 66 | font-size: 28px; 67 | line-height: 47px; 68 | } 69 | 70 | .custom-title, 71 | .custom-menu-icon { 72 | color: #fff; 73 | } 74 | 75 | .custom-btn { 76 | position: fixed; 77 | right: 26px; 78 | bottom: 26px; 79 | background: #448aff; 80 | border-radius: 50%; 81 | border: none; 82 | width: 56px; 83 | height: 56px; 84 | outline: none; 85 | box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12); 86 | z-index: 9999; 87 | } 88 | 89 | .custom-btn:active { 90 | box-shadow: none; 91 | } 92 | 93 | .custom-msg { 94 | text-align: center; 95 | width: 90%; 96 | height: 50%; 97 | overflow: auto; 98 | margin: auto; 99 | position: absolute; 100 | top: 0; 101 | left: 0; 102 | bottom: 0; 103 | right: 0; 104 | font-size: 16px; 105 | } 106 | 107 | .custom-fab-icon { 108 | color: #fff; 109 | font-size: 30px; 110 | margin-top: 2px; 111 | user-select: none; 112 | } 113 | 114 | video { 115 | transform: translateX(-50%) translateY(-50%); 116 | top: 50%; 117 | left: 50%; 118 | min-width: 100%; 119 | min-height: 100%; 120 | width: auto; 121 | height: auto; 122 | position: absolute; 123 | } 124 | 125 | #list li { 126 | list-style-type: none; 127 | text-decoration: underline; 128 | color: #00F; 129 | } 130 | 131 | .custom-copy-btn { 132 | opacity: 0; 133 | } 134 | 135 | .hide { 136 | display: none; 137 | } 138 | 139 | @-webkit-keyframes scanner { 140 | 0% { 141 | bottom: 100%; 142 | } 143 | 50% { 144 | bottom: 0%; 145 | } 146 | 100% { 147 | bottom: 100%; 148 | } 149 | } 150 | 151 | @-moz-keyframes scanner { 152 | 0% { 153 | bottom: 100%; 154 | } 155 | 50% { 156 | bottom: 0%; 157 | } 158 | 100% { 159 | bottom: 100%; 160 | } 161 | } 162 | 163 | @-o-keyframes scanner { 164 | 0% { 165 | bottom: 100%; 166 | } 167 | 50% { 168 | bottom: 0%; 169 | } 170 | 100% { 171 | bottom: 100%; 172 | } 173 | } 174 | 175 | @keyframes scanner { 176 | 0% { 177 | bottom: 100%; 178 | } 179 | 50% { 180 | bottom: 0%; 181 | } 182 | 100% { 183 | bottom: 100%; 184 | } 185 | } 186 | 187 | .custom-scanner { 188 | width: 100%; 189 | height: 2px; 190 | background: #4CAF50; 191 | position: absolute; 192 | -webkit-transition: all 200ms linear; 193 | -moz-transition: all 200ms linear; 194 | transition: all 200ms linear; 195 | -webkit-animation: scanner 3s infinite linear; 196 | -moz-animation: scanner 3s infinite linear; 197 | -o-animation: scanner 3s infinite linear; 198 | animation: scanner 3s infinite linear; 199 | box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.4); 200 | display: none; 201 | } 202 | 203 | #camera { 204 | opacity: 0; 205 | } 206 | 207 | .no-support { 208 | font-size: 20px; 209 | text-align: center; 210 | } 211 | 212 | .app__snackbar { 213 | position: fixed; 214 | bottom: 15px; 215 | left: 20px; 216 | pointer-events: none; 217 | z-index: 9999; 218 | } 219 | 220 | .app__snackbar-msg { 221 | width: 250px; 222 | min-height: 50px; 223 | background-color: #404040; 224 | color: #fff; 225 | border-radius: 3px; 226 | box-shadow: 0 0 2px rgba(0,0,0,.12), 0 2px 4px rgba(0,0,0,.24); 227 | display: -webkit-box; 228 | display: -ms-flexbox; 229 | display: flex; 230 | -webkit-box-align: center; 231 | -ms-flex-align: center; 232 | align-items: center; 233 | -webkit-box-pack: justify; 234 | -ms-flex-pack: justify; 235 | justify-content: space-between; 236 | font-size: 14px; 237 | font-weight: 500; 238 | padding-left: 15px; 239 | padding-right: 10px; 240 | word-break: break-all; 241 | -webkit-transition: opacity 3s cubic-bezier(0, 0, 0.30, 1) 0; 242 | transition: opacity 0.30s cubic-bezier(0, 0, 0.30, 1) 0; 243 | text-transform: initial; 244 | margin-bottom: 10px; 245 | z-index: 9999; 246 | } 247 | 248 | .app__snackbar--hide { 249 | opacity: 0; 250 | } 251 | 252 | .app__dialog { 253 | z-index: 12; 254 | background-color: #fff; 255 | width: 290px; 256 | height: 180px; 257 | border-radius: 2px; 258 | display: flex; 259 | position: absolute; 260 | left: 0; 261 | right: 0; 262 | bottom: 0; 263 | top: 0; 264 | margin: auto; 265 | box-shadow: 0 9px 46px 8px rgba(0,0,0,.14), 0 11px 15px -7px rgba(0,0,0,.12), 0 24px 38px 3px rgba(0,0,0,.2); 266 | } 267 | 268 | .app__dialog h5 { 269 | margin-top: 20px; 270 | margin-left: 18px; 271 | font-weight: 500; 272 | } 273 | 274 | .app__dialog input { 275 | width: 250px; 276 | margin: 20px; 277 | height: 30px; 278 | border: none; 279 | border-bottom: 1px solid rgba(0,0,0,.12); 280 | outline: none; 281 | font-size: 15px; 282 | margin-top: 25px; 283 | color: rgba(0,0,0,.54); 284 | font-weight: 500; 285 | } 286 | 287 | .app__dialog-actions { 288 | display: block; 289 | position: absolute; 290 | bottom: 13px; 291 | right: 20px; 292 | } 293 | 294 | .app__dialog-open, 295 | .app__dialog-close { 296 | border: 0; 297 | height: 35px; 298 | width: 70px; 299 | font-size: 16px; 300 | background: transparent; 301 | font-weight: 500; 302 | outline: none; 303 | cursor: pointer; 304 | } 305 | 306 | .app__dialog-open { 307 | display: none; 308 | } 309 | 310 | .app__dialog-open:active, 311 | .app__dialog-close:active { 312 | opacity: 0.9; 313 | } 314 | 315 | .app__dialog--hide { 316 | display: none; 317 | } 318 | 319 | .app__overlay { 320 | position: fixed; 321 | top: 0; 322 | bottom: 0; 323 | right: 0; 324 | left: 0; 325 | transition: all 200ms ease-in; 326 | width: 320px; 327 | height: 320px; 328 | margin: auto; 329 | } 330 | 331 | 332 | .app__overlay-left, 333 | .app__overlay-right { 334 | width: 52px; 335 | height: 340px; 336 | background: #7f7f7f; 337 | } 338 | 339 | .app__overlay-left { 340 | margin-left: -57px; 341 | margin-top: -10px; 342 | } 343 | 344 | .app__overlay-right { 345 | margin-right: -57px; 346 | margin-top: -340px; 347 | float: right; 348 | } 349 | 350 | .app__overlay { 351 | border: 0; 352 | } 353 | 354 | .app__help-text, 355 | .app__select-photos { 356 | color: #fff; 357 | position: absolute; 358 | bottom: -70px; 359 | font-size: 18px; 360 | right: 0; 361 | text-align: center; 362 | user-select: none; 363 | } 364 | 365 | .app__help-text { 366 | display: none; 367 | left: 0; 368 | } 369 | 370 | .app__dialog-overlay { 371 | position: fixed; 372 | left: 0; 373 | right: 0; 374 | bottom: 0; 375 | top: 0; 376 | background: rgba(0, 0, 0, 0.55); 377 | z-index: 11; 378 | } 379 | 380 | .camera__icon, 381 | .focus__icon { 382 | position: relative; 383 | left: 10px; 384 | display: none; 385 | } 386 | 387 | .app__select-photos { 388 | width: 58px; 389 | height: 58px; 390 | cursor: pointer; 391 | position: fixed; 392 | bottom: 20px; 393 | right: 20px; 394 | border-radius: 50%; 395 | background-color: #3F51B5; 396 | background-image: url("/images/photo-camera.svg"); 397 | background-repeat: no-repeat; 398 | background-size: 26px; 399 | background-position: 16px 15px; 400 | } 401 | 402 | .app__select-photos:active { 403 | opacity: 0.8; 404 | } 405 | 406 | input[type='file'] { 407 | display: none; 408 | } 409 | 410 | #frame { 411 | width: auto; 412 | height: auto; 413 | } 414 | -------------------------------------------------------------------------------- /app/images/photo-camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/images/qrcode-scanner.svg: -------------------------------------------------------------------------------- 1 | 4 | 6 | 9 | 10 | 13 | 14 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/images/touch/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-kotis/qr-code-scanner/37f153b969fbd9b5d9e9aad5571efd133b0769ff/app/images/touch/android-chrome-192x192.png -------------------------------------------------------------------------------- /app/images/touch/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-kotis/qr-code-scanner/37f153b969fbd9b5d9e9aad5571efd133b0769ff/app/images/touch/android-chrome-512x512.png -------------------------------------------------------------------------------- /app/images/touch/apple-touch-icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-kotis/qr-code-scanner/37f153b969fbd9b5d9e9aad5571efd133b0769ff/app/images/touch/apple-touch-icon.jpg -------------------------------------------------------------------------------- /app/images/touch/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-kotis/qr-code-scanner/37f153b969fbd9b5d9e9aad5571efd133b0769ff/app/images/touch/favicon-16x16.png -------------------------------------------------------------------------------- /app/images/touch/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-kotis/qr-code-scanner/37f153b969fbd9b5d9e9aad5571efd133b0769ff/app/images/touch/favicon-32x32.png -------------------------------------------------------------------------------- /app/images/touch/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-kotis/qr-code-scanner/37f153b969fbd9b5d9e9aad5571efd133b0769ff/app/images/touch/favicon.ico -------------------------------------------------------------------------------- /app/images/touch/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-kotis/qr-code-scanner/37f153b969fbd9b5d9e9aad5571efd133b0769ff/app/images/touch/mstile-150x150.png -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QR Code Scanner 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 | 38 | 39 | 40 |
41 |
42 |
QR Code
43 | 44 |
45 |
46 | 47 | 48 |
49 |
50 | 51 |
52 | 53 | 54 |
55 |
56 |
57 |
58 |
59 | 60 |
61 |
62 |
63 | 64 |
65 | 66 | 77 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /app/js/main.js: -------------------------------------------------------------------------------- 1 | import QRReader from './vendor/qrscan.js'; 2 | import { snackbar } from './snackbar.js'; 3 | import styles from '../css/styles.css'; 4 | import isURL from 'is-url'; 5 | 6 | //If service worker is installed, show offline usage notification 7 | if ('serviceWorker' in navigator) { 8 | window.addEventListener('load', () => { 9 | navigator.serviceWorker 10 | .register('/service-worker.js') 11 | .then(reg => { 12 | console.log('SW registered: ', reg); 13 | if (!localStorage.getItem('offline')) { 14 | localStorage.setItem('offline', true); 15 | snackbar.show('App is ready for offline usage.', 5000); 16 | } 17 | }) 18 | .catch(regError => { 19 | console.log('SW registration failed: ', regError); 20 | }); 21 | }); 22 | } 23 | 24 | window.addEventListener('DOMContentLoaded', () => { 25 | //To check the device and add iOS support 26 | window.iOS = ['iPad', 'iPhone', 'iPod'].indexOf(navigator.platform) >= 0; 27 | window.isMediaStreamAPISupported = navigator && navigator.mediaDevices && 'enumerateDevices' in navigator.mediaDevices; 28 | window.noCameraPermission = false; 29 | 30 | var copiedText = null; 31 | var frame = null; 32 | var selectPhotoBtn = document.querySelector('.app__select-photos'); 33 | var dialogElement = document.querySelector('.app__dialog'); 34 | var dialogOverlayElement = document.querySelector('.app__dialog-overlay'); 35 | var dialogOpenBtnElement = document.querySelector('.app__dialog-open'); 36 | var dialogCloseBtnElement = document.querySelector('.app__dialog-close'); 37 | var scanningEle = document.querySelector('.custom-scanner'); 38 | var textBoxEle = document.querySelector('#result'); 39 | var helpTextEle = document.querySelector('.app__help-text'); 40 | var infoSvg = document.querySelector('.app__header-icon svg'); 41 | var videoElement = document.querySelector('video'); 42 | window.appOverlay = document.querySelector('.app__overlay'); 43 | 44 | //Initializing qr scanner 45 | window.addEventListener('load', event => { 46 | QRReader.init(); //To initialize QR Scanner 47 | // Set camera overlay size 48 | setTimeout(() => { 49 | setCameraOverlay(); 50 | if (window.isMediaStreamAPISupported) { 51 | scan(); 52 | } 53 | }, 1000); 54 | 55 | // To support other browsers who dont have mediaStreamAPI 56 | selectFromPhoto(); 57 | }); 58 | 59 | function setCameraOverlay() { 60 | window.appOverlay.style.borderStyle = 'solid'; 61 | } 62 | 63 | function createFrame() { 64 | frame = document.createElement('img'); 65 | frame.src = ''; 66 | frame.id = 'frame'; 67 | } 68 | 69 | //Dialog close btn event 70 | dialogCloseBtnElement.addEventListener('click', hideDialog, false); 71 | dialogOpenBtnElement.addEventListener('click', openInBrowser, false); 72 | 73 | //To open result in browser 74 | function openInBrowser() { 75 | console.log('Result: ', copiedText); 76 | window.open(copiedText, '_blank', 'toolbar=0,location=0,menubar=0'); 77 | copiedText = null; 78 | hideDialog(); 79 | } 80 | 81 | //Scan 82 | function scan(forSelectedPhotos = false) { 83 | if (window.isMediaStreamAPISupported && !window.noCameraPermission) { 84 | scanningEle.style.display = 'block'; 85 | } 86 | 87 | if (forSelectedPhotos) { 88 | scanningEle.style.display = 'block'; 89 | } 90 | 91 | QRReader.scan(result => { 92 | copiedText = result; 93 | textBoxEle.value = result; 94 | textBoxEle.select(); 95 | scanningEle.style.display = 'none'; 96 | if (isURL(result)) { 97 | dialogOpenBtnElement.style.display = 'inline-block'; 98 | } 99 | dialogElement.classList.remove('app__dialog--hide'); 100 | dialogOverlayElement.classList.remove('app__dialog--hide'); 101 | const frame = document.querySelector('#frame'); 102 | // if (forSelectedPhotos && frame) frame.remove(); 103 | }, forSelectedPhotos); 104 | } 105 | 106 | //Hide dialog 107 | function hideDialog() { 108 | copiedText = null; 109 | textBoxEle.value = ''; 110 | 111 | if (!window.isMediaStreamAPISupported) { 112 | frame.src = ''; 113 | frame.className = ''; 114 | } 115 | 116 | dialogElement.classList.add('app__dialog--hide'); 117 | dialogOverlayElement.classList.add('app__dialog--hide'); 118 | scan(); 119 | } 120 | 121 | function selectFromPhoto() { 122 | //Creating the camera element 123 | var camera = document.createElement('input'); 124 | camera.setAttribute('type', 'file'); 125 | camera.setAttribute('capture', 'camera'); 126 | camera.id = 'camera'; 127 | window.appOverlay.style.borderStyle = ''; 128 | selectPhotoBtn.style.display = 'block'; 129 | createFrame(); 130 | 131 | //Add the camera and img element to DOM 132 | var pageContentElement = document.querySelector('.app__layout-content'); 133 | pageContentElement.appendChild(camera); 134 | pageContentElement.appendChild(frame); 135 | 136 | //Click of camera fab icon 137 | selectPhotoBtn.addEventListener('click', () => { 138 | scanningEle.style.display = 'none'; 139 | document.querySelector('#camera').click(); 140 | }); 141 | 142 | //On camera change 143 | camera.addEventListener('change', event => { 144 | if (event.target && event.target.files.length > 0) { 145 | frame.className = 'app__overlay'; 146 | frame.src = URL.createObjectURL(event.target.files[0]); 147 | if (!window.noCameraPermission) scanningEle.style.display = 'block'; 148 | window.appOverlay.style.borderColor = 'rgb(62, 78, 184)'; 149 | scan(true); 150 | } 151 | }); 152 | } 153 | }); 154 | -------------------------------------------------------------------------------- /app/js/snackbar.js: -------------------------------------------------------------------------------- 1 | var snackbar = {}; 2 | var snackBarElement = document.querySelector('.app__snackbar'); 3 | var snackbarMsg = null; 4 | 5 | //To show notification 6 | snackbar.show = (msg, options = 4000) => { 7 | if (!msg) return; 8 | 9 | if (snackbarMsg) { 10 | snackbarMsg.remove(); 11 | } 12 | 13 | snackbarMsg = document.createElement('div'); 14 | snackbarMsg.className = 'app__snackbar-msg'; 15 | snackbarMsg.textContent = msg; 16 | snackBarElement.appendChild(snackbarMsg); 17 | 18 | //Show toast for 3secs and hide it 19 | setTimeout(() => { 20 | snackbarMsg.remove(); 21 | }, options); 22 | }; 23 | 24 | exports.snackbar = snackbar; 25 | -------------------------------------------------------------------------------- /app/js/vendor/qrscan.js: -------------------------------------------------------------------------------- 1 | import { snackbar } from '../snackbar.js'; 2 | 3 | var QRReader = {}; 4 | 5 | QRReader.active = false; 6 | QRReader.webcam = null; 7 | QRReader.canvas = null; 8 | QRReader.ctx = null; 9 | QRReader.decoder = null; 10 | 11 | QRReader.setCanvas = () => { 12 | QRReader.canvas = document.createElement('canvas'); 13 | QRReader.ctx = QRReader.canvas.getContext('2d'); 14 | }; 15 | 16 | function setPhotoSourceToScan(forSelectedPhotos) { 17 | if (!forSelectedPhotos && window.isMediaStreamAPISupported) { 18 | QRReader.webcam = document.querySelector('video'); 19 | } else { 20 | QRReader.webcam = document.querySelector('img'); 21 | } 22 | } 23 | 24 | QRReader.init = () => { 25 | var baseurl = ''; 26 | var streaming = false; 27 | 28 | // Init Webcam + Canvas 29 | setPhotoSourceToScan(); 30 | 31 | QRReader.setCanvas(); 32 | QRReader.decoder = new Worker(baseurl + 'decoder.js'); 33 | 34 | if (window.isMediaStreamAPISupported) { 35 | // Resize webcam according to input 36 | QRReader.webcam.addEventListener( 37 | 'play', 38 | function(ev) { 39 | if (!streaming) { 40 | setCanvasProperties(); 41 | streaming = true; 42 | } 43 | }, 44 | false 45 | ); 46 | } else { 47 | setCanvasProperties(); 48 | } 49 | 50 | function setCanvasProperties() { 51 | QRReader.canvas.width = window.innerWidth; 52 | QRReader.canvas.height = window.innerHeight; 53 | } 54 | 55 | function startCapture(constraints) { 56 | navigator.mediaDevices 57 | .getUserMedia(constraints) 58 | .then(function(stream) { 59 | QRReader.webcam.srcObject = stream; 60 | QRReader.webcam.setAttribute('playsinline', true); 61 | QRReader.webcam.setAttribute('controls', true); 62 | setTimeout(() => { 63 | document.querySelector('video').removeAttribute('controls'); 64 | }); 65 | }) 66 | .catch(function(err) { 67 | console.log('Error occurred ', err); 68 | showErrorMsg(); 69 | }); 70 | } 71 | 72 | if (window.isMediaStreamAPISupported) { 73 | navigator.mediaDevices 74 | .enumerateDevices() 75 | .then(function(devices) { 76 | var device = devices.filter(function(device) { 77 | var deviceLabel = device.label.split(',')[1]; 78 | if (device.kind == 'videoinput') { 79 | return device; 80 | } 81 | }); 82 | 83 | var constraints; 84 | if (device.length > 1) { 85 | constraints = { 86 | video: { 87 | mandatory: { 88 | sourceId: device[device.length - 1].deviceId ? device[device.length - 1].deviceId : null 89 | } 90 | }, 91 | audio: false 92 | }; 93 | 94 | if (window.iOS) { 95 | constraints.video.facingMode = 'environment'; 96 | } 97 | startCapture(constraints); 98 | } else if (device.length) { 99 | constraints = { 100 | video: { 101 | mandatory: { 102 | sourceId: device[0].deviceId ? device[0].deviceId : null 103 | } 104 | }, 105 | audio: false 106 | }; 107 | 108 | if (window.iOS) { 109 | constraints.video.facingMode = 'environment'; 110 | } 111 | 112 | if (!constraints.video.mandatory.sourceId && !window.iOS) { 113 | startCapture({ video: true }); 114 | } else { 115 | startCapture(constraints); 116 | } 117 | } else { 118 | startCapture({ video: true }); 119 | } 120 | }) 121 | .catch(function(error) { 122 | showErrorMsg(); 123 | console.error('Error occurred : ', error); 124 | }); 125 | } 126 | 127 | function showErrorMsg() { 128 | window.noCameraPermission = true; 129 | document.querySelector('.custom-scanner').style.display = 'none'; 130 | snackbar.show('Unable to access the camera', 10000); 131 | } 132 | }; 133 | 134 | /** 135 | * \brief QRReader Scan Action 136 | * Call this to start scanning for QR codes. 137 | * 138 | * \param A function(scan_result) 139 | */ 140 | QRReader.scan = function(callback, forSelectedPhotos) { 141 | QRReader.active = true; 142 | QRReader.setCanvas(); 143 | function onDecoderMessage(event) { 144 | if (event.data.length > 0) { 145 | var qrid = event.data[0][2]; 146 | QRReader.active = false; 147 | callback(qrid); 148 | } 149 | setTimeout(newDecoderFrame, 0); 150 | } 151 | 152 | QRReader.decoder.onmessage = onDecoderMessage; 153 | 154 | setTimeout(() => { 155 | setPhotoSourceToScan(forSelectedPhotos); 156 | }); 157 | 158 | // Start QR-decoder 159 | function newDecoderFrame() { 160 | if (!QRReader.active) return; 161 | try { 162 | QRReader.ctx.drawImage(QRReader.webcam, 0, 0, QRReader.canvas.width, QRReader.canvas.height); 163 | var imgData = QRReader.ctx.getImageData(0, 0, QRReader.canvas.width, QRReader.canvas.height); 164 | 165 | if (imgData.data) { 166 | QRReader.decoder.postMessage(imgData); 167 | } 168 | } catch (e) { 169 | // Try-Catch to circumvent Firefox Bug #879717 170 | if (e.name == 'NS_ERROR_NOT_AVAILABLE') setTimeout(newDecoderFrame, 0); 171 | } 172 | } 173 | newDecoderFrame(); 174 | }; 175 | 176 | export default QRReader; 177 | -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "QR Scanner", 3 | "name": "QR Code Scanner", 4 | "description": "QR code scanner progressive web application", 5 | "display": "standalone", 6 | "orientation": "portrait", 7 | "icons": [ 8 | { 9 | "src": "/images/touch/android-chrome-192x192.png", 10 | "sizes": "192x192", 11 | "type": "image/png" 12 | }, 13 | { 14 | "src": "/images/touch/android-chrome-512x512.png", 15 | "sizes": "512x512", 16 | "type": "image/png" 17 | }], 18 | "start_url": "/index.html?utm_source=homescreen", 19 | "theme_color": "#e4e4e4", 20 | "background_color": "#fff" 21 | } 22 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-kotis/qr-code-scanner/37f153b969fbd9b5d9e9aad5571efd133b0769ff/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qr-code-scanner", 3 | "description": "QR Code Scanner is the fastest and most user-friendly web application.", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "webpack-dev-server --hot --inline --open --mode=development", 7 | "build": "cross-env NODE_ENV=production webpack-cli --mode=production --config webpack.config.js", 8 | "stats": "cross-env NODE_ENV=production webpack-cli --mode=production --profile --json > stats.json", 9 | "precommit": "lint-staged", 10 | "pretty": "prettier --write 'app/**/*.js'" 11 | }, 12 | "lint-staged": { 13 | "*.{js,css}": [ 14 | "npm run pretty", 15 | "git add" 16 | ] 17 | }, 18 | "engines": { 19 | "node": ">=4.1.1" 20 | }, 21 | "dependencies": { 22 | "is-url": "^1.2.4" 23 | }, 24 | "devDependencies": { 25 | "clean-webpack-plugin": "^0.1.19", 26 | "copy-webpack-plugin": "^4.5.3", 27 | "cross-env": "^5.2.0", 28 | "css-loader": "^1.0.0", 29 | "extract-text-webpack-plugin": "4.0.0-beta.0", 30 | "file-loader": "^2.0.0", 31 | "html-webpack-plugin": "^3.2.0", 32 | "husky": "^1.1.2", 33 | "lint-staged": "^7.3.0", 34 | "mini-css-extract-plugin": "^0.4.4", 35 | "optimize-css-assets-webpack-plugin": "^5.0.1", 36 | "prettier": "^1.14.3", 37 | "sitemap-webpack-plugin": "^0.8.0", 38 | "style-loader": "^0.23.1", 39 | "webpack": "^4.20.2", 40 | "webpack-cli": "^3.1.2", 41 | "webpack-dev-server": "^3.1.9", 42 | "workbox-webpack-plugin": "^3.6.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const WorkboxPlugin = require('workbox-webpack-plugin'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 8 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 9 | const SitemapPlugin = require('sitemap-webpack-plugin').default; 10 | 11 | module.exports = { 12 | entry: './app/js/main.js', 13 | output: { 14 | path: path.resolve(__dirname, 'dist'), 15 | filename: '[name].[hash].bundle.js' 16 | }, 17 | devServer: { 18 | contentBase: __dirname + '/app' 19 | }, 20 | optimization: {}, 21 | plugins: [ 22 | new CleanWebpackPlugin(['dist']), 23 | new MiniCssExtractPlugin({ 24 | filename: '[name].css', 25 | chunkFilename: '[id].css' 26 | }), 27 | new WorkboxPlugin.GenerateSW({ 28 | clientsClaim: true, 29 | skipWaiting: true, 30 | runtimeCaching: [{ urlPattern: new RegExp('/'), handler: 'staleWhileRevalidate' }] 31 | }), 32 | new HtmlWebpackPlugin({ 33 | template: './app/index.html', 34 | minify: { 35 | collapseWhitespace: true 36 | } 37 | }), 38 | new ExtractTextPlugin({ 39 | filename: 'styles.css' 40 | }), 41 | new OptimizeCssAssetsPlugin({ 42 | cssProcessorPluginOptions: { 43 | preset: ['default', { discardComments: { removeAll: true } }] 44 | } 45 | }), 46 | new CopyWebpackPlugin([{ from: 'images/', to: 'images' }, 'decoder.js', 'manifest.json', 'CNAME'], { 47 | context: './app' 48 | }), 49 | new SitemapPlugin('https://qrcodescan.in', ['/']) 50 | ], 51 | module: { 52 | rules: [ 53 | { 54 | test: /\.css$/, 55 | use: ExtractTextPlugin.extract({ 56 | use: 'css-loader?importLoaders=1', 57 | fallback: 'style-loader' 58 | }) 59 | }, 60 | { 61 | test: /.*\.(gif|png|jpe?g|svg)$/i, 62 | use: ['file-loader'] 63 | } 64 | ] 65 | } 66 | }; 67 | --------------------------------------------------------------------------------