8 Commits

Author SHA1 Message Date
Hayden
1ba80fab2e prepare release 2.2.3 2025-01-16 10:24:42 +08:00
Hayden
b9e637049a fix: Inability to Scroll model dir list (#101) 2025-01-16 10:23:51 +08:00
Hayden
bfccc6f04f fix: can't change or delete preview (#100) 2025-01-15 16:48:41 +08:00
Hayden
89c249542a fix: cant't close create task dialog (#98) 2025-01-15 16:11:21 +08:00
Hayden
136bc0ecd5 feat(dialog): Optimize dialog closing logic (#97)
- Add optional parameters to the close function to support parameterless calling
- When the dialog parameter is not provided, automatically close the dialog box on the top of the stack
2025-01-15 16:03:46 +08:00
Hayden
8653af1f14 fix: misplaced preview buttons (#96) 2025-01-14 16:05:11 +08:00
Hayden
354b5c840a feat: allow multi create task dialog (#95) 2025-01-14 11:27:05 +08:00
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
18 changed files with 203 additions and 73 deletions

View File

@@ -95,7 +95,7 @@ async def get_model_paths(request):
""" """
Returns the base folders for models. Returns the base folders for models.
""" """
model_base_paths = utils.resolve_model_base_paths(request) model_base_paths = utils.resolve_model_base_paths()
return web.json_response({"success": True, "data": model_base_paths}) return web.json_response({"success": True, "data": model_base_paths})
@@ -114,7 +114,8 @@ async def create_model(request):
- downloadUrl: download url. - downloadUrl: download url.
- hash: a JSON string containing the hash value of the downloaded model. - hash: a JSON string containing the hash value of the downloaded model.
""" """
task_data = await request.json() task_data = await request.post()
task_data = dict(task_data)
try: try:
task_id = await services.create_model_download_task(task_data, request) task_id = await services.create_model_download_task(task_data, request)
return web.json_response({"success": True, "data": {"taskId": task_id}}) return web.json_response({"success": True, "data": {"taskId": task_id}})
@@ -130,7 +131,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(request) result = utils.resolve_model_base_paths()
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)}"
@@ -160,7 +161,7 @@ async def read_model_info(request):
filename = request.match_info.get("filename", None) filename = request.match_info.get("filename", None)
try: try:
model_path = utils.get_valid_full_path(model_type, index, filename, request) model_path = utils.get_valid_full_path(model_type, index, filename)
result = services.get_model_info(model_path) result = services.get_model_info(model_path)
return web.json_response({"success": True, "data": result}) return web.json_response({"success": True, "data": result})
except Exception as e: except Exception as e:
@@ -186,13 +187,14 @@ async def update_model(request):
index = int(request.match_info.get("index", None)) index = int(request.match_info.get("index", None))
filename = request.match_info.get("filename", None) filename = request.match_info.get("filename", None)
model_data: dict = await request.json() model_data = await request.post()
model_data = dict(model_data)
try: try:
model_path = utils.get_valid_full_path(model_type, index, filename, request) model_path = utils.get_valid_full_path(model_type, index, filename)
if model_path is None: if model_path is None:
raise RuntimeError(f"File {filename} not found") raise RuntimeError(f"File {filename} not found")
services.update_model(model_path, model_data, request) services.update_model(model_path, model_data)
return web.json_response({"success": True}) return web.json_response({"success": True})
except Exception as e: except Exception as e:
error_msg = f"Update model failed: {str(e)}" error_msg = f"Update model failed: {str(e)}"
@@ -210,7 +212,7 @@ async def delete_model(request):
filename = request.match_info.get("filename", None) filename = request.match_info.get("filename", None)
try: try:
model_path = utils.get_valid_full_path(model_type, index, filename, request) model_path = utils.get_valid_full_path(model_type, index, filename)
if model_path is None: if model_path is None:
raise RuntimeError(f"File {filename} not found") raise RuntimeError(f"File {filename} not found")
services.remove_model(model_path) services.remove_model(model_path)

View File

@@ -12,8 +12,7 @@ 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",
}, },
} }

View File

