mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-12 19:41:11 +00:00
Add progress view for incoming transfer on iOS
This commit is contained in:
1 parent
62f5a13fd6
commit
288625cbd3
8 files changed
+1786
-1553
No files matched your search
@@ -25,6 +25,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
private var currentState: State = .initial
|
||||
private var cipherCommitment: Data?
|
||||
private var textPayloadID: Int64 = 0
|
||||
private var bytesToBeTransferred: Int64 = 0
|
||||
var isPlainTextTransfer = false
|
||||
|
||||
var wasUserRejected = false
|
||||
@@ -154,6 +155,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
|
||||
// only for logging
|
||||
self.bytesTransferred += Int64(frame.payloadChunk.body.count)
|
||||
NearbyConnectionManager.shared.inboundConnection(connection: self, transferProgress: Double(self.bytesTransferred) / Double(self.bytesToBeTransferred))
|
||||
} catch {
|
||||
log("[InboundNearbyConnection \(self.id)] Error occurred during writing file: \(error.localizedDescription)")
|
||||
|
||||
@@ -172,6 +174,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
|
||||
if filesToBeReceived.isEmpty {
|
||||
log("[InboundNearbyConnection \(self.id)] All files received, sending disconnection frame and disconnecting.")
|
||||
NearbyConnectionManager.shared.inboundConnection(connection: self, transferProgress: 1)
|
||||
try sendDisconnectionAndDisconnect()
|
||||
}
|
||||
}
|
||||
@@ -218,6 +221,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL)
|
||||
|
||||
log("[InboundNearbyConnection \(self.id)] Received file payload. Disconnecting...")
|
||||
NearbyConnectionManager.shared.inboundConnection(connection: self, transferProgress: 1)
|
||||
try sendDisconnectionAndDisconnect()
|
||||
return true
|
||||
}
|
||||
@@ -426,6 +430,7 @@ class InboundNearbyConnection: NearbyConnection {
|
||||
payloadID: file.payloadID,
|
||||
destinationURL: dest)
|
||||
filesToBeReceived[file.payloadID] = info
|
||||
bytesToBeTransferred += file.size
|
||||
}
|
||||
let metadata = TransferMetadata(files: filesToBeReceived.map { $0.value.meta }, id: id, pinCode: pinCode)
|
||||
DispatchQueue.main.async {
|
||||
|
||||
@@ -134,6 +134,11 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
||||
}
|
||||
|
||||
|
||||
func inboundConnection(connection: InboundNearbyConnection, transferProgress: Double) {
|
||||
mainAppDelegate?.transferProgress(progress: transferProgress)
|
||||
}
|
||||
|
||||
|
||||
public func submitUserConsent(transferID: String, accept: Bool, storeInTemp: Bool = false) {
|
||||
guard let conn = incomingConnections[transferID] else { return }
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import SwiftECC
|
||||
|
||||
protocol InboundNearbyConnectionDelegate {
|
||||
func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo, connection: InboundNearbyConnection)
|
||||
func inboundConnection(connection: InboundNearbyConnection, transferProgress: Double)
|
||||
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?)
|
||||
}
|
||||
|
||||
@@ -29,6 +30,7 @@ protocol OutboundNearbyConnectionDelegate {
|
||||
public protocol InboundAppDelegate: AnyObject {
|
||||
func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo)
|
||||
func connectionWasTerminated(from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: Error?)
|
||||
func transferProgress(progress: Double)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import Foundation
|
||||
import ImageIO
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
public class SaveFilesManager {
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
EB97A0132E51D6B2008B8C5C /* RNDeviceName in Frameworks */ = {isa = PBXBuildFile; productRef = EB97A0122E51D6B2008B8C5C /* RNDeviceName */; };
|
||||
EB97A0142E51DD00008B8C5C /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; };
|
||||
EB97A0152E51DD00008B8C5C /* timeIsNow.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = EBC05C8A2E33B59E00EECC18 /* timeIsNow.mp3 */; };
|
||||
EB97A01A2E51EAA2008B8C5C /* UIAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB97A0192E51EA9C008B8C5C /* UIAlert.swift */; };
|
||||
EB9925802E45D53E00357055 /* sharing_enums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */; };
|
||||
EB9925812E45D53E00357055 /* sharing_enums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */; };
|
||||
EB9925822E45D53E00357055 /* sharing_enums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */; };
|
||||
@@ -249,6 +250,7 @@
|
||||
EB979F992E50CAA6008B8C5C /* PieProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PieProgressView.swift; sourceTree = "<group>"; };
|
||||
EB97A00E2E511120008B8C5C /* ReceiveModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveModel.swift; sourceTree = "<group>"; };
|
||||
EB97A0162E51DF4F008B8C5C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
EB97A0192E51EA9C008B8C5C /* UIAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlert.swift; sourceTree = "<group>"; };
|
||||
EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = sharing_enums.pb.swift; sourceTree = "<group>"; };
|
||||
EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlertHandler.swift; sourceTree = "<group>"; };
|
||||
EBC05C6D2E33B57D00EECC18 /* Converter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Converter.swift; sourceTree = "<group>"; };
|
||||
@@ -447,6 +449,7 @@
|
||||
EB979F8F2E50CA23008B8C5C /* FilePicker.swift */,
|
||||
EB979F902E50CA23008B8C5C /* SendModel.swift */,
|
||||
EB97A00E2E511120008B8C5C /* ReceiveModel.swift */,
|
||||
EB97A0192E51EA9C008B8C5C /* UIAlert.swift */,
|
||||
EB979F992E50CAA6008B8C5C /* PieProgressView.swift */,
|
||||
EB979F912E50CA23008B8C5C /* QuickDropiOS.entitlements */,
|
||||
EB979F922E50CA23008B8C5C /* QuickDropiOSApp.swift */,
|
||||
@@ -813,6 +816,7 @@
|
||||
EB5C383D2E38C2D90052EFD7 /* AudioManager.swift in Sources */,
|
||||
EB979F942E50CA23008B8C5C /* Configuration.swift in Sources */,
|
||||
EB979F952E50CA23008B8C5C /* ContentView.swift in Sources */,
|
||||
EB97A01A2E51EAA2008B8C5C /* UIAlert.swift in Sources */,
|
||||
EB979F962E50CA23008B8C5C /* FilePicker.swift in Sources */,
|
||||
EB979F9A2E50CAA6008B8C5C /* PieProgressView.swift in Sources */,
|
||||
EB979F972E50CA23008B8C5C /* SendModel.swift in Sources */,
|
||||
|
||||
+1634
-1527
File diff suppressed because it is too large.
Load diff
@@ -16,7 +16,7 @@ import LUI
|
||||
#endif
|
||||
|
||||
class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||
|
||||
|
||||
let controlPlusScreen: (Bool) -> Void
|
||||
|
||||
init(controlPlusScreen: @escaping (Bool) -> Void = { _ in }) {
|
||||
@@ -91,7 +91,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||
let primaryButtonTitle = "Accept".localized()
|
||||
let primaryButtonAction = { self.pressAcceptButton(transferID: transferID) }
|
||||
let secondaryButtonTitle = "Decline".localized()
|
||||
let secondaryButtonAction = { self.continueTransmission(accept: false, transferID: transferID) }
|
||||
let secondaryButtonAction = { NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: false, storeInTemp: false) }
|
||||
|
||||
#if os(macOS)
|
||||
let alert = NSAlert()
|
||||
@@ -111,31 +111,51 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||
}
|
||||
#else
|
||||
// iOS
|
||||
showAlert(title: title, message: mainMessage, primaryButton: LUIAlertButton(title: secondaryButtonTitle, action: secondaryButtonAction), secondaryButton: LUIAlertButton(title: primaryButtonTitle, action: primaryButtonAction))
|
||||
ProgressAlert.shared.askForUserPermission(title: title, message: mainMessage, acceptLabel: primaryButtonTitle, rejectLabel: secondaryButtonTitle) { accepted in
|
||||
accepted ? primaryButtonAction() : secondaryButtonAction()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func transferProgress(progress: Double) {
|
||||
#if os(iOS)
|
||||
ProgressAlert.shared.updateProgress(progress)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
func connectionWasTerminated(from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: (any Error)?) {
|
||||
if let error = error {
|
||||
controlPlusScreen(false)
|
||||
ErrorAlertHandler.shared.showErrorAlert(for: device.name ?? "Android", error: error)
|
||||
} else {
|
||||
|
||||
if wasPlainTextTransfer {
|
||||
showCopiedToClipboardAlert()
|
||||
}
|
||||
|
||||
let currentCount = incomingTransmissionCount()
|
||||
if currentCount == 0 {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
SKStoreReviewController.requestReview()
|
||||
|
||||
#if os(macOS)
|
||||
finish()
|
||||
#else
|
||||
ProgressAlert.shared.updateProgress(nil) {
|
||||
finish()
|
||||
}
|
||||
#endif
|
||||
|
||||
func finish() {
|
||||
if let error = error {
|
||||
controlPlusScreen(false)
|
||||
ErrorAlertHandler.shared.showErrorAlert(for: device.name ?? "Android", error: error)
|
||||
} else {
|
||||
|
||||
if wasPlainTextTransfer {
|
||||
showCopiedToClipboardAlert()
|
||||
}
|
||||
|
||||
let currentCount = incomingTransmissionCount()
|
||||
if currentCount == 0 {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
SKStoreReviewController.requestReview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UserDefaults.standard.set(currentCount + 1, forKey: UserDefaultsKeys.transmissionCount.rawValue)
|
||||
log("[ReceiveModel] Successful transmission. Current count: \(currentCount)")
|
||||
UserDefaults.standard.set(currentCount + 1, forKey: UserDefaultsKeys.transmissionCount.rawValue)
|
||||
log("[ReceiveModel] Successful transmission. Current count: \(currentCount)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,16 +174,11 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
||||
|
||||
private func pressAcceptButton(transferID: String) {
|
||||
if isFileTransferRestricted() {
|
||||
continueTransmission(accept: true, transferID: transferID, storeInTemp: true)
|
||||
log("[ReceiveModel] Showing plus screen...")
|
||||
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, storeInTemp: true)
|
||||
controlPlusScreen(true)
|
||||
} else {
|
||||
continueTransmission(accept: true, transferID: transferID)
|
||||
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, storeInTemp: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func continueTransmission(accept: Bool, transferID: String, storeInTemp: Bool = false) {
|
||||
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: accept, storeInTemp: storeInTemp)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// UIAlert.swift
|
||||
// QuickDrop
|
||||
//
|
||||
// Created by Leon Böttger on 17.08.25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
// MARK: - UIKit Alert Presenter
|
||||
|
||||
final class ProgressAlert {
|
||||
static let shared = ProgressAlert()
|
||||
private init() {}
|
||||
|
||||
private weak var progressAlert: UIAlertController?
|
||||
private weak var progressView: UIProgressView?
|
||||
|
||||
// MARK: - Initial Accept / Decline
|
||||
func askForUserPermission(title: String, message: String, acceptLabel: String, rejectLabel: String, onAccept: @escaping (Bool) -> Void) {
|
||||
guard let vc = topMostViewController() else { return }
|
||||
|
||||
let alert = UIAlertController(title: title,
|
||||
message: message,
|
||||
preferredStyle: .alert)
|
||||
|
||||
alert.addAction(UIAlertAction(title: rejectLabel, style: .cancel, handler: { _ in
|
||||
onAccept(false)
|
||||
}))
|
||||
|
||||
alert.addAction(UIAlertAction(title: acceptLabel, style: .default, handler: { _ in
|
||||
onAccept(true)
|
||||
self.showProgressAlert(on: vc)
|
||||
}))
|
||||
|
||||
vc.present(alert, animated: true)
|
||||
}
|
||||
|
||||
// MARK: - Progress Alert
|
||||
private func showProgressAlert(on vc: UIViewController) {
|
||||
let alert = UIAlertController(title: "Receiving".localized(),
|
||||
message: "\n\n",
|
||||
preferredStyle: .alert)
|
||||
|
||||
let pv = UIProgressView(progressViewStyle: .default)
|
||||
pv.translatesAutoresizingMaskIntoConstraints = false
|
||||
alert.view.addSubview(pv)
|
||||
NSLayoutConstraint.activate([
|
||||
pv.leadingAnchor.constraint(equalTo: alert.view.leadingAnchor, constant: 20),
|
||||
pv.trailingAnchor.constraint(equalTo: alert.view.trailingAnchor, constant: -20),
|
||||
pv.centerYAnchor.constraint(equalTo: alert.view.centerYAnchor, constant: 8)
|
||||
])
|
||||
|
||||
let cancel = UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: { _ in
|
||||
print("Cancel during progress")
|
||||
})
|
||||
alert.addAction(cancel)
|
||||
|
||||
self.progressAlert = alert
|
||||
self.progressView = pv
|
||||
|
||||
vc.present(alert, animated: true)
|
||||
}
|
||||
|
||||
// MARK: - Update Progress
|
||||
// progress: 0…1 → update progress, nil → finished
|
||||
func updateProgress(_ progress: Double?, completion: @escaping () -> Void = {}) {
|
||||
DispatchQueue.main.async {
|
||||
if let p = progress {
|
||||
self.progressView?.setProgress(Float(p), animated: true)
|
||||
completion()
|
||||
} else {
|
||||
// Completed: dismiss progress and show final alert
|
||||
self.progressAlert?.dismiss(animated: true) {
|
||||
completion()
|
||||
}
|
||||
self.progressAlert = nil
|
||||
self.progressView = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helpers
|
||||
private func topMostViewController(base: UIViewController? = UIApplication.shared.connectedScenes
|
||||
.compactMap { ($0 as? UIWindowScene)?.keyWindow?.rootViewController }.first) -> UIViewController? {
|
||||
if let nav = base as? UINavigationController { return topMostViewController(base: nav.visibleViewController) }
|
||||
if let tab = base as? UITabBarController { return topMostViewController(base: tab.selectedViewController) }
|
||||
if let presented = base?.presentedViewController { return topMostViewController(base: presented) }
|
||||
return base
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user