mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-07 22:20:03 +00:00
## Summary Add SVG-based brand loading indicators (LogoCFillLoader, LogoComfyWaveLoader) and use the wave loader as the app loading screen. ## Changes - **What**: New `LogoCFillLoader` (bottom-to-top fill, plays once) and `LogoComfyWaveLoader` (wave water-fill animation) components with `size`, `color`, `bordered`, and `disableAnimation` props. Move all loaders from `components/common/` to `components/loader/`. Use `LogoComfyWaveLoader` in `App.vue` and `WorkspaceAuthGate.vue`. Render loader above BlockUI overlay (z-1200) to prevent dim wash-out. - **Dependencies**: None ## Review Focus - SVG mask-based animation approach using `currentColor` for flexible theming - z-index layering: loader at z-1200 renders above PrimeVue BlockUI's z-1100 modal overlay - `disableAnimation` prop used in WorkspaceAuthGate to show static logo outline during auth loading ## Screenshots (if applicable) [loading_record.webm](https://github.com/user-attachments/assets/b34f7296-9904-4a42-9273-a7d5fda49d15) Storybook stories added for both components under `Components/Loader/`. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9433-feat-add-Logo-C-fill-and-Comfy-wave-loading-indicator-components-31a6d73d3650811cacfdcf867b1f835f) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com>
257 lines
7.8 KiB
Vue
257 lines
7.8 KiB
Vue
<script setup lang="ts">
|
|
import { whenever } from '@vueuse/core'
|
|
import Popover from 'primevue/popover'
|
|
import { computed, ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import Loader from '@/components/loader/Loader.vue'
|
|
import HoneyToast from '@/components/honeyToast/HoneyToast.vue'
|
|
import ProgressToastItem from '@/components/toast/ProgressToastItem.vue'
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import { useAssetDownloadStore } from '@/stores/assetDownloadStore'
|
|
import { cn } from '@/utils/tailwindUtil'
|
|
|
|
const { t } = useI18n()
|
|
const assetDownloadStore = useAssetDownloadStore()
|
|
|
|
const visible = computed(() => assetDownloadStore.hasDownloads)
|
|
|
|
const isExpanded = ref(false)
|
|
const activeFilter = ref<'all' | 'completed' | 'failed'>('all')
|
|
const filterPopoverRef = ref<InstanceType<typeof Popover> | null>(null)
|
|
|
|
whenever(
|
|
() => !isExpanded.value,
|
|
() => filterPopoverRef.value?.hide()
|
|
)
|
|
|
|
const filterOptions = [
|
|
{ value: 'all', label: 'all' },
|
|
{ value: 'completed', label: 'completed' },
|
|
{ value: 'failed', label: 'failed' }
|
|
] as const
|
|
|
|
function onFilterClick(event: Event) {
|
|
filterPopoverRef.value?.toggle(event)
|
|
}
|
|
|
|
function setFilter(filter: typeof activeFilter.value) {
|
|
activeFilter.value = filter
|
|
filterPopoverRef.value?.hide()
|
|
}
|
|
|
|
const downloadJobs = computed(() => assetDownloadStore.downloadList)
|
|
const completedJobs = computed(() =>
|
|
assetDownloadStore.finishedDownloads.filter((d) => d.status === 'completed')
|
|
)
|
|
const failedJobs = computed(() =>
|
|
assetDownloadStore.finishedDownloads.filter((d) => d.status === 'failed')
|
|
)
|
|
|
|
const isInProgress = computed(() => assetDownloadStore.hasActiveDownloads)
|
|
const currentJobName = computed(() => {
|
|
const activeJob = downloadJobs.value.find((job) => job.status === 'running')
|
|
return activeJob?.assetName || t('progressToast.downloadingModel')
|
|
})
|
|
|
|
const completedCount = computed(
|
|
() => completedJobs.value.length + failedJobs.value.length
|
|
)
|
|
const totalCount = computed(() => downloadJobs.value.length)
|
|
|
|
const filteredJobs = computed(() => {
|
|
switch (activeFilter.value) {
|
|
case 'completed':
|
|
return completedJobs.value
|
|
case 'failed':
|
|
return failedJobs.value
|
|
default:
|
|
return downloadJobs.value
|
|
}
|
|
})
|
|
|
|
const activeFilterLabel = computed(() => {
|
|
const option = filterOptions.find((f) => f.value === activeFilter.value)
|
|
return option
|
|
? t(`progressToast.filter.${option.label}`)
|
|
: t('progressToast.filter.all')
|
|
})
|
|
|
|
function closeDialog() {
|
|
assetDownloadStore.clearFinishedDownloads()
|
|
isExpanded.value = false
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<HoneyToast v-model:expanded="isExpanded" :visible>
|
|
<template #default>
|
|
<div
|
|
class="flex h-12 items-center justify-between border-b border-border-default px-4"
|
|
>
|
|
<h3 class="text-sm font-bold text-base-foreground">
|
|
{{ t('progressToast.importingModels') }}
|
|
</h3>
|
|
<div class="flex items-center gap-2">
|
|
<Button
|
|
variant="secondary"
|
|
size="md"
|
|
class="gap-1.5 px-2"
|
|
@click="onFilterClick"
|
|
>
|
|
<i class="icon-[lucide--list-filter] size-4" />
|
|
<span>{{ activeFilterLabel }}</span>
|
|
<i class="icon-[lucide--chevron-down] size-3" />
|
|
</Button>
|
|
<Popover
|
|
ref="filterPopoverRef"
|
|
:dismissable="true"
|
|
:close-on-escape="true"
|
|
unstyled
|
|
:base-z-index="9999"
|
|
:pt="{
|
|
root: { class: 'absolute z-50' },
|
|
content: {
|
|
class:
|
|
'bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg'
|
|
}
|
|
}"
|
|
>
|
|
<div
|
|
class="flex min-w-30 flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3"
|
|
>
|
|
<Button
|
|
v-for="option in filterOptions"
|
|
:key="option.value"
|
|
variant="textonly"
|
|
size="sm"
|
|
:class="
|
|
cn(
|
|
'w-full justify-start bg-transparent',
|
|
activeFilter === option.value &&
|
|
'bg-secondary-background-selected'
|
|
)
|
|
"
|
|
@click="setFilter(option.value)"
|
|
>
|
|
{{ t(`progressToast.filter.${option.label}`) }}
|
|
</Button>
|
|
</div>
|
|
</Popover>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="relative max-h-75 overflow-y-auto p-4">
|
|
<div
|
|
v-if="filteredJobs.length > 3"
|
|
class="absolute top-4 right-1 h-12 w-1 rounded-full bg-muted-foreground"
|
|
/>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<ProgressToastItem
|
|
v-for="job in filteredJobs"
|
|
:key="job.taskId"
|
|
:job="job"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="filteredJobs.length === 0"
|
|
class="flex flex-col items-center justify-center py-6 text-center"
|
|
>
|
|
<span class="text-sm text-muted-foreground">
|
|
{{
|
|
t('progressToast.noImportsInQueue', {
|
|
filter: activeFilterLabel
|
|
})
|
|
}}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #footer="{ toggle }">
|
|
<div
|
|
class="flex h-12 items-center justify-between gap-2 border-t border-border-default px-4"
|
|
>
|
|
<div class="flex min-w-0 flex-1 items-center gap-2 text-sm">
|
|
<template v-if="isInProgress">
|
|
<Loader size="sm" class="shrink-0 text-muted-foreground" />
|
|
<span
|
|
class="min-w-0 flex-1 truncate font-bold text-base-foreground"
|
|
>
|
|
{{ currentJobName }}
|
|
</span>
|
|
</template>
|
|
<template v-else-if="failedJobs.length > 0">
|
|
<i
|
|
class="icon-[lucide--circle-alert] size-4 shrink-0 text-destructive-background"
|
|
/>
|
|
<span class="min-w-0 truncate font-bold text-base-foreground">
|
|
{{
|
|
t('progressToast.downloadsFailed', {
|
|
count: failedJobs.length
|
|
})
|
|
}}
|
|
</span>
|
|
</template>
|
|
<template v-else>
|
|
<i
|
|
class="icon-[lucide--check-circle] size-4 shrink-0 text-jade-600"
|
|
/>
|
|
<span class="min-w-0 truncate font-bold text-base-foreground">
|
|
{{ t('progressToast.allDownloadsCompleted') }}
|
|
</span>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="flex shrink-0 items-center gap-2">
|
|
<span
|
|
v-if="isInProgress"
|
|
class="text-sm whitespace-nowrap text-muted-foreground"
|
|
>
|
|
{{
|
|
t('progressToast.progressCount', {
|
|
completed: completedCount,
|
|
total: totalCount
|
|
})
|
|
}}
|
|
</span>
|
|
|
|
<div class="flex items-center">
|
|
<Button
|
|
variant="muted-textonly"
|
|
size="icon"
|
|
:aria-label="
|
|
isExpanded ? t('contextMenu.Collapse') : t('contextMenu.Expand')
|
|
"
|
|
@click.stop="toggle"
|
|
>
|
|
<i
|
|
:class="
|
|
cn(
|
|
'size-4',
|
|
isExpanded
|
|
? 'icon-[lucide--chevron-down]'
|
|
: 'icon-[lucide--chevron-up]'
|
|
)
|
|
"
|
|
/>
|
|
</Button>
|
|
|
|
<Button
|
|
v-if="!isInProgress"
|
|
variant="muted-textonly"
|
|
size="icon"
|
|
:aria-label="t('g.close')"
|
|
@click.stop="closeDialog"
|
|
>
|
|
<i class="icon-[lucide--x] size-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</HoneyToast>
|
|
</template>
|