mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-10 10:00:08 +00:00
Merge branch 'main' into fix/vuenodes-extension-resize-sync
This commit is contained in:
@@ -47,7 +47,7 @@ export function useWorkspaceBilling(): BillingState & BillingActions {
|
||||
tier: status.subscription_tier ?? null,
|
||||
duration: status.subscription_duration ?? null,
|
||||
planSlug: status.plan_slug ?? null,
|
||||
renewalDate: null, // Workspace billing uses cancel_at for end date
|
||||
renewalDate: status.renewal_date ?? null,
|
||||
endDate: status.cancel_at ?? null,
|
||||
isCancelled: status.subscription_status === 'canceled',
|
||||
hasFunds: status.has_funds
|
||||
|
||||
@@ -134,6 +134,25 @@ vi.mock('@/stores/executionStore', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
let jobPreviewStoreMock: {
|
||||
previewsByPromptId: Record<string, string>
|
||||
isPreviewEnabled: boolean
|
||||
}
|
||||
const ensureJobPreviewStore = () => {
|
||||
if (!jobPreviewStoreMock) {
|
||||
jobPreviewStoreMock = reactive({
|
||||
previewsByPromptId: {} as Record<string, string>,
|
||||
isPreviewEnabled: true
|
||||
})
|
||||
}
|
||||
return jobPreviewStoreMock
|
||||
}
|
||||
vi.mock('@/stores/jobPreviewStore', () => ({
|
||||
useJobPreviewStore: () => {
|
||||
return ensureJobPreviewStore()
|
||||
}
|
||||
}))
|
||||
|
||||
let workflowStoreMock: {
|
||||
activeWorkflow: null | { activeState?: { id?: string } }
|
||||
}
|
||||
@@ -186,6 +205,10 @@ const resetStores = () => {
|
||||
executionStore.activePromptId = null
|
||||
executionStore.executingNode = null
|
||||
|
||||
const jobPreviewStore = ensureJobPreviewStore()
|
||||
jobPreviewStore.previewsByPromptId = {}
|
||||
jobPreviewStore.isPreviewEnabled = true
|
||||
|
||||
const workflowStore = ensureWorkflowStore()
|
||||
workflowStore.activeWorkflow = null
|
||||
|
||||
@@ -437,6 +460,44 @@ describe('useJobList', () => {
|
||||
expect(otherJob.computeHours).toBeCloseTo(1)
|
||||
})
|
||||
|
||||
it('assigns preview urls for running jobs when previews enabled', async () => {
|
||||
queueStoreMock.runningTasks = [
|
||||
createTask({
|
||||
promptId: 'live-preview',
|
||||
queueIndex: 1,
|
||||
mockState: 'running'
|
||||
})
|
||||
]
|
||||
jobPreviewStoreMock.previewsByPromptId = {
|
||||
'live-preview': 'blob:preview-url'
|
||||
}
|
||||
jobPreviewStoreMock.isPreviewEnabled = true
|
||||
|
||||
const { jobItems } = initComposable()
|
||||
await flush()
|
||||
|
||||
expect(jobItems.value[0].iconImageUrl).toBe('blob:preview-url')
|
||||
})
|
||||
|
||||
it('omits preview urls when previews are disabled', async () => {
|
||||
queueStoreMock.runningTasks = [
|
||||
createTask({
|
||||
promptId: 'disabled-preview',
|
||||
queueIndex: 1,
|
||||
mockState: 'running'
|
||||
})
|
||||
]
|
||||
jobPreviewStoreMock.previewsByPromptId = {
|
||||
'disabled-preview': 'blob:preview-url'
|
||||
}
|
||||
jobPreviewStoreMock.isPreviewEnabled = false
|
||||
|
||||
const { jobItems } = initComposable()
|
||||
await flush()
|
||||
|
||||
expect(jobItems.value[0].iconImageUrl).toBeUndefined()
|
||||
})
|
||||
|
||||
it('derives current node name from execution store fallbacks', async () => {
|
||||
const instance = initComposable()
|
||||
await flush()
|
||||
|
||||
@@ -7,6 +7,7 @@ import { st } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useJobPreviewStore } from '@/stores/jobPreviewStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
import type { JobState } from '@/types/queue'
|
||||
@@ -96,6 +97,7 @@ export function useJobList() {
|
||||
const { t, locale } = useI18n()
|
||||
const queueStore = useQueueStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const jobPreviewStore = useJobPreviewStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const seenPendingIds = ref<Set<string>>(new Set())
|
||||
@@ -256,6 +258,11 @@ export function useJobList() {
|
||||
String(task.promptId ?? '') ===
|
||||
String(executionStore.activePromptId ?? '')
|
||||
const showAddedHint = shouldShowAddedHint(task, state)
|
||||
const promptKey = taskIdToKey(task.promptId)
|
||||
const promptPreviewUrl =
|
||||
state === 'running' && jobPreviewStore.isPreviewEnabled && promptKey
|
||||
? jobPreviewStore.previewsByPromptId[promptKey]
|
||||
: undefined
|
||||
|
||||
const display = buildJobDisplay(task, state, {
|
||||
t,
|
||||
@@ -275,7 +282,7 @@ export function useJobList() {
|
||||
meta: display.secondary,
|
||||
state,
|
||||
iconName: display.iconName,
|
||||
iconImageUrl: display.iconImageUrl,
|
||||
iconImageUrl: promptPreviewUrl ?? display.iconImageUrl,
|
||||
showClear: display.showClear,
|
||||
taskRef: task,
|
||||
progressTotalPercent:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import PricingTable from '@/platform/cloud/subscription/components/PricingTable.vue'
|
||||
@@ -15,6 +15,7 @@ const mockIsYearlySubscription = ref(false)
|
||||
const mockAccessBillingPortal = vi.fn()
|
||||
const mockReportError = vi.fn()
|
||||
const mockTrackBeginCheckout = vi.fn()
|
||||
const mockUserId = ref<string | undefined>('user-123')
|
||||
const mockGetFirebaseAuthHeader = vi.fn(() =>
|
||||
Promise.resolve({ Authorization: 'Bearer test-token' })
|
||||
)
|
||||
@@ -55,10 +56,11 @@ vi.mock('@/composables/useErrorHandling', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: () => ({
|
||||
getFirebaseAuthHeader: mockGetFirebaseAuthHeader,
|
||||
userId: 'user-123'
|
||||
}),
|
||||
useFirebaseAuthStore: () =>
|
||||
reactive({
|
||||
getFirebaseAuthHeader: mockGetFirebaseAuthHeader,
|
||||
userId: computed(() => mockUserId.value)
|
||||
}),
|
||||
FirebaseAuthStoreError: class extends Error {}
|
||||
}))
|
||||
|
||||
@@ -151,6 +153,7 @@ describe('PricingTable', () => {
|
||||
mockIsActiveSubscription.value = false
|
||||
mockSubscriptionTier.value = null
|
||||
mockIsYearlySubscription.value = false
|
||||
mockUserId.value = 'user-123'
|
||||
mockTrackBeginCheckout.mockReset()
|
||||
vi.mocked(global.fetch).mockResolvedValue({
|
||||
ok: true,
|
||||
@@ -201,6 +204,33 @@ describe('PricingTable', () => {
|
||||
expect(mockAccessBillingPortal).toHaveBeenCalledWith('pro-yearly')
|
||||
})
|
||||
|
||||
it('should use the latest userId value when it changes after mount', async () => {
|
||||
mockIsActiveSubscription.value = true
|
||||
mockSubscriptionTier.value = 'STANDARD'
|
||||
mockUserId.value = 'user-early'
|
||||
|
||||
const wrapper = createWrapper()
|
||||
await flushPromises()
|
||||
|
||||
mockUserId.value = 'user-late'
|
||||
|
||||
const creatorButton = wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('Creator'))
|
||||
|
||||
await creatorButton?.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(mockTrackBeginCheckout).toHaveBeenCalledTimes(1)
|
||||
expect(mockTrackBeginCheckout).toHaveBeenCalledWith({
|
||||
user_id: 'user-late',
|
||||
tier: 'creator',
|
||||
cycle: 'yearly',
|
||||
checkout_type: 'change',
|
||||
previous_tier: 'standard'
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call accessBillingPortal when clicking current plan', async () => {
|
||||
mockIsActiveSubscription.value = true
|
||||
mockSubscriptionTier.value = 'CREATOR'
|
||||
|
||||
@@ -243,6 +243,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Popover from 'primevue/popover'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import type { ToggleButtonPassThroughMethodOptions } from 'primevue/togglebutton'
|
||||
@@ -333,7 +334,7 @@ const tiers: PricingTierConfig[] = [
|
||||
const { isActiveSubscription, subscriptionTier, isYearlySubscription } =
|
||||
useSubscription()
|
||||
const telemetry = useTelemetry()
|
||||
const { userId } = useFirebaseAuthStore()
|
||||
const { userId } = storeToRefs(useFirebaseAuthStore())
|
||||
const { accessBillingPortal, reportError } = useFirebaseAuthActions()
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
@@ -415,9 +416,9 @@ const handleSubscribe = wrapWithErrorHandlingAsync(
|
||||
try {
|
||||
if (isActiveSubscription.value) {
|
||||
const checkoutAttribution = getCheckoutAttribution()
|
||||
if (userId) {
|
||||
if (userId.value) {
|
||||
telemetry?.trackBeginCheckout({
|
||||
user_id: userId,
|
||||
user_id: userId.value,
|
||||
tier: tierKey,
|
||||
cycle: currentBillingCycle.value,
|
||||
checkout_type: 'change',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { computed, reactive } from 'vue'
|
||||
|
||||
import { performSubscriptionCheckout } from './subscriptionCheckoutUtil'
|
||||
|
||||
@@ -15,7 +16,7 @@ const {
|
||||
mockGetAuthHeader: vi.fn(() =>
|
||||
Promise.resolve({ Authorization: 'Bearer test-token' })
|
||||
),
|
||||
mockUserId: { value: 'user-123' },
|
||||
mockUserId: { value: 'user-123' as string | undefined },
|
||||
mockIsCloud: { value: true },
|
||||
mockGetCheckoutAttribution: vi.fn(() => ({
|
||||
ga_client_id: 'ga-client-id',
|
||||
@@ -32,12 +33,12 @@ vi.mock('@/platform/telemetry', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
getFirebaseAuthHeader: mockGetAuthHeader,
|
||||
get userId() {
|
||||
return mockUserId.value
|
||||
}
|
||||
})),
|
||||
useFirebaseAuthStore: vi.fn(() =>
|
||||
reactive({
|
||||
getFirebaseAuthHeader: mockGetAuthHeader,
|
||||
userId: computed(() => mockUserId.value)
|
||||
})
|
||||
),
|
||||
FirebaseAuthStoreError: class extends Error {}
|
||||
}))
|
||||
|
||||
@@ -53,6 +54,15 @@ vi.mock('@/platform/telemetry/utils/checkoutAttribution', () => ({
|
||||
|
||||
global.fetch = vi.fn()
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve: (value: T) => void = () => {}
|
||||
const promise = new Promise<T>((res) => {
|
||||
resolve = res
|
||||
})
|
||||
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
describe('performSubscriptionCheckout', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -105,4 +115,35 @@ describe('performSubscriptionCheckout', () => {
|
||||
)
|
||||
expect(openSpy).toHaveBeenCalledWith(checkoutUrl, '_blank')
|
||||
})
|
||||
|
||||
it('uses the latest userId when it changes after checkout starts', async () => {
|
||||
const checkoutUrl = 'https://checkout.stripe.com/test'
|
||||
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
const authHeader = createDeferred<{ Authorization: string }>()
|
||||
|
||||
mockUserId.value = 'user-early'
|
||||
mockGetAuthHeader.mockImplementationOnce(() => authHeader.promise)
|
||||
vi.mocked(global.fetch).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ checkout_url: checkoutUrl })
|
||||
} as Response)
|
||||
|
||||
const checkoutPromise = performSubscriptionCheckout('pro', 'yearly', true)
|
||||
|
||||
mockUserId.value = 'user-late'
|
||||
authHeader.resolve({ Authorization: 'Bearer test-token' })
|
||||
|
||||
await checkoutPromise
|
||||
|
||||
expect(mockTelemetry.trackBeginCheckout).toHaveBeenCalledTimes(1)
|
||||
expect(mockTelemetry.trackBeginCheckout).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
user_id: 'user-late',
|
||||
tier: 'pro',
|
||||
cycle: 'yearly',
|
||||
checkout_type: 'new'
|
||||
})
|
||||
)
|
||||
expect(openSpy).toHaveBeenCalledWith(checkoutUrl, '_blank')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { getComfyApiBaseUrl } from '@/config/comfyApi'
|
||||
import { t } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
@@ -37,9 +39,10 @@ export async function performSubscriptionCheckout(
|
||||
): Promise<void> {
|
||||
if (!isCloud) return
|
||||
|
||||
const { getFirebaseAuthHeader, userId } = useFirebaseAuthStore()
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const { userId } = storeToRefs(firebaseAuthStore)
|
||||
const telemetry = useTelemetry()
|
||||
const authHeader = await getFirebaseAuthHeader()
|
||||
const authHeader = await firebaseAuthStore.getFirebaseAuthHeader()
|
||||
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
@@ -84,9 +87,9 @@ export async function performSubscriptionCheckout(
|
||||
const data = await response.json()
|
||||
|
||||
if (data.checkout_url) {
|
||||
if (userId) {
|
||||
if (userId.value) {
|
||||
telemetry?.trackBeginCheckout({
|
||||
user_id: userId,
|
||||
user_id: userId.value,
|
||||
tier: tierKey,
|
||||
cycle: currentBillingCycle,
|
||||
checkout_type: 'new',
|
||||
|
||||
@@ -211,6 +211,7 @@ export interface BillingStatusResponse {
|
||||
billing_status?: BillingStatus
|
||||
has_funds: boolean
|
||||
cancel_at?: string
|
||||
renewal_date?: string
|
||||
}
|
||||
|
||||
export interface BillingBalanceResponse {
|
||||
|
||||
@@ -61,6 +61,7 @@ import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { useJobPreviewStore } from '@/stores/jobPreviewStore'
|
||||
import { KeyComboImpl } from '@/platform/keybindings/keyCombo'
|
||||
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||
import { useModelStore } from '@/stores/modelStore'
|
||||
@@ -86,6 +87,10 @@ import {
|
||||
fixLinkInputSlots,
|
||||
isImageNode
|
||||
} from '@/utils/litegraphUtil'
|
||||
import {
|
||||
createSharedObjectUrl,
|
||||
releaseSharedObjectUrl
|
||||
} from '@/utils/objectUrlUtil'
|
||||
import {
|
||||
findLegacyRerouteNodes,
|
||||
noNativeReroutes
|
||||
@@ -701,12 +706,13 @@ export class ComfyApp {
|
||||
|
||||
api.addEventListener('b_preview_with_metadata', ({ detail }) => {
|
||||
// Enhanced preview with explicit node context
|
||||
const { blob, displayNodeId } = detail
|
||||
const { blob, displayNodeId, promptId } = detail
|
||||
const { setNodePreviewsByExecutionId, revokePreviewsByExecutionId } =
|
||||
useNodeOutputStore()
|
||||
const blobUrl = createSharedObjectUrl(blob)
|
||||
useJobPreviewStore().setPreviewUrl(promptId, blobUrl)
|
||||
// Ensure clean up if `executing` event is missed.
|
||||
revokePreviewsByExecutionId(displayNodeId)
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
// Preview cleanup is handled in progress_state event to support multiple concurrent previews
|
||||
const nodeParents = displayNodeId.split(':')
|
||||
for (let i = 1; i <= nodeParents.length; i++) {
|
||||
@@ -714,6 +720,7 @@ export class ComfyApp {
|
||||
blobUrl
|
||||
])
|
||||
}
|
||||
releaseSharedObjectUrl(blobUrl)
|
||||
})
|
||||
|
||||
api.init()
|
||||
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { useJobPreviewStore } from '@/stores/jobPreviewStore'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { forEachNode, getNodeByExecutionId } from '@/utils/graphTraversalUtil'
|
||||
@@ -454,6 +455,7 @@ export const useExecutionStore = defineStore('execution', () => {
|
||||
const map = { ...nodeProgressStatesByPrompt.value }
|
||||
delete map[promptId]
|
||||
nodeProgressStatesByPrompt.value = map
|
||||
useJobPreviewStore().clearPreview(promptId)
|
||||
}
|
||||
if (activePromptId.value) {
|
||||
delete queuedPrompts.value[activePromptId.value]
|
||||
|
||||
@@ -15,6 +15,10 @@ import { useExecutionStore } from '@/stores/executionStore'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { parseFilePath } from '@/utils/formatUtil'
|
||||
import { isVideoNode } from '@/utils/litegraphUtil'
|
||||
import {
|
||||
releaseSharedObjectUrl,
|
||||
retainSharedObjectUrl
|
||||
} from '@/utils/objectUrlUtil'
|
||||
|
||||
const PREVIEW_REVOKE_DELAY_MS = 400
|
||||
|
||||
@@ -216,10 +220,19 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
) {
|
||||
const nodeLocatorId = executionIdToNodeLocatorId(executionId)
|
||||
if (!nodeLocatorId) return
|
||||
const existingPreviews = app.nodePreviewImages[nodeLocatorId]
|
||||
if (scheduledRevoke[nodeLocatorId]) {
|
||||
scheduledRevoke[nodeLocatorId].stop()
|
||||
delete scheduledRevoke[nodeLocatorId]
|
||||
}
|
||||
if (existingPreviews?.[Symbol.iterator]) {
|
||||
for (const url of existingPreviews) {
|
||||
releaseSharedObjectUrl(url)
|
||||
}
|
||||
}
|
||||
for (const url of previewImages) {
|
||||
retainSharedObjectUrl(url)
|
||||
}
|
||||
latestPreview.value = previewImages
|
||||
app.nodePreviewImages[nodeLocatorId] = previewImages
|
||||
nodePreviewImages.value[nodeLocatorId] = previewImages
|
||||
@@ -237,10 +250,19 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
previewImages: string[]
|
||||
) {
|
||||
const nodeLocatorId = nodeIdToNodeLocatorId(nodeId)
|
||||
const existingPreviews = app.nodePreviewImages[nodeLocatorId]
|
||||
if (scheduledRevoke[nodeLocatorId]) {
|
||||
scheduledRevoke[nodeLocatorId].stop()
|
||||
delete scheduledRevoke[nodeLocatorId]
|
||||
}
|
||||
if (existingPreviews?.[Symbol.iterator]) {
|
||||
for (const url of existingPreviews) {
|
||||
releaseSharedObjectUrl(url)
|
||||
}
|
||||
}
|
||||
for (const url of previewImages) {
|
||||
retainSharedObjectUrl(url)
|
||||
}
|
||||
app.nodePreviewImages[nodeLocatorId] = previewImages
|
||||
nodePreviewImages.value[nodeLocatorId] = previewImages
|
||||
}
|
||||
@@ -270,7 +292,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
if (!previews?.[Symbol.iterator]) return
|
||||
|
||||
for (const url of previews) {
|
||||
URL.revokeObjectURL(url)
|
||||
releaseSharedObjectUrl(url)
|
||||
}
|
||||
|
||||
delete app.nodePreviewImages[nodeLocatorId]
|
||||
@@ -287,7 +309,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
if (!previews?.[Symbol.iterator]) continue
|
||||
|
||||
for (const url of previews) {
|
||||
URL.revokeObjectURL(url)
|
||||
releaseSharedObjectUrl(url)
|
||||
}
|
||||
}
|
||||
app.nodePreviewImages = {}
|
||||
@@ -326,6 +348,12 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
|
||||
// Clear preview images
|
||||
if (app.nodePreviewImages[nodeLocatorId]) {
|
||||
const previews = app.nodePreviewImages[nodeLocatorId]
|
||||
if (previews?.[Symbol.iterator]) {
|
||||
for (const url of previews) {
|
||||
releaseSharedObjectUrl(url)
|
||||
}
|
||||
}
|
||||
delete app.nodePreviewImages[nodeLocatorId]
|
||||
delete nodePreviewImages.value[nodeLocatorId]
|
||||
}
|
||||
|
||||
62
src/stores/jobPreviewStore.ts
Normal file
62
src/stores/jobPreviewStore.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, readonly, ref, watch } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import {
|
||||
releaseSharedObjectUrl,
|
||||
retainSharedObjectUrl
|
||||
} from '@/utils/objectUrlUtil'
|
||||
|
||||
type PromptPreviewMap = Record<string, string>
|
||||
|
||||
export const useJobPreviewStore = defineStore('jobPreview', () => {
|
||||
const settingStore = useSettingStore()
|
||||
const previewsByPromptId = ref<PromptPreviewMap>({})
|
||||
const readonlyPreviewsByPromptId = readonly(previewsByPromptId)
|
||||
|
||||
const previewMethod = computed(() =>
|
||||
settingStore.get('Comfy.Execution.PreviewMethod')
|
||||
)
|
||||
const isPreviewEnabled = computed(() => previewMethod.value !== 'none')
|
||||
|
||||
function setPreviewUrl(promptId: string | undefined, url: string) {
|
||||
if (!promptId || !isPreviewEnabled.value) return
|
||||
const current = previewsByPromptId.value[promptId]
|
||||
if (current === url) return
|
||||
if (current) releaseSharedObjectUrl(current)
|
||||
retainSharedObjectUrl(url)
|
||||
previewsByPromptId.value = {
|
||||
...previewsByPromptId.value,
|
||||
[promptId]: url
|
||||
}
|
||||
}
|
||||
|
||||
function clearPreview(promptId: string | undefined) {
|
||||
if (!promptId) return
|
||||
const current = previewsByPromptId.value[promptId]
|
||||
if (!current) return
|
||||
releaseSharedObjectUrl(current)
|
||||
const next = { ...previewsByPromptId.value }
|
||||
delete next[promptId]
|
||||
previewsByPromptId.value = next
|
||||
}
|
||||
|
||||
function clearAllPreviews() {
|
||||
Object.values(previewsByPromptId.value).forEach((url) => {
|
||||
releaseSharedObjectUrl(url)
|
||||
})
|
||||
previewsByPromptId.value = {}
|
||||
}
|
||||
|
||||
watch(isPreviewEnabled, (enabled) => {
|
||||
if (!enabled) clearAllPreviews()
|
||||
})
|
||||
|
||||
return {
|
||||
previewsByPromptId: readonlyPreviewsByPromptId,
|
||||
isPreviewEnabled,
|
||||
setPreviewUrl,
|
||||
clearPreview,
|
||||
clearAllPreviews
|
||||
}
|
||||
})
|
||||
27
src/utils/objectUrlUtil.ts
Normal file
27
src/utils/objectUrlUtil.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
const objectUrlRefCounts = new Map<string, number>()
|
||||
|
||||
const isBlobUrl = (url: string) => url.startsWith('blob:')
|
||||
|
||||
export function createSharedObjectUrl(blob: Blob): string {
|
||||
const url = URL.createObjectURL(blob)
|
||||
objectUrlRefCounts.set(url, 1)
|
||||
return url
|
||||
}
|
||||
|
||||
export function retainSharedObjectUrl(url: string | undefined): void {
|
||||
if (!url || !isBlobUrl(url)) return
|
||||
objectUrlRefCounts.set(url, (objectUrlRefCounts.get(url) ?? 0) + 1)
|
||||
}
|
||||
|
||||
export function releaseSharedObjectUrl(url: string | undefined): void {
|
||||
if (!url || !isBlobUrl(url)) return
|
||||
|
||||
const currentCount = objectUrlRefCounts.get(url)
|
||||
if (currentCount === undefined || currentCount <= 1) {
|
||||
objectUrlRefCounts.delete(url)
|
||||
URL.revokeObjectURL(url)
|
||||
return
|
||||
}
|
||||
|
||||
objectUrlRefCounts.set(url, currentCount - 1)
|
||||
}
|
||||
Reference in New Issue
Block a user