Skip to content

Commit f622290

Browse files
authored
Qrcode reader (#5)
* QRCode reader * 0.0.2
1 parent ee271da commit f622290

File tree

9 files changed

+144
-11
lines changed

9 files changed

+144
-11
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- [x] Markdown to HTML Converter
1111
- [x] HTML Preview
1212
- [x] QRCode Generator
13+
- [x] QRCode Reader
1314
- [x] Base64 Encode/Decode
1415
- [x] Text Diff
1516
- [x] JSON Formatter

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@
172172
"@types/jest": "^26.0.15",
173173
"@types/marked": "^2.0.4",
174174
"@types/node": "14.14.10",
175+
"@types/pngjs": "^6.0.1",
175176
"@types/qrcode": "^1.4.1",
176177
"@types/react": "^16.9.44",
177178
"@types/react-dom": "^16.9.9",
@@ -258,7 +259,9 @@
258259
"electron-log": "^4.2.4",
259260
"electron-updater": "^4.3.4",
260261
"history": "^5.0.0",
262+
"jsqr": "^1.4.0",
261263
"marked": "^2.1.3",
264+
"pngjs": "^6.0.0",
262265
"qrcode": "^1.4.4",
263266
"react": "^17.0.1",
264267
"react-dom": "^17.0.1",

src/components/Main.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Base64 from './base64/Base64';
1111
import DiffText from './diff/TextDiff';
1212
import SqlFormatter from './sql/SqlFormatter';
1313
import JsonFormatter from './json/JsonFormatter';
14+
import QRCodeReader from './qrcode/QrCodeReader';
1415

1516
const Main = () => {
1617
const routes = [
@@ -38,6 +39,12 @@ const Main = () => {
3839
name: 'QRCode Generator',
3940
Component: QrCodeGenerator,
4041
},
42+
{
43+
icon: <FontAwesomeIcon icon="camera" />,
44+
path: '/qrcode-reader',
45+
name: 'QRCode Reader',
46+
Component: QRCodeReader,
47+
},
4148
{
4249
icon: <FontAwesomeIcon icon="code" />,
4350
path: '/base64-encoder',

src/components/qrcode/QrCodeGenerator.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { clipboard, ipcRenderer } from 'electron';
22
import React, { useState } from 'react';
33
import { useDebouncedEffect } from '../../helpers/effectHooks';
44

5-
const HtmlPreview = () => {
5+
const QRCodeGenerator = () => {
66
const [content, setContent] = useState('https://plainbelt.github.io');
77
const [qrCode, setQrCode] = useState();
88
const [opening, setOpening] = useState(false);
@@ -34,8 +34,9 @@ const HtmlPreview = () => {
3434
const handleSave = async () => {
3535
setSaving(true);
3636
await ipcRenderer.invoke('save-file', {
37-
content: qrCode,
37+
content: (qrCode || ',').split(',')[1],
3838
defaultPath: 'qrcode.png',
39+
encoding: 'base64',
3940
});
4041
setSaving(false);
4142
};
@@ -84,4 +85,4 @@ const HtmlPreview = () => {
8485
);
8586
};
8687

87-
export default HtmlPreview;
88+
export default QRCodeGenerator;
+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { clipboard, ipcRenderer, nativeImage } from 'electron';
2+
import jsQR from 'jsqr';
3+
import { PNG } from 'pngjs';
4+
import React, { useEffect, useState } from 'react';
5+
6+
const QRCodeReader = () => {
7+
const [image, setImage] = useState(nativeImage.createEmpty());
8+
const [content, setContent] = useState('');
9+
const [opening, setOpening] = useState(false);
10+
const [copied, setCopied] = useState(false);
11+
12+
const handleOpen = async () => {
13+
setOpening(true);
14+
const filters = [{ name: 'Images', extensions: ['jpg', 'jpeg', 'png'] }];
15+
const buff = await ipcRenderer.invoke('open-file', filters);
16+
setImage(nativeImage.createFromBuffer(buff));
17+
setOpening(false);
18+
};
19+
20+
const handleClipboard = () => {
21+
setImage(clipboard.readImage());
22+
};
23+
24+
const handleCopy = () => {
25+
setCopied(true);
26+
clipboard.write({ text: content });
27+
setTimeout(() => setCopied(false), 500);
28+
};
29+
30+
useEffect(() => {
31+
try {
32+
const qr = jsQR(
33+
Uint8ClampedArray.from(PNG.sync.read(image.toPNG()).data),
34+
image.getSize().width,
35+
image.getSize().height
36+
);
37+
setContent(qr?.data || 'No QRCode detected');
38+
} catch (e) {
39+
setContent('No QRCode detected');
40+
}
41+
}, [image]);
42+
43+
return (
44+
<div className="flex flex-col min-h-full">
45+
<div className="flex justify-between mb-1">
46+
<span className="flex space-x-2">
47+
<button type="button" className="btn" onClick={handleClipboard}>
48+
Clipboard
49+
</button>
50+
<button
51+
type="button"
52+
className="btn"
53+
onClick={handleOpen}
54+
disabled={opening}
55+
>
56+
Open...
57+
</button>
58+
</span>
59+
60+
<button
61+
type="button"
62+
className="btn"
63+
onClick={handleCopy}
64+
disabled={copied}
65+
>
66+
{copied ? 'Copied' : 'Copy'}
67+
</button>
68+
</div>
69+
<div className="flex flex-1 min-h-full space-x-2">
70+
<section className="flex items-center flex-1 max-w-full min-h-full p-4 prose bg-gray-100 rounded-md">
71+
{image && !image.isEmpty() && (
72+
<img src={image.toDataURL()} alt="QRCode" />
73+
)}
74+
</section>
75+
<textarea
76+
className="flex-1 min-h-full p-4 bg-white rounded-md"
77+
value={content}
78+
readOnly
79+
/>
80+
</div>
81+
</div>
82+
);
83+
};
84+
85+
export default QRCodeReader;

src/helpers/fontAwesome.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
faJsSquare,
77
} from '@fortawesome/free-brands-svg-icons';
88
import {
9+
faCamera,
910
faClock,
1011
faCode,
1112
faCopy,
@@ -24,5 +25,6 @@ library.add(
2425
faCode,
2526
faExchangeAlt,
2627
faDatabase,
27-
faJsSquare
28+
faJsSquare,
29+
faCamera
2830
);

src/main.dev.ts

+23-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111
import 'core-js/stable';
1212
import 'regenerator-runtime/runtime';
1313
import path from 'path';
14-
import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron';
14+
import {
15+
app,
16+
BrowserWindow,
17+
dialog,
18+
ipcMain,
19+
nativeImage,
20+
shell,
21+
} from 'electron';
1522
import { autoUpdater } from 'electron-updater';
1623
import log from 'electron-log';
1724
import { FileFilter, IpcMainInvokeEvent } from 'electron/main';
@@ -123,16 +130,24 @@ const createWindow = async () => {
123130
// use Buffer.from(buffer).toString()
124131
ipcMain.handle(
125132
'open-file',
126-
async (_event: IpcMainInvokeEvent, filters: FileFilter[]) => {
133+
async (
134+
_event: IpcMainInvokeEvent,
135+
filters: FileFilter[],
136+
type: 'path' | 'buffer'
137+
) => {
127138
const files = await dialog.showOpenDialog({
128139
properties: ['openFile'],
129140
filters,
130141
});
131142

132143
let content;
133144
if (files) {
134-
const buffer = await promisify(fs.readFile)(files.filePaths[0]);
135-
content = buffer;
145+
const fpath = files.filePaths[0];
146+
if (type === 'path') {
147+
content = fpath;
148+
} else {
149+
content = await promisify(fs.readFile)(fpath);
150+
}
136151
}
137152
return content;
138153
}
@@ -147,14 +162,16 @@ ipcMain.handle(
147162

148163
ipcMain.handle(
149164
'save-file',
150-
async (_event: IpcMainInvokeEvent, { defaultPath, content }) => {
165+
async (_event: IpcMainInvokeEvent, { defaultPath, content, encoding }) => {
151166
const file = await dialog.showSaveDialog({
152167
defaultPath,
153168
});
154169

155170
if (!file || !file.filePath) return;
156171

157-
await promisify(fs.writeFile)(file.filePath, content);
172+
await promisify(fs.writeFile)(file.filePath, content, {
173+
encoding,
174+
});
158175
}
159176
);
160177

src/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "plainbelt",
33
"productName": "plainbelt",
4-
"version": "0.0.1",
4+
"version": "0.0.2",
55
"description": "A toolbelt for all your plain text",
66
"main": "./main.prod.js",
77
"author": {

yarn.lock

+17
Original file line numberDiff line numberDiff line change
@@ -1756,6 +1756,13 @@
17561756
"@types/node" "*"
17571757
xmlbuilder ">=11.0.1"
17581758

1759+
"@types/pngjs@^6.0.1":
1760+
version "6.0.1"
1761+
resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-6.0.1.tgz#c711ec3fbbf077fed274ecccaf85dd4673130072"
1762+
integrity sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg==
1763+
dependencies:
1764+
"@types/node" "*"
1765+
17591766
"@types/prettier@^2.0.0":
17601767
version "2.1.5"
17611768
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00"
@@ -7708,6 +7715,11 @@ jsprim@^1.2.2:
77087715
json-schema "0.2.3"
77097716
verror "1.10.0"
77107717

7718+
jsqr@^1.4.0:
7719+
version "1.4.0"
7720+
resolved "https://registry.yarnpkg.com/jsqr/-/jsqr-1.4.0.tgz#8efb8d0a7cc6863cb6d95116b9069123ce9eb2d1"
7721+
integrity sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==
7722+
77117723
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.1.0:
77127724
version "3.1.0"
77137725
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.1.0.tgz#642f1d7b88aa6d7eb9d8f2210e166478444fa891"
@@ -9374,6 +9386,11 @@ pngjs@^3.3.0:
93749386
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
93759387
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
93769388

9389+
pngjs@^6.0.0:
9390+
version "6.0.0"
9391+
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821"
9392+
integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==
9393+
93779394
portfinder@^1.0.26:
93789395
version "1.0.28"
93799396
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"

0 commit comments

Comments
 (0)