chore: setup color settings
cafc4f9c
5 file(s) · +60 −11
| 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 | + | } |
| 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 | } |
| 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()) |
|
| 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() }) { |
|
| 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): |
|