mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Add custom nodes manager UI (#2923)
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
13
public/assets/images/fallback-gradient-avatar.svg
Normal file
13
public/assets/images/fallback-gradient-avatar.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 81 KiB |
31
src/components/common/ContentDivider.vue
Normal file
31
src/components/common/ContentDivider.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<hr
|
||||
:class="{
|
||||
'm-0': true,
|
||||
'border-t': orientation === 'horizontal',
|
||||
'border-l': orientation === 'vertical',
|
||||
'h-full': orientation === 'vertical',
|
||||
'w-full': orientation === 'horizontal'
|
||||
}"
|
||||
:style="{
|
||||
borderColor: isLightTheme ? '#DCDAE1' : '#2C2C2C',
|
||||
borderWidth: `${width}px !important`
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const { orientation = 'horizontal', width = 0.3 } = defineProps<{
|
||||
orientation?: 'horizontal' | 'vertical'
|
||||
width?: number
|
||||
}>()
|
||||
|
||||
const isLightTheme = computed(
|
||||
() => colorPaletteStore.completedActivePalette.light_theme
|
||||
)
|
||||
</script>
|
||||
202
src/components/dialog/content/manager/ManagerDialogContent.vue
Normal file
202
src/components/dialog/content/manager/ManagerDialogContent.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col mx-auto overflow-hidden h-[83vh] relative"
|
||||
:aria-label="$t('manager.title')"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<div class="flex flex-1 relative overflow-hidden">
|
||||
<ManagerNavSidebar
|
||||
v-if="isSideNavOpen"
|
||||
:tabs="tabs"
|
||||
:selected-tab="selectedTab"
|
||||
@update:selected-tab="handleTabSelection"
|
||||
/>
|
||||
<div
|
||||
class="flex-1 overflow-auto"
|
||||
:class="{
|
||||
'transition-all duration-300': isSmallScreen,
|
||||
'pl-80': isSideNavOpen || !isSmallScreen,
|
||||
'pl-8': !isSideNavOpen && isSmallScreen,
|
||||
'pr-80': showInfoPanel
|
||||
}"
|
||||
>
|
||||
<div class="px-6 pt-6 flex flex-col h-full">
|
||||
<RegistrySearchBar
|
||||
v-if="!hideSearchBar"
|
||||
v-model:searchQuery="searchQuery"
|
||||
:searchResults="searchResults"
|
||||
@update:sortBy="handleSortChange"
|
||||
@update:filterBy="handleFilterChange"
|
||||
/>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<NoResultsPlaceholder
|
||||
v-if="error || searchResults.length === 0"
|
||||
:title="
|
||||
error
|
||||
? $t('manager.errorConnecting')
|
||||
: $t('manager.noResultsFound')
|
||||
"
|
||||
:message="
|
||||
error
|
||||
? $t('manager.tryAgainLater')
|
||||
: $t('manager.tryDifferentSearch')
|
||||
"
|
||||
/>
|
||||
<div
|
||||
v-else-if="isLoading"
|
||||
class="flex justify-center items-center h-full"
|
||||
>
|
||||
<ProgressSpinner />
|
||||
</div>
|
||||
<div v-else class="h-full" @click="handleGridContainerClick">
|
||||
<VirtualGrid
|
||||
:items="resultsWithKeys"
|
||||
:defaultItemSize="DEFAULT_CARD_SIZE"
|
||||
class="p-0 m-0 max-w-full"
|
||||
:buffer-rows="2"
|
||||
:gridStyle="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(${DEFAULT_CARD_SIZE}px, 1fr))`,
|
||||
padding: '0.5rem',
|
||||
gap: '1.125rem 1.25rem',
|
||||
justifyContent: 'stretch'
|
||||
}"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<div
|
||||
class="relative w-full aspect-square cursor-pointer"
|
||||
@click.stop="(event) => selectNodePack(item, event)"
|
||||
>
|
||||
<PackCard
|
||||
:node-pack="item"
|
||||
:is-selected="
|
||||
selectedNodePacks.some((pack) => pack.id === item.id)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showInfoPanel"
|
||||
class="w-80 border-l-0 border-surface-border absolute right-0 top-0 bottom-0 flex z-20"
|
||||
>
|
||||
<ContentDivider orientation="vertical" :width="0.2" />
|
||||
<div class="flex-1 flex flex-col isolate">
|
||||
<InfoPanel
|
||||
v-if="!hasMultipleSelections"
|
||||
:node-pack="selectedNodePack"
|
||||
/>
|
||||
<InfoPanelMultiItem v-else :node-packs="selectedNodePacks" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
import ManagerNavSidebar from '@/components/dialog/content/manager/ManagerNavSidebar.vue'
|
||||
import InfoPanel from '@/components/dialog/content/manager/infoPanel/InfoPanel.vue'
|
||||
import InfoPanelMultiItem from '@/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue'
|
||||
import PackCard from '@/components/dialog/content/manager/packCard/PackCard.vue'
|
||||
import RegistrySearchBar from '@/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue'
|
||||
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
|
||||
import { useRegistrySearch } from '@/composables/useRegistrySearch'
|
||||
import type { NodeField, TabItem } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const DEFAULT_CARD_SIZE = 512
|
||||
|
||||
const {
|
||||
isSmallScreen,
|
||||
isOpen: isSideNavOpen,
|
||||
toggle: toggleSideNav
|
||||
} = useResponsiveCollapse()
|
||||
const hideSearchBar = computed(() => isSmallScreen.value && showInfoPanel.value)
|
||||
|
||||
const tabs = ref<TabItem[]>([
|
||||
{ id: 'all', label: 'All', icon: 'pi-list' },
|
||||
{ id: 'community', label: 'Community', icon: 'pi-globe' },
|
||||
{ id: 'installed', label: 'Installed', icon: 'pi-box' }
|
||||
])
|
||||
const selectedTab = ref<TabItem>(tabs.value[0])
|
||||
const handleTabSelection = (tab: TabItem) => {
|
||||
selectedTab.value = tab
|
||||
}
|
||||
|
||||
const { searchQuery, pageNumber, sortField, isLoading, error, searchResults } =
|
||||
useRegistrySearch()
|
||||
pageNumber.value = 1
|
||||
const resultsWithKeys = computed(() =>
|
||||
searchResults.value.map((item) => ({
|
||||
...item,
|
||||
key: item.id || item.name
|
||||
}))
|
||||
)
|
||||
|
||||
const selectedNodePacks = ref<components['schemas']['Node'][]>([])
|
||||
const selectedNodePack = computed(() =>
|
||||
selectedNodePacks.value.length === 1 ? selectedNodePacks.value[0] : null
|
||||
)
|
||||
|
||||
const selectNodePack = (
|
||||
nodePack: components['schemas']['Node'],
|
||||
event: MouseEvent
|
||||
) => {
|
||||
// Handle multi-select with Shift or Ctrl/Cmd key
|
||||
if (event.shiftKey || event.ctrlKey || event.metaKey) {
|
||||
const index = selectedNodePacks.value.findIndex(
|
||||
(pack) => pack.id === nodePack.id
|
||||
)
|
||||
|
||||
if (index === -1) {
|
||||
// Add to selection if not already selected
|
||||
selectedNodePacks.value.push(nodePack)
|
||||
} else {
|
||||
// Remove from selection if already selected
|
||||
selectedNodePacks.value.splice(index, 1)
|
||||
}
|
||||
} else {
|
||||
// Single select behavior
|
||||
selectedNodePacks.value = [nodePack]
|
||||
}
|
||||
}
|
||||
|
||||
const unSelectItems = () => {
|
||||
selectedNodePacks.value = []
|
||||
}
|
||||
const handleGridContainerClick = (event: MouseEvent) => {
|
||||
const targetElement = event.target as HTMLElement
|
||||
if (targetElement && !targetElement.closest('[data-virtual-grid-item]')) {
|
||||
unSelectItems()
|
||||
}
|
||||
}
|
||||
|
||||
const showInfoPanel = computed(() => selectedNodePacks.value.length > 0)
|
||||
const hasMultipleSelections = computed(() => selectedNodePacks.value.length > 1)
|
||||
|
||||
const currentFilterBy = ref('all')
|
||||
const handleSortChange = (sortBy: NodeField) => {
|
||||
sortField.value = sortBy
|
||||
}
|
||||
const handleFilterChange = (filterBy: NodeField) => {
|
||||
currentFilterBy.value = filterBy
|
||||
}
|
||||
</script>
|
||||
14
src/components/dialog/content/manager/ManagerHeader.vue
Normal file
14
src/components/dialog/content/manager/ManagerHeader.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="px-6 py-4">
|
||||
<h2 class="text-lg font-normal text-left">
|
||||
{{ $t('manager.discoverCommunityContent') }}
|
||||
</h2>
|
||||
</div>
|
||||
<ContentDivider :width="0.3" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
</script>
|
||||
50
src/components/dialog/content/manager/ManagerNavSidebar.vue
Normal file
50
src/components/dialog/content/manager/ManagerNavSidebar.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<aside
|
||||
class="absolute translate-x-0 top-0 left-0 h-full w-80 shadow-md z-5 transition-transform duration-300 ease-in-out flex"
|
||||
>
|
||||
<ScrollPanel class="w-80 mt-7">
|
||||
<Listbox
|
||||
:model-value="selectedTab"
|
||||
:options="tabs"
|
||||
optionLabel="label"
|
||||
listStyle="max-height:unset"
|
||||
:pt="{
|
||||
root: { class: 'w-full border-0 bg-transparent' },
|
||||
list: { class: 'p-5' },
|
||||
option: { class: 'px-8 py-3 text-lg rounded-xl' },
|
||||
optionGroup: { class: 'p-0 text-left text-inherit' }
|
||||
}"
|
||||
@update:model-value="handleTabSelection"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="text-left flex items-center">
|
||||
<i :class="['pi', slotProps.option.icon, 'mr-3']"></i>
|
||||
<span class="text-lg">{{ slotProps.option.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Listbox>
|
||||
</ScrollPanel>
|
||||
<ContentDivider orientation="vertical" />
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Listbox from 'primevue/listbox'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import type { TabItem } from '@/types/comfyManagerTypes'
|
||||
|
||||
defineProps<{
|
||||
tabs: TabItem[]
|
||||
selectedTab: TabItem
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:selectedTab': [value: TabItem]
|
||||
}>()
|
||||
|
||||
const handleTabSelection = (tab: TabItem) => {
|
||||
emit('update:selectedTab', tab)
|
||||
}
|
||||
</script>
|
||||
24
src/components/dialog/content/manager/PackInstallButton.vue
Normal file
24
src/components/dialog/content/manager/PackInstallButton.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<Button
|
||||
outlined
|
||||
class="m-0 p-0 rounded-lg border-neutral-700"
|
||||
severity="secondary"
|
||||
:class="{
|
||||
'w-full': fullWidth,
|
||||
'w-min-content': !fullWidth
|
||||
}"
|
||||
>
|
||||
<span class="py-2.5 px-3">
|
||||
{{ multi ? $t('manager.installSelected') : $t('g.install') }}
|
||||
</span>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
defineProps<{
|
||||
fullWidth?: boolean
|
||||
multi?: boolean
|
||||
}>()
|
||||
</script>
|
||||
82
src/components/dialog/content/manager/PackStatusMessage.vue
Normal file
82
src/components/dialog/content/manager/PackStatusMessage.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<Message
|
||||
:severity="statusSeverity"
|
||||
class="p-0 flex items-center rounded-xl break-words w-fit"
|
||||
:pt="{
|
||||
text: { class: 'text-xs' },
|
||||
content: { class: 'px-2 py-0.5' }
|
||||
}"
|
||||
>
|
||||
<i
|
||||
class="pi pi-circle-fill mr-1.5 text-[0.6rem] p-0"
|
||||
:style="{ opacity: 0.8 }"
|
||||
></i>
|
||||
{{ $t(`manager.status.${statusLabel}`) }}
|
||||
</Message>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Message from 'primevue/message'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { VueSeverity } from '@/types/primeVueTypes'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
type PackVersionStatus = components['schemas']['NodeVersionStatus']
|
||||
type PackStatus = components['schemas']['NodeStatus']
|
||||
type Status = PackVersionStatus | PackStatus
|
||||
|
||||
type StatusProps = {
|
||||
label: string
|
||||
severity: VueSeverity
|
||||
}
|
||||
|
||||
const { statusType } = defineProps<{
|
||||
statusType: Status
|
||||
}>()
|
||||
|
||||
const statusPropsMap: Record<Status, StatusProps> = {
|
||||
NodeStatusActive: {
|
||||
label: 'active',
|
||||
severity: 'success'
|
||||
},
|
||||
NodeStatusDeleted: {
|
||||
label: 'deleted',
|
||||
severity: 'warn'
|
||||
},
|
||||
NodeStatusBanned: {
|
||||
label: 'banned',
|
||||
severity: 'danger'
|
||||
},
|
||||
NodeVersionStatusActive: {
|
||||
label: 'active',
|
||||
severity: 'success'
|
||||
},
|
||||
NodeVersionStatusPending: {
|
||||
label: 'pending',
|
||||
severity: 'warn'
|
||||
},
|
||||
NodeVersionStatusDeleted: {
|
||||
label: 'deleted',
|
||||
severity: 'warn'
|
||||
},
|
||||
NodeVersionStatusFlagged: {
|
||||
label: 'flagged',
|
||||
severity: 'danger'
|
||||
},
|
||||
NodeVersionStatusBanned: {
|
||||
label: 'banned',
|
||||
severity: 'danger'
|
||||
}
|
||||
}
|
||||
|
||||
const statusLabel = computed(
|
||||
() => statusPropsMap[statusType]?.label || 'unknown'
|
||||
)
|
||||
const statusSeverity = computed(
|
||||
() => statusPropsMap[statusType]?.severity || 'secondary'
|
||||
)
|
||||
</script>
|
||||
23
src/components/dialog/content/manager/PackVersionBadge.vue
Normal file
23
src/components/dialog/content/manager/PackVersionBadge.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<Button
|
||||
v-if="version"
|
||||
:label="version"
|
||||
severity="secondary"
|
||||
icon="pi pi-chevron-right"
|
||||
icon-pos="right"
|
||||
class="rounded-xl text-xs tracking-tighter"
|
||||
:pt="{
|
||||
root: { class: 'p-0' },
|
||||
label: { class: 'pl-2 pr-0 py-0.5' },
|
||||
icon: { class: 'text-xs pl-0 pr-2 py-0.5' }
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
defineProps<{
|
||||
version: string | undefined
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full z-40 hidden-scrollbar w-80">
|
||||
<div class="p-6 flex-1 overflow-hidden text-sm">
|
||||
<PackCardHeader
|
||||
:node-pack="nodePack"
|
||||
:install-button-full-width="false"
|
||||
/>
|
||||
<div class="mb-6">
|
||||
<MetadataRow
|
||||
v-for="item in infoItems"
|
||||
v-show="item.value !== undefined && item.value !== null"
|
||||
:key="item.key"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
<MetadataRow :label="t('g.status')">
|
||||
<PackStatusMessage
|
||||
:status-type="
|
||||
nodePack.status as components['schemas']['NodeVersionStatus']
|
||||
"
|
||||
/>
|
||||
</MetadataRow>
|
||||
<MetadataRow :label="t('manager.version')">
|
||||
<PackVersionBadge :version="nodePack.latest_version?.version" />
|
||||
</MetadataRow>
|
||||
</div>
|
||||
<div class="mb-6 overflow-hidden">
|
||||
<InfoTabs :node-pack="nodePack" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PackStatusMessage from '@/components/dialog/content/manager/PackStatusMessage.vue'
|
||||
import PackVersionBadge from '@/components/dialog/content/manager/PackVersionBadge.vue'
|
||||
import InfoTabs from '@/components/dialog/content/manager/infoPanel/InfoTabs.vue'
|
||||
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
|
||||
import PackCardHeader from '@/components/dialog/content/manager/packCard/PackCardHeader.vue'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
import { formatNumber } from '@/utils/formatUtil'
|
||||
|
||||
interface InfoItem {
|
||||
key: string
|
||||
label: string
|
||||
value: string | number | undefined
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const infoItems = computed<InfoItem[]>(() => [
|
||||
{
|
||||
key: 'publisher',
|
||||
label: `${t('manager.createdBy')}`,
|
||||
value: nodePack.publisher?.name
|
||||
},
|
||||
{
|
||||
key: 'downloads',
|
||||
label: t('manager.downloads'),
|
||||
value: nodePack.downloads ? formatNumber(nodePack.downloads) : undefined
|
||||
},
|
||||
{
|
||||
key: 'lastUpdated',
|
||||
label: t('manager.lastUpdated'),
|
||||
value: nodePack.latest_version?.createdAt
|
||||
? new Date(nodePack.latest_version.createdAt).toLocaleDateString()
|
||||
: undefined
|
||||
}
|
||||
])
|
||||
</script>
|
||||
<style scoped>
|
||||
.hidden-scrollbar {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
/* Firefox */
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="p-6 flex-1 overflow-auto">
|
||||
<PackCardHeader>
|
||||
<template #thumbnail>
|
||||
<PackIconStacked :node-packs="nodePacks" />
|
||||
</template>
|
||||
<template #title>
|
||||
{{ nodePacks.length }}
|
||||
{{ $t('manager.packsSelected') }}
|
||||
</template>
|
||||
<template #install-button>
|
||||
<PackInstallButton :full-width="true" :multi="true" />
|
||||
</template>
|
||||
</PackCardHeader>
|
||||
<div class="mb-6">
|
||||
<MetadataRow :label="$t('g.status')">
|
||||
<PackStatusMessage status-type="NodeVersionStatusActive" />
|
||||
</MetadataRow>
|
||||
<MetadataRow
|
||||
:label="$t('manager.totalNodes')"
|
||||
:value="totalNodesCount"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import PackInstallButton from '@/components/dialog/content/manager/PackInstallButton.vue'
|
||||
import PackStatusMessage from '@/components/dialog/content/manager/PackStatusMessage.vue'
|
||||
import MetadataRow from '@/components/dialog/content/manager/infoPanel/MetadataRow.vue'
|
||||
import PackCardHeader from '@/components/dialog/content/manager/packCard/PackCardHeader.vue'
|
||||
import PackIconStacked from '@/components/dialog/content/manager/packIcon/PackIconStacked.vue'
|
||||
import { useComfyRegistryService } from '@/services/comfyRegistryService'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePacks } = defineProps<{
|
||||
nodePacks: components['schemas']['Node'][]
|
||||
}>()
|
||||
|
||||
const comfyRegistryService = useComfyRegistryService()
|
||||
|
||||
const getPackNodes = async (pack: components['schemas']['Node']) => {
|
||||
if (!comfyRegistryService.packNodesAvailable(pack)) return []
|
||||
return comfyRegistryService.getNodeDefs({
|
||||
packId: pack.id,
|
||||
versionId: pack.latest_version.id
|
||||
})
|
||||
}
|
||||
|
||||
const { state: allNodeDefs } = useAsyncState(
|
||||
() => Promise.all(nodePacks.map(getPackNodes)),
|
||||
[],
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
const totalNodesCount = computed(() =>
|
||||
allNodeDefs.value.reduce(
|
||||
(total, nodeDefs) => total + (nodeDefs?.length || 0),
|
||||
0
|
||||
)
|
||||
)
|
||||
</script>
|
||||
37
src/components/dialog/content/manager/infoPanel/InfoTabs.vue
Normal file
37
src/components/dialog/content/manager/infoPanel/InfoTabs.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="overflow-hidden">
|
||||
<Tabs :value="activeTab">
|
||||
<TabList>
|
||||
<Tab value="description">{{ $t('g.description') }}</Tab>
|
||||
<Tab value="nodes">{{ $t('g.nodes') }}</Tab>
|
||||
</TabList>
|
||||
<TabPanels class="overflow-auto">
|
||||
<TabPanel value="description">
|
||||
<DescriptionTabPanel :node-pack="nodePack" />
|
||||
</TabPanel>
|
||||
<TabPanel value="nodes">
|
||||
<NodesTabPanel :node-pack="nodePack" />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Tab from 'primevue/tab'
|
||||
import TabList from 'primevue/tablist'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import TabPanels from 'primevue/tabpanels'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import DescriptionTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/DescriptionTabPanel.vue'
|
||||
import NodesTabPanel from '@/components/dialog/content/manager/infoPanel/tabs/NodesTabPanel.vue'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const activeTab = ref('description')
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 text-sm">
|
||||
<div v-for="(section, index) in sections" :key="index" class="mb-4">
|
||||
<div class="mb-1">{{ section.title }}</div>
|
||||
<div class="text-muted break-words">
|
||||
<a
|
||||
v-if="section.isUrl"
|
||||
:href="section.text"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center gap-2 hover:underline"
|
||||
>
|
||||
<i
|
||||
v-if="isGitHubLink(section.text)"
|
||||
class="pi pi-github text-base"
|
||||
></i>
|
||||
<span class="break-all">{{ section.text }}</span>
|
||||
</a>
|
||||
<MarkdownText v-else :text="section.text" class="text-muted" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MarkdownText from '@/components/dialog/content/manager/infoPanel/MarkdownText.vue'
|
||||
|
||||
export interface TextSection {
|
||||
title: string
|
||||
text: string
|
||||
isUrl?: boolean
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
sections: TextSection[]
|
||||
}>()
|
||||
|
||||
const isGitHubLink = (url: string): boolean => url.includes('github.com')
|
||||
</script>
|
||||
108
src/components/dialog/content/manager/infoPanel/MarkdownText.vue
Normal file
108
src/components/dialog/content/manager/infoPanel/MarkdownText.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="hover:underline">
|
||||
<div v-if="!hasMarkdown" v-text="text" class="break-words"></div>
|
||||
<div v-else class="break-words">
|
||||
<template v-for="(segment, index) in parsedSegments" :key="index">
|
||||
<a
|
||||
v-if="segment.type === 'link' && 'url' in segment"
|
||||
:href="segment.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="hover:underline"
|
||||
>
|
||||
<span class="text-blue-600">{{ segment.text }}</span>
|
||||
</a>
|
||||
<strong v-else-if="segment.type === 'bold'">{{ segment.text }}</strong>
|
||||
<em v-else-if="segment.type === 'italic'">{{ segment.text }}</em>
|
||||
<code
|
||||
v-else-if="segment.type === 'code'"
|
||||
class="bg-surface-100 px-1 py-0.5 rounded text-xs"
|
||||
>{{ segment.text }}</code
|
||||
>
|
||||
<span v-else>{{ segment.text }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { text } = defineProps<{
|
||||
text: string
|
||||
}>()
|
||||
|
||||
type MarkdownSegment = {
|
||||
type: 'text' | 'link' | 'bold' | 'italic' | 'code'
|
||||
text: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
const hasMarkdown = computed(() => {
|
||||
const hasMarkdown =
|
||||
/(\[.*?\]\(.*?\)|(\*\*|__)(.*?)(\*\*|__)|(\*|_)(.*?)(\*|_)|`(.*?)`)/.test(
|
||||
text
|
||||
)
|
||||
return hasMarkdown
|
||||
})
|
||||
|
||||
const parsedSegments = computed(() => {
|
||||
if (!hasMarkdown.value) return [{ type: 'text', text }]
|
||||
|
||||
const segments: MarkdownSegment[] = []
|
||||
const remainingText = text
|
||||
let lastIndex: number = 0
|
||||
|
||||
const linkRegex = /\[(.*?)\]\((.*?)\)/g
|
||||
let linkMatch: RegExpExecArray | null
|
||||
|
||||
while ((linkMatch = linkRegex.exec(remainingText)) !== null) {
|
||||
// Add text before the match
|
||||
if (linkMatch.index > lastIndex) {
|
||||
segments.push({
|
||||
type: 'text',
|
||||
text: remainingText.substring(lastIndex, linkMatch.index)
|
||||
})
|
||||
}
|
||||
|
||||
// Add the link
|
||||
segments.push({
|
||||
type: 'link',
|
||||
text: linkMatch[1],
|
||||
url: linkMatch[2]
|
||||
})
|
||||
|
||||
lastIndex = linkMatch.index + linkMatch[0].length
|
||||
}
|
||||
|
||||
// Add remaining text after all links
|
||||
if (lastIndex < remainingText.length) {
|
||||
let rest = remainingText.substring(lastIndex)
|
||||
|
||||
// Process bold text
|
||||
rest = rest.replace(/(\*\*|__)(.*?)(\*\*|__)/g, (_, __, p2) => {
|
||||
segments.push({ type: 'bold', text: p2 })
|
||||
return ''
|
||||
})
|
||||
|
||||
// Process italic text
|
||||
rest = rest.replace(/(\*|_)(.*?)(\*|_)/g, (_, __, p2) => {
|
||||
segments.push({ type: 'italic', text: p2 })
|
||||
return ''
|
||||
})
|
||||
|
||||
// Process code
|
||||
rest = rest.replace(/`(.*?)`/g, (_, p1) => {
|
||||
segments.push({ type: 'code', text: p1 })
|
||||
return ''
|
||||
})
|
||||
|
||||
// Add any remaining text
|
||||
if (rest) {
|
||||
segments.push({ type: 'text', text: rest })
|
||||
}
|
||||
}
|
||||
|
||||
return segments
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="flex py-1.5 text-xs">
|
||||
<div class="w-1/3 text-color-secondary truncate pr-2 text-muted">
|
||||
{{ label }}:
|
||||
</div>
|
||||
<div class="w-2/3">
|
||||
<slot>{{ value }}</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { value = 'N/A' } = defineProps<{
|
||||
label: string
|
||||
value?: string | number
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="mt-4 overflow-hidden">
|
||||
<InfoTextSection
|
||||
v-if="nodePack.description"
|
||||
:sections="descriptionSections"
|
||||
/>
|
||||
<p v-else class="text-muted italic text-sm">
|
||||
{{ $t('manager.noDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import InfoTextSection, {
|
||||
type TextSection
|
||||
} from '@/components/dialog/content/manager/infoPanel/InfoTextSection.vue'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const isValidUrl = (url: string): boolean => {
|
||||
try {
|
||||
new URL(url)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const isLicenseFile = (filename: string): boolean => {
|
||||
// Match LICENSE, LICENSE.md, LICENSE.txt (case insensitive)
|
||||
const licensePattern = /^license(\.md|\.txt)?$/i
|
||||
return licensePattern.test(filename)
|
||||
}
|
||||
|
||||
const extractBaseRepoUrl = (repoUrl: string): string => {
|
||||
// Match GitHub repository URL and extract the base URL
|
||||
const githubRepoPattern = /^(https?:\/\/github\.com\/[^/]+\/[^/]+)/i
|
||||
const match = repoUrl.match(githubRepoPattern)
|
||||
return match ? match[1] : repoUrl
|
||||
}
|
||||
|
||||
const createLicenseUrl = (filename: string, repoUrl: string): string => {
|
||||
if (!repoUrl || !filename) return ''
|
||||
|
||||
// Use the filename if it's a license file, otherwise use LICENSE
|
||||
const licenseFile = isLicenseFile(filename) ? filename : 'LICENSE'
|
||||
|
||||
// Get the base repository URL
|
||||
const baseRepoUrl = extractBaseRepoUrl(repoUrl)
|
||||
|
||||
return `${baseRepoUrl}/blob/main/${licenseFile}`
|
||||
}
|
||||
|
||||
const parseLicenseObject = (
|
||||
licenseObj: any
|
||||
): { text: string; isUrl: boolean } => {
|
||||
// Get the license file or text
|
||||
const licenseFile = licenseObj.file || licenseObj.text
|
||||
|
||||
// If it's a string and a license file, create a URL
|
||||
if (typeof licenseFile === 'string' && isLicenseFile(licenseFile)) {
|
||||
const url = createLicenseUrl(licenseFile, props.nodePack.repository)
|
||||
return {
|
||||
text: url,
|
||||
isUrl: !!url && isValidUrl(url)
|
||||
}
|
||||
}
|
||||
// Otherwise use the text directly
|
||||
else if (licenseObj.text) {
|
||||
return {
|
||||
text: licenseObj.text,
|
||||
isUrl: false
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return {
|
||||
text: JSON.stringify(licenseObj),
|
||||
isUrl: false
|
||||
}
|
||||
}
|
||||
|
||||
const formatLicense = (license: string): { text: string; isUrl: boolean } => {
|
||||
try {
|
||||
const licenseObj = JSON.parse(license)
|
||||
return parseLicenseObject(licenseObj)
|
||||
} catch (e) {
|
||||
// Not JSON, handle as plain string
|
||||
// If it's a license file, create a URL
|
||||
if (isLicenseFile(license)) {
|
||||
const url = createLicenseUrl(license, props.nodePack.repository)
|
||||
return {
|
||||
text: url,
|
||||
isUrl: !!url && isValidUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise return as is
|
||||
return {
|
||||
text: license,
|
||||
isUrl: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const descriptionSections = computed<TextSection[]>(() => {
|
||||
const sections: TextSection[] = [
|
||||
{
|
||||
title: t('g.description'),
|
||||
text: props.nodePack.description || t('manager.noDescription')
|
||||
}
|
||||
]
|
||||
|
||||
if (props.nodePack.repository) {
|
||||
sections.push({
|
||||
title: t('manager.repository'),
|
||||
text: props.nodePack.repository,
|
||||
isUrl: isValidUrl(props.nodePack.repository)
|
||||
})
|
||||
}
|
||||
|
||||
if (props.nodePack.license) {
|
||||
const { text, isUrl } = formatLicense(props.nodePack.license)
|
||||
if (text) {
|
||||
sections.push({
|
||||
title: t('manager.license'),
|
||||
text,
|
||||
isUrl
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return sections
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 mt-4 overflow-auto text-sm">
|
||||
<div v-if="nodeDefs?.length">
|
||||
<!-- TODO: when registry returns node defs, use them here -->
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
v-for="i in 3"
|
||||
:key="i"
|
||||
class="border border-surface-border rounded-lg p-4"
|
||||
>
|
||||
<NodePreview :node-def="placeholderNodeDef" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import NodePreview from '@/components/node/NodePreview.vue'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
nodeDefs?: components['schemas']['ComfyNode'][]
|
||||
}>()
|
||||
|
||||
// TODO: when registry returns node defs, use them here
|
||||
const placeholderNodeDef = {
|
||||
display_name: 'Sample Node',
|
||||
description: 'This is a sample node for preview purposes',
|
||||
inputs: {
|
||||
input1: { name: 'Input 1', type: 'IMAGE' },
|
||||
input2: { name: 'Input 2', type: 'CONDITIONING' }
|
||||
},
|
||||
outputs: [
|
||||
{ name: 'Output 1', type: 'IMAGE', index: 0 },
|
||||
{ name: 'Output 2', type: 'MASK', index: 1 }
|
||||
]
|
||||
}
|
||||
</script>
|
||||
103
src/components/dialog/content/manager/packCard/PackCard.vue
Normal file
103
src/components/dialog/content/manager/packCard/PackCard.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<Card
|
||||
class="absolute inset-0 flex flex-col overflow-hidden rounded-2xl shadow-[0_0_15px_rgba(0,0,0,0.15),0_10px_15px_-3px_rgba(0,0,0,0.12),0_4px_6px_-4px_rgba(0,0,0,0.08)] transition-all duration-200"
|
||||
:class="{
|
||||
'bg-[#ffffff08]': !isLightTheme,
|
||||
'outline outline-[6px] outline-[var(--p-primary-color)]': isSelected
|
||||
}"
|
||||
:pt="{
|
||||
body: { class: 'p-0 flex flex-col h-full rounded-2xl' },
|
||||
content: { class: 'flex-1 flex flex-col rounded-2xl' },
|
||||
title: { class: 'p-0 m-0' },
|
||||
footer: { class: 'p-0 m-0' }
|
||||
}"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex justify-between p-5 pb-1 align-middle text-sm">
|
||||
<span class="flex items-start mt-2">
|
||||
<i
|
||||
class="pi pi-box text-muted text-2xl ml-1 mr-5"
|
||||
style="opacity: 0.5"
|
||||
></i>
|
||||
<span class="text-lg relative top-[.25rem]">{{
|
||||
$t('manager.nodePack')
|
||||
}}</span>
|
||||
</span>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div
|
||||
v-if="nodePack.downloads"
|
||||
class="flex items-center text-sm text-muted tracking-tighter"
|
||||
>
|
||||
<i class="pi pi-download mr-2"></i>
|
||||
{{ formatNumber(nodePack.downloads) }}
|
||||
</div>
|
||||
<PackInstallButton />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<ContentDivider />
|
||||
<div class="flex flex-1 p-5 mt-3 cursor-pointer">
|
||||
<div class="flex-shrink-0 mr-4">
|
||||
<PackIcon :node-pack="nodePack" />
|
||||
</div>
|
||||
<div class="flex flex-col flex-1 min-w-0">
|
||||
<span
|
||||
class="text-lg font-bold pb-4 truncate overflow-hidden text-ellipsis"
|
||||
:title="nodePack.name"
|
||||
>
|
||||
{{ nodePack.name }}
|
||||
</span>
|
||||
<div class="flex-1">
|
||||
<p
|
||||
v-if="nodePack.description"
|
||||
class="text-sm text-color-secondary m-0 line-clamp-3 overflow-hidden"
|
||||
:title="nodePack.description"
|
||||
>
|
||||
{{ nodePack.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<ContentDivider :width="0.1" />
|
||||
<div class="flex justify-between p-5 text-xs text-muted">
|
||||
<div class="flex items-center gap-2 cursor-pointer">
|
||||
<span v-if="nodePack.publisher?.name">
|
||||
{{ nodePack.publisher.name }}
|
||||
</span>
|
||||
<span v-if="nodePack.latest_version">
|
||||
{{ nodePack.latest_version.version }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="nodePack.latest_version" class="flex items-center gap-2">
|
||||
{{ $t('g.updated') }}
|
||||
{{ new Date(nodePack.latest_version.createdAt).toLocaleDateString() }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Card from 'primevue/card'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/PackInstallButton.vue'
|
||||
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { formatNumber } from '@/utils/formatUtil'
|
||||
|
||||
defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
isSelected?: boolean
|
||||
}>()
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const isLightTheme = computed(
|
||||
() => colorPaletteStore.completedActivePalette.light_theme
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center mb-6">
|
||||
<slot name="thumbnail">
|
||||
<PackIcon :node-pack="nodePack" width="24" height="24" />
|
||||
</slot>
|
||||
<h2
|
||||
class="text-2xl font-bold text-center mt-4 mb-2"
|
||||
style="word-break: break-all"
|
||||
>
|
||||
<slot name="title">{{ nodePack?.name }}</slot>
|
||||
</h2>
|
||||
<div class="mt-2 mb-4 w-full max-w-xs flex justify-center">
|
||||
<slot name="install-button">
|
||||
<PackInstallButton
|
||||
:full-width="installButtonFullWidth"
|
||||
:multi="multi"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PackInstallButton from '@/components/dialog/content/manager/PackInstallButton.vue'
|
||||
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
defineProps<{
|
||||
nodePack?: components['schemas']['Node']
|
||||
multi?: boolean
|
||||
installButtonFullWidth?: boolean
|
||||
}>()
|
||||
</script>
|
||||
41
src/components/dialog/content/manager/packIcon/PackIcon.vue
Normal file
41
src/components/dialog/content/manager/packIcon/PackIcon.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<img
|
||||
:src="isImageError ? DEFAULT_ICON : imgSrc"
|
||||
:alt="nodePack.name + ' icon'"
|
||||
class="object-contain rounded-lg"
|
||||
:style="{ width: cssWidth, height: cssHeight }"
|
||||
@error="isImageError = true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const DEFAULT_ICON = '/assets/images/fallback-gradient-avatar.svg'
|
||||
|
||||
const {
|
||||
nodePack,
|
||||
width = '4.5rem',
|
||||
height = '4.5rem'
|
||||
} = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
width?: string
|
||||
height?: string
|
||||
}>()
|
||||
|
||||
const isImageError = ref(false)
|
||||
const shouldShowFallback = computed(
|
||||
() => !nodePack.icon || nodePack.icon.trim() === '' || isImageError.value
|
||||
)
|
||||
const imgSrc = computed(() =>
|
||||
shouldShowFallback.value ? DEFAULT_ICON : nodePack.icon
|
||||
)
|
||||
|
||||
const convertToCssValue = (value: string | number) =>
|
||||
typeof value === 'number' ? `${value}rem` : value
|
||||
|
||||
const cssWidth = computed(() => convertToCssValue(width))
|
||||
const cssHeight = computed(() => convertToCssValue(height))
|
||||
</script>
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="relative w-24 h-24">
|
||||
<div
|
||||
v-for="(pack, index) in nodePacks.slice(0, maxVisible)"
|
||||
:key="pack.id"
|
||||
class="absolute"
|
||||
:style="{
|
||||
bottom: `${index * offset}px`,
|
||||
right: `${index * offset}px`,
|
||||
zIndex: maxVisible - index
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="border border-surface-border bg-surface-card rounded-lg p-0.5"
|
||||
>
|
||||
<PackIcon :node-pack="pack" width="4.5rem" height="4.5rem" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="nodePacks.length > maxVisible"
|
||||
class="absolute -top-2 -right-2 bg-primary rounded-full w-7 h-7 flex items-center justify-center text-xs font-bold shadow-md z-10"
|
||||
>
|
||||
+{{ nodePacks.length - maxVisible }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const {
|
||||
nodePacks,
|
||||
maxVisible = 3,
|
||||
offset = 8
|
||||
} = defineProps<{
|
||||
nodePacks: components['schemas']['Node'][]
|
||||
maxVisible?: number
|
||||
offset?: number
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="relative w-full p-6">
|
||||
<div class="flex items-center w-full">
|
||||
<IconField class="w-5/12">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText
|
||||
:model-value="searchQuery"
|
||||
@update:model-value="$emit('update:searchQuery', $event)"
|
||||
:placeholder="$t('manager.searchPlaceholder')"
|
||||
class="w-full rounded-2xl"
|
||||
autofocus
|
||||
/>
|
||||
</IconField>
|
||||
</div>
|
||||
<div class="flex mt-3 text-sm">
|
||||
<div class="flex gap-6 ml-1">
|
||||
<SearchFilterDropdown
|
||||
v-model="currentFilter"
|
||||
:options="filterOptions"
|
||||
:label="$t('g.filter')"
|
||||
@update:model-value="handleFilterChange"
|
||||
/>
|
||||
<SearchFilterDropdown
|
||||
v-model="currentSort"
|
||||
:options="sortOptions"
|
||||
:label="$t('g.sort')"
|
||||
@update:model-value="handleSortChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 ml-6">
|
||||
<small v-if="hasResults" class="text-color-secondary">
|
||||
{{ $t('g.resultsCount', { count: searchResults.length }) }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SearchFilterDropdown from '@/components/dialog/content/manager/registrySearchBar/SearchFilterDropdown.vue'
|
||||
import type { NodeField, SearchOption } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const DEFAULT_SORT: NodeField = 'downloads'
|
||||
const DEFAULT_FILTER = 'nodePack'
|
||||
|
||||
const props = defineProps<{
|
||||
searchQuery: string
|
||||
searchResults?: components['schemas']['Node'][]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const currentSort = ref<NodeField>(DEFAULT_SORT)
|
||||
const currentFilter = ref<string>(DEFAULT_FILTER)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:searchQuery': [value: string]
|
||||
'update:sortBy': [value: NodeField]
|
||||
'update:filterBy': [value: string]
|
||||
}>()
|
||||
|
||||
const hasResults = computed(
|
||||
() => props.searchQuery.trim() && props.searchResults?.length
|
||||
)
|
||||
|
||||
const sortOptions: SearchOption<NodeField>[] = [
|
||||
{ id: 'downloads', label: t('manager.sort.downloads') },
|
||||
{ id: 'name', label: t('g.name') },
|
||||
{ id: 'rating', label: t('manager.sort.rating') },
|
||||
{ id: 'category', label: t('g.category') }
|
||||
]
|
||||
const filterOptions: SearchOption<string>[] = [
|
||||
{ id: 'nodePack', label: t('manager.filter.nodePack') },
|
||||
{ id: 'node', label: t('g.nodes') }
|
||||
]
|
||||
|
||||
const handleSortChange = () => {
|
||||
// TODO: emit to Algolia service
|
||||
emit('update:sortBy', currentSort.value)
|
||||
}
|
||||
const handleFilterChange = () => {
|
||||
// TODO: emit to Algolia service
|
||||
emit('update:filterBy', currentFilter.value)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-muted">{{ label }}:</span>
|
||||
<Dropdown
|
||||
v-model="selectedValue"
|
||||
:options="options"
|
||||
optionLabel="label"
|
||||
optionValue="id"
|
||||
class="min-w-[6rem]"
|
||||
@change="handleChange"
|
||||
:pt="{
|
||||
root: { class: 'border-none' },
|
||||
input: { class: 'py-0 px-1 border-none' },
|
||||
trigger: { class: 'hidden' },
|
||||
panel: { class: 'shadow-md' },
|
||||
item: { class: 'py-2 px-3 text-sm' }
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T">
|
||||
import Dropdown from 'primevue/dropdown'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SearchOption } from '@/types/comfyManagerTypes'
|
||||
|
||||
const { modelValue, options, label } = defineProps<{
|
||||
modelValue: T
|
||||
options: SearchOption<T>[]
|
||||
label: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: T]
|
||||
}>()
|
||||
|
||||
const selectedValue = computed({
|
||||
get: () => modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const handleChange = () => {
|
||||
emit('update:modelValue', selectedValue.value)
|
||||
}
|
||||
</script>
|
||||
@@ -581,6 +581,15 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
app.canvas.deleteSelected()
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.CustomNodesManager',
|
||||
icon: 'pi pi-puzzle',
|
||||
label: 'Custom Nodes Manager',
|
||||
versionAdded: '1.12.10',
|
||||
function: () => {
|
||||
dialogService.showManagerDialog()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const SEARCH_DEBOUNCE_TIME = 256
|
||||
const DEFAULT_PAGE_SIZE = 60
|
||||
const DEFAULT_SORT_FIELD: keyof components['schemas']['Node'] = 'downloads'
|
||||
|
||||
/**
|
||||
* Composable for managing UI state of Comfy Node Registry search.
|
||||
@@ -18,6 +19,7 @@ export function useRegistrySearch() {
|
||||
const searchQuery = ref('')
|
||||
const pageNumber = ref(1)
|
||||
const pageSize = ref(DEFAULT_PAGE_SIZE)
|
||||
const sortField = ref<keyof components['schemas']['Node']>(DEFAULT_SORT_FIELD)
|
||||
const searchResults = ref<components['schemas']['Node'][]>([])
|
||||
|
||||
const search = async () => {
|
||||
@@ -26,7 +28,8 @@ export function useRegistrySearch() {
|
||||
const result = isEmptySearch
|
||||
? await registryStore.listAllPacks({
|
||||
page: pageNumber.value,
|
||||
limit: pageSize.value
|
||||
limit: pageSize.value,
|
||||
sort: [sortField.value]
|
||||
})
|
||||
: await registryService.search({
|
||||
search: searchQuery.value,
|
||||
@@ -45,12 +48,12 @@ export function useRegistrySearch() {
|
||||
}
|
||||
}
|
||||
|
||||
// Debounce search when query changes
|
||||
const debouncedSearch = debounce(search, SEARCH_DEBOUNCE_TIME)
|
||||
|
||||
// Debounce search when query changes
|
||||
watch(() => searchQuery.value, debouncedSearch)
|
||||
|
||||
// Normal search when page number changes and on load
|
||||
watch(() => pageNumber.value, search, { immediate: true })
|
||||
watch(() => [pageNumber.value, sortField.value], search, { immediate: true })
|
||||
|
||||
onUnmounted(() => {
|
||||
debouncedSearch.cancel() // Cancel debounced searches
|
||||
@@ -61,6 +64,7 @@ export function useRegistrySearch() {
|
||||
return {
|
||||
pageNumber,
|
||||
pageSize,
|
||||
sortField,
|
||||
searchQuery,
|
||||
searchResults,
|
||||
isLoading: registryService.isLoading,
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Load Default Workflow"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Custom Nodes Manager"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "New Blank Workflow"
|
||||
},
|
||||
|
||||
@@ -78,7 +78,55 @@
|
||||
"control_after_generate": "control after generate",
|
||||
"control_before_generate": "control before generate",
|
||||
"choose_file_to_upload": "choose file to upload",
|
||||
"capture": "capture"
|
||||
"capture": "capture",
|
||||
"nodes": "Nodes",
|
||||
"community": "Community",
|
||||
"all": "All",
|
||||
"updated": "Updated",
|
||||
"resultsCount": "Found {count} Results",
|
||||
"status": "Status",
|
||||
"description": "Description",
|
||||
"name": "Name",
|
||||
"category": "Category",
|
||||
"sort": "Sort",
|
||||
"filter": "Filter"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
"downloads": "Downloads",
|
||||
"repository": "Repository",
|
||||
"license": "License",
|
||||
"createdBy": "Created By",
|
||||
"totalNodes": "Total Nodes",
|
||||
"discoverCommunityContent": "Discover community-made Node Packs, Extensions, and more...",
|
||||
"errorConnecting": "Error connecting to the Comfy Node Registry.",
|
||||
"noResultsFound": "No results found matching your search.",
|
||||
"tryDifferentSearch": "Please try a different search query.",
|
||||
"tryAgainLater": "Please try again later.",
|
||||
"nodePack": "Node Pack",
|
||||
"searchPlaceholder": "Search",
|
||||
"version": "Version",
|
||||
"lastUpdated": "Last Updated",
|
||||
"noDescription": "No description available",
|
||||
"installSelected": "Install Selected",
|
||||
"packsSelected": "Packs Selected",
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"pending": "Pending",
|
||||
"flagged": "Flagged",
|
||||
"deleted": "Deleted",
|
||||
"banned": "Banned",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"sort": {
|
||||
"rating": "Rating",
|
||||
"downloads": "Most Popular"
|
||||
},
|
||||
"filter": {
|
||||
"nodePack": "Node Pack",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
}
|
||||
},
|
||||
"issueReport": {
|
||||
"submitErrorReport": "Submit Error Report (Optional)",
|
||||
@@ -512,6 +560,7 @@
|
||||
"ComfyUI Issues": "ComfyUI Issues",
|
||||
"Interrupt": "Interrupt",
|
||||
"Load Default Workflow": "Load Default Workflow",
|
||||
"Custom Nodes Manager": "Custom Nodes Manager",
|
||||
"New": "New",
|
||||
"Clipspace": "Clipspace",
|
||||
"Open": "Open",
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Charger le flux de travail par défaut"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Gestionnaire de Nœuds Personnalisés"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Nouveau flux de travail vierge"
|
||||
},
|
||||
|
||||
@@ -116,14 +116,17 @@
|
||||
"g": {
|
||||
"about": "À propos",
|
||||
"add": "Ajouter",
|
||||
"all": "Tout",
|
||||
"back": "Retour",
|
||||
"cancel": "Annuler",
|
||||
"capture": "capture",
|
||||
"category": "Catégorie",
|
||||
"choose_file_to_upload": "choisissez le fichier à télécharger",
|
||||
"close": "Fermer",
|
||||
"color": "Couleur",
|
||||
"comingSoon": "Bientôt disponible",
|
||||
"command": "Commande",
|
||||
"community": "Communauté",
|
||||
"confirm": "Confirmer",
|
||||
"continue": "Continuer",
|
||||
"control_after_generate": "contrôle après génération",
|
||||
@@ -134,6 +137,7 @@
|
||||
"customizeFolder": "Personnaliser le dossier",
|
||||
"delete": "Supprimer",
|
||||
"deprecated": "DEPR",
|
||||
"description": "Description",
|
||||
"devices": "Appareils",
|
||||
"disableAll": "Désactiver tout",
|
||||
"download": "Télécharger",
|
||||
@@ -144,6 +148,7 @@
|
||||
"export": "Exportation",
|
||||
"extensionName": "Nom de l'extension",
|
||||
"feedback": "Commentaires",
|
||||
"filter": "Filtrer",
|
||||
"findIssues": "Trouver des problèmes",
|
||||
"firstTimeUIMessage": "C'est la première fois que vous utilisez la nouvelle interface utilisateur. Choisissez \"Menu > Utiliser le nouveau menu > Désactivé\" pour restaurer l'ancienne interface utilisateur.",
|
||||
"goToNode": "Aller au nœud",
|
||||
@@ -157,6 +162,7 @@
|
||||
"loadWorkflow": "Charger le flux de travail",
|
||||
"loading": "Chargement",
|
||||
"logs": "Journaux",
|
||||
"name": "Nom",
|
||||
"newFolder": "Nouveau dossier",
|
||||
"next": "Suivant",
|
||||
"no": "Non",
|
||||
@@ -164,6 +170,7 @@
|
||||
"noTasksFound": "Aucune tâche trouvée",
|
||||
"noTasksFoundMessage": "Il n'y a pas de tâches dans la file d'attente.",
|
||||
"noWorkflowsFound": "Aucun flux de travail trouvé.",
|
||||
"nodes": "Nœuds",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "Ouvrir un nouveau problème",
|
||||
"overwrite": "Écraser",
|
||||
@@ -177,6 +184,7 @@
|
||||
"reportSent": "Rapport soumis",
|
||||
"reset": "Réinitialiser",
|
||||
"resetKeybindingsTooltip": "Réinitialiser les raccourcis clavier par défaut",
|
||||
"resultsCount": "{count} Résultats Trouvés",
|
||||
"save": "Enregistrer",
|
||||
"searchExtensions": "Rechercher des extensions",
|
||||
"searchFailedMessage": "Nous n'avons trouvé aucun paramètre correspondant à votre recherche. Essayez d'ajuster vos termes de recherche.",
|
||||
@@ -187,9 +195,12 @@
|
||||
"searchWorkflows": "Rechercher des flux de travail",
|
||||
"settings": "Paramètres",
|
||||
"showReport": "Afficher le rapport",
|
||||
"sort": "Trier",
|
||||
"status": "Statut",
|
||||
"success": "Succès",
|
||||
"systemInfo": "Informations système",
|
||||
"terminal": "Terminal",
|
||||
"updated": "Mis à jour",
|
||||
"upload": "Téléverser",
|
||||
"videoFailedToLoad": "Échec du chargement de la vidéo",
|
||||
"workflow": "Flux de travail"
|
||||
@@ -359,6 +370,43 @@
|
||||
"terminalDefaultMessage": "Lorsque vous exécutez une commande de dépannage, toute sortie sera affichée ici.",
|
||||
"title": "Maintenance"
|
||||
},
|
||||
"manager": {
|
||||
"createdBy": "Créé par",
|
||||
"discoverCommunityContent": "Découvrez les packs de nœuds, extensions et plus encore créés par la communauté...",
|
||||
"downloads": "Téléchargements",
|
||||
"errorConnecting": "Erreur de connexion au registre de nœuds Comfy.",
|
||||
"filter": {
|
||||
"disabled": "Désactivé",
|
||||
"enabled": "Activé",
|
||||
"nodePack": "Pack de Nœuds"
|
||||
},
|
||||
"installSelected": "Installer sélectionné",
|
||||
"lastUpdated": "Dernière mise à jour",
|
||||
"license": "Licence",
|
||||
"noDescription": "Aucune description disponible",
|
||||
"noResultsFound": "Aucun résultat trouvé correspondant à votre recherche.",
|
||||
"nodePack": "Pack de Nœuds",
|
||||
"packsSelected": "Packs sélectionnés",
|
||||
"repository": "Référentiel",
|
||||
"searchPlaceholder": "Recherche",
|
||||
"sort": {
|
||||
"downloads": "Le plus populaire",
|
||||
"rating": "Évaluation"
|
||||
},
|
||||
"status": {
|
||||
"active": "Actif",
|
||||
"banned": "Banni",
|
||||
"deleted": "Supprimé",
|
||||
"flagged": "Signalé",
|
||||
"pending": "En attente",
|
||||
"unknown": "Inconnu"
|
||||
},
|
||||
"title": "Gestionnaire de Nœuds Personnalisés",
|
||||
"totalNodes": "Total de Nœuds",
|
||||
"tryAgainLater": "Veuillez réessayer plus tard.",
|
||||
"tryDifferentSearch": "Veuillez essayer une autre requête de recherche.",
|
||||
"version": "Version"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "File d'attente automatique",
|
||||
"batchCount": "Nombre de lots",
|
||||
@@ -398,6 +446,7 @@
|
||||
"ComfyUI Forum": "Forum ComfyUI",
|
||||
"ComfyUI Issues": "Problèmes de ComfyUI",
|
||||
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
|
||||
"Custom Nodes Manager": "Gestionnaire de Nœuds Personnalisés",
|
||||
"Delete Selected Items": "Supprimer les éléments sélectionnés",
|
||||
"Desktop User Guide": "Guide de l'utilisateur de bureau",
|
||||
"Duplicate Current Workflow": "Dupliquer le flux de travail actuel",
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "デフォルトのワークフローを読み込む"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "カスタムノードマネージャ"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新しい空のワークフロー"
|
||||
},
|
||||
|
||||
@@ -116,14 +116,17 @@
|
||||
"g": {
|
||||
"about": "情報",
|
||||
"add": "追加",
|
||||
"all": "すべて",
|
||||
"back": "戻る",
|
||||
"cancel": "キャンセル",
|
||||
"capture": "キャプチャ",
|
||||
"category": "カテゴリ",
|
||||
"choose_file_to_upload": "アップロードするファイルを選択",
|
||||
"close": "閉じる",
|
||||
"color": "色",
|
||||
"comingSoon": "近日公開",
|
||||
"command": "コマンド",
|
||||
"community": "コミュニティ",
|
||||
"confirm": "確認",
|
||||
"continue": "続ける",
|
||||
"control_after_generate": "生成後の制御",
|
||||
@@ -134,6 +137,7 @@
|
||||
"customizeFolder": "フォルダーをカスタマイズ",
|
||||
"delete": "削除",
|
||||
"deprecated": "非推奨",
|
||||
"description": "説明",
|
||||
"devices": "デバイス",
|
||||
"disableAll": "すべて無効にする",
|
||||
"download": "ダウンロード",
|
||||
@@ -144,6 +148,7 @@
|
||||
"export": "エクスポート",
|
||||
"extensionName": "拡張機能名",
|
||||
"feedback": "フィードバック",
|
||||
"filter": "フィルタ",
|
||||
"findIssues": "問題を見つける",
|
||||
"firstTimeUIMessage": "新しいUIを初めて使用しています。「メニュー > 新しいメニューを使用 > 無効」を選択することで古いUIに戻すことが可能です。",
|
||||
"goToNode": "ノードに移動",
|
||||
@@ -157,6 +162,7 @@
|
||||
"loadWorkflow": "ワークフローを読み込む",
|
||||
"loading": "読み込み中",
|
||||
"logs": "ログ",
|
||||
"name": "名前",
|
||||
"newFolder": "新しいフォルダー",
|
||||
"next": "次へ",
|
||||
"no": "いいえ",
|
||||
@@ -164,6 +170,7 @@
|
||||
"noTasksFound": "タスクが見つかりません",
|
||||
"noTasksFoundMessage": "キューにタスクがありません。",
|
||||
"noWorkflowsFound": "ワークフローが見つかりません。",
|
||||
"nodes": "ノード",
|
||||
"ok": "OK",
|
||||
"openNewIssue": "新しい問題を開く",
|
||||
"overwrite": "上書き",
|
||||
@@ -177,6 +184,7 @@
|
||||
"reportSent": "レポートが送信されました",
|
||||
"reset": "リセット",
|
||||
"resetKeybindingsTooltip": "キーバインディングをデフォルトにリセット",
|
||||
"resultsCount": "{count}件の結果が見つかりました",
|
||||
"save": "保存",
|
||||
"searchExtensions": "拡張機能を検索",
|
||||
"searchFailedMessage": "検索に一致する設定が見つかりませんでした。検索キーワードを調整してみてください。",
|
||||
@@ -187,9 +195,12 @@
|
||||
"searchWorkflows": "ワークフローを検索",
|
||||
"settings": "設定",
|
||||
"showReport": "レポートを表示",
|
||||
"sort": "並び替え",
|
||||
"status": "ステータス",
|
||||
"success": "成功",
|
||||
"systemInfo": "システム情報",
|
||||
"terminal": "ターミナル",
|
||||
"updated": "更新済み",
|
||||
"upload": "アップロード",
|
||||
"videoFailedToLoad": "ビデオの読み込みに失敗しました",
|
||||
"workflow": "ワークフロー"
|
||||
@@ -359,6 +370,43 @@
|
||||
"terminalDefaultMessage": "トラブルシューティングコマンドを実行すると、出力はここに表示されます。",
|
||||
"title": "メンテナンス"
|
||||
},
|
||||
"manager": {
|
||||
"createdBy": "作成者",
|
||||
"discoverCommunityContent": "コミュニティが作成したノードパック、拡張機能などを探す...",
|
||||
"downloads": "ダウンロード",
|
||||
"errorConnecting": "Comfy Node Registryへの接続エラー。",
|
||||
"filter": {
|
||||
"disabled": "無効",
|
||||
"enabled": "有効",
|
||||
"nodePack": "ノードパック"
|
||||
},
|
||||
"installSelected": "選択したものをインストール",
|
||||
"lastUpdated": "最終更新日",
|
||||
"license": "ライセンス",
|
||||
"noDescription": "説明はありません",
|
||||
"noResultsFound": "検索に一致する結果が見つかりませんでした。",
|
||||
"nodePack": "ノードパック",
|
||||
"packsSelected": "選択したパック",
|
||||
"repository": "リポジトリ",
|
||||
"searchPlaceholder": "検索",
|
||||
"sort": {
|
||||
"downloads": "最も人気",
|
||||
"rating": "評価"
|
||||
},
|
||||
"status": {
|
||||
"active": "アクティブ",
|
||||
"banned": "禁止",
|
||||
"deleted": "削除済み",
|
||||
"flagged": "フラグ付き",
|
||||
"pending": "保留中",
|
||||
"unknown": "不明"
|
||||
},
|
||||
"title": "カスタムノードマネージャ",
|
||||
"totalNodes": "合計ノード数",
|
||||
"tryAgainLater": "後ほど再試行してください。",
|
||||
"tryDifferentSearch": "別の検索クエリを試してみてください。",
|
||||
"version": "バージョン"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "自動キュー",
|
||||
"batchCount": "バッチ数",
|
||||
@@ -398,6 +446,7 @@
|
||||
"ComfyUI Forum": "ComfyUI フォーラム",
|
||||
"ComfyUI Issues": "ComfyUIの問題",
|
||||
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
|
||||
"Custom Nodes Manager": "カスタムノードマネージャ",
|
||||
"Delete Selected Items": "選択したアイテムを削除",
|
||||
"Desktop User Guide": "デスクトップユーザーガイド",
|
||||
"Duplicate Current Workflow": "現在のワークフローを複製",
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "기본 워크플로 로드"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "사용자 정의 노드 관리자"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "새로운 빈 워크플로"
|
||||
},
|
||||
|
||||
@@ -116,14 +116,17 @@
|
||||
"g": {
|
||||
"about": "정보",
|
||||
"add": "추가",
|
||||
"all": "모두",
|
||||
"back": "뒤로",
|
||||
"cancel": "취소",
|
||||
"capture": "캡처",
|
||||
"category": "카테고리",
|
||||
"choose_file_to_upload": "업로드할 파일 선택",
|
||||
"close": "닫기",
|
||||
"color": "색상",
|
||||
"comingSoon": "곧 출시 예정",
|
||||
"command": "명령",
|
||||
"community": "커뮤니티",
|
||||
"confirm": "확인",
|
||||
"continue": "계속",
|
||||
"control_after_generate": "생성 후 제어",
|
||||
@@ -134,6 +137,7 @@
|
||||
"customizeFolder": "폴더 사용자 정의",
|
||||
"delete": "삭제",
|
||||
"deprecated": "사용 중단",
|
||||
"description": "설명",
|
||||
"devices": "장치",
|
||||
"disableAll": "모두 비활성화",
|
||||
"download": "다운로드",
|
||||
@@ -144,6 +148,7 @@
|
||||
"export": "내보내기",
|
||||
"extensionName": "확장 이름",
|
||||
"feedback": "피드백",
|
||||
"filter": "필터",
|
||||
"findIssues": "문제 찾기",
|
||||
"firstTimeUIMessage": "새 UI를 처음 사용합니다. \"메뉴 > 새 메뉴 사용 > 비활성화\"를 선택하여 이전 UI로 복원하세요.",
|
||||
"goToNode": "노드로 이동",
|
||||
@@ -157,6 +162,7 @@
|
||||
"loadWorkflow": "워크플로 로드",
|
||||
"loading": "로딩 중",
|
||||
"logs": "로그",
|
||||
"name": "이름",
|
||||
"newFolder": "새 폴더",
|
||||
"next": "다음",
|
||||
"no": "아니오",
|
||||
@@ -164,6 +170,7 @@
|
||||
"noTasksFound": "작업을 찾을 수 없습니다.",
|
||||
"noTasksFoundMessage": "대기열에 작업이 없습니다.",
|
||||
"noWorkflowsFound": "워크플로를 찾을 수 없습니다.",
|
||||
"nodes": "노드",
|
||||
"ok": "확인",
|
||||
"openNewIssue": "새 문제 열기",
|
||||
"overwrite": "덮어쓰기",
|
||||
@@ -177,6 +184,7 @@
|
||||
"reportSent": "보고서 제출됨",
|
||||
"reset": "재설정",
|
||||
"resetKeybindingsTooltip": "키 바인딩을 기본값으로 재설정",
|
||||
"resultsCount": "{count} 개의 결과를 찾았습니다",
|
||||
"save": "저장",
|
||||
"searchExtensions": "확장 검색",
|
||||
"searchFailedMessage": "검색어와 일치하는 설정을 찾을 수 없습니다. 검색어를 조정해 보세요.",
|
||||
@@ -187,9 +195,12 @@
|
||||
"searchWorkflows": "워크플로 검색",
|
||||
"settings": "설정",
|
||||
"showReport": "보고서 보기",
|
||||
"sort": "정렬",
|
||||
"status": "상태",
|
||||
"success": "성공",
|
||||
"systemInfo": "시스템 정보",
|
||||
"terminal": "터미널",
|
||||
"updated": "업데이트됨",
|
||||
"upload": "업로드",
|
||||
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
|
||||
"workflow": "워크플로"
|
||||
@@ -359,6 +370,43 @@
|
||||
"terminalDefaultMessage": "문제 해결 명령을 실행하면 출력이 여기에 표시됩니다.",
|
||||
"title": "유지 보수"
|
||||
},
|
||||
"manager": {
|
||||
"createdBy": "작성자",
|
||||
"discoverCommunityContent": "커뮤니티에서 만든 노드 팩, 확장 프로그램 등을 찾아보세요...",
|
||||
"downloads": "다운로드",
|
||||
"errorConnecting": "Comfy Node Registry에 연결하는 중 오류가 발생했습니다.",
|
||||
"filter": {
|
||||
"disabled": "비활성화",
|
||||
"enabled": "활성화",
|
||||
"nodePack": "노드 팩"
|
||||
},
|
||||
"installSelected": "선택한 항목 설치",
|
||||
"lastUpdated": "마지막 업데이트",
|
||||
"license": "라이선스",
|
||||
"noDescription": "설명이 없습니다",
|
||||
"noResultsFound": "검색과 일치하는 결과가 없습니다.",
|
||||
"nodePack": "노드 팩",
|
||||
"packsSelected": "선택한 팩",
|
||||
"repository": "저장소",
|
||||
"searchPlaceholder": "검색",
|
||||
"sort": {
|
||||
"downloads": "가장 인기 있는",
|
||||
"rating": "평점"
|
||||
},
|
||||
"status": {
|
||||
"active": "활성",
|
||||
"banned": "금지됨",
|
||||
"deleted": "삭제됨",
|
||||
"flagged": "플래그 표시됨",
|
||||
"pending": "대기 중",
|
||||
"unknown": "알 수 없음"
|
||||
},
|
||||
"title": "사용자 정의 노드 관리자",
|
||||
"totalNodes": "총 노드",
|
||||
"tryAgainLater": "나중에 다시 시도해 주세요.",
|
||||
"tryDifferentSearch": "다른 검색어를 시도해 주세요.",
|
||||
"version": "버전"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "자동 실행 큐",
|
||||
"batchCount": "배치 수",
|
||||
@@ -398,6 +446,7 @@
|
||||
"ComfyUI Forum": "ComfyUI 포럼",
|
||||
"ComfyUI Issues": "ComfyUI 이슈 페이지",
|
||||
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
|
||||
"Custom Nodes Manager": "사용자 정의 노드 관리자",
|
||||
"Delete Selected Items": "선택한 항목 삭제",
|
||||
"Desktop User Guide": "데스크톱 사용자 가이드",
|
||||
"Duplicate Current Workflow": "현재 워크플로 복제",
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "Загрузить стандартный рабочий процесс"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Менеджер Пользовательских Узлов"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Новый пустой рабочий процесс"
|
||||
},
|
||||
|
||||
@@ -116,14 +116,17 @@
|
||||
"g": {
|
||||
"about": "О программе",
|
||||
"add": "Добавить",
|
||||
"all": "Все",
|
||||
"back": "Назад",
|
||||
"cancel": "Отмена",
|
||||
"capture": "захват",
|
||||
"category": "Категория",
|
||||
"choose_file_to_upload": "выберите файл для загрузки",
|
||||
"close": "Закрыть",
|
||||
"color": "Цвет",
|
||||
"comingSoon": "Скоро будет",
|
||||
"command": "Команда",
|
||||
"community": "Сообщество",
|
||||
"confirm": "Подтвердить",
|
||||
"continue": "Продолжить",
|
||||
"control_after_generate": "управление после генерации",
|
||||
@@ -134,6 +137,7 @@
|
||||
"customizeFolder": "Настроить папку",
|
||||
"delete": "Удалить",
|
||||
"deprecated": "Устарело",
|
||||
"description": "Описание",
|
||||
"devices": "Устройства",
|
||||
"disableAll": "Отключить все",
|
||||
"download": "Скачать",
|
||||
@@ -144,6 +148,7 @@
|
||||
"export": "Экспорт",
|
||||
"extensionName": "Название расширения",
|
||||
"feedback": "Обратная связь",
|
||||
"filter": "Фильтр",
|
||||
"findIssues": "Найти проблемы",
|
||||
"firstTimeUIMessage": "Вы впервые используете новый интерфейс. Выберите \"Меню > Использовать новое меню > Отключено\", чтобы восстановить старый интерфейс.",
|
||||
"goToNode": "Перейти к ноде",
|
||||
@@ -157,6 +162,7 @@
|
||||
"loadWorkflow": "Загрузить рабочий процесс",
|
||||
"loading": "Загрузка",
|
||||
"logs": "Логи",
|
||||
"name": "Имя",
|
||||
"newFolder": "Новая папка",
|
||||
"next": "Далее",
|
||||
"no": "Нет",
|
||||
@@ -164,6 +170,7 @@
|
||||
"noTasksFound": "Задачи не найдены",
|
||||
"noTasksFoundMessage": "В очереди нет задач.",
|
||||
"noWorkflowsFound": "Рабочие процессы не найдены.",
|
||||
"nodes": "Узлы",
|
||||
"ok": "ОК",
|
||||
"openNewIssue": "Открыть новую проблему",
|
||||
"overwrite": "Перезаписать",
|
||||
@@ -177,6 +184,7 @@
|
||||
"reportSent": "Отчёт отправлен",
|
||||
"reset": "Сбросить",
|
||||
"resetKeybindingsTooltip": "Сбросить сочетания клавиш по умолчанию",
|
||||
"resultsCount": "Найдено {count} результатов",
|
||||
"save": "Сохранить",
|
||||
"searchExtensions": "Поиск расширений",
|
||||
"searchFailedMessage": "Мы не смогли найти настройки, соответствующие вашему запросу. Попробуйте изменить поисковые термины.",
|
||||
@@ -187,9 +195,12 @@
|
||||
"searchWorkflows": "Поиск рабочих процессов",
|
||||
"settings": "Настройки",
|
||||
"showReport": "Показать отчёт",
|
||||
"sort": "Сортировать",
|
||||
"status": "Статус",
|
||||
"success": "Успех",
|
||||
"systemInfo": "Информация о системе",
|
||||
"terminal": "Терминал",
|
||||
"updated": "Обновлено",
|
||||
"upload": "Загрузить",
|
||||
"videoFailedToLoad": "Не удалось загрузить видео",
|
||||
"workflow": "Рабочий процесс"
|
||||
@@ -359,6 +370,43 @@
|
||||
"terminalDefaultMessage": "Когда вы запускаете команду для устранения неполадок, любой вывод будет отображаться здесь.",
|
||||
"title": "Обслуживание"
|
||||
},
|
||||
"manager": {
|
||||
"createdBy": "Создано",
|
||||
"discoverCommunityContent": "Откройте для себя пакеты узлов, расширения и многое другое, созданные сообществом...",
|
||||
"downloads": "Загрузки",
|
||||
"errorConnecting": "Ошибка подключения к реестру Comfy Node.",
|
||||
"filter": {
|
||||
"disabled": "Отключено",
|
||||
"enabled": "Включено",
|
||||
"nodePack": "Пакет Узлов"
|
||||
},
|
||||
"installSelected": "Установить выбранное",
|
||||
"lastUpdated": "Последнее обновление",
|
||||
"license": "Лицензия",
|
||||
"noDescription": "Описание отсутствует",
|
||||
"noResultsFound": "По вашему запросу ничего не найдено.",
|
||||
"nodePack": "Пакет Узлов",
|
||||
"packsSelected": "Выбрано пакетов",
|
||||
"repository": "Репозиторий",
|
||||
"searchPlaceholder": "Поиск",
|
||||
"sort": {
|
||||
"downloads": "Самые популярные",
|
||||
"rating": "Рейтинг"
|
||||
},
|
||||
"status": {
|
||||
"active": "Активный",
|
||||
"banned": "Заблокировано",
|
||||
"deleted": "Удалено",
|
||||
"flagged": "Отмечено",
|
||||
"pending": "В ожидании",
|
||||
"unknown": "Неизвестно"
|
||||
},
|
||||
"title": "Менеджер Пользовательских Узлов",
|
||||
"totalNodes": "Всего Узлов",
|
||||
"tryAgainLater": "Пожалуйста, попробуйте позже.",
|
||||
"tryDifferentSearch": "Пожалуйста, попробуйте изменить запрос.",
|
||||
"version": "Версия"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "Автоочередь",
|
||||
"batchCount": "Количество пакетов",
|
||||
@@ -398,6 +446,7 @@
|
||||
"ComfyUI Forum": "Форум ComfyUI",
|
||||
"ComfyUI Issues": "Проблемы ComfyUI",
|
||||
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
|
||||
"Custom Nodes Manager": "Менеджер Пользовательских Узлов",
|
||||
"Delete Selected Items": "Удалить выбранные элементы",
|
||||
"Desktop User Guide": "Руководство пользователя для настольных ПК",
|
||||
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
"Comfy_LoadDefaultWorkflow": {
|
||||
"label": "加载默认工作流"
|
||||
},
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "自定义节点管理器"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新建空白工作流"
|
||||
},
|
||||
|
||||
@@ -116,14 +116,17 @@
|
||||
"g": {
|
||||
"about": "关于",
|
||||
"add": "添加",
|
||||
"all": "全部",
|
||||
"back": "返回",
|
||||
"cancel": "取消",
|
||||
"capture": "捕获",
|
||||
"category": "类别",
|
||||
"choose_file_to_upload": "选择要上传的文件",
|
||||
"close": "关闭",
|
||||
"color": "颜色",
|
||||
"comingSoon": "即将推出",
|
||||
"command": "指令",
|
||||
"community": "社区",
|
||||
"confirm": "确认",
|
||||
"continue": "继续",
|
||||
"control_after_generate": "生成后控制",
|
||||
@@ -134,6 +137,7 @@
|
||||
"customizeFolder": "自定义文件夹",
|
||||
"delete": "删除",
|
||||
"deprecated": "已弃用",
|
||||
"description": "描述",
|
||||
"devices": "设备",
|
||||
"disableAll": "禁用全部",
|
||||
"download": "下载",
|
||||
@@ -144,6 +148,7 @@
|
||||
"export": "导出",
|
||||
"extensionName": "扩展名称",
|
||||
"feedback": "反馈",
|
||||
"filter": "过滤",
|
||||
"findIssues": "查找问题",
|
||||
"firstTimeUIMessage": "这是您第一次使用新界面。选择 \"菜单 > 使用新菜单 > 禁用\" 来恢复旧界面。",
|
||||
"goToNode": "转到节点",
|
||||
@@ -157,6 +162,7 @@
|
||||
"loadWorkflow": "加载工作流",
|
||||
"loading": "加载中",
|
||||
"logs": "日志",
|
||||
"name": "名称",
|
||||
"newFolder": "新文件夹",
|
||||
"next": "下一个",
|
||||
"no": "否",
|
||||
@@ -164,6 +170,7 @@
|
||||
"noTasksFound": "未找到任务",
|
||||
"noTasksFoundMessage": "队列中没有任务。",
|
||||
"noWorkflowsFound": "未找到工作流。",
|
||||
"nodes": "节点",
|
||||
"ok": "确定",
|
||||
"openNewIssue": "打开新问题",
|
||||
"overwrite": "覆盖",
|
||||
@@ -177,6 +184,7 @@
|
||||
"reportSent": "报告已提交",
|
||||
"reset": "重置",
|
||||
"resetKeybindingsTooltip": "将快捷键重置为默认",
|
||||
"resultsCount": "找到 {count} 个结果",
|
||||
"save": "保存",
|
||||
"searchExtensions": "搜索扩展",
|
||||
"searchFailedMessage": "我们找不到任何与您的搜索匹配的设置。请尝试调整您的搜索词。",
|
||||
@@ -187,9 +195,12 @@
|
||||
"searchWorkflows": "搜索工作流",
|
||||
"settings": "设置",
|
||||
"showReport": "显示报告",
|
||||
"sort": "排序",
|
||||
"status": "状态",
|
||||
"success": "成功",
|
||||
"systemInfo": "系统信息",
|
||||
"terminal": "终端",
|
||||
"updated": "已更新",
|
||||
"upload": "上传",
|
||||
"videoFailedToLoad": "视频加载失败",
|
||||
"workflow": "工作流"
|
||||
@@ -359,6 +370,43 @@
|
||||
"terminalDefaultMessage": "当你运行一个故障排除命令时,任何输出都会在这里显示。",
|
||||
"title": "维护"
|
||||
},
|
||||
"manager": {
|
||||
"createdBy": "创建者",
|
||||
"discoverCommunityContent": "发现社区制作的节点包,扩展等等...",
|
||||
"downloads": "下载",
|
||||
"errorConnecting": "连接到Comfy节点注册表时出错。",
|
||||
"filter": {
|
||||
"disabled": "已禁用",
|
||||
"enabled": "已启用",
|
||||
"nodePack": "节点包"
|
||||
},
|
||||
"installSelected": "安装选定",
|
||||
"lastUpdated": "最后更新",
|
||||
"license": "许可证",
|
||||
"noDescription": "无可用描述",
|
||||
"noResultsFound": "未找到符合您搜索的结果。",
|
||||
"nodePack": "节点包",
|
||||
"packsSelected": "选定的包",
|
||||
"repository": "仓库",
|
||||
"searchPlaceholder": "搜索",
|
||||
"sort": {
|
||||
"downloads": "最受欢迎",
|
||||
"rating": "评级"
|
||||
},
|
||||
"status": {
|
||||
"active": "活跃",
|
||||
"banned": "已禁止",
|
||||
"deleted": "已删除",
|
||||
"flagged": "已标记",
|
||||
"pending": "待定",
|
||||
"unknown": "未知"
|
||||
},
|
||||
"title": "自定义节点管理器",
|
||||
"totalNodes": "节点总数",
|
||||
"tryAgainLater": "请稍后再试。",
|
||||
"tryDifferentSearch": "请尝试不同的搜索查询。",
|
||||
"version": "版本"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "自动执行",
|
||||
"batchCount": "批次数量",
|
||||
@@ -398,6 +446,7 @@
|
||||
"ComfyUI Forum": "ComfyUI 论坛",
|
||||
"ComfyUI Issues": "ComfyUI 问题",
|
||||
"Convert selected nodes to group node": "将选中节点转换为组节点",
|
||||
"Custom Nodes Manager": "自定义节点管理器",
|
||||
"Delete Selected Items": "删除选定的项目",
|
||||
"Desktop User Guide": "桌面端用户指南",
|
||||
"Duplicate Current Workflow": "复制当前工作流",
|
||||
|
||||
@@ -23,6 +23,9 @@ export const useComfyRegistryService = () => {
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
|
||||
const isLocalNodePack = (nodePackId: string) =>
|
||||
!!nodeDefStore.nodeDefsByName[nodePackId]
|
||||
|
||||
const isLocalNode = (nodeName: string, nodePackId: string) => {
|
||||
if (!nodeDefStore.nodeDefsByName[nodeName]) return false
|
||||
return (
|
||||
@@ -31,6 +34,16 @@ export const useComfyRegistryService = () => {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the node definitions for the pack are available
|
||||
*/
|
||||
const packNodesAvailable = (node: components['schemas']['Node']) => {
|
||||
if (node.id && isLocalNodePack(node.id)) return true
|
||||
if (node.latest_version?.comfy_node_extract_status !== 'success')
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
const handleApiError = (
|
||||
err: unknown,
|
||||
context: string,
|
||||
@@ -100,24 +113,23 @@ export const useComfyRegistryService = () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Comfy Node definition for a specific node in a specific version of a node pack
|
||||
* Get the Comfy Node definitions in a specific version of a node pack
|
||||
* @param packId - The ID of the node pack
|
||||
* @param versionId - The version of the node pack
|
||||
* @param comfyNodeName - The name of the comfy node (corresponds to `ComfyNodeDef#name`)
|
||||
* @returns The node definition or null if not found or an error occurred
|
||||
* @returns The node definitions or null if not found or an error occurred
|
||||
*/
|
||||
const getNodeDef = async (
|
||||
packId: components['schemas']['Node']['id'],
|
||||
versionId: components['schemas']['NodeVersion']['id'],
|
||||
comfyNodeName: components['schemas']['ComfyNode']['comfy_node_name'],
|
||||
const getNodeDefs = async (
|
||||
params: {
|
||||
packId: components['schemas']['Node']['id']
|
||||
versionId: components['schemas']['NodeVersion']['id']
|
||||
},
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
if (!comfyNodeName || !packId) return null
|
||||
if (isLocalNode(comfyNodeName, packId))
|
||||
return nodeDefStore.nodeDefsByName[comfyNodeName]
|
||||
const { packId, versionId } = params
|
||||
if (!packId || !versionId) return null
|
||||
|
||||
const endpoint = `/nodes/${packId}/versions/${versionId}/comfy-nodes/${comfyNodeName}`
|
||||
const errorContext = 'Failed to get node definition'
|
||||
const endpoint = `/nodes/${packId}/versions/${versionId}/comfy-nodes`
|
||||
const errorContext = 'Failed to get node definitions'
|
||||
const routeSpecificErrors = {
|
||||
403: 'This pack has been banned and its definition is not available',
|
||||
404: 'The requested node, version, or comfy node does not exist'
|
||||
@@ -125,7 +137,7 @@ export const useComfyRegistryService = () => {
|
||||
|
||||
return executeApiRequest(
|
||||
() =>
|
||||
registryApiClient.get<components['schemas']['ComfyNode']>(endpoint, {
|
||||
registryApiClient.get<components['schemas']['ComfyNode'][]>(endpoint, {
|
||||
signal
|
||||
}),
|
||||
errorContext,
|
||||
@@ -133,6 +145,33 @@ export const useComfyRegistryService = () => {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Comfy Node definition for a specific node in a specific version of a node pack
|
||||
* @param packId - The ID of the node pack
|
||||
* @param versionId - The version of the node pack
|
||||
* @param comfyNodeName - The name of the comfy node (corresponds to `ComfyNodeDef#name`)
|
||||
* @returns The node definition or null if not found or an error occurred
|
||||
*/
|
||||
const getNodeDef = async (
|
||||
params: {
|
||||
packId: components['schemas']['Node']['id']
|
||||
versionId: components['schemas']['NodeVersion']['id']
|
||||
comfyNodeName: components['schemas']['ComfyNode']['comfy_node_name']
|
||||
},
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const { packId, versionId, comfyNodeName } = params
|
||||
if (!comfyNodeName || !packId || !versionId) return null
|
||||
if (isLocalNode(comfyNodeName, packId))
|
||||
return nodeDefStore.nodeDefsByName[comfyNodeName]
|
||||
|
||||
const nodeDefs = await getNodeDefs({ packId, versionId }, signal)
|
||||
return (
|
||||
nodeDefs?.find((nodeDef) => nodeDef.comfy_node_name === comfyNodeName) ||
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a paginated list of packs matching specific criteria.
|
||||
* Search packs using `search` param. Search individual nodes using `comfy_node_search` param.
|
||||
@@ -335,6 +374,8 @@ export const useComfyRegistryService = () => {
|
||||
getPublisherById,
|
||||
listPacksForPublisher,
|
||||
getNodeDef,
|
||||
postPackReview
|
||||
getNodeDefs,
|
||||
postPackReview,
|
||||
packNodesAvailable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning
|
||||
import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
|
||||
import PromptDialogContent from '@/components/dialog/content/PromptDialogContent.vue'
|
||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||
import ManagerDialogContent from '@/components/dialog/content/manager/ManagerDialogContent.vue'
|
||||
import ManagerHeader from '@/components/dialog/content/manager/ManagerHeader.vue'
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import TemplateWorkflowsContent from '@/components/templates/TemplateWorkflowsContent.vue'
|
||||
import TemplateWorkflowsDialogHeader from '@/components/templates/TemplateWorkflowsDialogHeader.vue'
|
||||
@@ -101,6 +103,24 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showManagerDialog(
|
||||
props: InstanceType<typeof ManagerDialogContent>['$props'] = {}
|
||||
) {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-manager',
|
||||
component: ManagerDialogContent,
|
||||
headerComponent: ManagerHeader,
|
||||
dialogComponentProps: {
|
||||
closable: false,
|
||||
pt: {
|
||||
header: { class: '!p-0 !m-0' },
|
||||
content: { class: '!px-0 h-[83vh] w-[90vw] overflow-y-hidden' }
|
||||
}
|
||||
},
|
||||
props
|
||||
})
|
||||
}
|
||||
|
||||
async function prompt({
|
||||
title,
|
||||
message,
|
||||
@@ -182,6 +202,7 @@ export const useDialogService = () => {
|
||||
showExecutionErrorDialog,
|
||||
showTemplateWorkflowsDialog,
|
||||
showIssueReportDialog,
|
||||
showManagerDialog,
|
||||
prompt,
|
||||
confirm
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ type NodePack = components['schemas']['Node']
|
||||
type ListPacksParams = operations['listAllNodes']['parameters']['query']
|
||||
type ListPacksResult =
|
||||
operations['listAllNodes']['responses'][200]['content']['application/json']
|
||||
type ComfyNode = components['schemas']['ComfyNode']
|
||||
|
||||
/**
|
||||
* Store for managing remote custom nodes
|
||||
@@ -23,6 +24,9 @@ export const useComfyRegistryStore = defineStore('comfyRegistry', () => {
|
||||
typeof useCachedRequest<ListPacksParams, ListPacksResult>
|
||||
>
|
||||
let getPackByIdHandler: ReturnType<typeof useCachedRequest<string, NodePack>>
|
||||
let getNodeDefsHandler: ReturnType<
|
||||
typeof useCachedRequest<{ packId: string; versionId: string }, ComfyNode[]>
|
||||
>
|
||||
|
||||
const recentListResult = ref<NodePack[]>([])
|
||||
const hasPacks = computed(() => recentListResult.value.length > 0)
|
||||
@@ -58,6 +62,23 @@ export const useComfyRegistryStore = defineStore('comfyRegistry', () => {
|
||||
return getPackByIdHandler.call(packId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node definitions for a pack
|
||||
*/
|
||||
const getNodeDefs = async (
|
||||
packId: NodePack['id'],
|
||||
versionId: NonNullable<NodePack['latest_version']>['id']
|
||||
) => {
|
||||
if (!packId || !versionId) return null
|
||||
|
||||
getNodeDefsHandler ??= useCachedRequest<
|
||||
{ packId: string; versionId: string },
|
||||
ComfyNode[]
|
||||
>(registryService.getNodeDefs, { maxSize: PACK_BY_ID_CACHE_SIZE })
|
||||
|
||||
return getNodeDefsHandler.call({ packId, versionId })
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached data
|
||||
*/
|
||||
@@ -80,6 +101,8 @@ export const useComfyRegistryStore = defineStore('comfyRegistry', () => {
|
||||
|
||||
listAllPacks,
|
||||
getPackById,
|
||||
getNodeDefs,
|
||||
|
||||
clearCache,
|
||||
cancelRequests,
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ interface CustomDialogComponentProps {
|
||||
maximizable?: boolean
|
||||
maximized?: boolean
|
||||
onClose?: () => void
|
||||
closable?: boolean
|
||||
pt?: DialogPassThroughOptions
|
||||
}
|
||||
|
||||
|
||||
14
src/types/comfyManagerTypes.ts
Normal file
14
src/types/comfyManagerTypes.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
export interface TabItem {
|
||||
id: string
|
||||
label: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
export type NodeField = keyof components['schemas']['Node'] | null
|
||||
|
||||
export interface SearchOption<T> {
|
||||
id: T
|
||||
label: string
|
||||
}
|
||||
@@ -320,3 +320,11 @@ export const paramsToCacheKey = (params: unknown): string => {
|
||||
*/
|
||||
export const generateRandomSuffix = (): string =>
|
||||
Math.random().toString(36).substring(2, 6)
|
||||
|
||||
/**
|
||||
* Formats a number to a locale-specific string
|
||||
* @param num The number to format
|
||||
* @returns The formatted number or 'N/A' if the number is undefined
|
||||
*/
|
||||
export const formatNumber = (num?: number): string =>
|
||||
num?.toLocaleString() ?? 'N/A'
|
||||
|
||||
Reference in New Issue
Block a user