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,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
|
||||
}
|
||||
Reference in new issue
Block a user