Revert "Fix Fyne threading error and queue persistence issues"

This reverts commit cfb608e191.
This commit is contained in:
Stu Leak 2025-11-27 00:12:00 -05:00
parent c44074f043
commit 2552e0fcad
3 changed files with 104 additions and 378 deletions

View File

@ -51,7 +51,6 @@ type Job struct {
StartedAt *time.Time `json:"started_at,omitempty"` StartedAt *time.Time `json:"started_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"` CompletedAt *time.Time `json:"completed_at,omitempty"`
Priority int `json:"priority"` // Higher priority = runs first Priority int `json:"priority"` // Higher priority = runs first
HasModifiedSettings bool `json:"has_modified_settings"`
cancel context.CancelFunc `json:"-"` cancel context.CancelFunc `json:"-"`
} }
@ -60,12 +59,11 @@ type JobExecutor func(ctx context.Context, job *Job, progressCallback func(float
// Queue manages a queue of jobs // Queue manages a queue of jobs
type Queue struct { type Queue struct {
jobs []*Job jobs []*Job
executor JobExecutor executor JobExecutor
running bool running bool
paused bool // When paused, no jobs are processed even if running mu sync.RWMutex
mu sync.RWMutex onChange func() // Callback when queue state changes
onChange func() // Callback when queue state changes
} }
// New creates a new queue with the given executor // New creates a new queue with the given executor
@ -74,7 +72,6 @@ func New(executor JobExecutor) *Queue {
jobs: make([]*Job, 0), jobs: make([]*Job, 0),
executor: executor, executor: executor,
running: false, running: false,
paused: true, // Start paused by default
} }
} }
@ -252,22 +249,6 @@ func (q *Queue) Stop() {
q.running = false q.running = false
} }
// PauseProcessing pauses job processing (jobs stay in queue)
func (q *Queue) PauseProcessing() {
q.mu.Lock()
defer q.mu.Unlock()
q.paused = true
q.notifyChange()
}
// ResumeProcessing resumes job processing
func (q *Queue) ResumeProcessing() {
q.mu.Lock()
defer q.mu.Unlock()
q.paused = false
q.notifyChange()
}
// processJobs continuously processes pending jobs // processJobs continuously processes pending jobs
func (q *Queue) processJobs() { func (q *Queue) processJobs() {
for { for {
@ -277,13 +258,6 @@ func (q *Queue) processJobs() {
return return
} }
// If paused, wait and don't process jobs
if q.paused {
q.mu.Unlock()
time.Sleep(500 * time.Millisecond)
continue
}
// Find highest priority pending job // Find highest priority pending job
var nextJob *Job var nextJob *Job
highestPriority := -1 highestPriority := -1
@ -404,15 +378,6 @@ func (q *Queue) Clear() {
q.notifyChange() q.notifyChange()
} }
// ClearAll removes all jobs from the queue
func (q *Queue) ClearAll() {
q.mu.Lock()
defer q.mu.Unlock()
q.jobs = make([]*Job, 0)
q.notifyChange()
}
// generateID generates a unique ID for a job // generateID generates a unique ID for a job
func generateID() string { func generateID() string {
return fmt.Sprintf("job-%d", time.Now().UnixNano()) return fmt.Sprintf("job-%d", time.Now().UnixNano())

View File

@ -21,8 +21,6 @@ func BuildQueueView(
onCancel func(string), onCancel func(string),
onRemove func(string), onRemove func(string),
onClear func(), onClear func(),
onClearAll func(),
onProcess func(),
titleColor, bgColor, textColor color.Color, titleColor, bgColor, textColor color.Color,
) fyne.CanvasObject { ) fyne.CanvasObject {
// Header // Header
@ -31,73 +29,20 @@ func BuildQueueView(
title.TextSize = 24 title.TextSize = 24
backBtn := widget.NewButton("← Back", onBack) backBtn := widget.NewButton("← Back", onBack)
backBtn.Importance = widget.LowImportance
clearBtn := widget.NewButton("Clear Completed", onClear) clearBtn := widget.NewButton("Clear Completed", onClear)
clearBtn.Importance = widget.LowImportance
clearAllBtn := widget.NewButton("Clear All", onClearAll)
clearAllBtn.Importance = widget.DangerImportance
processBtn := widget.NewButton("▶ Process Queue", onProcess)
processBtn.Importance = widget.HighImportance
// Only show process button if there are pending jobs
if len(jobs) == 0 {
processBtn.Disable()
}
var hasPending bool
for _, job := range jobs {
if job.Status == queue.JobStatusPending {
hasPending = true
break
}
}
if !hasPending {
processBtn.Disable()
}
header := container.NewBorder( header := container.NewBorder(
nil, nil, nil, nil,
backBtn, backBtn,
container.NewHBox(clearBtn, clearAllBtn, processBtn), clearBtn,
container.NewCenter(title), container.NewCenter(title),
) )
// Count stats
pending := 0
running := 0
failed := 0
completed := 0
for _, job := range jobs {
switch job.Status {
case queue.JobStatusPending:
pending++
case queue.JobStatusRunning:
running++
case queue.JobStatusCompleted:
completed++
case queue.JobStatusFailed, queue.JobStatusCancelled:
failed++
}
}
// Stats display with better formatting
var statsText string
if len(jobs) == 0 {
statsText = "Queue is empty"
} else {
statsText = fmt.Sprintf(" Total: %d | Running: %d | Pending: %d | Completed: %d | Failed: %d ",
len(jobs), running, pending, completed, failed)
}
statsLabel := widget.NewLabel(statsText)
statsLabel.Alignment = fyne.TextAlignCenter
// Job list // Job list
var jobItems []fyne.CanvasObject var jobItems []fyne.CanvasObject
if len(jobs) == 0 { if len(jobs) == 0 {
emptyMsg := widget.NewLabel("Drop videos on modules to add conversion jobs") emptyMsg := widget.NewLabel("No jobs in queue")
emptyMsg.Alignment = fyne.TextAlignCenter emptyMsg.Alignment = fyne.TextAlignCenter
jobItems = append(jobItems, container.NewCenter(emptyMsg)) jobItems = append(jobItems, container.NewCenter(emptyMsg))
} else { } else {
@ -109,10 +54,9 @@ func BuildQueueView(
jobList := container.NewVBox(jobItems...) jobList := container.NewVBox(jobItems...)
scrollable := container.NewVScroll(jobList) scrollable := container.NewVScroll(jobList)
// Create body with header, stats, and scrollable list
body := container.NewBorder( body := container.NewBorder(
header, header,
statsLabel, nil, nil, nil, nil, nil,
scrollable, scrollable,
) )
@ -131,31 +75,26 @@ func buildJobItem(
// Status color // Status color
statusColor := getStatusColor(job.Status) statusColor := getStatusColor(job.Status)
// Status indicator bar // Status indicator
statusRect := canvas.NewRectangle(statusColor) statusRect := canvas.NewRectangle(statusColor)
statusRect.SetMinSize(fyne.NewSize(4, 0)) statusRect.SetMinSize(fyne.NewSize(6, 0))
// Title with modified indicator // Title and description
titleText := job.Title titleLabel := widget.NewLabel(job.Title)
if job.HasModifiedSettings && job.Status == queue.JobStatusPending {
titleText = titleText + " ⚙ (custom settings)"
}
titleLabel := widget.NewLabel(titleText)
titleLabel.TextStyle = fyne.TextStyle{Bold: true} titleLabel.TextStyle = fyne.TextStyle{Bold: true}
// Description/output path
descLabel := widget.NewLabel(job.Description) descLabel := widget.NewLabel(job.Description)
descLabel.TextStyle = fyne.TextStyle{Italic: true} descLabel.TextStyle = fyne.TextStyle{Italic: true}
// Progress bar (for running/completed jobs) // Progress bar (for running jobs)
var progressWidget fyne.CanvasObject var progressWidget fyne.CanvasObject
if job.Status == queue.JobStatusRunning || job.Status == queue.JobStatusCompleted { if job.Status == queue.JobStatusRunning {
progress := widget.NewProgressBar() progress := widget.NewProgressBar()
if job.Status == queue.JobStatusRunning { progress.SetValue(job.Progress / 100.0)
progress.SetValue(job.Progress / 100.0) progressWidget = progress
} else { } else if job.Status == queue.JobStatusCompleted {
progress.SetValue(1.0) progress := widget.NewProgressBar()
} progress.SetValue(1.0)
progressWidget = progress progressWidget = progress
} else { } else {
progressWidget = widget.NewLabel("") progressWidget = widget.NewLabel("")
@ -166,40 +105,29 @@ func buildJobItem(
statusLabel := widget.NewLabel(statusText) statusLabel := widget.NewLabel(statusText)
statusLabel.TextStyle = fyne.TextStyle{Monospace: true} statusLabel.TextStyle = fyne.TextStyle{Monospace: true}
// Control buttons with status-appropriate styling // Control buttons
var buttons []fyne.CanvasObject var buttons []fyne.CanvasObject
switch job.Status { switch job.Status {
case queue.JobStatusRunning: case queue.JobStatusRunning:
pauseBtn := widget.NewButton("⏸ Pause", func() { onPause(job.ID) }) buttons = append(buttons,
pauseBtn.Importance = widget.MediumImportance widget.NewButton("Pause", func() { onPause(job.ID) }),
cancelBtn := widget.NewButton("⊗ Cancel", func() { onCancel(job.ID) }) widget.NewButton("Cancel", func() { onCancel(job.ID) }),
cancelBtn.Importance = widget.DangerImportance )
buttons = append(buttons, pauseBtn, cancelBtn)
case queue.JobStatusPaused: case queue.JobStatusPaused:
resumeBtn := widget.NewButton("▶ Resume", func() { onResume(job.ID) }) buttons = append(buttons,
resumeBtn.Importance = widget.MediumImportance widget.NewButton("Resume", func() { onResume(job.ID) }),
cancelBtn := widget.NewButton("⊗ Cancel", func() { onCancel(job.ID) }) widget.NewButton("Cancel", func() { onCancel(job.ID) }),
cancelBtn.Importance = widget.DangerImportance )
buttons = append(buttons, resumeBtn, cancelBtn)
case queue.JobStatusPending: case queue.JobStatusPending:
cancelBtn := widget.NewButton("⊗ Cancel", func() { onCancel(job.ID) }) buttons = append(buttons,
cancelBtn.Importance = widget.DangerImportance widget.NewButton("Cancel", func() { onCancel(job.ID) }),
buttons = append(buttons, cancelBtn) )
case queue.JobStatusCompleted: case queue.JobStatusCompleted, queue.JobStatusFailed, queue.JobStatusCancelled:
removeBtn := widget.NewButton("✓ Remove", func() { onRemove(job.ID) }) buttons = append(buttons,
removeBtn.Importance = widget.LowImportance widget.NewButton("Remove", func() { onRemove(job.ID) }),
buttons = append(buttons, removeBtn) )
case queue.JobStatusFailed:
removeBtn := widget.NewButton("✗ Remove", func() { onRemove(job.ID) })
removeBtn.Importance = widget.LowImportance
buttons = append(buttons, removeBtn)
case queue.JobStatusCancelled:
removeBtn := widget.NewButton("⊗ Remove", func() { onRemove(job.ID) })
removeBtn.Importance = widget.LowImportance
buttons = append(buttons, removeBtn)
} }
// Layout buttons in a responsive way
buttonBox := container.NewHBox(buttons...) buttonBox := container.NewHBox(buttons...)
// Info section // Info section
@ -210,7 +138,7 @@ func buildJobItem(
statusLabel, statusLabel,
) )
// Main content with borders // Main content
content := container.NewBorder( content := container.NewBorder(
nil, nil, nil, nil,
statusRect, statusRect,
@ -218,7 +146,7 @@ func buildJobItem(
infoBox, infoBox,
) )
// Card background with padding // Card background
card := canvas.NewRectangle(bgColor) card := canvas.NewRectangle(bgColor)
card.CornerRadius = 4 card.CornerRadius = 4

301
main.go
View File

@ -151,31 +151,30 @@ func (c convertConfig) CoverLabel() string {
} }
type appState struct { type appState struct {
window fyne.Window window fyne.Window
active string active string
initComplete bool // Track if initialization is complete source *videoSource
source *videoSource loadedVideos []*videoSource // Multiple loaded videos for navigation
loadedVideos []*videoSource // Multiple loaded videos for navigation currentIndex int // Current video index in loadedVideos
currentIndex int // Current video index in loadedVideos anim *previewAnimator
anim *previewAnimator convert convertConfig
convert convertConfig currentFrame string
currentFrame string player player.Controller
player player.Controller playerReady bool
playerReady bool playerVolume float64
playerVolume float64 playerMuted bool
playerMuted bool lastVolume float64
lastVolume float64 playerPaused bool
playerPaused bool playerPos float64
playerPos float64 playerLast time.Time
playerLast time.Time progressQuit chan struct{}
progressQuit chan struct{} convertCancel context.CancelFunc
convertCancel context.CancelFunc playerSurf *playerSurface
playerSurf *playerSurface convertBusy bool
convertBusy bool convertStatus string
convertStatus string playSess *playSession
playSess *playSession jobQueue *queue.Queue
jobQueue *queue.Queue statsBar *ui.ConversionStatsBar
statsBar *ui.ConversionStatsBar
} }
func (s *appState) stopPreview() { func (s *appState) stopPreview() {
@ -309,36 +308,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 {
// Only use DoFromGoroutine if initialization is complete and we might be on a goroutine s.window.SetContent(bg)
// During early initialization, always call directly since we're on the main thread return
if !s.initComplete {
// During initialization, call directly (we're on main thread)
if body == nil {
s.window.SetContent(bg)
} else {
s.window.SetContent(container.NewMax(bg, body))
}
} else {
// After initialization, use DoFromGoroutine to be safe
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 driver not available
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
@ -450,14 +424,6 @@ func (s *appState) showQueue() {
s.jobQueue.Clear() s.jobQueue.Clear()
s.showQueue() // Refresh s.showQueue() // Refresh
}, },
func() { // onClearAll
s.jobQueue.ClearAll()
s.showQueue() // Refresh
},
func() { // onProcess
s.jobQueue.ResumeProcessing()
logging.Debug(logging.CatSystem, "queue processing started")
},
utils.MustHex("#4CE870"), // titleColor utils.MustHex("#4CE870"), // titleColor
gridColor, // bgColor gridColor, // bgColor
textColor, // textColor textColor, // textColor
@ -466,102 +432,6 @@ func (s *appState) showQueue() {
s.setContent(container.NewPadded(view)) s.setContent(container.NewPadded(view))
} }
// hasModifiedConvertSettings checks if the current conversion settings differ from defaults
func (s *appState) hasModifiedConvertSettings() bool {
cfg := s.convert
// Check if any non-default values are set
if cfg.OutputBase != "" && cfg.OutputBase != "converted" {
return true
}
if cfg.Quality != "" && cfg.Quality != "Standard (CRF 23)" {
return true
}
if cfg.VideoCodec != "" && cfg.VideoCodec != "H.264" {
return true
}
if cfg.EncoderPreset != "" && cfg.EncoderPreset != "medium" {
return true
}
if cfg.CRF != "" {
return true
}
if cfg.BitrateMode != "" && cfg.BitrateMode != "CRF" {
return true
}
if cfg.VideoBitrate != "" {
return true
}
if cfg.TargetResolution != "" && cfg.TargetResolution != "Source" {
return true
}
if cfg.FrameRate != "" && cfg.FrameRate != "Source" {
return true
}
if cfg.PixelFormat != "" && cfg.PixelFormat != "yuv420p" {
return true
}
if cfg.HardwareAccel != "" && cfg.HardwareAccel != "none" {
return true
}
if cfg.TwoPass {
return true
}
if cfg.AudioCodec != "" && cfg.AudioCodec != "AAC" {
return true
}
if cfg.AudioBitrate != "" && cfg.AudioBitrate != "192k" {
return true
}
if cfg.AudioChannels != "" && cfg.AudioChannels != "Source" {
return true
}
if cfg.InverseTelecine {
return true
}
if cfg.CoverArtPath != "" {
return true
}
if cfg.AspectHandling != "" && cfg.AspectHandling != "Auto" {
return true
}
if cfg.OutputAspect != "" && cfg.OutputAspect != "Source" {
return true
}
return false
}
// removeQueuedConvertJob removes pending conversion jobs for the current video
func (s *appState) removeQueuedConvertJob() error {
if s.source == nil {
return fmt.Errorf("no video loaded")
}
jobs := s.jobQueue.List()
removed := 0
// Remove all pending convert jobs for this video
for _, job := range jobs {
if job.Type == queue.JobTypeConvert && job.Status == queue.JobStatusPending {
if inputPath, ok := job.Config["inputPath"].(string); ok && inputPath == s.source.Path {
if err := s.jobQueue.Remove(job.ID); err != nil {
logging.Debug(logging.CatSystem, "failed to remove job %s: %v", job.ID, err)
} else {
removed++
}
}
}
}
if removed == 0 {
return fmt.Errorf("no pending conversion jobs found for this video")
}
logging.Debug(logging.CatSystem, "removed %d conversion jobs from queue for: %s", removed, s.source.Path)
return nil
}
// addConvertToQueue adds a conversion job to the queue // addConvertToQueue adds a conversion job to the queue
func (s *appState) addConvertToQueue() error { func (s *appState) addConvertToQueue() error {
if s.source == nil { if s.source == nil {
@ -611,14 +481,13 @@ func (s *appState) addConvertToQueue() error {
} }
job := &queue.Job{ job := &queue.Job{
Type: queue.JobTypeConvert, Type: queue.JobTypeConvert,
Title: fmt.Sprintf("Convert %s", filepath.Base(src.Path)), Title: fmt.Sprintf("Convert %s", filepath.Base(src.Path)),
Description: fmt.Sprintf("Output: %s → %s", filepath.Base(src.Path), filepath.Base(outPath)), Description: fmt.Sprintf("Output: %s → %s", filepath.Base(src.Path), filepath.Base(outPath)),
InputFile: src.Path, InputFile: src.Path,
OutputFile: outPath, OutputFile: outPath,
Config: config, Config: config,
Priority: 0, Priority: 0,
HasModifiedSettings: s.hasModifiedConvertSettings(),
} }
s.jobQueue.Add(job) s.jobQueue.Add(job)
@ -782,14 +651,13 @@ func (s *appState) batchAddToQueue(paths []string) {
} }
job := &queue.Job{ job := &queue.Job{
Type: queue.JobTypeConvert, Type: queue.JobTypeConvert,
Title: fmt.Sprintf("Convert %s", filepath.Base(path)), Title: fmt.Sprintf("Convert %s", filepath.Base(path)),
Description: fmt.Sprintf("Output: %s → %s", filepath.Base(path), filepath.Base(outPath)), Description: fmt.Sprintf("Output: %s → %s", filepath.Base(path), filepath.Base(outPath)),
InputFile: path, InputFile: path,
OutputFile: outPath, OutputFile: outPath,
Config: config, Config: config,
Priority: 0, Priority: 0,
HasModifiedSettings: s.hasModifiedConvertSettings(),
} }
s.jobQueue.Add(job) s.jobQueue.Add(job)
@ -1094,10 +962,13 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
} }
func (s *appState) shutdown() { func (s *appState) shutdown() {
// Queue is not persisted between sessions - start fresh each time // Save queue before shutting down
if s.jobQueue != nil { if s.jobQueue != nil {
s.jobQueue.Stop() s.jobQueue.Stop()
// Don't save the queue - we want a clean slate each session queuePath := filepath.Join(os.TempDir(), "videotools-queue.json")
if err := s.jobQueue.Save(queuePath); err != nil {
logging.Debug(logging.CatSystem, "failed to save queue: %v", err)
}
} }
s.stopPlayer() s.stopPlayer()
@ -1165,10 +1036,7 @@ func runGUI() {
logging.Debug(logging.CatUI, "window initialized at 1120x640") logging.Debug(logging.CatUI, "window initialized at 1120x640")
state := &appState{ state := &appState{
window: w, window: w,
source: nil, // Start with no video source
loadedVideos: nil, // Start with no videos loaded
currentIndex: 0,
convert: convertConfig{ convert: convertConfig{
OutputBase: "converted", OutputBase: "converted",
SelectedFormat: formatOptions[0], SelectedFormat: formatOptions[0],
@ -1213,11 +1081,23 @@ func runGUI() {
// Initialize job queue // Initialize job queue
state.jobQueue = queue.New(state.jobExecutor) state.jobQueue = queue.New(state.jobExecutor)
state.jobQueue.SetChangeCallback(func() {
// Update stats bar
state.updateStatsBar()
// Start with a clean queue for each session // Refresh UI when queue changes
// Queue will be populated as user adds videos via drag-and-drop if state.active == "" {
// Queue is saved to disk on shutdown but not loaded on startup state.showMainMenu()
// Start the queue but keep it paused by default - only process when user explicitly requests }
})
// Load saved queue
queuePath := filepath.Join(os.TempDir(), "videotools-queue.json")
if err := state.jobQueue.Load(queuePath); err != nil {
logging.Debug(logging.CatSystem, "failed to load queue: %v", err)
}
// Start queue processing
state.jobQueue.Start() state.jobQueue.Start()
defer state.shutdown() defer state.shutdown()
@ -1226,36 +1106,6 @@ func runGUI() {
}) })
state.showMainMenu() state.showMainMenu()
logging.Debug(logging.CatUI, "main menu rendered with %d modules", len(modulesList)) logging.Debug(logging.CatUI, "main menu rendered with %d modules", len(modulesList))
// Set queue change callback AFTER window is shown to avoid threading issues during startup
// Use a small delay to ensure everything is fully initialized
go func() {
time.Sleep(100 * time.Millisecond)
state.jobQueue.SetChangeCallback(func() {
// Only handle queue changes after initialization is complete
if !state.initComplete {
return
}
// Queue callbacks come from goroutines, so wrap UI calls
app := fyne.CurrentApp()
if app == nil || app.Driver() == nil {
return
}
app.Driver().DoFromGoroutine(func() {
// Update stats bar
state.updateStatsBar()
// Refresh UI when queue changes
if state.active == "" {
state.showMainMenu()
}
}, false)
})
state.initComplete = true
}()
w.ShowAndRun() w.ShowAndRun()
} }
@ -1785,31 +1635,18 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
cancelBtn.Importance = widget.DangerImportance cancelBtn.Importance = widget.DangerImportance
cancelBtn.Disable() cancelBtn.Disable()
// Queue buttons - Add and Remove // Add to Queue button
addQueueBtn := widget.NewButton("+ Add", func() { addQueueBtn := widget.NewButton("Add to Queue", func() {
if err := state.addConvertToQueue(); err != nil { if err := state.addConvertToQueue(); err != nil {
dialog.ShowError(err, state.window) dialog.ShowError(err, state.window)
} else { } else {
dialog.ShowInformation("Queue", "Job added to queue!", state.window) dialog.ShowInformation("Queue", "Job added to queue!", state.window)
} }
}) })
addQueueBtn.Importance = widget.MediumImportance
if src == nil { if src == nil {
addQueueBtn.Disable() addQueueBtn.Disable()
} }
removeQueueBtn := widget.NewButton("- Remove", func() {
if err := state.removeQueuedConvertJob(); err != nil {
dialog.ShowError(err, state.window)
} else {
dialog.ShowInformation("Queue", "Job removed from queue!", state.window)
}
})
removeQueueBtn.Importance = widget.MediumImportance
if src == nil {
removeQueueBtn.Disable()
}
convertBtn = widget.NewButton("CONVERT NOW", func() { convertBtn = widget.NewButton("CONVERT NOW", func() {
state.startConvert(statusLabel, convertBtn, cancelBtn, activity) state.startConvert(statusLabel, convertBtn, cancelBtn, activity)
}) })
@ -1821,13 +1658,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
convertBtn.Disable() convertBtn.Disable()
cancelBtn.Enable() cancelBtn.Enable()
addQueueBtn.Disable() addQueueBtn.Disable()
removeQueueBtn.Disable()
} }
// Queue management container actionInner := container.NewHBox(resetBtn, activity, statusLabel, layout.NewSpacer(), cancelBtn, addQueueBtn, convertBtn)
queueBox := container.NewHBox(addQueueBtn, removeQueueBtn)
actionInner := container.NewHBox(resetBtn, activity, statusLabel, layout.NewSpacer(), cancelBtn, queueBox, convertBtn)
actionBar := ui.TintedBar(convertColor, actionInner) actionBar := ui.TintedBar(convertColor, actionInner)
// 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