diff --git a/internal/ui/benchmarkview.go b/internal/ui/benchmarkview.go index 60570da..1983025 100644 --- a/internal/ui/benchmarkview.go +++ b/internal/ui/benchmarkview.go @@ -305,3 +305,114 @@ func BuildBenchmarkResultsView( return container.NewPadded(body) } + +// BuildBenchmarkHistoryView creates the benchmark history browser UI +func BuildBenchmarkHistoryView( + history []BenchmarkHistoryRun, + onSelectRun func(int), + onClose func(), + titleColor, bgColor, textColor color.Color, +) fyne.CanvasObject { + // Header + title := canvas.NewText("BENCHMARK HISTORY", titleColor) + title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true} + title.TextSize = 24 + + closeBtn := widget.NewButton("← Back", onClose) + closeBtn.Importance = widget.LowImportance + + header := container.NewBorder( + nil, nil, + closeBtn, + nil, + container.NewCenter(title), + ) + + if len(history) == 0 { + emptyMsg := widget.NewLabel("No benchmark history yet.\n\nRun your first benchmark to see results here.") + emptyMsg.Alignment = fyne.TextAlignCenter + emptyMsg.Wrapping = fyne.TextWrapWord + + body := container.NewBorder( + header, + nil, nil, nil, + container.NewCenter(emptyMsg), + ) + + return container.NewPadded(body) + } + + // Build list of benchmark runs + var runItems []fyne.CanvasObject + for i, run := range history { + idx := i // Capture for closure + runItems = append(runItems, buildHistoryRunItem(run, idx, onSelectRun, bgColor, textColor)) + } + + runsList := container.NewVBox(runItems...) + runsScroll := container.NewVScroll(runsList) + runsScroll.SetMinSize(fyne.NewSize(0, 400)) + + infoLabel := widget.NewLabel("Click on a benchmark run to view detailed results") + infoLabel.Alignment = fyne.TextAlignCenter + infoLabel.TextStyle = fyne.TextStyle{Italic: true} + + body := container.NewBorder( + header, + container.NewVBox(widget.NewSeparator(), infoLabel), + nil, nil, + runsScroll, + ) + + return container.NewPadded(body) +} + +// BenchmarkHistoryRun represents a benchmark run in the history view +type BenchmarkHistoryRun struct { + Timestamp string + ResultCount int + RecommendedEncoder string + RecommendedPreset string + RecommendedFPS float64 +} + +func buildHistoryRunItem( + run BenchmarkHistoryRun, + index int, + onSelect func(int), + bgColor, textColor color.Color, +) fyne.CanvasObject { + // Timestamp label + timeLabel := widget.NewLabel(run.Timestamp) + timeLabel.TextStyle = fyne.TextStyle{Bold: true} + + // Recommendation info + recLabel := widget.NewLabel(fmt.Sprintf("Recommended: %s (%s) - %.1f FPS", + run.RecommendedEncoder, run.RecommendedPreset, run.RecommendedFPS)) + + // Result count + countLabel := widget.NewLabel(fmt.Sprintf("%d encoders tested", run.ResultCount)) + countLabel.TextStyle = fyne.TextStyle{Italic: true} + + // Content + content := container.NewVBox( + timeLabel, + recLabel, + countLabel, + ) + + // Card background + card := canvas.NewRectangle(bgColor) + card.CornerRadius = 4 + + item := container.NewPadded( + container.NewMax(card, content), + ) + + // Make it tappable + tappable := NewTappable(item, func() { + onSelect(index) + }) + + return tappable +} diff --git a/internal/ui/mainmenu.go b/internal/ui/mainmenu.go index 2be4242..d75a11b 100644 --- a/internal/ui/mainmenu.go +++ b/internal/ui/mainmenu.go @@ -23,20 +23,23 @@ type ModuleInfo 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(), 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(), 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) - benchmarkBtn := widget.NewButton("Benchmark", onBenchmarkClick) + 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(), benchmarkBtn, logsBtn, queueTile) + header := container.New(layout.NewHBoxLayout(), title, layout.NewSpacer(), benchmarkBtn, viewResultsBtn, logsBtn, queueTile) categorized := map[string][]fyne.CanvasObject{} for i := range modules { diff --git a/main.go b/main.go index 314f24c..3fde24e 100644 --- a/main.go +++ b/main.go @@ -509,12 +509,19 @@ func savePersistedConvertConfig(cfg convertConfig) error { return os.WriteFile(path, data, 0o644) } -// benchmarkConfig holds benchmark results and recommendations +// benchmarkRun represents a single benchmark test run +type benchmarkRun struct { + Timestamp time.Time `json:"timestamp"` + Results []benchmark.Result `json:"results"` + RecommendedEncoder string `json:"recommended_encoder"` + RecommendedPreset string `json:"recommended_preset"` + RecommendedHWAccel string `json:"recommended_hwaccel"` + RecommendedFPS float64 `json:"recommended_fps"` +} + +// benchmarkConfig holds benchmark history type benchmarkConfig struct { - RecommendedEncoder string `json:"recommended_encoder"` - RecommendedPreset string `json:"recommended_preset"` - RecommendedHWAccel string `json:"recommended_hwaccel"` - LastBenchmarkTime time.Time `json:"last_benchmark_time"` + History []benchmarkRun `json:"history"` } func benchmarkConfigPath() string { @@ -937,7 +944,7 @@ func (s *appState) showMainMenu() { viewAppLogBtn, ) dialog.ShowCustom("Logs", "Close", logOptions, s.window) - }, s.showBenchmark, titleColor, queueColor, textColor, queueCompleted, queueTotal) + }, s.showBenchmark, s.showBenchmarkHistory, titleColor, queueColor, textColor, queueCompleted, queueTotal) // Update stats bar s.updateStatsBar() @@ -1293,14 +1300,20 @@ func (s *appState) showBenchmark() { // Get recommendation encoder, preset, rec := suite.GetRecommendation() + + // Save benchmark run to history + if err := s.saveBenchmarkRun(suite.Results, encoder, preset, rec.FPS); err != nil { + logging.Debug(logging.CatSystem, "failed to save benchmark run: %v", err) + } + if encoder != "" { logging.Debug(logging.CatSystem, "benchmark recommendation: %s (preset: %s) - %.1f FPS", encoder, preset, rec.FPS) // Show results dialog with option to apply go func() { - topResults := suite.GetTopN(10) + allResults := suite.Results // Show all results, not just top 10 resultsView := ui.BuildBenchmarkResultsView( - topResults, + allResults, rec, func() { // Apply recommended settings @@ -1351,7 +1364,7 @@ func (s *appState) detectHardwareEncoders() []string { return available } -func (s *appState) applyBenchmarkRecommendation(encoder, preset string) { +func (s *appState) saveBenchmarkRun(results []benchmark.Result, encoder, preset string, fps float64) error { // Map encoder to hardware acceleration setting var hwAccel string switch { @@ -1367,22 +1380,123 @@ func (s *appState) applyBenchmarkRecommendation(encoder, preset string) { hwAccel = "none" } - // Save benchmark recommendation - cfg := benchmarkConfig{ - RecommendedEncoder: encoder, - RecommendedPreset: preset, - RecommendedHWAccel: hwAccel, - LastBenchmarkTime: time.Now(), - } - if err := saveBenchmarkConfig(cfg); err != nil { - logging.Debug(logging.CatSystem, "failed to save benchmark recommendation: %v", err) + // Load existing config + cfg, err := loadBenchmarkConfig() + if err != nil { + // Create new config if loading fails + cfg = benchmarkConfig{History: []benchmarkRun{}} } - logging.Debug(logging.CatSystem, "applied benchmark recommendation: encoder=%s preset=%s hwaccel=%s", encoder, preset, hwAccel) + // Create new benchmark run + run := benchmarkRun{ + Timestamp: time.Now(), + Results: results, + RecommendedEncoder: encoder, + RecommendedPreset: preset, + RecommendedHWAccel: hwAccel, + RecommendedFPS: fps, + } + + // Add to history (keep last 10 runs) + cfg.History = append([]benchmarkRun{run}, cfg.History...) + if len(cfg.History) > 10 { + cfg.History = cfg.History[:10] + } + + // Save config + if err := saveBenchmarkConfig(cfg); err != nil { + return err + } + + logging.Debug(logging.CatSystem, "saved benchmark run: encoder=%s preset=%s fps=%.1f results=%d", encoder, preset, fps, len(results)) + return nil +} + +func (s *appState) applyBenchmarkRecommendation(encoder, preset string) { + logging.Debug(logging.CatSystem, "applied benchmark recommendation: encoder=%s preset=%s", encoder, preset) dialog.ShowInformation("Benchmark Settings Applied", - fmt.Sprintf("Your system's optimal encoder settings have been saved:\n\nEncoder: %s\nPreset: %s\nHardware: %s\n\nThese are available for reference in the Convert module.", - encoder, preset, hwAccel), s.window) + fmt.Sprintf("Recommended encoder noted:\n\nEncoder: %s\nPreset: %s\n\nYou can reference these settings in the Convert module.", + encoder, preset), s.window) +} + +func (s *appState) showBenchmarkHistory() { + s.stopPreview() + s.stopPlayer() + s.active = "benchmark-history" + + // Load benchmark history + cfg, err := loadBenchmarkConfig() + if err != nil || len(cfg.History) == 0 { + // Show empty state + view := ui.BuildBenchmarkHistoryView( + []ui.BenchmarkHistoryRun{}, + nil, + s.showMainMenu, + utils.MustHex("#4CE870"), + utils.MustHex("#1E1E1E"), + utils.MustHex("#FFFFFF"), + ) + s.setContent(view) + return + } + + // Convert history to UI format + var historyRuns []ui.BenchmarkHistoryRun + for _, run := range cfg.History { + historyRuns = append(historyRuns, ui.BenchmarkHistoryRun{ + Timestamp: run.Timestamp.Format("2006-01-02 15:04:05"), + ResultCount: len(run.Results), + RecommendedEncoder: run.RecommendedEncoder, + RecommendedPreset: run.RecommendedPreset, + RecommendedFPS: run.RecommendedFPS, + }) + } + + // Build history view + view := ui.BuildBenchmarkHistoryView( + historyRuns, + func(index int) { + // Show detailed results for this run + if index < 0 || index >= len(cfg.History) { + return + } + run := cfg.History[index] + + // Create a fake recommendation result for the results view + rec := benchmark.Result{ + Encoder: run.RecommendedEncoder, + Preset: run.RecommendedPreset, + FPS: run.RecommendedFPS, + Score: run.RecommendedFPS, + } + + resultsView := ui.BuildBenchmarkResultsView( + run.Results, + rec, + func() { + // Apply this recommendation + s.applyBenchmarkRecommendation(run.RecommendedEncoder, run.RecommendedPreset) + s.showBenchmarkHistory() + }, + func() { + // Back to history + s.showBenchmarkHistory() + }, + utils.MustHex("#4CE870"), + utils.MustHex("#1E1E1E"), + utils.MustHex("#FFFFFF"), + ) + + s.setContent(resultsView) + }, + s.showMainMenu, + utils.MustHex("#4CE870"), + utils.MustHex("#1E1E1E"), + utils.MustHex("#FFFFFF"), + ) + + s.setContent(view) } func (s *appState) showModule(id string) {