field_ios

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

TeraDraftsSheet.swift (6107B)


      1 import SwiftUI
      2 
      3 struct TeraDraftsSheet: View {
      4   @ObservedObject var store: TeraAddStore
      5   @ObservedObject private var recovery: TeraDraftRecoveryStore
      6   @ObservedObject private var protection: TeraEditingProtection
      7   @Environment(\.dismiss) private var dismiss
      8 
      9   init(store: TeraAddStore) {
     10     self.store = store
     11     recovery = store.recovery
     12     protection = store.protection
     13   }
     14 
     15   var body: some View {
     16     NavigationStack {
     17       List {
     18         TeraRestoreRecoverySection(store: recovery.restore)
     19         if recovery.isLoading {
     20           ProgressView("Loading saved work…")
     21         }
     22         if let message = store.message {
     23           Text(message)
     24         }
     25         TeraEditingProtectionActions(protection: protection)
     26         composerSection
     27         TeraSubmissionInventoryView(store: store.submissions)
     28         TeraNativeRepairView(store: recovery.transfers)
     29         legacySection
     30         Section {
     31           Button("Reload from first page") { recovery.start() }
     32             .disabled(recovery.isLoading)
     33           Text("Each page shows saved work on this device. Reload to include newly saved drafts.")
     34             .font(.footnote)
     35         }
     36       }
     37       .navigationTitle("Drafts & outbox")
     38       .toolbar {
     39         ToolbarItem(placement: .confirmationAction) { Button("Done") { dismiss() } }
     40       }
     41     }
     42     .accessibilityIdentifier("radroots.add.drafts.sheet")
     43     .onAppear { recovery.start(); store.submissions.inventory.start() }
     44     .onChange(of: protection.reopened) { _, value in
     45       if value != nil {
     46         dismiss()
     47       }
     48     }
     49   }
     50 
     51   private var composerSection: some View {
     52     Section("Saved editing") {
     53       if let error = recovery.composerError {
     54         Text(error).foregroundStyle(.secondary)
     55       }
     56       if recovery.composers.isEmpty, recovery.composerError == nil, !recovery.isLoading {
     57         Text("No saved editing on this page.").foregroundStyle(.secondary)
     58       }
     59       ForEach(Array(recovery.composers.enumerated()), id: \.offset) { _, entry in
     60         switch entry {
     61         case let .draft(summary):
     62           VStack(alignment: .leading) {
     63             Text(summary.commandType.label).font(.headline)
     64             savedTime(summary.updatedAtUnixMilliseconds)
     65             Button("Reopen") { reopen(.composer(summary.id)) }
     66               .disabled(store.isWorking)
     67           }
     68           .accessibilityIdentifier("tera.add.composer.\(summary.id)")
     69         case let .repair(_, _, reason):
     70           Text(reason == .unsupportedSchema
     71             ? "This saved draft needs a compatible app. Its data has been preserved."
     72             : "This saved draft cannot be read. Its data has been preserved; other drafts remain available.")
     73             .accessibilityIdentifier("tera.add.composer.repair")
     74         }
     75       }
     76       if recovery.composerCursor != nil {
     77         Button("Next editing page") { recovery.moreComposers() }
     78           .disabled(recovery.isLoading)
     79       }
     80     }
     81   }
     82 
     83   private var legacySection: some View {
     84     Section("Saved operations") {
     85       if let error = recovery.legacyError {
     86         Text(error).foregroundStyle(.secondary)
     87       }
     88       if recovery.legacy.isEmpty, recovery.legacyError == nil, !recovery.isLoading {
     89         Text("No saved operations on this page.").foregroundStyle(.secondary)
     90       }
     91       ForEach(Array(recovery.legacy.enumerated()), id: \.offset) { _, entry in
     92         switch entry {
     93         case let .draft(summary): legacyRow(summary)
     94         case let .repair(_, _, reason):
     95           Text(legacyRepairMessage(reason)).accessibilityIdentifier("tera.add.operation.repair")
     96         }
     97       }
     98       if recovery.legacyCursor != nil {
     99         Button("Next operations page") { recovery.moreLegacy() }
    100           .disabled(recovery.isLoading)
    101       }
    102     }
    103   }
    104 
    105   private func legacyRow(_ summary: TeraLegacyDraftSummary) -> some View {
    106     VStack(alignment: .leading, spacing: 8) {
    107       Text(summary.commandType.label).font(.headline)
    108       Text(summary.honestSummary).font(.subheadline)
    109       if summary.mediaCount > 0 {
    110         Text(summary.mediaSummary).font(.caption).foregroundStyle(.secondary)
    111           .accessibilityIdentifier("radroots.add.draft.media_status.\(summary.id)")
    112       }
    113       savedTime(summary.updatedAtUnixMilliseconds)
    114       HStack {
    115         if summary.isRevision || summary.revisionParentID != nil {
    116           NavigationLink("Revision details") {
    117             TeraRevisionDetailView(store: recovery.revisions, operationID: summary.revisionParentID ?? summary.id)
    118           }
    119         }
    120         if summary.hasForm {
    121           Button(summary.isEditable ? "Reopen" : "View") { reopen(.legacy(summary.id)) }
    122         }
    123         if !summary.isRevision, summary.revisionParentID == nil, summary.canAdvance || summary.canQueue {
    124           Button("Retry") { Task { await store.retry(id: summary.id); recovery.start() } }
    125         }
    126         if !summary.isRevision, summary.revisionParentID == nil, summary.state.canCancel {
    127           Button("Cancel", role: .destructive) { Task { await store.cancel(id: summary.id); recovery.start() } }
    128         }
    129       }
    130       .buttonStyle(.borderless)
    131       .disabled(store.isWorking)
    132     }
    133     .accessibilityElement(children: .contain)
    134     .accessibilityIdentifier("radroots.add.draft.\(summary.id)")
    135   }
    136 
    137   private func savedTime(_ milliseconds: UInt64) -> some View {
    138     Text(Date(timeIntervalSince1970: Double(milliseconds) / 1000), format: .dateTime.year().month().day().hour().minute())
    139       .font(.caption).foregroundStyle(.secondary)
    140   }
    141 
    142   private func reopen(_ selection: TeraDraftRecoverySelection) {
    143     Task {
    144       if await store.reopenSaved(selection) {
    145         dismiss()
    146       }
    147     }
    148   }
    149 
    150   private func legacyRepairMessage(_ reason: TeraLegacyDraftRepairReason) -> String {
    151     switch reason {
    152     case .unsupportedSchema: "This saved operation needs a compatible app. Its data has been preserved."
    153     case .corruptRecord: "This saved operation cannot be read. Its data has been preserved; other operations remain available."
    154     case .needsAttention: "Local operation details are unavailable. Retry the list; other drafts remain available."
    155     }
    156   }
    157 }