mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 02:51:11 +00:00
Refactoring
This commit is contained in:
1 parent
1ac988a4fc
commit
a69a75cb07
12 files changed
+39
-15
No files matched your search
@@ -0,0 +1,325 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// QuickDropiOS
|
||||
//
|
||||
// Created by Leon Böttger on 28.07.25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import LUI
|
||||
import StoreKit
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@ObservedObject var luiSettings = LUISettings.sharedInstance
|
||||
@ObservedObject var settings = Settings.shared
|
||||
|
||||
var body: some View {
|
||||
AppRootView(isPlus: $settings.gotPlus, phoneView: {
|
||||
DeviceListView()
|
||||
.environment(\.sheetActive, isShareExtension())
|
||||
}, settingsView: {
|
||||
CustomSection(header: settings.gotPlus ? "" : "General", footer: "TrustedDevicesFooterShort") {
|
||||
LUIButton {
|
||||
SaveFilesManager.shared.openDownloadedFilesFolder()
|
||||
} label: {
|
||||
NavigationLinkLabel(imageName: "folder.fill", text: "BrowseDownloadedFiles")
|
||||
}
|
||||
|
||||
LUILink(destination: NavigationSubView(header: "TrustedDevices") {
|
||||
TrustedDevicesView()
|
||||
}) {
|
||||
NavigationLinkLabel(imageName: "checkmark.shield.fill", text: "ManageTrustedDevices", backgroundColor: .green)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct DeviceListView: View {
|
||||
|
||||
@State private var qrCode: Image? = nil
|
||||
@StateObject var sendModel = SendModel()
|
||||
|
||||
#if !EXTENSION
|
||||
@StateObject var receiveModel = ReceiveModel()
|
||||
#endif
|
||||
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@Environment(\.sheetActive) var sheetActive
|
||||
@ObservedObject var nearbyConnectionManager = NearbyConnectionManager.shared
|
||||
@ObservedObject var settings = Settings.shared
|
||||
|
||||
var body: some View {
|
||||
|
||||
let showsLoadingIndicator = nearbyConnectionManager.hasLocalNetworkPermission && nearbyConnectionManager.isConnectedToLocalNetwork
|
||||
|
||||
BottomBarView(header: "QuickDrop ", navigationBarLayout: isShareExtension() ? .SmallOnlyAlways : .Default, bottomViewHeight: 30) {
|
||||
VStack {
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
|
||||
let attachments = nearbyConnectionManager.attachments
|
||||
|
||||
FormHeader(name: attachments == nil ? "YouAreVisibleAs".localized() : "YouAreSending".localized())
|
||||
|
||||
HStack {
|
||||
|
||||
if let image = attachments?.previewImage {
|
||||
image
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 10, height: 10)
|
||||
}
|
||||
|
||||
LUIText(attachments?.shortDescription ?? NearbyConnectionManager.shared.deviceInfo.name ?? "Unknown".localized(), isBold: true)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 6)
|
||||
.background(sheetActive ? Color.sheetForegroundColor : Color.defaultForegroundColor)
|
||||
.cornerRadius(24)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
if !nearbyConnectionManager.hasLocalNetworkPermission {
|
||||
CardView(backgroundColor: .red, title: "NoNetworkAccess", titleSymbol: "network.slash") {
|
||||
CardSubView(symbol: "exclamationmark.triangle.fill", text: "NoLocalNetworkAccessDescription")
|
||||
}
|
||||
.padding(.bottom, 10)
|
||||
.onTapGesture {
|
||||
openAppSettings()
|
||||
}
|
||||
}
|
||||
|
||||
if sendModel.foundDevices.isEmpty {
|
||||
|
||||
CustomSection(header: "NoDevicesFound") {
|
||||
|
||||
let hasWifi = nearbyConnectionManager.isConnectedToLocalNetwork
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: hasWifi ? "arrow.down.circle.fill" : "wifi.slash")
|
||||
.font(.system(size: 30))
|
||||
.foregroundColor(.blue)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(Color.white)
|
||||
.padding(3)
|
||||
.opacity(hasWifi ? 1 : 0)
|
||||
)
|
||||
.frame(width: 32)
|
||||
|
||||
LUIText(hasWifi ? "DownloadQuickDropOnPlayStore" : "NoWiFiConnectionDescription")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.animation(.default, value: hasWifi)
|
||||
.padding(.vertical)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
CustomSection(header: "AvailableDevices") {
|
||||
ForEach(sendModel.foundDevices) { device in
|
||||
|
||||
let isSelected = sendModel.selectedDevice == device
|
||||
|
||||
ZStack {
|
||||
if let attachments = NearbyConnectionManager.shared.attachments {
|
||||
LUIButton {
|
||||
lightVibration()
|
||||
sendModel.selectDevice(device: device, with: attachments)
|
||||
} label: {
|
||||
DeviceButtonLabel(device: device, isSelected: isSelected, progress: sendModel.progressState, progressValue: sendModel.progressValue)
|
||||
}
|
||||
}
|
||||
else {
|
||||
DeviceFilePickerButton(device: device, isSelected: isSelected, progressState: sendModel.progressState, progressValue: sendModel.progressValue, sendModel: sendModel)
|
||||
}
|
||||
}
|
||||
.animation(.easeInOut, value: sendModel.progressValue)
|
||||
.animation(.easeInOut, value: isSelected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if showsLoadingIndicator {
|
||||
HStack(spacing: 8) {
|
||||
Text("SearchingForDevices")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(Color.primary.opacity(0.6))
|
||||
|
||||
LoadingIndicator()
|
||||
.opacity(0.75)
|
||||
.frame(width: 12, height: 12)
|
||||
}
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 700)
|
||||
} bottomView: {
|
||||
|
||||
LUIButton {
|
||||
qrCode = NearbyConnectionManager.shared.generateQrCodeKey()
|
||||
sendModel.showQrCodeView = true
|
||||
} label: {
|
||||
UnderlineText(label: "DeviceNotShown")
|
||||
.padding()
|
||||
}
|
||||
.opacity(showsLoadingIndicator ? 1 : 0)
|
||||
}
|
||||
.luiSheet(isPresented: $sendModel.showQrCodeView, content: {
|
||||
NavigationView {
|
||||
NavigationSubView(header: "ConnectDevice", navigationBarLayout: .SmallOnlyAlways) {
|
||||
SmallSheetView(type: .sendToDeviceQrCode, dynamicQrCode: qrCode, closeView: {})
|
||||
}
|
||||
.navigationBarItems(trailing: XButton(action: { sendModel.showQrCodeView = false }))
|
||||
}
|
||||
})
|
||||
.onChange(of: scenePhase) { newValue in
|
||||
|
||||
if newValue == .active {
|
||||
|
||||
log("[ScenePhase] App became active")
|
||||
NearbyConnectionManager.shared.startDeviceDiscovery()
|
||||
#if !EXTENSION
|
||||
NearbyConnectionManager.shared.becomeVisible()
|
||||
|
||||
if Settings.shared.incomingTransmissionCount > 0 {
|
||||
runAfter(seconds: 0.3) {
|
||||
requestReviewOnce()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if newValue == .background {
|
||||
|
||||
log("[ScenePhase] App went to background")
|
||||
#if !EXTENSION
|
||||
NearbyConnectionManager.shared.becomeInvisible()
|
||||
#endif
|
||||
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
||||
}
|
||||
}
|
||||
.animation(.smooth, value: nearbyConnectionManager.hasLocalNetworkPermission)
|
||||
.animation(.smooth, value: nearbyConnectionManager.isConnectedToLocalNetwork)
|
||||
.navigationBarItems(trailing: ZStack {
|
||||
if isShareExtension() {
|
||||
XButton(action: { NearbyConnectionManager.shared.attachments?.closeView?() })
|
||||
}
|
||||
else {
|
||||
LUISettingsButton()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct DeviceFilePickerButton: View {
|
||||
|
||||
let device: RemoteDeviceInfo
|
||||
let isSelected: Bool
|
||||
let progressState: String?
|
||||
let progressValue: Double?
|
||||
let sendModel: SendModel
|
||||
|
||||
@State private var isPreparing = false
|
||||
|
||||
var body: some View {
|
||||
SendPickerButton {
|
||||
DeviceButtonLabel(device: device, isSelected: isPreparing || isSelected, progress: isPreparing ? "Preparing".localized() : progressState, progressValue: progressValue)
|
||||
} onResult: { urls, text in
|
||||
isPreparing = false
|
||||
sendModel.selectDevice(device: device, with: AttachmentDetails(urls: urls ?? [], textToSend: text, shortDescription: ""))
|
||||
} onPrepare: {
|
||||
isPreparing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct DeviceButtonLabel: View {
|
||||
|
||||
let device: RemoteDeviceInfo
|
||||
let isSelected: Bool
|
||||
let progress: String?
|
||||
let progressValue: Double?
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
|
||||
let name = device.name ?? "Android"
|
||||
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [Color(red: 0.623, green: 0.659, blue: 0.855),
|
||||
Color(red: 0.474, green: 0.525, blue: 0.796)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
.frame(width: 40, height: 40)
|
||||
|
||||
Image(systemName: device.icon)
|
||||
.foregroundColor(.white)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(name)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(Color.primary)
|
||||
|
||||
Text(isSelected ? progress ?? "..." : "Available".localized())
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(Color.primary.opacity(0.6))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if isSelected, let progress = progressValue {
|
||||
PieProgressView(progress: progress, size: 20)
|
||||
}
|
||||
else {
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(Color.primary.opacity(0.6))
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 13)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func isShareExtension() -> Bool {
|
||||
#if EXTENSION
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
//extension DeviceListView {
|
||||
// static var preview: DeviceListView {
|
||||
// let model = SendModel()
|
||||
// model.foundDevices = [
|
||||
// RemoteDeviceInfo(name: "MacBook Pro", type: .computer, id: "macbook-pro-id"),
|
||||
// RemoteDeviceInfo(name: "iPhone 14", type: .phone, id: "iphone-14-id"),
|
||||
// RemoteDeviceInfo(name: "Samsung Galaxy S21", type: .phone, id: "samsung-galaxy-s21-id")
|
||||
// ]
|
||||
//
|
||||
// return DeviceListView(sendModel: model, receiveModel: ReceiveModel())
|
||||
// }
|
||||
//}
|
||||
//
|
||||
#Preview {
|
||||
NavigationView {
|
||||
DeviceListView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
//
|
||||
// FilePicker.swift
|
||||
// QuickDrop
|
||||
//
|
||||
// Created by Leon Böttger on 15.08.25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
import LUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct SendPickerButton<Label: View>: View {
|
||||
var label: () -> Label
|
||||
var onResult: (_ urls: [URL]?, _ text: String?) -> Void
|
||||
var onPrepare: (() -> Void)? = nil
|
||||
|
||||
@State private var showPhotoPicker = false
|
||||
@State private var selectedPhotoItems: [PhotosPickerItem] = []
|
||||
|
||||
@State private var showFilePicker = false
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Button("SendImages", systemImage: "photo") {
|
||||
showPhotoPicker = true
|
||||
}
|
||||
Button("SendFiles", systemImage: "folder") {
|
||||
showFilePicker = true
|
||||
}
|
||||
Button("SendClipboard", systemImage: "doc.on.clipboard") {
|
||||
if let clipboardText = UIPasteboard.general.string, !clipboardText.isEmpty {
|
||||
onResult(nil, clipboardText)
|
||||
} else {
|
||||
showAlert(title: "ClipboardEmpty", message: "ClipboardEmptyMessage")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
label()
|
||||
}
|
||||
.photosPicker(
|
||||
isPresented: $showPhotoPicker,
|
||||
selection: $selectedPhotoItems,
|
||||
maxSelectionCount: nil,
|
||||
matching: .any(of: [.images, .videos])
|
||||
)
|
||||
.onChange(of: selectedPhotoItems) { newItems in
|
||||
guard !newItems.isEmpty else { return }
|
||||
onPrepare?()
|
||||
updatedPhotoSelection(content: newItems)
|
||||
}
|
||||
// File picker
|
||||
.fileImporter(isPresented: $showFilePicker,
|
||||
allowedContentTypes: [.item, .folder], // any file
|
||||
allowsMultipleSelection: true) { result in
|
||||
switch result {
|
||||
case .success(let urls):
|
||||
onResult(urls, nil)
|
||||
case .failure:
|
||||
onResult(nil, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updatedPhotoSelection(content: [PhotosPickerItem]) {
|
||||
Task {
|
||||
do {
|
||||
let urls = try await fetchContentUrls(content: content)
|
||||
onResult(urls, nil)
|
||||
selectedPhotoItems = []
|
||||
} catch {
|
||||
log("Could not fetch photo URLs: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func fetchContentUrls(content: [PhotosPickerItem]) async throws -> [URL] {
|
||||
try await withThrowingTaskGroup(of: URL?.self, returning: [URL].self) { group in
|
||||
let fileManager = FileManager.default
|
||||
|
||||
for item in content {
|
||||
group.addTask {
|
||||
if let dataUrl = try await item.loadTransferable(type: Data.self) {
|
||||
|
||||
if let contentType = item.supportedContentTypes.first {
|
||||
// Step 2: make the URL file name and a get a file extention.
|
||||
let url = fileManager.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).\(contentType.preferredFilenameExtension ?? "")")
|
||||
|
||||
do {
|
||||
// Step 3: write to temp App file directory and return in completionHandler
|
||||
try dataUrl.write(to: url)
|
||||
return url
|
||||
} catch {
|
||||
log("Failed to write data to file: \(error)")
|
||||
throw error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// let destination = fileManager.temporaryDirectory.appendingPathComponent(dataUrl.url.lastPathComponent)
|
||||
//
|
||||
// // overwrite if file exists
|
||||
// if fileManager.fileExists(atPath: destination.path) {
|
||||
// try fileManager.removeItem(at: destination)
|
||||
// }
|
||||
//
|
||||
// log("Copying photo at \(dataUrl.url) to \(destination)")
|
||||
//
|
||||
// do {
|
||||
// try fileManager.copyItem(at: dataUrl.url, to: destination)
|
||||
// }
|
||||
// catch {
|
||||
// log("Failed to copy photo: \(error)")
|
||||
// throw error
|
||||
// }
|
||||
// return destination
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var finalUrls = [URL]()
|
||||
for try await result in group {
|
||||
if let url = result {
|
||||
finalUrls.append(url)
|
||||
}
|
||||
}
|
||||
|
||||
return finalUrls
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// File representation
|
||||
fileprivate struct DataUrl: Transferable {
|
||||
let url: URL
|
||||
|
||||
static var transferRepresentation: some TransferRepresentation {
|
||||
FileRepresentation(contentType: .data) { data in
|
||||
|
||||
log("Received data: \(data.url)")
|
||||
return SentTransferredFile(data.url)
|
||||
} importing: { received in
|
||||
|
||||
log("Importing file: \(received.file)")
|
||||
return Self(url: received.file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
SendPickerButton {
|
||||
Text("Send Files")
|
||||
.font(.headline)
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(8)
|
||||
} onResult: { urls, text in
|
||||
if let urls = urls {
|
||||
log("Selected URLs: \(urls)")
|
||||
}
|
||||
if let text = text {
|
||||
log("Clipboard text: \(text)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// PieProgressView.swift
|
||||
// QuickDrop
|
||||
//
|
||||
// Created by Leon Böttger on 16.08.25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PieProgressView: View {
|
||||
let progress: CGFloat // value between 0.0 and 1.0
|
||||
var size: CGFloat = 25.0
|
||||
|
||||
var color: Color? = nil
|
||||
|
||||
var body: some View {
|
||||
|
||||
let color = color ?? .gray
|
||||
|
||||
ZStack {
|
||||
|
||||
Circle()
|
||||
.stroke(lineWidth: size * 0.08)
|
||||
.foregroundColor(color)
|
||||
|
||||
ZStack {
|
||||
PieSlice(progress: progress)
|
||||
.fill(color)
|
||||
.rotationEffect(.degrees(-90)) // Start from the top
|
||||
}
|
||||
.overlay (
|
||||
Image(systemName: "checkmark")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundColor(color == .white ? .blue : .white)
|
||||
.opacity(progress >= 1.0 ? 1.0 : 0.0) // Show checkmark only when complete
|
||||
)
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct PieSlice: Shape {
|
||||
var progress: CGFloat // from 0.0 to 1.0
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
|
||||
let center = CGPoint(x: rect.midX, y: rect.midY)
|
||||
let radius = rect.width / 2
|
||||
let endAngle = Angle(degrees: 360 * Double(progress))
|
||||
|
||||
path.move(to: center)
|
||||
path.addArc(center: center,
|
||||
radius: radius,
|
||||
startAngle: .degrees(0),
|
||||
endAngle: endAngle,
|
||||
clockwise: false)
|
||||
path.closeSubpath()
|
||||
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
PieProgressView(progress: 0.76)
|
||||
.padding()
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
//
|
||||
// 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, acceptAlwaysLabel: String?, rejectLabel: String, acceptAutomatically: Bool, onAccept: @escaping (AcceptAction) -> Void, onCancel: @escaping () -> Void) {
|
||||
guard let vc = topMostViewController() else { return }
|
||||
|
||||
if acceptAutomatically {
|
||||
onAccept(.Accept)
|
||||
self.showProgressAlert(on: vc, onCancel: onCancel)
|
||||
}
|
||||
else {
|
||||
|
||||
let alert = UIAlertController(title: title,
|
||||
message: message,
|
||||
preferredStyle: .alert)
|
||||
|
||||
alert.addAction(UIAlertAction(title: rejectLabel, style: .cancel, handler: { _ in
|
||||
onAccept(.Decline)
|
||||
}))
|
||||
|
||||
alert.addAction(UIAlertAction(title: acceptLabel, style: .default, handler: { _ in
|
||||
onAccept(.Accept)
|
||||
self.showProgressAlert(on: vc, onCancel: onCancel)
|
||||
}))
|
||||
|
||||
if let acceptAlways = acceptAlwaysLabel {
|
||||
alert.addAction(UIAlertAction(title: acceptAlways, style: .default, handler: { _ in
|
||||
onAccept(.AcceptAlways)
|
||||
self.showProgressAlert(on: vc, onCancel: onCancel)
|
||||
}))
|
||||
}
|
||||
|
||||
vc.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Progress Alert
|
||||
private func showProgressAlert(on vc: UIViewController, onCancel: @escaping () -> Void) {
|
||||
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
|
||||
log("Cancel during progress")
|
||||
onCancel()
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
enum AcceptAction {
|
||||
case Accept
|
||||
case AcceptAlways
|
||||
case Decline
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user