ShareViewController.swift 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970
  1. //
  2. // ShareViewController.swift
  3. // AppBuilderShare
  4. //
  5. // Created by Qindi on 11/02/25.
  6. //
  7. import UIKit
  8. import Social
  9. import UniformTypeIdentifiers
  10. import AVFoundation
  11. import QuickLook
  12. class ShareViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UITextViewDelegate, QLPreviewControllerDataSource {
  13. let tableView = UITableView()
  14. let searchBar = UISearchBar()
  15. var contacts: [Contact] = []
  16. var filteredContacts: [Contact] = []
  17. var selectedContact: Contact!
  18. private var textViewBottomConstraint: NSLayoutConstraint?
  19. private var containerBottomConstraint: NSLayoutConstraint?
  20. private var heightTextView: NSLayoutConstraint?
  21. let vcHandleText = UIViewController()
  22. let vcHandleImage = UIViewController()
  23. let vcHandleVideo = UIViewController()
  24. let vcHandleFile = UIViewController()
  25. var textView = UITextView()
  26. var typeShareNum = 0
  27. var selectedImage: URL!
  28. var selectedVideo: URL!
  29. var selectedFile: URL!
  30. var selectedImageTypeImage: UIImage!
  31. private var previewView: VideoPreviewView?
  32. let previewController = QLPreviewController()
  33. private var preparingView: UIView!
  34. private var activityIndicator: UIActivityIndicatorView!
  35. private var preparingLabel: UILabel!
  36. let nameGroupShare = "group.nexilis.share"
  37. override func viewDidLoad() {
  38. super.viewDidLoad()
  39. loadCustomContacts()
  40. registerKeyboardNotifications()
  41. }
  42. deinit {
  43. NotificationCenter.default.removeObserver(self) // Remove observers when view controller deallocates
  44. }
  45. override func viewDidAppear(_ animated: Bool) {
  46. setupUI()
  47. }
  48. private func setupPreparingOverlay() {
  49. // full‐screen semi‐transparent background
  50. preparingView = UIView(frame: view.bounds)
  51. preparingView.backgroundColor = UIColor(white: 0, alpha: 0.5)
  52. preparingView.isHidden = true
  53. // spinner
  54. activityIndicator = UIActivityIndicatorView(style: .large)
  55. activityIndicator.translatesAutoresizingMaskIntoConstraints = false
  56. preparingView.addSubview(activityIndicator)
  57. // label
  58. preparingLabel = UILabel()
  59. preparingLabel.translatesAutoresizingMaskIntoConstraints = false
  60. preparingLabel.text = "Preparing…"
  61. preparingLabel.textColor = .white
  62. preparingLabel.font = UIFont.systemFont(ofSize: 17, weight: .medium)
  63. preparingView.addSubview(preparingLabel)
  64. vcHandleVideo.view.addSubview(preparingView)
  65. // constraints: center spinner, label below
  66. NSLayoutConstraint.activate([
  67. activityIndicator.centerXAnchor.constraint(equalTo: preparingView.centerXAnchor),
  68. activityIndicator.centerYAnchor.constraint(equalTo: preparingView.centerYAnchor, constant: -10),
  69. preparingLabel.topAnchor.constraint(equalTo: activityIndicator.bottomAnchor, constant: 12),
  70. preparingLabel.centerXAnchor.constraint(equalTo: preparingView.centerXAnchor)
  71. ])
  72. }
  73. private func showPreparingOverlay(_ show: Bool) {
  74. preparingView.isHidden = !show
  75. if show {
  76. activityIndicator.startAnimating()
  77. self.view.isUserInteractionEnabled = false
  78. } else {
  79. activityIndicator.stopAnimating()
  80. self.view.isUserInteractionEnabled = true
  81. }
  82. }
  83. func setupUI() {
  84. let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(cancelAction))
  85. cancelButton.tintColor = .label
  86. self.navigationItem.leftBarButtonItem = cancelButton
  87. // Search Bar (Right)
  88. searchBar.placeholder = "Search"
  89. searchBar.delegate = self
  90. searchBar.sizeToFit()
  91. self.navigationItem.titleView = searchBar
  92. self.navigationController?.navigationBar.backgroundColor = .systemBackground
  93. self.navigationController?.navigationBar.tintColor = .label
  94. // TableView Setup
  95. tableView.translatesAutoresizingMaskIntoConstraints = false
  96. tableView.delegate = self
  97. tableView.dataSource = self
  98. tableView.register(ContactCell.self, forCellReuseIdentifier: "ContactCell")
  99. tableView.separatorStyle = .singleLine
  100. view.addSubview(tableView)
  101. // Auto Layout Constraints
  102. NSLayoutConstraint.activate([
  103. tableView.topAnchor.constraint(equalTo: view.topAnchor),
  104. tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  105. tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  106. tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
  107. ])
  108. }
  109. func loadCustomContacts() {
  110. if let userDefaults = UserDefaults(suiteName: nameGroupShare),
  111. let value = userDefaults.string(forKey: "shareContacts") {
  112. if let jsonData = value.data(using: .utf8) {
  113. do {
  114. if let jsonArray = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [[String: Any]] {
  115. for json in jsonArray {
  116. let id = json["id"] as? String ?? ""
  117. let name = json["name"] as? String ?? ""
  118. let imageId = json["image"] as? String ?? ""
  119. let type = json["type"] as? Int ?? 0
  120. var profileImage = type == 0 ? UIImage(systemName: "person.fill") : UIImage(systemName: "bubble.right.fill")
  121. if !imageId.isEmpty {
  122. if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: nameGroupShare) {
  123. let sharedFileURL = appGroupURL.appendingPathComponent(imageId)
  124. if FileManager.default.fileExists(atPath: sharedFileURL.path) {
  125. profileImage = UIImage(contentsOfFile: sharedFileURL.path)
  126. }
  127. }
  128. }
  129. contacts.append(Contact(id: id, name: name, profileImage: profileImage, imageId: imageId, typeContact: "\(type)"))
  130. }
  131. filteredContacts = contacts
  132. tableView.reloadData()
  133. }
  134. } catch {
  135. print("Error parsing JSON: \(error)")
  136. }
  137. }
  138. }
  139. }
  140. // TableView DataSource Methods
  141. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  142. return filteredContacts.count
  143. }
  144. func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  145. return 60
  146. }
  147. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  148. let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell", for: indexPath) as! ContactCell
  149. cell.separatorInset = UIEdgeInsets(top: 0, left: 65, bottom: 0, right: 25)
  150. let contact = filteredContacts[indexPath.row]
  151. cell.configure(with: contact)
  152. return cell
  153. }
  154. // Handle Contact Selection
  155. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  156. tableView.deselectRow(at: indexPath, animated: true)
  157. selectedContact = filteredContacts[indexPath.row]
  158. handleSharedContent(selectedContact)
  159. }
  160. // Cancel Button Action
  161. @objc func cancelAction() {
  162. self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
  163. }
  164. @objc func sendAction() {
  165. if let userDefaults = UserDefaults(suiteName: nameGroupShare) {
  166. do {
  167. var dataShared: [String: Any] = [:]
  168. dataShared["typeShare"] = typeShareNum
  169. dataShared["typeContact"] = selectedContact.typeContact
  170. dataShared["idContact"] = selectedContact.id
  171. dataShared["data"] = textView.text
  172. if typeShareNum == TypeShare.image {
  173. let compressedImageName = "Nexilis_image_\(Int(Date().timeIntervalSince1970 * 1000))_\(selectedImage != nil ? selectedImage.lastPathComponent.components(separatedBy: ".")[0] : "SS_Image").jpeg"
  174. let thumbName = "THUMB_Nexilis_image_\(Int(Date().timeIntervalSince1970 * 1000))_\(selectedImage != nil ? selectedImage.lastPathComponent.components(separatedBy: ".")[0] : "SS_Image").jpeg"
  175. if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: nameGroupShare) {
  176. let sharedImageURL = appGroupURL.appendingPathComponent(compressedImageName)
  177. let sharedThumbURL = appGroupURL.appendingPathComponent(thumbName)
  178. if selectedImage != nil {
  179. try? UIImage(contentsOfFile: selectedImage.path)?.jpegData(compressionQuality: 0.25)?.write(to: sharedThumbURL)
  180. if let dataImage = UIImage(contentsOfFile: selectedImage.path)?.jpegData(compressionQuality: 1.0) {
  181. try? dataImage.write(to: sharedImageURL)
  182. }
  183. } else {
  184. try? selectedImageTypeImage?.jpegData(compressionQuality: 0.25)?.write(to: sharedThumbURL)
  185. if let dataImage = selectedImageTypeImage?.jpegData(compressionQuality: 1.0) {
  186. try? dataImage.write(to: sharedImageURL)
  187. }
  188. }
  189. }
  190. dataShared["thumb"] = thumbName
  191. dataShared["image"] = compressedImageName
  192. let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
  193. if let jsonString = String(data: jsonData, encoding: .utf8) {
  194. userDefaults.set(jsonString, forKey: "sharedItem")
  195. userDefaults.synchronize()
  196. let notificationName = "realtimeShareExtensionNexilis" as CFString
  197. CFNotificationCenterPostNotification(
  198. CFNotificationCenterGetDarwinNotifyCenter(),
  199. CFNotificationName(notificationName),
  200. nil,
  201. nil,
  202. true
  203. )
  204. }
  205. } else if typeShareNum == TypeShare.video {
  206. showPreparingOverlay(true)
  207. let dispatchGroup = DispatchGroup()
  208. dispatchGroup.enter()
  209. let compressedURL = NSURL.fileURL(withPath: NSTemporaryDirectory() + UUID().uuidString + ".mp4")
  210. compressVideo(inputURL: selectedVideo,outputURL: compressedURL) { exportSession in
  211. guard let session = exportSession else {
  212. return
  213. }
  214. if session.status == .completed {
  215. dispatchGroup.leave()
  216. guard let compressedData = try? Data(contentsOf: compressedURL) else {
  217. return
  218. }
  219. self.sendVideoToMainApp(compressedData, dataShared)
  220. }
  221. }
  222. dispatchGroup.notify(queue: .main) {
  223. self.showPreparingOverlay(false)
  224. }
  225. } else if typeShareNum == TypeShare.file || typeShareNum == TypeShare.audio {
  226. let fileName = selectedFile.lastPathComponent
  227. if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: nameGroupShare) {
  228. let sharedFileURL = appGroupURL.appendingPathComponent(fileName)
  229. try? Data(contentsOf: selectedFile).write(to: sharedFileURL)
  230. }
  231. dataShared[typeShareNum == TypeShare.audio ? "audio" : "file"] = fileName
  232. let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
  233. if let jsonString = String(data: jsonData, encoding: .utf8) {
  234. userDefaults.set(jsonString, forKey: "sharedItem")
  235. userDefaults.synchronize()
  236. let notificationName = "realtimeShareExtensionNexilis" as CFString
  237. CFNotificationCenterPostNotification(
  238. CFNotificationCenterGetDarwinNotifyCenter(),
  239. CFNotificationName(notificationName),
  240. nil,
  241. nil,
  242. true
  243. )
  244. }
  245. } else {
  246. let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
  247. if let jsonString = String(data: jsonData, encoding: .utf8) {
  248. userDefaults.set(jsonString, forKey: "sharedItem")
  249. userDefaults.synchronize()
  250. let notificationName = "realtimeShareExtensionNexilis" as CFString
  251. CFNotificationCenterPostNotification(
  252. CFNotificationCenterGetDarwinNotifyCenter(),
  253. CFNotificationName(notificationName),
  254. nil,
  255. nil,
  256. true
  257. )
  258. }
  259. }
  260. if typeShareNum != TypeShare.video {
  261. self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
  262. }
  263. } catch {
  264. }
  265. }
  266. // self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
  267. }
  268. private func sendVideoToMainApp(_ data: Data, _ dataShared: [String: Any]) {
  269. do {
  270. var dataShared = dataShared
  271. let originalVideoName = self.selectedVideo.lastPathComponent
  272. let renamedVideoName = "Nexilis_video_\(Int(Date().timeIntervalSince1970 * 1000))_\(originalVideoName.components(separatedBy: ".")[0]).mp4"
  273. let thumbName = "THUMB_Nexilis_video_\(Int(Date().timeIntervalSince1970 * 1000))_\(originalVideoName.components(separatedBy: ".")[0]).jpeg"
  274. if let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: nameGroupShare) {
  275. let sharedVideoURL = appGroupURL.appendingPathComponent(renamedVideoName)
  276. let sharedThumbURL = appGroupURL.appendingPathComponent(thumbName)
  277. try? data.write(to: sharedVideoURL)
  278. let asset = AVURLAsset(url: self.selectedVideo, options: nil)
  279. let imgGenerator = AVAssetImageGenerator(asset: asset)
  280. imgGenerator.appliesPreferredTrackTransform = true
  281. let cgImage = try imgGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil)
  282. let thumbnail = UIImage(cgImage: cgImage)
  283. try? thumbnail.jpegData(compressionQuality: 1.0)?.write(to: sharedThumbURL)
  284. dataShared["thumb"] = thumbName
  285. dataShared["video"] = renamedVideoName
  286. let jsonData = try JSONSerialization.data(withJSONObject: dataShared, options: .prettyPrinted)
  287. if let jsonString = String(data: jsonData, encoding: .utf8) {
  288. let userDefaults = UserDefaults(suiteName: nameGroupShare)
  289. userDefaults!.set(jsonString, forKey: "sharedItem")
  290. userDefaults!.synchronize()
  291. let notificationName = "realtimeShareExtensionNexilis" as CFString
  292. CFNotificationCenterPostNotification(
  293. CFNotificationCenterGetDarwinNotifyCenter(),
  294. CFNotificationName(notificationName),
  295. nil,
  296. nil,
  297. true
  298. )
  299. }
  300. }
  301. self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
  302. } catch {
  303. }
  304. }
  305. func compressImageLikeWhatsApp(_ image: UIImage, maxFileSizeMB: Double = 1.0, maxDimension: CGFloat = 1280) -> Data? {
  306. let resizedImage = resizeImage(image: image, maxDimension: maxDimension)
  307. var compressedData = resizedImage.jpegData(compressionQuality: 0.7) ?? Data()
  308. var imageSizeMB = Double(compressedData.count) / (1024.0 * 1024.0)
  309. while imageSizeMB > maxFileSizeMB {
  310. guard let tempImage = UIImage(data: compressedData) else { break }
  311. compressedData = tempImage.jpegData(compressionQuality: 0.5) ?? compressedData
  312. imageSizeMB = Double(compressedData.count) / (1024.0 * 1024.0)
  313. print("Compressed to: \(imageSizeMB) MB")
  314. }
  315. return compressedData
  316. }
  317. func resizeImage(image: UIImage, maxDimension: CGFloat) -> UIImage {
  318. let size = image.size
  319. let aspectRatio = size.width / size.height
  320. var newSize: CGSize
  321. if aspectRatio > 1 {
  322. newSize = CGSize(width: maxDimension, height: maxDimension / aspectRatio)
  323. } else {
  324. newSize = CGSize(width: maxDimension * aspectRatio, height: maxDimension)
  325. }
  326. let renderer = UIGraphicsImageRenderer(size: newSize)
  327. return renderer.image { _ in
  328. image.draw(in: CGRect(origin: .zero, size: newSize))
  329. }
  330. }
  331. func compressVideo(inputURL: URL,
  332. outputURL: URL,
  333. handler:@escaping (_ exportSession: AVAssetExportSession?) -> Void) {
  334. let urlAsset = AVURLAsset(url: inputURL, options: nil)
  335. guard let exportSession = AVAssetExportSession(asset: urlAsset,
  336. presetName: AVAssetExportPresetMediumQuality) else {
  337. handler(nil)
  338. return
  339. }
  340. exportSession.outputURL = outputURL
  341. exportSession.outputFileType = .mp4
  342. exportSession.shouldOptimizeForNetworkUse = true
  343. exportSession.exportAsynchronously {
  344. handler(exportSession)
  345. }
  346. }
  347. @objc func backAction() {
  348. if let previewView = previewView {
  349. previewView.stopVideo()
  350. }
  351. self.dismiss(animated: false, completion: nil)
  352. }
  353. // SearchBar Delegate Methods
  354. func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
  355. if searchText.isEmpty {
  356. filteredContacts = contacts
  357. } else {
  358. filteredContacts = contacts.filter { $0.name.lowercased().contains(searchText.lowercased()) }
  359. }
  360. tableView.reloadData()
  361. }
  362. private func registerKeyboardNotifications() {
  363. NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
  364. NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
  365. }
  366. @objc private func keyboardWillShow(_ notification: Notification) {
  367. if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
  368. let keyboardHeight = keyboardFrame.height
  369. let info:NSDictionary = notification.userInfo! as NSDictionary
  370. let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
  371. updateTextViewBottomConstraint(-keyboardHeight - 20, duration)
  372. }
  373. }
  374. @objc private func keyboardWillHide(_ notification: Notification) {
  375. let info:NSDictionary = notification.userInfo! as NSDictionary
  376. let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
  377. updateTextViewBottomConstraint(-20, duration) // Reset bottom constraint
  378. }
  379. private func updateTextViewBottomConstraint(_ constant: CGFloat, _ duration: CGFloat) {
  380. if typeShareNum == TypeShare.text {
  381. textViewBottomConstraint?.constant = constant
  382. } else {
  383. containerBottomConstraint?.constant = constant + 20
  384. }
  385. UIView.animate(withDuration: TimeInterval(duration)) { // Smooth animation
  386. self.view.layoutIfNeeded()
  387. }
  388. }
  389. func textViewDidChange(_ textView: UITextView) {
  390. vcHandleText.navigationItem.rightBarButtonItem?.isEnabled = !textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
  391. }
  392. func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
  393. if text == "\n" {
  394. textView.resignFirstResponder()
  395. return false
  396. }
  397. return true
  398. }
  399. func textViewDidChangeSelection(_ textView: UITextView) {
  400. if typeShareNum == TypeShare.text {
  401. return
  402. }
  403. let cursorPosition = textView.caretRect(for: textView.selectedTextRange!.start).origin
  404. let doubleCurrentLine = cursorPosition.y / textView.font!.lineHeight
  405. if doubleCurrentLine.isFinite {
  406. let currentLine = Int(doubleCurrentLine)
  407. UIView.animate(withDuration: 0.3) {
  408. let numberOfLines = textView.textContainer.lineBreakMode == .byWordWrapping ? Int(textView.contentSize.height / textView.font!.lineHeight) - 1 : 1
  409. if currentLine == 0 && numberOfLines == 1 {
  410. self.heightTextView?.constant = 45
  411. } else if currentLine >= 4 {
  412. self.heightTextView?.constant = 95.0
  413. } else if currentLine < 4 && numberOfLines < 5 {
  414. self.heightTextView?.constant = textView.contentSize.height
  415. }
  416. }
  417. }
  418. }
  419. func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
  420. return 1
  421. }
  422. func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
  423. return selectedFile as QLPreviewItem
  424. }
  425. private func buildAppearance(_ contact: Contact, _ viewVc: UIView) {
  426. viewVc.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
  427. let buttonClose = UIButton(type: .system)
  428. buttonClose.setImage(UIImage(systemName: "xmark.circle.fill")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 40)), for: .normal)
  429. buttonClose.tintColor = .gray.withAlphaComponent(0.8)
  430. buttonClose.imageView?.contentMode = .scaleAspectFit
  431. buttonClose.clipsToBounds = true
  432. buttonClose.addTarget(self, action: #selector(backAction), for: .touchUpInside)
  433. viewVc.addSubview(buttonClose)
  434. buttonClose.translatesAutoresizingMaskIntoConstraints = false
  435. NSLayoutConstraint.activate([
  436. buttonClose.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor, constant: 20.0),
  437. buttonClose.topAnchor.constraint(equalTo: viewVc.topAnchor, constant: 20.0),
  438. buttonClose.widthAnchor.constraint(equalToConstant: 40.0),
  439. buttonClose.heightAnchor.constraint(equalToConstant: 40.0),
  440. ])
  441. let containerView = UIView()
  442. containerView.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
  443. viewVc.addSubview(containerView)
  444. containerView.translatesAutoresizingMaskIntoConstraints = false
  445. NSLayoutConstraint.activate([
  446. containerView.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor),
  447. containerView.trailingAnchor.constraint(equalTo: viewVc.trailingAnchor),
  448. containerView.heightAnchor.constraint(equalToConstant: 55)
  449. ])
  450. containerBottomConstraint = containerView.bottomAnchor.constraint(equalTo: viewVc.bottomAnchor)
  451. containerBottomConstraint?.isActive = true
  452. let containerTo = UIView()
  453. containerView.addSubview(containerTo)
  454. containerTo.translatesAutoresizingMaskIntoConstraints = false
  455. containerTo.layer.cornerRadius = 8
  456. containerTo.clipsToBounds = true
  457. containerTo.backgroundColor = .darkGray
  458. NSLayoutConstraint.activate([
  459. containerTo.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 15),
  460. containerTo.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10),
  461. containerTo.heightAnchor.constraint(equalToConstant: 30),
  462. containerTo.widthAnchor.constraint(greaterThanOrEqualToConstant: 50)
  463. ])
  464. let textTo = UILabel()
  465. containerTo.addSubview(textTo)
  466. textTo.text = contact.name
  467. textTo.textColor = .white
  468. textTo.font = .systemFont(ofSize: 15)
  469. textTo.textAlignment = .center
  470. textTo.translatesAutoresizingMaskIntoConstraints = false
  471. NSLayoutConstraint.activate([
  472. textTo.leadingAnchor.constraint(equalTo: containerTo.leadingAnchor, constant: 10),
  473. textTo.trailingAnchor.constraint(equalTo: containerTo.trailingAnchor, constant: -10),
  474. textTo.centerYAnchor.constraint(equalTo: containerTo.centerYAnchor)
  475. ])
  476. let buttonTo = UIButton(type: .system)
  477. buttonTo.setImage(UIImage(systemName: "paperplane.circle.fill")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 35)), for: .normal)
  478. buttonTo.tintColor = .systemBlue
  479. buttonTo.addTarget(self, action: #selector(sendAction), for: .touchUpInside)
  480. containerView.addSubview(buttonTo)
  481. buttonTo.translatesAutoresizingMaskIntoConstraints = false
  482. NSLayoutConstraint.activate([
  483. buttonTo.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -15),
  484. buttonTo.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10),
  485. buttonTo.widthAnchor.constraint(equalToConstant: 35),
  486. buttonTo.heightAnchor.constraint(equalToConstant: 35),
  487. ])
  488. textView = UITextView()
  489. viewVc.addSubview(textView)
  490. textView.textColor = .label
  491. textView.font = .systemFont(ofSize: 17)
  492. textView.textContainerInset = UIEdgeInsets(top: 10.5, left: 15, bottom: 10.5, right: 15)
  493. textView.translatesAutoresizingMaskIntoConstraints = false
  494. textView.layer.cornerRadius = 22.5
  495. textView.clipsToBounds = true
  496. textView.layer.borderColor = UIColor.gray.cgColor
  497. textView.layer.borderWidth = 1
  498. textView.delegate = self
  499. NSLayoutConstraint.activate([
  500. textView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -15),
  501. textView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 15),
  502. textView.bottomAnchor.constraint(equalTo: containerView.topAnchor, constant: -20),
  503. ])
  504. heightTextView = textView.heightAnchor.constraint(equalToConstant: 45)
  505. heightTextView?.isActive = true
  506. }
  507. func handleSharedContent(_ contact: Contact) {
  508. guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem else { return }
  509. for attachment in extensionItem.attachments ?? [] {
  510. if attachment.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
  511. // Handle Text
  512. attachment.loadItem(forTypeIdentifier: UTType.text.identifier, options: nil) { (textItem, error) in
  513. if let sharedText = textItem as? String {
  514. DispatchQueue.main.async { [self] in
  515. typeShareNum = TypeShare.text
  516. if let viewVc = vcHandleText.view {
  517. viewVc.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .black : .white
  518. self.navigationItem.backButtonTitle = ""
  519. let sendButton = UIBarButtonItem(title: "Send", style: .plain, target: self, action: #selector(sendAction))
  520. sendButton.tintColor = .label
  521. vcHandleText.navigationItem.rightBarButtonItem = sendButton
  522. let containerTo = UIView()
  523. viewVc.addSubview(containerTo)
  524. containerTo.translatesAutoresizingMaskIntoConstraints = false
  525. NSLayoutConstraint.activate([
  526. containerTo.topAnchor.constraint(equalTo: viewVc.safeAreaLayoutGuide.topAnchor, constant: 8.0),
  527. containerTo.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor),
  528. containerTo.trailingAnchor.constraint(equalTo: viewVc.trailingAnchor),
  529. containerTo.heightAnchor.constraint(equalToConstant: 44)
  530. ])
  531. containerTo.backgroundColor = self.traitCollection.userInterfaceStyle == .dark ? .systemBackground : .gray
  532. let textTo = UILabel()
  533. containerTo.addSubview(textTo)
  534. textTo.translatesAutoresizingMaskIntoConstraints = false
  535. NSLayoutConstraint.activate([
  536. textTo.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor, constant: 10.0),
  537. textTo.centerYAnchor.constraint(equalTo: containerTo.centerYAnchor)
  538. ])
  539. textTo.text = "To: \(contact.name)"
  540. textTo.font = .systemFont(ofSize: 13)
  541. textTo.textColor = .label
  542. textView = UITextView()
  543. textView.translatesAutoresizingMaskIntoConstraints = false
  544. textView.isScrollEnabled = true
  545. textView.text = sharedText
  546. textView.textColor = .label
  547. textView.font = .systemFont(ofSize: 16)
  548. textView.backgroundColor = .clear
  549. textView.delegate = self
  550. viewVc.addSubview(textView)
  551. NSLayoutConstraint.activate([
  552. textView.leadingAnchor.constraint(equalTo: viewVc.leadingAnchor),
  553. textView.trailingAnchor.constraint(equalTo: viewVc.trailingAnchor),
  554. textView.topAnchor.constraint(equalTo: containerTo.bottomAnchor, constant: 5)
  555. ])
  556. textViewBottomConstraint = textView.bottomAnchor.constraint(equalTo: viewVc.bottomAnchor, constant: -20)
  557. textViewBottomConstraint?.isActive = true
  558. self.navigationController?.pushViewController(vcHandleText, animated: true)
  559. }
  560. }
  561. }
  562. }
  563. return
  564. } else if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
  565. // Handle Image
  566. attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { (imageItem, error) in
  567. if let imageURL = imageItem as? URL {
  568. DispatchQueue.main.async { [self] in
  569. typeShareNum = TypeShare.image
  570. selectedImage = imageURL
  571. if let viewVc = vcHandleImage.view {
  572. let imageView = UIImageView()
  573. imageView.image = UIImage(contentsOfFile: imageURL.path)
  574. imageView.contentMode = .scaleAspectFit
  575. imageView.clipsToBounds = true
  576. viewVc.addSubview(imageView)
  577. imageView.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: self.view.bounds.height - 150)
  578. buildAppearance(contact, viewVc)
  579. vcHandleImage.modalPresentationStyle = .fullScreen
  580. self.navigationController?.present(vcHandleImage, animated: true)
  581. }
  582. }
  583. } else if let data = imageItem as? Data {
  584. DispatchQueue.main.async { [self] in
  585. let image = UIImage(data: data)
  586. typeShareNum = TypeShare.image
  587. selectedImageTypeImage = image
  588. if let viewVc = vcHandleImage.view {
  589. let imageView = UIImageView()
  590. imageView.image = image
  591. imageView.contentMode = .scaleAspectFit
  592. imageView.clipsToBounds = true
  593. viewVc.addSubview(imageView)
  594. imageView.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: self.view.bounds.height - 150)
  595. buildAppearance(contact, viewVc)
  596. vcHandleImage.modalPresentationStyle = .fullScreen
  597. self.navigationController?.present(vcHandleImage, animated: true)
  598. }
  599. }
  600. } else if let image = imageItem as? UIImage {
  601. DispatchQueue.main.async { [self] in
  602. typeShareNum = TypeShare.image
  603. selectedImageTypeImage = image
  604. if let viewVc = vcHandleImage.view {
  605. let imageView = UIImageView()
  606. imageView.image = image
  607. imageView.contentMode = .scaleAspectFit
  608. imageView.clipsToBounds = true
  609. viewVc.addSubview(imageView)
  610. imageView.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: self.view.bounds.height - 150)
  611. buildAppearance(contact, viewVc)
  612. vcHandleImage.modalPresentationStyle = .fullScreen
  613. self.navigationController?.present(vcHandleImage, animated: true)
  614. }
  615. }
  616. }
  617. }
  618. return
  619. } else if attachment.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
  620. // Handle Video
  621. attachment.loadItem(forTypeIdentifier: UTType.movie.identifier, options: nil) { (videoItem, error) in
  622. if let videoURL = videoItem as? URL {
  623. DispatchQueue.main.async { [self] in
  624. typeShareNum = TypeShare.video
  625. selectedVideo = videoURL
  626. if let viewVc = vcHandleVideo.view {
  627. previewView = VideoPreviewView(frame: CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: self.view.bounds.height - 190))
  628. viewVc.addSubview(previewView!)
  629. previewView!.configure(with: videoURL)
  630. buildAppearance(contact, viewVc)
  631. vcHandleVideo.modalPresentationStyle = .fullScreen
  632. self.navigationController?.present(vcHandleVideo, animated: true)
  633. setupPreparingOverlay()
  634. }
  635. }
  636. }
  637. }
  638. return
  639. } else if isSupportedAudioFormat(attachment: attachment) {
  640. attachment.loadItem(forTypeIdentifier: UTType.data.identifier, options: nil) { (fileItem, error) in
  641. if let fileURL = fileItem as? URL {
  642. self.getExactAudioDuration(url: fileURL) { durationFormatted in
  643. let alert = UIAlertController(title: "Send to: \(contact.name)?", message: "File size: \(self.getExactFileSize(url: fileURL)) KB\nDuration: \(durationFormatted)", preferredStyle: .alert)
  644. alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
  645. alert.addAction(UIAlertAction(title: "Send", style: .default, handler: {[self] _ in
  646. typeShareNum = TypeShare.audio
  647. selectedFile = fileURL
  648. sendAction()
  649. }))
  650. DispatchQueue.main.async {
  651. self.navigationController?.present(alert, animated: true, completion: nil)
  652. }
  653. }
  654. }
  655. }
  656. } else if attachment.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
  657. // Handle Other Files
  658. attachment.loadItem(forTypeIdentifier: UTType.data.identifier, options: nil) { (fileItem, error) in
  659. if let fileURL = fileItem as? URL {
  660. DispatchQueue.main.async { [self] in
  661. typeShareNum = TypeShare.file
  662. selectedFile = fileURL
  663. if let viewVc = vcHandleFile.view {
  664. vcHandleFile.addChild(previewController)
  665. previewController.dataSource = self
  666. previewController.view.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: viewVc.bounds.size.height - 190)
  667. previewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  668. viewVc.addSubview(previewController.view)
  669. previewController.didMove(toParent: vcHandleFile)
  670. buildAppearance(contact, viewVc)
  671. vcHandleFile.modalPresentationStyle = .fullScreen
  672. self.navigationController?.present(vcHandleFile, animated: true)
  673. }
  674. }
  675. } else {
  676. attachment.loadItem(forTypeIdentifier: "public.file-url", options: nil) { (urlData, error) in
  677. if let url = urlData as? URL {
  678. DispatchQueue.main.async { [self] in
  679. typeShareNum = TypeShare.file
  680. selectedFile = url
  681. if let viewVc = vcHandleFile.view {
  682. vcHandleFile.addChild(previewController)
  683. previewController.dataSource = self
  684. previewController.view.frame = CGRect(x: 0, y: 70, width: viewVc.bounds.size.width, height: viewVc.bounds.size.height - 190)
  685. previewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  686. viewVc.addSubview(previewController.view)
  687. previewController.didMove(toParent: vcHandleFile)
  688. buildAppearance(contact, viewVc)
  689. vcHandleFile.modalPresentationStyle = .fullScreen
  690. self.navigationController?.present(vcHandleFile, animated: true)
  691. }
  692. }
  693. }
  694. }
  695. }
  696. }
  697. return
  698. }
  699. }
  700. }
  701. func isSupportedAudioFormat(attachment: NSItemProvider) -> Bool {
  702. var supportedAudioTypes: [UTType] = [
  703. .mpeg4Audio,
  704. .mp3,
  705. .aiff,
  706. .wav,
  707. .appleProtectedMPEG4Audio
  708. ]
  709. if let flacType = UTType(filenameExtension: "flac") {
  710. supportedAudioTypes.append(flacType)
  711. }
  712. if let oggType = UTType(filenameExtension: "ogg") {
  713. supportedAudioTypes.append(oggType)
  714. }
  715. if let cafType = UTType(filenameExtension: "caf") {
  716. supportedAudioTypes.append(cafType)
  717. }
  718. if let opusType = UTType(filenameExtension: "opus") {
  719. supportedAudioTypes.append(opusType)
  720. }
  721. return supportedAudioTypes.contains { attachment.hasItemConformingToTypeIdentifier($0.identifier) }
  722. }
  723. func getExactFileSize(url: URL) -> Int64 {
  724. do {
  725. let fileData = try Data(contentsOf: url)
  726. let fileSizeInBytes = Int64(fileData.count)
  727. return (fileSizeInBytes + 1023) / 1000 // Using 1000 instead of 1024
  728. } catch {
  729. print("Error reading file size: \(error)")
  730. }
  731. return 0
  732. }
  733. func getExactAudioDuration(url: URL, completion: @escaping (String) -> Void) {
  734. let asset = AVURLAsset(url: url)
  735. asset.loadValuesAsynchronously(forKeys: ["duration"]) {
  736. DispatchQueue.main.async {
  737. let durationSeconds = CMTimeGetSeconds(asset.duration)
  738. // Apply rounding logic like WhatsApp
  739. let roundedDuration = Int(durationSeconds.rounded(.up))
  740. let minutes = roundedDuration / 60
  741. let seconds = roundedDuration % 60
  742. let formattedDuration = String(format: "%d:%02d", minutes, seconds)
  743. completion(formattedDuration)
  744. }
  745. }
  746. }
  747. }
  748. struct Contact {
  749. let id: String
  750. let name: String
  751. let profileImage: UIImage?
  752. let imageId: String
  753. let typeContact: String
  754. }
  755. class TypeShare {
  756. static let text = 1
  757. static let image = 2
  758. static let video = 3
  759. static let file = 4
  760. static let audio = 5
  761. }
  762. class VideoPreviewView: UIView {
  763. private var player: AVPlayer?
  764. private var playerLayer: AVPlayerLayer?
  765. private var isPlaying = false
  766. private let playPauseButton: UIButton = {
  767. let button = UIButton(type: .system)
  768. button.setImage(UIImage(systemName: "play.fill"), for: .normal)
  769. button.tintColor = .white
  770. button.backgroundColor = UIColor.black.withAlphaComponent(0.5)
  771. button.layer.cornerRadius = 25
  772. button.clipsToBounds = true
  773. return button
  774. }()
  775. override init(frame: CGRect) {
  776. super.init(frame: frame)
  777. setupUI()
  778. }
  779. required init?(coder: NSCoder) {
  780. super.init(coder: coder)
  781. setupUI()
  782. }
  783. private func setupUI() {
  784. playPauseButton.frame = CGRect(x: (bounds.width - 50) / 2, y: (bounds.height - 50) / 2, width: 50, height: 50)
  785. playPauseButton.addTarget(self, action: #selector(playPauseTapped), for: .touchUpInside)
  786. addSubview(playPauseButton)
  787. }
  788. func configure(with url: URL) {
  789. // Setup AVPlayer
  790. player = AVPlayer(url: url)
  791. playerLayer = AVPlayerLayer(player: player)
  792. playerLayer?.frame = bounds
  793. playerLayer?.videoGravity = .resizeAspect
  794. if let playerLayer = playerLayer {
  795. layer.insertSublayer(playerLayer, below: playPauseButton.layer)
  796. }
  797. // Observe when video ends
  798. NotificationCenter.default.addObserver(self, selector: #selector(videoDidEnd), name: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem)
  799. }
  800. @objc private func playPauseTapped() {
  801. guard let player = player else { return }
  802. if isPlaying {
  803. player.pause()
  804. playPauseButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
  805. } else {
  806. player.play()
  807. playPauseButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
  808. }
  809. isPlaying.toggle()
  810. }
  811. @objc private func videoDidEnd() {
  812. guard let player = player else { return }
  813. // Replay video from start
  814. player.seek(to: .zero)
  815. player.play()
  816. playPauseButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
  817. isPlaying = true
  818. }
  819. func stopVideo() {
  820. player?.pause()
  821. player?.seek(to: .zero)
  822. playPauseButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
  823. isPlaying = false
  824. }
  825. deinit {
  826. NotificationCenter.default.removeObserver(self)
  827. }
  828. }
  829. class ContactCell: UITableViewCell {
  830. let profileImageView = UIImageView()
  831. let nameLabel = UILabel()
  832. override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
  833. super.init(style: style, reuseIdentifier: reuseIdentifier)
  834. setupUI()
  835. }
  836. required init?(coder: NSCoder) {
  837. fatalError("init(coder:) has not been implemented")
  838. }
  839. func setupUI() {
  840. profileImageView.translatesAutoresizingMaskIntoConstraints = false
  841. profileImageView.layer.cornerRadius = 25
  842. profileImageView.clipsToBounds = true
  843. profileImageView.contentMode = .center
  844. nameLabel.translatesAutoresizingMaskIntoConstraints = false
  845. nameLabel.font = UIFont.systemFont(ofSize: 16, weight: .medium)
  846. contentView.addSubview(profileImageView)
  847. contentView.addSubview(nameLabel)
  848. NSLayoutConstraint.activate([
  849. profileImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15),
  850. profileImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
  851. profileImageView.widthAnchor.constraint(equalToConstant: 50),
  852. profileImageView.heightAnchor.constraint(equalToConstant: 50),
  853. nameLabel.leadingAnchor.constraint(equalTo: profileImageView.trailingAnchor, constant: 15),
  854. nameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
  855. ])
  856. }
  857. func configure(with contact: Contact) {
  858. nameLabel.text = contact.name
  859. profileImageView.image = contact.profileImage ?? UIImage(systemName: "person.circle")
  860. if !contact.imageId.isEmpty {
  861. profileImageView.contentMode = .scaleAspectFill
  862. }
  863. }
  864. }