VLCDragAndDropManager.swift 22.9 KB
Newer Older
Carola Nitz's avatar
Carola Nitz committed
1 2 3 4 5 6 7 8 9 10 11 12 13
/*****************************************************************************
 * VLCDragAndDropManager.swift
 * VLC for iOS
 *****************************************************************************
 * Copyright (c) 2017 VideoLAN. All rights reserved.
 * $Id$
 *
 * Authors: Carola Nitz <caro # videolan.org>
 *
 * Refer to the COPYING file of the official project for license.
 *****************************************************************************/

import MobileCoreServices
Kevin Bettin's avatar
Kevin Bettin committed
14
import UIKit
Carola Nitz's avatar
Carola Nitz committed
15 16 17 18 19 20 21 22 23 24

@available(iOS 11.0, *)
struct DropError: Error {
    enum ErrorKind {
        case moveFileToDocuments
        case loadFileRepresentationFailed
    }

    let kind: ErrorKind
}
Kevin Bettin's avatar
Kevin Bettin committed
25

Carola Nitz's avatar
Carola Nitz committed
26
@available(iOS 11.0, *)
27
@objc protocol VLCDragAndDropManagerDelegate: NSObjectProtocol {
28
    func dragAndDropManagerRequestsFile(manager: VLCDragAndDropManager, atIndexPath indexPath: IndexPath) -> Any?
29 30 31 32
    func dragAndDropManagerInsertItem(manager: VLCDragAndDropManager, item: NSManagedObject, atIndexPath indexPath: IndexPath)
    func dragAndDropManagerDeleteItem(manager: VLCDragAndDropManager, atIndexPath indexPath: IndexPath)
    func dragAndDropManagerRemoveFileFromFolder(manager: VLCDragAndDropManager, file: NSManagedObject)
    func dragAndDropManagerCurrentSelection(manager: VLCDragAndDropManager) -> AnyObject?
Carola Nitz's avatar
Carola Nitz committed
33 34 35
}

@available(iOS 11.0, *)
36 37
class VLCDragAndDropManager: NSObject, UICollectionViewDragDelegate, UITableViewDragDelegate, UICollectionViewDropDelegate, UITableViewDropDelegate, UIDropInteractionDelegate {
    @objc weak var delegate: VLCDragAndDropManagerDelegate?
Carola Nitz's avatar
Carola Nitz committed
38

39
    let utiTypeIdentifiers: [String] = VLCDragAndDropManager.supportedTypeIdentifiers()
Kevin Bettin's avatar
Kevin Bettin committed
40
    var mediaType: VLCMediaType
Carola Nitz's avatar
Carola Nitz committed
41 42 43 44 45
    /// Returns the supported type identifiers that VLC can process.
    /// It fetches the identifiers in LSItemContentTypes from all the CFBundleDocumentTypes in the info.plist.
    /// Video, Audio and Subtitle formats
    ///
    /// - Returns: Array of UTITypeIdentifiers
46
    private class func supportedTypeIdentifiers() -> [String] {
47 48
        var typeIdentifiers: [String] = []
        if let documents = Bundle.main.infoDictionary?["CFBundleDocumentTypes"] as? [[String: Any]] {
49
            for item in documents {
50
                if let value = item["LSItemContentTypes"] as? [String] {
51
                    typeIdentifiers.append(contentsOf: value)
Carola Nitz's avatar
Carola Nitz committed
52 53 54 55 56 57
                }
            }
        }
        return typeIdentifiers
    }

58 59 60 61 62 63 64 65 66
    @available(*, unavailable, message: "use init(category:)")
    override init() {
        fatalError()
    }

    init(type: VLCMediaType) {
        mediaType = type
        super.init()
    }
Kevin Bettin's avatar
Kevin Bettin committed
67

68
    // MARK: - TableView
Kevin Bettin's avatar
Kevin Bettin committed
69

Carola Nitz's avatar
Carola Nitz committed
70 71 72 73 74
    func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
        return canHandleDropSession(session: session)
    }

    func tableView(_ tableView: UITableView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] {
75
        return dragItems(forIndexPath: indexPath)
Carola Nitz's avatar
Carola Nitz committed
76 77 78
    }

    func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
