44 :class =" [`job-item--${job.status}`, { 'job-item--dragging': isDragging, 'job-item--selected': isSelected }]"
55 :draggable =" draggable && job.status !== 'rendering'"
66 @click =" handleClick"
7+ @contextmenu.prevent =" handleContextMenu"
78 @dragstart =" handleDragStart"
89 @dragend =" handleDragEnd"
910 >
11+ <!-- Context Menu -->
12+ <div
13+ v-if =" showContextMenu"
14+ class =" context-menu"
15+ :style =" contextMenuPosition"
16+ @click.stop
17+ >
18+ <button class =" context-menu__item" @click =" openOutputFolder" >
19+ <svg width =" 16" height =" 16" viewBox =" 0 0 24 24" fill =" currentColor" >
20+ <path d =" M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" />
21+ </svg >
22+ <span >Open Output Folder</span >
23+ </button >
24+ <button class =" context-menu__item" @click =" openProjectFolder" >
25+ <svg width =" 16" height =" 16" viewBox =" 0 0 24 24" fill =" currentColor" >
26+ <path d =" M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-6 10H6v-2h8v2zm4-4H6v-2h12v2z" />
27+ </svg >
28+ <span >Open Project Folder</span >
29+ </button >
30+ <button class =" context-menu__item" @click =" openProject" >
31+ <svg width =" 16" height =" 16" viewBox =" 0 0 24 24" fill =" currentColor" >
32+ <path d =" M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z" />
33+ </svg >
34+ <span >Open Project in {{ appLabel }}</span >
35+ </button >
36+ <div class =" context-menu__divider" ></div >
37+ <button
38+ class =" context-menu__item"
39+ @click =" resetJob"
40+ :disabled =" job.status === 'rendering' || job.status === 'idle'"
41+ >
42+ <svg width =" 16" height =" 16" viewBox =" 0 0 24 24" fill =" currentColor" >
43+ <path d =" M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" />
44+ </svg >
45+ <span >Reset to Pending</span >
46+ </button >
47+ </div >
48+
1049 <div class =" job-item__header" >
1150 <div
1251 class =" job-item__drag-handle"
66105 <div
67106 class =" progress-bar__fill"
68107 :class =" [`progress-bar__fill--${job.status}`]"
69- :style =" { width: `${job.progress}%` }"
108+ :style =" { width: job.status === 'loading' ? '100%' : `${job.progress}%` }"
70109 />
71110 </div >
72111 <div class =" job-item__progress-info" >
73112 <span class =" job-item__progress-text" >
74- {{ Math.round(job.progress) }}%
75- <template v-if =" job .status === ' rendering' || job .status === ' paused' " >
76- • Frame {{ job.currentFrame }} / {{ job.totalFrames }}
113+ <template v-if =" job .status === ' loading' " >
114+ Loading...
115+ </template >
116+ <template v-else >
117+ {{ Math.round(job.progress) }}%
118+ <template v-if =" job .status === ' rendering' || job .status === ' paused' " >
119+ • Frame {{ job.currentFrame }} / {{ job.totalFrames }}
120+ </template >
77121 </template >
78122 </span >
79- <span v-if =" job.estimatedTimeRemaining > 0" class =" job-item__eta" >
123+ <span v-if =" job.estimatedTimeRemaining > 0 && job.status !== 'loading' " class =" job-item__eta" >
80124 ETA: {{ formatTime(job.estimatedTimeRemaining) }}
81125 </span >
82126 </div >
163207</template >
164208
165209<script setup lang="ts">
166- import { ref , computed } from ' vue' ;
210+ import { ref , computed , onUnmounted } from ' vue' ;
167211import type { RenderJob } from ' ~/stores/renderQueue' ;
168212import { ApplicationType , APPLICATION_INFO } from ' ~/types/applications' ;
169213
@@ -183,10 +227,13 @@ const emit = defineEmits<{
183227 (e : ' dragstart' , event : DragEvent ): void ;
184228 (e : ' dragend' , event : DragEvent ): void ;
185229 (e : ' select' ): void ;
230+ (e : ' reset' ): void ;
186231}>();
187232
188233const expanded = ref (false );
189234const isDragging = ref (false );
235+ const showContextMenu = ref (false );
236+ const contextMenuPosition = ref ({ top: ' 0px' , left: ' 0px' });
190237
191238const statusLabel = computed (() => {
192239 switch (props .job .status ) {
@@ -196,10 +243,20 @@ const statusLabel = computed(() => {
196243 case ' complete' : return ' Complete' ;
197244 case ' error' : return ' Error' ;
198245 case ' missing-app' : return ' Missing App' ;
246+ case ' loading' : return loadingLabel .value ;
199247 default : return props .job .status ;
200248 }
201249});
202250
251+ // Loading label - different text for After Effects/Nuke (comp) vs others (scene)
252+ const loadingLabel = computed (() => {
253+ const appType = props .job .applicationType || ApplicationType .BLENDER ;
254+ if (appType === ApplicationType .AFTER_EFFECTS || appType === ApplicationType .NUKE ) {
255+ return ' Fetching comp metadata...' ;
256+ }
257+ return ' Fetching scene metadata...' ;
258+ });
259+
203260// Application type display
204261const appLabel = computed (() => {
205262 const appType = props .job .applicationType || ApplicationType .BLENDER ;
@@ -280,15 +337,84 @@ function openOutputFolder() {
280337 if (typeof window !== ' undefined' && (window as any ).electronAPI ) {
281338 (window as any ).electronAPI .openInExplorer (props .job .outputDir );
282339 }
340+ closeContextMenu ();
283341}
342+
343+ // Context menu handlers
344+ function handleContextMenu(e : MouseEvent ) {
345+ e .preventDefault ();
346+
347+ // Get the bounding rect of the job item to position the menu
348+ const rect = (e .currentTarget as HTMLElement ).getBoundingClientRect ();
349+
350+ // Position the menu at the click point, but relative to the item
351+ contextMenuPosition .value = {
352+ top: ` ${e .clientY - rect .top }px ` ,
353+ left: ` ${e .clientX - rect .left }px `
354+ };
355+
356+ showContextMenu .value = true ;
357+
358+ // Close menu when clicking elsewhere
359+ setTimeout (() => {
360+ document .addEventListener (' click' , closeContextMenu , { once: true });
361+ document .addEventListener (' contextmenu' , closeContextMenu , { once: true });
362+ }, 0 );
363+ }
364+
365+ function closeContextMenu() {
366+ showContextMenu .value = false ;
367+ }
368+
369+ function openProjectFolder() {
370+ if (typeof window !== ' undefined' && (window as any ).electronAPI ) {
371+ // Get the directory containing the project file
372+ const filePath = props .job .filePath ;
373+ (window as any ).electronAPI .openInExplorer (filePath );
374+ }
375+ closeContextMenu ();
376+ }
377+
378+ function openProject() {
379+ if (typeof window !== ' undefined' && (window as any ).electronAPI ) {
380+ // Open the project file with its default application (like double-clicking)
381+ (window as any ).electronAPI .openFileWithDefaultApp (props .job .filePath );
382+ }
383+ closeContextMenu ();
384+ }
385+
386+ function resetJob() {
387+ if (props .job .status !== ' rendering' && props .job .status !== ' idle' ) {
388+ emit (' update' , {
389+ status: ' idle' ,
390+ progress: 0 ,
391+ currentFrame: 0 ,
392+ elapsedTime: 0 ,
393+ estimatedTimeRemaining: 0 ,
394+ lastRenderedFrame: null ,
395+ error: null ,
396+ renderStartTime: null ,
397+ frameTimes: [],
398+ renderedFramePaths: [],
399+ });
400+ }
401+ closeContextMenu ();
402+ }
403+
404+ // Cleanup on unmount
405+ onUnmounted (() => {
406+ document .removeEventListener (' click' , closeContextMenu );
407+ document .removeEventListener (' contextmenu' , closeContextMenu );
408+ });
284409 </script >
285410
286411<style lang="scss">
287412.job-item {
413+ position : relative ;
288414 background-color : $bg-tertiary ;
289415 border : 1px solid $border-subtle ;
290416 border-radius : $radius-lg ;
291- overflow : hidden ;
417+ overflow : visible ;
292418 transition : border-color $transition-fast ease , opacity $transition-fast ease , transform $transition-fast ease ;
293419
294420 & :hover {
@@ -345,6 +471,15 @@ function openOutputFolder() {
345471 }
346472 }
347473
474+ & --loading {
475+ border-color : $text-tertiary ;
476+ opacity : 0.8 ;
477+
478+ .job-item__name {
479+ color : $text-secondary ;
480+ }
481+ }
482+
348483 & __header {
349484 display : flex ;
350485 justify-content : space-between ;
@@ -561,4 +696,51 @@ function openOutputFolder() {
561696 color : $text-primary ;
562697 }
563698}
699+
700+ .context-menu {
701+ position : absolute ;
702+ z-index : 1000 ;
703+ min-width : 200px ;
704+ background-color : $bg-secondary ;
705+ border : 1px solid $border-subtle ;
706+ border-radius : $radius-md ;
707+ box-shadow : 0 8px 24px rgba (0 , 0 , 0 , 0.4 );
708+ padding : $spacing-02 ;
709+
710+ & __item {
711+ display : flex ;
712+ align-items : center ;
713+ gap : $spacing-03 ;
714+ width : 100% ;
715+ padding : $spacing-03 $spacing-04 ;
716+ background : none ;
717+ border : none ;
718+ border-radius : $radius-sm ;
719+ color : $text-primary ;
720+ font-size : $font-size-sm ;
721+ text-align : left ;
722+ cursor : pointer ;
723+ transition : background-color $transition-fast ease ;
724+
725+ & :hover:not (:disabled ) {
726+ background-color : rgba ($accent-primary , 0.1 );
727+ }
728+
729+ & :disabled {
730+ opacity : 0.5 ;
731+ cursor : not-allowed ;
732+ }
733+
734+ svg {
735+ flex-shrink : 0 ;
736+ color : $text-secondary ;
737+ }
738+ }
739+
740+ & __divider {
741+ height : 1px ;
742+ background-color : $border-subtle ;
743+ margin : $spacing-02 0 ;
744+ }
745+ }
564746 </style >
0 commit comments