CasinoGames/Blackjack/Blackjack/Views/Sheets/SettingsView.swift

442 lines
21 KiB
Swift

//
// SettingsView.swift
// Blackjack
//
// Game settings and rule configuration.
//
import SwiftUI
import CasinoKit
struct SettingsView: View {
@Bindable var settings: GameSettings
let gameState: GameState?
@Environment(\.dismiss) private var dismiss
@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 "Blackjack 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. Game Style (Blackjack-specific)
SheetSection(title: String(localized: "GAME STYLE"), icon: "suit.club.fill") {
GameStylePicker(selection: $settings.gameStyle)
}
// 2. Rules (Blackjack-specific, custom only)
if settings.gameStyle == .custom {
SheetSection(title: String(localized: "RULES"), icon: "list.bullet.clipboard") {
VStack(spacing: Design.Spacing.small) {
SettingsToggle(
title: String(localized: "Dealer Hits Soft 17"),
subtitle: String(localized: "H17 rule, increases house edge"),
isOn: $settings.dealerHitsSoft17,
accentColor: accent
)
Divider().background(Color.white.opacity(Design.Opacity.hint))
SettingsToggle(
title: String(localized: "Double After Split"),
subtitle: String(localized: "Allow doubling on split hands"),
isOn: $settings.doubleAfterSplit,
accentColor: accent
)
Divider().background(Color.white.opacity(Design.Opacity.hint))
SettingsToggle(
title: String(localized: "Re-split Aces"),
subtitle: String(localized: "Allow splitting aces again"),
isOn: $settings.resplitAces,
accentColor: accent
)
Divider().background(Color.white.opacity(Design.Opacity.hint))
SettingsToggle(
title: String(localized: "Late Surrender"),
subtitle: String(localized: "Surrender after dealer checks for blackjack"),
isOn: $settings.lateSurrender,
accentColor: accent
)
}
}
}
// 3. Table Limits
SheetSection(title: String(localized: "TABLE LIMITS"), icon: "banknote") {
TableLimitsPicker(selection: $settings.tableLimits)
}
// 3.5. Side Bets
SheetSection(title: String(localized: "SIDE BETS"), icon: "dollarsign.arrow.trianglehead.counterclockwise.rotate.90") {
SettingsToggle(
title: String(localized: "Enable Side Bets"),
subtitle: String(localized: "Perfect Pairs (25:1) and 21+3 (100:1)"),
isOn: $settings.sideBetsEnabled,
accentColor: accent
)
}
// 4. Deck Settings
SheetSection(title: String(localized: "DECK SETTINGS"), icon: "rectangle.portrait.on.rectangle.portrait") {
DeckCountPicker(selection: $settings.deckCount)
}
.onChange(of: settings.deckCount) { _, _ in
gameState?.applyDeckCountChange()
}
// 5. Starting Balance
SheetSection(title: String(localized: "STARTING BALANCE"), icon: "dollarsign.circle") {
BalancePicker(balance: $settings.startingBalance, accentColor: accent)
}
// 6. Display
SheetSection(title: String(localized: "DISPLAY"), icon: "eye") {
VStack(spacing: Design.Spacing.small) {
SettingsToggle(
title: String(localized: "Show Animations"),
subtitle: String(localized: "Card dealing animations"),
isOn: $settings.showAnimations,
accentColor: accent
)
if settings.showAnimations {
Divider().background(Color.white.opacity(Design.Opacity.hint))
SpeedPicker(speed: $settings.dealingSpeed, accentColor: accent)
}
Divider().background(Color.white.opacity(Design.Opacity.hint))
SettingsToggle(
title: String(localized: "Show Hints"),
subtitle: String(localized: "Basic strategy suggestions"),
isOn: $settings.showHints,
accentColor: accent
)
Divider().background(Color.white.opacity(Design.Opacity.hint))
SettingsToggle(
title: String(localized: "Card Count"),
subtitle: String(localized: "Show Hi-Lo running count & card values"),
isOn: $settings.showCardCount,
accentColor: accent
)
Divider().background(Color.white.opacity(Design.Opacity.hint))
SettingsToggle(
title: String(localized: "Cards Remaining"),
subtitle: String(localized: "Show cards left in shoe"),
isOn: $settings.showCardsRemaining,
accentColor: accent
)
}
}
// 7. Sound & Haptics
SheetSection(title: String(localized: "SOUND & HAPTICS"), icon: "speaker.wave.2") {
VStack(spacing: Design.Spacing.small) {
SettingsToggle(
title: String(localized: "Sound Effects"),
subtitle: String(localized: "Chips, cards, and results"),
isOn: $settings.soundEnabled,
accentColor: accent
)
.onChange(of: settings.soundEnabled) { _, newValue in
SoundManager.shared.soundEnabled = newValue
}
if settings.soundEnabled {
Divider().background(Color.white.opacity(Design.Opacity.hint))
VolumePicker(volume: $settings.soundVolume, accentColor: accent)
.onChange(of: settings.soundVolume) { _, newValue in
SoundManager.shared.volume = newValue
}
}
Divider().background(Color.white.opacity(Design.Opacity.hint))
SettingsToggle(
title: String(localized: "Haptic Feedback"),
subtitle: String(localized: "Vibration on actions"),
isOn: $settings.hapticsEnabled,
accentColor: accent
)
.onChange(of: settings.hapticsEnabled) { _, newValue in
SoundManager.shared.hapticsEnabled = newValue
}
}
}
// 8. Cloud Sync
if let state = gameState {
SheetSection(title: String(localized: "CLOUD SYNC"), icon: "icloud") {
if state.persistence.iCloudAvailable {
Toggle(isOn: Binding(
get: { state.persistence.iCloudEnabled },
set: { state.persistence.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)
.padding(.vertical, Design.Spacing.xSmall)
if state.persistence.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 = state.persistence.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 {
state.persistence.sync()
} label: {
HStack {
Image(systemName: "arrow.triangle.2.circlepath")
Text(String(localized: "Sync Now"))
}
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(accent)
}
}
} 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)
}
}
}
// 9. Data
if let state = gameState {
SheetSection(title: String(localized: "DATA"), icon: "externaldrive") {
HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(String(localized: "Rounds Played"))
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
Text("\(state.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 = state.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)
}
}
}
// 10. 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))
}
}
}
// 11. Reset to Defaults
Button {
settings.resetToDefaults()
} 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)
// 12. Version info
Text(appVersionString)
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.light))
.frame(maxWidth: .infinity)
.padding(.top, Design.Spacing.large)
.padding(.bottom, Design.Spacing.medium)
},
onCancel: nil,
onDone: {
settings.save()
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: - Game Style Picker (Blackjack-specific)
struct GameStylePicker: View {
@Binding var selection: BlackjackStyle
var body: some View {
VStack(spacing: Design.Spacing.small) {
ForEach(BlackjackStyle.allCases) { style in
SelectableRow(
title: style.displayName,
subtitle: style.description,
isSelected: selection == style,
accentColor: Color.Sheet.accent,
action: { selection = style }
)
}
}
}
}
// MARK: - Deck Count Picker (Blackjack-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 (Blackjack-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: nil)
}