mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
Supports VHS video outputs in queue sidebar tab (#1174)
* Properly identify gifs * Detect VHS video * Basic video support in queue * Video in lightbox * Preview button * nit * Fix vitest
This commit is contained in:
@@ -249,7 +249,7 @@ const menuItems = computed<MenuItem[]>(() => [
|
|||||||
{
|
{
|
||||||
label: t('loadWorkflow'),
|
label: t('loadWorkflow'),
|
||||||
icon: 'pi pi-file-export',
|
icon: 'pi pi-file-export',
|
||||||
command: () => menuTargetTask.value?.loadWorkflow()
|
command: () => menuTargetTask.value?.loadWorkflow(app)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('goToNode'),
|
label: t('goToNode'),
|
||||||
|
|||||||
@@ -25,7 +25,12 @@
|
|||||||
:src="item.url"
|
:src="item.url"
|
||||||
:contain="false"
|
:contain="false"
|
||||||
class="galleria-image"
|
class="galleria-image"
|
||||||
|
v-if="item.isImage"
|
||||||
/>
|
/>
|
||||||
|
<video v-else-if="item.isVideo" controls width="100%" height="100%">
|
||||||
|
<source :src="item.url" :type="item.format" />
|
||||||
|
{{ $t('videoFailedToLoad') }}
|
||||||
|
</video>
|
||||||
</template>
|
</template>
|
||||||
</Galleria>
|
</Galleria>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="result-container" ref="resultContainer">
|
<div class="result-container" ref="resultContainer">
|
||||||
<template
|
<ComfyImage
|
||||||
v-if="result.mediaType === 'images' || result.mediaType === 'gifs'"
|
v-if="result.isImage"
|
||||||
>
|
:src="result.url"
|
||||||
<ComfyImage
|
class="task-output-image"
|
||||||
:src="result.url"
|
:contain="imageFit === 'contain'"
|
||||||
class="task-output-image"
|
/>
|
||||||
:contain="imageFit === 'contain'"
|
<template v-else-if="result.isVideo">
|
||||||
/>
|
<video controls width="100%" height="100%">
|
||||||
<div class="image-preview-mask">
|
<source :src="result.url" :type="result.format" />
|
||||||
<Button
|
{{ $t('videoFailedToLoad') }}
|
||||||
icon="pi pi-eye"
|
</video>
|
||||||
severity="secondary"
|
|
||||||
@click="emit('preview', result)"
|
|
||||||
rounded
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<!-- TODO: handle more media types -->
|
|
||||||
<div v-else class="task-result-preview">
|
<div v-else class="task-result-preview">
|
||||||
<i class="pi pi-file"></i>
|
<i class="pi pi-file"></i>
|
||||||
<span>{{ result.mediaType }}</span>
|
<span>{{ result.mediaType }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="result.supportsPreview" class="preview-mask">
|
||||||
|
<Button
|
||||||
|
icon="pi pi-eye"
|
||||||
|
severity="secondary"
|
||||||
|
@click="emit('preview', result)"
|
||||||
|
rounded
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -67,7 +70,7 @@ onMounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-preview-mask {
|
.preview-mask {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -80,7 +83,7 @@ onMounted(() => {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-container:hover .image-preview-mask {
|
.result-container:hover .preview-mask {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,15 +12,12 @@ describe('ResultGallery', () => {
|
|||||||
let mockResultItem: ResultItemImpl
|
let mockResultItem: ResultItemImpl
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockResultItem = {
|
mockResultItem = new ResultItemImpl({
|
||||||
filename: 'test.jpg',
|
filename: 'test.jpg',
|
||||||
type: 'images',
|
type: 'images',
|
||||||
nodeId: 'test',
|
nodeId: 1,
|
||||||
mediaType: 'images',
|
mediaType: 'images'
|
||||||
url: 'https://picsum.photos/200/300',
|
})
|
||||||
urlWithTimestamp: 'https://picsum.photos/200/300?t=123456',
|
|
||||||
supportsPreview: true
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const mountResultGallery = (props: ResultGalleryProps, options = {}) => {
|
const mountResultGallery = (props: ResultGalleryProps, options = {}) => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createI18n } from 'vue-i18n'
|
|||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
en: {
|
en: {
|
||||||
|
videoFailedToLoad: 'Video failed to load',
|
||||||
extensionName: 'Extension Name',
|
extensionName: 'Extension Name',
|
||||||
reloadToApplyChanges: 'Reload to apply changes',
|
reloadToApplyChanges: 'Reload to apply changes',
|
||||||
insert: 'Insert',
|
insert: 'Insert',
|
||||||
@@ -112,6 +113,7 @@ const messages = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
|
videoFailedToLoad: '视频加载失败',
|
||||||
extensionName: '扩展名称',
|
extensionName: '扩展名称',
|
||||||
reloadToApplyChanges: '重新加载以应用更改',
|
reloadToApplyChanges: '重新加载以应用更改',
|
||||||
insert: '插入',
|
insert: '插入',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
import { app } from '@/scripts/app'
|
import type { ComfyApp } from '@/scripts/app'
|
||||||
import type {
|
import type {
|
||||||
TaskItem,
|
TaskItem,
|
||||||
TaskType,
|
TaskType,
|
||||||
@@ -10,7 +10,6 @@ import type {
|
|||||||
ResultItem
|
ResultItem
|
||||||
} from '@/types/apiTypes'
|
} from '@/types/apiTypes'
|
||||||
import type { NodeId } from '@/types/comfyWorkflow'
|
import type { NodeId } from '@/types/comfyWorkflow'
|
||||||
import { plainToClass } from 'class-transformer'
|
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { toRaw } from 'vue'
|
import { toRaw } from 'vue'
|
||||||
@@ -35,6 +34,22 @@ export class ResultItemImpl {
|
|||||||
// 'audio' | 'images' | ...
|
// 'audio' | 'images' | ...
|
||||||
mediaType: string
|
mediaType: string
|
||||||
|
|
||||||
|
// VHS output specific fields
|
||||||
|
format?: string
|
||||||
|
frame_rate?: number
|
||||||
|
|
||||||
|
constructor(obj: Record<string, any>) {
|
||||||
|
this.filename = obj.filename
|
||||||
|
this.subfolder = obj.subfolder
|
||||||
|
this.type = obj.type
|
||||||
|
|
||||||
|
this.nodeId = obj.nodeId
|
||||||
|
this.mediaType = obj.mediaType
|
||||||
|
|
||||||
|
this.format = obj.format
|
||||||
|
this.frame_rate = obj.frame_rate
|
||||||
|
}
|
||||||
|
|
||||||
get url(): string {
|
get url(): string {
|
||||||
return api.apiURL(`/view?filename=${encodeURIComponent(this.filename)}&type=${this.type}&
|
return api.apiURL(`/view?filename=${encodeURIComponent(this.filename)}&type=${this.type}&
|
||||||
subfolder=${encodeURIComponent(this.subfolder || '')}`)
|
subfolder=${encodeURIComponent(this.subfolder || '')}`)
|
||||||
@@ -44,8 +59,20 @@ export class ResultItemImpl {
|
|||||||
return `${this.url}&t=${+new Date()}`
|
return `${this.url}&t=${+new Date()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isVideo(): boolean {
|
||||||
|
return this.format && this.format.startsWith('video/')
|
||||||
|
}
|
||||||
|
|
||||||
|
get isGif(): boolean {
|
||||||
|
return this.filename.endsWith('.gif')
|
||||||
|
}
|
||||||
|
|
||||||
|
get isImage(): boolean {
|
||||||
|
return this.mediaType === 'images' || this.isGif
|
||||||
|
}
|
||||||
|
|
||||||
get supportsPreview(): boolean {
|
get supportsPreview(): boolean {
|
||||||
return ['images', 'gifs'].includes(this.mediaType)
|
return this.isImage || this.isVideo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,12 +103,13 @@ export class TaskItemImpl {
|
|||||||
}
|
}
|
||||||
return Object.entries(this.outputs).flatMap(([nodeId, nodeOutputs]) =>
|
return Object.entries(this.outputs).flatMap(([nodeId, nodeOutputs]) =>
|
||||||
Object.entries(nodeOutputs).flatMap(([mediaType, items]) =>
|
Object.entries(nodeOutputs).flatMap(([mediaType, items]) =>
|
||||||
(items as ResultItem[]).map((item: ResultItem) =>
|
(items as ResultItem[]).map(
|
||||||
plainToClass(ResultItemImpl, {
|
(item: ResultItem) =>
|
||||||
...item,
|
new ResultItemImpl({
|
||||||
nodeId,
|
...item,
|
||||||
mediaType
|
nodeId,
|
||||||
})
|
mediaType
|
||||||
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -213,7 +241,7 @@ export class TaskItemImpl {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
public async loadWorkflow() {
|
public async loadWorkflow(app: ComfyApp) {
|
||||||
await app.loadGraphData(toRaw(this.workflow))
|
await app.loadGraphData(toRaw(this.workflow))
|
||||||
if (this.outputs) {
|
if (this.outputs) {
|
||||||
app.nodeOutputs = toRaw(this.outputs)
|
app.nodeOutputs = toRaw(this.outputs)
|
||||||
|
|||||||
Reference in New Issue
Block a user