| @@ -18,14 +18,17 @@ struct MirrorsView: View { |
| 18 | 18 | if let error = model.actionError, !adding { |
| 19 | 19 | Section { GBNotice(error, .gbWarn) } |
| 20 | 20 | } else if scheduled { |
| 21 | | Section { GBNotice("Sync scheduled. The list updates as each mirror finishes.", .gbOK) } |
| 21 | Section { GBNotice("Sync scheduled. Pull to refresh for each mirror's result.", .gbOK) } |
| 22 | 22 | } |
| 23 | 23 | Section { |
| 24 | 24 | ForEach(model.state.value ?? []) { mirror in |
| 25 | 25 | row(mirror) |
| 26 | 26 | .swipeActions { |
| 27 | | Button("Remove", role: .destructive) { removing = mirror } |
| 28 | | .disabled(model.working) |
| 27 | Button("Remove", role: .destructive) { |
| 28 | scheduled = false |
| 29 | removing = mirror |
| 30 | } |
| 31 | .disabled(model.working) |
| 29 | 32 | } |
| 30 | 33 | } |
| 31 | 34 | } footer: { |
| @@ -51,7 +54,10 @@ struct MirrorsView: View { |
| 51 | 54 | .navigationBarTitleDisplayMode(.inline) |
| 52 | 55 | .toolbar { |
| 53 | 56 | ToolbarItem(placement: .topBarTrailing) { |
| 54 | | Button { adding = true } label: { Image(systemName: "plus") } |
| 57 | Button { |
| 58 | scheduled = false |
| 59 | adding = true |
| 60 | } label: { Image(systemName: "plus") } |
| 55 | 61 | .accessibilityLabel("Add Mirror") |
| 56 | 62 | .disabled(model.working) |
| 57 | 63 | .accessibilityIdentifier("mirror-add-button") |
| @@ -115,6 +121,7 @@ private struct MirrorAddSheet: View { |
| 115 | 121 | @State private var pull = false |
| 116 | 122 | @State private var username = "" |
| 117 | 123 | @State private var token = "" |
| 124 | @State private var attempted = false |
| 118 | 125 | |
| 119 | 126 | var body: some View { |
| 120 | 127 | NavigationStack { |
| @@ -146,7 +153,7 @@ private struct MirrorAddSheet: View { |
| 146 | 153 | } footer: { |
| 147 | 154 | Text("For a remote that needs credentials. The token is stored for syncs and not shown again.") |
| 148 | 155 | } |
| 149 | | if let error = model.actionError { |
| 156 | if attempted, let error = model.actionError { |
| 150 | 157 | Section { GBNotice(error, .gbWarn) } |
| 151 | 158 | } |
| 152 | 159 | } |
| @@ -162,6 +169,7 @@ private struct MirrorAddSheet: View { |
| 162 | 169 | } else { |
| 163 | 170 | Button("Add") { |
| 164 | 171 | Task { |
| 172 | attempted = true |
| 165 | 173 | guard await model.add(url: url, pull: pull, username: username, token: token) else { return } |
| 166 | 174 | // A secure field that still holds text as the sheet |
| 167 | 175 | // goes makes iOS offer to save it as a password. |
| @@ -207,7 +215,7 @@ struct PageDomainsView: View { |
| 207 | 215 | VStack(alignment: .leading, spacing: 3) { |
| 208 | 216 | Text(claim.domain).font(.gbMono(.subheadline)) |
| 209 | 217 | HStack(spacing: 6) { |
| 210 | | GBChip(claim.isVerified ? "verified" : "pending", claim.isVerified ? Color.gbOK : Color.gbWarn) |
| 218 | GBChip(claim.state, claim.isVerified ? Color.gbOK : claim.isPending ? Color.gbWarn : Color.secondary) |
| 211 | 219 | if let at = claim.verifiedAt { |
| 212 | 220 | Text("since \(at.formatted(date: .abbreviated, time: .omitted))") |
| 213 | 221 | .font(.gbSans(.caption2)) |
| @@ -218,9 +226,12 @@ struct PageDomainsView: View { |
| 218 | 226 | .padding(.vertical, 2) |
| 219 | 227 | .accessibilityElement(children: .combine) |
| 220 | 228 | .swipeActions { |
| 221 | | Button("Remove", role: .destructive) { removing = claim } |
| 222 | | .disabled(model.working) |
| 223 | | if !claim.isVerified { |
| 229 | Button("Remove", role: .destructive) { |
| 230 | verified = nil |
| 231 | removing = claim |
| 232 | } |
| 233 | .disabled(model.working) |
| 234 | if claim.isPending { |
| 224 | 235 | Button("Verify") { verify(claim.domain) } |
| 225 | 236 | .tint(.gbAccent) |
| 226 | 237 | .disabled(model.working) |
| @@ -229,7 +240,7 @@ struct PageDomainsView: View { |
| 229 | 240 | } |
| 230 | 241 | } footer: { |
| 231 | 242 | if model.state.value != nil { |
| 232 | | Text("A domain serves this repository's pages branch once a DNS TXT record proves the claim. Swipe a pending claim to verify it.") |
| 243 | Text("A domain serves this repository's pages branch once a DNS TXT record proves the claim. Swipe a pending claim to verify it; an expired one has to be claimed again.") |
| 233 | 244 | } |
| 234 | 245 | } |
| 235 | 246 | } |
| @@ -238,7 +249,10 @@ struct PageDomainsView: View { |
| 238 | 249 | .navigationBarTitleDisplayMode(.inline) |
| 239 | 250 | .toolbar { |
| 240 | 251 | ToolbarItem(placement: .topBarTrailing) { |
| 241 | | Button { adding = true } label: { Image(systemName: "plus") } |
| 252 | Button { |
| 253 | verified = nil |
| 254 | adding = true |
| 255 | } label: { Image(systemName: "plus") } |
| 242 | 256 | .accessibilityLabel("Claim Domain") |
| 243 | 257 | .disabled(model.working) |
| 244 | 258 | .accessibilityIdentifier("domain-add-button") |
| @@ -283,23 +297,20 @@ private struct DomainClaimSheet: View { |
| 283 | 297 | |
| 284 | 298 | @State private var domain = "" |
| 285 | 299 | @State private var challenge: DomainChallenge? |
| 300 | @State private var attempted = false |
| 286 | 301 | |
| 287 | 302 | var body: some View { |
| 288 | 303 | NavigationStack { |
| 289 | 304 | Form { |
| 290 | 305 | if let challenge { |
| 291 | 306 | Section { |
| 292 | | LabeledContent("Name") { |
| 293 | | Text(challenge.name).font(.gbMono(.caption)).textSelection(.enabled) |
| 294 | | } |
| 307 | record("Name", challenge.name, id: "domain-copy-name") |
| 295 | 308 | LabeledContent("Type", value: "TXT") |
| 296 | | LabeledContent("Value") { |
| 297 | | Text(challenge.value).font(.gbMono(.caption)).textSelection(.enabled) |
| 298 | | } |
| 309 | record("Value", challenge.value, id: "domain-copy-value") |
| 299 | 310 | } header: { |
| 300 | 311 | Text("Create this DNS record") |
| 301 | 312 | } footer: { |
| 302 | | Text("Then verify. An unverified claim expires after a few days; it can also be verified later from the list.") |
| 313 | Text("This record is shown only now. Create it, then verify, here or later from the list. An unverified claim expires after a few days.") |
| 303 | 314 | } |
| 304 | 315 | } else { |
| 305 | 316 | Section { |
| @@ -312,7 +323,7 @@ private struct DomainClaimSheet: View { |
| 312 | 323 | Text("A domain you control. Claiming it gives you a DNS record that proves it.") |
| 313 | 324 | } |
| 314 | 325 | } |
| 315 | | if let error = model.actionError { |
| 326 | if attempted, let error = model.actionError { |
| 316 | 327 | Section { GBNotice(error, .gbWarn) } |
| 317 | 328 | } |
| 318 | 329 | } |
| @@ -329,19 +340,44 @@ private struct DomainClaimSheet: View { |
| 329 | 340 | ProgressView() |
| 330 | 341 | } else if let challenge { |
| 331 | 342 | Button("Verify") { |
| 332 | | Task { if await model.verify(challenge.domain) { onDone(challenge.domain) } } |
| 343 | Task { |
| 344 | attempted = true |
| 345 | if await model.verify(challenge.domain) { onDone(challenge.domain) } |
| 346 | } |
| 333 | 347 | } |
| 334 | 348 | .accessibilityIdentifier("domain-verify") |
| 335 | 349 | } else { |
| 336 | 350 | Button("Claim") { |
| 337 | | Task { challenge = await model.add(domain) } |
| 351 | Task { |
| 352 | attempted = true |
| 353 | challenge = await model.add(domain) |
| 354 | } |
| 338 | 355 | } |
| 339 | 356 | .disabled(domain.trimmingCharacters(in: .whitespaces).isEmpty) |
| 340 | 357 | .accessibilityIdentifier("domain-submit") |
| 341 | 358 | } |
| 342 | 359 | } |
| 343 | 360 | } |
| 344 | | .interactiveDismissDisabled(model.working) |
| 361 | // The record cannot be fetched again, so it closes only by Done. |
| 362 | .interactiveDismissDisabled(model.working || challenge != nil) |
| 363 | } |
| 364 | } |
| 365 | |
| 366 | private func record(_ label: String, _ text: String, id: String) -> some View { |
| 367 | HStack(alignment: .firstTextBaseline) { |
| 368 | VStack(alignment: .leading, spacing: 2) { |
| 369 | Text(label).font(.gbSans(.caption)).foregroundStyle(.secondary) |
| 370 | Text(text).font(.gbMono(.caption)).textSelection(.enabled) |
| 371 | } |
| 372 | Spacer() |
| 373 | Button { |
| 374 | UIPasteboard.general.string = text |
| 375 | } label: { |
| 376 | Image(systemName: "doc.on.doc") |
| 377 | } |
| 378 | .buttonStyle(.borderless) |
| 379 | .accessibilityLabel("Copy \(label)") |
| 380 | .accessibilityIdentifier(id) |
| 345 | 381 | } |
| 346 | 382 | } |
| 347 | 383 | } |