@monimesl/dot-simulator 0.1.0-alpha.15 → 0.1.0-alpha.18
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/dist/client/service-bridge.js +162 -35
- package/dist/client/service-bridge.js.map +1 -1
- package/dist/client/simulator.js +446 -81
- package/dist/client/simulator.js.map +1 -1
- package/dist/client/webview-bridge.js.map +1 -1
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +36 -3
- package/dist/node/index.js.map +1 -1
- package/package.json +3 -3
package/dist/client/simulator.js
CHANGED
|
@@ -76,6 +76,49 @@
|
|
|
76
76
|
"warning",
|
|
77
77
|
"danger"
|
|
78
78
|
];
|
|
79
|
+
const TEXT_TONES = {
|
|
80
|
+
words: [...TONES, "onPrimary"],
|
|
81
|
+
or: "themeColor"
|
|
82
|
+
};
|
|
83
|
+
const BACKGROUNDS = {
|
|
84
|
+
words: [
|
|
85
|
+
"background",
|
|
86
|
+
"surface",
|
|
87
|
+
"primary",
|
|
88
|
+
"muted",
|
|
89
|
+
"info",
|
|
90
|
+
"success",
|
|
91
|
+
"warning",
|
|
92
|
+
"danger"
|
|
93
|
+
],
|
|
94
|
+
or: "themeColor"
|
|
95
|
+
};
|
|
96
|
+
const BORDERS = [
|
|
97
|
+
"none",
|
|
98
|
+
"subtle",
|
|
99
|
+
"strong"
|
|
100
|
+
];
|
|
101
|
+
const RADII = [
|
|
102
|
+
"none",
|
|
103
|
+
"small",
|
|
104
|
+
"medium",
|
|
105
|
+
"large",
|
|
106
|
+
"full"
|
|
107
|
+
];
|
|
108
|
+
const LENGTH = {
|
|
109
|
+
words: ["fill"],
|
|
110
|
+
or: "number"
|
|
111
|
+
};
|
|
112
|
+
const BOX_PROPS = {
|
|
113
|
+
background: BACKGROUNDS,
|
|
114
|
+
border: BORDERS,
|
|
115
|
+
radius: RADII,
|
|
116
|
+
width: LENGTH,
|
|
117
|
+
height: LENGTH,
|
|
118
|
+
minHeight: "number",
|
|
119
|
+
maxWidth: "number",
|
|
120
|
+
aspect: ASPECTS
|
|
121
|
+
};
|
|
79
122
|
const STATUS_TONES = [
|
|
80
123
|
"default",
|
|
81
124
|
"info",
|
|
@@ -305,6 +348,45 @@
|
|
|
305
348
|
because: "the aspect sets the height"
|
|
306
349
|
}]
|
|
307
350
|
});
|
|
351
|
+
const Slider = defineComponent({
|
|
352
|
+
name: "Slider",
|
|
353
|
+
props: {
|
|
354
|
+
value: "number",
|
|
355
|
+
min: "number",
|
|
356
|
+
max: "number",
|
|
357
|
+
step: "number",
|
|
358
|
+
showValue: "boolean",
|
|
359
|
+
label: "string",
|
|
360
|
+
hint: "string",
|
|
361
|
+
disabled: "boolean"
|
|
362
|
+
},
|
|
363
|
+
events: ["change"],
|
|
364
|
+
children: "none"
|
|
365
|
+
});
|
|
366
|
+
const Prose = defineComponent({
|
|
367
|
+
name: "Prose",
|
|
368
|
+
props: { ...PADDING_PROPS },
|
|
369
|
+
children: "elements"
|
|
370
|
+
});
|
|
371
|
+
const Quote = defineComponent({
|
|
372
|
+
name: "Quote",
|
|
373
|
+
props: { cite: "string" },
|
|
374
|
+
children: "elements"
|
|
375
|
+
});
|
|
376
|
+
const BulletList = defineComponent({
|
|
377
|
+
name: "BulletList",
|
|
378
|
+
props: { ordered: "boolean" },
|
|
379
|
+
children: "elements"
|
|
380
|
+
});
|
|
381
|
+
const Bullet = defineComponent({
|
|
382
|
+
name: "Bullet",
|
|
383
|
+
children: "elements"
|
|
384
|
+
});
|
|
385
|
+
const RichText = defineComponent({
|
|
386
|
+
name: "RichText",
|
|
387
|
+
props: { markdown: "string" },
|
|
388
|
+
children: "none"
|
|
389
|
+
});
|
|
308
390
|
const Input = defineComponent({
|
|
309
391
|
name: "Input",
|
|
310
392
|
props: {
|
|
@@ -462,11 +544,14 @@
|
|
|
462
544
|
name: "Text",
|
|
463
545
|
props: {
|
|
464
546
|
variant: [
|
|
547
|
+
"display",
|
|
465
548
|
"title",
|
|
549
|
+
"subtitle",
|
|
550
|
+
"lead",
|
|
466
551
|
"body",
|
|
467
552
|
"caption"
|
|
468
553
|
],
|
|
469
|
-
tone:
|
|
554
|
+
tone: TEXT_TONES,
|
|
470
555
|
weight: [
|
|
471
556
|
"regular",
|
|
472
557
|
"medium",
|
|
@@ -478,6 +563,19 @@
|
|
|
478
563
|
"end"
|
|
479
564
|
],
|
|
480
565
|
maxLines: "number",
|
|
566
|
+
decoration: [
|
|
567
|
+
"none",
|
|
568
|
+
"underline",
|
|
569
|
+
"strikethrough"
|
|
570
|
+
],
|
|
571
|
+
numeric: "boolean",
|
|
572
|
+
leading: [
|
|
573
|
+
"tight",
|
|
574
|
+
"normal",
|
|
575
|
+
"relaxed"
|
|
576
|
+
],
|
|
577
|
+
italic: "boolean",
|
|
578
|
+
selectable: "boolean",
|
|
481
579
|
...PADDING_PROPS
|
|
482
580
|
},
|
|
483
581
|
children: "text",
|
|
@@ -528,9 +626,51 @@
|
|
|
528
626
|
events: ["change"],
|
|
529
627
|
children: "elements"
|
|
530
628
|
});
|
|
629
|
+
const Radio = defineComponent({
|
|
630
|
+
name: "Radio",
|
|
631
|
+
props: {
|
|
632
|
+
value: "string",
|
|
633
|
+
label: "string",
|
|
634
|
+
disabled: "boolean"
|
|
635
|
+
},
|
|
636
|
+
children: "none"
|
|
637
|
+
});
|
|
638
|
+
const View = defineComponent({
|
|
639
|
+
name: "View",
|
|
640
|
+
props: {
|
|
641
|
+
gap: SPACES,
|
|
642
|
+
direction: ["column", "row"],
|
|
643
|
+
align: [
|
|
644
|
+
"stretch",
|
|
645
|
+
"start",
|
|
646
|
+
"center",
|
|
647
|
+
"end"
|
|
648
|
+
],
|
|
649
|
+
justify: [
|
|
650
|
+
"start",
|
|
651
|
+
"center",
|
|
652
|
+
"end",
|
|
653
|
+
"between"
|
|
654
|
+
],
|
|
655
|
+
wrap: "boolean",
|
|
656
|
+
grow: "boolean",
|
|
657
|
+
...PADDING_PROPS,
|
|
658
|
+
variant: ["plain", "card"],
|
|
659
|
+
...BOX_PROPS
|
|
660
|
+
},
|
|
661
|
+
events: ["tap"],
|
|
662
|
+
children: "elements",
|
|
663
|
+
overrides: [{
|
|
664
|
+
prop: "aspect",
|
|
665
|
+
ignores: "height",
|
|
666
|
+
because: "the aspect sets the height"
|
|
667
|
+
}]
|
|
668
|
+
});
|
|
531
669
|
const builtinComponents = {
|
|
532
670
|
AmountInput,
|
|
533
671
|
Badge,
|
|
672
|
+
Bullet,
|
|
673
|
+
BulletList,
|
|
534
674
|
Button,
|
|
535
675
|
Carousel,
|
|
536
676
|
Checkbox,
|
|
@@ -551,53 +691,40 @@
|
|
|
551
691
|
Notice,
|
|
552
692
|
Option,
|
|
553
693
|
ProgressBar,
|
|
694
|
+
Prose,
|
|
554
695
|
QRCode,
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
props: {
|
|
558
|
-
value: "string",
|
|
559
|
-
label: "string",
|
|
560
|
-
disabled: "boolean"
|
|
561
|
-
},
|
|
562
|
-
children: "none"
|
|
563
|
-
}),
|
|
696
|
+
Quote,
|
|
697
|
+
Radio,
|
|
564
698
|
RadioGroup,
|
|
699
|
+
RichText,
|
|
565
700
|
Scroll,
|
|
566
701
|
Select,
|
|
567
702
|
Sheet,
|
|
568
703
|
Skeleton,
|
|
704
|
+
Slider,
|
|
569
705
|
Spinner,
|
|
706
|
+
Stack: defineComponent({
|
|
707
|
+
name: "Stack",
|
|
708
|
+
props: {
|
|
709
|
+
radius: BOX_PROPS.radius,
|
|
710
|
+
width: BOX_PROPS.width,
|
|
711
|
+
height: BOX_PROPS.height,
|
|
712
|
+
aspect: BOX_PROPS.aspect
|
|
713
|
+
},
|
|
714
|
+
children: "elements",
|
|
715
|
+
overrides: [{
|
|
716
|
+
prop: "aspect",
|
|
717
|
+
ignores: "height",
|
|
718
|
+
because: "the aspect sets the height"
|
|
719
|
+
}]
|
|
720
|
+
}),
|
|
570
721
|
Switch,
|
|
571
722
|
Tab,
|
|
572
723
|
Tabs,
|
|
573
724
|
Text,
|
|
574
725
|
TextArea,
|
|
575
726
|
Toast,
|
|
576
|
-
View
|
|
577
|
-
name: "View",
|
|
578
|
-
props: {
|
|
579
|
-
gap: SPACES,
|
|
580
|
-
direction: ["column", "row"],
|
|
581
|
-
align: [
|
|
582
|
-
"stretch",
|
|
583
|
-
"start",
|
|
584
|
-
"center",
|
|
585
|
-
"end"
|
|
586
|
-
],
|
|
587
|
-
justify: [
|
|
588
|
-
"start",
|
|
589
|
-
"center",
|
|
590
|
-
"end",
|
|
591
|
-
"between"
|
|
592
|
-
],
|
|
593
|
-
wrap: "boolean",
|
|
594
|
-
grow: "boolean",
|
|
595
|
-
...PADDING_PROPS,
|
|
596
|
-
variant: ["plain", "card"]
|
|
597
|
-
},
|
|
598
|
-
events: ["tap"],
|
|
599
|
-
children: "elements"
|
|
600
|
-
})
|
|
727
|
+
View
|
|
601
728
|
};
|
|
602
729
|
componentRegistry(Object.values(builtinComponents));
|
|
603
730
|
var HostError = class extends Error {
|
|
@@ -678,7 +805,7 @@
|
|
|
678
805
|
limit;
|
|
679
806
|
#pages = [];
|
|
680
807
|
#nextInstanceId = 1;
|
|
681
|
-
constructor(limit =
|
|
808
|
+
constructor(limit = 16) {
|
|
682
809
|
this.limit = limit;
|
|
683
810
|
}
|
|
684
811
|
get pages() {
|
|
@@ -779,7 +906,8 @@
|
|
|
779
906
|
this.#succeed(id, await entry(call));
|
|
780
907
|
return;
|
|
781
908
|
}
|
|
782
|
-
|
|
909
|
+
const permission = typeof entry.permission === "function" ? entry.permission(call.params) : entry.permission;
|
|
910
|
+
await this.#permissions.check(call.method, permission, call.signal);
|
|
783
911
|
this.#succeed(id, await entry.handle(call));
|
|
784
912
|
} catch (error) {
|
|
785
913
|
this.#fail(id, error);
|
|
@@ -979,7 +1107,7 @@
|
|
|
979
1107
|
}
|
|
980
1108
|
//#endregion
|
|
981
1109
|
//#region src/versions.ts
|
|
982
|
-
const SIMULATOR_VERSIONS = { runtimeVersion: "0.1.0-alpha.
|
|
1110
|
+
const SIMULATOR_VERSIONS = { runtimeVersion: "0.1.0-alpha.18" };
|
|
983
1111
|
//#endregion
|
|
984
1112
|
//#region src/paths.ts
|
|
985
1113
|
const CLIENT_PATH = "/__dot/";
|
|
@@ -988,6 +1116,7 @@
|
|
|
988
1116
|
const FILE_PATH = `${CLIENT_PATH}storage/file/`;
|
|
989
1117
|
const CACHE_PATH = `${CLIENT_PATH}storage/cache/`;
|
|
990
1118
|
const SECRET_PATH = `${CLIENT_PATH}storage/secret/`;
|
|
1119
|
+
const MEDIA_PATH = `${CLIENT_PATH}media/`;
|
|
991
1120
|
const DOT_PATH = "/dot/";
|
|
992
1121
|
const WEBVIEW_URL = `${DOT_PATH}ui/index.html`;
|
|
993
1122
|
const MANIFEST_URL = `${DOT_PATH}manifest.json`;
|
|
@@ -1003,6 +1132,196 @@
|
|
|
1003
1132
|
if ("error" in answer) throw new HostError(answer.error.code, answer.error.message);
|
|
1004
1133
|
return answer.result;
|
|
1005
1134
|
}
|
|
1135
|
+
function tooLargeMessage(name) {
|
|
1136
|
+
return `"${name}" is larger than 25 MB`;
|
|
1137
|
+
}
|
|
1138
|
+
//#endregion
|
|
1139
|
+
//#region src/client/dom.ts
|
|
1140
|
+
function element(selector) {
|
|
1141
|
+
const node = document.querySelector(selector);
|
|
1142
|
+
if (!node) throw new Error(`The simulator page is missing ${selector}`);
|
|
1143
|
+
return node;
|
|
1144
|
+
}
|
|
1145
|
+
function createElement(tag, className, text) {
|
|
1146
|
+
const node = document.createElement(tag);
|
|
1147
|
+
node.className = className;
|
|
1148
|
+
if (text !== void 0) node.textContent = text;
|
|
1149
|
+
return node;
|
|
1150
|
+
}
|
|
1151
|
+
//#endregion
|
|
1152
|
+
//#region src/client/dash/ui.ts
|
|
1153
|
+
const DEFAULT_ACTIONS = [{
|
|
1154
|
+
id: "ok",
|
|
1155
|
+
label: "OK"
|
|
1156
|
+
}];
|
|
1157
|
+
function alert(phone, options, signal, field) {
|
|
1158
|
+
return new Promise((resolve, reject) => {
|
|
1159
|
+
const layer = createElement("div", "dialog-layer");
|
|
1160
|
+
const dialog = createElement("div", "dialog");
|
|
1161
|
+
dialog.setAttribute("role", "alertdialog");
|
|
1162
|
+
const body = createElement("div", "dialog-body");
|
|
1163
|
+
body.append(createElement("div", "dialog-title", options.title));
|
|
1164
|
+
if (options.message) body.append(createElement("div", "dialog-message", options.message));
|
|
1165
|
+
if (field) body.append(field);
|
|
1166
|
+
const actions = options.actions ?? DEFAULT_ACTIONS;
|
|
1167
|
+
const buttons = createElement("div", actions.length === 2 ? "dialog-actions" : "dialog-actions stacked");
|
|
1168
|
+
for (const action of actions) {
|
|
1169
|
+
const button = createElement("button", action.role ?? "default", action.label);
|
|
1170
|
+
button.addEventListener("click", () => {
|
|
1171
|
+
layer.remove();
|
|
1172
|
+
resolve({ action: action.id });
|
|
1173
|
+
});
|
|
1174
|
+
buttons.append(button);
|
|
1175
|
+
}
|
|
1176
|
+
dialog.append(body, buttons);
|
|
1177
|
+
layer.append(dialog);
|
|
1178
|
+
field?.addEventListener("keydown", (event) => {
|
|
1179
|
+
if (event.key === "Enter") buttons.lastElementChild?.click();
|
|
1180
|
+
});
|
|
1181
|
+
signal.addEventListener("abort", () => {
|
|
1182
|
+
layer.remove();
|
|
1183
|
+
reject(new HostError("cancelled", "The alert was dismissed"));
|
|
1184
|
+
});
|
|
1185
|
+
phone.append(layer);
|
|
1186
|
+
field?.focus();
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
//#endregion
|
|
1190
|
+
//#region src/client/dash/media.ts
|
|
1191
|
+
const MAX_PHOTO_SIDE = 2048;
|
|
1192
|
+
const JPEG_QUALITY = .85;
|
|
1193
|
+
function photoPermission(params) {
|
|
1194
|
+
return photoSource(params) === "camera" ? "camera" : "photoLibrary";
|
|
1195
|
+
}
|
|
1196
|
+
async function pickPhoto(phone, note, params, signal) {
|
|
1197
|
+
const source = photoSource(params);
|
|
1198
|
+
await allowPicker(phone, source === "camera" ? "Take a photo" : "Choose a photo", signal);
|
|
1199
|
+
const file = await chooseFile("image/*", signal);
|
|
1200
|
+
const jpeg = await toJpeg(file);
|
|
1201
|
+
const name = jpegName(file.name);
|
|
1202
|
+
note(`photo (${source}): ${name}`);
|
|
1203
|
+
return keepOnDevServer("photo", {
|
|
1204
|
+
name,
|
|
1205
|
+
data: await base64(jpeg)
|
|
1206
|
+
}, signal);
|
|
1207
|
+
}
|
|
1208
|
+
async function pickFile(phone, note, params, signal) {
|
|
1209
|
+
const types = fileTypes(params);
|
|
1210
|
+
await allowPicker(phone, "Choose a file", signal);
|
|
1211
|
+
const file = await chooseFile(types.join(","), signal);
|
|
1212
|
+
if (file.size > 26214400) throw new HostError("invalid", tooLargeMessage(file.name));
|
|
1213
|
+
note(`file: ${file.name}`);
|
|
1214
|
+
const contentType = file.type || "application/octet-stream";
|
|
1215
|
+
return keepOnDevServer("file", {
|
|
1216
|
+
name: file.name,
|
|
1217
|
+
contentType,
|
|
1218
|
+
data: await base64(file)
|
|
1219
|
+
}, signal);
|
|
1220
|
+
}
|
|
1221
|
+
async function scanCode(phone, note, signal) {
|
|
1222
|
+
const field = createElement("input", "dialog-input");
|
|
1223
|
+
field.placeholder = "https://monime.io/pay/…";
|
|
1224
|
+
const { action } = await alert(phone, {
|
|
1225
|
+
title: "Scan a QR code",
|
|
1226
|
+
message: "Type or paste the text the code holds.",
|
|
1227
|
+
actions: [{
|
|
1228
|
+
id: "cancel",
|
|
1229
|
+
label: "Cancel",
|
|
1230
|
+
role: "cancel"
|
|
1231
|
+
}, {
|
|
1232
|
+
id: "scan",
|
|
1233
|
+
label: "Scan"
|
|
1234
|
+
}]
|
|
1235
|
+
}, signal, field);
|
|
1236
|
+
const text = field.value.trim();
|
|
1237
|
+
if (action !== "scan" || text === "") throw new HostError("cancelled", "No code was scanned");
|
|
1238
|
+
note(`scanned: ${text}`);
|
|
1239
|
+
return {
|
|
1240
|
+
text,
|
|
1241
|
+
format: "qr"
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
function jpegName(fileName) {
|
|
1245
|
+
const dot = fileName.lastIndexOf(".");
|
|
1246
|
+
return `${(dot > 0 ? fileName.slice(0, dot) : fileName) || "photo"}.jpg`;
|
|
1247
|
+
}
|
|
1248
|
+
function photoSource(params) {
|
|
1249
|
+
const source = params?.source;
|
|
1250
|
+
if (source !== "library" && source !== "camera") throw new HostError("invalid", "\"source\" must be \"library\" or \"camera\"");
|
|
1251
|
+
return source;
|
|
1252
|
+
}
|
|
1253
|
+
function fileTypes(params) {
|
|
1254
|
+
const types = params?.types ?? [];
|
|
1255
|
+
if (!Array.isArray(types) || types.some((type) => typeof type !== "string" || !type.includes("/"))) throw new HostError("invalid", "\"types\" must list content types, such as \"application/pdf\" or \"image/*\"");
|
|
1256
|
+
return types;
|
|
1257
|
+
}
|
|
1258
|
+
async function allowPicker(phone, title, signal) {
|
|
1259
|
+
if (navigator.userActivation.isActive) return;
|
|
1260
|
+
const { action } = await alert(phone, {
|
|
1261
|
+
title,
|
|
1262
|
+
message: "The simulator picks a file from this computer.",
|
|
1263
|
+
actions: [{
|
|
1264
|
+
id: "cancel",
|
|
1265
|
+
label: "Cancel",
|
|
1266
|
+
role: "cancel"
|
|
1267
|
+
}, {
|
|
1268
|
+
id: "choose",
|
|
1269
|
+
label: "Choose"
|
|
1270
|
+
}]
|
|
1271
|
+
}, signal);
|
|
1272
|
+
if (action !== "choose") throw new HostError("cancelled", "Nothing was chosen");
|
|
1273
|
+
}
|
|
1274
|
+
function chooseFile(accept, signal) {
|
|
1275
|
+
return new Promise((resolve, reject) => {
|
|
1276
|
+
const input = document.createElement("input");
|
|
1277
|
+
input.type = "file";
|
|
1278
|
+
input.accept = accept;
|
|
1279
|
+
input.addEventListener("change", () => {
|
|
1280
|
+
const file = input.files?.[0];
|
|
1281
|
+
if (file) resolve(file);
|
|
1282
|
+
else reject(new HostError("cancelled", "Nothing was chosen"));
|
|
1283
|
+
});
|
|
1284
|
+
input.addEventListener("cancel", () => {
|
|
1285
|
+
reject(new HostError("cancelled", "Nothing was chosen"));
|
|
1286
|
+
});
|
|
1287
|
+
signal.addEventListener("abort", () => {
|
|
1288
|
+
reject(new HostError("cancelled", "The call ended before anything was chosen"));
|
|
1289
|
+
});
|
|
1290
|
+
input.click();
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
async function toJpeg(file) {
|
|
1294
|
+
let image;
|
|
1295
|
+
try {
|
|
1296
|
+
image = await createImageBitmap(file, { imageOrientation: "from-image" });
|
|
1297
|
+
} catch {
|
|
1298
|
+
throw new HostError("failed", `"${file.name}" isn't a photo the simulator can read`);
|
|
1299
|
+
}
|
|
1300
|
+
const scale = Math.min(1, MAX_PHOTO_SIDE / Math.max(image.width, image.height));
|
|
1301
|
+
const canvas = new OffscreenCanvas(Math.round(image.width * scale), Math.round(image.height * scale));
|
|
1302
|
+
const context = canvas.getContext("2d");
|
|
1303
|
+
if (!context) throw new HostError("failed", "The browser couldn't draw the photo");
|
|
1304
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
1305
|
+
image.close();
|
|
1306
|
+
return canvas.convertToBlob({
|
|
1307
|
+
type: "image/jpeg",
|
|
1308
|
+
quality: JPEG_QUALITY
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
function keepOnDevServer(kind, params, signal) {
|
|
1312
|
+
return callDevServer(`${MEDIA_PATH}${kind}`, params, signal);
|
|
1313
|
+
}
|
|
1314
|
+
function base64(blob) {
|
|
1315
|
+
return new Promise((resolve, reject) => {
|
|
1316
|
+
const reader = new FileReader();
|
|
1317
|
+
reader.addEventListener("load", () => {
|
|
1318
|
+
const url = reader.result;
|
|
1319
|
+
resolve(url.slice(url.indexOf(",") + 1));
|
|
1320
|
+
});
|
|
1321
|
+
reader.addEventListener("error", () => reject(new HostError("failed", "The file couldn't be read")));
|
|
1322
|
+
reader.readAsDataURL(blob);
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1006
1325
|
//#endregion
|
|
1007
1326
|
//#region src/client/dash/system.ts
|
|
1008
1327
|
const SHAKE_DURATION_MS = 300;
|
|
@@ -1059,51 +1378,55 @@
|
|
|
1059
1378
|
});
|
|
1060
1379
|
}
|
|
1061
1380
|
//#endregion
|
|
1062
|
-
//#region src/client/
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
if (
|
|
1072
|
-
|
|
1381
|
+
//#region src/client/dash/pickers.ts
|
|
1382
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1383
|
+
const TIME = /^\d{2}:\d{2}$/;
|
|
1384
|
+
function pickDate(phone, note, params, signal) {
|
|
1385
|
+
const { title, value, min, max } = params ?? {};
|
|
1386
|
+
for (const [name, date] of Object.entries({
|
|
1387
|
+
value,
|
|
1388
|
+
min,
|
|
1389
|
+
max
|
|
1390
|
+
})) if (date !== void 0 && !DATE.test(date)) throw new HostError("invalid", `"${name}" must be a date such as "2026-10-24", not "${date}"`);
|
|
1391
|
+
if (min && max && min > max) throw new HostError("invalid", `"min" (${min}) is after "max" (${max})`);
|
|
1392
|
+
return pick(phone, note, signal, {
|
|
1393
|
+
type: "date",
|
|
1394
|
+
title: title ?? "Choose a date",
|
|
1395
|
+
value,
|
|
1396
|
+
min,
|
|
1397
|
+
max
|
|
1398
|
+
});
|
|
1073
1399
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
return new Promise((resolve, reject) => {
|
|
1082
|
-
const layer = createElement("div", "dialog-layer");
|
|
1083
|
-
const dialog = createElement("div", "dialog");
|
|
1084
|
-
dialog.setAttribute("role", "alertdialog");
|
|
1085
|
-
const body = createElement("div", "dialog-body");
|
|
1086
|
-
body.append(createElement("div", "dialog-title", options.title));
|
|
1087
|
-
if (options.message) body.append(createElement("div", "dialog-message", options.message));
|
|
1088
|
-
const actions = options.actions ?? DEFAULT_ACTIONS;
|
|
1089
|
-
const buttons = createElement("div", actions.length === 2 ? "dialog-actions" : "dialog-actions stacked");
|
|
1090
|
-
for (const action of actions) {
|
|
1091
|
-
const button = createElement("button", action.role ?? "default", action.label);
|
|
1092
|
-
button.addEventListener("click", () => {
|
|
1093
|
-
layer.remove();
|
|
1094
|
-
resolve({ action: action.id });
|
|
1095
|
-
});
|
|
1096
|
-
buttons.append(button);
|
|
1097
|
-
}
|
|
1098
|
-
dialog.append(body, buttons);
|
|
1099
|
-
layer.append(dialog);
|
|
1100
|
-
signal.addEventListener("abort", () => {
|
|
1101
|
-
layer.remove();
|
|
1102
|
-
reject(new HostError("cancelled", "The alert was dismissed"));
|
|
1103
|
-
});
|
|
1104
|
-
phone.append(layer);
|
|
1400
|
+
function pickTime(phone, note, params, signal) {
|
|
1401
|
+
const { title, value } = params ?? {};
|
|
1402
|
+
if (value !== void 0 && !TIME.test(value)) throw new HostError("invalid", `"value" must be a time such as "19:30", not "${value}"`);
|
|
1403
|
+
return pick(phone, note, signal, {
|
|
1404
|
+
type: "time",
|
|
1405
|
+
title: title ?? "Choose a time",
|
|
1406
|
+
value
|
|
1105
1407
|
});
|
|
1106
1408
|
}
|
|
1409
|
+
async function pick(phone, note, signal, { type, title, value, min, max }) {
|
|
1410
|
+
const field = createElement("input", "dialog-input");
|
|
1411
|
+
field.type = type;
|
|
1412
|
+
field.value = value ?? "";
|
|
1413
|
+
field.min = min ?? "";
|
|
1414
|
+
field.max = max ?? "";
|
|
1415
|
+
const { action } = await alert(phone, {
|
|
1416
|
+
title,
|
|
1417
|
+
actions: [{
|
|
1418
|
+
id: "cancel",
|
|
1419
|
+
label: "Cancel",
|
|
1420
|
+
role: "cancel"
|
|
1421
|
+
}, {
|
|
1422
|
+
id: "done",
|
|
1423
|
+
label: "Done"
|
|
1424
|
+
}]
|
|
1425
|
+
}, signal, field);
|
|
1426
|
+
if (action !== "done" || field.value === "") throw new HostError("cancelled", `No ${type} was chosen`);
|
|
1427
|
+
note(`${type}: ${field.value}`);
|
|
1428
|
+
return field.value;
|
|
1429
|
+
}
|
|
1107
1430
|
//#endregion
|
|
1108
1431
|
//#region src/client/dash/index.ts
|
|
1109
1432
|
const HTTP_METHODS = [
|
|
@@ -1141,12 +1464,26 @@
|
|
|
1141
1464
|
function dashHandlers(phone, note) {
|
|
1142
1465
|
return {
|
|
1143
1466
|
"dash.ui.alert": ({ params, signal }) => alert(phone, params, signal),
|
|
1467
|
+
"dash.ui.pickDate": ({ params, signal }) => pickDate(phone, note, params, signal),
|
|
1468
|
+
"dash.ui.pickTime": ({ params, signal }) => pickTime(phone, note, params, signal),
|
|
1144
1469
|
"dash.system.vibrate": ({ params }) => vibrate(phone, note, params),
|
|
1145
1470
|
"dash.system.torch": ({ params }) => torch(phone, note, params.state),
|
|
1146
1471
|
"dash.system.copyText": ({ params }) => copyText(note, params.text),
|
|
1147
1472
|
"dash.system.share": ({ params }) => share(note, params),
|
|
1148
1473
|
"dash.system.openUrl": ({ params }) => openUrl(note, params.url),
|
|
1149
1474
|
"dash.system.delay": ({ params, signal }) => delay(params.milliseconds, signal),
|
|
1475
|
+
"dash.media.pickPhoto": {
|
|
1476
|
+
permission: photoPermission,
|
|
1477
|
+
handle: ({ params, signal }) => pickPhoto(phone, note, params, signal)
|
|
1478
|
+
},
|
|
1479
|
+
"dash.media.scanCode": {
|
|
1480
|
+
permission: "camera",
|
|
1481
|
+
handle: ({ signal }) => scanCode(phone, note, signal)
|
|
1482
|
+
},
|
|
1483
|
+
"dash.storage.file.pick": {
|
|
1484
|
+
permission: "files",
|
|
1485
|
+
handle: ({ params, signal }) => pickFile(phone, note, params, signal)
|
|
1486
|
+
},
|
|
1150
1487
|
...devServerHandlers("dash.network.http", HTTP_PATH, HTTP_METHODS),
|
|
1151
1488
|
...devServerHandlers("dash.storage.file", FILE_PATH, FILE_OPERATIONS),
|
|
1152
1489
|
...devServerHandlers("dash.storage.cache", CACHE_PATH, CACHE_OPERATIONS),
|
|
@@ -1158,7 +1495,12 @@
|
|
|
1158
1495
|
}
|
|
1159
1496
|
//#endregion
|
|
1160
1497
|
//#region src/client/dash/permissions.ts
|
|
1161
|
-
const PERMISSION_SUMMARIES = {
|
|
1498
|
+
const PERMISSION_SUMMARIES = {
|
|
1499
|
+
account: "see your Dash account: your name, and your email or phone",
|
|
1500
|
+
camera: "use your camera to take photos and scan codes",
|
|
1501
|
+
photoLibrary: "use photos you choose from your library",
|
|
1502
|
+
files: "use files you choose from your phone"
|
|
1503
|
+
};
|
|
1162
1504
|
async function askPermission(phone, note, request, signal) {
|
|
1163
1505
|
const { action } = await alert(phone, {
|
|
1164
1506
|
title: `Allow “${request.dotName}” to ${PERMISSION_SUMMARIES[request.permission]}?`,
|
|
@@ -1205,8 +1547,11 @@
|
|
|
1205
1547
|
//#endregion
|
|
1206
1548
|
//#region src/client/simulator.ts
|
|
1207
1549
|
const MAX_LOG_ENTRIES = 500;
|
|
1550
|
+
const MESSAGES_SHOWN_KEY = "dot-simulator-messages";
|
|
1208
1551
|
const screen = element("#screen");
|
|
1209
1552
|
const log = element("#log");
|
|
1553
|
+
const messagesPanel = element("#messages-panel");
|
|
1554
|
+
const messagesButton = element("#messages");
|
|
1210
1555
|
const errorBanner = element("#error");
|
|
1211
1556
|
const phone = element(".phone");
|
|
1212
1557
|
const schemeSelect = element("#scheme");
|
|
@@ -1369,14 +1714,34 @@
|
|
|
1369
1714
|
hotReload();
|
|
1370
1715
|
});
|
|
1371
1716
|
events.addEventListener("build-error", (event) => showError(String(event.data)));
|
|
1717
|
+
function showMessages(shown) {
|
|
1718
|
+
messagesPanel.hidden = !shown;
|
|
1719
|
+
messagesButton.setAttribute("aria-pressed", String(shown));
|
|
1720
|
+
}
|
|
1721
|
+
function toggleMessages() {
|
|
1722
|
+
const shown = messagesPanel.hidden;
|
|
1723
|
+
showMessages(shown);
|
|
1724
|
+
try {
|
|
1725
|
+
localStorage.setItem(MESSAGES_SHOWN_KEY, String(shown));
|
|
1726
|
+
} catch {}
|
|
1727
|
+
}
|
|
1728
|
+
function messagesShownLastTime() {
|
|
1729
|
+
try {
|
|
1730
|
+
return localStorage.getItem(MESSAGES_SHOWN_KEY) !== "false";
|
|
1731
|
+
} catch {
|
|
1732
|
+
return true;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1372
1735
|
element("#reload").addEventListener("click", () => restart());
|
|
1373
1736
|
element("#back").addEventListener("click", goBack);
|
|
1374
1737
|
navigationBack.addEventListener("click", goBack);
|
|
1375
1738
|
element("#clear").addEventListener("click", () => log.replaceChildren());
|
|
1739
|
+
messagesButton.addEventListener("click", toggleMessages);
|
|
1376
1740
|
schemeSelect.addEventListener("change", () => changeScheme(schemeSelect.value));
|
|
1377
1741
|
browserDark.addEventListener("change", () => changeScheme(browserScheme()));
|
|
1378
1742
|
document.addEventListener("visibilitychange", syncSystemContext);
|
|
1379
1743
|
changeScheme(browserScheme());
|
|
1744
|
+
showMessages(messagesShownLastTime());
|
|
1380
1745
|
restart();
|
|
1381
1746
|
//#endregion
|
|
1382
1747
|
})();
|