TeraRootShell.swift (5717B)
1 import SwiftUI 2 3 enum TeraRootTab: String, CaseIterable, Sendable { 4 case today 5 case add 6 7 static func resolve(_ rawValue: String?) -> Self { 8 guard let rawValue, let tab = Self(rawValue: rawValue) else { return .today } 9 return tab 10 } 11 12 static func resolve(url: URL) -> Self? { 13 guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), 14 components.scheme?.lowercased() == "radroots", 15 components.user == nil, 16 components.password == nil, 17 components.port == nil, 18 components.query == nil, 19 components.fragment == nil, 20 components.percentEncodedPath.isEmpty, 21 let encodedHost = components.percentEncodedHost, 22 !encodedHost.contains("%") 23 else { 24 return nil 25 } 26 return Self(rawValue: encodedHost.lowercased()) 27 } 28 } 29 30 struct TeraRootShell: View { 31 let snapshot: TeraRuntimeSnapshot 32 let stores: TeraProductStores? 33 @SceneStorage("radroots.selected_root_tab") private var storedSelection = TeraRootTab.today 34 .rawValue 35 36 init( 37 snapshot: TeraRuntimeSnapshot, 38 stores: TeraProductStores? = nil 39 ) { 40 self.snapshot = snapshot 41 self.stores = stores 42 } 43 44 var body: some View { 45 TabView(selection: selection) { 46 Group { 47 if let stores { 48 TeraTodayNavigation(snapshot: snapshot, stores: stores) { 49 storedSelection = TeraRootTab.add.rawValue 50 } 51 } else { 52 NavigationStack { TeraTodayLanding(snapshot: snapshot) } 53 } 54 } 55 .tabItem { Label("Today", systemImage: "sun.max.fill") } 56 .tag(TeraRootTab.today) 57 .accessibilityIdentifier("radroots.tab.today") 58 59 NavigationStack { 60 if let stores { 61 TeraAddView(store: stores.add) 62 } else { 63 #if DEBUG 64 if ProcessInfo.processInfo.environment["TERA_IOS_UI_TEST_RECOVERY"] == "1" { 65 TeraRecoveryUITestSurface() 66 } else if ProcessInfo.processInfo.environment["TERA_IOS_UI_TEST_EDITING_PROTECTION"] == "1" { 67 TeraEditingProtectionUITestSurface() 68 } else { 69 TeraAddUnavailable() 70 } 71 #else 72 TeraAddUnavailable() 73 #endif 74 } 75 } 76 .tabItem { Label("Add", systemImage: "plus.circle.fill") } 77 .tag(TeraRootTab.add) 78 .accessibilityIdentifier("radroots.tab.add") 79 } 80 .tint(.primary) 81 .toolbarBackground(Color(uiColor: .systemBackground), for: .tabBar) 82 .toolbarBackground(.visible, for: .tabBar) 83 .accessibilityIdentifier("radroots.root.tabs") 84 .onOpenURL { url in 85 guard let tab = TeraRootTab.resolve(url: url) else { return } 86 storedSelection = tab.rawValue 87 } 88 } 89 90 private var selection: Binding<TeraRootTab> { 91 Binding( 92 get: { TeraRootTab.resolve(storedSelection) }, 93 set: { storedSelection = $0.rawValue } 94 ) 95 } 96 } 97 98 private struct TeraTodayLanding: View { 99 let snapshot: TeraRuntimeSnapshot 100 @State private var showsAccount = false 101 @State private var showsSearch = false 102 103 var body: some View { 104 ContentUnavailableView { 105 Label("Today", systemImage: "leaf.fill") 106 } description: { 107 Text("Your local food network is ready for its first refresh.") 108 } 109 .navigationTitle("Today") 110 .toolbar { 111 ToolbarItem(placement: .topBarTrailing) { 112 Button { 113 showsSearch = true 114 } label: { 115 Label("Search", systemImage: "magnifyingglass") 116 } 117 .accessibilityIdentifier("radroots.support.search") 118 } 119 ToolbarItem(placement: .topBarTrailing) { 120 Button { 121 showsAccount = true 122 } label: { 123 Label("Account", systemImage: "person.crop.circle") 124 } 125 .accessibilityIdentifier("radroots.support.account") 126 } 127 } 128 .sheet(isPresented: $showsAccount) { 129 TeraAccountUnavailableSheet(snapshot: snapshot) 130 } 131 .sheet(isPresented: $showsSearch) { 132 TeraSearchUnavailableSheet() 133 } 134 .accessibilityIdentifier("radroots.today.root") 135 } 136 } 137 138 struct TeraSearchUnavailableSheet: View { 139 @Environment(\.dismiss) private var dismiss 140 141 var body: some View { 142 NavigationStack { 143 ContentUnavailableView.search 144 .navigationTitle("Search") 145 .toolbar { 146 ToolbarItem(placement: .confirmationAction) { 147 Button("Done") { dismiss() } 148 } 149 } 150 } 151 .accessibilityIdentifier("radroots.support.search.sheet") 152 } 153 } 154 155 struct TeraAccountUnavailableSheet: View { 156 let snapshot: TeraRuntimeSnapshot 157 @Environment(\.dismiss) private var dismiss 158 159 var body: some View { 160 NavigationStack { 161 List { 162 Section("Me") { 163 LabeledContent("Public key", value: abbreviatedPublicKey) 164 } 165 Section("Connection") { 166 LabeledContent("Runtime", value: snapshot.crateVersion) 167 LabeledContent("Relay", value: snapshot.relay?.state ?? "Not configured") 168 } 169 } 170 .navigationTitle("Account") 171 .toolbar { 172 ToolbarItem(placement: .confirmationAction) { 173 Button("Done") { dismiss() } 174 } 175 } 176 } 177 .presentationDetents([.medium, .large]) 178 .accessibilityIdentifier("radroots.support.account.sheet") 179 } 180 181 private var abbreviatedPublicKey: String { 182 let key = snapshot.identity.publicKeyHex 183 guard key.count > 16 else { return key } 184 return "\(key.prefix(8))…\(key.suffix(8))" 185 } 186 } 187 188 private struct TeraAddUnavailable: View { 189 var body: some View { 190 ContentUnavailableView { 191 Label("Add", systemImage: "plus.circle.fill") 192 } description: { 193 Text("Choose what to share with your local food network.") 194 } 195 .navigationTitle("Add") 196 .accessibilityIdentifier("radroots.add.root") 197 } 198 }