diff --git a/Aerial.saver/Contents/Frameworks/libswiftAVFoundation.dylib b/Aerial.saver/Contents/Frameworks/libswiftAVFoundation.dylib deleted file mode 100755 index de05f8c4..00000000 Binary files a/Aerial.saver/Contents/Frameworks/libswiftAVFoundation.dylib and /dev/null differ diff --git a/Aerial.saver/Contents/Frameworks/libswiftAppKit.dylib b/Aerial.saver/Contents/Frameworks/libswiftAppKit.dylib deleted file mode 100755 index 1180a54e..00000000 Binary files a/Aerial.saver/Contents/Frameworks/libswiftAppKit.dylib and /dev/null differ diff --git a/Aerial.saver/Contents/Frameworks/libswiftCore.dylib b/Aerial.saver/Contents/Frameworks/libswiftCore.dylib deleted file mode 100755 index 3ed375dd..00000000 Binary files a/Aerial.saver/Contents/Frameworks/libswiftCore.dylib and /dev/null differ diff --git a/Aerial.saver/Contents/Frameworks/libswiftCoreAudio.dylib b/Aerial.saver/Contents/Frameworks/libswiftCoreAudio.dylib deleted file mode 100755 index 895367df..00000000 Binary files a/Aerial.saver/Contents/Frameworks/libswiftCoreAudio.dylib and /dev/null differ diff --git a/Aerial.saver/Contents/Frameworks/libswiftCoreData.dylib b/Aerial.saver/Contents/Frameworks/libswiftCoreData.dylib deleted file mode 100755 index 05013325..00000000 Binary files a/Aerial.saver/Contents/Frameworks/libswiftCoreData.dylib and /dev/null differ diff --git a/Aerial.saver/Contents/Frameworks/libswiftCoreGraphics.dylib b/Aerial.saver/Contents/Frameworks/libswiftCoreGraphics.dylib deleted file mode 100755 index 60d5d821..00000000 Binary files a/Aerial.saver/Contents/Frameworks/libswiftCoreGraphics.dylib and /dev/null differ diff --git a/Aerial.saver/Contents/Frameworks/libswiftCoreImage.dylib b/Aerial.saver/Contents/Frameworks/libswiftCoreImage.dylib deleted file mode 100755 index 45292d33..00000000 Binary files a/Aerial.saver/Contents/Frameworks/libswiftCoreImage.dylib and /dev/null differ diff --git a/Aerial.saver/Contents/Frameworks/libswiftCoreMedia.dylib b/Aerial.saver/Contents/Frameworks/libswiftCoreMedia.dylib deleted file mode 100755 index 56dc0158..00000000 Binary files a/Aerial.saver/Contents/Frameworks/libswiftCoreMedia.dylib and /dev/null differ diff --git a/Aerial.saver/Contents/Frameworks/libswiftDarwin.dylib b/Aerial.saver/Contents/Frameworks/libswiftDarwin.dylib deleted file mode 100755 index 15e5db90..00000000 Binary files a/Aerial.saver/Contents/Frameworks/libswiftDarwin.dylib and /dev/null differ diff --git a/Aerial.saver/Contents/Frameworks/libswiftDispatch.dylib b/Aerial.saver/Contents/Frameworks/libswiftDispatch.dylib deleted file mode 100755 index 81edc42c..00000000 Binary files a/Aerial.saver/Contents/Frameworks/libswiftDispatch.dylib and /dev/null differ diff --git a/Aerial.saver/Contents/Frameworks/libswiftFoundation.dylib b/Aerial.saver/Contents/Frameworks/libswiftFoundation.dylib deleted file mode 100755 index ac63bc71..00000000 Binary files a/Aerial.saver/Contents/Frameworks/libswiftFoundation.dylib and /dev/null differ diff --git a/Aerial.saver/Contents/Frameworks/libswiftObjectiveC.dylib b/Aerial.saver/Contents/Frameworks/libswiftObjectiveC.dylib deleted file mode 100755 index 0cc95355..00000000 Binary files a/Aerial.saver/Contents/Frameworks/libswiftObjectiveC.dylib and /dev/null differ diff --git a/Aerial.saver/Contents/Info.plist b/Aerial.saver/Contents/Info.plist deleted file mode 100644 index 7495e3c9..00000000 --- a/Aerial.saver/Contents/Info.plist +++ /dev/null @@ -1,53 +0,0 @@ - - - - - BuildMachineOSBuild - 15B42 - CFBundleDevelopmentRegion - en - CFBundleExecutable - Aerial - CFBundleIdentifier - com.JohnCoates.Aerial - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Aerial - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.2 - CFBundleSignature - ???? - CFBundleSupportedPlatforms - - MacOSX - - CFBundleVersion - 1.2.0 - DTCompiler - com.apple.compilers.llvm.clang.1_0 - DTPlatformBuild - 7B91b - DTPlatformVersion - GM - DTSDKBuild - 15A278 - DTSDKName - macosx10.11 - DTXcode - 0710 - DTXcodeBuild - 7B91b - LSMinimumSystemVersion - 10.9 - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSPrincipalClass - AerialView - - diff --git a/Aerial.saver/Contents/MacOS/Aerial b/Aerial.saver/Contents/MacOS/Aerial deleted file mode 100755 index 7650971b..00000000 Binary files a/Aerial.saver/Contents/MacOS/Aerial and /dev/null differ diff --git a/Aerial.saver/Contents/Resources/PreferencesWindow.nib b/Aerial.saver/Contents/Resources/PreferencesWindow.nib deleted file mode 100644 index 6737559f..00000000 Binary files a/Aerial.saver/Contents/Resources/PreferencesWindow.nib and /dev/null differ diff --git a/Aerial.saver/Contents/Resources/icon-day.pdf b/Aerial.saver/Contents/Resources/icon-day.pdf deleted file mode 100644 index 27c1445d..00000000 Binary files a/Aerial.saver/Contents/Resources/icon-day.pdf and /dev/null differ diff --git a/Aerial.saver/Contents/Resources/icon-night.pdf b/Aerial.saver/Contents/Resources/icon-night.pdf deleted file mode 100644 index 3a2e1fee..00000000 Binary files a/Aerial.saver/Contents/Resources/icon-night.pdf and /dev/null differ diff --git a/Aerial.saver/Contents/Resources/thumbnail.png b/Aerial.saver/Contents/Resources/thumbnail.png deleted file mode 100644 index d86a1918..00000000 Binary files a/Aerial.saver/Contents/Resources/thumbnail.png and /dev/null differ diff --git a/Aerial.xcodeproj/project.pbxproj b/Aerial.xcodeproj/project.pbxproj index 5fec6c9a..16eeab9f 100644 --- a/Aerial.xcodeproj/project.pbxproj +++ b/Aerial.xcodeproj/project.pbxproj @@ -10,12 +10,16 @@ 030D9B7C21551A8D00961E95 /* AerialPlayerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E2E5D1FC62E8B00E5F320 /* AerialPlayerItem.swift */; }; 03233B68217272640077D3F9 /* PoiStringProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03233B67217272640077D3F9 /* PoiStringProvider.swift */; }; 03233B692172762C0077D3F9 /* PoiStringProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03233B67217272640077D3F9 /* PoiStringProvider.swift */; }; + 033192E1217B78240073B580 /* en.json in Resources */ = {isa = PBXBuildFile; fileRef = 033192E0217B78240073B580 /* en.json */; }; + 033192E2217B78240073B580 /* en.json in Resources */ = {isa = PBXBuildFile; fileRef = 033192E0217B78240073B580 /* en.json */; }; 033D62AB216CADCD00F3AF83 /* icon-day-dark.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 033D62AA216CADCD00F3AF83 /* icon-day-dark.pdf */; }; 033D62AC216CADCD00F3AF83 /* icon-day-dark.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 033D62AA216CADCD00F3AF83 /* icon-day-dark.pdf */; }; 033D62AD216CADCD00F3AF83 /* icon-day-dark.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 033D62AA216CADCD00F3AF83 /* icon-day-dark.pdf */; }; 033D62AF216CAE2C00F3AF83 /* icon-night-dark.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 033D62AE216CAE2C00F3AF83 /* icon-night-dark.pdf */; }; 033D62B0216CAE2C00F3AF83 /* icon-night-dark.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 033D62AE216CAE2C00F3AF83 /* icon-night-dark.pdf */; }; 033D62B1216CAE2C00F3AF83 /* icon-night-dark.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 033D62AE216CAE2C00F3AF83 /* icon-night-dark.pdf */; }; + 03893CB3217749F0008E7125 /* ErrorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03893CB2217749F0008E7125 /* ErrorLog.swift */; }; + 03893CB4217753AC008E7125 /* ErrorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03893CB2217749F0008E7125 /* ErrorLog.swift */; }; 0393857A2175D4B80040B850 /* AVPlayerViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039385792175D4B80040B850 /* AVPlayerViewExtension.swift */; }; 0393857B2175D4B80040B850 /* AVPlayerViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039385792175D4B80040B850 /* AVPlayerViewExtension.swift */; }; 0393857C2175D4B80040B850 /* AVPlayerViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039385792175D4B80040B850 /* AVPlayerViewExtension.swift */; }; @@ -59,8 +63,6 @@ FAC36F651BE1772F007F2A20 /* CollectionType+Shuffling.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F631BE1772F007F2A20 /* CollectionType+Shuffling.swift */; }; FAC36F671BE1778C007F2A20 /* ManifestLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F661BE1778C007F2A20 /* ManifestLoader.swift */; }; FAC36F681BE1778C007F2A20 /* ManifestLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F661BE1778C007F2A20 /* ManifestLoader.swift */; }; - FAC36F6A1BE1780B007F2A20 /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F691BE1780B007F2A20 /* Debug.swift */; }; - FAC36F6B1BE1780B007F2A20 /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F691BE1780B007F2A20 /* Debug.swift */; }; FAF450211BE2B45D00C1F98A /* VideoLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF450201BE2B45D00C1F98A /* VideoLoader.swift */; }; FAF450221BE2B45D00C1F98A /* VideoLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF450201BE2B45D00C1F98A /* VideoLoader.swift */; }; FAF450241BE2D2FD00C1F98A /* VideoCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF450231BE2D2FD00C1F98A /* VideoCache.swift */; }; @@ -79,8 +81,10 @@ /* Begin PBXFileReference section */ 03233B67217272640077D3F9 /* PoiStringProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoiStringProvider.swift; sourceTree = ""; }; + 033192E0217B78240073B580 /* en.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = en.json; sourceTree = ""; }; 033D62AA216CADCD00F3AF83 /* icon-day-dark.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = "icon-day-dark.pdf"; sourceTree = ""; }; 033D62AE216CAE2C00F3AF83 /* icon-night-dark.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = "icon-night-dark.pdf"; sourceTree = ""; }; + 03893CB2217749F0008E7125 /* ErrorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLog.swift; sourceTree = ""; }; 039385792175D4B80040B850 /* AVPlayerViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerViewExtension.swift; sourceTree = ""; }; 03A2CB9B216BA9AF0061E8E8 /* VideoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoManager.swift; sourceTree = ""; }; 03E8730B2165013C002B469B /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = ""; }; @@ -110,7 +114,6 @@ FAC36F441BE1756D007F2A20 /* CheckCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CheckCellView.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; FAC36F631BE1772F007F2A20 /* CollectionType+Shuffling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CollectionType+Shuffling.swift"; sourceTree = ""; }; FAC36F661BE1778C007F2A20 /* ManifestLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ManifestLoader.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - FAC36F691BE1780B007F2A20 /* Debug.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Debug.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; FACAF1A51BD9FC6000E539DC /* Aerial.saver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Aerial.saver; sourceTree = BUILT_PRODUCTS_DIR; }; FAF450201BE2B45D00C1F98A /* VideoLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = VideoLoader.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; FAF450231BE2D2FD00C1F98A /* VideoCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = VideoCache.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; @@ -142,6 +145,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 033192DF217B77E90073B580 /* Community */ = { + isa = PBXGroup; + children = ( + 033192E0217B78240073B580 /* en.json */, + ); + path = Community; + sourceTree = ""; + }; 03E8730D216501B3002B469B /* Downloads */ = { isa = PBXGroup; children = ( @@ -191,6 +202,7 @@ FAC36F361BE1756D007F2A20 /* Resources */ = { isa = PBXGroup; children = ( + 033192DF217B77E90073B580 /* Community */, 033D62AA216CADCD00F3AF83 /* icon-day-dark.pdf */, FAC36F371BE1756D007F2A20 /* icon-day.pdf */, FAC36F381BE1756D007F2A20 /* icon-night.pdf */, @@ -231,7 +243,7 @@ FAC36F621BE17701007F2A20 /* Extensions */, FAC36F401BE1756D007F2A20 /* AerialVideo.swift */, FAC36F661BE1778C007F2A20 /* ManifestLoader.swift */, - FAC36F691BE1780B007F2A20 /* Debug.swift */, + 03893CB2217749F0008E7125 /* ErrorLog.swift */, 03E8731221675FE0002B469B /* TimeManagement.swift */, ); path = Models; @@ -409,6 +421,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 033192E2217B78240073B580 /* en.json in Resources */, FAC36F541BE1756D007F2A20 /* PreferencesWindow.xib in Resources */, FAC36F4E1BE1756D007F2A20 /* icon-day.pdf in Resources */, 033D62B0216CAE2C00F3AF83 /* icon-night-dark.pdf in Resources */, @@ -439,6 +452,7 @@ FAC36F551BE1756D007F2A20 /* thumbnail.png in Resources */, 033D62AF216CAE2C00F3AF83 /* icon-night-dark.pdf in Resources */, FAC36F4F1BE1756D007F2A20 /* icon-night.pdf in Resources */, + 033192E1217B78240073B580 /* en.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -465,6 +479,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 03893CB4217753AC008E7125 /* ErrorLog.swift in Sources */, 03233B692172762C0077D3F9 /* PoiStringProvider.swift in Sources */, 03A2CB9D216BB1490061E8E8 /* VideoManager.swift in Sources */, 03E87314216760B7002B469B /* TimeManagement.swift in Sources */, @@ -476,7 +491,6 @@ FAC36F5C1BE1756D007F2A20 /* AerialView.swift in Sources */, FAC36F681BE1778C007F2A20 /* ManifestLoader.swift in Sources */, FAC36F581BE1756D007F2A20 /* PreferencesWindowController.swift in Sources */, - FAC36F6B1BE1780B007F2A20 /* Debug.swift in Sources */, FAB22A7F1BE17D7D0065C0F5 /* AssetLoaderDelegate.swift in Sources */, FAC36F601BE175CF007F2A20 /* AppDelegate.swift in Sources */, FA36BD401BE57F8E00D5E03B /* VideoDownload.swift in Sources */, @@ -510,10 +524,10 @@ FAC36F571BE1756D007F2A20 /* PreferencesWindowController.swift in Sources */, FAC36F671BE1778C007F2A20 /* ManifestLoader.swift in Sources */, FAC36F591BE1756D007F2A20 /* AerialVideo.swift in Sources */, + 03893CB3217749F0008E7125 /* ErrorLog.swift in Sources */, AA7E2E5E1FC62E8B00E5F320 /* AerialPlayerItem.swift in Sources */, 03A2CB9C216BA9AF0061E8E8 /* VideoManager.swift in Sources */, FAF450211BE2B45D00C1F98A /* VideoLoader.swift in Sources */, - FAC36F6A1BE1780B007F2A20 /* Debug.swift in Sources */, 03E8731321675FE0002B469B /* TimeManagement.swift in Sources */, FAB22A7E1BE17D7D0065C0F5 /* AssetLoaderDelegate.swift in Sources */, 03233B68217272640077D3F9 /* PoiStringProvider.swift in Sources */, diff --git a/Aerial/App/AppDelegate.swift b/Aerial/App/AppDelegate.swift index 59623b20..c3174c51 100644 --- a/Aerial/App/AppDelegate.swift +++ b/Aerial/App/AppDelegate.swift @@ -16,14 +16,15 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { let objects = objectsFromNib(loadNibNamed: "PreferencesWindow") - - guard let windowIndex = objects.index(where: { $0 is NSWindow }), - let preferencesWindow = objects[windowIndex] as? NSWindow - else { - fatalError("Missing window object") + + // We need to find the correct window in our nib + for object in objects { + if object is NSWindow { + if (object as! NSWindow).identifier!.rawValue == "preferencesWindow" { + setUp(preferencesWindow: (object as! NSWindow)) + } + } } - - setUp(preferencesWindow: preferencesWindow) } private func setUp(preferencesWindow window: NSWindow) { diff --git a/Aerial/App/Resources/Info.plist b/Aerial/App/Resources/Info.plist index 4772a79e..6e49dcc9 100644 --- a/Aerial/App/Resources/Info.plist +++ b/Aerial/App/Resources/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.1 + 1.4.2test9 CFBundleSignature ???? CFBundleVersion diff --git a/Aerial/Resources/Community/en.json b/Aerial/Resources/Community/en.json new file mode 100644 index 00000000..7f7f946a --- /dev/null +++ b/Aerial/Resources/Community/en.json @@ -0,0 +1,358 @@ +{ + "version" : "0", + "language" : "en", + "assets" : [ + { + "id" : "6C3D54AE-0871-498A-81D0-56ED24E5FE9F", + "name" : "Korea and Japan Night" + }, + { + "id" : "B876B645-3955-420E-99DF-60139E451CF3", + "name" :"Wulingyuan National Park 1" + }, + { + "id" : "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C", + "name" : "Longji Rice Terraces" + }, + { + "id" : "D5E76230-81A3-4F65-A1BA-51B8CADED625", + "name" : "Wulingyuan National Park 2" + }, + { + "id" : "b6-1", + "name" : "Great Wall 1" + }, + { + "id" : "b2-1", + "name" :"Great Wall 2" + }, + { + "id" : "b5-1", + "name" : "Great Wall 3" + }, + + { + "id" : "AC9C09DD-1D97-4013-A09F-B0F5259E64C3", + "name" : "Sheikh Zayed Road" + }, + { + "id" : "49790B7C-7D8C-466C-A09E-83E38B6BE87A", + "name" :"Marina 1" + }, + { + "id" : "02EA5DBE-3A67-4DFA-8528-12901DFD6CC1", + "name" : "Downtown" + }, + { + "id" : "802866E6-4AAF-4A69-96EA-C582651391F1", + "name" : "Marina 2" + }, + + { + "id" : "BAF76353-3475-4855-B7E1-CE96CC9BC3A7", + "name" : "Approaching Burj Khalifa" + }, + { + "id" : "2F11E857-4F77-4476-8033-4A1E4610AFCC", + "name" : "Sheikh Zayed Road" + }, + + { + "id" : "E4ED0B22-EB81-4D4F-A29E-7E1EA6B6D980", + "name" : "Nuussuaq Peninsula" + }, + { + "id" : "30047FDA-3AE3-4E74-9575-3520AD77865B", + "name" : "Ilulissat Icefjord" + }, + + { + "id" : "7D4710EB-5BA4-42E6-AA60-68D77F67D9B9", + "name" : "Ilulissat Icefjord" + }, + + { + "id" : "b7-1", + "name" : "Laupāhoehoe Nui" + }, + { + "id" : "b1-1", + "name" : "Waimanu Valley" + }, + { + "id" : "b2-2", + "name" : "Honopū Valley" + }, + { + "id" : "b4-1", + "name" : "Pu‘u O ‘Umi" + }, + + { + "id" : "b6-2", + "name" : "Kohala coastline" + }, + { + "id" : "b8-1", + "name" : "Pu‘u O ‘Umi" + }, + + { + "id" : "102C19D1-9D9F-48EC-B492-074C985C4D9F", + "name" : "Victoria Harbour 1" + }, + { + "id" : "560E09E8-E89D-4ADB-8EEA-4754415383D4", + "name" : "Victoria Peak" + }, + { + "id" : "024891DE-B7F6-4187-BFE0-E6D237702EF0", + "name" : "Wan Chai" + }, + { + "id" : "786E674C-BB22-4AA9-9BD3-114D2020EC4D", + "name" : "Victoria Harbour 2" + }, + + { + "id" : "30313BC1-BF20-45EB-A7B1-5A6FFDBD2488", + "name" : "Victoria Harbour" + }, + + { + "id" : "6E2FC8AC-832D-46CF-B306-BB2A05030C17", + "name" : "Liwa Oasis" + }, + + { + "id" : "b6-3", + "name" : "River Thames", + "pointsOfInterest" : { + "0" : "Passing the Gherkin in the City of London, on the right", + "15" : "Approaching Tower Bridge on the River Thames, with City Hall behind and the Tower of London to the right", + "40" : "Approaching HMS Belfast in the River Thames, passing the Walkie-Talkie on the right", + "75" : "Traveling west up the River Thames, with The Shard on the left and St Paul’s Cathedral on the right", + "95" : "Passing over Southwark Cathedral, with St Paul's Cathedral on the right", + "130" : "Passing Shakespeare’s Globe in Southwark (right foreground)", + "195" : "Passing Waterloo Station on the left, with the Palace of Westminster (Houses of Parliament) across the Thames behind", + "210" : "Heading toward Westminster over the River Thames" + } + }, + { + "id" : "b5-2", + "name" :"Buckingham Palace", + "pointsOfInterest" : { + "0" : "Flying over Buckingham Palace and the Victoria Memorial, London", + "25" : "Passing Wellington Barracks on the right, following Birdcage Walk along St James’s Park", + "65" : "Passing over the Foreign & Commonwealth Office in Whitehall, with Horse Guards Parade on the left and Her Majesty’s Treasury on the right", + "75" : "Passing over the Cenotaph, with the Ministy of Defence on the left and Big Ben and the Palace of Westmintser (Houses of Parliament) on the right", + "95" : "Crossing the River Thames, flying over the London Eye", + "130" : "Passing over Waterloo Station in South London", + "242" : "Passing Southwark Cathedral on the left", + "255" : "Passing The Shard on the right", + "275" : "Passing the cruiser HMS Belfast in the River Thames", + "300" : "Crossing the River Thames at Tower Bridge, with City Hall on the right and the Tower of London on the left" + } + }, + + { + "id" : "b1-2", + "name" : "River Thames near Sunset", + "pointsOfInterest" : { + "0" : "Heading up the River Thames in London toward Tower Bridge", + "25" : "Crossing the River Thames and passing over City Hall", + "35" : "Passing the cruiser HMS Belfast in the River Thames", + "60" : "Approaching The Shard", + "92" : "Flying west over Southwark toward Lambeth in South London", + "200" : "Passing Lambeth Palace on the left and Waterloo Station on the right", + "225" : "Crossing the River Thames toward the Palace of Westminster (Houses of Parliament)" + } + }, + { + "id" : "b3-1", + "name" : "River Thames at Dusk", + "pointsOfInterest" : { + "0": "Heading up the River Thames in London toward Tower Bridge", + "28" : "Traveling west up the River Thames, past City Hall on the left", + "40" : "Passing HMS Belfast in the River Thames, with The Shard on the left and the Walkie-Talkie on the right", + "75" : "Passing Southwark Cathedral on the left", + "110" : "Passing St Paul’s Cathedral on the right", + "130" : "Passing Shakespeare’s Globe in the left foreground", + "210" : "Continuing up the River Thames toward Westminster", + "310" : "Passing the London Eye on the River Thames", + "325" : "Passing Big Ben and the Houses of Parliament, with Westminster Abbey on the right" + } + }, + + { + "id" : "829E69BA-BB53-4841-A138-4DF0C2A74236", + "name" : "Los Angeles Int’l Airport", + "pointsOfInterest" : { + "0" : "Heading west over Los Angeles International Airport", + "80" : "Passing over the Theme Building", + "210" : "Passing over the Tom Bradley International Terminal" + } + }, + { + "id" : "30A2A488-E708-42E7-9A90-B749A407AE1C", + "name" : "Harbor Freeway (Interstate 110)", + "pointsOfInterest" : { + "0" :"Following the Harbor Freeway (Interstate 110) north through South Los Angeles", + "50" : "Crossing the Century Freeway (Interstate 105)", + "65" : "Passing over the Harbor Freeway Station on the Metro Green Line, which runs along the Century Freeway", + "170" : "Crossing Imperial Highway" + } + }, + { + "id" : "B730433D-1B3B-4B99-9500-A286BF7A9940", + "name" : "Santa Monica Beach" + }, + + { + "id" : "89B1643B-06DD-4DEC-B1B0-774493B0F7B7", + "name" : "Griffith Observatory" + }, + { + "id" : "EC67726A-8212-4C5E-83CF-8412932740D2", + "name" : "Hollywood Hills", + "pointsOfInterest" : { + "0" : "Traveling north along Beachwood Drive through the Hollywood Hills in Los Angeles", + "60" : "Continuing north through Beachwood Canyon", + "250" : "Passing the Mount Lee Communications Center, heading toward Burbank, California" + } + }, + { + "id" : "A284F0BF-E690-4C13-92E2-4672D93E8DE5", + "name" : "Downtown", + "pointsOfInterest" : { + "0" : "Approaching Downtown Los Angeles, with the MTA Building (in yellow) and Union Station (in pink) in the left mid-ground", + "60": "Passing the Cathedral of Our Lady of the Angels (in the center)", + "80": "Passing the Los Angeles County Music Center (in the center)", + "95": "Crossing the Hollywood Freeway, traveling south through Downtown along the Harbor Freeway", + "140": "Passing Disney Hall (in the center)", + "155": "Passing City Hall (in pale green, in the left mid-ground)", + "215": "Passing the Library Tower (in the center)", + "235": "Passing the Bonaventure Hotel (in the center)", + "265": "The Los Angeles Central Library, brightly lit, appears from behind the Bonaventure Hotel", + "310": "Passing the Wilshire Grand Center (under construction)", + "400": "Passing L.A. Live", + "435": "Passing the Ritz-Carlton Hotel and Staples Center", + "465": "Passing the Los Angeles Convention Center", + "520": "Approaching the Santa Monica Freeway" + } + }, + + { + "id" : "b7-2", + "name" : "Central Park" + }, + { + "id" : "b1-3", + "name" : "Lower Manhattan" + }, + { + "id" : "b3-2", + "name" : "Upper East Side" + }, + + { + "id" : "b2-3", + "name" : "Seventh avenue" + }, + { + "id" : "b4-2", + "name" : "Lower Manhattan" + }, + + { + "id" : "b8-2", + "name" : "Marin Headlands" + }, + { + "id" : "b10-3", + "name" : "Marin to Golden Gate", + "pointsOfInterest" : { + "0" : "Moving over the Marin Headlands along Highway 101 toward the Golden Gate Bridge", + "20" : "Horseshoe Cove appearing on the left", + "130" : "Passing over Battery Spencer", + "192" : "Crossing the Golden Gate Bridge toward San Francisco" + } + }, + { + "id" : "b9-3", + "name" : "Bay and Golden Gate" + }, + { + "id" : "b8-3", + "name" : "Alamo Square", + "pointsOfInterest" : { + "0" : "Painted Ladies across from Alamo Square in San Francisco", + "35" : "St Mary’s Cathedral in the left mid-ground, with downtown SF in the background", + "70" : "The dome of City Hall appearing in the right mid-ground", + "210" : "The dome of City Hall clear in the right mid-ground" + } + }, + { + "id" : "b3-3", + "name" : "Embarcadero, Market Street", + "pointsOfInterest" : { + "0" : "Heading southwest over San Francisco Bay to the San Francisco Ferry Building", + "110" : "Approaching the Port of San Francisco Ferry Building and the Embarcadero", + "160" : "Heading southwest along Market Street in downtown San Francisco" + } + }, + { + "id" : "b4-3", + "name" : "Presidio to Golden Gate", + "pointsOfInterest" : { + "0" : "Moving along the Presidio of San Francisco toward the Golden Gate Bridge", + "190" : "Crossing the Golden Gate Bridge toward the Marin Headlands" + } + }, + + { + "id" : "b6-4", + "name" : "Downtown and Coit Tower", + "pointsOfInterest" : { + "0" : "Downtown San Francisco with the Transamerica Pyramid in the center", + "100" : "Downtown San Francisco with Coit Tower coming into view" + } + }, + { + "id" : "b7-3", + "name" : "Fisherman’s Wharf", + "pointsOfInterest" : { + "0" : "Heading southeast over Fisherman’s Wharf in San Francisco", + "30" : "Passing the Liberty ship SS Jeremiah O’Brien", + "50" : "Passing over the submarine USS Pampanito", + "80" : "Passing over North Beach in San Francisco", + "120" : "Approaching Coit Tower on Telegraph Hill, with the Bay Bridge and downtown SF in the background", + "150" : "Passing Coit Tower on Telegraph Hill, with Sts Peter and Paul Church in the right mid-ground" + } + }, + { + "id" : "b5-3", + "name" : "Embarcadero, Market Street", + "pointsOfInterest" : { + "0" : "Heading southwest over San Francisco Bay to the San Francisco Ferry Building", + "150" : "Approaching the Port of San Francisco Ferry Building and the Embarcadero", + "210" : "Heading southwest along Market Street in San Francisco" + } + }, + { + "id" : "b1-4", + "name" : "Bay Bridge" + }, + { + "id" : "b2-4", + "name" : "Downtown and Sutro Tower", + "pointsOfInterest" : { + "0" : "Heading into San Francisco’s Financial District, with Sutro Tower in the background", + "10" : "Passing the Transamerica Pyramid", + "50" : "Former Bank of America headquarters blotting out the sun" + } + } + ] +} + + diff --git a/Aerial/Resources/Info.plist b/Aerial/Resources/Info.plist index 467f233c..3a3fec1e 100644 --- a/Aerial/Resources/Info.plist +++ b/Aerial/Resources/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.1 + 1.4.2beta3 CFBundleSignature ???? CFBundleVersion - 1.4.1 + 1.4.2beta3 LSMinimumSystemVersion ${MACOSX_DEPLOYMENT_TARGET} NSAppTransportSecurity diff --git a/Aerial/Resources/PreferencesWindow.xib b/Aerial/Resources/PreferencesWindow.xib index fe9d48b5..d6d879a7 100644 --- a/Aerial/Resources/PreferencesWindow.xib +++ b/Aerial/Resources/PreferencesWindow.xib @@ -11,7 +11,7 @@ - + @@ -19,8 +19,11 @@ - + + + + @@ -31,6 +34,9 @@ + + + @@ -44,21 +50,24 @@ + + - + + @@ -81,7 +90,7 @@ - + @@ -169,7 +178,7 @@ - + @@ -395,7 +404,7 @@ is disabled - + @@ -569,9 +578,9 @@ should appear @@ -629,7 +638,7 @@ should appear - + @@ -640,17 +649,8 @@ should appear - - - - - - - - - - + @@ -658,7 +658,7 @@ should appear - + @@ -671,9 +671,9 @@ should appear - + @@ -698,6 +698,37 @@ should appear + + + + + + + + + + + @@ -707,11 +738,11 @@ should appear - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + If you are experiencing an issue with Aerial, we may ask you to enable the Debug and Log to disk options below. + + + + + + + + + + + + + + + + + + + + @@ -1042,14 +1111,20 @@ Shift, but macOS 10.12.4 or above and a compatible Mac are required) + + + @@ -1149,12 +1224,147 @@ Shift, but macOS 10.12.4 or above and a compatible Mac are required) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Aerial/Source/Controllers/Preferences.swift b/Aerial/Source/Controllers/Preferences.swift index 32ba7265..85e27321 100644 --- a/Aerial/Source/Controllers/Preferences.swift +++ b/Aerial/Source/Controllers/Preferences.swift @@ -18,11 +18,9 @@ class Preferences { case multiMonitorMode = "multiMonitorMode" case cacheAerials = "cacheAerials" case customCacheDirectory = "cacheDirectory" - case manifestTvOS10 = "manifestTvOS10" - case manifestTvOS11 = "manifestTvOS11" - case manifestTvOS12 = "manifestTvOS12" case videoFormat = "videoFormat" case showDescriptions = "showDescriptions" + case useCommunityDescriptions = "useCommunityDescriptions" case showDescriptionsMode = "showDescriptionsMode" case neverStreamVideos = "neverStreamVideos" case neverStreamPreviews = "neverStreamPreviews" @@ -36,13 +34,22 @@ class Preferences { case fontName = "fontName" case fontSize = "fontSize" case showClock = "showClock" + case withSeconds = "withSeconds" case showMessage = "showMessage" case showMessageString = "showMessageString" case extraFontName = "extraFontName" case extraFontSize = "extraFontSize" case extraCorner = "extraCorner" + case debugMode = "debugMode" + case logToDisk = "logToDisk" + case versionCheck = "versionCheck" + case alsoVersionCheckBeta = "alsoVersionCheckBeta" } - + + enum VersionCheck : Int { + case never, daily, weekly, monthly + } + enum ExtraCorner : Int { case same, hOpposed, dOpposed } @@ -76,7 +83,7 @@ class Preferences { let module = "com.JohnCoates.Aerial" guard let userDefaults = ScreenSaverDefaults(forModuleWithName: module) else { - print("Couldn't create ScreenSaverDefaults, creating generic UserDefaults") + warnLog("Couldn't create ScreenSaverDefaults, creating generic UserDefaults") return UserDefaults() } @@ -95,6 +102,7 @@ class Preferences { defaultValues[.cacheAerials] = true defaultValues[.videoFormat] = VideoFormat.v1080pH264 defaultValues[.showDescriptions] = true + defaultValues[.useCommunityDescriptions] = true defaultValues[.showDescriptionsMode] = DescriptionMode.fade10seconds defaultValues[.neverStreamVideos] = false defaultValues[.neverStreamPreviews] = false @@ -109,12 +117,16 @@ class Preferences { defaultValues[.fontName] = "Helvetica Neue Medium" defaultValues[.fontSize] = 28 defaultValues[.showClock] = false + defaultValues[.withSeconds] = false defaultValues[.showMessage] = false defaultValues[.showMessageString] = "" - defaultValues[.extraFontName] = "Helvetica Neue Medium" + defaultValues[.extraFontName] = "Monaco" defaultValues[.extraFontSize] = 28 defaultValues[.extraCorner] = ExtraCorner.same - + defaultValues[.debugMode] = false + defaultValues[.logToDisk] = false + defaultValues[.versionCheck] = VersionCheck.weekly + defaultValues[.alsoVersionCheckBeta] = false let defaults = defaultValues.reduce([String: Any]()) { (result, pair:(key: Identifiers, value: Any)) -> [String: Any] in @@ -127,6 +139,42 @@ class Preferences { } // MARK: - Variables + + var useCommunityDescriptions: Bool { + get { + return value(forIdentifier: .useCommunityDescriptions) + } + set { + setValue(forIdentifier: .useCommunityDescriptions, value: newValue) + } + } + + var debugMode: Bool { + get { + return value(forIdentifier: .debugMode) + } + set { + setValue(forIdentifier: .debugMode, value: newValue) + } + } + + var logToDisk: Bool { + get { + return value(forIdentifier: .logToDisk) + } + set { + setValue(forIdentifier: .logToDisk, value: newValue) + } + } + + var alsoVersionCheckBeta : Bool { + get { + return value(forIdentifier: .alsoVersionCheckBeta) + } + set { + setValue(forIdentifier: .alsoVersionCheckBeta, value: newValue) + } + } var showClock: Bool { get { @@ -136,7 +184,16 @@ class Preferences { setValue(forIdentifier: .showClock, value: newValue) } } - + + var withSeconds: Bool { + get { + return value(forIdentifier: .withSeconds) + } + set { + setValue(forIdentifier: .withSeconds, value: newValue) + } + } + var showMessage: Bool { get { return value(forIdentifier: .showMessage) @@ -264,33 +321,15 @@ class Preferences { } } - var manifestTvOS10: Data? { - get { - return optionalValue(forIdentifier: .manifestTvOS10) - } - set { - setValue(forIdentifier: .manifestTvOS10, value: newValue) - } - } - - var manifestTvOS11: Data? { + var versionCheck: Int? { get { - return optionalValue(forIdentifier: .manifestTvOS11) + return optionalValue(forIdentifier: .versionCheck) } set { - setValue(forIdentifier: .manifestTvOS11, value: newValue) + setValue(forIdentifier: .versionCheck, value: newValue) } } - var manifestTvOS12: Data? { - get { - return optionalValue(forIdentifier: .manifestTvOS12) - } - set { - setValue(forIdentifier: .manifestTvOS12, value: newValue) - } - } - var descriptionCorner: Int? { get { return optionalValue(forIdentifier: .descriptionCorner) @@ -412,12 +451,6 @@ class Preferences { return userDefaults.double(forKey: key) } - fileprivate func optionalValue(forIdentifier - identifier: Identifiers) -> Data? { - let key = identifier.rawValue - return userDefaults.data(forKey: key) - } - fileprivate func setValue(forIdentifier identifier: Identifiers, value: Any?) { let key = identifier.rawValue if value == nil { diff --git a/Aerial/Source/Controllers/PreferencesWindowController.swift b/Aerial/Source/Controllers/PreferencesWindowController.swift index f396a79d..8aec4297 100644 --- a/Aerial/Source/Controllers/PreferencesWindowController.swift +++ b/Aerial/Source/Controllers/PreferencesWindowController.swift @@ -31,11 +31,7 @@ class City { init(name: String) { self.name = name } - /*func addVideo(video: AerialVideo) - { - video.arrayPosition = videos.count - videos.append(video) - }*/ + func addVideoForTimeOfDay(_ timeOfDay: String, video: AerialVideo) { if timeOfDay.lowercased() == "night" { video.arrayPosition = night.videos.count @@ -49,12 +45,14 @@ class City { @objc(PreferencesWindowController) class PreferencesWindowController: NSWindowController, NSOutlineViewDataSource, -NSOutlineViewDelegate, VideoDownloadDelegate { +NSOutlineViewDelegate { + @IBOutlet weak var prefTabView: NSTabView! @IBOutlet var outlineView: NSOutlineView! @IBOutlet var outlineViewSettings: NSButton! @IBOutlet var playerView: AVPlayerView! @IBOutlet var showDescriptionsCheckbox: NSButton! + @IBOutlet weak var useCommunityCheckbox: NSButton! @IBOutlet var localizeForTvOS12Checkbox: NSButton! @IBOutlet var projectPageLink: NSButton! @IBOutlet var secondProjectPageLink: NSButton! @@ -62,6 +60,7 @@ NSOutlineViewDelegate, VideoDownloadDelegate { @IBOutlet var cacheAerialsAsTheyPlayCheckbox: NSButton! @IBOutlet var neverStreamVideosCheckbox: NSButton! @IBOutlet var neverStreamPreviewsCheckbox: NSButton! + @IBOutlet weak var downloadNowButton: NSButton! @IBOutlet var multiMonitorModePopup: NSPopUpButton! @IBOutlet var popupVideoFormat: NSPopUpButton! @@ -69,7 +68,10 @@ NSOutlineViewDelegate, VideoDownloadDelegate { @IBOutlet var fadeInOutModePopup: NSPopUpButton! @IBOutlet weak var fadeInOutTextModePopup: NSPopUpButton! + @IBOutlet weak var downloadProgressIndicator: NSProgressIndicator! + @IBOutlet weak var downloadStopButton: NSButton! @IBOutlet var versionLabel: NSTextField! + @IBOutlet var popover: NSPopover! @IBOutlet var popoverH264Indicator: NSButton! @IBOutlet var popoverHEVCIndicator: NSButton! @@ -100,11 +102,20 @@ NSOutlineViewDelegate, VideoDownloadDelegate { @IBOutlet var currentLocaleLabel: NSTextField! @IBOutlet var showClockCheckbox: NSButton! + @IBOutlet weak var withSecondsCheckbox: NSButton! @IBOutlet var showExtraMessage: NSButton! @IBOutlet var extraMessageTextField: NSTextField! @IBOutlet var extraMessageFontLabel: NSTextField! @IBOutlet weak var extraCornerPopup: NSPopUpButton! + @IBOutlet var logPanel: NSPanel! + @IBOutlet weak var showLogBottomClick: NSButton! + @IBOutlet weak var logTableView: NSTableView! + @IBOutlet weak var debugModeCheckbox: NSButton! + @IBOutlet weak var logToDiskCheckbox: NSButton! + + @IBOutlet weak var cacheSizeTextField: NSTextField! + var player: AVPlayer = AVPlayer() var videos: [AerialVideo]? @@ -118,13 +129,19 @@ NSOutlineViewDelegate, VideoDownloadDelegate { let fontManager: NSFontManager var fontEditing = 0 // To track the font we are changing + var highestLevel : ErrorLevel? // To track the largest level of error received + // MARK: - Init required init?(coder decoder: NSCoder) { self.fontManager = NSFontManager.shared + debugLog("pwc init1") super.init(coder: decoder) } + + // We start here from SysPref and App mode override init(window: NSWindow?) { self.fontManager = NSFontManager.shared + debugLog("pwc init2") super.init(window: window) } @@ -132,17 +149,30 @@ NSOutlineViewDelegate, VideoDownloadDelegate { override func awakeFromNib() { super.awakeFromNib() - + let logger = Logger.sharedInstance + logger.addCallback {level in + self.updateLogs(level:level) + } + let videoManager = VideoManager.sharedInstance + videoManager.addCallback { done,total in + self.updateDownloads(done: done,total: total) + } + self.fontManager.target = self - if let previewPlayer = AerialView.previewPlayer { + // This used to grab the preview player and put it in our own video preview thing. + // While kinda cool, it showed a random video that wasn't selected, and with new lifecycle, it was paused + /*if let previewPlayer = AerialView.previewPlayer { self.player = previewPlayer - } - + }*/ + updateCacheSize() outlineView.floatsGroupRows = false loadJSON() // Async loading + logTableView.delegate = self + logTableView.dataSource = self + if let version = Bundle(identifier: "com.johncoates.Aerial-Test")?.infoDictionary?["CFBundleShortVersionString"] as? String { versionLabel.stringValue = version } @@ -150,7 +180,7 @@ NSOutlineViewDelegate, VideoDownloadDelegate { versionLabel.stringValue = version } - // Some icons are 10.12.2+ only + // Some better icons are 10.12.2+ only if #available(OSX 10.12.2, *) { iconTime1.image = NSImage(named: NSImage.touchBarHistoryTemplateName) iconTime2.image = NSImage(named: NSImage.touchBarComposeTemplateName) @@ -208,12 +238,26 @@ NSOutlineViewDelegate, VideoDownloadDelegate { } else { showClockCheckbox.isEnabled = false } + + // Aerial panel + if preferences.debugMode { + debugModeCheckbox.state = NSControl.StateValue.on + } + + if preferences.logToDisk { + logToDiskCheckbox.state = NSControl.StateValue.on + } // Text panel if preferences.showClock { showClockCheckbox.state = NSControl.StateValue.on + withSecondsCheckbox.isEnabled = true } - + + if preferences.withSeconds { + withSecondsCheckbox.state = NSControl.StateValue.on + } + if preferences.showMessage { showExtraMessage.state = NSControl.StateValue.on extraMessageTextField.isEnabled = true @@ -236,6 +280,10 @@ NSOutlineViewDelegate, VideoDownloadDelegate { neverStreamPreviewsCheckbox.state = NSControl.StateValue.on } + if !preferences.useCommunityDescriptions { + useCommunityCheckbox.state = NSControl.StateValue.off + } + if !preferences.cacheAerials { cacheAerialsAsTheyPlayCheckbox.state = NSControl.StateValue.off } @@ -308,9 +356,11 @@ NSOutlineViewDelegate, VideoDownloadDelegate { if let cacheDirectory = VideoCache.cacheDirectory { cacheLocation.url = URL(fileURLWithPath: cacheDirectory as String) + } else { + cacheLocation.url = nil } - cacheStatusLabel.isEditable = false + //cacheStatusLabel.isEditable = false } override func windowDidLoad() { @@ -321,6 +371,7 @@ NSOutlineViewDelegate, VideoDownloadDelegate { } @IBAction func close(_ sender: AnyObject?) { + logPanel.close() window?.sheetParent?.endSheet(window!) } @@ -358,19 +409,94 @@ NSOutlineViewDelegate, VideoDownloadDelegate { secondProjectPageLink.attributedTitle = coloredLink } - // MARK: - Preferences + + // MARK: - Video panel + + @IBAction func popupVideoFormatChange(_ sender:NSPopUpButton) { + debugLog("UI popupVideoFormat: \(sender.indexOfSelectedItem)") + preferences.videoFormat = sender.indexOfSelectedItem + preferences.synchronize() + + outlineView.reloadData() + } + + @IBAction func helpButtonClick(_ button: NSButton!) { + popover.show(relativeTo: button.preparedContentRect, of: button, preferredEdge: .maxY) + } + + @IBAction func multiMonitorModePopupChange(_ sender:NSPopUpButton) { + debugLog("UI multiMonitorMode: \(sender.indexOfSelectedItem)") + preferences.multiMonitorMode = sender.indexOfSelectedItem + preferences.synchronize() + } + + @IBAction func fadeInOutModePopupChange(_ sender:NSPopUpButton) { + debugLog("UI fadeInOutMode: \(sender.indexOfSelectedItem)") + preferences.fadeMode = sender.indexOfSelectedItem + preferences.synchronize() + } + + func updateDownloads(done: Int, total: Int) { + print("VMQueue: done : \(done) \(total)") + if (total == 0) { + downloadProgressIndicator.isHidden = true + downloadStopButton.isHidden = true + downloadNowButton.isEnabled = true + } else { + downloadNowButton.isEnabled = false + downloadProgressIndicator.isHidden = false + downloadStopButton.isHidden = false + downloadProgressIndicator.doubleValue = Double(done) + downloadProgressIndicator.maxValue = Double(total) + } + } + @IBAction func cancelDownloadsClick(_ sender: Any) { + debugLog("UI cancelDownloadsClick") + let videoManager = VideoManager.sharedInstance + videoManager.cancelAll() + } + + // MARK: - Text panel + + @IBAction func showDescriptionsClick(button: NSButton?) { + let state = showDescriptionsCheckbox.state + let onState = (state == NSControl.StateValue.on) + preferences.showDescriptions = onState + debugLog("UI showDescriptions: \(onState)") + } + + @IBAction func useCommunityClick(_ button: NSButton) { + let state = useCommunityCheckbox.state + let onState = (state == NSControl.StateValue.on) + preferences.useCommunityDescriptions = onState + debugLog("UI useCommunity: \(onState)") + } + + @IBAction func localizeForTvOS12Click(button: NSButton?) { + let state = localizeForTvOS12Checkbox.state + let onState = (state == NSControl.StateValue.on) + preferences.localizeDescriptions = onState + debugLog("UI localizeDescriptions: \(onState)") + } + + @IBAction func descriptionModePopupChange(_ sender:NSPopUpButton) { + debugLog("UI descriptionMode: \(sender.indexOfSelectedItem)") + preferences.showDescriptionsMode = sender.indexOfSelectedItem + preferences.synchronize() + } + @IBAction func fontPickerClick(_ sender:NSButton?) { // Make a panel let fp = self.fontManager.fontPanel(true) - + // Set current font if let font = NSFont(name: preferences.fontName!,size: CGFloat(preferences.fontSize!)) { fp?.setPanelFont(font, isMultiple: false) - + } else { fp?.setPanelFont(NSFont(name: "Helvetica Neue Medium", size: 28)!, isMultiple: false) } - + // push the panel but mark which one we are editing fontEditing = 0 fp?.makeKeyAndOrderFront(sender) @@ -383,7 +509,7 @@ NSOutlineViewDelegate, VideoDownloadDelegate { // Update our label currentFontLabel.stringValue = preferences.fontName! + ", \(preferences.fontSize!) pt" } - + @IBAction func extraFontPickerClick(_ sender:NSButton?) { // Make a panel let fp = self.fontManager.fontPanel(true) @@ -400,7 +526,7 @@ NSOutlineViewDelegate, VideoDownloadDelegate { fontEditing = 1 fp?.makeKeyAndOrderFront(sender) } - + @IBAction func extraFontResetClick(_ sender:NSButton?) { preferences.extraFontName = "Helvetica Neue Medium" preferences.extraFontSize = 28 @@ -408,25 +534,13 @@ NSOutlineViewDelegate, VideoDownloadDelegate { // Update our label extraMessageFontLabel.stringValue = preferences.extraFontName! + ", \(preferences.extraFontSize!) pt" } - + @IBAction func extraTextFieldChange(_ sender: NSTextField) { - print("changed message \(sender.stringValue)") + debugLog("UI extraTextField \(sender.stringValue)") preferences.showMessageString = sender.stringValue } - @IBAction func timeModeChange(_ sender:NSButton?) { - if sender == timeDisabledRadio { - preferences.timeMode = Preferences.TimeMode.disabled.rawValue - } else if sender == timeNightShiftRadio { - preferences.timeMode = Preferences.TimeMode.nightShift.rawValue - } else if sender == timeManualRadio { - preferences.timeMode = Preferences.TimeMode.manual.rawValue - } else if sender == timeLightDarkModeRadio { - preferences.timeMode = Preferences.TimeMode.lightDarkMode.rawValue - } - } - @IBAction func descriptionCornerChange(_ sender:NSButton?) { if sender == cornerTopLeft { preferences.descriptionCorner = Preferences.DescriptionCorner.topLeft.rawValue @@ -440,67 +554,86 @@ NSOutlineViewDelegate, VideoDownloadDelegate { preferences.descriptionCorner = Preferences.DescriptionCorner.random.rawValue } } - - @IBAction func sunriseChange(_ sender:NSDatePicker?) { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "HH:mm" - let sunriseString = dateFormatter.string(from: (sender?.dateValue)!) - preferences.manualSunrise = sunriseString - } - @IBAction func sunsetChange(_ sender:NSDatePicker?) { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "HH:mm" - let sunsetString = dateFormatter.string(from: (sender?.dateValue)!) - preferences.manualSunset = sunsetString - } - - @IBAction func helpButtonClick(_ button: NSButton!) { - popover.show(relativeTo: button.preparedContentRect, of: button, preferredEdge: .maxY) - } - - @IBAction func showInFinder(_ button: NSButton!) { - NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: VideoCache.cacheDirectory!) - - } - @IBAction func showClockClick(_ sender: NSButton) { - debugLog("show clock click: \(convertFromNSControlStateValue(sender.state))") - let onState = (sender.state == NSControl.StateValue.on) preferences.showClock = onState - + withSecondsCheckbox.isEnabled = onState + debugLog("UI showClock: \(onState)") + } + + @IBAction func withSecondsClick(_ sender: NSButton) { + let onState = (sender.state == NSControl.StateValue.on) + preferences.withSeconds = onState + debugLog("UI withSeconds: \(onState)") } @IBAction func showExtraMessageClick(_ sender: NSButton) { - debugLog("show extra message: \(convertFromNSControlStateValue(sender.state))") - let onState = (sender.state == NSControl.StateValue.on) // We also need to enable/disable our message field extraMessageTextField.isEnabled = onState preferences.showMessage = onState + debugLog("UI showExtraMessage: \(onState)") + } - @IBAction func neverStreamVideosClick(_ button: NSButton!) { - debugLog("never stream videos: \(convertFromNSControlStateValue(button.state))") + @IBAction func fadeInOutTextModePopupChange(_ sender: NSPopUpButton) { + debugLog("UI fadeInOutTextMode: \(sender.indexOfSelectedItem)") + preferences.fadeModeText = sender.indexOfSelectedItem + preferences.synchronize() + } + + @IBAction func extraCornerPopupChange(_ sender: NSPopUpButton) { + debugLog("UI extraCorner: \(sender.indexOfSelectedItem)") + preferences.extraCorner = sender.indexOfSelectedItem + preferences.synchronize() + } + + // MARK: - Cache panel + + func updateCacheSize() { + // get your directory url + let documentsDirectoryURL = URL(fileURLWithPath: VideoCache.cacheDirectory!) + // FileManager.default.urls(for: VideoCache.cacheDirectory, in: .userDomainMask).first! + + // check if the url is a directory + if (try? documentsDirectoryURL.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true { + var folderSize = 0 + (FileManager.default.enumerator(at: documentsDirectoryURL, includingPropertiesForKeys: nil)?.allObjects as? [URL])?.lazy.forEach { + folderSize += (try? $0.resourceValues(forKeys: [.totalFileAllocatedSizeKey]))?.totalFileAllocatedSize ?? 0 + } + let byteCountFormatter = ByteCountFormatter() + byteCountFormatter.allowedUnits = .useGB + byteCountFormatter.countStyle = .file + let sizeToDisplay = byteCountFormatter.string(for: folderSize) ?? "" + debugLog("Cache size : \(sizeToDisplay)") + cacheSizeTextField.stringValue = "Cache all videos (current cache size \(sizeToDisplay))" + } + } + + @IBAction func cacheAerialsAsTheyPlayClick(_ button: NSButton!) { + let onState = (button.state == NSControl.StateValue.on) + preferences.cacheAerials = onState + debugLog("UI cacheAerialAsTheyPlay: \(onState)") + } + + @IBAction func neverStreamVideosClick(_ button: NSButton!) { let onState = (button.state == NSControl.StateValue.on) preferences.neverStreamVideos = onState + debugLog("UI neverStreamVideos: \(onState)") } @IBAction func neverStreamPreviewsClick(_ button: NSButton!) { - debugLog("never stream previews: \(convertFromNSControlStateValue(button.state))") - let onState = (button.state == NSControl.StateValue.on) preferences.neverStreamPreviews = onState - } - @IBAction func cacheAerialsAsTheyPlayClick(_ button: NSButton!) { - debugLog("cache aerials as they play: \(convertFromNSControlStateValue(button.state))") - - let onState = (button.state == NSControl.StateValue.on) - preferences.cacheAerials = onState + debugLog("UI neverStreamPreviews: \(onState)") } + @IBAction func showInFinder(_ button: NSButton!) { + NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: VideoCache.cacheDirectory!) + } + @IBAction func userSetCacheLocation(_ button: NSButton?) { let openPanel = NSOpenPanel() @@ -515,7 +648,7 @@ NSOutlineViewDelegate, VideoDownloadDelegate { openPanel.begin { result in guard result.rawValue == NSFileHandlingPanelOKButton, openPanel.urls.count > 0 else { - return + return } let cacheDirectory = openPanel.urls[0] @@ -523,76 +656,132 @@ NSOutlineViewDelegate, VideoDownloadDelegate { self.cacheLocation.url = cacheDirectory } } + @IBAction func resetCacheLocation(_ button: NSButton?) { preferences.customCacheDirectory = nil if let cacheDirectory = VideoCache.cacheDirectory { cacheLocation.url = URL(fileURLWithPath: cacheDirectory as String) } } - - @IBAction func popupVideoFormatChange(_ sender:NSPopUpButton) { - debugLog("index change : \(sender.indexOfSelectedItem)") - preferences.videoFormat = sender.indexOfSelectedItem - preferences.synchronize() - - outlineView.reloadData() + + @IBAction func downloadNowButton(_ sender: Any) { + downloadNowButton.isEnabled = false + prefTabView.selectTabViewItem(at: 0) + downloadAllVideos() } - - @IBAction func descriptionModePopupChange(_ sender:NSPopUpButton) { - debugLog("dindex change : \(sender.indexOfSelectedItem)") - - preferences.showDescriptionsMode = sender.indexOfSelectedItem - preferences.synchronize() + + // MARK: - Time panel + + @IBAction func timeModeChange(_ sender:NSButton?) { + if sender == timeDisabledRadio { + preferences.timeMode = Preferences.TimeMode.disabled.rawValue + } else if sender == timeNightShiftRadio { + preferences.timeMode = Preferences.TimeMode.nightShift.rawValue + } else if sender == timeManualRadio { + preferences.timeMode = Preferences.TimeMode.manual.rawValue + } else if sender == timeLightDarkModeRadio { + preferences.timeMode = Preferences.TimeMode.lightDarkMode.rawValue + } } - @IBAction func multiMonitorModePopupChange(_ sender:NSPopUpButton) { - debugLog("mm index change : \(sender.indexOfSelectedItem)") - - preferences.multiMonitorMode = sender.indexOfSelectedItem - preferences.synchronize() + @IBAction func sunriseChange(_ sender:NSDatePicker?) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm" + let sunriseString = dateFormatter.string(from: (sender?.dateValue)!) + preferences.manualSunrise = sunriseString + } + + @IBAction func sunsetChange(_ sender:NSDatePicker?) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm" + let sunsetString = dateFormatter.string(from: (sender?.dateValue)!) + preferences.manualSunset = sunsetString + } + + // MARK: - Aerial panel + + @IBAction func logButtonClick(_ sender: NSButton) { + logTableView.reloadData() + if logPanel.isVisible { + logPanel.close() + } else { + logPanel.makeKeyAndOrderFront(sender) + } + } + + @IBAction func logCopyToClipboardClick(_ sender: NSButton) { + if (errorMessages.count > 0) { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .none + dateFormatter.timeStyle = .medium + + var clipboard = "" + for log in errorMessages { + clipboard += dateFormatter.string(from:log.date) + " : " + log.message + "\n" + } + + let pasteBoard = NSPasteboard.general + pasteBoard.clearContents() + pasteBoard.setString(clipboard, forType: NSPasteboard.PasteboardType.string) + } + } + + @IBAction func logRefreshClick(_ sender: NSButton) { + logTableView.reloadData() } - @IBAction func fadeInOutModePopupChange(_ sender:NSPopUpButton) { - debugLog("fm index change : \(sender.indexOfSelectedItem)") - - preferences.fadeMode = sender.indexOfSelectedItem - preferences.synchronize() + @IBAction func debugModeClick(_ button: NSButton) { + let onState = (button.state == NSControl.StateValue.on) + preferences.debugMode = onState + debugLog("UI debugMode: \(onState)") } - @IBAction func fadeInOutTextModePopupChange(_ sender: NSPopUpButton) { - debugLog("fmt index change : \(sender.indexOfSelectedItem)") - - preferences.fadeModeText = sender.indexOfSelectedItem - preferences.synchronize() + @IBAction func logToDiskClick(_ button: NSButton) { + let onState = (button.state == NSControl.StateValue.on) + preferences.logToDisk = onState + debugLog("UI logToDisk: \(onState)") } - @IBAction func extraCornerPopupChange(_ sender: NSPopUpButton) { - debugLog("ec index change : \(sender.indexOfSelectedItem)") - - preferences.extraCorner = sender.indexOfSelectedItem - preferences.synchronize() + @IBAction func showLogInFinder(_ button: NSButton!) { + let logfile = VideoCache.cacheDirectory!.appending("/AerialLog.txt") + // If we don't have a log, just show the folder + if FileManager.default.fileExists(atPath: logfile) == false { + NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: VideoCache.cacheDirectory!) + } + else { + NSWorkspace.shared.selectFile(logfile, inFileViewerRootedAtPath: VideoCache.cacheDirectory!) + } } - @IBAction func showDescriptionsClick(button: NSButton?) { - let state = showDescriptionsCheckbox.state - let onState = (state == NSControl.StateValue.on) - - preferences.showDescriptions = onState + func updateLogs(level:ErrorLevel) + { + logTableView.reloadData() + if (highestLevel == nil) { + highestLevel = level + } else if (level.rawValue > highestLevel!.rawValue) { + highestLevel = level + } - debugLog("set showDescriptions to \(onState)") - } - - @IBAction func localizeForTvOS12Click(button: NSButton?) { - let state = localizeForTvOS12Checkbox.state - let onState = (state == NSControl.StateValue.on) + switch highestLevel! { + case ErrorLevel.debug: + showLogBottomClick.title = "Show Debug" + showLogBottomClick.image = NSImage.init(named: NSImage.actionTemplateName) + case ErrorLevel.info: + showLogBottomClick.title = "Show Info" + showLogBottomClick.image = NSImage.init(named: NSImage.infoName) + case ErrorLevel.warning: + showLogBottomClick.title = "Show Warning" + showLogBottomClick.image = NSImage.init(named: NSImage.cautionName) + default: + showLogBottomClick.title = "Show Error" + showLogBottomClick.image = NSImage.init(named: NSImage.stopProgressFreestandingTemplateName) + } - preferences.localizeDescriptions = onState - debugLog("set localizeDescriptions to \(onState)") + showLogBottomClick.isHidden = false } - - // MARK: Menu + // MARK: - Menu @IBAction func outlineViewSettingsClick(_ button: NSButton) { let menu = NSMenu() @@ -692,6 +881,10 @@ NSOutlineViewDelegate, VideoDownloadDelegate { } @objc func outlineViewDownloadAll(button: NSButton) { + downloadAllVideos() + } + + func downloadAllVideos() { guard let videos = videos else { return } @@ -886,14 +1079,14 @@ NSOutlineViewDelegate, VideoDownloadDelegate { switch item { case is City: let city = item as! City - let view = outlineView.makeView(withIdentifier: convertToNSUserInterfaceItemIdentifier("HeaderCell"), + let view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "HeaderCell"), owner: nil) as! NSTableCellView // if owner = self, awakeFromNib will be called for each created cell ! view.textField?.stringValue = city.name return view case is TimeOfDay: let timeOfDay = item as! TimeOfDay - let view = outlineView.makeView(withIdentifier: convertToNSUserInterfaceItemIdentifier("DataCell"), + let view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "DataCell"), owner: nil) as! NSTableCellView // if owner = self, awakeFromNib will be called for each created cell ! view.textField?.stringValue = timeOfDay.title.capitalized @@ -916,13 +1109,13 @@ NSOutlineViewDelegate, VideoDownloadDelegate { // TODO, change the icons for dark mode } else { - NSLog("\(#file) failed to find time of day icon") + errorLog("\(#file) failed to find time of day icon") } return view case is AerialVideo: let video = item as! AerialVideo - let view = outlineView.makeView(withIdentifier: convertToNSUserInterfaceItemIdentifier("CheckCell"), + let view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "CheckCell"), owner: nil) as! CheckCellView // if owner = self, awakeFromNib will be called for each created cell ! // Mark the new view for this video for subsequent callbacks let videoManager = VideoManager.sharedInstance @@ -943,7 +1136,7 @@ NSOutlineViewDelegate, VideoDownloadDelegate { guard let numberString = numberFormatter.string(from: number as NSNumber) else { - print("failed to create number with formatter") + errorLog("outlineView: failed to create number with formatter") return nil } @@ -978,7 +1171,7 @@ NSOutlineViewDelegate, VideoDownloadDelegate { playerView.player = player let video = item as! AerialVideo - debugLog("playing this preview \(video)") + debugLog("Playing this preview \(video)") // Workaround for cached videos generating online traffic if video.isAvailableOffline { previewDisabledTextfield.isHidden = true @@ -1022,10 +1215,7 @@ NSOutlineViewDelegate, VideoDownloadDelegate { } // MARK: - Caching - - @IBOutlet var totalProgress: NSProgressIndicator! - @IBOutlet var currentProgress: NSProgressIndicator! - @IBOutlet var cacheStatusLabel: NSTextField! + /* var currentVideoDownload: VideoDownload? var manifestVideos: [AerialVideo]? @@ -1037,7 +1227,6 @@ NSOutlineViewDelegate, VideoDownloadDelegate { DispatchQueue.main.async(execute: { () -> Void in self.manifestVideos = manifestVideos self.cacheNextVideo() - }) } } @@ -1052,11 +1241,11 @@ NSOutlineViewDelegate, VideoDownloadDelegate { return video.isAvailableOffline == false } - NSLog("uncached: \(uncached)") + debugLog("uncached: \(uncached)") totalProgress.maxValue = Double(manifestVideos.count) totalProgress.doubleValue = Double(manifestVideos.count) - Double(uncached.count) - NSLog("total process max value: \(totalProgress.maxValue), current value: \(totalProgress.doubleValue)") + debugLog("total process max value: \(totalProgress.maxValue), current value: \(totalProgress.doubleValue)") if uncached.count == 0 { cacheStatusLabel.stringValue = "All videos have been cached" @@ -1086,23 +1275,22 @@ NSOutlineViewDelegate, VideoDownloadDelegate { preferences.synchronize() outlineView.reloadData() - NSLog("video download finished with success: \(success))") + debugLog("video download finished with success: \(success))") } func videoDownload(_ videoDownload: VideoDownload, receivedBytes: Int, progress: Float) { currentProgress.doubleValue = Double(progress) -// NSLog("received bytes: \(receivedBytes), progress: \(progress)") - } - + }*/ } +// MARK: - Font Panel Delegates + extension PreferencesWindowController : NSFontChanging { func validModesForFontPanel(_ fontPanel: NSFontPanel) -> NSFontPanel.ModeMask { return [.size, .collection, .face] } func changeFont(_ sender: NSFontManager?) { - print("change font") // Set current font var oldFont = NSFont(name: "Helvetica Neue Medium", size: 28) @@ -1116,7 +1304,6 @@ extension PreferencesWindowController : NSFontChanging { } } - let newFont = sender?.convert(oldFont!) if (fontEditing == 0) { @@ -1137,14 +1324,56 @@ extension PreferencesWindowController : NSFontChanging { } +// MARK: - Log TableView Delegates -// Helper function inserted by Swift 4.2 migrator. -fileprivate func convertFromNSControlStateValue(_ input: NSControl.StateValue) -> Int { - return input.rawValue +extension PreferencesWindowController: NSTableViewDataSource { + func numberOfRows(in tableView: NSTableView) -> Int { + return errorMessages.count + } } -// Helper function inserted by Swift 4.2 migrator. -fileprivate func convertToNSUserInterfaceItemIdentifier(_ input: String) -> NSUserInterfaceItemIdentifier { - return NSUserInterfaceItemIdentifier(rawValue: input) +extension PreferencesWindowController : NSTableViewDelegate { + fileprivate enum CellIdentifiers { + static let DateCell = "DateCellID" + static let MessageCell = "MessageCellID" + } + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + var image: NSImage? + var text: String = "" + var cellIdentifier: String = "" + + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .none + dateFormatter.timeStyle = .medium + + let item = errorMessages[row] + + if tableColumn == tableView.tableColumns[0] { + text = dateFormatter.string(from: item.date) + cellIdentifier = CellIdentifiers.DateCell + } else if tableColumn == tableView.tableColumns[1] { + switch item.level { + case ErrorLevel.info: + image = NSImage.init(named: NSImage.infoName) + case ErrorLevel.warning: + image = NSImage.init(named: NSImage.cautionName) + case ErrorLevel.error: + image = NSImage.init(named: NSImage.stopProgressFreestandingTemplateName) + default: + image = NSImage.init(named: NSImage.actionTemplateName) + } + //image = + text = item.message + cellIdentifier = CellIdentifiers.MessageCell + } + + if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: cellIdentifier), owner: nil) as? NSTableCellView { + cell.textField?.stringValue = text + cell.imageView?.image = image ?? nil + return cell + } + + return nil + } } - diff --git a/Aerial/Source/Models/AerialVideo.swift b/Aerial/Source/Models/AerialVideo.swift index 18ec0403..79c00b56 100644 --- a/Aerial/Source/Models/AerialVideo.swift +++ b/Aerial/Source/Models/AerialVideo.swift @@ -62,6 +62,7 @@ class AerialVideo: CustomStringConvertible, Equatable { let url4KHEVC: String var sources: [Manifests] let poi: [String: String] + let communityPoi: [String: String] var duration: Double var arrayPosition = 1 @@ -85,11 +86,9 @@ class AerialVideo: CustomStringConvertible, Equatable { return URL(string: self.url4KHEVC)! } else if (url1080pHEVC != "") { - //debugLog("4K NOT AVAILABLE, retunring 1080P HEVC as closest available") return URL(string: self.url1080pHEVC)! } else { - //debugLog("4K NOT AVAILABLE, retunring 1080P H264 as closest available") return URL(string: self.url1080pH264)! } } @@ -99,11 +98,9 @@ class AerialVideo: CustomStringConvertible, Equatable { return URL(string: self.url1080pHEVC)! } else if (url1080pH264 != "") { - //debugLog("1080pHEVC NOT AVAILABLE, retunring 1080P H264 as closest available") return URL(string: self.url1080pH264)! } else { - //debugLog("1080pHEVC NOT AVAILABLE, retunring 4K HEVC as closest available") return URL(string: self.url4KHEVC)! } } @@ -113,32 +110,17 @@ class AerialVideo: CustomStringConvertible, Equatable { return URL(string: self.url1080pH264)! } else if (url1080pHEVC != "") { - //debugLog("1080pH264 NOT AVAILABLE, retunring 1080P HEVC as closest available") - return URL(string: self.url1080pHEVC)! + return URL(string: self.url1080pHEVC)! // With the latest versions, we should always have a H.264 fallback so this is just for future proofing } else { - //debugLog("1080pHEVC NOT AVAILABLE, retunring 4K HEVC as closest available") return URL(string: self.url4KHEVC)! } } - - - /*switch preferences.videoFormat { - case Preferences.VideoFormat.v1080pH264.rawValue: - return URL(string: self.url1080pH264)! - case Preferences.VideoFormat.v1080pHEVC.rawValue: - return URL(string: self.url1080pHEVC)! - case Preferences.VideoFormat.v4KHEVC.rawValue: - return URL(string: self.url4KHEVC)! - default: - return URL(string: url1080pH264)! - }*/ - } } init(id: String, name: String, secondaryName: String, type: String, - timeOfDay: String, url1080pH264: String, url1080pHEVC: String, url4KHEVC: String, manifest: Manifests, poi: [String: String]) { + timeOfDay: String, url1080pH264: String, url1080pHEVC: String, url4KHEVC: String, manifest: Manifests, poi: [String: String], communityPoi: [String:String]) { self.id = id // We override names for known space videos @@ -151,7 +133,7 @@ class AerialVideo: CustomStringConvertible, Equatable { } } else { self.name = name - self.secondaryName = secondaryName // We may have a secondary name from our merges + self.secondaryName = secondaryName // We may have a secondary name from our merges too now ! } self.type = type @@ -169,8 +151,9 @@ class AerialVideo: CustomStringConvertible, Equatable { self.url4KHEVC = url4KHEVC self.sources = [manifest] self.poi = poi - + self.communityPoi = communityPoi self.duration = 0 + updateDuration() } @@ -212,7 +195,7 @@ class AerialVideo: CustomStringConvertible, Equatable { } else { - print("Could not determine duration, video is not cached") + debugLog("Could not determine duration, video is not cached in any format") self.duration = 0 } } diff --git a/Aerial/Source/Models/Cache/AssetLoaderDelegate.swift b/Aerial/Source/Models/Cache/AssetLoaderDelegate.swift index 8c4615ad..6f43651e 100644 --- a/Aerial/Source/Models/Cache/AssetLoaderDelegate.swift +++ b/Aerial/Source/Models/Cache/AssetLoaderDelegate.swift @@ -13,12 +13,11 @@ import AVFoundation /// then returns the cached asset. func CachedOrCachingAsset(_ URL: Foundation.URL) -> AVURLAsset { let assetLoader = AssetLoaderDelegate(URL: URL) - let asset = AVURLAsset(url: assetLoader.URLWithCustomScheme) let queue = DispatchQueue.main asset.resourceLoader.setDelegate(assetLoader, queue: queue) objc_setAssociatedObject(asset, "assetLoader", assetLoader, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) - + //debugLog("\(asset)") return asset } @@ -36,7 +35,6 @@ class AssetLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, VideoLoaderD init(URL: Foundation.URL) { self.URL = URL -// self.URL = NSURL(string:"http://localhost/test.mov")! videoCache = VideoCache(URL: URL) } @@ -83,20 +81,20 @@ class AssetLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, VideoLoaderD func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool { - + /* // check if cache can fulfill this without a request if videoCache.canFulfillLoadingRequest(loadingRequest) { if videoCache.fulfillLoadingRequest(loadingRequest) { + debugLog("fullfilling loading request") return true } - } + }*/ // assign request to VideoLoader - + //debugLog("request to loader \(loadingRequest)") let videoLoader = VideoLoader(url: URL, loadingRequest: loadingRequest, delegate: self) videoLoaders.append(videoLoader) return true } - } diff --git a/Aerial/Source/Models/Cache/PoiStringProvider.swift b/Aerial/Source/Models/Cache/PoiStringProvider.swift index 90a8f6ea..c65f9d8b 100644 --- a/Aerial/Source/Models/Cache/PoiStringProvider.swift +++ b/Aerial/Source/Models/Cache/PoiStringProvider.swift @@ -8,6 +8,18 @@ import Foundation +class CommunityStrings { + let id : String + let name : String + let poi : [String:String] + + init(id: String, name: String, poi: [String:String]) { + self.id = id + self.name = name + self.poi = poi + } +} + class PoiStringProvider { static let sharedInstance = PoiStringProvider() var loadedDescriptions = false @@ -15,10 +27,17 @@ class PoiStringProvider { var stringBundle: Bundle? var stringDict: NSDictionary? + + var communityStrings = [CommunityStrings]() + + // MARK: - Lifecycle init() { + debugLog("Poi Strings Provider initialized") loadBundle() + loadCommunity() } - + + // MARK: - Bundle management private func loadBundle() { // Idle string bundle let preferences = Preferences.sharedInstance @@ -30,7 +49,6 @@ class PoiStringProvider { else { bundlePath.append(contentsOf: "/TVIdleScreenStrings.bundle/en.lproj/") } - if let sb = Bundle.init(path: bundlePath) { let dictPath = VideoCache.cacheDirectory!.appending("/TVIdleScreenStrings.bundle/en.lproj/Localizable.nocache.strings") @@ -43,6 +61,8 @@ class PoiStringProvider { self.stringBundle = sb self.loadedDescriptions = true self.loadedDescriptionsWasLocalized = preferences.localizeDescriptions + } else { + errorLog("TVIdleScreenStrings.bundle is missing, please remove entries.json in Cache folder to fix the issue") } } @@ -59,11 +79,24 @@ class PoiStringProvider { } // Return the Localized (or english) string for a key from the Strings Bundle - func getString(key:String) -> String { + func getString(key:String, video:AerialVideo) -> String { if !ensureLoadedBundle() { return "" } - return stringBundle!.localizedString(forKey: key, value: "", table: "Localizable.nocache") + let preferences = Preferences.sharedInstance + let locale: NSLocale = NSLocale(localeIdentifier: Locale.preferredLanguages[0]) + + if #available(OSX 10.12, *) { + if (preferences.localizeDescriptions && locale.languageCode != "en") { + return stringBundle!.localizedString(forKey: key, value: "", table: "Localizable.nocache") + } + } + + if preferences.useCommunityDescriptions && video.communityPoi.count > 0 { + return key // We directly store the string in the key + } else { + return stringBundle!.localizedString(forKey: key, value: "", table: "Localizable.nocache") + } } // Return all POIs for an id @@ -83,5 +116,89 @@ class PoiStringProvider { return found } + + // + func getPoiKeys(video: AerialVideo) -> [String:String] { + let preferences = Preferences.sharedInstance + let locale: NSLocale = NSLocale(localeIdentifier: Locale.preferredLanguages[0]) + + if #available(OSX 10.12, *) { + if (preferences.localizeDescriptions && locale.languageCode != "en") { + return video.poi + } + } + + if preferences.useCommunityDescriptions && video.communityPoi.count > 0 { + return video.communityPoi + } else { + return video.poi + } + } + + + // MARK: - Community data + + // Load the community strings + private func loadCommunity() + { + let preferences = Preferences.sharedInstance + + var bundlePath: String + if (preferences.localizeDescriptions) { + bundlePath = Bundle(for: PoiStringProvider.self).path(forResource: "en", ofType: "json")! + //bundlePath = Bundle.main.path(forResource: "en", ofType: "json")! + } + else { + // TODO + bundlePath = Bundle(for: PoiStringProvider.self).path(forResource: "en", ofType: "json")! + //bundlePath = Bundle.main.path(forResource: "en", ofType: "json")! + } + debugLog("path : \(bundlePath)") + do { + let data = try Data(contentsOf: URL(fileURLWithPath: bundlePath), options: .mappedIfSafe) + let batches = try JSONSerialization.jsonObject(with: data, options: .allowFragments) + + guard let batch = batches as? NSDictionary else { + errorLog("Community : Encountered unexpected content type for batch, please report !") + return + } + + let assets = batch["assets"] as! Array + + for item in assets { + let id = item["id"] as! String + let name = item["name"] as! String + let poi = item["pointsOfInterest"] as? [String: String] + + communityStrings.append(CommunityStrings(id: id, name: name, poi: poi ?? [:])) + } + } catch { + // handle error + errorLog("Community JSON ERROR") + } + debugLog("Community JSON : \(communityStrings.count)") + } + + func getCommunityName(id: String) -> String? { + for obj in communityStrings { + if obj.id == id { + return obj.name + } + } + + return nil + } + + func getCommunityPoi(id:String) -> [String:String] + { + for obj in communityStrings { + if obj.id == id { + return obj.poi + } + } + + return [:] + } + } diff --git a/Aerial/Source/Models/Cache/VideoCache.swift b/Aerial/Source/Models/Cache/VideoCache.swift index 474aded1..9e6f0d40 100644 --- a/Aerial/Source/Models/Cache/VideoCache.swift +++ b/Aerial/Source/Models/Cache/VideoCache.swift @@ -30,7 +30,7 @@ class VideoCache { .userDomainMask, true) if cachePaths.count == 0 { - NSLog("Aerial Error: Couldn't find cache paths!") + errorLog("Couldn't find cache paths!") return nil } @@ -50,7 +50,7 @@ class VideoCache { try fileManager.createDirectory(atPath: appCacheDirectory as String, withIntermediateDirectories: false, attributes: nil) } catch let error { - NSLog("Aerial Error: Couldn't create cache directory: \(error)") + errorLog("Couldn't create cache directory: \(error)") return nil } } @@ -60,7 +60,7 @@ class VideoCache { static func isAvailableOffline(video: AerialVideo) -> Bool { guard let videoCachePath = cachePath(forVideo: video) else { - NSLog("Aerial Error: Couldn't get video cache path!") + errorLog("Couldn't get video cache path!") return false } @@ -86,6 +86,7 @@ class VideoCache { } init(URL: Foundation.URL) { + debugLog("initvideocache") videoData = Data() loading = true self.URL = URL @@ -109,7 +110,7 @@ class VideoCache { func receivedData(_ data: Data, atRange range: NSRange) { guard let mutableVideoData = mutableVideoData else { - NSLog("Aerial Error: Received data without having mutable video data") + errorLog("Received data without having mutable video data") return } @@ -149,25 +150,25 @@ class VideoCache { let fileManager = FileManager.default guard let videoCachePath = videoCachePath else { - NSLog("Aerial Error: Couldn't save cache file") + errorLog("Couldn't save cache file") return } guard fileManager.fileExists(atPath: videoCachePath) == false else { - NSLog("Aerial Error: Cache file \(videoCachePath) already exists.") + errorLog("Cache file \(videoCachePath) already exists.") return } loading = false guard let mutableVideoData = mutableVideoData else { - NSLog("Aerial Error: Missing video data for save.") + errorLog("Missing video data for save.") return } do { try mutableVideoData.write(toFile: videoCachePath, options: .atomicWrite) } catch let error { - NSLog("Aerial Error: Couldn't write cache file: \(error)") + errorLog("Couldn't write cache file: \(error)") } } @@ -175,7 +176,7 @@ class VideoCache { let fileManager = FileManager.default guard let videoCachePath = self.videoCachePath else { - NSLog("Aerial Error: Couldn't load cache file.") + errorLog("Couldn't load cache file.") return } @@ -184,7 +185,7 @@ class VideoCache { } guard let videoData = try? Data(contentsOf: Foundation.URL(fileURLWithPath: videoCachePath)) else { - NSLog("Aerial Error: NSData failed to load cache file \(videoCachePath)") + errorLog("NSData failed to load cache file \(videoCachePath)") return } @@ -197,7 +198,7 @@ class VideoCache { func fulfillLoadingRequest(_ loadingRequest: AVAssetResourceLoadingRequest) -> Bool { guard let dataRequest = loadingRequest.dataRequest else { - NSLog("Aerial Error: Missing data request for \(loadingRequest)") + errorLog("Missing data request for \(loadingRequest)") return false } @@ -239,7 +240,7 @@ class VideoCache { } guard let dataRequest = loadingRequest.dataRequest else { - NSLog("Aerial Error: Missing data request for \(loadingRequest)") + errorLog("Missing data request for \(loadingRequest)") return false } diff --git a/Aerial/Source/Models/Cache/VideoDownload.swift b/Aerial/Source/Models/Cache/VideoDownload.swift index 593e7844..dca1afc8 100644 --- a/Aerial/Source/Models/Cache/VideoDownload.swift +++ b/Aerial/Source/Models/Cache/VideoDownload.swift @@ -57,6 +57,14 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { startDownloadForChunk(nil) } + func cancel() { + for stream in streams { + stream.connection.cancel() + } + infoLog("Video download cancelled") + delegate.videoDownload(self, finished: false, errorMessage: nil) + } + func startDownloadForChunk(_ chunk: NSRange?) { let request = NSMutableURLRequest(url: video.url as URL) request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringCacheData @@ -70,7 +78,7 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { guard let connection = NSURLConnection(request: request as URLRequest, delegate: self, startImmediately: false) else { - NSLog("Aerial: Error creating connection with request: \(request)") + errorLog("Error creating connection with request: \(request)") return } @@ -140,7 +148,7 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { func receiveDataForStream(_ stream: VideoDownloadStream, receivedData: Data) { guard let videoData = self.data else { - NSLog("Aerial error: video data missing!") + errorLog("Aerial error: video data missing!") return } @@ -152,13 +160,13 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { func finishedDownload() { guard let videoCachePath = VideoCache.cachePath(forVideo: video) else { - print("Aerial Error: Couldn't save video because couldn't get cache path\n") + errorLog("Couldn't save video because couldn't get cache path\n") failedDownload("Couldn't get cache path") return } guard let videoData = self.data else { - print("Aerial error: video data missing!\n") + errorLog("video data missing!\n") return } @@ -167,7 +175,7 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { do { try videoData.write(toFile: videoCachePath, options: .atomicWrite) } catch let error { - NSLog("Aerial Error: Couldn't write cache file: \(error)") + errorLog("Couldn't write cache file: \(error)") errorMessage = "Couldn't write to cache file!" success = false } @@ -186,7 +194,7 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { func connection(_ connection: NSURLConnection, didReceive response: URLResponse) { guard let stream = streamForConnection(connection) else { - NSLog("Aerial Error: No matching stream for connection: \(connection) with response: \(response)") + errorLog("No matching stream for connection: \(connection) with response: \(response)") return } @@ -206,7 +214,7 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { queue.async(execute: { () -> Void in guard let offset = self.startOffsetFromResponse(response) else { - NSLog("Aerial Error: Couldn't get start offset from response: \(response)") + errorLog("Couldn't get start offset from response: \(response)") return } @@ -226,7 +234,7 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { delegate.videoDownload(self, receivedBytes: data.count, progress: progress) guard let stream = self.streamForConnection(connection) else { - NSLog("Aerial Error: No matching stream for connection: \(connection)") + errorLog("No matching stream for connection: \(connection)") return } @@ -239,12 +247,12 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { debugLog("connectionDidFinishLoading") guard let stream = self.streamForConnection(connection) else { - NSLog("Aerial Error: No matching stream for connection: \(connection)") + errorLog("No matching stream for connection: \(connection)") return } guard let index = self.streams.index(where: { $0.connection == stream.connection }) else { - NSLog("Aerial Error: Couldn't find index of stream for finished connection!") + errorLog("Couldn't find index of stream for finished connection!") return } @@ -258,14 +266,14 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { } func connection(_ connection: NSURLConnection, didFailWithError error: Error) { - NSLog("Aerial Error: Couldn't download video: \(error)") + errorLog("Couldn't download video: \(error.localizedDescription)") queue.async { () -> Void in - self.failedDownload("Connection fail: \(error)") + self.failedDownload("Connection fail: \(error.localizedDescription)") } } func connection(_ connection: NSURLConnection, didReceive challenge: URLAuthenticationChallenge) { - NSLog("Aerial Error: Didn't expect authentication challenge while downloading videos!") + errorLog("Didn't expect authentication challenge while downloading videos!") queue.async { () -> Void in self.failedDownload("Connection fail: Received authentication request!") } @@ -280,21 +288,21 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { regex = try NSRegularExpression(pattern: "bytes (\\d+)-\\d+/\\d+", options: NSRegularExpression.Options.caseInsensitive) } catch let error as NSError { - NSLog("Aerial: Error formatting regex: \(error)") + errorLog("Error formatting regex: \(error)") return nil } let httpResponse = response as! HTTPURLResponse guard let contentRange = httpResponse.allHeaderFields["Content-Range"] as? NSString else { - debugLog("Weird, no byte response: \(response)") + errorLog("Weird, no byte response: \(response)") return nil } guard let match = regex.firstMatch(in: contentRange as String, options: NSRegularExpression.MatchingOptions.anchored, range: NSRange(location:0, length: contentRange.length)) else { - debugLog("Weird, couldn't make a regex match for byte offset: \(contentRange)") + errorLog("Weird, couldn't make a regex match for byte offset: \(contentRange)") return nil } let offsetMatchRange = match.range(at: 1) @@ -302,8 +310,6 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { let offset = offsetString.longLongValue -// debugLog("content range: \(contentRange), start offset: \(offset)") - return Int(offset) } } diff --git a/Aerial/Source/Models/Cache/VideoLoader.swift b/Aerial/Source/Models/Cache/VideoLoader.swift index a07179a8..8d9a61f5 100644 --- a/Aerial/Source/Models/Cache/VideoLoader.swift +++ b/Aerial/Source/Models/Cache/VideoLoader.swift @@ -28,11 +28,12 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { let queue = DispatchQueue.main init(url: URL, loadingRequest: AVAssetResourceLoadingRequest, delegate: VideoLoaderDelegate) { + //debugLog("videoloader init") self.delegate = delegate self.loadingRequest = loadingRequest let request = NSMutableURLRequest(url: url) - request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringCacheData + request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData loadRange = false loadedRange = NSRange(location: 0, length: 0) @@ -51,21 +52,22 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { request.setValue(requestRange, forHTTPHeaderField: "Range") } } - + //debugLog("loadedRange \(loadedRange)") + //debugLog("requestedRange \(requestedRange)") super.init() connection = NSURLConnection(request: request as URLRequest, delegate: self, startImmediately: false) guard let connection = connection else { - NSLog("Aerial error: Couldn't instantiate connection.") + errorLog("Couldn't instantiate connection.") return } connection.setDelegateQueue(OperationQueue.main) loadedRange = NSRange(location: requestedRange.location, length: 0) - + connection.start() -// debugLog("Starting request: \(request)") + //debugLog("Starting request: \(request)") } deinit { @@ -93,42 +95,52 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { func connection(_ connection: NSURLConnection, didReceive data: Data) { queue.async { () -> Void in - self.fillInContentInformation(self.loadingRequest) guard let dataRequest = self.loadingRequest.dataRequest else { - NSLog("Aerial Error: Data request missing for \(self.loadingRequest)") + errorLog("Data request missing for \(self.loadingRequest)") return } + //debugLog("drl \(dataRequest.requestedLength) dro \(dataRequest.requestedOffset)") + //debugLog("\(dataRequest)") + + /*if (data.count > 100000) { + debugLog("NOTGOOD") + dataLog(data) + }*/ let requestedRange = self.requestedRange let loadedRange = self.loadedRange let loadedLocation = loadedRange.location + loadedRange.length let dataRange = NSRange(location: loadedRange.location + loadedRange.length, length: data.count) + //debugLog("\(dataRange)") + self.delegate?.videoLoader(self, receivedData: data, forRange: dataRange) // check if we've already been sending content, or we're at right byte offset if loadedLocation >= requestedRange.location { - + //debugLog("case1") let requestedEndOffset = Int(dataRequest.requestedOffset + Int64(dataRequest.requestedLength)) - + let pendingDataEndOffset = loadedLocation + data.count + //debugLog("r \(requestedEndOffset) p \(pendingDataEndOffset)") if pendingDataEndOffset > requestedEndOffset { let truncateDataLength = pendingDataEndOffset - requestedEndOffset let truncatedData = data.subdata(in: 0..= requestedRange.location { + //debugLog("case2") // calculate how far along we need to be into the data before it's part of what // was requested let inset = requestedRange.location - loadedRange.location @@ -145,12 +157,12 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { self.connection?.cancel() } } else if inset < 1 { - NSLog("Aerial Error: Inset is invalid value: \(inset)") + errorLog("Inset is invalid value: \(inset)") } } -// debugLog("Received data with length: \(data.count)") + //debugLog("Received data with length: \(data.count)") self.loadedRange.length += data.count @@ -186,7 +198,7 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { return } - // debugLog("Processsing contentInformationRequest") + debugLog("Processsing contentInformationRequest") let contentType: String = uti.takeRetainedValue() as String @@ -194,7 +206,7 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { contentInformationRequest.contentType = contentType contentInformationRequest.contentLength = response.expectedContentLength - //debugLog("expected content length: \(response.expectedContentLength)") + debugLog("expected content length: \(response.expectedContentLength) type:\(contentType)") } // MARK: - Range @@ -208,21 +220,21 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { regex = try NSRegularExpression(pattern: "bytes (\\d+)-\\d+/\\d+", options: NSRegularExpression.Options.caseInsensitive) } catch let error as NSError { - NSLog("Aerial: Error formatting regex: \(error)") + errorLog("Error formatting regex: \(error)") return nil } let httpResponse = response as! HTTPURLResponse guard let contentRange = httpResponse.allHeaderFields["Content-Range"] as? NSString else { - debugLog("Weird, no byte response: \(response)") + errorLog("Weird, no byte response: \(response)") return nil } guard let match = regex.firstMatch(in: contentRange as String, options: NSRegularExpression.MatchingOptions.anchored, range: NSRange(location:0, length: contentRange.length)) else { - debugLog("Weird, couldn't make a regex match for byte offset: \(contentRange)") + errorLog("Weird, couldn't make a regex match for byte offset: \(contentRange)") return nil } let offsetMatchRange = match.range(at: 1) @@ -230,7 +242,7 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { let offset = offsetString.longLongValue -// debugLog("content range: \(contentRange), start offset: \(offset)") + //debugLog("content range: \(contentRange), start offset: \(offset)") return Int(offset) } diff --git a/Aerial/Source/Models/Cache/VideoManager.swift b/Aerial/Source/Models/Cache/VideoManager.swift index fde61f46..028ca199 100644 --- a/Aerial/Source/Models/Cache/VideoManager.swift +++ b/Aerial/Source/Models/Cache/VideoManager.swift @@ -7,19 +7,25 @@ // import Foundation +typealias VideoManagerCallback = (Int,Int) -> (Void) class VideoManager : NSObject { static let sharedInstance = VideoManager() + var managerCallbacks = [VideoManagerCallback]() /// Dictionary of CheckCellView, keyed by the video.id private var checkCells = [String: CheckCellView]() - + /// List of queued videos, by video.id private var queuedVideos = [String]() /// Dictionary of operations, keyed by the video.id fileprivate var operations = [String: VideoDownloadOperation]() + /// Number of videos that were queued + private var totalQueued = 0 + var stopAll = false + //var downloadItems: [VideoDownloadItem] /// Serial OperationQueue for downloads @@ -36,6 +42,10 @@ class VideoManager : NSObject { checkCells[id] = checkCellView } + func addCallback(_ callback:@escaping VideoManagerCallback) { + managerCallbacks.append(callback) + } + // Is the video queued for download ? func isVideoQueued(id: String) -> Bool { if queuedVideos.firstIndex(of: id) != nil { @@ -47,28 +57,55 @@ class VideoManager : NSObject { @discardableResult func queueDownload(_ video: AerialVideo) -> VideoDownloadOperation { - + print(queue.isSuspended) + if stopAll { + stopAll = false + } + + print(queue.operations) + let operation = VideoDownloadOperation(video:video, delegate: self) operations[video.id] = operation queue.addOperation(operation) queuedVideos.append(video.id) // Our Internal List of queued videos markAsQueued(id: video.id) // Callback the CheckCellView - + totalQueued = totalQueued+1 // Increment our count + + DispatchQueue.main.async { + // Callback the callbacks + for callback in self.managerCallbacks { + callback(self.totalQueued-self.queuedVideos.count, self.totalQueued) + } + } return operation } // Callbacks for Items - func finishedDownload(id: String) + func finishedDownload(id: String, success: Bool) { // Manage our queuedVideo index if let index = queuedVideos.firstIndex(of: id) { queuedVideos.remove(at: index) } + if queuedVideos.isEmpty { + totalQueued = 0 + } + + DispatchQueue.main.async { + // Callback the callbacks + for callback in self.managerCallbacks { + callback(self.totalQueued-self.queuedVideos.count, self.totalQueued) + } + } // Then callback the CheckCellView if let cell = checkCells[id] { - cell.markAsDownloaded() + if success { + cell.markAsDownloaded() + } else { + cell.markAsNotDownloaded() + } } } @@ -83,6 +120,13 @@ class VideoManager : NSObject { cell.updateProgressIndicator(progress: progress) } } + + /// Cancel all queued operations + + func cancelAll() { + stopAll = true + queue.cancelAllOperations() + } } @@ -97,23 +141,50 @@ class VideoDownloadOperation : AsynchronousOperation { } override func main() { - print("start \(video.name)") + let videoManager = VideoManager.sharedInstance + if videoManager.stopAll { + print("was cancelled and mained") + return + } + + debugLog("Starting download for \(video.name)") DispatchQueue.main.async { self.download = VideoDownload(video: self.video, delegate: self) self.download!.startDownload() } } + + override func cancel() { + defer { finish() } + let videoManager = VideoManager.sharedInstance + + if ((self.download) != nil) { + self.download!.cancel() + } else { + videoManager.finishedDownload(id: self.video.id, success: false) + } + super.cancel() + //finish() + } } extension VideoDownloadOperation : VideoDownloadDelegate { func videoDownload(_ videoDownload: VideoDownload, finished success: Bool, errorMessage: String?) { - print("finished") + debugLog("Finished") defer { finish() } - // Call up to clean the view let videoManager = VideoManager.sharedInstance - videoManager.finishedDownload(id: videoDownload.video.id) + if success { + // Call up to clean the view + videoManager.finishedDownload(id: videoDownload.video.id, success: true) + } else { + if (errorMessage != nil) { + errorLog(errorMessage!) + } + + videoManager.finishedDownload(id: videoDownload.video.id, success: false) + } } func videoDownload(_ videoDownload: VideoDownload, receivedBytes: Int, progress: Float) { diff --git a/Aerial/Source/Models/Debug.swift b/Aerial/Source/Models/Debug.swift deleted file mode 100644 index 2aed247f..00000000 --- a/Aerial/Source/Models/Debug.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Debug.swift -// Aerial -// -// Created by John Coates on 10/28/15. -// Copyright © 2015 John Coates. All rights reserved. -// - -import Foundation - -func debugLog(_ message: String) { - #if DEBUG - print("\(message)\n") - #endif -} diff --git a/Aerial/Source/Models/Downloads/DownloadManager.swift b/Aerial/Source/Models/Downloads/DownloadManager.swift index 6fc9eae3..85741cb7 100644 --- a/Aerial/Source/Models/Downloads/DownloadManager.swift +++ b/Aerial/Source/Models/Downloads/DownloadManager.swift @@ -110,28 +110,28 @@ extension DownloadOperation: URLSessionDownloadDelegate { // tvOS11 and tvOS10 JSONs are named entries.json, so we rename them here if downloadTask.originalRequest!.url!.absoluteString.contains("2x/entries.json") { - NSLog("Aerial: Caching tvos11.json") + debugLog("Caching tvos11.json") destinationURL.appendPathComponent("tvos11.json") } else if downloadTask.originalRequest!.url!.absoluteString.contains("Autumn") { - NSLog("Aerial: Caching tvos10.json") + debugLog("Caching tvos10.json") destinationURL.appendPathComponent("tvos10.json") } else { - NSLog("Aerial: Caching \(downloadTask.originalRequest!.url!.lastPathComponent)") + debugLog("Caching \(downloadTask.originalRequest!.url!.lastPathComponent)") destinationURL.appendPathComponent(downloadTask.originalRequest!.url!.lastPathComponent) } try? manager.removeItem(at: destinationURL) try manager.moveItem(at: location, to: destinationURL) } catch { - print(error) + errorLog("\(error)") } } func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { - let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) - print("\(downloadTask.originalRequest!.url!.absoluteString) \(progress)") + //let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) + //print("\(downloadTask.originalRequest!.url!.absoluteString) \(progress)") } } @@ -143,13 +143,13 @@ extension DownloadOperation: URLSessionTaskDelegate { defer { finish() } if let error = error { - print(error) + errorLog("\(error)") return } // We need to untar the resources.tar if task.originalRequest!.url!.absoluteString.contains("resources.tar") { - print("untaring resources.tar") + debugLog("untaring resources.tar") // Extract json let process:Process = Process() @@ -167,8 +167,6 @@ extension DownloadOperation: URLSessionTaskDelegate { process.waitUntilExit() } - print("Finished \(task.originalRequest!.url!.absoluteString)") - + debugLog("Finished downloading \(task.originalRequest!.url!.absoluteString)") } - } diff --git a/Aerial/Source/Models/ErrorLog.swift b/Aerial/Source/Models/ErrorLog.swift new file mode 100644 index 00000000..8fb6236e --- /dev/null +++ b/Aerial/Source/Models/ErrorLog.swift @@ -0,0 +1,154 @@ +// +// ErrorLog.swift +// Aerial +// +// Created by Guillaume Louel on 17/10/2018. +// Copyright © 2018 John Coates. All rights reserved. +// + +import Cocoa +import os.log + +enum ErrorLevel : Int { + case info, debug, warning, error +} + +class LogMessage { + let date : Date + let level : ErrorLevel + let message : String + var actionName : String? + var actionBlock : BlockOperation? + + init(level: ErrorLevel, message: String) { + self.level = level + self.message = message + self.date = Date() + } +} + +typealias LoggerCallback = (ErrorLevel) -> (Void) + +class Logger { + static let sharedInstance = Logger() + + var callbacks = [LoggerCallback]() + + func addCallback(_ callback:@escaping LoggerCallback) { + callbacks.append(callback) + } + + func callBack(level: ErrorLevel) { + DispatchQueue.main.async { + for callback in self.callbacks { + callback(level) + } + } + } +} +var errorMessages = [LogMessage]() + +func Log(level: ErrorLevel, message: String) { + errorMessages.append(LogMessage(level: level, message: message)) + + + // We throw errors to console, they always matter + if (level == .error) { + if #available(OSX 10.12, *) { + // This is faster when available + let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Screensaver") + os_log("AerialError: %@", log: log, type: .error, message) + } else { + // Fallback on earlier versions + NSLog("AerialError: \(message)") + } + } + + let preferences = Preferences.sharedInstance + + // We may callback + if (level == .warning || level == .error || (level == .debug && preferences.debugMode)) { + let logger = Logger.sharedInstance + logger.callBack(level: level) + } + + // We may log to disk + if (preferences.logToDisk) { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .none + dateFormatter.timeStyle = .medium + let string = dateFormatter.string(from: Date()) + " : " + message + "\n" + //let string = message + "\n" + if let cacheDirectory = VideoCache.cacheDirectory { + var cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) + cacheFileUrl.appendPathComponent("AerialLog.txt") + + let data = string.data(using: String.Encoding.utf8, allowLossyConversion: false)! + //let data = message.data(using: String.Encoding.utf8, allowLossyConversion: false)! + + if FileManager.default.fileExists(atPath: cacheFileUrl.path) { + do { + let fileHandle = try FileHandle(forWritingTo: cacheFileUrl) + fileHandle.seekToEndOfFile() + fileHandle.write(data) + fileHandle.closeFile() + } catch { + print("Can't open handle") + } + } else { + do { + try data.write(to: cacheFileUrl, options: .atomic) + } catch { + print("Can't write to file") + } + } + } + } +} + +func debugLog(_ message: String) { + #if DEBUG + print("\(message)\n") + #endif + + let preferences = Preferences.sharedInstance + if (preferences.debugMode) { + Log(level:.debug, message:message) + } +} + +func infoLog(_ message: String) { + Log(level:.info, message:message) +} + +func warnLog(_ message: String) { + Log(level:.warning, message:message) +} + +func errorLog(_ message: String) { + Log(level:.error, message:message) +} + +func dataLog(_ data:Data) { + let cacheDirectory = VideoCache.cacheDirectory! + var cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) + cacheFileUrl.appendPathComponent("AerialData.txt") + + if FileManager.default.fileExists(atPath: cacheFileUrl.path) { + do { + let fileHandle = try FileHandle(forWritingTo: cacheFileUrl) + fileHandle.seekToEndOfFile() + fileHandle.write(data) + fileHandle.closeFile() + } catch { + print("Can't open handle") + } + } else { + do { + try data.write(to: cacheFileUrl, options: .atomic) + } catch { + print("Can't write to file") + } + } + +} diff --git a/Aerial/Source/Models/ManifestLoader.swift b/Aerial/Source/Models/ManifestLoader.swift index 84a82d5e..5a68c19a 100644 --- a/Aerial/Source/Models/ManifestLoader.swift +++ b/Aerial/Source/Models/ManifestLoader.swift @@ -20,6 +20,10 @@ class ManifestLoader { var processedVideos = [AerialVideo]() var lastPluckedFromPlaylist: AerialVideo? + var manifestTvOS10: Data? + var manifestTvOS11: Data? + var manifestTvOS12: Data? + // Playlist management var playlistIsRestricted = false var playlistRestrictedTo = "" @@ -42,81 +46,6 @@ class ManifestLoader { ["url-1080-SDR":"https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_2K_SDR_HEVC.mov", "url-4K-SDR":"https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_4K_SDR_HEVC.mov"]] // Dubai night 2 - // Better Descriptions - let mergeName = [ - "6C3D54AE-0871-498A-81D0-56ED24E5FE9F":"Korea and Japan Night", // Fixint Typo - "B876B645-3955-420E-99DF-60139E451CF3":"Wulingyuan National Park 1", // China day 1 - "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C":"Longji Rice Terraces", // China day 2 - "D5E76230-81A3-4F65-A1BA-51B8CADED625":"Wulingyuan National Park 2", // China day 3 - "b6-1":"Great Wall 1", // China day 4 - "b2-1":"Great Wall 2", // China day 5 - "b5-1":"Great Wall 3", // China day 6 - - "AC9C09DD-1D97-4013-A09F-B0F5259E64C3":"Sheikh Zayed Road", // Dubai day 1 - "49790B7C-7D8C-466C-A09E-83E38B6BE87A":"Marina 1", // Dubai day 2 - "02EA5DBE-3A67-4DFA-8528-12901DFD6CC1":"Downtown", // Dubai day 3 - "802866E6-4AAF-4A69-96EA-C582651391F1":"Marina 2", // Dubai day 4 - - "BAF76353-3475-4855-B7E1-CE96CC9BC3A7":"Approaching Burj Khalifa", // Dubai night 1 - "2F11E857-4F77-4476-8033-4A1E4610AFCC":"Sheikh Zayed Road", // Dubai night 2 - - "E4ED0B22-EB81-4D4F-A29E-7E1EA6B6D980":"Nuussuaq Peninsula", // Greenland day 1 - "30047FDA-3AE3-4E74-9575-3520AD77865B":"Ilulissat Icefjord", // Greenland day 2 - - "7D4710EB-5BA4-42E6-AA60-68D77F67D9B9":"Ilulissat Icefjord", // Greenland night 1 - - "b7-1":"Laupāhoehoe Nui", // Hawaii day 1 - "b1-1":"Waimanu Valley", // Hawaii day 2 - "b2-2":"Honopū Valley", // Hawaii day 3 - "b4-1":"Pu‘u O ‘Umi", // Hawaii day 4 - - "b6-2":"Kohala coastline", // Hawaii night 1 - "b8-1":"Pu‘u O ‘Umi", // Hawaii night 2 - - "102C19D1-9D9F-48EC-B492-074C985C4D9F":"Victoria Harbour 1", // Hong Kong day 1 - "560E09E8-E89D-4ADB-8EEA-4754415383D4":"Victoria Peak", // Hong Kong day 2 - "024891DE-B7F6-4187-BFE0-E6D237702EF0":"Wan Chai", // Hong Kong day 3 - "786E674C-BB22-4AA9-9BD3-114D2020EC4D":"Victoria Harbour 2", // Hong Kong day 4 - - "30313BC1-BF20-45EB-A7B1-5A6FFDBD2488":"Victoria Harbour", // Hong Kong night 1 - - "6E2FC8AC-832D-46CF-B306-BB2A05030C17":"Liwa Oasis", // Liwa day 1 - - "b6-3":"Tower Bridge", // London day 1 - "b5-2":"Buckingham Palace", // London day 2 - - "b1-2":"Tower Bridge 1", // London night 1 - "b3-1":"Tower Bridge 2", // London night 2 - - "829E69BA-BB53-4841-A138-4DF0C2A74236":"LAX", // Los Angeles day 1 - "30A2A488-E708-42E7-9A90-B749A407AE1C":"Interstate 110", // Los Angeles day 2 - "B730433D-1B3B-4B99-9500-A286BF7A9940":"Santa Monica Beach", // Los Angeles day 3 - - "89B1643B-06DD-4DEC-B1B0-774493B0F7B7":"Griffith Observatory", // Los Angeles night 1 - "EC67726A-8212-4C5E-83CF-8412932740D2":"Hollywood Sign", // Los Angeles night 2 - "A284F0BF-E690-4C13-92E2-4672D93E8DE5":"Downtown", // Los Angeles night 3 - - "b7-2":"Central Park", // New York day 1 - "b1-3":"Lower Manhattan", // New York day 2 - "b3-2":"Upper East Side", // New York day 3 - - "b2-3":"7th avenue", // New York night 1 - "b4-2":"Lower Manhattan", // New York night 2 - - - "b8-2":"Marin Headlands", // San Francisco day 1 - "b10-3":"Presidio to Golden Gate", // San Francisco day 2 - "b9-3":"Bay and Golden Gate", // San Francisco day 3 - "b8-3":"Downtown", // San Francisco day 4 - "b3-3":"Embarcadero/Market Street", // San Francisco day 5 - "b4-3":"Golden Gate from SF", // San Francisco day 6 - - "b6-4":"Downtown/Coit Tower", // San Francisco night 1 - "b7-3":"Fisherman's Wharf", // San Francisco night 2 - "b5-3":"Embarcadero/Market Street", // San Francisco night 3 - "b1-4":"Bay Bridge", // San Francisco night 4 - "b2-4":"Downtown/Sutro Tower" // San Francisco night 5 - ] // Extra POI let mergePOI = [ @@ -156,14 +85,8 @@ class ManifestLoader { "b2-4":"A018_C014_" // San Francisco night 5 ] - func addCallback(_ callback:@escaping manifestLoadCallback) { - if loadedManifest.count > 0 { - callback(loadedManifest) - } else { - callbacks.append(callback) - } - } + // MARK: - Playlist generation func generatePlaylist(isRestricted:Bool, restrictedTo:String) { // Start fresh playlist = [AerialVideo]() @@ -204,7 +127,6 @@ class ManifestLoader { // On regenerating a new playlist, we try to avoid repeating while (playlist.count > 1 && lastPluckedFromPlaylist == playlist.first) { - //NSLog("AerialDBG: Reshuffle") playlist.shuffle() } } @@ -214,7 +136,6 @@ class ManifestLoader { let (shouldRestrictByDayNight,restrictTo) = timeManagement.shouldRestrictPlaybackToDayNightVideo() if (playlist.count == 0 || (restrictTo != playlistRestrictedTo) || (shouldRestrictByDayNight != playlistIsRestricted)) { - //NSLog("AerialDBG: Generating new playlist") generatePlaylist(isRestricted: shouldRestrictByDayNight, restrictedTo: restrictTo) } @@ -224,7 +145,6 @@ class ManifestLoader { } else { return findBestEffortVideo() } - } // Find a backup plan when conditions are not met @@ -239,10 +159,10 @@ class ManifestLoader { // - return a random one from the manifest that is cached // - return a random video that is not cached (slight betrayal of the Never stream videos) - NSLog("AerialDBG: empty playlist, not good !") + warnLog("Empty playlist, not good !") if lastPluckedFromPlaylist != nil { - NSLog("AerialDBG: returning last played video after condition change not met !") + warnLog("returning last played video after condition change not met !") return lastPluckedFromPlaylist! } else { // Start with a shuffled list @@ -251,7 +171,7 @@ class ManifestLoader { if (shuffled.count == 0) { // This is super bad, no manifest at all - NSLog("AerialDBG: No manifest, nothing to play !") + errorLog("No manifest, nothing to play !") return nil } @@ -261,42 +181,44 @@ class ManifestLoader { // If we find anything cached and in rotation, we send that back if video.isAvailableOffline && inRotation { - NSLog("AerialDBG: returning random cached in rotation video after condition change not met !") + warnLog("returning random cached in rotation video after condition change not met !") return video } } // Nothing ? Sorry but you'll get a non cached file - NSLog("AerialDBG: returning random video after condition change not met !") + warnLog("returning random video after condition change not met !") return shuffled.first! } } + // MARK: - Lifecycle + init() { - NSLog("AerialML: Manifest init") + debugLog("Manifest init") // We try to load our video manifests in 3 steps : - // - use locally saved data in preferences plist + // - reload from local variables (unused for now) // - reprocess the saved files in cache directory (full offline mode) // - download the manifests from servers - NSLog("AerialML: 10 \(isManifestCached(manifest: .tvOS10))") - NSLog("AerialML: 11 \(isManifestCached(manifest: .tvOS11))") - NSLog("AerialML: 12 \(isManifestCached(manifest: .tvOS12))") + debugLog("isManifestCached 10 \(isManifestCached(manifest: .tvOS10))") + debugLog("isManifestCached 11 \(isManifestCached(manifest: .tvOS11))") + debugLog("isManifestCached 12 \(isManifestCached(manifest: .tvOS12))") - if areManifestsSaved() { - NSLog("AerialML: Loading from plist") - loadSavedManifests() + if areManifestsFilesLoaded() { + debugLog("Files were already loaded") + loadManifestsFromLoadedFiles() } else { - NSLog("AerialML: Not available from plist") + debugLog("Files were not already loaded") // Manifests are not in our preferences plist, are they cached on disk ? if areManifestsCached() { - NSLog("AerialML: Manifests are cached on disk, loading") + debugLog("Manifests are cached on disk, loading") loadCachedManifests() } else { // Ok then, we fetch them... - NSLog("AerialML: fetching missing manifests online") + debugLog("Fetching missing manifests online") let downloadManager = DownloadManager() var urls: [URL] = [] @@ -315,7 +237,7 @@ class ManifestLoader { } let completion = BlockOperation { - NSLog("AerialML: fetching all done") + debugLog("Fetching manifests all done") // We can now load from the newly cached files self.loadCachedManifests() @@ -331,14 +253,24 @@ class ManifestLoader { } } - // Check if the Manifests have been saved in our preferences plist - func areManifestsSaved() -> Bool { - if (preferences.manifestTvOS12 != nil && preferences.manifestTvOS11 != nil && preferences.manifestTvOS10 != nil) { - NSLog("AerialML: manifests are saved in preferences") + func addCallback(_ callback:@escaping manifestLoadCallback) { + if loadedManifest.count > 0 { + callback(loadedManifest) + } else { + callbacks.append(callback) + } + } + + // MARK: - Manifests + + // Check if the Manifests have been loaded in this class already + func areManifestsFilesLoaded() -> Bool { + if (manifestTvOS12 != nil && manifestTvOS11 != nil && manifestTvOS10 != nil) { + debugLog("Manifests files were loaded in class") return true } else { - NSLog("AerialML: manifests are NOT saved in preferences") + debugLog("Manifests files were not loaded in class") return false } } @@ -359,8 +291,6 @@ class ManifestLoader { if !fileManager.fileExists(atPath: cacheResourcesString) { return false } - - NSLog("AerialML: \(manifest.rawValue) manifest is cached") } else { @@ -376,98 +306,102 @@ class ManifestLoader { // tvOS12 var cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) cacheFileUrl.appendPathComponent("entries.json") - NSLog("AerialML: 12path : \(cacheFileUrl)") do { let ndata = try Data(contentsOf: cacheFileUrl) - self.preferences.manifestTvOS12 = ndata + manifestTvOS12 = ndata } catch { - NSLog("Aerial: Error can't load entries.json from cached directory (tvOS12)") + errorLog("Can't load entries.json from cached directory (tvOS12)") } // tvOS11 cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) cacheFileUrl.appendPathComponent("tvos11.json") - NSLog("AerialML: 11path : \(cacheFileUrl)") - do { let ndata = try Data(contentsOf: cacheFileUrl) - self.preferences.manifestTvOS11 = ndata + manifestTvOS11 = ndata } catch { - NSLog("Aerial: Error can't load tvos11.json from cached directory ") + errorLog("Can't load tvos11.json from cached directory") } // tvOS10 cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) cacheFileUrl.appendPathComponent("tvos10.json") - NSLog("AerialML: 10path : \(cacheFileUrl)") - do { let ndata = try Data(contentsOf: cacheFileUrl) - self.preferences.manifestTvOS10 = ndata + manifestTvOS10 = ndata } catch { - NSLog("Aerial: Error can't load tvos10.json from cached directory") + errorLog("Can't load tvos10.json from cached directory") } - if self.preferences.manifestTvOS10 != nil || self.preferences.manifestTvOS11 != nil || self.preferences.manifestTvOS12 != nil { - loadSavedManifests() + if manifestTvOS10 != nil || manifestTvOS11 != nil || manifestTvOS12 != nil { + loadManifestsFromLoadedFiles() } else { // No internet, no anything, nothing to do - NSLog("AerialDBG: No video to load, no internet connexion ?") + errorLog("No video to load, no internet connexion ?") } } } // Load Manifests from the saved preferences - func loadSavedManifests() { - NSLog("AerialML: LSM") - + func loadManifestsFromLoadedFiles() { // Reset our array processedVideos = [] - if (preferences.manifestTvOS12 != nil) { - NSLog("AerialML: lsm12") + if (manifestTvOS12 != nil) { // We start with the more recent one, it has more information (poi, etc) - readJSONFromData(preferences.manifestTvOS12!, manifest: .tvOS12) + readJSONFromData(manifestTvOS12!, manifest: .tvOS12) + } else { + warnLog("tvOS12 manifest is absent") } - if (preferences.manifestTvOS11 != nil) { - NSLog("AerialML: lsm11") + + if (manifestTvOS11 != nil) { // This one has a couple videos not in the tvOS12 JSON. No H264 for these ! - readJSONFromData(preferences.manifestTvOS11!, manifest: .tvOS11) + readJSONFromData(manifestTvOS11!, manifest: .tvOS11) + } else { + warnLog("tvOS11 manifest is absent") } - if (preferences.manifestTvOS10 != nil) { - NSLog("AerialML: lsm10") + + if (manifestTvOS10 != nil) { // The original manifest is in another format - readOldJSONFromData(preferences.manifestTvOS10!, manifest: .tvOS10) + readOldJSONFromData(manifestTvOS10!, manifest: .tvOS10) + } else { + warnLog("tvOS10 manifest is absent") } - NSLog("AerialML: post json loading") - - processedVideos = processedVideos.sorted { $0.secondaryName < $1.secondaryName } // Only matters for Space videos, this way they show sorted in the Space category + processedVideos = processedVideos.sorted { $0.secondaryName < $1.secondaryName } // We sort videos by secondary names, so they can display sorted in our view later self.loadedManifest = processedVideos - - NSLog("AerialML: \(processedVideos.count) videos processed !") + /* + // POI Extracter code + infoLog("\(processedVideos.count) videos processed !") + let poiStringProvider = PoiStringProvider.sharedInstance + for video in processedVideos { + infoLog(video.name + " " + video.secondaryName) + for poi in video.poi { + infoLog(poi.key + ": " + poiStringProvider.getString(key: poi.value)) + } + }*/ // callbacks for callback in self.callbacks { - NSLog("AerialML: Calling back") callback(self.loadedManifest) } self.callbacks.removeAll() } + // MARK: - JSON func readJSONFromData(_ data: Data, manifest: Manifests) { - //var videos = [AerialVideo]() - do { + let poiStringProvider = PoiStringProvider.sharedInstance + let options = JSONSerialization.ReadingOptions.allowFragments let batches = try JSONSerialization.jsonObject(with: data, options: options) guard let batch = batches as? NSDictionary else { - NSLog("Aerial: Encountered unexpected content type for batch, please report !") + errorLog("Encountered unexpected content type for batch, please report !") return } @@ -481,27 +415,29 @@ class ManifestLoader { let name = item["accessibilityLabel"] as! String var secondaryName = "" // We may have a secondary name - if let mergeName = mergeName[id] { - secondaryName = mergeName + if let mergename = poiStringProvider.getCommunityName(id: id) { + secondaryName = mergename } +/* if let mergeName = mergeName[id] { + secondaryName = mergeName + }*/ let timeOfDay = "day" // TODO, this is hardcoded as it's no longer available in the modern JSONs let type = "video" var poi : [String:String]? - if let mergeId = mergePOI[id] { - let poiStringProvider = PoiStringProvider.sharedInstance poi = poiStringProvider.fetchExtraPoiForId(id: mergeId) - } - else { + } else { poi = item["pointsOfInterest"] as? [String: String] } + + let communityPoi = poiStringProvider.getCommunityPoi(id: id) + + let (isDupe,foundDupe) = findDuplicate(id: id, url1080pH264: url1080pH264 ?? "") if (isDupe) { - //debugLog("duplicate found, adding \(manifest) as source to \(name)") foundDupe!.sources.append(manifest) - } - else { + } else { let video = AerialVideo(id: id, // Must have name: name, // Must have secondaryName: secondaryName, // Optional @@ -511,20 +447,22 @@ class ManifestLoader { url1080pHEVC: url1080pHEVC ?? "", url4KHEVC: url4KHEVC ?? "", manifest: manifest, - poi: poi ?? [:] ) // tvOS12 only + poi: poi ?? [:], + communityPoi: communityPoi) processedVideos.append(video) - //checkContentLength(video) } } } catch { - NSLog("Aerial: Error retrieving content listing.") + errorLog("Error retrieving content listing") return } } func readOldJSONFromData(_ data: Data, manifest: Manifests) { do { + let poiStringProvider = PoiStringProvider.sharedInstance + let options = JSONSerialization.ReadingOptions.allowFragments let batches = try JSONSerialization.jsonObject(with: data, options: options) as! Array @@ -543,32 +481,32 @@ class ManifestLoader { continue } - var secondaryName = "" // We may have a secondary name - if let mergeName = mergeName[id] { - secondaryName = mergeName + var secondaryName = "" + if let mergename = poiStringProvider.getCommunityName(id: id) { + secondaryName = mergename } - + + // We may have POIs to merge var poi : [String:String]? if let mergeId = mergePOI[id] { let poiStringProvider = PoiStringProvider.sharedInstance poi = poiStringProvider.fetchExtraPoiForId(id: mergeId) } - + + let communityPoi = poiStringProvider.getCommunityPoi(id: id) + + // We may have dupes... let (isDupe,foundDupe) = findDuplicate(id: id, url1080pH264: url) if isDupe { if (foundDupe != nil) { - //debugLog("duplicate found, adding \(manifest) as source to \(name)") foundDupe!.sources.append(manifest) if (foundDupe?.url1080pH264 == "") { - //debugLog("merging urls for \(url)") foundDupe?.url1080pH264 = url } - } - } - else { + } else { var url4khevc = "" var url1080phevc = "" // Check if we have some HEVC urls to merge @@ -577,6 +515,7 @@ class ManifestLoader { url4khevc = val["url-4K-SDR"]! } + // Now we can finally add... let video = AerialVideo(id: id, // Must have name: name, // Must have secondaryName: secondaryName, @@ -586,26 +525,15 @@ class ManifestLoader { url1080pHEVC: url1080phevc, url4KHEVC: url4khevc, manifest: manifest, - poi: poi ?? [:]) // tvOS12 only + poi: poi ?? [:], + communityPoi: communityPoi) processedVideos.append(video) - //checkContentLength(video) } - /*let video = AerialVideo(id: id, - name: name, - type: type, - timeOfDay: timeOfDay, - url: url) - - videos.append(video) - - checkContentLength(video)*/ } } - - //self.loadedManifest = videos } catch { - NSLog("Aerial: Error retrieving content listing.") + errorLog("Error retrieving content listing") return } } @@ -623,16 +551,13 @@ class ManifestLoader { if (url1080pH264 != "") { if (blacklist.contains((URL(string:url1080pH264)?.lastPathComponent)!)) { - //debugLog("Blacklisted video : \(url1080pH264)") return (true,nil) } } // We also have a Dictionary of duplicates that need source merging for (pid,replace) in dupePairs { - if (id == pid) - { - //debugLog("duplicate found by dupePairs \(id)") + if (id == pid) { for vid in processedVideos { if vid.id == replace { return (true,vid) @@ -643,12 +568,9 @@ class ManifestLoader { for video in processedVideos { if id == video.id { - //debugLog("duplicate found by ID") return (true,video) - } - else if (url1080pH264 != "" && video.url1080pH264 != "") { + } else if (url1080pH264 != "" && video.url1080pH264 != "") { if (URL(string:url1080pH264)?.lastPathComponent == URL(string:video.url1080pH264)?.lastPathComponent) { - //debugLog("duplicate found by filename") return (true,video) } } @@ -656,99 +578,4 @@ class ManifestLoader { return (false,nil) } - -/* func checkContentLength(_ video: AerialVideo) { - let config = URLSessionConfiguration.default - let session = URLSession(configuration: config) - let request = NSMutableURLRequest(url: video.url as URL) - - request.httpMethod = "HEAD" - - let task = session.dataTask(with: request as URLRequest, - completionHandler: { - data, response, error in - video.contentLengthChecked = true - - if let error = error { - NSLog("error fetching content length: \(error)") - DispatchQueue.main.async(execute: { () -> Void in - self.receivedContentLengthResponse() - }) - return - } - - guard let response = response else { - return - } - - video.contentLength = Int(response.expectedContentLength) -// NSLog("content length: \(response.expectedContentLength)") - DispatchQueue.main.async(execute: { () -> Void in - self.receivedContentLengthResponse() - }) - }) - - task.resume() - } - - func receivedContentLengthResponse() { - // check if content length on all videos has been checked - for video in loadedManifest { - if video.contentLengthChecked == false { - return - } - } - - filterVideoAndProcessCallbacks() - } - - func filterVideoAndProcessCallbacks() { - let unfiltered = loadedManifest - - var filtered = [AerialVideo]() - for video in unfiltered { - // offline? eror? just put it through - if video.contentLength == 0 { - filtered.append(video) - continue - } - - // check to see if we find another video with the same content length - var isDuplicate = false - for videoCheck in filtered { - if videoCheck.id == video.id { - isDuplicate = true - continue - } - - if videoCheck.name != video.name { - continue - } - - if videoCheck.timeOfDay != video.timeOfDay { - continue - } - - if videoCheck.contentLength == video.contentLength { -// NSLog("removing duplicate video \(videoCheck.name) \(videoCheck.timeOfDay)") - isDuplicate = true - break - } - } // dupe check - - if isDuplicate == true { - continue - } - - filtered.append(video) - } - - loadedManifest = filtered - - // callbacks - for callback in self.callbacks { - callback(filtered) - } - self.callbacks.removeAll() - }*/ } diff --git a/Aerial/Source/Models/TimeManagement.swift b/Aerial/Source/Models/TimeManagement.swift index b63b1491..f2006ac9 100644 --- a/Aerial/Source/Models/TimeManagement.swift +++ b/Aerial/Source/Models/TimeManagement.swift @@ -37,7 +37,7 @@ class TimeManagement { else if preferences.timeMode == Preferences.TimeMode.nightShift.rawValue { let (isNSCapable, sunrise, sunset, _) = getNightShiftInformation() if (!isNSCapable) { - NSLog("Aerial : Trying to use Night Shift on a non capable Mac") + errorLog("Trying to use Night Shift on a non capable Mac") return (false,"") } @@ -49,11 +49,11 @@ class TimeManagement { dateFormatter.dateFormat = "HH:mm" guard let dateSunrise = dateFormatter.date(from: preferences.manualSunrise!) else { - NSLog("Aerial : Invalid sunrise time in preferences") + errorLog("Invalid sunrise time in preferences") return(false,"") } guard let dateSunset = dateFormatter.date(from: preferences.manualSunset!) else { - NSLog("Aerial : Invalid sunrise time in preferences") + errorLog("Invalid sunset time in preferences") return(false,"") } @@ -130,7 +130,6 @@ class TimeManagement { func isDarkModeEnabled() -> Bool { if #available(OSX 10.14, *) { let modeString = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") - return (modeString == "Dark") } else { @@ -218,7 +217,7 @@ class TimeManagement { } // /usr/bin/corebrightnessdiag nightshift-internal | grep nextSunset | cut -d \" -f2 - + warnLog("Location services may be disabled, Night Shift can't detect Sunrise and Sunset times without them") return (false,nil,nil,"Location services may be disabled") } @@ -239,5 +238,4 @@ class TimeManagement { return (output, task.terminationStatus) } - } diff --git a/Aerial/Source/Views/AerialView.swift b/Aerial/Source/Views/AerialView.swift index de95a8a5..e4848f03 100644 --- a/Aerial/Source/Views/AerialView.swift +++ b/Aerial/Source/Views/AerialView.swift @@ -29,7 +29,11 @@ class AerialView: ScreenSaverView { var currentVideo: AerialVideo? var observerWasSet = false - + var hasStartedPlaying = false + var wasStopped = false + var isDisabled = false + var timeObserver : Any? + static var shouldFade: Bool { let preferences = Preferences.sharedInstance return (preferences.fadeMode != Preferences.FadeMode.disabled.rawValue) @@ -69,10 +73,11 @@ class AerialView: ScreenSaverView { } static var sharedViews: [AerialView] = [] - // MARK: - Shared Player static var singlePlayerAlreadySetup: Bool = false + static var sharedPlayerIndex: Int? + class var sharedPlayer: AVPlayer { struct Static { static let instance: AVPlayer = AVPlayer() @@ -91,21 +96,23 @@ class AerialView: ScreenSaverView { } // MARK: - Init / Setup - + // This is the one used by System Preferences override init?(frame: NSRect, isPreview: Bool) { super.init(frame: frame, isPreview: isPreview) - + debugLog("avInit1") self.animationTimeInterval = 1.0 / 30.0 setup() } + // This is the one used by App required init?(coder: NSCoder) { super.init(coder: coder) + debugLog("avInit2") setup() } deinit { - debugLog("deinit AerialView") + debugLog("\(self.description) deinit AerialView") NotificationCenter.default.removeObserver(self) // set player item to nil if not preview player @@ -129,85 +136,39 @@ class AerialView: ScreenSaverView { AerialView.players.remove(at: index) } - func setupPlayerLayer(withPlayer player: AVPlayer) { - self.layer = CALayer() - guard let layer = self.layer else { - NSLog("Aerial Error: Couldn't create CALayer") - return - } - self.wantsLayer = true - layer.backgroundColor = NSColor.black.cgColor - layer.needsDisplayOnBoundsChange = true - layer.frame = self.bounds - - debugLog("setting up player layer with frame: \(self.bounds) / \(self.frame)") - - playerLayer = AVPlayerLayer(player: player) - if #available(OSX 10.10, *) { - playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill + func setup() { + debugLog("\(self.description) AerialView setup init") + if (AerialView.singlePlayerAlreadySetup) { + debugLog("\(AerialView.sharedViews[AerialView.sharedPlayerIndex!].wasStopped)") + // On previews, it's possible that our shared player was stopped and is not reusable + if AerialView.sharedViews[AerialView.sharedPlayerIndex!].wasStopped { + debugLog("Purging previous singlePlayer") + AerialView.singlePlayerAlreadySetup = false + AerialView.sharedPlayerIndex = nil + } } - playerLayer.autoresizingMask = [CAAutoresizingMask.layerWidthSizable, CAAutoresizingMask.layerHeightSizable] - playerLayer.frame = layer.bounds - playerLayer.contentsScale = NSScreen.main?.backingScaleFactor ?? 1.0 - - layer.addSublayer(playerLayer) - - textLayer = CATextLayer() -/* textLayer.frame = CGRect(x: 20, y: layer.bounds.height-60, width: layer.bounds.width-40, height: 40) - textLayer.font = NSFont(name: "Helvetica Neue Medium", size: 28) - if self.frame.height < 400 { - textLayer.fontSize = 12 // Seems needed despite line above - - } else { - textLayer.fontSize = 28 // Seems needed despite line above - }*/ - textLayer.string = "" - textLayer.opacity = 0 - // Add a bit of shadow to give an outline and better readability - textLayer.shadowRadius = 10 - textLayer.shadowOpacity = 1.0 - textLayer.shadowColor = CGColor.black - textLayer.contentsScale = NSScreen.main?.backingScaleFactor ?? 1.0 - layer.addSublayer(textLayer) - // Clock Layer - clockLayer = CATextLayer() - clockLayer.opacity = 0 - // Add a bit of shadow to give an outline and better readability - clockLayer.shadowRadius = 10 - clockLayer.shadowOpacity = 1.0 - clockLayer.contentsScale = NSScreen.main?.backingScaleFactor ?? 1.0 - layer.addSublayer(clockLayer) - - // Message Layer - messageLayer = CATextLayer() - messageLayer.opacity = 0 - // Add a bit of shadow to give an outline and better readability - messageLayer.shadowRadius = 10 - messageLayer.shadowOpacity = 1.0 - messageLayer.contentsScale = NSScreen.main?.backingScaleFactor ?? 1.0 - layer.addSublayer(messageLayer) - } - - func setup() { - NSLog("AerialMM : setup init") var localPlayer: AVPlayer? let notPreview = !isPreview + debugLog("\(self.description) isPreview : \(isPreview)") if notPreview { let preferences = Preferences.sharedInstance - + debugLog("\(self.description) singlePlayerAlreadySetup \(AerialView.singlePlayerAlreadySetup)") if (AerialView.singlePlayerAlreadySetup && preferences.multiMonitorMode == Preferences.MultiMonitorMode.mainOnly.rawValue) { + isDisabled = true return } // check if we should share preview's player - let noPlayers = (AerialView.players.count == 0) + //let noPlayers = (AerialView.players.count == 0) let previewPlayerExists = (AerialView.previewPlayer != nil) - if noPlayers && previewPlayerExists { + debugLog("\(self.description) nbPlayers \(AerialView.players.count) previewPlayerExists \(previewPlayerExists)") + /*if noPlayers && previewPlayerExists { + localPlayer = AerialView.previewPlayer - } + }*/ } else { AerialView.previewView = self } @@ -217,19 +178,22 @@ class AerialView: ScreenSaverView { } if localPlayer == nil { + debugLog("\(self.description) no local player") + if AerialView.sharingPlayers { - if AerialView.previewPlayer != nil { + /*if AerialView.previewPlayer != nil { localPlayer = AerialView.previewPlayer - } else { - localPlayer = AerialView.sharedPlayer - } + } else {*/ + + localPlayer = AerialView.sharedPlayer + //} } else { localPlayer = AVPlayer() } } guard let player = localPlayer else { - NSLog("Aerial Error: Couldn't create AVPlayer!") + errorLog("\(self.description) Couldn't create AVPlayer!") return } @@ -245,46 +209,175 @@ class AerialView: ScreenSaverView { setupPlayerLayer(withPlayer: player) if AerialView.sharingPlayers && AerialView.singlePlayerAlreadySetup { - self.playerLayer.player = AerialView.sharedViews[0].player + self.playerLayer.player = AerialView.sharedViews[AerialView.sharedPlayerIndex!].player self.playerLayer.opacity = 0 return } - - AerialView.singlePlayerAlreadySetup = true + + // We're NOT sharing the preview !!!!! + if !isPreview { + AerialView.singlePlayerAlreadySetup = true + AerialView.sharedPlayerIndex = AerialView.sharedViews.count-1 + } ManifestLoader.instance.addCallback { videos in self.playNextVideo() } } + override func viewDidChangeBackingProperties() { + debugLog("\(self.description) backing change \((self.window?.backingScaleFactor) ?? 1.0) isDisabled: \(isDisabled)") + if (!isDisabled) + { + self.layer!.contentsScale = (self.window?.backingScaleFactor) ?? 1.0 + self.playerLayer.contentsScale = (self.window?.backingScaleFactor) ?? 1.0 + self.textLayer.contentsScale = (self.window?.backingScaleFactor) ?? 1.0 + self.clockLayer.contentsScale = (self.window?.backingScaleFactor) ?? 1.0 + self.messageLayer.contentsScale = (self.window?.backingScaleFactor) ?? 1.0 + } + } + + func setupPlayerLayer(withPlayer player: AVPlayer) { + debugLog("\(self.description) setupPlayerLayer") + + self.layer = CALayer() + guard let layer = self.layer else { + errorLog("\(self.description) Couldn't create CALayer") + return + } + self.wantsLayer = true + layer.backgroundColor = NSColor.black.cgColor + layer.needsDisplayOnBoundsChange = true + layer.frame = self.bounds + + //self. + debugLog("\(self.description) setting up player layer with frame: \(self.bounds) / \(self.frame)") + + playerLayer = AVPlayerLayer(player: player) + if #available(OSX 10.10, *) { + playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill + } + playerLayer.autoresizingMask = [CAAutoresizingMask.layerWidthSizable, CAAutoresizingMask.layerHeightSizable] + playerLayer.frame = layer.bounds + //playerLayer.contentsScale = 1.0 // NSScreen.main?.backingScaleFactor ?? 1.0 + layer.addSublayer(playerLayer) + + textLayer = CATextLayer() + textLayer.frame = layer.bounds + textLayer.opacity = 0 + // Add a bit of shadow to give an outline and better readability + textLayer.shadowRadius = 10 + textLayer.shadowOpacity = 1.0 + textLayer.shadowColor = CGColor.black + //textLayer.contentsScale = 1.0 // NSScreen.main?.backingScaleFactor ?? 1.0 + layer.addSublayer(textLayer) + + // Clock Layer + clockLayer = CATextLayer() + clockLayer.opacity = 0 + // Add a bit of shadow to give an outline and better readability + clockLayer.shadowRadius = 10 + clockLayer.shadowOpacity = 1.0 + textLayer.shadowColor = CGColor.black + //clockLayer.contentsScale = 1.0 // NSScreen.main?.backingScaleFactor ?? 1.0 + layer.addSublayer(clockLayer) + + // Message Layer + messageLayer = CATextLayer() + messageLayer.opacity = 0 + // Add a bit of shadow to give an outline and better readability + messageLayer.shadowRadius = 10 + messageLayer.shadowOpacity = 1.0 + textLayer.shadowColor = CGColor.black + //messageLayer.contentsScale = 1.0 // NSScreen.main?.backingScaleFactor ?? 1.0 + layer.addSublayer(messageLayer) + } + + // MARK: - Lifecycle stuff +/* override func draw(_ rect: NSRect) { + }*/ + override func startAnimation() { + super.startAnimation() + debugLog("\(self.description) startAnimation") + + if !isDisabled{ + // Previews may be restarted, but our layer will get hidden (somehow) so show it back + if (isPreview && player?.currentTime() != CMTime.zero) { + playerLayer.opacity = 1 + player?.play() + } + + /*if player?.rate == 0 { + + }*/ + } + } + + override func stopAnimation() { + super.stopAnimation() + wasStopped = true + debugLog("\(self.description) stopAnimation") + if !isDisabled { + player?.pause() + } + } + // MARK: - AVPlayerItem Notifications @objc func playerItemFailedtoPlayToEnd(_ aNotification: Notification) { - NSLog("AVPlayerItemFailedToPlayToEndTimeNotification \(aNotification)") - + warnLog("\(self.description) AVPlayerItemFailedToPlayToEndTimeNotification \(aNotification)") playNextVideo() } @objc func playerItemNewErrorLogEntryNotification(_ aNotification: Notification) { - NSLog("AVPlayerItemNewErrorLogEntryNotification \(aNotification)") + warnLog("\(self.description) AVPlayerItemNewErrorLogEntryNotification \(aNotification)") } @objc func playerItemPlaybackStalledNotification(_ aNotification: Notification) { - NSLog("AVPlayerItemPlaybackStalledNotification \(aNotification)") + warnLog("\(self.description) AVPlayerItemPlaybackStalledNotification \(aNotification)") } @objc func playerItemDidReachEnd(_ aNotification: Notification) { - debugLog("played did reach end") - debugLog("notification: \(aNotification)") + debugLog("\(self.description) played did reach end") + debugLog("\(self.description) notification: \(aNotification)") playNextVideo() + debugLog("\(self.description) playing next video for player \(String(describing: player))") + } + + // Wait for the player to be ready + internal override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + debugLog("\(self.description) observeValue \(String(describing: keyPath))") + if self.playerLayer.isReadyForDisplay { + self.player!.play() + hasStartedPlaying = true - debugLog("playing next video for player \(String(describing: player))") + // All playerLayers should fade, we only have one shared player + if AerialView.sharingPlayers { + for view in AerialView.sharedViews { + self.addPlayerFades(player: self.player!, playerLayer: view.playerLayer, video: self.currentVideo!) + } + } else { + self.addPlayerFades(player: self.player!, playerLayer: self.playerLayer, video: self.currentVideo!) + } + + // Descriptions on main only for now + + self.addDescriptions(player: self.player!, video: self.currentVideo!) + } } + // MARK: - playNextVideo() func playNextVideo() { //let timeManagement = TimeManagement.sharedInstance let notificationCenter = NotificationCenter.default + // Clear everything + if (timeObserver != nil) { + self.player!.removeTimeObserver(timeObserver!) + } + self.textLayer.removeAllAnimations() + self.clockLayer.removeAllAnimations() + self.messageLayer.removeAllAnimations() // remove old entries notificationCenter.removeObserver(self) @@ -303,7 +396,7 @@ class AerialView: ScreenSaverView { AerialView.previewPlayer = player } - debugLog("Setting player for all player layers in \(AerialView.sharedViews)") + debugLog("\(self.description) Setting player for all player layers in \(AerialView.sharedViews)") for view in AerialView.sharedViews { view.playerLayer.player = player } @@ -323,7 +416,7 @@ class AerialView: ScreenSaverView { let randomVideo = ManifestLoader.instance.randomVideo(excluding: currentVideos) guard let video = randomVideo else { - NSLog("Aerial: Error grabbing random video!") + errorLog("\(self.description) Error grabbing random video!") return } self.currentVideo = video @@ -333,27 +426,27 @@ class AerialView: ScreenSaverView { if !video.isAvailableOffline { player.replaceCurrentItem(with: item) - debugLog("streaming video (not fully available offline) : \(video.url)") + debugLog("\(self.description) streaming video (not fully available offline) : \(video.url)") } else { let localurl = URL(fileURLWithPath: VideoCache.cachePath(forVideo: video)!) let localitem = AVPlayerItem(url: localurl) player.replaceCurrentItem(with: localitem) - debugLog("playing video (OFFLINE MODE) : \(localurl)") + debugLog("\(self.description) playing video (OFFLINE MODE) : \(localurl)") } - - if player.rate == 0 { +/* + // The first time we start from start animation ! + if hasStartedPlaying && player.rate == 0 { player.play() - //player.rate = 32.0 } - + */ guard let currentItem = player.currentItem else { - NSLog("Aerial Error: No current item!") + errorLog("\(self.description) No current item!") return } - debugLog("observing current item \(currentItem)") + debugLog("\(self.description) observing current item \(currentItem)") // Descriptions and fades are set when we begin playback if !observerWasSet { @@ -380,24 +473,8 @@ class AerialView: ScreenSaverView { player.actionAtItemEnd = AVPlayer.ActionAtItemEnd.none } - // Wait for the player to be ready - internal override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - if self.playerLayer.isReadyForDisplay { - // All playerLayers should fade, we only have one shared player - if AerialView.sharingPlayers { - for view in AerialView.sharedViews { - self.addPlayerFades(player: self.player!, playerLayer: view.playerLayer, video: self.currentVideo!) - } - } else { - self.addPlayerFades(player: self.player!, playerLayer: self.playerLayer, video: self.currentVideo!) - } - - // Descriptions on main only for now + // MARK: - Extra Animations - self.addDescriptions(player: self.player!, video: self.currentVideo!) - } - } - private func addPlayerFades(player: AVPlayer, playerLayer: AVPlayerLayer, video: AerialVideo) { // We only fade in/out if we have duration @@ -423,14 +500,13 @@ class AerialView: ScreenSaverView { if (preferences.showDescriptions) { // Preventively, make sure we have poi as tvOS11/10 videos won't have them - if video.poi.count > 0 && poiStringProvider.loadedDescriptions + if (video.poi.count > 0 && poiStringProvider.loadedDescriptions) || (preferences.useCommunityDescriptions && video.communityPoi.count > 0) { // Collect all the timestamps from the JSON var times = [NSValue]() + let keys = poiStringProvider.getPoiKeys(video: video) - for pkv in video.poi - { - //print("time \(pkv.key) \(poiStringProvider.getString(key: video.poi[pkv.key]!))") + for pkv in keys { let timeStamp = Double(pkv.key)! times.append(NSValue(time: CMTime(seconds: timeStamp, preferredTimescale: 1))) } @@ -438,8 +514,8 @@ class AerialView: ScreenSaverView { times.sort(by: { ($0 as! CMTime).seconds < ($1 as! CMTime).seconds } ) // Animate the very first one on it's own - let str = poiStringProvider.getString(key: video.poi["0"]!) - + let str = poiStringProvider.getString(key: keys["0"]!, video: video) + var fadeAnimation:CAKeyframeAnimation if (preferences.showDescriptionsMode == Preferences.DescriptionMode.fade10seconds.rawValue) @@ -466,12 +542,17 @@ class AerialView: ScreenSaverView { } self.textLayer.add(fadeAnimation, forKey: "textfade") - setupTextLayer(string: str, duration: fadeAnimation.duration) + if (video.duration > 0) { + setupTextLayer(string: str, duration: fadeAnimation.duration, isInitial: true, totalDuration: video.duration - 1) + } else { + setupTextLayer(string: str, duration: fadeAnimation.duration, isInitial: true, totalDuration: 807) + } let mainQueue = DispatchQueue.main + // We then callback for each timestamp - player.addBoundaryTimeObserver(forTimes: times, queue: mainQueue) { + timeObserver = player.addBoundaryTimeObserver(forTimes: times, queue: mainQueue) { var isLastTimeStamp = true var intervalUntilNextTimeStamp = 0.0 @@ -521,16 +602,21 @@ class AerialView: ScreenSaverView { } // Get the string for the current timestamp let key = String(format: "%.0f",closestTime) - let str = poiStringProvider.getString(key: video.poi[key]!) - self.setupTextLayer(string: str, duration: fadeAnimation.duration) + let str = poiStringProvider.getString(key: keys[key]!, video: video) + self.setupTextLayer(string: str, duration: fadeAnimation.duration, isInitial: false, totalDuration: video.duration-1) self.textLayer.add(fadeAnimation, forKey: "textfade") } } else { - // We don't have any extended description, using video name (City) - let str = video.name + // We don't have any extended description, using Secondary name (location) or video name (City) + let str: String + if (video.secondaryName != "") { + str = video.secondaryName + } else { + str = video.name + } var fadeAnimation:CAKeyframeAnimation if (preferences.showDescriptionsMode == Preferences.DescriptionMode.fade10seconds.rawValue) @@ -551,25 +637,15 @@ class AerialView: ScreenSaverView { } } self.textLayer.add(fadeAnimation, forKey: "textfade") - setupTextLayer(string: str, duration : fadeAnimation.duration) + setupTextLayer(string: str, duration : fadeAnimation.duration, isInitial: false, totalDuration: video.duration) } } } - // Create a Fade In/Out animation - func createFadeInOutAnimation(duration: Double) -> CAKeyframeAnimation { - let fadeAnimation = CAKeyframeAnimation(keyPath: "opacity") - fadeAnimation.values = [0, 0, 1, 1, 0] as [NSNumber] - fadeAnimation.keyTimes = [0, Double( 1/duration ), Double( (1+AerialView.textFadeDuration)/duration ), Double( 1-AerialView.textFadeDuration/duration ), 1] as [NSNumber] - fadeAnimation.duration = duration - - return fadeAnimation - } - - func setupTextLayer(string:String, duration: CFTimeInterval) { + func setupTextLayer(string:String, duration: CFTimeInterval, isInitial: Bool, totalDuration: Double) { // Setup string self.textLayer.string = string - + self.textLayer.isWrapped = true let preferences = Preferences.sharedInstance // We override font size on previews @@ -587,13 +663,16 @@ class AerialView: ScreenSaverView { // Make sure we change the layer font/size self.textLayer.font = font self.textLayer.fontSize = fontSize - + let attributes: [NSAttributedString.Key : Any] = [NSAttributedString.Key.font : font as Any] // Calculate bounding box let s = NSAttributedString(string: string, attributes: attributes) - let rect = s.boundingRect(with: layer!.visibleRect.size, options: NSString.DrawingOptions.usesLineFragmentOrigin) - + + var rect = s.boundingRect(with: layer!.visibleRect.size, options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin]) + // Last line won't appear if we don't adjust + rect = CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.width, height: rect.height+10) + // Rebind frame self.textLayer.frame = rect @@ -607,65 +686,78 @@ class AerialView: ScreenSaverView { lastCorner = corner repositionTextLayer(position: corner) - setupAndRepositionExtra(position: corner, duration: duration) + setupAndRepositionExtra(position: corner, duration: duration, isInitial: isInitial, totalDuration: totalDuration) } else { repositionTextLayer(position: preferences.descriptionCorner!) // Or set position from pref - setupAndRepositionExtra(position: preferences.descriptionCorner!, duration: duration) + setupAndRepositionExtra(position: preferences.descriptionCorner!, duration: duration, isInitial: isInitial, totalDuration: totalDuration) } } - private func setupAndRepositionExtra(position: Int, duration: CFTimeInterval) + private func setupAndRepositionExtra(position: Int, duration: CFTimeInterval, isInitial: Bool, totalDuration: Double) { let preferences = Preferences.sharedInstance if (preferences.showClock) { - if (clockTimer == nil) - { - if #available(OSX 10.12, *) { - clockTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (Timer) in - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "j:mm:ss", options: 0, locale: Locale.current) - let dateString = dateFormatter.string(from: Date()) - self.clockLayer.string = dateString - }) + if (isInitial) { + if (clockTimer == nil) + { + if #available(OSX 10.12, *) { + clockTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (Timer) in + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "j:mm:ss", options: 0, locale: Locale.current) + let dateString = dateFormatter.string(from: Date()) + self.clockLayer.string = dateString + }) + } + } - } - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "j:mm:ss", options: 0, locale: Locale.current) - let dateString = dateFormatter.string(from: Date()) - - self.clockLayer.string = dateString - - let preferences = Preferences.sharedInstance - - // We override font size on previews - var fontSize = CGFloat(preferences.extraFontSize!) - if (layer!.bounds.height < 200) { - fontSize = 12 + + let dateFormatter = DateFormatter() + if (preferences.withSeconds) { + dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "j:mm:ss", options: 0, locale: Locale.current) + } else { + dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "j:mm", options: 0, locale: Locale.current) + } + let dateString = dateFormatter.string(from: Date()) + + self.clockLayer.string = dateString + + // We override font size on previews + var fontSize = CGFloat(preferences.extraFontSize!) + if (layer!.bounds.height < 200) { + fontSize = 12 + } + + // Get font with a fallback in case + var font = NSFont(name: "Monaco", size: 28) + if let tryFont = NSFont(name: preferences.extraFontName!,size: fontSize) { + font = tryFont + } + + // Make sure we change the layer font/size + self.clockLayer.font = font + self.clockLayer.fontSize = fontSize + + let attributes: [NSAttributedString.Key : Any] = [NSAttributedString.Key.font : font as Any] + + // Calculate bounding box + let s = NSAttributedString(string: dateString, attributes: attributes) + let rect = s.boundingRect(with: layer!.visibleRect.size, options: NSString.DrawingOptions.usesLineFragmentOrigin) + + // Rebind frame + self.clockLayer.frame = rect + //clockLayer.anchorPoint = CGPoint(x: 0, y:0) + //clockLayer.position = CGPoint(x:10 ,y:10+textLayer.visibleRect.height) + //clockLayer.opacity = 1.0 } - // Get font with a fallback in case - var font = NSFont(name: "Helvetica Neue Medium", size: 28) - if let tryFont = NSFont(name: preferences.extraFontName!,size: fontSize) { - font = tryFont + if (preferences.descriptionCorner == Preferences.DescriptionCorner.random.rawValue) { + clockLayer.add(createFadeInOutAnimation(duration: duration), forKey: "textfade") + } else if isInitial && preferences.showDescriptionsMode == Preferences.DescriptionMode.always.rawValue { + clockLayer.add(createFadeInOutAnimation(duration: totalDuration), forKey: "textfade") + } else if preferences.showDescriptionsMode == Preferences.DescriptionMode.fade10seconds.rawValue { + clockLayer.add(createFadeInOutAnimation(duration: duration), forKey: "textfade") } - - // Make sure we change the layer font/size - self.clockLayer.font = font - self.clockLayer.fontSize = fontSize - - let attributes: [NSAttributedString.Key : Any] = [NSAttributedString.Key.font : font as Any] - - // Calculate bounding box - let s = NSAttributedString(string: dateString, attributes: attributes) - let rect = s.boundingRect(with: layer!.visibleRect.size, options: NSString.DrawingOptions.usesLineFragmentOrigin) - - // Rebind frame - self.clockLayer.frame = rect - //clockLayer.anchorPoint = CGPoint(x: 0, y:0) - //clockLayer.position = CGPoint(x:10 ,y:10+textLayer.visibleRect.height) - //clockLayer.opacity = 1.0 - clockLayer.add(createFadeInOutAnimation(duration: duration), forKey: "textfade") } if (preferences.showMessage && preferences.showMessageString != "") { @@ -698,18 +790,65 @@ class AerialView: ScreenSaverView { //messageLayer.anchorPoint = CGPoint(x: 0, y:0) //messageLayer.position = CGPoint(x:10 ,y:10+textLayer.visibleRect.height) //messageLayer.opacity = 1.0 - self.messageLayer.add(createFadeInOutAnimation(duration: duration), forKey: "textfade") + if (preferences.descriptionCorner == Preferences.DescriptionCorner.random.rawValue) { + self.messageLayer.add(createFadeInOutAnimation(duration: duration), forKey: "textfade") + } else if isInitial && preferences.showDescriptionsMode == Preferences.DescriptionMode.always.rawValue { + self.messageLayer.add(createFadeInOutAnimation(duration: totalDuration), forKey: "textfade") + } else if preferences.showDescriptionsMode == Preferences.DescriptionMode.fade10seconds.rawValue { + self.messageLayer.add(createFadeInOutAnimation(duration: duration), forKey: "textfade") + } + + } - if preferences.extraCorner == Preferences.ExtraCorner.same.rawValue{ - repositionClockAndMessageLayer(position: position, alone: false) - } else if preferences.extraCorner == Preferences.ExtraCorner.hOpposed.rawValue{ - repositionClockAndMessageLayer(position: (position+2)%4, alone: true) - } else if preferences.extraCorner == Preferences.ExtraCorner.dOpposed.rawValue{ - repositionClockAndMessageLayer(position: 3-position, alone: true) + if (!isInitial && preferences.extraCorner == Preferences.ExtraCorner.same.rawValue && preferences.showDescriptionsMode == Preferences.DescriptionMode.always.rawValue && preferences.descriptionCorner != Preferences.DescriptionCorner.random.rawValue) { + animateClockAndMessageLayer(position: position) + } else { + if preferences.extraCorner == Preferences.ExtraCorner.same.rawValue{ + repositionClockAndMessageLayer(position: position, alone: false) + } else if preferences.extraCorner == Preferences.ExtraCorner.hOpposed.rawValue{ + repositionClockAndMessageLayer(position: (position+2)%4, alone: true) + } else if preferences.extraCorner == Preferences.ExtraCorner.dOpposed.rawValue{ + repositionClockAndMessageLayer(position: 3-position, alone: true) + } } } + private func animateClockAndMessageLayer(position: Int) { + var clockDecal : CGFloat = 0 + var messageDecal : CGFloat = 0 + let preferences = Preferences.sharedInstance + + clockDecal += textLayer.visibleRect.height + messageDecal += textLayer.visibleRect.height + + if preferences.showMessage { + clockDecal += messageLayer.visibleRect.height + } + let duration = 1 + AerialView.textFadeDuration + + var cto, mto : CGPoint + if (position == Preferences.DescriptionCorner.topLeft.rawValue) { + cto = CGPoint(x: 10, y: layer!.bounds.height-10-clockDecal) + mto = CGPoint(x: 10, y: layer!.bounds.height-10-messageDecal) + } else if (position == Preferences.DescriptionCorner.bottomLeft.rawValue) { + cto = CGPoint(x: 10, y: 10+clockDecal) + mto = CGPoint(x: 10, y: 10+messageDecal) + } else if (position == Preferences.DescriptionCorner.topRight.rawValue) { + cto = CGPoint(x: layer!.bounds.width-10, y: layer!.bounds.height-10-clockDecal) + mto = CGPoint(x: layer!.bounds.width-10, y: layer!.bounds.height-10-messageDecal) + } else { + cto = CGPoint(x: layer!.bounds.width-10, y: 10+clockDecal) + mto = CGPoint(x: layer!.bounds.width-10, y: 10+messageDecal) + } + + self.clockLayer.add(createMoveAnimation(layer: clockLayer, to: cto, duration: duration), forKey: "position") + self.messageLayer.add(createMoveAnimation(layer: messageLayer, to: mto, duration: duration), forKey: "position") + } + + + + private func repositionClockAndMessageLayer(position:Int, alone:Bool) { var clockDecal : CGFloat = 0 var messageDecal : CGFloat = 0 @@ -763,6 +902,25 @@ class AerialView: ScreenSaverView { } } + // Create a Fade In/Out animation + func createFadeInOutAnimation(duration: Double) -> CAKeyframeAnimation { + let fadeAnimation = CAKeyframeAnimation(keyPath: "opacity") + fadeAnimation.values = [0, 0, 1, 1, 0] as [NSNumber] + fadeAnimation.keyTimes = [0, Double( 1/duration ), Double( (1+AerialView.textFadeDuration)/duration ), Double( 1-AerialView.textFadeDuration/duration ), 1] as [NSNumber] + fadeAnimation.duration = duration + + return fadeAnimation + } + + func createMoveAnimation(layer : CALayer, to: CGPoint, duration: Double) -> CABasicAnimation { + let moveAnimation = CABasicAnimation(keyPath: "position") + moveAnimation.fromValue = layer.position + moveAnimation.toValue = to + moveAnimation.duration = duration + layer.position = to; + return moveAnimation + } + // MARK: - Preferences override var hasConfigureSheet: Bool { diff --git a/Aerial/Source/Views/CheckCellView.swift b/Aerial/Source/Views/CheckCellView.swift index 4de14c87..1560fdc3 100644 --- a/Aerial/Source/Views/CheckCellView.swift +++ b/Aerial/Source/Views/CheckCellView.swift @@ -103,12 +103,21 @@ class CheckCellView: NSTableCellView { queuedImage.isHidden = true status = .downloaded - NSLog("video download finished") + debugLog("Video download finished") video!.updateDuration() } + func markAsNotDownloaded() { + addButton.isHidden = false + progressIndicator.isHidden = true + queuedImage.isHidden = true + status = .notAvailable + + debugLog("Video download finished with error/cancel") + } + func markAsQueued() { - debugLog("queued \(video!)") + debugLog("Queued \(video!)") status = .queued addButton.isHidden = true progressIndicator.isHidden = true