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
}
}