feat: added color settings
0b06b19d
6 file(s) · +73 −5
| 26 | 26 | ||
| 27 | 27 | /// The home page URL that the browser navigates to on launch and when pressing Home |
|
| 28 | 28 | @AppStorage("homePage") var homePage: String = "gemini://geminiprotocol.net/" |
|
| 29 | + | ||
| 30 | + | /// Key for persisting accent color hex value |
|
| 31 | + | private static let accentColorKey = "accentColorHex" |
|
| 32 | + | ||
| 33 | + | init() { |
|
| 34 | + | if let hex = UserDefaults.standard.string(forKey: Self.accentColorKey), |
|
| 35 | + | let color = Color(hex: hex) { |
|
| 36 | + | setAllColors(color) |
|
| 37 | + | } |
|
| 38 | + | } |
|
| 39 | + | ||
| 40 | + | /// Sets all accent colors to the given color and persists the choice |
|
| 41 | + | func setAllColors(_ color: Color) { |
|
| 42 | + | accentColor = color |
|
| 43 | + | progressBarColor = color |
|
| 44 | + | linkColor = color |
|
| 45 | + | mediaAccentColor = color |
|
| 46 | + | toolbarButtonColor = color |
|
| 47 | + | ||
| 48 | + | if let hex = color.toHex() { |
|
| 49 | + | UserDefaults.standard.set(hex, forKey: Self.accentColorKey) |
|
| 50 | + | } |
|
| 51 | + | } |
|
| 52 | + | } |
|
| 53 | + | ||
| 54 | + | // MARK: - Color Hex Conversion |
|
| 55 | + | ||
| 56 | + | extension Color { |
|
| 57 | + | init?(hex: String) { |
|
| 58 | + | var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) |
|
| 59 | + | hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") |
|
| 60 | + | ||
| 61 | + | guard hexSanitized.count == 6 else { return nil } |
|
| 62 | + | ||
| 63 | + | var rgb: UInt64 = 0 |
|
| 64 | + | guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { return nil } |
|
| 65 | + | ||
| 66 | + | let red = Double((rgb & 0xFF0000) >> 16) / 255.0 |
|
| 67 | + | let green = Double((rgb & 0x00FF00) >> 8) / 255.0 |
|
| 68 | + | let blue = Double(rgb & 0x0000FF) / 255.0 |
|
| 69 | + | ||
| 70 | + | self.init(red: red, green: green, blue: blue) |
|
| 71 | + | } |
|
| 72 | + | ||
| 73 | + | func toHex() -> String? { |
|
| 74 | + | guard let components = UIColor(self).cgColor.components else { return nil } |
|
| 75 | + | ||
| 76 | + | let r = components.count > 0 ? components[0] : 0 |
|
| 77 | + | let g = components.count > 1 ? components[1] : 0 |
|
| 78 | + | let b = components.count > 2 ? components[2] : 0 |
|
| 79 | + | ||
| 80 | + | return String(format: "#%02X%02X%02X", |
|
| 81 | + | Int(r * 255), |
|
| 82 | + | Int(g * 255), |
|
| 83 | + | Int(b * 255)) |
|
| 84 | + | } |
|
| 29 | 85 | } |
|
| 30 | 86 | ||
| 31 | 87 | // MARK: - Environment Key |
| 15 | 15 | WindowGroup { |
|
| 16 | 16 | ContentView() |
|
| 17 | 17 | .environment(\.themeSettings, themeSettings) |
|
| 18 | + | .environmentObject(themeSettings) |
|
| 18 | 19 | } |
|
| 19 | 20 | } |
|
| 20 | 21 | } |
| 28 | 28 | } |
|
| 29 | 29 | ||
| 30 | 30 | struct ContentView: View { |
|
| 31 | - | @Environment(\.themeSettings) private var themeSettings |
|
| 31 | + | @EnvironmentObject private var themeSettings: ThemeSettings |
|
| 32 | 32 | @State private var urlText = "" |
|
| 33 | 33 | @State private var responseText = "" |
|
| 34 | 34 | @State private var isLoading = false |
| 12 | 12 | let media: MediaContent |
|
| 13 | 13 | let onDismiss: () -> Void |
|
| 14 | 14 | ||
| 15 | - | @Environment(\.themeSettings) private var themeSettings |
|
| 15 | + | @EnvironmentObject private var themeSettings: ThemeSettings |
|
| 16 | 16 | @State private var showingSaveOptions = false |
|
| 17 | 17 | @State private var saveMessage: String? |
|
| 18 | 18 | @State private var showingSaveAlert = false |
|
| 175 | 175 | let data: Data |
|
| 176 | 176 | let filename: String |
|
| 177 | 177 | ||
| 178 | - | @Environment(\.themeSettings) private var themeSettings |
|
| 178 | + | @EnvironmentObject private var themeSettings: ThemeSettings |
|
| 179 | 179 | @StateObject private var audioPlayer = AudioPlayerViewModel() |
|
| 180 | 180 | ||
| 181 | 181 | var body: some View { |
|
| 6 | 6 | import SwiftUI |
|
| 7 | 7 | ||
| 8 | 8 | struct SettingsView: View { |
|
| 9 | - | @Environment(\.themeSettings) private var themeSettings |
|
| 9 | + | @EnvironmentObject private var themeSettings: ThemeSettings |
|
| 10 | 10 | @Environment(\.dismiss) private var dismiss |
|
| 11 | 11 | ||
| 12 | 12 | @State private var homePageText: String = "" |
|
| 13 | + | @State private var selectedAccentColor: Color = .blue |
|
| 13 | 14 | ||
| 14 | 15 | var body: some View { |
|
| 15 | 16 | NavigationStack { |
|
| 24 | 25 | } footer: { |
|
| 25 | 26 | Text("The page that loads when you open the app or tap the Home button.") |
|
| 26 | 27 | } |
|
| 28 | + | ||
| 29 | + | Section { |
|
| 30 | + | ColorPicker("Accent Color", selection: $selectedAccentColor, supportsOpacity: false) |
|
| 31 | + | } header: { |
|
| 32 | + | Text("Appearance") |
|
| 33 | + | } footer: { |
|
| 34 | + | Text("Changes the color of links, buttons, and other interactive elements.") |
|
| 35 | + | } |
|
| 27 | 36 | } |
|
| 28 | 37 | .navigationTitle("Settings") |
|
| 29 | 38 | .navigationBarTitleDisplayMode(.inline) |
|
| 36 | 45 | ToolbarItem(placement: .confirmationAction) { |
|
| 37 | 46 | Button("Save") { |
|
| 38 | 47 | themeSettings.homePage = homePageText |
|
| 48 | + | themeSettings.setAllColors(selectedAccentColor) |
|
| 39 | 49 | dismiss() |
|
| 40 | 50 | } |
|
| 41 | 51 | } |
|
| 42 | 52 | } |
|
| 43 | 53 | .onAppear { |
|
| 44 | 54 | homePageText = themeSettings.homePage |
|
| 55 | + | selectedAccentColor = themeSettings.accentColor |
|
| 45 | 56 | } |
|
| 46 | 57 | } |
|
| 47 | 58 | } |
|
| 52 | 52 | let baseURL: String |
|
| 53 | 53 | let onLinkTap: (String) -> Void |
|
| 54 | 54 | ||
| 55 | - | @Environment(\.themeSettings) private var themeSettings |
|
| 55 | + | @EnvironmentObject private var themeSettings: ThemeSettings |
|
| 56 | 56 | ||
| 57 | 57 | init(content: String, baseURL: String = "", onLinkTap: @escaping (String) -> Void) { |
|
| 58 | 58 | self.content = content |