feat(apple): storage screen, About content, and fixes

- Settings: add Storage screen (size breakdown + delete-all-transfers) and
  expand About (what it is/isn't, privacy & security, license/source)
- Move Report a bug to a toolbar sheet (cancel-only unless empty)
- Send progress: derive the list-row bar from receiver delivery status so it
  clears once every receiver completes
- Fixes: iPad orientations, onChange(of:) iOS 17 API, weak-self captures,
  invalid SF Symbol, macOS bug-report form labels; bump core build target to
  match the app (18.2/15.0)
This commit is contained in:
2026-07-19 13:58:20 +02:00
parent ceccfcda71
commit bfa489def1
13 changed files with 685 additions and 132 deletions

View File

@@ -110,69 +110,6 @@ func progressForReceiver(
)
}
/// Send-side progress summary for a transfer's list row. Aggregates only the
/// receivers that are *currently downloading* (in-flight), so the bar disappears
/// once every receiver has completed/aborted, and its value is deterministic
/// (order-independent) rather than "whichever receiver's event arrived last".
func sendProgressSummary(
events: [CoreEventModel],
transferId: UInt64,
totalSizeHint: UInt64? = nil
) -> TransferProgress? {
let endpointIds = events
.filter { $0.transferId == transferId && $0.direction == "send" && $0.phase == "transfer" }
.compactMap { findString($0.dataJson, key: "endpoint_id") }
.reduce(into: [String]()) { acc, id in if !acc.contains(id) { acc.append(id) } }
// No per-endpoint attribution: fall back to the single connection-scoped
// stream, hidden once it completes/aborts.
if endpointIds.isEmpty {
let relevant = events.filter {
$0.transferId == transferId && $0.direction == "send" && $0.phase == "transfer"
&& ["started", "progress", "completed", "aborted"].contains($0.kind)
}
guard let latest = relevant.first else { return nil }
if latest.kind == "completed" || latest.kind == "aborted" { return nil }
let live = relevant.filter { ["started", "progress"].contains($0.kind) }
return TransferProgress(
transferId: transferId, phase: "transfer", kind: latest.kind,
labelKey: "progress_sending",
progress: aggregateReceiverProgress(events: live, totalSizeHint: totalSizeHint),
detail: progressDetail(latest)
)
}
// Keep only receivers still in flight (latest state started/progress).
var activeCount = 0
var fractions: [Double] = []
var lastActive: TransferProgress?
for id in endpointIds {
guard let p = progressForReceiver(
events: events, transferId: transferId, remoteEndpointId: id, totalSizeHint: totalSizeHint
) else { continue }
guard p.kind == "progress" || p.kind == "started" else { continue }
activeCount += 1
lastActive = p
if let fraction = p.progress { fractions.append(fraction) }
}
if activeCount == 0 { return nil }
// Same total per receiver, so the byte-summed progress is the mean of the
// per-receiver fractions.
let combined = fractions.isEmpty ? nil : fractions.reduce(0, +) / Double(fractions.count)
if activeCount == 1 {
return TransferProgress(
transferId: transferId, phase: "transfer", kind: "progress",
labelKey: "progress_sending", progress: combined, detail: lastActive?.detail
)
}
return TransferProgress(
transferId: transferId, phase: "transfer", kind: "progress",
labelKey: "progress_sending", progress: combined,
label: String(format: String(localized: "progress_sending_to_count"), activeCount)
)
}
func formatBytes(_ size: UInt64) -> String {
var scaled = Double(size)
let units = ["B", "KB", "MB", "GB", "TB"]