Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4132b2d8c4 | ||
|
|
40a1a7f43a | ||
|
|
14bb6f194d | ||
|
|
97b26549ce | ||
|
|
e75275dfff | ||
|
|
c1e89eb177 |
@@ -65,4 +65,3 @@ There are three installation methods, choose one
|
|||||||
<img src="demo/scan-model-info.png" alt="Model Manager Demo Screenshot" style="max-width: 100%; max-height: 300px"/>
|
<img src="demo/scan-model-info.png" alt="Model Manager Demo Screenshot" style="max-width: 100%; max-height: 300px"/>
|
||||||
|
|
||||||
- Scan models and try to download information & preview.
|
- Scan models and try to download information & preview.
|
||||||
- Support migration from `cdb-boop/ComfyUI-Model-Manager/main`
|
|
||||||
|
|||||||
16
__init__.py
16
__init__.py
@@ -130,7 +130,7 @@ async def list_model_types(request):
|
|||||||
Scan all models and read their information.
|
Scan all models and read their information.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = utils.resolve_model_base_paths()
|
result = utils.resolve_model_base_paths(request)
|
||||||
return web.json_response({"success": True, "data": result})
|
return web.json_response({"success": True, "data": result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Read models failed: {str(e)}"
|
error_msg = f"Read models failed: {str(e)}"
|
||||||
@@ -294,20 +294,6 @@ async def read_download_preview(request):
|
|||||||
return web.FileResponse(preview_path)
|
return web.FileResponse(preview_path)
|
||||||
|
|
||||||
|
|
||||||
@routes.post("/model-manager/migrate")
|
|
||||||
async def migrate_legacy_information(request):
|
|
||||||
"""
|
|
||||||
Migrate legacy information.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
await services.migrate_legacy_information(request)
|
|
||||||
return web.json_response({"success": True})
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"Migrate model info failed: {str(e)}"
|
|
||||||
utils.print_error(error_msg)
|
|
||||||
return web.json_response({"success": False, "error": error_msg})
|
|
||||||
|
|
||||||
|
|
||||||
WEB_DIRECTORY = "web"
|
WEB_DIRECTORY = "web"
|
||||||
NODE_CLASS_MAPPINGS = {}
|
NODE_CLASS_MAPPINGS = {}
|
||||||
__all__ = ["WEB_DIRECTORY", "NODE_CLASS_MAPPINGS"]
|
__all__ = ["WEB_DIRECTORY", "NODE_CLASS_MAPPINGS"]
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
|
||||||
"@types/lodash": "^4.17.9",
|
"@types/lodash": "^4.17.9",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^22.5.5",
|
"@types/node": "^22.5.5",
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -36,9 +36,6 @@ importers:
|
|||||||
specifier: ^2.6.0
|
specifier: ^2.6.0
|
||||||
version: 2.6.0
|
version: 2.6.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tailwindcss/container-queries':
|
|
||||||
specifier: ^0.1.1
|
|
||||||
version: 0.1.1(tailwindcss@3.4.12)
|
|
||||||
'@types/lodash':
|
'@types/lodash':
|
||||||
specifier: ^4.17.9
|
specifier: ^4.17.9
|
||||||
version: 4.17.9
|
version: 4.17.9
|
||||||
@@ -470,11 +467,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@tailwindcss/container-queries@0.1.1':
|
|
||||||
resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==}
|
|
||||||
peerDependencies:
|
|
||||||
tailwindcss: '>=3.2.0'
|
|
||||||
|
|
||||||
'@types/estree@1.0.5':
|
'@types/estree@1.0.5':
|
||||||
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
||||||
|
|
||||||
@@ -2000,10 +1992,6 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc@4.22.0':
|
'@rollup/rollup-win32-x64-msvc@4.22.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.12)':
|
|
||||||
dependencies:
|
|
||||||
tailwindcss: 3.4.12
|
|
||||||
|
|
||||||
'@types/estree@1.0.5': {}
|
'@types/estree@1.0.5': {}
|
||||||
|
|
||||||
'@types/linkify-it@5.0.0': {}
|
'@types/linkify-it@5.0.0': {}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ setting_key = {
|
|||||||
"max_task_count": "ModelManager.Download.MaxTaskCount",
|
"max_task_count": "ModelManager.Download.MaxTaskCount",
|
||||||
},
|
},
|
||||||
"scan": {
|
"scan": {
|
||||||
"include_hidden_files": "ModelManager.Scan.IncludeHiddenFiles"
|
"include_hidden_files": "ModelManager.Scan.IncludeHiddenFiles",
|
||||||
|
"exclude_scan_types": "ModelManager.Scan.excludeScanTypes",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import os
|
|||||||
|
|
||||||
import folder_paths
|
import folder_paths
|
||||||
|
|
||||||
from . import config
|
|
||||||
from . import utils
|
from . import utils
|
||||||
from . import download
|
from . import download
|
||||||
from . import searcher
|
from . import searcher
|
||||||
@@ -189,86 +188,3 @@ async def download_model_info(scan_mode: str, request):
|
|||||||
utils.print_error(f"Failed to download model info for {abs_model_path}: {e}")
|
utils.print_error(f"Failed to download model info for {abs_model_path}: {e}")
|
||||||
|
|
||||||
utils.print_debug("Completed scan model information.")
|
utils.print_debug("Completed scan model information.")
|
||||||
|
|
||||||
|
|
||||||
async def migrate_legacy_information(request):
|
|
||||||
import json
|
|
||||||
import yaml
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
utils.print_info(f"Migrating legacy information...")
|
|
||||||
|
|
||||||
model_base_paths = utils.resolve_model_base_paths()
|
|
||||||
for model_type in model_base_paths:
|
|
||||||
|
|
||||||
folders, extensions = folder_paths.folder_names_and_paths[model_type]
|
|
||||||
for path_index, base_path in enumerate(folders):
|
|
||||||
files = utils.recursive_search_files(base_path, request)
|
|
||||||
|
|
||||||
models = folder_paths.filter_files_extensions(files, folder_paths.supported_pt_extensions)
|
|
||||||
|
|
||||||
for fullname in models:
|
|
||||||
fullname = utils.normalize_path(fullname)
|
|
||||||
|
|
||||||
abs_model_path = utils.join_path(base_path, fullname)
|
|
||||||
|
|
||||||
base_file_name = os.path.splitext(abs_model_path)[0]
|
|
||||||
|
|
||||||
utils.print_debug(f"Try to migrate legacy info for {abs_model_path}")
|
|
||||||
|
|
||||||
preview_path = utils.join_path(
|
|
||||||
os.path.dirname(abs_model_path),
|
|
||||||
utils.get_model_preview_name(abs_model_path),
|
|
||||||
)
|
|
||||||
new_preview_path = f"{base_file_name}.webp"
|
|
||||||
|
|
||||||
if os.path.isfile(preview_path) and preview_path != new_preview_path:
|
|
||||||
utils.print_info(f"Migrate preview image from {fullname}")
|
|
||||||
with Image.open(preview_path) as image:
|
|
||||||
image.save(new_preview_path, format="WEBP")
|
|
||||||
|
|
||||||
description_path = f"{base_file_name}.md"
|
|
||||||
|
|
||||||
metadata_info = {
|
|
||||||
"website": "Civitai",
|
|
||||||
}
|
|
||||||
|
|
||||||
url_info_path = f"{base_file_name}.url"
|
|
||||||
if os.path.isfile(url_info_path):
|
|
||||||
with open(url_info_path, "r", encoding="utf-8") as f:
|
|
||||||
for line in f:
|
|
||||||
if line.startswith("URL="):
|
|
||||||
model_page_url = line[len("URL=") :].strip()
|
|
||||||
metadata_info.update({"modelPage": model_page_url})
|
|
||||||
|
|
||||||
json_info_path = f"{base_file_name}.json"
|
|
||||||
if os.path.isfile(json_info_path):
|
|
||||||
with open(json_info_path, "r", encoding="utf-8") as f:
|
|
||||||
version = json.load(f)
|
|
||||||
metadata_info.update(
|
|
||||||
{
|
|
||||||
"baseModel": version.get("baseModel"),
|
|
||||||
"preview": [i["url"] for i in version["images"]],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
description_parts: list[str] = [
|
|
||||||
"---",
|
|
||||||
yaml.dump(metadata_info).strip(),
|
|
||||||
"---",
|
|
||||||
"",
|
|
||||||
]
|
|
||||||
|
|
||||||
text_info_path = f"{base_file_name}.txt"
|
|
||||||
if os.path.isfile(text_info_path):
|
|
||||||
with open(text_info_path, "r", encoding="utf-8") as f:
|
|
||||||
description_parts.append(f.read())
|
|
||||||
|
|
||||||
description_path = f"{base_file_name}.md"
|
|
||||||
|
|
||||||
if os.path.isfile(text_info_path):
|
|
||||||
utils.print_info(f"Migrate description from {fullname}")
|
|
||||||
with open(description_path, "w", encoding="utf-8", newline="") as f:
|
|
||||||
f.write("\n".join(description_parts))
|
|
||||||
|
|
||||||
utils.print_debug("Completed migrate model information.")
|
|
||||||
|
|||||||
@@ -116,10 +116,13 @@ def download_web_distribution(version: str):
|
|||||||
print_error(f"An unexpected error occurred: {e}")
|
print_error(f"An unexpected error occurred: {e}")
|
||||||
|
|
||||||
|
|
||||||
def resolve_model_base_paths():
|
def resolve_model_base_paths(request):
|
||||||
folders = list(folder_paths.folder_names_and_paths.keys())
|
folders = list(folder_paths.folder_names_and_paths.keys())
|
||||||
model_base_paths = {}
|
model_base_paths = {}
|
||||||
folder_black_list = ["configs", "custom_nodes"]
|
folder_black_list = ["configs", "custom_nodes"]
|
||||||
|
custom_folders = get_setting_value(request, "scan.exclude_scan_types", "")
|
||||||
|
custom_black_list = [f.strip() for f in custom_folders.split(",") if f.strip()]
|
||||||
|
folder_black_list.extend(custom_black_list)
|
||||||
for folder in folders:
|
for folder in folders:
|
||||||
if folder in folder_black_list:
|
if folder in folder_black_list:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[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.1.6"
|
version = "2.2.0"
|
||||||
license = "LICENSE"
|
license = { file = "LICENSE" }
|
||||||
dependencies = ["markdownify"]
|
dependencies = ["markdownify"]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-4">
|
<div class="flex h-full flex-col gap-4">
|
||||||
<div class="whitespace-nowrap px-4 @container">
|
<div class="whitespace-nowrap px-4" v-container="container">
|
||||||
<div class="flex gap-4 @sm:justify-end">
|
<div :class="['flex gap-4', $sm('justify-end')]">
|
||||||
<Button
|
<Button
|
||||||
class="w-full @sm:w-auto"
|
:class="[$sm('w-auto', 'w-full')]"
|
||||||
:label="$t('createDownloadTask')"
|
:label="$t('createDownloadTask')"
|
||||||
@click="openCreateTask"
|
@click="openCreateTask"
|
||||||
></Button>
|
></Button>
|
||||||
@@ -73,6 +73,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DialogCreateTask from 'components/DialogCreateTask.vue'
|
import DialogCreateTask from 'components/DialogCreateTask.vue'
|
||||||
import ResponseScroll from 'components/ResponseScroll.vue'
|
import ResponseScroll from 'components/ResponseScroll.vue'
|
||||||
|
import { useContainerQueries } from 'hooks/container'
|
||||||
import { useDialog } from 'hooks/dialog'
|
import { useDialog } from 'hooks/dialog'
|
||||||
import { useDownload } from 'hooks/download'
|
import { useDownload } from 'hooks/download'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
@@ -90,4 +91,7 @@ const openCreateTask = () => {
|
|||||||
content: DialogCreateTask,
|
content: DialogCreateTask,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const container = Symbol('container')
|
||||||
|
const { $sm } = useContainerQueries(container)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex h-full flex-col gap-4 overflow-hidden @container/content"
|
class="flex h-full flex-col gap-4 overflow-hidden"
|
||||||
:style="{
|
|
||||||
['--card-width']: `${cardWidth}px`,
|
|
||||||
['--gutter']: `${gutter}px`,
|
|
||||||
}"
|
|
||||||
v-resize="onContainerResize"
|
v-resize="onContainerResize"
|
||||||
|
v-container="contentContainer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:class="[
|
class="grid grid-cols-1 justify-center gap-4 px-8"
|
||||||
'grid grid-cols-1 justify-center gap-4 px-8',
|
:style="$content_lg(contentStyle)"
|
||||||
'@lg/content:grid-cols-[repeat(auto-fit,var(--card-width))]',
|
|
||||||
'@lg/content:gap-[var(--gutter)]',
|
|
||||||
'@lg/content:px-4',
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
<div class="col-span-full @container/toolbar">
|
<div class="col-span-full" v-container="toolbarContainer">
|
||||||
<div :class="['flex flex-col gap-4', '@2xl/toolbar:flex-row']">
|
<div class="flex flex-col gap-4" :style="$toolbar_2xl(toolbarStyle)">
|
||||||
<ResponseInput
|
<ResponseInput
|
||||||
v-model="searchContent"
|
v-model="searchContent"
|
||||||
:placeholder="$t('searchModels')"
|
:placeholder="$t('searchModels')"
|
||||||
@@ -48,12 +41,8 @@
|
|||||||
>
|
>
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<div
|
<div
|
||||||
:class="[
|
class="grid grid-cols-1 justify-center gap-8 px-8"
|
||||||
'grid grid-cols-1 justify-center gap-8 px-8',
|
:style="contentStyle"
|
||||||
'@lg/content:grid-cols-[repeat(auto-fit,var(--card-width))]',
|
|
||||||
'@lg/content:gap-[var(--gutter)]',
|
|
||||||
'@lg/content:px-4',
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
<ModelCard
|
<ModelCard
|
||||||
v-for="model in item"
|
v-for="model in item"
|
||||||
@@ -80,6 +69,7 @@ import ResponseInput from 'components/ResponseInput.vue'
|
|||||||
import ResponseScroll from 'components/ResponseScroll.vue'
|
import ResponseScroll from 'components/ResponseScroll.vue'
|
||||||
import ResponseSelect from 'components/ResponseSelect.vue'
|
import ResponseSelect from 'components/ResponseSelect.vue'
|
||||||
import { useConfig } from 'hooks/config'
|
import { useConfig } from 'hooks/config'
|
||||||
|
import { useContainerQueries } from 'hooks/container'
|
||||||
import { useModels } from 'hooks/model'
|
import { useModels } from 'hooks/model'
|
||||||
import { defineResizeCallback } from 'hooks/resize'
|
import { defineResizeCallback } from 'hooks/resize'
|
||||||
import { chunk } from 'lodash'
|
import { chunk } from 'lodash'
|
||||||
@@ -179,6 +169,22 @@ const list = computed(() => {
|
|||||||
return chunk(sortedList, colSpan.value)
|
return chunk(sortedList, colSpan.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const toolbarContainer = Symbol('toolbar')
|
||||||
|
const { $2xl: $toolbar_2xl } = useContainerQueries(toolbarContainer)
|
||||||
|
|
||||||
|
const contentContainer = Symbol('content')
|
||||||
|
const { $lg: $content_lg } = useContainerQueries(contentContainer)
|
||||||
|
|
||||||
|
const contentStyle = {
|
||||||
|
gridTemplateColumns: `repeat(auto-fit, ${cardWidth}px)`,
|
||||||
|
gap: `${gutter}px`,
|
||||||
|
paddingLeft: `1rem`,
|
||||||
|
paddingRight: `1rem`,
|
||||||
|
}
|
||||||
|
const toolbarStyle = {
|
||||||
|
flexDirection: 'row',
|
||||||
|
}
|
||||||
|
|
||||||
const onContainerResize = defineResizeCallback((entries) => {
|
const onContainerResize = defineResizeCallback((entries) => {
|
||||||
const entry = entries[0]
|
const entry = entries[0]
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
|
|||||||
@@ -20,7 +20,12 @@
|
|||||||
<div class="relative h-full w-full text-white">
|
<div class="relative h-full w-full text-white">
|
||||||
<div class="absolute bottom-0 left-0">
|
<div class="absolute bottom-0 left-0">
|
||||||
<div class="drop-shadow-[0px_2px_2px_rgba(0,0,0,0.75)]">
|
<div class="drop-shadow-[0px_2px_2px_rgba(0,0,0,0.75)]">
|
||||||
<div class="line-clamp-3 break-all text-2xl font-bold @lg:text-lg">
|
<div
|
||||||
|
:class="[
|
||||||
|
'line-clamp-3 break-all font-bold',
|
||||||
|
$lg('text-lg', 'text-2xl'),
|
||||||
|
]"
|
||||||
|
>
|
||||||
{{ model.basename }}
|
{{ model.basename }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +34,7 @@
|
|||||||
<div class="absolute left-0 top-0 w-full">
|
<div class="absolute left-0 top-0 w-full">
|
||||||
<div class="flex flex-row items-start justify-between">
|
<div class="flex flex-row items-start justify-between">
|
||||||
<div class="flex items-center rounded-full bg-black/30 px-3 py-2">
|
<div class="flex items-center rounded-full bg-black/30 px-3 py-2">
|
||||||
<div class="font-bold @lg:text-xs">
|
<div :class="['font-bold', $lg('text-xs')]">
|
||||||
{{ model.type }}
|
{{ model.type }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,6 +71,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import DialogModelDetail from 'components/DialogModelDetail.vue'
|
import DialogModelDetail from 'components/DialogModelDetail.vue'
|
||||||
|
import { useContainerQueries } from 'hooks/container'
|
||||||
import { useDialog } from 'hooks/dialog'
|
import { useDialog } from 'hooks/dialog'
|
||||||
import { useModelNodeAction } from 'hooks/model'
|
import { useModelNodeAction } from 'hooks/model'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
@@ -101,4 +107,6 @@ const preview = computed(() =>
|
|||||||
|
|
||||||
const { addModelNode, dragToAddModelNode, copyModelNode, loadPreviewWorkflow } =
|
const { addModelNode, dragToAddModelNode, copyModelNode, loadPreviewWorkflow } =
|
||||||
useModelNodeAction(props.model)
|
useModelNodeAction(props.model)
|
||||||
|
|
||||||
|
const { $lg } = useContainerQueries()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<form
|
<form
|
||||||
class="@container"
|
|
||||||
@submit.prevent="handleSubmit"
|
@submit.prevent="handleSubmit"
|
||||||
@reset.prevent="handleReset"
|
@reset.prevent="handleReset"
|
||||||
|
v-container="container"
|
||||||
>
|
>
|
||||||
<div class="mx-auto w-full max-w-[50rem]">
|
<div class="mx-auto w-full max-w-[50rem]">
|
||||||
<div class="relative flex flex-col gap-4 overflow-hidden @xl:flex-row">
|
<div
|
||||||
|
:class="[
|
||||||
|
'relative flex gap-4 overflow-hidden',
|
||||||
|
$xl('flex-row', 'flex-col'),
|
||||||
|
]"
|
||||||
|
>
|
||||||
<ModelPreview
|
<ModelPreview
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
v-model:editable="editable"
|
v-model:editable="editable"
|
||||||
@@ -43,6 +48,7 @@ import ModelBaseInfo from 'components/ModelBaseInfo.vue'
|
|||||||
import ModelDescription from 'components/ModelDescription.vue'
|
import ModelDescription from 'components/ModelDescription.vue'
|
||||||
import ModelMetadata from 'components/ModelMetadata.vue'
|
import ModelMetadata from 'components/ModelMetadata.vue'
|
||||||
import ModelPreview from 'components/ModelPreview.vue'
|
import ModelPreview from 'components/ModelPreview.vue'
|
||||||
|
import { useContainerQueries } from 'hooks/container'
|
||||||
import {
|
import {
|
||||||
useModelBaseInfoEditor,
|
useModelBaseInfoEditor,
|
||||||
useModelDescriptionEditor,
|
useModelDescriptionEditor,
|
||||||
@@ -94,4 +100,7 @@ watch(
|
|||||||
handleReset()
|
handleReset()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const container = Symbol('container')
|
||||||
|
const { $xl } = useContainerQueries(container)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="flex flex-col gap-4">
|
||||||
class="flex flex-col gap-4"
|
|
||||||
:style="{ ['--preview-width']: `${cardWidth}px` }"
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
:class="[
|
class="relative mx-auto w-full overflow-hidden rounded-lg preview-aspect"
|
||||||
'relative mx-auto w-full',
|
:style="$sm({ width: `${cardWidth}px` })"
|
||||||
'@sm:w-[var(--preview-width)]',
|
|
||||||
'overflow-hidden rounded-lg preview-aspect',
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
<ResponseImage :src="preview" :error="noPreviewContent"></ResponseImage>
|
<ResponseImage :src="preview" :error="noPreviewContent"></ResponseImage>
|
||||||
|
|
||||||
@@ -52,7 +46,7 @@
|
|||||||
:class="[
|
:class="[
|
||||||
'flex h-10 items-center gap-4',
|
'flex h-10 items-center gap-4',
|
||||||
'absolute left-1/2 -translate-x-1/2',
|
'absolute left-1/2 -translate-x-1/2',
|
||||||
'@xl:left-0 @xl:translate-x-0',
|
$xl('left-0 translate-x-0'),
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -92,6 +86,7 @@ import ResponseFileUpload from 'components/ResponseFileUpload.vue'
|
|||||||
import ResponseImage from 'components/ResponseImage.vue'
|
import ResponseImage from 'components/ResponseImage.vue'
|
||||||
import ResponseInput from 'components/ResponseInput.vue'
|
import ResponseInput from 'components/ResponseInput.vue'
|
||||||
import { useConfig } from 'hooks/config'
|
import { useConfig } from 'hooks/config'
|
||||||
|
import { useContainerQueries } from 'hooks/container'
|
||||||
import { useModelPreview } from 'hooks/model'
|
import { useModelPreview } from 'hooks/model'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import Carousel from 'primevue/carousel'
|
import Carousel from 'primevue/carousel'
|
||||||
@@ -109,4 +104,6 @@ const {
|
|||||||
updateLocalContent,
|
updateLocalContent,
|
||||||
noPreviewContent,
|
noPreviewContent,
|
||||||
} = useModelPreview()
|
} = useModelPreview()
|
||||||
|
|
||||||
|
const { $sm, $xl } = useContainerQueries()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { request } from 'hooks/request'
|
|||||||
import { defineStore } from 'hooks/store'
|
import { defineStore } from 'hooks/store'
|
||||||
import { $el, app, ComfyDialog } from 'scripts/comfyAPI'
|
import { $el, app, ComfyDialog } from 'scripts/comfyAPI'
|
||||||
import { onMounted, onUnmounted, ref } from 'vue'
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
|
|
||||||
export const useConfig = defineStore('config', (store) => {
|
export const useConfig = defineStore('config', (store) => {
|
||||||
@@ -42,6 +43,7 @@ declare module 'hooks/store' {
|
|||||||
|
|
||||||
function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const confirm = (opts: {
|
const confirm = (opts: {
|
||||||
message?: string
|
message?: string
|
||||||
@@ -79,6 +81,7 @@ function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
|||||||
// API keys
|
// API keys
|
||||||
app.ui?.settings.addSetting({
|
app.ui?.settings.addSetting({
|
||||||
id: 'ModelManager.APIKey.HuggingFace',
|
id: 'ModelManager.APIKey.HuggingFace',
|
||||||
|
category: [t('modelManager'), t('setting.apiKey'), 'HuggingFace'],
|
||||||
name: 'HuggingFace API Key',
|
name: 'HuggingFace API Key',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
@@ -86,61 +89,17 @@ function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
|||||||
|
|
||||||
app.ui?.settings.addSetting({
|
app.ui?.settings.addSetting({
|
||||||
id: 'ModelManager.APIKey.Civitai',
|
id: 'ModelManager.APIKey.Civitai',
|
||||||
|
category: [t('modelManager'), t('setting.apiKey'), 'Civitai'],
|
||||||
name: 'Civitai API Key',
|
name: 'Civitai API Key',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Migrate
|
|
||||||
app.ui?.settings.addSetting({
|
|
||||||
id: 'ModelManager.Migrate.Migrate',
|
|
||||||
name: 'Migrate information from cdb-boop/main',
|
|
||||||
defaultValue: '',
|
|
||||||
type: () => {
|
|
||||||
return $el('button.p-button.p-component.p-button-secondary', {
|
|
||||||
textContent: 'Migrate',
|
|
||||||
onclick: () => {
|
|
||||||
confirm({
|
|
||||||
message: [
|
|
||||||
'This operation will delete old files and override current files if it exists.',
|
|
||||||
// 'This may take a while and generate MANY server requests!',
|
|
||||||
'Continue?',
|
|
||||||
].join('\n'),
|
|
||||||
accept: () => {
|
|
||||||
store.loading.loading.value = true
|
|
||||||
request('/migrate', {
|
|
||||||
method: 'POST',
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.add({
|
|
||||||
severity: 'success',
|
|
||||||
summary: 'Complete migration',
|
|
||||||
life: 2000,
|
|
||||||
})
|
|
||||||
store.models.refresh()
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.add({
|
|
||||||
severity: 'error',
|
|
||||||
summary: 'Error',
|
|
||||||
detail: err.message ?? 'Failed to migrate information',
|
|
||||||
life: 15000,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
store.loading.loading.value = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Scan information
|
// Scan information
|
||||||
app.ui?.settings.addSetting({
|
app.ui?.settings.addSetting({
|
||||||
id: 'ModelManager.ScanFiles.Full',
|
id: 'ModelManager.ScanFiles.Full',
|
||||||
name: "Override all models' information and preview",
|
category: [t('modelManager'), t('setting.scan'), 'Full'],
|
||||||
|
name: t('setting.scanAll'),
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
type: () => {
|
type: () => {
|
||||||
return $el('button.p-button.p-component.p-button-secondary', {
|
return $el('button.p-button.p-component.p-button-secondary', {
|
||||||
@@ -186,7 +145,8 @@ function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
|||||||
|
|
||||||
app.ui?.settings.addSetting({
|
app.ui?.settings.addSetting({
|
||||||
id: 'ModelManager.ScanFiles.Incremental',
|
id: 'ModelManager.ScanFiles.Incremental',
|
||||||
name: 'Download missing information or preview',
|
category: [t('modelManager'), t('setting.scan'), 'Incremental'],
|
||||||
|
name: t('setting.scanMissing'),
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
type: () => {
|
type: () => {
|
||||||
return $el('button.p-button.p-component.p-button-secondary', {
|
return $el('button.p-button.p-component.p-button-secondary', {
|
||||||
@@ -230,9 +190,18 @@ function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.ui?.settings.addSetting({
|
||||||
|
id: 'ModelManager.Scan.excludeScanTypes',
|
||||||
|
category: [t('modelManager'), t('setting.scan'), 'ExcludeScanTypes'],
|
||||||
|
name: t('setting.excludeScanTypes'),
|
||||||
|
defaultValue: undefined,
|
||||||
|
type: 'text',
|
||||||
|
})
|
||||||
|
|
||||||
app.ui?.settings.addSetting({
|
app.ui?.settings.addSetting({
|
||||||
id: 'ModelManager.Scan.IncludeHiddenFiles',
|
id: 'ModelManager.Scan.IncludeHiddenFiles',
|
||||||
name: 'Include hidden files(start with .)',
|
category: [t('modelManager'), t('setting.scan'), 'IncludeHiddenFiles'],
|
||||||
|
name: t('setting.includeHiddenFiles'),
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
})
|
})
|
||||||
|
|||||||
60
src/hooks/container.ts
Normal file
60
src/hooks/container.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { defineResizeCallback } from 'hooks/resize'
|
||||||
|
import { computed, Directive, inject, InjectionKey, provide, ref } from 'vue'
|
||||||
|
|
||||||
|
const globalContainerSize = ref<Record<symbol, number>>({})
|
||||||
|
|
||||||
|
const containerNameKey = Symbol('containerName') as InjectionKey<symbol>
|
||||||
|
|
||||||
|
export const containerDirective: Directive<HTMLElement, symbol> = {
|
||||||
|
mounted: (el, binding) => {
|
||||||
|
const containerName = binding.value || Symbol('container')
|
||||||
|
const resizeCallback = defineResizeCallback((entries) => {
|
||||||
|
const entry = entries[0]
|
||||||
|
globalContainerSize.value[containerName] = entry.contentRect.width
|
||||||
|
})
|
||||||
|
const observer = new ResizeObserver(resizeCallback)
|
||||||
|
observer.observe(el)
|
||||||
|
el['_containerObserver'] = observer
|
||||||
|
},
|
||||||
|
unmounted: (el) => {
|
||||||
|
const observer = el['_containerObserver']
|
||||||
|
observer.disconnect()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||||
|
|
||||||
|
export const useContainerQueries = (containerName?: symbol) => {
|
||||||
|
const parentContainer = inject(containerNameKey, Symbol('unknown'))
|
||||||
|
|
||||||
|
const name = containerName ?? parentContainer
|
||||||
|
|
||||||
|
provide(containerNameKey, name)
|
||||||
|
|
||||||
|
const currentContainerSize = computed(() => {
|
||||||
|
return globalContainerSize.value[name] ?? 0
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param size unit rem
|
||||||
|
*/
|
||||||
|
const generator = (size: number) => {
|
||||||
|
return (content: any, defaultContent: any = undefined) => {
|
||||||
|
return currentContainerSize.value > size * rem ? content : defaultContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
$xs: generator(20),
|
||||||
|
$sm: generator(24),
|
||||||
|
$md: generator(28),
|
||||||
|
$lg: generator(32),
|
||||||
|
$xl: generator(36),
|
||||||
|
$2xl: generator(42),
|
||||||
|
$3xl: generator(48),
|
||||||
|
$4xl: generator(54),
|
||||||
|
$5xl: generator(60),
|
||||||
|
$6xl: generator(66),
|
||||||
|
$7xl: generator(72),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@ export const useModels = defineStore('models', (store) => {
|
|||||||
|
|
||||||
const refreshAllModels = async (force = false) => {
|
const refreshAllModels = async (force = false) => {
|
||||||
const forceRefresh = force ? refreshFolders() : Promise.resolve()
|
const forceRefresh = force ? refreshFolders() : Promise.resolve()
|
||||||
|
models.value = {}
|
||||||
return forceRefresh.then(() =>
|
return forceRefresh.then(() =>
|
||||||
Promise.allSettled(Object.keys(folders.value).map(refreshModels)),
|
Promise.allSettled(Object.keys(folders.value).map(refreshModels)),
|
||||||
)
|
)
|
||||||
|
|||||||
21
src/i18n.ts
21
src/i18n.ts
@@ -1,3 +1,4 @@
|
|||||||
|
import { app } from 'scripts/comfyAPI'
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
@@ -38,6 +39,14 @@ const messages = {
|
|||||||
createdAt: 'Created At',
|
createdAt: 'Created At',
|
||||||
updatedAt: 'Updated At',
|
updatedAt: 'Updated At',
|
||||||
},
|
},
|
||||||
|
setting: {
|
||||||
|
apiKey: 'API Key',
|
||||||
|
scan: 'Scan',
|
||||||
|
scanMissing: 'Download missing information or preview',
|
||||||
|
scanAll: "Override all models' information and preview",
|
||||||
|
includeHiddenFiles: 'Include hidden files(start with .)',
|
||||||
|
excludeScanTypes: 'Exclude scan types (separate with commas)',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
model: '模型',
|
model: '模型',
|
||||||
@@ -76,16 +85,24 @@ const messages = {
|
|||||||
createdAt: '创建时间',
|
createdAt: '创建时间',
|
||||||
updatedAt: '更新时间',
|
updatedAt: '更新时间',
|
||||||
},
|
},
|
||||||
|
setting: {
|
||||||
|
apiKey: '密钥',
|
||||||
|
scan: '扫描',
|
||||||
|
scanMissing: '下载缺失的信息或预览图片',
|
||||||
|
scanAll: '覆盖所有模型信息和预览图片',
|
||||||
|
includeHiddenFiles: '包含隐藏文件(以 . 开头的文件或文件夹)',
|
||||||
|
excludeScanTypes: '排除扫描类型(使用英文逗号隔开)',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLocalLanguage = () => {
|
const getLocalLanguage = () => {
|
||||||
const local =
|
const local =
|
||||||
localStorage.getItem('Comfy.Settings.Comfy.Locale') ||
|
app.ui?.settings.getSettingValue<string>('Comfy.Locale') ||
|
||||||
navigator.language.split('-')[0] ||
|
navigator.language.split('-')[0] ||
|
||||||
'en'
|
'en'
|
||||||
|
|
||||||
return local.replace(/['"]/g, '')
|
return local
|
||||||
}
|
}
|
||||||
|
|
||||||
export const i18n = createI18n({
|
export const i18n = createI18n({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { definePreset } from '@primevue/themes'
|
import { definePreset } from '@primevue/themes'
|
||||||
import Aura from '@primevue/themes/aura'
|
import Aura from '@primevue/themes/aura'
|
||||||
|
import { containerDirective } from 'hooks/container'
|
||||||
import { resizeDirective } from 'hooks/resize'
|
import { resizeDirective } from 'hooks/resize'
|
||||||
import PrimeVue from 'primevue/config'
|
import PrimeVue from 'primevue/config'
|
||||||
import ConfirmationService from 'primevue/confirmationservice'
|
import ConfirmationService from 'primevue/confirmationservice'
|
||||||
@@ -21,6 +22,7 @@ function createVueApp(rootContainer: string | HTMLElement) {
|
|||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.directive('tooltip', Tooltip)
|
app.directive('tooltip', Tooltip)
|
||||||
app.directive('resize', resizeDirective)
|
app.directive('resize', resizeDirective)
|
||||||
|
app.directive('container', containerDirective)
|
||||||
app
|
app
|
||||||
.use(PrimeVue, {
|
.use(PrimeVue, {
|
||||||
theme: {
|
theme: {
|
||||||
|
|||||||
2
src/types/global.d.ts
vendored
2
src/types/global.d.ts
vendored
@@ -157,6 +157,8 @@ declare namespace ComfyAPI {
|
|||||||
|
|
||||||
class ComfySettingsDialog {
|
class ComfySettingsDialog {
|
||||||
addSetting: (params: SettingParams) => { value: any }
|
addSetting: (params: SettingParams) => { value: any }
|
||||||
|
getSettingValue: <T>(id: string, defaultValue?: T) => T
|
||||||
|
setSettingValue: <T>(id: string, value: T) => void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
src/types/shims.d.ts
vendored
1
src/types/shims.d.ts
vendored
@@ -3,6 +3,7 @@ export {}
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
interface ComponentCustomProperties {
|
interface ComponentCustomProperties {
|
||||||
vResize: (typeof import('hooks/resize'))['resizeDirective']
|
vResize: (typeof import('hooks/resize'))['resizeDirective']
|
||||||
|
vContainer: (typeof import('hooks/container'))['containerDirective']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import container from '@tailwindcss/container-queries'
|
|
||||||
import plugin from 'tailwindcss/plugin'
|
import plugin from 'tailwindcss/plugin'
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
@@ -8,7 +7,6 @@ export default {
|
|||||||
darkMode: ['selector', '.dark-theme'],
|
darkMode: ['selector', '.dark-theme'],
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
container,
|
|
||||||
plugin(({ addUtilities }) => {
|
plugin(({ addUtilities }) => {
|
||||||
addUtilities({
|
addUtilities({
|
||||||
'.scrollbar-none': {
|
'.scrollbar-none': {
|
||||||
|
|||||||
Reference in New Issue
Block a user