forked from BRETT/BRETT
110 lines
3.3 KiB
Vue
110 lines
3.3 KiB
Vue
<script setup lang="ts">
|
|
import { useDropZone, useEventListener } from '@vueuse/core'
|
|
import { toast } from 'vue-sonner'
|
|
import { isMediaFile, isMediaItemType } from '~~/shared/utils/media'
|
|
import { Button } from '@/components/ui/button'
|
|
|
|
const model = defineModel<File | null>()
|
|
|
|
const props = withDefaults(defineProps<{
|
|
disabled?: boolean
|
|
accept?: string
|
|
hint?: string
|
|
}>(), {
|
|
accept: 'image/*,video/*,audio/*,.mp3,.flac',
|
|
})
|
|
|
|
const { t } = useI18n()
|
|
const hintText = computed(() => props.hint ?? t('upload.dropzoneHint'))
|
|
|
|
const dropzoneRef = ref<HTMLElement>()
|
|
const inputRef = ref<HTMLInputElement>()
|
|
const previewUrl = ref<string | null>(null)
|
|
|
|
const { isOverDropZone } = useDropZone(dropzoneRef, {
|
|
dataTypes: types => types.some(isMediaItemType),
|
|
onDrop(files) {
|
|
if (props.disabled) return
|
|
const file = files?.find(isMediaFile)
|
|
if (file) select(file)
|
|
else if (files?.length) toast.error(t('upload.onlyMedia'))
|
|
},
|
|
})
|
|
|
|
// Drag-Events nicht bis zur seitenweiten Dropzone (document.body) durchreichen —
|
|
// sonst würde ein Drop hier zusätzlich den globalen Upload auslösen
|
|
useEventListener(dropzoneRef, ['dragenter', 'dragover', 'dragleave', 'drop'], e => e.stopPropagation())
|
|
|
|
function select(file: File | null) {
|
|
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
|
|
previewUrl.value = file?.type.startsWith('image/') ? URL.createObjectURL(file) : null
|
|
model.value = file
|
|
if (inputRef.value) inputRef.value.value = ''
|
|
}
|
|
|
|
function openPicker() {
|
|
if (!props.disabled) inputRef.value?.click()
|
|
}
|
|
|
|
function formatSize(bytes: number) {
|
|
return bytes >= 1024 * 1024
|
|
? `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
|
: `${Math.max(1, Math.round(bytes / 1024))} KB`
|
|
}
|
|
|
|
onUnmounted(() => {
|
|
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
ref="dropzoneRef"
|
|
role="button"
|
|
tabindex="0"
|
|
:aria-disabled="disabled"
|
|
class="flex min-h-32 cursor-pointer flex-col items-center justify-center gap-2 rounded-md border-2 border-dashed p-4 text-center text-sm transition-colors"
|
|
:class="[
|
|
isOverDropZone ? 'border-primary bg-accent' : 'border-primary/40 hover:border-primary',
|
|
disabled && 'pointer-events-none opacity-50',
|
|
]"
|
|
@click="openPicker"
|
|
@keydown.enter.prevent="openPicker"
|
|
@keydown.space.prevent="openPicker"
|
|
>
|
|
<input
|
|
ref="inputRef"
|
|
type="file"
|
|
:accept="accept"
|
|
class="hidden"
|
|
@change="select(($event.target as HTMLInputElement).files?.[0] ?? null)"
|
|
>
|
|
|
|
<template v-if="model">
|
|
<img
|
|
v-if="previewUrl"
|
|
:src="previewUrl"
|
|
:alt="model.name"
|
|
class="max-h-32 rounded-md"
|
|
>
|
|
<span class="max-w-full truncate font-medium">{{ model.name }}</span>
|
|
<span class="text-xs text-muted-foreground">{{ formatSize(model.size) }}</span>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
@click.stop="select(null)"
|
|
>
|
|
<Icon name="ph:x-fill" class="size-4" />
|
|
{{ $t('common.remove') }}
|
|
</Button>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<Icon name="ph:cloud-arrow-up-fill" class="size-8 text-muted-foreground" />
|
|
<span>{{ $t('upload.dropzoneText') }}</span>
|
|
<span class="text-xs text-muted-foreground">{{ hintText }}</span>
|
|
</template>
|
|
</div>
|
|
</template>
|