@monimesl/dot-cli 0.1.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -0
- package/bin/dotctl.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +942 -0
- package/dist/cli.js.map +1 -0
- package/dist/server-Dbrgkelo.js +11215 -0
- package/dist/server-Dbrgkelo.js.map +1 -0
- package/dist/templates/showcase/.env +3 -0
- package/dist/templates/showcase/dot.ts +24 -0
- package/dist/templates/showcase/pages/cache/controller.ts +45 -0
- package/dist/templates/showcase/pages/cache/state.ts +17 -0
- package/dist/templates/showcase/pages/cache/view.dml +36 -0
- package/dist/templates/showcase/pages/carousel/controller.ts +40 -0
- package/dist/templates/showcase/pages/carousel/state.ts +77 -0
- package/dist/templates/showcase/pages/carousel/view.dml +40 -0
- package/dist/templates/showcase/pages/device/controller.ts +80 -0
- package/dist/templates/showcase/pages/device/state.ts +7 -0
- package/dist/templates/showcase/pages/device/view.dml +49 -0
- package/dist/templates/showcase/pages/feedback/controller.ts +21 -0
- package/dist/templates/showcase/pages/feedback/state.ts +8 -0
- package/dist/templates/showcase/pages/feedback/view.dml +87 -0
- package/dist/templates/showcase/pages/files/controller.ts +66 -0
- package/dist/templates/showcase/pages/files/state.ts +19 -0
- package/dist/templates/showcase/pages/files/view.dml +50 -0
- package/dist/templates/showcase/pages/filters/controller.ts +27 -0
- package/dist/templates/showcase/pages/filters/state.ts +118 -0
- package/dist/templates/showcase/pages/filters/view.dml +44 -0
- package/dist/templates/showcase/pages/forms/controller.ts +68 -0
- package/dist/templates/showcase/pages/forms/state.ts +34 -0
- package/dist/templates/showcase/pages/forms/view.dml +49 -0
- package/dist/templates/showcase/pages/images/controller.ts +41 -0
- package/dist/templates/showcase/pages/images/state.ts +10 -0
- package/dist/templates/showcase/pages/images/view.dml +53 -0
- package/dist/templates/showcase/pages/index/controller.ts +10 -0
- package/dist/templates/showcase/pages/index/state.ts +128 -0
- package/dist/templates/showcase/pages/index/view.dml +20 -0
- package/dist/templates/showcase/pages/layout/controller.ts +60 -0
- package/dist/templates/showcase/pages/layout/state.ts +34 -0
- package/dist/templates/showcase/pages/layout/view.dml +103 -0
- package/dist/templates/showcase/pages/lists/controller.ts +37 -0
- package/dist/templates/showcase/pages/lists/state.ts +117 -0
- package/dist/templates/showcase/pages/lists/view.dml +59 -0
- package/dist/templates/showcase/pages/navigation/controller.ts +65 -0
- package/dist/templates/showcase/pages/navigation/state.ts +17 -0
- package/dist/templates/showcase/pages/navigation/view.dml +34 -0
- package/dist/templates/showcase/pages/network/controller.ts +151 -0
- package/dist/templates/showcase/pages/network/state.ts +22 -0
- package/dist/templates/showcase/pages/network/view.dml +67 -0
- package/dist/templates/showcase/pages/payments/controller.ts +46 -0
- package/dist/templates/showcase/pages/payments/state.ts +13 -0
- package/dist/templates/showcase/pages/payments/view.dml +45 -0
- package/dist/templates/showcase/pages/secrets/controller.ts +43 -0
- package/dist/templates/showcase/pages/secrets/state.ts +5 -0
- package/dist/templates/showcase/pages/secrets/view.dml +24 -0
- package/dist/templates/showcase/pages/state/controller.ts +22 -0
- package/dist/templates/showcase/pages/state/state.ts +10 -0
- package/dist/templates/showcase/pages/state/view.dml +33 -0
- package/dist/templates/showcase/pages/text/controller.ts +10 -0
- package/dist/templates/showcase/pages/text/state.ts +8 -0
- package/dist/templates/showcase/pages/text/view.dml +58 -0
- package/dist/templates/showcase/statics/logo.png +0 -0
- package/dist/templates/starter/.env +3 -0
- package/dist/templates/starter/dot.ts +3 -0
- package/dist/templates/starter/pages/index/controller.ts +9 -0
- package/dist/templates/starter/pages/index/state.ts +4 -0
- package/dist/templates/starter/pages/index/view.dml +16 -0
- package/dist/templates/starter/statics/logo.png +0 -0
- package/package.json +53 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { Release } from './state';
|
|
2
|
+
import state from './state';
|
|
3
|
+
|
|
4
|
+
const PICTURE = 'pictures/artwork.jpg';
|
|
5
|
+
|
|
6
|
+
// Apple's public catalog search, which needs no key: https://performance-partners.apple.com/search-api
|
|
7
|
+
interface CatalogAnswer {
|
|
8
|
+
results: {
|
|
9
|
+
trackId: number;
|
|
10
|
+
trackName: string;
|
|
11
|
+
collectionName: string;
|
|
12
|
+
releaseDate: string;
|
|
13
|
+
artworkUrl100: string;
|
|
14
|
+
}[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Echo {
|
|
18
|
+
json: unknown;
|
|
19
|
+
files: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Songs to ask for at a time, and at most.
|
|
23
|
+
const PAGE = 10;
|
|
24
|
+
const MOST = 50;
|
|
25
|
+
|
|
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}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default definePage({
|
|
32
|
+
initialState: state,
|
|
33
|
+
mutations: {
|
|
34
|
+
setArtist: (_state, event: DotEvent<{ value: string }>) => ({ artist: event.detail.value }),
|
|
35
|
+
},
|
|
36
|
+
actions: {
|
|
37
|
+
async searchAsText(state) {
|
|
38
|
+
try {
|
|
39
|
+
const response = await this.dash.network.http.request({ url: searchUrl(state.artist) });
|
|
40
|
+
this.setState({
|
|
41
|
+
raw: response.ok
|
|
42
|
+
? `${response.body.trim().slice(0, 90)}…`
|
|
43
|
+
: `Apple answered ${response.status}`,
|
|
44
|
+
});
|
|
45
|
+
} catch (error) {
|
|
46
|
+
this.setState({ note: (error as Error).message });
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
async search() {
|
|
50
|
+
this.setState({ limit: PAGE, releases: [] });
|
|
51
|
+
await this.actions.load();
|
|
52
|
+
},
|
|
53
|
+
// The List asks for this as its last song comes into view: the next page, while there is one.
|
|
54
|
+
async loadMore(state) {
|
|
55
|
+
if (state.searching || state.releases.length < state.limit || state.limit >= MOST) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.setState({ limit: state.limit + PAGE });
|
|
59
|
+
await this.actions.load();
|
|
60
|
+
},
|
|
61
|
+
async load(state) {
|
|
62
|
+
this.setState({ searching: true, note: '' });
|
|
63
|
+
try {
|
|
64
|
+
const { body } = await this.dash.network.http.request<CatalogAnswer>(
|
|
65
|
+
{ url: searchUrl(state.artist, state.limit), as: 'json' },
|
|
66
|
+
{ timeout: 10_000 },
|
|
67
|
+
);
|
|
68
|
+
const releases: Release[] = body.results.map((result) => ({
|
|
69
|
+
id: result.trackId,
|
|
70
|
+
title: result.trackName,
|
|
71
|
+
album: result.collectionName,
|
|
72
|
+
year: result.releaseDate.slice(0, 4),
|
|
73
|
+
artwork: result.artworkUrl100,
|
|
74
|
+
}));
|
|
75
|
+
this.setState({ releases });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
this.setState({ note: (error as Error).message });
|
|
78
|
+
}
|
|
79
|
+
this.setState({ searching: false });
|
|
80
|
+
},
|
|
81
|
+
async sendJson(state) {
|
|
82
|
+
try {
|
|
83
|
+
const { body } = await this.dash.network.http.request<Echo>({
|
|
84
|
+
url: 'https://httpbin.org/post',
|
|
85
|
+
method: 'POST',
|
|
86
|
+
body: { artist: state.artist, favourite: state.releases[0]?.title ?? null },
|
|
87
|
+
as: 'json',
|
|
88
|
+
});
|
|
89
|
+
this.setState({ echo: JSON.stringify(body.json) });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
this.setState({ note: (error as Error).message });
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
async download(state) {
|
|
95
|
+
const release = state.releases[0];
|
|
96
|
+
if (!release) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
// The catalog's artwork comes in any size its address names.
|
|
101
|
+
const response = await this.dash.network.http.download({
|
|
102
|
+
url: release.artwork.replace('100x100bb', '300x300bb'),
|
|
103
|
+
name: 'artwork.jpg',
|
|
104
|
+
});
|
|
105
|
+
if (response.ok) {
|
|
106
|
+
const { id, url, size } = response.file;
|
|
107
|
+
this.setState({ picture: { id, url, size }, kept: false });
|
|
108
|
+
} else {
|
|
109
|
+
this.setState({ note: `Apple answered ${response.status}` });
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
this.setState({ note: (error as Error).message });
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
async upload(state) {
|
|
116
|
+
try {
|
|
117
|
+
const { body } = await this.dash.network.http.upload<Echo>({
|
|
118
|
+
url: 'https://httpbin.org/post',
|
|
119
|
+
fields: { artist: state.artist },
|
|
120
|
+
files: [{ field: 'artwork', file: state.picture.id }],
|
|
121
|
+
as: 'json',
|
|
122
|
+
});
|
|
123
|
+
this.setState({ uploaded: Object.keys(body.files).join(', ') });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
this.setState({ note: (error as Error).message });
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
async sharePicture(state) {
|
|
129
|
+
try {
|
|
130
|
+
await this.dash.system.share({ file: state.picture.id });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
this.setState({ note: (error as Error).message });
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
async keep(state) {
|
|
136
|
+
try {
|
|
137
|
+
await this.dash.storage.file.save(state.picture.id, PICTURE, { overwrite: true });
|
|
138
|
+
this.setState({ kept: true });
|
|
139
|
+
} catch (error) {
|
|
140
|
+
this.setState({ note: (error as Error).message });
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
async tryUnlistedHost() {
|
|
144
|
+
try {
|
|
145
|
+
await this.dash.network.http.request({ url: 'https://example.com' });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
this.setState({ note: (error as Error).message });
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface Release {
|
|
2
|
+
id: number;
|
|
3
|
+
title: string;
|
|
4
|
+
album: string;
|
|
5
|
+
year: string;
|
|
6
|
+
artwork: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
artist: 'Drizilik',
|
|
11
|
+
// The start of Apple's answer, as the text it came as.
|
|
12
|
+
raw: '',
|
|
13
|
+
releases: [] as Release[],
|
|
14
|
+
// How many songs to ask for: more each time the list's end comes into view.
|
|
15
|
+
limit: 10,
|
|
16
|
+
searching: false,
|
|
17
|
+
echo: '',
|
|
18
|
+
picture: { id: '', url: '', size: 0 },
|
|
19
|
+
uploaded: '',
|
|
20
|
+
kept: false,
|
|
21
|
+
note: '',
|
|
22
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<View>
|
|
2
|
+
<Text variant="title">Network</Text>
|
|
3
|
+
<Text>Dash sends the dot's requests, only to hosts listed in dot.json. This page searches Apple's music catalog.</Text>
|
|
4
|
+
|
|
5
|
+
<View variant="card" gap="small">
|
|
6
|
+
<Input label="Artist" placeholder="Drizilik" value="{{ artist }}" maxLength="40" onChange="setArtist" onSubmit="search" />
|
|
7
|
+
<View direction="row" wrap gap="small">
|
|
8
|
+
<Button onClick="search" loading="{{ searching }}" disabled="{{ !artist }}">Search</Button>
|
|
9
|
+
<Button variant="secondary" onClick="searchAsText" disabled="{{ !artist }}">Read as text</Button>
|
|
10
|
+
</View>
|
|
11
|
+
<If condition="{{ raw }}">
|
|
12
|
+
<Text variant="caption" tone="muted">The answer as text starts with: {{ raw }}</Text>
|
|
13
|
+
</If>
|
|
14
|
+
</View>
|
|
15
|
+
|
|
16
|
+
<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>
|
|
17
|
+
<If condition="{{ releases.length > 0 }}">
|
|
18
|
+
<List onEndReached="loadMore">
|
|
19
|
+
<For each="{{ releases }}" item="release" key="id">
|
|
20
|
+
<ListItem image="{{ release.artwork }}" title="{{ release.title }}" subtitle="{{ release.album }} · {{ release.year }}" />
|
|
21
|
+
</For>
|
|
22
|
+
</List>
|
|
23
|
+
</If>
|
|
24
|
+
<Else>
|
|
25
|
+
<View variant="card">
|
|
26
|
+
<EmptyState icon="search" title="No songs yet" text="Search for an artist, such as Drizilik, Kao Denero or Emmerson." />
|
|
27
|
+
</View>
|
|
28
|
+
</Else>
|
|
29
|
+
|
|
30
|
+
<Text variant="caption" tone="muted">JSON sent to httpbin, which sends it back</Text>
|
|
31
|
+
<View variant="card" gap="small">
|
|
32
|
+
<Button variant="secondary" onClick="sendJson">Send the first song as JSON</Button>
|
|
33
|
+
<If condition="{{ echo }}">
|
|
34
|
+
<Text>httpbin got {{ echo }}</Text>
|
|
35
|
+
</If>
|
|
36
|
+
</View>
|
|
37
|
+
|
|
38
|
+
<Text variant="caption" tone="muted">Files: download the first song's artwork, upload it, and save it</Text>
|
|
39
|
+
<View variant="card" gap="small">
|
|
40
|
+
<View direction="row" wrap gap="small">
|
|
41
|
+
<Button variant="secondary" onClick="download" disabled="{{ releases.length === 0 }}">Download</Button>
|
|
42
|
+
<Button variant="secondary" onClick="upload" disabled="{{ !picture.id }}">Upload</Button>
|
|
43
|
+
<Button variant="secondary" onClick="sharePicture" disabled="{{ !picture.id }}">Share</Button>
|
|
44
|
+
<Button variant="secondary" onClick="keep" disabled="{{ !picture.id || kept }}">Keep</Button>
|
|
45
|
+
</View>
|
|
46
|
+
<If condition="{{ picture.url }}">
|
|
47
|
+
<View direction="row" align="center" gap="medium">
|
|
48
|
+
<Image src="{{ picture.url }}" alt="Artwork" width="64" height="64" />
|
|
49
|
+
<Text variant="caption" tone="muted">{{ picture.size }} bytes, in a temporary file until the dot closes</Text>
|
|
50
|
+
</View>
|
|
51
|
+
</If>
|
|
52
|
+
<If condition="{{ uploaded }}">
|
|
53
|
+
<Text>httpbin got the file field "{{ uploaded }}"</Text>
|
|
54
|
+
</If>
|
|
55
|
+
<If condition="{{ kept }}">
|
|
56
|
+
<Text>Saved in the dot's files. See the Files page.</Text>
|
|
57
|
+
</If>
|
|
58
|
+
</View>
|
|
59
|
+
|
|
60
|
+
<Text variant="caption" tone="muted">A host that dot.json does not list</Text>
|
|
61
|
+
<View variant="card" gap="small">
|
|
62
|
+
<Button variant="secondary" onClick="tryUnlistedHost">Reach example.com</Button>
|
|
63
|
+
<If condition="{{ note }}">
|
|
64
|
+
<Text variant="caption" tone="muted">{{ note }}</Text>
|
|
65
|
+
</If>
|
|
66
|
+
</View>
|
|
67
|
+
</View>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import state from './state';
|
|
2
|
+
|
|
3
|
+
type Typed = DotEvent<{ value: string }>;
|
|
4
|
+
|
|
5
|
+
// The code this page accepts; a real dot checks it with its server.
|
|
6
|
+
const RIGHT_CODE = '123456';
|
|
7
|
+
|
|
8
|
+
// In dollars, as the pledge field sends them.
|
|
9
|
+
const MAX_PLEDGE = 1000;
|
|
10
|
+
|
|
11
|
+
export default definePage({
|
|
12
|
+
initialState: state,
|
|
13
|
+
mutations: {
|
|
14
|
+
setAmount: (_state, event: DotEvent<{ value: number | null }>) => {
|
|
15
|
+
const amount = event.detail.value;
|
|
16
|
+
if (!amount) {
|
|
17
|
+
return { amount, amountText: '', request: '' };
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
amount,
|
|
21
|
+
amountText: (amount / 100).toLocaleString('en-US', { minimumFractionDigits: 2 }),
|
|
22
|
+
request: `https://pay.example.com/tip?amount=${amount}¤cy=SLE&in=cents`,
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
setCode: (_state, event: Typed) => ({ code: event.detail.value, codeError: '' }),
|
|
26
|
+
checkCode: (_state, event: Typed) => {
|
|
27
|
+
if (event.detail.value === RIGHT_CODE) {
|
|
28
|
+
return { verified: true };
|
|
29
|
+
}
|
|
30
|
+
return { code: '', codeError: 'That code is wrong. Try again.' };
|
|
31
|
+
},
|
|
32
|
+
startAgain: () => ({ verified: false, code: '' }),
|
|
33
|
+
setPin: (_state, event: Typed) => ({ pin: event.detail.value }),
|
|
34
|
+
clearPin: () => ({ pin: '' }),
|
|
35
|
+
setLocked: (_state, event: DotEvent<{ checked: boolean }>) => ({
|
|
36
|
+
locked: event.detail.checked,
|
|
37
|
+
}),
|
|
38
|
+
setPledge: (_state, event: DotEvent<{ value: number | null }>) => {
|
|
39
|
+
const pledge = event.detail.value;
|
|
40
|
+
return {
|
|
41
|
+
pledge,
|
|
42
|
+
pledgeError: pledge !== null && pledge > MAX_PLEDGE ? 'Pledge up to USD 1,000' : '',
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
amount: null as number | null,
|
|
3
|
+
// The amount with its thousands grouped, and what the QR code asks for.
|
|
4
|
+
amountText: '',
|
|
5
|
+
request: '',
|
|
6
|
+
code: '',
|
|
7
|
+
codeError: '',
|
|
8
|
+
verified: false,
|
|
9
|
+
pin: '',
|
|
10
|
+
locked: false,
|
|
11
|
+
pledge: null as number | null,
|
|
12
|
+
pledgeError: '',
|
|
13
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<View>
|
|
2
|
+
<Text variant="title">Payments</Text>
|
|
3
|
+
<Text>Fields for money and codes. Tip the band with a QR code. Confirm tickets with a code.</Text>
|
|
4
|
+
|
|
5
|
+
<View gap="small">
|
|
6
|
+
<Text variant="caption" tone="muted">An amount. It is formatted as you type and sent as a number.</Text>
|
|
7
|
+
<AmountInput label="Tip for the band" value="{{ amount }}" disabled="{{ locked }}" onChange="setAmount" hint="The QR code below asks for it" />
|
|
8
|
+
<Switch label="Lock the tip" checked="{{ locked }}" onChange="setLocked" />
|
|
9
|
+
<View variant="card" align="center" gap="medium">
|
|
10
|
+
<If condition="{{ request }}">
|
|
11
|
+
<QRCode value="{{ request }}" size="180" label="A QR code to tip SLE {{ amountText }}" />
|
|
12
|
+
<Text tone="muted">Scan to tip the band SLE {{ amountText }}</Text>
|
|
13
|
+
</If>
|
|
14
|
+
<Else>
|
|
15
|
+
<EmptyState icon="qr-code" title="No tip yet" text="Type an amount above to make a QR code the crowd can scan." />
|
|
16
|
+
</Else>
|
|
17
|
+
</View>
|
|
18
|
+
</View>
|
|
19
|
+
|
|
20
|
+
<View gap="small">
|
|
21
|
+
<Text variant="caption" tone="muted">Another currency: US dollars. This one sends dollars, not cents</Text>
|
|
22
|
+
<AmountInput label="Pledge for the band's tour" currency="USD" unit="major" value="{{ pledge }}" error="{{ pledgeError }}" hint="Up to USD 1,000" onChange="setPledge" />
|
|
23
|
+
</View>
|
|
24
|
+
|
|
25
|
+
<View gap="small">
|
|
26
|
+
<Text variant="caption" tone="muted">A one-time code. Type it, paste it, or let the phone fill it from an SMS.</Text>
|
|
27
|
+
<If condition="{{ verified }}">
|
|
28
|
+
<Notice tone="success" title="Code accepted">Your two tickets are confirmed.</Notice>
|
|
29
|
+
<Button variant="secondary" onClick="startAgain">Start again</Button>
|
|
30
|
+
</If>
|
|
31
|
+
<Else>
|
|
32
|
+
<CodeInput label="The code we sent to 076 •••• 21" value="{{ code }}" onChange="setCode" onComplete="checkCode" error="{{ codeError }}" hint="Use 123456" />
|
|
33
|
+
</Else>
|
|
34
|
+
</View>
|
|
35
|
+
|
|
36
|
+
<View gap="small">
|
|
37
|
+
<Text variant="caption" tone="muted">A PIN. It is hidden as you type and locks when complete.</Text>
|
|
38
|
+
<CodeInput label="PIN" length="4" secure value="{{ pin }}" disabled="{{ pin.length === 4 }}" onChange="setPin" />
|
|
39
|
+
<View direction="row" align="center" gap="small">
|
|
40
|
+
<Text tone="muted">{{ pin.length }} of 4 digits</Text>
|
|
41
|
+
<View grow />
|
|
42
|
+
<Button variant="secondary" onClick="clearPin" disabled="{{ pin.length === 0 }}">Clear the PIN</Button>
|
|
43
|
+
</View>
|
|
44
|
+
</View>
|
|
45
|
+
</View>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import state from './state';
|
|
2
|
+
|
|
3
|
+
const SESSION = 'session';
|
|
4
|
+
const NAME = 'session-name';
|
|
5
|
+
|
|
6
|
+
export default definePage({
|
|
7
|
+
initialState: state,
|
|
8
|
+
hooks: {
|
|
9
|
+
async onShow() {
|
|
10
|
+
await this.actions.refresh();
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
mutations: {
|
|
14
|
+
setName: (_state, event: DotEvent<{ value: string }>) => ({ name: event.detail.value }),
|
|
15
|
+
},
|
|
16
|
+
actions: {
|
|
17
|
+
async refresh() {
|
|
18
|
+
const token = await this.dash.storage.secret.get(SESSION);
|
|
19
|
+
const signedInAs = await this.dash.storage.cache.get<string>(NAME);
|
|
20
|
+
this.setState({
|
|
21
|
+
token: token ? `${token.slice(0, 12)}…` : '',
|
|
22
|
+
signedInAs: signedInAs ?? '',
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
async signIn(state) {
|
|
26
|
+
// A real dot gets its token from its own API, once the person signs in there.
|
|
27
|
+
const token = `demo-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
|
|
28
|
+
await this.dash.storage.secret.set(SESSION, token);
|
|
29
|
+
await this.dash.storage.cache.set(NAME, state.name);
|
|
30
|
+
this.setState({ name: '' });
|
|
31
|
+
await this.actions.refresh();
|
|
32
|
+
},
|
|
33
|
+
async signOut() {
|
|
34
|
+
await this.dash.storage.secret.remove(SESSION);
|
|
35
|
+
await this.dash.storage.cache.remove(NAME);
|
|
36
|
+
await this.actions.refresh();
|
|
37
|
+
},
|
|
38
|
+
async forgetAll() {
|
|
39
|
+
await this.dash.storage.secret.clear();
|
|
40
|
+
await this.actions.refresh();
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<View>
|
|
2
|
+
<Text variant="title">Secrets</Text>
|
|
3
|
+
<Text>Secrets hold sign-in tokens from the dot's own server. Dash keeps them encrypted on the phone. A dot can keep up to 8.</Text>
|
|
4
|
+
|
|
5
|
+
<Text variant="caption" tone="muted">A sign-in that stays</Text>
|
|
6
|
+
<View variant="card" gap="small">
|
|
7
|
+
<If condition="{{ token }}">
|
|
8
|
+
<Text>Signed in as {{ signedInAs }}</Text>
|
|
9
|
+
<Text variant="caption" tone="muted">Session token {{ token }}</Text>
|
|
10
|
+
<View direction="row" gap="small">
|
|
11
|
+
<Button variant="secondary" onClick="signOut">Sign out</Button>
|
|
12
|
+
<Button variant="secondary" onClick="forgetAll">Forget every secret</Button>
|
|
13
|
+
</View>
|
|
14
|
+
</If>
|
|
15
|
+
<Else>
|
|
16
|
+
<Input label="Name" placeholder="Mariama" value="{{ name }}" maxLength="40" onChange="setName" onSubmit="signIn" />
|
|
17
|
+
<Button onClick="signIn" disabled="{{ !name }}">Sign in</Button>
|
|
18
|
+
<Text variant="caption" tone="muted">Sign in, then close and reopen the dot. You stay signed in.</Text>
|
|
19
|
+
</Else>
|
|
20
|
+
</View>
|
|
21
|
+
|
|
22
|
+
<Text variant="caption" tone="muted">Your name is saved in the cache. Only the token is a secret.</Text>
|
|
23
|
+
<Text variant="caption" tone="muted">Never put your API keys in the dot's code: it is open to read.</Text>
|
|
24
|
+
</View>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import state from './state';
|
|
2
|
+
|
|
3
|
+
export default definePage({
|
|
4
|
+
initialState: state,
|
|
5
|
+
mutations: {
|
|
6
|
+
increment: (state) => {
|
|
7
|
+
const count = state.count + 1;
|
|
8
|
+
return { count, taps: [{ id: count }, ...state.taps] };
|
|
9
|
+
},
|
|
10
|
+
reset: () => ({ count: 0, taps: [] }),
|
|
11
|
+
},
|
|
12
|
+
actions: {
|
|
13
|
+
fillUp() {
|
|
14
|
+
while (this.state.count < this.state.limit) {
|
|
15
|
+
this.mutations.increment();
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
undo() {
|
|
19
|
+
this.setState((current) => ({ count: current.count - 1, taps: current.taps.slice(1) }));
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<View>
|
|
2
|
+
<Text variant="title">State, mutations and actions</Text>
|
|
3
|
+
<Text>The page keeps its data in state. A mutation changes state when something happens. An action can run several steps or wait for Dash. Here, the crowd wants one more song.</Text>
|
|
4
|
+
|
|
5
|
+
<View variant="card" gap="small">
|
|
6
|
+
<Text variant="title">{{ count }} of {{ limit }} {{ title }}</Text>
|
|
7
|
+
<If condition="{{ count >= limit }}">
|
|
8
|
+
<Text>Encore! The band comes back on</Text>
|
|
9
|
+
</If>
|
|
10
|
+
<ElseIf condition="{{ count > 0 }}">
|
|
11
|
+
<Text>{{ limit - count }} more and the band comes back</Text>
|
|
12
|
+
</ElseIf>
|
|
13
|
+
<Else>
|
|
14
|
+
<Text>Tap Cheer to start</Text>
|
|
15
|
+
</Else>
|
|
16
|
+
<View direction="row" wrap gap="small">
|
|
17
|
+
<Button onClick="increment" disabled="{{ count >= limit }}">Cheer</Button>
|
|
18
|
+
<Button variant="secondary" onClick="fillUp" disabled="{{ count >= limit }}">Whole crowd</Button>
|
|
19
|
+
<Button variant="secondary" onClick="undo" disabled="{{ count === 0 }}">Undo</Button>
|
|
20
|
+
<Button variant="secondary" onClick="reset" disabled="{{ count === 0 }}">Reset</Button>
|
|
21
|
+
</View>
|
|
22
|
+
</View>
|
|
23
|
+
|
|
24
|
+
<Text variant="caption" tone="muted">Each cheer, newest first</Text>
|
|
25
|
+
<View variant="card" gap="small">
|
|
26
|
+
<For each="{{ taps }}" index="position" item="tap" key="id">
|
|
27
|
+
<Text>{{ position + 1 }}. Cheer #{{ tap.id }}</Text>
|
|
28
|
+
</For>
|
|
29
|
+
<If condition="{{ taps.length === 0 }}">
|
|
30
|
+
<Text variant="caption" tone="muted">No cheers yet</Text>
|
|
31
|
+
</If>
|
|
32
|
+
</View>
|
|
33
|
+
</View>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import state from './state';
|
|
2
|
+
|
|
3
|
+
export default definePage({
|
|
4
|
+
initialState: state,
|
|
5
|
+
mutations: {
|
|
6
|
+
addTicket: (state) => ({ tickets: state.tickets + 1 }),
|
|
7
|
+
removeTicket: (state) => ({ tickets: Math.max(0, state.tickets - 1) }),
|
|
8
|
+
toggleStageName: (state) => ({ stageName: state.stageName === null ? 'Lady M' : null }),
|
|
9
|
+
},
|
|
10
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<View>
|
|
2
|
+
<Text variant="title">Text and bindings</Text>
|
|
3
|
+
|
|
4
|
+
<View variant="card" gap="small">
|
|
5
|
+
<Text variant="title">Palm-wine</Text>
|
|
6
|
+
<Text>Guitar music for a calm evening. This is body text.</Text>
|
|
7
|
+
<Text variant="caption" tone="muted">S. E. Rogie and Ebenezer Calendar, in a caption</Text>
|
|
8
|
+
<Text>Text can hold{{ ' ' }}<Text variant="title">bigger</Text>{{ ' ' }}and{{ ' ' }}<Text variant="caption" tone="muted">smaller</Text>{{ ' ' }}words.</Text>
|
|
9
|
+
</View>
|
|
10
|
+
|
|
11
|
+
<Text variant="caption" tone="muted">Links open a web page or run something on this page</Text>
|
|
12
|
+
<View variant="card" gap="small">
|
|
13
|
+
<Text>Read about{{ ' ' }}<Link href="https://en.wikipedia.org/wiki/Music_of_Sierra_Leone">the music of Sierra Leone</Link>{{ ' ' }}on Wikipedia, in the phone's browser.</Text>
|
|
14
|
+
<View direction="row" gap="medium">
|
|
15
|
+
<Link onTap="addTicket">Add a ticket below</Link>
|
|
16
|
+
<Link tone="danger" onTap="removeTicket">Remove one</Link>
|
|
17
|
+
</View>
|
|
18
|
+
</View>
|
|
19
|
+
|
|
20
|
+
<Text variant="caption" tone="muted">Tones, weights and alignment</Text>
|
|
21
|
+
<View variant="card" gap="small">
|
|
22
|
+
<View direction="row" wrap gap="medium">
|
|
23
|
+
<Text tone="muted">muted</Text>
|
|
24
|
+
<Text tone="info">info</Text>
|
|
25
|
+
<Text tone="success">success</Text>
|
|
26
|
+
<Text tone="warning">warning</Text>
|
|
27
|
+
<Text tone="danger">danger</Text>
|
|
28
|
+
</View>
|
|
29
|
+
<View direction="row" gap="medium">
|
|
30
|
+
<Text weight="regular">regular</Text>
|
|
31
|
+
<Text weight="medium">medium</Text>
|
|
32
|
+
<Text weight="bold">bold</Text>
|
|
33
|
+
</View>
|
|
34
|
+
<Text align="center">In the center</Text>
|
|
35
|
+
<Text align="end">At the end</Text>
|
|
36
|
+
<Text maxLines="2">Sierra Leone's Refugee All Stars came together in refugee camps in Guinea, recorded their first album, Living Like a Refugee, back in Freetown, and went on to play Bonnaroo, open for Aerosmith and appear on Oprah. Two lines are allowed here, so the rest is cut off.</Text>
|
|
37
|
+
</View>
|
|
38
|
+
|
|
39
|
+
<Text variant="caption" tone="muted">Bindings show state in the view</Text>
|
|
40
|
+
<View variant="card" gap="small">
|
|
41
|
+
<Text>Hello, {{ name }}</Text>
|
|
42
|
+
<Text>{{ tickets }} {{ tickets === 1 ? 'ticket' : 'tickets' }} at Le {{ price }} each: Le {{ tickets * price }}</Text>
|
|
43
|
+
<Text>Stage name: {{ stageName ?? 'none yet' }}</Text>
|
|
44
|
+
<Text>{{ genres.length }} favourite genres, the first is "{{ genres[0] }}"</Text>
|
|
45
|
+
<View direction="row" wrap gap="small">
|
|
46
|
+
<Button variant="secondary" onClick="addTicket">Add a ticket</Button>
|
|
47
|
+
<Button variant="secondary" onClick="removeTicket" disabled="{{ tickets === 0 }}">Remove one</Button>
|
|
48
|
+
<Button variant="secondary" onClick="toggleStageName">{{ stageName === null ? 'Give a stage name' : 'Drop the stage name' }}</Button>
|
|
49
|
+
</View>
|
|
50
|
+
</View>
|
|
51
|
+
|
|
52
|
+
<Text variant="caption" tone="muted">A list, one line per item</Text>
|
|
53
|
+
<View variant="card" gap="small">
|
|
54
|
+
<For each="{{ genres }}" item="genre" index="position" key="*this">
|
|
55
|
+
<Text>{{ position + 1 }}. {{ genre }}</Text>
|
|
56
|
+
</For>
|
|
57
|
+
</View>
|
|
58
|
+
</View>
|
|
Binary file
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<View>
|
|
2
|
+
<View direction="row" align="center" gap="medium">
|
|
3
|
+
<Image src="statics/logo.png" alt="Logo" width="40" height="40" />
|
|
4
|
+
<Text variant="title">{{ greeting }}</Text>
|
|
5
|
+
</View>
|
|
6
|
+
<Text>Change pages/index/view.dml and save: the simulator shows it at once.</Text>
|
|
7
|
+
|
|
8
|
+
<View variant="card" align="center" gap="medium">
|
|
9
|
+
<Text variant="title">{{ count }}</Text>
|
|
10
|
+
<Text tone="muted">{{ count === 1 ? 'tap' : 'taps' }} so far</Text>
|
|
11
|
+
<View direction="row" gap="small">
|
|
12
|
+
<Button variant="secondary" onClick="reset" disabled="{{ count === 0 }}">Reset</Button>
|
|
13
|
+
<Button onClick="increment">Tap me</Button>
|
|
14
|
+
</View>
|
|
15
|
+
</View>
|
|
16
|
+
</View>
|
|
Binary file
|