445 lines
21 KiB
Swift
445 lines
21 KiB
Swift
//
|
|
// 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
|
|
@State private var showPrivacyPolicy = 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))"
|
|
}
|
|
|
|
/// Accent color for settings components
|
|
private let accent = Color.Sheet.accent
|
|
|
|
var body: some View {
|
|
SheetContainerView(
|
|
title: String(localized: "Settings"),
|
|
content: {
|
|
// 1. Table Limits
|
|
SheetSection(title: String(localized: "TABLE LIMITS"), icon: "banknote") {
|
|
TableLimitsPicker(selection: $settings.tableLimits)
|
|
.onChange(of: settings.tableLimits) { _, _ in
|
|
hasChanges = true
|
|
}
|
|
}
|
|
|
|
// 2. Deck Settings
|
|
SheetSection(title: String(localized: "DECK SETTINGS"), icon: "rectangle.portrait.on.rectangle.portrait") {
|
|
DeckCountPicker(selection: $settings.deckCount)
|
|
.onChange(of: settings.deckCount) { _, _ in
|
|
hasChanges = true
|
|
}
|
|
}
|
|
|
|
// 3. Starting Balance
|
|
SheetSection(title: String(localized: "STARTING BALANCE"), icon: "dollarsign.circle") {
|
|
BalancePicker(balance: $settings.startingBalance, accentColor: accent)
|
|
.onChange(of: settings.startingBalance) { _, _ in
|
|
hasChanges = true
|
|
}
|
|
}
|
|
|
|
// 4. Display (includes animations)
|
|
SheetSection(title: String(localized: "DISPLAY"), icon: "eye") {
|
|
SettingsToggle(
|
|
title: String(localized: "Card Animations"),
|
|
subtitle: String(localized: "Animate dealing and flipping"),
|
|
isOn: $settings.showAnimations,
|
|
accentColor: accent
|
|
)
|
|
|
|
if settings.showAnimations {
|
|
Divider()
|
|
.background(Color.white.opacity(Design.Opacity.subtle))
|
|
|
|
SpeedPicker(speed: $settings.dealingSpeed, accentColor: accent)
|
|
}
|
|
|
|
Divider()
|
|
.background(Color.white.opacity(Design.Opacity.subtle))
|
|
|
|
SettingsToggle(
|
|
title: String(localized: "Show Cards Remaining"),
|
|
subtitle: String(localized: "Display deck counter at top"),
|
|
isOn: $settings.showCardsRemaining,
|
|
accentColor: accent
|
|
)
|
|
|
|
Divider()
|
|
.background(Color.white.opacity(Design.Opacity.subtle))
|
|
|
|
SettingsToggle(
|
|
title: String(localized: "Show History"),
|
|
subtitle: String(localized: "Display result road map"),
|
|
isOn: $settings.showHistory,
|
|
accentColor: accent
|
|
)
|
|
|
|
Divider()
|
|
.background(Color.white.opacity(Design.Opacity.subtle))
|
|
|
|
SettingsToggle(
|
|
title: String(localized: "Show Hints"),
|
|
subtitle: String(localized: "Betting tips and trend analysis"),
|
|
isOn: $settings.showHints,
|
|
accentColor: accent
|
|
)
|
|
}
|
|
|
|
// 5. Sound & Haptics
|
|
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,
|
|
accentColor: accent
|
|
)
|
|
.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, accentColor: accent)
|
|
.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,
|
|
accentColor: accent
|
|
)
|
|
.onChange(of: settings.hapticsEnabled) { _, _ in
|
|
hasChanges = true
|
|
}
|
|
}
|
|
|
|
// 6. Cloud Sync
|
|
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(accent)
|
|
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
|
|
|
|
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))
|
|
}
|
|
}
|
|
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
|
|
|
|
Divider()
|
|
.background(Color.white.opacity(Design.Opacity.subtle))
|
|
|
|
Button {
|
|
gameState.syncWithCloud()
|
|
} label: {
|
|
HStack {
|
|
Text(String(localized: "Sync Now"))
|
|
Spacer()
|
|
Image(systemName: "arrow.triangle.2.circlepath")
|
|
}
|
|
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
|
|
.foregroundStyle(accent)
|
|
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
|
|
}
|
|
}
|
|
} 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))
|
|
}
|
|
}
|
|
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
|
|
}
|
|
}
|
|
|
|
// 7. Data
|
|
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.aggregatedStats.totalRoundsPlayed)")
|
|
.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.aggregatedStats.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))
|
|
|
|
// Reset Game - resets balance, keeps stats
|
|
Button {
|
|
gameState.resetGame()
|
|
dismiss()
|
|
} label: {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
|
Text(String(localized: "Reset Game"))
|
|
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
|
|
.foregroundStyle(.white)
|
|
Text(String(localized: "Restore starting balance and reshuffle"))
|
|
.font(.system(size: Design.BaseFontSize.small))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
|
}
|
|
Spacer()
|
|
Image(systemName: "arrow.counterclockwise")
|
|
.font(.system(size: Design.BaseFontSize.large))
|
|
.foregroundStyle(Color.Sheet.accent)
|
|
}
|
|
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
|
|
}
|
|
|
|
Divider()
|
|
.background(Color.white.opacity(Design.Opacity.subtle))
|
|
|
|
// Clear All Data - nuclear option
|
|
Button(role: .destructive) {
|
|
showClearDataAlert = true
|
|
} label: {
|
|
HStack {
|
|
Text(String(localized: "Clear All Data"))
|
|
Spacer()
|
|
Image(systemName: "trash")
|
|
}
|
|
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
|
|
.foregroundStyle(.red)
|
|
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
|
|
}
|
|
}
|
|
|
|
// 8. Legal
|
|
SheetSection(title: String(localized: "LEGAL"), icon: "doc.text") {
|
|
Button {
|
|
showPrivacyPolicy = true
|
|
} label: {
|
|
HStack {
|
|
Text(String(localized: "Privacy Policy"))
|
|
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
|
|
.foregroundStyle(.white)
|
|
Spacer()
|
|
Image(systemName: "chevron.right")
|
|
.font(.system(size: Design.BaseFontSize.body))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
|
}
|
|
.frame(minHeight: CasinoDesign.Size.actionRowMinHeight)
|
|
}
|
|
}
|
|
|
|
// 9. Reset to Defaults
|
|
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)
|
|
|
|
// 10. Show Welcome Again (Reset Onboarding)
|
|
Button {
|
|
gameState.onboarding.reset()
|
|
dismiss()
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "hand.wave")
|
|
Text(String(localized: "Show Welcome Again"))
|
|
}
|
|
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
|
.padding()
|
|
.frame(maxWidth: .infinity)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
|
.fill(Color.white.opacity(Design.Opacity.subtle))
|
|
)
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.top, Design.Spacing.xSmall)
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// DEBUG SECTION - Only visible in DEBUG builds
|
|
// ─────────────────────────────────────────────────────────────
|
|
#if DEBUG
|
|
SheetSection(title: "DEBUG", icon: "ant.fill") {
|
|
BrandingDebugRows(
|
|
iconConfig: .baccarat,
|
|
launchConfig: .baccarat,
|
|
appName: "Baccarat"
|
|
)
|
|
}
|
|
#endif
|
|
|
|
// 11. 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: nil,
|
|
onDone: {
|
|
settings.save()
|
|
if hasChanges {
|
|
onApplyChanges()
|
|
}
|
|
dismiss()
|
|
},
|
|
doneButtonText: String(localized: "Done")
|
|
)
|
|
.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."))
|
|
}
|
|
.sheet(isPresented: $showPrivacyPolicy) {
|
|
PrivacyPolicyView(
|
|
developerName: "Your Name", // TODO: Replace with your name/company
|
|
contactEmail: "your@email.com" // TODO: Replace with your email
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Deck Count Picker (Baccarat-specific)
|
|
|
|
struct DeckCountPicker: View {
|
|
@Binding var selection: DeckCount
|
|
|
|
var body: some View {
|
|
VStack(spacing: Design.Spacing.medium) {
|
|
ForEach(DeckCount.allCases) { count in
|
|
SelectableRow(
|
|
title: count.displayName,
|
|
subtitle: count.description,
|
|
isSelected: selection == count,
|
|
accentColor: Color.Sheet.accent,
|
|
action: { selection = count }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Table Limits Picker (Baccarat-specific)
|
|
|
|
struct TableLimitsPicker: View {
|
|
@Binding var selection: TableLimits
|
|
|
|
var body: some View {
|
|
VStack(spacing: Design.Spacing.small) {
|
|
ForEach(TableLimits.allCases) { limit in
|
|
SelectableRow(
|
|
title: limit.displayName,
|
|
subtitle: limit.detailedDescription,
|
|
isSelected: selection == limit,
|
|
accentColor: Color.Sheet.accent,
|
|
badge: { BadgePill(text: limit.description, isSelected: selection == limit, accentColor: Color.Sheet.accent) },
|
|
action: { selection = limit }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
SettingsView(settings: GameSettings(), gameState: GameState()) { }
|
|
}
|