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