MediaMoreOptionsActionSheet.swift 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. /*****************************************************************************
  2. * MediaMoreOptionsActionSheet.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 (VLCMediaMoreOptionsActionSheetDelegate)
  33. protocol MediaMoreOptionsActionSheetDelegate {
  34. func mediaMoreOptionsDidToggleInterfaceLock(state: Bool)
  35. }
  36. @objc (VLCMediaMoreOptionsActionSheet)
  37. class MediaMoreOptionsActionSheet: ActionSheet {
  38. // MARK: Private Instance Properties
  39. private weak var currentChildView: UIView?
  40. @objc weak var moreOptionsDelegate: MediaMoreOptionsActionSheetDelegate?
  41. @objc var interfaceDisabled: Bool = false {
  42. didSet {
  43. collectionView.visibleCells.forEach {
  44. if let cell = $0 as? ActionSheetCell, let id = cell.identifier {
  45. if id == .interfaceLock {
  46. cell.setToggleSwitch(state: interfaceDisabled)
  47. } else {
  48. cell.alpha = interfaceDisabled ? 0.5 : 1
  49. }
  50. }
  51. }
  52. collectionView.allowsSelection = !interfaceDisabled
  53. }
  54. }
  55. private var offScreenFrame: CGRect {
  56. let y = collectionView.frame.origin.y + headerView.cellHeight
  57. let w = collectionView.frame.size.width
  58. let h = collectionView.frame.size.height
  59. return CGRect(x: w, y: y, width: w, height: h)
  60. }
  61. private var leftToRightGesture: UIPanGestureRecognizer {
  62. let leftToRight = UIPanGestureRecognizer(target: self, action: #selector(draggedRight(panGesture:)))
  63. return leftToRight
  64. }
  65. // To be removed when Designs are done for the Filters, Equalizer etc views are added to Figma
  66. lazy private var mockView: UIView = {
  67. let v = UIView()
  68. v.backgroundColor = .green
  69. v.frame = offScreenFrame
  70. return v
  71. }()
  72. lazy private var cellModels: [ActionSheetCellModel] = {
  73. var models: [ActionSheetCellModel] = []
  74. MediaPlayerActionSheetCellIdentifier.allCases.forEach {
  75. var cellModel = ActionSheetCellModel(
  76. title: String(describing: $0),
  77. imageIdentifier: $0.rawValue,
  78. viewToPresent: mockView,
  79. cellIdentifier: $0
  80. )
  81. if $0 == .interfaceLock {
  82. cellModel.accessoryType = .toggleSwitch
  83. cellModel.viewToPresent = nil
  84. }
  85. models.append(cellModel)
  86. }
  87. return models
  88. }()
  89. // MARK: Private Methods
  90. private func add(childView child: UIView) {
  91. UIView.animate(withDuration: 0.3, animations: {
  92. child.frame = self.collectionView.frame
  93. self.addChildToStackView(child)
  94. }) {
  95. (completed) in
  96. child.addGestureRecognizer(self.leftToRightGesture)
  97. self.currentChildView = child
  98. }
  99. }
  100. private func remove(childView child: UIView) {
  101. UIView.animate(withDuration: 0.3, animations: {
  102. child.frame = self.offScreenFrame
  103. }) { (completed) in
  104. child.removeFromSuperview()
  105. child.removeGestureRecognizer(self.leftToRightGesture)
  106. }
  107. }
  108. @objc func removeCurrentChild() {
  109. if let current = currentChildView {
  110. remove(childView: current)
  111. }
  112. }
  113. func setTheme() {
  114. let darkColors = PresentationTheme.darkTheme.colors
  115. collectionView.backgroundColor = darkColors.background
  116. headerView.backgroundColor = darkColors.background
  117. headerView.title.textColor = darkColors.cellTextColor
  118. for cell in collectionView.visibleCells {
  119. if let cell = cell as? ActionSheetCell {
  120. cell.backgroundColor = darkColors.background
  121. cell.name.textColor = darkColors.cellTextColor
  122. cell.icon.tintColor = .orange
  123. // toggleSwitch's tintColor should not be changed
  124. if cell.accessoryType == .disclosureChevron {
  125. cell.accessoryView.tintColor = darkColors.cellDetailTextColor
  126. } else if cell.accessoryType == .checkmark {
  127. cell.accessoryView.tintColor = .orange
  128. }
  129. }
  130. }
  131. collectionView.layoutIfNeeded()
  132. }
  133. /// Animates the removal of the `currentChildViewController` when it is dragged from its left edge to the right
  134. @objc private func draggedRight(panGesture: UIPanGestureRecognizer) {
  135. if let current = currentChildView {
  136. let translation = panGesture.translation(in: view)
  137. let x = translation.x + current.center.x
  138. let halfWidth = current.frame.size.width / 2
  139. panGesture.setTranslation(.zero, in: view)
  140. if panGesture.state == .began || panGesture.state == .changed {
  141. // only enable left-to-right drags
  142. if current.frame.minX + translation.x >= 0 {
  143. current.center = CGPoint(x: x, y: current.center.y)
  144. }
  145. } else if panGesture.state == .ended {
  146. if current.frame.minX > halfWidth {
  147. removeCurrentChild()
  148. } else {
  149. UIView.animate(withDuration: 0.3) {
  150. current.frame = self.collectionView.frame
  151. }
  152. }
  153. }
  154. }
  155. }
  156. // MARK: Overridden superclass methods
  157. // Removed the automatic dismissal of the view when a cell is selected
  158. override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  159. if let delegate = delegate {
  160. if let item = delegate.itemAtIndexPath(indexPath) {
  161. delegate.actionSheet?(collectionView: collectionView, didSelectItem: item, At: indexPath)
  162. action?(item)
  163. }
  164. if let cell = collectionView.cellForItem(at: indexPath) as? ActionSheetCell, cell.accessoryType == .checkmark {
  165. removeActionSheet()
  166. }
  167. }
  168. }
  169. override func viewDidLoad() {
  170. super.viewDidLoad()
  171. // Remove the themeDidChangeNotification set in the superclass
  172. // MovieViewController Video Options should be dark at all times
  173. NotificationCenter.default.removeObserver(self, name: .VLCThemeDidChangeNotification, object: nil)
  174. setTheme()
  175. }
  176. // MARK: Initializers
  177. override init() {
  178. super.init()
  179. delegate = self
  180. dataSource = self
  181. modalPresentationStyle = .custom
  182. setAction { (item) in
  183. if let item = item as? UIView {
  184. self.add(childView: item)
  185. } else {
  186. preconditionFailure("MediaMoreOptionsActionSheet: Cell item could not be casted as UIView")
  187. }
  188. }
  189. setTheme()
  190. }
  191. required init?(coder aDecoder: NSCoder) {
  192. fatalError("init(coder:) has not been implemented")
  193. }
  194. }
  195. extension MediaMoreOptionsActionSheet: ActionSheetDataSource {
  196. func numberOfRows() -> Int {
  197. return cellModels.count
  198. }
  199. func actionSheet(collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  200. guard indexPath.row < cellModels.count else {
  201. assertionFailure("MediaMoreOptionsActionSheet: Out of range.")
  202. return ActionSheetCell()
  203. }
  204. var sheetCell: ActionSheetCell
  205. if let cell = collectionView.dequeueReusableCell(
  206. withReuseIdentifier: ActionSheetCell.identifier,
  207. for: indexPath) as? ActionSheetCell {
  208. sheetCell = cell
  209. sheetCell.configure(withModel: cellModels[indexPath.row])
  210. } else {
  211. assertionFailure("MediaMoreOptionsActionSheet: Could not dequeue reusable cell")
  212. sheetCell = ActionSheetCell(withCellModel: cellModels[indexPath.row])
  213. }
  214. sheetCell.accessoryView.tintColor = PresentationTheme.darkTheme.colors.cellDetailTextColor
  215. sheetCell.delegate = self
  216. return sheetCell
  217. }
  218. }
  219. extension MediaMoreOptionsActionSheet: ActionSheetDelegate {
  220. func itemAtIndexPath(_ indexPath: IndexPath) -> Any? {
  221. if indexPath.row < cellModels.count {
  222. return cellModels[indexPath.row].viewToPresent
  223. }
  224. return nil
  225. }
  226. func headerViewTitle() -> String? {
  227. return NSLocalizedString("MORE_OPTIONS_HEADER_TITLE", comment: "")
  228. }
  229. }
  230. extension MediaMoreOptionsActionSheet: ActionSheetCellDelegate {
  231. func actionSheetCellShouldUpdateColors() -> Bool {
  232. return false
  233. }
  234. func actionSheetCellDidToggleSwitch(for cell: ActionSheetCell, state: Bool) {
  235. assert(moreOptionsDelegate != nil, "MediaMoreOptionsActionSheet: Delegate not set.")
  236. if let identifier = cell.identifier {
  237. if identifier == .interfaceLock {
  238. moreOptionsDelegate?.mediaMoreOptionsDidToggleInterfaceLock(state: state)
  239. }
  240. }
  241. }
  242. }