// // CustomizeView.swift // Feels (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 var body: some View { ScrollView { VStack(spacing: 24) { // APPEARANCE SettingsSection(title: "Appearance") { VStack(spacing: 16) { // Theme SettingsRow(title: "Theme") { ThemePickerCompact() } Divider() // Text Color SettingsRow(title: "Text Color") { TextColorPickerCompact() } } } // MOOD STYLE SettingsSection(title: "Mood Style") { VStack(spacing: 16) { // Icon Style SettingsRow(title: "Icons") { ImagePackPickerCompact() } Divider() // Mood Colors SettingsRow(title: "Colors") { TintPickerCompact() } Divider() // Day View Style SettingsRow(title: "Entry Style") { DayViewStylePickerCompact() } Divider() // Voting Layout SettingsRow(title: "Voting Layout") { VotingLayoutPickerCompact() } } } // WIDGETS SettingsSection(title: "Widgets") { CustomWidgetSection() } // NOTIFICATIONS SettingsSection(title: "Notifications") { PersonalityPackPickerCompact() } // FILTERS SettingsSection(title: "Day Filter") { DayFilterPickerCompact() } } .padding(.horizontal, 16) .padding(.bottom, 32) } .onAppear(perform: { EventLogger.log(event: "show_customize_view") }) } } // 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 @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor var body: some View { ScrollView { VStack(spacing: 24) { // Header headerView // Subscription Banner SubscriptionBannerView(showSubscriptionStore: $showSubscriptionStore) .environmentObject(iapManager) // APPEARANCE SettingsSection(title: "Appearance") { VStack(spacing: 16) { // Theme SettingsRow(title: "Theme") { ThemePickerCompact() } Divider() // Text Color SettingsRow(title: "Text Color") { TextColorPickerCompact() } } } // MOOD STYLE SettingsSection(title: "Mood Style") { VStack(spacing: 16) { // Icon Style SettingsRow(title: "Icons") { ImagePackPickerCompact() } Divider() // Mood Colors SettingsRow(title: "Colors") { TintPickerCompact() } Divider() // Day View Style SettingsRow(title: "Entry Style") { DayViewStylePickerCompact() } Divider() // Voting Layout SettingsRow(title: "Voting Layout") { VotingLayoutPickerCompact() } } } // WIDGETS SettingsSection(title: "Widgets") { CustomWidgetSection() } // NOTIFICATIONS SettingsSection(title: "Notifications") { PersonalityPackPickerCompact() } // FILTERS SettingsSection(title: "Day Filter") { DayFilterPickerCompact() } } .padding(.horizontal, 16) .padding(.bottom, 32) } .onAppear(perform: { EventLogger.log(event: "show_customize_view") }) .sheet(isPresented: $showSubscriptionStore) { FeelsSubscriptionStoreView() } .background( theme.currentTheme.bg .edgesIgnoringSafeArea(.all) ) } private var headerView: some View { HStack { Text("Customize") .font(.system(size: 28, weight: .bold, design: .rounded)) .foregroundColor(textColor) 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.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor var body: some View { VStack(alignment: .leading, spacing: 12) { Text(title.uppercased()) .font(.system(size: 13, weight: .semibold)) .foregroundColor(textColor.opacity(0.4)) .tracking(0.5) VStack(spacing: 0) { content } .padding(16) .background( RoundedRectangle(cornerRadius: 16) .fill(colorScheme == .dark ? Color(.systemGray6) : .white) ) } } } // MARK: - Settings Row struct SettingsRow: View { let title: String @ViewBuilder let content: Content @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor var body: some View { VStack(alignment: .leading, spacing: 12) { Text(title) .font(.system(size: 15, weight: .medium)) .foregroundColor(textColor.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 @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor var body: some View { HStack(spacing: 20) { ForEach(Theme.allCases, id: \.rawValue) { aTheme in Button(action: { theme = aTheme changeTextColor(forTheme: aTheme) EventLogger.log(event: "change_theme_id", withData: ["id": 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(.system(size: 18)) .foregroundColor(.accentColor) .background(Circle().fill(.white).padding(2)) .offset(x: 14, y: 14) } } Text(aTheme.title) .font(.system(size: 12, weight: .medium)) .foregroundColor(theme == aTheme ? .accentColor : textColor.opacity(0.6)) } } .buttonStyle(.plain) } Spacer() } } private func changeTextColor(forTheme theme: Theme) { if [Theme.iFeel, Theme.system].contains(theme) { let currentSystemScheme = UITraitCollection.current.userInterfaceStyle textColor = currentSystemScheme == .dark ? .white : .black } if theme == Theme.dark { textColor = .white } if theme == Theme.light { textColor = .black } } } // MARK: - Text Color Picker struct TextColorPickerCompact: View { @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor var body: some View { HStack(spacing: 16) { ColorPicker("", selection: $textColor) .labelsHidden() Text("Sample Text") .font(.system(size: 16, weight: .medium)) .foregroundColor(textColor) 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 EventLogger.log(event: "change_image_pack_id", withData: ["id": 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)) } } Spacer() if imagePack == images { Image(systemName: "checkmark.circle.fill") .font(.system(size: 22)) .foregroundColor(.accentColor) } } .padding(14) .background( RoundedRectangle(cornerRadius: 12) .fill(imagePack == images ? Color.accentColor.opacity(0.08) : (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6))) ) } .buttonStyle(.plain) } } } } // MARK: - Tint Picker struct TintPickerCompact: View { @AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default @AppStorage(UserDefaultsStore.Keys.customMoodTintUpdateNumber.rawValue, store: GroupUserDefaults.groupDefaults) private var customMoodTintUpdateNumber: Int = 0 @StateObject private var customMoodTint = UserDefaultsStore.getCustomMoodTint() @Environment(\.colorScheme) private var colorScheme var body: some View { VStack(spacing: 8) { ForEach(MoodTints.defaultOptions, id: \.rawValue) { tint in Button(action: { let impactMed = UIImpactFeedbackGenerator(style: .medium) impactMed.impactOccurred() moodTint = tint EventLogger.log(event: "change_mood_tint_id", withData: ["id": tint.rawValue]) }) { HStack { HStack(spacing: 12) { ForEach(Mood.allValues, id: \.self) { mood in Circle() .fill(tint.color(forMood: mood)) .frame(width: 28, height: 28) } } Spacer() if moodTint == tint { Image(systemName: "checkmark.circle.fill") .font(.system(size: 22)) .foregroundColor(.accentColor) } } .padding(14) .background( RoundedRectangle(cornerRadius: 12) .fill(moodTint == tint ? Color.accentColor.opacity(0.08) : (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6))) ) } .buttonStyle(.plain) } // Custom colors Button(action: { moodTint = .Custom }) { HStack { HStack(spacing: 12) { ForEach(0..<5, id: \.self) { index in ColorPicker("", selection: colorBinding(for: index)) .labelsHidden() .onChange(of: colorBinding(for: index).wrappedValue) { saveCustomMoodTint() } } } Spacer() if moodTint == .Custom { Image(systemName: "checkmark.circle.fill") .font(.system(size: 22)) .foregroundColor(.accentColor) } else { Text("Custom") .font(.system(size: 13)) .foregroundColor(.secondary) } } .padding(14) .background( RoundedRectangle(cornerRadius: 12) .fill(moodTint == .Custom ? Color.accentColor.opacity(0.08) : (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6))) ) } .buttonStyle(.plain) } } private func colorBinding(for index: Int) -> Binding { switch index { case 0: return $customMoodTint.colorOne case 1: return $customMoodTint.colorTwo case 2: return $customMoodTint.colorThree case 3: return $customMoodTint.colorFour default: return $customMoodTint.colorFive } } private func saveCustomMoodTint() { UserDefaultsStore.saveCustomMoodTint(customTint: customMoodTint) moodTint = .Custom EventLogger.log(event: "change_mood_tint_id", withData: ["id": MoodTints.Custom.rawValue]) customMoodTintUpdateNumber += 1 } } // MARK: - Voting Layout Picker struct VotingLayoutPickerCompact: View { @AppStorage(UserDefaultsStore.Keys.votingLayoutStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var votingLayoutStyle: Int = 0 @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor @Environment(\.colorScheme) private var colorScheme private var currentLayout: VotingLayoutStyle { VotingLayoutStyle(rawValue: votingLayoutStyle) ?? .horizontal } var body: some View { HStack(spacing: 10) { ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in Button(action: { withAnimation(.easeInOut(duration: 0.2)) { votingLayoutStyle = layout.rawValue } EventLogger.log(event: "change_voting_layout", withData: ["layout": layout.displayName]) }) { VStack(spacing: 6) { layoutIcon(for: layout) .frame(width: 44, height: 44) .foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.4)) Text(layout.displayName) .font(.system(size: 11, weight: .medium)) .foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.5)) } .frame(maxWidth: .infinity) .padding(.vertical, 12) .background( RoundedRectangle(cornerRadius: 12) .fill(currentLayout == layout ? Color.accentColor.opacity(0.1) : (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6))) ) } .buttonStyle(.plain) } } } @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 .radial: ZStack { ForEach(0..<5, id: \.self) { index in Circle() .frame(width: 7, height: 7) .offset(radialOffset(index: index, total: 5, radius: 15)) } } case .stacked: VStack(spacing: 4) { ForEach(0..<4, id: \.self) { _ in RoundedRectangle(cornerRadius: 2).frame(width: 32, height: 7) } } } } private func radialOffset(index: Int, total: Int, radius: CGFloat) -> CGSize { let angle = Double.pi - (Double.pi * Double(index) / Double(total - 1)) return CGSize(width: radius * CGFloat(cos(angle)), height: -radius * CGFloat(sin(angle)) + 4) } } // MARK: - Custom Widget Section struct CustomWidgetSection: View { @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor @StateObject private var selectedWidget = StupidAssCustomWidgetObservableObject() 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) .onTapGesture { EventLogger.log(event: "show_widget") selectedWidget.fuckingWrapped = widget.copy() as? CustomWidgetModel selectedWidget.showFuckingSheet = true } } // Add button Button(action: { EventLogger.log(event: "tap_create_new_widget") selectedWidget.fuckingWrapped = CustomWidgetModel.randomWidget selectedWidget.showFuckingSheet = true }) { ZStack { RoundedRectangle(cornerRadius: 12) .fill(Color(.systemGray5)) .frame(width: 60, height: 60) Image(systemName: "plus") .font(.system(size: 24, weight: .medium)) .foregroundColor(.secondary) } } } } Link(destination: URL(string: "https://support.apple.com/guide/iphone/add-widgets-iphb8f1bf206/ios")!) { HStack(spacing: 6) { Image(systemName: "questionmark.circle") .font(.system(size: 14)) Text("How to add widgets") .font(.system(size: 14)) } .foregroundColor(.accentColor) } } .sheet(isPresented: $selectedWidget.showFuckingSheet) { if let fuckingWrapped = selectedWidget.fuckingWrapped { CreateWidgetView(customWidget: fuckingWrapped) } } } } // 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.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor @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 EventLogger.log(event: "show_over_18_alert") } else { let impactMed = UIImpactFeedbackGenerator(style: .medium) impactMed.impactOccurred() personalityPack = aPack EventLogger.log(event: "change_personality_pack", withData: ["pack_title": aPack.title()]) LocalNotification.rescheduleNotifiations() } }) { HStack { VStack(alignment: .leading, spacing: 4) { Text(String(aPack.title())) .font(.system(size: 15, weight: .semibold)) .foregroundColor(textColor) let strings = aPack.randomPushNotificationStrings() Text(strings.body) .font(.system(size: 13)) .foregroundColor(textColor.opacity(0.5)) .lineLimit(2) } Spacer() if personalityPack == aPack { Image(systemName: "checkmark.circle.fill") .font(.system(size: 22)) .foregroundColor(.accentColor) } } .padding(14) .background( RoundedRectangle(cornerRadius: 12) .fill(personalityPack == aPack ? Color.accentColor.opacity(0.08) : (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6))) ) } .buttonStyle(.plain) .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: - Day Filter Picker struct DayFilterPickerCompact: View { @StateObject private var filteredDays = DaysFilterClass.shared @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor @Environment(\.colorScheme) private var colorScheme let weekdays = [(Calendar.current.shortWeekdaySymbols[0], 1), (Calendar.current.shortWeekdaySymbols[1], 2), (Calendar.current.shortWeekdaySymbols[2], 3), (Calendar.current.shortWeekdaySymbols[3], 4), (Calendar.current.shortWeekdaySymbols[4], 5), (Calendar.current.shortWeekdaySymbols[5], 6), (Calendar.current.shortWeekdaySymbols[6], 7)] var body: some View { VStack(spacing: 14) { HStack(spacing: 8) { ForEach(weekdays.indices, id: \.self) { dayIdx in let day = String(weekdays[dayIdx].0) let value = weekdays[dayIdx].1 let isActive = filteredDays.currentFilters.contains(value) Button(action: { if isActive { filteredDays.removeFilter(filter: value) } else { filteredDays.addFilter(newFilter: value) } let impactMed = UIImpactFeedbackGenerator(style: .medium) impactMed.impactOccurred() }) { Text(day.prefix(2).uppercased()) .font(.system(size: 13, weight: .semibold)) .foregroundColor(isActive ? .white : textColor.opacity(0.5)) .frame(maxWidth: .infinity) .frame(height: 40) .background( RoundedRectangle(cornerRadius: 10) .fill(isActive ? Color.accentColor : (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6))) ) } .buttonStyle(.plain) } } Text(String(localized: "day_picker_view_text")) .font(.system(size: 13)) .foregroundColor(textColor.opacity(0.5)) .multilineTextAlignment(.center) } } } // 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(.system(size: 28)) .foregroundColor(.green) VStack(alignment: .leading, spacing: 2) { Text("Premium Active") .font(.system(size: 16, weight: .semibold)) Text("You have full access") .font(.system(size: 13)) .foregroundColor(.secondary) } Spacer() Button("Manage") { Task { await openSubscriptionManagement() } } .font(.system(size: 14, weight: .semibold)) .foregroundColor(.green) .padding(.horizontal, 16) .padding(.vertical, 8) .background(Capsule().fill(Color.green.opacity(0.15))) } .padding(16) .background( RoundedRectangle(cornerRadius: 16) .fill(colorScheme == .dark ? Color(.systemGray6) : .white) ) } private var notSubscribedView: some View { Button(action: { EventLogger.log(event: "customize_subscribe_tapped") showSubscriptionStore = true }) { HStack(spacing: 12) { Image(systemName: "crown.fill") .font(.system(size: 28)) .foregroundColor(.orange) VStack(alignment: .leading, spacing: 2) { Text("Unlock Premium") .font(.system(size: 16, weight: .semibold)) .foregroundColor(colorScheme == .dark ? .white : .black) Text("Month & Year views, Insights & more") .font(.system(size: 13)) .foregroundColor(.secondary) } Spacer() Image(systemName: "chevron.right") .font(.system(size: 14, 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) } private func openSubscriptionManagement() async { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { do { try await AppStore.showManageSubscriptions(in: windowScene) } catch { print("Failed to open subscription management: \(error)") } } } } // MARK: - Day View Style Picker struct DayViewStylePickerCompact: View { @AppStorage(UserDefaultsStore.Keys.dayViewStyle.rawValue, store: GroupUserDefaults.groupDefaults) private var dayViewStyle: DayViewStyle = .classic @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor @Environment(\.colorScheme) private var colorScheme var body: some View { HStack(spacing: 10) { ForEach(DayViewStyle.allCases, id: \.rawValue) { style in Button(action: { withAnimation(.easeInOut(duration: 0.2)) { dayViewStyle = style } let impactMed = UIImpactFeedbackGenerator(style: .medium) impactMed.impactOccurred() EventLogger.log(event: "change_day_view_style", withData: ["style": style.displayName]) }) { VStack(spacing: 6) { styleIcon(for: style) .frame(width: 44, height: 44) .foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.4)) Text(style.displayName) .font(.system(size: 11, weight: .medium)) .foregroundColor(dayViewStyle == style ? .accentColor : textColor.opacity(0.5)) } .frame(maxWidth: .infinity) .padding(.vertical, 12) .background( RoundedRectangle(cornerRadius: 12) .fill(dayViewStyle == style ? Color.accentColor.opacity(0.1) : (colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6))) ) } .buttonStyle(.plain) } } } @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) } } } } struct CustomizeView_Previews: PreviewProvider { static var previews: some View { Group { CustomizeView() .environmentObject(IAPManager()) CustomizeView() .preferredColorScheme(.dark) .environmentObject(IAPManager()) } } }