@@ -167,7 +167,7 @@ async def create_model_download_task(task_data: dict, request):
path_index = int(task_data.get("pathIndex", None)) path_index = int(task_data.get("pathIndex", None))
fullname = task_data.get("fullname", None) fullname = task_data.get("fullname", None)
model_path = utils.get_full_path(model_type, path_index, fullname, request) model_path = utils.get_full_path(model_type, path_index, fullname)
# Check if the model path is valid # Check if the model path is valid
if os.path.exists(model_path): if os.path.exists(model_path):
raise RuntimeError(f"File already exists: {model_path}") raise RuntimeError(f"File already exists: {model_path}")
@@ -180,8 +180,8 @@ async def create_model_download_task(task_data: dict, request):
raise RuntimeError(f"Task {task_id} already exists") raise RuntimeError(f"Task {task_id} already exists")
try: try:
preview_url = task_data.pop("preview", None) previewFile = task_data.pop("previewFile", None)
utils.save_model_preview_image(task_path, preview_url) utils.save_model_preview_image(task_path, previewFile)
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,
@@ -261,7 +261,6 @@ async def download_model(task_id: str, request):
progress_interval = 1.0 progress_interval = 1.0
await download_model_file( await download_model_file(
request,
task_id=task_id, task_id=task_id,
headers=headers, headers=headers,
progress_callback=report_progress, progress_callback=report_progress,
@@ -289,7 +288,6 @@ async def download_model(task_id: str, request):
async def download_model_file( async def download_model_file(
request,
task_id: str, task_id: str,
headers: dict, headers: dict,
progress_callback: Callable[[TaskStatus], Awaitable[Any]], progress_callback: Callable[[TaskStatus], Awaitable[Any]],
@@ -310,7 +308,7 @@ async def download_model_file(
with open(description_file, "w", encoding="utf-8", newline="") as f: with open(description_file, "w", encoding="utf-8", newline="") as f:
f.write(description) f.write(description)
model_path = utils.get_full_path(model_type, path_index, fullname, request) model_path = utils.get_full_path(model_type, path_index, fullname)
utils.rename_model(download_tmp_file, model_path) utils.rename_model(download_tmp_file, model_path)

View File

@@ -69,11 +69,14 @@ def get_model_info(model_path: str):
} }
def update_model(model_path: str, model_data: dict, request): def update_model(model_path: str, model_data: dict):
if "previewFile" in model_data: if "previewFile" in model_data:
previewFile = model_data["previewFile"] previewFile = model_data["previewFile"]
utils.save_model_preview_image(model_path, previewFile) if type(previewFile) is str and previewFile == "undefined":
utils.remove_model_preview_image(model_path)
else:
utils.save_model_preview_image(model_path, previewFile)
if "description" in model_data: if "description" in model_data:
description = model_data["description"] description = model_data["description"]
@@ -87,7 +90,7 @@ def update_model(model_path: str, model_data: dict, request):
raise RuntimeError("Invalid type or pathIndex or fullname") raise RuntimeError("Invalid type or pathIndex or fullname")
# get new path # get new path
new_model_path = utils.get_full_path(model_type, path_index, fullname, request) new_model_path = utils.get_full_path(model_type, path_index, fullname)
utils.rename_model(model_path, new_model_path) utils.rename_model(model_path, new_model_path)
@@ -136,7 +139,7 @@ def fetch_model_info(model_page: str):
async def download_model_info(scan_mode: str, request): async def download_model_info(scan_mode: str, request):
utils.print_info(f"Download model info for {scan_mode}") utils.print_info(f"Download model info for {scan_mode}")
model_base_paths = utils.resolve_model_base_paths(request) 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, extensions = folder_paths.folder_names_and_paths[model_type]

View File

@@ -116,13 +116,10 @@ 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(request): def resolve_model_base_paths():
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
@@ -131,11 +128,11 @@ def resolve_model_base_paths(request):
return model_base_paths return model_base_paths
def get_full_path(model_type: str, path_index: int, filename: str, request): def get_full_path(model_type: str, path_index: int, filename: str):
""" """
Get the absolute path in the model type through string concatenation. Get the absolute path in the model type through string concatenation.
""" """
folders = resolve_model_base_paths(request).get(model_type, []) folders = resolve_model_base_paths().get(model_type, [])
if not path_index < len(folders): if not path_index < len(folders):
raise RuntimeError(f"PathIndex {path_index} is not in {model_type}") raise RuntimeError(f"PathIndex {path_index} is not in {model_type}")
base_path = folders[path_index] base_path = folders[path_index]
@@ -143,11 +140,11 @@ def get_full_path(model_type: str, path_index: int, filename: str, request):
return full_path return full_path
def get_valid_full_path(model_type: str, path_index: int, filename: str, request): def get_valid_full_path(model_type: str, path_index: int, filename: str):
""" """
Like get_full_path but it will check whether the file is valid. Like get_full_path but it will check whether the file is valid.
""" """
folders = resolve_model_base_paths(request).get(model_type, []) folders = resolve_model_base_paths().get(model_type, [])
if not path_index < len(folders): if not path_index < len(folders):
raise RuntimeError(f"PathIndex {path_index} is not in {model_type}") raise RuntimeError(f"PathIndex {path_index} is not in {model_type}")
base_path = folders[path_index] base_path = folders[path_index]
@@ -252,19 +249,45 @@ from PIL import Image
from io import BytesIO from io import BytesIO
def save_model_preview_image(model_path: str, image_url: str): def remove_model_preview_image(model_path: str):
try: basename = os.path.splitext(model_path)[0]
image_response = requests.get(image_url) preview_path = f"{basename}.webp"
image_response.raise_for_status() if os.path.exists(preview_path):
os.remove(preview_path)
basename = os.path.splitext(model_path)[0]
preview_path = f"{basename}.webp" def save_model_preview_image(model_path: str, image_file_or_url: Any):
image = Image.open(BytesIO(image_response.content)) basename = os.path.splitext(model_path)[0]
preview_path = f"{basename}.webp"
# Download image file if it is url
if type(image_file_or_url) is str:
image_url = image_file_or_url
try:
image_response = requests.get(image_url)
image_response.raise_for_status()
image = Image.open(BytesIO(image_response.content))
image.save(preview_path, "WEBP")
except Exception as e:
print_error(f"Failed to download image: {e}")
else:
# Assert image as file
image_file = image_file_or_url
if not isinstance(image_file, web.FileField):
raise RuntimeError("Invalid image file")
content_type: str = image_file.content_type
if not content_type.startswith("image/"):
raise RuntimeError(f"FileTypeError: expected image, got {content_type}")
image = Image.open(image_file.file)
image.save(preview_path, "WEBP") image.save(preview_path, "WEBP")
except Exception as e:
print_error(f"Failed to download image: {e}")
def get_model_all_descriptions(model_path: str): def get_model_all_descriptions(model_path: str):
base_dirname = os.path.dirname(model_path) base_dirname = os.path.dirname(model_path)

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.2.1" version = "2.2.3"
license = { file = "LICENSE" } license = { file = "LICENSE" }
dependencies = ["markdownify"] dependencies = ["markdownify"]

View File

@@ -69,7 +69,8 @@ import { useLoading } from 'hooks/loading'
import { request } from 'hooks/request' import { request } from 'hooks/request'
import { useToast } from 'hooks/toast' import { useToast } from 'hooks/toast'
import Button from 'primevue/button' import Button from 'primevue/button'
import { VersionModel } from 'types/typings' import { VersionModel, WithResolved } from 'types/typings'
import { previewUrlToFile } from 'utils/common'
import { ref } from 'vue' import { ref } from 'vue'
const { isMobile } = useConfig() const { isMobile } = useConfig()
@@ -87,15 +88,52 @@ const searchModelsByUrl = async () => {
} }
} }
const createDownTask = async (data: VersionModel) => { const createDownTask = async (data: WithResolved<VersionModel>) => {
loading.show() loading.show()
const formData = new FormData()
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
let value = data[key]
// set preview file
if (key === 'preview') {
if (value) {
const previewFile = await previewUrlToFile(value).catch(() => {
loading.hide()
toast.add({
severity: 'error',
summary: 'Error',
detail: 'Failed to download preview',
life: 5000,
})
throw new Error('Failed to download preview')
})
formData.append('previewFile', previewFile)
} else {
formData.append('previewFile', value)
}
continue
}
if (typeof value === 'object') {
value = JSON.stringify(value)
}
if (typeof value === 'number') {
value = value.toString()
}
formData.append(key, value)
}
}
await request('/model', { await request('/model', {
method: 'POST', method: 'POST',
body: JSON.stringify(data), body: formData,
}) })
.then(() => { .then(() => {
dialog.close({ key: 'model-manager-create-task' }) dialog.close()
}) })
.catch((e) => { .catch((e) => {
toast.add({ toast.add({

View File

@@ -86,7 +86,7 @@ const dialog = useDialog()
const openCreateTask = () => { const openCreateTask = () => {
dialog.open({ dialog.open({
key: 'model-manager-create-task', key: `model-manager-create-task-${Date.now()}`,
title: t('parseModelUrl'), title: t('parseModelUrl'),
content: DialogCreateTask, content: DialogCreateTask,
}) })

View File

@@ -68,11 +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 { 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'
@@ -89,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,

View File

@@ -47,7 +47,7 @@ import ResponseScroll from 'components/ResponseScroll.vue'
import { useModelNodeAction, useModels } from 'hooks/model' import { useModelNodeAction, useModels } from 'hooks/model'
import { useRequest } from 'hooks/request' import { useRequest } from 'hooks/request'
import Button from 'primevue/button' import Button from 'primevue/button'
import { BaseModel, Model } from 'types/typings' import { BaseModel, Model, WithResolved } from 'types/typings'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
interface Props { interface Props {
@@ -72,7 +72,7 @@ const handleCancel = () => {
editable.value = false editable.value = false
} }
const handleSave = async (data: BaseModel) => { const handleSave = async (data: WithResolved<BaseModel>) => {
await update(modelContent.value, data) await update(modelContent.value, data)
editable.value = false editable.value = false
} }

View File

@@ -62,7 +62,7 @@ import TabList from 'primevue/tablist'
import TabPanel from 'primevue/tabpanel' import TabPanel from 'primevue/tabpanel'
import TabPanels from 'primevue/tabpanels' import TabPanels from 'primevue/tabpanels'
import Tabs from 'primevue/tabs' import Tabs from 'primevue/tabs'
import { BaseModel } from 'types/typings' import { BaseModel, WithResolved } from 'types/typings'
import { toRaw, watch } from 'vue' import { toRaw, watch } from 'vue'
interface Props { interface Props {
@@ -73,7 +73,7 @@ const props = defineProps<Props>()
const editable = defineModel<boolean>('editable') const editable = defineModel<boolean>('editable')
const emits = defineEmits<{ const emits = defineEmits<{
submit: [formData: BaseModel] submit: [formData: WithResolved<BaseModel>]
reset: [] reset: []
}>() }>()

View File

@@ -44,9 +44,8 @@
<div class="h-10"></div> <div class="h-10"></div>
<div <div
:class="[ :class="[
'flex h-10 items-center gap-4', 'absolute flex h-10 items-center gap-4',
'absolute left-1/2 -translate-x-1/2', $xl('left-0 translate-x-0', 'left-1/2 -translate-x-1/2'),
$xl('left-0 translate-x-0'),
]" ]"
> >
<Button <Button

View File

@@ -130,7 +130,13 @@
<slot v-else name="desktop"> <slot v-else name="desktop">
<slot name="container"> <slot name="container">
<slot name="desktop:container"> <slot name="desktop:container">
<Menu ref="menu" :model="items" :popup="true" :base-z-index="1000"> <Menu
ref="menu"
:model="items"
:popup="true"
:base-z-index="1000"
:pt:root:style="{ maxHeight: '300px', overflowX: 'hidden' }"
>
<template #item="{ item }"> <template #item="{ item }">
<slot name="item" :item="item"> <slot name="item" :item="item">
<slot name="desktop:container:item" :item="item"> <slot name="desktop:container:item" :item="item">

View File

@@ -41,6 +41,10 @@ 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 { t } = useI18n()
@@ -191,7 +195,7 @@ function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
}) })
app.ui?.settings.addSetting({ app.ui?.settings.addSetting({
id: 'ModelManager.Scan.excludeScanTypes', id: configSetting.excludeScanTypes,
category: [t('modelManager'), t('setting.scan'), 'ExcludeScanTypes'], category: [t('modelManager'), t('setting.scan'), 'ExcludeScanTypes'],
name: t('setting.excludeScanTypes'), name: t('setting.excludeScanTypes'),
defaultValue: undefined, defaultValue: undefined,

View File

@@ -49,7 +49,12 @@ export const useDialog = defineStore('dialog', () => {
} }
} }
const close = (dialog: { key: string }) => { const close = (dialog?: { key: string }) => {
if (!dialog) {
stack.value.pop()
return
}
const item = stack.value.find((item) => item.key === dialog.key) const item = stack.value.find((item) => item.key === dialog.key)
if (item?.keepAlive) { if (item?.keepAlive) {
item.visible = false item.visible = false

View File

@@ -3,10 +3,10 @@ import { useMarkdown } from 'hooks/markdown'
import { request } from 'hooks/request' import { request } from 'hooks/request'
import { defineStore } from 'hooks/store' import { defineStore } from 'hooks/store'
import { useToast } from 'hooks/toast' import { useToast } from 'hooks/toast'
import { cloneDeep } from 'lodash' import { castArray, cloneDeep } from 'lodash'
import { app } from 'scripts/comfyAPI' import { app } from 'scripts/comfyAPI'
import { BaseModel, Model, SelectEvent } from 'types/typings' import { BaseModel, Model, SelectEvent, WithResolved } from 'types/typings'
import { bytesToSize, formatDate } from 'utils/common' import { bytesToSize, formatDate, previewUrlToFile } from 'utils/common'
import { ModelGrid } from 'utils/legacy' import { ModelGrid } from 'utils/legacy'
import { genModelKey, resolveModelTypeLoader } from 'utils/model' import { genModelKey, resolveModelTypeLoader } from 'utils/model'
import { import {
@@ -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[]>
@@ -56,23 +57,47 @@ 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 = {} 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),
),
) )
} }
const updateModel = async (model: BaseModel, data: BaseModel) => { const updateModel = async (
const updateData = new Map() model: BaseModel,
data: WithResolved<BaseModel>,
) => {
const updateData = new FormData()
let oldKey: string | null = null let oldKey: string | null = null
let needUpdate = false
// Check current preview // Check current preview
if (model.preview !== data.preview) { if (model.preview !== data.preview) {
updateData.set('previewFile', data.preview) const preview = data.preview
if (preview) {
const previewFile = await previewUrlToFile(data.preview as string)
updateData.set('previewFile', previewFile)
} else {
updateData.set('previewFile', 'undefined')
}
needUpdate = true
} }
// Check current description // Check current description
if (model.description !== data.description) { if (model.description !== data.description) {
updateData.set('description', data.description) updateData.set('description', data.description)
needUpdate = true
} }
// Check current name and pathIndex // Check current name and pathIndex
@@ -84,16 +109,17 @@ export const useModels = defineStore('models', (store) => {
updateData.set('type', data.type) updateData.set('type', data.type)
updateData.set('pathIndex', data.pathIndex.toString()) updateData.set('pathIndex', data.pathIndex.toString())
updateData.set('fullname', data.fullname) updateData.set('fullname', data.fullname)
needUpdate = true
} }
if (updateData.size === 0) { if (!needUpdate) {
return return
} }
loading.show() loading.show()
await request(`/model/${model.type}/${model.pathIndex}/${model.fullname}`, { await request(`/model/${model.type}/${model.pathIndex}/${model.fullname}`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify(Object.fromEntries(updateData.entries())), body: updateData,
}) })
.catch((err) => { .catch((err) => {
const error_message = err.message ?? err.error const error_message = err.message ?? err.error
@@ -203,15 +229,15 @@ export const useModelFormData = (getFormData: () => BaseModel) => {
} }
} }
type SubmitCallback = (data: BaseModel) => void type SubmitCallback = (data: WithResolved<BaseModel>) => void
const submitCallback = ref<SubmitCallback[]>([]) const submitCallback = ref<SubmitCallback[]>([])
const registerSubmit = (callback: SubmitCallback) => { const registerSubmit = (callback: SubmitCallback) => {
submitCallback.value.push(callback) submitCallback.value.push(callback)
} }
const submit = () => { const submit = (): WithResolved<BaseModel> => {
const data = cloneDeep(toRaw(unref(formData))) const data: any = cloneDeep(toRaw(unref(formData)))
for (const callback of submitCallback.value) { for (const callback of submitCallback.value) {
callback(data) callback(data)
} }
@@ -381,9 +407,7 @@ export const useModelPreviewEditor = (formInstance: ModelFormInstance) => {
* Default images * Default images
*/ */
const defaultContent = computed(() => { const defaultContent = computed(() => {
return Array.isArray(model.value.preview) return model.value.preview ? castArray(model.value.preview) : []
? model.value.preview
: [model.value.preview]
}) })
const defaultContentPage = ref(0) const defaultContentPage = ref(0)
@@ -422,7 +446,7 @@ export const useModelPreviewEditor = (formInstance: ModelFormInstance) => {
content = localContent.value content = localContent.value
break break
default: default:
content = noPreviewContent.value content = undefined
break break
} }
@@ -438,7 +462,7 @@ export const useModelPreviewEditor = (formInstance: ModelFormInstance) => {
}) })
registerSubmit((data) => { registerSubmit((data) => {
data.preview = preview.value ?? noPreviewContent.value data.preview = preview.value
}) })
}) })

View File

@@ -26,6 +26,10 @@ export interface VersionModel extends BaseModel {
hashes?: Record<string, string> hashes?: Record<string, string>
} }
export type WithResolved<T> = Omit<T, 'preview'> & {
preview: string | undefined
}
export type PassThrough<T = void> = T | object | undefined export type PassThrough<T = void> = T | object | undefined
export interface SelectOptions { export interface SelectOptions {

View File

@@ -26,3 +26,14 @@ export const bytesToSize = (
export const formatDate = (date: number | string | Date) => { export const formatDate = (date: number | string | Date) => {
return dayjs(date).format('YYYY-MM-DD HH:mm:ss') return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
} }
export const previewUrlToFile = async (url: string) => {
return fetch(url)
.then((res) => res.blob())
.then((blob) => {
const type = blob.type
const extension = type.split('/')[1]
const file = new File([blob], `preview.${extension}`, { type })
return file
})
}