Features: - New "In Progress" tab shows running/pending jobs - Displays active jobs without opening full queue - Tab positioned first for quick visibility - Shows "Running..." or "Pending" status - No delete button on active jobs (only completed/failed) Implementation: - Updated BuildHistorySidebar to accept activeJobs parameter - Converts queue.Job to ui.HistoryEntry for display - Filters running/pending jobs from queue - Conditional delete button (nil check) - Dynamic status text based on job state UX Improvements: - Quick glance at current activity without queue view - Three-tab layout: In Progress → Completed → Failed - Consistent styling with existing history entries - Tappable entries to view full job details This allows users to monitor active conversions directly from the history sidebar, reducing the need to constantly check the full job queue view.
282 lines
8.5 KiB
Go
282 lines
8.5 KiB
Go
package ui
|
||
|
||
import (
|
||
"fmt"
|
||
"image/color"
|
||
"sort"
|
||
"time"
|
||
|
||
"fyne.io/fyne/v2"
|
||
"fyne.io/fyne/v2/canvas"
|
||
"fyne.io/fyne/v2/container"
|
||
"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
|
||
type ModuleInfo struct {
|
||
ID string
|
||
Label string
|
||
Color color.Color
|
||
Enabled bool
|
||
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(), onToggleSidebar func(), sidebarVisible bool, sidebar fyne.CanvasObject, titleColor, queueColor, textColor color.Color, queueCompleted, queueTotal int) fyne.CanvasObject {
|
||
title := canvas.NewText("VIDEOTOOLS", titleColor)
|
||
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
||
title.TextSize = 28
|
||
|
||
queueTile := buildQueueTile(queueCompleted, queueTotal, queueColor, textColor, onQueueClick)
|
||
|
||
sidebarToggleBtn := widget.NewButton("☰ History", onToggleSidebar)
|
||
sidebarToggleBtn.Importance = widget.LowImportance
|
||
|
||
benchmarkBtn := widget.NewButton("Run Benchmark", onBenchmarkClick)
|
||
benchmarkBtn.Importance = widget.LowImportance
|
||
|
||
viewResultsBtn := widget.NewButton("View Results", onBenchmarkHistoryClick)
|
||
viewResultsBtn.Importance = widget.LowImportance
|
||
|
||
logsBtn := widget.NewButton("Logs", onLogsClick)
|
||
logsBtn.Importance = widget.LowImportance
|
||
|
||
header := container.New(layout.NewHBoxLayout(), title, layout.NewSpacer(), sidebarToggleBtn, benchmarkBtn, viewResultsBtn, logsBtn, queueTile)
|
||
|
||
categorized := map[string][]fyne.CanvasObject{}
|
||
for i := range modules {
|
||
mod := modules[i] // Create new variable for this iteration
|
||
modID := mod.ID // Capture for closure
|
||
cat := mod.Category
|
||
if cat == "" {
|
||
cat = "General"
|
||
}
|
||
var tapFunc func()
|
||
var dropFunc func([]fyne.URI)
|
||
if mod.Enabled {
|
||
// Create new closure with properly captured modID
|
||
id := modID // Explicit capture
|
||
tapFunc = func() {
|
||
onModuleClick(id)
|
||
}
|
||
dropFunc = func(items []fyne.URI) {
|
||
logging.Debug(logging.CatUI, "MainMenu dropFunc called for module=%s itemCount=%d", id, len(items))
|
||
onModuleDrop(id, items)
|
||
}
|
||
}
|
||
logging.Debug(logging.CatUI, "Creating tile for module=%s enabled=%v hasDropFunc=%v", modID, mod.Enabled, dropFunc != nil)
|
||
categorized[cat] = append(categorized[cat], buildModuleTile(mod, tapFunc, dropFunc))
|
||
}
|
||
|
||
var sections []fyne.CanvasObject
|
||
for _, cat := range sortedKeys(categorized) {
|
||
sections = append(sections,
|
||
canvas.NewText(cat, textColor),
|
||
container.NewGridWithColumns(3, categorized[cat]...),
|
||
)
|
||
}
|
||
|
||
padding := canvas.NewRectangle(color.Transparent)
|
||
padding.SetMinSize(fyne.NewSize(0, 14))
|
||
|
||
body := container.New(layout.NewVBoxLayout(),
|
||
header,
|
||
padding,
|
||
container.NewVBox(sections...),
|
||
)
|
||
|
||
// Wrap with HSplit if sidebar is visible
|
||
if sidebarVisible && sidebar != nil {
|
||
split := container.NewHSplit(sidebar, body)
|
||
split.Offset = 0.2
|
||
return split
|
||
}
|
||
|
||
return body
|
||
}
|
||
|
||
// buildModuleTile creates a single module tile
|
||
func buildModuleTile(mod ModuleInfo, tapped func(), dropped func([]fyne.URI)) fyne.CanvasObject {
|
||
logging.Debug(logging.CatUI, "building tile %s color=%v enabled=%v", mod.ID, mod.Color, mod.Enabled)
|
||
return container.NewPadded(NewModuleTile(mod.Label, mod.Color, mod.Enabled, tapped, dropped))
|
||
}
|
||
|
||
// buildQueueTile creates the queue status tile
|
||
func buildQueueTile(completed, total int, queueColor, textColor color.Color, onClick func()) fyne.CanvasObject {
|
||
rect := canvas.NewRectangle(queueColor)
|
||
rect.CornerRadius = 8
|
||
rect.SetMinSize(fyne.NewSize(160, 60))
|
||
|
||
text := canvas.NewText(fmt.Sprintf("QUEUE: %d/%d", completed, total), textColor)
|
||
text.Alignment = fyne.TextAlignCenter
|
||
text.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
||
text.TextSize = 18
|
||
|
||
tile := container.NewMax(rect, container.NewCenter(text))
|
||
|
||
// Make it tappable
|
||
tappable := NewTappable(tile, onClick)
|
||
return tappable
|
||
}
|
||
|
||
// sortedKeys returns sorted keys for stable category ordering
|
||
func sortedKeys(m map[string][]fyne.CanvasObject) []string {
|
||
keys := make([]string, 0, len(m))
|
||
for k := range m {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
return keys
|
||
}
|
||
|
||
// BuildHistorySidebar creates the history sidebar with tabs
|
||
func BuildHistorySidebar(
|
||
entries []HistoryEntry,
|
||
activeJobs []HistoryEntry,
|
||
onEntryClick func(HistoryEntry),
|
||
onEntryDelete 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
|
||
inProgressList := buildHistoryList(activeJobs, onEntryClick, nil, bgColor, textColor) // No delete for active jobs
|
||
completedList := buildHistoryList(completedEntries, onEntryClick, onEntryDelete, bgColor, textColor)
|
||
failedList := buildHistoryList(failedEntries, onEntryClick, onEntryDelete, bgColor, textColor)
|
||
|
||
// Tabs - In Progress first for quick visibility
|
||
tabs := container.NewAppTabs(
|
||
container.NewTabItem("In Progress", container.NewVScroll(inProgressList)),
|
||
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),
|
||
onEntryDelete 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, onEntryDelete, bgColor, textColor))
|
||
}
|
||
return container.NewVBox(items...)
|
||
}
|
||
|
||
func buildHistoryItem(
|
||
entry HistoryEntry,
|
||
onEntryClick func(HistoryEntry),
|
||
onEntryDelete func(HistoryEntry),
|
||
bgColor, textColor color.Color,
|
||
) fyne.CanvasObject {
|
||
// Badge
|
||
badge := BuildModuleBadge(entry.Type)
|
||
|
||
// Capture entry for closures
|
||
capturedEntry := entry
|
||
|
||
// Build header row with badge and optional delete button
|
||
headerItems := []fyne.CanvasObject{badge, layout.NewSpacer()}
|
||
if onEntryDelete != nil {
|
||
// Delete button - small "×" button (only for completed/failed)
|
||
deleteBtn := widget.NewButton("×", func() {
|
||
onEntryDelete(capturedEntry)
|
||
})
|
||
deleteBtn.Importance = widget.LowImportance
|
||
headerItems = append(headerItems, deleteBtn)
|
||
}
|
||
|
||
// Title
|
||
titleLabel := widget.NewLabel(utils.ShortenMiddle(entry.Title, 25))
|
||
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||
|
||
// Timestamp or status info
|
||
var timeStr string
|
||
if entry.Status == queue.JobStatusRunning || entry.Status == queue.JobStatusPending {
|
||
// For in-progress jobs, show status
|
||
if entry.Status == queue.JobStatusRunning {
|
||
timeStr = "Running..."
|
||
} else {
|
||
timeStr = "Pending"
|
||
}
|
||
} else {
|
||
// For completed/failed jobs, show timestamp
|
||
if entry.CompletedAt != nil {
|
||
timeStr = entry.CompletedAt.Format("Jan 2, 15:04")
|
||
} else {
|
||
timeStr = "Unknown"
|
||
}
|
||
}
|
||
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(headerItems...),
|
||
titleLabel,
|
||
timeLabel,
|
||
),
|
||
)
|
||
|
||
card := canvas.NewRectangle(bgColor)
|
||
card.CornerRadius = 4
|
||
|
||
item := container.NewPadded(container.NewMax(card, content))
|
||
|
||
return NewTappable(item, func() { onEntryClick(capturedEntry) })
|
||
}
|