Kevin Bettin's avatar
Kevin Bettin committed
79
        return dragItems(forIndexPath: indexPath)
Carola Nitz's avatar
Carola Nitz committed
80 81 82
    }

    func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
83
        let operation = dropOperation(hasActiveDrag: tableView.hasActiveDrag, firstSessionItem: session.items.first, withDestinationIndexPath: destinationIndexPath)
Carola Nitz's avatar
Carola Nitz committed
84 85 86 87 88 89 90 91 92 93
        return UITableViewDropProposal(operation: operation, intent: .insertIntoDestinationIndexPath)
    }

    func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
        let section = tableView.numberOfSections - 1
        let row = tableView.numberOfRows(inSection: section)
        let destinationPath = coordinator.destinationIndexPath ?? IndexPath(row: row, section: section)

        for item in coordinator.items {
            let itemProvider = item.dragItem.itemProvider
Kevin Bettin's avatar
Kevin Bettin committed
94
            // we're not gonna handle moving of folders
Carola Nitz's avatar
Carola Nitz committed
95 96 97 98
            if let sourceItem = item.dragItem.localObject, fileIsCollection(file: sourceItem as AnyObject) {
                continue
            }

Kevin Bettin's avatar
Kevin Bettin committed
99 100
            if fileIsFolder(atIndexPath: destinationPath) { // handle dropping onto a folder
                addDragItem(tableView: tableView, dragItem: item, toFolderAt: destinationPath)
Carola Nitz's avatar
Carola Nitz committed
101 102 103
                continue
            }

Kevin Bettin's avatar
Kevin Bettin committed
104 105
            if item.sourceIndexPath != nil { // element within VLC
                moveItem(tableView: tableView, item: item, toIndexPath: destinationPath)
Carola Nitz's avatar
Carola Nitz committed
106 107
                continue
            }
Kevin Bettin's avatar
Kevin Bettin committed
108
            // Element dragging from another App
109
            let placeholder = UITableViewDropPlaceholder(insertionIndexPath: destinationPath, reuseIdentifier: VLCPlaylistTableViewCell.cellIdentifier(), rowHeight: VLCPlaylistTableViewCell.heightOfCell())
Carola Nitz's avatar
Carola Nitz committed
110
            let placeholderContext = coordinator.drop(item.dragItem, to: placeholder)
Kevin Bettin's avatar
Kevin Bettin committed
111
            createFileWith(itemProvider: itemProvider) {
Carola Nitz's avatar
Carola Nitz committed
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
                [weak self] file, error in

                guard let strongSelf = self else { return }

                if let file = file {
                    placeholderContext.commitInsertion() {
                        insertionIndexPath in
                        strongSelf.delegate?.dragAndDropManagerInsertItem(manager: strongSelf, item: file, atIndexPath: insertionIndexPath)
                    }
                }
                if let error = error as? DropError {
                    strongSelf.handleError(error: error, itemProvider: item.dragItem.itemProvider)
                    placeholderContext.deletePlaceholder()
                }
            }
        }
    }

    private func inFolder() -> Bool {
        return delegate?.dragAndDropManagerCurrentSelection(manager: self) as? MLLabel != nil
    }

134 135
    private func moveItem(tableView: UITableView, item: UITableViewDropItem, toIndexPath destinationPath: IndexPath) {
        if let mlFile = item.dragItem.localObject as? MLFile, !mlFile.labels.isEmpty && !inFolder() {
Carola Nitz's avatar
Carola Nitz committed
136 137 138
            tableView.performBatchUpdates({
                tableView.insertRows(at: [destinationPath], with: .automatic)
                delegate?.dragAndDropManagerInsertItem(manager: self, item: mlFile, atIndexPath: destinationPath)
Kevin Bettin's avatar
Kevin Bettin committed
139 140
                delegate?.dragAndDropManagerRemoveFileFromFolder(manager: self, file: mlFile)
            }, completion: nil)
Carola Nitz's avatar
Carola Nitz committed
141 142 143
        }
    }

