1- /* eslint-disable no-console */
21import { useToast } from 'primevue/usetoast'
32import { inject } from 'vue'
43
54import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
65import { downloadFile } from '@/base/common/downloadUtil'
6+ import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
77import { t } from '@/i18n'
88import { 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'
913import { 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'
1019import { getOutputAssetMetadata } from '../schemas/assetMetadataSchema'
1120import { useAssetsStore } from '@/stores/assetsStore'
1221import { useDialogStore } from '@/stores/dialogStore'
22+ import { createAnnotatedPath } from '@/utils/createAnnotatedPath'
23+ import { appendJsonExt } from '@/utils/formatUtil'
1324
1425import type { AssetItem } from '../schemas/assetSchema'
1526import { MediaAssetKey } from '../schemas/mediaAssetSchema'
1627import { assetService } from '../services/assetService'
28+ import type { ResultItemType } from '@/schemas/apiSchema'
1729
1830export 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