Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f042042
feat: Add Media Assets sidebar tab for file management
viva-jinyi Oct 17, 2025
c20ea26
refactor: Apply PR #6112 review feedback for Media Assets feature
viva-jinyi Oct 20, 2025
fffdae1
chore: unexpected export
viva-jinyi Oct 20, 2025
8765a10
feat: Improve media asset display with file format tags and filename …
viva-jinyi Oct 20, 2025
52b2129
feat: Add includePublic parameter to getAssetsByTag API
viva-jinyi Oct 22, 2025
aa3354a
fix: test code
viva-jinyi Oct 22, 2025
cddd8ea
refactor: useQueueStore
viva-jinyi Oct 22, 2025
ea7e910
refactor: Apply review feedback for media assets implementation
viva-jinyi Oct 23, 2025
02a1810
Extract AssetsSidebarTab template and improve UI structure (#6164)
viva-jinyi Oct 23, 2025
5c01e61
feat: Implement centralized AssetsStore for reactive assets updates
viva-jinyi Oct 24, 2025
38885b7
refactor: Apply formatUtil code review feedback and improve type safety
viva-jinyi Oct 24, 2025
fd953c6
[automated] Update test expectations
invalid-email-address Oct 24, 2025
4e2fc4a
feat: Auto-refresh assets on file upload
viva-jinyi Oct 24, 2025
9d28ec8
fix: Add AssetsStore update trigger to WidgetSelectDropdown uploads
viva-jinyi Oct 24, 2025
cb33c8f
refactor:
viva-jinyi Oct 27, 2025
9125459
fix: Prevent gallery index shift when new outputs are generated
viva-jinyi Oct 27, 2025
993f08f
refactor: delete unused export
viva-jinyi Oct 27, 2025
e0a0d9f
refactor: Simplify asset ID handling and remove UUID extraction, Acce…
viva-jinyi Oct 27, 2025
b778cde
feat: implement asset deletion functionality (#6203)
viva-jinyi Oct 27, 2025
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
Prev Previous commit
Next Next commit
feat: Implement centralized AssetsStore for reactive assets updates
- Create AssetsStore following QueueStore pattern for history-based assets
- Use useAsyncState for async state management (loading/error handling)
- Support both cloud and local environments (via isCloud flag)
- Auto-update history assets on status events in GraphView
- Refactor useMediaAssets composables to use AssetsStore
  • Loading branch information
viva-jinyi committed Oct 29, 2025
commit 5c01e61c9d9d967215f28f4e658737df2e2d6b1a
75 changes: 22 additions & 53 deletions src/platform/assets/composables/useMediaAssets/useAssetsApi.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,39 @@
import { useAsyncState } from '@vueuse/core'
import { computed } from 'vue'

import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { assetService } from '@/platform/assets/services/assetService'
import { useQueueStore } from '@/stores/queueStore'

import { mapTaskOutputToAssetItem } from './assetMappers'

/**
* Fetch input assets from cloud service
*/
async function fetchInputAssets(directory: string): Promise<AssetItem[]> {
const assets = await assetService.getAssetsByTag(directory, false)
return assets
}
import { useAssetsStore } from '@/stores/assetsStore'

/**
* Fetch output assets from queue store
* Composable for fetching media assets from cloud environment
* Uses AssetsStore for centralized state management
*/
function fetchOutputAssets(): AssetItem[] {
const queueStore = useQueueStore()

const assetItems: AssetItem[] = queueStore.tasks
.filter((task) => task.previewOutput && task.displayStatus === 'Completed')
.map((task) => {
const output = task.previewOutput!
const assetItem = mapTaskOutputToAssetItem(task, output)
export function useAssetsApi(directory: 'input' | 'output') {
const assetsStore = useAssetsStore()

// Add output count and all outputs for folder view
assetItem.user_metadata = {
...assetItem.user_metadata,
outputCount: task.flatOutputs.filter((o) => o.supportsPreview).length,
allOutputs: task.flatOutputs.filter((o) => o.supportsPreview)
}
const media = computed(() =>
directory === 'input' ? assetsStore.inputAssets : assetsStore.historyAssets
)

return assetItem
})
const loading = computed(() =>
directory === 'input'
? assetsStore.inputLoading
: assetsStore.historyLoading
)

return assetItems
}
const error = computed(() =>
directory === 'input' ? assetsStore.inputError : assetsStore.historyError
)

/**
* Composable for fetching media assets from cloud environment
* Creates an independent instance for each directory
*/
export function useAssetsApi(directory: 'input' | 'output') {
const fetchAssets = async (): Promise<AssetItem[]> => {
const fetchMediaList = async (): Promise<AssetItem[]> => {
if (directory === 'input') {
return fetchInputAssets(directory)
await assetsStore.updateInputs()
return assetsStore.inputAssets
} else {
return fetchOutputAssets()
await assetsStore.updateHistory()
return assetsStore.historyAssets
}
}

const {
state: media,
isLoading: loading,
error,
execute: fetchMediaList
} = useAsyncState(fetchAssets, [], {
immediate: false,
resetOnExecute: false,
onError: (err) => {
console.error(`Error fetching ${directory} cloud assets:`, err)
}
})

const refresh = () => fetchMediaList()

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,90 +1,39 @@
import { useAsyncState } from '@vueuse/core'
import { computed } from 'vue'

import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { api } from '@/scripts/api'
import { useQueueStore } from '@/stores/queueStore'

import {
mapInputFileToAssetItem,
mapTaskOutputToAssetItem
} from './assetMappers'
import { useAssetsStore } from '@/stores/assetsStore'

/**
* Fetch input directory files from the internal API
* Composable for fetching media assets from local environment
* Uses AssetsStore for centralized state management
*/
async function fetchInputFiles(directory: string): Promise<AssetItem[]> {
const response = await fetch(api.internalURL(`/files/${directory}`), {
headers: {
'Comfy-User': api.user
}
})

if (!response.ok) {
throw new Error(`Failed to fetch ${directory} files`)
}
export function useInternalFilesApi(directory: 'input' | 'output') {
const assetsStore = useAssetsStore()

const filenames: string[] = await response.json()
return filenames.map((name, index) =>
mapInputFileToAssetItem(name, index, directory as 'input')
const media = computed(() =>
directory === 'input' ? assetsStore.inputAssets : assetsStore.historyAssets
)
}

/**
* Fetch output files from the queue store
*/
function fetchOutputFiles(): AssetItem[] {
const queueStore = useQueueStore()

// Use tasks (already grouped by promptId) instead of flatTasks
const assetItems: AssetItem[] = queueStore.tasks
.filter((task) => task.previewOutput && task.displayStatus === 'Completed')
.map((task) => {
const output = task.previewOutput!
const assetItem = mapTaskOutputToAssetItem(task, output)

// Add output count and all outputs for folder view
assetItem.user_metadata = {
...assetItem.user_metadata,
outputCount: task.flatOutputs.filter((o) => o.supportsPreview).length,
allOutputs: task.flatOutputs.filter((o) => o.supportsPreview)
}

return assetItem
})
const loading = computed(() =>
directory === 'input'
? assetsStore.inputLoading
: assetsStore.historyLoading
)

// Sort by creation date (newest first)
return assetItems.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
const error = computed(() =>
directory === 'input' ? assetsStore.inputError : assetsStore.historyError
)
}

/**
* Composable for fetching media assets from local environment
* Creates an independent instance for each directory
*/
export function useInternalFilesApi(directory: 'input' | 'output') {
const fetchAssets = async (): Promise<AssetItem[]> => {
const fetchMediaList = async (): Promise<AssetItem[]> => {
if (directory === 'input') {
return fetchInputFiles(directory)
await assetsStore.updateInputs()
return assetsStore.inputAssets
} else {
return fetchOutputFiles()
await assetsStore.updateHistory()
return assetsStore.historyAssets
}
}

const {
state: media,
isLoading: loading,
error,
execute: fetchMediaList
} = useAsyncState(fetchAssets, [], {
immediate: false,
resetOnExecute: false,
onError: (err) => {
console.error(`Error fetching ${directory} assets:`, err)
}
})

const refresh = () => fetchMediaList()

return {
Expand Down
142 changes: 142 additions & 0 deletions src/stores/assetsStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { useAsyncState } from '@vueuse/core'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follow-up: This should go in /platform/assets

import { defineStore } from 'pinia'
import { computed } from 'vue'

import {
mapInputFileToAssetItem,
mapTaskOutputToAssetItem
} from '@/platform/assets/composables/useMediaAssets/assetMappers'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { assetService } from '@/platform/assets/services/assetService'
import { isCloud } from '@/platform/distribution/types'
import { api } from '@/scripts/api'

import { TaskItemImpl } from './queueStore'

/**
* Fetch input files from the internal API (OSS version)
*/
async function fetchInputFilesFromAPI(): Promise<AssetItem[]> {
const response = await fetch(api.internalURL('/files/input'), {
headers: {
'Comfy-User': api.user
}
})

if (!response.ok) {
throw new Error('Failed to fetch input files')
}

const filenames: string[] = await response.json()
return filenames.map((name, index) =>
mapInputFileToAssetItem(name, index, 'input')
)
}

/**
* Fetch input files from cloud service
*/
async function fetchInputFilesFromCloud(): Promise<AssetItem[]> {
return await assetService.getAssetsByTag('input', false)
}

/**
* Convert history task items to asset items
*/
function mapHistoryToAssets(historyItems: any[]): AssetItem[] {
const assetItems: AssetItem[] = []

for (const item of historyItems) {
if (!item.outputs || !item.status || item.status?.status_str === 'error') {
continue
}

const task = new TaskItemImpl(
'History',
item.prompt,
item.status,
item.outputs
)

if (!task.previewOutput) {
continue
}

const assetItem = mapTaskOutputToAssetItem(task, task.previewOutput)

const supportedOutputs = task.flatOutputs.filter((o) => o.supportsPreview)
assetItem.user_metadata = {
...assetItem.user_metadata,
outputCount: supportedOutputs.length,
allOutputs: supportedOutputs
}

assetItems.push(assetItem)
}

return assetItems.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
)
}

export const useAssetsStore = defineStore('assets', () => {
const maxHistoryItems = 200

const fetchInputFiles = isCloud
? fetchInputFilesFromCloud
: fetchInputFilesFromAPI

const {
state: inputAssets,
isLoading: inputLoading,
error: inputError,
execute: updateInputs
} = useAsyncState(fetchInputFiles, [], {
immediate: false,
resetOnExecute: false,
onError: (err) => {
console.error('Error fetching input assets:', err)
}
})

const fetchHistoryAssets = async (): Promise<AssetItem[]> => {
const history = await api.getHistory(maxHistoryItems)
return mapHistoryToAssets(history.History)
}

const {
state: historyAssets,
isLoading: historyLoading,
error: historyError,
execute: updateHistory
} = useAsyncState(fetchHistoryAssets, [], {
immediate: false,
resetOnExecute: false,
onError: (err) => {
console.error('Error fetching history assets:', err)
}
})

const isLoading = computed(() => inputLoading.value || historyLoading.value)

const update = async () => {
await Promise.all([updateInputs(), updateHistory()])
}

return {
// States
inputAssets,
historyAssets,
inputLoading,
historyLoading,
inputError,
historyError,
isLoading,

// Actions
updateInputs,
updateHistory,
update
}
})
12 changes: 10 additions & 2 deletions src/views/GraphView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { setupAutoQueueHandler } from '@/services/autoQueueService'
import { useKeybindingService } from '@/services/keybindingService'
import { useAssetsStore } from '@/stores/assetsStore'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
Expand All @@ -80,6 +81,7 @@ const settingStore = useSettingStore()
const executionStore = useExecutionStore()
const colorPaletteStore = useColorPaletteStore()
const queueStore = useQueueStore()
const assetsStore = useAssetsStore()
const versionCompatibilityStore = useVersionCompatibilityStore()
const graphCanvasContainerRef = ref<HTMLDivElement | null>(null)

Expand Down Expand Up @@ -188,11 +190,17 @@ const init = () => {
const queuePendingTaskCountStore = useQueuePendingTaskCountStore()
const onStatus = async (e: CustomEvent<StatusWsMessageStatus>) => {
queuePendingTaskCountStore.update(e)
await queueStore.update()
await Promise.all([
queueStore.update(),
assetsStore.updateHistory() // Update history assets when status changes
])
}

const onExecutionSuccess = async () => {
await queueStore.update()
await Promise.all([
queueStore.update(),
assetsStore.updateHistory() // Update history assets on execution success
])
}

const reconnectingMessage: ToastMessageOptions = {
Expand Down