WIP: Add abiity to receive using iOS app

This commit is contained in:
Leon Böttger committed 2025-08-17 11:46:47 +02:00
1 parent 608ab41c0b
commit 6ec3902788
13 files changed
+473 -214

No files matched your search

+17 -15
View File
@@ -10,11 +10,12 @@ import LUI
struct ContentView: View {
@StateObject var model = ShareViewModel()
@StateObject var sendModel = SendModel()
@StateObject var receiveModel = ReceiveModel()
var body: some View {
AppRootView(isPlus: .constant(true), phoneView: {
DeviceListView(model: model)
DeviceListView(sendModel: sendModel, receiveModel: receiveModel)
}, settingsView: {
EmptyView()
})
@@ -23,7 +24,8 @@ struct ContentView: View {
struct DeviceListView: View {
@ObservedObject var model: ShareViewModel
@ObservedObject var sendModel: SendModel
@ObservedObject var receiveModel: ReceiveModel
var body: some View {
@@ -33,7 +35,7 @@ struct DeviceListView: View {
VStack(alignment: .leading, spacing: 8) {
FooterView(text: "YouAreVisibleAs")
Text(NearbyConnectionManager.shared.getEndpointInfo().name ?? "QuickDrop")
Text(NearbyConnectionManager.shared.deviceInfo.name ?? "Unknown".localized())
.fontWeight(.bold)
.padding(.horizontal, 16)
.padding(.vertical, 6)
@@ -45,7 +47,7 @@ struct DeviceListView: View {
.frame(maxWidth: .infinity, alignment: .leading)
if model.foundDevices.isEmpty {
if sendModel.foundDevices.isEmpty {
CustomSection(header: "NoDevicesFound") {
@@ -61,9 +63,9 @@ struct DeviceListView: View {
} else {
CustomSection(header: "AvailableDevices") {
ForEach(model.foundDevices) { device in
ForEach(sendModel.foundDevices) { device in
let isSelected = model.selectedDevice == device
let isSelected = sendModel.selectedDevice == device
SendPickerButton {
HStack(spacing: 12) {
@@ -92,14 +94,14 @@ struct DeviceListView: View {
.fontWeight(.medium)
.foregroundColor(Color.primary)
Text(isSelected ? model.progressState ?? "..." : "Available".localized())
Text(isSelected ? sendModel.progressState ?? "..." : "Available".localized())
.font(.system(size: 12))
.foregroundColor(Color.primary.opacity(0.6))
}
Spacer()
if isSelected, let progress = model.progressValue {
if isSelected, let progress = sendModel.progressValue {
PieProgressView(progress: progress, size: 20)
}
else {
@@ -109,11 +111,11 @@ struct DeviceListView: View {
}
.padding(.vertical, 13)
} onResult: { urls, text in
model.urls = urls ?? []
model.textToSend = text
model.selectDevice(device: device)
sendModel.urls = urls ?? []
sendModel.textToSend = text
sendModel.selectDevice(device: device)
}
.animation(.easeInOut, value: model.progressValue)
.animation(.easeInOut, value: sendModel.progressValue)
.animation(.easeInOut, value: isSelected)
}
}
@@ -135,14 +137,14 @@ struct DeviceListView: View {
extension DeviceListView {
static var preview: DeviceListView {
let model = ShareViewModel()
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(model: model)
return DeviceListView(sendModel: model, receiveModel: ReceiveModel())
}
}
+147 -3
View File
@@ -6,20 +6,164 @@
//
import SwiftUI
import StoreKit
import UserNotifications
#if os(macOS)
import BezelNotification
#else
import LUI
#endif
class ReceiveModel: ObservableObject, InboundAppDelegate {
let controlPlusScreen: (Bool) -> Void
init() {
init(controlPlusScreen: @escaping (Bool) -> Void = { _ in }) {
self.controlPlusScreen = controlPlusScreen
NearbyConnectionManager.shared.mainAppDelegate = self
NearbyConnectionManager.shared.becomeVisible()
}
func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo) {
#if os(macOS)
NSApp.activate(ignoringOtherApps: true)
AudioManager.playIncomingFileSound()
#endif
let acceptAutomatically = UserDefaults.standard.bool(forKey: UserDefaultsKeys.automaticallyAcceptFiles.rawValue)
let fileStr: String
if let textTitle = transfer.textDescription {
fileStr = textTitle
} else if transfer.files.count == 1 {
fileStr = transfer.files[0].name
} else {
fileStr = String.localizedStringWithFormat("NFiles".localized(), transfer.files.count)
}
let mainMessage: String
let name = device.name ?? "Android"
switch transfer.type {
case .file:
mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingFiles" : "DeviceSendingFiles").localized(), arguments: [name, fileStr])
case .text:
mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingText" : "DeviceSendingText").localized(), arguments: [name, fileStr])
case .url:
mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingUrl" : "DeviceSendingUrl").localized(), arguments: [name, fileStr])
}
let pinCodeMessage = String(format: "PinCode".localized(), arguments: [transfer.pinCode ?? "?"])
let transferID = transfer.id
if acceptAutomatically {
pressAcceptButton(transferID: transfer.id)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, _ in
if granted {
log("[ReceiveModel] User granted notification permissions")
let notificationContent = UNMutableNotificationContent()
notificationContent.title = "QuickDrop"
notificationContent.body = mainMessage
notificationContent.sound = nil
let notificationId = UUID().uuidString
let notificationReq = UNNotificationRequest(identifier: notificationId, content: notificationContent, trigger: nil)
UNUserNotificationCenter.current().add(notificationReq)
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notificationId])
}
}
}
} else {
let title = "QuickDrop - \(pinCodeMessage)"
let primaryButtonTitle = "Accept".localized()
let primaryButtonAction = { self.pressAcceptButton(transferID: transferID) }
let secondaryButtonTitle = "Decline".localized()
let secondaryButtonAction = { self.continueTransmission(accept: false, transferID: transferID) }
#if os(macOS)
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = title
alert.informativeText = mainMessage
alert.addButton(withTitle: primaryButtonTitle)
alert.addButton(withTitle: secondaryButtonTitle)
let result = alert.runModal()
if result == .alertFirstButtonReturn {
primaryButtonAction()
} else if result == .alertSecondButtonReturn {
secondaryButtonAction()
}
#else
// iOS
showAlert(title: title, message: mainMessage, primaryButton: LUIAlertButton(title: secondaryButtonTitle, action: secondaryButtonAction), secondaryButton: LUIAlertButton(title: primaryButtonTitle, action: primaryButtonAction))
#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()
}
}
UserDefaults.standard.set(currentCount + 1, forKey: UserDefaultsKeys.transmissionCount.rawValue)
log("[ReceiveModel] Successful transmission. Current count: \(currentCount)")
}
}
private func showCopiedToClipboardAlert() {
#if os(macOS)
DispatchQueue.main.async {
BezelNotification.show(messageText: "InsertedIntoClipboard".localized(), icon: .clipboard)
}
#else
// iOS
doubleVibration()
#endif
}
private func pressAcceptButton(transferID: String) {
if isFileTransferRestricted() {
continueTransmission(accept: true, transferID: transferID, storeInTemp: true)
log("[ReceiveModel] Showing plus screen...")
controlPlusScreen(true)
} else {
continueTransmission(accept: true, transferID: transferID)
}
}
private func continueTransmission(accept: Bool, transferID: String, storeInTemp: Bool = false) {
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: accept, storeInTemp: storeInTemp)
}
}
+90
View File
@@ -0,0 +1,90 @@
//
// Model.swift
// QuickDrop
//
// Created by Leon Böttger on 28.07.25.
//
import SwiftUI
class SendModel: ObservableObject, OutboundAppDelegate {
@Published var foundDevices: [RemoteDeviceInfo] = []
@Published var selectedDevice: RemoteDeviceInfo?
@Published var progressState: String? = nil
@Published var progressValue: Double? = nil
var urls: [URL] = []
var textToSend: String? = nil
private var connectionEstablished = false
private var timeoutDispatchWorkItem: DispatchWorkItem? = nil
init() {
NearbyConnectionManager.shared.startDeviceDiscovery()
NearbyConnectionManager.shared.becomeVisible()
NearbyConnectionManager.shared.addShareExtensionDelegate(self)
}
func addDevice(device: RemoteDeviceInfo) {
withAnimation {
foundDevices.append(device)
}
}
func removeDevice(id: String) {
withAnimation {
foundDevices.removeAll { $0.id == id }
}
}
func connectionWasEstablished(pinCode: String) {
connectionEstablished = true
progressState = String(format: "PinCode".localized(), arguments: [pinCode])
progressValue = 0
}
func connectionFailed(with error: any Error) {
ErrorAlertHandler.shared.showErrorAlert(for: selectedDevice?.name ?? "?", error: error)
progressValue = nil
selectedDevice = nil
}
func transferAccepted() {
progressState = "Sending".localized()
}
func transferProgress(progress: Double) {
progressValue = progress
}
func transferFinished() {
progressValue = 0
selectedDevice = nil
progressState = "TransferFinished".localized()
}
func startTransferWithQrCode(device: RemoteDeviceInfo) {
selectDevice(device: device)
}
func selectDevice(device: RemoteDeviceInfo) {
// already selected, cancel transfer
if device == selectedDevice {
progressValue = 0
progressState = nil
selectedDevice = nil
NearbyConnectionManager.shared.cancelOutgoingTransfer(id: device.id!)
}
else {
progressValue = 0
progressState = "Connecting".localized()
selectedDevice = device
NearbyConnectionManager.shared.startOutgoingTransfer(deviceID: device.id!, delegate: self, urls: urls, textToSend: textToSend)
}
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
import SwiftUI
class ShareViewModel: ObservableObject, OutboundAppDelegate {
class SendModel: ObservableObject, OutboundAppDelegate {
@Published var foundDevices: [RemoteDeviceInfo] = []
@Published var selectedDevice: RemoteDeviceInfo?