diff --git a/internal/ui/mainmenu.go b/internal/ui/mainmenu.go index 709227b..b7928c6 100644 --- a/internal/ui/mainmenu.go +++ b/internal/ui/mainmenu.go @@ -13,30 +13,33 @@ import ( // ModuleInfo contains information about a module for display type ModuleInfo struct { - ID string - Label string - Color color.Color - Enabled bool + ID string + Label string + Color color.Color + Enabled bool + Category string } -// BuildMainMenu creates the main menu view with module tiles -func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDrop func(string, []fyne.URI), onQueueClick func(), titleColor, queueColor, textColor color.Color, queueCompleted, queueTotal int) fyne.CanvasObject { +// 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(), 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) + logsBtn := widget.NewButton("Logs", onLogsClick) + logsBtn.Importance = widget.LowImportance - header := container.New(layout.NewHBoxLayout(), - title, - layout.NewSpacer(), - queueTile, - ) + header := container.New(layout.NewHBoxLayout(), title, layout.NewSpacer(), logsBtn, queueTile) - var tileObjects []fyne.CanvasObject + categorized := map[string][]fyne.CanvasObject{} for i := range modules { - mod := modules[i] // Create new variable for this iteration - modID := mod.ID // Capture for closure + 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 { @@ -53,10 +56,16 @@ func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDro } fmt.Printf("[MAINMENU] Creating tile for module=%s enabled=%v hasDropFunc=%v\n", modID, mod.Enabled, dropFunc != nil) logging.Debug(logging.CatUI, "Creating tile for module=%s enabled=%v hasDropFunc=%v", modID, mod.Enabled, dropFunc != nil) - tileObjects = append(tileObjects, buildModuleTile(mod, tapFunc, dropFunc)) + categorized[cat] = append(categorized[cat], buildModuleTile(mod, tapFunc, dropFunc)) } - grid := container.NewGridWithColumns(3, tileObjects...) + 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)) @@ -64,7 +73,7 @@ func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDro body := container.New(layout.NewVBoxLayout(), header, padding, - grid, + container.NewVBox(sections...), ) return body @@ -93,3 +102,13 @@ func buildQueueTile(completed, total int, queueColor, textColor color.Color, onC 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 +} diff --git a/main.go b/main.go index 5a465a0..33628ab 100644 --- a/main.go +++ b/main.go @@ -46,10 +46,11 @@ import ( // Module describes a high level tool surface that gets a tile on the menu. type Module struct { - ID string - Label string - Color color.Color - Handle func(files []string) + ID string + Label string + Color color.Color + Category string + Handle func(files []string) } var ( @@ -66,15 +67,15 @@ var ( logsDirPath string modulesList = []Module{ - {"convert", "Convert", utils.MustHex("#8B44FF"), modules.HandleConvert}, // Violet - {"merge", "Merge", utils.MustHex("#4488FF"), modules.HandleMerge}, // Blue - {"trim", "Trim", utils.MustHex("#44DDFF"), modules.HandleTrim}, // Cyan - {"filters", "Filters", utils.MustHex("#44FF88"), modules.HandleFilters}, // Green - {"upscale", "Upscale", utils.MustHex("#AAFF44"), modules.HandleUpscale}, // Yellow-Green - {"audio", "Audio", utils.MustHex("#FFD744"), modules.HandleAudio}, // Yellow - {"thumb", "Thumb", utils.MustHex("#FF8844"), modules.HandleThumb}, // Orange - {"compare", "Compare", utils.MustHex("#FF44AA"), modules.HandleCompare}, // Pink - {"inspect", "Inspect", utils.MustHex("#FF4444"), modules.HandleInspect}, // Red + {"convert", "Convert", utils.MustHex("#8B44FF"), "Convert", modules.HandleConvert}, // Violet + {"merge", "Merge", utils.MustHex("#4488FF"), "Convert", modules.HandleMerge}, // Blue + {"trim", "Trim", utils.MustHex("#44DDFF"), "Convert", modules.HandleTrim}, // Cyan + {"filters", "Filters", utils.MustHex("#44FF88"), "Convert", modules.HandleFilters}, // Green + {"upscale", "Upscale", utils.MustHex("#AAFF44"), "Advanced", modules.HandleUpscale}, // Yellow-Green + {"audio", "Audio", utils.MustHex("#FFD744"), "Convert", modules.HandleAudio}, // Yellow + {"thumb", "Thumb", utils.MustHex("#FF8844"), "Screenshots", modules.HandleThumb}, // Orange + {"compare", "Compare", utils.MustHex("#FF44AA"), "Inspect", modules.HandleCompare}, // Pink + {"inspect", "Inspect", utils.MustHex("#FF4444"), "Inspect", modules.HandleInspect}, // Red } // Platform-specific configuration @@ -196,6 +197,24 @@ func (s *appState) openLogViewer(title, path string, live bool) { d.Show() } +// openFolder tries to open a folder in the OS file browser. +func openFolder(path string) error { + if strings.TrimSpace(path) == "" { + return fmt.Errorf("path is empty") + } + var cmd *exec.Cmd + switch runtime.GOOS { + case "windows": + cmd = exec.Command("explorer", path) + case "darwin": + cmd = exec.Command("open", path) + default: + cmd = exec.Command("xdg-open", path) + } + utils.ApplyNoWindow(cmd) + return cmd.Start() +} + type formatOption struct { Label string Ext string @@ -558,10 +577,11 @@ func (s *appState) showMainMenu() { var mods []ui.ModuleInfo for _, m := range modulesList { mods = append(mods, ui.ModuleInfo{ - ID: m.ID, - Label: m.Label, - Color: m.Color, - Enabled: m.ID == "convert" || m.ID == "compare" || m.ID == "inspect", // Convert, compare, and inspect modules are functional + ID: m.ID, + Label: m.Label, + Color: m.Color, + Category: m.Category, + Enabled: m.ID == "convert" || m.ID == "compare" || m.ID == "inspect", // Convert, compare, and inspect modules are functional }) } @@ -575,7 +595,25 @@ func (s *appState) showMainMenu() { queueTotal = len(s.jobQueue.List()) } - menu := ui.BuildMainMenu(mods, s.showModule, s.handleModuleDrop, s.showQueue, titleColor, queueColor, textColor, queueCompleted, queueTotal) + menu := ui.BuildMainMenu(mods, s.showModule, s.handleModuleDrop, s.showQueue, func() { + // Logs button: offer to open logs folder or view app log + logOptions := container.NewVBox( + widget.NewButton("Open Logs Folder", func() { + if err := openFolder(getLogsDir()); err != nil { + dialog.ShowError(fmt.Errorf("failed to open logs folder: %w", err), s.window) + } + }), + widget.NewButton("View App Log", func() { + path := logging.FilePath() + if strings.TrimSpace(path) == "" { + dialog.ShowInformation("No Log", "No app log file found.", s.window) + return + } + s.openLogViewer("App Log", path, false) + }), + ) + dialog.ShowCustom("Logs", "Close", logOptions, s.window) + }, titleColor, queueColor, textColor, queueCompleted, queueTotal) // Update stats bar s.updateStatsBar()