// // SettingsView.swift // Baccarat // // Settings screen for game customization. // import SwiftUI import CasinoKit /// The settings screen for customizing game options. struct SettingsView: View { @Bindable var settings: GameSettings let gameState: GameState @Environment(\.dismiss) private var dismiss let onApplyChanges: () -> Void @State private var hasChanges = false @State private var showClearDataAlert = false /// App version string from bundle info private var appVersionString: String { let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0" let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1" return "Baccarat v\(version) (\(build))" } var body: some View { SheetContainerView( title: String(localized: "Settings"), content: { // Table Limits Section (First!) SheetSection(title: String(localized: "TABLE LIMITS"), icon: "banknote") { TableLimitsPicker(selection: $settings.tableLimits) .onChange(of: settings.tableLimits) { _, _ in hasChanges = true } } // Deck Settings Section SheetSection(title: String(localized: "DECK SETTINGS"), icon: "rectangle.portrait.on.rectangle.portrait") { DeckCountPicker(selection: $settings.deckCount) .onChange(of: settings.deckCount) { _, _ in hasChanges = true } } // Starting Balance Section SheetSection(title: String(localized: "STARTING BALANCE"), icon: "dollarsign.circle") { BalancePicker(balance: $settings.startingBalance) .onChange(of: settings.startingBalance) { _, _ in hasChanges = true } } // Display Settings Section SheetSection(title: String(localized: "DISPLAY"), icon: "eye") { SettingsToggle( title: String(localized: "Show Cards Remaining"), subtitle: String(localized: "Display deck counter at top"), isOn: $settings.showCardsRemaining ) Divider() .background(Color.white.opacity(Design.Opacity.subtle)) SettingsToggle( title: String(localized: "Show History"), subtitle: String(localized: "Display result road map"), isOn: $settings.showHistory ) } // Animation Settings Section SheetSection(title: String(localized: "ANIMATIONS"), icon: "sparkles") { SettingsToggle( title: String(localized: "Card Animations"), subtitle: String(localized: "Animate dealing and flipping"), isOn: $settings.showAnimations ) if settings.showAnimations { Divider() .background(Color.white.opacity(Design.Opacity.subtle)) SpeedPicker(speed: $settings.dealingSpeed) } } // Sound & Haptics Section SheetSection(title: String(localized: "SOUND & HAPTICS"), icon: "speaker.wave.2") { SettingsToggle( title: String(localized: "Sound Effects"), subtitle: String(localized: "Chips, cards, and result sounds"), isOn: $settings.soundEnabled ) .onChange(of: settings.soundEnabled) { _, newValue in SoundManager.shared.soundEnabled = newValue hasChanges = true } if settings.soundEnabled { Divider() .background(Color.white.opacity(Design.Opacity.subtle)) VolumePicker(volume: $settings.soundVolume) .onChange(of: settings.soundVolume) { _, newValue in SoundManager.shared.volume = newValue hasChanges = true } } Divider() .background(Color.white.opacity(Design.Opacity.subtle)) SettingsToggle( title: String(localized: "Haptic Feedback"), subtitle: String(localized: "Vibration for actions and results"), isOn: $settings.hapticsEnabled ) .onChange(of: settings.hapticsEnabled) { _, _ in hasChanges = true } } // iCloud Sync Section SheetSection(title: String(localized: "CLOUD SYNC"), icon: "icloud") { if gameState.iCloudAvailable { Toggle(isOn: Binding( get: { gameState.iCloudEnabled }, set: { gameState.iCloudEnabled = $0 } )) { VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { Text(String(localized: "iCloud Sync")) .font(.system(size: Design.BaseFontSize.subheadline, weight: .medium)) .foregroundStyle(.white) Text(String(localized: "Sync progress across devices")) .font(.system(size: Design.BaseFontSize.body)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) } } .tint(.yellow) .padding(.vertical, Design.Spacing.xSmall) if gameState.iCloudEnabled { Divider() .background(Color.white.opacity(Design.Opacity.subtle)) HStack { Text(String(localized: "Last Synced")) .font(.system(size: Design.BaseFontSize.body)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) Spacer() if let lastSync = gameState.lastSyncDate { Text(lastSync, style: .relative) .font(.system(size: Design.BaseFontSize.body)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) } else { Text(String(localized: "Never")) .font(.system(size: Design.BaseFontSize.body)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) } } Divider() .background(Color.white.opacity(Design.Opacity.subtle)) Button { gameState.syncWithCloud() } label: { HStack { Image(systemName: "arrow.triangle.2.circlepath") Text(String(localized: "Sync Now")) } .font(.system(size: Design.BaseFontSize.body, weight: .medium)) .foregroundStyle(.yellow) } } } else { HStack { Image(systemName: "icloud.slash") .foregroundStyle(.white.opacity(Design.Opacity.medium)) VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { Text(String(localized: "iCloud Unavailable")) .font(.system(size: Design.BaseFontSize.subheadline, weight: .medium)) .foregroundStyle(.white) Text(String(localized: "Sign in to iCloud to sync progress")) .font(.system(size: Design.BaseFontSize.body)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) } } .padding(.vertical, Design.Spacing.xSmall) } } // Data Section SheetSection(title: String(localized: "DATA"), icon: "externaldrive") { HStack { VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { Text(String(localized: "Lifetime Rounds")) .font(.system(size: Design.BaseFontSize.body)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) Text("\(gameState.lifetimeStats.roundsPlayed)") .font(.system(size: Design.BaseFontSize.xxLarge, weight: .bold, design: .rounded)) .foregroundStyle(.white) } Spacer() VStack(alignment: .trailing, spacing: Design.Spacing.xxSmall) { Text(String(localized: "Total Winnings")) .font(.system(size: Design.BaseFontSize.body)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) let winnings = gameState.lifetimeStats.totalWinnings Text(winnings >= 0 ? "+$\(winnings)" : "-$\(abs(winnings))") .font(.system(size: Design.BaseFontSize.xxLarge, weight: .bold, design: .rounded)) .foregroundStyle(winnings >= 0 ? .green : .red) } } Divider() .background(Color.white.opacity(Design.Opacity.subtle)) Button(role: .destructive) { showClearDataAlert = true } label: { HStack { Image(systemName: "trash") Text(String(localized: "Clear All Data")) } .font(.system(size: Design.BaseFontSize.body, weight: .medium)) .foregroundStyle(.red) } } // Reset Button Button { settings.resetToDefaults() hasChanges = true } label: { HStack { Image(systemName: "arrow.counterclockwise") Text(String(localized: "Reset to Defaults")) } .font(.system(size: Design.BaseFontSize.medium, weight: .medium)) .foregroundStyle(.red.opacity(Design.Opacity.heavy)) .padding() .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: Design.CornerRadius.large) .fill(Color.red.opacity(Design.Opacity.subtle)) ) } .padding(.horizontal) .padding(.top, Design.Spacing.small) // App Version Text(appVersionString) .font(.system(size: Design.BaseFontSize.callout)) .foregroundStyle(.white.opacity(Design.Opacity.light)) .frame(maxWidth: .infinity) .padding(.top, Design.Spacing.large) .padding(.bottom, Design.Spacing.medium) }, onCancel: { settings.load() // Revert changes dismiss() }, onDone: { settings.save() if hasChanges { onApplyChanges() } dismiss() }, doneButtonText: String(localized: "Done"), cancelButtonText: String(localized: "Cancel") ) .alert(String(localized: "Clear All Data?"), isPresented: $showClearDataAlert) { Button(String(localized: "Cancel"), role: .cancel) { } Button(String(localized: "Clear"), role: .destructive) { gameState.clearAllData() } } message: { Text(String(localized: "This will delete all saved progress, statistics, and reset your balance. This cannot be undone.")) } } } /// Deck count picker with visual options. struct DeckCountPicker: View { @Binding var selection: DeckCount var body: some View { VStack(spacing: Design.Spacing.medium) { ForEach(DeckCount.allCases) { count in Button { selection = count } label: { HStack { VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { Text(count.displayName) .font(.system(size: Design.BaseFontSize.large, weight: .semibold)) .foregroundStyle(.white) Text(count.description) .font(.system(size: Design.BaseFontSize.body)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) } Spacer() if selection == count { Image(systemName: "checkmark.circle.fill") .font(.system(size: Design.Size.checkmark)) .foregroundStyle(.yellow) } else { Circle() .strokeBorder(Color.white.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.medium) .frame(width: Design.Size.checkmark, height: Design.Size.checkmark) } } .padding() .background( RoundedRectangle(cornerRadius: Design.CornerRadius.medium) .fill(selection == count ? Color.yellow.opacity(Design.Opacity.subtle) : Color.clear) ) .overlay( RoundedRectangle(cornerRadius: Design.CornerRadius.medium) .strokeBorder( selection == count ? Color.yellow.opacity(Design.Opacity.medium) : Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin ) ) } .buttonStyle(.plain) } } } } /// Starting balance picker. struct BalancePicker: View { @Binding var balance: Int private let options = [1_000, 5_000, 10_000, 25_000, 50_000, 100_000] var body: some View { LazyVGrid(columns: [ GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()) ], spacing: Design.Spacing.small) { ForEach(options, id: \.self) { amount in Button { balance = amount } label: { Text("$\(amount / 1000)K") .font(.system(size: Design.BaseFontSize.medium, weight: .bold)) .foregroundStyle(balance == amount ? .black : .white) .padding(.vertical, Design.Spacing.medium) .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: Design.CornerRadius.small) .fill(balance == amount ? Color.yellow : Color.white.opacity(Design.Opacity.subtle)) ) } .buttonStyle(.plain) } } } } /// A toggle setting row. struct SettingsToggle: View { let title: String let subtitle: String @Binding var isOn: Bool var body: some View { Toggle(isOn: $isOn) { VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { Text(title) .font(.system(size: Design.BaseFontSize.subheadline, weight: .medium)) .foregroundStyle(.white) Text(subtitle) .font(.system(size: Design.BaseFontSize.body)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) } } .tint(.yellow) .padding(.vertical, Design.Spacing.xSmall) } } /// Animation speed picker. struct SpeedPicker: View { @Binding var speed: Double private let options: [(String, Double)] = [ ("Fast", 0.5), ("Normal", 1.0), ("Slow", 2.0) ] var body: some View { VStack(alignment: .leading, spacing: Design.Spacing.small) { Text(String(localized: "Dealing Speed")) .font(.system(size: Design.BaseFontSize.subheadline, weight: .medium)) .foregroundStyle(.white) HStack(spacing: Design.Spacing.small) { ForEach(options, id: \.1) { option in Button { speed = option.1 } label: { Text(option.0) .font(.system(size: Design.BaseFontSize.callout, weight: .medium)) .foregroundStyle(speed == option.1 ? .black : .white.opacity(Design.Opacity.strong)) .padding(.vertical, Design.Spacing.small) .frame(maxWidth: .infinity) .background( Capsule() .fill(speed == option.1 ? Color.yellow : Color.white.opacity(Design.Opacity.subtle)) ) } .buttonStyle(.plain) } } } .padding(.vertical, Design.Spacing.xSmall) } } /// Table limits picker for min/max bets. struct TableLimitsPicker: View { @Binding var selection: TableLimits var body: some View { VStack(spacing: Design.Spacing.small) { ForEach(TableLimits.allCases) { limit in Button { selection = limit } label: { HStack { VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { Text(limit.displayName) .font(.system(size: Design.BaseFontSize.large, weight: .semibold)) .foregroundStyle(.white) Text(limit.detailedDescription) .font(.system(size: Design.BaseFontSize.body)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) } Spacer() // Limits badge Text(limit.description) .font(.system(size: Design.BaseFontSize.body, weight: .bold, design: .rounded)) .foregroundStyle(selection == limit ? .black : .yellow) .padding(.horizontal, Design.Spacing.small) .padding(.vertical, Design.Spacing.xSmall) .background( Capsule() .fill(selection == limit ? Color.yellow : Color.yellow.opacity(Design.Opacity.hint)) ) if selection == limit { Image(systemName: "checkmark.circle.fill") .font(.system(size: Design.Size.checkmark - 2)) .foregroundStyle(.yellow) } else { Circle() .strokeBorder(Color.white.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.medium) .frame(width: Design.Size.checkmark - 2, height: Design.Size.checkmark - 2) } } .padding() .background( RoundedRectangle(cornerRadius: Design.CornerRadius.medium) .fill(selection == limit ? Color.yellow.opacity(Design.Opacity.subtle) : Color.clear) ) .overlay( RoundedRectangle(cornerRadius: Design.CornerRadius.medium) .strokeBorder( selection == limit ? Color.yellow.opacity(Design.Opacity.medium) : Color.white.opacity(Design.Opacity.subtle), lineWidth: Design.LineWidth.thin ) ) } .buttonStyle(.plain) } } } } /// Volume slider for sound effects. struct VolumePicker: View { @Binding var volume: Float var body: some View { VStack(alignment: .leading, spacing: Design.Spacing.small) { HStack { Text(String(localized: "Volume")) .font(.system(size: Design.BaseFontSize.subheadline, weight: .medium)) .foregroundStyle(.white) Spacer() Text("\(Int(volume * 100))%") .font(.system(size: Design.BaseFontSize.body, weight: .medium, design: .rounded)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) } HStack(spacing: Design.Spacing.medium) { Image(systemName: "speaker.fill") .font(.system(size: Design.BaseFontSize.body)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) Slider(value: $volume, in: 0...1, step: 0.1) .tint(.yellow) Image(systemName: "speaker.wave.3.fill") .font(.system(size: Design.BaseFontSize.body)) .foregroundStyle(.white.opacity(Design.Opacity.medium)) } } .padding(.vertical, Design.Spacing.xSmall) } } #Preview { SettingsView(settings: GameSettings(), gameState: GameState()) { } }