mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 19:11:10 +00:00
404 lines
14 KiB
Swift
404 lines
14 KiB
Swift
//
|
|
// ShareViewController.swift
|
|
// ShareExtension
|
|
//
|
|
// Created by Grishka on 12.09.2023.
|
|
//
|
|
|
|
import Cocoa
|
|
import Foundation
|
|
import SwiftUI
|
|
import QRCode
|
|
|
|
class ShareViewController: NSViewController, OutboundAppDelegate {
|
|
|
|
private var urls: [URL] = []
|
|
private var textToSend: String? = nil
|
|
private var foundDevices: [RemoteDeviceInfo] = []
|
|
private var chosenDevice: RemoteDeviceInfo?
|
|
private var lastError: Error?
|
|
private var errorAlertHandler = ErrorAlertHandler.shared
|
|
|
|
private var connectionEstablished = false
|
|
private var timeoutDispatchWorkItem: DispatchWorkItem? = nil
|
|
|
|
@IBOutlet var filesIcon: NSImageView?
|
|
@IBOutlet var filesLabel: NSTextField?
|
|
@IBOutlet var loadingOverlay: NSStackView?
|
|
@IBOutlet var largeProgress: NSProgressIndicator?
|
|
@IBOutlet var listView: NSCollectionView?
|
|
@IBOutlet var listViewWrapper: NSView?
|
|
@IBOutlet var contentWrap: NSView?
|
|
@IBOutlet var progressView: NSView?
|
|
@IBOutlet var progressDeviceIcon: NSImageView?
|
|
@IBOutlet var progressDeviceName: NSTextField?
|
|
@IBOutlet var progressProgressBar: NSProgressIndicator?
|
|
@IBOutlet var progressState: NSTextField?
|
|
@IBOutlet var progressDeviceIconWrap: NSView?
|
|
@IBOutlet var progressDeviceSecondaryIcon: NSImageView?
|
|
@IBOutlet var dontSeeDeviceButton: NSButton?
|
|
|
|
private var qrCodeSheetView: NSPanel? = nil
|
|
private var sheetAttachedWindow: NSWindow? = nil
|
|
|
|
|
|
override var nibName: NSNib.Name? {
|
|
return NSNib.Name("ShareViewController")
|
|
}
|
|
|
|
|
|
override func loadView() {
|
|
|
|
super.loadView()
|
|
loadAttachments(with: extensionContext, loadedItems: { result in
|
|
|
|
log("Loaded attachments: \(result)")
|
|
|
|
self.urls = result.urls
|
|
self.textToSend = result.textToSend
|
|
self.filesLabel?.stringValue = result.shortDescription
|
|
self.filesIcon?.image = result.previewImage
|
|
})
|
|
|
|
contentWrap!.addSubview(listViewWrapper!)
|
|
contentWrap!.addSubview(loadingOverlay!)
|
|
contentWrap!.addSubview(progressView!)
|
|
progressView!.isHidden = true
|
|
|
|
listViewWrapper!.translatesAutoresizingMaskIntoConstraints = false
|
|
loadingOverlay!.translatesAutoresizingMaskIntoConstraints = false
|
|
progressView!.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
NSLayoutConstraint.activate([
|
|
NSLayoutConstraint(item: listViewWrapper!, attribute: .width, relatedBy: .equal, toItem: contentWrap, attribute: .width, multiplier: 1, constant: 0),
|
|
NSLayoutConstraint(item: listViewWrapper!, attribute: .height, relatedBy: .equal, toItem: contentWrap, attribute: .height, multiplier: 1, constant: 0),
|
|
|
|
NSLayoutConstraint(item: loadingOverlay!, attribute: .width, relatedBy: .equal, toItem: contentWrap, attribute: .width, multiplier: 1, constant: 0),
|
|
NSLayoutConstraint(item: loadingOverlay!, attribute: .centerY, relatedBy: .equal, toItem: contentWrap, attribute: .centerY, multiplier: 1, constant: 0),
|
|
|
|
NSLayoutConstraint(item: progressView!, attribute: .width, relatedBy: .equal, toItem: contentWrap, attribute: .width, multiplier: 1, constant: 0),
|
|
NSLayoutConstraint(item: progressView!, attribute: .centerY, relatedBy: .equal, toItem: contentWrap, attribute: .centerY, multiplier: 1, constant: 0),
|
|
])
|
|
|
|
largeProgress!.startAnimation(nil)
|
|
|
|
let flowLayout = NSCollectionViewFlowLayout()
|
|
flowLayout.itemSize = NSSize(width: 75, height: 90)
|
|
flowLayout.sectionInset = NSEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
|
|
flowLayout.minimumInteritemSpacing = 10
|
|
flowLayout.minimumLineSpacing = 10
|
|
listView!.collectionViewLayout = flowLayout
|
|
listView!.dataSource = self
|
|
|
|
progressDeviceIconWrap!.wantsLayer = true
|
|
progressDeviceIconWrap!.layer!.masksToBounds = false
|
|
}
|
|
|
|
|
|
override func viewDidLoad() {
|
|
|
|
super.viewDidLoad()
|
|
|
|
dicoverDevices()
|
|
NearbyConnectionManager.shared.addShareExtensionDelegate(self)
|
|
}
|
|
|
|
|
|
override func viewWillDisappear() {
|
|
log("ShareViewController: viewWillDisappear")
|
|
|
|
timeoutDispatchWorkItem?.cancel()
|
|
|
|
if chosenDevice == nil {
|
|
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
|
}
|
|
NearbyConnectionManager.shared.removeShareExtensionDelegate(self)
|
|
}
|
|
|
|
|
|
@IBAction func cancel(_: AnyObject?) {
|
|
if let device = chosenDevice {
|
|
NearbyConnectionManager.shared.cancelOutgoingTransfer(id: device.id!)
|
|
}
|
|
let cancelError = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil)
|
|
extensionContext!.cancelRequest(withError: cancelError)
|
|
}
|
|
|
|
|
|
@IBAction func dontSeeDeviceButton(_: AnyObject?) {
|
|
openQrCodeView()
|
|
}
|
|
|
|
|
|
private func openQrCodeView() {
|
|
if qrCodeSheetView == nil {
|
|
|
|
var image: Image? = nil
|
|
|
|
do {
|
|
let qrKey = NearbyConnectionManager.shared.generateQrCodeKey()
|
|
let qrCodeImage = try QRCode.build
|
|
.text("https://quickshare.google/qrcode#key=\(qrKey)")
|
|
.backgroundColor(CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 0))
|
|
.quietZonePixelCount(3)
|
|
.onPixels.shape(.circle())
|
|
.eye.shape(.squircle())
|
|
.errorCorrection(.low)
|
|
.generate.image(dimension: 1000)
|
|
|
|
image = Image(nsImage: NSImage(cgImage: qrCodeImage, size: qrCodeImage.size))
|
|
}
|
|
catch {
|
|
log("Error generating QR code: \(error)")
|
|
}
|
|
|
|
let contentView = SmallSheetView(type: .sendToDeviceQrCode, dynamicQrCode: image) {
|
|
self.closeQrCodeView()
|
|
}
|
|
|
|
let hostingView = NSHostingView(rootView: contentView)
|
|
|
|
let panel = NSPanel(contentRect: NSRect(x: 0, y: 0, width: smallSheetViewSize.width, height: smallSheetViewSize.height),
|
|
styleMask: [.titled, .closable, .utilityWindow],
|
|
backing: .buffered,
|
|
defer: false)
|
|
|
|
panel.contentView = hostingView
|
|
|
|
qrCodeSheetView = panel
|
|
|
|
if let mainWindow = NSApp.mainWindow {
|
|
sheetAttachedWindow = mainWindow
|
|
mainWindow.beginSheet(panel) { _ in }
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private func closeQrCodeView() {
|
|
if let mainWindow = sheetAttachedWindow, let qrCodeView = qrCodeSheetView {
|
|
mainWindow.endSheet(qrCodeView)
|
|
|
|
qrCodeSheetView = nil
|
|
sheetAttachedWindow = nil
|
|
}
|
|
}
|
|
|
|
|
|
func addDevice(device: RemoteDeviceInfo) {
|
|
|
|
if chosenDevice != nil {
|
|
return
|
|
}
|
|
if foundDevices.isEmpty {
|
|
loadingOverlay?.animator().isHidden = true
|
|
}
|
|
foundDevices.append(device)
|
|
listView?.animator().insertItems(at: [[0, foundDevices.count - 1]])
|
|
|
|
closeQrCodeView()
|
|
}
|
|
|
|
|
|
func removeDevice(id: String) {
|
|
|
|
if chosenDevice != nil {
|
|
return
|
|
}
|
|
for i in foundDevices.indices {
|
|
if foundDevices[i].id == id {
|
|
foundDevices.remove(at: i)
|
|
listView?.animator().deleteItems(at: [[0, i]])
|
|
break
|
|
}
|
|
}
|
|
if foundDevices.isEmpty {
|
|
loadingOverlay?.animator().isHidden = false
|
|
}
|
|
}
|
|
|
|
|
|
func startTransferWithQrCode(device: RemoteDeviceInfo) {
|
|
closeQrCodeView()
|
|
selectDevice(device: device)
|
|
}
|
|
|
|
|
|
func connectionWasEstablished(pinCode: String) {
|
|
connectionEstablished = true
|
|
|
|
progressState?.stringValue = String(format: "PinCode".localized(), arguments: [pinCode])
|
|
progressProgressBar?.isIndeterminate = false
|
|
progressProgressBar?.maxValue = 1000
|
|
progressProgressBar?.doubleValue = 0
|
|
}
|
|
|
|
|
|
func connectionFailed(with error: Error) {
|
|
progressProgressBar?.isIndeterminate = false
|
|
progressProgressBar?.maxValue = 1000
|
|
progressProgressBar?.doubleValue = 0
|
|
lastError = error
|
|
|
|
ErrorAlertHandler.shared.showErrorAlert(for: chosenDevice?.name ?? "", error: error)
|
|
self.extensionContext!.cancelRequest(withError: error)
|
|
}
|
|
|
|
|
|
func transferAccepted() {
|
|
progressState?.stringValue = "Sending".localized()
|
|
}
|
|
|
|
|
|
func transferProgress(progress: Double) {
|
|
progressProgressBar!.doubleValue = progress * progressProgressBar!.maxValue
|
|
}
|
|
|
|
|
|
func transferFinished() {
|
|
progressState?.stringValue = "TransferFinished".localized()
|
|
dismissDelayed()
|
|
}
|
|
|
|
|
|
func selectDevice(device: RemoteDeviceInfo) {
|
|
|
|
listViewWrapper?.animator().isHidden = true
|
|
dontSeeDeviceButton?.animator().isHidden = true
|
|
progressView?.animator().isHidden = false
|
|
progressDeviceName?.stringValue = getDeviceName(device: device)
|
|
progressDeviceIcon?.image = imageForDeviceType(type: device.type)
|
|
progressProgressBar?.startAnimation(nil)
|
|
progressState?.stringValue = "Connecting".localized()
|
|
chosenDevice = device
|
|
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
|
NearbyConnectionManager.shared.startOutgoingTransfer(deviceID: device.id!, delegate: self, urls: urls, textToSend: textToSend)
|
|
|
|
let timeoutAlert = DispatchWorkItem {
|
|
if !self.connectionEstablished {
|
|
AudioManager.playErrorSound()
|
|
let alert = NSAlert()
|
|
alert.alertStyle = .critical
|
|
|
|
alert.messageText = "TimeoutTitle".localized()
|
|
alert.informativeText = "TimeoutDescription".localized()
|
|
alert.addButton(withTitle: "TimeoutButton".localized())
|
|
|
|
alert.beginSheetModal(for: self.view.window!) { _ in }
|
|
}
|
|
}
|
|
|
|
timeoutDispatchWorkItem = timeoutAlert
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0, execute: timeoutAlert)
|
|
}
|
|
|
|
|
|
private func dismissDelayed() {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
|
if let error = self.lastError {
|
|
self.extensionContext!.cancelRequest(withError: error)
|
|
} else {
|
|
self.extensionContext!.completeRequest(returningItems: nil, completionHandler: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private func dicoverDevices() {
|
|
|
|
let bundleIdentifier = "com.leonboettger.neardrop"
|
|
let runningApps = NSWorkspace.shared.runningApplications
|
|
|
|
// Check if the app is already running
|
|
let isRunning = runningApps.contains { $0.bundleIdentifier == bundleIdentifier }
|
|
|
|
if !isRunning {
|
|
log("Launching main app")
|
|
if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) {
|
|
let configuration = NSWorkspace.OpenConfiguration()
|
|
NSWorkspace.shared.openApplication(at: url, configuration: configuration) { app, error in
|
|
if let error = error {
|
|
log("Failed to launch application: \(error)")
|
|
} else {
|
|
log("Application launched successfully")
|
|
}
|
|
}
|
|
} else {
|
|
log("Could not find application with bundle identifier \(bundleIdentifier)")
|
|
}
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
|
NearbyConnectionManager.shared.startDeviceDiscovery()
|
|
self.scheduleAutomaticQrCodeView()
|
|
}
|
|
} else {
|
|
log("Main app is already running")
|
|
NearbyConnectionManager.shared.startDeviceDiscovery()
|
|
scheduleAutomaticQrCodeView()
|
|
}
|
|
}
|
|
|
|
|
|
private func scheduleAutomaticQrCodeView() {
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
|
|
if self.foundDevices.isEmpty {
|
|
self.openQrCodeView()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private func imageForDeviceType(type: RemoteDeviceInfo.DeviceType) -> NSImage {
|
|
let imageName: String
|
|
switch type {
|
|
case .tablet:
|
|
imageName = "com.apple.ipad"
|
|
case .computer:
|
|
imageName = "com.apple.macbookpro-13-unibody"
|
|
default: // also .phone
|
|
imageName = "com.apple.iphone"
|
|
}
|
|
return NSImage(contentsOfFile: "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/\(imageName).icns")!
|
|
}
|
|
|
|
|
|
extension ShareViewController: NSCollectionViewDataSource {
|
|
|
|
func numberOfSections(in _: NSCollectionView) -> Int {
|
|
return 1
|
|
}
|
|
|
|
|
|
func collectionView(_: NSCollectionView, numberOfItemsInSection _: Int) -> Int {
|
|
return foundDevices.count
|
|
}
|
|
|
|
|
|
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
|
|
let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "DeviceListCell"), for: indexPath)
|
|
|
|
guard let collectionViewItem = item as? DeviceListCell else { return item }
|
|
|
|
let device = foundDevices[indexPath[1]]
|
|
|
|
collectionViewItem.textField?.stringValue = getDeviceName(device: device)
|
|
collectionViewItem.imageView?.image = imageForDeviceType(type: device.type)
|
|
collectionViewItem.clickHandler = {
|
|
self.selectDevice(device: device)
|
|
}
|
|
|
|
return collectionViewItem
|
|
}
|
|
|
|
|
|
func getDeviceName(device: RemoteDeviceInfo) -> String {
|
|
if let name = device.name, name.count > 1 {
|
|
return name
|
|
}
|
|
|
|
return "Android"
|
|
}
|
|
}
|