VT_Player/internal/ui/queueview.go
Stu Leak cfb608e191 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

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 00:06:19 -05:00

279 lines
7.8 KiB
Go

package ui
import (
"fmt"
"image/color"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
"git.leaktechnologies.dev/stu/VideoTools/internal/queue"
)
// BuildQueueView creates the queue viewer UI
func BuildQueueView(
jobs []*queue.Job,
onBack func(),
onPause func(string),
onResume func(string),
onCancel func(string),
onRemove func(string),
onClear func(),
onClearAll func(),
onProcess func(),
titleColor, bgColor, textColor color.Color,
) fyne.CanvasObject {
// Header
title := canvas.NewText("JOB QUEUE", titleColor)
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
title.TextSize = 24
backBtn := widget.NewButton("← Back", onBack)
backBtn.Importance = widget.LowImportance
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(
nil, nil,
backBtn,
container.NewHBox(clearBtn, clearAllBtn, processBtn),
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
var jobItems []fyne.CanvasObject
if len(jobs) == 0 {
emptyMsg := widget.NewLabel("Drop videos on modules to add conversion jobs")
emptyMsg.Alignment = fyne.TextAlignCenter
jobItems = append(jobItems, container.NewCenter(emptyMsg))
} else {
for _, job := range jobs {
jobItems = append(jobItems, buildJobItem(job, onPause, onResume, onCancel, onRemove, bgColor, textColor))
}
}
jobList := container.NewVBox(jobItems...)
scrollable := container.NewVScroll(jobList)
// Create body with header, stats, and scrollable list
body := container.NewBorder(
header,
statsLabel, nil, nil,
scrollable,
)
return container.NewPadded(body)
}
// buildJobItem creates a single job item in the queue list
func buildJobItem(
job *queue.Job,
onPause func(string),
onResume func(string),
onCancel func(string),
onRemove func(string),
bgColor, textColor color.Color,
) fyne.CanvasObject {
// Status color
statusColor := getStatusColor(job.Status)
// Status indicator bar
statusRect := canvas.NewRectangle(statusColor)
statusRect.SetMinSize(fyne.NewSize(4, 0))
// Title with modified indicator
titleText := job.Title
if job.HasModifiedSettings && job.Status == queue.JobStatusPending {
titleText = titleText + " ⚙ (custom settings)"
}
titleLabel := widget.NewLabel(titleText)
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
// Description/output path
descLabel := widget.NewLabel(job.Description)
descLabel.TextStyle = fyne.TextStyle{Italic: true}
// Progress bar (for running/completed jobs)
var progressWidget fyne.CanvasObject
if job.Status == queue.JobStatusRunning || job.Status == queue.JobStatusCompleted {
progress := widget.NewProgressBar()
if job.Status == queue.JobStatusRunning {
progress.SetValue(job.Progress / 100.0)
} else {
progress.SetValue(1.0)
}
progressWidget = progress
} else {
progressWidget = widget.NewLabel("")
}
// Status text
statusText := getStatusText(job)
statusLabel := widget.NewLabel(statusText)
statusLabel.TextStyle = fyne.TextStyle{Monospace: true}
// Control buttons with status-appropriate styling
var buttons []fyne.CanvasObject
switch job.Status {
case queue.JobStatusRunning:
pauseBtn := widget.NewButton("⏸ Pause", func() { onPause(job.ID) })
pauseBtn.Importance = widget.MediumImportance
cancelBtn := widget.NewButton("⊗ Cancel", func() { onCancel(job.ID) })
cancelBtn.Importance = widget.DangerImportance
buttons = append(buttons, pauseBtn, cancelBtn)
case queue.JobStatusPaused:
resumeBtn := widget.NewButton("▶ Resume", func() { onResume(job.ID) })
resumeBtn.Importance = widget.MediumImportance
cancelBtn := widget.NewButton("⊗ Cancel", func() { onCancel(job.ID) })
cancelBtn.Importance = widget.DangerImportance
buttons = append(buttons, resumeBtn, cancelBtn)
case queue.JobStatusPending:
cancelBtn := widget.NewButton("⊗ Cancel", func() { onCancel(job.ID) })
cancelBtn.Importance = widget.DangerImportance
buttons = append(buttons, cancelBtn)
case queue.JobStatusCompleted:
removeBtn := 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...)
// Info section
infoBox := container.NewVBox(
titleLabel,
descLabel,
progressWidget,
statusLabel,
)
// Main content with borders
content := container.NewBorder(
nil, nil,
statusRect,
buttonBox,
infoBox,
)
// Card background with padding
card := canvas.NewRectangle(bgColor)
card.CornerRadius = 4
item := container.NewPadded(
container.NewMax(card, content),
)
return item
}
// getStatusColor returns the color for a job status
func getStatusColor(status queue.JobStatus) color.Color {
switch status {
case queue.JobStatusPending:
return color.RGBA{R: 150, G: 150, B: 150, A: 255} // Gray
case queue.JobStatusRunning:
return color.RGBA{R: 68, G: 136, B: 255, A: 255} // Blue
case queue.JobStatusPaused:
return color.RGBA{R: 255, G: 193, B: 7, A: 255} // Yellow
case queue.JobStatusCompleted:
return color.RGBA{R: 76, G: 232, B: 112, A: 255} // Green
case queue.JobStatusFailed:
return color.RGBA{R: 255, G: 68, B: 68, A: 255} // Red
case queue.JobStatusCancelled:
return color.RGBA{R: 255, G: 136, B: 68, A: 255} // Orange
default:
return color.Gray{Y: 128}
}
}
// getStatusText returns a human-readable status string
func getStatusText(job *queue.Job) string {
switch job.Status {
case queue.JobStatusPending:
return fmt.Sprintf("Status: Pending | Priority: %d", job.Priority)
case queue.JobStatusRunning:
elapsed := ""
if job.StartedAt != nil {
elapsed = fmt.Sprintf(" | Elapsed: %s", time.Since(*job.StartedAt).Round(time.Second))
}
return fmt.Sprintf("Status: Running | Progress: %.1f%%%s", job.Progress, elapsed)
case queue.JobStatusPaused:
return "Status: Paused"
case queue.JobStatusCompleted:
duration := ""
if job.StartedAt != nil && job.CompletedAt != nil {
duration = fmt.Sprintf(" | Duration: %s", job.CompletedAt.Sub(*job.StartedAt).Round(time.Second))
}
return fmt.Sprintf("Status: Completed%s", duration)
case queue.JobStatusFailed:
return fmt.Sprintf("Status: Failed | Error: %s", job.Error)
case queue.JobStatusCancelled:
return "Status: Cancelled"
default:
return fmt.Sprintf("Status: %s", job.Status)
}
}