AppTheme.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. import SwiftUI
  2. /// Centralized theme system for MixBoard.
  3. /// Supports retro and modern skins inspired by classic players.
  4. final class AppTheme: ObservableObject {
  5. // MARK: - Available Skins
  6. enum Skin: String, CaseIterable, Identifiable, Codable {
  7. // Modern
  8. case dark = "Dark"
  9. case midnight = "Midnight"
  10. case forest = "Forest"
  11. case ocean = "Ocean"
  12. case warm = "Warm"
  13. case light = "Light"
  14. // Retro
  15. case winampClassic = "Winamp Classic"
  16. case winampModern = "Winamp Modern"
  17. case foobarDark = "foobar2000 Dark"
  18. case foobarLight = "foobar2000 Light"
  19. case win95 = "Windows 95"
  20. case win98 = "Windows 98"
  21. case xpLuna = "XP Luna"
  22. case macOSClassic = "Mac OS 9"
  23. var id: String { rawValue }
  24. /// Whether this skin requires dark or light appearance.
  25. var colorScheme: ColorScheme {
  26. switch self {
  27. case .dark, .midnight, .forest, .ocean, .warm,
  28. .winampClassic, .winampModern, .foobarDark:
  29. return .dark
  30. case .light, .foobarLight, .win95, .win98, .xpLuna, .macOSClassic:
  31. return .light
  32. }
  33. }
  34. }
  35. // MARK: - Published
  36. @Published var currentSkin: Skin {
  37. didSet {
  38. applySkin(currentSkin)
  39. UserDefaults.standard.set(currentSkin.rawValue, forKey: "appThemeSkin")
  40. }
  41. }
  42. /// The color scheme the current skin requires (dark or light).
  43. var preferredScheme: ColorScheme {
  44. currentSkin.colorScheme
  45. }
  46. // MARK: - Colors
  47. @Published var accent: Color = .green
  48. @Published var seekbarBackground: Color = Color.gray.opacity(0.3)
  49. @Published var seekbarForeground: Color = .green
  50. @Published var waveformBackground: Color = Color.gray.opacity(0.25)
  51. @Published var waveformForeground: Color = Color(red: 0.2, green: 0.7, blue: 0.3)
  52. @Published var waveformSeparator: Color = Color.black.opacity(0.6)
  53. @Published var playerBarBackground: Color = Color(nsColor: .controlBackgroundColor)
  54. @Published var toolbarBackground: Color = Color(nsColor: .controlBackgroundColor)
  55. @Published var columnHeaderBackground: Color = Color(nsColor: .controlBackgroundColor)
  56. @Published var primaryText: Color = .primary
  57. @Published var secondaryText: Color = .secondary
  58. @Published var tertiaryText: Color = Color.gray.opacity(0.5)
  59. @Published var playingHighlight: Color = .green
  60. @Published var groupHeaderText: Color = .primary
  61. // MARK: - Sizes
  62. @Published var seekbarHeight: CGFloat = 8
  63. @Published var playerBarHeight: CGFloat = 32
  64. @Published var rowHeight: CGFloat = 22
  65. @Published var dataFontSize: CGFloat = 11
  66. @Published var smallFontSize: CGFloat = 9
  67. // MARK: - Init
  68. init() {
  69. let saved = UserDefaults.standard.string(forKey: "appThemeSkin")
  70. // Migrate legacy "foobar2000" → "foobar2000 Dark"
  71. let skinName = (saved == "foobar2000") ? "foobar2000 Dark" : saved
  72. let skin = skinName.flatMap { Skin(rawValue: $0) } ?? .dark
  73. self.currentSkin = skin
  74. applySkin(skin)
  75. }
  76. // MARK: - Apply Skin
  77. private func applySkin(_ skin: Skin) {
  78. // Reset to defaults first
  79. // Base sizes — readable defaults
  80. seekbarHeight = 8
  81. playerBarHeight = 36
  82. rowHeight = 24
  83. dataFontSize = 13
  84. smallFontSize = 11
  85. playerBarBackground = Color(nsColor: .controlBackgroundColor)
  86. toolbarBackground = Color(nsColor: .controlBackgroundColor)
  87. columnHeaderBackground = Color(nsColor: .controlBackgroundColor)
  88. switch skin {
  89. // ── Modern Skins ──────────────────────────────────
  90. case .dark:
  91. accent = Color(red: 0.3, green: 0.85, blue: 0.4)
  92. seekbarBackground = Color(red: 0.15, green: 0.25, blue: 0.35)
  93. seekbarForeground = Color(red: 0.3, green: 0.85, blue: 0.4)
  94. waveformBackground = Color(red: 0.12, green: 0.15, blue: 0.22)
  95. waveformForeground = Color(red: 0.3, green: 0.7, blue: 1.0)
  96. waveformSeparator = Color.black.opacity(0.7)
  97. playingHighlight = Color(red: 0.3, green: 0.85, blue: 0.4)
  98. primaryText = Color.white
  99. secondaryText = Color.white.opacity(0.75)
  100. tertiaryText = Color.white.opacity(0.5)
  101. groupHeaderText = Color.white
  102. case .midnight:
  103. accent = Color(red: 0.5, green: 0.6, blue: 1.0)
  104. seekbarBackground = Color(red: 0.2, green: 0.1, blue: 0.15)
  105. seekbarForeground = Color(red: 0.5, green: 0.6, blue: 1.0)
  106. waveformBackground = Color(red: 0.15, green: 0.08, blue: 0.12)
  107. waveformForeground = Color(red: 0.9, green: 0.3, blue: 0.6)
  108. waveformSeparator = Color.black.opacity(0.7)
  109. playingHighlight = Color(red: 0.5, green: 0.6, blue: 1.0)
  110. primaryText = Color.white
  111. secondaryText = Color.white.opacity(0.75)
  112. tertiaryText = Color.white.opacity(0.45)
  113. groupHeaderText = Color.white
  114. case .forest:
  115. accent = Color(red: 0.35, green: 0.85, blue: 0.35)
  116. seekbarBackground = Color(red: 0.25, green: 0.2, blue: 0.1)
  117. seekbarForeground = Color(red: 0.35, green: 0.85, blue: 0.35)
  118. waveformBackground = Color(red: 0.18, green: 0.14, blue: 0.06)
  119. waveformForeground = Color(red: 0.95, green: 0.75, blue: 0.2)
  120. waveformSeparator = Color.black.opacity(0.7)
  121. playingHighlight = Color(red: 0.35, green: 0.85, blue: 0.35)
  122. primaryText = Color.white
  123. secondaryText = Color.white.opacity(0.7)
  124. tertiaryText = Color.white.opacity(0.45)
  125. groupHeaderText = Color.white
  126. case .ocean:
  127. accent = Color(red: 0.3, green: 0.8, blue: 0.95)
  128. seekbarBackground = Color(red: 0.2, green: 0.12, blue: 0.1)
  129. seekbarForeground = Color(red: 0.3, green: 0.8, blue: 0.95)
  130. waveformBackground = Color(red: 0.15, green: 0.08, blue: 0.06)
  131. waveformForeground = Color(red: 1.0, green: 0.45, blue: 0.3)
  132. waveformSeparator = Color.black.opacity(0.7)
  133. playingHighlight = Color(red: 0.3, green: 0.8, blue: 0.95)
  134. primaryText = Color.white
  135. secondaryText = Color.white.opacity(0.75)
  136. tertiaryText = Color.white.opacity(0.45)
  137. groupHeaderText = Color.white
  138. case .warm:
  139. accent = Color(red: 1.0, green: 0.65, blue: 0.25)
  140. seekbarBackground = Color(red: 0.1, green: 0.18, blue: 0.2)
  141. seekbarForeground = Color(red: 1.0, green: 0.65, blue: 0.25)
  142. waveformBackground = Color(red: 0.06, green: 0.12, blue: 0.15)
  143. waveformForeground = Color(red: 0.2, green: 0.8, blue: 0.75)
  144. waveformSeparator = Color.black.opacity(0.7)
  145. playingHighlight = Color(red: 1.0, green: 0.65, blue: 0.25)
  146. primaryText = Color.white
  147. secondaryText = Color.white.opacity(0.75)
  148. tertiaryText = Color.white.opacity(0.45)
  149. groupHeaderText = Color.white
  150. case .light:
  151. accent = Color(red: 0.15, green: 0.45, blue: 0.85)
  152. seekbarBackground = Color(red: 0.92, green: 0.88, blue: 0.85)
  153. seekbarForeground = Color(red: 0.15, green: 0.45, blue: 0.85)
  154. waveformBackground = Color(red: 0.94, green: 0.9, blue: 0.88)
  155. waveformForeground = Color(red: 0.85, green: 0.3, blue: 0.15)
  156. waveformSeparator = Color.black.opacity(0.15)
  157. playingHighlight = Color(red: 0.15, green: 0.45, blue: 0.85)
  158. playerBarBackground = Color(red: 0.95, green: 0.95, blue: 0.96)
  159. toolbarBackground = Color(red: 0.95, green: 0.95, blue: 0.96)
  160. columnHeaderBackground = Color(red: 0.92, green: 0.92, blue: 0.93)
  161. primaryText = Color.black
  162. secondaryText = Color(red: 0.25, green: 0.25, blue: 0.3)
  163. tertiaryText = Color(red: 0.5, green: 0.5, blue: 0.55)
  164. groupHeaderText = Color.black
  165. // ── Retro Skins ──────────────────────────────────
  166. case .winampClassic:
  167. // Classic Winamp: dark background, neon green text, dark chrome
  168. accent = Color(red: 0.0, green: 1.0, blue: 0.0) // #00FF00
  169. seekbarBackground = Color(red: 0.15, green: 0.15, blue: 0.15)
  170. seekbarForeground = Color(red: 0.0, green: 1.0, blue: 0.0)
  171. waveformBackground = Color(red: 0.05, green: 0.05, blue: 0.05)
  172. waveformForeground = Color(red: 1.0, green: 0.9, blue: 0.0)
  173. waveformSeparator = Color(red: 0.0, green: 0.3, blue: 0.0)
  174. playingHighlight = Color(red: 0.0, green: 1.0, blue: 0.0)
  175. playerBarBackground = Color(red: 0.12, green: 0.12, blue: 0.14)
  176. toolbarBackground = Color(red: 0.12, green: 0.12, blue: 0.14)
  177. columnHeaderBackground = Color(red: 0.1, green: 0.1, blue: 0.12)
  178. primaryText = Color(red: 0.0, green: 0.9, blue: 0.0) // green text
  179. secondaryText = Color(red: 0.0, green: 0.7, blue: 0.0)
  180. tertiaryText = Color(red: 0.0, green: 0.4, blue: 0.0)
  181. groupHeaderText = Color(red: 0.0, green: 1.0, blue: 0.0)
  182. seekbarHeight = 6
  183. dataFontSize = 11
  184. case .winampModern:
  185. // Winamp Modern/Bento: dark blue-gray, orange accent
  186. accent = Color(red: 1.0, green: 0.55, blue: 0.0) // orange
  187. seekbarBackground = Color(red: 0.15, green: 0.17, blue: 0.22)
  188. seekbarForeground = Color(red: 1.0, green: 0.55, blue: 0.0)
  189. waveformBackground = Color(red: 0.08, green: 0.09, blue: 0.12)
  190. waveformForeground = Color(red: 0.3, green: 0.85, blue: 0.9)
  191. waveformSeparator = Color(red: 0.25, green: 0.15, blue: 0.0)
  192. playingHighlight = Color(red: 1.0, green: 0.55, blue: 0.0)
  193. playerBarBackground = Color(red: 0.13, green: 0.14, blue: 0.18)
  194. toolbarBackground = Color(red: 0.13, green: 0.14, blue: 0.18)
  195. columnHeaderBackground = Color(red: 0.11, green: 0.12, blue: 0.16)
  196. primaryText = Color(red: 0.85, green: 0.85, blue: 0.9)
  197. secondaryText = Color(red: 0.6, green: 0.6, blue: 0.65)
  198. tertiaryText = Color(red: 0.4, green: 0.4, blue: 0.45)
  199. groupHeaderText = Color(red: 1.0, green: 0.55, blue: 0.0)
  200. case .foobarDark:
  201. // foobar2000 Dark: dark gray background, light text, minimal chrome
  202. accent = Color(red: 0.35, green: 0.55, blue: 0.85) // muted blue
  203. seekbarBackground = Color(red: 0.25, green: 0.25, blue: 0.25)
  204. seekbarForeground = Color(red: 0.35, green: 0.55, blue: 0.85)
  205. waveformBackground = Color(red: 0.1, green: 0.1, blue: 0.12)
  206. waveformForeground = Color(red: 0.9, green: 0.65, blue: 0.2)
  207. waveformSeparator = Color(red: 0.18, green: 0.18, blue: 0.25)
  208. playingHighlight = Color(red: 0.35, green: 0.55, blue: 0.85)
  209. playerBarBackground = Color(red: 0.14, green: 0.14, blue: 0.14)
  210. toolbarBackground = Color(red: 0.14, green: 0.14, blue: 0.14)
  211. columnHeaderBackground = Color(red: 0.16, green: 0.16, blue: 0.16)
  212. primaryText = Color(red: 0.85, green: 0.85, blue: 0.85)
  213. secondaryText = Color(red: 0.6, green: 0.6, blue: 0.6)
  214. tertiaryText = Color(red: 0.4, green: 0.4, blue: 0.4)
  215. groupHeaderText = Color(red: 0.85, green: 0.85, blue: 0.85)
  216. rowHeight = 18
  217. dataFontSize = 11
  218. case .foobarLight:
  219. // foobar2000 Light: classic foobar — white/light gray, minimal, system colors
  220. accent = Color(red: 0.0, green: 0.0, blue: 0.5) // navy selection
  221. seekbarBackground = Color(red: 0.85, green: 0.85, blue: 0.85)
  222. seekbarForeground = Color(red: 0.0, green: 0.0, blue: 0.6)
  223. waveformBackground = Color(red: 0.93, green: 0.93, blue: 0.95)
  224. waveformForeground = Color(red: 0.7, green: 0.15, blue: 0.1)
  225. waveformSeparator = Color(red: 0.75, green: 0.75, blue: 0.75)
  226. playingHighlight = Color(red: 0.0, green: 0.0, blue: 0.5)
  227. playerBarBackground = Color(red: 0.94, green: 0.94, blue: 0.94)
  228. toolbarBackground = Color(red: 0.94, green: 0.94, blue: 0.94)
  229. columnHeaderBackground = Color(red: 0.9, green: 0.9, blue: 0.9)
  230. primaryText = Color.black
  231. secondaryText = Color(red: 0.3, green: 0.3, blue: 0.3)
  232. tertiaryText = Color(red: 0.55, green: 0.55, blue: 0.55)
  233. groupHeaderText = Color.black
  234. rowHeight = 18
  235. dataFontSize = 11
  236. case .win95:
  237. // Windows 95: classic silver/gray, teal highlights, 3D beveled look
  238. accent = Color(red: 0.0, green: 0.0, blue: 0.5) // navy blue
  239. seekbarBackground = Color(red: 0.75, green: 0.75, blue: 0.75)
  240. seekbarForeground = Color(red: 0.0, green: 0.0, blue: 0.5)
  241. waveformBackground = Color(red: 0.65, green: 0.65, blue: 0.65)
  242. waveformForeground = Color(red: 0.0, green: 0.5, blue: 0.5)
  243. waveformSeparator = Color(red: 0.5, green: 0.5, blue: 0.5)
  244. playingHighlight = Color(red: 0.0, green: 0.0, blue: 0.5)
  245. playerBarBackground = Color(red: 0.75, green: 0.75, blue: 0.75) // classic gray
  246. toolbarBackground = Color(red: 0.75, green: 0.75, blue: 0.75)
  247. columnHeaderBackground = Color(red: 0.8, green: 0.8, blue: 0.8)
  248. primaryText = Color.black
  249. secondaryText = Color(red: 0.25, green: 0.25, blue: 0.25)
  250. tertiaryText = Color(red: 0.5, green: 0.5, blue: 0.5)
  251. groupHeaderText = Color.black
  252. dataFontSize = 11
  253. seekbarHeight = 10
  254. case .win98:
  255. // Windows 98/2000: slightly softer than 95, classic blue title bars
  256. accent = Color(red: 0.0, green: 0.0, blue: 0.65)
  257. seekbarBackground = Color(red: 0.82, green: 0.82, blue: 0.82)
  258. seekbarForeground = Color(red: 0.0, green: 0.27, blue: 0.65)
  259. waveformBackground = Color(red: 0.72, green: 0.72, blue: 0.72)
  260. waveformForeground = Color(red: 0.0, green: 0.5, blue: 0.25)
  261. waveformSeparator = Color(red: 0.55, green: 0.55, blue: 0.55)
  262. playingHighlight = Color(red: 0.0, green: 0.0, blue: 0.65)
  263. playerBarBackground = Color(red: 0.83, green: 0.82, blue: 0.78) // warm gray
  264. toolbarBackground = Color(red: 0.83, green: 0.82, blue: 0.78)
  265. columnHeaderBackground = Color(red: 0.87, green: 0.86, blue: 0.82)
  266. primaryText = Color.black
  267. secondaryText = Color(red: 0.2, green: 0.2, blue: 0.2)
  268. tertiaryText = Color(red: 0.45, green: 0.45, blue: 0.45)
  269. groupHeaderText = Color(red: 0.0, green: 0.0, blue: 0.4)
  270. seekbarHeight = 10
  271. case .xpLuna:
  272. // Windows XP Luna: blue toolbar, olive/silver accents, rounded
  273. accent = Color(red: 0.22, green: 0.44, blue: 0.87) // XP blue
  274. seekbarBackground = Color(red: 0.88, green: 0.9, blue: 0.94)
  275. seekbarForeground = Color(red: 0.22, green: 0.44, blue: 0.87)
  276. waveformBackground = Color(red: 0.82, green: 0.85, blue: 0.92)
  277. waveformForeground = Color(red: 0.1, green: 0.6, blue: 0.3)
  278. waveformSeparator = Color(red: 0.7, green: 0.73, blue: 0.82)
  279. playingHighlight = Color(red: 0.22, green: 0.44, blue: 0.87)
  280. playerBarBackground = Color(red: 0.92, green: 0.93, blue: 0.96) // XP light blue
  281. toolbarBackground = Color(red: 0.85, green: 0.89, blue: 0.95)
  282. columnHeaderBackground = Color(red: 0.88, green: 0.91, blue: 0.96)
  283. primaryText = Color.black
  284. secondaryText = Color(red: 0.2, green: 0.2, blue: 0.3)
  285. tertiaryText = Color(red: 0.45, green: 0.45, blue: 0.55)
  286. groupHeaderText = Color(red: 0.1, green: 0.2, blue: 0.6)
  287. seekbarHeight = 10
  288. case .macOSClassic:
  289. // Classic Mac OS 9: platinum gray, black text, pinstripes
  290. accent = Color(red: 0.0, green: 0.0, blue: 0.6)
  291. seekbarBackground = Color(red: 0.8, green: 0.8, blue: 0.8)
  292. seekbarForeground = Color(red: 0.3, green: 0.3, blue: 0.85)
  293. waveformBackground = Color(red: 0.75, green: 0.75, blue: 0.75)
  294. waveformForeground = Color(red: 0.7, green: 0.3, blue: 0.1)
  295. waveformSeparator = Color(red: 0.6, green: 0.6, blue: 0.6)
  296. playingHighlight = Color(red: 0.3, green: 0.3, blue: 0.85)
  297. playerBarBackground = Color(red: 0.86, green: 0.86, blue: 0.86) // platinum
  298. toolbarBackground = Color(red: 0.86, green: 0.86, blue: 0.86)
  299. columnHeaderBackground = Color(red: 0.9, green: 0.9, blue: 0.9)
  300. primaryText = Color.black
  301. secondaryText = Color(red: 0.2, green: 0.2, blue: 0.2)
  302. tertiaryText = Color(red: 0.5, green: 0.5, blue: 0.5)
  303. groupHeaderText = Color.black
  304. dataFontSize = 12
  305. seekbarHeight = 8
  306. }
  307. }
  308. }