Files
ComfyUI-Model-Manager/src/components/DialogManager.vue

306 lines
8.7 KiB
Vue

<template>
<div
ref="contentContainer"
class="flex h-full flex-col gap-4 overflow-hidden"
>
<div
class="grid grid-cols-1 justify-center gap-4 px-8"
:style="$content_lg(contentStyle)"
>
<div ref="toolbarContainer" class="col-span-full">
<div :class="['flex gap-4', $toolbar_2xl('flex-row', 'flex-col')]">
<div class="flex-1">
<ResponseInput
v-model="searchContent"
:placeholder="$t('searchModels')"
:allow-clear="true"
suffix-icon="pi pi-search"
></ResponseInput>
</div>
<div class="flex items-center justify-between gap-4 overflow-hidden">
<ResponseSelect
class="flex-1"
v-model="currentType"
:items="typeOptions"
></ResponseSelect>
<ResponseSelect
class="flex-1"
v-model="sortOrder"
:items="sortOrderOptions"
></ResponseSelect>
<ResponseSelect
class="flex-1"
v-model="cardSizeFlag"
:items="cardSizeOptions"
></ResponseSelect>
</div>
</div>
</div>
</div>
<ResponseScroll :items="list" :itemSize="itemSize" class="h-full flex-1">
<template #item="{ item }">
<div
class="grid grid-cols-1 justify-center gap-8 px-8"
:style="contentStyle"
>
<ModelCard
v-for="model in item.row"
:key="genModelKey(model)"
:model="model"
:style="{
width: `${cardSize.width}px`,
height: `${cardSize.height}px`,
}"
class="group/card cursor-pointer !p-0"
@click="openModelDetail(model)"
v-tooltip.top="{
value: getFullPath(model),
autoHide: false,
showDelay: 800,
hideDelay: 300,
pt: { root: { style: { zIndex: 2100, maxWidth: '32rem' } } },
}"
>
<template #name>
<div
v-show="showModelName"
class="absolute top-0 h-full w-full p-2"
>
<div class="flex h-full flex-col justify-end text-lg">
<div class="line-clamp-3 break-all font-bold text-shadow">
{{ model.basename }}
</div>
</div>
</div>
</template>
<template #extra>
<div
v-show="showModeAction"
class="pointer-events-none absolute right-2 top-2 opacity-0 duration-300 group-hover/card:opacity-100"
>
<div class="flex flex-col gap-2">
<Button
icon="pi pi-plus"
severity="secondary"
rounded
@click.stop="addModelNode(model)"
></Button>
<Button
icon="pi pi-copy"
severity="secondary"
rounded
@click.stop="copyModelNode(model)"
></Button>
<Button
v-show="model.preview"
icon="pi pi-file-import"
severity="secondary"
rounded
@click.stop="loadPreviewWorkflow(model)"
></Button>
</div>
</div>
</template>
</ModelCard>
<div class="col-span-full"></div>
</div>
</template>
<template #empty>
<div class="flex flex-col items-center gap-4 pt-20 opacity-70">
<i class="pi pi-box text-4xl"></i>
<div class="select-none text-lg font-bold">No models found</div>
</div>
</template>
</ResponseScroll>
</div>
</template>
<script setup lang="ts" name="manager-dialog">
import { useElementSize } from '@vueuse/core'
import ModelCard from 'components/ModelCard.vue'
import ResponseInput from 'components/ResponseInput.vue'
import ResponseScroll from 'components/ResponseScroll.vue'
import ResponseSelect from 'components/ResponseSelect.vue'
import { configSetting, useConfig } from 'hooks/config'
import { useContainerQueries } from 'hooks/container'
import { useModelNodeAction, useModels } from 'hooks/model'
import { chunk } from 'lodash'
import Button from 'primevue/button'
import { app } from 'scripts/comfyAPI'
import { Model } from 'types/typings'
import { genModelKey } from 'utils/model'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const {
isMobile,
gutter,
cardSize,
cardSizeMap,
cardSizeFlag,
dialog: settings,
} = useConfig()
const { data, folders, openModelDetail, getFullPath } = useModels()
const { t } = useI18n()
const toolbarContainer = ref<HTMLElement | null>(null)
const { $2xl: $toolbar_2xl } = useContainerQueries(toolbarContainer)
const contentContainer = ref<HTMLElement | null>(null)
const { $lg: $content_lg } = useContainerQueries(contentContainer)
const searchContent = ref<string>()
const allType = 'All'
const currentType = ref(allType)
const typeOptions = computed(() => {
const excludeScanTypes = app.ui?.settings.getSettingValue<string>(
configSetting.excludeScanTypes,
)
const customBlackList =
excludeScanTypes
?.split(',')
.map((type) => type.trim())
.filter(Boolean) ?? []
return [
allType,
...Object.keys(folders.value).filter(
(folder) => !customBlackList.includes(folder),
),
].map((type) => {
return {
label: type,
value: type,
command: () => {
currentType.value = type
},
}
})
})
const sortOrder = ref('name')
const sortOrderOptions = ref(
['name', 'size', 'created', 'modified'].map((key) => {
return {
label: t(`sort.${key}`),
value: key,
icon: key === 'name' ? 'pi pi-sort-alpha-down' : 'pi pi-sort-amount-down',
command: () => {
sortOrder.value = key
},
}
}),
)
const itemSize = computed(() => {
let itemHeight = cardSize.value.height
let itemGutter = gutter
if (isMobile.value) {
const baseSize = 16
itemHeight = window.innerWidth - baseSize * 2 * 2
itemGutter = baseSize * 2
}
return itemHeight + itemGutter
})
const { width } = useElementSize(contentContainer)
const cols = computed(() => {
if (isMobile.value) {
return 1
}
const containerWidth = width.value
const itemWidth = cardSize.value.width
return Math.floor((containerWidth - gutter) / (itemWidth + gutter))
})
const list = computed(() => {
const mergedList = Object.values(data.value).flat()
const pureModels = mergedList.filter((item) => {
return !item.isFolder
})
const filterList = pureModels.filter((model) => {
const showAllModel = currentType.value === allType
const matchType = showAllModel || model.type === currentType.value
const filter = searchContent.value?.toLowerCase() ?? ''
const matchSubFolder = model.subFolder.toLowerCase().includes(filter)
const matchName = model.basename.toLowerCase().includes(filter)
return matchType && (matchSubFolder || matchName)
})
let sortStrategy: (a: Model, b: Model) => number = () => 0
switch (sortOrder.value) {
case 'name':
sortStrategy = (a, b) => a.basename.localeCompare(b.basename)
break
case 'size':
sortStrategy = (a, b) => b.sizeBytes - a.sizeBytes
break
case 'created':
sortStrategy = (a, b) => b.createdAt - a.createdAt
break
case 'modified':
sortStrategy = (a, b) => b.updatedAt - a.updatedAt
break
default:
break
}
const sortedList = filterList.sort(sortStrategy)
return chunk(sortedList, cols.value).map((row) => {
return { key: row.map(genModelKey).join(','), row }
})
})
const contentStyle = computed(() => ({
gridTemplateColumns: `repeat(auto-fit, ${cardSize.value.width}px)`,
gap: `${gutter}px`,
paddingLeft: `1rem`,
paddingRight: `1rem`,
}))
const cardSizeOptions = computed(() => {
const customSize = 'size.custom'
const customOptionMap = {
...cardSizeMap.value,
[customSize]: 'custom',
}
return Object.keys(customOptionMap).map((key) => {
return {
label: t(key),
value: key,
command: () => {
if (key === customSize) {
settings.showCardSizeSetting()
} else {
cardSizeFlag.value = key
}
},
}
})
})
const showModelName = computed(() => {
return cardSize.value.width > 120 && cardSize.value.height > 160
})
const showModeAction = computed(() => {
return cardSize.value.width > 120 && cardSize.value.height > 160
})
const { addModelNode, copyModelNode, loadPreviewWorkflow } =
useModelNodeAction()
</script>