diff --git a/Front Row/Support/Extensions.swift b/Front Row/Support/Extensions.swift index af09cc2..00966ba 100644 --- a/Front Row/Support/Extensions.swift +++ b/Front Row/Support/Extensions.swift @@ -6,6 +6,84 @@ // import Foundation +import SwiftUI + +struct AnyDropDelegate: DropDelegate { + var isTargeted: Binding? + var onValidate: ((DropInfo) -> Bool)? + let onPerform: (DropInfo) -> Bool + var onEntered: ((DropInfo) -> Void)? + var onExited: ((DropInfo) -> Void)? + var onUpdated: ((DropInfo) -> DropProposal?)? + + func performDrop(info: DropInfo) -> Bool { + onPerform(info) + } + + func validateDrop(info: DropInfo) -> Bool { + onValidate?(info) ?? true + } + + func dropEntered(info: DropInfo) { + isTargeted?.wrappedValue = true + onEntered?(info) + } + + func dropExited(info: DropInfo) { + isTargeted?.wrappedValue = false + onExited?(info) + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + onUpdated?(info) + } +} + +extension NSItemProvider: @unchecked Sendable {} + +extension NSItemProvider { + func loadObject(ofClass: T.Type) async throws -> T? where T: NSItemProviderReading { + try await withCheckedThrowingContinuation { continuation in + _ = loadObject(ofClass: ofClass) { data, error in + if let error { + continuation.resume(throwing: error) + return + } + + guard let object = data as? T else { + continuation.resume(returning: nil) + return + } + + continuation.resume(returning: object) + } + } + } + + func loadObject(ofClass: T.Type) async throws -> T? + where T: _ObjectiveCBridgeable, T._ObjectiveCType: NSItemProviderReading { + try await withCheckedThrowingContinuation { continuation in + _ = loadObject(ofClass: ofClass) { data, error in + if let error { + continuation.resume(throwing: error) + return + } + + guard let data else { + continuation.resume(returning: nil) + return + } + + continuation.resume(returning: data) + } + } + } + + /// Get a URL from the item provider, if any. + func getURL() async -> URL? { + try? await loadObject(ofClass: URL.self) + } +} extension NSSize { var aspect: CGFloat { diff --git a/Front Row/Support/PlayEngine.swift b/Front Row/Support/PlayEngine.swift index d9615e3..54b1391 100644 --- a/Front Row/Support/PlayEngine.swift +++ b/Front Row/Support/PlayEngine.swift @@ -12,6 +12,14 @@ import SwiftUI static let shared = PlayEngine() + static let supportedFileTypes: [UTType] = [ + .mp3, + .mpeg2TransportStream, + .mpeg4Audio, + .mpeg4Movie, + .quickTimeMovie, + ] + var player = AVPlayer() var isLoaded = false @@ -72,7 +80,7 @@ import SwiftUI @MainActor func showOpenFileDialog() async { let panel = NSOpenPanel() - panel.allowedContentTypes = [.mpeg4Movie] + panel.allowedContentTypes = PlayEngine.supportedFileTypes panel.allowsMultipleSelection = false panel.canChooseDirectories = false panel.canChooseFiles = true diff --git a/Front Row/Views/ContentView.swift b/Front Row/Views/ContentView.swift index 49f8c2d..89d4ff3 100644 --- a/Front Row/Views/ContentView.swift +++ b/Front Row/Views/ContentView.swift @@ -12,16 +12,24 @@ struct ContentView: View { var body: some View { VideoPlayer(player: PlayEngine.shared.player) .onDrop( - of: [.mpeg4Movie], isTargeted: nil, - perform: { providers -> Bool in - guard let provider = providers.first else { return false } - provider.loadItem(forTypeIdentifier: UTType.mpeg4Movie.identifier, options: nil) - { (urlData, _) in - guard let url = urlData as? URL else { return } - PlayEngine.shared.openFile(url: url) + of: [.fileURL], + delegate: AnyDropDelegate( + onValidate: { + $0.hasItemsConforming(to: PlayEngine.supportedFileTypes) + }, + onPerform: { + guard let provider = $0.itemProviders(for: [.fileURL]).first else { + return false + } + + Task { + guard let url = await provider.getURL() else { return } + PlayEngine.shared.openFile(url: url) + } + + return true } - return true - } + ) ) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .ignoresSafeArea() diff --git a/FrontRowInfo.plist b/FrontRowInfo.plist index 85da9b7..105b3ee 100644 --- a/FrontRowInfo.plist +++ b/FrontRowInfo.plist @@ -8,9 +8,61 @@ CFBundleTypeExtensions mp4 + m4v CFBundleTypeName - MPEG4 Video File + MPEG-4 Video + CFBundleTypeRole + Viewer + LSHandlerRank + Default + + + CFBundleTypeExtensions + + m4a + + CFBundleTypeName + MPEG-4 Audio + CFBundleTypeRole + Viewer + LSHandlerRank + Default + + + CFBundleTypeExtensions + + mov + qt + + CFBundleTypeName + QuickTime Media + CFBundleTypeRole + Viewer + LSHandlerRank + Default + + + CFBundleTypeExtensions + + ts + mts + m2ts + + CFBundleTypeName + MPEG Transport Stream + CFBundleTypeRole + Viewer + LSHandlerRank + Default + + + CFBundleTypeExtensions + + mp3 + + CFBundleTypeName + MPEG Layer III Audio CFBundleTypeRole Viewer LSHandlerRank