@monimesl/dot-cli 0.1.0-alpha.4 → 0.1.0-alpha.40

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.
Files changed (161) hide show
  1. package/README.md +84 -29
  2. package/dist/cli.js +686 -319
  3. package/dist/cli.js.map +1 -1
  4. package/dist/{server-DFOUQnLA.js → server-DMnWDdJ6.js} +582 -537
  5. package/dist/server-DMnWDdJ6.js.map +1 -0
  6. package/dist/templates/showcase/.env +1 -1
  7. package/dist/templates/showcase/dot.ts +53 -14
  8. package/dist/templates/showcase/pages/basics/bars/controller.ts +47 -0
  9. package/dist/templates/showcase/pages/basics/bars/state.ts +7 -0
  10. package/dist/templates/showcase/pages/basics/bars/view.dml +36 -0
  11. package/dist/templates/showcase/pages/basics/bindings/controller.ts +15 -0
  12. package/dist/templates/showcase/pages/basics/bindings/state.ts +14 -0
  13. package/dist/templates/showcase/pages/basics/bindings/view.dml +41 -0
  14. package/dist/templates/showcase/pages/basics/colors/controller.ts +112 -0
  15. package/dist/templates/showcase/pages/basics/colors/palette.ts +52 -0
  16. package/dist/templates/showcase/pages/basics/colors/state.ts +17 -0
  17. package/dist/templates/showcase/pages/basics/colors/view.dml +142 -0
  18. package/dist/templates/showcase/pages/basics/concert/controller.ts +16 -0
  19. package/dist/templates/showcase/pages/basics/concert/state.ts +7 -0
  20. package/dist/templates/showcase/pages/basics/concert/view.dml +20 -0
  21. package/dist/templates/showcase/pages/basics/control-flow/controller.ts +18 -0
  22. package/dist/templates/showcase/pages/basics/control-flow/state.ts +16 -0
  23. package/dist/templates/showcase/pages/basics/control-flow/view.dml +55 -0
  24. package/dist/templates/showcase/pages/basics/events/controller.ts +43 -0
  25. package/dist/templates/showcase/pages/basics/events/state.ts +11 -0
  26. package/dist/templates/showcase/pages/basics/events/view.dml +51 -0
  27. package/dist/templates/showcase/pages/basics/hooks/controller.ts +43 -0
  28. package/dist/templates/showcase/pages/basics/hooks/state.ts +10 -0
  29. package/dist/templates/showcase/pages/basics/hooks/view.dml +37 -0
  30. package/dist/templates/showcase/pages/basics/navigation/controller.ts +41 -0
  31. package/dist/templates/showcase/pages/basics/navigation/state.ts +11 -0
  32. package/dist/templates/showcase/pages/basics/navigation/view.dml +48 -0
  33. package/dist/templates/showcase/pages/basics/pick-genre/controller.ts +18 -0
  34. package/dist/templates/showcase/pages/basics/pick-genre/state.ts +3 -0
  35. package/dist/templates/showcase/pages/basics/pick-genre/view.dml +10 -0
  36. package/dist/templates/showcase/pages/basics/share/concerts.ts +31 -0
  37. package/dist/templates/showcase/pages/basics/share/controller.ts +12 -0
  38. package/dist/templates/showcase/pages/basics/share/state.ts +5 -0
  39. package/dist/templates/showcase/pages/basics/share/view.dml +21 -0
  40. package/dist/templates/showcase/pages/{state → basics/state}/controller.ts +1 -0
  41. package/dist/templates/showcase/pages/{state → basics/state}/state.ts +1 -1
  42. package/dist/templates/showcase/pages/basics/state/view.dml +46 -0
  43. package/dist/templates/showcase/pages/components/carousel/controller.ts +41 -0
  44. package/dist/templates/showcase/pages/components/carousel/state.ts +77 -0
  45. package/dist/templates/showcase/pages/components/carousel/view.dml +42 -0
  46. package/dist/templates/showcase/pages/components/feedback/controller.ts +32 -0
  47. package/dist/templates/showcase/pages/components/feedback/state.ts +41 -0
  48. package/dist/templates/showcase/pages/components/feedback/view.dml +116 -0
  49. package/dist/templates/showcase/pages/components/forms/controller.ts +72 -0
  50. package/dist/templates/showcase/pages/components/forms/state.ts +37 -0
  51. package/dist/templates/showcase/pages/components/forms/view.dml +97 -0
  52. package/dist/templates/showcase/pages/components/icons/controller.ts +15 -0
  53. package/dist/templates/showcase/pages/components/icons/state.ts +75 -0
  54. package/dist/templates/showcase/pages/components/icons/view.dml +36 -0
  55. package/dist/templates/showcase/pages/components/images/controller.ts +44 -0
  56. package/dist/templates/showcase/pages/{images → components/images}/state.ts +3 -0
  57. package/dist/templates/showcase/pages/components/images/view.dml +83 -0
  58. package/dist/templates/showcase/pages/components/layout/controller.ts +61 -0
  59. package/dist/templates/showcase/pages/components/layout/state.ts +34 -0
  60. package/dist/templates/showcase/pages/components/layout/view.dml +143 -0
  61. package/dist/templates/showcase/pages/components/lists/controller.ts +18 -0
  62. package/dist/templates/showcase/pages/components/lists/state.ts +130 -0
  63. package/dist/templates/showcase/pages/components/lists/view.dml +88 -0
  64. package/dist/templates/showcase/pages/components/money/controller.ts +45 -0
  65. package/dist/templates/showcase/pages/{payments → components/money}/state.ts +3 -0
  66. package/dist/templates/showcase/pages/components/money/view.dml +54 -0
  67. package/dist/templates/showcase/pages/components/reading/controller.ts +6 -0
  68. package/dist/templates/showcase/pages/components/reading/state.ts +23 -0
  69. package/dist/templates/showcase/pages/components/reading/view.dml +42 -0
  70. package/dist/templates/showcase/pages/components/sheets/controller.ts +36 -0
  71. package/dist/templates/showcase/pages/components/sheets/state.ts +26 -0
  72. package/dist/templates/showcase/pages/components/sheets/view.dml +38 -0
  73. package/dist/templates/showcase/pages/components/styles/controller.ts +6 -0
  74. package/dist/templates/showcase/pages/components/styles/state.ts +3 -0
  75. package/dist/templates/showcase/pages/components/styles/view.dml +101 -0
  76. package/dist/templates/showcase/pages/components/tabs/controller.ts +41 -0
  77. package/dist/templates/showcase/pages/components/tabs/state.ts +127 -0
  78. package/dist/templates/showcase/pages/components/tabs/view.dml +57 -0
  79. package/dist/templates/showcase/pages/components/text/controller.ts +10 -0
  80. package/dist/templates/showcase/pages/components/text/state.ts +3 -0
  81. package/dist/templates/showcase/pages/components/text/view.dml +95 -0
  82. package/dist/templates/showcase/pages/dash/account/controller.ts +32 -0
  83. package/dist/templates/showcase/pages/dash/account/state.ts +9 -0
  84. package/dist/templates/showcase/pages/dash/account/view.dml +33 -0
  85. package/dist/templates/showcase/pages/dash/cache/controller.ts +50 -0
  86. package/dist/templates/showcase/pages/dash/cache/state.ts +17 -0
  87. package/dist/templates/showcase/pages/dash/cache/view.dml +48 -0
  88. package/dist/templates/showcase/pages/dash/calls/controller.ts +67 -0
  89. package/dist/templates/showcase/pages/dash/calls/state.ts +9 -0
  90. package/dist/templates/showcase/pages/dash/calls/view.dml +69 -0
  91. package/dist/templates/showcase/pages/dash/contacts/controller.ts +30 -0
  92. package/dist/templates/showcase/pages/dash/contacts/state.ts +6 -0
  93. package/dist/templates/showcase/pages/dash/contacts/view.dml +33 -0
  94. package/dist/templates/showcase/pages/dash/crypto/controller.ts +38 -0
  95. package/dist/templates/showcase/pages/dash/crypto/state.ts +10 -0
  96. package/dist/templates/showcase/pages/dash/crypto/view.dml +48 -0
  97. package/dist/templates/showcase/pages/dash/files/controller.ts +71 -0
  98. package/dist/templates/showcase/pages/dash/files/state.ts +23 -0
  99. package/dist/templates/showcase/pages/dash/files/view.dml +78 -0
  100. package/dist/templates/showcase/pages/dash/media/controller.ts +65 -0
  101. package/dist/templates/showcase/pages/dash/media/state.ts +12 -0
  102. package/dist/templates/showcase/pages/dash/media/view.dml +53 -0
  103. package/dist/templates/showcase/pages/dash/network/controller.ts +215 -0
  104. package/dist/templates/showcase/pages/dash/network/state.ts +35 -0
  105. package/dist/templates/showcase/pages/dash/network/view.dml +108 -0
  106. package/dist/templates/showcase/pages/dash/payments/controller.ts +83 -0
  107. package/dist/templates/showcase/pages/dash/payments/state.ts +10 -0
  108. package/dist/templates/showcase/pages/dash/payments/view.dml +38 -0
  109. package/dist/templates/showcase/pages/dash/secrets/controller.ts +44 -0
  110. package/dist/templates/showcase/pages/dash/secrets/state.ts +5 -0
  111. package/dist/templates/showcase/pages/dash/secrets/view.dml +26 -0
  112. package/dist/templates/showcase/pages/dash/system/controller.ts +89 -0
  113. package/dist/templates/showcase/pages/dash/system/state.ts +13 -0
  114. package/dist/templates/showcase/pages/dash/system/view.dml +96 -0
  115. package/dist/templates/showcase/pages/dash/ui/controller.ts +70 -0
  116. package/dist/templates/showcase/pages/dash/ui/state.ts +8 -0
  117. package/dist/templates/showcase/pages/dash/ui/view.dml +31 -0
  118. package/dist/templates/showcase/pages/index/controller.ts +1 -1
  119. package/dist/templates/showcase/pages/index/state.ts +181 -54
  120. package/dist/templates/showcase/pages/index/view.dml +8 -5
  121. package/dist/templates/showcase/statics/logo.png +0 -0
  122. package/package.json +6 -6
  123. package/dist/server-DFOUQnLA.js.map +0 -1
  124. package/dist/templates/showcase/pages/carousel/controller.ts +0 -10
  125. package/dist/templates/showcase/pages/carousel/state.ts +0 -40
  126. package/dist/templates/showcase/pages/carousel/view.dml +0 -37
  127. package/dist/templates/showcase/pages/device/controller.ts +0 -70
  128. package/dist/templates/showcase/pages/device/state.ts +0 -7
  129. package/dist/templates/showcase/pages/device/view.dml +0 -44
  130. package/dist/templates/showcase/pages/feedback/controller.ts +0 -16
  131. package/dist/templates/showcase/pages/feedback/state.ts +0 -7
  132. package/dist/templates/showcase/pages/feedback/view.dml +0 -87
  133. package/dist/templates/showcase/pages/files/controller.ts +0 -47
  134. package/dist/templates/showcase/pages/files/state.ts +0 -11
  135. package/dist/templates/showcase/pages/files/view.dml +0 -35
  136. package/dist/templates/showcase/pages/filters/controller.ts +0 -28
  137. package/dist/templates/showcase/pages/filters/state.ts +0 -72
  138. package/dist/templates/showcase/pages/filters/view.dml +0 -43
  139. package/dist/templates/showcase/pages/forms/controller.ts +0 -44
  140. package/dist/templates/showcase/pages/forms/state.ts +0 -15
  141. package/dist/templates/showcase/pages/forms/view.dml +0 -39
  142. package/dist/templates/showcase/pages/images/controller.ts +0 -15
  143. package/dist/templates/showcase/pages/images/view.dml +0 -38
  144. package/dist/templates/showcase/pages/layout/controller.ts +0 -26
  145. package/dist/templates/showcase/pages/layout/state.ts +0 -14
  146. package/dist/templates/showcase/pages/layout/view.dml +0 -80
  147. package/dist/templates/showcase/pages/lists/controller.ts +0 -36
  148. package/dist/templates/showcase/pages/lists/state.ts +0 -96
  149. package/dist/templates/showcase/pages/lists/view.dml +0 -59
  150. package/dist/templates/showcase/pages/navigation/controller.ts +0 -64
  151. package/dist/templates/showcase/pages/navigation/state.ts +0 -17
  152. package/dist/templates/showcase/pages/navigation/view.dml +0 -34
  153. package/dist/templates/showcase/pages/network/controller.ts +0 -107
  154. package/dist/templates/showcase/pages/network/state.ts +0 -15
  155. package/dist/templates/showcase/pages/network/view.dml +0 -56
  156. package/dist/templates/showcase/pages/payments/controller.ts +0 -42
  157. package/dist/templates/showcase/pages/payments/view.dml +0 -35
  158. package/dist/templates/showcase/pages/state/view.dml +0 -33
  159. package/dist/templates/showcase/pages/text/controller.ts +0 -10
  160. package/dist/templates/showcase/pages/text/state.ts +0 -7
  161. package/dist/templates/showcase/pages/text/view.dml +0 -49
