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 MissingDependencies bool // true if disabled due to missing dependencies } // 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 Progress float64 // 0.0 to 1.0 for in-progress jobs } // 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, hasBenchmark bool) fyne.CanvasObject { title := canvas.NewText("VIDEOTOOLS", titleColor) title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true} title.TextSize = 18 queueTile := buildQueueTile(queueCompleted, queueTotal, queueColor, textColor, onQueueClick) sidebarToggleBtn := widget.NewButton("☰", onToggleSidebar) sidebarToggleBtn.Importance = widget.LowImportance benchmarkBtn := widget.NewButton("Benchmark", onBenchmarkClick) // Highlight the benchmark button if no benchmark has been run if !hasBenchmark { benchmarkBtn.Importance = widget.HighImportance } else { benchmarkBtn.Importance = widget.LowImportance } viewResultsBtn := widget.NewButton("Results", onBenchmarkHistoryClick) viewResultsBtn.Importance = widget.LowImportance // Build header controls dynamically - only show logs button if callback is provided headerControls := []fyne.CanvasObject{sidebarToggleBtn} if onLogsClick != nil { logsBtn := widget.NewButton("Logs", onLogsClick) logsBtn.Importance = widget.LowImportance headerControls = append(headerControls, logsBtn) } headerControls = append(headerControls, benchmarkBtn, viewResultsBtn, queueTile) // Compact header - title on left, controls on right header := container.NewBorder( nil, nil, title, container.NewHBox(headerControls...), nil, ) // Create module map for quick lookup moduleMap := make(map[string]ModuleInfo) for _, mod := range modules { moduleMap[mod.ID] = mod } // Helper to build a tile buildTile := func(modID string) fyne.CanvasObject { mod, exists := moduleMap[modID] if !exists { return layout.NewSpacer() } var tapFunc func() var dropFunc func([]fyne.URI) if mod.Enabled { id := modID 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) } } return buildModuleTile(mod, tapFunc, dropFunc) } // Helper to create category label makeCatLabel := func(text string) *canvas.Text { label := canvas.NewText(text, textColor) label.TextSize = 10 label.Alignment = fyne.TextAlignLeading return label } // Build rows with category labels above tiles var rows []fyne.CanvasObject // Convert section rows = append(rows, makeCatLabel("Convert")) rows = append(rows, container.NewGridWithColumns(3, buildTile("convert"), buildTile("merge"), buildTile("trim"), )) rows = append(rows, container.NewGridWithColumns(3, buildTile("filters"), buildTile("audio"), buildTile("subtitles"), )) // Inspect section rows = append(rows, makeCatLabel("Inspect")) rows = append(rows, container.NewGridWithColumns(3, buildTile("compare"), buildTile("inspect"), buildTile("upscale"), )) // Disc section rows = append(rows, makeCatLabel("Disc")) rows = append(rows, container.NewGridWithColumns(3, buildTile("author"), buildTile("rip"), buildTile("bluray"), )) // Playback section rows = append(rows, makeCatLabel("Playback")) rows = append(rows, container.NewGridWithColumns(3, buildTile("player"), buildTile("thumb"), buildTile("settings"), )) gridBox := container.NewVBox(rows...) scroll := container.NewVScroll(gridBox) scroll.SetMinSize(fyne.NewSize(0, 0)) body := container.NewBorder( header, nil, nil, nil, scroll, ) // 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 missingDeps=%v", mod.ID, mod.Color, mod.Enabled, mod.MissingDependencies) return NewModuleTile(mod.Label, mod.Color, mod.Enabled, mod.MissingDependencies, 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 = 6 rect.SetMinSize(fyne.NewSize(120, 40)) 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 = 14 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} // Progress bar for in-progress jobs contentItems := []fyne.CanvasObject{ container.NewHBox(headerItems...), titleLabel, timeLabel, } if entry.Status == queue.JobStatusRunning || entry.Status == queue.JobStatusPending { // Add progress bar for active jobs moduleCol := ModuleColor(entry.Type) progressBar := NewStripedProgress(moduleCol) progressBar.SetProgress(entry.Progress) contentItems = append(contentItems, progressBar) } // 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(contentItems...), ) card := canvas.NewRectangle(bgColor) card.CornerRadius = 4 item := container.NewPadded(container.NewMax(card, content)) return NewTappable(item, func() { onEntryClick(capturedEntry) }) }