// // AppThemePickerView.swift // Feels (iOS) // // Created by Claude Code on 12/26/24. // import SwiftUI struct AppThemePickerView: View { @Environment(\.dismiss) private var dismiss @Environment(\.colorScheme) private var colorScheme @AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: Int = 0 @AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var moodImages: Int = 0 @AppStorage(UserDefaultsStore.Keys.dayViewStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var dayViewStyle: Int = 0 @AppStorage(UserDefaultsStore.Keys.votingLayoutStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var votingLayoutStyle: Int = 0 @State private var selectedTheme: AppTheme? var body: some View { NavigationStack { ScrollView { VStack(spacing: 32) { // Header headerSection // Theme Grid LazyVGrid(columns: [ GridItem(.flexible(), spacing: 16), GridItem(.flexible(), spacing: 16) ], spacing: 20) { ForEach(AppTheme.allCases) { theme in AppThemeCard( theme: theme, isSelected: isThemeActive(theme), onTap: { selectTheme(theme) } ) } } .padding(.horizontal, 20) // Footer note footerNote .padding(.bottom, 40) } } .background(colorScheme == .dark ? Color.black : Color(.systemGroupedBackground)) .navigationTitle("Themes") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("Done") { dismiss() } .accessibilityIdentifier(AccessibilityID.Customize.appThemePickerDoneButton) } } .sheet(item: $selectedTheme) { theme in AppThemePreviewSheet(theme: theme) { applyTheme(theme) } } } } // MARK: - Subviews private var headerSection: some View { VStack(spacing: 12) { Text("Choose Your Vibe") .font(.system(.title, design: .rounded, weight: .bold)) .foregroundColor(.primary) Text("Each theme combines colors, icons, layouts, and styles into a cohesive experience.") .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) .padding(.horizontal, 32) } .padding(.top, 20) } private var footerNote: some View { VStack(spacing: 8) { Text("Themes set all four options at once") .font(.caption) .foregroundColor(.secondary) Text("You can still customize individual settings after applying a theme") .font(.caption2) .foregroundColor(.secondary.opacity(0.7)) } .multilineTextAlignment(.center) .padding(.horizontal, 32) } // MARK: - Logic private func isThemeActive(_ theme: AppTheme) -> Bool { return moodTint == theme.colorTint.rawValue && moodImages == theme.iconPack.rawValue && dayViewStyle == theme.entryStyle.rawValue && votingLayoutStyle == theme.votingLayout.rawValue } private func selectTheme(_ theme: AppTheme) { selectedTheme = theme } private func applyTheme(_ theme: AppTheme) { withAnimation(.easeInOut(duration: 0.3)) { theme.apply() selectedTheme = nil } } } // MARK: - Theme Card struct AppThemeCard: View { let theme: AppTheme let isSelected: Bool let onTap: () -> Void @Environment(\.colorScheme) private var colorScheme var body: some View { Button(action: onTap) { VStack(spacing: 0) { // Preview area ZStack { // Background gradient LinearGradient( colors: theme.previewColors, startPoint: .topLeading, endPoint: .bottomTrailing ) // Theme emoji Text(theme.emoji) .font(.system(size: 44)) .shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2) // Selected checkmark if isSelected { VStack { HStack { Spacer() Image(systemName: "checkmark.circle.fill") .font(.title2) .foregroundStyle(.white) .background( Circle() .fill(.green) .frame(width: 28, height: 28) ) .padding(8) } Spacer() } } } .frame(height: 100) .clipShape( UnevenRoundedRectangle( topLeadingRadius: 16, bottomLeadingRadius: 0, bottomTrailingRadius: 0, topTrailingRadius: 16 ) ) // Info area VStack(alignment: .leading, spacing: 4) { Text(theme.name) .font(.headline) .foregroundColor(.primary) .lineLimit(1) Text(theme.tagline) .font(.caption) .foregroundColor(.secondary) .lineLimit(1) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 12) .padding(.vertical, 10) .background(colorScheme == .dark ? Color(.systemGray6) : .white) .clipShape( UnevenRoundedRectangle( topLeadingRadius: 0, bottomLeadingRadius: 16, bottomTrailingRadius: 16, topTrailingRadius: 0 ) ) } .overlay( RoundedRectangle(cornerRadius: 16) .stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 3) ) .shadow( color: colorScheme == .dark ? .clear : .black.opacity(0.08), radius: 8, x: 0, y: 4 ) } .buttonStyle(.plain) .accessibilityIdentifier(AccessibilityID.Customize.appThemeCard(theme.name)) } } // MARK: - Theme Preview Sheet struct AppThemePreviewSheet: View { let theme: AppTheme let onApply: () -> Void @Environment(\.dismiss) private var dismiss @Environment(\.colorScheme) private var colorScheme var body: some View { NavigationStack { ScrollView { VStack(spacing: 24) { // Hero heroSection // What's included componentsSection // Apply button applyButton .padding(.bottom, 40) } } .background(colorScheme == .dark ? Color.black : Color(.systemGroupedBackground)) .navigationTitle(theme.name) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Cancel") { dismiss() } .accessibilityIdentifier(AccessibilityID.Customize.appThemePreviewCancelButton) } } } .presentationDetents([.large]) } private var heroSection: some View { VStack(spacing: 16) { Text(theme.emoji) .font(.system(size: 72)) .shadow(color: .black.opacity(0.3), radius: 8, x: 0, y: 4) Text(theme.tagline) .font(.title3.weight(.medium)) .foregroundColor(.white) .shadow(color: .black.opacity(0.3), radius: 4, x: 0, y: 2) } .frame(maxWidth: .infinity) .frame(height: 200) .background( LinearGradient( colors: theme.previewColors + [theme.previewColors[0].opacity(0.5)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .clipShape(RoundedRectangle(cornerRadius: 20)) .padding(.horizontal, 20) .padding(.top, 16) } private var componentsSection: some View { VStack(alignment: .leading, spacing: 16) { Text("This theme includes") .font(.headline) .padding(.horizontal, 20) VStack(spacing: 12) { ThemeColorRow( icon: "paintpalette.fill", title: "Colors", moodTint: theme.colorTint, color: .orange ) ThemeComponentRow( icon: "face.smiling.fill", title: "Icons", value: iconName(for: theme.iconPack), color: .purple ) ThemeComponentRow( icon: "rectangle.stack.fill", title: "Entry Style", value: theme.entryStyle.displayName, color: .blue ) ThemeComponentRow( icon: "hand.tap.fill", title: "Voting Layout", value: theme.votingLayout.displayName, color: .green ) } .padding(.horizontal, 20) // Description Text(theme.description) .font(.subheadline) .foregroundColor(.secondary) .padding(.horizontal, 20) .padding(.top, 8) } } private var applyButton: some View { Button(action: { onApply() dismiss() }) { HStack { Image(systemName: "paintbrush.fill") Text("Apply \(theme.name) Theme") } .font(.headline) .foregroundColor(.white) .frame(maxWidth: .infinity) .padding(.vertical, 16) .background( LinearGradient( colors: [theme.previewColors[0], theme.previewColors[1]], startPoint: .leading, endPoint: .trailing ) ) .clipShape(RoundedRectangle(cornerRadius: 14)) .shadow(color: theme.previewColors[0].opacity(0.4), radius: 8, x: 0, y: 4) } .padding(.horizontal, 20) .accessibilityIdentifier(AccessibilityID.Customize.appThemePreviewApplyButton) } private func iconName(for pack: MoodImages) -> String { switch pack { case .FontAwesome: return "Classic Faces" case .Emoji: return "Emoji" case .HandEmjoi: return "Hand Gestures" case .Weather: return "Weather" case .Garden: return "Garden" case .Hearts: return "Hearts" case .Cosmic: return "Cosmic" } } } // MARK: - Component Row struct ThemeComponentRow: View { let icon: String let title: String let value: String let color: Color @Environment(\.colorScheme) private var colorScheme var body: some View { HStack(spacing: 14) { Image(systemName: icon) .font(.title3) .foregroundColor(color) .frame(width: 36, height: 36) .background(color.opacity(0.15)) .clipShape(RoundedRectangle(cornerRadius: 8)) Text(title) .font(.subheadline) .foregroundColor(.secondary) Spacer() Text(value) .font(.subheadline.weight(.medium)) .foregroundColor(.primary) } .padding(.horizontal, 16) .padding(.vertical, 12) .background(colorScheme == .dark ? Color(.systemGray6) : .white) .clipShape(RoundedRectangle(cornerRadius: 12)) } } // MARK: - Color Row with Circles struct ThemeColorRow: View { let icon: String let title: String let moodTint: MoodTints let color: Color @Environment(\.colorScheme) private var colorScheme private let moods: [Mood] = [.great, .good, .average, .bad, .horrible] var body: some View { HStack(spacing: 14) { Image(systemName: icon) .font(.title3) .foregroundColor(color) .frame(width: 36, height: 36) .background(color.opacity(0.15)) .clipShape(RoundedRectangle(cornerRadius: 8)) Text(title) .font(.subheadline) .foregroundColor(.secondary) Spacer() HStack(spacing: 6) { ForEach(moods, id: \.rawValue) { mood in Circle() .fill(moodTint.color(forMood: mood)) .frame(width: 20, height: 20) } } } .padding(.horizontal, 16) .padding(.vertical, 12) .background(colorScheme == .dark ? Color(.systemGray6) : .white) .clipShape(RoundedRectangle(cornerRadius: 12)) } } // MARK: - Preview struct AppThemePickerView_Previews: PreviewProvider { static var previews: some View { AppThemePickerView() } }