|
@@ -0,0 +1,755 @@
|
|
|
+//
|
|
|
+// ShareViewController.swift
|
|
|
+// AppBuilderShare
|
|
|
+//
|
|
|
+// Created by Qindi on 11/02/25.
|
|
|
+//
|
|
|
+
|
|
|
+import UIKit
|
|
|
+import Social
|
|
|
+import UniformTypeIdentifiers
|
|
|
+import AVFoundation
|
|
|
+import QuickLook
|
|
|
+
|
|
|
+class ShareViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UITextViewDelegate, QLPreviewControllerDataSource {
|
|
|
+ let tableView = UITableView()
|
|
|
+ let searchBar = UISearchBar()
|
|
|
+ var contacts: [Contact] = []
|
|
|
+ var filteredContacts: [Contact] = []
|
|
|
+ var selectedContact: Contact!
|
|
|
+ private var textViewBottomConstraint: NSLayoutConstraint?
|
|
|
+ private var containerBottomConstraint: NSLayoutConstraint?
|
|
|
+ private var heightTextView: NSLayoutConstraint?
|
|
|
+ let vcHandleText = UIViewController()
|
|
|
+ let vcHandleImage = UIViewController()
|
|
|
+ let vcHandleVideo = UIViewController()
|
|
|
+ let vcHandleFile = UIViewController()
|
|
|
+ var textView = UITextView()
|
|
|
+ var typeShareNum = 0
|
|
|
+ var selectedImage: URL!
|
|
|
+ var selectedVideo: URL!
|
|
|
+ var selectedFile: URL!
|
|
|
+ private var previewView: VideoPreviewView?
|
|
|
+ let previewController = QLPreviewController()
|
|
|
+
|
|
|
+ override func viewDidLoad() {
|
|
|
+ super.viewDidLoad()
|
|
|
+ loadCustomContacts()
|
|
|
+ registerKeyboardNotifications()
|
|
|
+ }
|
|
|
+
|
|
|
+ deinit {
|
|
|
+ NotificationCenter.default.removeObserver(self) // Remove observers when view controller deallocates
|
|
|
+ }
|
|
|
+
|
|
|
+ override func viewDidAppear(_ animated: Bool) {
|
|
|
+ setupUI()
|
|
|
+ }
|
|
|
+
|
|
|
+ func setupUI() {
|
|
|
+ let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(cancelAction))
|
|
|
+ cancelButton.tintColor = .label
|
|
|
+ self.navigationItem.leftBarButtonItem = cancelButton
|
|
|
+
|
|
|
+ // Search Bar (Right)
|
|
|
+ searchBar.placeholder = "Search"
|
|
|
+ searchBar.delegate = self
|
|
|
+ searchBar.sizeToFit()
|
|
|
+ self.navigationItem.titleView = searchBar
|
|
|
+
|
|
|
+ self.navigationController?.navigationBar.backgroundColor = .systemBackground
|
|
|
+ self.navigationController?.navigationBar.tintColor = .label
|
|
|
+
|
|
|
+ // TableView Setup
|
|
|
+ tableView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ tableView.delegate = self
|
|
|
+ tableView.dataSource = self
|
|
|
+ tableView.register(ContactCell.self, forCellReuseIdentifier: "ContactCell")
|
|
|
+ tableView.separatorStyle = .singleLine
|
|
|
+ view.addSubview(tableView)
|
|
|
+
|
|
|
+ // Auto Layout Constraints
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
+ tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
+ tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
+ tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
|
|
+ ])
|
|
|
+ }
|
|
|
+
|
|
|
+ func loadCustomContacts() {
|
|
|
+ if let userDefaults = UserDefaults(suiteName: "group.nexilis.share"),
|
|
|
+ let value = userDefaults.string(forKey: "shareContacts") {
|
|
|
+ if let jsonData = value.data(using: .utf8) {
|
|
|
+ do {
|
|
|
+ if let jsonArray = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [[String: Any]] {
|
|
|
+ for json in jsonArray {
|
|
|
+ let id = json["id"] as? String ?? ""
|
|
|
+ let name = json["name"] as? String ?? ""
|
|
|
+ let imageId = json["image"] as? String ?? ""
|
|
|
+ let type = json["type"] as? Int ?? 0
|
|
|
+ var profileImage = type == 0 ? UIImage(systemName: "person.fill") : UIImage(systemName: "bubble.right.fill")
|
|
|
+ if !imageId.isEmpty {
|
|
|
+ if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.nexilis.share") {
|
|
|
+ let sharedFileURL = appGroupURL.appendingPathComponent(imageId)
|
|
|
+ if FileManager.default.fileExists(atPath: sharedFileURL.path) {
|
|
|
+ profileImage = UIImage(contentsOfFile: sharedFileURL.path)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ contacts.append(Contact(id: id, name: name, profileImage: profileImage, imageId: imageId, typeContact: "\(type)"))
|
|
|
+ }
|
|
|
+ filteredContacts = contacts
|
|
|
+ tableView.reloadData()
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ print("Error parsing JSON: \(error)")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // TableView DataSource Methods
|
|
|
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
|
+ return filteredContacts.count
|
|
|
+ }
|
|
|
+
|
|
|
+ func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
|
+ return 60
|
|
|
+ }
|
|
|
+
|
|
|
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
|
+ let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell", for: indexPath) as! ContactCell
|
|
|
+ cell.separatorInset = UIEdgeInsets(top: 0, left: 65, bottom: 0, right: 25)
|
|
|
+ let contact = filteredContacts[indexPath.row]
|
|
|
+ cell.configure(with: contact)
|
|
|
+ return cell
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle Contact Selection
|
|
|
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
|
+ tableView.deselectRow(at: indexPath, animated: true)
|
|
|
+ selectedContact = filteredContacts[indexPath.row]
|
|
|
+ handleSharedContent(selectedContact)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Cancel Button Action
|
|
|
+ @objc func cancelAction() {
|
|
|
+ self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc func sendAction() {
|
|
|
+ if let userDefaults = UserDefaults(suiteName: "group.nexilis.share") {
|
|
|
+ do {
|
|
|
+ var dataShared: [String: Any] = [:]
|
|
|
+ dataShared["typeShare"] = typeShareNum
|
|
|
+ dataShared["typeContact"] = selectedContact.typeContact
|
|
|
+ dataShared["idContact"] = selectedContact.id
|
|
|
+ dataShared["data"] = textView.text
|
|
|
+ if typeShareNum == TypeShare.image {
|
|
|
+ let compressedImageName = "Nexilis_image_\(Int(Date().timeIntervalSince1970 * 1000))_\(selectedImage.lastPathComponent)"
|
|
|
+ let thumbName = "THUMB_Nexilis_image_\(Int(Date().timeIntervalSince1970 * 1000))_\(selectedImage.lastPathComponent)"
|
|
|
+ if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.nexilis.share") {
|
|
|
+ let sharedImageURL = appGroupURL.appendingPathComponent(compressedImageName)
|
|
|
+ let sharedThumbURL = appGroupURL.appendingPathComponent(thumbName)
|
|
|
+ try? UIImage(contentsOfFile: selectedImage.path)?.jpegData(compressionQuality: 0.25)?.write(to: sharedThumbURL)
|
|
|
+ try? UIImage(contentsOfFile: selectedImage.path)?.jpegData(compressionQuality: 0.5)?.write(to: sharedImageURL)
|
|
|
+ }
|
|
|
+ dataShared["thumb"] = thumbName
|
|
|
+ dataShared["image"] = compressedImageName
|
|
|
+ let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
|
|
|
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
|
+ userDefaults.set(jsonString, forKey: "sharedItem")
|
|
|
+ userDefaults.synchronize()
|
|
|
+ }
|
|
|
+ } else if typeShareNum == TypeShare.video {
|
|
|
+ let dataVideo = try? Data(contentsOf: selectedVideo)
|
|
|
+ if let dataVideotoCompress = dataVideo {
|
|
|
+ let sizeInKB = Double(dataVideotoCompress.count) / 1024.0
|
|
|
+ let sizeOfVideo = sizeInKB / 1024.0
|
|
|
+ if (sizeOfVideo > 10.0) {
|
|
|
+ let compressedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + UUID().uuidString + ".mp4")
|
|
|
+ compressVideo(inputURL: selectedVideo,
|
|
|
+ outputURL: compressedURL) { exportSession in
|
|
|
+ guard let session = exportSession else {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ switch session.status {
|
|
|
+ case .unknown:
|
|
|
+ break
|
|
|
+ case .waiting:
|
|
|
+ break
|
|
|
+ case .exporting:
|
|
|
+ break
|
|
|
+ case .completed:
|
|
|
+ guard let compressedData = try? Data(contentsOf: compressedURL) else {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ self.sendVideoToMainApp(compressedData, dataShared)
|
|
|
+ case .failed:
|
|
|
+ break
|
|
|
+ case .cancelled:
|
|
|
+ break
|
|
|
+ @unknown default:
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return
|
|
|
+ } else {
|
|
|
+ self.sendVideoToMainApp(dataVideotoCompress, dataShared)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if typeShareNum == TypeShare.file {
|
|
|
+ let fileName = selectedFile.lastPathComponent
|
|
|
+ if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.nexilis.share") {
|
|
|
+ let sharedFileURL = appGroupURL.appendingPathComponent(fileName)
|
|
|
+ try? Data(contentsOf: selectedFile).write(to: sharedFileURL)
|
|
|
+ }
|
|
|
+ dataShared["file"] = fileName
|
|
|
+ let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
|
|
|
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
|
+ userDefaults.set(jsonString, forKey: "sharedItem")
|
|
|
+ userDefaults.synchronize()
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
|
|
|
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
|
+ userDefaults.set(jsonString, forKey: "sharedItem")
|
|
|
+ userDefaults.synchronize()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+ self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func sendVideoToMainApp(_ data: Data, _ dataShared: [String: Any]) {
|
|
|
+ do {
|
|
|
+ var dataShared = dataShared
|
|
|
+ let originalVideoName = self.selectedVideo.lastPathComponent
|
|
|
+ let renamedVideoName = "Nexilis_video_\(Int(Date().timeIntervalSince1970 * 1000))_\(originalVideoName)"
|
|
|
+ let thumbName = "THUMB_Nexilis_video_\(Int(Date().timeIntervalSince1970 * 1000))_\(originalVideoName)"
|
|
|
+ if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.nexilis.share") {
|
|
|
+ let sharedVideoURL = appGroupURL.appendingPathComponent(renamedVideoName)
|
|
|
+ let sharedThumbURL = appGroupURL.appendingPathComponent(thumbName)
|
|
|
+ try? data.write(to: sharedVideoURL)
|
|
|
+ let asset = AVURLAsset(url: self.selectedVideo, options: nil)
|
|
|
+ let imgGenerator = AVAssetImageGenerator(asset: asset)
|
|
|
+ imgGenerator.appliesPreferredTrackTransform = true
|
|
|
+ let cgImage = try imgGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil)
|
|
|
+ let thumbnail = UIImage(cgImage: cgImage)
|
|
|
+ try? thumbnail.jpegData(compressionQuality: 1.0)?.write(to: sharedThumbURL)
|
|
|
+ dataShared["thumb"] = thumbName
|
|
|
+ dataShared["video"] = renamedVideoName
|
|
|
+ let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
|
|
|
+ if let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
|
+ let userDefaults = UserDefaults(suiteName: "group.nexilis.share")
|
|
|
+ userDefaults!.set(jsonString, forKey: "sharedItem")
|
|
|
+ userDefaults!.synchronize()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func compressVideo(inputURL: URL,
|
|
|
+ outputURL: URL,
|
|
|
+ handler:@escaping (_ exportSession: AVAssetExportSession?) -> Void) {
|
|
|
+ let urlAsset = AVURLAsset(url: inputURL, options: nil)
|
|
|
+ guard let exportSession = AVAssetExportSession(asset: urlAsset,
|
|
|
+ presetName: AVAssetExportPresetMediumQuality) else {
|
|
|
+ handler(nil)
|
|
|
+
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ exportSession.outputURL = outputURL
|
|
|
+ exportSession.outputFileType = .mp4
|
|
|
+ exportSession.shouldOptimizeForNetworkUse = true
|
|
|
+ exportSession.exportAsynchronously {
|
|
|
+ handler(exportSession)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc func backAction() {
|
|
|
+ if let previewView = previewView {
|
|
|
+ previewView.stopVideo()
|
|
|
+ }
|
|
|
+ self.dismiss(animated: false, completion: nil)
|
|
|
+ }
|
|
|
+
|
|
|
+ // SearchBar Delegate Methods
|
|
|
+ func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
|
|
+ if searchText.isEmpty {
|
|
|
+ filteredContacts = contacts
|
|
|
+ } else {
|
|
|
+ filteredContacts = contacts.filter { $0.name.lowercased().contains(searchText.lowercased()) }
|
|
|
+ }
|
|
|
+ tableView.reloadData()
|
|
|
+ }
|
|
|
+
|
|
|
+ private func registerKeyboardNotifications() {
|
|
|
+ NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
|
|
|
+ NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func keyboardWillShow(_ notification: Notification) {
|
|
|
+ if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
|
|
|
+ let keyboardHeight = keyboardFrame.height
|
|
|
+ let info:NSDictionary = notification.userInfo! as NSDictionary
|
|
|
+ let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
|
|
|
+ updateTextViewBottomConstraint(-keyboardHeight - 20, duration)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func keyboardWillHide(_ notification: Notification) {
|
|
|
+ let info:NSDictionary = notification.userInfo! as NSDictionary
|
|
|
+ let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
|
|
|
+ updateTextViewBottomConstraint(-20, duration) // Reset bottom constraint
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateTextViewBottomConstraint(_ constant: CGFloat, _ duration: CGFloat) {
|
|
|
+ if typeShareNum == TypeShare.text {
|
|
|
+ textViewBottomConstraint?.constant = constant
|
|
|
+ } else {
|
|
|
+ containerBottomConstraint?.constant = constant + 20
|
|
|
+ }
|
|
|
+
|
|
|
+ UIView.animate(withDuration: TimeInterval(duration)) { // Smooth animation
|
|
|
+ self.view.layoutIfNeeded()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func textViewDidChange(_ textView: UITextView) {
|
|
|
+ vcHandleText.navigationItem.rightBarButtonItem?.isEnabled = !textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
|
+ }
|
|
|
+
|
|
|
+ func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
|
|
+ if text == "\n" {
|
|
|
+ textView.resignFirstResponder()
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ func textViewDidChangeSelection(_ textView: UITextView) {
|
|
|
+ if typeShareNum == TypeShare.text {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ let cursorPosition = textView.caretRect(for: textView.selectedTextRange!.start).origin
|
|
|
+ let doubleCurrentLine = cursorPosition.y / textView.font!.lineHeight
|
|
|
+ if doubleCurrentLine.isFinite {
|
|
|
+ let currentLine = Int(doubleCurrentLine)
|
|
|
+ UIView.animate(withDuration: 0.3) {
|
|
|
+ let numberOfLines = textView.textContainer.lineBreakMode == .byWordWrapping ? Int(textView.contentSize.height / textView.font!.lineHeight) - 1 : 1
|
|
|
+ if currentLine == 0 && numberOfLines == 1 {
|
|
|
+ self.heightTextView?.constant = 45
|
|
|
+ } else if currentLine >= 4 {
|
|
|
+ self.heightTextView?.constant = 95.0
|
|
|
+ } else if currentLine < 4 && numberOfLines < 5 {
|
|
|
+ self.heightTextView?.constant = textView.contentSize.height
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
|
|
+ return 1
|
|
|
+ }
|
|
|
+
|
|
|
+ func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
|
|
+ return selectedFile as QLPreviewItem
|
|
|
+ }
|
|
|
+
|
|
|
+ private func buildAppearance(_ contact: Contact, _ viewVc: UIView) {
|
|
|
+ viewVc.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
|
|
|
+ let buttonClose = UIButton(type: .system)
|
|
|
+ buttonClose.setImage(UIImage(systemName: "xmark.circle.fill")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 40)), for: .normal)
|
|
|
+ buttonClose.tintColor = .gray.withAlphaComponent(0.8)
|
|
|
+ buttonClose.imageView?.contentMode = .scaleAspectFit
|
|
|
+ buttonClose.clipsToBounds = true
|
|
|
+ buttonClose.addTarget(self, action: #selector(backAction), for: .touchUpInside)
|
|
|
+ viewVc.addSubview(buttonClose)
|
|
|
+ buttonClose.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ buttonClose.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor, constant: 20.0),
|
|
|
+ buttonClose.topAnchor.constraint(equalTo: viewVc.topAnchor, constant: 20.0),
|
|
|
+ buttonClose.widthAnchor.constraint(equalToConstant: 40.0),
|
|
|
+ buttonClose.heightAnchor.constraint(equalToConstant: 40.0),
|
|
|
+ ])
|
|
|
+
|
|
|
+ let containerView = UIView()
|
|
|
+ containerView.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
|
|
|
+ viewVc.addSubview(containerView)
|
|
|
+ containerView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ containerView.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor),
|
|
|
+ containerView.trailingAnchor.constraint(equalTo: viewVc.trailingAnchor),
|
|
|
+ containerView.heightAnchor.constraint(equalToConstant: 55)
|
|
|
+ ])
|
|
|
+ containerBottomConstraint = containerView.bottomAnchor.constraint(equalTo: viewVc.bottomAnchor)
|
|
|
+ containerBottomConstraint?.isActive = true
|
|
|
+
|
|
|
+ let containerTo = UIView()
|
|
|
+ containerView.addSubview(containerTo)
|
|
|
+ containerTo.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ containerTo.layer.cornerRadius = 8
|
|
|
+ containerTo.clipsToBounds = true
|
|
|
+ containerTo.backgroundColor = .darkGray
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ containerTo.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 15),
|
|
|
+ containerTo.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10),
|
|
|
+ containerTo.heightAnchor.constraint(equalToConstant: 30),
|
|
|
+ containerTo.widthAnchor.constraint(greaterThanOrEqualToConstant: 50)
|
|
|
+ ])
|
|
|
+
|
|
|
+ let textTo = UILabel()
|
|
|
+ containerTo.addSubview(textTo)
|
|
|
+ textTo.text = contact.name
|
|
|
+ textTo.textColor = .white
|
|
|
+ textTo.font = .systemFont(ofSize: 15)
|
|
|
+ textTo.textAlignment = .center
|
|
|
+ textTo.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ textTo.leadingAnchor.constraint(equalTo: containerTo.leadingAnchor, constant: 10),
|
|
|
+ textTo.trailingAnchor.constraint(equalTo: containerTo.trailingAnchor, constant: -10),
|
|
|
+ textTo.centerYAnchor.constraint(equalTo: containerTo.centerYAnchor)
|
|
|
+ ])
|
|
|
+
|
|
|
+ let buttonTo = UIButton(type: .system)
|
|
|
+ buttonTo.setImage(UIImage(systemName: "paperplane.circle.fill")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 35)), for: .normal)
|
|
|
+ buttonTo.tintColor = .systemBlue
|
|
|
+ buttonTo.addTarget(self, action: #selector(sendAction), for: .touchUpInside)
|
|
|
+ containerView.addSubview(buttonTo)
|
|
|
+ buttonTo.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ buttonTo.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -15),
|
|
|
+ buttonTo.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10),
|
|
|
+ buttonTo.widthAnchor.constraint(equalToConstant: 35),
|
|
|
+ buttonTo.heightAnchor.constraint(equalToConstant: 35),
|
|
|
+ ])
|
|
|
+
|
|
|
+ textView = UITextView()
|
|
|
+ viewVc.addSubview(textView)
|
|
|
+ textView.textColor = .white
|
|
|
+ textView.font = .systemFont(ofSize: 17)
|
|
|
+ textView.textContainerInset = UIEdgeInsets(top: 10.5, left: 15, bottom: 10.5, right: 15)
|
|
|
+ textView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ textView.layer.cornerRadius = 22.5
|
|
|
+ textView.clipsToBounds = true
|
|
|
+ textView.layer.borderColor = UIColor.gray.cgColor
|
|
|
+ textView.layer.borderWidth = 1
|
|
|
+ textView.delegate = self
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ textView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -15),
|
|
|
+ textView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 15),
|
|
|
+ textView.bottomAnchor.constraint(equalTo: containerView.topAnchor, constant: -20),
|
|
|
+ ])
|
|
|
+ heightTextView = textView.heightAnchor.constraint(equalToConstant: 45)
|
|
|
+ heightTextView?.isActive = true
|
|
|
+ }
|
|
|
+
|
|
|
+ func handleSharedContent(_ contact: Contact) {
|
|
|
+ guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem else { return }
|
|
|
+
|
|
|
+ for attachment in extensionItem.attachments ?? [] {
|
|
|
+ if attachment.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
|
|
|
+ // Handle Text
|
|
|
+ attachment.loadItem(forTypeIdentifier: UTType.text.identifier, options: nil) { (textItem, error) in
|
|
|
+ if let sharedText = textItem as? String {
|
|
|
+ DispatchQueue.main.async { [self] in
|
|
|
+ typeShareNum = TypeShare.text
|
|
|
+ if let viewVc = vcHandleText.view {
|
|
|
+ viewVc.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
|
|
|
+ self.navigationItem.backButtonTitle = ""
|
|
|
+ let sendButton = UIBarButtonItem(title: "Send", style: .plain, target: self, action: #selector(sendAction))
|
|
|
+ sendButton.tintColor = .label
|
|
|
+ vcHandleText.navigationItem.rightBarButtonItem = sendButton
|
|
|
+
|
|
|
+ let containerTo = UIView()
|
|
|
+ viewVc.addSubview(containerTo)
|
|
|
+ containerTo.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ containerTo.topAnchor.constraint(equalTo: viewVc.safeAreaLayoutGuide.topAnchor, constant: 8.0),
|
|
|
+ containerTo.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor),
|
|
|
+ containerTo.trailingAnchor.constraint(equalTo: viewVc.trailingAnchor),
|
|
|
+ containerTo.heightAnchor.constraint(equalToConstant: 44)
|
|
|
+ ])
|
|
|
+ containerTo.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .systemBackground : .gray
|
|
|
+
|
|
|
+ let textTo = UILabel()
|
|
|
+ containerTo.addSubview(textTo)
|
|
|
+ textTo.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ textTo.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor, constant: 10.0),
|
|
|
+ textTo.centerYAnchor.constraint(equalTo: containerTo.centerYAnchor)
|
|
|
+ ])
|
|
|
+ textTo.text = "To: \(contact.name)"
|
|
|
+ textTo.font = .systemFont(ofSize: 13)
|
|
|
+ textTo.textColor = .label
|
|
|
+
|
|
|
+ textView = UITextView()
|
|
|
+ textView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ textView.isScrollEnabled = true
|
|
|
+ textView.text = sharedText
|
|
|
+ textView.textColor = .label
|
|
|
+ textView.font = .systemFont(ofSize: 16)
|
|
|
+ textView.backgroundColor = .clear
|
|
|
+ textView.delegate = self
|
|
|
+ viewVc.addSubview(textView)
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ textView.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor),
|
|
|
+ textView.trailingAnchor.constraint(equalTo: viewVc.trailingAnchor),
|
|
|
+ textView.topAnchor.constraint(equalTo: containerTo.bottomAnchor, constant: 5)
|
|
|
+ ])
|
|
|
+ textViewBottomConstraint = textView.bottomAnchor.constraint(equalTo: viewVc.bottomAnchor, constant: -20)
|
|
|
+ textViewBottomConstraint?.isActive = true
|
|
|
+
|
|
|
+ self.navigationController?.pushViewController(vcHandleText, animated: true)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return
|
|
|
+ } else if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
|
|
+ // Handle Image
|
|
|
+ attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { (imageItem, error) in
|
|
|
+ if let imageURL = imageItem as? URL {
|
|
|
+ DispatchQueue.main.async { [self] in
|
|
|
+ typeShareNum = TypeShare.image
|
|
|
+ selectedImage = imageURL
|
|
|
+ if let viewVc = vcHandleImage.view {
|
|
|
+ let imageView = UIImageView()
|
|
|
+ imageView.image = UIImage(contentsOfFile: imageURL.path)
|
|
|
+ imageView.contentMode = .scaleAspectFit
|
|
|
+ imageView.clipsToBounds = true
|
|
|
+ viewVc.addSubview(imageView)
|
|
|
+ imageView.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: self.view.bounds.height - 150)
|
|
|
+
|
|
|
+ buildAppearance(contact, viewVc)
|
|
|
+
|
|
|
+ vcHandleImage.modalPresentationStyle = .fullScreen
|
|
|
+ self.navigationController?.present(vcHandleImage, animated: true)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return
|
|
|
+ } else if attachment.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
|
|
|
+ // Handle Video
|
|
|
+ attachment.loadItem(forTypeIdentifier: UTType.movie.identifier, options: nil) { (videoItem, error) in
|
|
|
+ if let videoURL = videoItem as? URL {
|
|
|
+ DispatchQueue.main.async { [self] in
|
|
|
+ typeShareNum = TypeShare.video
|
|
|
+ selectedVideo = videoURL
|
|
|
+ if let viewVc = vcHandleVideo.view {
|
|
|
+ previewView = VideoPreviewView(frame: CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: self.view.bounds.height - 190))
|
|
|
+ viewVc.addSubview(previewView!)
|
|
|
+ previewView!.configure(with: videoURL)
|
|
|
+
|
|
|
+ buildAppearance(contact, viewVc)
|
|
|
+
|
|
|
+ vcHandleVideo.modalPresentationStyle = .fullScreen
|
|
|
+ self.navigationController?.present(vcHandleVideo, animated: true)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return
|
|
|
+ } else if attachment.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
|
|
|
+ // Handle Other Files
|
|
|
+ attachment.loadItem(forTypeIdentifier: UTType.data.identifier, options: nil) { (fileItem, error) in
|
|
|
+ if let fileURL = fileItem as? URL {
|
|
|
+ DispatchQueue.main.async { [self] in
|
|
|
+ typeShareNum = TypeShare.file
|
|
|
+ selectedFile = fileURL
|
|
|
+ if let viewVc = vcHandleFile.view {
|
|
|
+ vcHandleFile.addChild(previewController)
|
|
|
+ previewController.dataSource = self
|
|
|
+ previewController.view.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: viewVc.bounds.size.height - 190)
|
|
|
+ previewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
|
+ viewVc.addSubview(previewController.view)
|
|
|
+ previewController.didMove(toParent: vcHandleFile)
|
|
|
+
|
|
|
+ buildAppearance(contact, viewVc)
|
|
|
+
|
|
|
+ vcHandleFile.modalPresentationStyle = .fullScreen
|
|
|
+ self.navigationController?.present(vcHandleFile, animated: true)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ attachment.loadItem(forTypeIdentifier: "public.file-url", options: nil) { (urlData, error) in
|
|
|
+ if let url = urlData as? URL {
|
|
|
+ DispatchQueue.main.async { [self] in
|
|
|
+ typeShareNum = TypeShare.file
|
|
|
+ selectedFile = url
|
|
|
+ if let viewVc = vcHandleFile.view {
|
|
|
+ vcHandleFile.addChild(previewController)
|
|
|
+ previewController.dataSource = self
|
|
|
+ previewController.view.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: viewVc.bounds.size.height - 190)
|
|
|
+ previewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
|
+ viewVc.addSubview(previewController.view)
|
|
|
+ previewController.didMove(toParent: vcHandleFile)
|
|
|
+
|
|
|
+ buildAppearance(contact, viewVc)
|
|
|
+
|
|
|
+ vcHandleFile.modalPresentationStyle = .fullScreen
|
|
|
+ self.navigationController?.present(vcHandleFile, animated: true)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+struct Contact {
|
|
|
+ let id: String
|
|
|
+ let name: String
|
|
|
+ let profileImage: UIImage?
|
|
|
+ let imageId: String
|
|
|
+ let typeContact: String
|
|
|
+}
|
|
|
+
|
|
|
+class TypeShare {
|
|
|
+ static let text = 1
|
|
|
+ static let image = 2
|
|
|
+ static let video = 3
|
|
|
+ static let file = 4
|
|
|
+}
|
|
|
+
|
|
|
+class VideoPreviewView: UIView {
|
|
|
+
|
|
|
+ private var player: AVPlayer?
|
|
|
+ private var playerLayer: AVPlayerLayer?
|
|
|
+ private var isPlaying = false
|
|
|
+
|
|
|
+ private let playPauseButton: UIButton = {
|
|
|
+ let button = UIButton(type: .system)
|
|
|
+ button.setImage(UIImage(systemName: "play.fill"), for: .normal)
|
|
|
+ button.tintColor = .white
|
|
|
+ button.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
|
|
+ button.layer.cornerRadius = 25
|
|
|
+ button.clipsToBounds = true
|
|
|
+ return button
|
|
|
+ }()
|
|
|
+
|
|
|
+ override init(frame: CGRect) {
|
|
|
+ super.init(frame: frame)
|
|
|
+ setupUI()
|
|
|
+ }
|
|
|
+
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
+ super.init(coder: coder)
|
|
|
+ setupUI()
|
|
|
+ }
|
|
|
+
|
|
|
+ private func setupUI() {
|
|
|
+ playPauseButton.frame = CGRect(x: (bounds.width - 50) / 2, y: (bounds.height - 50) / 2, width: 50, height: 50)
|
|
|
+ playPauseButton.addTarget(self, action: #selector(playPauseTapped), for: .touchUpInside)
|
|
|
+ addSubview(playPauseButton)
|
|
|
+ }
|
|
|
+
|
|
|
+ func configure(with url: URL) {
|
|
|
+ // Setup AVPlayer
|
|
|
+ player = AVPlayer(url: url)
|
|
|
+ playerLayer = AVPlayerLayer(player: player)
|
|
|
+ playerLayer?.frame = bounds
|
|
|
+ playerLayer?.videoGravity = .resizeAspect
|
|
|
+ if let playerLayer = playerLayer {
|
|
|
+ layer.insertSublayer(playerLayer, below: playPauseButton.layer)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Observe when video ends
|
|
|
+ NotificationCenter.default.addObserver(self, selector: #selector(videoDidEnd), name: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem)
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func playPauseTapped() {
|
|
|
+ guard let player = player else { return }
|
|
|
+
|
|
|
+ if isPlaying {
|
|
|
+ player.pause()
|
|
|
+ playPauseButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
|
|
|
+ } else {
|
|
|
+ player.play()
|
|
|
+ playPauseButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
|
|
|
+ }
|
|
|
+
|
|
|
+ isPlaying.toggle()
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func videoDidEnd() {
|
|
|
+ guard let player = player else { return }
|
|
|
+
|
|
|
+ // Replay video from start
|
|
|
+ player.seek(to: .zero)
|
|
|
+ player.play()
|
|
|
+ playPauseButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
|
|
|
+ isPlaying = true
|
|
|
+ }
|
|
|
+
|
|
|
+ func stopVideo() {
|
|
|
+ player?.pause()
|
|
|
+ player?.seek(to: .zero)
|
|
|
+ playPauseButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
|
|
|
+ isPlaying = false
|
|
|
+ }
|
|
|
+
|
|
|
+ deinit {
|
|
|
+ NotificationCenter.default.removeObserver(self)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+class ContactCell: UITableViewCell {
|
|
|
+
|
|
|
+ let profileImageView = UIImageView()
|
|
|
+ let nameLabel = UILabel()
|
|
|
+
|
|
|
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
|
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
|
+ setupUI()
|
|
|
+ }
|
|
|
+
|
|
|
+ required init?(coder: NSCoder) {
|
|
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
+ }
|
|
|
+
|
|
|
+ func setupUI() {
|
|
|
+ profileImageView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ profileImageView.layer.cornerRadius = 25
|
|
|
+ profileImageView.clipsToBounds = true
|
|
|
+ profileImageView.contentMode = .center
|
|
|
+
|
|
|
+ nameLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ nameLabel.font = UIFont.systemFont(ofSize: 16, weight: .medium)
|
|
|
+
|
|
|
+ contentView.addSubview(profileImageView)
|
|
|
+ contentView.addSubview(nameLabel)
|
|
|
+
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ profileImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15),
|
|
|
+ profileImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
|
|
+ profileImageView.widthAnchor.constraint(equalToConstant: 50),
|
|
|
+ profileImageView.heightAnchor.constraint(equalToConstant: 50),
|
|
|
+
|
|
|
+ nameLabel.leadingAnchor.constraint(equalTo: profileImageView.trailingAnchor, constant: 15),
|
|
|
+ nameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
|
|
|
+ ])
|
|
|
+ }
|
|
|
+
|
|
|
+ func configure(with contact: Contact) {
|
|
|
+ nameLabel.text = contact.name
|
|
|
+ profileImageView.image = contact.profileImage ?? UIImage(systemName: "person.circle")
|
|
|
+ if !contact.imageId.isEmpty {
|
|
|
+ profileImageView.contentMode = .scaleAspectFill
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|