VLCDragAndDropManager.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. /*****************************************************************************
  2. * VLCDragAndDropManager.swift
  3. * VLC for iOS
  4. *****************************************************************************
  5. * Copyright (c) 2017 VideoLAN. All rights reserved.
  6. * $Id$
  7. *
  8. * Authors: Carola Nitz <caro # videolan.org>
  9. *
  10. * Refer to the COPYING file of the official project for license.
  11. *****************************************************************************/
  12. import UIKit
  13. import MobileCoreServices
  14. @available(iOS 11.0, *)
  15. struct DropError: Error {
  16. enum ErrorKind {
  17. case moveFileToDocuments
  18. case loadFileRepresentationFailed
  19. }
  20. let kind: ErrorKind
  21. }
  22. @available(iOS 11.0, *)
  23. @objc protocol VLCDragAndDropManagerDelegate : NSObjectProtocol {
  24. func dragAndDropManagerRequestsFile(manager:VLCDragAndDropManager, atIndexPath indexPath:IndexPath) -> AnyObject?
  25. func dragAndDropManagerInsertItem(manager:VLCDragAndDropManager, item:NSManagedObject, atIndexPath indexPath:IndexPath)
  26. func dragAndDropManagerDeleteItem(manager:VLCDragAndDropManager, atIndexPath indexPath:IndexPath)
  27. func dragAndDropManagerRemoveFileFromFolder(manager:VLCDragAndDropManager, file:NSManagedObject)
  28. func dragAndDropManagerCurrentSelection(manager:VLCDragAndDropManager) -> AnyObject?
  29. }
  30. @available(iOS 11.0, *)
  31. class VLCDragAndDropManager: NSObject, UICollectionViewDragDelegate, UITableViewDragDelegate, UICollectionViewDropDelegate, UITableViewDropDelegate, UIDropInteractionDelegate
  32. {
  33. @objc weak var delegate:VLCDragAndDropManagerDelegate?
  34. let utiTypeIdentifiers:[String] = VLCDragAndDropManager.supportedTypeIdentifiers()
  35. /// Returns the supported type identifiers that VLC can process.
  36. /// It fetches the identifiers in LSItemContentTypes from all the CFBundleDocumentTypes in the info.plist.
  37. /// Video, Audio and Subtitle formats
  38. ///
  39. /// - Returns: Array of UTITypeIdentifiers
  40. private class func supportedTypeIdentifiers() -> [String] {
  41. var typeIdentifiers:[String] = []
  42. if let documents = Bundle.main.infoDictionary?["CFBundleDocumentTypes"] as? [[String:Any]] {
  43. for item in documents {
  44. if let value = item["LSItemContentTypes"] as? Array<String> {
  45. typeIdentifiers.append(contentsOf: value)
  46. }
  47. }
  48. }
  49. return typeIdentifiers
  50. }
  51. //MARK: - TableView
  52. func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
  53. return canHandleDropSession(session: session)
  54. }
  55. func tableView(_ tableView: UITableView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] {
  56. return dragItems(forIndexPath:indexPath)
  57. }
  58. func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
  59. return dragItems(forIndexPath:indexPath)
  60. }
  61. func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
  62. let operation = dropOperation(hasActiveDrag: tableView.hasActiveDrag, firstSessionItem: session.items.first, withDestinationIndexPath: destinationIndexPath)
  63. return UITableViewDropProposal(operation: operation, intent: .insertIntoDestinationIndexPath)
  64. }
  65. func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
  66. let section = tableView.numberOfSections - 1
  67. let row = tableView.numberOfRows(inSection: section)
  68. let destinationPath = coordinator.destinationIndexPath ?? IndexPath(row: row, section: section)
  69. for item in coordinator.items {
  70. let itemProvider = item.dragItem.itemProvider
  71. //we're not gonna handle moving of folders
  72. if let sourceItem = item.dragItem.localObject, fileIsCollection(file: sourceItem as AnyObject) {
  73. continue
  74. }
  75. if fileIsFolder(atIndexPath:destinationPath) { //handle dropping onto a folder
  76. addDragItem(tableView:tableView, dragItem:item, toFolderAt:destinationPath)
  77. continue
  78. }
  79. if item.sourceIndexPath != nil { //element within VLC
  80. moveItem(tableView:tableView, item:item, toIndexPath:destinationPath)
  81. continue
  82. }
  83. //Element dragging from another App
  84. let placeholder = UITableViewDropPlaceholder(insertionIndexPath: destinationPath, reuseIdentifier: "PlaylistCell", rowHeight: VLCPlaylistTableViewCell.heightOfCell())
  85. let placeholderContext = coordinator.drop(item.dragItem, to: placeholder)
  86. createFileWith(itemProvider:itemProvider) {
  87. [weak self] file, error in
  88. guard let strongSelf = self else { return }
  89. if let file = file {
  90. placeholderContext.commitInsertion() {
  91. insertionIndexPath in
  92. strongSelf.delegate?.dragAndDropManagerInsertItem(manager: strongSelf, item: file, atIndexPath: insertionIndexPath)
  93. }
  94. }
  95. if let error = error as? DropError {
  96. strongSelf.handleError(error: error, itemProvider: item.dragItem.itemProvider)
  97. placeholderContext.deletePlaceholder()
  98. }
  99. }
  100. }
  101. }
  102. private func inFolder() -> Bool {
  103. return delegate?.dragAndDropManagerCurrentSelection(manager: self) as? MLLabel != nil
  104. }
  105. private func moveItem(tableView:UITableView, item:UITableViewDropItem, toIndexPath destinationPath:IndexPath) {
  106. if let mlFile = item.dragItem.localObject as? MLFile, mlFile.labels.count > 0 && !inFolder() {
  107. tableView.performBatchUpdates({
  108. tableView.insertRows(at: [destinationPath], with: .automatic)
  109. delegate?.dragAndDropManagerInsertItem(manager: self, item: mlFile, atIndexPath: destinationPath)
  110. delegate?.dragAndDropManagerRemoveFileFromFolder(manager:self, file:mlFile)
  111. }, completion:nil)
  112. }
  113. }
  114. private func addDragItem(tableView:UITableView, dragItem item:UITableViewDropItem, toFolderAt index:IndexPath) {
  115. if let sourcepath = item.sourceIndexPath { //local file that just needs to be moved
  116. tableView.performBatchUpdates({
  117. if let file = delegate?.dragAndDropManagerRequestsFile(manager:self, atIndexPath: sourcepath) as? MLFile {
  118. tableView.deleteRows(at: [sourcepath], with: .automatic)
  119. addFile(file:file, toFolderAt:index)
  120. delegate?.dragAndDropManagerDeleteItem(manager: self, atIndexPath:sourcepath)
  121. }
  122. }, completion:nil)
  123. return
  124. }
  125. // file from other app
  126. createFileWith(itemProvider:item.dragItem.itemProvider) {
  127. [weak self] file, error in
  128. if let strongSelf = self, let file = file {
  129. strongSelf.addFile(file:file, toFolderAt:index)
  130. }
  131. }
  132. }
  133. //MARK: - Collectionview
  134. func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool {
  135. return canHandleDropSession(session: session)
  136. }
  137. func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
  138. return dragItems(forIndexPath: indexPath)
  139. }
  140. func collectionView(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] {
  141. return dragItems(forIndexPath: indexPath)
  142. }
  143. func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
  144. let operation = dropOperation(hasActiveDrag: collectionView.hasActiveDrag, firstSessionItem: session.items.first, withDestinationIndexPath: destinationIndexPath)
  145. return UICollectionViewDropProposal(operation: operation, intent: .insertIntoDestinationIndexPath)
  146. }
  147. func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
  148. let section = collectionView.numberOfSections - 1
  149. let row = collectionView.numberOfItems(inSection: section)
  150. let destinationPath = coordinator.destinationIndexPath ?? IndexPath(row: row, section: section)
  151. for item in coordinator.items {
  152. if let sourceItem = item.dragItem.localObject, fileIsCollection(file: sourceItem as AnyObject) { //We're not handling moving of Collection
  153. continue
  154. }
  155. if fileIsFolder(atIndexPath:destinationPath) { //handle dropping onto a folder
  156. addDragItem(collectionView:collectionView, dragItem:item, toFolderAt:destinationPath)
  157. continue
  158. }
  159. if item.sourceIndexPath != nil { //element within VLC
  160. moveItem(collectionView:collectionView, item:item, toIndexPath:destinationPath)
  161. continue
  162. }
  163. //Element from another App
  164. let placeholder = UICollectionViewDropPlaceholder(insertionIndexPath: destinationPath, reuseIdentifier: "PlaylistCell")
  165. let placeholderContext = coordinator.drop(item.dragItem, to: placeholder)
  166. createFileWith(itemProvider:item.dragItem.itemProvider) {
  167. [weak self] file, error in
  168. guard let strongSelf = self else { return }
  169. if let file = file {
  170. placeholderContext.commitInsertion() {
  171. insertionIndexPath in
  172. strongSelf.delegate?.dragAndDropManagerInsertItem(manager: strongSelf, item: file, atIndexPath: insertionIndexPath)
  173. }
  174. }
  175. if let error = error as? DropError {
  176. strongSelf.handleError(error: error, itemProvider: item.dragItem.itemProvider)
  177. placeholderContext.deletePlaceholder()
  178. }
  179. }
  180. }
  181. }
  182. private func moveItem(collectionView:UICollectionView, item:UICollectionViewDropItem, toIndexPath destinationPath:IndexPath) {
  183. if let mlFile = item.dragItem.localObject as? MLFile, mlFile.labels.count > 0 && !inFolder() {
  184. collectionView.performBatchUpdates({
  185. collectionView.insertItems(at: [destinationPath])
  186. delegate?.dragAndDropManagerInsertItem(manager: self, item: mlFile, atIndexPath: destinationPath)
  187. delegate?.dragAndDropManagerRemoveFileFromFolder(manager:self, file:mlFile)
  188. }, completion:nil)
  189. }
  190. }
  191. private func addDragItem(collectionView:UICollectionView, dragItem item:UICollectionViewDropItem, toFolderAt index:IndexPath) {
  192. if let sourcepath = item.sourceIndexPath {
  193. //local file that just needs to be moved
  194. collectionView.performBatchUpdates({
  195. if let file = delegate?.dragAndDropManagerRequestsFile(manager:self, atIndexPath: sourcepath) as? MLFile {
  196. collectionView.deleteItems(at:[sourcepath])
  197. addFile(file:file, toFolderAt:index)
  198. delegate?.dragAndDropManagerDeleteItem(manager: self, atIndexPath:sourcepath)
  199. }
  200. }, completion:nil)
  201. } else {
  202. // file from other app
  203. createFileWith(itemProvider:item.dragItem.itemProvider) {
  204. [weak self] file, error in
  205. if let strongSelf = self, let file = file {
  206. strongSelf.addFile(file:file, toFolderAt:index)
  207. }
  208. }
  209. }
  210. }
  211. //MARK: - DropInteractionDelegate for EmptyView
  212. func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
  213. return canHandleDropSession(session: session)
  214. }
  215. func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
  216. return UIDropProposal(operation: .copy)
  217. }
  218. func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
  219. for item in session.items {
  220. createFileWith(itemProvider:item.itemProvider) {
  221. [weak self] _, error in
  222. if let error = error as? DropError {
  223. self?.handleError(error: error, itemProvider: item.itemProvider)
  224. }
  225. //no need to handle the file case since the libraryVC updates itself after getting a file
  226. }
  227. }
  228. }
  229. //MARK: - Shared Methods
  230. //Checks if the session has items conforming to typeidentifiers
  231. private func canHandleDropSession(session:UIDropSession) -> Bool {
  232. if (session.localDragSession != nil) {
  233. return true
  234. }
  235. return session.hasItemsConforming(toTypeIdentifiers: utiTypeIdentifiers)
  236. }
  237. /// Returns a drop operation type
  238. ///
  239. /// - Parameters:
  240. /// - hasActiveDrag: State if the drag started within the app
  241. /// - item: UIDragItem from session
  242. /// - Returns: UIDropOperation
  243. private func dropOperation(hasActiveDrag: Bool, firstSessionItem item: AnyObject?, withDestinationIndexPath destinationIndexPath:IndexPath?) -> UIDropOperation {
  244. let inAlbum = delegate?.dragAndDropManagerCurrentSelection(manager: self) as? MLAlbum != nil
  245. let inShow = delegate?.dragAndDropManagerCurrentSelection(manager: self) as? MLShow != nil
  246. //you can move files into a folder or copy from anothr app into a folder
  247. if fileIsFolder(atIndexPath:destinationIndexPath) {
  248. //no dragging entire shows and albums into folders
  249. if let dragItem = item, let mlFile = dragItem.localObject as? MLFile, mlFile.isAlbumTrack() || mlFile.isShowEpisode() {
  250. return .forbidden
  251. }
  252. return hasActiveDrag ? .move : .copy
  253. }
  254. //you can't reorder
  255. if inFolder() {
  256. return hasActiveDrag ? .forbidden : .copy
  257. }
  258. //you can't reorder in or drag into an Album or Show
  259. if inAlbum || inShow {
  260. return .cancel
  261. }
  262. //we're dragging a file out of a folder
  263. if let dragItem = item, let mlFile = dragItem.localObject as? MLFile, mlFile.labels.count > 0 {
  264. return .copy
  265. }
  266. //no reorder from another app into the top layer
  267. return hasActiveDrag ? .forbidden : .copy
  268. }
  269. /// show an Alert when dropping failed
  270. ///
  271. /// - Parameters:
  272. /// - error: the type of error that happend
  273. /// - itemProvider: the itemProvider to retrieve the suggestedName
  274. private func handleError(error: DropError, itemProvider: NSItemProvider) {
  275. let message: String
  276. let filename = itemProvider.suggestedName ?? NSLocalizedString("THIS_FILE", comment:"")
  277. switch (error.kind) {
  278. case .loadFileRepresentationFailed:
  279. message = String(format: NSLocalizedString("NOT_SUPPORTED_FILETYPE", comment: ""), filename)
  280. case .moveFileToDocuments:
  281. message = String(format: NSLocalizedString("FILE_EXISTS", comment: ""), filename)
  282. }
  283. let alert = UIAlertController(title: NSLocalizedString("ERROR", comment:""), message: message, preferredStyle: .alert)
  284. alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment:""), style: .default, handler: nil))
  285. UIApplication.shared.keyWindow?.rootViewController?.present(alert, animated: true, completion: nil)
  286. }
  287. private func fileIsFolder(atIndexPath indexPath:IndexPath?) -> Bool {
  288. if let indexPath = indexPath {
  289. let file = delegate?.dragAndDropManagerRequestsFile(manager: self, atIndexPath: indexPath)
  290. return file as? MLLabel != nil
  291. }
  292. return false
  293. }
  294. private func fileIsCollection(file:AnyObject?) -> Bool {
  295. let isFolder = file as? MLLabel != nil
  296. let isAlbum = file as? MLAlbum != nil
  297. let isShow = file as? MLShow != nil
  298. return isFolder || isAlbum || isShow
  299. }
  300. private func fileIsCollection(atIndexPath indexPath:IndexPath?) -> Bool {
  301. if let indexPath = indexPath {
  302. let file = delegate?.dragAndDropManagerRequestsFile(manager: self, atIndexPath: indexPath)
  303. return fileIsCollection(file:file)
  304. }
  305. return false
  306. }
  307. //creating dragItems for the file at indexpath
  308. private func dragItems(forIndexPath indexPath:IndexPath) -> [UIDragItem] {
  309. if let file = delegate?.dragAndDropManagerRequestsFile(manager: self, atIndexPath: indexPath) {
  310. if fileIsCollection(atIndexPath: indexPath) {
  311. return dragItemsforCollection(file: file)
  312. }
  313. return dragItem(fromFile:file)
  314. }
  315. assert(false, "we can't generate a dragfile if the delegate can't return a file ")
  316. return []
  317. }
  318. /// Iterates over the items of a collection to create dragitems.
  319. /// Since we're not storing collections as folders we have to provide single files
  320. ///
  321. /// - Parameter file: Can be of type MLAlbum, MLLabel or MLShow
  322. /// - Returns: An array of UIDragItems
  323. private func dragItemsforCollection(file: AnyObject) -> [UIDragItem] {
  324. var dragItems = [UIDragItem]()
  325. var set = Set<AnyHashable>()
  326. if let folder = file as? MLLabel {
  327. set = folder.files
  328. } else if let album = file as? MLAlbum {
  329. for track in album.tracks {
  330. if let mlfile = (track as? MLAlbumTrack)?.files.first {
  331. _ = set.insert(mlfile)
  332. }
  333. }
  334. } else if let show = file as? MLShow {
  335. for episode in show.episodes {
  336. if let mlfile = (episode as? MLShowEpisode)?.files {
  337. set = set.union(mlfile)
  338. }
  339. }
  340. } else {
  341. assert(false, "can't get dragitems from a file that is not a collection")
  342. }
  343. for convertibleFile in set {
  344. if let mlfile = convertibleFile as? MLFile, let item = dragItem(fromFile:mlfile).first {
  345. dragItems.append(item)
  346. }
  347. }
  348. return dragItems
  349. }
  350. //Provides an item for other applications
  351. private func dragItem(fromFile file:AnyObject) -> [UIDragItem] {
  352. guard let file = mlFile(from: file), let path = file.url else {
  353. assert(false, "can't create a dragitem if there is no file or the file has no url")
  354. return []
  355. }
  356. let data = try? Data(contentsOf: path, options: .mappedIfSafe)
  357. let itemProvider = NSItemProvider()
  358. itemProvider.suggestedName = path.lastPathComponent
  359. //maybe use UTTypeForFileURL
  360. if let identifiers = try? path.resourceValues(forKeys: [.typeIdentifierKey]), let identifier = identifiers.typeIdentifier {
  361. //here we can show progress
  362. itemProvider.registerDataRepresentation(forTypeIdentifier: identifier, visibility: .all) { completion -> Progress? in
  363. completion(data, nil)
  364. return nil
  365. }
  366. let dragitem = UIDragItem(itemProvider: itemProvider)
  367. dragitem.localObject = file
  368. return [dragitem]
  369. }
  370. assert(false, "we can't provide a typeidentifier")
  371. return []
  372. }
  373. private func mlFile(from file:AnyObject) -> MLFile? {
  374. if let episode = file as? MLShowEpisode, let convertedfile = episode.files.first as? MLFile{
  375. return convertedfile
  376. }
  377. if let track = file as? MLAlbumTrack, let convertedfile = track.files.first as? MLFile{
  378. return convertedfile
  379. }
  380. if let convertedfile = file as? MLFile {
  381. return convertedfile
  382. }
  383. return nil
  384. }
  385. private func addFile(file:MLFile, toFolderAt folderIndex:IndexPath) {
  386. let label = delegate?.dragAndDropManagerRequestsFile(manager: self, atIndexPath: folderIndex) as! MLLabel
  387. DispatchQueue.main.async {
  388. _ = label.files.insert(file)
  389. file.labels = [label]
  390. file.folderTrackNumber = NSNumber(integerLiteral: label.files.count - 1)
  391. }
  392. }
  393. /// try to create a file from the dropped item
  394. ///
  395. /// - Parameters:
  396. /// - itemProvider: itemprovider which is used to load the files from
  397. /// - completion: callback with the successfully created file or error if it failed
  398. private func createFileWith(itemProvider:NSItemProvider, completion: @escaping ((MLFile?, Error?) -> Void))
  399. {
  400. itemProvider.loadFileRepresentation(forTypeIdentifier: kUTTypeData as String) {
  401. [weak self] (url, error) in
  402. guard let strongSelf = self else { return }
  403. guard let url = url else {
  404. DispatchQueue.main.async {
  405. completion(nil, DropError(kind: .loadFileRepresentationFailed))
  406. }
  407. return
  408. }
  409. //returns nil for local session but this should also not be called for a local session
  410. guard let destinationURL = strongSelf.moveFileToDocuments(fromURL: url) else {
  411. DispatchQueue.main.async {
  412. completion(nil, DropError(kind: .moveFileToDocuments))
  413. }
  414. return
  415. }
  416. DispatchQueue.global(qos: .background).async {
  417. let sharedlib = MLMediaLibrary.sharedMediaLibrary() as? MLMediaLibrary
  418. sharedlib?.addFilePaths([destinationURL.path])
  419. if let file = MLFile.file(for: destinationURL).first as? MLFile {
  420. DispatchQueue.main.async {
  421. //we dragged into a folder
  422. if let selection = strongSelf.delegate?.dragAndDropManagerCurrentSelection(manager: strongSelf) as? MLLabel {
  423. file.labels = [selection]
  424. }
  425. completion(file, nil)
  426. }
  427. }
  428. }
  429. }
  430. }
  431. private func moveFileToDocuments(fromURL filepath:URL?) -> URL? {
  432. let searchPaths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
  433. let newDirectoryPath = searchPaths.first
  434. guard let directoryPath = newDirectoryPath, let url = filepath else {
  435. return nil
  436. }
  437. let destinationURL = URL(fileURLWithPath: "\(directoryPath)" + "/" + "\(url.lastPathComponent)")
  438. do {
  439. try FileManager.default.moveItem(at: url, to: destinationURL)
  440. } catch let error {
  441. print(error.localizedDescription)
  442. return nil
  443. }
  444. return destinationURL
  445. }
  446. }