2022-12-26 11:50:11 +03:00
|
|
|
<script lang="ts" setup>
|
2023-01-01 23:47:46 +03:00
|
|
|
import { fileOpen } from 'browser-fs-access'
|
|
|
|
import type { FileWithHandle } from 'browser-fs-access'
|
|
|
|
|
2022-12-26 11:50:11 +03:00
|
|
|
const props = withDefaults(defineProps<{
|
2023-01-05 19:48:20 +03:00
|
|
|
modelValue?: FileWithHandle | null
|
2022-12-26 11:50:11 +03:00
|
|
|
/** The image src before change */
|
|
|
|
original?: string
|
|
|
|
/** Allowed file types */
|
|
|
|
allowedFileTypes?: string[]
|
|
|
|
/** Allowed file size */
|
|
|
|
allowedFileSize?: number
|
|
|
|
|
|
|
|
imgClass?: string
|
|
|
|
|
|
|
|
loading?: boolean
|
|
|
|
}>(), {
|
|
|
|
allowedFileTypes: () => ['image/jpeg', 'image/png'],
|
|
|
|
allowedFileSize: 1024 * 1024 * 5, // 5 MB
|
|
|
|
})
|
2023-01-01 23:47:46 +03:00
|
|
|
const emit = defineEmits<{
|
|
|
|
(event: 'update:modelValue', value: FileWithHandle): void
|
|
|
|
(event: 'pick', value: FileWithHandle): void
|
2022-12-26 11:50:11 +03:00
|
|
|
(event: 'error', code: number, message: string): void
|
|
|
|
}>()
|
|
|
|
|
2023-01-01 23:47:46 +03:00
|
|
|
const file = useVModel(props, 'modelValue', emit, { passive: true })
|
2022-12-26 11:50:11 +03:00
|
|
|
|
|
|
|
const { t } = useI18n()
|
|
|
|
|
2023-01-01 23:47:46 +03:00
|
|
|
const defaultImage = computed(() => props.original || '')
|
|
|
|
/** Preview of selected images */
|
|
|
|
const previewImage = ref('')
|
|
|
|
/** The current images on display */
|
|
|
|
const imageSrc = computed<string>(() => previewImage.value || defaultImage.value)
|
2022-12-26 11:50:11 +03:00
|
|
|
|
2023-01-01 23:47:46 +03:00
|
|
|
const pickImage = async () => {
|
|
|
|
const image = await fileOpen({
|
|
|
|
description: 'Image',
|
|
|
|
mimeTypes: props.allowedFileTypes,
|
|
|
|
})
|
2022-12-26 11:50:11 +03:00
|
|
|
|
2023-01-01 23:47:46 +03:00
|
|
|
if (!props.allowedFileTypes.includes(image.type)) {
|
|
|
|
emit('error', 1, t('error.unsupported_file_format'))
|
|
|
|
return
|
2022-12-26 11:50:11 +03:00
|
|
|
}
|
|
|
|
else if (image.size > props.allowedFileSize) {
|
2023-01-01 23:47:46 +03:00
|
|
|
emit('error', 2, t('error.file_size_cannot_exceed_n_mb', [5]))
|
|
|
|
return
|
2022-12-26 11:50:11 +03:00
|
|
|
}
|
|
|
|
|
2023-01-01 23:47:46 +03:00
|
|
|
file.value = image
|
|
|
|
emit('pick', file.value)
|
|
|
|
}
|
2022-12-26 11:50:11 +03:00
|
|
|
|
2023-01-01 23:47:46 +03:00
|
|
|
watch(file, (image, _, onCleanup) => {
|
2022-12-26 11:50:11 +03:00
|
|
|
let expired = false
|
|
|
|
onCleanup(() => expired = true)
|
|
|
|
|
2023-01-01 23:47:46 +03:00
|
|
|
if (!image) {
|
2022-12-26 11:50:11 +03:00
|
|
|
previewImage.value = ''
|
2023-01-01 23:47:46 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
const reader = new FileReader()
|
|
|
|
reader.readAsDataURL(image)
|
|
|
|
reader.onload = (evt) => {
|
|
|
|
if (expired)
|
|
|
|
return
|
|
|
|
previewImage.value = evt.target?.result as string
|
2022-12-26 11:50:11 +03:00
|
|
|
}
|
|
|
|
})
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<label
|
|
|
|
class="bg-slate-500/10 focus-within:(outline outline-primary)"
|
|
|
|
relative
|
|
|
|
flex justify-center items-center
|
|
|
|
cursor-pointer
|
|
|
|
of-hidden
|
2023-01-01 23:47:46 +03:00
|
|
|
@click="pickImage"
|
2022-12-26 11:50:11 +03:00
|
|
|
>
|
|
|
|
<img
|
|
|
|
v-if="imageSrc"
|
|
|
|
:src="imageSrc"
|
|
|
|
:class="imgClass || ''"
|
|
|
|
object-cover
|
|
|
|
w-full
|
|
|
|
h-full
|
|
|
|
>
|
|
|
|
<div absolute bg="black/50" text-white rounded-full text-xl w12 h12 flex justify-center items-center hover="bg-black/40 text-primary">
|
|
|
|
<div i-ri:upload-line />
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div
|
|
|
|
v-if="loading"
|
|
|
|
absolute inset-0
|
|
|
|
bg="black/30" text-white
|
|
|
|
flex justify-center items-center
|
|
|
|
>
|
|
|
|
<div class="i-ri:loader-4-line animate-spin animate-duration-[2.5s]" text-4xl />
|
|
|
|
</div>
|
|
|
|
</label>
|
|
|
|
</template>
|