Skip to content

Commit e8bb684

Browse files
committed
Allow to show video / audio preview on upload
1 parent b3c4232 commit e8bb684

9 files changed

Lines changed: 175 additions & 99 deletions

File tree

demo/app/Sharp/Posts/PostForm.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@ public function buildFormFields(FieldsContainer $formFields): void
126126
)
127127
->addItemField(
128128
SharpFormUploadField::make('document')
129-
->setMaxFileSize(1)
130-
->setAllowedExtensions(['pdf', 'zip'])
129+
->setMaxFileSize(2)
130+
->setAllowedExtensions(['pdf', 'zip', 'mp4', 'mp3'])
131131
->setStorageDisk('local')
132132
->setStorageBasePath('data/posts/{id}')
133133
->addConditionalDisplay('!is_link'),
@@ -203,7 +203,7 @@ public function find($id): array
203203
return $this
204204
->setCustomTransformer('author_id', fn ($value, Post $instance) => $instance->author)
205205
->setCustomTransformer('cover', new SharpUploadModelFormAttributeTransformer())
206-
->setCustomTransformer('attachments[document]', new SharpUploadModelFormAttributeTransformer())
206+
->setCustomTransformer('attachments[document]', new SharpUploadModelFormAttributeTransformer(withPlayablePreview: true))
207207
->transform(Post::with('cover', 'attachments', 'categories')->findOrFail($id));
208208
}
209209

