8 Commits

Author SHA1 Message Date
Hayden
8877c1599b prepare release 2.5.1 2025-02-24 11:09:08 +08:00
Hayden
965905305e fix: find subfolder incorrect (#154) 2025-02-24 11:07:43 +08:00
Hayden
312138f981 fix: auto open root folder (#151) 2025-02-22 18:30:29 +08:00
Hayden
76df8cd3cb prepare release 2.5.0 2025-02-22 18:14:38 +08:00
Hayden
df17eae0a2 fix: dialog cover tooltip (#150) 2025-02-22 18:10:43 +08:00
Hayden
7df89c7265 feat: add tooltip for model card and folder path (#149) 2025-02-22 18:10:28 +08:00
Hayden
450072e49d refactor(explorer): optimize openFolder (#148) 2025-02-22 18:10:11 +08:00
Hayden
759865e8ea feat: support search sub folder (#147) 2025-02-22 18:09:59 +08:00
10 changed files with 143 additions and 72 deletions

View File

@@ -138,8 +138,9 @@ class ModelManager:
stat = entry.stat() stat = entry.stat()
return { return {
"type": folder if is_file else "folder", "type": folder,
"subFolder": sub_folder, "subFolder": sub_folder,
"isFolder": not is_file,
"basename": basename, "basename": basename,
"extension": extension, "extension": extension,
"pathIndex": path_index, "pathIndex": path_index,

View File

@@ -1,7 +1,7 @@
[project] [project]
name = "comfyui-model-manager" name = "comfyui-model-manager"
description = "Manage models: browsing, download and delete." description = "Manage models: browsing, download and delete."
version = "2.4.0" version = "2.5.1"
license = { file = "LICENSE" } license = { file = "LICENSE" }
dependencies = ["markdownify"] dependencies = ["markdownify"]

View File

@@ -20,7 +20,6 @@
v-show="!showToolbar" v-show="!showToolbar"
class="h-10 flex-1" class="h-10 flex-1"
:items="folderPaths" :items="folderPaths"
@item-click="(item, index) => openFolder(index, item.name, item.icon)"
></ResponseBreadcrumb> ></ResponseBreadcrumb>
</div> </div>
@@ -69,13 +68,21 @@
}" }"
> >
<ModelCard <ModelCard
:model="rowItem"
v-for="rowItem in item.row" v-for="rowItem in item.row"
:model="rowItem"
:key="genModelKey(rowItem)" :key="genModelKey(rowItem)"
:style="{ :style="{
width: `${cardSize.width}px`, width: `${cardSize.width}px`,
height: `${cardSize.height}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)" @dblclick="openItem(rowItem, $event)"
@contextmenu.stop.prevent="openItemContext(rowItem, $event)" @contextmenu.stop.prevent="openItemContext(rowItem, $event)"
></ModelCard> ></ModelCard>
@@ -138,8 +145,14 @@ const gutter = {
y: 32, y: 32,
} }
const { dataTreeList, folderPaths, findFolder, openFolder, openModelDetail } = const {
useModelExplorer() dataTreeList,
folderPaths,
findFolder,
openFolder,
openModelDetail,
getFullPath,
} = useModelExplorer()
const { cardSize, cardSizeMap, cardSizeFlag, dialog: settings } = useConfig() const { cardSize, cardSizeMap, cardSizeFlag, dialog: settings } = useConfig()
const showToolbar = ref(false) const showToolbar = ref(false)
@@ -180,11 +193,15 @@ const sortOrderOptions = ref(
const currentDataList = computed(() => { const currentDataList = computed(() => {
let renderedList = dataTreeList.value let renderedList = dataTreeList.value
for (const folderItem of folderPaths.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 || [] renderedList = found?.children || []
} }
if (searchContent.value) { const filter = searchContent.value?.toLowerCase().trim() ?? ''
if (filter) {
const filterItems: ModelTreeNode[] = [] const filterItems: ModelTreeNode[] = []
const searchList = [...renderedList] const searchList = [...renderedList]
@@ -194,11 +211,10 @@ const currentDataList = computed(() => {
const children = (item as any).children ?? [] const children = (item as any).children ?? []
searchList.push(...children) searchList.push(...children)
if ( const matchSubFolder = `${item.subFolder}/`.toLowerCase().includes(filter)
item.basename const matchName = item.basename.toLowerCase().includes(filter)
.toLocaleLowerCase()
.includes(searchContent.value.toLocaleLowerCase()) if (matchSubFolder || matchName) {
) {
filterItems.push(item) filterItems.push(item)
} }
} }
@@ -211,7 +227,7 @@ const currentDataList = computed(() => {
const modelItems: ModelTreeNode[] = [] const modelItems: ModelTreeNode[] = []
for (const item of renderedList) { for (const item of renderedList) {
if (item.type === 'folder') { if (item.isFolder) {
folderItems.push(item) folderItems.push(item)
} else { } else {
modelItems.push(item) modelItems.push(item)
@@ -281,8 +297,9 @@ const confirmName = ref('')
const openItem = (item: ModelTreeNode, e: Event) => { const openItem = (item: ModelTreeNode, e: Event) => {
menu.value.hide(e) menu.value.hide(e)
if (item.type === 'folder') { if (item.isFolder) {
openFolder(folderPaths.value.length, item.basename) searchContent.value = undefined
openFolder(item)
} else { } else {
openModelDetail(item) openModelDetail(item)
} }

View File

@@ -55,7 +55,13 @@
}" }"
class="group/card cursor-pointer !p-0" class="group/card cursor-pointer !p-0"
@click="openModelDetail(model)" @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> <template #name>
<div <div
@@ -139,7 +145,7 @@ const {
dialog: settings, dialog: settings,
} = useConfig() } = useConfig()
const { data, folders, openModelDetail } = useModels() const { data, folders, openModelDetail, getFullPath } = useModels()
const { t } = useI18n() const { t } = useI18n()
const toolbarContainer = ref<HTMLElement | null>(null) const toolbarContainer = ref<HTMLElement | null>(null)
@@ -216,18 +222,19 @@ const cols = computed(() => {
const list = computed(() => { const list = computed(() => {
const mergedList = Object.values(data.value).flat() const mergedList = Object.values(data.value).flat()
const pureModels = mergedList.filter((item) => { const pureModels = mergedList.filter((item) => {
return item.type !== 'folder' return !item.isFolder
}) })
const filterList = pureModels.filter((model) => { const filterList = pureModels.filter((model) => {
const showAllModel = currentType.value === allType const showAllModel = currentType.value === allType
const matchType = showAllModel || model.type === currentType.value 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 let sortStrategy: (a: Model, b: Model) => number = () => 0

View File

@@ -12,7 +12,7 @@
:min-height="item.minHeight" :min-height="item.minHeight"
:max-height="item.maxHeight" :max-height="item.maxHeight"
:auto-z-index="false" :auto-z-index="false"
:pt:mask:style="{ zIndex: baseZIndex - 100 + index + 1 }" :pt:mask:style="{ zIndex: baseZIndex + index + 1 }"
:pt:root:onMousedown="() => rise(item)" :pt:root:onMousedown="() => rise(item)"
@hide="() => close(item)" @hide="() => close(item)"
> >
@@ -37,6 +37,7 @@
<component :is="item.content" v-bind="item.contentProps"></component> <component :is="item.content" v-bind="item.contentProps"></component>
</template> </template>
</ResponseDialog> </ResponseDialog>
<Dialog :visible="true" :pt:mask:style="{ display: 'none' }"></Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -44,6 +45,7 @@ import ResponseDialog from 'components/ResponseDialog.vue'
import { useDialog } from 'hooks/dialog' import { useDialog } from 'hooks/dialog'
import Button from 'primevue/button' import Button from 'primevue/button'
import { usePrimeVue } from 'primevue/config' import { usePrimeVue } from 'primevue/config'
import Dialog from 'primevue/dialog'
import { computed } from 'vue' import { computed } from 'vue'
const { stack, rise, close } = useDialog() const { stack, rise, close } = useDialog()

View File

@@ -84,7 +84,17 @@
<td class="border-r bg-gray-300 px-4 dark:bg-gray-800"> <td class="border-r bg-gray-300 px-4 dark:bg-gray-800">
{{ $t(`info.${item.key}`) }} {{ $t(`info.${item.key}`) }}
</td> </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 }} {{ item.display }}
</td> </td>
</tr> </tr>

View File

@@ -5,7 +5,7 @@
> >
<div data-card-main class="flex h-full w-full flex-col"> <div data-card-main class="flex h-full w-full flex-col">
<div data-card-preview class="flex-1 overflow-hidden"> <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 <svg
class="icon" class="icon"
viewBox="0 0 1024 1024" viewBox="0 0 1024 1024"
@@ -39,7 +39,7 @@
</div> </div>
<div <div
v-if="model.type !== 'folder'" v-if="!model.isFolder"
data-draggable-overlay data-draggable-overlay
class="absolute left-0 top-0 h-full w-full" class="absolute left-0 top-0 h-full w-full"
draggable="true" draggable="true"
@@ -47,7 +47,7 @@
></div> ></div>
<div <div
v-if="model.type !== 'folder'" v-if="!model.isFolder"
data-mode-type data-mode-type
class="pointer-events-none absolute left-2 top-2" class="pointer-events-none absolute left-2 top-2"
:style="{ :style="{

View File

@@ -1,17 +1,17 @@
import { genModelFullName, useModels } from 'hooks/model' import { genModelFullName, useModels } from 'hooks/model'
import { cloneDeep, filter, find } from 'lodash' import { cloneDeep, filter, find } from 'lodash'
import { BaseModel, Model, SelectOptions } from 'types/typings' import { BaseModel, Model, SelectOptions } from 'types/typings'
import { computed, ref, watchEffect } from 'vue' import { computed, ref, watch } from 'vue'
export interface FolderPathItem { export interface FolderPathItem {
name: string name: string
pathIndex: number
icon?: string icon?: string
onClick: () => void onClick: () => void
children: SelectOptions[] children: SelectOptions[]
} }
export type ModelFolder = BaseModel & { export type ModelFolder = BaseModel & {
type: 'folder'
children: ModelTreeNode[] children: ModelTreeNode[]
} }
@@ -27,22 +27,27 @@ export type TreeItemNode = ModelTreeNode & {
} }
export const useModelExplorer = () => { export const useModelExplorer = () => {
const { data, folders, ...modelRest } = useModels() const { data, folders, initialized, ...modelRest } = useModels()
const folderPaths = ref<FolderPathItem[]>([]) const folderPaths = ref<FolderPathItem[]>([])
const genFolderItem = (basename: string, subFolder: string): ModelFolder => { const genFolderItem = (
basename: string,
folder?: string,
subFolder?: string,
): ModelFolder => {
return { return {
id: basename, id: basename,
basename: basename, basename: basename,
subFolder: subFolder, subFolder: subFolder ?? '',
pathIndex: 0, pathIndex: 0,
sizeBytes: 0, sizeBytes: 0,
extension: '', extension: '',
description: '', description: '',
metadata: {}, metadata: {},
preview: '', preview: '',
type: 'folder', type: folder ?? '',
isFolder: true,
children: [], children: [],
} }
} }
@@ -52,7 +57,7 @@ export const useModelExplorer = () => {
for (const folder in folders.value) { for (const folder in folders.value) {
if (Object.prototype.hasOwnProperty.call(folders.value, folder)) { if (Object.prototype.hasOwnProperty.call(folders.value, folder)) {
const folderItem = genFolderItem(folder, '') const folderItem = genFolderItem(folder)
const folderModels = cloneDeep(data.value[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 root.children = rootChildren
return [root] return [root]
}) })
function findFolder(list: ModelTreeNode[], name: string) { function findFolder(
return find(list, { type: 'folder', basename: name }) as list: ModelTreeNode[],
| ModelFolder feature: { basename: string; pathIndex: number },
| undefined ) {
return find(list, { ...feature, isFolder: true }) as ModelFolder | undefined
} }
function findFolders(list: ModelTreeNode[]) { 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) { async function openFolder(item: BaseModel) {
if (folderPaths.value.length >= level) { const folderItems: FolderPathItem[] = []
folderPaths.value.splice(level)
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 let levelFolders = findFolders(dataTreeList.value)
for (const folderItem of folderPaths.value) { for (const [index, part] of pathParts.entries()) {
const found = findFolder(currentLevel, folderItem.name) const pathIndex = index < 2 ? 0 : item.pathIndex
currentLevel = found?.children || []
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) folderPaths.value = folderItems
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),
}
}),
})
} }
watchEffect(() => { watch(initialized, (val) => {
if (Object.keys(folders.value).length > 0 && folderPaths.value.length < 2) { if (val) {
openFolder(0, 'root', 'pi pi-desktop') openFolder(dataTreeList.value[0])
} }
}, {}) })
return { return {
folders, folders,

View File

@@ -50,10 +50,12 @@ export const useModels = defineStore('models', (store) => {
const loading = useLoading() const loading = useLoading()
const folders = ref<ModelFolder>({}) const folders = ref<ModelFolder>({})
const initialized = ref(false)
const refreshFolders = async () => { const refreshFolders = async () => {
return request('/models').then((resData) => { return request('/models').then((resData) => {
folders.value = 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 { return {
initialized: initialized,
folders: folders, folders: folders,
data: models, data: models,
refresh: refreshAllModels, refresh: refreshAllModels,
remove: deleteModel, remove: deleteModel,
update: updateModel, update: updateModel,
openModelDetail: openModelDetail, openModelDetail: openModelDetail,
getFullPath: getFullPath,
} }
}) })
@@ -446,7 +456,7 @@ export const useModelFolder = (
} }
const folderItems = cloneDeep(models.value[type]) ?? [] 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)) pureFolders.sort((a, b) => a.basename.localeCompare(b.basename))
const folders = modelFolders.value[type] ?? [] const folders = modelFolders.value[type] ?? []

View File

@@ -9,6 +9,7 @@ export interface BaseModel {
type: string type: string
subFolder: string subFolder: string
pathIndex: number pathIndex: number
isFolder: boolean
preview: string | string[] preview: string | string[]
description: string description: string
metadata: Record<string, string> metadata: Record<string, string>