feat: Add model upload functionality (#194)
This commit is contained in:
21
src/App.vue
21
src/App.vue
@@ -10,6 +10,7 @@ import DialogDownload from 'components/DialogDownload.vue'
|
||||
import DialogExplorer from 'components/DialogExplorer.vue'
|
||||
import DialogManager from 'components/DialogManager.vue'
|
||||
import DialogScanning from 'components/DialogScanning.vue'
|
||||
import DialogUpload from 'components/DialogUpload.vue'
|
||||
import GlobalDialogStack from 'components/GlobalDialogStack.vue'
|
||||
import GlobalLoading from 'components/GlobalLoading.vue'
|
||||
import GlobalToast from 'components/GlobalToast.vue'
|
||||
@@ -64,6 +65,21 @@ onMounted(() => {
|
||||
})
|
||||
}
|
||||
|
||||
const openUploadDialog = () => {
|
||||
dialog.open({
|
||||
key: 'model-manager-upload',
|
||||
title: t('uploadModel'),
|
||||
content: DialogUpload,
|
||||
headerButtons: [
|
||||
{
|
||||
key: 'refresh',
|
||||
icon: 'pi pi-refresh',
|
||||
command: refreshModelsAndConfig,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const openManagerDialog = () => {
|
||||
const { cardWidth, gutter, aspect, flat } = config
|
||||
|
||||
@@ -93,6 +109,11 @@ onMounted(() => {
|
||||
icon: 'pi pi-download',
|
||||
command: openDownloadDialog,
|
||||
},
|
||||
{
|
||||
key: 'upload',
|
||||
icon: 'pi pi-upload',
|
||||
command: openUploadDialog,
|
||||
},
|
||||
],
|
||||
minWidth: cardWidth * 2 + gutter + 42,
|
||||
minHeight: (cardWidth / aspect) * 0.5 + 162,
|
||||
|
||||
274
src/components/DialogUpload.vue
Normal file
274
src/components/DialogUpload.vue
Normal file
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div class="h-full px-4">
|
||||
<!-- <div v-show="batchScanningStep === 0" class="h-full">
|
||||
<div class="flex h-full items-center px-8">
|
||||
<div class="h-20 w-full opacity-60">
|
||||
<ProgressBar mode="indeterminate" style="height: 6px"></ProgressBar>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<Stepper v-model:value="stepValue" class="flex h-full flex-col" linear>
|
||||
<StepList>
|
||||
<Step :value="1">{{ $t('selectModelType') }}</Step>
|
||||
<Step :value="2">{{ $t('selectSubdirectory') }}</Step>
|
||||
<Step :value="3">{{ $t('chooseFile') }}</Step>
|
||||
</StepList>
|
||||
<StepPanels class="flex-1 overflow-hidden">
|
||||
<StepPanel :value="1" class="h-full">
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
<ResponseScroll>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<Button
|
||||
v-for="item in typeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
@click="item.command"
|
||||
></Button>
|
||||
</div>
|
||||
</ResponseScroll>
|
||||
</div>
|
||||
</StepPanel>
|
||||
<StepPanel :value="2" class="h-full">
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
<ResponseScroll class="flex-1">
|
||||
<Tree
|
||||
class="h-full"
|
||||
v-model:selection-keys="selectedKey"
|
||||
:value="pathOptions"
|
||||
selectionMode="single"
|
||||
:pt:nodeLabel:class="'text-ellipsis overflow-hidden'"
|
||||
></Tree>
|
||||
</ResponseScroll>
|
||||
|
||||
<div class="flex justify-between pt-6">
|
||||
<Button
|
||||
:label="$t('back')"
|
||||
severity="secondary"
|
||||
icon="pi pi-arrow-left"
|
||||
@click="handleBackTypeSelect"
|
||||
></Button>
|
||||
<Button
|
||||
:label="$t('next')"
|
||||
icon="pi pi-arrow-right"
|
||||
icon-pos="right"
|
||||
:disabled="!enabledUpload"
|
||||
@click="handleConfirmSubdir"
|
||||
></Button>
|
||||
</div>
|
||||
</div>
|
||||
</StepPanel>
|
||||
<StepPanel :value="3" class="h-full">
|
||||
<div class="flex h-full flex-col items-center justify-center">
|
||||
<template v-if="showUploadProgress">
|
||||
<div class="w-4/5">
|
||||
<ProgressBar
|
||||
:value="uploadProgress"
|
||||
:pt:value:style="{ transition: 'width .1s linear' }"
|
||||
></ProgressBar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="overflow-hidden break-words py-8">
|
||||
<div class="overflow-hidden px-8">
|
||||
<div class="text-center">
|
||||
<div class="pb-2">
|
||||
{{ $t('selectedSpecialPath') }}
|
||||
</div>
|
||||
<div class="leading-5 opacity-60">
|
||||
{{ selectedModelFolder }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<Button
|
||||
v-for="item in uploadActions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:icon="item.icon"
|
||||
@click="item.command.call(item)"
|
||||
></Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="h-1/4"></div>
|
||||
</div>
|
||||
</StepPanel>
|
||||
</StepPanels>
|
||||
</Stepper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ResponseScroll from 'components/ResponseScroll.vue'
|
||||
import { configSetting } from 'hooks/config'
|
||||
import { useModelFolder, useModels } from 'hooks/model'
|
||||
import { request } from 'hooks/request'
|
||||
import { useToast } from 'hooks/toast'
|
||||
import Button from 'primevue/button'
|
||||
import ProgressBar from 'primevue/progressbar'
|
||||
import Step from 'primevue/step'
|
||||
import StepList from 'primevue/steplist'
|
||||
import StepPanel from 'primevue/steppanel'
|
||||
import StepPanels from 'primevue/steppanels'
|
||||
import Stepper from 'primevue/stepper'
|
||||
import Tree from 'primevue/tree'
|
||||
import { api, app } from 'scripts/comfyAPI'
|
||||
import { computed, onMounted, onUnmounted, ref, toValue } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { toast } = useToast()
|
||||
|
||||
const stepValue = ref(1)
|
||||
|
||||
const { folders } = useModels()
|
||||
|
||||
const currentType = ref<string>()
|
||||
const typeOptions = computed(() => {
|
||||
const excludeScanTypes = app.ui?.settings.getSettingValue<string>(
|
||||
configSetting.excludeScanTypes,
|
||||
)
|
||||
const customBlackList =
|
||||
excludeScanTypes
|
||||
?.split(',')
|
||||
.map((type) => type.trim())
|
||||
.filter(Boolean) ?? []
|
||||
return Object.keys(folders.value)
|
||||
.filter((folder) => !customBlackList.includes(folder))
|
||||
.map((type) => {
|
||||
return {
|
||||
label: type,
|
||||
value: type,
|
||||
command: () => {
|
||||
currentType.value = type
|
||||
stepValue.value++
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const { pathOptions } = useModelFolder({ type: currentType })
|
||||
|
||||
const selectedModelFolder = ref<string>()
|
||||
const selectedKey = computed({
|
||||
get: () => {
|
||||
const key = selectedModelFolder.value
|
||||
return key ? { [key]: true } : {}
|
||||
},
|
||||
set: (val) => {
|
||||
const key = Object.keys(val)[0]
|
||||
selectedModelFolder.value = key
|
||||
},
|
||||
})
|
||||
|
||||
const enabledUpload = computed(() => {
|
||||
return !!selectedModelFolder.value
|
||||
})
|
||||
|
||||
const handleBackTypeSelect = () => {
|
||||
selectedModelFolder.value = undefined
|
||||
currentType.value = undefined
|
||||
stepValue.value--
|
||||
}
|
||||
|
||||
const handleConfirmSubdir = () => {
|
||||
stepValue.value++
|
||||
}
|
||||
|
||||
const uploadTotalSize = ref<number>()
|
||||
const uploadSize = ref<number>()
|
||||
const uploadProgress = computed(() => {
|
||||
const total = toValue(uploadTotalSize)
|
||||
const size = toValue(uploadSize)
|
||||
if (typeof total === 'number' && typeof size === 'number') {
|
||||
return Math.floor((size / total) * 100)
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
const showUploadProgress = computed(() => {
|
||||
return typeof uploadProgress.value !== 'undefined'
|
||||
})
|
||||
|
||||
const uploadActions = ref([
|
||||
{
|
||||
value: 'back',
|
||||
label: t('back'),
|
||||
icon: 'pi pi-arrow-left',
|
||||
command: () => {
|
||||
stepValue.value--
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'full',
|
||||
label: t('chooseFile'),
|
||||
command: () => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = supportedExtensions.value.join(',')
|
||||
input.onchange = async () => {
|
||||
const files = input.files
|
||||
const file = files?.item(0)
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
uploadTotalSize.value = file.size
|
||||
uploadSize.value = 0
|
||||
const body = new FormData()
|
||||
body.append('folder', toValue(selectedModelFolder)!)
|
||||
body.append('file', file)
|
||||
|
||||
await request('/upload', {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: error.message,
|
||||
life: 5000,
|
||||
})
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const supportedExtensions = ref([])
|
||||
|
||||
const fetchSupportedExtensions = async () => {
|
||||
try {
|
||||
const result = await request('/supported-extensions')
|
||||
supportedExtensions.value = result ?? []
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: error.message,
|
||||
life: 5000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const update_process = (event: CustomEvent) => {
|
||||
const detail = event.detail
|
||||
uploadSize.value = detail.uploaded_size
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSupportedExtensions()
|
||||
|
||||
api.addEventListener('update_upload_progress', update_process)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
api.removeEventListener('update_upload_progress', update_process)
|
||||
})
|
||||
</script>
|
||||
@@ -41,6 +41,8 @@ const messages = {
|
||||
scanMissInformation: 'Download missing information',
|
||||
scanFullInformation: 'Override full information',
|
||||
noModelsInCurrentPath: 'There are no models available in the current path',
|
||||
uploadModel: 'Upload Model',
|
||||
chooseFile: 'Choose File',
|
||||
sort: {
|
||||
name: 'Name',
|
||||
size: 'Largest',
|
||||
@@ -116,6 +118,8 @@ const messages = {
|
||||
scanMissInformation: '下载缺失信息',
|
||||
scanFullInformation: '覆盖所有信息',
|
||||
noModelsInCurrentPath: '当前路径中没有可用的模型',
|
||||
uploadModel: '上传模型',
|
||||
chooseFile: '选择文件',
|
||||
sort: {
|
||||
name: '名称',
|
||||
size: '最大',
|
||||
|
||||
5
src/types/global.d.ts
vendored
5
src/types/global.d.ts
vendored
@@ -12,6 +12,11 @@ declare namespace ComfyAPI {
|
||||
callback: (event: CustomEvent) => void,
|
||||
options?: AddEventListenerOptions,
|
||||
) => void
|
||||
removeEventListener: (
|
||||
type: string,
|
||||
callback: (event: CustomEvent) => void,
|
||||
options?: AddEventListenerOptions,
|
||||
) => void
|
||||
}
|
||||
|
||||
const api: ComfyApi
|
||||
|
||||
Reference in New Issue
Block a user