EditController.swift 18 KB

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