Fix conversion progress updates when navigating between modules

Previously, when a conversion was started and the user navigated away from
the Convert module and returned, the progress stats would freeze (though the
progress bar would continue animating). This was caused by the conversion
goroutine updating stale widget references.

Changes:
- Decoupled conversion state from UI widgets
- Conversion goroutine now only updates appState (convertBusy, convertStatus)
- Added 200ms UI refresh ticker in buildConvertView to update widgets from state
- Removed all direct widget manipulation from background conversion process

This ensures conversion progress stats remain accurate and update correctly
regardless of module navigation, supporting the persistent video context
design where conversions continue running while users work in other modules.
This commit is contained in:
Stu Leak 2025-11-25 18:48:09 -05:00
parent 103d8ded83
commit d7ec373470

142
main.go
View File

@ -993,6 +993,65 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
// Wrap mainArea in a scroll container to prevent content from forcing window resize // Wrap mainArea in a scroll container to prevent content from forcing window resize
scrollableMain := container.NewScroll(mainArea) scrollableMain := container.NewScroll(mainArea)
// Start a UI refresh ticker to update widgets from state while conversion is active
// This ensures progress updates even when navigating between modules
go func() {
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
// Track the previous busy state to detect transitions
wasBusy := state.convertBusy
for {
select {
case <-ticker.C:
isBusy := state.convertBusy
// Update UI on the main thread
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
// Update status label from state
if isBusy {
statusLabel.SetText(state.convertStatus)
} else if wasBusy {
// Just finished - update one last time
statusLabel.SetText(state.convertStatus)
}
// Update button states
if isBusy {
convertBtn.Disable()
cancelBtn.Enable()
activity.Show()
if !activity.Running() {
activity.Start()
}
} else {
if src != nil {
convertBtn.Enable()
} else {
convertBtn.Disable()
}
cancelBtn.Disable()
activity.Stop()
activity.Hide()
}
}, false)
// If conversion finished, stop the ticker after one final update
if wasBusy && !isBusy {
return
}
wasBusy = isBusy
case <-time.After(30 * time.Second):
// Safety timeout - if no conversion after 30s, stop ticker
if !state.convertBusy {
return
}
}
}
}()
return container.NewBorder( return container.NewBorder(
backBar, backBar,
container.NewVBox(widget.NewSeparator(), actionBar), container.NewVBox(widget.NewSeparator(), actionBar),
@ -2123,13 +2182,8 @@ func (s *appState) cancelConvert(cancelBtn, btn *widget.Button, spinner *widget.
if s.convertCancel == nil { if s.convertCancel == nil {
return return
} }
if cancelBtn != nil {
cancelBtn.Disable()
}
s.convertStatus = "Cancelling…" s.convertStatus = "Cancelling…"
if status != nil { // Widget states will be updated by the UI refresh ticker
status.SetText(s.convertStatus)
}
s.convertCancel() s.convertCancel()
} }
@ -2137,9 +2191,8 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
setStatus := func(msg string) { setStatus := func(msg string) {
s.convertStatus = msg s.convertStatus = msg
logging.Debug(logging.CatFFMPEG, "convert status: %s", msg) logging.Debug(logging.CatFFMPEG, "convert status: %s", msg)
if status != nil { // Note: Don't update widgets here - they may be stale if user navigated away
status.SetText(msg) // The UI will refresh from state.convertStatus via a ticker
}
} }
if s.source == nil { if s.source == nil {
dialog.ShowInformation("Convert", "Load a video first.", s.window) dialog.ShowInformation("Convert", "Load a video first.", s.window)
@ -2316,16 +2369,7 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
logging.Debug(logging.CatFFMPEG, "convert command: ffmpeg %s", strings.Join(args, " ")) logging.Debug(logging.CatFFMPEG, "convert command: ffmpeg %s", strings.Join(args, " "))
s.convertBusy = true s.convertBusy = true
setStatus("Preparing conversion…") setStatus("Preparing conversion…")
if btn != nil { // Widget states will be updated by the UI refresh ticker
btn.Disable()
}
if spinner != nil {
spinner.Show()
spinner.Start()
}
if cancelBtn != nil {
cancelBtn.Enable()
}
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
s.convertCancel = cancel s.convertCancel = cancel
@ -2344,16 +2388,6 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
dialog.ShowError(fmt.Errorf("convert failed: %w", err), s.window) dialog.ShowError(fmt.Errorf("convert failed: %w", err), s.window)
s.convertBusy = false s.convertBusy = false
setStatus("Failed") setStatus("Failed")
if btn != nil {
btn.Enable()
}
if cancelBtn != nil {
cancelBtn.Disable()
}
if spinner != nil {
spinner.Stop()
spinner.Hide()
}
}, false) }, false)
s.convertCancel = nil s.convertCancel = nil
return return
@ -2418,16 +2452,6 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
dialog.ShowError(fmt.Errorf("convert failed: %w", err), s.window) dialog.ShowError(fmt.Errorf("convert failed: %w", err), s.window)
s.convertBusy = false s.convertBusy = false
setStatus("Failed") setStatus("Failed")
if btn != nil {
btn.Enable()
}
if cancelBtn != nil {
cancelBtn.Disable()
}
if spinner != nil {
spinner.Stop()
spinner.Hide()
}
}, false) }, false)
s.convertCancel = nil s.convertCancel = nil
return return
@ -2441,16 +2465,6 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
fyne.CurrentApp().Driver().DoFromGoroutine(func() { fyne.CurrentApp().Driver().DoFromGoroutine(func() {
s.convertBusy = false s.convertBusy = false
setStatus("Cancelled") setStatus("Cancelled")
if btn != nil {
btn.Enable()
}
if cancelBtn != nil {
cancelBtn.Disable()
}
if spinner != nil {
spinner.Stop()
spinner.Hide()
}
}, false) }, false)
s.convertCancel = nil s.convertCancel = nil
return return
@ -2460,16 +2474,6 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
dialog.ShowError(fmt.Errorf("convert failed: %w", err), s.window) dialog.ShowError(fmt.Errorf("convert failed: %w", err), s.window)
s.convertBusy = false s.convertBusy = false
setStatus("Failed") setStatus("Failed")
if btn != nil {
btn.Enable()
}
if cancelBtn != nil {
cancelBtn.Disable()
}
if spinner != nil {
spinner.Stop()
spinner.Hide()
}
}, false) }, false)
s.convertCancel = nil s.convertCancel = nil
return return
@ -2483,16 +2487,6 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
dialog.ShowError(fmt.Errorf("conversion output is invalid: %w", probeErr), s.window) dialog.ShowError(fmt.Errorf("conversion output is invalid: %w", probeErr), s.window)
s.convertBusy = false s.convertBusy = false
setStatus("Failed") setStatus("Failed")
if btn != nil {
btn.Enable()
}
if cancelBtn != nil {
cancelBtn.Disable()
}
if spinner != nil {
spinner.Stop()
spinner.Hide()
}
}, false) }, false)
s.convertCancel = nil s.convertCancel = nil
return return
@ -2502,16 +2496,6 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
dialog.ShowInformation("Convert", fmt.Sprintf("Saved %s", outPath), s.window) dialog.ShowInformation("Convert", fmt.Sprintf("Saved %s", outPath), s.window)
s.convertBusy = false s.convertBusy = false
setStatus("Done") setStatus("Done")
if btn != nil {
btn.Enable()
}
if cancelBtn != nil {
cancelBtn.Disable()
}
if spinner != nil {
spinner.Stop()
spinner.Hide()
}
}, false) }, false)
s.convertCancel = nil s.convertCancel = nil
}() }()