diff --git a/internal/ui/components.go b/internal/ui/components.go index 2935e75..ff634b2 100644 --- a/internal/ui/components.go +++ b/internal/ui/components.go @@ -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)) +} diff --git a/internal/ui/mainmenu.go b/internal/ui/mainmenu.go index d75a11b..0b5187f 100644 --- a/internal/ui/mainmenu.go +++ b/internal/ui/mainmenu.go @@ -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) }) +} diff --git a/internal/ui/queueview.go b/internal/ui/queueview.go index df3fd24..d527ec3 100644 --- a/internal/ui/queueview.go +++ b/internal/ui/queueview.go @@ -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 { diff --git a/main.go b/main.go index b95cae2..cc03dbd 100644 --- a/main.go +++ b/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