chore: setup color settings cafc4f9c
Steve · 2025-12-24 16:43 5 file(s) · +60 −11
Titan/Settings/ThemeSettings.swift (added) +39 −0
1 +
//
2 +
//  ThemeSettings.swift
3 +
//  Titan
4 +
//
5 +
6 +
import SwiftUI
7 +
import Combine
8 +
9 +
/// Observable object that manages theme customization settings.
10 +
/// This provides centralized accent color management that views can subscribe to.
11 +
class ThemeSettings: ObservableObject {
12 +
    /// The primary accent color used for interactive elements like links and buttons
13 +
    @Published var accentColor: Color = .blue
14 +
15 +
    /// The color used specifically for the loading progress bar
16 +
    @Published var progressBarColor: Color = .blue
17 +
18 +
    /// The color used for link text in Gemini content
19 +
    @Published var linkColor: Color = .blue
20 +
21 +
    /// The color used for media player controls (play button, slider, etc.)
22 +
    @Published var mediaAccentColor: Color = .blue
23 +
24 +
    /// The color used for toolbar buttons (navigation, menu, etc.)
25 +
    @Published var toolbarButtonColor: Color = .blue
26 +
}
27 +
28 +
// MARK: - Environment Key
29 +
30 +
private struct ThemeSettingsKey: EnvironmentKey {
31 +
    static let defaultValue = ThemeSettings()
32 +
}
33 +
34 +
extension EnvironmentValues {
35 +
    var themeSettings: ThemeSettings {
36 +
        get { self[ThemeSettingsKey.self] }
37 +
        set { self[ThemeSettingsKey.self] = newValue }
38 +
    }
39 +
}
Titan/TitanApp.swift +3 −0
9 9
10 10
@main
11 11
struct TitanApp: App {
12 +
    @StateObject private var themeSettings = ThemeSettings()
13 +
12 14
    var body: some Scene {
13 15
        WindowGroup {
14 16
            ContentView()
17 +
                .environment(\.themeSettings, themeSettings)
15 18
        }
16 19
    }
17 20
}
Titan/Views/ContentView.swift +8 −5
6 6
import SwiftUI
7 7
8 8
struct IndeterminateProgressBar: View {
9 +
    let color: Color
10 +
9 11
    @State private var animationOffset: CGFloat = -1.0
10 12
11 13
    var body: some View {
12 14
        GeometryReader { geometry in
13 15
            Rectangle()
14 -
                .fill(Color.orange)
16 +
                .fill(color)
15 17
                .frame(width: geometry.size.width * 0.3)
16 18
                .offset(x: animationOffset * geometry.size.width)
17 19
        }
28 30
struct ContentView: View {
29 31
    private let homeSite = "gemini://geminiprotocol.net/"
30 32
33 +
    @Environment(\.themeSettings) private var themeSettings
31 34
    @State private var urlText = ""
32 35
    @State private var responseText = ""
33 36
    @State private var isLoading = false
71 74
            .safeAreaInset(edge: .bottom) {
72 75
                VStack(spacing: 0) {
73 76
                    if isLoading {
74 -
                        IndeterminateProgressBar()
77 +
                        IndeterminateProgressBar(color: themeSettings.progressBarColor)
75 78
                    } else {
76 79
                        Color.clear
77 80
                            .frame(height: 3)
84 87
                                Button(action: goBack) {
85 88
                                    Image(systemName: "chevron.left")
86 89
                                        .font(.title2)
87 -
                                        .foregroundStyle(canGoBack && !isLoading ? .primary : .tertiary)
90 +
                                        .foregroundStyle(canGoBack && !isLoading ? themeSettings.toolbarButtonColor : themeSettings.toolbarButtonColor.opacity(0.3))
88 91
                                        .frame(width: 44, height: 44)
89 92
                                }
90 93
                                .disabled(!canGoBack || isLoading)
95 98
                                Button(action: goForward) {
96 99
                                    Image(systemName: "chevron.right")
97 100
                                        .font(.title2)
98 -
                                        .foregroundStyle(canGoForward && !isLoading ? .primary : .tertiary)
101 +
                                        .foregroundStyle(canGoForward && !isLoading ? themeSettings.toolbarButtonColor : themeSettings.toolbarButtonColor.opacity(0.3))
99 102
                                        .frame(width: 44, height: 44)
100 103
                                }
101 104
                                .disabled(!canGoForward || isLoading)
123 126
                            } label: {
124 127
                                Image(systemName: "ellipsis.circle")
125 128
                                    .font(.title2)
126 -
                                    .foregroundStyle(.primary)
129 +
                                    .foregroundStyle(themeSettings.toolbarButtonColor)
127 130
                                    .frame(width: 44, height: 44)
128 131
                            }
129 132
                            .glassEffect(.regular.interactive())
Titan/Views/MediaPreviewView.swift +7 −5
12 12
    let media: MediaContent
13 13
    let onDismiss: () -> Void
14 14
15 +
    @Environment(\.themeSettings) private var themeSettings
15 16
    @State private var showingSaveOptions = false
16 17
    @State private var saveMessage: String?
17 18
    @State private var showingSaveAlert = false
38 39
                            Image(systemName: "chevron.left")
39 40
                            Text("Back")
40 41
                        }
41 -
                        .foregroundColor(.orange)
42 +
                        .foregroundColor(themeSettings.accentColor)
42 43
                    }
43 44
                }
44 45
45 46
                ToolbarItem(placement: .navigationBarTrailing) {
46 47
                    Button(action: { showingSaveOptions = true }) {
47 48
                        Image(systemName: "square.and.arrow.down")
48 -
                            .foregroundColor(.orange)
49 +
                            .foregroundColor(themeSettings.accentColor)
49 50
                    }
50 51
                }
51 52
            }
174 175
    let data: Data
175 176
    let filename: String
176 177
178 +
    @Environment(\.themeSettings) private var themeSettings
177 179
    @StateObject private var audioPlayer = AudioPlayerViewModel()
178 180
179 181
    var body: some View {
180 182
        VStack(spacing: 32) {
181 183
            Image(systemName: "waveform.circle.fill")
182 184
                .font(.system(size: 120))
183 -
                .foregroundColor(.orange)
185 +
                .foregroundColor(themeSettings.mediaAccentColor)
184 186
185 187
            Text(filename)
186 188
                .font(.headline)
198 200
                    ),
199 201
                    in: 0...max(audioPlayer.duration, 0.01)
200 202
                )
201 -
                .accentColor(.orange)
203 +
                .accentColor(themeSettings.mediaAccentColor)
202 204
                .padding(.horizontal, 32)
203 205
204 206
                // Time labels
224 226
                    Button(action: { audioPlayer.togglePlayPause() }) {
225 227
                        Image(systemName: audioPlayer.isPlaying ? "pause.circle.fill" : "play.circle.fill")
226 228
                            .font(.system(size: 64))
227 -
                            .foregroundColor(.orange)
229 +
                            .foregroundColor(themeSettings.mediaAccentColor)
228 230
                    }
229 231
230 232
                    Button(action: { audioPlayer.skipForward() }) {
Titan/Views/TitanContentView.swift +3 −1
52 52
    let baseURL: String
53 53
    let onLinkTap: (String) -> Void
54 54
55 +
    @Environment(\.themeSettings) private var themeSettings
56 +
55 57
    init(content: String, baseURL: String = "", onLinkTap: @escaping (String) -> Void) {
56 58
        self.content = content
57 59
        self.baseURL = baseURL
84 86
                        .font(.system(size: 14, design: .monospaced))
85 87
                }
86 88
            }
87 -
            .foregroundColor(.orange)
89 +
            .foregroundColor(themeSettings.linkColor)
88 90
            .padding(.vertical, 6)
89 91
90 92
        case .heading1(let text):