Files
ComfyUI-Model-Manager/src/components/DialogUpload.vue
2025-08-11 09:10:39 +08:00

275 lines
7.7 KiB
Vue

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