mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 19:11:10 +00:00
WIP: Add abiity to receive using iOS app
This commit is contained in:
1 parent
608ab41c0b
commit
6ec3902788
13 files changed
+473
-214
No files matched your search
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
class ShareViewModel: ObservableObject, OutboundAppDelegate {
|
||||
class SendModel: ObservableObject, OutboundAppDelegate {
|
||||
|
||||
@Published var foundDevices: [RemoteDeviceInfo] = []
|
||||
@Published var selectedDevice: RemoteDeviceInfo?
|
||||
|
||||
Reference in new issue
Block a user