@monimesl/dot-cli 0.1.0-alpha.19 → 0.1.0-alpha.21

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 (58) hide show
  1. package/dist/cli.js +381 -261
  2. package/dist/cli.js.map +1 -1
  3. package/dist/{server-uIyYmYnp.js → server-B9-F93mS.js} +539 -464
  4. package/dist/server-B9-F93mS.js.map +1 -0
  5. package/dist/templates/showcase/dot.ts +2 -0
  6. package/dist/templates/showcase/pages/basics/bars/controller.ts +1 -1
  7. package/dist/templates/showcase/pages/basics/control-flow/controller.ts +1 -1
  8. package/dist/templates/showcase/pages/basics/events/controller.ts +4 -4
  9. package/dist/templates/showcase/pages/basics/events/view.dml +2 -2
  10. package/dist/templates/showcase/pages/basics/hooks/controller.ts +6 -0
  11. package/dist/templates/showcase/pages/basics/hooks/view.dml +5 -0
  12. package/dist/templates/showcase/pages/basics/navigation/controller.ts +7 -0
  13. package/dist/templates/showcase/pages/basics/navigation/state.ts +2 -0
  14. package/dist/templates/showcase/pages/basics/navigation/view.dml +11 -0
  15. package/dist/templates/showcase/pages/basics/pick-genre/controller.ts +12 -0
  16. package/dist/templates/showcase/pages/basics/pick-genre/state.ts +3 -0
  17. package/dist/templates/showcase/pages/basics/pick-genre/view.dml +9 -0
  18. package/dist/templates/showcase/pages/components/carousel/controller.ts +1 -1
  19. package/dist/templates/showcase/pages/components/feedback/controller.ts +12 -2
  20. package/dist/templates/showcase/pages/components/feedback/state.ts +33 -0
  21. package/dist/templates/showcase/pages/components/feedback/view.dml +21 -1
  22. package/dist/templates/showcase/pages/components/forms/controller.ts +22 -22
  23. package/dist/templates/showcase/pages/components/forms/state.ts +3 -3
  24. package/dist/templates/showcase/pages/components/forms/view.dml +9 -2
  25. package/dist/templates/showcase/pages/components/icons/controller.ts +1 -1
  26. package/dist/templates/showcase/pages/components/images/controller.ts +3 -1
  27. package/dist/templates/showcase/pages/components/images/view.dml +13 -0
  28. package/dist/templates/showcase/pages/components/layout/controller.ts +2 -2
  29. package/dist/templates/showcase/pages/components/lists/controller.ts +1 -1
  30. package/dist/templates/showcase/pages/components/lists/view.dml +18 -0
  31. package/dist/templates/showcase/pages/components/money/controller.ts +6 -8
  32. package/dist/templates/showcase/pages/components/sheets/controller.ts +2 -2
  33. package/dist/templates/showcase/pages/components/tabs/controller.ts +22 -10
  34. package/dist/templates/showcase/pages/components/tabs/state.ts +1 -0
  35. package/dist/templates/showcase/pages/components/tabs/view.dml +4 -3
  36. package/dist/templates/showcase/pages/dash/cache/controller.ts +2 -2
  37. package/dist/templates/showcase/pages/dash/calls/controller.ts +7 -6
  38. package/dist/templates/showcase/pages/dash/calls/state.ts +1 -1
  39. package/dist/templates/showcase/pages/dash/calls/view.dml +3 -2
  40. package/dist/templates/showcase/pages/dash/contacts/controller.ts +30 -0
  41. package/dist/templates/showcase/pages/dash/contacts/state.ts +6 -0
  42. package/dist/templates/showcase/pages/dash/contacts/view.dml +33 -0
  43. package/dist/templates/showcase/pages/dash/files/controller.ts +15 -13
  44. package/dist/templates/showcase/pages/dash/files/state.ts +3 -6
  45. package/dist/templates/showcase/pages/dash/media/controller.ts +16 -13
  46. package/dist/templates/showcase/pages/dash/network/controller.ts +71 -16
  47. package/dist/templates/showcase/pages/dash/network/state.ts +10 -0
  48. package/dist/templates/showcase/pages/dash/network/view.dml +32 -4
  49. package/dist/templates/showcase/pages/dash/secrets/controller.ts +1 -1
  50. package/dist/templates/showcase/pages/dash/system/controller.ts +32 -5
  51. package/dist/templates/showcase/pages/dash/system/state.ts +6 -2
  52. package/dist/templates/showcase/pages/dash/system/view.dml +39 -1
  53. package/dist/templates/showcase/pages/dash/ui/controller.ts +23 -23
  54. package/dist/templates/showcase/pages/dash/ui/view.dml +1 -1
  55. package/dist/templates/showcase/pages/index/controller.ts +1 -1
  56. package/dist/templates/showcase/pages/index/state.ts +17 -10
  57. package/package.json +6 -6
  58. package/dist/server-uIyYmYnp.js.map +0 -1
