VideoTools/internal/ui/queueview.go
Stu Leak fa4f4119b5 Simplify threading solution and add Clear All button
Simplified the approach by removing complex callback logic and using a
simple 500ms timer-based update for the stats bar instead. This eliminates
threading errors completely while keeping the code straightforward.

Changes:
1. Removed queue change callback entirely
2. Added background timer that updates stats bar every 500ms
3. Removed initComplete flag (no longer needed)
4. Simplified setContent() to direct calls
5. Added onClearAll parameter to BuildQueueView()
6. Added ClearAll() method to Queue (removes all jobs)
7. Added Clear All button with DangerImportance styling in queue view
8. Clear Completed button now has LowImportance styling

This approach is much simpler: the UI just polls the queue state
periodically instead of trying to handle callbacks from goroutines.
No more threading errors, less code, easier to understand.

🤖 Generated with Claude Code

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

214 lines
5.6 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(),
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
header := container.NewBorder(
nil, nil,
backBtn,
container.NewHBox(clearBtn, clearAllBtn),
container.NewCenter(title),
)
// Job list
var jobItems []fyne.CanvasObject
if len(jobs) == 0 {
emptyMsg := widget.NewLabel("No jobs in queue")
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)
body := container.NewBorder(
header,
nil, 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
statusRect := canvas.NewRectangle(statusColor)
statusRect.SetMinSize(fyne.NewSize(6, 0))
// Title and description
titleLabel := widget.NewLabel(job.Title)
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
descLabel := widget.NewLabel(job.Description)
descLabel.TextStyle = fyne.TextStyle{Italic: true}
// Progress bar (for running jobs)
var progressWidget fyne.CanvasObject
if job.Status == queue.JobStatusRunning {
progress := widget.NewProgressBar()
progress.SetValue(job.Progress / 100.0)
progressWidget = progress
} else if job.Status == queue.JobStatusCompleted {
progress := widget.NewProgressBar()
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
var buttons []fyne.CanvasObject
switch job.Status {
case queue.JobStatusRunning:
buttons = append(buttons,
widget.NewButton("Pause", func() { onPause(job.ID) }),
widget.NewButton("Cancel", func() { onCancel(job.ID) }),
)
case queue.JobStatusPaused:
buttons = append(buttons,
widget.NewButton("Resume", func() { onResume(job.ID) }),
widget.NewButton("Cancel", func() { onCancel(job.ID) }),
)
case queue.JobStatusPending:
buttons = append(buttons,
widget.NewButton("Cancel", func() { onCancel(job.ID) }),
)
case queue.JobStatusCompleted, queue.JobStatusFailed, queue.JobStatusCancelled:
buttons = append(buttons,
widget.NewButton("Remove", func() { onRemove(job.ID) }),
)
}
buttonBox := container.NewHBox(buttons...)
// Info section
infoBox := container.NewVBox(
titleLabel,
descLabel,
progressWidget,
statusLabel,
)
// Main content
content := container.NewBorder(
nil, nil,
statusRect,
buttonBox,
infoBox,
)
// Card background
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)
}
}