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