From 69a00e922f1cabda1aab087767fe5e76c1fb18bd Mon Sep 17 00:00:00 2001 From: Stu Leak Date: Sat, 3 Jan 2026 22:15:46 -0500 Subject: [PATCH] Optimize queue updates and colored selects --- internal/ui/components.go | 10 +- internal/ui/queueview.go | 299 +++++++++++++++++++++++---------- main.go | 345 ++++++++++++++++++-------------------- 3 files changed, 385 insertions(+), 269 deletions(-) diff --git a/internal/ui/components.go b/internal/ui/components.go index 7a91fb8..48ee0cf 100644 --- a/internal/ui/components.go +++ b/internal/ui/components.go @@ -1186,10 +1186,18 @@ func (cs *ColoredSelect) showPopup() { // Create scrollable list with proper spacing list := container.NewVBox(items...) scroll := container.NewVScroll(list) - scroll.SetMinSize(fyne.NewSize(300, 200)) // Add back minimum size for usability + dropWidth := cs.Size().Width + if dropWidth <= 0 { + dropWidth = cs.MinSize().Width + } + if dropWidth < 200 { + dropWidth = 200 + } + scroll.SetMinSize(fyne.NewSize(dropWidth, 200)) // Create popup cs.popup = widget.NewPopUp(scroll, cs.window.Canvas()) + cs.popup.Resize(fyne.NewSize(dropWidth, 200)) // Position popup below the select widget popupPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(cs) diff --git a/internal/ui/queueview.go b/internal/ui/queueview.go index ebe44eb..7d6385e 100644 --- a/internal/ui/queueview.go +++ b/internal/ui/queueview.go @@ -193,6 +193,54 @@ func applyAlpha(c color.Color, alpha uint8) color.Color { return color.NRGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: alpha} } +type queueCallbacks struct { + onBack func() + onPause func(string) + onResume func(string) + onCancel func(string) + onRemove func(string) + onMoveUp func(string) + onMoveDown func(string) + onPauseAll func() + onResumeAll func() + onStart func() + onClear func() + onClearAll func() + onCopyError func(string) + onViewLog func(string) + onCopyCommand func(string) +} + +type queueItemWidgets struct { + jobID string + status queue.JobStatus + container fyne.CanvasObject + titleLabel *widget.Label + descLabel *widget.Label + statusLabel *widget.Label + progress *StripedProgress + buttonBox *fyne.Container +} + +type QueueView struct { + Root fyne.CanvasObject + Scroll *container.Scroll + jobList *fyne.Container + emptyLabel fyne.CanvasObject + items map[string]*queueItemWidgets + callbacks queueCallbacks + bgColor color.Color + textColor color.Color +} + +func (v *QueueView) StopAnimations() { + for _, item := range v.items { + if item != nil && item.progress != nil { + item.progress.StopAnimation() + } + } +} + // BuildQueueView creates the queue viewer UI func BuildQueueView( jobs []*queue.Job, @@ -212,9 +260,7 @@ func BuildQueueView( onViewLog func(string), onCopyCommand func(string), titleColor, bgColor, textColor color.Color, -) (fyne.CanvasObject, *container.Scroll, []*StripedProgress) { - // Track active progress animations to prevent goroutine leaks - var activeProgress []*StripedProgress +) *QueueView { // Header title := canvas.NewText("JOB QUEUE", titleColor) title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true} @@ -247,30 +293,11 @@ func BuildQueueView( container.NewCenter(title), ) - // Job list - var jobItems []fyne.CanvasObject + jobList := container.NewVBox() + emptyMsg := widget.NewLabel("No jobs in queue") + emptyMsg.Alignment = fyne.TextAlignCenter + emptyLabel := container.NewCenter(emptyMsg) - if len(jobs) == 0 { - emptyMsg := widget.NewLabel("No jobs in queue") - emptyMsg.Alignment = fyne.TextAlignCenter - jobItems = append(jobItems, container.NewCenter(emptyMsg)) - } else { - // Calculate queue positions for pending/paused jobs - queuePositions := make(map[string]int) - position := 1 - for _, job := range jobs { - if job.Status == queue.JobStatusPending || job.Status == queue.JobStatusPaused { - queuePositions[job.ID] = position - position++ - } - } - - for _, job := range jobs { - jobItems = append(jobItems, buildJobItem(job, queuePositions, onPause, onResume, onCancel, onRemove, onMoveUp, onMoveDown, onCopyError, onViewLog, onCopyCommand, bgColor, textColor, &activeProgress)) - } - } - - jobList := container.NewVBox(jobItems...) // Use a scroll container anchored to the top to avoid jumpy scroll-to-content behavior. scrollable := container.NewScroll(jobList) // scrollable.SetMinSize(fyne.NewSize(0, 0)) // Removed for flexible sizing @@ -282,25 +309,43 @@ func BuildQueueView( scrollable, ) - return container.NewPadded(body), scrollable, activeProgress + view := &QueueView{ + Root: container.NewPadded(body), + Scroll: scrollable, + jobList: jobList, + emptyLabel: emptyLabel, + items: make(map[string]*queueItemWidgets), + callbacks: queueCallbacks{ + onBack: onBack, + onPause: onPause, + onResume: onResume, + onCancel: onCancel, + onRemove: onRemove, + onMoveUp: onMoveUp, + onMoveDown: onMoveDown, + onPauseAll: onPauseAll, + onResumeAll: onResumeAll, + onStart: onStart, + onClear: onClear, + onClearAll: onClearAll, + onCopyError: onCopyError, + onViewLog: onViewLog, + onCopyCommand: onCopyCommand, + }, + bgColor: bgColor, + textColor: textColor, + } + view.UpdateJobs(jobs) + return view } // buildJobItem creates a single job item in the queue list func buildJobItem( job *queue.Job, queuePositions map[string]int, - onPause func(string), - onResume func(string), - onCancel func(string), - onRemove func(string), - onMoveUp func(string), - onMoveDown func(string), - onCopyError func(string), - onViewLog func(string), - onCopyCommand func(string), + callbacks queueCallbacks, bgColor, textColor color.Color, - activeProgress *[]*StripedProgress, -) fyne.CanvasObject { +) *queueItemWidgets { // Status color statusColor := GetStatusColor(job.Status) @@ -328,8 +373,6 @@ 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() @@ -345,52 +388,7 @@ func buildJobItem( statusLabel.TextStyle = fyne.TextStyle{Monospace: true} statusLabel.Wrapping = fyne.TextTruncate - // Control buttons - var buttons []fyne.CanvasObject - // Reorder arrows for pending/paused jobs - if job.Status == queue.JobStatusPending || job.Status == queue.JobStatusPaused { - buttons = append(buttons, - widget.NewButton("↑", func() { onMoveUp(job.ID) }), - widget.NewButton("↓", func() { onMoveDown(job.ID) }), - ) - } - - switch job.Status { - case queue.JobStatusRunning: - buttons = append(buttons, - widget.NewButton("Copy Command", func() { onCopyCommand(job.ID) }), - widget.NewButton("Pause", func() { onPause(job.ID) }), - widget.NewButton("Cancel", func() { onCancel(job.ID) }), - ) - case queue.JobStatusPaused: - buttons = append(buttons, - widget.NewButton("Resume", func() { onResume(job.ID) }), - widget.NewButton("Cancel", func() { onCancel(job.ID) }), - ) - case queue.JobStatusPending: - buttons = append(buttons, - widget.NewButton("Copy Command", func() { onCopyCommand(job.ID) }), - ) - buttons = append(buttons, - widget.NewButton("Remove", func() { onRemove(job.ID) }), - ) - case queue.JobStatusCompleted, queue.JobStatusFailed, queue.JobStatusCancelled: - if job.Status == queue.JobStatusFailed && strings.TrimSpace(job.Error) != "" && onCopyError != nil { - buttons = append(buttons, - widget.NewButton("Copy Error", func() { onCopyError(job.ID) }), - ) - } - if job.LogPath != "" && onViewLog != nil { - buttons = append(buttons, - widget.NewButton("View Log", func() { onViewLog(job.ID) }), - ) - } - buttons = append(buttons, - widget.NewButton("Remove", func() { onRemove(job.ID) }), - ) - } - - buttonBox := container.NewHBox(buttons...) + buttonBox := buildJobButtons(job, callbacks) // Info section infoBox := container.NewVBox( @@ -418,13 +416,136 @@ func buildJobItem( ) // Wrap with draggable to allow drag-to-reorder (up/down by drag direction) - return newDraggableJobItem(job.ID, item, func(id string, dir int) { + wrapped := newDraggableJobItem(job.ID, item, func(id string, dir int) { if dir < 0 { - onMoveUp(id) + callbacks.onMoveUp(id) } else if dir > 0 { - onMoveDown(id) + callbacks.onMoveDown(id) } }) + + return &queueItemWidgets{ + jobID: job.ID, + status: job.Status, + container: wrapped, + titleLabel: titleLabel, + descLabel: descLabel, + statusLabel: statusLabel, + progress: progress, + buttonBox: buttonBox, + } +} + +func buildJobButtons(job *queue.Job, callbacks queueCallbacks) *fyne.Container { + var buttons []fyne.CanvasObject + + if job.Status == queue.JobStatusPending || job.Status == queue.JobStatusPaused { + buttons = append(buttons, + widget.NewButton("↑", func() { callbacks.onMoveUp(job.ID) }), + widget.NewButton("↓", func() { callbacks.onMoveDown(job.ID) }), + ) + } + + switch job.Status { + case queue.JobStatusRunning: + buttons = append(buttons, + widget.NewButton("Copy Command", func() { callbacks.onCopyCommand(job.ID) }), + widget.NewButton("Pause", func() { callbacks.onPause(job.ID) }), + widget.NewButton("Cancel", func() { callbacks.onCancel(job.ID) }), + ) + case queue.JobStatusPaused: + buttons = append(buttons, + widget.NewButton("Resume", func() { callbacks.onResume(job.ID) }), + widget.NewButton("Cancel", func() { callbacks.onCancel(job.ID) }), + ) + case queue.JobStatusPending: + buttons = append(buttons, + widget.NewButton("Copy Command", func() { callbacks.onCopyCommand(job.ID) }), + widget.NewButton("Remove", func() { callbacks.onRemove(job.ID) }), + ) + case queue.JobStatusCompleted, queue.JobStatusFailed, queue.JobStatusCancelled: + if job.Status == queue.JobStatusFailed && strings.TrimSpace(job.Error) != "" && callbacks.onCopyError != nil { + buttons = append(buttons, + widget.NewButton("Copy Error", func() { callbacks.onCopyError(job.ID) }), + ) + } + if job.LogPath != "" && callbacks.onViewLog != nil { + buttons = append(buttons, + widget.NewButton("View Log", func() { callbacks.onViewLog(job.ID) }), + ) + } + buttons = append(buttons, + widget.NewButton("Remove", func() { callbacks.onRemove(job.ID) }), + ) + } + + return container.NewHBox(buttons...) +} + +func updateJobItem(item *queueItemWidgets, job *queue.Job, queuePositions map[string]int, callbacks queueCallbacks) { + item.titleLabel.SetText(utils.ShortenMiddle(job.Title, 60)) + item.descLabel.SetText(utils.ShortenMiddle(job.Description, 90)) + item.statusLabel.SetText(getStatusText(job, queuePositions)) + + if job.Status == queue.JobStatusCompleted { + item.progress.SetProgress(1.0) + } else { + item.progress.SetProgress(job.Progress / 100.0) + } + + if job.Status == queue.JobStatusRunning { + item.progress.SetActivity(job.Progress <= 0.01) + item.progress.StartAnimation() + } else { + item.progress.SetActivity(false) + item.progress.StopAnimation() + } + + if item.status != job.Status { + item.status = job.Status + item.buttonBox.Objects = buildJobButtons(job, callbacks).Objects + item.buttonBox.Refresh() + } +} + +func (v *QueueView) UpdateJobs(jobs []*queue.Job) { + if len(jobs) == 0 { + v.jobList.Objects = []fyne.CanvasObject{v.emptyLabel} + v.jobList.Refresh() + return + } + + queuePositions := make(map[string]int) + position := 1 + for _, job := range jobs { + if job.Status == queue.JobStatusPending || job.Status == queue.JobStatusPaused { + queuePositions[job.ID] = position + position++ + } + } + + ordered := make([]fyne.CanvasObject, 0, len(jobs)) + seen := make(map[string]struct{}, len(jobs)) + for _, job := range jobs { + seen[job.ID] = struct{}{} + item := v.items[job.ID] + if item == nil { + item = buildJobItem(job, queuePositions, v.callbacks, v.bgColor, v.textColor) + v.items[job.ID] = item + } else { + updateJobItem(item, job, queuePositions, v.callbacks) + } + ordered = append(ordered, item.container) + } + + for id := range v.items { + if _, ok := seen[id]; !ok { + delete(v.items, id) + } + } + + v.jobList.Objects = ordered + v.jobList.Refresh() } // getStatusText returns a human-readable status string diff --git a/main.go b/main.go index eae3c04..5eb62ea 100644 --- a/main.go +++ b/main.go @@ -1002,7 +1002,7 @@ type appState struct { queueAutoRefreshStop chan struct{} queueAutoRefreshRunning bool - queueActiveProgress []*ui.StripedProgress // Track active progress animations to prevent goroutine leaks + queueView *ui.QueueView // Main menu refresh throttling mainMenuLastRefresh time.Time @@ -1643,6 +1643,9 @@ func (s *appState) showMainMenu() { s.stopPreview() s.stopPlayer() s.stopQueueAutoRefresh() + if s.queueView != nil { + s.queueView.StopAnimations() + } s.active = "" s.queueBackTarget = "" @@ -1791,6 +1794,9 @@ func (s *appState) showQueue() { } s.active = "queue" s.refreshQueueView() + if s.queueView != nil { + s.setContent(s.queueView.Root) + } s.startQueueAutoRefresh() } @@ -1839,178 +1845,159 @@ func (s *appState) refreshQueueView() { }}, jobs...) } - // 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 - s.stopQueueAutoRefresh() - target := s.queueBackTarget - if target == "" { - target = s.lastModule - } - if target != "" && target != "queue" && target != "menu" { - s.showModule(target) - } else { - s.showMainMenu() - } - }, - func(id string) { // onPause - if err := s.jobQueue.Pause(id); err != nil { - logging.Debug(logging.CatSystem, "failed to pause job: %v", err) - } - // Queue onChange callback handles refresh automatically - }, - func(id string) { // onResume - if err := s.jobQueue.Resume(id); err != nil { - logging.Debug(logging.CatSystem, "failed to resume job: %v", err) - } - // Queue onChange callback handles refresh automatically - }, - func(id string) { // onCancel - if err := s.jobQueue.Cancel(id); err != nil { - logging.Debug(logging.CatSystem, "failed to cancel job: %v", err) - } - // Queue onChange callback handles refresh automatically - }, - func(id string) { // onRemove - if err := s.jobQueue.Remove(id); err != nil { - logging.Debug(logging.CatSystem, "failed to remove job: %v", err) - } - // Queue onChange callback handles refresh automatically - }, - func(id string) { // onMoveUp - if err := s.jobQueue.MoveUp(id); err != nil { - logging.Debug(logging.CatSystem, "failed to move job up: %v", err) - } - // Queue onChange callback handles refresh automatically - }, - func(id string) { // onMoveDown - if err := s.jobQueue.MoveDown(id); err != nil { - logging.Debug(logging.CatSystem, "failed to move job down: %v", err) - } - // Queue onChange callback handles refresh automatically - }, - func() { // onPauseAll - s.jobQueue.PauseAll() - // Queue onChange callback handles refresh automatically - }, - func() { // onResumeAll - s.jobQueue.ResumeAll() - // Queue onChange callback handles refresh automatically - }, - func() { // onStart - s.jobQueue.ResumeAll() - // Queue onChange callback handles refresh automatically - }, - func() { // onClear - // Stop auto-refresh to prevent double UI updates - s.stopQueueAutoRefresh() - s.jobQueue.Clear() - - // Always return to main menu after clearing - if len(s.jobQueue.List()) == 0 { - s.showMainMenu() - } else { - // Restart auto-refresh and do single refresh - s.startQueueAutoRefresh() - s.refreshQueueView() - } - }, - func() { // onClearAll - // Stop auto-refresh to prevent double UI updates during navigation - s.stopQueueAutoRefresh() - s.jobQueue.ClearAll() - // Return to the module we were working on if possible - if s.lastModule != "" && s.lastModule != "queue" && s.lastModule != "menu" { - s.showModule(s.lastModule) - } else { - s.showMainMenu() - } - }, - func(id string) { // onCopyError - job, err := s.jobQueue.Get(id) - if err != nil { - logging.Debug(logging.CatSystem, "copy error text failed: %v", err) - return - } - text := strings.TrimSpace(job.Error) - if text == "" { - text = fmt.Sprintf("%s: no error message available", job.Title) - } - s.window.Clipboard().SetContent(text) - }, - func(id string) { // onViewLog - job, err := s.jobQueue.Get(id) - if err != nil { - logging.Debug(logging.CatSystem, "view log failed: %v", err) - return - } - path := strings.TrimSpace(job.LogPath) - if path == "" { - dialog.ShowInformation("No Log", "No log path recorded for this job.", s.window) - return - } - data, err := os.ReadFile(path) - if err != nil { - dialog.ShowError(fmt.Errorf("failed to read log: %w", err), s.window) - return - } - text := widget.NewMultiLineEntry() - text.SetText(string(data)) - text.Wrapping = fyne.TextWrapWord - text.Disable() - dialog.ShowCustom("Conversion Log", "Close", container.NewVScroll(text), s.window) - }, - func(id string) { // onCopyCommand - job, err := s.jobQueue.Get(id) - if err != nil { - logging.Debug(logging.CatSystem, "copy command failed: %v", err) - return - } - cmdStr := buildFFmpegCommandFromJob(job) - if cmdStr == "" { - dialog.ShowInformation("No Command", "Unable to generate FFmpeg command for this job.", s.window) - return - } - s.window.Clipboard().SetContent(cmdStr) - dialog.ShowInformation("Copied", "FFmpeg command copied to clipboard", s.window) - }, - utils.MustHex("#4CE870"), // titleColor - gridColor, // bgColor - textColor, // textColor - ) - - // Restore scroll offset - s.queueScroll = scroll - if s.queueScroll != nil && s.active == "queue" { - // Restore scroll position immediately to reduce jankiness - // Set offset before showing to avoid visible jumping - savedOffset := s.queueOffset - go func() { - // Minimal delay to allow layout calculation - time.Sleep(10 * time.Millisecond) - fyne.CurrentApp().Driver().DoFromGoroutine(func() { - if s.queueScroll != nil { - s.queueScroll.Offset = savedOffset - s.queueScroll.Refresh() + if s.queueView == nil { + view := ui.BuildQueueView( + jobs, + func() { // onBack + // Stop auto-refresh before navigating away for snappy response + s.stopQueueAutoRefresh() + target := s.queueBackTarget + if target == "" { + target = s.lastModule } - }, false) - }() + if target != "" && target != "queue" && target != "menu" { + s.showModule(target) + } else { + s.showMainMenu() + } + }, + func(id string) { // onPause + if err := s.jobQueue.Pause(id); err != nil { + logging.Debug(logging.CatSystem, "failed to pause job: %v", err) + } + }, + func(id string) { // onResume + if err := s.jobQueue.Resume(id); err != nil { + logging.Debug(logging.CatSystem, "failed to resume job: %v", err) + } + }, + func(id string) { // onCancel + if err := s.jobQueue.Cancel(id); err != nil { + logging.Debug(logging.CatSystem, "failed to cancel job: %v", err) + } + }, + func(id string) { // onRemove + if err := s.jobQueue.Remove(id); err != nil { + logging.Debug(logging.CatSystem, "failed to remove job: %v", err) + } + }, + func(id string) { // onMoveUp + if err := s.jobQueue.MoveUp(id); err != nil { + logging.Debug(logging.CatSystem, "failed to move job up: %v", err) + } + }, + func(id string) { // onMoveDown + if err := s.jobQueue.MoveDown(id); err != nil { + logging.Debug(logging.CatSystem, "failed to move job down: %v", err) + } + }, + func() { // onPauseAll + s.jobQueue.PauseAll() + }, + func() { // onResumeAll + s.jobQueue.ResumeAll() + }, + func() { // onStart + s.jobQueue.ResumeAll() + }, + func() { // onClear + // Stop auto-refresh to prevent double UI updates + s.stopQueueAutoRefresh() + s.jobQueue.Clear() + + // Always return to main menu after clearing + if len(s.jobQueue.List()) == 0 { + s.showMainMenu() + } else { + // Restart auto-refresh and do single refresh + s.startQueueAutoRefresh() + s.refreshQueueView() + } + }, + func() { // onClearAll + // Stop auto-refresh to prevent double UI updates during navigation + s.stopQueueAutoRefresh() + s.jobQueue.ClearAll() + // Return to the module we were working on if possible + if s.lastModule != "" && s.lastModule != "queue" && s.lastModule != "menu" { + s.showModule(s.lastModule) + } else { + s.showMainMenu() + } + }, + func(id string) { // onCopyError + job, err := s.jobQueue.Get(id) + if err != nil { + logging.Debug(logging.CatSystem, "copy error text failed: %v", err) + return + } + text := strings.TrimSpace(job.Error) + if text == "" { + text = fmt.Sprintf("%s: no error message available", job.Title) + } + s.window.Clipboard().SetContent(text) + }, + func(id string) { // onViewLog + job, err := s.jobQueue.Get(id) + if err != nil { + logging.Debug(logging.CatSystem, "view log failed: %v", err) + return + } + path := strings.TrimSpace(job.LogPath) + if path == "" { + dialog.ShowInformation("No Log", "No log path recorded for this job.", s.window) + return + } + data, err := os.ReadFile(path) + if err != nil { + dialog.ShowError(fmt.Errorf("failed to read log: %w", err), s.window) + return + } + text := widget.NewMultiLineEntry() + text.SetText(string(data)) + text.Wrapping = fyne.TextWrapWord + text.Disable() + dialog.ShowCustom("Conversion Log", "Close", container.NewVScroll(text), s.window) + }, + func(id string) { // onCopyCommand + job, err := s.jobQueue.Get(id) + if err != nil { + logging.Debug(logging.CatSystem, "copy command failed: %v", err) + return + } + cmdStr := buildFFmpegCommandFromJob(job) + if cmdStr == "" { + dialog.ShowInformation("No Command", "Unable to generate FFmpeg command for this job.", s.window) + return + } + s.window.Clipboard().SetContent(cmdStr) + dialog.ShowInformation("Copied", "FFmpeg command copied to clipboard", s.window) + }, + utils.MustHex("#4CE870"), // titleColor + gridColor, // bgColor + textColor, // textColor + ) + + s.queueView = view + s.queueScroll = view.Scroll + s.setContent(view.Root) + + // Restore scroll offset + if s.queueScroll != nil && s.active == "queue" { + savedOffset := s.queueOffset + go func() { + time.Sleep(10 * time.Millisecond) + fyne.CurrentApp().Driver().DoFromGoroutine(func() { + if s.queueScroll != nil { + s.queueScroll.Offset = savedOffset + s.queueScroll.Refresh() + } + }, false) + }() + } + } else { + s.queueView.UpdateJobs(jobs) } - - // Store active progress bars to stop them on next refresh - s.queueActiveProgress = activeProgress - - s.setContent(container.NewPadded(view)) } func (s *appState) startQueueAutoRefresh() { @@ -2058,13 +2045,9 @@ 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() - } + if s.queueView != nil { + s.queueView.StopAnimations() } - s.queueActiveProgress = nil } // addConvertToQueue adds a conversion job to the queue @@ -2738,6 +2721,9 @@ func (s *appState) showMissingDependenciesDialog(moduleID string) { func (s *appState) showModule(id string) { if id != "queue" { s.stopQueueAutoRefresh() + if s.queueView != nil { + s.queueView.StopAnimations() + } } // Check if module has missing dependencies @@ -6629,7 +6615,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { } // Format selector - formatContainer := widget.NewSelect(formatLabels, func(selected string) { + formatColors := ui.BuildFormatColorMap(formatLabels) + formatContainer := ui.NewColoredSelect(formatLabels, formatColors, func(selected string) { for _, opt := range formatOptions { if opt.Label == selected { state.convert.SelectedFormat = opt @@ -6643,7 +6630,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { break } } - }) + }, state.window) formatContainer.SetSelected(state.convert.SelectedFormat.Label) outputHint := widget.NewLabel(fmt.Sprintf("Output file: %s", state.convert.OutputFile()))