import AVKit import AVFoundation import SDWebImage import DcCore protocol ContextMenuItem { var msg: DcMsg { get set } var thumbnailImage: UIImage? { get set } } // MARK: - ContextMenuController class ContextMenuController: UIViewController { let item: ContextMenuItem var msg: DcMsg { return item.msg } init(item: ContextMenuItem) { self.item = item super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - lifecycle override func viewDidLoad() { super.viewDidLoad() let viewType = msg.viewtype var thumbnailView: UIView? switch viewType { case .image: thumbnailView = makeImageView(image: msg.image) case .video: thumbnailView = makeVideoView(videoUrl: msg.fileURL) case .gif: thumbnailView = makeGifView(gifImage: item.thumbnailImage) default: return } guard let contentView = thumbnailView else { return } view.addSubview(contentView) contentView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ contentView.leftAnchor.constraint(equalTo: view.leftAnchor), contentView.rightAnchor.constraint(equalTo: view.rightAnchor), contentView.topAnchor.constraint(equalTo: view.topAnchor), contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } // MARK: - thumbnailView creation private func makeGifView(gifImage: UIImage?) -> UIView? { let view = SDAnimatedImageView() view.contentMode = .scaleAspectFill view.clipsToBounds = true view.backgroundColor = DcColors.defaultBackgroundColor if let image = gifImage { setPreferredContentSize(for: image) } view.image = gifImage return view } private func makeImageView(image: UIImage?) -> UIView? { guard let image = image else { safe_fatalError("unexpected nil value") return nil } let imageView = UIImageView() imageView.clipsToBounds = true imageView.contentMode = .scaleAspectFill imageView.image = image setPreferredContentSize(for: image) return imageView } private func makeVideoView(videoUrl: URL?) -> UIView? { guard let videoUrl = videoUrl, let videoSize = item.thumbnailImage?.size else { return nil } let player = AVPlayer(url: videoUrl) let playerController = AVPlayerViewController() addChild(playerController) view.addSubview(playerController.view) playerController.didMove(toParent: self) playerController.view.backgroundColor = .darkGray playerController.view.clipsToBounds = true playerController.player = player playerController.showsPlaybackControls = false player.play() // truncate edges on top/bottom or sides let resizedHeightFactor = view.frame.height / videoSize.height let resizedWidthFactor = view.frame.width / videoSize.width let effectiveResizeFactor = min(resizedWidthFactor, resizedHeightFactor) let maxHeight = videoSize.height * effectiveResizeFactor let maxWidth = videoSize.width * effectiveResizeFactor let size = CGSize(width: maxWidth, height: maxHeight) preferredContentSize = size return playerController.view } private func setPreferredContentSize(for image: UIImage) { let width = view.bounds.width let height = image.size.height * (width / image.size.width) self.preferredContentSize = CGSize(width: width, height: height) } } class ContextMenuProvider { var menu: [ContextMenuItem] = [] init(menu: [ContextMenuItem] = []) { self.menu = menu } func setMenu(_ menu: [ContextMenuItem]) { self.menu = menu } // iOS 12- action menu var menuItems: [UIMenuItem] { return menu .filter({ $0.title != nil && $0.action != nil }) .map({ return UIMenuItem(title: $0.title!, action: $0.action!) }) } // iOS13+ action menu @available(iOS 13, *) func actionProvider(title: String = "", image: UIImage? = nil, identifier: UIMenu.Identifier? = nil, indexPath: IndexPath) -> UIMenu { var children: [UIMenuElement] = [] for item in menu { //we only support 1 submenu layer for now if let subMenus = item.children { var submenuChildren: [UIMenuElement] = [] for submenuItem in subMenus { submenuChildren.append(generateUIAction(item: submenuItem, indexPath: indexPath)) } let submenu = UIMenu(title: "", options: .displayInline, children: submenuChildren) children.append(submenu) } else { children.append(generateUIAction(item: item, indexPath: indexPath)) } } return UIMenu( title: title, image: image, identifier: identifier, children: children ) } @available(iOS 13, *) private func generateUIAction(item: ContextMenuItem, indexPath: IndexPath) -> UIAction { let image = UIImage(systemName: item.imageName ?? "") ?? UIImage(named: item.imageName ?? "") let action = UIAction( title: item.title ?? "", image: image, handler: { _ in item.onPerform?(indexPath) } ) if item.isDestructive ?? false { action.attributes = [.destructive] } return action } func canPerformAction(action: Selector) -> Bool { return !menu.filter { $0.action == action }.isEmpty } func performAction(action: Selector, indexPath: IndexPath) { menu.filter { $0.action == action }.first?.onPerform?(indexPath) } } extension ContextMenuProvider { struct ContextMenuItem { var title: String? var imageName: String? let isDestructive: Bool? var action: Selector? var onPerform: ((IndexPath) -> Void)? var children: [ContextMenuItem]? init(title: String, imageName: String, isDestructive: Bool = false, action: Selector, onPerform: ((IndexPath) -> Void)?) { self.title = title self.imageName = imageName self.isDestructive = isDestructive self.action = action self.onPerform = onPerform } init(submenuitems: [ContextMenuItem]) { title = nil imageName = nil isDestructive = nil action = nil onPerform = nil children = submenuitems } } }