ShareViewController.swift 47 KB

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