@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.
- package/dist/cli.js +381 -261
- package/dist/cli.js.map +1 -1
- package/dist/{server-uIyYmYnp.js → server-B9-F93mS.js} +539 -464
- package/dist/server-B9-F93mS.js.map +1 -0
- package/dist/templates/showcase/dot.ts +2 -0
- package/dist/templates/showcase/pages/basics/bars/controller.ts +1 -1
- package/dist/templates/showcase/pages/basics/control-flow/controller.ts +1 -1
- package/dist/templates/showcase/pages/basics/events/controller.ts +4 -4
- package/dist/templates/showcase/pages/basics/events/view.dml +2 -2
- package/dist/templates/showcase/pages/basics/hooks/controller.ts +6 -0
- package/dist/templates/showcase/pages/basics/hooks/view.dml +5 -0
- package/dist/templates/showcase/pages/basics/navigation/controller.ts +7 -0
- package/dist/templates/showcase/pages/basics/navigation/state.ts +2 -0
- package/dist/templates/showcase/pages/basics/navigation/view.dml +11 -0
- package/dist/templates/showcase/pages/basics/pick-genre/controller.ts +12 -0
- package/dist/templates/showcase/pages/basics/pick-genre/state.ts +3 -0
- package/dist/templates/showcase/pages/basics/pick-genre/view.dml +9 -0
- package/dist/templates/showcase/pages/components/carousel/controller.ts +1 -1
- package/dist/templates/showcase/pages/components/feedback/controller.ts +12 -2
- package/dist/templates/showcase/pages/components/feedback/state.ts +33 -0
- package/dist/templates/showcase/pages/components/feedback/view.dml +21 -1
- package/dist/templates/showcase/pages/components/forms/controller.ts +22 -22
- package/dist/templates/showcase/pages/components/forms/state.ts +3 -3
- package/dist/templates/showcase/pages/components/forms/view.dml +9 -2
- package/dist/templates/showcase/pages/components/icons/controller.ts +1 -1
- package/dist/templates/showcase/pages/components/images/controller.ts +3 -1
- package/dist/templates/showcase/pages/components/images/view.dml +13 -0
- package/dist/templates/showcase/pages/components/layout/controller.ts +2 -2
- package/dist/templates/showcase/pages/components/lists/controller.ts +1 -1
- package/dist/templates/showcase/pages/components/lists/view.dml +18 -0
- package/dist/templates/showcase/pages/components/money/controller.ts +6 -8
- package/dist/templates/showcase/pages/components/sheets/controller.ts +2 -2
- package/dist/templates/showcase/pages/components/tabs/controller.ts +22 -10
- package/dist/templates/showcase/pages/components/tabs/state.ts +1 -0
- package/dist/templates/showcase/pages/components/tabs/view.dml +4 -3
- package/dist/templates/showcase/pages/dash/cache/controller.ts +2 -2
- package/dist/templates/showcase/pages/dash/calls/controller.ts +7 -6
- package/dist/templates/showcase/pages/dash/calls/state.ts +1 -1
- package/dist/templates/showcase/pages/dash/calls/view.dml +3 -2
- package/dist/templates/showcase/pages/dash/contacts/controller.ts +30 -0
- package/dist/templates/showcase/pages/dash/contacts/state.ts +6 -0
- package/dist/templates/showcase/pages/dash/contacts/view.dml +33 -0
- package/dist/templates/showcase/pages/dash/files/controller.ts +15 -13
- package/dist/templates/showcase/pages/dash/files/state.ts +3 -6
- package/dist/templates/showcase/pages/dash/media/controller.ts +16 -13
- package/dist/templates/showcase/pages/dash/network/controller.ts +71 -16
- package/dist/templates/showcase/pages/dash/network/state.ts +10 -0
- package/dist/templates/showcase/pages/dash/network/view.dml +32 -4
- package/dist/templates/showcase/pages/dash/secrets/controller.ts +1 -1
- package/dist/templates/showcase/pages/dash/system/controller.ts +32 -5
- package/dist/templates/showcase/pages/dash/system/state.ts +6 -2
- package/dist/templates/showcase/pages/dash/system/view.dml +39 -1
- package/dist/templates/showcase/pages/dash/ui/controller.ts +23 -23
- package/dist/templates/showcase/pages/dash/ui/view.dml +1 -1
- package/dist/templates/showcase/pages/index/controller.ts +1 -1
- package/dist/templates/showcase/pages/index/state.ts +17 -10
- package/package.json +6 -6
- 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:
|
|
8
|
+
chooseTab: (state, event: ValueEvent) => {
|
|
11
9
|
const tab = event.detail.value;
|
|
12
|
-
return { tab, shown: filter(state.releases,
|
|
10
|
+
return { tab, shown: filter(state.releases, { ...state, tab }) };
|
|
13
11
|
},
|
|
14
|
-
chooseGenre: (state, event:
|
|
12
|
+
chooseGenre: (state, event: DataEvent<{ genre: string }>) => {
|
|
15
13
|
const { genre } = event.data;
|
|
16
|
-
return { genre, shown: filter(state.releases, state
|
|
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:
|
|
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
|
-
|
|
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) &&
|
|
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
|
|
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:
|
|
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:
|
|
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 {
|
|
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:
|
|
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
|
|
36
|
+
if (isDashError(error)) {
|
|
37
37
|
this.setState({ code: error.code, message: error.message });
|
|
38
38
|
} else {
|
|
39
|
-
this.setState({ code: 'not a
|
|
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
|
<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
|
|
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">
|
|
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,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 {
|
|
1
|
+
import { errorMessage, unlessCancelled } from '@monimesl/dot-runtime';
|
|
2
2
|
|
|
3
|
-
import type { Archive,
|
|
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:
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
22
|
+
documents: [] as FileEntry[],
|
|
26
23
|
attachNote: '',
|
|
27
24
|
};
|
|
@@ -1,32 +1,35 @@
|
|
|
1
|
-
import {
|
|
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:
|
|
11
|
+
async pickPhoto(_state, event: DataEvent<{ source: PhotoSource }>) {
|
|
16
12
|
try {
|
|
17
|
-
const photo = await
|
|
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:
|
|
22
|
+
this.setState({ note: errorMessage(error) });
|
|
22
23
|
}
|
|
23
24
|
},
|
|
24
25
|
async scanTicket() {
|
|
25
26
|
try {
|
|
26
|
-
const
|
|
27
|
-
|
|
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:
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
return `https://itunes.apple.com/search?entity
|
|
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
|
|
43
|
+
configs: { navigationBar: { title: 'dash.network' } },
|
|
34
44
|
mutations: {
|
|
35
|
-
setArtist: (_state, event:
|
|
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({
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3
|
-
<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">
|
|
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:
|
|
15
|
+
setName: (_state, event: ValueEvent) => ({ name: event.detail.value }),
|
|
16
16
|
},
|
|
17
17
|
actions: {
|
|
18
18
|
async refresh() {
|