EditController.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. /*****************************************************************************
  2. * EditController.swift
  3. *
  4. * Copyright © 2018 VLC authors and VideoLAN
  5. * Copyright © 2018 Videolabs
  6. *
  7. * Authors: Soomin Lee <bubu@mikan.io>
  8. *
  9. * Refer to the COPYING file of the official project for license.
  10. *****************************************************************************/
  11. protocol EditControllerDelegate: class {
  12. func editController(editController: EditController, cellforItemAt indexPath: IndexPath) -> BaseCollectionViewCell?
  13. func editController(editController: EditController, present viewController: UIViewController)
  14. func editControllerDidFinishEditing(editController: EditController?)
  15. }
  16. class EditController: UIViewController {
  17. // Cache selectedIndexPath separately to indexPathsForSelectedItems in order to have persistance
  18. private var selectedCellIndexPaths = Set<IndexPath>()
  19. private let model: MediaLibraryBaseModel
  20. private let mediaLibraryService: MediaLibraryService
  21. private let presentingView: UICollectionView
  22. private lazy var addToPlaylistViewController: AddToPlaylistViewController = {
  23. var addToPlaylistViewController = AddToPlaylistViewController(playlists: mediaLibraryService.playlists())
  24. addToPlaylistViewController.delegate = self
  25. return addToPlaylistViewController
  26. }()
  27. weak var delegate: EditControllerDelegate?
  28. override func loadView() {
  29. let editToolbar = EditToolbar(category: model)
  30. editToolbar.delegate = self
  31. self.view = editToolbar
  32. }
  33. init(mediaLibraryService: MediaLibraryService,
  34. model: MediaLibraryBaseModel,
  35. presentingView: UICollectionView) {
  36. self.mediaLibraryService = mediaLibraryService
  37. self.model = model
  38. self.presentingView = presentingView
  39. super.init(nibName: nil, bundle: nil)
  40. }
  41. required init?(coder aDecoder: NSCoder) {
  42. fatalError("init(coder:) has not been implemented")
  43. }
  44. func resetSelections(resetUI: Bool) {
  45. for indexPath in selectedCellIndexPaths {
  46. presentingView.deselectItem(at: indexPath, animated: true)
  47. if resetUI {
  48. collectionView(presentingView, didDeselectItemAt: indexPath)
  49. }
  50. }
  51. selectedCellIndexPaths.removeAll()
  52. }
  53. }
  54. // MARK: - Helpers
  55. private extension EditController {
  56. private struct TextFieldAlertInfo {
  57. var alertTitle: String
  58. var alertDescription: String
  59. var placeHolder: String
  60. var textfieldText: String
  61. var confirmActionTitle: String
  62. init(alertTitle: String = "",
  63. alertDescription: String = "",
  64. placeHolder: String = "",
  65. textfieldText: String = "",
  66. confirmActionTitle: String = NSLocalizedString("BUTTON_DONE", comment: "")) {
  67. self.alertTitle = alertTitle
  68. self.alertDescription = alertDescription
  69. self.placeHolder = placeHolder
  70. self.textfieldText = textfieldText
  71. self.confirmActionTitle = confirmActionTitle
  72. }
  73. }
  74. private func presentTextFieldAlert(with info: TextFieldAlertInfo,
  75. completionHandler: @escaping (String) -> Void) {
  76. let alertController = UIAlertController(title: info.alertTitle,
  77. message: info.alertDescription,
  78. preferredStyle: .alert)
  79. alertController.addTextField(configurationHandler: {
  80. textField in
  81. textField.text = info.textfieldText
  82. textField.placeholder = info.placeHolder
  83. })
  84. let cancelButton = UIAlertAction(title: NSLocalizedString("BUTTON_CANCEL", comment: ""),
  85. style: .cancel)
  86. let confirmAction = UIAlertAction(title: info.confirmActionTitle, style: .default) {
  87. [weak alertController] _ in
  88. guard let alertController = alertController,
  89. let textField = alertController.textFields?.first else { return }
  90. completionHandler(textField.text ?? "")
  91. }
  92. alertController.addAction(cancelButton)
  93. alertController.addAction(confirmAction)
  94. present(alertController, animated: true, completion: nil)
  95. }
  96. private func createPlaylist(_ name: String) {
  97. guard let playlist = mediaLibraryService.createPlaylist(with: name) else {
  98. assertionFailure("MediaModel: createPlaylist: Failed to create a playlist.")
  99. DispatchQueue.main.async {
  100. VLCAlertViewController.alertViewManager(title: NSLocalizedString("ERROR_PLAYLIST_CREATION",
  101. comment: ""),
  102. viewController: self)
  103. }
  104. return
  105. }
  106. // In the case of Video, Tracks
  107. if let files = model.anyfiles as? [VLCMLMedia] {
  108. for index in selectedCellIndexPaths where index.row < files.count {
  109. playlist.appendMedia(withIdentifier: files[index.row].identifier())
  110. }
  111. } else if let mediaCollectionArray = model.anyfiles as? [MediaCollectionModel] {
  112. for index in selectedCellIndexPaths where index.row < mediaCollectionArray.count {
  113. guard let tracks = mediaCollectionArray[index.row].files() else {
  114. assertionFailure("EditController: Fail to retrieve tracks.")
  115. DispatchQueue.main.async {
  116. VLCAlertViewController.alertViewManager(title: NSLocalizedString("ERROR_PLAYLIST_TRACKS",
  117. comment: ""),
  118. viewController: self)
  119. }
  120. return
  121. }
  122. tracks.forEach() {
  123. playlist.appendMedia(withIdentifier: $0.identifier())
  124. }
  125. }
  126. }
  127. resetSelections(resetUI: true)
  128. delegate?.editControllerDidFinishEditing(editController: self)
  129. }
  130. }
  131. // MARK: - VLCEditToolbarDelegate
  132. extension EditController: EditToolbarDelegate {
  133. func addToNewPlaylist() {
  134. let alertInfo = TextFieldAlertInfo(alertTitle: NSLocalizedString("PLAYLISTS", comment: ""),
  135. placeHolder: NSLocalizedString("PLAYLIST_PLACEHOLDER",
  136. comment: ""))
  137. presentTextFieldAlert(with: alertInfo) {
  138. [unowned self] text -> Void in
  139. guard text != "" else {
  140. DispatchQueue.main.async {
  141. VLCAlertViewController.alertViewManager(title: NSLocalizedString("ERROR_EMPTY_NAME",
  142. comment: ""),
  143. viewController: self)
  144. }
  145. return
  146. }
  147. self.createPlaylist(text)
  148. }
  149. }
  150. func editToolbarDidAddToPlaylist(_ editToolbar: EditToolbar) {
  151. if !mediaLibraryService.playlists().isEmpty && !selectedCellIndexPaths.isEmpty {
  152. addToPlaylistViewController.playlists = mediaLibraryService.playlists()
  153. delegate?.editController(editController: self,
  154. present: addToPlaylistViewController)
  155. } else {
  156. addToNewPlaylist()
  157. }
  158. }
  159. func editToolbarDidDelete(_ editToolbar: EditToolbar) {
  160. guard !selectedCellIndexPaths.isEmpty else {
  161. assertionFailure("EditController: Delete called without selection")
  162. return
  163. }
  164. var objectsToDelete = [VLCMLObject]()
  165. for indexPath in selectedCellIndexPaths.sorted(by: { $0 > $1 }) {
  166. objectsToDelete.append(model.anyfiles[indexPath.row])
  167. }
  168. var message = NSLocalizedString("DELETE_MESSAGE", comment: "")
  169. // Check if we are deleting media inside a playlist
  170. if let collectionModel = model as? CollectionModel {
  171. if collectionModel.mediaCollection is VLCMLPlaylist {
  172. message = NSLocalizedString("DELETE_MESSAGE_PLAYLIST", comment: "")
  173. }
  174. }
  175. let cancelButton = VLCAlertButton(title: NSLocalizedString("BUTTON_CANCEL", comment: ""),
  176. style: .cancel)
  177. let deleteButton = VLCAlertButton(title: NSLocalizedString("BUTTON_DELETE", comment: ""),
  178. style: .destructive,
  179. action: {
  180. [weak self] action in
  181. self?.model.delete(objectsToDelete)
  182. // Update directly the cached indexes since cells will be destroyed
  183. self?.selectedCellIndexPaths.removeAll()
  184. self?.delegate?.editControllerDidFinishEditing(editController: self)
  185. })
  186. VLCAlertViewController.alertViewManager(title: NSLocalizedString("DELETE_TITLE", comment: ""),
  187. errorMessage: message,
  188. viewController: (UIApplication.shared.keyWindow?.rootViewController)!,
  189. buttonsAction: [cancelButton,
  190. deleteButton])
  191. }
  192. func editToolbarDidShare(_ editToolbar: EditToolbar, presentFrom button: UIButton) {
  193. UIApplication.shared.beginIgnoringInteractionEvents()
  194. let rootViewController = UIApplication.shared.keyWindow?.rootViewController
  195. guard let controller = VLCActivityViewControllerVendor
  196. .activityViewController(forFiles: fileURLsFromSelection(),
  197. presenting: button,
  198. presenting: rootViewController,
  199. completionHandler: {
  200. [weak self] completion in
  201. self?.delegate?.editControllerDidFinishEditing(editController: self)
  202. }) else {
  203. UIApplication.shared.endIgnoringInteractionEvents()
  204. return
  205. }
  206. controller.popoverPresentationController?.sourceView = editToolbar
  207. rootViewController?.present(controller, animated: true) {
  208. UIApplication.shared.endIgnoringInteractionEvents()
  209. }
  210. }
  211. func fileURLsFromSelection() -> [URL] {
  212. var fileURLS = [URL]()
  213. for indexPath in selectedCellIndexPaths {
  214. let file = model.anyfiles[indexPath.row]
  215. if let collection = file as? MediaCollectionModel,
  216. let files = collection.files() {
  217. files.forEach {
  218. if let mainFile = $0.mainFile() {
  219. fileURLS.append(mainFile.mrl)
  220. }
  221. }
  222. } else if let media = file as? VLCMLMedia, let mainFile = media.mainFile() {
  223. fileURLS.append(mainFile.mrl)
  224. } else {
  225. assertionFailure("we're trying to share something that doesn't have an mrl")
  226. return fileURLS
  227. }
  228. }
  229. return fileURLS
  230. }
  231. func editToolbarDidRename(_ editToolbar: EditToolbar) {
  232. guard let indexPath = selectedCellIndexPaths.first else {
  233. assertionFailure("EditController: Rename called without selection.")
  234. return
  235. }
  236. var mlObjectName = ""
  237. let mlObject = model.anyfiles[indexPath.row]
  238. if let media = mlObject as? VLCMLMedia {
  239. mlObjectName = media.title
  240. } else if let playlist = mlObject as? VLCMLPlaylist {
  241. mlObjectName = playlist.name
  242. } else {
  243. assertionFailure("EditController: Rename called with wrong model.")
  244. }
  245. // Not using VLCAlertViewController to have more customization in text fields
  246. let alertInfo = TextFieldAlertInfo(alertTitle: String(format: NSLocalizedString("RENAME_MEDIA_TO", comment: ""), mlObjectName),
  247. textfieldText: mlObjectName,
  248. confirmActionTitle: NSLocalizedString("BUTTON_RENAME", comment: ""))
  249. presentTextFieldAlert(with: alertInfo, completionHandler: {
  250. [weak self] text -> Void in
  251. guard text != "" else {
  252. VLCAlertViewController.alertViewManager(title: NSLocalizedString("ERROR_RENAME_FAILED", comment: ""),
  253. errorMessage: NSLocalizedString("ERROR_EMPTY_NAME", comment: ""),
  254. viewController: (UIApplication.shared.keyWindow?.rootViewController)!)
  255. return
  256. }
  257. let mlObject = self?.model.anyfiles[indexPath.row]
  258. if let media = mlObject as? VLCMLMedia {
  259. media.updateTitle(text)
  260. } else if let playlist = mlObject as? VLCMLPlaylist {
  261. playlist.updateName(text)
  262. }
  263. guard let strongself = self else {
  264. return
  265. }
  266. strongself.presentingView.deselectItem(at: indexPath, animated: true)
  267. strongself.collectionView(strongself.presentingView, didDeselectItemAt: indexPath)
  268. //call until all items are renamed
  269. if !strongself.selectedCellIndexPaths.isEmpty {
  270. strongself.editToolbarDidRename(editToolbar)
  271. } else {
  272. strongself.delegate?.editControllerDidFinishEditing(editController: self)
  273. }
  274. })
  275. }
  276. }
  277. // MARK: - UICollectionViewDelegate
  278. extension EditController: UICollectionViewDelegate {
  279. func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  280. selectedCellIndexPaths.insert(indexPath)
  281. // Isolate selectionViewOverlay changes inside EditController
  282. if let cell = collectionView.cellForItem(at: indexPath) as? BaseCollectionViewCell {
  283. cell.selectionViewOverlay?.isHidden = false
  284. }
  285. }
  286. func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
  287. selectedCellIndexPaths.remove(indexPath)
  288. if let cell = collectionView.cellForItem(at: indexPath) as? BaseCollectionViewCell {
  289. cell.selectionViewOverlay?.isHidden = true
  290. }
  291. }
  292. }
  293. // MARK: - UICollectionViewDataSource
  294. extension EditController: UICollectionViewDataSource {
  295. func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  296. return model.anyfiles.count
  297. }
  298. func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  299. if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: model.cellType.defaultReuseIdentifier,
  300. for: indexPath) as? BaseCollectionViewCell {
  301. cell.media = model.anyfiles[indexPath.row]
  302. cell.isSelected = selectedCellIndexPaths.contains(indexPath)
  303. cell.isAccessibilityElement = true
  304. cell.checkImageView?.isHidden = false
  305. if let cell = cell as? MediaCollectionViewCell,
  306. let collectionModel = model as? CollectionModel, collectionModel.mediaCollection is VLCMLPlaylist {
  307. cell.dragIndicatorImageView.isHidden = false
  308. }
  309. if cell.isSelected {
  310. cell.selectionViewOverlay?.isHidden = false
  311. }
  312. return cell
  313. } else {
  314. assertionFailure("We couldn't dequeue a reusable cell, the cell might not be registered or is not a MediaEditCell")
  315. return UICollectionViewCell()
  316. }
  317. }
  318. func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
  319. guard let collectionModel = model as? CollectionModel, let playlist = collectionModel.mediaCollection as? VLCMLPlaylist else {
  320. assertionFailure("can Move should've been false")
  321. return
  322. }
  323. playlist.moveMedia(fromPosition: UInt32(sourceIndexPath.row), toDestination: UInt32(destinationIndexPath.row))
  324. }
  325. func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
  326. if let collectionModel = model as? CollectionModel, collectionModel.mediaCollection is VLCMLPlaylist {
  327. return true
  328. }
  329. return false
  330. }
  331. }
  332. // MARK: - UICollectionViewDelegateFlowLayout
  333. extension EditController: UICollectionViewDelegateFlowLayout {
  334. func collectionView(_ collectionView: UICollectionView,
  335. layout collectionViewLayout: UICollectionViewLayout,
  336. sizeForItemAt indexPath: IndexPath) -> CGSize {
  337. var toWidth = collectionView.frame.size.width
  338. if #available(iOS 11.0, *) {
  339. toWidth = collectionView.safeAreaLayoutGuide.layoutFrame.width
  340. }
  341. return model.cellType.cellSizeForWidth(toWidth)
  342. }
  343. func collectionView(_ collectionView: UICollectionView,
  344. layout collectionViewLayout: UICollectionViewLayout,
  345. insetForSectionAt section: Int) -> UIEdgeInsets {
  346. return UIEdgeInsets(top: model.cellType.edgePadding,
  347. left: model.cellType.edgePadding,
  348. bottom: model.cellType.edgePadding,
  349. right: model.cellType.edgePadding)
  350. }
  351. func collectionView(_ collectionView: UICollectionView,
  352. layout collectionViewLayout: UICollectionViewLayout,
  353. minimumLineSpacingForSectionAt section: Int) -> CGFloat {
  354. return model.cellType.edgePadding
  355. }
  356. func collectionView(_ collectionView: UICollectionView,
  357. layout collectionViewLayout: UICollectionViewLayout,
  358. minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
  359. return model.cellType.interItemPadding
  360. }
  361. }
  362. // MARK: - AddToPlaylistViewControllerDelegate
  363. extension EditController: AddToPlaylistViewControllerDelegate {
  364. func addToPlaylistViewController(_ addToPlaylistViewController: AddToPlaylistViewController,
  365. didSelectPlaylist playlist: VLCMLPlaylist) {
  366. let files = model.anyfiles
  367. var mediaObjects = [VLCMLObject]()
  368. for index in selectedCellIndexPaths where index.row < files.count {
  369. if let mediaCollection = files[index.row] as? MediaCollectionModel {
  370. mediaObjects += mediaCollection.files() ?? []
  371. } else {
  372. mediaObjects.append(files[index.row])
  373. }
  374. }
  375. for media in mediaObjects {
  376. if !playlist.appendMedia(withIdentifier: media.identifier()) {
  377. assertionFailure("EditController: AddToPlaylistViewControllerDelegate: Failed to add item.")
  378. }
  379. }
  380. resetSelections(resetUI: false)
  381. addToPlaylistViewController.dismiss(animated: true, completion: nil)
  382. delegate?.editControllerDidFinishEditing(editController: self)
  383. }
  384. func addToPlaylistViewController(_ addToPlaylistViewController: AddToPlaylistViewController,
  385. newPlaylistWithName name: String) {
  386. createPlaylist(name)
  387. addToPlaylistViewController.dismiss(animated: true, completion: nil)
  388. }
  389. }