Phase 5: Integrate sidebar into main menu

Integrated history sidebar into main menu with toggle button and split
view layout. Added history details dialog with FFmpeg command copy.

Changes:
- internal/ui/mainmenu.go:
  * Updated BuildMainMenu() signature to accept sidebar parameters
  * Added "☰ History" toggle button to header
  * Implemented HSplit layout (20% sidebar, 80% main) when sidebar visible

- main.go:
  * Added "sort" import for showHistoryDetails
  * Added showHistoryDetails() method to display job details dialog
  * Shows timestamps, config, error messages, FFmpeg command
  * "Show in Folder" button (only if output file exists)
  * "View Log" button (only if log file exists)
  * Updated showMainMenu() to build and pass sidebar
  * Implemented sidebar toggle that refreshes main menu

The sidebar can be toggled on/off from the main menu, shows history
entries with filtering by status (Completed vs Failed/Cancelled), and
clicking an entry opens a detailed view with all job information and
the ability to copy the FFmpeg command for manual execution.

🤖 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:36:39 -05:00
parent 385c6f736d
commit d7389a25bc
2 changed files with 127 additions and 3 deletions

View File

@ -43,13 +43,16 @@ type HistoryEntry struct {
}
// 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 {
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
@ -59,7 +62,7 @@ func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDro
logsBtn := widget.NewButton("Logs", onLogsClick)
logsBtn.Importance = widget.LowImportance
header := container.New(layout.NewHBoxLayout(), title, layout.NewSpacer(), benchmarkBtn, viewResultsBtn, logsBtn, queueTile)
header := container.New(layout.NewHBoxLayout(), title, layout.NewSpacer(), sidebarToggleBtn, benchmarkBtn, viewResultsBtn, logsBtn, queueTile)
categorized := map[string][]fyne.CanvasObject{}
for i := range modules {
@ -103,6 +106,13 @@ func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDro
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
}

116
main.go
View File

@ -21,6 +21,7 @@ import (
"regexp"
"runtime"
"slices"
"sort"
"strconv"
"strings"
"sync"
@ -815,6 +816,103 @@ func (s *appState) addToHistory(job *queue.Job) {
}
}
// showHistoryDetails displays detailed information about a history entry
func (s *appState) showHistoryDetails(entry ui.HistoryEntry) {
// Format config
var configLines []string
for key, value := range entry.Config {
configLines = append(configLines, fmt.Sprintf("%s: %v", key, value))
}
sort.Strings(configLines)
// Format timestamps
createdStr := entry.CreatedAt.Format("2006-01-02 15:04:05")
startedStr := "N/A"
if entry.StartedAt != nil {
startedStr = entry.StartedAt.Format("2006-01-02 15:04:05")
}
completedStr := "N/A"
if entry.CompletedAt != nil {
completedStr = entry.CompletedAt.Format("2006-01-02 15:04:05")
}
details := fmt.Sprintf(`Type: %s
Status: %s
Input: %s
Output: %s
Created: %s
Started: %s
Completed: %s
Config:
%s`, entry.Type, entry.Status, entry.InputFile, entry.OutputFile,
createdStr, startedStr, completedStr, strings.Join(configLines, "\n"))
if entry.Error != "" {
details += fmt.Sprintf("\n\nError:\n%s", entry.Error)
}
detailsLabel := widget.NewLabel(details)
detailsLabel.Wrapping = fyne.TextWrapWord
// FFmpeg Command section
var ffmpegSection fyne.CanvasObject
if entry.FFmpegCmd != "" {
cmdWidget := ui.NewFFmpegCommandWidget(entry.FFmpegCmd, s.window)
ffmpegSection = container.NewVBox(
widget.NewSeparator(),
widget.NewLabel("FFmpeg Command:"),
cmdWidget,
)
}
// Buttons
var buttons []fyne.CanvasObject
if entry.OutputFile != "" {
if _, err := os.Stat(entry.OutputFile); err == nil {
buttons = append(buttons, widget.NewButton("Show in Folder", func() {
dir := filepath.Dir(entry.OutputFile)
if err := openFolder(dir); err != nil {
dialog.ShowError(err, s.window)
}
}))
}
}
if entry.LogPath != "" {
if _, err := os.Stat(entry.LogPath); err == nil {
buttons = append(buttons, widget.NewButton("View Log", func() {
s.openLogViewer(entry.Title, entry.LogPath, false)
}))
}
}
closeBtn := widget.NewButton("Close", nil)
buttons = append(buttons, layout.NewSpacer(), closeBtn)
// Layout
contentVBox := container.NewVBox(
container.NewVScroll(detailsLabel),
)
if ffmpegSection != nil {
contentVBox.Add(ffmpegSection)
}
content := container.NewBorder(
nil,
container.NewHBox(buttons...),
nil, nil,
contentVBox,
)
d := dialog.NewCustom("Job Details", "Close", content, s.window)
d.Resize(fyne.NewSize(700, 600))
closeBtn.OnTapped = func() { d.Hide() }
d.Show()
}
func (s *appState) stopPreview() {
if s.anim != nil {
s.anim.Stop()
@ -1233,6 +1331,18 @@ func (s *appState) showMainMenu() {
queueTotal = len(s.jobQueue.List())
}
// Build sidebar if visible
var sidebar fyne.CanvasObject
if s.sidebarVisible {
sidebar = ui.BuildHistorySidebar(
s.historyEntries,
s.showHistoryDetails,
titleColor,
utils.MustHex("#1A1F2E"),
textColor,
)
}
menu := ui.BuildMainMenu(mods, s.showModule, s.handleModuleDrop, s.showQueue, func() {
logDir := getLogsDir()
_ = os.MkdirAll(logDir, 0o755)
@ -1264,7 +1374,11 @@ func (s *appState) showMainMenu() {
viewAppLogBtn,
)
dialog.ShowCustom("Logs", "Close", logOptions, s.window)
}, s.showBenchmark, s.showBenchmarkHistory, titleColor, queueColor, textColor, queueCompleted, queueTotal)
}, s.showBenchmark, s.showBenchmarkHistory, func() {
// Toggle sidebar
s.sidebarVisible = !s.sidebarVisible
s.showMainMenu()
}, s.sidebarVisible, sidebar, titleColor, queueColor, textColor, queueCompleted, queueTotal)
// Update stats bar
s.updateStatsBar()