Phase 4: Create sidebar UI components

Added history sidebar UI with tabs for completed and failed jobs.
Created reusable UI components and helpers for displaying history entries.

Changes:
- internal/ui/mainmenu.go:
  * Added HistoryEntry type definition
  * Added BuildHistorySidebar() for main sidebar UI with tabs
  * Added buildHistoryList() and buildHistoryItem() helpers
  * Added imports for queue and utils packages

- internal/ui/components.go:
  * Moved GetStatusColor() and BuildModuleBadge() here as shared functions
  * Added queue and utils imports for shared helpers

- internal/ui/queueview.go:
  * Updated to use shared GetStatusColor() and BuildModuleBadge()
  * Removed duplicate function definitions

- main.go:
  * Updated to use ui.HistoryEntry type throughout
  * Updated historyConfig, appState, and all methods to use ui.HistoryEntry

The sidebar displays history entries with:
- Status-colored indicators (green/red/orange)
- Module type badges with colors
- Shortened titles and formatted timestamps
- Separate tabs for "Completed" and "Failed" (includes cancelled)
- Empty state messages when no entries exist

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Stu Leak 2025-12-17 19:34:22 -05:00
parent d785e4dc91
commit 385c6f736d
4 changed files with 191 additions and 58 deletions

View File

@ -15,6 +15,8 @@ import (
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"git.leaktechnologies.dev/stu/VideoTools/internal/logging"
"git.leaktechnologies.dev/stu/VideoTools/internal/queue"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
)
var (
@ -707,3 +709,64 @@ func (w *FFmpegCommandWidget) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(content)
}
// GetStatusColor returns the color for a job status
func GetStatusColor(status queue.JobStatus) color.Color {
switch status {
case queue.JobStatusCompleted:
return utils.MustHex("#4CAF50") // Green
case queue.JobStatusFailed:
return utils.MustHex("#F44336") // Red
case queue.JobStatusCancelled:
return utils.MustHex("#FF9800") // Orange
default:
return utils.MustHex("#808080") // Gray
}
}
// BuildModuleBadge creates a small colored badge for the job type
func BuildModuleBadge(jobType queue.JobType) fyne.CanvasObject {
var badgeColor color.Color
var badgeText string
switch jobType {
case queue.JobTypeConvert:
badgeColor = utils.MustHex("#4A90E2")
badgeText = "CONVERT"
case queue.JobTypeMerge:
badgeColor = utils.MustHex("#E24A90")
badgeText = "MERGE"
case queue.JobTypeTrim:
badgeColor = utils.MustHex("#90E24A")
badgeText = "TRIM"
case queue.JobTypeFilter:
badgeColor = utils.MustHex("#E2904A")
badgeText = "FILTER"
case queue.JobTypeUpscale:
badgeColor = utils.MustHex("#9A4AE2")
badgeText = "UPSCALE"
case queue.JobTypeAudio:
badgeColor = utils.MustHex("#4AE290")
badgeText = "AUDIO"
case queue.JobTypeThumb:
badgeColor = utils.MustHex("#E2E24A")
badgeText = "THUMB"
case queue.JobTypeSnippet:
badgeColor = utils.MustHex("#4AE2E2")
badgeText = "SNIPPET"
default:
badgeColor = utils.MustHex("#808080")
badgeText = "OTHER"
}
rect := canvas.NewRectangle(badgeColor)
rect.CornerRadius = 3
rect.SetMinSize(fyne.NewSize(70, 20))
text := canvas.NewText(badgeText, color.White)
text.Alignment = fyne.TextAlignCenter
text.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
text.TextSize = 10
return container.NewMax(rect, container.NewCenter(text))
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"image/color"
"sort"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
@ -11,6 +12,8 @@ import (
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"git.leaktechnologies.dev/stu/VideoTools/internal/logging"
"git.leaktechnologies.dev/stu/VideoTools/internal/queue"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
)
// ModuleInfo contains information about a module for display
@ -22,6 +25,23 @@ type ModuleInfo struct {
Category string
}
// HistoryEntry represents a completed job in the history
type HistoryEntry struct {
ID string
Type queue.JobType
Status queue.JobStatus
Title string
InputFile string
OutputFile string
LogPath string
Config map[string]interface{}
CreatedAt time.Time
StartedAt *time.Time
CompletedAt *time.Time
Error string
FFmpegCmd string
}
// BuildMainMenu creates the main menu view with module tiles grouped by category
func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDrop func(string, []fyne.URI), onQueueClick func(), onLogsClick func(), onBenchmarkClick func(), onBenchmarkHistoryClick func(), titleColor, queueColor, textColor color.Color, queueCompleted, queueTotal int) fyne.CanvasObject {
title := canvas.NewText("VIDEOTOOLS", titleColor)
@ -119,3 +139,103 @@ func sortedKeys(m map[string][]fyne.CanvasObject) []string {
sort.Strings(keys)
return keys
}
// BuildHistorySidebar creates the history sidebar with tabs
func BuildHistorySidebar(
entries []HistoryEntry,
onEntryClick func(HistoryEntry),
titleColor, bgColor, textColor color.Color,
) fyne.CanvasObject {
// Filter by status
var completedEntries, failedEntries []HistoryEntry
for _, entry := range entries {
if entry.Status == queue.JobStatusCompleted {
completedEntries = append(completedEntries, entry)
} else {
failedEntries = append(failedEntries, entry)
}
}
// Build lists
completedList := buildHistoryList(completedEntries, onEntryClick, bgColor, textColor)
failedList := buildHistoryList(failedEntries, onEntryClick, bgColor, textColor)
// Tabs
tabs := container.NewAppTabs(
container.NewTabItem("Completed", container.NewVScroll(completedList)),
container.NewTabItem("Failed", container.NewVScroll(failedList)),
)
tabs.SetTabLocation(container.TabLocationTop)
// Header
title := canvas.NewText("HISTORY", titleColor)
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
title.TextSize = 18
header := container.NewVBox(
container.NewCenter(title),
widget.NewSeparator(),
)
return container.NewBorder(header, nil, nil, nil, tabs)
}
func buildHistoryList(
entries []HistoryEntry,
onEntryClick func(HistoryEntry),
bgColor, textColor color.Color,
) *fyne.Container {
if len(entries) == 0 {
return container.NewCenter(widget.NewLabel("No entries"))
}
var items []fyne.CanvasObject
for _, entry := range entries {
items = append(items, buildHistoryItem(entry, onEntryClick, bgColor, textColor))
}
return container.NewVBox(items...)
}
func buildHistoryItem(
entry HistoryEntry,
onEntryClick func(HistoryEntry),
bgColor, textColor color.Color,
) fyne.CanvasObject {
// Badge
badge := BuildModuleBadge(entry.Type)
// Title
titleLabel := widget.NewLabel(utils.ShortenMiddle(entry.Title, 25))
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
// Timestamp
timeStr := "Unknown"
if entry.CompletedAt != nil {
timeStr = entry.CompletedAt.Format("Jan 2, 15:04")
}
timeLabel := widget.NewLabel(timeStr)
timeLabel.TextStyle = fyne.TextStyle{Monospace: true}
// Status color bar
statusColor := GetStatusColor(entry.Status)
statusRect := canvas.NewRectangle(statusColor)
statusRect.SetMinSize(fyne.NewSize(4, 0))
content := container.NewBorder(
nil, nil, statusRect, nil,
container.NewVBox(
container.NewHBox(badge, layout.NewSpacer()),
titleLabel,
timeLabel,
),
)
card := canvas.NewRectangle(bgColor)
card.CornerRadius = 4
item := container.NewPadded(container.NewMax(card, content))
// Capture entry for closure
capturedEntry := entry
return NewTappable(item, func() { onEntryClick(capturedEntry) })
}

View File

@ -216,7 +216,7 @@ func buildJobItem(
bgColor, textColor color.Color,
) fyne.CanvasObject {
// Status color
statusColor := getStatusColor(job.Status)
statusColor := GetStatusColor(job.Status)
// Status indicator
statusRect := canvas.NewRectangle(statusColor)
@ -242,7 +242,7 @@ func buildJobItem(
progressWidget := progress
// Module badge
badge := buildModuleBadge(job.Type)
badge := BuildModuleBadge(job.Type)
// Status text
statusText := getStatusText(job)
@ -329,26 +329,6 @@ func buildJobItem(
})
}
// 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 {
@ -398,19 +378,6 @@ func getStatusText(job *queue.Job) string {
}
}
// buildModuleBadge renders a small colored pill to show which module created the job.
func buildModuleBadge(t queue.JobType) fyne.CanvasObject {
label := widget.NewLabel(string(t))
label.TextStyle = fyne.TextStyle{Bold: true}
label.Alignment = fyne.TextAlignCenter
bg := canvas.NewRectangle(moduleColor(t))
bg.CornerRadius = 6
bg.SetMinSize(fyne.NewSize(label.MinSize().Width+12, label.MinSize().Height+6))
return container.NewMax(bg, container.NewCenter(label))
}
// moduleColor maps job types to distinct colors matching the main module colors
func moduleColor(t queue.JobType) color.Color {
switch t {

29
main.go
View File

@ -593,26 +593,9 @@ func saveBenchmarkConfig(cfg benchmarkConfig) error {
return os.WriteFile(path, data, 0o644)
}
// HistoryEntry represents a completed job in the history
type HistoryEntry struct {
ID string `json:"id"`
Type queue.JobType `json:"type"`
Status queue.JobStatus `json:"status"`
Title string `json:"title"`
InputFile string `json:"input_file"`
OutputFile string `json:"output_file"`
LogPath string `json:"log_path,omitempty"`
Config map[string]interface{} `json:"config"`
CreatedAt time.Time `json:"created_at"`
StartedAt *time.Time `json:"started_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
Error string `json:"error,omitempty"`
FFmpegCmd string `json:"ffmpeg_cmd,omitempty"`
}
// historyConfig holds conversion history
type historyConfig struct {
Entries []HistoryEntry `json:"entries"`
Entries []ui.HistoryEntry `json:"entries"`
}
func historyConfigPath() string {
@ -634,7 +617,7 @@ func loadHistoryConfig() (historyConfig, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return historyConfig{Entries: []HistoryEntry{}}, nil
return historyConfig{Entries: []ui.HistoryEntry{}}, nil
}
return historyConfig{}, err
}
@ -767,7 +750,7 @@ type appState struct {
interlaceAnalyzing bool
// History sidebar state
historyEntries []HistoryEntry
historyEntries []ui.HistoryEntry
sidebarVisible bool
}
@ -799,7 +782,7 @@ func (s *appState) addToHistory(job *queue.Job) {
// Build FFmpeg command from job config
cmdStr := buildFFmpegCommandFromJob(job)
entry := HistoryEntry{
entry := ui.HistoryEntry{
ID: job.ID,
Type: job.Type,
Status: job.Status,
@ -823,7 +806,7 @@ func (s *appState) addToHistory(job *queue.Job) {
}
// Prepend to history (newest first)
s.historyEntries = append([]HistoryEntry{entry}, s.historyEntries...)
s.historyEntries = append([]ui.HistoryEntry{entry}, s.historyEntries...)
// Save to disk
cfg := historyConfig{Entries: s.historyEntries}
@ -4716,7 +4699,7 @@ func runGUI() {
if historyCfg, err := loadHistoryConfig(); err == nil {
state.historyEntries = historyCfg.Entries
} else {
state.historyEntries = []HistoryEntry{}
state.historyEntries = []ui.HistoryEntry{}
logging.Debug(logging.CatSystem, "failed to load history config: %v", err)
}
state.sidebarVisible = false