8 Commits

Author SHA1 Message Date
Hayden
be383ac6e1 fix: potential bug after adding excluded directories (#94)
* Revert "fix: missing parameter (#93)"

This reverts commit c2406a1fd1.

* Revert "feat: add exclude scan model types (#92)"

This reverts commit 40a1a7f43a.

* feat: add exclude scan model types

* fix: potential bug after adding excluded directories
2025-01-14 11:04:41 +08:00
Hayden
c2406a1fd1 fix: missing parameter (#93) 2025-01-13 15:58:11 +08:00
Hayden
4132b2d8c4 prepare release 2.2.0 2025-01-13 15:16:31 +08:00
Hayden
40a1a7f43a feat: add exclude scan model types (#92) 2025-01-13 15:15:32 +08:00
Hayden
14bb6f194d Fix: i18n settings (#91)
* fix(i18n): Getting language configuration exception

* feat(i18n): Change settings display
2025-01-13 11:58:17 +08:00
Hayden
97b26549ce feat: Remove migration functionality (#89) 2025-01-10 17:11:15 +08:00
Hayden
e75275dfff fix: Container queries occasionally fail (#88)
- Use js dynamic calculation instead of container query
- Remove @tailwindcss/container-queries
2025-01-10 16:04:49 +08:00
Robin Huang
c1e89eb177 chore(licence-update): Update PyProject Toml - License (#87)
Co-authored-by: snomiao <snomiao+comfy-pr@gmail.com>
2025-01-09 10:16:53 +08:00
19 changed files with 199 additions and 206 deletions

View File

@@ -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`

View File

@@ -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"]

View File

@@ -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
View File

@@ -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': {}

View File

@@ -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.")

View File

@@ -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.2"
license = "LICENSE" license = { file = "LICENSE" }
dependencies = ["markdownify"] dependencies = ["markdownify"]
[project.urls] [project.urls]

View File

@@ -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>

View File

@@ -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"
@@ -79,10 +68,12 @@ import ModelCard from 'components/ModelCard.vue'
import ResponseInput from 'components/ResponseInput.vue' 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 { configSetting, 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'
import { app } from 'scripts/comfyAPI'
import { Model } from 'types/typings' import { Model } from 'types/typings'
import { genModelKey } from 'utils/model' import { genModelKey } from 'utils/model'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
@@ -99,7 +90,20 @@ const searchContent = ref<string>()
const currentType = ref('all') const currentType = ref('all')
const typeOptions = computed(() => { const typeOptions = computed(() => {
return ['all', ...Object.keys(folders.value)].map((type) => { const excludeScanTypes = app.ui?.settings.getSettingValue<string>(
configSetting.excludeScanTypes,
)
const customBlackList =
excludeScanTypes
?.split(',')
.map((type) => type.trim())
.filter(Boolean) ?? []
return [
'all',
...Object.keys(folders.value).filter(
(folder) => !customBlackList.includes(folder),
),
].map((type) => {
return { return {
label: type, label: type,
value: type, value: type,
@@ -179,6 +183,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) {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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) => {
@@ -40,8 +41,13 @@ declare module 'hooks/store' {
} }
} }
export const configSetting = {
excludeScanTypes: 'ModelManager.Scan.excludeScanTypes',
}
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 +85,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 +93,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 +149,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 +194,18 @@ function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
}, },
}) })
app.ui?.settings.addSetting({
id: configSetting.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
View 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),
}
}

View File

@@ -20,6 +20,7 @@ import {
unref, unref,
} from 'vue' } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { configSetting } from './config'
type ModelFolder = Record<string, string[]> type ModelFolder = Record<string, string[]>
@@ -55,8 +56,21 @@ 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 = {}
const excludeScanTypes = app.ui?.settings.getSettingValue<string>(
configSetting.excludeScanTypes,
)
const customBlackList =
excludeScanTypes
?.split(',')
.map((type) => type.trim())
.filter(Boolean) ?? []
return forceRefresh.then(() => return forceRefresh.then(() =>
Promise.allSettled(Object.keys(folders.value).map(refreshModels)), Promise.allSettled(
Object.keys(folders.value)
.filter((folder) => !customBlackList.includes(folder))
.map(refreshModels),
),
) )
} }

View File

@@ -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({

View File

@@ -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: {

View File

@@ -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
} }
} }

View File

@@ -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']
} }
} }

View File

@@ -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': {