Optimize queue updates and colored selects

This commit is contained in:
Stu Leak 2026-01-03 22:15:46 -05:00
parent 2332f2e9ca
commit 69a00e922f
3 changed files with 385 additions and 269 deletions

View File

@ -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)

View File

@ -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

345
main.go
View File

@ -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()))