Add separate light/dark mode accent colors

Adds an option in Appearance settings to use different accent colors
in light and dark mode, with a toggle to keep a single shared color
(the default, preserving existing behavior).

- New synced settings keys: accentColorDark, customAccentColorDark,
  useSeparateDarkAccentColor; dark values fall back to the light
  selection until explicitly set, so no migration is needed
- resolvedAccentColor returns a dynamic platform color (UIColor trait
  provider / NSColor appearance provider) when the toggle is on, so
  the root tint and all direct readers adapt to light/dark
  automatically without consumer changes
- Accent Color section shows two stacked preset+custom grids with
  Light/Dark headers when enabled, extracted into AccentColorGrid;
  CustomAccentColorButton now takes bindings so the macOS color panel
  edits whichever target was last opened
- New DarkAccentColorTests covering sync contract, default-off state,
  fallbacks, and toggle-off resolution

Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
This commit is contained in:
Arkadiusz Fal
2026-07-05 21:06:26 +02:00
parent b47888fe04
commit b632f11b2f
6 changed files with 250 additions and 28 deletions

View File

@@ -14,6 +14,9 @@ enum SettingsKey: String, CaseIterable {
case theme
case accentColor
case customAccentColor
case accentColorDark
case customAccentColorDark
case useSeparateDarkAccentColor
case showWatchedCheckmark
// Playback

View File

@@ -7,9 +7,9 @@
import Foundation
import SwiftUI
#if os(iOS)
#if canImport(UIKit)
import UIKit
#elseif os(macOS)
#elseif canImport(AppKit)
import AppKit
#endif
@@ -50,11 +50,78 @@ extension SettingsManager {
}
}
/// The effective accent color, resolving `.custom` to the user-picked value.
var resolvedAccentColor: Color {
/// Whether the user picked a separate accent color for dark mode.
var useSeparateDarkAccentColor: Bool {
get {
if let cached = _useSeparateDarkAccentColor { return cached }
return bool(for: .useSeparateDarkAccentColor, default: false)
}
set {
_useSeparateDarkAccentColor = newValue
set(newValue, for: .useSeparateDarkAccentColor)
}
}
/// Accent color choice for dark mode. Falls back to the light selection until explicitly set.
var accentColorDark: AccentColor {
get {
if let cached = _accentColorDark { return cached }
if let raw = string(for: .accentColorDark), let value = AccentColor(rawValue: raw) {
return value
}
return accentColor
}
set {
_accentColorDark = newValue
set(newValue.rawValue, for: .accentColorDark)
}
}
/// Custom accent color for dark mode. Falls back to the light custom color until explicitly set.
var customAccentColorDark: Color {
get {
if let hex = _customAccentColorDark ?? string(for: .customAccentColorDark),
let color = Color(hex: hex) {
return color
}
return customAccentColor
}
set {
let hex = newValue.toHexString()
_customAccentColorDark = hex
set(hex, for: .customAccentColorDark)
}
}
/// Light (or shared) accent color, resolving `.custom` to the user-picked value.
private var resolvedLightAccentColor: Color {
accentColor == .custom ? customAccentColor : accentColor.color
}
/// Dark accent color, resolving `.custom` to the user-picked value.
private var resolvedDarkAccentColor: Color {
accentColorDark == .custom ? customAccentColorDark : accentColorDark.color
}
/// The effective accent color. When a separate dark color is enabled this is a
/// dynamic platform color that resolves per trait environment, so all consumers
/// (root tint and direct readers) adapt to light/dark automatically.
var resolvedAccentColor: Color {
let light = resolvedLightAccentColor
guard useSeparateDarkAccentColor else { return light }
let dark = resolvedDarkAccentColor
#if os(macOS)
return Color(nsColor: NSColor(name: nil, dynamicProvider: { appearance in
let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua
return NSColor(isDark ? dark : light)
}))
#else
return Color(uiColor: UIColor { traits in
UIColor(traits.userInterfaceStyle == .dark ? dark : light)
})
#endif
}
// MARK: - App Icon Settings
var appIcon: AppIcon {

View File

@@ -29,6 +29,9 @@ final class SettingsManager {
var _theme: AppTheme?
var _accentColor: AccentColor?
var _customAccentColor: String?
var _accentColorDark: AccentColor?
var _customAccentColorDark: String?
var _useSeparateDarkAccentColor: Bool?
var _showWatchedCheckmark: Bool?
// Playback
@@ -419,6 +422,9 @@ final class SettingsManager {
_theme = nil
_accentColor = nil
_customAccentColor = nil
_accentColorDark = nil
_customAccentColorDark = nil
_useSeparateDarkAccentColor = nil
_showWatchedCheckmark = nil
_preferredQuality = nil
_cellularQuality = nil