Refactoring

This commit is contained in:
Leon Böttger committed 2025-10-19 14:01:02 +02:00
1 parent 1ac988a4fc
commit a69a75cb07
12 files changed
+39 -15

No files matched your search

+319
View File
@@ -0,0 +1,319 @@
//
// ReceiveModel.swift
// QuickDrop
//
// Created by Leon Böttger on 16.08.25.
//
import SwiftUI
import StoreKit
import UserNotifications
#if os(macOS)
import BezelNotification
#else
import LUI
#endif
class ReceiveModel: ObservableObject, InboundAppDelegate {
#if os(macOS)
@Published var progress: Double? = nil
private var toastWindow: NSWindow?
private var toastHosting: NSHostingView<QuickDropToastView>?
#endif
let controlPlusScreen: (Bool) -> Void
init(controlPlusScreen: @escaping (Bool) -> Void = { _ in }) {
self.controlPlusScreen = controlPlusScreen
NearbyConnectionManager.shared.mainAppDelegate = self
NearbyConnectionManager.shared.becomeVisible()
}
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, acceptAutomatically: Bool) {
#if os(macOS)
NSApp.activate(ignoringOtherApps: true)
AudioManager.playIncomingFileSound()
#endif
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 os(macOS)
let isMac = true
#else
let isMac = false
#endif
if acceptAutomatically && isMac {
pressAcceptButton(transferID: transfer.id, trustDevice: false)
if transfer.type != .file {
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 = { (trustDevice: Bool) in self.pressAcceptButton(transferID: transferID, trustDevice: trustDevice) }
let secondaryButtonTitle = "Decline".localized()
let secondaryButtonAction = { NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: false, trustDevice: false, storeInTemp: false) }
#if os(macOS)
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = title
alert.informativeText = mainMessage
alert.addButton(withTitle: primaryButtonTitle)
alert.addButton(withTitle: secondaryButtonTitle)
if transfer.allowsToBeAddedAsTrustedDevice {
alert.showsSuppressionButton = true
alert.suppressionButton?.title = "AutoAcceptFromThisDevice".localized()
}
let result = alert.runModal()
if result == .alertFirstButtonReturn {
primaryButtonAction(alert.suppressionButton?.state == .on)
} else if result == .alertSecondButtonReturn {
secondaryButtonAction()
}
#else
// iOS
let alwaysAcceptLabel = transfer.allowsToBeAddedAsTrustedDevice ? "AlwaysAccept".localized() : nil
ProgressAlert.shared.askForUserPermission(title: title, message: mainMessage, acceptLabel: primaryButtonTitle, acceptAlwaysLabel: alwaysAcceptLabel, rejectLabel: secondaryButtonTitle, acceptAutomatically: acceptAutomatically) { accepted in
switch accepted {
case .Accept:
primaryButtonAction(false)
case .AcceptAlways:
primaryButtonAction(true)
case .Decline:
secondaryButtonAction()
}
} onCancel: {
NearbyConnectionManager.shared.cancelTransfer(id: transferID)
}
#endif
}
}
func transferProgress(connectionID: String, progress: Double) {
#if os(iOS)
ProgressAlert.shared.updateProgress(progress)
#else
DispatchQueue.main.async {
self.progress = progress
self.showQuickDropToast(for: connectionID)
}
#endif
}
func connectionWasTerminated(from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: (any Error)?) {
#if os(macOS)
DispatchQueue.main.async {
self.hideQuickDropToast()
self.progress = nil
}
finish()
#else
ProgressAlert.shared.updateProgress(nil) {
finish()
}
#endif
func finish() {
if let error = error {
#if os(iOS)
errorVibration()
#endif
controlPlusScreen(false)
ErrorAlertHandler.shared.showErrorAlert(for: device.name ?? "Android", error: error)
} else {
#if os(iOS)
doubleVibration()
#endif
if wasPlainTextTransfer {
showCopiedToClipboardAlert()
}
let currentCount = Settings.shared.incomingTransmissionCount
#if os(macOS)
if currentCount == 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
SKStoreReviewController.requestReview()
}
}
#endif
Settings.shared.incomingTransmissionCount = currentCount + 1
log("[ReceiveModel] Successful transmission. Current count: \(currentCount)")
}
}
}
private func showCopiedToClipboardAlert() {
#if os(macOS)
DispatchQueue.main.async {
BezelNotification.show(messageText: "InsertedIntoClipboard".localized(), icon: .clipboard)
}
#endif
}
private func pressAcceptButton(transferID: String, trustDevice: Bool) {
if isFileTransferRestricted() {
log("[ReceiveModel] Showing plus screen...")
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, trustDevice: trustDevice, storeInTemp: true)
controlPlusScreen(true)
} else {
NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, trustDevice: trustDevice, storeInTemp: false)
}
}
// MARK: - macOS Progress Overlay
#if os(macOS)
func showQuickDropToast(for connectionID: String) {
guard toastWindow == nil else { return }
let contentView = QuickDropToastView(
receiveModel: self,
onCancel: {
NearbyConnectionManager.shared.cancelTransfer(id: connectionID)
}
)
let hostingView = NSHostingView(rootView: contentView)
hostingView.frame.size = toastViewSize
// Prefer the screen under the mouse, then the key window’s screen, then main
let screen = NSScreen.screens.first { $0.frame.contains(NSEvent.mouseLocation) }
?? NSApp.keyWindow?.screen
?? NSScreen.main
guard let screen else { return }
let frame = screen.frame
let visible = screen.visibleFrame
let marginX: CGFloat = 24
let marginFromTop: CGFloat = 30
// If the menu bar is auto-hidden, visible.maxY == frame.maxY; subtract the status bar thickness to stay clear
let menuBarHidden = abs(frame.maxY - visible.maxY) < 0.5
let extraTopInset = menuBarHidden ? NSStatusBar.system.thickness : 0
let targetX = visible.maxX - toastViewSize.width - marginX
let targetY = visible.maxY - toastViewSize.height - marginFromTop - extraTopInset
// Start slightly above the final spot for a smooth slide-in
let startY = min(frame.maxY + 10, targetY + 10)
let startFrame = CGRect(x: targetX, y: startY, width: toastViewSize.width, height: toastViewSize.height)
let window = NSWindow(contentRect: startFrame,
styleMask: [.borderless],
backing: .buffered,
defer: false)
window.isOpaque = false
window.backgroundColor = .clear
window.hasShadow = true
window.level = .statusBar
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
window.contentView = hostingView
window.ignoresMouseEvents = false
window.alphaValue = 0
window.makeKeyAndOrderFront(nil)
self.toastWindow = window
self.toastHosting = hostingView
NSAnimationContext.runAnimationGroup { ctx in
ctx.duration = 0.35
ctx.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
window.animator().setFrameOrigin(CGPoint(x: targetX, y: targetY))
window.animator().alphaValue = 1
}
}
func hideQuickDropToast() {
guard let window = toastWindow,
let screen = NSScreen.main else { return }
let offscreenY = screen.visibleFrame.origin.y + screen.visibleFrame.height + 20
NSAnimationContext.runAnimationGroup({ ctx in
ctx.duration = 0.35
ctx.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
window.animator().setFrameOrigin(CGPoint(x: window.frame.origin.x, y: offscreenY))
window.animator().alphaValue = 0
}, completionHandler: {
window.orderOut(nil)
self.toastWindow = nil
self.toastHosting = nil
})
}
#endif
}