field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

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 }