Skip to content

Commit 53a49c7

Browse files
authored
Merge pull request #617 from code16/upload-playable-preview
Allow to show video / audio preview on upload
2 parents 33673cd + 3c3a6c2 commit 53a49c7

15 files changed

Lines changed: 282 additions & 101 deletions

File tree

demo/app/Sharp/Posts/PostForm.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public function buildFormFields(FieldsContainer $formFields): void
6767
SharpFormEditorUpload::make()
6868
->setStorageDisk('local')
6969
->setStorageBasePath('data/posts/{id}/embed')
70-
->setMaxFileSize(1)
70+
->setMaxFileSize(2)
7171
->setHasLegend()
7272
)
7373
->setMaxLength(2000)
@@ -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))

demo/config/filesystems.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
'local' => [
3434
'driver' => 'local',
3535
'root' => storage_path('app'),
36+
'serve' => true,
3637
],
3738

3839
'public' => [

docs/guide/sharp-uploads.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,3 +336,26 @@ $this->addField(
336336
```
337337

338338
In this code, the `legend` designates a custom attribute.
339+
340+
## Preview audio or video upload
341+
342+
If the field allows to upload an audio or video file, you can display a preview of it by specifying the `withPlayablePreview` option:
343+
344+
```php
345+
class MyForm extends SharpForm
346+
{
347+
// ...
348+
function find($id): array
349+
{
350+
return $this
351+
->setCustomTransformer(
352+
'video',
353+
new SharpUploadModelFormAttributeTransformer(withPlayablePreview: true)
354+
)
355+
->transform(Book::with('video')->findOrFail($id));
356+
}
357+
```
358+
359+
::: warning
360+
This feature is using Laravel's file [Temporary URL](https://laravel.com/docs/12.x/filesystem#temporary-urls) feature which only supports S3 & local driver.
361+
:::

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>

0 commit comments

Comments
 (0)