144
    private func addDragItem(tableView: UITableView, dragItem item: UITableViewDropItem, toFolderAt index: IndexPath) {
Kevin Bettin's avatar
Kevin Bettin committed
145
        if let sourcepath = item.sourceIndexPath { // local file that just needs to be moved
Carola Nitz's avatar
Carola Nitz committed
146
            tableView.performBatchUpdates({
Kevin Bettin's avatar
Kevin Bettin committed
147
                if let file = delegate?.dragAndDropManagerRequestsFile(manager: self, atIndexPath: sourcepath) as? MLFile {
148
                    tableView.deleteRows(at: [sourcepath], with: .automatic)
Kevin Bettin's avatar
Kevin Bettin committed
149 150
                    addFile(file: file, toFolderAt: index)
                    delegate?.dragAndDropManagerDeleteItem(manager: self, atIndexPath: sourcepath)
Carola Nitz's avatar
Carola Nitz committed
151
                }
Kevin Bettin's avatar
Kevin Bettin committed
152
            }, completion: nil)
Carola Nitz's avatar
Carola Nitz committed
153 154 155
            return
        }
        // file from other app
Kevin Bettin's avatar
Kevin Bettin committed
156
        createFileWith(itemProvider: item.dragItem.itemProvider) {
Carola Nitz's avatar
Carola Nitz committed
157 158 159
            [weak self] file, error in

            if let strongSelf = self, let file = file {
Kevin Bettin's avatar
Kevin Bettin committed
160
                strongSelf.addFile(file: file, toFolderAt: index)
Carola Nitz's avatar
Carola Nitz committed
161 162 163 164
            }
        }
    }

165
    // MARK: - Collectionview
Kevin Bettin's avatar
Kevin Bettin committed
166

Carola Nitz's avatar
Carola Nitz committed
167 168 169 170 171 172 173 174 175 176 177 178 179
    func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool {
        return canHandleDropSession(session: session)
    }

    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        return dragItems(forIndexPath: indexPath)
    }

    func collectionView(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] {
        return dragItems(forIndexPath: indexPath)
    }

    func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
180
        let operation = dropOperation(hasActiveDrag: collectionView.hasActiveDrag, firstSessionItem: session.items.first, withDestinationIndexPath: destinationIndexPath)
Carola Nitz's avatar
Carola Nitz committed
181 182 183 184 185 186 187 188 189
        return UICollectionViewDropProposal(operation: operation, intent: .insertIntoDestinationIndexPath)
    }

    func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
        let section = collectionView.numberOfSections - 1
        let row = collectionView.numberOfItems(inSection: section)
        let destinationPath = coordinator.destinationIndexPath ?? IndexPath(row: row, section: section)

        for item in coordinator.items {
Kevin Bettin's avatar
Kevin Bettin committed
190
            if let sourceItem = item.dragItem.localObject, fileIsCollection(file: sourceItem as AnyObject) { // We're not handling moving of Collection
Carola Nitz's avatar
Carola Nitz committed
191 192
                continue
            }
Kevin Bettin's avatar
Kevin Bettin committed
193 194
            if fileIsFolder(atIndexPath: destinationPath) { // handle dropping onto a folder
                addDragItem(collectionView: collectionView, dragItem: item, toFolderAt: destinationPath)
Carola Nitz's avatar
Carola Nitz committed
195 196
                continue
            }
Kevin Bettin's avatar
Kevin Bettin committed
197 198
            if item.sourceIndexPath != nil { // element within VLC
                moveItem(collectionView: collectionView, item: item, toIndexPath: destinationPath)
Carola Nitz's avatar
Carola Nitz committed
199 200
                continue
            }
Kevin Bettin's avatar
Kevin Bettin committed
201
            // Element from another App
202
            let placeholder = UICollectionViewDropPlaceholder(insertionIndexPath: destinationPath, reuseIdentifier: VLCPlaylistCollectionViewCell.cellIdentifier())
Carola Nitz's avatar
Carola Nitz committed
203
            let placeholderContext = coordinator.drop(item.dragItem, to: placeholder)
Kevin Bettin's avatar
Kevin Bettin committed
204
            createFileWith(itemProvider: item.dragItem.itemProvider) {
Carola Nitz's avatar
Carola Nitz committed
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
                [weak self] file, error in

                guard let strongSelf = self else { return }

                if let file = file {
                    placeholderContext.commitInsertion() {
                        insertionIndexPath in
                        strongSelf.delegate?.dragAndDropManagerInsertItem(manager: strongSelf, item: file, atIndexPath: insertionIndexPath)
                    }
                }
                if let error = error as? DropError {
                    strongSelf.handleError(error: error, itemProvider: item.dragItem.itemProvider)
                    placeholderContext.deletePlaceholder()
                }
            }
        }
    }

