chore: added light and dark mode 19497d4b
Steve · 2025-12-25 21:44 3 file(s) · +161 −34
Titan/Settings/ThemeSettings.swift +118 −23
6 6
import SwiftUI
7 7
import Combine
8 8
9 +
/// Appearance mode options
10 +
enum AppearanceMode: String, CaseIterable, Identifiable {
11 +
    case automatic = "Automatic"
12 +
    case light = "Light"
13 +
    case dark = "Dark"
14 +
15 +
    var id: String { rawValue }
16 +
17 +
    var colorScheme: ColorScheme? {
18 +
        switch self {
19 +
        case .automatic: return nil
20 +
        case .light: return .light
21 +
        case .dark: return .dark
22 +
        }
23 +
    }
24 +
25 +
    var icon: String {
26 +
        switch self {
27 +
        case .automatic: return "circle.lefthalf.filled"
28 +
        case .light: return "sun.max.fill"
29 +
        case .dark: return "moon.fill"
30 +
        }
31 +
    }
32 +
}
33 +
9 34
/// Available font design options for the browser
10 35
enum FontDesignOption: String, CaseIterable, Identifiable {
11 36
    case system = "System"
28 53
/// Observable object that manages theme customization settings.
29 54
/// This provides centralized accent color management that views can subscribe to.
30 55
class ThemeSettings: ObservableObject {
56 +
    /// The appearance mode (light, dark, or automatic)
57 +
    @Published var appearanceMode: AppearanceMode = .automatic
58 +
31 59
    /// The primary accent color used for interactive elements like links and buttons
32 60
    @Published var accentColor: Color = .blue
33 61
43 71
    /// The color used for toolbar buttons (navigation, menu, etc.)
44 72
    @Published var toolbarButtonColor: Color = .blue
45 73
46 -
    /// The background color for the main content area
47 -
    @Published var backgroundColor: Color = Color(UIColor.systemBackground)
74 +
    /// The background color for the main content area (light mode)
75 +
    @Published var lightBackgroundColor: Color = .white
76 +
77 +
    /// The text color for content (light mode)
78 +
    @Published var lightTextColor: Color = .black
79 +
80 +
    /// The background color for the main content area (dark mode)
81 +
    @Published var darkBackgroundColor: Color = Color(red: 0.1, green: 0.1, blue: 0.1)
48 82
49 -
    /// The text color for content
50 -
    @Published var textColor: Color = Color(UIColor.label)
83 +
    /// The text color for content (dark mode)
84 +
    @Published var darkTextColor: Color = .white
51 85
52 86
    /// The font design for content
53 87
    @Published var fontDesign: FontDesignOption = .monospaced
55 89
    /// The home page URL that the browser navigates to on launch and when pressing Home
56 90
    @AppStorage("homePage") var homePage: String = "gemini://geminiprotocol.net/"
57 91
58 -
    /// Key for persisting accent color hex value
92 +
    /// Computed property for current background color based on system appearance
93 +
    var backgroundColor: Color {
94 +
        switch appearanceMode {
95 +
        case .light:
96 +
            return lightBackgroundColor
97 +
        case .dark:
98 +
            return darkBackgroundColor
99 +
        case .automatic:
100 +
            return Color(UIColor.systemBackground)
101 +
        }
102 +
    }
103 +
104 +
    /// Computed property for current text color based on system appearance
105 +
    var textColor: Color {
106 +
        switch appearanceMode {
107 +
        case .light:
108 +
            return lightTextColor
109 +
        case .dark:
110 +
            return darkTextColor
111 +
        case .automatic:
112 +
            return Color(UIColor.label)
113 +
        }
114 +
    }
115 +
116 +
    /// Key for persisting values
59 117
    private static let accentColorKey = "accentColorHex"
60 -
    private static let backgroundColorKey = "backgroundColorKey"
61 -
    private static let textColorKey = "textColorHex"
118 +
    private static let appearanceModeKey = "appearanceMode"
119 +
    private static let lightBackgroundColorKey = "lightBackgroundColorHex"
120 +
    private static let lightTextColorKey = "lightTextColorHex"
121 +
    private static let darkBackgroundColorKey = "darkBackgroundColorHex"
122 +
    private static let darkTextColorKey = "darkTextColorHex"
62 123
    private static let fontDesignKey = "fontDesign"
63 124
64 125
    init() {
126 +
        if let modeRaw = UserDefaults.standard.string(forKey: Self.appearanceModeKey),
127 +
           let mode = AppearanceMode(rawValue: modeRaw) {
128 +
            appearanceMode = mode
129 +
        }
65 130
        if let hex = UserDefaults.standard.string(forKey: Self.accentColorKey),
66 131
           let color = Color(hex: hex) {
67 -
            setAllAccentColors(color)
132 +
            setAllAccentColors(color, persist: false)
133 +
        }
134 +
        if let hex = UserDefaults.standard.string(forKey: Self.lightBackgroundColorKey),
135 +
           let color = Color(hex: hex) {
136 +
            lightBackgroundColor = color
137 +
        }
138 +
        if let hex = UserDefaults.standard.string(forKey: Self.lightTextColorKey),
139 +
           let color = Color(hex: hex) {
140 +
            lightTextColor = color
68 141
        }
69 -
        if let hex = UserDefaults.standard.string(forKey: Self.backgroundColorKey),
142 +
        if let hex = UserDefaults.standard.string(forKey: Self.darkBackgroundColorKey),
70 143
           let color = Color(hex: hex) {
71 -
            backgroundColor = color
144 +
            darkBackgroundColor = color
72 145
        }
73 -
        if let hex = UserDefaults.standard.string(forKey: Self.textColorKey),
146 +
        if let hex = UserDefaults.standard.string(forKey: Self.darkTextColorKey),
74 147
           let color = Color(hex: hex) {
75 -
            textColor = color
148 +
            darkTextColor = color
76 149
        }
77 150
        if let fontRaw = UserDefaults.standard.string(forKey: Self.fontDesignKey),
78 151
           let font = FontDesignOption(rawValue: fontRaw) {
80 153
        }
81 154
    }
82 155
83 -
    /// Sets all accent colors to the given color and persists the choice
84 -
    func setAllAccentColors(_ color: Color) {
156 +
    /// Sets all accent colors to the given color and optionally persists the choice
157 +
    func setAllAccentColors(_ color: Color, persist: Bool = true) {
85 158
        accentColor = color
86 159
        progressBarColor = color
87 160
        linkColor = color
88 161
        mediaAccentColor = color
89 162
        toolbarButtonColor = color
90 163
91 -
        if let hex = color.toHex() {
164 +
        if persist, let hex = color.toHex() {
92 165
            UserDefaults.standard.set(hex, forKey: Self.accentColorKey)
93 166
        }
94 167
    }
95 168
96 -
    /// Sets the background color and persists the choice
97 -
    func setBackgroundColor(_ color: Color) {
98 -
        backgroundColor = color
169 +
    /// Sets the appearance mode and persists the choice
170 +
    func setAppearanceMode(_ mode: AppearanceMode) {
171 +
        appearanceMode = mode
172 +
        UserDefaults.standard.set(mode.rawValue, forKey: Self.appearanceModeKey)
173 +
    }
174 +
175 +
    /// Sets the light mode background color and persists the choice
176 +
    func setLightBackgroundColor(_ color: Color) {
177 +
        lightBackgroundColor = color
99 178
        if let hex = color.toHex() {
100 -
            UserDefaults.standard.set(hex, forKey: Self.backgroundColorKey)
179 +
            UserDefaults.standard.set(hex, forKey: Self.lightBackgroundColorKey)
180 +
        }
181 +
    }
182 +
183 +
    /// Sets the light mode text color and persists the choice
184 +
    func setLightTextColor(_ color: Color) {
185 +
        lightTextColor = color
186 +
        if let hex = color.toHex() {
187 +
            UserDefaults.standard.set(hex, forKey: Self.lightTextColorKey)
188 +
        }
189 +
    }
190 +
191 +
    /// Sets the dark mode background color and persists the choice
192 +
    func setDarkBackgroundColor(_ color: Color) {
193 +
        darkBackgroundColor = color
194 +
        if let hex = color.toHex() {
195 +
            UserDefaults.standard.set(hex, forKey: Self.darkBackgroundColorKey)
101 196
        }
102 197
    }
103 198
104 -
    /// Sets the text color and persists the choice
105 -
    func setTextColor(_ color: Color) {
106 -
        textColor = color
199 +
    /// Sets the dark mode text color and persists the choice
200 +
    func setDarkTextColor(_ color: Color) {
201 +
        darkTextColor = color
107 202
        if let hex = color.toHex() {
108 -
            UserDefaults.standard.set(hex, forKey: Self.textColorKey)
203 +
            UserDefaults.standard.set(hex, forKey: Self.darkTextColorKey)
109 204
        }
110 205
    }
111 206
Titan/TitanApp.swift +1 −0
16 16
            ContentView()
17 17
                .environment(\.themeSettings, themeSettings)
18 18
                .environmentObject(themeSettings)
19 +
                .preferredColorScheme(themeSettings.appearanceMode.colorScheme)
19 20
        }
20 21
    }
21 22
}
Titan/Views/SettingsView.swift +42 −11
10 10
    @Environment(\.dismiss) private var dismiss
11 11
12 12
    @State private var homePageText: String = ""
13 +
    @State private var selectedAppearanceMode: AppearanceMode = .automatic
13 14
    @State private var selectedAccentColor: Color = .blue
14 -
    @State private var selectedBackgroundColor: Color = Color(UIColor.systemBackground)
15 -
    @State private var selectedTextColor: Color = Color(UIColor.label)
15 +
    @State private var selectedLightBackgroundColor: Color = .white
16 +
    @State private var selectedLightTextColor: Color = .black
17 +
    @State private var selectedDarkBackgroundColor: Color = .black
18 +
    @State private var selectedDarkTextColor: Color = .white
16 19
    @State private var selectedFontDesign: FontDesignOption = .monospaced
17 20
18 21
    var body: some View {
30 33
                }
31 34
32 35
                Section {
36 +
                    Picker("Appearance", selection: $selectedAppearanceMode) {
37 +
                        ForEach(AppearanceMode.allCases) { mode in
38 +
                            Text(mode.rawValue).tag(mode)
39 +
                        }
40 +
                    }
41 +
                } header: {
42 +
                    Text("Theme")
43 +
                } footer: {
44 +
                    Text("Choose between light, dark, or automatic appearance.")
45 +
                }
46 +
47 +
                Section {
33 48
                    ColorPicker("Accent Color", selection: $selectedAccentColor, supportsOpacity: false)
34 -
                    ColorPicker("Background Color", selection: $selectedBackgroundColor, supportsOpacity: false)
35 -
                    ColorPicker("Text Color", selection: $selectedTextColor, supportsOpacity: false)
36 49
                    Picker("Font", selection: $selectedFontDesign) {
37 50
                        ForEach(FontDesignOption.allCases) { option in
38 51
                            Text(option.rawValue).tag(option)
39 52
                        }
40 53
                    }
41 54
                } header: {
42 -
                    Text("Appearance")
43 -
                } footer: {
44 -
                    Text("Customize the look of your browser interface.")
55 +
                    Text("General")
56 +
                }
57 +
58 +
                Section {
59 +
                    ColorPicker("Background", selection: $selectedLightBackgroundColor, supportsOpacity: false)
60 +
                    ColorPicker("Text", selection: $selectedLightTextColor, supportsOpacity: false)
61 +
                } header: {
62 +
                    Text("Light Mode Colors")
63 +
                }
64 +
65 +
                Section {
66 +
                    ColorPicker("Background", selection: $selectedDarkBackgroundColor, supportsOpacity: false)
67 +
                    ColorPicker("Text", selection: $selectedDarkTextColor, supportsOpacity: false)
68 +
                } header: {
69 +
                    Text("Dark Mode Colors")
45 70
                }
46 71
            }
47 72
            .navigationTitle("Settings")
55 80
                ToolbarItem(placement: .confirmationAction) {
56 81
                    Button("Save") {
57 82
                        themeSettings.homePage = homePageText
83 +
                        themeSettings.setAppearanceMode(selectedAppearanceMode)
58 84
                        themeSettings.setAllAccentColors(selectedAccentColor)
59 -
                        themeSettings.setBackgroundColor(selectedBackgroundColor)
60 -
                        themeSettings.setTextColor(selectedTextColor)
85 +
                        themeSettings.setLightBackgroundColor(selectedLightBackgroundColor)
86 +
                        themeSettings.setLightTextColor(selectedLightTextColor)
87 +
                        themeSettings.setDarkBackgroundColor(selectedDarkBackgroundColor)
88 +
                        themeSettings.setDarkTextColor(selectedDarkTextColor)
61 89
                        themeSettings.setFontDesign(selectedFontDesign)
62 90
                        dismiss()
63 91
                    }
65 93
            }
66 94
            .onAppear {
67 95
                homePageText = themeSettings.homePage
96 +
                selectedAppearanceMode = themeSettings.appearanceMode
68 97
                selectedAccentColor = themeSettings.accentColor
69 -
                selectedBackgroundColor = themeSettings.backgroundColor
70 -
                selectedTextColor = themeSettings.textColor
98 +
                selectedLightBackgroundColor = themeSettings.lightBackgroundColor
99 +
                selectedLightTextColor = themeSettings.lightTextColor
100 +
                selectedDarkBackgroundColor = themeSettings.darkBackgroundColor
101 +
                selectedDarkTextColor = themeSettings.darkTextColor
71 102
                selectedFontDesign = themeSettings.fontDesign
72 103
            }
73 104
        }