ambient-display 1.1.0
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/.nvmrc +1 -0
- package/.prettierignore +4 -0
- package/.prettierrc +17 -0
- package/CHANGELOG.md +34 -0
- package/README.md +84 -0
- package/ambient.config.js.template +13 -0
- package/env.template +2 -0
- package/eslint.config.js +24 -0
- package/jsconfig.json +19 -0
- package/package.json +67 -0
- package/screenshot.png +0 -0
- package/server/api/index.js +213 -0
- package/server/api/interact.js +234 -0
- package/server/api/utils.js +284 -0
- package/server/comms.js +13 -0
- package/server/config.js +27 -0
- package/server/constants.js +52 -0
- package/server/events.js +41 -0
- package/server/history.js +47 -0
- package/server/index.js +155 -0
- package/server/logs.js +15 -0
- package/server/memo.js +63 -0
- package/server/run.js +5 -0
- package/server/spotify/auth.js +98 -0
- package/server/spotify/index.js +105 -0
- package/server/spotify/sdk.js +217 -0
- package/server/types/comms.js +7 -0
- package/server/types/data.js +94 -0
- package/server/types/index.js +16 -0
- package/server/types/options.js +72 -0
- package/server/utils.js +101 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +12 -0
- package/src/lib/actions/qr.svelte.js +23 -0
- package/src/lib/comms.js +25 -0
- package/src/lib/components/AuthenticateTrigger.svelte +74 -0
- package/src/lib/components/Controls.svelte +91 -0
- package/src/lib/components/ImageLoad.svelte +79 -0
- package/src/lib/components/LoadingIndicator.svelte +75 -0
- package/src/lib/components/MediaItem.svelte +75 -0
- package/src/lib/components/PlayingTracker.svelte +94 -0
- package/src/lib/components/ResultsList.svelte +80 -0
- package/src/lib/components/Toast/Item.svelte +71 -0
- package/src/lib/components/Toast/Manager.svelte +34 -0
- package/src/lib/icons/disc.svg +1 -0
- package/src/lib/index.js +1 -0
- package/src/lib/store.js +146 -0
- package/src/lib/styles.scss +166 -0
- package/src/lib/toast.js +57 -0
- package/src/lib/utils.js +723 -0
- package/src/routes/+layout.server.js +25 -0
- package/src/routes/+layout.svelte +72 -0
- package/src/routes/+page.svelte +381 -0
- package/src/routes/player/+page.svelte +294 -0
- package/static/favicon.ico +0 -0
- package/static/favicon.png +0 -0
- package/static/icons/144.favicon.png +0 -0
- package/static/icons/168.favicon.png +0 -0
- package/static/icons/192.favicon.png +0 -0
- package/static/icons/48.favicon.png +0 -0
- package/static/icons/72.favicon.png +0 -0
- package/static/icons/96.favicon.png +0 -0
- package/static/manifest.json +40 -0
- package/svelte.config.js +19 -0
- package/tools/BuildManifest.js +87 -0
- package/vite.config.js +46 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { qr } from '$lib/actions/qr.svelte';
|
|
3
|
+
import { address, api, config } from '$lib/store';
|
|
4
|
+
import { fade } from 'svelte/transition';
|
|
5
|
+
import ImageLoad from '$lib/components/ImageLoad.svelte';
|
|
6
|
+
import PlayingTracker from '$lib/components/PlayingTracker.svelte';
|
|
7
|
+
import LoadingIndicator from '$lib/components/LoadingIndicator.svelte';
|
|
8
|
+
import * as DataTypes from '$server/types/data.js';
|
|
9
|
+
import { listenCb } from '$lib/utils';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @type {DataTypes.ApiInfoResponse | null | {noTrack: boolean}}
|
|
13
|
+
*/
|
|
14
|
+
let playing = $state(null);
|
|
15
|
+
|
|
16
|
+
let title = $derived.by(() => {
|
|
17
|
+
if (!playing || 'noTrack' in playing) {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if ('normalised' in playing.context) {
|
|
22
|
+
return playing.context.normalised.title;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if ('album' in playing.track) {
|
|
26
|
+
return playing.track.album;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if ('show' in playing.track) {
|
|
30
|
+
return playing.track.show;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return '';
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
let subtitle = $derived.by(() => {
|
|
37
|
+
if (!playing || 'noTrack' in playing) {
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!playing.isPlaying) {
|
|
42
|
+
return 'Paused';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
switch (playing.context?.type) {
|
|
46
|
+
case 'album':
|
|
47
|
+
return [playing.track.number, playing.context.total].join(' – ');
|
|
48
|
+
default:
|
|
49
|
+
return playing?.context?.normalised?.subtitle ?? '';
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/** @type {null | (() => void)} */
|
|
54
|
+
let focusEvt = null;
|
|
55
|
+
async function keepScreenAwake() {
|
|
56
|
+
let wakeLock = null;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
focusEvt?.();
|
|
60
|
+
focusEvt = null;
|
|
61
|
+
wakeLock = await navigator.wakeLock.request('screen');
|
|
62
|
+
console.log('Screen locked');
|
|
63
|
+
return () => {};
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err.code === 0 && !focusEvt) {
|
|
66
|
+
focusEvt = listenCb(window, 'focus', keepScreenAwake);
|
|
67
|
+
return focusEvt;
|
|
68
|
+
} else {
|
|
69
|
+
console.log(err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let idle = $state(true);
|
|
75
|
+
function mouseIdle() {
|
|
76
|
+
let timerId = 0;
|
|
77
|
+
const unlisten = listenCb(document, 'mousemove', () => {
|
|
78
|
+
clearTimeout(timerId);
|
|
79
|
+
idle = false;
|
|
80
|
+
|
|
81
|
+
timerId = window.setTimeout(() => {
|
|
82
|
+
idle = true;
|
|
83
|
+
}, 2000);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return () => {
|
|
87
|
+
unlisten();
|
|
88
|
+
clearTimeout(timerId);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
$effect(() => {
|
|
93
|
+
document.documentElement.classList.toggle('idle', idle);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
$effect(() => {
|
|
97
|
+
keepScreenAwake();
|
|
98
|
+
mouseIdle();
|
|
99
|
+
});
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<PlayingTracker bind:playing />
|
|
103
|
+
|
|
104
|
+
{#if !playing}
|
|
105
|
+
<LoadingIndicator floating />
|
|
106
|
+
{:else if 'noTrack' in playing}
|
|
107
|
+
<div class="empty bg-color-3 color-1">No Track playing currently</div>
|
|
108
|
+
{:else}
|
|
109
|
+
<div class="page bg-color-bg" class:idle>
|
|
110
|
+
<div class="top">
|
|
111
|
+
<div class="context color-1">
|
|
112
|
+
<span>{title}</span>
|
|
113
|
+
<span class="size-small-2">{subtitle}</span>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div class="middle">
|
|
118
|
+
<div class="middle-image">
|
|
119
|
+
{#key playing.track.normalised.image.full.url}
|
|
120
|
+
<span transition:fade>
|
|
121
|
+
<ImageLoad
|
|
122
|
+
full={playing.track.normalised.image.full.url}
|
|
123
|
+
low={playing.track.normalised.image.low.url}
|
|
124
|
+
/>
|
|
125
|
+
</span>
|
|
126
|
+
{/key}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div class="bottom">
|
|
131
|
+
<div class="left color-1">
|
|
132
|
+
<h1 class="title ellipsis">{playing.track.normalised.title}</h1>
|
|
133
|
+
<span class="headline-3">{playing.track.normalised.subtitle}</span>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div class="right">
|
|
137
|
+
{#if $config.api?.canAdd}
|
|
138
|
+
<div class="badge bg-color-3 color-bg">
|
|
139
|
+
<span class="headline-3">Add</span>
|
|
140
|
+
|
|
141
|
+
<span class="badge-qr" use:qr={{ url: $address.get('/') }}></span>
|
|
142
|
+
</div>
|
|
143
|
+
{/if}
|
|
144
|
+
|
|
145
|
+
<span class="size-small-1 color-3">{$address.naked}</span>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
{/if}
|
|
150
|
+
|
|
151
|
+
<style lang="scss">
|
|
152
|
+
.page {
|
|
153
|
+
display: grid;
|
|
154
|
+
|
|
155
|
+
grid-template-rows: auto 1fr auto;
|
|
156
|
+
gap: var(--spacing-large);
|
|
157
|
+
|
|
158
|
+
width: 100vw;
|
|
159
|
+
height: 100dvh;
|
|
160
|
+
|
|
161
|
+
transition: {
|
|
162
|
+
duration: 0.5s;
|
|
163
|
+
property: background-color;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.top {
|
|
168
|
+
display: flex;
|
|
169
|
+
|
|
170
|
+
justify-content: center;
|
|
171
|
+
align-items: center;
|
|
172
|
+
|
|
173
|
+
padding: var(--spacing-large);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.middle {
|
|
177
|
+
min-height: 0;
|
|
178
|
+
|
|
179
|
+
display: grid;
|
|
180
|
+
|
|
181
|
+
grid-template-columns: 1fr;
|
|
182
|
+
|
|
183
|
+
&-image {
|
|
184
|
+
display: grid;
|
|
185
|
+
|
|
186
|
+
grid-template-columns: 1fr;
|
|
187
|
+
grid-template-rows: 1fr;
|
|
188
|
+
|
|
189
|
+
min-height: 0;
|
|
190
|
+
padding: var(--spacing-x-large);
|
|
191
|
+
|
|
192
|
+
@media (orientation: landscape) {
|
|
193
|
+
padding: var(--spacing-large);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
span {
|
|
197
|
+
grid-column: 1;
|
|
198
|
+
grid-row: 1;
|
|
199
|
+
|
|
200
|
+
min-height: 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
:global(img) {
|
|
204
|
+
width: 100%;
|
|
205
|
+
height: 100%;
|
|
206
|
+
min-height: 0;
|
|
207
|
+
min-width: 0;
|
|
208
|
+
|
|
209
|
+
object-fit: contain;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.context {
|
|
215
|
+
display: flex;
|
|
216
|
+
|
|
217
|
+
flex-direction: column;
|
|
218
|
+
|
|
219
|
+
align-items: center;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.bottom {
|
|
223
|
+
display: grid;
|
|
224
|
+
|
|
225
|
+
grid-template-columns: 1fr auto;
|
|
226
|
+
gap: var(--spacing-large);
|
|
227
|
+
|
|
228
|
+
justify-content: space-between;
|
|
229
|
+
align-items: flex-start;
|
|
230
|
+
|
|
231
|
+
padding: var(--spacing-large);
|
|
232
|
+
|
|
233
|
+
.badge {
|
|
234
|
+
display: grid;
|
|
235
|
+
|
|
236
|
+
grid-template-columns: 1fr 50px;
|
|
237
|
+
gap: var(--spacing-small);
|
|
238
|
+
|
|
239
|
+
align-items: center;
|
|
240
|
+
|
|
241
|
+
padding: var(--spacing-small);
|
|
242
|
+
|
|
243
|
+
border-radius: var(--border-radius-normal);
|
|
244
|
+
|
|
245
|
+
min-width: 150px;
|
|
246
|
+
|
|
247
|
+
&-qr {
|
|
248
|
+
line-height: 0;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.left {
|
|
253
|
+
display: flex;
|
|
254
|
+
|
|
255
|
+
flex-direction: column;
|
|
256
|
+
align-items: flex-start;
|
|
257
|
+
|
|
258
|
+
gap: var(--spacing-small);
|
|
259
|
+
|
|
260
|
+
flex: 1;
|
|
261
|
+
min-width: 0;
|
|
262
|
+
|
|
263
|
+
.title {
|
|
264
|
+
width: 100%;
|
|
265
|
+
line-height: 1.2;
|
|
266
|
+
margin-bottom: -0.1em;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.right {
|
|
271
|
+
display: flex;
|
|
272
|
+
|
|
273
|
+
flex-direction: column;
|
|
274
|
+
align-items: flex-end;
|
|
275
|
+
align-self: flex-end;
|
|
276
|
+
|
|
277
|
+
gap: var(--spacing-small);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.empty {
|
|
282
|
+
position: absolute;
|
|
283
|
+
|
|
284
|
+
top: 50%;
|
|
285
|
+
left: 50%;
|
|
286
|
+
|
|
287
|
+
transform: translate3d(-50%, -50%, 0);
|
|
288
|
+
|
|
289
|
+
padding: var(--spacing-normal);
|
|
290
|
+
border-radius: var(--border-radius-normal);
|
|
291
|
+
|
|
292
|
+
white-space: nowrap;
|
|
293
|
+
}
|
|
294
|
+
</style>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Spotify Party",
|
|
3
|
+
"short_name": "Spotify Party",
|
|
4
|
+
"start_url": ".",
|
|
5
|
+
"display": "standalone",
|
|
6
|
+
"background_color": "#000",
|
|
7
|
+
"description": "A frontend for social spotify-ing",
|
|
8
|
+
"icons": [
|
|
9
|
+
{
|
|
10
|
+
"sizes": "48x48",
|
|
11
|
+
"type": "image/png",
|
|
12
|
+
"src": "/icons/48.favicon.png"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"sizes": "72x72",
|
|
16
|
+
"type": "image/png",
|
|
17
|
+
"src": "/icons/72.favicon.png"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"sizes": "96x96",
|
|
21
|
+
"type": "image/png",
|
|
22
|
+
"src": "/icons/96.favicon.png"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"sizes": "144x144",
|
|
26
|
+
"type": "image/png",
|
|
27
|
+
"src": "/icons/144.favicon.png"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"sizes": "168x168",
|
|
31
|
+
"type": "image/png",
|
|
32
|
+
"src": "/icons/168.favicon.png"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"sizes": "192x192",
|
|
36
|
+
"type": "image/png",
|
|
37
|
+
"src": "/icons/192.favicon.png"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
package/svelte.config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import adapter from '@sveltejs/adapter-node';
|
|
2
|
+
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|
3
|
+
|
|
4
|
+
/** @type {import('@sveltejs/kit').Config} */
|
|
5
|
+
const config = {
|
|
6
|
+
kit: {
|
|
7
|
+
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
|
8
|
+
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
|
9
|
+
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
|
10
|
+
adapter: adapter(),
|
|
11
|
+
alias: {
|
|
12
|
+
$server: './server',
|
|
13
|
+
$config: './ambient.config.js'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
preprocess: [vitePreprocess()]
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default config;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import sharp from 'sharp';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import ico from 'sharp-ico';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_MANIFEST = async () => {
|
|
8
|
+
const pkg = await import('../package.json');
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
name: pkg.name,
|
|
12
|
+
short_name: pkg.name,
|
|
13
|
+
start_url: '.',
|
|
14
|
+
display: 'standalone',
|
|
15
|
+
background_color: '#fff',
|
|
16
|
+
description: pkg.default?.description ?? ''
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const ICON_SIZES = [48, 72, 96, 144, 168, 192];
|
|
21
|
+
|
|
22
|
+
function generateIco(sharpNode, iconName = 'favicon.ico', sizes = [128, 64, 32, 24]) {
|
|
23
|
+
return ico.sharpsToIco([sharpNode], iconName, {
|
|
24
|
+
sizes
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function manifestPlugin({
|
|
29
|
+
icon = 'favicon.png',
|
|
30
|
+
outputDir = 'icons',
|
|
31
|
+
outputIco = 'favicon.ico',
|
|
32
|
+
faviconSizes = [128, 64, 32, 24],
|
|
33
|
+
manifestIconSizes = ICON_SIZES,
|
|
34
|
+
manifest = {}
|
|
35
|
+
} = {}) {
|
|
36
|
+
let viteConfig;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
name: 'build-manifest', // required, will show up in warnings and errors
|
|
40
|
+
|
|
41
|
+
configResolved(config) {
|
|
42
|
+
viteConfig = config;
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async buildStart() {
|
|
46
|
+
const defaultManifest = await DEFAULT_MANIFEST();
|
|
47
|
+
|
|
48
|
+
if (fs.existsSync(path.join(viteConfig.publicDir, icon))) {
|
|
49
|
+
fs.ensureDirSync(path.join(viteConfig.publicDir, outputDir));
|
|
50
|
+
|
|
51
|
+
const iconNode = sharp(path.join(viteConfig.publicDir, icon));
|
|
52
|
+
|
|
53
|
+
console.log(`Generating .ico file`);
|
|
54
|
+
await generateIco(iconNode, path.join(viteConfig.publicDir, outputIco), faviconSizes);
|
|
55
|
+
|
|
56
|
+
console.log(`Generating icons`);
|
|
57
|
+
await Promise.all(
|
|
58
|
+
manifestIconSizes.map((size) =>
|
|
59
|
+
iconNode
|
|
60
|
+
.toFormat('png')
|
|
61
|
+
.resize(size, size)
|
|
62
|
+
.toFile(
|
|
63
|
+
path.join(viteConfig.publicDir, outputDir, [size, path.basename(icon)].join('.'))
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const finalManifest = {
|
|
69
|
+
...defaultManifest,
|
|
70
|
+
...manifest,
|
|
71
|
+
icons: manifestIconSizes.map((size) => ({
|
|
72
|
+
sizes: `${size}x${size}`,
|
|
73
|
+
type: `image/png`,
|
|
74
|
+
src: `/${path.join(outputDir, [size, path.basename(icon)].join('.'))}`
|
|
75
|
+
}))
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
console.log(`Writing manifest file`);
|
|
79
|
+
fs.writeFileSync(
|
|
80
|
+
path.join(viteConfig.publicDir, 'manifest.json'),
|
|
81
|
+
JSON.stringify(finalManifest, null, 2),
|
|
82
|
+
'utf-8'
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
package/vite.config.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { sveltekit } from '@sveltejs/kit/vite';
|
|
2
|
+
import { defineConfig, searchForWorkspaceRoot } from 'vite';
|
|
3
|
+
import svg from '@poppanator/sveltekit-svg';
|
|
4
|
+
import BuildManifest from './tools/BuildManifest.js';
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
server: {
|
|
8
|
+
host: true,
|
|
9
|
+
fs: {
|
|
10
|
+
allow: [searchForWorkspaceRoot(process.cwd()), './server', './ambient.config.js']
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
plugins: [
|
|
14
|
+
sveltekit(),
|
|
15
|
+
svg({
|
|
16
|
+
includePaths: ['./src/lib/icons/'],
|
|
17
|
+
svgoOptions: {
|
|
18
|
+
multipass: true,
|
|
19
|
+
plugins: [
|
|
20
|
+
{
|
|
21
|
+
name: 'preset-default',
|
|
22
|
+
// by default svgo removes the viewBox which prevents svg icons from scaling
|
|
23
|
+
// not a good idea! https://github.com/svg/svgo/pull/1461
|
|
24
|
+
params: { overrides: { removeViewBox: false } }
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'convertColors',
|
|
28
|
+
params: {
|
|
29
|
+
currentColor: true
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{ name: 'removeAttrs', params: { attrs: '(width|height)' } }
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}),
|
|
36
|
+
|
|
37
|
+
BuildManifest({
|
|
38
|
+
manifest: {
|
|
39
|
+
name: 'Spotify Party',
|
|
40
|
+
short_name: 'Spotify Party',
|
|
41
|
+
description: 'A frontend for social spotify-ing',
|
|
42
|
+
background_color: '#000'
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
]
|
|
46
|
+
});
|