VLCDragAndDropManager.swift 23 KB

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