AppTheme.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import SwiftUI
  2. /// Theme system for MixBoard iOS.
  3. /// Multiple skins from retro to modern aesthetics.
  4. final class AppTheme: ObservableObject {
  5. // MARK: - Available Skins
  6. enum Skin: String, CaseIterable, Identifiable, Codable {
  7. case winamp = "Winamp"
  8. case foobarLight = "foobar Light"
  9. case foobarDark = "foobar Dark"
  10. case wmp = "Windows Media"
  11. case obsidian = "Obsidian"
  12. case vinyl = "Vinyl"
  13. case tidal = "Tidal"
  14. var id: String { rawValue }
  15. var icon: String {
  16. switch self {
  17. case .winamp: return "bolt.fill"
  18. case .foobarLight: return "list.bullet"
  19. case .foobarDark: return "list.bullet.rectangle"
  20. case .wmp: return "play.rectangle.fill"
  21. case .obsidian: return "diamond.fill"
  22. case .vinyl: return "record.circle"
  23. case .tidal: return "waveform"
  24. }
  25. }
  26. var description: String {
  27. switch self {
  28. case .winamp: return "Neon retro vibes"
  29. case .foobarLight: return "Clean & minimal, light"
  30. case .foobarDark: return "Clean & minimal, dark"
  31. case .wmp: return "Glossy blue media player"
  32. case .obsidian: return "Deep purple, elegant"
  33. case .vinyl: return "Warm analog warmth"
  34. case .tidal: return "Dark with teal glow"
  35. }
  36. }
  37. }
  38. // MARK: - Published
  39. @Published var currentSkin: Skin {
  40. didSet {
  41. applySkin(currentSkin)
  42. UserDefaults.standard.set(currentSkin.rawValue, forKey: "appThemeSkin")
  43. }
  44. }
  45. // MARK: - Colors
  46. @Published var accent: Color = .green
  47. @Published var background: Color = .black
  48. @Published var secondaryBackground: Color = Color(white: 0.1)
  49. @Published var seekbarBackground: Color = Color.gray.opacity(0.3)
  50. @Published var seekbarForeground: Color = .green
  51. @Published var playerBarBackground: Color = Color(white: 0.08)
  52. @Published var cardBackground: Color = Color(white: 0.12)
  53. @Published var primaryText: Color = .white
  54. @Published var secondaryText: Color = .gray
  55. @Published var tertiaryText: Color = Color.gray.opacity(0.5)
  56. @Published var playingHighlight: Color = .green
  57. @Published var groupHeaderText: Color = .white
  58. @Published var tabBarBackground: Color = Color(white: 0.06)
  59. @Published var separatorColor: Color = Color.white.opacity(0.1)
  60. // MARK: - Sizes
  61. @Published var seekbarHeight: CGFloat = 6
  62. @Published var rowHeight: CGFloat = 56
  63. @Published var dataFontSize: CGFloat = 15
  64. @Published var smallFontSize: CGFloat = 12
  65. @Published var cornerRadius: CGFloat = 8
  66. // MARK: - Style flags
  67. @Published var useDarkMode: Bool = true
  68. @Published var preferredColorScheme: ColorScheme? = .dark
  69. // MARK: - Init
  70. init() {
  71. let savedSkin = UserDefaults.standard.string(forKey: "appThemeSkin")
  72. .flatMap { Skin(rawValue: $0) } ?? .winamp
  73. self.currentSkin = savedSkin
  74. applySkin(savedSkin)
  75. }
  76. // MARK: - Apply Skin
  77. private func applySkin(_ skin: Skin) {
  78. switch skin {
  79. // ── Winamp Classic ──────────────────────────────
  80. // Dark background, neon green text, retro aesthetic
  81. case .winamp:
  82. accent = Color(red: 0.0, green: 1.0, blue: 0.0)
  83. background = Color(red: 0.06, green: 0.06, blue: 0.08)
  84. secondaryBackground = Color(red: 0.1, green: 0.1, blue: 0.12)
  85. seekbarBackground = Color(red: 0.15, green: 0.15, blue: 0.15)
  86. seekbarForeground = Color(red: 0.0, green: 1.0, blue: 0.0)
  87. playerBarBackground = Color(red: 0.08, green: 0.08, blue: 0.1)
  88. cardBackground = Color(red: 0.1, green: 0.1, blue: 0.12)
  89. primaryText = Color(red: 0.0, green: 0.9, blue: 0.0)
  90. secondaryText = Color(red: 0.0, green: 0.65, blue: 0.0)
  91. tertiaryText = Color(red: 0.0, green: 0.35, blue: 0.0)
  92. playingHighlight = Color(red: 0.0, green: 1.0, blue: 0.0)
  93. groupHeaderText = Color(red: 0.0, green: 1.0, blue: 0.0)
  94. tabBarBackground = Color(red: 0.05, green: 0.05, blue: 0.07)
  95. separatorColor = Color(red: 0.0, green: 0.3, blue: 0.0)
  96. seekbarHeight = 6; rowHeight = 52; dataFontSize = 14; smallFontSize = 11; cornerRadius = 4
  97. useDarkMode = true; preferredColorScheme = .dark
  98. // ── foobar2000 Light ────────────────────────────
  99. // Clean white, minimal, data-focused — default foobar aesthetic
  100. case .foobarLight:
  101. accent = Color(red: 0.0, green: 0.0, blue: 0.55)
  102. background = Color(uiColor: .systemBackground)
  103. secondaryBackground = Color(uiColor: .secondarySystemBackground)
  104. seekbarBackground = Color(uiColor: .systemGray5)
  105. seekbarForeground = Color(red: 0.0, green: 0.0, blue: 0.55)
  106. playerBarBackground = Color(uiColor: .secondarySystemBackground)
  107. cardBackground = Color(uiColor: .secondarySystemGroupedBackground)
  108. primaryText = Color(uiColor: .label)
  109. secondaryText = Color(uiColor: .secondaryLabel)
  110. tertiaryText = Color(uiColor: .tertiaryLabel)
  111. playingHighlight = Color(red: 0.0, green: 0.0, blue: 0.55)
  112. groupHeaderText = Color(uiColor: .label)
  113. tabBarBackground = Color(uiColor: .secondarySystemBackground)
  114. separatorColor = Color(uiColor: .separator)
  115. seekbarHeight = 4; rowHeight = 44; dataFontSize = 14; smallFontSize = 11; cornerRadius = 6
  116. useDarkMode = false; preferredColorScheme = .light
  117. // ── foobar2000 Dark ─────────────────────────────
  118. // Dark mode foobar — charcoal gray, muted blue accent
  119. case .foobarDark:
  120. accent = Color(red: 0.45, green: 0.55, blue: 0.75)
  121. background = Color(red: 0.11, green: 0.11, blue: 0.12)
  122. secondaryBackground = Color(red: 0.15, green: 0.15, blue: 0.16)
  123. seekbarBackground = Color(red: 0.2, green: 0.2, blue: 0.22)
  124. seekbarForeground = Color(red: 0.45, green: 0.55, blue: 0.75)
  125. playerBarBackground = Color(red: 0.13, green: 0.13, blue: 0.14)
  126. cardBackground = Color(red: 0.16, green: 0.16, blue: 0.17)
  127. primaryText = Color(red: 0.88, green: 0.88, blue: 0.9)
  128. secondaryText = Color(red: 0.55, green: 0.55, blue: 0.58)
  129. tertiaryText = Color(red: 0.38, green: 0.38, blue: 0.4)
  130. playingHighlight = Color(red: 0.45, green: 0.55, blue: 0.75)
  131. groupHeaderText = Color(red: 0.65, green: 0.65, blue: 0.7)
  132. tabBarBackground = Color(red: 0.09, green: 0.09, blue: 0.1)
  133. separatorColor = Color(red: 0.22, green: 0.22, blue: 0.24)
  134. seekbarHeight = 4; rowHeight = 44; dataFontSize = 14; smallFontSize = 11; cornerRadius = 6
  135. useDarkMode = true; preferredColorScheme = .dark
  136. // ── Windows Media Player ────────────────────────
  137. // Glossy dark blue, silver chrome, WMP 11 aesthetic
  138. case .wmp:
  139. accent = Color(red: 0.2, green: 0.5, blue: 0.95)
  140. background = Color(red: 0.05, green: 0.08, blue: 0.15)
  141. secondaryBackground = Color(red: 0.08, green: 0.12, blue: 0.2)
  142. seekbarBackground = Color(red: 0.12, green: 0.16, blue: 0.25)
  143. seekbarForeground = Color(red: 0.3, green: 0.6, blue: 1.0)
  144. playerBarBackground = Color(red: 0.06, green: 0.1, blue: 0.18)
  145. cardBackground = Color(red: 0.1, green: 0.14, blue: 0.22)
  146. primaryText = Color(red: 0.9, green: 0.92, blue: 0.96)
  147. secondaryText = Color(red: 0.55, green: 0.62, blue: 0.75)
  148. tertiaryText = Color(red: 0.35, green: 0.4, blue: 0.52)
  149. playingHighlight = Color(red: 0.3, green: 0.65, blue: 1.0)
  150. groupHeaderText = Color(red: 0.6, green: 0.7, blue: 0.9)
  151. tabBarBackground = Color(red: 0.04, green: 0.06, blue: 0.12)
  152. separatorColor = Color(red: 0.15, green: 0.2, blue: 0.3)
  153. seekbarHeight = 6; rowHeight = 50; dataFontSize = 14; smallFontSize = 11; cornerRadius = 8
  154. useDarkMode = true; preferredColorScheme = .dark
  155. // ── Obsidian ────────────────────────────────────
  156. // Deep dark with purple/violet accent, elegant and modern
  157. case .obsidian:
  158. accent = Color(red: 0.6, green: 0.4, blue: 0.9)
  159. background = Color(red: 0.07, green: 0.06, blue: 0.1)
  160. secondaryBackground = Color(red: 0.1, green: 0.09, blue: 0.14)
  161. seekbarBackground = Color(red: 0.15, green: 0.13, blue: 0.2)
  162. seekbarForeground = Color(red: 0.6, green: 0.4, blue: 0.9)
  163. playerBarBackground = Color(red: 0.08, green: 0.07, blue: 0.12)
  164. cardBackground = Color(red: 0.12, green: 0.1, blue: 0.16)
  165. primaryText = Color(red: 0.9, green: 0.88, blue: 0.95)
  166. secondaryText = Color(red: 0.55, green: 0.5, blue: 0.65)
  167. tertiaryText = Color(red: 0.35, green: 0.32, blue: 0.45)
  168. playingHighlight = Color(red: 0.65, green: 0.45, blue: 0.95)
  169. groupHeaderText = Color(red: 0.7, green: 0.6, blue: 0.85)
  170. tabBarBackground = Color(red: 0.05, green: 0.04, blue: 0.08)
  171. separatorColor = Color(red: 0.2, green: 0.17, blue: 0.28)
  172. seekbarHeight = 5; rowHeight = 52; dataFontSize = 15; smallFontSize = 12; cornerRadius = 12
  173. useDarkMode = true; preferredColorScheme = .dark
  174. // ── Vinyl ───────────────────────────────────────
  175. // Warm browns, cream text, analog feel — like vintage hi-fi equipment
  176. case .vinyl:
  177. accent = Color(red: 0.85, green: 0.55, blue: 0.2) // warm amber/orange
  178. background = Color(red: 0.12, green: 0.1, blue: 0.08) // dark walnut
  179. secondaryBackground = Color(red: 0.16, green: 0.13, blue: 0.1)
  180. seekbarBackground = Color(red: 0.22, green: 0.18, blue: 0.14)
  181. seekbarForeground = Color(red: 0.85, green: 0.55, blue: 0.2)
  182. playerBarBackground = Color(red: 0.14, green: 0.11, blue: 0.09)
  183. cardBackground = Color(red: 0.18, green: 0.15, blue: 0.11)
  184. primaryText = Color(red: 0.92, green: 0.88, blue: 0.78) // warm cream
  185. secondaryText = Color(red: 0.65, green: 0.58, blue: 0.48)
  186. tertiaryText = Color(red: 0.45, green: 0.4, blue: 0.32)
  187. playingHighlight = Color(red: 0.9, green: 0.6, blue: 0.2)
  188. groupHeaderText = Color(red: 0.8, green: 0.65, blue: 0.4)
  189. tabBarBackground = Color(red: 0.1, green: 0.08, blue: 0.06)
  190. separatorColor = Color(red: 0.25, green: 0.2, blue: 0.15)
  191. seekbarHeight = 5; rowHeight = 54; dataFontSize = 15; smallFontSize = 12; cornerRadius = 8
  192. useDarkMode = true; preferredColorScheme = .dark
  193. // ── Tidal ───────────────────────────────────────
  194. // Dark warm background, teal/cyan accent, like screenshot
  195. case .tidal:
  196. accent = Color(red: 0.2, green: 0.85, blue: 0.75) // teal/cyan
  197. background = Color(red: 0.08, green: 0.07, blue: 0.06) // warm near-black
  198. secondaryBackground = Color(red: 0.12, green: 0.11, blue: 0.09)
  199. seekbarBackground = Color(red: 0.25, green: 0.24, blue: 0.22)
  200. seekbarForeground = Color.white
  201. playerBarBackground = Color(red: 0.1, green: 0.09, blue: 0.07)
  202. cardBackground = Color(red: 0.14, green: 0.13, blue: 0.11)
  203. primaryText = Color.white
  204. secondaryText = Color(red: 0.65, green: 0.63, blue: 0.58)
  205. tertiaryText = Color(red: 0.4, green: 0.38, blue: 0.35)
  206. playingHighlight = Color(red: 0.2, green: 0.85, blue: 0.75)
  207. groupHeaderText = Color(red: 0.75, green: 0.73, blue: 0.68)
  208. tabBarBackground = Color(red: 0.06, green: 0.05, blue: 0.04)
  209. separatorColor = Color(red: 0.2, green: 0.19, blue: 0.17)
  210. seekbarHeight = 4; rowHeight = 56; dataFontSize = 15; smallFontSize = 12; cornerRadius = 10
  211. useDarkMode = true; preferredColorScheme = .dark
  212. }
  213. }
  214. }
  215. // MARK: - Color hex helper
  216. extension Color {
  217. init?(hex: String) {
  218. var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
  219. hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
  220. guard hexSanitized.count == 6,
  221. let rgb = UInt64(hexSanitized, radix: 16) else { return nil }
  222. self.init(
  223. red: Double((rgb >> 16) & 0xFF) / 255.0,
  224. green: Double((rgb >> 8) & 0xFF) / 255.0,
  225. blue: Double(rgb & 0xFF) / 255.0
  226. )
  227. }
  228. }