Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e58d0a82d | ||
|
|
55a4eff01b | ||
|
|
45cf18299f | ||
|
|
c7898c47f1 | ||
|
|
17ab373b9c | ||
|
|
f6368fe20b | ||
|
|
92f2d5ab9e | ||
|
|
130c75f5bf | ||
|
|
921dabc057 | ||
|
|
ac21c8015d |
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm run build
|
pnpm run build
|
||||||
tar -czf dist.tar.gz py/ web/ __init__.py LICENSE pyproject.toml
|
tar -czf dist.tar.gz py/ web/ __init__.py LICENSE pyproject.toml requirements.txt
|
||||||
|
|
||||||
- name: Create release draft
|
- name: Create release draft
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class TaskContent:
|
|||||||
description: str
|
description: str
|
||||||
downloadPlatform: str
|
downloadPlatform: str
|
||||||
downloadUrl: str
|
downloadUrl: str
|
||||||
sizeBytes: int
|
sizeBytes: float
|
||||||
hashes: Optional[dict[str, str]] = None
|
hashes: Optional[dict[str, str]] = None
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@@ -71,7 +71,7 @@ class TaskContent:
|
|||||||
self.description = kwargs.get("description", None)
|
self.description = kwargs.get("description", None)
|
||||||
self.downloadPlatform = kwargs.get("downloadPlatform", None)
|
self.downloadPlatform = kwargs.get("downloadPlatform", None)
|
||||||
self.downloadUrl = kwargs.get("downloadUrl", None)
|
self.downloadUrl = kwargs.get("downloadUrl", None)
|
||||||
self.sizeBytes = int(kwargs.get("sizeBytes", 0))
|
self.sizeBytes = float(kwargs.get("sizeBytes", 0))
|
||||||
self.hashes = kwargs.get("hashes", None)
|
self.hashes = kwargs.get("hashes", None)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
@@ -103,6 +103,8 @@ def get_task_content(task_id: str):
|
|||||||
if not os.path.isfile(task_file):
|
if not os.path.isfile(task_file):
|
||||||
raise RuntimeError(f"Task {task_id} not found")
|
raise RuntimeError(f"Task {task_id} not found")
|
||||||
task_content = utils.load_dict_pickle_file(task_file)
|
task_content = utils.load_dict_pickle_file(task_file)
|
||||||
|
if isinstance(task_content, TaskContent):
|
||||||
|
return task_content
|
||||||
return TaskContent(**task_content)
|
return TaskContent(**task_content)
|
||||||
|
|
||||||
|
|
||||||
@@ -178,17 +180,18 @@ async def create_model_download_task(task_data: dict, request):
|
|||||||
task_path = utils.join_path(download_path, f"{task_id}.task")
|
task_path = utils.join_path(download_path, f"{task_id}.task")
|
||||||
if os.path.exists(task_path):
|
if os.path.exists(task_path):
|
||||||
raise RuntimeError(f"Task {task_id} already exists")
|
raise RuntimeError(f"Task {task_id} already exists")
|
||||||
|
download_platform = task_data.get("downloadPlatform", None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
previewFile = task_data.pop("previewFile", None)
|
preview_file = task_data.pop("previewFile", None)
|
||||||
utils.save_model_preview_image(task_path, previewFile)
|
utils.save_model_preview_image(task_path, preview_file, download_platform)
|
||||||
set_task_content(task_id, task_data)
|
set_task_content(task_id, task_data)
|
||||||
task_status = TaskStatus(
|
task_status = TaskStatus(
|
||||||
taskId=task_id,
|
taskId=task_id,
|
||||||
type=model_type,
|
type=model_type,
|
||||||
fullname=fullname,
|
fullname=fullname,
|
||||||
preview=utils.get_model_preview_name(task_path),
|
preview=utils.get_model_preview_name(task_path),
|
||||||
platform=task_data.get("downloadPlatform", None),
|
platform=download_platform,
|
||||||
totalSize=float(task_data.get("sizeBytes", 0)),
|
totalSize=float(task_data.get("sizeBytes", 0)),
|
||||||
)
|
)
|
||||||
download_model_task_status[task_id] = task_status
|
download_model_task_status[task_id] = task_status
|
||||||
@@ -379,7 +382,7 @@ async def download_model_file(
|
|||||||
# When parsing model information from HuggingFace API,
|
# When parsing model information from HuggingFace API,
|
||||||
# the file size was not found and needs to be obtained from the response header.
|
# the file size was not found and needs to be obtained from the response header.
|
||||||
if total_size == 0:
|
if total_size == 0:
|
||||||
total_size = int(response.headers.get("content-length", 0))
|
total_size = float(response.headers.get("content-length", 0))
|
||||||
task_content.sizeBytes = total_size
|
task_content.sizeBytes = total_size
|
||||||
task_status.totalSize = total_size
|
task_status.totalSize = total_size
|
||||||
set_task_content(task_id, task_content)
|
set_task_content(task_id, task_content)
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ class HuggingfaceModelSearcher(ModelSearcher):
|
|||||||
"pathIndex": 0,
|
"pathIndex": 0,
|
||||||
"description": "\n".join(description_parts),
|
"description": "\n".join(description_parts),
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"downloadPlatform": "",
|
"downloadPlatform": "huggingface",
|
||||||
"downloadUrl": f"https://huggingface.co/{model_id}/resolve/main/{filename}?download=true",
|
"downloadUrl": f"https://huggingface.co/{model_id}/resolve/main/{filename}?download=true",
|
||||||
}
|
}
|
||||||
models.append(model)
|
models.append(model)
|
||||||
@@ -386,7 +386,7 @@ class Information:
|
|||||||
model_base_paths = utils.resolve_model_base_paths()
|
model_base_paths = utils.resolve_model_base_paths()
|
||||||
for model_type in model_base_paths:
|
for model_type in model_base_paths:
|
||||||
|
|
||||||
folders, extensions = folder_paths.folder_names_and_paths[model_type]
|
folders, *others = folder_paths.folder_names_and_paths[model_type]
|
||||||
for path_index, base_path in enumerate(folders):
|
for path_index, base_path in enumerate(folders):
|
||||||
files = utils.recursive_search_files(base_path, request)
|
files = utils.recursive_search_files(base_path, request)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import folder_paths
|
import folder_paths
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
@@ -115,43 +116,57 @@ class ModelManager:
|
|||||||
def scan_models(self, folder: str, request):
|
def scan_models(self, folder: str, request):
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
folders, extensions = folder_paths.folder_names_and_paths[folder]
|
include_hidden_files = utils.get_setting_value(request, "scan.include_hidden_files", False)
|
||||||
|
folders, *others = folder_paths.folder_names_and_paths[folder]
|
||||||
|
|
||||||
|
def get_file_info(entry: os.DirEntry[str], base_path: str, path_index: int):
|
||||||
|
fullname = utils.normalize_path(entry.path).replace(f"{base_path}/", "")
|
||||||
|
basename = os.path.splitext(fullname)[0]
|
||||||
|
extension = os.path.splitext(fullname)[1]
|
||||||
|
|
||||||
|
if extension not in folder_paths.supported_pt_extensions:
|
||||||
|
return None
|
||||||
|
|
||||||
|
model_preview = f"/model-manager/preview/{folder}/{path_index}/{basename}.webp"
|
||||||
|
|
||||||
|
stat = entry.stat()
|
||||||
|
return {
|
||||||
|
"fullname": fullname,
|
||||||
|
"basename": basename,
|
||||||
|
"extension": extension,
|
||||||
|
"type": folder,
|
||||||
|
"pathIndex": path_index,
|
||||||
|
"sizeBytes": stat.st_size,
|
||||||
|
"preview": model_preview,
|
||||||
|
"createdAt": round(stat.st_ctime_ns / 1000000),
|
||||||
|
"updatedAt": round(stat.st_mtime_ns / 1000000),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_all_files_entry(directory: str):
|
||||||
|
files = []
|
||||||
|
with os.scandir(directory) as it:
|
||||||
|
for entry in it:
|
||||||
|
# Skip hidden files
|
||||||
|
if not include_hidden_files:
|
||||||
|
if entry.name.startswith("."):
|
||||||
|
continue
|
||||||
|
if entry.is_dir():
|
||||||
|
files.extend(get_all_files_entry(entry.path))
|
||||||
|
elif entry.is_file():
|
||||||
|
files.append(entry)
|
||||||
|
return files
|
||||||
|
|
||||||
for path_index, base_path in enumerate(folders):
|
for path_index, base_path in enumerate(folders):
|
||||||
files = utils.recursive_search_files(base_path, request)
|
if not os.path.exists(base_path):
|
||||||
|
continue
|
||||||
models = folder_paths.filter_files_extensions(files, folder_paths.supported_pt_extensions)
|
file_entries = get_all_files_entry(base_path)
|
||||||
|
with ThreadPoolExecutor() as executor:
|
||||||
for fullname in models:
|
futures = {executor.submit(get_file_info, entry, base_path, path_index): entry for entry in file_entries}
|
||||||
fullname = utils.normalize_path(fullname)
|
for future in as_completed(futures):
|
||||||
basename = os.path.splitext(fullname)[0]
|
file_info = future.result()
|
||||||
extension = os.path.splitext(fullname)[1]
|
if file_info is None:
|
||||||
|
continue
|
||||||
abs_path = utils.join_path(base_path, fullname)
|
result.append(file_info)
|
||||||
file_stats = os.stat(abs_path)
|
|
||||||
|
|
||||||
# Resolve preview
|
|
||||||
image_name = utils.get_model_preview_name(abs_path)
|
|
||||||
image_name = utils.join_path(os.path.dirname(fullname), image_name)
|
|
||||||
abs_image_path = utils.join_path(base_path, image_name)
|
|
||||||
if os.path.isfile(abs_image_path):
|
|
||||||
image_state = os.stat(abs_image_path)
|
|
||||||
image_timestamp = round(image_state.st_mtime_ns / 1000000)
|
|
||||||
image_name = f"{image_name}?ts={image_timestamp}"
|
|
||||||
model_preview = f"/model-manager/preview/{folder}/{path_index}/{image_name}"
|
|
||||||
|
|
||||||
model_info = {
|
|
||||||
"fullname": fullname,
|
|
||||||
"basename": basename,
|
|
||||||
"extension": extension,
|
|
||||||
"type": folder,
|
|
||||||
"pathIndex": path_index,
|
|
||||||
"sizeBytes": file_stats.st_size,
|
|
||||||
"preview": model_preview,
|
|
||||||
"createdAt": round(file_stats.st_ctime_ns / 1000000),
|
|
||||||
"updatedAt": round(file_stats.st_mtime_ns / 1000000),
|
|
||||||
}
|
|
||||||
|
|
||||||
result.append(model_info)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
10
py/utils.py
10
py/utils.py
@@ -277,10 +277,9 @@ def remove_model_preview_image(model_path: str):
|
|||||||
os.remove(preview_path)
|
os.remove(preview_path)
|
||||||
|
|
||||||
|
|
||||||
def save_model_preview_image(model_path: str, image_file_or_url: Any):
|
def save_model_preview_image(model_path: str, image_file_or_url: Any, platform: str | None = None):
|
||||||
basename = os.path.splitext(model_path)[0]
|
basename = os.path.splitext(model_path)[0]
|
||||||
preview_path = f"{basename}.webp"
|
preview_path = f"{basename}.webp"
|
||||||
|
|
||||||
# Download image file if it is url
|
# Download image file if it is url
|
||||||
if type(image_file_or_url) is str:
|
if type(image_file_or_url) is str:
|
||||||
image_url = image_file_or_url
|
image_url = image_file_or_url
|
||||||
@@ -304,8 +303,11 @@ def save_model_preview_image(model_path: str, image_file_or_url: Any):
|
|||||||
|
|
||||||
content_type: str = image_file.content_type
|
content_type: str = image_file.content_type
|
||||||
if not content_type.startswith("image/"):
|
if not content_type.startswith("image/"):
|
||||||
raise RuntimeError(f"FileTypeError: expected image, got {content_type}")
|
if platform == "huggingface":
|
||||||
|
# huggingface previewFile content_type='text/plain', not startswith("image/")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"FileTypeError: expected image, got {content_type}")
|
||||||
image = Image.open(image_file.file)
|
image = Image.open(image_file.file)
|
||||||
image.save(preview_path, "WEBP")
|
image.save(preview_path, "WEBP")
|
||||||
|
|
||||||
|
|||||||
@@ -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.3.0"
|
version = "2.3.3"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
dependencies = ["markdownify"]
|
dependencies = ["markdownify"]
|
||||||
|
|
||||||
|
|||||||
@@ -9,25 +9,26 @@
|
|||||||
>
|
>
|
||||||
<div ref="toolbarContainer" class="col-span-full">
|
<div ref="toolbarContainer" class="col-span-full">
|
||||||
<div :class="['flex gap-4', $toolbar_2xl('flex-row', 'flex-col')]">
|
<div :class="['flex gap-4', $toolbar_2xl('flex-row', 'flex-col')]">
|
||||||
<ResponseInput
|
<div class="flex-1">
|
||||||
v-model="searchContent"
|
<ResponseInput
|
||||||
:placeholder="$t('searchModels')"
|
v-model="searchContent"
|
||||||
:allow-clear="true"
|
:placeholder="$t('searchModels')"
|
||||||
suffix-icon="pi pi-search"
|
:allow-clear="true"
|
||||||
></ResponseInput>
|
suffix-icon="pi pi-search"
|
||||||
|
></ResponseInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-4 overflow-hidden">
|
<div class="flex items-center justify-between gap-4 overflow-hidden">
|
||||||
<ResponseSelect
|
<ResponseSelect
|
||||||
v-model="currentType"
|
v-model="currentType"
|
||||||
:items="typeOptions"
|
:items="typeOptions"
|
||||||
:type="isMobile ? 'drop' : 'button'"
|
|
||||||
></ResponseSelect>
|
></ResponseSelect>
|
||||||
<ResponseSelect
|
<ResponseSelect
|
||||||
v-model="sortOrder"
|
v-model="sortOrder"
|
||||||
:items="sortOrderOptions"
|
:items="sortOrderOptions"
|
||||||
></ResponseSelect>
|
></ResponseSelect>
|
||||||
<ResponseSelect
|
<ResponseSelect
|
||||||
v-model="currentCardSize"
|
v-model="cardSizeFlag"
|
||||||
:items="cardSizeOptions"
|
:items="cardSizeOptions"
|
||||||
></ResponseSelect>
|
></ResponseSelect>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,7 +77,14 @@ import { genModelKey } from 'utils/model'
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const { isMobile, gutter, cardSize } = useConfig()
|
const {
|
||||||
|
isMobile,
|
||||||
|
gutter,
|
||||||
|
cardSize,
|
||||||
|
cardSizeMap,
|
||||||
|
cardSizeFlag,
|
||||||
|
dialog: settings,
|
||||||
|
} = useConfig()
|
||||||
|
|
||||||
const { data, folders } = useModels()
|
const { data, folders } = useModels()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -197,48 +205,24 @@ const contentStyle = computed(() => ({
|
|||||||
paddingRight: `1rem`,
|
paddingRight: `1rem`,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const currentCardSize = computed({
|
|
||||||
get: () => {
|
|
||||||
const options = cardSizeOptions.value.map((item) => item.value)
|
|
||||||
const current = [cardSize.value.width, cardSize.value.height].join('x')
|
|
||||||
if (options.includes(current)) {
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
return 'custom'
|
|
||||||
},
|
|
||||||
set: (val) => {
|
|
||||||
if (val === 'custom') {
|
|
||||||
app.ui?.settings.show(t('size.customTip'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const [width, height] = val.split('x')
|
|
||||||
app.ui?.settings.setSettingValue(
|
|
||||||
'ModelManager.UI.CardWidth',
|
|
||||||
parseInt(width),
|
|
||||||
)
|
|
||||||
app.ui?.settings.setSettingValue(
|
|
||||||
'ModelManager.UI.CardHeight',
|
|
||||||
parseInt(height),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const cardSizeOptions = computed(() => {
|
const cardSizeOptions = computed(() => {
|
||||||
const defineOptions = {
|
const customSize = 'size.custom'
|
||||||
extraLarge: '240x320',
|
|
||||||
large: '180x240',
|
const customOptionMap = {
|
||||||
medium: '120x160',
|
...cardSizeMap.value,
|
||||||
small: '80x120',
|
[customSize]: 'custom',
|
||||||
custom: 'custom',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.entries(defineOptions).map(([key, value]) => {
|
return Object.keys(customOptionMap).map((key) => {
|
||||||
return {
|
return {
|
||||||
label: t(`size.${key}`),
|
label: t(key),
|
||||||
value,
|
value: key,
|
||||||
command() {
|
command: () => {
|
||||||
currentCardSize.value = value
|
if (key === customSize) {
|
||||||
|
settings.showCardSizeSetting()
|
||||||
|
} else {
|
||||||
|
cardSizeFlag.value = key
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
110
src/components/SettingCardSize.vue
Normal file
110
src/components/SettingCardSize.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full flex-col">
|
||||||
|
<div class="flex-1 px-4">
|
||||||
|
<DataTable :value="sizeList">
|
||||||
|
<Column field="name" :header="$t('name')">
|
||||||
|
<template #body="{ data, field }">
|
||||||
|
{{ $t(data[field]) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="width" :header="$t('width')" class="min-w-36">
|
||||||
|
<template #body="{ data, field }">
|
||||||
|
<span class="flex items-center gap-4">
|
||||||
|
<Slider
|
||||||
|
v-model="data[field]"
|
||||||
|
class="flex-1"
|
||||||
|
v-bind="sizeStint"
|
||||||
|
></Slider>
|
||||||
|
<span>{{ data[field] }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="height" :header="$t('height')" class="min-w-36">
|
||||||
|
<template #body="{ data, field }">
|
||||||
|
<span class="flex items-center gap-4">
|
||||||
|
<Slider
|
||||||
|
v-model="data[field]"
|
||||||
|
class="flex-1"
|
||||||
|
v-bind="sizeStint"
|
||||||
|
></Slider>
|
||||||
|
<span>{{ data[field] }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between px-4">
|
||||||
|
<div></div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
icon="pi pi-refresh"
|
||||||
|
:label="$t('reset')"
|
||||||
|
@click="handleReset"
|
||||||
|
></Button>
|
||||||
|
<Button :label="$t('cancel')" @click="handleCancelEditor"></Button>
|
||||||
|
<Button :label="$t('save')" @click="handleSaveSizeMap"></Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useConfig } from 'hooks/config'
|
||||||
|
import { useDialog } from 'hooks/dialog'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import Column from 'primevue/column'
|
||||||
|
import DataTable from 'primevue/datatable'
|
||||||
|
import Slider from 'primevue/slider'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const { cardSizeMap, defaultCardSizeMap } = useConfig()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
const sizeList = ref()
|
||||||
|
|
||||||
|
const sizeStint = {
|
||||||
|
step: 10,
|
||||||
|
min: 80,
|
||||||
|
max: 320,
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveSizeMap = (sizeMap: Record<string, string>) => {
|
||||||
|
return Object.entries(sizeMap).map(([key, value]) => {
|
||||||
|
const [width, height] = value.split('x')
|
||||||
|
return {
|
||||||
|
id: key,
|
||||||
|
name: key,
|
||||||
|
width: parseInt(width),
|
||||||
|
height: parseInt(height),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveSizeList = (
|
||||||
|
sizeList: { name: string; width: number; height: number }[],
|
||||||
|
) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
sizeList.map(({ name, width, height }) => {
|
||||||
|
return [name, [width, height].join('x')]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
sizeList.value = resolveSizeMap(cardSizeMap.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
sizeList.value = resolveSizeMap(defaultCardSizeMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancelEditor = () => {
|
||||||
|
sizeList.value = resolveSizeMap(cardSizeMap.value)
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveSizeMap = () => {
|
||||||
|
cardSizeMap.value = resolveSizeList(sizeList.value)
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
|
import SettingCardSize from 'components/SettingCardSize.vue'
|
||||||
import { request } from 'hooks/request'
|
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 { computed, onMounted, onUnmounted, readonly, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
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) => {
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const mobileDeviceBreakPoint = 759
|
const mobileDeviceBreakPoint = 759
|
||||||
const isMobile = ref(window.innerWidth < mobileDeviceBreakPoint)
|
const isMobile = ref(window.innerWidth < mobileDeviceBreakPoint)
|
||||||
|
|
||||||
@@ -21,23 +24,59 @@ export const useConfig = defineStore('config', (store) => {
|
|||||||
window.removeEventListener('resize', checkDeviceType)
|
window.removeEventListener('resize', checkDeviceType)
|
||||||
})
|
})
|
||||||
|
|
||||||
const cardSize = ref({
|
const defaultCardSizeMap = readonly({
|
||||||
width:
|
'size.extraLarge': '240x320',
|
||||||
app.ui?.settings.getSettingValue<number>('ModelManager.UI.CardWidth') ??
|
'size.large': '180x240',
|
||||||
240,
|
'size.medium': '120x160',
|
||||||
height:
|
'size.small': '80x120',
|
||||||
app.ui?.settings.getSettingValue<number>('ModelManager.UI.CardHeight') ??
|
})
|
||||||
310,
|
|
||||||
|
const cardSizeMap = ref<Record<string, string>>({ ...defaultCardSizeMap })
|
||||||
|
const cardSizeFlag = ref('size.extraLarge')
|
||||||
|
const cardSize = computed(() => {
|
||||||
|
const size = cardSizeMap.value[cardSizeFlag.value]
|
||||||
|
const [width = '120', height = '240'] = size.split('x')
|
||||||
|
return {
|
||||||
|
width: parseInt(width),
|
||||||
|
height: parseInt(height),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
isMobile,
|
isMobile,
|
||||||
gutter: 16,
|
gutter: 16,
|
||||||
cardSize,
|
defaultCardSizeMap: defaultCardSizeMap,
|
||||||
|
cardSizeMap: cardSizeMap,
|
||||||
|
cardSizeFlag: cardSizeFlag,
|
||||||
|
cardSize: cardSize,
|
||||||
cardWidth: 240,
|
cardWidth: 240,
|
||||||
aspect: 7 / 9,
|
aspect: 7 / 9,
|
||||||
|
dialog: {
|
||||||
|
showCardSizeSetting: () => {
|
||||||
|
store.dialog.open({
|
||||||
|
key: 'setting.cardSize',
|
||||||
|
title: t('setting.cardSize'),
|
||||||
|
content: SettingCardSize,
|
||||||
|
defaultSize: {
|
||||||
|
width: 500,
|
||||||
|
height: 390,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(cardSizeFlag, (val) => {
|
||||||
|
app.ui?.settings.setSettingValue('ModelManager.UI.CardSize', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(cardSizeMap, (val) => {
|
||||||
|
app.ui?.settings.setSettingValue(
|
||||||
|
'ModelManager.UI.CardSizeMap',
|
||||||
|
JSON.stringify(val),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
useAddConfigSettings(store)
|
useAddConfigSettings(store)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
@@ -109,36 +148,27 @@ function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
|||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
// UI settings
|
const defaultCardSize = store.config.defaultCardSizeMap
|
||||||
|
|
||||||
app.ui?.settings.addSetting({
|
app.ui?.settings.addSetting({
|
||||||
id: 'ModelManager.UI.CardWidth',
|
id: 'ModelManager.UI.CardSize',
|
||||||
category: [t('modelManager'), t('setting.ui'), 'CardWidth'],
|
category: [t('modelManager'), t('setting.ui'), 'CardSize'],
|
||||||
name: t('setting.cardWidth'),
|
name: t('setting.cardSize'),
|
||||||
type: 'slider',
|
defaultValue: 'size.extraLarge',
|
||||||
defaultValue: 240,
|
type: 'hidden',
|
||||||
attrs: {
|
onChange: (val) => {
|
||||||
min: 80,
|
store.config.cardSizeFlag.value = val
|
||||||
max: 320,
|
|
||||||
step: 10,
|
|
||||||
},
|
|
||||||
onChange(value) {
|
|
||||||
store.config.cardSize.value.width = value
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
app.ui?.settings.addSetting({
|
app.ui?.settings.addSetting({
|
||||||
id: 'ModelManager.UI.CardHeight',
|
id: 'ModelManager.UI.CardSizeMap',
|
||||||
category: [t('modelManager'), t('setting.ui'), 'CardHeight'],
|
category: [t('modelManager'), t('setting.ui'), 'CardSizeMap'],
|
||||||
name: t('setting.cardHeight'),
|
name: t('setting.cardSize'),
|
||||||
type: 'slider',
|
defaultValue: JSON.stringify(defaultCardSize),
|
||||||
defaultValue: 320,
|
type: 'hidden',
|
||||||
attrs: {
|
|
||||||
min: 80,
|
|
||||||
max: 320,
|
|
||||||
step: 10,
|
|
||||||
},
|
|
||||||
onChange(value) {
|
onChange(value) {
|
||||||
store.config.cardSize.value.height = value
|
store.config.cardSizeMap.value = JSON.parse(value)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
10
src/i18n.ts
10
src/i18n.ts
@@ -25,6 +25,10 @@ const messages = {
|
|||||||
none: 'None',
|
none: 'None',
|
||||||
uploadFile: 'Upload File',
|
uploadFile: 'Upload File',
|
||||||
tapToChange: 'Tap description to change content',
|
tapToChange: 'Tap description to change content',
|
||||||
|
name: 'Name',
|
||||||
|
width: 'Width',
|
||||||
|
height: 'Height',
|
||||||
|
reset: 'Reset',
|
||||||
sort: {
|
sort: {
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
size: 'Largest',
|
size: 'Largest',
|
||||||
@@ -57,6 +61,7 @@ const messages = {
|
|||||||
includeHiddenFiles: 'Include hidden files(start with .)',
|
includeHiddenFiles: 'Include hidden files(start with .)',
|
||||||
excludeScanTypes: 'Exclude scan types (separate with commas)',
|
excludeScanTypes: 'Exclude scan types (separate with commas)',
|
||||||
ui: 'UI',
|
ui: 'UI',
|
||||||
|
cardSize: 'Card Size',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
@@ -82,6 +87,10 @@ const messages = {
|
|||||||
none: '无',
|
none: '无',
|
||||||
uploadFile: '上传文件',
|
uploadFile: '上传文件',
|
||||||
tapToChange: '点击描述可更改内容',
|
tapToChange: '点击描述可更改内容',
|
||||||
|
name: '名称',
|
||||||
|
width: '宽度',
|
||||||
|
height: '高度',
|
||||||
|
reset: '重置',
|
||||||
sort: {
|
sort: {
|
||||||
name: '名称',
|
name: '名称',
|
||||||
size: '最大',
|
size: '最大',
|
||||||
@@ -114,6 +123,7 @@ const messages = {
|
|||||||
includeHiddenFiles: '包含隐藏文件(以 . 开头的文件或文件夹)',
|
includeHiddenFiles: '包含隐藏文件(以 . 开头的文件或文件夹)',
|
||||||
excludeScanTypes: '排除扫描类型(使用英文逗号隔开)',
|
excludeScanTypes: '排除扫描类型(使用英文逗号隔开)',
|
||||||
ui: '外观',
|
ui: '外观',
|
||||||
|
cardSize: '卡片尺寸',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user