Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions src/components/common/UserCredit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@
<Skeleton width="8rem" height="2rem" />
</div>
<div v-else class="flex items-center gap-1">
<Tag
severity="secondary"
icon="pi pi-dollar"
rounded
class="p-1 text-amber-400"
/>
<Tag severity="secondary" rounded class="p-1 text-amber-400">
<template #icon>
<i
:class="
flags.subscriptionTiersEnabled
? 'icon-[lucide--component]'
: 'pi pi-dollar'
"
/>
</template>
</Tag>
<div :class="textClass">{{ formattedBalance }}</div>
</div>
</template>
Expand All @@ -22,6 +27,7 @@ import Skeleton from 'primevue/skeleton'
import Tag from 'primevue/tag'
import { computed } from 'vue'

import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { formatMetronomeCurrency } from '@/utils/formatUtil'

Expand All @@ -30,6 +36,7 @@ const { textClass } = defineProps<{
}>()

const authStore = useFirebaseAuthStore()
const { flags } = useFeatureFlags()
const balanceLoading = computed(() => authStore.isFetchingBalance)

const formattedBalance = computed(() => {
Expand Down
18 changes: 18 additions & 0 deletions src/components/topbar/CurrentUserPopover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@
<div v-if="isActiveSubscription" class="flex items-center justify-between">
<div class="flex flex-col gap-1">
<UserCredit text-class="text-2xl" />
<div
v-if="flags.subscriptionTiersEnabled"
class="flex items-center gap-2"
>
<i
v-tooltip="{
value: $t('credits.unified.tooltip'),
showDelay: 300,
hideDelay: 300
}"
class="icon-[lucide--circle-help] text-muted cursor-help text-xs"
/>
<span class="text-xs text-muted">{{
$t('credits.unified.message')
}}</span>
</div>
<Button
:label="$t('subscription.partnerNodesCredits')"
severity="secondary"
Expand Down Expand Up @@ -102,6 +118,7 @@ import UserCredit from '@/components/common/UserCredit.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useExternalLink } from '@/composables/useExternalLink'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { isCloud } from '@/platform/distribution/types'
Expand All @@ -123,6 +140,7 @@ const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
const authActions = useFirebaseAuthActions()
const dialogService = useDialogService()
const { isActiveSubscription, fetchStatus } = useSubscription()
const { flags } = useFeatureFlags()

const handleOpenUserSettings = () => {
dialogService.showSettingsDialog('user')
Expand Down
70 changes: 48 additions & 22 deletions src/composables/node/usePriceBadge.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LGraphBadge } from '@/lib/litegraph/src/litegraph'

import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { adjustColor } from '@/utils/colorUtil'

const componentIconSvg = new Image()
componentIconSvg.src =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='oklch(83.01%25 0.163 83.16)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0zm-13.239 0a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0zm6.619 6.619a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0zm0-13.238a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z'/%3E%3C/svg%3E"

export const usePriceBadge = () => {
const { flags } = useFeatureFlags()
function updateSubgraphCredits(node: LGraphNode) {
if (!node.isSubgraphNode()) return
node.badges = node.badges.filter((b) => !isCreditsBadge(b))
Expand Down Expand Up @@ -33,34 +39,54 @@ export const usePriceBadge = () => {
}

function isCreditsBadge(badge: LGraphBadge | (() => LGraphBadge)): boolean {
return (
(typeof badge === 'function' ? badge() : badge).icon?.unicode === '\ue96b'
)
const badgeInstance = typeof badge === 'function' ? badge() : badge
if (flags.subscriptionTiersEnabled) {
return badgeInstance.icon?.image === componentIconSvg
} else {
return badgeInstance.icon?.unicode === '\ue96b'
}
}

const colorPaletteStore = useColorPaletteStore()
function getCreditsBadge(price: string): LGraphBadge {
const isLightTheme = colorPaletteStore.completedActivePalette.light_theme
return new LGraphBadge({
text: price,
iconOptions: {
unicode: '\ue96b',
fontFamily: 'PrimeIcons',
color: isLightTheme
? adjustColor('#FABC25', { lightness: 0.5 })
: '#FABC25',

if (flags.subscriptionTiersEnabled) {
return new LGraphBadge({
text: price,
iconOptions: {
image: componentIconSvg,
size: 8
},
fgColor:
colorPaletteStore.completedActivePalette.colors.litegraph_base
.BADGE_FG_COLOR,
bgColor: isLightTheme
? adjustColor('#654020', { lightness: 0.5 })
: '#654020',
fontSize: 8
},
fgColor:
colorPaletteStore.completedActivePalette.colors.litegraph_base
.BADGE_FG_COLOR,
bgColor: isLightTheme
? adjustColor('#8D6932', { lightness: 0.5 })
: '#8D6932'
})
? adjustColor('#8D6932', { lightness: 0.5 })
: '#8D6932'
})
} else {
return new LGraphBadge({
text: price,
iconOptions: {
unicode: '\ue96b',
fontFamily: 'PrimeIcons',
color: isLightTheme
? adjustColor('#FABC25', { lightness: 0.5 })
: '#FABC25',
bgColor: isLightTheme
? adjustColor('#654020', { lightness: 0.5 })
: '#654020',
fontSize: 8
},
fgColor:
colorPaletteStore.completedActivePalette.colors.litegraph_base
.BADGE_FG_COLOR,
bgColor: isLightTheme
? adjustColor('#8D6932', { lightness: 0.5 })
: '#8D6932'
})
}
}
return {
getCreditsBadge,
Expand Down
13 changes: 12 additions & 1 deletion src/composables/useFeatureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export enum ServerFeatureFlag {
MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4',
MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled',
ASSET_UPDATE_OPTIONS_ENABLED = 'asset_update_options_enabled',
PRIVATE_MODELS_ENABLED = 'private_models_enabled'
PRIVATE_MODELS_ENABLED = 'private_models_enabled',
SUBSCRIPTION_TIERS_ENABLED = 'subscription_tiers_enabled'
}

/**
Expand Down Expand Up @@ -55,6 +56,16 @@ export function useFeatureFlags() {
remoteConfig.value.private_models_enabled ??
api.getServerFeature(ServerFeatureFlag.PRIVATE_MODELS_ENABLED, false)
)
},
get subscriptionTiersEnabled() {
// Check remote config first (from /api/features), fall back to websocket feature flags
return (
remoteConfig.value.subscription_tiers_enabled ??
api.getServerFeature(
ServerFeatureFlag.SUBSCRIPTION_TIERS_ENABLED,
false
)
)
}
})

Expand Down
11 changes: 8 additions & 3 deletions src/lib/litegraph/src/LGraphBadge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,12 @@ export class LGraphBadge {
const { font } = ctx
let iconWidth = 0
if (this.icon) {
ctx.font = `${this.icon.fontSize}px '${this.icon.fontFamily}'`
iconWidth = ctx.measureText(this.icon.unicode).width + this.padding
if (this.icon.image) {
iconWidth = this.icon.size + this.padding
} else if (this.icon.unicode) {
ctx.font = `${this.icon.fontSize}px '${this.icon.fontFamily}'`
iconWidth = ctx.measureText(this.icon.unicode).width + this.padding
}
}
ctx.font = `${this.fontSize}px sans-serif`
const textWidth = this.text ? ctx.measureText(this.text).width : 0
Expand Down Expand Up @@ -104,7 +108,8 @@ export class LGraphBadge {
// Draw icon if present
if (this.icon) {
this.icon.draw(ctx, drawX, centerY)
drawX += this.icon.fontSize + this.padding / 2 + 4
const iconWidth = this.icon.image ? this.icon.size : this.icon.fontSize
drawX += iconWidth + this.padding / 2 + 4
}

// Draw badge text
Expand Down
68 changes: 47 additions & 21 deletions src/lib/litegraph/src/LGraphIcon.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,47 @@
export interface LGraphIconOptions {
unicode: string
unicode?: string
fontFamily?: string
image?: HTMLImageElement
color?: string
bgColor?: string
fontSize?: number
size?: number
circlePadding?: number
xOffset?: number
yOffset?: number
}

export class LGraphIcon {
unicode: string
unicode?: string
fontFamily: string
image?: HTMLImageElement
color: string
bgColor?: string
fontSize: number
size: number
circlePadding: number
xOffset: number
yOffset: number

constructor({
unicode,
fontFamily = 'PrimeIcons',
image,
color = '#e6c200',
bgColor,
fontSize = 16,
size,
circlePadding = 2,
xOffset = 0,
yOffset = 0
}: LGraphIconOptions) {
this.unicode = unicode
this.fontFamily = fontFamily
this.image = image
this.color = color
this.bgColor = bgColor
this.fontSize = fontSize
this.size = size ?? fontSize
this.circlePadding = circlePadding
this.xOffset = xOffset
this.yOffset = yOffset
Expand All @@ -43,26 +51,44 @@ export class LGraphIcon {
x += this.xOffset
y += this.yOffset

const { font, textBaseline, textAlign, fillStyle } = ctx
if (this.image) {
const iconSize = this.size
const iconRadius = iconSize / 2 + this.circlePadding

ctx.font = `${this.fontSize}px '${this.fontFamily}'`
ctx.textBaseline = 'middle'
ctx.textAlign = 'center'
const iconRadius = this.fontSize / 2 + this.circlePadding
// Draw icon background circle if bgColor is set
if (this.bgColor) {
ctx.beginPath()
ctx.arc(x + iconRadius, y, iconRadius, 0, 2 * Math.PI)
ctx.fillStyle = this.bgColor
ctx.fill()
}
// Draw icon
ctx.fillStyle = this.color
ctx.fillText(this.unicode, x + iconRadius, y)
if (this.bgColor) {
const { fillStyle } = ctx
ctx.beginPath()
ctx.arc(x + iconRadius, y, iconRadius, 0, 2 * Math.PI)
ctx.fillStyle = this.bgColor
ctx.fill()
ctx.fillStyle = fillStyle
}

const imageX = x + this.circlePadding
const imageY = y - iconSize / 2
ctx.drawImage(this.image, imageX, imageY, iconSize, iconSize)
} else if (this.unicode) {
const { font, textBaseline, textAlign, fillStyle } = ctx

ctx.font = `${this.fontSize}px '${this.fontFamily}'`
ctx.textBaseline = 'middle'
ctx.textAlign = 'center'
const iconRadius = this.fontSize / 2 + this.circlePadding

ctx.font = font
ctx.textBaseline = textBaseline
ctx.textAlign = textAlign
ctx.fillStyle = fillStyle
if (this.bgColor) {
ctx.beginPath()
ctx.arc(x + iconRadius, y, iconRadius, 0, 2 * Math.PI)
ctx.fillStyle = this.bgColor
ctx.fill()
}

ctx.fillStyle = this.color
ctx.fillText(this.unicode, x + iconRadius, y)

ctx.font = font
ctx.textBaseline = textBaseline
ctx.textAlign = textAlign
ctx.fillStyle = fillStyle
}
}
}
6 changes: 5 additions & 1 deletion src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -1841,7 +1841,11 @@
"additionalInfo": "Additional Info",
"model": "Model",
"added": "Added",
"accountInitialized": "Account initialized"
"accountInitialized": "Account initialized",
"unified": {
"message": "Credits have been unified",
"tooltip": "We've unified payments across Comfy. Everything now runs on Comfy Credits:\n- Partner Nodes (formerly API nodes)\n- Cloud workflows\n\nYour existing Partner node balance has been converted into credits.\nLearn more here."
}
},
"subscription": {
"title": "Subscription",
Expand Down
1 change: 1 addition & 0 deletions src/platform/remoteConfig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ export type RemoteConfig = {
model_upload_button_enabled?: boolean
asset_update_options_enabled?: boolean
private_models_enabled?: boolean
subscription_tiers_enabled?: boolean
}
14 changes: 13 additions & 1 deletion src/renderer/extensions/vueNodes/components/NodeHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,16 @@
</IconButton>
</div>

<div v-if="isApiNode" class="icon-[lucide--dollar-sign] size-4" />
<div v-if="isSubgraphNode" class="icon-[comfy--workflow] size-4" />
<div
v-if="isApiNode"
:class="
flags.subscriptionTiersEnabled
? 'icon-[lucide--component]'
: 'icon-[lucide--dollar-sign]'
"
class="size-4"
/>

<!-- Node Title -->
<div
Expand Down Expand Up @@ -98,6 +107,7 @@ import IconButton from '@/components/button/IconButton.vue'
import EditableText from '@/components/common/EditableText.vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { st } from '@/i18n'
import { LGraphEventMode } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
Expand Down Expand Up @@ -128,6 +138,8 @@ const emit = defineEmits<{
'enter-subgraph': []
}>()

const { flags } = useFeatureFlags()

// Error boundary implementation
const renderError = ref<string | null>(null)
const { toastErrorHandler } = useErrorHandling()
Expand Down
Loading