diff --git a/README.md b/README.md
index 3c6bcd2..98816f5 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,7 @@
- [x] Markdown to HTML Converter
- [x] HTML Preview
- [x] QRCode Generator
+- [x] QRCode Reader
- [x] Base64 Encode/Decode
- [x] Text Diff
- [x] JSON Formatter
diff --git a/package.json b/package.json
index 9a2177a..ce9086f 100644
--- a/package.json
+++ b/package.json
@@ -172,6 +172,7 @@
"@types/jest": "^26.0.15",
"@types/marked": "^2.0.4",
"@types/node": "14.14.10",
+ "@types/pngjs": "^6.0.1",
"@types/qrcode": "^1.4.1",
"@types/react": "^16.9.44",
"@types/react-dom": "^16.9.9",
@@ -258,7 +259,9 @@
"electron-log": "^4.2.4",
"electron-updater": "^4.3.4",
"history": "^5.0.0",
+ "jsqr": "^1.4.0",
"marked": "^2.1.3",
+ "pngjs": "^6.0.0",
"qrcode": "^1.4.4",
"react": "^17.0.1",
"react-dom": "^17.0.1",
diff --git a/src/components/Main.tsx b/src/components/Main.tsx
index 1a22cd5..1848731 100644
--- a/src/components/Main.tsx
+++ b/src/components/Main.tsx
@@ -11,6 +11,7 @@ import Base64 from './base64/Base64';
import DiffText from './diff/TextDiff';
import SqlFormatter from './sql/SqlFormatter';
import JsonFormatter from './json/JsonFormatter';
+import QRCodeReader from './qrcode/QrCodeReader';
const Main = () => {
const routes = [
@@ -38,6 +39,12 @@ const Main = () => {
name: 'QRCode Generator',
Component: QrCodeGenerator,
},
+ {
+ icon: ,
+ path: '/qrcode-reader',
+ name: 'QRCode Reader',
+ Component: QRCodeReader,
+ },
{
icon: ,
path: '/base64-encoder',
diff --git a/src/components/qrcode/QrCodeGenerator.tsx b/src/components/qrcode/QrCodeGenerator.tsx
index c1fb52a..711683d 100644
--- a/src/components/qrcode/QrCodeGenerator.tsx
+++ b/src/components/qrcode/QrCodeGenerator.tsx
@@ -2,7 +2,7 @@ import { clipboard, ipcRenderer } from 'electron';
import React, { useState } from 'react';
import { useDebouncedEffect } from '../../helpers/effectHooks';
-const HtmlPreview = () => {
+const QRCodeGenerator = () => {
const [content, setContent] = useState('https://plainbelt.github.io');
const [qrCode, setQrCode] = useState();
const [opening, setOpening] = useState(false);
@@ -34,8 +34,9 @@ const HtmlPreview = () => {
const handleSave = async () => {
setSaving(true);
await ipcRenderer.invoke('save-file', {
- content: qrCode,
+ content: (qrCode || ',').split(',')[1],
defaultPath: 'qrcode.png',
+ encoding: 'base64',
});
setSaving(false);
};
@@ -84,4 +85,4 @@ const HtmlPreview = () => {
);
};
-export default HtmlPreview;
+export default QRCodeGenerator;
diff --git a/src/components/qrcode/QrCodeReader.tsx b/src/components/qrcode/QrCodeReader.tsx
new file mode 100644
index 0000000..ef26dc2
--- /dev/null
+++ b/src/components/qrcode/QrCodeReader.tsx
@@ -0,0 +1,85 @@
+import { clipboard, ipcRenderer, nativeImage } from 'electron';
+import jsQR from 'jsqr';
+import { PNG } from 'pngjs';
+import React, { useEffect, useState } from 'react';
+
+const QRCodeReader = () => {
+ const [image, setImage] = useState(nativeImage.createEmpty());
+ const [content, setContent] = useState('');
+ const [opening, setOpening] = useState(false);
+ const [copied, setCopied] = useState(false);
+
+ const handleOpen = async () => {
+ setOpening(true);
+ const filters = [{ name: 'Images', extensions: ['jpg', 'jpeg', 'png'] }];
+ const buff = await ipcRenderer.invoke('open-file', filters);
+ setImage(nativeImage.createFromBuffer(buff));
+ setOpening(false);
+ };
+
+ const handleClipboard = () => {
+ setImage(clipboard.readImage());
+ };
+
+ const handleCopy = () => {
+ setCopied(true);
+ clipboard.write({ text: content });
+ setTimeout(() => setCopied(false), 500);
+ };
+
+ useEffect(() => {
+ try {
+ const qr = jsQR(
+ Uint8ClampedArray.from(PNG.sync.read(image.toPNG()).data),
+ image.getSize().width,
+ image.getSize().height
+ );
+ setContent(qr?.data || 'No QRCode detected');
+ } catch (e) {
+ setContent('No QRCode detected');
+ }
+ }, [image]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {image && !image.isEmpty() && (
+
+ )}
+
+
+
+
+ );
+};
+
+export default QRCodeReader;
diff --git a/src/helpers/fontAwesome.ts b/src/helpers/fontAwesome.ts
index ee76725..8c07f7a 100644
--- a/src/helpers/fontAwesome.ts
+++ b/src/helpers/fontAwesome.ts
@@ -6,6 +6,7 @@ import {
faJsSquare,
} from '@fortawesome/free-brands-svg-icons';
import {
+ faCamera,
faClock,
faCode,
faCopy,
@@ -24,5 +25,6 @@ library.add(
faCode,
faExchangeAlt,
faDatabase,
- faJsSquare
+ faJsSquare,
+ faCamera
);
diff --git a/src/main.dev.ts b/src/main.dev.ts
index a5bdf42..04ee4a1 100644
--- a/src/main.dev.ts
+++ b/src/main.dev.ts
@@ -11,7 +11,14 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import path from 'path';
-import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron';
+import {
+ app,
+ BrowserWindow,
+ dialog,
+ ipcMain,
+ nativeImage,
+ shell,
+} from 'electron';
import { autoUpdater } from 'electron-updater';
import log from 'electron-log';
import { FileFilter, IpcMainInvokeEvent } from 'electron/main';
@@ -123,7 +130,11 @@ const createWindow = async () => {
// use Buffer.from(buffer).toString()
ipcMain.handle(
'open-file',
- async (_event: IpcMainInvokeEvent, filters: FileFilter[]) => {
+ async (
+ _event: IpcMainInvokeEvent,
+ filters: FileFilter[],
+ type: 'path' | 'buffer'
+ ) => {
const files = await dialog.showOpenDialog({
properties: ['openFile'],
filters,
@@ -131,8 +142,12 @@ ipcMain.handle(
let content;
if (files) {
- const buffer = await promisify(fs.readFile)(files.filePaths[0]);
- content = buffer;
+ const fpath = files.filePaths[0];
+ if (type === 'path') {
+ content = fpath;
+ } else {
+ content = await promisify(fs.readFile)(fpath);
+ }
}
return content;
}
@@ -147,14 +162,16 @@ ipcMain.handle(
ipcMain.handle(
'save-file',
- async (_event: IpcMainInvokeEvent, { defaultPath, content }) => {
+ async (_event: IpcMainInvokeEvent, { defaultPath, content, encoding }) => {
const file = await dialog.showSaveDialog({
defaultPath,
});
if (!file || !file.filePath) return;
- await promisify(fs.writeFile)(file.filePath, content);
+ await promisify(fs.writeFile)(file.filePath, content, {
+ encoding,
+ });
}
);
diff --git a/src/package.json b/src/package.json
index d63503b..828a38c 100644
--- a/src/package.json
+++ b/src/package.json
@@ -1,7 +1,7 @@
{
"name": "plainbelt",
"productName": "plainbelt",
- "version": "0.0.1",
+ "version": "0.0.2",
"description": "A toolbelt for all your plain text",
"main": "./main.prod.js",
"author": {
diff --git a/yarn.lock b/yarn.lock
index 97d137c..5cdc898 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1756,6 +1756,13 @@
"@types/node" "*"
xmlbuilder ">=11.0.1"
+"@types/pngjs@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-6.0.1.tgz#c711ec3fbbf077fed274ecccaf85dd4673130072"
+ integrity sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg==
+ dependencies:
+ "@types/node" "*"
+
"@types/prettier@^2.0.0":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00"
@@ -7708,6 +7715,11 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
+jsqr@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/jsqr/-/jsqr-1.4.0.tgz#8efb8d0a7cc6863cb6d95116b9069123ce9eb2d1"
+ integrity sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==
+
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.1.0.tgz#642f1d7b88aa6d7eb9d8f2210e166478444fa891"
@@ -9374,6 +9386,11 @@ pngjs@^3.3.0:
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
+pngjs@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821"
+ integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==
+
portfinder@^1.0.26:
version "1.0.28"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"