From b079bff6fbcfb267aa97b662319d10916bbf34c9 Mon Sep 17 00:00:00 2001 From: Stu Leak Date: Wed, 31 Dec 2025 08:43:30 -0500 Subject: [PATCH] feat(settings): Add Settings module with dependency management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive Settings module for managing system dependencies and application preferences: - Created settings_module.go with dependency checking system - Maps modules to required dependencies (FFmpeg, DVDAuthor, xorriso, Real-ESRGAN, Whisper) - Displays dependency status with visual indicators (green/red) - Shows platform-specific installation commands - Auto-enables/disables modules based on installed dependencies - Added Settings tile to main menu (always enabled) - Integrated module availability checking via isModuleAvailable() This provides users a centralized location to check and install missing dependencies, addressing the requirement to disable modules when dependencies aren't available. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- main.go | 7 +- settings_module.go | 290 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 settings_module.go diff --git a/main.go b/main.go index cd9b592..eb81e09 100644 --- a/main.go +++ b/main.go @@ -96,6 +96,7 @@ var ( {"compare", "Compare", utils.MustHex("#E91E63"), "Inspect", modules.HandleCompare}, // Pink (comparison) {"inspect", "Inspect", utils.MustHex("#F44336"), "Inspect", modules.HandleInspect}, // Red (analysis) {"player", "Player", utils.MustHex("#3F51B5"), "Playback", modules.HandlePlayer}, // Indigo (playback) + {"settings", "Settings", utils.MustHex("#607D8B"), "Settings", nil}, // Blue Grey (settings) } // Platform-specific configuration @@ -1616,12 +1617,14 @@ func (s *appState) showMainMenu() { // Convert Module slice to ui.ModuleInfo slice var mods []ui.ModuleInfo for _, m := range modulesList { + // Settings module is always enabled + enabled := m.ID == "settings" || isModuleAvailable(m.ID) mods = append(mods, ui.ModuleInfo{ ID: m.ID, Label: m.Label, Color: m.Color, Category: m.Category, - Enabled: m.ID == "convert" || m.ID == "compare" || m.ID == "inspect" || m.ID == "merge" || m.ID == "thumb" || m.ID == "player" || m.ID == "filters" || m.ID == "upscale" || m.ID == "author" || m.ID == "subtitles" || m.ID == "rip", // Enabled modules + Enabled: enabled, }) } @@ -2692,6 +2695,8 @@ func (s *appState) showModule(id string) { s.showRipView() case "subtitles": s.showSubtitlesView() + case "settings": + s.showSettingsView() case "mainmenu": s.showMainMenu() default: diff --git a/settings_module.go b/settings_module.go new file mode 100644 index 0000000..4b5971c --- /dev/null +++ b/settings_module.go @@ -0,0 +1,290 @@ +package main + +import ( + "image/color" + "os/exec" + "runtime" + "strings" + + "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/ui" + "git.leaktechnologies.dev/stu/VideoTools/internal/utils" +) + +// Dependency represents a system dependency +type Dependency struct { + Name string + Command string // Command to check if installed + Required bool // If true, core functionality requires this + Description string + InstallCmd string // Command to install (platform-specific) +} + +// ModuleDependencies maps module IDs to their required dependencies +var moduleDependencies = map[string][]string{ + "convert": {"ffmpeg"}, + "merge": {"ffmpeg"}, + "trim": {"ffmpeg"}, + "filters": {"ffmpeg"}, + "upscale": {"ffmpeg", "realesrgan-ncnn-vulkan"}, + "audio": {"ffmpeg"}, + "author": {"ffmpeg", "dvdauthor", "xorriso"}, + "rip": {"ffmpeg", "xorriso"}, + "bluray": {"ffmpeg"}, + "subtitles": {"ffmpeg", "whisper"}, + "thumb": {"ffmpeg"}, + "compare": {"ffmpeg"}, + "inspect": {"ffmpeg"}, + "player": {"ffmpeg"}, +} + +// AllDependencies defines all possible dependencies +var allDependencies = map[string]Dependency{ + "ffmpeg": { + Name: "FFmpeg", + Command: "ffmpeg", + Required: true, + Description: "Core video processing engine", + InstallCmd: getFFmpegInstallCmd(), + }, + "dvdauthor": { + Name: "DVDAuthor", + Command: "dvdauthor", + Required: false, + Description: "DVD authoring tool", + InstallCmd: getDVDAuthorInstallCmd(), + }, + "xorriso": { + Name: "xorriso", + Command: "xorriso", + Required: false, + Description: "ISO creation and extraction", + InstallCmd: getXorrisoInstallCmd(), + }, + "realesrgan-ncnn-vulkan": { + Name: "Real-ESRGAN", + Command: "realesrgan-ncnn-vulkan", + Required: false, + Description: "AI video upscaling", + InstallCmd: "See install.sh --skip-ai=false", + }, + "whisper": { + Name: "Whisper", + Command: "whisper", + Required: false, + Description: "AI subtitle generation", + InstallCmd: "pip3 install --user openai-whisper", + }, +} + +func getFFmpegInstallCmd() string { + switch runtime.GOOS { + case "linux": + return "sudo apt-get install ffmpeg # or dnf/pacman/zypper" + case "darwin": + return "brew install ffmpeg" + case "windows": + return "Download from ffmpeg.org" + default: + return "See ffmpeg.org for installation" + } +} + +func getDVDAuthorInstallCmd() string { + switch runtime.GOOS { + case "linux": + return "sudo apt-get install dvdauthor # or dnf/pacman/zypper" + case "darwin": + return "brew install dvdauthor" + default: + return "./scripts/install.sh" + } +} + +func getXorrisoInstallCmd() string { + switch runtime.GOOS { + case "linux": + return "sudo apt-get install xorriso # or dnf/pacman/zypper" + case "darwin": + return "brew install xorriso" + default: + return "./scripts/install.sh" + } +} + +// checkDependency checks if a command is available +func checkDependency(command string) bool { + _, err := exec.LookPath(command) + return err == nil +} + +// getModuleDependencyStatus checks which dependencies a module is missing +func getModuleDependencyStatus(moduleID string) (missing []string, hasAll bool) { + deps, ok := moduleDependencies[moduleID] + if !ok { + return nil, true // Module has no dependencies + } + + for _, depName := range deps { + dep, exists := allDependencies[depName] + if !exists { + continue + } + if !checkDependency(dep.Command) { + missing = append(missing, depName) + } + } + + return missing, len(missing) == 0 +} + +// isModuleAvailable returns true if all required dependencies are installed +func isModuleAvailable(moduleID string) bool { + _, hasAll := getModuleDependencyStatus(moduleID) + return hasAll +} + +func buildSettingsView(state *appState) fyne.CanvasObject { + settingsColor := utils.MustHex("#607D8B") // Blue Grey for settings + + backBtn := widget.NewButton("< BACK", func() { + state.showMainMenu() + }) + backBtn.Importance = widget.LowImportance + + topBar := ui.TintedBar(settingsColor, container.NewHBox(backBtn, layout.NewSpacer())) + bottomBar := moduleFooter(settingsColor, layout.NewSpacer(), state.statsBar) + + tabs := container.NewAppTabs( + container.NewTabItem("Dependencies", buildDependenciesTab(state)), + container.NewTabItem("Preferences", buildPreferencesTab(state)), + ) + tabs.SetTabLocation(container.TabLocationTop) + + return container.NewBorder(topBar, bottomBar, nil, nil, tabs) +} + +func buildDependenciesTab(state *appState) fyne.CanvasObject { + content := container.NewVBox() + + // Header + header := widget.NewLabel("System Dependencies") + header.TextStyle = fyne.TextStyle{Bold: true} + content.Add(header) + + desc := widget.NewLabel("Manage VideoTools dependencies. Some modules require specific tools to be installed.") + desc.Wrapping = fyne.TextWrapWord + content.Add(desc) + + content.Add(widget.NewSeparator()) + + // Check all dependencies + for depName, dep := range allDependencies { + isInstalled := checkDependency(dep.Command) + + nameLabel := widget.NewLabel(dep.Name) + nameLabel.TextStyle = fyne.TextStyle{Bold: true} + + statusLabel := widget.NewLabel("") + if isInstalled { + statusLabel.SetText("✓ Installed") + statusLabel.TextStyle = fyne.TextStyle{Italic: true} + } else { + statusLabel.SetText("✗ Not Installed") + statusLabel.TextStyle = fyne.TextStyle{Italic: true} + } + + descLabel := widget.NewLabel(dep.Description) + descLabel.TextStyle = fyne.TextStyle{Italic: true} + + installLabel := widget.NewLabel(dep.InstallCmd) + installLabel.Wrapping = fyne.TextWrapWord + + var statusColor color.Color + if isInstalled { + statusColor = utils.MustHex("#4CAF50") // Green + } else { + statusColor = utils.MustHex("#F44336") // Red + } + + statusBg := canvas.NewRectangle(statusColor) + statusBg.CornerRadius = 3 + statusBg.SetMinSize(fyne.NewSize(12, 12)) + + statusRow := container.NewHBox(statusBg, statusLabel) + + infoBox := container.NewVBox( + container.NewHBox(nameLabel, layout.NewSpacer(), statusRow), + descLabel, + ) + + if !isInstalled { + infoBox.Add(widget.NewLabel("Install: " + installLabel.Text)) + } + + // Check which modules need this dependency + modulesNeeding := []string{} + for modID, deps := range moduleDependencies { + for _, d := range deps { + if d == depName { + // Find module name + for _, m := range modulesList { + if m.ID == modID { + modulesNeeding = append(modulesNeeding, m.Label) + break + } + } + break + } + } + } + + if len(modulesNeeding) > 0 { + neededLabel := widget.NewLabel("Required by: " + strings.Join(modulesNeeding, ", ")) + neededLabel.TextStyle = fyne.TextStyle{Italic: true} + infoBox.Add(neededLabel) + } + + cardBg := canvas.NewRectangle(utils.MustHex("#171C2A")) + cardBg.CornerRadius = 6 + card := container.NewPadded(container.NewMax(cardBg, infoBox)) + content.Add(card) + } + + // Refresh button + content.Add(widget.NewSeparator()) + refreshBtn := widget.NewButton("Refresh Status", func() { + state.showSettingsView() + }) + content.Add(refreshBtn) + + return container.NewVScroll(content) +} + +func buildPreferencesTab(state *appState) fyne.CanvasObject { + content := container.NewVBox() + + header := widget.NewLabel("Application Preferences") + header.TextStyle = fyne.TextStyle{Bold: true} + content.Add(header) + + content.Add(widget.NewLabel("Preferences panel - Coming soon")) + content.Add(widget.NewLabel("This will include settings for:")) + content.Add(widget.NewLabel("• Default output directories")) + content.Add(widget.NewLabel("• Default encoding presets")) + content.Add(widget.NewLabel("• UI theme preferences")) + content.Add(widget.NewLabel("• Automatic updates")) + + return container.NewVScroll(content) +} + +func (s *appState) showSettingsView() { + s.stopPreview() + s.lastModule = s.active + s.active = "settings" + s.setContent(buildSettingsView(s)) +}