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