mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-12 08:30:08 +00:00
Update rh-test (as of 2025-10-11) (#6044)
## Summary Tested these changes and confirmed that: 1. Feedback button shows. 2. You can run workflows and switch out models. 3. You can use the mask editor. (thank you @ric-yu for helping me verify). ## Changes A lot, please see commits. Gets us up to date with `main` as of 10-11-2025. --------- Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com> Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: snomiao <snomiao@gmail.com> Co-authored-by: Christian Byrne <cbyrne@comfy.org> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: DrJKL <DrJKL@users.noreply.github.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: Marwan Ahmed <155799754+marawan206@users.noreply.github.com> Co-authored-by: DrJKL <DrJKL0424@gmail.com> Co-authored-by: Rizumu Ayaka <rizumu@ayaka.moe> Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com> Co-authored-by: AustinMroz <4284322+AustinMroz@users.noreply.github.com> Co-authored-by: Austin Mroz <austin@comfy.org> Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com> Co-authored-by: Benjamin Lu <benceruleanlu@proton.me> Co-authored-by: Jin Yi <jin12cc@gmail.com> Co-authored-by: Robin Huang <robin.j.huang@gmail.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="absolute bottom-2 right-2 flex flex-wrap justify-end gap-1">
|
||||
<div class="absolute right-2 bottom-2 flex flex-wrap justify-end gap-1">
|
||||
<span
|
||||
v-for="badge in badges"
|
||||
:key="badge.label"
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
@click="interactive && $emit('select', asset)"
|
||||
@keydown.enter="interactive && $emit('select', asset)"
|
||||
>
|
||||
<div class="relative w-full aspect-square overflow-hidden">
|
||||
<div class="relative aspect-square w-full overflow-hidden">
|
||||
<div
|
||||
class="w-full h-full bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-600 flex items-center justify-center"
|
||||
class="flex h-full w-full items-center justify-center bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-600"
|
||||
></div>
|
||||
<AssetBadgeGroup :badges="asset.badges" />
|
||||
</div>
|
||||
@@ -66,15 +66,15 @@
|
||||
"
|
||||
>
|
||||
<span v-if="asset.stats.stars" class="flex items-center gap-1">
|
||||
<i-lucide:star class="size-3" />
|
||||
<i class="icon-[lucide--star] size-3" />
|
||||
{{ asset.stats.stars }}
|
||||
</span>
|
||||
<span v-if="asset.stats.downloadCount" class="flex items-center gap-1">
|
||||
<i-lucide:download class="size-3" />
|
||||
<i class="icon-[lucide--download] size-3" />
|
||||
{{ asset.stats.downloadCount }}
|
||||
</span>
|
||||
<span v-if="asset.stats.formattedDate" class="flex items-center gap-1">
|
||||
<i-lucide:clock class="size-3" />
|
||||
<i class="icon-[lucide--clock] size-3" />
|
||||
{{ asset.stats.formattedDate }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
@update:model-value="handleFilterChange"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:arrow-up-down class="size-3" />
|
||||
<i class="icon-[lucide--arrow-up-down] size-3" />
|
||||
</template>
|
||||
</SingleSelect>
|
||||
</div>
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
)
|
||||
"
|
||||
>
|
||||
<i-lucide:search class="size-10 mb-4" />
|
||||
<h3 class="text-lg font-medium mb-2">
|
||||
<i class="mb-4 icon-[lucide--search] size-10" />
|
||||
<h3 class="mb-2 text-lg font-medium">
|
||||
{{ $t('assetBrowser.noAssetsFound') }}
|
||||
</h3>
|
||||
<p class="text-sm">{{ $t('assetBrowser.tryAdjustingFilters') }}</p>
|
||||
@@ -39,7 +39,8 @@
|
||||
v-if="loading"
|
||||
class="col-span-full flex items-center justify-center py-16"
|
||||
>
|
||||
<i-lucide:loader
|
||||
<i
|
||||
class="icon-[lucide--loader]"
|
||||
:class="
|
||||
cn('size-6 animate-spin', 'text-stone-300 dark-theme:text-stone-200')
|
||||
"
|
||||
|
||||
29
src/platform/assets/components/Media3DBottom.vue
Normal file
29
src/platform/assets/components/Media3DBottom.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<h3
|
||||
class="m-0 line-clamp-1 text-sm font-bold text-zinc-900 dark-theme:text-white"
|
||||
:title="asset.name"
|
||||
>
|
||||
{{ fileName }}
|
||||
</h3>
|
||||
<div class="flex items-center gap-2 text-xs text-zinc-400">
|
||||
<span>{{ formatSize(asset.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { formatSize, getFilenameDetails } from '@/utils/formatUtil'
|
||||
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
|
||||
const { asset } = defineProps<{
|
||||
asset: AssetMeta
|
||||
}>()
|
||||
|
||||
const fileName = computed(() => {
|
||||
return getFilenameDetails(asset.name).filename
|
||||
})
|
||||
</script>
|
||||
16
src/platform/assets/components/Media3DTop.vue
Normal file
16
src/platform/assets/components/Media3DTop.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="relative h-full w-full overflow-hidden rounded">
|
||||
<div
|
||||
class="flex h-full w-full flex-col items-center justify-center gap-2 bg-zinc-200 dark-theme:bg-zinc-700/50"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--box] text-3xl text-zinc-600 dark-theme:text-zinc-200"
|
||||
/>
|
||||
<span class="text-zinc-600 dark-theme:text-zinc-200">{{
|
||||
$t('3D Model')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
50
src/platform/assets/components/MediaAssetActions.vue
Normal file
50
src/platform/assets/components/MediaAssetActions.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<IconGroup>
|
||||
<IconButton size="sm" @click="handleDelete">
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</IconButton>
|
||||
<IconButton size="sm" @click="handleDownload">
|
||||
<i class="icon-[lucide--download] size-4" />
|
||||
</IconButton>
|
||||
<MoreButton
|
||||
size="sm"
|
||||
@menu-opened="emit('menuStateChanged', true)"
|
||||
@menu-closed="emit('menuStateChanged', false)"
|
||||
>
|
||||
<template #default="{ close }">
|
||||
<MediaAssetMoreMenu :close="close" />
|
||||
</template>
|
||||
</MoreButton>
|
||||
</IconGroup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconGroup from '@/components/button/IconGroup.vue'
|
||||
import MoreButton from '@/components/button/MoreButton.vue'
|
||||
|
||||
import { useMediaAssetActions } from '../composables/useMediaAssetActions'
|
||||
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
|
||||
import MediaAssetMoreMenu from './MediaAssetMoreMenu.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
menuStateChanged: [isOpen: boolean]
|
||||
}>()
|
||||
|
||||
const { asset } = inject(MediaAssetKey)!
|
||||
const actions = useMediaAssetActions()
|
||||
|
||||
const handleDelete = () => {
|
||||
if (asset.value) {
|
||||
actions.deleteAsset(asset.value.id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
if (asset.value) {
|
||||
actions.downloadAsset(asset.value.id)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,4 @@
|
||||
<template>
|
||||
<div class="h-[1px] bg-neutral-200 dark-theme:bg-neutral-700"></div>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
318
src/platform/assets/components/MediaAssetCard.stories.ts
Normal file
318
src/platform/assets/components/MediaAssetCard.stories.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import MediaAssetCard from './MediaAssetCard.vue'
|
||||
|
||||
const meta: Meta<typeof MediaAssetCard> = {
|
||||
title: 'AssetLibrary/MediaAssetCard',
|
||||
component: MediaAssetCard,
|
||||
argTypes: {
|
||||
context: {
|
||||
control: 'select',
|
||||
options: ['input', 'output']
|
||||
},
|
||||
loading: {
|
||||
control: 'boolean'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Public sample media URLs
|
||||
const SAMPLE_MEDIA = {
|
||||
image1: 'https://i.imgur.com/OB0y6MR.jpg',
|
||||
image2: 'https://i.imgur.com/CzXTtJV.jpg',
|
||||
image3: 'https://farm9.staticflickr.com/8505/8441256181_4e98d8bff5_z_d.jpg',
|
||||
video:
|
||||
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
|
||||
videoThumbnail:
|
||||
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg',
|
||||
audio: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
|
||||
}
|
||||
|
||||
const sampleAsset: AssetMeta = {
|
||||
id: 'asset-1',
|
||||
name: 'sample-image.png',
|
||||
kind: 'image',
|
||||
duration: 3345,
|
||||
size: 2048576,
|
||||
created_at: Date.now().toString(),
|
||||
src: SAMPLE_MEDIA.image1,
|
||||
dimensions: {
|
||||
width: 1920,
|
||||
height: 1080
|
||||
},
|
||||
tags: []
|
||||
}
|
||||
|
||||
export const ImageAsset: Story = {
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div style="max-width: 280px;"><story /></div>'
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'output', outputCount: 3 },
|
||||
asset: sampleAsset,
|
||||
loading: false
|
||||
}
|
||||
}
|
||||
|
||||
export const VideoAsset: Story = {
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div style="max-width: 280px;"><story /></div>'
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'input' },
|
||||
asset: {
|
||||
...sampleAsset,
|
||||
id: 'asset-2',
|
||||
name: 'Big_Buck_Bunny.mp4',
|
||||
kind: 'video',
|
||||
size: 10485760,
|
||||
duration: 13425,
|
||||
preview_url: SAMPLE_MEDIA.videoThumbnail, // Poster image
|
||||
src: SAMPLE_MEDIA.video, // Actual video file
|
||||
dimensions: {
|
||||
width: 1280,
|
||||
height: 720
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const Model3DAsset: Story = {
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div style="max-width: 280px;"><story /></div>'
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'input' },
|
||||
asset: {
|
||||
...sampleAsset,
|
||||
id: 'asset-3',
|
||||
name: 'Asset-3d-model.glb',
|
||||
kind: '3D',
|
||||
size: 7340032,
|
||||
src: '',
|
||||
dimensions: undefined,
|
||||
duration: 18023
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const AudioAsset: Story = {
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div style="max-width: 280px;"><story /></div>'
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'input' },
|
||||
asset: {
|
||||
...sampleAsset,
|
||||
id: 'asset-3',
|
||||
name: 'SoundHelix-Song.mp3',
|
||||
kind: 'audio',
|
||||
size: 5242880,
|
||||
src: SAMPLE_MEDIA.audio,
|
||||
dimensions: undefined,
|
||||
duration: 23180
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const LoadingState: Story = {
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div style="max-width: 280px;"><story /></div>'
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'input' },
|
||||
asset: sampleAsset,
|
||||
loading: true
|
||||
}
|
||||
}
|
||||
|
||||
export const LongFileName: Story = {
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div style="max-width: 280px;"><story /></div>'
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'input' },
|
||||
asset: {
|
||||
...sampleAsset,
|
||||
name: 'very-long-file-name-that-should-be-truncated-in-the-ui-to-prevent-overflow.png'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const SelectedState: Story = {
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div style="max-width: 280px;"><story /></div>'
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'output', outputCount: 2 },
|
||||
asset: sampleAsset,
|
||||
selected: true
|
||||
}
|
||||
}
|
||||
|
||||
export const WebMVideo: Story = {
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div style="max-width: 280px;"><story /></div>'
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'input' },
|
||||
asset: {
|
||||
id: 'asset-webm',
|
||||
name: 'animated-clip.webm',
|
||||
kind: 'video',
|
||||
size: 3145728,
|
||||
created_at: Date.now().toString(),
|
||||
preview_url: SAMPLE_MEDIA.image1, // Poster image
|
||||
src: 'https://www.w3schools.com/html/movie.mp4', // Actual video
|
||||
duration: 620,
|
||||
dimensions: {
|
||||
width: 640,
|
||||
height: 360
|
||||
},
|
||||
tags: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const GifAnimation: Story = {
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div style="max-width: 280px;"><story /></div>'
|
||||
})
|
||||
],
|
||||
args: {
|
||||
context: { type: 'input' },
|
||||
asset: {
|
||||
id: 'asset-gif',
|
||||
name: 'animation.gif',
|
||||
kind: 'image',
|
||||
size: 1572864,
|
||||
duration: 1345,
|
||||
created_at: Date.now().toString(),
|
||||
src: 'https://media.giphy.com/media/3o7aCTPPm4OHfRLSH6/giphy.gif',
|
||||
dimensions: {
|
||||
width: 480,
|
||||
height: 270
|
||||
},
|
||||
tags: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const GridLayout: Story = {
|
||||
render: () => ({
|
||||
components: { MediaAssetCard },
|
||||
setup() {
|
||||
const assets: AssetMeta[] = [
|
||||
{
|
||||
id: 'grid-1',
|
||||
name: 'image-file.jpg',
|
||||
kind: 'image',
|
||||
size: 2097152,
|
||||
duration: 4500,
|
||||
created_at: Date.now().toString(),
|
||||
src: SAMPLE_MEDIA.image1,
|
||||
dimensions: { width: 1920, height: 1080 },
|
||||
tags: []
|
||||
},
|
||||
{
|
||||
id: 'grid-2',
|
||||
name: 'image-file.jpg',
|
||||
kind: 'image',
|
||||
size: 2097152,
|
||||
duration: 4500,
|
||||
created_at: Date.now().toString(),
|
||||
src: SAMPLE_MEDIA.image2,
|
||||
dimensions: { width: 1920, height: 1080 },
|
||||
tags: []
|
||||
},
|
||||
{
|
||||
id: 'grid-3',
|
||||
name: 'video-file.mp4',
|
||||
kind: 'video',
|
||||
size: 10485760,
|
||||
duration: 13425,
|
||||
created_at: Date.now().toString(),
|
||||
preview_url: SAMPLE_MEDIA.videoThumbnail, // Poster image
|
||||
src: SAMPLE_MEDIA.video, // Actual video
|
||||
dimensions: { width: 1280, height: 720 },
|
||||
tags: []
|
||||
},
|
||||
{
|
||||
id: 'grid-4',
|
||||
name: 'audio-file.mp3',
|
||||
kind: 'audio',
|
||||
size: 5242880,
|
||||
duration: 180,
|
||||
created_at: Date.now().toString(),
|
||||
src: SAMPLE_MEDIA.audio,
|
||||
tags: []
|
||||
},
|
||||
{
|
||||
id: 'grid-5',
|
||||
name: 'animation.gif',
|
||||
kind: 'image',
|
||||
size: 3145728,
|
||||
duration: 1345,
|
||||
created_at: Date.now().toString(),
|
||||
src: 'https://media.giphy.com/media/l0HlNaQ6gWfllcjDO/giphy.gif',
|
||||
dimensions: { width: 480, height: 360 },
|
||||
tags: []
|
||||
},
|
||||
{
|
||||
id: 'grid-6',
|
||||
name: 'Asset-3d-model.glb',
|
||||
kind: '3D',
|
||||
size: 7340032,
|
||||
src: '',
|
||||
dimensions: undefined,
|
||||
duration: 18023,
|
||||
created_at: Date.now().toString(),
|
||||
tags: []
|
||||
},
|
||||
{
|
||||
id: 'grid-7',
|
||||
name: 'image-file.jpg',
|
||||
kind: 'image',
|
||||
size: 2097152,
|
||||
duration: 4500,
|
||||
created_at: Date.now().toString(),
|
||||
src: SAMPLE_MEDIA.image3,
|
||||
dimensions: { width: 1920, height: 1080 },
|
||||
tags: []
|
||||
}
|
||||
]
|
||||
return { assets }
|
||||
},
|
||||
template: `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; padding: 16px;">
|
||||
<MediaAssetCard
|
||||
v-for="asset in assets"
|
||||
:key="asset.id"
|
||||
:context="{ type: Math.random() > 0.5 ? 'input' : 'output', outputCount: Math.floor(Math.random() * 5) }"
|
||||
:asset="asset"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
233
src/platform/assets/components/MediaAssetCard.vue
Normal file
233
src/platform/assets/components/MediaAssetCard.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<CardContainer
|
||||
ref="cardContainerRef"
|
||||
role="button"
|
||||
:aria-label="
|
||||
asset ? `${asset.name} - ${asset.kind} asset` : 'Loading asset'
|
||||
"
|
||||
:tabindex="loading ? -1 : 0"
|
||||
size="mini"
|
||||
variant="ghost"
|
||||
rounded="lg"
|
||||
:class="containerClasses"
|
||||
@click="handleCardClick"
|
||||
@keydown.enter="handleCardClick"
|
||||
@keydown.space.prevent="handleCardClick"
|
||||
>
|
||||
<template #top>
|
||||
<CardTop
|
||||
ratio="square"
|
||||
:bottom-left-class="durationChipClasses"
|
||||
:bottom-right-class="durationChipClasses"
|
||||
>
|
||||
<!-- Loading State -->
|
||||
<template v-if="loading">
|
||||
<div
|
||||
class="h-full w-full animate-pulse rounded-lg bg-zinc-200 dark-theme:bg-zinc-700"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Content based on asset type -->
|
||||
<template v-else-if="asset">
|
||||
<component
|
||||
:is="getTopComponent(asset.kind)"
|
||||
:asset="asset"
|
||||
:context="context"
|
||||
@view="actions.viewAsset(asset!.id)"
|
||||
@download="actions.downloadAsset(asset!.id)"
|
||||
@play="actions.playAsset(asset!.id)"
|
||||
@video-playing-state-changed="isVideoPlaying = $event"
|
||||
@video-controls-changed="showVideoControls = $event"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Actions overlay (top-left) - show on hover or when menu is open, but not when video is playing -->
|
||||
<template v-if="showActionsOverlay" #top-left>
|
||||
<MediaAssetActions @menu-state-changed="isMenuOpen = $event" />
|
||||
</template>
|
||||
|
||||
<!-- Zoom button (top-right) - show on hover, but not when video is playing -->
|
||||
<template v-if="showZoomOverlay" #top-right>
|
||||
<IconButton size="sm" @click="actions.viewAsset(asset!.id)">
|
||||
<i class="icon-[lucide--zoom-in] size-4" />
|
||||
</IconButton>
|
||||
</template>
|
||||
|
||||
<!-- Duration/Format chips (bottom-left) - hide when video is playing -->
|
||||
<template v-if="showDurationChips" #bottom-left>
|
||||
<SquareChip variant="light" :label="formattedDuration" />
|
||||
<SquareChip v-if="fileFormat" variant="light" :label="fileFormat" />
|
||||
</template>
|
||||
|
||||
<!-- Output count (bottom-right) - hide when video is playing -->
|
||||
<template v-if="showOutputCount" #bottom-right>
|
||||
<IconTextButton
|
||||
type="secondary"
|
||||
size="sm"
|
||||
:label="context?.outputCount?.toString() ?? '0'"
|
||||
@click="actions.openMoreOutputs(asset?.id || '')"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--layers] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</template>
|
||||
</CardTop>
|
||||
</template>
|
||||
|
||||
<template #bottom>
|
||||
<CardBottom>
|
||||
<!-- Loading State -->
|
||||
<template v-if="loading">
|
||||
<div class="flex flex-col items-center justify-between gap-1">
|
||||
<div
|
||||
class="h-4 w-2/3 animate-pulse rounded bg-zinc-200 dark-theme:bg-zinc-700"
|
||||
/>
|
||||
<div
|
||||
class="h-3 w-1/2 animate-pulse rounded bg-zinc-200 dark-theme:bg-zinc-700"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Content based on asset type -->
|
||||
<template v-else-if="asset">
|
||||
<component
|
||||
:is="getBottomComponent(asset.kind)"
|
||||
:asset="asset"
|
||||
:context="context"
|
||||
/>
|
||||
</template>
|
||||
</CardBottom>
|
||||
</template>
|
||||
</CardContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useElementHover } from '@vueuse/core'
|
||||
import { computed, defineAsyncComponent, provide, ref, toRef } from 'vue'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import CardBottom from '@/components/card/CardBottom.vue'
|
||||
import CardContainer from '@/components/card/CardContainer.vue'
|
||||
import CardTop from '@/components/card/CardTop.vue'
|
||||
import SquareChip from '@/components/chip/SquareChip.vue'
|
||||
import { formatDuration } from '@/utils/formatUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { useMediaAssetActions } from '../composables/useMediaAssetActions'
|
||||
import type {
|
||||
AssetContext,
|
||||
AssetMeta,
|
||||
MediaKind
|
||||
} from '../schemas/mediaAssetSchema'
|
||||
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
|
||||
import MediaAssetActions from './MediaAssetActions.vue'
|
||||
|
||||
const mediaComponents = {
|
||||
top: {
|
||||
video: defineAsyncComponent(() => import('./MediaVideoTop.vue')),
|
||||
audio: defineAsyncComponent(() => import('./MediaAudioTop.vue')),
|
||||
image: defineAsyncComponent(() => import('./MediaImageTop.vue')),
|
||||
'3D': defineAsyncComponent(() => import('./Media3DTop.vue'))
|
||||
},
|
||||
bottom: {
|
||||
video: defineAsyncComponent(() => import('./MediaVideoBottom.vue')),
|
||||
audio: defineAsyncComponent(() => import('./MediaAudioBottom.vue')),
|
||||
image: defineAsyncComponent(() => import('./MediaImageBottom.vue')),
|
||||
'3D': defineAsyncComponent(() => import('./Media3DBottom.vue'))
|
||||
}
|
||||
}
|
||||
|
||||
function getTopComponent(kind: MediaKind) {
|
||||
return mediaComponents.top[kind] || mediaComponents.top.image
|
||||
}
|
||||
|
||||
function getBottomComponent(kind: MediaKind) {
|
||||
return mediaComponents.bottom[kind] || mediaComponents.bottom.image
|
||||
}
|
||||
|
||||
const { context, asset, loading, selected } = defineProps<{
|
||||
context: AssetContext
|
||||
asset?: AssetMeta
|
||||
loading?: boolean
|
||||
selected?: boolean
|
||||
}>()
|
||||
|
||||
const cardContainerRef = ref<HTMLElement>()
|
||||
|
||||
const isVideoPlaying = ref(false)
|
||||
const isMenuOpen = ref(false)
|
||||
const showVideoControls = ref(false)
|
||||
|
||||
const isHovered = useElementHover(cardContainerRef)
|
||||
|
||||
const actions = useMediaAssetActions()
|
||||
|
||||
provide(MediaAssetKey, {
|
||||
asset: toRef(() => asset),
|
||||
context: toRef(() => context),
|
||||
isVideoPlaying,
|
||||
showVideoControls
|
||||
})
|
||||
|
||||
const containerClasses = computed(() => {
|
||||
return cn(
|
||||
'gap-1',
|
||||
selected
|
||||
? 'border-3 border-zinc-900 dark-theme:border-white bg-zinc-200 dark-theme:bg-zinc-700'
|
||||
: 'hover:bg-zinc-100 dark-theme:hover:bg-zinc-800'
|
||||
)
|
||||
})
|
||||
|
||||
const formattedDuration = computed(() => {
|
||||
if (!asset?.duration) return ''
|
||||
return formatDuration(asset.duration)
|
||||
})
|
||||
|
||||
const fileFormat = computed(() => {
|
||||
if (!asset?.name) return ''
|
||||
const parts = asset.name.split('.')
|
||||
return parts.length > 1 ? parts[parts.length - 1].toUpperCase() : ''
|
||||
})
|
||||
|
||||
const durationChipClasses = computed(() => {
|
||||
if (asset?.kind === 'audio') {
|
||||
return '-translate-y-11'
|
||||
}
|
||||
if (asset?.kind === 'video' && showVideoControls.value) {
|
||||
return '-translate-y-16'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const showHoverActions = computed(() => {
|
||||
return !loading && !!asset && (isHovered.value || isMenuOpen.value)
|
||||
})
|
||||
|
||||
const showZoomButton = computed(() => {
|
||||
return asset?.kind === 'image' || asset?.kind === '3D'
|
||||
})
|
||||
|
||||
const showActionsOverlay = computed(() => {
|
||||
return showHoverActions.value && !isVideoPlaying.value
|
||||
})
|
||||
|
||||
const showZoomOverlay = computed(() => {
|
||||
return showHoverActions.value && showZoomButton.value && !isVideoPlaying.value
|
||||
})
|
||||
|
||||
const showDurationChips = computed(() => {
|
||||
return !loading && asset?.duration && !isVideoPlaying.value
|
||||
})
|
||||
|
||||
const showOutputCount = computed(() => {
|
||||
return !loading && context?.outputCount && !isVideoPlaying.value
|
||||
})
|
||||
|
||||
const handleCardClick = () => {
|
||||
if (asset) {
|
||||
actions.selectAsset(asset)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
158
src/platform/assets/components/MediaAssetMoreMenu.vue
Normal file
158
src/platform/assets/components/MediaAssetMoreMenu.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<IconTextButton
|
||||
type="transparent"
|
||||
class="dark-theme:text-white"
|
||||
label="Inspect asset"
|
||||
@click="handleInspect"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--zoom-in] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
|
||||
<IconTextButton
|
||||
type="transparent"
|
||||
class="dark-theme:text-white"
|
||||
label="Add to current workflow"
|
||||
@click="handleAddToWorkflow"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[comfy--node] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
|
||||
<IconTextButton
|
||||
type="transparent"
|
||||
class="dark-theme:text-white"
|
||||
label="Download"
|
||||
@click="handleDownload"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--download] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
|
||||
<MediaAssetButtonDivider />
|
||||
|
||||
<IconTextButton
|
||||
v-if="showWorkflowOptions"
|
||||
type="transparent"
|
||||
class="dark-theme:text-white"
|
||||
label="Open as workflow in new tab"
|
||||
@click="handleOpenWorkflow"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[comfy--workflow] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
|
||||
<IconTextButton
|
||||
v-if="showWorkflowOptions"
|
||||
type="transparent"
|
||||
class="dark-theme:text-white"
|
||||
label="Export workflow"
|
||||
@click="handleExportWorkflow"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--file-output] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
|
||||
<MediaAssetButtonDivider v-if="showWorkflowOptions" />
|
||||
|
||||
<IconTextButton
|
||||
type="transparent"
|
||||
class="dark-theme:text-white"
|
||||
label="Copy job ID"
|
||||
@click="handleCopyJobId"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--copy] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
|
||||
<MediaAssetButtonDivider />
|
||||
|
||||
<IconTextButton
|
||||
type="transparent"
|
||||
class="dark-theme:text-white"
|
||||
label="Delete"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
|
||||
import { useMediaAssetActions } from '../composables/useMediaAssetActions'
|
||||
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
|
||||
import MediaAssetButtonDivider from './MediaAssetButtonDivider.vue'
|
||||
|
||||
const { close } = defineProps<{
|
||||
close: () => void
|
||||
}>()
|
||||
|
||||
const { asset, context } = inject(MediaAssetKey)!
|
||||
const actions = useMediaAssetActions()
|
||||
|
||||
const showWorkflowOptions = computed(() => {
|
||||
return context.value.type
|
||||
})
|
||||
|
||||
const handleInspect = () => {
|
||||
if (asset.value) {
|
||||
actions.viewAsset(asset.value.id)
|
||||
}
|
||||
close()
|
||||
}
|
||||
|
||||
const handleAddToWorkflow = () => {
|
||||
if (asset.value) {
|
||||
actions.addWorkflow(asset.value.id)
|
||||
}
|
||||
close()
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
if (asset.value) {
|
||||
actions.downloadAsset(asset.value.id)
|
||||
}
|
||||
close()
|
||||
}
|
||||
|
||||
const handleOpenWorkflow = () => {
|
||||
if (asset.value) {
|
||||
actions.openWorkflow(asset.value.id)
|
||||
}
|
||||
close()
|
||||
}
|
||||
|
||||
const handleExportWorkflow = () => {
|
||||
if (asset.value) {
|
||||
actions.exportWorkflow(asset.value.id)
|
||||
}
|
||||
close()
|
||||
}
|
||||
|
||||
const handleCopyJobId = () => {
|
||||
if (asset.value) {
|
||||
actions.copyAssetUrl(asset.value.id)
|
||||
}
|
||||
close()
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
if (asset.value) {
|
||||
actions.deleteAsset(asset.value.id)
|
||||
}
|
||||
close()
|
||||
}
|
||||
</script>
|
||||
30
src/platform/assets/components/MediaAudioBottom.vue
Normal file
30
src/platform/assets/components/MediaAudioBottom.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<h3
|
||||
class="m-0 line-clamp-1 text-sm font-bold text-zinc-900 dark-theme:text-white"
|
||||
:title="asset.name"
|
||||
>
|
||||
{{ fileName }}
|
||||
</h3>
|
||||
<div class="flex items-center gap-2 text-xs text-zinc-400">
|
||||
<span>{{ formatSize(asset.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { formatSize, getFilenameDetails } from '@/utils/formatUtil'
|
||||
|
||||
import type { AssetContext, AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
|
||||
const { asset } = defineProps<{
|
||||
asset: AssetMeta
|
||||
context: AssetContext
|
||||
}>()
|
||||
|
||||
const fileName = computed(() => {
|
||||
return getFilenameDetails(asset.name).filename
|
||||
})
|
||||
</script>
|
||||
28
src/platform/assets/components/MediaAudioTop.vue
Normal file
28
src/platform/assets/components/MediaAudioTop.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="relative h-full w-full overflow-hidden rounded">
|
||||
<div
|
||||
class="flex h-full w-full flex-col items-center justify-center gap-2 bg-zinc-200 dark-theme:bg-zinc-700/50"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--music] text-3xl text-zinc-600 dark-theme:text-zinc-200"
|
||||
/>
|
||||
<span class="text-zinc-600 dark-theme:text-zinc-200">{{
|
||||
$t('Audio')
|
||||
}}</span>
|
||||
</div>
|
||||
<audio
|
||||
controls
|
||||
class="absolute bottom-0 left-0 w-full p-2"
|
||||
:src="asset.src"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
|
||||
const { asset } = defineProps<{
|
||||
asset: AssetMeta
|
||||
}>()
|
||||
</script>
|
||||
30
src/platform/assets/components/MediaImageBottom.vue
Normal file
30
src/platform/assets/components/MediaImageBottom.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<h3
|
||||
class="m-0 line-clamp-1 text-sm font-bold text-zinc-900 dark-theme:text-white"
|
||||
:title="asset.name"
|
||||
>
|
||||
{{ fileName }}
|
||||
</h3>
|
||||
<div class="flex items-center text-xs text-zinc-400">
|
||||
<span>{{ asset.dimensions?.width }}x{{ asset.dimensions?.height }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { getFilenameDetails } from '@/utils/formatUtil'
|
||||
|
||||
import type { AssetContext, AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
|
||||
const { asset } = defineProps<{
|
||||
asset: AssetMeta
|
||||
context: AssetContext
|
||||
}>()
|
||||
|
||||
const fileName = computed(() => {
|
||||
return getFilenameDetails(asset.name).filename
|
||||
})
|
||||
</script>
|
||||
27
src/platform/assets/components/MediaImageTop.vue
Normal file
27
src/platform/assets/components/MediaImageTop.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="relative h-full w-full overflow-hidden rounded">
|
||||
<LazyImage
|
||||
v-if="asset.src"
|
||||
:src="asset.src"
|
||||
:alt="asset.name"
|
||||
:container-class="'aspect-square'"
|
||||
:image-class="'w-full h-full object-cover'"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex h-full w-full items-center justify-center bg-zinc-200 dark-theme:bg-zinc-700/50"
|
||||
>
|
||||
<i class="pi pi-image text-3xl text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LazyImage from '@/components/common/LazyImage.vue'
|
||||
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
|
||||
const { asset } = defineProps<{
|
||||
asset: AssetMeta
|
||||
}>()
|
||||
</script>
|
||||
30
src/platform/assets/components/MediaVideoBottom.vue
Normal file
30
src/platform/assets/components/MediaVideoBottom.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<h3
|
||||
class="m-0 line-clamp-1 text-sm font-bold text-zinc-900 dark-theme:text-white"
|
||||
:title="asset.name"
|
||||
>
|
||||
{{ fileName }}
|
||||
</h3>
|
||||
<div class="flex items-center text-xs text-zinc-400">
|
||||
<span>{{ asset.dimensions?.width }}x{{ asset.dimensions?.height }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { getFilenameDetails } from '@/utils/formatUtil'
|
||||
|
||||
import type { AssetContext, AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
|
||||
const { asset } = defineProps<{
|
||||
asset: AssetMeta
|
||||
context: AssetContext
|
||||
}>()
|
||||
|
||||
const fileName = computed(() => {
|
||||
return getFilenameDetails(asset.name).filename
|
||||
})
|
||||
</script>
|
||||
57
src/platform/assets/components/MediaVideoTop.vue
Normal file
57
src/platform/assets/components/MediaVideoTop.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative h-full w-full overflow-hidden rounded bg-black"
|
||||
@mouseenter="showControls = true"
|
||||
@mouseleave="showControls = false"
|
||||
>
|
||||
<video
|
||||
ref="videoRef"
|
||||
:controls="showControls"
|
||||
preload="none"
|
||||
:poster="asset.preview_url"
|
||||
class="relative h-full w-full object-contain"
|
||||
@click.stop
|
||||
@play="onVideoPlay"
|
||||
@pause="onVideoPause"
|
||||
>
|
||||
<source :src="asset.src || ''" />
|
||||
</video>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
import type { AssetContext, AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
|
||||
const { asset } = defineProps<{
|
||||
asset: AssetMeta
|
||||
context: AssetContext
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
play: [assetId: string]
|
||||
videoPlayingStateChanged: [isPlaying: boolean]
|
||||
videoControlsChanged: [showControls: boolean]
|
||||
}>()
|
||||
|
||||
const videoRef = ref<HTMLVideoElement>()
|
||||
const showControls = ref(true)
|
||||
|
||||
watch(showControls, (controlsVisible) => {
|
||||
emit('videoControlsChanged', controlsVisible)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
emit('videoControlsChanged', showControls.value)
|
||||
})
|
||||
|
||||
const onVideoPlay = () => {
|
||||
showControls.value = true
|
||||
emit('videoPlayingStateChanged', true)
|
||||
}
|
||||
|
||||
const onVideoPause = () => {
|
||||
emit('videoPlayingStateChanged', false)
|
||||
}
|
||||
</script>
|
||||
@@ -2,7 +2,8 @@ import { t } from '@/i18n'
|
||||
import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import { type DialogComponentProps, useDialogStore } from '@/stores/dialogStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import type { DialogComponentProps } from '@/stores/dialogStore'
|
||||
|
||||
interface ShowOptions {
|
||||
/** ComfyUI node type for context (e.g., 'CheckpointLoaderSimple') */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { uniqWith } from 'es-toolkit'
|
||||
import { type MaybeRefOrGetter, computed, toValue } from 'vue'
|
||||
import { computed, toValue } from 'vue'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
|
||||
import type { SelectOption } from '@/components/input/types'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
|
||||
62
src/platform/assets/composables/useMediaAssetActions.ts
Normal file
62
src/platform/assets/composables/useMediaAssetActions.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable no-console */
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
|
||||
export function useMediaAssetActions() {
|
||||
const selectAsset = (asset: AssetMeta) => {
|
||||
console.log('Asset selected:', asset)
|
||||
}
|
||||
|
||||
const viewAsset = (assetId: string) => {
|
||||
console.log('Viewing asset:', assetId)
|
||||
}
|
||||
|
||||
const downloadAsset = (assetId: string) => {
|
||||
console.log('Downloading asset:', assetId)
|
||||
}
|
||||
|
||||
const deleteAsset = (assetId: string) => {
|
||||
console.log('Deleting asset:', assetId)
|
||||
}
|
||||
|
||||
const playAsset = (assetId: string) => {
|
||||
console.log('Playing asset:', assetId)
|
||||
}
|
||||
|
||||
const copyAssetUrl = (assetId: string) => {
|
||||
console.log('Copy asset URL:', assetId)
|
||||
}
|
||||
|
||||
const copyJobId = (jobId: string) => {
|
||||
console.log('Copy job ID:', jobId)
|
||||
}
|
||||
|
||||
const addWorkflow = (assetId: string) => {
|
||||
console.log('Adding asset to workflow:', assetId)
|
||||
}
|
||||
|
||||
const openWorkflow = (assetId: string) => {
|
||||
console.log('Opening workflow for asset:', assetId)
|
||||
}
|
||||
|
||||
const exportWorkflow = (assetId: string) => {
|
||||
console.log('Exporting workflow for asset:', assetId)
|
||||
}
|
||||
|
||||
const openMoreOutputs = (assetId: string) => {
|
||||
console.log('Opening more outputs for asset:', assetId)
|
||||
}
|
||||
|
||||
return {
|
||||
selectAsset,
|
||||
viewAsset,
|
||||
downloadAsset,
|
||||
deleteAsset,
|
||||
playAsset,
|
||||
copyAssetUrl,
|
||||
copyJobId,
|
||||
addWorkflow,
|
||||
openWorkflow,
|
||||
exportWorkflow,
|
||||
openMoreOutputs
|
||||
}
|
||||
}
|
||||
46
src/platform/assets/schemas/mediaAssetSchema.ts
Normal file
46
src/platform/assets/schemas/mediaAssetSchema.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { assetItemSchema } from './assetSchema'
|
||||
|
||||
const zMediaKindSchema = z.enum(['video', 'audio', 'image', '3D'])
|
||||
export type MediaKind = z.infer<typeof zMediaKindSchema>
|
||||
|
||||
const zDimensionsSchema = z.object({
|
||||
width: z.number().positive(),
|
||||
height: z.number().positive()
|
||||
})
|
||||
|
||||
// Extend the base asset schema with media-specific fields
|
||||
const zMediaAssetDisplayItemSchema = assetItemSchema.extend({
|
||||
// New required fields
|
||||
kind: zMediaKindSchema,
|
||||
src: z.string().url(),
|
||||
|
||||
// New optional fields
|
||||
duration: z.number().nonnegative().optional(),
|
||||
dimensions: zDimensionsSchema.optional(),
|
||||
jobId: z.string().optional(),
|
||||
isMulti: z.boolean().optional()
|
||||
})
|
||||
|
||||
// Asset context schema
|
||||
const zAssetContextSchema = z.object({
|
||||
type: z.enum(['input', 'output']),
|
||||
outputCount: z.number().positive().optional() // Only for output context
|
||||
})
|
||||
|
||||
// Export the inferred types
|
||||
export type AssetMeta = z.infer<typeof zMediaAssetDisplayItemSchema>
|
||||
export type AssetContext = z.infer<typeof zAssetContextSchema>
|
||||
|
||||
// Injection key for MediaAsset provide/inject pattern
|
||||
interface MediaAssetProviderValue {
|
||||
asset: Ref<AssetMeta | undefined>
|
||||
context: Ref<AssetContext>
|
||||
isVideoPlaying: Ref<boolean>
|
||||
showVideoControls: Ref<boolean>
|
||||
}
|
||||
|
||||
export const MediaAssetKey: InjectionKey<MediaAssetProviderValue> =
|
||||
Symbol('mediaAsset')
|
||||
@@ -1,11 +1,11 @@
|
||||
import { fromZodError } from 'zod-validation-error'
|
||||
|
||||
import {
|
||||
type AssetItem,
|
||||
type AssetResponse,
|
||||
type ModelFile,
|
||||
type ModelFolder,
|
||||
assetResponseSchema
|
||||
import { assetResponseSchema } from '@/platform/assets/schemas/assetSchema'
|
||||
import type {
|
||||
AssetItem,
|
||||
AssetResponse,
|
||||
ModelFile,
|
||||
ModelFolder
|
||||
} from '@/platform/assets/schemas/assetSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import {
|
||||
type LGraphNode,
|
||||
LiteGraph,
|
||||
type Point
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
type AssetItem,
|
||||
assetItemSchema
|
||||
} from '@/platform/assets/schemas/assetSchema'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode, Point } from '@/lib/litegraph/src/litegraph'
|
||||
import { assetItemSchema } from '@/platform/assets/schemas/assetSchema'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import {
|
||||
MISSING_TAG,
|
||||
MODELS_TAG
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="h-full flex items-center justify-center p-8">
|
||||
<div class="lg:w-96 max-w-[100vw] text-center">
|
||||
<h2 class="text-xl mb-4">
|
||||
<div class="flex h-full items-center justify-center p-8">
|
||||
<div class="max-w-[100vw] text-center lg:w-96">
|
||||
<h2 class="mb-4 text-xl">
|
||||
{{ $t('cloudOnboarding.authTimeout.title') }}
|
||||
</h2>
|
||||
<p class="mb-6 text-gray-600">
|
||||
|
||||
@@ -3,40 +3,40 @@
|
||||
<main class="w-full max-w-md px-6 py-12 text-center" role="main">
|
||||
<!-- Title -->
|
||||
<h1
|
||||
class="text-white font-abcrom font-black italic uppercase my-0 text-3xl"
|
||||
class="font-abcrom my-0 text-3xl font-black text-white uppercase italic"
|
||||
>
|
||||
{{ t('cloudInvite_title') }}
|
||||
</h1>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<p v-if="inviteCodeClaimed" class="mt-6 text-amber-500 leading-relaxed">
|
||||
<p v-if="inviteCodeClaimed" class="mt-6 leading-relaxed text-amber-500">
|
||||
{{ t('cloudInvite_alreadyClaimed_prefix') }}
|
||||
<strong>{{ userEmail }}</strong>
|
||||
</p>
|
||||
<p
|
||||
v-else-if="inviteCodeExpired"
|
||||
class="mt-6 text-amber-500 leading-relaxed"
|
||||
class="mt-6 leading-relaxed text-amber-500"
|
||||
>
|
||||
{{ t('cloudInvite_expired_prefix') }}
|
||||
</p>
|
||||
<p v-else class="mt-6 text-neutral-300 leading-relaxed">
|
||||
<p v-else class="mt-6 leading-relaxed text-neutral-300">
|
||||
{{ t('cloudInvite_subtitle') }}
|
||||
</p>
|
||||
|
||||
<div v-if="inviteCodeClaimed || inviteCodeExpired" class="mb-2">
|
||||
<span
|
||||
class="text-blue-400 no-underline cursor-pointer"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
@click="onClickSupport"
|
||||
>
|
||||
{{ t('cloudInvite_contactLink') }}</span
|
||||
>
|
||||
<span class="text-neutral-400 ml-2">
|
||||
<span class="ml-2 text-neutral-400">
|
||||
{{ t('cloudInvite_contactLink_suffix') }}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class="text-blue-400 no-underline cursor-pointer"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
@click="onSwitchAccounts"
|
||||
>
|
||||
{{ t('cloudInvite_switchAccounts') }}</span
|
||||
@@ -52,14 +52,14 @@
|
||||
<div class="mt-4 flex flex-col items-center justify-center gap-4">
|
||||
<!-- Avatar box -->
|
||||
<div
|
||||
class="relative grid place-items-center h-28 w-28 rounded-2xl border border-neutral-700 bg-neutral-800 shadow-inner"
|
||||
class="relative grid h-28 w-28 place-items-center rounded-2xl border border-neutral-700 bg-neutral-800 shadow-inner"
|
||||
>
|
||||
<span class="text-5xl font-semibold select-none">{{
|
||||
userInitial
|
||||
}}</span>
|
||||
<!-- subtle ring to mimic screenshot gradient border -->
|
||||
<span
|
||||
class="pointer-events-none absolute inset-0 rounded-2xl ring-1 ring-inset ring-neutral-600/40"
|
||||
class="pointer-events-none absolute inset-0 rounded-2xl ring-1 ring-neutral-600/40 ring-inset"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
? t('cloudInvite_processing')
|
||||
: t('cloudInvite_acceptButton')
|
||||
"
|
||||
class="w-full h-12 font-medium mt-12 text-white"
|
||||
class="mt-12 h-12 w-full font-medium text-white"
|
||||
:disabled="processing || inviteCodeClaimed || inviteCodeExpired"
|
||||
@click="onClaim"
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="h-full flex items-center justify-center p-8">
|
||||
<div class="lg:w-96 max-w-[100vw] p-2">
|
||||
<div class="flex h-full items-center justify-center p-8">
|
||||
<div class="max-w-[100vw] p-2 lg:w-96">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col gap-4 mb-8">
|
||||
<h1 class="text-xl font-medium leading-normal my-0">
|
||||
<div class="mb-8 flex flex-col gap-4">
|
||||
<h1 class="my-0 text-xl leading-normal font-medium">
|
||||
{{ t('cloudForgotPassword_title') }}
|
||||
</h1>
|
||||
<p class="text-base my-0 text-muted">
|
||||
<p class="my-0 text-base text-muted">
|
||||
{{ t('cloudForgotPassword_instructions') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -15,7 +15,7 @@
|
||||
<form class="flex flex-col gap-6" @submit.prevent="handleSubmit">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
class="opacity-80 text-base font-medium mb-2"
|
||||
class="mb-2 text-base font-medium opacity-80"
|
||||
for="reset-email"
|
||||
>
|
||||
{{ t('cloudForgotPassword_emailLabel') }}
|
||||
@@ -63,7 +63,7 @@
|
||||
{{ t('cloudForgotPassword_didntReceiveEmail') }}
|
||||
<a
|
||||
href="https://support.comfy.org"
|
||||
class="text-blue-400 no-underline cursor-pointer"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="h-full flex items-center justify-center p-8">
|
||||
<div class="lg:w-96 max-w-[100vw] p-2">
|
||||
<div class="flex h-full items-center justify-center p-8">
|
||||
<div class="max-w-[100vw] p-2 lg:w-96">
|
||||
<template v-if="!hasInviteCode">
|
||||
<div class="bg-[#2d2e32] p-4 rounded-lg">
|
||||
<div class="rounded-lg bg-[#2d2e32] p-4">
|
||||
<h4 class="m-0 pb-2 text-lg">
|
||||
{{ t('cloudPrivateBeta_title') }}
|
||||
</h4>
|
||||
@@ -12,11 +12,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col gap-4 mt-6 mb-8">
|
||||
<h1 class="text-xl font-medium leading-normal my-0">
|
||||
<div class="mt-6 mb-8 flex flex-col gap-4">
|
||||
<h1 class="my-0 text-xl leading-normal font-medium">
|
||||
{{ t('auth.login.title') }}
|
||||
</h1>
|
||||
<p class="text-base my-0">
|
||||
<p class="my-0 text-base">
|
||||
<span class="text-muted">{{ t('auth.login.newUser') }}</span>
|
||||
<span
|
||||
class="ml-1 cursor-pointer text-blue-500"
|
||||
@@ -28,13 +28,13 @@
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex flex-col gap-1 mt-6 mb-8">
|
||||
<div class="mt-6 mb-8 flex flex-col gap-1">
|
||||
<h1
|
||||
class="text-white font-abcrom font-black italic uppercase my-0 text-2xl"
|
||||
class="font-abcrom my-0 text-2xl font-black text-white uppercase italic"
|
||||
>
|
||||
{{ t('cloudStart_invited') }}
|
||||
</h1>
|
||||
<p class="text-base my-0">
|
||||
<p class="my-0 text-base">
|
||||
<span class="text-muted">{{ t('cloudStart_invited_signin') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -80,7 +80,7 @@
|
||||
{{ t('cloudWaitlist_questionsText') }}
|
||||
<a
|
||||
href="https://support.comfy.org"
|
||||
class="text-blue-400 no-underline cursor-pointer"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@@ -90,7 +90,7 @@
|
||||
<p v-else class="mt-5 text-sm text-gray-600">
|
||||
{{ t('cloudStart_invited_signup_title') }}
|
||||
<span
|
||||
class="text-blue-400 no-underline cursor-pointer"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
@click="navigateToSignup"
|
||||
>
|
||||
{{ t('cloudStart_invited_signup_description') }}</span
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="h-full flex items-center justify-center p-8">
|
||||
<div class="lg:w-96 max-w-[100vw] p-2">
|
||||
<div class="flex h-full items-center justify-center p-8">
|
||||
<div class="max-w-[100vw] p-2 lg:w-96">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col gap-4 mb-8">
|
||||
<h1 class="text-xl font-medium leading-normal my-0">
|
||||
<div class="mb-8 flex flex-col gap-4">
|
||||
<h1 class="my-0 text-xl leading-normal font-medium">
|
||||
{{ t('auth.signup.title') }}
|
||||
</h1>
|
||||
<p class="text-base my-0">
|
||||
<p class="my-0 text-base">
|
||||
<span class="text-muted">{{
|
||||
t('auth.signup.alreadyHaveAccount')
|
||||
}}</span>
|
||||
@@ -62,7 +62,7 @@
|
||||
<a
|
||||
href="https://www.comfy.org/terms-of-service"
|
||||
target="_blank"
|
||||
class="text-blue-400 no-underline cursor-pointer"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
>
|
||||
{{ t('auth.login.termsLink') }}
|
||||
</a>
|
||||
@@ -70,7 +70,7 @@
|
||||
<a
|
||||
href="/privacy-policy"
|
||||
target="_blank"
|
||||
class="text-blue-400 no-underline cursor-pointer"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
>
|
||||
{{ t('auth.login.privacyLink') }} </a
|
||||
>.
|
||||
@@ -78,7 +78,7 @@
|
||||
{{ t('cloudWaitlist_questionsText') }}
|
||||
<a
|
||||
href="https://support.comfy.org"
|
||||
class="text-blue-400 no-underline cursor-pointer"
|
||||
class="cursor-pointer text-blue-400 no-underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
<div>
|
||||
<Stepper
|
||||
value="1"
|
||||
class="flex flex-col max-h-[80vh] h-[638px] max-w-[90vw] w-[320px]"
|
||||
class="flex h-[638px] max-h-[80vh] w-[320px] max-w-[90vw] flex-col"
|
||||
>
|
||||
<ProgressBar
|
||||
:value="progressPercent"
|
||||
:show-value="false"
|
||||
class="h-2 mb-8"
|
||||
class="mb-8 h-2"
|
||||
/>
|
||||
|
||||
<StepPanels class="p-0 flex-1 flex flex-col">
|
||||
<StepPanels class="flex flex-1 flex-col p-0">
|
||||
<StepPanel
|
||||
v-slot="{ activateCallback }"
|
||||
value="1"
|
||||
class="flex-1 min-h-full flex flex-col justify-between bg-transparent"
|
||||
class="flex min-h-full flex-1 flex-col justify-between bg-transparent"
|
||||
>
|
||||
<div>
|
||||
<label class="text-lg font-medium block mb-8">{{
|
||||
<label class="mb-8 block text-lg font-medium">{{
|
||||
t('cloudSurvey_steps_familiarity')
|
||||
}}</label>
|
||||
<div class="flex flex-col gap-6">
|
||||
@@ -34,7 +34,7 @@
|
||||
/>
|
||||
<label
|
||||
:for="`fam-${opt.value}`"
|
||||
class="text-sm cursor-pointer"
|
||||
class="cursor-pointer text-sm"
|
||||
>{{ opt.label }}</label
|
||||
>
|
||||
</div>
|
||||
@@ -46,7 +46,7 @@
|
||||
<Button
|
||||
label="Next"
|
||||
:disabled="!validStep1"
|
||||
class="w-full h-10 border-none text-white"
|
||||
class="h-10 w-full border-none text-white"
|
||||
@click="goTo(2, activateCallback)"
|
||||
/>
|
||||
</div>
|
||||
@@ -55,10 +55,10 @@
|
||||
<StepPanel
|
||||
v-slot="{ activateCallback }"
|
||||
value="2"
|
||||
class="flex-1 min-h-full flex flex-col justify-between bg-transparent"
|
||||
class="flex min-h-full flex-1 flex-col justify-between bg-transparent"
|
||||
>
|
||||
<div>
|
||||
<label class="text-lg font-medium block mb-8">{{
|
||||
<label class="mb-8 block text-lg font-medium">{{
|
||||
t('cloudSurvey_steps_purpose')
|
||||
}}</label>
|
||||
<div class="flex flex-col gap-6">
|
||||
@@ -75,7 +75,7 @@
|
||||
/>
|
||||
<label
|
||||
:for="`purpose-${opt.value}`"
|
||||
class="text-sm cursor-pointer"
|
||||
class="cursor-pointer text-sm"
|
||||
>{{ opt.label }}</label
|
||||
>
|
||||
</div>
|
||||
@@ -93,13 +93,13 @@
|
||||
<Button
|
||||
label="Back"
|
||||
severity="secondary"
|
||||
class="text-white flex-1"
|
||||
class="flex-1 text-white"
|
||||
@click="goTo(1, activateCallback)"
|
||||
/>
|
||||
<Button
|
||||
label="Next"
|
||||
:disabled="!validStep2"
|
||||
class="flex-1 h-10 text-white"
|
||||
class="h-10 flex-1 text-white"
|
||||
@click="goTo(3, activateCallback)"
|
||||
/>
|
||||
</div>
|
||||
@@ -108,10 +108,10 @@
|
||||
<StepPanel
|
||||
v-slot="{ activateCallback }"
|
||||
value="3"
|
||||
class="flex-1 min-h-full flex flex-col justify-between bg-transparent"
|
||||
class="flex min-h-full flex-1 flex-col justify-between bg-transparent"
|
||||
>
|
||||
<div>
|
||||
<label class="text-lg font-medium block mb-8">{{
|
||||
<label class="mb-8 block text-lg font-medium">{{
|
||||
t('cloudSurvey_steps_industry')
|
||||
}}</label>
|
||||
<div class="flex flex-col gap-6">
|
||||
@@ -128,7 +128,7 @@
|
||||
/>
|
||||
<label
|
||||
:for="`industry-${opt.value}`"
|
||||
class="text-sm cursor-pointer"
|
||||
class="cursor-pointer text-sm"
|
||||
>{{ opt.label }}</label
|
||||
>
|
||||
</div>
|
||||
@@ -146,13 +146,13 @@
|
||||
<Button
|
||||
label="Back"
|
||||
severity="secondary"
|
||||
class="text-white flex-1"
|
||||
class="flex-1 text-white"
|
||||
@click="goTo(2, activateCallback)"
|
||||
/>
|
||||
<Button
|
||||
label="Next"
|
||||
:disabled="!validStep3"
|
||||
class="flex-1 h-10 border-none text-white"
|
||||
class="h-10 flex-1 border-none text-white"
|
||||
@click="goTo(4, activateCallback)"
|
||||
/>
|
||||
</div>
|
||||
@@ -161,10 +161,10 @@
|
||||
<StepPanel
|
||||
v-slot="{ activateCallback }"
|
||||
value="4"
|
||||
class="flex-1 min-h-full flex flex-col justify-between bg-transparent"
|
||||
class="flex min-h-full flex-1 flex-col justify-between bg-transparent"
|
||||
>
|
||||
<div>
|
||||
<label class="text-lg font-medium block mb-8">{{
|
||||
<label class="mb-8 block text-lg font-medium">{{
|
||||
t('cloudSurvey_steps_making')
|
||||
}}</label>
|
||||
<div class="flex flex-col gap-6">
|
||||
@@ -180,7 +180,7 @@
|
||||
/>
|
||||
<label
|
||||
:for="`making-${opt.value}`"
|
||||
class="text-sm cursor-pointer"
|
||||
class="cursor-pointer text-sm"
|
||||
>{{ opt.label }}</label
|
||||
>
|
||||
</div>
|
||||
@@ -191,14 +191,14 @@
|
||||
<Button
|
||||
label="Back"
|
||||
severity="secondary"
|
||||
class="text-white flex-1"
|
||||
class="flex-1 text-white"
|
||||
@click="goTo(3, activateCallback)"
|
||||
/>
|
||||
<Button
|
||||
label="Submit"
|
||||
:disabled="!validStep4 || isSubmitting"
|
||||
:loading="isSubmitting"
|
||||
class="flex-1 h-10 border-none text-white"
|
||||
class="h-10 flex-1 border-none text-white"
|
||||
@click="onSubmitSurvey"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="px-6 py-8 max-w-[640px] mx-auto">
|
||||
<div class="mx-auto max-w-[640px] px-6 py-8">
|
||||
<!-- Back button -->
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center size-10 rounded-lg bg-transparent border border-white text-foreground/80"
|
||||
class="text-foreground/80 flex size-10 items-center justify-center rounded-lg border border-white bg-transparent"
|
||||
aria-label="{{ t('cloudVerifyEmail_back') }}"
|
||||
@click="goBack"
|
||||
>
|
||||
@@ -16,18 +16,18 @@
|
||||
</h1>
|
||||
|
||||
<!-- Body copy -->
|
||||
<p class="mt-6 text-base text-foreground/80">
|
||||
<p class="text-foreground/80 mt-6 text-base">
|
||||
{{ t('cloudVerifyEmail_sent') }}
|
||||
</p>
|
||||
<p class="mt-3 text-base font-medium">{{ authStore.userEmail }}</p>
|
||||
|
||||
<p class="mt-6 text-base text-foreground/80">
|
||||
<p class="text-foreground/80 mt-6 text-base">
|
||||
{{ t('cloudVerifyEmail_clickToContinue') }}
|
||||
</p>
|
||||
|
||||
<p class="mt-10 text-base text-foreground/80">
|
||||
<p class="text-foreground/80 mt-10 text-base">
|
||||
{{ t('cloudVerifyEmail_didntReceive') }}
|
||||
<span class="text-blue-400 no-underline cursor-pointer" @click="onSend">
|
||||
<span class="cursor-pointer text-blue-400 no-underline" @click="onSend">
|
||||
{{ t('cloudVerifyEmail_resend') }}</span
|
||||
>
|
||||
</p>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center p-8">
|
||||
<div class="w-full max-w-md text-center">
|
||||
<h1 class="font-abcrom font-black italic uppercase hero-title">
|
||||
<h1 class="font-abcrom hero-title font-black uppercase italic">
|
||||
{{ t('cloudWaitlist_titleLine1') }}<br />
|
||||
{{ t('cloudWaitlist_titleLine2') }}
|
||||
</h1>
|
||||
<div class="max-w-[320px] text-lg font-light m-auto">
|
||||
<div class="m-auto max-w-[320px] text-lg font-light">
|
||||
<p class="text-white">
|
||||
{{ t('cloudWaitlist_message') }}
|
||||
</p>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<CloudLoginViewSkeleton v-if="skeletonType === 'login'" />
|
||||
<CloudSurveyViewSkeleton v-else-if="skeletonType === 'survey'" />
|
||||
<CloudWaitlistViewSkeleton v-else-if="skeletonType === 'waitlist'" />
|
||||
<div v-else-if="error" class="h-full flex items-center justify-center p-8">
|
||||
<div class="lg:w-96 max-w-[100vw] p-2 text-center">
|
||||
<p class="text-red-500 mb-4">{{ errorMessage }}</p>
|
||||
<div v-else-if="error" class="flex h-full items-center justify-center p-8">
|
||||
<div class="max-w-[100vw] p-2 text-center lg:w-96">
|
||||
<p class="mb-4 text-red-500">{{ errorMessage }}</p>
|
||||
<Button
|
||||
:label="
|
||||
isRetrying
|
||||
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center">
|
||||
<ProgressSpinner class="w-8 h-8" />
|
||||
<ProgressSpinner class="h-8 w-8" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="w-5/6 h-[7%] max-h-[70px] mx-auto flex items-end">
|
||||
<div class="mx-auto flex h-[7%] max-h-[70px] w-5/6 items-end">
|
||||
<img
|
||||
src="/assets/images/comfy-cloud-logo.svg"
|
||||
alt="Comfy Cloud Logo"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
>
|
||||
<!-- Email Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="opacity-80 text-base font-medium mb-2" :for="emailInputId">
|
||||
<label class="mb-2 text-base font-medium opacity-80" :for="emailInputId">
|
||||
{{ t('auth.login.emailLabel') }}
|
||||
</label>
|
||||
<InputText
|
||||
@@ -26,9 +26,9 @@
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<label
|
||||
class="opacity-80 text-base font-medium"
|
||||
class="text-base font-medium opacity-80"
|
||||
for="cloud-sign-in-password"
|
||||
>
|
||||
{{ t('auth.login.passwordLabel') }}
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
<router-link
|
||||
:to="{ name: 'cloud-forgot-password' }"
|
||||
class="text-muted text-sm font-medium no-underline"
|
||||
class="text-sm font-medium text-muted no-underline"
|
||||
>
|
||||
{{ t('auth.login.forgotPassword') }}
|
||||
</router-link>
|
||||
@@ -63,12 +63,12 @@
|
||||
</Message>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<ProgressSpinner v-if="loading" class="w-8 h-8" />
|
||||
<ProgressSpinner v-if="loading" class="h-8 w-8" />
|
||||
<Button
|
||||
v-else
|
||||
type="submit"
|
||||
:label="t('auth.login.loginButton')"
|
||||
class="h-10 font-medium mt-4 text-white"
|
||||
class="mt-4 h-10 font-medium text-white"
|
||||
/>
|
||||
</Form>
|
||||
</template>
|
||||
@@ -85,7 +85,8 @@ import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { type SignInData, signInSchema } from '@/schemas/signInSchema'
|
||||
import { signInSchema } from '@/schemas/signInSchema'
|
||||
import type { SignInData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<CloudTemplateFooter />
|
||||
</template>
|
||||
</BaseViewTemplate>
|
||||
<div class="flex-1 relative bg-black overflow-hidden hidden lg:block">
|
||||
<div class="relative hidden flex-1 overflow-hidden bg-black lg:block">
|
||||
<!-- Video Background -->
|
||||
<video
|
||||
class="absolute inset-0 w-full h-full object-cover"
|
||||
class="absolute inset-0 h-full w-full object-cover"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
@@ -22,20 +22,20 @@
|
||||
<source :src="videoSrc" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
<div class="absolute inset-0 w-full h-full bg-black/30"></div>
|
||||
<div class="absolute inset-0 h-full w-full bg-black/30"></div>
|
||||
|
||||
<!-- Optional Overlay for better visual -->
|
||||
<div
|
||||
class="absolute inset-0 flex justify-center items-center text-white text-center"
|
||||
class="absolute inset-0 flex items-center justify-center text-center text-white"
|
||||
>
|
||||
<div>
|
||||
<h1 class="font-abcrom font-black italic uppercase hero-title">
|
||||
<h1 class="font-abcrom hero-title font-black uppercase italic">
|
||||
{{ t('cloudStart_title') }}
|
||||
</h1>
|
||||
<p class="m-2 text-xl text-center text-white">
|
||||
<p class="m-2 text-center text-xl text-white">
|
||||
{{ t('cloudStart_desc') }}
|
||||
</p>
|
||||
<p class="m-0 text-xl text-center text-white">
|
||||
<p class="m-0 text-center text-xl text-white">
|
||||
{{ t('cloudStart_explain') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -48,7 +48,7 @@
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10 bg-black text-white font-bold"
|
||||
class="h-10 bg-black font-bold text-white"
|
||||
severity="secondary"
|
||||
@click="handleDownloadClick"
|
||||
>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<template>
|
||||
<footer class="flex gap-2.5 w-5/6 h-[5%] max-h-[60px] mx-auto items-start">
|
||||
<footer class="mx-auto flex h-[5%] max-h-[60px] w-5/6 items-start gap-2.5">
|
||||
<a
|
||||
href="https://www.comfy.org/terms-of-service"
|
||||
target="_blank"
|
||||
class="text-sm text-gray-600 no-underline cursor-pointer"
|
||||
class="cursor-pointer text-sm text-gray-600 no-underline"
|
||||
>
|
||||
{{ t('auth.login.termsLink') }}
|
||||
</a>
|
||||
<a
|
||||
href="https://www.comfy.org/privacy-policy"
|
||||
target="_blank"
|
||||
class="text-sm text-gray-600 no-underline cursor-pointer"
|
||||
class="cursor-pointer text-sm text-gray-600 no-underline"
|
||||
>
|
||||
{{ t('auth.login.privacyLink') }}
|
||||
</a>
|
||||
<a
|
||||
href="https://support.comfy.org"
|
||||
class="text-sm text-gray-600 no-underline cursor-pointer"
|
||||
class="cursor-pointer text-sm text-gray-600 no-underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-center items-center h-screen font-mono text-black gap-4"
|
||||
class="flex h-screen flex-col items-center justify-center gap-4 font-mono text-black"
|
||||
>
|
||||
<Skeleton width="60%" height="2rem" />
|
||||
<Skeleton width="30%" height="2.5rem" />
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="h-full flex items-center justify-center p-8">
|
||||
<div class="lg:w-96 max-w-[100vw]">
|
||||
<div class="bg-[#2d2e32] p-4 rounded-lg">
|
||||
<div class="flex h-full items-center justify-center p-8">
|
||||
<div class="max-w-[100vw] lg:w-96">
|
||||
<div class="rounded-lg bg-[#2d2e32] p-4">
|
||||
<Skeleton width="60%" height="1.125rem" class="mb-2" />
|
||||
<Skeleton width="90%" height="1rem" class="mb-2" />
|
||||
<Skeleton width="80%" height="1rem" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 mt-6 mb-8">
|
||||
<div class="mt-6 mb-8 flex flex-col gap-4">
|
||||
<Skeleton width="45%" height="1.5rem" class="my-0" />
|
||||
<div class="flex items-center">
|
||||
<Skeleton width="25%" height="1rem" class="mr-1" />
|
||||
@@ -24,7 +24,7 @@
|
||||
<Skeleton width="100%" height="2.5rem" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center my-8">
|
||||
<div class="my-8 flex items-center">
|
||||
<div class="flex-1 border-t border-gray-300"></div>
|
||||
<Skeleton width="30%" height="1rem" class="mx-4" />
|
||||
<div class="flex-1 border-t border-gray-300"></div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col min-h-[638px] min-w-[320px]">
|
||||
<div class="flex min-h-[638px] min-w-[320px] flex-col">
|
||||
<Skeleton width="100%" height="0.5rem" class="mb-8" />
|
||||
|
||||
<div class="p-0 flex-1 flex flex-col">
|
||||
<div class="flex-1 min-h-full flex flex-col justify-between">
|
||||
<div class="flex flex-1 flex-col p-0">
|
||||
<div class="flex min-h-full flex-1 flex-col justify-between">
|
||||
<div>
|
||||
<Skeleton width="70%" height="1.75rem" class="mb-8" />
|
||||
<div class="flex flex-col gap-6">
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
<div class="flex flex-col items-center justify-center p-8">
|
||||
<div class="w-full max-w-md text-center">
|
||||
<div class="mb-8">
|
||||
<Skeleton width="80%" height="3rem" class="mb-2 mx-auto" />
|
||||
<Skeleton width="80%" height="3rem" class="mx-auto mb-2" />
|
||||
<Skeleton width="60%" height="3rem" class="mx-auto" />
|
||||
</div>
|
||||
<div class="max-w-[320px] mx-auto">
|
||||
<div class="mx-auto max-w-[320px]">
|
||||
<div class="mb-4">
|
||||
<Skeleton width="90%" height="1.5rem" class="mb-2 mx-auto" />
|
||||
<Skeleton width="90%" height="1.5rem" class="mx-auto mb-2" />
|
||||
<Skeleton width="70%" height="1.5rem" class="mx-auto" />
|
||||
</div>
|
||||
<div>
|
||||
<Skeleton width="80%" height="1.5rem" class="mb-2 mx-auto" />
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
<Skeleton width="80%" height="1.5rem" class="mx-auto mb-2" />
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<Skeleton width="20%" height="1.5rem" />
|
||||
<Skeleton width="15%" height="1.5rem" />
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</Message>
|
||||
<Message v-if="commandLineArgs" severity="secondary" pt:text="w-full">
|
||||
<template #icon>
|
||||
<i-lucide:terminal class="text-xl font-bold" />
|
||||
<i class="icon-[lucide--terminal] text-xl font-bold" />
|
||||
</template>
|
||||
<div class="flex items-center justify-between">
|
||||
<p>{{ commandLineArgs }}</p>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
<ScrollPanel class="settings-sidebar shrink-0 p-2 w-48 2xl:w-64">
|
||||
<ScrollPanel class="settings-sidebar w-48 shrink-0 p-2 2xl:w-64">
|
||||
<SearchBox
|
||||
v-model:model-value="searchQuery"
|
||||
class="settings-search-box w-full mb-2"
|
||||
class="settings-search-box mb-2 w-full"
|
||||
:placeholder="$t('g.searchSettings') + '...'"
|
||||
:debounce-time="128"
|
||||
autofocus
|
||||
@@ -20,14 +20,14 @@
|
||||
(option: SettingTreeNode) =>
|
||||
!queryIsEmpty && !searchResultsCategories.has(option.label ?? '')
|
||||
"
|
||||
class="border-none w-full"
|
||||
class="w-full border-none"
|
||||
>
|
||||
<template #optiongroup>
|
||||
<Divider class="my-0" />
|
||||
</template>
|
||||
</Listbox>
|
||||
</ScrollPanel>
|
||||
<Divider layout="vertical" class="mx-1 2xl:mx-4 hidden md:flex" />
|
||||
<Divider layout="vertical" class="mx-1 hidden md:flex 2xl:mx-4" />
|
||||
<Divider layout="horizontal" class="flex md:hidden" />
|
||||
<Tabs :value="tabValue" :lazy="true" class="settings-content h-full w-full">
|
||||
<TabPanels class="settings-tab-panels h-full w-full pr-0">
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import {
|
||||
type Component,
|
||||
computed,
|
||||
defineAsyncComponent,
|
||||
onMounted,
|
||||
ref
|
||||
} from 'vue'
|
||||
import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
|
||||
@@ -8,7 +8,8 @@ import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import { stringToLocale } from '@/utils/formatUtil'
|
||||
|
||||
import { type ReleaseNote, useReleaseService } from './releaseService'
|
||||
import { useReleaseService } from './releaseService'
|
||||
import type { ReleaseNote } from './releaseService'
|
||||
|
||||
// Store for managing release notes
|
||||
export const useReleaseStore = defineStore('release', () => {
|
||||
@@ -72,14 +73,10 @@ export const useReleaseStore = defineStore('release', () => {
|
||||
) === 0
|
||||
)
|
||||
|
||||
const hasMediumOrHighAttention = computed(() =>
|
||||
recentReleases.value
|
||||
.slice(0, -1)
|
||||
.some(
|
||||
(release) =>
|
||||
release.attention === 'medium' || release.attention === 'high'
|
||||
)
|
||||
)
|
||||
const hasMediumOrHighAttention = computed(() => {
|
||||
const attention = recentRelease.value?.attention
|
||||
return attention === 'medium' || attention === 'high'
|
||||
})
|
||||
|
||||
// Show toast if needed
|
||||
const shouldShowToast = computed(() => {
|
||||
|
||||
@@ -172,7 +172,7 @@ onMounted(async () => {
|
||||
width: 448px;
|
||||
padding: 16px 16px 8px;
|
||||
background: #353535;
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: 0 4px 4px rgb(0 0 0 / 0.25);
|
||||
border-radius: 12px;
|
||||
outline: 1px solid #4e4e4e;
|
||||
outline-offset: -1px;
|
||||
@@ -193,7 +193,7 @@ onMounted(async () => {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
padding: 10px;
|
||||
background: rgba(0, 122, 255, 0.2);
|
||||
background: rgb(0 122 255 / 0.2);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -218,7 +218,7 @@ defineExpose({
|
||||
width: 400px;
|
||||
outline: 1px solid #4e4e4e;
|
||||
outline-offset: -1px;
|
||||
box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 8px 32px rgb(0 0 0 / 0.3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -293,12 +293,6 @@ defineExpose({
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
/* Content Section */
|
||||
.popup-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content-text {
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { type Raw, computed, markRaw, ref, shallowRef, watch } from 'vue'
|
||||
import { computed, markRaw, ref, shallowRef, watch } from 'vue'
|
||||
import type { Raw } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import type {
|
||||
@@ -8,8 +9,10 @@ import type {
|
||||
LGraphNode,
|
||||
Subgraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type {
|
||||
ComfyWorkflowJSON,
|
||||
NodeId
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { useWorkflowThumbnail } from '@/renderer/core/thumbnail/useWorkflowThumbnail'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type SafeParseReturnType, z } from 'zod'
|
||||
import { z } from 'zod'
|
||||
import type { SafeParseReturnType } from 'zod'
|
||||
import { fromZodError } from 'zod-validation-error'
|
||||
|
||||
// GroupNode is hacking node id to be a string, so we need to allow that.
|
||||
|
||||
Reference in New Issue
Block a user