field_ios

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

RelaysView.swift (7062B)


      1 import RadrootsKit
      2 import SwiftUI
      3 
      4 struct RelaysView: View {
      5     @EnvironmentObject private var app: AppState
      6     @State private var preparedExport: RadrootsPreparedExportDocument?
      7     @State private var activeExport: RadrootsPreparedExportDocument?
      8     @State private var importRequest: RadrootsDocumentImportRequest?
      9     @State private var fileAccess: RadrootsAppleFileAccess?
     10     @State private var importedRelays: [String] = []
     11     @State private var documentMessage: String?
     12     @State private var documentError: String?
     13 
     14     private var configuredRelays: [String] {
     15         app.configuredRelayURLs
     16     }
     17 
     18     var body: some View {
     19         List {
     20             Section("Relays") {
     21                 RelayMetricRow(
     22                     label: "Connected",
     23                     systemImage: "dot.radiowaves.left.and.right",
     24                     value: app.relayConnectedCount,
     25                     accessibilityID: "field_ios.relays.connected_count"
     26                 )
     27                 RelayMetricRow(
     28                     label: "Connecting",
     29                     systemImage: "antenna.radiowaves.left.and.right",
     30                     value: app.relayConnectingCount,
     31                     accessibilityID: "field_ios.relays.connecting_count"
     32                 )
     33                 if let last = app.relayLastError {
     34                     Text(last)
     35                         .foregroundStyle(.red)
     36                         .font(.footnote)
     37                         .accessibilityIdentifier("field_ios.relays.last_error")
     38                 }
     39             }
     40 
     41             Section("Configured Relays") {
     42                 LabeledContent("Source", value: app.relaySettingsSourceLabel)
     43                     .accessibilityIdentifier("field_ios.relays.settings_source")
     44                 if configuredRelays.isEmpty {
     45                     Text("No relays configured")
     46                         .foregroundStyle(.secondary)
     47                 } else {
     48                     ForEach(configuredRelays, id: \.self) { url in
     49                         Text(url)
     50                             .font(.callout.monospaced())
     51                             .accessibilityIdentifier("field_ios.relays.configured_url")
     52                     }
     53                 }
     54             }
     55 
     56             Section("Document Interchange") {
     57                 Button {
     58                     prepareRelayExport()
     59                 } label: {
     60                     Label("Export Relay Config", systemImage: "square.and.arrow.up")
     61                 }
     62                 .accessibilityIdentifier("field_ios.relays.export")
     63 
     64                 Button {
     65                     prepareRelayImport()
     66                 } label: {
     67                     Label("Import Relay Config", systemImage: "square.and.arrow.down")
     68                 }
     69                 .accessibilityIdentifier("field_ios.relays.import")
     70 
     71                 if let documentMessage {
     72                     Text(documentMessage)
     73                         .foregroundStyle(.secondary)
     74                         .accessibilityIdentifier("field_ios.relays.document_status")
     75                 }
     76                 if let documentError {
     77                     Text(documentError)
     78                         .foregroundStyle(.red)
     79                         .font(.footnote)
     80                         .accessibilityIdentifier("field_ios.relays.document_error")
     81                 }
     82             }
     83 
     84             if !importedRelays.isEmpty {
     85                 Section("Imported Relays") {
     86                     ForEach(importedRelays, id: \.self) { url in
     87                         Text(url)
     88                             .font(.callout.monospaced())
     89                             .accessibilityIdentifier("field_ios.relays.imported_url")
     90                     }
     91                 }
     92             }
     93         }
     94         .listStyle(.insetGrouped)
     95         .inlineNavigationTitle("Relays")
     96         .task {
     97             fileAccess = try? app.documentFileAccess()
     98         }
     99         .radrootsDocumentExporter(preparedExport: $preparedExport) { result in
    100             handleRelayExportCompletion(result)
    101         }
    102         .background {
    103             if let fileAccess {
    104                 Color.clear.radrootsDocumentImporter(
    105                     request: $importRequest,
    106                     fileAccess: fileAccess
    107                 ) { result in
    108                     handleRelayImportCompletion(result)
    109                 }
    110             }
    111         }
    112         .accessibilityIdentifier("field_ios.relays")
    113     }
    114 
    115     private func prepareRelayExport() {
    116         documentMessage = nil
    117         documentError = nil
    118         do {
    119             let export = try app.prepareRelayConfigDocumentExport()
    120             activeExport = export
    121             preparedExport = export
    122         } catch {
    123             documentError = error.fieldRuntimeMessage
    124         }
    125     }
    126 
    127     private func prepareRelayImport() {
    128         documentMessage = nil
    129         documentError = nil
    130         do {
    131             if fileAccess == nil {
    132                 fileAccess = try app.documentFileAccess()
    133             }
    134             importRequest = try RadrootsDocumentImportRequest(
    135                 allowedContentKinds: [.json],
    136                 allowsMultipleSelection: false,
    137                 destinationScope: .temporary
    138             )
    139         } catch {
    140             documentError = error.fieldRuntimeMessage
    141         }
    142     }
    143 
    144     private func handleRelayExportCompletion(_ result: Result<RadrootsExportDocumentResult, Error>) {
    145         if let activeExport {
    146             app.releasePreparedDocumentExport(activeExport)
    147         }
    148         activeExport = nil
    149         switch result {
    150         case .success(let exportResult):
    151             documentMessage = "Exported \(exportResult.exportedFilename)"
    152             documentError = nil
    153         case .failure(let error):
    154             documentError = error.fieldRuntimeMessage
    155         }
    156     }
    157 
    158     private func handleRelayImportCompletion(_ result: Result<RadrootsDocumentImportResult, Error>) {
    159         Task {
    160             await applyRelayImportCompletion(result)
    161         }
    162     }
    163 
    164     @MainActor
    165     private func applyRelayImportCompletion(_ result: Result<RadrootsDocumentImportResult, Error>) async {
    166         do {
    167             let importResult = try result.get()
    168             guard let document = importResult.documents.first else {
    169                 throw FieldDocumentInterchangeError.invalidRelayConfigDocument
    170             }
    171             importedRelays = try await app.applyImportedRelayConfig(from: document)
    172             documentMessage = "Imported and applied \(importedRelays.count) relay config entries"
    173             documentError = nil
    174         } catch {
    175             documentError = error.fieldRuntimeMessage
    176         }
    177     }
    178 }
    179 
    180 private struct RelayMetricRow: View {
    181     let label: String
    182     let systemImage: String
    183     let value: UInt32
    184     let accessibilityID: String
    185 
    186     var body: some View {
    187         HStack {
    188             Label(label, systemImage: systemImage)
    189             Spacer()
    190             Text("\(value)")
    191         }
    192         .accessibilityElement(children: .ignore)
    193         .accessibilityLabel(label)
    194         .accessibilityValue("\(value)")
    195         .accessibilityIdentifier(accessibilityID)
    196     }
    197 }