Fix threading errors with proper initialization guard

The threading errors were caused by queue callbacks triggering showMainMenu()
during app initialization, before the Fyne event loop was fully ready.

Changes:
1. Added initComplete flag to appState struct
2. Queue callback returns early if !initComplete, preventing UI updates
   during initialization
3. Set initComplete=true AFTER ShowAndRun() would handle the event loop
4. Removed nested DoFromGoroutine() which was causing double-wrapping
5. Simplified setContent() to direct calls (no thread wrapping)
6. Callback properly marshals UI updates via DoFromGoroutine() after init

This ensures the queue callback only affects UI after the app is fully
initialized and the Fyne event loop is running.
This commit is contained in:
Stu Leak 2025-11-27 00:23:03 -05:00
parent b16b3439fb
commit 3ffb7c8a97

116
main.go
View File

@ -151,30 +151,31 @@ func (c convertConfig) CoverLabel() string {
} }
type appState struct { type appState struct {
window fyne.Window window fyne.Window
active string active string
source *videoSource initComplete bool // True after initial UI setup completes
loadedVideos []*videoSource // Multiple loaded videos for navigation source *videoSource
currentIndex int // Current video index in loadedVideos loadedVideos []*videoSource // Multiple loaded videos for navigation
anim *previewAnimator currentIndex int // Current video index in loadedVideos
convert convertConfig anim *previewAnimator
currentFrame string convert convertConfig
player player.Controller currentFrame string
playerReady bool player player.Controller
playerVolume float64 playerReady bool
playerMuted bool playerVolume float64
lastVolume float64 playerMuted bool
playerPaused bool lastVolume float64
playerPos float64 playerPaused bool
playerLast time.Time playerPos float64
progressQuit chan struct{} playerLast time.Time
convertCancel context.CancelFunc progressQuit chan struct{}
playerSurf *playerSurface convertCancel context.CancelFunc
convertBusy bool playerSurf *playerSurface
convertStatus string convertBusy bool
playSess *playSession convertStatus string
jobQueue *queue.Queue playSess *playSession
statsBar *ui.ConversionStatsBar jobQueue *queue.Queue
statsBar *ui.ConversionStatsBar
} }
func (s *appState) stopPreview() { func (s *appState) stopPreview() {
@ -308,26 +309,11 @@ func (s *appState) applyInverseDefaults(src *videoSource) {
func (s *appState) setContent(body fyne.CanvasObject) { func (s *appState) setContent(body fyne.CanvasObject) {
bg := canvas.NewRectangle(backgroundColor) bg := canvas.NewRectangle(backgroundColor)
// Don't set a minimum size - let content determine layout naturally // Don't set a minimum size - let content determine layout naturally
if body == nil {
// Always use DoFromGoroutine to ensure we're on the main thread s.window.SetContent(bg)
// This is safe even when already on the main thread return
app := fyne.CurrentApp()
if app != nil && app.Driver() != nil {
app.Driver().DoFromGoroutine(func() {
if body == nil {
s.window.SetContent(bg)
} else {
s.window.SetContent(container.NewMax(bg, body))
}
}, false)
} else {
// Fallback if app not ready yet (during initialization)
if body == nil {
s.window.SetContent(bg)
} else {
s.window.SetContent(container.NewMax(bg, body))
}
} }
s.window.SetContent(container.NewMax(bg, body))
} }
// showErrorWithCopy displays an error dialog with a "Copy Error" button // showErrorWithCopy displays an error dialog with a "Copy Error" button
@ -1096,28 +1082,32 @@ func runGUI() {
// Start queue processing (but paused by default) // Start queue processing (but paused by default)
state.jobQueue.Start() state.jobQueue.Start()
// Set callback AFTER showing the window to avoid threading issues during startup // Set callback - queue changes are triggered by job processor goroutine
// Use a goroutine with delay to ensure UI is fully initialized state.jobQueue.SetChangeCallback(func() {
go func() { // Skip updates during initialization
time.Sleep(100 * time.Millisecond) if !state.initComplete {
state.jobQueue.SetChangeCallback(func() { return
// Queue callbacks come from goroutines, so wrap UI calls }
app := fyne.CurrentApp()
if app == nil || app.Driver() == nil { // Marshal UI updates to main thread
return app := fyne.CurrentApp()
if app == nil || app.Driver() == nil {
return
}
app.Driver().DoFromGoroutine(func() {
// Update stats bar
state.updateStatsBar()
// Refresh UI when queue changes and we're on main menu
if state.active == "" {
state.showMainMenu()
} }
}, false)
})
app.Driver().DoFromGoroutine(func() { // Mark initialization as complete
// Update stats bar state.initComplete = true
state.updateStatsBar()
// Refresh UI when queue changes
if state.active == "" {
state.showMainMenu()
}
}, false)
})
}()
defer state.shutdown() defer state.shutdown()
w.SetOnDropped(func(pos fyne.Position, items []fyne.URI) { w.SetOnDropped(func(pos fyne.Position, items []fyne.URI) {