mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
## Summary Fix download icon not appearing when file size is successfully fetched in the missing models dialog. ## Changes - **What**: Restructured the `v-if/v-else-if` chain in `MissingModelsContent.vue` so that file size and download icon render together instead of being mutually exclusive. Previously, a successful file size fetch would prevent the download button from rendering. ## Review Focus The file size span and download/gated-link are now inside a shared `<template v-else-if="model.isDownloadable">` block. File size uses `v-if` (independent), while gated link and download button remain `v-if/v-else` (mutually exclusive with each other). [screen-capture.webm](https://github.com/user-attachments/assets/f2f04d52-265b-4d05-992e-0ffe9bf64026) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9850-fix-show-download-icon-alongside-file-size-in-missing-models-dialog-3226d73d365081fd943bcfdedda87c73) by [Unito](https://www.unito.io)
192 lines
5.9 KiB
Vue
192 lines
5.9 KiB
Vue
<template>
|
|
<div
|
|
class="flex w-full max-w-[490px] flex-col border-t border-border-default"
|
|
>
|
|
<div class="flex size-full flex-col gap-4 p-4">
|
|
<p class="m-0 text-sm/5 text-muted-foreground">
|
|
{{ $t('missingModelsDialog.description') }}
|
|
</p>
|
|
|
|
<div
|
|
class="flex scrollbar-custom max-h-[300px] flex-col overflow-y-auto rounded-lg bg-secondary-background"
|
|
>
|
|
<div
|
|
v-for="model in processedModels"
|
|
:key="model.name"
|
|
class="flex items-center justify-between px-3 py-2"
|
|
>
|
|
<div class="flex items-center gap-2 overflow-hidden">
|
|
<span
|
|
class="text-foreground min-w-0 truncate text-sm"
|
|
:title="model.name"
|
|
>
|
|
{{ model.name }}
|
|
</span>
|
|
<span
|
|
class="inline-flex h-4 shrink-0 items-center rounded-full bg-muted-foreground/20 px-1.5 text-xxxs font-semibold text-muted-foreground uppercase"
|
|
>
|
|
{{ model.badgeLabel }}
|
|
</span>
|
|
</div>
|
|
<div class="flex shrink-0 items-center gap-2">
|
|
<Skeleton v-if="showSkeleton(model)" class="ml-1.5 h-4 w-12" />
|
|
<template v-else-if="model.isDownloadable">
|
|
<span
|
|
v-if="fileSizes.get(model.url)"
|
|
class="pl-1.5 text-xs text-muted-foreground"
|
|
>
|
|
{{ formatSize(fileSizes.get(model.url)) }}
|
|
</span>
|
|
<a
|
|
v-if="gatedModelUrls.has(model.url)"
|
|
:href="gatedModelUrls.get(model.url)"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="text-xs text-primary hover:underline"
|
|
>
|
|
{{ $t('missingModelsDialog.acceptTerms') }}
|
|
</a>
|
|
<Button
|
|
v-else
|
|
variant="textonly"
|
|
size="icon"
|
|
:title="model.url"
|
|
:aria-label="$t('g.download')"
|
|
@click="downloadModel(model, paths)"
|
|
>
|
|
<i class="icon-[lucide--download] size-4" />
|
|
</Button>
|
|
</template>
|
|
<Button
|
|
v-else
|
|
variant="textonly"
|
|
size="icon"
|
|
:title="model.url"
|
|
:aria-label="$t('g.copyURL')"
|
|
@click="void copyToClipboard(model.url)"
|
|
>
|
|
<i class="icon-[lucide--copy] size-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="totalDownloadSize > 0"
|
|
class="sticky bottom-0 flex items-center justify-between border-t border-border-default bg-secondary-background px-3 py-2"
|
|
>
|
|
<span class="text-xs font-medium text-muted-foreground">
|
|
{{ $t('missingModelsDialog.totalSize') }}
|
|
</span>
|
|
<span class="text-xs text-muted-foreground">
|
|
{{ formatSize(totalDownloadSize) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="m-0 text-xs/5 whitespace-pre-line text-muted-foreground">
|
|
{{ $t('missingModelsDialog.footerDescription') }}
|
|
</p>
|
|
|
|
<div
|
|
v-if="hasCustomModels"
|
|
class="flex gap-3 rounded-lg border border-warning-background bg-warning-background/10 p-3"
|
|
>
|
|
<i
|
|
class="mt-0.5 icon-[lucide--triangle-alert] size-4 shrink-0 text-warning-background"
|
|
/>
|
|
<div class="flex flex-col gap-1">
|
|
<p class="m-0 text-xs/5 font-semibold text-warning-background">
|
|
{{ $t('missingModelsDialog.customModelsWarning') }}
|
|
</p>
|
|
<p class="m-0 text-xs/5 text-warning-background">
|
|
{{ $t('missingModelsDialog.customModelsInstruction') }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import Skeleton from '@/components/ui/skeleton/Skeleton.vue'
|
|
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
|
import { formatSize } from '@/utils/formatUtil'
|
|
|
|
import type { ModelWithUrl } from './missingModelsUtils'
|
|
import {
|
|
downloadModel,
|
|
fetchModelMetadata,
|
|
getBadgeLabel,
|
|
hasValidDirectory,
|
|
isModelDownloadable
|
|
} from './missingModelsUtils'
|
|
|
|
const { missingModels, paths } = defineProps<{
|
|
missingModels: ModelWithUrl[]
|
|
paths: Record<string, string[]>
|
|
}>()
|
|
|
|
interface ProcessedModel {
|
|
name: string
|
|
url: string
|
|
directory: string
|
|
badgeLabel: string
|
|
isDownloadable: boolean
|
|
}
|
|
|
|
const processedModels = computed<ProcessedModel[]>(() =>
|
|
missingModels.map((model) => ({
|
|
name: model.name,
|
|
url: model.url,
|
|
directory: model.directory,
|
|
badgeLabel: getBadgeLabel(model.directory),
|
|
isDownloadable:
|
|
hasValidDirectory(model, paths) && isModelDownloadable(model)
|
|
}))
|
|
)
|
|
|
|
const hasCustomModels = computed(() =>
|
|
processedModels.value.some((m) => !m.isDownloadable)
|
|
)
|
|
|
|
const loading = ref(true)
|
|
const fileSizes = reactive(new Map<string, number>())
|
|
|
|
const totalDownloadSize = computed(() =>
|
|
processedModels.value
|
|
.filter((model) => model.isDownloadable)
|
|
.reduce((total, model) => total + (fileSizes.get(model.url) ?? 0), 0)
|
|
)
|
|
|
|
const gatedModelUrls = reactive(new Map<string, string>())
|
|
|
|
function showSkeleton(model: ProcessedModel): boolean {
|
|
return (
|
|
model.isDownloadable &&
|
|
loading.value &&
|
|
!fileSizes.has(model.url) &&
|
|
!gatedModelUrls.has(model.url)
|
|
)
|
|
}
|
|
|
|
onMounted(async () => {
|
|
const downloadableUrls = processedModels.value
|
|
.filter((m) => m.isDownloadable)
|
|
.map((m) => m.url)
|
|
|
|
await Promise.allSettled(
|
|
downloadableUrls.map(async (url) => {
|
|
const metadata = await fetchModelMetadata(url)
|
|
if (metadata.fileSize !== null) fileSizes.set(url, metadata.fileSize)
|
|
if (metadata.gatedRepoUrl) gatedModelUrls.set(url, metadata.gatedRepoUrl)
|
|
})
|
|
)
|
|
loading.value = false
|
|
})
|
|
|
|
const { copyToClipboard } = useCopyToClipboard()
|
|
</script>
|