EditController.swift 17 KB

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