04417cf05c
- WireGuard: refactor to multi-interface 'access classes' model; extract config generation and helpers into lib/wireguard.py; add per-class up/down endpoints and API routes; update UI with class management pages and QR code component - Firewall: fix zone creation with --new-zone before --set-target; skip masquerade on public zone; add masquerade propagation for nftables backend so NAT works when internal zones exit via public - Network: rename sync event subsystem 'network' -> 'networkd'; always stamp config hash even when deployment fails (fixes pending-changes detection) - DHCP: add new API endpoint and update frontend page - State/Sync: update state collectors and sync buses for new subsystems - Docs: update API and config documentation for new endpoints and schemas
154 lines
4.9 KiB
JavaScript
154 lines
4.9 KiB
JavaScript
/**
|
|
* Hoover — components/qr.js
|
|
*
|
|
* QR code SVG renderer with optional logo overlay.
|
|
* Uses qrcode-svg library for generation.
|
|
*/
|
|
|
|
import { h } from '../vdom.js?v=9';
|
|
import { esc } from '../helpers.js?v=9';
|
|
import QRCode from '../../../vendor/qrcode-svg-1.1.0.js';
|
|
|
|
/**
|
|
* Generate a QR code SVG string from text content.
|
|
*
|
|
* @param {object} props
|
|
* @param {string} props.text - Text to encode
|
|
* @param {number} [props.size=200] - QR code size in px
|
|
* @param {number} [props.margin=2] - Quiet zone margin
|
|
* @param {string} [props.ecLevel='Q'] - Error correction level (L/M/Q/H)
|
|
* @param {string} [props.logo] - Base64 data URL for center logo
|
|
* @param {number} [props.logoSize=40] - Logo size in px (when overlaying)
|
|
* @param {string} [props.color] - Foreground color (default: #000000)
|
|
* @param {string} [props.background] - Background color (default: #ffffff)
|
|
* @returns {string} SVG markup string
|
|
*/
|
|
export function qrSVG(props = {}) {
|
|
const {
|
|
text,
|
|
size = 200,
|
|
margin = 2,
|
|
ecLevel = 'Q',
|
|
logo,
|
|
logoSize = 40,
|
|
color = '#000000',
|
|
background = '#ffffff',
|
|
} = props;
|
|
|
|
if (!text) return '';
|
|
|
|
const qr = new QRCode({
|
|
content: text,
|
|
container: 'svg',
|
|
margin: margin,
|
|
padding: 0,
|
|
width: size,
|
|
height: size,
|
|
color: color,
|
|
background: background,
|
|
ecl: ecLevel,
|
|
creambo: false,
|
|
prettyprint: false,
|
|
});
|
|
|
|
let svg = qr.svg();
|
|
|
|
// Add center logo overlay if provided
|
|
if (logo) {
|
|
const halfSize = size / 2;
|
|
const halfLogo = logoSize / 2;
|
|
|
|
const ns = 'http://www.w3.org/2000/svg';
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(svg, 'image/svg+xml');
|
|
const rootSvg = doc.documentElement;
|
|
const viewBox = rootSvg.getAttribute('viewBox') || `0 0 ${size} ${size}`;
|
|
|
|
const newSvg = doc.createElementNS(ns, 'svg');
|
|
newSvg.setAttribute('xmlns', ns);
|
|
newSvg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
|
newSvg.setAttribute('width', size);
|
|
newSvg.setAttribute('height', size);
|
|
newSvg.setAttribute('viewBox', viewBox);
|
|
|
|
// Clone original content
|
|
const clone = rootSvg.cloneNode(true);
|
|
while (clone.firstChild) {
|
|
newSvg.appendChild(clone.firstChild);
|
|
}
|
|
|
|
// White background behind logo
|
|
const bgRect = doc.createElementNS(ns, 'rect');
|
|
bgRect.setAttribute('x', halfSize - halfLogo - 4);
|
|
bgRect.setAttribute('y', halfSize - halfLogo - 4);
|
|
bgRect.setAttribute('width', logoSize + 8);
|
|
bgRect.setAttribute('height', logoSize + 8);
|
|
bgRect.setAttribute('fill', background);
|
|
newSvg.appendChild(bgRect);
|
|
|
|
// Logo image overlay
|
|
const img = doc.createElementNS(ns, 'image');
|
|
img.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', logo);
|
|
img.setAttribute('x', halfSize - halfLogo);
|
|
img.setAttribute('y', halfSize - halfLogo);
|
|
img.setAttribute('width', logoSize);
|
|
img.setAttribute('height', logoSize);
|
|
newSvg.appendChild(img);
|
|
|
|
const serializer = new XMLSerializer();
|
|
svg = serializer.serializeToString(newSvg);
|
|
}
|
|
|
|
return svg;
|
|
}
|
|
|
|
/**
|
|
* Render a QR code as a VNode with innerHTML for the SVG.
|
|
*
|
|
* @param {object} props
|
|
* @param {string} props.text - Text to encode
|
|
* @param {number} [props.size=200] - QR code size
|
|
* @param {string} [props.logo] - Base64 data URL for logo
|
|
* @param {number} [props.logoSize=40] - Logo overlay size
|
|
* @returns {object} VNode
|
|
*/
|
|
export function QRCodeVNode(props = {}) {
|
|
const svgString = qrSVG(props);
|
|
if (!svgString) {
|
|
return h('div', {}, h('span', { class: 'text-muted' }, 'No content'));
|
|
}
|
|
return h('div', {
|
|
class: 'qr-code-container text-center',
|
|
innerHTML: svgString,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Logo upload widget — file input that produces base64 data URL.
|
|
*
|
|
* @param {object} props
|
|
* @param {string} props.id - Input element ID
|
|
* @param {function} props.onChange - Callback(logoBase64) on file select
|
|
*/
|
|
export function LogoUpload(props = {}) {
|
|
const inputId = props.id || 'qr-logo-input';
|
|
return h('div', { class: 'mb-2' }, [
|
|
h('label', { class: 'form-label' }, 'Logo (optional)'),
|
|
h('input', {
|
|
type: 'file',
|
|
id: inputId,
|
|
accept: 'image/*',
|
|
class: 'form-control',
|
|
onChange: function (e) {
|
|
const file = e.target.files?.[0];
|
|
if (!file || !props.onChange) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = function (ev) {
|
|
props.onChange(ev.target.result);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
},
|
|
}),
|
|
]);
|
|
} |