mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-26 17:54:14 +00:00
Use responsive grid for templates dialog (#2791)
This commit is contained in:
@@ -2,13 +2,14 @@
|
||||
<Card
|
||||
ref="cardRef"
|
||||
:data-testid="`template-workflow-${template.name}`"
|
||||
class="w-64 group"
|
||||
class="w-64 template-card rounded-2xl overflow-hidden cursor-pointer shadow-[0_10px_15px_-3px_rgba(0,0,0,0.08),0_4px_6px_-4px_rgba(0,0,0,0.05)]"
|
||||
:pt="{
|
||||
body: { class: 'p-0' }
|
||||
}"
|
||||
@click="$emit('loadWorkflow', template.name)"
|
||||
>
|
||||
<template #header>
|
||||
<div
|
||||
class="flex items-center justify-center cursor-pointer"
|
||||
@click="$emit('loadWorkflow', template.name)"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="relative overflow-hidden rounded-t-lg">
|
||||
<template v-if="template.mediaType === 'audio'">
|
||||
<AudioThumbnail :src="baseThumbnailSrc" />
|
||||
@@ -33,6 +34,7 @@
|
||||
<DefaultThumbnail
|
||||
:src="baseThumbnailSrc"
|
||||
:alt="title"
|
||||
:is-hovered="isHovered"
|
||||
:hover-zoom="
|
||||
template.thumbnailVariant === 'zoomHover'
|
||||
? UPSCALE_ZOOM_SCALE
|
||||
@@ -47,11 +49,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #subtitle>
|
||||
<div
|
||||
class="text-center py-2 opacity-85 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
{{ title }}
|
||||
<template #content>
|
||||
<div class="flex items-center px-4 py-3">
|
||||
<div class="flex-1">
|
||||
<h3
|
||||
class="line-clamp-1 text-lg font-normal text-surface-900 dark:text-surface-100"
|
||||
>
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p class="line-clamp-2 text-sm text-surface-600 dark:text text-muted">
|
||||
{{ template.description.replace(/[-_]/g, ' ') }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex md:hidden xl:flex items-center justify-center ml-4 w-10 h-10 rounded-full bg-surface-100"
|
||||
>
|
||||
<i class="pi pi-angle-right text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
@@ -71,8 +85,8 @@ import HoverDissolveThumbnail from '@/components/templates/thumbnails/HoverDisso
|
||||
import { TemplateInfo } from '@/types/workflowTemplateTypes'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
const UPSCALE_ZOOM_SCALE = 38 // for upscale templates, exaggerate the hover zoom
|
||||
const DEFAULT_ZOOM_SCALE = 6
|
||||
const UPSCALE_ZOOM_SCALE = 16 // for upscale templates, exaggerate the hover zoom
|
||||
const DEFAULT_ZOOM_SCALE = 5
|
||||
|
||||
const { sourceModule, categoryTitle, loading, template } = defineProps<{
|
||||
sourceModule: string
|
||||
@@ -86,18 +100,23 @@ const { t } = useI18n()
|
||||
const cardRef = ref<HTMLElement | null>(null)
|
||||
const isHovered = useElementHover(cardRef)
|
||||
|
||||
const thumbnailSrc = computed(() =>
|
||||
sourceModule === 'default'
|
||||
? `/templates/${template.name}`
|
||||
: `/api/workflow_templates/${sourceModule}/${template.name}`
|
||||
)
|
||||
const getThumbnailUrl = (index = '') => {
|
||||
const basePath =
|
||||
sourceModule === 'default'
|
||||
? `/templates/${template.name}`
|
||||
: `/api/workflow_templates/${sourceModule}/${template.name}`
|
||||
|
||||
const baseThumbnailSrc = computed(
|
||||
() => `${thumbnailSrc.value}-1.${template.mediaSubtype}`
|
||||
)
|
||||
// For templates from custom nodes, multiple images is not yet supported
|
||||
const indexSuffix = sourceModule === 'default' && index ? `-${index}` : ''
|
||||
|
||||
const overlayThumbnailSrc = computed(
|
||||
() => `${thumbnailSrc.value}-2.${template.mediaSubtype}`
|
||||
return `${basePath}${indexSuffix}.${template.mediaSubtype}`
|
||||
}
|
||||
|
||||
const baseThumbnailSrc = computed(() =>
|
||||
getThumbnailUrl(sourceModule === 'default' ? '1' : '')
|
||||
)
|
||||
const overlayThumbnailSrc = computed(() =>
|
||||
getThumbnailUrl(sourceModule === 'default' ? '2' : '')
|
||||
)
|
||||
|
||||
const title = computed(() => {
|
||||
@@ -112,10 +131,3 @@ defineEmits<{
|
||||
loadWorkflow: [name: string]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.p-card {
|
||||
--p-card-body-padding: 10px 0 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,56 +1,75 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex h-96 overflow-y-hidden"
|
||||
class="flex flex-col h-[83vh] w-[90vw] relative"
|
||||
data-testid="template-workflows-content"
|
||||
>
|
||||
<div class="relative">
|
||||
<ProgressSpinner
|
||||
v-if="!workflowTemplatesStore.isLoaded"
|
||||
class="absolute w-8 h-full inset-0"
|
||||
/>
|
||||
<Listbox
|
||||
:model-value="selectedTab"
|
||||
@update:model-value="handleTabSelection"
|
||||
:options="tabs"
|
||||
option-group-label="label"
|
||||
option-label="title"
|
||||
option-group-children="modules"
|
||||
scroll-height="auto"
|
||||
class="overflow-y-auto w-64 h-full"
|
||||
listStyle="max-height:unset"
|
||||
/>
|
||||
</div>
|
||||
<Carousel
|
||||
class="carousel justify-center"
|
||||
:value="selectedTab.templates"
|
||||
:responsive-options="responsiveOptions"
|
||||
:num-visible="4"
|
||||
:num-scroll="3"
|
||||
:key="`${selectedTab.moduleName}${selectedTab.title}`"
|
||||
>
|
||||
<template #item="slotProps">
|
||||
<div class="p-2 justify-items-center">
|
||||
<TemplateWorkflowCard
|
||||
:sourceModule="selectedTab.moduleName"
|
||||
:template="slotProps.data"
|
||||
:loading="slotProps.data.name === workflowLoading"
|
||||
:categoryTitle="selectedTab.title"
|
||||
@loadWorkflow="loadWorkflow"
|
||||
/>
|
||||
<Button
|
||||
v-if="isSmallScreen"
|
||||
:icon="isSideNavOpen ? 'pi pi-chevron-left' : 'pi pi-chevron-right'"
|
||||
text
|
||||
class="absolute top-1/2 -translate-y-1/2 z-10"
|
||||
:class="isSideNavOpen ? 'left-[19rem]' : 'left-2'"
|
||||
@click="toggleSideNav"
|
||||
/>
|
||||
<Divider
|
||||
class="m-0 [&::before]:border-surface-border/70 [&::before]:border-t-2"
|
||||
/>
|
||||
<div class="flex flex-1 relative overflow-hidden">
|
||||
<aside
|
||||
v-if="isSideNavOpen"
|
||||
class="absolute translate-x-0 top-0 left-0 h-full w-80 shadow-md z-5 transition-transform duration-300 ease-in-out"
|
||||
>
|
||||
<ProgressSpinner
|
||||
v-if="!workflowTemplatesStore.isLoaded"
|
||||
class="absolute w-8 h-full inset-0"
|
||||
/>
|
||||
<TemplateWorkflowsSideNav
|
||||
:tabs="tabs"
|
||||
:selected-tab="selectedTab"
|
||||
@update:selected-tab="handleTabSelection"
|
||||
/>
|
||||
</aside>
|
||||
<div
|
||||
class="flex-1 overflow-auto transition-all duration-300"
|
||||
:class="{
|
||||
'pl-80': isSideNavOpen || !isSmallScreen,
|
||||
'pl-8': !isSideNavOpen && isSmallScreen
|
||||
}"
|
||||
>
|
||||
<div v-if="selectedTab" class="flex flex-col px-12 pb-4">
|
||||
<div class="py-3 text-left">
|
||||
<h2 class="text-lg">{{ selectedTab.title }}</h2>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-[repeat(auto-fill,minmax(16rem,1fr))] gap-8 justify-items-center"
|
||||
>
|
||||
<div v-for="template in selectedTab.templates" :key="template.name">
|
||||
<TemplateWorkflowCard
|
||||
:sourceModule="selectedTab.moduleName"
|
||||
:template="template"
|
||||
:loading="template.name === workflowLoading"
|
||||
:categoryTitle="selectedTab.title"
|
||||
@loadWorkflow="loadWorkflow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Carousel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Carousel from 'primevue/carousel'
|
||||
import Listbox from 'primevue/listbox'
|
||||
import { useBreakpoints } from '@vueuse/core'
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import TemplateWorkflowCard from '@/components/templates/TemplateWorkflowCard.vue'
|
||||
import TemplateWorkflowsSideNav from '@/components/templates/TemplateWorkflowsSideNav.vue'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
@@ -59,43 +78,46 @@ import type { WorkflowTemplates } from '@/types/workflowTemplateTypes'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const responsiveOptions = ref([
|
||||
{
|
||||
breakpoint: '1660px',
|
||||
numVisible: 3,
|
||||
numScroll: 2
|
||||
},
|
||||
{
|
||||
breakpoint: '1360px',
|
||||
numVisible: 2,
|
||||
numScroll: 1
|
||||
},
|
||||
{
|
||||
breakpoint: '960px',
|
||||
numVisible: 1,
|
||||
numScroll: 1
|
||||
}
|
||||
])
|
||||
const breakpoints = useBreakpoints({
|
||||
mobile: 0,
|
||||
tablet: 768,
|
||||
desktop: 1024
|
||||
})
|
||||
const isSmallScreen = breakpoints.between('mobile', 'desktop')
|
||||
const isSideNavOpen = ref(!isSmallScreen.value)
|
||||
const toggleSideNav = () => {
|
||||
isSideNavOpen.value = !isSideNavOpen.value
|
||||
}
|
||||
watch(isSmallScreen, toggleSideNav)
|
||||
|
||||
const workflowTemplatesStore = useWorkflowTemplatesStore()
|
||||
const { isReady } = useAsyncState(
|
||||
workflowTemplatesStore.loadWorkflowTemplates,
|
||||
null
|
||||
)
|
||||
|
||||
const selectedTab = ref<WorkflowTemplates | null>(
|
||||
workflowTemplatesStore?.defaultTemplate
|
||||
workflowTemplatesStore.defaultTemplate
|
||||
)
|
||||
const workflowLoading = ref<string | null>(null)
|
||||
|
||||
const tabs = computed(() => workflowTemplatesStore.groupedTemplates)
|
||||
|
||||
onMounted(async () => {
|
||||
await workflowTemplatesStore.loadWorkflowTemplates()
|
||||
})
|
||||
|
||||
const handleTabSelection = (selection: WorkflowTemplates | null) => {
|
||||
//Listbox allows deselecting so this special case is ignored here
|
||||
if (selection !== selectedTab.value && selection !== null)
|
||||
if (selection !== selectedTab.value && selection !== null) {
|
||||
selectedTab.value = selection
|
||||
|
||||
// On small screens, close the sidebar when a category is selected
|
||||
if (isSmallScreen.value) {
|
||||
isSideNavOpen.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadWorkflow = async (id: string) => {
|
||||
if (!isReady.value) return
|
||||
|
||||
workflowLoading.value = id
|
||||
let json
|
||||
if (selectedTab.value.moduleName === 'default') {
|
||||
@@ -120,9 +142,3 @@ const loadWorkflow = async (id: string) => {
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.carousel {
|
||||
width: 66vw;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3 class="px-4">
|
||||
<span>{{ $t('templateWorkflows.title') }}</span>
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
48
src/components/templates/TemplateWorkflowsSideNav.vue
Normal file
48
src/components/templates/TemplateWorkflowsSideNav.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<ScrollPanel class="w-80" style="height: calc(85vh - 48px)">
|
||||
<Listbox
|
||||
:model-value="selectedTab"
|
||||
@update:model-value="handleTabSelection"
|
||||
:options="tabs"
|
||||
option-group-label="label"
|
||||
option-label="title"
|
||||
option-group-children="modules"
|
||||
:pt="{
|
||||
root: { class: 'w-full border-0 bg-transparent' },
|
||||
list: { class: 'p-0' },
|
||||
option: { class: 'px-12 py-3 text-lg' },
|
||||
optionGroup: { class: 'p-0 text-left text-inherit' }
|
||||
}"
|
||||
listStyle="max-height:unset"
|
||||
>
|
||||
<template #optiongroup="slotProps">
|
||||
<div class="text-left py-3 px-12">
|
||||
<h2 class="text-lg">{{ slotProps.option.label }}</h2>
|
||||
</div>
|
||||
</template>
|
||||
</Listbox>
|
||||
</ScrollPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Listbox from 'primevue/listbox'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
|
||||
import type {
|
||||
TemplateGroup,
|
||||
WorkflowTemplates
|
||||
} from '@/types/workflowTemplateTypes'
|
||||
|
||||
defineProps<{
|
||||
tabs: TemplateGroup[]
|
||||
selectedTab: WorkflowTemplates | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:selectedTab', tab: WorkflowTemplates): void
|
||||
}>()
|
||||
|
||||
const handleTabSelection = (tab: WorkflowTemplates) => {
|
||||
emit('update:selectedTab', tab)
|
||||
}
|
||||
</script>
|
||||
@@ -1,6 +1,13 @@
|
||||
<template>
|
||||
<div class="relative w-64 h-64 rounded-t-lg overflow-hidden select-none">
|
||||
<div v-if="!error" ref="contentRef">
|
||||
<div
|
||||
v-if="!error"
|
||||
ref="contentRef"
|
||||
class="w-64 h-64 object-cover transform-gpu transition-transform duration-1000 ease-out"
|
||||
:style="
|
||||
isHovered ? { transform: `scale(${1 + hoverZoom / 100})` } : undefined
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
<div
|
||||
@@ -19,6 +26,11 @@ import { onMounted, ref } from 'vue'
|
||||
const error = ref(false)
|
||||
const contentRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const { hoverZoom = 4 } = defineProps<{
|
||||
hoverZoom?: number
|
||||
isHovered?: boolean
|
||||
}>()
|
||||
|
||||
onMounted(() => {
|
||||
const images = Array.from(contentRef.value?.getElementsByTagName('img') ?? [])
|
||||
images.forEach((img) => {
|
||||
@@ -28,3 +40,8 @@ onMounted(() => {
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
img {
|
||||
transition: transform 1s cubic-bezier(0.2, 0, 0.4, 1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<BaseThumbnail>
|
||||
<BaseThumbnail :is-hovered="isHovered">
|
||||
<img :src="baseImageSrc" :alt="alt" class="w-full h-full object-cover" />
|
||||
<div ref="containerRef" class="absolute inset-0">
|
||||
<img
|
||||
@@ -26,6 +26,8 @@ import { ref, watch } from 'vue'
|
||||
|
||||
import BaseThumbnail from '@/components/templates/thumbnails/BaseThumbnail.vue'
|
||||
|
||||
const SLIDER_START_POSITION = 21
|
||||
|
||||
const { isHovered } = defineProps<{
|
||||
baseImageSrc: string
|
||||
overlayImageSrc: string
|
||||
@@ -33,7 +35,7 @@ const { isHovered } = defineProps<{
|
||||
isHovered?: boolean
|
||||
}>()
|
||||
|
||||
const sliderPosition = ref(21)
|
||||
const sliderPosition = ref(SLIDER_START_POSITION)
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const { elementX, elementWidth, isOutside } = useMouseInElement(containerRef)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<BaseThumbnail>
|
||||
<div ref="containerRef" class="overflow-hidden">
|
||||
<BaseThumbnail :hover-zoom="hoverZoom" :is-hovered="isHovered">
|
||||
<div class="overflow-hidden">
|
||||
<img
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
@@ -15,23 +15,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useElementHover } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import BaseThumbnail from '@/components/templates/thumbnails/BaseThumbnail.vue'
|
||||
|
||||
const { hoverZoom = 8 } = defineProps<{
|
||||
defineProps<{
|
||||
src: string
|
||||
alt: string
|
||||
hoverZoom?: number
|
||||
isHovered?: boolean
|
||||
}>()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const isHovered = useElementHover(containerRef)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
img {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<BaseThumbnail>
|
||||
<BaseThumbnail :is-hovered="isHovered">
|
||||
<div class="relative w-full h-full">
|
||||
<img
|
||||
:src="baseImageSrc"
|
||||
|
||||
@@ -7,12 +7,14 @@ export const CORE_TEMPLATES = [
|
||||
{
|
||||
name: 'default',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp'
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images from text descriptions.'
|
||||
},
|
||||
{
|
||||
name: 'image2image',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Transform existing images using text prompts.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/img2img/'
|
||||
},
|
||||
@@ -20,12 +22,14 @@ export const CORE_TEMPLATES = [
|
||||
name: 'lora',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Apply LoRA models for specialized styles or subjects.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/lora/'
|
||||
},
|
||||
{
|
||||
name: 'inpaint_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Edit specific parts of images seamlessly.',
|
||||
thumbnailVariant: 'compareSlider',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/inpaint/'
|
||||
@@ -34,6 +38,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'inpain_model_outpainting',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Extend images beyond their original boundaries.',
|
||||
thumbnailVariant: 'compareSlider',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/inpaint/#outpainting'
|
||||
@@ -42,6 +47,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'embedding_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Use textual inversion for consistent styles',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/textual_inversion_embeddings/'
|
||||
},
|
||||
@@ -49,12 +55,14 @@ export const CORE_TEMPLATES = [
|
||||
name: 'gligen_textbox_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Specify the location and size of objects.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/gligen/'
|
||||
},
|
||||
{
|
||||
name: 'lora_multiple',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Combine multiple LoRA models for unique results.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/lora/'
|
||||
}
|
||||
]
|
||||
@@ -68,6 +76,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'flux_dev_checkpoint_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create images using Flux development models.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#flux-dev-1'
|
||||
},
|
||||
@@ -75,6 +84,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'flux_schnell',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images quickly with Flux Schnell.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#flux-schnell-1'
|
||||
},
|
||||
@@ -82,6 +92,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'flux_fill_inpaint_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Fill in missing parts of images.',
|
||||
thumbnailVariant: 'compareSlider',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#fill-inpainting-model'
|
||||
@@ -90,6 +101,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'flux_fill_outpaint_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Extend images using Flux outpainting.',
|
||||
thumbnailVariant: 'compareSlider',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#fill-inpainting-model'
|
||||
@@ -98,6 +110,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'flux_canny_model_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images from edge detection.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#canny-and-depth'
|
||||
@@ -106,6 +119,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'flux_depth_lora_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create images with depth-aware LoRA.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#canny-and-depth'
|
||||
@@ -114,6 +128,8 @@ export const CORE_TEMPLATES = [
|
||||
name: 'flux_redux_model_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Transfer style from a reference image to guide image generation with Flux.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#redux'
|
||||
}
|
||||
@@ -128,6 +144,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'controlnet_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Control image generation with reference images.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/'
|
||||
@@ -136,6 +153,7 @@ export const CORE_TEMPLATES = [
|
||||
name: '2_pass_pose_worship',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images from pose references.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#pose-controlnet'
|
||||
@@ -144,6 +162,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'depth_controlnet',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create images with depth-aware generation.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#t2i-adapter-vs-controlnets'
|
||||
@@ -152,6 +171,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'depth_t2i_adapter',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Quickly generate depth-aware images with a T2I adapter.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#t2i-adapter-vs-controlnets'
|
||||
@@ -160,6 +180,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'mixing_controlnets',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Combine multiple ControlNet models together.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#mixing-controlnets'
|
||||
@@ -175,6 +196,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'hiresfix_latent_workflow',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Enhance image quality in latent space.',
|
||||
thumbnailVariant: 'zoomHover',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/'
|
||||
@@ -183,6 +205,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'esrgan_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Use upscale models to enhance image quality.',
|
||||
thumbnailVariant: 'zoomHover',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/upscale_models/'
|
||||
@@ -191,6 +214,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'hiresfix_esrgan_workflow',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Use upscale models during intermediate steps.',
|
||||
thumbnailVariant: 'zoomHover',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/#non-latent-upscaling'
|
||||
@@ -199,6 +223,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'latent_upscale_different_prompt_model',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Upscale and change prompt across passes',
|
||||
thumbnailVariant: 'zoomHover',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/#more-examples'
|
||||
@@ -210,28 +235,34 @@ export const CORE_TEMPLATES = [
|
||||
title: 'Video',
|
||||
type: 'video',
|
||||
templates: [
|
||||
{
|
||||
name: 'ltxv_image_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/ltxv/'
|
||||
},
|
||||
{
|
||||
name: 'ltxv_text_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/ltxv/'
|
||||
description: 'Generate videos from text descriptions.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/ltxv/#text-to-video'
|
||||
},
|
||||
{
|
||||
name: 'ltxv_image_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Convert still images into videos.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/ltxv/#image-to-video'
|
||||
},
|
||||
{
|
||||
name: 'mochi_text_to_video_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create videos with Mochi model.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/mochi/'
|
||||
},
|
||||
{
|
||||
name: 'hunyuan_video_text_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate videos using Hunyuan model.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_video/'
|
||||
},
|
||||
@@ -239,6 +270,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'image_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Transform images into animated videos.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/video/#image-to-video'
|
||||
},
|
||||
@@ -246,6 +278,8 @@ export const CORE_TEMPLATES = [
|
||||
name: 'txt_to_image_to_video',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Generate images from text and then convert them into videos.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/video/#image-to-video'
|
||||
}
|
||||
@@ -260,6 +294,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'sd3.5_simple_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images with SD 3.5.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35'
|
||||
},
|
||||
@@ -267,6 +302,8 @@ export const CORE_TEMPLATES = [
|
||||
name: 'sd3.5_large_canny_controlnet_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Use edge detection to guide image generation with SD 3.5.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35-controlnets'
|
||||
@@ -275,6 +312,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'sd3.5_large_depth',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create depth-aware images with SD 3.5.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35-controlnets'
|
||||
@@ -283,6 +321,8 @@ export const CORE_TEMPLATES = [
|
||||
name: 'sd3.5_large_blur',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Generate images from blurred reference images with SD 3.5.',
|
||||
thumbnailVariant: 'hoverDissolve',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35-controlnets'
|
||||
@@ -298,18 +338,22 @@ export const CORE_TEMPLATES = [
|
||||
name: 'sdxl_simple_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create high-quality images with SDXL.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/'
|
||||
},
|
||||
{
|
||||
name: 'sdxl_refiner_prompt_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Enhance SDXL outputs with refiners.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/'
|
||||
},
|
||||
{
|
||||
name: 'sdxl_revision_text_prompts',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Transfer concepts from reference images to guide image generation with SDXL.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/#revision'
|
||||
},
|
||||
@@ -317,6 +361,8 @@ export const CORE_TEMPLATES = [
|
||||
name: 'sdxl_revision_zero_positive',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description:
|
||||
'Add text prompts alongside reference images to guide image generation with SDXL.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/#revision'
|
||||
},
|
||||
@@ -324,6 +370,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'sdxlturbo_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate images in a single step with SDXL Turbo.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/sdturbo/'
|
||||
}
|
||||
@@ -338,6 +385,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'area_composition',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Control image composition with areas.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/area_composition/'
|
||||
},
|
||||
@@ -345,6 +393,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'area_composition_reversed',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Reverse area composition workflow.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/area_composition/'
|
||||
},
|
||||
@@ -352,6 +401,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'area_composition_square_area_for_subject',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Create consistent subject placement.',
|
||||
tutorialUrl:
|
||||
'https://comfyanonymous.github.io/ComfyUI_examples/area_composition/#increasing-consistency-of-images-with-area-composition'
|
||||
}
|
||||
@@ -366,6 +416,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'stable_zero123_example',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'Generate 3D views from single images.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/3d/'
|
||||
}
|
||||
]
|
||||
@@ -379,6 +430,7 @@ export const CORE_TEMPLATES = [
|
||||
name: 'stable_audio_example',
|
||||
mediaType: 'audio',
|
||||
mediaSubtype: 'flac',
|
||||
description: 'Generate audio from text descriptions.',
|
||||
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/audio/'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -7,6 +7,7 @@ import PromptDialogContent from '@/components/dialog/content/PromptDialogContent
|
||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import TemplateWorkflowsContent from '@/components/templates/TemplateWorkflowsContent.vue'
|
||||
import TemplateWorkflowsDialogHeader from '@/components/templates/TemplateWorkflowsDialogHeader.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { type ShowDialogOptions, useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
@@ -80,6 +81,12 @@ export const useDialogService = () => {
|
||||
key: 'global-template-workflows',
|
||||
title: t('templateWorkflows.title'),
|
||||
component: TemplateWorkflowsContent,
|
||||
headerComponent: TemplateWorkflowsDialogHeader,
|
||||
dialogComponentProps: {
|
||||
pt: {
|
||||
content: { class: '!px-0' }
|
||||
}
|
||||
},
|
||||
props
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ export const useWorkflowTemplatesStore = defineStore(
|
||||
templates: templates.map((name) => ({
|
||||
name,
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'jpg'
|
||||
mediaSubtype: 'jpg',
|
||||
description: name
|
||||
}))
|
||||
})
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface TemplateInfo {
|
||||
mediaType: string
|
||||
mediaSubtype: string
|
||||
thumbnailVariant?: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface WorkflowTemplates {
|
||||
@@ -11,7 +12,9 @@ export interface WorkflowTemplates {
|
||||
templates: TemplateInfo[]
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface TemplateGroup {
|
||||
label: string
|
||||
icon?: string
|
||||
modules: WorkflowTemplates[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user