223 224
    private func moveItem(collectionView: UICollectionView, item: UICollectionViewDropItem, toIndexPath destinationPath: IndexPath) {
        if let mlFile = item.dragItem.localObject as? MLFile, !mlFile.labels.isEmpty && !inFolder() {
Carola Nitz's avatar
Carola Nitz committed
225 226 227
            collectionView.performBatchUpdates({
                collectionView.insertItems(at: [destinationPath])
                delegate?.dragAndDropManagerInsertItem(manager: self, item: mlFile, atIndexPath: destinationPath)
Kevin Bettin's avatar
Kevin Bettin committed
228 229
                delegate?.dragAndDropManagerRemoveFileFromFolder(manager: self, file: mlFile)
            }, completion: nil)
Carola Nitz's avatar
Carola Nitz committed
230 231 232
        }
    }

233
    private func addDragItem(collectionView: UICollectionView, dragItem item: UICollectionViewDropItem, toFolderAt index: IndexPath) {
Carola Nitz's avatar
Carola Nitz committed
234
        if let sourcepath = item.sourceIndexPath {
Kevin Bettin's avatar
Kevin Bettin committed
235
            // local file that just needs to be moved
Carola Nitz's avatar
Carola Nitz committed
236
            collectionView.performBatchUpdates({
Kevin Bettin's avatar
Kevin Bettin committed
237 238 239 240
                if let file = delegate?.dragAndDropManagerRequestsFile(manager: self, atIndexPath: sourcepath) as? MLFile {
                    collectionView.deleteItems(at: [sourcepath])
                    addFile(file: file, toFolderAt: index)
                    delegate?.dragAndDropManagerDeleteItem(manager: self, atIndexPath: sourcepath)
Carola Nitz's avatar
Carola Nitz committed
241
                }
Kevin Bettin's avatar
Kevin Bettin committed
242
            }, completion: nil)
Carola Nitz's avatar
Carola Nitz committed
243 244
        } else {
            // file from other app
Kevin Bettin's avatar
Kevin Bettin committed
245
            createFileWith(itemProvider: item.dragItem.itemProvider) {
Carola Nitz's avatar
Carola Nitz committed
246 247
                [weak self] file, error in
                if let strongSelf = self, let file = file {
Kevin Bettin's avatar
Kevin Bettin committed
248
                    strongSelf.addFile(file: file, toFolderAt: index)
Carola Nitz's avatar
Carola Nitz committed
249 250 251 252 253
                }
            }
        }
    }

254
    // MARK: - DropInteractionDelegate for EmptyView
Carola Nitz's avatar
Carola Nitz committed
255 256 257 258 259 260 261 262 263 264 265

    func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
        return canHandleDropSession(session: session)
    }

    func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
        return UIDropProposal(operation: .copy)
    }

    func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
        for item in session.items {
Kevin Bettin's avatar
Kevin Bettin committed
266
            createFileWith(itemProvider: item.itemProvider) {
Carola Nitz's avatar
Carola Nitz committed
267 268 269 270
                [weak self] _, error in
                if let error = error as? DropError {
                    self?.handleError(error: error, itemProvider: item.itemProvider)
                }
Kevin Bettin's avatar
Kevin Bettin committed
271
                // no need to handle the file case since the libraryVC updates itself after getting a file
Carola Nitz's avatar
Carola Nitz committed
272 273 274 275
            }
        }
    }

276
    // MARK: - Shared Methods
Kevin Bettin's avatar
Kevin Bettin committed
277

