mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-05 14:50:11 +00:00
Compare commits
3
Commits
ce3674669e
...
73bbe8b268
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73bbe8b268 | ||
|
|
d024798548 | ||
|
|
b0a679b1c2 |
Generated
+7
@@ -14,6 +14,7 @@
|
|||||||
"@tanstack/react-query": "^5.101.2",
|
"@tanstack/react-query": "^5.101.2",
|
||||||
"@zip.js/zip.js": "^2.8.26",
|
"@zip.js/zip.js": "^2.8.26",
|
||||||
"fflate": "^0.8.3",
|
"fflate": "^0.8.3",
|
||||||
|
"jsqr": "1.4.0",
|
||||||
"lucide-preact": "^1.22.0",
|
"lucide-preact": "^1.22.0",
|
||||||
"preact": "^10.29.3",
|
"preact": "^10.29.3",
|
||||||
"qrcode-generator": "^2.0.4",
|
"qrcode-generator": "^2.0.4",
|
||||||
@@ -3442,6 +3443,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsqr": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/kleur": {
|
"node_modules/kleur": {
|
||||||
"version": "4.1.5",
|
"version": "4.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
"@tanstack/react-query": "^5.101.2",
|
"@tanstack/react-query": "^5.101.2",
|
||||||
"@zip.js/zip.js": "^2.8.26",
|
"@zip.js/zip.js": "^2.8.26",
|
||||||
"fflate": "^0.8.3",
|
"fflate": "^0.8.3",
|
||||||
|
"jsqr": "1.4.0",
|
||||||
"lucide-preact": "^1.22.0",
|
"lucide-preact": "^1.22.0",
|
||||||
"preact": "^10.29.3",
|
"preact": "^10.29.3",
|
||||||
"qrcode-generator": "^2.0.4",
|
"qrcode-generator": "^2.0.4",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { RefObject } from 'preact';
|
import type { RefObject } from 'preact';
|
||||||
import { createPortal } from 'preact/compat';
|
import { createPortal } from 'preact/compat';
|
||||||
import { ArrowDown, ArrowUp, CheckCheck, Download, Paperclip, Plus, QrCode, RefreshCw, Star, StarOff, Trash2, Upload, X } from 'lucide-preact';
|
import { ArrowDown, ArrowUp, CheckCheck, Download, Paperclip, Plus, QrCode, RefreshCw, Star, StarOff, Trash2, Upload, X } from 'lucide-preact';
|
||||||
|
import jsQR from 'jsqr';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { useDialogLifecycle } from '@/components/ConfirmDialog';
|
import { useDialogLifecycle } from '@/components/ConfirmDialog';
|
||||||
import type { Cipher, Folder, VaultDraft, VaultDraftField } from '@/lib/types';
|
import type { Cipher, Folder, VaultDraft, VaultDraftField } from '@/lib/types';
|
||||||
@@ -171,16 +172,38 @@ export default function VaultEditor(props: VaultEditorProps) {
|
|||||||
return new window.BarcodeDetector({ formats: ['qr_code'] });
|
return new window.BarcodeDetector({ formats: ['qr_code'] });
|
||||||
};
|
};
|
||||||
|
|
||||||
const decodeTotpQrImage = async (source: ImageBitmapSource): Promise<boolean> => {
|
const decodeTotpQrCanvas = (source: ImageBitmap | HTMLVideoElement): string => {
|
||||||
|
const width = 'videoWidth' in source ? source.videoWidth : source.width;
|
||||||
|
const height = 'videoHeight' in source ? source.videoHeight : source.height;
|
||||||
|
if (!width || !height) return '';
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
if (!context) return '';
|
||||||
|
// jsQR ignores alpha and reads RGB directly, so transparent pixels would be
|
||||||
|
// treated as black. Composite over white first so transparent-background QR
|
||||||
|
// exports do not become black-on-black and fail to decode.
|
||||||
|
context.fillStyle = '#ffffff';
|
||||||
|
context.fillRect(0, 0, width, height);
|
||||||
|
context.drawImage(source, 0, 0, width, height);
|
||||||
|
const imageData = context.getImageData(0, 0, width, height);
|
||||||
|
return String(jsQR(imageData.data, width, height)?.data || '').trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const decodeTotpQrImage = async (source: ImageBitmap): Promise<boolean> => {
|
||||||
const detector = createTotpQrDetector();
|
const detector = createTotpQrDetector();
|
||||||
if (!detector) {
|
if (detector) {
|
||||||
setTotpQrStatus(t('txt_totp_qr_unsupported'));
|
try {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const results = await detector.detect(source);
|
const results = await detector.detect(source);
|
||||||
const value = String(results[0]?.rawValue || '').trim();
|
const value = String(results[0]?.rawValue || '').trim();
|
||||||
if (!value) return false;
|
if (value && applyTotpQrValue(value)) return true;
|
||||||
return applyTotpQrValue(value);
|
} catch {
|
||||||
|
// Fall back to jsQR when the native detector is present but not usable.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const value = decodeTotpQrCanvas(source);
|
||||||
|
return value ? applyTotpQrValue(value) : false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTotpQrFile = async (file: File | null) => {
|
const handleTotpQrFile = async (file: File | null) => {
|
||||||
@@ -206,14 +229,8 @@ export default function VaultEditor(props: VaultEditorProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
|
let lastCanvasScan = 0;
|
||||||
const detector = createTotpQrDetector();
|
const detector = createTotpQrDetector();
|
||||||
if (!detector) {
|
|
||||||
setTotpQrStatus(t('txt_totp_qr_unsupported'));
|
|
||||||
return () => {
|
|
||||||
stopped = true;
|
|
||||||
stopTotpQrScanner();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!navigator.mediaDevices?.getUserMedia) {
|
if (!navigator.mediaDevices?.getUserMedia) {
|
||||||
setTotpQrStatus(t('txt_totp_qr_camera_unavailable'));
|
setTotpQrStatus(t('txt_totp_qr_camera_unavailable'));
|
||||||
return () => {
|
return () => {
|
||||||
@@ -229,9 +246,26 @@ export default function VaultEditor(props: VaultEditorProps) {
|
|||||||
totpQrFrameRef.current = window.requestAnimationFrame(scan);
|
totpQrFrameRef.current = window.requestAnimationFrame(scan);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
let value = '';
|
||||||
|
if (detector) {
|
||||||
try {
|
try {
|
||||||
const results = await detector.detect(video);
|
const results = await detector.detect(video);
|
||||||
const value = String(results[0]?.rawValue || '').trim();
|
value = String(results[0]?.rawValue || '').trim();
|
||||||
|
} catch {
|
||||||
|
// Fall back to jsQR when the native detector is present but not usable.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The jsQR fallback runs a synchronous full-frame decode, so throttle
|
||||||
|
// it to a few times per second instead of every animation frame to
|
||||||
|
// avoid pegging the CPU while a code is being aligned.
|
||||||
|
if (!value) {
|
||||||
|
const now = performance.now();
|
||||||
|
if (now - lastCanvasScan >= 250) {
|
||||||
|
lastCanvasScan = now;
|
||||||
|
value = decodeTotpQrCanvas(video);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (value && applyTotpQrValue(value)) return;
|
if (value && applyTotpQrValue(value)) return;
|
||||||
} catch {
|
} catch {
|
||||||
// Keep the camera active; transient frame decode failures are common.
|
// Keep the camera active; transient frame decode failures are common.
|
||||||
|
|||||||
Reference in New Issue
Block a user