import SwiftUI /// Centralized theme system for MixBoard. /// Supports retro and modern skins inspired by classic players. final class AppTheme: ObservableObject { // MARK: - Available Skins enum Skin: String, CaseIterable, Identifiable, Codable { // Modern case dark = "Dark" case midnight = "Midnight" case forest = "Forest" case ocean = "Ocean" case warm = "Warm" case light = "Light" // Retro case winampClassic = "Winamp Classic" case winampModern = "Winamp Modern" case foobarDark = "foobar2000 Dark" case foobarLight = "foobar2000 Light" case win95 = "Windows 95" case win98 = "Windows 98" case xpLuna = "XP Luna" case macOSClassic = "Mac OS 9" var id: String { rawValue } /// Whether this skin requires dark or light appearance. var colorScheme: ColorScheme { switch self { case .dark, .midnight, .forest, .ocean, .warm, .winampClassic, .winampModern, .foobarDark: return .dark case .light, .foobarLight, .win95, .win98, .xpLuna, .macOSClassic: return .light } } } // MARK: - Published @Published var currentSkin: Skin { didSet { applySkin(currentSkin) UserDefaults.standard.set(currentSkin.rawValue, forKey: "appThemeSkin") } } /// The color scheme the current skin requires (dark or light). var preferredScheme: ColorScheme { currentSkin.colorScheme } // MARK: - Colors @Published var accent: Color = .green @Published var seekbarBackground: Color = Color.gray.opacity(0.3) @Published var seekbarForeground: Color = .green @Published var waveformBackground: Color = Color.gray.opacity(0.25) @Published var waveformForeground: Color = Color(red: 0.2, green: 0.7, blue: 0.3) @Published var waveformSeparator: Color = Color.black.opacity(0.6) @Published var playerBarBackground: Color = Color(nsColor: .controlBackgroundColor) @Published var toolbarBackground: Color = Color(nsColor: .controlBackgroundColor) @Published var columnHeaderBackground: Color = Color(nsColor: .controlBackgroundColor) @Published var primaryText: Color = .primary @Published var secondaryText: Color = .secondary @Published var tertiaryText: Color = Color.gray.opacity(0.5) @Published var playingHighlight: Color = .green @Published var groupHeaderText: Color = .primary // MARK: - Sizes @Published var seekbarHeight: CGFloat = 8 @Published var playerBarHeight: CGFloat = 32 @Published var rowHeight: CGFloat = 22 @Published var dataFontSize: CGFloat = 11 @Published var smallFontSize: CGFloat = 9 // MARK: - Init init() { let saved = UserDefaults.standard.string(forKey: "appThemeSkin") // Migrate legacy "foobar2000" → "foobar2000 Dark" let skinName = (saved == "foobar2000") ? "foobar2000 Dark" : saved let skin = skinName.flatMap { Skin(rawValue: $0) } ?? .dark self.currentSkin = skin applySkin(skin) } // MARK: - Apply Skin private func applySkin(_ skin: Skin) { // Reset to defaults first // Base sizes — readable defaults seekbarHeight = 8 playerBarHeight = 36 rowHeight = 24 dataFontSize = 13 smallFontSize = 11 playerBarBackground = Color(nsColor: .controlBackgroundColor) toolbarBackground = Color(nsColor: .controlBackgroundColor) columnHeaderBackground = Color(nsColor: .controlBackgroundColor) switch skin { // ── Modern Skins ────────────────────────────────── case .dark: accent = Color(red: 0.3, green: 0.85, blue: 0.4) seekbarBackground = Color(red: 0.15, green: 0.25, blue: 0.35) seekbarForeground = Color(red: 0.3, green: 0.85, blue: 0.4) waveformBackground = Color(red: 0.12, green: 0.15, blue: 0.22) waveformForeground = Color(red: 0.3, green: 0.7, blue: 1.0) waveformSeparator = Color.black.opacity(0.7) playingHighlight = Color(red: 0.3, green: 0.85, blue: 0.4) primaryText = Color.white secondaryText = Color.white.opacity(0.75) tertiaryText = Color.white.opacity(0.5) groupHeaderText = Color.white case .midnight: accent = Color(red: 0.5, green: 0.6, blue: 1.0) seekbarBackground = Color(red: 0.2, green: 0.1, blue: 0.15) seekbarForeground = Color(red: 0.5, green: 0.6, blue: 1.0) waveformBackground = Color(red: 0.15, green: 0.08, blue: 0.12) waveformForeground = Color(red: 0.9, green: 0.3, blue: 0.6) waveformSeparator = Color.black.opacity(0.7) playingHighlight = Color(red: 0.5, green: 0.6, blue: 1.0) primaryText = Color.white secondaryText = Color.white.opacity(0.75) tertiaryText = Color.white.opacity(0.45) groupHeaderText = Color.white case .forest: accent = Color(red: 0.35, green: 0.85, blue: 0.35) seekbarBackground = Color(red: 0.25, green: 0.2, blue: 0.1) seekbarForeground = Color(red: 0.35, green: 0.85, blue: 0.35) waveformBackground = Color(red: 0.18, green: 0.14, blue: 0.06) waveformForeground = Color(red: 0.95, green: 0.75, blue: 0.2) waveformSeparator = Color.black.opacity(0.7) playingHighlight = Color(red: 0.35, green: 0.85, blue: 0.35) primaryText = Color.white secondaryText = Color.white.opacity(0.7) tertiaryText = Color.white.opacity(0.45) groupHeaderText = Color.white case .ocean: accent = Color(red: 0.3, green: 0.8, blue: 0.95) seekbarBackground = Color(red: 0.2, green: 0.12, blue: 0.1) seekbarForeground = Color(red: 0.3, green: 0.8, blue: 0.95) waveformBackground = Color(red: 0.15, green: 0.08, blue: 0.06) waveformForeground = Color(red: 1.0, green: 0.45, blue: 0.3) waveformSeparator = Color.black.opacity(0.7) playingHighlight = Color(red: 0.3, green: 0.8, blue: 0.95) primaryText = Color.white secondaryText = Color.white.opacity(0.75) tertiaryText = Color.white.opacity(0.45) groupHeaderText = Color.white case .warm: accent = Color(red: 1.0, green: 0.65, blue: 0.25) seekbarBackground = Color(red: 0.1, green: 0.18, blue: 0.2) seekbarForeground = Color(red: 1.0, green: 0.65, blue: 0.25) waveformBackground = Color(red: 0.06, green: 0.12, blue: 0.15) waveformForeground = Color(red: 0.2, green: 0.8, blue: 0.75) waveformSeparator = Color.black.opacity(0.7) playingHighlight = Color(red: 1.0, green: 0.65, blue: 0.25) primaryText = Color.white secondaryText = Color.white.opacity(0.75) tertiaryText = Color.white.opacity(0.45) groupHeaderText = Color.white case .light: accent = Color(red: 0.15, green: 0.45, blue: 0.85) seekbarBackground = Color(red: 0.92, green: 0.88, blue: 0.85) seekbarForeground = Color(red: 0.15, green: 0.45, blue: 0.85) waveformBackground = Color(red: 0.94, green: 0.9, blue: 0.88) waveformForeground = Color(red: 0.85, green: 0.3, blue: 0.15) waveformSeparator = Color.black.opacity(0.15) playingHighlight = Color(red: 0.15, green: 0.45, blue: 0.85) playerBarBackground = Color(red: 0.95, green: 0.95, blue: 0.96) toolbarBackground = Color(red: 0.95, green: 0.95, blue: 0.96) columnHeaderBackground = Color(red: 0.92, green: 0.92, blue: 0.93) primaryText = Color.black secondaryText = Color(red: 0.25, green: 0.25, blue: 0.3) tertiaryText = Color(red: 0.5, green: 0.5, blue: 0.55) groupHeaderText = Color.black // ── Retro Skins ────────────────────────────────── case .winampClassic: // Classic Winamp: dark background, neon green text, dark chrome accent = Color(red: 0.0, green: 1.0, blue: 0.0) // #00FF00 seekbarBackground = Color(red: 0.15, green: 0.15, blue: 0.15) seekbarForeground = Color(red: 0.0, green: 1.0, blue: 0.0) waveformBackground = Color(red: 0.05, green: 0.05, blue: 0.05) waveformForeground = Color(red: 1.0, green: 0.9, blue: 0.0) waveformSeparator = Color(red: 0.0, green: 0.3, blue: 0.0) playingHighlight = Color(red: 0.0, green: 1.0, blue: 0.0) playerBarBackground = Color(red: 0.12, green: 0.12, blue: 0.14) toolbarBackground = Color(red: 0.12, green: 0.12, blue: 0.14) columnHeaderBackground = Color(red: 0.1, green: 0.1, blue: 0.12) primaryText = Color(red: 0.0, green: 0.9, blue: 0.0) // green text secondaryText = Color(red: 0.0, green: 0.7, blue: 0.0) tertiaryText = Color(red: 0.0, green: 0.4, blue: 0.0) groupHeaderText = Color(red: 0.0, green: 1.0, blue: 0.0) seekbarHeight = 6 dataFontSize = 11 case .winampModern: // Winamp Modern/Bento: dark blue-gray, orange accent accent = Color(red: 1.0, green: 0.55, blue: 0.0) // orange seekbarBackground = Color(red: 0.15, green: 0.17, blue: 0.22) seekbarForeground = Color(red: 1.0, green: 0.55, blue: 0.0) waveformBackground = Color(red: 0.08, green: 0.09, blue: 0.12) waveformForeground = Color(red: 0.3, green: 0.85, blue: 0.9) waveformSeparator = Color(red: 0.25, green: 0.15, blue: 0.0) playingHighlight = Color(red: 1.0, green: 0.55, blue: 0.0) playerBarBackground = Color(red: 0.13, green: 0.14, blue: 0.18) toolbarBackground = Color(red: 0.13, green: 0.14, blue: 0.18) columnHeaderBackground = Color(red: 0.11, green: 0.12, blue: 0.16) primaryText = Color(red: 0.85, green: 0.85, blue: 0.9) secondaryText = Color(red: 0.6, green: 0.6, blue: 0.65) tertiaryText = Color(red: 0.4, green: 0.4, blue: 0.45) groupHeaderText = Color(red: 1.0, green: 0.55, blue: 0.0) case .foobarDark: // foobar2000 Dark: dark gray background, light text, minimal chrome accent = Color(red: 0.35, green: 0.55, blue: 0.85) // muted blue seekbarBackground = Color(red: 0.25, green: 0.25, blue: 0.25) seekbarForeground = Color(red: 0.35, green: 0.55, blue: 0.85) waveformBackground = Color(red: 0.1, green: 0.1, blue: 0.12) waveformForeground = Color(red: 0.9, green: 0.65, blue: 0.2) waveformSeparator = Color(red: 0.18, green: 0.18, blue: 0.25) playingHighlight = Color(red: 0.35, green: 0.55, blue: 0.85) playerBarBackground = Color(red: 0.14, green: 0.14, blue: 0.14) toolbarBackground = Color(red: 0.14, green: 0.14, blue: 0.14) columnHeaderBackground = Color(red: 0.16, green: 0.16, blue: 0.16) primaryText = Color(red: 0.85, green: 0.85, blue: 0.85) secondaryText = Color(red: 0.6, green: 0.6, blue: 0.6) tertiaryText = Color(red: 0.4, green: 0.4, blue: 0.4) groupHeaderText = Color(red: 0.85, green: 0.85, blue: 0.85) rowHeight = 18 dataFontSize = 11 case .foobarLight: // foobar2000 Light: classic foobar — white/light gray, minimal, system colors accent = Color(red: 0.0, green: 0.0, blue: 0.5) // navy selection seekbarBackground = Color(red: 0.85, green: 0.85, blue: 0.85) seekbarForeground = Color(red: 0.0, green: 0.0, blue: 0.6) waveformBackground = Color(red: 0.93, green: 0.93, blue: 0.95) waveformForeground = Color(red: 0.7, green: 0.15, blue: 0.1) waveformSeparator = Color(red: 0.75, green: 0.75, blue: 0.75) playingHighlight = Color(red: 0.0, green: 0.0, blue: 0.5) playerBarBackground = Color(red: 0.94, green: 0.94, blue: 0.94) toolbarBackground = Color(red: 0.94, green: 0.94, blue: 0.94) columnHeaderBackground = Color(red: 0.9, green: 0.9, blue: 0.9) primaryText = Color.black secondaryText = Color(red: 0.3, green: 0.3, blue: 0.3) tertiaryText = Color(red: 0.55, green: 0.55, blue: 0.55) groupHeaderText = Color.black rowHeight = 18 dataFontSize = 11 case .win95: // Windows 95: classic silver/gray, teal highlights, 3D beveled look accent = Color(red: 0.0, green: 0.0, blue: 0.5) // navy blue seekbarBackground = Color(red: 0.75, green: 0.75, blue: 0.75) seekbarForeground = Color(red: 0.0, green: 0.0, blue: 0.5) waveformBackground = Color(red: 0.65, green: 0.65, blue: 0.65) waveformForeground = Color(red: 0.0, green: 0.5, blue: 0.5) waveformSeparator = Color(red: 0.5, green: 0.5, blue: 0.5) playingHighlight = Color(red: 0.0, green: 0.0, blue: 0.5) playerBarBackground = Color(red: 0.75, green: 0.75, blue: 0.75) // classic gray toolbarBackground = Color(red: 0.75, green: 0.75, blue: 0.75) columnHeaderBackground = Color(red: 0.8, green: 0.8, blue: 0.8) primaryText = Color.black secondaryText = Color(red: 0.25, green: 0.25, blue: 0.25) tertiaryText = Color(red: 0.5, green: 0.5, blue: 0.5) groupHeaderText = Color.black dataFontSize = 11 seekbarHeight = 10 case .win98: // Windows 98/2000: slightly softer than 95, classic blue title bars accent = Color(red: 0.0, green: 0.0, blue: 0.65) seekbarBackground = Color(red: 0.82, green: 0.82, blue: 0.82) seekbarForeground = Color(red: 0.0, green: 0.27, blue: 0.65) waveformBackground = Color(red: 0.72, green: 0.72, blue: 0.72) waveformForeground = Color(red: 0.0, green: 0.5, blue: 0.25) waveformSeparator = Color(red: 0.55, green: 0.55, blue: 0.55) playingHighlight = Color(red: 0.0, green: 0.0, blue: 0.65) playerBarBackground = Color(red: 0.83, green: 0.82, blue: 0.78) // warm gray toolbarBackground = Color(red: 0.83, green: 0.82, blue: 0.78) columnHeaderBackground = Color(red: 0.87, green: 0.86, blue: 0.82) primaryText = Color.black secondaryText = Color(red: 0.2, green: 0.2, blue: 0.2) tertiaryText = Color(red: 0.45, green: 0.45, blue: 0.45) groupHeaderText = Color(red: 0.0, green: 0.0, blue: 0.4) seekbarHeight = 10 case .xpLuna: // Windows XP Luna: blue toolbar, olive/silver accents, rounded accent = Color(red: 0.22, green: 0.44, blue: 0.87) // XP blue seekbarBackground = Color(red: 0.88, green: 0.9, blue: 0.94) seekbarForeground = Color(red: 0.22, green: 0.44, blue: 0.87) waveformBackground = Color(red: 0.82, green: 0.85, blue: 0.92) waveformForeground = Color(red: 0.1, green: 0.6, blue: 0.3) waveformSeparator = Color(red: 0.7, green: 0.73, blue: 0.82) playingHighlight = Color(red: 0.22, green: 0.44, blue: 0.87) playerBarBackground = Color(red: 0.92, green: 0.93, blue: 0.96) // XP light blue toolbarBackground = Color(red: 0.85, green: 0.89, blue: 0.95) columnHeaderBackground = Color(red: 0.88, green: 0.91, blue: 0.96) primaryText = Color.black secondaryText = Color(red: 0.2, green: 0.2, blue: 0.3) tertiaryText = Color(red: 0.45, green: 0.45, blue: 0.55) groupHeaderText = Color(red: 0.1, green: 0.2, blue: 0.6) seekbarHeight = 10 case .macOSClassic: // Classic Mac OS 9: platinum gray, black text, pinstripes accent = Color(red: 0.0, green: 0.0, blue: 0.6) seekbarBackground = Color(red: 0.8, green: 0.8, blue: 0.8) seekbarForeground = Color(red: 0.3, green: 0.3, blue: 0.85) waveformBackground = Color(red: 0.75, green: 0.75, blue: 0.75) waveformForeground = Color(red: 0.7, green: 0.3, blue: 0.1) waveformSeparator = Color(red: 0.6, green: 0.6, blue: 0.6) playingHighlight = Color(red: 0.3, green: 0.3, blue: 0.85) playerBarBackground = Color(red: 0.86, green: 0.86, blue: 0.86) // platinum toolbarBackground = Color(red: 0.86, green: 0.86, blue: 0.86) columnHeaderBackground = Color(red: 0.9, green: 0.9, blue: 0.9) primaryText = Color.black secondaryText = Color(red: 0.2, green: 0.2, blue: 0.2) tertiaryText = Color(red: 0.5, green: 0.5, blue: 0.5) groupHeaderText = Color.black dataFontSize = 12 seekbarHeight = 8 } } }