demo/app/Sharp/Posts/PostShow.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public function find(mixed $id): array
154154
->setCustomTransformer('cover', new SharpUploadModelThumbnailUrlTransformer(500))
155155
->setCustomTransformer(
156156
'attachments[document]',
157-
new SharpUploadModelFormAttributeTransformer(withThumbnails: true)
157+
new SharpUploadModelFormAttributeTransformer(withPlayablePreview: true)
158158
)
159159
->setCustomTransformer('attachments[link_url]', fn ($value, $instance) => $instance->is_link
160160
? sprintf('<a href="%s" alt="">%s</a>', $value, str($value)->limit(30))

resources/js/form/components/fields/upload/Upload.vue

Lines changed: 72 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
}>();
6969
const form = useParentForm();
7070
const transformedImg = ref<string>();
71+
const playablePreviewUrl = ref<string>();
7172
const uppyFile = ref<UppyFile<{}, {}>>();
7273
const isEditable = computed(() => {
7374
return props.value && canTransform(props.value.name, props.value.mime_type) && !props.hasError
@@ -135,6 +136,8 @@
135136
const blob = await response.blob();
136137
transformedImg.value = URL.createObjectURL(blob);
137138
}
139+
} else if(file.type?.startsWith('video/') || file.type?.startsWith('audio/')) {
140+
playablePreviewUrl.value = URL.createObjectURL(file.data);
138141
}
139142
})
140143
.on('restriction-failed', (file, error) => {
@@ -350,6 +353,7 @@
350353
uppy.removeFile(uppyFile.value.id);
351354
uppyFile.value = null;
352355
transformedImg.value = null;
356+
playablePreviewUrl.value = null;
353357
editModalImageUrl.value = null;
354358
}
355359
}
@@ -382,6 +386,9 @@
382386
if(!props.persistThumbnailUrl && transformedImg.value) {
383387
URL.revokeObjectURL(transformedImg.value);
384388
}
389+
if(playablePreviewUrl.value) {
390+
URL.revokeObjectURL(playablePreviewUrl.value);
391+
}
385392
emit('uploading', false);
386393
});
387394
</script>
@@ -392,69 +399,83 @@
392399
<template v-if="value?.path || value?.uploaded || uppyFile">
393400
<div :class="{ 'bg-background border rounded-md p-4': !asEditorEmbed }">
394401
<div class="flex gap-4">
395-
<template v-if="transformedImg ?? value?.thumbnail ?? uppyFile?.preview">
396-
<div class="self-center group/img relative rounded-md overflow-hidden">
397-
<img class="rounded-md min-w-[50px] max-h-[150px] max-w-[150px] object-contain"
398-
:class="uppyFile && !transformedImg && field.imageCropRatio ? 'object-cover aspect-(--ratio)' : ''"
399-
:style="{
402+
<div class="flex-1 min-w-0 flex gap-4" :class="playablePreviewUrl ?? value?.playable_preview_url ? 'flex-col' : ''">
403+
<template v-if="transformedImg ?? value?.thumbnail ?? uppyFile?.preview">
404+
<div class="self-center group/img relative rounded-md overflow-hidden">
405+
<img class="rounded-md min-w-[50px] max-h-[150px] max-w-[150px] object-contain"
406+
:class="uppyFile && !transformedImg && field.imageCropRatio ? 'object-cover aspect-(--ratio)' : ''"
407+
:style="{
400408
'--ratio': field.imageCropRatio ? `${field.imageCropRatio[0]} / ${field.imageCropRatio[1]}` : null
401409
}"
402-
:src="transformedImg ?? value?.thumbnail ?? uppyFile.preview"
403-
:alt="value?.name ?? uppyFile?.name"
404-
>
405-
<template v-if="isEditable && !props.field.readOnly">
406-
<button class="absolute flex justify-center items-center gap-2 inset-0 bg-black/50 transition text-white text-xs font-medium opacity-0 group-hover/img:opacity-100" tabindex="-1" @click="onEdit">
407-
{{ __('sharp::form.upload.edit_button') }}
408-
</button>
410+
:src="transformedImg ?? value?.thumbnail ?? uppyFile.preview"
411+
:alt="value?.name ?? uppyFile?.name"
412+
>
413+
<template v-if="isEditable && !props.field.readOnly">
414+
<button class="absolute flex justify-center items-center gap-2 inset-0 bg-black/50 transition text-white text-xs font-medium opacity-0 group-hover/img:opacity-100" tabindex="-1" @click="onEdit">
415+
{{ __('sharp::form.upload.edit_button') }}
416+
</button>
417+
</template>
418+
</div>
419+
</template>
420+
<template v-else-if="playablePreviewUrl ?? value?.playable_preview_url">
421+
<template v-if="(uppyFile?.type ?? value?.mime_type)?.startsWith('video/')">
422+
<video class="rounded-md max-h-[150px]" controls>
423+
<source :src="playablePreviewUrl ?? value.playable_preview_url" :type="uppyFile?.type ?? value?.mime_type">
424+
</video>
409425
</template>
410-
</div>
411-
</template>
412-
<template v-else>
413-
<FileIcon class="self-center size-4" :mime-type="value?.mime_type || uppyFile?.type" />
414-
</template>
415-
<div class="flex-1 min-w-0">
416-
<div class="text-sm truncate">
417-
<template v-if="value?.path">
418-
<TooltipProvider>
419-
<Tooltip :delay-duration="0" disable-hoverable-content>
420-
<TooltipTrigger as-child>
421-
<a class="text-foreground underline underline-offset-4 decoration-foreground/20 hover:decoration-foreground"
422-
:href="route('code16.sharp.download.show', {
426+
<template v-else-if="(uppyFile?.type ?? value?.mime_type)?.startsWith('audio/')">
427+
<audio controls>
428+
<source :src="playablePreviewUrl ?? value.playable_preview_url" :type="uppyFile?.type ?? value?.mime_type">
429+
</audio>
430+
</template>
431+
</template>
432+
<template v-else>
433+
<FileIcon class="self-center size-4" :mime-type="value?.mime_type || uppyFile?.type" />
434+
</template>
435+
<div class="flex-1 min-w-0">
436+
<div class="text-sm truncate">
437+
<template v-if="value?.path">
438+
<TooltipProvider>
439+
<Tooltip :delay-duration="0" disable-hoverable-content>
440+
<TooltipTrigger as-child>
441+
<a class="text-foreground underline underline-offset-4 decoration-foreground/20 hover:decoration-foreground"
442+
:href="route('code16.sharp.download.show', {
423443
entityKey: form.entityKey,
424444
instanceId: form.instanceId,
425445
disk: value.disk,
426446
path: value.path,
427447
})"
428-
:download="value?.name"
429-
>
430-
{{ value?.name }}
431-
</a>
432-
</TooltipTrigger>
448+
:download="value?.name"
449+
>
450+
{{ value?.name }}
451+
</a>
452+
</TooltipTrigger>
433453

