EditController.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  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(model: 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. if model is PlaylistModel {
  170. message = NSLocalizedString("DELETE_MESSAGE_PLAYLIST", comment: "")
  171. } else if (model as? CollectionModel)?.mediaCollection is VLCMLPlaylist {
  172. message = NSLocalizedString("DELETE_MESSAGE_PLAYLIST_CONTENT", comment: "")
  173. }
  174. let cancelButton = VLCAlertButton(title: NSLocalizedString("BUTTON_CANCEL", comment: ""),
  175. style: .cancel)
  176. let deleteButton = VLCAlertButton(title: NSLocalizedString("BUTTON_DELETE", comment: ""),
  177. style: .destructive,
  178. action: {
  179. [weak self] action in
  180. self?.model.delete(objectsToDelete)
  181. // Update directly the cached indexes since cells will be destroyed
  182. self?.selectedCellIndexPaths.removeAll()
  183. self?.delegate?.editControllerDidFinishEditing(editController: self)
  184. })
  185. VLCAlertViewController.alertViewManager(title: NSLocalizedString("DELETE_TITLE", comment: ""),
  186. errorMessage: message,
  187. viewController: (UIApplication.shared.keyWindow?.rootViewController)!,
  188. buttonsAction: [cancelButton,
  189. deleteButton])
  190. }
  191. func editToolbarDidShare(_ editToolbar: EditToolbar, presentFrom button: UIButton) {
  192. UIApplication.shared.beginIgnoringInteractionEvents()
  193. let rootViewController = UIApplication.shared.keyWindow?.rootViewController
  194. guard let controller = VLCActivityViewControllerVendor
  195. .activityViewController(forFiles: fileURLsFromSelection(),
  196. presenting: button,
  197. presenting: rootViewController,
  198. completionHandler: {
  199. [weak self] completion in
  200. self?.delegate?.editControllerDidFinishEditing(editController: self)
  201. }) else {
  202. UIApplication.shared.endIgnoringInteractionEvents()
  203. return
  204. }
  205. controller.popoverPresentationController?.sourceView = editToolbar
  206. rootViewController?.present(controller, animated: true) {
  207. UIApplication.shared.endIgnoringInteractionEvents()
  208. }
  209. }
  210. func fileURLsFromSelection() -> [URL] {
  211. var fileURLS = [URL]()
  212. for indexPath in selectedCellIndexPaths {
  213. let file = model.anyfiles[indexPath.row]
  214. if let collection = file as? MediaCollectionModel,
  215. let files = collection.files() {
  216. files.forEach {
  217. if let mainFile = $0.mainFile() {
  218. fileURLS.append(mainFile.mrl)
  219. }
  220. }
  221. } else if let media = file as? VLCMLMedia, let mainFile = media.mainFile() {
  222. fileURLS.append(mainFile.mrl)
  223. } else {
  224. assertionFailure("we're trying to share something that doesn't have an mrl")
  225. return fileURLS
  226. }
  227. }
  228. return fileURLS
  229. }
  230. func editToolbarDidRename(_ editToolbar: EditToolbar) {
  231. guard let indexPath = selectedCellIndexPaths.first else {
  232. assertionFailure("EditController: Rename called without selection.")
  233. return
  234. }
  235. var mlObjectName = ""
  236. let mlObject = model.anyfiles[indexPath.row]
  237. if let media = mlObject as? VLCMLMedia {
  238. mlObjectName = media.title
  239. } else if let playlist = mlObject as? VLCMLPlaylist {
  240. mlObjectName = playlist.name
  241. } else {
  242. assertionFailure("EditController: Rename called with wrong model.")
  243. }
  244. // Not using VLCAlertViewController to have more customization in text fields
  245. let alertInfo = TextFieldAlertInfo(alertTitle: String(format: NSLocalizedString("RENAME_MEDIA_TO", comment: ""), mlObjectName),
  246. textfieldText: mlObjectName,
  247. confirmActionTitle: NSLocalizedString("BUTTON_RENAME", comment: ""))
  248. presentTextFieldAlert(with: alertInfo, completionHandler: {
  249. [weak self] text -> Void in
  250. guard text != "" else {
  251. VLCAlertViewController.alertViewManager(title: NSLocalizedString("ERROR_RENAME_FAILED", comment: ""),
  252. errorMessage: NSLocalizedString("ERROR_EMPTY_NAME", comment: ""),
  253. viewController: (UIApplication.shared.keyWindow?.rootViewController)!)
  254. return
  255. }
  256. let mlObject = self?.model.anyfiles[indexPath.row]
  257. if let media = mlObject as? VLCMLMedia {
  258. media.updateTitle(text)
  259. } else if let playlist = mlObject as? VLCMLPlaylist {
  260. playlist.updateName(text)
  261. }
  262. guard let strongself = self else {
  263. return
  264. }
  265. strongself.presentingView.deselectItem(at: indexPath, animated: true)
  266. strongself.collectionView(strongself.presentingView, didDeselectItemAt: indexPath)
  267. //call until all items are renamed
  268. if !strongself.selectedCellIndexPaths.isEmpty {
  269. strongself.editToolbarDidRename(editToolbar)
  270. } else {
  271. strongself.delegate?.editControllerDidFinishEditing(editController: self)
  272. }
  273. })
  274. }
  275. }
  276. // MARK: - UICollectionViewDelegate
  277. extension EditController: UICollectionViewDelegate {
  278. func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  279. selectedCellIndexPaths.insert(indexPath)
  280. // Isolate selectionViewOverlay changes inside EditController
  281. if let cell = collectionView.cellForItem(at: indexPath) as? BaseCollectionViewCell {
  282. cell.selectionViewOverlay?.isHidden = false
  283. }
  284. }
  285. func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
  286. selectedCellIndexPaths.remove(indexPath)
  287. if let cell = collectionView.cellForItem(at: indexPath) as? BaseCollectionViewCell {
  288. cell.selectionViewOverlay?.isHidden = true
  289. }
  290. }
  291. }
  292. // MARK: - UICollectionViewDataSource
  293. extension EditController: UICollectionViewDataSource {
  294. func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  295. return model.anyfiles.count
  296. }
  297. func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  298. if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: model.cellType.defaultReuseIdentifier,
  299. for: indexPath) as? BaseCollectionViewCell {
  300. cell.media = model.anyfiles[indexPath.row]
  301. cell.isSelected = selectedCellIndexPaths.contains(indexPath)
  302. cell.isAccessibilityElement = true
  303. cell.checkImageView?.isHidden = false
  304. if let cell = cell as? MediaCollectionViewCell,
  305. let collectionModel = model as? CollectionModel, collectionModel.mediaCollection is VLCMLPlaylist {
  306. cell.dragIndicatorImageView.isHidden = false
  307. }
  308. if cell.isSelected {
  309. cell.selectionViewOverlay?.isHidden = false
  310. }
  311. return cell
  312. } else {
  313. assertionFailure("We couldn't dequeue a reusable cell, the cell might not be registered or is not a MediaEditCell")
  314. return UICollectionViewCell()
  315. }
  316. }
  317. func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
  318. guard let collectionModel = model as? CollectionModel, let playlist = collectionModel.mediaCollection as? VLCMLPlaylist else {
  319. assertionFailure("can Move should've been false")
  320. return
  321. }
  322. playlist.moveMedia(fromPosition: UInt32(sourceIndexPath.row), toDestination: UInt32(destinationIndexPath.row))
  323. }
  324. func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
  325. if let collectionModel = model as? CollectionModel, collectionModel.mediaCollection is VLCMLPlaylist {
  326. return true
  327. }
  328. return false
  329. }
  330. }
  331. // MARK: - UICollectionViewDelegateFlowLayout
  332. extension EditController: UICollectionViewDelegateFlowLayout {
  333. func collectionView(_ collectionView: UICollectionView,
  334. layout collectionViewLayout: UICollectionViewLayout,
  335. sizeForItemAt indexPath: IndexPath) -> CGSize {
  336. var toWidth = collectionView.frame.size.width
  337. if #available(iOS 11.0, *) {
  338. toWidth = collectionView.safeAreaLayoutGuide.layoutFrame.width
  339. }
  340. return model.cellType.cellSizeForWidth(toWidth)
  341. }
  342. func collectionView(_ collectionView: UICollectionView,
  343. layout collectionViewLayout: UICollectionViewLayout,
  344. insetForSectionAt section: Int) -> UIEdgeInsets {
  345. return UIEdgeInsets(top: model.cellType.edgePadding,
  346. left: model.cellType.edgePadding,
  347. bottom: model.cellType.edgePadding,
  348. right: model.cellType.edgePadding)
  349. }
  350. func collectionView(_ collectionView: UICollectionView,
  351. layout collectionViewLayout: UICollectionViewLayout,
  352. minimumLineSpacingForSectionAt section: Int) -> CGFloat {
  353. return model.cellType.edgePadding
  354. }
  355. func collectionView(_ collectionView: UICollectionView,
  356. layout collectionViewLayout: UICollectionViewLayout,
  357. minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
  358. return model.cellType.interItemPadding
  359. }
  360. }
  361. // MARK: - AddToPlaylistViewControllerDelegate
  362. extension EditController: AddToPlaylistViewControllerDelegate {
  363. func addToPlaylistViewController(_ addToPlaylistViewController: AddToPlaylistViewController,
  364. didSelectPlaylist playlist: VLCMLPlaylist) {
  365. let files = model.anyfiles
  366. var mediaObjects = [VLCMLObject]()
  367. for index in selectedCellIndexPaths where index.row < files.count {
  368. if let mediaCollection = files[index.row] as? MediaCollectionModel {
  369. mediaObjects += mediaCollection.files() ?? []
  370. } else {
  371. mediaObjects.append(files[index.row])
  372. }
  373. }
  374. for media in mediaObjects {
  375. if !playlist.appendMedia(withIdentifier: media.identifier()) {
  376. assertionFailure("EditController: AddToPlaylistViewControllerDelegate: Failed to add item.")
  377. }
  378. }
  379. resetSelections(resetUI: false)
  380. addToPlaylistViewController.dismiss(animated: true, completion: nil)
  381. delegate?.editControllerDidFinishEditing(editController: self)
  382. }
  383. func addToPlaylistViewController(_ addToPlaylistViewController: AddToPlaylistViewController,
  384. newPlaylistWithName name: String) {
  385. createPlaylist(name)
  386. addToPlaylistViewController.dismiss(animated: true, completion: nil)
  387. }
  388. }