Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1975e2056d | ||
|
|
8877c1599b | ||
|
|
965905305e | ||
|
|
312138f981 | ||
|
|
76df8cd3cb | ||
|
|
df17eae0a2 | ||
|
|
7df89c7265 | ||
|
|
450072e49d | ||
|
|
759865e8ea |
@@ -124,13 +124,13 @@ class ModelManager:
|
||||
if not prefix_path.endswith("/"):
|
||||
prefix_path = f"{prefix_path}/"
|
||||
|
||||
is_file = entry.is_file()
|
||||
relative_path = utils.normalize_path(entry.path).replace(prefix_path, "")
|
||||
sub_folder = os.path.dirname(relative_path)
|
||||
filename = os.path.basename(relative_path)
|
||||
basename = os.path.splitext(filename)[0]
|
||||
extension = os.path.splitext(filename)[1]
|
||||
basename = os.path.splitext(filename)[0] if is_file else filename
|
||||
extension = os.path.splitext(filename)[1] if is_file else ""
|
||||
|
||||
is_file = entry.is_file()
|
||||
if is_file and extension not in folder_paths.supported_pt_extensions:
|
||||
return None
|
||||
|
||||
@@ -138,8 +138,9 @@ class ModelManager:
|
||||
|
||||
stat = entry.stat()
|
||||
return {
|
||||
"type": folder if is_file else "folder",
|
||||
"type": folder,
|
||||
"subFolder": sub_folder,
|
||||
"isFolder": not is_file,
|
||||
"basename": basename,
|
||||
"extension": extension,
|
||||
"pathIndex": path_index,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-model-manager"
|
||||
description = "Manage models: browsing, download and delete."
|
||||
version = "2.4.0"
|
||||
version = "2.5.2"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["markdownify"]
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
v-show="!showToolbar"
|
||||
class="h-10 flex-1"
|
||||
:items="folderPaths"
|
||||
@item-click="(item, index) => openFolder(index, item.name, item.icon)"
|
||||
></ResponseBreadcrumb>
|
||||
</div>
|
||||
|
||||
@@ -69,13 +68,21 @@
|
||||
}"
|
||||
>
|
||||
<ModelCard
|
||||
:model="rowItem"
|
||||
v-for="rowItem in item.row"
|
||||
:model="rowItem"
|
||||
:key="genModelKey(rowItem)"
|
||||
:style="{
|
||||
width: `${cardSize.width}px`,
|
||||
height: `${cardSize.height}px`,
|
||||
}"
|
||||
v-tooltip.top="{
|
||||
value: getFullPath(rowItem),
|
||||
disabled: folderPaths.length < 2,
|
||||
autoHide: false,
|
||||
showDelay: 800,
|
||||
hideDelay: 300,
|
||||
pt: { root: { style: { zIndex: 2100, maxWidth: '32rem' } } },
|
||||
}"
|
||||
@dblclick="openItem(rowItem, $event)"
|
||||
@contextmenu.stop.prevent="openItemContext(rowItem, $event)"
|
||||
></ModelCard>
|
||||
@@ -138,8 +145,14 @@ const gutter = {
|
||||
y: 32,
|
||||
}
|
||||
|
||||
const { dataTreeList, folderPaths, findFolder, openFolder, openModelDetail } =
|
||||
useModelExplorer()
|
||||
const {
|
||||
dataTreeList,
|
||||
folderPaths,
|
||||
findFolder,
|
||||
openFolder,
|
||||
openModelDetail,
|
||||
getFullPath,
|
||||
} = useModelExplorer()
|
||||
const { cardSize, cardSizeMap, cardSizeFlag, dialog: settings } = useConfig()
|
||||
|
||||
const showToolbar = ref(false)
|
||||
@@ -180,11 +193,15 @@ const sortOrderOptions = ref(
|
||||
const currentDataList = computed(() => {
|
||||
let renderedList = dataTreeList.value
|
||||
for (const folderItem of folderPaths.value) {
|
||||
const found = findFolder(renderedList, folderItem.name)
|
||||
const found = findFolder(renderedList, {
|
||||
basename: folderItem.name,
|
||||
pathIndex: folderItem.pathIndex,
|
||||
})
|
||||
renderedList = found?.children || []
|
||||
}
|
||||
|
||||
if (searchContent.value) {
|
||||
const filter = searchContent.value?.toLowerCase().trim() ?? ''
|
||||
if (filter) {
|
||||
const filterItems: ModelTreeNode[] = []
|
||||
|
||||
const searchList = [...renderedList]
|
||||
@@ -194,11 +211,10 @@ const currentDataList = computed(() => {
|
||||
const children = (item as any).children ?? []
|
||||
searchList.push(...children)
|
||||
|
||||
if (
|
||||
item.basename
|
||||
.toLocaleLowerCase()
|
||||
.includes(searchContent.value.toLocaleLowerCase())
|
||||
) {
|
||||
const matchSubFolder = `${item.subFolder}/`.toLowerCase().includes(filter)
|
||||
const matchName = item.basename.toLowerCase().includes(filter)
|
||||
|
||||
if (matchSubFolder || matchName) {
|
||||
filterItems.push(item)
|
||||
}
|
||||
}
|
||||
@@ -211,7 +227,7 @@ const currentDataList = computed(() => {
|
||||
const modelItems: ModelTreeNode[] = []
|
||||
|
||||
for (const item of renderedList) {
|
||||
if (item.type === 'folder') {
|
||||
if (item.isFolder) {
|
||||
folderItems.push(item)
|
||||
} else {
|
||||
modelItems.push(item)
|
||||
@@ -281,8 +297,9 @@ const confirmName = ref('')
|
||||
|
||||
const openItem = (item: ModelTreeNode, e: Event) => {
|
||||
menu.value.hide(e)
|
||||
if (item.type === 'folder') {
|
||||
openFolder(folderPaths.value.length, item.basename)
|
||||
if (item.isFolder) {
|
||||
searchContent.value = undefined
|
||||
openFolder(item)
|
||||
} else {
|
||||
openModelDetail(item)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,13 @@
|
||||
}"
|
||||
class="group/card cursor-pointer !p-0"
|
||||
@click="openModelDetail(model)"
|
||||
v-tooltip.top="{ value: model.basename, disabled: showModelName }"
|
||||
v-tooltip.top="{
|
||||
value: getFullPath(model),
|
||||
autoHide: false,
|
||||
showDelay: 800,
|
||||
hideDelay: 300,
|
||||
pt: { root: { style: { zIndex: 2100, maxWidth: '32rem' } } },
|
||||
}"
|
||||
>
|
||||
<template #name>
|
||||
<div
|
||||
@@ -139,7 +145,7 @@ const {
|
||||
dialog: settings,
|
||||
} = useConfig()
|
||||
|
||||
const { data, folders, openModelDetail } = useModels()
|
||||
const { data, folders, openModelDetail, getFullPath } = useModels()
|
||||
const { t } = useI18n()
|
||||
|
||||
const toolbarContainer = ref<HTMLElement | null>(null)
|
||||
@@ -216,18 +222,19 @@ const cols = computed(() => {
|
||||
const list = computed(() => {
|
||||
const mergedList = Object.values(data.value).flat()
|
||||
const pureModels = mergedList.filter((item) => {
|
||||
return item.type !== 'folder'
|
||||
return !item.isFolder
|
||||
})
|
||||
|
||||
const filterList = pureModels.filter((model) => {
|
||||
const showAllModel = currentType.value === allType
|
||||
|
||||
const matchType = showAllModel || model.type === currentType.value
|
||||
const matchName = model.basename
|
||||
.toLowerCase()
|
||||
.includes(searchContent.value?.toLowerCase() || '')
|
||||
|
||||
return matchType && matchName
|
||||
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
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
:min-height="item.minHeight"
|
||||
:max-height="item.maxHeight"
|
||||
:auto-z-index="false"
|
||||
:pt:mask:style="{ zIndex: baseZIndex - 100 + index + 1 }"
|
||||
:pt:mask:style="{ zIndex: baseZIndex + index + 1 }"
|
||||
:pt:root:onMousedown="() => rise(item)"
|
||||
@hide="() => close(item)"
|
||||
>
|
||||
@@ -37,6 +37,7 @@
|
||||
<component :is="item.content" v-bind="item.contentProps"></component>
|
||||
</template>
|
||||
</ResponseDialog>
|
||||
<Dialog :visible="true" :pt:mask:style="{ display: 'none' }"></Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -44,6 +45,7 @@ import ResponseDialog from 'components/ResponseDialog.vue'
|
||||
import { useDialog } from 'hooks/dialog'
|
||||
import Button from 'primevue/button'
|
||||
import { usePrimeVue } from 'primevue/config'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { stack, rise, close } = useDialog()
|
||||
|
||||
@@ -84,7 +84,17 @@
|
||||
<td class="border-r bg-gray-300 px-4 dark:bg-gray-800">
|
||||
{{ $t(`info.${item.key}`) }}
|
||||
</td>
|
||||
<td class="overflow-hidden text-ellipsis break-all px-4">
|
||||
<td
|
||||
class="overflow-hidden text-ellipsis break-all px-4"
|
||||
v-tooltip.top="{
|
||||
value: item.display,
|
||||
disabled: !['pathIndex', 'basename'].includes(item.key),
|
||||
autoHide: false,
|
||||
showDelay: 800,
|
||||
hideDelay: 300,
|
||||
pt: { root: { style: { zIndex: 2100, maxWidth: '32rem' } } },
|
||||
}"
|
||||
>
|
||||
{{ item.display }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
>
|
||||
<div data-card-main class="flex h-full w-full flex-col">
|
||||
<div data-card-preview class="flex-1 overflow-hidden">
|
||||
<div v-if="model.type === 'folder'" class="h-full w-full">
|
||||
<div v-if="model.isFolder" class="h-full w-full">
|
||||
<svg
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="model.type !== 'folder'"
|
||||
v-if="!model.isFolder"
|
||||
data-draggable-overlay
|
||||
class="absolute left-0 top-0 h-full w-full"
|
||||
draggable="true"
|
||||
@@ -47,7 +47,7 @@
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="model.type !== 'folder'"
|
||||
v-if="!model.isFolder"
|
||||
data-mode-type
|
||||
class="pointer-events-none absolute left-2 top-2"
|
||||
:style="{
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { genModelFullName, useModels } from 'hooks/model'
|
||||
import { cloneDeep, filter, find } from 'lodash'
|
||||
import { BaseModel, Model, SelectOptions } from 'types/typings'
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
export interface FolderPathItem {
|
||||
name: string
|
||||
pathIndex: number
|
||||
icon?: string
|
||||
onClick: () => void
|
||||
children: SelectOptions[]
|
||||
}
|
||||
|
||||
export type ModelFolder = BaseModel & {
|
||||
type: 'folder'
|
||||
children: ModelTreeNode[]
|
||||
}
|
||||
|
||||
@@ -27,22 +27,27 @@ export type TreeItemNode = ModelTreeNode & {
|
||||
}
|
||||
|
||||
export const useModelExplorer = () => {
|
||||
const { data, folders, ...modelRest } = useModels()
|
||||
const { data, folders, initialized, ...modelRest } = useModels()
|
||||
|
||||
const folderPaths = ref<FolderPathItem[]>([])
|
||||
|
||||
const genFolderItem = (basename: string, subFolder: string): ModelFolder => {
|
||||
const genFolderItem = (
|
||||
basename: string,
|
||||
folder?: string,
|
||||
subFolder?: string,
|
||||
): ModelFolder => {
|
||||
return {
|
||||
id: basename,
|
||||
basename: basename,
|
||||
subFolder: subFolder,
|
||||
subFolder: subFolder ?? '',
|
||||
pathIndex: 0,
|
||||
sizeBytes: 0,
|
||||
extension: '',
|
||||
description: '',
|
||||
metadata: {},
|
||||
preview: '',
|
||||
type: 'folder',
|
||||
type: folder ?? '',
|
||||
isFolder: true,
|
||||
children: [],
|
||||
}
|
||||
}
|
||||
@@ -52,7 +57,7 @@ export const useModelExplorer = () => {
|
||||
|
||||
for (const folder in folders.value) {
|
||||
if (Object.prototype.hasOwnProperty.call(folders.value, folder)) {
|
||||
const folderItem = genFolderItem(folder, '')
|
||||
const folderItem = genFolderItem(folder)
|
||||
|
||||
const folderModels = cloneDeep(data.value[folder]) ?? []
|
||||
|
||||
@@ -82,58 +87,76 @@ export const useModelExplorer = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const root: ModelTreeNode = genFolderItem('root', '')
|
||||
const root: ModelTreeNode = genFolderItem('root')
|
||||
root.children = rootChildren
|
||||
return [root]
|
||||
})
|
||||
|
||||
function findFolder(list: ModelTreeNode[], name: string) {
|
||||
return find(list, { type: 'folder', basename: name }) as
|
||||
| ModelFolder
|
||||
| undefined
|
||||
function findFolder(
|
||||
list: ModelTreeNode[],
|
||||
feature: { basename: string; pathIndex: number },
|
||||
) {
|
||||
return find(list, { ...feature, isFolder: true }) as ModelFolder | undefined
|
||||
}
|
||||
|
||||
function findFolders(list: ModelTreeNode[]) {
|
||||
return filter(list, { type: 'folder' }) as ModelFolder[]
|
||||
return filter(list, { isFolder: true }) as ModelFolder[]
|
||||
}
|
||||
|
||||
async function openFolder(level: number, name: string, icon?: string) {
|
||||
if (folderPaths.value.length >= level) {
|
||||
folderPaths.value.splice(level)
|
||||
async function openFolder(item: BaseModel) {
|
||||
const folderItems: FolderPathItem[] = []
|
||||
|
||||
const folder = item.type
|
||||
const subFolderParts = item.subFolder.split('/').filter(Boolean)
|
||||
|
||||
const pathParts: string[] = []
|
||||
if (folder) {
|
||||
pathParts.push(folder, ...subFolderParts)
|
||||
}
|
||||
pathParts.push(item.basename)
|
||||
if (pathParts[0] !== 'root') {
|
||||
pathParts.unshift('root')
|
||||
}
|
||||
|
||||
let currentLevel = dataTreeList.value
|
||||
for (const folderItem of folderPaths.value) {
|
||||
const found = findFolder(currentLevel, folderItem.name)
|
||||
currentLevel = found?.children || []
|
||||
let levelFolders = findFolders(dataTreeList.value)
|
||||
for (const [index, part] of pathParts.entries()) {
|
||||
const pathIndex = index < 2 ? 0 : item.pathIndex
|
||||
|
||||
const currentFolder = findFolder(levelFolders, {
|
||||
basename: part,
|
||||
pathIndex: pathIndex,
|
||||
})
|
||||
if (!currentFolder) {
|
||||
break
|
||||
}
|
||||
|
||||
levelFolders = findFolders(currentFolder.children ?? [])
|
||||
folderItems.push({
|
||||
name: currentFolder.basename,
|
||||
pathIndex: pathIndex,
|
||||
icon: index === 0 ? 'pi pi-desktop' : '',
|
||||
onClick: () => {
|
||||
openFolder(currentFolder)
|
||||
},
|
||||
children: levelFolders.map((child) => {
|
||||
const name = child.basename
|
||||
return {
|
||||
value: name,
|
||||
label: name,
|
||||
command: () => openFolder(child),
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
const folderItem = findFolder(currentLevel, name)
|
||||
const folderItemChildren = folderItem?.children ?? []
|
||||
const subFolders = findFolders(folderItemChildren)
|
||||
|
||||
folderPaths.value.push({
|
||||
name,
|
||||
icon,
|
||||
onClick: () => {
|
||||
openFolder(level, name, icon)
|
||||
},
|
||||
children: subFolders.map((item) => {
|
||||
const name = item.basename
|
||||
return {
|
||||
value: name,
|
||||
label: name,
|
||||
command: () => openFolder(level + 1, name),
|
||||
}
|
||||
}),
|
||||
})
|
||||
folderPaths.value = folderItems
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (Object.keys(folders.value).length > 0 && folderPaths.value.length < 2) {
|
||||
openFolder(0, 'root', 'pi pi-desktop')
|
||||
watch(initialized, (val) => {
|
||||
if (val) {
|
||||
openFolder(dataTreeList.value[0])
|
||||
}
|
||||
}, {})
|
||||
})
|
||||
|
||||
return {
|
||||
folders,
|
||||
|
||||
@@ -50,10 +50,12 @@ export const useModels = defineStore('models', (store) => {
|
||||
const loading = useLoading()
|
||||
|
||||
const folders = ref<ModelFolder>({})
|
||||
const initialized = ref(false)
|
||||
|
||||
const refreshFolders = async () => {
|
||||
return request('/models').then((resData) => {
|
||||
folders.value = resData
|
||||
initialized.value = true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -226,13 +228,21 @@ export const useModels = defineStore('models', (store) => {
|
||||
})
|
||||
}
|
||||
|
||||
function getFullPath(model: BaseModel) {
|
||||
const fullname = genModelFullName(model)
|
||||
const prefixPath = folders.value[model.type]?.[model.pathIndex]
|
||||
return [prefixPath, fullname].filter(Boolean).join('/')
|
||||
}
|
||||
|
||||
return {
|
||||
initialized: initialized,
|
||||
folders: folders,
|
||||
data: models,
|
||||
refresh: refreshAllModels,
|
||||
remove: deleteModel,
|
||||
update: updateModel,
|
||||
openModelDetail: openModelDetail,
|
||||
getFullPath: getFullPath,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -446,7 +456,7 @@ export const useModelFolder = (
|
||||
}
|
||||
|
||||
const folderItems = cloneDeep(models.value[type]) ?? []
|
||||
const pureFolders = folderItems.filter((item) => item.type === 'folder')
|
||||
const pureFolders = folderItems.filter((item) => item.isFolder)
|
||||
pureFolders.sort((a, b) => a.basename.localeCompare(b.basename))
|
||||
|
||||
const folders = modelFolders.value[type] ?? []
|
||||
|
||||
1
src/types/typings.d.ts
vendored
1
src/types/typings.d.ts
vendored
@@ -9,6 +9,7 @@ export interface BaseModel {
|
||||
type: string
|
||||
subFolder: string
|
||||
pathIndex: number
|
||||
isFolder: boolean
|
||||
preview: string | string[]
|
||||
description: string
|
||||
metadata: Record<string, string>
|
||||
|
||||
Reference in New Issue
Block a user