@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
|
@@ -5,22 +5,40 @@ import { ssrAttribute as _$ssrAttribute } from "solid-js/web";
|
|
|
5
5
|
import { escape as _$escape } from "solid-js/web";
|
|
6
6
|
var _tmpl$ = ["<p role=\"alert\">", "</p>"],
|
|
7
7
|
_tmpl$2 = ["<p role=\"status\">", "</p>"],
|
|
8
|
-
_tmpl$3 = ["<
|
|
9
|
-
_tmpl$4 = ["<
|
|
10
|
-
_tmpl$5 = ["<
|
|
11
|
-
_tmpl$6 = ["<
|
|
8
|
+
_tmpl$3 = ["<p>", "</p>"],
|
|
9
|
+
_tmpl$4 = ["<i", " aria-hidden=\"true\"></i>"],
|
|
10
|
+
_tmpl$5 = ["<code>", "</code>"],
|
|
11
|
+
_tmpl$6 = ["<div", ">", "", "</div>"],
|
|
12
12
|
_tmpl$7 = ["<ol class=\"k2b-install-guide__steps\">", "</ol>"],
|
|
13
|
-
_tmpl$8 = ["<li><span class=\"k2b-install-guide__step-icon\"><i", " aria-hidden=\"true\"></i></span><div><h3>", "</h3><p>", "</p></div></li>"]
|
|
13
|
+
_tmpl$8 = ["<li><span class=\"k2b-install-guide__step-icon\"><i", " aria-hidden=\"true\"></i></span><div><h3>", "</h3><p>", "</p></div></li>"],
|
|
14
|
+
_tmpl$9 = "<i class=\"ti ti-brand-chrome\" aria-hidden=\"true\"></i>";
|
|
14
15
|
import { clipboard } from "@k2b/stdlib/solid";
|
|
15
|
-
import { createMemo, For, Show } from "solid-js";
|
|
16
|
-
import { Button } from "../actions/Button";
|
|
16
|
+
import { createMemo, For, Match, Show, Switch } from "solid-js";
|
|
17
|
+
import { Button, ButtonLink } from "../actions/Button";
|
|
17
18
|
import { useUiMessages } from "../intl/messages";
|
|
18
19
|
import { NoticeCard } from "../surfaces/NoticeCard";
|
|
20
|
+
/**
|
|
21
|
+
* Opens `url` in Chrome on Android. Without Chrome, the browser loads `url` itself. Only an absolute http or https
|
|
22
|
+
* address has an intent; anything else returns `undefined`.
|
|
23
|
+
*/
|
|
24
|
+
const chromeIntent = url => {
|
|
25
|
+
let target;
|
|
26
|
+
try {
|
|
27
|
+
target = new URL(url);
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
if (target.protocol !== "https:" && target.protocol !== "http:") return undefined;
|
|
32
|
+
const scheme = target.protocol.slice(0, -1);
|
|
33
|
+
return `intent://${target.host}${target.pathname}${target.search}${target.hash}#Intent;scheme=${scheme};package=com.android.chrome;S.browser_fallback_url=${encodeURIComponent(url)};end`;
|
|
34
|
+
};
|
|
35
|
+
|
|
19
36
|
/**
|
|
20
37
|
* Explains how to install the current page as an app on this platform: the browser's own dialog where it offers
|
|
21
38
|
* one, otherwise the steps for Safari on iPhone, iPad, and Mac, other browsers on iPhone and iPad, Android, and other
|
|
22
39
|
* browsers. A browser inside another app cannot install, so it gets a warning and the link to copy into Safari or
|
|
23
|
-
* Chrome instead.
|
|
40
|
+
* Chrome instead. Samsung Internet gets a link to Chrome even when it offers its own dialog, because Android may block
|
|
41
|
+
* the app package it builds. The host owns the surrounding surface, its heading, and dismissal.
|
|
24
42
|
*/
|
|
25
43
|
export function InstallGuide(props) {
|
|
26
44
|
const messages = useUiMessages();
|
|
@@ -77,108 +95,106 @@ export function InstallGuide(props) {
|
|
|
77
95
|
detail: state().platform === "android" || state().platform === "android-browser" ? t.installAndroidDetail : t.installBrowserDetail
|
|
78
96
|
}];
|
|
79
97
|
});
|
|
80
|
-
return _$ssr(_tmpl$
|
|
98
|
+
return _$ssr(_tmpl$6, _$ssrAttribute("class", props.class ? `k2b-install-guide ${_$escape(props.class, true)}` : "k2b-install-guide", false) + _$ssrAttribute("data-platform", _$escape(state().platform, true), false), _$escape(_$createComponent(Show, {
|
|
81
99
|
get when() {
|
|
82
100
|
return state().failed();
|
|
83
101
|
},
|
|
84
102
|
get children() {
|
|
85
103
|
return _$ssr(_tmpl$, _$escape(messages().installFailed));
|
|
86
104
|
}
|
|
87
|
-
})), _$escape(_$createComponent(
|
|
88
|
-
get when() {
|
|
89
|
-
return state().requested();
|
|
90
|
-
},
|
|
105
|
+
})), _$escape(_$createComponent(Switch, {
|
|
91
106
|
get fallback() {
|
|
92
|
-
return _$createComponent(Show, {
|
|
107
|
+
return [_$createComponent(Show, {
|
|
93
108
|
get when() {
|
|
94
|
-
return
|
|
109
|
+
return hint();
|
|
110
|
+
},
|
|
111
|
+
children: hint => _$createComponent(NoticeCard, {
|
|
112
|
+
tone: "info",
|
|
113
|
+
get title() {
|
|
114
|
+
return hint().title;
|
|
115
|
+
},
|
|
116
|
+
get detail() {
|
|
117
|
+
return hint().detail;
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
}), _$ssr(_tmpl$7, _$escape(_$createComponent(For, {
|
|
121
|
+
get each() {
|
|
122
|
+
return steps();
|
|
123
|
+
},
|
|
124
|
+
children: step => _$ssr(_tmpl$8, _$ssrAttribute("class", _$escape(step.icon, true), false), _$escape(step.title), _$escape(step.detail))
|
|
125
|
+
}))), _$createComponent(Show, {
|
|
126
|
+
get when() {
|
|
127
|
+
return state().platform === "apple-mobile";
|
|
128
|
+
},
|
|
129
|
+
get children() {
|
|
130
|
+
return _$ssr(_tmpl$3, _$escape(messages().installSafariFallback));
|
|
131
|
+
}
|
|
132
|
+
}), _$createComponent(Show, {
|
|
133
|
+
get when() {
|
|
134
|
+
return props.note;
|
|
135
|
+
},
|
|
136
|
+
get children() {
|
|
137
|
+
return _$ssr(_tmpl$3, _$escape(props.note));
|
|
138
|
+
}
|
|
139
|
+
}), _$createComponent(Show, {
|
|
140
|
+
get when() {
|
|
141
|
+
return state().platform === "generic";
|
|
142
|
+
},
|
|
143
|
+
get children() {
|
|
144
|
+
return _$ssr(_tmpl$3, _$escape(messages().installUnavailable));
|
|
145
|
+
}
|
|
146
|
+
})];
|
|
147
|
+
},
|
|
148
|
+
get children() {
|
|
149
|
+
return [_$createComponent(Match, {
|
|
150
|
+
get when() {
|
|
151
|
+
return state().requested();
|
|
152
|
+
},
|
|
153
|
+
get children() {
|
|
154
|
+
return _$ssr(_tmpl$2, _$escape(messages().installRequested));
|
|
155
|
+
}
|
|
156
|
+
}), _$createComponent(Match, {
|
|
157
|
+
get when() {
|
|
158
|
+
return state().platform === "android-samsung";
|
|
95
159
|
},
|
|
96
|
-
get
|
|
97
|
-
return _$createComponent(
|
|
160
|
+
get children() {
|
|
161
|
+
return [_$createComponent(NoticeCard, {
|
|
162
|
+
tone: "info",
|
|
163
|
+
get title() {
|
|
164
|
+
return messages().installSamsungTitle;
|
|
165
|
+
},
|
|
166
|
+
get detail() {
|
|
167
|
+
return messages().installSamsungDetail({
|
|
168
|
+
appName: props.appName
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}), _$createComponent(Show, {
|
|
98
172
|
get when() {
|
|
99
|
-
return
|
|
173
|
+
return chromeIntent(props.url);
|
|
100
174
|
},
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return hint().detail;
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
}), _$ssr(_tmpl$7, _$escape(_$createComponent(For, {
|
|
116
|
-
get each() {
|
|
117
|
-
return steps();
|
|
118
|
-
},
|
|
119
|
-
children: step => _$ssr(_tmpl$8, _$ssrAttribute("class", _$escape(step.icon, true), false), _$escape(step.title), _$escape(step.detail))
|
|
120
|
-
}))), _$createComponent(Show, {
|
|
121
|
-
get when() {
|
|
122
|
-
return state().platform === "apple-mobile";
|
|
123
|
-
},
|
|
124
|
-
get children() {
|
|
125
|
-
return _$ssr(_tmpl$4, _$escape(messages().installSafariFallback));
|
|
126
|
-
}
|
|
127
|
-
}), _$createComponent(Show, {
|
|
128
|
-
get when() {
|
|
129
|
-
return props.note;
|
|
130
|
-
},
|
|
131
|
-
get children() {
|
|
132
|
-
return _$ssr(_tmpl$4, _$escape(props.note));
|
|
133
|
-
}
|
|
134
|
-
}), _$createComponent(Show, {
|
|
135
|
-
get when() {
|
|
136
|
-
return state().platform === "generic";
|
|
137
|
-
},
|
|
138
|
-
get children() {
|
|
139
|
-
return _$ssr(_tmpl$4, _$escape(messages().installUnavailable));
|
|
140
|
-
}
|
|
141
|
-
})];
|
|
175
|
+
children: href => _$createComponent(ButtonLink, {
|
|
176
|
+
get href() {
|
|
177
|
+
return href();
|
|
178
|
+
},
|
|
179
|
+
get children() {
|
|
180
|
+
return [_$ssr(_tmpl$9), messages().installOpenChrome];
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
}), _$createComponent(Show, {
|
|
184
|
+
get when() {
|
|
185
|
+
return props.note;
|
|
142
186
|
},
|
|
143
187
|
get children() {
|
|
144
|
-
return
|
|
145
|
-
tone: "warning",
|
|
146
|
-
get title() {
|
|
147
|
-
return _$memo(() => state().platform === "apple-in-app")() ? messages().installAppleInAppTitle : messages().installInAppTitle;
|
|
148
|
-
},
|
|
149
|
-
get detail() {
|
|
150
|
-
return _$memo(() => state().platform === "apple-in-app")() ? messages().installAppleInAppDetail({
|
|
151
|
-
appName: props.appName
|
|
152
|
-
}) : messages().installInAppDetail({
|
|
153
|
-
appName: props.appName
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}), _$createComponent(Button, {
|
|
157
|
-
variant: "secondary",
|
|
158
|
-
onClick: () => void copy.copy(props.url),
|
|
159
|
-
get children() {
|
|
160
|
-
return [_$ssr(_tmpl$5, _$ssrAttribute("class", copy.wasCopied() ? "ti ti-check" : "ti ti-copy", false)), copy.wasCopied() ? messages().installLinkCopied : messages().installCopyLink];
|
|
161
|
-
}
|
|
162
|
-
}), _$createComponent(Show, {
|
|
163
|
-
get when() {
|
|
164
|
-
return copy.error();
|
|
165
|
-
},
|
|
166
|
-
get children() {
|
|
167
|
-
return [_$ssr(_tmpl$, _$escape(messages().installCopyFailed)), _$ssr(_tmpl$6, _$escape(props.url))];
|
|
168
|
-
}
|
|
169
|
-
}), _$createComponent(Show, {
|
|
170
|
-
get when() {
|
|
171
|
-
return props.note;
|
|
172
|
-
},
|
|
173
|
-
get children() {
|
|
174
|
-
return _$ssr(_tmpl$4, _$escape(props.note));
|
|
175
|
-
}
|
|
176
|
-
})];
|
|
188
|
+
return _$ssr(_tmpl$3, _$escape(props.note));
|
|
177
189
|
}
|
|
178
|
-
});
|
|
190
|
+
})];
|
|
191
|
+
}
|
|
192
|
+
}), _$createComponent(Match, {
|
|
193
|
+
get when() {
|
|
194
|
+
return state().canPrompt() || state().busy();
|
|
179
195
|
},
|
|
180
196
|
get children() {
|
|
181
|
-
return [_$ssr(_tmpl$
|
|
197
|
+
return [_$ssr(_tmpl$3, _$escape(messages().installNative({
|
|
182
198
|
appName: props.appName
|
|
183
199
|
}))), _$createComponent(Button, {
|
|
184
200
|
get loading() {
|
|
@@ -190,10 +206,46 @@ export function InstallGuide(props) {
|
|
|
190
206
|
}
|
|
191
207
|
})];
|
|
192
208
|
}
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
209
|
+
}), _$createComponent(Match, {
|
|
210
|
+
get when() {
|
|
211
|
+
return embedded();
|
|
212
|
+
},
|
|
213
|
+
get children() {
|
|
214
|
+
return [_$createComponent(NoticeCard, {
|
|
215
|
+
tone: "warning",
|
|
216
|
+
get title() {
|
|
217
|
+
return _$memo(() => state().platform === "apple-in-app")() ? messages().installAppleInAppTitle : messages().installInAppTitle;
|
|
218
|
+
},
|
|
219
|
+
get detail() {
|
|
220
|
+
return _$memo(() => state().platform === "apple-in-app")() ? messages().installAppleInAppDetail({
|
|
221
|
+
appName: props.appName
|
|
222
|
+
}) : messages().installInAppDetail({
|
|
223
|
+
appName: props.appName
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}), _$createComponent(Button, {
|
|
227
|
+
variant: "secondary",
|
|
228
|
+
onClick: () => void copy.copy(props.url),
|
|
229
|
+
get children() {
|
|
230
|
+
return [_$ssr(_tmpl$4, _$ssrAttribute("class", copy.wasCopied() ? "ti ti-check" : "ti ti-copy", false)), copy.wasCopied() ? messages().installLinkCopied : messages().installCopyLink];
|
|
231
|
+
}
|
|
232
|
+
}), _$createComponent(Show, {
|
|
233
|
+
get when() {
|
|
234
|
+
return copy.error();
|
|
235
|
+
},
|
|
236
|
+
get children() {
|
|
237
|
+
return [_$ssr(_tmpl$, _$escape(messages().installCopyFailed)), _$ssr(_tmpl$5, _$escape(props.url))];
|
|
238
|
+
}
|
|
239
|
+
}), _$createComponent(Show, {
|
|
240
|
+
get when() {
|
|
241
|
+
return props.note;
|
|
242
|
+
},
|
|
243
|
+
get children() {
|
|
244
|
+
return _$ssr(_tmpl$3, _$escape(props.note));
|
|
245
|
+
}
|
|
246
|
+
})];
|
|
247
|
+
}
|
|
248
|
+
})];
|
|
197
249
|
}
|
|
198
250
|
})));
|
|
199
251
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sources":["feedback/InstallGuide.tsx"],"sourcesContent":["import { clipboard } from \"@k2b/stdlib/solid\";\nimport { createMemo, For, type JSX, Show } from \"solid-js\";\nimport { Button } from \"../actions/Button\";\nimport { useUiMessages } from \"../intl/messages\";\nimport { NoticeCard } from \"../surfaces/NoticeCard\";\nimport type { InstallPrompt } from \"./install\";\n\nexport type InstallGuideProps = {\n /** The name people see on their Home Screen, used in the guidance text. */\n appName: string;\n install: InstallPrompt;\n /**\n * The address a browser inside another app copies, to open it in Safari or Chrome. Shown as text when copying\n * fails, so it can be copied by hand.\n */\n url: string;\n /**\n * An application note under the installation steps or the embedded browser's link, for example what else the\n * installed app enables.\n */\n note?: string;\n class?: string;\n};\n\n/**\n * Explains how to install the current page as an app on this platform: the browser's own dialog where it offers\n * one, otherwise the steps for Safari on iPhone, iPad, and Mac, other browsers on iPhone and iPad, Android, and other\n * browsers. A browser inside another app cannot install, so it gets a warning and the link to copy into Safari or\n * Chrome instead. The host owns the surrounding surface, its heading, and dismissal.\n */\nexport function InstallGuide(props: InstallGuideProps): JSX.Element {\n const messages = useUiMessages();\n // The host may hand over another state after mount, such as the browser's own in place of the server's view.\n const state = () => props.install;\n const copy = clipboard.createWriter({ write: (url: string) => navigator.clipboard.writeText(url) });\n const embedded = () => state().platform === \"in-app\" || state().platform === \"apple-in-app\";\n /** A browser that installs, but less reliably than the platform's own. */\n const hint = createMemo(() => {\n const t = messages();\n if (state().platform === \"apple-browser\") return { title: t.installAppleBrowserTitle, detail: t.installAppleBrowserDetail };\n if (state().platform === \"android-browser\") return { title: t.installAndroidBrowserTitle, detail: t.installAndroidBrowserDetail };\n return undefined;\n });\n const steps = createMemo(() => {\n const t = messages();\n if (state().platform === \"apple-mobile\" || state().platform === \"apple-browser\")\n return [\n {\n icon: \"ti ti-share-2\",\n title: t.installOpenShare,\n detail: state().platform === \"apple-browser\" ? t.installOpenShareBrowserDetail : t.installOpenShareDetail,\n },\n { icon: \"ti ti-square-plus\", title: t.installHomeScreen, detail: t.installHomeScreenDetail },\n { icon: \"ti ti-check\", title: t.installConfirmAdd, detail: t.installConfirmAddDetail },\n ];\n if (state().platform === \"apple-desktop\")\n return [\n { icon: \"ti ti-share-2\", title: t.installSafariMenu, detail: t.installSafariMenuDetail },\n { icon: \"ti ti-app-window\", title: t.installAddDock, detail: t.installAddDockDetail },\n ];\n return [\n { icon: \"ti ti-dots\", title: t.installBrowserMenu, detail: t.installBrowserMenuDetail },\n {\n icon: \"ti ti-square-plus\",\n title: t.installApp,\n detail: state().platform === \"android\" || state().platform === \"android-browser\" ? t.installAndroidDetail : t.installBrowserDetail,\n },\n ];\n });\n return (\n <div class={props.class ? `k2b-install-guide ${props.class}` : \"k2b-install-guide\"} data-platform={state().platform}>\n <Show when={state().failed()}>\n <p role=\"alert\">{messages().installFailed}</p>\n </Show>\n <Show\n when={state().requested()}\n fallback={\n <Show\n when={state().canPrompt() || state().busy()}\n fallback={\n <Show\n when={embedded()}\n fallback={\n <>\n <Show when={hint()}>{(hint) => <NoticeCard tone=\"info\" title={hint().title} detail={hint().detail} />}</Show>\n <ol class=\"k2b-install-guide__steps\">\n <For each={steps()}>\n {(step) => (\n <li>\n <span class=\"k2b-install-guide__step-icon\">\n <i class={step.icon} aria-hidden=\"true\" />\n </span>\n <div>\n <h3>{step.title}</h3>\n <p>{step.detail}</p>\n </div>\n </li>\n )}\n </For>\n </ol>\n <Show when={state().platform === \"apple-mobile\"}>\n <p>{messages().installSafariFallback}</p>\n </Show>\n <Show when={props.note}>\n <p>{props.note}</p>\n </Show>\n <Show when={state().platform === \"generic\"}>\n <p>{messages().installUnavailable}</p>\n </Show>\n </>\n }\n >\n <NoticeCard\n tone=\"warning\"\n title={state().platform === \"apple-in-app\" ? messages().installAppleInAppTitle : messages().installInAppTitle}\n detail={\n state().platform === \"apple-in-app\"\n ? messages().installAppleInAppDetail({ appName: props.appName })\n : messages().installInAppDetail({ appName: props.appName })\n }\n />\n <Button variant=\"secondary\" onClick={() => void copy.copy(props.url)}>\n <i class={copy.wasCopied() ? \"ti ti-check\" : \"ti ti-copy\"} aria-hidden=\"true\" />\n {copy.wasCopied() ? messages().installLinkCopied : messages().installCopyLink}\n </Button>\n <Show when={copy.error()}>\n <p role=\"alert\">{messages().installCopyFailed}</p>\n <code>{props.url}</code>\n </Show>\n <Show when={props.note}>\n <p>{props.note}</p>\n </Show>\n </Show>\n }\n >\n <p>{messages().installNative({ appName: props.appName })}</p>\n <Button loading={state().busy()} onClick={() => void state().install()}>\n {messages().installApp}\n </Button>\n </Show>\n }\n >\n <p role=\"status\">{messages().installRequested}</p>\n </Show>\n </div>\n );\n}\n\nexport default InstallGuide;\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":[],"sources":["feedback/InstallGuide.tsx"],"sourcesContent":["import { clipboard } from \"@k2b/stdlib/solid\";\nimport { createMemo, For, type JSX, Match, Show, Switch } from \"solid-js\";\nimport { Button, ButtonLink } from \"../actions/Button\";\nimport { useUiMessages } from \"../intl/messages\";\nimport { NoticeCard } from \"../surfaces/NoticeCard\";\nimport type { InstallPrompt } from \"./install\";\n\nexport type InstallGuideProps = {\n /** The name people see on their Home Screen, used in the guidance text. */\n appName: string;\n install: InstallPrompt;\n /**\n * The absolute address of the page to install. A browser inside another app copies it, to open it in Safari or\n * Chrome, and shows it as text when copying fails, so it can be copied by hand. Samsung Internet opens an http or\n * https address in Chrome.\n */\n url: string;\n /**\n * An application note under the installation steps or the embedded browser's link, for example what else the\n * installed app enables.\n */\n note?: string;\n class?: string;\n};\n\n/**\n * Opens `url` in Chrome on Android. Without Chrome, the browser loads `url` itself. Only an absolute http or https\n * address has an intent; anything else returns `undefined`.\n */\nconst chromeIntent = (url: string) => {\n let target: URL;\n try {\n target = new URL(url);\n } catch {\n return undefined;\n }\n if (target.protocol !== \"https:\" && target.protocol !== \"http:\") return undefined;\n const scheme = target.protocol.slice(0, -1);\n return `intent://${target.host}${target.pathname}${target.search}${target.hash}#Intent;scheme=${scheme};package=com.android.chrome;S.browser_fallback_url=${encodeURIComponent(url)};end`;\n};\n\n/**\n * Explains how to install the current page as an app on this platform: the browser's own dialog where it offers\n * one, otherwise the steps for Safari on iPhone, iPad, and Mac, other browsers on iPhone and iPad, Android, and other\n * browsers. A browser inside another app cannot install, so it gets a warning and the link to copy into Safari or\n * Chrome instead. Samsung Internet gets a link to Chrome even when it offers its own dialog, because Android may block\n * the app package it builds. The host owns the surrounding surface, its heading, and dismissal.\n */\nexport function InstallGuide(props: InstallGuideProps): JSX.Element {\n const messages = useUiMessages();\n // The host may hand over another state after mount, such as the browser's own in place of the server's view.\n const state = () => props.install;\n const copy = clipboard.createWriter({ write: (url: string) => navigator.clipboard.writeText(url) });\n const embedded = () => state().platform === \"in-app\" || state().platform === \"apple-in-app\";\n /** A browser that installs, but less reliably than the platform's own. */\n const hint = createMemo(() => {\n const t = messages();\n if (state().platform === \"apple-browser\") return { title: t.installAppleBrowserTitle, detail: t.installAppleBrowserDetail };\n if (state().platform === \"android-browser\") return { title: t.installAndroidBrowserTitle, detail: t.installAndroidBrowserDetail };\n return undefined;\n });\n const steps = createMemo(() => {\n const t = messages();\n if (state().platform === \"apple-mobile\" || state().platform === \"apple-browser\")\n return [\n {\n icon: \"ti ti-share-2\",\n title: t.installOpenShare,\n detail: state().platform === \"apple-browser\" ? t.installOpenShareBrowserDetail : t.installOpenShareDetail,\n },\n { icon: \"ti ti-square-plus\", title: t.installHomeScreen, detail: t.installHomeScreenDetail },\n { icon: \"ti ti-check\", title: t.installConfirmAdd, detail: t.installConfirmAddDetail },\n ];\n if (state().platform === \"apple-desktop\")\n return [\n { icon: \"ti ti-share-2\", title: t.installSafariMenu, detail: t.installSafariMenuDetail },\n { icon: \"ti ti-app-window\", title: t.installAddDock, detail: t.installAddDockDetail },\n ];\n return [\n { icon: \"ti ti-dots\", title: t.installBrowserMenu, detail: t.installBrowserMenuDetail },\n {\n icon: \"ti ti-square-plus\",\n title: t.installApp,\n detail: state().platform === \"android\" || state().platform === \"android-browser\" ? t.installAndroidDetail : t.installBrowserDetail,\n },\n ];\n });\n return (\n <div class={props.class ? `k2b-install-guide ${props.class}` : \"k2b-install-guide\"} data-platform={state().platform}>\n <Show when={state().failed()}>\n <p role=\"alert\">{messages().installFailed}</p>\n </Show>\n <Switch\n fallback={\n <>\n <Show when={hint()}>{(hint) => <NoticeCard tone=\"info\" title={hint().title} detail={hint().detail} />}</Show>\n <ol class=\"k2b-install-guide__steps\">\n <For each={steps()}>\n {(step) => (\n <li>\n <span class=\"k2b-install-guide__step-icon\">\n <i class={step.icon} aria-hidden=\"true\" />\n </span>\n <div>\n <h3>{step.title}</h3>\n <p>{step.detail}</p>\n </div>\n </li>\n )}\n </For>\n </ol>\n <Show when={state().platform === \"apple-mobile\"}>\n <p>{messages().installSafariFallback}</p>\n </Show>\n <Show when={props.note}>\n <p>{props.note}</p>\n </Show>\n <Show when={state().platform === \"generic\"}>\n <p>{messages().installUnavailable}</p>\n </Show>\n </>\n }\n >\n <Match when={state().requested()}>\n <p role=\"status\">{messages().installRequested}</p>\n </Match>\n <Match when={state().platform === \"android-samsung\"}>\n <NoticeCard\n tone=\"info\"\n title={messages().installSamsungTitle}\n detail={messages().installSamsungDetail({ appName: props.appName })}\n />\n <Show when={chromeIntent(props.url)}>\n {(href) => (\n <ButtonLink href={href()}>\n <i class=\"ti ti-brand-chrome\" aria-hidden=\"true\" />\n {messages().installOpenChrome}\n </ButtonLink>\n )}\n </Show>\n <Show when={props.note}>\n <p>{props.note}</p>\n </Show>\n </Match>\n <Match when={state().canPrompt() || state().busy()}>\n <p>{messages().installNative({ appName: props.appName })}</p>\n <Button loading={state().busy()} onClick={() => void state().install()}>\n {messages().installApp}\n </Button>\n </Match>\n <Match when={embedded()}>\n <NoticeCard\n tone=\"warning\"\n title={state().platform === \"apple-in-app\" ? messages().installAppleInAppTitle : messages().installInAppTitle}\n detail={\n state().platform === \"apple-in-app\"\n ? messages().installAppleInAppDetail({ appName: props.appName })\n : messages().installInAppDetail({ appName: props.appName })\n }\n />\n <Button variant=\"secondary\" onClick={() => void copy.copy(props.url)}>\n <i class={copy.wasCopied() ? \"ti ti-check\" : \"ti ti-copy\"} aria-hidden=\"true\" />\n {copy.wasCopied() ? messages().installLinkCopied : messages().installCopyLink}\n </Button>\n <Show when={copy.error()}>\n <p role=\"alert\">{messages().installCopyFailed}</p>\n <code>{props.url}</code>\n </Show>\n <Show when={props.note}>\n <p>{props.note}</p>\n </Show>\n </Match>\n </Switch>\n </div>\n );\n}\n\nexport default InstallGuide;\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -3,8 +3,9 @@ import { createSignal, onCleanup } from "solid-js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Which installation steps a browser needs. `apple-in-app` and `in-app` are browsers embedded in another app, on
|
|
5
5
|
* iPhone or iPad and elsewhere, which cannot install at all. `apple-browser` is Chrome, Firefox, Edge, or another
|
|
6
|
-
* browser on iPhone or iPad, which installs through its Share menu but less reliably than Safari. `android-
|
|
7
|
-
*
|
|
6
|
+
* browser on iPhone or iPad, which installs through its Share menu but less reliably than Safari. `android-samsung` is
|
|
7
|
+
* Samsung Internet on Android, whose installed apps Android may block as built for an older Android version.
|
|
8
|
+
* `android-browser` is another Android browser, such as Firefox or Edge.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
/** Tokens of apps that open links in their own browser view. Case-sensitive, so `Line/` matches only LINE. */
|
|
@@ -12,7 +13,7 @@ const EMBEDDED = /FBAN|FBAV|FB_IAB|Instagram|LinkedInApp|Twitter|Snapchat|musica
|
|
|
12
13
|
/** Browsers on iPhone and iPad other than Safari. */
|
|
13
14
|
const APPLE_BROWSER = /CriOS\/|FxiOS\/|EdgiOS\/|OPiOS\/|OPT\/|Ddg\/|DuckDuckGo\/|YaBrowser\//;
|
|
14
15
|
/** Android browsers whose user agent also names Chrome. */
|
|
15
|
-
const ANDROID_OTHER = /
|
|
16
|
+
const ANDROID_OTHER = /EdgA\/|OPR\/|YaBrowser|Firefox\//;
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Classifies the browser from its user agent; `platform` and `touchPoints` only tell an iPad that reports a Mac apart.
|
|
@@ -29,7 +30,10 @@ export function installationPlatform(userAgent, platform, touchPoints) {
|
|
|
29
30
|
}
|
|
30
31
|
if (EMBEDDED.test(userAgent)) return "in-app";
|
|
31
32
|
if (/Macintosh/.test(userAgent) && /Safari/.test(userAgent) && !/Chrome|Chromium|Edg\//.test(userAgent)) return "apple-desktop";
|
|
32
|
-
if (/Android/.test(userAgent))
|
|
33
|
+
if (/Android/.test(userAgent)) {
|
|
34
|
+
if (/SamsungBrowser\//.test(userAgent)) return "android-samsung";
|
|
35
|
+
return /Chrome\//.test(userAgent) && !ANDROID_OTHER.test(userAgent) ? "android" : "android-browser";
|
|
36
|
+
}
|
|
33
37
|
return "generic";
|
|
34
38
|
}
|
|
35
39
|
function isInstallPrompt(event) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sources":["feedback/install.ts"],"sourcesContent":["import { createSignal, onCleanup } from \"solid-js\";\n\n/**\n * Which installation steps a browser needs. `apple-in-app` and `in-app` are browsers embedded in another app, on\n * iPhone or iPad and elsewhere, which cannot install at all. `apple-browser` is Chrome, Firefox, Edge, or another\n * browser on iPhone or iPad, which installs through its Share menu but less reliably than Safari. `android-
|
|
1
|
+
{"version":3,"names":[],"sources":["feedback/install.ts"],"sourcesContent":["import { createSignal, onCleanup } from \"solid-js\";\n\n/**\n * Which installation steps a browser needs. `apple-in-app` and `in-app` are browsers embedded in another app, on\n * iPhone or iPad and elsewhere, which cannot install at all. `apple-browser` is Chrome, Firefox, Edge, or another\n * browser on iPhone or iPad, which installs through its Share menu but less reliably than Safari. `android-samsung` is\n * Samsung Internet on Android, whose installed apps Android may block as built for an older Android version.\n * `android-browser` is another Android browser, such as Firefox or Edge.\n */\nexport type InstallationPlatform =\n | \"in-app\"\n | \"apple-in-app\"\n | \"apple-browser\"\n | \"apple-mobile\"\n | \"apple-desktop\"\n | \"android-samsung\"\n | \"android-browser\"\n | \"android\"\n | \"generic\";\n\n/** Tokens of apps that open links in their own browser view. Case-sensitive, so `Line/` matches only LINE. */\nconst EMBEDDED =\n /FBAN|FBAV|FB_IAB|Instagram|LinkedInApp|Twitter|Snapchat|musical_ly|BytedanceWebview|Pinterest|MicroMessenger|\\bLine\\/|GSA\\/|; wv\\)/;\n/** Browsers on iPhone and iPad other than Safari. */\nconst APPLE_BROWSER = /CriOS\\/|FxiOS\\/|EdgiOS\\/|OPiOS\\/|OPT\\/|Ddg\\/|DuckDuckGo\\/|YaBrowser\\//;\n/** Android browsers whose user agent also names Chrome. */\nconst ANDROID_OTHER = /EdgA\\/|OPR\\/|YaBrowser|Firefox\\//;\n\n/**\n * Classifies the browser from its user agent; `platform` and `touchPoints` only tell an iPad that reports a Mac apart.\n * A pure function, so the server and the browser compute the same value from the same user agent. Not a device\n * check: an installed app on iPhone may itself look like an embedded browser.\n */\nexport function installationPlatform(userAgent: string, platform: string, touchPoints: number): InstallationPlatform {\n if (/iPad|iPhone|iPod/.test(userAgent) || (platform === \"MacIntel\" && touchPoints > 1)) {\n if (EMBEDDED.test(userAgent)) return \"apple-in-app\";\n if (APPLE_BROWSER.test(userAgent)) return \"apple-browser\";\n // Safari and every real browser send `Safari/`; a bare web view inside an app, such as a QR scanner, does not.\n if (!/Safari\\//.test(userAgent)) return \"apple-in-app\";\n return \"apple-mobile\";\n }\n if (EMBEDDED.test(userAgent)) return \"in-app\";\n if (/Macintosh/.test(userAgent) && /Safari/.test(userAgent) && !/Chrome|Chromium|Edg\\//.test(userAgent)) return \"apple-desktop\";\n if (/Android/.test(userAgent)) {\n if (/SamsungBrowser\\//.test(userAgent)) return \"android-samsung\";\n return /Chrome\\//.test(userAgent) && !ANDROID_OTHER.test(userAgent) ? \"android\" : \"android-browser\";\n }\n return \"generic\";\n}\n\ninterface BeforeInstallPromptEvent extends Event {\n prompt(): Promise<{ outcome: \"accepted\" | \"dismissed\" }>;\n}\n\nfunction isInstallPrompt(event: Event): event is BeforeInstallPromptEvent {\n return \"prompt\" in event && typeof event.prompt === \"function\";\n}\n\nexport type InstallPrompt = {\n platform: InstallationPlatform;\n /** Running from the Home Screen or as an installed app, or installed during this visit. */\n installed: () => boolean;\n /** The browser offered its own installation dialog, which `install()` opens. */\n canPrompt: () => boolean;\n busy: () => boolean;\n /** The person accepted the browser's dialog; the browser finishes the installation. */\n requested: () => boolean;\n /** The browser's dialog could not be opened. */\n failed: () => boolean;\n /** Opens the browser's dialog once. Call it directly from a click. */\n install: () => Promise<void>;\n};\n\n/**\n * Tracks whether the page runs installed and captures the browser's installation prompt. Browser-only; call it once,\n * when the page loads, under an owner that lives as long as the page: Chrome offers its prompt only once, early.\n */\nexport function createInstallPrompt(): InstallPrompt {\n const displayMode = matchMedia(\"(display-mode: standalone)\");\n const isStandalone = () => displayMode.matches || (\"standalone\" in navigator && navigator.standalone === true);\n const [installed, setInstalled] = createSignal(isStandalone());\n const [prompt, setPrompt] = createSignal<BeforeInstallPromptEvent>();\n const [busy, setBusy] = createSignal(false);\n const [requested, setRequested] = createSignal(false);\n const [failed, setFailed] = createSignal(false);\n const capture = (event: Event) => {\n if (!isInstallPrompt(event)) return;\n event.preventDefault();\n if (installed()) return;\n setPrompt(event);\n setRequested(false);\n };\n const complete = () => {\n setInstalled(true);\n setPrompt(undefined);\n };\n const displayChanged = () => {\n if (isStandalone()) complete();\n };\n window.addEventListener(\"beforeinstallprompt\", capture);\n window.addEventListener(\"appinstalled\", complete);\n displayMode.addEventListener(\"change\", displayChanged);\n onCleanup(() => {\n window.removeEventListener(\"beforeinstallprompt\", capture);\n window.removeEventListener(\"appinstalled\", complete);\n displayMode.removeEventListener(\"change\", displayChanged);\n });\n const install = async () => {\n if (busy() || installed()) return;\n const event = prompt();\n setFailed(false);\n if (!event) return;\n // Consume once, and invoke synchronously within the user's click.\n setPrompt(undefined);\n setBusy(true);\n try {\n const result = await event.prompt();\n if (!installed()) setRequested(result.outcome === \"accepted\");\n } catch {\n setFailed(true);\n } finally {\n setBusy(false);\n }\n };\n return {\n platform: installationPlatform(navigator.userAgent, navigator.platform, navigator.maxTouchPoints),\n installed,\n canPrompt: () => Boolean(prompt()),\n busy,\n requested,\n failed,\n install,\n };\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -7,18 +7,87 @@ var _tmpl$ = ["<p class=\"k2b-qr-scanner__note\">", "</p>"],
|
|
|
7
7
|
import { timed } from "@k2b/stdlib/solid";
|
|
8
8
|
import { createSignal, onCleanup, onMount, Show } from "solid-js";
|
|
9
9
|
import { useUiMessages } from "../intl/messages";
|
|
10
|
+
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* Why the scanner could not start, as far as the browser tells: `"denied"` when camera access is off, `"no-camera"`
|
|
13
|
+
* when the device has no camera, `"in-use"` when the camera is busy or could not start, most often because another app
|
|
14
|
+
* holds it, and `"unavailable"` for anything else, including a scanning engine that cannot load.
|
|
13
15
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
|
|
17
|
+
const sizes = [{
|
|
18
|
+
width: {
|
|
19
|
+
min: 1024
|
|
20
|
+
}
|
|
21
|
+
}, {
|
|
22
|
+
width: {
|
|
23
|
+
min: 768
|
|
24
|
+
}
|
|
25
|
+
}, {}];
|
|
26
|
+
/** The rear camera at a resolution that keeps small codes readable, then any camera: the order qr-scanner tries. */
|
|
27
|
+
const cameraRequests = [...sizes.map(size => ({
|
|
28
|
+
...size,
|
|
29
|
+
facingMode: {
|
|
30
|
+
exact: "environment"
|
|
21
31
|
}
|
|
32
|
+
})), ...sizes];
|
|
33
|
+
const errorName = error => typeof error === "object" && error !== null && "name" in error ? String(error.name) : "";
|
|
34
|
+
const cameraFailure = name => {
|
|
35
|
+
if (name === "NotAllowedError" || name === "SecurityError") return "denied";
|
|
36
|
+
if (name === "NotFoundError" || name === "OverconstrainedError") return "no-camera";
|
|
37
|
+
if (name === "NotReadableError") return "in-use";
|
|
38
|
+
return "unavailable";
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** The facing a track reports, or else the one its label names, as qr-scanner reads it. */
|
|
42
|
+
const facingOf = track => {
|
|
43
|
+
if (!track) return undefined;
|
|
44
|
+
const {
|
|
45
|
+
facingMode
|
|
46
|
+
} = track.getSettings();
|
|
47
|
+
if (facingMode) return facingMode;
|
|
48
|
+
if (/rear|back|environment/i.test(track.label)) return "environment";
|
|
49
|
+
if (/front|user|face/i.test(track.label)) return "user";
|
|
50
|
+
return undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Opens the camera here rather than in qr-scanner, which turns every failure into "Camera not found.". A refusal ends
|
|
55
|
+
* the search, because asking again could show the permission prompt again. Any other failure may belong to the request,
|
|
56
|
+
* such as a camera that cannot start at the larger size, so the next, looser request follows, as in qr-scanner. When
|
|
57
|
+
* none succeeds, a camera that exists but fails outweighs a request no camera could meet. A closed scanner asks no more.
|
|
58
|
+
*/
|
|
59
|
+
const openCamera = async closed => {
|
|
60
|
+
if (!navigator.mediaDevices) return {
|
|
61
|
+
error: "unavailable"
|
|
62
|
+
};
|
|
63
|
+
let error = "no-camera";
|
|
64
|
+
for (const video of cameraRequests) {
|
|
65
|
+
if (closed()) break;
|
|
66
|
+
try {
|
|
67
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
68
|
+
video,
|
|
69
|
+
audio: false
|
|
70
|
+
});
|
|
71
|
+
const facing = facingOf(stream.getVideoTracks()[0]);
|
|
72
|
+
// Like qr-scanner, mirror a front camera, and a camera of unknown facing that the rear request did not find.
|
|
73
|
+
return {
|
|
74
|
+
stream,
|
|
75
|
+
mirrored: facing ? facing === "user" : !video.facingMode
|
|
76
|
+
};
|
|
77
|
+
} catch (failure) {
|
|
78
|
+
const reason = cameraFailure(errorName(failure));
|
|
79
|
+
if (reason === "denied") return {
|
|
80
|
+
error: reason
|
|
81
|
+
};
|
|
82
|
+
if (reason !== "no-camera") error = reason;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
error
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
const stopStream = stream => {
|
|
90
|
+
for (const track of stream.getTracks()) track.stop();
|
|
22
91
|
};
|
|
23
92
|
|
|
24
93
|
/**
|
|
@@ -38,24 +107,22 @@ export function QrScanner(props) {
|
|
|
38
107
|
let video;
|
|
39
108
|
let scanner;
|
|
40
109
|
let closed = false;
|
|
41
|
-
/** The host has unmounted the scanner; it no longer hears about failures that finish later. */
|
|
42
|
-
let unmounted = false;
|
|
43
110
|
let cleanupMotion;
|
|
44
111
|
const destroy = () => {
|
|
45
112
|
closed = true;
|
|
46
113
|
cleanupMotion?.();
|
|
47
114
|
resetRejected.cancel();
|
|
48
115
|
resetFeedback.cancel();
|
|
49
|
-
// pause(true) stops
|
|
116
|
+
// pause(true) stops the camera stream immediately.
|
|
50
117
|
void scanner?.pause(true);
|
|
51
118
|
scanner?.destroy();
|
|
52
119
|
scanner = undefined;
|
|
53
120
|
};
|
|
54
|
-
|
|
121
|
+
// Once closed, the host has unmounted the scanner or heard why it stopped; later failures stay silent.
|
|
122
|
+
const fail = reason => {
|
|
55
123
|
if (closed) return;
|
|
56
124
|
destroy();
|
|
57
|
-
|
|
58
|
-
if (!unmounted) props.onError(denied ? "denied" : "unavailable");
|
|
125
|
+
props.onError(reason);
|
|
59
126
|
};
|
|
60
127
|
onMount(() => {
|
|
61
128
|
const hidden = () => {
|
|
@@ -65,7 +132,6 @@ export function QrScanner(props) {
|
|
|
65
132
|
document.addEventListener("visibilitychange", hidden);
|
|
66
133
|
window.addEventListener("pagehide", leaving);
|
|
67
134
|
onCleanup(() => {
|
|
68
|
-
unmounted = true;
|
|
69
135
|
destroy();
|
|
70
136
|
document.removeEventListener("visibilitychange", hidden);
|
|
71
137
|
window.removeEventListener("pagehide", leaving);
|
|
@@ -89,12 +155,20 @@ export function QrScanner(props) {
|
|
|
89
155
|
setInvalid(true);
|
|
90
156
|
resetFeedback.debouncedFn();
|
|
91
157
|
}, {
|
|
92
|
-
preferredCamera: "environment",
|
|
93
158
|
highlightScanRegion: true,
|
|
94
|
-
//
|
|
159
|
+
// A frame that cannot be decoded never ends the session: the camera runs, and the next frame may read.
|
|
160
|
+
// An engine error other than an empty frame, which the native BarcodeDetector reports as "Scanner error:
|
|
161
|
+
// No QR code found", means the detector cannot read on this device. Fall back to the bundled worker for
|
|
162
|
+
// the page through the flag qr-scanner 1.4.2 sets itself for "not implemented" and "service unavailable";
|
|
163
|
+
// it swaps engines after this frame. Worker errors carry the same prefix; the flag changes nothing there.
|
|
164
|
+
// qr-scanner reports frames only as strings: an Error means the engine itself did not load, such as a
|
|
165
|
+
// decoder chunk gone after a deploy or a worker that cannot start, and no later frame can read. Never log
|
|
166
|
+
// decoded content or frame data.
|
|
95
167
|
onDecodeError: error => {
|
|
96
|
-
if (
|
|
97
|
-
|
|
168
|
+
if (typeof error !== "string") return fail("unavailable");
|
|
169
|
+
if (error.startsWith("Scanner error: ") && !error.endsWith(Engine.NO_QR_CODE_FOUND)) {
|
|
170
|
+
Reflect.set(Engine, "_disableBarcodeDetector", true);
|
|
171
|
+
}
|
|
98
172
|
}
|
|
99
173
|
});
|
|
100
174
|
const motion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
@@ -108,10 +182,16 @@ export function QrScanner(props) {
|
|
|
108
182
|
applyMotion();
|
|
109
183
|
motion.addEventListener("change", applyMotion);
|
|
110
184
|
cleanupMotion = () => motion.removeEventListener("change", applyMotion);
|
|
185
|
+
const camera = await openCamera(() => closed);
|
|
186
|
+
if ("error" in camera) return fail(camera.error);
|
|
187
|
+
if (closed) return stopStream(camera.stream);
|
|
188
|
+
// qr-scanner plays a stream that is already set instead of opening its own.
|
|
189
|
+
video.srcObject = camera.stream;
|
|
190
|
+
video.style.transform = camera.mirrored ? "scaleX(-1)" : "";
|
|
111
191
|
await scanner.start();
|
|
112
192
|
if (!closed) setStarting(false);
|
|
113
193
|
} catch {
|
|
114
|
-
|
|
194
|
+
fail("unavailable");
|
|
115
195
|
}
|
|
116
196
|
})();
|
|
117
197
|
});
|