Refactor scan infomation featurn (#174)
* feat: add scanning setting panel * feat: implement the back-end interface * feat: add i18n-zh * chore: remove never used code
This commit is contained in:
19
src/App.vue
19
src/App.vue
@@ -9,6 +9,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 GlobalDialogStack from 'components/GlobalDialogStack.vue'
|
||||
import GlobalLoading from 'components/GlobalLoading.vue'
|
||||
import GlobalToast from 'components/GlobalToast.vue'
|
||||
@@ -35,6 +36,19 @@ onMounted(() => {
|
||||
})
|
||||
}
|
||||
|
||||
const openModelScanning = () => {
|
||||
dialog.open({
|
||||
key: 'model-information-scanning',
|
||||
title: t('batchScanModelInformation'),
|
||||
content: DialogScanning,
|
||||
modal: true,
|
||||
defaultSize: {
|
||||
width: 680,
|
||||
height: 490,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const openDownloadDialog = () => {
|
||||
dialog.open({
|
||||
key: 'model-manager-download-list',
|
||||
@@ -64,6 +78,11 @@ onMounted(() => {
|
||||
content: flat.value ? DialogManager : DialogExplorer,
|
||||
keepAlive: true,
|
||||
headerButtons: [
|
||||
{
|
||||
key: 'scanning',
|
||||
icon: 'mdi mdi-folder-search-outline text-lg',
|
||||
command: openModelScanning,
|
||||
},
|
||||
{
|
||||
key: 'refresh',
|
||||
icon: 'pi pi-refresh',
|
||||
|
||||
@@ -133,7 +133,7 @@ import Button from 'primevue/button'
|
||||
import ConfirmDialog from 'primevue/confirmdialog'
|
||||
import ContextMenu from 'primevue/contextmenu'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { MenuItem } from 'primevue/menuitem'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { genModelKey } from 'utils/model'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
271
src/components/DialogScanning.vue
Normal file
271
src/components/DialogScanning.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<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-show="batchScanningStep === 1"
|
||||
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('scanModelInformation') }}</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="!enabledScan"
|
||||
@click="handleConfirmSubdir"
|
||||
></Button>
|
||||
</div>
|
||||
</div>
|
||||
</StepPanel>
|
||||
<StepPanel value="3" class="h-full">
|
||||
<div class="overflow-hidden break-words py-8">
|
||||
<div class="overflow-hidden px-8">
|
||||
<div v-show="currentType === allType" class="text-center">
|
||||
{{ $t('selectedAllPaths') }}
|
||||
</div>
|
||||
<div v-show="currentType !== allType" 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 scanActions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:icon="item.icon"
|
||||
@click="item.command.call(item)"
|
||||
></Button>
|
||||
</div>
|
||||
</StepPanel>
|
||||
</StepPanels>
|
||||
</Stepper>
|
||||
|
||||
<div v-show="batchScanningStep === 2" class="h-full">
|
||||
<div class="flex h-full items-center px-8">
|
||||
<div class="h-20 w-full">
|
||||
<div v-show="scanProgress > -1">
|
||||
<ProgressBar :value="scanProgress">
|
||||
{{ scanCompleteCount }} / {{ scanTotalCount }}
|
||||
</ProgressBar>
|
||||
</div>
|
||||
|
||||
<div v-show="scanProgress === -1" class="text-center">
|
||||
<Button
|
||||
severity="secondary"
|
||||
:label="$t('back')"
|
||||
icon="pi pi-arrow-left"
|
||||
@click="handleBackTypeSelect"
|
||||
></Button>
|
||||
<span class="pl-2">{{ $t('noModelsInCurrentPath') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 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, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const stepValue = ref('1')
|
||||
|
||||
const { folders } = useModels()
|
||||
|
||||
const allType = 'All'
|
||||
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 [
|
||||
allType,
|
||||
...Object.keys(folders.value).filter(
|
||||
(folder) => !customBlackList.includes(folder),
|
||||
),
|
||||
].map((type) => {
|
||||
return {
|
||||
label: type,
|
||||
value: type,
|
||||
command: () => {
|
||||
currentType.value = type
|
||||
stepValue.value = currentType.value === allType ? '3' : '2'
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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 enabledScan = computed(() => {
|
||||
return currentType.value === allType || !!selectedModelFolder.value
|
||||
})
|
||||
|
||||
const handleBackTypeSelect = () => {
|
||||
selectedModelFolder.value = undefined
|
||||
currentType.value = undefined
|
||||
stepValue.value = '1'
|
||||
batchScanningStep.value = 1
|
||||
}
|
||||
|
||||
const handleConfirmSubdir = () => {
|
||||
stepValue.value = '3'
|
||||
}
|
||||
|
||||
const batchScanningStep = ref(0)
|
||||
const scanModelsList = ref<Record<string, boolean>>({})
|
||||
const scanTotalCount = computed(() => {
|
||||
return Object.keys(scanModelsList.value).length
|
||||
})
|
||||
const scanCompleteCount = computed(() => {
|
||||
return Object.keys(scanModelsList.value).filter(
|
||||
(key) => scanModelsList.value[key],
|
||||
).length
|
||||
})
|
||||
const scanProgress = computed(() => {
|
||||
if (scanTotalCount.value === 0) {
|
||||
return -1
|
||||
}
|
||||
const progress = scanCompleteCount.value / scanTotalCount.value
|
||||
return Number(progress.toFixed(4)) * 100
|
||||
})
|
||||
|
||||
const handleScanModelInformation = async function () {
|
||||
batchScanningStep.value = 0
|
||||
const mode = this.value
|
||||
const path = selectedModelFolder.value
|
||||
|
||||
try {
|
||||
const result = await request('/model-info/scan', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ mode, path }),
|
||||
})
|
||||
scanModelsList.value = result?.models ?? {}
|
||||
batchScanningStep.value = 2
|
||||
} catch {
|
||||
batchScanningStep.value = 1
|
||||
}
|
||||
}
|
||||
|
||||
const scanActions = ref([
|
||||
{
|
||||
value: 'back',
|
||||
label: t('back'),
|
||||
icon: 'pi pi-arrow-left',
|
||||
command: () => {
|
||||
stepValue.value = currentType.value === allType ? '1' : '2'
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'full',
|
||||
label: t('scanFullInformation'),
|
||||
command: handleScanModelInformation,
|
||||
},
|
||||
{
|
||||
value: 'diff',
|
||||
label: t('scanMissInformation'),
|
||||
command: handleScanModelInformation,
|
||||
},
|
||||
])
|
||||
|
||||
const refreshTaskContent = async () => {
|
||||
const result = await request('/model-info/scan')
|
||||
const listContent = result?.models ?? {}
|
||||
scanModelsList.value = listContent
|
||||
batchScanningStep.value = Object.keys(listContent).length ? 2 : 1
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refreshTaskContent()
|
||||
|
||||
api.addEventListener('update_scan_information_task', (event) => {
|
||||
const content = event.detail
|
||||
scanModelsList.value = content.models
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -1,10 +1,8 @@
|
||||
import SettingCardSize from 'components/SettingCardSize.vue'
|
||||
import { request } from 'hooks/request'
|
||||
import { defineStore } from 'hooks/store'
|
||||
import { $el, app, ComfyDialog } from 'scripts/comfyAPI'
|
||||
import { app } from 'scripts/comfyAPI'
|
||||
import { computed, onMounted, onUnmounted, readonly, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from './toast'
|
||||
|
||||
export const useConfig = defineStore('config', (store) => {
|
||||
const { t } = useI18n()
|
||||
@@ -98,41 +96,8 @@ export const configSetting = {
|
||||
}
|
||||
|
||||
function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
||||
const { toast } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const confirm = (opts: {
|
||||
message?: string
|
||||
accept?: () => void
|
||||
reject?: () => void
|
||||
}) => {
|
||||
const dialog = new ComfyDialog('div', [])
|
||||
|
||||
dialog.show(
|
||||
$el('div', [
|
||||
$el('p', { textContent: opts.message }),
|
||||
$el('div.flex.gap-4', [
|
||||
$el('button.flex-1', {
|
||||
textContent: 'Cancel',
|
||||
onclick: () => {
|
||||
opts.reject?.()
|
||||
dialog.close()
|
||||
document.body.removeChild(dialog.element)
|
||||
},
|
||||
}),
|
||||
$el('button.flex-1', {
|
||||
textContent: 'Continue',
|
||||
onclick: () => {
|
||||
opts.accept?.()
|
||||
dialog.close()
|
||||
document.body.removeChild(dialog.element)
|
||||
},
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// API keys
|
||||
app.ui?.settings.addSetting({
|
||||
@@ -187,101 +152,6 @@ function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
||||
},
|
||||
})
|
||||
|
||||
// Scan information
|
||||
app.ui?.settings.addSetting({
|
||||
id: 'ModelManager.ScanFiles.Full',
|
||||
category: [t('modelManager'), t('setting.scan'), 'Full'],
|
||||
name: t('setting.scanAll'),
|
||||
defaultValue: '',
|
||||
type: () => {
|
||||
return $el('button.p-button.p-component.p-button-secondary', {
|
||||
textContent: 'Full Scan',
|
||||
onclick: () => {
|
||||
confirm({
|
||||
message: [
|
||||
'This operation will override current files.',
|
||||
'This may take a while and generate MANY server requests!',
|
||||
'USE AT YOUR OWN RISK! Continue?',
|
||||
].join('\n'),
|
||||
accept: () => {
|
||||
store.loading.loading.value = true
|
||||
request('/model-info/scan', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ scanMode: 'full' }),
|
||||
})
|
||||
.then(() => {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Complete download information',
|
||||
life: 2000,
|
||||
})
|
||||
store.models.refresh()
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: err.message ?? 'Failed to download information',
|
||||
life: 15000,
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
store.loading.loading.value = false
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
app.ui?.settings.addSetting({
|
||||
id: 'ModelManager.ScanFiles.Incremental',
|
||||
category: [t('modelManager'), t('setting.scan'), 'Incremental'],
|
||||
name: t('setting.scanMissing'),
|
||||
defaultValue: '',
|
||||
type: () => {
|
||||
return $el('button.p-button.p-component.p-button-secondary', {
|
||||
textContent: 'Diff Scan',
|
||||
onclick: () => {
|
||||
confirm({
|
||||
message: [
|
||||
'Download missing information or preview.',
|
||||
'This may take a while and generate MANY server requests!',
|
||||
'USE AT YOUR OWN RISK! Continue?',
|
||||
].join('\n'),
|
||||
accept: () => {
|
||||
store.loading.loading.value = true
|
||||
request('/model-info/scan', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ scanMode: 'diff' }),
|
||||
})
|
||||
.then(() => {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Complete download information',
|
||||
life: 2000,
|
||||
})
|
||||
store.models.refresh()
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: err.message ?? 'Failed to download information',
|
||||
life: 15000,
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
store.loading.loading.value = false
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
app.ui?.settings.addSetting({
|
||||
id: configSetting.excludeScanTypes,
|
||||
category: [t('modelManager'), t('setting.scan'), 'ExcludeScanTypes'],
|
||||
|
||||
@@ -443,7 +443,7 @@ export const useModelBaseInfo = () => {
|
||||
|
||||
export const useModelFolder = (
|
||||
option: {
|
||||
type?: MaybeRefOrGetter<string>
|
||||
type?: MaybeRefOrGetter<string | undefined>
|
||||
} = {},
|
||||
) => {
|
||||
const { data: models, folders: modelFolders } = useModels()
|
||||
|
||||
24
src/i18n.ts
24
src/i18n.ts
@@ -29,6 +29,18 @@ const messages = {
|
||||
width: 'Width',
|
||||
height: 'Height',
|
||||
reset: 'Reset',
|
||||
back: 'Back',
|
||||
next: 'Next',
|
||||
batchScanModelInformation: 'Batch scan model information',
|
||||
modelInformationScanning: 'Scanning model information',
|
||||
selectModelType: 'Select model type',
|
||||
selectSubdirectory: 'Select subdirectory',
|
||||
scanModelInformation: 'Scan model information',
|
||||
selectedAllPaths: 'Selected all model paths',
|
||||
selectedSpecialPath: 'Selected special path',
|
||||
scanMissInformation: 'Download missing information',
|
||||
scanFullInformation: 'Override full information',
|
||||
noModelsInCurrentPath: 'There are no models available in the current path',
|
||||
sort: {
|
||||
name: 'Name',
|
||||
size: 'Largest',
|
||||
@@ -92,6 +104,18 @@ const messages = {
|
||||
width: '宽度',
|
||||
height: '高度',
|
||||
reset: '重置',
|
||||
back: '返回',
|
||||
next: '下一步',
|
||||
batchScanModelInformation: '批量扫描模型信息',
|
||||
modelInformationScanning: '扫描模型信息',
|
||||
selectModelType: '选择模型类型',
|
||||
selectSubdirectory: '选择子目录',
|
||||
scanModelInformation: '扫描模型信息',
|
||||
selectedAllPaths: '已选所有模型路径',
|
||||
selectedSpecialPath: '已选指定路径',
|
||||
scanMissInformation: '下载缺失信息',
|
||||
scanFullInformation: '覆盖所有信息',
|
||||
noModelsInCurrentPath: '当前路径中没有可用的模型',
|
||||
sort: {
|
||||
name: '名称',
|
||||
size: '最大',
|
||||
|
||||
Reference in New Issue
Block a user