SettingsView.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. import SwiftData
  2. import SwiftUI
  3. /// Full settings window using macOS native TabView.
  4. /// Contains: Mix Targets, Appearance (themes), Playlist (columns, artwork, grouping), Playback, General.
  5. struct SettingsView: View {
  6. var body: some View {
  7. TabView {
  8. MixTargetSettings()
  9. .tabItem {
  10. Label("Mix Targets", systemImage: "target")
  11. }
  12. ChadMusicSettings()
  13. .tabItem {
  14. Label("Chad Music", systemImage: "cloud.fill")
  15. }
  16. AppearanceSettings()
  17. .tabItem {
  18. Label("Appearance", systemImage: "paintbrush")
  19. }
  20. PlaylistSettings()
  21. .tabItem {
  22. Label("Playlist", systemImage: "list.bullet")
  23. }
  24. PlaybackSettings()
  25. .tabItem {
  26. Label("Playback", systemImage: "play.circle")
  27. }
  28. KeyboardShortcutSettings()
  29. .tabItem {
  30. Label("Shortcuts", systemImage: "keyboard")
  31. }
  32. GeneralSettings()
  33. .tabItem {
  34. Label("General", systemImage: "gearshape")
  35. }
  36. }
  37. .frame(width: 580, height: 500)
  38. }
  39. }
  40. // MARK: - Mix Colors (shared across views)
  41. /// The 3 mix target colors: Red, Blue, Gold.
  42. let mixTargetColors: [Color] = [
  43. Color(red: 0.95, green: 0.3, blue: 0.3), // Red
  44. Color(red: 0.3, green: 0.75, blue: 0.95), // Blue
  45. Color(red: 0.95, green: 0.75, blue: 0.2), // Yellow/Gold
  46. ]
  47. // MARK: - Mix Target Settings
  48. private struct MixTargetSettings: View {
  49. @Environment(PlaylistViewModel.self) private var playlistVM
  50. @EnvironmentObject private var theme: AppTheme
  51. @ObservedObject private var shortcutConfig = KeyboardShortcutConfig.shared
  52. @Query(sort: \Playlist.dateModified, order: .reverse) private var playlists: [Playlist]
  53. private let mixActions: [ShortcutAction] = [.addToMix1, .addToMix2, .addToMix3]
  54. var body: some View {
  55. VStack(alignment: .leading, spacing: 20) {
  56. Text("Mix Targets")
  57. .font(.title3.bold())
  58. Text("Assign playlists to the 3 mix slots. Use the configured shortcuts to quick-add the current track, or click the numbered buttons at the top of the window.")
  59. .font(.callout)
  60. .foregroundStyle(.secondary)
  61. VStack(spacing: 12) {
  62. ForEach(0..<3, id: \.self) { slot in
  63. HStack(spacing: 12) {
  64. // Colored number badge
  65. Text("\(slot + 1)")
  66. .font(.system(size: 14, weight: .bold, design: .rounded))
  67. .frame(width: 30, height: 30)
  68. .foregroundStyle(mixTargetColors[slot])
  69. .background(mixTargetColors[slot].opacity(0.15))
  70. .clipShape(RoundedRectangle(cornerRadius: 6))
  71. // Playlist picker — Picker with menu style for full-width clickability
  72. Picker(selection: Binding(
  73. get: { playlistVM.mixTargets[slot]?.id },
  74. set: { newID in
  75. if let newID, let playlist = playlists.first(where: { $0.id == newID }) {
  76. playlistVM.setMixTarget(slot, playlist: playlist)
  77. } else {
  78. playlistVM.setMixTarget(slot, playlist: nil)
  79. }
  80. }
  81. )) {
  82. Text("Not set").tag(UUID?.none)
  83. Divider()
  84. ForEach(playlists) { playlist in
  85. Text(playlist.name).tag(Optional(playlist.id))
  86. }
  87. } label: {
  88. EmptyView()
  89. }
  90. .labelsHidden()
  91. .frame(minWidth: 160)
  92. // Shortcut hint
  93. Text(shortcutConfig.binding(for: mixActions[slot]).displayString)
  94. .font(.system(size: 11, design: .monospaced))
  95. .foregroundStyle(.secondary)
  96. .frame(width: 50)
  97. }
  98. }
  99. }
  100. Divider()
  101. Text("Tip: You can also right-click a playlist in the sidebar to assign it to a mix slot.")
  102. .font(.callout)
  103. .foregroundStyle(.secondary)
  104. Spacer()
  105. }
  106. .padding(24)
  107. }
  108. }
  109. // MARK: - Appearance Settings (Theme/Skin picker)
  110. private struct AppearanceSettings: View {
  111. @EnvironmentObject private var theme: AppTheme
  112. @ObservedObject private var iconConfig = AppIconConfig.shared
  113. private let modernSkins: [AppTheme.Skin] = [.dark, .midnight, .forest, .ocean, .warm, .light]
  114. private let retroSkins: [AppTheme.Skin] = [.winampClassic, .winampModern, .foobarDark, .foobarLight, .win95, .win98, .xpLuna, .macOSClassic]
  115. var body: some View {
  116. ScrollView {
  117. VStack(alignment: .leading, spacing: 20) {
  118. // MARK: - App Icon Color
  119. Text("App Icon")
  120. .font(.title3.bold())
  121. Text("Choose an accent color for the Dock icon.")
  122. .font(.callout)
  123. .foregroundStyle(.secondary)
  124. ScrollView(.horizontal, showsIndicators: false) {
  125. HStack(spacing: 12) {
  126. ForEach(AppIconConfig.iconColors) { option in
  127. Button {
  128. iconConfig.selectedColorName = option.name
  129. } label: {
  130. VStack(spacing: 6) {
  131. RoundedRectangle(cornerRadius: 12)
  132. .fill(option.color)
  133. .frame(width: 50, height: 50)
  134. .overlay(
  135. RoundedRectangle(cornerRadius: 12)
  136. .stroke(
  137. iconConfig.selectedColorName == option.name
  138. ? Color.accentColor : Color.white.opacity(0.2),
  139. lineWidth: iconConfig.selectedColorName == option.name ? 2.5 : 1
  140. )
  141. )
  142. .overlay {
  143. if iconConfig.selectedColorName == option.name {
  144. Image(systemName: "checkmark")
  145. .font(.system(size: 16, weight: .bold))
  146. .foregroundStyle(.white)
  147. .shadow(radius: 2)
  148. }
  149. }
  150. Text(option.name)
  151. .font(.caption2)
  152. .foregroundStyle(.secondary)
  153. }
  154. }
  155. .buttonStyle(.plain)
  156. }
  157. }
  158. .padding(.vertical, 4)
  159. }
  160. Divider()
  161. // MARK: - Theme
  162. Text("Theme")
  163. .font(.title3.bold())
  164. Text("Choose a visual theme for MixBoard. Each theme enforces its own light/dark appearance.")
  165. .font(.callout)
  166. .foregroundStyle(.secondary)
  167. // Modern
  168. VStack(alignment: .leading, spacing: 8) {
  169. Text("Modern")
  170. .font(.headline)
  171. .foregroundStyle(.secondary)
  172. LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))], spacing: 8) {
  173. ForEach(modernSkins) { skin in
  174. SkinCard(skin: skin, isSelected: theme.currentSkin == skin) {
  175. withAnimation(.easeInOut(duration: 0.2)) {
  176. theme.currentSkin = skin
  177. }
  178. }
  179. }
  180. }
  181. }
  182. Divider()
  183. // Retro
  184. VStack(alignment: .leading, spacing: 8) {
  185. Text("Retro")
  186. .font(.headline)
  187. .foregroundStyle(.secondary)
  188. LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))], spacing: 8) {
  189. ForEach(retroSkins) { skin in
  190. SkinCard(skin: skin, isSelected: theme.currentSkin == skin) {
  191. withAnimation(.easeInOut(duration: 0.2)) {
  192. theme.currentSkin = skin
  193. }
  194. }
  195. }
  196. }
  197. }
  198. }
  199. .padding(24)
  200. }
  201. }
  202. }
  203. /// A compact card showing a skin preview swatch and name.
  204. private struct SkinCard: View {
  205. let skin: AppTheme.Skin
  206. let isSelected: Bool
  207. let action: () -> Void
  208. var body: some View {
  209. Button(action: action) {
  210. HStack(spacing: 10) {
  211. // Color swatch showing the skin's approximate palette
  212. RoundedRectangle(cornerRadius: 4)
  213. .fill(skin.previewColor)
  214. .frame(width: 28, height: 28)
  215. .overlay {
  216. if skin.colorScheme == .dark {
  217. Image(systemName: "moon.fill")
  218. .font(.system(size: 10))
  219. .foregroundStyle(.white.opacity(0.7))
  220. } else {
  221. Image(systemName: "sun.max.fill")
  222. .font(.system(size: 10))
  223. .foregroundStyle(.black.opacity(0.5))
  224. }
  225. }
  226. VStack(alignment: .leading, spacing: 1) {
  227. Text(skin.rawValue)
  228. .font(.system(size: 12, weight: .medium))
  229. .lineLimit(1)
  230. Text(skin.colorScheme == .dark ? "Dark" : "Light")
  231. .font(.system(size: 10))
  232. .foregroundStyle(.secondary)
  233. }
  234. Spacer(minLength: 0)
  235. if isSelected {
  236. Image(systemName: "checkmark.circle.fill")
  237. .foregroundStyle(.green)
  238. .font(.system(size: 14))
  239. }
  240. }
  241. .padding(8)
  242. .background(isSelected ? Color.accentColor.opacity(0.1) : Color.clear)
  243. .overlay(
  244. RoundedRectangle(cornerRadius: 8)
  245. .stroke(isSelected ? Color.accentColor : Color.gray.opacity(0.3), lineWidth: isSelected ? 2 : 1)
  246. )
  247. .clipShape(RoundedRectangle(cornerRadius: 8))
  248. .contentShape(Rectangle())
  249. }
  250. .buttonStyle(.plain)
  251. }
  252. }
  253. // MARK: - Playlist Settings (Columns, Artwork, Grouping)
  254. private struct PlaylistSettings: View {
  255. @ObservedObject private var viewConfig = PlaylistViewConfig.shared
  256. var body: some View {
  257. ScrollView {
  258. VStack(alignment: .leading, spacing: 20) {
  259. // Visible columns
  260. VStack(alignment: .leading, spacing: 8) {
  261. HStack {
  262. Text("Visible Columns")
  263. .font(.title3.bold())
  264. Spacer()
  265. Button("Reset to Defaults") {
  266. viewConfig.resetToDefaults()
  267. }
  268. .font(.caption)
  269. }
  270. Text("Select which columns appear in the playlist view.")
  271. .font(.callout)
  272. .foregroundStyle(.secondary)
  273. LazyVGrid(columns: [GridItem(.adaptive(minimum: 130))], spacing: 6) {
  274. ForEach(PlaylistViewConfig.Column.allCases) { column in
  275. Toggle(column.rawValue, isOn: Binding(
  276. get: { viewConfig.isColumnVisible(column) },
  277. set: { _ in viewConfig.toggleColumn(column) }
  278. ))
  279. .toggleStyle(.checkbox)
  280. .font(.system(size: 12))
  281. }
  282. }
  283. }
  284. Divider()
  285. // Artwork
  286. VStack(alignment: .leading, spacing: 8) {
  287. Text("Artwork")
  288. .font(.headline)
  289. Toggle("Show artwork thumbnails in playlist rows", isOn: $viewConfig.showArtwork)
  290. if viewConfig.showArtwork {
  291. Picker("Artwork size", selection: $viewConfig.artworkSize) {
  292. ForEach(PlaylistViewConfig.ArtworkSize.allCases) { size in
  293. Text(size.rawValue).tag(size)
  294. }
  295. }
  296. .pickerStyle(.segmented)
  297. .frame(width: 250)
  298. }
  299. }
  300. Divider()
  301. }
  302. .padding(24)
  303. }
  304. }
  305. }
  306. // MARK: - Playback Settings
  307. private struct PlaybackSettings: View {
  308. @ObservedObject private var viewConfig = PlaylistViewConfig.shared
  309. @AppStorage("playbackMode") private var playbackMode: String = "queue"
  310. var body: some View {
  311. VStack(alignment: .leading, spacing: 20) {
  312. Text("Playback Mode")
  313. .font(.title3.bold())
  314. Picker("Mode", selection: $playbackMode) {
  315. Text("Playlist Mode").tag("playlist")
  316. Text("Queue Mode").tag("queue")
  317. }
  318. .pickerStyle(.radioGroup)
  319. .labelsHidden()
  320. Group {
  321. if playbackMode == "playlist" {
  322. Text("Click a track to play it and continue through the playlist (foobar-style).")
  323. } else {
  324. Text("Manage a playback queue with Play Next and Add to Queue (Spotify-style).")
  325. }
  326. }
  327. .font(.callout)
  328. .foregroundStyle(.secondary)
  329. Divider()
  330. Text("Cursor Behavior")
  331. .font(.title3.bold())
  332. VStack(alignment: .leading, spacing: 12) {
  333. Toggle("Cursor follows playback", isOn: Binding(
  334. get: { viewConfig.cursorFollowsPlayback },
  335. set: { newValue in
  336. if newValue {
  337. viewConfig.cursorFollowsPlayback = true
  338. viewConfig.playbackFollowsCursor = false
  339. } else {
  340. viewConfig.cursorFollowsPlayback = false
  341. viewConfig.playbackFollowsCursor = true
  342. }
  343. }
  344. ))
  345. Text("Auto-select and scroll to the currently playing track.")
  346. .font(.callout)
  347. .foregroundStyle(.secondary)
  348. .padding(.leading, 20)
  349. Toggle("Playback follows cursor", isOn: Binding(
  350. get: { viewConfig.playbackFollowsCursor },
  351. set: { newValue in
  352. if newValue {
  353. viewConfig.playbackFollowsCursor = true
  354. viewConfig.cursorFollowsPlayback = false
  355. } else {
  356. viewConfig.playbackFollowsCursor = false
  357. viewConfig.cursorFollowsPlayback = true
  358. }
  359. }
  360. ))
  361. Text("When a track finishes, play the currently selected track next (foobar2000 behavior).")
  362. .font(.callout)
  363. .foregroundStyle(.secondary)
  364. .padding(.leading, 20)
  365. }
  366. Spacer()
  367. }
  368. .padding(24)
  369. }
  370. }
  371. // MARK: - General Settings
  372. private struct GeneralSettings: View {
  373. @AppStorage("autoAnalyzeOnImport") private var autoAnalyzeOnImport = true
  374. var body: some View {
  375. VStack(alignment: .leading, spacing: 20) {
  376. Text("General")
  377. .font(.title3.bold())
  378. VStack(alignment: .leading, spacing: 12) {
  379. Toggle("Auto-analyze BPM & Key on import", isOn: $autoAnalyzeOnImport)
  380. Text("Automatically detect BPM and musical key when adding tracks.")
  381. .font(.callout)
  382. .foregroundStyle(.secondary)
  383. .padding(.leading, 20)
  384. }
  385. Spacer()
  386. }
  387. .padding(24)
  388. }
  389. }
  390. // MARK: - Keyboard Shortcut Settings
  391. private struct KeyboardShortcutSettings: View {
  392. @ObservedObject private var config = KeyboardShortcutConfig.shared
  393. @State private var recordingAction: ShortcutAction?
  394. @State private var keyMonitor: Any?
  395. var body: some View {
  396. ScrollView {
  397. VStack(alignment: .leading, spacing: 20) {
  398. HStack {
  399. Text("Keyboard Shortcuts")
  400. .font(.title3.bold())
  401. Spacer()
  402. Button("Reset to Defaults") {
  403. config.resetToDefaults()
  404. }
  405. .font(.caption)
  406. }
  407. Text("Click a shortcut to re-assign it. Press Escape to cancel.")
  408. .font(.callout)
  409. .foregroundStyle(.secondary)
  410. ForEach(ShortcutGroup.allCases, id: \.rawValue) { group in
  411. VStack(alignment: .leading, spacing: 6) {
  412. Text(group.rawValue)
  413. .font(.headline)
  414. .foregroundStyle(.secondary)
  415. ForEach(group.actions) { action in
  416. ShortcutRow(
  417. action: action,
  418. binding: config.binding(for: action),
  419. isRecording: recordingAction == action,
  420. onStartRecording: { startRecording(for: action) }
  421. )
  422. }
  423. }
  424. if group != .general {
  425. Divider()
  426. }
  427. }
  428. }
  429. .padding(24)
  430. }
  431. .onDisappear {
  432. stopRecording()
  433. }
  434. }
  435. private func startRecording(for action: ShortcutAction) {
  436. // Stop any existing recording
  437. stopRecording()
  438. recordingAction = action
  439. keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
  440. // Escape cancels
  441. if event.keyCode == 53 {
  442. self.stopRecording()
  443. return nil
  444. }
  445. let binding = ShortcutBinding.from(event)
  446. self.config.shortcuts[action] = binding
  447. self.stopRecording()
  448. return nil // consume the event
  449. }
  450. }
  451. private func stopRecording() {
  452. recordingAction = nil
  453. if let monitor = keyMonitor {
  454. NSEvent.removeMonitor(monitor)
  455. keyMonitor = nil
  456. }
  457. }
  458. }
  459. /// A single row: action name + clickable shortcut recorder button.
  460. private struct ShortcutRow: View {
  461. let action: ShortcutAction
  462. let binding: ShortcutBinding
  463. let isRecording: Bool
  464. let onStartRecording: () -> Void
  465. var body: some View {
  466. HStack {
  467. Text(action.rawValue)
  468. .font(.system(size: 12))
  469. .frame(maxWidth: .infinity, alignment: .leading)
  470. Button {
  471. onStartRecording()
  472. } label: {
  473. Text(isRecording ? "Press shortcut…" : binding.displayString)
  474. .font(.system(size: 12, design: isRecording ? .default : .monospaced))
  475. .frame(minWidth: 90)
  476. .padding(.horizontal, 8)
  477. .padding(.vertical, 4)
  478. .background(isRecording ? Color.accentColor.opacity(0.15) : Color.primary.opacity(0.05))
  479. .clipShape(RoundedRectangle(cornerRadius: 6))
  480. .overlay(
  481. RoundedRectangle(cornerRadius: 6)
  482. .stroke(isRecording ? Color.accentColor : Color.gray.opacity(0.3), lineWidth: 1)
  483. )
  484. }
  485. .buttonStyle(.plain)
  486. .animation(.easeInOut(duration: 0.15), value: isRecording)
  487. }
  488. .padding(.vertical, 2)
  489. }
  490. }
  491. // MARK: - Chad Music Settings
  492. private struct ChadMusicSettings: View {
  493. @AppStorage("chadMusic.serverURL") private var serverURL: String = ""
  494. @AppStorage("chadMusic.apiKey") private var apiKey: String = ""
  495. @State private var connectionStatus: ConnectionStatus = .unknown
  496. @State private var isTesting = false
  497. @State private var statsText: String = ""
  498. private enum ConnectionStatus {
  499. case unknown, testing, success, failed(String)
  500. }
  501. var body: some View {
  502. VStack(alignment: .leading, spacing: 20) {
  503. Text("Chad Music")
  504. .font(.title3.bold())
  505. Text("Connect to your Chad Music server to browse and stream your cloud library.")
  506. .font(.callout)
  507. .foregroundStyle(.secondary)
  508. // Server URL
  509. VStack(alignment: .leading, spacing: 6) {
  510. Text("Server URL")
  511. .font(.headline)
  512. TextField("https://music.tailnet.ts.net", text: $serverURL)
  513. .textFieldStyle(.roundedBorder)
  514. .onChange(of: serverURL) { _, _ in
  515. connectionStatus = .unknown
  516. }
  517. }
  518. // API Key
  519. VStack(alignment: .leading, spacing: 6) {
  520. Text("API Key")
  521. .font(.headline)
  522. SecureField("Enter API key", text: $apiKey)
  523. .textFieldStyle(.roundedBorder)
  524. .onChange(of: apiKey) { _, _ in
  525. connectionStatus = .unknown
  526. }
  527. }
  528. Divider()
  529. // Connection test
  530. HStack(spacing: 12) {
  531. Button("Test Connection") {
  532. testConnection()
  533. }
  534. .disabled(serverURL.isEmpty || apiKey.isEmpty || isTesting)
  535. switch connectionStatus {
  536. case .unknown:
  537. EmptyView()
  538. case .testing:
  539. ProgressView()
  540. .controlSize(.small)
  541. Text("Connecting...")
  542. .font(.callout)
  543. .foregroundStyle(.secondary)
  544. case .success:
  545. Image(systemName: "checkmark.circle.fill")
  546. .foregroundStyle(.green)
  547. Text(statsText)
  548. .font(.callout)
  549. .foregroundStyle(.secondary)
  550. case .failed(let message):
  551. Image(systemName: "xmark.circle.fill")
  552. .foregroundStyle(.red)
  553. Text(message)
  554. .font(.callout)
  555. .foregroundStyle(.red)
  556. }
  557. }
  558. Spacer()
  559. }
  560. .padding(24)
  561. }
  562. private func testConnection() {
  563. connectionStatus = .testing
  564. isTesting = true
  565. Task {
  566. let client = ChadMusicAPIClient.shared
  567. let result = await client.testConnection()
  568. switch result {
  569. case .success(let stats):
  570. let parts = [
  571. stats.tracks.map { "\($0) tracks" },
  572. stats.albums.map { "\($0) albums" },
  573. stats.artists.map { "\($0) artists" },
  574. ].compactMap { $0 }
  575. statsText = "Connected — " + parts.joined(separator: ", ")
  576. connectionStatus = .success
  577. case .failure(let error):
  578. connectionStatus = .failed(error.localizedDescription)
  579. }
  580. isTesting = false
  581. }
  582. }
  583. }
  584. // MARK: - Skin Preview Color
  585. extension AppTheme.Skin {
  586. /// Approximate preview swatch color for the skin card.
  587. var previewColor: Color {
  588. switch self {
  589. case .dark: return Color(red: 0.15, green: 0.15, blue: 0.17)
  590. case .midnight: return Color(red: 0.1, green: 0.1, blue: 0.2)
  591. case .forest: return Color(red: 0.1, green: 0.18, blue: 0.1)
  592. case .ocean: return Color(red: 0.1, green: 0.15, blue: 0.2)
  593. case .warm: return Color(red: 0.2, green: 0.15, blue: 0.1)
  594. case .light: return Color(red: 0.95, green: 0.95, blue: 0.96)
  595. case .winampClassic: return Color(red: 0.12, green: 0.12, blue: 0.14)
  596. case .winampModern: return Color(red: 0.13, green: 0.14, blue: 0.18)
  597. case .foobarDark: return Color(red: 0.14, green: 0.14, blue: 0.14)
  598. case .foobarLight: return Color(red: 0.94, green: 0.94, blue: 0.94)
  599. case .win95: return Color(red: 0.75, green: 0.75, blue: 0.75)
  600. case .win98: return Color(red: 0.83, green: 0.82, blue: 0.78)
  601. case .xpLuna: return Color(red: 0.85, green: 0.89, blue: 0.95)
  602. case .macOSClassic: return Color(red: 0.86, green: 0.86, blue: 0.86)
  603. }
  604. }
  605. }