diff --git a/internal/ui/queueview.go b/internal/ui/queueview.go index 3fd3315..d750cc2 100644 --- a/internal/ui/queueview.go +++ b/internal/ui/queueview.go @@ -212,7 +212,9 @@ func BuildQueueView( onViewLog func(string), onCopyCommand func(string), titleColor, bgColor, textColor color.Color, -) (fyne.CanvasObject, *container.Scroll) { +) (fyne.CanvasObject, *container.Scroll, []*StripedProgress) { + // Track active progress animations to prevent goroutine leaks + var activeProgress []*StripedProgress // Header title := canvas.NewText("JOB QUEUE", titleColor) title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true} @@ -264,7 +266,7 @@ func BuildQueueView( } for _, job := range jobs { - jobItems = append(jobItems, buildJobItem(job, queuePositions, onPause, onResume, onCancel, onRemove, onMoveUp, onMoveDown, onCopyError, onViewLog, onCopyCommand, bgColor, textColor)) + jobItems = append(jobItems, buildJobItem(job, queuePositions, onPause, onResume, onCancel, onRemove, onMoveUp, onMoveDown, onCopyError, onViewLog, onCopyCommand, bgColor, textColor, &activeProgress)) } } @@ -280,7 +282,7 @@ func BuildQueueView( scrollable, ) - return container.NewPadded(body), scrollable + return container.NewPadded(body), scrollable, activeProgress } // buildJobItem creates a single job item in the queue list @@ -297,6 +299,7 @@ func buildJobItem( onViewLog func(string), onCopyCommand func(string), bgColor, textColor color.Color, + activeProgress *[]*StripedProgress, ) fyne.CanvasObject { // Status color statusColor := GetStatusColor(job.Status) @@ -325,6 +328,8 @@ func buildJobItem( if job.Status == queue.JobStatusRunning { progress.SetActivity(job.Progress <= 0.01) progress.StartAnimation() + // Track active progress to stop animation on next refresh (prevents goroutine leaks) + *activeProgress = append(*activeProgress, progress) } else { progress.SetActivity(false) progress.StopAnimation() diff --git a/main.go b/main.go index ac8fd17..8908c9f 100644 --- a/main.go +++ b/main.go @@ -986,6 +986,7 @@ type appState struct { queueAutoRefreshStop chan struct{} queueAutoRefreshRunning bool + queueActiveProgress []*ui.StripedProgress // Track active progress animations to prevent goroutine leaks // Main menu refresh throttling mainMenuLastRefresh time.Time @@ -1781,7 +1782,16 @@ func (s *appState) refreshQueueView() { }}, jobs...) } - view, scroll := ui.BuildQueueView( + // CRITICAL: Stop all active progress animations before rebuilding to prevent goroutine leaks + // Each refresh creates new StripedProgress widgets, and old animation goroutines must be stopped + for _, progress := range s.queueActiveProgress { + if progress != nil { + progress.StopAnimation() + } + } + s.queueActiveProgress = nil + + view, scroll, activeProgress := ui.BuildQueueView( jobs, func() { // onBack // Stop auto-refresh before navigating away for snappy response @@ -1938,6 +1948,9 @@ func (s *appState) refreshQueueView() { }() } + // Store active progress bars to stop them on next refresh + s.queueActiveProgress = activeProgress + s.setContent(container.NewPadded(view)) } @@ -1984,6 +1997,14 @@ func (s *appState) stopQueueAutoRefresh() { } s.queueAutoRefreshStop = nil s.queueAutoRefreshRunning = false + + // Stop all active progress animations to prevent goroutine leaks when leaving queue view + for _, progress := range s.queueActiveProgress { + if progress != nil { + progress.StopAnimation() + } + } + s.queueActiveProgress = nil } // addConvertToQueue adds a conversion job to the queue