feat: support download multiple actual files (#196)

This commit is contained in:
Hayden
2025-08-11 09:10:20 +08:00
committed by GitHub
parent 8b9f3a0e65
commit ac4a168f13
5 changed files with 89 additions and 16 deletions

View File

@@ -69,8 +69,8 @@ class CivitaiModelSearcher(ModelSearcher):
models: list[dict] = [] models: list[dict] = []
for version in model_versions: for version in model_versions:
model_files: list[dict] = version.get("files", []) version_files: list[dict] = version.get("files", [])
model_files = utils.filter_with(model_files, {"type": "Model"}) model_files = utils.filter_with(version_files, {"type": "Model"})
shortname = version.get("name", None) if len(model_files) > 0 else None shortname = version.get("name", None) if len(model_files) > 0 else None
@@ -108,7 +108,7 @@ class CivitaiModelSearcher(ModelSearcher):
description_parts.append("") description_parts.append("")
model = { model = {
"id": file.get("id"), "id": version.get("id"),
"shortname": shortname or basename, "shortname": shortname or basename,
"basename": basename, "basename": basename,
"extension": extension, "extension": extension,
@@ -122,6 +122,7 @@ class CivitaiModelSearcher(ModelSearcher):
"downloadPlatform": "civitai", "downloadPlatform": "civitai",
"downloadUrl": file.get("downloadUrl"), "downloadUrl": file.get("downloadUrl"),
"hashes": file.get("hashes"), "hashes": file.get("hashes"),
"files": version_files if len(version_files) > 1 else None,
} }
models.append(model) models.append(model)

View File

@@ -31,12 +31,20 @@
<KeepAlive> <KeepAlive>
<ModelContent <ModelContent
v-if="currentModel" v-if="currentModel"
:key="currentModel.id" :key="`${currentModel.id}-${currentModel.currentFileId}`"
:model="currentModel" :model="currentModel"
:editable="true" :editable="true"
@submit="createDownTask" @submit="createDownTask"
> >
<template #action> <template #action>
<div v-if="currentModel.files" class="flex-1">
<ResponseSelect
:model-value="currentModel.currentFileId"
:items="currentModel.selectionFiles"
:type="isMobile ? 'drop' : 'button'"
>
</ResponseSelect>
</div>
<Button <Button
icon="pi pi-download" icon="pi pi-download"
:label="$t('download')" :label="$t('download')"

View File

@@ -17,7 +17,7 @@
></ModelPreview> ></ModelPreview>
<div class="flex flex-col gap-4 overflow-hidden"> <div class="flex flex-col gap-4 overflow-hidden">
<div class="flex items-center justify-end gap-4"> <div class="flex h-10 items-center justify-end gap-4">
<slot name="action" :metadata="formInstance.metadata.value"></slot> <slot name="action" :metadata="formInstance.metadata.value"></slot>
</div> </div>

View File

@@ -2,17 +2,19 @@ import { useLoading } from 'hooks/loading'
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 { upperFirst } from 'lodash'
import { api } from 'scripts/comfyAPI' import { api } from 'scripts/comfyAPI'
import { import {
BaseModel,
DownloadTask, DownloadTask,
DownloadTaskOptions, DownloadTaskOptions,
SelectOptions, SelectOptions,
VersionModel, VersionModel,
VersionModelFile,
} from 'types/typings' } from 'types/typings'
import { bytesToSize } from 'utils/common' import { bytesToSize } from 'utils/common'
import { onBeforeMount, onMounted, ref, watch } from 'vue' import { onBeforeMount, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import yaml from 'yaml'
export const useDownload = defineStore('download', (store) => { export const useDownload = defineStore('download', (store) => {
const { toast, confirm, wrapperToastError } = useToast() const { toast, confirm, wrapperToastError } = useToast()
@@ -162,12 +164,60 @@ declare module 'hooks/store' {
} }
} }
type WithSelection<T> = SelectOptions & { item: T }
type FileSelectionVersionModel = VersionModel & {
currentFileId?: number
selectionFiles?: WithSelection<VersionModelFile>[]
}
export const useModelSearch = () => { export const useModelSearch = () => {
const loading = useLoading() const loading = useLoading()
const { toast } = useToast() const { toast } = useToast()
const data = ref<(SelectOptions & { item: VersionModel })[]>([]) const data = ref<WithSelection<FileSelectionVersionModel>[]>([])
const current = ref<string | number>() const current = ref<string | number>()
const currentModel = ref<BaseModel>() const currentModel = ref<FileSelectionVersionModel>()
const genFileSelectionItem = (
item: VersionModel,
): FileSelectionVersionModel => {
const fileSelectionItem: FileSelectionVersionModel = { ...item }
fileSelectionItem.selectionFiles = fileSelectionItem.files
?.sort((file) => (file.type === 'Model' ? -1 : 1))
.map((file) => {
const parts = file.name.split('.')
const extension = `.${parts.pop()}`
const basename = parts.join('.')
const regexp = /---\n([\s\S]*?)\n---/
const yamlMetadataMatch = item.description.match(regexp)
const yamlMetadata = yaml.parse(yamlMetadataMatch?.[1] || '')
yamlMetadata.hashes = file.hashes
yamlMetadata.metadata = file.metadata
const yamlContent = `---\n${yaml.stringify(yamlMetadata)}---`
const description = item.description.replace(regexp, yamlContent)
return {
label: file.type === 'Model' ? upperFirst(item.type) : file.type,
value: file.id,
item: file,
command() {
if (currentModel.value) {
currentModel.value.basename = basename
currentModel.value.extension = extension
currentModel.value.sizeBytes = file.sizeKB * 1024
currentModel.value.metadata = file.metadata
currentModel.value.downloadUrl = file.downloadUrl
currentModel.value.hashes = file.hashes
currentModel.value.description = description
currentModel.value.currentFileId = file.id
}
},
}
})
fileSelectionItem.currentFileId = item.files?.[0]?.id
return fileSelectionItem
}
const handleSearchByUrl = async (url: string) => { const handleSearchByUrl = async (url: string) => {
if (!url) { if (!url) {
@@ -177,14 +227,17 @@ export const useModelSearch = () => {
loading.show() loading.show()
return request(`/model-info?model-page=${encodeURIComponent(url)}`, {}) return request(`/model-info?model-page=${encodeURIComponent(url)}`, {})
.then((resData: VersionModel[]) => { .then((resData: VersionModel[]) => {
data.value = resData.map((item) => ({ data.value = resData.map((item) => {
label: item.shortname, const resolvedItem = genFileSelectionItem(item)
value: item.id, return {
item, label: item.shortname,
command() { value: item.id,
current.value = item.id item: resolvedItem,
}, command() {
})) current.value = item.id
},
}
})
current.value = data.value[0]?.value current.value = data.value[0]?.value
currentModel.value = data.value[0]?.item currentModel.value = data.value[0]?.item

View File

@@ -22,11 +22,22 @@ export interface Model extends BaseModel {
children?: Model[] children?: Model[]
} }
export interface VersionModelFile {
id: number
sizeKB: number
name: string
type: string
metadata: Record<string, string>
hashes: Record<string, string>
downloadUrl: string
}
export interface VersionModel extends BaseModel { export interface VersionModel extends BaseModel {
shortname: string shortname: string
downloadPlatform: string downloadPlatform: string
downloadUrl: string downloadUrl: string
hashes?: Record<string, string> hashes?: Record<string, string>
files?: VersionModelFile[]
} }
export type WithResolved<T> = Omit<T, 'preview'> & { export type WithResolved<T> = Omit<T, 'preview'> & {