storyloco 1.0.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/LICENSE +21 -0
- package/README.md +210 -0
- package/package.json +77 -0
- package/packages/heading/README.md +68 -0
- package/packages/heading/src/app.svelte +64 -0
- package/packages/mux/README.md +68 -0
- package/packages/mux/src/app.svelte +576 -0
- package/packages/shared/README.md +58 -0
- package/packages/shared/dist/components/ui/input/index.d.ts +2 -0
- package/packages/shared/dist/components/ui/input/index.js +4 -0
- package/packages/shared/dist/components/ui/input/input.svelte +51 -0
- package/packages/shared/dist/components/ui/input/input.svelte.d.ts +13 -0
- package/packages/shared/dist/components/ui/label/index.d.ts +2 -0
- package/packages/shared/dist/components/ui/label/index.js +4 -0
- package/packages/shared/dist/components/ui/label/label.svelte +16 -0
- package/packages/shared/dist/components/ui/label/label.svelte.d.ts +4 -0
- package/packages/shared/dist/components/ui/skeleton/index.d.ts +2 -0
- package/packages/shared/dist/components/ui/skeleton/index.js +4 -0
- package/packages/shared/dist/components/ui/skeleton/skeleton.svelte +17 -0
- package/packages/shared/dist/components/ui/skeleton/skeleton.svelte.d.ts +5 -0
- package/packages/shared/dist/components/ui/switch/index.d.ts +2 -0
- package/packages/shared/dist/components/ui/switch/index.js +4 -0
- package/packages/shared/dist/components/ui/switch/switch.svelte +29 -0
- package/packages/shared/dist/components/ui/switch/switch.svelte.d.ts +4 -0
- package/packages/shared/dist/global.css +161 -0
- package/packages/shared/dist/index.d.ts +5 -0
- package/packages/shared/dist/index.js +7 -0
- package/packages/shared/dist/setup.d.ts +2 -0
- package/packages/shared/dist/setup.js +25 -0
- package/packages/shared/dist/utils.d.ts +12 -0
- package/packages/shared/dist/utils.js +5 -0
- package/packages/theme/README.md +68 -0
- package/packages/theme/src/app.svelte +114 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import { createFieldPlugin, type FieldPluginResponse } from '@storyblok/field-plugin'
|
|
3
|
+
import { onMount } from 'svelte'
|
|
4
|
+
import { flip } from 'svelte/animate'
|
|
5
|
+
import { fly, fade, slide } from 'svelte/transition'
|
|
6
|
+
import Mux from '@mux/mux-node'
|
|
7
|
+
import { format_date, format_elapse } from 'kitto'
|
|
8
|
+
import {
|
|
9
|
+
VideoIcon,
|
|
10
|
+
VideoOffIcon,
|
|
11
|
+
HourglassIcon,
|
|
12
|
+
Trash2Icon,
|
|
13
|
+
RefreshCwIcon,
|
|
14
|
+
CheckCircle2Icon,
|
|
15
|
+
EllipsisIcon,
|
|
16
|
+
Settings2Icon,
|
|
17
|
+
} from '@lucide/svelte'
|
|
18
|
+
import '@mux/mux-uploader'
|
|
19
|
+
import { cn } from 'shared/utils'
|
|
20
|
+
import { Input, Label, Skeleton, Switch } from 'shared'
|
|
21
|
+
|
|
22
|
+
export interface Video {
|
|
23
|
+
mux_video?: Mux.Video.Assets.Asset
|
|
24
|
+
playback_id?: string
|
|
25
|
+
m3u8_url?: string
|
|
26
|
+
title?: string
|
|
27
|
+
autoplay?: boolean
|
|
28
|
+
muted?: boolean
|
|
29
|
+
loop?: boolean
|
|
30
|
+
playsinline?: boolean
|
|
31
|
+
controls?: boolean
|
|
32
|
+
preload?: 'auto' | 'metadata' | 'none'
|
|
33
|
+
poster?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type MuxAsset = Mux.Video.Assets.Asset
|
|
37
|
+
type Plugin = FieldPluginResponse<Video | null>
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<script lang="ts">
|
|
41
|
+
let content: Video | null = $state(null)
|
|
42
|
+
let plugin: Plugin | null = $state(null)
|
|
43
|
+
let assets: Array<MuxAsset> | null = $state(null)
|
|
44
|
+
let open_actions: string | null = $state(null)
|
|
45
|
+
let timeout: NodeJS.Timeout | null = $state(null)
|
|
46
|
+
let video_options_open = $state(false)
|
|
47
|
+
|
|
48
|
+
const is_mux_poster = $derived.by(() => content?.poster?.startsWith('https://image.mux.com/'))
|
|
49
|
+
const poster = $derived.by(() => {
|
|
50
|
+
if (is_mux_poster) return `${content?.poster}?width=558&height=314&fit_mode=smartcrop`
|
|
51
|
+
if (content?.poster?.endsWith('.svg')) return content.poster
|
|
52
|
+
return `${content?.poster}/m/558x314/smart`
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
onMount(() => {
|
|
56
|
+
createFieldPlugin({
|
|
57
|
+
enablePortalModal: true,
|
|
58
|
+
validateContent: (c) => {
|
|
59
|
+
if (typeof c !== 'object' || c === null) {
|
|
60
|
+
return { content: null }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Update local state
|
|
64
|
+
content = c as Video
|
|
65
|
+
|
|
66
|
+
return { content: c }
|
|
67
|
+
},
|
|
68
|
+
onUpdateState: (state) => {
|
|
69
|
+
plugin = state as Plugin
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const mux = $derived.by(() => {
|
|
75
|
+
if (plugin?.type !== 'loaded' || !plugin.data.options.MOXY_SECRET_ID) return null
|
|
76
|
+
|
|
77
|
+
return new Mux({
|
|
78
|
+
baseURL: 'https://moxy.admin-54b.workers.dev/api/',
|
|
79
|
+
tokenId: '',
|
|
80
|
+
tokenSecret: '',
|
|
81
|
+
defaultHeaders: {
|
|
82
|
+
authorization: `Bearer ${plugin.data.options.MOXY_SECRET_ID}`,
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
function get_poster(video: MuxAsset) {
|
|
88
|
+
const playback_id = video.playback_ids?.[0]?.id
|
|
89
|
+
return playback_id ? `https://image.mux.com/${playback_id}/thumbnail.jpg` : undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const actions = {
|
|
93
|
+
update(c?: Partial<Video>) {
|
|
94
|
+
if (plugin?.type !== 'loaded') return
|
|
95
|
+
const state = $state.snapshot({ ...content, ...c })
|
|
96
|
+
plugin.actions.setContent(state)
|
|
97
|
+
},
|
|
98
|
+
set_video(video: MuxAsset | null) {
|
|
99
|
+
if (!video) {
|
|
100
|
+
content = null
|
|
101
|
+
actions.update()
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const playback_id = video.playback_ids?.[0]?.id
|
|
106
|
+
|
|
107
|
+
content = {
|
|
108
|
+
...content,
|
|
109
|
+
playback_id,
|
|
110
|
+
title: video.meta?.title,
|
|
111
|
+
m3u8_url: playback_id ? `https://stream.mux.com/${playback_id}.m3u8` : undefined,
|
|
112
|
+
poster: get_poster(video),
|
|
113
|
+
mux_video: video,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
actions.update()
|
|
117
|
+
},
|
|
118
|
+
async list() {
|
|
119
|
+
if (!mux) throw new Error('Mux not initialised')
|
|
120
|
+
|
|
121
|
+
assets = (await mux.video.assets.list()).data
|
|
122
|
+
},
|
|
123
|
+
async delete(id: string) {
|
|
124
|
+
if (plugin?.type !== 'loaded' || !plugin.data.options.MOXY_SECRET_ID || !mux)
|
|
125
|
+
throw new Error('Mux not initialised')
|
|
126
|
+
|
|
127
|
+
const confirm = window.confirm('Are you sure you want to delete this video?')
|
|
128
|
+
|
|
129
|
+
if (!confirm) return
|
|
130
|
+
|
|
131
|
+
// Update local state, for smooth transition
|
|
132
|
+
if (assets?.length) assets = assets.filter((asset) => asset.id !== id)
|
|
133
|
+
|
|
134
|
+
// Delete the video
|
|
135
|
+
await mux.video.assets.delete(id)
|
|
136
|
+
|
|
137
|
+
// Update content if the deleted video was the current one
|
|
138
|
+
if (content?.mux_video?.id === id) actions.set_video(null)
|
|
139
|
+
|
|
140
|
+
// Refresh the list
|
|
141
|
+
await actions.list()
|
|
142
|
+
},
|
|
143
|
+
async get_upload_endpoint() {
|
|
144
|
+
if (!mux) throw new Error('Mux not initialised')
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
await mux.video.uploads.create({
|
|
148
|
+
cors_origin: 'https://storyblok.com',
|
|
149
|
+
new_asset_settings: {
|
|
150
|
+
playback_policy: ['public'],
|
|
151
|
+
encoding_tier: 'baseline',
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
).url
|
|
155
|
+
},
|
|
156
|
+
toggle_actions(id: string) {
|
|
157
|
+
open_actions = open_actions === id ? null : id
|
|
158
|
+
},
|
|
159
|
+
async set_title(title: string, id: string) {
|
|
160
|
+
if (timeout) clearTimeout(timeout)
|
|
161
|
+
|
|
162
|
+
actions.update({ title })
|
|
163
|
+
|
|
164
|
+
timeout = setTimeout(async () => {
|
|
165
|
+
if (!mux) return
|
|
166
|
+
await mux.video.assets.update(id, {
|
|
167
|
+
meta: {
|
|
168
|
+
title,
|
|
169
|
+
},
|
|
170
|
+
})
|
|
171
|
+
}, 1000)
|
|
172
|
+
},
|
|
173
|
+
async select_poster() {
|
|
174
|
+
if (!content?.mux_video) return
|
|
175
|
+
const asset = await plugin?.actions?.selectAsset()
|
|
176
|
+
|
|
177
|
+
if (!asset) actions.update({ poster: get_poster(content.mux_video) })
|
|
178
|
+
else actions.update({ poster: asset.filename })
|
|
179
|
+
},
|
|
180
|
+
async delete_poster() {
|
|
181
|
+
if (!content?.mux_video) return
|
|
182
|
+
actions.update({ poster: get_poster(content.mux_video) })
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function format_duration(seconds: number): string {
|
|
187
|
+
const hours = Math.floor(seconds / 3600)
|
|
188
|
+
const minutes = Math.floor((seconds % 3600) / 60)
|
|
189
|
+
const remaining_seconds = Math.floor(seconds % 60)
|
|
190
|
+
|
|
191
|
+
if (hours > 0) {
|
|
192
|
+
return `${hours}:${minutes.toString().padStart(2, '0')}:${remaining_seconds.toString().padStart(2, '0')}`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return `${minutes}:${remaining_seconds.toString().padStart(2, '0')}`
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function date(date: string): string {
|
|
199
|
+
const d = new Date(Number(date) * 1000)
|
|
200
|
+
return format_elapse(d) || format_date('{MMM} {d}, {YYYY}', d)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const is_modal_open = $derived.by(() => plugin?.type === 'loaded' && plugin.data.isModalOpen)
|
|
204
|
+
|
|
205
|
+
$effect(() => {
|
|
206
|
+
document.documentElement.setAttribute('data-modal-open', is_modal_open ? 'true' : 'false')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const actions_menu_classes = `
|
|
210
|
+
absolute
|
|
211
|
+
right-4
|
|
212
|
+
top-4
|
|
213
|
+
flex
|
|
214
|
+
items-center
|
|
215
|
+
bg-card
|
|
216
|
+
text-card-foreground
|
|
217
|
+
border
|
|
218
|
+
rounded
|
|
219
|
+
divide-x
|
|
220
|
+
opacity-0
|
|
221
|
+
pointer-events-none
|
|
222
|
+
-translate-y-10
|
|
223
|
+
transition-[translate,opacity]
|
|
224
|
+
overflow-hidden
|
|
225
|
+
|
|
226
|
+
[&>li]:flex
|
|
227
|
+
[&_button]:p-2
|
|
228
|
+
[&_button]:hover:bg-muted
|
|
229
|
+
[&_button]:outline-none
|
|
230
|
+
[&_button]:focus:bg-muted
|
|
231
|
+
|
|
232
|
+
group-hover:opacity-100
|
|
233
|
+
group-hover:pointer-events-auto
|
|
234
|
+
group-hover:translate-y-0
|
|
235
|
+
|
|
236
|
+
group-focus-within:opacity-100
|
|
237
|
+
group-focus-within:pointer-events-auto
|
|
238
|
+
group-focus-within:translate-y-0
|
|
239
|
+
`
|
|
240
|
+
</script>
|
|
241
|
+
|
|
242
|
+
<svelte:window
|
|
243
|
+
onclick={(event) => {
|
|
244
|
+
if (!(event.target instanceof HTMLElement)) return
|
|
245
|
+
|
|
246
|
+
// reset target when we click outside of actions
|
|
247
|
+
if (event.target.closest('.actions')) return
|
|
248
|
+
|
|
249
|
+
open_actions = null
|
|
250
|
+
}}
|
|
251
|
+
/>
|
|
252
|
+
|
|
253
|
+
{#snippet skeleton()}
|
|
254
|
+
<ol class="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-2">
|
|
255
|
+
{#each Array(12)}
|
|
256
|
+
<li>
|
|
257
|
+
<div class="p-3 grid gap-2">
|
|
258
|
+
<Skeleton class="aspect-video size-full" />
|
|
259
|
+
<div class="flex gap-2 justify-between">
|
|
260
|
+
<Skeleton class="w-24 h-4" />
|
|
261
|
+
<Skeleton class="w-16 h-4" />
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</li>
|
|
265
|
+
{/each}
|
|
266
|
+
</ol>
|
|
267
|
+
{/snippet}
|
|
268
|
+
|
|
269
|
+
{#snippet asset_preview(video?: MuxAsset)}
|
|
270
|
+
{@const playback_id = video?.playback_ids?.[0]?.id}
|
|
271
|
+
{@const is_selected = content?.mux_video?.id === video?.id}
|
|
272
|
+
|
|
273
|
+
<figure
|
|
274
|
+
class={cn([
|
|
275
|
+
'flex items-center justify-center [&>svg]:absolute [&>svg]:size-5 bg-muted text-muted-foreground rounded w-full aspect-video relative border',
|
|
276
|
+
{ 'bg-primary [&>svg]:text-primary-foreground': video && is_selected },
|
|
277
|
+
])}
|
|
278
|
+
>
|
|
279
|
+
{#if playback_id && video.status === 'ready'}
|
|
280
|
+
<img
|
|
281
|
+
class={['rounded shrink-0 size-full absolute cover', { 'opacity-50': is_selected }]}
|
|
282
|
+
src="https://image.mux.com/{playback_id}/thumbnail.jpg?width=240&height=135&fit_mode=smartcrop"
|
|
283
|
+
alt={video.meta?.title}
|
|
284
|
+
/>
|
|
285
|
+
<img
|
|
286
|
+
class="rounded shrink-0 size-full absolute cover hover:opacity-100 transition-opacity opacity-0"
|
|
287
|
+
src="https://image.mux.com/{playback_id}/animated.gif?width=240&height=135&fit_mode=smartcrop"
|
|
288
|
+
alt={video.meta?.title}
|
|
289
|
+
/>
|
|
290
|
+
{/if}
|
|
291
|
+
{#if !video}
|
|
292
|
+
<VideoIcon />
|
|
293
|
+
{:else if video.status === 'errored'}
|
|
294
|
+
<VideoOffIcon />
|
|
295
|
+
{:else if video.status === 'preparing'}
|
|
296
|
+
<HourglassIcon />
|
|
297
|
+
{:else if is_selected}
|
|
298
|
+
<CheckCircle2Icon />
|
|
299
|
+
{/if}
|
|
300
|
+
</figure>
|
|
301
|
+
{/snippet}
|
|
302
|
+
|
|
303
|
+
{#snippet asset_meta()}
|
|
304
|
+
<p class="font-medium truncate">
|
|
305
|
+
{content?.title}
|
|
306
|
+
</p>
|
|
307
|
+
<span class="justify-between flex text-muted-foreground text-xs">
|
|
308
|
+
{#if content?.mux_video?.status === 'errored'}
|
|
309
|
+
{content.mux_video.errors?.messages}
|
|
310
|
+
{:else if content?.mux_video?.status === 'preparing'}
|
|
311
|
+
Preparing...
|
|
312
|
+
{:else if content?.mux_video?.duration}
|
|
313
|
+
<span>
|
|
314
|
+
{format_duration(content.mux_video.duration)}
|
|
315
|
+
</span>
|
|
316
|
+
<time datetime={content.mux_video.created_at}>
|
|
317
|
+
{date(content.mux_video.created_at)}
|
|
318
|
+
</time>
|
|
319
|
+
{/if}
|
|
320
|
+
</span>
|
|
321
|
+
{/snippet}
|
|
322
|
+
|
|
323
|
+
{#if plugin?.type === 'loaded' && mux}
|
|
324
|
+
{#if plugin.data.isModalOpen}
|
|
325
|
+
<div class="p-8 grid gap-8">
|
|
326
|
+
<mux-uploader onsuccess={actions.list} endpoint={actions.get_upload_endpoint}></mux-uploader>
|
|
327
|
+
{#await actions.list()}
|
|
328
|
+
{@render skeleton()}
|
|
329
|
+
{:then}
|
|
330
|
+
{#if !assets || assets.length === 0}
|
|
331
|
+
<p class="text-muted-foreground">No videos found</p>
|
|
332
|
+
{:else}
|
|
333
|
+
<ol class="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-2">
|
|
334
|
+
{#each assets as video (video.id)}
|
|
335
|
+
{@const actions_open = open_actions === video.id}
|
|
336
|
+
<li
|
|
337
|
+
class="relative group size-full"
|
|
338
|
+
class:active={actions_open}
|
|
339
|
+
animate:flip={{ duration: 300 }}
|
|
340
|
+
in:fly={{ y: 20, duration: 300, delay: 100 }}
|
|
341
|
+
out:fade={{ duration: 200 }}
|
|
342
|
+
>
|
|
343
|
+
{#if video.id}
|
|
344
|
+
<button
|
|
345
|
+
class="
|
|
346
|
+
absolute
|
|
347
|
+
top-6
|
|
348
|
+
right-6
|
|
349
|
+
bg-muted
|
|
350
|
+
text-muted-foreground
|
|
351
|
+
rounded
|
|
352
|
+
p-1
|
|
353
|
+
z-10
|
|
354
|
+
|
|
355
|
+
opacity-0
|
|
356
|
+
pointer-events-none
|
|
357
|
+
-translate-y-10
|
|
358
|
+
transition-[translate,opacity]
|
|
359
|
+
|
|
360
|
+
group-hover:opacity-100
|
|
361
|
+
group-hover:pointer-events-auto
|
|
362
|
+
group-hover:translate-y-0
|
|
363
|
+
|
|
364
|
+
group-focus-within:opacity-100
|
|
365
|
+
group-focus-within:pointer-events-auto
|
|
366
|
+
group-focus-within:translate-y-0
|
|
367
|
+
"
|
|
368
|
+
onclick={() => actions.toggle_actions(video.id)}
|
|
369
|
+
>
|
|
370
|
+
<EllipsisIcon size={18} />
|
|
371
|
+
</button>
|
|
372
|
+
{#if actions_open}
|
|
373
|
+
<ol
|
|
374
|
+
class="absolute top-14 w-full bg-card text-card-foreground rounded shadow-md z-10 border"
|
|
375
|
+
transition:fly={{ y: 20, duration: 300, delay: 100 }}
|
|
376
|
+
>
|
|
377
|
+
<li>
|
|
378
|
+
<button
|
|
379
|
+
class="p-3 w-full text-start hover:bg-muted transition-colors"
|
|
380
|
+
onclick={() => {
|
|
381
|
+
plugin?.actions?.setModalOpen(false)
|
|
382
|
+
actions.set_video(video)
|
|
383
|
+
}}
|
|
384
|
+
>
|
|
385
|
+
Select
|
|
386
|
+
</button>
|
|
387
|
+
</li>
|
|
388
|
+
<li>
|
|
389
|
+
<button
|
|
390
|
+
class="p-3 w-full text-start hover:bg-muted transition-colors"
|
|
391
|
+
onclick={() => actions.delete(video.id)}
|
|
392
|
+
>
|
|
393
|
+
Delete
|
|
394
|
+
</button>
|
|
395
|
+
</li>
|
|
396
|
+
</ol>
|
|
397
|
+
{/if}
|
|
398
|
+
{/if}
|
|
399
|
+
<div class="grid gap-1 rounded hover:bg-muted transition-colors p-3 w-full">
|
|
400
|
+
<button
|
|
401
|
+
onclick={() => {
|
|
402
|
+
actions.set_video(video)
|
|
403
|
+
plugin?.actions?.setModalOpen(false)
|
|
404
|
+
}}
|
|
405
|
+
>
|
|
406
|
+
{@render asset_preview(video)}
|
|
407
|
+
</button>
|
|
408
|
+
{@render asset_meta()}
|
|
409
|
+
</div>
|
|
410
|
+
</li>
|
|
411
|
+
{/each}
|
|
412
|
+
</ol>
|
|
413
|
+
{/if}
|
|
414
|
+
{:catch}
|
|
415
|
+
An error occurred while loading videos.
|
|
416
|
+
{/await}
|
|
417
|
+
</div>
|
|
418
|
+
{:else if content?.mux_video}
|
|
419
|
+
<div
|
|
420
|
+
class="p-4 grid grid-cols-[140px_1fr] w-full border border rounded hover:border-primary transition-colors bg-card text-card-foreground items-center gap-x-5 group"
|
|
421
|
+
>
|
|
422
|
+
{@render asset_preview(content.mux_video)}
|
|
423
|
+
<div class="grid">{@render asset_meta()}</div>
|
|
424
|
+
<ul class={actions_menu_classes}>
|
|
425
|
+
<li>
|
|
426
|
+
<button
|
|
427
|
+
onclick={() => plugin?.actions?.setModalOpen(true)}
|
|
428
|
+
title="Replace video"
|
|
429
|
+
aria-label="Replace video"
|
|
430
|
+
>
|
|
431
|
+
<RefreshCwIcon size={18} />
|
|
432
|
+
</button>
|
|
433
|
+
</li>
|
|
434
|
+
<li>
|
|
435
|
+
<button
|
|
436
|
+
title="Settings"
|
|
437
|
+
aria-label="Settings"
|
|
438
|
+
onclick={() => (video_options_open = !video_options_open)}
|
|
439
|
+
>
|
|
440
|
+
<Settings2Icon size={18} />
|
|
441
|
+
</button>
|
|
442
|
+
</li>
|
|
443
|
+
<li>
|
|
444
|
+
<button
|
|
445
|
+
onclick={() => actions.set_video(null)}
|
|
446
|
+
title="Remove video"
|
|
447
|
+
aria-label="Remove video"
|
|
448
|
+
>
|
|
449
|
+
<Trash2Icon size={18} />
|
|
450
|
+
</button>
|
|
451
|
+
</li>
|
|
452
|
+
</ul>
|
|
453
|
+
{#if video_options_open}
|
|
454
|
+
<div class="grid gap-5 mt-5 col-span-full" transition:slide={{ duration: 300 }}>
|
|
455
|
+
<div class="grid gap-2">
|
|
456
|
+
<Label for="title">Title</Label>
|
|
457
|
+
<Input
|
|
458
|
+
id="title"
|
|
459
|
+
placeholder="Video title…"
|
|
460
|
+
value={content.mux_video.meta?.title}
|
|
461
|
+
oninput={(event) => {
|
|
462
|
+
if (!event.target || !(event.target instanceof HTMLInputElement)) return
|
|
463
|
+
if (!content?.mux_video) return
|
|
464
|
+
|
|
465
|
+
actions.set_title(event.target.value, content.mux_video.id)
|
|
466
|
+
}}
|
|
467
|
+
/>
|
|
468
|
+
</div>
|
|
469
|
+
<div class="flex items-center space-x-2">
|
|
470
|
+
<Switch
|
|
471
|
+
id="autoplay"
|
|
472
|
+
checked={content.autoplay}
|
|
473
|
+
onCheckedChange={(autoplay) => actions.update({ autoplay })}
|
|
474
|
+
/>
|
|
475
|
+
<Label for="autoplay">Autoplay</Label>
|
|
476
|
+
</div>
|
|
477
|
+
<div class="flex items-center space-x-2">
|
|
478
|
+
<Switch
|
|
479
|
+
id="playsinline"
|
|
480
|
+
checked={content.playsinline}
|
|
481
|
+
onCheckedChange={(playsinline) => actions.update({ playsinline })}
|
|
482
|
+
/>
|
|
483
|
+
<Label for="playsinline">Playsinline</Label>
|
|
484
|
+
</div>
|
|
485
|
+
<div class="flex items-center space-x-2">
|
|
486
|
+
<Switch
|
|
487
|
+
id="muted"
|
|
488
|
+
checked={content.muted}
|
|
489
|
+
onCheckedChange={(muted) => actions.update({ muted })}
|
|
490
|
+
/>
|
|
491
|
+
<Label for="muted">Muted</Label>
|
|
492
|
+
</div>
|
|
493
|
+
<div class="flex items-center space-x-2">
|
|
494
|
+
<Switch
|
|
495
|
+
id="loop"
|
|
496
|
+
checked={content.loop}
|
|
497
|
+
onCheckedChange={(loop) => actions.update({ loop })}
|
|
498
|
+
/>
|
|
499
|
+
<Label for="loop">Loop</Label>
|
|
500
|
+
</div>
|
|
501
|
+
<div class="grid gap-2">
|
|
502
|
+
<Label for="preload">Preload</Label>
|
|
503
|
+
<select
|
|
504
|
+
class="bg-background border rounded min-h-10 outline-none focus-visible:border-ring transition-colors w-full"
|
|
505
|
+
bind:value={content.preload}
|
|
506
|
+
onchange={(event) => {
|
|
507
|
+
if (!event.target || !(event.target instanceof HTMLSelectElement)) return
|
|
508
|
+
actions.update({ preload: event.target.value as 'auto' | 'metadata' | 'none' })
|
|
509
|
+
}}
|
|
510
|
+
id="preload"
|
|
511
|
+
>
|
|
512
|
+
<option disabled selected value={undefined}>Select preload…</option>
|
|
513
|
+
<option value="auto">Auto</option>
|
|
514
|
+
<option value="metadata">Metadata (default)</option>
|
|
515
|
+
<option value="none">None</option>
|
|
516
|
+
</select>
|
|
517
|
+
<p class="text-muted-foreground text-xs">
|
|
518
|
+
Note: The preload attribute is ignored if autoplay is present.
|
|
519
|
+
</p>
|
|
520
|
+
</div>
|
|
521
|
+
<div class="grid gap-2">
|
|
522
|
+
<Label for="poster">Poster</Label>
|
|
523
|
+
<figure class="relative rounded overflow-hidden border">
|
|
524
|
+
<ul class={actions_menu_classes}>
|
|
525
|
+
<li>
|
|
526
|
+
<button
|
|
527
|
+
onclick={actions.select_poster}
|
|
528
|
+
title="Select poster"
|
|
529
|
+
aria-label="Select poster"
|
|
530
|
+
>
|
|
531
|
+
<RefreshCwIcon size={18} />
|
|
532
|
+
</button>
|
|
533
|
+
</li>
|
|
534
|
+
{#if !is_mux_poster}
|
|
535
|
+
<li>
|
|
536
|
+
<button
|
|
537
|
+
onclick={actions.delete_poster}
|
|
538
|
+
title="Delete poster"
|
|
539
|
+
aria-label="Delete poster"
|
|
540
|
+
>
|
|
541
|
+
<Trash2Icon size={18} />
|
|
542
|
+
</button>
|
|
543
|
+
</li>
|
|
544
|
+
{/if}
|
|
545
|
+
</ul>
|
|
546
|
+
<img
|
|
547
|
+
class="shrink-0 aspect-video object-contain"
|
|
548
|
+
src={poster}
|
|
549
|
+
width={558}
|
|
550
|
+
height={314}
|
|
551
|
+
alt={content.mux_video.meta?.title}
|
|
552
|
+
/>
|
|
553
|
+
{#if is_mux_poster}
|
|
554
|
+
<figcaption
|
|
555
|
+
class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black to-transparent p-4 h-20 flex items-end text-muted-foreground text-xs"
|
|
556
|
+
>
|
|
557
|
+
Auto-generated poster from video source
|
|
558
|
+
</figcaption>
|
|
559
|
+
{/if}
|
|
560
|
+
</figure>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
{/if}
|
|
564
|
+
</div>
|
|
565
|
+
{:else}
|
|
566
|
+
<button
|
|
567
|
+
class="p-4 grid grid-cols-[140px_1fr] w-full border border rounded hover:border-primary transition-colors bg-card text-card-foreground items-center justify-items-start gap-5 font-medium"
|
|
568
|
+
onclick={() => plugin?.actions?.setModalOpen(true)}
|
|
569
|
+
title="Add video"
|
|
570
|
+
aria-label="Add video"
|
|
571
|
+
>
|
|
572
|
+
{@render asset_preview()}
|
|
573
|
+
+ Add Video
|
|
574
|
+
</button>
|
|
575
|
+
{/if}
|
|
576
|
+
{/if}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Svelte library
|
|
2
|
+
|
|
3
|
+
Everything you need to build a Svelte library, powered by [`sv`](https://npmjs.com/package/sv).
|
|
4
|
+
|
|
5
|
+
Read more about creating a library [in the docs](https://svelte.dev/docs/kit/packaging).
|
|
6
|
+
|
|
7
|
+
## Creating a project
|
|
8
|
+
|
|
9
|
+
If you're seeing this, you've probably already done this step. Congrats!
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# create a new project in the current directory
|
|
13
|
+
npx sv create
|
|
14
|
+
|
|
15
|
+
# create a new project in my-app
|
|
16
|
+
npx sv create my-app
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Developing
|
|
20
|
+
|
|
21
|
+
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm run dev
|
|
25
|
+
|
|
26
|
+
# or start the server and open the app in a new browser tab
|
|
27
|
+
npm run dev -- --open
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Everything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.
|
|
31
|
+
|
|
32
|
+
## Building
|
|
33
|
+
|
|
34
|
+
To build your library:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm run package
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
To create a production version of your showcase app:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm run build
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can preview the production build with `npm run preview`.
|
|
47
|
+
|
|
48
|
+
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
|
49
|
+
|
|
50
|
+
## Publishing
|
|
51
|
+
|
|
52
|
+
Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).
|
|
53
|
+
|
|
54
|
+
To publish your library to [npm](https://www.npmjs.com):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm publish
|
|
58
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements'
|
|
3
|
+
import { cn, type WithElementRef } from '../../../utils.js'
|
|
4
|
+
|
|
5
|
+
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>
|
|
6
|
+
|
|
7
|
+
type Props = WithElementRef<
|
|
8
|
+
Omit<HTMLInputAttributes, 'type'> &
|
|
9
|
+
({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined })
|
|
10
|
+
>
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
ref = $bindable(null),
|
|
14
|
+
value = $bindable(),
|
|
15
|
+
type,
|
|
16
|
+
files = $bindable(),
|
|
17
|
+
class: classname,
|
|
18
|
+
...rest
|
|
19
|
+
}: Props = $props()
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
{#if type === 'file'}
|
|
23
|
+
<input
|
|
24
|
+
bind:this={ref}
|
|
25
|
+
data-slot="input"
|
|
26
|
+
class={cn(
|
|
27
|
+
'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
|
28
|
+
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
|
29
|
+
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
|
30
|
+
classname
|
|
31
|
+
)}
|
|
32
|
+
type="file"
|
|
33
|
+
bind:files
|
|
34
|
+
bind:value
|
|
35
|
+
{...rest}
|
|
36
|
+
/>
|
|
37
|
+
{:else}
|
|
38
|
+
<input
|
|
39
|
+
bind:this={ref}
|
|
40
|
+
data-slot="input"
|
|
41
|
+
class={cn(
|
|
42
|
+
'border-input bg-white selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
|
43
|
+
'focus-visible:border-ring',
|
|
44
|
+
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
|
45
|
+
classname
|
|
46
|
+
)}
|
|
47
|
+
{type}
|
|
48
|
+
bind:value
|
|
49
|
+
{...rest}
|
|
50
|
+
/>
|
|
51
|
+
{/if}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
|
|
2
|
+
import { type WithElementRef } from '../../../utils.js';
|
|
3
|
+
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
|
|
4
|
+
type Props = WithElementRef<Omit<HTMLInputAttributes, 'type'> & ({
|
|
5
|
+
type: 'file';
|
|
6
|
+
files?: FileList;
|
|
7
|
+
} | {
|
|
8
|
+
type?: InputType;
|
|
9
|
+
files?: undefined;
|
|
10
|
+
})>;
|
|
11
|
+
declare const Input: import("svelte").Component<Props, {}, "files" | "value" | "ref">;
|
|
12
|
+
type Input = ReturnType<typeof Input>;
|
|
13
|
+
export default Input;
|