diff --git a/SDO-Mobile.xcodeproj/project.pbxproj b/SDO-Mobile.xcodeproj/project.pbxproj index ec55dbb..1bae429 100644 --- a/SDO-Mobile.xcodeproj/project.pbxproj +++ b/SDO-Mobile.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 052DCA792D060135003F7891 /* ImageInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 052DCA782D060132003F7891 /* ImageInfoView.swift */; }; 052DCA7B2D0603A7003F7891 /* ImageInfoDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 052DCA7A2D0603A5003F7891 /* ImageInfoDetailView.swift */; }; 05957BDF2D060BD200E021AA /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05957BDE2D060BCF00E021AA /* VideoView.swift */; }; + 05957BE42D06281200E021AA /* VideoDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05957BE32D06280E00E021AA /* VideoDownload.swift */; }; 05CE14112D05D30300C757FD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 05CE140B2D05D30300C757FD /* Preview Assets.xcassets */; }; 05CE14122D05D30300C757FD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 05CE140D2D05D30300C757FD /* Assets.xcassets */; }; 05CE14132D05D30300C757FD /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05CE140E2D05D30300C757FD /* ContentView.swift */; }; @@ -31,6 +32,7 @@ 052DCA782D060132003F7891 /* ImageInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageInfoView.swift; sourceTree = ""; }; 052DCA7A2D0603A5003F7891 /* ImageInfoDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageInfoDetailView.swift; sourceTree = ""; }; 05957BDE2D060BCF00E021AA /* VideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView.swift; sourceTree = ""; }; + 05957BE32D06280E00E021AA /* VideoDownload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDownload.swift; sourceTree = ""; }; 05CE13FA2D05D2BC00C757FD /* SDO.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SDO.app; sourceTree = BUILT_PRODUCTS_DIR; }; 05CE140B2D05D30300C757FD /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 05CE140D2D05D30300C757FD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -107,6 +109,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 05957BE02D0627F900E021AA /* Utility */ = { + isa = PBXGroup; + children = ( + 05CE147E2D05E65B00C757FD /* PreviewData.swift */, + 05957BE32D06280E00E021AA /* VideoDownload.swift */, + ); + path = Utility; + sourceTree = ""; + }; 05CE13F12D05D2BC00C757FD = { isa = PBXGroup; children = ( @@ -128,7 +139,6 @@ 05CE140C2D05D30300C757FD /* Preview Content */ = { isa = PBXGroup; children = ( - 05CE147E2D05E65B00C757FD /* PreviewData.swift */, 05CE140B2D05D30300C757FD /* Preview Assets.xcassets */, ); path = "Preview Content"; @@ -137,6 +147,7 @@ 05CE14102D05D30300C757FD /* SDO-Mobile */ = { isa = PBXGroup; children = ( + 05957BE02D0627F900E021AA /* Utility */, 05CE144F2D05D51D00C757FD /* Views */, 05CE140D2D05D30300C757FD /* Assets.xcassets */, 05CE144D2D05D51900C757FD /* Info.plist */, @@ -406,6 +417,7 @@ 052DCA7B2D0603A7003F7891 /* ImageInfoDetailView.swift in Sources */, 05CE14132D05D30300C757FD /* ContentView.swift in Sources */, 05CE14142D05D30300C757FD /* SDOApp.swift in Sources */, + 05957BE42D06281200E021AA /* VideoDownload.swift in Sources */, 05CE14782D05E14300C757FD /* ImageView.swift in Sources */, 05957BDF2D060BD200E021AA /* VideoView.swift in Sources */, 05CE14762D05DE6F00C757FD /* ImageData.swift in Sources */, diff --git a/SDO-Mobile/Preview Content/PreviewData.swift b/SDO-Mobile/Utility/PreviewData.swift similarity index 100% rename from SDO-Mobile/Preview Content/PreviewData.swift rename to SDO-Mobile/Utility/PreviewData.swift diff --git a/SDO-Mobile/Utility/VideoDownload.swift b/SDO-Mobile/Utility/VideoDownload.swift new file mode 100644 index 0000000..cb9bff0 --- /dev/null +++ b/SDO-Mobile/Utility/VideoDownload.swift @@ -0,0 +1,113 @@ +/******************************************************************************* + * The MIT License (MIT) + * + * Copyright (c) 2023, Jean-David Gadina - www.xs-labs.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the Software), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +import Foundation + +public class VideoDownload +{ + public var url: URL? + + private let remoteURL: URL + private var completion: ( ( VideoDownload ) -> Void )? + + private static var downloads: [ VideoDownload ] = [] + + public init( remoteURL: URL, completion: @escaping ( VideoDownload ) -> Void ) + { + self.remoteURL = remoteURL + self.completion = completion + + let session = URLSession( configuration: .ephemeral ) + let request = URLRequest( url: remoteURL ) + let task = session.downloadTask( with: request ) + { + location, response, error in + + guard let location = location, + let response = response as? HTTPURLResponse, + response.statusCode == 200 + else + { + self.complete() + + return + } + + let copy = URL( fileURLWithPath: NSTemporaryDirectory() ).appendingPathComponent( NSUUID().uuidString ).appendingPathExtension( "mp4" ) + + do + { + try FileManager.default.copyItem( at: location, to: copy ) + } + catch + { + self.complete() + + return + } + + DispatchQueue.main.async + { + self.url = copy + + self.complete() + } + } + + task.resume() + } + + private func complete() + { + DispatchQueue.main.async + { + self.completion?( self ) + VideoDownload.downloads.removeAll( where: { $0 === self } ) + + self.completion = nil + } + } + + deinit + { + if let url = self.url + { + try? FileManager.default.removeItem( at: url ) + } + } + + public class func download( video: String, completion: @escaping ( VideoDownload ) -> Void ) + { + guard let url = URL( string: "https://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/\( video )" ) + else + { + return + } + + DispatchQueue.main.async + { + VideoDownload.downloads.append( VideoDownload( remoteURL: url, completion: completion ) ) + } + } +} diff --git a/SDO-Mobile/Views/ContentView.swift b/SDO-Mobile/Views/ContentView.swift index d100375..97880c5 100644 --- a/SDO-Mobile/Views/ContentView.swift +++ b/SDO-Mobile/Views/ContentView.swift @@ -35,7 +35,7 @@ struct ContentView: View { if self.images.isEmpty { - LoadingView() + LoadingView( text: "Loading Latest Images from SDO\nPlease Wait" ) } else { diff --git a/SDO-Mobile/Views/ImageView.swift b/SDO-Mobile/Views/ImageView.swift index d8ad758..bb805b9 100644 --- a/SDO-Mobile/Views/ImageView.swift +++ b/SDO-Mobile/Views/ImageView.swift @@ -72,7 +72,7 @@ struct ImageView: View } .popover( isPresented: $isShowingVideoPopover, arrowEdge: .bottom ) { - VideoView( video: video ).padding() + VideoView( video: video ) } } } diff --git a/SDO-Mobile/Views/LoadingView.swift b/SDO-Mobile/Views/LoadingView.swift index ac8376e..079c94b 100644 --- a/SDO-Mobile/Views/LoadingView.swift +++ b/SDO-Mobile/Views/LoadingView.swift @@ -26,17 +26,19 @@ import SwiftUI struct LoadingView: View { + public var text: String + var body: some View { VStack { ProgressView().scaleEffect( 1.5 ).padding() - Text( "Loading Latest Images from SDO\nPlease Wait" ).foregroundStyle( .secondary ).multilineTextAlignment( .center ) + Text( self.text ).foregroundStyle( .secondary ).multilineTextAlignment( .center ) } } } #Preview { - LoadingView() + LoadingView( text: "Loading..." ).padding() } diff --git a/SDO-Mobile/Views/VideoView.swift b/SDO-Mobile/Views/VideoView.swift index c4a0cc5..c6ff0e3 100644 --- a/SDO-Mobile/Views/VideoView.swift +++ b/SDO-Mobile/Views/VideoView.swift @@ -27,28 +27,35 @@ import SwiftUI struct VideoView: View { - @State public var video: String + @State public var video: String + @State public var download: VideoDownload? var body: some View { - if let player = self.player + ZStack { - VideoPlayer( player: player ) + if let url = self.download?.url + { + VideoPlayer( player: self.player( url: url ) ) + } + else + { + LoadingView( text: "Loading Video - Please Wait" ) + } } - else + .frame( maxWidth: .infinity, maxHeight: .infinity ) + .background( .black ) + .onAppear { - Label( "Invalid video URL", systemImage: "video.slash.fill" ) + VideoDownload.download( video: self.video ) + { + self.download = $0 + } } } - private var player: AVPlayer? + private func player( url: URL ) -> AVPlayer? { - guard let url = URL( string: "https://sdo.gsfc.nasa.gov/assets/img/latest/mpeg/\( self.video )" ) - else - { - return nil - } - let player = AVPlayer( url: url ) player.play() @@ -59,5 +66,5 @@ struct VideoView: View #Preview { - VideoView( video: PreviewData.images.first!.video! ).padding() + VideoView( video: PreviewData.images.first!.video! ) }