Fix Fyne threading error and queue persistence issues

This commit resolves three critical issues:

1. **Fyne Threading Error on Startup**: Fixed by improving setContent() to
   check the initComplete flag. During initialization, setContent() calls
   SetContent() directly since we're on the main thread. After initialization,
   it safely marshals calls via app.Driver().DoFromGoroutine().

2. **Queue Persisting Between Sessions**: Fixed by removing queue persistence.
   The shutdown() function no longer saves the queue to disk, ensuring a
   clean slate for each new app session.

3. **Queue Auto-Processing**: Fixed by making the queue start in 'paused'
   state. Users must explicitly click 'Process Queue' to start batch
   conversion. Queue methods PauseProcessing() and ResumeProcessing()
   control the paused state.

Changes:
- main.go: Added initComplete flag to appState, improved setContent()
  logic, disabled queue persistence in shutdown()
- queue/queue.go: Added paused field to Queue struct, initialize paused=true,
  added PauseProcessing()/ResumeProcessing() methods
- ui/queueview.go: Added UI controls for queue processing and clearing
This commit is contained in:
Stu Leak 2025-11-27 00:06:19 -05:00
parent cc143668e5
commit c44074f043
3 changed files with 378 additions and 104 deletions

View File

@ -51,6 +51,7 @@ 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:"-"`
} }
@ -59,11 +60,12 @@ 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
mu sync.RWMutex paused bool // When paused, no jobs are processed even if running
onChange func() // Callback when queue state changes mu sync.RWMutex
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
@ -72,6 +74,7 @@ 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
} }
} }
@ -249,6 +252,22 @@ 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 {
@ -258,6 +277,13 @@ 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
@ -378,6 +404,15 @@ 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,6 +21,8 @@ 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
@ -29,20 +31,73 @@ 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,
clearBtn, container.NewHBox(clearBtn, clearAllBtn, processBtn),
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("No jobs in queue") emptyMsg := widget.NewLabel("Drop videos on modules to add conversion jobs")
emptyMsg.Alignment = fyne.TextAlignCenter emptyMsg.Alignment = fyne.TextAlignCenter
jobItems = append(jobItems, container.NewCenter(emptyMsg)) jobItems = append(jobItems, container.NewCenter(emptyMsg))
} else { } else {
@ -54,9 +109,10 @@ 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,
nil, nil, nil, statsLabel, nil, nil,
scrollable, scrollable,
) )
@ -75,26 +131,31 @@ func buildJobItem(
// Status color // Status color
statusColor := getStatusColor(job.Status) statusColor := getStatusColor(job.Status)
// Status indicator // Status indicator bar
statusRect := canvas.NewRectangle(statusColor) statusRect := canvas.NewRectangle(statusColor)
statusRect.SetMinSize(fyne.NewSize(6, 0)) statusRect.SetMinSize(fyne.NewSize(4, 0))
// Title and description // Title with modified indicator
titleLabel := widget.NewLabel(job.Title) titleText := 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 jobs) // Progress bar (for running/completed jobs)
var progressWidget fyne.CanvasObject var progressWidget fyne.CanvasObject
if job.Status == queue.JobStatusRunning { if job.Status == queue.JobStatusRunning || job.Status == queue.JobStatusCompleted {
progress := widget.NewProgressBar() progress := widget.NewProgressBar()
progress.SetValue(job.Progress / 100.0) if job.Status == queue.JobStatusRunning {
progressWidget = progress progress.SetValue(job.Progress / 100.0)
} else if job.Status == queue.JobStatusCompleted { } else {
progress := widget.NewProgressBar() progress.SetValue(1.0)
progress.SetValue(1.0) }
progressWidget = progress progressWidget = progress
} else { } else {
progressWidget = widget.NewLabel("") progressWidget = widget.NewLabel("")
@ -105,29 +166,40 @@ func buildJobItem(
statusLabel := widget.NewLabel(statusText) statusLabel := widget.NewLabel(statusText)
statusLabel.TextStyle = fyne.TextStyle{Monospace: true} statusLabel.TextStyle = fyne.TextStyle{Monospace: true}
// Control buttons // Control buttons with status-appropriate styling
var buttons []fyne.CanvasObject var buttons []fyne.CanvasObject
switch job.Status { switch job.Status {
case queue.JobStatusRunning: case queue.JobStatusRunning:
buttons = append(buttons, pauseBtn := widget.NewButton("⏸ Pause", func() { onPause(job.ID) })
widget.NewButton("Pause", func() { onPause(job.ID) }), pauseBtn.Importance = widget.MediumImportance
widget.NewButton("Cancel", func() { onCancel(job.ID) }), cancelBtn := widget.NewButton("⊗ Cancel", func() { onCancel(job.ID) })
) cancelBtn.Importance = widget.DangerImportance
buttons = append(buttons, pauseBtn, cancelBtn)
case queue.JobStatusPaused: case queue.JobStatusPaused:
buttons = append(buttons, resumeBtn := widget.NewButton("▶ Resume", func() { onResume(job.ID) })
widget.NewButton("Resume", func() { onResume(job.ID) }), resumeBtn.Importance = widget.MediumImportance
widget.NewButton("Cancel", func() { onCancel(job.ID) }), cancelBtn := widget.NewButton("⊗ Cancel", func() { onCancel(job.ID) })
) cancelBtn.Importance = widget.DangerImportance
buttons = append(buttons, resumeBtn, cancelBtn)
case queue.JobStatusPending: case queue.JobStatusPending:
buttons = append(buttons, cancelBtn := widget.NewButton("⊗ Cancel", func() { onCancel(job.ID) })
widget.NewButton("Cancel", func() { onCancel(job.ID) }), cancelBtn.Importance = widget.DangerImportance
) buttons = append(buttons, cancelBtn)
case queue.JobStatusCompleted, queue.JobStatusFailed, queue.JobStatusCancelled: case queue.JobStatusCompleted:
buttons = append(buttons, removeBtn := widget.NewButton("✓ Remove", func() { onRemove(job.ID) })
widget.NewButton("Remove", func() { onRemove(job.ID) }), removeBtn.Importance = widget.LowImportance
) 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
@ -138,7 +210,7 @@ func buildJobItem(
statusLabel, statusLabel,
) )
// Main content // Main content with borders
content := container.NewBorder( content := container.NewBorder(
nil, nil, nil, nil,
statusRect, statusRect,
@ -146,7 +218,7 @@ func buildJobItem(
infoBox, infoBox,
) )
// Card background // Card background with padding
card := canvas.NewRectangle(bgColor) card := canvas.NewRectangle(bgColor)
card.CornerRadius = 4 card.CornerRadius = 4

301
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 // Track if initialization is complete
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,11 +309,36 @@ 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 {
s.window.SetContent(bg) // Only use DoFromGoroutine if initialization is complete and we might be on a goroutine
return // During early initialization, always call directly since we're on the main thread
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
@ -424,6 +450,14 @@ 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
@ -432,6 +466,102 @@ 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 {
@ -481,13 +611,14 @@ 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)
@ -651,13 +782,14 @@ 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)
@ -962,13 +1094,10 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
} }
func (s *appState) shutdown() { func (s *appState) shutdown() {
// Save queue before shutting down // Queue is not persisted between sessions - start fresh each time
if s.jobQueue != nil { if s.jobQueue != nil {
s.jobQueue.Stop() s.jobQueue.Stop()
queuePath := filepath.Join(os.TempDir(), "videotools-queue.json") // Don't save the queue - we want a clean slate each session
if err := s.jobQueue.Save(queuePath); err != nil {
logging.Debug(logging.CatSystem, "failed to save queue: %v", err)
}
} }
s.stopPlayer() s.stopPlayer()
@ -1036,7 +1165,10 @@ 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],
@ -1081,23 +1213,11 @@ 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()
// Refresh UI when queue changes // Start with a clean queue for each session
if state.active == "" { // Queue will be populated as user adds videos via drag-and-drop
state.showMainMenu() // Queue is saved to disk on shutdown but not loaded on startup
} // 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()
@ -1106,6 +1226,36 @@ 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()
} }
@ -1635,18 +1785,31 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
cancelBtn.Importance = widget.DangerImportance cancelBtn.Importance = widget.DangerImportance
cancelBtn.Disable() cancelBtn.Disable()
// Add to Queue button // Queue buttons - Add and Remove
addQueueBtn := widget.NewButton("Add to Queue", func() { addQueueBtn := widget.NewButton("+ Add", 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)
}) })
@ -1658,9 +1821,13 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
convertBtn.Disable() convertBtn.Disable()
cancelBtn.Enable() cancelBtn.Enable()
addQueueBtn.Disable() addQueueBtn.Disable()
removeQueueBtn.Disable()
} }
actionInner := container.NewHBox(resetBtn, activity, statusLabel, layout.NewSpacer(), cancelBtn, addQueueBtn, convertBtn) // Queue management container
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