// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import UIKit import CocoaLumberjackSwift class MediaPreviewViewController: UIViewController, UIGestureRecognizerDelegate { @IBOutlet weak var largeCollectionView: UICollectionView! @IBOutlet weak var smallCollectionView: UICollectionView! @IBOutlet weak var middeStackView: UIStackView! @IBOutlet weak var bottomLayoutConstraint: NSLayoutConstraint! @IBOutlet weak var textField: UITextField! @IBOutlet weak var navigationBar: UINavigationBar! @IBOutlet weak var backButton: UIBarButtonItem! @IBOutlet weak var addButton: UIButton! @IBOutlet weak var sendButton: UIButton! @IBOutlet weak var deleteButton: UIBarButtonItem! @IBOutlet weak var moreButton: UIBarButtonItem! @IBOutlet weak var largeCollectionViewContainerView: MediaPreviewCarouselContainerView! var keyboardResize: KeyboardResizeCenterY? var mediaData: [MediaPreviewItem] = [] let mediaFetchQueue = DispatchQueue(label: "MediaDataFetchQueue", qos: .userInitiated, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil) var completion: (([Any], Bool, [String]) -> Void)? var returnToMe: (([DKAsset], [MediaPreviewItem]) -> Void)? var addMore: (([DKAsset], [MediaPreviewItem]) -> Void)? weak var delegate: SendMediaAction? @objc var backIsCancel : Bool = false var mainCollectionViewController : MainCollectionViewController? var miniController: ThumbnailCollectionViewController? var currentItem : IndexPath = IndexPath(item: 0, section: 0) var errorList : [PhotosPickerError] = [] var selection : IndexPath? override func viewDidLoad() { super.viewDidLoad() self.hideKeyboardOnTap() self.textField.placeholder = BundleUtil.localizedString(forKey:"add_caption_to_image") self.textField.delegate = self if backIsCancel { self.backButton.title = BundleUtil.localizedString(forKey:"cancel") } else { self.backButton.title = BundleUtil.localizedString(forKey:"back") } self.sendButton.setTitle(BundleUtil.localizedString(forKey:"send"), for: .normal) NotificationCenter.default.addObserver(self, selector: #selector(self.updateLayoutForKeyboard(notification:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) self.largeCollectionView.delegate = mainCollectionViewController self.largeCollectionView.dataSource = mainCollectionViewController let layout = MediaPreviewFlowLayout() layout.scrollDirection = .horizontal self.largeCollectionView.collectionViewLayout = layout self.largeCollectionView.isPagingEnabled = true self.largeCollectionView.allowsMultipleSelection = false self.smallCollectionView.delegate = miniController! self.smallCollectionView.dataSource = miniController! self.smallCollectionView.allowsMultipleSelection = false self.largeCollectionView.selectItem(at: self.currentItem, animated: true, scrollPosition: .centeredHorizontally) self.smallCollectionView.selectItem(at: self.currentItem, animated: true, scrollPosition: .left) if #available(iOS 11.0, *) { self.smallCollectionView.dragInteractionEnabled = true self.smallCollectionView.dragDelegate = miniController! self.smallCollectionView.dropDelegate = miniController! } self.largeCollectionViewContainerView.delegate = self self.addAccessibilityLabels() self.updateTextForIndex(indexPath: IndexPath(item: 0, section: 0), animated: false) } override func viewWillAppear(_ animated: Bool) { self.largeCollectionView.collectionViewLayout.invalidateLayout() if self.errorList.count > 0 { showError(errorList: self.errorList) self.errorList = [] } if self.mediaData.count > 1 { self.navigationBar.topItem?.title = String(format: BundleUtil.localizedString(forKey:"multiple_media_items"), self.mediaData.count) } else { self.navigationBar.topItem?.title = BundleUtil.localizedString(forKey:"media_item") } } func hideKeyboardOnTap() { let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard)) tap.cancelsTouchesInView = false tap.delegate = self view.addGestureRecognizer(tap) } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { if touch.view == self.sendButton { return false } if touch.view == self.addButton { return false } return true } @objc override func dismissKeyboard() { self.textField.resignFirstResponder() } func addAccessibilityLabels() { self.sendButton.accessibilityLabel = BundleUtil.localizedString(forKey:"send") self.addButton.accessibilityLabel = BundleUtil.localizedString(forKey:"add_more_images") self.textField.accessibilityLabel = BundleUtil.localizedString(forKey:"add_caption_to_image") self.deleteButton.accessibilityLabel = BundleUtil.localizedString(forKey:"remove_current_image_from_selected_images") self.moreButton.accessibilityLabel = BundleUtil.localizedString(forKey:"send_options") if backIsCancel { self.backButton.accessibilityLabel = BundleUtil.localizedString(forKey:"back_to_media_selection") } else { self.backButton.accessibilityLabel = BundleUtil.localizedString(forKey:"cancel") } } deinit { NotificationCenter.default.removeObserver(self) } @objc public func initWithMedia(dataArray: [Any], delegate: SendMediaAction, completion: (([Any], Bool, [String]) -> Void)?, returnToMe: (([DKAsset], [MediaPreviewItem]) -> Void)?, addMore: (([DKAsset], [MediaPreviewItem]) -> Void)?) { self.completion = completion self.returnToMe = returnToMe self.addMore = addMore self.delegate = delegate mainCollectionViewController = MainCollectionViewController(delegate: self) miniController = ThumbnailCollectionViewController() miniController?.parent = self self.resetMediaTo(dataArray: dataArray, reloadData: false) } private func addDataItemFrom(url: URL) { let mimeType = UTIConverter.mimeType(fromUTI: UTIConverter.uti(forFileURL: url)) if UTIConverter.isImageMimeType(mimeType) { let item = ImagePreviewItem(itemUrl: url) self.mediaData.append(item) } else if UTIConverter.isMovieMimeType(mimeType) || UTIConverter.isVideoMimeType(mimeType) { let item = VideoPreviewItem(itemUrl: url) self.mediaData.append(item) } else { self.errorList.append(PhotosPickerError.unknown) } } private func requestAssets() { self.mediaFetchQueue.async { for index in 0.. 0 { showError(errorList: self.errorList) self.errorList = [] } } } private func showError(errorList : [PhotosPickerError]) { let items = errorList.count var title = BundleUtil.localizedString(forKey:"could_not_add_items_title") var message = String(format: BundleUtil.localizedString(forKey:"multiple_media_items_could_not_be_processed"), items) if items == 1 { title = BundleUtil.localizedString(forKey:"could_not_add_all_items_title") message = BundleUtil.localizedString(forKey:"one_media_item_could_not_be_processed") } UIAlertTemplate.showAlert(owner: self, title: title, message: message, actionOk: {_ in if self.mediaData.count == 0 { self.backButtonPressed(self) } }) } func reloadData() { self.largeCollectionView.reloadData() self.smallCollectionView.reloadData() self.updateSelection() } func mediaPreviewItemFromDKAsset(asset : DKAsset) -> MediaPreviewItem { var mediaItem : MediaPreviewItem if asset.isVideo { mediaItem = VideoPreviewItem.init(originalAsset: asset) } else { mediaItem = ImagePreviewItem.init(originalAsset: asset) } return mediaItem } func reloadCollectionViewData() { self.largeCollectionView.reloadData() self.smallCollectionView.reloadData() DispatchQueue.main.async { self.smallCollectionView.selectItem(at: IndexPath(item: 0, section: 0), animated: false, scrollPosition: .left) } } func initProgress() { DispatchQueue.main.async(execute: { let hud = MBProgressHUD.showAdded(to: self.view, animated: true) if hud.progressObject == nil { hud.mode = .annularDeterminate let po = Progress(totalUnitCount: Int64(self.mediaData.count)) hud.progressObject = po hud.label.text = String(format: BundleUtil.localizedString(forKey:"processing_items_progress"), po.completedUnitCount, po.totalUnitCount) } }) } func incrementProgress() { DispatchQueue.main.async(execute: { guard let hud = MBProgressHUD(for: self.view) else { return } if hud.progressObject != nil { guard let po = hud.progressObject else { return } hud.mode = .annularDeterminate po.completedUnitCount += 1 hud.label.text = String(format: BundleUtil.localizedString(forKey:"processing_items_progress"), po.completedUnitCount, po.totalUnitCount) } }) } func presentSizeAlertWithSize(size : Int64) { let size = ByteCountFormatter.string(fromByteCount: size, countStyle: .file) let allowed = ByteCountFormatter.string(fromByteCount: Int64(kMaxFileSize), countStyle: .file) let title = BundleUtil.localizedString(forKey:"item_too_large_title") let message = String(format: BundleUtil.localizedString(forKey:"maximum_file_size_exceeded"), allowed, size) UIAlertTemplate.showAlert(owner: self, title: title, message: message) } @IBAction func sendButtonPressed(_ sender: Any) { self.initProgress() DispatchQueue.global(qos: .userInitiated).async { var returnVal: [Any] = [] var captions: [String] = [] for item in self.mediaData { if item is ImagePreviewItem { if item.originalAsset != nil { guard let originalAsset = item.originalAsset else { continue } guard let asset = originalAsset.originalAsset else { DDLogError("Original Asset is unavailable.") continue } returnVal.append(asset) } else { guard let assetUrl = item.itemUrl else { continue } returnVal.append(assetUrl) } } if item is VideoPreviewItem { guard let videoItem = item as? VideoPreviewItem else { continue } guard let assetUrl : URL = videoItem.getTranscodedItem() else { continue } returnVal.append(assetUrl) } captions.append(item.caption ?? "") self.incrementProgress() } let deadlineTime = DispatchTime.now() + .seconds(1) DispatchQueue.main.asyncAfter(deadline: deadlineTime) { self.hideProgressHud() self.completion?(returnVal, self.mediaData[0].sendAsFile, captions) } } } func hideProgressHud() { DispatchQueue.main.async { MBProgressHUD.hide(for: self.view, animated: true) } } @IBAction func trashTapped(_ sender: Any) { guard let indexPath = self.getCurrentlyVisibleItem() else { return } self.mediaData[indexPath.item].removeItem() _ = self.mediaData.remove(at: indexPath.item) self.largeCollectionView.deleteItems(at: [indexPath]) self.smallCollectionView.deleteItems(at: [indexPath]) if self.mediaData.count == 0 { self.backButtonPressed(self) } else { let newItem = min(indexPath.item, self.mediaData.count - 1) self.currentItem = IndexPath(item:newItem, section: indexPath.section) self.updateSelection() } } override func willRotate(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) { selection = self.getCurrentlyVisibleItem() DispatchQueue.main.asyncAfter(deadline: .now() + duration / 2, execute: { self.largeCollectionView.collectionViewLayout.invalidateLayout() }) } func updateSelection() { guard let indexPath = self.getCurrentlyVisibleItem() else { return } self.updateTextForIndex(indexPath: indexPath, animated: true) self.largeCollectionViewContainerView.currentImage = self.mediaData[min(indexPath.item, self.mediaData.count - 1)] DispatchQueue.main.async { self.smallCollectionView.selectItem(at: indexPath, animated: true, scrollPosition: UICollectionView.ScrollPosition.centeredHorizontally) NotificationCenter.default.post(name: NSNotification.Name(rawValue: kMediaPreviewPauseVideo), object: nil) } } @IBAction func moreButtonPressed(_ sender: Any) { let sb = UIStoryboard(name: "MediaShareStoryboard", bundle: nil) let moreOptionsNavigationController = sb.instantiateViewController(withIdentifier: "moreOptionsNavigationController") (moreOptionsNavigationController.children.first as? MediaShareOptionsViewController)?.setupOptions(options: MediaShareOptionsViewController.ImageSendOptions(sendAsFile: self.mediaData[0].sendAsFile , imageQuality: "")) self.present(moreOptionsNavigationController, animated: true, completion: { (moreOptionsNavigationController.children.first as? MediaShareOptionsViewController)?.delegate = self }) } func updateOptions(imageSendOptions: MediaShareOptionsViewController.ImageSendOptions) { for index in 0...self.mediaData.count - 1 { let item = self.mediaData[index] item.sendAsFile = imageSendOptions.sendAsFile } } @IBAction func smallAddButtonPressed(_ sender: Any) { var returnVal: [DKAsset] = [] for item in self.mediaData { guard let originalAsset = item.originalAsset else { continue } returnVal.append(originalAsset) } self.addMore?(returnVal, self.mediaData) } @IBAction func backButtonPressed(_ sender: Any) { var returnVal: [DKAsset] = [] for item in self.mediaData { guard let originalAsset = item.originalAsset else { continue } returnVal.append(originalAsset) } self.returnToMe?(returnVal, self.mediaData) } @objc static func equals(asset: DKAsset, item: MediaPreviewItem) -> Bool { return item.originalAsset == asset } @objc static func isURLItem(item : MediaPreviewItem) -> Bool { return item.itemUrl != nil } @objc static func contains(asset: DKAsset, itemList: [MediaPreviewItem]) -> Int { for index in 0.. IndexPath? { return self.currentItem } @IBAction func captionEditingChanged(_ sender: Any) { guard let indexPath = self.getCurrentlyVisibleItem() else { return } self.mediaData[indexPath.item].caption = self.textField.text } func updateTextForIndex(indexPath: IndexPath, animated: Bool) { if self.mediaData.count - 1 < indexPath.item { return } DispatchQueue.main.async { let index = indexPath.item let textColor = Colors.fontNormal() let tintColor = Colors.main() if !animated { self.textField.text = self.mediaData[index].caption } else { self.textField.text = self.mediaData[index].caption let fadeOut = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut, animations: { self.textField.textColor = self.textField.backgroundColor self.textField.tintColor = .clear self.textField.text = "" }) let fadeIn = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut, animations: { self.textField.textColor = textColor self.textField.tintColor = tintColor let index = indexPath.item self.textField.text = self.mediaData[index].caption }) fadeOut.addCompletion({_ in fadeIn.startAnimation() }) fadeOut.startAnimation() } } } @objc func updateLayoutForKeyboard(notification: NSNotification) { let prevConst = self.bottomLayoutConstraint?.constant if let userInfo = notification.userInfo { let endFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue let endFrameY = endFrame?.origin.y ?? 0 let duration: TimeInterval = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0 let animationCurveRawNSN = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIView.AnimationOptions.curveEaseInOut.rawValue let animationCurve: UIView.AnimationOptions = UIView.AnimationOptions(rawValue: animationCurveRaw) if endFrameY >= UIScreen.main.bounds.size.height { self.bottomLayoutConstraint?.constant = 0.0 } else { if let keyBoardHeight = endFrame?.size.height { let safeInset: CGFloat if #available(iOS 11.0, *) { safeInset = self.view.safeAreaInsets.bottom } else { safeInset = 0.0 } self.bottomLayoutConstraint?.constant = -(keyBoardHeight - smallCollectionView.frame.height - safeInset) } else { self.bottomLayoutConstraint?.constant = 0.0 } } let layout = UICollectionViewFlowLayout() layout.minimumLineSpacing = 0.0 layout.minimumInteritemSpacing = 0.0 layout.scrollDirection = .horizontal layout.itemSize = self.largeCollectionView.frame.size if self.bottomLayoutConstraint.constant != 0.0 { layout.itemSize.height = self.largeCollectionView.frame.height + self.bottomLayoutConstraint.constant } else { layout.itemSize.height = self.largeCollectionView.frame.height - (prevConst ?? 0.0) } UIView.animate(withDuration: duration, delay: TimeInterval(0), options: animationCurve, animations: { self.view.layoutIfNeeded() self.largeCollectionView.setCollectionViewLayout(layout, animated: false) }, completion:nil) } } } extension MediaPreviewViewController : UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() } }