mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
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:
@@ -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 {
|
||||
|
||||
@@ -22,7 +22,7 @@ struct SendScreen: View {
|
||||
NavigationStack {
|
||||
Group {
|
||||
if outgoing.isEmpty {
|
||||
ScrollView { emptyState }
|
||||
emptyState
|
||||
} else {
|
||||
catalog
|
||||
}
|
||||
@@ -100,11 +100,11 @@ struct SendScreen: View {
|
||||
}
|
||||
|
||||
private var emptyState: some View {
|
||||
EmptyStateView(
|
||||
systemImage: "paperplane",
|
||||
title: String(localized: "send_empty_title"),
|
||||
message: String(localized: "send_empty_body")
|
||||
) {
|
||||
ContentUnavailableView {
|
||||
Label(String(localized: "send_empty_title"), systemImage: "paperplane")
|
||||
} description: {
|
||||
Text(LocalizedStringKey("send_empty_body"))
|
||||
} actions: {
|
||||
Button(action: model.openComposer) {
|
||||
Label(String(localized: "button_create_new_transfer"), systemImage: "plus")
|
||||
}
|
||||
@@ -116,10 +116,30 @@ struct SendScreen: View {
|
||||
private func progress(for transfer: Transfer) -> TransferProgress? {
|
||||
switch transfer.status {
|
||||
case .importing: return progressForTransfer(events: model.coreState.events, transferId: transfer.transferId)
|
||||
case .sharing: return sendProgressSummary(events: model.coreState.events, transferId: transfer.transferId, totalSizeHint: transfer.totalSize)
|
||||
case .sharing: return sharingProgress(for: transfer)
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Progress for an active share, driven by receivers whose delivery is still
|
||||
/// in flight (`.accepted`). Returns nil when none are downloading, so the bar
|
||||
/// clears once every receiver has completed even if byte events lag.
|
||||
private func sharingProgress(for transfer: Transfer) -> TransferProgress? {
|
||||
let active = (model.receiversByTransfer[transfer.transferId] ?? []).filter { $0.status == .accepted }
|
||||
if active.isEmpty { return nil }
|
||||
let fractions = active.compactMap {
|
||||
progressForReceiver(events: model.coreState.events, transferId: transfer.transferId,
|
||||
remoteEndpointId: $0.remoteEndpointId, totalSizeHint: transfer.totalSize)?.progress
|
||||
}
|
||||
let combined = fractions.isEmpty ? nil : fractions.reduce(0, +) / Double(fractions.count)
|
||||
if active.count == 1 {
|
||||
return TransferProgress(transferId: transfer.transferId, phase: "transfer", kind: "progress",
|
||||
labelKey: "progress_sending", progress: combined)
|
||||
}
|
||||
return TransferProgress(transferId: transfer.transferId, phase: "transfer", kind: "progress",
|
||||
labelKey: "progress_sending", progress: combined,
|
||||
label: String(format: String(localized: "progress_sending_to_count"), active.count))
|
||||
}
|
||||
}
|
||||
|
||||
private struct TransferListItem: View {
|
||||
|
||||
Reference in New Issue
Block a user