434-
<TooltipContent class="pointer-events-none" :side-offset="10">
435-
{{ __('sharp::form.upload.download_tooltip') }}
436-
</TooltipContent>
437-
</Tooltip>
438-
</TooltipProvider>
454+
<TooltipContent class="pointer-events-none" :side-offset="10">
455+
{{ __('sharp::form.upload.download_tooltip') }}
456+
</TooltipContent>
457+
</Tooltip>
458+
</TooltipProvider>
459+
</template>
460+
<template v-else>
461+
{{ value?.name ?? uppyFile?.name }}
462+
</template>
463+
</div>
464+
<template v-if="value?.size ?? uppyFile?.size">
465+
<div class="mt-2 text-xs text-muted-foreground">
466+
{{ filesizeLabel(value?.size ?? uppyFile.size) }}
467+
</div>
439468
</template>
440-
<template v-else>
441-
{{ value?.name ?? uppyFile?.name }}
469+
<template v-if="legend">
470+
<div class="mt-2 text-xs">{{ legend }}</div>
442471
</template>
443-
</div>
444-
<template v-if="value?.size ?? uppyFile?.size">
445-
<div class="mt-2 text-xs text-muted-foreground">
446-
{{ filesizeLabel(value?.size ?? uppyFile.size) }}
447-
</div>
448-
</template>
449-
<template v-if="legend">
450-
<div class="mt-2 text-xs">{{ legend }}</div>
451-
</template>
452-
<template v-if="uppyFile?.progress.percentage < 100 && !hasError">
453-
<div class="mt-2">
454-
<div class="bg-primary h-0.5 transition-all" :style="{ width: `${uppyFile.progress.percentage}%` }" role="progressbar">
472+
<template v-if="uppyFile?.progress.percentage < 100 && !hasError">
473+
<div class="mt-2">
474+
<div class="bg-primary h-0.5 transition-all" :style="{ width: `${uppyFile.progress.percentage}%` }" role="progressbar">
475+
</div>
455476
</div>
456-
</div>
457-
</template>
477+
</template>
478+
</div>
458479
</div>
459480
<DropdownMenu :modal="false">
460481
<DropdownMenuTrigger as-child>

resources/js/show/components/fields/File.vue

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -25,52 +25,66 @@
2525
<ShowFieldLayout v-bind="props">
2626
<div class="flex gap-4 p-4 border rounded-md max-w-[600px]">
2727
<template v-if="value">
28-
<template v-if="value.thumbnail">
29-
<div class="self-center">
30-
<img class="rounded-sm min-w-[40px] max-w-[100px] max-h-[100px] object-contain"
31-
:src="value.thumbnail"
32-
:alt="field.label"
33-
>
34-
</div>
35-
</template>
36-
<template v-else>
37-
<FileIcon class="self-center size-4" :mime-type="value.mime_type" />
38-
</template>
39-
<div class="flex-1 min-w-0 flex flex-col">
40-
<div class="truncate text-sm font-medium">
41-
<TooltipProvider>
42-
<Tooltip :delay-duration="0" disable-hoverable-content>
43-
<TooltipTrigger as-child>
44-
<a class="text-foreground underline underline-offset-4 decoration-foreground/20 hover:underline hover:decoration-foreground"
45-
:href="route('code16.sharp.download.show', {
28+
<div class="flex-1 min-w-0 flex gap-4" :class="value.playable_preview_url ? 'flex-col' : ''">
29+
<template v-if="value.thumbnail">
30+
<div class="self-center">
31+
<img class="rounded-sm min-w-[40px] max-w-[100px] max-h-[100px] object-contain"
32+
:src="value.thumbnail"
33+
:alt="field.label"
34+
>
35+
</div>
36+
</template>
37+
<template v-else-if="value.playable_preview_url">
38+
<template v-if="value.mime_type?.startsWith('video/')">
39+
<video class="rounded-md max-h-[150px]" controls preload="metadata">
40+
<source :src="value.playable_preview_url" :type="value.mime_type">
41+
</video>
42+
</template>
43+
<template v-else-if="value.mime_type?.startsWith('audio/')">
44+
<audio controls preload="metadata">
45+
<source :src="value.playable_preview_url" :type="value.mime_type">
46+
</audio>
47+
</template>
48+
</template>
49+
<template v-else>
50+
<FileIcon class="self-center size-4" :mime-type="value.mime_type" />
51+
</template>
52+
<div class="flex flex-col flex-1 min-w-0">
53+
<div class="truncate text-sm font-medium">
54+
<TooltipProvider>
55+
<Tooltip :delay-duration="0" disable-hoverable-content>
56+
<TooltipTrigger as-child>
57+
<a class="text-foreground underline underline-offset-4 decoration-foreground/20 hover:underline hover:decoration-foreground"
58+
:href="route('code16.sharp.download.show', {
4659
entityKey: show.entityKey,
4760
instanceId: show.instanceId,
4861
disk: value.disk,
4962
path: value.path,
5063
})"
51-
:download="value.name ?? ''"
52-
>
53-
{{ value.name }}
54-
</a>
55-
</TooltipTrigger>
64+
:download="value.name ?? ''"
65+
>
66+
{{ value.name }}
67+
</a>
68+
</TooltipTrigger>
5669

