Import pk from camera, QR visibility toggles, docs
CI / test (push) Successful in 5s

- Add Import pk from camera: scan QR → restore pb → auto login → refresh
- Add scanner.js (jsQR) for camera QR decode
- QR visibility: pk shown by default, pb hidden by default (toggles)
- Update docs/frontend-development.md with scanner, Import pk, QR behavior

Made-with: Cursor
This commit is contained in:
2026-03-01 14:05:43 +00:00
parent ef3957b618
commit 8a3cd2c27e
4 changed files with 154 additions and 11 deletions
+59
View File
@@ -0,0 +1,59 @@
/**
* QR code scanner from camera. Decodes private key (pk) from QR.
*/
import jsQR from "https://esm.sh/jsqr@1.4.0";
/**
* Scan QR code from camera video stream.
* @param {HTMLVideoElement} videoEl - Video element to render the stream.
* @param {AbortSignal} [signal] - AbortSignal to stop scanning (e.g. when dialog closes).
* @returns {Promise<string>} Decoded QR text.
*/
export async function scanQRFromCamera(videoEl, signal) {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" },
});
videoEl.srcObject = stream;
await videoEl.play();
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
return new Promise((resolve, reject) => {
if (signal?.aborted) {
stream.getTracks().forEach((t) => t.stop());
reject(new DOMException("Aborted", "AbortError"));
return;
}
signal?.addEventListener("abort", () => {
stream.getTracks().forEach((t) => t.stop());
videoEl.srcObject = null;
reject(new DOMException("Aborted", "AbortError"));
});
function tick() {
if (signal?.aborted) return;
if (videoEl.readyState !== videoEl.HAVE_ENOUGH_DATA) {
requestAnimationFrame(tick);
return;
}
canvas.width = videoEl.videoWidth;
canvas.height = videoEl.videoHeight;
if (canvas.width === 0 || canvas.height === 0) {
requestAnimationFrame(tick);
return;
}
ctx.drawImage(videoEl, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const result = jsQR(imageData.data, imageData.width, imageData.height);
if (result) {
stream.getTracks().forEach((t) => t.stop());
videoEl.srcObject = null;
resolve(result.data);
return;
}
requestAnimationFrame(tick);
}
tick();
});
}