NewGameSheet.swift (4160B)
1 import SwiftUI 2 3 struct NewGameSheet: View { 4 let store: GameStore 5 var onCreated: (UUID) -> Void = { _ in } 6 7 @Environment(\.dismiss) private var dismiss 8 @Environment(NYTAuthService.self) private var nytAuth 9 @AppStorage("lastPuzzleSource") private var storedSourceRaw = PuzzleSource.bundles.rawValue 10 @AppStorage("debugMode") private var debugMode = false 11 @State private var selection: PuzzleSource = .bundles 12 @State private var duplicateSource: String? 13 @State private var createError: String? 14 15 private var availableSources: [PuzzleSource] { 16 var sources: [PuzzleSource] = [.bundles] 17 if debugMode { 18 sources.append(.debug) 19 } 20 sources.append(.imported) 21 if nytAuth.canAttemptNYTFetch { 22 sources.append(.nyt) 23 } 24 return sources 25 } 26 27 var body: some View { 28 NavigationStack { 29 VStack(spacing: 0) { 30 Picker("Source", selection: $selection) { 31 ForEach(availableSources) { source in 32 Text(source.title).tag(source) 33 } 34 } 35 .pickerStyle(.segmented) 36 .padding() 37 38 Group { 39 switch selection { 40 case .bundles: 41 BundledBrowseView(onSelected: handleSelected) 42 case .debug: 43 DebugBrowseView(onSelected: handleSelected) 44 case .imported: 45 ImportedBrowseView(onSelected: handleSelected) 46 case .nyt: 47 NYTBrowseView( 48 onSelected: handleSelected, 49 excludedDates: store.nytPuzzleDatesInLibrary() 50 ) 51 } 52 } 53 } 54 .navigationTitle("New Puzzle") 55 .navigationBarTitleDisplayMode(.inline) 56 .toolbar { 57 ToolbarItem(placement: .cancellationAction) { 58 Button { 59 dismiss() 60 } label: { 61 Image(systemName: "xmark") 62 } 63 .accessibilityLabel("Cancel") 64 } 65 } 66 } 67 .onAppear { 68 // Fall back to .bundles if the stored raw value is missing or no 69 // longer maps to a case (e.g. the renamed "bundled" -> "bundles"). 70 let stored = PuzzleSource(rawValue: storedSourceRaw) ?? .bundles 71 selection = availableSources.contains(stored) ? stored : .bundles 72 } 73 .onChange(of: selection) { _, newValue in 74 storedSourceRaw = newValue.rawValue 75 } 76 .alert( 77 "Puzzle Already in Library", 78 isPresented: .init( 79 get: { duplicateSource != nil }, 80 set: { if !$0 { duplicateSource = nil } } 81 ), 82 presenting: duplicateSource 83 ) { source in 84 Button("Create Copy") { 85 create(from: source) 86 } 87 Button("Cancel", role: .cancel) {} 88 } message: { _ in 89 Text("You already have a game for this puzzle. Cancel and resume it from the library, or create a new copy.") 90 } 91 .alert( 92 "Couldn't Create Game", 93 isPresented: .init( 94 get: { createError != nil }, 95 set: { if !$0 { createError = nil } } 96 ), 97 presenting: createError 98 ) { _ in 99 Button("OK", role: .cancel) {} 100 } message: { message in 101 Text(message) 102 } 103 } 104 105 private func handleSelected(_ source: String) { 106 if store.findGameID(matching: source) != nil { 107 duplicateSource = source 108 } else { 109 create(from: source) 110 } 111 } 112 113 private func create(from source: String) { 114 do { 115 let gameID = try store.createGame(from: source) 116 dismiss() 117 onCreated(gameID) 118 } catch { 119 createError = error.localizedDescription 120 } 121 } 122 }