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 }