// // ShareViewController.swift // ShareExtension // // Created by Grishka on 12.09.2023. // import Cocoa import Foundation import SwiftUI class ShareViewController: NSViewController, ShareExtensionDelegate { 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() let item = extensionContext!.inputItems[0] as! NSExtensionItem if let attachments = item.attachments { if let text = item.attributedContentText?.string, attachments.isEmpty { textToSend = text DispatchQueue.main.async { self.zipFolderAndSetUpIcon() } } else { let filteredAttachments = attachments.filter { $0.hasItemConformingToTypeIdentifier(kUTTypeURL as String) } for attachment in filteredAttachments { attachment.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) { data, _ in if let urlData = data as? Data, let url = URL(dataRepresentation: urlData, relativeTo: nil, isAbsolute: false) { self.urls.append(url) } else if let url = data as? NSURL { self.urls.append(url as URL) } if self.urls.count == filteredAttachments.count { DispatchQueue.main.async { self.zipFolderAndSetUpIcon() } } } } } } else { let cancelError = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil) extensionContext!.cancelRequest(withError: cancelError) return } 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 { let contentView = SmallSheetView(type: .sendToDeviceQrCode) { 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 } } private func zipFolderAndSetUpIcon() { for url in urls { if url.isFileURL { let isDirectory = UnsafeMutablePointer.allocate(capacity: 1) if FileManager.default.fileExists(atPath: url.path, isDirectory: isDirectory) && isDirectory.pointee.boolValue { do { let zipUrl = try createZipAtTmp(zipFilename: url.lastPathComponent, fromDirectory: url) let index = urls.firstIndex(of: url) urls[index!] = zipUrl } catch { log("Error creating zip file: \(error)") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { log("Canceling share request because URL \(url) is a directory") AudioManager.playErrorSound() let alert = NSAlert() alert.alertStyle = .critical alert.messageText = "TypeNotSupported".localized() alert.informativeText = "TypeNotSupportedDescription".localized() alert.addButton(withTitle: "TypeNotSupportedButton".localized()) alert.beginSheetModal(for: self.view.window!) { _ in let cancelError = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil) self.extensionContext!.cancelRequest(withError: cancelError) } } return } } } } if let textToSend = textToSend { let maxLength = 50 let cleanedText = textToSend.replacingOccurrences(of: "\n", with: " ").replacingOccurrences(of: "\r", with: "") if cleanedText.count > maxLength { let index = cleanedText.index(cleanedText.startIndex, offsetBy: maxLength) filesLabel!.stringValue = String(cleanedText[.. 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 device.name.count <= 1 { return "Android" } return device.name } }