Universal Links (Associated Domains) sind an eine feste Domain gebunden - fuer einen selbstgehosteten Dienst untauglich (jede Instanz hat eine andere URL) und die Domain stand im offenen Quellcode (Leak). Jetzt tragen Einzelstueck-QRs das Custom-Scheme vorrania://i/<UID>: Die normale Kamera-App oeffnet die App weiterhin, aber ohne Domain-Bindung und ohne Server im Code. applinks-Eintrag entfernt. Web: itemDeepLink()-Helfer, genutzt in Einzelstuecke-Liste/-Etiketten und im P-touch-Export. Lagerort-QRs bleiben unveraendert (https). iOS: Router erkennt vorrania://i/<UID> und oeffnet das Einzelstueck am aktiven Server. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
52 lines
2.2 KiB
React
52 lines
2.2 KiB
React
import { useEffect, useState } from "react";
|
|
import QRCode from "qrcode";
|
|
|
|
/**
|
|
* QR-Inhalt für ein Einzelstück. Bewusst das Custom-Scheme `vorrania://` statt
|
|
* einer https-URL: Die normale Kamera-App öffnet damit trotzdem die App, aber
|
|
* OHNE an eine feste Domain gebunden zu sein. Das ist für einen selbstgehosteten
|
|
* Dienst nötig (jede Instanz hat eine andere URL) und der QR verrät keinen Server.
|
|
* Die App öffnet die UID am gerade aktiven Server.
|
|
*/
|
|
export const itemDeepLink = (uid) => `vorrania://i/${uid}`;
|
|
|
|
/** Kleines QR-Bild für einen Wert (asynchron erzeugt). */
|
|
export function QrImg({ text, size = 60 }) {
|
|
const [url, setUrl] = useState(null);
|
|
useEffect(() => {
|
|
let ok = true;
|
|
QRCode.toDataURL(text, { margin: 1, width: size * 2 })
|
|
.then((u) => { if (ok) setUrl(u); })
|
|
.catch(() => {});
|
|
return () => { ok = false; };
|
|
}, [text, size]);
|
|
return url
|
|
? <img src={url} width={size} height={size} alt="QR" style={{ display: "block" }} />
|
|
: <span style={{ display: "inline-block", width: size, height: size }} />;
|
|
}
|
|
|
|
const esc = (s) => (s || "").replace(/[<>&]/g, "");
|
|
|
|
/** Druckt QR-Etiketten in einem neuen Fenster. entries: [{ text, line1, line2 }]. */
|
|
export async function printQrLabels(title, entries) {
|
|
const labels = await Promise.all(entries.map(async (e) => {
|
|
const url = await QRCode.toDataURL(e.text, { margin: 1, width: 260 });
|
|
return `<div class="label"><img src="${url}"/><div class="l1">${esc(e.line1)}</div>`
|
|
+ `<div class="l2">${esc(e.line2)}</div></div>`;
|
|
}));
|
|
const w = window.open("", "_blank");
|
|
if (!w) return false;
|
|
w.document.write(
|
|
`<!doctype html><html><head><meta charset="utf-8"><title>${esc(title)}</title><style>
|
|
body{font-family:sans-serif;margin:8mm;display:flex;flex-wrap:wrap;gap:6mm}
|
|
.label{width:34mm;text-align:center;border:1px solid #ccc;border-radius:3mm;padding:3mm;page-break-inside:avoid}
|
|
.label img{width:28mm;height:28mm}
|
|
.l1{font-weight:700;font-size:10pt;margin-top:1mm}
|
|
.l2{font-size:8pt;color:#444}
|
|
</style></head><body>${labels.join("")}
|
|
<script>window.onload=function(){window.print()}</script></body></html>`,
|
|
);
|
|
w.document.close();
|
|
return true;
|
|
}
|