@k2b/ui 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/base.css +1097 -0
- package/dist/browser/chat/ChatPrimitives.js +19 -16
- package/dist/browser/chat/ChatPrimitives.js.map +1 -1
- package/dist/browser/chat/ChatTimeline.js +110 -87
- package/dist/browser/chat/ChatTimeline.js.map +1 -1
- package/dist/browser/content/FileGrid.js +28 -8
- package/dist/browser/content/FileGrid.js.map +1 -1
- package/dist/browser/content/MarkdownView.js +11 -5
- package/dist/browser/content/MarkdownView.js.map +1 -1
- package/dist/browser/content/MessageRow.js +1250 -0
- package/dist/browser/content/MessageRow.js.map +1 -0
- package/dist/browser/content/StructuredDataPreview.js +4 -1
- package/dist/browser/content/StructuredDataPreview.js.map +1 -1
- package/dist/browser/content/VirtualFeed.js +1127 -0
- package/dist/browser/content/VirtualFeed.js.map +1 -0
- package/dist/browser/content/collection-selection.js +27 -11
- package/dist/browser/content/collection-selection.js.map +1 -1
- package/dist/browser/content/index.js +2 -0
- package/dist/browser/content/index.js.map +1 -1
- package/dist/browser/feedback/InstallGuide.js +199 -145
- package/dist/browser/feedback/InstallGuide.js.map +1 -1
- package/dist/browser/feedback/install.js +8 -4
- package/dist/browser/feedback/install.js.map +1 -1
- package/dist/browser/inputs/QrScanner.js +101 -21
- package/dist/browser/inputs/QrScanner.js.map +1 -1
- package/dist/browser/intl/messages.js +86 -0
- package/dist/browser/intl/messages.js.map +1 -1
- package/dist/browser/surfaces/Avatar.js +33 -17
- package/dist/browser/surfaces/Avatar.js.map +1 -1
- package/dist/ssr/chat/ChatPrimitives.js +2 -2
- package/dist/ssr/chat/ChatPrimitives.js.map +1 -1
- package/dist/ssr/chat/ChatTimeline.js +110 -87
- package/dist/ssr/chat/ChatTimeline.js.map +1 -1
- package/dist/ssr/content/FileGrid.js +17 -3
- package/dist/ssr/content/FileGrid.js.map +1 -1
- package/dist/ssr/content/MarkdownView.js +11 -5
- package/dist/ssr/content/MarkdownView.js.map +1 -1
- package/dist/ssr/content/MessageRow.js +796 -0
- package/dist/ssr/content/MessageRow.js.map +1 -0
- package/dist/ssr/content/StructuredDataPreview.js +1 -1
- package/dist/ssr/content/StructuredDataPreview.js.map +1 -1
- package/dist/ssr/content/VirtualFeed.js +989 -0
- package/dist/ssr/content/VirtualFeed.js.map +1 -0
- package/dist/ssr/content/collection-selection.js +27 -11
- package/dist/ssr/content/collection-selection.js.map +1 -1
- package/dist/ssr/content/index.js +2 -0
- package/dist/ssr/content/index.js.map +1 -1
- package/dist/ssr/feedback/InstallGuide.js +150 -98
- package/dist/ssr/feedback/InstallGuide.js.map +1 -1
- package/dist/ssr/feedback/install.js +8 -4
- package/dist/ssr/feedback/install.js.map +1 -1
- package/dist/ssr/inputs/QrScanner.js +101 -21
- package/dist/ssr/inputs/QrScanner.js.map +1 -1
- package/dist/ssr/intl/messages.js +86 -0
- package/dist/ssr/intl/messages.js.map +1 -1
- package/dist/ssr/surfaces/Avatar.js +14 -1
- package/dist/ssr/surfaces/Avatar.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/chat/ChatPrimitives.d.ts +2 -0
- package/dist/types/chat/ChatTimeline.d.ts +2 -0
- package/dist/types/content/FileGrid.d.ts +6 -1
- package/dist/types/content/MarkdownView.d.ts +8 -0
- package/dist/types/content/MessageRow.d.ts +168 -0
- package/dist/types/content/VirtualFeed.d.ts +81 -0
- package/dist/types/content/collection-selection.d.ts +15 -1
- package/dist/types/content/index.d.ts +4 -0
- package/dist/types/feedback/InstallGuide.d.ts +5 -3
- package/dist/types/feedback/install.d.ts +4 -3
- package/dist/types/inputs/QrScanner.d.ts +10 -2
- package/dist/types/intl/messages.d.ts +43 -0
- package/package.json +3 -1
|
@@ -13,18 +13,87 @@ var _tmpl$ = /*#__PURE__*/_$template(`<p class=k2b-qr-scanner__note>`),
|
|
|
13
13
|
import { timed } from "@k2b/stdlib/solid";
|
|
14
14
|
import { createSignal, onCleanup, onMount, Show } from "solid-js";
|
|
15
15
|
import { useUiMessages } from "../intl/messages";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Why the scanner could not start, as far as the browser tells: `"denied"` when camera access is off, `"no-camera"`
|
|
19
|
+
* when the device has no camera, `"in-use"` when the camera is busy or could not start, most often because another app
|
|
20
|
+
* holds it, and `"unavailable"` for anything else, including a scanning engine that cannot load.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const sizes = [{
|
|
24
|
+
width: {
|
|
25
|
+
min: 1024
|
|
26
|
+
}
|
|
27
|
+
}, {
|
|
28
|
+
width: {
|
|
29
|
+
min: 768
|
|
30
|
+
}
|
|
31
|
+
}, {}];
|
|
32
|
+
/** The rear camera at a resolution that keeps small codes readable, then any camera: the order qr-scanner tries. */
|
|
33
|
+
const cameraRequests = [...sizes.map(size => ({
|
|
34
|
+
...size,
|
|
35
|
+
facingMode: {
|
|
36
|
+
exact: "environment"
|
|
37
|
+
}
|
|
38
|
+
})), ...sizes];
|
|
39
|
+
const errorName = error => typeof error === "object" && error !== null && "name" in error ? String(error.name) : "";
|
|
40
|
+
const cameraFailure = name => {
|
|
41
|
+
if (name === "NotAllowedError" || name === "SecurityError") return "denied";
|
|
42
|
+
if (name === "NotFoundError" || name === "OverconstrainedError") return "no-camera";
|
|
43
|
+
if (name === "NotReadableError") return "in-use";
|
|
44
|
+
return "unavailable";
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** The facing a track reports, or else the one its label names, as qr-scanner reads it. */
|
|
48
|
+
const facingOf = track => {
|
|
49
|
+
if (!track) return undefined;
|
|
50
|
+
const {
|
|
51
|
+
facingMode
|
|
52
|
+
} = track.getSettings();
|
|
53
|
+
if (facingMode) return facingMode;
|
|
54
|
+
if (/rear|back|environment/i.test(track.label)) return "environment";
|
|
55
|
+
if (/front|user|face/i.test(track.label)) return "user";
|
|
56
|
+
return undefined;
|
|
57
|
+
};
|
|
58
|
+
|
|
16
59
|
/**
|
|
17
|
-
* qr-scanner turns every
|
|
18
|
-
*
|
|
60
|
+
* Opens the camera here rather than in qr-scanner, which turns every failure into "Camera not found.". A refusal ends
|
|
61
|
+
* the search, because asking again could show the permission prompt again. Any other failure may belong to the request,
|
|
62
|
+
* such as a camera that cannot start at the larger size, so the next, looser request follows, as in qr-scanner. When
|
|
63
|
+
* none succeeds, a camera that exists but fails outweighs a request no camera could meet. A closed scanner asks no more.
|
|
19
64
|
*/
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
65
|
+
const openCamera = async closed => {
|
|
66
|
+
if (!navigator.mediaDevices) return {
|
|
67
|
+
error: "unavailable"
|
|
68
|
+
};
|
|
69
|
+
let error = "no-camera";
|
|
70
|
+
for (const video of cameraRequests) {
|
|
71
|
+
if (closed()) break;
|
|
72
|
+
try {
|
|
73
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
74
|
+
video,
|
|
75
|
+
audio: false
|
|
76
|
+
});
|
|
77
|
+
const facing = facingOf(stream.getVideoTracks()[0]);
|
|
78
|
+
// Like qr-scanner, mirror a front camera, and a camera of unknown facing that the rear request did not find.
|
|
79
|
+
return {
|
|
80
|
+
stream,
|
|
81
|
+
mirrored: facing ? facing === "user" : !video.facingMode
|
|
82
|
+
};
|
|
83
|
+
} catch (failure) {
|
|
84
|
+
const reason = cameraFailure(errorName(failure));
|
|
85
|
+
if (reason === "denied") return {
|
|
86
|
+
error: reason
|
|
87
|
+
};
|
|
88
|
+
if (reason !== "no-camera") error = reason;
|
|
89
|
+
}
|
|
27
90
|
}
|
|
91
|
+
return {
|
|
92
|
+
error
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
const stopStream = stream => {
|
|
96
|
+
for (const track of stream.getTracks()) track.stop();
|
|
28
97
|
};
|
|
29
98
|
|
|
30
99
|
/**
|
|
@@ -44,24 +113,22 @@ export function QrScanner(props) {
|
|
|
44
113
|
let video;
|
|
45
114
|
let scanner;
|
|
46
115
|
let closed = false;
|
|
47
|
-
/** The host has unmounted the scanner; it no longer hears about failures that finish later. */
|
|
48
|
-
let unmounted = false;
|
|
49
116
|
let cleanupMotion;
|
|
50
117
|
const destroy = () => {
|
|
51
118
|
closed = true;
|
|
52
119
|
cleanupMotion?.();
|
|
53
120
|
resetRejected.cancel();
|
|
54
121
|
resetFeedback.cancel();
|
|
55
|
-
// pause(true) stops
|
|
122
|
+
// pause(true) stops the camera stream immediately.
|
|
56
123
|
void scanner?.pause(true);
|
|
57
124
|
scanner?.destroy();
|
|
58
125
|
scanner = undefined;
|
|
59
126
|
};
|
|
60
|
-
|
|
127
|
+
// Once closed, the host has unmounted the scanner or heard why it stopped; later failures stay silent.
|
|
128
|
+
const fail = reason => {
|
|
61
129
|
if (closed) return;
|
|
62
130
|
destroy();
|
|
63
|
-
|
|
64
|
-
if (!unmounted) props.onError(denied ? "denied" : "unavailable");
|
|
131
|
+
props.onError(reason);
|
|
65
132
|
};
|
|
66
133
|
onMount(() => {
|
|
67
134
|
const hidden = () => {
|
|
@@ -71,7 +138,6 @@ export function QrScanner(props) {
|
|
|
71
138
|
document.addEventListener("visibilitychange", hidden);
|
|
72
139
|
window.addEventListener("pagehide", leaving);
|
|
73
140
|
onCleanup(() => {
|
|
74
|
-
unmounted = true;
|
|
75
141
|
destroy();
|
|
76
142
|
document.removeEventListener("visibilitychange", hidden);
|
|
77
143
|
window.removeEventListener("pagehide", leaving);
|
|
@@ -95,12 +161,20 @@ export function QrScanner(props) {
|
|
|
95
161
|
setInvalid(true);
|
|
96
162
|
resetFeedback.debouncedFn();
|
|
97
163
|
}, {
|
|
98
|
-
preferredCamera: "environment",
|
|
99
164
|
highlightScanRegion: true,
|
|
100
|
-
//
|
|
165
|
+
// A frame that cannot be decoded never ends the session: the camera runs, and the next frame may read.
|
|
166
|
+
// An engine error other than an empty frame, which the native BarcodeDetector reports as "Scanner error:
|
|
167
|
+
// No QR code found", means the detector cannot read on this device. Fall back to the bundled worker for
|
|
168
|
+
// the page through the flag qr-scanner 1.4.2 sets itself for "not implemented" and "service unavailable";
|
|
169
|
+
// it swaps engines after this frame. Worker errors carry the same prefix; the flag changes nothing there.
|
|
170
|
+
// qr-scanner reports frames only as strings: an Error means the engine itself did not load, such as a
|
|
171
|
+
// decoder chunk gone after a deploy or a worker that cannot start, and no later frame can read. Never log
|
|
172
|
+
// decoded content or frame data.
|
|
101
173
|
onDecodeError: error => {
|
|
102
|
-
if (
|
|
103
|
-
|
|
174
|
+
if (typeof error !== "string") return fail("unavailable");
|
|
175
|
+
if (error.startsWith("Scanner error: ") && !error.endsWith(Engine.NO_QR_CODE_FOUND)) {
|
|
176
|
+
Reflect.set(Engine, "_disableBarcodeDetector", true);
|
|
177
|
+
}
|
|
104
178
|
}
|
|
105
179
|
});
|
|
106
180
|
const motion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
@@ -114,10 +188,16 @@ export function QrScanner(props) {
|
|
|
114
188
|
applyMotion();
|
|
115
189
|
motion.addEventListener("change", applyMotion);
|
|
116
190
|
cleanupMotion = () => motion.removeEventListener("change", applyMotion);
|
|
191
|
+
const camera = await openCamera(() => closed);
|
|
192
|
+
if ("error" in camera) return fail(camera.error);
|
|
193
|
+
if (closed) return stopStream(camera.stream);
|
|
194
|
+
// qr-scanner plays a stream that is already set instead of opening its own.
|
|
195
|
+
video.srcObject = camera.stream;
|
|
196
|
+
video.style.transform = camera.mirrored ? "scaleX(-1)" : "";
|
|
117
197
|
await scanner.start();
|
|
118
198
|
if (!closed) setStarting(false);
|
|
119
199
|
} catch {
|
|
120
|
-
|
|
200
|
+
fail("unavailable");
|
|
121
201
|
}
|
|
122
202
|
})();
|
|
123
203
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sources":["inputs/QrScanner.tsx"],"sourcesContent":["import { timed } from \"@k2b/stdlib/solid\";\nimport type QrScannerEngine from \"qr-scanner\";\nimport { createSignal, type JSX, onCleanup, onMount, Show } from \"solid-js\";\nimport { useUiMessages } from \"../intl/messages\";\n\nexport type QrScannerError = \"denied\" | \"unavailable\";\n\nexport type QrScannerProps = {\n /** Decoded text is untrusted. Return `true` to accept it and stop the camera, `false` to reject it and keep scanning. */\n onResult: (text: string) => boolean;\n /** The page was hidden or left; unmount the scanner. */\n onStop: () => void;\n /** The camera could not start: access was denied, or there is no usable camera. Offer another way in. */\n onError: (reason: QrScannerError) => void;\n /** What to point the camera at. Defaults to a generic QR code hint. */\n instructions?: string;\n class?: string;\n};\n\n/**\n * qr-scanner turns every getUserMedia failure into the string \"Camera not found.\", so only the Permissions API can\n * tell a refusal apart. Safari and Chrome expose the camera permission; elsewhere a refusal counts as unavailable.\n */\nconst cameraDenied = async (): Promise<boolean> => {\n try {\n return (await navigator.permissions.query({ name: \"camera\" as PermissionName })).state === \"denied\";\n } catch {\n return false;\n }\n};\n\n/**\n * Scans a QR code with the rear camera. Each mount owns exactly one camera session, which stops when the scanner\n * unmounts, the page is hidden, or a result is accepted. The scanning engine loads only when the scanner mounts.\n * Decoded content is never logged.\n */\nexport function QrScanner(props: QrScannerProps): JSX.Element {\n const messages = useUiMessages();\n const [starting, setStarting] = createSignal(true);\n const [invalid, setInvalid] = createSignal(false);\n let lastRejected: string | undefined;\n const resetRejected = timed.debounce(() => {\n lastRejected = undefined;\n }, 1000);\n const resetFeedback = timed.debounce(() => setInvalid(false), 1500);\n let video!: HTMLVideoElement;\n let scanner: QrScannerEngine | undefined;\n let closed = false;\n /** The host has unmounted the scanner; it no longer hears about failures that finish later. */\n let unmounted = false;\n let cleanupMotion: (() => void) | undefined;\n const destroy = () => {\n closed = true;\n cleanupMotion?.();\n resetRejected.cancel();\n resetFeedback.cancel();\n // pause(true) stops existing tracks immediately; destroy also handles a still-pending permission request.\n void scanner?.pause(true);\n scanner?.destroy();\n scanner = undefined;\n };\n const fail = async () => {\n if (closed) return;\n destroy();\n const denied = await cameraDenied();\n if (!unmounted) props.onError(denied ? \"denied\" : \"unavailable\");\n };\n onMount(() => {\n const hidden = () => {\n if (document.visibilityState !== \"visible\") props.onStop();\n };\n const leaving = () => props.onStop();\n document.addEventListener(\"visibilitychange\", hidden);\n window.addEventListener(\"pagehide\", leaving);\n onCleanup(() => {\n unmounted = true;\n destroy();\n document.removeEventListener(\"visibilitychange\", hidden);\n window.removeEventListener(\"pagehide\", leaving);\n });\n void (async () => {\n try {\n const { default: Engine } = await import(\"qr-scanner\");\n if (closed || document.visibilityState !== \"visible\") return;\n scanner = new Engine(\n video,\n (result) => {\n if (closed) return;\n // Rearm only after a full second without a decoded QR, not between individual frames.\n resetRejected.debouncedFn();\n if (result.data === lastRejected) return;\n if (props.onResult(result.data)) {\n destroy();\n return;\n }\n lastRejected = result.data;\n setInvalid(true);\n resetFeedback.debouncedFn();\n },\n {\n preferredCamera: \"environment\",\n highlightScanRegion: true,\n // Empty frames are expected. Never log decoded content or frame data.\n onDecodeError: (error) => {\n if (closed || error === Engine.NO_QR_CODE_FOUND) return;\n void fail();\n },\n },\n );\n const motion = matchMedia(\"(prefers-reduced-motion: reduce)\");\n const applyMotion = () => {\n for (const animation of scanner?.$overlay?.getAnimations({ subtree: true }) ?? []) {\n if (motion.matches) animation.pause();\n else animation.play();\n }\n };\n applyMotion();\n motion.addEventListener(\"change\", applyMotion);\n cleanupMotion = () => motion.removeEventListener(\"change\", applyMotion);\n await scanner.start();\n if (!closed) setStarting(false);\n } catch {\n await fail();\n }\n })();\n });\n return (\n <div class={props.class ? `k2b-qr-scanner ${props.class}` : \"k2b-qr-scanner\"}>\n <div class=\"k2b-qr-scanner__preview\" data-invalid={invalid() || undefined}>\n {/* Live camera preview contains no prerecorded speech or audio. */}\n <video ref={video} muted playsinline aria-label={messages().qrCameraPreview} />\n </div>\n <p role=\"status\">{starting() ? messages().qrCameraStarting : (props.instructions ?? messages().qrCameraInstructions)}</p>\n <Show when={!starting()}>\n <p class=\"k2b-qr-scanner__note\">{messages().qrCameraPrivacy}</p>\n </Show>\n </div>\n );\n}\n\nexport default QrScanner;\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":[],"sources":["inputs/QrScanner.tsx"],"sourcesContent":["import { timed } from \"@k2b/stdlib/solid\";\nimport type QrScannerEngine from \"qr-scanner\";\nimport { createSignal, type JSX, onCleanup, onMount, Show } from \"solid-js\";\nimport { useUiMessages } from \"../intl/messages\";\n\n/**\n * Why the scanner could not start, as far as the browser tells: `\"denied\"` when camera access is off, `\"no-camera\"`\n * when the device has no camera, `\"in-use\"` when the camera is busy or could not start, most often because another app\n * holds it, and `\"unavailable\"` for anything else, including a scanning engine that cannot load.\n */\nexport type QrScannerError = \"denied\" | \"no-camera\" | \"in-use\" | \"unavailable\";\n\nexport type QrScannerProps = {\n /** Decoded text is untrusted. Return `true` to accept it and stop the camera, `false` to reject it and keep scanning. */\n onResult: (text: string) => boolean;\n /** The page was hidden or left; unmount the scanner. */\n onStop: () => void;\n /**\n * The camera or the scanning engine could not start; the scanner has stopped. Offer another way in. A frame that\n * cannot be decoded never ends here.\n */\n onError: (reason: QrScannerError) => void;\n /** What to point the camera at. Defaults to a generic QR code hint. */\n instructions?: string;\n class?: string;\n};\n\nconst sizes: MediaTrackConstraints[] = [{ width: { min: 1024 } }, { width: { min: 768 } }, {}];\n/** The rear camera at a resolution that keeps small codes readable, then any camera: the order qr-scanner tries. */\nconst cameraRequests: MediaTrackConstraints[] = [...sizes.map((size) => ({ ...size, facingMode: { exact: \"environment\" } })), ...sizes];\n\nconst errorName = (error: unknown): string => (typeof error === \"object\" && error !== null && \"name\" in error ? String(error.name) : \"\");\n\nconst cameraFailure = (name: string): QrScannerError => {\n if (name === \"NotAllowedError\" || name === \"SecurityError\") return \"denied\";\n if (name === \"NotFoundError\" || name === \"OverconstrainedError\") return \"no-camera\";\n if (name === \"NotReadableError\") return \"in-use\";\n return \"unavailable\";\n};\n\n/** The facing a track reports, or else the one its label names, as qr-scanner reads it. */\nconst facingOf = (track: MediaStreamTrack | undefined): string | undefined => {\n if (!track) return undefined;\n const { facingMode } = track.getSettings();\n if (facingMode) return facingMode;\n if (/rear|back|environment/i.test(track.label)) return \"environment\";\n if (/front|user|face/i.test(track.label)) return \"user\";\n return undefined;\n};\n\n/**\n * Opens the camera here rather than in qr-scanner, which turns every failure into \"Camera not found.\". A refusal ends\n * the search, because asking again could show the permission prompt again. Any other failure may belong to the request,\n * such as a camera that cannot start at the larger size, so the next, looser request follows, as in qr-scanner. When\n * none succeeds, a camera that exists but fails outweighs a request no camera could meet. A closed scanner asks no more.\n */\nconst openCamera = async (closed: () => boolean): Promise<{ stream: MediaStream; mirrored: boolean } | { error: QrScannerError }> => {\n if (!navigator.mediaDevices) return { error: \"unavailable\" };\n let error: QrScannerError = \"no-camera\";\n for (const video of cameraRequests) {\n if (closed()) break;\n try {\n const stream = await navigator.mediaDevices.getUserMedia({ video, audio: false });\n const facing = facingOf(stream.getVideoTracks()[0]);\n // Like qr-scanner, mirror a front camera, and a camera of unknown facing that the rear request did not find.\n return { stream, mirrored: facing ? facing === \"user\" : !video.facingMode };\n } catch (failure) {\n const reason = cameraFailure(errorName(failure));\n if (reason === \"denied\") return { error: reason };\n if (reason !== \"no-camera\") error = reason;\n }\n }\n return { error };\n};\n\nconst stopStream = (stream: MediaStream) => {\n for (const track of stream.getTracks()) track.stop();\n};\n\n/**\n * Scans a QR code with the rear camera. Each mount owns exactly one camera session, which stops when the scanner\n * unmounts, the page is hidden, or a result is accepted. The scanning engine loads only when the scanner mounts.\n * Decoded content is never logged.\n */\nexport function QrScanner(props: QrScannerProps): JSX.Element {\n const messages = useUiMessages();\n const [starting, setStarting] = createSignal(true);\n const [invalid, setInvalid] = createSignal(false);\n let lastRejected: string | undefined;\n const resetRejected = timed.debounce(() => {\n lastRejected = undefined;\n }, 1000);\n const resetFeedback = timed.debounce(() => setInvalid(false), 1500);\n let video!: HTMLVideoElement;\n let scanner: QrScannerEngine | undefined;\n let closed = false;\n let cleanupMotion: (() => void) | undefined;\n const destroy = () => {\n closed = true;\n cleanupMotion?.();\n resetRejected.cancel();\n resetFeedback.cancel();\n // pause(true) stops the camera stream immediately.\n void scanner?.pause(true);\n scanner?.destroy();\n scanner = undefined;\n };\n // Once closed, the host has unmounted the scanner or heard why it stopped; later failures stay silent.\n const fail = (reason: QrScannerError) => {\n if (closed) return;\n destroy();\n props.onError(reason);\n };\n onMount(() => {\n const hidden = () => {\n if (document.visibilityState !== \"visible\") props.onStop();\n };\n const leaving = () => props.onStop();\n document.addEventListener(\"visibilitychange\", hidden);\n window.addEventListener(\"pagehide\", leaving);\n onCleanup(() => {\n destroy();\n document.removeEventListener(\"visibilitychange\", hidden);\n window.removeEventListener(\"pagehide\", leaving);\n });\n void (async () => {\n try {\n const { default: Engine } = await import(\"qr-scanner\");\n if (closed || document.visibilityState !== \"visible\") return;\n scanner = new Engine(\n video,\n (result) => {\n if (closed) return;\n // Rearm only after a full second without a decoded QR, not between individual frames.\n resetRejected.debouncedFn();\n if (result.data === lastRejected) return;\n if (props.onResult(result.data)) {\n destroy();\n return;\n }\n lastRejected = result.data;\n setInvalid(true);\n resetFeedback.debouncedFn();\n },\n {\n highlightScanRegion: true,\n // A frame that cannot be decoded never ends the session: the camera runs, and the next frame may read.\n // An engine error other than an empty frame, which the native BarcodeDetector reports as \"Scanner error:\n // No QR code found\", means the detector cannot read on this device. Fall back to the bundled worker for\n // the page through the flag qr-scanner 1.4.2 sets itself for \"not implemented\" and \"service unavailable\";\n // it swaps engines after this frame. Worker errors carry the same prefix; the flag changes nothing there.\n // qr-scanner reports frames only as strings: an Error means the engine itself did not load, such as a\n // decoder chunk gone after a deploy or a worker that cannot start, and no later frame can read. Never log\n // decoded content or frame data.\n onDecodeError: (error) => {\n if (typeof error !== \"string\") return fail(\"unavailable\");\n if (error.startsWith(\"Scanner error: \") && !error.endsWith(Engine.NO_QR_CODE_FOUND)) {\n Reflect.set(Engine, \"_disableBarcodeDetector\", true);\n }\n },\n },\n );\n const motion = matchMedia(\"(prefers-reduced-motion: reduce)\");\n const applyMotion = () => {\n for (const animation of scanner?.$overlay?.getAnimations({ subtree: true }) ?? []) {\n if (motion.matches) animation.pause();\n else animation.play();\n }\n };\n applyMotion();\n motion.addEventListener(\"change\", applyMotion);\n cleanupMotion = () => motion.removeEventListener(\"change\", applyMotion);\n const camera = await openCamera(() => closed);\n if (\"error\" in camera) return fail(camera.error);\n if (closed) return stopStream(camera.stream);\n // qr-scanner plays a stream that is already set instead of opening its own.\n video.srcObject = camera.stream;\n video.style.transform = camera.mirrored ? \"scaleX(-1)\" : \"\";\n await scanner.start();\n if (!closed) setStarting(false);\n } catch {\n fail(\"unavailable\");\n }\n })();\n });\n return (\n <div class={props.class ? `k2b-qr-scanner ${props.class}` : \"k2b-qr-scanner\"}>\n <div class=\"k2b-qr-scanner__preview\" data-invalid={invalid() || undefined}>\n {/* Live camera preview contains no prerecorded speech or audio. */}\n <video ref={video} muted playsinline aria-label={messages().qrCameraPreview} />\n </div>\n <p role=\"status\">{starting() ? messages().qrCameraStarting : (props.instructions ?? messages().qrCameraInstructions)}</p>\n <Show when={!starting()}>\n <p class=\"k2b-qr-scanner__note\">{messages().qrCameraPrivacy}</p>\n </Show>\n </div>\n );\n}\n\nexport default QrScanner;\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -66,6 +66,8 @@ const uiMessages = i18n.define({
|
|
|
66
66
|
hidePassword: "Hide password",
|
|
67
67
|
noColumns: "No columns.",
|
|
68
68
|
noData: "No data.",
|
|
69
|
+
viewRaw: "View raw",
|
|
70
|
+
viewFormatted: "View formatted",
|
|
69
71
|
noRecords: "No records",
|
|
70
72
|
noTagsYet: "No tags yet",
|
|
71
73
|
noResultsFound: "No results found",
|
|
@@ -197,6 +199,42 @@ const uiMessages = i18n.define({
|
|
|
197
199
|
dropFilesToAttach: "Drop files to attach",
|
|
198
200
|
conversation: "Conversation",
|
|
199
201
|
jumpToLatest: "Jump to latest",
|
|
202
|
+
feedJumpToLatest: "Jump to latest",
|
|
203
|
+
feedJumpToLatestCount: ({
|
|
204
|
+
count
|
|
205
|
+
}) => `Jump to latest, ${count} new`,
|
|
206
|
+
feedNewItems: ({
|
|
207
|
+
count
|
|
208
|
+
}) => count === 1 ? "1 new item" : `${count} new items`,
|
|
209
|
+
feedNewMarker: "New",
|
|
210
|
+
messageSent: "Sent",
|
|
211
|
+
messageNotSent: "Not sent",
|
|
212
|
+
messageRetry: "Retry",
|
|
213
|
+
messageShowMore: "Show more",
|
|
214
|
+
messageShowLess: "Show less",
|
|
215
|
+
messageReplyTo: "In reply to",
|
|
216
|
+
messageForwarded: "Forwarded",
|
|
217
|
+
messageEdited: "edited",
|
|
218
|
+
messageDeleted: "This message was deleted",
|
|
219
|
+
messageVideo: "Video",
|
|
220
|
+
messageMoreAttachments: ({
|
|
221
|
+
count
|
|
222
|
+
}) => `${count} more`,
|
|
223
|
+
messageReactions: "Reactions",
|
|
224
|
+
messageReaction: ({
|
|
225
|
+
emoji,
|
|
226
|
+
count,
|
|
227
|
+
names,
|
|
228
|
+
own
|
|
229
|
+
}) => `${emoji} ${own && count === 1 ? "your reaction" : `${count} ${count === 1 ? "reaction" : "reactions"}${own ? ", including yours" : ""}`}${names ? `: ${names}` : ""}`,
|
|
230
|
+
messageAddReaction: "Add reaction",
|
|
231
|
+
messageReplies: ({
|
|
232
|
+
count
|
|
233
|
+
}) => count === 1 ? "1 reply" : `${count} replies`,
|
|
234
|
+
messageLastReply: ({
|
|
235
|
+
time
|
|
236
|
+
}) => `Last reply ${time}`,
|
|
237
|
+
messageStop: "Stop",
|
|
200
238
|
conversationMessages: ({
|
|
201
239
|
label
|
|
202
240
|
}) => `${label} messages`,
|
|
@@ -506,6 +544,11 @@ const uiMessages = i18n.define({
|
|
|
506
544
|
installAppleBrowserDetail: "This browser installs through Share → Add to Home Screen. If that isn’t there, open this page in Safari.",
|
|
507
545
|
installAndroidBrowserTitle: "Chrome works best",
|
|
508
546
|
installAndroidBrowserDetail: "In this browser, look for “Install” or “Add to Home screen” in the menu. If that isn’t there, open this page in Chrome.",
|
|
547
|
+
installSamsungTitle: "Install with Chrome",
|
|
548
|
+
installSamsungDetail: ({
|
|
549
|
+
appName
|
|
550
|
+
}) => `On Samsung phones, Samsung Internet builds its own app package, which Android 14 and later may block as “built for an older version of Android”. That warning is about the package, not about ${appName}. Chrome installs ${appName} without it. Without Chrome, use this browser’s menu and continue through that warning.`,
|
|
551
|
+
installOpenChrome: "Open in Chrome",
|
|
509
552
|
installCopyLink: "Copy link",
|
|
510
553
|
installLinkCopied: "Link copied",
|
|
511
554
|
installCopyFailed: "The link could not be copied. Copy this address manually:",
|
|
@@ -577,6 +620,8 @@ const uiMessages = i18n.define({
|
|
|
577
620
|
hidePassword: "Passwort ausblenden",
|
|
578
621
|
noColumns: "Keine Spalten.",
|
|
579
622
|
noData: "Keine Daten.",
|
|
623
|
+
viewRaw: "Rohdaten anzeigen",
|
|
624
|
+
viewFormatted: "Formatiert anzeigen",
|
|
580
625
|
noRecords: "Keine Einträge",
|
|
581
626
|
noTagsYet: "Noch keine Tags",
|
|
582
627
|
noResultsFound: "Keine Ergebnisse gefunden",
|
|
@@ -708,6 +753,42 @@ const uiMessages = i18n.define({
|
|
|
708
753
|
dropFilesToAttach: "Dateien zum Anhängen ablegen",
|
|
709
754
|
conversation: "Unterhaltung",
|
|
710
755
|
jumpToLatest: "Zur neuesten Nachricht",
|
|
756
|
+
feedJumpToLatest: "Zum Neuesten",
|
|
757
|
+
feedJumpToLatestCount: ({
|
|
758
|
+
count
|
|
759
|
+
}) => `Zum Neuesten, ${count} neu`,
|
|
760
|
+
feedNewItems: ({
|
|
761
|
+
count
|
|
762
|
+
}) => count === 1 ? "1 neuer Eintrag" : `${count} neue Einträge`,
|
|
763
|
+
feedNewMarker: "Neu",
|
|
764
|
+
messageSent: "Gesendet",
|
|
765
|
+
messageNotSent: "Nicht gesendet",
|
|
766
|
+
messageRetry: "Erneut senden",
|
|
767
|
+
messageShowMore: "Mehr anzeigen",
|
|
768
|
+
messageShowLess: "Weniger anzeigen",
|
|
769
|
+
messageReplyTo: "Antwort auf",
|
|
770
|
+
messageForwarded: "Weitergeleitet",
|
|
771
|
+
messageEdited: "bearbeitet",
|
|
772
|
+
messageDeleted: "Diese Nachricht wurde gelöscht",
|
|
773
|
+
messageVideo: "Video",
|
|
774
|
+
messageMoreAttachments: ({
|
|
775
|
+
count
|
|
776
|
+
}) => `${count} weitere`,
|
|
777
|
+
messageReactions: "Reaktionen",
|
|
778
|
+
messageReaction: ({
|
|
779
|
+
emoji,
|
|
780
|
+
count,
|
|
781
|
+
names,
|
|
782
|
+
own
|
|
783
|
+
}) => `${emoji} ${own && count === 1 ? "deine Reaktion" : `${count} ${count === 1 ? "Reaktion" : "Reaktionen"}${own ? ", einschließlich deiner" : ""}`}${names ? `: ${names}` : ""}`,
|
|
784
|
+
messageAddReaction: "Reaktion hinzufügen",
|
|
785
|
+
messageReplies: ({
|
|
786
|
+
count
|
|
787
|
+
}) => count === 1 ? "1 Antwort" : `${count} Antworten`,
|
|
788
|
+
messageLastReply: ({
|
|
789
|
+
time
|
|
790
|
+
}) => `Letzte Antwort ${time}`,
|
|
791
|
+
messageStop: "Stoppen",
|
|
711
792
|
conversationMessages: ({
|
|
712
793
|
label
|
|
713
794
|
}) => `Nachrichten in ${label}`,
|
|
@@ -1017,6 +1098,11 @@ const uiMessages = i18n.define({
|
|
|
1017
1098
|
installAppleBrowserDetail: "In diesem Browser installierst du über Teilen → Zum Home-Bildschirm. Fehlt der Eintrag, öffne diese Seite in Safari.",
|
|
1018
1099
|
installAndroidBrowserTitle: "Am besten mit Chrome",
|
|
1019
1100
|
installAndroidBrowserDetail: "Suche in diesem Browser im Menü nach „Installieren“ oder „Zum Startbildschirm hinzufügen“. Fehlt der Eintrag, öffne diese Seite in Chrome.",
|
|
1101
|
+
installSamsungTitle: "Mit Chrome installieren",
|
|
1102
|
+
installSamsungDetail: ({
|
|
1103
|
+
appName
|
|
1104
|
+
}) => `Auf Samsung-Handys erstellt Samsung Internet ein eigenes App-Paket, das Android ab Version 14 als „für eine ältere Android-Version entwickelt“ blockieren kann. Diese Warnung betrifft das Paket, nicht ${appName}. Mit Chrome installierst du ${appName} ohne sie. Ohne Chrome nutze das Menü dieses Browsers und fahre trotz dieser Warnung fort.`,
|
|
1105
|
+
installOpenChrome: "In Chrome öffnen",
|
|
1020
1106
|
installCopyLink: "Link kopieren",
|
|
1021
1107
|
installLinkCopied: "Link kopiert",
|
|
1022
1108
|
installCopyFailed: "Der Link konnte nicht kopiert werden. Kopiere diese Adresse manuell:",
|