@@ -0,0 +1,71 @@
1
+ import type { Archive, Song } from './state';
2
+ import state from './state';
3
+
4
+ const PICTURE = 'pictures/artwork.jpg';
5
+
6
+ export default definePage({
7
+ initialState: state,
8
+ configs: { navigationBar: { title: 'Files' } },
9
+ hooks: {
10
+ async onShow() {
11
+ await this.actions.refresh();
12
+ },
13
+ },
14
+ mutations: {
15
+ setDraft: (_state, event: ValueEvent) => ({ draft: event.detail.value }),
16
+ },
17
+ actions: {
18
+ async refresh() {
19
+ const entries = await this.dash.storage.file.list('', { recursive: true });
20
+ const songs: Song[] = [];
21
+ for (const entry of entries) {
22
+ if (entry.kind === 'file' && entry.path.startsWith('setlist/')) {
23
+ songs.push({ path: entry.path, text: await this.dash.storage.file.read(entry.path) });
24
+ }
25
+ }
26
+ const archives: Archive[] = entries
27
+ .filter((entry) => entry.kind === 'folder' && /^archive\/[^/]+$/.test(entry.path))
28
+ .map((entry) => ({
29
+ path: entry.path,
30
+ time: new Date(Number(entry.name)).toLocaleTimeString('en-GB'),
31
+ }))
32
+ .reverse();
33
+ const documents = entries.filter(
34
+ (entry) => entry.kind === 'file' && entry.path.startsWith('documents/'),
35
+ );
36
+ const { used, quota } = await this.dash.storage.file.usage();
37
+ const kept = await this.dash.storage.file.stat(PICTURE);
38
+ this.setState({
39
+ songs,
40
+ archives,
41
+ documents,
42
+ usage: `${used} bytes used of ${quota / 1024 / 1024} MB`,
43
+ picture: kept ? (await this.dash.storage.file.open(PICTURE)).url : '',
44
+ });
45
+ },
46
+ async save(state) {
47
+ await this.dash.storage.file.write(`setlist/${Date.now()}.txt`, state.draft);
48
+ this.setState({ draft: '' });
49
+ await this.actions.refresh();
50
+ },
51
+ // Moves the whole setlist folder into the archive, under the time it was archived.
52
+ async archive() {
53
+ await this.dash.storage.file.createFolder('archive');
54
+ await this.dash.storage.file.move('setlist', `archive/${Date.now()}`);
55
+ await this.actions.refresh();
56
+ },
57
+ // Copies an archived setlist back, in place of the current one.
58
+ async copyBack(_state, event: DataEvent<{ path: string }>) {
59
+ await this.dash.storage.file.copy(event.data.path, 'setlist', { overwrite: true });
60
+ await this.actions.refresh();
61
+ },
62
+ async clearSetlist() {
63
+ await this.dash.storage.file.remove('setlist', { recursive: true });
64
+ await this.actions.refresh();
65
+ },
66
+ async forgetPicture() {
67
+ await this.dash.storage.file.remove(PICTURE);
68
+ await this.actions.refresh();
69
+ },
70
+ },
71
+ });
@@ -0,0 +1,23 @@
1
+ import type { FileEntry } from '@monimesl/dot-runtime';
2
+
3
+ export interface Archive {
4
+ path: string;
5
+ // When it was archived, from its folder's name.
6
+ time: string;
7
+ }
8
+
9
+ export interface Song {
10
+ path: string;
11
+ text: string;
12
+ }
13
+
14
+ export default {
15
+ draft: '',
16
+ songs: [] as Song[],
17
+ // The setlists archived in folders of their own, newest first.
18
+ archives: [] as Archive[],
19
+ usage: '',
20
+ picture: '',
21
+ // Files saved into the dot's documents folder, such as a ticket from the dash.media page.
22
+ documents: [] as FileEntry[],
23
+ };
@@ -0,0 +1,78 @@
1
+ <View>
2
+ <Text variant="title">dash.storage.file</Text>
3
+ <Text>Each dot has its own folder on the phone. Other dots can't see it, and its files stay after the dot closes.</Text>
4
+
5
+ <View gap="small">
6
+ <Text variant="subtitle">write, read and list</Text>
7
+ <Text variant="caption" tone="muted">A setlist, where each song is a text file. Add some, then close and reopen the dot: they are still here.</Text>
8
+ <View variant="card" gap="small">
9
+ <Input label="Song" placeholder="Exodus, by Drizilik" value="{{ draft }}" maxLength="80" onChange="setDraft" onSubmit="save" />
10
+ <View direction="row" gap="small" wrap>
11
+ <Button onClick="save" disabled="{{ !draft }}">Add to the setlist</Button>
12
+ <Button variant="secondary" onClick="archive" disabled="{{ songs.length === 0 }}">Archive it</Button>
13
+ <Button variant="secondary" onClick="clearSetlist" disabled="{{ songs.length === 0 }}">Clear it</Button>
14
+ </View>
15
+ <For each="{{ songs }}" item="song" key="path">
16
+ <View gap="none">
17
+ <Text>{{ song.text }}</Text>
18
+ <Text variant="caption" tone="muted">{{ song.path }}</Text>
19
+ </View>
20
+ </For>
21
+ <If condition="{{ songs.length === 0 }}">
22
+ <Text variant="caption" tone="muted">No songs yet</Text>
23
+ </If>
24
+ </View>
25
+ </View>
26
+
27
+ <View gap="small">
28
+ <Text variant="subtitle">createFolder, move, copy and remove</Text>
29
+ <Text variant="caption" tone="muted">Archive it moves the setlist folder into archive/. Tap an archived setlist to copy it back. Clear it removes the folder and all in it.</Text>
30
+ <View variant="card" gap="small">
31
+ <If condition="{{ archives.length > 0 }}">
32
+ <List>
33
+ <For each="{{ archives }}" item="archive" key="path">
34
+ <ListItem icon="playlist" title="Archived at {{ archive.time }}" subtitle="{{ archive.path }}" value="Copy back" data-path="{{ archive.path }}" onTap="copyBack" />
35
+ </For>
36
+ </List>
37
+ </If>
38
+ <Else>
39
+ <Text variant="caption" tone="muted">None yet. Add songs, then archive the setlist.</Text>
40
+ </Else>
41
+ </View>
42
+ </View>
43
+
44
+ <View gap="small">
45
+ <Text variant="subtitle">save</Text>
46
+ <Text variant="caption" tone="muted">A picked photo or file, or a download, goes when the dot closes. save copies it into the dot's folder to keep it. These are the tickets kept on the dash.media page.</Text>
47
+ <View variant="card" gap="small">
48
+ <For each="{{ documents }}" item="document" key="path">
49
+ <View gap="none">
50
+ <Text>{{ document.name }}</Text>
51
+ <Text variant="caption" tone="muted">{{ document.path }}</Text>
52
+ </View>
53
+ </For>
54
+ <If condition="{{ documents.length === 0 }}">
55
+ <Text variant="caption" tone="muted">None yet. On the dash.media page, attach a ticket and tap Keep it.</Text>
56
+ </If>
57
+ </View>
58
+ </View>
59
+
60
+ <View gap="small">
61
+ <Text variant="subtitle">stat and open</Text>
62
+ <Text variant="caption" tone="muted">stat tells whether a file is there. open gives an address to show it with. This is the artwork kept on the dash.network page.</Text>
63
+ <View variant="card" gap="small">
64
+ <If condition="{{ picture }}">
65
+ <Image src="{{ picture }}" alt="Kept artwork" width="64" height="64" />
66
+ <Button variant="secondary" onClick="forgetPicture">Forget it</Button>
67
+ </If>
68
+ <Else>
69
+ <Text variant="caption" tone="muted">None yet. On the dash.network page, search, download the artwork and tap Keep.</Text>
70
+ </Else>
71
+ </View>
72
+ </View>
73
+
74
+ <View gap="small">
75
+ <Text variant="subtitle">usage</Text>
76
+ <Text>{{ usage }}</Text>
77
+ </View>
78
+ </View>
@@ -0,0 +1,65 @@
1
+ import type { PhotoSource } from '@monimesl/dot-runtime';
2
+ import { errorMessage, unlessCancelled } from '@monimesl/dot-runtime';
3
+
4
+ import state from './state';
5
+
6
+ export default definePage({
7
+ initialState: state,
8
+ configs: { navigationBar: { title: 'Photos, files and scanning' } },
9
+ // unlessCancelled gives undefined when the person closes the picker or scanner.
10
+ actions: {
11
+ async pickPhoto(_state, event: DataEvent<{ source: PhotoSource }>) {
12
+ try {
13
+ const photo = await unlessCancelled(
14
+ this.dash.media.pickPhoto({ source: event.data.source }),
15
+ );
16
+ if (!photo) {
17
+ return;
18
+ }
19
+ const details = `${photo.name}, ${Math.round(photo.size / 1024)} KB as a JPEG`;
20
+ this.setState({ photo, photoDetails: details, note: '' });
21
+ } catch (error) {
22
+ this.setState({ note: errorMessage(error) });
23
+ }
24
+ },
25
+ async pickTicket() {
26
+ try {
27
+ const ticket = await unlessCancelled(
28
+ this.dash.media.pickFile({ types: ['application/pdf', 'image/*'] }),
29
+ );
30
+ if (!ticket) {
31
+ return;
32
+ }
33
+ const ticketSize = `${Math.round(ticket.size / 1024)} KB`;
34
+ this.setState({ ticket, ticketSize, ticketKept: false, note: '' });
35
+ } catch (error) {
36
+ this.setState({ note: errorMessage(error) });
37
+ }
38
+ },
39
+ // A picked file goes when the dot closes, so it's saved into the dot's own folder.
40
+ async keepTicket(state) {
41
+ if (!state.ticket) {
42
+ return;
43
+ }
44
+ try {
45
+ await this.dash.storage.file.createFolder('documents');
46
+ await this.dash.storage.file.save(state.ticket, `documents/${state.ticket.name}`, {
47
+ overwrite: true,
48
+ });
49
+ this.setState({ ticketKept: true, note: '' });
50
+ } catch (error) {
51
+ this.setState({ note: errorMessage(error) });
52
+ }
53
+ },
54
+ async scanTicket() {
55
+ try {
56
+ const scanned = await unlessCancelled(this.dash.media.scanCode());
57
+ if (scanned) {
58
+ this.setState({ scanned: scanned.text, note: '' });
59
+ }
60
+ } catch (error) {
61
+ this.setState({ note: errorMessage(error) });
62
+ }
63
+ },
64
+ },
65
+ });
@@ -0,0 +1,12 @@
1
+ import type { DotFile } from '@monimesl/dot-runtime';
2
+
3
+ export default {
4
+ photo: null as DotFile | null,
5
+ photoDetails: '',
6
+ ticket: null as DotFile | null,
7
+ ticketSize: '',
8
+ // Whether the ticket was saved into the dot's documents folder.
9
+ ticketKept: false,
10
+ scanned: '',
11
+ note: '',
12
+ };
@@ -0,0 +1,53 @@
1
+ <View>
2
+ <Text variant="title">dash.media</Text>
3
+ <Text>Photos, files and codes from the phone. Each needs a permission in dot.json, which Dash asks the person for the first time.</Text>
4
+
5
+ <View gap="small">
6
+ <Text variant="subtitle">pickPhoto</Text>
7
+ <Text variant="caption" tone="muted">source="library" needs the photoLibrary permission, and source="camera" the camera one. The photo comes back as a JPEG file.</Text>
8
+ <View variant="card" gap="small">
9
+ <View direction="row" wrap gap="small">
10
+ <Button variant="secondary" data-source="library" onClick="pickPhoto">Choose a photo</Button>
11
+ <Button variant="secondary" data-source="camera" onClick="pickPhoto">Take a photo</Button>
12
+ </View>
13
+ <If condition="{{ photo }}">
14
+ <Image src="{{ photo.url }}" alt="The photo you picked" aspect="4:3" fit="contain" />
15
+ <Text variant="caption" tone="muted">{{ photoDetails }}. It goes when the dot closes, unless the dot saves it with dash.storage.file.save.</Text>
16
+ </If>
17
+ </View>
18
+ </View>
19
+
20
+ <View gap="small">
21
+ <Text variant="subtitle">pickFile</Text>
22
+ <Text variant="caption" tone="muted">Lets the person choose a file from the phone, of at most 25 MB, with the files permission. types narrows what they can choose, here a PDF or a picture. Like a photo, it goes when the dot closes, so keep it with dash.storage.file.save.</Text>
23
+ <View variant="card" gap="small">
24
+ <Button variant="secondary" onClick="pickTicket">Attach your concert ticket</Button>
25
+ <If condition="{{ ticket }}">
26
+ <DetailRow label="Name" value="{{ ticket.name }}" />
27
+ <DetailRow label="Size" value="{{ ticketSize }}" />
28
+ <DetailRow label="Type" value="{{ ticket.contentType }}" />
29
+ <If condition="{{ ticketKept }}">
30
+ <Text variant="caption" tone="success">Kept in documents/. The dash.storage.file page lists it.</Text>
31
+ </If>
32
+ <Else>
33
+ <Button variant="secondary" onClick="keepTicket">Keep it</Button>
34
+ </Else>
35
+ </If>
36
+ </View>
37
+ </View>
38
+
39
+ <View gap="small">
40
+ <Text variant="subtitle">scanCode</Text>
41
+ <Text variant="caption" tone="muted">Opens Dash's scanner until it reads a QR code. It needs the camera permission.</Text>
42
+ <View variant="card" gap="small">
43
+ <Button variant="secondary" onClick="scanTicket">Scan a ticket's QR code</Button>
44
+ <If condition="{{ scanned }}">
45
+ <Text>{{ scanned }}</Text>
46
+ </If>
47
+ </View>
48
+ </View>
49
+
50
+ <If condition="{{ note }}">
51
+ <Notice tone="danger">{{ note }}</Notice>
52
+ </If>
53
+ </View>
@@ -0,0 +1,215 @@
1
+ import { debounce, errorMessage, latest, unlessCancelled } from '@monimesl/dot-runtime';
2
+
3
+ import type { Release } from './state';
4
+ import state from './state';
5
+
6
+ const PICTURE = 'pictures/artwork.jpg';
7
+
8
+ // Apple's public catalog search, which needs no key: https://performance-partners.apple.com/search-api
9
+ interface CatalogAnswer {
10
+ results: {
11
+ trackId: number;
12
+ trackName: string;
13
+ collectionName: string;
14
+ releaseDate: string;
15
+ artworkUrl100: string;
16
+ }[];
17
+ }
18
+
19
+ interface ArtistAnswer {
20
+ results: { artistId: number; artistName: string; primaryGenreName?: string }[];
21
+ }
22
+
23
+ interface Echo {
24
+ json: unknown;
25
+ files: Record<string, string>;
26
+ }
27
+
28
+ // Songs to ask for at a time, and at most.
29
+ const PAGE = 10;
30
+ const MOST = 50;
31
+
32
+ // Songs or artists whose names match `term`, at most `limit` of them.
33
+ function catalogUrl(entity: 'song' | 'musicArtist', term: string, limit: number): string {
34
+ return `https://itunes.apple.com/search?entity=${entity}&limit=${limit}&term=${encodeURIComponent(term)}`;
35
+ }
36
+
37
+ // For searching as the person types: a pause in typing, and only the newest search's answer.
38
+ const typingPause = debounce(400);
39
+ const artistSearch = latest();
40
+
41
+ export default definePage({
42
+ initialState: state,
43
+ configs: { navigationBar: { title: 'Network' } },
44
+ mutations: {
45
+ setArtist: (_state, event: ValueEvent) => ({ artist: event.detail.value }),
46
+ },
47
+ actions: {
48
+ async suggest(_state, event: ValueEvent) {
49
+ const typed = event.detail.value;
50
+ this.setState({ typed });
51
+ if (!typed.trim()) {
52
+ this.setState({ suggestions: [] });
53
+ return;
54
+ }
55
+ // False while the person is still typing: a later keystroke runs this again.
56
+ if (!(await typingPause(this.dash))) {
57
+ return;
58
+ }
59
+ try {
60
+ const answer = await unlessCancelled(
61
+ artistSearch(
62
+ this.dash.network.http.request<ArtistAnswer>({
63
+ url: catalogUrl('musicArtist', typed, 5),
64
+ as: 'json',
65
+ }),
66
+ ),
67
+ );
68
+ // Undefined when a newer search took its place.
69
+ if (answer) {
70
+ this.setState({
71
+ suggestions: answer.body.results.map((artist) => ({
72
+ id: artist.artistId,
73
+ name: artist.artistName,
74
+ genre: artist.primaryGenreName ?? '',
75
+ })),
76
+ });
77
+ }
78
+ } catch (error) {
79
+ this.setState({ note: errorMessage(error) });
80
+ }
81
+ },
82
+ async checkStatus() {
83
+ try {
84
+ const { online, type } = await this.dash.network.status();
85
+ this.setState({ connection: online ? `Online, by ${type}` : 'Offline' });
86
+ } catch (error) {
87
+ this.setState({ connection: errorMessage(error) });
88
+ }
89
+ },
90
+ async searchAsText(state) {
91
+ try {
92
+ const response = await this.dash.network.http.request({
93
+ url: catalogUrl('song', state.artist, PAGE),
94
+ });
95
+ this.setState({
96
+ raw: response.ok
97
+ ? `${response.body.trim().slice(0, 90)}…`
98
+ : `Apple answered ${response.status}`,
99
+ });
100
+ } catch (error) {
101
+ this.setState({ note: errorMessage(error) });
102
+ }
103
+ },
104
+ async search() {
105
+ this.setState({ limit: PAGE, releases: [] });
106
+ await this.actions.load();
107
+ },
108
+ // The List asks for this as its last song comes into view: the next page, while there is one.
109
+ async loadMore(state) {
110
+ if (state.searching || state.releases.length < state.limit || state.limit >= MOST) {
111
+ return;
112
+ }
113
+ this.setState({ limit: state.limit + PAGE });
114
+ await this.actions.load();
115
+ },
116
+ async load(state) {
117
+ this.setState({ searching: true, note: '' });
118
+ try {
119
+ const { body } = await this.dash.network.http.request<CatalogAnswer>(
120
+ { url: catalogUrl('song', state.artist, state.limit), as: 'json' },
121
+ { timeout: 10_000 },
122
+ );
123
+ const releases: Release[] = body.results.map((result) => ({
124
+ id: result.trackId,
125
+ title: result.trackName,
126
+ album: result.collectionName,
127
+ year: result.releaseDate.slice(0, 4),
128
+ artwork: result.artworkUrl100,
129
+ }));
130
+ this.setState({ releases });
131
+ } catch (error) {
132
+ this.setState({ note: errorMessage(error) });
133
+ } finally {
134
+ this.setState({ searching: false });
135
+ }
136
+ },
137
+ async sendJson(state) {
138
+ try {
139
+ const { body } = await this.dash.network.http.request<Echo>({
140
+ url: 'https://httpbin.org/post',
141
+ method: 'POST',
142
+ body: { artist: state.artist, favourite: state.releases[0]?.title ?? null },
143
+ as: 'json',
144
+ });
145
+ this.setState({ echo: JSON.stringify(body.json) });
146
+ } catch (error) {
147
+ this.setState({ note: errorMessage(error) });
148
+ }
149
+ },
150
+ async download(state) {
151
+ const release = state.releases[0];
152
+ if (!release) {
153
+ return;
154
+ }
155
+ try {
156
+ // The catalog's artwork comes in any size its address names.
157
+ const response = await this.dash.network.http.download({
158
+ url: release.artwork.replace('100x100bb', '300x300bb'),
159
+ name: 'artwork.jpg',
160
+ });
161
+ if (response.ok) {
162
+ this.setState({ picture: response.file, kept: false });
163
+ } else {
164
+ this.setState({ note: `Apple answered ${response.status}` });
165
+ }
166
+ } catch (error) {
167
+ this.setState({ note: errorMessage(error) });
168
+ }
169
+ },
170
+ async upload(state) {
171
+ if (!state.picture) {
172
+ return;
173
+ }
174
+ try {
175
+ const { body } = await this.dash.network.http.upload<Echo>({
176
+ url: 'https://httpbin.org/post',
177
+ fields: { artist: state.artist },
178
+ files: [{ field: 'artwork', file: state.picture }],
179
+ as: 'json',
180
+ });
181
+ this.setState({ uploaded: Object.keys(body.files).join(', ') });
182
+ } catch (error) {
183
+ this.setState({ note: errorMessage(error) });
184
+ }
185
+ },
186
+ async sharePicture(state) {
187
+ if (!state.picture) {
188
+ return;
189
+ }
190
+ try {
191
+ await this.dash.system.share({ file: state.picture });
192
+ } catch (error) {
193
+ this.setState({ note: errorMessage(error) });
194
+ }
195
+ },
196
+ async keep(state) {
197
+ if (!state.picture) {
198
+ return;
199
+ }
200
+ try {
201
+ await this.dash.storage.file.save(state.picture, PICTURE, { overwrite: true });
202
+ this.setState({ kept: true });
203
+ } catch (error) {
204
+ this.setState({ note: errorMessage(error) });
205
+ }
206
+ },
207
+ async tryUnlistedHost() {
208
+ try {
209
+ await this.dash.network.http.request({ url: 'https://example.com' });
210
+ } catch (error) {
211
+ this.setState({ note: errorMessage(error) });
212
+ }
213
+ },
214
+ },
215
+ });
@@ -0,0 +1,35 @@
1
+ import type { DotFile } from '@monimesl/dot-runtime';
2
+
3
+ export interface Suggestion {
4
+ id: number;
5
+ name: string;
6
+ genre: string;
7
+ }
8
+
9
+ export interface Release {
10
+ id: number;
11
+ title: string;
12
+ album: string;
13
+ year: string;
14
+ artwork: string;
15
+ }
16
+
17
+ export default {
18
+ connection: 'Not checked yet',
19
+ // What the person typed to search as they type, and the artists found.
20
+ typed: '',
21
+ suggestions: [] as Suggestion[],
22
+ artist: 'Drizilik',
23
+ // The start of Apple's answer, as the text it came as.
24
+ raw: '',
25
+ releases: [] as Release[],
26
+ // How many songs to ask for: more each time the list's end comes into view.
27
+ limit: 10,
28
+ searching: false,
29
+ echo: '',
30
+ // The downloaded artwork, in a temporary file until the dot closes.
31
+ picture: null as DotFile | null,
32
+ uploaded: '',
33
+ kept: false,
34
+ note: '',
35
+ };
@@ -0,0 +1,108 @@
1
+ <View>
2
+ <Text variant="title">dash.network</Text>
3
+ <Text>Whether the phone is online, and the dot's requests, which Dash sends only to the hosts listed in dot.json. This page searches Apple's music catalog.</Text>
4
+
5
+ <View gap="small">
6
+ <Text variant="subtitle">status</Text>
7
+ <Text variant="caption" tone="muted">Whether the phone is online now, and how: wifi, cellular, other or none. Online doesn't promise a server answers.</Text>
8
+ <View variant="card" direction="row" align="center" gap="medium">
9
+ <View grow>
10
+ <Text>{{ connection }}</Text>
11
+ </View>
12
+ <Button variant="secondary" onClick="checkStatus">Check</Button>
13
+ </View>
14
+ </View>
15
+
16
+ <View gap="small">
17
+ <Text variant="subtitle">http.request</Text>
18
+ <Text variant="caption" tone="muted">The answer comes as text, or as JSON with as: 'json'. timeout gives up after a while.</Text>
19
+ <View variant="card" gap="small">
20
+ <Input label="Artist" placeholder="Drizilik" value="{{ artist }}" maxLength="40" onChange="setArtist" onSubmit="search" />
21
+ <View direction="row" wrap gap="small">
22
+ <Button onClick="search" loading="{{ searching }}" disabled="{{ !artist }}">Search</Button>
23
+ <Button variant="secondary" onClick="searchAsText" disabled="{{ !artist }}">Read as text</Button>
24
+ </View>
25
+ <If condition="{{ raw }}">
26
+ <Text variant="caption" tone="muted">The answer as text starts with: {{ raw }}</Text>
27
+ </If>
28
+ </View>
29
+
30
+ <Text variant="caption" tone="muted">The same answer read as JSON, with artwork. Near the end, the list loads 10 more, up to 50. {{ releases.length }} so far.</Text>
31
+ <If condition="{{ releases.length > 0 }}">
32
+ <List onEndReached="loadMore">
33
+ <For each="{{ releases }}" item="release" key="id">
34
+ <ListItem image="{{ release.artwork }}" title="{{ release.title }}" subtitle="{{ release.album }} · {{ release.year }}" />
35
+ </For>
36
+ </List>
37
+ </If>
38
+ <Else>
39
+ <View variant="card">
40
+ <EmptyState icon="search" title="No songs yet" text="Search for an artist, such as Drizilik, Kao Denero or Emmerson." />
41
+ </View>
42
+ </Else>
43
+ </View>
44
+
45
+ <View gap="small">
46
+ <Text variant="subtitle">Searching as you type: debounce and latest</Text>
47
+ <Text variant="caption" tone="muted">debounce waits for a pause in typing before searching, and latest cancels a search still running when a newer one starts, so an older answer can't replace a newer one.</Text>
48
+ <View variant="card" gap="small">
49
+ <Input type="search" placeholder="Type an artist, such as Emmerson" clearable value="{{ typed }}" onChange="suggest" />
50
+ <For each="{{ suggestions }}" item="artist" key="id">
51
+ <View direction="row" align="center" gap="medium">
52
+ <Avatar name="{{ artist.name }}" size="small" />
53
+ <View gap="none">
54
+ <Text>{{ artist.name }}</Text>
55
+ <Text variant="caption" tone="muted">{{ artist.genre }}</Text>
56
+ </View>
57
+ </View>
58
+ </For>
59
+ </View>
60
+ </View>
61
+
62
+ <View gap="small">
63
+ <Text variant="subtitle">Sending JSON</Text>
64
+ <Text variant="caption" tone="muted">method: 'POST' with a body. httpbin sends back what it got.</Text>
65
+ <View variant="card" gap="small">
66
+ <Button variant="secondary" onClick="sendJson">Send the first song as JSON</Button>
67
+ <If condition="{{ echo }}">
68
+ <Text>httpbin got {{ echo }}</Text>
69
+ </If>
70
+ </View>
71
+ </View>
72
+
73
+ <View gap="small">
74
+ <Text variant="subtitle">http.download and http.upload</Text>
75
+ <Text variant="caption" tone="muted">download saves the answer as a file. upload sends files with form fields. Then share the file, or keep it in the dot's folder.</Text>
76
+ <View variant="card" gap="small">
77
+ <View direction="row" wrap gap="small">
78
+ <Button variant="secondary" onClick="download" disabled="{{ releases.length === 0 }}">Download</Button>
79
+ <Button variant="secondary" onClick="upload" disabled="{{ !picture }}">Upload</Button>
80
+ <Button variant="secondary" onClick="sharePicture" disabled="{{ !picture }}">Share</Button>
81
+ <Button variant="secondary" onClick="keep" disabled="{{ !picture || kept }}">Keep</Button>
82
+ </View>
83
+ <If condition="{{ picture }}">
84
+ <View direction="row" align="center" gap="medium">
85
+ <Image src="{{ picture.url }}" alt="Artwork" width="64" height="64" />
86
+ <Text variant="caption" tone="muted">{{ picture.size }} bytes, in a temporary file until the dot closes</Text>
87
+ </View>
88
+ </If>
89
+ <If condition="{{ uploaded }}">
90
+ <Text>httpbin got the file field "{{ uploaded }}"</Text>
91
+ </If>
92
+ <If condition="{{ kept }}">
93
+ <Text>Saved in the dot's folder. See the dash.storage.file page.</Text>
94
+ </If>
95
+ </View>
96
+ </View>
97
+
98
+ <View gap="small">
99
+ <Text variant="subtitle">Hosts dot.json doesn't list</Text>
100
+ <Text variant="caption" tone="muted">Dash turns the request down.</Text>
101
+ <View variant="card" gap="small">
102
+ <Button variant="secondary" onClick="tryUnlistedHost">Reach example.com</Button>
103
+ <If condition="{{ note }}">
104
+ <Text variant="caption" tone="muted">{{ note }}</Text>
105
+ </If>
106
+ </View>
107
+ </View>
108
+ </View>