@@ -1,29 +1,41 @@
1
1
  import type { Release } from './state';
2
2
  import state from './state';
3
3
 
4
- type Chosen = DotEvent<{ value: string }>;
5
-
6
4
  export default definePage({
7
5
  initialState: state,
8
6
  configs: { navigationBar: { title: 'Tabs and chips' } },
9
7
  mutations: {
10
- chooseTab: (state, event: Chosen) => {
8
+ chooseTab: (state, event: ValueEvent) => {
11
9
  const tab = event.detail.value;
12
- return { tab, shown: filter(state.releases, tab, state.genre) };
10
+ return { tab, shown: filter(state.releases, { ...state, tab }) };
13
11
  },
14
- chooseGenre: (state, event: DotEvent<unknown, { genre: string }>) => {
12
+ chooseGenre: (state, event: DataEvent<{ genre: string }>) => {
15
13
  const { genre } = event.data;
16
- return { genre, shown: filter(state.releases, state.tab, genre) };
14
+ return { genre, shown: filter(state.releases, { ...state, genre }) };
15
+ },
16
+ search: (state, event: ValueEvent) => {
17
+ const query = event.detail.value;
18
+ return { query, shown: filter(state.releases, { ...state, query }) };
17
19
  },
18
- clearFilters: (state) => ({ tab: 'all', genre: 'All', shown: state.releases }),
19
- chooseWhere: (_state, event: Chosen) => ({ where: event.detail.value }),
20
+ clearFilters: (state) => ({ tab: 'all', genre: 'All', query: '', shown: state.releases }),
21
+ chooseWhere: (_state, event: ValueEvent) => ({ where: event.detail.value }),
20
22
  toggleLiked: (state) => ({ liked: !state.liked }),
21
23
  },
22
24
  });
23
25
 
24
- function filter(releases: readonly Release[], tab: string, genre: string): Release[] {
26
+ interface Filters {
27
+ tab: string;
28
+ genre: string;
29
+ // Words the title contains, in any case.
30
+ query: string;
31
+ }
32
+
33
+ function filter(releases: readonly Release[], { tab, genre, query }: Filters): Release[] {
34
+ const words = query.trim().toLowerCase();
25
35
  return releases.filter(
26
36
  (release) =>
27
- (tab === 'all' || release.kind === tab) && (genre === 'All' || release.genre === genre),
37
+ (tab === 'all' || release.kind === tab) &&
38
+ (genre === 'All' || release.genre === genre) &&
39
+ release.title.toLowerCase().includes(words),
28
40
  );
29
41
  }
@@ -67,6 +67,7 @@ export default {
67
67
  songs: releases.filter((item) => item.kind === 'song').length,
68
68
  genres: ['All', 'Kriofusion', 'Afropop', 'Hip hop', 'R&B', 'Palm-wine', 'Gospel', 'Bubu'],
69
69
  genre: 'All',
70
+ query: '',
70
71
  liked: false,
71
72
  // Shows at home in Sierra Leone, and abroad in the Mano River region.
72
73
  where: 'home',
@@ -1,15 +1,16 @@
1
1
  <View>
2
2
  <Text variant="title">Tabs and chips</Text>
3
- <Text>Tabs change what the page shows. Chips narrow it down. Try Songs with Palm-wine: nothing matches.</Text>
3
+ <Text>Tabs change what the page shows. Chips and a search narrow it down. Try Songs with Palm-wine: nothing matches.</Text>
4
4
 
5
5
  <View gap="small">
6
6
  <Text variant="subtitle">Tabs and Chip</Text>
7
- <Text variant="caption" tone="muted">The underline tabs, with a badge for each count. A chip is selected when the page says so.</Text>
7
+ <Text variant="caption" tone="muted">The underline tabs, with a badge for each count. A chip is selected when the page says so. An Input with type="search" and clearable makes a search box.</Text>
8
8
  <Tabs variant="underline" value="{{ tab }}" onChange="chooseTab">
9
9
  <Tab value="all" label="All" />
10
10
  <Tab value="album" label="Albums" badge="{{ albums }}" />
11
11
  <Tab value="song" label="Songs" badge="{{ songs }}" />
12
12
  </Tabs>
13
+ <Input type="search" placeholder="Search by title" clearable value="{{ query }}" onChange="search" />
13
14
  <View direction="row" gap="small" wrap>
14
15
  <For each="{{ genres }}" item="option" key="*this">
15
16
  <Chip label="{{ option }}" selected="{{ option === genre }}" data-genre="{{ option }}" onTap="chooseGenre" />
@@ -25,7 +26,7 @@
25
26
  </List>
26
27
  </If>
27
28
  <Else>
28
- <EmptyState icon="search" title="Nothing here" text="No release matches this tab and genre.">
29
+ <EmptyState icon="search" title="Nothing here" text="No release matches this tab, genre and search.">
29
30
  <Button variant="secondary" onClick="clearFilters">Clear filters</Button>
30
31
  </EmptyState>
31
32
  </Else>
@@ -27,14 +27,14 @@ export default definePage({
27
27
  });
28
28
  },
29
29
  // Each view lasts 30 seconds, then the cache forgets it.
30
- async view(_state, event: DotEvent<unknown, { id: string; name: string }>) {
30
+ async view(_state, event: DataEvent<{ id: string; name: string }>) {
31
31
  await this.dash.storage.cache.set(`${RECENT}${event.data.id}`, event.data.name, {
32
32
  ttl: RECENT_SECONDS,
33
33
  });
34
34
  await this.actions.refresh();
35
35
  },
36
36
  // With no ttl, the genre lasts until it's removed.
37
- async setGenre(_state, event: DotEvent<{ value: string }>) {
37
+ async setGenre(_state, event: ValueEvent) {
38
38
  await this.dash.storage.cache.set(GENRE, event.detail.value);
39
39
  await this.actions.refresh();
40
40
  },
@@ -1,5 +1,5 @@
1
1
  import type { DashRequest } from '@monimesl/dot-runtime';
2
- import { HostError } from '@monimesl/dot-runtime';
2
+ import { errorMessage, isDashError } from '@monimesl/dot-runtime';
3
3
 
4
4
  import state from './state';
5
5
 
@@ -11,7 +11,7 @@ export default definePage({
11
11
  configs: { navigationBar: { title: 'Calls and errors' } },
12
12
  actions: {
13
13
  // Makes the call a button names, and shows how it ended.
14
- async show(_state, event: DotEvent<unknown, { call: string }>) {
14
+ async show(_state, event: DataEvent<{ call: string }>) {
15
15
  this.setState({ code: '', message: '' });
16
16
  try {
17
17
  switch (event.data.call) {
@@ -33,14 +33,15 @@ export default definePage({
33
33
  }
34
34
  this.setState({ code: 'none', message: 'It worked' });
35
35
  } catch (error) {
36
- if (error instanceof HostError) {
36
+ if (isDashError(error)) {
37
37
  this.setState({ code: error.code, message: error.message });
38
38
  } else {
39
- this.setState({ code: 'not a HostError', message: (error as Error).message });
39
+ this.setState({ code: 'not a DashError', message: errorMessage(error) });
40
40
  }
41
+ } finally {
42
+ wait = null;
43
+ this.setState({ waiting: false });
41
44
  }
42
- wait = null;
43
- this.setState({ waiting: false });
44
45
  },
45
46
  cancelWait() {
46
47
  wait?.cancel();
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  waiting: false,
3
- // The last call's outcome: the HostError's code and message, or how it ended.
3
+ // The last call's outcome: the DashError's code and message, or how it ended.
4
4
  code: '',
5
5
  message: '',
6
6
  };
@@ -1,6 +1,6 @@
1
1
  <View>
2
2
  <Text variant="title">Calls and errors</Text>
3
- <Text>Every this.dash call goes to Dash and comes back later, so await it. When it fails, it throws a HostError with a code that says why.</Text>
3
+ <Text>Every this.dash call goes to Dash and comes back later, so await it. When it fails, it throws a DashError with a code that says why.</Text>
4
4
 
5
5
  <View gap="small">
6
6
  <Text variant="subtitle">timeout</Text>
@@ -18,7 +18,8 @@
18
18
  </View>
19
19
 
20
20
  <View gap="small">
21
- <Text variant="subtitle">HostError codes</Text>
21
+ <Text variant="subtitle">DashError codes</Text>
22
+ <Text variant="caption" tone="muted">isDashError(error) tells a Dash call's error from any other, and isDashError(error, 'timeout') also checks its code. Each code has its own check too, such as isDashCancelled(error).</Text>
22
23
  <View variant="card" gap="none">
23
24
  <DetailRow label="cancelled" value="The person closed it, or the call was cancelled" />
24
25
  <DetailRow label="timeout" value="It took longer than the timeout" />
@@ -0,0 +1,30 @@
1
+ import { errorMessage, unlessCancelled } from '@monimesl/dot-runtime';
2
+
3
+ import state from './state';
4
+
5
+ export default definePage({
6
+ initialState: state,
7
+ configs: { navigationBar: { title: 'dash.contacts' } },
8
+ actions: {
9
+ async choose() {
10
+ try {
11
+ const contact = await unlessCancelled(this.dash.contacts.pickPhone());
12
+ if (contact) {
13
+ this.setState({ contact, note: '' });
14
+ }
15
+ } catch (error) {
16
+ this.setState({ note: errorMessage(error) });
17
+ }
18
+ },
19
+ async call(state) {
20
+ if (!state.contact) {
21
+ return;
22
+ }
23
+ try {
24
+ await this.dash.system.dial(state.contact.phone);
25
+ } catch (error) {
26
+ this.setState({ note: errorMessage(error) });
27
+ }
28
+ },
29
+ },
30
+ });
@@ -0,0 +1,6 @@
1
+ import type { PickedContact } from '@monimesl/dot-runtime';
2
+
3
+ export default {
4
+ contact: null as PickedContact | null,
5
+ note: '',
6
+ };
@@ -0,0 +1,33 @@
1
+ <View>
2
+ <Text variant="title">dash.contacts</Text>
3
+ <Text>The phone's own contacts. The dot sees only the one the person chooses.</Text>
4
+
5
+ <View gap="small">
6
+ <Text variant="subtitle">pickPhone</Text>
7
+ <Text variant="caption" tone="muted">The person chooses one phone number, such as who to send money to. It needs the contacts permission in dot.json.</Text>
8
+ <View variant="card" gap="medium">
9
+ <If condition="{{ contact }}">
10
+ <View direction="row" align="center" gap="medium">
11
+ <Avatar name="{{ contact.name }}" />
12
+ <View grow gap="none">
13
+ <Text weight="medium">{{ contact.name || 'No name' }}</Text>
14
+ <Text variant="caption" tone="muted">{{ contact.phone }}</Text>
15
+ </View>
16
+ </View>
17
+ <View direction="row" gap="small">
18
+ <Button variant="secondary" onClick="choose">Choose another</Button>
19
+ <Button variant="secondary" onClick="call">Call</Button>
20
+ </View>
21
+ </If>
22
+ <Else>
23
+ <EmptyState icon="users" title="No one chosen" text="Choose a contact to send money to.">
24
+ <Button onClick="choose">Choose a contact</Button>
25
+ </EmptyState>
26
+ </Else>
27
+ </View>
28
+ </View>
29
+
30
+ <If condition="{{ note }}">
31
+ <Notice tone="danger">{{ note }}</Notice>
32
+ </If>
33
+ </View>
@@ -1,6 +1,6 @@
1
- import { HostError } from '@monimesl/dot-runtime';
1
+ import { errorMessage, unlessCancelled } from '@monimesl/dot-runtime';
2
2
 
3
- import type { Archive, Document, Song } from './state';
3
+ import type { Archive, Song } from './state';
4
4
  import state from './state';
5
5
 
6
6
  const PICTURE = 'pictures/artwork.jpg';
@@ -14,7 +14,7 @@ export default definePage({
14
14
  },
15
15
  },
16
16
  mutations: {
17
- setDraft: (_state, event: DotEvent<{ value: string }>) => ({ draft: event.detail.value }),
17
+ setDraft: (_state, event: ValueEvent) => ({ draft: event.detail.value }),
18
18
  },
19
19
  actions: {
20
20
  async refresh() {
@@ -32,9 +32,9 @@ export default definePage({
32
32
  time: new Date(Number(entry.name)).toLocaleTimeString('en-GB'),
33
33
  }))
34
34
  .reverse();
35
- const documents: Document[] = entries
36
- .filter((entry) => entry.kind === 'file' && entry.path.startsWith('documents/'))
37
- .map((entry) => ({ path: entry.path, name: entry.name }));
35
+ const documents = entries.filter(
36
+ (entry) => entry.kind === 'file' && entry.path.startsWith('documents/'),
37
+ );
38
38
  const { used, quota } = await this.dash.storage.file.usage();
39
39
  const kept = await this.dash.storage.file.stat(PICTURE);
40
40
  this.setState({
@@ -57,7 +57,7 @@ export default definePage({
57
57
  await this.actions.refresh();
58
58
  },
59
59
  // Copies an archived setlist back, in place of the current one.
60
- async copyBack(_state, event: DotEvent<unknown, { path: string }>) {
60
+ async copyBack(_state, event: DataEvent<{ path: string }>) {
61
61
  await this.dash.storage.file.copy(event.data.path, 'setlist', { overwrite: true });
62
62
  await this.actions.refresh();
63
63
  },
@@ -68,17 +68,19 @@ export default definePage({
68
68
  // A picked file goes when the dot closes, so it's saved into the dot's own folder.
69
69
  async attach() {
70
70
  try {
71
- const file = await this.dash.storage.file.pick({ types: ['application/pdf', 'image/*'] });
71
+ // Undefined when the person closes the picker, which isn't a problem.
72
+ const file = await unlessCancelled(
73
+ this.dash.storage.file.pick({ types: ['application/pdf', 'image/*'] }),
74
+ );
75
+ if (!file) {
76
+ return;
77
+ }
72
78
  await this.dash.storage.file.createFolder('documents');
73
79
  await this.dash.storage.file.save(file, `documents/${file.name}`, { overwrite: true });
74
80
  this.setState({ attachNote: '' });
75
81
  await this.actions.refresh();
76
82
  } catch (error) {
77
- // Closing the picker or scanner isn't a problem to show.
78
- const closedByPerson = error instanceof HostError && error.code === 'cancelled';
79
- if (!closedByPerson) {
80
- this.setState({ attachNote: (error as Error).message });
81
- }
83
+ this.setState({ attachNote: errorMessage(error) });
82
84
  }
83
85
  },
84
86
  async forgetPicture() {
@@ -1,14 +1,11 @@
1
+ import type { FileEntry } from '@monimesl/dot-runtime';
2
+
1
3
  export interface Archive {
2
4
  path: string;
3
5
  // When it was archived, from its folder's name.
4
6
  time: string;
5
7
  }
6
8
 
7
- export interface Document {
8
- path: string;
9
- name: string;
10
- }
11
-
12
9
  export interface Song {
13
10
  path: string;
14
11
  text: string;
@@ -22,6 +19,6 @@ export default {
22
19
  usage: '',
23
20
  picture: '',
24
21
  // Files the person attached, kept in the dot's documents folder.
25
- documents: [] as Document[],
22
+ documents: [] as FileEntry[],
26
23
  attachNote: '',
27
24
  };
@@ -1,32 +1,35 @@
1
- import { HostError } from '@monimesl/dot-runtime';
1
+ import type { PhotoSource } from '@monimesl/dot-runtime';
2
+ import { errorMessage, unlessCancelled } from '@monimesl/dot-runtime';
2
3
 
3
4
  import state from './state';
4
5
 
5
- // Closing a picker or scanner isn't a problem to show.
6
- function problemToShow(error: unknown): string {
7
- const closedByPerson = error instanceof HostError && error.code === 'cancelled';
8
- return closedByPerson ? '' : (error as Error).message;
9
- }
10
-
11
6
  export default definePage({
12
7
  initialState: state,
13
8
  configs: { navigationBar: { title: 'dash.media' } },
9
+ // unlessCancelled gives undefined when the person closes the picker or scanner.
14
10
  actions: {
15
- async pickPhoto(_state, event: DotEvent<unknown, { source: 'library' | 'camera' }>) {
11
+ async pickPhoto(_state, event: DataEvent<{ source: PhotoSource }>) {
16
12
  try {
17
- const photo = await this.dash.media.pickPhoto({ source: event.data.source });
13
+ const photo = await unlessCancelled(
14
+ this.dash.media.pickPhoto({ source: event.data.source }),
15
+ );
16
+ if (!photo) {
17
+ return;
18
+ }
18
19
  const details = `${photo.name}, ${Math.round(photo.size / 1024)} KB as a JPEG`;
19
20
  this.setState({ photo, photoDetails: details, note: '' });
20
21
  } catch (error) {
21
- this.setState({ note: problemToShow(error) });
22
+ this.setState({ note: errorMessage(error) });
22
23
  }
23
24
  },
24
25
  async scanTicket() {
25
26
  try {
26
- const { text } = await this.dash.media.scanCode();
27
- this.setState({ scanned: text, note: '' });
27
+ const scanned = await unlessCancelled(this.dash.media.scanCode());
28
+ if (scanned) {
29
+ this.setState({ scanned: scanned.text, note: '' });
30
+ }
28
31
  } catch (error) {
29
- this.setState({ note: problemToShow(error) });
32
+ this.setState({ note: errorMessage(error) });
30
33
  }
31
34
  },
32
35
  },
@@ -1,3 +1,5 @@
1
+ import { debounce, errorMessage, latest, unlessCancelled } from '@monimesl/dot-runtime';
2
+
1
3
  import type { Release } from './state';
2
4
  import state from './state';
3
5
 
@@ -14,6 +16,10 @@ interface CatalogAnswer {
14
16
  }[];
15
17
  }
16
18
 
19
+ interface ArtistAnswer {
20
+ results: { artistId: number; artistName: string; primaryGenreName?: string }[];
21
+ }
22
+
17
23
  interface Echo {
18
24
  json: unknown;
19
25
  files: Record<string, string>;
@@ -23,28 +29,76 @@ interface Echo {
23
29
  const PAGE = 10;
24
30
  const MOST = 50;
25
31
 
26
- function searchUrl(artist: string, limit = PAGE): string {
27
- const term = encodeURIComponent(artist);
28
- return `https://itunes.apple.com/search?entity=song&limit=${limit}&term=${term}`;
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)}`;
29
35
  }
30
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
+
31
41
  export default definePage({
32
42
  initialState: state,
33
- configs: { navigationBar: { title: 'dash.network.http' } },
43
+ configs: { navigationBar: { title: 'dash.network' } },
34
44
  mutations: {
35
- setArtist: (_state, event: DotEvent<{ value: string }>) => ({ artist: event.detail.value }),
45
+ setArtist: (_state, event: ValueEvent) => ({ artist: event.detail.value }),
36
46
  },
37
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
+ },
38
90
  async searchAsText(state) {
39
91
  try {
40
- const response = await this.dash.network.http.request({ url: searchUrl(state.artist) });
92
+ const response = await this.dash.network.http.request({
93
+ url: catalogUrl('song', state.artist, PAGE),
94
+ });
41
95
  this.setState({
42
96
  raw: response.ok
43
97
  ? `${response.body.trim().slice(0, 90)}…`
44
98
  : `Apple answered ${response.status}`,
45
99
  });
46
100
  } catch (error) {
47
- this.setState({ note: (error as Error).message });
101
+ this.setState({ note: errorMessage(error) });
48
102
  }
49
103
  },
50
104
  async search() {
@@ -63,7 +117,7 @@ export default definePage({
63
117
  this.setState({ searching: true, note: '' });
64
118
  try {
65
119
  const { body } = await this.dash.network.http.request<CatalogAnswer>(
66
- { url: searchUrl(state.artist, state.limit), as: 'json' },
120
+ { url: catalogUrl('song', state.artist, state.limit), as: 'json' },
67
121
  { timeout: 10_000 },
68
122
  );
69
123
  const releases: Release[] = body.results.map((result) => ({
@@ -75,9 +129,10 @@ export default definePage({
75
129
  }));
76
130
  this.setState({ releases });
77
131
  } catch (error) {
78
- this.setState({ note: (error as Error).message });
132
+ this.setState({ note: errorMessage(error) });
133
+ } finally {
134
+ this.setState({ searching: false });
79
135
  }
80
- this.setState({ searching: false });
81
136
  },
82
137
  async sendJson(state) {
83
138
  try {
@@ -89,7 +144,7 @@ export default definePage({
89
144
  });
90
145
  this.setState({ echo: JSON.stringify(body.json) });
91
146
  } catch (error) {
92
- this.setState({ note: (error as Error).message });
147
+ this.setState({ note: errorMessage(error) });
93
148
  }
94
149
  },
95
150
  async download(state) {
@@ -109,7 +164,7 @@ export default definePage({
109
164
  this.setState({ note: `Apple answered ${response.status}` });
110
165
  }
111
166
  } catch (error) {
112
- this.setState({ note: (error as Error).message });
167
+ this.setState({ note: errorMessage(error) });
113
168
  }
114
169
  },
115
170
  async upload(state) {
@@ -125,7 +180,7 @@ export default definePage({
125
180
  });
126
181
  this.setState({ uploaded: Object.keys(body.files).join(', ') });
127
182
  } catch (error) {
128
- this.setState({ note: (error as Error).message });
183
+ this.setState({ note: errorMessage(error) });
129
184
  }
130
185
  },
131
186
  async sharePicture(state) {
@@ -135,7 +190,7 @@ export default definePage({
135
190
  try {
136
191
  await this.dash.system.share({ file: state.picture });
137
192
  } catch (error) {
138
- this.setState({ note: (error as Error).message });
193
+ this.setState({ note: errorMessage(error) });
139
194
  }
140
195
  },
141
196
  async keep(state) {
@@ -146,14 +201,14 @@ export default definePage({
146
201
  await this.dash.storage.file.save(state.picture, PICTURE, { overwrite: true });
147
202
  this.setState({ kept: true });
148
203
  } catch (error) {
149
- this.setState({ note: (error as Error).message });
204
+ this.setState({ note: errorMessage(error) });
150
205
  }
151
206
  },
152
207
  async tryUnlistedHost() {
153
208
  try {
154
209
  await this.dash.network.http.request({ url: 'https://example.com' });
155
210
  } catch (error) {
156
- this.setState({ note: (error as Error).message });
211
+ this.setState({ note: errorMessage(error) });
157
212
  }
158
213
  },
159
214
  },
@@ -1,5 +1,11 @@
1
1
  import type { DotFile } from '@monimesl/dot-runtime';
2
2
 
3
+ export interface Suggestion {
4
+ id: number;
5
+ name: string;
6
+ genre: string;
7
+ }
8
+
3
9
  export interface Release {
4
10
  id: number;
5
11
  title: string;
@@ -9,6 +15,10 @@ export interface Release {
9
15
  }
10
16
 
11
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[],
12
22
  artist: 'Drizilik',
13
23
  // The start of Apple's answer, as the text it came as.
14
24
  raw: '',
@@ -1,9 +1,20 @@
1
1
  <View>
2
- <Text variant="title">dash.network.http</Text>
3
- <Text>Dash sends the dot's requests, only to the hosts listed in dot.json. This page searches Apple's music catalog.</Text>
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
4
 
5
5
  <View gap="small">
6
- <Text variant="subtitle">request</Text>
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>
7
18
  <Text variant="caption" tone="muted">The answer comes as text, or as JSON with as: 'json'. timeout gives up after a while.</Text>
8
19
  <View variant="card" gap="small">
9
20
  <Input label="Artist" placeholder="Drizilik" value="{{ artist }}" maxLength="40" onChange="setArtist" onSubmit="search" />
@@ -31,6 +42,23 @@
31
42
  </Else>
32
43
  </View>
33
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
+
34
62
  <View gap="small">
35
63
  <Text variant="subtitle">Sending JSON</Text>
36
64
  <Text variant="caption" tone="muted">method: 'POST' with a body. httpbin sends back what it got.</Text>
@@ -43,7 +71,7 @@
43
71
  </View>
44
72
 
45
73
  <View gap="small">
46
- <Text variant="subtitle">download and upload</Text>
74
+ <Text variant="subtitle">http.download and http.upload</Text>
47
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>
48
76
  <View variant="card" gap="small">
49
77
  <View direction="row" wrap gap="small">
@@ -12,7 +12,7 @@ export default definePage({
12
12
  },
13
13
  },
14
14
  mutations: {
15
- setName: (_state, event: DotEvent<{ value: string }>) => ({ name: event.detail.value }),
15
+ setName: (_state, event: ValueEvent) => ({ name: event.detail.value }),
16
16
  },
17
17
  actions: {
18
18
  async refresh() {