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:
parent
d785e4dc91
commit
385c6f736d
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
29
main.go
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user