mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 14:27:40 +00:00
[feat] Add cloud notification modal for macOS desktop users
Adds a one-time informational modal that introduces Comfy Cloud to macOS desktop users. The modal emphasizes that ComfyUI remains free and open source, with Cloud as an optional service for GPU access. Key features: - Shows once on first launch for macOS + Electron users - Persistent badge in topbar after dismissal for easy re-access - Clean design with Comfy Cloud branding - Non-intrusive messaging focused on infrastructure benefits 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
15
src/App.vue
15
src/App.vue
@@ -17,6 +17,7 @@ import { computed, onMounted } from 'vue'
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import config from '@/config'
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
@@ -47,7 +48,7 @@ const showContextMenu = (event: MouseEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
window['__COMFYUI_FRONTEND_VERSION__'] = config.app_version
|
||||
|
||||
if (isElectron()) {
|
||||
@@ -77,5 +78,17 @@ onMounted(() => {
|
||||
// Initialize conflict detection in background
|
||||
// This runs async and doesn't block UI setup
|
||||
void conflictDetection.initializeConflictDetection()
|
||||
|
||||
// Show cloud notification for macOS desktop users (one-time)
|
||||
const isMacOS = navigator.platform.toLowerCase().includes('mac')
|
||||
const settingStore = useSettingStore()
|
||||
const hasShownNotification = settingStore.get(
|
||||
'Comfy.Desktop.CloudNotificationShown'
|
||||
)
|
||||
|
||||
if (isElectron() && isMacOS && !hasShownNotification) {
|
||||
dialogService.showCloudNotification()
|
||||
await settingStore.set('Comfy.Desktop.CloudNotificationShown', true)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
104
src/components/dialog/content/CloudNotificationContent.vue
Normal file
104
src/components/dialog/content/CloudNotificationContent.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="w-[480px] p-6">
|
||||
<!-- Header with Logo -->
|
||||
<div class="mb-6">
|
||||
<div class="mb-2 flex items-center gap-3">
|
||||
<img
|
||||
src="/assets/images/comfy-cloud-logo.svg"
|
||||
alt="Comfy Cloud"
|
||||
class="h-8 w-8 shrink-0"
|
||||
/>
|
||||
<h1 class="text-2xl font-semibold">
|
||||
{{ t('cloudNotification.title') }}
|
||||
</h1>
|
||||
</div>
|
||||
<p class="text-base text-muted">
|
||||
{{ t('cloudNotification.message') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
<div class="mb-6 space-y-4">
|
||||
<div class="flex gap-3">
|
||||
<i class="pi pi-check-circle mt-0.5 shrink-0 text-xl text-blue-500"></i>
|
||||
<div class="flex-1">
|
||||
<div class="mb-1 font-medium">
|
||||
{{ t('cloudNotification.feature1Title') }}
|
||||
</div>
|
||||
<div class="text-sm text-muted">
|
||||
{{ t('cloudNotification.feature1') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<i class="pi pi-server mt-0.5 shrink-0 text-xl text-blue-500"></i>
|
||||
<div class="flex-1">
|
||||
<div class="mb-1 font-medium">
|
||||
{{ t('cloudNotification.feature2Title') }}
|
||||
</div>
|
||||
<div class="text-sm text-muted">
|
||||
{{ t('cloudNotification.feature2') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<i class="pi pi-tag mt-0.5 shrink-0 text-xl text-blue-500"></i>
|
||||
<div class="flex-1">
|
||||
<div class="mb-1 font-medium">
|
||||
{{ t('cloudNotification.feature3Title') }}
|
||||
</div>
|
||||
<div class="text-sm text-muted">
|
||||
{{ t('cloudNotification.feature3') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Note -->
|
||||
<div
|
||||
class="mb-6 rounded border-l-2 border-blue-500 bg-blue-500/5 py-2.5 pl-3 pr-4"
|
||||
>
|
||||
<p class="whitespace-pre-line text-sm text-muted">
|
||||
{{ t('cloudNotification.feature4') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3">
|
||||
<Button
|
||||
:label="t('cloudNotification.continueLocally')"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="flex-1"
|
||||
@click="onDismiss"
|
||||
/>
|
||||
<Button
|
||||
:label="t('cloudNotification.exploreCloud')"
|
||||
icon="pi pi-arrow-right"
|
||||
icon-pos="right"
|
||||
class="flex-1"
|
||||
@click="onExplore"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const onDismiss = () => {
|
||||
useDialogStore().closeDialog()
|
||||
}
|
||||
|
||||
const onExplore = () => {
|
||||
window.open('https://www.comfy.org/cloud', '_blank')
|
||||
useDialogStore().closeDialog()
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,22 @@
|
||||
<template>
|
||||
<div class="flex h-full shrink-0 items-center">
|
||||
<!-- Cloud Notification Badge for Desktop -->
|
||||
<div
|
||||
v-if="cloudBadge"
|
||||
class="relative inline-flex h-full shrink-0 cursor-pointer items-center justify-center gap-2 px-3 transition-opacity hover:opacity-70"
|
||||
@click="handleCloudBadgeClick"
|
||||
>
|
||||
<div
|
||||
class="rounded-full bg-white px-1.5 py-0.5 text-xxxs font-semibold text-black"
|
||||
>
|
||||
{{ t('cloudNotification.badgeLabel') }}
|
||||
</div>
|
||||
<div v-if="displayMode !== 'icon-only'" class="text-sm font-inter">
|
||||
{{ t('cloudNotification.badgeText') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Extension Badges -->
|
||||
<TopbarBadge
|
||||
v-for="badge in topbarBadgeStore.badges"
|
||||
:key="badge.text"
|
||||
@@ -14,8 +31,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useTopbarBadgeStore } from '@/stores/topbarBadgeStore'
|
||||
import type { TopbarBadge as TopbarBadgeType } from '@/types/comfy'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
import TopbarBadge from './TopbarBadge.vue'
|
||||
|
||||
@@ -41,4 +63,35 @@ const displayMode = computed<'full' | 'compact' | 'icon-only'>(() => {
|
||||
})
|
||||
|
||||
const topbarBadgeStore = useTopbarBadgeStore()
|
||||
|
||||
// Cloud notification badge
|
||||
const { t } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
const isMacOS = computed(() => navigator.platform.toLowerCase().includes('mac'))
|
||||
|
||||
const hasShownNotification = computed(() =>
|
||||
settingStore.get('Comfy.Desktop.CloudNotificationShown')
|
||||
)
|
||||
|
||||
const shouldShowCloudBadge = computed(
|
||||
() => isElectron() && isMacOS.value && hasShownNotification.value
|
||||
)
|
||||
|
||||
const cloudBadge = computed<TopbarBadgeType | null>(() => {
|
||||
if (!shouldShowCloudBadge.value) return null
|
||||
|
||||
return {
|
||||
text: 'Discover Comfy Cloud',
|
||||
label: 'NEW',
|
||||
icon: 'pi pi-cloud',
|
||||
variant: 'info',
|
||||
tooltip: 'Learn about Comfy Cloud'
|
||||
}
|
||||
})
|
||||
|
||||
const handleCloudBadgeClick = () => {
|
||||
dialogService.showCloudNotification()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2212,5 +2212,21 @@
|
||||
"description": "This workflow uses custom nodes you haven't installed yet.",
|
||||
"replacementInstruction": "Install these nodes to run this workflow, or replace them with installed alternatives. Missing nodes are highlighted in red on the canvas."
|
||||
}
|
||||
},
|
||||
"cloudNotification": {
|
||||
"title": "Discover Comfy Cloud",
|
||||
"message": "Get access to industry-grade GPUs and run workflows up to 10x faster",
|
||||
"feature1Title": "No Setup Required",
|
||||
"feature1": "Start creating instantly with popular models pre-installed",
|
||||
"feature2Title": "Powerful GPUs",
|
||||
"feature2": "A100 and RTX PRO 6000 GPUs for heavy video models",
|
||||
"feature3Title": "$20/month",
|
||||
"feature3": "Simple subscription with unlimited workflow runs",
|
||||
"feature4": "ComfyUI stays free and open source.\nCloud is optional—for instant access to high-end GPUs.",
|
||||
"continueLocally": "Continue Locally",
|
||||
"exploreCloud": "Explore Cloud",
|
||||
"badgeTooltip": "Learn about Comfy Cloud",
|
||||
"badgeLabel": "NEW",
|
||||
"badgeText": "Discover Comfy Cloud"
|
||||
}
|
||||
}
|
||||
@@ -293,6 +293,12 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
type: 'boolean',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Desktop.CloudNotificationShown',
|
||||
name: 'Cloud notification shown',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.ZoomSpeed',
|
||||
category: ['LiteGraph', 'Canvas', 'ZoomSpeed'],
|
||||
|
||||
@@ -374,6 +374,7 @@ const zSettings = z.object({
|
||||
'Comfy.Workflow.ShowMissingNodesWarning': z.boolean(),
|
||||
'Comfy.Workflow.ShowMissingModelsWarning': z.boolean(),
|
||||
'Comfy.Workflow.WarnBlueprintOverwrite': z.boolean(),
|
||||
'Comfy.Desktop.CloudNotificationShown': z.boolean(),
|
||||
'Comfy.DisableFloatRounding': z.boolean(),
|
||||
'Comfy.DisableSliders': z.boolean(),
|
||||
'Comfy.DOMClippingEnabled': z.boolean(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { merge } from 'es-toolkit/compat'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
|
||||
import CloudNotificationContent from '@/components/dialog/content/CloudNotificationContent.vue'
|
||||
import MissingNodesContent from '@/components/dialog/content/MissingNodesContent.vue'
|
||||
import MissingNodesFooter from '@/components/dialog/content/MissingNodesFooter.vue'
|
||||
import MissingNodesHeader from '@/components/dialog/content/MissingNodesHeader.vue'
|
||||
@@ -541,6 +542,16 @@ export const useDialogService = () => {
|
||||
show()
|
||||
}
|
||||
|
||||
function showCloudNotification() {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-cloud-notification',
|
||||
component: CloudNotificationContent,
|
||||
dialogComponentProps: {
|
||||
closable: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
showLoadWorkflowWarning,
|
||||
showMissingModelsWarning,
|
||||
@@ -555,6 +566,7 @@ export const useDialogService = () => {
|
||||
showTopUpCreditsDialog,
|
||||
showUpdatePasswordDialog,
|
||||
showExtensionDialog,
|
||||
showCloudNotification,
|
||||
prompt,
|
||||
showErrorDialog,
|
||||
confirm,
|
||||
|
||||
Reference in New Issue
Block a user