refactor(front): improve image handling in FileIcon component by adding HEIC support and optimizing file type checks

This commit is contained in:
keven1024
2026-04-11 10:47:52 +08:00
parent 0e0f6bb1c5
commit 018d13ea78
4 changed files with 64 additions and 63 deletions

View File

@@ -1,16 +1,21 @@
<script setup lang="ts">
import type { filePreview } from './Index.vue'
import { isHeic, heicTo } from 'heic-to'
const props = defineProps<{
file: File | filePreview
file: File
}>()
const imageUrl = computed(() => {
if (props?.file?.type?.startsWith('image/') && props?.file instanceof File) {
return URL.createObjectURL(props?.file)
const { state: imageUrl } = useAsyncState(async () => {
let blob: Blob = props?.file
if (await isHeic(props?.file)) {
blob = await heicTo({
blob: props?.file,
type: 'image/jpeg',
quality: 1,
})
}
return null
})
return URL.createObjectURL(blob)
}, null)
onUnmounted(() => {
if (imageUrl.value) {

View File

@@ -9,7 +9,6 @@ export type filePreview = {
size: number
}
import { LucideFileAudio, LucideFileVideo, LucideFile, LucideFileCode, LucideFileArchive, LucideFileText } from 'lucide-vue-next'
const props = withDefaults(
defineProps<{
file: File | filePreview
@@ -20,15 +19,16 @@ const props = withDefaults(
size: 'md',
}
)
const isImage = computed(() => props?.file?.type?.startsWith('image/'))
const isVideo = computed(() => props?.file?.type?.startsWith('video/'))
const isFile = computed(() => props?.file instanceof File)
const isImage = computed(() => isFile.value && props?.file?.type?.startsWith('image/'))
const isVideo = computed(() => isFile.value && props?.file?.type?.startsWith('video/'))
</script>
<template>
<div v-if="isImage || isVideo" :class="cx('flex overflow-hidden', size === 'sm' && 'max-w-20 max-h-16', size === 'md' && 'max-w-30 max-h-20')">
<component
:is="isImage ? ImageIcon : VideoIcon"
:file="props?.file"
:file="props?.file as File"
class="block max-w-full max-h-full object-contain border border-black/20 rounded"
/>
</div>

View File

@@ -1,60 +1,17 @@
<script setup lang="ts">
import type { filePreview } from './Index.vue'
import getVideoFileThumbnail from '@/lib/getVideoFileThumbnail'
const props = defineProps<{
file: File | filePreview
file: File
}>()
const { state: thumbnailUrl } = useAsyncState(async () => {
if (props.file instanceof File && props.file.type.startsWith('video/')) {
return await extractThumbnail(props.file)
if (props.file.type.startsWith('video/')) {
return await getVideoFileThumbnail(props.file)
}
return null
}, null)
async function extractThumbnail(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const video = document.createElement('video')
const objectUrl = URL.createObjectURL(file)
video.muted = true
video.playsInline = true
video.preload = 'metadata'
video.onloadedmetadata = () => {
video.currentTime = video.duration * 0.1
}
video.onseeked = async () => {
try {
// WebCodecs: capture a VideoFrame from the video element
const frame = new VideoFrame(video)
const bitmap = await createImageBitmap(frame)
frame.close()
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height)
const ctx = canvas.getContext('2d')!
ctx.drawImage(bitmap, 0, 0)
bitmap.close()
const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.8 })
URL.revokeObjectURL(objectUrl)
resolve(URL.createObjectURL(blob))
} catch (e) {
URL.revokeObjectURL(objectUrl)
reject(e)
}
}
video.onerror = () => {
URL.revokeObjectURL(objectUrl)
reject(new Error('Video load failed'))
}
video.src = objectUrl
})
}
onUnmounted(() => {
if (!!thumbnailUrl.value) {
URL.revokeObjectURL(thumbnailUrl.value)
@@ -67,9 +24,4 @@ onUnmounted(() => {
<img :src="thumbnailUrl" class="object-contain block max-w-full max-h-full" />
<LucidePlay class="size-[40%] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white" />
</div>
<div v-else>
<div class="h-16 aspect-video flex justify-center items-center bg-white/80 rounded-sm">
<LucideVideo class="size-10" />
</div>
</div>
</template>

View File

@@ -0,0 +1,44 @@
async function getVideoFileThumbnail(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const video = document.createElement('video')
const objectUrl = URL.createObjectURL(file)
video.muted = true
video.playsInline = true
video.preload = 'metadata'
video.onloadedmetadata = () => {
video.currentTime = video.duration * 0.1
}
video.onseeked = async () => {
try {
// WebCodecs: capture a VideoFrame from the video element
const frame = new VideoFrame(video)
const bitmap = await createImageBitmap(frame)
frame.close()
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height)
const ctx = canvas.getContext('2d')!
ctx.drawImage(bitmap, 0, 0)
bitmap.close()
const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.8 })
URL.revokeObjectURL(objectUrl)
resolve(URL.createObjectURL(blob))
} catch (e) {
URL.revokeObjectURL(objectUrl)
reject(e)
}
}
video.onerror = () => {
URL.revokeObjectURL(objectUrl)
reject(new Error('Video load failed'))
}
video.src = objectUrl
})
}
export default getVideoFileThumbnail