Compare commits
11 Commits
refactor/c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fefbe7843c | ||
|
|
6445690ed3 | ||
|
|
603914e78f | ||
|
|
c7797b201e | ||
|
|
aa68573a6e | ||
|
|
79acf7be5e | ||
|
|
02adfd4b83 | ||
|
|
7c2c78b537 | ||
|
|
bd1fd0680e | ||
|
|
9617e498c9 | ||
|
|
25205c0f55 |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 56 KiB |
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"name": "Comfy",
|
||||
"short_name": "Comfy",
|
||||
"id": "/",
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#211927",
|
||||
"background_color": "#211927",
|
||||
"display": "standalone"
|
||||
"display": "standalone",
|
||||
"id": "/",
|
||||
"start_url": "/"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 8.8 KiB |
@@ -1,10 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { EventItem } from '../../content.config'
|
||||
import type { Locale, TranslationKey } from '../../i18n/translations'
|
||||
import type {
|
||||
Locale,
|
||||
LocalizedText,
|
||||
TranslationKey
|
||||
} from '../../i18n/translations'
|
||||
|
||||
import { t } from '../../i18n/translations'
|
||||
import BrandButton from './BrandButton.vue'
|
||||
|
||||
export type EventItem = {
|
||||
label: LocalizedText
|
||||
title: LocalizedText
|
||||
cta: LocalizedText
|
||||
href: string
|
||||
}
|
||||
|
||||
const {
|
||||
locale = 'en',
|
||||
headingKey,
|
||||
@@ -30,12 +40,12 @@ const {
|
||||
<div class="grid grid-cols-1 gap-12 lg:grid-cols-2 lg:gap-16">
|
||||
<div class="flex flex-col gap-8">
|
||||
<h2
|
||||
class="text-4xl font-light tracking-tight text-primary-comfy-canvas lg:text-6xl"
|
||||
class="text-primary-comfy-canvas text-4xl font-light tracking-tight lg:text-6xl"
|
||||
>
|
||||
{{ t(headingKey, locale) }}
|
||||
</h2>
|
||||
<p
|
||||
class="max-w-sm text-sm/relaxed text-primary-comfy-canvas lg:text-base"
|
||||
class="text-primary-comfy-canvas max-w-sm text-sm/relaxed lg:text-base"
|
||||
>
|
||||
{{ t(descriptionKey, locale) }}
|
||||
</p>
|
||||
@@ -56,20 +66,20 @@ const {
|
||||
v-for="(event, i) in events"
|
||||
:key="i"
|
||||
:href="event.href"
|
||||
class="group flex items-center gap-4 border-b border-primary-comfy-canvas/15 py-6 lg:gap-8"
|
||||
class="group border-primary-comfy-canvas/15 flex items-center gap-4 border-b py-6 lg:gap-8"
|
||||
>
|
||||
<span
|
||||
class="shrink-0 text-sm font-medium text-primary-comfy-canvas"
|
||||
class="text-primary-comfy-canvas shrink-0 text-sm font-medium"
|
||||
>
|
||||
{{ event.label }}
|
||||
{{ event.label[locale] }}
|
||||
</span>
|
||||
<span class="text-primary-warm-gray flex-1 text-sm">
|
||||
{{ event.title }}
|
||||
{{ event.title[locale] }}
|
||||
</span>
|
||||
<span
|
||||
class="text-primary-comfy-yellow flex shrink-0 items-center gap-2 text-sm"
|
||||
>
|
||||
{{ event.cta }}
|
||||
{{ event.cta[locale] }}
|
||||
<svg
|
||||
class="size-4 transition-transform group-hover:translate-x-0.5"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -21,7 +21,7 @@ export const Default: Story = {
|
||||
args: {
|
||||
title: 'Product',
|
||||
links: [
|
||||
{ label: 'Local', href: '/local' },
|
||||
{ label: 'Desktop', href: '/download' },
|
||||
{ label: 'Cloud', href: '/cloud' },
|
||||
{ label: 'API', href: '/api' },
|
||||
{ label: 'Enterprise', href: '/enterprise' }
|
||||
|
||||
@@ -12,9 +12,9 @@ const meta: Meta<typeof ProductCard> = {
|
||||
})
|
||||
],
|
||||
args: {
|
||||
title: 'Comfy\nLocal',
|
||||
title: 'Comfy\nDesktop',
|
||||
description: 'Run ComfyUI on your own hardware.',
|
||||
cta: 'SEE LOCAL FEATURES',
|
||||
cta: 'SEE DESKTOP FEATURES',
|
||||
href: '#',
|
||||
bg: 'bg-primary-warm-gray'
|
||||
}
|
||||
@@ -31,9 +31,9 @@ export const AllCards: Story = {
|
||||
template: `
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<ProductCard
|
||||
title="Comfy\nLocal"
|
||||
title="Comfy\nDesktop"
|
||||
description="Run ComfyUI on your own hardware."
|
||||
cta="SEE LOCAL FEATURES"
|
||||
cta="SEE DESKTOP FEATURES"
|
||||
href="#"
|
||||
bg="bg-primary-warm-gray"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { GalleryItem } from '../../content.config'
|
||||
import type { GalleryItem } from '../../data/gallery'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import GalleryItemAttribution from './GalleryItemAttribution.vue'
|
||||
|
||||
@@ -53,7 +53,7 @@ defineEmits<{ click: [] }>()
|
||||
<div class="flex w-full items-end justify-between p-4">
|
||||
<div class="gap-2">
|
||||
<p class="text-sm font-bold text-white">{{ item.title }}</p>
|
||||
<p class="text-xs text-primary-comfy-canvas">
|
||||
<p class="text-primary-comfy-canvas text-xs">
|
||||
<GalleryItemAttribution :item :locale />
|
||||
</p>
|
||||
</div>
|
||||
@@ -82,7 +82,7 @@ defineEmits<{ click: [] }>()
|
||||
<!-- Mobile metadata -->
|
||||
<div v-if="mobile" class="mt-2 gap-2">
|
||||
<p class="text-sm font-bold text-white">{{ item.title }}</p>
|
||||
<p class="text-xs text-primary-comfy-canvas">
|
||||
<p class="text-primary-comfy-canvas text-xs">
|
||||
<GalleryItemAttribution :item :locale />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
import { lockScroll, unlockScroll } from '../../composables/scrollLock'
|
||||
import { prefersReducedMotion } from '../../composables/useReducedMotion'
|
||||
import type { GalleryItem } from '../../content.config'
|
||||
import type { GalleryItem } from '../../data/gallery'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import { t } from '../../i18n/translations'
|
||||
import BrandButton from '../common/BrandButton.vue'
|
||||
@@ -123,11 +123,11 @@ onUnmounted(() => {
|
||||
<!-- Close button -->
|
||||
<button
|
||||
:aria-label="t('gallery.detail.close', locale)"
|
||||
class="border-primary-comfy-yellow hover:bg-primary-comfy-yellow group absolute right-10 z-10 flex size-10 cursor-pointer items-center justify-center rounded-2xl border-2 bg-primary-comfy-ink transition-colors lg:top-8 lg:right-26"
|
||||
class="border-primary-comfy-yellow bg-primary-comfy-ink hover:bg-primary-comfy-yellow group absolute right-10 z-10 flex size-10 cursor-pointer items-center justify-center rounded-2xl border-2 transition-colors lg:top-8 lg:right-26"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<span
|
||||
class="bg-primary-comfy-yellow size-5 transition-colors group-hover:bg-primary-comfy-ink"
|
||||
class="bg-primary-comfy-yellow group-hover:bg-primary-comfy-ink size-5 transition-colors"
|
||||
style="mask: url('/icons/close.svg') center / contain no-repeat"
|
||||
/>
|
||||
</button>
|
||||
@@ -136,7 +136,7 @@ onUnmounted(() => {
|
||||
<div class="relative hidden min-h-0 w-full flex-1 pt-12 lg:flex">
|
||||
<!-- Left: info card -->
|
||||
<div
|
||||
class="bg-primary-comfy-yellow rounded-5xl relative z-10 flex w-80 shrink-0 flex-col justify-between self-start p-8 text-primary-comfy-ink"
|
||||
class="bg-primary-comfy-yellow text-primary-comfy-ink rounded-5xl relative z-10 flex w-80 shrink-0 flex-col justify-between self-start p-8"
|
||||
>
|
||||
<div
|
||||
:class="transitioning ? 'opacity-0' : 'opacity-100'"
|
||||
@@ -170,7 +170,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- Right: large image -->
|
||||
<div
|
||||
class="border-primary-comfy-yellow rounded-5xl flex max-h-full min-h-0 flex-1 items-center justify-center overflow-hidden border-2 bg-primary-comfy-ink p-4"
|
||||
class="border-primary-comfy-yellow bg-primary-comfy-ink rounded-5xl flex max-h-full min-h-0 flex-1 items-center justify-center overflow-hidden border-2 p-4"
|
||||
>
|
||||
<component
|
||||
:is="activeItem.video ? 'video' : 'img'"
|
||||
@@ -197,7 +197,7 @@ onUnmounted(() => {
|
||||
>
|
||||
<!-- Image -->
|
||||
<div
|
||||
class="border-primary-comfy-yellow flex w-full flex-1 items-center overflow-hidden rounded-4xl border-2 bg-primary-comfy-ink p-3"
|
||||
class="border-primary-comfy-yellow bg-primary-comfy-ink flex w-full flex-1 items-center overflow-hidden rounded-4xl border-2 p-3"
|
||||
>
|
||||
<component
|
||||
:is="activeItem.video ? 'video' : 'img'"
|
||||
@@ -223,7 +223,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- Info card -->
|
||||
<div
|
||||
class="bg-primary-comfy-yellow w-full rounded-4xl p-6 text-primary-comfy-ink"
|
||||
class="bg-primary-comfy-yellow text-primary-comfy-ink w-full rounded-4xl p-6"
|
||||
>
|
||||
<div
|
||||
:class="transitioning ? 'opacity-0' : 'opacity-100'"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { GalleryItem } from '../../content.config'
|
||||
import type { GalleryItem } from '../../data/gallery'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { GalleryItem } from '../../content.config'
|
||||
import { visibleGalleryItems as items } from '../../data/gallery'
|
||||
import type { GalleryItem } from '../../data/gallery'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
import GalleryCard from './GalleryCard.vue'
|
||||
import GalleryDetailModal from './GalleryDetailModal.vue'
|
||||
|
||||
const { items, locale = 'en' } = defineProps<{
|
||||
items: GalleryItem[]
|
||||
locale?: Locale
|
||||
}>()
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const modalOpen = ref(false)
|
||||
const modalIndex = ref(0)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, useTemplateRef } from 'vue'
|
||||
|
||||
import type { ResolvedTutorial } from '../../content.config'
|
||||
import type { LearningTutorial } from '../../data/learningTutorials'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import { lockScroll, unlockScroll } from '../../composables/scrollLock'
|
||||
@@ -9,7 +9,7 @@ import { t } from '../../i18n/translations'
|
||||
import VideoPlayer from '../common/VideoPlayer.vue'
|
||||
|
||||
const { tutorial, locale = 'en' } = defineProps<{
|
||||
tutorial: ResolvedTutorial
|
||||
tutorial: LearningTutorial
|
||||
locale?: Locale
|
||||
}>()
|
||||
|
||||
@@ -39,7 +39,7 @@ onUnmounted(() => {
|
||||
<Teleport to="body">
|
||||
<dialog
|
||||
ref="dialogRef"
|
||||
:aria-label="tutorial.title"
|
||||
:aria-label="tutorial.title[locale]"
|
||||
class="fixed inset-0 z-50 flex size-full max-h-none max-w-none flex-col items-center justify-center border-0 bg-transparent px-4 py-8 backdrop-blur-xl backdrop:bg-transparent lg:px-20 lg:py-8"
|
||||
@click="handleBackdropClick"
|
||||
@keydown="handleKeydown"
|
||||
@@ -60,7 +60,7 @@ onUnmounted(() => {
|
||||
class="border-primary-comfy-yellow rounded-5xl flex w-full max-w-7xl items-center justify-center overflow-hidden border-2 bg-primary-comfy-ink p-3 lg:p-4"
|
||||
>
|
||||
<VideoPlayer
|
||||
:key="tutorial.slug"
|
||||
:key="tutorial.id"
|
||||
:locale
|
||||
:src="tutorial.videoSrc"
|
||||
:poster="tutorial.poster"
|
||||
@@ -73,7 +73,7 @@ onUnmounted(() => {
|
||||
class="mt-6 text-center text-lg font-medium text-primary-comfy-canvas lg:text-xl"
|
||||
>
|
||||
{{ t('learning.tutorials.titlePrefix', locale) }}
|
||||
{{ tutorial.title }}
|
||||
{{ tutorial.title[locale] }}
|
||||
</h2>
|
||||
</dialog>
|
||||
</Teleport>
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { ResolvedTutorial } from '../../content.config'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import {
|
||||
getTutorialPosterSrc,
|
||||
learningTutorials
|
||||
} from '../../data/learningTutorials'
|
||||
import { t } from '../../i18n/translations'
|
||||
import { getTutorialPosterSrc } from '../../utils/tutorial'
|
||||
import Badge from '../common/Badge.vue'
|
||||
import MaskRevealButton from '../common/MaskRevealButton.vue'
|
||||
import TutorialDetailDialog from './TutorialDetailDialog.vue'
|
||||
|
||||
const { tutorials, locale = 'en' } = defineProps<{
|
||||
tutorials: readonly ResolvedTutorial[]
|
||||
locale?: Locale
|
||||
}>()
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const activeTutorialSlug = ref<string | null>(null)
|
||||
const activeTutorialId = ref<string | null>(null)
|
||||
const activeTutorial = () =>
|
||||
tutorials.find((tutorial) => tutorial.slug === activeTutorialSlug.value)
|
||||
learningTutorials.find((tutorial) => tutorial.id === activeTutorialId.value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -32,15 +31,15 @@ const activeTutorial = () =>
|
||||
class="grid grid-cols-1 gap-x-6 gap-y-10 md:grid-cols-2 lg:grid-cols-3 lg:gap-x-8"
|
||||
>
|
||||
<li
|
||||
v-for="tutorial in tutorials"
|
||||
:key="tutorial.slug"
|
||||
v-for="tutorial in learningTutorials"
|
||||
:key="tutorial.id"
|
||||
class="bg-transparency-white-t4 flex flex-col gap-4 overflow-hidden rounded-3xl border-0 p-2"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="group relative block aspect-video cursor-pointer overflow-hidden rounded-3xl"
|
||||
:aria-label="`${t('learning.tutorials.titlePrefix', locale)} ${tutorial.title}`"
|
||||
@click="activeTutorialSlug = tutorial.slug"
|
||||
:aria-label="`${t('learning.tutorials.titlePrefix', locale)} ${tutorial.title[locale]}`"
|
||||
@click="activeTutorialId = tutorial.id"
|
||||
>
|
||||
<video
|
||||
:src="getTutorialPosterSrc(tutorial)"
|
||||
@@ -75,7 +74,7 @@ const activeTutorial = () =>
|
||||
class="text-sm/snug text-primary-comfy-canvas lg:text-base/snug"
|
||||
>
|
||||
{{ t('learning.tutorials.titlePrefix', locale) }}<br />
|
||||
{{ tutorial.title }}
|
||||
{{ tutorial.title[locale] }}
|
||||
</h3>
|
||||
<MaskRevealButton
|
||||
v-if="tutorial.href"
|
||||
@@ -104,7 +103,7 @@ const activeTutorial = () =>
|
||||
|
||||
<ul class="flex flex-wrap gap-2">
|
||||
<li v-for="tag in tutorial.tags" :key="tag">
|
||||
<Badge>{{ tag }}</Badge>
|
||||
<Badge>{{ t(tag, locale) }}</Badge>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -115,7 +114,7 @@ const activeTutorial = () =>
|
||||
v-if="activeTutorial()"
|
||||
:tutorial="activeTutorial()!"
|
||||
:locale="locale"
|
||||
@close="activeTutorialSlug = null"
|
||||
@close="activeTutorialId = null"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { GalleryItem } from '../../content.config'
|
||||
import type { GalleryItem } from '../../data/gallery'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import { t } from '../../i18n/translations'
|
||||
@@ -9,14 +9,64 @@ import BrandButton from '../common/BrandButton.vue'
|
||||
import GalleryCard from '../gallery/GalleryCard.vue'
|
||||
import GalleryDetailModal from '../gallery/GalleryDetailModal.vue'
|
||||
|
||||
const { items, locale = 'en' } = defineProps<{
|
||||
items: readonly GalleryItem[]
|
||||
locale?: Locale
|
||||
}>()
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const modelName = 'Grok'
|
||||
const ctaHref = 'https://comfy.org/workflows/model/grok'
|
||||
|
||||
const items: GalleryItem[] = [
|
||||
{
|
||||
id: 'subway-swan',
|
||||
image: 'https://media.comfy.org/website/gallery/subway-swan_compressed.png',
|
||||
title: 'Subway Swan',
|
||||
userAlias: 'Purz Beats',
|
||||
teamAlias: 'Comfy',
|
||||
tool: 'Grok Imagine',
|
||||
href: 'https://www.youtube.com/@PurzBeats'
|
||||
},
|
||||
{
|
||||
id: 'milos-little-wonder',
|
||||
video:
|
||||
'https://media.comfy.org/website/gallery/milos-little-wonder_compressed.mp4',
|
||||
title: 'Milos Little Wonder',
|
||||
userAlias: 'Purz Beats',
|
||||
teamAlias: 'Comfy',
|
||||
tool: 'Grok Imagine',
|
||||
href: 'https://www.youtube.com/@PurzBeats'
|
||||
},
|
||||
{
|
||||
id: 'amber-passage',
|
||||
image:
|
||||
'https://media.comfy.org/website/gallery/amber-passage_compressed.jpg',
|
||||
title: 'Amber Passage',
|
||||
userAlias: 'Purz Beats',
|
||||
teamAlias: 'Comfy',
|
||||
tool: 'Grok Imagine',
|
||||
href: 'https://www.youtube.com/@PurzBeats',
|
||||
objectPosition: 'bottom'
|
||||
},
|
||||
{
|
||||
id: 'neon-revenant',
|
||||
video:
|
||||
'https://media.comfy.org/website/gallery/neon-revenant_compressed.mp4',
|
||||
title: 'Neon Revenant',
|
||||
userAlias: 'Eric Solorio',
|
||||
teamAlias: 'Comfy',
|
||||
tool: 'Grok Imagine',
|
||||
href: 'https://www.instagram.com/enigmatic_e'
|
||||
},
|
||||
{
|
||||
id: 'midnight-umami',
|
||||
image:
|
||||
'https://media.comfy.org/website/gallery/midnight_umami_compressed.png',
|
||||
title: 'Midnight Umami',
|
||||
userAlias: 'Purz Beats',
|
||||
teamAlias: 'Comfy',
|
||||
tool: 'Grok Imagine',
|
||||
href: 'https://www.youtube.com/@PurzBeats'
|
||||
}
|
||||
]
|
||||
|
||||
const modalOpen = ref(false)
|
||||
const modalIndex = ref(0)
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ import type { Locale } from '../../../i18n/translations'
|
||||
import { computed } from 'vue'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import type { Platform } from '../../../composables/useDownloadUrl'
|
||||
import {
|
||||
downloadUrls,
|
||||
useDownloadUrl
|
||||
} from '../../../composables/useDownloadUrl'
|
||||
import { t } from '../../../i18n/translations'
|
||||
import { captureDownloadClick } from '../../../scripts/posthog'
|
||||
import BrandButton from '../../common/BrandButton.vue'
|
||||
|
||||
const { locale = 'en', class: customClass = '' } = defineProps<{
|
||||
@@ -17,13 +19,15 @@ const { locale = 'en', class: customClass = '' } = defineProps<{
|
||||
|
||||
const { downloadUrl, platform, showFallback } = useDownloadUrl()
|
||||
|
||||
const ICONS = {
|
||||
const label = computed(() => t('download.hero.downloadLocal', locale))
|
||||
|
||||
const ICONS: Record<Platform, string> = {
|
||||
windows: '/icons/os/windows.svg',
|
||||
mac: '/icons/os/apple.svg'
|
||||
} as const
|
||||
}
|
||||
|
||||
interface ButtonSpec {
|
||||
key: string
|
||||
key: Platform
|
||||
href: string
|
||||
icon: string
|
||||
ariaLabel?: string
|
||||
@@ -40,19 +44,18 @@ const buttons = computed<ButtonSpec[]>(() => {
|
||||
]
|
||||
}
|
||||
if (showFallback.value) {
|
||||
const label = t('download.hero.downloadLocal', locale)
|
||||
return [
|
||||
{
|
||||
key: 'windows',
|
||||
href: downloadUrls.windows,
|
||||
icon: ICONS.windows,
|
||||
ariaLabel: `${label} — Windows`
|
||||
ariaLabel: `${label.value} — Windows`
|
||||
},
|
||||
{
|
||||
key: 'mac',
|
||||
href: downloadUrls.macArm,
|
||||
icon: ICONS.mac,
|
||||
ariaLabel: `${label} — macOS`
|
||||
ariaLabel: `${label.value} — macOS`
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -69,17 +72,15 @@ const buttons = computed<ButtonSpec[]>(() => {
|
||||
size="lg"
|
||||
:class="customClass"
|
||||
:aria-label="btn.ariaLabel"
|
||||
@click="captureDownloadClick(btn.key)"
|
||||
>
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<img
|
||||
:src="btn.icon"
|
||||
alt=""
|
||||
class="ppformula-text-center size-5 -translate-y-0.75"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="ppformula-text-center">{{
|
||||
t('download.hero.downloadLocal', locale)
|
||||
}}</span>
|
||||
<span class="ppformula-text-center">{{ label }}</span>
|
||||
</span>
|
||||
</BrandButton>
|
||||
</template>
|
||||
|
||||
@@ -7,13 +7,13 @@ export const downloadUrls = {
|
||||
macArm: 'https://download.comfy.org/mac/dmg/arm64'
|
||||
} as const
|
||||
|
||||
type DetectedPlatform = 'windows' | 'mac' | null
|
||||
export type Platform = 'windows' | 'mac'
|
||||
|
||||
function isMobile(ua: string): boolean {
|
||||
return /iphone|ipad|ipod|android/.test(ua)
|
||||
}
|
||||
|
||||
function detectPlatform(ua: string): DetectedPlatform {
|
||||
function detectPlatform(ua: string): Platform | null {
|
||||
if (isMobile(ua)) return null
|
||||
if (ua.includes('win')) return 'windows'
|
||||
if (ua.includes('macintosh') || ua.includes('mac os x')) return 'mac'
|
||||
@@ -23,7 +23,7 @@ function detectPlatform(ua: string): DetectedPlatform {
|
||||
// TODO: Only Windows x64 and macOS arm64 are available today.
|
||||
// When Linux and/or macIntel builds are added, extend detection and URLs here.
|
||||
export function useDownloadUrl() {
|
||||
const platform = ref<DetectedPlatform>(null)
|
||||
const platform = ref<Platform | null>(null)
|
||||
const detected = ref(false)
|
||||
const isMobileUa = ref(false)
|
||||
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('astro:content', () => ({
|
||||
defineCollection: (config: unknown) => config
|
||||
}))
|
||||
vi.mock('astro/loaders', () => ({
|
||||
glob: () => ({})
|
||||
}))
|
||||
|
||||
import { eventsSchema, gallerySchema, tutorialsSchema } from './content.config'
|
||||
|
||||
const validEntry = {
|
||||
order: 1,
|
||||
title: 'Until Our Eye Interlink harajuku',
|
||||
userAlias: 'ShaneF Motion Design',
|
||||
teamAlias: 'ThinkDiffusion',
|
||||
tool: 'ComfyUI',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/eye.webm',
|
||||
href: 'https://www.thinkdiffusion.com/studio'
|
||||
}
|
||||
|
||||
describe('gallerySchema', () => {
|
||||
it('accepts a valid entry', () => {
|
||||
const result = gallerySchema.safeParse(validEntry)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects an entry missing a required field with a Zod error naming the field', () => {
|
||||
const { title: _omit, ...withoutTitle } = validEntry
|
||||
const result = gallerySchema.safeParse(withoutTitle)
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
const paths = result.error.issues.map((issue) => issue.path.join('.'))
|
||||
expect(paths).toContain('title')
|
||||
}
|
||||
})
|
||||
|
||||
it('defaults visible to true when omitted', () => {
|
||||
const result = gallerySchema.safeParse(validEntry)
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.visible).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves an explicit visible: false', () => {
|
||||
const result = gallerySchema.safeParse({ ...validEntry, visible: false })
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.visible).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects an invalid URL in image/video/href', () => {
|
||||
const result = gallerySchema.safeParse({
|
||||
...validEntry,
|
||||
href: 'not a url'
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
const validEvent = {
|
||||
order: 1,
|
||||
label: 'Live Stream:',
|
||||
title: 'Zero to Node: Building Your First Workflow',
|
||||
cta: 'Link',
|
||||
href: 'https://example.com/event'
|
||||
}
|
||||
|
||||
describe('eventsSchema', () => {
|
||||
it('accepts a valid entry', () => {
|
||||
const result = eventsSchema.safeParse(validEvent)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects an entry missing a required field with a Zod error naming the field', () => {
|
||||
const { title: _omit, ...withoutTitle } = validEvent
|
||||
const result = eventsSchema.safeParse(withoutTitle)
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
const paths = result.error.issues.map((issue) => issue.path.join('.'))
|
||||
expect(paths).toContain('title')
|
||||
}
|
||||
})
|
||||
|
||||
it('accepts "#" as a placeholder href', () => {
|
||||
const result = eventsSchema.safeParse({ ...validEvent, href: '#' })
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a truly malformed href that is neither a URL nor "#"', () => {
|
||||
const result = eventsSchema.safeParse({ ...validEvent, href: 'not a url' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
const validTutorial = {
|
||||
order: 1,
|
||||
tags: ['Partner Nodes', 'Image To Video'],
|
||||
title: 'Cleanplate Walkthrough',
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/cleanplate_walkthrough_v03.mp4',
|
||||
poster:
|
||||
'https://media.comfy.org/website/learning/cleanplate_walkthrough_v03_thumbnail.jpg'
|
||||
}
|
||||
|
||||
describe('tutorialsSchema', () => {
|
||||
it('accepts a valid entry', () => {
|
||||
const result = tutorialsSchema.safeParse(validTutorial)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects an entry missing a required field with a Zod error naming the field', () => {
|
||||
const { title: _omit, ...withoutTitle } = validTutorial
|
||||
const result = tutorialsSchema.safeParse(withoutTitle)
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
const paths = result.error.issues.map((issue) => issue.path.join('.'))
|
||||
expect(paths).toContain('title')
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects an invalid videoSrc URL', () => {
|
||||
const result = tutorialsSchema.safeParse({
|
||||
...validTutorial,
|
||||
videoSrc: 'not a url'
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,78 +0,0 @@
|
||||
import { defineCollection } from 'astro:content'
|
||||
import { glob } from 'astro/loaders'
|
||||
import { z } from 'astro/zod'
|
||||
|
||||
export const gallerySchema = z.object({
|
||||
order: z.number().int(),
|
||||
image: z.string().url().optional(),
|
||||
video: z.string().url().optional(),
|
||||
title: z.string(),
|
||||
userAlias: z.string(),
|
||||
teamAlias: z.string(),
|
||||
tool: z.string(),
|
||||
href: z.string().url().optional(),
|
||||
objectPosition: z.string().optional(),
|
||||
objectFit: z.string().optional(),
|
||||
visible: z.boolean().default(true)
|
||||
})
|
||||
|
||||
export type GalleryItem = z.infer<typeof gallerySchema>
|
||||
|
||||
export const eventsSchema = z.object({
|
||||
order: z.number().int(),
|
||||
label: z.string(),
|
||||
title: z.string(),
|
||||
cta: z.string(),
|
||||
href: z.string().url().or(z.literal('#'))
|
||||
})
|
||||
|
||||
export type EventItem = z.infer<typeof eventsSchema>
|
||||
|
||||
export const tutorialsSchema = z.object({
|
||||
order: z.number().int(),
|
||||
tags: z.array(z.string()),
|
||||
title: z.string(),
|
||||
videoSrc: z.string().url(),
|
||||
href: z.string().url().optional(),
|
||||
poster: z.string().url().optional(),
|
||||
posterTime: z.number().optional()
|
||||
})
|
||||
|
||||
export type LearningTutorial = z.infer<typeof tutorialsSchema>
|
||||
export type ResolvedTutorial = LearningTutorial & { slug: string }
|
||||
|
||||
// The default `generateId` lowercases path segments (e.g. `zh-CN/foo` becomes
|
||||
// `zh-cn/foo`), which collides with the BCP-47 locale codes Astro's i18n
|
||||
// config uses elsewhere. Strip the file extension to keep the path — and
|
||||
// therefore the locale prefix — verbatim.
|
||||
const preservePathId = ({ entry }: { entry: string }): string =>
|
||||
entry.replace(/\.[^.]+$/, '')
|
||||
|
||||
const gallery = defineCollection({
|
||||
loader: glob({
|
||||
pattern: '**/*.json',
|
||||
base: './src/content/gallery',
|
||||
generateId: preservePathId
|
||||
}),
|
||||
schema: gallerySchema
|
||||
})
|
||||
|
||||
const events = defineCollection({
|
||||
loader: glob({
|
||||
pattern: '**/*.json',
|
||||
base: './src/content/events',
|
||||
generateId: preservePathId
|
||||
}),
|
||||
schema: eventsSchema
|
||||
})
|
||||
|
||||
const tutorials = defineCollection({
|
||||
loader: glob({
|
||||
pattern: '**/*.md',
|
||||
base: './src/content/tutorials',
|
||||
generateId: preservePathId
|
||||
}),
|
||||
schema: tutorialsSchema
|
||||
})
|
||||
|
||||
export const collections = { gallery, events, tutorials }
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"order": 2,
|
||||
"label": "Event 1",
|
||||
"title": "Lorem ipsum dollar sita met",
|
||||
"cta": "London, UK",
|
||||
"href": "#"
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"order": 3,
|
||||
"label": "Event 2",
|
||||
"title": "Lorem ipsum dollar sita met",
|
||||
"cta": "San Francisco",
|
||||
"href": "#"
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"order": 1,
|
||||
"label": "Live Stream:",
|
||||
"title": "Zero to Node: Building Your First Workflow",
|
||||
"cta": "Link",
|
||||
"href": "#"
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"order": 2,
|
||||
"label": "活动 1",
|
||||
"title": "此处为活动描述的占位文本",
|
||||
"cta": "英国伦敦",
|
||||
"href": "#"
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"order": 3,
|
||||
"label": "活动 2",
|
||||
"title": "此处为活动描述的占位文本",
|
||||
"cta": "旧金山",
|
||||
"href": "#"
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"order": 1,
|
||||
"label": "直播:",
|
||||
"title": "从零到节点:构建你的第一个工作流",
|
||||
"cta": "链接",
|
||||
"href": "#"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 17,
|
||||
"image": "https://media.comfy.org/website/gallery/gallery.webp",
|
||||
"title": "Amber Astronaut",
|
||||
"userAlias": "Yogo",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://de.linkedin.com/in/milan-kastenmueller-18778a174"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"order": 21,
|
||||
"image": "https://media.comfy.org/website/gallery/amber-passage_compressed.jpg",
|
||||
"title": "Amber Passage",
|
||||
"userAlias": "Purz Beats",
|
||||
"teamAlias": "Comfy",
|
||||
"tool": "Grok Imagine",
|
||||
"href": "https://www.youtube.com/@PurzBeats",
|
||||
"objectPosition": "bottom",
|
||||
"visible": false
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 16,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/clouds_statue.webm",
|
||||
"title": "Animation Reel",
|
||||
"userAlias": "Andidea",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.youtube.com/watch?v=qu3eIQ1uln8"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 5,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/cigarette.webm",
|
||||
"title": "Autopoiesis",
|
||||
"userAlias": "Yogo",
|
||||
"teamAlias": "Visual Frisson",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/visualfrisson/?hl=en"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 13,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/paul_trillo.webm",
|
||||
"title": "Cuco - A Love Letter To LA",
|
||||
"userAlias": "Paul Trillo",
|
||||
"teamAlias": "CoffeeVectors",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://vimeo.com/1062859798"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 12,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/dududu.webm",
|
||||
"title": "DDU-DU DDU-DU",
|
||||
"userAlias": "Purz",
|
||||
"teamAlias": "Andidea",
|
||||
"tool": "Animatediff",
|
||||
"href": "https://vimeo.com/1019924290"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 18,
|
||||
"image": "https://media.comfy.org/website/gallery/desert.webp",
|
||||
"title": "Desert Landing",
|
||||
"userAlias": "Yogo",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://de.linkedin.com/in/milan-kastenmueller-18778a174"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 6,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/Eat%20It%20-%20Dance%20%5BWanAnimate%5D2.webm",
|
||||
"title": "Eat It - Dance",
|
||||
"userAlias": "Johana Lyu",
|
||||
"teamAlias": "Visual Frisson",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.joannalyu.com/"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 7,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/flower.webm",
|
||||
"title": "Fall",
|
||||
"userAlias": "Nathan Shipley",
|
||||
"teamAlias": "Visual Frisson",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/p/C3k9t_6vH5F/"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 11,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/clouds.webm",
|
||||
"title": "It's gonna be a good good summer",
|
||||
"userAlias": "Paul Trillo",
|
||||
"teamAlias": "",
|
||||
"tool": "CogvideoX",
|
||||
"href": "https://vimeo.com/1019685900"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 15,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/swings.webm",
|
||||
"title": "Goodbye Beijing",
|
||||
"userAlias": "Rui",
|
||||
"teamAlias": "makeitrad",
|
||||
"tool": "Animatediff",
|
||||
"href": "https://x.com/rui40000"
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"order": 23,
|
||||
"image": "https://media.comfy.org/website/gallery/midnight_umami_compressed.png",
|
||||
"title": "Midnight Umami",
|
||||
"userAlias": "Purz Beats",
|
||||
"teamAlias": "Comfy",
|
||||
"tool": "Grok Imagine",
|
||||
"href": "https://www.youtube.com/@PurzBeats",
|
||||
"visible": false
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"order": 20,
|
||||
"video": "https://media.comfy.org/website/gallery/milos-little-wonder_compressed.mp4",
|
||||
"title": "Milos Little Wonder",
|
||||
"userAlias": "Purz Beats",
|
||||
"teamAlias": "Comfy",
|
||||
"tool": "Grok Imagine",
|
||||
"href": "https://www.youtube.com/@PurzBeats",
|
||||
"visible": false
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 3,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/arcade.webm",
|
||||
"title": "Neon Nights",
|
||||
"userAlias": "ShaneF Motion Design",
|
||||
"teamAlias": "DOGSTUDIO/DEPT\u00ae",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/p/C1kG1oErzUV/"
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"order": 22,
|
||||
"video": "https://media.comfy.org/website/gallery/neon-revenant_compressed.mp4",
|
||||
"title": "Neon Revenant",
|
||||
"userAlias": "Eric Solorio",
|
||||
"teamAlias": "Comfy",
|
||||
"tool": "Grok Imagine",
|
||||
"href": "https://www.instagram.com/enigmatic_e",
|
||||
"visible": false
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 9,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/origami_shortened.webm",
|
||||
"title": "Origami world",
|
||||
"userAlias": "Karen X",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/karenxcheng/"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 2,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/kyrie.webm",
|
||||
"title": "Origins - Kyrie Irving",
|
||||
"userAlias": "ShaneF Motion Design",
|
||||
"teamAlias": "ThinkDiffusion",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://vimeo.com/1021360563"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 10,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/biking.webm",
|
||||
"title": "Shot on InstaX",
|
||||
"userAlias": "Karen X",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/karenxcheng/"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 14,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/chibi_fish_tank_shortened.webm",
|
||||
"title": "Show you my garden",
|
||||
"userAlias": "Paul Trillo",
|
||||
"teamAlias": "",
|
||||
"tool": "CogvideoX",
|
||||
"href": "https://vimeo.com/1019685479"
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"order": 19,
|
||||
"image": "https://media.comfy.org/website/gallery/subway-swan_compressed.png",
|
||||
"title": "Subway Swan",
|
||||
"userAlias": "Purz Beats",
|
||||
"teamAlias": "Comfy",
|
||||
"tool": "Grok Imagine",
|
||||
"href": "https://www.youtube.com/@PurzBeats",
|
||||
"visible": false
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 1,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/eye.webm",
|
||||
"title": "Until Our Eye Interlink harajuku",
|
||||
"userAlias": "ShaneF Motion Design",
|
||||
"teamAlias": "ThinkDiffusion",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.thinkdiffusion.com/studio#success-stories-anta"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 8,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/buildings.webm",
|
||||
"title": "Untitled",
|
||||
"userAlias": "Nathan Shipley",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/p/C6rEuJ4p9xU/"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 4,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/dusk_mountains.webm",
|
||||
"title": "Untitled",
|
||||
"userAlias": "MidJourney man",
|
||||
"teamAlias": "DOGSTUDIO/DEPT\u00ae",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/midjourney.man/?hl=fr"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 17,
|
||||
"image": "https://media.comfy.org/website/gallery/gallery.webp",
|
||||
"title": "Amber Astronaut",
|
||||
"userAlias": "Yogo",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://de.linkedin.com/in/milan-kastenmueller-18778a174"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 16,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/clouds_statue.webm",
|
||||
"title": "Animation Reel",
|
||||
"userAlias": "Andidea",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.youtube.com/watch?v=qu3eIQ1uln8"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 5,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/cigarette.webm",
|
||||
"title": "Autopoiesis",
|
||||
"userAlias": "Yogo",
|
||||
"teamAlias": "Visual Frisson",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/visualfrisson/?hl=en"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 13,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/paul_trillo.webm",
|
||||
"title": "Cuco - A Love Letter To LA",
|
||||
"userAlias": "Paul Trillo",
|
||||
"teamAlias": "CoffeeVectors",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://vimeo.com/1062859798"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 12,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/dududu.webm",
|
||||
"title": "DDU-DU DDU-DU",
|
||||
"userAlias": "Purz",
|
||||
"teamAlias": "Andidea",
|
||||
"tool": "Animatediff",
|
||||
"href": "https://vimeo.com/1019924290"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 18,
|
||||
"image": "https://media.comfy.org/website/gallery/desert.webp",
|
||||
"title": "Desert Landing",
|
||||
"userAlias": "Yogo",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://de.linkedin.com/in/milan-kastenmueller-18778a174"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 6,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/Eat%20It%20-%20Dance%20%5BWanAnimate%5D2.webm",
|
||||
"title": "Eat It - Dance",
|
||||
"userAlias": "Johana Lyu",
|
||||
"teamAlias": "Visual Frisson",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.joannalyu.com/"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 7,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/flower.webm",
|
||||
"title": "Fall",
|
||||
"userAlias": "Nathan Shipley",
|
||||
"teamAlias": "Visual Frisson",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/p/C3k9t_6vH5F/"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 11,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/clouds.webm",
|
||||
"title": "It's gonna be a good good summer",
|
||||
"userAlias": "Paul Trillo",
|
||||
"teamAlias": "",
|
||||
"tool": "CogvideoX",
|
||||
"href": "https://vimeo.com/1019685900"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 15,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/swings.webm",
|
||||
"title": "Goodbye Beijing",
|
||||
"userAlias": "Rui",
|
||||
"teamAlias": "makeitrad",
|
||||
"tool": "Animatediff",
|
||||
"href": "https://x.com/rui40000"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 3,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/arcade.webm",
|
||||
"title": "Neon Nights",
|
||||
"userAlias": "ShaneF Motion Design",
|
||||
"teamAlias": "DOGSTUDIO/DEPT\u00ae",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/p/C1kG1oErzUV/"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 9,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/origami_shortened.webm",
|
||||
"title": "Origami world",
|
||||
"userAlias": "Karen X",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/karenxcheng/"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 2,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/kyrie.webm",
|
||||
"title": "Origins - Kyrie Irving",
|
||||
"userAlias": "ShaneF Motion Design",
|
||||
"teamAlias": "ThinkDiffusion",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://vimeo.com/1021360563"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 10,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/biking.webm",
|
||||
"title": "Shot on InstaX",
|
||||
"userAlias": "Karen X",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/karenxcheng/"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 14,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/chibi_fish_tank_shortened.webm",
|
||||
"title": "Show you my garden",
|
||||
"userAlias": "Paul Trillo",
|
||||
"teamAlias": "",
|
||||
"tool": "CogvideoX",
|
||||
"href": "https://vimeo.com/1019685479"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 1,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/eye.webm",
|
||||
"title": "Until Our Eye Interlink harajuku",
|
||||
"userAlias": "ShaneF Motion Design",
|
||||
"teamAlias": "ThinkDiffusion",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.thinkdiffusion.com/studio#success-stories-anta"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 8,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/buildings.webm",
|
||||
"title": "Untitled",
|
||||
"userAlias": "Nathan Shipley",
|
||||
"teamAlias": "",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/p/C6rEuJ4p9xU/"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"order": 4,
|
||||
"video": "https://media.comfy.org/videos/compressed_512/dusk_mountains.webm",
|
||||
"title": "Untitled",
|
||||
"userAlias": "MidJourney man",
|
||||
"teamAlias": "DOGSTUDIO/DEPT\u00ae",
|
||||
"tool": "ComfyUI",
|
||||
"href": "https://www.instagram.com/midjourney.man/?hl=fr"
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getCollection } from 'astro:content'
|
||||
|
||||
import {
|
||||
getEventsByLocale,
|
||||
getGalleryByIds,
|
||||
getTutorialsByLocale,
|
||||
getVisibleGalleryByLocale,
|
||||
slugOf
|
||||
} from './queries'
|
||||
|
||||
vi.mock('astro:content', () => ({
|
||||
getCollection: vi.fn()
|
||||
}))
|
||||
|
||||
const getCollectionMock = vi.mocked(getCollection)
|
||||
|
||||
interface FixtureEntry {
|
||||
id: string
|
||||
collection: 'gallery'
|
||||
data: {
|
||||
order: number
|
||||
title: string
|
||||
userAlias: string
|
||||
teamAlias: string
|
||||
tool: string
|
||||
image?: string
|
||||
video?: string
|
||||
href?: string
|
||||
objectPosition?: string
|
||||
objectFit?: string
|
||||
visible: boolean
|
||||
}
|
||||
}
|
||||
|
||||
function entry(
|
||||
id: string,
|
||||
overrides: Partial<FixtureEntry['data']> = {}
|
||||
): FixtureEntry {
|
||||
return {
|
||||
id,
|
||||
collection: 'gallery',
|
||||
data: {
|
||||
order: 0,
|
||||
title: 'Title',
|
||||
userAlias: 'User',
|
||||
teamAlias: 'Team',
|
||||
tool: 'ComfyUI',
|
||||
visible: true,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
getCollectionMock.mockReset()
|
||||
})
|
||||
|
||||
describe('slugOf', () => {
|
||||
it('strips the locale prefix from an entry id', () => {
|
||||
const entry = { id: 'en/until-our-eye-interlink-harajuku' }
|
||||
expect(slugOf(entry)).toBe('until-our-eye-interlink-harajuku')
|
||||
})
|
||||
|
||||
it('handles zh-CN prefix', () => {
|
||||
const entry = { id: 'zh-CN/desert-landing' }
|
||||
expect(slugOf(entry)).toBe('desert-landing')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getVisibleGalleryByLocale', () => {
|
||||
it('returns only entries whose id starts with the requested locale prefix', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
entry('en/alpha'),
|
||||
entry('en/beta'),
|
||||
entry('zh-CN/alpha'),
|
||||
entry('zh-CN/gamma')
|
||||
] as never)
|
||||
|
||||
const en = await getVisibleGalleryByLocale('en')
|
||||
expect(en.map((e) => e.id)).toEqual(['en/alpha', 'en/beta'])
|
||||
|
||||
const zh = await getVisibleGalleryByLocale('zh-CN')
|
||||
expect(zh.map((e) => e.id)).toEqual(['zh-CN/alpha', 'zh-CN/gamma'])
|
||||
})
|
||||
|
||||
it('excludes entries with visible: false', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
entry('en/shown', { visible: true }),
|
||||
entry('en/hidden', { visible: false }),
|
||||
entry('en/also-shown', { visible: true })
|
||||
] as never)
|
||||
|
||||
const result = await getVisibleGalleryByLocale('en')
|
||||
expect(result.map((e) => e.id)).toEqual(['en/shown', 'en/also-shown'])
|
||||
})
|
||||
|
||||
it('sorts entries by the order field ascending, not by id', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
entry('en/charlie', { order: 1 }),
|
||||
entry('en/alpha', { order: 3 }),
|
||||
entry('en/bravo', { order: 2 })
|
||||
] as never)
|
||||
|
||||
const result = await getVisibleGalleryByLocale('en')
|
||||
expect(result.map((e) => e.id)).toEqual([
|
||||
'en/charlie',
|
||||
'en/bravo',
|
||||
'en/alpha'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getGalleryByIds', () => {
|
||||
it('returns entries in the order of the input slug array', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
entry('en/alpha'),
|
||||
entry('en/beta'),
|
||||
entry('en/gamma'),
|
||||
entry('zh-CN/alpha')
|
||||
] as never)
|
||||
|
||||
const result = await getGalleryByIds(['gamma', 'alpha', 'beta'], 'en')
|
||||
expect(result.map((e) => e.id)).toEqual(['en/gamma', 'en/alpha', 'en/beta'])
|
||||
})
|
||||
|
||||
it('drops slugs with no matching entry in the requested locale', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
entry('en/alpha'),
|
||||
entry('zh-CN/beta')
|
||||
] as never)
|
||||
|
||||
const result = await getGalleryByIds(['alpha', 'missing', 'beta'], 'en')
|
||||
expect(result.map((e) => e.id)).toEqual(['en/alpha'])
|
||||
})
|
||||
})
|
||||
|
||||
interface EventsFixtureEntry {
|
||||
id: string
|
||||
collection: 'events'
|
||||
data: {
|
||||
order: number
|
||||
label: string
|
||||
title: string
|
||||
cta: string
|
||||
href: string
|
||||
}
|
||||
}
|
||||
|
||||
function eventEntry(
|
||||
id: string,
|
||||
overrides: Partial<EventsFixtureEntry['data']> = {}
|
||||
): EventsFixtureEntry {
|
||||
return {
|
||||
id,
|
||||
collection: 'events',
|
||||
data: {
|
||||
order: 0,
|
||||
label: 'Label',
|
||||
title: 'Title',
|
||||
cta: 'CTA',
|
||||
href: '#',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('getEventsByLocale', () => {
|
||||
it('returns only entries whose id starts with the requested locale prefix', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
eventEntry('en/alpha'),
|
||||
eventEntry('en/beta'),
|
||||
eventEntry('zh-CN/alpha')
|
||||
] as never)
|
||||
|
||||
const en = await getEventsByLocale('en')
|
||||
expect(en.map((e) => e.id)).toEqual(['en/alpha', 'en/beta'])
|
||||
})
|
||||
|
||||
it('sorts entries by the order field ascending, not by id', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
eventEntry('en/charlie', { order: 1 }),
|
||||
eventEntry('en/alpha', { order: 3 }),
|
||||
eventEntry('en/bravo', { order: 2 })
|
||||
] as never)
|
||||
|
||||
const result = await getEventsByLocale('en')
|
||||
expect(result.map((e) => e.id)).toEqual([
|
||||
'en/charlie',
|
||||
'en/bravo',
|
||||
'en/alpha'
|
||||
])
|
||||
})
|
||||
|
||||
it('returns an empty array for a locale with no entries', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
eventEntry('en/alpha'),
|
||||
eventEntry('en/beta')
|
||||
] as never)
|
||||
|
||||
const result = await getEventsByLocale('zh-CN')
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
interface TutorialsFixtureEntry {
|
||||
id: string
|
||||
collection: 'tutorials'
|
||||
data: {
|
||||
order: number
|
||||
tags: string[]
|
||||
title: string
|
||||
videoSrc: string
|
||||
href?: string
|
||||
poster?: string
|
||||
posterTime?: number
|
||||
}
|
||||
}
|
||||
|
||||
function tutorialEntry(
|
||||
id: string,
|
||||
overrides: Partial<TutorialsFixtureEntry['data']> = {}
|
||||
): TutorialsFixtureEntry {
|
||||
return {
|
||||
id,
|
||||
collection: 'tutorials',
|
||||
data: {
|
||||
order: 0,
|
||||
tags: ['Tag'],
|
||||
title: 'Title',
|
||||
videoSrc: 'https://example.com/video.mp4',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('getTutorialsByLocale', () => {
|
||||
it('returns only entries whose id starts with the requested locale prefix', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
tutorialEntry('en/alpha'),
|
||||
tutorialEntry('en/beta'),
|
||||
tutorialEntry('zh-CN/alpha')
|
||||
] as never)
|
||||
|
||||
const en = await getTutorialsByLocale('en')
|
||||
expect(en.map((e) => e.id)).toEqual(['en/alpha', 'en/beta'])
|
||||
})
|
||||
|
||||
it('sorts entries by the order field ascending, not by id', async () => {
|
||||
getCollectionMock.mockResolvedValue([
|
||||
tutorialEntry('en/charlie', { order: 1 }),
|
||||
tutorialEntry('en/alpha', { order: 3 }),
|
||||
tutorialEntry('en/bravo', { order: 2 })
|
||||
] as never)
|
||||
|
||||
const result = await getTutorialsByLocale('en')
|
||||
expect(result.map((e) => e.id)).toEqual([
|
||||
'en/charlie',
|
||||
'en/bravo',
|
||||
'en/alpha'
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,61 +0,0 @@
|
||||
import { getCollection } from 'astro:content'
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
|
||||
import type { Locale } from '../i18n/translations'
|
||||
|
||||
export type GalleryEntry = CollectionEntry<'gallery'>
|
||||
export type EventsEntry = CollectionEntry<'events'>
|
||||
export type TutorialsEntry = CollectionEntry<'tutorials'>
|
||||
|
||||
export function slugOf(entry: { id: string }): string {
|
||||
const slash = entry.id.indexOf('/')
|
||||
return slash === -1 ? entry.id : entry.id.slice(slash + 1)
|
||||
}
|
||||
|
||||
export async function getVisibleGalleryByLocale(
|
||||
locale: Locale
|
||||
): Promise<GalleryEntry[]> {
|
||||
const prefix = `${locale}/`
|
||||
const entries: GalleryEntry[] = await getCollection('gallery')
|
||||
return entries
|
||||
.filter(
|
||||
(entry) => entry.id.startsWith(prefix) && entry.data.visible !== false
|
||||
)
|
||||
.sort((a, b) => a.data.order - b.data.order)
|
||||
}
|
||||
|
||||
export async function getGalleryByIds(
|
||||
slugs: readonly string[],
|
||||
locale: Locale
|
||||
): Promise<GalleryEntry[]> {
|
||||
const entries: GalleryEntry[] = await getCollection('gallery')
|
||||
const bySlug = new Map<string, GalleryEntry>()
|
||||
for (const entry of entries) {
|
||||
if (entry.id.startsWith(`${locale}/`)) {
|
||||
bySlug.set(slugOf(entry), entry)
|
||||
}
|
||||
}
|
||||
return slugs
|
||||
.map((slug) => bySlug.get(slug))
|
||||
.filter((entry): entry is GalleryEntry => entry !== undefined)
|
||||
}
|
||||
|
||||
export async function getEventsByLocale(
|
||||
locale: Locale
|
||||
): Promise<EventsEntry[]> {
|
||||
const prefix = `${locale}/`
|
||||
const entries: EventsEntry[] = await getCollection('events')
|
||||
return entries
|
||||
.filter((entry) => entry.id.startsWith(prefix))
|
||||
.sort((a, b) => a.data.order - b.data.order)
|
||||
}
|
||||
|
||||
export async function getTutorialsByLocale(
|
||||
locale: Locale
|
||||
): Promise<TutorialsEntry[]> {
|
||||
const prefix = `${locale}/`
|
||||
const entries: TutorialsEntry[] = await getCollection('tutorials')
|
||||
return entries
|
||||
.filter((entry) => entry.id.startsWith(prefix))
|
||||
.sort((a, b) => a.data.order - b.data.order)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
order: 1
|
||||
tags:
|
||||
- Partner Nodes
|
||||
- Image To Video
|
||||
title: Cleanplate Walkthrough
|
||||
videoSrc: https://media.comfy.org/website/learning/cleanplate_walkthrough_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/cleanplate_walkthrough_v03_thumbnail.jpg
|
||||
---
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
order: 2
|
||||
tags:
|
||||
- Partner Nodes
|
||||
- Image To Video
|
||||
title: Deaging Workflow
|
||||
videoSrc: https://media.comfy.org/website/learning/deaging_workflow_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/deaging_workflow_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=93f286fbc2c8
|
||||
---
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
order: 3
|
||||
tags:
|
||||
- Partner Nodes
|
||||
- Image To Video
|
||||
title: Frame Adjustments Demo
|
||||
videoSrc: https://media.comfy.org/website/learning/frame_adjustments_demo_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/frame_adjustments_demo_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=7dca0438edf4
|
||||
---
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
order: 4
|
||||
tags:
|
||||
- Partner Nodes
|
||||
- Image To Video
|
||||
title: Mattes and Utilities
|
||||
videoSrc: https://media.comfy.org/website/learning/mattes_and_utilities_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/mattes_and_utilities_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=be0889296f65
|
||||
---
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
order: 5
|
||||
tags:
|
||||
- Partner Nodes
|
||||
- Image To Video
|
||||
title: Seedance Demo ComfyUI
|
||||
videoSrc: https://media.comfy.org/website/learning/seedance_demo_comfyui_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/seedance seedance_demo_comfyui_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=ef543bd4a773
|
||||
---
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
order: 6
|
||||
tags:
|
||||
- Partner Nodes
|
||||
- Image To Video
|
||||
title: Sky Replacement
|
||||
videoSrc: https://media.comfy.org/website/learning/skyreplacement_smaller_v06.mp4
|
||||
poster: https://media.comfy.org/website/learning/skyreplacement_smaller_v06_thumbnail.jpg
|
||||
href: https://comfy.org/workflows/537cf7f1f745-537cf7f1f745/
|
||||
---
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
order: 1
|
||||
tags:
|
||||
- 合作伙伴节点
|
||||
- 图像生成视频
|
||||
title: 净板演练
|
||||
videoSrc: https://media.comfy.org/website/learning/cleanplate_walkthrough_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/cleanplate_walkthrough_v03_thumbnail.jpg
|
||||
---
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
order: 2
|
||||
tags:
|
||||
- 合作伙伴节点
|
||||
- 图像生成视频
|
||||
title: 减龄工作流
|
||||
videoSrc: https://media.comfy.org/website/learning/deaging_workflow_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/deaging_workflow_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=93f286fbc2c8
|
||||
---
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
order: 3
|
||||
tags:
|
||||
- 合作伙伴节点
|
||||
- 图像生成视频
|
||||
title: 帧调整演示
|
||||
videoSrc: https://media.comfy.org/website/learning/frame_adjustments_demo_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/frame_adjustments_demo_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=7dca0438edf4
|
||||
---
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
order: 4
|
||||
tags:
|
||||
- 合作伙伴节点
|
||||
- 图像生成视频
|
||||
title: 遮罩与实用工具
|
||||
videoSrc: https://media.comfy.org/website/learning/mattes_and_utilities_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/mattes_and_utilities_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=be0889296f65
|
||||
---
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
order: 5
|
||||
tags:
|
||||
- 合作伙伴节点
|
||||
- 图像生成视频
|
||||
title: Seedance ComfyUI 演示
|
||||
videoSrc: https://media.comfy.org/website/learning/seedance_demo_comfyui_v03.mp4
|
||||
poster: https://media.comfy.org/website/learning/seedance seedance_demo_comfyui_v03_thumbnail.jpg
|
||||
href: https://cloud.comfy.org/?share=ef543bd4a773
|
||||
---
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
order: 6
|
||||
tags:
|
||||
- 合作伙伴节点
|
||||
- 图像生成视频
|
||||
title: 天空替换
|
||||
videoSrc: https://media.comfy.org/website/learning/skyreplacement_smaller_v06.mp4
|
||||
poster: https://media.comfy.org/website/learning/skyreplacement_smaller_v06_thumbnail.jpg
|
||||
href: https://comfy.org/workflows/537cf7f1f745-537cf7f1f745/
|
||||
---
|
||||
31
apps/website/src/data/events.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { EventItem } from '../components/common/EventsSection.vue'
|
||||
|
||||
export const learningEvents: readonly EventItem[] = [
|
||||
{
|
||||
label: { en: 'Live Stream:', 'zh-CN': '直播:' },
|
||||
title: {
|
||||
en: 'Zero to Node: Building Your First Workflow',
|
||||
'zh-CN': '从零到节点:构建你的第一个工作流'
|
||||
},
|
||||
cta: { en: 'Link', 'zh-CN': '链接' },
|
||||
href: '#'
|
||||
},
|
||||
{
|
||||
label: { en: 'Event 1', 'zh-CN': '活动 1' },
|
||||
title: {
|
||||
en: 'Lorem ipsum dollar sita met',
|
||||
'zh-CN': '此处为活动描述的占位文本'
|
||||
},
|
||||
cta: { en: 'London, UK', 'zh-CN': '英国伦敦' },
|
||||
href: '#'
|
||||
},
|
||||
{
|
||||
label: { en: 'Event 2', 'zh-CN': '活动 2' },
|
||||
title: {
|
||||
en: 'Lorem ipsum dollar sita met',
|
||||
'zh-CN': '此处为活动描述的占位文本'
|
||||
},
|
||||
cta: { en: 'San Francisco', 'zh-CN': '旧金山' },
|
||||
href: '#'
|
||||
}
|
||||
] as const
|
||||
191
apps/website/src/data/gallery.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
export interface GalleryItem {
|
||||
id: string
|
||||
image?: string
|
||||
video?: string
|
||||
title: string
|
||||
userAlias: string
|
||||
teamAlias: string
|
||||
tool: string
|
||||
href?: string
|
||||
objectPosition?: string
|
||||
objectFit?: string
|
||||
/** Defaults to true. Set to false to hide this item from rendered lists. */
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
const galleryItems: GalleryItem[] = [
|
||||
{
|
||||
id: 'until-our-eye-interlink-harajuku',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/eye.webm',
|
||||
title: 'Until Our Eye Interlink harajuku',
|
||||
userAlias: 'ShaneF Motion Design',
|
||||
teamAlias: 'ThinkDiffusion',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.thinkdiffusion.com/studio#success-stories-anta'
|
||||
},
|
||||
{
|
||||
id: 'origins-kyrie-irving',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/kyrie.webm',
|
||||
title: 'Origins - Kyrie Irving',
|
||||
userAlias: 'ShaneF Motion Design',
|
||||
teamAlias: 'ThinkDiffusion',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://vimeo.com/1021360563'
|
||||
},
|
||||
{
|
||||
id: 'neon-nights',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/arcade.webm',
|
||||
title: 'Neon Nights',
|
||||
userAlias: 'ShaneF Motion Design',
|
||||
teamAlias: 'DOGSTUDIO/DEPT®',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.instagram.com/p/C1kG1oErzUV/'
|
||||
},
|
||||
{
|
||||
id: 'untitled-dusk-mountains',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/dusk_mountains.webm',
|
||||
title: 'Untitled',
|
||||
userAlias: 'MidJourney man',
|
||||
teamAlias: 'DOGSTUDIO/DEPT®',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.instagram.com/midjourney.man/?hl=fr'
|
||||
},
|
||||
{
|
||||
id: 'autopoiesis',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/cigarette.webm',
|
||||
title: 'Autopoiesis',
|
||||
userAlias: 'Yogo',
|
||||
teamAlias: 'Visual Frisson',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.instagram.com/visualfrisson/?hl=en'
|
||||
},
|
||||
{
|
||||
id: 'eat-it-dance',
|
||||
video:
|
||||
'https://media.comfy.org/videos/compressed_512/Eat%20It%20-%20Dance%20%5BWanAnimate%5D2.webm',
|
||||
title: 'Eat It - Dance',
|
||||
userAlias: 'Johana Lyu',
|
||||
teamAlias: 'Visual Frisson',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.joannalyu.com/'
|
||||
},
|
||||
{
|
||||
id: 'fall',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/flower.webm',
|
||||
title: 'Fall',
|
||||
userAlias: 'Nathan Shipley',
|
||||
teamAlias: 'Visual Frisson',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.instagram.com/p/C3k9t_6vH5F/'
|
||||
},
|
||||
{
|
||||
id: 'untitled-buildings',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/buildings.webm',
|
||||
title: 'Untitled',
|
||||
userAlias: 'Nathan Shipley',
|
||||
teamAlias: '',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.instagram.com/p/C6rEuJ4p9xU/'
|
||||
},
|
||||
{
|
||||
id: 'origami-world',
|
||||
video:
|
||||
'https://media.comfy.org/videos/compressed_512/origami_shortened.webm',
|
||||
title: 'Origami world',
|
||||
userAlias: 'Karen X',
|
||||
teamAlias: '',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.instagram.com/karenxcheng/'
|
||||
},
|
||||
{
|
||||
id: 'shot-on-instax',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/biking.webm',
|
||||
title: 'Shot on InstaX',
|
||||
userAlias: 'Karen X',
|
||||
teamAlias: '',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.instagram.com/karenxcheng/'
|
||||
},
|
||||
{
|
||||
id: 'good-good-summer',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/clouds.webm',
|
||||
title: "It's gonna be a good good summer",
|
||||
userAlias: 'Paul Trillo',
|
||||
teamAlias: '',
|
||||
tool: 'CogvideoX',
|
||||
href: 'https://vimeo.com/1019685900'
|
||||
},
|
||||
{
|
||||
id: 'ddu-du-ddu-du',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/dududu.webm',
|
||||
title: 'DDU-DU DDU-DU',
|
||||
userAlias: 'Purz',
|
||||
teamAlias: 'Andidea',
|
||||
tool: 'Animatediff',
|
||||
href: 'https://vimeo.com/1019924290'
|
||||
},
|
||||
{
|
||||
id: 'cuco-love-letter-to-la',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/paul_trillo.webm',
|
||||
title: 'Cuco - A Love Letter To LA',
|
||||
userAlias: 'Paul Trillo',
|
||||
teamAlias: 'CoffeeVectors',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://vimeo.com/1062859798'
|
||||
},
|
||||
{
|
||||
id: 'show-you-my-garden',
|
||||
video:
|
||||
'https://media.comfy.org/videos/compressed_512/chibi_fish_tank_shortened.webm',
|
||||
title: 'Show you my garden',
|
||||
userAlias: 'Paul Trillo',
|
||||
teamAlias: '',
|
||||
tool: 'CogvideoX',
|
||||
href: 'https://vimeo.com/1019685479'
|
||||
},
|
||||
{
|
||||
id: 'goodbye-beijing',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/swings.webm',
|
||||
title: 'Goodbye Beijing',
|
||||
userAlias: 'Rui',
|
||||
teamAlias: 'makeitrad',
|
||||
tool: 'Animatediff',
|
||||
href: 'https://x.com/rui40000'
|
||||
},
|
||||
{
|
||||
id: 'animation-reel',
|
||||
video: 'https://media.comfy.org/videos/compressed_512/clouds_statue.webm',
|
||||
title: 'Animation Reel',
|
||||
userAlias: 'Andidea',
|
||||
teamAlias: '',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://www.youtube.com/watch?v=qu3eIQ1uln8'
|
||||
},
|
||||
{
|
||||
id: 'amber-astronaut',
|
||||
image: 'https://media.comfy.org/website/gallery/gallery.webp',
|
||||
title: 'Amber Astronaut',
|
||||
userAlias: 'Yogo',
|
||||
teamAlias: '',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://de.linkedin.com/in/milan-kastenmueller-18778a174'
|
||||
},
|
||||
{
|
||||
id: 'desert-landing',
|
||||
image: 'https://media.comfy.org/website/gallery/desert.webp',
|
||||
title: 'Desert Landing',
|
||||
userAlias: 'Yogo',
|
||||
teamAlias: '',
|
||||
tool: 'ComfyUI',
|
||||
href: 'https://de.linkedin.com/in/milan-kastenmueller-18778a174'
|
||||
}
|
||||
]
|
||||
|
||||
export const visibleGalleryItems: GalleryItem[] = galleryItems.filter(
|
||||
(item) => item.visible !== false
|
||||
)
|
||||
|
||||
/** @knipIgnoreUsedByStackedPR */
|
||||
export function getGalleryItemById(id: string): GalleryItem | undefined {
|
||||
return galleryItems.find((item) => item.id === id)
|
||||
}
|
||||
84
apps/website/src/data/learningTutorials.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { LocalizedText, TranslationKey } from '../i18n/translations'
|
||||
|
||||
export interface LearningTutorial {
|
||||
id: string
|
||||
tags: readonly TranslationKey[]
|
||||
title: LocalizedText
|
||||
videoSrc: string
|
||||
href?: string
|
||||
poster?: string
|
||||
posterTime?: number
|
||||
}
|
||||
|
||||
const DEFAULT_POSTER_TIME_SECONDS = 1
|
||||
|
||||
const partnerNodesTag: TranslationKey = 'tags.partnerNodes'
|
||||
const imageToVideoTag: TranslationKey = 'tags.imageToVideo'
|
||||
|
||||
export const getTutorialPosterSrc = (tutorial: LearningTutorial): string =>
|
||||
tutorial.poster
|
||||
? tutorial.poster
|
||||
: `${tutorial.videoSrc}#t=${tutorial.posterTime ?? DEFAULT_POSTER_TIME_SECONDS}`
|
||||
|
||||
export const learningTutorials: readonly LearningTutorial[] = [
|
||||
{
|
||||
id: 'cleanplate_walkthrough_v03',
|
||||
title: { en: 'Cleanplate Walkthrough', 'zh-CN': '净板演练' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/cleanplate_walkthrough_v03.mp4',
|
||||
poster:
|
||||
'https://media.comfy.org/website/learning/cleanplate_walkthrough_v03_thumbnail.jpg',
|
||||
// href: '#',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
},
|
||||
{
|
||||
id: 'deaging_workflow_v03',
|
||||
title: { en: 'Deaging Workflow', 'zh-CN': '减龄工作流' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/deaging_workflow_v03.mp4',
|
||||
poster:
|
||||
'https://media.comfy.org/website/learning/deaging_workflow_v03_thumbnail.jpg',
|
||||
href: 'https://cloud.comfy.org/?share=93f286fbc2c8',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
},
|
||||
{
|
||||
id: 'frame_adjustments_demo_v03',
|
||||
title: { en: 'Frame Adjustments Demo', 'zh-CN': '帧调整演示' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/frame_adjustments_demo_v03.mp4',
|
||||
poster:
|
||||
'https://media.comfy.org/website/learning/frame_adjustments_demo_v03_thumbnail.jpg',
|
||||
href: 'https://cloud.comfy.org/?share=7dca0438edf4',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
},
|
||||
{
|
||||
id: 'mattes_and_utilities_v03',
|
||||
title: { en: 'Mattes and Utilities', 'zh-CN': '遮罩与实用工具' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/mattes_and_utilities_v03.mp4',
|
||||
poster:
|
||||
'https://media.comfy.org/website/learning/mattes_and_utilities_v03_thumbnail.jpg',
|
||||
href: 'https://cloud.comfy.org/?share=be0889296f65',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
},
|
||||
{
|
||||
id: 'seedance_demo_comfyui_v03',
|
||||
title: { en: 'Seedance Demo ComfyUI', 'zh-CN': 'Seedance ComfyUI 演示' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/seedance_demo_comfyui_v03.mp4',
|
||||
poster:
|
||||
'https://media.comfy.org/website/learning/seedance seedance_demo_comfyui_v03_thumbnail.jpg',
|
||||
href: 'https://cloud.comfy.org/?share=ef543bd4a773',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
},
|
||||
{
|
||||
id: 'skyreplacement_smaller_v06',
|
||||
title: { en: 'Sky Replacement', 'zh-CN': '天空替换' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/skyreplacement_smaller_v06.mp4',
|
||||
poster:
|
||||
'https://media.comfy.org/website/learning/skyreplacement_smaller_v06_thumbnail.jpg',
|
||||
href: 'https://comfy.org/workflows/537cf7f1f745-537cf7f1f745/',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
}
|
||||
] as const
|
||||
@@ -174,16 +174,16 @@ const translations = {
|
||||
'zh-CN': '掌控每个模型、每个节点、每个步骤、每个输出。'
|
||||
},
|
||||
'products.local.title': {
|
||||
en: 'Comfy\nLocal',
|
||||
'zh-CN': 'Comfy\n本地版'
|
||||
en: 'Comfy\nDesktop',
|
||||
'zh-CN': 'Comfy\n桌面版'
|
||||
},
|
||||
'products.local.description': {
|
||||
en: 'Run ComfyUI on your own hardware.',
|
||||
'zh-CN': '在您自己的硬件上运行 ComfyUI。'
|
||||
},
|
||||
'products.local.cta': {
|
||||
en: 'SEE LOCAL FEATURES',
|
||||
'zh-CN': '查看本地版属性'
|
||||
en: 'SEE DESKTOP FEATURES',
|
||||
'zh-CN': '查看桌面版属性'
|
||||
},
|
||||
'products.cloud.title': {
|
||||
en: 'Comfy\nCloud',
|
||||
@@ -1057,18 +1057,18 @@ const translations = {
|
||||
'zh-CN': 'Cloud 与本地运行 ComfyUI 有什么区别?'
|
||||
},
|
||||
'cloud.faq.2.a': {
|
||||
en: 'Cloud runs on powerful remote GPUs and is accessible from any device. Local runs entirely on your computer, giving you full control and offline use.',
|
||||
en: 'Cloud runs on powerful remote GPUs and is accessible from any device. Comfy Desktop runs entirely on your computer, giving you full control and offline use.',
|
||||
'zh-CN':
|
||||
'Cloud 在强大的远程 GPU 上运行,可从任何设备访问。本地版完全在您的电脑上运行,提供完全控制和离线使用。'
|
||||
'Cloud 在强大的远程 GPU 上运行,可从任何设备访问。Comfy 桌面版完全在您的电脑上运行,提供完全控制和离线使用。'
|
||||
},
|
||||
'cloud.faq.3.q': {
|
||||
en: 'Which version should I choose, Comfy Cloud or local ComfyUI (self-hosted)?',
|
||||
'zh-CN': '我应该选择 Comfy Cloud 还是本地 ComfyUI(自托管)?'
|
||||
en: 'Which version should I choose, Comfy Cloud or Comfy Desktop?',
|
||||
'zh-CN': '我应该选择 Comfy Cloud 还是 Comfy 桌面版?'
|
||||
},
|
||||
'cloud.faq.3.a': {
|
||||
en: "Comfy Cloud has zero setup, is easy to share with your team, and is faster than most GPUs you can run on a desktop workstation. You can immediately run the best models and workflows from the community on Comfy Cloud.\nLocal ComfyUI is infinitely customizable, works offline, and you don't need to worry about queue times. However, depending on what you want to create, you might need to have a good GPU and some amount of technical knowledge to install community-created custom nodes.",
|
||||
en: "Comfy Cloud has zero setup, is easy to share with your team, and is faster than most GPUs you can run on a desktop workstation. You can immediately run the best models and workflows from the community on Comfy Cloud.\nComfy Desktop is infinitely customizable, works offline, and you don't need to worry about queue times. However, depending on what you want to create, you might need to have a good GPU and some amount of technical knowledge to install community-created custom nodes.",
|
||||
'zh-CN':
|
||||
'Comfy Cloud 无需任何设置,方便与团队共享,比大多数桌面工作站 GPU 更快。您可以立即在 Comfy Cloud 上运行社区中最好的模型和工作流。\n本地 ComfyUI 可以无限定制,支持离线工作,无需担心排队时间。但根据您的创作需求,可能需要一块好的 GPU 以及一定的技术知识来安装社区创建的自定义节点。'
|
||||
'Comfy Cloud 无需任何设置,方便与团队共享,比大多数桌面工作站 GPU 更快。您可以立即在 Comfy Cloud 上运行社区中最好的模型和工作流。\nComfy 桌面版可以无限定制,支持离线工作,无需担心排队时间。但根据您的创作需求,可能需要一块好的 GPU 以及一定的技术知识来安装社区创建的自定义节点。'
|
||||
},
|
||||
'cloud.faq.4.q': {
|
||||
en: 'Do I need a GPU or a strong computer to use Comfy Cloud?',
|
||||
@@ -1091,9 +1091,9 @@ const translations = {
|
||||
'zh-CN': '我可以在 Comfy Cloud 上使用现有的工作流吗?'
|
||||
},
|
||||
'cloud.faq.6.a': {
|
||||
en: 'Yes, your workflows work across Local and Cloud. Just note that only the most popular custom nodes are supported for now, but more will be added soon.',
|
||||
en: 'Yes, your workflows work across Desktop and Cloud. Just note that only the most popular custom nodes are supported for now, but more will be added soon.',
|
||||
'zh-CN':
|
||||
'可以,您的工作流在本地和云端都能使用。请注意,目前仅支持最热门的自定义节点,但很快会添加更多。'
|
||||
'可以,您的工作流在桌面版和云端都能使用。请注意,目前仅支持最热门的自定义节点,但很快会添加更多。'
|
||||
},
|
||||
'cloud.faq.7.q': {
|
||||
en: 'Are all ComfyUI extensions and custom nodes supported?',
|
||||
@@ -1145,9 +1145,9 @@ const translations = {
|
||||
'zh-CN': '合作伙伴节点积分和我的 Cloud 订阅有什么区别?'
|
||||
},
|
||||
'cloud.faq.12.a': {
|
||||
en: 'Comfy Cloud has a credit system that is used for both Partner nodes (formerly API nodes) and running workflows on cloud.\n1. Partner Nodes (Pay-as-you-go): These nodes (formerly called API nodes) run third-party models via API calls and can be used on both Comfy Cloud and Local/Self-Hosted ComfyUI. Each node has its own usage cost, determined by the API provider, and we directly match their pricing.\n2. Running workflows on cloud: Exclusive to Comfy Cloud, you get a set amount of credits per month, with the amount differing based on your plan. More credits can be topped up anytime. Credits are only used up for GPU time while workflows are running — not while editing or building them. No idle costs, no setup, and no infrastructure to manage.',
|
||||
en: 'Comfy Cloud has a credit system that is used for both Partner nodes (formerly API nodes) and running workflows on cloud.\n1. Partner Nodes (Pay-as-you-go): These nodes (formerly called API nodes) run third-party models via API calls and can be used on both Comfy Cloud and Comfy Desktop. Each node has its own usage cost, determined by the API provider, and we directly match their pricing.\n2. Running workflows on cloud: Exclusive to Comfy Cloud, you get a set amount of credits per month, with the amount differing based on your plan. More credits can be topped up anytime. Credits are only used up for GPU time while workflows are running — not while editing or building them. No idle costs, no setup, and no infrastructure to manage.',
|
||||
'zh-CN':
|
||||
'Comfy Cloud 有一个积分系统,用于合作伙伴节点(原 API 节点)和在云端运行工作流。\n1. 合作伙伴节点(按需付费):这些节点(原称 API 节点)通过 API 调用运行第三方模型,可在 Comfy Cloud 和本地/自托管 ComfyUI 上使用。每个节点有其自身的使用成本,由 API 提供商决定,我们直接匹配他们的定价。\n2. 在云端运行工作流:Comfy Cloud 专属,您每月获得一定数量的积分,数量根据您的计划而不同。积分可随时充值。积分仅在工作流运行时用于 GPU 时间——编辑或构建时不消耗。无闲置成本,无需设置,无需管理基础设施。'
|
||||
'Comfy Cloud 有一个积分系统,用于合作伙伴节点(原 API 节点)和在云端运行工作流。\n1. 合作伙伴节点(按需付费):这些节点(原称 API 节点)通过 API 调用运行第三方模型,可在 Comfy Cloud 和 Comfy 桌面版上使用。每个节点有其自身的使用成本,由 API 提供商决定,我们直接匹配他们的定价。\n2. 在云端运行工作流:Comfy Cloud 专属,您每月获得一定数量的积分,数量根据您的计划而不同。积分可随时充值。积分仅在工作流运行时用于 GPU 时间——编辑或构建时不消耗。无闲置成本,无需设置,无需管理基础设施。'
|
||||
},
|
||||
'cloud.faq.13.q': {
|
||||
en: 'Can I cancel my subscription?',
|
||||
@@ -1411,9 +1411,9 @@ const translations = {
|
||||
'zh-CN': '合作伙伴节点'
|
||||
},
|
||||
'pricing.included.feature8.description': {
|
||||
en: 'Run <strong>proprietary models</strong> through Comfy\'s <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">Partner Nodes</a>, such as Nano Banana. The amount of credits each node uses depends on the model and parameters you set in the node, but these credits are the same ones that your monthly subscription comes with. These credits can also be used across Comfy Cloud and local ComfyUI. Read more about Partner nodes <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">here</a>.',
|
||||
en: 'Run <strong>proprietary models</strong> through Comfy\'s <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">Partner Nodes</a>, such as Nano Banana. The amount of credits each node uses depends on the model and parameters you set in the node, but these credits are the same ones that your monthly subscription comes with. These credits can also be used across Comfy Cloud and Comfy Desktop. Read more about Partner nodes <a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">here</a>.',
|
||||
'zh-CN':
|
||||
'通过 Comfy 的<a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">合作伙伴节点</a>运行<strong>专有模型</strong>,如 Nano Banana。每个节点消耗的积分取决于所用模型和参数设置,且与月度订阅积分通用。积分可在 Comfy Cloud 和本地 ComfyUI 间通用。了解更多关于合作伙伴节点的信息请点击<a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">此处</a>。'
|
||||
'通过 Comfy 的<a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">合作伙伴节点</a>运行<strong>专有模型</strong>,如 Nano Banana。每个节点消耗的积分取决于所用模型和参数设置,且与月度订阅积分通用。积分可在 Comfy Cloud 和 Comfy 桌面版间通用。了解更多关于合作伙伴节点的信息请点击<a href="https://docs.comfy.org/tutorials/partner-nodes/overview" class="text-primary-comfy-yellow underline">此处</a>。'
|
||||
},
|
||||
'pricing.included.feature9.title': {
|
||||
en: 'Job queue',
|
||||
|
||||
@@ -73,7 +73,7 @@ const websiteJsonLd = {
|
||||
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" sizes="48x48" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#211927" />
|
||||
|
||||
@@ -11,9 +11,9 @@ import { t } from '../i18n/translations'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Download Comfy — Run AI Locally"
|
||||
title="Download Comfy Desktop — Run AI on Your Hardware"
|
||||
description={t('download.hero.subtitle', 'en')}
|
||||
keywords={['comfyui app', 'comfyui desktop app', 'comfy ui application', 'comfyui download', 'download comfyui', 'comfyui windows', 'comfyui mac', 'comfyui linux', 'comfyui local']}
|
||||
keywords={['comfyui app', 'comfyui desktop app', 'comfyui desktop', 'comfy ui application', 'comfyui download', 'download comfyui', 'comfyui windows', 'comfyui mac', 'comfyui linux']}
|
||||
>
|
||||
<CloudBannerSection />
|
||||
<HeroSection client:load />
|
||||
|
||||
@@ -3,14 +3,10 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import HeroSection from '../components/gallery/HeroSection.vue'
|
||||
import GallerySection from '../components/gallery/GallerySection.vue'
|
||||
import ContactSection from '../components/gallery/ContactSection.vue'
|
||||
import { getVisibleGalleryByLocale } from '../content/queries'
|
||||
|
||||
const entries = await getVisibleGalleryByLocale('en')
|
||||
const items = entries.map((entry) => entry.data)
|
||||
---
|
||||
|
||||
<BaseLayout title="Gallery — Comfy">
|
||||
<HeroSection />
|
||||
<GallerySection items={items} client:load />
|
||||
<GallerySection client:load />
|
||||
<ContactSection />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -8,20 +8,14 @@ import CallToActionSection from '../components/common/CallToActionSection.vue'
|
||||
import { getRoutes } from '../config/routes'
|
||||
import { externalLinks } from '../config/routes'
|
||||
// import { learningEvents } from '../data/events'
|
||||
import { getTutorialsByLocale, slugOf } from '../content/queries'
|
||||
|
||||
const routes = getRoutes('en')
|
||||
const tutorialEntries = await getTutorialsByLocale('en')
|
||||
const tutorials = tutorialEntries.map((entry) => ({
|
||||
slug: slugOf(entry),
|
||||
...entry.data
|
||||
}))
|
||||
---
|
||||
|
||||
<BaseLayout title="Learning — Comfy">
|
||||
<HeroSection client:load />
|
||||
<FeaturedWorkflowSection client:visible />
|
||||
<TutorialsSection tutorials={tutorials} client:visible />
|
||||
<TutorialsSection client:visible />
|
||||
<CallToActionSection
|
||||
headingKey="learning.cta.heading"
|
||||
primaryLabelKey="learning.cta.contactSales"
|
||||
|
||||
@@ -3,18 +3,7 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import ModelsHeroSection from '../components/models/ModelsHeroSection.vue'
|
||||
import ModelCreationsSection from '../components/models/ModelCreationsSection.vue'
|
||||
import AIModelsSection from '../components/product/shared/AIModelsSection.vue'
|
||||
import ProductShowcaseSection from '../components/home/ProductShowcaseSection.vue'
|
||||
import { getGalleryByIds } from '../content/queries'
|
||||
|
||||
const featuredCreationSlugs = [
|
||||
'subway-swan',
|
||||
'milos-little-wonder',
|
||||
'amber-passage',
|
||||
'neon-revenant',
|
||||
'midnight-umami'
|
||||
]
|
||||
const featuredEntries = await getGalleryByIds(featuredCreationSlugs, 'en')
|
||||
const featuredItems = featuredEntries.map((entry) => entry.data)
|
||||
import ProductShowcaseSection from '../components/home/ProductShowcaseSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -27,7 +16,7 @@ const featuredItems = featuredEntries.map((entry) => entry.data)
|
||||
videoSrc="https://media.comfy.org/website/models/video_ComfdyUI_00001_.mp4"
|
||||
videoAriaLabel="Grok Imagine output created with ComfyUI"
|
||||
/>
|
||||
<ModelCreationsSection items={featuredItems} client:load />
|
||||
<ModelCreationsSection client:load />
|
||||
<AIModelsSection client:load />
|
||||
<ProductShowcaseSection client:load />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { t } from '../../i18n/translations'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="下载 — Comfy"
|
||||
title="下载 Comfy 桌面版 — 在您的硬件上运行 AI"
|
||||
description={t('download.hero.subtitle', 'zh-CN')}
|
||||
keywords={['comfyui app', 'comfyui desktop app', 'comfyui download', 'ComfyUI 下载', 'ComfyUI 桌面应用', 'ComfyUI 应用', 'ComfyUI Windows', 'ComfyUI macOS', 'ComfyUI Linux']}
|
||||
>
|
||||
|
||||
@@ -3,14 +3,10 @@ import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import HeroSection from '../../components/gallery/HeroSection.vue'
|
||||
import GallerySection from '../../components/gallery/GallerySection.vue'
|
||||
import ContactSection from '../../components/gallery/ContactSection.vue'
|
||||
import { getVisibleGalleryByLocale } from '../../content/queries'
|
||||
|
||||
const entries = await getVisibleGalleryByLocale('zh-CN')
|
||||
const items = entries.map((entry) => entry.data)
|
||||
---
|
||||
|
||||
<BaseLayout title="作品集 — Comfy">
|
||||
<HeroSection locale="zh-CN" />
|
||||
<GallerySection locale="zh-CN" items={items} client:load />
|
||||
<GallerySection locale="zh-CN" client:load />
|
||||
<ContactSection locale="zh-CN" />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -6,34 +6,15 @@ import TutorialsSection from '../../components/learning/TutorialsSection.vue'
|
||||
import CallToActionSection from '../../components/common/CallToActionSection.vue'
|
||||
import EventsSection from '../../components/common/EventsSection.vue'
|
||||
import { getRoutes, externalLinks } from '../../config/routes'
|
||||
import {
|
||||
getEventsByLocale,
|
||||
getTutorialsByLocale,
|
||||
slugOf
|
||||
} from '../../content/queries'
|
||||
import { learningEvents } from '../../data/events'
|
||||
|
||||
const routes = getRoutes('zh-CN')
|
||||
const eventEntries = await getEventsByLocale('zh-CN')
|
||||
const events = eventEntries.map((entry) => entry.data)
|
||||
const tutorialEntries = await getTutorialsByLocale('zh-CN')
|
||||
const tutorials = tutorialEntries.map((entry) => ({
|
||||
slug: slugOf(entry),
|
||||
...entry.data
|
||||
}))
|
||||
---
|
||||
|
||||
<BaseLayout title="学习 — Comfy">
|
||||
<HeroSection locale="zh-CN" client:load />
|
||||
<FeaturedWorkflowSection locale="zh-CN" client:visible />
|
||||
<TutorialsSection locale="zh-CN" tutorials={tutorials} client:visible />
|
||||
<EventsSection
|
||||
locale="zh-CN"
|
||||
headingKey="learning.events.heading"
|
||||
descriptionKey="learning.events.description"
|
||||
notifyLabelKey="learning.events.getNotified"
|
||||
events={events}
|
||||
client:visible
|
||||
/>
|
||||
<TutorialsSection locale="zh-CN" client:visible />
|
||||
<CallToActionSection
|
||||
locale="zh-CN"
|
||||
headingKey="learning.cta.heading"
|
||||
|
||||
@@ -4,17 +4,6 @@ import ModelsHeroSection from '../../components/models/ModelsHeroSection.vue'
|
||||
import ModelCreationsSection from '../../components/models/ModelCreationsSection.vue'
|
||||
import AIModelsSection from '../../components/product/shared/AIModelsSection.vue'
|
||||
import ProductShowcaseSection from '../../components/home/ProductShowcaseSection.vue'
|
||||
import { getGalleryByIds } from '../../content/queries'
|
||||
|
||||
const featuredCreationSlugs = [
|
||||
'subway-swan',
|
||||
'milos-little-wonder',
|
||||
'amber-passage',
|
||||
'neon-revenant',
|
||||
'midnight-umami'
|
||||
]
|
||||
const featuredEntries = await getGalleryByIds(featuredCreationSlugs, 'en')
|
||||
const featuredItems = featuredEntries.map((entry) => entry.data)
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -28,7 +17,7 @@ const featuredItems = featuredEntries.map((entry) => entry.data)
|
||||
videoSrc="https://media.comfy.org/website/models/video_ComfdyUI_00001_.mp4"
|
||||
videoAriaLabel="使用 ComfyUI 创建的 Grok Imagine 作品"
|
||||
/>
|
||||
<ModelCreationsSection items={featuredItems} client:load locale="zh-CN" />
|
||||
<ModelCreationsSection client:load locale="zh-CN" />
|
||||
<AIModelsSection client:load locale="zh-CN" />
|
||||
<ProductShowcaseSection client:load locale="zh-CN" />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -53,3 +53,28 @@ describe('initPostHog', () => {
|
||||
expect(result.$set_once).toHaveProperty('plan', 'free')
|
||||
})
|
||||
})
|
||||
|
||||
describe('captureDownloadClick', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
it('captures the download event with the platform', async () => {
|
||||
const { initPostHog, captureDownloadClick } = await import('./posthog')
|
||||
initPostHog()
|
||||
captureDownloadClick('mac')
|
||||
|
||||
expect(hoisted.mockCapture).toHaveBeenCalledWith(
|
||||
'website:download_button_clicked',
|
||||
{ platform: 'mac' }
|
||||
)
|
||||
})
|
||||
|
||||
it('does not capture before PostHog is initialized', async () => {
|
||||
const { captureDownloadClick } = await import('./posthog')
|
||||
captureDownloadClick('windows')
|
||||
|
||||
expect(hoisted.mockCapture).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||