57-
<TooltipContent class="pointer-events-none" :side-offset="10">
58-
{{ __('sharp::form.upload.download_tooltip') }}
59-
</TooltipContent>
60-
</Tooltip>
61-
</TooltipProvider>
62-
</div>
63-
<template v-if="legend">
64-
<div class="mt-2 text-muted-foreground text-sm">
65-
{{ legend }}
70+
<TooltipContent class="pointer-events-none" :side-offset="10">
71+
{{ __('sharp::form.upload.download_tooltip') }}
72+
</TooltipContent>
73+
</Tooltip>
74+
</TooltipProvider>
6675
</div>
67-
</template>
68-
<div class="pt-2 flex gap-4 text-sm">
69-
<template v-if="value.size">
70-
<div class="text-xs text-muted-foreground">
71-
{{ filesizeLabel(value.size) }}
76+
<template v-if="legend">
77+
<div class="mt-2 text-muted-foreground text-sm">
78+
{{ legend }}
7279
</div>
7380
</template>
81+
<div class="pt-2 flex gap-4 text-sm">
82+
<template v-if="value.size">
83+
<div class="text-xs text-muted-foreground">
84+
{{ filesizeLabel(value.size) }}
85+
</div>
86+
</template>
87+
</div>
7488
</div>
7589
</div>
7690
<DropdownMenu :modal="false">

resources/js/types/generated.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,7 @@ export type FormUploadFieldValueData = {
616616
mime_type: string;
617617
size: number;
618618
thumbnail: string | null;
619+
playable_preview_url: string | null;
619620
uploaded: boolean | null;
620621
transformed: boolean | null;
621622
not_found: boolean | null;
@@ -834,6 +835,7 @@ export type ShowFileFieldData = {
834835
name: string;
835836
path: string;
836837
thumbnail: string;
838+
playable_preview_url: string;
837839
size: number;
838840
mime_type: string;
839841
};

src/Data/Form/Fields/FormUploadFieldValueData.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public function __construct(
1919
public string $mime_type,
2020
public int $size,
2121
public ?string $thumbnail,
22+
public ?string $playable_preview_url,
2223
public ?bool $uploaded,
2324
public ?bool $transformed,
2425
public ?bool $not_found,

src/Data/Show/Fields/ShowFileFieldData.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ final class ShowFileFieldData extends Data
1919
'name' => 'string',
2020
'path' => 'string',
2121
'thumbnail' => 'string',
22+
'playable_preview_url' => 'string',
2223
'size' => 'int',
2324
'mime_type' => 'string',
2425
])]

0 commit comments

Comments
 (0)