VLCDragAndDropManager.swift 23 KB

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