ShareViewController.swift 43 KB

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