add support for video previews (#197)

* add support for video previews

* fix two cases where video previews did not show
This commit is contained in:
Kevin Lewis
2025-08-14 22:12:13 -04:00
committed by GitHub
parent c96a164f68
commit 71a200ed5c
13 changed files with 267 additions and 120 deletions

View File

@@ -20,7 +20,10 @@
>
<div class="flex gap-4 overflow-hidden whitespace-nowrap">
<div class="h-18 preview-aspect">
<img :src="item.preview" />
<div v-if="isVideoUrl(item.preview)" class="h-full w-full">
<PreviewVideo :src="item.preview" />
</div>
<img v-else :src="item.preview" />
</div>
<div class="flex flex-1 flex-col gap-3 overflow-hidden">
@@ -72,11 +75,13 @@
<script setup lang="ts">
import DialogCreateTask from 'components/DialogCreateTask.vue'
import PreviewVideo from 'components/PreviewVideo.vue'
import ResponseScroll from 'components/ResponseScroll.vue'
import { useContainerQueries } from 'hooks/container'
import { useDialog } from 'hooks/dialog'
import { useDownload } from 'hooks/download'
import Button from 'primevue/button'
import { isVideoUrl } from 'utils/media'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -25,19 +25,10 @@
</svg>
</div>
<div
v-else-if="model.previewType === 'video'"
v-else-if="isVideoUrl(preview)"
class="h-full w-full p-1 hover:p-0"
>
<video
class="h-full w-full object-cover"
playsinline
autoplay
loop
disablepictureinpicture
preload="none"
>
<source :src="preview" />
</video>
<PreviewVideo :src="preview" />
</div>
<div v-else class="h-full w-full p-1 hover:p-0">
<img class="h-full w-full rounded-lg object-cover" :src="preview" />
@@ -81,8 +72,10 @@
<script setup lang="ts">
import { useElementSize } from '@vueuse/core'
import PreviewVideo from 'components/PreviewVideo.vue'
import { useModelNodeAction } from 'hooks/model'
import { BaseModel } from 'types/typings'
import { isVideoUrl } from 'utils/media'
import { computed, ref } from 'vue'
interface Props {

View File

@@ -5,17 +5,17 @@
class="relative mx-auto w-full overflow-hidden rounded-lg preview-aspect"
:style="$sm({ width: `${cardWidth}px` })"
>
<div v-if="previewType === 'video'" class="h-full w-full p-1 hover:p-0">
<video
class="h-full w-full object-cover"
playsinline
autoplay
loop
disablepictureinpicture
preload="none"
>
<source :src="preview" />
</video>
<div
v-if="
preview &&
isVideoUrl(
preview,
currentType === 'local' ? localContentType : undefined,
)
"
class="h-full w-full p-1 hover:p-0"
>
<PreviewVideo :src="preview" />
</div>
<ResponseImage
@@ -48,7 +48,14 @@
}"
>
<template #item="slotProps">
<div
v-if="isVideoUrl(slotProps.data)"
class="h-full w-full p-1 hover:p-0"
>
<PreviewVideo :src="slotProps.data" />
</div>
<ResponseImage
v-else
:src="slotProps.data"
:error="noPreviewContent"
></ResponseImage>
@@ -98,6 +105,7 @@
</template>
<script setup lang="ts">
import PreviewVideo from 'components/PreviewVideo.vue'
import ResponseFileUpload from 'components/ResponseFileUpload.vue'
import ResponseImage from 'components/ResponseImage.vue'
import ResponseInput from 'components/ResponseInput.vue'
@@ -106,13 +114,13 @@ import { useContainerQueries } from 'hooks/container'
import { useModelPreview } from 'hooks/model'
import Button from 'primevue/button'
import Carousel from 'primevue/carousel'
import { isVideoUrl } from 'utils/media'
const editable = defineModel<boolean>('editable')
const { cardWidth } = useConfig()
const {
preview,
previewType,
typeOptions,
currentType,
defaultContent,
@@ -120,6 +128,7 @@ const {
networkContent,
updateLocalContent,
noPreviewContent,
localContentType,
} = useModelPreview()
const { $sm, $xl } = useContainerQueries()

View File

@@ -0,0 +1,25 @@
<template>
<video
class="h-full w-full object-cover"
playsinline
autoplay
loop
muted
disablepictureinpicture
:preload="preload"
>
<source :src="src" type="video/mp4" />
<source :src="src" type="video/webm" />
</video>
</template>
<script setup lang="ts">
interface Props {
src: string
preload?: 'none' | 'metadata' | 'auto'
}
withDefaults(defineProps<Props>(), {
preload: 'metadata',
})
</script>

View File

@@ -46,7 +46,7 @@ const handleDropFile = (event: DragEvent) => {
const handleClick = (event: MouseEvent) => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.accept = 'image/*,video/*'
input.onchange = () => {
const files = input.files
if (files) {

View File

@@ -46,7 +46,6 @@ export const useModelExplorer = () => {
description: '',
metadata: {},
preview: '',
previewType: 'image',
type: folder ?? '',
isFolder: true,
children: [],

View File

@@ -553,9 +553,11 @@ export const useModelPreviewEditor = (formInstance: ModelFormInstance) => {
* Local file url
*/
const localContent = ref<string>()
const localContentType = ref<string>()
const updateLocalContent = async (event: SelectEvent) => {
const { files } = event
localContent.value = files[0].objectURL
localContentType.value = files[0].type
}
/**
@@ -587,16 +589,13 @@ export const useModelPreviewEditor = (formInstance: ModelFormInstance) => {
return content
})
const previewType = computed(() => {
return model.value.previewType
})
onMounted(() => {
registerReset(() => {
currentType.value = 'default'
defaultContentPage.value = 0
networkContent.value = undefined
localContent.value = undefined
localContentType.value = undefined
})
registerSubmit((data) => {
@@ -606,7 +605,6 @@ export const useModelPreviewEditor = (formInstance: ModelFormInstance) => {
const result = {
preview,
previewType,
typeOptions,
currentType,
// default value
@@ -616,6 +614,7 @@ export const useModelPreviewEditor = (formInstance: ModelFormInstance) => {
networkContent,
// local file
localContent,
localContentType,
updateLocalContent,
// no preview
noPreviewContent,

View File

@@ -11,7 +11,6 @@ export interface BaseModel {
pathIndex: number
isFolder: boolean
preview: string | string[]
previewType: string
description: string
metadata: Record<string, string>
}

53
src/utils/media.ts Normal file
View File

@@ -0,0 +1,53 @@
/**
* Media file utility functions
*/
const VIDEO_EXTENSIONS = [
'.mp4',
'.webm',
'.mov',
'.avi',
'.mkv',
'.flv',
'.wmv',
'.m4v',
'.ogv',
]
const VIDEO_HOST_PATTERNS = [
'/video', // Civitai video URLs often end with /video
'type=video', // URLs with video type parameter
'format=video', // URLs with video format parameter
'video.civitai.com', // Civitai video domain
]
/**
* Detect if a URL points to a video based on extension or URL patterns
* @param url - The URL to check
* @param localContentType - Optional MIME type for local files
*/
export const isVideoUrl = (url: string, localContentType?: string): boolean => {
if (!url) return false
// For local files with known MIME type
if (localContentType && localContentType.startsWith('video/')) {
return true
}
const urlLower = url.toLowerCase()
// First check if URL ends with a video extension
for (const ext of VIDEO_EXTENSIONS) {
if (urlLower.endsWith(ext)) {
return true
}
}
// Check if URL contains a video extension anywhere (for complex URLs like Civitai)
if (VIDEO_EXTENSIONS.some((ext) => urlLower.includes(ext))) {
return true
}
// Check for specific video hosting patterns
return VIDEO_HOST_PATTERNS.some((pattern) => urlLower.includes(pattern))
}