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

@@ -44,6 +44,12 @@ final class SendModel: ObservableObject {
@Published var pendingFilePick = false
@Published var pendingFolderPick = false
/// Receiver delivery records per active (sharing) transfer, used to decide
/// which transfers still have an in-flight receiver. Delivery status is the
/// authoritative signal; byte-transfer events alone don't reliably mark a
/// small transfer complete.
@Published private(set) var receiversByTransfer: [UInt64: [ReceiverRequestModel]] = [:]
private let repository: CoreRepository
private let fileSystemService: FileSystemService
private let filePreviewRepository: FilePreviewRepository
@@ -72,10 +78,24 @@ final class SendModel: ObservableObject {
Task { _ = await self.repository.refresh() }
case .receiverHistoryChanged(let id), .approvalChanged(let id):
if id == self.state.selectedTransferId { self.refreshReceivers(id) }
self.refreshReceiverStatuses(for: id)
}
}
.store(in: &cancellables)
// Keep receiver delivery records current for every sharing/importing
// outgoing transfer (new shares appear here; status transitions arrive via
// the receiverHistoryChanged signal above).
repository.$state
.map { core -> Set<UInt64> in
Set(core.transfers
.filter { $0.direction == .send && ($0.status == .sharing || $0.status == .importing) }
.map(\.transferId))
}
.removeDuplicates()
.sink { [weak self] ids in self?.syncSharingReceivers(ids) }
.store(in: &cancellables)
filePreviewRepository.$previews
.sink { [weak self] previews in self?.state.transferThumbnails = previews }
.store(in: &cancellables)
@@ -299,6 +319,21 @@ final class SendModel: ObservableObject {
Task { await fileSystemService.discardPickedFiles(files) }
}
/// Refresh the receiver records for the sharing set, pruning transfers that are
/// no longer active.
private func syncSharingReceivers(_ ids: Set<UInt64>) {
receiversByTransfer = receiversByTransfer.filter { ids.contains($0.key) }
for id in ids { refreshReceiverStatuses(for: id) }
}
private func refreshReceiverStatuses(for transferId: UInt64) {
Task {
if case .success(let requests) = await repository.receiverRequests(transferId: transferId) {
receiversByTransfer[transferId] = requests
}
}
}
private func refreshReceivers(_ transferId: UInt64) {
state.isLoadingReceivers = true
Task {