278 279 280
    // Checks if the session has items conforming to typeidentifiers
    private func canHandleDropSession(session: UIDropSession) -> Bool {
        if session.localDragSession != nil {
Carola Nitz's avatar
Carola Nitz committed
281 282 283 284 285 286 287 288 289 290 291
            return true
        }
        return session.hasItemsConforming(toTypeIdentifiers: utiTypeIdentifiers)
    }

    /// Returns a drop operation type
    ///
    /// - Parameters:
    ///   - hasActiveDrag: State if the drag started within the app
    ///   - item: UIDragItem from session
    /// - Returns: UIDropOperation
292
    private func dropOperation(hasActiveDrag: Bool, firstSessionItem item: AnyObject?, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UIDropOperation {
Carola Nitz's avatar
Carola Nitz committed
293 294
        let inAlbum = delegate?.dragAndDropManagerCurrentSelection(manager: self) as? MLAlbum != nil
        let inShow = delegate?.dragAndDropManagerCurrentSelection(manager: self) as? MLShow != nil
Kevin Bettin's avatar
Kevin Bettin committed
295 296 297 298
        // you can move files into a folder or copy from anothr app into a folder
        if fileIsFolder(atIndexPath: destinationIndexPath) {
            // no dragging entire shows and albums into folders
            if let dragItem = item, let mlFile = dragItem.localObject as? MLFile, mlFile.isAlbumTrack() || mlFile.isShowEpisode() {
299 300
                return .forbidden
            }
Carola Nitz's avatar
Carola Nitz committed
301 302
            return hasActiveDrag ? .move : .copy
        }
Kevin Bettin's avatar
Kevin Bettin committed
303
        // you can't reorder
Carola Nitz's avatar
Carola Nitz committed
304 305 306
        if inFolder() {
            return hasActiveDrag ? .forbidden : .copy
        }
Kevin Bettin's avatar
Kevin Bettin committed
307
        // you can't reorder in or drag into an Album or Show
Carola Nitz's avatar
Carola Nitz committed
308 309 310
        if inAlbum || inShow {
            return .cancel
        }
Kevin Bettin's avatar
Kevin Bettin committed
311
        // we're dragging a file out of a folder
312
        if let dragItem = item, let mlFile = dragItem.localObject as? MLFile, !mlFile.labels.isEmpty {
Carola Nitz's avatar
Carola Nitz committed
313 314
            return .copy
        }
Kevin Bettin's avatar
Kevin Bettin committed
315
        // no reorder from another app into the top layer
Carola Nitz's avatar
Carola Nitz committed
316 317 318 319 320 321 322 323 324 325
        return hasActiveDrag ? .forbidden : .copy
    }

    /// show an Alert when dropping failed
    ///
    /// - Parameters:
    ///   - error: the type of error that happend
    ///   - itemProvider: the itemProvider to retrieve the suggestedName
    private func handleError(error: DropError, itemProvider: NSItemProvider) {
        let message: String
Kevin Bettin's avatar
Kevin Bettin committed
326
        let filename = itemProvider.suggestedName ?? NSLocalizedString("THIS_FILE", comment: "")
327
        switch error.kind {
Carola Nitz's avatar
Carola Nitz committed
328 329 330 331 332
        case .loadFileRepresentationFailed:
            message = String(format: NSLocalizedString("NOT_SUPPORTED_FILETYPE", comment: ""), filename)
        case .moveFileToDocuments:
            message = String(format: NSLocalizedString("FILE_EXISTS", comment: ""), filename)
        }
Kevin Bettin's avatar
Kevin Bettin committed
333 334
        let alert = UIAlertController(title: NSLocalizedString("ERROR", comment: ""), message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
Carola Nitz's avatar
Carola Nitz committed
335 336 337
        UIApplication.shared.keyWindow?.rootViewController?.present(alert, animated: true, completion: nil)
    }

338
    private func fileIsFolder(atIndexPath indexPath: IndexPath?) -> Bool {
Carola Nitz's avatar
Carola Nitz committed
339 340 341 342 343 344 345
        if let indexPath = indexPath {
            let file = delegate?.dragAndDropManagerRequestsFile(manager: self, atIndexPath: indexPath)
            return file as? MLLabel != nil
        }
        return false
    }

346
    private func fileIsCollection(file: Any?) -> Bool {
Carola Nitz's avatar
Carola Nitz committed
347 348 349 350 351 352
        let isFolder = file as? MLLabel != nil
        let isAlbum = file as? MLAlbum != nil
        let isShow = file as? MLShow != nil
        return isFolder || isAlbum || isShow
    }

353
    private func fileIsCollection(atIndexPath indexPath: IndexPath?) -> Bool {
Carola Nitz's avatar
Carola Nitz committed
354
        if let indexPath = indexPath {
355 356 357
            if let file = delegate?.dragAndDropManagerRequestsFile(manager: self, atIndexPath: indexPath) {
                return fileIsCollection(file:file)
            }
Carola Nitz's avatar
Carola Nitz committed
358 359 360 361
        }
        return false
    }

Kevin Bettin's avatar
Kevin Bettin committed
362
    // creating dragItems for the file at indexpath
363
    private func dragItems(forIndexPath indexPath: IndexPath) -> [UIDragItem] {
Carola Nitz's avatar
Carola Nitz committed
364 365 366 367
        if let file = delegate?.dragAndDropManagerRequestsFile(manager: self, atIndexPath: indexPath) {
            if fileIsCollection(atIndexPath: indexPath) {
                return dragItemsforCollection(file: file)
            }
Kevin Bettin's avatar
Kevin Bettin committed
368
            return dragItem(fromFile: file)
Carola Nitz's avatar
Carola Nitz committed
369 370 371 372 373 374 375 376 377 378
        }
        assert(false, "we can't generate a dragfile if the delegate can't return a file ")
        return []
    }

    /// Iterates over the items of a collection to create dragitems.
    /// Since we're not storing collections as folders we have to provide single files
    ///
    /// - Parameter file: Can be of type MLAlbum, MLLabel or MLShow
    /// - Returns: An array of UIDragItems
379
    private func dragItemsforCollection(file: Any) -> [UIDragItem] {
Carola Nitz's avatar
Carola Nitz committed
380 381 382 383 384 385 386 387 388 389 390 391
        var dragItems = [UIDragItem]()
        var set = Set<AnyHashable>()
        if let folder = file as? MLLabel {
            set = folder.files
        } else if let album = file as? MLAlbum {
            for track in album.tracks {
                if let mlfile = (track as? MLAlbumTrack)?.files.first {
                    _ = set.insert(mlfile)
                }
            }
        } else if let show = file as? MLShow {
            for episode in show.episodes {
392 393
                if let mlfile = (episode as? MLShowEpisode)?.files {
                    set = set.union(mlfile)
Carola Nitz's avatar
Carola Nitz committed
394 395 396 397 398 399
                }
            }
        } else {
            assert(false, "can't get dragitems from a file that is not a collection")
        }
        for convertibleFile in set {
Kevin Bettin's avatar
Kevin Bettin committed
400
            if let mlfile = convertibleFile as? MLFile, let item = dragItem(fromFile: mlfile).first {
Carola Nitz's avatar
Carola Nitz committed
401 402 403 404 405 406
                dragItems.append(item)
            }
        }
        return dragItems
    }

407 408 409
    //Provides an item for other applications
    private func dragItem(fromFile file: Any) -> [UIDragItem] {
        guard let file = mlFile(from: file as AnyObject), let path = file.url else {
Carola Nitz's avatar
Carola Nitz committed
410 411 412 413 414 415 416
            assert(false, "can't create a dragitem if there is no file or the file has no url")
            return []
        }

        let data = try? Data(contentsOf: path, options: .mappedIfSafe)
        let itemProvider = NSItemProvider()
        itemProvider.suggestedName = path.lastPathComponent
Kevin Bettin's avatar
Kevin Bettin committed
417
        // maybe use UTTypeForFileURL
Carola Nitz's avatar
Carola Nitz committed
418
        if let identifiers = try? path.resourceValues(forKeys: [.typeIdentifierKey]), let identifier = identifiers.typeIdentifier {
Kevin Bettin's avatar
Kevin Bettin committed
419
            // here we can show progress
Carola Nitz's avatar
Carola Nitz committed
420 421 422 423 424 425 426 427 428 429 430 431
            itemProvider.registerDataRepresentation(forTypeIdentifier: identifier, visibility: .all) { completion -> Progress? in
                completion(data, nil)
                return nil
            }
            let dragitem = UIDragItem(itemProvider: itemProvider)
            dragitem.localObject = file
            return [dragitem]
        }
        assert(false, "we can't provide a typeidentifier")
        return []
    }

432 433
    private func mlFile(from file: AnyObject) -> MLFile? {
        if let episode = file as? MLShowEpisode, let convertedfile = episode.files.first as? MLFile {
Carola Nitz's avatar
Carola Nitz committed
434 435 436
            return convertedfile
        }

437
        if let track = file as? MLAlbumTrack, let convertedfile = track.files.first as? MLFile {
Carola Nitz's avatar
Carola Nitz committed
438 439 440 441 442 443 444 445 446
            return convertedfile
        }

        if let convertedfile = file as? MLFile {
            return convertedfile
        }
        return nil
    }

447
    private func addFile(file: MLFile, toFolderAt folderIndex: IndexPath) {
Carola Nitz's avatar
Carola Nitz committed
448 449 450 451 452 453 454 455 456 457 458 459 460
        let label = delegate?.dragAndDropManagerRequestsFile(manager: self, atIndexPath: folderIndex) as! MLLabel
        DispatchQueue.main.async {
            _ = label.files.insert(file)
            file.labels = [label]
            file.folderTrackNumber = NSNumber(integerLiteral: label.files.count - 1)
        }
    }

    /// try to create a file from the dropped item
    ///
    /// - Parameters:
    ///   - itemProvider: itemprovider which is used to load the files from
    ///   - completion: callback with the successfully created file or error if it failed
461
    private func createFileWith(itemProvider: NSItemProvider, completion: @escaping ((MLFile?, Error?) -> Void)) {
Carola Nitz's avatar
Carola Nitz committed
462 463 464 465 466 467 468 469 470 471
        itemProvider.loadFileRepresentation(forTypeIdentifier: kUTTypeData as String) {
            [weak self] (url, error) in
            guard let strongSelf = self else { return }

            guard let url = url else {
                DispatchQueue.main.async {
                    completion(nil, DropError(kind: .loadFileRepresentationFailed))
                }
                return
            }
Kevin Bettin's avatar
Kevin Bettin committed
472
            // returns nil for local session but this should also not be called for a local session
Carola Nitz's avatar
Carola Nitz committed
473 474 475 476 477 478 479 480 481 482 483 484
            guard let destinationURL = strongSelf.moveFileToDocuments(fromURL: url) else {
                DispatchQueue.main.async {
                    completion(nil, DropError(kind: .moveFileToDocuments))
                }
                return
            }
            DispatchQueue.global(qos: .background).async {
                let sharedlib = MLMediaLibrary.sharedMediaLibrary() as? MLMediaLibrary
                sharedlib?.addFilePaths([destinationURL.path])

                if let file = MLFile.file(for: destinationURL).first as? MLFile {
                    DispatchQueue.main.async {
Kevin Bettin's avatar
Kevin Bettin committed
485
                        // we dragged into a folder
Carola Nitz's avatar
Carola Nitz committed
486 487 488 489 490 491 492 493 494 495
                        if let selection = strongSelf.delegate?.dragAndDropManagerCurrentSelection(manager: strongSelf) as? MLLabel {
                            file.labels = [selection]
                        }
                        completion(file, nil)
                    }
                }
            }
        }
    }

496
    private func moveFileToDocuments(fromURL filepath: URL?) -> URL? {
Carola Nitz's avatar
Carola Nitz committed
497 498 499 500 501 502 503 504 505 506 507 508 509 510 511
        let searchPaths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
        let newDirectoryPath = searchPaths.first
        guard let directoryPath = newDirectoryPath, let url = filepath else {
            return nil
        }
        let destinationURL = URL(fileURLWithPath: "\(directoryPath)" + "/" + "\(url.lastPathComponent)")
        do {
            try FileManager.default.moveItem(at: url, to: destinationURL)
        } catch let error {
            print(error.localizedDescription)
            return nil
        }
        return destinationURL
    }
}