ShareViewController.swift 41 KB

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