import SwiftUI /// Theme system for MixBoard iOS. /// Multiple skins from retro to modern aesthetics. final class AppTheme: ObservableObject { // MARK: - Available Skins enum Skin: String, CaseIterable, Identifiable, Codable { case winamp = "Winamp" case foobarLight = "foobar Light" case foobarDark = "foobar Dark" case wmp = "Windows Media" case obsidian = "Obsidian" case vinyl = "Vinyl" case tidal = "Tidal" var id: String { rawValue } var icon: String { switch self { case .winamp: return "bolt.fill" case .foobarLight: return "list.bullet" case .foobarDark: return "list.bullet.rectangle" case .wmp: return "play.rectangle.fill" case .obsidian: return "diamond.fill" case .vinyl: return "record.circle" case .tidal: return "waveform" } } var description: String { switch self { case .winamp: return "Neon retro vibes" case .foobarLight: return "Clean & minimal, light" case .foobarDark: return "Clean & minimal, dark" case .wmp: return "Glossy blue media player" case .obsidian: return "Deep purple, elegant" case .vinyl: return "Warm analog warmth" case .tidal: return "Dark with teal glow" } } } // MARK: - Published @Published var currentSkin: Skin { didSet { applySkin(currentSkin) UserDefaults.standard.set(currentSkin.rawValue, forKey: "appThemeSkin") } } // MARK: - Colors @Published var accent: Color = .green @Published var background: Color = .black @Published var secondaryBackground: Color = Color(white: 0.1) @Published var seekbarBackground: Color = Color.gray.opacity(0.3) @Published var seekbarForeground: Color = .green @Published var playerBarBackground: Color = Color(white: 0.08) @Published var cardBackground: Color = Color(white: 0.12) @Published var primaryText: Color = .white @Published var secondaryText: Color = .gray @Published var tertiaryText: Color = Color.gray.opacity(0.5) @Published var playingHighlight: Color = .green @Published var groupHeaderText: Color = .white @Published var tabBarBackground: Color = Color(white: 0.06) @Published var separatorColor: Color = Color.white.opacity(0.1) // MARK: - Sizes @Published var seekbarHeight: CGFloat = 6 @Published var rowHeight: CGFloat = 56 @Published var dataFontSize: CGFloat = 15 @Published var smallFontSize: CGFloat = 12 @Published var cornerRadius: CGFloat = 8 // MARK: - Style flags @Published var useDarkMode: Bool = true @Published var preferredColorScheme: ColorScheme? = .dark // MARK: - Init init() { let savedSkin = UserDefaults.standard.string(forKey: "appThemeSkin") .flatMap { Skin(rawValue: $0) } ?? .winamp self.currentSkin = savedSkin applySkin(savedSkin) } // MARK: - Apply Skin private func applySkin(_ skin: Skin) { switch skin { // ── Winamp Classic ────────────────────────────── // Dark background, neon green text, retro aesthetic case .winamp: accent = Color(red: 0.0, green: 1.0, blue: 0.0) background = Color(red: 0.06, green: 0.06, blue: 0.08) secondaryBackground = Color(red: 0.1, green: 0.1, blue: 0.12) seekbarBackground = Color(red: 0.15, green: 0.15, blue: 0.15) seekbarForeground = Color(red: 0.0, green: 1.0, blue: 0.0) playerBarBackground = Color(red: 0.08, green: 0.08, blue: 0.1) cardBackground = Color(red: 0.1, green: 0.1, blue: 0.12) primaryText = Color(red: 0.0, green: 0.9, blue: 0.0) secondaryText = Color(red: 0.0, green: 0.65, blue: 0.0) tertiaryText = Color(red: 0.0, green: 0.35, blue: 0.0) playingHighlight = Color(red: 0.0, green: 1.0, blue: 0.0) groupHeaderText = Color(red: 0.0, green: 1.0, blue: 0.0) tabBarBackground = Color(red: 0.05, green: 0.05, blue: 0.07) separatorColor = Color(red: 0.0, green: 0.3, blue: 0.0) seekbarHeight = 6; rowHeight = 52; dataFontSize = 14; smallFontSize = 11; cornerRadius = 4 useDarkMode = true; preferredColorScheme = .dark // ── foobar2000 Light ──────────────────────────── // Clean white, minimal, data-focused — default foobar aesthetic case .foobarLight: accent = Color(red: 0.0, green: 0.0, blue: 0.55) background = Color(uiColor: .systemBackground) secondaryBackground = Color(uiColor: .secondarySystemBackground) seekbarBackground = Color(uiColor: .systemGray5) seekbarForeground = Color(red: 0.0, green: 0.0, blue: 0.55) playerBarBackground = Color(uiColor: .secondarySystemBackground) cardBackground = Color(uiColor: .secondarySystemGroupedBackground) primaryText = Color(uiColor: .label) secondaryText = Color(uiColor: .secondaryLabel) tertiaryText = Color(uiColor: .tertiaryLabel) playingHighlight = Color(red: 0.0, green: 0.0, blue: 0.55) groupHeaderText = Color(uiColor: .label) tabBarBackground = Color(uiColor: .secondarySystemBackground) separatorColor = Color(uiColor: .separator) seekbarHeight = 4; rowHeight = 44; dataFontSize = 14; smallFontSize = 11; cornerRadius = 6 useDarkMode = false; preferredColorScheme = .light // ── foobar2000 Dark ───────────────────────────── // Dark mode foobar — charcoal gray, muted blue accent case .foobarDark: accent = Color(red: 0.45, green: 0.55, blue: 0.75) background = Color(red: 0.11, green: 0.11, blue: 0.12) secondaryBackground = Color(red: 0.15, green: 0.15, blue: 0.16) seekbarBackground = Color(red: 0.2, green: 0.2, blue: 0.22) seekbarForeground = Color(red: 0.45, green: 0.55, blue: 0.75) playerBarBackground = Color(red: 0.13, green: 0.13, blue: 0.14) cardBackground = Color(red: 0.16, green: 0.16, blue: 0.17) primaryText = Color(red: 0.88, green: 0.88, blue: 0.9) secondaryText = Color(red: 0.55, green: 0.55, blue: 0.58) tertiaryText = Color(red: 0.38, green: 0.38, blue: 0.4) playingHighlight = Color(red: 0.45, green: 0.55, blue: 0.75) groupHeaderText = Color(red: 0.65, green: 0.65, blue: 0.7) tabBarBackground = Color(red: 0.09, green: 0.09, blue: 0.1) separatorColor = Color(red: 0.22, green: 0.22, blue: 0.24) seekbarHeight = 4; rowHeight = 44; dataFontSize = 14; smallFontSize = 11; cornerRadius = 6 useDarkMode = true; preferredColorScheme = .dark // ── Windows Media Player ──────────────────────── // Glossy dark blue, silver chrome, WMP 11 aesthetic case .wmp: accent = Color(red: 0.2, green: 0.5, blue: 0.95) background = Color(red: 0.05, green: 0.08, blue: 0.15) secondaryBackground = Color(red: 0.08, green: 0.12, blue: 0.2) seekbarBackground = Color(red: 0.12, green: 0.16, blue: 0.25) seekbarForeground = Color(red: 0.3, green: 0.6, blue: 1.0) playerBarBackground = Color(red: 0.06, green: 0.1, blue: 0.18) cardBackground = Color(red: 0.1, green: 0.14, blue: 0.22) primaryText = Color(red: 0.9, green: 0.92, blue: 0.96) secondaryText = Color(red: 0.55, green: 0.62, blue: 0.75) tertiaryText = Color(red: 0.35, green: 0.4, blue: 0.52) playingHighlight = Color(red: 0.3, green: 0.65, blue: 1.0) groupHeaderText = Color(red: 0.6, green: 0.7, blue: 0.9) tabBarBackground = Color(red: 0.04, green: 0.06, blue: 0.12) separatorColor = Color(red: 0.15, green: 0.2, blue: 0.3) seekbarHeight = 6; rowHeight = 50; dataFontSize = 14; smallFontSize = 11; cornerRadius = 8 useDarkMode = true; preferredColorScheme = .dark // ── Obsidian ──────────────────────────────────── // Deep dark with purple/violet accent, elegant and modern case .obsidian: accent = Color(red: 0.6, green: 0.4, blue: 0.9) background = Color(red: 0.07, green: 0.06, blue: 0.1) secondaryBackground = Color(red: 0.1, green: 0.09, blue: 0.14) seekbarBackground = Color(red: 0.15, green: 0.13, blue: 0.2) seekbarForeground = Color(red: 0.6, green: 0.4, blue: 0.9) playerBarBackground = Color(red: 0.08, green: 0.07, blue: 0.12) cardBackground = Color(red: 0.12, green: 0.1, blue: 0.16) primaryText = Color(red: 0.9, green: 0.88, blue: 0.95) secondaryText = Color(red: 0.55, green: 0.5, blue: 0.65) tertiaryText = Color(red: 0.35, green: 0.32, blue: 0.45) playingHighlight = Color(red: 0.65, green: 0.45, blue: 0.95) groupHeaderText = Color(red: 0.7, green: 0.6, blue: 0.85) tabBarBackground = Color(red: 0.05, green: 0.04, blue: 0.08) separatorColor = Color(red: 0.2, green: 0.17, blue: 0.28) seekbarHeight = 5; rowHeight = 52; dataFontSize = 15; smallFontSize = 12; cornerRadius = 12 useDarkMode = true; preferredColorScheme = .dark // ── Vinyl ─────────────────────────────────────── // Warm browns, cream text, analog feel — like vintage hi-fi equipment case .vinyl: accent = Color(red: 0.85, green: 0.55, blue: 0.2) // warm amber/orange background = Color(red: 0.12, green: 0.1, blue: 0.08) // dark walnut secondaryBackground = Color(red: 0.16, green: 0.13, blue: 0.1) seekbarBackground = Color(red: 0.22, green: 0.18, blue: 0.14) seekbarForeground = Color(red: 0.85, green: 0.55, blue: 0.2) playerBarBackground = Color(red: 0.14, green: 0.11, blue: 0.09) cardBackground = Color(red: 0.18, green: 0.15, blue: 0.11) primaryText = Color(red: 0.92, green: 0.88, blue: 0.78) // warm cream secondaryText = Color(red: 0.65, green: 0.58, blue: 0.48) tertiaryText = Color(red: 0.45, green: 0.4, blue: 0.32) playingHighlight = Color(red: 0.9, green: 0.6, blue: 0.2) groupHeaderText = Color(red: 0.8, green: 0.65, blue: 0.4) tabBarBackground = Color(red: 0.1, green: 0.08, blue: 0.06) separatorColor = Color(red: 0.25, green: 0.2, blue: 0.15) seekbarHeight = 5; rowHeight = 54; dataFontSize = 15; smallFontSize = 12; cornerRadius = 8 useDarkMode = true; preferredColorScheme = .dark // ── Tidal ─────────────────────────────────────── // Dark warm background, teal/cyan accent, like screenshot case .tidal: accent = Color(red: 0.2, green: 0.85, blue: 0.75) // teal/cyan background = Color(red: 0.08, green: 0.07, blue: 0.06) // warm near-black secondaryBackground = Color(red: 0.12, green: 0.11, blue: 0.09) seekbarBackground = Color(red: 0.25, green: 0.24, blue: 0.22) seekbarForeground = Color.white playerBarBackground = Color(red: 0.1, green: 0.09, blue: 0.07) cardBackground = Color(red: 0.14, green: 0.13, blue: 0.11) primaryText = Color.white secondaryText = Color(red: 0.65, green: 0.63, blue: 0.58) tertiaryText = Color(red: 0.4, green: 0.38, blue: 0.35) playingHighlight = Color(red: 0.2, green: 0.85, blue: 0.75) groupHeaderText = Color(red: 0.75, green: 0.73, blue: 0.68) tabBarBackground = Color(red: 0.06, green: 0.05, blue: 0.04) separatorColor = Color(red: 0.2, green: 0.19, blue: 0.17) seekbarHeight = 4; rowHeight = 56; dataFontSize = 15; smallFontSize = 12; cornerRadius = 10 useDarkMode = true; preferredColorScheme = .dark } } } // MARK: - Color hex helper extension Color { init?(hex: String) { var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") guard hexSanitized.count == 6, let rgb = UInt64(hexSanitized, radix: 16) else { return nil } self.init( red: Double((rgb >> 16) & 0xFF) / 255.0, green: Double((rgb >> 8) & 0xFF) / 255.0, blue: Double(rgb & 0xFF) / 255.0 ) } }