package web import ( "encoding/json" "fmt" "html/template" "strings" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/installer" "gitgud.io/mike/mpv-manager/pkg/locale" "gitgud.io/mike/mpv-manager/pkg/platform" "gitgud.io/mike/mpv-manager/pkg/version" ) type DashboardData struct { Title string AppVersion string Platform *platform.Platform InstalledApps []config.InstalledApp Updates *UpdateStatus Config *ConfigSettings ConfigBackups []ConfigBackup InstallMethods []installer.InstallMethod InstalledAppGroups map[string][]AppGroupInfo ManagerUpdate *ManagerUpdateInfo ClientUpdates []ClientUpdateInfo HWAOptions []string CurrentHWA string RecommendedHWA string Nav []NavItem SystemInfo *SystemInfo SelfUpdateDisabled bool // True when built with ldflag for package managers (Arch AUR, etc.) } func IsSelfUpdateDisabled() bool { return version.SelfUpdateDisabled == "true" } type AppGroupInfo struct { App config.InstalledApp Logo string TypeTag string TypeLogo string // Logo for the install method type (flatpak, snap, portable, etc.) UpdateAvailable bool UpdateChecked bool CurrentVersion string AvailableVersion string Platform *platform.Platform AppName string AppType string InstallMethod string UIType string // "uosc", "modernz", or empty Version string // Installed version from config Managed bool // true if installed/configured by MPV Manager, false if externally detected } // AppCardTemplateData is used to pass app and platform data to the app-card template type AppCardTemplateData struct { App AppGroupInfo Platform *platform.Platform } type ManagerUpdateInfo struct { CurrentVersion string LatestVersion string UpdateAvailable bool } // SystemInfo holds application and environment information for the system page type SystemInfo struct { // Application info AppName string AppVersion string GoVersion string BuildTime string RepositoryURL string RepositoryName string // Environment paths (platform-specific) InstallPath string MPVConfigPath string LogPath string BackupPath string } type ClientUpdateInfo struct { AppName string CurrentVersion string AvailableVersion string MethodID string Logo string UpdateAvailable bool PackageManager string } type UpdateStatus struct { ManagerUpdateAvailable bool ManagerCurrentVersion string ManagerLatestVersion string AppUpdatesAvailable int Apps []AppUpdateInfo } type AppUpdateInfo struct { AppName string CurrentVersion string AvailableVersion string } type ConfigBackup struct { Filename string CreatedDate string FilePath string } type ConfigSettings struct { AudioLanguage string AudioLanguages []string SubtitleLanguage string SubtitleLanguages []string HardwareAccel string VideoOutputDriver string VideoScaleFilter string DitherAlgorithm string SavePositionOnQuit string Border string Profile string ConfigFile string // Screenshot settings (NEW) ScreenshotFormat string ScreenshotTagColorspace string ScreenshotHighBitDepth string ScreenshotTemplate string ScreenshotDirectory string ScreenshotJpegQuality string ScreenshotPngCompression string ScreenshotWebpLossless string ScreenshotWebpQuality string ScreenshotJxlDistance string ScreenshotAvifEncoder string ScreenshotAvifPixfmt string ScreenshotAvifOpts string } type LanguagesData struct { Title string AppVersion string Platform *platform.Platform Config *ConfigSettings CommonLanguages []locale.LocaleEntry AllLanguageOptions []LanguageOption CommonLanguageOptions []LanguageOption Nav []NavItem } type NavItem struct { Icon template.HTML Name string URL string Active bool } var NavItems = []NavItem{ {Icon: template.HTML(""), Name: "Dashboard", URL: "/dashboard", Active: true}, {Icon: template.HTML(""), Name: "MPV Player Apps", URL: "/apps"}, {Icon: template.HTML(""), Name: "Hardware Acceleration", URL: "/hwaccel"}, {Icon: template.HTML(""), Name: "MPV Config Management", URL: "/config"}, {Icon: template.HTML(""), Name: "Language Preferences", URL: "/languages"}, {Icon: template.HTML(""), Name: "Keyboard Shortcuts", URL: "/hotkeys"}, {Icon: template.HTML(""), Name: "System Information", URL: "/system"}, {Icon: template.HTML(""), Name: "Logs Viewer", URL: "/logs"}, {Icon: template.HTML(""), Name: "Manager Settings", URL: "/settings"}, } func getCodecColor(codec string) string { codec = strings.ToLower(codec) switch codec { case "av1": return "text-amber-500" case "av2": return "text-pink-700" case "vvc": return "text-blue-500" case "hevc": return "text-blue-400" case "vp9": return "text-green-500" case "vp8": return "text-green-400" case "avc": return "text-red-600" case "mpeg2", "mpeg-2": return "text-purple-600" case "vc1", "vc-1": return "text-orange-600" case "prores": return "text-white" default: return "text-gray-400" } } func formatCodecName(codec string) string { codec = strings.ToUpper(codec) switch codec { case "MPEG2": return "MPEG-2" case "VC1": return "VC-1" default: return codec } } func getCodecBadgeColor(codec string) string { codec = strings.ToLower(codec) switch codec { case "av1": return "bg-amber-500" case "av2": return "bg-pink-700" case "vvc": return "bg-blue-500" case "hevc": return "bg-blue-600" case "vp9": return "bg-green-600" case "vp8": return "bg-green-500" case "avc": return "bg-red-600" case "mpeg2", "mpeg-2": return "bg-purple-600" case "vc1", "vc-1": return "bg-orange-600" case "prores": return "bg-black text-white" default: return "bg-gray-600" } } func LanguageName(code string) string { if code == "" { return "Not set" } entries, err := LoadLocales() if err != nil { return code } langEntry := locale.FindByLanguageCode(code, entries) if langEntry != nil { return langEntry.LanguageName } return code } func getVendorColor(vendor string) string { vendor = strings.ToLower(vendor) switch vendor { case "intel", "genuineintel": return "text-blue-400" case "amd", "authenticamd": return "text-red-400" case "nvidia": return "text-green-400" case "apple": return "apple-gradient" default: return "text-white" } } func getVendorClass(vendor string) string { vendor = strings.ToLower(vendor) switch vendor { case "intel", "genuineintel": return "text-blue-400" case "amd", "authenticamd": return "text-red-400" case "nvidia": return "text-green-400" case "apple": return "apple-gradient" default: return "text-white" } } func getVendorDisplay(vendor string) string { vendorLower := strings.ToLower(vendor) switch vendorLower { case "intel", "genuineintel": return "Intel" case "amd", "authenticamd": return "AMD" case "nvidia": return "NVIDIA" case "apple": return "Apple" default: return vendor } } func isX86Architecture(arch string) bool { arch = strings.ToLower(arch) return arch == "amd64" || arch == "x86_64" || arch == "386" } func isARMArchitecture(arch string) bool { arch = strings.ToLower(arch) return arch == "arm64" || arch == "aarch64" || arch == "arm" } // getDistroLogo returns logo filename for a Linux distribution func getDistroLogo(distro string) string { distroLower := strings.ToLower(distro) // Map distro names to logo filenames switch { case distroLower == "ubuntu" || distroLower == "kubuntu" || strings.Contains(distroLower, "ubuntu"): return "ubuntu.avif" case distroLower == "debian": return "debian.avif" case distroLower == "fedora": return "fedora.avif" case distroLower == "arch": return "arch.avif" case distroLower == "cachyos": return "cachyos.svg" case distroLower == "opensuse" || distroLower == "suse": return "opensuse.avif" case distroLower == "centos": return "centos.svg" case distroLower == "redhat" || distroLower == "rhel": return "redhat.avif" case distroLower == "rocky": return "rocky.avif" case distroLower == "almalinux": return "almalinux.avif" case distroLower == "linuxmint" || distroLower == "mint": return "mint.avif" case distroLower == "pop" || distroLower == "popos" || distroLower == "pop!_os": return "popos.avif" case distroLower == "manjaro": return "manjaro.svg" case distroLower == "endeavouros" || distroLower == "endeavor": return "endeavor.avif" case distroLower == "garuda": return "garuda.avif" case distroLower == "gentoo": return "gentoo.avif" case distroLower == "nixos": return "nixos.avif" case distroLower == "void": return "void.avif" case distroLower == "slackware": return "slackware.avif" case distroLower == "alpine": return "alpine.avif" case distroLower == "clearlinux": return "clearlinux.avif" case distroLower == "oracle": return "oracle.avif" case distroLower == "artix": return "artix.avif" case distroLower == "steamos": return "steamos.avif" case distroLower == "zorin": return "zorin.avif" case distroLower == "bazzite": return "bazzite.avif" case distroLower == "gobolinux": return "gobolinux.avif" case distroLower == "kdeneon": return "kdeneon.avif" case distroLower == "ubuntustudio": return "ubuntustudio.avif" default: return "linux.avif" } } // getPackageTypeLogo returns the logo filename for a package type (e.g., Flatpak, portable) // For package manager type, it returns an empty string to allow caller to use distro-specific logo // For installed apps, returns platform-specific icons func getPackageTypeLogo(pkgType string, p *platform.Platform) string { switch strings.ToLower(pkgType) { case "flatpak": return "flatpak.avif" case "portable": return "portable.avif" case "installed app": // Return platform-specific icon for installed apps if p != nil { if p.IsDarwin() { return "mac-app.avif" } if p.IsWindows() { return "win-app.avif" } } return "installed-app.avif" // fallback for Linux case "package manager": // On macOS, return brew icon since Homebrew is the package manager if p != nil && p.IsDarwin() { return "brew.avif" } // Return empty string - caller should use distro logo instead return "" default: return "" } } // ModalConfig represents configuration for confirmation modals type ModalConfig struct { Title string `json:"title"` Message string `json:"message"` ConfirmText string `json:"confirm_text"` CancelText string `json:"cancel_text"` ConfirmURL string `json:"confirm_url"` ConfirmData string `json:"confirm_data"` Danger bool `json:"danger"` Success bool `json:"success"` // For success modals (green styling, no cancel button) // HTMX partial refresh configuration (optional, for partial page refresh) HTMXRefresh string `json:"htmx_refresh,omitempty"` // URL to fetch for refresh HTMXTarget string `json:"htmx_target,omitempty"` // ID of element to refresh } // toJSON converts a value to a JSON string for use in templates func toJSON(v interface{}) string { b, err := json.Marshal(v) if err != nil { return "[]" } return string(b) } // toString converts any type to a string for use in templates // This is needed for custom types like platform.OSType func toString(v interface{}) string { switch val := v.(type) { case string: return val case int, int8, int16, int32, int64: return fmt.Sprintf("%d", val) case uint, uint8, uint16, uint32, uint64: return fmt.Sprintf("%d", val) case float32, float64: return fmt.Sprintf("%f", val) case bool: return fmt.Sprintf("%t", val) default: return fmt.Sprintf("%v", val) } } // capitalize capitalizes the first letter of a string func capitalize(s string) string { if len(s) == 0 { return s } return strings.ToUpper(string(s[0])) + s[1:] } // capitalizeArchitecture formats architecture string with proper capitalization func capitalizeArchitecture(arch string) string { archLower := strings.ToLower(arch) switch archLower { case "amd64", "x86_64": return "AMD64" case "arm64", "aarch64": return "ARM64" case "arm": return "ARM" case "386": return "x86" default: return strings.ToUpper(arch) } } // getOSLogo returns logo filename for OS type func getOSLogo(osType string) string { osLower := strings.ToLower(osType) switch osLower { case "linux": return "linux.avif" case "windows": return "windows.avif" case "darwin", "macos": return "macos.avif" default: return "linux.avif" // fallback } } // getOSDisplayName returns the display name for OS type func getOSDisplayName(osType string) string { osLower := strings.ToLower(osType) switch osLower { case "linux": return "Linux" case "windows": return "Windows" case "darwin": return "macOS" default: return strings.Title(osType) // fallback with title case } } // getDistroFamilyLogo returns logo filename for distro family func getDistroFamilyLogo(family string) string { familyLower := strings.ToLower(family) switch familyLower { case "debian": return "debian.avif" case "rhel", "redhat", "red hat": return "redhat.avif" case "fedora": return "fedora.avif" case "arch": return "arch.avif" case "opensuse", "suse": return "opensuse.avif" case "ubuntu": return "ubuntu.avif" case "centos": return "centos.avif" // if we have it, otherwise fallback case "rocky": return "rocky.avif" // if we have it, otherwise fallback case "almalinux": return "almalinux.avif" case "manjaro": return "manjaro.avif" // if we have it, otherwise fallback case "endeavouros": return "endeavouros.avif" // if we have it, otherwise fallback case "nixos": return "nixos.avif" // if we have it, otherwise fallback case "void": return "void.avif" // if we have it, otherwise fallback default: // Try to find a matching distro logo return getDistroLogo(family) } } // MajorLanguageData holds data for major language display type MajorLanguageData struct { LanguageCode string `json:"language_code"` LanguageName string `json:"language_name"` LanguageLocal string `json:"language_local"` Flag string `json:"flag"` } // RegionalVariantData holds data for regional variant display type RegionalVariantData struct { Locale string `json:"locale"` CountryName string `json:"country_name"` CountryLocal string `json:"country_local"` CountryCode string `json:"country_code"` Flag string `json:"flag"` MajorRegionalVariant bool `json:"major_regional_variant"` Population int `json:"population"` } // ExtendedRegionalData holds regional data with parent language info for search type ExtendedRegionalData struct { Locale string `json:"locale"` CountryName string `json:"country_name"` CountryLocal string `json:"country_local"` CountryCode string `json:"country_code"` Flag string `json:"flag"` MajorRegionalVariant bool `json:"major_regional_variant"` Population int `json:"population"` LanguageCode string `json:"language_code"` LanguageName string `json:"language_name"` LanguageLocal string `json:"language_local"` } // LanguagePriorityData holds data for language priority list display type LanguagePriorityData struct { Index int `json:"index"` LanguageCode string `json:"language_code"` LanguageName string `json:"language_name"` LanguageLocal string `json:"language_local"` Flag string `json:"flag"` Locale string `json:"locale"` } // LanguagesPageData holds data for the language preferences page type LanguagesPageData struct { Title string Nav []NavItem Config *ConfigSettings AppVersion string MajorLanguages []MajorLanguageData SelectedMajorLanguage *MajorLanguageData RegionalVariants []RegionalVariantData AudioPriority []LanguagePriorityData SubtitlePriority []LanguagePriorityData } // HotkeyCategoryData holds hotkeys grouped by category type HotkeyCategoryData struct { Category string Hotkeys []HotkeyItem Count int } // HotkeyItem represents a single hotkey for display type HotkeyItem struct { ID string Keys string Description string Category string Priority int } // HotkeysPageData holds data for the keyboard shortcuts page type HotkeysPageData struct { Title string Nav []NavItem AppVersion string Categories []HotkeyCategoryData CategoryCounts []CategoryCountItem TotalCount int } // CategoryCountItem holds category name and count for filter buttons type CategoryCountItem struct { Category string Count int } // getHWABadgeColor returns CSS class for hardware acceleration status badge func getHWABadgeColor(hwa string) string { hwaLower := strings.ToLower(hwa) if hwaLower == "no" || hwaLower == "" { return "bg-red-600" // Disabled } return "bg-green-600" // Enabled } // getHWABadgeText returns "Enabled" or "Disabled" text for badge func getHWABadgeText(hwa string) string { hwaLower := strings.ToLower(hwa) if hwaLower == "no" || hwaLower == "" { return "Disabled" } return "Enabled" } // getGPUAPIDescription returns human-readable description for GPU API func getGPUAPIDescription(gpuAPI string) string { gpuAPILower := strings.ToLower(gpuAPI) switch gpuAPILower { case "auto": return "Automatic selection" case "opengl": return "OpenGL" case "vulkan": return "Vulkan" case "d3d11": return "Direct3D 11 (Windows)" case "d3d9": return "Direct3D 9 (Windows)" case "d3d9ex": return "Direct3D 9Ex (Windows)" case "dxinterop": return "DirectX Interop (Windows)" case "d3d11-interop": return "Direct3D 11 Interop (Windows)" case "metal": return "Metal (macOS)" case "": return "Not configured" default: return gpuAPI } } // categoryIcon returns a Boxicon class for a hotkey category func categoryIcon(category string) string { switch category { case "Playback": return "bx-play-circle" case "Seeking": return "bx-search-alt" case "Audio & Volume": return "bx-volume-full" case "Subtitles": return "bx-comment-detail" case "Video & Display": return "bx-desktop" case "Playlist": return "bx-list-ul" case "Screenshots": return "bx-camera" case "Advanced": return "bx-slider-alt" case "Menu Navigation": return "bx-menu" case "Multimedia Keys": return "bx-keyboard" default: return "bx-key" } } // formatKeys formats a key string into styled keyboard keys HTML func formatKeys(keys string) template.HTML { // Split by common separators parts := strings.Fields(keys) var html strings.Builder for i, part := range parts { // Check for separator tokens: / (or), + (connector), "or" // Only treat as separator if NOT at the last position // (if + or / is at the end, it's the actual key being pressed) if (part == "/" || part == "+" || part == "or") && i < len(parts)-1 { if part == "+" { html.WriteString(`+`) } else { html.WriteString(`` + part + ``) } continue } // Check if this is a single uppercase letter (requires Shift) if len(part) == 1 && part[0] >= 'A' && part[0] <= 'Z' { // Convert to "Shift + lowercase" format lowerKey := strings.ToLower(part) html.WriteString(`Shift`) html.WriteString(`+`) html.WriteString(`` + lowerKey + ``) } else if strings.Contains(part, "+") && len(part) > 1 { // Handle combined keys like "Ctrl+Shift+→" (but not standalone "+") subParts := strings.Split(part, "+") for j, subPart := range subParts { // Check if subPart is a single uppercase letter if len(subPart) == 1 && subPart[0] >= 'A' && subPart[0] <= 'Z' { // Already has modifiers, just convert to lowercase subPart = strings.ToLower(subPart) } if j > 0 { html.WriteString(`+`) } html.WriteString(`` + formatSingleKey(subPart) + ``) } } else { html.WriteString(`` + formatSingleKey(part) + ``) } } return template.HTML(html.String()) } // formatSingleKey formats a single key name for display func formatSingleKey(key string) string { // Convert arrow symbols to text or keep as is key = strings.TrimSpace(key) switch key { case "→": return "→" case "←": return "←" case "↑": return "↑" case "↓": return "↓" case "⌘": return "⌘" case "+": return "+" // Plus key case "-": return "-" // Minus key case "/": return "/" // Slash key case "*": return "*" // Asterisk key default: return key } }