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:
parent
385c6f736d
commit
d7389a25bc
|
|
@ -43,13 +43,16 @@ type HistoryEntry struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildMainMenu creates the main menu view with module tiles grouped by category
|
// 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 := canvas.NewText("VIDEOTOOLS", titleColor)
|
||||||
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
||||||
title.TextSize = 28
|
title.TextSize = 28
|
||||||
|
|
||||||
queueTile := buildQueueTile(queueCompleted, queueTotal, queueColor, textColor, onQueueClick)
|
queueTile := buildQueueTile(queueCompleted, queueTotal, queueColor, textColor, onQueueClick)
|
||||||
|
|
||||||
|
sidebarToggleBtn := widget.NewButton("☰ History", onToggleSidebar)
|
||||||
|
sidebarToggleBtn.Importance = widget.LowImportance
|
||||||
|
|
||||||
benchmarkBtn := widget.NewButton("Run Benchmark", onBenchmarkClick)
|
benchmarkBtn := widget.NewButton("Run Benchmark", onBenchmarkClick)
|
||||||
benchmarkBtn.Importance = widget.LowImportance
|
benchmarkBtn.Importance = widget.LowImportance
|
||||||
|
|
||||||
|
|
@ -59,7 +62,7 @@ func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDro
|
||||||
logsBtn := widget.NewButton("Logs", onLogsClick)
|
logsBtn := widget.NewButton("Logs", onLogsClick)
|
||||||
logsBtn.Importance = widget.LowImportance
|
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{}
|
categorized := map[string][]fyne.CanvasObject{}
|
||||||
for i := range modules {
|
for i := range modules {
|
||||||
|
|
@ -103,6 +106,13 @@ func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDro
|
||||||
container.NewVBox(sections...),
|
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
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
116
main.go
116
main.go
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"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() {
|
func (s *appState) stopPreview() {
|
||||||
if s.anim != nil {
|
if s.anim != nil {
|
||||||
s.anim.Stop()
|
s.anim.Stop()
|
||||||
|
|
@ -1233,6 +1331,18 @@ func (s *appState) showMainMenu() {
|
||||||
queueTotal = len(s.jobQueue.List())
|
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() {
|
menu := ui.BuildMainMenu(mods, s.showModule, s.handleModuleDrop, s.showQueue, func() {
|
||||||
logDir := getLogsDir()
|
logDir := getLogsDir()
|
||||||
_ = os.MkdirAll(logDir, 0o755)
|
_ = os.MkdirAll(logDir, 0o755)
|
||||||
|
|
@ -1264,7 +1374,11 @@ func (s *appState) showMainMenu() {
|
||||||
viewAppLogBtn,
|
viewAppLogBtn,
|
||||||
)
|
)
|
||||||
dialog.ShowCustom("Logs", "Close", logOptions, s.window)
|
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
|
// Update stats bar
|
||||||
s.updateStatsBar()
|
s.updateStatsBar()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user