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:
@@ -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'
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
25
src/components/PreviewVideo.vue
Normal file
25
src/components/PreviewVideo.vue
Normal 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>
|
||||
@@ -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) {
|
||||
|
||||
@@ -46,7 +46,6 @@ export const useModelExplorer = () => {
|
||||
description: '',
|
||||
metadata: {},
|
||||
preview: '',
|
||||
previewType: 'image',
|
||||
type: folder ?? '',
|
||||
isFolder: true,
|
||||
children: [],
|
||||
|
||||
@@ -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,
|
||||
|
||||
1
src/types/typings.d.ts
vendored
1
src/types/typings.d.ts
vendored
@@ -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
53
src/utils/media.ts
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user