MediaPlayerActionSheet.swift 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. /*****************************************************************************
  2. * MediaPlayerActionSheet.swift
  3. *
  4. * Copyright © 2019 VLC authors and VideoLAN
  5. *
  6. * Authors: Robert Gordon <robwaynegordon@gmail.com>
  7. *
  8. *
  9. * Refer to the COPYING file of the official project for license.
  10. *****************************************************************************/
  11. enum MediaPlayerActionSheetCellIdentifier: String, CustomStringConvertible, CaseIterable {
  12. case filter
  13. case playback
  14. case equalizer
  15. case sleepTimer
  16. case interfaceLock
  17. var description: String {
  18. switch self {
  19. case .filter:
  20. return NSLocalizedString("VIDEO_FILTER", comment: "")
  21. case .playback:
  22. return NSLocalizedString("PLAYBACK_SPEED", comment: "")
  23. case .equalizer:
  24. return NSLocalizedString("EQUALIZER_CELL_TITLE", comment: "")
  25. case .sleepTimer:
  26. return NSLocalizedString("BUTTON_SLEEP_TIMER", comment: "")
  27. case .interfaceLock:
  28. return NSLocalizedString("INTERFACE_LOCK_BUTTON", comment: "")
  29. }
  30. }
  31. }
  32. @objc (VLCMediaPlayerActionSheetDataSource)
  33. protocol MediaPlayerActionSheetDataSource {
  34. var configurableCellModels: [ActionSheetCellModel] { get }
  35. }
  36. @objc (VLCMediaPlayerActionSheetDelegate)
  37. protocol MediaPlayerActionSheetDelegate {
  38. func mediaPlayerActionSheetHeaderTitle() -> String?
  39. @objc optional func mediaPlayerDidToggleSwitch(for cell: ActionSheetCell, state: Bool)
  40. }
  41. @objc (VLCMediaPlayerActionSheet)
  42. class MediaPlayerActionSheet: ActionSheet {
  43. // MARK: Private Instance Properties
  44. private weak var currentChildView: UIView?
  45. @objc weak var mediaPlayerActionSheetDelegate: MediaPlayerActionSheetDelegate?
  46. @objc weak var mediaPlayerActionSheetDataSource: MediaPlayerActionSheetDataSource?
  47. var offScreenFrame: CGRect {
  48. let y = collectionView.frame.origin.y + headerView.cellHeight
  49. let w = collectionView.frame.size.width
  50. let h = collectionView.frame.size.height
  51. return CGRect(x: w, y: y, width: w, height: h)
  52. }
  53. private var leftToRightGesture: UIPanGestureRecognizer {
  54. let leftToRight = UIPanGestureRecognizer(target: self, action: #selector(draggedRight(panGesture:)))
  55. return leftToRight
  56. }
  57. // MARK: Private Methods
  58. private func add(childView child: UIView) {
  59. UIView.animate(withDuration: 0.3, animations: {
  60. child.frame = self.collectionView.frame
  61. self.addChildToStackView(child)
  62. }) {
  63. (completed) in
  64. child.addGestureRecognizer(self.leftToRightGesture)
  65. self.currentChildView = child
  66. }
  67. }
  68. private func remove(childView child: UIView) {
  69. UIView.animate(withDuration: 0.3, animations: {
  70. child.frame = self.offScreenFrame
  71. }) { (completed) in
  72. child.removeFromSuperview()
  73. child.removeGestureRecognizer(self.leftToRightGesture)
  74. }
  75. }
  76. @objc func removeCurrentChild() {
  77. if let current = currentChildView {
  78. remove(childView: current)
  79. }
  80. }
  81. func setTheme() {
  82. let darkColors = PresentationTheme.darkTheme.colors
  83. collectionView.backgroundColor = darkColors.background
  84. headerView.backgroundColor = darkColors.background
  85. headerView.title.textColor = darkColors.cellTextColor
  86. for cell in collectionView.visibleCells {
  87. if let cell = cell as? ActionSheetCell {
  88. cell.backgroundColor = darkColors.background
  89. cell.name.textColor = darkColors.cellTextColor
  90. cell.icon.tintColor = .orange
  91. // toggleSwitch's tintColor should not be changed
  92. if cell.accessoryType == .disclosureChevron {
  93. cell.accessoryView.tintColor = darkColors.cellDetailTextColor
  94. } else if cell.accessoryType == .checkmark {
  95. cell.accessoryView.tintColor = .orange
  96. }
  97. }
  98. }
  99. collectionView.layoutIfNeeded()
  100. }
  101. /// Animates the removal of the `currentChildViewController` when it is dragged from its left edge to the right
  102. @objc private func draggedRight(panGesture: UIPanGestureRecognizer) {
  103. if let current = currentChildView {
  104. let translation = panGesture.translation(in: view)
  105. let x = translation.x + current.center.x
  106. let halfWidth = current.frame.size.width / 2
  107. panGesture.setTranslation(.zero, in: view)
  108. if panGesture.state == .began || panGesture.state == .changed {
  109. // only enable left-to-right drags
  110. if current.frame.minX + translation.x >= 0 {
  111. current.center = CGPoint(x: x, y: current.center.y)
  112. }
  113. } else if panGesture.state == .ended {
  114. if current.frame.minX > halfWidth {
  115. removeCurrentChild()
  116. } else {
  117. UIView.animate(withDuration: 0.3) {
  118. current.frame = self.collectionView.frame
  119. }
  120. }
  121. }
  122. }
  123. }
  124. // MARK: Overridden superclass methods
  125. // Removed the automatic dismissal of the view when a cell is selected
  126. override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  127. if let delegate = delegate {
  128. if let item = delegate.itemAtIndexPath(indexPath) {
  129. delegate.actionSheet?(collectionView: collectionView, didSelectItem: item, At: indexPath)
  130. action?(item)
  131. }
  132. if let cell = collectionView.cellForItem(at: indexPath) as? ActionSheetCell, cell.accessoryType == .checkmark {
  133. removeActionSheet()
  134. }
  135. }
  136. }
  137. override func viewDidLoad() {
  138. super.viewDidLoad()
  139. // Remove the themeDidChangeNotification set in the superclass
  140. // MovieViewController Video Options should be dark at all times
  141. NotificationCenter.default.removeObserver(self, name: .VLCThemeDidChangeNotification, object: nil)
  142. }
  143. override func viewWillAppear(_ animated: Bool) {
  144. super.viewWillAppear(animated)
  145. setTheme()
  146. }
  147. // MARK: Initializers
  148. override init() {
  149. super.init()
  150. delegate = self
  151. dataSource = self
  152. modalPresentationStyle = .custom
  153. setAction { (item) in
  154. if let item = item as? UIView {
  155. self.add(childView: item)
  156. } else {
  157. preconditionFailure("MediaMoreOptionsActionSheet: Action:: Item's could not be cased as UIView")
  158. }
  159. }
  160. setTheme()
  161. }
  162. required init?(coder aDecoder: NSCoder) {
  163. fatalError("init(coder:) has not been implemented")
  164. }
  165. }
  166. extension MediaPlayerActionSheet: ActionSheetDataSource {
  167. func numberOfRows() -> Int {
  168. return mediaPlayerActionSheetDataSource?.configurableCellModels.count ?? 0
  169. }
  170. func actionSheet(collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  171. guard let source = mediaPlayerActionSheetDataSource,
  172. indexPath.row < source.configurableCellModels.count else {
  173. preconditionFailure("MediaPlayerActionSheet: mediaPlayerActionSheetDataSource or invalid indexPath")
  174. }
  175. var sheetCell: ActionSheetCell
  176. let cellModel = source.configurableCellModels[indexPath.row]
  177. if let cell = collectionView.dequeueReusableCell(
  178. withReuseIdentifier: ActionSheetCell.identifier,
  179. for: indexPath) as? ActionSheetCell {
  180. sheetCell = cell
  181. sheetCell.configure(withModel: cellModel)
  182. } else {
  183. assertionFailure("MediaMoreOptionsActionSheet: Could not dequeue reusable cell")
  184. sheetCell = ActionSheetCell(withCellModel: cellModel)
  185. }
  186. sheetCell.accessoryView.tintColor = PresentationTheme.darkTheme.colors.cellDetailTextColor
  187. sheetCell.delegate = self
  188. return sheetCell
  189. }
  190. }
  191. extension MediaPlayerActionSheet: ActionSheetDelegate {
  192. func itemAtIndexPath(_ indexPath: IndexPath) -> Any? {
  193. guard let source = mediaPlayerActionSheetDataSource,
  194. indexPath.row < source.configurableCellModels.count else {
  195. preconditionFailure("MediaPlayerActionSheet: mediaPlayerActionSheetDataSource not set")
  196. }
  197. let cellModel = source.configurableCellModels[indexPath.row]
  198. return cellModel.viewToPresent
  199. }
  200. func headerViewTitle() -> String? {
  201. return mediaPlayerActionSheetDelegate?.mediaPlayerActionSheetHeaderTitle()
  202. }
  203. }
  204. extension MediaPlayerActionSheet: ActionSheetCellDelegate {
  205. func actionSheetCellShouldUpdateColors() -> Bool {
  206. return false
  207. }
  208. func actionSheetCellDidToggleSwitch(for cell: ActionSheetCell, state: Bool) {
  209. guard let mediaDelegate = mediaPlayerActionSheetDelegate else {
  210. preconditionFailure("MediaPlayerActionSheet: mediaPlayerActionSheetDelegate not set")
  211. }
  212. mediaDelegate.mediaPlayerDidToggleSwitch?(for: cell, state: state)
  213. }
  214. }