Skip to content

Commit 839e7c1

Browse files
committed
feature: (WIP) useMediaAssetAction added
1 parent b347dd1 commit 839e7c1

File tree

3 files changed

+252
-26
lines changed

3 files changed

+252
-26
lines changed

src/platform/assets/components/MediaAssetCard.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
:context="{ type: assetType }"
3838
@view="handleZoomClick"
3939
@download="actions.downloadAsset()"
40-
@play="actions.playAsset(asset.id)"
4140
@video-playing-state-changed="isVideoPlaying = $event"
4241
@video-controls-changed="showVideoControls = $event"
4342
@image-loaded="handleImageLoaded"

src/platform/assets/components/MediaAssetMoreMenu.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ const handleInspect = () => {
129129
130130
const handleAddToWorkflow = () => {
131131
if (asset.value) {
132-
actions.addWorkflow(asset.value.id)
132+
actions.addWorkflow()
133133
}
134134
close()
135135
}
@@ -143,14 +143,14 @@ const handleDownload = () => {
143143
144144
const handleOpenWorkflow = () => {
145145
if (asset.value) {
146-
actions.openWorkflow(asset.value.id)
146+
actions.openWorkflow()
147147
}
148148
close()
149149
}
150150
151151
const handleExportWorkflow = () => {
152152
if (asset.value) {
153-
actions.exportWorkflow(asset.value.id)
153+
actions.exportWorkflow()
154154
}
155155
close()
156156
}

src/platform/assets/composables/useMediaAssetActions.ts

Lines changed: 249 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,43 @@
1-
/* eslint-disable no-console */
21
import { useToast } from 'primevue/usetoast'
32
import { inject } from 'vue'
43

54
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
65
import { downloadFile } from '@/base/common/downloadUtil'
6+
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
77
import { t } from '@/i18n'
88
import { isCloud } from '@/platform/distribution/types'
9+
import { useSettingStore } from '@/platform/settings/settingStore'
10+
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
11+
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
12+
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
913
import { api } from '@/scripts/api'
14+
import { getWorkflowDataFromFile } from '@/scripts/metadata/parser'
15+
import { downloadBlob } from '@/scripts/utils'
16+
import { useDialogService } from '@/services/dialogService'
17+
import { useLitegraphService } from '@/services/litegraphService'
18+
import { useNodeDefStore } from '@/stores/nodeDefStore'
1019
import { getOutputAssetMetadata } from '../schemas/assetMetadataSchema'
1120
import { useAssetsStore } from '@/stores/assetsStore'
1221
import { useDialogStore } from '@/stores/dialogStore'
22+
import { createAnnotatedPath } from '@/utils/createAnnotatedPath'
23+
import { appendJsonExt } from '@/utils/formatUtil'
1324

1425
import type { AssetItem } from '../schemas/assetSchema'
1526
import { MediaAssetKey } from '../schemas/mediaAssetSchema'
1627
import { assetService } from '../services/assetService'
28+
import type { ResultItemType } from '@/schemas/apiSchema'
1729

1830
export function useMediaAssetActions() {
1931
const toast = useToast()
2032
const dialogStore = useDialogStore()
2133
const mediaContext = inject(MediaAssetKey, null)
34+
const { copyToClipboard } = useCopyToClipboard()
35+
const workflowStore = useWorkflowStore()
36+
const workflowService = useWorkflowService()
37+
const litegraphService = useLitegraphService()
38+
const nodeDefStore = useNodeDefStore()
39+
const settingStore = useSettingStore()
40+
const dialogService = useDialogService()
2241

2342
const downloadAsset = () => {
2443
const asset = mediaContext?.asset.value
@@ -179,10 +198,6 @@ export function useMediaAssetActions() {
179198
}
180199
}
181200

182-
const playAsset = (assetId: string) => {
183-
console.log('Playing asset:', assetId)
184-
}
185-
186201
const copyJobId = async () => {
187202
const asset = mediaContext?.asset.value
188203
if (!asset) return
@@ -201,38 +216,252 @@ export function useMediaAssetActions() {
201216
return
202217
}
203218

219+
await copyToClipboard(promptId)
220+
}
221+
222+
/**
223+
* Helper function to get workflow data from asset
224+
* Tries to get workflow from metadata first, then falls back to extracting from file
225+
*/
226+
const getWorkflowFromAsset = async (
227+
asset: AssetItem
228+
): Promise<ComfyWorkflowJSON | null> => {
229+
// First try to get workflow from metadata (for output assets)
230+
const metadata = getOutputAssetMetadata(asset.user_metadata)
231+
if (metadata?.workflow) {
232+
return metadata.workflow as ComfyWorkflowJSON
233+
}
234+
235+
// For input assets or assets with embedded workflow, try to extract from file
236+
// Fetch the file and extract workflow metadata
237+
try {
238+
const assetType = asset.tags?.[0] || 'output'
239+
const fileUrl = api.apiURL(
240+
`/view?filename=${encodeURIComponent(asset.name)}&type=${assetType}`
241+
)
242+
const response = await fetch(fileUrl)
243+
if (!response.ok) {
244+
return null
245+
}
246+
247+
const blob = await response.blob()
248+
const file = new File([blob], asset.name, { type: blob.type })
249+
250+
const workflowData = await getWorkflowDataFromFile(file)
251+
if (workflowData?.workflow) {
252+
// Handle both string and object workflow data
253+
if (typeof workflowData.workflow === 'string') {
254+
return JSON.parse(workflowData.workflow) as ComfyWorkflowJSON
255+
}
256+
return workflowData.workflow as ComfyWorkflowJSON
257+
}
258+
} catch (error) {
259+
console.error('Failed to extract workflow from file:', error)
260+
}
261+
262+
return null
263+
}
264+
265+
/**
266+
* Add a loader node to the current workflow for this asset
267+
* Similar to useJobMenu's addOutputLoaderNode
268+
*/
269+
const addWorkflow = async () => {
270+
const asset = mediaContext?.asset.value
271+
if (!asset) return
272+
273+
// Determine the appropriate loader node type based on file extension
274+
const filename = asset.name.toLowerCase()
275+
let nodeType: 'LoadImage' | 'LoadVideo' | 'LoadAudio' | null = null
276+
let widgetName: 'image' | 'file' | 'audio' | null = null
277+
278+
if (
279+
filename.endsWith('.png') ||
280+
filename.endsWith('.jpg') ||
281+
filename.endsWith('.jpeg') ||
282+
filename.endsWith('.webp') ||
283+
filename.endsWith('.gif')
284+
) {
285+
nodeType = 'LoadImage'
286+
widgetName = 'image'
287+
} else if (
288+
filename.endsWith('.mp4') ||
289+
filename.endsWith('.webm') ||
290+
filename.endsWith('.mov')
291+
) {
292+
nodeType = 'LoadVideo'
293+
widgetName = 'file'
294+
} else if (
295+
filename.endsWith('.mp3') ||
296+
filename.endsWith('.wav') ||
297+
filename.endsWith('.ogg') ||
298+
filename.endsWith('.flac')
299+
) {
300+
nodeType = 'LoadAudio'
301+
widgetName = 'audio'
302+
}
303+
304+
if (!nodeType || !widgetName) {
305+
toast.add({
306+
severity: 'warn',
307+
summary: t('g.warning'),
308+
detail: 'Unsupported file type for loader node',
309+
life: 2000
310+
})
311+
return
312+
}
313+
314+
const nodeDef = nodeDefStore.nodeDefsByName[nodeType]
315+
if (!nodeDef) {
316+
toast.add({
317+
severity: 'error',
318+
summary: t('g.error'),
319+
detail: `Node type ${nodeType} not found`,
320+
life: 3000
321+
})
322+
return
323+
}
324+
325+
const node = litegraphService.addNodeOnGraph(nodeDef, {
326+
pos: litegraphService.getCanvasCenter()
327+
})
328+
329+
if (!node) {
330+
toast.add({
331+
severity: 'error',
332+
summary: t('g.error'),
333+
detail: 'Failed to create node',
334+
life: 3000
335+
})
336+
return
337+
}
338+
339+
// Get metadata to construct the annotated path
340+
const metadata = getOutputAssetMetadata(asset.user_metadata)
341+
const assetType = asset.tags?.[0] || 'input'
342+
343+
const isResultItemType = (v: string | undefined): v is ResultItemType =>
344+
v === 'input' || v === 'output' || v === 'temp'
345+
346+
// Create annotated path for the asset
347+
const annotated = createAnnotatedPath(
348+
{
349+
filename: asset.name,
350+
subfolder: metadata?.subfolder || '',
351+
type: isResultItemType(assetType) ? assetType : undefined
352+
},
353+
{
354+
rootFolder: isResultItemType(assetType) ? assetType : undefined
355+
}
356+
)
357+
358+
const widget = node.widgets?.find((w) => w.name === widgetName)
359+
if (widget) {
360+
widget.value = annotated
361+
widget.callback?.(annotated)
362+
}
363+
node.graph?.setDirtyCanvas(true, true)
364+
365+
toast.add({
366+
severity: 'success',
367+
summary: t('g.success'),
368+
detail: `${nodeType} node added to workflow`,
369+
life: 2000
370+
})
371+
}
372+
373+
/**
374+
* Open the workflow from this asset in a new tab
375+
* Similar to useJobMenu's openJobWorkflow
376+
*/
377+
const openWorkflow = async () => {
378+
const asset = mediaContext?.asset.value
379+
if (!asset) return
380+
381+
const workflow = await getWorkflowFromAsset(asset)
382+
if (!workflow) {
383+
toast.add({
384+
severity: 'warn',
385+
summary: t('g.warning'),
386+
detail: 'No workflow data found in this asset',
387+
life: 2000
388+
})
389+
return
390+
}
391+
204392
try {
205-
await navigator.clipboard.writeText(promptId)
393+
const filename = `${asset.name.replace(/\.[^/.]+$/, '')}.json`
394+
const temp = workflowStore.createTemporary(filename, workflow)
395+
await workflowService.openWorkflow(temp)
396+
206397
toast.add({
207398
severity: 'success',
208399
summary: t('g.success'),
209-
detail: t('mediaAsset.jobIdToast.jobIdCopied'),
400+
detail: 'Workflow opened in new tab',
210401
life: 2000
211402
})
212403
} catch (error) {
213404
toast.add({
214405
severity: 'error',
215406
summary: t('g.error'),
216-
detail: t('mediaAsset.jobIdToast.jobIdCopyFailed'),
407+
detail:
408+
error instanceof Error ? error.message : 'Failed to open workflow',
217409
life: 3000
218410
})
219411
}
220412
}
221413

222-
const addWorkflow = (assetId: string) => {
223-
console.log('Adding asset to workflow:', assetId)
224-
}
414+
/**
415+
* Export the workflow from this asset as a JSON file
416+
* Similar to useJobMenu's exportJobWorkflow
417+
*/
418+
const exportWorkflow = async () => {
419+
const asset = mediaContext?.asset.value
420+
if (!asset) return
225421

226-
const openWorkflow = (assetId: string) => {
227-
console.log('Opening workflow for asset:', assetId)
228-
}
422+
const workflow = await getWorkflowFromAsset(asset)
423+
if (!workflow) {
424+
toast.add({
425+
severity: 'warn',
426+
summary: t('g.warning'),
427+
detail: 'No workflow data found in this asset',
428+
life: 2000
429+
})
430+
return
431+
}
229432

230-
const exportWorkflow = (assetId: string) => {
231-
console.log('Exporting workflow for asset:', assetId)
232-
}
433+
try {
434+
let filename = `${asset.name.replace(/\.[^/.]+$/, '')}.json`
435+
436+
if (settingStore.get('Comfy.PromptFilename')) {
437+
const input = await dialogService.prompt({
438+
title: t('workflowService.exportWorkflow'),
439+
message: t('workflowService.enterFilename') + ':',
440+
defaultValue: filename
441+
})
442+
if (!input) return
443+
filename = appendJsonExt(input)
444+
}
445+
446+
const json = JSON.stringify(workflow, null, 2)
447+
const blob = new Blob([json], { type: 'application/json' })
448+
downloadBlob(filename, blob)
233449

234-
const openMoreOutputs = (assetId: string) => {
235-
console.log('Opening more outputs for asset:', assetId)
450+
toast.add({
451+
severity: 'success',
452+
summary: t('g.success'),
453+
detail: 'Workflow exported successfully',
454+
life: 2000
455+
})
456+
} catch (error) {
457+
toast.add({
458+
severity: 'error',
459+
summary: t('g.error'),
460+
detail:
461+
error instanceof Error ? error.message : 'Failed to export workflow',
462+
life: 3000
463+
})
464+
}
236465
}
237466

238467
/**
@@ -314,11 +543,9 @@ export function useMediaAssetActions() {
314543
confirmDelete,
315544
deleteAsset,
316545
deleteMultipleAssets,
317-
playAsset,
318546
copyJobId,
319547
addWorkflow,
320548
openWorkflow,
321-
exportWorkflow,
322-
openMoreOutputs
549+
exportWorkflow
323550
}
324551
}

0 commit comments

Comments
 (0)