Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cog/Sources/App/CogApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SwiftUI
@main
struct CogApp: App {
@State private var appState: AppState
@AppStorage(ThemeColor.storageKey) private var storedThemeColor = ThemeColor.blue.rawValue

init() {
#if DEBUG
Expand All @@ -16,6 +17,7 @@ struct CogApp: App {
WindowGroup {
RootView()
.environment(appState)
.tint(ThemeColor(storedValue: storedThemeColor).color)
.onOpenURL { url in
handleDeepLink(url)
}
Expand Down
47 changes: 47 additions & 0 deletions Cog/Sources/Models/ThemeColor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import SwiftUI

enum ThemeColor: String, CaseIterable, Identifiable {
static let storageKey = "themeColor"

case blue
case indigo
case purple
case pink
case red
case orange
case yellow
case green
case mint
case teal
case cyan
case brown
case gray

var id: String { rawValue }

init(storedValue: String) {
self = ThemeColor(rawValue: storedValue) ?? .blue
}

var name: String {
rawValue.capitalized
}

var color: Color {
switch self {
case .blue: .blue
case .indigo: .indigo
case .purple: .purple
case .pink: .pink
case .red: .red
case .orange: .orange
case .yellow: .yellow
case .green: .green
case .mint: .mint
case .teal: .teal
case .cyan: .cyan
case .brown: .brown
case .gray: .gray
}
}
}
87 changes: 86 additions & 1 deletion Cog/Sources/Views/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import SwiftUI

struct SettingsView: View {
@Environment(AppState.self) private var appState
@AppStorage(ThemeColor.storageKey) private var storedThemeColor = ThemeColor.blue.rawValue
@State private var selfInfo: SelfResponse?
@State private var showSignOutConfirmation = false

Expand Down Expand Up @@ -68,6 +69,26 @@ struct SettingsView: View {
}
}

Section {
VStack(alignment: .leading, spacing: 14) {
HStack {
Text("App Tint")

Spacer()

Text(selectedThemeColor.name)
.foregroundStyle(.secondary)
}

ThemeColorPicker(storedThemeColor: $storedThemeColor)
}
.padding(.vertical, 4)
} header: {
Text("Theme")
} footer: {
Text("Changes the tint color throughout Cog.")
}

Section("App") {
Link(destination: contactURL) {
SettingsNavigationRow(
Expand Down Expand Up @@ -136,6 +157,10 @@ struct SettingsView: View {
return String(key.prefix(4)) + "----" + String(key.suffix(4))
}

private var selectedThemeColor: ThemeColor {
ThemeColor(storedValue: storedThemeColor)
}

private var orgId: String {
appState.storedOrganizationId ?? "Unknown"
}
Expand Down Expand Up @@ -167,6 +192,66 @@ struct SettingsView: View {
}
}

private struct ThemeColorPicker: View {
@Binding var storedThemeColor: String

private let columns = Array(
repeating: GridItem(.flexible(), spacing: 10),
count: 6
)

var body: some View {
LazyVGrid(columns: columns, spacing: 12) {
ForEach(ThemeColor.allCases) { themeColor in
ThemeColorButton(
themeColor: themeColor,
isSelected: themeColor == selectedThemeColor
) {
withAnimation(.easeInOut(duration: 0.2)) {
storedThemeColor = themeColor.rawValue
}
}
}
}
}

private var selectedThemeColor: ThemeColor {
ThemeColor(storedValue: storedThemeColor)
}
}

private struct ThemeColorButton: View {
let themeColor: ThemeColor
let isSelected: Bool
let action: () -> Void

var body: some View {
Button(action: action) {
ZStack {
Circle()
.fill(themeColor.color.gradient)
.frame(width: 34, height: 34)

if isSelected {
Circle()
.stroke(.primary, lineWidth: 2)
.frame(width: 42, height: 42)

Image(systemName: "checkmark")
.font(.caption.bold())
.foregroundStyle(.white)
.shadow(radius: 1)
}
}
.frame(width: 44, height: 44)
}
.buttonStyle(.plain)
.accessibilityLabel(themeColor.name)
.accessibilityValue(isSelected ? "Selected" : "")
.accessibilityAddTraits(isSelected ? .isSelected : [])
}
}

private struct PrivacyPolicyView: View {
var body: some View {
ScrollView {
Expand Down Expand Up @@ -204,7 +289,7 @@ private struct SettingsAccountSummaryRow: View {
Image(systemName: "command.circle.fill")
.symbolRenderingMode(.palette)
.font(.system(size: 48, weight: .semibold))
.foregroundStyle(.white, Color.devinBlue)
.foregroundStyle(.white, Color.accentColor)
.accessibilityHidden(true)

VStack(alignment: .leading, spacing: 4) {
Expand Down
18 changes: 18 additions & 0 deletions Cog/Tests/ThemeColorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import XCTest
@testable import Cog

final class ThemeColorTests: XCTestCase {
func testStoredValueRestoresThemeColor() {
XCTAssertEqual(ThemeColor(storedValue: "purple"), .purple)
}

func testUnknownStoredValueFallsBackToBlue() {
XCTAssertEqual(ThemeColor(storedValue: "not-a-color"), .blue)
}

func testEveryThemeColorHasAUniqueStoredValue() {
let storedValues = ThemeColor.allCases.map(\.rawValue)

XCTAssertEqual(Set(storedValues).count, ThemeColor.allCases.count)
}
}
Loading