// // CustomizeView.swift // Reflect (iOS) // // Created by Trey Tartt on 2/19/22. // import SwiftUI import StoreKit // MARK: - Customize Content View (for use in SettingsTabView) struct CustomizeContentView: View { @Environment(\.colorScheme) private var colorScheme @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @State private var showThemePicker = false var body: some View { ScrollView { VStack(spacing: 24) { // QUICK THEMES SettingsSection(title: "Quick Start") { Button(action: { showThemePicker = true }) { HStack(spacing: 16) { // Emoji preview Text("🎨") .font(.title) .frame(width: 56, height: 56) .background( LinearGradient( colors: [.purple.opacity(0.8), .blue.opacity(0.8), .cyan.opacity(0.8)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .clipShape(RoundedRectangle(cornerRadius: 12)) VStack(alignment: .leading, spacing: 4) { Text("Browse Themes") .font(.headline) .foregroundColor(.primary) Text("12 curated combinations of colors, icons, and layouts") .font(.caption) .foregroundColor(.secondary) .lineLimit(2) } Spacer() Image(systemName: "chevron.right") .font(.subheadline.weight(.semibold)) .foregroundColor(.secondary) } .padding(12) } .buttonStyle(.plain) .accessibilityIdentifier(AccessibilityID.Customize.browseThemesButton) } .sheet(isPresented: $showThemePicker) { AppThemePickerView() } // APPEARANCE SettingsSection(title: "Appearance") { SettingsRow(title: "Theme") { ThemePickerCompact() } } // MOOD STYLE SettingsSection(title: "Mood Style") { VStack(spacing: 16) { // Icon Style SettingsRow(title: "Icons") { ImagePackPickerCompact() } Divider() // Day View Style SettingsRow(title: "Entry Style") { DayViewStylePickerCompact() } Divider() // Voting Layout SettingsRow(title: "Voting Layout") { VotingLayoutPickerCompact() } } } // VOTE ANIMATION SettingsSection(title: "Vote Animation") { CelebrationAnimationPickerCompact() } // WIDGETS // SettingsSection(title: "Widgets") { // CustomWidgetSection() // } // NOTIFICATIONS SettingsSection(title: "Notifications") { PersonalityPackPickerCompact() } } .padding(.horizontal, 16) .padding(.bottom, 32) } .onAppear(perform: { AnalyticsManager.shared.trackScreen(.customize) }) .customizeLayoutTip() } } // MARK: - Legacy CustomizeView (kept for backwards compatibility) struct CustomizeView: View { @State private var showSubscriptionStore = false @EnvironmentObject var iapManager: IAPManager @Environment(\.colorScheme) private var colorScheme @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system var body: some View { ScrollView { VStack(spacing: 24) { // Header headerView // Subscription Banner SubscriptionBannerView(showSubscriptionStore: $showSubscriptionStore) .environmentObject(iapManager) // APPEARANCE SettingsSection(title: "Appearance") { SettingsRow(title: "Theme") { ThemePickerCompact() } } // MOOD STYLE SettingsSection(title: "Mood Style") { VStack(spacing: 16) { // Icon Style SettingsRow(title: "Icons") { ImagePackPickerCompact() } Divider() // Day View Style SettingsRow(title: "Entry Style") { DayViewStylePickerCompact() } Divider() // Voting Layout SettingsRow(title: "Voting Layout") { VotingLayoutPickerCompact() } } } // VOTE ANIMATION SettingsSection(title: "Vote Animation") { CelebrationAnimationPickerCompact() } // WIDGETS // SettingsSection(title: "Widgets") { // CustomWidgetSection() // } // NOTIFICATIONS SettingsSection(title: "Notifications") { PersonalityPackPickerCompact() } } .padding(.horizontal, 16) .padding(.bottom, 32) } .onAppear(perform: { AnalyticsManager.shared.trackScreen(.customize) }) .sheet(isPresented: $showSubscriptionStore) { ReflectSubscriptionStoreView(source: "customize") } .background( theme.currentTheme.bg .edgesIgnoringSafeArea(.all) ) } private var headerView: some View { HStack { Text("Customize") .font(.title.weight(.bold)) .foregroundColor(theme.currentTheme.labelColor) Spacer() } .padding(.top, 8) } } // MARK: - Settings Section struct SettingsSection: View { let title: String @ViewBuilder let content: Content @Environment(\.colorScheme) private var colorScheme @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system var body: some View { VStack(alignment: .leading, spacing: 12) { Text(title.uppercased()) .font(.caption.weight(.semibold)) .foregroundColor(theme.currentTheme.labelColor.opacity(0.4)) .tracking(0.5) VStack(spacing: 0) { content } .padding(16) .background( RoundedRectangle(cornerRadius: 16) .fill(Color(.systemGray6)) ) } } } // MARK: - Settings Row struct SettingsRow: View { let title: String @ViewBuilder let content: Content @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system var body: some View { VStack(alignment: .leading, spacing: 12) { Text(title) .font(.subheadline.weight(.medium)) .foregroundColor(theme.currentTheme.labelColor.opacity(0.7)) content } } } // MARK: - Theme Picker struct ThemePickerCompact: View { @Environment(\.colorScheme) var colorScheme @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system var body: some View { HStack(spacing: 20) { ForEach(Theme.allCases, id: \.rawValue) { aTheme in Button(action: { theme = aTheme AnalyticsManager.shared.track(.themeChanged(themeId: aTheme.rawValue)) }) { VStack(spacing: 8) { ZStack { aTheme.currentTheme.preview .overlay( Circle() .stroke(theme == aTheme ? Color.accentColor : Color(.systemGray4), lineWidth: 2) ) if theme == aTheme { Image(systemName: "checkmark.circle.fill") .font(.headline) .foregroundColor(.accentColor) .background(Circle().fill(.white).padding(2)) .offset(x: 14, y: 14) } } Text(aTheme.title) .font(.caption.weight(.medium)) .foregroundColor(theme == aTheme ? .accentColor : theme.currentTheme.labelColor.opacity(0.6)) } } .buttonStyle(BorderlessButtonStyle()) .accessibilityIdentifier(AccessibilityID.Customize.themeButton(aTheme.title)) } Spacer() } } } // MARK: - Image Pack Picker struct ImagePackPickerCompact: View { @AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default @AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome @AppStorage(UserDefaultsStore.Keys.customMoodTintUpdateNumber.rawValue, store: GroupUserDefaults.groupDefaults) private var customMoodTintUpdateNumber: Int = 0 @Environment(\.colorScheme) private var colorScheme var body: some View { Text(String(customMoodTintUpdateNumber)).hidden().frame(height: 0) VStack(spacing: 8) { ForEach(MoodImages.allCases, id: \.rawValue) { images in Button(action: { let impactMed = UIImpactFeedbackGenerator(style: .medium) impactMed.impactOccurred() imagePack = images AnalyticsManager.shared.track(.iconPackChanged(packId: images.rawValue)) }) { HStack { HStack(spacing: 16) { ForEach(Mood.allValues, id: \.self) { mood in images.icon(forMood: mood) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 28, height: 28) .foregroundColor(moodTint.color(forMood: mood)) .accessibilityLabel(mood.strValue) } } Spacer() if imagePack == images { Image(systemName: "checkmark.circle.fill") .font(.title2) .foregroundColor(.accentColor) } } .padding(14) .background( RoundedRectangle(cornerRadius: 12) .fill(imagePack == images ? Color.accentColor.opacity(0.08) : (colorScheme == .dark ? Color(.systemGray5) : .white)) ) } .buttonStyle(.plain) .accessibilityIdentifier(AccessibilityID.Customize.iconPackButton("\(images)")) } } } } // MARK: - Voting Layout Picker struct VotingLayoutPickerCompact: View { @AppStorage(UserDefaultsStore.Keys.votingLayoutStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var votingLayoutStyle: Int = 0 @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @Environment(\.colorScheme) private var colorScheme private var currentLayout: VotingLayoutStyle { VotingLayoutStyle(rawValue: votingLayoutStyle) ?? .horizontal } var body: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in Button(action: { if UIAccessibility.isReduceMotionEnabled { votingLayoutStyle = layout.rawValue } else { withAnimation(.easeInOut(duration: 0.2)) { votingLayoutStyle = layout.rawValue } } AnalyticsManager.shared.track(.votingLayoutChanged(layout: layout.displayName)) }) { VStack(spacing: 6) { layoutIcon(for: layout) .frame(width: 44, height: 44) .foregroundColor(currentLayout == layout ? .accentColor : theme.currentTheme.labelColor.opacity(0.4)) Text(layout.displayName) .font(.caption2.weight(.medium)) .foregroundColor(currentLayout == layout ? .accentColor : theme.currentTheme.labelColor.opacity(0.5)) } .frame(width: 70) .padding(.vertical, 12) .background( RoundedRectangle(cornerRadius: 12) .fill(currentLayout == layout ? Color.accentColor.opacity(0.1) : (colorScheme == .dark ? Color(.systemGray5) : .white)) ) } .buttonStyle(.plain) .accessibilityIdentifier(AccessibilityID.Customize.votingLayoutButton(layout.displayName)) } } .padding(.horizontal, 4) } } @ViewBuilder private func layoutIcon(for layout: VotingLayoutStyle) -> some View { switch layout { case .horizontal: HStack(spacing: 4) { ForEach(0..<5, id: \.self) { _ in Circle().frame(width: 7, height: 7) } } case .cards: LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 4) { ForEach(0..<6, id: \.self) { _ in RoundedRectangle(cornerRadius: 3).frame(width: 10, height: 12) } } case .stacked: VStack(spacing: 4) { ForEach(0..<4, id: \.self) { _ in RoundedRectangle(cornerRadius: 2).frame(width: 32, height: 7) } } case .aura: // Glowing orbs in 2 rows VStack(spacing: 4) { HStack(spacing: 6) { ForEach(0..<3, id: \.self) { _ in ZStack { Circle() .fill(RadialGradient(colors: [.green.opacity(0.5), .clear], center: .center, startRadius: 0, endRadius: 8)) .frame(width: 14, height: 14) Circle() .fill(.green) .frame(width: 8, height: 8) } } } HStack(spacing: 10) { ForEach(0..<2, id: \.self) { _ in ZStack { Circle() .fill(RadialGradient(colors: [.green.opacity(0.5), .clear], center: .center, startRadius: 0, endRadius: 8)) .frame(width: 14, height: 14) Circle() .fill(.green) .frame(width: 8, height: 8) } } } } case .orbit: // Center core with orbiting planets ZStack { Circle() .stroke(Color.primary.opacity(0.2), lineWidth: 1) .frame(width: 32, height: 32) Circle() .fill(Color.primary.opacity(0.8)) .frame(width: 8, height: 8) ForEach(0..<5, id: \.self) { index in Circle() .fill(Color.accentColor) .frame(width: 6, height: 6) .offset(orbitOffset(index: index, total: 5, radius: 16)) } } case .neon: // Equalizer bars ZStack { RoundedRectangle(cornerRadius: 4) .fill(Color.black) .frame(width: 36, height: 36) HStack(spacing: 2) { ForEach(0..<5, id: \.self) { index in let heights: [CGFloat] = [24, 18, 14, 10, 8] RoundedRectangle(cornerRadius: 1) .fill( LinearGradient( colors: [Color(red: 0, green: 1, blue: 0.82), Color(red: 1, green: 0, blue: 0.8)], startPoint: .top, endPoint: .bottom ) ) .frame(width: 4, height: heights[index]) } } } } } private func orbitOffset(index: Int, total: Int, radius: CGFloat) -> CGSize { let startAngle = -Double.pi / 2 let angleStep = (2 * Double.pi) / Double(total) let angle = startAngle + angleStep * Double(index) return CGSize(width: radius * CGFloat(cos(angle)), height: radius * CGFloat(sin(angle))) } } // MARK: - Celebration Animation Picker struct CelebrationAnimationPickerCompact: View { private enum AnimationConstants { static let previewTriggerDelay: TimeInterval = 0.5 static let dismissTransitionDelay: TimeInterval = 0.35 } @AppStorage(UserDefaultsStore.Keys.celebrationAnimation.rawValue, store: GroupUserDefaults.groupDefaults) private var celebrationAnimationIndex: Int = 0 @AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @AppStorage(UserDefaultsStore.Keys.hapticFeedbackEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var hapticFeedbackEnabled = true @Environment(\.colorScheme) private var colorScheme // Preview state @State private var previewAnimation: CelebrationAnimationType? @State private var previewMood: Mood = .great @State private var showPreviewCelebration = false @State private var previewScale: CGFloat = 1.0 @State private var previewOpacity: Double = 1.0 private var currentAnimation: CelebrationAnimationType { CelebrationAnimationType.fromIndex(celebrationAnimationIndex) } var body: some View { VStack(spacing: 0) { // Animation style picker ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(CelebrationAnimationType.allCases) { animation in Button(action: { selectAnimation(animation) }) { VStack(spacing: 6) { Image(systemName: animation.icon) .font(.title2) .frame(width: 44, height: 44) .foregroundColor(currentAnimation == animation ? animation.accentColor : theme.currentTheme.labelColor.opacity(0.4)) Text(animation.rawValue) .font(.caption2.weight(.medium)) .foregroundColor(currentAnimation == animation ? animation.accentColor : theme.currentTheme.labelColor.opacity(0.5)) } .frame(width: 70) .padding(.vertical, 12) .background( RoundedRectangle(cornerRadius: 12) .fill(currentAnimation == animation ? animation.accentColor.opacity(0.1) : (colorScheme == .dark ? Color(.systemGray5) : .white)) ) } .buttonStyle(.plain) .accessibilityIdentifier(AccessibilityID.Customize.celebrationAnimationButton(animation.rawValue)) } } .padding(.horizontal, 4) } // Inline animation preview if let animation = previewAnimation { AnimationPreviewView( animation: animation, mood: previewMood, moodTint: moodTint, showCelebration: showPreviewCelebration, onCelebrationComplete: { dismissPreview() } ) .scaleEffect(previewScale) .opacity(previewOpacity) .padding(.top, 16) .transition(.scale(scale: 0.8).combined(with: .opacity)) } } } private func selectAnimation(_ animation: CelebrationAnimationType) { // Save preference if UIAccessibility.isReduceMotionEnabled { celebrationAnimationIndex = animation.index } else { withAnimation(.easeInOut(duration: 0.2)) { celebrationAnimationIndex = animation.index } } AnalyticsManager.shared.track(.celebrationAnimationChanged(animation: animation.rawValue)) // Reset and show preview previewScale = 1.0 previewOpacity = 1.0 showPreviewCelebration = false previewMood = Mood.allValues.randomElement() ?? .great withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) { previewAnimation = animation } // Auto-trigger the celebration after a brief pause Task { @MainActor in try? await Task.sleep(for: .seconds(AnimationConstants.previewTriggerDelay)) guard previewAnimation == animation else { return } if hapticFeedbackEnabled { HapticFeedbackManager.shared.play(for: animation) } withAnimation(.easeInOut(duration: 0.3)) { showPreviewCelebration = true } } } private func dismissPreview() { withAnimation(.easeIn(duration: 0.3)) { previewScale = 0.6 previewOpacity = 0 } Task { @MainActor in try? await Task.sleep(for: .seconds(AnimationConstants.dismissTransitionDelay)) withAnimation(.easeOut(duration: 0.15)) { previewAnimation = nil } } } } // MARK: - Animation Preview (inline in Customize) private struct AnimationPreviewView: View { let animation: CelebrationAnimationType let mood: Mood let moodTint: MoodTints let showCelebration: Bool let onCelebrationComplete: () -> Void var body: some View { ZStack { // Mini voting row (fades out when celebration starts) HStack(spacing: 12) { ForEach(Mood.allValues, id: \.self) { m in m.icon .resizable() .aspectRatio(contentMode: .fit) .frame(width: 36, height: 36) .foregroundColor(moodTint.color(forMood: m)) .scaleEffect(showCelebration && m == mood ? 1.3 : 1.0) .opacity(showCelebration && m != mood ? 0.3 : 1.0) } } .opacity(showCelebration ? 0 : 1) // Celebration overlay if showCelebration { CelebrationOverlayView( animationType: animation, mood: mood, onComplete: onCelebrationComplete ) } } .frame(height: 160) .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 16) .fill(Color(.systemGray6).opacity(0.5)) ) .clipShape(RoundedRectangle(cornerRadius: 16)) } } // MARK: - Custom Widget Section struct CustomWidgetSection: View { @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @StateObject private var selectedWidget = CustomWidgetStateViewModel() var body: some View { VStack(spacing: 12) { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { ForEach(UserDefaultsStore.getCustomWidgets(), id: \.uuid) { widget in CustomWidgetView(customWidgetModel: widget) .frame(width: 60, height: 60) .cornerRadius(12) .accessibilityIdentifier(AccessibilityID.Customize.customWidget(UserDefaultsStore.getCustomWidgets().firstIndex(where: { $0.uuid == widget.uuid }) ?? 0)) .onTapGesture { AnalyticsManager.shared.track(.widgetViewed) selectedWidget.selectedItem = widget.copy() as? CustomWidgetModel selectedWidget.showSheet = true } } // Add button Button(action: { AnalyticsManager.shared.track(.widgetCreateTapped) selectedWidget.selectedItem = CustomWidgetModel.randomWidget selectedWidget.showSheet = true }) { ZStack { RoundedRectangle(cornerRadius: 12) .fill(Color(.systemGray5)) .frame(width: 60, height: 60) Image(systemName: "plus") .font(.title2.weight(.medium)) .foregroundColor(.secondary) } } .accessibilityIdentifier(AccessibilityID.Customize.customWidgetAdd) } } Link(destination: URL(string: "https://support.apple.com/guide/iphone/add-widgets-iphb8f1bf206/ios")!) { HStack(spacing: 6) { Image(systemName: "questionmark.circle") .font(.subheadline) Text("How to add widgets") .font(.subheadline) } .foregroundColor(.accentColor) } .accessibilityIdentifier(AccessibilityID.Customize.widgetHowToLink) } .sheet(isPresented: $selectedWidget.showSheet) { if let selectedItem = selectedWidget.selectedItem { CreateWidgetView(customWidget: selectedItem) } } } } // MARK: - Personality Pack Picker struct PersonalityPackPickerCompact: View { @AppStorage(UserDefaultsStore.Keys.personalityPack.rawValue, store: GroupUserDefaults.groupDefaults) private var personalityPack: PersonalityPack = .Default @AppStorage(UserDefaultsStore.Keys.showNSFW.rawValue, store: GroupUserDefaults.groupDefaults) private var showNSFW: Bool = false @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @State private var showOver18Alert = false @Environment(\.colorScheme) private var colorScheme var body: some View { VStack(spacing: 8) { ForEach(PersonalityPack.allCases, id: \.self) { aPack in Button(action: { // if aPack.rawValue == PersonalityPack.Rude.rawValue && !showNSFW { // showOver18Alert = true // } else { let impactMed = UIImpactFeedbackGenerator(style: .medium) impactMed.impactOccurred() personalityPack = aPack AnalyticsManager.shared.track(.personalityPackChanged(packTitle: aPack.title())) LocalNotification.rescheduleNotifiations() // } }) { HStack { VStack(alignment: .leading, spacing: 4) { Text(String(aPack.title())) .font(.subheadline.weight(.semibold)) .foregroundColor(theme.currentTheme.labelColor) let strings = aPack.randomPushNotificationStrings() Text(strings.body) .font(.caption) .foregroundColor(theme.currentTheme.labelColor.opacity(0.5)) .lineLimit(2) } Spacer() if personalityPack == aPack { Image(systemName: "checkmark.circle.fill") .font(.title2) .foregroundColor(.accentColor) } } .padding(14) .background( RoundedRectangle(cornerRadius: 12) .fill(personalityPack == aPack ? Color.accentColor.opacity(0.08) : (colorScheme == .dark ? Color(.systemGray5) : .white)) ) } .buttonStyle(.plain) .accessibilityIdentifier(AccessibilityID.Customize.personalityPackButton(aPack.title())) // .blur(radius: aPack.rawValue == PersonalityPack.Rude.rawValue && !showNSFW ? 4 : 0) } } .alert(isPresented: $showOver18Alert) { Alert( title: Text(String(localized: "customize_view_over18alert_title")), message: Text(String(localized: "customize_view_over18alert_body")), primaryButton: .default(Text(String(localized: "customize_view_over18alert_ok"))) { showNSFW = true }, secondaryButton: .cancel(Text(String(localized: "customize_view_over18alert_no"))) { showNSFW = false } ) } } } // MARK: - Subscription Banner struct SubscriptionBannerView: View { @Binding var showSubscriptionStore: Bool @EnvironmentObject var iapManager: IAPManager @Environment(\.colorScheme) private var colorScheme var body: some View { if iapManager.isSubscribed { subscribedView } else { notSubscribedView } } private var subscribedView: some View { HStack(spacing: 12) { Image(systemName: "checkmark.seal.fill") .font(.title) .foregroundColor(.green) VStack(alignment: .leading, spacing: 2) { Text("Premium Active") .font(.body.weight(.semibold)) Text("You have full access") .font(.caption) .foregroundColor(.secondary) } Spacer() Button("Manage") { Task { await openSubscriptionManagement() } } .font(.subheadline.weight(.semibold)) .foregroundColor(.green) .padding(.horizontal, 16) .padding(.vertical, 8) .background(Capsule().fill(Color.green.opacity(0.15))) .accessibilityIdentifier(AccessibilityID.Customize.manageSubscriptionButton) } .padding(16) } private var notSubscribedView: some View { Button(action: { AnalyticsManager.shared.track(.paywallSubscribeTapped(source: "customize")) showSubscriptionStore = true }) { HStack(spacing: 12) { Image(systemName: "crown.fill") .font(.title) .foregroundColor(.orange) VStack(alignment: .leading, spacing: 2) { Text("Unlock Premium") .font(.body.weight(.semibold)) .foregroundColor(colorScheme == .dark ? .white : .black) Text("Month & Year views, Insights & more") .font(.caption) .foregroundColor(.secondary) } Spacer() Image(systemName: "chevron.right") .font(.subheadline.weight(.semibold)) .foregroundColor(.secondary) } .padding(16) .contentShape(Rectangle()) .background( RoundedRectangle(cornerRadius: 16) .fill( LinearGradient( colors: [Color.orange.opacity(0.12), Color.pink.opacity(0.08)], startPoint: .leading, endPoint: .trailing ) ) ) } .buttonStyle(.plain) .accessibilityIdentifier(AccessibilityID.Customize.unlockPremiumButton) } private func openSubscriptionManagement() async { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { do { try await AppStore.showManageSubscriptions(in: windowScene) } catch { #if DEBUG print("Failed to open subscription management: \(error)") #endif } } } } // MARK: - Day View Style Picker struct DayViewStylePickerCompact: View { @AppStorage(UserDefaultsStore.Keys.dayViewStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var dayViewStyle: DayViewStyle = .classic @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @Environment(\.colorScheme) private var colorScheme var body: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(DayViewStyle.availableCases, id: \.rawValue) { style in Button(action: { if UIAccessibility.isReduceMotionEnabled { dayViewStyle = style } else { withAnimation(.easeInOut(duration: 0.2)) { dayViewStyle = style } } let impactMed = UIImpactFeedbackGenerator(style: .medium) impactMed.impactOccurred() AnalyticsManager.shared.track(.dayViewStyleChanged(style: style.displayName)) }) { VStack(spacing: 6) { styleIcon(for: style) .frame(width: 44, height: 44) .foregroundColor(dayViewStyle == style ? .accentColor : theme.currentTheme.labelColor.opacity(0.4)) Text(style.displayName) .font(.caption2.weight(.medium)) .foregroundColor(dayViewStyle == style ? .accentColor : theme.currentTheme.labelColor.opacity(0.5)) } .frame(width: 70) .padding(.vertical, 12) .background( RoundedRectangle(cornerRadius: 12) .fill(dayViewStyle == style ? Color.accentColor.opacity(0.1) : (colorScheme == .dark ? Color(.systemGray5) : .white)) ) } .buttonStyle(.plain) .accessibilityIdentifier(AccessibilityID.Customize.dayViewStyleButton(style.displayName)) } } .padding(.horizontal, 4) } } @ViewBuilder private func styleIcon(for style: DayViewStyle) -> some View { switch style { case .classic: // Card with gradient circle and text HStack(spacing: 6) { Circle() .fill(LinearGradient(colors: [.green, .green.opacity(0.5)], startPoint: .topLeading, endPoint: .bottomTrailing)) .frame(width: 16, height: 16) VStack(alignment: .leading, spacing: 2) { RoundedRectangle(cornerRadius: 1).frame(width: 18, height: 4) RoundedRectangle(cornerRadius: 1).frame(width: 12, height: 3).opacity(0.5) } } case .minimal: // Simple flat card HStack(spacing: 6) { Circle() .strokeBorder(lineWidth: 1.5) .frame(width: 14, height: 14) VStack(alignment: .leading, spacing: 2) { RoundedRectangle(cornerRadius: 1).frame(width: 18, height: 4) RoundedRectangle(cornerRadius: 1).frame(width: 10, height: 3).opacity(0.5) } } case .compact: // Timeline dots with bars HStack(spacing: 4) { VStack(spacing: 3) { Circle().frame(width: 6, height: 6) Circle().frame(width: 6, height: 6) Circle().frame(width: 6, height: 6) } VStack(spacing: 3) { RoundedRectangle(cornerRadius: 2).frame(width: 24, height: 8) RoundedRectangle(cornerRadius: 2).frame(width: 24, height: 8) RoundedRectangle(cornerRadius: 2).frame(width: 24, height: 8) } } case .bubble: // Full-width colored bars VStack(spacing: 4) { RoundedRectangle(cornerRadius: 4).fill(.green).frame(width: 34, height: 10) RoundedRectangle(cornerRadius: 4).fill(.yellow).frame(width: 34, height: 10) RoundedRectangle(cornerRadius: 4).fill(.blue).frame(width: 34, height: 10) } case .grid: // 3x3 grid of circles LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 4) { Circle().fill(.green).frame(width: 10, height: 10) Circle().fill(.yellow).frame(width: 10, height: 10) Circle().fill(.blue).frame(width: 10, height: 10) Circle().fill(.orange).frame(width: 10, height: 10) Circle().fill(.green).frame(width: 10, height: 10) Circle().fill(.yellow).frame(width: 10, height: 10) } case .aura: // Giant number with glowing orb HStack(spacing: 4) { Text("17") .font(.title3.weight(.black)) .foregroundStyle( LinearGradient(colors: [.green, .green.opacity(0.5)], startPoint: .top, endPoint: .bottom) ) ZStack { Circle() .fill( RadialGradient(colors: [.green.opacity(0.6), .clear], center: .center, startRadius: 0, endRadius: 12) ) .frame(width: 24, height: 24) Circle() .fill(.green) .frame(width: 12, height: 12) } } case .chronicle: // Editorial magazine style VStack(alignment: .leading, spacing: 2) { Rectangle().frame(width: 34, height: 2) HStack(spacing: 4) { Text("12") .font(.headline.weight(.regular)) Rectangle().frame(width: 1, height: 20) VStack(alignment: .leading, spacing: 1) { RoundedRectangle(cornerRadius: 1).frame(width: 12, height: 3) RoundedRectangle(cornerRadius: 1).frame(width: 8, height: 2).opacity(0.5) } } } case .neon: // Cyberpunk neon style ZStack { RoundedRectangle(cornerRadius: 2) .fill(Color.black) .frame(width: 38, height: 28) RoundedRectangle(cornerRadius: 4) .stroke(Color.green, lineWidth: 1) .frame(width: 16, height: 16) .shadow(color: .green, radius: 4, x: 0, y: 0) RoundedRectangle(cornerRadius: 2) .stroke(Color.green.opacity(0.5), lineWidth: 0.5) .frame(width: 38, height: 28) } case .ink: // Japanese zen style HStack(spacing: 6) { ZStack { Circle() .trim(from: 0, to: 0.85) .stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round)) .frame(width: 18, height: 18) .rotationEffect(.degrees(20)) } VStack(alignment: .leading, spacing: 2) { RoundedRectangle(cornerRadius: 1).frame(width: 14, height: 2).opacity(0.3) RoundedRectangle(cornerRadius: 1).frame(width: 10, height: 2).opacity(0.6) } } case .prism: // Glassmorphism with rainbow edge ZStack { RoundedRectangle(cornerRadius: 6) .fill( AngularGradient(colors: [.red, .orange, .yellow, .green, .blue, .purple, .red], center: .center) ) .frame(width: 36, height: 26) .blur(radius: 3) .opacity(0.6) RoundedRectangle(cornerRadius: 5) .fill(.ultraThinMaterial) .frame(width: 32, height: 22) Circle() .fill(.green.opacity(0.5)) .frame(width: 10, height: 10) .offset(x: -6) } case .tape: // Cassette tape reels HStack(spacing: 8) { ZStack { Circle().stroke(lineWidth: 2).frame(width: 14, height: 14) Circle().frame(width: 6, height: 6) } VStack(spacing: 2) { RoundedRectangle(cornerRadius: 1).frame(width: 16, height: 3) RoundedRectangle(cornerRadius: 1).frame(width: 16, height: 2).opacity(0.5) } ZStack { Circle().stroke(lineWidth: 2).frame(width: 14, height: 14) Circle().frame(width: 6, height: 6) } } case .morph: // Organic blob shapes ZStack { Ellipse() .fill(.green.opacity(0.4)) .frame(width: 28, height: 22) .blur(radius: 4) Ellipse() .fill(.green.opacity(0.6)) .frame(width: 18, height: 14) .offset(x: 4, y: 2) .blur(radius: 2) Circle() .fill(.green) .frame(width: 12, height: 12) } case .stack: // Layered paper notes ZStack { RoundedRectangle(cornerRadius: 3) .frame(width: 28, height: 22) .opacity(0.3) .offset(x: 3, y: 3) RoundedRectangle(cornerRadius: 3) .frame(width: 28, height: 22) .opacity(0.5) .offset(x: 1.5, y: 1.5) RoundedRectangle(cornerRadius: 3) .frame(width: 28, height: 22) VStack(spacing: 3) { Rectangle().frame(width: 18, height: 2) Rectangle().frame(width: 14, height: 2).opacity(0.5) } } case .wave: // Horizontal gradient wave VStack(spacing: 3) { Capsule().fill(.green).frame(width: 34, height: 8) Capsule().fill(.green.opacity(0.6)).frame(width: 34, height: 8) Capsule().fill(.green.opacity(0.3)).frame(width: 34, height: 8) } case .pattern: // Repeating pattern of icons ZStack { VStack(spacing: 6) { HStack(spacing: 8) { Circle().frame(width: 6, height: 6).opacity(0.2) Circle().frame(width: 6, height: 6).opacity(0.2) Circle().frame(width: 6, height: 6).opacity(0.2) } HStack(spacing: 8) { Circle().frame(width: 6, height: 6).opacity(0.2) Circle().frame(width: 6, height: 6).opacity(0.2) Circle().frame(width: 6, height: 6).opacity(0.2) } } RoundedRectangle(cornerRadius: 4) .fill(.green.opacity(0.3)) .frame(width: 28, height: 18) Circle() .fill(.green) .frame(width: 12, height: 12) .offset(x: -6) } case .leather: // Skeuomorphic leather ZStack { RoundedRectangle(cornerRadius: 4) .fill(Color(red: 0.4, green: 0.28, blue: 0.18)) .frame(width: 36, height: 26) RoundedRectangle(cornerRadius: 3) .strokeBorder(style: StrokeStyle(lineWidth: 1, dash: [2, 2])) .foregroundColor(Color(red: 0.6, green: 0.5, blue: 0.35)) .frame(width: 30, height: 20) Circle() .fill(Color(red: 0.8, green: 0.7, blue: 0.5)) .frame(width: 10, height: 10) } case .glass: // Liquid glass effect ZStack { RoundedRectangle(cornerRadius: 8) .fill(.ultraThinMaterial) .frame(width: 36, height: 26) RoundedRectangle(cornerRadius: 8) .fill( LinearGradient( colors: [.white.opacity(0.5), .white.opacity(0.1)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 36, height: 26) Circle() .fill(.green.opacity(0.5)) .frame(width: 12, height: 12) .offset(x: -6) .blur(radius: 2) } case .motion: // Accelerometer motion effect ZStack { RoundedRectangle(cornerRadius: 8) .fill( LinearGradient( colors: [.blue.opacity(0.3), .purple.opacity(0.3)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 36, height: 26) Circle() .fill(.purple.opacity(0.4)) .frame(width: 16, height: 16) .offset(x: 8, y: -4) .blur(radius: 3) Circle() .fill(.blue.opacity(0.4)) .frame(width: 12, height: 12) .offset(x: -6, y: 4) .blur(radius: 2) Image(systemName: "gyroscope") .font(.caption.weight(.medium)) .foregroundColor(.white) } case .micro: // Ultra compact micro style VStack(spacing: 2) { HStack(spacing: 3) { Circle() .fill(.green) .frame(width: 4, height: 4) RoundedRectangle(cornerRadius: 1) .fill(Color.gray.opacity(0.3)) .frame(width: 20, height: 4) } HStack(spacing: 3) { Circle() .fill(.orange) .frame(width: 4, height: 4) RoundedRectangle(cornerRadius: 1) .fill(Color.gray.opacity(0.3)) .frame(width: 20, height: 4) } HStack(spacing: 3) { Circle() .fill(.blue) .frame(width: 4, height: 4) RoundedRectangle(cornerRadius: 1) .fill(Color.gray.opacity(0.3)) .frame(width: 20, height: 4) } } case .orbit: // Celestial orbit style ZStack { Circle() .stroke(Color.primary.opacity(0.15), lineWidth: 1) .frame(width: 28, height: 28) Circle() .fill(Color.primary.opacity(0.8)) .frame(width: 8, height: 8) Circle() .fill(Color.accentColor) .frame(width: 10, height: 10) .offset(x: 14, y: 0) } } } } struct CustomizeView_Previews: PreviewProvider { static var previews: some View { Group { CustomizeView() .environmentObject(IAPManager()) CustomizeView() .preferredColorScheme(.dark) .environmentObject(IAPManager()) } } }