Repository: JohnCoates/Aerial Branch: master Commit: 43daa78e8f71 Files: 225 Total size: 2.3 MB Directory structure: gitextract_s15f9pm6/ ├── .codeclimate.yml ├── .gitignore ├── .gitmodules ├── .swiftlint.yml ├── .travis.yml ├── Aerial/ │ ├── App/ │ │ ├── AppDelegate.swift │ │ └── Resources/ │ │ ├── Assets.xcassets/ │ │ │ ├── Accent Color.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ └── FirstPanelBackground.colorset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ └── MainMenu.xib │ │ └── Info.plist │ └── Source/ │ ├── Controllers/ │ │ └── CustomVideoController.swift │ ├── Header.h │ ├── Models/ │ │ ├── API/ │ │ │ ├── Forecast.swift │ │ │ ├── GeoCoding.swift │ │ │ ├── OneCall.swift │ │ │ └── OpenWeather.swift │ │ ├── Aerial.swift │ │ ├── AerialVideo.swift │ │ ├── Cache/ │ │ │ ├── AssetLoaderDelegate.swift │ │ │ ├── Cache.swift │ │ │ ├── PoiStringProvider.swift │ │ │ ├── Thumbnails.swift │ │ │ ├── TimeMachine.swift │ │ │ ├── VideoCache.swift │ │ │ ├── VideoDownload.swift │ │ │ ├── VideoLoader.swift │ │ │ └── VideoManager.swift │ │ ├── CompanionBridge.swift │ │ ├── CustomVideoFolders+helpers.swift │ │ ├── CustomVideoFolders.swift │ │ ├── Downloads/ │ │ │ ├── AsynchronousOperation.swift │ │ │ ├── DownloadManager.swift │ │ │ └── FileHelpers.swift │ │ ├── ErrorLog.swift │ │ ├── Extensions/ │ │ │ ├── AVAsset+VideoOrientation.swift │ │ │ ├── AVPlayerItem+vibrance.swift │ │ │ ├── AVPlayerViewExtension.swift │ │ │ ├── DispatchQueue+Extension.swift │ │ │ ├── NSButton+icons.swift │ │ │ ├── NSImage+trim.swift │ │ │ └── NSMenuItem+icons.swift │ │ ├── Hardware/ │ │ │ ├── Battery.swift │ │ │ ├── Brightness.swift │ │ │ ├── DarkMode.swift │ │ │ ├── DisplayDetection.swift │ │ │ ├── HardwareDetection.swift │ │ │ ├── ISSoundAdditions/ │ │ │ │ ├── Sound.swift │ │ │ │ ├── SoundOutputManager+Goodies.swift │ │ │ │ ├── SoundOutputManager+Properties.swift │ │ │ │ └── SoundOutputManager.swift │ │ │ └── NightShift.swift │ │ ├── Locations.swift │ │ ├── ManifestLoader.swift │ │ ├── Music/ │ │ │ └── Music.swift │ │ ├── PlaybackSpeed.swift │ │ ├── Prefs/ │ │ │ ├── PrefsAdvanced.swift │ │ │ ├── PrefsCache.swift │ │ │ ├── PrefsDisplays.swift │ │ │ ├── PrefsInfo.swift │ │ │ ├── PrefsTime.swift │ │ │ ├── PrefsUpdates.swift │ │ │ └── PrefsVideos.swift │ │ ├── SeededGenerator.swift │ │ ├── Sources/ │ │ │ ├── Sidebar.swift │ │ │ ├── Source.swift │ │ │ ├── SourceInfo.swift │ │ │ ├── SourceList.swift │ │ │ └── VideoList.swift │ │ └── Time/ │ │ ├── Aerial-Bridging-Header.h │ │ ├── IOBridge.m │ │ ├── Solar.swift │ │ └── TimeManagement.swift │ └── Views/ │ ├── AerialPlayerItem.swift │ ├── AerialView+Brightness.swift │ ├── AerialView+Player.swift │ ├── AerialView.swift │ ├── Layers/ │ │ ├── AnimatableLayer.swift │ │ ├── AnimationLayer.swift │ │ ├── AnimationTextLayer.swift │ │ ├── BatteryIconLayer.swift │ │ ├── ClockLayer.swift │ │ ├── CountdownLayer.swift │ │ ├── DateLayer.swift │ │ ├── DownloadIndicatorLayer.swift │ │ ├── LayerManager.swift │ │ ├── LayerOffsets.swift │ │ ├── LocationLayer.swift │ │ ├── MessageLayer.swift │ │ ├── Music/ │ │ │ ├── ArtworkLayer.swift │ │ │ └── MusicLayer.swift │ │ ├── TimerLayer.swift │ │ └── Weather/ │ │ ├── ConditionLayer.swift │ │ ├── ConditionSymbolLayer.swift │ │ ├── ForecastLayer.swift │ │ ├── WeatherLayer.swift │ │ ├── WindDirectionLayer.swift │ │ └── YahooLogoLayer.swift │ ├── MainUI/ │ │ ├── AspectFillNSImageView.swift │ │ ├── NowPlayingCollectionView.swift │ │ ├── ShadowTextFieldCell.swift │ │ ├── SidebarOutlineView.swift │ │ └── VideoCellView.swift │ ├── PrefPanel/ │ │ ├── CheckCellView.swift │ │ ├── DisplayView.swift │ │ ├── InfoBatteryView.swift │ │ ├── InfoClockView.swift │ │ ├── InfoCommonView.swift │ │ ├── InfoContainerView.swift │ │ ├── InfoCountdownView.swift │ │ ├── InfoDateView.swift │ │ ├── InfoLocationView.swift │ │ ├── InfoMessageView.swift │ │ ├── InfoMusicView.swift │ │ ├── InfoSettingsTableSource.swift │ │ ├── InfoSettingsView.swift │ │ ├── InfoTableSource.swift │ │ ├── InfoTimerView.swift │ │ ├── InfoWeatherView.swift │ │ ├── VideoHeaderView.swift │ │ └── VideoViewItem.swift │ └── Sources/ │ ├── ActionCellView.swift │ ├── CheckboxCellView.swift │ ├── DescriptionCellView.swift │ └── SourceOutlineView.swift ├── Aerial copy-Info.plist ├── Aerial.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata/ │ └── xcschemes/ │ └── Aerial.xcscheme ├── AerialApp copy-Info.plist ├── Documentation/ │ ├── AutoUpdates.md │ ├── ChangeLog.md │ ├── Contribute.md │ ├── CustomVideos.md │ ├── FAQs.md │ ├── HardwareDecoding.md │ ├── Installation.md │ ├── MoreVideos.md │ ├── OfflineMode.md │ ├── README.md │ └── Troubleshooting.md ├── LICENSE ├── Makefile ├── Podfile ├── Readme.md ├── Resources/ │ ├── Community/ │ │ ├── Readme.md │ │ ├── ar.json │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fr.json │ │ ├── he.json │ │ ├── hu.json │ │ ├── it.json │ │ ├── ja.json │ │ ├── ko.json │ │ ├── missingvideos.json │ │ ├── nl.json │ │ ├── pl.json │ │ ├── pt.json │ │ ├── pt_BR.json │ │ ├── ru.json │ │ ├── sv.json │ │ ├── tl.json │ │ ├── zh_CN.json │ │ └── zh_TW.json │ ├── MainUI/ │ │ ├── First time setup/ │ │ │ ├── CacheSetupViewController.swift │ │ │ ├── CacheSetupViewController.xib │ │ │ ├── FirstSetupWindowController.swift │ │ │ ├── FirstSetupWindowController.xib │ │ │ ├── NextViewController.swift │ │ │ ├── NextViewController.xib │ │ │ ├── RecapViewController.swift │ │ │ ├── RecapViewController.xib │ │ │ ├── TimeSetupViewController.swift │ │ │ ├── TimeSetupViewController.xib │ │ │ ├── VideoFormatViewController.swift │ │ │ ├── VideoFormatViewController.xib │ │ │ ├── WelcomeViewController.swift │ │ │ └── WelcomeViewController.xib │ │ ├── Infos panels/ │ │ │ ├── CreditsViewController.swift │ │ │ ├── CreditsViewController.xib │ │ │ ├── HelpViewController.swift │ │ │ ├── HelpViewController.xib │ │ │ ├── InfoViewController.swift │ │ │ └── InfoViewController.xib │ │ ├── PanelWindowController.swift │ │ ├── PanelWindowController.xib │ │ ├── Settings panels/ │ │ │ ├── AdvancedViewController.swift │ │ │ ├── AdvancedViewController.xib │ │ │ ├── BrightnessViewController.swift │ │ │ ├── BrightnessViewController.xib │ │ │ ├── CacheViewController.swift │ │ │ ├── CacheViewController.xib │ │ │ ├── Collection View/ │ │ │ │ ├── PlayingCollectionViewItem.swift │ │ │ │ └── PlayingCollectionViewItem.xib │ │ │ ├── CompanionCacheViewController.swift │ │ │ ├── CompanionCacheViewController.xib │ │ │ ├── DisplaysViewController.swift │ │ │ ├── DisplaysViewController.xib │ │ │ ├── FiltersViewController.swift │ │ │ ├── FiltersViewController.xib │ │ │ ├── NowPlayingViewController.swift │ │ │ ├── NowPlayingViewController.xib │ │ │ ├── OverlaysViewController.swift │ │ │ ├── OverlaysViewController.xib │ │ │ ├── SourcesViewController.swift │ │ │ ├── SourcesViewController.xib │ │ │ ├── TimeViewController.swift │ │ │ └── TimeViewController.xib │ │ ├── SidebarViewController.swift │ │ ├── SidebarViewController.xib │ │ ├── VideosViewController.swift │ │ └── VideosViewController.xib │ └── Old stuff/ │ ├── CustomVideos.xib │ └── Info.plist ├── Tests/ │ ├── Info.plist │ └── PreferencesTests.swift ├── appcast.xml ├── beta-appcast.xml ├── issue_template.md └── lokalise.example.cfg ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codeclimate.yml ================================================ engines: tailor: enabled: true ratings: paths: - "**.swift" exclude_paths: [] ================================================ FILE: .gitignore ================================================ lokalise.cfg .DS_Store xcuserdata/ compile/ build/ DerivedData/ *.xccheckout release/ debug.plist Examples/debug.html Aerial/Source/Models/API/APISecrets.swift ================================================ FILE: .gitmodules ================================================ [submodule "Extern/OAuthSwift"] path = Extern/OAuthSwift url = https://github.com/OAuthSwift/OAuthSwift.git branch = 2.0.0 ignore = dirty ================================================ FILE: .swiftlint.yml ================================================ disabled_rules: # Allow force-casting (e.g. `x as! UICollectionViewCell`). # We may want to re-enable and address this rule. - force_cast # Allow `TODO` and `FIXME` comments. - todo # Allow the use of `let _ = ` - unused_optional_binding # Allow the use of parantheses when calling methods with trailing completion closures - empty_parentheses_with_trailing_closure # We use enum "namespaces" which leads to nesting violations - nesting # Re-evalature to shorten functions up - function_body_length # Allow declaring operators without extra whitespace, like so: `func ==(_ lhs, ...)` - operator_whitespace - redundant_string_enum_value - inclusive_language excluded: - Extern opt_in_rules: # Prefer checking `isEmpty` over `count > 0` - empty_count file_length: warning: 1000 error: 2000 line_length: 250 identifier_name: min_length: warning: 2 ================================================ FILE: .travis.yml ================================================ language: objective-c osx_image: xcode11.2 before_install: - pod repo update after_success: - bash <(curl -s https://codecov.io/bash) -J 'AerialApp' before_script: - make lint script: - make test-travis ================================================ FILE: Aerial/App/AppDelegate.swift ================================================ // // AppDelegate.swift // Aerial Test // // Created by John Coates on 10/23/15. // Copyright © 2015 John Coates. All rights reserved. // import Cocoa @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { // So this is where we come in, when compiled as an Application override init() { super.init() // First thing : let our model know we are an app and not a screensaver ! Aerial.helper.appMode = true let panelWindowController = PanelWindowController() panelWindowController.showWindow(self) panelWindowController.window?.makeKeyAndOrderFront(nil) } } ================================================ FILE: Aerial/App/Resources/Assets.xcassets/Accent Color.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0.517", "green" : "0.585", "red" : "0.176" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0.726", "green" : "0.800", "red" : "0.354" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Aerial/App/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "filename" : "icon-color-1-1024x1024-transparent.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Aerial/App/Resources/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Aerial/App/Resources/Assets.xcassets/FirstPanelBackground.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "1.000", "green" : "1.000", "red" : "1.000" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "37", "green" : "35", "red" : "33" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Aerial/App/Resources/Base.lproj/MainMenu.xib ================================================ ================================================ FILE: Aerial/App/Resources/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSAppTransportSecurity NSAllowsArbitraryLoads NSHumanReadableCopyright Copyright © 2015 John Coates. All rights reserved. NSLocationAlwaysUsageDescription Aerial uses location services to calculate Sunset and Sunrise times from your position NSLocationWhenInUseUsageDescription Aerial uses location services to calculate Sunset and Sunrise times from your position NSMainNibFile MainMenu NSPrincipalClass NSApplication SUFeedURL https://raw.githubusercontent.com/JohnCoates/Aerial/master/appcast.xml SUPublicEDKey fbiQGEFq55xl4bjwj2/SpIO4JMsKmEyhHEWlMMueyDY= ================================================ FILE: Aerial/Source/Controllers/CustomVideoController.swift ================================================ // // CustomVideoController.swift // Aerial // // Created by Guillaume Louel on 21/05/2019. // Copyright © 2019 John Coates. All rights reserved. // import Foundation import AppKit import AVKit class CustomVideoController: NSWindowController, NSWindowDelegate, NSDraggingDestination { @IBOutlet var mainPanel: NSWindow! // This is the panel workaround for Catalina @IBOutlet var addFolderCatalinaPanel: NSPanel! @IBOutlet var addFolderTextField: NSTextField! @IBOutlet var folderOutlineView: NSOutlineView! @IBOutlet var topPathControl: NSPathControl! @IBOutlet var folderView: NSView! @IBOutlet var fileView: NSView! @IBOutlet var onboardingLabel: NSTextField! @IBOutlet var folderShortNameTextField: NSTextField! @IBOutlet var timePopUpButton: NSPopUpButton! @IBOutlet var editPlayerView: AVPlayerView! @IBOutlet var videoNameTextField: NSTextField! @IBOutlet var poiTableView: NSTableView! @IBOutlet var addPoi: NSButton! @IBOutlet var removePoi: NSButton! @IBOutlet var addPoiPopover: NSPopover! @IBOutlet var timeTextField: NSTextField! @IBOutlet var timeTextStepper: NSStepper! @IBOutlet var timeTextFormatter: NumberFormatter! @IBOutlet var descriptionTextField: NSTextField! @IBOutlet var durationLabel: NSTextField! @IBOutlet var resolutionLabel: NSTextField! @IBOutlet var cvcMenu: NSMenu! @IBOutlet var menuRemoveFolderAndVideos: NSMenuItem! @IBOutlet var menuRemoveVideo: NSMenuItem! var currentFolder: Folder? var currentAsset: Asset? var currentAssetDuration: Int? var hasAwokenAlready = false var sw: NSWindow? var controller: SourcesViewController? // MARK: - Lifecycle required init?(coder: NSCoder) { super.init(coder: coder) debugLog("cvcinit") } override init(window: NSWindow?) { super.init(window: window) self.sw = window debugLog("cvcinit2") } override func awakeFromNib() { if !hasAwokenAlready { debugLog("cvcawake") // self.menu = cvcMenu folderOutlineView.dataSource = self folderOutlineView.delegate = self folderOutlineView.menu = cvcMenu cvcMenu.delegate = self if #available(OSX 10.13, *) { folderOutlineView.registerForDraggedTypes([.fileURL, .URL]) } else { // Fallback on earlier versions } poiTableView.dataSource = self poiTableView.delegate = self hasAwokenAlready = true editPlayerView.player = AVPlayer() NotificationCenter.default.addObserver( self, selector: #selector(self.windowWillClose(_:)), name: NSWindow.willCloseNotification, object: nil) } } // We will receive this notification for every panel/window so we need to ensure it's the correct one func windowWillClose(_ notification: Notification) { if let wobj = notification.object as? NSPanel { if wobj.title == "Manage Custom Videos" { debugLog("Closing cvc") // TODO 2.0 /* let manifestInstance = ManifestLoader.instance manifestInstance.saveCustomVideos() manifestInstance.addCallback { manifestVideos in if let contr = self.controller { contr.loaded(manifestVideos: []) } } manifestInstance.loadManifestsFromLoadedFiles() */ } } } // This is the public function to make this visible func show(sender: NSButton, controller: SourcesViewController) { self.controller = controller if !mainPanel.isVisible { mainPanel.makeKeyAndOrderFront(sender) folderOutlineView.expandItem(nil, expandChildren: true) folderOutlineView.deselectAll(self) folderView.isHidden = true fileView.isHidden = true topPathControl.isHidden = true } } // MARK: - Edit Folders @IBAction func folderNameChange(_ sender: NSTextField) { if let folder = currentFolder { folder.label = sender.stringValue folderOutlineView.reloadData() } } // MARK: - Add a new folder of videos to parse @IBAction func addFolderButton(_ sender: NSButton) { debugLog("addFolder") if #available(OSX 10.15, *) { // On Catalina, we can't use NSOpenPanel right now addFolderTextField.stringValue = "" addFolderCatalinaPanel.makeKeyAndOrderFront(self) } else { let addFolderPanel = NSOpenPanel() addFolderPanel.allowsMultipleSelection = false addFolderPanel.canChooseDirectories = true addFolderPanel.canCreateDirectories = false addFolderPanel.canChooseFiles = false addFolderPanel.title = "Select a folder containing videos" addFolderPanel.begin { (response) in if response.rawValue == NSFileHandlingPanelOKButton { self.processPathForVideos(url: addFolderPanel.url!) } addFolderPanel.close() } } } @IBAction func addFolderCatalinaConfirm(_ sender: Any) { let strFolder = addFolderTextField.stringValue if FileManager.default.fileExists(atPath: strFolder as String) { self.processPathForVideos(url: URL(fileURLWithPath: strFolder, isDirectory: true)) } addFolderCatalinaPanel.close() } func processPathForVideos(url: URL) { debugLog("processing url for videos : \(url) ") let folderName = url.lastPathComponent // let manifestInstance = ManifestLoader.instance do { let urls = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) var assets = [VideoAsset]() for lurl in urls { if lurl.path.lowercased().hasSuffix(".mp4") || lurl.path.lowercased().hasSuffix(".mov") { assets.append(VideoAsset(accessibilityLabel: folderName, id: NSUUID().uuidString, title: lurl.lastPathComponent, timeOfDay: "day", scene: "", pointsOfInterest: [:], url4KHDR: "", url4KSDR: lurl.path, url1080H264: "", url1080HDR: "", url4KSDR120FPS: "", url4KSDR240FPS: "", url1080SDR: "", url: "", type: "nature")) } } // ... if SourceList.hasNamed(name: url.lastPathComponent) { Aerial.helper.showInfoAlert(title: "Source name mismatch", text: "A source with this name already exists. Try renaming your folder and try again.") } else { debugLog("Creating source \(url.lastPathComponent)") // Generate and save the Source let source = Source(name: url.lastPathComponent, description: "Local files from \(url.path)", manifestUrl: "manifest.json", type: .local, scenes: [.nature], isCachable: false, license: "", more: "") SourceList.saveSource(source) // Then the entries let videoManifest = VideoManifest(assets: assets, initialAssetCount: 1, version: 1) SourceList.saveEntries(source: source, manifest: videoManifest) } /* if let cvf = manifestInstance.customVideoFolders { // check if we have this folder already ? if !cvf.hasFolder(withUrl: url.path) && !assets.isEmpty { cvf.folders.append(Folder(url: url.path, label: folderName, assets: assets)) } else if !assets.isEmpty { // We need to append in place those that don't exist yet let folderIndex = cvf.getFolderIndex(withUrl: url.path) for asset in assets { if !cvf.folders[folderIndex].hasAsset(withUrl: asset.url) { cvf.folders[folderIndex].assets.append(asset) } } } } else { // Create our initial CVF with the parsed folder manifestInstance.customVideoFolders = CustomVideoFolders(folders: [Folder(url: url.path, label: folderName, assets: assets)]) }*/ folderOutlineView.reloadData() folderOutlineView.expandItem(nil, expandChildren: true) folderOutlineView.deselectAll(self) } catch { errorLog("Could not process directory") } } // MARK: - Edit Files @IBAction func videoNameChange(_ sender: NSTextField) { if let asset = currentAsset { asset.accessibilityLabel = sender.stringValue folderOutlineView.reloadData() } } @IBAction func timePopUpChange(_ sender: NSPopUpButton) { if let asset = currentAsset { if sender.indexOfSelectedItem == 0 { asset.time = "day" } else { asset.time = "night" } } } // MARK: - Add/Remove POIs @IBAction func addPoiClick(_ sender: NSButton) { addPoiPopover.show(relativeTo: sender.preparedContentRect, of: sender, preferredEdge: .maxY) } @IBAction func removePoiClick(_ sender: NSButton) { if let asset = currentAsset { let keys = asset.pointsOfInterest.keys.map { Int($0)!}.sorted() asset.pointsOfInterest.removeValue(forKey: String(keys[poiTableView.selectedRow])) poiTableView.reloadData() } } @IBAction func addPoiValidate(_ sender: NSButton) { if let asset = currentAsset { if timeTextField.stringValue != "" && descriptionTextField.stringValue != "" { if asset.pointsOfInterest[timeTextField.stringValue] == nil { asset.pointsOfInterest[timeTextField.stringValue] = descriptionTextField.stringValue // We also reset the popup so it's clean for next poi timeTextField.stringValue = "" descriptionTextField.stringValue = "" poiTableView.reloadData() addPoiPopover.close() } } } } @IBAction func timeStepperChange(_ sender: NSStepper) { if let player = editPlayerView.player { player.seek(to: CMTime(seconds: Double(sender.intValue), preferredTimescale: 1)) } } @IBAction func timeTextChange(_ sender: NSTextField) { if let player = editPlayerView.player { player.seek(to: CMTime(seconds: Double(sender.intValue), preferredTimescale: 1)) } } @IBAction func tableViewTimeField(_ sender: NSTextField) { if let asset = currentAsset { if poiTableView.selectedRow != -1 { let keys = asset.pointsOfInterest.keys.map { Int($0)!}.sorted() asset.pointsOfInterest.switchKey(fromKey: String(keys[poiTableView.selectedRow]), toKey: sender.stringValue) } } } @IBAction func tableViewDescField(_ sender: NSTextField) { if let asset = currentAsset { if poiTableView.selectedRow != -1 { let keys = asset.pointsOfInterest.keys.map { Int($0)!}.sorted() asset.pointsOfInterest[String(keys[poiTableView.selectedRow])] = sender.stringValue } } } // MARK: - Context menu @IBAction func menuRemoveFolderAndVideoClick(_ sender: NSMenuItem) { if let folder = sender.representedObject as? Folder { let manifestInstance = ManifestLoader.instance if let cvf = manifestInstance.customVideoFolders { cvf.folders.remove(at: cvf.getFolderIndex(withUrl: folder.url)) } } folderOutlineView.reloadData() } @IBAction func menuRemoveVideoClick(_ sender: NSMenuItem) { if let asset = sender.representedObject as? Asset { let manifestInstance = ManifestLoader.instance if let cvf = manifestInstance.customVideoFolders { for fld in cvf.folders { let index = fld.getAssetIndex(withUrl: asset.url) if index > -1 { fld.assets.remove(at: index) } } } } folderOutlineView.reloadData() } } // MARK: - Data source for side bar extension CustomVideoController: NSOutlineViewDataSource { // Find and return the child of an item. If item == nil, we need to return a child of the // root node otherwise we find and return the child of the parent node indicated by 'item' func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { let manifestInstance = ManifestLoader.instance if let source = item as? Source { return VideoList.instance.videos.filter({ $0.source.name == source.name })[index] } // Return a source return SourceList.foundSources.filter({ $0.type == .local })[index] } // Tell the view controller whether an item can be expanded (i.e. it has children) or not // (i.e. it doesn't) func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { // A folder may have childs if it's not empty if let folder = item as? Folder { return !folder.assets.isEmpty } // But not assets return false } // Tell the view how many children an item has func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { let manifestInstance = ManifestLoader.instance // A folder may have childs if let source = item as? Source { return VideoList.instance.videos.filter({ $0.source.name == source.name }).count } return SourceList.foundSources.filter({ $0.type == .local }).count } } // MARK: - Delegate for side bar extension CustomVideoController: NSOutlineViewDelegate { // Add text to the view. 'item' will either be a Creature object or a string. If it's the former we just // use the 'type' attribute otherwise we downcast it to a string and use that instead. func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { var text = "" if let source = item as? Source { text = source.name } else if let video = item as? AerialVideo { text = video.name } // Create our table cell -- note the reference to 'creatureCell' that we set when configuring the table cell let tableCell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "folderCell"), owner: nil) as! NSTableCellView tableCell.textField!.stringValue = text return tableCell } // We update our view here when an item is selected func outlineView(_ outlineView: NSOutlineView, shouldSelectItem item: Any) -> Bool { debugLog("selected \(item)") if let source = item as? Source { topPathControl.isHidden = false folderView.isHidden = false fileView.isHidden = true onboardingLabel.isHidden = true topPathControl.url = URL(fileURLWithPath: source.manifestUrl) folderShortNameTextField.stringValue = source.description currentAsset = nil currentFolder = nil // folder } else if let file = item as? Asset { topPathControl.isHidden = false folderView.isHidden = true fileView.isHidden = false onboardingLabel.isHidden = true topPathControl.url = URL(fileURLWithPath: file.url) videoNameTextField.stringValue = file.accessibilityLabel if file.time == "day" { timePopUpButton.selectItem(at: 0) } else { timePopUpButton.selectItem(at: 1) } currentFolder = nil currentAsset = file // We use this later to populate the table view removePoi.isEnabled = false if let player = editPlayerView.player { let localitem = AVPlayerItem(url: URL(fileURLWithPath: file.url)) currentAssetDuration = Int(localitem.asset.duration.convertScale(1, method: .default).value) let currentResolution = getResolution(asset: localitem.asset) let crString = String(Int(currentResolution.width)) + "x" + String(Int(currentResolution.height)) timeTextStepper.minValue = 0 timeTextStepper.maxValue = Double(currentAssetDuration!) timeTextFormatter.minimum = 0 timeTextFormatter.maximum = NSNumber(value: currentAssetDuration!) durationLabel.stringValue = String(currentAssetDuration!) + " seconds" resolutionLabel.stringValue = crString player.replaceCurrentItem(with: localitem) } poiTableView.reloadData() } else { topPathControl.isHidden = true folderView.isHidden = true fileView.isHidden = true onboardingLabel.isHidden = false } return true } func getResolution(asset: AVAsset) -> CGSize { guard let track = asset.tracks(withMediaType: AVMediaType.video).first else { return CGSize.zero } let size = track.naturalSize.applying(track.preferredTransform) return CGSize(width: abs(size.width), height: abs(size.height)) } func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation { return NSDragOperation.copy } func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool { if let items = info.draggingPasteboard.pasteboardItems { for item in items { if #available(OSX 10.13, *) { if let str = item.string(forType: .fileURL) { let surl = URL(fileURLWithPath: str).standardized debugLog("received drop \(surl)") if surl.isDirectory { debugLog("processing dir") self.processPathForVideos(url: surl) } } } else { // Fallback on earlier versions } } } return true } } // MARK: - Extension for poi table view extension CustomVideoController: NSTableViewDataSource, NSTableViewDelegate { // currentAsset contains the selected video asset func numberOfRows(in tableView: NSTableView) -> Int { if let asset = currentAsset { return asset.pointsOfInterest.count } else { return 0 } } // This is where we populate the tableview func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { if let asset = currentAsset { var text: String if tableColumn!.identifier.rawValue == "timeColumn" { let keys = asset.pointsOfInterest.keys.map { Int($0)!}.sorted() text = String(keys[row]) } else { let keys = asset.pointsOfInterest.keys.map { Int($0)!}.sorted() text = asset.pointsOfInterest[String(keys[row])]! } if let cell = tableView.makeView(withIdentifier: tableColumn!.identifier, owner: self) as? NSTableCellView { cell.textField?.stringValue = text cell.imageView?.image = nil return cell } } return nil } func tableViewSelectionDidChange(_ notification: Notification) { if let asset = currentAsset { if poiTableView.selectedRow >= 0 { removePoi.isEnabled = true let keys = asset.pointsOfInterest.keys.map { Int($0)!}.sorted() if let player = editPlayerView.player { player.seek(to: CMTime(seconds: Double(keys[poiTableView.selectedRow]), preferredTimescale: 1)) } } else { removePoi.isEnabled = false } } } } extension Dictionary { mutating func switchKey(fromKey: Key, toKey: Key) { if let entry = removeValue(forKey: fromKey) { self[toKey] = entry } } } extension URL { /*var isDirectory: Bool? { do { let values = try self.resourceValues( forKeys: Set([URLResourceKey.isDirectoryKey]) ) return values.isDirectory } catch { return nil } }*/ var isDirectory: Bool { return (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true } var subDirectories: [URL] { guard isDirectory else { return [] } return (try? FileManager.default.contentsOfDirectory(at: self, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]).filter(\.isDirectory)) ?? [] } } extension CustomVideoController: NSMenuDelegate { func menuNeedsUpdate(_ menu: NSMenu) { let row = folderOutlineView.clickedRow guard row != -1 else { return } let rowItem = folderOutlineView.item(atRow: row) if (rowItem as? Folder) != nil { menuRemoveVideo.isHidden = true menuRemoveFolderAndVideos.isHidden = false } else if (rowItem as? Asset) != nil { menuRemoveVideo.isHidden = false menuRemoveFolderAndVideos.isHidden = true } // Mark the clicked item here for item in menu.items { item.representedObject = rowItem } } } ================================================ FILE: Aerial/Source/Header.h ================================================ // // Header.h // Aerial // // Created by Guillaume Louel on 26/07/2023. // Copyright © 2023 Guillaume Louel. All rights reserved. // #ifndef Header_h #define Header_h #endif /* Header_h */ ================================================ FILE: Aerial/Source/Models/API/Forecast.swift ================================================ // // Forecast.swift // Aerial // // Created by Guillaume Louel on 26/04/2021. // Copyright © 2021 Guillaume Louel. All rights reserved. // // This file was generated from JSON Schema using quicktype, do not modify it directly. // To parse the JSON, add this file to your project and do: // // let forecast = try? newJSONDecoder().decode(Forecast.self, from: jsonData) import Foundation // MARK: - Forecast struct ForecastElement: Codable { let cod: String? let message, cnt: Int? let list: [FList]? let city: City? } // MARK: - City struct City: Codable { let id: Int? let name: String? let coord: Coord? let country: String? let population, timezone, sunrise, sunset: Int? } // MARK: - Coord struct Coord: Codable { let lat, lon: Double? } // MARK: - List struct FList: Codable { let dt: Int? let main: MainClass? let weather: [OWWeather]? let clouds: Clouds? let wind: Wind? let visibility: Int? let pop: Double? let sys: Sys? let dtTxt: String? let rain: Rain? enum CodingKeys: String, CodingKey { case dt, main, weather, clouds, wind, visibility, pop, sys case dtTxt = "dt_txt" case rain } } // MARK: - Clouds struct Clouds: Codable { let all: Int? } // MARK: - MainClass struct MainClass: Codable { let temp, feelsLike, tempMin, tempMax: Double? let pressure, seaLevel, grndLevel, humidity: Int? let tempKf: Double? enum CodingKeys: String, CodingKey { case temp case feelsLike = "feels_like" case tempMin = "temp_min" case tempMax = "temp_max" case pressure case seaLevel = "sea_level" case grndLevel = "grnd_level" case humidity case tempKf = "temp_kf" } } // MARK: - Rain struct Rain: Codable { let the3H: Double? enum CodingKeys: String, CodingKey { case the3H = "3h" } } // MARK: - Sys struct Sys: Codable { let pod: String? } // MARK: - ForecastError struct ForecastError: Codable { let cod, message: String? } // MARK: - Wind struct Wind: Codable { let speed: Double? let deg: Int? let gust: Double? } struct Forecast { static var testJson = "" static func getUnits() -> String { if PrefsInfo.weather.degree == .celsius { return "metric" } else { return "imperial" } } static func getShortcodeLanguage() -> String { // Those are the languages supported by OpenWeather let weatherLanguages = ["af", "al", "ar", "az", "bg", "ca", "cz", "da", "de", "el", "en", "eu", "fa", "fi", "fr", "gl", "he", "hi", "hr", "hu", "id", "it", "ja", "kr", "la", "lt", "mk", "no", "nl", "pl", "pt", "pt_br", "ro", "ru", "sv", "sk", "sl", "es", "sr", "th", "tr", "uk", "vi", "zh_cn", "zh_tw", "zu" ] if PrefsAdvanced.ciOverrideLanguage == "" { let bestMatchedLanguage = Bundle.preferredLocalizations(from: weatherLanguages, forPreferences: Locale.preferredLanguages).first if let match = bestMatchedLanguage { debugLog("Best matched language : \(match)") return match } } else { debugLog("Overrode matched language : \(PrefsAdvanced.ciOverrideLanguage)") return PrefsAdvanced.ciOverrideLanguage } // We fallback here if nothing works return "en" } static func makeUrl(lat: String, lon: String) -> String { return "https://api.openweathermap.org/data/2.5/forecast" + "?lat=\(lat)&lon=\(lon)" + "&units=\(getUnits())" + "&lang=\(getShortcodeLanguage())" + "&APPID=\(APISecrets.openWeatherAppId)" } static func makeUrl(location: String) -> String { let nloc = location.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! return "https://api.openweathermap.org/data/2.5/forecast" + "?q=\(nloc)" + "&units=\(getUnits())" + "&lang=\(getShortcodeLanguage())" + "&APPID=\(APISecrets.openWeatherAppId)" } // swiftlint:disable:next cyclomatic_complexity static func fetch(completion: @escaping(Result) -> Void) { guard testJson == "" else { let jsonData = testJson.data(using: .utf8)! if let forecast = try? newJSONDecoder().decode(ForecastElement.self, from: jsonData) { completion(.success(forecast)) } else { completion(.failure(.unknown)) } return } if PrefsInfo.weather.locationMode == .useCurrent { let location = Locations.sharedInstance location.getCoordinates(failure: { (_) in completion(.failure(.unknown)) }, success: { (coordinates) in let lat = String(format: "%.2f", coordinates.latitude) let lon = String(format: "%.2f", coordinates.longitude) debugLog("=== OF: Starting locationMode") fetchData(from: makeUrl(lat: lat, lon: lon)) { result in switch result { case .success(let jsonString): let jsonData = jsonString.data(using: .utf8)! if let forecast = try? newJSONDecoder().decode(ForecastElement.self, from: jsonData) { completion(.success(forecast)) } else if (try? newJSONDecoder().decode(ForecastError.self, from: jsonData)) != nil { completion(.failure(.cityNotFound)) } else { completion(.failure(.unknown)) } case .failure(let error): completion(.failure(.unknown)) print(error.localizedDescription) } } }) } else { // Just in case, we add a failsafe if PrefsInfo.weather.locationString == "" { PrefsInfo.weather.locationString = "Paris, FR" } debugLog("=== OF: Starting manual mode") fetchData(from: makeUrl(location: PrefsInfo.weather.locationString)) { result in switch result { case .success(let jsonString): let jsonData = jsonString.data(using: .utf8)! if let forecast = try? newJSONDecoder().decode(ForecastElement.self, from: jsonData) { completion(.success(forecast)) } else if (try? newJSONDecoder().decode(ForecastError.self, from: jsonData)) != nil { completion(.failure(.cityNotFound)) } else { completion(.failure(.unknown)) } case .failure(_): completion(.failure(.unknown)) } } } } private static func fetchData(from urlString: String, completion: @escaping (Result) -> Void) { // check the URL is OK, otherwise return with a failure guard let url = URL(string: urlString) else { completion(.failure(.badURL)) return } URLSession.shared.dataTask(with: url) { data, _, error in // the task has completed – push our work back to the main thread DispatchQueue.main.async { if let data = data { // success: convert the data to a string and send it back let stringData = String(decoding: data, as: UTF8.self) completion(.success(stringData)) } else if error != nil { // any sort of network failure completion(.failure(.requestFailed)) } else { // this ought not to be possible, yet here we are completion(.failure(.unknown)) } } }.resume() } } ================================================ FILE: Aerial/Source/Models/API/GeoCoding.swift ================================================ // // GeoCoding.swift // Aerial // // Created by Guillaume Louel on 22/04/2021. // Copyright © 2021 Guillaume Louel. All rights reserved. // import Foundation // MARK: - GeoCodingElement struct GeoCodingElement: Codable { let name: String? let lat, lon: Double? let country, state: String? enum CodingKeys: String, CodingKey { case name case lat, lon, country, state } } typealias GeoCodingArray = [GeoCodingElement] struct GeoLocation { let lat, lon: String } struct GeoCoding { static func fetch(completion: @escaping(Result) -> Void) { // Check if we already have a geocoded location for this ? if PrefsTime.geocodedString == PrefsInfo.weather.locationString { debugLog("returning cached location from previous geocoding") let lat = String(format: "%.2f", PrefsTime.cachedLatitude) let lon = String(format: "%.2f", PrefsTime.cachedLongitude) completion(.success(GeoLocation(lat: lat, lon: lon))) } else { // Seriously, please use Location services... debugLog("looking for location through geocoding api") // Just in case, we add a ugly failsafe if PrefsInfo.weather.locationString == "" { PrefsInfo.weather.locationString = "Paris, FR" } fetchData(from: makeUrl()) { result in switch result { case .success(let jsonString): let jsonData = jsonString.data(using: .utf8)! if let geoEntity = try? newJSONDecoder().decode(GeoCodingArray.self, from: jsonData) { if geoEntity.count >= 1 { let lat = String(format: "%.2f", geoEntity[0].lat!) let lon = String(format: "%.2f", geoEntity[0].lon!) // Let's save for next time PrefsTime.geocodedString = PrefsInfo.weather.locationString PrefsTime.cachedLatitude = geoEntity[0].lat! PrefsTime.cachedLongitude = geoEntity[0].lon! completion(.success(GeoLocation(lat: lat, lon: lon))) } else { completion(.failure(.unknown)) } } else { completion(.failure(.unknown)) } case .failure(_): completion(.failure(.unknown)) } } } } static func makeUrl() -> String { let nloc = PrefsInfo.weather.locationString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! return "https://api.openweathermap.org/geo/1.0/direct" + "?q=\(nloc)" + "&appid=\(APISecrets.openWeatherAppId)" } private static func fetchData(from urlString: String, completion: @escaping (Result) -> Void) { // check the URL is OK, otherwise return with a failure guard let url = URL(string: urlString) else { completion(.failure(.badURL)) return } URLSession.shared.dataTask(with: url) { data, _, error in // the task has completed – push our work back to the main thread DispatchQueue.main.async { if let data = data { // success: convert the data to a string and send it back let stringData = String(decoding: data, as: UTF8.self) completion(.success(stringData)) } else if error != nil { // any sort of network failure completion(.failure(.requestFailed)) } else { // this ought not to be possible, yet here we are completion(.failure(.unknown)) } } }.resume() } } ================================================ FILE: Aerial/Source/Models/API/OneCall.swift ================================================ // // OWOneCall.swift // Aerial // // Created by Guillaume Louel on 23/03/2021. // Copyright © 2021 Guillaume Louel. All rights reserved. // // This file was generated from JSON Schema using quicktype, do not modify it directly. // To parse the JSON, add this file to your project and do: // // let oCOneCall = try? newJSONDecoder().decode(OCOneCall.self, from: jsonData) import Foundation // MARK: - OCOneCall struct OCOneCall: Codable { let lat, lon: Double? let timezone: String? let timezoneOffset: Int? let current: OCCurrent? let minutely: [OCMinutely]? let hourly: [OCCurrent]? let daily: [OCDaily]? enum CodingKeys: String, CodingKey { case lat, lon, timezone case timezoneOffset = "timezone_offset" case current, minutely, hourly, daily } // We round them down a bit as openweather provides up to two decimal point precision mutating func processTemperatures() { /* guard main != nil else { return } if PrefsInfo.weather.degree == .celsius { main!.temp = main!.temp.rounded(toPlaces: 1) main!.feelsLike = main!.feelsLike.rounded(toPlaces: 1) } else { main!.temp = main!.temp.rounded() main!.feelsLike = main!.feelsLike.rounded() }*/ } } // MARK: - OCCurrent struct OCCurrent: Codable { let dt, sunrise, sunset: Int? let temp, feelsLike: Double? let pressure, humidity: Int? let dewPoint, uvi: Double? let clouds, visibility: Int? let windSpeed: Double? let windDeg: Int? let weather: [OWWeather]? let windGust, pop: Double? let rain: OCRain? enum CodingKeys: String, CodingKey { case dt, sunrise, sunset, temp case feelsLike = "feels_like" case pressure, humidity case dewPoint = "dew_point" case uvi, clouds, visibility case windSpeed = "wind_speed" case windDeg = "wind_deg" case weather case windGust = "wind_gust" case pop, rain } } // MARK: - OCRain struct OCRain: Codable { let the1H: Double? enum CodingKeys: String, CodingKey { case the1H = "1h" } } /* // MARK: - OCWeather struct OCWeather: Codable { let id: Int? let main: String? let weatherDescription: String? let icon: String? enum CodingKeys: String, CodingKey { case id, main case weatherDescription = "description" case icon } }*/ // MARK: - OCDaily struct OCDaily: Codable { let dt, sunrise, sunset: Int? let temp: OCTemp? let feelsLike: OCFeelsLike? let pressure, humidity: Int? let dewPoint, windSpeed: Double? let windDeg: Int? let weather: [OWWeather]? let clouds: Int? let pop, uvi, rain: Double? enum CodingKeys: String, CodingKey { case dt, sunrise, sunset, temp case feelsLike = "feels_like" case pressure, humidity case dewPoint = "dew_point" case windSpeed = "wind_speed" case windDeg = "wind_deg" case weather, clouds, pop, uvi, rain } } // MARK: - OCFeelsLike struct OCFeelsLike: Codable { let day, night, eve, morn: Double? } // MARK: - OCTemp struct OCTemp: Codable { let day, min, max, night: Double? let eve, morn: Double? } // MARK: - OCMinutely struct OCMinutely: Codable { let dt, precipitation: Int? } struct OneCall { static var testJson = "" static func getUnits() -> String { if PrefsInfo.weather.degree == .celsius { return "metric" } else { return "imperial" } } static func getShortcodeLanguage() -> String { // Those are the languages supported by OpenWeather let weatherLanguages = ["af", "al", "ar", "az", "bg", "ca", "cz", "da", "de", "el", "en", "eu", "fa", "fi", "fr", "gl", "he", "hi", "hr", "hu", "id", "it", "ja", "kr", "la", "lt", "mk", "no", "nl", "pl", "pt", "pt_br", "ro", "ru", "sv", "sk", "sl", "es", "sr", "th", "tr", "uk", "vi", "zh_cn", "zh_tw", "zu" ] if PrefsAdvanced.ciOverrideLanguage == "" { let bestMatchedLanguage = Bundle.preferredLocalizations(from: weatherLanguages, forPreferences: Locale.preferredLanguages).first if let match = bestMatchedLanguage { debugLog("Best matched language : \(match)") return match } } else { debugLog("Overrode matched language : \(PrefsAdvanced.ciOverrideLanguage)") return PrefsAdvanced.ciOverrideLanguage } // We fallback here if nothing works return "en" } static func makeUrl(lat: String, lon: String) -> String { return "https://api.openweathermap.org/data/2.5/onecall" + "?lat=\(lat)&lon=\(lon)" + "&units=\(getUnits())" + "&lang=\(getShortcodeLanguage())" + "&APPID=\(APISecrets.openWeatherAppId)" } /* static func makeUrl(location: String) -> String { let nloc = location.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! return "https://api.openweathermap.org/data/2.5/onecall" + "?q=\(nloc)" + "&units=\(getUnits())" + "&lang=\(getShortcodeLanguage())" + "&APPID=\(APISecrets.openWeatherAppId)" }*/ // swiftlint:disable:next cyclomatic_complexity static func fetch(completion: @escaping(Result) -> Void) { guard testJson == "" else { let jsonData = testJson.data(using: .utf8)! do { let openWeather = try newJSONDecoder().decode(OCOneCall.self, from: jsonData) completion(.success(openWeather)) } catch { completion(.failure(.unknown)) } return } if PrefsInfo.weather.locationMode == .useCurrent { let location = Locations.sharedInstance location.getCoordinates(failure: { (_) in completion(.failure(.unknown)) }, success: { (coordinates) in let lat = String(format: "%.2f", coordinates.latitude) let lon = String(format: "%.2f", coordinates.longitude) debugLog("=== OC: Starting locationMode") fetchData(from: makeUrl(lat: lat, lon: lon)) { result in switch result { case .success(let jsonString): let jsonData = jsonString.data(using: .utf8)! if var openWeather = try? newJSONDecoder().decode(OCOneCall.self, from: jsonData) { openWeather.processTemperatures() completion(.success(openWeather)) } else { completion(.failure(.unknown)) } case .failure(_): completion(.failure(.unknown)) } } }) } else { // Urgh, please use location services... debugLog("=== OC: Starting manual mode") GeoCoding.fetch { result in switch result { case .success(let geoLocation): fetchData(from: makeUrl(lat: geoLocation.lat, lon: geoLocation.lon)) { result in switch result { case .success(let jsonString): let jsonData = jsonString.data(using: .utf8)! if var openWeather = try? newJSONDecoder().decode(OCOneCall.self, from: jsonData) { openWeather.processTemperatures() completion(.success(openWeather)) } else { completion(.failure(.unknown)) } case .failure(_): completion(.failure(.unknown)) } } case .failure(let error): debugLog(error.localizedDescription) completion(.failure(.unknown)) } } } } private static func fetchData(from urlString: String, completion: @escaping (Result) -> Void) { // check the URL is OK, otherwise return with a failure guard let url = URL(string: urlString) else { completion(.failure(.badURL)) return } URLSession.shared.dataTask(with: url) { data, _, error in // the task has completed – push our work back to the main thread DispatchQueue.main.async { if let data = data { // success: convert the data to a string and send it back let stringData = String(decoding: data, as: UTF8.self) completion(.success(stringData)) } else if error != nil { // any sort of network failure completion(.failure(.requestFailed)) } else { // this ought not to be possible, yet here we are completion(.failure(.unknown)) } } }.resume() } } ================================================ FILE: Aerial/Source/Models/API/OpenWeather.swift ================================================ // // OpenWeather.swift // Aerial // // Created by Guillaume Louel on 04/03/2021. // Copyright © 2021 Guillaume Louel. All rights reserved. // // This file was generated from JSON Schema using quicktype, do not modify it directly. // To parse the JSON, add this file to your project and do: // // let openWeather = try? newJSONDecoder().decode(OWeather.self, from: jsonData) import Foundation enum NetworkError: Error { case badURL case requestFailed case unknown case cityNotFound } // MARK: - OpenWeather struct OWeather: Codable { let coord: OWCoord? let weather: [OWWeather]? let base: String? var main: OWMain? let visibility: Int? let wind: OWWind? let clouds: OWClouds? let dt: Int? let sys: OWSys? let timezone, id: Int? let name: String? let cod: Int? // We round them down a bit as openweather provides up to two decimal point precision mutating func processTemperatures() { guard main != nil else { return } if PrefsInfo.weather.degree == .celsius { main!.temp = main!.temp.rounded(toPlaces: 1) main!.feelsLike = main!.feelsLike.rounded(toPlaces: 1) } else { main!.temp = main!.temp.rounded() main!.feelsLike = main!.feelsLike.rounded() } } } // MARK: - OWClouds struct OWClouds: Codable { let all: Int? } // MARK: - OWCoord struct OWCoord: Codable { let lon, lat: Double? } // MARK: - OWMain struct OWMain: Codable { var temp: Double var feelsLike: Double var tempMin, tempMax, pressure, humidity: Double enum CodingKeys: String, CodingKey { case temp case feelsLike = "feels_like" case tempMin = "temp_min" case tempMax = "temp_max" case pressure, humidity } } // MARK: - OWSys struct OWSys: Codable { let type, id: Int? let country: String let sunrise, sunset: Int } // MARK: - OWWeather struct OWWeather: Codable { let id: Int let main, weatherDescription, icon: String enum CodingKeys: String, CodingKey { case id, main case weatherDescription = "description" case icon } } // MARK: - OWWind struct OWWind: Codable { let speed: Double let deg: Int let gust: Double? } struct OpenWeather { static var testJson = "" static func getUnits() -> String { if PrefsInfo.weather.degree == .celsius { return "metric" } else { return "imperial" } } static func getShortcodeLanguage() -> String { // Those are the languages supported by OpenWeather let weatherLanguages = ["af", "al", "ar", "az", "bg", "ca", "cz", "da", "de", "el", "en", "eu", "fa", "fi", "fr", "gl", "he", "hi", "hr", "hu", "id", "it", "ja", "kr", "la", "lt", "mk", "no", "nl", "pl", "pt", "pt_br", "ro", "ru", "sv", "sk", "sl", "es", "sr", "th", "tr", "uk", "vi", "zh_cn", "zh_tw", "zu" ] if PrefsAdvanced.ciOverrideLanguage == "" { let bestMatchedLanguage = Bundle.preferredLocalizations(from: weatherLanguages, forPreferences: Locale.preferredLanguages).first if let match = bestMatchedLanguage { debugLog("Best matched language : \(match)") return match } } else { debugLog("Overrode matched language : \(PrefsAdvanced.ciOverrideLanguage)") return PrefsAdvanced.ciOverrideLanguage } // We fallback here if nothing works return "en" } static func makeUrl(lat: String, lon: String) -> String { return "https://api.openweathermap.org/data/2.5/weather" + "?lat=\(lat)&lon=\(lon)" + "&units=\(getUnits())" + "&lang=\(getShortcodeLanguage())" + "&APPID=\(APISecrets.openWeatherAppId)" } static func makeUrl(location: String) -> String { let nloc = location.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! return "https://api.openweathermap.org/data/2.5/weather" + "?q=\(nloc)" + "&units=\(getUnits())" + "&lang=\(getShortcodeLanguage())" + "&APPID=\(APISecrets.openWeatherAppId)" } static func fetch(completion: @escaping(Result) -> Void) { guard testJson == "" else { let jsonData = testJson.data(using: .utf8)! if var openWeather = try? newJSONDecoder().decode(OWeather.self, from: jsonData) { openWeather.processTemperatures() completion(.success(openWeather)) } else { completion(.failure(.cityNotFound)) } return } if PrefsInfo.weather.locationMode == .useCurrent { let location = Locations.sharedInstance location.getCoordinates(failure: { (_) in completion(.failure(.unknown)) }, success: { (coordinates) in let lat = String(format: "%.2f", coordinates.latitude) let lon = String(format: "%.2f", coordinates.longitude) debugLog("=== OW: Starting locationMode") fetchData(from: makeUrl(lat: lat, lon: lon)) { result in switch result { case .success(let jsonString): let jsonData = jsonString.data(using: .utf8)! do { var openWeather = try newJSONDecoder().decode(OWeather.self, from: jsonData) openWeather.processTemperatures() completion(.success(openWeather)) } catch { debugLog("=== OW: JSON decoding failed: \(error)") if let decodingError = error as? DecodingError { switch decodingError { case .keyNotFound(let key, let context): debugLog("=== OW: Missing key '\(key.stringValue)' at: \(context.codingPath.map { $0.stringValue }.joined(separator: "."))") case .typeMismatch(let type, let context): debugLog("=== OW: Type mismatch for \(type) at: \(context.codingPath.map { $0.stringValue }.joined(separator: "."))") case .valueNotFound(let type, let context): debugLog("=== OW: Missing value for \(type) at: \(context.codingPath.map { $0.stringValue }.joined(separator: "."))") case .dataCorrupted(let context): debugLog("=== OW: Data corrupted at: \(context.codingPath.map { $0.stringValue }.joined(separator: "."))") @unknown default: debugLog("=== OW: Unknown decoding error") } } completion(.failure(.cityNotFound)) } case .failure(let error): completion(.failure(.unknown)) } } }) } else { // Just in case, we add a failsafe if PrefsInfo.weather.locationString == "" { PrefsInfo.weather.locationString = "Paris, FR" } debugLog("=== OW: Starting manual mode") fetchData(from: makeUrl(location: PrefsInfo.weather.locationString)) { result in switch result { case .success(let jsonString): let jsonData = jsonString.data(using: .utf8)! do { var openWeather = try newJSONDecoder().decode(OWeather.self, from: jsonData) openWeather.processTemperatures() completion(.success(openWeather)) } catch { debugLog("=== OW: JSON decoding failed: \(error)") if let decodingError = error as? DecodingError { switch decodingError { case .keyNotFound(let key, let context): debugLog("=== OW: Missing key '\(key.stringValue)' at: \(context.codingPath.map { $0.stringValue }.joined(separator: "."))") case .typeMismatch(let type, let context): debugLog("=== OW: Type mismatch for \(type) at: \(context.codingPath.map { $0.stringValue }.joined(separator: "."))") case .valueNotFound(let type, let context): debugLog("=== OW: Missing value for \(type) at: \(context.codingPath.map { $0.stringValue }.joined(separator: "."))") case .dataCorrupted(let context): debugLog("=== OW: Data corrupted at: \(context.codingPath.map { $0.stringValue }.joined(separator: "."))") @unknown default: debugLog("=== OW: Unknown decoding error") } } completion(.failure(.cityNotFound)) } case .failure(_): completion(.failure(.unknown)) } } } } private static func fetchData(from urlString: String, completion: @escaping (Result) -> Void) { // check the URL is OK, otherwise return with a failure guard let url = URL(string: urlString) else { completion(.failure(.badURL)) return } URLSession.shared.dataTask(with: url) { data, _, error in // the task has completed – push our work back to the main thread DispatchQueue.main.async { if let data = data { // success: convert the data to a string and send it back let stringData = String(decoding: data, as: UTF8.self) completion(.success(stringData)) } else if error != nil { // any sort of network failure completion(.failure(.requestFailed)) } else { // this ought not to be possible, yet here we are completion(.failure(.unknown)) } } }.resume() } } ================================================ FILE: Aerial/Source/Models/Aerial.swift ================================================ // // Aerial.swift // Aerial // // Contains some common helpers used throughout the code // // Created by Guillaume Louel on 17/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class Aerial: NSObject { static let helper = Aerial() var windowController: PanelWindowController? // We use this to track whether we run as a screen saver or an app var appMode = false // We also track darkmode here now var darkMode = false // And we track if we are running under Aerial's Companion var underCompanion = false let userName = NSUserName() // Track our version number for logs and stuff var version: String = { if let version = Bundle(identifier: "com.johncoates.Aerial-Test")?.infoDictionary?["CFBundleShortVersionString"] as? String { return "Version " + version } else if let version = Bundle(identifier: "com.JohnCoates.Aerial")?.infoDictionary?["CFBundleShortVersionString"] as? String { return "Version " + version } return "Version ?" }() // Using HDR in the panel will crash System Settings in macOS 13. This is fixed in macOS 13.4 🎉 func canHDR() -> Bool { if #available(OSX 13.0, *) { if #unavailable(OSX 13.4) { return false } } return true } // Are we running under Aerial Companion ? Desktop mode/Fullscreen mode // Xcode debug mode is also considered as running under Companion func checkCompanion() { logToConsole("Checking for companion") if appMode { underCompanion = true logToConsole("> Running in appMode, simming Companion!") } else { for bundle in Bundle.allBundles { if let bundleId = bundle.bundleIdentifier { if bundleId.contains("AerialUpdater") { underCompanion = true logToConsole("> Running under Aerial Companion!") } } } } } func computeDarkMode(view: NSView) { if #available(OSX 10.14, *) { //debugLog("Best match appearance : \(view.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]))") //debugLog("Effective Appearence : \(view.effectiveAppearance)") darkMode = view.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua } else { darkMode = false } } // Language detection func getPreferredLanguage() -> String { let printOutputLocale: NSLocale = NSLocale(localeIdentifier: Locale.preferredLanguages[0]) if let deviceLanguageName: String = printOutputLocale.displayName(forKey: .identifier, value: Locale.preferredLanguages[0]) { if #available(OSX 10.12, *) { return "Preferred language: \(deviceLanguageName) [\(printOutputLocale.languageCode)]" } else { return "Preferred language: \(deviceLanguageName)" } } else { return "" } } // Alerts func showErrorAlert(question: String, text: String, button: String = "OK") { let alert = NSAlert() alert.messageText = question alert.informativeText = text alert.alertStyle = .critical alert.icon = NSImage(named: NSImage.cautionName) alert.addButton(withTitle: button) alert.runModal() } func showAlert(question: String, text: String, button1: String = "OK", button2: String = "Cancel") -> Bool { let alert = NSAlert() alert.messageText = question alert.informativeText = text alert.alertStyle = .warning alert.icon = NSImage(named: NSImage.cautionName) alert.addButton(withTitle: button1) alert.addButton(withTitle: button2) return alert.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn } func showInfoAlert(title: String, text: String, button1: String = "OK", caution: Bool = false) { let alert = NSAlert() alert.messageText = title alert.informativeText = text alert.alertStyle = .warning if caution { alert.icon = NSImage(named: NSImage.cautionName) } else { alert.icon = NSImage(named: NSImage.infoName) } alert.addButton(withTitle: button1) alert.runModal() } // Symbol/icon generation // Symbol as a CALayer func getSymbolLayer(_ named: String, size: CGFloat) -> CALayer { let imglayer = CALayer() imglayer.contents = Aerial.helper.getSymbol(named) imglayer.frame.size = CGSize(width: size, height: size) return imglayer } // Symbol as a NSImage func getSymbol(_ named: String) -> NSImage? { // Use SFSymbols if available if #available(macOS 11.0, *) { if let image = NSImage(systemSymbolName: named, accessibilityDescription: named) { image.isTemplate = true // return image let config = NSImage.SymbolConfiguration(pointSize: 100, weight: .regular) return image.withSymbolConfiguration(config)?.tinting(with: .white) } } if let imagePath = Bundle(for: PanelWindowController.self).path( forResource: fallbackSymbol(named), ofType: "pdf") { return NSImage(contentsOfFile: imagePath) } return nil } func getMiniSymbol(_ named: String, tint: NSColor = .labelColor) -> NSImage? { if let symbol = getSymbol(named) { return resize(image: symbol, w: Int(symbol.size.width)/10, h: Int(symbol.size.height)/10).tinting(with: tint) } else { return nil } } // TODO: move to extension of NSImage... // swiftlint:disable:next identifier_name func resize(image: NSImage, w: Int, h: Int) -> NSImage { let destSize = NSSize(width: CGFloat(w), height: CGFloat(h)) let newImage = NSImage(size: destSize) newImage.lockFocus() image.draw(in: NSRect(x: 0, y: 0, width: destSize.width, height: destSize.height), from: NSRect(x: 0, y: 0, width: image.size.width, height: image.size.height), operation: NSCompositingOperation.sourceOver, fraction: CGFloat(1)) newImage.unlockFocus() newImage.size = destSize return NSImage(data: newImage.tiffRepresentation!)! } func getAccentedSymbol(_ named: String) -> NSImage? { if #available(OSX 10.14, *) { return getSymbol(named)?.tinting(with: .controlAccentColor) } else { // Fallback on earlier versions return getSymbol(named)?.tinting(with: .systemBlue) } } // This is a list of fallback symbols, until we can use those from SF Symbols 2, // we export from SF Symbols 1... private func fallbackSymbol(_ forName: String) -> String { switch forName { case "cloud": return "regular.cloud" case "sun.max": return "regular.sun.max" case "sun.min": return "regular.sun.min" case "moon.stars": return "regular.moon.stars" case "leaf": return "flame" case "dial.min": return "dial" case "internaldrive": return "arrow.down.circle" case "display.2": return "tv" case "wrench.and.screwdriver": return "wrench" default: return forName } } // Launch a process through shell and capture/return output func shell(launchPath: String, arguments: [String] = []) -> (String?, Int32) { let task = Process() task.launchPath = launchPath task.arguments = arguments let pipe = Pipe() task.standardOutput = pipe task.standardError = pipe if #available(OSX 10.13, *) { do { try task.run() } catch { // handle errors debugLog("Error: \(error.localizedDescription)") } } else { // A non existing command will crash 10.12 task.launch() } let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) task.waitUntilExit() return (output, task.terminationStatus) } func shell(_ command:String, args: [String] = []) -> String { let task = Process() var arguments = ["-c"] arguments.append(command) arguments += args task.launchPath = "/bin/bash" task.arguments = arguments let pipe = Pipe() task.standardOutput = pipe task.launch() let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) task.waitUntilExit() return output ?? "" /* let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: String.Encoding.utf8)! */ /*if output.count > 0 { //remove newline character. let lastIndex = output.index(before: output.endIndex) return String(output[output.startIndex ..< lastIndex]) }*/ //return output } // Launch a process through shell and capture/return output func shell(executableURL: String, arguments: [String] = []) -> (String?, Int32) { let task = Process() task.executableURL = URL(fileURLWithPath: executableURL) task.arguments = arguments let pipe = Pipe() task.standardOutput = pipe task.standardError = pipe if #available(OSX 10.13, *) { do { try task.run() } catch { // handle errors debugLog("Error: \(error.localizedDescription)") } } else { // A non existing command will crash 10.12 task.launch() } let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) task.waitUntilExit() return (output, task.terminationStatus) } /* func trySettings() { let date = Date() let dateFormatter = DateFormatter() dateFormatter.timeStyle = .long dateFormatter.dateStyle = .none let time = dateFormatter.string(from: date) let bundleID = "/Users/guillaume/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Preferences/com.glouel.synctest" // Test 1 CFPreferencesSetValue("underCompanion" as CFString, (underCompanion ? "under" : "notunder") as CFString, bundleID as CFString as CFString, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) CFPreferencesSetValue("lastRun" as CFString, time as CFString, bundleID as CFString as CFString, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) let val = CFPreferencesAppSynchronize(bundleID as CFString) print("value : " + String(val)) // Test 2 let bundleID2 = "/Users/guillaume/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Preferences/com.glouel.synctest2" let userDefaults = UserDefaults(suiteName: bundleID2) userDefaults?.setValue(time, forKey: "lastRun") userDefaults?.synchronize() userDefaults?.setValue(underCompanion ? "under" : "notunder", forKey: "underCompanion") userDefaults?.setValue(time, forKey: "lastRun") userDefaults?.synchronize() /*let (result, _) = shell(launchPath: "/usr/bin/defaults", arguments: ["read", "~/Library/Preferences/com.glouel.synctest","lastRun"]) debugLog(result!) print(result!)*/ }*/ func getPreferencesDirectory() -> String { // Grab an array of Application Support paths let libPaths = NSSearchPathForDirectoriesInDomains( .libraryDirectory, .userDomainMask, true) if !libPaths.isEmpty { if underCompanion { return libPaths.first! + "/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Preferences/" } else { return libPaths.first! + "/Preferences/" } } else { return "/Users/" + Aerial.helper.userName + "/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Preferences/" } } // Starting with 3.1.0beta2, existing settings are moved from Preferences/ByHost to Preferences // This allows the sharing of preferences between regular screen saver and companion-hosted screensaver func migratePreferences() { // First check if the new settings already exists ! let baseContainerPrefPath = getPreferencesDirectory() let newBundleFile = baseContainerPrefPath + "com.glouel.Aerial.plist" if FileManager.default.fileExists(atPath: newBundleFile) { // We are done logToConsole("!!! New prefs already exists") } else { logToConsole("!!! New prefs does NOT exist") //Look for ByHost let byHostPath = baseContainerPrefPath + "ByHost/" if FileManager.default.fileExists(atPath: byHostPath) { logToConsole("ByHost exists") var oldPlist = "" // Try and find the old plist do { let directoryContents = try FileManager.default.contentsOfDirectory(atPath: byHostPath) for directoryContent in directoryContents { if directoryContent.starts(with: "com.JohnCoates.Aerial") { // We found it ! oldPlist = directoryContent break } } } catch { logToConsole(error.localizedDescription) } // Did we get it ? if oldPlist != "" { logToConsole("plist found " + oldPlist) do { try FileManager.default.copyItem(atPath: byHostPath + oldPlist, toPath: newBundleFile) logToConsole("plist moved") } catch { logToConsole(error.localizedDescription) } } } } } // Mute me maybe func maybeMuteSound() { if !appMode && !underCompanion && PrefsAdvanced.muteGlobalSound{ Sound.output.isMuted = true } } func maybeUnmuteSound() { if !appMode && !underCompanion && PrefsAdvanced.muteGlobalSound { Sound.output.isMuted = false } } } ================================================ FILE: Aerial/Source/Models/AerialVideo.swift ================================================ // // AerialVideo.swift // Aerial // // Created by John Coates on 10/23/15. // Copyright © 2015 John Coates. All rights reserved. // import Cocoa import AVFoundation enum Manifests: String { case tvOS10 = "tvos10.json", tvOS11 = "tvos11.json", tvOS12 = "tvos12.json", tvOS13 = "tvos13.json", tvOS13Strings = "TVIdleScreenStrings13.bundle", customVideos = "customvideos.json" } final class AerialVideo: CustomStringConvertible, Equatable { static func ==(lhs: AerialVideo, rhs: AerialVideo) -> Bool { return lhs.id == rhs.id // TODO && lhs.url1080pHEVC == rhs.url1080pHEVC } let id: String let name: String let secondaryName: String let type: String let timeOfDay: String let scene: SourceScene var urls: [VideoFormat: String] let source: Source // var sources: [Manifests] let poi: [String: String] let communityPoi: [String: String] var duration: Double var arrayPosition = 1 var contentLength = 0 var contentLengthChecked = false var isVertical: Bool var isAvailableOffline: Bool { return VideoCache.isAvailableOffline(video: self) } // MARK: - Public getter var url: URL { return getClosestAvailable(wanted: PrefsVideos.videoFormat) } // Returns the closest video we have in the manifests private func getClosestAvailable(wanted: VideoFormat) -> URL { if urls[wanted] != "" && urls[wanted] != nil { return getURL(string: urls[wanted]!) } else { // Fallback if urls.keys.contains(.v4KHEVC), urls[.v4KHEVC] != "" { return getURL(string: urls[.v4KHEVC]!) } else if urls.keys.contains(.v4KSDR240), urls[.v4KSDR240] != "" { // macOS manifest only have those return getURL(string: urls[.v4KSDR240]!) } else if urls.keys.contains(.v1080pHEVC), urls[.v1080pHEVC] != "" { return getURL(string: urls[.v1080pHEVC]!) } else if urls.keys.contains(.v1080pH264), urls[.v1080pH264] != "" { // Last resort return getURL(string: urls[.v1080pH264]!) } else { errorLog("getClosestAvailable failed back hard to 4KHDR") // Something went very wrong if we are here return getURL(string: urls[.v4KHDR]!) } } } private func getURL(string: String) -> URL { if string.starts(with: "/") { return URL(fileURLWithPath: string) } else { return URL(string: string)! } } // swiftlint:disable cyclomatic_complexity // MARK: - Init init(id: String, name: String, secondaryName: String, type: String, timeOfDay: String, scene: String, urls: [VideoFormat: String], source: Source, poi: [String: String], communityPoi: [String: String] ) { self.id = id // We override names for known space videos if SourceInfo.seaVideos.contains(id) { self.name = "Sea" if secondaryName != "" { self.secondaryName = secondaryName } else { self.secondaryName = name } } else if SourceInfo.spaceVideos.contains(id) { self.name = "Space" if secondaryName != "" { self.secondaryName = secondaryName } else { self.secondaryName = name } } else { // We align to the new jsons... if name == "New York City" { self.name = "New York" } else { self.name = name } self.secondaryName = secondaryName // We may have a secondary name from our merges too now ! } self.type = type // We override timeOfDay based on our own list if let val = SourceInfo.timeInformation[id] { self.timeOfDay = val } else { self.timeOfDay = timeOfDay } switch scene { case "sea": self.scene = .sea case "space": self.scene = .space case "city": self.scene = .city case "countryside": self.scene = .countryside case "beach": self.scene = .beach default: self.scene = .nature } self.urls = urls self.source = source // self.sources = [manifest] self.poi = poi self.communityPoi = communityPoi // Default stuff, we double check those below self.duration = 0 self.isVertical = false updateDuration() // We need to have the video duration } func updateDuration() { // We need to retrieve video duration from the cached files. // This is a workaround as currently, the VideoCache infrastructure // relies on AVAsset with an external URL all the time, even when // working on a cached copy which makes the native duration retrieval fail // // And... we also check the orientation now too ;) let fileManager = FileManager.default if let duration = PrefsVideos.durationCache[self.id] { // debugLog("Using cache duration : \(duration)") self.duration = duration return } // With custom videos, we may already store the local path // If so, check it if self.url.absoluteString.starts(with: "file") { if fileManager.fileExists(atPath: self.url.path) { let asset = AVAsset(url: self.url) self.duration = CMTimeGetSeconds(asset.duration) self.isVertical = asset.isVertical() } else { errorLog("Custom video is missing : \(self.url.path)") self.duration = 0 } } else { // If not, iterate through all possible versions to see if any is cached for format in VideoFormat.allCases { // swiftlint:disable:next for_where if urls[format] != "" { let path = VideoList.instance.localPathFor(video: self) if fileManager.fileExists(atPath: path) { let asset = AVAsset(url: URL(fileURLWithPath: path)) self.duration = CMTimeGetSeconds(asset.duration) // debugLog("Caching video duration") PrefsVideos.durationCache[self.id] = self.duration return } } } } } /// Check if a video has HDR files or not func hasHDR() -> Bool { if urls[.v1080pHDR] != "" || urls[.v4KHDR] != "" { return true } else { return false } } /// Check if what we are playing is HDR or not func isHDR() -> Bool { if urls[.v1080pHDR] != "" { if url == URL(string: urls[.v1080pHDR]!) { return true } } if urls[.v4KHDR] != "" { if url == URL(string: urls[.v4KHDR]!) { return true } } return false } func getCurrentFormat() -> String { let wanted = PrefsVideos.videoFormat if urls[wanted] != "" { switch wanted { case .v4KHDR: return "4K HDR" case .v1080pH264: return "1080p" case .v1080pHEVC: return "1080p" case .v1080pHDR: return "1080p HDR" case .v4KHEVC: return "4K" case .v4KSDR240: return "4K 240FPS" } } else { return getBestFormat() } } private func getBestFormat() -> String { if urls[.v4KHDR] != "" { return "4K HDR" } else if urls[.v4KHEVC] != "" { return "4K" } else { return "1080p" } } var description: String { return """ id=\(id), name=\(name), type=\(type), timeofDay=\(timeOfDay), urls=\(urls) """ } } ================================================ FILE: Aerial/Source/Models/Cache/AssetLoaderDelegate.swift ================================================ // // AssetLoaderDelegate.swift // Aerial // // This class adapted from https://github.com/renjithn/AVAssetResourceLoader-Video-Example import Foundation import AVKit import AVFoundation /// Returns an AVURLAsset that is automatically cached. If already cached /// 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 } final class AssetLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, VideoLoaderDelegate { let URL: Foundation.URL var videoLoaders: [VideoLoader] = [] let videoCache: VideoCache var URLWithCustomScheme: Foundation.URL { var components = URLComponents(url: URL, resolvingAgainstBaseURL: false)! components.scheme = "streaming" return components.url! } init(URL: Foundation.URL) { self.URL = URL videoCache = VideoCache(URL: URL) } deinit { debugLog("AssetLoaderDelegate deinit") } // MARK: - Video Loader Delegate func videoLoader(_ videoLoader: VideoLoader, receivedResponse response: URLResponse) { videoCache.receivedContentLength(Int(response.expectedContentLength)) } func videoLoader(_ videoLoader: VideoLoader, receivedData data: Data, forRange range: NSRange) { videoCache.receivedData(data, atRange: range) } // MARK: - Asset Resource Loader Delegate func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) { // debugLog("cancelled load request: \(loadingRequest)") var remove: VideoLoader? for loader in videoLoaders { if loader.loadingRequest != loadingRequest { continue } if let connection = loader.connection { connection.cancel() } remove = loader break } if let removeLoader = remove { if let index = videoLoaders.firstIndex(of: removeLoader) { videoLoaders.remove(at: index) } } } 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 } } ================================================ FILE: Aerial/Source/Models/Cache/Cache.swift ================================================ // // Cache.swift // Aerial // // Created by Guillaume Louel on 06/06/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa import CoreWLAN import AVKit /** Aerial's new Cache management Everything Cache related is moved here. - Note: Where is our cache ? Starting with 2.0, Aerial is putting its files in two locations : - `~/Library/Application Support/Aerial/` : Contains manifests files and strings bundles for each source, in their own directory - `~/Library/Application Support/Aerial/Cache/` : Contains (only) the cached videos Users of version 1.x.x will automatically see their video files migrated to the correct location. In Catalina, those paths live inside a user's container : `~/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Application Support/` - Attention: Shared by multiple users writable locations are no longer possible, because sandboxing is awesome ! */ // swiftlint:disable:next type_body_length struct Cache { /** Returns the SSID of the Wi-Fi network the user is currently connected to. - Note: Returns an empty string if not connected to Wi-Fi */ static var ssid: String { return CWWiFiClient.shared().interface(withName: nil)?.ssid() ?? "" } static var processedSupportPath = "" /** Returns Aerial's Application Support path. + On macOS 10.14 and earlier : `~/Library/Application Support/Aerial/` + Starting with 10.15 : `~/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Application Support/Aerial/` - Note: Returns `/` on failure. In some rare instances those system folders may not exist in the container, in this case Aerial can't work. */ static var supportPath: String { // Dont' redo the thing all the time if processedSupportPath != "" { return processedSupportPath } var appPath = "" if PrefsCache.overrideCache { debugLog("Cache Override") if !Aerial.helper.underCompanion, #available(macOS 12, *) { if let bookmarkData = PrefsCache.supportBookmarkData { do { var isStale = false let bookmarkUrl = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale) //debugLog("Bookmark is stale : \(isStale)") appPath = bookmarkUrl.path do { let url = try NSURL.init(resolvingBookmarkData: bookmarkData, options: .withoutUI, relativeTo: nil, bookmarkDataIsStale: nil) url.startAccessingSecurityScopedResource() } catch let error as NSError { errorLog("Bookmark Access Failed: \(error.description)") } } catch let error { errorLog("Can't process bookmark \(error)") } } else { errorLog("Can't find supportBookmarkData on macOS 12") } } else { if let customPath = PrefsCache.supportPath { debugLog("Trying \(customPath)") if FileManager.default.fileExists(atPath: customPath) { appPath = customPath } else { errorLog("Could not find your custom Caches path, reverting to default settings") } } else { errorLog("Empty path, reverting to default settings") } } } // This is the normal(ish) path if appPath == "" { // This is the normal path via screensaver if !Aerial.helper.underCompanion { // Grab an array of Application Support paths let appSupportPaths = NSSearchPathForDirectoriesInDomains( .applicationSupportDirectory, .userDomainMask, true) if appSupportPaths.isEmpty { errorLog("FATAL : app support does not exist!") return "/" } appPath = appSupportPaths[0] } else { // If we are underCompanion, we need to add the container on 10.15+ // Grab an array of Application Support paths if #available(OSX 10.15, *) { let libPaths = NSSearchPathForDirectoriesInDomains( .libraryDirectory, .userDomainMask, true) appPath = libPaths.first! + "/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Application Support/" } else { let appSupportPaths = NSSearchPathForDirectoriesInDomains( .applicationSupportDirectory, .userDomainMask, true) if appSupportPaths.isEmpty { errorLog("FATAL : app support does not exist!") return "/" } appPath = appSupportPaths[0] } } } let appSupportDirectory = appPath as NSString if aerialFolderExists(at: appSupportDirectory) { processedSupportPath = appSupportDirectory.appendingPathComponent("Aerial") return processedSupportPath } else { debugLog("Creating app support directory...") let asPath = appSupportDirectory.appendingPathComponent("Aerial") let fileManager = FileManager.default do { try fileManager.createDirectory(atPath: asPath, withIntermediateDirectories: true, attributes: nil) processedSupportPath = asPath return asPath } catch let error { errorLog("FATAL : Couldn't create app support directory in User directory: \(error)") return "/" } } } /** Returns Aerial's Caches path. + On macOS 10.14 and earlier : `~/Library/Application Support/Aerial/Cache/` + Starting with 10.15 : `~/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Application Support/Aerial/Cache/` - Note: Returns `/` on failure. In some rare instances those system folders may not exist in the container, in this case Aerial can't work. Also note that the shared `Caches` folder, `/Library/Caches/Aerial/`, is no longer user writable in Catalina and will be ignored. */ static var path: String = { var path = "" /*if PrefsCache.overrideCache { if #available(macOS 12, *) { if let bookmarkData = PrefsCache.cacheBookmarkData { do { var isStale = false let bookmarkUrl = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale) debugLog("Bookmark is stale : \(isStale)") debugLog("\(bookmarkUrl)") path = bookmarkUrl.path debugLog("\(path)") } catch { errorLog("Can't process bookmark") } } else { errorLog("Can't find cacheBookmarkData on macOS 12") } } else { if let customPath = Preferences.sharedInstance.customCacheDirectory { debugLog("Trying \(customPath)") if FileManager.default.fileExists(atPath: customPath) { path = customPath } else { errorLog("Could not find your custom Caches path, reverting to default settings") } } else { errorLog("Empty path, reverting to default settings") } } if path == "" { PrefsCache.overrideCache = false path = Cache.supportPath.appending("/Cache") } } else {*/ path = Cache.supportPath.appending("/Cache") // } if FileManager.default.fileExists(atPath: path as String) { return path } else { do { try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) return path } catch let error { errorLog("FATAL : Couldn't create Cache directory in Aerial's AppSupport directory: \(error)") return "/" } } }() static var pathUrl: URL = { if #available(macOS 12, *) { if PrefsCache.overrideCache { if let bookmarkData = PrefsCache.cacheBookmarkData { do { var isStale = false let bookmarkUrl = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale) debugLog("Bookmark is stale : \(isStale)") debugLog("\(bookmarkUrl)") return bookmarkUrl } catch { errorLog("Can't process bookmark") } } } } return URL(fileURLWithPath: path) }() /** Returns Aerial's thumbnail cache path, creating it if needed. + On macOS 10.14 and earlier : `~/Library/Application Support/Aerial/Thumbnails/` + Starting with 10.15 : `~/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Application Support/Aerial/Thumbnails/` - Note: Returns `/` on failure. */ static var thumbnailsPath: String = { let path = Cache.supportPath.appending("/Thumbnails") if FileManager.default.fileExists(atPath: path as String) { return path } else { do { try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) return path } catch let error { errorLog("FATAL : Couldn't create Thumbnails directory in Aerial's AppSupport directory: \(error)") return "/" } } }() /** Returns Aerial's former cache path, if it exists. + On macOS 10.14 and earlier : `~/Library/Caches/Aerial/` + Starting with 10.15 : `~/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Caches/Aerial/` - Note: Returns `nil` on failure. */ static private var formerCachePath: String? = { // Grab an array of Cache paths let cacheSupportPaths = NSSearchPathForDirectoriesInDomains( .cachesDirectory, .userDomainMask, true) if cacheSupportPaths.isEmpty { errorLog("Couldn't find Caches paths!") return nil } let cacheSupportDirectory = cacheSupportPaths[0] as NSString if aerialFolderExists(at: cacheSupportDirectory) { return cacheSupportDirectory.appendingPathComponent("Aerial") } else { do { debugLog("trying to create \(cacheSupportDirectory.appendingPathComponent("Aerial"))") try FileManager.default.createDirectory(atPath: cacheSupportDirectory.appendingPathComponent("Aerial"), withIntermediateDirectories: true, attributes: nil) return path } catch { errorLog("Could not create Aerial's Caches path") } return nil } }() // MARK: - Migration from Aerial 1.x.x to 2.x.x /** Migrate files from previous versions of Aerial to the 2.x.x structure. - Moves the video files from Application Support to the `Application Support/Aerial/Cache` sub directory. - Moves the video files from Caches to the `Application Support/Aerial/Cache` sub directory */ static func migrate() { if !PrefsCache.overrideCache { migrateAppSupport() migrateOldCache() } } /** Migrate video that may be at the root of /Application Support/Aerial/ */ static private func migrateAppSupport() { let supportURL = URL(fileURLWithPath: supportPath as String) do { let directoryContent = try FileManager.default.contentsOfDirectory(at: supportURL, includingPropertiesForKeys: nil) let videoURLs = directoryContent.filter { $0.pathExtension == "mov" } if !videoURLs.isEmpty { debugLog("Starting migration of your video files from Application Support to the /Cache subfolder") for videoURL in videoURLs { debugLog("moving \(videoURL.lastPathComponent)") let newURL = URL(fileURLWithPath: path.appending("/\(videoURL.lastPathComponent)")) try FileManager.default.moveItem(at: videoURL, to: newURL) } debugLog("Migration done.") } } catch { errorLog("Error during migration, please report") errorLog(error.localizedDescription) } } /** Migrate video that may be at the root of a user's /Caches/Aerial/ */ static private func migrateOldCache() { if let formerCachePath = formerCachePath { do { let formerCacheURL = URL(fileURLWithPath: formerCachePath as String) let directoryContent = try FileManager.default.contentsOfDirectory(at: formerCacheURL, includingPropertiesForKeys: nil) let videoURLs = directoryContent.filter { $0.pathExtension == "mov" } if !videoURLs.isEmpty { debugLog("Starting migration of your video files from Caches to the /Cache subfolder of Application Support") for videoURL in videoURLs { debugLog("moving \(videoURL.lastPathComponent)") let newURL = URL(fileURLWithPath: path.appending("/\(videoURL.lastPathComponent)")) try FileManager.default.moveItem(at: videoURL, to: newURL) } debugLog("Migration done.") } } catch { errorLog("Error during migration, please report") errorLog(error.localizedDescription) } } } // Remove files in bad format or outdated static func removeCruft() { // TODO: kind of a temporary safety if VideoList.instance.videos.count > 90 { // First let's look at the cache // let pathURL = URL(fileURLWithPath: path) do { guard pathUrl.startAccessingSecurityScopedResource() else { errorLog("removeCruft couldn't access scoped resouce") return } let directoryContent = try FileManager.default.contentsOfDirectory(at: pathUrl, includingPropertiesForKeys: nil) debugLog("count : \(directoryContent.count)") let videoURLs = directoryContent.filter { $0.pathExtension == "mov" } for video in videoURLs { let filename = video.lastPathComponent debugLog("\(filename)") var found = false // swiftlint:disable for_where for candidate in VideoList.instance.videos { if candidate.url.lastPathComponent == filename { found = true } } if !found { debugLog("This file is not in the correct format or outdated, removing : \(video)") try? FileManager.default.removeItem(at: video) } } pathUrl.stopAccessingSecurityScopedResource() } catch { errorLog("Error during removing of videos in wrong format, please report") errorLog(error.localizedDescription) } // Also remove uncached cruft removeUncachedCruft() } } static func removeUncachedCruft() { for source in SourceList.foundSources where !source.isCachable && source.type != .local { debugLog("Checking cruft in \(source.name)") let pathURL = URL(fileURLWithPath: supportPath.appending("/" + source.name)) let unprocessed = source.getUnprocessedVideos() debugLog(pathURL.absoluteString) do { let directoryContent = try FileManager.default.contentsOfDirectory(at: pathURL, includingPropertiesForKeys: nil) let videoURLs = directoryContent.filter { $0.pathExtension == "mov" } for video in videoURLs { let filename = video.lastPathComponent var found = false // swiftlint:disable for_where for candidate in unprocessed { if candidate.url.lastPathComponent == filename { found = true } } if !found { debugLog("This file is not in the correct format or outdated, removing : \(video)") try? FileManager.default.removeItem(at: video) } } } catch { errorLog("Error during removal of videos in wrong format, please report") errorLog(error.localizedDescription) } } } /// This clears the whole cache. User beware ! static func clearCache() { let pathURL = URL(fileURLWithPath: path) do { let directoryContent = try FileManager.default.contentsOfDirectory(at: pathURL, includingPropertiesForKeys: nil) let videoURLs = directoryContent.filter { $0.pathExtension == "mov" } for video in videoURLs { try? FileManager.default.removeItem(at: video) } } catch { errorLog("Error during removal of videos in wrong format, please report") errorLog(error.localizedDescription) } } static func clearNonCacheableSources() { // Then we need to look at individual online sources // let onlineVideos = VideoList.instance.videos.filter({ !$0.source.isCachable }) for source in SourceList.foundSources.filter({!$0.isCachable}) { let pathSource = URL(fileURLWithPath: supportPath).appendingPathComponent(source.name) if FileManager.default.fileExists(atPath: pathSource.path) { do { let directoryContent = try FileManager.default.contentsOfDirectory(at: pathSource, includingPropertiesForKeys: nil) let videoURLs = directoryContent.filter { $0.pathExtension == "mov" } for video in videoURLs { debugLog("Removing file : \(video)") try? FileManager.default.removeItem(at: video) } } catch { errorLog("Error during removing of videos in wrong format, please report") errorLog(error.localizedDescription) } } } } // MARK: - About the cache /** Is our cache full ? */ static func isFull() -> Bool { return size() > PrefsCache.cacheLimit } /** Do we still have a bit of free space (0.5 GB) */ static func hasSomeFreeSpace() -> Bool { return size() < PrefsCache.cacheLimit - 0.5 } /** Returns the cache size in GB as a string (eg. 5.1 GB) */ static func sizeString() -> String { let pathURL = Foundation.URL(fileURLWithPath: path) // check if the url is a directory if (try? pathURL.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true { var folderSize = 0 (FileManager.default.enumerator(at: pathURL, 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) ?? "" return sizeToDisplay } // In case it fails somehow return "No cache found" } // MARK: - Helpers /** Does an `/Aerial/` subfolder exist inside the given path - parameter at: Source path - returns: Path existance as a Bool. */ private static func aerialFolderExists(at: NSString) -> Bool { let aerialFolder = at.appendingPathComponent("Aerial") return FileManager.default.fileExists(atPath: aerialFolder as String) } /** Returns cache size in GB */ static func size() -> Double { let pathURL = URL(fileURLWithPath: path) // check if the url is a directory if (try? pathURL.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true { var folderSize = 0 (FileManager.default.enumerator(at: pathURL, includingPropertiesForKeys: nil)?.allObjects as? [URL])?.lazy.forEach { folderSize += (try? $0.resourceValues(forKeys: [.totalFileAllocatedSizeKey]))?.totalFileAllocatedSize ?? 0 } return Double(folderSize) / 1000000000 } return 0 } static func getDirectorySize(directory: String) -> Double { if FileManager.default.fileExists(atPath: directory) { let pathURL = URL(fileURLWithPath: directory) // check if the url is a directory if (try? pathURL.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true { var folderSize = 0 (FileManager.default.enumerator(at: pathURL, includingPropertiesForKeys: nil)?.allObjects as? [URL])?.lazy.forEach { folderSize += (try? $0.resourceValues(forKeys: [.totalFileAllocatedSizeKey]))?.totalFileAllocatedSize ?? 0 } return Double(folderSize) / 1000000000 } return 0 } else { return 0 } } static func packsSize() -> Double { var totalSize: Double = 0 for source in SourceList.foundSources where !source.isCachable { let sourcePath = supportPath.appending("/" + source.name) totalSize += getDirectorySize(directory: sourcePath) } return totalSize } // swiftlint:disable line_length // MARK: Networking restrictions for cache /** Can we download a file ? Depending on user's settings, the cache may be full or the user may not be on a trusted network. - Note: If a user disabled cache management (full manual mode), this will always be true. - parameter action: A closure with the action to be accomplished should the conditions be met. */ static func ensureDownload(action: @escaping () -> Void) { // Do we manage the cache or not ? if PrefsCache.enableManagement { // Check network first if !canNetwork() { if !Aerial.helper.showAlert(question: "You are on a restricted WiFi network", text: "Your current settings restrict downloads when not connected to a trusted network. Do you wish to proceed?\n\nReminder: You can change this setting in the Cache tab.", button1: "Download Anyway", button2: "Cancel") { return } } // Then cache status if isFull() { let formatter = NumberFormatter() formatter.locale = Locale.current // USA: Locale(identifier: "en_US") formatter.numberStyle = .decimal let result = formatter.string(from: NSNumber(value: PrefsCache.cacheLimit.rounded(toPlaces: 1)))! //print(result) // -> US$9,999.99 if !Aerial.helper.showAlert(question: "Your cache is full", text: "Your cache limit is currently set to \(result) GB, and currently contains \(Cache.sizeString()) of files.\n\n Do you want to proceed with the download anyway?\n\nYou can manually increase or decrease your cache size in Settings > Cache.", button1: "Download Anyway", button2: "Cancel") { return } } } // If all is fine then proceed action() } /** Can we safely use network ? Depending on user's settings, they may not be on a trusted network. - Note: If a user disabled cache management (full manual mode), this will always be true. */ static func canNetwork() -> Bool { if !PrefsCache.enableManagement { return true } if PrefsCache.restrictOnWiFi { // If we are not connected to WiFi we allow if Cache.ssid == "" || PrefsCache.allowedNetworks.contains(ssid) { return true } else { return false } } else { return true } } static func outdatedVideos() -> [AerialVideo] { guard PrefsCache.enableManagement else { return [] } var cutoffDate = Date() switch PrefsCache.cachePeriodicity { case .daily: cutoffDate = Calendar.current.date(byAdding: .day, value: -1, to: cutoffDate)! case .weekly: cutoffDate = Calendar.current.date(byAdding: .day, value: -7, to: cutoffDate)! case .monthly: cutoffDate = Calendar.current.date(byAdding: .month, value: -1, to: cutoffDate)! case .never: return [] } // Get a list of cached videos that are not favorites, and are from a cacheable source (not a pack) // Yes this is getting a bit complicated var evictable: [Date: AerialVideo] = [:] let currentlyCached = VideoList.instance.videos.filter({ $0.isAvailableOffline && $0.source.isCachable && !PrefsVideos.favorites.contains($0.id)}) for video in currentlyCached { let path = VideoCache.cachePath(forVideo: video)! // swiftlint:disable:next force_try let attributes = try! FileManager.default.attributesOfItem(atPath: path) let creationDate = attributes[.creationDate] as! Date if creationDate < cutoffDate { evictable[creationDate] = video } } return evictable.sorted { $0.key < $1.key }.map({ $0.value }) } // swiftlint:disable:next cyclomatic_complexity static func freeCache() { guard PrefsCache.enableManagement else { return } // Step 1 : Delete hidden videos debugLog("Looking for hidden videos to delete...") for video in VideoList.instance.videos.filter({ PrefsVideos.hidden.contains($0.id) && $0.isAvailableOffline }) { debugLog("Deleting hidden video \(video.secondaryName)") do { let path = VideoList.instance.localPathFor(video: video) try FileManager.default.removeItem(atPath: path) } catch { errorLog("Could not delete video : \(video.secondaryName)") } } // We may be good ? if hasSomeFreeSpace() { return } // Step 2 : Delete videos that are out of rotation let evictables = outdatedVideos() if evictables.isEmpty { debugLog("No outdated videos, we won't delete anything") return } debugLog("Looking for outdated videos that aren't in rotation (candidates : \(evictables.count)") outerLoop: for video in evictables { if VideoList.instance.currentRotation().contains(video) { // Outdated but in rotation, so keep it ! // debugLog("outdated but in rotation \(video.secondaryName)") } else { debugLog("Removing outdated video not in rotation \(video.secondaryName)") do { try FileManager.default.removeItem(atPath: VideoCache.cachePath(forVideo: video)!) } catch { errorLog("Could not delete video : \(video.secondaryName)") } if hasSomeFreeSpace() { // Removed enough break outerLoop } } } // Are we there yeeeet ? if hasSomeFreeSpace() { return } debugLog("Looking for outdated videos that may still be in rotation (candidates : \(evictables.count)") var currentVideos = [AerialVideo]() for view in AerialView.instanciatedViews { if let video = view.currentVideo { currentVideos.append(video) } } outerLoop2: for video in evictables { if currentVideos.contains(video) { debugLog("\(video.secondaryName) is currently playing, trying another") } else { debugLog("Removing outdated video that was in rotation \(video.secondaryName)") do { try FileManager.default.removeItem(atPath: VideoCache.cachePath(forVideo: video)!) } catch { errorLog("Could not delete video : \(video.secondaryName)") } if hasSomeFreeSpace() { // Removed enough break outerLoop2 } } } // At this point we can't do more } static func fillOrRollCache() { guard PrefsCache.enableManagement && canNetwork() else { return } // Grab a *shuffled* list of uncached in rotation videos let rotation = VideoList.instance.currentRotation().filter { !$0.isAvailableOffline }.shuffled() if rotation.isEmpty { debugLog("> Current playlist is already fully cached, no download/rotation needed") return } debugLog("> Fill or roll cache") // Do we have some space to download at least a video (by default .5 GB) ? if !hasSomeFreeSpace() { freeCache() if !hasSomeFreeSpace() { debugLog("No free space to reclaim currently.") return } } debugLog("Uncached videos in rotation : \(rotation.count)") // We may be satisfied already if rotation.isEmpty { return } // Queue the first video on the list debugLog("Queuing video : \(rotation.first!.secondaryName)") VideoManager.sharedInstance.queueDownload(rotation.first!) } } ================================================ FILE: Aerial/Source/Models/Cache/PoiStringProvider.swift ================================================ // // PoiStringProvider.swift // Aerial // // Created by Guillaume Louel on 13/10/2018. // Copyright © 2018 John Coates. All rights reserved. // import Foundation final 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 } } final class PoiStringProvider { static let sharedInstance = PoiStringProvider() var loadedDescriptions = false var loadedDescriptionsWasLocalized = false var stringBundle: Bundle? var stringDict: [String: String]? var communityStrings = [CommunityStrings]() var communityLanguage = "" // MARK: - Lifecycle init() { debugLog("Poi Strings Provider initialized") loadBundle() loadCommunity() } // MARK: - Bundle management private func getBundleLanguages() -> [String] { // Might want to improve that... // This is a static list of what's supposed to be in the bundle // swiftlint:disable:next line_length return ["de", "he", "en_AU", "ar", "el", "ja", "en", "uk", "es_419", "zh_CN", "es", "pt_BR", "da", "it", "sk", "pt_PT", "ms", "sv", "cs", "ko", "no", "hu", "zh_HK", "tr", "pl", "zh_TW", "en_GB", "vi", "ru", "fr_CA", "fr", "fi", "id", "nl", "th", "pt", "ro", "hr", "hi", "ca"] } private func loadBundle() { // Idle string bundle var bundlePath = Cache.supportPath.appending("/macOS 26") if PrefsAdvanced.ciOverrideLanguage == "" { debugLog("Preferred languages : \(Locale.preferredLanguages)") let bestMatchedLanguage = Bundle.preferredLocalizations(from: getBundleLanguages(), forPreferences: Locale.preferredLanguages).first if let match = bestMatchedLanguage { debugLog("Best matched language : \(match)") bundlePath.append(contentsOf: "/TVIdleScreenStrings.bundle/" + match + ".lproj/") } else { debugLog("No match, reverting to english") // We load the bundle and let system grab the closest available preferred language // This no longer works in Catalina and defaults back to english // as legacyScreenSaver.appex, our new "mainbundle" is english only bundlePath.append(contentsOf: "/TVIdleScreenStrings.bundle") } } else { let bestMatchedLanguage = Bundle.preferredLocalizations(from: getBundleLanguages(), forPreferences: [PrefsAdvanced.ciOverrideLanguage]).first if let match = bestMatchedLanguage { debugLog("Best matched language : \(match)") bundlePath.append(contentsOf: "/TVIdleScreenStrings.bundle/" + match + ".lproj/") } else { debugLog("No match, reverting to english") // We load the bundle and let system grab the closest available preferred language // This no longer works in Catalina and defaults back to english // as legacyScreenSaver.appex, our new "mainbundle" is english only bundlePath.append(contentsOf: "/TVIdleScreenStrings.bundle") } /*debugLog("Language overriden to \(String(describing: bestMatchedLanguage))") // Or we load the overriden one bundlePath.append(contentsOf: "/TVIdleScreenStrings.bundle/" + PrefsAdvanced.ciOverrideLanguage + ".lproj/")*/ } if let sb = Bundle.init(path: bundlePath) { let dictPath = Cache.supportPath.appending("/macOS 26/TVIdleScreenStrings.bundle/en.lproj/Localizable.nocache.strings") // We could probably only work with that... if let sd = NSDictionary(contentsOfFile: dictPath) as? [String: String] { self.stringDict = sd } self.stringBundle = sb self.loadedDescriptions = true } else { errorLog("TVIdleScreenStrings.bundle is missing, please remove entries.json in Cache folder to fix the issue") } } // Make sure we have the correct bundle loaded private func ensureLoadedBundle() -> Bool { if loadedDescriptions { return true } else { loadBundle() return loadedDescriptions } } // Return the Localized (or english) string for a key from the Strings Bundle func getString(key: String, video: AerialVideo) -> String { guard ensureLoadedBundle() else { return "" } /*let preferences = Preferences.sharedInstance let locale: NSLocale = NSLocale(localeIdentifier: Locale.preferredLanguages[0]) if #available(OSX 10.12, *) { if preferences.localizeDescriptions && locale.languageCode != communityLanguage && preferences.ciOverrideLanguage == "" { return stringBundle!.localizedString(forKey: key, value: "", table: "Localizable.nocache") } }*/ if !video.communityPoi.isEmpty { 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 func fetchExtraPoiForId(id: String) -> [String: String]? { guard let stringDict = stringDict, ensureLoadedBundle() else { return [:] } var found = [String: String]() for key in stringDict.keys where key.starts(with: id) { found[String(key.split(separator: "_").last!)] = key // FIXME: crash if key doesn't have "_" } return found } // func getPoiKeys(video: AerialVideo) -> [String: String] { if !video.communityPoi.isEmpty { return video.communityPoi } else { return video.poi } } // Do we have any keys, anywhere, for said video ? func hasPoiKeys(video: AerialVideo) -> Bool { return (!video.poi.isEmpty && loadedDescriptions) || (!video.communityPoi.isEmpty && !getPoiKeys(video: video).isEmpty) } func getLocalizedNameKey(key: String) -> String { guard ensureLoadedBundle() else { return "" } return stringBundle!.localizedString(forKey: key, value: "", table: "Localizable.nocache") } // MARK: - Community data // siftlint:disable:next cyclomatic_complexity private func getCommunityPathForLocale() -> String { let locale: NSLocale = NSLocale(localeIdentifier: Locale.preferredLanguages[0]) // Do we have a language override ? if PrefsAdvanced.ciOverrideLanguage != "" { let path = Bundle(for: PoiStringProvider.self).path(forResource: PrefsAdvanced.ciOverrideLanguage, ofType: "json") if path != nil { let fileManager = FileManager.default if fileManager.fileExists(atPath: path!) { debugLog("Community Language overriden to : \(PrefsAdvanced.ciOverrideLanguage)") communityLanguage = PrefsAdvanced.ciOverrideLanguage return path! } } } if #available(OSX 10.12, *) { // First we look in the Cache Folder for a locale directory let cacheDirectory = Cache.supportPath var cacheResourcesString = cacheDirectory cacheResourcesString.append(contentsOf: "/locale") let cacheUrl = URL(fileURLWithPath: cacheResourcesString) if cacheUrl.hasDirectoryPath { debugLog("Aerial cache directory contains /locale") let cc = locale.languageCode debugLog("Looking for \(cc).json") let fileUrl = URL(fileURLWithPath: cacheResourcesString.appending("/\(cc).json")) debugLog(fileUrl.absoluteString) let fileManager = FileManager.default if fileManager.fileExists(atPath: fileUrl.path) { debugLog("Locale description found") communityLanguage = cc return fileUrl.path } else { debugLog("Locale description not found") } } debugLog("Defaulting to bundle") let cc = locale.languageCode let path = Bundle(for: PoiStringProvider.self).path(forResource: cc, ofType: "json") if path != nil { let fileManager = FileManager.default if fileManager.fileExists(atPath: path!) { communityLanguage = cc return path! } } } // Fallback to english in bundle communityLanguage = "en" return Bundle(for: PoiStringProvider.self).path(forResource: "en", ofType: "json")! } // Load the community strings private func loadCommunity() { let bundlePath = getCommunityPathForLocale() 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 } for item in batch { let id = item.key as! String let name = (item.value as! NSDictionary)["name"] as! String let poi = (item.value as! NSDictionary)["pointsOfInterest"] as? [String: String] communityStrings.append(CommunityStrings(id: id, name: name, poi: poi ?? [:])) } } catch { // handle error errorLog("Community JSON ERROR : \(error)") } debugLog("Community JSON : \(communityStrings.count) entries") } func getCommunityName(id: String) -> String? { return communityStrings.first(where: { $0.id == id }).map { $0.name } } func getCommunityPoi(id: String) -> [String: String] { return communityStrings.first(where: { $0.id == id }).map { $0.poi } ?? [:] } // Helpers for the main popup // swiftlint:disable:next cyclomatic_complexity func getLanguagePosition() -> Int { // The list is alphabetized based on their english name in the UI switch PrefsAdvanced.ciOverrideLanguage { case "ar": // Arabic return 1 case "zh_CN": // Chinese Simplified return 2 case "zh_TW": // Chinese Traditional return 3 case "nl": // Dutch return 4 case "en": // English return 5 case "fr": // French return 6 case "de": // German return 7 case "he": // Hebrew return 8 case "hu": // Hungarian return 9 case "it": // Italian return 10 case "ja": // Japanese return 11 case "ko": // Korean return 12 case "pl": // Polish return 13 case "pt": // Portuguese return 14 case "pt_BR": // Portuguese (Brazil) return 15 case "ru": // Russian return 16 case "es": // Spanish return 17 case "sv": // Swedish return 18 case "tl": // Tagalog return 19 default: // This is the default, preferred language return 0 } } // swiftlint:disable:next cyclomatic_complexity func getLanguageStringFromPosition(pos: Int) -> String { switch pos { case 1: return "ar" case 2: return "zh_CN" case 3: return "zh_TW" case 4: return "nl" case 5: return "en" case 6: return "fr" case 7: return "de" case 8: return "he" case 9: return "hu" case 10: return "it" case 11: return "ja" case 12: return "ko" case 13: return "pl" case 14: return "pt" case 15: return "pt_BR" case 16: return "ru" case 17: return "es" case 18: return "sv" case 19: return "tl" default: return "" } } } ================================================ FILE: Aerial/Source/Models/Cache/Thumbnails.swift ================================================ // // Thumbnails.swift // Aerial // // Created by Guillaume Louel on 20/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa import AVKit struct Thumbnails { static let thumbSize = CGSize.init(width: 192, height: 108) /** Generate thumbnails for the videos When a video is not available offline, it will also save a larger version of the first frame of the video, to be used later in the UI as a placeholder */ /* static func generateAllThumbnails(forVideos videos: [AerialVideo]) { print("starting thumb generation") for video in videos { if cached(forVideo: video) == nil { DispatchQueue.global().async { generate(forVideo: video) } } } print("/thumb generation") }*/ /** Generate a thumbnail for the video When a video is not available offline, it will also save a larger version of the first frame of the video, to be used later in the UI as a placeholder */ static func generate(forVideo video: AerialVideo) { do { var asset: AVURLAsset if video.isAvailableOffline { // If a video is already cached, we may still need to use an online fetch as there's a bug // with AVAssetImageGenerator and Dolby Vision files if (PrefsVideos.videoFormat == .v1080pHDR || PrefsVideos.videoFormat == .v4KHDR) && video.source.name.starts(with: "tvOS") { // We workaround here by grabbing online a 1080 SDR instead let urlHEVC = video.urls[.v1080pHEVC] let url264 = video.urls[.v1080pH264] if urlHEVC != nil && urlHEVC != "" { asset = AVURLAsset(url: URL(string: urlHEVC!)!) } else if url264 != nil && url264 != "" { asset = AVURLAsset(url: URL(string: url264!)!) } else { // Well... asset = AVURLAsset(url: video.url) } } else { // let path = VideoCache.cachePath(forVideo: video)! let path = VideoList.instance.localPathFor(video: video) asset = AVURLAsset(url: URL(fileURLWithPath: path)) } } else { if (PrefsVideos.videoFormat == .v1080pHDR || PrefsVideos.videoFormat == .v4KHDR) && video.source.name.starts(with: "tvOS") { // We workaround here by grabbing online a 1080 SDR instead let urlHEVC = video.urls[.v1080pHEVC] let url264 = video.urls[.v1080pH264] if urlHEVC != nil && urlHEVC != "" { asset = AVURLAsset(url: URL(string: urlHEVC!)!) } else if url264 != nil && url264 != "" { asset = AVURLAsset(url: URL(string: url264!)!) } else { // Well... asset = AVURLAsset(url: video.url) } } else { asset = AVURLAsset(url: video.url) } } // maybe that doesn't work great with HDR, or a Big Sur thing ? let imageGenerator = AVAssetImageGenerator(asset: asset) imageGenerator.appliesPreferredTrackTransform = true let cgImage = try imageGenerator.copyCGImage(at: .zero, actualTime: nil) let saveURL = URL(fileURLWithPath: getPath(forVideo: video)) try writeImage(image: NSImage(cgImage: cgImage, size: thumbSize), usingType: .png, withSizeInPixels: thumbSize, to: saveURL) let largeURL = URL(fileURLWithPath: getLargePath(forVideo: video)) let fullSize = CGSize.init(width: cgImage.width, height: cgImage.height) try writeImage(image: NSImage(cgImage: cgImage, size: fullSize), usingType: .jpeg, withSizeInPixels: fullSize, to: largeURL) } catch { errorLog(error.localizedDescription) } } static private func unscaledBitmapImageRep(forImage image: NSImage) -> NSBitmapImageRep { guard let rep = NSBitmapImageRep( bitmapDataPlanes: nil, pixelsWide: Int(image.size.width), pixelsHigh: Int(image.size.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0 ) else { preconditionFailure() } NSGraphicsContext.saveGraphicsState() NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep) image.draw(at: .zero, from: .zero, operation: .sourceOver, fraction: 1.0) NSGraphicsContext.restoreGraphicsState() return rep } static private func writeImage( image: NSImage, usingType type: NSBitmapImageRep.FileType, withSizeInPixels size: NSSize?, to url: URL) throws { if let size = size { image.size = size } let rep = unscaledBitmapImageRep(forImage: image) guard let data = rep.representation(using: type, properties: [.compressionFactor: 0.8]) else { preconditionFailure() } try data.write(to: url) } /** */ static func cached(forVideo video: AerialVideo) -> NSImage? { let candidateThumb = getPath(forVideo: video) if FileManager.default.fileExists(atPath: candidateThumb) { return NSImage(contentsOfFile: candidateThumb) } else { return nil } } static private func getPath(forVideo video: AerialVideo) -> String { return Cache.thumbnailsPath.appending("/"+video.id+".png") } static private func getLargePath(forVideo video: AerialVideo) -> String { return Cache.thumbnailsPath.appending("/"+video.id+"-large.jpg") } static func get(forVideo video: AerialVideo, _ completion: @escaping ((_ image: NSImage?) -> Void)) { if let thumb = cached(forVideo: video) { completion(thumb) } else if video.isAvailableOffline { DispatchQueue.global().async { generate(forVideo: video) // Completion on the main queue DispatchQueue.main.async { completion(cached(forVideo: video)) } } } else { if Cache.canNetwork() { DispatchQueue.global().async { generate(forVideo: video) DispatchQueue.main.async { completion(cached(forVideo: video)) } } } else { completion(nil) } } } static func getLarge(forVideo video: AerialVideo, _ completion: @escaping ((_ image: NSImage?) -> Void)) { let candidateLarge = getLargePath(forVideo: video) if FileManager.default.fileExists(atPath: candidateLarge) { return completion(NSImage(contentsOfFile: candidateLarge)) } else { // This may happen in a race... return completion(nil) } } static func getLargeURL(forVideo video: AerialVideo) -> URL? { let candidateLarge = getLargePath(forVideo: video) if FileManager.default.fileExists(atPath: candidateLarge) { return URL(fileURLWithPath: candidateLarge) } else { // This may happen in a race... return nil } } } ================================================ FILE: Aerial/Source/Models/Cache/TimeMachine.swift ================================================ // // TimeMachine.swift // Aerial // // Created by Guillaume Louel on 13/09/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation struct TimeMachine { static func isExcluded() -> Bool { let process: Process = Process() debugLog("Checking if our path \(Cache.path) is excluded in Time Machine") process.launchPath = "/usr/bin/tmutil" process.arguments = ["isexcluded", Cache.path] let pipe = Pipe() process.standardOutput = pipe process.standardError = pipe process.launch() let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) debugLog(output ?? "No output from tmutil") process.waitUntilExit() // Now parse output if any, we're looking for "Excluded" string // Tested on 10.14/10.16, should be "safe" even if it doesn't work on other oses if let output = output { return output.contains("Excluded") } else { return false } } static func exclude() { let process: Process = Process() debugLog("Trying to exclude our path \(Cache.path) in Time Machine") process.launchPath = "/usr/bin/tmutil" process.arguments = ["addexclusion", Cache.path] let pipe = Pipe() process.standardOutput = pipe process.standardError = pipe process.launch() let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) debugLog(output ?? "No output from tmutil") process.waitUntilExit() } static func reinclude() { let process: Process = Process() debugLog("Trying to reinclude our path \(Cache.path) in Time Machine") process.launchPath = "/usr/bin/tmutil" process.arguments = ["removeexclusion", Cache.path] let pipe = Pipe() process.standardOutput = pipe process.standardError = pipe process.launch() let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) debugLog(output ?? "No output from tmutil") process.waitUntilExit() } } ================================================ FILE: Aerial/Source/Models/Cache/VideoCache.swift ================================================ // // VideoCache.swift // Aerial // // Created by John Coates on 10/29/15. // Copyright © 2015 John Coates. All rights reserved. // import Foundation import AVFoundation import ScreenSaver final class VideoCache { var videoData: Data var mutableVideoData: NSMutableData? var loading: Bool var loadedRanges: [NSRange] = [] let URL: URL static var computedCacheDirectory: String? static var computedAppSupportDirectory: String? // MARK: - Application Support directory static var appSupportDirectory: String? { // TODO : temporary for the migration return Cache.supportPath // // // We only process this once if successful // if computedAppSupportDirectory != nil { // return computedAppSupportDirectory // } // // var foundDirectory: String? // // let appSupportPaths = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, // .userDomainMask, // true) // // if appSupportPaths.isEmpty { // errorLog("Couldn't find appSupport paths!") // return nil // } // let appSupportDirectory = appSupportPaths[0] as NSString // if aerialFolderExists(at: appSupportDirectory) { // debugLog("app support exists") // foundDirectory = appSupportDirectory.appendingPathComponent("Aerial") // } else { // debugLog("creating app support directory") // // We create in user appSupport which may be containairized // // so ~/Library/Application Support/ on pre 10.15 // // or ~/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver // // /Data/Library/Application Support/ // foundDirectory = appSupportDirectory.appendingPathComponent("Aerial") // // let fileManager = FileManager.default // if fileManager.fileExists(atPath: foundDirectory!) == false { // do { // try fileManager.createDirectory(atPath: foundDirectory!, // withIntermediateDirectories: false, attributes: nil) // } catch let error { // errorLog("Couldn't create appSupport directory in User directory: \(error)") // errorLog("FATAL : There's nothing more we can do at this point") // return nil // } // } // } // // // Cache the computed value // computedAppSupportDirectory = foundDirectory // return computedAppSupportDirectory } // MARK: - User Video cache directory static var cacheDirectory: String? { // TODO : Until refactor is done return Cache.path // // We only process this once if successful // if computedCacheDirectory != nil { // return computedCacheDirectory // } // // var cacheDirectory: String? // let preferences = Preferences.sharedInstance // // if let customCacheDirectory = preferences.customCacheDirectory { // // We may have overriden the cache directory, but it may no longer exist ! // if FileManager.default.fileExists(atPath: customCacheDirectory as String) { // debugLog("Using exiting customCacheDirectory : \(customCacheDirectory)") // cacheDirectory = customCacheDirectory // } /*else { // // If it doesn't we need to reset that preference // preferences.customCacheDirectory = nil // }*/ // } // // if cacheDirectory == nil { // let userCachePaths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, // .userDomainMask, // true) // let localCachePaths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, // .localDomainMask, // true) // // if !localCachePaths.isEmpty { // let localCacheDirectory = localCachePaths[0] as NSString // if aerialFolderExists(at: localCacheDirectory) { // debugLog("Using existing local cache /Library/Caches/Aerial") // cacheDirectory = localCacheDirectory.appendingPathComponent("Aerial") // } // } // // if !userCachePaths.isEmpty && cacheDirectory == nil { // let userCacheDirectory = userCachePaths[0] as NSString // // if aerialFolderExists(at: userCacheDirectory) { // debugLog("Using existing user cache ~/Library/Caches/Aerial") // cacheDirectory = userCacheDirectory.appendingPathComponent("Aerial") // } else { // debugLog("No local or user cache exists, using ~/Library/Application Support/Aerial") // cacheDirectory = appSupportDirectory // } // } // } // // // Cache the computed value // computedCacheDirectory = cacheDirectory // // debugLog("cache to be used : \(String(describing: cacheDirectory))") // return cacheDirectory } // MARK: - Helpers static func aerialFolderExists(at: NSString) -> Bool { let aerialFolder = at.appendingPathComponent("Aerial") if FileManager.default.fileExists(atPath: aerialFolder as String) { return true } else { return false } } // Is a video cached (in either appSupport or cache) static func isAvailableOffline(video: AerialVideo) -> Bool { let fileManager = FileManager.default if video.url.absoluteString.starts(with: "file") { return fileManager.fileExists(atPath: video.url.path) } else { if video.source.isCachable { guard let videoCachePath = cachePath(forVideo: video) else { errorLog("Couldn't get video cache path!") return false } if fileManager.fileExists(atPath: videoCachePath) { do { let fileUrl = Foundation.URL(fileURLWithPath: videoCachePath) let resourceValues = try fileUrl.resourceValues(forKeys: [.fileSizeKey]) let fileSize = resourceValues.fileSize! // Make sure the file is big enough to be a video and not some network failure if fileSize > 500000 { return true } } catch { errorLog("File check throw") } } return false } else { let path = sourcePathFor(video) do { let fileUrl = Foundation.URL(fileURLWithPath: path) let resourceValues = try fileUrl.resourceValues(forKeys: [.fileSizeKey]) let fileSize = resourceValues.fileSize! // Make sure the file is big enough to be a video and not some network failure if fileSize > 500000 { return true } } catch { errorLog("File check throw") } return false } } } static func moveToTrash(video: AerialVideo) { let videoCachePath = VideoList.instance.localPathFor(video: video) guard videoCachePath != "" else { errorLog("Couldn't get video cache path to trash!") return } let vurl = Foundation.URL(fileURLWithPath: videoCachePath as String) debugLog("trashing \(vurl))") do { try FileManager.default.trashItem(at: vurl, resultingItemURL: nil) } catch let error { errorLog("Could not move \(video.url) to trash \(error)") } } static func cachePath(forVideo video: AerialVideo) -> String? { if video.url.absoluteString.starts(with: "file") { return video.url.path } let vurl = video.url let filename = vurl.lastPathComponent return cachePath(forFilename: filename) } static func cachePath(forFilename filename: String) -> String? { guard let cacheDirectory = VideoCache.cacheDirectory, let appSupportDirectory = VideoCache.appSupportDirectory else { return nil } // Let's compute both let appSupportPath = appSupportDirectory as NSString let appSupportVideoPath = appSupportPath.appendingPathComponent(filename) let cacheDirectoryPath = cacheDirectory as NSString let cacheVideoPath = cacheDirectoryPath.appendingPathComponent(filename) // If the file exists in either dir, returns that if FileManager.default.fileExists(atPath: appSupportVideoPath as String) { return appSupportVideoPath } else if FileManager.default.fileExists(atPath: cacheVideoPath as String) { return cacheVideoPath } else { // File doesn't have to exist, this is also used to compute the save location // So now with Catalina, considering containerization we need to use appSupport // Pre catalina we return cache folder instead (no change for users) return cacheVideoPath /* if #available(OSX 10.15, *) { return appSupportVideoPath } else { return cacheVideoPath } */ } } static func sourcePathFor(_ video: AerialVideo) -> String { if video.url.isFileURL { return video.url.path } else { return Cache.supportPath.appending("/" + video.source.name + "/" + video.url.lastPathComponent) } } static func sourcePathFor(_ filename: String, video: AerialVideo) -> String { return Cache.supportPath.appending("/" + video.source.name + "/" + filename) } init(URL: Foundation.URL) { debugLog("initvideocache") videoData = Data() loading = true self.URL = URL loadCachedVideoIfPossible() } // MARK: - Data Adding func receivedContentLength(_ contentLength: Int) { if loading == false { return } if mutableVideoData != nil { return } mutableVideoData = NSMutableData(length: contentLength) videoData = mutableVideoData! as Data } func receivedData(_ data: Data, atRange range: NSRange) { guard let mutableVideoData = mutableVideoData else { errorLog("Received data without having mutable video data") return } mutableVideoData.replaceBytes(in: range, withBytes: (data as NSData).bytes) loadedRanges.append(range) consolidateLoadedRanges() // debugLog("loaded ranges: \(loadedRanges)") if loadedRanges.count == 1 { let range = loadedRanges[0] // debugLog("checking if range \(range) matches length \(mutableVideoData.length)") if range.location == 0 && range.length == mutableVideoData.length { // done loading, save saveCachedVideo() } } } // MARK: - Save / Load Cache var videoCachePath: String? { let filename = URL.lastPathComponent if let video = VideoList.instance.videoForFilename(filename) { if !video.source.isCachable { return VideoCache.sourcePathFor(filename, video: video) } } return VideoCache.cachePath(forFilename: filename) } func saveCachedVideo() { let fileManager = FileManager.default guard let videoCachePath = videoCachePath else { errorLog("Couldn't save cache file") return } guard fileManager.fileExists(atPath: videoCachePath) == false else { errorLog("Cache file \(videoCachePath) already exists.") return } loading = false if mutableVideoData == nil { errorLog("Missing video data for save.") return } do { try mutableVideoData!.write(toFile: videoCachePath, options: .atomicWrite) mutableVideoData = nil videoData.removeAll() } catch let error { errorLog("Couldn't write cache file: \(error)") } } func loadCachedVideoIfPossible() { let fileManager = FileManager.default guard let videoCachePath = self.videoCachePath else { errorLog("Couldn't load cache file.") return } if fileManager.fileExists(atPath: videoCachePath) == false { return } guard let videoData = try? Data(contentsOf: Foundation.URL(fileURLWithPath: videoCachePath)) else { errorLog("NSData failed to load cache file \(videoCachePath)") return } self.videoData = videoData loading = false debugLog("cached video file with length: \(self.videoData.count)") } // MARK: - Fulfilling cache func fulfillLoadingRequest(_ loadingRequest: AVAssetResourceLoadingRequest) -> Bool { guard let dataRequest = loadingRequest.dataRequest else { errorLog("Missing data request for \(loadingRequest)") return false } let requestedOffset = Int(dataRequest.requestedOffset) let requestedLength = Int(dataRequest.requestedLength) let data = videoData.subdata(in: requestedOffset.. Void in self.fillInContentInformation(loadingRequest) dataRequest.respond(with: data) loadingRequest.finishLoading() } return true } func fillInContentInformation(_ loadingRequest: AVAssetResourceLoadingRequest) { guard let contentInformationRequest = loadingRequest.contentInformationRequest else { return } let contentType: String = kUTTypeQuickTimeMovie as String contentInformationRequest.isByteRangeAccessSupported = true contentInformationRequest.contentType = contentType contentInformationRequest.contentLength = Int64(videoData.count) } // MARK: - Cache Checking // Whether the video cache can fulfill this request func canFulfillLoadingRequest(_ loadingRequest: AVAssetResourceLoadingRequest) -> Bool { if !loading { return true } guard let dataRequest = loadingRequest.dataRequest else { errorLog("Missing data request for \(loadingRequest)") return false } let requestedOffset = Int(dataRequest.requestedOffset) let requestedLength = Int(dataRequest.requestedLength) let requestedEnd = requestedOffset + requestedLength for range in loadedRanges { let rangeStart = range.location let rangeEnd = range.location + range.length if requestedOffset >= rangeStart && requestedEnd <= rangeEnd { return true } } return false } // MARK: - Consolidating func consolidateLoadedRanges() { var consolidatedRanges: [NSRange] = [] let sortedRanges = loadedRanges.sorted { $0.location < $1.location } var previousRange: NSRange? var lastIndex: Int? for range in sortedRanges { if let lastRange: NSRange = previousRange { let lastRangeEndOffset = lastRange.location + lastRange.length // check if range can be consumed by lastRange // or if they're at each other's edges if it can be merged if lastRangeEndOffset >= range.location { let endOffset = range.location + range.length // check if this range's end offset is larger than lastRange's if endOffset > lastRangeEndOffset { previousRange!.length = endOffset - lastRange.location // replace lastRange in array with new value consolidatedRanges.remove(at: lastIndex!) consolidatedRanges.append(previousRange!) continue } else { // skip adding this to the array, previous range is already bigger // debugLog("skipping add of \(range), previous: \(previousRange)") continue } } } lastIndex = consolidatedRanges.count previousRange = range consolidatedRanges.append(range) } loadedRanges = consolidatedRanges } } ================================================ FILE: Aerial/Source/Models/Cache/VideoDownload.swift ================================================ // // VideoDownload.swift // Aerial // // Created by John Coates on 10/31/15. // Copyright © 2015 John Coates. All rights reserved. // import Foundation protocol VideoDownloadDelegate: NSObjectProtocol { func videoDownload(_ videoDownload: VideoDownload, finished success: Bool, errorMessage: String?) // bytes received for bytes/second count func videoDownload(_ videoDownload: VideoDownload, receivedBytes: Int, progress: Float) } final class VideoDownloadStream { var connection: NSURLConnection var response: URLResponse? var contentInformationRequest: Bool = false var downloadOffset = 0 init(connection: NSURLConnection) { self.connection = connection } deinit { connection.cancel() } } final class VideoDownload: NSObject, NSURLConnectionDataDelegate { var streams: [VideoDownloadStream] = [] weak var delegate: VideoDownloadDelegate! let queue = DispatchQueue.main let video: AerialVideo var data: NSMutableData? var downloadedData: Int = 0 var contentLength: Int = 0 init(video: AerialVideo, delegate: VideoDownloadDelegate) { self.video = video self.delegate = delegate } deinit { //print("deinit VideoDownload") } func startDownload() { // first start content information download startDownloadForContentInformation() } // download a couple bytes to get the content length func startDownloadForContentInformation() { 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 if let requestedRange = chunk { // set Range: bytes=startOffset-endOffset let requestRangeField = "bytes=\(requestedRange.location)-\(requestedRange.location+requestedRange.length)" request.setValue(requestRangeField, forHTTPHeaderField: "Range") debugLog("Starting download for range \(requestRangeField)") } guard let connection = NSURLConnection(request: request as URLRequest, delegate: self, startImmediately: false) else { errorLog("Error creating connection with request: \(request)") return } let stream = VideoDownloadStream(connection: connection) if chunk == nil { debugLog("Starting download for content information") stream.contentInformationRequest = true } connection.start() streams.append(stream) } func streamForConnection(_ connection: NSURLConnection) -> VideoDownloadStream? { return streams.first(where: { $0.connection == connection }) } func createStreamsBasedOnContentLength(_ contentLength: Int) { self.contentLength = contentLength // remove content length request stream streams.removeFirst() data = NSMutableData(length: contentLength) // start 4 streams for maximum throughput let streamCount = 1 // TODO let pace = 0.2; // pace stream creation a little bit let streamPiece = Int(floor(Double(contentLength) / Double(streamCount))) debugLog("Starting \(streamCount) streams with \(streamPiece) each, for content length of \(contentLength)") var offset = 0 var delayTime: Double = 0 // let queue = DispatchQueue.main for idx in 0 ..< streamCount { let isLastStream: Bool = idx == (streamCount - 1) var range = NSRange(location: offset, length: streamPiece) if isLastStream { let bytesLeft = contentLength - offset range = NSRange(location: offset, length: bytesLeft) debugLog("last stream range: \(range)") } let delay = DispatchTime.now() + Double(Int64(delayTime * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) queue.asyncAfter(deadline: delay) { self.startDownloadForChunk(range) } // increase delay delayTime += pace // increase offset offset += range.length } } func receiveDataForStream(_ stream: VideoDownloadStream, receivedData: Data) { guard let videoData = self.data else { errorLog("Aerial error: video data missing!") return } let replaceRange = NSRange(location: stream.downloadOffset, length: receivedData.count) videoData.replaceBytes(in: replaceRange, withBytes: (receivedData as NSData).bytes) stream.downloadOffset += receivedData.count } func finishedDownload() { var tentativeCachePath: String? if video.source.isCachable { tentativeCachePath = VideoCache.cachePath(forVideo: video) } else { tentativeCachePath = VideoCache.sourcePathFor(video) } guard let videoCachePath = tentativeCachePath else { errorLog("Couldn't save video because couldn't get cache path\n") failedDownload("Couldn't get cache path") return } if self.data == nil { errorLog("video data missing!\n") return } var success: Bool = true var errorMessage: String? do { try self.data!.write(toFile: videoCachePath, options: .atomicWrite) self.data = nil } catch let error { errorLog("Couldn't write cache file: \(error)") errorMessage = "Couldn't write to cache file!" success = false } // notify delegate delegate.videoDownload(self, finished: success, errorMessage: errorMessage) } func failedDownload(_ errorMessage: String) { delegate.videoDownload(self, finished: false, errorMessage: errorMessage) } // MARK: - NSURLConnection Delegate func connection(_ connection: NSURLConnection, didReceive response: URLResponse) { guard let stream = streamForConnection(connection) else { errorLog("No matching stream for connection: \(connection) with response: \(response)") return } stream.response = response as? HTTPURLResponse if stream.contentInformationRequest == true { connection.cancel() queue.async(execute: { () -> Void in let contentLength = Int(response.expectedContentLength) self.createStreamsBasedOnContentLength(contentLength) }) return } else { // get real offset of receiving data queue.async(execute: { () -> Void in guard let offset = self.startOffsetFromResponse(response) else { errorLog("Couldn't get start offset from response: \(response)") return } stream.downloadOffset = offset }) } } func connection(_ connection: NSURLConnection, didReceive data: Data) { guard let delegate = self.delegate else { return } queue.async { () -> Void in self.downloadedData += data.count let progress: Float = Float(self.downloadedData) / Float(self.contentLength) delegate.videoDownload(self, receivedBytes: data.count, progress: progress) guard let stream = self.streamForConnection(connection) else { errorLog("No matching stream for connection: \(connection)") return } self.receiveDataForStream(stream, receivedData: data) } } func connectionDidFinishLoading(_ connection: NSURLConnection) { queue.async { () -> Void in debugLog("connectionDidFinishLoading") guard let stream = self.streamForConnection(connection) else { errorLog("No matching stream for connection: \(connection)") return } guard let index = self.streams.firstIndex(where: { $0.connection == stream.connection }) else { errorLog("Couldn't find index of stream for finished connection!") return } self.streams.remove(at: index) if self.streams.isEmpty { debugLog("Finished downloading!") self.finishedDownload() } } } func connection(_ connection: NSURLConnection, didFailWithError error: Error) { errorLog("Couldn't download video: \(error.localizedDescription)") queue.async { () -> Void in self.failedDownload("Connection fail: \(error.localizedDescription)") } } func connection(_ connection: NSURLConnection, didReceive challenge: URLAuthenticationChallenge) { errorLog("Didn't expect authentication challenge while downloading videos!") queue.async { () -> Void in self.failedDownload("Connection fail: Received authentication request!") } } // MARK: - Range func startOffsetFromResponse(_ response: URLResponse) -> Int? { // get range response var regex: NSRegularExpression! do { // Check to see if the server returned a valid byte-range regex = try NSRegularExpression(pattern: "bytes (\\d+)-\\d+/\\d+", options: NSRegularExpression.Options.caseInsensitive) } catch let error as NSError { errorLog("Error formatting regex: \(error)") return nil } let httpResponse = response as! HTTPURLResponse guard let contentRange = httpResponse.allHeaderFields["Content-Range"] as? NSString else { 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 { errorLog("Weird, couldn't make a regex match for byte offset: \(contentRange)") return nil } let offsetMatchRange = match.range(at: 1) let offsetString = contentRange.substring(with: offsetMatchRange) as NSString let offset = offsetString.longLongValue return Int(offset) } } ================================================ FILE: Aerial/Source/Models/Cache/VideoLoader.swift ================================================ // // VideoLoader.swift // Aerial // // Created by John Coates on 10/29/15. // Copyright © 2015 John Coates. All rights reserved. // import Foundation import AVFoundation protocol VideoLoaderDelegate: NSObjectProtocol { func videoLoader(_ videoLoader: VideoLoader, receivedResponse response: URLResponse) func videoLoader(_ videoLoader: VideoLoader, receivedData data: Data, forRange range: NSRange) } final class VideoLoader: NSObject, NSURLConnectionDataDelegate { var connection: NSURLConnection? var response: HTTPURLResponse? weak var delegate: VideoLoaderDelegate? var loadingRequest: AVAssetResourceLoadingRequest // range params var loadedRange: NSRange var requestedRange: NSRange var loadRange: Bool 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.reloadIgnoringLocalCacheData loadRange = false loadedRange = NSRange(location: 0, length: 0) requestedRange = NSRange(location: 0, length: 0) if let dataRequest = loadingRequest.dataRequest { if dataRequest.requestedOffset > 0 { loadRange = true let startOffset = Int(dataRequest.requestedOffset) let requestedBytes = Int(dataRequest.requestedLength) loadedRange = NSRange(location: startOffset, length: 0) requestedRange = NSRange(location: startOffset, length: requestedBytes) // set Range: bytes=startOffset-endOffset let requestRange = "bytes=\(requestedRange.location)-\(requestedRange.location+requestedRange.length)" request.setValue(requestRange, forHTTPHeaderField: "Range") } } super.init() connection = NSURLConnection(request: request as URLRequest, delegate: self, startImmediately: false) guard let connection = connection else { errorLog("Couldn't instantiate connection.") return } connection.setDelegateQueue(OperationQueue.main) loadedRange = NSRange(location: requestedRange.location, length: 0) connection.start() // debugLog("Starting request: \(request)") } deinit { connection?.cancel() } // MARK: - NSURLConnection Delegate func connection(_ connection: NSURLConnection, didReceive response: URLResponse) { if loadRange { if let startOffset = startOffsetFromResponse(response) { loadedRange.location = startOffset } } self.response = response as? HTTPURLResponse queue.async { () -> Void in self.delegate?.videoLoader(self, receivedResponse: response) self.fillInContentInformation(self.loadingRequest) } } func connection(_ connection: NSURLConnection, didReceive data: Data) { queue.async { () -> Void in self.fillInContentInformation(self.loadingRequest) guard let dataRequest = self.loadingRequest.dataRequest else { errorLog("Data request missing for \(self.loadingRequest)") return } 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) 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 { 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 if inset > 0 { let start = inset let length = data.count - inset let end = start + length let responseData = data.subdata(in: inset..= dataRequest.requestedOffset + Int64(dataRequest.requestedLength) { self.loadingRequest.finishLoading() self.connection?.cancel() } } else if inset < 1 { errorLog("Inset is invalid value: \(inset)") } } // debugLog("Received data with length: \(data.count)") self.loadedRange.length += data.count } } func connectionDidFinishLoading(_ connection: NSURLConnection) { queue.async { () -> Void in debugLog("connectionDidFinishLoading") self.loadingRequest.finishLoading() } } func fillInContentInformation(_ loadingRequest: AVAssetResourceLoadingRequest) { guard let contentInformationRequest = loadingRequest.contentInformationRequest else { return } guard let response = self.response else { debugLog("No response") return } guard let mimeType = response.mimeType else { debugLog("no mimeType for \(response)") return } guard let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString, nil) else { debugLog("couldn't create prefered identifier for tag \(mimeType)") return } debugLog("Processsing contentInformationRequest") let contentType: String = uti.takeRetainedValue() as String contentInformationRequest.isByteRangeAccessSupported = true contentInformationRequest.contentType = contentType contentInformationRequest.contentLength = response.expectedContentLength debugLog("expected content length: \(response.expectedContentLength) type:\(contentType)") } // MARK: - Range func startOffsetFromResponse(_ response: URLResponse) -> Int? { // get range response var regex: NSRegularExpression! do { // Check to see if the server returned a valid byte-range regex = try NSRegularExpression(pattern: "bytes (\\d+)-\\d+/\\d+", options: NSRegularExpression.Options.caseInsensitive) } catch let error as NSError { errorLog("Error formatting regex: \(error)") return nil } let httpResponse = response as! HTTPURLResponse guard let contentRange = httpResponse.allHeaderFields["Content-Range"] as? NSString else { 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 { errorLog("Weird, couldn't make a regex match for byte offset: \(contentRange)") return nil } let offsetMatchRange = match.range(at: 1) let offsetString = contentRange.substring(with: offsetMatchRange) as NSString let offset = offsetString.longLongValue // debugLog("content range: \(contentRange), start offset: \(offset)") return Int(offset) } } ================================================ FILE: Aerial/Source/Models/Cache/VideoManager.swift ================================================ // // VideoManager.swift // Aerial // // Created by Guillaume Louel on 08/10/2018. // Copyright © 2018 John Coates. All rights reserved. // import Foundation typealias VideoManagerCallback = (Int, Int) -> Void typealias VideoProgressCallback = (Int, Int, Double) -> Void final class VideoManager: NSObject { static let sharedInstance = VideoManager() var managerCallbacks = [VideoManagerCallback]() var progressCallbacks = [VideoProgressCallback]() /// 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 private let queue: OperationQueue = { // swiftlint:disable:next identifier_name let _queue = OperationQueue() _queue.name = "videodownload" _queue.maxConcurrentOperationCount = 1 return _queue }() // MARK: Tracking CheckCellView func addCheckCellView(id: String, checkCellView: CheckCellView) { checkCells[id] = checkCellView } func addCallback(_ callback:@escaping VideoManagerCallback) { managerCallbacks.append(callback) } func addProgressCallback(_ callback:@escaping VideoProgressCallback) { progressCallbacks.append(callback) } func updateAllCheckCellView() { for view in checkCells { view.value.adaptIndicators() } } // Is the video queued for download ? func isVideoQueued(id: String) -> Bool { if queuedVideos.firstIndex(of: id) != nil { return true } else { return false } } @discardableResult func queueDownload(_ video: AerialVideo) -> VideoDownloadOperation { if stopAll { stopAll = false } 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 += 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, 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] { if success { cell.markAsDownloaded() } else { cell.markAsNotDownloaded() } } } func markAsQueued(id: String) { // Manage our queuedVideo index if let cell = checkCells[id] { cell.markAsQueued() } } func updateProgress(id: String, progress: Double) { if let cell = checkCells[id] { cell.updateProgressIndicator(progress: progress) } DispatchQueue.main.async { // Callback the callbacks for callback in self.progressCallbacks { callback(self.totalQueued-self.queuedVideos.count, self.totalQueued, progress) } } } /// Cancel all queued operations func cancelAll() { stopAll = true queue.cancelAllOperations() } } final class VideoDownloadOperation: AsynchronousOperation { var video: AerialVideo var download: VideoDownload? init(video: AerialVideo, delegate: VideoManager) { debugLog("Video queued \(video.name)") self.video = video } override func main() { let videoManager = VideoManager.sharedInstance if videoManager.stopAll { 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 let _ = self.download { self.download!.cancel() } else { videoManager.finishedDownload(id: self.video.id, success: false) } self.download = nil super.cancel() // finish() } } extension VideoDownloadOperation: VideoDownloadDelegate { func videoDownload(_ videoDownload: VideoDownload, finished success: Bool, errorMessage: String?) { debugLog("Finished") defer { finish() } let videoManager = VideoManager.sharedInstance if success { // Call up to clean the view videoManager.finishedDownload(id: videoDownload.video.id, success: true) } else { if let _ = errorMessage { errorLog(errorMessage!) } videoManager.finishedDownload(id: videoDownload.video.id, success: false) } } func videoDownload(_ videoDownload: VideoDownload, receivedBytes: Int, progress: Float) { // Call up to update the view let videoManager = VideoManager.sharedInstance videoManager.updateProgress(id: videoDownload.video.id, progress: Double(progress)) } } ================================================ FILE: Aerial/Source/Models/CompanionBridge.swift ================================================ // // CompanionBridge.swift // Aerial // // Created by Guillaume Louel on 09/10/2023. // Copyright © 2023 Guillaume Louel. All rights reserved. // // This acts as our bridge to Companion when the plugin needs data FROM companion // Currently using DistributedNotificationCenter, until *that* breaks too... import Foundation struct CompanionBridge { static var nightShiftSunrise: Date? static var nightShiftSunset: Date? static var locationLat: Double? static var locationLong: Double? static func setNotifications() { debugLog("🌉 seting up CompanionBridge") // Get nightshift DistributedNotificationCenter.default().addObserver(forName: NSNotification.Name("com.glouel.aerial.nightshift"), object: nil, queue: nil) { notification in debugLog("🌉😻 received nightshift") debugLog(notification.debugDescription) if let sunrise = notification.userInfo?["sunrise"] as? Date { debugLog("parsed sunrise") nightShiftSunrise = sunrise } else { debugLog("can't parse sunrise") } if let sunset = notification.userInfo?["sunset"] as? Date { debugLog("parsed sunset") nightShiftSunset = sunset } } // Get location DistributedNotificationCenter.default().addObserver(forName: NSNotification.Name("com.glouel.aerial.location"), object: nil, queue: nil) { notification in debugLog("🌉😻 received location") debugLog(notification.debugDescription) if let lat = notification.userInfo?["latitude"] as? Double { debugLog("parsed latitude") locationLat = lat } else { debugLog("can't parse latitude") } if let long = notification.userInfo?["longitude"] as? Double { debugLog("parsed longitude") locationLong = long } } // Test request DistributedNotificationCenter.default().postNotificationName(NSNotification.Name("com.glouel.aerial.getnightshift"), object: nil, deliverImmediately: true) if PrefsInfo.weather.locationMode == .useCurrent || PrefsTime.timeMode == .locationService { debugLog("🌉 asking for location") DistributedNotificationCenter.default().postNotificationName(NSNotification.Name("com.glouel.aerial.getlocation"), object: nil, deliverImmediately: true) } } } ================================================ FILE: Aerial/Source/Models/CustomVideoFolders+helpers.swift ================================================ // // CustomVideoFolders+helpers.swift // Aerial // // Created by Guillaume Louel on 24/05/2019. // Copyright © 2019 John Coates. All rights reserved. // import Foundation // Helpers added on top of our generated json class extension CustomVideoFolders { func hasFolder(withUrl: String) -> Bool { for folder in folders where folder.url == withUrl { return true } return false } func getFolderIndex(withUrl: String) -> Int { var index = 0 for folder in folders { if folder.url == withUrl { return index } index += 1 } return -1 } func getFolder(withUrl: String) -> Folder? { for folder in folders where folder.url == withUrl { return folder } return nil } } extension Folder { func hasAsset(withUrl: String) -> Bool { for asset in assets where asset.url == withUrl { return true } return false } func getAssetIndex(withUrl: String) -> Int { var index = 0 for asset in assets { if asset.url == withUrl { return index } index += 1 } return -1 } } ================================================ FILE: Aerial/Source/Models/CustomVideoFolders.swift ================================================ // This file was generated from JSON Schema using quicktype, do not modify it directly. // To parse the JSON, add this file to your project and do: // // let customVideoFolders = try CustomVideoFolders(json) import Foundation // MARK: - CustomVideoFolders class CustomVideoFolders: Codable { var folders: [Folder] enum CodingKeys: String, CodingKey { case folders = "folders" } init(folders: [Folder]) { self.folders = folders } } // MARK: CustomVideoFolders convenience initializers and mutators extension CustomVideoFolders { convenience init(data: Data) throws { let me = try newJSONDecoder().decode(CustomVideoFolders.self, from: data) self.init(folders: me.folders) } convenience init(_ json: String, using encoding: String.Encoding = .utf8) throws { guard let data = json.data(using: encoding) else { throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil) } try self.init(data: data) } convenience init(fromURL url: URL) throws { try self.init(data: try Data(contentsOf: url)) } func with( folders: [Folder]? = nil ) -> CustomVideoFolders { return CustomVideoFolders( folders: folders ?? self.folders ) } func jsonData() throws -> Data { return try newJSONEncoder().encode(self) } func jsonString(encoding: String.Encoding = .utf8) throws -> String? { return String(data: try self.jsonData(), encoding: encoding) } } // MARK: - Folder class Folder: Codable { var url: String var label: String var assets: [Asset] enum CodingKeys: String, CodingKey { case url = "url" case label = "label" case assets = "assets" } init(url: String, label: String, assets: [Asset]) { self.url = url self.label = label self.assets = assets } } // MARK: Folder convenience initializers and mutators extension Folder { convenience init(data: Data) throws { let me = try newJSONDecoder().decode(Folder.self, from: data) self.init(url: me.url, label: me.label, assets: me.assets) } convenience init(_ json: String, using encoding: String.Encoding = .utf8) throws { guard let data = json.data(using: encoding) else { throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil) } try self.init(data: data) } convenience init(fromURL url: URL) throws { try self.init(data: try Data(contentsOf: url)) } func with( url: String? = nil, label: String? = nil, assets: [Asset]? = nil ) -> Folder { return Folder( url: url ?? self.url, label: label ?? self.label, assets: assets ?? self.assets ) } func jsonData() throws -> Data { return try newJSONEncoder().encode(self) } func jsonString(encoding: String.Encoding = .utf8) throws -> String? { return String(data: try self.jsonData(), encoding: encoding) } } // MARK: - Asset class Asset: Codable { var pointsOfInterest: [String: String] var url: String var accessibilityLabel: String var id: String var time: String enum CodingKeys: String, CodingKey { case pointsOfInterest = "pointsOfInterest" case url = "url" case accessibilityLabel = "accessibilityLabel" case id = "id" case time = "time" } init(pointsOfInterest: [String: String], url: String, accessibilityLabel: String, id: String, time: String) { self.pointsOfInterest = pointsOfInterest self.url = url self.accessibilityLabel = accessibilityLabel self.id = id self.time = time } } // MARK: Asset convenience initializers and mutators extension Asset { convenience init(data: Data) throws { let me = try newJSONDecoder().decode(Asset.self, from: data) self.init(pointsOfInterest: me.pointsOfInterest, url: me.url, accessibilityLabel: me.accessibilityLabel, id: me.id, time: me.time) } convenience init(_ json: String, using encoding: String.Encoding = .utf8) throws { guard let data = json.data(using: encoding) else { throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil) } try self.init(data: data) } convenience init(fromURL url: URL) throws { try self.init(data: try Data(contentsOf: url)) } func with( pointsOfInterest: [String: String]? = nil, url: String? = nil, accessibilityLabel: String? = nil, id: String? = nil, time: String? = nil ) -> Asset { return Asset( pointsOfInterest: pointsOfInterest ?? self.pointsOfInterest, url: url ?? self.url, accessibilityLabel: accessibilityLabel ?? self.accessibilityLabel, id: id ?? self.id, time: time ?? self.time ) } func jsonData() throws -> Data { return try newJSONEncoder().encode(self) } func jsonString(encoding: String.Encoding = .utf8) throws -> String? { return String(data: try self.jsonData(), encoding: encoding) } } // MARK: - Helper functions for creating encoders and decoders func newJSONDecoder() -> JSONDecoder { let decoder = JSONDecoder() if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) { decoder.dateDecodingStrategy = .iso8601 } return decoder } func newJSONEncoder() -> JSONEncoder { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) { encoder.dateEncodingStrategy = .iso8601 } return encoder } ================================================ FILE: Aerial/Source/Models/Downloads/AsynchronousOperation.swift ================================================ // // AsynchronousOperation.swift // Aerial // // Created by Guillaume Louel on 03/10/2018. // Copyright © 2018 John Coates. All rights reserved. // From https://stackoverflow.com/questions/32322386/how-to-download-multiple-files-sequentially-using-nsurlsession-downloadtask-in-s import Foundation /// Asynchronous operation base class /// /// This is abstract to class performs all of the necessary KVN of `isFinished` and /// `isExecuting` for a concurrent `Operation` subclass. You can subclass this and /// implement asynchronous operations. All you must do is: /// /// - override `main()` with the tasks that initiate the asynchronous task; /// /// - call `completeOperation()` function when the asynchronous task is done; /// /// - optionally, periodically check `self.cancelled` status, performing any clean-up /// necessary and then ensuring that `finish()` is called; or /// override `cancel` method, calling `super.cancel()` and then cleaning-up /// and ensuring `finish()` is called. class AsynchronousOperation: Operation { /// State for this operation. @objc private enum OperationState: Int { case ready case executing case finished } /// Concurrent queue for synchronizing access to `state`. private let stateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".rw.state", attributes: .concurrent) /// Private backing stored property for `state`. private var rawState: OperationState = .ready /// The state of the operation @objc private dynamic var state: OperationState { get { return stateQueue.sync { rawState } } set { stateQueue.sync(flags: .barrier) { rawState = newValue } } } // MARK: - Various `Operation` properties open override var isReady: Bool { return state == .ready && super.isReady } public final override var isExecuting: Bool { return state == .executing } public final override var isFinished: Bool { return state == .finished } // KVN for dependent properties open override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set { if ["isReady", "isFinished", "isExecuting"].contains(key) { return [#keyPath(state)] } return super.keyPathsForValuesAffectingValue(forKey: key) } // Start public final override func start() { if isCancelled { finish() return } state = .executing main() } /// Subclasses must implement this to perform their work and they must not call `super`. /// The default implementation of this function throws an exception. open override func main() { fatalError("Subclasses must implement `main`.") } /// Call this function to finish an operation that is currently executing public final func finish() { if isExecuting { state = .finished } } } ================================================ FILE: Aerial/Source/Models/Downloads/DownloadManager.swift ================================================ // // DownloadManager.swift // Aerial // // Created by Guillaume Louel on 03/10/2018. // Copyright © 2018 John Coates. All rights reserved. import Cocoa /// Manager of asynchronous download `Operation` objects final class DownloadManager: NSObject { /// Dictionary of operations, keyed by the `taskIdentifier` of the `URLSessionTask` fileprivate var operations = [Int: DownloadOperation]() /// Serial OperationQueue for downloads private let queue: OperationQueue = { let operationQueue = OperationQueue() operationQueue.name = "download" operationQueue.maxConcurrentOperationCount = 3 return operationQueue }() /// Delegate-based `URLSession` for DownloadManager lazy var session: URLSession = { let configuration = URLSessionConfiguration.default return URLSession(configuration: configuration, delegate: self, delegateQueue: nil) }() /// Add download /// /// - parameter URL: The URL of the file to be downloaded /// folder: The name of the subfolder where the file will be stored /// /// - returns: The DownloadOperation of the operation that was queued @discardableResult func queueDownload(_ url: URL, folder: String) -> DownloadOperation { let operation = DownloadOperation(session: session, url: url, folder: folder) operations[operation.task.taskIdentifier] = operation queue.addOperation(operation) return operation } /// Cancel all queued operations func cancelAll() { queue.cancelAllOperations() } } // MARK: URLSessionDownloadDelegate methods extension DownloadManager: URLSessionDownloadDelegate { func urlSession( _ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL ) { operations[downloadTask.taskIdentifier]?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location) } func urlSession( _ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64 ) { operations[downloadTask.taskIdentifier]?.urlSession(session, downloadTask: downloadTask, didWriteData: bytesWritten, totalBytesWritten: totalBytesWritten, totalBytesExpectedToWrite: totalBytesExpectedToWrite) } } // MARK: URLSessionTaskDelegate methods extension DownloadManager: URLSessionTaskDelegate { func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { let key = task.taskIdentifier operations[key]?.urlSession(session, task: task, didCompleteWithError: error) operations.removeValue(forKey: key) } } /// Asynchronous Operation subclass for downloading final class DownloadOperation: AsynchronousOperation { let task: URLSessionTask let folder: String init(session: URLSession, url: URL, folder: String) { self.folder = folder task = session.downloadTask(with: url) super.init() } override func cancel() { task.cancel() super.cancel() } override func main() { task.resume() } } // MARK: NSURLSessionDownloadDelegate methods // Customized for our usage extension DownloadOperation: URLSessionDownloadDelegate { // This is where we save the file to its location func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { do { // We may need to create our destination let destinationDirectory = Cache.supportPath.appending("/" + folder) FileHelpers.createDirectory(atPath: destinationDirectory) let manager = FileManager.default let supportURL = URL(fileURLWithPath: Cache.supportPath.appending("/" + folder)) debugLog("Caching \(downloadTask.originalRequest!.url!.lastPathComponent) at \(folder)") // The file may exist, remove it try? manager.removeItem(at: supportURL.appendingPathComponent( downloadTask.originalRequest!.url!.lastPathComponent)) // Finally move the file try manager.moveItem(at: location, to: supportURL.appendingPathComponent( downloadTask.originalRequest!.url!.lastPathComponent)) } catch { 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)") } } // MARK: URLSessionTaskDelegate methods extension DownloadOperation: URLSessionTaskDelegate { func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { defer { finish() } if let error = error { errorLog("\(error)") return } let destinationDirectory = Cache.supportPath.appending("/" + folder) // Some manifests come in tar form, in that case untar them here if folder == "tvOS 13" { FileHelpers.unTar(file: destinationDirectory.appending("/resources-13.tar"), atPath: destinationDirectory) } else if folder == "tvOS 16" { FileHelpers.unTar(file: destinationDirectory.appending("/resources-16.tar"), atPath: destinationDirectory) } else if folder == "tvOS 12" { FileHelpers.unTar(file: destinationDirectory.appending("/resources.tar"), atPath: destinationDirectory) } else if folder == "macOS 14" { FileHelpers.unTar(file: destinationDirectory.appending("/resources-14-0-10.tar"), atPath: destinationDirectory) } else if folder == "macOS 15" { FileHelpers.unTar(file: destinationDirectory.appending("/resources-15-0-2.tar"), atPath: destinationDirectory) } else if folder == "macOS 26" { FileHelpers.unTar(file: destinationDirectory.appending("/resources-26-0-1.tar"), atPath: destinationDirectory) } debugLog("Finished downloading \(task.originalRequest!.url!.absoluteString)") } } ================================================ FILE: Aerial/Source/Models/Downloads/FileHelpers.swift ================================================ // // FileHelpers.swift // Aerial // // Created by Guillaume Louel on 08/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation struct FileHelpers { static func createDirectory(atPath: String) { let fileManager = FileManager.default if fileManager.fileExists(atPath: atPath) == false { do { try fileManager.createDirectory(atPath: atPath, withIntermediateDirectories: true, attributes: nil) } catch let error { errorLog("Couldn't create directory at \(atPath) : \(error)") errorLog("FATAL : There's nothing more we can do at this point, please report") } } } static func unTar(file: String, atPath: String) { let process: Process = Process() debugLog("untaring \(file) at \(atPath)") process.currentDirectoryPath = atPath process.launchPath = "/usr/bin/tar" process.arguments = ["-xvf", file] process.launch() process.waitUntilExit() } } ================================================ FILE: Aerial/Source/Models/ErrorLog.swift ================================================ // // 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 } final 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 final 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 appSupportPath() -> String { var appPath = "" // Grab an array of Application Support paths let appSupportPaths = NSSearchPathForDirectoriesInDomains( .applicationSupportDirectory, .userDomainMask, true) if appSupportPaths.isEmpty { errorLog("FATAL : app support does not exist!") return "/" } appPath = appSupportPaths[0] let appSupportDirectory = appPath as NSString return appSupportDirectory.appendingPathComponent("Aerial") }*/ // This will clear the existing log if > 1MB // This is called at startup func rollLogIfNeeded() { let cacheDirectory = Cache.supportPath // if let cacheDirectory = path() { var cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) if Aerial.helper.underCompanion { cacheFileUrl.appendPathComponent("AerialUnderCompanionLog.txt") } else { cacheFileUrl.appendPathComponent("AerialLog.txt") } if FileManager.default.fileExists(atPath: cacheFileUrl.path) { do { let resourceValues = try cacheFileUrl.resourceValues(forKeys: [.fileSizeKey]) let fileSize = Int64(resourceValues.fileSize!) if (fileSize > 1000000) { try FileManager.default.removeItem(at: cacheFileUrl) } } catch { logToConsole(error.localizedDescription) } } } // swiftlint:disable:next identifier_name func Log(level: ErrorLevel, message: String) { #if DEBUG print("\(message)\n") #endif errorMessages.append(LogMessage(level: level, message: message)) // We report errors to Console.app 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: %{public}@", log: log, type: .error, message) } else { NSLog("AerialError: \(message)") } } // We may have set callbacks if level == .warning || level == .error || (level == .debug && PrefsAdvanced.debugMode) { Logger.sharedInstance.callBack(level: level) } // Log to disk if PrefsAdvanced.debugMode { logToConsole(message) logToDisk(message) } } func logToConsole(_ message: String) { if #available(OSX 10.12, *) { // This is faster when available let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Screensaver") os_log("Aerial: %{public}@", log: log, type: .default, message) } else { NSLog("Aerial: \(message)") } } func logToDisk(_ message: String) { DispatchQueue.main.async { // Prefix message with date let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" let string = dateFormatter.string(from: Date()) + " : " + message + "\n" // if let cacheDirectory = VideoCache.appSupportDirectory { let cacheDirectory = Cache.supportPath // if let cacheDirectory = path() { var cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) if Aerial.helper.underCompanion { cacheFileUrl.appendPathComponent("AerialUnderCompanionLog.txt") } else { cacheFileUrl.appendPathComponent("AerialLog.txt") } let data = string.data(using: String.Encoding.utf8, allowLossyConversion: false)! if FileManager.default.fileExists(atPath: cacheFileUrl.path) { // Append to log do { let fileHandle = try FileHandle(forWritingTo: cacheFileUrl) fileHandle.seekToEndOfFile() fileHandle.write(data) fileHandle.closeFile() } catch { NSLog("AerialError: Can't open handle for AerialLog.txt") } } else { // Create new log do { try data.write(to: cacheFileUrl, options: .atomic) } catch { NSLog("AerialError: Can't write to file AerialLog.txt") } } // } } } func debugLog(_ message: String) { // Comment the condition to always log debug mode if PrefsAdvanced.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) } ================================================ FILE: Aerial/Source/Models/Extensions/AVAsset+VideoOrientation.swift ================================================ // // AVAsset+VideoOrientation.swift // AVAsset+VideoOrientation // // Created by Guillaume Louel on 26/08/2021. // Copyright © 2021 Guillaume Louel. All rights reserved. // // Created by Wesley Van der Klomp on 1/7/19. // // Translated from: https://gist.github.com/lukabernardi/5020724 // // Modified to add some extra checks import AVFoundation extension AVAsset { enum VideoOrientation { case right, up, left, down static func fromVideoWithAngle(ofDegree degree: CGFloat) -> VideoOrientation? { switch Int(degree) { case 0: return .right case 90: return .up case 180: return .left case -90: return .down default: return nil } } } // This also checks for videos that may have their rotation baked in, // and not provided as a metadata (so 1080x1920 instead of 1920x1080 with 90° rotation) func isVertical() -> Bool { if self.videoOrientation() == .right || self.videoOrientation() == .left { // So at this point this is the natural(ish) orientation, we need to check the width/height let track = self.tracks(withMediaType: .video).first! return track.naturalSize.height > track.naturalSize.width } else { return true } } // This checks for a rotation metadata ONLY which is what works for iPhone videos. func videoOrientation() -> VideoOrientation? { func radiansToDegrees(_ radians: Float) -> CGFloat { return CGFloat(radians * 180.0 / Float.pi) } guard let firstVideoTrack = self.tracks(withMediaType: .video).first else { return nil } let transform = firstVideoTrack.preferredTransform let videoAngleInDegree = radiansToDegrees(atan2f(Float(transform.b), Float(transform.a))) return VideoOrientation.fromVideoWithAngle(ofDegree: videoAngleInDegree) } } import Foundation ================================================ FILE: Aerial/Source/Models/Extensions/AVPlayerItem+vibrance.swift ================================================ // // AVPlayerItem+vibrance.swift // Aerial // // Created by Guillaume Louel on 02/08/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import AVKit extension AVPlayerItem { func setVibrance(_ value: Double) { var useValue = PrefsVideos.globalVibrance if value != 0 { useValue = value } guard useValue != 0 else { return } if #available(OSX 10.14, *) { debugLog("Applying vibrance of \(useValue)") let filter = CIFilter(name: "CIVibrance")! self.videoComposition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in let source = request.sourceImage.clampedToExtent() filter.setValue(source, forKey: kCIInputImageKey) filter.setValue(useValue, forKey: kCIInputAmountKey) let output = filter.outputImage request.finish(with: output!, context: nil) }) } } func setColorInvert() { if #available(OSX 10.14, *) { debugLog("Applying color invert") if let invertFilter = CIFilter(name: "CIColorInvert") { let context = CIContext(options: [.workingColorSpace: CGColorSpace(name: CGColorSpace.sRGB)!]) self.videoComposition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in let source = request.sourceImage.clampedToExtent() invertFilter.setValue(source, forKey: kCIInputImageKey) guard let output = invertFilter.outputImage else { request.finish(with: source, context: nil) return } request.finish(with: output, context: context) }) } } } } ================================================ FILE: Aerial/Source/Models/Extensions/AVPlayerViewExtension.swift ================================================ // // AVPlayerViewExtension.swift // Aerial // // Created by Guillaume Louel on 16/10/2018. // Copyright © 2018 John Coates. All rights reserved. // import Foundation import Cocoa import AVKit extension AVPlayerView { override open func scrollWheel(with event: NSEvent) { // Disable scrolling that can cause accidental video playback control (seek) return } override open func keyDown(with event: NSEvent) { // Disable space key (do not pause video playback) let spaceBarKeyCode = UInt16(49) if event.keyCode == spaceBarKeyCode { return } } } ================================================ FILE: Aerial/Source/Models/Extensions/DispatchQueue+Extension.swift ================================================ // // DispatchQueue+Extension.swift // Aerial // // Created by Guillaume Louel on 28/12/2021. // Copyright © 2021 Guillaume Louel. All rights reserved. // import Foundation extension DispatchQueue { static func background(delay: Double = 0.0, background: (() -> Void)? = nil, completion: (() -> Void)? = nil) { DispatchQueue.global(qos: .background).async { background?() if let completion = completion { DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: { completion() }) } } } } ================================================ FILE: Aerial/Source/Models/Extensions/NSButton+icons.swift ================================================ // // NSButton+icons.swift // Aerial // // Created by Guillaume Louel on 01/08/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation import Cocoa extension NSButton { func setIcons(_ named: String) { self.image = Aerial.helper.getMiniSymbol(named) self.image?.isTemplate = true } func setLargeIcon(_ named: String) { self.image = Aerial.helper.getSymbol(named)!.tinting(with: .secondaryLabelColor) } } ================================================ FILE: Aerial/Source/Models/Extensions/NSImage+trim.swift ================================================ // // NSImage+trim.swift // Aerial // // Created by Guillaume Louel on 23/04/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa extension NSImage { func trim() -> NSImage? { var imageRect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height) let imageRef = self.cgImage(forProposedRect: &imageRect, context: nil, hints: nil) let trimTo = self.getTrimmedRect() // let cutRef = imageRef?.cropping(to: trimTo) guard let cutRef = imageRef?.cropping(to: trimTo) else { return nil } return NSImage(cgImage: cutRef, size: trimTo.size) } // There might be a better way to do this but that's all I found... // swiftlint:disable:next cyclomatic_complexity private func getTrimmedRect() -> CGRect { let bmp = self.representations[0] as! NSBitmapImageRep let data: UnsafeMutablePointer = bmp.bitmapData! var alpha: UInt8 var topCrop = 0 var bottomCrop = bmp.pixelsHigh var leftCrop = 0 var rightCrop = bmp.pixelsWide // Top crop outerTop: for row in 0.. NSImage { guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return self } return NSImage(size: size, flipped: false) { bounds in guard let context = NSGraphicsContext.current?.cgContext else { return false } tintColor.set() context.clip(to: bounds, mask: cgImage) context.fill(bounds) return true } } } ================================================ FILE: Aerial/Source/Models/Extensions/NSMenuItem+icons.swift ================================================ // // NSMenuItem+icons.swift // Aerial // // Created by Guillaume Louel on 30/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa extension NSMenuItem { func setIcons(_ named: String) { self.image = Aerial.helper.getMiniSymbol(named) self.image?.isTemplate = true } } ================================================ FILE: Aerial/Source/Models/Hardware/Battery.swift ================================================ // // Battery.swift // Aerial // // Created by Guillaume Louel on 06/12/2019. // Copyright © 2019 John Coates. All rights reserved. // import Foundation struct Battery { // MARK: - Battery detection static func isUnplugged() -> Bool { return IOPSGetTimeRemainingEstimate() != kIOPSTimeRemainingUnlimited } static func isCharging() -> Bool { let timeRemaining: CFTimeInterval = IOPSGetTimeRemainingEstimate() if timeRemaining == -2.0 { return true } else { return false } } static func isLow() -> Bool { let batteryLevel = getRemainingPercent() // If we have no battery, we'll get 0, so in that case we're NOT low if batteryLevel == 0 { return false } return batteryLevel < 20 } static func getRemainingPercent() -> Int { // Take a snapshot of all the power source info guard let snapshot = IOPSCopyPowerSourcesInfo()?.takeRetainedValue() else { return 0 } // Pull out a list of power sources guard let sources: NSArray = IOPSCopyPowerSourcesList(snapshot)?.takeRetainedValue() else { return 0 } // swiftlint:disable:next empty_count if sources.count > 0 { // For each power source... for ps in sources { // Fetch the information for a given power source out of our snapshot guard let info: NSDictionary = IOPSGetPowerSourceDescription(snapshot, ps as CFTypeRef)?.takeUnretainedValue() else { return 0 } // Pull out the name and current capacity if let capacity = info[kIOPSCurrentCapacityKey] as? Int, let max = info[kIOPSMaxCapacityKey] as? Int { return Int(Double(capacity)/Double(max)*100) } } } return 0 } } ================================================ FILE: Aerial/Source/Models/Hardware/Brightness.swift ================================================ // // Brightness.swift // Aerial // // Created by Guillaume Louel on 18/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Foundation struct Brightness { static func get() -> Float { let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IODisplayConnect")) let pointer = UnsafeMutablePointer.allocate(capacity: 1) IODisplayGetFloatParameter(service, 0, kIODisplayBrightnessKey as CFString, pointer) let brightness = pointer.pointee IOObjectRelease(service) return brightness } static func set(level: Float) { let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IODisplayConnect")) IODisplaySetFloatParameter(service, 0, kIODisplayBrightnessKey as CFString, level) IOObjectRelease(service) } } ================================================ FILE: Aerial/Source/Models/Hardware/DarkMode.swift ================================================ // // DarkMode.swift // Aerial // // Created by Guillaume Louel on 19/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Foundation import Cocoa struct DarkMode { static func isAvailable() -> (Bool, reason: String) { if #available(OSX 10.14, *) { if Aerial.helper.darkMode { return (true, "Your Mac is currently in Dark Mode") } else { return (true, "Your Mac is currently in Light Mode") } } else { // Fallback on earlier versions return (false, "macOS 10.14 Mojave or above is required") } } static func isEnabled() -> Bool { return Aerial.helper.darkMode } } ================================================ FILE: Aerial/Source/Models/Hardware/DisplayDetection.swift ================================================ // // DisplayDetection.swift // Aerial // // Created by Guillaume Louel on 09/05/2019. // Copyright © 2019 John Coates. All rights reserved. // import Foundation import Cocoa class Screen: NSObject { var id: CGDirectDisplayID var width: Int var height: Int var bottomLeftFrame: CGRect var topRightCorner: CGPoint var zeroedOrigin: CGPoint var isMain: Bool var backingScaleFactor: CGFloat init(id: CGDirectDisplayID, width: Int, height: Int, bottomLeftFrame: CGRect, isMain: Bool, backingScaleFactor: CGFloat) { self.id = id self.width = width self.height = height self.bottomLeftFrame = bottomLeftFrame // We precalculate the right corner too, as we will need this ! self.topRightCorner = CGPoint(x: bottomLeftFrame.origin.x + CGFloat(width), y: bottomLeftFrame.origin.y + CGFloat(height)) self.zeroedOrigin = CGPoint(x: 0, y: 0) self.isMain = isMain self.backingScaleFactor = backingScaleFactor } override var description: String { return "[id=\(self.id), width=\(self.width), height=\(self.height), bottomLeftFrame=\(self.bottomLeftFrame), topRightCorner=\(self.topRightCorner), isMain=\(self.isMain), backingScaleFactor=\(self.backingScaleFactor)]" } } // swiftlint:disable:next type_body_length final class DisplayDetection: NSObject { static let sharedInstance = DisplayDetection() var screens = [Screen]() var unusedScreens = [Screen]() var cmInPoints: CGFloat = 40 var maxLeftScreens: CGFloat = 0 var maxBelowScreens: CGFloat = 0 var advancedScreenRect: CGRect? var advancedZeroedScreenRect: CGRect? // MARK: - Lifecycle override init() { super.init() debugLog("📺 Display Detection initialized") detectDisplays() } func resetUnusedScreens() { debugLog("📺 reset unused screens") unusedScreens = screens debugLog("📺☢️ s \(screens.count) us \(unusedScreens.count)") } // MARK: - Detection func detectDisplays() { // Display detection is done in two passes : // - Through CGDisplay, we grab all online screens (connected, but // may or may not be powered on !) and get most information needed // - Through NSScreen to get the backingScaleFactor (retinaness of a screen) // Cleanup a bit in case of redetection screens = [Screen]() maxLeftScreens = 0 maxBelowScreens = 0 // First pass let maxDisplays: UInt32 = 32 var onlineDisplays = [CGDirectDisplayID](repeating: 0, count: Int(maxDisplays)) var displayCount: UInt32 = 0 _ = CGGetOnlineDisplayList(maxDisplays, &onlineDisplays, &displayCount) debugLog("\(displayCount) display(s) detected") var mainID: CGDirectDisplayID? for currentDisplay in onlineDisplays[0.. Int { var count = 0 for screen in screens where screen.height > 200 { count += 1 } return count } // MARK: - Helpers // Regular calculation func calculateZeroedOrigins() { let orect = getGlobalScreenRect() for screen in screens { debugLog("src orig : \(screen.bottomLeftFrame.origin)") let (leftScreens, belowScreens) = detectBorders(forScreen: screen) if leftScreens > maxLeftScreens { maxLeftScreens = leftScreens } if belowScreens > maxBelowScreens { maxBelowScreens = belowScreens } screen.zeroedOrigin = CGPoint(x: screen.bottomLeftFrame.origin.x - orect.origin.x + (leftScreens * leftMargin()), y: screen.bottomLeftFrame.origin.y - orect.origin.y + (belowScreens * belowMargin())) } } // Advanced calculation, this is a bit messy... func calculateAdvancedZeroedOrigins() { // 2 pass, first we calculate the real position of each screen with offsets applied for screen in screens { debugLog("Asrc orig : \(screen.bottomLeftFrame.origin)") var offsetleft: CGFloat = 0 var offsettop: CGFloat = 0 if let display = findDisplayAdvancedMargins(posx: screen.bottomLeftFrame.origin.x, posy: screen.bottomLeftFrame.origin.y) { offsetleft = display.offsetleft offsettop = display.offsettop } // These are NOT zeroed at this point !!! screen.zeroedOrigin = CGPoint(x: screen.bottomLeftFrame.origin.x + (offsetleft * cmInPoints), y: screen.bottomLeftFrame.origin.y + (offsettop * cmInPoints)) } // We get an intermediate representation of whole bunch, non zeroed let irect = getIntermediateAdvancedScreenRect() advancedScreenRect = irect // We store this for later... // And now we zero them ! for screen in screens { screen.zeroedOrigin = CGPoint(x: screen.zeroedOrigin.x - irect.origin.x, y: screen.zeroedOrigin.y - irect.origin.y) debugLog("Zorig : \(screen.zeroedOrigin)") } // Now that zeroed is really zeroed, we can cheat a bit let i0rect = getIntermediateAdvancedScreenRect() advancedZeroedScreenRect = i0rect // We store this for later... let orect = getGlobalScreenRect() debugLog("Orect : \(orect)") } // Border detection // This will work for most cases, but will fail in some grid/tetris like arrangements func detectBorders(forScreen: Screen) -> (CGFloat, CGFloat) { var leftScreens: CGFloat = 0 var belowScreens: CGFloat = 0 for screen in screens where screen != forScreen { if screen.bottomLeftFrame.origin.x < forScreen.bottomLeftFrame.origin.x && screen.bottomLeftFrame.origin.x + CGFloat(screen.width) <= forScreen.bottomLeftFrame.origin.x { leftScreens += 1 } if screen.bottomLeftFrame.origin.y < forScreen.bottomLeftFrame.origin.y && screen.bottomLeftFrame.origin.y + CGFloat(screen.height) <= forScreen.bottomLeftFrame.origin.y { belowScreens += 1 } } debugLog("left \(leftScreens) below \(belowScreens)") return (leftScreens, belowScreens) } func leftMargin() -> CGFloat { return cmInPoints * CGFloat(PrefsDisplays.horizontalMargin) } func belowMargin() -> CGFloat { return cmInPoints * CGFloat(PrefsDisplays.verticalMargin) } func findScreenWith(frame: CGRect) -> Screen? { for screen in screens where frame == screen.bottomLeftFrame { return screen } return nil } func alternateFindScreenWith(frame: CGRect) -> Screen? { debugLog("📺☢️ fs : \(frame.size.debugDescription)") // This is a really simple workaround, we look at the size only, and with the screen list in reverse which seems to kindaaaa match ? // We temporarily ignore bsf as we may not be able to access view.window this early it seems debugLog("📺☢️ s \(screens.count) us \(unusedScreens.count)") for i in (0 ..< unusedScreens.count).reversed() { if unusedScreens[i].bottomLeftFrame.size == frame.size { let foundScreen = unusedScreens[i] unusedScreens.remove(at: i) debugLog("foundScreen : \(foundScreen.bottomLeftFrame.debugDescription)") return foundScreen } } return nil } func findScreenWith(id: CGDirectDisplayID) -> Screen? { for screen in screens where screen.id == id { return screen } return nil } func markScreenAsUsed(id: CGDirectDisplayID) { // remove the screen from the unused list debugLog("pre filter \(unusedScreens.count)") let filteredScreens = unusedScreens.filter { $0.id != id } unusedScreens = filteredScreens debugLog("post filter \(unusedScreens.count)") } // Calculate the size of the global screen (the composite of all the displays attached) func getGlobalScreenRect() -> CGRect { if PrefsDisplays.displayMarginsAdvanced && !advancedMargins.displays.isEmpty, let adv = advancedScreenRect { // Now this is awkward... we precalculated this at detectdisplays->advancedZeroedOrigins return adv } else { var minX: CGFloat = 0.0, minY: CGFloat = 0.0, maxX: CGFloat = 0.0, maxY: CGFloat = 0.0 for screen in screens { if screen.bottomLeftFrame.origin.x < minX { minX = screen.bottomLeftFrame.origin.x } if screen.bottomLeftFrame.origin.y < minY { minY = screen.bottomLeftFrame.origin.y } if screen.topRightCorner.x > maxX { maxX = screen.topRightCorner.x } if screen.topRightCorner.y > maxY { maxY = screen.topRightCorner.y } } return CGRect(x: minX, y: minY, width: maxX-minX+(maxLeftScreens*leftMargin()), height: maxY-minY+(maxBelowScreens*belowMargin())) } } func getIntermediateAdvancedScreenRect() -> CGRect { // At this point, this is non zeroed var minX: CGFloat = 0.0, minY: CGFloat = 0.0, maxX: CGFloat = 0.0, maxY: CGFloat = 0.0 for screen in screens { if screen.zeroedOrigin.x < minX { minX = screen.zeroedOrigin.x } if screen.zeroedOrigin.y < minY { minY = screen.zeroedOrigin.y } if (screen.zeroedOrigin.x + CGFloat(screen.width)) > maxX { maxX = screen.zeroedOrigin.x + CGFloat(screen.width) } if (screen.zeroedOrigin.y + CGFloat(screen.height)) > maxY { maxY = screen.zeroedOrigin.y + CGFloat(screen.height) } } return CGRect(x: minX, y: minY, width: maxX-minX, height: maxY-minY) } func getZeroedActiveSpannedRect() -> CGRect { if PrefsDisplays.displayMarginsAdvanced && !advancedMargins.displays.isEmpty, let advz = advancedZeroedScreenRect { // Now this is awkward... we precalculated this at detectdisplays->advancedZeroedOrigins return advz } else { var minX: CGFloat = 0.0, minY: CGFloat = 0.0, maxX: CGFloat = 0.0, maxY: CGFloat = 0.0 for screen in screens where isScreenActive(id: screen.id) { if screen.bottomLeftFrame.origin.x < minX { minX = screen.bottomLeftFrame.origin.x } if screen.bottomLeftFrame.origin.y < minY { minY = screen.bottomLeftFrame.origin.y } if screen.topRightCorner.x > maxX { maxX = screen.topRightCorner.x } if screen.topRightCorner.y > maxY { maxY = screen.topRightCorner.y } } let width = maxX - minX let height = maxY - minY // Zero the origin to the global rect let orect = getGlobalScreenRect() minX -= orect.origin.x minY -= orect.origin.y return CGRect(x: minX, y: minY, width: width+(maxLeftScreens*leftMargin()), height: height+(maxBelowScreens*belowMargin())) } } // NSScreen coordinates are with a bottom left origin, whereas CGDisplay // coordinates are top left origin, this function converts the origin.y value func convertTopLeftToBottomLeft(rect: CGRect) -> CGRect { let screenFrame = (NSScreen.main?.frame)! let newY = 0 - (rect.origin.y - screenFrame.size.height + rect.height) return CGRect(x: rect.origin.x, y: newY, width: rect.width, height: rect.height) } // MARK: - Public utility fuctions func isScreenActive(id: CGDirectDisplayID) -> Bool { let screen = findScreenWith(id: id) debugLog("ISA : \(screen)") switch PrefsDisplays.displayMode { case .allDisplays: // This one is easy return true case .mainOnly: if let scr = screen { if scr.isMain { return true } } return false case .secondaryOnly: if getScreenCount() > 1 { if let scr = screen { if scr.isMain { return false } } } return true case .selection: if isScreenSelected(id: id) { return true } return false } } func isScreenSelected(id: CGDirectDisplayID) -> Bool { // If we have it in the dictionnary, then return that if PrefsAdvanced.newDisplayDict.keys.contains(String(id)) { return PrefsAdvanced.newDisplayDict[String(id)]! } return false // Unknown screens will not be considered selected } func selectScreen(id: CGDirectDisplayID) { PrefsAdvanced.newDisplayDict[String(id)] = true } func unselectScreen(id: CGDirectDisplayID) { PrefsAdvanced.newDisplayDict[String(id)] = false } func getMarginsJSON() -> String { var adv: AdvancedMargin if !advancedMargins.displays.isEmpty { // If we have something already in preferences, return that adv = advancedMargins } else { // Generate a JSON from current config var marginArray = [DisplayAdvancedMargin]() for screen in screens { let zleft = screen.bottomLeftFrame.origin.x let ztop = screen.bottomLeftFrame.origin.y let (leftScreens, belowScreens) = detectBorders(forScreen: screen) let offsetleft = leftScreens * CGFloat(PrefsDisplays.horizontalMargin) let offsettop = belowScreens * CGFloat(PrefsDisplays.verticalMargin) marginArray.append(DisplayAdvancedMargin(zleft: zleft, ztop: ztop, offsetleft: offsetleft, offsettop: offsettop)) } adv = AdvancedMargin(displays: marginArray) } let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted do { let jsonData = try encoder.encode(adv) if let jsonString = String(data: jsonData, encoding: .utf8) { return jsonString } } catch { errorLog(error.localizedDescription) } return "" } func findDisplayAdvancedMargins(posx: CGFloat, posy: CGFloat) -> DisplayAdvancedMargin? { for display in advancedMargins.displays { if posx == display.zleft && posy == display.ztop { return display } } return nil } var advancedMargins: AdvancedMargin { get { let jsonString = PrefsDisplays.advancedMargins if let jsonData = jsonString.data(using: .utf8) { let decoder = JSONDecoder() do { let adv = try decoder.decode(AdvancedMargin.self, from: jsonData) return adv } catch { errorLog(error.localizedDescription) } } return AdvancedMargin(displays: [DisplayAdvancedMargin]()) } set { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted do { let jsonData = try encoder.encode(newValue) if let jsonString = String(data: jsonData, encoding: .utf8) { PrefsDisplays.advancedMargins = jsonString } } catch { errorLog(error.localizedDescription) } } } } struct AdvancedMargin: Codable { let displays: [DisplayAdvancedMargin] } struct DisplayAdvancedMargin: Codable { var zleft: CGFloat var ztop: CGFloat var offsetleft: CGFloat var offsettop: CGFloat } ================================================ FILE: Aerial/Source/Models/Hardware/HardwareDetection.swift ================================================ // // HardwareDetection.swift // Aerial // // Created by Guillaume Louel on 03/06/2019. // Copyright © 2019 John Coates. All rights reserved. // // When available, macOS will use the fixed functions units in Intel CPUs (QuickSync) for hardware // decoding of H.264 and H.265, independent of if there's a GPU present. // This is an issue as H.265 decoding is only partially supported on some Intel CPUs, up to Kaby Lake // generation where they support Main profile decoding, but not Main10 (which is used by Apple's videos). // // Mode info can be found here : https://github.com/JohnCoates/Aerial/blob/master/Documentation/HardwareDecoding.md import Foundation enum HEVCMain10Support: Int { case notsupported, unsure, partial, supported } final class HardwareDetection: NSObject { static let sharedInstance = HardwareDetection() // MARK: - Mac Model detection private func getMacModel() -> String { var size = 0 sysctlbyname("hw.model", nil, &size, nil, 0) var machine = [CChar](repeating: 0, count: size) sysctlbyname("hw.model", &machine, &size, nil, 0) return String(cString: machine) } private func extractMacVersion(macModel: String, macSubmodel: String) -> Double { // Substring the thing let str = String(macModel.dropFirst(macSubmodel.count)) let formatter = NumberFormatter() formatter.locale = Locale(identifier: "fr_FR") return formatter.number(from: str)?.doubleValue ?? 0.0 } // Get best suggestion // swiftlint:disable:next cyclomatic_complexity func getSuggestedFormat() -> VideoFormat { switch isHEVCMain10HWDecodingAvailable() { case .supported: if #available(OSX 10.15, *) { return .v4KHDR } else { // That was a fun one to track... return .v4KHEVC } case .notsupported: return .v1080pH264 case .partial: // This is tricky let macModel = getMacModel() if macModel.starts(with: "iMac") { // iMacs, as far as we know, partial 17+, full 18+ let ver = extractMacVersion(macModel: macModel, macSubmodel: "iMac") if ver >= 17.0 { return .v4KHEVC } else { return .v1080pH264 } } else if macModel.starts(with: "MacBookPro") { let ver = extractMacVersion(macModel: macModel, macSubmodel: "MacBookPro") // MacBookPro full 14+ if ver >= 17.0 { return .v1080pHEVC } else { return .v1080pH264 } } else if macModel.starts(with: "MacBookAir") { // Retina 8+, I *think* they handle main10 return .v1080pH264 } else if macModel.starts(with: "MacBook") { // MacBook 10+ return .v1080pH264 } return .v1080pH264 case .unsure: // Eh return .v1080pH264 } } // MARK: - HEVC Main10 detection func isHEVCMain10HWDecodingAvailable() -> HEVCMain10Support { let macModel = getMacModel() // Apple silicon supports everything! if isAppleSilicon() { return .supported } // This is a manually compiled list based on CPU generations of each mac model line if macModel.starts(with: "iMacPro") || macModel.starts(with: "ADP") { // iMacPro - always return .supported } else if macModel.starts(with: "iMac") { // iMacs, as far as we know, partial 17+, full 18+ return getHEVCMain10Support(macModel: macModel, macSubmodel: "iMac", partial: 17.0, full: 18.0) } else if macModel.starts(with: "MacBookPro") { // MacBookPro full 14+ return getHEVCMain10Support(macModel: macModel, macSubmodel: "MacBookPro", partial: 13.0, full: 14.0) } else if macModel.starts(with: "MacBookAir") { // Retina 8+, I *think* they handle main10 return getHEVCMain10Support(macModel: macModel, macSubmodel: "MacBookAir", partial: 8.0, full: 8.0) } else if macModel.starts(with: "MacBook") { // MacBook 10+ return getHEVCMain10Support(macModel: macModel, macSubmodel: "MacBook", partial: 9.0, full: 10.0) } else if macModel.starts(with: "Macmini") { // MacMini 8+ return getHEVCMain10Support(macModel: macModel, macSubmodel: "Macmini", partial: 8.0, full: 8.0) } else if macModel.starts(with: "MacPro") { // Tentative, I *think* 7+ (2019 MacPro) should always support independant of GPU, akin to iMac Pro ? return getHEVCMain10Support(macModel: macModel, macSubmodel: "MacPro", partial: 7.0, full: 7.0) } // Older stuff (power/etc) should not even run, so list should be complete // Hackintosh/new SKUs will fail this test, this is indicative in any case so that's fine return .unsure } // Helper private func getHEVCMain10Support(macModel: String, macSubmodel: String, partial: Double, full: Double) -> HEVCMain10Support { let ver = extractMacVersion(macModel: macModel, macSubmodel: macSubmodel) if ver >= full { return .supported } else if ver >= partial { return .partial } else { return .notsupported } } func isAppleSilicon() -> Bool { if #available(macOS 12, *) { var systeminfo = utsname() uname(&systeminfo) let machine = withUnsafeBytes(of: &systeminfo.machine) {bufPtr->String in let data = Data(bufPtr) if let lastIndex = data.lastIndex(where: {$0 != 0}) { return String(data: data[0...lastIndex], encoding: .isoLatin1)! } else { return String(data: data, encoding: .isoLatin1)! } } debugLog(machine) return machine != "x86_64" } else { return false } } } ================================================ FILE: Aerial/Source/Models/Hardware/ISSoundAdditions/Sound.swift ================================================ // // SoundOutputManager.swift // // // Created by Alessio Moiso on 08.03.22. // /// Entry point to access and modify the system sound settings, such /// muting/unmuting and changing the volume. /// /// # Overview /// This class cannot be instantiated, but you can interact with its `output` property directly. /// You can use the shared instance to change the output volume as well as /// mute and unmute. public enum Sound { static let output = SoundOutputManager() } ================================================ FILE: Aerial/Source/Models/Hardware/ISSoundAdditions/SoundOutputManager+Goodies.swift ================================================ // // File.swift // // // Created by Alessio Moiso on 09.03.22. // public extension Sound.SoundOutputManager { /// Increase the volume of the default output device /// by the given amount. /// /// Errors will be ignored. /// /// The values range between 0 and 1. If the increase results /// in a value outside of the bounds, it will be normalized to the closest /// value in the bounds. func increaseVolume(by value: Float, autoMuteUnmute: Bool = false, muteThreshold: Float = 0.005) { setVolume(volume+value, autoMuteUnmute: autoMuteUnmute, muteThreshold: muteThreshold) } /// Decrease the volume of the default output device /// by the given amount. /// /// Errors will be ignored. /// /// The values range between 0 and 1. If the decrease results /// in a value outside of the bounds, it will be normalized to the closest /// value in the bounds. func decreaseVolume(by value: Float, autoMuteUnmute: Bool = false, muteThreshold: Float = 0.005) { setVolume(volume-value, autoMuteUnmute: autoMuteUnmute, muteThreshold: muteThreshold) } /// Set the volume of the default output device and, /// if lower or higher then `muteThreshold` also toggles the mute property. /// /// - warning: This function will unmute a muted device, if the volume is greater /// then `muteThreshold`. Please, make sure that the user is aware of this and always /// respect the Do Not Disturb modes and other system settings. /// /// - parameters: /// - newValue: The volume. /// - autoMuteUnmute: If `true`, will use the `muteThreshold` to determine whether the device /// should also be muted or unmuted. /// - muteThreshold: Defines the threshold that should cause an automatic mute for all values below it. func setVolume(_ newValue: Float, autoMuteUnmute: Bool, muteThreshold: Float = 0.005) { volume = newValue guard autoMuteUnmute else { return } isMuted = newValue <= muteThreshold } } ================================================ FILE: Aerial/Source/Models/Hardware/ISSoundAdditions/SoundOutputManager+Properties.swift ================================================ // // SoundOutputManager+Properties.swift // // // Created by Alessio Moiso on 09.03.22. // import CoreAudio public extension Sound.SoundOutputManager { /// Get the system default output device. /// /// You can use this value to interact with the device directly via /// other system calls. /// /// This value will return `nil` if there is currently no device selected in /// System Preferences > Sound > Output. var defaultOutputDevice: AudioDeviceID? { try? retrieveDefaultOutputDevice() } /// Get or set the volume of the default output device. /// /// Errors will be ignored. If you need to handle errors, /// use `readVolume` and `setVolume`. /// /// The values range between 0 and 1. var volume: Float { get { (try? readVolume()) ?? 0 } set { do { try setVolume(newValue) } catch { } } } /// Get or set whether the system default output device is muted or not. /// /// Errors will be ignored. If you need to handle errors, /// use `readMute` and `mute`. Devices that do not support muting /// will always return `false`. var isMuted: Bool { get { (try? readMute()) ?? false } set { do { try mute(newValue) } catch { } } } } ================================================ FILE: Aerial/Source/Models/Hardware/ISSoundAdditions/SoundOutputManager.swift ================================================ // // SoundOutputManager.swift // // // Created by Alessio Moiso on 08.03.22. // import CoreAudio import AudioToolbox import Cocoa extension Sound { /// Mute, unmute and change the volume of the system default output device. /// /// # Overview /// You can interact with this class in two ways: /// - you can interact with its properties, meaning that all changes /// will be applied immediately and errors will be hidden. /// - you can call its methods and handle errors manually. public final class SoundOutputManager { /// All the possible errors that could occur while interacting /// with the default output device. enum Errors: Error { /// The system couldn't complete the requested operation and /// returned the given status. case operationFailed(OSStatus) /// The current default output device doesn't support the requested property. case unsupportedProperty /// The current default output device doesn't allow changing the requested property. case immutableProperty /// There is no default output device. case noDevice } internal init() { } /// Get the system default output device. /// /// You can use this value to interact with the device directly /// via other system calls. /// /// - throws: `Errors.operationFailed` if the system fails to return the default output device. /// - returns: the default device ID or `nil` if none is set. public func retrieveDefaultOutputDevice() throws -> AudioDeviceID? { var result = kAudioObjectUnknown var size = UInt32(MemoryLayout.size) var address = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDefaultOutputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) // Ensure that a default device exists. guard AudioObjectHasProperty(AudioObjectID(kAudioObjectSystemObject), &address) else { return nil } // Attempt to get the default output device. let error = AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size, &result) guard error == noErr else { throw Errors.operationFailed(error) } if result == kAudioObjectUnknown { throw Errors.noDevice } return result } /// Get the volume of the system default output device. /// /// - throws: `Errors.noDevice` if the system doesn't have a default output device; `Errors.unsupportedProperty` if the current device doesn't have a volume property; `Errors.operationFailed` if the system is unable to read the property value. /// - returns: The current volume in a range between 0 and 1. public func readVolume() throws -> Float { guard let deviceID = try retrieveDefaultOutputDevice() else { throw Errors.noDevice } var size = UInt32(MemoryLayout.size) var volume: Float = 0 var address = AudioObjectPropertyAddress( mSelector: kAudioHardwareServiceDeviceProperty_VirtualMainVolume, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain ) // Ensure the device has a volume property. guard AudioObjectHasProperty(deviceID, &address) else { throw Errors.unsupportedProperty } let error = AudioObjectGetPropertyData(deviceID, &address, 0, nil, &size, &volume) guard error == noErr else { throw Errors.operationFailed(error) } return min(max(0, volume), 1) } /// Set the volume of the system default output device. /// /// - parameter newValue: The volume to set in a range between 0 and 1. /// - throws: `Erors.noDevice` if the system doesn't have a default output device; `Errors.unsupportedProperty` or `Errors.immutableProperty` if the output device doesn't support setting or doesn't currently allow changes to its volume; `Errors.operationFailed` if the system is unable to apply the volume change. public func setVolume(_ newValue: Float) throws { guard let deviceID = try retrieveDefaultOutputDevice() else { throw Errors.noDevice } var normalizedValue = min(max(0, newValue), 1) var address = AudioObjectPropertyAddress( mSelector: kAudioHardwareServiceDeviceProperty_VirtualMainVolume, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain ) // Ensure the device has a volume property. guard AudioObjectHasProperty(deviceID, &address) else { throw Errors.unsupportedProperty } var canChangeVolume = DarwinBoolean(true) let size = UInt32(MemoryLayout.size(ofValue: normalizedValue)) let isSettableError = AudioObjectIsPropertySettable(deviceID, &address, &canChangeVolume) // Ensure the volume property is editable. guard isSettableError == noErr else { throw Errors.operationFailed(isSettableError) } guard canChangeVolume.boolValue else { throw Errors.immutableProperty } let error = AudioObjectSetPropertyData(deviceID, &address, 0, nil, size, &normalizedValue) if error != noErr { throw Errors.operationFailed(error) } } /// Get whether the system default output device is currently muted or not. /// /// - throws: `Errors.noDevice` if the system doesn't have a default output device; /// `Errors.unsupportedProperty` if the current device doesn't have a mute property; /// `Errors.operationFailed` if the system is unable to read the property value. /// - returns: Whether the device is muted or not. public func readMute() throws -> Bool { guard let deviceID = try retrieveDefaultOutputDevice() else { throw Errors.noDevice } var isMuted: UInt32 = 0 var size = UInt32(MemoryLayout.size(ofValue: isMuted)) var address = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyMute, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain ) // Ensure the device supports the option to be muted. guard AudioObjectHasProperty(deviceID, &address) else { throw Errors.unsupportedProperty } let error = AudioObjectGetPropertyData(deviceID, &address, 0, nil, &size, &isMuted) guard error == noErr else { throw Errors.operationFailed(error) } return isMuted == 1 } /// Mute or unmute the system default output device. /// /// - parameter isMuted: Mute or unmute. /// - throws: `Errors.noDevice` if the system doesn't have a default output device; /// `Errors.unsupportedProperty` or `Errors.immutableProperty` if the output device doesn't /// support setting or doesn't currently allow changes to its mute property; `Errors.operationFailed` /// if the system is unable to apply the change. public func mute(_ isMuted: Bool) throws { guard let deviceID = try retrieveDefaultOutputDevice() else { throw Errors.noDevice } var normalizedValue: UInt = isMuted ? 1 : 0 var address = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyMute, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMain ) // Ensure the device supports the option to be muted. guard AudioObjectHasProperty(deviceID, &address) else { throw Errors.unsupportedProperty } var canMute = DarwinBoolean(true) let size = UInt32(MemoryLayout.size(ofValue: normalizedValue)) let isSettableError = AudioObjectIsPropertySettable(deviceID, &address, &canMute) // Ensure that the mute property is editable. guard isSettableError == noErr, canMute.boolValue else { throw Errors.immutableProperty } let error = AudioObjectSetPropertyData(deviceID, &address, 0, nil, size, &normalizedValue) if error != noErr { throw Errors.operationFailed(error) } } } } ================================================ FILE: Aerial/Source/Models/Hardware/NightShift.swift ================================================ // // NightShift.swift // Aerial // // Created by Guillaume Louel on 19/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Foundation struct NightShift { static var isNightShiftDataCached = false static var nightShiftAvailable = false static var nightShiftSunrise = Date() static var nightShiftSunset = Date() // MARK: Night Shift static func isAvailable() -> (Bool, reason: String) { if #available(OSX 10.12.4, *) { let (isAvailable, sunriseDate, sunsetDate, errorMessage) = NightShift.getInformation() if isAvailable { let dateFormatter = DateFormatter() dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "j:mm:ss", options: 0, locale: Locale.current) let sunriseString = dateFormatter.string(from: sunriseDate!) let sunsetString = dateFormatter.string(from: sunsetDate!) return (true, "Today’s sunrise: " + sunriseString + " Today’s sunset: " + sunsetString) } else { isNightShiftDataCached = true return (false, errorMessage!) } } else { return (false, "macOS 10.12.4 or above is required") } } // swiftlint:disable:next large_tuple static func getInformation() -> (Bool, sunrise: Date?, sunset: Date?, error: String?) { // Sonoma workaround if !Aerial.helper.underCompanion { if #available(macOS 14.0, *) { if CompanionBridge.nightShiftSunrise != nil { debugLog("Nightshift using CompanionBridge data") return (true, CompanionBridge.nightShiftSunrise, CompanionBridge.nightShiftSunset, nil) } else { return (false, nil, nil, "Sonoma requires Aerial Companion") } } } if isNightShiftDataCached { return (nightShiftAvailable, nightShiftSunrise, nightShiftSunset, nil) } // On Catalina, corebrightnessdiag was moved to /usr/libexec/ ! var cbdpath = "/usr/bin/corebrightnessdiag" if #available(OSX 10.15, *) { cbdpath = "/usr/libexec/corebrightnessdiag" } let (nsInfo, ts) = Aerial.helper.shell(launchPath: cbdpath, arguments: ["nightshift-internal"]) if ts != 0 { // Task didn't return correctly ? Abort return (false, nil, nil, "Your Mac does not support Night Shift") } let lines = nsInfo?.split(separator: "\n") if lines!.count < 5 { // We get a couple of lines of output on unsupported Macs return (false, nil, nil, "Your Mac does not support Night Shift") } var sunrise: Date?, sunset: Date? for line in lines ?? [""] { if line.contains("sunrise") { if let gdate = getDateFromLine(String(line)) { sunrise = gdate } } else if line.contains("sunset") { if let gdate = getDateFromLine(String(line)) { sunset = gdate } } } if sunset != nil && sunrise != nil { nightShiftSunrise = sunrise! nightShiftSunset = sunset! nightShiftAvailable = true isNightShiftDataCached = true return (true, sunrise, sunset, nil) } // /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") } // Helpers private static func getDateFromLine(_ line: String) -> Date? { let tmp = line.split(separator: "\"") if tmp.count > 1 { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss ZZZ" if let dateObj = dateFormatter.date(from: String(tmp[1])) { return dateObj } } return nil } } ================================================ FILE: Aerial/Source/Models/Locations.swift ================================================ // // Location.swift // Aerial // // Created by Guillaume Louel on 24/05/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation import CoreLocation class Locations: NSObject { static let sharedInstance = Locations() let locationManager = CLLocationManager() var coordinates: CLLocationCoordinate2D? var failures: [(String) -> Void] = [] var successes: [(CLLocationCoordinate2D) -> Void] = [] // MARK: - Lifecycle override init() { super.init() locationManager.delegate = self debugLog("Starting Location initialization") } func getCoordinates(failure: @escaping (_ error: String) -> Void, success: @escaping (_ response: CLLocationCoordinate2D) -> Void) { // Sonoma workaround via CompanionBridge if !Aerial.helper.underCompanion { if #available(macOS 14.0, *) { if CompanionBridge.locationLat != nil && CompanionBridge.locationLong != nil { debugLog("Location using CompanionBridge data") let coords = CLLocationCoordinate2DMake( CompanionBridge.locationLat! as CLLocationDegrees, CompanionBridge.locationLong! as CLLocationDegrees) success(coords) return } } } // Perhaps they are cached already ? if coordinates != nil { debugLog("Location using cached data") success(coordinates!) return } // Check for access & start if CLLocationManager.locationServicesEnabled() { debugLog("Location services enabled") locationManager.startUpdatingLocation() } else { debugLog("Location services disabled") if PrefsTime.cachedLatitude != 0 { debugLog("Couldn't retrieve your location, using latest cached coordinates instead") // Read them coordinates = CLLocationCoordinate2DMake( PrefsTime.cachedLatitude as CLLocationDegrees, PrefsTime.cachedLongitude as CLLocationDegrees) // Pretend we didn't fail success(coordinates!) } else { debugLog("No cached coordinates") failure("Location services disabled") } } // This seems super wrong... if #available(OSX 10.14, *) { // Add our callbacks, as this is the only time we'll really defer failures.append(failure) successes.append(success) locationManager.requestLocation() } else { // Fallback on earlier versions failure("macOS 10.14 is required") } } } extension Locations: CLLocationManagerDelegate { // Auth status callback func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { debugLog("LMauth status change : \(status.rawValue)") if status == .denied { if PrefsTime.cachedLatitude != 0 { debugLog("Couldn't retrieve your location, using latest cached coordinates instead") // Read them coordinates = CLLocationCoordinate2DMake( PrefsTime.cachedLatitude as CLLocationDegrees, PrefsTime.cachedLongitude as CLLocationDegrees) // Pretend we didn't fail for success in successes { success(coordinates!) } // Then cleanup successes.removeAll() failures.removeAll() } else { debugLog("Location services are either globally disabled, or disabled for Aerial. Please enable them at least once so Aerial can get your coordinates, or use another Time management mode.") } } } // Location fetch Success callback func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { let currentLocation = locations[locations.count - 1] coordinates = currentLocation.coordinate // Wondering, why singular? debugLog("Location coordinate : \(currentLocation.coordinate)") locationManager.stopUpdatingLocation() // We only want them once // We cache for next time if we are WiFi-less PrefsTime.cachedLatitude = coordinates?.latitude ?? 0 PrefsTime.cachedLongitude = coordinates?.longitude ?? 0 for success in successes { success(currentLocation.coordinate) } successes.removeAll() failures.removeAll() } // Location fetch Failure callback func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { // So we failed, but maybe we have something cached to pretent we didn't fail if PrefsTime.cachedLatitude != 0 { debugLog("Couldn't retrieve your location: \(error.localizedDescription), using latest cached coordinates instead") // Store them coordinates = CLLocationCoordinate2DMake(PrefsTime.cachedLatitude as CLLocationDegrees, PrefsTime.cachedLongitude as CLLocationDegrees) // Pretend we didn't fail for success in successes { success(coordinates!) } } else { // This is a total failure for failure in failures { failure("Unable to fetch location") } } // Then cleanup successes.removeAll() failures.removeAll() } } ================================================ FILE: Aerial/Source/Models/ManifestLoader.swift ================================================ // // ManifestLoader.swift // Aerial // WARNING : This is the old deprecated stuff // // Created by John Coates on 10/28/15. // Copyright © 2015 John Coates. All rights reserved. // import Foundation import ScreenSaver import GameplayKit import AVFoundation typealias ManifestLoadCallback = ([AerialVideo]) -> Void // swiftlint:disable:next type_body_length class ManifestLoader { static let instance: ManifestLoader = ManifestLoader() var callbacks = [ManifestLoadCallback]() var loadedManifest = [AerialVideo]() var processedVideos = [AerialVideo]() var lastPluckedFromPlaylist: AerialVideo? var customVideoFolders: CustomVideoFolders? var manifestTvOS10: Data? var manifestTvOS11: Data? var manifestTvOS12: Data? var manifestTvOS13: Data? // Playlist management var playlistIsRestricted = false var playlistRestrictedTo = "" var playlist = [AerialVideo]() // Those videos will be ignored let blacklist = ["b10-1.mov", // Dupe of b1-1 (Hawaii, day) "b10-2.mov", // Dupe of b2-3 (New York, night) "b10-4.mov", // Dupe of b2-4 (San Francisco, night) "b9-1.mov", // Dupe of b2-2 (Hawaii, day) "b9-2.mov", // Dupe of b3-1 (London, night) "comp_LA_A005_C009_v05_t9_6M.mov", // Low quality version of Los Angeles day 687B36CB-BA5D-4434-BA99-2F2B8B6EC163 "comp_LA_A009_C009_t9_6M_tag0.mov" ] // Low quality version of Los Angeles night 89B1643B-06DD-4DEC-B1B0-774493B0F7B7 // This is used for videos where URLs should be merged with different ID // This is used to dedupe old versions of videos // old : new let dupePairs = [ "A2BE2E4A-AD4B-428A-9C41-BDAE1E78E816": "12318CCB-3F78-43B7-A854-EFDCCE5312CD", // California to Vegas (v7 -> v8) "6A74D52E-2447-4B84-AE45-0DEF2836C3CC": "7825C73A-658F-48EE-B14C-EC56673094AC", // China "6C3D54AE-0871-498A-81D0-56ED24E5FE9F": "009BA758-7060-4479-8EE8-FB9B40C8FB97", // Korean and Japan night "b5-1": "044AD56C-A107-41B2-90CC-E60CCACFBCF5", // Great Wall 3 "b2-1": "22162A9B-DB90-4517-867C-C676BC3E8E95", // Great wall 2 "b6-1": "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF", // Great wall 1 "BAF76353-3475-4855-B7E1-CE96CC9BC3A7": "9680B8EB-CE2A-4395-AF41-402801F4D6A6", // Approaching Burj Khalifa (night) "B3BDC635-756D-4B82-B01A-A2620D1DBF10": "9680B8EB-CE2A-4395-AF41-402801F4D6A6", // Approaching Burj Khalifa (night) "15F9B681-9EA8-4DD1-AD26-F111BC5CF64B": "E991AC0C-F272-44D8-88F3-05F44EDFE3AE", // Marina 1 "49790B7C-7D8C-466C-A09E-83E38B6BE87A": "E991AC0C-F272-44D8-88F3-05F44EDFE3AE", // Marina 1 "802866E6-4AAF-4A69-96EA-C582651391F1": "3FFA2A97-7D28-49EA-AA39-5BC9051B2745", // Marina 2 "D34A7B19-EC33-4300-B4ED-0C8BC494C035": "3FFA2A97-7D28-49EA-AA39-5BC9051B2745", // Marina 2 "02EA5DBE-3A67-4DFA-8528-12901DFD6CC1": "00BA71CD-2C54-415A-A68A-8358E677D750", // Downtown "AC9C09DD-1D97-4013-A09F-B0F5259E64C3": "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9", // Sheikh Zayed Road (day) "DFA399FA-620A-4517-94D6-BF78BF8C5E5A": "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9", // Sheikh Zayed Road (day) "D388F00A-5A32-4431-A95C-38BF7FF7268D": "B8F204CE-6024-49AB-85F9-7CA2F6DCD226", // Nuusuaq Peninsula "E4ED0B22-EB81-4D4F-A29E-7E1EA6B6D980": "B8F204CE-6024-49AB-85F9-7CA2F6DCD226", // Nuusuaq Peninsula "30047FDA-3AE3-4E74-9575-3520AD77865B": "2F52E34C-39D4-4AB1-9025-8F7141FAA720", // Ilulissat Icefjord day "7D4710EB-5BA4-42E6-AA60-68D77F67D9B9": "EE01F02D-1413-436C-AB05-410F224A5B7B", // Ilulissat Icefjord Night "b8-1": "82BD33C9-B6D2-47E7-9C42-AA3B7758921A", // Pu'u O 'Umi Night "b4-1": "258A6797-CC13-4C3A-AB35-4F25CA3BF474", // Pu'u O 'Umi day "b1-1": "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7", // Waimanu Valley "b7-1": "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943", // Laupāhoehoe Nui "b6-2": "3D729CFC-9000-48D3-A052-C5BD5B7A6842", // Kohala coastline "30313BC1-BF20-45EB-A7B1-5A6FFDBD2488": "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A", // Hong Kong Victoria Harbour night "2A57BB93-1825-484C-9609-FF8580CAE77B": "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A", // Hong Kong Victoria Harbour night "102C19D1-9D9F-48EC-B492-074C985C4D9F": "FE8E1F9D-59BA-4207-B626-28E34D810D0A", // Hong Kong Victoria Harbour 1 "786E674C-BB22-4AA9-9BD3-114D2020EC4D": "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA", // Hong Kong Victoria Harbour 2 "560E09E8-E89D-4ADB-8EEA-4754415383D4": "C8559883-6F3E-4AF2-8960-903710CD47B7", // Hong Kong Victoria Peak "6E2FC8AC-832D-46CF-B306-BB2A05030C17": "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8", // Liwa oasis 1 "88025454-6D58-48E8-A2DB-924988FAD7AC": "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8", // Liwa oasis 1 "b6-3": "58754319-8709-4AB0-8674-B34F04E7FFE2", // River Thames "b1-2": "F604AF56-EA77-4960-AEF7-82533CC1A8B3", // River Thames near sunset "b3-1": "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97", // River Times at Dusk "b5-2": "A5AAFF5D-8887-42BB-8AFD-867EF557ED85", // Buckingham Palace "BEED64EC-2DB7-47E1-A67E-59C101E73C04": "CE279831-1CA7-4A83-A97B-FF1E20234396", // LAX "829E69BA-BB53-4841-A138-4DF0C2A74236": "CE279831-1CA7-4A83-A97B-FF1E20234396", // LAX "60CD8E2E-35CD-4192-A5A4-D5E10BFE158B": "92E48DE9-13A1-4172-B560-29B4668A87EE", // Santa Monica Beach "B730433D-1B3B-4B99-9500-A286BF7A9940": "92E48DE9-13A1-4172-B560-29B4668A87EE", // Santa Monica Beach "30A2A488-E708-42E7-9A90-B749A407AE1C": "35693AEA-F8C4-4A80-B77D-C94B20A68956", // Harbor Freeway "A284F0BF-E690-4C13-92E2-4672D93E8DE5": "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9", // Downtown "b3-2": "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8", // Upper East side "b4-2": "640DFB00-FBB9-45DA-9444-9F663859F4BC", // Lower Manhattan (night) "b2-3": "44166C39-8566-4ECA-BD16-43159429B52F", // Seventh Avenue "b7-2": "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89", // Central Park "b10-3": "EE533FBD-90AE-419A-AD13-D7A60E2015D6", // Marin Headlands in Fog "b1-4": "3E94AE98-EAF2-4B09-96E3-452F46BC114E", // Bay bridge night "b9-3": "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51", // Bay and Golden Bridge "b7-3": "29BDF297-EB43-403A-8719-A78DA11A2948", // Fisherman's Wharf "b3-3": "85CE77BF-3413-4A7B-9B0F-732E96229A73" // Embarcadero, Market Street ] // Extra info to be merged for a given ID, as of right now only one known video let mergeInfo = [ "2F11E857-4F77-4476-8033-4A1E4610AFCC": ["url-1080-SDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_2K_SDR_HEVC.mov", "url-1080-HDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_2K_HDR_HEVC.mov", "url-4K-SDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_4K_SDR_HEVC.mov", "url-4K-HDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_4K_HDR_HEVC.mov" ] // Dubai night 2 ] // Extra POI let mergePOI = [ "b6-1": "C001_C005_", // China day 4 "b2-1": "C004_C003_", // China day 5 "b5-1": "C003_C003_", // China day 6 "7D4710EB-5BA4-42E6-AA60-68D77F67D9B9": "GL_G010_C006_", // Greenland night 1 "b7-1": "H007_C003", // Hawaii day 1 "b1-1": "H005_C012_", // Hawaii day 2 "b2-2": "H010_C006_", // Hawaii day 3 "b4-1": "H004_C007_", // Hawaii day 4 "b6-2": "H012_C009_", // Hawaii night 1 "b8-1": "H004_C009_", // Hawaii night 2 "6E2FC8AC-832D-46CF-B306-BB2A05030C17": "LW_L001_C006_", // Liwa day 1 LW_L001_C006_0 "b6-3": "L010_C006_", // London day 1 "b5-2": "L007_C007_", // London day 2 "b1-2": "L012_C002_", // London night 1 "b3-1": "L004_C011_", // London night 2 "A284F0BF-E690-4C13-92E2-4672D93E8DE5": "LA_A011_C003_", // Los Angeles night 3 "b7-2": "N008_C009_", // New York day 1 "b1-3": "N006_C003_", // New York day 2 "b3-2": "N003_C006_", // New York day 3 "b2-3": "N013_C004_", // New York night 1 "b4-2": "N008_C003_", // New York night 2 "b8-2": "A008_C007_", // San Francisco day 1 // "b10-3": , // San Francisco day 2 "b9-3": "A006_C003_", // San Francisco day 3 // "b8-3":"", San Francisco day 4 (no extra poi ?) "b3-3": "A012_C014_", // San Francisco day 5 // maybe A013_C004 ? "b4-3": "A013_C005_", // San Francisco day 6 "b6-4": "A004_C012_", // San Francisco night 1 "b7-3": "A007_C017_", // San Francisco night 2 "b5-3": "A015_C014_", // San Francisco night 3 "b1-4": "A015_C018_", // San Francisco night 4 "b2-4": "A018_C014_" // San Francisco night 5 ] // MARK: - Playlist generation func generatePlaylist(isRestricted: Bool, restrictedTo: String) { // Start fresh playlist = [AerialVideo]() playlistIsRestricted = isRestricted playlistRestrictedTo = restrictedTo // Start with a shuffled list, we may have synchronized seed shuffle var shuffled: [AerialVideo] /*if preferences.synchronizedMode { if #available(OSX 10.11, *) { let date = Date() let calendar = NSCalendar.current let minutes = calendar.component(.minute, from: date) debugLog("seed : \(minutes)") var generator = SeededGenerator(seed: UInt64(minutes)) shuffled = loadedManifest.shuffled(using: &generator) } else { // Fallback on earlier versions shuffled = loadedManifest.shuffled() } } else { shuffled = loadedManifest.shuffled() }*/ // Somehow code above doesn't work anymore, force disabling it for everyone for now shuffled = loadedManifest.shuffled() for video in shuffled { // We exclude videos not in rotation /*let inRotation = preferences.videoIsInRotation(videoID: video.id) if !inRotation { continue }*/ // Do we restrict video types by day/night ? if isRestricted { if video.timeOfDay != restrictedTo { continue } } // Are we in full manual mode ?? This replace the old never stream setting if !video.isAvailableOffline && !PrefsCache.enableManagement { continue } // Is the video cached, and if not, are we full ? if !video.isAvailableOffline && Cache.isFull() { continue } // If the video isn't cached, can we network ? if !video.isAvailableOffline && !Cache.canNetwork() { continue } // All good ? Add to playlist playlist.append(video) } // On regenerating a new playlist, we try to avoid repeating the last thing we played! while playlist.count > 1 && lastPluckedFromPlaylist == playlist.first { playlist.shuffle() } } func randomVideo(excluding: [AerialVideo]) -> (AerialVideo?, Bool) { var shouldLoop = false let timeManagement = TimeManagement.sharedInstance let (shouldRestrictByDayNight, restrictTo) = timeManagement.shouldRestrictPlaybackToDayNightVideo() // We may need to regenerate a playlist! if playlist.isEmpty || restrictTo != playlistRestrictedTo || shouldRestrictByDayNight != playlistIsRestricted { generatePlaylist(isRestricted: shouldRestrictByDayNight, restrictedTo: restrictTo) if playlist.count == 1 { debugLog("playlist only has one element, looping!") shouldLoop = true } } // If not pluck one from current playlist and return that if !playlist.isEmpty { lastPluckedFromPlaylist = playlist.removeFirst() return (lastPluckedFromPlaylist, shouldLoop) } else { // If we don't have any playlist, something's got awfully wrong so deal with that! return (findBestEffortVideo(), shouldLoop) } } // Find a backup plan when conditions are not met func findBestEffortVideo() -> AerialVideo? { // So this is embarassing. This can happen if : // - No video checked // - No video for current conditions (only day video checked, and looking for night) // - We don't want to stream but don't have any video // - We may not have the manifests // At this point we're doing a best effort : // - Did we play something previously ? If so play that back (will loop) // - 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) warnLog("Empty playlist, not good !") if lastPluckedFromPlaylist != nil { warnLog("Repeating last played video, after condition change not met !") return lastPluckedFromPlaylist! } else { // Start with a shuffled list let shuffled = loadedManifest.shuffled() if shuffled.isEmpty { // This is super bad, no manifest at all errorLog("No manifest, nothing to play !") return nil } /*for video in shuffled { // We exclude videos not in rotation let inRotation = preferences.videoIsInRotation(videoID: video.id) // If we find anything cached and in rotation, we send that back if video.isAvailableOffline && inRotation { warnLog("returning random cached in rotation video after condition change not met !") return video } }*/ // Nothing ? Sorry but you'll get a non cached file warnLog("returning random video after condition change not met !") return shuffled.first! } } // MARK: - Lifecycle init() { debugLog("Manifest init") // 2.0 remove everything here /* // tmp loadCustomVideos() // We try to load our video manifests in 3 steps : // - reload from local variables (unused for now, maybe with previews+screensaver // in some weird edge case on some systems) // - reprocess the saved files in cache directory (full offline mode) // - download the manifests from servers // // Starting with 1.4.6, we also may now periodically recheck for changed files! debugLog("isManifestCached 10 \(isManifestCached(manifest: .tvOS10))") debugLog("isManifestCached 11 \(isManifestCached(manifest: .tvOS11))") debugLog("isManifestCached 12 \(isManifestCached(manifest: .tvOS12))") debugLog("isManifestCached 13 \(isManifestCached(manifest: .tvOS13))") checkIfShouldRedownloadFiles() if areManifestsFilesLoaded() { debugLog("Files were already loaded in memory") loadManifestsFromLoadedFiles() } else { debugLog("Files were not already loaded in memory") // Manifests are not in our preferences plist, are they cached on disk ? if areManifestsCached() { debugLog("Manifests are cached on disk, loading") loadCachedManifests() } else { // Ok then, we fetch them... debugLog("Fetching missing manifests online") let dateFormatter = DateFormatter() let current = Date() dateFormatter.dateFormat = "yyyy-MM-dd" preferences.lastVideoCheck = dateFormatter.string(from: current) let downloadManager = DownloadManager() var urls: [URL] = [] // For tvOS12-13, json is now in a tar file if !isManifestCached(manifest: .tvOS13) || !isManifestCached(manifest: .tvOS13Strings) { urls.append(URL(string: "https://sylvan.apple.com/Aerials/resources-13.tar")!) } if !isManifestCached(manifest: .tvOS12) { urls.append(URL(string: "https://sylvan.apple.com/Aerials/resources.tar")!) } if !isManifestCached(manifest: .tvOS11) { urls.append(URL(string: "https://sylvan.apple.com/Aerials/2x/entries.json")!) } if !isManifestCached(manifest: .tvOS10) { urls.append(URL(string: "http://a1.phobos.apple.com/us/r1000/000/Features/atv/AutumnResources/videos/entries.json")!) } let completion = BlockOperation { debugLog("Fetching manifests all done") // We can now load from the newly cached files self.loadCachedManifests() } for url in urls { let operation = downloadManager.queueDownload(url, folder: "") completion.addDependency(operation) } OperationQueue.main.addOperation(completion) } }*/ } // MARK: - This will refetch the manifests online func reloadFiles() { moveOldManifests() // Ok then, we fetch them... debugLog("Fetching missing manifests online") let dateFormatter = DateFormatter() let current = Date() dateFormatter.dateFormat = "yyyy-MM-dd" PrefsVideos.lastVideoCheck = dateFormatter.string(from: current) let downloadManager = DownloadManager() var urls: [URL] = [] // For tvOS12, json is now in a tar file if !isManifestCached(manifest: .tvOS13) { urls.append(URL(string: "https://sylvan.apple.com/Aerials/resources-13.tar")!) } if !isManifestCached(manifest: .tvOS12) { urls.append(URL(string: "https://sylvan.apple.com/Aerials/resources.tar")!) } if !isManifestCached(manifest: .tvOS11) { urls.append(URL(string: "https://sylvan.apple.com/Aerials/2x/entries.json")!) } if !isManifestCached(manifest: .tvOS10) { urls.append(URL(string: "http://a1.phobos.apple.com/us/r1000/000/Features/atv/AutumnResources/videos/entries.json")!) } // Setup and start async fetching let completion = BlockOperation { debugLog("Fetching manifests all done") // We can now load from the newly cached files self.loadCachedManifests() } for url in urls { let operation = downloadManager.queueDownload(url, folder: "") completion.addDependency(operation) } OperationQueue.main.addOperation(completion) } func addCallback(_ callback:@escaping ManifestLoadCallback) { if !loadedManifest.isEmpty { callback(loadedManifest) } else { callbacks.append(callback) } } // MARK: - Custom videos func loadCustomVideos() { do { if let cacheDirectory = VideoCache.appSupportDirectory { // customvideos.json var cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) cacheFileUrl.appendPathComponent("customvideos.json") if FileManager.default.fileExists(atPath: cacheFileUrl.path) { debugLog("loading custom file : \(cacheFileUrl)") let ndata = try Data(contentsOf: cacheFileUrl) customVideoFolders = try CustomVideoFolders(data: ndata) } else { debugLog("No customvideos.json at : \(cacheFileUrl.path)") } } } catch { debugLog("Error loading customvideos.json : \(error)") } } func saveCustomVideos() { if let cvf = customVideoFolders, let cacheDirectory = VideoCache.appSupportDirectory { var cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) cacheFileUrl.appendPathComponent("customvideos.json") do { if let encodedData = try? cvf.jsonData() { try encodedData.write(to: cacheFileUrl) debugLog("customvideos.json saved successfully!") loadedManifest.removeAll() // we remove our previously loaded manifest, it's invalid } } catch let error as NSError { errorLog("customvideos.json could not be saved: \(error.localizedDescription)") } } } // This is where we merge with the processed list func mergeCustomVideos() { /* if let cvf = customVideoFolders { for folder in cvf.folders { for asset in folder.assets { let avResolution = getResolution(asset: AVAsset(url: URL(fileURLWithPath: asset.url))) var url1080p = "" var url4K = "" if avResolution.height > 1080 { url4K = URL(fileURLWithPath: asset.url).absoluteString } else { url1080p = URL(fileURLWithPath: asset.url).absoluteString } let urls: [VideoFormat: String] = [.v1080pH264: url1080p, .v1080pHEVC: url1080p, .v1080pHDR: url1080p, .v4KHEVC: url4K, .v4KHDR: url4K, ] let video = AerialVideo(id: asset.id, name: folder.label, secondaryName: asset.accessibilityLabel, type: "video", timeOfDay: asset.time, scene: "landscape", urls: urls, source: nil, poi: [:], communityPoi: asset.pointsOfInterest) processedVideos.append(video) } } } */ } func getResolution(asset: AVAsset) -> CGSize { guard let track = asset.tracks(withMediaType: AVMediaType.video).first else { return CGSize.zero } let size = track.naturalSize.applying(track.preferredTransform) return CGSize(width: abs(size.width), height: abs(size.height)) } // MARK: - Periodically check for new videos func checkIfShouldRedownloadFiles() { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd" dateFormatter.locale = Locale.init(identifier: "en_GB") let dateObj = dateFormatter.date(from: PrefsVideos.lastVideoCheck) debugLog(PrefsVideos.lastVideoCheck) var dayCheck: Int switch PrefsVideos.refreshPeriodicity { case .weekly: dayCheck = 7 case .monthly: dayCheck = 30 case .never: dayCheck = 9999 } let cacheDirectory = VideoCache.appSupportDirectory! var cacheResourcesString = cacheDirectory cacheResourcesString.append(contentsOf: "/backups") let cacheUrl = URL(fileURLWithPath: cacheResourcesString) if #available(OSX 10.11, *) { if !cacheUrl.hasDirectoryPath { // If there's no backup directory, we force the first check moveOldManifests() return } } else { // Fallback on earlier versions } //debugLog("Interval : \(String(describing: dateObj?.timeIntervalSinceNow))") if Int((dateObj?.timeIntervalSinceNow)!) < -dayCheck * 86400 { // We need to redownload then debugLog("Checking for new videos") moveOldManifests() } else { debugLog("No need to check for new videos") } } // We only backup the current tvos and TVStringsBundle (tvOS13) // Previous versions don't change func moveOldManifests() { debugLog("move") let cacheDirectory = VideoCache.appSupportDirectory! var cacheResourcesString = cacheDirectory // Generate the backup path let dateFormatter = DateFormatter() let current = Date() dateFormatter.dateFormat = "yyyy-MM-dd" let today = dateFormatter.string(from: current) cacheResourcesString.append(contentsOf: "/backups/"+today) // The previous files we want to move let previous = URL(fileURLWithPath: cacheDirectory.appending("/tvos13.json")) let previousBnd = URL(fileURLWithPath: cacheDirectory.appending("/TVIdleScreenStrings13.bundle")) if FileManager.default.fileExists(atPath: cacheDirectory.appending("/tvos13.json")) || FileManager.default.fileExists(atPath: cacheDirectory.appending("/TVIdleScreenStrings13.bundle")) { let new = URL(fileURLWithPath: cacheResourcesString.appending("/tvos13.json")) let newBnd = URL(fileURLWithPath: cacheResourcesString.appending("/TVIdleScreenStrings13.bundle")) let cacheUrl = URL(fileURLWithPath: cacheResourcesString) if #available(OSX 10.11, *) { if !cacheUrl.hasDirectoryPath { do { debugLog("creating dir \(cacheResourcesString)") try FileManager.default.createDirectory(atPath: cacheResourcesString, withIntermediateDirectories: true, attributes: nil) debugLog("moving tvos13.json and TVIdleScreenStrings13.bundle") try FileManager.default.moveItem(at: previous, to: new) try FileManager.default.moveItem(at: previousBnd, to: newBnd) } catch { errorLog("\(error.localizedDescription)") } } } } } // MARK: - Manifests // Check if the Manifests have been loaded in this class already func areManifestsFilesLoaded() -> Bool { if manifestTvOS13 != nil && manifestTvOS12 != nil && manifestTvOS11 != nil && manifestTvOS10 != nil { debugLog("Manifests files were loaded in class") return true } else { debugLog("Manifests files were not loaded in class") return false } } // Check if the Manifests are saved in our cache directory func areManifestsCached() -> Bool { return isManifestCached(manifest: .tvOS10) && isManifestCached(manifest: .tvOS11) && isManifestCached(manifest: .tvOS12) && isManifestCached(manifest: .tvOS13) && isManifestCached(manifest: .tvOS13Strings) } // Check if a Manifest is saved in our cache directory func isManifestCached(manifest: Manifests) -> Bool { if let cacheDirectory = VideoCache.appSupportDirectory { let fileManager = FileManager.default var cacheResourcesString = cacheDirectory cacheResourcesString.append(contentsOf: "/" + manifest.rawValue) if !fileManager.fileExists(atPath: cacheResourcesString) { return false } } else { return false } return true } // Load the JSON Data cached on disk func loadCachedManifests() { if let cacheDirectory = VideoCache.appSupportDirectory { // tvOS13 var cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) cacheFileUrl.appendPathComponent("tvos13.json") do { let ndata = try Data(contentsOf: cacheFileUrl) manifestTvOS13 = ndata } catch { errorLog("Can't load tvos13.json from cached directory") } // tvOS12 cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) cacheFileUrl.appendPathComponent("tvos12.json") do { let ndata = try Data(contentsOf: cacheFileUrl) manifestTvOS12 = ndata } catch { errorLog("Can't load tvos12.json from cached directory") } // tvOS11 cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) cacheFileUrl.appendPathComponent("tvos11.json") do { let ndata = try Data(contentsOf: cacheFileUrl) manifestTvOS11 = ndata } catch { errorLog("Can't load tvos11.json from cached directory") } // tvOS10 cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) cacheFileUrl.appendPathComponent("tvos10.json") do { let ndata = try Data(contentsOf: cacheFileUrl) manifestTvOS10 = ndata } catch { errorLog("Can't load tvos10.json from cached directory") } if manifestTvOS10 != nil || manifestTvOS11 != nil || manifestTvOS12 != nil || manifestTvOS13 != nil { loadManifestsFromLoadedFiles() } else { // No internet, no anything, nothing to do errorLog("No video to load, no internet connexion ?") } } } // Load Manifests from the saved preferences func loadManifestsFromLoadedFiles() { // Reset our array processedVideos = [] if manifestTvOS13 != nil { // We start with the more recent one, it has more information (poi, etc) readJSONFromData(manifestTvOS13!, manifest: .tvOS13) } else { warnLog("tvOS13 manifest is absent") } if manifestTvOS12 != nil { // We start with the more recent one, it has more information (poi, etc) readJSONFromData(manifestTvOS12!, manifest: .tvOS12) // We also need to add the missing videos let bundlePath = Bundle(for: ManifestLoader.self).path(forResource: "missingvideos", ofType: "json")! do { let data = try Data(contentsOf: URL(fileURLWithPath: bundlePath), options: .mappedIfSafe) readJSONFromData(data, manifest: .tvOS12) } catch { errorLog("missingvideos.json was not found in the bundle") } } else { warnLog("tvOS12 manifest is absent") } if manifestTvOS11 != nil { // This one has a couple videos not in the tvOS12 JSON. No H264 for these ! readJSONFromData(manifestTvOS11!, manifest: .tvOS11) } else { warnLog("tvOS11 manifest is absent") } if manifestTvOS10 != nil { // The original manifest is in another format readOldJSONFromData(manifestTvOS10!, manifest: .tvOS10) } else { warnLog("tvOS10 manifest is absent") } if customVideoFolders != nil { mergeCustomVideos() } // We sort videos by secondary names, so they can display sorted in our view later processedVideos = processedVideos.sorted { $0.secondaryName < $1.secondaryName } self.loadedManifest = processedVideos debugLog("Total videos processed : \(processedVideos.count) callbacks : \(callbacks.count)") // callbacks for callback in self.callbacks { callback(self.loadedManifest) } self.callbacks.removeAll() } // MARK: - JSON func readJSONFromData(_ data: Data, manifest: Manifests) { /* 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 { errorLog("Encountered unexpected content type for batch, please report !") return } let assets = batch["assets"] as! [NSDictionary] for item in assets { let id = item["id"] as! String let url1080pH264 = item["url-1080-H264"] as? String let url1080pHEVC = item["url-1080-SDR"] as? String let url1080pHDR = item["url-1080-HDR"] as? String let url4KHEVC = item["url-4K-SDR"] as? String let url4KHDR = item["url-4K-HDR"] as? String let name = item["accessibilityLabel"] as! String let urls: [VideoFormat: String] = [.v1080pH264: url1080pH264 ?? "", .v1080pHEVC: url1080pHEVC ?? "", .v1080pHDR: url1080pHDR ?? "", .v4KHEVC: url4KHEVC ?? "", .v4KHDR: url4KHDR ?? "", ] var secondaryName = "" // We may have a secondary name if let mergename = poiStringProvider.getCommunityName(id: 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] { poi = poiStringProvider.fetchExtraPoiForId(id: mergeId) } else { poi = item["pointsOfInterest"] as? [String: String] } let communityPoi = poiStringProvider.getCommunityPoi(id: id) let (isDupe, foundDupe) = findDuplicate(id: id, url1080pH264: url1080pH264 ?? "") if isDupe { //foundDupe!.sources.append(manifest) } else { let video = AerialVideo(id: id, // Must have name: name, // Must have secondaryName: secondaryName, // Optional type: type, // Not sure the point of this one ? timeOfDay: timeOfDay, scene: "landscape", urls: urls, source: nil, poi: poi ?? [:], communityPoi: communityPoi) processedVideos.append(video) } } } catch { errorLog("Error retrieving content listing (new)") 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! [NSDictionary] for batch: NSDictionary in batches { let assets = batch["assets"] as! [NSDictionary] for item in assets { let url = item["url"] as! String let name = item["accessibilityLabel"] as! String let timeOfDay = item["timeOfDay"] as! String let id = item["id"] as! String let type = item["type"] as! String if type != "video" { continue } // We may have a secondary name 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 { //foundDupe!.sources.append(manifest) if foundDupe?.urls[.v1080pH264] == "" { foundDupe?.urls[.v1080pH264] = url } } } else { var url1080pHEVC = "" var url1080pHDR = "" var url4KHEVC = "" var url4KHDR = "" // Check if we have some HEVC urls to merge if let val = mergeInfo[id] { url1080pHEVC = val["url-1080-SDR"]! url1080pHDR = val["url-1080-HDR"]! url4KHEVC = val["url-4K-SDR"]! url4KHDR = val["url-4K-HDR"]! } let urls: [VideoFormat: String] = [.v1080pH264: url, .v1080pHEVC: url1080pHEVC, .v1080pHDR: url1080pHDR, .v4KHEVC: url4KHEVC, .v4KHDR: url4KHDR, ] // Now we can finally add... let video = AerialVideo(id: id, // Must have name: name, // Must have secondaryName: secondaryName, type: type, // Not sure the point of this one ? timeOfDay: timeOfDay, scene: "landscape", urls: urls, source: Source(), poi: poi ?? [:], communityPoi: communityPoi) processedVideos.append(video) } } } } catch { errorLog("Error retrieving content listing (old)") return }*/ } // Look for a previously processed similar video // // tvOS11 and 12 JSON are using the same ID (and tvOS12 JSON always has better data, // so no need for a fancy merge) // // tvOS10 however JSON DOES NOT use the same ID, so we need to dupecheck on the h264 // (only available format there) filename (they actually have different URLs !) func findDuplicate(id: String, url1080pH264: String) -> (Bool, AerialVideo?) { // We blacklist some duplicates if url1080pH264 != "" { if blacklist.contains((URL(string: url1080pH264)?.lastPathComponent)!) { return (true, nil) } } // We also have a Dictionary of duplicates that need source merging for (pid, replace) in dupePairs where id == pid { for vid in processedVideos where vid.id == replace { // Found dupe pair return (true, vid) } } for video in processedVideos { if id == video.id { return (true, video) } else if url1080pH264 != "" && video.urls[.v1080pH264] != "" { if URL(string: url1080pH264)?.lastPathComponent == URL(string: video.urls[.v1080pH264]!)?.lastPathComponent { return (true, video) } } } return (false, nil) } // MARK: - Old video management // Try to estimate how many old (unlinked) files we have func getOldFilesEstimation() -> (String, Int) { // loadedManifests contains the full deduplicated list of videos debugLog("Looking for outdated files") if loadedManifest.isEmpty { warnLog("We have no videos in the manifest") return ("Can't estimate duplicates", 0) } guard let cacheDirectory = VideoCache.appSupportDirectory else { warnLog("No cache directory") return ("Can't estimate duplicates", 0) } var foundOldFiles = 0 let cacheDirectoryUrl = URL(fileURLWithPath: cacheDirectory as String) let fileManager = FileManager.default do { let directoryContent = try fileManager.contentsOfDirectory(at: cacheDirectoryUrl, includingPropertiesForKeys: nil) let videoFileURLs = directoryContent.filter { $0.pathExtension == "mov" } // We check all formats we may have for fileURL in videoFileURLs { var found = false for video in loadedManifest { for format in VideoFormat.allCases where video.urls[format] != "" { if fileURL.lastPathComponent == URL(string: video.urls[format]!)?.lastPathComponent { found = true break } } } if !found { debugLog("\(fileURL.lastPathComponent) NOT FOUND in manifest") foundOldFiles += 1 } } } catch { errorLog("Error while enumerating files \(cacheDirectoryUrl.path): \(error.localizedDescription)") } if foundOldFiles == 0 { debugLog("No old files found") return ("No old files found", 0) } debugLog("\(foundOldFiles) old files found") return ("\(foundOldFiles) old files found", foundOldFiles) } /* func moveOldVideos() { debugLog("move old videos") let cacheDirectory = VideoCache.appSupportDirectory! var cacheResourcesString = cacheDirectory let dateFormatter = DateFormatter() let current = Date() dateFormatter.dateFormat = "yyyy-MM-dd" let today = dateFormatter.string(from: current) cacheResourcesString.append(contentsOf: "/oldvideos/"+today) let cacheUrl = URL(fileURLWithPath: cacheResourcesString) if #available(OSX 10.11, *) { if !cacheUrl.hasDirectoryPath { do { try FileManager.default.createDirectory(atPath: cacheResourcesString, withIntermediateDirectories: true, attributes: nil) debugLog("creating dir \(cacheResourcesString)") } catch { errorLog("\(error.localizedDescription)") } } } if loadedManifest.isEmpty { warnLog("We have no videos in the manifest") return } let cacheDirectoryUrl = URL(fileURLWithPath: cacheDirectory as String) let fileManager = FileManager.default do { let directoryContent = try fileManager.contentsOfDirectory(at: cacheDirectoryUrl, includingPropertiesForKeys: nil) let videoFileURLs = directoryContent.filter { $0.pathExtension == "mov" } // We check the 5 fields for fileURL in videoFileURLs { var found = false for video in loadedManifest { for format in VideoFormat.allCases where video.urls[format] != "" { if fileURL.lastPathComponent == URL(string: video.urls[format]!)?.lastPathComponent { found = true break } } } if !found { do { debugLog("moving \(fileURL.lastPathComponent)") let new = URL(fileURLWithPath: cacheResourcesString.appending("/\(fileURL.lastPathComponent)")) try FileManager.default.moveItem(at: fileURL, to: new) } catch { errorLog("\(error.localizedDescription)") } } } } catch { errorLog("Error while enumerating files \(cacheDirectoryUrl.path): \(error.localizedDescription)") } } func trashOldVideos() { debugLog("trash old videos") let cacheDirectory = VideoCache.appSupportDirectory! var cacheResourcesString = cacheDirectory let dateFormatter = DateFormatter() let current = Date() dateFormatter.dateFormat = "yyyy-MM-dd" let today = dateFormatter.string(from: current) cacheResourcesString.append(contentsOf: "/oldvideos/"+today) let cacheUrl = URL(fileURLWithPath: cacheResourcesString) if #available(OSX 10.11, *) { if !cacheUrl.hasDirectoryPath { do { try FileManager.default.createDirectory(atPath: cacheResourcesString, withIntermediateDirectories: true, attributes: nil) debugLog("creating dir \(cacheResourcesString)") } catch { errorLog("\(error.localizedDescription)") } } } if loadedManifest.isEmpty { warnLog("We have no videos in the manifest") return } let cacheDirectoryUrl = URL(fileURLWithPath: cacheDirectory as String) let fileManager = FileManager.default do { let directoryContent = try fileManager.contentsOfDirectory(at: cacheDirectoryUrl, includingPropertiesForKeys: nil) let videoFileURLs = directoryContent.filter { $0.pathExtension == "mov" } // We check the 5 fields for fileURL in videoFileURLs { var found = false for video in loadedManifest { for format in VideoFormat.allCases where video.urls[format] != "" { if fileURL.lastPathComponent == URL(string: video.urls[format]!)?.lastPathComponent { found = true break } } } if !found { debugLog("trashing \(fileURL.lastPathComponent)") NSWorkspace.shared.recycle([fileURL]) { trashedFiles, error in for file in [fileURL] where trashedFiles[file] == nil { errorLog("\(file.relativePath) could not be moved to trash \(error!.localizedDescription)") } } } } } catch { errorLog("Error while enumerating files \(cacheDirectoryUrl.path): \(error.localizedDescription)") } }*/ } // swiftlint:disable:this file_length ================================================ FILE: Aerial/Source/Models/Music/Music.swift ================================================ // // Music.swift // Aerial // // Created by Guillaume Louel on 29/06/2021. // Copyright © 2021 Guillaume Louel. All rights reserved. // import Foundation import AppKit typealias MusicCallback = (SongInfo) -> Void struct SongInfo { let name: String let artist: String let album: String let artwork: NSImage? } // swiftlint:disable:next type_body_length class Music { static let instance: Music = Music() var callbacks = [MusicCallback]() var wasSetup = false // This is called once at init to set our observer func setup() { if !wasSetup { debugLog("🎧 registering private callback") // Load framework let bundle = CFBundleCreate(kCFAllocatorDefault, NSURL(fileURLWithPath: "/System/Library/PrivateFrameworks/MediaRemote.framework")) // Get a Swift function for MRMediaRemoteRegisterForNowPlayingNotifications guard let MRMediaRemoteRegisterForNowPlayingNotificationsPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteRegisterForNowPlayingNotifications" as CFString) else { return } typealias MRMediaRemoteRegisterForNowPlayingNotificationsFunction = @convention(c) (DispatchQueue) -> Void let MRMediaRemoteRegisterForNowPlayingNotifications = unsafeBitCast(MRMediaRemoteRegisterForNowPlayingNotificationsPointer, to: MRMediaRemoteRegisterForNowPlayingNotificationsFunction.self) // Call the register function MRMediaRemoteRegisterForNowPlayingNotifications(DispatchQueue.main) DispatchQueue.main.async { // Register App state change callback NotificationCenter.default.addObserver(self, selector: #selector(Music.mediaRemoteAppStateChange(_:)), name: NSNotification.Name("kMRMediaRemoteNowPlayingApplicationIsPlayingDidChangeNotification"), object: nil) // Register playback info change callback NotificationCenter.default.addObserver(self, selector: #selector(Music.mediaRemoteCallback(_:)), name: NSNotification.Name("kMRMediaRemoteNowPlayingInfoDidChangeNotification"), object: nil) } wasSetup = true } } // Callback to get paused status from some apps that may not update info pause on change @objc func mediaRemoteAppStateChange(_ aNotification: Notification) { debugLog("🎧 app state change") if let userInfo = aNotification.userInfo { if let rate = userInfo["kMRMediaRemoteNowPlayingApplicationIsPlayingUserInfoKey"] as? Double { if rate == 0 { debugLog("🎧 playback is paused, clearing") // Pause the thing for callback in self.callbacks { callback(SongInfo(name: "", artist: "", album: "", artwork: nil)) } } } } } // General info change callback @objc func mediaRemoteCallback(_ aNotification: Notification?) { var album = "" var name = "" var artist = "" var artwork: NSImage? debugLog("🎧 media remote callback") // Load framework let bundle = CFBundleCreate(kCFAllocatorDefault, NSURL(fileURLWithPath: "/System/Library/PrivateFrameworks/MediaRemote.framework")) // Get a Swift function for MRMediaRemoteGetNowPlayingInfo guard let MRMediaRemoteGetNowPlayingInfoPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteGetNowPlayingInfo" as CFString) else { return } typealias MRMediaRemoteGetNowPlayingInfoFunction = @convention(c) (DispatchQueue, @escaping ([String: Any]) -> Void) -> Void let MRMediaRemoteGetNowPlayingInfo = unsafeBitCast(MRMediaRemoteGetNowPlayingInfoPointer, to: MRMediaRemoteGetNowPlayingInfoFunction.self) // Get song info MRMediaRemoteGetNowPlayingInfo(DispatchQueue.main, { (information) in debugLog("🎧 audio info") if let info = information["kMRMediaRemoteNowPlayingInfoPlaybackRate"] as? Double { if (info != 0.0) { // Player is running if let info = information["kMRMediaRemoteNowPlayingInfoArtist"] as? String { artist = info } if let info = information["kMRMediaRemoteNowPlayingInfoTitle"] as? String { name = info } if let info = information["kMRMediaRemoteNowPlayingInfoAlbum"] as? String { album = info } // try to grab image from the keys if information.keys.contains("kMRMediaRemoteNowPlayingInfoArtworkData") { if let _artwork = NSImage(data: information["kMRMediaRemoteNowPlayingInfoArtworkData"] as! Data) { artwork = _artwork } } debugLog("🎧 " + artist + " - " + name + " (" + album + ")" + ((artwork != nil) ? " with artwork " : " without artwork")) } else { debugLog("🎧 Player is paused") } } // Let everyone who wants to know that we have a new song playing ! for callback in self.callbacks { callback(SongInfo(name: name, artist: artist, album: album, artwork: artwork)) } }) } // MARK: - Callbacks func addCallback(_ callback:@escaping MusicCallback) { debugLog("🎧 Adding music callback") callbacks.append(callback) } } ================================================ FILE: Aerial/Source/Models/PlaybackSpeed.swift ================================================ // // PlaybackSpeed.swift // Aerial // // Created by Guillaume Louel on 08/07/2021. // Copyright © 2021 Guillaume Louel. All rights reserved. // import Foundation struct PlaybackSpeed { static func forVideo(_ id: String) -> Float { if let value = PrefsVideos.playbackSpeed[id] { return value } else { return 1 } } static func update(video: String, value: Float) { // Just in case... if value == 0 { PrefsVideos.playbackSpeed[video] = 0.01 } else { PrefsVideos.playbackSpeed[video] = value } } static func reset(video: String) { PrefsVideos.playbackSpeed[video] = 1 } } ================================================ FILE: Aerial/Source/Models/Prefs/PrefsAdvanced.swift ================================================ // // PrefsAdvanced.swift // Aerial // // Created by Guillaume Louel on 23/04/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation struct PrefsAdvanced { // Display margins @SimpleStorage(key: "muteSound", defaultValue: true) static var muteSound: Bool @SimpleStorage(key: "muteGlobalSound", defaultValue: false) static var muteGlobalSound: Bool @SimpleStorage(key: "autoPlayPreviews", defaultValue: true) static var autoPlayPreviews: Bool @SimpleStorage(key: "firstTimeSetup", defaultValue: false) static var firstTimeSetup: Bool @SimpleStorage(key: "favorOrientation", defaultValue: true) static var favorOrientation: Bool // Invert colors @SimpleStorage(key: "invertColors", defaultValue: false) static var invertColors: Bool // Debug mode @SimpleStorage(key: "debugMode", defaultValue: false) static var debugMode: Bool // OVerride Language @SimpleStorage(key: "ciOverrideLanguage", defaultValue: "") static var ciOverrideLanguage: String @SimpleStorage(key: "newDisplayDict", defaultValue: [String: Bool]()) static var newDisplayDict: [String: Bool] } ================================================ FILE: Aerial/Source/Models/Prefs/PrefsCache.swift ================================================ // // PrefsCache.swift // Aerial // // Created by Guillaume Louel on 03/06/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation enum CachePeriodicity: Int, Codable { case daily, weekly, monthly, never } struct PrefsCache { @SimpleStorage(key: "enableManagement", defaultValue: true) static var enableManagement: Bool // Cache limit (in GiB) @SimpleStorage(key: "cacheLimit", defaultValue: 5) static var cacheLimit: Double // How often should cache gets refreshed @SimpleStorage(key: "intCachePeriodicity", defaultValue: CachePeriodicity.never.rawValue) static var intCachePeriodicity: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var cachePeriodicity: CachePeriodicity { get { return CachePeriodicity(rawValue: intCachePeriodicity)! } set(value) { intCachePeriodicity = value.rawValue } } // Do we restrict network traffic on Wi-Fi @SimpleStorage(key: "restrictOnWiFi", defaultValue: false) static var restrictOnWiFi: Bool // List of allowed networks (using SSID) @SimpleStorage(key: "allowedNetworks", defaultValue: []) static var allowedNetworks: [String] // Should we show the download indicator or not @SimpleStorage(key: "showBackgroundDownloads", defaultValue: false) static var showBackgroundDownloads: Bool // Should we override the cache @SimpleStorage(key: "overrideCache", defaultValue: false) static var overrideCache: Bool // App-scoped bookmark to cache, in NSData form @SimpleStorage(key: "cacheBookmarkData", defaultValue: nil) static var cacheBookmarkData: Data? // The raw path in string form @SimpleStorage(key: "cachePath", defaultValue: nil) static var cachePath: String? // App-scoped bookmark to cache, in NSData form @SimpleStorage(key: "supportBookmarkData", defaultValue: nil) static var supportBookmarkData: Data? // The raw path in string form @SimpleStorage(key: "supportPath", defaultValue: nil) static var supportPath: String? } ================================================ FILE: Aerial/Source/Models/Prefs/PrefsDisplays.swift ================================================ // // PrefsDisplays.swift // Aerial // // Created by Guillaume Louel on 21/01/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation enum DisplayMode: Int { case allDisplays, mainOnly, secondaryOnly, selection } enum AspectMode: Int { case fill, fit } enum ViewingMode: Int { case independent, cloned, spanned, mirrored } struct PrefsDisplays { // Display Mode @SimpleStorage(key: "newDisplayMode", defaultValue: DisplayMode.allDisplays.rawValue) static var intDisplayMode: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var displayMode: DisplayMode { get { return DisplayMode(rawValue: intDisplayMode)! } set(value) { intDisplayMode = value.rawValue } } // Viewing Mode @SimpleStorage(key: "newViewingMode", defaultValue: ViewingMode.independent.rawValue) static var intViewingMode: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var viewingMode: ViewingMode { get { return ViewingMode(rawValue: intViewingMode)! } set(value) { intViewingMode = value.rawValue } } // Aspect Mode @SimpleStorage(key: "aspectMode", defaultValue: AspectMode.fill.rawValue) static var intAspectMode: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var aspectMode: AspectMode { get { return AspectMode(rawValue: intAspectMode)! } set(value) { intAspectMode = value.rawValue } } // Display margins @SimpleStorage(key: "displayMarginsAdvanced", defaultValue: false) static var displayMarginsAdvanced: Bool @SimpleStorage(key: "horizontalMargin", defaultValue: 0) static var horizontalMargin: Double @SimpleStorage(key: "verticalMargin", defaultValue: 0) static var verticalMargin: Double // Advanced margins are stored as a string @SimpleStorage(key: "advancedMargins", defaultValue: "") static var advancedMargins: String @SimpleStorage(key: "dimBrightness", defaultValue: false) static var dimBrightness: Bool @SimpleStorage(key: "dimOnlyAtNight", defaultValue: false) static var dimOnlyAtNight: Bool @SimpleStorage(key: "dimOnlyOnBattery", defaultValue: false) static var dimOnlyOnBattery: Bool @SimpleStorage(key: "overrideDimInMinutes", defaultValue: false) static var overrideDimInMinutes: Bool @SimpleStorage(key: "startDim", defaultValue: 0.5) static var startDim: Double @SimpleStorage(key: "endDim", defaultValue: 0.0) static var endDim: Double @SimpleStorage(key: "dimInMinutes", defaultValue: 30) static var dimInMinutes: Int } struct PrefsDisplaysDesktop { // Display Mode @SimpleStorage(key: "newDisplayMode", defaultValue: DisplayMode.allDisplays.rawValue) static var intDisplayMode: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var displayMode: DisplayMode { get { return DisplayMode(rawValue: intDisplayMode)! } set(value) { intDisplayMode = value.rawValue } } // Viewing Mode @SimpleStorage(key: "newViewingMode", defaultValue: ViewingMode.independent.rawValue) static var intViewingMode: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var viewingMode: ViewingMode { get { return ViewingMode(rawValue: intViewingMode)! } set(value) { intViewingMode = value.rawValue } } // Aspect Mode @SimpleStorage(key: "aspectMode", defaultValue: AspectMode.fill.rawValue) static var intAspectMode: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var aspectMode: AspectMode { get { return AspectMode(rawValue: intAspectMode)! } set(value) { intAspectMode = value.rawValue } } // Display margins @SimpleStorage(key: "displayMarginsAdvanced", defaultValue: false) static var displayMarginsAdvanced: Bool @SimpleStorage(key: "horizontalMargin", defaultValue: 0) static var horizontalMargin: Double @SimpleStorage(key: "verticalMargin", defaultValue: 0) static var verticalMargin: Double } ================================================ FILE: Aerial/Source/Models/Prefs/PrefsInfo.swift ================================================ // // PrefsInfo.swift // Aerial // // Created by Guillaume Louel on 16/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Foundation import ScreenSaver protocol CommonInfo { var isEnabled: Bool { get set } var fontName: String { get set } var fontSize: Double { get set } var corner: InfoCorner { get set } var displays: InfoDisplays { get set } } // Helper Enums for the common infos enum InfoCorner: Int, Codable, CaseIterable { case topLeft, topCenter, topRight, bottomLeft, bottomCenter, bottomRight, screenCenter, random, absTopRight } enum InfoDisplays: Int, Codable { case allDisplays, mainOnly, secondaryOnly } enum InfoTime: Int, Codable { case always, tenSeconds } enum InfoClockFormat: Int, Codable { case tdefault, t24hours, t12hours, custom } enum InfoDate: Int, Codable { case textual, compact, custom } enum InfoIconText: Int, Codable { case text, icon } enum InfoCountdownMode: Int, Codable { case preciseDate, timeOfDay } enum InfoLocationMode: Int, Codable { case useCurrent, manuallySpecify } enum InfoDegree: Int, Codable { case celsius, fahrenheit } enum InfoIconsWeather: Int, Codable { case flat, colorflat, oweather } // The various info types available enum InfoType: String, Codable { case location, message, clock, date, battery, updates, weather, countdown, timer, music } enum InfoMessageType: Int, Codable { case text, shell, textfile } enum InfoRefreshPeriodicity: Int, Codable { case never, tenseconds, thirtyseconds, oneminute, fiveminutes, tenminutes } enum InfoWeatherMode: Int, Codable { case current, forecast6hours, forecast3days, forecast5days } enum InfoWeatherWind: Int, Codable { case kph, mps } // swiftlint:disable:next type_body_length struct PrefsInfo { struct Location: CommonInfo, Codable { var isEnabled: Bool var fontName: String var fontSize: Double var corner: InfoCorner var displays: InfoDisplays var time: InfoTime } struct Message: CommonInfo, Codable { var isEnabled: Bool var fontName: String var fontSize: Double var corner: InfoCorner var displays: InfoDisplays var message: String var shellScript: String var textFile: String var messageType: InfoMessageType var refreshPeriodicity: InfoRefreshPeriodicity } struct Clock: CommonInfo, Codable { var isEnabled: Bool var fontName: String var fontSize: Double var corner: InfoCorner var displays: InfoDisplays var showSeconds: Bool var hideAmPm: Bool var clockFormat: InfoClockFormat } struct IDate: CommonInfo, Codable { var isEnabled: Bool var fontName: String var fontSize: Double var corner: InfoCorner var displays: InfoDisplays var format: InfoDate var withYear: Bool } struct Weather: CommonInfo, Codable { var isEnabled: Bool var fontName: String var fontSize: Double var corner: InfoCorner var displays: InfoDisplays var locationMode: InfoLocationMode var locationString: String var degree: InfoDegree var icons: InfoIconsWeather var mode: InfoWeatherMode var showHumidity: Bool var showWind: Bool var showCity: Bool } struct Battery: CommonInfo, Codable { var isEnabled: Bool var fontName: String var fontSize: Double var corner: InfoCorner var displays: InfoDisplays var mode: InfoIconText var disableWhenFull: Bool } struct Updates: CommonInfo, Codable { var isEnabled: Bool var fontName: String var fontSize: Double var corner: InfoCorner var displays: InfoDisplays var betaReset: Bool // This is useless, just to reload default settings for users of 1.7.2 early betas } struct Countdown: CommonInfo, Codable { var isEnabled: Bool var fontName: String var fontSize: Double var corner: InfoCorner var displays: InfoDisplays var mode: InfoCountdownMode var targetDate: Date var enforceInterval: Bool var triggerDate: Date var showSeconds: Bool } struct Timer: CommonInfo, Codable { var isEnabled: Bool var fontName: String var fontSize: Double var corner: InfoCorner var displays: InfoDisplays var duration: Date var showSeconds: Bool var disableWhenElapsed: Bool var replaceWithMessage: Bool var customMessage: String } struct Music: CommonInfo, Codable { var isEnabled: Bool var fontName: String var fontSize: Double var corner: InfoCorner var displays: InfoDisplays } // Our array of Info layers. User can reorder the array, and we may periodically add new Info types @Storage(key: "layers", defaultValue: [ .message, .clock, .date, .location, .battery, .updates, .weather, .countdown, .timer]) static var layers: [InfoType] // Location information @Storage(key: "LayerLocation", defaultValue: Location(isEnabled: true, fontName: "Helvetica Neue Medium", fontSize: 28, corner: .random, displays: .allDisplays, time: .always)) static var location: Location // Custom string message @Storage(key: "LayerMessage", defaultValue: Message(isEnabled: false, fontName: "Helvetica Neue Medium", fontSize: 20, corner: .topCenter, displays: .allDisplays, message: "Hello there!", shellScript: "", textFile: "", messageType: .text, refreshPeriodicity: .tenminutes)) static var message: Message // Clock @Storage(key: "LayerClock", defaultValue: Clock(isEnabled: true, fontName: "Helvetica Neue Medium", fontSize: 50, corner: .bottomLeft, displays: .allDisplays, showSeconds: true, hideAmPm: false, clockFormat: .tdefault)) static var clock: Clock // Date @Storage(key: "LayerDate", defaultValue: IDate(isEnabled: false, fontName: "Helvetica Neue Thin", fontSize: 25, corner: .bottomLeft, displays: .allDisplays, format: .textual, withYear: false)) static var date: IDate // Battery @Storage(key: "LayerBattery", defaultValue: Battery(isEnabled: false, fontName: "Helvetica Neue Medium", fontSize: 20, corner: .topRight, displays: .allDisplays, mode: .icon, disableWhenFull: false)) static var battery: Battery // Updates @Storage(key: "LayerUpdates", defaultValue: Updates(isEnabled: true, fontName: "Helvetica Neue Medium", fontSize: 20, corner: .topRight, displays: .allDisplays, betaReset: true)) static var updates: Updates // Weather @Storage(key: "LayerWeather", defaultValue: Weather(isEnabled: false, fontName: "Helvetica Neue Medium", fontSize: 40, corner: .topRight, displays: .allDisplays, locationMode: .manuallySpecify, locationString: "", degree: .celsius, icons: isMacOS11() ? .colorflat : .flat, mode: .current, showHumidity: true, showWind: true, showCity: true)) static var weather: Weather // Text fade in/out mode @SimpleStorage(key: "weatherWindMode", defaultValue: InfoWeatherWind.kph.rawValue) static var intWeatherWindMode: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var weatherWindMode: InfoWeatherWind { get { return InfoWeatherWind(rawValue: intWeatherWindMode)! } set(value) { intWeatherWindMode = value.rawValue } } // Music @Storage(key: "LayerMusic", defaultValue: Music(isEnabled: true, fontName: "Helvetica Neue Medium", fontSize: 20, corner: .topRight, displays: .allDisplays)) static var music: Music // Apple Music storefront to be used @SimpleStorage(key: "appleMusicStoreFront", defaultValue: "United States") static var appleMusicStoreFront: String // Apple Music storefront to be used @SimpleStorage(key: "musicProvider", defaultValue: "Apple Music") static var musicProvider: String // Countdown @Storage(key: "LayerCountdown", defaultValue: Countdown(isEnabled: false, fontName: "Helvetica Neue Medium", fontSize: 100, corner: .screenCenter, displays: .allDisplays, mode: .timeOfDay, targetDate: Date(), enforceInterval: false, triggerDate: Date(), showSeconds: true)) static var countdown: Countdown // Timer @Storage(key: "LayerTimer", defaultValue: Timer(isEnabled: false, fontName: "Helvetica Neue Medium", fontSize: 100, corner: .screenCenter, displays: .allDisplays, duration: Date(timeIntervalSince1970: 300), showSeconds: true, disableWhenElapsed: true, replaceWithMessage: false, customMessage: "")) static var timer: Timer @SimpleStorage(key: "customDateFormat", defaultValue: "") static var customDateFormat: String @SimpleStorage(key: "customTimeFormat", defaultValue: "") static var customTimeFormat: String // MARK: - Advanced text settings // Text fade in/out mode @SimpleStorage(key: "fadeModeText", defaultValue: FadeMode.t1.rawValue) static var intFadeModeText: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var fadeModeText: FadeMode { get { return FadeMode(rawValue: intFadeModeText)! } set(value) { intFadeModeText = value.rawValue } } // Fast rendering mode @SimpleStorage(key: "highQualityTextRendering", defaultValue: HardwareDetection.sharedInstance.isAppleSilicon()) static var highQualityTextRendering: Bool // Override margins @SimpleStorage(key: "overrideMargins", defaultValue: false) static var overrideMargins: Bool // Hide overlays under Companion @SimpleStorage(key: "hideUnderCompanion", defaultValue: true) static var hideUnderCompanion: Bool @SimpleStorage(key: "marginX", defaultValue: 50) static var marginX: Int @SimpleStorage(key: "marginY", defaultValue: 50) static var marginY: Int // MARK: - Shadows // Shadow radius @SimpleStorage(key: "shadowRadius", defaultValue: 2) static var shadowRadius: Int @SimpleStorage(key: "shadowOpacity", defaultValue: 1.0) static var shadowOpacity: Float @SimpleStorage(key: "shadowOffsetX", defaultValue: 0.0) static var shadowOffsetX: CGFloat @SimpleStorage(key: "shadowOffsetY", defaultValue: -3.0) static var shadowOffsetY: CGFloat static func isMacOS11() -> Bool { if #available(macOS 11.0, *) { return true } else { return false } } // MARK: - Helpers // Helper to quickly access a given struct (read-only as we return a copy of the struct) static func ofType(_ type: InfoType) -> CommonInfo { switch type { case .location: return location case .message: return message case .clock: return clock case .date: return date case .battery: return battery case .updates: return updates case .weather: return weather case .countdown: return countdown case .timer: return timer case .music: return music } } // Helpers to store the value for the common properties of all info layers static func setEnabled(_ type: InfoType, value: Bool) { switch type { case .location: location.isEnabled = value case .message: message.isEnabled = value case .clock: clock.isEnabled = value case .date: date.isEnabled = value case .battery: battery.isEnabled = value case .updates: updates.isEnabled = value case .weather: weather.isEnabled = value case .countdown: countdown.isEnabled = value case .timer: timer.isEnabled = value case .music: music.isEnabled = value } } static func setFontName(_ type: InfoType, name: String) { switch type { case .location: location.fontName = name case .message: message.fontName = name case .clock: clock.fontName = name case .date: date.fontName = name case .battery: battery.fontName = name case .updates: updates.fontName = name case .weather: weather.fontName = name case .countdown: countdown.fontName = name case .timer: timer.fontName = name case .music: music.fontName = name } } static func setFontSize(_ type: InfoType, size: Double) { switch type { case .location: location.fontSize = size case .message: message.fontSize = size case .clock: clock.fontSize = size case .date: date.fontSize = size case .battery: battery.fontSize = size case .updates: updates.fontSize = size case .weather: weather.fontSize = size case .countdown: countdown.fontSize = size case .timer: timer.fontSize = size case .music: music.fontSize = size } } static func setCorner(_ type: InfoType, corner: InfoCorner) { switch type { case .location: location.corner = corner case .message: message.corner = corner case .clock: clock.corner = corner case .date: date.corner = corner case .battery: battery.corner = corner case .updates: updates.corner = corner case .weather: weather.corner = corner case .countdown: countdown.corner = corner case .timer: timer.corner = corner case .music: music.corner = corner } } static func setDisplayMode(_ type: InfoType, mode: InfoDisplays) { switch type { case .location: location.displays = mode case .message: message.displays = mode case .clock: clock.displays = mode case .date: date.displays = mode case .battery: battery.displays = mode case .updates: updates.displays = mode case .weather: weather.displays = mode case .countdown: countdown.displays = mode case .timer: timer.displays = mode case .music: music.displays = mode } } // This may be a temp workaround, will depend on where it goes // We periodically add new types so we must add them static func updateLayerList() { if !PrefsInfo.layers.contains(.battery) { PrefsInfo.layers.append(.battery) } if !PrefsInfo.layers.contains(.countdown) { PrefsInfo.layers.append(.countdown) } if !PrefsInfo.layers.contains(.timer) { PrefsInfo.layers.append(.timer) } if !PrefsInfo.layers.contains(.date) { PrefsInfo.layers.append(.date) } if !PrefsInfo.layers.contains(.weather) { PrefsInfo.layers.append(.weather) } if !PrefsInfo.layers.contains(.music) { PrefsInfo.layers.append(.music) } // Annnd for backward compatibility with 1.7.2 betas, remove the updates that was once here ;) if PrefsInfo.layers.contains(.updates) { PrefsInfo.layers.remove(at: PrefsInfo.layers.firstIndex(of: .updates)!) } } } // This retrieves/store any type of property in our plist @propertyWrapper struct Storage { private let key: String private let defaultValue: T private let module = "com.JohnCoates.Aerial" private let bundleID = Aerial.helper.getPreferencesDirectory() + "com.glouel.Aerial" init(key: String, defaultValue: T) { self.key = key self.defaultValue = defaultValue } var wrappedValue: T { get { if #available(OSX 10.15, *) { if let userDefaults = UserDefaults(suiteName: bundleID) { // We shoot for a string in the new system if let jsonString = userDefaults.string(forKey: key) { guard let jsonData = jsonString.data(using: .utf8) else { return defaultValue } guard let value = try? JSONDecoder().decode(T.self, from: jsonData) else { return defaultValue } return value } else { // Old time users may have the prefs stored as a data blob though if let data = userDefaults.object(forKey: key) as? Data { let value = try? JSONDecoder().decode(T.self, from: data) return value ?? defaultValue } else { return defaultValue } } } } else { if let userDefaults = ScreenSaverDefaults(forModuleWithName: module) { // We shoot for a string in the new system if let jsonString = userDefaults.string(forKey: key) { guard let jsonData = jsonString.data(using: .utf8) else { return defaultValue } guard let value = try? JSONDecoder().decode(T.self, from: jsonData) else { return defaultValue } return value } else { // Old time users may have the prefs stored as a data blob though if let data = userDefaults.object(forKey: key) as? Data { let value = try? JSONDecoder().decode(T.self, from: data) return value ?? defaultValue } else { return defaultValue } } } } return defaultValue } set { let encoder = JSONEncoder() if #available(OSX 10.13, *) { encoder.outputFormatting = [.prettyPrinted, .sortedKeys] } else { encoder.outputFormatting = [.prettyPrinted] } let jsonData = try? encoder.encode(newValue) let jsonString = String(bytes: jsonData!, encoding: .utf8) if #available(OSX 10.15, *) { if let userDefaults = UserDefaults(suiteName: bundleID) { // Set value to UserDefaults userDefaults.set(jsonString, forKey: key) // We force the sync so the settings are automatically saved // This is needed as the System Preferences instance of Aerial // is a separate instance from the screensaver ones userDefaults.synchronize() } else { errorLog("UserDefaults set failed for \(key)") } } else { if let userDefaults = ScreenSaverDefaults(forModuleWithName: module) { // Set value to UserDefaults userDefaults.set(jsonString, forKey: key) // We force the sync so the settings are automatically saved // This is needed as the System Preferences instance of Aerial // is a separate instance from the screensaver ones userDefaults.synchronize() } else { errorLog("UserDefaults set failed for \(key)") } } } } } // This retrieves store "simple" types that are natively storable on plists @propertyWrapper struct SimpleStorage { private let key: String private let defaultValue: T private let module = "com.JohnCoates.Aerial" private let bundleID = Aerial.helper.getPreferencesDirectory() + "com.glouel.Aerial" init(key: String, defaultValue: T) { self.key = key self.defaultValue = defaultValue } var wrappedValue: T { get { if #available(OSX 10.15, *) { if let userDefaults = UserDefaults(suiteName: bundleID) { return userDefaults.object(forKey: key) as? T ?? defaultValue } } else { if let userDefaults = ScreenSaverDefaults(forModuleWithName: module) { return userDefaults.object(forKey: key) as? T ?? defaultValue } } return defaultValue } set { if #available(OSX 10.15, *) { if let userDefaults = UserDefaults(suiteName: bundleID) { userDefaults.set(newValue, forKey: key) userDefaults.synchronize() } } else { if let userDefaults = ScreenSaverDefaults(forModuleWithName: module) { userDefaults.set(newValue, forKey: key) userDefaults.synchronize() } } } } } ================================================ FILE: Aerial/Source/Models/Prefs/PrefsTime.swift ================================================ // // PrefsTime.swift // Aerial // // Created by Guillaume Louel on 21/01/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation enum TimeMode: Int { case disabled, nightShift, manual, lightDarkMode, coordinates, locationService } enum SolarMode: Int { case strict, official, civil, nautical, astronomical } struct PrefsTime { // Time Mode @SimpleStorage(key: "timeMode", defaultValue: TimeMode.disabled.rawValue) static var intTimeMode: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var timeMode: TimeMode { get { return TimeMode(rawValue: intTimeMode)! } set(value) { intTimeMode = value.rawValue } } // Manually specified sunrise/sunsets @SimpleStorage(key: "manualSunrise", defaultValue: "09:00") static var manualSunrise: String @SimpleStorage(key: "manualSunset", defaultValue: "19:00") static var manualSunset: String // Manually specified latitude/longitude (strings) @SimpleStorage(key: "latitude", defaultValue: "") static var latitude: String @SimpleStorage(key: "longitude", defaultValue: "") static var longitude: String // Solar Mode @SimpleStorage(key: "solarMode", defaultValue: SolarMode.official.rawValue) static var intSolarMode: Int // Prefs sunrise/sunset duration @SimpleStorage(key: "sunEventWindow", defaultValue: 60*180) static var sunEventWindow: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var solarMode: SolarMode { get { return SolarMode(rawValue: intSolarMode)! } set(value) { intSolarMode = value.rawValue } } // Override on macOS dark mode @SimpleStorage(key: "darkModeNightOverride", defaultValue: false) static var darkModeNightOverride: Bool // Last successful coordinates, we're going to save those so we can reuse them if we can't get // Anything from Location Services (laptop plugged on ethernet with wifi off is the scenario) @SimpleStorage(key: "cachedLatitude", defaultValue: 0) static var cachedLatitude: Double @SimpleStorage(key: "cachedLongitude", defaultValue: 0) static var cachedLongitude: Double // Last geocoded string, the result is stored in cachedLatitude/cachedLongitude above @SimpleStorage(key: "geocodedString", defaultValue: "") static var geocodedString: String } ================================================ FILE: Aerial/Source/Models/Prefs/PrefsUpdates.swift ================================================ // // PrefsUpdates.swift // Aerial // // Created by Guillaume Louel on 16/02/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation enum UpdateMode: Int { case notify, install } struct PrefsUpdates { // Update Mode when the screensaver runs (notify or install) @SimpleStorage(key: "checkForUpdates", defaultValue: true) static var checkForUpdates: Bool // Update Mode when the screensaver runs (notify or install) @SimpleStorage(key: "sparkleUpdateMode", defaultValue: getDefaultUpdateMode()) static var intSparkleUpdateMode: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var sparkleUpdateMode: UpdateMode { get { return UpdateMode(rawValue: intSparkleUpdateMode)! } set(value) { intSparkleUpdateMode = value.rawValue } } // On Catalina, we notify by default, on previous OSes we install by default static func getDefaultUpdateMode() -> Int { if #available(OSX 10.15, *) { return UpdateMode.notify.rawValue } else { return UpdateMode.install.rawValue } } } ================================================ FILE: Aerial/Source/Models/Prefs/PrefsVideos.swift ================================================ // // PrefsVideos.swift // Aerial // // Created by Guillaume Louel on 23/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Foundation enum VideoFormat: Int, Codable, CaseIterable { case v1080pH264, v1080pHEVC, v1080pHDR, v4KHEVC, v4KHDR, v4KSDR240 } enum OnBatteryMode: Int, Codable { case keepEnabled, alwaysDisabled, disableOnLow } enum FadeMode: Int { // swiftlint:disable:next identifier_name case disabled, t0_5, t1, t2 } enum ShouldPlay: Int { case everything, favorites, location, time, scene, source, collection } enum NewShouldPlay: Int { case location, favorites, time, scene, source } enum RefreshPeriodicity: Int { case weekly, monthly, never } struct PrefsVideos { // Main playback mode after v2.5 @SimpleStorage(key: "intNewShouldPlay", defaultValue: NewShouldPlay.location.rawValue) static var intNewShouldPlay: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var newShouldPlay: NewShouldPlay { get { return NewShouldPlay(rawValue: intNewShouldPlay)! } set(value) { intNewShouldPlay = value.rawValue } } // Main playback mode (deprecated in 2.5) @SimpleStorage(key: "intShouldPlay", defaultValue: ShouldPlay.everything.rawValue) static var intShouldPlay: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var shouldPlay: ShouldPlay { get { return ShouldPlay(rawValue: intShouldPlay)! } set(value) { intShouldPlay = value.rawValue } } // Starting with v2.5 @SimpleStorage(key: "newShouldPlayString", defaultValue: []) static var newShouldPlayString: [String] // Deprecated in v2.5 @SimpleStorage(key: "shouldPlayString", defaultValue: "") static var shouldPlayString: String // What do we do on battery ? @SimpleStorage(key: "intOnBatteryMode", defaultValue: OnBatteryMode.keepEnabled.rawValue) static var intOnBatteryMode: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var onBatteryMode: OnBatteryMode { get { return OnBatteryMode(rawValue: intOnBatteryMode)! } set(value) { intOnBatteryMode = value.rawValue } } // Internal storage for video format @SimpleStorage(key: "intVideoFormat", defaultValue: VideoFormat.v1080pH264.rawValue) static var intVideoFormat: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var videoFormat: VideoFormat { get { return VideoFormat(rawValue: intVideoFormat)! } set(value) { intVideoFormat = value.rawValue } } // Video fade in/out mode @SimpleStorage(key: "fadeMode", defaultValue: FadeMode.t1.rawValue) static var intFadeMode: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var fadeMode: FadeMode { get { return FadeMode(rawValue: intFadeMode)! } set(value) { intFadeMode = value.rawValue } } // How often should we look for new videos ? @SimpleStorage(key: "intRefreshPeriodicity", defaultValue: PrefsCache.enableManagement ? RefreshPeriodicity.monthly.rawValue : RefreshPeriodicity.never.rawValue) static var intRefreshPeriodicity: Int // We wrap in a separate value, as we can't store an enum as a Codable in // macOS < 10.15 static var refreshPeriodicity: RefreshPeriodicity { get { return RefreshPeriodicity(rawValue: intRefreshPeriodicity)! } set(value) { intRefreshPeriodicity = value.rawValue } } // Allow video skips with right arrow key (on supporting OSes) @SimpleStorage(key: "allowSkips", defaultValue: true) static var allowSkips: Bool @SimpleStorage(key: "sourcesEnabled", defaultValue: ["macOS 26": true, "tvOS 16": false, "tvOS 13": false]) static var enabledSources: [String: Bool] // Favorites (we use the video ID) @SimpleStorage(key: "favorites", defaultValue: []) static var favorites: [String] // Hidden list (same) @SimpleStorage(key: "hidden", defaultValue: []) static var hidden: [String] @SimpleStorage(key: "vibrance", defaultValue: [:]) static var vibrance: [String: Double] @SimpleStorage(key: "durationCache", defaultValue: [:]) static var durationCache: [String: Double] @SimpleStorage(key: "playbackSpeed", defaultValue: [:]) static var playbackSpeed: [String: Float] @SimpleStorage(key: "globalVibrance", defaultValue: 0) static var globalVibrance: Double @SimpleStorage(key: "allowPerVideoVibrance", defaultValue: false) static var allowPerVideoVibrance: Bool static private func defaultLastVideoCheck() -> String { let dateFormatter = DateFormatter() let current = Date(timeIntervalSinceReferenceDate: -123456789.0) dateFormatter.dateFormat = "yyyy-MM-dd" return dateFormatter.string(from: current) } @SimpleStorage(key: "lastVideoCheck", defaultValue: defaultLastVideoCheck()) static var lastVideoCheck: String static private func intervalSinceLastVideoCheck() -> TimeInterval { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd" dateFormatter.locale = Locale.init(identifier: "en_GB") let dateObj = dateFormatter.date(from: PrefsVideos.lastVideoCheck)! // debugLog("Last manifest check : \(dateObj)") return dateObj.timeIntervalSinceNow } static func saveLastVideoCheck() { let dateFormatter = DateFormatter() let current = Date() dateFormatter.dateFormat = "yyyy-MM-dd" PrefsVideos.lastVideoCheck = dateFormatter.string(from: current) } static func shouldCheckForNewVideos() -> Bool { if refreshPeriodicity == .never { return false } var dayCheck = 7 if refreshPeriodicity == .monthly { dayCheck = 30 } // debugLog("Interval : \(intervalSinceLastVideoCheck())") if Int(intervalSinceLastVideoCheck()) < -dayCheck * 86400 { // debugLog("Checking for new videos") return true } else { // debugLog("No need to check for new videos") return false } } } ================================================ FILE: Aerial/Source/Models/SeededGenerator.swift ================================================ // // SeededGenerator.swift // Aerial // // Created by Guillaume Louel on 21/05/2019. // Copyright © 2019 John Coates. All rights reserved. // import Foundation import GameplayKit @available(OSX 10.11, *) class SeededGenerator: RandomNumberGenerator { let seed: UInt64 private let generator: GKMersenneTwisterRandomSource convenience init() { self.init(seed: 0) } init(seed: UInt64) { self.seed = seed generator = GKMersenneTwisterRandomSource(seed: seed) } func next() -> UInt64 { return UInt64(abs(generator.nextInt())) } func next(upperBound: T) -> T where T: FixedWidthInteger, T: UnsignedInteger { return T(abs(generator.nextInt(upperBound: Int(upperBound)))) } func next() -> T where T: FixedWidthInteger, T: UnsignedInteger { return T(abs(generator.nextInt())) } } ================================================ FILE: Aerial/Source/Models/Sources/Sidebar.swift ================================================ // // Sidebar.swift // Aerial // // Created by Guillaume Louel on 15/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class Sidebar { var modern: [Any] = [] struct Header { let name: String let entries: [MenuEntry] } struct MenuEntry { let name: String let path: String } static let instance: Sidebar = Sidebar() init() { makeModern() } // The new modern menu in 3.0 func makeModern() { modern = [ Header(name: "Aerials", entries: [ MenuEntry(name: "Now Playing", path: "modern:nowplaying"), MenuEntry(name: "Browse Videos", path: "videos:all"), MenuEntry(name: "More Videos", path: "settings:sources") ]), Header(name: "Settings", entries: [ MenuEntry(name: "Time", path: "settings:time"), MenuEntry(name: "Displays", path: "settings:displays"), MenuEntry(name: "Brightness", path: "settings:brightness"), MenuEntry(name: "Cache", path: "settings:cache"), MenuEntry(name: "Overlays", path: "settings:overlays"), MenuEntry(name: "Filters", path: "settings:filters"), // MenuEntry(name: "Auto Updates", path: "settings:updates"), MenuEntry(name: "Advanced", path: "settings:advanced") ]), Header(name: "Information", entries: [ MenuEntry(name: "About", path: "infos:about"), MenuEntry(name: "Credits", path: "infos:credits"), MenuEntry(name: "Help", path: "infos:help") ]) ] } // Helper to get the various icons for the sidebar // swiftlint:disable:next cyclomatic_complexity static func iconFor(_ path: String, name: String) -> NSImage? { if path.starts(with: "videos:location") { return Aerial.helper.getAccentedSymbol("mappin.and.ellipse") } else if path.starts(with: "videos:cache") && name == VideoList.instance.cacheDownloaded { return Aerial.helper.getAccentedSymbol("internaldrive") } else if path.starts(with: "videos:cache") && name == VideoList.instance.cacheOnline { return Aerial.helper.getAccentedSymbol("cloud") } else if path.starts(with: "videos:time") && name == "Day" { return Aerial.helper.getAccentedSymbol("sun.max") } else if path.starts(with: "videos:time") && name == "Night" { return Aerial.helper.getAccentedSymbol("moon.stars") } else if path.starts(with: "videos:time") && name == "Sunrise" { return Aerial.helper.getAccentedSymbol("sunrise") } else if path.starts(with: "videos:time") && name == "Sunset" { return Aerial.helper.getAccentedSymbol("sunset") } else if path.starts(with: "videos:scene") && name == "Nature" { return Aerial.helper.getAccentedSymbol("leaf") } else if path.starts(with: "videos:scene") && name == "City" { return Aerial.helper.getAccentedSymbol("tram.fill") } else if path.starts(with: "videos:scene") && name == "Space" { return Aerial.helper.getAccentedSymbol("sparkles") } else if path.starts(with: "videos:scene") && name == "Sea" { return Aerial.helper.getAccentedSymbol("helm") } else if path.starts(with: "videos:scene") && name == "Beach" { return Aerial.helper.getAccentedSymbol("helm") } else if path.starts(with: "videos:scene") && name == "Countryside" { return Aerial.helper.getAccentedSymbol("helm") } else if path.starts(with: "videos:rotation") { return Aerial.helper.getAccentedSymbol("dial.min") } else if path.starts(with: "videos:favorite") { return Aerial.helper.getSymbol("star") } else if path.starts(with: "videos:hidden") { return Aerial.helper.getAccentedSymbol("eye.slash") } else if path.starts(with: "videos:source") { return Aerial.helper.getAccentedSymbol("antenna.radiowaves.left.and.right") } else if path.starts(with: "videos:") { return Aerial.helper.getAccentedSymbol("film") } else if path.starts(with: "settings:sources") { return Aerial.helper.getAccentedSymbol("antenna.radiowaves.left.and.right") } else if path.starts(with: "settings:time") { return Aerial.helper.getAccentedSymbol("clock") } else if path.starts(with: "settings:displays") { return Aerial.helper.getAccentedSymbol("display.2") } else if path.starts(with: "settings:brightness") { return Aerial.helper.getAccentedSymbol("sun.min") } else if path.starts(with: "settings:cache") { return Aerial.helper.getAccentedSymbol("internaldrive") } else if path.starts(with: "settings:overlays") { return Aerial.helper.getAccentedSymbol("text.bubble") } else if path.starts(with: "settings:filters") { return Aerial.helper.getAccentedSymbol("slider.horizontal.3") } else if path.starts(with: "settings:updates") { return Aerial.helper.getAccentedSymbol("arrow.down.circle") } else if path.starts(with: "settings:advanced") { return Aerial.helper.getAccentedSymbol("wrench.and.screwdriver") } else if path.starts(with: "infos:help") { return Aerial.helper.getAccentedSymbol("bubble.left.and.bubble.right") } else if path.starts(with: "infos:credits") { return Aerial.helper.getAccentedSymbol("person.3") } else if path.starts(with: "infos:about") { return Aerial.helper.getAccentedSymbol("info.circle") } else if path.starts(with: "modern:nowplaying") { return Aerial.helper.getAccentedSymbol("play.circle") } else { // For the WIP return Aerial.helper.getSymbol("wrench") } } } ================================================ FILE: Aerial/Source/Models/Sources/Source.swift ================================================ // // Source.swift // Aerial // // Created by Guillaume Louel on 01/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation // 10 has a different format // 11 is similar to 12+, but does not include pointsOfInterests // 12/13 share a same format, and we use that format for local videos too enum SourceType: Int, Codable { case local, tvOS10, tvOS11, tvOS12, macOS } enum SourceScene: String, Codable { case nature = "Nature", city = "City", space = "Space", sea = "Sea", beach = "Beach", countryside = "Countryside" } // swiftlint:disable:next type_body_length struct Source: Codable { var name: String var description: String var manifestUrl: String var type: SourceType var scenes: [SourceScene] var isCachable: Bool var license: String var more: String func isEnabled() -> Bool { if PrefsVideos.enabledSources.keys.contains(name) { return PrefsVideos.enabledSources[name]! } // Unknown sources are enabled by default return true } func diskUsage() -> Double { let path = Cache.supportPath.appending("/" + name) return Cache.getDirectorySize(directory: path) } func wipeFromDisk() { let path = Cache.supportPath.appending("/" + name) if FileManager.default.fileExists(atPath: path) { try? FileManager.default.removeItem(atPath: path) } } func setEnabled(_ enabled: Bool) { PrefsVideos.enabledSources[name] = enabled VideoList.instance.reloadSources() } // Is the source already cached or not ? func isCached() -> Bool { let fileManager = FileManager.default return fileManager.fileExists(atPath: Cache.supportPath.appending("/" + name + "/entries.json")) } func lastUpdated() -> String { if isCached() { var date: Date? if !isCachable && type == .local { date = (try? FileManager.default.attributesOfItem(atPath: Cache.supportPath.appending("/" + name + "/entries.json")))?[.modificationDate] as? Date } else { date = (try? FileManager.default.attributesOfItem(atPath: Cache.supportPath.appending("/" + name + "/entries.json")))?[.creationDate] as? Date } if date != nil { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd" return dateFormatter.string(from: date!) } else { return "" } } return "" } // Read local entries.json and return the video assets as an array // This is used to update in place the entries.json at startup when updating local sources func getUnprocessedAssets() -> [VideoAsset] { if isCached() { do { let cacheFileUrl = URL(fileURLWithPath: Cache.supportPath.appending("/" + name + "/entries.json")) let jsondata = try Data(contentsOf: cacheFileUrl) if let videoManifest = try? newJSONDecoder().decode(VideoManifest.self, from: jsondata) { return videoManifest.assets } errorLog("### Could not parse manifest data") return [] } catch { errorLog("\(name) could not be opened") return [] } } else { debugLog("\(name) is not cached") return [] } } func getUnprocessedVideos() -> [AerialVideo] { if isCached() { do { let cacheFileUrl = URL(fileURLWithPath: Cache.supportPath.appending("/" + name + "/entries.json")) let jsondata = try Data(contentsOf: cacheFileUrl) return readVideoManifest(jsondata) } catch { errorLog("\(name) could not be opened") return [] } } else { debugLog("\(name) is not cached") return [] } } func getVideos() -> [AerialVideo] { if isCached() { do { let cacheFileUrl = URL(fileURLWithPath: Cache.supportPath.appending("/" + name + "/entries.json")) let jsondata = try Data(contentsOf: cacheFileUrl) if name == "tvOS 10" { return parseOldVideoManifest(jsondata) } else if name.starts(with: "tvOS 13") { return parseVideoManifest(jsondata) + getMissingVideos() // Oh, Victoria Harbour 2... } else if name.starts(with: "macOS") { return parseMacManifest(jsondata) } else { return parseVideoManifest(jsondata) } } catch { errorLog("\(name) could not be opened") return [] } } else { debugLog("\(name) is not cached") return [] } } func localizePath(_ path: String?) -> String { if let tpath = path { if manifestUrl.starts(with: "file://") { return manifestUrl + tpath } return tpath } else { return "" } } // The things we do for one single missing video (for now) ;) func getMissingVideos() -> [AerialVideo] { // We also need to add the missing videos let bundlePath = Bundle(for: PanelWindowController.self).path(forResource: "missingvideos", ofType: "json")! do { let data = try Data(contentsOf: URL(fileURLWithPath: bundlePath), options: .mappedIfSafe) return parseVideoManifest(data) } catch { errorLog("missingvideos.json was not found in the bundle") } return [] } // MARK: - JSON processing func readOldJSONFromData(_ data: Data) -> [AerialVideo] { var processedVideos: [AerialVideo] = [] do { let poiStringProvider = PoiStringProvider.sharedInstance let options = JSONSerialization.ReadingOptions.allowFragments let batches = try JSONSerialization.jsonObject(with: data, options: options) as! [NSDictionary] for batch: NSDictionary in batches { let assets = batch["assets"] as! [NSDictionary] // rawCount = assets.count for item in assets { let url = item["url"] as! String let name = item["accessibilityLabel"] as! String let timeOfDay = item["timeOfDay"] as! String let id = item["id"] as! String let type = item["type"] as! String if type != "video" { continue } // We may have a secondary name var secondaryName = "" if let mergename = poiStringProvider.getCommunityName(id: id) { secondaryName = mergename } // We may have POIs to merge /*var poi: [String: String]? if let mergeId = SourceInfo.mergePOI[id] { let poiStringProvider = PoiStringProvider.sharedInstance poi = poiStringProvider.fetchExtraPoiForId(id: mergeId) }*/ let communityPoi = poiStringProvider.getCommunityPoi(id: id) // We may have dupes... let (isDupe, foundDupe) = SourceInfo.findDuplicate(id: id, url1080pH264: url) if isDupe { if foundDupe != nil { // foundDupe!.sources.append(manifest) if foundDupe?.urls[.v1080pH264] == "" { foundDupe?.urls[.v1080pH264] = url } } } else { var url1080pHEVC = "" var url1080pHDR = "" var url4KHEVC = "" var url4KHDR = "" // Check if we have some HEVC urls to merge if let val = SourceInfo.mergeInfo[id] { url1080pHEVC = val["url-1080-SDR"]! url1080pHDR = val["url-1080-HDR"]! url4KHEVC = val["url-4K-SDR"]! url4KHDR = val["url-4K-HDR"]! } let urls: [VideoFormat: String] = [.v1080pH264: url, .v1080pHEVC: url1080pHEVC, .v1080pHDR: url1080pHDR, .v4KHEVC: url4KHEVC, .v4KHDR: url4KHDR ] // Now we can finally add... let video = AerialVideo(id: id, // Must have name: name, // Must have secondaryName: secondaryName, type: type, // Not sure the point of this one ? timeOfDay: timeOfDay, scene: "landscape", urls: urls, source: self, poi: [:], communityPoi: communityPoi) processedVideos.append(video) } } } return processedVideos } catch { errorLog("Error retrieving content listing (old)") return [] } } func getSubcategoryFor(_ asset: MacAsset, manifest: MacManifest) -> String { for category in manifest.categories { if category.subcategories != nil { for subcategory in category.subcategories! { if subcategory.id == asset.subcategories.first { return PoiStringProvider.sharedInstance.getLocalizedNameKey(key:subcategory.localizedNameKey) } } } } return "Not found" } func getSecondaryNameFor(_ asset: VideoAsset) -> String { let poiStringProvider = PoiStringProvider.sharedInstance if let mergename = poiStringProvider.getCommunityName(id: asset.id) { return mergename } else { return asset.title ?? "Unknown" } } func getSecondaryNameFor(_ asset: MacAsset) -> String { let poiStringProvider = PoiStringProvider.sharedInstance return poiStringProvider.getLocalizedNameKey(key: asset.localizedNameKey) } func getSceneFor(_ asset: VideoAsset) -> String { if let updatedScene = SourceInfo.getSceneForVideo(id: asset.id) { return updatedScene.rawValue.lowercased() } else { return asset.scene ?? "landscape" } } func getSceneFor(_ asset: MacAsset) -> String { if let updatedScene = SourceInfo.getSceneForVideo(id: asset.id) { return updatedScene.rawValue.lowercased() } else { return "landscape" } } // Generate URLs func urlsFor(_ asset: VideoAsset) -> [VideoFormat: String] { return [.v1080pH264: localizePath(asset.url1080H264), .v1080pHEVC: localizePath(asset.url1080SDR), .v1080pHDR: localizePath(asset.url1080HDR), .v4KHEVC: localizePath(asset.url4KSDR), .v4KHDR: localizePath(asset.url4KHDR), .v4KSDR240: localizePath(asset.url4KSDR240FPS) ] } // Mac manifest only has 240 fps func urlsFor(_ asset: MacAsset) -> [VideoFormat: String] { return [.v1080pH264: "", .v1080pHEVC: "", .v1080pHDR: "", .v4KHEVC: "", .v4KHDR: "", .v4KSDR240: localizePath(asset.url4KSDR240FPS) ] } func oldUrlsFor(_ asset: VideoAsset) -> [VideoFormat: String] { var url1080pHEVC = "" var url1080pHDR = "" var url4KHEVC = "" var url4KHDR = "" // Check if we have some HEVC urls to merge if let val = SourceInfo.mergeInfo[asset.id] { url1080pHEVC = val["url-1080-SDR"]! url1080pHDR = val["url-1080-HDR"]! url4KHEVC = val["url-4K-SDR"]! url4KHDR = val["url-4K-HDR"]! } return [.v1080pH264: asset.url ?? "", .v1080pHEVC: url1080pHEVC, .v1080pHDR: url1080pHDR, .v4KHEVC: url4KHEVC, .v4KHDR: url4KHDR ] } func parseOldVideoManifest(_ data: Data) -> [AerialVideo] { do { let oldVideoManifest = try newJSONDecoder().decode(OldVideoManifest.self, from: data) var processedVideos: [AerialVideo] = [] for group in oldVideoManifest { for asset in group.assets { let (isDupe, foundDupe) = SourceInfo.findDuplicate(id: asset.id, url1080pH264: asset.url ?? "") if isDupe { if let dupe = foundDupe { if dupe.urls[.v1080pH264] == "" { dupe.urls[.v1080pH264] = asset.url } } } else { var poi: [String: String]? if let mergeId = SourceInfo.mergePOI[asset.id] { let poiStringProvider = PoiStringProvider.sharedInstance poi = poiStringProvider.fetchExtraPoiForId(id: mergeId) } let video = AerialVideo(id: asset.id, name: asset.accessibilityLabel, secondaryName: getSecondaryNameFor(asset), type: "video", timeOfDay: asset.timeOfDay ?? "day", scene: getSceneFor(asset), urls: oldUrlsFor(asset), source: self, poi: poi ?? [:], communityPoi: PoiStringProvider.sharedInstance.getCommunityPoi(id: asset.id)) processedVideos.append(video) } } } return processedVideos } catch let error { debugLog(error.localizedDescription) errorLog("### Could not parse manifest data") return [] } } func readVideoManifest(_ data: Data) -> [AerialVideo] { if let videoManifest = try? newJSONDecoder().decode(VideoManifest.self, from: data) { var processedVideos: [AerialVideo] = [] for asset in videoManifest.assets { let video = AerialVideo(id: asset.id, name: asset.accessibilityLabel, secondaryName: getSecondaryNameFor(asset), type: "video", timeOfDay: asset.timeOfDay ?? "day", scene: getSceneFor(asset), urls: urlsFor(asset), source: self, poi: asset.pointsOfInterest ?? [:], communityPoi: PoiStringProvider.sharedInstance.getCommunityPoi(id: asset.id)) processedVideos.append(video) } return processedVideos } errorLog("### Could not parse manifest data") return [] } func parseVideoManifest(_ data: Data) -> [AerialVideo] { if let videoManifest = try? newJSONDecoder().decode(VideoManifest.self, from: data) { var processedVideos: [AerialVideo] = [] for asset in videoManifest.assets { let (isDupe, foundVideo) = SourceInfo.findDuplicate(id: asset.id, url1080pH264: asset.url1080H264 ?? "") if !isDupe { let video = AerialVideo(id: asset.id, name: asset.accessibilityLabel, secondaryName: getSecondaryNameFor(asset), type: "video", timeOfDay: asset.timeOfDay ?? "day", scene: getSceneFor(asset), urls: urlsFor(asset), source: self, poi: asset.pointsOfInterest ?? [:], communityPoi: PoiStringProvider.sharedInstance.getCommunityPoi(id: asset.id)) processedVideos.append(video) } else { // Merge urls with macOS manifest let assetURLs = urlsFor(asset) if foundVideo?.urls[.v4KHDR] == "" { foundVideo?.urls[.v4KHDR] = assetURLs[.v4KHDR] } if foundVideo?.urls[.v4KHEVC] == "" { foundVideo?.urls[.v4KHEVC] = assetURLs[.v4KHEVC] } if foundVideo?.urls[.v1080pHDR] == "" { foundVideo?.urls[.v1080pHDR] = assetURLs[.v1080pHDR] } if foundVideo?.urls[.v1080pHEVC] == "" { foundVideo?.urls[.v1080pHEVC] = assetURLs[.v1080pHEVC] } if foundVideo?.urls[.v1080pH264] == "" { foundVideo?.urls[.v1080pH264] = assetURLs[.v1080pH264] } } } return processedVideos } errorLog("### Could not parse manifest data") return [] } func parseMacManifest(_ data: Data) -> [AerialVideo] { if let videoManifest = try? newJSONDecoder().decode(MacManifest.self, from: data) { var processedVideos: [AerialVideo] = [] for asset in videoManifest.assets { let (isDupe, _) = SourceInfo.findDuplicate(id: asset.id, url1080pH264: "") if !isDupe { let video = AerialVideo(id: asset.id, name: getSubcategoryFor(asset, manifest: videoManifest), secondaryName: getSecondaryNameFor(asset), type: "video", timeOfDay: "day", scene: getSceneFor(asset), urls: urlsFor(asset), source: self, poi: asset.pointsOfInterest, // ?? [:], communityPoi: PoiStringProvider.sharedInstance.getCommunityPoi(id: asset.id)) processedVideos.append(video) } } return processedVideos } errorLog("### Could not parse manifest data") return [] } } // MARK: - VideoManifest /// The newer format used by all our other JSONs struct VideoManifest: Codable { let assets: [VideoAsset] let initialAssetCount, version: Int? } // MARK: - OldVideoManifestElement /// This is tvOS 10's manifest format struct OldVideoManifestElement: Codable { let id: String let assets: [VideoAsset] } typealias OldVideoManifest = [OldVideoManifestElement] // MARK: - VideoAsset /// Common Asset structure for all our JSONs /// /// I've added multiple extra fields that aren't in Apple's JSONs, including: /// - title: as in Los Angeles (accesibilityLabel) / Santa Monica Beach (title) /// - timeOfDay: only on tvOS 10, resurected for custom sources, can also be sunset or sunrise /// - scene: landscape, city, space, sea struct VideoAsset: Codable { let accessibilityLabel, id: String let title: String? let timeOfDay: String? let scene: String? let pointsOfInterest: [String: String]? let url4KHDR, url4KSDR, url1080H264, url1080HDR, url4KSDR120FPS, url4KSDR240FPS: String? let url1080SDR, url: String? let type: String? enum CodingKeys: String, CodingKey { case accessibilityLabel, id, pointsOfInterest case title, timeOfDay, scene case url4KHDR = "url-4K-HDR" case url4KSDR = "url-4K-SDR" case url1080H264 = "url-1080-H264" case url1080HDR = "url-1080-HDR" case url1080SDR = "url-1080-SDR" case url4KSDR240FPS = "url-4K-SDR-240FPS" case url4KSDR120FPS = "url-4K-SDR-120FPS" case url case type } } // MARK: - MACManifest struct MacManifest: Codable { let localizationVersion: LocalizationVersion let categories: [SubcategoryElement] let initialAssetCount: Int let assets: [MacAsset] let version: Int } // MARK: - Asset struct MacAsset: Codable { let shotID: String let previewImage: String let previewImage900x580: String? let localizedNameKey, accessibilityLabel: String let preferredOrder: Int let categories: [String] let id: String let subcategories: [String] let pointsOfInterest: [String: String] let url4KSDR240FPS: String let includeInShuffle, showInTopLevel: Bool let group: LocalizationVersion? enum CodingKeys: String, CodingKey { case shotID, previewImage, localizedNameKey, accessibilityLabel, preferredOrder, categories, id, subcategories, pointsOfInterest case previewImage900x580 = "previewImage-900x580" case url4KSDR240FPS = "url-4K-SDR-240FPS" case includeInShuffle, showInTopLevel, group } } enum LocalizationVersion: String, Codable { case the19J1 = "19J-1" case the19K1 = "19K-1" case the21J1 = "21J-1" case the22L1 = "22L-1" } // MARK: - SubcategoryElement struct SubcategoryElement: Codable { let subcategories: [SubcategoryElement]? let localizedDescriptionKey, representativeAssetID: String let previewImage: String let id: String let preferredOrder: Int let localizedNameKey: String } ================================================ FILE: Aerial/Source/Models/Sources/SourceInfo.swift ================================================ // // SourceInfo.swift // Aerial // // Created by Guillaume Louel on 08/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation // swiftlint:disable:next type_body_length struct SourceInfo { // Those videos will be ignored static let blacklist = ["b10-1.mov", // Dupe of b1-1 (Hawaii, day) "b10-2.mov", // Dupe of b2-3 (New York, night) "b10-4.mov", // Dupe of b2-4 (San Francisco, night) "b9-1.mov", // Dupe of b2-2 (Hawaii, day) "b9-2.mov", // Dupe of b3-1 (London, night) "comp_LA_A005_C009_v05_t9_6M.mov", // Low quality version of Los Angeles day 687B36CB-BA5D-4434-BA99-2F2B8B6EC163 "comp_LA_A009_C009_t9_6M_tag0.mov" ] // Low quality version of Los Angeles night 89B1643B-06DD-4DEC-B1B0-774493B0F7B7 // This is used for videos where URLs should be merged with different ID // This is used to dedupe old versions of videos // old : new static let dupePairs = [ "A2BE2E4A-AD4B-428A-9C41-BDAE1E78E816": "12318CCB-3F78-43B7-A854-EFDCCE5312CD", // California to Vegas (v7 -> v8) "6A74D52E-2447-4B84-AE45-0DEF2836C3CC": "7825C73A-658F-48EE-B14C-EC56673094AC", // China "7825C73A-658F-48EE-B14C-EC56673094AC": "6324F6EB-E0F1-468F-AC2E-A983EBDDD53B", // China again "6C3D54AE-0871-498A-81D0-56ED24E5FE9F": "009BA758-7060-4479-8EE8-FB9B40C8FB97", // Korean and Japan night "b5-1": "044AD56C-A107-41B2-90CC-E60CCACFBCF5", // Great Wall 3 "b2-1": "22162A9B-DB90-4517-867C-C676BC3E8E95", // Great wall 2 "b6-1": "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF", // Great wall 1 "BAF76353-3475-4855-B7E1-CE96CC9BC3A7": "9680B8EB-CE2A-4395-AF41-402801F4D6A6", // Approaching Burj Khalifa (night) "B3BDC635-756D-4B82-B01A-A2620D1DBF10": "9680B8EB-CE2A-4395-AF41-402801F4D6A6", // Approaching Burj Khalifa (night) "15F9B681-9EA8-4DD1-AD26-F111BC5CF64B": "E991AC0C-F272-44D8-88F3-05F44EDFE3AE", // Marina 1 "49790B7C-7D8C-466C-A09E-83E38B6BE87A": "E991AC0C-F272-44D8-88F3-05F44EDFE3AE", // Marina 1 "802866E6-4AAF-4A69-96EA-C582651391F1": "3FFA2A97-7D28-49EA-AA39-5BC9051B2745", // Marina 2 "D34A7B19-EC33-4300-B4ED-0C8BC494C035": "3FFA2A97-7D28-49EA-AA39-5BC9051B2745", // Marina 2 "02EA5DBE-3A67-4DFA-8528-12901DFD6CC1": "00BA71CD-2C54-415A-A68A-8358E677D750", // Downtown "AC9C09DD-1D97-4013-A09F-B0F5259E64C3": "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9", // Sheikh Zayed Road (day) "DFA399FA-620A-4517-94D6-BF78BF8C5E5A": "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9", // Sheikh Zayed Road (day) "D388F00A-5A32-4431-A95C-38BF7FF7268D": "B8F204CE-6024-49AB-85F9-7CA2F6DCD226", // Nuusuaq Peninsula "E4ED0B22-EB81-4D4F-A29E-7E1EA6B6D980": "B8F204CE-6024-49AB-85F9-7CA2F6DCD226", // Nuusuaq Peninsula "30047FDA-3AE3-4E74-9575-3520AD77865B": "2F52E34C-39D4-4AB1-9025-8F7141FAA720", // Ilulissat Icefjord day "7D4710EB-5BA4-42E6-AA60-68D77F67D9B9": "EE01F02D-1413-436C-AB05-410F224A5B7B", // Ilulissat Icefjord Night "b8-1": "82BD33C9-B6D2-47E7-9C42-AA3B7758921A", // Pu'u O 'Umi Night "b4-1": "258A6797-CC13-4C3A-AB35-4F25CA3BF474", // Pu'u O 'Umi day "b1-1": "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7", // Waimanu Valley "b7-1": "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943", // Laupāhoehoe Nui "b6-2": "3D729CFC-9000-48D3-A052-C5BD5B7A6842", // Kohala coastline "30313BC1-BF20-45EB-A7B1-5A6FFDBD2488": "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A", // Hong Kong Victoria Harbour night "2A57BB93-1825-484C-9609-FF8580CAE77B": "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A", // Hong Kong Victoria Harbour night "102C19D1-9D9F-48EC-B492-074C985C4D9F": "FE8E1F9D-59BA-4207-B626-28E34D810D0A", // Hong Kong Victoria Harbour 1 "786E674C-BB22-4AA9-9BD3-114D2020EC4D": "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA", // Hong Kong Victoria Harbour 2 "560E09E8-E89D-4ADB-8EEA-4754415383D4": "C8559883-6F3E-4AF2-8960-903710CD47B7", // Hong Kong Victoria Peak "6E2FC8AC-832D-46CF-B306-BB2A05030C17": "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8", // Liwa oasis 1 "88025454-6D58-48E8-A2DB-924988FAD7AC": "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8", // Liwa oasis 1 "b6-3": "58754319-8709-4AB0-8674-B34F04E7FFE2", // River Thames "b1-2": "F604AF56-EA77-4960-AEF7-82533CC1A8B3", // River Thames near sunset "b3-1": "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97", // River Times at Dusk "b5-2": "A5AAFF5D-8887-42BB-8AFD-867EF557ED85", // Buckingham Palace "BEED64EC-2DB7-47E1-A67E-59C101E73C04": "CE279831-1CA7-4A83-A97B-FF1E20234396", // LAX "829E69BA-BB53-4841-A138-4DF0C2A74236": "CE279831-1CA7-4A83-A97B-FF1E20234396", // LAX "60CD8E2E-35CD-4192-A5A4-D5E10BFE158B": "92E48DE9-13A1-4172-B560-29B4668A87EE", // Santa Monica Beach "B730433D-1B3B-4B99-9500-A286BF7A9940": "92E48DE9-13A1-4172-B560-29B4668A87EE", // Santa Monica Beach "30A2A488-E708-42E7-9A90-B749A407AE1C": "35693AEA-F8C4-4A80-B77D-C94B20A68956", // Harbor Freeway "A284F0BF-E690-4C13-92E2-4672D93E8DE5": "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9", // Downtown "b3-2": "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8", // Upper East side "b4-2": "640DFB00-FBB9-45DA-9444-9F663859F4BC", // Lower Manhattan (night) "b2-3": "44166C39-8566-4ECA-BD16-43159429B52F", // Seventh Avenue "b7-2": "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89", // Central Park "b10-3": "EE533FBD-90AE-419A-AD13-D7A60E2015D6", // Marin Headlands in Fog "b1-4": "3E94AE98-EAF2-4B09-96E3-452F46BC114E", // Bay bridge night "b9-3": "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51", // Bay and Golden Bridge "b7-3": "29BDF297-EB43-403A-8719-A78DA11A2948", // Fisherman's Wharf "b3-3": "85CE77BF-3413-4A7B-9B0F-732E96229A73", // Embarcadero, Market Street "391BDF6E-3279-4CE1-9CA5-0F82811452D7": "83C65C90-270C-4490-9C69-F51FE03D7F06" // Seals tvOS 15 is reusing an old id ] // Extra info to be merged for a given ID, as of right now only one known video static let mergeInfo = [ "2F11E857-4F77-4476-8033-4A1E4610AFCC": ["url-1080-SDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_2K_SDR_HEVC.mov", "url-1080-HDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_2K_HDR_HEVC.mov", "url-4K-SDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_4K_SDR_HEVC.mov", "url-4K-HDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_4K_HDR_HEVC.mov" ] // Dubai night 2 ] static let cityVideos = [ "b8-3", // San Francisco - Alamo Square "9680B8EB-CE2A-4395-AF41-402801F4D6A6", // Dubai - Approaching Burj Khalifa "3E94AE98-EAF2-4B09-96E3-452F46BC114E", // San Francisco - Bay Bridge "4AD99907-9E76-408D-A7FC-8429FF014201", // San Francisco - Bay and Embarcadero "A5AAFF5D-8887-42BB-8AFD-867EF557ED85", // London - Buckingham Palace "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89", // New York - Central Park "00BA71CD-2C54-415A-A68A-8358E677D750", // Dubai - Downtown "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9", // Los Angeles - Downtown "b6-4", // San Francisco - Downtown and Coit Tower "b2-4", // San Francisco - Downtown and Sutro Tower "85CE77BF-3413-4A7B-9B0F-732E96229A73", // San Francisco - Embarcadero, Market Street "b5-3", // San Francisco - Embarcadero, Market Street "29BDF297-EB43-403A-8719-A78DA11A2948", // San Francisco - Fisherman’s Wharf "35693AEA-F8C4-4A80-B77D-C94B20A68956", // Los Angeles - Harbor Freeway "CE279831-1CA7-4A83-A97B-FF1E20234396", // Los Angeles - Los Angeles Int’l Airport "640DFB00-FBB9-45DA-9444-9F663859F4BC", // New York - Lower Manhattan "b1-3", // New York - Lower Manhattan "E991AC0C-F272-44D8-88F3-05F44EDFE3AE", // Dubai - Marina 1 "3FFA2A97-7D28-49EA-AA39-5BC9051B2745", // Dubai - Marina 2 "58754319-8709-4AB0-8674-B34F04E7FFE2", // London - River Thames "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97", // London - River Thames at Dusk "F604AF56-EA77-4960-AEF7-82533CC1A8B3", // London - River Thames near Sunset "44166C39-8566-4ECA-BD16-43159429B52F", // New York - Seventh Avenue "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9", // Dubai - Sheikh Zayed Road "2F11E857-4F77-4476-8033-4A1E4610AFCC", // Dubai - Sheikh Zayed Road "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8", // New York - Upper East Side "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A", // Hong Kong - Victoria Harbour "FE8E1F9D-59BA-4207-B626-28E34D810D0A", // Hong Kong - Victoria Harbour 1 "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA", // Hong Kong - Victoria Harbour 2 "C8559883-6F3E-4AF2-8960-903710CD47B7", // Hong Kong - Victoria Peak "024891DE-B7F6-4187-BFE0-E6D237702EF0" // Hong Kong - Wan Chai ] static let countrySideVideos = [ "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51", // San Francisco - Bay and Golden Gate "72B4390D-DF1D-4D51-B179-229BBAEFFF2C", // San Francisco - Golden Gate from SF "b8-2", // San Francisco - Marin Headlands "EE533FBD-90AE-419A-AD13-D7A60E2015D6", // San Francisco - Marin Headlands in Fog "89B1643B-06DD-4DEC-B1B0-774493B0F7B7", // Los Angeles - Griffith Observatory "EC67726A-8212-4C5E-83CF-8412932740D2", // Los Angeles - Hollywood Hills "b4-3" // San Francisco - Presidio to Golden Gate ] static let beachVideos = [ "b2-2", // Hawaii - Honopū Valley "3D729CFC-9000-48D3-A052-C5BD5B7A6842", // Hawaii - Kohala Coastline "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7", // Hawaii - Laupāhoehoe Nui "92E48DE9-13A1-4172-B560-29B4668A87EE" // Los Angeles - Santa Monica Beach ] static let spaceVideos = [ "A837FA8C-C643-4705-AE92-074EFDD067F7", "2F72BC1E-3D76-456C-81EB-842EBA488C27", "A2BE2E4A-AD4B-428A-9C41-BDAE1E78E816", "12318CCB-3F78-43B7-A854-EFDCCE5312CD", "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8", "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589", "6A74D52E-2447-4B84-AE45-0DEF2836C3CC", "7825C73A-658F-48EE-B14C-EC56673094AC", "E5DB138A-F04E-4619-B896-DE5CB538C534", "F439B0A7-D18C-4B14-9681-6520E6A74FE9", "62A926BE-AA0B-4A34-9653-78C4F130543F", "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E", "6C3D54AE-0871-498A-81D0-56ED24E5FE9F", "009BA758-7060-4479-8EE8-FB9B40C8FB97", "78911B7E-3C69-47AD-B635-9C2486F6301D", "D60B4DDA-69EB-4841-9690-E8BAE7BC4F80", "7719B48A-2005-4011-9280-2F64EEC6FD91", "63C042F0-90EF-4A95-B7CC-CC9A64BF8421", "B1B5DDC5-73C8-4920-8133-BACCE38A08DE", "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182", // 25/01 Antartica Aurora "737E9E24-49BE-4104-9B72-F352DE1AD2BF", // North America Aurora "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4", // Sahara and Italy "64D11DAB-3B57-4F14-AD2F-E59A9282FA44", // Atlantic Ocean to Spain and France "81337355-E156-4242-AAF4-711768D30A54", // Australia "1088217C-1410-4CF7-BDE9-8F573A4DBCD9", // Caribbean "3C4678E4-4D3D-4A40-8817-77752AEA62EB", // Nile Delta "87060EC2-D006-4102-98CC-3005C68BB343" // South Africa to North Asia ] static let seaVideos = [ "83C65C90-270C-4490-9C69-F51FE03D7F06", // Seals (outdated) "BA4ECA11-592F-4727-9221-D2A32A16EB28", // Palau Jellies * "F07CC61B-30FC-4614-BDAD-3240B61F6793", // Palau Coral "6143116D-03BB-485E-864E-A8CF58ACF6F1", // Kelp "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC", // Barracuda "E580E5A5-0888-4BE8-A4CA-F74A18A643C3", // Palau Jellies * "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8", // Palau Jellies * "581A4F1A-2B6D-468C-A1BE-6F473F06D10B", // Sea Stars "687D03A2-18A5-4181-8E85-38F3A13409B9", // Bumpheads "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268", // Jacks "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1", // Alaskan Jellies * "C6DC4E54-1130-44F8-AF6F-A551D8E8A181", // Alaskan Jellies * "27A37B0F-738D-4644-A7A4-E33E7A6C1175", // California Dolphins "EB3F48E7-D30F-4079-858F-1A61331D5026", // California Kelp Forest "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB", // Costa Rica Dolphins "58C75C62-3290-47B8-849C-56A583173570", // Cownose Rays "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB", // Gray Reef Sharks "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E", // Humpback Whale "82175C1F-153C-4EC8-AE37-2860EA828004", // Red Sea Coral "149E7795-DBDA-4F5D-B39A-14712F841118", // Tahiti Waves * "8C31B06F-91A4-4F7C-93ED-56146D7F48B9", // Tahiti Waves * "391BDF6E-3279-4CE1-9CA5-0F82811452D7" // Seals (new version) ] static let timeInformation = [ "A837FA8C-C643-4705-AE92-074EFDD067F7": "night", // Africa Night "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": "sunrise", // Space - Antartica "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": "sunset", // Space - Atlantic Ocean to Spain and France "81337355-E156-4242-AAF4-711768D30A54": "night", // Space - Australia "A2BE2E4A-AD4B-428A-9C41-BDAE1E78E816": "night", // California to Vegas (v7) "12318CCB-3F78-43B7-A854-EFDCCE5312CD": "night", // California to Vegas (v8) "6A74D52E-2447-4B84-AE45-0DEF2836C3CC": "night", // China "7825C73A-658F-48EE-B14C-EC56673094AC": "night", // China (new id) "E5DB138A-F04E-4619-B896-DE5CB538C534": "night", // Italy to Asia "F439B0A7-D18C-4B14-9681-6520E6A74FE9": "sunset", // Iran and Afghanistan "62A926BE-AA0B-4A34-9653-78C4F130543F": "night", // Ireland to Asia "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": "night", // Ireland to Asia "6C3D54AE-0871-498A-81D0-56ED24E5FE9F": "night", // Korean and Japan Night (v17) "009BA758-7060-4479-8EE8-FB9B40C8FB97": "night", // Korean and Japan Night (v18) "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": "night", // Space - Mexico City to New York "78911B7E-3C69-47AD-B635-9C2486F6301D": "sunrise", // Space - New Zealand "737E9E24-49BE-4104-9B72-F352DE1AD2BF": "sunrise", // Space - North America Aurora "87060EC2-D006-4102-98CC-3005C68BB343": "sunset", // Space - South Africa to North Asia "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": "sunset", // Space - West Africa to the Alps "044AD56C-A107-41B2-90CC-E60CCACFBCF5": "sunset", // China - Great Wall 3 "EE01F02D-1413-436C-AB05-410F224A5B7B": "sunset", // Greenland - Ilulissat Icefjord "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": "sunrise", // Greenland - Nuussuaq Peninsula "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": "sunset", // Hawaii - Pu‘u O ‘Umi "9680B8EB-CE2A-4395-AF41-402801F4D6A6": "night", // Approaching Burj Khalifa "3E94AE98-EAF2-4B09-96E3-452F46BC114E": "night", // San Francisco - Bay Bridge "4AD99907-9E76-408D-A7FC-8429FF014201": "sunset", // San Francisco - Bay and Embarcadero "00BA71CD-2C54-415A-A68A-8358E677D750": "sunrise", // Dubai - Downtown "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": "night", // Los Angeles - Downtown "b6-4": "sunset", // San Francisco - Downtown and Coit Tower "b2-4": "sunset", // San Francisco - Downtown and Sutro Tower "85CE77BF-3413-4A7B-9B0F-732E96229A73": "sunrise", // San Francisco - Embarcadero, Market Street "b5-3": "sunset", // San Francisco - Embarcadero, Market Street "29BDF297-EB43-403A-8719-A78DA11A2948": "sunrise", // San Francisco - Fisherman’s Wharf "640DFB00-FBB9-45DA-9444-9F663859F4BC": "sunset", // New York - Lower Manhattan "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": "sunset", // London - River Thames at Dusk "F604AF56-EA77-4960-AEF7-82533CC1A8B3": "sunset", // London - River Thames near Sunset "44166C39-8566-4ECA-BD16-43159429B52F": "night", // New York - Seventh Avenue "2F11E857-4F77-4476-8033-4A1E4610AFCC": "night", // Dubai - Sheikh Zayed Road "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": "sunset", // Hong Kong - Victoria Harbour "3D729CFC-9000-48D3-A052-C5BD5B7A6842": "sunset", // Hawaii - Kohala Coastline "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": "sunset", // Los Angeles - Griffith Observatory "EC67726A-8212-4C5E-83CF-8412932740D2": "sunset", // Los Angeles - Hollywood Hills "EE533FBD-90AE-419A-AD13-D7A60E2015D6": "sunrise", // San Francisco - Marin Headlands in Fog "b4-3": "sunrise" // San Francisco - Presidio to Golden Gate // "BAF76353-3475-4855-B7E1-CE96CC9BC3A7": "night", // Dubai // "30313BC1-BF20-45EB-A7B1-5A6FFDBD2488": "night", // Hong Kong // "A284F0BF-E690-4C13-92E2-4672D93E8DE5": "night", // Los Angeles (old ?) // "44166C39-8566-4ECA-BD16-43159429B52F": "night", // Seventh Avenue ] // Extra POI static let mergePOI = [ "b6-1": "C001_C005_", // China day 4 "b2-1": "C004_C003_", // China day 5 "b5-1": "C003_C003_", // China day 6 "7D4710EB-5BA4-42E6-AA60-68D77F67D9B9": "GL_G010_C006_", // Greenland night 1 "b7-1": "H007_C003", // Hawaii day 1 "b1-1": "H005_C012_", // Hawaii day 2 "b2-2": "H010_C006_", // Hawaii day 3 "b4-1": "H004_C007_", // Hawaii day 4 "b6-2": "H012_C009_", // Hawaii night 1 "b8-1": "H004_C009_", // Hawaii night 2 "6E2FC8AC-832D-46CF-B306-BB2A05030C17": "LW_L001_C006_", // Liwa day 1 LW_L001_C006_0 "b6-3": "L010_C006_", // London day 1 "b5-2": "L007_C007_", // London day 2 "b1-2": "L012_C002_", // London night 1 "b3-1": "L004_C011_", // London night 2 "A284F0BF-E690-4C13-92E2-4672D93E8DE5": "LA_A011_C003_", // Los Angeles night 3 "b7-2": "N008_C009_", // New York day 1 "b1-3": "N006_C003_", // New York day 2 "b3-2": "N003_C006_", // New York day 3 "b2-3": "N013_C004_", // New York night 1 "b4-2": "N008_C003_", // New York night 2 "b8-2": "A008_C007_", // San Francisco day 1 // "b10-3": , // San Francisco day 2 "b9-3": "A006_C003_", // San Francisco day 3 // "b8-3":"", San Francisco day 4 (no extra poi ?) "b3-3": "A012_C014_", // San Francisco day 5 // maybe A013_C004 ? "b4-3": "A013_C005_", // San Francisco day 6 "b6-4": "A004_C012_", // San Francisco night 1 "b7-3": "A007_C017_", // San Francisco night 2 "b5-3": "A015_C014_", // San Francisco night 3 "b1-4": "A015_C018_", // San Francisco night 4 "b2-4": "A018_C014_", // San Francisco night 5 "2F11E857-4F77-4476-8033-4A1E4610AFCC": "DB_D008_C010_" // Stealing the day description for the night one ] // Look for a previously processed similar video // // tvOS11 and 12 JSON are using the same ID (and tvOS12 JSON always has better data, // so no need for a fancy merge) // // tvOS10 however JSON DOES NOT use the same ID, so we need to dupecheck on the h264 // (only available format there) filename (they actually have different URLs !) static func findDuplicate(id: String, url1080pH264: String) -> (Bool, AerialVideo?) { // We blacklist some duplicates if url1080pH264 != "" { if blacklist.contains((URL(string: url1080pH264)?.lastPathComponent)!) { return (true, nil) } } // We also have a Dictionary of duplicates that need source merging for (pid, replace) in dupePairs where id == pid { for vid in VideoList.instance.videos where vid.id == replace { // Found dupe pair return (true, vid) } } for video in VideoList.instance.videos { if id == video.id { return (true, video) } else if url1080pH264 != "" && video.urls[.v1080pH264] != "" { if URL(string: url1080pH264)?.lastPathComponent == URL(string: video.urls[.v1080pH264]!)?.lastPathComponent { return (true, video) } } } return (false, nil) } static func getSceneForVideo(id: String) -> SourceScene? { if seaVideos.contains(id) { return .sea } else if spaceVideos.contains(id) { return .space } else if cityVideos.contains(id) { return .city } else if countrySideVideos.contains(id) { return .countryside } else if beachVideos.contains(id) { return .beach } return nil } } ================================================ FILE: Aerial/Source/Models/Sources/SourceList.swift ================================================ // // SourceList.swift // Aerial // // Created by Guillaume Louel on 01/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation struct SourceHeader { let name: String let sources: [Source] } // swiftlint:disable:next type_body_length struct SourceList { // This is the current one until next fall /*static let macOS15 = Source(name: "macOS 15", description: "High framerate videos from macOS 15 Sequoia", manifestUrl: "https://sylvan.apple.com/itunes-assets/Aerials126/v4/82/2e/34/822e344c-f5d2-878c-3d56-508d5b09ed61/resources-15-0-2.tar", type: .macOS, scenes: [.nature, .city, .space, .sea], isCachable: true, license: "", more: "")*/ static let macOS26 = Source(name: "macOS 26", description: "High framerate videos from macOS 26", manifestUrl: "https://sylvan.apple.com/itunes-assets/Aerials126/v4/82/2e/34/822e344c-f5d2-878c-3d56-508d5b09ed61/resources-26-0-1.tar", type: .macOS, scenes: [.nature, .city, .space, .sea], isCachable: true, license: "", more: "") // This is the current one until next fall /*static let macOS14 = Source(name: "macOS 14", description: "High framerate videos from macOS 14 Sonoma", manifestUrl: "https://sylvan.apple.com/itunes-assets/Aerials126/v4/82/2e/34/822e344c-f5d2-878c-3d56-508d5b09ed61/resources-14-0-10.tar", type: .macOS, scenes: [.nature, .city, .space, .sea], isCachable: true, license: "", more: "")*/ // This is the current one until next fall static let tvOS16 = Source(name: "tvOS 16", description: "Apple TV screensavers from tvOS 16", manifestUrl: "https://sylvan.apple.com/Aerials/resources-16.tar", type: .tvOS12, scenes: [.nature, .city, .space, .sea], isCachable: true, license: "", more: "") // Legacy sources static let tvOS13 = Source(name: "tvOS 13", description: "Apple TV screensavers from tvOS 13", manifestUrl: "https://sylvan.apple.com/Aerials/resources-13.tar", type: .tvOS12, scenes: [.nature, .city, .space, .sea], isCachable: true, license: "", more: "") /*static let tvOS12 = Source(name: "tvOS 12", description: "Apple TV screensavers from tvOS 12", manifestUrl: "https://sylvan.apple.com/Aerials/resources.tar", type: .tvOS12, scenes: [.nature, .city, .space], isCachable: true, license: "", more: "") static let tvOS11 = Source(name: "tvOS 11", description: "Apple TV screensavers from tvOS 11", manifestUrl: "https://sylvan.apple.com/Aerials/2x/entries.json", type: .tvOS11, scenes: [.nature, .city], isCachable: true, license: "", more: "")*/ /* static let tvOS10 = Source(name: "tvOS 10", description: "Apple TV screensavers from tvOS 10", manifestUrl: "http://a1.phobos.apple.com/us/r1000/000/Features/atv/AutumnResources/videos/entries.json", type: .tvOS10, scenes: [.nature, .city], isCachable: true, license: "", more: "")*/ static var list: [Source] = [macOS26, tvOS16, tvOS13] + foundSources // static var list: [Source] = foundSources // This is where the magic happens static var foundSources: [Source] { var sources: [Source] = [] var foundCommunity = false for folder in URL(fileURLWithPath: Cache.supportPath).subDirectories { if !folder.lastPathComponent.starts(with: "tvOS") && !folder.lastPathComponent.starts(with: "macOS") && !folder.lastPathComponent.starts(with: "backups") && !folder.lastPathComponent.starts(with: "Thumbnails") && !folder.lastPathComponent.starts(with: "Cache") { if folder.lastPathComponent.starts(with: "Community") || folder.lastPathComponent.starts(with: "From") { foundCommunity = true } // If it's valid, let's add ! if let source = loadManifest(url: folder) { sources.append(source) } else if let newsources = loadMetaManifest(url: folder) { sources.append(contentsOf: newsources) } } } if !foundCommunity { DispatchQueue.main.async { fetchOnlineManifest(url: URL(string: "https://aerialscreensaver.github.io/community/")!) } } return sources } // swiftlint:disable for_where static func hasNamed(name: String) -> Bool { for source in list where source.type == .local { if source.name == name { return true } } return false } static func categorizedSourceList() -> [SourceHeader] { var communities: [Source] = [] var online: [Source] = [] var local: [Source] = [] var apple: [Source] = [] for source in list { // where !source.name.starts(with: "tvOS") { if source.type == .local { local.append(source) } else { // This may need to be changed in the future if !source.isCachable { online.append(source) } else if source.name.starts(with: "tvOS") || source.name.starts(with: "macOS") { apple.append(source) } else { communities.append(source) } } } // Then we build our list var output: [SourceHeader] = [] if !communities.isEmpty { output.append(SourceHeader(name: "Community Videos", sources: communities)) } if !online.isEmpty { output.append(SourceHeader(name: "Online Sources", sources: online)) } if !apple.isEmpty { output.append(SourceHeader(name: "Apple", sources: apple)) } if !local.isEmpty { output.append(SourceHeader(name: "Local Sources", sources: local)) } return output } static func fetchOnlineManifest(url: URL) { if let source = loadManifest(url: url) { debugLog("Source loaded") // Then save ! let downloadManager = DownloadManager() downloadManager.queueDownload(url.appendingPathComponent("manifest.json"), folder: source.name) downloadManager.queueDownload(URL(string: source.manifestUrl)!, folder: source.name) list.append(source) source.setEnabled(true) // This will reload the main video list } else if let sources = loadMetaManifest(url: url) { debugLog("Sources loaded") for source in sources { // Then save ! saveSource(source) let downloadManager = DownloadManager() downloadManager.queueDownload(URL(string: source.manifestUrl)!, folder: source.name) list.append(source) source.setEnabled(true) // This will reload the main video list } } else { debugLog("Something went wrong here") let task = URLSession.shared.dataTask(with: url) { _, response, error in if let error = error { debugLog("Can't load file, possible firewall issue") DispatchQueue.main.async { Aerial.helper.showErrorAlert(question: "An error occured loading the file", text: "Please check your network connection, firewall, and try again. \n\nError : \(error.localizedDescription)") } return } guard let response = response as? HTTPURLResponse else { debugLog("No HTTP response") DispatchQueue.main.async { Aerial.helper.showErrorAlert(question: "No HTTP Response", text: "Please check your network connection, firewall, and try again.") } return } if response.statusCode != 200 { DispatchQueue.main.async { debugLog("HTTP error") Aerial.helper.showErrorAlert(question: "HTTP Error", text: "Please verify the URL (and check your network connexion and firewall). HTTP error: \(response.statusCode)") } return } else { DispatchQueue.main.async { debugLog("Incorect JSON format") Aerial.helper.showErrorAlert(question: "Incorrect JSON Format", text: "Your URL was valid, but the file is not in the correct format. Please check the URL.") } return } } task.resume() } } static func updateLocalSource(source: Source, reload: Bool) { // We need the raw manifest to find the path inside let videos = source.getUnprocessedVideos() let originalAssets = source.getUnprocessedAssets() var updatedAssets = [VideoAsset]() if videos.count >= 1 { let url = videos.first!.url.deletingLastPathComponent() let folderName = url.lastPathComponent debugLog("processing url for videos : \(url)") do { let urls = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) // var assets = [VideoAsset]() for lurl in urls { if lurl.path.lowercased().hasSuffix(".mp4") || lurl.path.lowercased().hasSuffix(".mov") { let fileManager = FileManager.default let attributes = try? fileManager.attributesOfItem(atPath: lurl.path) let fileType = attributes?[.type] as? FileAttributeType let resourceValues = try lurl.resourceValues(forKeys: [.fileSizeKey]) let fileSize = resourceValues.fileSize! if fileSize > 500000 || fileType == .typeSymbolicLink { // Check if the asset was there previously let foundAssets = originalAssets.filter { $0.url4KSDR == lurl.path } if let foundAsset = foundAssets.first { // Just add the asset to the new array updatedAssets.append(foundAsset) } else { // Create a new entry updatedAssets.append(VideoAsset(accessibilityLabel: folderName, id: NSUUID().uuidString, title: lurl.lastPathComponent, timeOfDay: "day", scene: "", pointsOfInterest: [:], url4KHDR: "", url4KSDR: lurl.path, url1080H264: "", url1080HDR: "", url4KSDR120FPS: "", url4KSDR240FPS: "", url1080SDR: "", url: "", type: "nature")) } } } } debugLog("Updating manifest \(url.lastPathComponent)") let videoManifest = VideoManifest(assets: updatedAssets, initialAssetCount: 1, version: 1) SourceList.saveEntries(source: source, manifest: videoManifest) if reload { VideoList.instance.reloadSources() } } catch { errorLog("Could not process directory") } } else { debugLog("Cannot parse your directory, did you delete your videos ?") } } static func processPathForVideos(url: URL) { debugLog("processing url for videos : \(url) ") let folderName = url.lastPathComponent do { let urls = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) var assets = [VideoAsset]() for lurl in urls { if lurl.path.lowercased().hasSuffix(".mp4") || lurl.path.lowercased().hasSuffix(".mov") { let fileManager = FileManager.default let attributes = try? fileManager.attributesOfItem(atPath: lurl.path) let fileType = attributes?[.type] as? FileAttributeType let resourceValues = try lurl.resourceValues(forKeys: [.fileSizeKey]) let fileSize = resourceValues.fileSize! if fileSize > 500000 || fileType == .typeSymbolicLink { assets.append(VideoAsset(accessibilityLabel: folderName, id: NSUUID().uuidString, title: lurl.lastPathComponent, timeOfDay: "day", scene: "", pointsOfInterest: [:], url4KHDR: "", url4KSDR: lurl.path, url1080H264: "", url1080HDR: "", url4KSDR120FPS: "", url4KSDR240FPS: "", url1080SDR: "", url: "", type: "nature")) } } } // ... if SourceList.hasNamed(name: url.lastPathComponent) { Aerial.helper.showInfoAlert(title: "Source name mismatch", text: "A source with this name already exists. Try renaming your folder and try again.") } else { debugLog("Creating source \(url.lastPathComponent)") // Generate and save the Source let source = Source(name: url.lastPathComponent, description: "Local files from \(url.path)", manifestUrl: "manifest.json", type: .local, scenes: [.nature], isCachable: false, license: "", more: "") SourceList.saveSource(source) // Then the entries let videoManifest = VideoManifest(assets: assets, initialAssetCount: 1, version: 1) SourceList.saveEntries(source: source, manifest: videoManifest) list.append(source) VideoList.instance.reloadSources() } } catch { errorLog("Could not process directory") } } static func saveSource(_ source: Source) { let manifest = Manifest.init(name: source.name, manifestDescription: source.description, scenes: source.scenes.map({ $0.rawValue }), local: source.type == .local, cacheable: source.isCachable, manifestUrl: source.manifestUrl, license: source.license, more: source.more) // First make the folder FileHelpers.createDirectory(atPath: Cache.supportPath.appending("/"+source.name)) let json = try? JSONEncoder().encode(manifest) do { try json!.write(to: URL(fileURLWithPath: Cache.supportPath.appending("/"+source.name+"/manifest.json"))) } catch { errorLog("Can't save local source : \(error.localizedDescription)") } } static func saveEntries(source: Source, manifest: VideoManifest) { let json = try? JSONEncoder().encode(manifest) do { try json!.write(to: URL(fileURLWithPath: Cache.supportPath.appending("/"+source.name+"/entries.json"))) } catch { errorLog("Can't save local entries : \(error.localizedDescription)") } } static func loadMetaManifest(url: URL) -> [Source]? { // Let's make sure we have the required files if !areManifestPresent(url: url) && !url.absoluteString.starts(with: "http") { return nil } do { let jsonData = try Data(contentsOf: url.appendingPathComponent("manifest.json")) if let metamanifest = try? newJSONDecoder().decode(MetaManifest.self, from: jsonData) { var sources: [Source] = [] for manifest in metamanifest.sources { sources.append(parseSourceFromManifest(manifest, url: url)) } return sources } } catch { errorLog("Could not open manifest for source at \(url)") return nil } return nil } static func loadManifest(url: URL) -> Source? { // Let's make sure we have the required files if !areManifestPresent(url: url) && !url.absoluteString.starts(with: "http") { return nil } do { let jsonData = try Data(contentsOf: url.appendingPathComponent("manifest.json")) if let manifest = try? newJSONDecoder().decode(Manifest.self, from: jsonData) { debugLog("Manifest opened, going to parsing") return parseSourceFromManifest(manifest, url: nil) } } catch { errorLog("Could not open manifest for source at \(url)") return nil } return nil } static private func parseSourceFromManifest(_ manifest: Manifest, url: URL?) -> Source { var local = true var mURL: String if let isLocal = manifest.local { local = isLocal } if local { mURL = (url != nil) ? url!.absoluteString : manifest.manifestUrl ?? "" } else { mURL = manifest.manifestUrl ?? "" } let cacheable: Bool = manifest.cacheable ?? !local debugLog("Parsed \(manifest.name)") return Source(name: manifest.name, description: manifest.manifestDescription, manifestUrl: mURL, type: local ? .local : .tvOS12, scenes: jsonToSceneArray(array: manifest.scenes ?? []), isCachable: cacheable, license: manifest.license ?? "", more: manifest.more ?? "") } /// Helper to convert an array of strings to an array of sources /// /// ["landscape"] -> [.landscape] static func jsonToSceneArray(array: [String]) -> [SourceScene] { var output: [SourceScene] = [] for scene in array { switch scene { case "sea": output.append(.sea) case "space": output.append(.space) case "city": output.append(.city) case "beach": output.append(.beach) case "countryside": output.append(.countryside) default: output.append(.nature) } } return output } static func areManifestPresent(url: URL) -> Bool { // For a source to be valid we at the very least need two things // manifest.json <- a description of the source // entries.json <- the classic video manifest return FileManager.default.fileExists(atPath: url.path.appending("/entries.json")) || FileManager.default.fileExists(atPath: url.path.appending("/manifest.json")) } } // MARK: - MetaManifest struct MetaManifest: Codable { let sources: [Manifest] } // MARK: - Manifest struct Manifest: Codable { let name, manifestDescription: String let scenes: [String]? let local: Bool? let cacheable: Bool? let manifestUrl: String? let license: String? let more: String? enum CodingKeys: String, CodingKey { case name case manifestDescription = "description" case scenes case local case cacheable case manifestUrl case license case more } } ================================================ FILE: Aerial/Source/Models/Sources/VideoList.swift ================================================ // // VideoList.swift // Aerial // // Created by Guillaume Louel on 08/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation typealias VideoListRefreshCallback = () -> Void extension RangeReplaceableCollection { /// Returns a collection containing, in order, the first instances of /// elements of the sequence that compare equally for the keyPath. func unique(for keyPath: KeyPath) -> Self { var unique = Set() return filter { unique.insert($0[keyPath: keyPath]).inserted } } } // swiftlint:disable:next type_body_length class VideoList { enum FilterMode { case location, cache, time, scene, source, rotation, favorite, hidden } static let instance: VideoList = VideoList() var callbacks = [VideoListRefreshCallback]() var videos: [AerialVideo] = [] // OLD Playlist management var playlistIsRestricted = false var playlistRestrictedTo = "" var playlistHasVerticalVideos = false var playlist = [AerialVideo]() var playlistForScreen: [String:[AerialVideo]] = [:] var lastPluckedFromPlaylist: AerialVideo? let cacheDownloaded = "Downloaded" let cacheOnline = "Online" init() { downloadManifestsIfNeeded() } func videoForFilename(_ name: String) -> AerialVideo? { for video in videos where video.url.lastPathComponent == name { return video } errorLog("vFF unknown video filename") return nil } // This is used to grab the correct path depending on whether a source is cacheable or not func localPathFor(video: AerialVideo) -> String { if video.source.isCachable { return VideoCache.cachePath(forVideo: video) ?? "" } else { return VideoCache.sourcePathFor(video) } } // MARK: - Helpers for the various filterings private func cacheSources() -> [String] { var cache: [String] = [] if !videos.filter({ $0.isAvailableOffline && !PrefsVideos.hidden.contains($0.id) }).isEmpty { cache.append(cacheDownloaded) } if !videos.filter({ !$0.isAvailableOffline && !PrefsVideos.hidden.contains($0.id) }).isEmpty { cache.append(cacheOnline) } return cache } private func sourcesFor(_ mode: FilterMode) -> [String] { switch mode { case .location: return videos.filter { !PrefsVideos.hidden.contains($0.id) }.map { $0.name }.unique(for: \.self) case .time: return videos.filter { !PrefsVideos.hidden.contains($0.id) }.map { $0.timeOfDay.capitalizeFirstLetter() }.unique(for: \.self) case .scene: return videos.filter { !PrefsVideos.hidden.contains($0.id) }.map { $0.scene.rawValue.capitalizeFirstLetter() }.unique(for: \.self) case .source: return videos.filter { !PrefsVideos.hidden.contains($0.id) }.map { $0.source.name }.unique(for: \.self) case .cache: return cacheSources() case .rotation: return ["On Rotation"] case .favorite: return ["Favorites"] case .hidden: return ["Hidden"] } } private func filteredVideosFor(_ mode: FilterMode, section: Int) -> [AerialVideo] { switch mode { case .location: let filter = sourcesFor(mode)[section].lowercased() return videos .filter { $0.name.lowercased() == filter && !PrefsVideos.hidden.contains($0.id) } .sorted { $0.secondaryName < $1.secondaryName } case .time: let filter = sourcesFor(mode)[section].lowercased() return videos .filter { $0.timeOfDay.lowercased() == filter && !PrefsVideos.hidden.contains($0.id) } .sorted { $0.secondaryName < $1.secondaryName } case .scene: let filter = sourcesFor(mode)[section].lowercased() return videos .filter { $0.scene.rawValue.lowercased() == filter && !PrefsVideos.hidden.contains($0.id) } .sorted { $0.secondaryName < $1.secondaryName } case .source: let filter = sourcesFor(mode)[section].lowercased() return videos .filter { $0.source.name.lowercased() == filter && !PrefsVideos.hidden.contains($0.id) } .sorted { $0.secondaryName < $1.secondaryName } case .cache: // TODO FIX THIS IT CRASHES WHEN YOU FAV FROM ONLINE // if let cacheSources(). if cacheSources()[section] == cacheDownloaded { return videos .filter({ $0.isAvailableOffline && !PrefsVideos.hidden.contains($0.id) }) .sorted { $0.secondaryName < $1.secondaryName } } else { return videos .filter({ !$0.isAvailableOffline && !PrefsVideos.hidden.contains($0.id) }) .sorted { $0.secondaryName < $1.secondaryName } } case .rotation: return currentRotation() // Result is already sorted there case .favorite: return videos .filter { PrefsVideos.favorites.contains($0.id) && !PrefsVideos.hidden.contains($0.id)} .sorted { $0.secondaryName < $1.secondaryName } case .hidden: return videos .filter { PrefsVideos.hidden.contains($0.id) } .sorted { $0.secondaryName < $1.secondaryName } } } // swiftlint:disable:next cyclomatic_complexity private func filteredVideosFor(_ mode: FilterMode, filter: [String]) -> [AerialVideo] { // Our preference filters contains ALL sorts of filters (location, time) that are // saved for better user experience. So we need to filter the filters first ! var filters: [String] = [] for afilter in filter { switch mode { case .location: if afilter.starts(with: "location") { filters.append(afilter.split(separator: ":")[1].lowercased()) } case .cache: filters.append(afilter.lowercased()) case .time: if afilter.starts(with: "time") { filters.append(afilter.split(separator: ":")[1].lowercased()) } case .scene: if afilter.starts(with: "scene") { filters.append(afilter.split(separator: ":")[1].lowercased()) } case .source: if afilter.starts(with: "source") { filters.append(afilter.split(separator: ":")[1].lowercased()) } case .rotation: filters.append(afilter.lowercased()) case .favorite: filters.append(afilter.lowercased()) case .hidden: filters.append(afilter.lowercased()) } } switch mode { case .location: let vids = videos .filter { filters.contains($0.name.lowercased()) && !PrefsVideos.hidden.contains($0.id) } .sorted { $0.secondaryName < $1.secondaryName } return vids case .time: return videos .filter { filters.contains($0.timeOfDay.lowercased()) && !PrefsVideos.hidden.contains($0.id) } .sorted { $0.secondaryName < $1.secondaryName } case .scene: return videos .filter { filters.contains($0.scene.rawValue.lowercased()) && !PrefsVideos.hidden.contains($0.id) } .sorted { $0.secondaryName < $1.secondaryName } case .source: return videos .filter { filters.contains($0.source.name.lowercased()) && !PrefsVideos.hidden.contains($0.id) } .sorted { $0.secondaryName < $1.secondaryName } case .favorite: return videos .filter { PrefsVideos.favorites.contains($0.id) && !PrefsVideos.hidden.contains($0.id) } .sorted { $0.secondaryName < $1.secondaryName } case .hidden: return videos .filter { PrefsVideos.hidden.contains($0.id) } .sorted { $0.secondaryName < $1.secondaryName } default: return videos .filter({ $0.isAvailableOffline }) .sorted { $0.secondaryName < $1.secondaryName } } } // MARK: - Public getters to filter the list func getSources(mode: FilterMode) -> [String] { return sourcesFor(mode) } func getSourcesCount(mode: FilterMode) -> Int { return sourcesFor(mode).count } func getSourceName(_ section: Int, mode: FilterMode) -> String { return sourcesFor(mode)[section] } func getVideosCountForSource(_ section: Int, mode: FilterMode) -> Int { return filteredVideosFor(mode, section: section).count } func getVideoForSource(_ section: Int, item: Int, mode: FilterMode) -> AerialVideo { return filteredVideosFor(mode, section: section)[item] } func getVideosForSource(_ section: Int, mode: FilterMode) -> [AerialVideo] { return filteredVideosFor(mode, section: section) } // MARK: - Public getter for a video list from paths func getVideosForPath(_ path: String) -> [AerialVideo] { if let mode = VideoList.instance.modeFromPath(path) { let index = Int(path.split(separator: ":")[1])! return VideoList.instance.getVideosForSource(index, mode: mode) } else { // all return VideoList.instance.videos.filter({ !PrefsVideos.hidden.contains($0.id) }).sorted { $0.secondaryName < $1.secondaryName } } } func modeFromPath(_ path: String) -> FilterMode? { if path.starts(with: "location") { return .location } else if path.starts(with: "cache") { return .cache } else if path.starts(with: "time") { return .time } else if path.starts(with: "scene") { return .scene } else if path.starts(with: "rotation") { return .rotation } else if path.starts(with: "source") { return .source } else if path.starts(with: "favorites") { return .favorite } else if path.starts(with: "hidden") { return .hidden } else { return nil } } // MARK: - Callbacks func addCallback(_ callback:@escaping VideoListRefreshCallback) { callbacks.append(callback) // We may need to insta callback if we were already inited if !videos.isEmpty { callback() } } // This is how we force a source refresh, it will trigger various callbacks when done // (e.g. to refresh video list in the ui) func reloadSources() { videos = [] downloadManifestsIfNeeded() } func downloadSource(source: Source) { let downloadManager = DownloadManager() let completion = BlockOperation { self.refreshVideoList() if !PrefsCache.enableManagement { Aerial.helper.showInfoAlert(title: "Automatic downloads are disabled", text: "In order to watch the new videos, you will need to manually download them (for example by pressing the down arrow button on the right).") } } for src in SourceList.list where source.name == src.name { debugLog("Marking \(source.name) for redownload") // Then queue the download let operation = downloadManager.queueDownload(URL(string: source.manifestUrl)!, folder: source.name) completion.addDependency(operation) OperationQueue.main.addOperation(completion) } } private func downloadManifestsIfNeeded() { let downloadManager = DownloadManager() var sourceQueue: [Source] = [] let completion = BlockOperation { self.refreshVideoList() } // Let's check our sources first for source in SourceList.list { // But only the enabled ones if source.isEnabled() { // We may need to download it if !source.isCached() { debugLog("\(source.name) is not cached, downloading...") sourceQueue.append(source) } else if PrefsVideos.shouldCheckForNewVideos() && Cache.canNetwork() { debugLog("\(source.name) looking for updated manifest...") sourceQueue.append(source) } else { debugLog("\(source.name) is enabled, cached and up to date") } } } for source in SourceList.list { if source.type == .local { debugLog("\(source.name) updating local source") SourceList.updateLocalSource(source: source, reload: false) } } if !sourceQueue.isEmpty { // Now queue and download for source in sourceQueue { // Then queue the download let operation = downloadManager.queueDownload(URL(string: source.manifestUrl)!, folder: source.name) completion.addDependency(operation) // Mark that we updated our sources PrefsVideos.saveLastVideoCheck() } OperationQueue.main.addOperation(completion) } else { DispatchQueue.main.async { self.refreshVideoList() } } } // This is called when all our files are downloaded private func refreshVideoList() { debugLog("Refreshing video list") videos = [] for source in SourceList.list { if source.isEnabled() { // We may need to download it if source.isCached() { let vids = source.getVideos() videos.append(contentsOf: vids) debugLog("source : \(source.name) contains \(vids.count) new videos (total \(videos.count))") } } } videos = videos.sorted { $0.name < $1.name } // Let everyone who wants to know that our list is updated for callback in callbacks { callback() } } // MARK: - New rotation management func currentRotation() -> [AerialVideo] { var mode: FilterMode switch PrefsVideos.newShouldPlay { case .location: mode = .location case .time: mode = .time case .scene: mode = .scene case .source: mode = .source default: mode = .cache } switch PrefsVideos.newShouldPlay { /* case .everything: return videos .filter({ !PrefsVideos.hidden.contains($0.id) }) .sorted { $0.secondaryName < $1.secondaryName }*/ case .favorites: return videos .filter({ PrefsVideos.favorites.contains($0.id) && !PrefsVideos.hidden.contains($0.id) }) .sorted { $0.secondaryName < $1.secondaryName } default: print(PrefsVideos.newShouldPlayString) return filteredVideosFor(mode, filter: PrefsVideos.newShouldPlayString) } } func everythingRotation() -> [AerialVideo] { return videos .filter({ !PrefsVideos.hidden.contains($0.id) }) .sorted { $0.secondaryName < $1.secondaryName } } // MARK: - Playlist management func generatePlaylist(isRestricted: Bool, restrictedTo: String, isVertical: Bool) { debugLog("generate playlist (isVertical: \(isVertical)") // Start fresh playlist = [AerialVideo]() playlistIsRestricted = isRestricted playlistRestrictedTo = restrictedTo playlistHasVerticalVideos = false var shuffled = currentRotation().shuffled() // If we have nothing, just get everything if shuffled.count == 0 { shuffled = everythingRotation().shuffled() } let cachedShuffled = shuffled.filter({ $0.isAvailableOffline }) debugLog("Playlist raw count: \(shuffled.count) raw cached count \(cachedShuffled.count) isRestricted: \(isRestricted) restrictedTo: \(restrictedTo)") if PrefsDisplays.viewingMode == .independent && PrefsAdvanced.favorOrientation { // We check cached videos only as those are the only ones for which we know the orientation for video in cachedShuffled { // swiftlint:disable:next for_where if video.isVertical { playlistHasVerticalVideos = true debugLog(">>> Playlist contains vertical videos (favoring ON)") } } } for video in shuffled { /* // Do we restrict videos by screen orientation ? if restrictOrientation { print(video.url) print(video.isVertical) if !video.isVertical && isVertical { // Block landscape videos on vertical screens continue } else if video.isVertical && !isVertical { // Block portrait videos on horizontal screens continue } }*/ // Do we restrict video types by day/night ? if isRestricted { if video.timeOfDay != restrictedTo { continue } } if !video.isAvailableOffline { continue } // All good ? Add to playlist playlist.append(video) } debugLog("Final count : \(playlist.count)") // On regenerating a new playlist, we try to avoid repeating the last thing we played! while playlist.count > 1 && lastPluckedFromPlaylist == playlist.first { playlist.shuffle() } } func randomVideo(excluding: [AerialVideo], isVertical: Bool) -> (AerialVideo?, Bool) { var shouldLoop = false let timeManagement = TimeManagement.sharedInstance let (shouldRestrictByDayNight, restrictTo) = timeManagement.shouldRestrictPlaybackToDayNightVideo() // Do we still have a video in the correct format in the playlist? var needOrientedVideo = false if playlistHasVerticalVideos && !playlist.isEmpty { needOrientedVideo = true for video in playlist { if isVertical && video.isVertical { needOrientedVideo = false } else if !isVertical && !video.isVertical { needOrientedVideo = false } } } debugLog("remaining in playlist : \(playlist.count) needOrientedVideo : \(needOrientedVideo)") // We may need to regenerate a playlist! if playlist.isEmpty || restrictTo != playlistRestrictedTo || shouldRestrictByDayNight != playlistIsRestricted || needOrientedVideo { generatePlaylist(isRestricted: shouldRestrictByDayNight, restrictedTo: restrictTo, isVertical: isVertical) if playlist.count == 1 { debugLog("playlist only has one element, looping!") shouldLoop = true } } // If not pluck one from current playlist and return that if !playlist.isEmpty { if playlistHasVerticalVideos { lastPluckedFromPlaylist = pluckOrientedVideo(isVertical: isVertical) } else { lastPluckedFromPlaylist = playlist.removeFirst() } return (lastPluckedFromPlaylist, shouldLoop) } else { // If we don't have any playlist, something's got awfully wrong so deal with that! return (findBestEffortVideo(), shouldLoop) } } func pluckOrientedVideo(isVertical: Bool) -> AerialVideo? { // Grab first one corresponding to orientation lastPluckedFromPlaylist = playlist.first(where: { $0.isVertical == isVertical })! debugLog("lastplucked") // And actually remove it debugLog("pre pluck \(playlist.count)") playlist = playlist.filter { $0 != lastPluckedFromPlaylist } debugLog("post pluck \(playlist.count)") return lastPluckedFromPlaylist } // Find a backup plan when conditions are not met func findBestEffortVideo() -> AerialVideo? { // So this is embarassing. This can happen if : // - No video checked // - No video for current conditions (only day video checked, and looking for night) // - We don't want to stream but don't have any video // - We may not have the manifests // At this point we're doing a best effort : // - Did we play something previously ? If so play that back (will loop) // - 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) warnLog("Empty playlist, not good !") if lastPluckedFromPlaylist != nil { warnLog("Repeating last played video, after condition change not met !") return lastPluckedFromPlaylist! } else { // Start with a shuffled list let shuffled = videos.shuffled() if shuffled.isEmpty { // This is super bad, no manifest at all errorLog("No manifest, nothing to play !") return nil } for video in shuffled { // If we find anything cached and in rotation, we send that back if video.isAvailableOffline && currentRotation().contains(video) { warnLog("returning random cached in rotation video after condition change not met !") return video } } // We try to return something that's at least in the rotation, if there is one if !currentRotation().isEmpty { warnLog("returning random non cached BUT in rotation video after condition change not met !") return currentRotation().shuffled().first } // Really nothing ? I can't even ! warnLog("returning truly random video after condition change not met !") return shuffled.first! } } } ================================================ FILE: Aerial/Source/Models/Time/Aerial-Bridging-Header.h ================================================ // // Use this file to import your target's public headers that you would like to expose to Swift. // // We need this to be able to use IOPowerSources for battery status detection in Swift #import ================================================ FILE: Aerial/Source/Models/Time/IOBridge.m ================================================ // // IOBridge.m // Aerial // // Created by Guillaume Louel on 26/10/2018. // Copyright © 2018 John Coates. All rights reserved. // // We need this to be able to use IOPowerSources for battery status detection in Swift #import ================================================ FILE: Aerial/Source/Models/Time/Solar.swift ================================================ // // Solar.swift // SolarExample // // Created by Chris Howell on 16/01/2016. // Copyright © 2016 Chris Howell. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the “Software”), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // // Modifications for Aerial/glouel // 26/10/2018: - added an intermediate mode that's closer to night shift sunset/sunrise times // - added a isDaylight(zenith: Zenith) function import Foundation import CoreLocation public struct Solar { /// The coordinate that is used for the calculation public let coordinate: CLLocationCoordinate2D /// The date to generate sunrise / sunset times for public fileprivate(set) var date: Date public fileprivate(set) var sunrise: Date? public fileprivate(set) var sunset: Date? public fileprivate(set) var civilSunrise: Date? public fileprivate(set) var civilSunset: Date? public fileprivate(set) var strictSunrise: Date? public fileprivate(set) var strictSunset: Date? public fileprivate(set) var nauticalSunrise: Date? public fileprivate(set) var nauticalSunset: Date? public fileprivate(set) var astronomicalSunrise: Date? public fileprivate(set) var astronomicalSunset: Date? // MARK: Init public init?(for date: Date = Date(), coordinate: CLLocationCoordinate2D) { self.date = date guard CLLocationCoordinate2DIsValid(coordinate) else { return nil } self.coordinate = coordinate // Fill this Solar object with relevant data calculate() } // MARK: - Public functions /// Sets all of the Solar object's sunrise / sunset variables, if possible. /// - Note: Can return `nil` objects if sunrise / sunset does not occur on that day. public mutating func calculate() { strictSunrise = calculate(.sunrise, for: date, and: .strict) strictSunset = calculate(.sunset, for: date, and: .strict) sunrise = calculate(.sunrise, for: date, and: .official) sunset = calculate(.sunset, for: date, and: .official) civilSunrise = calculate(.sunrise, for: date, and: .civil) civilSunset = calculate(.sunset, for: date, and: .civil) nauticalSunrise = calculate(.sunrise, for: date, and: .nautical) nauticalSunset = calculate(.sunset, for: date, and: .nautical) astronomicalSunrise = calculate(.sunrise, for: date, and: .astronimical) astronomicalSunset = calculate(.sunset, for: date, and: .astronimical) } // MARK: - Private functions fileprivate enum SunriseSunset { case sunrise case sunset } /// Used for generating several of the possible sunrise / sunset times public enum Zenith: Double { case strict = 90 case official = 90.83 case civil = 96 case nautical = 102 case astronimical = 108 } // swiftlint:disable identifier_name fileprivate func calculate(_ sunriseSunset: SunriseSunset, for date: Date, and zenith: Zenith) -> Date? { guard let utcTimezone = TimeZone(identifier: "UTC") else { return nil } // Get the day of the year var calendar = Calendar(identifier: .gregorian) calendar.timeZone = utcTimezone guard let dayInt = calendar.ordinality(of: .day, in: .year, for: date) else { return nil } let day = Double(dayInt) // Convert longitude to hour value and calculate an approx. time let lngHour = coordinate.longitude / 15 let hourTime: Double = sunriseSunset == .sunrise ? 6 : 18 let t = day + ((hourTime - lngHour) / 24) // Calculate the suns mean anomaly let M = (0.9856 * t) - 3.289 // Calculate the sun's true longitude let subexpression1 = 1.916 * sin(M.degreesToRadians) let subexpression2 = 0.020 * sin(2 * M.degreesToRadians) var L = M + subexpression1 + subexpression2 + 282.634 // Normalise L into [0, 360] range L = normalise(L, withMaximum: 360) // Calculate the Sun's right ascension var RA = atan(0.91764 * tan(L.degreesToRadians)).radiansToDegrees // Normalise RA into [0, 360] range RA = normalise(RA, withMaximum: 360) // Right ascension value needs to be in the same quadrant as L... let Lquadrant = floor(L / 90) * 90 let RAquadrant = floor(RA / 90) * 90 RA += (Lquadrant - RAquadrant) // Convert RA into hours RA /= 15 // Calculate Sun's declination let sinDec = 0.39782 * sin(L.degreesToRadians) let cosDec = cos(asin(sinDec)) // Calculate the Sun's local hour angle let cosH = (cos(zenith.rawValue.degreesToRadians) - (sinDec * sin(coordinate.latitude.degreesToRadians))) / (cosDec * cos(coordinate.latitude.degreesToRadians)) // No sunrise guard cosH < 1 else { return nil } // No sunset guard cosH > -1 else { return nil } // Finish calculating H and convert into hours let tempH = sunriseSunset == .sunrise ? 360 - acos(cosH).radiansToDegrees : acos(cosH).radiansToDegrees let H = tempH / 15.0 // Calculate local mean time of rising let T = H + RA - (0.06571 * t) - 6.622 // Adjust time back to UTC var UT = T - lngHour // Normalise UT into [0, 24] range UT = normalise(UT, withMaximum: 24) // Calculate all of the sunrise's / sunset's date components let hour = floor(UT) let minute = floor((UT - hour) * 60.0) let second = (((UT - hour) * 60) - minute) * 60.0 let shouldBeYesterday = lngHour > 0 && UT > 12 && sunriseSunset == .sunrise let shouldBeTomorrow = lngHour < 0 && UT < 12 && sunriseSunset == .sunset let setDate: Date if shouldBeYesterday { setDate = Date(timeInterval: -(60 * 60 * 24), since: date) } else if shouldBeTomorrow { setDate = Date(timeInterval: (60 * 60 * 24), since: date) } else { setDate = date } var components = calendar.dateComponents([.day, .month, .year], from: setDate) components.hour = Int(hour) components.minute = Int(minute) components.second = Int(second) calendar.timeZone = utcTimezone return calendar.date(from: components) } // swiftlint:enable identifier_name /// Normalises a value between 0 and `maximum`, by adding or subtracting `maximum` fileprivate func normalise(_ value: Double, withMaximum maximum: Double) -> Double { var value = value if value < 0 { value += maximum } if value > maximum { value -= maximum } return value } } extension Solar { /// Whether the location specified by the `latitude` and `longitude` is in daytime on `date` /// - Complexity: O(1) public var isDaytime: Bool { guard let sunrise = sunrise, let sunset = sunset else { return false } let beginningOfDay = sunrise.timeIntervalSince1970 let endOfDay = sunset.timeIntervalSince1970 let currentTime = self.date.timeIntervalSince1970 let isSunriseOrLater = currentTime >= beginningOfDay let isBeforeSunset = currentTime < endOfDay return isSunriseOrLater && isBeforeSunset } /// Whether the location specified by the `latitude` and `longitude` is in nighttime on `date` /// - Complexity: O(1) public var isNighttime: Bool { return !isDaytime } /// Whether the location specified by the `latitude` and `longitude` is in daytime on `date` /// Takes an extra Zenith parameter to handle all cases /// - Complexity: O(1) public func isDaytime(zenith: Zenith) -> Bool { guard let _ = sunrise, let _ = sunset else { return false } var lsunrise, lsunset: Date switch zenith { case .strict: lsunrise = strictSunrise! lsunset = strictSunset! case .civil: lsunrise = civilSunrise! lsunset = civilSunset! case .nautical: lsunrise = nauticalSunrise! lsunset = nauticalSunset! case .astronimical: lsunrise = astronomicalSunrise! lsunset = astronomicalSunset! default: lsunrise = sunrise! lsunset = sunset! } let beginningOfDay = lsunrise.timeIntervalSince1970 let endOfDay = lsunset.timeIntervalSince1970 let currentTime = self.date.timeIntervalSince1970 let isSunriseOrLater = currentTime >= beginningOfDay let isBeforeSunset = currentTime < endOfDay return isSunriseOrLater && isBeforeSunset } public func getTimeSlice() -> String { guard let _ = sunrise, let _ = sunset else { return "" } // We use var lsunrise, lsunset: Date if astronomicalSunset != nil && astronomicalSunrise != nil { lsunrise = astronomicalSunrise! lsunset = astronomicalSunset! } else if nauticalSunset != nil && nauticalSunrise != nil { lsunrise = nauticalSunrise! lsunset = nauticalSunset! } else if civilSunset != nil && civilSunrise != nil { lsunrise = civilSunrise! lsunset = civilSunset! } else { lsunrise = sunrise! lsunset = sunset! } debugLog("lsunrise \(lsunrise) lsunriseEnd \(lsunrise.addingTimeInterval(TimeInterval(PrefsTime.sunEventWindow)))") debugLog("psunset \(lsunset.addingTimeInterval(TimeInterval(-PrefsTime.sunEventWindow))) lsunset \(lsunset)") debugLog("current \(self.date)") if self.date < lsunrise || self.date > lsunset { // So this is night, before sunrise, after sunset debugLog("night") return "night" } else if self.date > lsunrise && self.date < lsunrise.addingTimeInterval(TimeInterval(PrefsTime.sunEventWindow)) { // Sunrise-period is a 3hr period after astro sunrise debugLog("sunrise") return "sunrise" } else if self.date > lsunset.addingTimeInterval(TimeInterval(-PrefsTime.sunEventWindow)) && self.date < lsunset { // Sunset-period is a 3hr period prior astro sunset debugLog("sunset") return "sunset" } else { // Let's say this is day debugLog("day") return "day" } } } // MARK: - Helper extensions private extension Double { var degreesToRadians: Double { return Double(self) * (Double.pi / 180.0) } var radiansToDegrees: Double { return (Double(self) * 180.0) / Double.pi } } ================================================ FILE: Aerial/Source/Models/Time/TimeManagement.swift ================================================ // // TimeManagement.swift // Aerial // // Created by Guillaume Louel on 05/10/2018. // Copyright © 2018 John Coates. All rights reserved. // import Foundation import Cocoa import CoreLocation import IOKit.ps // swiftlint:disable:next type_body_length final class TimeManagement: NSObject { static let sharedInstance = TimeManagement() var solar: Solar? var lsLatitude: Double? var lsLongitude: Double? // MARK: - Lifecycle override init() { super.init() debugLog("Time Management initialized") if PrefsTime.timeMode == .locationService { // This is racy... I think we're ok because time/location gets inited first, but still... let location = Locations.sharedInstance location.getCoordinates(failure: { (_) in errorLog("Location services denied access to your location. Please make sure you allowed ScreenSaverEngine, Aerial, or legacyScreenSaver to access your location in System Preferences > Security and Privacy > Privacy") }, success: { (coordinates) in self.lsLatitude = coordinates.latitude self.lsLongitude = coordinates.longitude debugLog("Location found \(self.lsLatitude ?? 0) \(self.lsLongitude ?? 0)") _ = self.calculateFrom(latitude: self.lsLatitude!, longitude: self.lsLongitude!) }) } else { _ = calculateFromCoordinates() } } // MARK: - What should we play ? // swiftlint:disable:next cyclomatic_complexity func shouldRestrictPlaybackToDayNightVideo() -> (Bool, String) { debugLog("PrefsTime : \(PrefsTime.timeMode)") // We override everything on dark mode if we need to if PrefsTime.darkModeNightOverride && DarkMode.isEnabled() { debugLog("Dark Mode override") return (true, "night") } // If not we check the modes if PrefsTime.timeMode == .locationService { if let lat = lsLatitude, let lon = lsLongitude { _ = calculateFrom(latitude: lat, longitude: lon) if solar != nil { debugLog("Location service : \(solar!.getTimeSlice())") return (true, solar!.getTimeSlice()) } } else { debugLog("No location available, failing timeMode") } return (false, "") } else if PrefsTime.timeMode == .lightDarkMode { debugLog("Light/dark : \(DarkMode.isEnabled() ? "night" : "day")") return (true, DarkMode.isEnabled() ? "night" : "day") } else if PrefsTime.timeMode == .coordinates { _ = calculateFromCoordinates() if solar != nil { debugLog("Coordinates : \(solar!.getTimeSlice())") return (true, solar!.getTimeSlice()) } else { errorLog("You need to input latitude and longitude for calculations to work") return (false, "") } } else if PrefsTime.timeMode == .nightShift { let (isNSCapable, sunrise, sunset, _) = NightShift.getInformation() if !isNSCapable { errorLog("Trying to use Night Shift on a non capable Mac") return (false, "") } debugLog("Night shift : \(dayNightCheck(sunrise: sunrise!, sunset: sunset!))") return (true, dayNightCheck(sunrise: sunrise!, sunset: sunset!)) } else if PrefsTime.timeMode == .manual { // We get the manual values from our preferences, as string, and convert them to dates let dateFormatter = DateFormatter() dateFormatter.dateFormat = "HH:mm" guard let dateSunrise = dateFormatter.date(from: PrefsTime.manualSunrise) else { errorLog("Invalid sunrise time in preferences") return(false, "") } guard let dateSunset = dateFormatter.date(from: PrefsTime.manualSunset) else { errorLog("Invalid sunset time in preferences") return(false, "") } debugLog("Manual : \(dayNightCheck(sunrise: dateSunrise, sunset: dateSunset))") return (true, dayNightCheck(sunrise: dateSunrise, sunset: dateSunset)) } // default is show anything return (false, "") } public func getSunriseSunset() -> (Date?, Date?) { switch PrefsTime.timeMode { case .disabled: return (nil, nil) case .nightShift: let (_, sunrise, sunset, _) = NightShift.getInformation() return (sunrise, sunset) case .manual: let dateFormatter = DateFormatter() dateFormatter.dateFormat = "HH:mm" guard let dateSunrise = dateFormatter.date(from: PrefsTime.manualSunrise) else { errorLog("Invalid sunrise time in preferences") return(nil, nil) } guard let dateSunset = dateFormatter.date(from: PrefsTime.manualSunset) else { errorLog("Invalid sunset time in preferences") return(nil, nil) } return (dateSunrise, dateSunset) case .lightDarkMode: return (nil, nil) case .coordinates: return (nil, nil) case .locationService: if let lat = lsLatitude, let lon = lsLongitude { _ = calculateFrom(latitude: lat, longitude: lon) return (solar?.astronomicalSunrise, solar?.astronomicalSunset) } return(nil, nil) } } // Get the correct Zenith value for our pref private func getZenith(_ mode: SolarMode) -> Solar.Zenith { switch mode { case .strict: return .strict case .official: return .official case .civil: return .civil case .nautical: return .nautical default: return .astronimical } } // Check if we are at day or night based on provided sunrise and sunset dates private func dayNightCheck(sunrise: Date, sunset: Date) -> String { var nsunrise = sunrise var nsunset = sunset let now = Date() // When used with manual mode, sunrise and sunset will always be set to 2000-01-01 // With night mode, sunrise and sunset are the "current" ones (if at 23:00, sunset = today, sunrise = tomorrow) // That may not always be true though, if you mess with your system clock (go back in time), both values // can be in the future (and possibly in the past) // // As a sanity check, we check if we are between a sunset and a sunrise (prefered calculation mode with night // shift as it takes into account everything correctly for us), if not we todayize the dates. In manual mode, // will always be todayized if (now < sunrise && now < sunset) || (now > sunrise && now > sunset) { nsunrise = todayizeDate(date: sunrise)! nsunset = todayizeDate(date: sunset)! } if now < nsunrise || now > nsunset { // So this is night, before sunrise, after sunset debugLog("night") return "night" } else if now > nsunrise && now < nsunrise.addingTimeInterval(TimeInterval(PrefsTime.sunEventWindow)) { // Sunrise-period is a 3hr period after astro sunrise debugLog("sunrise") return "sunrise" } else if now > nsunset.addingTimeInterval(TimeInterval(-PrefsTime.sunEventWindow)) && now < nsunset { // Sunset-period is a 3hr period prior astro sunset debugLog("sunset") return "sunset" } else { // Let's say this is day debugLog("day") return "day" } } // Change a date's day to today private func todayizeDate(date: Date) -> Date? { // Get today's date as a string let dateFormatter = DateFormatter() let current = Date() dateFormatter.dateFormat = "yyyy-MM-dd" let today = dateFormatter.string(from: current) // Extract hour from date dateFormatter.dateFormat = "HH:mm:ss +zzzz" let format = today + " " + dateFormatter.string(from: date) // Now return the todayized string dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss +zzzz" if let newdate = dateFormatter.date(from: format) { return newdate } else { return nil } } // MARK: Calculate using Solar func calculateFromCoordinates() -> (Bool, String) { if PrefsTime.timeMode == .locationService { // This is racy... I think we're ok because time/location gets inited first, but still... let location = Locations.sharedInstance location.getCoordinates(failure: { (_) in errorLog("Location services denied access to your location. Please make sure you allowed ScreenSaverEngine, Aerial, or legacyScreenSaver to access your location in System Preferences > Security and Privacy > Privacy") }, success: { (coordinates) in self.lsLatitude = coordinates.latitude self.lsLongitude = coordinates.longitude _ = self.calculateFrom(latitude: self.lsLatitude!, longitude: self.lsLongitude!) }) } else { if PrefsTime.latitude != "" && PrefsTime.longitude != "" { return calculateFrom(latitude: Double(PrefsTime.latitude) ?? 0, longitude: Double(PrefsTime.longitude) ?? 0) } } return (false, "Can't process your coordinates, please verify") } private func calculateFrom(latitude: Double, longitude: Double) -> (Bool, String) { solar = Solar.init(coordinate: CLLocationCoordinate2D( latitude: latitude, longitude: longitude)) if solar != nil { let dateFormatter = DateFormatter() dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "j:mm:ss", options: 0, locale: Locale.current) let (sunrise, sunset) = getSunriseSunsetForMode(PrefsTime.solarMode) if sunrise == nil || sunset == nil { return (false, "Can't process your coordinates, please verify") } let sunriseString = dateFormatter.string(from: sunrise!) let sunsetString = dateFormatter.string(from: sunset!) if PrefsTime.solarMode == .official || PrefsTime.solarMode == .strict { return(true, "Today’s sunrise: " + sunriseString + " Today’s sunset: " + sunsetString) } else { return(true, "Today’s dawn: " + sunriseString + " Today’s dusk: " + sunsetString) } } return (false, "Can't process your coordinates, please verify") } // Helper to get the correct sunrise/sunset func getSunriseSunsetForMode(_ mode: SolarMode) -> (Date?, Date?) { if let sol = solar { switch mode { case .official: return (sol.sunrise, sol.sunset) case .strict: return (sol.strictSunrise, sol.strictSunset) case .civil: return (sol.civilSunrise, sol.civilSunset) case .nautical: return (sol.nauticalSunrise, sol.nauticalSunset) default: return (sol.astronomicalSunrise, sol.astronomicalSunset) } } return (nil, nil) } // MARK: - Brightness stuff (early, may get moved/will change) func getCurrentSleepTime() -> Int { // pmset -g | grep "^[ ]*sleep" | awk '{ print $2 }' let pipe1 = Pipe() let pmset = Process() pmset.launchPath = "/usr/bin/env" pmset.arguments = ["pmset", "-g"] pmset.standardOutput = pipe1 let pipe2 = Pipe() let grep = Process() grep.launchPath = "/usr/bin/env" grep.arguments = ["grep", "^[ ]*sleep"] grep.standardInput = pipe1 grep.standardOutput = pipe2 let pipeOut = Pipe() let awk = Process() awk.launchPath = "/usr/bin/env" awk.arguments = ["awk", "{ print $2 }"] awk.standardInput = pipe2 awk.standardOutput = pipeOut awk.standardOutput = pipeOut pmset.launch() grep.launch() awk.launch() awk.waitUntilExit() let data = pipeOut.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) if output != nil { let lines = output!.split(separator: "\n") if lines.count == 1 { let newline = Int(lines[0]) if let newLineIndex = newline { return newLineIndex } } } return 0 } /* // MARK: - Location detection func startLocationDetection() { let locationManager = CLLocationManager() locationManager.delegate = self if CLLocationManager.locationServicesEnabled() { debugLog("Location services enabled") locationManager.startUpdatingLocation() } else { errorLog("Location services are disabled, please check your macOS settings!") } if #available(OSX 10.14, *) { locationManager.requestLocation() } else { // Fallback on earlier versions } }*/ } /* // MARK: - Core Location Delegates extension TimeManagement: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { _ = locations[locations.count - 1] } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { errorLog("Location Manager error : \(error)") } }*/ ================================================ FILE: Aerial/Source/Views/AerialPlayerItem.swift ================================================ // // AerialPlayerItem.swift // Aerial // // Created by Ethan Setnik on 11/22/17. // Copyright © 2017 John Coates. All rights reserved. // import AVFoundation import AVKit final class AerialPlayerItem: AVPlayerItem { var video: AerialVideo? init(video: AerialVideo) { let videoURL = video.url let asset = cachedOrCachingAsset(videoURL) super.init(asset: asset, automaticallyLoadedAssetKeys: nil) self.video = video } } ================================================ FILE: Aerial/Source/Views/AerialView+Brightness.swift ================================================ // // AerialView+Brightness.swift // Aerial // // Created by Guillaume Louel on 06/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Foundation extension AerialView { // We make sure we should dim, we're not a preview, we haven't dimmed yet (multi monitor) // and ensure we properly apply the night/battery restrictions ! func checkIfShouldSetBrightness() { let timeManagement = TimeManagement.sharedInstance if PrefsDisplays.dimBrightness && !isPreview && brightnessToRestore == nil { let (should, to) = timeManagement.shouldRestrictPlaybackToDayNightVideo() if !PrefsDisplays.dimOnlyAtNight || (PrefsDisplays.dimOnlyAtNight && should && to == "night") { if !PrefsDisplays.dimOnlyOnBattery || (PrefsDisplays.dimOnlyOnBattery && Battery.isUnplugged()) { brightnessToRestore = Brightness.get() // brightnessToRestore = timeManagement.getBrightness() debugLog("Brightness before Aerial was launched : \(String(describing: brightnessToRestore))") Brightness.set(level: min(Float(PrefsDisplays.startDim), brightnessToRestore!)) setDimTimers() } } } } // Set the timers to progressively dim the screen brightness (in 10 steps) // Currently, this only works with internal monitors func setDimTimers() { if #available(OSX 10.12, *) { let timeManagement = TimeManagement.sharedInstance let startValue = min(PrefsDisplays.startDim, Double(brightnessToRestore!)) if PrefsDisplays.dimBrightness && startValue > PrefsDisplays.endDim { debugLog("setting brightness timers from \(String(describing: startValue)) to \(String(describing: PrefsDisplays.endDim))") var interval: Int if PrefsDisplays.overrideDimInMinutes { interval = PrefsDisplays.dimInMinutes * 6 // * 60 / 10, we make 10 intermediate steps } else { interval = timeManagement.getCurrentSleepTime() * 6 if interval == 0 { interval = 180 // Fallback to 30 mins if no sleep } } debugLog("Step size: \(interval) seconds") for idx in 1...10 { _ = Timer.scheduledTimer(withTimeInterval: TimeInterval(interval * idx), repeats: false) { (_) in let val = startValue - ((startValue - PrefsDisplays.endDim) / 10 * Double(idx)) debugLog("Firing event \(idx) brightness to \(val)") Brightness.set(level: Float(val)) } } } } else { // Fallback on earlier versions warnLog("Brightness control not available < macOS 10.12") } } } ================================================ FILE: Aerial/Source/Views/AerialView+Player.swift ================================================ // // AerialView+Player.swift // Aerial // // Created by Guillaume Louel on 06/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Foundation import AVFoundation import AVKit extension AerialView { func setupPlayerLayer(withPlayer player: AVPlayer) { let displayDetection = DisplayDetection.sharedInstance 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 debugLog("\(self.description) setting up player layer with bounds/frame: \(layer.bounds) / \(layer.frame)") playerLayer = AVPlayerLayer(player: player) // Fill/fit is only available in 10.10+ if #available(OSX 10.10, *) { if PrefsDisplays.aspectMode == .fill { playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill } else { playerLayer.videoGravity = AVLayerVideoGravity.resizeAspect } } playerLayer.autoresizingMask = [CAAutoresizingMask.layerWidthSizable, CAAutoresizingMask.layerHeightSizable] // In case of span mode we need to compute the size of our layer if PrefsDisplays.viewingMode == .spanned && !isPreview { let zRect = displayDetection.getZeroedActiveSpannedRect() debugLog("foundScreen check : \(foundScreen.debugDescription)") if let scr = foundScreen { let tRect = CGRect(x: zRect.origin.x - scr.zeroedOrigin.x, y: zRect.origin.y - scr.zeroedOrigin.y, width: zRect.width, height: zRect.height) debugLog("tRect : \(tRect)") playerLayer.frame = tRect } else { debugLog("This is an unknown screen in span mode, workarounding...") if let alternateScreen = DisplayDetection.sharedInstance.alternateFindScreenWith(frame: self.frame) { foundScreen = alternateScreen debugLog("📺 alternate screen found : \(alternateScreen.description)") let tRect = CGRect(x: zRect.origin.x - alternateScreen.zeroedOrigin.x, y: zRect.origin.y - alternateScreen.zeroedOrigin.y, width: zRect.width, height: zRect.height) playerLayer.frame = tRect } else { errorLog("No alternate screen found, reverting to single screen mode") playerLayer.frame = layer.bounds } } } else { playerLayer.frame = layer.bounds // "true" mirrored mode let index = AerialView.instanciatedViews.firstIndex(of: self) ?? 0 if index % 2 == 1 && PrefsDisplays.viewingMode == .mirrored { playerLayer.transform = CATransform3DMakeAffineTransform(CGAffineTransform(scaleX: -1, y: 1)) } } layer.addSublayer(playerLayer) layer.contentsScale = (self.window?.backingScaleFactor) ?? 1.0 self.playerLayer.contentsScale = (self.window?.backingScaleFactor) ?? 1.0 // The layers for descriptions, clock, message // On Sonoma we can't use the reported frame! if foundFrame != nil { layerManager.setupExtraLayers(layer: layer, frame: foundFrame!) } else { layerManager.setupExtraLayers(layer: layer, frame: self.frame) } // Make sure we set the retinaness here layerManager.setContentScale(scale: self.window?.backingScaleFactor ?? 1.0) // An extra layer to try and contravent a macOS graphics driver bug // This is useful on High Sierra+ on Intel Macs if #available(macOS 12.0, *) { } else { setupGlitchWorkaroundLayer(layer: layer) } } // MARK: - AVPlayerItem Notifications @objc func playerItemFailedtoPlayToEnd(_ aNotification: Notification) { warnLog("\(self.description) AVPlayerItemFailedToPlayToEndTimeNotification \(aNotification)") playNextVideo() } @objc func playerItemNewErrorLogEntryNotification(_ aNotification: Notification) { warnLog("\(self.description) AVPlayerItemNewErrorLogEntryNotification \(aNotification)") } @objc func playerItemPlaybackStalledNotification(_ aNotification: Notification) { warnLog("\(self.description) AVPlayerItemPlaybackStalledNotification \(aNotification)") } @objc func playerItemDidReachEnd(_ aNotification: Notification) { debugLog("\(self.description) played did reach end") debugLog("\(self.description) notification: \(aNotification)") if shouldLoop { debugLog("Rewinding video!") if let playerItem = aNotification.object as? AVPlayerItem { playerItem.seek(to: CMTime.zero, completionHandler: nil) } } else { playNextVideo() debugLog("\(self.description) playing next video for player \(String(describing: player))") } } // Video fade-in/out func addPlayerFades(view: AerialView, player: AVPlayer, video: AerialVideo) { if !Aerial.helper.underCompanion { // We only fade in/out if we have duration if video.duration > 0 && AerialView.shouldFade && !shouldLoop { let playbackSpeed = Double(PlaybackSpeed.forVideo(video.id)) view.playerLayer.opacity = 0 let fadeAnimation = CAKeyframeAnimation(keyPath: "opacity") fadeAnimation.values = [0, 1, 1, 0] as [Int] fadeAnimation.keyTimes = [0, AerialView.fadeDuration/(video.duration/playbackSpeed), 1-(AerialView.fadeDuration/(video.duration/playbackSpeed)), 1 ] as [NSNumber] fadeAnimation.duration = video.duration/playbackSpeed if #available(macOS 10.14, *) { fadeAnimation.calculationMode = CAAnimationCalculationMode.cubic } else { // Fallback on earlier versions } view.playerLayer.add(fadeAnimation, forKey: "mainfade") } else { view.playerLayer.opacity = 1.0 } } else { view.playerLayer.opacity = 1.0 } } func removePlayerFades() { self.playerLayer.removeAllAnimations() self.playerLayer.opacity = 1.0 } // This works pre Catalina as of right now func setupGlitchWorkaroundLayer(layer: CALayer) { debugLog("Using dot workaround for video driver corruption") let workaroundLayer = CATextLayer() workaroundLayer.frame = self.bounds workaroundLayer.opacity = 1.0 workaroundLayer.font = NSFont(name: "Helvetica Neue Medium", size: 4) workaroundLayer.fontSize = 4 workaroundLayer.string = "." let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: workaroundLayer.font as Any] // Calculate bounding box let attrString = NSAttributedString(string: workaroundLayer.string as! String, attributes: attributes) let rect = attrString.boundingRect(with: layer.visibleRect.size, options: NSString.DrawingOptions.usesLineFragmentOrigin) workaroundLayer.frame = rect workaroundLayer.position = CGPoint(x: 2, y: 2) workaroundLayer.anchorPoint = CGPoint(x: 0, y: 0) layer.addSublayer(workaroundLayer) } } ================================================ FILE: Aerial/Source/Views/AerialView.swift ================================================ // // AerialView.swift // Aerial // // Created by John Coates on 10/22/15. // Copyright © 2015 John Coates. All rights reserved. // import Foundation import ScreenSaver import AVFoundation import AVKit @objc(AerialView) // swiftlint:disable:next type_body_length final class AerialView: ScreenSaverView, CAAnimationDelegate { var layerManager: LayerManager var playerLayer: AVPlayerLayer! static var players: [AVPlayer] = [AVPlayer]() static var previewPlayer: AVPlayer? static var previewView: AerialView? var player: AVPlayer? var currentVideo: AerialVideo? var preferencesController: PanelWindowController? var observerWasSet = false var hasStartedPlaying = false var wasStopped = false var isDisabled = false var isQuickFading = false var brightnessToRestore: Float? var globalSpeed: Float = 1.0 var globalPause = false // We use this for tentative Catalina bug workaround var originalWidth, originalHeight: CGFloat // We use this for tentative Sonoma bug workaround var foundScreen: Screen? var foundFrame: NSRect? // Tentative improvement when only one video in playlist var shouldLoop = false static var shouldFade: Bool { return (PrefsVideos.fadeMode != .disabled) } static var fadeDuration: Double { switch PrefsVideos.fadeMode { case .t0_5: return 0.5 case .t1: return 1 case .t2: return 2 default: return 0.10 } } static var textFadeDuration: Double { switch PrefsInfo.fadeModeText { case .t0_5: return 0.5 case .t1: return 1 case .t2: return 2 default: return 0.10 } } // Mirrored/cloned viewing mode and Spanned viewing mode share the same player for sync & ressource saving static var sharingPlayers: Bool { switch PrefsDisplays.viewingMode { case .cloned, .mirrored, .spanned: return true default: return false } } static var sharedViews: [AerialView] = [] // Because of lifecycle in Preview, we may pile up old/no longer // shared instanciated views that we need to track to not reuse static var instanciatedViews: [AerialView] = [] // MARK: - Shared Player static var singlePlayerAlreadySetup: Bool = false static var sharedPlayerIndex: Int? static var didSkipMain: Bool = false class var sharedPlayer: AVPlayer { struct Static { static let instance: AVPlayer = AVPlayer() // swiftlint:disable:next identifier_name static var _player: AVPlayer? static var player: AVPlayer { if let activePlayer = _player { return activePlayer } _player = AVPlayer() return _player! } } return Static.player } // MARK: - Init / Setup // This is the one used by System Preferences/ScreenSaverEngine override init?(frame: NSRect, isPreview: Bool) { Aerial.helper.checkCompanion() // Clear log if > 1MB on startup rollLogIfNeeded() // Set Companion bridge notifications under Sonoma, but not under Companion if !Aerial.helper.underCompanion { if #available(macOS 14, *) { CompanionBridge.setNotifications() } } // legacyScreenSaver always return true for isPreview on Catalina // We need to detect and override ourselves // This is finally fixed in Ventura var preview = false self.originalWidth = frame.width self.originalHeight = frame.height if frame.width < 400 && frame.height < 300 { preview = true } // This is where we manage our location info layers, clock, etc self.layerManager = LayerManager(isPreview: preview) super.init(frame: frame, isPreview: preview) debugLog("🖼️ AVinit (.saver) \(frame) p: \(isPreview) o: \(preview)") self.animationTimeInterval = 1.0 / 30.0 if Aerial.helper.underCompanion && isPreview { debugLog("Running under companion in preview mode, preventing setup") } else { // We need to delay things under Sonoma because legacyScreenSaver is awesome if #available(macOS 14.0, *) { var delay = 0.01 // If nightshift we delay more if !Aerial.helper.underCompanion && PrefsTime.timeMode == .nightShift { delay = 0.5 } DispatchQueue.main.asyncAfter(deadline: .now() + delay) { debugLog("🖼️ AVinit delayed setup!") self.setup() } } else { setup() } } } // This is the one used by our App target used for debugging required init?(coder: NSCoder) { Aerial.helper.appMode = true Aerial.helper.checkCompanion() // Clear log if > 1MB on startup rollLogIfNeeded() // Set Companion bridge notifications under Sonoma, but not under Companion if !Aerial.helper.underCompanion { if #available(macOS 14, *) { CompanionBridge.setNotifications() } } self.layerManager = LayerManager(isPreview: false) // ... self.originalWidth = 0 self.originalHeight = 0 super.init(coder: coder) self.originalWidth = frame.width self.originalHeight = frame.height debugLog("🖼️ AVinit .app") // We need to delay things under Sonoma because legacyScreenSaver is awesome if #available(macOS 14.0, *) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { debugLog("🖼️ AVinit delayed setup!") self.setup() } } else { setup() } } deinit { Aerial.helper.maybeUnmuteSound() debugLog("🖼️ \(self.description) AVdeinit ") NotificationCenter.default.removeObserver(self) } func ensureCorrectFormat() { if #available(OSX 10.15, *) { } else { // No HDR allowed here if PrefsVideos.videoFormat == .v4KHDR { debugLog("🖼️⚠️ Fixing 4K HDR not allowed prior to Catalina") PrefsVideos.videoFormat = .v4KHEVC } else if PrefsVideos.videoFormat == .v1080pHDR { debugLog("🖼️⚠️ Fixing 1080p HDR not allowed prior to Catalina") PrefsVideos.videoFormat = .v1080pHEVC } } } // swiftlint:disable:next cyclomatic_complexity func setup() { // Disable HDR only on macOS Ventura if !Aerial.helper.canHDR() { if isPreview && (PrefsVideos.videoFormat == .v4KHDR || PrefsVideos.videoFormat == .v1080pHDR) { // This will lead to crashing in up to Ventura beta5 so disable let debugTextView = NSTextView(frame: bounds.insetBy(dx: 20, dy: 20)) debugTextView.font = .labelFont(ofSize: 10) debugTextView.string += "HDR Previews hidden on Ventura" isDisabled = true self.addSubview(debugTextView) return } } // First we check the system appearance, as it relies on our view Aerial.helper.computeDarkMode(view: self) // Then check if we need to mute/unmute sound Aerial.helper.maybeMuteSound() // Kick up the timezone detection _ = TimeManagement.sharedInstance // This is to make sure we don't start in a format that's unsupported ensureCorrectFormat() if let version = Bundle(identifier: "com.JohnCoates.Aerial")?.infoDictionary?["CFBundleShortVersionString"] as? String { debugLog("🖼️ \(self.description) AV setup init (V\(version)) preview: \(self.isPreview)") debugLog("🖼️ Running \(ProcessInfo.processInfo.operatingSystemVersionString)") } // First thing, we may need to migrate the cache ! Cache.migrate() // Now we need to check if we should remove lingering stuff from the cache ! if Cache.canNetwork() { Cache.removeCruft() } // Check early if we need to enable power saver mode, // black screen with minimal brightness if !isPreview { if (PrefsVideos.onBatteryMode == .alwaysDisabled && Battery.isUnplugged()) || (PrefsVideos.onBatteryMode == .disableOnLow && Battery.isLow()) { debugLog("🖼️ Engaging power saving mode") isDisabled = true Brightness.set(level: 0.0) return } } // We may need to set timers to progressively dim the screen checkIfShouldSetBrightness() // Shared views can get stuck, we may need to clean them up here cleanupSharedViews() // We look for the screen in our detected list. // In case of preview or unknown screen result will be nil let displayDetection = DisplayDetection.sharedInstance let screenCount = displayDetection.getScreenCount() debugLog("🖼️ Real screen count : \(screenCount)") var thisScreen: Screen? = nil if #available(macOS 14.0, *) { if foundScreen == nil { debugLog("🖼️ missing foundScreen, workarounding \(String(describing: self.window?.screen))") if let missingScreen = self.window?.screen { debugLog("🖼️ screen attached") matchScreen(thisScreen: missingScreen) } else { errorLog("🖼️ still missing screen") } } else { debugLog("🖼️ early foundScreen ok \(String(describing: foundScreen))") } } else { thisScreen = displayDetection.findScreenWith(frame: self.frame) } // We note the foundFrame as this is more accurate than the reported one! We need this for coordinates mapping if let thisScreen = thisScreen { foundFrame = thisScreen.bottomLeftFrame foundScreen = thisScreen debugLog("🖼️ Using : \(String(describing: thisScreen))") } for twindow in NSApplication.shared.windows { debugLog("window : \(twindow.debugDescription)") } var localPlayer: AVPlayer? // Is the current screen disabled by user ? if !isPreview { // If it's an unknown screen, we leave it enabled if let screen = foundScreen { if !displayDetection.isScreenActive(id: screen.id) { // Then we disable and exit debugLog("🖼️ This display is not active, disabling") isDisabled = true return } else { debugLog("Screen is active") } } } else { AerialView.previewView = self } // Track which views are sharing the sharedPlayer if AerialView.sharingPlayers { AerialView.sharedViews.append(self) } // We track all instanciated views here, independand of their shared status AerialView.instanciatedViews.append(self) // Setup the AVPlayer if AerialView.sharingPlayers { localPlayer = AerialView.sharedPlayer } else { localPlayer = AVPlayer() } guard let player = localPlayer else { errorLog("\(self.description) Couldn't create AVPlayer!") return } self.player = player if isPreview { AerialView.previewPlayer = player } else if !AerialView.sharingPlayers { // add to player list AerialView.players.append(player) } setupPlayerLayer(withPlayer: player) // In mirror mode we use the main instance player if AerialView.sharingPlayers && AerialView.singlePlayerAlreadySetup { if let index = AerialView.sharedPlayerIndex { self.playerLayer.player = AerialView.instanciatedViews[index].player self.playerLayer.opacity = 0 return } } // We're never sharing the preview ! if !isPreview { AerialView.singlePlayerAlreadySetup = true AerialView.sharedPlayerIndex = AerialView.instanciatedViews.count-1 } // So first we wait for our list to be ready VideoList.instance.addCallback { // Then we may need to delay things a bit if we haven't gathered the coordinates yet if PrefsTime.timeMode == .locationService && Locations.sharedInstance.coordinates == nil { debugLog("🖼️⚠️ No coordinates yet, delaying a bit...") DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { self.playNextVideo() } } else { self.playNextVideo() } } } override func viewDidMoveToWindow() { super.viewDidMoveToWindow() if foundScreen == nil { debugLog("🖼️ \(self.description) viewDidMoveToWindow frame: \(self.frame) window: \(String(describing: self.window))") debugLog(self.window?.screen.debugDescription ?? "Unknown") if let thisScreen = self.window?.screen { matchScreen(thisScreen: thisScreen) } else { // For some reason we may not have a screen here! debugLog("🖼️ no screen attached, will try again later") } } else { debugLog("🖼️ wdmtw after we already have a screen, ignoring") } } func matchScreen(thisScreen: NSScreen) { let screenID = thisScreen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! CGDirectDisplayID debugLog(screenID.description) foundScreen = DisplayDetection.sharedInstance.findScreenWith(id: screenID) if let foundScreen = foundScreen { foundFrame = foundScreen.bottomLeftFrame if #available(macOS 14, *) { self.frame = foundFrame! // remove it from the list of unused screens DisplayDetection.sharedInstance.markScreenAsUsed(id: screenID) } } debugLog("🖼️🌾 Using : \(String(describing: foundScreen))") debugLog("🥬🌾 window.screen \(String(describing: self.window?.screen.debugDescription))") debugLog("🖼️🌾 self.frame : \(String(describing: self.frame))") } // Handle window resize override func viewDidEndLiveResize() { layerManager.redrawAllCorners() } override func viewDidChangeBackingProperties() { debugLog("🖼️ \(self.description) backing change \((self.window?.backingScaleFactor) ?? 1.0) isDisabled: \(isDisabled) frame: \(self.frame) preview: \(self.isPreview)") // Tentative workaround for a Catalina+ bug if self.frame.width < 300 && !isPreview { debugLog("🖼️☢️ Frame size bug, trying to override to \(originalWidth)x\(originalHeight)!") self.frame = CGRect(x: 0, y: 0, width: originalWidth, height: originalHeight) } if !isDisabled { if let layer = layer, let window = self.window { layer.contentsScale = (window.backingScaleFactor) ?? 1.0 self.playerLayer.contentsScale = (window.backingScaleFactor) ?? 1.0 // And our additional layers layerManager.setContentScale(scale: (window.backingScaleFactor) ?? 1.0) } } } // On previews, it's possible that our shared player was stopped and is not reusable func cleanupSharedViews() { if AerialView.singlePlayerAlreadySetup { if let index = AerialView.sharedPlayerIndex { if AerialView.instanciatedViews[index].wasStopped { AerialView.singlePlayerAlreadySetup = false AerialView.sharedPlayerIndex = nil AerialView.instanciatedViews = [AerialView]() // Clear the list of instanciated stuff AerialView.sharedViews = [AerialView]() // And the list of sharedViews } } } } // MARK: - Lifecycle stuff override func startAnimation() { super.startAnimation() debugLog("🖼️ \(self.description) startAnimation frame \(self.frame) bounds \(self.bounds)") if !isDisabled { // Previews may be restarted, but our layer will get hidden (somehow) so show it back if isPreview && player?.currentTime() != CMTime.zero { debugLog("restarting playback") playerLayer.opacity = 1 player?.play() } } } override func stopAnimation() { Aerial.helper.maybeUnmuteSound() wasStopped = true debugLog("🖼️ \(self.description) stopAnimation") if !isDisabled { player?.pause() player?.rate = 0 layerManager.removeAllLayers() playerLayer.removeAllAnimations() player?.replaceCurrentItem(with: nil) isDisabled = true } if PrefsDisplays.dimBrightness { if !isPreview, let brightnessToRestore = brightnessToRestore { Brightness.set(level: brightnessToRestore) self.brightnessToRestore = nil } } teardown() } func teardown() { debugLog("🖼️ \(self.description) teardown") // Remove notifications observer debugLog("🖼️ \(self.description) clear notif") //clearNotifications() // tmptest // Clear layer animations debugLog("🖼️ \(self.description) clear anims") clearAllLayerAnimations() if let player = player { // Remove from player index let indexMaybe = AerialView.players.firstIndex(of: player) guard let index = indexMaybe else { return } AerialView.players.remove(at: index) } // Remove any download VideoManager.sharedInstance.cancelAll() debugLog("🖼️ end teardown, exiting") } // Wait for the player to be ready // swiftlint:disable:next block_based_kvo internal override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { debugLog("🖼️ \(description) observeValue \(String(describing: keyPath)) \(playerLayer.isReadyForDisplay)") if let player = player, let currentVideo = currentVideo, playerLayer.isReadyForDisplay { player.play() hasStartedPlaying = true if Aerial.helper.underCompanion { player.rate = globalSpeed } else { player.rate = PlaybackSpeed.forVideo(currentVideo.id) } debugLog("🖼️ start playback: \(frame) \(bounds) rate: \(player.rate)") debugLog("🥬🥬 window2 \(String(describing: window?.screen))") // If we share a player, we need to add the fades and the text to all the // instanciated views using it (eg: in mirrored mode) if AerialView.sharingPlayers { for view in AerialView.sharedViews { self.addPlayerFades(view: view, player: player, video: currentVideo) if (Aerial.helper.underCompanion && PrefsInfo.hideUnderCompanion) { debugLog("🖼️ Disable overlays under Companion") } else { view.layerManager.setupLayersForVideo(video: currentVideo, player: player) } } } else { self.addPlayerFades(view: self, player: player, video: currentVideo) if (Aerial.helper.underCompanion && PrefsInfo.hideUnderCompanion) { debugLog("🖼️ Disable overlays under Companion") } else { self.layerManager.setupLayersForVideo(video: currentVideo, player: player) } } } } // Remove all the layer animations on all shared views func clearAllLayerAnimations() { // Clear everything if let player = player { layerManager.clearLayerAnimations(player: player) for view in AerialView.sharedViews { view.layerManager.clearLayerAnimations(player: player) } } } func clearNotifications() { NotificationCenter.default.removeObserver(self) DistributedNotificationCenter.default.removeObserver(self) } func setNotifications(_ currentItem: AVPlayerItem) { let notificationCenter = NotificationCenter.default notificationCenter.addObserver(self, selector: #selector(AerialView.playerItemDidReachEnd(_:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: currentItem) notificationCenter.addObserver(self, selector: #selector(AerialView.playerItemNewErrorLogEntryNotification(_:)), name: NSNotification.Name.AVPlayerItemNewErrorLogEntry, object: currentItem) notificationCenter.addObserver(self, selector: #selector(AerialView.playerItemFailedtoPlayToEnd(_:)), name: NSNotification.Name.AVPlayerItemFailedToPlayToEndTime, object: currentItem) notificationCenter.addObserver(self, selector: #selector(AerialView.playerItemPlaybackStalledNotification(_:)), name: NSNotification.Name.AVPlayerItemPlaybackStalled, object: currentItem) NSWorkspace.shared.notificationCenter.addObserver( self, selector: #selector(onSleepNote(note:)), name: NSWorkspace.willSleepNotification, object: nil) DistributedNotificationCenter.default.addObserver(self, selector: #selector(AerialView.willStart(_:)), name: Notification.Name("com.apple.screensaver.willstart"), object: nil) DistributedNotificationCenter.default.addObserver(self, selector: #selector(AerialView.willStop(_:)), name: Notification.Name("com.apple.screensaver.willstop"), object: nil) /*DistributedNotificationCenter.default.addObserver(self, selector: #selector(AerialView.screenIsUnlocked(_:)), name: Notification.Name("com.apple.screenIsUnlocked"), object: nil) */ Music.instance.setup() } func sendNotification(video: AerialVideo) { DistributedNotificationCenter.default.post(name: Notification.Name("com.glouel.aerial.nextvideo"), object: "aerialtest : " + video.name) } @objc func willStart(_ aNotification: Notification) { if Aerial.helper.underCompanion { debugLog("🖼️ 📢📢📢 willStart") player?.pause() } } @objc func screenIsUnlocked(_ aNotification: Notification) { if #available(macOS 14.0, *) { debugLog("🖼️ 📢📢📢 ☢️sonoma☢️ workaround screenIsUnlocked") if !Aerial.helper.underCompanion { if let player = player { layerManager.removeAllLayers() player.pause() } self.stopAnimation() } else { if !globalPause { player?.play() player?.rate = globalSpeed } } } } @objc func onSleepNote(note: Notification) { debugLog("🖼️ 📢📢📢 onSleepNote") if !Aerial.helper.underCompanion { if #available(macOS 14.0, *) { exit(0) } } } @objc func willStop(_ aNotification: Notification) { DisplayDetection.sharedInstance.resetUnusedScreens() /* if #available(macOS 14.0, *) { debugLog("🖼️ 📢📢📢 🖼️ 📢📢📢 ☢️sonoma☢️ workaround IGNORING willStop") } else {*/ debugLog("🖼️ 📢📢📢 willStop") if !Aerial.helper.underCompanion { if let player = player { player.pause() } if #available(macOS 14.0, *) { debugLog("🖼️ ⏱️ Setting up 2-second delayed exit") DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { debugLog("🖼️ 🚪 Exiting application now") exit(0) } } self.stopAnimation() } else { if !globalPause { player?.play() player?.rate = globalSpeed } } //} } // Tentative integration with companion of extra features @objc func togglePause() { debugLog("🖼️ Toggling pause") if player?.rate == 0 { player?.play() player?.rate = globalSpeed globalPause = false } else { player?.pause() globalPause = true } removePlayerFades() } @objc func nextVideo() { debugLog("🖼️ Next video") fastFadeOut(andPlayNext: true) } @objc func skipAndHide() { guard let currentVideo = currentVideo else { errorLog("skipAndHide, no currentVideo") return } debugLog("🖼️ Skip video and hide") PrefsVideos.hidden.append(currentVideo.id) fastFadeOut(andPlayNext: true) } @objc func getGlobalSpeed() -> Float { guard let player = player else { errorLog("getGlobalSpeed, no player") return 0 } debugLog("🖼️ Current global speed : " + String(globalSpeed)) return player.rate } @objc func setGlobalSpeed(_ speed : Float) { debugLog("🖼️ Setting speed to : " + String(speed)) globalSpeed = speed // Apply now if playing if let player = player { if (player.rate != 0) { player.rate = globalSpeed } } } // MARK: - playNextVideo() // swiftlint:disable:next cyclomatic_complexity func playNextVideo() { debugLog("🖼️ \(self) pnv") clearAllLayerAnimations() clearNotifications() // play another video let player = AVPlayer() let oldPlayer = self.player self.player = player player.isMuted = PrefsAdvanced.muteSound self.playerLayer.player = self.player self.playerLayer.opacity = AerialView.shouldFade ? 0 : 1.0 if self.isPreview { AerialView.previewPlayer = player } debugLog("🖼️ \(self.description) Setting player for all player layers in \(AerialView.sharedViews)") for view in AerialView.sharedViews { view.playerLayer.player = player } if oldPlayer == AerialView.previewPlayer { AerialView.previewView?.playerLayer.player = self.player } playerLayer.drawsAsynchronously = true // get a list of current videos that should be excluded from the candidate selection // for the next video. This prevents the same video from being shown twice in a row // as well as the same video being shown on two different monitors even when sharingPlayers // is false let currentVideos: [AerialVideo] = AerialView.players.compactMap { (player) -> AerialVideo? in (player.currentItem as? AerialPlayerItem)?.video } let (randomVideo, pshouldLoop) = VideoList.instance.randomVideo(excluding: currentVideos, isVertical: isScreenVertical()) // If we only have one video in the playlist, we can rewind it for seamless transitions self.shouldLoop = pshouldLoop guard let video = randomVideo else { errorLog("\(self.description) Error grabbing random video!") return } self.currentVideo = video // Workaround to avoid local playback making network calls let item = AerialPlayerItem(video: video) if !video.isAvailableOffline { if let value = PrefsVideos.vibrance[video.id], !video.isHDR() { item.setVibrance(value) } if PrefsAdvanced.invertColors { item.setColorInvert() } player.replaceCurrentItem(with: item) debugLog("🖼️ \(self.description) streaming video (not fully available offline) : \(video.url)") guard let currentItem = player.currentItem else { errorLog("\(self.description) No current item!") return } debugLog("🖼️ \(self.description) observing current item \(currentItem)") // Descriptions and fades are set when we begin playback if !self.observerWasSet { observerWasSet = true playerLayer.addObserver(self, forKeyPath: "readyForDisplay", options: .initial, context: nil) } sendNotification(video: video) setNotifications(currentItem) player.actionAtItemEnd = AVPlayer.ActionAtItemEnd.none // Let's never download stuff in preview... if !isPreview { Cache.fillOrRollCache() } } else { // The new localpath getter let localPath = VideoList.instance.localPathFor(video: video) // let localurl = URL(fileURLWithPath: VideoCache.cachePath(forVideo: video)!) let localurl = URL(fileURLWithPath: localPath) let localitem = AVPlayerItem(url: localurl) if !video.isHDR() { let value = PrefsVideos.vibrance[video.id] ?? 0 localitem.setVibrance(value) } if PrefsAdvanced.invertColors { localitem.setColorInvert() } DispatchQueue.global(qos: .default).async { [self] in player.replaceCurrentItem(with: localitem) debugLog("🖼️ \(self.description) playing video (OFFLINE MODE) : \(localurl)") guard let currentItem = player.currentItem else { errorLog("\(self.description) No current item!") return } debugLog("🖼️ \(self.description) observing current item \(currentItem)") // Descriptions and fades are set when we begin playback if !self.observerWasSet { observerWasSet = true playerLayer.addObserver(self, forKeyPath: "readyForDisplay", options: .initial, context: nil) } sendNotification(video: video) setNotifications(currentItem) player.actionAtItemEnd = AVPlayer.ActionAtItemEnd.none // Let's never download stuff in preview... if !isPreview { Cache.fillOrRollCache() } } } } // Is the current screen vertical? func isScreenVertical() -> Bool { return self.frame.size.width < self.frame.size.height } override func keyDown(with event: NSEvent) { debugLog("🖼️ keyDown") if PrefsVideos.allowSkips { if event.keyCode == 124 { if !isQuickFading { // If we share, just call this on our main view if AerialView.sharingPlayers { // The first view with the player gets the fade and the play next instruction, // it controls the others for view in AerialView.sharedViews where AerialView.sharedViews.first != view { view.fastFadeOut(andPlayNext: false) } AerialView.sharedViews.first!.fastFadeOut(andPlayNext: true) } else { // If we do independant playback we have to skip all views for view in AerialView.instanciatedViews { view.fastFadeOut(andPlayNext: true) } } } else { debugLog("🖼️⚠️ Right arrow key currently locked") } } else if event.keyCode == 125 { stopAnimation() } else { self.nextResponder!.keyDown(with: event) // super.keyDown(with: event) } } else { self.nextResponder?.keyDown(with: event) // super.keyDown(with: event) } } override var acceptsFirstResponder: Bool { // swiftlint:disable:next implicit_getter get { return true } } // MARK: - Extra Animations private func fastFadeOut(andPlayNext: Bool) { // We need to clear the current animations running on playerLayer isQuickFading = true // Lock the use of keydown playerLayer.removeAllAnimations() let fadeOutAnimation = CAKeyframeAnimation(keyPath: "opacity") fadeOutAnimation.values = [1, 0] as [Int] fadeOutAnimation.keyTimes = [0, AerialView.fadeDuration] as [NSNumber] if !Aerial.helper.underCompanion { fadeOutAnimation.duration = AerialView.fadeDuration } else { fadeOutAnimation.values = [1, 1] as [Int] fadeOutAnimation.duration = 0.1 } fadeOutAnimation.delegate = self fadeOutAnimation.isRemovedOnCompletion = false fadeOutAnimation.calculationMode = CAAnimationCalculationMode.cubic if andPlayNext { playerLayer.add(fadeOutAnimation, forKey: "quickfadeandnext") } else { playerLayer.add(fadeOutAnimation, forKey: "quickfade") } } // Stop callback for fastFadeOut func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { isQuickFading = false // Release our ugly lock playerLayer.opacity = 0 if anim == playerLayer.animation(forKey: "quickfadeandnext") { debugLog("🖼️ stop and next") playerLayer.removeAllAnimations() // Make sure we get rid of our anim playNextVideo() } else { debugLog("🖼️ stop") playerLayer.removeAllAnimations() // Make sure we get rid of our anim } } // Create a move animation 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 { return true } override var configureSheet: NSWindow? { if let controller = preferencesController { return controller.window } let controller = PanelWindowController() preferencesController = controller return controller.window } } ================================================ FILE: Aerial/Source/Views/Layers/AnimatableLayer.swift ================================================ // // AnimatableLayer.swift // Aerial // // Created by Guillaume Louel on 17/04/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation import AVKit protocol AnimatableLayer: CALayer { var layerManager: LayerManager { get set } var lastCorner: Int { get set } var isPreview: Bool { get set } var baseLayer: CALayer { get set } var offsets: LayerOffsets { get set } var corner: InfoCorner { get set } var currentCorner: InfoCorner? { get set } var currentHeight: CGFloat? { get set } var currentPosition: CGPoint? { get set } // Lifecycle, can be overriden so this does NOT go into the extension func clear(player: AVPlayer) func setupForVideo(video: AerialVideo, player: AVPlayer) func setContentScale(scale: CGFloat) // Used by TextLayers only func setAlignment(mode: CATextLayerAlignmentMode) // Corner movement stuff func move(toCorner: InfoCorner, fullRedraw: Bool) func moveTo(point: CGPoint) // Margins func getHorizontalMargin() -> CGFloat func getVerticalMargin(forCorner: InfoCorner) -> CGFloat } extension AnimatableLayer { // MARK: Move layers // swiftlint:disable:next cyclomatic_complexity func move(toCorner: InfoCorner, fullRedraw: Bool) { if let currCorner = currentCorner, !fullRedraw { // Are we on the same corner ? if currCorner == toCorner { // And same height ? if currentHeight! == frame.height { // position is reset, so we need to set it again position = currentPosition! return } else { // It's a whole corner redraw, then layerManager.redrawCorner(corner: toCorner) return } } else { // So we changed corner... we redraw our previous corner // and redraw the new one too ! let prevCorner = currCorner currentCorner = toCorner layerManager.redrawCorner(corner: prevCorner) layerManager.redrawCorner(corner: toCorner) return } } let mx = getHorizontalMargin() let my = getVerticalMargin(forCorner: toCorner) var newPos: CGPoint switch toCorner { case .topLeft: anchorPoint = CGPoint(x: 0, y: 1) newPos = CGPoint(x: mx, y: baseLayer.bounds.height - my) setAlignment(mode: .left) case .topCenter: anchorPoint = CGPoint(x: 0.5, y: 1) newPos = CGPoint(x: baseLayer.bounds.width/2, y: baseLayer.bounds.height-my) setAlignment(mode: .center) case .topRight: anchorPoint = CGPoint(x: 1, y: 1) newPos = CGPoint(x: baseLayer.bounds.width-mx, y: baseLayer.bounds.height-my) setAlignment(mode: .right) case .screenCenter: anchorPoint = CGPoint(x: 0.5, y: 0) newPos = CGPoint(x: baseLayer.bounds.width/2, y: baseLayer.bounds.height/2 - my + 20) setAlignment(mode: .center) case .bottomLeft: anchorPoint = CGPoint(x: 0, y: 0) newPos = CGPoint(x: mx, y: my) setAlignment(mode: .left) case .bottomCenter: anchorPoint = CGPoint(x: 0.5, y: 0) newPos = CGPoint(x: baseLayer.bounds.width/2, y: my) setAlignment(mode: .center) case .absTopRight: anchorPoint = CGPoint(x: 1, y: 1) newPos = CGPoint(x: baseLayer.bounds.width-mx, y: baseLayer.bounds.height-10) setAlignment(mode: .right) default: // bottomRight anchorPoint = CGPoint(x: 1, y: 0) newPos = CGPoint(x: baseLayer.bounds.width-mx, y: my) setAlignment(mode: .right) } moveTo(point: newPos) let offset = offsets.corner[toCorner] == 0 ? my + frame.height : frame.height // Make sure we update our offsets for the next layer offsets.corner[toCorner]! += offset // We need to save for next time ! currentCorner = toCorner currentHeight = frame.height currentPosition = newPos } // Move in 1 second to a position // Those are masked by the transition between fades func moveTo(point: CGPoint) { CATransaction.begin() CATransaction.setValue(1, forKey: kCATransactionAnimationDuration) self.position = point CATransaction.commit() } // MARK: Corners // Handle the random corner func getCorner() -> InfoCorner { if corner != .random { return corner } // Find a new corner, different from the previous one var newCorner = getRandomCorner() while newCorner == lastCorner { newCorner = getRandomCorner() } return InfoCorner(rawValue: newCorner)! } // Return a strict corner, not a center pos func getRandomCorner() -> Int { let rnd = Int.random(in: 0...3) if rnd == 0 { return 0 } else if rnd == 1 { return 2 } else if rnd == 2 { return 3 } else { return 5 } } // MARK: - Margins // Get the horizontal margin to the border of the screen func getHorizontalMargin() -> CGFloat { // We override for previews if isPreview { return 10 } var mx: CGFloat = 50 // We may override margins if PrefsInfo.overrideMargins { mx = CGFloat(PrefsInfo.marginX) } return mx } // Get the horizontal margin to the border of the screen func getVerticalMargin(forCorner: InfoCorner) -> CGFloat { // If we already have an offset, use that ! if offsets.corner[forCorner] != 0 { return offsets.corner[forCorner]! } // We override for previews if isPreview { offsets.corner[forCorner] = 10 return offsets.corner[forCorner]! } var my: CGFloat = 50 // We may override margins if PrefsInfo.overrideMargins { my = CGFloat(PrefsInfo.marginY) } offsets.corner[forCorner] = my return my } // MARK: Animations // 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 } // Create a Fade In (only) animation, used for things that // should always be on screen (clock, etc) func createFadeInAnimation() -> CAKeyframeAnimation { let fadeAnimation = CAKeyframeAnimation(keyPath: "opacity") fadeAnimation.values = [0, 0, 1] as [NSNumber] fadeAnimation.keyTimes = [ 0, Double(1 / (1 + AerialView.textFadeDuration)), Double(1) ] as [NSNumber] fadeAnimation.duration = 1 + AerialView.textFadeDuration return fadeAnimation } } // MARK: Extra helpers for text layers extension CATextLayer { // Calculate the screen rect that will be used by our string func calculateRect(string: String, font: NSFont) -> CGRect { let boundingRect = self.frame.size // We need an attributed string to take the font into account let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font as Any] let str = NSAttributedString(string: string, attributes: attributes) // Calculate bounding box let rect = str.boundingRect(with: boundingRect, options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin]) return CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.width+10, height: rect.height + 10) } func calculateRect(string: String, font: NSFont, maxWidth: Double) -> CGRect { let boundingRect = CGSize(width: maxWidth, height: Double(self.frame.size.height)) // We need an attributed string to take the font into account let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font as Any] let str = NSAttributedString(string: string, attributes: attributes) // Calculate bounding box let rect = str.boundingRect(with: boundingRect, options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin]) return CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.width+10, height: rect.height + 10) } // Get the font and font size func makeFont(name: String, size: Double) -> (NSFont, CGFloat) { let fontSize = CGFloat(size) // Mayyybe some isPreview global somewhere // Get font with a fallback in case var font = NSFont(name: "Helvetica Neue Medium", size: 28) if let tryFont = NSFont(name: name, size: fontSize) { font = tryFont } return (font!, fontSize) } // Set font & size from some Aerial Preferences func setFont(name: String, size: Double) { (self.font, self.fontSize) = self.makeFont(name: name, size: size) } } ================================================ FILE: Aerial/Source/Views/Layers/AnimationLayer.swift ================================================ // // AnimationLayer.swift // Aerial // // Created by Guillaume Louel on 17/04/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation import AVKit class AnimationLayer: CALayer, AnimatableLayer { var layerManager: LayerManager var lastCorner = -1 var isPreview: Bool var baseLayer: CALayer var offsets: LayerOffsets var corner: InfoCorner = .bottomLeft var currentCorner: InfoCorner? var currentHeight: CGFloat? var currentPosition: CGPoint? func clear(player: AVPlayer) {} // Optional func setupForVideo(video: AerialVideo, player: AVPlayer) {} // Pretty much required func setContentScale(scale: CGFloat) {} // Called by the extension to set the text alignment func setAlignment(mode: CATextLayerAlignmentMode) { // alignmentMode = mode } // Super init, used by CATextLayer's setFont, etc override init(layer: Any) { layerManager = (layer as! AnimationLayer).layerManager isPreview = (layer as! AnimationLayer).isPreview baseLayer = (layer as! AnimationLayer).baseLayer offsets = (layer as! AnimationLayer).offsets corner = (layer as! AnimationLayer).corner super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // Our init init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager) { self.layerManager = manager self.isPreview = isPreview self.baseLayer = withLayer self.offsets = offsets super.init() // Same size as the screen self.frame = withLayer.bounds // Starts hidden, with a bit of shadow for text separation self.opacity = 0 self.shadowRadius = CGFloat(PrefsInfo.shadowRadius) self.shadowOpacity = PrefsInfo.shadowOpacity self.shadowOffset = CGSize(width: PrefsInfo.shadowOffsetX, height: PrefsInfo.shadowOffsetY) self.shadowColor = CGColor.black } // Update and move to a corner func update(redraw: Bool = false) { // This is the rect resized to our string let newCorner = getCorner() // For non text layer, we need to do this here, this is done in calculateRect for text layers... if frame.size.width+10 > offsets.maxWidth[corner]! { offsets.maxWidth[corner] = frame.size.width+10 } move(toCorner: newCorner, fullRedraw: false) } } ================================================ FILE: Aerial/Source/Views/Layers/AnimationTextLayer.swift ================================================ // // AnimationLayer.swift // Aerial // // Created by Guillaume Louel on 11/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Foundation import AVKit // s*wiftlint:disable:next type_body_length class AnimationTextLayer: CATextLayer, AnimatableLayer { var layerManager: LayerManager var lastCorner = -1 var isPreview: Bool var baseLayer: CALayer var offsets: LayerOffsets var corner: InfoCorner = .bottomLeft var currentCorner: InfoCorner? var currentHeight: CGFloat? var currentPosition: CGPoint? // Super init, used by CATextLayer's setFont, etc override init(layer: Any) { layerManager = (layer as! AnimationTextLayer).layerManager isPreview = (layer as! AnimationTextLayer).isPreview baseLayer = (layer as! AnimationTextLayer).baseLayer offsets = (layer as! AnimationTextLayer).offsets corner = (layer as! AnimationTextLayer).corner super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // Our init init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager) { self.layerManager = manager self.isPreview = isPreview self.baseLayer = withLayer self.offsets = offsets super.init() // Same size as the screen self.frame = withLayer.bounds // Starts hidden, with a bit of shadow for text separation self.opacity = 0 self.shadowRadius = CGFloat(PrefsInfo.shadowRadius) self.shadowOpacity = PrefsInfo.shadowOpacity self.shadowOffset = CGSize(width: PrefsInfo.shadowOffsetX, height: PrefsInfo.shadowOffsetY) self.shadowColor = CGColor.black } // To be overriden if needed func clear(player: AVPlayer) {} // Optional func setupForVideo(video: AerialVideo, player: AVPlayer) {} // Pretty much required func setContentScale(scale: CGFloat) {} // Called by the extension to set the text alignment func setAlignment(mode: CATextLayerAlignmentMode) { alignmentMode = mode } // Update the string and move to a corner func update(string: String) { // Setup string self.string = string self.isWrapped = true // This is the rect resized to our string let newCorner = getCorner() frame = calculateRect(string: string, font: font as! NSFont, newCorner: newCorner) //debugLog(frame.debugDescription) move(toCorner: newCorner, fullRedraw: false) } // MARK: Text/Font stuff // Calculate the screen rect that will be used by our string func calculateRect(string: String, font: NSFont, newCorner: InfoCorner) -> CGRect { let mx = getHorizontalMargin() var oppoMargin: CGFloat if self is LocationLayer { oppoMargin = getOppoMargin(corner: newCorner) } else { oppoMargin = 0 } let boundingRect = CGSize(width: baseLayer.visibleRect.size.width-2*mx-oppoMargin, height: baseLayer.visibleRect.size.height) // We need an attributed string to take the font into account let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font as Any] let str = NSAttributedString(string: string, attributes: attributes) // Calculate bounding box let rect = str.boundingRect(with: boundingRect, options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin]) if !(self is LocationLayer) { if rect.width+10 > offsets.maxWidth[corner]! { offsets.maxWidth[corner] = rect.width+10 } } // Last line won't appear if we don't adjust a bit (why!?) return CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.width+10, height: rect.height + 10) } func getOppoMargin(corner: InfoCorner) -> CGFloat { // Handle the special cases of having something in the center if offsets.maxWidth[.topCenter]! > 0 && (corner == .topLeft || corner == .topRight) { return (baseLayer.visibleRect.size.width + offsets.maxWidth[.topCenter]!) / 2 } if offsets.maxWidth[.bottomCenter]! > 0 && (corner == .bottomLeft || corner == .bottomRight) { return (baseLayer.visibleRect.size.width + offsets.maxWidth[.bottomCenter]!) / 2 } // Then the regular cases switch corner { case .topLeft: return offsets.maxWidth[.topRight]! case .topRight: return offsets.maxWidth[.topLeft]! case .bottomLeft: return offsets.maxWidth[.bottomRight]! default: // .bottomRight, we only allow the 4 corners for random return offsets.maxWidth[.bottomLeft]! } } // Get the font and font size func getFont(name: String, size: Double) -> (NSFont, CGFloat) { let fontSize = isPreview ? 12 : CGFloat(size) // Get font with a fallback in case var font = NSFont(name: "Helvetica Neue Medium", size: 28) if let tryFont = NSFont(name: name, size: fontSize) { font = tryFont } return (font!, fontSize) } // Transform a date by setting it to today (or tommorrow) func todayizeDate(_ target: Date, strict: Bool) -> Date { let now = Date() let calendar = Calendar.current var targetComponent = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: target) let nowComponent = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: now) targetComponent.year = nowComponent.year targetComponent.month = nowComponent.month targetComponent.day = nowComponent.day let candidate = Calendar.current.date(from: targetComponent) ?? target if strict { return candidate } else { // In non strict mode, if the hour is passed already // we return tomorrow if candidate > now { return candidate } else { return candidate.tomorrow ?? candidate } } } } ================================================ FILE: Aerial/Source/Views/Layers/BatteryIconLayer.swift ================================================ // // BatteryIconLayer.swift // Aerial // // Created by Guillaume Louel on 01/05/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation import AVKit class BatteryIconLayer: AnimationLayer { var config: PrefsInfo.Battery? var wasSetup = false var batteryTimer: Timer? var iconLayer: CALayer? var textLayer: CATextLayer? var charging: CALayer? var backupHeight: CGFloat? override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // Our inits override init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager) { super.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) // Always on layers should start with full opacity self.opacity = 1 } convenience init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager, config: PrefsInfo.Battery) { self.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) self.config = config /* // Set our layer's font & corner now (self.font, self.fontSize) = getFont(name: config.fontName, size: config.fontSize)*/ self.corner = config.corner iconLayer = CALayer() charging = CALayer() textLayer = CATextLayer() } func setup() { let imagePath = Bundle(for: PanelWindowController.self).path( forResource: "battery.0", ofType: "pdf") guard let img = NSImage(contentsOfFile: imagePath!) else { errorLog("BatteryIconLayer couldn't load the icon files") return } iconLayer!.frame.size.height = img.size.height / 3 iconLayer!.frame.size.width = img.size.width / 3 iconLayer!.anchorPoint = CGPoint(x: 1, y: 1) iconLayer!.contents = img frame.size.height = iconLayer!.frame.size.height + 10 frame.size.width = iconLayer!.frame.size.width + 20 // We need that for later backupHeight = frame.size.height iconLayer!.position.x = frame.size.width iconLayer!.position.y = frame.size.height textLayer!.frame = CGRect(x: 20, y: 0, width: iconLayer!.frame.size.width-5, height: iconLayer!.frame.size.height) textLayer!.fontSize = 15 textLayer!.alignmentMode = .center textLayer!.string = "100%" textLayer!.foregroundColor = .white textLayer!.position.y = 19.5 self.addSublayer(iconLayer!) self.addSublayer(textLayer!) let chargingPath = Bundle(for: PanelWindowController.self).path( forResource: "bolt.fill", ofType: "pdf") if chargingPath != nil { let cimg = NSImage(contentsOfFile: chargingPath!) charging!.contents = cimg charging!.frame.size.height = cimg!.size.height / 6 charging!.frame.size.width = cimg!.size.width / 6 charging!.anchorPoint = CGPoint(x: 0, y: 0.5) charging!.position.y = frame.size.height/2+5 charging!.position.x = 2 self.addSublayer(charging!) } } override func setContentScale(scale: CGFloat) { self.contentsScale = scale iconLayer!.contentsScale = scale textLayer!.contentsScale = scale charging!.contentsScale = scale } override func setupForVideo(video: AerialVideo, player: AVPlayer) { // Only run this once if !wasSetup { wasSetup = true setup() // Update also moves and align everything... So we call it here self.updateStatus() self.update() if #available(OSX 10.12, *) { batteryTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true, block: { (_) in self.updateStatus() self.update() }) } let fadeAnimation = self.createFadeInAnimation() add(fadeAnimation, forKey: "textfade") } } func updateStatus() { let percent = Battery.getRemainingPercent() if PrefsInfo.battery.disableWhenFull { if percent == 100 { opacity = 0 frame.size.height = 1 } else { opacity = 1 frame.size.height = backupHeight! } } // Should we put the bolt or not if !Battery.isUnplugged() { charging!.opacity = 1 } else { charging!.opacity = 0 } // Update the string textLayer!.string = "\(percent) %" } } ================================================ FILE: Aerial/Source/Views/Layers/ClockLayer.swift ================================================ // // ClockLayer.swift // Aerial // // Created by Guillaume Louel on 12/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Foundation import AVKit class ClockLayer: AnimationTextLayer { var config: PrefsInfo.Clock? var wasSetup = false var clockTimer: Timer? override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // Our inits override init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager) { super.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) // Always on layers should start with full opacity self.opacity = 1 } convenience init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager, config: PrefsInfo.Clock) { self.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) self.config = config // Set our layer's font & corner now (self.font, self.fontSize) = getFont(name: config.fontName, size: config.fontSize) self.corner = config.corner } // Called at each new video, we only setup once though ! override func setupForVideo(video: AerialVideo, player: AVPlayer) { // Only run this once if !wasSetup { wasSetup = true if #available(OSX 10.12, *) { clockTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (_) in self.update(string: self.getTimeString()) }) } update(string: getTimeString()) let fadeAnimation = self.createFadeInAnimation() add(fadeAnimation, forKey: "textfade") } } func getTimeString() -> String { var locale = Locale.current if PrefsAdvanced.ciOverrideLanguage != "" { locale = Locale(identifier: PrefsAdvanced.ciOverrideLanguage) } // Handle the manual override if PrefsInfo.clock.clockFormat == .t12hours { locale = Locale(identifier: "en_US") } else if PrefsInfo.clock.clockFormat != .custom { locale = Locale(identifier: "fr_FR") } let dateFormatter = DateFormatter() if config!.clockFormat == .custom { dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: PrefsInfo.customTimeFormat, options: 0, locale: locale) } else { dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: config!.showSeconds ? "j:mm:ss" : "j:mm", options: 0, locale: locale) } if config!.hideAmPm { dateFormatter.amSymbol = "" dateFormatter.pmSymbol = "" } return dateFormatter.string(from: Date()) } } ================================================ FILE: Aerial/Source/Views/Layers/CountdownLayer.swift ================================================ // // CountdownLayer.swift // Aerial // // Created by Guillaume Louel on 13/02/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation import AVKit class CountdownLayer: AnimationTextLayer { var config: PrefsInfo.Countdown? var wasSetup = false var countdownTimer: Timer? override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // Our inits override init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager) { super.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) // Always on layers should start with full opacity self.opacity = 1 } convenience init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager, config: PrefsInfo.Countdown) { self.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) self.config = config // Set our layer's font & corner now (self.font, self.fontSize) = getFont(name: config.fontName, size: config.fontSize) self.corner = config.corner } // Called at each new video, we only setup once though ! override func setupForVideo(video: AerialVideo, player: AVPlayer) { // Only run this once if !wasSetup { wasSetup = true if shouldCountdown() { if #available(OSX 10.12, *) { countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (_) in self.update(string: self.getTimeString()) }) } update(string: getTimeString()) let fadeAnimation = self.createFadeInAnimation() add(fadeAnimation, forKey: "textfade") } } } func shouldCountdown() -> Bool { let now = Date() var target = PrefsInfo.countdown.targetDate var trigger = PrefsInfo.countdown.triggerDate // We ignore the day, in timeOfDay mode by normalizing it to today if config!.mode == .timeOfDay { target = todayizeDate(target, strict: false) trigger = todayizeDate(trigger, strict: true) } // We only start the countdown if we're later than the trigger if config!.enforceInterval { if trigger > now { return false } } // Are we still before the countdown date or not ? if now < target { return true } return false } func getTimeString() -> String { if #available(OSX 10.12, *) { // Handle locale var locale = Locale(identifier: Locale.preferredLanguages[0]) if PrefsAdvanced.ciOverrideLanguage != "" { locale = Locale(identifier: PrefsAdvanced.ciOverrideLanguage) } var calendar = Calendar.current calendar.locale = locale let dateComponentsFormatter = DateComponentsFormatter() dateComponentsFormatter.calendar = calendar if config!.showSeconds { dateComponentsFormatter.allowedUnits = [.day, .hour, .minute, .second] dateComponentsFormatter.maximumUnitCount = 4 } else { dateComponentsFormatter.allowedUnits = [.day, .hour, .minute] dateComponentsFormatter.maximumUnitCount = 3 } dateComponentsFormatter.unitsStyle = .full var target = PrefsInfo.countdown.targetDate // We ignore the day, in timeOfDay mode by normalizing it to today if config!.mode == .timeOfDay { target = todayizeDate(target, strict: false) } return dateComponentsFormatter.string(from: Date(), to: target) ?? "" } else { // Fallback on earlier versions return "" } } } extension Date { var tomorrow: Date? { return Calendar.current.date(byAdding: .day, value: 1, to: self) } } ================================================ FILE: Aerial/Source/Views/Layers/DateLayer.swift ================================================ // // DateLayer.swift // Aerial // // Created by Guillaume Louel on 23/03/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation import AVKit class DateLayer: AnimationTextLayer { var config: PrefsInfo.IDate? var wasSetup = false var dateTimer: Timer? override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // Our inits override init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager) { super.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) // Always on layers should start with full opacity self.opacity = 1 } convenience init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager, config: PrefsInfo.IDate) { self.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) self.config = config // Set our layer's font & corner now (self.font, self.fontSize) = getFont(name: config.fontName, size: config.fontSize) self.corner = config.corner } // Called at each new video, we only setup once though ! override func setupForVideo(video: AerialVideo, player: AVPlayer) { // Only run this once if !wasSetup { wasSetup = true if #available(OSX 10.12, *) { dateTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (_) in self.update(string: self.getTimeString()) }) } update(string: getTimeString()) let fadeAnimation = self.createFadeInAnimation() add(fadeAnimation, forKey: "textfade") } } func getTimeString() -> String { // Handle locale var locale = Locale(identifier: Locale.preferredLanguages[0]) if PrefsAdvanced.ciOverrideLanguage != "" { locale = Locale(identifier: PrefsAdvanced.ciOverrideLanguage) } var template = "" let dateFormatter = DateFormatter() if config!.format == .textual { if config!.withYear { template = "EEEE, MMMM dd, yyyy" } else { template = "EEEE, MMMM dd" } dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: template, options: 0, locale: locale) dateFormatter.locale = locale return dateFormatter.string(from: Date()).capitalizeFirstLetter() } else if config!.format == .compact { if config!.withYear { template = "MM/dd/yy" } else { template = "MM/dd" } dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: template, options: 0, locale: locale) dateFormatter.locale = locale return dateFormatter.string(from: Date()).capitalizeFirstLetter() } else { let RFC3339DateFormatter = DateFormatter() RFC3339DateFormatter.locale = Locale(identifier: "en_US_POSIX") RFC3339DateFormatter.dateFormat = PrefsInfo.customDateFormat return RFC3339DateFormatter.string(from: Date()) } } } ================================================ FILE: Aerial/Source/Views/Layers/DownloadIndicatorLayer.swift ================================================ // // UpdatesLayer.swift // Aerial // // Created by Guillaume Louel on 11/02/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation import AVKit class DownloadIndicatorLayer: AnimationTextLayer { var config: PrefsInfo.Updates? var wasSetup = false var updateTimer: Timer? override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // Our inits override init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager) { super.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) // We start with a full opacity self.opacity = 1 } convenience init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager, config: PrefsInfo.Updates) { self.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) self.config = config // Set our layer's font & corner now (self.font, self.fontSize) = getFont(name: config.fontName, size: config.fontSize) self.corner = .absTopRight } override func setupForVideo(video: AerialVideo, player: AVPlayer) { if !wasSetup && PrefsCache.showBackgroundDownloads { update(string: "") setupDownloadIndicatorLayer() } } // Setup the callbacks func setupDownloadIndicatorLayer() { // Setup the updates for the download status let videoManager = VideoManager.sharedInstance videoManager.addCallback { done, total in self.updateDownloads(done: done, total: total, progress: 0) } videoManager.addProgressCallback { done, total, progress in self.updateDownloads(done: done, total: total, progress: progress) } } func updateDownloads(done: Int, total: Int, progress: Double) { if total == 0 { update(string: "") } else { let progInt = Int(progress * 100) update(string: "Downloading: \(progInt) %") } } } ================================================ FILE: Aerial/Source/Views/Layers/LayerManager.swift ================================================ // // LayerManager.swift // Aerial // // Created by Guillaume Louel on 12/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Foundation import AVKit class LayerManager { var additionalLayers = [AnimatableLayer]() let offsets = LayerOffsets() var isPreview: Bool var frame: CGRect? init(isPreview: Bool) { self.isPreview = isPreview } // Initial setup of all layers, at Aerial startup func setupExtraLayers(layer: CALayer, frame: CGRect) { self.frame = frame var topRow = [InfoType]() var bottomRow = [InfoType]() // The list of known layers is in an ordered array // we need to split the bottom row though, as drawing them "in order" would look // reversed to users as we draw from the corner out for layerType in PrefsInfo.layers { let pos = PrefsInfo.ofType(layerType).corner if pos == .topCenter || pos == .topLeft || pos == .topRight || pos == .screenCenter { topRow.append(layerType) } else { bottomRow.append(layerType) } } // Then add top row for layerType in topRow { addLayerForType(layerType, layer: layer) } // Then we may need to add our special update layer // It doesn't show in the main UI, it's linked to // options in the Updates tab // And reversed bottomRow for layerType in bottomRow.reversed() { addLayerForType(layerType, layer: layer) } } // swiftlint:disable:next cyclomatic_complexity private func addLayerForType(_ layerType: InfoType, layer: CALayer) { var newLayer: AnimatableLayer? if PrefsInfo.ofType(layerType).isEnabled && shouldEnableOnScreen(PrefsInfo.ofType(layerType).displays) { switch layerType { case .location: newLayer = LocationLayer(withLayer: layer, isPreview: isPreview, offsets: offsets, manager: self, config: PrefsInfo.location) case .message: newLayer = MessageLayer(withLayer: layer, isPreview: isPreview, offsets: offsets, manager: self, config: PrefsInfo.message) case .clock: newLayer = ClockLayer(withLayer: layer, isPreview: isPreview, offsets: offsets, manager: self, config: PrefsInfo.clock) case .date: newLayer = DateLayer(withLayer: layer, isPreview: isPreview, offsets: offsets, manager: self, config: PrefsInfo.date) case .battery: newLayer = BatteryIconLayer(withLayer: layer, isPreview: isPreview, offsets: offsets, manager: self, config: PrefsInfo.battery) case .updates: newLayer = DownloadIndicatorLayer(withLayer: layer, isPreview: isPreview, offsets: offsets, manager: self, config: PrefsInfo.updates) case .weather: newLayer = WeatherLayer(withLayer: layer, isPreview: isPreview, offsets: offsets, manager: self, config: PrefsInfo.weather) case .countdown: newLayer = CountdownLayer(withLayer: layer, isPreview: isPreview, offsets: offsets, manager: self, config: PrefsInfo.countdown) case .timer: newLayer = TimerLayer(withLayer: layer, isPreview: isPreview, offsets: offsets, manager: self, config: PrefsInfo.timer) case .music: newLayer = MusicLayer(withLayer: layer, isPreview: isPreview, offsets: offsets, manager: self, config: PrefsInfo.music) } } if let nLayer = newLayer { nLayer.drawsAsynchronously = true if !PrefsInfo.highQualityTextRendering { // This seems to help on some configurations // It has no impact on others and wrecks retina fonts though... nLayer.shouldRasterize = true } additionalLayers.append(nLayer) layer.addSublayer(nLayer) } } // Each layer may not be displayed on each screen func shouldEnableOnScreen(_ displayMode: InfoDisplays) -> Bool { let displayDetection = DisplayDetection.sharedInstance let thisScreen = displayDetection.findScreenWith(frame: frame!) if let screen = thisScreen, !isPreview { switch displayMode { case .allDisplays: debugLog("allDisplays") return true case .mainOnly: debugLog("mainOnly") return screen.isMain case .secondaryOnly: debugLog("secOnly") return !screen.isMain } } // If it's an unknown screen or a preview, we leave it enabled return true } // Called before starting a new video func clearLayerAnimations(player: AVPlayer) { for layer in additionalLayers { print(layer) layer.clear(player: player) layer.removeAllAnimations() } } func removeAllLayers() { for layer in additionalLayers { print(layer) layer.removeAllAnimations() layer.removeFromSuperlayer() } } // Called at each new video func setupLayersForVideo(video: AerialVideo, player: AVPlayer) { // We first setup all the regular layers, this will fill up the margin information // and act as a preflight so we can calculate how to wrap things for long location layer text for layer in additionalLayers where !(layer is LocationLayer) { layer.setupForVideo(video: video, player: player) } // And only last the Location layer ! for layer in additionalLayers where layer is LocationLayer { layer.setupForVideo(video: video, player: player) } } // This is called if a screen changes resolution // Can possibly happen when a new screen is connected/disconnected func setContentScale(scale: CGFloat) { for layer in additionalLayers { layer.contentsScale = scale layer.setContentScale(scale: scale) } } // We use this to fully redraw all layers in a given corner // This is used by transient layers, like location information that's only shown // for a predefined amount of time func redrawCorner(corner: InfoCorner) { // first clear the offset on that corner offsets.corner[corner] = 0 // Then move all our layers on that corner for layer in additionalLayers { if let layerCorner = layer.currentCorner { if layerCorner == corner { layer.move(toCorner: corner, fullRedraw: true) } } } } // This is called when our view gets resized. In theory this is limited to Companion but who knows func redrawAllCorners() { InfoCorner.allCases.forEach { corner in redrawCorner(corner: corner) } } // Do we allow a random description in a corner or not ? // This is a best effort to try and avoid overlaps, // but it's not 100% depending on font choices func isCornerAcceptable(corner: Int) -> Bool { // Not the prettiest helper, this is a bit of a hack // If we have something in both topCenter and bottomCenter, we could infinite loop // So as a precaution we allow whatever was picked for layer in additionalLayers where layer.corner == .topCenter { for layer2 in additionalLayers where layer2.corner == .bottomCenter { return true } } // If we have something topCenter, never allow random on top left/right if corner == 0 || corner == 2 { for layer in additionalLayers where layer.corner == .topCenter { return false } } // Same thing on the bottom if corner == 3 || corner == 5 { for layer in additionalLayers where layer.corner == .bottomCenter { return false } } // And never allow center if there's something in a corner // this one is a bit drastic as overlap isn't guaranteed but... if corner == 1 { for layer in additionalLayers where layer.corner == .topLeft || layer.corner == .topRight { return false } } // And same at the bottom if corner == 4 { for layer in additionalLayers where layer.corner == .bottomLeft || layer.corner == .bottomRight { return false } } return true } } ================================================ FILE: Aerial/Source/Views/Layers/LayerOffsets.swift ================================================ // // LayerOffsets.swift // Aerial // // Created by Guillaume Louel on 11/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Foundation class LayerOffsets { var corner = [InfoCorner: CGFloat]() var maxWidth = [InfoCorner: CGFloat]() init() { corner[.topLeft] = 0 corner[.topCenter] = 0 corner[.topRight] = 0 corner[.bottomLeft] = 0 corner[.bottomCenter] = 0 corner[.bottomRight] = 0 corner[.screenCenter] = 0 corner[.absTopRight] = 0 maxWidth[.topLeft] = 0 maxWidth[.topCenter] = 0 maxWidth[.topRight] = 0 maxWidth[.bottomLeft] = 0 maxWidth[.bottomCenter] = 0 maxWidth[.bottomRight] = 0 maxWidth[.screenCenter] = 0 maxWidth[.absTopRight] = 0 } } ================================================ FILE: Aerial/Source/Views/Layers/LocationLayer.swift ================================================ // // LocationLayer.swift // Aerial // // Created by Guillaume Louel on 11/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Foundation import AVKit class LocationLayer: AnimationTextLayer { var config: PrefsInfo.Location? var timeObserver: Any? override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // Our inits override init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager) { super.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) } convenience init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager, config: PrefsInfo.Location) { self.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) self.config = config // Set our layer's font & corner now (self.font, self.fontSize) = getFont(name: config.fontName, size: config.fontSize) self.corner = config.corner } // We need to clear our callbacks on the player override func clear(player: AVPlayer) { if timeObserver != nil { player.removeTimeObserver(timeObserver!) timeObserver = nil } } // Called at each new video override func setupForVideo(video: AerialVideo, player: AVPlayer) { let poiStringProvider = PoiStringProvider.sharedInstance // We need to make sure we actually have descriptions to show. // Custom videos, and earlier tvOS videos may not if poiStringProvider.hasPoiKeys(video: video) { // Grab a sorted array of timestamps and the keys let (keys, times) = getKeysAndTimestamps(video: video) // Animate the very first one on it's own var initialKey = keys["0"]! // Oh Apple... This is a temporary fix for Coit Tower Night where a key was reused if initialKey == "A004_C012_0" && video.id == "b6-4" { initialKey = "A004_C012_100" } let str = poiStringProvider.getString(key: initialKey, video: video) let duration = calculateAnimationDuration(times: times, current: times[0], video: video) let fadeAnimation = createFadeInOutAnimation(duration: duration) update(string: str) add(fadeAnimation, forKey: "textfade") // AVPlayer requires NSValues of CMTime var timevals = [NSValue]() for time in times { timevals.append(NSValue(time: time)) } // We then callback for each timestamp timeObserver = player.addBoundaryTimeObserver(forTimes: timevals, queue: DispatchQueue.main) { // find closest timestamp to when we're waking up var closest = 1000.0 var closestTime = CMTime.zero for time in times { let ts = time.seconds let distance = abs(ts - player.currentTime().seconds) if distance < closest { closest = distance closestTime = time } } // Get the string for the current timestamp let key = String(format: "%.0f", closestTime.seconds) let str = poiStringProvider.getString(key: keys[key]!, video: video) let duration = self.calculateAnimationDuration(times: times, current: closestTime, video: video) let fadeAnimation = self.createFadeInOutAnimation(duration: duration) self.update(string: str) self.add(fadeAnimation, forKey: "textfade") } } else { // 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 } let duration = self.calculateAnimationDuration(times: [], current: CMTime.zero, video: video) let fadeAnimation = self.createFadeInOutAnimation(duration: duration) update(string: str) add(fadeAnimation, forKey: "textfade") } } // MARK: - Time helpers func getKeysAndTimestamps(video: AerialVideo) -> ([String: String], [CMTime]) { let poiStringProvider = PoiStringProvider.sharedInstance // Collect all the timestamps and keys from the JSON // They are store as [Time, Key] let keys = poiStringProvider.getPoiKeys(video: video) var times = [CMTime]() for pkv in keys { let timeStamp = Double(pkv.key)! times.append(CMTime(seconds: timeStamp, preferredTimescale: 1)) } // The JSON isn't sorted though, so we fix that times.sort(by: { $0.seconds < $1.seconds }) return (keys, times) } func calculateAnimationDuration(times: [CMTime], current: CMTime, video: AerialVideo) -> Double { // We may only show for 10s if PrefsInfo.location.time == .tenSeconds { return 10 } else { if let idx = times.firstIndex(of: current) { if times.count > idx + 1 { return times[idx+1].seconds - times[idx].seconds - 1 } } // We may not have a video duration, if so show it for 15 mins return video.duration > 0 ? video.duration : 900 } } } ================================================ FILE: Aerial/Source/Views/Layers/MessageLayer.swift ================================================ // // MessageLayer.swift // Aerial // // Created by Guillaume Louel on 12/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Foundation import AVKit class MessageLayer: AnimationTextLayer { var config: PrefsInfo.Message? var wasSetup = false var messageTimer: Timer? override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // Our inits override init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager) { super.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) // We start with a full opacity self.opacity = 1 } convenience init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager, config: PrefsInfo.Message) { self.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) self.config = config // Set our layer's font & corner now (self.font, self.fontSize) = getFont(name: config.fontName, size: config.fontSize) self.corner = config.corner } override func setupForVideo(video: AerialVideo, player: AVPlayer) { guard let config = config else { return } // Only run this once, if enabled if !wasSetup { wasSetup = true switch config.messageType { case .text: update(string: config.message) case .shell: update(string: "") DispatchQueue.global().async { debugLog("setting up initial") let result = self.runShell() if let result = result { // Do it on the main queue... DispatchQueue.main.async { debugLog("updating initial " + result) self.update(string: result) } } } //setupRefresh() case .textfile: // TODO update(string: config.message) } let fadeAnimation = self.createFadeInAnimation() add(fadeAnimation, forKey: "textfade") } } func setupRefresh() { debugLog("setting up refresh") guard let config = config else { return } guard config.refreshPeriodicity != .never else { return } if #available(OSX 10.12, *) { var interval = 0.0 switch config.refreshPeriodicity { case .never: interval = 1 case .tenseconds: interval = 10 case .thirtyseconds: interval = 30 case .oneminute: interval = 60 case .fiveminutes: interval = 300 case .tenminutes: interval = 600 } messageTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true, block: { [self] (_) in DispatchQueue.global().async { let result = self.runShell() self.update(string: result ?? "") } }) } } func runShell() -> String? { guard let config = config else { return nil } if config.shellScript != "" { if FileManager.default.fileExists(atPath: PrefsInfo.message.shellScript) { var result: String? if #available(macOS 14.0, *) { (result, _) = Aerial.helper.shell(executableURL: PrefsInfo.message.shellScript) } else { (result, _) = Aerial.helper.shell(launchPath: PrefsInfo.message.shellScript) } debugLog("result " + (result ?? "")) if let res = result { return res } } } return nil } } ================================================ FILE: Aerial/Source/Views/Layers/Music/ArtworkLayer.swift ================================================ // // ArtworkLayer.swift // Aerial // // Created by Guillaume Louel on 30/06/2021. // Copyright © 2021 Guillaume Louel. All rights reserved. // import Cocoa import Foundation class ArtworkLayer: CALayer { var defaultImg: NSImage? override init() { super.init() if #available(macOS 11.0, *) { let size: CGFloat = 200 if let image = NSImage(systemSymbolName: "music.note", accessibilityDescription: "music.note") { image.isTemplate = true // return image let config = NSImage.SymbolConfiguration(pointSize: size, weight: .regular) let img = image.withSymbolConfiguration(config)?.tinting(with: .white) if let img = img { frame.size.height = size frame.size.width = size contents = img defaultImg = img } } } } override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func updateArtwork(artwork: NSImage?) { if let artwork = artwork { contents = artwork } } /* func updateArtwork(id: String) { Music.instance.getArtworkUrl(id: id) { [self] artworkUrl in guard let artworkUrl = artworkUrl else { debugLog("no url found") if let defaultImg = defaultImg { contents = defaultImg } return } print(artworkUrl) // Then grab said url getData(from: URL(string: artworkUrl)!) { data, _, error in guard let data = data, error == nil else { if let defaultImg = defaultImg { contents = defaultImg } return } DispatchQueue.main.async() { let img = NSImage(data: data) // Update it in the main thread contents = img } } } } func getData(from url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> Void) { URLSession.shared.dataTask(with: url, completionHandler: completion).resume() }*/ } ================================================ FILE: Aerial/Source/Views/Layers/Music/MusicLayer.swift ================================================ // // MusicLayer.swift // Aerial // // Created by Guillaume Louel on 11/06/2021. // Copyright © 2021 Guillaume Louel. All rights reserved. // import Foundation import AVKit class MusicLayer: AnimationLayer { var config: PrefsInfo.Music? var wasSetup = false var timer: Timer? var startTime: Date? var endTime: Date? let artworkLayer = ArtworkLayer() let nameLayer = CATextLayer() let artistLayer = CATextLayer() let albumLayer = CATextLayer() override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // Our inits override init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager) { super.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) self.opacity = 0 } convenience init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager, config: PrefsInfo.Music) { self.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) self.config = config // Set our layer's font & corner now /*(self.font, self.fontSize) = getFont(name: config.fontName, size: config.fontSize)*/ self.corner = config.corner } // Called at each new video, we only setup once though ! override func setupForVideo(video: AerialVideo, player: AVPlayer) { // Only run this once if !wasSetup { setupLayer() // This is where the magic happens, we get notified if we need to display something Music.instance.addCallback { [self] songInfo in updateStatus(songInfo: songInfo) update() } wasSetup = true update() /* let fadeAnimation = self.createFadeInAnimation() add(fadeAnimation, forKey: "textfade")*/ } // We trigger the first one manually, so we get data immediately debugLog("🎧🟧 manual triggering") Music.instance.mediaRemoteCallback(nil) } func setupLayer() { addSublayer(artworkLayer) // Song name on top nameLayer.string = "" (nameLayer.font, nameLayer.fontSize) = nameLayer.makeFont(name: PrefsInfo.music.fontName, size: PrefsInfo.music.fontSize) addSublayer(nameLayer) // Artist name below artistLayer.string = "" (artistLayer.font, artistLayer.fontSize) = artistLayer.makeFont(name: PrefsInfo.music.fontName, size: PrefsInfo.music.fontSize) addSublayer(artistLayer) // Artist name below albumLayer.string = "" (albumLayer.font, albumLayer.fontSize) = albumLayer.makeFont(name: PrefsInfo.music.fontName, size: PrefsInfo.music.fontSize) addSublayer(albumLayer) // frame/position stuff reframe() } func reframe() { // ReRect the name & artist let rect = nameLayer.calculateRect(string: nameLayer.string as! String, font: nameLayer.font as! NSFont, maxWidth: Double(layerManager.frame!.size.width)) nameLayer.frame = rect nameLayer.contentsScale = self.contentsScale let rect2 = artistLayer.calculateRect(string: artistLayer.string as! String, font: artistLayer.font as! NSFont, maxWidth: Double(layerManager.frame!.size.width)) artistLayer.frame = rect2 artistLayer.contentsScale = self.contentsScale let rect3 = albumLayer.calculateRect(string: albumLayer.string as! String, font: albumLayer.font as! NSFont, maxWidth: Double(layerManager.frame!.size.width)) albumLayer.frame = rect3 albumLayer.contentsScale = self.contentsScale artworkLayer.contentsScale = self.contentsScale // Then calc our parent frame size let textHeight = nameLayer.frame.height + artistLayer.frame.height + albumLayer.frame.height let textWidth = max(nameLayer.frame.width, artistLayer.frame.width, albumLayer.frame.width) let artworkOffset = textHeight + 20 frame.size = CGSize(width: textWidth + artworkOffset, height: textHeight) // If we don't have any song playing, we change the height to 0 if (nameLayer.string as! String == "") && (artistLayer.string as! String == "") { frame.size.height = 0 } // Position the things albumLayer.anchorPoint = CGPoint(x: 0, y: 0) albumLayer.position = CGPoint(x: artworkOffset, y: 0) nameLayer.anchorPoint = CGPoint(x: 0, y: 0) nameLayer.position = CGPoint(x: artworkOffset, y: albumLayer.frame.height - 6) artistLayer.anchorPoint = CGPoint(x: 0, y: 0) artistLayer.position = CGPoint(x: artworkOffset, y: albumLayer.frame.height + nameLayer.frame.height - 12) artworkLayer.anchorPoint = CGPoint(x: 0, y: 0) artworkLayer.position = CGPoint(x: 0, y: 0) artworkLayer.frame.size = CGSize(width: frame.size.height, height: frame.size.height) } func updateStatus(songInfo: SongInfo) { debugLog("🎧🟧 updateStatus") guard songInfo.name != "" else { opacity = 0 frame.size.height = 0 return } opacity = 1 nameLayer.string = songInfo.name artistLayer.string = songInfo.artist albumLayer.string = songInfo.album artworkLayer.updateArtwork(artwork: songInfo.artwork) // frame/position stuff reframe() } } ================================================ FILE: Aerial/Source/Views/Layers/TimerLayer.swift ================================================ // // TimerLayer.swift // Aerial // // Created by Guillaume Louel on 19/03/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation import AVKit class TimerLayer: AnimationTextLayer { var config: PrefsInfo.Timer? var wasSetup = false var timer: Timer? var startTime: Date? var endTime: Date? override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // Our inits override init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager) { super.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) // Always on layers should start with full opacity self.opacity = 1 } convenience init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager, config: PrefsInfo.Timer) { self.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) self.config = config // Set our layer's font & corner now (self.font, self.fontSize) = getFont(name: config.fontName, size: config.fontSize) self.corner = config.corner } // Called at each new video, we only setup once though ! override func setupForVideo(video: AerialVideo, player: AVPlayer) { // Only run this once if !wasSetup { wasSetup = true startTime = Date() // Now let calendar = Calendar.current let targetComponent = calendar.dateComponents([.hour, .minute, .second], from: PrefsInfo.timer.duration) let timerInSeconds = targetComponent.hour! * 3600 + targetComponent.minute! * 60 + targetComponent.second! endTime = startTime?.addingTimeInterval(TimeInterval(timerInSeconds)) if #available(OSX 10.12, *) { timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (_) in self.update(string: self.getTimeString()) }) } update(string: getTimeString()) let fadeAnimation = self.createFadeInAnimation() add(fadeAnimation, forKey: "textfade") } } func getTimeString() -> String { if #available(OSX 10.12, *) { // Handle locale var locale = Locale(identifier: Locale.preferredLanguages[0]) if PrefsAdvanced.ciOverrideLanguage != "" { locale = Locale(identifier: PrefsAdvanced.ciOverrideLanguage) } var calendar = Calendar.current calendar.locale = locale let dateComponentsFormatter = DateComponentsFormatter() dateComponentsFormatter.calendar = calendar if config!.showSeconds { dateComponentsFormatter.allowedUnits = [.hour, .minute, .second] dateComponentsFormatter.maximumUnitCount = 3 } else { dateComponentsFormatter.allowedUnits = [.hour, .minute] dateComponentsFormatter.maximumUnitCount = 2 } dateComponentsFormatter.unitsStyle = .full if Date() > endTime! && PrefsInfo.timer.disableWhenElapsed { // Disabling for next launch PrefsInfo.timer.isEnabled = false } if Date() > endTime! { // We may show a message when the timer is elapsed if PrefsInfo.timer.replaceWithMessage { return PrefsInfo.timer.customMessage } } return dateComponentsFormatter.string(from: Date(), to: endTime!) ?? "" } else { // Fallback on earlier versions return "" } } } ================================================ FILE: Aerial/Source/Views/Layers/Weather/ConditionLayer.swift ================================================ // // ConditionLayer.swift // Aerial // // Created by Guillaume Louel on 17/04/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation import AVKit // Vertically centered CATextLayer class CAVCTextLayer: CATextLayer { // REF: http://lists.apple.com/archives/quartz-dev/2008/Aug/msg00016.html // CREDIT: David Hoerl - https://github.com/dhoerl // USAGE: To fix the vertical alignment issue that currently exists within the CATextLayer class. Change made to the yDiff calculation. override func draw(in context: CGContext) { let height = self.bounds.size.height let fontSize = self.fontSize let yDiff = (height-fontSize)/2 - fontSize/10 context.saveGState() context.translateBy(x: 0, y: -yDiff) super.draw(in: context) context.restoreGState() } } class ConditionLayer: CALayer { var condition: OWeather? init(condition: OWeather, scale: CGFloat) { self.condition = condition super.init() // backgroundColor = .init(gray: 0.2, alpha: 0.2) contentsScale = scale // First we make the temperatures block (accurate and feels like) let tempBlock = makeTemperatureBlock() let feelsBlock = makeFeelsLikeBlock() var cityNameBlock: CALayer if PrefsInfo.weather.showCity { cityNameBlock = makeCityNameBlock() } else { cityNameBlock = CALayer() } // We make the symbol a square of the combined height of both blocks let combinedHeight = tempBlock.frame.height + feelsBlock.frame.height // Create a symbol that fits the size let imglayer = ConditionSymbolLayer(weather: condition.weather![0], dt: condition.dt!, sunrise: condition.sys!.sunrise, sunset: condition.sys!.sunset, size: Int(combinedHeight)) // Add the Wind layer var windHeight: CGFloat = 0 if PrefsInfo.weather.showWind || PrefsInfo.weather.showHumidity { windHeight = addWindAndHumidity(x: (imglayer.frame.width + combinedHeight/10 + tempBlock.frame.width) / 2, y: cityNameBlock.frame.height) } imglayer.anchorPoint = CGPoint(x: 0, y: 0) imglayer.position = CGPoint(x: 0, y: windHeight + cityNameBlock.frame.height) self.addSublayer(imglayer) frame.size = CGSize(width: imglayer.frame.width + combinedHeight/10 + tempBlock.frame.width, height: tempBlock.frame.height + feelsBlock.frame.height + windHeight + cityNameBlock.frame.height) addSublayer(cityNameBlock) cityNameBlock.anchorPoint = CGPoint(x: 0.5, y: 0) cityNameBlock.position = CGPoint(x: frame.size.width/2, y: 0) addSublayer(tempBlock) tempBlock.anchorPoint = CGPoint(x: 1, y: 1) tempBlock.position = CGPoint(x: frame.size.width, y: tempBlock.frame.height + feelsBlock.frame.height + windHeight + cityNameBlock.frame.height) addSublayer(feelsBlock) feelsBlock.anchorPoint = CGPoint(x: 0.5, y: 0) feelsBlock.position = CGPoint(x: imglayer.frame.width + combinedHeight/10 + tempBlock.frame.width/2, y: windHeight + cityNameBlock.frame.height) } override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func makeCityNameBlock() -> CATextLayer { let temp = CATextLayer() temp.isWrapped = true temp.string = condition!.name (temp.font, temp.fontSize) = temp.makeFont(name: PrefsInfo.weather.fontName, size: PrefsInfo.weather.fontSize/1.5) temp.alignmentMode = .center // ReRect the temperature let rect = temp.calculateRect(string: temp.string as! String, font: temp.font as! NSFont, maxWidth: 220) temp.frame = rect temp.contentsScale = self.contentsScale return temp } func makeTemperatureBlock() -> CATextLayer { let temp = CAVCTextLayer() // First we start with the real temperature // We keep the decimal for now on celcius, this may become optional if PrefsInfo.weather.degree == .celsius { temp.string = "\(condition!.main!.temp)°" } else { temp.string = "\(Int(condition!.main!.temp))°" } (temp.font, temp.fontSize) = temp.makeFont(name: PrefsInfo.weather.fontName, size: PrefsInfo.weather.fontSize) // ReRect the temperature let rect = temp.calculateRect(string: temp.string as! String, font: temp.font as! NSFont) temp.frame = rect temp.contentsScale = self.contentsScale return temp } func makeFeelsLikeBlock() -> CATextLayer { // Make a vertically centered layer for t° let feel = CAVCTextLayer() if PrefsInfo.weather.degree == .celsius { feel.string = "(\(condition!.main!.feelsLike)°)" } else { feel.string = "(\(Int(condition!.main!.feelsLike))°)" } feel.contentsScale = self.contentsScale (feel.font, feel.fontSize) = feel.makeFont(name: PrefsInfo.weather.fontName, size: PrefsInfo.weather.fontSize/2.2) // ReRect the temperature let rect2 = feel.calculateRect(string: feel.string as! String, font: feel.font as! NSFont) feel.frame = rect2 return feel } // swiftlint:disable:next identifier_name func addWindAndHumidity(x: CGFloat, y: CGFloat) -> CGFloat { // We need to make sure we have the data, and the options are selected var addWind = false, addHumidity = false let wind = condition?.wind let humidity = condition?.main?.humidity if PrefsInfo.weather.showWind && wind != nil { addWind = true } if PrefsInfo.weather.showHumidity && humidity != nil { addHumidity = true } // If we shouldn't display/should and don't have data if !addWind && !addHumidity { return 0 } // Ughhhhh, this code is so ugly var windBlock: CALayer? var humidityBlock: CALayer? if addWind { windBlock = makeWindBlock(wind: wind!) // windBlock!.anchorPoint = CGPoint(x: 0, y: 0) } if addHumidity { humidityBlock = makeHumidityBlock(humidity: humidity!) // humidityBlock!.anchorPoint = CGPoint(x: 0, y: 0) } // Haaaaaaaa I hate this if addWind && addHumidity { let halfTotalWidth = (windBlock!.frame.size.width + humidityBlock!.frame.size.width)/2 windBlock!.position = CGPoint(x: x - halfTotalWidth + windBlock!.frame.size.width/2, y: y) humidityBlock!.position = CGPoint(x: x + halfTotalWidth - humidityBlock!.frame.size.width/2, y: y) self.addSublayer(windBlock!) self.addSublayer(humidityBlock!) return windBlock!.frame.height } else if addWind { windBlock!.position = CGPoint(x: x, y: y) self.addSublayer(windBlock!) return windBlock!.frame.height } else if addHumidity { humidityBlock!.position = CGPoint(x: x, y: y) self.addSublayer(humidityBlock!) return humidityBlock!.frame.height } // tmp return 0 } func makeHumidityBlock(humidity: Double) -> CALayer { let humidityBlock = CALayer() // Make a vertically centered layer for t° let textHumidity = CAVCTextLayer() textHumidity.string = " \(Int(humidity))%" // Get something large first (textHumidity.font, textHumidity.fontSize) = textHumidity.makeFont(name: PrefsInfo.weather.fontName, size: PrefsInfo.weather.fontSize/2.2) textHumidity.contentsScale = self.contentsScale // ReRect the temperature let rect2 = textHumidity.calculateRect(string: textHumidity.string as! String, font: textHumidity.font as! NSFont) textHumidity.frame = rect2 textHumidity.contentsScale = self.contentsScale humidityBlock.addSublayer(textHumidity) let imglayer = Aerial.helper.getSymbolLayer("humidity", size: CGFloat(PrefsInfo.weather.fontSize/2.8)) // We put the temperature at the right of the wind icon textHumidity.anchorPoint = CGPoint(x: 0, y: 0) textHumidity.position = CGPoint(x: imglayer.frame.height, y: 0) imglayer.anchorPoint = CGPoint(x: 0.5, y: 0.5) imglayer.position = CGPoint(x: imglayer.frame.height/2, y: textHumidity.frame.height/2) imglayer.contentsScale = self.contentsScale humidityBlock.frame.size = CGSize(width: textHumidity.frame.width+imglayer.frame.width, height: max(textHumidity.frame.height, imglayer.frame.height)) humidityBlock.anchorPoint = CGPoint(x: 0.5, y: 0) humidityBlock.addSublayer(imglayer) return humidityBlock } func makeWindBlock(wind: OWWind) -> CALayer { let windBlock = CALayer() // Make a vertically centered layer for t° let textWind = CAVCTextLayer() if PrefsInfo.weather.degree == .celsius { if PrefsInfo.weatherWindMode == .kph { textWind.string = "\(Int(wind.speed * 3.6)) km/h" } else { textWind.string = "\(Int(wind.speed)) m/s" } } else { textWind.string = "\(Int(wind.speed)) mph" } // Get something large first (textWind.font, textWind.fontSize) = textWind.makeFont(name: PrefsInfo.weather.fontName, size: PrefsInfo.weather.fontSize/2.2) textWind.contentsScale = self.contentsScale // ReRect the temperature let rect2 = textWind.calculateRect(string: textWind.string as! String, font: textWind.font as! NSFont) textWind.frame = rect2 textWind.contentsScale = self.contentsScale windBlock.addSublayer(textWind) // Create the wind indicator let imglayer = WindDirectionLayer(direction: 225, size: CGFloat(PrefsInfo.weather.fontSize/2.8)) textWind.anchorPoint = CGPoint(x: 0, y: 0) textWind.position = CGPoint(x: imglayer.frame.height, y: 0) // Rotation is relative to anchorpoint, so it has to be middle imglayer.anchorPoint = CGPoint(x: 0.5, y: 0.5) imglayer.position = CGPoint(x: imglayer.frame.height/2, y: textWind.frame.height/2) // Rotation is done here imglayer.transform = CATransform3DMakeRotation(CGFloat((180 + wind.deg)) / 180.0 * .pi, 0.0, 0.0, -1.0) imglayer.contentsScale = self.contentsScale windBlock.frame.size = CGSize(width: textWind.frame.width+imglayer.frame.width, height: max(textWind.frame.height, imglayer.frame.height)) windBlock.addSublayer(imglayer) windBlock.anchorPoint = CGPoint(x: 0.5, y: 0) return windBlock } } ================================================ FILE: Aerial/Source/Views/Layers/Weather/ConditionSymbolLayer.swift ================================================ // // ConditionSymbolLayer.swift // Aerial // // Created by Guillaume Louel on 24/04/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa import Foundation class ConditionSymbolLayer: CALayer { let mainSymbols = [200: "cloud.bolt.rain", 201: "cloud.bolt.rain", 202: "cloud.bolt.rain", 210: "cloud.sun.bolt", 211: "cloud.bolt", 212: "cloud.bolt", 221: "cloud.bolt", 230: "cloud.bolt.rain", 231: "cloud.bolt.rain", 232: "cloud.bolt.rain", 300: "cloud.drizzle", 301: "cloud.drizzle", 302: "cloud.drizzle", 310: "cloud.drizzle", 311: "cloud.drizzle", 312: "cloud.drizzle", 313: "cloud.drizzle", 314: "cloud.drizzle", 321: "cloud.drizzle", 500: "cloud.sun.rain", 501: "cloud.rain", 502: "cloud.heavyrain", 503: "cloud.heavyrain", 504: "cloud.heavyrain", 511: "cloud.sleet", 520: "cloud.rain", 521: "cloud.rain", 522: "cloud.heavyrain", 531: "cloud.rain", 600: "snow", 601: "snow", 602: "cloud.snow", 611: "cloud.sleet", 612: "cloud.sleet", 613: "cloud.sleet", 615: "cloud.sleet", 616: "cloud.sleet", 620: "snow", 621: "snow", 622: "cloud.snow", 701: "sun.haze", 711: "smoke", 721: "sun.haze", 731: "sun.dust", 741: "sun.haze", 751: "sun.dust", 761: "sun.dust", 762: "sun.dust", 781: "tornado", 800: "sun.max", 801: "sun.max", 802: "cloud.sun", 803: "cloud.sun", 804: "cloud" ]// let nightSymbols = [210: "cloud.moon.bolt", 500: "cloud.moon.rain", 800: "moon.stars", 801: "moon", 802: "cloud.moon", 803: "cloud.moon" ] init(weather: OWWeather, dt: Int, isNight: Bool, size: Int, square: Bool = false) { super.init() var img: NSImage? switch PrefsInfo.weather.icons { case .flat: img = makeSymbol(name: getSymbol(condition: weather.id, isNight: isNight), size: size) case .colorflat: img = makeColorSymbol(name: getColorSymbol(condition: weather.id, isNight: isNight), size: size) case .oweather: downloadImage(from: URL(string: "https://openweathermap.org/img/wn/\(weather.icon)@4x.png")!, size: size) img = nil } if let img = img { if !square { frame.size.height = CGFloat(size) frame.size.width = CGFloat(size) * img.size.width / img.size.height } else { if frame.size.height > frame.size.width { frame.size.height = CGFloat(size) frame.size.width = CGFloat(size) * img.size.width / img.size.height } else { frame.size.width = CGFloat(size) frame.size.height = CGFloat(size) * img.size.height / img.size.width } } contents = img } } init(weather: OWWeather, dt: Int, sunrise: Int, sunset: Int, size: Int, square: Bool = false) { super.init() // In case icons are updated, it's important to test them ! // test() let isNight = isNight(dt: dt, sunrise: sunrise, sunset: sunset) var img: NSImage? switch PrefsInfo.weather.icons { case .flat: img = makeSymbol(name: getSymbol(condition: weather.id, isNight: isNight), size: size) case .colorflat: img = makeColorSymbol(name: getColorSymbol(condition: weather.id, isNight: isNight), size: size) case .oweather: downloadImage(from: URL(string: "https://openweathermap.org/img/wn/\(weather.icon)@4x.png")!, size: size) img = nil } if let img = img { if !square { frame.size.height = CGFloat(size) frame.size.width = CGFloat(size) * img.size.width / img.size.height } else { if frame.size.height > frame.size.width { frame.size.height = CGFloat(size) frame.size.width = CGFloat(size) * img.size.width / img.size.height } else { frame.size.width = CGFloat(size) frame.size.height = CGFloat(size) * img.size.height / img.size.width } } contents = img } } override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func makeSymbol(name: String, size: Int) -> NSImage? { if #available(macOS 11.0, *) { if let image = NSImage(systemSymbolName: name, accessibilityDescription: name) { image.isTemplate = true // return image let config = NSImage.SymbolConfiguration(pointSize: CGFloat(size), weight: .regular) return image.withSymbolConfiguration(config)?.tinting(with: .white) } } else { // We fallback on the pdf icons let imagePath = Bundle(for: PanelWindowController.self).path( forResource: name, ofType: "pdf") ?? "" let img = NSImage(contentsOfFile: imagePath) return img } return nil } func makeColorSymbol(name: String, size: Int) -> NSImage? { if #available(macOS 11.0, *) { if let image = NSImage(systemSymbolName: name, accessibilityDescription: name) { image.isTemplate = false // return image let config = NSImage.SymbolConfiguration(pointSize: CGFloat(size), weight: .regular) return image.withSymbolConfiguration(config) // ?.tinting(with: .white) } } return nil } func getData(from url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> Void) { URLSession.shared.dataTask(with: url, completionHandler: completion).resume() } func downloadImage(from url: URL, size: Int) { frame.size.height = CGFloat(size) frame.size.width = CGFloat(size) getData(from: url) { data, _, error in guard let data = data, error == nil else { return } DispatchQueue.main.async() { let img = NSImage(data: data) self.contents = img /* // If we have something, trim and put it up if let img = imgs { // Get the trimmed image first, goes on the left let trimmedimg = img.trim()! let imglayer = CALayer() imglayer.frame.size.height = trimmedimg.size.height / 2 imglayer.frame.size.width = trimmedimg.size.width / 2 imglayer.contents = trimmedimg imglayer.anchorPoint = CGPoint(x: 0, y: 0.5) imglayer.position = CGPoint(x: 0, y: 50) self.addSublayer(imglayer) let tempWidth = self.addTemperature(at: imglayer.frame.width + 15) self.addFeelsLike(at: imglayer.frame.width + 15 + (tempWidth / 2)) self.addWind(at: (imglayer.frame.width + 15 + tempWidth) / 2) // Set the final size self.frame.size = CGSize(width: imglayer.frame.width + 15 + tempWidth, height: 75) } */ } } } func test() { nightSymbols.forEach { (key: Int, value: String) in let imagePath = Bundle(for: PanelWindowController.self).path( forResource: getSymbol(condition: key, isNight: true), ofType: "pdf") if imagePath == nil { debugLog("ERROR night \(key) \(value)") } else { debugLog("OK night \(key) \(value)") } } mainSymbols.forEach { (key: Int, value: String) in let imagePath = Bundle(for: PanelWindowController.self).path( forResource: getSymbol(condition: key, isNight: true), ofType: "pdf") if imagePath == nil { debugLog("ERROR day \(key) \(value)") } else { debugLog("OK day \(key) \(value)") } } } func getSymbol(condition: Int, isNight: Bool) -> String { if isNight && nightSymbols[condition] != nil { return nightSymbols[condition]! } else { if mainSymbols[condition] != nil { return mainSymbols[condition]! } else { return "wrench" } } } func getColorSymbol(condition: Int, isNight: Bool) -> String { let regular = getSymbol(condition: condition, isNight: isNight) if regular != "wrench" && regular != "snow" && regular != "tornado" { return regular + ".fill" } else { return regular } } func isNight(dt: Int, sunrise: Int, sunset: Int) -> Bool { if dt < sunrise || dt > sunset { return true } else { return false } } } ================================================ FILE: Aerial/Source/Views/Layers/Weather/ForecastLayer.swift ================================================ // // ForecastLayer.swift // Aerial // // Created by Guillaume Louel on 23/03/2021. // Copyright © 2021 Guillaume Louel. All rights reserved. // import Foundation import AVKit // swiftlint:disable:next type_body_length class ForecastLayer: CALayer { var condition: ForecastElement? // swiftlint:disable:next cyclomatic_complexity init(condition: ForecastElement, scale: CGFloat) { self.condition = condition super.init() // backgroundColor = .init(gray: 0.2, alpha: 0.2) contentsScale = scale let size = PrefsInfo.weather.fontSize // We have daily forecasts, and hourly forecasts available (woo) if PrefsInfo.weather.mode == .forecast3days || PrefsInfo.weather.mode == .forecast5days { // How many days to display, currently we do 3 and 5 var days = 5 if PrefsInfo.weather.mode == .forecast3days { days = 3 } if let flist = condition.list { let breakIndex = detectDayChange(list: flist) if flist.count >= 40 { var height: CGFloat = 0 for dayidx in 0 ..< days { let start = breakIndex + (8 * (dayidx-1)) var day: CALayer if dayidx == 0 { day = makeDayBlock(slice: flist[0.. height { height = day.frame.height } } let legend = makeLegendBlock(size: size*2) legend.anchorPoint = CGPoint(x: 0, y: 0) legend.position = CGPoint(x: Int(size*2) * days, y: 0) self.addSublayer(legend) self.frame = CGRect(x: 0, y: 0, width: CGFloat(Double((days + 1)) * (size * 2)), height: height) } } } else { // Hourly forecast, we do 6 hours if let flist = condition.list { // Just in case if flist.count > 5 { var height: CGFloat = 0 for houridx in 0 ..< 6 { let day = makeHourBlock(hour: flist[houridx], size: size*2) day.anchorPoint = CGPoint(x: 0, y: 0) day.position = CGPoint(x: Int(size * 2) * houridx, y: 0) self.addSublayer(day) if day.frame.height > height { height = day.frame.height } } let legend = makeLegendBlock(size: size*2) legend.anchorPoint = CGPoint(x: 0, y: 0) legend.position = CGPoint(x: Int(size*2) * 6, y: 0) self.addSublayer(legend) self.frame = CGRect(x: 0, y: 0, width: CGFloat(7 * (size * 2)), height: height) } } } } func detectDayChange(list: [FList]) -> Int { var firstDay: String? var index = 0 for day in list { if firstDay == nil { firstDay = dayStringFromTimeStamp(timeStamp: Double(day.dt!)) } else { if firstDay != dayStringFromTimeStamp(timeStamp: Double(day.dt!)) { return index } } index += 1 } // fallback, uh... return 1 } override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func makeDayBlock(slice: ArraySlice, size: Double) -> CALayer { // This is ugly but we try and do the best from the data we get... var tmin, tmax: Double? var day: Int? let array = Array(slice) for element in array { if day == nil { day = element.dt } if tmin == nil { tmin = element.main!.tempMin } else { if element.main!.tempMin! < tmin! { tmin = element.main!.tempMin } } if tmax == nil { tmax = element.main!.tempMax } else { if element.main!.tempMax! > tmax! { tmax = element.main!.tempMax } } } let list = array[array.count/2] let weather = list.weather![0] let windSpeed = list.wind!.speed! let windDeg = list.wind!.deg! let whumidity = list.main!.humidity let mainLayer = CALayer() // First create the symbol let imglayer = ConditionSymbolLayer(weather: weather, dt: day!, isNight: false, size: Int(size), square: true) let windLayer = makeWindBlock(speed: windSpeed, degree: windDeg, size: size/4) let max = CAVCTextLayer() max.string = "\(String(format: "%.0f", tmax!))°" (max.font, max.fontSize) = max.makeFont(name: PrefsInfo.weather.fontName, size: size/4) // ReRect the temperature let rect = max.calculateRect(string: max.string as! String, font: max.font as! NSFont) max.frame = rect max.contentsScale = self.contentsScale max.alignmentMode = .center let min = CAVCTextLayer() min.string = "\(String(format: "%.0f", tmin!))°" (min.font, min.fontSize) = min.makeFont(name: PrefsInfo.weather.fontName, size: size/4) // ReRect the temperature let rect2 = min.calculateRect(string: min.string as! String, font: min.font as! NSFont) min.frame = rect2 min.contentsScale = self.contentsScale min.alignmentMode = .center let humidity = CAVCTextLayer() humidity.string = "\(whumidity!)%" (humidity.font, humidity.fontSize) = humidity.makeFont(name: PrefsInfo.weather.fontName, size: size/4) // ReRect the temperature let recth = humidity.calculateRect(string: humidity.string as! String, font: humidity.font as! NSFont) humidity.frame = recth humidity.contentsScale = self.contentsScale humidity.alignmentMode = .center let dayi = CAVCTextLayer() dayi.string = dayStringFromTimeStamp(timeStamp: Double(day!)) (dayi.font, dayi.fontSize) = dayi.makeFont(name: PrefsInfo.weather.fontName, size: size/4) // ReRect the temperature let rect4 = dayi.calculateRect(string: dayi.string as! String, font: dayi.font as! NSFont) dayi.frame = rect4 dayi.contentsScale = self.contentsScale dayi.alignmentMode = .center // Then we draw bottom to top dayi.anchorPoint = CGPoint(x: 0.5, y: 0) dayi.position = CGPoint(x: size/2, y: 0) mainLayer.addSublayer(dayi) var offset = dayi.frame.height if PrefsInfo.weather.showWind { windLayer.anchorPoint = CGPoint(x: 0.5, y: 0) windLayer.position = CGPoint(x: CGFloat(size)/2, y: offset) mainLayer.addSublayer(windLayer) offset += windLayer.frame.height } if PrefsInfo.weather.showHumidity { humidity.anchorPoint = CGPoint(x: 0.5, y: 0) humidity.position = CGPoint(x: CGFloat(size)/2, y: offset) mainLayer.addSublayer(humidity) offset += humidity.frame.height } min.anchorPoint = CGPoint(x: 0.5, y: 0) min.position = CGPoint(x: CGFloat(size)/2, y: offset) mainLayer.addSublayer(min) offset += min.frame.height max.anchorPoint = CGPoint(x: 0.5, y: 0) max.position = CGPoint(x: CGFloat(size) / 2, y: offset) mainLayer.addSublayer(max) offset += max.frame.height imglayer.anchorPoint = CGPoint(x: 0.5, y: 0.5) imglayer.position = CGPoint(x: Double(size) / 2, y: Double(offset) + size/2) mainLayer.addSublayer(imglayer) mainLayer.frame = CGRect(x: 0, y: 0, width: CGFloat(size), height: offset + imglayer.frame.height) return mainLayer } func makeHourBlock(hour: FList, size: Double) -> CALayer { let mainLayer = CALayer() let isNight = hour.sys!.pod! == "n" ? true : false // First create the symbol let imglayer = ConditionSymbolLayer(weather: hour.weather![0], dt: hour.dt!, isNight: isNight, size: Int(size), square: true) let windLayer = makeWindBlock(speed: hour.wind!.speed!, degree: hour.wind!.deg!, size: size/4) let temp = CAVCTextLayer() temp.string = "\(String(format: "%.0f", hour.main!.temp!))°" (temp.font, temp.fontSize) = temp.makeFont(name: PrefsInfo.weather.fontName, size: size/4) // ReRect the temperature let rect = temp.calculateRect(string: temp.string as! String, font: temp.font as! NSFont) temp.frame = rect temp.contentsScale = self.contentsScale temp.alignmentMode = .center let feelsLike = CAVCTextLayer() feelsLike.string = "\(String(format: "%.0f", hour.main!.feelsLike!))°" (feelsLike.font, feelsLike.fontSize) = feelsLike.makeFont(name: PrefsInfo.weather.fontName, size: size/4) // ReRect the temperature let rect2 = feelsLike.calculateRect(string: feelsLike.string as! String, font: feelsLike.font as! NSFont) feelsLike.frame = rect2 feelsLike.contentsScale = self.contentsScale feelsLike.alignmentMode = .center let humidity = CAVCTextLayer() humidity.string = "\(hour.main!.humidity!)%" (humidity.font, humidity.fontSize) = humidity.makeFont(name: PrefsInfo.weather.fontName, size: size/4) // ReRect the temperature let recth = humidity.calculateRect(string: humidity.string as! String, font: humidity.font as! NSFont) humidity.frame = recth humidity.contentsScale = self.contentsScale humidity.alignmentMode = .center let dayi = CAVCTextLayer() dayi.string = hourStringFromTimeStamp(timeStamp: Double(hour.dt!)) (dayi.font, dayi.fontSize) = dayi.makeFont(name: PrefsInfo.weather.fontName, size: size/4) // ReRect the temperature let rect4 = dayi.calculateRect(string: dayi.string as! String, font: dayi.font as! NSFont) dayi.frame = rect4 dayi.contentsScale = self.contentsScale dayi.alignmentMode = .center // Then we draw bottom to top dayi.anchorPoint = CGPoint(x: 0.5, y: 0) dayi.position = CGPoint(x: size/2, y: 0) mainLayer.addSublayer(dayi) var offset = dayi.frame.height if PrefsInfo.weather.showWind { windLayer.anchorPoint = CGPoint(x: 0.5, y: 0) windLayer.position = CGPoint(x: CGFloat(size)/2, y: offset) mainLayer.addSublayer(windLayer) offset += windLayer.frame.height } if PrefsInfo.weather.showHumidity { humidity.anchorPoint = CGPoint(x: 0.5, y: 0) humidity.position = CGPoint(x: CGFloat(size)/2, y: offset) mainLayer.addSublayer(humidity) offset += humidity.frame.height } feelsLike.anchorPoint = CGPoint(x: 0.5, y: 0) feelsLike.position = CGPoint(x: CGFloat(size)/2, y: offset) mainLayer.addSublayer(feelsLike) offset += feelsLike.frame.height temp.anchorPoint = CGPoint(x: 0.5, y: 0) temp.position = CGPoint(x: CGFloat(size) / 2, y: offset) mainLayer.addSublayer(temp) offset += temp.frame.height imglayer.anchorPoint = CGPoint(x: 0.5, y: 0.5) imglayer.position = CGPoint(x: Double(size) / 2, y: Double(offset) + size/2) mainLayer.addSublayer(imglayer) mainLayer.frame = CGRect(x: 0, y: 0, width: CGFloat(size), height: offset + imglayer.frame.height) return mainLayer } func makeLegendBlock(size: Double) -> CALayer { let mainLayer = CALayer() // Make a vertically centered layer for t° let windLayer = CAVCTextLayer() if PrefsInfo.weather.degree == .celsius { if PrefsInfo.weatherWindMode == .kph { windLayer.string = "km/h" } else { windLayer.string = "m/s" } } else { windLayer.string = "mph" } // Get something large first (windLayer.font, windLayer.fontSize) = windLayer.makeFont(name: PrefsInfo.weather.fontName, size: size/4) // ReRect the temperature let rect2 = windLayer.calculateRect(string: windLayer.string as! String, font: windLayer.font as! NSFont) windLayer.frame = rect2 windLayer.contentsScale = self.contentsScale windLayer.alignmentMode = .center let max = CAVCTextLayer() if PrefsInfo.weather.mode == .forecast6hours { max.string = "Temperature" } else { max.string = "Max" } (max.font, max.fontSize) = max.makeFont(name: PrefsInfo.weather.fontName, size: size/4) // ReRect the temperature let rect = max.calculateRect(string: max.string as! String, font: max.font as! NSFont) max.frame = rect max.contentsScale = self.contentsScale max.alignmentMode = .center let min = CAVCTextLayer() min.string = "Min" if PrefsInfo.weather.mode == .forecast6hours { min.string = "Feels Like" } else { min.string = "Min" } (min.font, min.fontSize) = min.makeFont(name: PrefsInfo.weather.fontName, size: size/4) // ReRect the temperature let rect3 = min.calculateRect(string: min.string as! String, font: min.font as! NSFont) min.frame = rect3 min.contentsScale = self.contentsScale min.alignmentMode = .center let humidity = CAVCTextLayer() humidity.string = "Humidity" (humidity.font, humidity.fontSize) = humidity.makeFont(name: PrefsInfo.weather.fontName, size: size/4) // ReRect the temperature let recth = humidity.calculateRect(string: humidity.string as! String, font: humidity.font as! NSFont) humidity.frame = recth humidity.contentsScale = self.contentsScale humidity.alignmentMode = .center // Then we draw bottom to top, we skip the first which shows the hour/day var offset: CGFloat = min.frame.height if PrefsInfo.weather.showWind { windLayer.anchorPoint = CGPoint(x: 0.5, y: 0) windLayer.position = CGPoint(x: CGFloat(size)/2, y: offset) mainLayer.addSublayer(windLayer) offset += windLayer.frame.height } if PrefsInfo.weather.showHumidity { humidity.anchorPoint = CGPoint(x: 0.5, y: 0) humidity.position = CGPoint(x: CGFloat(size)/2, y: offset) mainLayer.addSublayer(humidity) offset += humidity.frame.height } min.anchorPoint = CGPoint(x: 0.5, y: 0) min.position = CGPoint(x: CGFloat(size)/2, y: offset) mainLayer.addSublayer(min) offset += min.frame.height max.anchorPoint = CGPoint(x: 0.5, y: 0) max.position = CGPoint(x: CGFloat(size) / 2, y: offset) mainLayer.addSublayer(max) // offset += max.frame.height return mainLayer } func makeWindBlock(speed: Double, degree: Int, size: Double) -> CALayer { let windLayer = CALayer() // Make a vertically centered layer for t° let wind = CAVCTextLayer() if PrefsInfo.weatherWindMode == .kph && PrefsInfo.weather.degree == .celsius { wind.string = String(format: "%.0f", speed * 3.6) } else { wind.string = String(format: "%.0f", speed) } // Get something large first (wind.font, wind.fontSize) = wind.makeFont(name: PrefsInfo.weather.fontName, size: size) // ReRect the temperature let rect2 = wind.calculateRect(string: wind.string as! String, font: wind.font as! NSFont) wind.frame = rect2 wind.contentsScale = self.contentsScale // Create the wind indicator let imglayer = WindDirectionLayer(direction: 225, size: CGFloat(size/1.27)) imglayer.contentsScale = self.contentsScale imglayer.transform = CATransform3DMakeRotation(CGFloat((180 + degree)) / 180.0 * .pi, 0.0, 0.0, -1.0) imglayer.anchorPoint = CGPoint(x: 0.5, y: 0.5) imglayer.position = CGPoint(x: imglayer.frame.width/2, y: wind.frame.height/2) windLayer.addSublayer(imglayer) // We put the temperature at the right of the weather icon wind.anchorPoint = CGPoint(x: 0, y: 0) wind.position = CGPoint(x: imglayer.frame.width + 3, y: 0) windLayer.addSublayer(wind) // Reset the container frame windLayer.frame = CGRect(x: 0, y: 0, width: imglayer.frame.width + wind.frame.width + 3, height: wind.frame.height) return windLayer } func dayStringFromTimeStamp(timeStamp: Double) -> String { let date = Date(timeIntervalSince1970: timeStamp) let dateFormatter = DateFormatter() var locale = Locale(identifier: Locale.preferredLanguages[0]) if PrefsAdvanced.ciOverrideLanguage != "" { locale = Locale(identifier: PrefsAdvanced.ciOverrideLanguage) } dateFormatter.locale = locale dateFormatter.dateFormat = "E" return dateFormatter.string(from: date) } func hourStringFromTimeStamp(timeStamp: Double) -> String { let date = Date(timeIntervalSince1970: timeStamp) let dateFormatter = DateFormatter() var locale = Locale(identifier: Locale.preferredLanguages[0]) if PrefsAdvanced.ciOverrideLanguage != "" { locale = Locale(identifier: PrefsAdvanced.ciOverrideLanguage) } dateFormatter.locale = locale dateFormatter.dateFormat = "HH" return dateFormatter.string(from: date) + "h" } } extension Double { func roundTemp() -> Double { if PrefsInfo.weather.degree == .celsius { return self.rounded(toPlaces: 0) // rounded(toPlaces: 1) } else { return self.rounded() } } } ================================================ FILE: Aerial/Source/Views/Layers/Weather/WeatherLayer.swift ================================================ // // WeatherLayer.swift // Aerial // // Created by Guillaume Louel on 16/04/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation import AVKit class WeatherLayer: AnimationLayer { var config: PrefsInfo.Weather? var wasSetup = false var todayCond: ConditionLayer? var forecastCond: ForecastLayer? var cscale: CGFloat? private static let cachedWeatherURL = URL(fileURLWithPath: Cache.supportPath, isDirectory: true).appendingPathComponent("Weather.json") private static let cachedWeatherForecastURL = URL(fileURLWithPath: Cache.supportPath, isDirectory: true).appendingPathComponent("Forecast.json") private static let cachedWeatherUpdateInterval: TimeInterval = 60 * 15 var cachedWeather: OWeather? { get { let fm = FileManager.default guard fm.fileExists(atPath: WeatherLayer.cachedWeatherURL.path) else { return nil } do { guard let date = try fm.attributesOfItem(atPath: WeatherLayer.cachedWeatherURL.path)[.modificationDate] as? Date else { assertionFailure("Couldn't get modificationDate from File System!") return nil } // Make sure the cache was written in the last "update interval" seconds, otherwise download now guard date >= Date().addingTimeInterval(-WeatherLayer.cachedWeatherUpdateInterval) else { return nil } let data = try Data(contentsOf: WeatherLayer.cachedWeatherURL) let result = try JSONDecoder().decode(OWeather.self, from: data) return result } catch { // Handle error assertionFailure("Error decoding Weather: \(error.localizedDescription)") return nil } } set { do { guard let newValue else { /* Don't store nil */ return } let data = try JSONEncoder().encode(newValue) try FileManager.default.createDirectory(atPath: Cache.supportPath, withIntermediateDirectories: true) try data.write(to: Self.cachedWeatherURL) } catch { // Handle error assertionFailure("Error encoding Weather: \(error.localizedDescription)") } } } var cachedForecast: ForecastElement? { get { let fm = FileManager.default guard fm.fileExists(atPath: WeatherLayer.cachedWeatherForecastURL.path) else { return nil } do { guard let date = try fm.attributesOfItem(atPath: WeatherLayer.cachedWeatherForecastURL.path)[.modificationDate] as? Date else { assertionFailure("Couldn't get modificationDate from File System!") return nil } // Make sure the cache was written in the last "update interval" seconds, otherwise download now guard date >= Date().addingTimeInterval(-WeatherLayer.cachedWeatherUpdateInterval) else { return nil } let data = try Data(contentsOf: WeatherLayer.cachedWeatherForecastURL) let result = try JSONDecoder().decode(ForecastElement.self, from: data) return result } catch { // Handle error assertionFailure("Error decoding Weather: \(error.localizedDescription)") return nil } } set { do { guard let newValue else { /* Don't store nil */ return } let data = try JSONEncoder().encode(newValue) try FileManager.default.createDirectory(atPath: Cache.supportPath, withIntermediateDirectories: true) try data.write(to: Self.cachedWeatherForecastURL) } catch { // Handle error assertionFailure("Error encoding Weather: \(error.localizedDescription)") } } } override init(layer: Any) { super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // Our inits override init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager) { super.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) // Always on layers should start with full opacity self.opacity = 1 } convenience init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager, config: PrefsInfo.Weather) { self.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) self.config = config /* // Set our layer's font & corner now (self.font, self.fontSize) = getFont(name: config.fontName, size: config.fontSize)*/ self.corner = config.corner } override func setContentScale(scale: CGFloat) { if let todayCond = todayCond { todayCond.contentsScale = scale if todayCond.sublayers != nil { for layer in todayCond.sublayers! { layer.contentsScale = scale } } } if let forecastCond = forecastCond { forecastCond.contentsScale = scale } // In case we haven't called displayWeatherBlock yet (should be all the time but hmm) cscale = scale } // Called at each new video, we only setup once though ! override func setupForVideo(video: AerialVideo, player: AVPlayer) { // Only run this once if !wasSetup { wasSetup = true frame.size = CGSize(width: 100, height: 1) update() } if PrefsInfo.weather.mode == .current { if cachedWeather != nil { debugLog("weather using cache") displayWeatherBlock() } else { debugLog("fetching fresh weather") OpenWeather.fetch { result in switch result { case .success(let openWeather): self.cachedWeather = openWeather self.displayWeatherBlock() case .failure(let error): debugLog(error.localizedDescription) } } } } else { if cachedForecast != nil && cachedWeather != nil { debugLog("weather using cache") displayWeatherBlock() } else { debugLog("fetching fresh weather") Forecast.fetch { result in switch result { case .success(let openWeather): self.cachedForecast = openWeather // self.displayWeatherBlock() OpenWeather.fetch { result in switch result { case .success(let openWeather): self.cachedWeather = openWeather self.displayWeatherBlock() case .failure(let error): debugLog(error.localizedDescription) } } case .failure(let error): debugLog(error.localizedDescription) } } } } } func displayWeatherBlock() { guard cachedWeather != nil || cachedForecast != nil else { errorLog("No weather info in dWB please report") return } todayCond?.removeFromSuperlayer() todayCond = nil forecastCond?.removeFromSuperlayer() forecastCond = nil if PrefsInfo.weather.mode == .current { todayCond = ConditionLayer(condition: cachedWeather!, scale: contentsScale) if cscale != nil { todayCond!.contentsScale = cscale! } todayCond!.anchorPoint = CGPoint(x: 0, y: 0) todayCond!.position = CGPoint(x: 0, y: 10) addSublayer(todayCond!) self.frame.size = todayCond!.frame.size } else { todayCond = ConditionLayer(condition: cachedWeather!, scale: contentsScale) if cscale != nil { todayCond!.contentsScale = cscale! } todayCond!.anchorPoint = CGPoint(x: 0, y: 0) addSublayer(todayCond!) forecastCond = ForecastLayer(condition: cachedForecast!, scale: contentsScale) if cscale != nil { forecastCond!.contentsScale = cscale! } forecastCond!.anchorPoint = CGPoint(x: 0, y: 0) forecastCond!.position = CGPoint(x: todayCond!.frame.width, y: 10) addSublayer(forecastCond!) todayCond!.position = CGPoint(x: 0, y: forecastCond!.frame.height - todayCond!.frame.height + 10) self.frame.size = CGSize(width: todayCond!.frame.width + forecastCond!.frame.width, height: forecastCond!.frame.height) // self.frame.size = forecastCond!.frame.size } update(redraw: true) let fadeAnimation = self.createFadeInAnimation() add(fadeAnimation, forKey: "weatherfade") } } ================================================ FILE: Aerial/Source/Views/Layers/Weather/WindDirectionLayer.swift ================================================ // // WindDirectionLayer.swift // Aerial // // Created by Guillaume Louel on 05/03/2021. // Copyright © 2021 Guillaume Louel. All rights reserved. // import Foundation import AVKit class WindDirectionLayer: CALayer { init(direction: CGFloat, size: CGFloat) { super.init() let imagePath = Bundle(for: PanelWindowController.self).path( forResource: "location.north", ofType: "pdf") let img = NSImage(contentsOfFile: imagePath!) frame.size.height = size frame.size.width = size contents = img } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } ================================================ FILE: Aerial/Source/Views/Layers/Weather/YahooLogoLayer.swift ================================================ // // YahooLogoLayer.swift // Aerial // CALayer for Yahoo logo (attribution is required for API access) // // Created by Guillaume Louel on 17/04/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Foundation import AVKit class YahooLayer: CALayer { override init() { super.init() let imagePath = Bundle(for: PanelWindowController.self).path( forResource: "white_retina", ofType: "png") let img = NSImage(contentsOfFile: imagePath!) frame.size.height = img!.size.height / 3 frame.size.width = img!.size.width / 3 contents = img } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } ================================================ FILE: Aerial/Source/Views/MainUI/AspectFillNSImageView.swift ================================================ // // AspectFillNSImageView.swift // Aerial // // Created by Guillaume Louel on 20/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa open class AspectFillNSImageView: NSImageView { open override var image: NSImage? { get { return super.image } set { self.layer = CALayer() if #available(macOS 10.14, *) { self.layer?.contentsGravity = CALayerContentsGravity.resizeAspectFill } else { // Fallback on earlier versions } self.layer?.contents = newValue self.wantsLayer = true super.image = newValue } } public override init(frame frameRect: NSRect) { super.init(frame: frameRect) } // the image setter isn't called when loading from a storyboard // manually set the image if it is already set required public init?(coder: NSCoder) { super.init(coder: coder) if let theImage = image { self.image = theImage } } } ================================================ FILE: Aerial/Source/Views/MainUI/NowPlayingCollectionView.swift ================================================ // // NowPlayingCollectionView.swift // Aerial // // Created by Guillaume Louel on 18/08/2022. // Copyright © 2022 Guillaume Louel. All rights reserved. // import Cocoa class NowPlayingCollectionView: NSCollectionView { var clickedIndex: Int? override func menu(for event: NSEvent) -> NSMenu? { clickedIndex = nil let point = convert(event.locationInWindow, from: nil) for index in 0.. NSMenu? } class SidebarOutlineView: NSOutlineView { /*override func menu(for event: NSEvent) -> NSMenu? { let point = self.convert(event.locationInWindow, from: nil) let row = self.row(at: point) if let item = self.item(atRow: row) { return (self.delegate as! SidebarOutlineViewDelegate).outlineView(outlineView: self, menuForItem: item) } return nil }*/ } ================================================ FILE: Aerial/Source/Views/MainUI/VideoCellView.swift ================================================ // // VideoCellView.swift // Aerial // // Created by Guillaume Louel on 16/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class VideoCellView: NSTableCellView { @IBOutlet var thumbView: NSImageView! @IBOutlet var label: NSTextField! @IBOutlet var checkButton: NSButton! @IBOutlet var downloadButton: NSButton! // var delegate: VideoCellViewDelegate? var video: AerialVideo? override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) checkButton.target = self checkButton.action = #selector(self.didChangeState(_:)) checkButton.image = Aerial.helper.getSymbol("star")!.tinting(with: .white) checkButton.alternateImage = Aerial.helper.getSymbol("star.fill")!.tinting(with: .white) let shadow: NSShadow = NSShadow() shadow.shadowBlurRadius = 2 shadow.shadowOffset = NSSize(width: 0, height: 2) shadow.shadowColor = NSColor.black checkButton.shadow = shadow downloadButton.shadow = shadow // Drawing code here. } // Notify the delegate that the checkbox's state has changed @objc private func didChangeState(_ sender: NSObject) { if PrefsVideos.favorites.contains(video!.id) { PrefsVideos.favorites.remove(at: PrefsVideos.favorites.firstIndex(of: video!.id)!) } else { if !video!.isAvailableOffline { Cache.ensureDownload { PrefsVideos.favorites.append(self.video!.id) VideoManager.sharedInstance.queueDownload(self.video!) } } else { PrefsVideos.favorites.append(self.video!.id) } } } @IBAction func downloadButtonClick(_ sender: NSButton) { let videoManager = VideoManager.sharedInstance Cache.ensureDownload { videoManager.queueDownload(self.video!) } } } ================================================ FILE: Aerial/Source/Views/PrefPanel/CheckCellView.swift ================================================ // // CheckCellView.swift // Aerial // // Created by John Coates on 10/24/15. // Copyright © 2015 John Coates. All rights reserved. // import Cocoa enum VideoStatus { case unknown, notAvailable, queued, downloading, downloaded } final class CheckCellView: NSTableCellView { @IBOutlet var checkButton: NSButton! @IBOutlet var addButton: NSButton! @IBOutlet var progressIndicator: NSProgressIndicator! @IBOutlet var formatLabel: NSTextField! @IBOutlet var queuedImage: NSImageView! @IBOutlet var mainTextField: NSTextField! var onCheck: ((Bool) -> Void)? var video: (AerialVideo)? var status = VideoStatus.unknown override required init(frame frameRect: NSRect) { super.init(frame: frameRect) } required init?(coder: NSCoder) { super.init(coder: coder) } override func awakeFromNib() { checkButton.target = self checkButton.action = #selector(CheckCellView.check(_:)) } @objc func check(_ button: AnyObject?) { guard let onCheck = self.onCheck else { return } onCheck(checkButton.state == NSControl.StateValue.on) } override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) } func adaptIndicators() { let videoManager = VideoManager.sharedInstance if #available(OSX 10.12.2, *) { queuedImage.image = NSImage(named: NSImage.touchBarDownloadTemplateName) } if video!.isAvailableOffline { status = .downloaded addButton.isHidden = true progressIndicator.isHidden = true queuedImage.isHidden = true } else if videoManager.isVideoQueued(id: video!.id) { status = .queued addButton.isHidden = true progressIndicator.isHidden = true queuedImage.isHidden = false } else { status = .notAvailable addButton.isHidden = false progressIndicator.isHidden = true queuedImage.isHidden = true } // formatLabel.isHidden = !(video!.has4KVersion()) } func updateProgressIndicator(progress: Double) { if status != .downloading { addButton.isHidden = true progressIndicator.isHidden = false queuedImage.isHidden = true status = .downloading } progressIndicator.doubleValue = Double(progress) } // Add video handling func setVideo(video: AerialVideo) { self.video = video } func markAsDownloaded() { addButton.isHidden = true progressIndicator.isHidden = true queuedImage.isHidden = true status = .downloaded 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!)") status = .queued addButton.isHidden = true progressIndicator.isHidden = true queuedImage.isHidden = false } func queueVideo() { let videoManager = VideoManager.sharedInstance Cache.ensureDownload { videoManager.queueDownload(self.video!) } } @IBAction func addClick(_ button: NSButton?) { queueVideo() } } final class VerticallyAlignedTextFieldCell: NSTextFieldCell { override func drawingRect(forBounds rect: NSRect) -> NSRect { let newRect = NSRect(x: 0, y: (rect.size.height - 20) / 2, width: rect.size.width, height: 20) return super.drawingRect(forBounds: newRect) } } ================================================ FILE: Aerial/Source/Views/PrefPanel/DisplayView.swift ================================================ // // DisplayView.swift // Aerial // // Created by Guillaume Louel on 09/05/2019. // Copyright © 2019 John Coates. All rights reserved. // import Foundation import Cocoa class DisplayPreview: NSObject { var screen: Screen var previewRect: CGRect init(screen: Screen, previewRect: CGRect) { self.screen = screen self.previewRect = previewRect } } extension NSImage { func flipped(flipHorizontally: Bool = false, flipVertically: Bool = false) -> NSImage { let flippedImage = NSImage(size: size) flippedImage.lockFocus() NSGraphicsContext.current?.imageInterpolation = .high let transform = NSAffineTransform() transform.translateX(by: flipHorizontally ? size.width : 0, yBy: flipVertically ? size.height : 0) transform.scaleX(by: flipHorizontally ? -1 : 1, yBy: flipVertically ? -1 : 1) transform.concat() draw(at: .zero, from: NSRect(origin: .zero, size: size), operation: .sourceOver, fraction: 1) flippedImage.unlockFocus() return flippedImage } } class DisplayView: NSView { // We store our computed previews here var displayPreviews = [DisplayPreview]() // MARK: - Lifecycle override init(frame: CGRect) { super.init(frame: frame) } required init?(coder: NSCoder) { super.init(coder: coder) } // MARK: - Drawing // swiftlint:disable:next cyclomatic_complexity override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) // We need to handle dark mode var backgroundColor = NSColor.init(white: 0.9, alpha: 1.0) var borderColor = NSColor.init(white: 0.8, alpha: 1.0) // let screenColor = NSColor.init(red: 0.38, green: 0.60, blue: 0.85, alpha: 1.0) let screenBorderColor = NSColor.black if DarkMode.isEnabled() { backgroundColor = NSColor.init(white: 0.2, alpha: 1.0) borderColor = NSColor.init(white: 0.6, alpha: 1.0) } // Draw background with a 1pt border borderColor.setFill() __NSRectFill(dirtyRect) let path = NSBezierPath(rect: dirtyRect.insetBy(dx: 1, dy: 1)) backgroundColor.setFill() path.fill() let displayDetection = DisplayDetection.sharedInstance displayPreviews = [DisplayPreview]() // Empty the array in case we redraw // In order to draw the screen we need to know the total size of all // the displays together let globalRect = displayDetection.getGlobalScreenRect() var minX: CGFloat, minY: CGFloat, maxX: CGFloat, maxY: CGFloat, scaleFactor: CGFloat if (frame.width / frame.height) > (globalRect.width / globalRect.height) { // We fill vertically then maxY = frame.height - 60 minY = 30 scaleFactor = globalRect.height / maxY maxX = globalRect.width / scaleFactor minX = (frame.width - maxX)/2 } else { // We fill horizontally maxX = frame.width - 60 minX = 30 scaleFactor = globalRect.width / maxX maxY = globalRect.height / scaleFactor minY = (frame.height - maxY)/2 } // In spanned mode, we start by a faint full view of the span if PrefsDisplays.viewingMode == .spanned { let activeRect = displayDetection.getZeroedActiveSpannedRect() debugLog("spanned active rect \(activeRect)") let activeSRect = NSRect(x: minX + (activeRect.origin.x/scaleFactor), y: minY + (activeRect.origin.y/scaleFactor), width: activeRect.width/scaleFactor, height: activeRect.height/scaleFactor) let bundle = Bundle(for: PanelWindowController.self) if let imagePath = bundle.path(forResource: "screen0", ofType: "jpg") { let image = NSImage(contentsOfFile: imagePath) image!.draw(in: activeSRect, from: calcScreenshotRect(src: activeSRect), operation: NSCompositingOperation.copy, fraction: 0.1) } else { errorLog("\(#file) screenshot is missing!!!") } } var idx = 0 var shouldFlip = true // Now we draw each individual screen for screen in displayDetection.screens { let sRect = NSRect(x: minX + (screen.zeroedOrigin.x/scaleFactor), y: minY + (screen.zeroedOrigin.y/scaleFactor), width: screen.bottomLeftFrame.width/scaleFactor, height: screen.bottomLeftFrame.height/scaleFactor) let sPath = NSBezierPath(rect: sRect) screenBorderColor.setFill() sPath.fill() let sInRect = sRect.insetBy(dx: 1, dy: 1) if PrefsDisplays.viewingMode != .spanned { if displayDetection.isScreenActive(id: screen.id) { let bundle = Bundle(for: PanelWindowController.self) if let imagePath = bundle.path(forResource: "screen"+String(idx), ofType: "jpg") { var image = NSImage(contentsOfFile: imagePath) if PrefsDisplays.viewingMode == .mirrored && shouldFlip { image = image?.flipped(flipHorizontally: true, flipVertically: false) } shouldFlip = !shouldFlip image!.draw(in: sInRect, from: calcScreenshotRect(src: sInRect), operation: NSCompositingOperation.copy, fraction: 1.0) } else { errorLog("\(#file) screenshot is missing!!!") } // Show difference images in independant mode to simulate if PrefsDisplays.viewingMode == .independent { if idx < 2 { idx += 1 } else { idx = 0 } } } else { // If the screen is innactive we fill it with a near black color let sInPath = NSBezierPath(rect: sInRect) let grey = NSColor(white: 0.1, alpha: 1.0) grey.setFill() sInPath.fill() } } else { // Spanned mode if displayDetection.isScreenActive(id: screen.id) { // Calculate which portion of the image to display let activeRect = displayDetection.getZeroedActiveSpannedRect() let activeSRect = NSRect(x: minX + (activeRect.origin.x/scaleFactor), y: minY + (activeRect.origin.y/scaleFactor), width: activeRect.width/scaleFactor, height: activeRect.height/scaleFactor) let ssRect = calcScreenshotRect(src: activeSRect) let xFactor = ssRect.width / activeSRect.width let yFactor = ssRect.height / activeSRect.height // ... let sFRect = CGRect(x: (sInRect.origin.x - activeSRect.origin.x) * xFactor + ssRect.origin.x, y: (sInRect.origin.y - activeSRect.origin.y) * yFactor + ssRect.origin.y, width: sInRect.width*xFactor, height: sInRect.height*yFactor) let bundle = Bundle(for: PanelWindowController.self) if let imagePath = bundle.path(forResource: "screen0", ofType: "jpg") { let image = NSImage(contentsOfFile: imagePath) // image!.draw(in: sInRect) image!.draw(in: sInRect, from: sFRect, operation: NSCompositingOperation.copy, fraction: 1.0) } else { errorLog("\(#file) screenshot is missing!!!") } } } // We preserve those calculations to handle our clicking logic displayPreviews.append(DisplayPreview(screen: screen, previewRect: sInRect)) // We put a white bar on the main screen if screen.isMain { let mainRect = CGRect(x: sRect.origin.x, y: sRect.origin.y + sRect.height-8, width: sRect.width, height: 8) let sMainPath = NSBezierPath(rect: mainRect) NSColor.black.setFill() sMainPath.fill() let sMainInPath = NSBezierPath(rect: mainRect.insetBy(dx: 1, dy: 1)) NSColor.white.setFill() sMainInPath.fill() } } } // Helper to keep aspect ratio of screenshots to be displayed func calcScreenshotRect(src: CGRect) -> CGRect { var minX: CGFloat, minY: CGFloat, maxX: CGFloat, maxY: CGFloat, scaleFactor: CGFloat let imgw: CGFloat = 720 let imgh: CGFloat = 400 if (imgw/imgh) < (src.width/src.height) { minX = 0 maxX = imgw scaleFactor = src.width / maxX maxY = src.height / scaleFactor minY = (imgh - maxY)/2 } else { minY = 0 maxY = imgh scaleFactor = src.height / maxY maxX = src.width / scaleFactor minX = (imgw - maxX)/2 } return CGRect(x: minX, y: minY, width: maxX, height: maxY) } // MARK: - Clicking override func mouseDown(with event: NSEvent) { let displayDetection = DisplayDetection.sharedInstance // Grab relative location of the click in view let point = convert(event.locationInWindow, from: nil) // If in selection mode, toggle the screen & redraw if PrefsDisplays.displayMode == .selection { for displayPreview in displayPreviews { if displayPreview.previewRect.contains(point) { if displayDetection.isScreenActive(id: displayPreview.screen.id) { displayDetection.unselectScreen(id: displayPreview.screen.id) } else { displayDetection.selectScreen(id: displayPreview.screen.id) } debugLog("Clicked on \(displayPreview.screen.id)") self.needsDisplay = true } } } } } ================================================ FILE: Aerial/Source/Views/PrefPanel/InfoBatteryView.swift ================================================ // // InfoBatteryView.swift // Aerial // // Created by Guillaume Louel on 27/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Cocoa class InfoBatteryView: NSView { @IBOutlet var hideWhenFull: NSButton! // Init(ish) func setStates() { hideWhenFull.state = PrefsInfo.battery.disableWhenFull ? .on : .off } @IBAction func hideWhenFullCheck(_ sender: NSButton) { let onState = sender.state == .on PrefsInfo.battery.disableWhenFull = onState } } ================================================ FILE: Aerial/Source/Views/PrefPanel/InfoClockView.swift ================================================ // // InfoClockView.swift // Aerial // // Created by Guillaume Louel on 18/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Cocoa class InfoClockView: NSView { @IBOutlet var secondsCheckbox: NSButton! @IBOutlet var hideAmPmCheckbox: NSButton! @IBOutlet var clockFormat: NSPopUpButton! @IBOutlet var customTimeFormatField: NSTextField! // Init(ish) func setStates() { secondsCheckbox.state = PrefsInfo.clock.showSeconds ? .on : .off hideAmPmCheckbox.state = PrefsInfo.clock.hideAmPm ? .on : .off clockFormat.selectItem(at: PrefsInfo.clock.clockFormat.rawValue) customTimeFormatField.stringValue = PrefsInfo.customTimeFormat updateAmPmCheckbox() } @IBAction func secondsClick(_ sender: NSButton) { let onState = sender.state == .on PrefsInfo.clock.showSeconds = onState } @IBAction func hideAmPmCheckboxClick(_ sender: NSButton) { let onState = sender.state == .on PrefsInfo.clock.hideAmPm = onState } @IBAction func clockFormatChange(_ sender: NSPopUpButton) { PrefsInfo.clock.clockFormat = InfoClockFormat(rawValue: sender.indexOfSelectedItem)! updateAmPmCheckbox() } // Update the 12/24hr visibility func updateAmPmCheckbox() { switch PrefsInfo.clock.clockFormat { case .tdefault: hideAmPmCheckbox.isHidden = false // meh secondsCheckbox.isHidden = false case .t12hours: hideAmPmCheckbox.isHidden = false secondsCheckbox.isHidden = false case .t24hours: hideAmPmCheckbox.isHidden = true secondsCheckbox.isHidden = false case .custom: hideAmPmCheckbox.isHidden = true secondsCheckbox.isHidden = true } customTimeFormatField.isHidden = !(PrefsInfo.clock.clockFormat == .custom) } @IBAction func customTimeFormatFieldChange(_ sender: NSTextField) { PrefsInfo.customTimeFormat = sender.stringValue } } ================================================ FILE: Aerial/Source/Views/PrefPanel/InfoCommonView.swift ================================================ // // InfoCommonView.swift // Aerial // // Created by Guillaume Louel on 17/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Cocoa class InfoCommonView: NSView { var forType: InfoType = .location var controller: OverlaysViewController? @IBOutlet var enabledButton: NSButton! @IBOutlet var fontLabel: NSTextField! @IBOutlet var displaysPopup: NSPopUpButton! @IBOutlet var posTopLeft: NSButton! @IBOutlet var posTopCenter: NSButton! @IBOutlet var posTopRight: NSButton! @IBOutlet var posBottomLeft: NSButton! @IBOutlet var posBottomCenter: NSButton! @IBOutlet var posBottomRight: NSButton! @IBOutlet var posScreenCenter: NSButton! @IBOutlet var posRandom: NSButton! // MARK: - init(ish) // This is what tells us what we are editing exactly func setType(_ forType: InfoType, controller: OverlaysViewController) { // We need the controller for callbacks, when we update the isEnabled state, // we need to update the list view on the left too self.controller = controller // Store type self.forType = forType // Update our states enabledButton.state = PrefsInfo.ofType(forType).isEnabled ? .on : .off setPosition(PrefsInfo.ofType(forType).corner) displaysPopup.selectItem(at: PrefsInfo.ofType(forType).displays.rawValue) fontLabel.stringValue = PrefsInfo.ofType(forType).fontName + ", \(PrefsInfo.ofType(forType).fontSize) pt" switch forType { case .location: //controller.infoBox.title = "Video location information" posRandom.isHidden = false case .message: //controller.infoBox.title = "Custom message" posRandom.isHidden = true case .clock: //controller.infoBox.title = "Current time" posRandom.isHidden = true case .date: //controller.infoBox.title = "Current date" posRandom.isHidden = true case .battery: //controller.infoBox.title = "Battery status" posRandom.isHidden = true case .updates: //controller.infoBox.title = "Updates notifications" posRandom.isHidden = true case .weather: //controller.infoBox.title = "Weather provided by OpenWeather" posRandom.isHidden = true case .countdown: //controller.infoBox.title = "Countdown to a time/date" posRandom.isHidden = true case .timer: //controller.infoBox.title = "Timer" posRandom.isHidden = true case .music: //controller.infoBox.title = "Music" posRandom.isHidden = true } } // MARK: - Position on screen func setPosition(_ corner: InfoCorner) { switch corner { case .topLeft: posTopLeft.state = .on case .topCenter: posTopCenter.state = .on case .topRight: posTopRight.state = .on case .bottomLeft: posBottomLeft.state = .on case .bottomCenter: posBottomCenter.state = .on case .bottomRight: posBottomRight.state = .on case .screenCenter: posScreenCenter.state = .on case .random: posRandom.state = .on case .absTopRight: posTopRight.state = .on } } @IBAction func changePosition(_ sender: NSButton) { var pos: InfoCorner // Which button ? switch sender { case posTopLeft: pos = .topLeft case posTopCenter: pos = .topCenter case posTopRight: pos = .topRight case posBottomLeft: pos = .bottomLeft case posBottomCenter: pos = .bottomCenter case posBottomRight: pos = .bottomRight case posScreenCenter: pos = .screenCenter case posRandom: pos = .random default: pos = .bottomLeft } // Then set pref PrefsInfo.setCorner(forType, corner: pos) } // MARK: - Displays it should appear on @IBAction func changeDisplays(_ sender: NSPopUpButton) { PrefsInfo.setDisplayMode(forType, mode: InfoDisplays(rawValue: sender.indexOfSelectedItem)!) } // MARK: - enabled @IBAction func enabledClick(_ sender: NSButton) { PrefsInfo.setEnabled(forType, value: sender.state == .on) // We need to update the side column! controller!.infoTableView.reloadDataKeepingSelection() } // MARK: - Font picker @IBAction func changeFontClick(_ sender: Any) { // Make sure we get the callback NSFontManager.shared.target = self // Make a panel if let fp = NSFontManager.shared.fontPanel(true) { fp.setPanelFont(makeFont(name: PrefsInfo.ofType(forType).fontName, size: PrefsInfo.ofType(forType).fontSize), isMultiple: false) // Push the panel fp.makeKeyAndOrderFront(sender) } } func makeFont(name: String, size: Double) -> NSFont { if let font = NSFont(name: name, size: CGFloat(size)) { return font } else { // This is probably enough return NSFont(name: "Helvetica Neue Medium", size: 28)! } } @IBAction func resetFontClick(_ sender: Any) { // We use a default font for all types PrefsInfo.setFontName(forType, name: "Helvetica Neue Medium") // Default Size varies though per type switch forType { case .location: PrefsInfo.location.fontSize = 28 case .message: PrefsInfo.message.fontSize = 20 case .clock: PrefsInfo.clock.fontSize = 50 case .date: PrefsInfo.date.fontSize = 20 case .battery: PrefsInfo.battery.fontSize = 20 case .updates: PrefsInfo.updates.fontSize = 20 case .weather: PrefsInfo.weather.fontSize = 20 case .countdown: PrefsInfo.countdown.fontSize = 100 case .timer: PrefsInfo.timer.fontSize = 100 case .music: PrefsInfo.music.fontSize = 20 } fontLabel.stringValue = PrefsInfo.ofType(forType).fontName + ", \(PrefsInfo.ofType(forType).fontSize) pt" } } // MARK: - Font Panel Delegates extension InfoCommonView: NSFontChanging { func validModesForFontPanel(_ fontPanel: NSFontPanel) -> NSFontPanel.ModeMask { return [.size, .collection, .face] } func changeFont(_ sender: NSFontManager?) { // Set current font let oldFont = makeFont(name: PrefsInfo.ofType(forType).fontName, size: PrefsInfo.ofType(forType).fontSize) if let newFont = sender?.convert(oldFont) { PrefsInfo.setFontName(forType, name: newFont.fontName) PrefsInfo.setFontSize(forType, size: Double(newFont.pointSize)) fontLabel.stringValue = newFont.fontName + ", \(Double(newFont.pointSize)) pt" } else { errorLog("New font failure") } } } ================================================ FILE: Aerial/Source/Views/PrefPanel/InfoContainerView.swift ================================================ // // InfoContainerView.swift // Aerial // // Created by Guillaume Louel on 17/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Cocoa class InfoContainerView: NSView { // We need to override the coordinate mode (bottom left origin to top left origin) // so we can later add our child views from top to bottom override var isFlipped: Bool { return true } } ================================================ FILE: Aerial/Source/Views/PrefPanel/InfoCountdownView.swift ================================================ // // InfoCountdownView.swift // Aerial // // Created by Guillaume Louel on 12/02/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class InfoCountdownView: NSView { @IBOutlet var timeModePopup: NSPopUpButton! @IBOutlet var withSecondsCheckbox: NSButton! @IBOutlet var targetTimeDatePicker: NSDatePicker! @IBOutlet var limitToIntervalCheckbox: NSButton! @IBOutlet var limitIntervalDatePicker: NSDatePicker! // Init(ish) func setStates() { timeModePopup.selectItem(at: PrefsInfo.countdown.mode.rawValue) withSecondsCheckbox.state = PrefsInfo.countdown.showSeconds ? .on : .off targetTimeDatePicker.dateValue = PrefsInfo.countdown.targetDate updatePickerFormat() limitToIntervalCheckbox.state = PrefsInfo.countdown.enforceInterval ? .on : .off limitIntervalDatePicker.dateValue = PrefsInfo.countdown.triggerDate } func updatePickerFormat() { switch PrefsInfo.countdown.mode { case .preciseDate: targetTimeDatePicker.datePickerElements = [.yearMonthDay, .hourMinuteSecond] limitIntervalDatePicker.datePickerElements = [.yearMonthDay, .hourMinuteSecond] case .timeOfDay: targetTimeDatePicker.datePickerElements = [.hourMinuteSecond] limitIntervalDatePicker.datePickerElements = [.hourMinuteSecond] // TODO hide day } } // UI Actions @IBAction func timeModePopupChange(_ sender: NSPopUpButton) { PrefsInfo.countdown.mode = InfoCountdownMode(rawValue: sender.indexOfSelectedItem)! updatePickerFormat() } @IBAction func withSecondsCheckboxClick(_ sender: NSButton) { let onState = sender.state == .on PrefsInfo.countdown.showSeconds = onState } @IBAction func targetTimeDatePickerChange(_ sender: NSDatePicker) { PrefsInfo.countdown.targetDate = sender.dateValue } @IBAction func limitToIntervalClick(_ sender: NSButton) { let onState = sender.state == .on PrefsInfo.countdown.enforceInterval = onState } @IBAction func limitIntervalDatePickerChange(_ sender: NSDatePicker) { PrefsInfo.countdown.triggerDate = sender.dateValue } } ================================================ FILE: Aerial/Source/Views/PrefPanel/InfoDateView.swift ================================================ // // InfoDateView.swift // Aerial // // Created by Guillaume Louel on 23/03/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class InfoDateView: NSView { @IBOutlet var dateFormatPopup: NSPopUpButton! @IBOutlet var withYearCheckbox: NSButton! @IBOutlet var customDateFormatField: NSTextField! // Init(ish) func setStates() { dateFormatPopup.selectItem(at: PrefsInfo.date.format.rawValue) withYearCheckbox.state = PrefsInfo.date.withYear ? .on : .off withYearCheckbox.isHidden = (PrefsInfo.date.format == .custom) customDateFormatField.stringValue = PrefsInfo.customDateFormat customDateFormatField.isHidden = !(PrefsInfo.date.format == .custom) } @IBAction func dateFormatPopupChange(_ sender: NSPopUpButton) { PrefsInfo.date.format = InfoDate(rawValue: sender.indexOfSelectedItem)! withYearCheckbox.isHidden = (PrefsInfo.date.format == .custom) customDateFormatField.isHidden = !(PrefsInfo.date.format == .custom) } @IBAction func withYearCheckboxChange(_ sender: NSButton) { let onState = sender.state == .on PrefsInfo.date.withYear = onState } @IBAction func customDateFormatFieldChange(_ sender: NSTextField) { PrefsInfo.customDateFormat = sender.stringValue } } ================================================ FILE: Aerial/Source/Views/PrefPanel/InfoLocationView.swift ================================================ // // InfoLocationView.swift // Aerial // // Created by Guillaume Louel on 19/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Cocoa class InfoLocationView: NSView { @IBOutlet var showTimePopup: NSPopUpButton! // Init(ish) func setStates() { showTimePopup.selectItem(at: PrefsInfo.location.time.rawValue) } @IBAction func showTimeChange(_ sender: NSPopUpButton) { PrefsInfo.location.time = InfoTime(rawValue: sender.indexOfSelectedItem)! } } ================================================ FILE: Aerial/Source/Views/PrefPanel/InfoMessageView.swift ================================================ // // InfoMessageView.swift // Aerial // // Created by Guillaume Louel on 18/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Cocoa class InfoMessageView: NSView, NSTextViewDelegate { @IBOutlet var messageType: NSPopUpButton! // Basic text view @IBOutlet var messageTextView: NSTextView! // Shell script view @IBOutlet weak var shellScript: NSTextField! @IBOutlet weak var shellScriptTest: NSButton! @IBOutlet weak var shellRefreshPeriodicity: NSPopUpButton! @IBOutlet weak var shellScriptLabel: NSTextField! func setStates() { messageType.selectItem(at: PrefsInfo.message.messageType.rawValue) messageTextView.delegate = self messageTextView.string = PrefsInfo.message.message shellScript.stringValue = PrefsInfo.message.shellScript shellRefreshPeriodicity.selectItem(at: PrefsInfo.message.refreshPeriodicity.rawValue) shellScriptLabel.stringValue = "" } @IBAction func messageTypeChange(_ sender: NSPopUpButton) { PrefsInfo.message.messageType = InfoMessageType(rawValue: sender.indexOfSelectedItem)! guard let overlayController = self.parentViewController as? OverlaysViewController else { return } overlayController.switchSubMessagePanel() } // Basic text func textDidChange(_ notification: Notification) { guard let textView = notification.object as? NSTextView else { return } PrefsInfo.message.message = textView.string } // Shell script @IBAction func shellScriptChange(_ sender: NSTextField) { PrefsInfo.message.shellScript = sender.stringValue } @IBAction func shellScriptTestClick(_ sender: Any) { PrefsInfo.message.shellScript = shellScript.stringValue if PrefsInfo.message.shellScript != "" { if FileManager.default.fileExists(atPath: PrefsInfo.message.shellScript) { let (result, code) = Aerial.helper.shell(launchPath: PrefsInfo.message.shellScript) if let res = result { shellScriptLabel.stringValue = res } else { shellScriptLabel.stringValue = "Empty return value, return code: \(code)" } } else { shellScriptLabel.stringValue = "No file found at your location, please check your path" } } else { shellScriptLabel.stringValue = "Script location empty" } } @IBAction func shellRefreshPeriodicityChange(_ sender: NSPopUpButton) { PrefsInfo.message.refreshPeriodicity = InfoRefreshPeriodicity(rawValue: sender.indexOfSelectedItem)! } } ================================================ FILE: Aerial/Source/Views/PrefPanel/InfoMusicView.swift ================================================ // // InfoMusicView.swift // Aerial // // Created by Guillaume Louel on 11/06/2021. // Copyright © 2021 Guillaume Louel. All rights reserved. // import Cocoa class InfoMusicView: NSView { // Init(ish) func setStates() { // Nothing here for now in new universal playback } } ================================================ FILE: Aerial/Source/Views/PrefPanel/InfoSettingsTableSource.swift ================================================ // // InfoSettingsTableSource.swift // Aerial // // Created by Guillaume Louel on 14/02/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class InfoSettingsTableSource: NSTableView, NSTableViewDataSource, NSTableViewDelegate { var controller: OverlaysViewController? func setController(_ controller: OverlaysViewController) { self.controller = controller } override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) } func numberOfRows(in tableView: NSTableView) -> Int { return 1 } func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { return 30 } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let cid = NSUserInterfaceItemIdentifier(rawValue: "InfoSettingsCellID") if let cell = tableView.makeView(withIdentifier: cid, owner: self) as? NSTableCellView { cell.textField?.stringValue = "Text settings" if #available(OSX 10.12.2, *) { cell.imageView?.image = NSImage(named: NSImage.touchBarTextBoxTemplateName) } else { // Fallback on earlier versions cell.imageView?.image = NSImage(named: NSImage.fontPanelName) } return cell } return nil } // This is where selection happens func tableViewSelectionDidChange(_ notification: Notification) { let tableView = notification.object as! NSTableView if tableView.selectedRow >= 0 { controller!.drawInfoSettingsPanel() controller!.infoTableView.deselectAll(controller!) } } } ================================================ FILE: Aerial/Source/Views/PrefPanel/InfoSettingsView.swift ================================================ // // InfoSettingsView.swift // Aerial // // Created by Guillaume Louel on 14/02/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class InfoSettingsView: NSView { @IBOutlet weak var fadeInOutTextModePopup: NSPopUpButton! @IBOutlet var highQualityTextRendering: NSButton! @IBOutlet var changeCornerMargins: NSButton! @IBOutlet var marginHorizontalTextfield: NSTextField! @IBOutlet var marginVerticalTextfield: NSTextField! // Shadows @IBOutlet var shadowRadiusTextField: NSTextField! @IBOutlet var shadowRadiusFormatter: NumberFormatter! @IBOutlet var shadowOpacitySlider: NSSlider! @IBOutlet var shadowOffsetXTextfield: NSTextField! @IBOutlet var shadowOffsetYTextfield: NSTextField! @IBOutlet var shadowOffsetXFormatter: NumberFormatter! @IBOutlet var shadowOffsetYFormatter: NumberFormatter! func setStates() { // Margins override if PrefsInfo.overrideMargins { changeCornerMargins.state = .on marginHorizontalTextfield.isEnabled = true marginVerticalTextfield.isEnabled = true } highQualityTextRendering.state = PrefsInfo.highQualityTextRendering ? .on : .off marginHorizontalTextfield.stringValue = String(PrefsInfo.marginX) marginVerticalTextfield.stringValue = String(PrefsInfo.marginY) fadeInOutTextModePopup.selectItem(at: PrefsInfo.fadeModeText.rawValue) shadowRadiusFormatter.allowsFloats = false shadowRadiusTextField.stringValue = String(PrefsInfo.shadowRadius) shadowOpacitySlider.doubleValue = Double(PrefsInfo.shadowOpacity * 100) shadowOffsetXTextfield.doubleValue = Double(PrefsInfo.shadowOffsetX) shadowOffsetYTextfield.doubleValue = Double(PrefsInfo.shadowOffsetY) } // MARK: - Shadows @IBAction func highQualityTextRenderingChange(_ sender: NSButton) { PrefsInfo.highQualityTextRendering = sender.state == .on } @IBAction func shadowRadiusChange(_ sender: NSTextField) { PrefsInfo.shadowRadius = Int(sender.intValue) } @IBAction func shadowOpacityChange(_ sender: NSSlider) { PrefsInfo.shadowOpacity = Float(sender.intValue)/100 } @IBAction func shadowOffsetXChange(_ sender: NSTextField) { PrefsInfo.shadowOffsetX = CGFloat(sender.doubleValue) } @IBAction func shadowOffsetYChange(_ sender: NSTextField) { PrefsInfo.shadowOffsetY = CGFloat(sender.doubleValue) } // MARK: - Fades @IBAction func fadeInOutTextModePopupChange(_ sender: NSPopUpButton) { debugLog("UI fadeInOutTextMode: \(sender.indexOfSelectedItem)") PrefsInfo.fadeModeText = FadeMode(rawValue: sender.indexOfSelectedItem)! } // MARK: - Margins @IBAction func changeMarginsToCornerClick(_ sender: NSButton) { let onState = sender.state == .on debugLog("UI changeMarginsToCorner: \(onState)") marginHorizontalTextfield.isEnabled = onState marginVerticalTextfield.isEnabled = onState PrefsInfo.overrideMargins = onState } @IBAction func marginXChange(_ sender: NSTextField) { PrefsInfo.marginX = Int(sender.stringValue) ?? 50 } @IBAction func marginYChange(_ sender: NSTextField) { PrefsInfo.marginY = Int(sender.stringValue) ?? 50 } } ================================================ FILE: Aerial/Source/Views/PrefPanel/InfoTableSource.swift ================================================ // // InfoTableSource.swift // Aerial // // Created by Guillaume Louel on 16/12/2019. // Copyright © 2019 Guillaume Louel. All rights reserved. // import Cocoa class InfoTableSource: NSTableView, NSTableViewDataSource, NSTableViewDelegate { private var dragDropType = NSPasteboard.PasteboardType(rawValue: "private.table-row") var controller: OverlaysViewController? func setController(_ controller: OverlaysViewController) { self.controller = controller } override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) } func numberOfRows(in tableView: NSTableView) -> Int { return PrefsInfo.layers.count } func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { return 30 } // This is where we fill each cell func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { if tableColumn == tableView.tableColumns[0] { let cid = NSUserInterfaceItemIdentifier(rawValue: "InfoSourceCellID") if let cell = tableView.makeView(withIdentifier: cid, owner: self) as? NSTableCellView { cell.textField?.stringValue = PrefsInfo.layers[row].rawValue.capitalizeFirstLetter() let isEnabled = PrefsInfo.ofType(PrefsInfo.layers[row]).isEnabled // Aerial.getAccentedSymbol(<#T##named: String##String#>) // Aerial.getSymbol("checkmark.circle.fill") cell.imageView?.image = isEnabled ? Aerial.helper.getAccentedSymbol("checkmark.circle.fill") : Aerial.helper.getAccentedSymbol("circle") /*cell.imageView?.image = NSImage(named: isEnabled ? NSImage.statusAvailableName : NSImage.statusUnavailableName)*/ return cell } } else { let cid = NSUserInterfaceItemIdentifier(rawValue: "InfoSourceGripID") if let cell = tableView.makeView(withIdentifier: cid, owner: self) as? NSTableCellView { cell.imageView?.image = NSImage(named: NSImage.listViewTemplateName) return cell } } return nil } // This is where selection happens func tableViewSelectionDidChange(_ notification: Notification) { let tableView = notification.object as! NSTableView if tableView.selectedRow < 0 { // controller!.resetInfoPanel() } else { controller!.drawInfoPanel(forType: PrefsInfo.layers[tableView.selectedRow]) controller!.infoSettingsTableView.deselectAll(controller!) } } // MARK: - Drag 'n Drop func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? { let item = NSPasteboardItem() item.setString(String(row), forType: self.dragDropType) return item } func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation { if dropOperation == .above { return .move } return [] } func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableView.DropOperation) -> Bool { var oldIndexes = [Int]() info.enumerateDraggingItems(options: [], for: tableView, classes: [NSPasteboardItem.self], searchOptions: [:]) { dragItem, _, _ in if let str = (dragItem.item as! NSPasteboardItem).string(forType: self.dragDropType), let index = Int(str) { oldIndexes.append(index) } } var oldIndexOffset = 0 var newIndexOffset = 0 for oldIndex in oldIndexes { if oldIndex < row { PrefsInfo.layers.move(from: oldIndex + oldIndexOffset, to: row - 1) oldIndexOffset -= 1 } else { PrefsInfo.layers.move(from: oldIndex, to: row + newIndexOffset) newIndexOffset += 1 } } tableView.reloadData() return true } } // Helpers to move items in array extension Array where Element: Equatable { mutating func move(_ element: Element, to newIndex: Index) { if let oldIndex: Int = self.firstIndex(of: element) { self.move(from: oldIndex, to: newIndex) } } } extension Array { mutating func move(from oldIndex: Index, to newIndex: Index) { // Don't work for free and use swap when indices are next to each other - this // won't rebuild array and will be super efficient. if oldIndex == newIndex { return } if abs(newIndex - oldIndex) == 1 { return self.swapAt(oldIndex, newIndex) } self.insert(self.remove(at: oldIndex), at: newIndex) } } extension String { func capitalizeFirstLetter() -> String { return self.prefix(1).capitalized + dropFirst() } } extension NSTableView { func reloadDataKeepingSelection() { let selectedRowIndexes = self.selectedRowIndexes self.reloadData() self.selectRowIndexes(selectedRowIndexes, byExtendingSelection: true) } } ================================================ FILE: Aerial/Source/Views/PrefPanel/InfoTimerView.swift ================================================ // // InfoTimerView.swift // Aerial // // Created by Guillaume Louel on 19/03/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class InfoTimerView: NSView { @IBOutlet var durationTimePicker: NSDatePicker! @IBOutlet var withSecondsCheckbox: NSButton! @IBOutlet var disableWhenElapsedCheckbox: NSButton! @IBOutlet var replaceWithMessageCheckbox: NSButton! @IBOutlet var customMessageTextField: NSTextField! // Init(ish) func setStates() { durationTimePicker.dateValue = PrefsInfo.timer.duration durationTimePicker.locale = Locale(identifier: "fr_FR") withSecondsCheckbox.state = PrefsInfo.timer.showSeconds ? .on : .off disableWhenElapsedCheckbox.state = PrefsInfo.timer.disableWhenElapsed ? .on : .off replaceWithMessageCheckbox.state = PrefsInfo.timer.replaceWithMessage ? .on : .off customMessageTextField.stringValue = PrefsInfo.timer.customMessage customMessageTextField.isEnabled = PrefsInfo.timer.replaceWithMessage } @IBAction func durationChange(_ sender: NSDatePicker) { PrefsInfo.timer.duration = sender.dateValue } @IBAction func withSecondsClick(_ sender: NSButton) { let onState = sender.state == .on PrefsInfo.timer.showSeconds = onState } @IBAction func disableWhenElapsedClick(_ sender: NSButton) { let onState = sender.state == .on PrefsInfo.timer.disableWhenElapsed = onState } @IBAction func replaceWithMessageClick(_ sender: NSButton) { let onState = sender.state == .on PrefsInfo.timer.replaceWithMessage = onState customMessageTextField.isEnabled = onState } @IBAction func customMessageTextFieldChange(_ sender: NSTextField) { PrefsInfo.timer.customMessage = sender.stringValue } } ================================================ FILE: Aerial/Source/Views/PrefPanel/InfoWeatherView.swift ================================================ // // InfoWeatherView.swift // Aerial // // Created by Guillaume Louel on 25/03/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class InfoWeatherView: NSView { @IBOutlet var withCity: NSButton! @IBOutlet var withWind: NSButton! @IBOutlet var withHumidity: NSButton! @IBOutlet var locationMode: NSPopUpButton! @IBOutlet var locationString: NSTextField! @IBOutlet var degreePopup: NSPopUpButton! @IBOutlet var iconsPopup: NSPopUpButton! @IBOutlet var locationLabel: NSTextField! @IBOutlet var weatherModePopup: NSPopUpButton! @IBOutlet var windModePopup: NSPopUpButton! @IBOutlet weak var testButtonLocation: NSButton! @IBOutlet weak var testButtonCity: NSButton! // Init(ish) func setStates() { locationMode.selectItem(at: PrefsInfo.weather.locationMode.rawValue) degreePopup.selectItem(at: PrefsInfo.weather.degree.rawValue) iconsPopup.selectItem(at: PrefsInfo.weather.icons.rawValue) weatherModePopup.selectItem(at: PrefsInfo.weather.mode.rawValue) windModePopup.selectItem(at: PrefsInfo.weatherWindMode.rawValue) if PrefsInfo.weather.degree == .fahrenheit { windModePopup.isHidden = true } withCity.state = PrefsInfo.weather.showCity ? .on : .off withWind.state = PrefsInfo.weather.showWind ? .on : .off withHumidity.state = PrefsInfo.weather.showHumidity ? .on : .off // Hide the flat color icons pre Big Sur as those are not available if #available(macOS 11.0, *) { } else { iconsPopup.item(at: 1)?.isHidden = true } locationString.stringValue = PrefsInfo.weather.locationString locationLabel.stringValue = "" locationString.delegate = self updateLocationMode() } @IBAction func windModePopupChange(_ sender: NSPopUpButton) { PrefsInfo.weatherWindMode = InfoWeatherWind(rawValue: sender.indexOfSelectedItem)! } @IBAction func weatherModePopupChange(_ sender: NSPopUpButton) { PrefsInfo.weather.mode = InfoWeatherMode(rawValue: sender.indexOfSelectedItem)! } @IBAction func locationModeChange(_ sender: NSPopUpButton) { PrefsInfo.weather.locationMode = InfoLocationMode(rawValue: sender.indexOfSelectedItem)! if PrefsInfo.weather.locationMode == .useCurrent { // Get the location let location = Locations.sharedInstance location.getCoordinates(failure: { (_) in // swiftlint:disable:next line_length Aerial.helper.showInfoAlert(title: "Could not get your location", text: "Make sure you enabled location services on your Mac (and Wi-Fi!), and that Aerial (or legacyScreenSaver on macOS 10.15 and later) is allowed to use your location. If you use Aerial Companion, you will also need also allow location services for it.", button1: "OK", caution: true) }, success: { (coordinates) in self.locationLabel.stringValue = "Location found (\(String(format: "%.2f", coordinates.latitude)), \(String(format: "%.2f", coordinates.longitude)))" }) } updateLocationMode() } @IBAction func withCityChange(_ sender: NSButton) { let onState = sender.state == .on PrefsInfo.weather.showCity = onState } @IBAction func withWindChange(_ sender: NSButton) { let onState = sender.state == .on PrefsInfo.weather.showWind = onState } @IBAction func withHumidityChange(_ sender: NSButton) { let onState = sender.state == .on PrefsInfo.weather.showHumidity = onState } func updateLocationMode() { if PrefsInfo.weather.locationMode == .manuallySpecify { locationString.isHidden = false testButtonLocation.isHidden = true testButtonCity.isHidden = false } else { locationString.isHidden = true testButtonLocation.isHidden = false testButtonCity.isHidden = true } } @IBAction func iconsChange(_ sender: NSPopUpButton) { PrefsInfo.weather.icons = InfoIconsWeather(rawValue: sender.indexOfSelectedItem)! } @IBAction func degreePopupChange(_ sender: NSPopUpButton) { PrefsInfo.weather.degree = InfoDegree(rawValue: sender.indexOfSelectedItem)! if PrefsInfo.weather.degree == .fahrenheit { windModePopup.isHidden = true } else { windModePopup.isHidden = false } } @IBAction func locationStringChange(_ sender: NSTextField) { PrefsInfo.weather.locationString = sender.stringValue } @IBAction func testLocationButtonClick(_ sender: NSButton) { // Clear out weather from existing location let cachedWeatherURL = URL(fileURLWithPath: Cache.supportPath, isDirectory: true).appendingPathComponent("Weather.json") let cachedWeatherForecastURL = URL(fileURLWithPath: Cache.supportPath, isDirectory: true).appendingPathComponent("Forecast.json") let fm = FileManager.default // Clear out current weather if fm.fileExists(atPath: cachedWeatherURL.path){ do { try fm.removeItem(atPath: cachedWeatherURL.path) } catch {} } // Clear out forecast if fm.fileExists(atPath: cachedWeatherForecastURL.path) { do { try fm.removeItem(atPath: cachedWeatherForecastURL.path) } catch {} } if PrefsInfo.weather.mode == .current { OpenWeather.fetch { result in switch result { case .success(let openWeather): let ovc = self.parentViewController as! OverlaysViewController ovc.openWeatherPreview(weather: openWeather) if let name = openWeather.name { self.locationLabel.stringValue = name } case .failure(let error): if error == .cityNotFound { self.locationLabel.stringValue = "City not found, make sure you don't use state abbreviations" } else { self.locationLabel.stringValue = error.localizedDescription } } } } else { Forecast.fetch { result in switch result { case .success(let openWeather): let ovc = self.parentViewController as! OverlaysViewController ovc.openWeatherPreview(weather: openWeather) if let lat = openWeather.city?.coord?.lat, let lon = openWeather.city?.coord?.lon, let name = openWeather.city?.name { self.locationLabel.stringValue = name + " lat: " + String(format: "%.2f", lat) + " lon: " + String(format: "%.2f", lon) } case .failure(let error): if error == .cityNotFound { self.locationLabel.stringValue = "City not found, make sure you don't use state abbreviations" } else { self.locationLabel.stringValue = error.localizedDescription } } } } } @IBAction func openWeatherLogoButton(_ sender: Any) { NSWorkspace.shared.open(URL(string: "https://openweathermap.org/")!) } } extension InfoWeatherView: NSTextFieldDelegate { // We need the delegate to intercept changes without the // enter key being pressed on the textfield func controlTextDidChange(_ obj: Notification) { let textField = obj.object as! NSTextField // Just in case... if textField == locationString { // print(textField.stringValue) PrefsInfo.weather.locationString = textField.stringValue } } } extension NSView { var parentViewController: NSViewController? { sequence(first: self) { $0.nextResponder } .first(where: { $0 is NSViewController }) .flatMap { $0 as? NSViewController } } } ================================================ FILE: Aerial/Source/Views/PrefPanel/VideoHeaderView.swift ================================================ // // VideoHeaderView.swift // Aerial // // Created by Guillaume Louel on 14/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa import AppKit class VideoHeaderView: NSView { @IBOutlet weak var sectionTitle: NSTextField! override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) // NSColor(calibratedWhite: 0.8, alpha: 0.8).set() // NSRectFillUsingOperation(dirtyRect, NSCompositingOperation.sourceOver) } } ================================================ FILE: Aerial/Source/Views/PrefPanel/VideoViewItem.swift ================================================ // // VideoViewItem.swift // Aerial // // Created by Guillaume Louel on 13/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class VideoViewItem: NSCollectionViewItem { override func viewDidLoad() { super.viewDidLoad() view.wantsLayer = true } var video: AerialVideo? { didSet { guard isViewLoaded else { return } if let video = video { Thumbnails.get(forVideo: video) { [weak self] (img) in guard let _ = self else { return } if let img = img { self!.imageView?.image = img } else { self!.imageView?.image = nil } } if video.secondaryName != "" { textField?.stringValue = video.secondaryName } else { textField?.stringValue = video.name } } else { imageView?.image = nil textField?.stringValue = "" } } } /*var videoFile: VideoFile? { didSet { guard isViewLoaded else { return } if let videoFile = videoFile { imageView.image = videoFile.thumba } } }*/ } ================================================ FILE: Aerial/Source/Views/Sources/ActionCellView.swift ================================================ // // ActionCellView.swift // Aerial // // Created by Guillaume Louel on 31/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class ActionCellView: NSTableCellView { @IBOutlet var actionButton: NSButton! @IBOutlet var spinner: NSProgressIndicator! var source: Source? @IBAction func actionButton(_ sender: NSButton) { if let source = source { if source.type == .local { NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: Cache.supportPath.appending("/"+source.name)) } else { if source.isCachable { Cache.ensureDownload { sender.isHidden = true self.spinner.isHidden = false self.spinner.startAnimation(self) for video in VideoList.instance.videos.filter({ $0.source.name == source.name && !$0.isAvailableOffline }) { VideoManager.sharedInstance.queueDownload(video) } } } else { sender.isHidden = true spinner.isHidden = false spinner.startAnimation(self) for video in VideoList.instance.videos.filter({ $0.source.name == source.name && !$0.isAvailableOffline }) { VideoManager.sharedInstance.queueDownload(video) } } } } } } ================================================ FILE: Aerial/Source/Views/Sources/CheckboxCellView.swift ================================================ // // CheckboxCellView.swift // Aerial // // Created by Guillaume Louel on 09/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa // swiftlint:disable class_delegate_protocol weak_delegate /// A set of methods that `CheckboxCelView` use to communicate changes to another object protocol CheckboxCellViewDelegate { func checkboxCellView(_ cell: CheckboxCellView, didChangeState state: NSControl.StateValue) } class CheckboxCellView: NSTableCellView { /// The checkbox button @IBOutlet weak var checkboxButton: NSButton! /// The item that represent the row in the outline view /// We may potentially use this cell for multiple outline views so let's make it generic var item: Any? /// The delegate of the cell var delegate: CheckboxCellViewDelegate? override func awakeFromNib() { checkboxButton.target = self checkboxButton.action = #selector(self.didChangeState(_:)) } /// Notify the delegate that the checkbox's state has changed @objc private func didChangeState(_ sender: NSObject) { delegate?.checkboxCellView(self, didChangeState: checkboxButton.state) } } ================================================ FILE: Aerial/Source/Views/Sources/DescriptionCellView.swift ================================================ // // DescriptionCellView.swift // Aerial // // Created by Guillaume Louel on 09/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class DescriptionCellView: NSTableCellView { @IBOutlet weak var titleLabel: NSTextField! @IBOutlet weak var descriptionLabel: NSTextField! @IBOutlet weak var lastUpdatedLabel: NSTextField! @IBOutlet weak var videoCount: NSTextField! @IBOutlet weak var imageScene1: NSImageView! @IBOutlet weak var imageScene2: NSImageView! @IBOutlet weak var imageScene3: NSImageView! @IBOutlet weak var imageScene4: NSImageView! @IBOutlet weak var imageScene5: NSImageView! @IBOutlet weak var imageScene6: NSImageView! @IBOutlet weak var imageFilm: NSImageView! @IBOutlet weak var licenseButton: NSButton! @IBOutlet weak var moreButton: NSButton! @IBOutlet weak var refreshNowButton: NSButton! /// The item that represent the row in the outline view /// We may potentially use this cell for multiple outline views so let's make it generic var item: Any? /// The delegate of the cell // var delegate: CheckboxCellViewDelegate? override func awakeFromNib() { imageScene1.image = Aerial.helper.getMiniSymbol("flame") imageScene2.image = Aerial.helper.getMiniSymbol("tram.fill") imageScene3.image = Aerial.helper.getMiniSymbol("sparkles") imageScene4.image = Aerial.helper.getMiniSymbol("helm") imageScene5.image = Aerial.helper.getMiniSymbol("helm") imageScene6.image = Aerial.helper.getMiniSymbol("helm") imageFilm.image = Aerial.helper.getMiniSymbol("film") // imageScene1. // checkboxButton.target = self // checkboxButton.action = #selector(self.didChangeState(_:)) } /// Notify the delegate that the checkbox's state has changed @objc private func didChangeState(_ sender: NSObject) { // delegate?.checkboxCellView(self, didChangeState: checkboxButton.state) } @IBAction func licenseButtonClick(_ sender: NSButton) { if let source = item as? Source { let workspace = NSWorkspace.shared let url = URL(string: source.license)! workspace.open(url) } } @IBAction func moreButtonClick(_ sender: NSButton) { if let source = item as? Source { let workspace = NSWorkspace.shared let url = URL(string: source.more)! workspace.open(url) } } @IBAction func refreshNowButtonClick(_ sender: NSButton) { if let source = item as? Source { if source.isCachable { debugLog("Refreshing cacheable source") VideoList.instance.downloadSource(source: source) } else if source.type == .local { debugLog("Checking local directory") SourceList.updateLocalSource(source: source, reload: true) } else { debugLog("Refreshing non-cacheable source") VideoList.instance.downloadSource(source: source) } } } } ================================================ FILE: Aerial/Source/Views/Sources/SourceOutlineView.swift ================================================ // // SourceOutlineView.swift // Aerial // // Created by Guillaume Louel on 16/08/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa protocol SourceOutlineViewDelegate: NSOutlineViewDelegate { func outlineView(outlineView: NSOutlineView, menuForItem item: Any) -> NSMenu? } class SourceOutlineView: NSOutlineView { override func menu(for event: NSEvent) -> NSMenu? { let point = self.convert(event.locationInWindow, from: nil) let row = self.row(at: point) if let item = self.item(atRow: row) { return (self.delegate as! SourceOutlineViewDelegate).outlineView(outlineView: self, menuForItem: item) } return nil } } ================================================ FILE: Aerial copy-Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier com.JohnCoates.Aerial CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) LSApplicationCategoryType LSMinimumSystemVersion ${MACOSX_DEPLOYMENT_TARGET} NSAppTransportSecurity NSAllowsArbitraryLoads NSLocationAlwaysUsageDescription Aerial uses location services to calculate Sunset and Sunrise times from your position NSLocationWhenInUseUsageDescription Aerial uses location services to calculate Sunset and Sunrise times from your position NSPrincipalClass AerialView SUFeedURL https://raw.githubusercontent.com/JohnCoates/Aerial/master/appcast.xml SUPublicEDKey fbiQGEFq55xl4bjwj2/SpIO4JMsKmEyhHEWlMMueyDY= ================================================ FILE: Aerial.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 0300109724D6EF4C0092AE68 /* AVPlayerItem+vibrance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0300109624D6EF4C0092AE68 /* AVPlayerItem+vibrance.swift */; }; 0300109824D6EF4C0092AE68 /* AVPlayerItem+vibrance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0300109624D6EF4C0092AE68 /* AVPlayerItem+vibrance.swift */; }; 0300109924D6EF4C0092AE68 /* AVPlayerItem+vibrance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0300109624D6EF4C0092AE68 /* AVPlayerItem+vibrance.swift */; }; 030010A224D706DB0092AE68 /* SidebarOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030010A124D706DB0092AE68 /* SidebarOutlineView.swift */; }; 030010A324D706DB0092AE68 /* SidebarOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030010A124D706DB0092AE68 /* SidebarOutlineView.swift */; }; 030010A424D706DB0092AE68 /* SidebarOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030010A124D706DB0092AE68 /* SidebarOutlineView.swift */; }; 030010A624D718D30092AE68 /* slider.horizontal.3.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 030010A524D718D30092AE68 /* slider.horizontal.3.pdf */; }; 030010A724D718D30092AE68 /* slider.horizontal.3.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 030010A524D718D30092AE68 /* slider.horizontal.3.pdf */; }; 030010A824D718D30092AE68 /* slider.horizontal.3.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 030010A524D718D30092AE68 /* slider.horizontal.3.pdf */; }; 030010AB24D71EB20092AE68 /* FiltersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030010A924D71EB20092AE68 /* FiltersViewController.swift */; }; 030010AC24D71EB20092AE68 /* FiltersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030010A924D71EB20092AE68 /* FiltersViewController.swift */; }; 030010AD24D71EB20092AE68 /* FiltersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030010A924D71EB20092AE68 /* FiltersViewController.swift */; }; 030010AE24D71EB20092AE68 /* FiltersViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 030010AA24D71EB20092AE68 /* FiltersViewController.xib */; }; 030010AF24D71EB20092AE68 /* FiltersViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 030010AA24D71EB20092AE68 /* FiltersViewController.xib */; }; 030010B024D71EB20092AE68 /* FiltersViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 030010AA24D71EB20092AE68 /* FiltersViewController.xib */; }; 0300B7E924D1B536006132E5 /* person.3.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CA24D1B531006132E5 /* person.3.pdf */; }; 0300B7EA24D1B536006132E5 /* person.3.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CA24D1B531006132E5 /* person.3.pdf */; }; 0300B7EB24D1B536006132E5 /* person.3.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CA24D1B531006132E5 /* person.3.pdf */; }; 0300B7EC24D1B536006132E5 /* bubble.left.and.bubble.right.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CB24D1B531006132E5 /* bubble.left.and.bubble.right.pdf */; }; 0300B7ED24D1B536006132E5 /* bubble.left.and.bubble.right.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CB24D1B531006132E5 /* bubble.left.and.bubble.right.pdf */; }; 0300B7EE24D1B536006132E5 /* bubble.left.and.bubble.right.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CB24D1B531006132E5 /* bubble.left.and.bubble.right.pdf */; }; 0300B7EF24D1B536006132E5 /* helm.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CC24D1B531006132E5 /* helm.pdf */; }; 0300B7F024D1B536006132E5 /* helm.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CC24D1B531006132E5 /* helm.pdf */; }; 0300B7F124D1B536006132E5 /* helm.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CC24D1B531006132E5 /* helm.pdf */; }; 0300B7F224D1B536006132E5 /* gear.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CD24D1B532006132E5 /* gear.pdf */; }; 0300B7F324D1B536006132E5 /* gear.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CD24D1B532006132E5 /* gear.pdf */; }; 0300B7F424D1B536006132E5 /* gear.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CD24D1B532006132E5 /* gear.pdf */; }; 0300B7F524D1B536006132E5 /* eye.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CE24D1B532006132E5 /* eye.pdf */; }; 0300B7F624D1B536006132E5 /* eye.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CE24D1B532006132E5 /* eye.pdf */; }; 0300B7F724D1B536006132E5 /* eye.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CE24D1B532006132E5 /* eye.pdf */; }; 0300B7F824D1B536006132E5 /* text.bubble.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CF24D1B532006132E5 /* text.bubble.pdf */; }; 0300B7F924D1B536006132E5 /* text.bubble.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CF24D1B532006132E5 /* text.bubble.pdf */; }; 0300B7FA24D1B536006132E5 /* text.bubble.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7CF24D1B532006132E5 /* text.bubble.pdf */; }; 0300B7FB24D1B536006132E5 /* circle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D024D1B532006132E5 /* circle.pdf */; }; 0300B7FC24D1B536006132E5 /* circle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D024D1B532006132E5 /* circle.pdf */; }; 0300B7FD24D1B536006132E5 /* circle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D024D1B532006132E5 /* circle.pdf */; }; 0300B7FE24D1B536006132E5 /* star.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D124D1B532006132E5 /* star.pdf */; }; 0300B7FF24D1B536006132E5 /* star.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D124D1B532006132E5 /* star.pdf */; }; 0300B80024D1B536006132E5 /* star.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D124D1B532006132E5 /* star.pdf */; }; 0300B80124D1B536006132E5 /* clock.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D224D1B532006132E5 /* clock.pdf */; }; 0300B80224D1B536006132E5 /* clock.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D224D1B532006132E5 /* clock.pdf */; }; 0300B80324D1B536006132E5 /* clock.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D224D1B532006132E5 /* clock.pdf */; }; 0300B80424D1B536006132E5 /* sparkles.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D324D1B532006132E5 /* sparkles.pdf */; }; 0300B80524D1B536006132E5 /* sparkles.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D324D1B532006132E5 /* sparkles.pdf */; }; 0300B80624D1B536006132E5 /* sparkles.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D324D1B532006132E5 /* sparkles.pdf */; }; 0300B80724D1B536006132E5 /* film.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D424D1B533006132E5 /* film.pdf */; }; 0300B80824D1B536006132E5 /* film.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D424D1B533006132E5 /* film.pdf */; }; 0300B80924D1B536006132E5 /* film.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D424D1B533006132E5 /* film.pdf */; }; 0300B80A24D1B536006132E5 /* eye.slash.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D524D1B533006132E5 /* eye.slash.pdf */; }; 0300B80B24D1B536006132E5 /* eye.slash.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D524D1B533006132E5 /* eye.slash.pdf */; }; 0300B80C24D1B536006132E5 /* eye.slash.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D524D1B533006132E5 /* eye.slash.pdf */; }; 0300B80D24D1B536006132E5 /* info.circle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D624D1B533006132E5 /* info.circle.pdf */; }; 0300B80E24D1B536006132E5 /* info.circle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D624D1B533006132E5 /* info.circle.pdf */; }; 0300B80F24D1B536006132E5 /* info.circle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D624D1B533006132E5 /* info.circle.pdf */; }; 0300B81024D1B536006132E5 /* regular.moon.stars.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D724D1B533006132E5 /* regular.moon.stars.pdf */; }; 0300B81124D1B536006132E5 /* regular.moon.stars.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D724D1B533006132E5 /* regular.moon.stars.pdf */; }; 0300B81224D1B536006132E5 /* regular.moon.stars.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D724D1B533006132E5 /* regular.moon.stars.pdf */; }; 0300B81324D1B536006132E5 /* antenna.radiowaves.left.and.right.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D824D1B533006132E5 /* antenna.radiowaves.left.and.right.pdf */; }; 0300B81424D1B536006132E5 /* antenna.radiowaves.left.and.right.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D824D1B533006132E5 /* antenna.radiowaves.left.and.right.pdf */; }; 0300B81524D1B536006132E5 /* antenna.radiowaves.left.and.right.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D824D1B533006132E5 /* antenna.radiowaves.left.and.right.pdf */; }; 0300B81624D1B536006132E5 /* sunset.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D924D1B533006132E5 /* sunset.pdf */; }; 0300B81724D1B536006132E5 /* sunset.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D924D1B533006132E5 /* sunset.pdf */; }; 0300B81824D1B536006132E5 /* sunset.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7D924D1B533006132E5 /* sunset.pdf */; }; 0300B81924D1B536006132E5 /* tram.fill.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DA24D1B534006132E5 /* tram.fill.pdf */; }; 0300B81A24D1B536006132E5 /* tram.fill.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DA24D1B534006132E5 /* tram.fill.pdf */; }; 0300B81B24D1B536006132E5 /* tram.fill.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DA24D1B534006132E5 /* tram.fill.pdf */; }; 0300B81C24D1B536006132E5 /* arrow.down.circle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DB24D1B534006132E5 /* arrow.down.circle.pdf */; }; 0300B81D24D1B536006132E5 /* arrow.down.circle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DB24D1B534006132E5 /* arrow.down.circle.pdf */; }; 0300B81E24D1B536006132E5 /* arrow.down.circle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DB24D1B534006132E5 /* arrow.down.circle.pdf */; }; 0300B81F24D1B536006132E5 /* flame.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DC24D1B534006132E5 /* flame.pdf */; }; 0300B82024D1B536006132E5 /* flame.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DC24D1B534006132E5 /* flame.pdf */; }; 0300B82124D1B536006132E5 /* flame.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DC24D1B534006132E5 /* flame.pdf */; }; 0300B82224D1B536006132E5 /* regular.sun.min.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DD24D1B534006132E5 /* regular.sun.min.pdf */; }; 0300B82324D1B536006132E5 /* regular.sun.min.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DD24D1B534006132E5 /* regular.sun.min.pdf */; }; 0300B82424D1B536006132E5 /* regular.sun.min.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DD24D1B534006132E5 /* regular.sun.min.pdf */; }; 0300B82524D1B536006132E5 /* mappin.and.ellipse.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DE24D1B534006132E5 /* mappin.and.ellipse.pdf */; }; 0300B82624D1B536006132E5 /* mappin.and.ellipse.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DE24D1B534006132E5 /* mappin.and.ellipse.pdf */; }; 0300B82724D1B536006132E5 /* mappin.and.ellipse.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DE24D1B534006132E5 /* mappin.and.ellipse.pdf */; }; 0300B82824D1B536006132E5 /* arrow.down.circle.fill.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DF24D1B534006132E5 /* arrow.down.circle.fill.pdf */; }; 0300B82924D1B536006132E5 /* arrow.down.circle.fill.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DF24D1B534006132E5 /* arrow.down.circle.fill.pdf */; }; 0300B82A24D1B536006132E5 /* arrow.down.circle.fill.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7DF24D1B534006132E5 /* arrow.down.circle.fill.pdf */; }; 0300B82B24D1B536006132E5 /* location.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E024D1B535006132E5 /* location.pdf */; }; 0300B82C24D1B536006132E5 /* location.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E024D1B535006132E5 /* location.pdf */; }; 0300B82D24D1B536006132E5 /* location.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E024D1B535006132E5 /* location.pdf */; }; 0300B82E24D1B536006132E5 /* tv.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E124D1B535006132E5 /* tv.pdf */; }; 0300B82F24D1B536006132E5 /* tv.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E124D1B535006132E5 /* tv.pdf */; }; 0300B83024D1B536006132E5 /* tv.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E124D1B535006132E5 /* tv.pdf */; }; 0300B83124D1B536006132E5 /* sunrise.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E224D1B535006132E5 /* sunrise.pdf */; }; 0300B83224D1B536006132E5 /* sunrise.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E224D1B535006132E5 /* sunrise.pdf */; }; 0300B83324D1B536006132E5 /* sunrise.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E224D1B535006132E5 /* sunrise.pdf */; }; 0300B83424D1B536006132E5 /* star.fill.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E324D1B535006132E5 /* star.fill.pdf */; }; 0300B83524D1B536006132E5 /* star.fill.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E324D1B535006132E5 /* star.fill.pdf */; }; 0300B83624D1B536006132E5 /* star.fill.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E324D1B535006132E5 /* star.fill.pdf */; }; 0300B83724D1B536006132E5 /* regular.cloud.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E424D1B535006132E5 /* regular.cloud.pdf */; }; 0300B83824D1B536006132E5 /* regular.cloud.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E424D1B535006132E5 /* regular.cloud.pdf */; }; 0300B83924D1B536006132E5 /* regular.cloud.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E424D1B535006132E5 /* regular.cloud.pdf */; }; 0300B83A24D1B536006132E5 /* regular.sun.max.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E524D1B535006132E5 /* regular.sun.max.pdf */; }; 0300B83B24D1B536006132E5 /* regular.sun.max.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E524D1B535006132E5 /* regular.sun.max.pdf */; }; 0300B83C24D1B536006132E5 /* regular.sun.max.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E524D1B535006132E5 /* regular.sun.max.pdf */; }; 0300B83D24D1B536006132E5 /* dial.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E624D1B536006132E5 /* dial.pdf */; }; 0300B83E24D1B536006132E5 /* dial.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E624D1B536006132E5 /* dial.pdf */; }; 0300B83F24D1B536006132E5 /* dial.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E624D1B536006132E5 /* dial.pdf */; }; 0300B84024D1B536006132E5 /* checkmark.circle.fill.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E724D1B536006132E5 /* checkmark.circle.fill.pdf */; }; 0300B84124D1B536006132E5 /* checkmark.circle.fill.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E724D1B536006132E5 /* checkmark.circle.fill.pdf */; }; 0300B84224D1B536006132E5 /* checkmark.circle.fill.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E724D1B536006132E5 /* checkmark.circle.fill.pdf */; }; 0300B84324D1B536006132E5 /* square.and.arrow.down.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E824D1B536006132E5 /* square.and.arrow.down.pdf */; }; 0300B84424D1B536006132E5 /* square.and.arrow.down.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E824D1B536006132E5 /* square.and.arrow.down.pdf */; }; 0300B84524D1B536006132E5 /* square.and.arrow.down.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0300B7E824D1B536006132E5 /* square.and.arrow.down.pdf */; }; 0300B84924D1FD24006132E5 /* FirstSetupWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0300B84724D1FD24006132E5 /* FirstSetupWindowController.swift */; }; 0300B84A24D1FD24006132E5 /* FirstSetupWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0300B84724D1FD24006132E5 /* FirstSetupWindowController.swift */; }; 0300B84B24D1FD24006132E5 /* FirstSetupWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0300B84724D1FD24006132E5 /* FirstSetupWindowController.swift */; }; 0300B84C24D1FD24006132E5 /* FirstSetupWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0300B84824D1FD24006132E5 /* FirstSetupWindowController.xib */; }; 0300B84D24D1FD24006132E5 /* FirstSetupWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0300B84824D1FD24006132E5 /* FirstSetupWindowController.xib */; }; 0300B84E24D1FD24006132E5 /* FirstSetupWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0300B84824D1FD24006132E5 /* FirstSetupWindowController.xib */; }; 0300B86124D2052B006132E5 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0300B85F24D2052B006132E5 /* WelcomeViewController.swift */; }; 0300B86224D2052B006132E5 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0300B85F24D2052B006132E5 /* WelcomeViewController.swift */; }; 0300B86324D2052B006132E5 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0300B85F24D2052B006132E5 /* WelcomeViewController.swift */; }; 0300B86424D2052B006132E5 /* WelcomeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0300B86024D2052B006132E5 /* WelcomeViewController.xib */; }; 0300B86524D2052B006132E5 /* WelcomeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0300B86024D2052B006132E5 /* WelcomeViewController.xib */; }; 0300B86624D2052B006132E5 /* WelcomeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0300B86024D2052B006132E5 /* WelcomeViewController.xib */; }; 0300B86924D20B12006132E5 /* NextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0300B86724D20B12006132E5 /* NextViewController.swift */; }; 0300B86A24D20B12006132E5 /* NextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0300B86724D20B12006132E5 /* NextViewController.swift */; }; 0300B86B24D20B12006132E5 /* NextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0300B86724D20B12006132E5 /* NextViewController.swift */; }; 0300B86C24D20B12006132E5 /* NextViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0300B86824D20B12006132E5 /* NextViewController.xib */; }; 0300B86D24D20B12006132E5 /* NextViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0300B86824D20B12006132E5 /* NextViewController.xib */; }; 0300B86E24D20B12006132E5 /* NextViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0300B86824D20B12006132E5 /* NextViewController.xib */; }; 030473CB24BCA9A40094A1A6 /* VideoViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030473C924BCA9A40094A1A6 /* VideoViewItem.swift */; }; 030473CC24BCA9A40094A1A6 /* VideoViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030473C924BCA9A40094A1A6 /* VideoViewItem.swift */; }; 030473CD24BCA9A40094A1A6 /* VideoViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030473C924BCA9A40094A1A6 /* VideoViewItem.swift */; }; 03047A5424D2DD8E000EFE62 /* NSMenuItem+icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03047A5324D2DD8E000EFE62 /* NSMenuItem+icons.swift */; }; 03047A5524D2DD8E000EFE62 /* NSMenuItem+icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03047A5324D2DD8E000EFE62 /* NSMenuItem+icons.swift */; }; 03047A5624D2DD8E000EFE62 /* NSMenuItem+icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03047A5324D2DD8E000EFE62 /* NSMenuItem+icons.swift */; }; 0306336823A1012200046A59 /* LocationLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0306336723A1012200046A59 /* LocationLayer.swift */; }; 0306336923A1026800046A59 /* LocationLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0306336723A1012200046A59 /* LocationLayer.swift */; }; 0306336B23A142FA00046A59 /* LayerOffsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0306336A23A142FA00046A59 /* LayerOffsets.swift */; }; 0306336C23A143D900046A59 /* LayerOffsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0306336A23A142FA00046A59 /* LayerOffsets.swift */; }; 0306336E23A15FA900046A59 /* AnimationTextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0306336D23A15FA900046A59 /* AnimationTextLayer.swift */; }; 03075EB124ED794F00FDBE48 /* trash.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03075EA824ED794F00FDBE48 /* trash.pdf */; }; 03075EB224ED794F00FDBE48 /* trash.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03075EA824ED794F00FDBE48 /* trash.pdf */; }; 03075EB324ED794F00FDBE48 /* trash.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03075EA824ED794F00FDBE48 /* trash.pdf */; }; 03075EB424ED794F00FDBE48 /* textformat.alt.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03075EB024ED794F00FDBE48 /* textformat.alt.pdf */; }; 03075EB524ED794F00FDBE48 /* textformat.alt.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03075EB024ED794F00FDBE48 /* textformat.alt.pdf */; }; 03075EB624ED794F00FDBE48 /* textformat.alt.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03075EB024ED794F00FDBE48 /* textformat.alt.pdf */; }; 030A0F29245C7C7D009E1D97 /* BatteryIconLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030A0F28245C7C7D009E1D97 /* BatteryIconLayer.swift */; }; 030A0F2A245C7C7D009E1D97 /* BatteryIconLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030A0F28245C7C7D009E1D97 /* BatteryIconLayer.swift */; }; 030D9B7C21551A8D00961E95 /* AerialPlayerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E2E5D1FC62E8B00E5F320 /* AerialPlayerItem.swift */; }; 030DDA902423C3BE0072D5C9 /* InfoTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030DDA8F2423C3BE0072D5C9 /* InfoTimerView.swift */; }; 0313329C24BF3FA700C84A05 /* SidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313329A24BF3FA700C84A05 /* SidebarViewController.swift */; }; 0313329D24BF3FA700C84A05 /* SidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313329A24BF3FA700C84A05 /* SidebarViewController.swift */; }; 0313329E24BF3FA700C84A05 /* SidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313329A24BF3FA700C84A05 /* SidebarViewController.swift */; }; 0313329F24BF3FA700C84A05 /* SidebarViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0313329B24BF3FA700C84A05 /* SidebarViewController.xib */; }; 031332A024BF3FA700C84A05 /* SidebarViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0313329B24BF3FA700C84A05 /* SidebarViewController.xib */; }; 031332A124BF3FA700C84A05 /* SidebarViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0313329B24BF3FA700C84A05 /* SidebarViewController.xib */; }; 0313F9E622942AA500B074BB /* CustomVideos.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0313F9E522942AA500B074BB /* CustomVideos.xib */; }; 0313F9E822942B4500B074BB /* CustomVideoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313F9E722942B4500B074BB /* CustomVideoController.swift */; }; 0313F9E92294337F00B074BB /* CustomVideos.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0313F9E522942AA500B074BB /* CustomVideos.xib */; }; 0313F9EA2294338300B074BB /* CustomVideoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313F9E722942B4500B074BB /* CustomVideoController.swift */; }; 0313F9EC2294468600B074BB /* SeededGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313F9EB2294468600B074BB /* SeededGenerator.swift */; }; 0313F9ED2294468600B074BB /* SeededGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313F9EB2294468600B074BB /* SeededGenerator.swift */; }; 0313F9EF22955F3B00B074BB /* CustomVideoFolders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313F9EE22955F3B00B074BB /* CustomVideoFolders.swift */; }; 0313F9F022955F3B00B074BB /* CustomVideoFolders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313F9EE22955F3B00B074BB /* CustomVideoFolders.swift */; }; 0317C19C268B65D10082A40C /* Music.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0317C19B268B65D10082A40C /* Music.swift */; }; 0317C19D268B65D10082A40C /* Music.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0317C19B268B65D10082A40C /* Music.swift */; }; 0317C19E268B65D10082A40C /* Music.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0317C19B268B65D10082A40C /* Music.swift */; }; 031945F324CCC48C00F37B35 /* CreditsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031945F124CCC48C00F37B35 /* CreditsViewController.swift */; }; 031945F424CCC48C00F37B35 /* CreditsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031945F124CCC48C00F37B35 /* CreditsViewController.swift */; }; 031945F524CCC48C00F37B35 /* CreditsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031945F124CCC48C00F37B35 /* CreditsViewController.swift */; }; 031945F624CCC48C00F37B35 /* CreditsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 031945F224CCC48C00F37B35 /* CreditsViewController.xib */; }; 031945F724CCC48C00F37B35 /* CreditsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 031945F224CCC48C00F37B35 /* CreditsViewController.xib */; }; 031945F824CCC48C00F37B35 /* CreditsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 031945F224CCC48C00F37B35 /* CreditsViewController.xib */; }; 031945FB24CCC52600F37B35 /* HelpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031945F924CCC52600F37B35 /* HelpViewController.swift */; }; 031945FC24CCC52600F37B35 /* HelpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031945F924CCC52600F37B35 /* HelpViewController.swift */; }; 031945FD24CCC52600F37B35 /* HelpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031945F924CCC52600F37B35 /* HelpViewController.swift */; }; 031945FE24CCC52600F37B35 /* HelpViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 031945FA24CCC52600F37B35 /* HelpViewController.xib */; }; 031945FF24CCC52600F37B35 /* HelpViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 031945FA24CCC52600F37B35 /* HelpViewController.xib */; }; 0319460024CCC52600F37B35 /* HelpViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 031945FA24CCC52600F37B35 /* HelpViewController.xib */; }; 0319461024D1A09F00F37B35 /* icon-320@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0319460824D1A09F00F37B35 /* icon-320@2x.png */; }; 0319461124D1A09F00F37B35 /* icon-320@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0319460824D1A09F00F37B35 /* icon-320@2x.png */; }; 0319461224D1A09F00F37B35 /* icon-320@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0319460824D1A09F00F37B35 /* icon-320@2x.png */; }; 031C5CB8268CA4E700CE35B4 /* ArtworkLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031C5CB7268CA4E700CE35B4 /* ArtworkLayer.swift */; }; 031C5CB9268CA4E700CE35B4 /* ArtworkLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031C5CB7268CA4E700CE35B4 /* ArtworkLayer.swift */; }; 031C5CBA268CA4E700CE35B4 /* ArtworkLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031C5CB7268CA4E700CE35B4 /* ArtworkLayer.swift */; }; 031FB78B248A87330054BAFD /* PrefsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031FB78A248A87330054BAFD /* PrefsCache.swift */; }; 031FB7A2248A873C0054BAFD /* PrefsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031FB78A248A87330054BAFD /* PrefsCache.swift */; }; 0321A53324D44E80004F1975 /* ActionCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0321A53224D44E80004F1975 /* ActionCellView.swift */; }; 0321A53424D44E80004F1975 /* ActionCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0321A53224D44E80004F1975 /* ActionCellView.swift */; }; 0321A53524D44E80004F1975 /* ActionCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0321A53224D44E80004F1975 /* ActionCellView.swift */; }; 0321A53E24D4515B004F1975 /* folder.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0321A53D24D4515B004F1975 /* folder.pdf */; }; 0321A53F24D4515B004F1975 /* folder.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0321A53D24D4515B004F1975 /* folder.pdf */; }; 0321A54024D4515B004F1975 /* folder.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 0321A53D24D4515B004F1975 /* folder.pdf */; }; 0321A54424D5C863004F1975 /* NSButton+icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0321A54324D5C863004F1975 /* NSButton+icons.swift */; }; 0321A54524D5C863004F1975 /* NSButton+icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0321A54324D5C863004F1975 /* NSButton+icons.swift */; }; 0321A54624D5C863004F1975 /* NSButton+icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0321A54324D5C863004F1975 /* NSButton+icons.swift */; }; 03233B68217272640077D3F9 /* PoiStringProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03233B67217272640077D3F9 /* PoiStringProvider.swift */; }; 03233B692172762C0077D3F9 /* PoiStringProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03233B67217272640077D3F9 /* PoiStringProvider.swift */; }; 032851F7260A4C2C00684A81 /* OneCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032851F6260A4C2B00684A81 /* OneCall.swift */; }; 032851F8260A4C2C00684A81 /* OneCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032851F6260A4C2B00684A81 /* OneCall.swift */; }; 032851F9260A4C2C00684A81 /* OneCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032851F6260A4C2B00684A81 /* OneCall.swift */; }; 032851FB260A625100684A81 /* ForecastLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032851FA260A625100684A81 /* ForecastLayer.swift */; }; 032851FC260A625100684A81 /* ForecastLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032851FA260A625100684A81 /* ForecastLayer.swift */; }; 032851FD260A625100684A81 /* ForecastLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032851FA260A625100684A81 /* ForecastLayer.swift */; }; 03298775274687340036D898 /* NowPlayingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03298773274687340036D898 /* NowPlayingViewController.swift */; }; 03298776274687340036D898 /* NowPlayingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03298773274687340036D898 /* NowPlayingViewController.swift */; }; 03298777274687340036D898 /* NowPlayingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03298773274687340036D898 /* NowPlayingViewController.swift */; }; 03298778274687340036D898 /* NowPlayingViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03298774274687340036D898 /* NowPlayingViewController.xib */; }; 03298779274687340036D898 /* NowPlayingViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03298774274687340036D898 /* NowPlayingViewController.xib */; }; 0329877A274687340036D898 /* NowPlayingViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03298774274687340036D898 /* NowPlayingViewController.xib */; }; 032D1161239A7D82007E7756 /* Battery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032D1160239A7D82007E7756 /* Battery.swift */; }; 032D1162239A7E03007E7756 /* Battery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032D1160239A7D82007E7756 /* Battery.swift */; }; 032D1164239A7F0C007E7756 /* AerialView+Brightness.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032D1163239A7F0C007E7756 /* AerialView+Brightness.swift */; }; 032D1165239A7F17007E7756 /* AerialView+Brightness.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032D1163239A7F0C007E7756 /* AerialView+Brightness.swift */; }; 032E099E24C3897E00387230 /* AdvancedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032E099C24C3897E00387230 /* AdvancedViewController.swift */; }; 032E099F24C3897E00387230 /* AdvancedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032E099C24C3897E00387230 /* AdvancedViewController.swift */; }; 032E09A024C3897E00387230 /* AdvancedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032E099C24C3897E00387230 /* AdvancedViewController.swift */; }; 032E09A124C3897E00387230 /* AdvancedViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 032E099D24C3897E00387230 /* AdvancedViewController.xib */; }; 032E09A224C3897E00387230 /* AdvancedViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 032E099D24C3897E00387230 /* AdvancedViewController.xib */; }; 032E09A324C3897E00387230 /* AdvancedViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 032E099D24C3897E00387230 /* AdvancedViewController.xib */; }; 0332076D26D7C355001F9837 /* AVAsset+VideoOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0332076C26D7C355001F9837 /* AVAsset+VideoOrientation.swift */; }; 0332076E26D7C355001F9837 /* AVAsset+VideoOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0332076C26D7C355001F9837 /* AVAsset+VideoOrientation.swift */; }; 0332076F26D7C355001F9837 /* AVAsset+VideoOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0332076C26D7C355001F9837 /* AVAsset+VideoOrientation.swift */; }; 0338119524C1D15B002E23E0 /* Aerial.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0338119424C1D15B002E23E0 /* Aerial.swift */; }; 0338119624C1D15B002E23E0 /* Aerial.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0338119424C1D15B002E23E0 /* Aerial.swift */; }; 0338119724C1D15B002E23E0 /* Aerial.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0338119424C1D15B002E23E0 /* Aerial.swift */; }; 033811B024C1E243002E23E0 /* InfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033811AE24C1E243002E23E0 /* InfoViewController.swift */; }; 033811B124C1E243002E23E0 /* InfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033811AE24C1E243002E23E0 /* InfoViewController.swift */; }; 033811B224C1E243002E23E0 /* InfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033811AE24C1E243002E23E0 /* InfoViewController.swift */; }; 033811B324C1E243002E23E0 /* InfoViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 033811AF24C1E243002E23E0 /* InfoViewController.xib */; }; 033811B424C1E243002E23E0 /* InfoViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 033811AF24C1E243002E23E0 /* InfoViewController.xib */; }; 033811B524C1E243002E23E0 /* InfoViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 033811AF24C1E243002E23E0 /* InfoViewController.xib */; }; 033842E124489D7300A2C523 /* WeatherLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033842E024489D7300A2C523 /* WeatherLayer.swift */; }; 033842F824489EC600A2C523 /* WeatherLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033842E024489D7300A2C523 /* WeatherLayer.swift */; }; 033D68812453080C0016F837 /* ConditionSymbolLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D68802453080C0016F837 /* ConditionSymbolLayer.swift */; }; 034116D323F9BD3100CD7674 /* PrefsUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034116D223F9BD3100CD7674 /* PrefsUpdates.swift */; }; 034116D423F9BD3100CD7674 /* PrefsUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034116D223F9BD3100CD7674 /* PrefsUpdates.swift */; }; 0343E9F92630590A00AC702F /* openweather_logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 0343E9F82630590A00AC702F /* openweather_logo.png */; }; 0343E9FA2630590A00AC702F /* openweather_logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 0343E9F82630590A00AC702F /* openweather_logo.png */; }; 0343E9FB2630590A00AC702F /* openweather_logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 0343E9F82630590A00AC702F /* openweather_logo.png */; }; 0345872D2449C52F00C97D1B /* AnimatableLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0345872C2449C52F00C97D1B /* AnimatableLayer.swift */; }; 0345872E2449C59900C97D1B /* AnimatableLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0345872C2449C52F00C97D1B /* AnimatableLayer.swift */; }; 034587322449D8EB00C97D1B /* AnimationLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034587312449D8EB00C97D1B /* AnimationLayer.swift */; }; 034587332449E22000C97D1B /* AnimationLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034587312449D8EB00C97D1B /* AnimationLayer.swift */; }; 0345A24B24532E4600DD47CD /* ConditionSymbolLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D68802453080C0016F837 /* ConditionSymbolLayer.swift */; }; 0345CFED24BF43280001045C /* VideosViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0345CFEB24BF43280001045C /* VideosViewController.swift */; }; 0345CFEE24BF43280001045C /* VideosViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0345CFEB24BF43280001045C /* VideosViewController.swift */; }; 0345CFEF24BF43280001045C /* VideosViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0345CFEB24BF43280001045C /* VideosViewController.swift */; }; 0345CFF024BF43280001045C /* VideosViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0345CFEC24BF43280001045C /* VideosViewController.xib */; }; 0345CFF124BF43280001045C /* VideosViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0345CFEC24BF43280001045C /* VideosViewController.xib */; }; 0345CFF224BF43280001045C /* VideosViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0345CFEC24BF43280001045C /* VideosViewController.xib */; }; 0345D00A24BF4E8C0001045C /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0345D00924BF4E8C0001045C /* Sidebar.swift */; }; 0345D00B24BF4E8C0001045C /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0345D00924BF4E8C0001045C /* Sidebar.swift */; }; 0345D00C24BF4E8C0001045C /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0345D00924BF4E8C0001045C /* Sidebar.swift */; }; 0345D00E24C07CC70001045C /* VideoCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0345D00D24C07CC70001045C /* VideoCellView.swift */; }; 0345D00F24C07CC70001045C /* VideoCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0345D00D24C07CC70001045C /* VideoCellView.swift */; }; 0345D01024C07CC70001045C /* VideoCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0345D00D24C07CC70001045C /* VideoCellView.swift */; }; 0345EDE824C3239400C73038 /* DisplaysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0345EDE624C3239400C73038 /* DisplaysViewController.swift */; }; 0345EDE924C3239400C73038 /* DisplaysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0345EDE624C3239400C73038 /* DisplaysViewController.swift */; }; 0345EDEA24C3239400C73038 /* DisplaysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0345EDE624C3239400C73038 /* DisplaysViewController.swift */; }; 0345EDEB24C3239400C73038 /* DisplaysViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0345EDE724C3239400C73038 /* DisplaysViewController.xib */; }; 0345EDEC24C3239400C73038 /* DisplaysViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0345EDE724C3239400C73038 /* DisplaysViewController.xib */; }; 0345EDED24C3239400C73038 /* DisplaysViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0345EDE724C3239400C73038 /* DisplaysViewController.xib */; }; 034A6DC224ACC7C800D62129 /* SourceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034A6DC124ACC7C800D62129 /* SourceList.swift */; }; 034A6DC424ACC80200D62129 /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034A6DC324ACC80200D62129 /* Source.swift */; }; 034A6DC524ACC80B00D62129 /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034A6DC324ACC80200D62129 /* Source.swift */; }; 034A6DC624AF3EC600D62129 /* SourceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034A6DC124ACC7C800D62129 /* SourceList.swift */; }; 034DEE2E24BF1BC700A2D3CD /* PanelWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034DEE2C24BF1BC700A2D3CD /* PanelWindowController.swift */; }; 034DEE2F24BF1BC700A2D3CD /* PanelWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034DEE2C24BF1BC700A2D3CD /* PanelWindowController.swift */; }; 034DEE3024BF1BC700A2D3CD /* PanelWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034DEE2C24BF1BC700A2D3CD /* PanelWindowController.swift */; }; 034DEE3124BF1BC700A2D3CD /* PanelWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 034DEE2D24BF1BC700A2D3CD /* PanelWindowController.xib */; }; 034DEE3224BF1BC700A2D3CD /* PanelWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 034DEE2D24BF1BC700A2D3CD /* PanelWindowController.xib */; }; 034DEE3324BF1BC700A2D3CD /* PanelWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 034DEE2D24BF1BC700A2D3CD /* PanelWindowController.xib */; }; 034F29B723A7A93D004B34D5 /* InfoTableSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034F29B623A7A93D004B34D5 /* InfoTableSource.swift */; }; 034F29B823A7A9B3004B34D5 /* InfoTableSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034F29B623A7A93D004B34D5 /* InfoTableSource.swift */; }; 034F29BF23A7E28E004B34D5 /* PrefsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034F29BE23A7E28E004B34D5 /* PrefsInfo.swift */; }; 034F29C023A7E58C004B34D5 /* PrefsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034F29BE23A7E28E004B34D5 /* PrefsInfo.swift */; }; 0350CB7C279481F2005F8625 /* hu.json in Resources */ = {isa = PBXBuildFile; fileRef = 0350CB78279481F2005F8625 /* hu.json */; }; 0350CB7D279481F2005F8625 /* hu.json in Resources */ = {isa = PBXBuildFile; fileRef = 0350CB78279481F2005F8625 /* hu.json */; }; 0350CB7E279481F2005F8625 /* hu.json in Resources */ = {isa = PBXBuildFile; fileRef = 0350CB78279481F2005F8625 /* hu.json */; }; 03510C6B21834EB2008F74F2 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03510C6A21834EB2008F74F2 /* IOKit.framework */; }; 03510C6C21834EFF008F74F2 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03510C6A21834EB2008F74F2 /* IOKit.framework */; }; 03510C6F21834F38008F74F2 /* IOBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 03510C6E21834F38008F74F2 /* IOBridge.m */; }; 03510C7021834FC3008F74F2 /* Aerial-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 03510C6D21834F38008F74F2 /* Aerial-Bridging-Header.h */; }; 03510C7121834FC7008F74F2 /* IOBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 03510C6E21834F38008F74F2 /* IOBridge.m */; }; 03510C732185EF76008F74F2 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03510C722185EF76008F74F2 /* CoreLocation.framework */; }; 03510C772185EF8F008F74F2 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03510C722185EF76008F74F2 /* CoreLocation.framework */; }; 0354D0E923F6C31800D86F9E /* InfoSettingsTableSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0354D0E823F6C31800D86F9E /* InfoSettingsTableSource.swift */; }; 0354D0EA23F6C3EE00D86F9E /* InfoSettingsTableSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0354D0E823F6C31800D86F9E /* InfoSettingsTableSource.swift */; }; 0354D0EC23F6CB7B00D86F9E /* InfoSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0354D0EB23F6CB7B00D86F9E /* InfoSettingsView.swift */; }; 0354D0ED23F6CB7B00D86F9E /* InfoSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0354D0EB23F6CB7B00D86F9E /* InfoSettingsView.swift */; }; 035D524F239AA31A00DC29DC /* AerialView+Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035D524E239AA31A00DC29DC /* AerialView+Player.swift */; }; 035D5250239AA31A00DC29DC /* AerialView+Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035D524E239AA31A00DC29DC /* AerialView+Player.swift */; }; 03608A2C22A56465008F08A2 /* HardwareDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03608A2B22A56465008F08A2 /* HardwareDetection.swift */; }; 03608A2D22A56465008F08A2 /* HardwareDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03608A2B22A56465008F08A2 /* HardwareDetection.swift */; }; 0361B9A823D732A300B6252D /* PrefsDisplays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0361B9A723D732A300B6252D /* PrefsDisplays.swift */; }; 0361B9A923D732A300B6252D /* PrefsDisplays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0361B9A723D732A300B6252D /* PrefsDisplays.swift */; }; 0361B9AB23D73D4500B6252D /* PrefsTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0361B9AA23D73D4500B6252D /* PrefsTime.swift */; }; 0361B9AC23D73D4500B6252D /* PrefsTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0361B9AA23D73D4500B6252D /* PrefsTime.swift */; }; 0369985D2196103300E359D3 /* missingvideos.json in Resources */ = {isa = PBXBuildFile; fileRef = 0369985C2196103300E359D3 /* missingvideos.json */; }; 0369985E2196129C00E359D3 /* missingvideos.json in Resources */ = {isa = PBXBuildFile; fileRef = 0369985C2196103300E359D3 /* missingvideos.json */; }; 036A57D523F30DD00009DC02 /* DownloadIndicatorLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A57D423F30DD00009DC02 /* DownloadIndicatorLayer.swift */; }; 036A57D623F30F490009DC02 /* DownloadIndicatorLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A57D423F30DD00009DC02 /* DownloadIndicatorLayer.swift */; }; 036A57D823F470940009DC02 /* InfoCountdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A57D723F470940009DC02 /* InfoCountdownView.swift */; }; 036A57D923F4747D0009DC02 /* InfoCountdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A57D723F470940009DC02 /* InfoCountdownView.swift */; }; 036A57DB23F5820A0009DC02 /* CountdownLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A57DA23F5820A0009DC02 /* CountdownLayer.swift */; }; 036A57DC23F5828E0009DC02 /* CountdownLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A57DA23F5820A0009DC02 /* CountdownLayer.swift */; }; 036A7E9B26370C260019186B /* Forecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A7E9A26370C260019186B /* Forecast.swift */; }; 036A7E9C26370C260019186B /* Forecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A7E9A26370C260019186B /* Forecast.swift */; }; 036A7E9D26370C260019186B /* Forecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A7E9A26370C260019186B /* Forecast.swift */; }; 0374C9FE247AC5BC002F29D3 /* Locations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0374C9FD247AC5BC002F29D3 /* Locations.swift */; }; 0374C9FF247AC5BC002F29D3 /* Locations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0374C9FD247AC5BC002F29D3 /* Locations.swift */; }; 037772E124E43AF300D81EEA /* TimeSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037772DF24E43AF300D81EEA /* TimeSetupViewController.swift */; }; 037772E224E43AF300D81EEA /* TimeSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037772DF24E43AF300D81EEA /* TimeSetupViewController.swift */; }; 037772E324E43AF300D81EEA /* TimeSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037772DF24E43AF300D81EEA /* TimeSetupViewController.swift */; }; 037772E424E43AF300D81EEA /* TimeSetupViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 037772E024E43AF300D81EEA /* TimeSetupViewController.xib */; }; 037772E524E43AF300D81EEA /* TimeSetupViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 037772E024E43AF300D81EEA /* TimeSetupViewController.xib */; }; 037772E624E43AF300D81EEA /* TimeSetupViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 037772E024E43AF300D81EEA /* TimeSetupViewController.xib */; }; 037772E824E44BB100D81EEA /* xmark.circle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 037772E724E44BB100D81EEA /* xmark.circle.pdf */; }; 037772E924E44BB100D81EEA /* xmark.circle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 037772E724E44BB100D81EEA /* xmark.circle.pdf */; }; 037772EA24E44BB100D81EEA /* xmark.circle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 037772E724E44BB100D81EEA /* xmark.circle.pdf */; }; 037772ED24E44CE100D81EEA /* RecapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037772EB24E44CE100D81EEA /* RecapViewController.swift */; }; 037772EE24E44CE100D81EEA /* RecapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037772EB24E44CE100D81EEA /* RecapViewController.swift */; }; 037772EF24E44CE100D81EEA /* RecapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037772EB24E44CE100D81EEA /* RecapViewController.swift */; }; 037772F024E44CE100D81EEA /* RecapViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 037772EC24E44CE100D81EEA /* RecapViewController.xib */; }; 037772F124E44CE100D81EEA /* RecapViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 037772EC24E44CE100D81EEA /* RecapViewController.xib */; }; 037772F224E44CE100D81EEA /* RecapViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 037772EC24E44CE100D81EEA /* RecapViewController.xib */; }; 037772FC24E4668600D81EEA /* aspectratio.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 037772F424E4668600D81EEA /* aspectratio.pdf */; }; 037772FD24E4668600D81EEA /* aspectratio.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 037772F424E4668600D81EEA /* aspectratio.pdf */; }; 037772FE24E4668600D81EEA /* aspectratio.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 037772F424E4668600D81EEA /* aspectratio.pdf */; }; 0377BF8624BA15E700C33F9F /* btn_donate.png in Resources */ = {isa = PBXBuildFile; fileRef = 0377BF6F24BA15E600C33F9F /* btn_donate.png */; }; 0377BF8724BA15E700C33F9F /* btn_donate.png in Resources */ = {isa = PBXBuildFile; fileRef = 0377BF6F24BA15E600C33F9F /* btn_donate.png */; }; 0377BF8824BA15E700C33F9F /* btn_donate.png in Resources */ = {isa = PBXBuildFile; fileRef = 0377BF6F24BA15E600C33F9F /* btn_donate.png */; }; 0378985E24C35F8A009B9418 /* CacheViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0378985C24C35F8A009B9418 /* CacheViewController.swift */; }; 0378985F24C35F8A009B9418 /* CacheViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0378985C24C35F8A009B9418 /* CacheViewController.swift */; }; 0378986024C35F8A009B9418 /* CacheViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0378985C24C35F8A009B9418 /* CacheViewController.swift */; }; 0378986124C35F8A009B9418 /* CacheViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0378985D24C35F8A009B9418 /* CacheViewController.xib */; }; 0378986224C35F8A009B9418 /* CacheViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0378985D24C35F8A009B9418 /* CacheViewController.xib */; }; 0378986324C35F8A009B9418 /* CacheViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0378985D24C35F8A009B9418 /* CacheViewController.xib */; }; 0385FC59242B9AE1007E6513 /* APISecrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0385FC58242B9AE1007E6513 /* APISecrets.swift */; }; 0385FC5A242B9AE1007E6513 /* APISecrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0385FC58242B9AE1007E6513 /* APISecrets.swift */; }; 0385FC5D242B9F6E007E6513 /* InfoWeatherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0385FC5C242B9F6E007E6513 /* InfoWeatherView.swift */; }; 0385FC6D242BA097007E6513 /* InfoWeatherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0385FC5C242B9F6E007E6513 /* InfoWeatherView.swift */; }; 03893CB3217749F0008E7125 /* ErrorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03893CB2217749F0008E7125 /* ErrorLog.swift */; }; 03893CB4217753AC008E7125 /* ErrorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03893CB2217749F0008E7125 /* ErrorLog.swift */; }; 038C584723A9304800224630 /* InfoContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038C584623A9304800224630 /* InfoContainerView.swift */; }; 038C584823A9308C00224630 /* InfoContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038C584623A9304800224630 /* InfoContainerView.swift */; }; 038C584A23A9394000224630 /* InfoCommonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038C584923A9394000224630 /* InfoCommonView.swift */; }; 038C584B23A9394000224630 /* InfoCommonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038C584923A9394000224630 /* InfoCommonView.swift */; }; 038D2EBD23AB91C300CD91F7 /* InfoLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A596D823AB8F000097EA66 /* InfoLocationView.swift */; }; 038D2EDE23B0FB0D00CD91F7 /* PrefsVideos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038D2EDD23B0FB0D00CD91F7 /* PrefsVideos.swift */; }; 038D2EDF23B0FB0D00CD91F7 /* PrefsVideos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038D2EDD23B0FB0D00CD91F7 /* PrefsVideos.swift */; }; 038D2EE423B6565900CD91F7 /* InfoBatteryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038D2EE323B6565900CD91F7 /* InfoBatteryView.swift */; }; 038D2EE523B6565900CD91F7 /* InfoBatteryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038D2EE323B6565900CD91F7 /* InfoBatteryView.swift */; }; 03933B8B24C3986800A98D94 /* SourcesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03933B8924C3986800A98D94 /* SourcesViewController.swift */; }; 03933B8C24C3986800A98D94 /* SourcesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03933B8924C3986800A98D94 /* SourcesViewController.swift */; }; 03933B8D24C3986800A98D94 /* SourcesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03933B8924C3986800A98D94 /* SourcesViewController.swift */; }; 03933B8E24C3986800A98D94 /* SourcesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03933B8A24C3986800A98D94 /* SourcesViewController.xib */; }; 03933B8F24C3986800A98D94 /* SourcesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03933B8A24C3986800A98D94 /* SourcesViewController.xib */; }; 03933B9024C3986800A98D94 /* SourcesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03933B8A24C3986800A98D94 /* SourcesViewController.xib */; }; 0393857A2175D4B80040B850 /* AVPlayerViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039385792175D4B80040B850 /* AVPlayerViewExtension.swift */; }; 0393857B2175D4B80040B850 /* AVPlayerViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039385792175D4B80040B850 /* AVPlayerViewExtension.swift */; }; 03958349217F4416008E8F9C /* Solar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03958348217F4416008E8F9C /* Solar.swift */; }; 0395834A217F442A008E8F9C /* Solar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03958348217F4416008E8F9C /* Solar.swift */; }; 0395835321807D1F008E8F9C /* thumbnail@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0395835121807D1F008E8F9C /* thumbnail@2x.png */; }; 0395835421807D1F008E8F9C /* thumbnail@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0395835121807D1F008E8F9C /* thumbnail@2x.png */; }; 0395835521807D1F008E8F9C /* thumbnail.png in Resources */ = {isa = PBXBuildFile; fileRef = 0395835221807D1F008E8F9C /* thumbnail.png */; }; 0395835621807D1F008E8F9C /* thumbnail.png in Resources */ = {isa = PBXBuildFile; fileRef = 0395835221807D1F008E8F9C /* thumbnail.png */; }; 0396D50B24B8B7ED00CC021B /* DisplayDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1E7892284471A00D10CF7 /* DisplayDetection.swift */; }; 0396D50C24B8B7ED00CC021B /* LayerOffsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0306336A23A142FA00046A59 /* LayerOffsets.swift */; }; 0396D50D24B8B7ED00CC021B /* APISecrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0385FC58242B9AE1007E6513 /* APISecrets.swift */; }; 0396D50E24B8B7ED00CC021B /* NightShift.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00864B623AAE8E9003210EF /* NightShift.swift */; }; 0396D51024B8B7ED00CC021B /* CheckCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F441BE1756D007F2A20 /* CheckCellView.swift */; }; 0396D51124B8B7ED00CC021B /* PrefsUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034116D223F9BD3100CD7674 /* PrefsUpdates.swift */; }; 0396D51224B8B7ED00CC021B /* AerialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F431BE1756D007F2A20 /* AerialView.swift */; }; 0396D51324B8B7ED00CC021B /* PrefsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031FB78A248A87330054BAFD /* PrefsCache.swift */; }; 0396D51424B8B7ED00CC021B /* AVPlayerViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039385792175D4B80040B850 /* AVPlayerViewExtension.swift */; }; 0396D51524B8B7ED00CC021B /* AsynchronousOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E8730E216501ED002B469B /* AsynchronousOperation.swift */; }; 0396D51624B8B7ED00CC021B /* LayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF51BB23A2643C008AD373 /* LayerManager.swift */; }; 0396D51824B8B7ED00CC021B /* IOBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 03510C6E21834F38008F74F2 /* IOBridge.m */; }; 0396D51924B8B7ED00CC021B /* DarkMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00864B323AAE7F0003210EF /* DarkMode.swift */; }; 0396D51A24B8B7ED00CC021B /* CustomVideoFolders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313F9EE22955F3B00B074BB /* CustomVideoFolders.swift */; }; 0396D51B24B8B7ED00CC021B /* InfoTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030DDA8F2423C3BE0072D5C9 /* InfoTimerView.swift */; }; 0396D51D24B8B7ED00CC021B /* MessageLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF51BE23A274CA008AD373 /* MessageLayer.swift */; }; 0396D51E24B8B7ED00CC021B /* InfoCommonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038C584923A9394000224630 /* InfoCommonView.swift */; }; 0396D51F24B8B7ED00CC021B /* AerialView+Brightness.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032D1163239A7F0C007E7756 /* AerialView+Brightness.swift */; }; 0396D52124B8B7ED00CC021B /* Locations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0374C9FD247AC5BC002F29D3 /* Locations.swift */; }; 0396D52224B8B7ED00CC021B /* VideoCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF450231BE2D2FD00C1F98A /* VideoCache.swift */; }; 0396D52324B8B7ED00CC021B /* BatteryIconLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030A0F28245C7C7D009E1D97 /* BatteryIconLayer.swift */; }; 0396D52424B8B7ED00CC021B /* PrefsDisplays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0361B9A723D732A300B6252D /* PrefsDisplays.swift */; }; 0396D52624B8B7ED00CC021B /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03DC004D248BC5A4005DB0F4 /* Cache.swift */; }; 0396D52824B8B7ED00CC021B /* AnimationTextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0306336D23A15FA900046A59 /* AnimationTextLayer.swift */; }; 0396D52924B8B7ED00CC021B /* InfoWeatherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0385FC5C242B9F6E007E6513 /* InfoWeatherView.swift */; }; 0396D52B24B8B7ED00CC021B /* SeededGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313F9EB2294468600B074BB /* SeededGenerator.swift */; }; 0396D52C24B8B7ED00CC021B /* InfoDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FA49B32428EE3300863AF6 /* InfoDateView.swift */; }; 0396D52D24B8B7ED00CC021B /* ManifestLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F661BE1778C007F2A20 /* ManifestLoader.swift */; }; 0396D52E24B8B7ED00CC021B /* TimerLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FA49A02423DA1F00863AF6 /* TimerLayer.swift */; }; 0396D52F24B8B7ED00CC021B /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034A6DC324ACC80200D62129 /* Source.swift */; }; 0396D53024B8B7ED00CC021B /* PrefsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034F29BE23A7E28E004B34D5 /* PrefsInfo.swift */; }; 0396D53124B8B7ED00CC021B /* ClockLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF51C123A2978B008AD373 /* ClockLayer.swift */; }; 0396D53224B8B7ED00CC021B /* AerialVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F401BE1756D007F2A20 /* AerialVideo.swift */; }; 0396D53324B8B7ED00CC021B /* ErrorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03893CB2217749F0008E7125 /* ErrorLog.swift */; }; 0396D53424B8B7ED00CC021B /* InfoSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0354D0EB23F6CB7B00D86F9E /* InfoSettingsView.swift */; }; 0396D53524B8B7ED00CC021B /* NSImage+trim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A4A80A2451CE2C00A1F7A3 /* NSImage+trim.swift */; }; 0396D53624B8B7ED00CC021B /* Battery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032D1160239A7D82007E7756 /* Battery.swift */; }; 0396D53724B8B7ED00CC021B /* VideoList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C97BA624B5F74900739CED /* VideoList.swift */; }; 0396D53824B8B7ED00CC021B /* InfoBatteryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038D2EE323B6565900CD91F7 /* InfoBatteryView.swift */; }; 0396D53A24B8B7ED00CC021B /* DateLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FA49B62428F84500863AF6 /* DateLayer.swift */; }; 0396D53B24B8B7ED00CC021B /* AerialPlayerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E2E5D1FC62E8B00E5F320 /* AerialPlayerItem.swift */; }; 0396D53C24B8B7ED00CC021B /* HardwareDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03608A2B22A56465008F08A2 /* HardwareDetection.swift */; }; 0396D53E24B8B7ED00CC021B /* YahooLogoLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A42A9F2449F959003B3012 /* YahooLogoLayer.swift */; }; 0396D54024B8B7ED00CC021B /* InfoMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A596D223AA750F0097EA66 /* InfoMessageView.swift */; }; 0396D54224B8B7ED00CC021B /* ConditionLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A42AA2244A0E5F003B3012 /* ConditionLayer.swift */; }; 0396D54324B8B7ED00CC021B /* VideoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A2CB9B216BA9AF0061E8E8 /* VideoManager.swift */; }; 0396D54524B8B7ED00CC021B /* InfoContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038C584623A9304800224630 /* InfoContainerView.swift */; }; 0396D54624B8B7ED00CC021B /* AerialView+Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035D524E239AA31A00DC29DC /* AerialView+Player.swift */; }; 0396D54724B8B7ED00CC021B /* CustomVideoFolders+helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AD45FE22981B0C00261325 /* CustomVideoFolders+helpers.swift */; }; 0396D54824B8B7ED00CC021B /* VideoLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF450201BE2B45D00C1F98A /* VideoLoader.swift */; }; 0396D54924B8B7ED00CC021B /* TimeManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E8731221675FE0002B469B /* TimeManagement.swift */; }; 0396D54A24B8B7ED00CC021B /* DownloadIndicatorLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A57D423F30DD00009DC02 /* DownloadIndicatorLayer.swift */; }; 0396D54B24B8B7ED00CC021B /* CountdownLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A57DA23F5820A0009DC02 /* CountdownLayer.swift */; }; 0396D54C24B8B7ED00CC021B /* SourceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034A6DC124ACC7C800D62129 /* SourceList.swift */; }; 0396D54D24B8B7ED00CC021B /* DescriptionCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C344FE24B7A22300906EA6 /* DescriptionCellView.swift */; }; 0396D54E24B8B7ED00CC021B /* DisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1E78622842FB300D10CF7 /* DisplayView.swift */; }; 0396D54F24B8B7ED00CC021B /* InfoLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A596D823AB8F000097EA66 /* InfoLocationView.swift */; }; 0396D55024B8B7ED00CC021B /* AssetLoaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB22A7D1BE17D7D0065C0F5 /* AssetLoaderDelegate.swift */; }; 0396D55124B8B7ED00CC021B /* PrefsVideos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038D2EDD23B0FB0D00CD91F7 /* PrefsVideos.swift */; }; 0396D55224B8B7ED00CC021B /* LocationLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0306336723A1012200046A59 /* LocationLayer.swift */; }; 0396D55424B8B7ED00CC021B /* InfoTableSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034F29B623A7A93D004B34D5 /* InfoTableSource.swift */; }; 0396D55524B8B7ED00CC021B /* Solar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03958348217F4416008E8F9C /* Solar.swift */; }; 0396D55624B8B7ED00CC021B /* Brightness.swift in Sources */ = {isa = PBXBuildFile; fileRef = F008DAFC23AADCFB00739DE1 /* Brightness.swift */; }; 0396D55724B8B7ED00CC021B /* SourceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C97BC224B6210500739CED /* SourceInfo.swift */; }; 0396D55924B8B7ED00CC021B /* PrefsTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0361B9AA23D73D4500B6252D /* PrefsTime.swift */; }; 0396D55A24B8B7ED00CC021B /* PoiStringProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03233B67217272640077D3F9 /* PoiStringProvider.swift */; }; 0396D55B24B8B7ED00CC021B /* PrefsAdvanced.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A4A80D2451D04C00A1F7A3 /* PrefsAdvanced.swift */; }; 0396D55C24B8B7ED00CC021B /* WeatherLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033842E024489D7300A2C523 /* WeatherLayer.swift */; }; 0396D55D24B8B7ED00CC021B /* VideoDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA36BD3E1BE57F8E00D5E03B /* VideoDownload.swift */; }; 0396D55E24B8B7ED00CC021B /* FileHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C97BBF24B60E2F00739CED /* FileHelpers.swift */; }; 0396D55F24B8B7ED00CC021B /* InfoSettingsTableSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0354D0E823F6C31800D86F9E /* InfoSettingsTableSource.swift */; }; 0396D56024B8B7ED00CC021B /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E8730B2165013C002B469B /* DownloadManager.swift */; }; 0396D56224B8B7ED00CC021B /* InfoCountdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A57D723F470940009DC02 /* InfoCountdownView.swift */; }; 0396D56324B8B7ED00CC021B /* ConditionSymbolLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D68802453080C0016F837 /* ConditionSymbolLayer.swift */; }; 0396D56424B8B7ED00CC021B /* CheckboxCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C344FB24B778EE00906EA6 /* CheckboxCellView.swift */; }; 0396D56524B8B7ED00CC021B /* AnimationLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034587312449D8EB00C97D1B /* AnimationLayer.swift */; }; 0396D56624B8B7ED00CC021B /* AnimatableLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0345872C2449C52F00C97D1B /* AnimatableLayer.swift */; }; 0396D56724B8B7ED00CC021B /* InfoClockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A596D423AA752F0097EA66 /* InfoClockView.swift */; }; 0396D56824B8B7ED00CC021B /* CustomVideoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313F9E722942B4500B074BB /* CustomVideoController.swift */; }; 0396D56A24B8B7ED00CC021B /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03510C722185EF76008F74F2 /* CoreLocation.framework */; }; 0396D56D24B8B7ED00CC021B /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03510C6A21834EB2008F74F2 /* IOKit.framework */; }; 0396D56F24B8B7ED00CC021B /* Aerial-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 03510C6D21834F38008F74F2 /* Aerial-Bridging-Header.h */; }; 0396D57624B8B7ED00CC021B /* white_retina.png in Resources */ = {isa = PBXBuildFile; fileRef = 03A42A822449F5EA003B3012 /* white_retina.png */; }; 0396D57924B8B7ED00CC021B /* screen2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E78E22848F7F00D10CF7 /* screen2.jpg */; }; 0396D58B24B8B7ED00CC021B /* screen0.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E79022848F7F00D10CF7 /* screen0.jpg */; }; 0396D58D24B8B7ED00CC021B /* thumbnail@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0395835121807D1F008E8F9C /* thumbnail@2x.png */; }; 0396D58E24B8B7ED00CC021B /* thumbnail.png in Resources */ = {isa = PBXBuildFile; fileRef = 0395835221807D1F008E8F9C /* thumbnail.png */; }; 0396D59224B8B7ED00CC021B /* screen1.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E78F22848F7F00D10CF7 /* screen1.jpg */; }; 0396D59D24B8B7ED00CC021B /* missingvideos.json in Resources */ = {isa = PBXBuildFile; fileRef = 0369985C2196103300E359D3 /* missingvideos.json */; }; 0396D5A824B8B7ED00CC021B /* CustomVideos.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0313F9E522942AA500B074BB /* CustomVideos.xib */; }; 0396D5AB24B8B7ED00CC021B /* purple_retina.png in Resources */ = {isa = PBXBuildFile; fileRef = 03A42A9B2449F601003B3012 /* purple_retina.png */; }; 03977EF1250E6918008FBAFD /* sv.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE2250E6917008FBAFD /* sv.json */; }; 03977EF2250E6918008FBAFD /* sv.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE2250E6917008FBAFD /* sv.json */; }; 03977EF3250E6918008FBAFD /* sv.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE2250E6917008FBAFD /* sv.json */; }; 03977EF4250E6918008FBAFD /* ja.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE3250E6917008FBAFD /* ja.json */; }; 03977EF5250E6918008FBAFD /* ja.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE3250E6917008FBAFD /* ja.json */; }; 03977EF6250E6918008FBAFD /* ja.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE3250E6917008FBAFD /* ja.json */; }; 03977EF7250E6918008FBAFD /* pt.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE4250E6917008FBAFD /* pt.json */; }; 03977EF8250E6918008FBAFD /* pt.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE4250E6917008FBAFD /* pt.json */; }; 03977EF9250E6918008FBAFD /* pt.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE4250E6917008FBAFD /* pt.json */; }; 03977EFA250E6918008FBAFD /* zh_TW.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE5250E6917008FBAFD /* zh_TW.json */; }; 03977EFB250E6918008FBAFD /* zh_TW.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE5250E6917008FBAFD /* zh_TW.json */; }; 03977EFC250E6918008FBAFD /* zh_TW.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE5250E6917008FBAFD /* zh_TW.json */; }; 03977EFD250E6918008FBAFD /* pl.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE6250E6917008FBAFD /* pl.json */; }; 03977EFE250E6918008FBAFD /* pl.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE6250E6917008FBAFD /* pl.json */; }; 03977EFF250E6918008FBAFD /* pl.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE6250E6917008FBAFD /* pl.json */; }; 03977F00250E6918008FBAFD /* de.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE7250E6918008FBAFD /* de.json */; }; 03977F01250E6918008FBAFD /* de.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE7250E6918008FBAFD /* de.json */; }; 03977F02250E6918008FBAFD /* de.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE7250E6918008FBAFD /* de.json */; }; 03977F03250E6918008FBAFD /* fr.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE8250E6918008FBAFD /* fr.json */; }; 03977F04250E6918008FBAFD /* fr.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE8250E6918008FBAFD /* fr.json */; }; 03977F05250E6918008FBAFD /* fr.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE8250E6918008FBAFD /* fr.json */; }; 03977F06250E6918008FBAFD /* es.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE9250E6918008FBAFD /* es.json */; }; 03977F07250E6918008FBAFD /* es.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE9250E6918008FBAFD /* es.json */; }; 03977F08250E6918008FBAFD /* es.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EE9250E6918008FBAFD /* es.json */; }; 03977F09250E6918008FBAFD /* pt_BR.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EEA250E6918008FBAFD /* pt_BR.json */; }; 03977F0A250E6918008FBAFD /* pt_BR.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EEA250E6918008FBAFD /* pt_BR.json */; }; 03977F0B250E6918008FBAFD /* pt_BR.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EEA250E6918008FBAFD /* pt_BR.json */; }; 03977F0C250E6918008FBAFD /* it.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EEB250E6918008FBAFD /* it.json */; }; 03977F0D250E6918008FBAFD /* it.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EEB250E6918008FBAFD /* it.json */; }; 03977F0E250E6918008FBAFD /* it.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EEB250E6918008FBAFD /* it.json */; }; 03977F0F250E6918008FBAFD /* he.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EEC250E6918008FBAFD /* he.json */; }; 03977F10250E6918008FBAFD /* he.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EEC250E6918008FBAFD /* he.json */; }; 03977F11250E6918008FBAFD /* he.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EEC250E6918008FBAFD /* he.json */; }; 03977F12250E6918008FBAFD /* en.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EED250E6918008FBAFD /* en.json */; }; 03977F13250E6918008FBAFD /* en.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EED250E6918008FBAFD /* en.json */; }; 03977F14250E6918008FBAFD /* en.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EED250E6918008FBAFD /* en.json */; }; 03977F15250E6918008FBAFD /* zh_CN.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EEE250E6918008FBAFD /* zh_CN.json */; }; 03977F16250E6918008FBAFD /* zh_CN.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EEE250E6918008FBAFD /* zh_CN.json */; }; 03977F17250E6918008FBAFD /* zh_CN.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EEE250E6918008FBAFD /* zh_CN.json */; }; 03977F18250E6918008FBAFD /* ar.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EEF250E6918008FBAFD /* ar.json */; }; 03977F19250E6918008FBAFD /* ar.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EEF250E6918008FBAFD /* ar.json */; }; 03977F1A250E6918008FBAFD /* ar.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EEF250E6918008FBAFD /* ar.json */; }; 03977F1B250E6918008FBAFD /* nl.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EF0250E6918008FBAFD /* nl.json */; }; 03977F1C250E6918008FBAFD /* nl.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EF0250E6918008FBAFD /* nl.json */; }; 03977F1D250E6918008FBAFD /* nl.json in Resources */ = {isa = PBXBuildFile; fileRef = 03977EF0250E6918008FBAFD /* nl.json */; }; 03977F20250E7165008FBAFD /* TimeMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03977F1F250E7165008FBAFD /* TimeMachine.swift */; }; 03977F21250E7165008FBAFD /* TimeMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03977F1F250E7165008FBAFD /* TimeMachine.swift */; }; 03977F22250E7165008FBAFD /* TimeMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03977F1F250E7165008FBAFD /* TimeMachine.swift */; }; 03A2CB9C216BA9AF0061E8E8 /* VideoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A2CB9B216BA9AF0061E8E8 /* VideoManager.swift */; }; 03A2CB9D216BB1490061E8E8 /* VideoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A2CB9B216BA9AF0061E8E8 /* VideoManager.swift */; }; 03A42A992449F5EA003B3012 /* white_retina.png in Resources */ = {isa = PBXBuildFile; fileRef = 03A42A822449F5EA003B3012 /* white_retina.png */; }; 03A42A9A2449F5EA003B3012 /* white_retina.png in Resources */ = {isa = PBXBuildFile; fileRef = 03A42A822449F5EA003B3012 /* white_retina.png */; }; 03A42A9C2449F602003B3012 /* purple_retina.png in Resources */ = {isa = PBXBuildFile; fileRef = 03A42A9B2449F601003B3012 /* purple_retina.png */; }; 03A42A9D2449F602003B3012 /* purple_retina.png in Resources */ = {isa = PBXBuildFile; fileRef = 03A42A9B2449F601003B3012 /* purple_retina.png */; }; 03A42AA02449F959003B3012 /* YahooLogoLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A42A9F2449F959003B3012 /* YahooLogoLayer.swift */; }; 03A42AA12449F959003B3012 /* YahooLogoLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A42A9F2449F959003B3012 /* YahooLogoLayer.swift */; }; 03A42AA3244A0E5F003B3012 /* ConditionLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A42AA2244A0E5F003B3012 /* ConditionLayer.swift */; }; 03A42AA4244A0E5F003B3012 /* ConditionLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A42AA2244A0E5F003B3012 /* ConditionLayer.swift */; }; 03A4A80B2451CE2C00A1F7A3 /* NSImage+trim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A4A80A2451CE2C00A1F7A3 /* NSImage+trim.swift */; }; 03A4A80C2451CE2C00A1F7A3 /* NSImage+trim.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A4A80A2451CE2C00A1F7A3 /* NSImage+trim.swift */; }; 03A4A80E2451D04C00A1F7A3 /* PrefsAdvanced.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A4A80D2451D04C00A1F7A3 /* PrefsAdvanced.swift */; }; 03A4A80F2451D04C00A1F7A3 /* PrefsAdvanced.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A4A80D2451D04C00A1F7A3 /* PrefsAdvanced.swift */; }; 03A596D323AA750F0097EA66 /* InfoMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A596D223AA750F0097EA66 /* InfoMessageView.swift */; }; 03A596D523AA752F0097EA66 /* InfoClockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A596D423AA752F0097EA66 /* InfoClockView.swift */; }; 03A596D623AA752F0097EA66 /* InfoClockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A596D423AA752F0097EA66 /* InfoClockView.swift */; }; 03A596D723AA75490097EA66 /* InfoMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A596D223AA750F0097EA66 /* InfoMessageView.swift */; }; 03A596D923AB8F000097EA66 /* InfoLocationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A596D823AB8F000097EA66 /* InfoLocationView.swift */; }; 03A6D14625F109B900960135 /* OpenWeather.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A6D14525F109B900960135 /* OpenWeather.swift */; }; 03A6D14725F109B900960135 /* OpenWeather.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A6D14525F109B900960135 /* OpenWeather.swift */; }; 03A6D14825F109B900960135 /* OpenWeather.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A6D14525F109B900960135 /* OpenWeather.swift */; }; 03A6D14F25F294C900960135 /* location.north.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03A6D14E25F294C900960135 /* location.north.pdf */; }; 03A6D15025F294C900960135 /* location.north.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03A6D14E25F294C900960135 /* location.north.pdf */; }; 03A6D15125F294C900960135 /* location.north.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03A6D14E25F294C900960135 /* location.north.pdf */; }; 03A6D15325F297CE00960135 /* WindDirectionLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A6D15225F297CE00960135 /* WindDirectionLayer.swift */; }; 03A6D15425F297CE00960135 /* WindDirectionLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A6D15225F297CE00960135 /* WindDirectionLayer.swift */; }; 03A6D15525F297CE00960135 /* WindDirectionLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03A6D15225F297CE00960135 /* WindDirectionLayer.swift */; }; 03AA27BD2614A6F800A4D2CF /* ko.json in Resources */ = {isa = PBXBuildFile; fileRef = 03AA27BC2614A6F800A4D2CF /* ko.json */; }; 03AA27BE2614A6F800A4D2CF /* ko.json in Resources */ = {isa = PBXBuildFile; fileRef = 03AA27BC2614A6F800A4D2CF /* ko.json */; }; 03AA27BF2614A6F800A4D2CF /* ko.json in Resources */ = {isa = PBXBuildFile; fileRef = 03AA27BC2614A6F800A4D2CF /* ko.json */; }; 03AA32622631A8F2002198C3 /* GeoCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AA32612631A8F2002198C3 /* GeoCoding.swift */; }; 03AA32632631A8F2002198C3 /* GeoCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AA32612631A8F2002198C3 /* GeoCoding.swift */; }; 03AA32642631A8F2002198C3 /* GeoCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AA32612631A8F2002198C3 /* GeoCoding.swift */; }; 03AA7A5D24C84C6300A47970 /* cloud.bolt.rain.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A2524C84C6200A47970 /* cloud.bolt.rain.pdf */; }; 03AA7A5E24C84C6300A47970 /* cloud.bolt.rain.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A2524C84C6200A47970 /* cloud.bolt.rain.pdf */; }; 03AA7A5F24C84C6300A47970 /* cloud.bolt.rain.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A2524C84C6200A47970 /* cloud.bolt.rain.pdf */; }; 03AA7A6024C84C6300A47970 /* wind.snow.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A3C24C84C6200A47970 /* wind.snow.pdf */; }; 03AA7A6124C84C6300A47970 /* wind.snow.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A3C24C84C6200A47970 /* wind.snow.pdf */; }; 03AA7A6224C84C6300A47970 /* wind.snow.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A3C24C84C6200A47970 /* wind.snow.pdf */; }; 03AA7A6324C84C6300A47970 /* snow.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A3D24C84C6200A47970 /* snow.pdf */; }; 03AA7A6424C84C6300A47970 /* snow.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A3D24C84C6200A47970 /* snow.pdf */; }; 03AA7A6524C84C6300A47970 /* snow.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A3D24C84C6200A47970 /* snow.pdf */; }; 03AA7A6624C84C6300A47970 /* moon.stars.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A3E24C84C6200A47970 /* moon.stars.pdf */; }; 03AA7A6724C84C6300A47970 /* moon.stars.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A3E24C84C6200A47970 /* moon.stars.pdf */; }; 03AA7A6824C84C6300A47970 /* moon.stars.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A3E24C84C6200A47970 /* moon.stars.pdf */; }; 03AA7A6924C84C6300A47970 /* cloud.heavyrain.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A3F24C84C6200A47970 /* cloud.heavyrain.pdf */; }; 03AA7A6A24C84C6300A47970 /* cloud.heavyrain.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A3F24C84C6200A47970 /* cloud.heavyrain.pdf */; }; 03AA7A6B24C84C6300A47970 /* cloud.heavyrain.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A3F24C84C6200A47970 /* cloud.heavyrain.pdf */; }; 03AA7A6C24C84C6300A47970 /* battery.0.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4024C84C6200A47970 /* battery.0.pdf */; }; 03AA7A6D24C84C6300A47970 /* battery.0.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4024C84C6200A47970 /* battery.0.pdf */; }; 03AA7A6E24C84C6300A47970 /* battery.0.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4024C84C6200A47970 /* battery.0.pdf */; }; 03AA7A6F24C84C6300A47970 /* wrench.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4124C84C6200A47970 /* wrench.pdf */; }; 03AA7A7024C84C6300A47970 /* wrench.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4124C84C6200A47970 /* wrench.pdf */; }; 03AA7A7124C84C6300A47970 /* wrench.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4124C84C6200A47970 /* wrench.pdf */; }; 03AA7A7224C84C6300A47970 /* cloud.sun.rain.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4224C84C6200A47970 /* cloud.sun.rain.pdf */; }; 03AA7A7324C84C6300A47970 /* cloud.sun.rain.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4224C84C6200A47970 /* cloud.sun.rain.pdf */; }; 03AA7A7424C84C6300A47970 /* cloud.sun.rain.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4224C84C6200A47970 /* cloud.sun.rain.pdf */; }; 03AA7A7524C84C6300A47970 /* sun.dust.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4324C84C6200A47970 /* sun.dust.pdf */; }; 03AA7A7624C84C6300A47970 /* sun.dust.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4324C84C6200A47970 /* sun.dust.pdf */; }; 03AA7A7724C84C6300A47970 /* sun.dust.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4324C84C6200A47970 /* sun.dust.pdf */; }; 03AA7A7824C84C6300A47970 /* sun.max.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4424C84C6200A47970 /* sun.max.pdf */; }; 03AA7A7924C84C6300A47970 /* sun.max.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4424C84C6200A47970 /* sun.max.pdf */; }; 03AA7A7A24C84C6300A47970 /* sun.max.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4424C84C6200A47970 /* sun.max.pdf */; }; 03AA7A7B24C84C6300A47970 /* cloud.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4524C84C6200A47970 /* cloud.pdf */; }; 03AA7A7C24C84C6300A47970 /* cloud.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4524C84C6200A47970 /* cloud.pdf */; }; 03AA7A7D24C84C6300A47970 /* cloud.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4524C84C6200A47970 /* cloud.pdf */; }; 03AA7A7E24C84C6300A47970 /* cloud.sun.bolt.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4624C84C6200A47970 /* cloud.sun.bolt.pdf */; }; 03AA7A7F24C84C6300A47970 /* cloud.sun.bolt.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4624C84C6200A47970 /* cloud.sun.bolt.pdf */; }; 03AA7A8024C84C6300A47970 /* cloud.sun.bolt.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4624C84C6200A47970 /* cloud.sun.bolt.pdf */; }; 03AA7A8124C84C6300A47970 /* thermometer.snowflake.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4724C84C6200A47970 /* thermometer.snowflake.pdf */; }; 03AA7A8224C84C6300A47970 /* thermometer.snowflake.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4724C84C6200A47970 /* thermometer.snowflake.pdf */; }; 03AA7A8324C84C6300A47970 /* thermometer.snowflake.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4724C84C6200A47970 /* thermometer.snowflake.pdf */; }; 03AA7A8424C84C6300A47970 /* hurricane.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4824C84C6200A47970 /* hurricane.pdf */; }; 03AA7A8524C84C6300A47970 /* hurricane.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4824C84C6200A47970 /* hurricane.pdf */; }; 03AA7A8624C84C6300A47970 /* hurricane.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4824C84C6200A47970 /* hurricane.pdf */; }; 03AA7A8724C84C6300A47970 /* cloud.fog.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4924C84C6200A47970 /* cloud.fog.pdf */; }; 03AA7A8824C84C6300A47970 /* cloud.fog.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4924C84C6200A47970 /* cloud.fog.pdf */; }; 03AA7A8924C84C6300A47970 /* cloud.fog.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4924C84C6200A47970 /* cloud.fog.pdf */; }; 03AA7A8A24C84C6300A47970 /* thermometer.sun.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4A24C84C6200A47970 /* thermometer.sun.pdf */; }; 03AA7A8B24C84C6300A47970 /* thermometer.sun.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4A24C84C6200A47970 /* thermometer.sun.pdf */; }; 03AA7A8C24C84C6300A47970 /* thermometer.sun.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4A24C84C6200A47970 /* thermometer.sun.pdf */; }; 03AA7A8D24C84C6300A47970 /* tropicalstorm.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4B24C84C6200A47970 /* tropicalstorm.pdf */; }; 03AA7A8E24C84C6300A47970 /* tropicalstorm.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4B24C84C6200A47970 /* tropicalstorm.pdf */; }; 03AA7A8F24C84C6300A47970 /* tropicalstorm.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4B24C84C6200A47970 /* tropicalstorm.pdf */; }; 03AA7A9024C84C6300A47970 /* moon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4C24C84C6300A47970 /* moon.pdf */; }; 03AA7A9124C84C6300A47970 /* moon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4C24C84C6300A47970 /* moon.pdf */; }; 03AA7A9224C84C6300A47970 /* moon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4C24C84C6300A47970 /* moon.pdf */; }; 03AA7A9324C84C6300A47970 /* sun.min.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4D24C84C6300A47970 /* sun.min.pdf */; }; 03AA7A9424C84C6300A47970 /* sun.min.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4D24C84C6300A47970 /* sun.min.pdf */; }; 03AA7A9524C84C6300A47970 /* sun.min.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4D24C84C6300A47970 /* sun.min.pdf */; }; 03AA7A9624C84C6300A47970 /* smoke.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4E24C84C6300A47970 /* smoke.pdf */; }; 03AA7A9724C84C6300A47970 /* smoke.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4E24C84C6300A47970 /* smoke.pdf */; }; 03AA7A9824C84C6300A47970 /* smoke.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4E24C84C6300A47970 /* smoke.pdf */; }; 03AA7A9924C84C6300A47970 /* cloud.snow.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4F24C84C6300A47970 /* cloud.snow.pdf */; }; 03AA7A9A24C84C6300A47970 /* cloud.snow.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4F24C84C6300A47970 /* cloud.snow.pdf */; }; 03AA7A9B24C84C6300A47970 /* cloud.snow.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A4F24C84C6300A47970 /* cloud.snow.pdf */; }; 03AA7A9C24C84C6300A47970 /* cloud.moon.rain.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5024C84C6300A47970 /* cloud.moon.rain.pdf */; }; 03AA7A9D24C84C6300A47970 /* cloud.moon.rain.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5024C84C6300A47970 /* cloud.moon.rain.pdf */; }; 03AA7A9E24C84C6300A47970 /* cloud.moon.rain.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5024C84C6300A47970 /* cloud.moon.rain.pdf */; }; 03AA7A9F24C84C6300A47970 /* cloud.hail.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5124C84C6300A47970 /* cloud.hail.pdf */; }; 03AA7AA024C84C6300A47970 /* cloud.hail.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5124C84C6300A47970 /* cloud.hail.pdf */; }; 03AA7AA124C84C6300A47970 /* cloud.hail.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5124C84C6300A47970 /* cloud.hail.pdf */; }; 03AA7AA224C84C6300A47970 /* cloud.drizzle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5224C84C6300A47970 /* cloud.drizzle.pdf */; }; 03AA7AA324C84C6300A47970 /* cloud.drizzle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5224C84C6300A47970 /* cloud.drizzle.pdf */; }; 03AA7AA424C84C6300A47970 /* cloud.drizzle.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5224C84C6300A47970 /* cloud.drizzle.pdf */; }; 03AA7AA524C84C6300A47970 /* cloud.rain.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5324C84C6300A47970 /* cloud.rain.pdf */; }; 03AA7AA624C84C6300A47970 /* cloud.rain.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5324C84C6300A47970 /* cloud.rain.pdf */; }; 03AA7AA724C84C6300A47970 /* cloud.rain.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5324C84C6300A47970 /* cloud.rain.pdf */; }; 03AA7AA824C84C6300A47970 /* sun.haze.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5424C84C6300A47970 /* sun.haze.pdf */; }; 03AA7AA924C84C6300A47970 /* sun.haze.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5424C84C6300A47970 /* sun.haze.pdf */; }; 03AA7AAA24C84C6300A47970 /* sun.haze.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5424C84C6300A47970 /* sun.haze.pdf */; }; 03AA7AAB24C84C6300A47970 /* tornado.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5524C84C6300A47970 /* tornado.pdf */; }; 03AA7AAC24C84C6300A47970 /* tornado.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5524C84C6300A47970 /* tornado.pdf */; }; 03AA7AAD24C84C6300A47970 /* tornado.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5524C84C6300A47970 /* tornado.pdf */; }; 03AA7AAE24C84C6300A47970 /* cloud.moon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5624C84C6300A47970 /* cloud.moon.pdf */; }; 03AA7AAF24C84C6400A47970 /* cloud.moon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5624C84C6300A47970 /* cloud.moon.pdf */; }; 03AA7AB024C84C6400A47970 /* cloud.moon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5624C84C6300A47970 /* cloud.moon.pdf */; }; 03AA7AB124C84C6400A47970 /* cloud.moon.bolt.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5724C84C6300A47970 /* cloud.moon.bolt.pdf */; }; 03AA7AB224C84C6400A47970 /* cloud.moon.bolt.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5724C84C6300A47970 /* cloud.moon.bolt.pdf */; }; 03AA7AB324C84C6400A47970 /* cloud.moon.bolt.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5724C84C6300A47970 /* cloud.moon.bolt.pdf */; }; 03AA7AB424C84C6400A47970 /* cloud.bolt.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5824C84C6300A47970 /* cloud.bolt.pdf */; }; 03AA7AB524C84C6400A47970 /* cloud.bolt.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5824C84C6300A47970 /* cloud.bolt.pdf */; }; 03AA7AB624C84C6400A47970 /* cloud.bolt.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5824C84C6300A47970 /* cloud.bolt.pdf */; }; 03AA7AB724C84C6400A47970 /* bolt.fill.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5924C84C6300A47970 /* bolt.fill.pdf */; }; 03AA7AB824C84C6400A47970 /* bolt.fill.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5924C84C6300A47970 /* bolt.fill.pdf */; }; 03AA7AB924C84C6400A47970 /* bolt.fill.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5924C84C6300A47970 /* bolt.fill.pdf */; }; 03AA7ABA24C84C6400A47970 /* cloud.sun.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5A24C84C6300A47970 /* cloud.sun.pdf */; }; 03AA7ABB24C84C6400A47970 /* cloud.sun.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5A24C84C6300A47970 /* cloud.sun.pdf */; }; 03AA7ABC24C84C6400A47970 /* cloud.sun.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5A24C84C6300A47970 /* cloud.sun.pdf */; }; 03AA7ABD24C84C6400A47970 /* cloud.sleet.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5B24C84C6300A47970 /* cloud.sleet.pdf */; }; 03AA7ABE24C84C6400A47970 /* cloud.sleet.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5B24C84C6300A47970 /* cloud.sleet.pdf */; }; 03AA7ABF24C84C6400A47970 /* cloud.sleet.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5B24C84C6300A47970 /* cloud.sleet.pdf */; }; 03AA7AC024C84C6400A47970 /* wind.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5C24C84C6300A47970 /* wind.pdf */; }; 03AA7AC124C84C6400A47970 /* wind.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5C24C84C6300A47970 /* wind.pdf */; }; 03AA7AC224C84C6400A47970 /* wind.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03AA7A5C24C84C6300A47970 /* wind.pdf */; }; 03AD45FF22981B0C00261325 /* CustomVideoFolders+helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AD45FE22981B0C00261325 /* CustomVideoFolders+helpers.swift */; }; 03AD460022981B1C00261325 /* CustomVideoFolders+helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AD45FE22981B0C00261325 /* CustomVideoFolders+helpers.swift */; }; 03B8742224E41CF8008E3D1B /* CacheSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B8742024E41CF8008E3D1B /* CacheSetupViewController.swift */; }; 03B8742324E41CF8008E3D1B /* CacheSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B8742024E41CF8008E3D1B /* CacheSetupViewController.swift */; }; 03B8742424E41CF8008E3D1B /* CacheSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B8742024E41CF8008E3D1B /* CacheSetupViewController.swift */; }; 03B8742524E41CF8008E3D1B /* CacheSetupViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03B8742124E41CF8008E3D1B /* CacheSetupViewController.xib */; }; 03B8742624E41CF8008E3D1B /* CacheSetupViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03B8742124E41CF8008E3D1B /* CacheSetupViewController.xib */; }; 03B8742724E41CF8008E3D1B /* CacheSetupViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03B8742124E41CF8008E3D1B /* CacheSetupViewController.xib */; }; 03B8743224E42675008E3D1B /* wand.and.rays.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03B8742F24E42675008E3D1B /* wand.and.rays.pdf */; }; 03B8743324E42675008E3D1B /* wand.and.rays.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03B8742F24E42675008E3D1B /* wand.and.rays.pdf */; }; 03B8743424E42675008E3D1B /* wand.and.rays.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03B8742F24E42675008E3D1B /* wand.and.rays.pdf */; }; 03B8743524E42675008E3D1B /* wand.and.stars.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03B8743024E42675008E3D1B /* wand.and.stars.pdf */; }; 03B8743624E42675008E3D1B /* wand.and.stars.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03B8743024E42675008E3D1B /* wand.and.stars.pdf */; }; 03B8743724E42675008E3D1B /* wand.and.stars.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03B8743024E42675008E3D1B /* wand.and.stars.pdf */; }; 03B8743824E42675008E3D1B /* hand.raised.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03B8743124E42675008E3D1B /* hand.raised.pdf */; }; 03B8743924E42675008E3D1B /* hand.raised.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03B8743124E42675008E3D1B /* hand.raised.pdf */; }; 03B8743A24E42675008E3D1B /* hand.raised.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03B8743124E42675008E3D1B /* hand.raised.pdf */; }; 03BDBEA224C467EC00BBD5E9 /* BrightnessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BDBEA024C467EC00BBD5E9 /* BrightnessViewController.swift */; }; 03BDBEA324C467EC00BBD5E9 /* BrightnessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BDBEA024C467EC00BBD5E9 /* BrightnessViewController.swift */; }; 03BDBEA424C467EC00BBD5E9 /* BrightnessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BDBEA024C467EC00BBD5E9 /* BrightnessViewController.swift */; }; 03BDBEA524C467EC00BBD5E9 /* BrightnessViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03BDBEA124C467EC00BBD5E9 /* BrightnessViewController.xib */; }; 03BDBEA624C467EC00BBD5E9 /* BrightnessViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03BDBEA124C467EC00BBD5E9 /* BrightnessViewController.xib */; }; 03BDBEA724C467EC00BBD5E9 /* BrightnessViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03BDBEA124C467EC00BBD5E9 /* BrightnessViewController.xib */; }; 03BDBEC024C4727C00BBD5E9 /* TimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BDBEBE24C4727C00BBD5E9 /* TimeViewController.swift */; }; 03BDBEC124C4727C00BBD5E9 /* TimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BDBEBE24C4727C00BBD5E9 /* TimeViewController.swift */; }; 03BDBEC224C4727C00BBD5E9 /* TimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BDBEBE24C4727C00BBD5E9 /* TimeViewController.swift */; }; 03BDBEC324C4727C00BBD5E9 /* TimeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03BDBEBF24C4727C00BBD5E9 /* TimeViewController.xib */; }; 03BDBEC424C4727C00BBD5E9 /* TimeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03BDBEBF24C4727C00BBD5E9 /* TimeViewController.xib */; }; 03BDBEC524C4727C00BBD5E9 /* TimeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03BDBEBF24C4727C00BBD5E9 /* TimeViewController.xib */; }; 03BF179F24E2F29C0080EF34 /* VideoFormatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF179D24E2F29C0080EF34 /* VideoFormatViewController.swift */; }; 03BF17A024E2F29C0080EF34 /* VideoFormatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF179D24E2F29C0080EF34 /* VideoFormatViewController.swift */; }; 03BF17A124E2F29C0080EF34 /* VideoFormatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF179D24E2F29C0080EF34 /* VideoFormatViewController.swift */; }; 03BF17A224E2F29C0080EF34 /* VideoFormatViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03BF179E24E2F29C0080EF34 /* VideoFormatViewController.xib */; }; 03BF17A324E2F29C0080EF34 /* VideoFormatViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03BF179E24E2F29C0080EF34 /* VideoFormatViewController.xib */; }; 03BF17A424E2F29C0080EF34 /* VideoFormatViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03BF179E24E2F29C0080EF34 /* VideoFormatViewController.xib */; }; 03BF51BA23A24B40008AD373 /* AnimationTextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0306336D23A15FA900046A59 /* AnimationTextLayer.swift */; }; 03BF51BC23A2643C008AD373 /* LayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF51BB23A2643C008AD373 /* LayerManager.swift */; }; 03BF51BD23A26522008AD373 /* LayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF51BB23A2643C008AD373 /* LayerManager.swift */; }; 03BF51BF23A274CA008AD373 /* MessageLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF51BE23A274CA008AD373 /* MessageLayer.swift */; }; 03BF51C023A275D9008AD373 /* MessageLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF51BE23A274CA008AD373 /* MessageLayer.swift */; }; 03BF51C223A2978B008AD373 /* ClockLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF51C123A2978B008AD373 /* ClockLayer.swift */; }; 03BF51C323A29E1D008AD373 /* ClockLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BF51C123A2978B008AD373 /* ClockLayer.swift */; }; 03C344FC24B778EE00906EA6 /* CheckboxCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C344FB24B778EE00906EA6 /* CheckboxCellView.swift */; }; 03C344FD24B778EE00906EA6 /* CheckboxCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C344FB24B778EE00906EA6 /* CheckboxCellView.swift */; }; 03C344FF24B7A22300906EA6 /* DescriptionCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C344FE24B7A22300906EA6 /* DescriptionCellView.swift */; }; 03C3450024B7A22300906EA6 /* DescriptionCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C344FE24B7A22300906EA6 /* DescriptionCellView.swift */; }; 03C605DC277B45CA005CA51F /* DispatchQueue+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C605DB277B45CA005CA51F /* DispatchQueue+Extension.swift */; }; 03C605DD277B45CA005CA51F /* DispatchQueue+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C605DB277B45CA005CA51F /* DispatchQueue+Extension.swift */; }; 03C605DE277B45CA005CA51F /* DispatchQueue+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C605DB277B45CA005CA51F /* DispatchQueue+Extension.swift */; }; 03C97BA724B5F74900739CED /* VideoList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C97BA624B5F74900739CED /* VideoList.swift */; }; 03C97BA824B5F74900739CED /* VideoList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C97BA624B5F74900739CED /* VideoList.swift */; }; 03C97BC024B60E2F00739CED /* FileHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C97BBF24B60E2F00739CED /* FileHelpers.swift */; }; 03C97BC124B60E2F00739CED /* FileHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C97BBF24B60E2F00739CED /* FileHelpers.swift */; }; 03C97BC324B6210500739CED /* SourceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C97BC224B6210500739CED /* SourceInfo.swift */; }; 03C97BC424B6210500739CED /* SourceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C97BC224B6210500739CED /* SourceInfo.swift */; }; 03D1E78722842FB300D10CF7 /* DisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1E78622842FB300D10CF7 /* DisplayView.swift */; }; 03D1E7882284367200D10CF7 /* DisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1E78622842FB300D10CF7 /* DisplayView.swift */; }; 03D1E78A2284471A00D10CF7 /* DisplayDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1E7892284471A00D10CF7 /* DisplayDetection.swift */; }; 03D1E78B22844AFD00D10CF7 /* DisplayDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D1E7892284471A00D10CF7 /* DisplayDetection.swift */; }; 03D1E79122848F7F00D10CF7 /* screen2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E78E22848F7F00D10CF7 /* screen2.jpg */; }; 03D1E79222848F7F00D10CF7 /* screen2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E78E22848F7F00D10CF7 /* screen2.jpg */; }; 03D1E79322848F7F00D10CF7 /* screen2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E78E22848F7F00D10CF7 /* screen2.jpg */; }; 03D1E79422848F7F00D10CF7 /* screen1.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E78F22848F7F00D10CF7 /* screen1.jpg */; }; 03D1E79522848F7F00D10CF7 /* screen1.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E78F22848F7F00D10CF7 /* screen1.jpg */; }; 03D1E79622848F7F00D10CF7 /* screen1.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E78F22848F7F00D10CF7 /* screen1.jpg */; }; 03D1E79722848F7F00D10CF7 /* screen0.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E79022848F7F00D10CF7 /* screen0.jpg */; }; 03D1E79822848F7F00D10CF7 /* screen0.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E79022848F7F00D10CF7 /* screen0.jpg */; }; 03D1E79922848F7F00D10CF7 /* screen0.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 03D1E79022848F7F00D10CF7 /* screen0.jpg */; }; 03D3A10D24C5D7CC0091FE99 /* Thumbnails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D3A10C24C5D7CC0091FE99 /* Thumbnails.swift */; }; 03D3A10E24C5D7CC0091FE99 /* Thumbnails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D3A10C24C5D7CC0091FE99 /* Thumbnails.swift */; }; 03D3A10F24C5D7CC0091FE99 /* Thumbnails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D3A10C24C5D7CC0091FE99 /* Thumbnails.swift */; }; 03D3A11324C5FC770091FE99 /* AspectFillNSImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D3A11224C5FC770091FE99 /* AspectFillNSImageView.swift */; }; 03D3A11424C5FC770091FE99 /* AspectFillNSImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D3A11224C5FC770091FE99 /* AspectFillNSImageView.swift */; }; 03D3A11524C5FC770091FE99 /* AspectFillNSImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D3A11224C5FC770091FE99 /* AspectFillNSImageView.swift */; }; 03DAD470229EAC66000DA6D1 /* Installation.md in Resources */ = {isa = PBXBuildFile; fileRef = 03DAD46F229EAC66000DA6D1 /* Installation.md */; }; 03DAD473229EC031000DA6D1 /* Readme.md in Resources */ = {isa = PBXBuildFile; fileRef = 03DAD472229EC031000DA6D1 /* Readme.md */; }; 03DB9674256569F800BFCF20 /* icon-512@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 03DB966D256569F800BFCF20 /* icon-512@2x.png */; }; 03DB9675256569F800BFCF20 /* icon-512@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 03DB966D256569F800BFCF20 /* icon-512@2x.png */; }; 03DB9676256569F800BFCF20 /* icon-512@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 03DB966D256569F800BFCF20 /* icon-512@2x.png */; }; 03DB968425657AE600BFCF20 /* LogoIcon-128px@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 03DB968325657AE600BFCF20 /* LogoIcon-128px@2x.png */; }; 03DB968525657AE600BFCF20 /* LogoIcon-128px@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 03DB968325657AE600BFCF20 /* LogoIcon-128px@2x.png */; }; 03DB968625657AE600BFCF20 /* LogoIcon-128px@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 03DB968325657AE600BFCF20 /* LogoIcon-128px@2x.png */; }; 03DC004E248BC5A4005DB0F4 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03DC004D248BC5A4005DB0F4 /* Cache.swift */; }; 03DC0065248BC60D005DB0F4 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03DC004D248BC5A4005DB0F4 /* Cache.swift */; }; 03E168162673A23900D7442D /* InfoMusicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E168152673A23900D7442D /* InfoMusicView.swift */; }; 03E168172673A23900D7442D /* InfoMusicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E168152673A23900D7442D /* InfoMusicView.swift */; }; 03E168182673A23900D7442D /* InfoMusicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E168152673A23900D7442D /* InfoMusicView.swift */; }; 03E1681A2673A63F00D7442D /* MusicLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E168192673A63F00D7442D /* MusicLayer.swift */; }; 03E1681B2673A63F00D7442D /* MusicLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E168192673A63F00D7442D /* MusicLayer.swift */; }; 03E1681C2673A63F00D7442D /* MusicLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E168192673A63F00D7442D /* MusicLayer.swift */; }; 03E2237124BE048900CD8ED4 /* VideoHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E2237024BE048900CD8ED4 /* VideoHeaderView.swift */; }; 03E2237224BE048900CD8ED4 /* VideoHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E2237024BE048900CD8ED4 /* VideoHeaderView.swift */; }; 03E2237324BE048900CD8ED4 /* VideoHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E2237024BE048900CD8ED4 /* VideoHeaderView.swift */; }; 03E3C1DE256AAAF6000A2A5B /* ru.json in Resources */ = {isa = PBXBuildFile; fileRef = 03E3C1D7256AAAF6000A2A5B /* ru.json */; }; 03E3C1DF256AAAF6000A2A5B /* ru.json in Resources */ = {isa = PBXBuildFile; fileRef = 03E3C1D7256AAAF6000A2A5B /* ru.json */; }; 03E3C1E0256AAAF6000A2A5B /* ru.json in Resources */ = {isa = PBXBuildFile; fileRef = 03E3C1D7256AAAF6000A2A5B /* ru.json */; }; 03E8730C2165013C002B469B /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E8730B2165013C002B469B /* DownloadManager.swift */; }; 03E8730F216501ED002B469B /* AsynchronousOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E8730E216501ED002B469B /* AsynchronousOperation.swift */; }; 03E8731021662AEB002B469B /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E8730B2165013C002B469B /* DownloadManager.swift */; }; 03E8731121662AEB002B469B /* AsynchronousOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E8730E216501ED002B469B /* AsynchronousOperation.swift */; }; 03E8731321675FE0002B469B /* TimeManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E8731221675FE0002B469B /* TimeManagement.swift */; }; 03E87314216760B7002B469B /* TimeManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E8731221675FE0002B469B /* TimeManagement.swift */; }; 03EBF0C02746A53B00EC09D1 /* PlayingCollectionViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EBF0BE2746A53B00EC09D1 /* PlayingCollectionViewItem.swift */; }; 03EBF0C12746A53B00EC09D1 /* PlayingCollectionViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EBF0BE2746A53B00EC09D1 /* PlayingCollectionViewItem.swift */; }; 03EBF0C22746A53B00EC09D1 /* PlayingCollectionViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EBF0BE2746A53B00EC09D1 /* PlayingCollectionViewItem.swift */; }; 03EBF0C32746A53B00EC09D1 /* PlayingCollectionViewItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03EBF0BF2746A53B00EC09D1 /* PlayingCollectionViewItem.xib */; }; 03EBF0C42746A53B00EC09D1 /* PlayingCollectionViewItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03EBF0BF2746A53B00EC09D1 /* PlayingCollectionViewItem.xib */; }; 03EBF0C52746A53B00EC09D1 /* PlayingCollectionViewItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03EBF0BF2746A53B00EC09D1 /* PlayingCollectionViewItem.xib */; }; 03EED2F024C44A7900F0C3D4 /* OverlaysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EED2EE24C44A7900F0C3D4 /* OverlaysViewController.swift */; }; 03EED2F124C44A7900F0C3D4 /* OverlaysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EED2EE24C44A7900F0C3D4 /* OverlaysViewController.swift */; }; 03EED2F224C44A7900F0C3D4 /* OverlaysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EED2EE24C44A7900F0C3D4 /* OverlaysViewController.swift */; }; 03EED2F324C44A7900F0C3D4 /* OverlaysViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03EED2EF24C44A7900F0C3D4 /* OverlaysViewController.xib */; }; 03EED2F424C44A7900F0C3D4 /* OverlaysViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03EED2EF24C44A7900F0C3D4 /* OverlaysViewController.xib */; }; 03EED2F524C44A7900F0C3D4 /* OverlaysViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03EED2EF24C44A7900F0C3D4 /* OverlaysViewController.xib */; }; 03F3C1BA24F2A923007733B5 /* kofi1@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F3C1B924F2A923007733B5 /* kofi1@2x.png */; }; 03F3C1BB24F2A923007733B5 /* kofi1@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F3C1B924F2A923007733B5 /* kofi1@2x.png */; }; 03F3C1BC24F2A923007733B5 /* kofi1@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F3C1B924F2A923007733B5 /* kofi1@2x.png */; }; 03F5551B24E9C091003AAD0B /* SourceOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F5551A24E9C091003AAD0B /* SourceOutlineView.swift */; }; 03F5551C24E9C091003AAD0B /* SourceOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F5551A24E9C091003AAD0B /* SourceOutlineView.swift */; }; 03F5551D24E9C091003AAD0B /* SourceOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F5551A24E9C091003AAD0B /* SourceOutlineView.swift */; }; 03F752D324EED19C00945B1D /* video_inspire_alabama_montgomery_countryside_00007.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752CF24EED19B00945B1D /* video_inspire_alabama_montgomery_countryside_00007.png */; }; 03F752D424EED19C00945B1D /* video_inspire_alabama_montgomery_countryside_00007.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752CF24EED19B00945B1D /* video_inspire_alabama_montgomery_countryside_00007.png */; }; 03F752D524EED19C00945B1D /* video_inspire_alabama_montgomery_countryside_00007.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752CF24EED19B00945B1D /* video_inspire_alabama_montgomery_countryside_00007.png */; }; 03F752D624EED19C00945B1D /* video_inspire_alabama_montgomery_countryside_00007.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752CF24EED19B00945B1D /* video_inspire_alabama_montgomery_countryside_00007.png */; }; 03F752D724EED19C00945B1D /* video_inspire_california_big-sur_2020_00001.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752D024EED19C00945B1D /* video_inspire_california_big-sur_2020_00001.png */; }; 03F752D824EED19C00945B1D /* video_inspire_california_big-sur_2020_00001.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752D024EED19C00945B1D /* video_inspire_california_big-sur_2020_00001.png */; }; 03F752D924EED19C00945B1D /* video_inspire_california_big-sur_2020_00001.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752D024EED19C00945B1D /* video_inspire_california_big-sur_2020_00001.png */; }; 03F752DA24EED19C00945B1D /* video_inspire_california_big-sur_2020_00001.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752D024EED19C00945B1D /* video_inspire_california_big-sur_2020_00001.png */; }; 03F752DB24EED19C00945B1D /* video_inspire_italy_como_cerano-dintelvi_006.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752D124EED19C00945B1D /* video_inspire_italy_como_cerano-dintelvi_006.png */; }; 03F752DC24EED19C00945B1D /* video_inspire_italy_como_cerano-dintelvi_006.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752D124EED19C00945B1D /* video_inspire_italy_como_cerano-dintelvi_006.png */; }; 03F752DD24EED19C00945B1D /* video_inspire_italy_como_cerano-dintelvi_006.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752D124EED19C00945B1D /* video_inspire_italy_como_cerano-dintelvi_006.png */; }; 03F752DE24EED19C00945B1D /* video_inspire_italy_como_cerano-dintelvi_006.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752D124EED19C00945B1D /* video_inspire_italy_como_cerano-dintelvi_006.png */; }; 03F752DF24EED19C00945B1D /* video_inspire_california_catalina_00005.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752D224EED19C00945B1D /* video_inspire_california_catalina_00005.png */; }; 03F752E024EED19C00945B1D /* video_inspire_california_catalina_00005.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752D224EED19C00945B1D /* video_inspire_california_catalina_00005.png */; }; 03F752E124EED19C00945B1D /* video_inspire_california_catalina_00005.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752D224EED19C00945B1D /* video_inspire_california_catalina_00005.png */; }; 03F752E224EED19C00945B1D /* video_inspire_california_catalina_00005.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752D224EED19C00945B1D /* video_inspire_california_catalina_00005.png */; }; 03F752E424EED21100945B1D /* video_inspire_california_vineyard_sierra-mar_sunrise_110.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752E324EED21100945B1D /* video_inspire_california_vineyard_sierra-mar_sunrise_110.png */; }; 03F752E524EED21100945B1D /* video_inspire_california_vineyard_sierra-mar_sunrise_110.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752E324EED21100945B1D /* video_inspire_california_vineyard_sierra-mar_sunrise_110.png */; }; 03F752E624EED21100945B1D /* video_inspire_california_vineyard_sierra-mar_sunrise_110.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752E324EED21100945B1D /* video_inspire_california_vineyard_sierra-mar_sunrise_110.png */; }; 03F752E724EED21100945B1D /* video_inspire_california_vineyard_sierra-mar_sunrise_110.png in Resources */ = {isa = PBXBuildFile; fileRef = 03F752E324EED21100945B1D /* video_inspire_california_vineyard_sierra-mar_sunrise_110.png */; }; 03F752E924EEFC3100945B1D /* text.cursor.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03F752E824EEFC3100945B1D /* text.cursor.pdf */; }; 03F752EA24EEFC3100945B1D /* text.cursor.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03F752E824EEFC3100945B1D /* text.cursor.pdf */; }; 03F752EB24EEFC3100945B1D /* text.cursor.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03F752E824EEFC3100945B1D /* text.cursor.pdf */; }; 03F752EC24EEFC3100945B1D /* text.cursor.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03F752E824EEFC3100945B1D /* text.cursor.pdf */; }; 03F752EE24EEFC8500945B1D /* house.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03F752ED24EEFC8500945B1D /* house.pdf */; }; 03F752EF24EEFC8500945B1D /* house.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03F752ED24EEFC8500945B1D /* house.pdf */; }; 03F752F024EEFC8500945B1D /* house.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03F752ED24EEFC8500945B1D /* house.pdf */; }; 03F752F124EEFC8500945B1D /* house.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 03F752ED24EEFC8500945B1D /* house.pdf */; }; 03FA49A12423DA1F00863AF6 /* TimerLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FA49A02423DA1F00863AF6 /* TimerLayer.swift */; }; 03FA49A22423DA1F00863AF6 /* TimerLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FA49A02423DA1F00863AF6 /* TimerLayer.swift */; }; 03FA49B22423DA7D00863AF6 /* InfoTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030DDA8F2423C3BE0072D5C9 /* InfoTimerView.swift */; }; 03FA49B42428EE3300863AF6 /* InfoDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FA49B32428EE3300863AF6 /* InfoDateView.swift */; }; 03FA49B52428EE3300863AF6 /* InfoDateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FA49B32428EE3300863AF6 /* InfoDateView.swift */; }; 03FA49B72428F84500863AF6 /* DateLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FA49B62428F84500863AF6 /* DateLayer.swift */; }; 03FA49B82428F84500863AF6 /* DateLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FA49B62428F84500863AF6 /* DateLayer.swift */; }; 03FF1930269709AB00A0FA7F /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FF192F269709AB00A0FA7F /* PlaybackSpeed.swift */; }; 03FF1931269709AB00A0FA7F /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FF192F269709AB00A0FA7F /* PlaybackSpeed.swift */; }; 03FF1932269709AB00A0FA7F /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FF192F269709AB00A0FA7F /* PlaybackSpeed.swift */; }; AA7E2E5E1FC62E8B00E5F320 /* AerialPlayerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E2E5D1FC62E8B00E5F320 /* AerialPlayerItem.swift */; }; F0058B8228ABF69B0053699B /* SoundOutputManager+Goodies.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0058B7E28ABF69B0053699B /* SoundOutputManager+Goodies.swift */; }; F0058B8328ABF69B0053699B /* SoundOutputManager+Goodies.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0058B7E28ABF69B0053699B /* SoundOutputManager+Goodies.swift */; }; F0058B8428ABF69B0053699B /* SoundOutputManager+Goodies.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0058B7E28ABF69B0053699B /* SoundOutputManager+Goodies.swift */; }; F0058B8528ABF69B0053699B /* SoundOutputManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0058B7F28ABF69B0053699B /* SoundOutputManager.swift */; }; F0058B8628ABF69B0053699B /* SoundOutputManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0058B7F28ABF69B0053699B /* SoundOutputManager.swift */; }; F0058B8728ABF69B0053699B /* SoundOutputManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0058B7F28ABF69B0053699B /* SoundOutputManager.swift */; }; F0058B8828ABF69B0053699B /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0058B8028ABF69B0053699B /* Sound.swift */; }; F0058B8928ABF69B0053699B /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0058B8028ABF69B0053699B /* Sound.swift */; }; F0058B8A28ABF69B0053699B /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0058B8028ABF69B0053699B /* Sound.swift */; }; F0058B8B28ABF69B0053699B /* SoundOutputManager+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0058B8128ABF69B0053699B /* SoundOutputManager+Properties.swift */; }; F0058B8C28ABF69B0053699B /* SoundOutputManager+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0058B8128ABF69B0053699B /* SoundOutputManager+Properties.swift */; }; F0058B8D28ABF69B0053699B /* SoundOutputManager+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0058B8128ABF69B0053699B /* SoundOutputManager+Properties.swift */; }; F00864B423AAE7F0003210EF /* DarkMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00864B323AAE7F0003210EF /* DarkMode.swift */; }; F00864B523AAE7F0003210EF /* DarkMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00864B323AAE7F0003210EF /* DarkMode.swift */; }; F00864B723AAE8E9003210EF /* NightShift.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00864B623AAE8E9003210EF /* NightShift.swift */; }; F00864B823AAE8E9003210EF /* NightShift.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00864B623AAE8E9003210EF /* NightShift.swift */; }; F008DAFD23AADCFB00739DE1 /* Brightness.swift in Sources */ = {isa = PBXBuildFile; fileRef = F008DAFC23AADCFB00739DE1 /* Brightness.swift */; }; F008DAFE23AADCFB00739DE1 /* Brightness.swift in Sources */ = {isa = PBXBuildFile; fileRef = F008DAFC23AADCFB00739DE1 /* Brightness.swift */; }; F05E805428AE8A9C0088B9C5 /* NowPlayingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F05E805328AE8A9C0088B9C5 /* NowPlayingCollectionView.swift */; }; F05E805528AE8A9C0088B9C5 /* NowPlayingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F05E805328AE8A9C0088B9C5 /* NowPlayingCollectionView.swift */; }; F05E805628AE8A9C0088B9C5 /* NowPlayingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F05E805328AE8A9C0088B9C5 /* NowPlayingCollectionView.swift */; }; F06FCF1A28DB8673007558BA /* tl.json in Resources */ = {isa = PBXBuildFile; fileRef = F06FCF1928DB8673007558BA /* tl.json */; }; F06FCF1B28DB8673007558BA /* tl.json in Resources */ = {isa = PBXBuildFile; fileRef = F06FCF1928DB8673007558BA /* tl.json */; }; F06FCF1C28DB8673007558BA /* tl.json in Resources */ = {isa = PBXBuildFile; fileRef = F06FCF1928DB8673007558BA /* tl.json */; }; F07221872AD4354E001F5452 /* CompanionBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07221862AD4354E001F5452 /* CompanionBridge.swift */; }; F07221882AD4354E001F5452 /* CompanionBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07221862AD4354E001F5452 /* CompanionBridge.swift */; }; F07221892AD4354E001F5452 /* CompanionBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07221862AD4354E001F5452 /* CompanionBridge.swift */; }; F0A3E0A32884683D005E8D8D /* CompanionCacheViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A3E0A12884683D005E8D8D /* CompanionCacheViewController.swift */; }; F0A3E0A42884683D005E8D8D /* CompanionCacheViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A3E0A12884683D005E8D8D /* CompanionCacheViewController.swift */; }; F0A3E0A52884683D005E8D8D /* CompanionCacheViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A3E0A12884683D005E8D8D /* CompanionCacheViewController.swift */; }; F0A3E0A62884683D005E8D8D /* CompanionCacheViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = F0A3E0A22884683D005E8D8D /* CompanionCacheViewController.xib */; }; F0A3E0A72884683D005E8D8D /* CompanionCacheViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = F0A3E0A22884683D005E8D8D /* CompanionCacheViewController.xib */; }; F0A3E0A82884683D005E8D8D /* CompanionCacheViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = F0A3E0A22884683D005E8D8D /* CompanionCacheViewController.xib */; }; FA143CE61BDA3EEF0041A82B /* AVKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA143CE51BDA3EEF0041A82B /* AVKit.framework */; }; FA36BD3F1BE57F8E00D5E03B /* VideoDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA36BD3E1BE57F8E00D5E03B /* VideoDownload.swift */; }; FA36BD401BE57F8E00D5E03B /* VideoDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA36BD3E1BE57F8E00D5E03B /* VideoDownload.swift */; }; FA7199711D94EC5A00FBC99B /* PreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7199701D94EC5A00FBC99B /* PreferencesTests.swift */; }; FAB22A7E1BE17D7D0065C0F5 /* AssetLoaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB22A7D1BE17D7D0065C0F5 /* AssetLoaderDelegate.swift */; }; FAB22A7F1BE17D7D0065C0F5 /* AssetLoaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB22A7D1BE17D7D0065C0F5 /* AssetLoaderDelegate.swift */; }; FAC36F481BE1756D007F2A20 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FAC36F321BE1756D007F2A20 /* Assets.xcassets */; }; FAC36F4A1BE1756D007F2A20 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = FAC36F331BE1756D007F2A20 /* MainMenu.xib */; }; FAC36F591BE1756D007F2A20 /* AerialVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F401BE1756D007F2A20 /* AerialVideo.swift */; }; FAC36F5A1BE1756D007F2A20 /* AerialVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F401BE1756D007F2A20 /* AerialVideo.swift */; }; FAC36F5B1BE1756D007F2A20 /* AerialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F431BE1756D007F2A20 /* AerialView.swift */; }; FAC36F5C1BE1756D007F2A20 /* AerialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F431BE1756D007F2A20 /* AerialView.swift */; }; FAC36F5D1BE1756D007F2A20 /* CheckCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F441BE1756D007F2A20 /* CheckCellView.swift */; }; FAC36F5E1BE1756D007F2A20 /* CheckCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F441BE1756D007F2A20 /* CheckCellView.swift */; }; FAC36F601BE175CF007F2A20 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F301BE1756D007F2A20 /* AppDelegate.swift */; }; FAC36F671BE1778C007F2A20 /* ManifestLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F661BE1778C007F2A20 /* ManifestLoader.swift */; }; FAC36F681BE1778C007F2A20 /* ManifestLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC36F661BE1778C007F2A20 /* ManifestLoader.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 */; }; FAF450251BE2D2FD00C1F98A /* VideoCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAF450231BE2D2FD00C1F98A /* VideoCache.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ FA7199731D94EC5A00FBC99B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = FACAF19C1BD9FC6000E539DC /* Project object */; proxyType = 1; remoteGlobalIDString = FA143CD51BDA3E880041A82B; remoteInfo = "Aerial App"; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 034116CE23F9AD0100CD7674 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; 034116D123F9AD0600CD7674 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; 0396D5AE24B8B7ED00CC021B /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 0300109624D6EF4C0092AE68 /* AVPlayerItem+vibrance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVPlayerItem+vibrance.swift"; sourceTree = ""; }; 030010A124D706DB0092AE68 /* SidebarOutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarOutlineView.swift; sourceTree = ""; }; 030010A524D718D30092AE68 /* slider.horizontal.3.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = slider.horizontal.3.pdf; sourceTree = ""; }; 030010A924D71EB20092AE68 /* FiltersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersViewController.swift; sourceTree = ""; }; 030010AA24D71EB20092AE68 /* FiltersViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FiltersViewController.xib; sourceTree = ""; }; 0300B7CA24D1B531006132E5 /* person.3.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = person.3.pdf; sourceTree = ""; }; 0300B7CB24D1B531006132E5 /* bubble.left.and.bubble.right.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = bubble.left.and.bubble.right.pdf; sourceTree = ""; }; 0300B7CC24D1B531006132E5 /* helm.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = helm.pdf; sourceTree = ""; }; 0300B7CD24D1B532006132E5 /* gear.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = gear.pdf; sourceTree = ""; }; 0300B7CE24D1B532006132E5 /* eye.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = eye.pdf; sourceTree = ""; }; 0300B7CF24D1B532006132E5 /* text.bubble.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = text.bubble.pdf; sourceTree = ""; }; 0300B7D024D1B532006132E5 /* circle.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = circle.pdf; sourceTree = ""; }; 0300B7D124D1B532006132E5 /* star.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = star.pdf; sourceTree = ""; }; 0300B7D224D1B532006132E5 /* clock.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = clock.pdf; sourceTree = ""; }; 0300B7D324D1B532006132E5 /* sparkles.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = sparkles.pdf; sourceTree = ""; }; 0300B7D424D1B533006132E5 /* film.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = film.pdf; sourceTree = ""; }; 0300B7D524D1B533006132E5 /* eye.slash.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = eye.slash.pdf; sourceTree = ""; }; 0300B7D624D1B533006132E5 /* info.circle.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = info.circle.pdf; sourceTree = ""; }; 0300B7D724D1B533006132E5 /* regular.moon.stars.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = regular.moon.stars.pdf; sourceTree = ""; }; 0300B7D824D1B533006132E5 /* antenna.radiowaves.left.and.right.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = antenna.radiowaves.left.and.right.pdf; sourceTree = ""; }; 0300B7D924D1B533006132E5 /* sunset.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = sunset.pdf; sourceTree = ""; }; 0300B7DA24D1B534006132E5 /* tram.fill.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = tram.fill.pdf; sourceTree = ""; }; 0300B7DB24D1B534006132E5 /* arrow.down.circle.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = arrow.down.circle.pdf; sourceTree = ""; }; 0300B7DC24D1B534006132E5 /* flame.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = flame.pdf; sourceTree = ""; }; 0300B7DD24D1B534006132E5 /* regular.sun.min.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = regular.sun.min.pdf; sourceTree = ""; }; 0300B7DE24D1B534006132E5 /* mappin.and.ellipse.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = mappin.and.ellipse.pdf; sourceTree = ""; }; 0300B7DF24D1B534006132E5 /* arrow.down.circle.fill.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = arrow.down.circle.fill.pdf; sourceTree = ""; }; 0300B7E024D1B535006132E5 /* location.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = location.pdf; sourceTree = ""; }; 0300B7E124D1B535006132E5 /* tv.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = tv.pdf; sourceTree = ""; }; 0300B7E224D1B535006132E5 /* sunrise.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = sunrise.pdf; sourceTree = ""; }; 0300B7E324D1B535006132E5 /* star.fill.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = star.fill.pdf; sourceTree = ""; }; 0300B7E424D1B535006132E5 /* regular.cloud.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = regular.cloud.pdf; sourceTree = ""; }; 0300B7E524D1B535006132E5 /* regular.sun.max.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = regular.sun.max.pdf; sourceTree = ""; }; 0300B7E624D1B536006132E5 /* dial.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = dial.pdf; sourceTree = ""; }; 0300B7E724D1B536006132E5 /* checkmark.circle.fill.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = checkmark.circle.fill.pdf; sourceTree = ""; }; 0300B7E824D1B536006132E5 /* square.and.arrow.down.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = square.and.arrow.down.pdf; sourceTree = ""; }; 0300B84724D1FD24006132E5 /* FirstSetupWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstSetupWindowController.swift; sourceTree = ""; }; 0300B84824D1FD24006132E5 /* FirstSetupWindowController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FirstSetupWindowController.xib; sourceTree = ""; }; 0300B85F24D2052B006132E5 /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; }; 0300B86024D2052B006132E5 /* WelcomeViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WelcomeViewController.xib; sourceTree = ""; }; 0300B86724D20B12006132E5 /* NextViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextViewController.swift; sourceTree = ""; }; 0300B86824D20B12006132E5 /* NextViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NextViewController.xib; sourceTree = ""; }; 030473C924BCA9A40094A1A6 /* VideoViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoViewItem.swift; sourceTree = ""; }; 03047A5324D2DD8E000EFE62 /* NSMenuItem+icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+icons.swift"; sourceTree = ""; }; 0306336723A1012200046A59 /* LocationLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationLayer.swift; sourceTree = ""; }; 0306336A23A142FA00046A59 /* LayerOffsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayerOffsets.swift; sourceTree = ""; }; 0306336D23A15FA900046A59 /* AnimationTextLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationTextLayer.swift; sourceTree = ""; }; 03075EA824ED794F00FDBE48 /* trash.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = trash.pdf; sourceTree = ""; }; 03075EB024ED794F00FDBE48 /* textformat.alt.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = textformat.alt.pdf; sourceTree = ""; }; 030A0F28245C7C7D009E1D97 /* BatteryIconLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryIconLayer.swift; sourceTree = ""; }; 030B5E32229FDD26008F2910 /* AutoUpdates.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = AutoUpdates.md; sourceTree = ""; }; 030DDA8F2423C3BE0072D5C9 /* InfoTimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoTimerView.swift; sourceTree = ""; }; 0313329A24BF3FA700C84A05 /* SidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewController.swift; sourceTree = ""; }; 0313329B24BF3FA700C84A05 /* SidebarViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SidebarViewController.xib; sourceTree = ""; }; 0313F9E522942AA500B074BB /* CustomVideos.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CustomVideos.xib; sourceTree = ""; }; 0313F9E722942B4500B074BB /* CustomVideoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomVideoController.swift; sourceTree = ""; }; 0313F9EB2294468600B074BB /* SeededGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeededGenerator.swift; sourceTree = ""; }; 0313F9EE22955F3B00B074BB /* CustomVideoFolders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomVideoFolders.swift; sourceTree = ""; }; 0317C19B268B65D10082A40C /* Music.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Music.swift; sourceTree = ""; }; 031945F124CCC48C00F37B35 /* CreditsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditsViewController.swift; sourceTree = ""; }; 031945F224CCC48C00F37B35 /* CreditsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CreditsViewController.xib; sourceTree = ""; }; 031945F924CCC52600F37B35 /* HelpViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpViewController.swift; sourceTree = ""; }; 031945FA24CCC52600F37B35 /* HelpViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HelpViewController.xib; sourceTree = ""; }; 0319460824D1A09F00F37B35 /* icon-320@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon-320@2x.png"; sourceTree = ""; }; 031C5CB7268CA4E700CE35B4 /* ArtworkLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtworkLayer.swift; sourceTree = ""; }; 031FB78A248A87330054BAFD /* PrefsCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsCache.swift; sourceTree = ""; }; 0321A53224D44E80004F1975 /* ActionCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionCellView.swift; sourceTree = ""; }; 0321A53D24D4515B004F1975 /* folder.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = folder.pdf; sourceTree = ""; }; 0321A54324D5C863004F1975 /* NSButton+icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSButton+icons.swift"; sourceTree = ""; }; 03233B67217272640077D3F9 /* PoiStringProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoiStringProvider.swift; sourceTree = ""; }; 032851F6260A4C2B00684A81 /* OneCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneCall.swift; sourceTree = ""; }; 032851FA260A625100684A81 /* ForecastLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastLayer.swift; sourceTree = ""; }; 03298773274687340036D898 /* NowPlayingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingViewController.swift; sourceTree = ""; }; 03298774274687340036D898 /* NowPlayingViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NowPlayingViewController.xib; sourceTree = ""; }; 032D1160239A7D82007E7756 /* Battery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Battery.swift; sourceTree = ""; }; 032D1163239A7F0C007E7756 /* AerialView+Brightness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AerialView+Brightness.swift"; sourceTree = ""; }; 032E099C24C3897E00387230 /* AdvancedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedViewController.swift; sourceTree = ""; }; 032E099D24C3897E00387230 /* AdvancedViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AdvancedViewController.xib; sourceTree = ""; }; 0332076C26D7C355001F9837 /* AVAsset+VideoOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAsset+VideoOrientation.swift"; sourceTree = ""; }; 0338119424C1D15B002E23E0 /* Aerial.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Aerial.swift; sourceTree = ""; }; 033811AE24C1E243002E23E0 /* InfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoViewController.swift; sourceTree = ""; }; 033811AF24C1E243002E23E0 /* InfoViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InfoViewController.xib; sourceTree = ""; }; 033842E024489D7300A2C523 /* WeatherLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherLayer.swift; sourceTree = ""; }; 033D68802453080C0016F837 /* ConditionSymbolLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionSymbolLayer.swift; sourceTree = ""; }; 034116D223F9BD3100CD7674 /* PrefsUpdates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsUpdates.swift; sourceTree = ""; }; 0343E9F82630590A00AC702F /* openweather_logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = openweather_logo.png; sourceTree = ""; }; 0345872C2449C52F00C97D1B /* AnimatableLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatableLayer.swift; sourceTree = ""; }; 034587312449D8EB00C97D1B /* AnimationLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationLayer.swift; sourceTree = ""; }; 0345CFEB24BF43280001045C /* VideosViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosViewController.swift; sourceTree = ""; }; 0345CFEC24BF43280001045C /* VideosViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = VideosViewController.xib; sourceTree = ""; }; 0345D00924BF4E8C0001045C /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = ""; }; 0345D00D24C07CC70001045C /* VideoCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCellView.swift; sourceTree = ""; }; 0345EDE624C3239400C73038 /* DisplaysViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplaysViewController.swift; sourceTree = ""; }; 0345EDE724C3239400C73038 /* DisplaysViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DisplaysViewController.xib; sourceTree = ""; }; 034A6DC124ACC7C800D62129 /* SourceList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceList.swift; sourceTree = ""; }; 034A6DC324ACC80200D62129 /* Source.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source.swift; sourceTree = ""; }; 034DEE2C24BF1BC700A2D3CD /* PanelWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanelWindowController.swift; sourceTree = ""; }; 034DEE2D24BF1BC700A2D3CD /* PanelWindowController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PanelWindowController.xib; sourceTree = ""; }; 034F29B623A7A93D004B34D5 /* InfoTableSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoTableSource.swift; sourceTree = ""; }; 034F29BE23A7E28E004B34D5 /* PrefsInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsInfo.swift; sourceTree = ""; }; 0350CB78279481F2005F8625 /* hu.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = hu.json; sourceTree = ""; }; 03510C6A21834EB2008F74F2 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; 03510C6D21834F38008F74F2 /* Aerial-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Aerial-Bridging-Header.h"; sourceTree = ""; }; 03510C6E21834F38008F74F2 /* IOBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IOBridge.m; sourceTree = ""; }; 03510C722185EF76008F74F2 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; 0354D0E823F6C31800D86F9E /* InfoSettingsTableSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoSettingsTableSource.swift; sourceTree = ""; }; 0354D0EB23F6CB7B00D86F9E /* InfoSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoSettingsView.swift; sourceTree = ""; }; 035D524E239AA31A00DC29DC /* AerialView+Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AerialView+Player.swift"; sourceTree = ""; }; 03608A2B22A56465008F08A2 /* HardwareDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardwareDetection.swift; sourceTree = ""; }; 0361B9A723D732A300B6252D /* PrefsDisplays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsDisplays.swift; sourceTree = ""; }; 0361B9AA23D73D4500B6252D /* PrefsTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsTime.swift; sourceTree = ""; }; 0369985C2196103300E359D3 /* missingvideos.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = missingvideos.json; sourceTree = ""; }; 036A57D423F30DD00009DC02 /* DownloadIndicatorLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadIndicatorLayer.swift; sourceTree = ""; }; 036A57D723F470940009DC02 /* InfoCountdownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoCountdownView.swift; sourceTree = ""; }; 036A57DA23F5820A0009DC02 /* CountdownLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownLayer.swift; sourceTree = ""; }; 036A7E9A26370C260019186B /* Forecast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Forecast.swift; sourceTree = ""; }; 0374C9FD247AC5BC002F29D3 /* Locations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locations.swift; sourceTree = ""; }; 037772DF24E43AF300D81EEA /* TimeSetupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeSetupViewController.swift; sourceTree = ""; }; 037772E024E43AF300D81EEA /* TimeSetupViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TimeSetupViewController.xib; sourceTree = ""; }; 037772E724E44BB100D81EEA /* xmark.circle.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = xmark.circle.pdf; sourceTree = ""; }; 037772EB24E44CE100D81EEA /* RecapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecapViewController.swift; sourceTree = ""; }; 037772EC24E44CE100D81EEA /* RecapViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RecapViewController.xib; sourceTree = ""; }; 037772F424E4668600D81EEA /* aspectratio.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = aspectratio.pdf; sourceTree = ""; }; 0377BF6F24BA15E600C33F9F /* btn_donate.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = btn_donate.png; sourceTree = ""; }; 0378985C24C35F8A009B9418 /* CacheViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheViewController.swift; sourceTree = ""; }; 0378985D24C35F8A009B9418 /* CacheViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CacheViewController.xib; sourceTree = ""; }; 0385FC58242B9AE1007E6513 /* APISecrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APISecrets.swift; sourceTree = ""; }; 0385FC5C242B9F6E007E6513 /* InfoWeatherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoWeatherView.swift; sourceTree = ""; }; 03893CB2217749F0008E7125 /* ErrorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLog.swift; sourceTree = ""; }; 038C584623A9304800224630 /* InfoContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoContainerView.swift; sourceTree = ""; }; 038C584923A9394000224630 /* InfoCommonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoCommonView.swift; sourceTree = ""; }; 038D2EDD23B0FB0D00CD91F7 /* PrefsVideos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsVideos.swift; sourceTree = ""; }; 038D2EE323B6565900CD91F7 /* InfoBatteryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoBatteryView.swift; sourceTree = ""; }; 03933B8924C3986800A98D94 /* SourcesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesViewController.swift; sourceTree = ""; }; 03933B8A24C3986800A98D94 /* SourcesViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SourcesViewController.xib; sourceTree = ""; }; 039385792175D4B80040B850 /* AVPlayerViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerViewExtension.swift; sourceTree = ""; }; 03958348217F4416008E8F9C /* Solar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Solar.swift; sourceTree = ""; }; 0395835121807D1F008E8F9C /* thumbnail@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "thumbnail@2x.png"; sourceTree = ""; }; 0395835221807D1F008E8F9C /* thumbnail.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = thumbnail.png; sourceTree = ""; }; 0396D50624B8B7D700CC021B /* AerialApp copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "AerialApp copy-Info.plist"; sourceTree = ""; }; 0396D5B524B8B7ED00CC021B /* Aerial (low dependencies).saver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Aerial (low dependencies).saver"; sourceTree = BUILT_PRODUCTS_DIR; }; 0396D5B624B8B7EE00CC021B /* Aerial copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Aerial copy-Info.plist"; sourceTree = ""; }; 03977EE2250E6917008FBAFD /* sv.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = sv.json; sourceTree = ""; }; 03977EE3250E6917008FBAFD /* ja.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ja.json; sourceTree = ""; }; 03977EE4250E6917008FBAFD /* pt.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = pt.json; sourceTree = ""; }; 03977EE5250E6917008FBAFD /* zh_TW.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = zh_TW.json; sourceTree = ""; }; 03977EE6250E6917008FBAFD /* pl.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = pl.json; sourceTree = ""; }; 03977EE7250E6918008FBAFD /* de.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = de.json; sourceTree = ""; }; 03977EE8250E6918008FBAFD /* fr.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = fr.json; sourceTree = ""; }; 03977EE9250E6918008FBAFD /* es.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = es.json; sourceTree = ""; }; 03977EEA250E6918008FBAFD /* pt_BR.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = pt_BR.json; sourceTree = ""; }; 03977EEB250E6918008FBAFD /* it.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = it.json; sourceTree = ""; }; 03977EEC250E6918008FBAFD /* he.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = he.json; sourceTree = ""; }; 03977EED250E6918008FBAFD /* en.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = en.json; sourceTree = ""; }; 03977EEE250E6918008FBAFD /* zh_CN.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = zh_CN.json; sourceTree = ""; }; 03977EEF250E6918008FBAFD /* ar.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ar.json; sourceTree = ""; }; 03977EF0250E6918008FBAFD /* nl.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = nl.json; sourceTree = ""; }; 03977F1F250E7165008FBAFD /* TimeMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeMachine.swift; sourceTree = ""; }; 03A2CB9B216BA9AF0061E8E8 /* VideoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoManager.swift; sourceTree = ""; }; 03A42A822449F5EA003B3012 /* white_retina.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = white_retina.png; sourceTree = ""; }; 03A42A9B2449F601003B3012 /* purple_retina.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = purple_retina.png; sourceTree = ""; }; 03A42A9F2449F959003B3012 /* YahooLogoLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YahooLogoLayer.swift; sourceTree = ""; }; 03A42AA2244A0E5F003B3012 /* ConditionLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionLayer.swift; sourceTree = ""; }; 03A4A80A2451CE2C00A1F7A3 /* NSImage+trim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+trim.swift"; sourceTree = ""; }; 03A4A80D2451D04C00A1F7A3 /* PrefsAdvanced.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsAdvanced.swift; sourceTree = ""; }; 03A596D223AA750F0097EA66 /* InfoMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoMessageView.swift; sourceTree = ""; }; 03A596D423AA752F0097EA66 /* InfoClockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoClockView.swift; sourceTree = ""; }; 03A596D823AB8F000097EA66 /* InfoLocationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoLocationView.swift; sourceTree = ""; }; 03A6D14525F109B900960135 /* OpenWeather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenWeather.swift; sourceTree = ""; }; 03A6D14E25F294C900960135 /* location.north.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = location.north.pdf; sourceTree = ""; }; 03A6D15225F297CE00960135 /* WindDirectionLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindDirectionLayer.swift; sourceTree = ""; }; 03AA27BC2614A6F800A4D2CF /* ko.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ko.json; sourceTree = ""; }; 03AA32612631A8F2002198C3 /* GeoCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoCoding.swift; sourceTree = ""; }; 03AA7A2524C84C6200A47970 /* cloud.bolt.rain.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.bolt.rain.pdf; sourceTree = ""; }; 03AA7A3C24C84C6200A47970 /* wind.snow.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = wind.snow.pdf; sourceTree = ""; }; 03AA7A3D24C84C6200A47970 /* snow.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = snow.pdf; sourceTree = ""; }; 03AA7A3E24C84C6200A47970 /* moon.stars.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = moon.stars.pdf; sourceTree = ""; }; 03AA7A3F24C84C6200A47970 /* cloud.heavyrain.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.heavyrain.pdf; sourceTree = ""; }; 03AA7A4024C84C6200A47970 /* battery.0.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = battery.0.pdf; sourceTree = ""; }; 03AA7A4124C84C6200A47970 /* wrench.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = wrench.pdf; sourceTree = ""; }; 03AA7A4224C84C6200A47970 /* cloud.sun.rain.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.sun.rain.pdf; sourceTree = ""; }; 03AA7A4324C84C6200A47970 /* sun.dust.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = sun.dust.pdf; sourceTree = ""; }; 03AA7A4424C84C6200A47970 /* sun.max.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = sun.max.pdf; sourceTree = ""; }; 03AA7A4524C84C6200A47970 /* cloud.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.pdf; sourceTree = ""; }; 03AA7A4624C84C6200A47970 /* cloud.sun.bolt.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.sun.bolt.pdf; sourceTree = ""; }; 03AA7A4724C84C6200A47970 /* thermometer.snowflake.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = thermometer.snowflake.pdf; sourceTree = ""; }; 03AA7A4824C84C6200A47970 /* hurricane.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = hurricane.pdf; sourceTree = ""; }; 03AA7A4924C84C6200A47970 /* cloud.fog.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.fog.pdf; sourceTree = ""; }; 03AA7A4A24C84C6200A47970 /* thermometer.sun.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = thermometer.sun.pdf; sourceTree = ""; }; 03AA7A4B24C84C6200A47970 /* tropicalstorm.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = tropicalstorm.pdf; sourceTree = ""; }; 03AA7A4C24C84C6300A47970 /* moon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = moon.pdf; sourceTree = ""; }; 03AA7A4D24C84C6300A47970 /* sun.min.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = sun.min.pdf; sourceTree = ""; }; 03AA7A4E24C84C6300A47970 /* smoke.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = smoke.pdf; sourceTree = ""; }; 03AA7A4F24C84C6300A47970 /* cloud.snow.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.snow.pdf; sourceTree = ""; }; 03AA7A5024C84C6300A47970 /* cloud.moon.rain.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.moon.rain.pdf; sourceTree = ""; }; 03AA7A5124C84C6300A47970 /* cloud.hail.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.hail.pdf; sourceTree = ""; }; 03AA7A5224C84C6300A47970 /* cloud.drizzle.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.drizzle.pdf; sourceTree = ""; }; 03AA7A5324C84C6300A47970 /* cloud.rain.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.rain.pdf; sourceTree = ""; }; 03AA7A5424C84C6300A47970 /* sun.haze.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = sun.haze.pdf; sourceTree = ""; }; 03AA7A5524C84C6300A47970 /* tornado.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = tornado.pdf; sourceTree = ""; }; 03AA7A5624C84C6300A47970 /* cloud.moon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.moon.pdf; sourceTree = ""; }; 03AA7A5724C84C6300A47970 /* cloud.moon.bolt.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.moon.bolt.pdf; sourceTree = ""; }; 03AA7A5824C84C6300A47970 /* cloud.bolt.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.bolt.pdf; sourceTree = ""; }; 03AA7A5924C84C6300A47970 /* bolt.fill.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = bolt.fill.pdf; sourceTree = ""; }; 03AA7A5A24C84C6300A47970 /* cloud.sun.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.sun.pdf; sourceTree = ""; }; 03AA7A5B24C84C6300A47970 /* cloud.sleet.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = cloud.sleet.pdf; sourceTree = ""; }; 03AA7A5C24C84C6300A47970 /* wind.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = wind.pdf; sourceTree = ""; }; 03AD45FE22981B0C00261325 /* CustomVideoFolders+helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomVideoFolders+helpers.swift"; sourceTree = ""; }; 03B8742024E41CF8008E3D1B /* CacheSetupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheSetupViewController.swift; sourceTree = ""; }; 03B8742124E41CF8008E3D1B /* CacheSetupViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CacheSetupViewController.xib; sourceTree = ""; }; 03B8742F24E42675008E3D1B /* wand.and.rays.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = wand.and.rays.pdf; sourceTree = ""; }; 03B8743024E42675008E3D1B /* wand.and.stars.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = wand.and.stars.pdf; sourceTree = ""; }; 03B8743124E42675008E3D1B /* hand.raised.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = hand.raised.pdf; sourceTree = ""; }; 03BDBEA024C467EC00BBD5E9 /* BrightnessViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrightnessViewController.swift; sourceTree = ""; }; 03BDBEA124C467EC00BBD5E9 /* BrightnessViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BrightnessViewController.xib; sourceTree = ""; }; 03BDBEBE24C4727C00BBD5E9 /* TimeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeViewController.swift; sourceTree = ""; }; 03BDBEBF24C4727C00BBD5E9 /* TimeViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TimeViewController.xib; sourceTree = ""; }; 03BF179D24E2F29C0080EF34 /* VideoFormatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoFormatViewController.swift; sourceTree = ""; }; 03BF179E24E2F29C0080EF34 /* VideoFormatViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = VideoFormatViewController.xib; sourceTree = ""; }; 03BF51BB23A2643C008AD373 /* LayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayerManager.swift; sourceTree = ""; }; 03BF51BE23A274CA008AD373 /* MessageLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLayer.swift; sourceTree = ""; }; 03BF51C123A2978B008AD373 /* ClockLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClockLayer.swift; sourceTree = ""; }; 03C344FB24B778EE00906EA6 /* CheckboxCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxCellView.swift; sourceTree = ""; }; 03C344FE24B7A22300906EA6 /* DescriptionCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionCellView.swift; sourceTree = ""; }; 03C605DB277B45CA005CA51F /* DispatchQueue+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Extension.swift"; sourceTree = ""; }; 03C97BA624B5F74900739CED /* VideoList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoList.swift; sourceTree = ""; }; 03C97BBF24B60E2F00739CED /* FileHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FileHelpers.swift; path = Downloads/FileHelpers.swift; sourceTree = ""; }; 03C97BC224B6210500739CED /* SourceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceInfo.swift; sourceTree = ""; }; 03D1E78622842FB300D10CF7 /* DisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DisplayView.swift; path = Aerial/Source/Views/PrefPanel/DisplayView.swift; sourceTree = SOURCE_ROOT; }; 03D1E7892284471A00D10CF7 /* DisplayDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayDetection.swift; sourceTree = ""; }; 03D1E78E22848F7F00D10CF7 /* screen2.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = screen2.jpg; sourceTree = ""; }; 03D1E78F22848F7F00D10CF7 /* screen1.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = screen1.jpg; sourceTree = ""; }; 03D1E79022848F7F00D10CF7 /* screen0.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = screen0.jpg; sourceTree = ""; }; 03D3A10C24C5D7CC0091FE99 /* Thumbnails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnails.swift; sourceTree = ""; }; 03D3A11224C5FC770091FE99 /* AspectFillNSImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AspectFillNSImageView.swift; sourceTree = ""; }; 03D6C78922A0384F00DE830B /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 03DAD46F229EAC66000DA6D1 /* Installation.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Installation.md; sourceTree = ""; }; 03DAD471229EB1E9000DA6D1 /* ChangeLog.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ChangeLog.md; sourceTree = ""; }; 03DAD472229EC031000DA6D1 /* Readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Readme.md; sourceTree = ""; }; 03DAD474229EC3CD000DA6D1 /* OfflineMode.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = OfflineMode.md; sourceTree = ""; }; 03DAD475229EC544000DA6D1 /* HardwareDecoding.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = HardwareDecoding.md; sourceTree = ""; }; 03DAD476229EC64D000DA6D1 /* Troubleshooting.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Troubleshooting.md; sourceTree = ""; }; 03DAD477229ECAAA000DA6D1 /* CustomVideos.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CustomVideos.md; sourceTree = ""; }; 03DB966D256569F800BFCF20 /* icon-512@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon-512@2x.png"; sourceTree = ""; }; 03DB968325657AE600BFCF20 /* LogoIcon-128px@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "LogoIcon-128px@2x.png"; sourceTree = ""; }; 03DC004D248BC5A4005DB0F4 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; 03E168152673A23900D7442D /* InfoMusicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoMusicView.swift; sourceTree = ""; }; 03E168192673A63F00D7442D /* MusicLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicLayer.swift; sourceTree = ""; }; 03E2237024BE048900CD8ED4 /* VideoHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoHeaderView.swift; sourceTree = ""; }; 03E3C1D7256AAAF6000A2A5B /* ru.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ru.json; sourceTree = ""; }; 03E8730B2165013C002B469B /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = ""; }; 03E8730E216501ED002B469B /* AsynchronousOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsynchronousOperation.swift; sourceTree = ""; }; 03E8731221675FE0002B469B /* TimeManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeManagement.swift; sourceTree = ""; }; 03EBF0BE2746A53B00EC09D1 /* PlayingCollectionViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayingCollectionViewItem.swift; sourceTree = ""; }; 03EBF0BF2746A53B00EC09D1 /* PlayingCollectionViewItem.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PlayingCollectionViewItem.xib; sourceTree = ""; }; 03EED2EE24C44A7900F0C3D4 /* OverlaysViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlaysViewController.swift; sourceTree = ""; }; 03EED2EF24C44A7900F0C3D4 /* OverlaysViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OverlaysViewController.xib; sourceTree = ""; }; 03F3C1B924F2A923007733B5 /* kofi1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "kofi1@2x.png"; sourceTree = ""; }; 03F5551A24E9C091003AAD0B /* SourceOutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceOutlineView.swift; sourceTree = ""; }; 03F752CF24EED19B00945B1D /* video_inspire_alabama_montgomery_countryside_00007.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = video_inspire_alabama_montgomery_countryside_00007.png; sourceTree = ""; }; 03F752D024EED19C00945B1D /* video_inspire_california_big-sur_2020_00001.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "video_inspire_california_big-sur_2020_00001.png"; sourceTree = ""; }; 03F752D124EED19C00945B1D /* video_inspire_italy_como_cerano-dintelvi_006.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "video_inspire_italy_como_cerano-dintelvi_006.png"; sourceTree = ""; }; 03F752D224EED19C00945B1D /* video_inspire_california_catalina_00005.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = video_inspire_california_catalina_00005.png; sourceTree = ""; }; 03F752E324EED21100945B1D /* video_inspire_california_vineyard_sierra-mar_sunrise_110.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "video_inspire_california_vineyard_sierra-mar_sunrise_110.png"; sourceTree = ""; }; 03F752E824EEFC3100945B1D /* text.cursor.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = text.cursor.pdf; sourceTree = ""; }; 03F752ED24EEFC8500945B1D /* house.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = house.pdf; sourceTree = ""; }; 03FA49A02423DA1F00863AF6 /* TimerLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerLayer.swift; sourceTree = ""; }; 03FA49B32428EE3300863AF6 /* InfoDateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoDateView.swift; sourceTree = ""; }; 03FA49B62428F84500863AF6 /* DateLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateLayer.swift; sourceTree = ""; }; 03FF192F269709AB00A0FA7F /* PlaybackSpeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSpeed.swift; sourceTree = ""; }; AA7E2E5D1FC62E8B00E5F320 /* AerialPlayerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AerialPlayerItem.swift; sourceTree = ""; }; F0058B7E28ABF69B0053699B /* SoundOutputManager+Goodies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SoundOutputManager+Goodies.swift"; sourceTree = ""; }; F0058B7F28ABF69B0053699B /* SoundOutputManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SoundOutputManager.swift; sourceTree = ""; }; F0058B8028ABF69B0053699B /* Sound.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sound.swift; sourceTree = ""; }; F0058B8128ABF69B0053699B /* SoundOutputManager+Properties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SoundOutputManager+Properties.swift"; sourceTree = ""; }; F00864B323AAE7F0003210EF /* DarkMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarkMode.swift; sourceTree = ""; }; F00864B623AAE8E9003210EF /* NightShift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightShift.swift; sourceTree = ""; }; F008DAFC23AADCFB00739DE1 /* Brightness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Brightness.swift; sourceTree = ""; }; F05E805328AE8A9C0088B9C5 /* NowPlayingCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingCollectionView.swift; sourceTree = ""; }; F06FCF1928DB8673007558BA /* tl.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tl.json; sourceTree = ""; }; F07221862AD4354E001F5452 /* CompanionBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompanionBridge.swift; sourceTree = ""; }; F0A3E0A12884683D005E8D8D /* CompanionCacheViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompanionCacheViewController.swift; sourceTree = ""; }; F0A3E0A22884683D005E8D8D /* CompanionCacheViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CompanionCacheViewController.xib; sourceTree = ""; }; FA143CD61BDA3E880041A82B /* AerialApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AerialApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; FA143CE51BDA3EEF0041A82B /* AVKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVKit.framework; path = System/Library/Frameworks/AVKit.framework; sourceTree = SDKROOT; }; FA36BD3E1BE57F8E00D5E03B /* VideoDownload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = VideoDownload.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; FA71996E1D94EC5A00FBC99B /* Aerial Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Aerial Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; FA7199701D94EC5A00FBC99B /* PreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesTests.swift; sourceTree = ""; }; FA7199721D94EC5A00FBC99B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; FAB22A7D1BE17D7D0065C0F5 /* AssetLoaderDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AssetLoaderDelegate.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; FAC36F301BE1756D007F2A20 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppDelegate.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; FAC36F321BE1756D007F2A20 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; FAC36F341BE1756D007F2A20 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; FAC36F351BE1756D007F2A20 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; FAC36F391BE1756D007F2A20 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; FAC36F401BE1756D007F2A20 /* AerialVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AerialVideo.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; FAC36F431BE1756D007F2A20 /* AerialView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AerialView.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; FAC36F441BE1756D007F2A20 /* CheckCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CheckCellView.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; FAC36F661BE1778C007F2A20 /* ManifestLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ManifestLoader.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; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 0396D56924B8B7ED00CC021B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 0396D56A24B8B7ED00CC021B /* CoreLocation.framework in Frameworks */, 0396D56D24B8B7ED00CC021B /* IOKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; FA143CD31BDA3E880041A82B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 03510C772185EF8F008F74F2 /* CoreLocation.framework in Frameworks */, 03510C6B21834EB2008F74F2 /* IOKit.framework in Frameworks */, FA143CE61BDA3EEF0041A82B /* AVKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; FA71996B1D94EC5A00FBC99B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; FACAF1A11BD9FC6000E539DC /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 03510C732185EF76008F74F2 /* CoreLocation.framework in Frameworks */, 03510C6C21834EFF008F74F2 /* IOKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 0300B84624D1FCFE006132E5 /* First time setup */ = { isa = PBXGroup; children = ( 0300B84824D1FD24006132E5 /* FirstSetupWindowController.xib */, 0300B84724D1FD24006132E5 /* FirstSetupWindowController.swift */, 0300B86024D2052B006132E5 /* WelcomeViewController.xib */, 0300B85F24D2052B006132E5 /* WelcomeViewController.swift */, 03BF179E24E2F29C0080EF34 /* VideoFormatViewController.xib */, 03BF179D24E2F29C0080EF34 /* VideoFormatViewController.swift */, 03B8742124E41CF8008E3D1B /* CacheSetupViewController.xib */, 03B8742024E41CF8008E3D1B /* CacheSetupViewController.swift */, 037772E024E43AF300D81EEA /* TimeSetupViewController.xib */, 037772DF24E43AF300D81EEA /* TimeSetupViewController.swift */, 037772EC24E44CE100D81EEA /* RecapViewController.xib */, 037772EB24E44CE100D81EEA /* RecapViewController.swift */, 0300B86824D20B12006132E5 /* NextViewController.xib */, 0300B86724D20B12006132E5 /* NextViewController.swift */, ); path = "First time setup"; sourceTree = ""; }; 0306336623A1010800046A59 /* Layers */ = { isa = PBXGroup; children = ( 031C5CB6268CA4D100CE35B4 /* Music */, 03A42A9E2449F946003B3012 /* Weather */, 03BF51BB23A2643C008AD373 /* LayerManager.swift */, 0306336A23A142FA00046A59 /* LayerOffsets.swift */, 0345872C2449C52F00C97D1B /* AnimatableLayer.swift */, 034587312449D8EB00C97D1B /* AnimationLayer.swift */, 0306336D23A15FA900046A59 /* AnimationTextLayer.swift */, 030A0F28245C7C7D009E1D97 /* BatteryIconLayer.swift */, 0306336723A1012200046A59 /* LocationLayer.swift */, 03BF51BE23A274CA008AD373 /* MessageLayer.swift */, 03BF51C123A2978B008AD373 /* ClockLayer.swift */, 03FA49B62428F84500863AF6 /* DateLayer.swift */, 036A57D423F30DD00009DC02 /* DownloadIndicatorLayer.swift */, 036A57DA23F5820A0009DC02 /* CountdownLayer.swift */, 03FA49A02423DA1F00863AF6 /* TimerLayer.swift */, ); path = Layers; sourceTree = ""; }; 031945E524CCC44600F37B35 /* Infos panels */ = { isa = PBXGroup; children = ( 033811AF24C1E243002E23E0 /* InfoViewController.xib */, 033811AE24C1E243002E23E0 /* InfoViewController.swift */, 031945F224CCC48C00F37B35 /* CreditsViewController.xib */, 031945F124CCC48C00F37B35 /* CreditsViewController.swift */, 031945FA24CCC52600F37B35 /* HelpViewController.xib */, 031945F924CCC52600F37B35 /* HelpViewController.swift */, ); path = "Infos panels"; sourceTree = ""; }; 031C5CB6268CA4D100CE35B4 /* Music */ = { isa = PBXGroup; children = ( 03E168192673A63F00D7442D /* MusicLayer.swift */, 031C5CB7268CA4E700CE35B4 /* ArtworkLayer.swift */, ); path = Music; sourceTree = ""; }; 031C5CBB268CBEDC00CE35B4 /* Music */ = { isa = PBXGroup; children = ( 0317C19B268B65D10082A40C /* Music.swift */, ); path = Music; sourceTree = ""; }; 033192DF217B77E90073B580 /* Community */ = { isa = PBXGroup; children = ( F06FCF1928DB8673007558BA /* tl.json */, 03977EEF250E6918008FBAFD /* ar.json */, 03977EE7250E6918008FBAFD /* de.json */, 03977EED250E6918008FBAFD /* en.json */, 0350CB78279481F2005F8625 /* hu.json */, 03977EE9250E6918008FBAFD /* es.json */, 03977EE8250E6918008FBAFD /* fr.json */, 03977EEC250E6918008FBAFD /* he.json */, 03977EEB250E6918008FBAFD /* it.json */, 03977EE3250E6917008FBAFD /* ja.json */, 03AA27BC2614A6F800A4D2CF /* ko.json */, 03977EF0250E6918008FBAFD /* nl.json */, 03977EE6250E6917008FBAFD /* pl.json */, 03977EEA250E6918008FBAFD /* pt_BR.json */, 03977EE4250E6917008FBAFD /* pt.json */, 03E3C1D7256AAAF6000A2A5B /* ru.json */, 03977EE2250E6917008FBAFD /* sv.json */, 03977EEE250E6918008FBAFD /* zh_CN.json */, 03977EE5250E6917008FBAFD /* zh_TW.json */, 0369985C2196103300E359D3 /* missingvideos.json */, ); path = Community; sourceTree = ""; }; 0345A23124532DC600DD47CD /* Weather */ = { isa = PBXGroup; children = ( 03AA7A4024C84C6200A47970 /* battery.0.pdf */, 03AA7A5924C84C6300A47970 /* bolt.fill.pdf */, 03AA7A5824C84C6300A47970 /* cloud.bolt.pdf */, 03AA7A2524C84C6200A47970 /* cloud.bolt.rain.pdf */, 03AA7A5224C84C6300A47970 /* cloud.drizzle.pdf */, 03AA7A4924C84C6200A47970 /* cloud.fog.pdf */, 03AA7A5124C84C6300A47970 /* cloud.hail.pdf */, 03AA7A3F24C84C6200A47970 /* cloud.heavyrain.pdf */, 03AA7A5724C84C6300A47970 /* cloud.moon.bolt.pdf */, 03AA7A5624C84C6300A47970 /* cloud.moon.pdf */, 03AA7A5024C84C6300A47970 /* cloud.moon.rain.pdf */, 03AA7A4524C84C6200A47970 /* cloud.pdf */, 03AA7A5324C84C6300A47970 /* cloud.rain.pdf */, 03AA7A5B24C84C6300A47970 /* cloud.sleet.pdf */, 03AA7A4F24C84C6300A47970 /* cloud.snow.pdf */, 03AA7A4624C84C6200A47970 /* cloud.sun.bolt.pdf */, 03AA7A5A24C84C6300A47970 /* cloud.sun.pdf */, 03AA7A4224C84C6200A47970 /* cloud.sun.rain.pdf */, 03AA7A4824C84C6200A47970 /* hurricane.pdf */, 03AA7A4C24C84C6300A47970 /* moon.pdf */, 03AA7A3E24C84C6200A47970 /* moon.stars.pdf */, 03AA7A4E24C84C6300A47970 /* smoke.pdf */, 03AA7A3D24C84C6200A47970 /* snow.pdf */, 03AA7A4324C84C6200A47970 /* sun.dust.pdf */, 03AA7A5424C84C6300A47970 /* sun.haze.pdf */, 03AA7A4424C84C6200A47970 /* sun.max.pdf */, 03AA7A4D24C84C6300A47970 /* sun.min.pdf */, 03AA7A4724C84C6200A47970 /* thermometer.snowflake.pdf */, 03AA7A4A24C84C6200A47970 /* thermometer.sun.pdf */, 03AA7A5524C84C6300A47970 /* tornado.pdf */, 03AA7A4B24C84C6200A47970 /* tropicalstorm.pdf */, 03AA7A5C24C84C6300A47970 /* wind.pdf */, 03AA7A3C24C84C6200A47970 /* wind.snow.pdf */, 03AA7A4124C84C6200A47970 /* wrench.pdf */, 03A6D14E25F294C900960135 /* location.north.pdf */, ); path = Weather; sourceTree = ""; }; 0345D01524C0A2F60001045C /* UI Icons */ = { isa = PBXGroup; children = ( 03F752ED24EEFC8500945B1D /* house.pdf */, 03F752E824EEFC3100945B1D /* text.cursor.pdf */, 03B8743124E42675008E3D1B /* hand.raised.pdf */, 03B8742F24E42675008E3D1B /* wand.and.rays.pdf */, 03B8743024E42675008E3D1B /* wand.and.stars.pdf */, 0300B7D824D1B533006132E5 /* antenna.radiowaves.left.and.right.pdf */, 0300B7DF24D1B534006132E5 /* arrow.down.circle.fill.pdf */, 0300B7DB24D1B534006132E5 /* arrow.down.circle.pdf */, 0300B7CB24D1B531006132E5 /* bubble.left.and.bubble.right.pdf */, 0300B7E724D1B536006132E5 /* checkmark.circle.fill.pdf */, 0300B7D024D1B532006132E5 /* circle.pdf */, 0300B7D224D1B532006132E5 /* clock.pdf */, 0300B7E624D1B536006132E5 /* dial.pdf */, 0300B7CE24D1B532006132E5 /* eye.pdf */, 0300B7D524D1B533006132E5 /* eye.slash.pdf */, 0300B7D424D1B533006132E5 /* film.pdf */, 0300B7DC24D1B534006132E5 /* flame.pdf */, 0321A53D24D4515B004F1975 /* folder.pdf */, 0300B7CD24D1B532006132E5 /* gear.pdf */, 0300B7CC24D1B531006132E5 /* helm.pdf */, 0300B7D624D1B533006132E5 /* info.circle.pdf */, 0300B7E024D1B535006132E5 /* location.pdf */, 0300B7DE24D1B534006132E5 /* mappin.and.ellipse.pdf */, 0300B7CA24D1B531006132E5 /* person.3.pdf */, 0300B7E424D1B535006132E5 /* regular.cloud.pdf */, 0300B7D724D1B533006132E5 /* regular.moon.stars.pdf */, 0300B7E524D1B535006132E5 /* regular.sun.max.pdf */, 0300B7DD24D1B534006132E5 /* regular.sun.min.pdf */, 030010A524D718D30092AE68 /* slider.horizontal.3.pdf */, 0300B7D324D1B532006132E5 /* sparkles.pdf */, 0300B7E824D1B536006132E5 /* square.and.arrow.down.pdf */, 0300B7E324D1B535006132E5 /* star.fill.pdf */, 0300B7D124D1B532006132E5 /* star.pdf */, 0300B7E224D1B535006132E5 /* sunrise.pdf */, 0300B7D924D1B533006132E5 /* sunset.pdf */, 03075EB024ED794F00FDBE48 /* textformat.alt.pdf */, 03075EA824ED794F00FDBE48 /* trash.pdf */, 0300B7CF24D1B532006132E5 /* text.bubble.pdf */, 0300B7DA24D1B534006132E5 /* tram.fill.pdf */, 0300B7E124D1B535006132E5 /* tv.pdf */, 037772E724E44BB100D81EEA /* xmark.circle.pdf */, 037772F424E4668600D81EEA /* aspectratio.pdf */, ); path = "UI Icons"; sourceTree = ""; }; 0345EDE524C3235900C73038 /* Settings panels */ = { isa = PBXGroup; children = ( 03EBF0C62746A54200EC09D1 /* Collection View */, 03298774274687340036D898 /* NowPlayingViewController.xib */, 03298773274687340036D898 /* NowPlayingViewController.swift */, 03933B8A24C3986800A98D94 /* SourcesViewController.xib */, 03933B8924C3986800A98D94 /* SourcesViewController.swift */, 0345EDE724C3239400C73038 /* DisplaysViewController.xib */, 0345EDE624C3239400C73038 /* DisplaysViewController.swift */, 03BDBEA124C467EC00BBD5E9 /* BrightnessViewController.xib */, 03BDBEA024C467EC00BBD5E9 /* BrightnessViewController.swift */, 03BDBEBF24C4727C00BBD5E9 /* TimeViewController.xib */, 03BDBEBE24C4727C00BBD5E9 /* TimeViewController.swift */, 0378985D24C35F8A009B9418 /* CacheViewController.xib */, 0378985C24C35F8A009B9418 /* CacheViewController.swift */, 032E099D24C3897E00387230 /* AdvancedViewController.xib */, 032E099C24C3897E00387230 /* AdvancedViewController.swift */, 03EED2EF24C44A7900F0C3D4 /* OverlaysViewController.xib */, 03EED2EE24C44A7900F0C3D4 /* OverlaysViewController.swift */, 030010AA24D71EB20092AE68 /* FiltersViewController.xib */, 030010A924D71EB20092AE68 /* FiltersViewController.swift */, F0A3E0A22884683D005E8D8D /* CompanionCacheViewController.xib */, F0A3E0A12884683D005E8D8D /* CompanionCacheViewController.swift */, ); path = "Settings panels"; sourceTree = ""; }; 0345EDF624C3462100C73038 /* Old stuff */ = { isa = PBXGroup; children = ( 03A42A9B2449F601003B3012 /* purple_retina.png */, 03A42A822449F5EA003B3012 /* white_retina.png */, FAC36F391BE1756D007F2A20 /* Info.plist */, 0395835221807D1F008E8F9C /* thumbnail.png */, 0395835121807D1F008E8F9C /* thumbnail@2x.png */, 0377BF6F24BA15E600C33F9F /* btn_donate.png */, 0313F9E522942AA500B074BB /* CustomVideos.xib */, ); path = "Old stuff"; sourceTree = ""; }; 034A6DAA24ACC7BC00D62129 /* Sources */ = { isa = PBXGroup; children = ( 0345D00924BF4E8C0001045C /* Sidebar.swift */, 03C97BA624B5F74900739CED /* VideoList.swift */, 034A6DC124ACC7C800D62129 /* SourceList.swift */, 034A6DC324ACC80200D62129 /* Source.swift */, 03C97BC224B6210500739CED /* SourceInfo.swift */, ); path = Sources; sourceTree = ""; }; 034DEE2B24BF1B8A00A2D3CD /* MainUI */ = { isa = PBXGroup; children = ( 0300B84624D1FCFE006132E5 /* First time setup */, 031945E524CCC44600F37B35 /* Infos panels */, 0345EDE524C3235900C73038 /* Settings panels */, 034DEE2D24BF1BC700A2D3CD /* PanelWindowController.xib */, 034DEE2C24BF1BC700A2D3CD /* PanelWindowController.swift */, 0313329B24BF3FA700C84A05 /* SidebarViewController.xib */, 0313329A24BF3FA700C84A05 /* SidebarViewController.swift */, 0345CFEC24BF43280001045C /* VideosViewController.xib */, 0345CFEB24BF43280001045C /* VideosViewController.swift */, ); path = MainUI; sourceTree = ""; }; 034DEE3424BF254F00A2D3CD /* MainUI */ = { isa = PBXGroup; children = ( 0345D00D24C07CC70001045C /* VideoCellView.swift */, 03D3A11224C5FC770091FE99 /* AspectFillNSImageView.swift */, 030010A124D706DB0092AE68 /* SidebarOutlineView.swift */, F05E805328AE8A9C0088B9C5 /* NowPlayingCollectionView.swift */, ); path = MainUI; sourceTree = ""; }; 034F29B523A7A8E1004B34D5 /* PrefPanel */ = { isa = PBXGroup; children = ( 030473C924BCA9A40094A1A6 /* VideoViewItem.swift */, 03E2237024BE048900CD8ED4 /* VideoHeaderView.swift */, FAC36F441BE1756D007F2A20 /* CheckCellView.swift */, 03D1E78622842FB300D10CF7 /* DisplayView.swift */, 034F29B623A7A93D004B34D5 /* InfoTableSource.swift */, 0354D0E823F6C31800D86F9E /* InfoSettingsTableSource.swift */, 038C584623A9304800224630 /* InfoContainerView.swift */, 038C584923A9394000224630 /* InfoCommonView.swift */, 03A596D823AB8F000097EA66 /* InfoLocationView.swift */, 03A596D223AA750F0097EA66 /* InfoMessageView.swift */, 03A596D423AA752F0097EA66 /* InfoClockView.swift */, 038D2EE323B6565900CD91F7 /* InfoBatteryView.swift */, 036A57D723F470940009DC02 /* InfoCountdownView.swift */, 0354D0EB23F6CB7B00D86F9E /* InfoSettingsView.swift */, 030DDA8F2423C3BE0072D5C9 /* InfoTimerView.swift */, 03FA49B32428EE3300863AF6 /* InfoDateView.swift */, 0385FC5C242B9F6E007E6513 /* InfoWeatherView.swift */, 03E168152673A23900D7442D /* InfoMusicView.swift */, ); path = PrefPanel; sourceTree = ""; }; 034F29BA23A7D516004B34D5 /* Prefs */ = { isa = PBXGroup; children = ( 034F29BE23A7E28E004B34D5 /* PrefsInfo.swift */, 038D2EDD23B0FB0D00CD91F7 /* PrefsVideos.swift */, 0361B9A723D732A300B6252D /* PrefsDisplays.swift */, 0361B9AA23D73D4500B6252D /* PrefsTime.swift */, 034116D223F9BD3100CD7674 /* PrefsUpdates.swift */, 03A4A80D2451D04C00A1F7A3 /* PrefsAdvanced.swift */, 031FB78A248A87330054BAFD /* PrefsCache.swift */, ); path = Prefs; sourceTree = ""; }; 0385FC57242B9A9B007E6513 /* API */ = { isa = PBXGroup; children = ( 0385FC58242B9AE1007E6513 /* APISecrets.swift */, 03A6D14525F109B900960135 /* OpenWeather.swift */, 032851F6260A4C2B00684A81 /* OneCall.swift */, 03AA32612631A8F2002198C3 /* GeoCoding.swift */, 036A7E9A26370C260019186B /* Forecast.swift */, ); path = API; sourceTree = ""; }; 03958347217F43FF008E8F9C /* Time */ = { isa = PBXGroup; children = ( 03958348217F4416008E8F9C /* Solar.swift */, 03E8731221675FE0002B469B /* TimeManagement.swift */, 03510C6E21834F38008F74F2 /* IOBridge.m */, 03510C6D21834F38008F74F2 /* Aerial-Bridging-Header.h */, ); path = Time; sourceTree = ""; }; 03A42A9E2449F946003B3012 /* Weather */ = { isa = PBXGroup; children = ( 033842E024489D7300A2C523 /* WeatherLayer.swift */, 03A42A9F2449F959003B3012 /* YahooLogoLayer.swift */, 032851FA260A625100684A81 /* ForecastLayer.swift */, 03A42AA2244A0E5F003B3012 /* ConditionLayer.swift */, 033D68802453080C0016F837 /* ConditionSymbolLayer.swift */, 03A6D15225F297CE00960135 /* WindDirectionLayer.swift */, ); path = Weather; sourceTree = ""; }; 03C344FA24B778D500906EA6 /* Sources */ = { isa = PBXGroup; children = ( 03F5551A24E9C091003AAD0B /* SourceOutlineView.swift */, 03C344FB24B778EE00906EA6 /* CheckboxCellView.swift */, 03C344FE24B7A22300906EA6 /* DescriptionCellView.swift */, 0321A53224D44E80004F1975 /* ActionCellView.swift */, ); path = Sources; sourceTree = ""; }; 03D1E78D22848F6D00D10CF7 /* Screenshots */ = { isa = PBXGroup; children = ( 03F752CF24EED19B00945B1D /* video_inspire_alabama_montgomery_countryside_00007.png */, 03F752D024EED19C00945B1D /* video_inspire_california_big-sur_2020_00001.png */, 03F752D224EED19C00945B1D /* video_inspire_california_catalina_00005.png */, 03F752D124EED19C00945B1D /* video_inspire_italy_como_cerano-dintelvi_006.png */, 03F752E324EED21100945B1D /* video_inspire_california_vineyard_sierra-mar_sunrise_110.png */, 03D1E79022848F7F00D10CF7 /* screen0.jpg */, 03D1E78F22848F7F00D10CF7 /* screen1.jpg */, 03D1E78E22848F7F00D10CF7 /* screen2.jpg */, ); path = Screenshots; sourceTree = ""; }; 03DAD46E229EAC66000DA6D1 /* Documentation */ = { isa = PBXGroup; children = ( 03DAD46F229EAC66000DA6D1 /* Installation.md */, 03DAD471229EB1E9000DA6D1 /* ChangeLog.md */, 03DAD474229EC3CD000DA6D1 /* OfflineMode.md */, 03DAD475229EC544000DA6D1 /* HardwareDecoding.md */, 03DAD476229EC64D000DA6D1 /* Troubleshooting.md */, 03DAD477229ECAAA000DA6D1 /* CustomVideos.md */, 030B5E32229FDD26008F2910 /* AutoUpdates.md */, 03D6C78922A0384F00DE830B /* README.md */, ); path = Documentation; sourceTree = ""; }; 03E8730D216501B3002B469B /* Downloads */ = { isa = PBXGroup; children = ( 03E8730B2165013C002B469B /* DownloadManager.swift */, 03E8730E216501ED002B469B /* AsynchronousOperation.swift */, ); path = Downloads; sourceTree = ""; }; 03EBF0C62746A54200EC09D1 /* Collection View */ = { isa = PBXGroup; children = ( 03EBF0BF2746A53B00EC09D1 /* PlayingCollectionViewItem.xib */, 03EBF0BE2746A53B00EC09D1 /* PlayingCollectionViewItem.swift */, ); path = "Collection View"; sourceTree = ""; }; F0058B7D28ABF69B0053699B /* ISSoundAdditions */ = { isa = PBXGroup; children = ( F0058B7E28ABF69B0053699B /* SoundOutputManager+Goodies.swift */, F0058B7F28ABF69B0053699B /* SoundOutputManager.swift */, F0058B8028ABF69B0053699B /* Sound.swift */, F0058B8128ABF69B0053699B /* SoundOutputManager+Properties.swift */, ); path = ISSoundAdditions; sourceTree = ""; }; F008DAFB23AADC8E00739DE1 /* Hardware */ = { isa = PBXGroup; children = ( F0058B7D28ABF69B0053699B /* ISSoundAdditions */, 03D1E7892284471A00D10CF7 /* DisplayDetection.swift */, 03608A2B22A56465008F08A2 /* HardwareDetection.swift */, 032D1160239A7D82007E7756 /* Battery.swift */, F008DAFC23AADCFB00739DE1 /* Brightness.swift */, F00864B323AAE7F0003210EF /* DarkMode.swift */, F00864B623AAE8E9003210EF /* NightShift.swift */, ); path = Hardware; sourceTree = ""; }; FA2D7AA01BDD849E009EA54C /* Frameworks */ = { isa = PBXGroup; children = ( 03510C722185EF76008F74F2 /* CoreLocation.framework */, 03510C6A21834EB2008F74F2 /* IOKit.framework */, FA143CE51BDA3EEF0041A82B /* AVKit.framework */, ); name = Frameworks; sourceTree = ""; }; FA71996F1D94EC5A00FBC99B /* Tests */ = { isa = PBXGroup; children = ( FA7199701D94EC5A00FBC99B /* PreferencesTests.swift */, FA7199721D94EC5A00FBC99B /* Info.plist */, ); path = Tests; sourceTree = ""; }; FAC36F2F1BE1756D007F2A20 /* App */ = { isa = PBXGroup; children = ( FAC36F301BE1756D007F2A20 /* AppDelegate.swift */, FAC36F311BE1756D007F2A20 /* Resources */, ); name = App; path = Aerial/App; sourceTree = ""; }; FAC36F311BE1756D007F2A20 /* Resources */ = { isa = PBXGroup; children = ( FAC36F321BE1756D007F2A20 /* Assets.xcassets */, FAC36F331BE1756D007F2A20 /* MainMenu.xib */, FAC36F351BE1756D007F2A20 /* Info.plist */, ); path = Resources; sourceTree = ""; }; FAC36F361BE1756D007F2A20 /* Resources */ = { isa = PBXGroup; children = ( 03F3C1B924F2A923007733B5 /* kofi1@2x.png */, 03DB968325657AE600BFCF20 /* LogoIcon-128px@2x.png */, 03DB966D256569F800BFCF20 /* icon-512@2x.png */, 0319460824D1A09F00F37B35 /* icon-320@2x.png */, 0343E9F82630590A00AC702F /* openweather_logo.png */, 0345EDF624C3462100C73038 /* Old stuff */, 0345D01524C0A2F60001045C /* UI Icons */, 034DEE2B24BF1B8A00A2D3CD /* MainUI */, 0345A23124532DC600DD47CD /* Weather */, 03D1E78D22848F6D00D10CF7 /* Screenshots */, 033192DF217B77E90073B580 /* Community */, ); path = Resources; sourceTree = ""; }; FAC36F3C1BE1756D007F2A20 /* Source */ = { isa = PBXGroup; children = ( FAC36F3D1BE1756D007F2A20 /* Controllers */, FAC36F3F1BE1756D007F2A20 /* Models */, FAC36F421BE1756D007F2A20 /* Views */, ); name = Source; path = Aerial/Source; sourceTree = ""; }; FAC36F3D1BE1756D007F2A20 /* Controllers */ = { isa = PBXGroup; children = ( 0313F9E722942B4500B074BB /* CustomVideoController.swift */, ); path = Controllers; sourceTree = ""; }; FAC36F3F1BE1756D007F2A20 /* Models */ = { isa = PBXGroup; children = ( 034A6DAA24ACC7BC00D62129 /* Sources */, 0385FC57242B9A9B007E6513 /* API */, F008DAFB23AADC8E00739DE1 /* Hardware */, 034F29BA23A7D516004B34D5 /* Prefs */, 03958347217F43FF008E8F9C /* Time */, 03E8730D216501B3002B469B /* Downloads */, FAC36F411BE1756D007F2A20 /* Cache */, FAC36F621BE17701007F2A20 /* Extensions */, 031C5CBB268CBEDC00CE35B4 /* Music */, 0338119424C1D15B002E23E0 /* Aerial.swift */, FAC36F401BE1756D007F2A20 /* AerialVideo.swift */, 0374C9FD247AC5BC002F29D3 /* Locations.swift */, FAC36F661BE1778C007F2A20 /* ManifestLoader.swift */, 03893CB2217749F0008E7125 /* ErrorLog.swift */, 0313F9EB2294468600B074BB /* SeededGenerator.swift */, 0313F9EE22955F3B00B074BB /* CustomVideoFolders.swift */, 03AD45FE22981B0C00261325 /* CustomVideoFolders+helpers.swift */, 03C97BBF24B60E2F00739CED /* FileHelpers.swift */, 03FF192F269709AB00A0FA7F /* PlaybackSpeed.swift */, F07221862AD4354E001F5452 /* CompanionBridge.swift */, ); path = Models; sourceTree = ""; }; FAC36F411BE1756D007F2A20 /* Cache */ = { isa = PBXGroup; children = ( FAB22A7D1BE17D7D0065C0F5 /* AssetLoaderDelegate.swift */, FAF450201BE2B45D00C1F98A /* VideoLoader.swift */, FAF450231BE2D2FD00C1F98A /* VideoCache.swift */, FA36BD3E1BE57F8E00D5E03B /* VideoDownload.swift */, 03A2CB9B216BA9AF0061E8E8 /* VideoManager.swift */, 03233B67217272640077D3F9 /* PoiStringProvider.swift */, 03DC004D248BC5A4005DB0F4 /* Cache.swift */, 03D3A10C24C5D7CC0091FE99 /* Thumbnails.swift */, 03977F1F250E7165008FBAFD /* TimeMachine.swift */, ); path = Cache; sourceTree = ""; }; FAC36F421BE1756D007F2A20 /* Views */ = { isa = PBXGroup; children = ( 034DEE3424BF254F00A2D3CD /* MainUI */, 03C344FA24B778D500906EA6 /* Sources */, 034F29B523A7A8E1004B34D5 /* PrefPanel */, 0306336623A1010800046A59 /* Layers */, FAC36F431BE1756D007F2A20 /* AerialView.swift */, 032D1163239A7F0C007E7756 /* AerialView+Brightness.swift */, 035D524E239AA31A00DC29DC /* AerialView+Player.swift */, AA7E2E5D1FC62E8B00E5F320 /* AerialPlayerItem.swift */, ); path = Views; sourceTree = ""; }; FAC36F621BE17701007F2A20 /* Extensions */ = { isa = PBXGroup; children = ( 039385792175D4B80040B850 /* AVPlayerViewExtension.swift */, 03A4A80A2451CE2C00A1F7A3 /* NSImage+trim.swift */, 03047A5324D2DD8E000EFE62 /* NSMenuItem+icons.swift */, 0321A54324D5C863004F1975 /* NSButton+icons.swift */, 0300109624D6EF4C0092AE68 /* AVPlayerItem+vibrance.swift */, 0332076C26D7C355001F9837 /* AVAsset+VideoOrientation.swift */, 03C605DB277B45CA005CA51F /* DispatchQueue+Extension.swift */, ); path = Extensions; sourceTree = ""; }; FACAF19B1BD9FC6000E539DC = { isa = PBXGroup; children = ( 03DAD472229EC031000DA6D1 /* Readme.md */, 03DAD46E229EAC66000DA6D1 /* Documentation */, FAC36F2F1BE1756D007F2A20 /* App */, FAC36F361BE1756D007F2A20 /* Resources */, FAC36F3C1BE1756D007F2A20 /* Source */, FA71996F1D94EC5A00FBC99B /* Tests */, FA2D7AA01BDD849E009EA54C /* Frameworks */, FACAF1A61BD9FC6000E539DC /* Products */, 0396D50624B8B7D700CC021B /* AerialApp copy-Info.plist */, 0396D5B624B8B7EE00CC021B /* Aerial copy-Info.plist */, ); sourceTree = ""; }; FACAF1A61BD9FC6000E539DC /* Products */ = { isa = PBXGroup; children = ( FACAF1A51BD9FC6000E539DC /* Aerial.saver */, FA143CD61BDA3E880041A82B /* AerialApp.app */, FA71996E1D94EC5A00FBC99B /* Aerial Tests.xctest */, 0396D5B524B8B7ED00CC021B /* Aerial (low dependencies).saver */, ); name = Products; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ 0396D56E24B8B7ED00CC021B /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 0396D56F24B8B7ED00CC021B /* Aerial-Bridging-Header.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; FACAF1A21BD9FC6000E539DC /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 03510C7021834FC3008F74F2 /* Aerial-Bridging-Header.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ 0396D50724B8B7ED00CC021B /* Aerial (low dependencies) */ = { isa = PBXNativeTarget; buildConfigurationList = 0396D5B224B8B7ED00CC021B /* Build configuration list for PBXNativeTarget "Aerial (low dependencies)" */; buildPhases = ( 0396D50824B8B7ED00CC021B /* Run Script - Swiftlint */, 0396D50924B8B7ED00CC021B /* Sources */, 0396D56924B8B7ED00CC021B /* Frameworks */, 0396D56E24B8B7ED00CC021B /* Headers */, 0396D57024B8B7ED00CC021B /* Resources */, 0396D5AE24B8B7ED00CC021B /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = "Aerial (low dependencies)"; packageProductDependencies = ( ); productName = Aerial; productReference = 0396D5B524B8B7ED00CC021B /* Aerial (low dependencies).saver */; productType = "com.apple.product-type.bundle"; }; FA143CD51BDA3E880041A82B /* AerialApp */ = { isa = PBXNativeTarget; buildConfigurationList = FA143CE01BDA3E880041A82B /* Build configuration list for PBXNativeTarget "AerialApp" */; buildPhases = ( FA74B8481D94DCE0004FE056 /* Run Script - Swiftlint */, FA143CD21BDA3E880041A82B /* Sources */, FA143CD31BDA3E880041A82B /* Frameworks */, FA143CD41BDA3E880041A82B /* Resources */, 034116D123F9AD0600CD7674 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = AerialApp; packageProductDependencies = ( ); productName = "Aerial Test"; productReference = FA143CD61BDA3E880041A82B /* AerialApp.app */; productType = "com.apple.product-type.application"; }; FA71996D1D94EC5A00FBC99B /* Aerial Tests */ = { isa = PBXNativeTarget; buildConfigurationList = FA7199751D94EC5A00FBC99B /* Build configuration list for PBXNativeTarget "Aerial Tests" */; buildPhases = ( FA71996A1D94EC5A00FBC99B /* Sources */, FA71996B1D94EC5A00FBC99B /* Frameworks */, FA71996C1D94EC5A00FBC99B /* Resources */, ); buildRules = ( ); dependencies = ( FA7199741D94EC5A00FBC99B /* PBXTargetDependency */, ); name = "Aerial Tests"; productName = "Aerial Tests"; productReference = FA71996E1D94EC5A00FBC99B /* Aerial Tests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; FACAF1A41BD9FC6000E539DC /* Aerial */ = { isa = PBXNativeTarget; buildConfigurationList = FACAF1AF1BD9FC6000E539DC /* Build configuration list for PBXNativeTarget "Aerial" */; buildPhases = ( 7E9B50FB2187D302002895ED /* Run Script - Swiftlint */, FACAF1A01BD9FC6000E539DC /* Sources */, FACAF1A11BD9FC6000E539DC /* Frameworks */, FACAF1A21BD9FC6000E539DC /* Headers */, FACAF1A31BD9FC6000E539DC /* Resources */, 034116CE23F9AD0100CD7674 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Aerial; packageProductDependencies = ( ); productName = Aerial; productReference = FACAF1A51BD9FC6000E539DC /* Aerial.saver */; productType = "com.apple.product-type.bundle"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ FACAF19C1BD9FC6000E539DC /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0800; LastUpgradeCheck = 1010; ORGANIZATIONNAME = "Guillaume Louel"; TargetAttributes = { 0396D50724B8B7ED00CC021B = { DevelopmentTeam = 3L54M5L5KK; ProvisioningStyle = Manual; }; FA143CD51BDA3E880041A82B = { CreatedOnToolsVersion = 7.0; DevelopmentTeam = 3L54M5L5KK; LastSwiftMigration = 1020; ProvisioningStyle = Automatic; }; FA71996D1D94EC5A00FBC99B = { CreatedOnToolsVersion = 8.0; LastSwiftMigration = 1020; ProvisioningStyle = Automatic; TestTargetID = FA143CD51BDA3E880041A82B; }; FACAF1A41BD9FC6000E539DC = { CreatedOnToolsVersion = 7.0; DevelopmentTeam = 3L54M5L5KK; LastSwiftMigration = 1020; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = FACAF19F1BD9FC6000E539DC /* Build configuration list for PBXProject "Aerial" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = FACAF19B1BD9FC6000E539DC; packageReferences = ( ); productRefGroup = FACAF1A61BD9FC6000E539DC /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( FACAF1A41BD9FC6000E539DC /* Aerial */, 0396D50724B8B7ED00CC021B /* Aerial (low dependencies) */, FA143CD51BDA3E880041A82B /* AerialApp */, FA71996D1D94EC5A00FBC99B /* Aerial Tests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 0396D57024B8B7ED00CC021B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 03977F19250E6918008FBAFD /* ar.json in Resources */, 03AA7A9424C84C6300A47970 /* sun.min.pdf in Resources */, 03AA7A7F24C84C6300A47970 /* cloud.sun.bolt.pdf in Resources */, 03977F0D250E6918008FBAFD /* it.json in Resources */, 03AA7AB524C84C6400A47970 /* cloud.bolt.pdf in Resources */, 03AA7A7C24C84C6300A47970 /* cloud.pdf in Resources */, 0300B83524D1B536006132E5 /* star.fill.pdf in Resources */, 03977F0A250E6918008FBAFD /* pt_BR.json in Resources */, 0300B86D24D20B12006132E5 /* NextViewController.xib in Resources */, 03298779274687340036D898 /* NowPlayingViewController.xib in Resources */, 0345CFF124BF43280001045C /* VideosViewController.xib in Resources */, 0300B81D24D1B536006132E5 /* arrow.down.circle.pdf in Resources */, 0300B81724D1B536006132E5 /* sunset.pdf in Resources */, 03F752EF24EEFC8500945B1D /* house.pdf in Resources */, 0300B7F924D1B536006132E5 /* text.bubble.pdf in Resources */, 032E09A224C3897E00387230 /* AdvancedViewController.xib in Resources */, 0300B82324D1B536006132E5 /* regular.sun.min.pdf in Resources */, 0321A53F24D4515B004F1975 /* folder.pdf in Resources */, 0300B82924D1B536006132E5 /* arrow.down.circle.fill.pdf in Resources */, F06FCF1B28DB8673007558BA /* tl.json in Resources */, 037772F124E44CE100D81EEA /* RecapViewController.xib in Resources */, 03977F10250E6918008FBAFD /* he.json in Resources */, 0396D57624B8B7ED00CC021B /* white_retina.png in Resources */, 037772FD24E4668600D81EEA /* aspectratio.pdf in Resources */, 03AA7A8224C84C6300A47970 /* thermometer.snowflake.pdf in Resources */, 03AA27BE2614A6F800A4D2CF /* ko.json in Resources */, 03DB968525657AE600BFCF20 /* LogoIcon-128px@2x.png in Resources */, 03AA7A7024C84C6300A47970 /* wrench.pdf in Resources */, 03AA7A9124C84C6300A47970 /* moon.pdf in Resources */, 0300B81124D1B536006132E5 /* regular.moon.stars.pdf in Resources */, 03AA7AA624C84C6300A47970 /* cloud.rain.pdf in Resources */, 03F752E024EED19C00945B1D /* video_inspire_california_catalina_00005.png in Resources */, 03AA7A9724C84C6300A47970 /* smoke.pdf in Resources */, 03AA7A8824C84C6300A47970 /* cloud.fog.pdf in Resources */, 030010A724D718D30092AE68 /* slider.horizontal.3.pdf in Resources */, 0300B82C24D1B536006132E5 /* location.pdf in Resources */, 0300B84D24D1FD24006132E5 /* FirstSetupWindowController.xib in Resources */, 03977F07250E6918008FBAFD /* es.json in Resources */, 03AA7A6D24C84C6300A47970 /* battery.0.pdf in Resources */, 03977F01250E6918008FBAFD /* de.json in Resources */, 0300B82624D1B536006132E5 /* mappin.and.ellipse.pdf in Resources */, 0300B80B24D1B536006132E5 /* eye.slash.pdf in Resources */, 0350CB7D279481F2005F8625 /* hu.json in Resources */, 0396D57924B8B7ED00CC021B /* screen2.jpg in Resources */, 03B8743324E42675008E3D1B /* wand.and.rays.pdf in Resources */, 0319461124D1A09F00F37B35 /* icon-320@2x.png in Resources */, 03AA7ABE24C84C6400A47970 /* cloud.sleet.pdf in Resources */, 03977EFB250E6918008FBAFD /* zh_TW.json in Resources */, 03BDBEC424C4727C00BBD5E9 /* TimeViewController.xib in Resources */, 03AA7A8B24C84C6300A47970 /* thermometer.sun.pdf in Resources */, 0300B83E24D1B536006132E5 /* dial.pdf in Resources */, 0300B83224D1B536006132E5 /* sunrise.pdf in Resources */, 03F752E524EED21100945B1D /* video_inspire_california_vineyard_sierra-mar_sunrise_110.png in Resources */, 0300B80824D1B536006132E5 /* film.pdf in Resources */, 0300B83B24D1B536006132E5 /* regular.sun.max.pdf in Resources */, 0300B7FF24D1B536006132E5 /* star.pdf in Resources */, 03AA7A7624C84C6300A47970 /* sun.dust.pdf in Resources */, 03B8742624E41CF8008E3D1B /* CacheSetupViewController.xib in Resources */, 0377BF8724BA15E700C33F9F /* btn_donate.png in Resources */, 037772E524E43AF300D81EEA /* TimeSetupViewController.xib in Resources */, 037772E924E44BB100D81EEA /* xmark.circle.pdf in Resources */, 0300B82F24D1B536006132E5 /* tv.pdf in Resources */, 03977EF8250E6918008FBAFD /* pt.json in Resources */, 0300B81424D1B536006132E5 /* antenna.radiowaves.left.and.right.pdf in Resources */, 03075EB224ED794F00FDBE48 /* trash.pdf in Resources */, 03AA7ABB24C84C6400A47970 /* cloud.sun.pdf in Resources */, 0396D58B24B8B7ED00CC021B /* screen0.jpg in Resources */, 0300B7F624D1B536006132E5 /* eye.pdf in Resources */, 03AA7AB824C84C6400A47970 /* bolt.fill.pdf in Resources */, 03AA7A9D24C84C6300A47970 /* cloud.moon.rain.pdf in Resources */, 0378986224C35F8A009B9418 /* CacheViewController.xib in Resources */, 03F3C1BB24F2A923007733B5 /* kofi1@2x.png in Resources */, 03977EF2250E6918008FBAFD /* sv.json in Resources */, 03977F16250E6918008FBAFD /* zh_CN.json in Resources */, 030010AF24D71EB20092AE68 /* FiltersViewController.xib in Resources */, 03AA7A7324C84C6300A47970 /* cloud.sun.rain.pdf in Resources */, 03977EF5250E6918008FBAFD /* ja.json in Resources */, 0396D58D24B8B7ED00CC021B /* thumbnail@2x.png in Resources */, 0396D58E24B8B7ED00CC021B /* thumbnail.png in Resources */, 03F752D824EED19C00945B1D /* video_inspire_california_big-sur_2020_00001.png in Resources */, 0300B86524D2052B006132E5 /* WelcomeViewController.xib in Resources */, 03AA7A6A24C84C6300A47970 /* cloud.heavyrain.pdf in Resources */, 03AA7A9A24C84C6300A47970 /* cloud.snow.pdf in Resources */, 0300B80224D1B536006132E5 /* clock.pdf in Resources */, 0300B81A24D1B536006132E5 /* tram.fill.pdf in Resources */, 0300B82024D1B536006132E5 /* flame.pdf in Resources */, 03977EFE250E6918008FBAFD /* pl.json in Resources */, 03B8743924E42675008E3D1B /* hand.raised.pdf in Resources */, 03AA7AA924C84C6300A47970 /* sun.haze.pdf in Resources */, 0300B7EA24D1B536006132E5 /* person.3.pdf in Resources */, 03F752DC24EED19C00945B1D /* video_inspire_italy_como_cerano-dintelvi_006.png in Resources */, 031332A024BF3FA700C84A05 /* SidebarViewController.xib in Resources */, 031945FF24CCC52600F37B35 /* HelpViewController.xib in Resources */, 0300B7F024D1B536006132E5 /* helm.pdf in Resources */, 03AA7A8E24C84C6300A47970 /* tropicalstorm.pdf in Resources */, 0345EDEC24C3239400C73038 /* DisplaysViewController.xib in Resources */, 0396D59224B8B7ED00CC021B /* screen1.jpg in Resources */, 03977F1C250E6918008FBAFD /* nl.json in Resources */, 0300B7ED24D1B536006132E5 /* bubble.left.and.bubble.right.pdf in Resources */, 03AA7AA324C84C6300A47970 /* cloud.drizzle.pdf in Resources */, 03977F04250E6918008FBAFD /* fr.json in Resources */, 03AA7AB224C84C6400A47970 /* cloud.moon.bolt.pdf in Resources */, 0300B7FC24D1B536006132E5 /* circle.pdf in Resources */, 0300B80524D1B536006132E5 /* sparkles.pdf in Resources */, 03E3C1DF256AAAF6000A2A5B /* ru.json in Resources */, 03B8743624E42675008E3D1B /* wand.and.stars.pdf in Resources */, 03BDBEA624C467EC00BBD5E9 /* BrightnessViewController.xib in Resources */, 03AA7AC124C84C6400A47970 /* wind.pdf in Resources */, 0396D59D24B8B7ED00CC021B /* missingvideos.json in Resources */, 034DEE3224BF1BC700A2D3CD /* PanelWindowController.xib in Resources */, 03AA7AAC24C84C6300A47970 /* tornado.pdf in Resources */, 0300B7F324D1B536006132E5 /* gear.pdf in Resources */, 03AA7A5E24C84C6300A47970 /* cloud.bolt.rain.pdf in Resources */, 0300B83824D1B536006132E5 /* regular.cloud.pdf in Resources */, 03AA7A8524C84C6300A47970 /* hurricane.pdf in Resources */, 0343E9FA2630590A00AC702F /* openweather_logo.png in Resources */, 0300B80E24D1B536006132E5 /* info.circle.pdf in Resources */, 03AA7A6424C84C6300A47970 /* snow.pdf in Resources */, 0396D5A824B8B7ED00CC021B /* CustomVideos.xib in Resources */, 03AA7AA024C84C6300A47970 /* cloud.hail.pdf in Resources */, 03BF17A324E2F29C0080EF34 /* VideoFormatViewController.xib in Resources */, 03F752D424EED19C00945B1D /* video_inspire_alabama_montgomery_countryside_00007.png in Resources */, 03DB9675256569F800BFCF20 /* icon-512@2x.png in Resources */, 03EED2F424C44A7900F0C3D4 /* OverlaysViewController.xib in Resources */, 0300B84124D1B536006132E5 /* checkmark.circle.fill.pdf in Resources */, 031945F724CCC48C00F37B35 /* CreditsViewController.xib in Resources */, 03075EB524ED794F00FDBE48 /* textformat.alt.pdf in Resources */, 03AA7AAF24C84C6400A47970 /* cloud.moon.pdf in Resources */, 0396D5AB24B8B7ED00CC021B /* purple_retina.png in Resources */, 03AA7A6124C84C6300A47970 /* wind.snow.pdf in Resources */, 03977F13250E6918008FBAFD /* en.json in Resources */, 03EBF0C42746A53B00EC09D1 /* PlayingCollectionViewItem.xib in Resources */, 03AA7A6724C84C6300A47970 /* moon.stars.pdf in Resources */, 0300B84424D1B536006132E5 /* square.and.arrow.down.pdf in Resources */, F0A3E0A72884683D005E8D8D /* CompanionCacheViewController.xib in Resources */, 03AA7A7924C84C6300A47970 /* sun.max.pdf in Resources */, 03933B8F24C3986800A98D94 /* SourcesViewController.xib in Resources */, 03F752EA24EEFC3100945B1D /* text.cursor.pdf in Resources */, 03A6D15025F294C900960135 /* location.north.pdf in Resources */, 033811B424C1E243002E23E0 /* InfoViewController.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; FA143CD41BDA3E880041A82B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 03977F0E250E6918008FBAFD /* it.json in Resources */, 0300B7F724D1B536006132E5 /* eye.pdf in Resources */, 03BDBEC524C4727C00BBD5E9 /* TimeViewController.xib in Resources */, 0300B80024D1B536006132E5 /* star.pdf in Resources */, 03977EFC250E6918008FBAFD /* zh_TW.json in Resources */, 03DAD470229EAC66000DA6D1 /* Installation.md in Resources */, 03AA7A6B24C84C6300A47970 /* cloud.heavyrain.pdf in Resources */, 033811B524C1E243002E23E0 /* InfoViewController.xib in Resources */, 03A42A9D2449F602003B3012 /* purple_retina.png in Resources */, 03AA7A7D24C84C6300A47970 /* cloud.pdf in Resources */, 037772F224E44CE100D81EEA /* RecapViewController.xib in Resources */, 03977F08250E6918008FBAFD /* es.json in Resources */, 03F752EB24EEFC3100945B1D /* text.cursor.pdf in Resources */, 03977F14250E6918008FBAFD /* en.json in Resources */, 0300B81E24D1B536006132E5 /* arrow.down.circle.pdf in Resources */, F06FCF1C28DB8673007558BA /* tl.json in Resources */, 0300B82724D1B536006132E5 /* mappin.and.ellipse.pdf in Resources */, 0300B7EB24D1B536006132E5 /* person.3.pdf in Resources */, 0343E9FB2630590A00AC702F /* openweather_logo.png in Resources */, 03AA7AB024C84C6400A47970 /* cloud.moon.pdf in Resources */, 0300B81524D1B536006132E5 /* antenna.radiowaves.left.and.right.pdf in Resources */, 03D1E79522848F7F00D10CF7 /* screen1.jpg in Resources */, 0300B80C24D1B536006132E5 /* eye.slash.pdf in Resources */, 03AA7AA124C84C6300A47970 /* cloud.hail.pdf in Resources */, 0345EDED24C3239400C73038 /* DisplaysViewController.xib in Resources */, 03AA7A7424C84C6300A47970 /* cloud.sun.rain.pdf in Resources */, 03AA7AC224C84C6400A47970 /* wind.pdf in Resources */, 03AA7A8F24C84C6300A47970 /* tropicalstorm.pdf in Resources */, 03AA7A6824C84C6300A47970 /* moon.stars.pdf in Resources */, 03B8743A24E42675008E3D1B /* hand.raised.pdf in Resources */, 03AA7A8324C84C6300A47970 /* thermometer.snowflake.pdf in Resources */, 0300B81824D1B536006132E5 /* sunset.pdf in Resources */, 03977F1A250E6918008FBAFD /* ar.json in Resources */, 03AA7A7124C84C6300A47970 /* wrench.pdf in Resources */, 0369985E2196129C00E359D3 /* missingvideos.json in Resources */, 0300B83024D1B536006132E5 /* tv.pdf in Resources */, 03AA7A9824C84C6300A47970 /* smoke.pdf in Resources */, 0300B84E24D1FD24006132E5 /* FirstSetupWindowController.xib in Resources */, 0319461224D1A09F00F37B35 /* icon-320@2x.png in Resources */, 03AA7ABF24C84C6400A47970 /* cloud.sleet.pdf in Resources */, 03977F11250E6918008FBAFD /* he.json in Resources */, 031945F824CCC48C00F37B35 /* CreditsViewController.xib in Resources */, 03933B9024C3986800A98D94 /* SourcesViewController.xib in Resources */, 0300B82D24D1B536006132E5 /* location.pdf in Resources */, 037772EA24E44BB100D81EEA /* xmark.circle.pdf in Resources */, F0A3E0A82884683D005E8D8D /* CompanionCacheViewController.xib in Resources */, 0300B83624D1B536006132E5 /* star.fill.pdf in Resources */, 03977EFF250E6918008FBAFD /* pl.json in Resources */, 03AA7A9B24C84C6300A47970 /* cloud.snow.pdf in Resources */, 0395835621807D1F008E8F9C /* thumbnail.png in Resources */, 0300B84224D1B536006132E5 /* checkmark.circle.fill.pdf in Resources */, 0300B83F24D1B536006132E5 /* dial.pdf in Resources */, 03E3C1E0256AAAF6000A2A5B /* ru.json in Resources */, 0313F9E92294337F00B074BB /* CustomVideos.xib in Resources */, 03977F05250E6918008FBAFD /* fr.json in Resources */, 0378986324C35F8A009B9418 /* CacheViewController.xib in Resources */, 0321A54024D4515B004F1975 /* folder.pdf in Resources */, 03977F1D250E6918008FBAFD /* nl.json in Resources */, 0300B7F124D1B536006132E5 /* helm.pdf in Resources */, 037772FE24E4668600D81EEA /* aspectratio.pdf in Resources */, 03AA27BF2614A6F800A4D2CF /* ko.json in Resources */, 0300B80F24D1B536006132E5 /* info.circle.pdf in Resources */, 03977EF3250E6918008FBAFD /* sv.json in Resources */, 03AA7A6E24C84C6300A47970 /* battery.0.pdf in Resources */, 0300B7F424D1B536006132E5 /* gear.pdf in Resources */, 0319460024CCC52600F37B35 /* HelpViewController.xib in Resources */, 03AA7AAA24C84C6300A47970 /* sun.haze.pdf in Resources */, 03977F17250E6918008FBAFD /* zh_CN.json in Resources */, 03AA7A9224C84C6300A47970 /* moon.pdf in Resources */, 03B8743724E42675008E3D1B /* wand.and.stars.pdf in Resources */, 0300B83924D1B536006132E5 /* regular.cloud.pdf in Resources */, 034DEE3324BF1BC700A2D3CD /* PanelWindowController.xib in Resources */, 03DB9676256569F800BFCF20 /* icon-512@2x.png in Resources */, 0300B84524D1B536006132E5 /* square.and.arrow.down.pdf in Resources */, 03AA7A8924C84C6300A47970 /* cloud.fog.pdf in Resources */, 03AA7A8024C84C6300A47970 /* cloud.sun.bolt.pdf in Resources */, 0300B83324D1B536006132E5 /* sunrise.pdf in Resources */, FAC36F481BE1756D007F2A20 /* Assets.xcassets in Resources */, 03AA7AAD24C84C6300A47970 /* tornado.pdf in Resources */, 03DAD473229EC031000DA6D1 /* Readme.md in Resources */, 0300B83C24D1B536006132E5 /* regular.sun.max.pdf in Resources */, 030010A824D718D30092AE68 /* slider.horizontal.3.pdf in Resources */, 03AA7A8624C84C6300A47970 /* hurricane.pdf in Resources */, 03F752F024EEFC8500945B1D /* house.pdf in Resources */, 0300B80924D1B536006132E5 /* film.pdf in Resources */, 0329877A274687340036D898 /* NowPlayingViewController.xib in Resources */, 03977F0B250E6918008FBAFD /* pt_BR.json in Resources */, 0300B80324D1B536006132E5 /* clock.pdf in Resources */, 03F3C1BC24F2A923007733B5 /* kofi1@2x.png in Resources */, 031332A124BF3FA700C84A05 /* SidebarViewController.xib in Resources */, 03AA7A7724C84C6300A47970 /* sun.dust.pdf in Resources */, 0350CB7E279481F2005F8625 /* hu.json in Resources */, 0300B86E24D20B12006132E5 /* NextViewController.xib in Resources */, 03AA7AB924C84C6400A47970 /* bolt.fill.pdf in Resources */, 03F752DD24EED19C00945B1D /* video_inspire_italy_como_cerano-dintelvi_006.png in Resources */, 03D1E79222848F7F00D10CF7 /* screen2.jpg in Resources */, 03B8743424E42675008E3D1B /* wand.and.rays.pdf in Resources */, FAC36F4A1BE1756D007F2A20 /* MainMenu.xib in Resources */, 0300B81224D1B536006132E5 /* regular.moon.stars.pdf in Resources */, 03DB968625657AE600BFCF20 /* LogoIcon-128px@2x.png in Resources */, 030010B024D71EB20092AE68 /* FiltersViewController.xib in Resources */, 0377BF8824BA15E700C33F9F /* btn_donate.png in Resources */, 0300B7FD24D1B536006132E5 /* circle.pdf in Resources */, 03BF17A424E2F29C0080EF34 /* VideoFormatViewController.xib in Resources */, 0300B7FA24D1B536006132E5 /* text.bubble.pdf in Resources */, 03EBF0C52746A53B00EC09D1 /* PlayingCollectionViewItem.xib in Resources */, 03977EF9250E6918008FBAFD /* pt.json in Resources */, 03AA7A6224C84C6300A47970 /* wind.snow.pdf in Resources */, 03EED2F524C44A7900F0C3D4 /* OverlaysViewController.xib in Resources */, 0300B82424D1B536006132E5 /* regular.sun.min.pdf in Resources */, 0300B81B24D1B536006132E5 /* tram.fill.pdf in Resources */, 03AA7AB324C84C6400A47970 /* cloud.moon.bolt.pdf in Resources */, 0300B80624D1B536006132E5 /* sparkles.pdf in Resources */, 03075EB624ED794F00FDBE48 /* textformat.alt.pdf in Resources */, 03F752D924EED19C00945B1D /* video_inspire_california_big-sur_2020_00001.png in Resources */, 0300B82A24D1B536006132E5 /* arrow.down.circle.fill.pdf in Resources */, 03F752E624EED21100945B1D /* video_inspire_california_vineyard_sierra-mar_sunrise_110.png in Resources */, 03AA7A5F24C84C6300A47970 /* cloud.bolt.rain.pdf in Resources */, 03AA7A7A24C84C6300A47970 /* sun.max.pdf in Resources */, 03A6D15125F294C900960135 /* location.north.pdf in Resources */, 0300B7EE24D1B536006132E5 /* bubble.left.and.bubble.right.pdf in Resources */, 03AA7A8C24C84C6300A47970 /* thermometer.sun.pdf in Resources */, 03977F02250E6918008FBAFD /* de.json in Resources */, 03AA7AA724C84C6300A47970 /* cloud.rain.pdf in Resources */, 03B8742724E41CF8008E3D1B /* CacheSetupViewController.xib in Resources */, 03AA7A9E24C84C6300A47970 /* cloud.moon.rain.pdf in Resources */, 03AA7ABC24C84C6400A47970 /* cloud.sun.pdf in Resources */, 03977EF6250E6918008FBAFD /* ja.json in Resources */, 0395835421807D1F008E8F9C /* thumbnail@2x.png in Resources */, 03AA7A6524C84C6300A47970 /* snow.pdf in Resources */, 0300B86624D2052B006132E5 /* WelcomeViewController.xib in Resources */, 03A42A9A2449F5EA003B3012 /* white_retina.png in Resources */, 0345CFF224BF43280001045C /* VideosViewController.xib in Resources */, 032E09A324C3897E00387230 /* AdvancedViewController.xib in Resources */, 03F752D524EED19C00945B1D /* video_inspire_alabama_montgomery_countryside_00007.png in Resources */, 03AA7AA424C84C6300A47970 /* cloud.drizzle.pdf in Resources */, 03F752E124EED19C00945B1D /* video_inspire_california_catalina_00005.png in Resources */, 03075EB324ED794F00FDBE48 /* trash.pdf in Resources */, 03BDBEA724C467EC00BBD5E9 /* BrightnessViewController.xib in Resources */, 03AA7AB624C84C6400A47970 /* cloud.bolt.pdf in Resources */, 03D1E79822848F7F00D10CF7 /* screen0.jpg in Resources */, 0300B82124D1B536006132E5 /* flame.pdf in Resources */, 037772E624E43AF300D81EEA /* TimeSetupViewController.xib in Resources */, 03AA7A9524C84C6300A47970 /* sun.min.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; FA71996C1D94EC5A00FBC99B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 03F752DA24EED19C00945B1D /* video_inspire_california_big-sur_2020_00001.png in Resources */, 03D1E79322848F7F00D10CF7 /* screen2.jpg in Resources */, 03F752E724EED21100945B1D /* video_inspire_california_vineyard_sierra-mar_sunrise_110.png in Resources */, 03F752D624EED19C00945B1D /* video_inspire_alabama_montgomery_countryside_00007.png in Resources */, 03F752E224EED19C00945B1D /* video_inspire_california_catalina_00005.png in Resources */, 03D1E79622848F7F00D10CF7 /* screen1.jpg in Resources */, 03F752DE24EED19C00945B1D /* video_inspire_italy_como_cerano-dintelvi_006.png in Resources */, 03F752EC24EEFC3100945B1D /* text.cursor.pdf in Resources */, 03D1E79922848F7F00D10CF7 /* screen0.jpg in Resources */, 03F752F124EEFC8500945B1D /* house.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; FACAF1A31BD9FC6000E539DC /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 03977F18250E6918008FBAFD /* ar.json in Resources */, 03AA7A9324C84C6300A47970 /* sun.min.pdf in Resources */, 03AA7A7E24C84C6300A47970 /* cloud.sun.bolt.pdf in Resources */, 03977F0C250E6918008FBAFD /* it.json in Resources */, 03AA7AB424C84C6400A47970 /* cloud.bolt.pdf in Resources */, 03AA7A7B24C84C6300A47970 /* cloud.pdf in Resources */, 0300B83424D1B536006132E5 /* star.fill.pdf in Resources */, 03977F09250E6918008FBAFD /* pt_BR.json in Resources */, 0300B86C24D20B12006132E5 /* NextViewController.xib in Resources */, 03298778274687340036D898 /* NowPlayingViewController.xib in Resources */, 0345CFF024BF43280001045C /* VideosViewController.xib in Resources */, 0300B81C24D1B536006132E5 /* arrow.down.circle.pdf in Resources */, 0300B81624D1B536006132E5 /* sunset.pdf in Resources */, 03F752EE24EEFC8500945B1D /* house.pdf in Resources */, 0300B7F824D1B536006132E5 /* text.bubble.pdf in Resources */, 032E09A124C3897E00387230 /* AdvancedViewController.xib in Resources */, 0300B82224D1B536006132E5 /* regular.sun.min.pdf in Resources */, 0321A53E24D4515B004F1975 /* folder.pdf in Resources */, 0300B82824D1B536006132E5 /* arrow.down.circle.fill.pdf in Resources */, F06FCF1A28DB8673007558BA /* tl.json in Resources */, 037772F024E44CE100D81EEA /* RecapViewController.xib in Resources */, 03977F0F250E6918008FBAFD /* he.json in Resources */, 03A42A992449F5EA003B3012 /* white_retina.png in Resources */, 037772FC24E4668600D81EEA /* aspectratio.pdf in Resources */, 03AA7A8124C84C6300A47970 /* thermometer.snowflake.pdf in Resources */, 03AA27BD2614A6F800A4D2CF /* ko.json in Resources */, 03DB968425657AE600BFCF20 /* LogoIcon-128px@2x.png in Resources */, 03AA7A6F24C84C6300A47970 /* wrench.pdf in Resources */, 03AA7A9024C84C6300A47970 /* moon.pdf in Resources */, 0300B81024D1B536006132E5 /* regular.moon.stars.pdf in Resources */, 03AA7AA524C84C6300A47970 /* cloud.rain.pdf in Resources */, 03F752DF24EED19C00945B1D /* video_inspire_california_catalina_00005.png in Resources */, 03AA7A9624C84C6300A47970 /* smoke.pdf in Resources */, 03AA7A8724C84C6300A47970 /* cloud.fog.pdf in Resources */, 030010A624D718D30092AE68 /* slider.horizontal.3.pdf in Resources */, 0300B82B24D1B536006132E5 /* location.pdf in Resources */, 0300B84C24D1FD24006132E5 /* FirstSetupWindowController.xib in Resources */, 03977F06250E6918008FBAFD /* es.json in Resources */, 03AA7A6C24C84C6300A47970 /* battery.0.pdf in Resources */, 03977F00250E6918008FBAFD /* de.json in Resources */, 0300B82524D1B536006132E5 /* mappin.and.ellipse.pdf in Resources */, 0300B80A24D1B536006132E5 /* eye.slash.pdf in Resources */, 0350CB7C279481F2005F8625 /* hu.json in Resources */, 03D1E79122848F7F00D10CF7 /* screen2.jpg in Resources */, 03B8743224E42675008E3D1B /* wand.and.rays.pdf in Resources */, 0319461024D1A09F00F37B35 /* icon-320@2x.png in Resources */, 03AA7ABD24C84C6400A47970 /* cloud.sleet.pdf in Resources */, 03977EFA250E6918008FBAFD /* zh_TW.json in Resources */, 03BDBEC324C4727C00BBD5E9 /* TimeViewController.xib in Resources */, 03AA7A8A24C84C6300A47970 /* thermometer.sun.pdf in Resources */, 0300B83D24D1B536006132E5 /* dial.pdf in Resources */, 0300B83124D1B536006132E5 /* sunrise.pdf in Resources */, 03F752E424EED21100945B1D /* video_inspire_california_vineyard_sierra-mar_sunrise_110.png in Resources */, 0300B80724D1B536006132E5 /* film.pdf in Resources */, 0300B83A24D1B536006132E5 /* regular.sun.max.pdf in Resources */, 0300B7FE24D1B536006132E5 /* star.pdf in Resources */, 03AA7A7524C84C6300A47970 /* sun.dust.pdf in Resources */, 03B8742524E41CF8008E3D1B /* CacheSetupViewController.xib in Resources */, 0377BF8624BA15E700C33F9F /* btn_donate.png in Resources */, 037772E424E43AF300D81EEA /* TimeSetupViewController.xib in Resources */, 037772E824E44BB100D81EEA /* xmark.circle.pdf in Resources */, 0300B82E24D1B536006132E5 /* tv.pdf in Resources */, 03977EF7250E6918008FBAFD /* pt.json in Resources */, 0300B81324D1B536006132E5 /* antenna.radiowaves.left.and.right.pdf in Resources */, 03075EB124ED794F00FDBE48 /* trash.pdf in Resources */, 03AA7ABA24C84C6400A47970 /* cloud.sun.pdf in Resources */, 03D1E79722848F7F00D10CF7 /* screen0.jpg in Resources */, 0300B7F524D1B536006132E5 /* eye.pdf in Resources */, 03AA7AB724C84C6400A47970 /* bolt.fill.pdf in Resources */, 03AA7A9C24C84C6300A47970 /* cloud.moon.rain.pdf in Resources */, 0378986124C35F8A009B9418 /* CacheViewController.xib in Resources */, 03F3C1BA24F2A923007733B5 /* kofi1@2x.png in Resources */, 03977EF1250E6918008FBAFD /* sv.json in Resources */, 03977F15250E6918008FBAFD /* zh_CN.json in Resources */, 030010AE24D71EB20092AE68 /* FiltersViewController.xib in Resources */, 03AA7A7224C84C6300A47970 /* cloud.sun.rain.pdf in Resources */, 03977EF4250E6918008FBAFD /* ja.json in Resources */, 0395835321807D1F008E8F9C /* thumbnail@2x.png in Resources */, 0395835521807D1F008E8F9C /* thumbnail.png in Resources */, 03F752D724EED19C00945B1D /* video_inspire_california_big-sur_2020_00001.png in Resources */, 0300B86424D2052B006132E5 /* WelcomeViewController.xib in Resources */, 03AA7A6924C84C6300A47970 /* cloud.heavyrain.pdf in Resources */, 03AA7A9924C84C6300A47970 /* cloud.snow.pdf in Resources */, 0300B80124D1B536006132E5 /* clock.pdf in Resources */, 0300B81924D1B536006132E5 /* tram.fill.pdf in Resources */, 0300B81F24D1B536006132E5 /* flame.pdf in Resources */, 03977EFD250E6918008FBAFD /* pl.json in Resources */, 03B8743824E42675008E3D1B /* hand.raised.pdf in Resources */, 03AA7AA824C84C6300A47970 /* sun.haze.pdf in Resources */, 0300B7E924D1B536006132E5 /* person.3.pdf in Resources */, 03F752DB24EED19C00945B1D /* video_inspire_italy_como_cerano-dintelvi_006.png in Resources */, 0313329F24BF3FA700C84A05 /* SidebarViewController.xib in Resources */, 031945FE24CCC52600F37B35 /* HelpViewController.xib in Resources */, 0300B7EF24D1B536006132E5 /* helm.pdf in Resources */, 03AA7A8D24C84C6300A47970 /* tropicalstorm.pdf in Resources */, 0345EDEB24C3239400C73038 /* DisplaysViewController.xib in Resources */, 03D1E79422848F7F00D10CF7 /* screen1.jpg in Resources */, 03977F1B250E6918008FBAFD /* nl.json in Resources */, 0300B7EC24D1B536006132E5 /* bubble.left.and.bubble.right.pdf in Resources */, 03AA7AA224C84C6300A47970 /* cloud.drizzle.pdf in Resources */, 03977F03250E6918008FBAFD /* fr.json in Resources */, 03AA7AB124C84C6400A47970 /* cloud.moon.bolt.pdf in Resources */, 0300B7FB24D1B536006132E5 /* circle.pdf in Resources */, 0300B80424D1B536006132E5 /* sparkles.pdf in Resources */, 03E3C1DE256AAAF6000A2A5B /* ru.json in Resources */, 03B8743524E42675008E3D1B /* wand.and.stars.pdf in Resources */, 03BDBEA524C467EC00BBD5E9 /* BrightnessViewController.xib in Resources */, 03AA7AC024C84C6400A47970 /* wind.pdf in Resources */, 0369985D2196103300E359D3 /* missingvideos.json in Resources */, 034DEE3124BF1BC700A2D3CD /* PanelWindowController.xib in Resources */, 03AA7AAB24C84C6300A47970 /* tornado.pdf in Resources */, 0300B7F224D1B536006132E5 /* gear.pdf in Resources */, 03AA7A5D24C84C6300A47970 /* cloud.bolt.rain.pdf in Resources */, 0300B83724D1B536006132E5 /* regular.cloud.pdf in Resources */, 03AA7A8424C84C6300A47970 /* hurricane.pdf in Resources */, 0343E9F92630590A00AC702F /* openweather_logo.png in Resources */, 0300B80D24D1B536006132E5 /* info.circle.pdf in Resources */, 03AA7A6324C84C6300A47970 /* snow.pdf in Resources */, 0313F9E622942AA500B074BB /* CustomVideos.xib in Resources */, 03AA7A9F24C84C6300A47970 /* cloud.hail.pdf in Resources */, 03BF17A224E2F29C0080EF34 /* VideoFormatViewController.xib in Resources */, 03F752D324EED19C00945B1D /* video_inspire_alabama_montgomery_countryside_00007.png in Resources */, 03DB9674256569F800BFCF20 /* icon-512@2x.png in Resources */, 03EED2F324C44A7900F0C3D4 /* OverlaysViewController.xib in Resources */, 0300B84024D1B536006132E5 /* checkmark.circle.fill.pdf in Resources */, 031945F624CCC48C00F37B35 /* CreditsViewController.xib in Resources */, 03075EB424ED794F00FDBE48 /* textformat.alt.pdf in Resources */, 03AA7AAE24C84C6300A47970 /* cloud.moon.pdf in Resources */, 03A42A9C2449F602003B3012 /* purple_retina.png in Resources */, 03AA7A6024C84C6300A47970 /* wind.snow.pdf in Resources */, 03977F12250E6918008FBAFD /* en.json in Resources */, 03EBF0C32746A53B00EC09D1 /* PlayingCollectionViewItem.xib in Resources */, 03AA7A6624C84C6300A47970 /* moon.stars.pdf in Resources */, 0300B84324D1B536006132E5 /* square.and.arrow.down.pdf in Resources */, F0A3E0A62884683D005E8D8D /* CompanionCacheViewController.xib in Resources */, 03AA7A7824C84C6300A47970 /* sun.max.pdf in Resources */, 03933B8E24C3986800A98D94 /* SourcesViewController.xib in Resources */, 03F752E924EEFC3100945B1D /* text.cursor.pdf in Resources */, 03A6D14F25F294C900960135 /* location.north.pdf in Resources */, 033811B324C1E243002E23E0 /* InfoViewController.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 0396D50824B8B7ED00CC021B /* Run Script - Swiftlint */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script - Swiftlint"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "#if which swiftlint >/dev/null; then\n# if [ -z \"$CI\" ]; then\n# make --directory=${SRCROOT} xcode-lint\n# fi\n#else\n# echo \"warning: SwiftLint not installed, install using `brew install swiftlint`\"\n#fi\nif which swiftlint >/dev/null; then\n swiftlint autocorrect\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; 7E9B50FB2187D302002895ED /* Run Script - Swiftlint */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script - Swiftlint"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "#if which swiftlint >/dev/null; then\n# if [ -z \"$CI\" ]; then\n# make --directory=${SRCROOT} xcode-lint\n# fi\n#else\n# echo \"warning: SwiftLint not installed, install using `brew install swiftlint`\"\n#fi\n#if which swiftlint >/dev/null; then\n# swiftlint autocorrect\n#else\n# echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\n#fi\n"; }; FA74B8481D94DCE0004FE056 /* Run Script - Swiftlint */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script - Swiftlint"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "#if which swiftlint >/dev/null; then\n# if [ -z \"$CI\" ]; then\n# make --directory=${SRCROOT} xcode-lint\n# fi\n#else\n# echo \"warning: SwiftLint not installed, install using `brew install swiftlint`\"\n#fi\n#if which swiftlint >/dev/null; then\n# swiftlint autocorrect\n# swiftlint\n#else\n# echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\n#fi\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 0396D50924B8B7ED00CC021B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 0396D50B24B8B7ED00CC021B /* DisplayDetection.swift in Sources */, F07221882AD4354E001F5452 /* CompanionBridge.swift in Sources */, 0396D50C24B8B7ED00CC021B /* LayerOffsets.swift in Sources */, 0396D50D24B8B7ED00CC021B /* APISecrets.swift in Sources */, 0396D50E24B8B7ED00CC021B /* NightShift.swift in Sources */, 0396D51024B8B7ED00CC021B /* CheckCellView.swift in Sources */, 0396D51124B8B7ED00CC021B /* PrefsUpdates.swift in Sources */, 0300B86224D2052B006132E5 /* WelcomeViewController.swift in Sources */, 03D3A10E24C5D7CC0091FE99 /* Thumbnails.swift in Sources */, 0396D51224B8B7ED00CC021B /* AerialView.swift in Sources */, 03BF17A024E2F29C0080EF34 /* VideoFormatViewController.swift in Sources */, 032851FC260A625100684A81 /* ForecastLayer.swift in Sources */, 0396D51324B8B7ED00CC021B /* PrefsCache.swift in Sources */, 0396D51424B8B7ED00CC021B /* AVPlayerViewExtension.swift in Sources */, 0396D51524B8B7ED00CC021B /* AsynchronousOperation.swift in Sources */, 0396D51624B8B7ED00CC021B /* LayerManager.swift in Sources */, 0396D51824B8B7ED00CC021B /* IOBridge.m in Sources */, 0396D51924B8B7ED00CC021B /* DarkMode.swift in Sources */, 03047A5524D2DD8E000EFE62 /* NSMenuItem+icons.swift in Sources */, 0300109824D6EF4C0092AE68 /* AVPlayerItem+vibrance.swift in Sources */, 0396D51A24B8B7ED00CC021B /* CustomVideoFolders.swift in Sources */, 036A7E9C26370C260019186B /* Forecast.swift in Sources */, 0396D51B24B8B7ED00CC021B /* InfoTimerView.swift in Sources */, 0396D51D24B8B7ED00CC021B /* MessageLayer.swift in Sources */, 0396D51E24B8B7ED00CC021B /* InfoCommonView.swift in Sources */, 031945FC24CCC52600F37B35 /* HelpViewController.swift in Sources */, 0396D51F24B8B7ED00CC021B /* AerialView+Brightness.swift in Sources */, 03D3A11424C5FC770091FE99 /* AspectFillNSImageView.swift in Sources */, F0058B8C28ABF69B0053699B /* SoundOutputManager+Properties.swift in Sources */, 030473CC24BCA9A40094A1A6 /* VideoViewItem.swift in Sources */, 0321A53424D44E80004F1975 /* ActionCellView.swift in Sources */, 0396D52124B8B7ED00CC021B /* Locations.swift in Sources */, 0396D52224B8B7ED00CC021B /* VideoCache.swift in Sources */, 0396D52324B8B7ED00CC021B /* BatteryIconLayer.swift in Sources */, 030010AC24D71EB20092AE68 /* FiltersViewController.swift in Sources */, 0396D52424B8B7ED00CC021B /* PrefsDisplays.swift in Sources */, 0396D52624B8B7ED00CC021B /* Cache.swift in Sources */, 0396D52824B8B7ED00CC021B /* AnimationTextLayer.swift in Sources */, 0345D00F24C07CC70001045C /* VideoCellView.swift in Sources */, 0345CFEE24BF43280001045C /* VideosViewController.swift in Sources */, F0058B8928ABF69B0053699B /* Sound.swift in Sources */, 03EBF0C12746A53B00EC09D1 /* PlayingCollectionViewItem.swift in Sources */, 0396D52924B8B7ED00CC021B /* InfoWeatherView.swift in Sources */, 0396D52B24B8B7ED00CC021B /* SeededGenerator.swift in Sources */, 0396D52C24B8B7ED00CC021B /* InfoDateView.swift in Sources */, 0345EDE924C3239400C73038 /* DisplaysViewController.swift in Sources */, 0396D52D24B8B7ED00CC021B /* ManifestLoader.swift in Sources */, 03E168172673A23900D7442D /* InfoMusicView.swift in Sources */, 037772E224E43AF300D81EEA /* TimeSetupViewController.swift in Sources */, 0396D52E24B8B7ED00CC021B /* TimerLayer.swift in Sources */, 0396D52F24B8B7ED00CC021B /* Source.swift in Sources */, 0396D53024B8B7ED00CC021B /* PrefsInfo.swift in Sources */, 0396D53124B8B7ED00CC021B /* ClockLayer.swift in Sources */, 03298776274687340036D898 /* NowPlayingViewController.swift in Sources */, 0396D53224B8B7ED00CC021B /* AerialVideo.swift in Sources */, 0396D53324B8B7ED00CC021B /* ErrorLog.swift in Sources */, 0396D53424B8B7ED00CC021B /* InfoSettingsView.swift in Sources */, 03C605DD277B45CA005CA51F /* DispatchQueue+Extension.swift in Sources */, 0396D53524B8B7ED00CC021B /* NSImage+trim.swift in Sources */, 0396D53624B8B7ED00CC021B /* Battery.swift in Sources */, 0396D53724B8B7ED00CC021B /* VideoList.swift in Sources */, 0332076E26D7C355001F9837 /* AVAsset+VideoOrientation.swift in Sources */, 0321A54524D5C863004F1975 /* NSButton+icons.swift in Sources */, 0396D53824B8B7ED00CC021B /* InfoBatteryView.swift in Sources */, 032E099F24C3897E00387230 /* AdvancedViewController.swift in Sources */, 037772EE24E44CE100D81EEA /* RecapViewController.swift in Sources */, 0396D53A24B8B7ED00CC021B /* DateLayer.swift in Sources */, F0058B8328ABF69B0053699B /* SoundOutputManager+Goodies.swift in Sources */, 0396D53B24B8B7ED00CC021B /* AerialPlayerItem.swift in Sources */, 0396D53C24B8B7ED00CC021B /* HardwareDetection.swift in Sources */, 03EED2F124C44A7900F0C3D4 /* OverlaysViewController.swift in Sources */, F05E805528AE8A9C0088B9C5 /* NowPlayingCollectionView.swift in Sources */, 0300B86A24D20B12006132E5 /* NextViewController.swift in Sources */, 0396D53E24B8B7ED00CC021B /* YahooLogoLayer.swift in Sources */, 0313329D24BF3FA700C84A05 /* SidebarViewController.swift in Sources */, 0345D00B24BF4E8C0001045C /* Sidebar.swift in Sources */, 0396D54024B8B7ED00CC021B /* InfoMessageView.swift in Sources */, 03977F21250E7165008FBAFD /* TimeMachine.swift in Sources */, 03BDBEA324C467EC00BBD5E9 /* BrightnessViewController.swift in Sources */, 0396D54224B8B7ED00CC021B /* ConditionLayer.swift in Sources */, 0396D54324B8B7ED00CC021B /* VideoManager.swift in Sources */, 0378985F24C35F8A009B9418 /* CacheViewController.swift in Sources */, 0396D54524B8B7ED00CC021B /* InfoContainerView.swift in Sources */, 0396D54624B8B7ED00CC021B /* AerialView+Player.swift in Sources */, 0396D54724B8B7ED00CC021B /* CustomVideoFolders+helpers.swift in Sources */, 0396D54824B8B7ED00CC021B /* VideoLoader.swift in Sources */, 03BDBEC124C4727C00BBD5E9 /* TimeViewController.swift in Sources */, 0396D54924B8B7ED00CC021B /* TimeManagement.swift in Sources */, 0396D54A24B8B7ED00CC021B /* DownloadIndicatorLayer.swift in Sources */, 0396D54B24B8B7ED00CC021B /* CountdownLayer.swift in Sources */, 031C5CB9268CA4E700CE35B4 /* ArtworkLayer.swift in Sources */, 0396D54C24B8B7ED00CC021B /* SourceList.swift in Sources */, 03B8742324E41CF8008E3D1B /* CacheSetupViewController.swift in Sources */, 0396D54D24B8B7ED00CC021B /* DescriptionCellView.swift in Sources */, 03FF1931269709AB00A0FA7F /* PlaybackSpeed.swift in Sources */, 0396D54E24B8B7ED00CC021B /* DisplayView.swift in Sources */, F0A3E0A42884683D005E8D8D /* CompanionCacheViewController.swift in Sources */, 0396D54F24B8B7ED00CC021B /* InfoLocationView.swift in Sources */, 0396D55024B8B7ED00CC021B /* AssetLoaderDelegate.swift in Sources */, 0396D55124B8B7ED00CC021B /* PrefsVideos.swift in Sources */, 0396D55224B8B7ED00CC021B /* LocationLayer.swift in Sources */, 0396D55424B8B7ED00CC021B /* InfoTableSource.swift in Sources */, F0058B8628ABF69B0053699B /* SoundOutputManager.swift in Sources */, 0396D55524B8B7ED00CC021B /* Solar.swift in Sources */, 03A6D15425F297CE00960135 /* WindDirectionLayer.swift in Sources */, 03E1681B2673A63F00D7442D /* MusicLayer.swift in Sources */, 0396D55624B8B7ED00CC021B /* Brightness.swift in Sources */, 0300B84A24D1FD24006132E5 /* FirstSetupWindowController.swift in Sources */, 0396D55724B8B7ED00CC021B /* SourceInfo.swift in Sources */, 0338119624C1D15B002E23E0 /* Aerial.swift in Sources */, 030010A324D706DB0092AE68 /* SidebarOutlineView.swift in Sources */, 0396D55924B8B7ED00CC021B /* PrefsTime.swift in Sources */, 0396D55A24B8B7ED00CC021B /* PoiStringProvider.swift in Sources */, 0396D55B24B8B7ED00CC021B /* PrefsAdvanced.swift in Sources */, 0396D55C24B8B7ED00CC021B /* WeatherLayer.swift in Sources */, 0396D55D24B8B7ED00CC021B /* VideoDownload.swift in Sources */, 0396D55E24B8B7ED00CC021B /* FileHelpers.swift in Sources */, 0396D55F24B8B7ED00CC021B /* InfoSettingsTableSource.swift in Sources */, 03F5551C24E9C091003AAD0B /* SourceOutlineView.swift in Sources */, 0396D56024B8B7ED00CC021B /* DownloadManager.swift in Sources */, 033811B124C1E243002E23E0 /* InfoViewController.swift in Sources */, 03AA32632631A8F2002198C3 /* GeoCoding.swift in Sources */, 031945F424CCC48C00F37B35 /* CreditsViewController.swift in Sources */, 0396D56224B8B7ED00CC021B /* InfoCountdownView.swift in Sources */, 0396D56324B8B7ED00CC021B /* ConditionSymbolLayer.swift in Sources */, 03A6D14725F109B900960135 /* OpenWeather.swift in Sources */, 0396D56424B8B7ED00CC021B /* CheckboxCellView.swift in Sources */, 032851F8260A4C2C00684A81 /* OneCall.swift in Sources */, 0317C19D268B65D10082A40C /* Music.swift in Sources */, 03933B8C24C3986800A98D94 /* SourcesViewController.swift in Sources */, 034DEE2F24BF1BC700A2D3CD /* PanelWindowController.swift in Sources */, 0396D56524B8B7ED00CC021B /* AnimationLayer.swift in Sources */, 0396D56624B8B7ED00CC021B /* AnimatableLayer.swift in Sources */, 0396D56724B8B7ED00CC021B /* InfoClockView.swift in Sources */, 0396D56824B8B7ED00CC021B /* CustomVideoController.swift in Sources */, 03E2237224BE048900CD8ED4 /* VideoHeaderView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; FA143CD21BDA3E880041A82B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 03B8742424E41CF8008E3D1B /* CacheSetupViewController.swift in Sources */, F07221892AD4354E001F5452 /* CompanionBridge.swift in Sources */, 03D3A11524C5FC770091FE99 /* AspectFillNSImageView.swift in Sources */, 031FB7A2248A873C0054BAFD /* PrefsCache.swift in Sources */, F00864B523AAE7F0003210EF /* DarkMode.swift in Sources */, 031945FD24CCC52600F37B35 /* HelpViewController.swift in Sources */, F0058B8D28ABF69B0053699B /* SoundOutputManager+Properties.swift in Sources */, 034A6DC624AF3EC600D62129 /* SourceList.swift in Sources */, 03FA49B82428F84500863AF6 /* DateLayer.swift in Sources */, 0361B9AC23D73D4500B6252D /* PrefsTime.swift in Sources */, 037772EF24E44CE100D81EEA /* RecapViewController.swift in Sources */, 0345D00C24BF4E8C0001045C /* Sidebar.swift in Sources */, 030010A424D706DB0092AE68 /* SidebarOutlineView.swift in Sources */, 032D1162239A7E03007E7756 /* Battery.swift in Sources */, 03893CB4217753AC008E7125 /* ErrorLog.swift in Sources */, 03C97BC124B60E2F00739CED /* FileHelpers.swift in Sources */, 03233B692172762C0077D3F9 /* PoiStringProvider.swift in Sources */, 03BF51C323A29E1D008AD373 /* ClockLayer.swift in Sources */, 03EED2F224C44A7900F0C3D4 /* OverlaysViewController.swift in Sources */, 0306336C23A143D900046A59 /* LayerOffsets.swift in Sources */, 03A2CB9D216BB1490061E8E8 /* VideoManager.swift in Sources */, 03BF17A124E2F29C0080EF34 /* VideoFormatViewController.swift in Sources */, 033811B224C1E243002E23E0 /* InfoViewController.swift in Sources */, 0354D0ED23F6CB7B00D86F9E /* InfoSettingsView.swift in Sources */, 032851F9260A4C2C00684A81 /* OneCall.swift in Sources */, F0058B8728ABF69B0053699B /* SoundOutputManager.swift in Sources */, 0321A53524D44E80004F1975 /* ActionCellView.swift in Sources */, 0345D01024C07CC70001045C /* VideoCellView.swift in Sources */, 03BDBEC224C4727C00BBD5E9 /* TimeViewController.swift in Sources */, 03E87314216760B7002B469B /* TimeManagement.swift in Sources */, 032851FD260A625100684A81 /* ForecastLayer.swift in Sources */, 038C584B23A9394000224630 /* InfoCommonView.swift in Sources */, 03A596D723AA75490097EA66 /* InfoMessageView.swift in Sources */, 03F5551D24E9C091003AAD0B /* SourceOutlineView.swift in Sources */, 03AA32642631A8F2002198C3 /* GeoCoding.swift in Sources */, 03D1E78B22844AFD00D10CF7 /* DisplayDetection.swift in Sources */, 03E8731021662AEB002B469B /* DownloadManager.swift in Sources */, 03E8731121662AEB002B469B /* AsynchronousOperation.swift in Sources */, 03E2237324BE048900CD8ED4 /* VideoHeaderView.swift in Sources */, 0313329E24BF3FA700C84A05 /* SidebarViewController.swift in Sources */, 03510C7121834FC7008F74F2 /* IOBridge.m in Sources */, 03E1681C2673A63F00D7442D /* MusicLayer.swift in Sources */, 032E09A024C3897E00387230 /* AdvancedViewController.swift in Sources */, F00864B823AAE8E9003210EF /* NightShift.swift in Sources */, 03D1E7882284367200D10CF7 /* DisplayView.swift in Sources */, 038D2EDF23B0FB0D00CD91F7 /* PrefsVideos.swift in Sources */, 03DC0065248BC60D005DB0F4 /* Cache.swift in Sources */, 038D2EE523B6565900CD91F7 /* InfoBatteryView.swift in Sources */, 03A6D15525F297CE00960135 /* WindDirectionLayer.swift in Sources */, 030D9B7C21551A8D00961E95 /* AerialPlayerItem.swift in Sources */, 030A0F2A245C7C7D009E1D97 /* BatteryIconLayer.swift in Sources */, 034587332449E22000C97D1B /* AnimationLayer.swift in Sources */, 0300B86B24D20B12006132E5 /* NextViewController.swift in Sources */, 03FA49A22423DA1F00863AF6 /* TimerLayer.swift in Sources */, 03608A2D22A56465008F08A2 /* HardwareDetection.swift in Sources */, 036A57D923F4747D0009DC02 /* InfoCountdownView.swift in Sources */, F0058B8A28ABF69B0053699B /* Sound.swift in Sources */, 03C97BC424B6210500739CED /* SourceInfo.swift in Sources */, 030473CD24BCA9A40094A1A6 /* VideoViewItem.swift in Sources */, 0378986024C35F8A009B9418 /* CacheViewController.swift in Sources */, 03C344FD24B778EE00906EA6 /* CheckboxCellView.swift in Sources */, 03D3A10F24C5D7CC0091FE99 /* Thumbnails.swift in Sources */, FAC36F5E1BE1756D007F2A20 /* CheckCellView.swift in Sources */, 034A6DC524ACC80B00D62129 /* Source.swift in Sources */, 0338119724C1D15B002E23E0 /* Aerial.swift in Sources */, FAC36F5C1BE1756D007F2A20 /* AerialView.swift in Sources */, 031C5CBA268CA4E700CE35B4 /* ArtworkLayer.swift in Sources */, FAC36F681BE1778C007F2A20 /* ManifestLoader.swift in Sources */, 03FA49B52428EE3300863AF6 /* InfoDateView.swift in Sources */, 0306336923A1026800046A59 /* LocationLayer.swift in Sources */, 034F29C023A7E58C004B34D5 /* PrefsInfo.swift in Sources */, 0385FC6D242BA097007E6513 /* InfoWeatherView.swift in Sources */, 03C3450024B7A22300906EA6 /* DescriptionCellView.swift in Sources */, 03BF51C023A275D9008AD373 /* MessageLayer.swift in Sources */, 035D5250239AA31A00DC29DC /* AerialView+Player.swift in Sources */, 0354D0EA23F6C3EE00D86F9E /* InfoSettingsTableSource.swift in Sources */, 0385FC5A242B9AE1007E6513 /* APISecrets.swift in Sources */, 0345872E2449C59900C97D1B /* AnimatableLayer.swift in Sources */, 03C605DE277B45CA005CA51F /* DispatchQueue+Extension.swift in Sources */, 0395834A217F442A008E8F9C /* Solar.swift in Sources */, FAB22A7F1BE17D7D0065C0F5 /* AssetLoaderDelegate.swift in Sources */, 0332076F26D7C355001F9837 /* AVAsset+VideoOrientation.swift in Sources */, 030010AD24D71EB20092AE68 /* FiltersViewController.swift in Sources */, FAC36F601BE175CF007F2A20 /* AppDelegate.swift in Sources */, 03C97BA824B5F74900739CED /* VideoList.swift in Sources */, 0345CFEF24BF43280001045C /* VideosViewController.swift in Sources */, 03AD460022981B1C00261325 /* CustomVideoFolders+helpers.swift in Sources */, 0361B9A923D732A300B6252D /* PrefsDisplays.swift in Sources */, 032D1165239A7F17007E7756 /* AerialView+Brightness.swift in Sources */, 031945F524CCC48C00F37B35 /* CreditsViewController.swift in Sources */, FA36BD401BE57F8E00D5E03B /* VideoDownload.swift in Sources */, 0313F9F022955F3B00B074BB /* CustomVideoFolders.swift in Sources */, 03BF51BD23A26522008AD373 /* LayerManager.swift in Sources */, 03977F22250E7165008FBAFD /* TimeMachine.swift in Sources */, 03933B8D24C3986800A98D94 /* SourcesViewController.swift in Sources */, 0321A54624D5C863004F1975 /* NSButton+icons.swift in Sources */, 0300B86324D2052B006132E5 /* WelcomeViewController.swift in Sources */, 034116D423F9BD3100CD7674 /* PrefsUpdates.swift in Sources */, 0345EDEA24C3239400C73038 /* DisplaysViewController.swift in Sources */, FAF450221BE2B45D00C1F98A /* VideoLoader.swift in Sources */, 0317C19E268B65D10082A40C /* Music.swift in Sources */, 03E168182673A23900D7442D /* InfoMusicView.swift in Sources */, F0058B8428ABF69B0053699B /* SoundOutputManager+Goodies.swift in Sources */, 036A7E9D26370C260019186B /* Forecast.swift in Sources */, 0374C9FF247AC5BC002F29D3 /* Locations.swift in Sources */, 036A57DC23F5828E0009DC02 /* CountdownLayer.swift in Sources */, 0313F9EA2294338300B074BB /* CustomVideoController.swift in Sources */, 03A6D14825F109B900960135 /* OpenWeather.swift in Sources */, FAC36F5A1BE1756D007F2A20 /* AerialVideo.swift in Sources */, 038D2EBD23AB91C300CD91F7 /* InfoLocationView.swift in Sources */, 034F29B823A7A9B3004B34D5 /* InfoTableSource.swift in Sources */, 03298777274687340036D898 /* NowPlayingViewController.swift in Sources */, 0313F9ED2294468600B074BB /* SeededGenerator.swift in Sources */, 034DEE3024BF1BC700A2D3CD /* PanelWindowController.swift in Sources */, F05E805628AE8A9C0088B9C5 /* NowPlayingCollectionView.swift in Sources */, 03A4A80F2451D04C00A1F7A3 /* PrefsAdvanced.swift in Sources */, 03FF1932269709AB00A0FA7F /* PlaybackSpeed.swift in Sources */, 03A4A80C2451CE2C00A1F7A3 /* NSImage+trim.swift in Sources */, 03FA49B22423DA7D00863AF6 /* InfoTimerView.swift in Sources */, 0300B84B24D1FD24006132E5 /* FirstSetupWindowController.swift in Sources */, 0393857B2175D4B80040B850 /* AVPlayerViewExtension.swift in Sources */, 03A42AA12449F959003B3012 /* YahooLogoLayer.swift in Sources */, 033842F824489EC600A2C523 /* WeatherLayer.swift in Sources */, 038C584823A9308C00224630 /* InfoContainerView.swift in Sources */, 03BF51BA23A24B40008AD373 /* AnimationTextLayer.swift in Sources */, F008DAFE23AADCFB00739DE1 /* Brightness.swift in Sources */, F0A3E0A52884683D005E8D8D /* CompanionCacheViewController.swift in Sources */, 03A42AA4244A0E5F003B3012 /* ConditionLayer.swift in Sources */, 03BDBEA424C467EC00BBD5E9 /* BrightnessViewController.swift in Sources */, 036A57D623F30F490009DC02 /* DownloadIndicatorLayer.swift in Sources */, 0300109924D6EF4C0092AE68 /* AVPlayerItem+vibrance.swift in Sources */, 03A596D623AA752F0097EA66 /* InfoClockView.swift in Sources */, 037772E324E43AF300D81EEA /* TimeSetupViewController.swift in Sources */, 03047A5624D2DD8E000EFE62 /* NSMenuItem+icons.swift in Sources */, 0345A24B24532E4600DD47CD /* ConditionSymbolLayer.swift in Sources */, 03EBF0C22746A53B00EC09D1 /* PlayingCollectionViewItem.swift in Sources */, FAF450251BE2D2FD00C1F98A /* VideoCache.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; FA71996A1D94EC5A00FBC99B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( FA7199711D94EC5A00FBC99B /* PreferencesTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; FACAF1A01BD9FC6000E539DC /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 03D1E78A2284471A00D10CF7 /* DisplayDetection.swift in Sources */, F07221872AD4354E001F5452 /* CompanionBridge.swift in Sources */, 0306336B23A142FA00046A59 /* LayerOffsets.swift in Sources */, 0385FC59242B9AE1007E6513 /* APISecrets.swift in Sources */, F00864B723AAE8E9003210EF /* NightShift.swift in Sources */, FAC36F5D1BE1756D007F2A20 /* CheckCellView.swift in Sources */, 034116D323F9BD3100CD7674 /* PrefsUpdates.swift in Sources */, 0300B86124D2052B006132E5 /* WelcomeViewController.swift in Sources */, 03D3A10D24C5D7CC0091FE99 /* Thumbnails.swift in Sources */, FAC36F5B1BE1756D007F2A20 /* AerialView.swift in Sources */, 03BF179F24E2F29C0080EF34 /* VideoFormatViewController.swift in Sources */, 032851FB260A625100684A81 /* ForecastLayer.swift in Sources */, 031FB78B248A87330054BAFD /* PrefsCache.swift in Sources */, 0393857A2175D4B80040B850 /* AVPlayerViewExtension.swift in Sources */, 03E8730F216501ED002B469B /* AsynchronousOperation.swift in Sources */, 03BF51BC23A2643C008AD373 /* LayerManager.swift in Sources */, 03510C6F21834F38008F74F2 /* IOBridge.m in Sources */, F00864B423AAE7F0003210EF /* DarkMode.swift in Sources */, 03047A5424D2DD8E000EFE62 /* NSMenuItem+icons.swift in Sources */, 0300109724D6EF4C0092AE68 /* AVPlayerItem+vibrance.swift in Sources */, 0313F9EF22955F3B00B074BB /* CustomVideoFolders.swift in Sources */, 036A7E9B26370C260019186B /* Forecast.swift in Sources */, 030DDA902423C3BE0072D5C9 /* InfoTimerView.swift in Sources */, 03BF51BF23A274CA008AD373 /* MessageLayer.swift in Sources */, 038C584A23A9394000224630 /* InfoCommonView.swift in Sources */, 031945FB24CCC52600F37B35 /* HelpViewController.swift in Sources */, 032D1164239A7F0C007E7756 /* AerialView+Brightness.swift in Sources */, 03D3A11324C5FC770091FE99 /* AspectFillNSImageView.swift in Sources */, F0058B8B28ABF69B0053699B /* SoundOutputManager+Properties.swift in Sources */, 030473CB24BCA9A40094A1A6 /* VideoViewItem.swift in Sources */, 0321A53324D44E80004F1975 /* ActionCellView.swift in Sources */, 0374C9FE247AC5BC002F29D3 /* Locations.swift in Sources */, FAF450241BE2D2FD00C1F98A /* VideoCache.swift in Sources */, 030A0F29245C7C7D009E1D97 /* BatteryIconLayer.swift in Sources */, 030010AB24D71EB20092AE68 /* FiltersViewController.swift in Sources */, 0361B9A823D732A300B6252D /* PrefsDisplays.swift in Sources */, 03DC004E248BC5A4005DB0F4 /* Cache.swift in Sources */, 0306336E23A15FA900046A59 /* AnimationTextLayer.swift in Sources */, 0345D00E24C07CC70001045C /* VideoCellView.swift in Sources */, 0345CFED24BF43280001045C /* VideosViewController.swift in Sources */, F0058B8828ABF69B0053699B /* Sound.swift in Sources */, 03EBF0C02746A53B00EC09D1 /* PlayingCollectionViewItem.swift in Sources */, 0385FC5D242B9F6E007E6513 /* InfoWeatherView.swift in Sources */, 0313F9EC2294468600B074BB /* SeededGenerator.swift in Sources */, 03FA49B42428EE3300863AF6 /* InfoDateView.swift in Sources */, 0345EDE824C3239400C73038 /* DisplaysViewController.swift in Sources */, FAC36F671BE1778C007F2A20 /* ManifestLoader.swift in Sources */, 03E168162673A23900D7442D /* InfoMusicView.swift in Sources */, 037772E124E43AF300D81EEA /* TimeSetupViewController.swift in Sources */, 03FA49A12423DA1F00863AF6 /* TimerLayer.swift in Sources */, 034A6DC424ACC80200D62129 /* Source.swift in Sources */, 034F29BF23A7E28E004B34D5 /* PrefsInfo.swift in Sources */, 03BF51C223A2978B008AD373 /* ClockLayer.swift in Sources */, 03298775274687340036D898 /* NowPlayingViewController.swift in Sources */, FAC36F591BE1756D007F2A20 /* AerialVideo.swift in Sources */, 03893CB3217749F0008E7125 /* ErrorLog.swift in Sources */, 0354D0EC23F6CB7B00D86F9E /* InfoSettingsView.swift in Sources */, 03C605DC277B45CA005CA51F /* DispatchQueue+Extension.swift in Sources */, 03A4A80B2451CE2C00A1F7A3 /* NSImage+trim.swift in Sources */, 032D1161239A7D82007E7756 /* Battery.swift in Sources */, 03C97BA724B5F74900739CED /* VideoList.swift in Sources */, 0332076D26D7C355001F9837 /* AVAsset+VideoOrientation.swift in Sources */, 0321A54424D5C863004F1975 /* NSButton+icons.swift in Sources */, 038D2EE423B6565900CD91F7 /* InfoBatteryView.swift in Sources */, 032E099E24C3897E00387230 /* AdvancedViewController.swift in Sources */, 037772ED24E44CE100D81EEA /* RecapViewController.swift in Sources */, 03FA49B72428F84500863AF6 /* DateLayer.swift in Sources */, F0058B8228ABF69B0053699B /* SoundOutputManager+Goodies.swift in Sources */, AA7E2E5E1FC62E8B00E5F320 /* AerialPlayerItem.swift in Sources */, 03608A2C22A56465008F08A2 /* HardwareDetection.swift in Sources */, 03EED2F024C44A7900F0C3D4 /* OverlaysViewController.swift in Sources */, F05E805428AE8A9C0088B9C5 /* NowPlayingCollectionView.swift in Sources */, 0300B86924D20B12006132E5 /* NextViewController.swift in Sources */, 03A42AA02449F959003B3012 /* YahooLogoLayer.swift in Sources */, 0313329C24BF3FA700C84A05 /* SidebarViewController.swift in Sources */, 0345D00A24BF4E8C0001045C /* Sidebar.swift in Sources */, 03A596D323AA750F0097EA66 /* InfoMessageView.swift in Sources */, 03977F20250E7165008FBAFD /* TimeMachine.swift in Sources */, 03BDBEA224C467EC00BBD5E9 /* BrightnessViewController.swift in Sources */, 03A42AA3244A0E5F003B3012 /* ConditionLayer.swift in Sources */, 03A2CB9C216BA9AF0061E8E8 /* VideoManager.swift in Sources */, 0378985E24C35F8A009B9418 /* CacheViewController.swift in Sources */, 038C584723A9304800224630 /* InfoContainerView.swift in Sources */, 035D524F239AA31A00DC29DC /* AerialView+Player.swift in Sources */, 03AD45FF22981B0C00261325 /* CustomVideoFolders+helpers.swift in Sources */, FAF450211BE2B45D00C1F98A /* VideoLoader.swift in Sources */, 03BDBEC024C4727C00BBD5E9 /* TimeViewController.swift in Sources */, 03E8731321675FE0002B469B /* TimeManagement.swift in Sources */, 036A57D523F30DD00009DC02 /* DownloadIndicatorLayer.swift in Sources */, 036A57DB23F5820A0009DC02 /* CountdownLayer.swift in Sources */, 031C5CB8268CA4E700CE35B4 /* ArtworkLayer.swift in Sources */, 034A6DC224ACC7C800D62129 /* SourceList.swift in Sources */, 03B8742224E41CF8008E3D1B /* CacheSetupViewController.swift in Sources */, 03C344FF24B7A22300906EA6 /* DescriptionCellView.swift in Sources */, 03FF1930269709AB00A0FA7F /* PlaybackSpeed.swift in Sources */, 03D1E78722842FB300D10CF7 /* DisplayView.swift in Sources */, F0A3E0A32884683D005E8D8D /* CompanionCacheViewController.swift in Sources */, 03A596D923AB8F000097EA66 /* InfoLocationView.swift in Sources */, FAB22A7E1BE17D7D0065C0F5 /* AssetLoaderDelegate.swift in Sources */, 038D2EDE23B0FB0D00CD91F7 /* PrefsVideos.swift in Sources */, 0306336823A1012200046A59 /* LocationLayer.swift in Sources */, 034F29B723A7A93D004B34D5 /* InfoTableSource.swift in Sources */, F0058B8528ABF69B0053699B /* SoundOutputManager.swift in Sources */, 03958349217F4416008E8F9C /* Solar.swift in Sources */, 03A6D15325F297CE00960135 /* WindDirectionLayer.swift in Sources */, 03E1681A2673A63F00D7442D /* MusicLayer.swift in Sources */, F008DAFD23AADCFB00739DE1 /* Brightness.swift in Sources */, 0300B84924D1FD24006132E5 /* FirstSetupWindowController.swift in Sources */, 03C97BC324B6210500739CED /* SourceInfo.swift in Sources */, 0338119524C1D15B002E23E0 /* Aerial.swift in Sources */, 030010A224D706DB0092AE68 /* SidebarOutlineView.swift in Sources */, 0361B9AB23D73D4500B6252D /* PrefsTime.swift in Sources */, 03233B68217272640077D3F9 /* PoiStringProvider.swift in Sources */, 03A4A80E2451D04C00A1F7A3 /* PrefsAdvanced.swift in Sources */, 033842E124489D7300A2C523 /* WeatherLayer.swift in Sources */, FA36BD3F1BE57F8E00D5E03B /* VideoDownload.swift in Sources */, 03C97BC024B60E2F00739CED /* FileHelpers.swift in Sources */, 0354D0E923F6C31800D86F9E /* InfoSettingsTableSource.swift in Sources */, 03F5551B24E9C091003AAD0B /* SourceOutlineView.swift in Sources */, 03E8730C2165013C002B469B /* DownloadManager.swift in Sources */, 033811B024C1E243002E23E0 /* InfoViewController.swift in Sources */, 03AA32622631A8F2002198C3 /* GeoCoding.swift in Sources */, 031945F324CCC48C00F37B35 /* CreditsViewController.swift in Sources */, 036A57D823F470940009DC02 /* InfoCountdownView.swift in Sources */, 033D68812453080C0016F837 /* ConditionSymbolLayer.swift in Sources */, 03A6D14625F109B900960135 /* OpenWeather.swift in Sources */, 03C344FC24B778EE00906EA6 /* CheckboxCellView.swift in Sources */, 032851F7260A4C2C00684A81 /* OneCall.swift in Sources */, 0317C19C268B65D10082A40C /* Music.swift in Sources */, 03933B8B24C3986800A98D94 /* SourcesViewController.swift in Sources */, 034DEE2E24BF1BC700A2D3CD /* PanelWindowController.swift in Sources */, 034587322449D8EB00C97D1B /* AnimationLayer.swift in Sources */, 0345872D2449C52F00C97D1B /* AnimatableLayer.swift in Sources */, 03A596D523AA752F0097EA66 /* InfoClockView.swift in Sources */, 0313F9E822942B4500B074BB /* CustomVideoController.swift in Sources */, 03E2237124BE048900CD8ED4 /* VideoHeaderView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ FA7199741D94EC5A00FBC99B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = FA143CD51BDA3E880041A82B /* AerialApp */; targetProxy = FA7199731D94EC5A00FBC99B /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ FAC36F331BE1756D007F2A20 /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( FAC36F341BE1756D007F2A20 /* Base */, ); name = MainMenu.xib; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 0396D5B324B8B7ED00CC021B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Developer ID Application: Guillaume Louel (3L54M5L5KK)"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 2.1.4beta1.appmode; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 3L54M5L5KK; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "Aerial copy-Info.plist"; INSTALL_PATH = "$(HOME)/Library/Screen Savers"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.13; MARKETING_VERSION = 2.1.4beta1.appmode; PRODUCT_BUNDLE_IDENTIFIER = com.johncoates.Aerial; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Aerial/Source/Models/Time/Aerial-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; WRAPPER_EXTENSION = saver; }; name = Debug; }; 0396D5B424B8B7ED00CC021B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Developer ID Application: Guillaume Louel (3L54M5L5KK)"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 2.1.4beta1.appmode; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 3L54M5L5KK; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "Aerial copy-Info.plist"; INSTALL_PATH = "$(HOME)/Library/Screen Savers"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.13; MARKETING_VERSION = 2.1.4beta1.appmode; OTHER_CODE_SIGN_FLAGS = "--timestamp"; PRODUCT_BUNDLE_IDENTIFIER = com.johncoates.Aerial; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Aerial/Source/Models/Time/Aerial-Bridging-Header.h"; SWIFT_VERSION = 5.0; WRAPPER_EXTENSION = saver; }; name = Release; }; FA143CE11BDA3E880041A82B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 2.1.4beta1.appmode; DEVELOPMENT_TEAM = 3L54M5L5KK; ENABLE_TESTABILITY = YES; INFOPLIST_FILE = Aerial/App/Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.14; MARKETING_VERSION = 2.1.4beta1.appmode; PRODUCT_BUNDLE_IDENTIFIER = "com.johncoates.Aerial-Test"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Aerial/Source/Models/Time/Aerial-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; FA143CE21BDA3E880041A82B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Mac Developer"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 2.1.4beta1.appmode; DEVELOPMENT_TEAM = 3L54M5L5KK; ENABLE_TESTABILITY = YES; INFOPLIST_FILE = Aerial/App/Resources/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.14; MARKETING_VERSION = 2.1.4beta1.appmode; PRODUCT_BUNDLE_IDENTIFIER = "com.johncoates.Aerial-Test"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = "Aerial/Source/Models/Time/Aerial-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Release; }; FA7199761D94EC5A00FBC99B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.12; PRODUCT_BUNDLE_IDENTIFIER = "com.johncoates.Aerial-Tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AerialApp.app/Contents/MacOS/AerialApp"; }; name = Debug; }; FA7199771D94EC5A00FBC99B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.12; PRODUCT_BUNDLE_IDENTIFIER = "com.johncoates.Aerial-Tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AerialApp.app/Contents/MacOS/AerialApp"; }; name = Release; }; FACAF1AD1BD9FC6000E539DC /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = "Accent Color"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_HARDENED_RUNTIME = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.12; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = NO; OTHER_CODE_SIGN_FLAGS = "--timestamp"; OTHER_SWIFT_FLAGS = "-D DEBUG -DNOSPARKLE"; SDKROOT = macosx; SWIFT_VERSION = ""; }; name = Debug; }; FACAF1AE1BD9FC6000E539DC /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = "Accent Color"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.12; MTL_ENABLE_DEBUG_INFO = NO; OTHER_CODE_SIGN_FLAGS = "--timestamp"; SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_VERSION = ""; }; name = Release; }; FACAF1B01BD9FC6000E539DC /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 3.6.1; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 3L54M5L5KK; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "$(SRCROOT)/Resources/Old stuff/Info.plist"; INSTALL_PATH = "$(HOME)/Library/Screen Savers"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.14; MARKETING_VERSION = 3.6.1; PRODUCT_BUNDLE_IDENTIFIER = com.johncoates.Aerial; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Aerial/Source/Models/Time/Aerial-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; WRAPPER_EXTENSION = saver; }; name = Debug; }; FACAF1B11BD9FC6000E539DC /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 3.6.1; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 3L54M5L5KK; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "$(SRCROOT)/Resources/Old stuff/Info.plist"; INSTALL_PATH = "$(HOME)/Library/Screen Savers"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.14; MARKETING_VERSION = 3.6.1; OTHER_CODE_SIGN_FLAGS = "--timestamp"; PRODUCT_BUNDLE_IDENTIFIER = com.johncoates.Aerial; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Aerial/Source/Models/Time/Aerial-Bridging-Header.h"; SWIFT_VERSION = 5.0; WRAPPER_EXTENSION = saver; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 0396D5B224B8B7ED00CC021B /* Build configuration list for PBXNativeTarget "Aerial (low dependencies)" */ = { isa = XCConfigurationList; buildConfigurations = ( 0396D5B324B8B7ED00CC021B /* Debug */, 0396D5B424B8B7ED00CC021B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; FA143CE01BDA3E880041A82B /* Build configuration list for PBXNativeTarget "AerialApp" */ = { isa = XCConfigurationList; buildConfigurations = ( FA143CE11BDA3E880041A82B /* Debug */, FA143CE21BDA3E880041A82B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; FA7199751D94EC5A00FBC99B /* Build configuration list for PBXNativeTarget "Aerial Tests" */ = { isa = XCConfigurationList; buildConfigurations = ( FA7199761D94EC5A00FBC99B /* Debug */, FA7199771D94EC5A00FBC99B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; FACAF19F1BD9FC6000E539DC /* Build configuration list for PBXProject "Aerial" */ = { isa = XCConfigurationList; buildConfigurations = ( FACAF1AD1BD9FC6000E539DC /* Debug */, FACAF1AE1BD9FC6000E539DC /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; FACAF1AF1BD9FC6000E539DC /* Build configuration list for PBXNativeTarget "Aerial" */ = { isa = XCConfigurationList; buildConfigurations = ( FACAF1B01BD9FC6000E539DC /* Debug */, FACAF1B11BD9FC6000E539DC /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = FACAF19C1BD9FC6000E539DC /* Project object */; } ================================================ FILE: Aerial.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Aerial.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Aerial.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ ================================================ FILE: Aerial.xcodeproj/xcshareddata/xcschemes/Aerial.xcscheme ================================================ ================================================ FILE: AerialApp copy-Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSAppTransportSecurity NSAllowsArbitraryLoads NSHumanReadableCopyright Copyright © 2015 John Coates. All rights reserved. NSLocationAlwaysUsageDescription Aerial uses location services to calculate Sunset and Sunrise times from your position NSLocationWhenInUseUsageDescription Aerial uses location services to calculate Sunset and Sunrise times from your position NSMainNibFile MainMenu NSPrincipalClass NSApplication SUFeedURL https://raw.githubusercontent.com/JohnCoates/Aerial/master/appcast.xml SUPublicEDKey fbiQGEFq55xl4bjwj2/SpIO4JMsKmEyhHEWlMMueyDY= ================================================ FILE: Documentation/AutoUpdates.md ================================================ # About auto-updates Starting with version 1.4.8, Aerial now includes the open source project [Sparkle](https://sparkle-project.org) to provide automatic updates. You can configure if and how you want this to work in the `Updates` tab: ![Capture d’écran 2019-05-30 à 11 45 55](https://user-images.githubusercontent.com/37544189/58624482-a5eb9900-82d0-11e9-8a93-0aeb71988802.jpg) What you are seeing above are the out-of-the-box default. ## Understanding the two settings Because Aerial is "just" a screen saver (technically, a plugin to System Preferences), providing updates is slightly more involved and because of this, we have two, separate, automatic update mechanisms with two separate settings to control them: - The first setting controls whether you want automatic updates or not. This check is done periodically (if 24 hours elapsed since last check), but *only* when the screen saver panel (the one you see in the screenshot) is open. When an update is available, you will see this window pop: ![Capture d’écran 2019-05-30 à 11 58 34](https://user-images.githubusercontent.com/37544189/58625280-6a51ce80-82d2-11e9-8dd0-a5ed92fa74f4.jpg) You can then decide if you want to install or not, the checkbox controls whether you want this to be done automatically for you *for this specific mechanism*. - The second setting controls whether you want Aerial to update itself while the screen saver is running. Because most people don't fiddle everyday with their screen saver settings, we've added this secondary mechanism to Aerial so everyone can stay up to date. Unlike the first mechanism above, this one is silent, and having this option enabled will automatically install the latest update without prompting you. The check is periodic (if 24 hours elapsed since last check), and done when the screen saver starts. If an update is available, the screen saver will exit, install the update, and open system preferences with the new version of Aerial. Your system will go back to sleep eventually. While we recognize that the second mechanism is highly perfectible, this is the only workaround we've found with Sparkle to provide automatic updates while Aerial runs, or without having some sort of "helper" app always running on your system to check for updates. Unless you want to manualy manage your updates, we highly recommend you keep this checked! ## Beta updates The third checkbox lets you opt-in to the beta updates. Beta releases are used to test fixes to reported issues, latest videos and new features. They are usually pretty stable. If you want those beta versions, you can enable this checkbox. Note that when a new non-beta release is available after the beta process, it will also be available in the beta track, so you are always up to date! ## What kind of network traffic does that entail? When a check happens, the auto update loads a [XML file from the GitHub repository](https://github.com/JohnCoates/Aerial/blob/master/appcast.xml) for the new updates. The updates are then downloaded from the GitHub repository's "Releases" section, the download link is included in the XML, they are always in the form of `https://github.com/JohnCoates/Aerial/releases/download/v1.5.0/Aerial.saver.zip` . While Sparkle optionally allows to [collect anonymous user data](https://sparkle-project.org/documentation/system-profiling/), we **do not** use this feature and **do not** collect any form of data whatshowever. ## Security? Each update is signed with a private EdDSA key when a release is built by the maintainer ([glouel](https://github.com/glouel). The [appcast.xml](https://github.com/JohnCoates/Aerial/blob/master/appcast.xml) provides that signature (and file size, for example for 1.4.9 : `sparkle:edSignature="5QFV0eqGRqCoZ8/TYbLXWOiVSifwNRUk4wuNFdjXJXpk/cRrceaTcs7SG168dawfOTpy9TOu283mb6WJGRQuDw==" length="5674805"` ) which will be checked against the public key bundled with Aerial. If the signature doesn't match, the update won't be installed. Each `Aerial.saver` is also signed with my ([glouel](https://github.com/glouel)) Apple ID certificate, which is also [checked by Sparkle](https://github.com/sparkle-project/Sparkle/issues/1283). Starting with 1.5.1 all Aerial builds are also notarized by Apple. ## Installed for all users and password prompt If you installed the screen saver for all users the first time (instead of for your individual user), macOS prompted you at install for your administrator password. The same thing will happen for automatic updates with the two mechanisms. Because of this, with the "Auto update when screen saver runs" checked, you will get a password prompt from Aerial/Sparkle when waking up your system. This is working as intended for macOS, if you are bothered by those prompt, consider reinstalling Aerial for your user account only. If you have multiple accounts, you can still install Aerial for each account, by default each will use the same shared cache for videos (in `/Library/Caches/Aerial/`). ## Homebrew and auto-updates If you installed Aerial through Homebrew, you will get updates automatically from that channel. Note that because of the way brew cask works, it may take a few hours for the update to show, compared to the Sparkle auto-update. We recommend that you disable the built in auto-updates if you use Homebrew. ================================================ FILE: Documentation/ChangeLog.md ================================================ # Aerial change log ## [1.8.0](https://github.com/JohnCoates/Aerial/releases/tag/v1.8.0) - February 18, 2020 - New update system for macOS Catalina. Starting with version 1.8.0, Aerial will now by default notify you with a message while the screen saver runs, when a new version of Aerial is available : ![Capture d’écran 2020-02-18 à 17 57 39](https://user-images.githubusercontent.com/37544189/74758954-5858f700-5278-11ea-8e17-d034fdf57f33.jpg) You will also be notified when a new version is available in Aerial's settings, with that new mechanism that will redirect you to the new release page where you can download the new version : ![Capture d’écran 2020-02-18 à 17 59 28](https://user-images.githubusercontent.com/37544189/74759068-7f172d80-5278-11ea-99bf-08621550087b.jpg) The update check process still uses Sparkle, but Aerial is not able to auto update in macOS Catalina due to the new sandboxing restrictions. I apologize for the inconvenience. - Add new shadow controls : ![Capture d’écran 2020-02-18 à 18 06 26](https://user-images.githubusercontent.com/37544189/74759836-b3d7b480-5279-11ea-84cf-3ddbc810cbce.jpg) - Add a new Countdown information option, to either countdown to a given date, or a given time of day : ![Capture d’écran 2020-02-18 à 18 07 49](https://user-images.githubusercontent.com/37544189/74759838-b4704b00-5279-11ea-8446-9cad67da60ea.jpg) This version also fixes many issues with macOS Catalina, namely localization that always defaulted to English, due to the restrictions applied by `legacyScreenSaver.appex`'s sandboxing. Starting with 1.8.0, Aerial requires at least macOS 10.12. ## [1.7.1](https://github.com/JohnCoates/Aerial/releases/tag/v1.7.1) - February 11, 2020 - Brings back "Allow right arrow to skip" for macOS versions prior to Catalina. That feature still won't work on Catalina. - Add seamless looping if you only have one video in your playlist. - Fix "new style" settings that weren't saved immediately, causing a discrepancy if you didn't close the preferences panel before launching the screen saver (with a hot corner). ## [1.7.0](https://github.com/JohnCoates/Aerial/releases/tag/v1.7.0) - January 29, 2020 - Add support for the 11 new sea videos that were just released, including dolphins, sharks and Tahiti waves. ![Capture d’écran 2020-01-29 à 20 52 20](https://user-images.githubusercontent.com/37544189/73392796-ca25cc80-42da-11ea-913a-c4dc1f310710.jpg) - Complete rewrite of the text/animation system, you can now select which information appears on which display, and configure more precisely the position of items on screen, or the font used. ![Capture d’écran 2020-01-29 à 21 00 08](https://user-images.githubusercontent.com/37544189/73392613-731ff780-42da-11ea-8421-8149f24be6e1.jpg) This version also fixes many issues with macOS Catalina. ## [1.6.0](https://github.com/JohnCoates/Aerial/releases/tag/v1.6.0) - September 26, 2019 - Support for the 15 new videos included in tvOS 13, including the ten new underwater seascape videos and five new ISS space videos. ![Capture d’écran 2019-09-26 à 15 29 20](https://user-images.githubusercontent.com/37544189/65692345-a9224600-e072-11e9-8c60-b0e0e546ad31.jpg) - The HDR versions of videos can now be played by Aerial. This requires macOS 10.15 Catalina, please note that you will need to redownload the HDR versions of those videos. ![Capture d’écran 2019-09-26 à 15 26 34](https://user-images.githubusercontent.com/37544189/65692344-a889af80-e072-11e9-8eac-361ba1f5d980.jpg) - Improved advanced multi monitor support. Advanced users can now finely specify the distance between each of their screens in the new "Spanned" mode included in Aerial 1.5.0, in order to accomodate more complex configurations. ![Capture d’écran 2019-09-26 à 15 24 08](https://user-images.githubusercontent.com/37544189/65692342-a889af80-e072-11e9-81ad-de70c7b2f0a1.jpg) - Support for macOS 10.15 Catalina. macOS Catalina includes new restrictions on third party screen savers, which impact some of the functionnalities. We highly suggest that current and would be Catalina users check this issue for more information on those restrictions : https://github.com/JohnCoates/Aerial/issues/801 ![Capture d’écran 2019-09-26 à 15 23 45](https://user-images.githubusercontent.com/37544189/65692340-a889af80-e072-11e9-8109-cdcd8f55fe86.jpg) Aerial is also now properly signed and notarized to comply with new restrictions introduced in macOS 10.15 Catalina. This version also includes many bugfixes, including bugs regarding multi monitor setups and previous versions of macOS. ## [1.5.0](https://github.com/JohnCoates/Aerial/releases/tag/v1.5.0) - May 31, 2019 - Completely rewritten multi monitor support. You can now enable and disable individual displays in the new Display tab: ![Capture d’écran 2019-05-29 à 14 44 01](https://user-images.githubusercontent.com/37544189/58558340-d116af80-8220-11e9-9081-696d805c1e29.jpg) - New "Spanned" viewing mode. Selecting this mode will span an Aerial video on all your (selected) screens. You can even adjust margins: ![Capture d’écran 2019-05-29 à 14 43 52](https://user-images.githubusercontent.com/37544189/58558342-d116af80-8220-11e9-8bb0-8d26f1e1b6ed.jpg) - Add your own videos to Aerial using the new Custom Videos features. You can add your own videos in the new video manager (found in the menu below the video list): ![Capture d’écran 2019-05-30 à 18 01 36](https://user-images.githubusercontent.com/37544189/58646171-24fac480-8305-11e9-98fd-c9ec7ef3a64c.jpg) You can find more [information here](CustomVideos.md). - You can now remove a single video from cache by right clicking it. - Sparkle updated to 1.21.3. - And many bug fixes! ## [1.4.9](https://github.com/JohnCoates/Aerial/releases/tag/v1.4.9) - May 1, 2019 - Fix a crashing bug in 1.4.8 for homebrew users. ## [1.4.8](https://github.com/JohnCoates/Aerial/releases/tag/v1.4.8) - April 30, 2019 - Add support for the 5 new 4K videos (January 25th update). - Automatic updates through Sparkle. ![Capture d’écran 2019-04-30 à 18 31 20](https://user-images.githubusercontent.com/37544189/56977789-4afe3f00-6b76-11e9-9985-1ca1a1866d6b.jpg) - Localization for community support in Arabic, Chinese Simplified, English, French, German, Hebrew, Polish and Spanish! Thanks to all the contributors. If you want to help, check here, we very much welcome new contributions ! - You can now skip an Aerial with the right arrow key. - You can now save your favorite videos sets to enable them quickly (look for the bookmark icon below the video list). - And many bug fixes! ## [1.4.6](https://github.com/JohnCoates/Aerial/releases/tag/v1.4.6) - December 28, 2018 - **25 extra videos now available in 4K:** Following the content updates from October 30th and December 5th, Aerial now includes 70 videos, 60 of which are also available in 4K. Aerial will periodically check for new videos, you can disable this feature in the `Cache` tab. ![screen shot 2018-10-29 at 13 21 05](https://user-images.githubusercontent.com/37544189/47649972-1f76a980-db7f-11e8-910b-1d5d50931ae2.png) - **Show videos in Quicktime:** You can now right click a video to open it in Quicktime. - **Remove video duplicates:** Aerial can now cleanup your old videos (They are periodically updated to fix colors, provide longer versions of previously existing videos, or upgraded to 4K). Go to the `Advanced` Tab and either move the files away or send them to the trash to reclaim free space. The `Move old videos` button will move the video files to a directory created within the Aerial cache called `oldvideos`, which will contain a dated directory within it. You can find them at `/Users/YOURUSERNAME/Library/Caches/Aerial/oldvideos/YYYY-MM-DD` ![capture d ecran 2018-12-13 a 15 06 49](https://user-images.githubusercontent.com/37544189/49943901-60394080-fee9-11e8-93b0-3cc68087b70e.png) ## [1.4.5](https://github.com/JohnCoates/Aerial/releases/tag/v1.4.5) - November 3, 2018 - **More battery controls:** Using Aerial on a Macbook ? You can now specify a different video format on battery mode if you wish, or simply video playback using the Power Saving mode (Aerial will show a blank screen and reduce screen brightness instead of showing videos). - You can now show day/night videos based on Dark Mode. - And many bug fixes! ## [1.4.4](https://github.com/JohnCoates/Aerial/releases/tag/v1.4.4) - October 29, 2018 - New sunset/sunrise dusk/dawn calculation modes from coordinates, Aerial can gather your location using your Mac's location service (you'll be asked for permission). Includes multiple calculations modes for dusk to better suite everyone's needs ![screen shot 2018-10-29 at 13 24 46](https://user-images.githubusercontent.com/37544189/47649974-1f76a980-db7f-11e8-8339-3f0424652b8c.png) - Control brightness, Aerial can progressively dim the brightness of your screen when it plays. Includes extra options to only enable at night or on battery ![screen shot 2018-10-29 at 13 25 10](https://user-images.githubusercontent.com/37544189/47649975-200f4000-db7f-11e8-9e8b-f75c4a5ebde4.png) - Add an option to define the margins from the border where descriptions should appear, changed the default for something more sensible - And many bug fixes/ui tweaks! ## [1.4.3](https://github.com/JohnCoates/Aerial/releases/tag/v1.4.3) - October 23, 2018 - Fix a memory retain cycle while downloading or playing cached videos ## [1.4.2](https://github.com/JohnCoates/Aerial/releases/tag/v1.4.2) - October 23, 2018 - Community location description, with better descriptions on many of the older videos (english only for this version) - Updated video names - Added logging options in Advanced panel, with better error messages when something goes wrong - You can now stop video downloads - You can now disable seconds on clock - We now have a retina(ish) thumbnail in System Preferences ## [1.4.1](https://github.com/JohnCoates/Aerial/releases/tag/v1.4.1) - October 16, 2018 - Better names for the videos - New location information for "old" videos (London, SF, etc) - You can now change the font/size of the location information displayed during videos - New options for text display (custom message, same styled clock, etc) - Add a "Main display only" option for multiple monitor setups ## [1.4.0](https://github.com/JohnCoates/Aerial/releases/tag/v1.4) - October 11, 2018 - Every Aerial video: From the very first Aerials in San Francisco to the new space videos shot from the ISS! - 4K HEVC: With the launch of Apple TV 4K, many videos are now available in this format. Aerial will show you the best format available based on your preferences. - Different videos based on time: Want to see night videos at night? You can either specify your sunset or sunrise time manually, or, if your Mac is compatible with Night Shift (see here for a list of compatible Mac), get those automatically (you do not need to enable Night Shift). - Feeling Dark?: Aerial is now compatible with Dark Mode in macOS 10.14 Mojave, and can play night videos when Dark Mode is enabled. - Descriptions: Wondering where an Aerial view was shot? Aerial can now tell you as they play. - Full offline mode:: Behind a firewall? Just copy the cache folder from another Mac and you are all set. You can also disable all streaming. Better cache management: You can now cache your favorite videos individually, no need to grab them all. Or just stream them as you go, they'll get cached automatically too. ## [1.2beta5](https://github.com/JohnCoates/Aerial/releases/tag/v1.2beta5) - December 28, 2016 - Latest beta from @JohnCoates You can find more information about older versions and betas in the project [Release history](https://github.com/JohnCoates/Aerial/releases). ================================================ FILE: Documentation/Contribute.md ================================================ # Contributing to Aerial (If you want to help with translations, please check [this page here](https://github.com/JohnCoates/Aerial/blob/master/Resources/Community/Readme.md).) If you want to contribute code to Aerial, you are more than welcome! Feel free to directly submit a PR for small changes or quick bug fixes, so we can have a look. If you want to implement something more substantial, it might be a good idea to open an issue first to discuss what you want to do, so we can coordinate efforts and help you get around the existing codebase and it's various pitfalls. # Warning if you setup your repo prior to 1.7.2 Starting with version 1.7.2, we've removed the cocoapods dependency to Sparkle, and replaced it with a git submodule reference in /Extern). I strongly recomend you pull anew. If you still want to fix your existing repo I would suggest : ``` pod deintegrate git pull git submodule update --init --recursive ``` From your main repo folder. # How to compile Aerial This is the easiest way to pull Aerial. - From terminal in a suitable location, run `git clone --recurse-submodules https://github.com/JohnCoates/Aerial.git`. This will bring Aerial and it's dependencies (Sparkle). - In the future, if you wish to update Sparkle, you can run `git submodule update --init --recursive` - Open the `Aerial.xcodeproj` in Xcode - Top left of the screen, pick the "AerialApp" scheme : ![Capture d’écran 2019-06-27 à 12 56 42](https://user-images.githubusercontent.com/37544189/60261086-569e8580-98db-11e9-8fd2-e579786f628d.jpg) - Build and run. The AerialApp scheme compiles Aerial as an App, instead of a screensaver, so you can more easily test and debug your code in Xcode. Use the Aerial scheme to compile as a screensaver. If you are running into an issue, feel free to open an issue so we can assist you. ================================================ FILE: Documentation/CustomVideos.md ================================================ # Add your own videos to Aerial Starting with version 1.5.0 of Aerial, you can now add your own videos to the playlist. In order to do this, click "Custom Videos..." at the bottom of the menu: ![Capture d’écran 2019-05-24 à 17 13 22](https://user-images.githubusercontent.com/37544189/58338271-c090be80-7e47-11e9-833a-d70ada56232b.jpg) This will open the "Manage Custom Videos" window. ![Capture d’écran 2019-05-30 à 17 45 15](https://user-images.githubusercontent.com/37544189/58646170-24622e00-8305-11e9-9235-9e7960bdf95e.jpg) Click the "Add folder" at the top of the window, and point it to a folder that contains videos. Aerial will scan that folder and show you the videos it found in the left panel. For long time users, a good way to try this is your `oldvideos` folder in your Aerial cache folder. **WARNING !!!!** If you are using Catalina, because of sandboxing restrictions, Aerial will not be able to load videos from your `Documents` folder, or the `Desktop` (precisely, you will be able to add them, but as soon as you close System Preferences or launch the full screen screensaver, Aerial will not be able to open them again and you'll get an error with a Play button in the preview, and a black screen in fullscreen mode). To get it to work in Catalina, please create a folder in `/Users/Shared/`, put your videos there and add them to Aerial from there. ## Folders and files Aerial will scan your folder for video files, including subfolders. After scanning, all these video files will show up in the left column, grouped under the name of the folder you picked. ![Capture d’écran 2019-05-24 à 17 13 44](https://user-images.githubusercontent.com/37544189/58338555-36952580-7e48-11e9-8f9b-4e69a48dc11b.jpg) You can override that name here. This folder name will be used to categorize those videos in the playlist, akin to the classical "city/country" category you see for Aerial videos. If you use an existing name (for example "Los Angeles"), videos will be merged in the playlist. If you click a file, you'll get the asset editor: ![Capture d’écran 2019-05-30 à 18 01 36](https://user-images.githubusercontent.com/37544189/58646171-24fac480-8305-11e9-98fd-c9ec7ef3a64c.jpg) You can change the name of the video, whether it's a day or night video (by default every file is imported as day) and let's you add points of interests. Points of interests are the descriptions that are shown periodically on screens when videos play. The format is simple, a time in seconds, and the description you would like to appear. We highly recommend you leave at least 10 to 15 seconds between two points of interests. ![Capture d’écran 2019-05-29 à 12 52 29](https://user-images.githubusercontent.com/37544189/58552781-8478a780-8213-11e9-99bc-2b55c75b6bd3.jpg) ## How is this stored ? When you close the window, all the information will be saved in a `customvideos.json` file in your Aerial cache folder. The format is close to the tvOS12 format, and can be edited manually. ## Video formats/containers supported As of version 1.5.0, Aerial will only look for .mov or .mp4 files (if you would like to see another extension added, please let us know but keep reading first). Aerial uses Apple's AVFoundation framework to play videos. Long story short, in theory anything that QuickTime Player X can play, will work with Aerial. As of macOS Mojave, this means that some container formats such as mkv won't be supported. ================================================ FILE: Documentation/FAQs.md ================================================ # Frequently Asked Questions This guide is meant to help you get started and answer some of the most common questions. If you wish to search for a specific term, please type `Command` + `F` and search for a single word such as "favorites".
## How do I install Aerial? If this is your first time on GitHub or you are not a "power user" we reccomend the following setup: 1. Navigate to the [latest releases](https://github.com/glouel/AerialCompanion/releases) 2. Select `AerialInstaller.dmg` on the most recent release (which is on the top). Disregard the remaining files/assets. 3. Navigate to your Downloads folder (or wherever you chose to save `AerialInstaller.dmg`) 4. Double click on `AerialInstaller.dmg` 5. It will momentarily pause to open the DMG file. 6. You will then see a window that prompts you to drag `Aerial.app` to the `Applications` folder. Please do so. ![Drag and Drop](https://user-images.githubusercontent.com/18543749/90923714-24dc5e00-e3bc-11ea-9a24-a650f42ea734.gif) 7. Now double click on the `Applications` folder that you just dragged the icon to. 8. Find and double click on `Aerial.app` 9. This will launch and install Aerial. You will notice a small circle in your menubar (at the top of your screen) 10. Lastly, go to the menubar, click on the icon and select `Open screen saver settings...` Screen Saver Settings 11. Click on the "Screen Saver" tab at the top of System Preferences 12. And select Aerial on the left hand side. 13. You're all set! If you want to change your settings, simply click on `Screen Saver Options...` on the right hand side. 14. You can now eject (delete) `AerialInstaller.dmg`
## How do I change my settings? 1. Open System Preferences 2. Click on Desktop & Screen Saver in System Preferences Desktop and Screen Saver Prefs 3. Scroll down until you see "Aerial" in the left hand panel 4. Click on `Screen Saver Options` Screen Saver Options 5. Now you are ready to manage your Aerial settings!
## Videos not playing If you have a selection of videos you want to play, but they aren't playing check the following: - *Have you selected the videos you want to play?* - *Have you cached the videos you want to play?* #### Have you selected the videos you want to play? You can do this in two different ways. Either *favorite* the videos and then choose to play only your favorites, or *limit the selection* of videos that you play. #### To play only favorites: 1. Open Aerial in System Preferences 2. Favorite (by starring) the videos you want to play. You can favorite an entire selection by right clicking on the left hand panel) “Favorite 3. Click on the `On Rotation` tab in the sidebar (at the top) 4. At the top of System Preferences, click on the dropdown next the `Aerial current plays:` and select `Only Favorites`. Play Only Favorites ### To play a single category of videos: 1. Open Aerial in System Preferences 2. Click on the `On Rotation` tab in the sidebar (at the top) 3. At the top of System Preferences, click on the dropdown next to `Aerial current plays:` and select `Location`, `Time`, `Scene`, or `Source` then select the proper sub-section to play. Sub Menus ### Have you cached the videos you want to play? 1. Open Aerial in System Preferences 2. Click on the `On Rotation` tab in the sidebar (at the top) 3. Click ‘Download rotation’ on the top right (or right click on `On Rotation` in the sidebar and click `Cache missing videos` Download rotation Cache Missing Videos
## My Cache is too large/small Aerial can cache videos in two different ways: - *Manually* - *Automatically* When caching is set to **manual** you will need to download and manage your cache entirely manually. When set to **automatic**, you can set the size of your cache and Aerial will fill it with the videos you love!

Manual Cache Management

1. Open Aerial in System Preferences 2. Click on the settings tab (gear at the top of the sidebar) 3. Click on `Cache` on the sidebar 4. Uncheck 'Automatically download videos` Manual management 5. Now download any videos you want to cache manually. 6. Note that this may take up a significant amount of disk space. To delete or prune your cache, click the `Show in Finder` button and delete any videos you no longer want. ### Automatic Cache Management 1. Open Aerial in System Preferences 2. Click on the settings tab (gear at the top of the sidebar) 3. Click on `Cache` on the sidebar 4. Check 'Automatically download videos` Automatic management 5. Set your **cache limit size** - This is the *MAXIMUM* size you want your cache. If you don't want to set a maximum, move the slider all the way to the right. 6. Select how often you want to rotate your videos in your cache Rotate videos > **NOTE:** if you have fewer videos selected than your cache size limit, videos will not rotate. If your cache is larger than your limit, then over the course of the selected time period your cache will be automatically pruned until you are under your cache limit and then videos will periodically rotate)
## What video format should I select? This can be a complicated question, however we have written about it extensively [here](https://github.com/JohnCoates/Aerial/blob/master/Documentation/HardwareDecoding.md). The short answer is: - If you have an older Mac (2011-2014) you should use 1080 H.264. - If your Mac is 2015-2017 consider use of 1080 HEVC. - If you have a newer Mac (Late 2017-present) you can consider using 1080 HEVC, HDR, or, if you have more than 8GB of RAM you can give 4K a shot! > **NOTE:** 4K HDR requires the most intensive processing and requires a HDR compatible monitor. #### To change the format of your videos: *Skip to step 4 if you have not downloaded any videos yet* 1. If you have previously downloaded/cached videos please open Aerial Settings and click on "Advanced" in the sidebar. Click on "Show Log in Finder" Cache Access 2. Inside this folder, click on the `Cache` folder 3. Keep the folder, but delete all content inside. Then empty your trash. 4. Go to Aerial > Settings > Advanced 5. Select your preferred video format Screen Shot 2020-08-21 at 3 00 11 PM 6. Close and relaunch System Preferences. 7. Open Aerial Settings again and select the videos you want. Then download (cache) these videos and they will download in the selected format.

What is "Download Rotation" and should I click it?

The `Download Rotation` button is visible under the `On Rotation` tab on the left. Download Rotation Videos in Aerial only play when they are cached on your computer. This allows them to play while you are not connected to the internet and always play immediately when your screen saver starts. > Previous versions of Aerial could "stream" videos without downloading a cache, but this was replaced in 2.0 with the automatic cache management tool. The download rotation button is synonomous with saying "Cache my rotation". In other words, any videos in the `On Rotation` category on the left hand panel will begin to download to your computer when you click `Download Rotation`. > NOTE: Only videos that are cached AND that are shown in the `On Rotation` tab will play when you start your screen saver. In the event that you want to download videos from a specific category (for example if you want to select only certain videos to play) you can additionally right click on any category and select "Cache missing videos" > NOTE: If this option is not available, it is because you have already downloaded these videos! Cache Missing Videos

Where is tvOS 11 and 12?

All the videos are still available in Aerial, don't worry! Aerial 2.0 takes advantage of the latest available videos from tvOS. tvOS 11 and 12 are not shown as tvOS 13 actually contains these videos! tvOS 10 videos support 1080p quality while tvOS 13 can support 4K and 4K HDR videos. You can still play videos from tvOS 13 in 1080p as well. tvOS

I have more technical questions...

This FAQ was made largely to help those who are new to Aerial and GitHub. If you have more technical questions you need answered, please visit our Additional Documentation avaialble here ================================================ FILE: Documentation/HardwareDecoding.md ================================================ # Which format should I pick ? You have a choice of video formats, which you can set as a preference in Settings/Advanced: 1080p H264; 1080p HEVC; 1080p HDR; 4K HEVC; or 4K HDR. At install, Aerial will suggest what it thinks is the best choice for your machine, but you can override it. They're listed in rough order of increasing quality, and you can check below about any constraints your set-up may present (just in general, if you see stuttering - non-smooth videos - choose a lower format). Some users dislike HDR, and find its colors unrealistic, so it's probably safer to choose HEVC, but, if you're curious, or are the lucky owner of a Pro Display XDR, choose HDR and see what you think! # About HEVC and hardware decoding, and HDR Aerial uses Apple's [AVFoundation framework](https://developer.apple.com/documentation/avfoundation) to play the videos as your screen saver. When available, AVFoundation will use hardware decoding (from your CPU or your graphics card) to minimize the resources needed for video playback. You can find guidelines in the help button next to the `Preferred video format` setting. By default, Aerial uses 1080p H.264 videos which is the most compatible format. Please note that all 4K HEVC videos are encoded with the `Main10` profile, which may not be hardware accelerated by your machine, while some other HEVC videos (encoded in `Main` profile) will be. While we wish to provide everyone with the best setting for their machine, the GVA framework from Apple doesn't let us distinguish HEVC `Main10` profile acceleration from general HEVC acceleration. Early feedback we gathered also seems to point that on machines with multiple decoding options (Intel QuickSync and AMD UVD), QuickSync will always be preferred (even if you "force" the discrete GPU use with an external monitor or via code, as of macOS Mojave). These are our recommendations so far: - Macs older than 2011 may lack H.264 acceleration. - Macs with an Intel CPU (With iGPU) from the Sandy Bridge (2011) generation to Broadwell (Early 2015) should have H.264 hardware acceleration available. - Late 2015 and 2016 Macs (Skylake and Kaby Lake) may only have partially accelerated HEVC decoding. We recommended you stick to 1080p H.264 on laptops. You may consider the HEVC format on desktops but understand that decoding may be CPU intensive and spin up your fans. - Macs 2017 and up should have full HEVC acceleration (the 2017 12 inch Macbook being a notable exception, only having partially accelerated HEVC decoding). You can easily check for yourself what to expect by opening a video in Quicktime (Use the `Show in Finder` option in the `Cache` tab to find the cached videos). In Activity Monitor, the AV Framework GVA process is called `VTDecoderXPCService`. # About macOS versions Because we use Apple's Framework, what is supported will depend on the version of macOS you use. - You need at least macOS 10.13 to play HEVC videos - You need at least macOS 10.15 to play HDR videos ================================================ FILE: Documentation/Installation.md ================================================ # Installation, setup and uninstallation ## Installation instructions Aerial now includes an auto-update mechanism using the [Sparkle open-source project](https://github.com/sparkle-project/Sparkle) (with EdDSA signatures). You will need to download it manually the first time : ### First Installation _Rather install from Terminal? Look at the Brew Cask section below!_ 1. Quit **System Preferences**. 2. [Download the latest release of Aerial.saver.zip](https://github.com/JohnCoates/Aerial/releases/latest). Alternatively, you can try the latest beta version [following this link](https://github.com/JohnCoates/Aerial/releases). 3. Unzip the downloaded file (if you use Safari, it should already be done for you). 4. Double-click `Aerial.saver`; it will open in `System Preferences` > `Desktop & Screen Saver` and ask you if you want to install for all users or for your user only. Be aware that installing for all users will require a password at install **and each subsequent update, including auto-updates.** If you see an error message saying "This app is damaged and can't be opened, you should move it to the trash", we suggest that **you download the file with Safari**, to prevent macOS Gatekeeper from throwing that error. Note that some outdated unzip software may cause that issue too. **Important**: If you haven't quit System Preferences before installation, you will need to quit and reopen System Preferences after installation for Aerial to work correcly. This is a macOS bug. ### Brew Cask Support If you're looking to install Aerial across many systems, remotely, or simply from Terminal we recommend [Brew Cask](https://caskroom.github.io). Simply issue the following Terminal command: ```sh brew install --cask aerial ``` To upgrade Aerial, run the following Terminal command: ```sh brew upgrade --cask aerial ``` Please note that if you prefer using homebrew to update Aerial, we recommend you disable Sparkle auto updates in the `Updates`tab. **Warning** If you see that your settings aren't saved in Catalina, please check if this folder exists : You may need to create this folder manually : `~/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Preferences/ByHost/` You may need to manually create the ByHost folder manually as Catalina may not do so for you. ## Setting Aerial as Your Screen Saver 1. Open `System Preferences` -> `Desktop & Screen Saver` -> `Screen Saver` 2. Choose Aerial and click on `Screen Saver Options` to select your settings. ![screen shot 2018-10-29 at 13 17 23](https://user-images.githubusercontent.com/37544189/47649971-1f76a980-db7f-11e8-97be-d1f90b943c9d.png) ## Uninstallation There are three ways to uninstall Aerial from your Mac. However please first read the "Removing the cache" section below. - Right-click on the Aerial screen saver in `System Preferences` and select `Delete "Aerial"`. This will uninstall the screen saver automatically. - If you prefer, you can delete the files manually. macOS can store screen savers in two locations depending on your choices, `/Library/Screen Savers` (if you installed for All Users) and `/Users/YOURUSERNAME/Library/Screen Savers` (installed for your user only). Check both locations for a file called `Aerial.saver` and delete any copies you find. - If you installed Aerial using Brew Cask, then enter the following command in a Terminal window to uninstall: ```sh brew uninstall --cask aerial ``` # Removing the cache Aerial stores your videos in a local cache on your machine. It's location depends on the version of macOS you used, how you installed Aerial (for one user or multiple user) and when you first installed Aerial. You can find the location of the cache prior to uninstalling by going into Aerial's `Caches` tab. Prior to macOS Catalina (10.15), the cache for multiple user was either : - `/Library/Caches/Aerial` (long time users) - `/Library/Application Support/Aerial` (if you installed for the first time after summer 2019) Prior to macOS Catalina (10.15), the cache for a single user was either : - `~/Library/Caches/Aerial` (long time users) - `~/Library/Application Support/Aerial` (if you installed for the first time after summer 2019) Starting with macOS Catalina (10.15), each user has a cache in it's own sandbox at this location : - `~/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Application Support/Aerial` Finally, the preference file is located either at : - `~/Library/Preferences/ByHost/com.JohnCoates.Aerial.{UUID}.plist` (before Catalina) - `~/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Preferences/ByHost/com.JohnCoates.Aerial.{UUID}.plist` (starting with Catalina) ================================================ FILE: Documentation/MoreVideos.md ================================================ # Community Videos The videos below have been shared with the project by artists, so they can be enjoyed in Aerial by everyone. ## From Joshua Michaels & Hal Bergman ### Series 1

Twenty videos kindly shared by Joshua Michaels & Hal Bergman. If you enjoyed them and want to support them, please check out the packs they created specifically for Aerial below. ##### Note If you are an artist or a tourism association that want to share videos, check out this page here for more information : https://github.com/glouel/AerialCommunity # Online Sources ## From Joshua Michaels & Hal Bergman ### Note A bundle including the packs listed below is [available here](https://www.jetsoncreative.com/aerial-expansions/bundle) at a discounted price.
### Cityscapes Air

Includes 50 Aerial Cityscape videos 20-60 seconds in length each in full 4K resolution. Locations include London, Brighton, Miami, Orlando, Los Angeles, Santa Monica, San Francisco, Birmingham, Flagstaff, Tuscon, Oklahoma City, Tulsa, Portland, Memphis, El Paso, Houston, and San Antonio.
### Countryside Air

Includes 50 Aerial Countryside videos 20-60 seconds in length each in full 4K resolution. Locations include Montgomery, Glenburn, Mendocino, Gaps Crown VIneyard, Sierra Mar Vineyard, Chesterfield, Lancashire, Monsal Head, Haworth, Leeming, Wycoller, Yorkshire, Haworth, Herford, Besalu, Castellfullit, Fortia, Oberhofen, Pecos, Cocking, Little Linford, Oxenhope, Peak District, Grassington, Genola, and Chewelah.
### Naturescapes Air

Includes 50 Aerial Naturescape videos 20-60 seconds in length at full 4K resolution. Locations include Big Sur, Catalina Island, Monument Valley, Alabama Hills, Angeles Crest, Gold Lake, Lake Shasta, Shelter Cove, Sonoma, Sonora Pass, Berthound Pass, Wolf Creek Pass, Babcock Reserve & Groves, Greyton Beach, Florida Savannas & Glades, Pascagoula River, Kootenai River, Cape Lookout, Hose Rock, Lone Ranch Beach, Interlaken, Oberhofen, Bryce Canyon, Castle Valley, Eagle Canyon, Grand Staircase Escalante, Stevens Pass, and Zion. ##### Note Check out [the instructions here](https://github.com/glouel/AerialCommunity/blob/master/CreatingASource.md) on how to create your own video sources. And drop us a line if you want to be featured here! # Local sources Aerial can also play videos that are on your machine. Rule of thumb is, if QuickTime can play it, Aerial can. There are a few complexities surrounding Catalina and Big Sur, because of sandboxing changes and security improvements in macOS. As a result, you cannot add videos from the "likely" location you'd want to use (Downloads, Desktop, Documents) and it's highly recommended to use a "safe location" like `/Users/Shared/`. Local sources are not supported in the current 2.0.0 build but will be back soon. In the meantime, check [those instructions, for use in version 1.9.2](https://github.com/JohnCoates/Aerial/blob/master/Documentation/CustomVideos.md). ================================================ FILE: Documentation/OfflineMode.md ================================================ # Offline Mode If you want to use Aerial on a Mac behind a firewall or with no network access, the easiest way starting is to copy the content of the cache folder from another Mac where Aerial is already installed. If that's not an option, you can manually recreate a cache folder by downloading files manually. This is how to download ALL videos. # Automated version (needs a terminal) Assuming you are using a Mac and have homebrew installed (if not use your package manager of choice on your system), install wget and jq : ``` brew install jq brew install wget ``` Then it's super easy, move to the location where files should be downloaded : `cd ~/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Application Support/Aerial/` Then simply do, for 4K SDR videos from tvOS12 : `cat entries.json | jq -r '.assets[]."url-4K-SDR"' | xargs wget -nc -i` (this will obviously take a while 😅) Replace `url-4K-SDR` with `url-1080-H264` or `url-1080-SDR` for 1080p H264 (default) or 1080p HEVC (and `url-4K-HDR` for 4K HDR). # Manual version We recommend you start with the first one, consider the others optional : - Download and untar `https://sylvan.apple.com/Aerials/resources-16.tar` (tvOS16 resources), unzip the `.tar` file and find the `entries.json` file. - Optionnally, also download and untar `https://sylvan.apple.com/Aerials/resources.tar` (tvOS12 resources), rename the bundle to `TVIdleScreenStrings12.bundle` and the JSON to `tvos12.json`. - Optionnally, also download and rename `https://sylvan.apple.com/Aerials/2x/entries.json` to `tvos11.json` (tvOS11 resources, also in 4K) - Optionnally, also download and rename `http://a1.phobos.apple.com/us/r1000/000/Features/atv/AutumnResources/videos/entries.json` to `tvos10.json` (The original Aerials, in 1080p H.264 only) You can then download the videos you want from the JSON files. In the 4K JSONs, you are looking for the `url-1080-H264` (1080p H.264, most compatible format), `url-1080-SDR` (1080p HEVC, better quality, requires a recent Mac for hardware decoding) or `url-4K-SDR` (4K HEVC). If you want to download the HDR versions, you can but please note that they will only play in Catalina, and a recent/fast Mac is heavily recommended. Please try to download the videos in the order mentionned (tvOS13 first) as videos routinely gets replaced with better versions. Because you will be downloading files manually, you will end up with many duplicate versions of the same videos. You can clean them up by going into the `Advanced` tab and use the `Trash old videos` feature there. You can find more information about the process [in this issue](https://github.com/JohnCoates/Aerial/issues/781#issuecomment-493677816) ================================================ FILE: Documentation/README.md ================================================ # Welcome to Aerial's documentation This documentation is still a work in progress, if you have any further question don't hesitate to post an issue. - [Frequently Asked Questions](FAQs.md) - [Troubleshooting information](Troubleshooting.md) - [Aerial's Change log](ChangeLog.md) - [Information on compiling Aerial and contributing code](Contribute.md) - [Offline mode and no network access.](OfflineMode.md) - [HEVC, HDR and hardware decoding](HardwareDecoding.md) - [How to add your own videos to Aerial](CustomVideos.md) - [Auto-updates](AutoUpdates.md) ================================================ FILE: Documentation/Troubleshooting.md ================================================ # Troubleshooting **Are you using Little Snitch or another firewall ?** Aerial requires network access for it to work, and default Little Snitch settings may impair some or all of it's features. Usually, downloading videos will be ok with default settings but you may see issues with : - Updates/auto-updates : Aerial does periodically check for new versions using Sparkle. The feed is hosted in this github repository, but access is usually denied. You may need to explicitely allow access to "raw.githubusercontent.com". For Catalina, you'll need to create the following rule : ![Screen Shot 2020-06-05 at 12 11 52 PM](https://user-images.githubusercontent.com/28914268/83914406-056f5680-a726-11ea-905d-821ccabd0469.png) - Weather : Aerial uses Yahoo Weather's API, and requires access to it. You may need to explicitely create the following rule to get it working: ![Screen Shot 2020-06-05 at 11 49 58 AM](https://user-images.githubusercontent.com/28914268/83912708-2a15ff00-a723-11ea-844d-9f6b9b18fbe8.png) If you want to use "Color icons", you will need to setup this additional rule: ![Screen Shot 2020-06-05 at 12 12 00 PM](https://user-images.githubusercontent.com/28914268/83914411-06a08380-a726-11ea-8f03-58400ce5a4a7.png) In Catalina, Aerial (like all third party screensavers) is hosted by "legacyScreenSaver.appex". In older macOS versions, it can be hosted either by System Preferences (for the control panel) or "ScreenSaverEngine.app". ## macOS Catalina specific issues - The sandboxing restrictions make it impossible, as far as we understand, for a screensaver file to auto-update itself, as Aerial did in the past through Sparkle. Technically speaking, you will still be prompted to install new updates, and the updates will look like they install, except the installation will silently fail. You can in the meantime either : - Update manually - Consider using homebrew Another solution would be to have a separate updater app, which is something that will hopefully be provided soon. You can follow the progress on this in this issue : https://github.com/JohnCoates/Aerial/issues/909 - Custom videos location : In Catalina, while it's possible to add videos that are stored in your user's Documents or Downloads folder, these files will not playback when Aerial is running as a screensaver. This is a sandboxing restriction, we recommend that you place your videos in a "less protected" folder such as `/Users/Shared/`. - Settings aren't saved : Some users (using MDM management software and/or Homebrew) seem to have run into an issue where macOS Catalina didn't create the folder where Aerial saves its preferences. You may need to create this folder manually : `~/Library/Containers/com.apple.ScreenSaver.Engine.legacyScreenSaver/Data/Library/Preferences/ByHost/` - Some current (or wanted/upcoming) features that require specific privileges are no longer working/impossible because of restrictions, this includes `Right arrow key to skip`. ## Issues on macOS 10.14 and earlier - If you enable Weather, or Adapting videos that shows based on time, you may encounter, when exiting the screen saver, this nagging panel that doesn't disappear despite clicking Allow : ![Messages_Image3499576739](https://user-images.githubusercontent.com/37544189/90182726-b6dbd980-ddb2-11ea-9a09-c5ed3efb1b48.png) If that is the case, you can simply fix it following these instructions : + Open System Preferences and go into Security & Privacy: ![Capture d’écran 2020-08-13 à 20 51 31](https://user-images.githubusercontent.com/37544189/90182715-b4797f80-ddb2-11ea-9174-a02471505876.jpg) + Then click on the `Privacy` tab and in order, click the green, orange and red circles : ![Capture d’écran 2020-08-13 à 20 52 46](https://user-images.githubusercontent.com/37544189/90182721-b5aaac80-ddb2-11ea-9edc-b0a400159dd1.jpg) This will allow Aerial to use your location to calculate sunset and sunrise times, and if you enabled it, provide your current weather conditions. ## Very common issues/macOS bugs - "You cannot use the Aerial screen saver with this version of macOS." error, or you don't see a preview and the prefererences button is greyed out: Select Aerial, close `System Preferences` with Aerial still selected, re-open System Preferences and Aerial should now work. This is a known bug with Swift screen savers in macOS/OS X reported (a long time ago...) to Apple as [rdar://25569037](http://www.openradar.me/25569037). - Some videos may not download, or you are seeing an error with "A server with the specified hostname could not be found.". This may be an issue with Content Caching in macOS, please check that link for more details and how to fix it : https://apple.stackexchange.com/questions/354197/macos-mojave-software-update-error-a-server-with-the-specified-hostname-could/371591#371591 - Screensaver hangs at start once a day or so, or unable to quit screensaver. Users of third party firewalls like Little Snitch have reported that it may interact by either blocking by default or popping a window while Aerial tries to connect (for update or download purposes). Please either disable "Auto update while the screen saver is running" in Advanced tab, or allow the connexion in your firewall to `raw.githubusercontent.com` in order to fix the issue. Aerial uses the [Sparkle](https://sparkle-project.org) open source project to provide automatic updates. This works by accessing a file hosted in this repository that you can see here : `https://github.com/JohnCoates/Aerial/blob/master/appcast.xml` (Aerial accesses this url to be exact which is the "raw" version of the file : `https://raw.githubusercontent.com/JohnCoates/Aerial/master/appcast.xml`) - "This app is damaged and can't be opened, you should move it to the trash" when double-clicking the `Aerial.saver` file: Please see the [installation notes](Installation.md), this is a GateKeeper issue. - Chrome complains that "This download is uncommon and potentilally malicious" on very fresh releases. Google seems to flag very recent files as "uncommon" and may block the download (more info on [Google's site here](https://support.google.com/chrome/answer/6261569). After a few hours/days, this warning will disappear. More info in this [issue](https://github.com/JohnCoates/Aerial/issues/759#issuecomment-489616050). - Can't use Aerial as a login screen saver: As far as we know, using 3rd party screen savers before login is no longer possible on modern versions of macOS (probably and rightly so for security reasons). More about this [here](https://github.com/JohnCoates/Aerial/issues/571). - Videos are stuttering: There are thread general causes of stuttering + Streaming: We heavily recommend you cache your videos instead of streaming. Streaming performance may cause stuttering or hanging as this is not something that's officially supported by the servers. + HDR playback: Playback of HDR videos may cause random stuttering on some Macs, this issue has been reported on Macs with AMD graphics, and 2015 and earlier Macs with Intel graphics. + Background tasks: MacOS may start some background tasks while the screensaver is running (usually after a set amount of time, like 5 minutes). `mediaanalysisd` is known to cause issues on some machines with integrated graphics. You can find more information on how to disable it here : https://github.com/JohnCoates/Aerial/issues/882#issuecomment-552104067 ## About custom videos - After playing a video, Aerial is stuck on the last frame for a while and does not go to the next video : Please check that your video contains correct duration information. Some export tools may generate incorrect video files and Aerial will not be able to properly detect the end of the file. To fix your files, you will need to "remux" them using a tool such as Handbrake or MP4Box. ## About video caching - Change cache location : Starting with Catalina, and because of the sandboxing limitations introduced with macOS 10.15, Aerial will use two distinct folders. Because of the sandbox, Aerial can **only** write inside the sandbox. You can however still specify a secondary cache location, this is what the cache location is about. This is a read-only folder where you can move your videos if you wish. You need to do this manually, changing the cache location **will not** move your videos as Aerial cannot write outside the sandbox. Please note that locations outside the main disk (including networked and external drives) are not allowed. This, again, is a macOS 10.15 limitation that we can't workaround. - Videos keeps disappearing, Aerial may not restart once in a while: Aerial stores all it's data in a Cache folder. This cache may get deleted by some third party software trying to free disk space. If you use such a "Cleaning" tool, we recommend you set a manual folder location in the Cache tab of Aerial. For example, you can create an Aerial folder in your User folder, and point to it. This will ensure Aerial files don't get deleted. - Black screen: If you are behind a firewall (Like Little Snitch or Hands Off!) try creating exceptions for Aerial to allow it access to Apple's servers. Be sure the applications `ScreenSaverEngine.app` and `System Preferences.app` are not being blocked access to `*.phobos.apple.com`, `*.phobos.apple.com.edgesuite.net` and `sylvan.apple.com`. If that isn't an option, please look at the [Offline mode](OfflineMode.md) documentation. ## Bugs related to old versions *Tip : you can see the version number in the bottom right corner of the preference panel. If you don't see a version number, your version is SEVERELY outdated (1.2 or below)!* - "Done" button doesn't close Aerial: Please update to latest available version, this is a bug on Mojave with very old versions of Aerial (1.2 and below). - Not seeing extended descriptions: Make sure you have version 1.4.2 or above. - Can't type into text fields with macOS High Sierra/Video corruption issue on High Sierra: Please make sure you have at least version 1.4.5. - Aerial logs you out of your user account everytime it starts: This looks like a new bug with macOS 10.14.5 beta 18F108f (similar to the Video corruption issue on High Sierra above), possibly only for Macs with Intel graphics. Please update to Aerial 1.5.0. More information here : https://github.com/JohnCoates/Aerial/issues/738 ## Misc. - Brightness control does not control external displays: Aerial uses the brightness API from macOS to change the brightness of your screens. As of version 1.5.0, this does not allow us to control the brightness of external screens. - High CPU usage/fan spinning all of a sudden: If you correctly configured the preferred video format [according to your Mac](HardwareDecoding.md) and still experience high CPU usage/fan spinning all of a sudden, please look for the cause with `Activity Monitor`, you may see a `com.apple.photos.ImageConversionService` responsible for this CPU load. This is the iCloud Photos process, you can find more about [what it does here](https://support.apple.com/en-gu/HT204264) and how to pause it. ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 John Coates Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ .DEFAULT_GOAL := default XCODEBUILD := xcodebuild BUILD_FLAGS = -scheme $(SCHEME) SCHEME ?= $(TARGET) TARGET ?= AerialApp clean: $(XCODEBUILD) clean $(BUILD_FLAGS) build: clean $(XCODEBUILD) build $(BUILD_FLAGS) test: clean $(XCODEBUILD) test $(BUILD_FLAGS) -enableCodeCoverage YES test-travis: clean $(XCODEBUILD) test -workspace Aerial.xcworkspace $(BUILD_FLAGS) -enableCodeCoverage YES CODE_SIGNING_ALLOWED=NO lint: @echo SwiftLint Version: $(shell swiftlint version) @echo PWD: $(shell pwd) @swiftlint lint --reporter json --strict lint-autocorrect: swiftlint autocorrect xcode-lint: swiftlint lint --lenient default: bootstrap ================================================ FILE: Podfile ================================================ # Uncomment the next line to define a global platform for your project platform :macos, '10.9' target 'Aerial' do # Comment the next line if you're not using Swift and don't want to use dynamic frameworks use_frameworks! # Pods for Aerial pod 'Sparkle' target 'Aerial Tests' do inherit! :search_paths # Pods for testing end end target 'AerialApp' do # Comment the next line if you're not using Swift and don't want to use dynamic frameworks use_frameworks! # Pods for AerialApp pod 'Sparkle' end post_install do |installer| # Sign the Sparkle helper binaries to pass App Notarization. system("codesign --force -o runtime -s 'Developer ID Application: Guillaume Louel (3L54M5L5KK)' Pods/Sparkle/Sparkle.framework/Resources/Autoupdate.app/Contents/MacOS/Autoupdate") system("codesign --force -o runtime -s 'Developer ID Application: Guillaume Louel (3L54M5L5KK)' Pods/Sparkle/Sparkle.framework/Resources/Autoupdate.app/Contents/MacOS/fileop") end ================================================ FILE: Readme.md ================================================

# Aerial - Apple TV Aerial Views Screen Saver ![Github All Releases](https://img.shields.io/github/downloads/johncoates/aerial/total.svg?maxAge=86400) ![GitHub contributors](https://img.shields.io/github/contributors/johncoates/aerial.svg?maxAge=2592000) Aerial is a Mac screensaver (macOS 10.12 or later) based on the new Apple TV screensaver that displays the Aerial movies Apple shot over New York, San Francisco, Hawaii, China, etc. Starting with version 2.0.0, it also includes videos shared by Joshua Michaels & Hal Bergman! Aerial is completely open source, so feel free to contribute to its development. This repository is used **solely** for development. Starting with version 2.3.0, Aerial can now display current weather information *and* forecasts to your location, thanks to [OpenWeather](https://openweathermap.org). ![openweather_logo](https://user-images.githubusercontent.com/37544189/115738975-d689bf80-a38d-11eb-809b-fbb019e6ed08.png) We thank [OpenWeather](https://openweathermap.org) for their support of Open Source projects. # For downloads and instructions, please go to our new website, hosted on GitHub Page: https://aerialscreensaver.github.io > Windows user? Try [OrangeJedi/Aerial](https://github.com/OrangeJedi/Aerial)
> Linux user? Try [graysky2/xscreensaver-aerial](https://github.com/graysky2/xscreensaver-aerial/) ## About Aerial Aerial was started in 2015 by John Coates ([Twitter](https://twitter.com/JohnCoatesDev), [Email](mailto:john@johncoates.me)) Starting with version 1.4, Aerial is maintained by [Guillaume Louel](https://github.com/glouel) ([Twitter](https://twitter.com/C_Wiz)). If you are looking to support the development of Aerial, feel free to donate using the following button : [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/A0A32385Y) ## Compatibility - macOS Sierra (10.12) and above, natively compiled for Apple Silicon ## Community - **Found a bug?** Make sure you are running the latest version and please check our [FAQ](https://aerialscreensaver.github.io/faq.html) and [troubleshooting page](Documentation/Troubleshooting.md) and [our issues](https://github.com/JohnCoates/Aerial/issues), as someone may already have reported it (a beta may be available with the fix you need). Feel free to [open an issue](https://github.com/JohnCoates/Aerial/issues/new), try to be as specific as possible. - **Have you fixed a bug?** Or want to implement a feature? Check instructions on how to compile Aerial and more on [contributing here](Documentation/Contribute.md). - **Can you translate videos names and their descriptions?** Awesome! [Read here for details](Resources/Community/Readme.md) on how to help us. - **Have a feature request?** [Open an issue](https://github.com/JohnCoates/Aerial/issues/new). Tell us why it would be useful, and why you and others would want it. - **Join our [Community Discord server](https://discord.gg/TPuA5WG)** for technical support, feature requests, and a fun time! ## Multilingual Support Aerial features overlay descriptions of the main geographical features displayed in the videos. ![Community Strings example](https://user-images.githubusercontent.com/4295/52958947-75bd6180-3395-11e9-947f-3c77d9f41928.jpg) Video descriptions are available in many languages (Spanish, French, Polish… [check the complete list here](Resources/Community/Readme.md)) and that is only possible thanks to the collaboration and interested work of many volunteers. To best serve the international community we've defined a translation workflow that allows any person, even with **no technical background** to help translate the descriptions. If you want to collaborate, please [read the details here](Resources/Community/Readme.md). ## License [MIT License](https://raw.githubusercontent.com/JohnCoates/Aerial/master/LICENSE) ================================================ FILE: Resources/Community/Readme.md ================================================ # Translations of the community strings Aerial features overlay descriptions of the main geographical features displayed in its videos. This is possible thanks to the collaboration and uninterested work of many. To best serve this international community we've a translation workflow defined that allows any person, even with no technical background to help translating the descriptions. In the following sections we explain how to collaborate in the internationalization process both as a translator and as a developer. ## For translators 1. Contact [@glouel](https://github.com/glouel) or [@aitor](https://github.com/aitor) to get added to the Translators team at Lokalise. 2. Access the translations dashboard at https://lokalise.co/ and translate or update the existing strings to your language. 3. PROFIT! ## For developers The translation workflow follows a pull mode, meaning that new strings will be defined in the Lokalise service and pulled into the project once they have been translated. The pulling process will be repeated after any changes has been made to the strings. ### Setting up the project 1. Contact [@glouel](https://github.com/glouel) or [@aitor](https://github.com/aitor) to get added to the Translators team at Lokalise. 2. Use the existing configuration example file to create your local configuration `cp lokalise.example.cfg lokalise.cfg` 3. In the created file update the `Token` string with your token. To get your token visit the section `API Tokens` at https://lokalise.co/profile ### Downloading/pulling translations 1. Install Lokalise CLI: https://docs.lokalise.co/api-and-cli/lokalise-cli-tool 2. Once the CLI has been installed you can pull the latest versions of each language with the following command: `lokalise --config lokalise.cfg d --type json --dest Resources/Community --unzip_to Resources/Community` 3. Commit and push the new `*.json` files to the repo. ## Supported languages & Kudos The following people has helped to improve Aerial by providing translations and text corrections. Thanks everybody! - English by [@SeanMSmith](https://github.com/SeanMSmith) - Spanish by [@aitor](https://github.com/aitor) - French by [@glouel](https://github.com/glouel) - Polish by [@Soruk](https://github.com/Soruk) - German by [@moeffju](https://github.com/moeffju) - Hebrew by [@kaaspad](https://github.com/kaaspad) - Arabic by [@kachikulu](https://github.com/kachikulu) - Simplified Chinese by [@LinkeyLeo](https://github.com/LinkeyLeo) - Japanese by [@DrMORO617](https://github.com/DrMORO617) ## Sponsorship ![Lokalise logo](https://user-images.githubusercontent.com/4295/52958944-73f39e00-3395-11e9-9350-79bb7ee43d14.png) [Lokalise](https://lokalise.co) supports Aerial and provides us with a free plan to make the translation of this project possible. Check it out for your next i18n project! ================================================ FILE: Resources/Community/ar.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "كوريا واليابان في الليل" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "مكسيكو سيتي إلى نيويورك" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "شمال كاليفورنيا إلى ولاية باها" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "الحديقة الوطنية وليغيوان ١" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "مدرجات الأرز لونججي" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "الحديقة الوطنية وليغيوان ٢" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "سور الصين العظيم 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "سور الصين العظيم 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "سور الصين العظيم 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "طريق الشيخ زايد" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "مرسى ١" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "وسط المدينة" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "مرسى ٢" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "الإقتراب من برج خليفة" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "طريق الشيخ زايد" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "شبه الجزيرة نووسواق" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "إيسيفجورد ايلوليسات" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "إيسيفجورد ايلوليسات" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "وادي وايمانو" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "لاوبهويهوي نوي، هاواي" }, "b2-2": { "name": "وادي هونوب" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "محمية پو أو أومي الطبيعية" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "ساحل كوهالا" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "محمية پو أو أومي الطبيعية" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "ميناء فيكتوريا ١" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "جبل فيكتوريا" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "وان تشاي" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "ميناء فيكتوريا ٢" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "ميناء فيكتوريا" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "واحة ليوا ١" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "واحة ليوا ٢" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "نهر التايمز" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "قصر باكنغهام" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "نهر التايمز مع قرب غروب الشمس" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "نهر التايمز في الغسق" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "مطار لوس أنجلوس الدولي" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "طريق هاربور السريع" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "شاطئ سانتا مونيكا" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "مرصد جريفيث" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "تلال هوليوود" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "وسط المدينة" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "سنترال بارك" }, "b1-3": { "name": "مانهاتن السفلى" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "الجانب الشرقي الاعلى" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "الجادة السابعة" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "مانهاتن السفلى" }, "b8-2": { "name": "شبة الجزيرة الجبلية مارين هيدلاندز" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "مارين هيدلاندز في الضباب" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "خليج سان فرانسيسكو و جسر البوابة الذهبية" }, "b8-3": { "name": "مربع آلامو" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "امباركادرو، شارع السوق" }, "b4-3": { "name": "بريسيديو إلى جسر البوابة الذهبية" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "جسر البوابة الذهبية من سان فرانسيسكو" }, "b6-4": { "name": "وسط المدينة وبرج كولت" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "مرسى الصيادين" }, "b5-3": { "name": "امباركادرو، شارع السوق" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "جسر الخليج" }, "b2-4": { "name": "وسط المدينة وبرج سوترو" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "خليج سان فرانسيسكو و امباركاديرو" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "القارة القطبية الجنوبية" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "الشفق القطبي لأمريكا الشمالية" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "أفريقيا في الليل" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "أفريقيا والشرق الأوسط" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "كاليفورنيا إلى فيغاس" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "منطقة البحر الكاريبي" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "اليوم الكاريبي" }, "7825C73A-658F-48EE-B14C-EC56673094AC": { "name": "الصين" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "إيطاليا إلى آسيا" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "إيران وأفغانستان" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "ايرلندا إلى آسيا" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "غرب أفريقيا إلى جبال الألب" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "نيوزيلندا" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "صحارى و إيطاليا" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "المحيط الأطلسي إلى إسبانيا وفرنسا" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "استراليا" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "منطقة البحر الكاريبي إلى أمريكا الوسطى" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "دلتا النيل" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "جنوب أفريقيا إلى شمال آسيا" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "مرجان بالاو" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "عشب البحر جنوب أفريقيا" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "أسماك الباراكودا" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "نجوم البحر" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "أسماك الروؤس المنتفخة" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "أسماك الشيم الحصاني" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "دلافين كاليفورنيا" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "غابات عشب البحر كاليفورنيا" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "دلافين كوستاريكا" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "أسماك الغرابي المشعة" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "اسماك قرش الشعب الرمادية" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "الحوت الأحدب" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "مرجان البحر الأحمر" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "فقمات" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "قناديل بحر پالاو 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "قناديل بحر پالاو 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "قناديل بحر پالاو 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "قناديل بحر الآسكا 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "قناديل بحر الآسكا 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "أمواج تاهيتي 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "أمواج تاهيتي 2" } } ================================================ FILE: Resources/Community/de.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "Korea und Japan bei Nacht" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "Mexiko-Stadt nach New York" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "Nordkalifornien nach Baja" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "Wulingyuan Nationalpark 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "Longji Reisterrassen" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "Wulingyuan Nationalpark 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "Chinesische Mauer 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "Chinesische Mauer 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "Chinesische Mauer 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "Sheikh Zayed Road" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "Marina 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "Innenstadt" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "Marina 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "Anflug zum Burj Khalifa" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "Sheikh Zayed Road" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "Nuussuaq-Halbinsel" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "Ilulissat-Eisfjord" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "Ilulissat-Eisfjord" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "Waimanu-Tal" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "Laupāhoehoe Nui" }, "b2-2": { "name": "Honopū-Tal" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "Pu'u O 'Umi" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "Küste von Kohala" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "Pu'u O 'Umi" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "Victoria Harbour 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "Victoria Peak" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "Wan Chai" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "Victoria Harbour 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "Victoria Harbour" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "Liwa-Oase 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "Liwa-Oase 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "Die Themse" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "Buckingham Palace" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "Die Themse bei Sonnenuntergang" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "Die Themse in der Abenddämmerung" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "Internationaler Flughafen von Los Angeles" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "Harbor Freeway" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "Strand von Santa Monica" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "Griffith-Sternwarte" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "Hollywood Hills" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "Innenstadt" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "Central Park" }, "b1-3": { "name": "Lower Manhattan" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "Upper East Side" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "Seventh Avenue" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "Lower Manhattan" }, "b8-2": { "name": "Marin Headlands" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "Marin Headlands im Nebel" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "Bucht und Golden Gate" }, "b8-3": { "name": "Alamo Square" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "Embarcadero, Market Street" }, "b4-3": { "name": "Presidio zur Golden Gate" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "Golden Gate aus Richtung San Francisco" }, "b6-4": { "name": "Innenstadt und Coit Tower" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "Fisherman's Wharf" }, "b5-3": { "name": "Embarcadero, Market Street" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "Brücke über die San Francisco Bay" }, "b2-4": { "name": "Innenstadt und Sutro Tower" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "Bucht von San Francisco und Embarcadero" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "Antarktis" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "Nordamerikanische Aurora" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "Afrika bei Nacht" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "Afrika und der Nahe Osten" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "Kalifornien nach Vegas" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "Karibik" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "Tag in der Karibik" }, "7825C73A-658F-48EE-B14C-EC56673094AC": { "name": "China" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "Italien nach Asien" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "Iran und Afghanistan" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "Irland bis Asien" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "Westafrika bis zu den Alpen" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "Neuseeland" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "Sahara und Italien" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "Atlantik nach Spanien und Frankreich" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "Australien" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "Karibik nach Mittelamerika" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "Nildelta" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "Südafrika nach Nordasien" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "Korallen bei Palau" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "Südafrikanischer Seetang" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "Barrakudas" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "Seesterne" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "Büffelkopf-Papageifische" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "Pferdemakrelen" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "Kalifornische Delfine" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "Kalifornischer Seetangwald" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "Delfine bei Costa Rica" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "Atlantischer Kuhnasenrochen" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "Grauer Riffhai" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "Buckelwal" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "Korallen im Roten Meer" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "Robben" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "Quallen bei Palau 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "Quallen bei Palau 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "Quallen bei Palau 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "Quallen bei Alaska 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "Quallen bei Alaska 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "Wellen bei Tahiti 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "Wellen bei Tahiti 2" } } ================================================ FILE: Resources/Community/en.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "Korea and Japan Night" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "Mexico City to New York" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "Northern California to Baja" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "Wulingyuan National Park 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "Longji Rice Terraces" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "Wulingyuan National Park 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "Great Wall 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "Great Wall 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "Great Wall 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "Sheikh Zayed Road" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "Marina 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "Downtown" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "Marina 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "Approaching Burj Khalifa" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "Sheikh Zayed Road" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "Nuussuaq Peninsula" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "Ilulissat Icefjord" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "Ilulissat Icefjord" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "Waimanu Valley" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "Laupāhoehoe Nui" }, "b2-2": { "name": "Honopū Valley" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "Pu‘u O ‘Umi" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "Kohala Coastline" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "Pu‘u O ‘Umi" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "Victoria Harbour 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "Victoria Peak" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "Wan Chai" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "Victoria Harbour 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "Victoria Harbour" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "Liwa Oasis 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "Liwa Oasis 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "River Thames" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "Buckingham Palace" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "River Thames near Sunset" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "River Thames at Dusk" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "Los Angeles Int’l Airport" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "Harbor Freeway" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "Santa Monica Beach" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "Griffith Observatory" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "Hollywood Hills" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "Downtown" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "Central Park" }, "b1-3": { "name": "Lower Manhattan" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "Upper East Side" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "Seventh Avenue" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "Lower Manhattan" }, "b8-2": { "name": "Marin Headlands" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "Marin Headlands in Fog" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "Bay and Golden Gate" }, "b8-3": { "name": "Alamo Square" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "Embarcadero, Market Street" }, "b4-3": { "name": "Presidio to Golden Gate" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "Golden Gate from SF" }, "b6-4": { "name": "Downtown and Coit Tower" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "Fisherman’s Wharf" }, "b5-3": { "name": "Embarcadero, Market Street" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "Bay Bridge" }, "b2-4": { "name": "Downtown and Sutro Tower" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "Bay and Embarcadero" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "Antartica" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "North America Aurora" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "Africa Night" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "Africa and the Middle East" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "California to Vegas" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "Caribbean" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "Caribbean Day" }, "6324F6EB-E0F1-468F-AC2E-A983EBDDD53B": { "name": "China" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "Italy to Asia" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "Iran and Afghanistan" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "Ireland to Asia" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "West Africa to the Alps" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "New Zealand" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "Sahara and Italy" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "Atlantic Ocean to Spain and France" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "Australia" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "Caribbean to Central America" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "Nile Delta" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "South Africa to North Asia" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "Palau Coral" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "South African Kelp" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "Barracuda" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "Sea Stars" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "Bumpheads" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "Jacks" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "California Dolphins" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "California Kelp Forest" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "Costa Rica Dolphins" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "Cownose Rays" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "Gray Reef Sharks" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "Humpback Whale" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "Red Sea Coral" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "Seals" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "Palau Jellies 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "Palau Jellies 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "Palau Jellies 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "Alaskan Jellies 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "Alaskan Jellies 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "Tahiti Waves 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "Tahiti Waves 2" }, "E334A6D2-7145-47C8-9B00-C20DED08B2D5": { "name": "Colorado River 1" }, "DD266E1F-5DF2-4CDB-A2EB-26CE35664657": { "name": "Burnt Canyon" }, "F9F918CD-E15F-4F01-A326-84A44650C5C9": { "name": "Sunset over Grand Canyon" }, "AE0115AE-C53B-4DB9-B12F-CA4B7B630CC9": { "name": "Colorado River 2" }, "8002C4C8-C611-4894-A068-3D3A3C03472A": { "name": "Colorado River 3" }, "5C987900-AD53-469C-8210-CABBCCDDFCAE": { "name": "Cuernos del Paine" }, "B004358B-5A27-42E5-B49E-93FC100B2371": { "name": "Lago Nordenskjöld 1" }, "25A6CFB2-3570-4448-B114-244A4E454B7A": { "name": "Lago Nordenskjöld 2" }, "E5D58CC2-3C52-4206-9DA2-427DC88B5896": { "name": "Torres del Paine" }, "E5799A24-1949-4E66-A17B-B5EB05F28C5D": { "name": "Half Dome" }, "E487C6EF-B3FB-427B-A2BE-8CBA60F902F0": { "name": "Yosemite Park 1" }, "E540DEE6-4C40-42C8-9CCC-D4CB0FAD7D7B": { "name": "Yosemite Park 2" }, "81CA5ACD-E682-4D8B-A948-0F147EB6ED4F": { "name": "Yosemite Park 3" }, "4109D42A-D717-46A7-A9A2-FE53A82B25C0": { "name": "Bridalveil Fall and El Capitan" }, "DAD82DCE-F3AE-4AEC-8A79-1694D412FC0A": { "name": "Tuolumne Meadows" }, "8D04D70F-738B-441D-8D43-AF46B2BF8062": { "name": "Matthes Crest" }, "DDE50C77-B7CB-4488-9EB1-D1B13BF21FFE": { "name": "Tungnaá" }, "E54D5AFE-F362-4D48-A20D-F2C21D2B5330": { "name": "Jökulgilskvísl River" }, "8ACF5D77-B22C-416F-B12A-72FB35E2834F": { "name": "Landmannalaugar" }, "8590D0C5-E344-4FAC-A39A-FD7BC652AEDA": { "name": "Langisjór" }, "2F17FCCE-6CCA-4AFA-A08A-C50BF9812DA5": { "name": "Mýrdalsjökull" }, "F9518D54-04A7-4793-8666-CFC114D73CE5": { "name": "Jökulgil" }, "3954A7C4-51EC-4ABC-ABA3-6757AC91C7CF": { "name": "Loch Moidart 1" }, "0C747C29-4BF8-43F6-A5CC-2E012E555341": { "name": "Isle of Skye" }, "E161929C-0819-4BC2-8359-550C081C7D54": { "name": "Loch Moidart 2" } } ================================================ FILE: Resources/Community/es.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "Korea y Japon de noche" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "De Ciudad de México a Nueva York" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "Del Norte de California a Baja" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "Parque Nacional de Wulingyuan 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "Las terrazas de arroz de Longsheng" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "Parque Nacional de Wulingyuan 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "La Gran Muralla 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "La Gran Muralla 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "La Gran Muralla 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "Carretera Sheikh Zayed" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "Marina 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "Downtown" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "Marina 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "Acercándose a Burj Khalifa" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "Carretera Sheikh Zayed" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "Península de Nuussuaq" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "Fiordo de Ilulissat" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "Fiordo de Ilulissat" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "Valle Waimanu" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "Laupāhoehoe Nui" }, "b2-2": { "name": "Valle Honopū" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "Pu‘u O ‘Umi" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "La costa de Kohala" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "Pu‘u O ‘Umi" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "Puerto Victoria 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "Cumbre Victoria" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "Wan Chai" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "Puerto Victoria 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "Puerto Victoria" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "El oasis de Liwa 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "El oasis de Liwa 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "Río Támesis" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "Palacio de Buckingham" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "El río Támesis al atardecer" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "El río Támesis al anochecer" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "Aeropuerto Internacional de Los Angeles" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "Harbor Freeway" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "Playa de Santa Monica" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "Observatorio Griffith" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "Hollywood Hills" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "El Downtown de Los Ángeles" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "Central Park" }, "b1-3": { "name": "Bajo Manhattan" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "Upper East Side" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "Séptima Avenida" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "Bajo Manhattan" }, "b8-2": { "name": "Marin Headlands" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "Marin Headlands en la niebla" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "Bahía de San Francisco y el Golden Gate" }, "b8-3": { "name": "Alamo Square" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "Embarcadero, Market Street" }, "b4-3": { "name": "De Presidio al Golden Gate" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "El Golden Gate desde San Francisco" }, "b6-4": { "name": "Downtown y la Torre Coit" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "Fisherman’s Wharf" }, "b5-3": { "name": "Embarcadero, Market Street" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "Puente de la Bahía" }, "b2-4": { "name": "Downtown y la Torre Sutro" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "Bahía de San Francisco y Embarcadero" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "Antártida" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "Aurora Boreal Norteamericana" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "Noche Africana" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "Africa y Oriente Medio" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "De California a Las Vegas" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "El Caribe" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "El Caribe de dia" }, "7825C73A-658F-48EE-B14C-EC56673094AC": { "name": "China" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "De Italia a Asia" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "Irán y Afganistán" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "De Irlanda a Asia" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "De África Occidental a los Alpes" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "Nueva Zelanda" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "El Sahara e Italia" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "Del Océano Atlántico a España y Francia" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "Australia" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "Del Caribe a América Central" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "Delta del Nilo" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "De Sudáfrica al Norte de Asia" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "Corales en Palau" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "Algas en Sudáfrica" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "Barracuda" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "Estrellas de mar" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "Pez Loro Búfalo" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "Jureles" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "Delfines de California" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "Bosque de Algas de California" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "Delfines de Costa Rica" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "Mantarrayas" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "Tiburones Grises de Arrecife" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "Ballena Jorobada" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "Coral del Mar Rojo" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "Focas" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "Medusas en Palau 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "Medusas en Palau 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "Medusas en Palau 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "Medusas en Alaska 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "Medusas en Alaska 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "Olas de Tahití 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "Olas de Tahití 2" } } ================================================ FILE: Resources/Community/fr.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "Corée et Japon" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "Mexico City vers New York" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "Californie vers Baja" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "Parc National Wulingyuan 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "Rizières en terrasses de Longji" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "Parc National Wulingyuan 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "Grande Muraille 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "Grande Muraille 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "Grande Muraille 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "Route Sheikh Zayed" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "Marina 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "Centre ville" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "Marina 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "A l'approche de la Burj Khalifa" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "Route Sheikh Zayed" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "Péninsule Nuussuaq" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "Fjord glacé d'Ilulissat" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "Fjord glacé d'Ilulissat" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "Vallée Waimanu" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "Laupāhoehoe Nui" }, "b2-2": { "name": "Vallée Honopū" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "Pu‘u O ‘Umi" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "Côte Kohala" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "Pu‘u O ‘Umi" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "Port Victoria 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "Pic Victoria" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "Wan Chai" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "Port Victoria 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "Port Victoria" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "Oasis de Liwa 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "Oasis de Liwa 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "Tamise" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "Palais de Buckingham" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "Tamise au coucher du soleil" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "Tamise au crépuscule" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "Aéroport de Los Angeles" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "Harbor Freeway (I-110)" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "Plage Santa Monica" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "Observatoire Griffith" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "Collines d'Hollywood" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "Centre ville" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "Central Park" }, "b1-3": { "name": "Sud de Manhattan" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "Upper East Side" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "Septième Avenue" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "Sud de Manhattan" }, "b8-2": { "name": "Marin Headlands" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "Marin Headlands dans le brouillard" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "Bay et Golden Gate" }, "b8-3": { "name": "Parc d'Alamo Square" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "Embarcadero, Market Street" }, "b4-3": { "name": "Presidio vers Golden Gate" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "Golden Gate depuis SF" }, "b6-4": { "name": "Centre ville et Tour Coit" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "Fisherman's Wharf" }, "b5-3": { "name": "Embarcadero, Market Street" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "Bay Bridge" }, "b2-4": { "name": "Centre-ville et tour Sutro" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "Bay et Embarcadero" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "Antarctique" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "Aurore en Amérique du Nord" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "Nuit Africaine" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "Afrique et Moyen-Orient" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "Californie vers Vegas" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "Caraïbes" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "Caraïbes de jour" }, "6324F6EB-E0F1-468F-AC2E-A983EBDDD53B": { "name": "Chine" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "Italie vers l’Asie" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "Iran et Afghanistan" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "Irlande vers l’Asie" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "Afrique de l'Ouest vers les Alpes" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "Nouvelle-Zélande" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "Sahara et Italie" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "Océan Atlantique vers l'Espagne et la France" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "Australie" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "Caraïbes et Amérique Centrale" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "Delta du Nil" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "Afrique du Sud vers Asie du Nord" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "Corail de Palaos" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "Macroalgues en Afrique du Sud" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "Barracuda" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "Étoiles de mer" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "Poissons-perroquets à bosse" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "Carangues à gros yeux" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "Dauphins en Californie" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "Forêt de macroalgues en Californie" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "Dauphins au Costa Rica" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "Mourines du Pacifique" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "Requins gris de récif" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "Baleine à bosse" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "Corail de la mer Rouge" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "Otaries à fourrure" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "Méduses dorées de Palaos 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "Méduses dorées de Palaos 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "Méduses dorées de Palaos 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "Méduses lune de l'Alaska 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "Méduses lune de l'Alaska 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "Vagues à Tahiti 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "Vagues à Tahiti 2" }, "E334A6D2-7145-47C8-9B00-C20DED08B2D5": { "name": "Fleuve Colorado 1" }, "DD266E1F-5DF2-4CDB-A2EB-26CE35664657": { "name": "Canyon brûlé" }, "F9F918CD-E15F-4F01-A326-84A44650C5C9": { "name": "Coucher de soleil sur le Grand Canyon" }, "AE0115AE-C53B-4DB9-B12F-CA4B7B630CC9": { "name": "Fleuve Colorado 2" }, "8002C4C8-C611-4894-A068-3D3A3C03472A": { "name": "Fleuve Colorado 3" }, "5C987900-AD53-469C-8210-CABBCCDDFCAE": { "name": "Cuernos del Paine" }, "B004358B-5A27-42E5-B49E-93FC100B2371": { "name": "Lac Nordenskjöld 1" }, "25A6CFB2-3570-4448-B114-244A4E454B7A": { "name": "Lac Nordenskjöld 2" }, "E5D58CC2-3C52-4206-9DA2-427DC88B5896": { "name": "Torres del Paine" }, "E5799A24-1949-4E66-A17B-B5EB05F28C5D": { "name": "Demi-Dome" }, "E487C6EF-B3FB-427B-A2BE-8CBA60F902F0": { "name": "Parc Yosemite 1" }, "E540DEE6-4C40-42C8-9CCC-D4CB0FAD7D7B": { "name": "Parc Yosemite 2" }, "81CA5ACD-E682-4D8B-A948-0F147EB6ED4F": { "name": "Parc Yosemite 3" }, "4109D42A-D717-46A7-A9A2-FE53A82B25C0": { "name": "Chute Bridalveil et El Capitan" }, "DAD82DCE-F3AE-4AEC-8A79-1694D412FC0A": { "name": "Prairies de Tuolumne" }, "8D04D70F-738B-441D-8D43-AF46B2BF8062": { "name": "Matthes Crest" }, "DDE50C77-B7CB-4488-9EB1-D1B13BF21FFE": { "name": "Tungnaá" }, "E54D5AFE-F362-4D48-A20D-F2C21D2B5330": { "name": "Rivière Jökulgilskvísl" }, "8ACF5D77-B22C-416F-B12A-72FB35E2834F": { "name": "Landmannalaugar" }, "8590D0C5-E344-4FAC-A39A-FD7BC652AEDA": { "name": "Langisjór" }, "2F17FCCE-6CCA-4AFA-A08A-C50BF9812DA5": { "name": "Mýrdalsjökull" }, "F9518D54-04A7-4793-8666-CFC114D73CE5": { "name": "Jökulgil" }, "3954A7C4-51EC-4ABC-ABA3-6757AC91C7CF": { "name": "Loch Moidart 1" }, "0C747C29-4BF8-43F6-A5CC-2E012E555341": { "name": "Île de Skye" }, "E161929C-0819-4BC2-8359-550C081C7D54": { "name": "Loch Moidart 2" } } ================================================ FILE: Resources/Community/he.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "קוריאה ולילה ביפן" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "מקסיקו סיטי לניו יורק" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "צפון קליפורניה לבאחה" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "הפארק הלאומי וולינגיואן 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "טרסות אורז לונגיי" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "הפארק הלאומי וולינגיואן 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "החומה הגדולה 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "החומה הגדולה 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "החומה הגדולה 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "רחוב שייח זאיד" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "מרינה 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "מרכז העיר" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "מרינה 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "מתקרב לבורז' ח'ליפה" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "רחוב שייח זאיד" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "חצי האי ניוסואק" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "אייספיורד אילוליסאט" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "אייספיורד אילוליסאט" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "עמק ויאמאנו" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "לאופאהוהו ניווי" }, "b2-2": { "name": "עמק הונפו" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "פו או אומי" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "קו החוף קוהלה" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "פו או אומי" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "נמל ויקטוריה 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "פסגת ויקטוריה" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "וואן צ'אי" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "נמל ויקטוריה 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "נמל ויקטוריה" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "נווה מדבר ליווה 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "נווה מדבר ליווה 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "נהר התמזה" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "ארמון בקינגהאם" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "נהר התמזה קרוב לשקיעה" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "נהר התמזה בשעת בין ערביים" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "נמל התעופה הבינלאומי של לוס אנג'לס" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "הכביש המהיר הארבור" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "חוף סנטה מוניקה" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "מצפה גריפית" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "הוליווד הילס" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "מרכז העיר" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "סנטרל פארק" }, "b1-3": { "name": "מנהטן התחתונה" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "אפר איסט סייד" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "השדרה השביעית" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "מנהטן התחתונה" }, "b8-2": { "name": "צוקי מארין" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "צוקי מארין בערפל" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "המפרץ וגשר גולדן גייט" }, "b8-3": { "name": "כיכר אלאמו" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "רחוב השוק, אמברקדרו" }, "b4-3": { "name": "פרסידיו לכיוון גשר גולדן גייט" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "גשר הגולדן גייט מכיוון סן פרנסיסקו" }, "b6-4": { "name": "מרכז העיר וקוויט טאוור" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "רציף הדייג" }, "b5-3": { "name": "רחוב השוק, אמברקדרו" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "גשר המפרץ" }, "b2-4": { "name": "מרכז העיר ומגדל סוטרו" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "ביי ואמברקדרו" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "אנטארטיקה" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "אורורה בצפון אמריקה" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "לילה אפריקני" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "אפריקה והמזרח התיכון" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "קליפורניה לווגאס" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "הקאריביים" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "יום קריבי" }, "7825C73A-658F-48EE-B14C-EC56673094AC": { "name": "סין" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "איטליה לאסיה" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "אירן ואפגניסטן" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "אירלנד לאסיה" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "מערב אפריקה ועד לאלפים" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "ניו זילנד" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "סהרה ואיטליה" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "Atlantic Ocean to Spain and France" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "Australia" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "Caribbean to Central America" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "Nile Delta" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "South Africa to North Asia" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "Palau Coral" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "South African Kelp" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "Barracuda" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "Sea Stars" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "Bumpheads" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "Jacks" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "California Dolphins" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "California Kelp Forest" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "Costa Rica Dolphins" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "Cownose Rays" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "Gray Reef Sharks" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "Humpback Whale" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "Red Sea Coral" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "Seals" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "Palau Jellies 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "Palau Jellies 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "Palau Jellies 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "Alaskan Jellies 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "Alaskan Jellies 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "Tahiti Waves 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "Tahiti Waves 2" } } ================================================ FILE: Resources/Community/hu.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "Korea és Japán éjszaka" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "Mexikóváros és New York között" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "Észak-Kalifornia és Baja között" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "Wulingyuan Nemzeti Park 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "Longji rizsteraszok" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "Wulingyuan Nemzeti Park 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "Nagy Fal 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "Nagy Fal 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "Nagy fal 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "Sheikh Zayed út" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "Marina 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "Belváros" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "Marina 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "Közeledés a Burj Khalifa felé" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "Sheikh Zayed út" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "Nuussuaq-félsziget" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "Ilulissat jégfjord" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "Ilulissat jégfjord" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "Waimanu-völgy" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "Laupāhoehoe Nui" }, "b2-2": { "name": "Honopū-völgy" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "Pu'u O 'Umi" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "Kohala partvonala" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "Pu'u O 'Umi" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "Viktória kikötő 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "Viktória csúcs" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "Wan Chai" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "Victoria kikötő 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "Victoria kikötő" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "Liwa oázis 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "Liwa oázis 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "Temze" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "Buckingham-palota" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "Temze naplementéhez közel" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "Temze alkonyatkor" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "Los Angeles-i nemzetközi repülőtér" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "Kikötői autópálya" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "Santa Monica Beach" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "Griffith Obszervatórium" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "Hollywoodi hegyek" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "Belváros" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "Central Park" }, "b1-3": { "name": "Alsó Manhattan" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "Upper East Side" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "Hetedik sugárút" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "Alsó Manhattan" }, "b8-2": { "name": "Marin Headlands" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "Marin Headlands a ködben" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "Golden Gate és az öböl" }, "b8-3": { "name": "Alamo Square" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "Embarcadero, Market Street" }, "b4-3": { "name": "Presidio és Golden Gate között" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "Golden Gate SF felől" }, "b6-4": { "name": "Belváros és a Coit torony" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "Fisherman's Wharf" }, "b5-3": { "name": "Embarcadero, Market Street" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "Öböl híd" }, "b2-4": { "name": "Belváros és a Sutro torony" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "Bay és Embarcadero " }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "Antarktisz" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "Észak-amerikai sarki fény" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "Afrika éjszaka" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "Afrika és a Közel-Kelet" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "Kalifornia és Vegas között" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "Karib-szigetek" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "Karibi nap" }, "6324F6EB-E0F1-468F-AC2E-A983EBDDD53B": { "name": "Kína" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "Olaszország és Ázsia között" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "Irán és Afganisztán" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "Írország és Ázsia között" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "Nyugat-Afrikától az Alpokig" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "Új-Zéland" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "Szahara és Olaszország" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "Az Atlanti-óceánon át Spanyol- és Franciaország felett" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "Ausztrália" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "Karib-térség és Közép-Amerika között" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "Nílus-delta" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "Dél-Afrika és Észak-Ázsia között" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "Palau korall" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "Dél-afrikai tengeri moszat" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "Barracudák" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "Tengeri csillagok" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "Bütykösfejű papagájhalak" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "Tüskés makrélák" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "Kaliforniai delfinek" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "Kaliforniai moszaterdő" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "Costa Rica-i delfinek" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "Tehénorrú ráják" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "Szürke szirtcápa" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "Hosszúszárnyú bálna" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "Vörös-tengeri korall" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "Fókák" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "Palau medúzák 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "Palau medúzák 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "Palau medúzák 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "Alaszkai medúzák 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "Alaszkai medúzák 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "Tahiti hullámok 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "Tahiti hullámok 2" }, "E334A6D2-7145-47C8-9B00-C20DED08B2D5": { "name": "Colorado folyó 1" }, "DD266E1F-5DF2-4CDB-A2EB-26CE35664657": { "name": "Burnt Canyon" }, "F9F918CD-E15F-4F01-A326-84A44650C5C9": { "name": "Naplemente a Grand Canyon felett" }, "AE0115AE-C53B-4DB9-B12F-CA4B7B630CC9": { "name": "Colorado folyó 2" }, "8002C4C8-C611-4894-A068-3D3A3C03472A": { "name": "Colorado folyó 3" }, "5C987900-AD53-469C-8210-CABBCCDDFCAE": { "name": "Cuernos del Paine" }, "B004358B-5A27-42E5-B49E-93FC100B2371": { "name": "Lago Nordenskjöld 1" }, "25A6CFB2-3570-4448-B114-244A4E454B7A": { "name": "Lago Nordenskjöld 2" }, "E5D58CC2-3C52-4206-9DA2-427DC88B5896": { "name": "Torres del Paine" }, "E5799A24-1949-4E66-A17B-B5EB05F28C5D": { "name": "Half Dome" }, "E487C6EF-B3FB-427B-A2BE-8CBA60F902F0": { "name": "Yosemite Park 1" }, "E540DEE6-4C40-42C8-9CCC-D4CB0FAD7D7B": { "name": "Yosemite Park 2" }, "81CA5ACD-E682-4D8B-A948-0F147EB6ED4F": { "name": "Yosemite Park 3" }, "4109D42A-D717-46A7-A9A2-FE53A82B25C0": { "name": "Bridalveil vízesés és El Capitan" }, "DAD82DCE-F3AE-4AEC-8A79-1694D412FC0A": { "name": "Tuolumne Meadows" }, "8D04D70F-738B-441D-8D43-AF46B2BF8062": { "name": "Matthes Crest" }, "DDE50C77-B7CB-4488-9EB1-D1B13BF21FFE": { "name": "Tungnaá" }, "E54D5AFE-F362-4D48-A20D-F2C21D2B5330": { "name": "Jökulgilskvísl folyó" }, "8ACF5D77-B22C-416F-B12A-72FB35E2834F": { "name": "Landmannalaugar" }, "8590D0C5-E344-4FAC-A39A-FD7BC652AEDA": { "name": "Langisjór" }, "2F17FCCE-6CCA-4AFA-A08A-C50BF9812DA5": { "name": "Mýrdalsjökull" }, "F9518D54-04A7-4793-8666-CFC114D73CE5": { "name": "Jökulgil" }, "3954A7C4-51EC-4ABC-ABA3-6757AC91C7CF": { "name": "Loch Moidart 1" }, "0C747C29-4BF8-43F6-A5CC-2E012E555341": { "name": "Skye szigete" }, "E161929C-0819-4BC2-8359-550C081C7D54": { "name": "Loch Moidart 2" } } ================================================ FILE: Resources/Community/it.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "Corea e Giappone di notte" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "Da Città del Messico a New York" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "Dalla California del Nord a Baja" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "Parco nazionale Wulingyuan 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "Terrazze di riso Longji" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "Parco nazionale Wulingyuan 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "Grande Muraglia 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "Grande Muraglia 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "Grande Muraglia 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "Sheikh Zayed Road" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "Marina 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "Centro città" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "Marina 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "Avvicinamento al Burj Khalifa" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "Strada dello Sceicco" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "Penisola di Nuussuaq" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "Fiordo Di Ilulissat" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "Fiordo Di Ilulissat" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "Valle di Waimanu" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "Laupāhoehoe Nui" }, "b2-2": { "name": "Valle di Honopū" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "Pu'u O 'Umi" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "Costa di Kohala" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "Pu'u O 'Umi" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "Il Porto Di Victoria, 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "Victoria Peak" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "Wan Chai" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "Il Porto Di Victoria 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "Porto di Victoria" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "Oasi Di Liwa, 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "Oasi di Liwa 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "Il Tamigi" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "Buckingham Palace" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "Fiume Tamigi al tramonto" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "Il Tamigi al crepuscolo" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "Aeroporto Internazionale di Los Angeles" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "Harbor Freeway" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "Spiaggia di Santa Monica" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "L'Osservatorio Griffith" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "Colline Di Hollywood" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "Centro città" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "Central Park" }, "b1-3": { "name": "Lower Manhattan" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "Upper East Side" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "Settima Avenue" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "Lower Manhattan" }, "b8-2": { "name": "Marin Headlands" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "Marin Headlands nella nebbia" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "Baia e Golden Gate" }, "b8-3": { "name": "Alamo Square" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "Embarcadero, Market Street" }, "b4-3": { "name": "Dal Presidio al Golden Gate" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "Golden Gate da San Francisco" }, "b6-4": { "name": "Centro città e Coit Tower" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "Fisherman's Wharf" }, "b5-3": { "name": "Embarcadero, Market Street" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "Bay Bridge" }, "b2-4": { "name": "Centro città e Torre Sutro" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "Baia e Embarcadero" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "Antartide" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "Aurora in Nord America" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "Africa di notte" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "Africa e Medio Oriente" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "California verso Las Vegas" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "Caraibi" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "Caraibi di giorno" }, "7825C73A-658F-48EE-B14C-EC56673094AC": { "name": "Cina" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "Italia verso l'Asia" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "Iran e Afghanistan" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "Irlanda verso l'Asia" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "Africa occidentale verso le Alpi" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "Nuova Zelanda" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "Sahara e Italia" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "Oceano Atlantico verso la Spagna e Francia" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "Australia" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "Caraibi verso l'America Centrale" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "Delta del Nilo" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "Sud Africa verso l'Asia del Nord" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "Corallo a Palau" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "Alghe Kelp in Sud Africa" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "Barracuda" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "Stelle marine" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "Pesce Pappagallo Gibboso" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "Jack (bigeye)" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "Delfini in California" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "Foresta di Kelp in California" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "Delfini in Costa Rica" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "Razza cownose" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "Squali grigi" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "Megattera" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "Corallo del Mar Rosso" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "Foche" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "Meduse in Palau 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "Meduse in Palau 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "Meduse in Palau 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "Meduse in Alaska 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "Meduse in Alaska 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "Onde a Tahiti 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "Onde a Tahiti 2" } } ================================================ FILE: Resources/Community/ja.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "韓国と日本の夜" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "メキシコ・シティからニューヨークへ" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "北カリフォルニアからバハ" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "武陵源国立公園 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "龍脊棚田" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "武陵源国立公園 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "万里の長城 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "万里の長城 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "万里の長城 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "シェイク・ザイード・ロード" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "マリーナ 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "ダウンタウン" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "マリーナ 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "ブルジュ・ハリファに接近" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "シェイク・ザイード・ロード" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "ヌウスアク半島" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "イルリサット・アイスフィヨルド" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "イルリサット・アイスフィヨルド" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "ワイマヌ渓谷" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "ラウパーホーホー・ヌイ" }, "b2-2": { "name": "ホノプ渓谷" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "プーウ・オウ・ウミ" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "コハラの海岸線" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "プーウ・オウ・ウミ" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "ビクトリア・ハーバー 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "ビクトリア・ピーク" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "ワンチャイ" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "ビクトリア・ハーバー 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "ビクトリア・ハーバー" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "リワ・オアシス 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "リワ・オアシス 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "テムズ川" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "バッキンガム宮殿" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "日没近くのテムズ川" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "夕暮れ時にテムズ川" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "ロサンゼルス国際空港" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "ハーバー・フリーウェイ" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "サンタモニカ・ビーチ" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "グリフィス天文台" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "ハリウッド・ヒルズ" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "ダウンタウン" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "セントラル・パーク" }, "b1-3": { "name": "ローワー・マンハッタン" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "アッパー・イースト・サイド" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "7番街" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "ローワー・マンハッタン" }, "b8-2": { "name": "マリン・ヘッドランズ" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "霧の中のマリン・ヘッドランズ" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "ベイとゴールデン・ゲート" }, "b8-3": { "name": "アラモ広場" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "エンバカデロ、マーケット・ストリート" }, "b4-3": { "name": "プレシディオからゴールデンゲートへ" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "SFからのゴールデンゲート" }, "b6-4": { "name": "ダウンタウンとコイト・タワー" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "フィッシャーマンズ・ワーフ" }, "b5-3": { "name": "エンバカデロ、マーケット・ストリート" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "ベイ・ブリッジ" }, "b2-4": { "name": "ダウンタウンとストロ・タワー" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "ベイとエンバカデロ" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "南極" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "北米のオーロラ" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "アフリカの夜" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "アフリカと中東" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "カリフォルニアからラスベガス" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "カリブ海" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "カリブ海の日" }, "7825C73A-658F-48EE-B14C-EC56673094AC": { "name": "中国" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "イタリアからアジアへ" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "イランとアフガニスタン" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "アイルランドからアジアへ" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "西アフリカからアルプスまで" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "ニュージーランド" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "サハラとイタリア" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "大西洋からスペインとフランスへ" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "オーストラリア" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "カリブ海から中央アメリカ" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "ナイル川デルタ" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "南アフリカから北アジアへ" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "パラオの珊瑚" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "南アフリカの昆布" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "バラクーダ" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "ヒトデ" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "カンムリブダイ" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "ギンガメアジ" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "カリフォルニアのイルカ" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "カリフォルニアの昆布の森" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "コスタリカのイルカ" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "クロガネウシバナトビエイ " }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "オグロメジロザメ" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "ザトウクジラ" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "紅海のサンゴ" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "アザラシ" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "パラオのクラゲ 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "パラオのクラゲ 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "パラオのクラゲ 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "アラスカのクラゲ 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "アラスカのクラゲ 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "タヒチの波 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "タヒチの波 2" } } ================================================ FILE: Resources/Community/ko.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "한국과 일본의 밤" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "멕시코시티에서 뉴욕까지" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "미국 캘리포니아 북부에서 멕시코 바하까지" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "장자지에 무릉원 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "롱성롱지지역의 계단식 논" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "장자지에 무릉원 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "만리장성 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "만리장성 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "만리장성 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "셰이크 자이드 도로" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "마리나 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "시내" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "마리나 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "부르즈 할리파" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "셰이크 자이드 도로" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "누우수아크 반도" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "일룰리사트 아이스피오르드" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "일룰리사트 아이스피오르드" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "와이마누 밸리" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "라우바호회 누이" }, "b2-2": { "name": "호노푸 밸리" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "푸우 오 '우미" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "카할라 해안" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "푸우 오 '우미" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "빅토리아 항구 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "빅토리아 피크" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "완차이" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "빅토리아 항구 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "빅토리아 항구" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "리와 오아시스 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "리와 오아시스 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "템스강" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "버킹엄 궁전" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "템스강의 일몰" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "황혼의 템스강" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "로스앤젤레스 국제공항" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "하버 프리웨이" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "산타모니카 해변" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "그리피스 천문대" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "할리우드 힐스" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "시내" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "센트럴 파크" }, "b1-3": { "name": "맨해튼 남부" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "어퍼 이스트 사이드" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "7번가" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "맨해튼 남부" }, "b8-2": { "name": "마린 헤드랜드" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "안개 속의 마린 헤드랜드" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "샌프란시스코만과 금문교" }, "b8-3": { "name": "알라모 스퀘어" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "엠바카데로와 마켓 스트리트" }, "b4-3": { "name": "미국 샌프란시스코 프레시디오에서 금문교까지" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "금문교에서 바라본 샌프란시스코" }, "b6-4": { "name": "샌프란시스코 시내와 코잇타워" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "샌프란시스코 피셔맨스 워프" }, "b5-3": { "name": "엠바카데로와 마켓 스트리트" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "샌프란시스코-오클랜드 베이 브릿지" }, "b2-4": { "name": "샌프란시스코 시내와 수트로 타워" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "샌프란시스코만과 엠바카데로" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "남극" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "북아메리카의 오로라" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "아프리카의 밤" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "아프리카와 중동" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "캘리포니아에서 라스베가스까지" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "카리브제도" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "카리브제도의 낮" }, "7825C73A-658F-48EE-B14C-EC56673094AC": { "name": "중국" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "이탈리아에서 아시아까지" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "이란과 아프가니스탄" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "아일랜드에서 아시아까지" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "서아프리카에서 알프스까지" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "뉴질랜드" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "사하라사막과 이탈리아" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "대서양에서 스페인 그리고 프랑스까지" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "호주" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "카리브제도에서 중앙아메리카까지" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "나일강 삼각주" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "남아프리카에서 북아시아까지" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "팔라우의 산호" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "남아프리카의 다시마목" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "바라쿠다" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "불가사리" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "녹색의 험프헤드 패럿피쉬" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "전갱이" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "캘리포니아의 돌고래들" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "캘리포니아의 다시마목" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "코스타리카의 돌고래들" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "카우노즈레이" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "그레이 리프 샤크" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "혹등고래" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "홍해 산호" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "물개들" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "팔라우의 해파리 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "팔라우의 해파리 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "팔라우의 해파리 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "알래스카의 해파리 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "알래스카의 해파리 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "타히티의 물결 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "타히티의 물결 2" } } ================================================ FILE: Resources/Community/missingvideos.json ================================================ { "assets" : [ { "pointsOfInterest" : { "0" : "HK_H004_C001_0" }, "url-1080-H264" : "https:/\/sylvan.apple.com\/Videos\/comp_HK_H004_C001_PSNK_DENOISE_v14_SDR_PS_FINAL_20180731_SDR_2K_AVC.mov", "accessibilityLabel" : "Hong Kong", "id" : "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA", "url-1080-HDR" : "https:\/\/sylvan.apple.com\/Aerials\/2x\/Videos\/comp_HK_H004_C001_PSNK_DENOISE_v14_HDR_PS_FINAL_20180731_HDR_2K_HEVC.mov", "url-1080-SDR" : "https:\/\/sylvan.apple.com\/Aerials\/2x\/Videos\/comp_HK_H004_C001_PSNK_DENOISE_v14_SDR_PS_FINAL_20180731_SDR_2K_HEVC.mov", "url-4K-SDR" : "https:\/\/sylvan.apple.com\/Aerials\/2x\/Videos\/comp_HK_H004_C001_PSNK_DENOISE_v14_SDR_PS_FINAL_20180731_SDR_4K_HEVC.mov", "url-4K-HDR" : "https:\/\/sylvan.apple.com\/Aerials\/2x\/Videos\/comp_HK_H004_C001_PSNK_DENOISE_v14_HDR_PS_FINAL_20180731_HDR_4K_HEVC.mov" }, ] } ================================================ FILE: Resources/Community/nl.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "Korea en Japan Nacht" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "Mexico-stad naar New York" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "Noord-Californië naar Baja" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "Wulingyuan Nationaal Park 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "Longji rijstterrassen" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "Wulingyuan Nationaal Park 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "Chinese Muur 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "Chinese Muur 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "Chinese Muur 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "Sheikh Zayed Road" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "Jachthaven 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "Het centrum" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "Jachthaven 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "Het naderen van Burj Khalifa" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "Sheikh Zayed Road" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "Nuussuaq Peninsula" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "Ilulissat Icefjord" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "Ilulissat Icefjord" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "Waimanu Valley" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "Laupāhoehoe Nui" }, "b2-2": { "name": "Honopū Valley" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "Pu‘u O ‘Umi" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "De kustlijn van Kohala" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "Pu‘u O ‘Umi" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "De Victoria-Haven 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "Victoria Peak" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "Wan Chai" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "De Victoria-Haven 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "De Victoria-Haven" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "Liwa Oasis 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "Liwa Oasis 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "Rivier de Theems" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "Buckingham Palace" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "Rivier de Theems bij zonsondergang" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "Rivier de Theems in de schemering" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "Los Angeles Int'l Airport" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "Harbor Freeway" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "Santa Monica Beach" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "Griffith Observatory" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "Hollywood Hills" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "Het centrum" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "Central Park" }, "b1-3": { "name": "Lower Manhattan" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "Upper East Side" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "Seventh Avenue" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "Lower Manhattan" }, "b8-2": { "name": "Marin Headlands" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "Marin Headlands in Fog" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "De baai en de Golden Gate" }, "b8-3": { "name": "Alamo Square" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "Embarcadero, Market Street" }, "b4-3": { "name": "Presidio naar Golden Gate" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "Golden Gate van SF" }, "b6-4": { "name": "Het centrum en de Coit Tower" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "Fisherman's Wharf" }, "b5-3": { "name": "Embarcadero, Market Street" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "Bay Bridge" }, "b2-4": { "name": "Het centrum en de Sutro Tower" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "Bay en Embarcadero" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "Antartica" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "Noord-Amerika Aurora" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "Afrikaanse nacht" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "Afrika en het Midden-Oosten" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "Californië naar Vegas" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "Caribisch gebied" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "Caribische dag" }, "7825C73A-658F-48EE-B14C-EC56673094AC": { "name": "China" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "Italië naar Azië" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "Iran en Afghanistan" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "Ierland naar Azië" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "West-Afrika naar de Alpen" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "Nieuw-Zeeland" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "Sahara en Italië" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "Atlantic Ocean to Spain and France" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "Australia" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "Caribbean to Central America" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "Nile Delta" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "South Africa to North Asia" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "Palau Coral" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "South African Kelp" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "Barracuda" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "Sea Stars" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "Bumpheads" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "Jacks" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "California Dolphins" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "California Kelp Forest" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "Costa Rica Dolphins" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "Cownose Rays" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "Gray Reef Sharks" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "Humpback Whale" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "Red Sea Coral" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "Seals" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "Palau Jellies 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "Palau Jellies 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "Palau Jellies 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "Alaskan Jellies 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "Alaskan Jellies 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "Tahiti Waves 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "Tahiti Waves 2" } } ================================================ FILE: Resources/Community/pl.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "Korea i Japonia" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "Od Mexico City do Nowego Jorku" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "Od Kalifornii do Baja" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "Park Narodowy Wulingyuan 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "Tarasy ryżowe Longji" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "Park Narodowy Wulingyuan 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "Wielki Mur 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "Wielki Mur 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "Wielki Mur 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "Droga Sheikh Zayed" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "Przystań 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "Śródmieście" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "Przystań 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "Zbliżając się do Burj Khalifa" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "Droga Sheikh Zayed" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "Półwysep Nuussuaq" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "Fjord lodowy w llulissat" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "Fjord lodowy w llulissat" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "Dolina Waimanu" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "Laupāhoehoe Nui" }, "b2-2": { "name": "Dolina Honopū" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "Pu'u O 'Umi" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "Wybrzeże Kohala" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "Pu'u O 'Umi" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "Port Wiktorii 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "Szczyt Wiktorii" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "Wan Chai" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "Port Wiktorii 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "Port Wiktorii" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "Oaza Liwa 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "Oaza Liwa 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "Tamiza" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "Pałac Buckingham" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "Rzeka Tamiza podczas zachodu słońca" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "Tamiza o zmierzchu" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "Międzynarodowy port lotniczy Los Angeles" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "Harbor Freeway" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "Plaża Santa Monica" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "Obserwatorium Griffitha" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "Wzgórza Hollywood" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "Śródmieście" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "Central Park" }, "b1-3": { "name": "Dolny Manhattan" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "Upper East Side" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "Siódma Aleja" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "Dolny Manhattan" }, "b8-2": { "name": "Marin Headlands" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "Marin Headlands we mgle" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "Zatoka i Most Golden Gate" }, "b8-3": { "name": "Plac Alama" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "Embarcadero, Market Street" }, "b4-3": { "name": "Od Prezydium do Mostu Golden Gate" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "Most Golden Gate od strony San Franciso" }, "b6-4": { "name": "Śródmieście i Wieża Coit" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "Fisherman's Wharf" }, "b5-3": { "name": "Embarcadero, Market Street" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "Most Zatokowy" }, "b2-4": { "name": "Śródmieście i wieża Sutro" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "Zatoka San Francisco i Embarcadero" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "Antarktyda" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "Zorza nad Ameryką Północną" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "Noc afrykańska" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "Afryka i Bliski Wschód" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "Od Kalifornii do Vegas" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "Karaiby" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "Karaiby za dnia" }, "7825C73A-658F-48EE-B14C-EC56673094AC": { "name": "Chiny" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "Od Włoch do Azji" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "Iran i Afganistan" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "Od Irlandii do Azji" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "Od Afryki Zachodniej do Alp" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "Nowa Zelandia" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "Sahara i Włochy" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "Od Oceanu Atlantyckiego do Hiszpanii i Francji" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "Australia" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "Od Karaibów do Ameryki Środkowej" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "Delta Nilu" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "Od Afryki Południowej do Azji Północnej" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "Rafa koralowa Palau" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "Wodorosty południowoafrykańskie" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "Barrakuda" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "Gwiazdy morskie" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "Bolbometopon muricatum" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "Karanks smugowy" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "Delfiny kalifornijskie" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "Las Kelp w Kalifornii" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "Delfiny kostarykańskie" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "Płaszczka karbogłowa" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "Rekin szary rafowy" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "Długopłetwiec oceaniczny" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "Koralowce Morza Czerwonego" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "Płetwonogie" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "Meduzy Palau 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "Meduzy Palau 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "Meduzy Palau 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "Meduzy Alaskie 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "Meduzy Alaskie 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "Fale Tahiti 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "Fale Tahiti 2" } } ================================================ FILE: Resources/Community/pt.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "Coreia e Japão a noite" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "Da Cidade do México para Nova York" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "Do Norte da Califórnia para Baja" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "Parque Nacional Wulingyuan 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "Terraços de Arroz de Longji" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "Parque Nacional Wulingyuan 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "Muralha da China 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "Muralha da China 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "Muralha da China 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "Sheikh Zayed Road" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "Marina 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "Centro da cidade" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "Marina 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "Aproximando-se do Burj Khalifa" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "Estrada Sheikh Zayed" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "Península de Nuussuaq" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "Fiorde de gelo de Ilulissat" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "Fiorde de gelo de Ilulissat" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "Vale de Waimanu" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "Laupāhoehoe Nui" }, "b2-2": { "name": "Honopū Vale" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "Pu'u O 'Umi" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "Costa de Kohala" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "Pu'u O 'Umi" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "Victoria Harbour 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "Victoria Peak" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "Wan Chai" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "Victoria Harbour 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "Victoria Harbour" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "Liwa Oasis 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "Liwa Oasis 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "Rio Tamisa" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "Palácio de Buckingham" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "Rio Tamisa ao pôr do sol" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "Rio Tamisa ao Anoitecer" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "Aeroporto Internacional de Los Angeles" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "Harbor Freeway (Rota 110)" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "Praia de Santa Monica" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "Observatório Griffith" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "Hollywood Hills" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "Centro da cidade" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "Central Park" }, "b1-3": { "name": "Baixa Manhattan" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "Upper East Side" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "Sétima Avenida" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "Baixa Manhattan" }, "b8-2": { "name": "Marin Headlands" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "Marin Headlands com Neblina" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "Baía de SF e Golden Gate Bridge" }, "b8-3": { "name": "Alamo Square" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "Embarcadero, Market Street" }, "b4-3": { "name": "Do Presídio para a Golden Gate" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "Ponte Golden Gate em SF" }, "b6-4": { "name": "Centro da cidade e Coit Tower" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "Fisherman’s Wharf" }, "b5-3": { "name": "Embarcadero, Market Street" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "Ponte São Francisco" }, "b2-4": { "name": "Centro da cidade e Torre Sutro" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "Baía e Embarcadero" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "Antartica" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "Aurora Borealis (América do Norte)" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "África a noite" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "África e o Oriente Médio" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "Da Califórnia para Vegas" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "Caribe" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "Caribe de dia" }, "7825C73A-658F-48EE-B14C-EC56673094AC": { "name": "China" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "Da Itália para a Ásia" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "Irã e Afeganistão" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "Da Irlanda para a Ásia" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "Da África Ocidental para os Alpes" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "Nova Zelândia" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "Deserto do Saara e Itália" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "Do Oceano Atlântico à Espanha e França" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "Austrália" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "Do Caribe para a América Central" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "Delta do Nilo" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "Da África do Sul para o Norte da Ásia" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "Corais em Palau" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "Algas na África do Sul" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "Barracudas" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "Estrelas do mar" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "Cardume de Peixe Papagaio Cabeçudo" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "Cardume de Sargento" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "Golfinhos na Califórnia" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "Floresta de Algas da Califórnia" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "Golfinhos da Costa Rica" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "Arraias do mar" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "Tubarões Cinzento dos Recifes" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "Baleia Jubarte" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "Coral do Mar Vermelho" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "Focas" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "Águas-vivas em Palau 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "Águas-vivas em Palau 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "Águas-vivas em Palau 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "Águas-vivas no Alasca 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "Águas-vivas no Alasca 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "Ondas do Taiti 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "Ondas do Taiti 2" } } ================================================ FILE: Resources/Community/pt_BR.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "Coreia e Japão a noite" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "Da Cidade do México para Nova York" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "Do Norte da Califórnia para Baja" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "Parque Nacional Wulingyuan 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "Terraços de Arroz de Longji" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "Parque Nacional Wulingyuan 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "Muralha da China 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "Muralha da China 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "Muralha da China 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "Sheikh Zayed Road" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "Marina 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "Centro da cidade" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "Marina 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "Aproximando-se do Burj Khalifa" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "Estrada Sheikh Zayed" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "Península de Nuussuaq" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "Fiorde de gelo de Ilulissat" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "Fiorde de gelo de Ilulissat" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "Vale de Waimanu" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "Laupāhoehoe Nui" }, "b2-2": { "name": "Honopū Vale" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "Pu'u O 'Umi" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "Costa de Kohala" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "Pu'u O 'Umi" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "Victoria Harbour 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "Victoria Peak" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "Wan Chai" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "Victoria Harbour 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "Victoria Harbour" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "Liwa Oasis 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "Liwa Oasis 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "Rio Tamisa" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "Palácio de Buckingham" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "Rio Tamisa ao pôr do sol" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "Rio Tamisa ao Anoitecer" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "Aeroporto Internacional de Los Angeles" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "Harbor Freeway (Rota 110)" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "Praia de Santa Monica" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "Observatório Griffith" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "Hollywood Hills" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "Centro da cidade" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "Central Park" }, "b1-3": { "name": "Baixa Manhattan" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "Upper East Side" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "Sétima Avenida" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "Baixa Manhattan" }, "b8-2": { "name": "Marin Headlands" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "Marin Headlands com Neblina" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "Baía de SF e Golden Gate Bridge" }, "b8-3": { "name": "Alamo Square" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "Embarcadero, Market Street" }, "b4-3": { "name": "Do Presídio para a Golden Gate" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "Ponte Golden Gate em SF" }, "b6-4": { "name": "Centro da cidade e Coit Tower" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "Fisherman’s Wharf" }, "b5-3": { "name": "Embarcadero, Market Street" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "Ponte São Francisco" }, "b2-4": { "name": "Centro da cidade e Torre Sutro" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "Baía e Embarcadero" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "Antartica" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "Aurora Borealis (América do Norte)" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "África a noite" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "África e o Oriente Médio" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "Da Califórnia para Vegas" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "Caribe" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "Caribe de dia" }, "7825C73A-658F-48EE-B14C-EC56673094AC": { "name": "China" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "Da Itália para a Ásia" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "Irã e Afeganistão" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "Da Irlanda para a Ásia" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "Da África Ocidental para os Alpes" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "Nova Zelândia" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "Deserto do Saara e Itália" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "Do Oceano Atlântico à Espanha e França" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "Austrália" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "Do Caribe para a América Central" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "Delta do Nilo" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "Da África do Sul para o Norte da Ásia" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "Corais em Palau" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "Algas na África do Sul" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "Barracudas" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "Estrelas do mar" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "Cardume de Peixe Papagaio Cabeçudo" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "Cardume de Sargento" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "Golfinhos na Califórnia" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "Floresta de Algas da Califórnia" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "Golfinhos da Costa Rica" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "Arraias do mar" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "Tubarões Cinzento dos Recifes" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "Baleia Jubarte" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "Coral do Mar Vermelho" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "Focas" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "Águas-vivas em Palau 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "Águas-vivas em Palau 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "Águas-vivas em Palau 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "Águas-vivas no Alasca 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "Águas-vivas no Alasca 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "Ondas do Taiti 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "Ondas do Taiti 2" } } ================================================ FILE: Resources/Community/ru.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "Корея и Япония ночью" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "Из Мехико в Нью-Йорк" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "Из Северной Калифорнии в Нижнюю Калифорнию" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "Национальный парк Улинъюань" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "Рисовые террасы Лунцзи" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "Национальный парк Улинъюань" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "Великая Китайская стена" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "Великая Китайская стена" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "Великая Китайская стена" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "Шоссе шейха Зайеда" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "Гавань" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "Центр города" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "Гавань" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "Приближаясь к Бурдж-Халифе" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "Шоссе шейха Зайеда" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "Полуостров Нууссуак" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "Фьорд Илулиссат-Исфьорд" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "Фьорд Илулиссат-Исфьорд" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "Долина Уэйману" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "Лаупахоехое" }, "b2-2": { "name": "Долина Хонопу" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "Пуу О Уми" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "Побережье Кохалы" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "Пуу О Уми" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "Бухта Виктория" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "Пик Виктория" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "Ваньчай" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "Бухта Виктория" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "Бухта Виктория" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "Оазис Лива" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "Оазис Лива" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "Река Темза" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "Букингемский дворец" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "Закат на Темзе" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "Река Темза в сумерках" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "Международный аэропорт Лос-Анджелеса" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "Автострада Харбор" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "Пляж Санта-Моника" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "Обсерватория Гриффита" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "Голливудские холмы" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "Центр города" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "Центральный парк" }, "b1-3": { "name": "Нижний Манхэттен" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "Верхний Ист-Сайд" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "Седьмая авеню" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "Нижний Манхэттен" }, "b8-2": { "name": "Мыс Марин" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "Мыс Марин в тумане" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "Залив и мост Золотые ворота" }, "b8-3": { "name": "Сквер Аламо" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "Набережная Эмбаркадеро, Маркет-стрит" }, "b4-3": { "name": "От Пресидио до моста Золотые Ворота" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "Мост Золотые Ворота" }, "b6-4": { "name": "Центр города и башня Койт" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "Рыбацкая пристань" }, "b5-3": { "name": "Набережная Эмбаркадеро, Маркет-стрит" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "Бэй-Бридж" }, "b2-4": { "name": "Центр города и башня Сютро" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "Залив и Эмбаркадеро" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "Антарктида" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "Северная Америка на рассвете" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "Африка ночью" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "Африка и Ближний Восток" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "Из Калифорнии в Вегас" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "Карибы" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "Карибы днём" }, "6324F6EB-E0F1-468F-AC2E-A983EBDDD53B": { "name": "Китай" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "Из Италии в Азию" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "Иран и Афганистан" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "Из Ирландии в Азию" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "От Западной Африки до Альп" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "Новая Зеландия" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "Сахара и Италия" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "От Атлантического океана к Испании и Франции" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "Австралия" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "Из Карибского бассейна в Центральную Америку" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "Дельта Нила" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "Из Южной Африки в Северную Азию" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "Кораллы у берегов Палау" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "Южноафриканские водоросли" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "Барракуды" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "Морские звёзды" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "Рыбы-попугаи" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "Каранксы" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "Калифорнийские дельфины" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "Калифорнийский лес водорослей" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "Дельфины Коста-Рики" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "Восточноамериканские бычерылы" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "Темнопёрые серые акулы" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "Горбатый кит" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "Кораллы Красного моря" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "Тюлени" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "Медузы у берегов Палау" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "Медузы у берегов Палау" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "Медузы у берегов Палау" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "Медузы Аляски" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "Медузы Аляски" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "Волны Таити" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "Волны Таити" }, "E334A6D2-7145-47C8-9B00-C20DED08B2D5": { "name": "Река Колорадо" }, "DD266E1F-5DF2-4CDB-A2EB-26CE35664657": { "name": "Каньон Бёрнт" }, "F9F918CD-E15F-4F01-A326-84A44650C5C9": { "name": "Закат над Большим каньоном" }, "AE0115AE-C53B-4DB9-B12F-CA4B7B630CC9": { "name": "Река Колорадо" }, "8002C4C8-C611-4894-A068-3D3A3C03472A": { "name": "Река Колорадо" }, "5C987900-AD53-469C-8210-CABBCCDDFCAE": { "name": "Куэрнос-дель-Пайне" }, "B004358B-5A27-42E5-B49E-93FC100B2371": { "name": "Озеро Норденшельд" }, "25A6CFB2-3570-4448-B114-244A4E454B7A": { "name": "Озеро Норденшельд" }, "E5D58CC2-3C52-4206-9DA2-427DC88B5896": { "name": "Национальный парк Торрес-дель-Пайне" }, "E5799A24-1949-4E66-A17B-B5EB05F28C5D": { "name": "Скала Хаф-Доум" }, "E487C6EF-B3FB-427B-A2BE-8CBA60F902F0": { "name": "Йосемитский национальный парк" }, "E540DEE6-4C40-42C8-9CCC-D4CB0FAD7D7B": { "name": "Йосемитский национальный парк" }, "81CA5ACD-E682-4D8B-A948-0F147EB6ED4F": { "name": "Йосемитский национальный парк" }, "4109D42A-D717-46A7-A9A2-FE53A82B25C0": { "name": "Водопад Брайделвейл и гора Эль-Капитан" }, "DAD82DCE-F3AE-4AEC-8A79-1694D412FC0A": { "name": "Луга Туолумне" }, "8D04D70F-738B-441D-8D43-AF46B2BF8062": { "name": "Гора Крест Маттеса" }, "DDE50C77-B7CB-4488-9EB1-D1B13BF21FFE": { "name": "Река Тунгнаау" }, "E54D5AFE-F362-4D48-A20D-F2C21D2B5330": { "name": "Река Йёкульгильсквисль" }, "8ACF5D77-B22C-416F-B12A-72FB35E2834F": { "name": "Ландманналаугар" }, "8590D0C5-E344-4FAC-A39A-FD7BC652AEDA": { "name": "Озеро Лаунгисьоур" }, "2F17FCCE-6CCA-4AFA-A08A-C50BF9812DA5": { "name": "Ледник Мирдальсйёкюдль" }, "F9518D54-04A7-4793-8666-CFC114D73CE5": { "name": "Каньон Йёкульгил" }, "3954A7C4-51EC-4ABC-ABA3-6757AC91C7CF": { "name": "Залив Лох-Мойдарт" }, "0C747C29-4BF8-43F6-A5CC-2E012E555341": { "name": "Остров Скай" }, "E161929C-0819-4BC2-8359-550C081C7D54": { "name": "Залив Лох-Мойдарт" } } ================================================ FILE: Resources/Community/sv.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "Koreahalvön och Japan på natten" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "Mexico City till New York" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "Norra Kalifornien till Baja" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "Nationalparken Wulingyuan 2" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "Longji-risterasserna" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "Nationalparken Wulingyuan 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "Kinesiska muren 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "Kinesiska muren 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "Kinesiska muren 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "Sheikh Zayed-vägen" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "Marina 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "Centrala stan" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "Marina 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "På väg mot Burj Khalifa" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "Sheikh Zayed-vägen" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "Nuussuaq-halvön" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "Ilulissatfjorden" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "Ilulissatfjorden" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "Waimanu Valley" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "Laupāhoehoe Nui" }, "b2-2": { "name": "Honopū Valley" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "Pu'u O 'Umi" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "Kohalas kust " }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "Pu'u O 'Umi" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "Victoria Harbour 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "Victoria Peak" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "Wan Chai" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "Victoria Harbour 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "Victoria Harbour" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "Liwa-oasen 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "Liwa-oasen 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "Themsen" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "Buckingham Palace" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "Themsen vid solnedgången" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "Themsen vid skymningen" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "Los Angeles Int'l Airport" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "Harbor Freeway" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "Santa Monica Beach" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "Griffith Observatory" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "Hollywood Hills" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "Centrala stan" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "Central Park" }, "b1-3": { "name": "Lower Manhattan" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "Upper East Side" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "7:e avenyn" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "Lower Manhattan" }, "b8-2": { "name": "Marin Headlands" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "Marin Headlands i dimma" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "Bukten och Golden Gate" }, "b8-3": { "name": "Alamo Square" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "Embarcadero, Market Street" }, "b4-3": { "name": "Presidio till Golden Gate" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "Golden Gate från SF" }, "b6-4": { "name": "Centrala stan och Coit Tower" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "Fisherman's Wharf" }, "b5-3": { "name": "Embarcadero, Market Street" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "Bay Bridge" }, "b2-4": { "name": "Centrala stan och Sutro Tower" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "Bukten och Embarcadero" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "Antarktis" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "Polarsken över Nordamerika" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "Afrika på natten" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "Afrika och Mellanöstern" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "Kalifornien till Vegas" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "Karibien" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "Karibien på dagen" }, "7825C73A-658F-48EE-B14C-EC56673094AC": { "name": "Kina" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "Italien till Asien" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "Iran och Afghanistan" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "Irland till Asien" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "Västafrika till Alperna" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "Nya Zeeland" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "Sahara och Italien" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "Atlanten till Spanien och Frankrike" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "Australien" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "Karibien till Centralamerika" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "Nildeltat" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "Sydafrika till Nordasien" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "Koraller i Palau" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "Sydafrikansk tång" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "Barracudor" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "Sjöstjärnor" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "Knölhuvuden" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "Makriller" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "Delfiner i Kalifornien" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "Kelpskog i Kalifornien" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "Delfiner i Costa Rica" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "Rockor" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "Gråa rävhajar" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "Knölval" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "Koraller i Röda havet " }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "Sälar" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "Maneter i Palau 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "Maneter i Palau 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "Maneter i Palau 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "Maneter i Alaska 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "Maneter i Alaska 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "Vågor i Tahiti 1 " }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "Vågor i Tahiti 2" } } ================================================ FILE: Resources/Community/tl.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "Korea at Japan sa Gabi" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "Siyudad ng Mexico patungong New York" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "Hilagang California patungong Baja" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "Wulingyuan National Park 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "Longji Rice Terraces" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "Wulingyuan National Park 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "Great Wall 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "Great Wall 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "Great Wall 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "Sheikh Zayed Road" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "Marina 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "Downtown" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "Marina 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "Papalapit ng Burj Khalifa" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "Sheikh Zayed Road" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "Nuussuaq Peninsula" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "Ilulissat Icefjord" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "Ilulissat Icefjord" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "Lambak ng Waimanu" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "Laupāhoehoe Nui" }, "b2-2": { "name": "Lambak ng Honopū" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "Pu'u O 'Umi" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "Tabing-Dagat sa Kohala" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "Pu'u O 'Umi" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "Victoria Harbour 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "Victoria Peak" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "Wan Chai" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "Victoria Harbour 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "Victoria Harbour" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "Liwa Oasis 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "Liwa Oasis 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "Ilog Thames" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "Buckingham Palace" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "Ilog Thames sa Paglubog ng Araw" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "Ilog Thames sa Takipsilim" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "Los Angeles Int'l Airport" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "Harbor Freeway" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "Santa Monica Beach" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "Griffith Observatory" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "Hollywood Hills" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "Downtown" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "Central Park" }, "b1-3": { "name": "Lower Manhattan" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "Upper East Side" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "Seventh Avenue" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "Lower Manhattan" }, "b8-2": { "name": "Marin Headlands" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "Marin Headlands sa Ilalim ng Hamog" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "Bay at Golden Gate" }, "b8-3": { "name": "Alamo Square" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "Embarcadero, Market Street" }, "b4-3": { "name": "Presidio patungong Golden Gate" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "Golden Gate mula SF" }, "b6-4": { "name": "Downtown at Coit Tower" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "Fisherman's Wharf" }, "b5-3": { "name": "Embarcadero, Market Street" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "Bay Bridge" }, "b2-4": { "name": "Downtown at Sutro Tower" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "Bay at Embarcadero" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "Antartica" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "Aurora sa Hilagang Amerika" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "Aprika sa Gabi" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "Aprika at Gitnang Silangan" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "California patungong Vegas" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "Caribbean" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "Caribbean sa Umaga" }, "6324F6EB-E0F1-468F-AC2E-A983EBDDD53B": { "name": "Tsina" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "Italya patungong Asya" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "Iran at Afghanistan" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "Ireland patungong Asya" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "Kanlurang Aprika patungo sa Alps" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "New Zealand" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "Sahara at Italya" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "Karagatang Atlantiko patungong Espanya at Pransya" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "Australya" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "Caribbean patungong Central America" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "Nile Delta" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "Timog Aprika patungong Hilagang Asya" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "Korales ng Palau" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "Kelp ng Timog Aprika" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "Barracuda" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "Mga Sea Star" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "Mga Bumphead" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "Mga Jack" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "Mga Dolphin sa California" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "Gubat ng Kelp sa California" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "Mga Dolphin sa Costa Rica" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "Mga Cownose Ray" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "Mga Gray Reef Shark" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "Humpback Whale" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "Korales ng Red Sea" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "Seals" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "Mga Dikya sa Palau 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "Mga Dikya sa Palau 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "Mga Dikya sa Palau 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "Mga Dikya sa Alaska 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "Mga Dikya sa Alaska 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "Mga Alon sa Tahiti 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "Mga Alon sa Tahiti 2" }, "E334A6D2-7145-47C8-9B00-C20DED08B2D5": { "name": "Ilog Colorado 1" }, "DD266E1F-5DF2-4CDB-A2EB-26CE35664657": { "name": "Burnt Canyon" }, "F9F918CD-E15F-4F01-A326-84A44650C5C9": { "name": "Sunset sa ibabaw ng Grand Canyon" }, "AE0115AE-C53B-4DB9-B12F-CA4B7B630CC9": { "name": "Ilog Colorado 2" }, "8002C4C8-C611-4894-A068-3D3A3C03472A": { "name": "Ilog Colorado 3" }, "5C987900-AD53-469C-8210-CABBCCDDFCAE": { "name": "Cuernos del Paine" }, "B004358B-5A27-42E5-B49E-93FC100B2371": { "name": "Lago Nordenskjöld 1" }, "25A6CFB2-3570-4448-B114-244A4E454B7A": { "name": "Lago Nordenskjöld 2" }, "E5D58CC2-3C52-4206-9DA2-427DC88B5896": { "name": "Torres del Paine" }, "E5799A24-1949-4E66-A17B-B5EB05F28C5D": { "name": "Half Dome" }, "E487C6EF-B3FB-427B-A2BE-8CBA60F902F0": { "name": "Yosemite Park 1" }, "E540DEE6-4C40-42C8-9CCC-D4CB0FAD7D7B": { "name": "Yosemite Park 2" }, "81CA5ACD-E682-4D8B-A948-0F147EB6ED4F": { "name": "Yosemite Park 3" }, "4109D42A-D717-46A7-A9A2-FE53A82B25C0": { "name": "Bridalveil Fall at El Capitan" }, "DAD82DCE-F3AE-4AEC-8A79-1694D412FC0A": { "name": "Tuolumne Meadows" }, "8D04D70F-738B-441D-8D43-AF46B2BF8062": { "name": "Matthes Crest" }, "DDE50C77-B7CB-4488-9EB1-D1B13BF21FFE": { "name": "Tungnaá" }, "E54D5AFE-F362-4D48-A20D-F2C21D2B5330": { "name": "Ilog Jökulgilskvísl" }, "8ACF5D77-B22C-416F-B12A-72FB35E2834F": { "name": "Landmannalaugar" }, "8590D0C5-E344-4FAC-A39A-FD7BC652AEDA": { "name": "Langisjór" }, "2F17FCCE-6CCA-4AFA-A08A-C50BF9812DA5": { "name": "Mýrdalsjökull" }, "F9518D54-04A7-4793-8666-CFC114D73CE5": { "name": "Jökulgil" }, "3954A7C4-51EC-4ABC-ABA3-6757AC91C7CF": { "name": "Loch Moidart 1" }, "0C747C29-4BF8-43F6-A5CC-2E012E555341": { "name": "Isle of Skye" }, "E161929C-0819-4BC2-8359-550C081C7D54": { "name": "Loch Moidart 2" } } ================================================ FILE: Resources/Community/zh_CN.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "韩国和日本的夜晚" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "墨西哥城到纽约" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "加利福尼亚北部到巴哈" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "武陵源国家公园 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "龙脊梯田" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "武陵源国家公园 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "万里长城 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "万里长城 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "万里长城 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "谢赫扎耶德路" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "码头 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "市区" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "码头 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "靠近哈利法塔" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "谢赫扎耶德路" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "努苏阿克半岛" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "伊卢利萨特冰峡湾" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "伊卢利萨特冰峡湾" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "鸟之河山谷" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "熔岩角" }, "b2-2": { "name": "海螺谷" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "海之山自然保护区" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "卡哈拉海岸线" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "海之山自然保护区" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "维多利亚港 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "太平山" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "湾仔" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "维多利亚港 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "维多利亚港" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "利瓦绿洲 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "利瓦绿洲 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "泰晤士河" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "白金汉宫" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "黄昏时刻的泰晤士河" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "黄昏时的泰晤士河" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "洛杉矶国际机场" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "港口高速公路" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "圣塔莫尼卡海滩" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "格里菲斯天文台" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "好莱坞山" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "市区" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "中央公园" }, "b1-3": { "name": "曼哈顿下城" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "上东区" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "第七大道" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "曼哈顿下城" }, "b8-2": { "name": "马林海岬" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "迷雾中的马林海岬" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "海湾和金门" }, "b8-3": { "name": "阿拉莫广场" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "市场街的内河码头" }, "b4-3": { "name": "要塞到金门" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "金门" }, "b6-4": { "name": "旧金山市区和科伊特塔" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "渔人码头" }, "b5-3": { "name": "市场街的内河码头" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "海湾大桥" }, "b2-4": { "name": "旧金山市区和苏特罗塔" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "海湾和内河码头" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "南极洲" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "北美极光" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "非洲之夜" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "非洲和中东" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "加利福尼亚到拉斯维加斯" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "加勒比" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "加勒比日" }, "7825C73A-658F-48EE-B14C-EC56673094AC": { "name": "中国" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "意大利到亚洲" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "伊朗和阿富汗" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "爱尔兰到亚洲" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "西非到阿尔卑斯山" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "新西兰" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "撒哈拉和意大利" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "从大西洋到西班牙和法国" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "澳大利亚" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "从加勒比到中美洲" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "尼罗河三角洲" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "从南非到北亚" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "帕劳珊瑚" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "南非海藻" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "梭鱼" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "海星" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "隆头鹦鹉鱼" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "杰克鱼群" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "加州海豚" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "加州海藻群" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "哥斯达黎加海豚" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "牛鼻魟" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "灰礁鲨" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "座头鲸" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "红海珊瑚" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "海豹" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "帕劳水母 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "帕劳水母 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "帕劳水母 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "阿拉斯加水母 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "阿拉斯加水母 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "大溪地海浪 1" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "大溪地海浪 2" } } ================================================ FILE: Resources/Community/zh_TW.json ================================================ { "009BA758-7060-4479-8EE8-FB9B40C8FB97": { "name": "韓國和日本的夜晚" }, "B1B5DDC5-73C8-4920-8133-BACCE38A08DE": { "name": "墨西哥城到紐約" }, "7719B48A-2005-4011-9280-2F64EEC6FD91": { "name": "加利福尼亞北部到巴哈" }, "B876B645-3955-420E-99DF-60139E451CF3": { "name": "武陵源國家公園 1" }, "9CCB8297-E9F5-4699-AE1F-890CFBD5E29C": { "name": "龍脊梯田" }, "D5E76230-81A3-4F65-A1BA-51B8CADED625": { "name": "武陵源國家公園 2" }, "F0236EC5-EE72-4058-A6CE-1F7D2E8253BF": { "name": "長城 1" }, "22162A9B-DB90-4517-867C-C676BC3E8E95": { "name": "長城 2" }, "044AD56C-A107-41B2-90CC-E60CCACFBCF5": { "name": "長城 3" }, "876D51F4-3D78-4221-8AD2-F9E78C0FD9B9": { "name": "謝赫扎耶德路" }, "E991AC0C-F272-44D8-88F3-05F44EDFE3AE": { "name": "碼頭 1" }, "00BA71CD-2C54-415A-A68A-8358E677D750": { "name": "市中心" }, "3FFA2A97-7D28-49EA-AA39-5BC9051B2745": { "name": "碼頭 2" }, "9680B8EB-CE2A-4395-AF41-402801F4D6A6": { "name": "接近哈利法塔" }, "2F11E857-4F77-4476-8033-4A1E4610AFCC": { "name": "謝赫扎耶德路" }, "B8F204CE-6024-49AB-85F9-7CA2F6DCD226": { "name": "怒蘇阿克半島" }, "2F52E34C-39D4-4AB1-9025-8F7141FAA720": { "name": "伊盧利薩特冰峽灣" }, "EE01F02D-1413-436C-AB05-410F224A5B7B": { "name": "伊盧利薩特冰峽灣" }, "499995FA-E51A-4ACE-8DFD-BDF8AFF6C943": { "name": "鳥之河山谷" }, "12E0343D-2CD9-48EA-AB57-4D680FB6D0C7": { "name": "熔岩角" }, "b2-2": { "name": "海螺谷" }, "258A6797-CC13-4C3A-AB35-4F25CA3BF474": { "name": "海之山自然保護區" }, "3D729CFC-9000-48D3-A052-C5BD5B7A6842": { "name": "卡哈拉海岸線" }, "82BD33C9-B6D2-47E7-9C42-AA3B7758921A": { "name": "海之山自然保護區" }, "FE8E1F9D-59BA-4207-B626-28E34D810D0A": { "name": "維多利亞港 1" }, "C8559883-6F3E-4AF2-8960-903710CD47B7": { "name": "太平山頂" }, "024891DE-B7F6-4187-BFE0-E6D237702EF0": { "name": "灣仔" }, "64EA30BD-C4B5-4CDD-86D7-DFE47E9CB9AA": { "name": "維多利亞港 2" }, "E99FA658-A59A-4A2D-9F3B-58E7BDC71A9A": { "name": "維多利亞港" }, "001C94AE-2BA4-4E77-A202-F7DE60E8B1C8": { "name": "利瓦綠洲 1" }, "AFA22C08-A486-4CE8-9A13-E355B6C38559": { "name": "利瓦綠洲 2" }, "58754319-8709-4AB0-8674-B34F04E7FFE2": { "name": "泰晤士河" }, "A5AAFF5D-8887-42BB-8AFD-867EF557ED85": { "name": "白金漢宮" }, "F604AF56-EA77-4960-AEF7-82533CC1A8B3": { "name": "日落時刻的泰晤士河" }, "7F4C26C2-67C2-4C3A-8F07-8A7BF6148C97": { "name": "黃昏的泰晤士河" }, "CE279831-1CA7-4A83-A97B-FF1E20234396": { "name": "洛杉磯國際機場" }, "35693AEA-F8C4-4A80-B77D-C94B20A68956": { "name": "港口高速公路" }, "92E48DE9-13A1-4172-B560-29B4668A87EE": { "name": "聖塔莫尼卡海灘" }, "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": { "name": "格里菲斯天文台" }, "EC67726A-8212-4C5E-83CF-8412932740D2": { "name": "好萊塢山" }, "F5804DD6-5963-40DA-9FA0-39C0C6E6DEF9": { "name": "市中心" }, "3BA0CFC7-E460-4B59-A817-B97F9EBB9B89": { "name": "中央公園" }, "b1-3": { "name": "曼哈頓下城" }, "840FE8E4-D952-4680-B1A7-AC5BACA2C1F8": { "name": "上東區" }, "44166C39-8566-4ECA-BD16-43159429B52F": { "name": "第七大道" }, "640DFB00-FBB9-45DA-9444-9F663859F4BC": { "name": "曼哈頓下城" }, "b8-2": { "name": "馬林岬角" }, "EE533FBD-90AE-419A-AD13-D7A60E2015D6": { "name": "霧中的馬林岬角" }, "DE851E6D-C2BE-4D9F-AB54-0F9CE994DC51": { "name": "海灣和金門" }, "b8-3": { "name": "阿拉莫廣場" }, "85CE77BF-3413-4A7B-9B0F-732E96229A73": { "name": "市場街的內河碼頭" }, "b4-3": { "name": "要塞到金門" }, "72B4390D-DF1D-4D51-B179-229BBAEFFF2C": { "name": "金門" }, "b6-4": { "name": "市中心和科伊特塔" }, "29BDF297-EB43-403A-8719-A78DA11A2948": { "name": "漁人碼頭" }, "b5-3": { "name": "市場街的內河碼頭" }, "3E94AE98-EAF2-4B09-96E3-452F46BC114E": { "name": "海灣大橋" }, "b2-4": { "name": "市中心和蘇特羅塔" }, "4AD99907-9E76-408D-A7FC-8429FF014201": { "name": "海灣和內河碼頭" }, "03EC0F5E-CCA8-4E0A-9FEC-5BD1CE151182": { "name": "南極洲" }, "737E9E24-49BE-4104-9B72-F352DE1AD2BF": { "name": "北美極光" }, "A837FA8C-C643-4705-AE92-074EFDD067F7": { "name": "非洲之夜" }, "2F72BC1E-3D76-456C-81EB-842EBA488C27": { "name": "非洲和中東" }, "12318CCB-3F78-43B7-A854-EFDCCE5312CD": { "name": "加利福尼亞到拉斯維加斯" }, "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": { "name": "加勒比" }, "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": { "name": "加勒比日" }, "7825C73A-658F-48EE-B14C-EC56673094AC": { "name": "中國" }, "E5DB138A-F04E-4619-B896-DE5CB538C534": { "name": "義大利到亞洲" }, "F439B0A7-D18C-4B14-9681-6520E6A74FE9": { "name": "伊朗和阿富汗" }, "7C643A39-C0B2-4BA0-8BC2-2EAA47CC580E": { "name": "愛爾蘭到亞洲" }, "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": { "name": "西非到阿爾卑斯山" }, "78911B7E-3C69-47AD-B635-9C2486F6301D": { "name": "紐西蘭" }, "E556BBC5-D0A0-4DB1-AC77-BC76E4A526F4": { "name": "撒哈拉和義大利" }, "64D11DAB-3B57-4F14-AD2F-E59A9282FA44": { "name": "從大西洋到西班牙和法國" }, "81337355-E156-4242-AAF4-711768D30A54": { "name": "澳大利亞" }, "1088217C-1410-4CF7-BDE9-8F573A4DBCD9": { "name": "從加勒比到中美洲" }, "3C4678E4-4D3D-4A40-8817-77752AEA62EB": { "name": "尼羅河三角洲" }, "87060EC2-D006-4102-98CC-3005C68BB343": { "name": "從南非到北亞" }, "F07CC61B-30FC-4614-BDAD-3240B61F6793": { "name": "帕勞珊瑚" }, "6143116D-03BB-485E-864E-A8CF58ACF6F1": { "name": "南非海藻" }, "2B30E324-E4FF-4CC1-BA45-A958C2D2B2EC": { "name": "梭魚" }, "581A4F1A-2B6D-468C-A1BE-6F473F06D10B": { "name": "海星" }, "687D03A2-18A5-4181-8E85-38F3A13409B9": { "name": "隆頭鸚鵡魚" }, "537A4DAB-83B0-4B66-BCD1-05E5DBB4A268": { "name": "傑克魚群" }, "27A37B0F-738D-4644-A7A4-E33E7A6C1175": { "name": "加州海豚" }, "EB3F48E7-D30F-4079-858F-1A61331D5026": { "name": "加州海藻群" }, "CE9B5D5B-B6E7-47C5-8C04-59BF182E98FB": { "name": "哥斯達黎加海豚" }, "58C75C62-3290-47B8-849C-56A583173570": { "name": "牛鼻魟" }, "3716DD4B-01C0-4F5B-8DD6-DB771EC472FB": { "name": "灰礁鯊" }, "DD47D8E1-CB66-4C12-BFEA-2ADB0D8D1E2E": { "name": "座頭鯨" }, "82175C1F-153C-4EC8-AE37-2860EA828004": { "name": "紅海珊瑚" }, "391BDF6E-3279-4CE1-9CA5-0F82811452D7": { "name": "海豹" }, "BA4ECA11-592F-4727-9221-D2A32A16EB28": { "name": "帛琉水母 1" }, "E580E5A5-0888-4BE8-A4CA-F74A18A643C3": { "name": "帛琉水母 2" }, "EC3DC957-D4C2-4732-AACE-7D0C0F390EC8": { "name": "帛琉水母 3" }, "C7AD3D0A-7EDF-412C-A237-B3C9D27381A1": { "name": "阿拉斯加水母 1" }, "C6DC4E54-1130-44F8-AF6F-A551D8E8A181": { "name": "阿拉斯加水母 2" }, "149E7795-DBDA-4F5D-B39A-14712F841118": { "name": "大溪地海浪 2" }, "8C31B06F-91A4-4F7C-93ED-56146D7F48B9": { "name": "大溪地海浪 2" } } ================================================ FILE: Resources/MainUI/First time setup/CacheSetupViewController.swift ================================================ // // CacheSetupViewController.swift // Aerial // // Created by Guillaume Louel on 12/08/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class CacheSetupViewController: NSViewController { @IBOutlet var imageView1: NSButton! @IBOutlet var imageView2: NSButton! @IBOutlet var imageView3: NSButton! @IBOutlet var choice1: NSButton! @IBOutlet var choice2: NSButton! @IBOutlet var choice3: NSButton! override func viewDidLoad() { super.viewDidLoad() // Do view setup here. imageView1.setLargeIcon("wand.and.stars") imageView2.setLargeIcon("wand.and.rays") imageView3.setLargeIcon("hand.raised") PrefsCache.enableManagement = true PrefsCache.cachePeriodicity = .weekly } @IBAction func radioChange(_ sender: NSButton) { switch sender { case choice1: PrefsCache.enableManagement = true PrefsCache.cachePeriodicity = .weekly case choice2: PrefsCache.enableManagement = true PrefsCache.cachePeriodicity = .never default: PrefsCache.enableManagement = false } } } ================================================ FILE: Resources/MainUI/First time setup/CacheSetupViewController.xib ================================================ Aerial offers several ways to download videos, from automatically managed to completely manual. Pick what suits you best (you can modify this later, no pressure): New videos will automatically download as needed. They will be replaced periodically to keep things fresh. If you really like a video, you can favorite it so it's never replaced by another one. New videos will automatically download as needed. Nothing gets deleted unless you say so (by hiding a video). You can still set a cache limit to prevent filling up your disk. You can change this setting, set your cache size limit, and restrict downloads to approved WiFi networks at any time in the Cache options ================================================ FILE: Resources/MainUI/First time setup/FirstSetupWindowController.swift ================================================ // // FirstSetupWindowController.swift // Aerial // // Created by Guillaume Louel on 29/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa enum Actions { case welcome, videoFormat, cache } class FirstSetupWindowController: NSWindowController, NSWindowDelegate { var welcomeViewItem: NSSplitViewItem? var videoFormatViewItem: NSSplitViewItem? var cacheSetupViewItem: NSSplitViewItem? var timeViewItem: NSSplitViewItem? var recapViewItem: NSSplitViewItem? var nextViewItem: NSSplitViewItem? lazy var splitVC = NSSplitViewController() var nextVC: NextViewController = { let bundle = Bundle(for: PanelWindowController.self) return NextViewController(nibName: .init("NextViewController"), bundle: bundle) }() var currentStep = 0 override func windowDidLoad() { super.windowDidLoad() splitVC.splitView.isVertical = false if splitVC.splitViewItems.count == 2 { splitVC.removeChild(at: 0) splitVC.removeChild(at: 0) } // We always need to specify a bundle manually, auto loading from bundle // does not work for screen savers when compiled as plugins let bundle = Bundle(for: PanelWindowController.self) let welcomeVC = WelcomeViewController(nibName: .init("WelcomeViewController"), bundle: bundle) let videoVC = VideoFormatViewController(nibName: .init("VideoFormatViewController"), bundle: bundle) let cacheVC = CacheSetupViewController(nibName: .init("CacheSetupViewController"), bundle: bundle) let timeVC = TimeSetupViewController(nibName: .init("TimeSetupViewController"), bundle: bundle) let recapVC = RecapViewController(nibName: .init("RecapViewController"), bundle: bundle) // let nextVC = NextViewController(nibName: .init("NextViewController"), bundle: bundle) nextVC.windowController = self welcomeViewItem = NSSplitViewItem(viewController: welcomeVC) videoFormatViewItem = NSSplitViewItem(viewController: videoVC) cacheSetupViewItem = NSSplitViewItem(viewController: cacheVC) timeViewItem = NSSplitViewItem(viewController: timeVC) recapViewItem = NSSplitViewItem(viewController: recapVC) nextViewItem = NSSplitViewItem(viewController: nextVC) splitVC.addSplitViewItem(welcomeViewItem!) splitVC.addSplitViewItem(nextViewItem!) window?.contentViewController = splitVC } func windowWillClose(_ notification: Notification) { PrefsAdvanced.firstTimeSetup = true } func nextAction() { currentStep += 1 redrawVC() } func previousAction() { currentStep -= 1 redrawVC() } func redrawVC() { splitVC.removeChild(at: 1) splitVC.removeChild(at: 0) switch currentStep { case 0: splitVC.addSplitViewItem(welcomeViewItem!) splitVC.addSplitViewItem(nextViewItem!) nextVC.setNoPrev() case 1: splitVC.addSplitViewItem(videoFormatViewItem!) splitVC.addSplitViewItem(nextViewItem!) nextVC.setPrevNext() case 2: splitVC.addSplitViewItem(cacheSetupViewItem!) splitVC.addSplitViewItem(nextViewItem!) nextVC.setPrevNext() case 3: splitVC.addSplitViewItem(timeViewItem!) splitVC.addSplitViewItem(nextViewItem!) nextVC.setPrevNext() case 4: splitVC.addSplitViewItem(recapViewItem!) splitVC.addSplitViewItem(nextViewItem!) nextVC.setClose() default: window?.close() PrefsAdvanced.firstTimeSetup = true } } } ================================================ FILE: Resources/MainUI/First time setup/FirstSetupWindowController.xib ================================================ ================================================ FILE: Resources/MainUI/First time setup/NextViewController.swift ================================================ // // NextViewController.swift // Aerial // // Created by Guillaume Louel on 29/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class NextViewController: NSViewController { var windowController: FirstSetupWindowController? @IBOutlet var nextButton: NSButton! @IBOutlet var previousButton: NSButton! override func viewDidLoad() { super.viewDidLoad() // Do view setup here. } func setNoPrev() { previousButton.isEnabled = false nextButton.isEnabled = true nextButton.title = "Next" } func setPrevNext() { previousButton.isEnabled = true nextButton.isEnabled = true nextButton.title = "Next" } func setClose() { previousButton.isEnabled = true nextButton.isEnabled = true nextButton.title = "Close" } @IBAction func previousButtonClick(_ sender: Any) { windowController!.previousAction() } @IBAction func nextButtonClick(_ sender: Any) { windowController!.nextAction() } } ================================================ FILE: Resources/MainUI/First time setup/NextViewController.xib ================================================ ================================================ FILE: Resources/MainUI/First time setup/RecapViewController.swift ================================================ // // RecapViewController.swift // Aerial // // Created by Guillaume Louel on 12/08/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class RecapViewController: NSViewController { @IBOutlet var imageDial: NSImageView! @IBOutlet var imageFav: NSImageView! @IBOutlet var imageHide: NSImageView! override func viewDidLoad() { super.viewDidLoad() // Do view setup here. imageDial.image = Aerial.helper.getSymbol("dial")?.tinting(with: .secondaryLabelColor) imageFav.image = Aerial.helper.getSymbol("star")?.tinting(with: .secondaryLabelColor) imageHide.image = Aerial.helper.getSymbol("eye.slash")?.tinting(with: .secondaryLabelColor) } @IBAction func checkFAQ(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://aerialscreensaver.github.io/faq.html")! workspace.open(url) } @IBAction func checkJoshHal(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://www.jetsoncreative.com/aerial")! workspace.open(url) } } ================================================ FILE: Resources/MainUI/First time setup/RecapViewController.xib ================================================ There are more than 100 different videos! You can pick what you want to see in the Now Playing section. You can pick several locations now, or just play everything. Aerial also includes videos shot by multiple contributors. Joshua Michaels & Hal Bergman gifted 20 videos to Aerial. If you enjoy these, consider supporting their work by checking out the additional packs they have made especially for Aerial! ================================================ FILE: Resources/MainUI/First time setup/TimeSetupViewController.swift ================================================ // // TimeSetupViewController.swift // Aerial // // Created by Guillaume Louel on 12/08/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class TimeSetupViewController: NSViewController { @IBOutlet var imageSunrise: NSImageView! @IBOutlet var imageDay: NSImageView! @IBOutlet var imageSunset: NSImageView! @IBOutlet var imageNight: NSImageView! @IBOutlet var imageLocation: NSImageView! @IBOutlet var imageClock: NSImageView! @IBOutlet var imageXmark: NSImageView! @IBOutlet var locationServicesLink: NSButton! @IBOutlet var choice1: NSButton! @IBOutlet var choice2: NSButton! @IBOutlet var choice3: NSButton! @IBOutlet var sunriseTime: NSDatePicker! @IBOutlet var sunsetTime: NSDatePicker! @IBOutlet var locationLabel: NSTextField! lazy var timeFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "HH:mm" return formatter }() override func viewDidLoad() { super.viewDidLoad() // Do view setup here. imageSunrise.image = Aerial.helper.getSymbol("sunrise")?.tinting(with: .secondaryLabelColor) imageDay.image = Aerial.helper.getSymbol("sun.max")?.tinting(with: .secondaryLabelColor) imageSunset.image = Aerial.helper.getSymbol("sunset")?.tinting(with: .secondaryLabelColor) imageNight.image = Aerial.helper.getSymbol("moon.stars")?.tinting(with: .secondaryLabelColor) imageLocation.image = Aerial.helper.getSymbol("mappin.and.ellipse")?.tinting(with: .secondaryLabelColor) imageClock.image = Aerial.helper.getSymbol("clock")?.tinting(with: .secondaryLabelColor) imageXmark.image = Aerial.helper.getSymbol("xmark.circle")?.tinting(with: .secondaryLabelColor) if let dateSunrise = timeFormatter.date(from: PrefsTime.manualSunrise) { sunriseTime.dateValue = dateSunrise } if let dateSunset = timeFormatter.date(from: PrefsTime.manualSunset) { sunsetTime.dateValue = dateSunset } if #available(OSX 10.15, *) { locationServicesLink.isHidden = true } locationLabel.stringValue = "" PrefsTime.timeMode = .disabled } @IBAction func choiceChange(_ sender: NSButton) { switch sender { case choice1: PrefsTime.timeMode = .locationService checkLocation() case choice2: PrefsTime.timeMode = .manual PrefsTime.manualSunrise = timeFormatter.string(from: sunriseTime.dateValue) PrefsTime.manualSunset = timeFormatter.string(from: sunsetTime.dateValue) default: PrefsTime.timeMode = .disabled } } func checkLocation() { // Get the location let location = Locations.sharedInstance locationLabel.stringValue = "Checking your location..." location.getCoordinates(failure: { (_) in // swiftlint:disable:next line_length Aerial.helper.showInfoAlert(title: "Could not get your location", text: "Make sure you enabled location services on your Mac, and that Aerial (or legacyScreenSaver on macOS 10.15 and later) is allowed to use your location.", button1: "OK", caution: true) self.locationLabel.stringValue = "Check your Location Services settings on your mac" }, success: { (_) in // let lat = String(format: "%.2f", coordinates.latitude) // let lon = String(format: "%.2f", coordinates.longitude) _ = TimeManagement.sharedInstance.calculateFromCoordinates() let (sunrise, sunset) = TimeManagement.sharedInstance.getSunriseSunsetForMode(.official) if let vSunrise = sunrise, let vSunset = sunset { self.locationLabel.stringValue = "Next Sunrise : \(self.timeFormatter.string(from: vSunrise)) Next Sunset: \(self.timeFormatter.string(from: vSunset))" } else { self.locationLabel.stringValue = "Cannot calculate sunset and sunrise" } }) } @IBAction func locationServicesClick(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://github.com/JohnCoates/Aerial/blob/master/Documentation/Troubleshooting.md#issues-on-macos-1014-and-earlier")! workspace.open(url) } } ================================================ FILE: Resources/MainUI/First time setup/TimeSetupViewController.xib ================================================ Aerial can use your local time to display a more immersive experience that only shows certain videos during a given time of day. For example, nighttime videos are only shown at night. Your location can be used to precisely calculate your local sunrise and sunset time. Your location isn't shared with anyone. ================================================ FILE: Resources/MainUI/First time setup/VideoFormatViewController.swift ================================================ // // VideoFormatViewController.swift // Aerial // // Created by Guillaume Louel on 11/08/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa import AVKit class VideoFormatViewController: NSViewController { @IBOutlet var videoFormatPopup: NSPopUpButton! @IBOutlet var previewView: AVPlayerView! // We need to hide HDR pre-Catalina @IBOutlet var menu1080pHDR: NSMenuItem! @IBOutlet var menu4KHDR: NSMenuItem! @IBOutlet var labelBelow: NSTextField! var currentVideo: AerialVideo? @IBOutlet var warnImage: NSImageView! @IBOutlet var warnLabel: NSTextField! var originalFormat: VideoFormat? override func viewDidLoad() { super.viewDidLoad() // We need catalina for HDR ! if #available(OSX 10.15, *) { } else { menu1080pHDR.isHidden = true menu4KHDR.isHidden = true } warnLabel.isHidden = true warnImage.isHidden = true // Only detect if we have the default basic format, don't override people's settings if PrefsVideos.videoFormat == .v1080pH264 { PrefsVideos.videoFormat = HardwareDetection.sharedInstance.getSuggestedFormat() } else { labelBelow.stringValue = "Videos are usually available in multiple formats. Your current format is preselected, but you can pick another one." originalFormat = PrefsVideos.videoFormat } videoFormatPopup.selectItem(at: PrefsVideos.videoFormat.rawValue) previewView.player = AVPlayer() previewView.showsFullScreenToggleButton = true // previewView.controlsStyle = .none if #available(OSX 10.10, *) { previewView.videoGravity = .resizeAspectFill } getNewVideo() setupPlayer() } @IBAction func moreInfoFormats(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://github.com/JohnCoates/Aerial/blob/master/Documentation/HardwareDecoding.md")! workspace.open(url) } @IBAction func newVideoClick(_ sender: Any) { getNewVideo() setupPlayer() } @IBAction func formatChange(_ sender: NSPopUpButton) { if let original = originalFormat { let candidateFormat = VideoFormat(rawValue: sender.indexOfSelectedItem)! if candidateFormat != original { warnLabel.isHidden = false warnImage.isHidden = false } else { warnLabel.isHidden = true warnImage.isHidden = true } } PrefsVideos.videoFormat = VideoFormat(rawValue: sender.indexOfSelectedItem)! setupPlayer() } func setupPlayer() { if let player = previewView.player { if let video = currentVideo { player.pause() if let onlineUrl = URL(string: (video.urls[PrefsVideos.videoFormat])!) { let asset = AVAsset(url: onlineUrl) let item = AVPlayerItem(asset: asset) player.replaceCurrentItem(with: item) player.play() } } } } // Get a random video available in all format func getNewVideo() { currentVideo = VideoList.instance.videos.filter({ $0.hasHDR() == true }).shuffled().first } } ================================================ FILE: Resources/MainUI/First time setup/VideoFormatViewController.xib ================================================ Videos are usually available in multiple formats. We've preselected the most energy efficient format for your machine below, but you can pick another one. YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T S2V5ZWRBcmNoaXZlctEICVRyb290gAGvEBcLDBkaIRQmKywzNjs+P0RHSEtVXV5iZVUkbnVsbNYNDg8Q ERITFBUWFxhWTlNTaXplXk5TUmVzaXppbmdNb2RlViRjbGFzc1xOU0ltYWdlRmxhZ3NWTlNSZXBzV05T Q29sb3KAAhAAgBYSIMMAAIADgBFWezEsIDF90hsPHCBaTlMub2JqZWN0c6MdHh+ABIAKgA2AENIbDyIl oiMkgAWABoAJ0w8nKCkqFF8QFE5TVElGRlJlcHJlc2VudGF0aW9uXxAZTlNJbnRlcm5hbExheW91dERp cmVjdGlvboAIgAdPEQ0qTU0AKgAAAAwAAAAAABABAAADAAAAAQABAAABAQADAAAAAQABAAABAgADAAAA BAAAANIBAwADAAAAAQABAAABBgADAAAAAQACAAABCgADAAAAAQABAAABEQAEAAAAAQAAAAgBEgADAAAA AQABAAABFQADAAAAAQAEAAABFgADAAAAAQABAAABFwAEAAAAAQAAAAQBHAADAAAAAQABAAABKAADAAAA AQACAAABUgADAAAAAQABAAABUwADAAAABAAAANqHcwAHAAAMSAAAAOIAAAAAAAgACAAIAAgAAQABAAEA AQAADEhMaW5vAhAAAG1udHJSR0IgWFlaIAfOAAIACQAGADEAAGFjc3BNU0ZUAAAAAElFQyBzUkdCAAAA AAAAAAAAAAAAAAD21gABAAAAANMtSFAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAEWNwcnQAAAFQAAAAM2Rlc2MAAAGEAAAAbHd0cHQAAAHwAAAAFGJrcHQAAAIEAAAA FHJYWVoAAAIYAAAAFGdYWVoAAAIsAAAAFGJYWVoAAAJAAAAAFGRtbmQAAAJUAAAAcGRtZGQAAALEAAAA iHZ1ZWQAAANMAAAAhnZpZXcAAAPUAAAAJGx1bWkAAAP4AAAAFG1lYXMAAAQMAAAAJHRlY2gAAAQwAAAA DHJUUkMAAAQ8AAAIDGdUUkMAAAQ8AAAIDGJUUkMAAAQ8AAAIDHRleHQAAAAAQ29weXJpZ2h0IChjKSAx OTk4IEhld2xldHQtUGFja2FyZCBDb21wYW55AABkZXNjAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAA AAAAAAAAAAAAEnNSR0IgSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAA81EAAQAAAAEWzFhZWiAAAAAAAAAAAAAAAAAAAAAAWFla IAAAAAAAAG+iAAA49QAAA5BYWVogAAAAAAAAYpkAALeFAAAY2lhZWiAAAAAAAAAkoAAAD4QAALbPZGVz YwAAAAAAAAAWSUVDIGh0dHA6Ly93d3cuaWVjLmNoAAAAAAAAAAAAAAAWSUVDIGh0dHA6Ly93d3cuaWVj LmNoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRlc2MAAAAAAAAA LklFQyA2MTk2Ni0yLjEgRGVmYXVsdCBSR0IgY29sb3VyIHNwYWNlIC0gc1JHQgAAAAAAAAAAAAAALklF QyA2MTk2Ni0yLjEgRGVmYXVsdCBSR0IgY29sb3VyIHNwYWNlIC0gc1JHQgAAAAAAAAAAAAAAAAAAAAAA AAAAAABkZXNjAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRpb24gaW4gSUVDNjE5NjYtMi4x AAAAAAAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAdmlldwAAAAAAE6T+ABRfLgAQzxQAA+3MAAQTCwADXJ4AAAABWFla IAAAAAAATAlWAFAAAABXH+dtZWFzAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAACjwAAAAJzaWcgAAAA AENSVCBjdXJ2AAAAAAAABAAAAAAFAAoADwAUABkAHgAjACgALQAyADcAOwBAAEUASgBPAFQAWQBeAGMA aABtAHIAdwB8AIEAhgCLAJAAlQCaAJ8ApACpAK4AsgC3ALwAwQDGAMsA0ADVANsA4ADlAOsA8AD2APsB AQEHAQ0BEwEZAR8BJQErATIBOAE+AUUBTAFSAVkBYAFnAW4BdQF8AYMBiwGSAZoBoQGpAbEBuQHBAckB 0QHZAeEB6QHyAfoCAwIMAhQCHQImAi8COAJBAksCVAJdAmcCcQJ6AoQCjgKYAqICrAK2AsECywLVAuAC 6wL1AwADCwMWAyEDLQM4A0MDTwNaA2YDcgN+A4oDlgOiA64DugPHA9MD4APsA/kEBgQTBCAELQQ7BEgE VQRjBHEEfgSMBJoEqAS2BMQE0wThBPAE/gUNBRwFKwU6BUkFWAVnBXcFhgWWBaYFtQXFBdUF5QX2BgYG FgYnBjcGSAZZBmoGewaMBp0GrwbABtEG4wb1BwcHGQcrBz0HTwdhB3QHhgeZB6wHvwfSB+UH+AgLCB8I MghGCFoIbgiCCJYIqgi+CNII5wj7CRAJJQk6CU8JZAl5CY8JpAm6Cc8J5Qn7ChEKJwo9ClQKagqBCpgK rgrFCtwK8wsLCyILOQtRC2kLgAuYC7ALyAvhC/kMEgwqDEMMXAx1DI4MpwzADNkM8w0NDSYNQA1aDXQN jg2pDcMN3g34DhMOLg5JDmQOfw6bDrYO0g7uDwkPJQ9BD14Peg+WD7MPzw/sEAkQJhBDEGEQfhCbELkQ 1xD1ERMRMRFPEW0RjBGqEckR6BIHEiYSRRJkEoQSoxLDEuMTAxMjE0MTYxODE6QTxRPlFAYUJxRJFGoU ixStFM4U8BUSFTQVVhV4FZsVvRXgFgMWJhZJFmwWjxayFtYW+hcdF0EXZReJF64X0hf3GBsYQBhlGIoY rxjVGPoZIBlFGWsZkRm3Gd0aBBoqGlEadxqeGsUa7BsUGzsbYxuKG7Ib2hwCHCocUhx7HKMczBz1HR4d Rx1wHZkdwx3sHhYeQB5qHpQevh7pHxMfPh9pH5Qfvx/qIBUgQSBsIJggxCDwIRwhSCF1IaEhziH7Iici VSKCIq8i3SMKIzgjZiOUI8Ij8CQfJE0kfCSrJNolCSU4JWgllyXHJfcmJyZXJocmtyboJxgnSSd6J6sn 3CgNKD8ocSiiKNQpBik4KWspnSnQKgIqNSpoKpsqzysCKzYraSudK9EsBSw5LG4soizXLQwtQS12Last 4S4WLkwugi63Lu4vJC9aL5Evxy/+MDUwbDCkMNsxEjFKMYIxujHyMioyYzKbMtQzDTNGM38zuDPxNCs0 ZTSeNNg1EzVNNYc1wjX9Njc2cjauNuk3JDdgN5w31zgUOFA4jDjIOQU5Qjl/Obw5+To2OnQ6sjrvOy07 azuqO+g8JzxlPKQ84z0iPWE9oT3gPiA+YD6gPuA/IT9hP6I/4kAjQGRApkDnQSlBakGsQe5CMEJyQrVC 90M6Q31DwEQDREdEikTORRJFVUWaRd5GIkZnRqtG8Ec1R3tHwEgFSEtIkUjXSR1JY0mpSfBKN0p9SsRL DEtTS5pL4kwqTHJMuk0CTUpNk03cTiVObk63TwBPSU+TT91QJ1BxULtRBlFQUZtR5lIxUnxSx1MTU19T qlP2VEJUj1TbVShVdVXCVg9WXFapVvdXRFeSV+BYL1h9WMtZGllpWbhaB1pWWqZa9VtFW5Vb5Vw1XIZc 1l0nXXhdyV4aXmxevV8PX2Ffs2AFYFdgqmD8YU9homH1YklinGLwY0Njl2PrZEBklGTpZT1lkmXnZj1m kmboZz1nk2fpaD9olmjsaUNpmmnxakhqn2r3a09rp2v/bFdsr20IbWBtuW4SbmtuxG8eb3hv0XArcIZw 4HE6cZVx8HJLcqZzAXNdc7h0FHRwdMx1KHWFdeF2Pnabdvh3VnezeBF4bnjMeSp5iXnnekZ6pXsEe2N7 wnwhfIF84X1BfaF+AX5ifsJ/I3+Ef+WAR4CogQqBa4HNgjCCkoL0g1eDuoQdhICE44VHhauGDoZyhteH O4efiASIaYjOiTOJmYn+imSKyoswi5aL/IxjjMqNMY2Yjf+OZo7OjzaPnpAGkG6Q1pE/kaiSEZJ6kuOT TZO2lCCUipT0lV+VyZY0lp+XCpd1l+CYTJi4mSSZkJn8mmia1ZtCm6+cHJyJnPedZJ3SnkCerp8dn4uf +qBpoNihR6G2oiailqMGo3aj5qRWpMelOKWpphqmi6b9p26n4KhSqMSpN6mpqhyqj6sCq3Wr6axcrNCt RK24ri2uoa8Wr4uwALB1sOqxYLHWskuywrM4s660JbSctRO1irYBtnm28Ldot+C4WbjRuUq5wro7urW7 LrunvCG8m70VvY++Cr6Evv+/er/1wHDA7MFnwePCX8Lbw1jD1MRRxM7FS8XIxkbGw8dBx7/IPci8yTrJ uco4yrfLNsu2zDXMtc01zbXONs62zzfPuNA50LrRPNG+0j/SwdNE08bUSdTL1U7V0dZV1tjXXNfg2GTY 6Nls2fHadtr724DcBdyK3RDdlt4c3qLfKd+v4DbgveFE4cziU+Lb42Pj6+Rz5PzlhOYN5pbnH+ep6DLo vOlG6dDqW+rl63Dr++yG7RHtnO4o7rTvQO/M8Fjw5fFy8f/yjPMZ86f0NPTC9VD13vZt9vv3ivgZ+Kj5 OPnH+lf65/t3/Af8mP0p/br+S/7c/23//9ItLi8wWiRjbGFzc25hbWVYJGNsYXNzZXNfEBBOU0JpdG1h cEltYWdlUmVwoy8xMlpOU0ltYWdlUmVwWE5TT2JqZWN00i0uNDVXTlNBcnJheaI0MtIbDzcloiM5gAWA C4AJ0w8nKCk9FIAIgAxPEQ1eTU0AKgAAABgAAAAAAAAAAAAAAAAAAAAAABIBAAADAAAAAQACAAABAQAD AAAAAQACAAABAgADAAAABAAAAQYBAwADAAAAAQABAAABBgADAAAAAQACAAABCgADAAAAAQABAAABEQAE AAAAAQAAAAgBEgADAAAAAQABAAABFQADAAAAAQAEAAABFgADAAAAAQACAAABFwAEAAAAAQAAABABGgAF AAAAAQAAAPYBGwAFAAAAAQAAAP4BHAADAAAAAQABAAABKAADAAAAAQACAAABUgADAAAAAQABAAABUwAD AAAABAAAAQ6HcwAHAAAMSAAAARYAAAAAAAAAkAAAAAEAAACQAAAAAQAIAAgACAAIAAEAAQABAAEAAAxI TGlubwIQAABtbnRyUkdCIFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAA AAAAAAAA9tYAAQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAABFjcHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFla AAACGAAAABRnWFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVk AAADTAAAAIZ2aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJD AAAEPAAACAxnVFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5OCBI ZXdsZXR0LVBhY2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEAAAAAAAAA AAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAA AABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z2Rlc2MAAAAA AAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkZXNjAAAAAAAAAC5JRUMg NjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5 NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAA AAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAAABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAA AEwJVgBQAAAAVx/nbWVhcwAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQg Y3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQBy AHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwEN ARMBGQEfASUBKwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHh AekB8gH6AgMCDAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMA AwsDFgMhAy0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRx BH4EjASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3 BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDIIRgha CG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqYCq4KxQrc CvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3D Dd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBhEH4QmxC5ENcQ9RET ETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT5RQGFCcUSRRqFIsUrRTO FPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReuF9IX9xgbGEAYZRiKGK8Y1Rj6 GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9ocAhwqHFIcexyjHMwc9R0eHUcdcB2Z HcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCYIMQg8CEcIUghdSGhIc4h+yInIlUigiKv It0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZclxyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/ KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5M LoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIxSjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTY NRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvo PCc8ZTykPOM9Ij1hPaE94D4gPmA+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9 Q8BEA0RHRIpEzkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0ua S+JMKkxyTLpNAk1KTZNN3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRC VI9U21UoVXVVwlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114 XcleGl5sXr1fD19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9 Z5Nn6Wg/aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGV cfByS3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyB fOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gE iGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLjk02TtpQg lIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDY oUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1q+msXKzQrUStuK4t rqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm40blKucK6O7q1uy67p7wh vJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZGxsPHQce/yD3IvMk6ybnKOMq3 yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx 2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ 6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX +uf7d/wH/Jj9Kf26/kv+3P9t///SGw9AJaIjQoAFgA6ACdMPJygpRhSACIAPTxENck1NACoAAAAsAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIBAAADAAAAAQADAAABAQADAAAAAQADAAAB AgADAAAABAAAARoBAwADAAAAAQABAAABBgADAAAAAQACAAABCgADAAAAAQABAAABEQAEAAAAAQAAAAgB EgADAAAAAQABAAABFQADAAAAAQAEAAABFgADAAAAAQADAAABFwAEAAAAAQAAACQBGgAFAAAAAQAAAQoB GwAFAAAAAQAAARIBHAADAAAAAQABAAABKAADAAAAAQACAAABUgADAAAAAQABAAABUwADAAAABAAAASKH cwAHAAAMSAAAASoAAAAAAAAA2AAAAAEAAADYAAAAAQAIAAgACAAIAAEAAQABAAEAAAxITGlubwIQAABt bnRyUkdCIFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYA AQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFj cHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFlaAAACGAAAABRn WFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVkAAADTAAAAIZ2 aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJDAAAEPAAACAxn VFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5OCBIZXdsZXR0LVBh Y2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAABJzUkdC IElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUA AAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z2Rlc2MAAAAAAAAAFklFQyBo dHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkZXNjAAAAAAAAAC5JRUMgNjE5NjYtMi4x IERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERl ZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAA AAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAALFJl ZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAHZpZXcAAAAAABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAAAEwJVgBQAAAA Vx/nbWVhcwAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAA AAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYA iwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUB KwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMC DAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMhAy0D OANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4EjASaBKgE tgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3BkgGWQZqBnsG jAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDIIRghaCG4IggiWCKoI vgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqYCq4KxQrcCvMLCwsiCzkL UQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3DDd4N+A4TDi4O SQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBhEH4QmxC5ENcQ9RETETERTxFtEYwR qhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYV eBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReuF9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZ txndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9ocAhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAe ah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCYIMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2Yj lCPCI/AkHyRNJHwkqyTaJQklOCVoJZclxyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYp OClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQv Wi+RL8cv/jA1MGwwpDDbMRIxSjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1 /TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9 Ij1hPaE94D4gPmA+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpE zkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpN Ak1KTZNN3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVV wlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1f D19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/aJZo 7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfByS3KmcwFz XXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyBfOF9QX2hfgF+ Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gEiGmIzokziZmJ /opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLjk02TtpQglIqU9JVflcmW NJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDYoUehtqImopaj BqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1q+msXKzQrUStuK4trqGvFq+LsACw dbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm40blKucK6O7q1uy67p7whvJu9Fb2Pvgq+ hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZGxsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXN Nc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXc it0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vs hu0R7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9 Kf26/kv+3P9t///SLS5JSl5OU011dGFibGVBcnJheaNJNDLVTE1OTw9QUVJTVFdOU1doaXRlXE5TQ29t cG9uZW50c1xOU0NvbG9yU3BhY2VfEBJOU0N1c3RvbUNvbG9yU3BhY2VEMCAwAEMwIDAQA4ASgBXUVldY D1laW1xUTlNJRFVOU0lDQ1dOU01vZGVsEAmAExAAgBRPERGcAAARnGFwcGwCAAAAbW50ckdSQVlYWVog B9wACAAXAA8ALgAPYWNzcEFQUEwAAAAAbm9uZQAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1hcHBs AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFZGVzYwAAAMAAAAB5 ZHNjbQAAATwAAAgaY3BydAAACVgAAAAjd3RwdAAACXwAAAAUa1RSQwAACZAAAAgMZGVzYwAAAAAAAAAf R2VuZXJpYyBHcmF5IEdhbW1hIDIuMiBQcm9maWxlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG1sdWMAAAAA AAAAHwAAAAxza1NLAAAALgAAAYRkYURLAAAAOgAAAbJjYUVTAAAAOAAAAex2aVZOAAAAQAAAAiRwdEJS AAAASgAAAmR1a1VBAAAALAAAAq5mckZVAAAAPgAAAtpodUhVAAAANAAAAxh6aFRXAAAAGgAAA0xrb0tS AAAAIgAAA2ZuYk5PAAAAOgAAA4hjc0NaAAAAKAAAA8JoZUlMAAAAJAAAA+pyb1JPAAAAKgAABA5kZURF AAAATgAABDhpdElUAAAATgAABIZzdlNFAAAAOAAABNR6aENOAAAAGgAABQxqYUpQAAAAJgAABSZlbEdS AAAAKgAABUxwdFBPAAAAUgAABXZubE5MAAAAQAAABchlc0VTAAAATAAABgh0aFRIAAAAMgAABlR0clRS AAAAJAAABoZmaUZJAAAARgAABqpockhSAAAAPgAABvBwbFBMAAAASgAABy5hckVHAAAALAAAB3hydVJV AAAAOgAAB6RlblVTAAAAPAAAB94AVgFhAGUAbwBiAGUAYwBuAOEAIABzAGkAdgDhACAAZwBhAG0AYQAg ADIALAAyAEcAZQBuAGUAcgBpAHMAawAgAGcAcgDlACAAMgAsADIAIABnAGEAbQBtAGEALQBwAHIAbwBm AGkAbABHAGEAbQBtAGEAIABkAGUAIABnAHIAaQBzAG8AcwAgAGcAZQBuAOgAcgBpAGMAYQAgADIALgAy AEMepQB1ACAAaADsAG4AaAAgAE0A4AB1ACAAeADhAG0AIABDAGgAdQBuAGcAIABHAGEAbQBtAGEAIAAy AC4AMgBQAGUAcgBmAGkAbAAgAEcAZQBuAOkAcgBpAGMAbwAgAGQAYQAgAEcAYQBtAGEAIABkAGUAIABD AGkAbgB6AGEAcwAgADIALAAyBBcEMAQzBDAEOwRMBD0EMAAgAEcAcgBhAHkALQQzBDAEPAQwACAAMgAu ADIAUAByAG8AZgBpAGwAIABnAOkAbgDpAHIAaQBxAHUAZQAgAGcAcgBpAHMAIABnAGEAbQBtAGEAIAAy ACwAMgDBAGwAdABhAGwA4QBuAG8AcwAgAHMAegD8AHIAawBlACAAZwBhAG0AbQBhACAAMgAuADKQGnUo cHCWjlFJXqYAMgAuADKCcl9pY8+P8Md8vBgAINaMwMkAIKwQucgAIAAyAC4AMgAg1QS4XNMMx3wARwBl AG4AZQByAGkAcwBrACAAZwByAOUAIABnAGEAbQBtAGEAIAAyACwAMgAtAHAAcgBvAGYAaQBsAE8AYgBl AGMAbgDhACABYQBlAGQA4QAgAGcAYQBtAGEAIAAyAC4AMgXSBdAF3gXUACAF0AXkBdUF6AAgBdsF3AXc BdkAIAAyAC4AMgBHAGEAbQBhACAAZwByAGkAIABnAGUAbgBlAHIAaQBjAQMAIAAyACwAMgBBAGwAbABn AGUAbQBlAGkAbgBlAHMAIABHAHIAYQB1AHMAdAB1AGYAZQBuAC0AUAByAG8AZgBpAGwAIABHAGEAbQBt AGEAIAAyACwAMgBQAHIAbwBmAGkAbABvACAAZwByAGkAZwBpAG8AIABnAGUAbgBlAHIAaQBjAG8AIABk AGUAbABsAGEAIABnAGEAbQBtAGEAIAAyACwAMgBHAGUAbgBlAHIAaQBzAGsAIABnAHIA5QAgADIALAAy ACAAZwBhAG0AbQBhAHAAcgBvAGYAaQBsZm6QGnBwXqZ8+2VwADIALgAyY8+P8GWHTvZOAIIsMLAw7DCk MKww8zDeACAAMgAuADIAIDDXMO0w1TChMKQw6wOTA7UDvQO5A7oDzAAgA5MDugPBA7kAIAOTA6wDvAO8 A7EAIAAyAC4AMgBQAGUAcgBmAGkAbAAgAGcAZQBuAOkAcgBpAGMAbwAgAGQAZQAgAGMAaQBuAHoAZQBu AHQAbwBzACAAZABhACAARwBhAG0AbQBhACAAMgAsADIAQQBsAGcAZQBtAGUAZQBuACAAZwByAGkAagBz ACAAZwBhAG0AbQBhACAAMgAsADIALQBwAHIAbwBmAGkAZQBsAFAAZQByAGYAaQBsACAAZwBlAG4A6QBy AGkAYwBvACAAZABlACAAZwBhAG0AbQBhACAAZABlACAAZwByAGkAcwBlAHMAIAAyACwAMg4jDjEOBw4q DjUOQQ4BDiEOIQ4yDkAOAQ4jDiIOTA4XDjEOSA4nDkQOGwAgADIALgAyAEcAZQBuAGUAbAAgAEcAcgBp ACAARwBhAG0AYQAgADIALAAyAFkAbABlAGkAbgBlAG4AIABoAGEAcgBtAGEAYQBuACAAZwBhAG0AbQBh ACAAMgAsADIAIAAtAHAAcgBvAGYAaQBpAGwAaQBHAGUAbgBlAHIAaQENAGsAaQAgAEcAcgBhAHkAIABH AGEAbQBtAGEAIAAyAC4AMgAgAHAAcgBvAGYAaQBsAFUAbgBpAHcAZQByAHMAYQBsAG4AeQAgAHAAcgBv AGYAaQBsACAAcwB6AGEAcgBvAVsAYwBpACAAZwBhAG0AbQBhACAAMgAsADIGOgYnBkUGJwAgADIALgAy ACAGRAZIBkYAIAYxBkUGJwYvBkoAIAY5BicGRQQeBDEESQQwBE8AIARBBDUEQAQwBE8AIAQzBDAEPAQ8 BDAAIAAyACwAMgAtBD8EQAQ+BEQEOAQ7BEwARwBlAG4AZQByAGkAYwAgAEcAcgBhAHkAIABHAGEAbQBt AGEAIAAyAC4AMgAgAFAAcgBvAGYAaQBsAGUAAHRleHQAAAAAQ29weXJpZ2h0IEFwcGxlIEluYy4sIDIw MTIAAFhZWiAAAAAAAADzUQABAAAAARbMY3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3 ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDL ANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGD AYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMCDAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKE Ao4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMhAy0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPT A+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4EjASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3 BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0 B4YHmQesB78H0gflB/gICwgfCDIIRghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnP CeUJ+woRCicKPQpUCmoKgQqYCq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyO DKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+z D88P7BAJECYQQxBhEH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxND E2MTgxOkE8UT5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdB F2UXiReuF9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2Mbihuy G9ocAhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCY IMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZclxyX3 JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvR LAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIxSjGCMbox8jIq MmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkF OUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+oD7gPyE/YT+iP+JAI0Bk QKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhL SJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN3E4lTm5Ot08AT0lPk0/dUCdQcVC7 UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4 WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1fD19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GND Y5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1g bbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfByS3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gR eG54zHkqeYl553pGeqV7BHtje8J8IXyBfOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INX g7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82 j56QBpBukNaRP5GokhGSepLjk02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuv nByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjE qTepqaocqo+rAqt1q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5 tvC3aLfguFm40blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTO xUvFyMZGxsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG 1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj 4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn 9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t///SLS5fYFxOU0NvbG9yU3Bh Y2WiYTJcTlNDb2xvclNwYWNl0i0uY2RXTlNDb2xvcqJjMtItLmZnV05TSW1hZ2WiZjIACAARABoAJAAp ADIANwBJAEwAUQBTAG0AcwCAAIcAlgCdAKoAsQC5ALsAvQC/AMQAxgDIAM8A1ADfAOMA5QDnAOkA6wDw APMA9QD3APkBAAEXATMBNQE3DmUOag51Dn4OkQ6VDqAOqQ6uDrYOuQ6+DsEOww7FDscOzg7QDtIcNBw5 HDwcPhxAHEIcSRxLHE0pwynIKdcp2ynmKe4p+yoIKh0qIiomKigqKiosKjUqOipAKkgqSipMKk4qUDvw O/U8AjwFPBI8FzwfPCI8JzwvAAAAAAAAAgEAAAAAAAAAaAAAAAAAAAAAAAAAAAAAPDI ================================================ FILE: Resources/MainUI/First time setup/WelcomeViewController.swift ================================================ // // WelcomeViewController.swift // Aerial // // Created by Guillaume Louel on 29/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class WelcomeViewController: NSViewController { @IBOutlet var bigTitle: NSTextField! @IBOutlet var textBelow: NSTextField! override func viewDidLoad() { super.viewDidLoad() // Do view setup here. if PrefsVideos.videoFormat != .v1080pH264 { bigTitle.stringValue = "Welcome back to Aerial" textBelow.stringValue = "We've changed a thing or two, so let's go over that!" } } } ================================================ FILE: Resources/MainUI/First time setup/WelcomeViewController.xib ================================================ Just a couple questions to adjust your settings, it will be quick! ================================================ FILE: Resources/MainUI/Infos panels/CreditsViewController.swift ================================================ // // CreditsViewController.swift // Aerial // // Created by Guillaume Louel on 25/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class CreditsViewController: NSViewController { @IBOutlet var scrollView: NSScrollView! override func viewDidLoad() { super.viewDidLoad() scrollView.contentView.scroll(NSMakePoint(0,0)) } @IBAction func translationButton(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://github.com/JohnCoates/Aerial/issues/792")! workspace.open(url) } @IBAction func websiteButton(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://aerialscreensaver.github.io")! workspace.open(url) } @IBAction func projectButton(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://github.com/JohnCoates/Aerial")! workspace.open(url) } @IBAction func discordButton(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://discord.gg/TPuA5WG")! workspace.open(url) } } ================================================ FILE: Resources/MainUI/Infos panels/CreditsViewController.xib ================================================ Aerial was started in 2015 by John Coates. Since version 1.4, Aerial is developed and maintained by Guillaume Louel. Translations contributed by: - Arabic by Kachi Kulu - Dutch by Sebastiaan Speck - English by Sean M Smith - German by Matthias Bauer & Thomas W. - Hebrew by Bry Jacks - Hungarian by Matt Crogan - Italian by Angelo Marguglio - Japanese by Junji 'Dr.MORO' Morokuma - Korean by Howon Kim - Polish by Aleksander Kasprzyk - Portuguese/Brazil by Renan Sigolo - Russian by Tema Sysoev and - Simplified & Traditional Chinese by Linkey Leo - Spanish by Aitor García Rey - Swedish by Samuel Holm Want to help with translations? ================================================ FILE: Resources/MainUI/Infos panels/HelpViewController.swift ================================================ // // HelpViewController.swift // Aerial // // Created by Guillaume Louel on 25/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class HelpViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() // Do view setup here. } @IBAction func faqButton(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://aerialscreensaver.github.io/faq.html")! workspace.open(url) } @IBAction func troubleshootButton(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://aerialscreensaver.github.io/troubleshooting.html")! workspace.open(url) } @IBAction func issuesButton(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://github.com/JohnCoates/Aerial/issues")! workspace.open(url) } @IBAction func visitDiscordClick(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://discord.gg/TPuA5WG")! workspace.open(url) } } ================================================ FILE: Resources/MainUI/Infos panels/HelpViewController.xib ================================================ Can't find what you are looking for? Then please check our issues pages on GitHub, you may find an answer to your question and if you don't, you can ask your question there. ================================================ FILE: Resources/MainUI/Infos panels/InfoViewController.swift ================================================ // // InfoViewController.swift // Aerial // // Created by Guillaume Louel on 17/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class InfoViewController: NSViewController { @IBOutlet var versionLabel: NSTextField! override func viewDidLoad() { super.viewDidLoad() versionLabel.stringValue = Aerial.helper.version } @IBAction func donateButton(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://ko-fi.com/A0A32385Y")! workspace.open(url) } @IBAction func iconWebsiteButton(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://infernodesign.com")! workspace.open(url) } } ================================================ FILE: Resources/MainUI/Infos panels/InfoViewController.xib ================================================ A free, open source, macOS Screen Saver developed and maintained by Guillaume Louel. Enjoying Aerial? ================================================ FILE: Resources/MainUI/PanelWindowController.swift ================================================ // // PanelWindowController.swift // Aerial // // Created by Guillaume Louel on 15/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa @objc class PanelWindowController: NSWindowController { var firstSetupWindowController: FirstSetupWindowController? var splitVC: NSSplitViewController? var videosVC: VideosViewController? var sidebarVC: SidebarViewController? var nowPlayingItem: NSSplitViewItem? // New main view var videoViewItem: NSSplitViewItem? // Main video view // Infos var infoViewItem: NSSplitViewItem? var creditsViewItem: NSSplitViewItem? var helpViewItem: NSSplitViewItem? // Settings var sourcesViewItem: NSSplitViewItem? var timeViewItem: NSSplitViewItem? var displaysViewItem: NSSplitViewItem? var brightnessViewItem: NSSplitViewItem? var cacheViewItem: NSSplitViewItem? var overlaysViewItem: NSSplitViewItem? var filtersViewItem: NSSplitViewItem? var advancedViewItem: NSSplitViewItem? var currentPath: String? convenience init() { self.init(windowNibName: "PanelWindowController") } override func windowDidLoad() { // debugLog("PWC2 wdl: Aerial version \(Aerial.version)") super.windowDidLoad() currentPath = "location:all" splitVC = NSSplitViewController() // This is the core of ui V2, we dynamically change the right view controller based on what's on the left // We always need to specify a bundle manually, auto loading from bundle // does not work for screen savers when compiled as plugins let bundle = Bundle(for: PanelWindowController.self) videosVC = VideosViewController(nibName: .init("VideosViewController"), bundle: bundle) let nowPlayingVC = NowPlayingViewController(nibName: .init("NowPlayingViewController"), bundle: bundle) // Infos let infoVC = InfoViewController(nibName: .init("InfoViewController"), bundle: bundle) let creditsVC = CreditsViewController(nibName: .init("CreditsViewController"), bundle: bundle) let helpVC = HelpViewController(nibName: .init("HelpViewController"), bundle: bundle) // Various settings let sourcesVC = SourcesViewController(nibName: .init("SourcesViewController"), bundle: bundle) let timeVC = TimeViewController(nibName: .init("TimeViewController"), bundle: bundle) let displaysVC = DisplaysViewController(nibName: .init("DisplaysViewController"), bundle: bundle) let brightnessVC = BrightnessViewController(nibName: .init("BrightnessViewController"), bundle: bundle) let cacheVC = CacheViewController(nibName: .init("CacheViewController"), bundle: bundle) let companionCacheVC = CompanionCacheViewController(nibName: .init("CompanionCacheViewController"), bundle: bundle) let overlaysVC = OverlaysViewController(nibName: .init("OverlaysViewController"), bundle: bundle) let filtersVC = FiltersViewController(nibName: .init("FiltersViewController"), bundle: bundle) let advancedVC = AdvancedViewController(nibName: .init("AdvancedViewController"), bundle: bundle) advancedVC.windowController = self // We do the sidebar last, as it bubble up events to the other ones sidebarVC = SidebarViewController(nibName: .init("SidebarViewController"), bundle: bundle) sidebarVC!.windowController = self // Also set the Aerial helper Aerial.helper.windowController = self // Create all the view items for the right panel nowPlayingItem = NSSplitViewItem(viewController: nowPlayingVC) videoViewItem = NSSplitViewItem(viewController: videosVC!) // Infos infoViewItem = NSSplitViewItem(viewController: infoVC) creditsViewItem = NSSplitViewItem(viewController: creditsVC) helpViewItem = NSSplitViewItem(viewController: helpVC) // All the settings have individual controllers sourcesViewItem = NSSplitViewItem(viewController: sourcesVC) timeViewItem = NSSplitViewItem(viewController: timeVC) displaysViewItem = NSSplitViewItem(viewController: displaysVC) brightnessViewItem = NSSplitViewItem(viewController: brightnessVC) if Aerial.helper.underCompanion { cacheViewItem = NSSplitViewItem(viewController: companionCacheVC) } else { cacheViewItem = NSSplitViewItem(viewController: cacheVC) } overlaysViewItem = NSSplitViewItem(viewController: overlaysVC) filtersViewItem = NSSplitViewItem(viewController: filtersVC) advancedViewItem = NSSplitViewItem(viewController: advancedVC) splitVC!.addSplitViewItem(NSSplitViewItem(sidebarWithViewController: sidebarVC!)) splitVC!.addSplitViewItem(videoViewItem!) splitVC!.splitViewItems[0].canCollapse = false splitVC!.splitViewItems[1].canCollapse = false window?.contentViewController = splitVC // PrefsAdvanced.firstTimeSetup = false doFirstTimeSetup() debugLog("/PWC2 wdl") } func stopVideo() { videosVC?.stopVideo() } func doFirstTimeSetup() { if PrefsAdvanced.firstTimeSetup == false && firstSetupWindowController == nil { let bundle = Bundle(for: PanelWindowController.self) // We also load our CustomVideos nib here firstSetupWindowController = FirstSetupWindowController() var topLevelObjects: NSArray? = NSArray() if !bundle.loadNibNamed(NSNib.Name("FirstSetupWindowController"), owner: firstSetupWindowController, topLevelObjects: &topLevelObjects) { errorLog("Could not load nib for CustomVideos, please report") } DispatchQueue.main.async { self.firstSetupWindowController!.windowDidLoad() self.firstSetupWindowController!.showWindow(self) self.firstSetupWindowController!.window!.makeKeyAndOrderFront(self) } } } // Switch from one menu list to another func switchFrom(_ from: SidebarMenus, to: SidebarMenus) { // Ugh... guard let splitVC = splitVC, let nowPlayingItem = nowPlayingItem, let videoViewItem = videoViewItem, let sourcesViewItem = sourcesViewItem, let infoViewItem = infoViewItem, from != to else { return } splitVC.removeChild(at: 1) // Put new switch to { case .modern: splitVC.addSplitViewItem(nowPlayingItem) case .videos: splitVC.addSplitViewItem(videoViewItem) case.settings: splitVC.addSplitViewItem(sourcesViewItem) case.infos: splitVC.addSplitViewItem(infoViewItem) } } // func browseTo(_ path: String) { guard let splitVC = splitVC, let sidebarVC = sidebarVC, let videosVC = videosVC else { return } // Remove the old one splitVC.removeChild(at: 1) // put up the video view splitVC.addSplitViewItem(videoViewItem!) sidebarVC.sidebarOutlineView.selectRowIndexes([2], byExtendingSelection: false) videosVC.reloadPath(path: path) } // Switch from one source to another func switchTo(_ path: String) { //print("switch to :" + path) guard let currentPath = currentPath, let videosVC = videosVC, path != currentPath else { //print("switch init issue") return } if path.starts(with: "modern:") { let idx = path.firstIndex(of: ":") switchToModern(String(path[idx!...].dropFirst())) // Oh Swift... } if path.starts(with: "videos:") { let idx = path.firstIndex(of: ":") videosVC.reloadFor(path: String(path[idx!...].dropFirst())) // Oh Swift... switchToModern(String(path[idx!...].dropFirst())) // Oh Swift... } if path.starts(with: "settings:") { let idx = path.firstIndex(of: ":") switchToSettings(String(path[idx!...].dropFirst())) // Oh Swift... } if path.starts(with: "infos:") { let idx = path.firstIndex(of: ":") switchToInfos(String(path[idx!...].dropFirst())) // Oh Swift... } // Save the new path self.currentPath = path } func switchToModern(_ path: String) { guard let splitVC = splitVC else { return } // Remove the old one splitVC.removeChild(at: 1) switch path { case "nowplaying": splitVC.addSplitViewItem(nowPlayingItem!) default: splitVC.addSplitViewItem(videoViewItem!) } } func switchToSettings(_ path: String) { guard let splitVC = splitVC else { return } // Remove the old one splitVC.removeChild(at: 1) switch path { case "sources": splitVC.addSplitViewItem(sourcesViewItem!) case "time": splitVC.addSplitViewItem(timeViewItem!) case "displays": splitVC.addSplitViewItem(displaysViewItem!) case "brightness": splitVC.addSplitViewItem(brightnessViewItem!) case "cache": splitVC.addSplitViewItem(cacheViewItem!) case "overlays": splitVC.addSplitViewItem(overlaysViewItem!) case "filters": splitVC.addSplitViewItem(filtersViewItem!) default: // case "advanced": splitVC.addSplitViewItem(advancedViewItem!) } } func switchToInfos(_ path: String) { guard let splitVC = splitVC else { return } // Remove the old one splitVC.removeChild(at: 1) switch path { // Infos case "about": splitVC.addSplitViewItem(infoViewItem!) case "credits": splitVC.addSplitViewItem(creditsViewItem!) default: // case "help": splitVC.addSplitViewItem(helpViewItem!) } } func updateViewInPlace() { if currentPath!.starts(with: "videos:") { videosVC!.updateInPlace() } else { debugLog("download callback happenning but we moved away") } } } extension PanelWindowController: NSWindowDelegate { func windowDidBecomeKey(_ notification: Notification) { if (notification.object as? NSWindow) == self.window { // doFirstTimeSetup() } } } ================================================ FILE: Resources/MainUI/PanelWindowController.xib ================================================ ================================================ FILE: Resources/MainUI/Settings panels/AdvancedViewController.swift ================================================ // // AdvancedViewController.swift // Aerial // // Created by Guillaume Louel on 18/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa import AVFoundation import VideoToolbox class AdvancedViewController: NSViewController { var windowController: PanelWindowController? var firstSetupWindowController: FirstSetupWindowController? @IBOutlet var scrollView: NSScrollView! @IBOutlet var popoverVideoFormat: NSPopover! @IBOutlet var popoverH264Indicator: NSButton! @IBOutlet var popoverHEVCIndicator: NSButton! @IBOutlet var popoverH264Label: NSTextField! @IBOutlet var popoverHEVCLabel: NSTextField! @IBOutlet var popoverOnBattery: NSPopover! @IBOutlet var videoFormatPopup: NSPopUpButton! // We need to hide HDR pre-Catalina @IBOutlet var menu1080pHDR: NSMenuItem! @IBOutlet var menu4KHDR: NSMenuItem! @IBOutlet var videoFadesPopup: NSPopUpButton! @IBOutlet weak var invertColorsCheckbox: NSButton! @IBOutlet var rightArrowSkipCheckbox: NSButton! @IBOutlet var muteSoundCheckbox: NSButton! @IBOutlet weak var muteAllMacOSSoundsCheckbox: NSButton! @IBOutlet var highQualityTextCheckbox: NSButton! @IBOutlet var favorOrientationCheckbox: NSButton! @IBOutlet var autoplayPreviews: NSButton! @IBOutlet var onBatteryPopup: NSPopUpButton! @IBOutlet var languagePopup: NSPopUpButton! @IBOutlet var languageLabel: NSTextField! @IBOutlet var debugCheckbox: NSButton! @IBOutlet var showLogButton: NSButton! @IBOutlet var launchSetupAgain: NSButton! var originalFormat: VideoFormat? override func viewDidLoad() { super.viewDidLoad() DispatchQueue.main.async { self.scrollView.contentView.scroll(NSMakePoint(0,0)) } // HEVC is available only in macOS 10.13+ if #available(OSX 10.13, *) { videoFormatPopup.selectItem(at: PrefsVideos.videoFormat.rawValue) } else { // We reset to 1080p below 10.13 PrefsVideos.videoFormat = VideoFormat.v1080pH264 videoFormatPopup.selectItem(at: PrefsVideos.videoFormat.rawValue) videoFormatPopup.isEnabled = false } // Save this for future use originalFormat = PrefsVideos.videoFormat videoFadesPopup.selectItem(at: PrefsVideos.fadeMode.rawValue) // We need catalina for HDR ! And we can't use right arrow to skip in Catalina if #available(OSX 10.15, *) { rightArrowSkipCheckbox.isEnabled = false } else { menu1080pHDR.isHidden = true menu4KHDR.isHidden = true } if !PrefsVideos.allowSkips { rightArrowSkipCheckbox.state = .off } invertColorsCheckbox.state = PrefsAdvanced.invertColors ? .on : .off highQualityTextCheckbox.state = PrefsInfo.highQualityTextRendering ? .on : .off muteSoundCheckbox.state = PrefsAdvanced.muteSound ? .on : .off muteAllMacOSSoundsCheckbox.state = PrefsAdvanced.muteGlobalSound ? .on : .off autoplayPreviews.state = PrefsAdvanced.autoPlayPreviews ? .on : .off favorOrientationCheckbox.state = PrefsAdvanced.favorOrientation ? .on : .off onBatteryPopup.selectItem(at: PrefsVideos.onBatteryMode.rawValue) if PrefsAdvanced.debugMode { debugCheckbox.state = .on } let poisp = PoiStringProvider.sharedInstance languagePopup.selectItem(at: poisp.getLanguagePosition()) // Grab preferred language as proper string languageLabel.stringValue = Aerial.helper.getPreferredLanguage() showLogButton.setIcons("folder") launchSetupAgain.setIcons("aspectratio") setupPopover() } func setupPopover() { // Help popover, GVA detection requires 10.13 if #available(OSX 10.13, *) { if !VTIsHardwareDecodeSupported(kCMVideoCodecType_H264) { popoverH264Label.stringValue = "H264 acceleration not supported" popoverH264Indicator.image = NSImage(named: NSImage.statusUnavailableName) } if !VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) { popoverHEVCLabel.stringValue = "HEVC Main10 acceleration not supported" popoverHEVCIndicator.image = NSImage(named: NSImage.statusUnavailableName) } else { let hardwareDetection = HardwareDetection.sharedInstance switch hardwareDetection.isHEVCMain10HWDecodingAvailable() { case .supported: popoverHEVCLabel.stringValue = "HEVC Main10 acceleration is supported" popoverHEVCIndicator.image = NSImage(named: NSImage.statusAvailableName) case .notsupported: popoverHEVCLabel.stringValue = "HEVC Main10 acceleration is not supported" popoverHEVCIndicator.image = NSImage(named: NSImage.statusUnavailableName) case .partial: popoverHEVCLabel.stringValue = "HEVC Main10 acceleration is partially supported" popoverHEVCIndicator.image = NSImage(named: NSImage.statusPartiallyAvailableName) default: popoverHEVCLabel.stringValue = "HEVC Main10 acceleration status unknown" popoverHEVCIndicator.image = NSImage(named: NSImage.cautionName) } } } else { // Fallback on earlier versions popoverHEVCIndicator.isHidden = true popoverH264Indicator.image = NSImage(named: NSImage.cautionName) popoverH264Label.stringValue = "macOS 10.13 or above required" popoverHEVCLabel.stringValue = "Hardware acceleration status unknown" } } @IBAction func launchSetupAgainClick(_ sender: NSButton) { if firstSetupWindowController == nil { let bundle = Bundle(for: PanelWindowController.self) // We also load our CustomVideos nib here firstSetupWindowController = FirstSetupWindowController() var topLevelObjects: NSArray? = NSArray() if !bundle.loadNibNamed(NSNib.Name("FirstSetupWindowController"), owner: firstSetupWindowController, topLevelObjects: &topLevelObjects) { errorLog("Could not load nib for FirstSetupWindowController, please report") } } DispatchQueue.main.async { self.firstSetupWindowController!.windowDidLoad() self.firstSetupWindowController!.showWindow(self) self.firstSetupWindowController!.window!.makeKeyAndOrderFront(self) } } @IBAction func videoFormatPopupChange(_ sender: NSPopUpButton) { let candidateFormat = VideoFormat(rawValue: sender.indexOfSelectedItem)! if candidateFormat != originalFormat { // swiftlint:disable:next line_length if Aerial.helper.showAlert(question: "Changing format will delete all videos", text: "Changing format will delete your downloaded videos. They will be re-downloaded based on your preferences. \n\nYou can also manually redownload videos in Custom Sources.", button1: "Change Format and Delete Videos", button2: "Cancel") { PrefsVideos.videoFormat = candidateFormat originalFormat = candidateFormat Cache.clearCache() Cache.clearNonCacheableSources() // Sidebar.instance.refreshVideos() } else { videoFormatPopup.selectItem(at: PrefsVideos.videoFormat.rawValue) } } else { PrefsVideos.videoFormat = candidateFormat } } @IBAction func invertColorsCheckboxClick(_ sender: NSButton) { PrefsAdvanced.invertColors = sender.state == .on } @IBAction func videoFadesPopupChange(_ sender: NSPopUpButton) { PrefsVideos.fadeMode = FadeMode(rawValue: sender.indexOfSelectedItem)! } @IBAction func highQualityTextClick(_ sender: NSButton) { PrefsInfo.highQualityTextRendering = sender.state == .on } @IBAction func rightArrowSkipClick(_ sender: NSButton) { PrefsVideos.allowSkips = sender.state == .on } @IBAction func muteSoundClick(_ sender: NSButton) { PrefsAdvanced.muteSound = sender.state == .on } @IBAction func muteAllMacOSSoundsClick(_ sender: NSButton) { PrefsAdvanced.muteGlobalSound = sender.state == .on } @IBAction func autoPlaysPreviewsClick(_ sender: NSButton) { PrefsAdvanced.autoPlayPreviews = sender.state == .on } @IBAction func favorOrientationClick(_ sender: NSButton) { PrefsAdvanced.favorOrientation = sender.state == .on } @IBAction func onBatteryPopupChange(_ sender: NSPopUpButton) { PrefsVideos.onBatteryMode = OnBatteryMode(rawValue: sender.indexOfSelectedItem)! } @IBAction func languagePopupChange(_ sender: NSPopUpButton) { let poisp = PoiStringProvider.sharedInstance PrefsAdvanced.ciOverrideLanguage = poisp.getLanguageStringFromPosition(pos: sender.indexOfSelectedItem) } @IBAction func debugCheckboxClick(_ sender: NSButton) { PrefsAdvanced.debugMode = sender.state == .on } @IBAction func showLogInFinderClick(_ sender: Any) { let logfile = Cache.supportPath.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: Cache.supportPath) } else { NSWorkspace.shared.selectFile(logfile, inFileViewerRootedAtPath: Cache.supportPath) } } @IBAction func resetAllSettings(_ sender: NSButton) { if Aerial.helper.showAlert( question: "Reset all settings?", text: "This will reset all your settings. After they are reset, Aerial will close System Preferences, you will have to reload it to access settings again.\n\nAre you sure you want to reset your settings?", button1: "Reset my settings", button2: "Cancel") { let process: Process = Process() debugLog("clearing old defaults") process.launchPath = "/usr/bin/defaults" // First remove old ByHost settings if #available(OSX 10.15, *) { process.arguments = ["-currentHost", "delete", Aerial.helper.getPreferencesDirectory() + "ByHost/com.JohnCoates.Aerial"] } else { process.arguments = ["-currentHost", "delete", "com.JohnCoates.Aerial"] } process.launch() process.waitUntilExit() let process2: Process = Process() debugLog("clearing new defaults") process2.launchPath = "/usr/bin/defaults" // First remove old ByHost settings if #available(OSX 10.15, *) { process2.arguments = ["delete", Aerial.helper.getPreferencesDirectory() + "com.glouel.Aerial"] } else { process2.arguments = ["delete", "com.glouel.Aerial"] } process2.launch() process2.waitUntilExit() Aerial.helper.showInfoAlert(title: "Settings reset to defaults", text: "Your settings were reset to defaults. \n\nPlease close Aerial and System Preferences in order to reload them.") } } // Helpers, to move in a model when I have a sec @IBAction func helpVideoFormat(_ sender: NSButton) { popoverVideoFormat.show(relativeTo: sender.preparedContentRect, of: sender, preferredEdge: .maxY) } @IBAction func helpOnBattery(_ sender: NSButton) { popoverOnBattery.show(relativeTo: sender.preparedContentRect, of: sender, preferredEdge: .maxY) } @IBAction func dolbyVisionClick(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://en.wikipedia.org/wiki/Dolby_Laboratories#Video_processing")! workspace.open(url) } @IBAction func projectPageClick(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://github.com/JohnCoates/Aerial/blob/master/Documentation/HardwareDecoding.md")! workspace.open(url) } } ================================================ FILE: Resources/MainUI/Settings panels/AdvancedViewController.xib ================================================ Aerial uses GPU hardware acceleration to reduce power consumption. You can still decide to disable Aerial when running on battery, or if your battery is low (<20%). If you enable one of those settings, Aerial will show a black screen and reduce your screen brightness to maximize power savings. Your screen brightness will be automatically restored on waking up. The most appropriate format depends on your Mac. Video playback on macOS can be accelerated by your GPU. - Macs 2017 and newer should either use 1080p HEVC or 4K HEVC. - Late 2015/2016 Macs may only partially support hardware accelerated HEVC decoding. You may see higher CPU usage with these formats. - Macs older than late 2015 should prefer 1080p H264 as they may lack HEVC hardware decoding support. HDR videos are encoded using Dolby Vision to represent a Higher Dynamic range of colors and lighting. These videos have wilder colors that may look unrealistic to some. HDR is only available starting with macOS Catalina. Please note that not every video is available in every format. Older videos may not be available in 4K, and some newer videos are not available in 1080 H.264. Aerial will try to play the best format available based on your choice. ================================================ FILE: Resources/MainUI/Settings panels/BrightnessViewController.swift ================================================ // // BrightnessViewController.swift // Aerial // // Created by Guillaume Louel on 19/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class BrightnessViewController: NSViewController { @IBOutlet var lowerBrightness: NSButton! @IBOutlet var startFromSlider: NSSlider! @IBOutlet var fadeToSlider: NSSlider! @IBOutlet var sleepAfterLabel: NSTextField! @IBOutlet var onlyDimAtNight: NSButton! @IBOutlet var onlyDimOnBattery: NSButton! var savedBrightness: Float? override func viewDidLoad() { super.viewDidLoad() // Main switch if PrefsDisplays.dimBrightness { lowerBrightness.state = .on changeBrightnessState(to: true) } else { changeBrightnessState(to: false) } startFromSlider.doubleValue = PrefsDisplays.startDim fadeToSlider.doubleValue = PrefsDisplays.endDim onlyDimAtNight.state = PrefsDisplays.dimOnlyAtNight ? .on : .off onlyDimOnBattery.state = PrefsDisplays.dimOnlyAtNight ? .on : .off DispatchQueue.main.async { let sleepTime = TimeManagement.sharedInstance.getCurrentSleepTime() if sleepTime != 0 { self.sleepAfterLabel.stringValue = "Your Mac currently goes to sleep after \(sleepTime) minute\(sleepTime != 1 ? "s" : "")" } else { self.sleepAfterLabel.stringValue = "Unable to determine your Mac sleep settings" } } } func changeBrightnessState(to: Bool) { onlyDimAtNight.isEnabled = to onlyDimOnBattery.isEnabled = to startFromSlider.isEnabled = to fadeToSlider.isEnabled = to } @IBAction func lowerBrightnessClick(_ sender: NSButton) { PrefsDisplays.dimBrightness = sender.state == .on changeBrightnessState(to: sender.state == .on) } @IBAction func startFromSliderChange(_ sender: NSSliderCell) { guard let event = NSApplication.shared.currentEvent else { return } guard [.leftMouseUp, .leftMouseDown, .leftMouseDragged].contains(event.type) else { return } if event.type == .leftMouseUp { if let brightness = savedBrightness { Brightness.set(level: brightness) savedBrightness = nil } PrefsDisplays.startDim = sender.doubleValue } else { if savedBrightness == nil { savedBrightness = Brightness.get() } Brightness.set(level: sender.floatValue) } } @IBAction func fadeToSliderChange(_ sender: NSSliderCell) { guard let event = NSApplication.shared.currentEvent else { return } // Hmm if ![.leftMouseUp, .leftMouseDown, .leftMouseDragged].contains(event.type) { } if event.type == .leftMouseUp { if let brightness = savedBrightness { Brightness.set(level: brightness) savedBrightness = nil } PrefsDisplays.endDim = sender.doubleValue } else { if savedBrightness == nil { savedBrightness = Brightness.get() } Brightness.set(level: sender.floatValue) } } @IBAction func onlyDimAtNightClick(_ sender: NSButton) { PrefsDisplays.dimOnlyAtNight = sender.state == .on } @IBAction func onlyDimOnBatteryClick(_ sender: NSButton) { PrefsDisplays.dimOnlyOnBattery = sender.state == .on } } ================================================ FILE: Resources/MainUI/Settings panels/BrightnessViewController.xib ================================================ ================================================ FILE: Resources/MainUI/Settings panels/CacheViewController.swift ================================================ // // CacheViewController.swift // Aerial // // Created by Guillaume Louel on 18/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa extension Double { /// Rounds the double to decimal places value func rounded(toPlaces places: Int) -> Double { let divisor = pow(10.0, Double(places)) return (self * divisor).rounded() / divisor } } class CacheViewController: NSViewController { @IBOutlet var automaticallyDownloadCheckbox: NSButton! // We use two views for two available modes @IBOutlet var automaticContainerView: NSView! @IBOutlet var manualContainerView: NSView! @IBOutlet var limitSlider: NSSlider! @IBOutlet var limitTextField: NSTextField! @IBOutlet var limitLabel: NSTextField! @IBOutlet var limitUnitLabel: NSTextField! @IBOutlet var rotateFrequencyLabel: NSTextField! @IBOutlet var rotateFrequencyPopup: NSPopUpButton! @IBOutlet var restrictWiFiCheckbox: NSButton! @IBOutlet var connectedIcon: NSButton! @IBOutlet var connectedLabel: NSTextField! @IBOutlet var addCurrentNetworkButton: NSButton! @IBOutlet var resetListButton: NSButton! @IBOutlet var allowedListLabel: NSTextField! @IBOutlet var showDownloadIndicator: NSButton! @IBOutlet var cacheBox: NSBox! @IBOutlet var freeBox: NSBox! @IBOutlet var packsBox: NSBox! // Manual mode @IBOutlet var cacheSize: NSTextField! @IBOutlet var makeTimeMachineIgnore: NSButton! @IBOutlet var makeTimeMachineIgnore2: NSButton! @IBOutlet var manuallyPick: NSButton! @IBOutlet var pickFolder: NSButton! @IBOutlet var manuallyPickLabel: NSTextField! override func viewDidLoad() { super.viewDidLoad() DispatchQueue.main.async { self.makeTimeMachineIgnore.state = TimeMachine.isExcluded() ? .on : .off self.makeTimeMachineIgnore2.state = self.makeTimeMachineIgnore.state debugLog("tm : \(self.makeTimeMachineIgnore.state)") } manuallyPick.state = PrefsCache.overrideCache ? .on : .off if #available(OSX 12, *) { updateCachePath() } else if #available(OSX 10.15, *) { manuallyPick.isEnabled = false pickFolder.isHidden = true } else { updateCachePath() } // Cache panel automaticallyDownloadCheckbox.state = PrefsCache.enableManagement ? .on : .off limitTextField.doubleValue = PrefsCache.cacheLimit limitSlider.doubleValue = PrefsCache.cacheLimit rotateFrequencyPopup.selectItem(at: PrefsCache.cachePeriodicity.rawValue) // Update the size of the cache and associated controls updateCacheSize() // Wi-Fi restrictions? restrictWiFiCheckbox.state = PrefsCache.restrictOnWiFi ? .on : .off updateNetworkStatus() // And the master switch! updateCacheVisibility() showDownloadIndicator.state = PrefsCache.showBackgroundDownloads ? .on : .off updateCacheBox() } func updateCachePath() { if PrefsCache.overrideCache { // if let cachePath = Cache.supportPath { manuallyPickLabel.stringValue = "Using \(Cache.supportPath)" /*} else { manuallyPickLabel.stringValue = "Select a path using the folder picker" }*/ } else { manuallyPickLabel.stringValue = "Using your Application Support directory" } } func updateCacheBox() { let usedCache = Cache.size() let packsSize = Cache.packsSize() //print("pack size : \(packsSize)") var maxCache = PrefsCache.cacheLimit var freeCache = usedCache > maxCache ? 0 : maxCache - usedCache if PrefsCache.cacheLimit == 101 || !PrefsCache.enableManagement { freeCache = 0 maxCache = usedCache } // This is the total max usage, used to draw the bar var totalPotentialSize = max(maxCache, usedCache) + packsSize if totalPotentialSize == 0 { totalPotentialSize = 1 } // let totalUsage = usedCache let cacheWidth = Int(usedCache * 486 / totalPotentialSize) let freeWidth = Int(freeCache * 486 / totalPotentialSize) let packsWidth = Int(packsSize * 486 / totalPotentialSize) var cacheString = "" if usedCache > 0 { cacheString.append("\(usedCache.rounded(toPlaces: 1)) GB used by cached videos") cacheBox.isHidden = false cacheBox.frame.origin.x = CGFloat(206) // We offset by 1px to make the borders overlap cacheBox.setFrameSize(NSSize(width: cacheWidth, height: 25)) } else { cacheString.append("No space used by cached videos") cacheBox.isHidden = true } if freeCache > 0 { cacheString.append(", \(freeCache.rounded(toPlaces: 1)) GB remaining in your cache limit") freeBox.isHidden = false freeBox.frame.origin.x = CGFloat(206 + cacheWidth - 1) // We offset by 1px to make the borders overlap freeBox.setFrameSize(NSSize(width: freeWidth, height: 25)) } else { if PrefsCache.cacheLimit != 101 && PrefsCache.enableManagement { cacheString.append(", your cache is full!") } freeBox.isHidden = true } if packsSize > 0.01 { cacheString.append(", \(packsSize.rounded(toPlaces: 1)) GB used by packs") packsBox.isHidden = false packsBox.frame.origin.x = CGFloat(206 + cacheWidth + freeWidth - 2) // We offset by 1px to make the borders overlap packsBox.setFrameSize(NSSize(width: packsWidth, height: 25)) } else { packsBox.isHidden = true } // (8 GB for packs, 32 GB for the cache, still 8 GB of free cache available for more videos) limitLabel.stringValue = cacheString } @IBAction func showDownloadIndicatorChange(_ sender: NSButton) { PrefsCache.showBackgroundDownloads = sender.state == .on } @IBAction func automaticallyDownloadClick(_ sender: NSButton) { PrefsCache.enableManagement = sender.state == .on updateCacheVisibility() updateCacheBox() } @IBAction func limitSliderChange(_ sender: NSSlider) { PrefsCache.cacheLimit = sender.doubleValue.rounded(toPlaces: 1) limitTextField.doubleValue = sender.doubleValue.rounded(toPlaces: 1) updateCacheSize() updateCacheBox() } @IBAction func limitTextFieldChange(_ sender: NSTextField) { PrefsCache.cacheLimit = sender.doubleValue limitSlider.doubleValue = sender.doubleValue } @IBAction func rotateFrequencyChange(_ sender: NSPopUpButton) { PrefsCache.cachePeriodicity = CachePeriodicity(rawValue: sender.indexOfSelectedItem)! } @IBAction func restrictWiFiCheck(_ sender: NSButton) { PrefsCache.restrictOnWiFi = sender.state == .on updateNetworkStatus() } @IBAction func addCurrentNetworkClick(_ sender: Any) { if !PrefsCache.allowedNetworks.contains(Cache.ssid) { PrefsCache.allowedNetworks.append(Cache.ssid) } updateNetworkStatus() } @IBAction func resetListClick(_ sender: Any) { PrefsCache.allowedNetworks.removeAll() updateNetworkStatus() } // Helpers func updateNetworkStatus() { if PrefsCache.restrictOnWiFi { connectedIcon.isHidden = false connectedLabel.isHidden = false addCurrentNetworkButton.isHidden = false resetListButton.isHidden = false allowedListLabel.isHidden = false } else { connectedIcon.isHidden = true connectedLabel.isHidden = true addCurrentNetworkButton.isHidden = true resetListButton.isHidden = true allowedListLabel.isHidden = true } connectedIcon.image = Cache.canNetwork() ? NSImage(named: NSImage.statusAvailableName) : NSImage(named: NSImage.statusUnavailableName) if Cache.ssid != "" { connectedLabel.stringValue = "Connected to: " + Cache.ssid + " " + (Cache.canNetwork() ? "(trusted)" : "(restricted)") } else { connectedLabel.stringValue = "Not connected to Wi-Fi" } if PrefsCache.allowedNetworks.isEmpty { allowedListLabel.stringValue = "No network currently allowed" } else { allowedListLabel.stringValue = "Allowed: " + PrefsCache.allowedNetworks.joined(separator: ", ") } } // Update UI depending on the master switch position func updateCacheVisibility() { if PrefsCache.enableManagement { automaticContainerView.isHidden = false manualContainerView.isHidden = true } else { automaticContainerView.isHidden = true manualContainerView.isHidden = false } } func updateCacheSize() { let size = Cache.sizeString() if PrefsCache.cacheLimit == 101 { limitTextField.isHidden = true limitUnitLabel.isHidden = true rotateFrequencyPopup.isEnabled = false rotateFrequencyLabel.isEnabled = false } else { limitTextField.isHidden = false limitUnitLabel.isHidden = false rotateFrequencyPopup.isEnabled = true rotateFrequencyLabel.isEnabled = true } // limitLabel.stringValue = "(Currently \(size))" cacheSize.stringValue = "Your videos take \(size) of disk space" } // Manual mode @IBAction func showInFinderClick(_ sender: Any) { NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: VideoCache.cacheDirectory!) } @IBAction func pickFolderButton(_ sender: Any) { let openPanel = NSOpenPanel() openPanel.canChooseDirectories = true openPanel.canChooseFiles = false openPanel.canCreateDirectories = true openPanel.allowsMultipleSelection = false openPanel.title = "Choose Aerial Cache Directory" openPanel.prompt = "Choose" // Grab the supportPath if let customPath = PrefsCache.supportPath { if customPath != "" { openPanel.directoryURL = URL(fileURLWithPath: customPath) } } openPanel.begin { result in guard result.rawValue == NSFileHandlingPanelOKButton, !openPanel.urls.isEmpty else { return } let cacheDirectory = openPanel.urls[0] PrefsCache.supportPath = cacheDirectory.path // On macOS 12 we save a security scoped bookmark if #available(macOS 12, *) { do { let cacheBookmark = try cacheDirectory.bookmarkData( options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil) PrefsCache.supportBookmarkData = cacheBookmark } catch let error { debugLog("Error saving the security scoped bookmark \(error)") } } Aerial.helper.showInfoAlert(title: "Cache path changed", text: "In order for your new cache path to take effect, please close this panel and System Preferences.") } } @IBAction func makeTimeMachineIgnore(_ sender: NSButton) { if sender.state == .on { TimeMachine.exclude() } else { TimeMachine.reinclude() } } @IBAction func manuallyPIckClick(_ sender: NSButton) { PrefsCache.overrideCache = sender.state == .on updateCachePath() } } ================================================ FILE: Resources/MainUI/Settings panels/CacheViewController.xib ================================================ ================================================ FILE: Resources/MainUI/Settings panels/Collection View/PlayingCollectionViewItem.swift ================================================ // // PlayingCollectionViewItem.swift // Aerial // // Created by Guillaume Louel on 18/11/2021. // Copyright © 2021 Guillaume Louel. All rights reserved. // import Cocoa class PlayingCollectionViewItem: NSCollectionViewItem { @IBOutlet var hiddenPath: NSTextField! @IBOutlet var extraTextField: NSTextField! @IBOutlet var browseImageButton: NSButton! @IBOutlet var mainImageButton: NSButton! @IBOutlet var checkImageButton: NSButton! @IBOutlet var numberedPath: NSTextField! var nowPlayingViewController: NowPlayingViewController? override func viewDidLoad() { super.viewDidLoad() // For images the height coordinates are reversed, obviously... let imgShadow: NSShadow = NSShadow() imgShadow.shadowBlurRadius = 2 imgShadow.shadowOffset = NSSize(width: 0, height: 3) imgShadow.shadowColor = NSColor.black browseImageButton.shadow = imgShadow checkImageButton.shadow = imgShadow // Do view setup here. } @IBAction func browseButton(_ sender: Any) { Aerial.helper.windowController?.browseTo(hiddenPath.stringValue) } @IBAction func mainImageClick(_ sender: NSButton) { let path = hiddenPath.stringValue if checkImageButton.state == .on { checkImageButton.state = .off checkImageButton.image = Aerial.helper.getSymbol("circle") if PrefsVideos.newShouldPlayString.contains(path) { PrefsVideos.newShouldPlayString.remove(at: PrefsVideos.newShouldPlayString.firstIndex(of: path)!) } } else { checkImageButton.state = .on checkImageButton.image = Aerial.helper.getSymbol("checkmark.circle.fill") if !PrefsVideos.newShouldPlayString.contains(path) { PrefsVideos.newShouldPlayString.append(path) } } } @IBAction func imageButtonClick(_ sender: NSButton) { let path = hiddenPath.stringValue if sender.state == .on { sender.image = Aerial.helper.getSymbol("checkmark.circle.fill") if !PrefsVideos.newShouldPlayString.contains(path) { PrefsVideos.newShouldPlayString.append(path) } } else { sender.image = Aerial.helper.getSymbol("circle") if PrefsVideos.newShouldPlayString.contains(path) { PrefsVideos.newShouldPlayString.remove(at: PrefsVideos.newShouldPlayString.firstIndex(of: path)!) } } } /* @IBAction func imageViewClick(_ sender: Any) { print("click") let path = hiddenPath.stringValue }*/ /* @IBAction func checkButtonChange(_ sender: NSButton) { let path = hiddenPath.stringValue if sender.state == .on { if !PrefsVideos.newShouldPlayString.contains(path) { PrefsVideos.newShouldPlayString.append(path) } } else { if PrefsVideos.newShouldPlayString.contains(path) { PrefsVideos.newShouldPlayString.remove(at: PrefsVideos.newShouldPlayString.firstIndex(of: path)!) } } }*/ @IBAction func hideAllVideosClick(_ sender: NSMenuItem) { let videos = VideoList.instance.getVideosForPath(numberedPath.stringValue) for video in videos.filter({ !PrefsVideos.hidden.contains($0.id) }) { PrefsVideos.hidden.append(video.id) } VideoList.instance.reloadSources() } @IBAction func favoriteAllClick(_ sender: Any) { let videos = VideoList.instance.getVideosForPath(numberedPath.stringValue) for video in videos.filter({ !PrefsVideos.favorites.contains($0.id) }) { PrefsVideos.favorites.append(video.id) } VideoList.instance.reloadSources() } @IBAction func cacheVideosClick(_ sender: Any) { Cache.ensureDownload { let videos = VideoList.instance.getVideosForPath(self.numberedPath.stringValue) for video in videos.filter({ !$0.isAvailableOffline }) { VideoManager.sharedInstance.queueDownload(video) } } VideoList.instance.reloadSources() } @IBAction func unfavoriteAllClick(_ sender: Any) { let videos = VideoList.instance.getVideosForPath(numberedPath.stringValue) for video in videos.filter({ PrefsVideos.favorites.contains($0.id) }) { PrefsVideos.favorites.remove(at: PrefsVideos.favorites.firstIndex(of: video.id)!) } VideoList.instance.reloadSources() } @IBAction func resetVibranceClick(_ sender: Any) { let videos = VideoList.instance.getVideosForPath(numberedPath.stringValue) for video in videos.filter({ PrefsVideos.vibrance.keys.contains($0.id) }) { PrefsVideos.vibrance.removeValue(forKey: video.id) } } } ================================================ FILE: Resources/MainUI/Settings panels/Collection View/PlayingCollectionViewItem.xib ================================================ ================================================ FILE: Resources/MainUI/Settings panels/CompanionCacheViewController.swift ================================================ // // CompanionCacheViewController.swift // Aerial // // Created by Guillaume Louel on 17/07/2022. // Copyright © 2022 Guillaume Louel. All rights reserved. // import Cocoa class CompanionCacheViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() // Do view setup here. } @IBAction func openSystemPreferences(_ sender: Any) { if #available(macOS 13, *) { _ = Aerial.helper.shell(launchPath: "/usr/bin/open", arguments: ["x-apple.systempreferences:com.apple.ScreenSaver-Settings.extension"]) } else { _ = Aerial.helper.shell(launchPath: "/usr/bin/osascript", arguments: [ "-e", "tell application \"System Preferences\"", "-e","set the current pane to pane id \"com.apple.preference.desktopscreeneffect\"", "-e","reveal anchor \"ScreenSaverPref\" of pane id \"com.apple.preference.desktopscreeneffect\"", "-e","activate", "-e","end tell"]) } } } ================================================ FILE: Resources/MainUI/Settings panels/CompanionCacheViewController.xib ================================================ ================================================ FILE: Resources/MainUI/Settings panels/DisplaysViewController.swift ================================================ // // DisplaysViewController.swift // Aerial // // Created by Guillaume Louel on 18/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class DisplaysViewController: NSViewController { @IBOutlet var displayView: DisplayView! @IBOutlet var displayInstructionLabel: NSTextField! @IBOutlet var displayPopup: NSPopUpButton! @IBOutlet var viewingModePopup: NSPopUpButton! @IBOutlet var aspectPopup: NSPopUpButton! @IBOutlet var marginBox: NSBox! @IBOutlet var horizontalMarginTextField: NSTextField! @IBOutlet var verticalMarginTextField: NSTextField! @IBOutlet var advancedMode: NSButton! @IBOutlet var advancedModeEdit: NSButton! // Advanced margin edit panel @IBOutlet var advancedEditPanel: NSPanel! @IBOutlet var advancedEditPanelTextfield: NSTextField! @IBOutlet var advancedEditApply: NSButton! @IBOutlet var advancedApplyClose: NSButton! override func viewDidLoad() { super.viewDidLoad() // This is the label in the large display view in the top if PrefsDisplays.displayMode == .selection { displayInstructionLabel.isHidden = false } // Popups displayPopup.selectItem(at: PrefsDisplays.displayMode.rawValue) viewingModePopup.selectItem(at: PrefsDisplays.viewingMode.rawValue) aspectPopup.selectItem(at: PrefsDisplays.aspectMode.rawValue) // Margins if PrefsDisplays.viewingMode == .spanned { marginBox.isHidden = false } else { marginBox.isHidden = true } if PrefsDisplays.displayMarginsAdvanced { advancedMode.state = .on advancedModeEdit.isEnabled = true } else { advancedMode.state = .off advancedModeEdit.isEnabled = false } horizontalMarginTextField.doubleValue = PrefsDisplays.horizontalMargin verticalMarginTextField.doubleValue = PrefsDisplays.verticalMargin } @IBAction func displayPopupChange(_ sender: NSPopUpButton) { PrefsDisplays.displayMode = DisplayMode(rawValue: sender.indexOfSelectedItem)! if PrefsDisplays.displayMode == .selection { displayInstructionLabel.isHidden = false } else { displayInstructionLabel.isHidden = true } displayView.needsDisplay = true } @IBAction func viewingModeChange(_ sender: NSPopUpButton) { PrefsDisplays.viewingMode = ViewingMode(rawValue: sender.indexOfSelectedItem)! DisplayDetection.sharedInstance.detectDisplays() // Force redetection to update our margin calculations in spanned mode displayView.needsDisplay = true if PrefsDisplays.viewingMode == .spanned { marginBox.isHidden = false } else { marginBox.isHidden = true } } @IBAction func aspectPopupChange(_ sender: NSPopUpButton) { PrefsDisplays.aspectMode = AspectMode(rawValue: sender.indexOfSelectedItem)! } @IBAction func horizontalMarginChange(_ sender: NSTextField) { PrefsDisplays.horizontalMargin = sender.doubleValue DisplayDetection.sharedInstance.detectDisplays() // Force redetection to update our margin calculations in spanned mode displayView.needsDisplay = true } @IBAction func verticalMarginChange(_ sender: NSTextField) { PrefsDisplays.verticalMargin = sender.doubleValue DisplayDetection.sharedInstance.detectDisplays() // Force redetection to update our margin calculations in spanned mode displayView.needsDisplay = true } @IBAction func advancedModeClick(_ sender: NSButton) { PrefsDisplays.displayMarginsAdvanced = sender.state == .on if PrefsDisplays.displayMarginsAdvanced { advancedModeEdit.isEnabled = true } else { advancedModeEdit.isEnabled = false } displayView.needsDisplay = true } // Advanced margins panel @IBAction func advancedModeEditClick(_ sender: Any) { if advancedEditPanel.isVisible { advancedEditPanel.close() } else { // Grab the JSON advancedEditPanelTextfield.stringValue = DisplayDetection.sharedInstance.getMarginsJSON() advancedEditPanel.makeKeyAndOrderFront(sender) } } @IBAction func advancedModePanelApply(_ sender: Any) { // We save the JSON as String PrefsDisplays.advancedMargins = advancedEditPanelTextfield.stringValue // And redetect DisplayDetection.sharedInstance.detectDisplays() // Force redetection to update our margin calculations in spanned mode displayView.needsDisplay = true } @IBAction func advancedModePanelApplyClose(_ sender: Any) { // We save the JSON as String PrefsDisplays.advancedMargins = advancedEditPanelTextfield.stringValue // And redetect DisplayDetection.sharedInstance.detectDisplays() // Force redetection to update our margin calculations in spanned mode displayView.needsDisplay = true advancedEditPanel.close() } } ================================================ FILE: Resources/MainUI/Settings panels/DisplaysViewController.xib ================================================ ================================================ FILE: Resources/MainUI/Settings panels/FiltersViewController.swift ================================================ // // FiltersViewController.swift // Aerial // // Created by Guillaume Louel on 02/08/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa import AVKit class FiltersViewController: NSViewController { @IBOutlet var imageView1: NSImageView! @IBOutlet var imageView2: NSImageView! @IBOutlet var imageView3: NSImageView! @IBOutlet var imageView4: NSImageView! @IBOutlet var vibranceSlider: NSSlider! @IBOutlet var infoIcon: NSButton! @IBOutlet var allowPerVideo: NSButton! var images: [NSImageView: CIImage] = [:] override func viewDidLoad() { super.viewDidLoad() vibranceSlider.doubleValue = PrefsVideos.globalVibrance setupImages() redrawFilteredImages() infoIcon.setIcons("info.circle") allowPerVideo.state = PrefsVideos.allowPerVideoVibrance ? .on : .off } func setupImages() { // Let's reset images = [:] // Get the cached currentRotation let videos = VideoList.instance.currentRotation().filter({ $0.isAvailableOffline && !$0.isHDR() }).shuffled() if !videos.isEmpty { images[imageView1] = getImage(videos.first!) } if videos.count > 1 { images[imageView2] = getImage(videos[1]) } if videos.count > 2 { images[imageView3] = getImage(videos[2]) } if videos.count > 3 { images[imageView4] = getImage(videos[3]) } } func redrawFilteredImages() { for (view, image) in images { setupImage(for: view, ciImage: image) } } func getImage(_ video: AerialVideo) -> CIImage? { if let url = Thumbnails.getLargeURL(forVideo: video) { return CIImage(contentsOf: url)! } else { return nil } } func setupImage(for imageView: NSImageView, ciImage: CIImage) { if #available(OSX 10.14, *) { if let vibrantImage = CIFilter(name: "CIVibrance", parameters: [kCIInputImageKey: ciImage, kCIInputAmountKey: PrefsVideos.globalVibrance] )?.outputImage { let rep = NSCIImageRep(ciImage: vibrantImage) let nsImage = NSImage(size: rep.size) nsImage.addRepresentation(rep) imageView.image = nsImage } else { errorLog("Couldn't apply vibrance filter, please report") } } else { // Fallback on earlier versions imageView.image = nil } } @IBAction func vibranceSliderChange(_ sender: NSSlider) { PrefsVideos.globalVibrance = sender.doubleValue redrawFilteredImages() } @IBAction func allowPerVideoChange(_ sender: NSButton) { PrefsVideos.allowPerVideoVibrance = sender.state == .on } } ================================================ FILE: Resources/MainUI/Settings panels/FiltersViewController.xib ================================================ ================================================ FILE: Resources/MainUI/Settings panels/NowPlayingViewController.swift ================================================ // // NowPlayingViewController.swift // Aerial // // Created by Guillaume Louel on 18/11/2021. // Copyright © 2021 Guillaume Louel. All rights reserved. // import Cocoa class NowPlayingViewController: NSViewController { // Top toolbar @IBOutlet var playIconImageView: NSImageView! @IBOutlet var currentlySelectedPopupButton: NSPopUpButton! @IBOutlet var selectAllButton: NSButton! @IBOutlet var deselectAllButton: NSButton! // Our main collection @IBOutlet var playingCollectionView: NowPlayingCollectionView! // Status stuff @IBOutlet var statusDriveImageView: NSImageView! @IBOutlet var statusDriveLabel: NSTextField! @IBOutlet var statusTimeImageView: NSImageView! @IBOutlet var statusTimeLabel: NSTextField! @IBOutlet weak var statusHiddenVideoButton: NSButton! @IBOutlet weak var statusFavoriteButton: NSButton! var sources: [String] = [] var currentSource: VideoList.FilterMode = .location override func viewDidLoad() { super.viewDidLoad() // This is the filter we use to populate the view updateCurrentSource() // Reflect on UI currentlySelectedPopupButton.selectItem(at: PrefsVideos.intNewShouldPlay) // Now update the UI reloadSources() updateStatusBar() // Setup collection playingCollectionView.dataSource = self playingCollectionView.wantsLayer = true VideoList.instance.addCallback { debugLog("NPrs") self.reloadSources() self.updateStatusBar() if self.isSelectionEmpty() { self.selectAllClick(self.selectAllButton!) } } } // This is our filter on this panel func updateCurrentSource() { switch PrefsVideos.newShouldPlay { case .location: currentSource = .location case .favorites: currentSource = .favorite case .time: currentSource = .time case .scene: currentSource = .scene case .source: currentSource = .source } } @IBAction func currentlySelectedChange(_ sender: NSPopUpButton) { PrefsVideos.newShouldPlay = NewShouldPlay(rawValue: sender.indexOfSelectedItem)! updateCurrentSource() reloadSources() updateStatusBar() if self.isSelectionEmpty() { self.selectAllClick(self.selectAllButton!) } } func isSelectionEmpty() -> Bool { let subSources = VideoList.instance.getSources(mode: currentSource) let mode = String(describing: currentSource) + ":" for source in subSources { let path = mode + source if PrefsVideos.newShouldPlayString.contains(path) { return false } } return true } @IBAction func selectAllClick(_ sender: Any) { let subSources = VideoList.instance.getSources(mode: currentSource) let mode = String(describing: currentSource) + ":" for source in subSources { let path = mode + source if !PrefsVideos.newShouldPlayString.contains(path) { PrefsVideos.newShouldPlayString.append(path) } } playingCollectionView.reloadData() } @IBAction func statusHiddenVideoButtonClick(_ sender: Any) { Aerial.helper.windowController?.browseTo("hidden:0") } @IBAction func statusFavoritesButtonClick(_ sender: Any) { Aerial.helper.windowController?.browseTo("favorites:0") } @IBAction func deselectAllClick(_ sender: Any) { let subSources = VideoList.instance.getSources(mode: currentSource) let mode = String(describing: currentSource) + ":" for source in subSources { let path = mode + source if PrefsVideos.newShouldPlayString.contains(path) { PrefsVideos.newShouldPlayString.remove(at: PrefsVideos.newShouldPlayString.firstIndex(of: path)!) } } playingCollectionView.reloadData() } public func reloadSources() { sources = VideoList.instance.getSources(mode: currentSource) playingCollectionView.reloadData() } func updateStatusBar() { if PrefsCache.enableManagement { // We are in managed mode if PrefsCache.cacheLimit >= 101 { statusDriveImageView.image = Aerial.helper.getAccentedSymbol("externaldrive.badge.checkmark") statusDriveLabel.stringValue = String(Cache.size().rounded(toPlaces: 1)) + " GB" } else if Cache.isFull() { statusDriveImageView.image = Aerial.helper.getAccentedSymbol("externaldrive.badge.xmark") statusDriveLabel.stringValue = Cache.sizeString() + " (your cache is full)" } else { statusDriveImageView.image = Aerial.helper.getAccentedSymbol("externaldrive.badge.checkmark") statusDriveLabel.stringValue = String(Cache.size().rounded(toPlaces: 1)) + " / " + String(PrefsCache.cacheLimit.rounded(toPlaces: 1)) + " GB" } } else { // Manual mode statusDriveImageView.image = Aerial.helper.getAccentedSymbol("internaldrive") statusDriveLabel.stringValue = Cache.sizeString() } // May get removed statusTimeImageView.isHidden = true statusTimeLabel.isHidden = true if PrefsVideos.hidden.isEmpty { statusHiddenVideoButton.title = "No hidden videos" } else if PrefsVideos.hidden.count == 1 { statusHiddenVideoButton.title = String(PrefsVideos.hidden.count) + " hidden video" } else { statusHiddenVideoButton.title = String(PrefsVideos.hidden.count) + " hidden videos" } if (PrefsVideos.favorites.isEmpty) { statusFavoriteButton.title = "No favorites" } else if PrefsVideos.favorites.count == 1 { statusFavoriteButton.title = String(PrefsVideos.favorites.count) + " favorite" } else { statusFavoriteButton.title = String(PrefsVideos.favorites.count) + " favorites" } } } extension NowPlayingViewController: NSCollectionViewDataSource { func numberOfSections(in collectionView: NSCollectionView) -> Int { return 1 } func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { return sources.count } func collectionView(_ itemForRepresentedObjectAtcollectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { let item = playingCollectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "PlayingCollectionViewItem"), for: indexPath) guard let playingCollectionViewItem = item as? PlayingCollectionViewItem else {return item} playingCollectionViewItem.nowPlayingViewController = self let path = String(describing: currentSource) + ":" + sources[indexPath[1]] playingCollectionViewItem.hiddenPath.stringValue = path playingCollectionViewItem.numberedPath.stringValue = String(describing: currentSource) + ":" + String(indexPath[1]) if PrefsVideos.newShouldPlayString.contains(path) { playingCollectionViewItem.checkImageButton?.state = .on playingCollectionViewItem.checkImageButton?.image = Aerial.helper.getSymbol("checkmark.circle.fill") } else { playingCollectionViewItem.checkImageButton?.state = .off playingCollectionViewItem.checkImageButton?.image = Aerial.helper.getSymbol("circle") } playingCollectionViewItem.textField?.stringValue = sources[indexPath[1]] let count = VideoList.instance.getVideosCountForSource(indexPath[1], mode: currentSource) if count == 1 { playingCollectionViewItem.extraTextField.stringValue = "\(count) video" } else { playingCollectionViewItem.extraTextField.stringValue = "\(count) videos" } let video = VideoList.instance.getVideosForSource(indexPath[1], mode: currentSource).first if let video = video { Thumbnails.get(forVideo: video) { [weak self] (img) in guard let _ = self else { return } if let img = img { playingCollectionViewItem.mainImageButton?.image = img } else { playingCollectionViewItem.mainImageButton?.image = nil } } } return item } } ================================================ FILE: Resources/MainUI/Settings panels/NowPlayingViewController.xib ================================================ ================================================ FILE: Resources/MainUI/Settings panels/OverlaysViewController.swift ================================================ // // OverlaysViewController.swift // Aerial // // Created by Guillaume Louel on 19/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class OverlaysViewController: NSViewController { @IBOutlet var popoverWeather: NSPopover! @IBOutlet var infoTableView: NSTableView! @IBOutlet var infoSettingsTableView: NSTableView! //@IBOutlet var infoBox: NSBox! //@IBOutlet var infoContainerView: InfoContainerView! @IBOutlet weak var hideUnderCompanion: NSButton! @IBOutlet var infoScrollView: NSScrollView! @IBOutlet var infoScrollableView: InfoContainerView! // Then all the individual views @IBOutlet var infoSettingsView: InfoSettingsView! @IBOutlet var infoCommonView: InfoCommonView! @IBOutlet var infoLocationView: InfoLocationView! @IBOutlet var infoClockView: InfoClockView! @IBOutlet var infoMessageView: InfoMessageView! // Message sub panels @IBOutlet var infoMessageTextView: NSView! @IBOutlet var infoMessageShellView: NSView! @IBOutlet var infoMessageTextFileView: NSView! @IBOutlet var infoBatteryView: InfoBatteryView! @IBOutlet var infoCountdownView: InfoCountdownView! @IBOutlet var infoTimerView: InfoTimerView! @IBOutlet var infoDateView: InfoDateView! @IBOutlet var infoWeatherView: InfoWeatherView! @IBOutlet var infoMusicView: InfoMusicView! @IBOutlet var fontButton: NSButton! @IBOutlet var trashButton: NSButton! // And our weather panel @IBOutlet var weatherPanel: NSPanel! @IBOutlet var weatherCustomView: NSView! @IBOutlet var weatherLabel: NSTextField! var infoSource = InfoTableSource() var infoSettingsSource = InfoSettingsTableSource() var currentSubMessage: NSView? override func viewDidLoad() { super.viewDidLoad() PrefsInfo.updateLayerList() // Do view setup here. fontButton.setIcons("textformat.alt") trashButton.setIcons("trash") infoSource.setController(self) infoTableView.dataSource = infoSource infoTableView.delegate = infoSource infoTableView.registerForDraggedTypes([NSPasteboard.PasteboardType(rawValue: "private.table-row")]) infoSettingsSource.setController(self) infoSettingsTableView.dataSource = infoSettingsSource infoSettingsTableView.delegate = infoSettingsSource hideUnderCompanion.state = PrefsInfo.hideUnderCompanion ? .on : .off // Calling this in both viewDidLoad and viewDidAppear prevents a vertical shift drawInfoPanel(forType: PrefsInfo.layers[infoTableView.selectedRow]) } override func viewDidAppear() { drawInfoPanel(forType: PrefsInfo.layers[infoTableView.selectedRow]) } @IBAction func hideUnderCompanionClick(_ sender: NSButton) { PrefsInfo.hideUnderCompanion = sender.state == .on } func drawInfoSettingsPanel() { resetInfoPanel() // Add the common block of features (enabled, font, position, screen) infoScrollableView.addSubview(infoSettingsView) //infoContainerView.addSubview(infoSettingsView) //infoBox.title = "Advanced text settings" infoSettingsView.setStates() } // We dynamically change the content here, based on what's selected func drawInfoPanel(forType: InfoType) { resetInfoPanel() // Add the common block of features (enabled, font, position, screen) infoScrollableView.addSubview(infoCommonView) //infoContainerView.addSubview(infoCommonView) infoCommonView.setType(forType, controller: self) infoScrollableView.setFrameSize(NSSize(width: infoScrollableView.frame.width, height: 800)) infoCommonView.frame.origin.y = 0 //infoScrollView.documentView?.scroll(.zero) // Then the per-type blocks if any switch forType { case .location: infoScrollableView.addSubview(infoLocationView) infoLocationView.frame.origin.y = infoCommonView.frame.height infoLocationView.setStates() //infoScrollableView.setFrameSize(NSSize(width: infoScrollableView.frame.width, height: infoCommonView.frame.height + infoLocationView.frame.height)) case .message: infoScrollableView.addSubview(infoMessageView) infoMessageView.frame.origin.y = infoCommonView.frame.height addSubMessagePanel() infoMessageView.setStates() //infoScrollableView.setFrameSize(NSSize(width: infoScrollableView.frame.width, height: infoCommonView.frame.height + infoLocationView.frame.height)) case .clock: infoScrollableView.addSubview(infoClockView) infoClockView.frame.origin.y = infoCommonView.frame.height infoClockView.setStates() //infoScrollableView.setFrameSize(NSSize(width: infoScrollableView.frame.width, height: infoCommonView.frame.height + infoClockView.frame.height)) case .date: infoScrollableView.addSubview(infoDateView) infoDateView.frame.origin.y = infoCommonView.frame.height infoDateView.setStates() //infoScrollableView.setFrameSize(NSSize(width: infoScrollableView.frame.width, height: infoCommonView.frame.height + infoDateView.frame.height)) case .battery: infoScrollableView.addSubview(infoBatteryView) infoBatteryView.frame.origin.y = infoCommonView.frame.height infoBatteryView.setStates() //infoScrollableView.setFrameSize(NSSize(width: infoScrollableView.frame.width, height: infoCommonView.frame.height + infoBatteryView.frame.height)) case .updates: break case .weather: infoScrollableView.addSubview(infoWeatherView) infoWeatherView.frame.origin.y = infoCommonView.frame.height infoWeatherView.setStates() //infoScrollableView.setFrameSize(NSSize(width: infoScrollableView.frame.width, height: infoCommonView.frame.height + infoWeatherView.frame.height)) case .countdown: infoScrollableView.addSubview(infoCountdownView) infoCountdownView.frame.origin.y = infoCommonView.frame.height infoCountdownView.setStates() //infoScrollableView.setFrameSize(NSSize(width: infoScrollableView.frame.width, height: infoCommonView.frame.height + infoCountdownView.frame.height)) case .timer: infoScrollableView.addSubview(infoTimerView) infoTimerView.frame.origin.y = infoCommonView.frame.height infoTimerView.setStates() //infoScrollableView.setFrameSize(NSSize(width: infoScrollableView.frame.width, height: infoCommonView.frame.height + infoTimerView.frame.height)) case .music: infoScrollableView.addSubview(infoMusicView) infoMusicView.frame.origin.y = infoCommonView.frame.height infoMusicView.setStates() //infoScrollableView.setFrameSize(NSSize(width: infoScrollableView.frame.width, height: infoCommonView.frame.height + infoMusicView.frame.height)) } infoScrollView.documentView?.scroll(.zero) } func addSubMessagePanel() { switch PrefsInfo.message.messageType { case .text: infoScrollableView.addSubview(infoMessageTextView) infoMessageTextView.frame.origin.y = infoCommonView.frame.height + infoMessageView.frame.height currentSubMessage = infoMessageTextView case .shell: infoScrollableView.addSubview(infoMessageShellView) infoMessageShellView.frame.origin.y = infoCommonView.frame.height + infoMessageView.frame.height currentSubMessage = infoMessageShellView case .textfile: infoScrollableView.addSubview(infoMessageTextFileView) infoMessageTextFileView.frame.origin.y = infoCommonView.frame.height + infoMessageView.frame.height currentSubMessage = infoMessageTextFileView } infoMessageTextView.frame.origin.y = infoCommonView.frame.height + infoMessageView.frame.height } // We call this when we switch from one mode to another public func switchSubMessagePanel() { if let cMessage = currentSubMessage { cMessage.removeFromSuperview() } addSubMessagePanel() } // Clear the panel func resetInfoPanel() { infoScrollableView.subviews.forEach({ $0.removeFromSuperview() }) } // MARK: Weather panel // Simple current conditions func openWeatherPreview(weather: OWeather) { if !weatherPanel.isVisible { weatherPanel.makeKeyAndOrderFront(self) } weatherLabel.stringValue = "\(String(describing: weather.name)) \n\n \(weather)" let cond = ConditionLayer(condition: weather, scale: 2.0) weatherCustomView.layer = cond weatherCustomView.wantsLayer = true } // Forecasts func openWeatherPreview(weather: ForecastElement) { if !weatherPanel.isVisible { weatherPanel.makeKeyAndOrderFront(self) } weatherLabel.stringValue = "\(weather)" let cond = ForecastLayer(condition: weather, scale: 2.0) weatherCustomView.layer = cond weatherCustomView.wantsLayer = true } @IBAction func helpWeatherButtonClick(_ button: NSButton) { popoverWeather.show(relativeTo: button.preparedContentRect, of: button, preferredEdge: .maxY) } } ================================================ FILE: Resources/MainUI/Settings panels/OverlaysViewController.xib ================================================ Aerial uses OpenWeather's API to retrieve and display current conditions and forecasts. In order for this to work, you will need to specify your location. There are two ways to do that: - Manually specify : Simply enter your location as the name of your city, followed by a country abbreviation. In the United States, to remove ambiguity, you can also add a state abbreviation (e.g. Cupertino, CA, US). - Use current location : For this to work, Aerial will ask your mac for your current location, which is provided by macOS location services. This information is then sent to OpenWeather's API to retrieve conditions to the city closest to the location specified. For privacy reasons, the coordinates of your location are rounded to two decimals, leaving a precision of about 1 km. Please note that you will need to allow Aerial to use location services for this to work. In macOS Big Sur, please navigate to System Preferences > Security & Privacy > Privacy > Location Services and enable "legacyScreenSaver" Independant of your choice, you can preview the results using the "Test" button below. ================================================ FILE: Resources/MainUI/Settings panels/SourcesViewController.swift ================================================ // // SourcesViewController.swift // Aerial // // Created by Guillaume Louel on 18/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class SourcesViewController: NSViewController { var customVideoController: CustomVideoController? @IBOutlet var sourceOutlineView: SourceOutlineView! @IBOutlet var addOnlineWindow: NSWindow! @IBOutlet var addOnlineTextField: NSTextField! @IBOutlet var addLocalWindow: NSWindow! @IBOutlet var addLocalTextfield: NSTextField! @IBOutlet var addLocalButton: NSButton! @IBOutlet var addOnlineButton: NSButton! @IBOutlet var getMoreVideosButton: NSButton! @IBOutlet var downloadAllVideosButton: NSButton! @IBOutlet var refreshPeriodicity: NSPopUpButton! @IBOutlet var allSpinner: NSProgressIndicator! var selectedSource: Source? override func viewDidLoad() { super.viewDidLoad() allSpinner.isHidden = true sourceOutlineView.dataSource = self sourceOutlineView.delegate = self addLocalButton.setIcons("folder") addOnlineButton.setIcons("antenna.radiowaves.left.and.right") getMoreVideosButton.setIcons("cloud") downloadAllVideosButton.setIcons("arrow.down.circle") refreshPeriodicity.selectItem(at: PrefsVideos.intRefreshPeriodicity) VideoManager.sharedInstance.addCallback { done, total in debugLog("vmsourcecallback \(done) \(total) ") if total == 0 { self.sourceOutlineView.reloadData() self.allSpinner.stopAnimation(self) self.allSpinner.isHidden = true self.downloadAllVideosButton.isEnabled = true self.sourceOutlineView.expandItem(nil, expandChildren: true) } } VideoList.instance.addCallback { debugLog("sourcecallback") self.sourceOutlineView.reloadData() self.sourceOutlineView.expandItem(nil, expandChildren: true) } } @IBAction func refreshPeriodicityChange(_ sender: NSPopUpButton) { PrefsVideos.refreshPeriodicity = RefreshPeriodicity(rawValue: sender.indexOfSelectedItem)! } @IBAction func getMoreVideosClick(_ sender: NSButton) { let workspace = NSWorkspace.shared let url = URL(string: "https://aerialscreensaver.github.io/morevideos.html")! workspace.open(url) // } @IBAction func downloadAllClick(_ sender: NSButton) { Cache.ensureDownload { self.allSpinner.startAnimation(self) self.allSpinner.isHidden = false self.downloadAllVideosButton.isEnabled = false for video in VideoList.instance.videos.filter({ !$0.isAvailableOffline && !PrefsVideos.hidden.contains($0.id) }) { VideoManager.sharedInstance.queueDownload(video) } } } @IBAction func addLocalClick(_ sender: NSButton) { addLocalWindow.makeKeyAndOrderFront(self) /* // We also load our CustomVideos nib here let bundle = Bundle(for: CustomVideoController.self) customVideoController = CustomVideoController() var topLevelObjects: NSArray? = NSArray() if !bundle.loadNibNamed(NSNib.Name("CustomVideos"), owner: customVideoController, topLevelObjects: &topLevelObjects) { errorLog("Could not load nib for CustomVideos, please report") } DispatchQueue.main.async { self.customVideoController!.windowDidLoad() self.customVideoController!.show(sender: sender, controller: self) //self.customVideoController!.window!.makeKeyAndOrderFront(self) }*/ } @IBAction func addLocalValidate(_ sender: Any) { let url = URL(fileURLWithPath: addLocalTextfield.stringValue) SourceList.processPathForVideos(url: url) addLocalWindow.close() addLocalTextfield.stringValue = "" sourceOutlineView.reloadData() sourceOutlineView.expandItem(nil, expandChildren: true) } @IBAction func findMoreVideos(_ sender: Any) { let workspace = NSWorkspace.shared let url = URL(string: "https://aerialscreensaver.github.io/morevideos.html")! workspace.open(url) } @IBAction func addLocalCancel(_ sender: Any) { addLocalWindow.close() addLocalTextfield.stringValue = "" } @IBAction func addOnlineClick(_ sender: Any) { debugLog("Add online clicked") addOnlineWindow.makeKeyAndOrderFront(self) } @IBAction func addOnlineDownload(_ sender: Any) { debugLog("Add online validated") let trimmedString = addOnlineTextField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) if let url = URL(string: trimmedString) { debugLog("URL was parsed, fetching") SourceList.fetchOnlineManifest(url: url) addOnlineWindow.close() addOnlineTextField.stringValue = "" sourceOutlineView.reloadData() sourceOutlineView.expandItem(nil, expandChildren: true) } else { debugLog("URL was NOT parsed") Aerial.helper.showErrorAlert(question: "Non valid URL", text: "Please type a valid URL to an Aerial source (see the more videos button), and make sure there are no trailing characters.") } } @IBAction func addOnlineCancel(_ sender: Any) { addOnlineWindow.close() addOnlineTextField.stringValue = "" } } extension SourcesViewController: SourceOutlineViewDelegate { func outlineView(outlineView: NSOutlineView, menuForItem item: Any) -> NSMenu? { if let source = item as? Source { let menu = NSMenu() selectedSource = source let mitem = NSMenuItem(title: "Remove source", action: #selector(removeSource(_:)), keyEquivalent: "") mitem.setIcons("eye.slash") menu.addItem(mitem) return menu } return nil } @objc func removeSource(_ sender: Any) { if let source = selectedSource { // swiftlint:disable:next line_length if Aerial.helper.showAlert(question: "Remove a source", text: "This will remove all files and videos relating to this source. Are you sure you want to proceed? \n\nYou will need to restart System Preferences to complete the operation.", button1: "Remove Source", button2: "Cancel") { source.wipeFromDisk() sourceOutlineView.reloadData() sourceOutlineView.expandItem(nil, expandChildren: true) } } } } extension SourcesViewController: NSOutlineViewDataSource, NSOutlineViewDelegate { // item == nil means it's the "root" row of the outline view, which is not visible func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { if item == nil { return SourceList.categorizedSourceList()[index] } else { if let item = item as? SourceHeader { return item.sources[index] } else { return 0 } } } // Tell how many children each row has: func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { if item == nil { return SourceList.categorizedSourceList().count } else { if let item = item as? SourceHeader { return item.sources.count } else { return 1 } } } // Tell whether the row is expandable. func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { guard let _ = item as? SourceHeader else { return false } return true } // Set the content for each row/column element // swiftlint:disable cyclomatic_complexity func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { guard let columnIdentifier = tableColumn?.identifier.rawValue else { return nil } if let sourceHeader = item as? SourceHeader { if columnIdentifier == "valueColumn" { let cell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "valueColumnCell"), owner: self) as! DescriptionCellView cell.titleLabel.stringValue = sourceHeader.name cell.descriptionLabel.stringValue = "" cell.lastUpdatedLabel.stringValue = "" cell.imageScene1.isHidden = true cell.imageScene2.isHidden = true cell.imageScene3.isHidden = true cell.imageScene4.isHidden = true cell.imageScene5.isHidden = true cell.imageScene6.isHidden = true cell.videoCount.stringValue = "" cell.licenseButton.isHidden = true cell.moreButton.isHidden = true cell.imageFilm.isHidden = true cell.refreshNowButton.isHidden = true return cell } else { return nil } } let source = item as! Source switch columnIdentifier { case "isSelected": let cell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "isSelectedCell"), owner: self) as! CheckboxCellView cell.checkboxButton.state = source.isEnabled() ? .on : .off cell.delegate = self cell.item = item return cell case "valueColumn": let cell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "valueColumnCell"), owner: self) as! DescriptionCellView cell.item = source cell.titleLabel.stringValue = source.name cell.descriptionLabel.stringValue = source.description cell.lastUpdatedLabel.stringValue = "Last updated: " + source.lastUpdated() cell.imageScene1.isHidden = !source.scenes.contains(.nature) cell.imageScene2.isHidden = !source.scenes.contains(.city) cell.imageScene3.isHidden = !source.scenes.contains(.space) cell.imageScene4.isHidden = !source.scenes.contains(.sea) cell.imageScene5.isHidden = !source.scenes.contains(.beach) cell.imageScene6.isHidden = !source.scenes.contains(.countryside) if source.isEnabled() { cell.imageFilm.isHidden = false let totalCount = VideoList.instance.videos.filter({ $0.source.name == source.name }).count let downloadedCount = VideoList.instance.videos.filter({ $0.source.name == source.name && $0.isAvailableOffline }).count let size = source.diskUsage().rounded(toPlaces: 1) if totalCount == downloadedCount { cell.videoCount.stringValue = "\(totalCount) videos" } else { cell.videoCount.stringValue = "\(downloadedCount) of \(totalCount) videos downloaded" } if !source.isCachable && source.type != .local { cell.videoCount.stringValue.append(", \(size) GB on disk") } } else { cell.imageFilm.isHidden = true cell.videoCount.stringValue = "" } cell.licenseButton.isHidden = (source.license == "") cell.moreButton.isHidden = (source.more == "") cell.refreshNowButton.isHidden = false return cell case "actionColumn": let cell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: columnIdentifier), owner: self) as! ActionCellView cell.source = source cell.spinner.stopAnimation(self) cell.spinner.isHidden = true if source.type == .local { cell.actionButton.setLargeIcon("folder") cell.actionButton.isEnabled = true } else { if VideoList.instance.videos.filter({ $0.source.name == source.name && !$0.isAvailableOffline }).isEmpty { cell.actionButton.image = Aerial.helper.getMiniSymbol("checkmark.circle.fill", tint: .systemGreen) cell.actionButton.isEnabled = false } else { cell.actionButton.setLargeIcon("arrow.down.circle") cell.actionButton.isEnabled = true } } return cell default: let cell = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: columnIdentifier), owner: self) as! NSTableCellView cell.textField?.stringValue = "" return cell } } func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat { if item is SourceHeader { return 24 } else { return 70 } } /* func outlineView(_ outlineView: NSOutlineView, dataCellFor tableColumn: NSTableColumn?, item: Any) -> NSCell? { print("dcf") if let item = item as? SourceHeader { return NSTextFieldCell(textCell: item.name) } return nil }*/ } extension SourcesViewController: CheckboxCellViewDelegate { /// A delegate function where we can act on update from the checkbox in the "Is Selected" column func checkboxCellView(_ cell: CheckboxCellView, didChangeState state: NSControl.StateValue) { guard let item = cell.item as? Source else { return } // The row and its children are selected if state == .on item.setEnabled(state == .on) // This is more efficient than calling reload on every child since collapsed children are // not reloaded. They will be reloaded when they become visible DispatchQueue.main.async { self.sourceOutlineView.reloadItem(item, reloadChildren: true) } } } ================================================ FILE: Resources/MainUI/Settings panels/SourcesViewController.xib ================================================ Multiline Label line2 On macOS 10.15 and above (Catalina and Big Sur), there are several restrictions to be aware of: - Your folder must be on the main disk drive - It must be on a non protected user folder (so NOT Downloads, Desktop, Documents) We very highly recommend you put your video folders in /Users/shared/ as this is a "safe" location to use. ================================================ FILE: Resources/MainUI/Settings panels/TimeViewController.swift ================================================ // // TimeViewController.swift // Aerial // // Created by Guillaume Louel on 19/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa class TimeViewController: NSViewController { // All the radios @IBOutlet var timeLocationRadio: NSButton! @IBOutlet var timeNightShiftRadio: NSButton! @IBOutlet var timeManualRadio: NSButton! @IBOutlet var timeLightDarkModeRadio: NSButton! @IBOutlet var timeDisabledRadio: NSButton! // Night Shift @IBOutlet var nightShiftLabel: NSTextField! // Manual @IBOutlet var sunriseTime: NSDatePicker! @IBOutlet var sunsetTime: NSDatePicker! // Advanced @IBOutlet var darkModeNightOverride: NSButton! @IBOutlet var myLocationImageView: NSImageView! @IBOutlet var nightShiftImageView: NSImageView! @IBOutlet var manualImageView: NSImageView! @IBOutlet var lightModeImageView: NSImageView! @IBOutlet var noAdaptImageView: NSImageView! @IBOutlet var popoverCalcMode: NSPopover! @IBOutlet var oSunrise: NSTextField! @IBOutlet var eSunrise: NSTextField! @IBOutlet var eSunset: NSTextField! @IBOutlet var oSunset: NSTextField! @IBOutlet var timeBarView: NSView! @IBOutlet var sunsetWindowPopup: NSPopUpButton! lazy var timeFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "HH:mm" return formatter }() // swiftlint:disable cyclomatic_complexity override func viewDidLoad() { super.viewDidLoad() setupDarkMode() DispatchQueue.main.async { self.setupNightShift() } if let dateSunrise = timeFormatter.date(from: PrefsTime.manualSunrise) { sunriseTime.dateValue = dateSunrise } if let dateSunset = timeFormatter.date(from: PrefsTime.manualSunset) { sunsetTime.dateValue = dateSunset } // Handle the time radios switch PrefsTime.timeMode { case .nightShift: timeNightShiftRadio.state = .on case .manual: timeManualRadio.state = .on case .lightDarkMode: timeLightDarkModeRadio.state = .on case .disabled: timeDisabledRadio.state = .on default: timeLocationRadio.state = .on } myLocationImageView.image = Aerial.helper.getSymbol("mappin.and.ellipse")?.tinting(with: .secondaryLabelColor) nightShiftImageView.image = Aerial.helper.getSymbol("house")?.tinting(with: .secondaryLabelColor) manualImageView.image = Aerial.helper.getSymbol("clock")?.tinting(with: .secondaryLabelColor) lightModeImageView.image = Aerial.helper.getSymbol("gear")?.tinting(with: .secondaryLabelColor) noAdaptImageView.image = Aerial.helper.getSymbol("xmark.circle")?.tinting(with: .secondaryLabelColor) updateTimeView() switch PrefsTime.sunEventWindow { case 60*60: sunsetWindowPopup.selectItem(at: 0) case 60*90: sunsetWindowPopup.selectItem(at: 1) case 60*120: sunsetWindowPopup.selectItem(at: 2) case 60*150: sunsetWindowPopup.selectItem(at: 3) case 60*180: sunsetWindowPopup.selectItem(at: 4) case 60*210: sunsetWindowPopup.selectItem(at: 5) default: sunsetWindowPopup.selectItem(at: 6) } } func setupDarkMode() { // Dark Mode is Mojave+ if #available(OSX 10.14, *) { if PrefsTime.darkModeNightOverride { darkModeNightOverride.state = .on } // We disable the checkbox if we are on nightShift mode if PrefsTime.timeMode == .lightDarkMode { darkModeNightOverride.isEnabled = false } } else { darkModeNightOverride.isEnabled = false } // Light/Dark mode only available on Mojave+ let (isLDMCapable, reason: _) = DarkMode.isAvailable() if !isLDMCapable { timeLightDarkModeRadio.isEnabled = false } } func setupNightShift() { // Night Shift requires 10.12.4+ and a compatible Mac let (isNSCapable, reason: NSReason) = NightShift.isAvailable() if !isNSCapable { timeNightShiftRadio.isEnabled = false } nightShiftLabel.stringValue = NSReason } @IBAction func sunsetSunriseWindowChange(_ sender: NSPopUpButton) { PrefsTime.sunEventWindow = 60 * ((2 + sender.indexOfSelectedItem) * 30) updateTimeView() } func updateTimeView() { switch PrefsTime.timeMode { case .disabled: timeBarView.isHidden = true return case .lightDarkMode: timeBarView.isHidden = true return case .nightShift: timeBarView.isHidden = false case .manual: timeBarView.isHidden = false case .coordinates: timeBarView.isHidden = true return case .locationService: timeBarView.isHidden = false _ = TimeManagement.sharedInstance.calculateFromCoordinates() } let (sunrise, sunset) = TimeManagement.sharedInstance.getSunriseSunset() if let lsunrise = sunrise, let lsunset = sunset { let esunrise = lsunrise.addingTimeInterval(TimeInterval(PrefsTime.sunEventWindow)) let psunset = lsunset.addingTimeInterval(TimeInterval(-PrefsTime.sunEventWindow)) oSunrise.stringValue = timeFormatter.string(from: lsunrise) oSunrise.sizeToFit() eSunrise.stringValue = timeFormatter.string(from: esunrise) eSunrise.sizeToFit() eSunset.stringValue = timeFormatter.string(from: psunset) eSunset.sizeToFit() oSunset.stringValue = timeFormatter.string(from: lsunset) oSunset.sizeToFit() } } @IBAction func timeModeChange(_ sender: NSButton) { if sender == timeLightDarkModeRadio { darkModeNightOverride.isEnabled = false } else { if #available(OSX 10.14, *) { darkModeNightOverride.isEnabled = true } } switch sender { case timeDisabledRadio: PrefsTime.timeMode = .disabled case timeNightShiftRadio: PrefsTime.timeMode = .nightShift case timeManualRadio: PrefsTime.timeMode = .manual case timeLightDarkModeRadio: PrefsTime.timeMode = .lightDarkMode case timeLocationRadio: PrefsTime.timeMode = .locationService default: () } updateTimeView() } @IBAction func sunriseChange(_ sender: NSDatePicker?) { guard let date = sender?.dateValue else { return } PrefsTime.manualSunrise = timeFormatter.string(from: date) updateTimeView() } @IBAction func sunsetChange(_ sender: NSDatePicker?) { guard let date = sender?.dateValue else { return } PrefsTime.manualSunset = timeFormatter.string(from: date) updateTimeView() } @IBAction func darkModeNightOverrideClick(_ sender: NSButton) { PrefsTime.darkModeNightOverride = sender.state == .on } @IBAction func testLocationClick(_ sender: Any) { // Get the location let location = Locations.sharedInstance location.getCoordinates(failure: { (_) in // swiftlint:disable:next line_length Aerial.helper.showInfoAlert(title: "Could not get your location", text: "Make sure you enabled location services on your Mac (and Wi-Fi!), and that Aerial (or legacyScreenSaver on macOS 10.15 and later) is allowed to use your location. If you use Aerial Companion, you will also need also allow location services for it.", button1: "OK", caution: true) }, success: { (coordinates) in let lat = String(format: "%.2f", coordinates.latitude) let lon = String(format: "%.2f", coordinates.longitude) Aerial.helper.showInfoAlert(title: "Success", text: "Aerial can access your location (latitude: \(lat), longitude: \(lon)) and will use it to show you the correct videos.") self.updateTimeView() }) } } ================================================ FILE: Resources/MainUI/Settings panels/TimeViewController.xib ================================================ There are two ways to calculate the sunset and sunrise time: - Strict (0°): Calculate when the center of the Sun crosses the horizon - Official (0.83°): Calculate when the Sun completely crosses the horizon Or instead of sunrise and sunset, you can pick from three options for the time of dawn and dusk: - Civil (6°): Calculate when the natural sunlight stops/starts requiring artificial light compensation - Nautical (12°): Calculate when stars stop/start being seen - Astronomical (18°): Calculate the point when the sun no longer interferes with astronomical observations Coordinates are expressed in degrees, e.g.: - Latitude : 48.85837 - Longitude : 2.294483 You can either enter those manually or try to use Location Services on your Mac by clicking the icon next to the longitude button. You may be asked for permission. ================================================ FILE: Resources/MainUI/SidebarViewController.swift ================================================ // // SidebarViewController.swift // Aerial // // Created by Guillaume Louel on 15/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa enum SidebarMenus { case modern, videos, settings, infos } class SidebarViewController: NSViewController { @IBOutlet var sidebarOutlineView: SidebarOutlineView! // For the download indicator @IBOutlet var downloadIndicator: NSVisualEffectView! @IBOutlet var downloadIndicatorProgress: NSProgressIndicator! @IBOutlet var downloadIndicatorLabel: NSTextField! @IBOutlet var downloadCancelButton: NSButton! var windowController: PanelWindowController? // Always start with the videos panel selected var menuSelection: SidebarMenus = .modern @IBOutlet var closeButton: NSButton! var menuPath = "" // eh... override func viewDidLoad() { super.viewDidLoad() if #available(OSX 10.16, *) { closeButton.isHighlighted = true } else { // For some reason we need this on 10.12/10.13, there's a bug that somehow inverts colors in the recent Big Sur SDK for those older OSes sidebarOutlineView.backgroundColor = .clear sidebarOutlineView.gridColor = .alternateSelectedControlColor } sidebarOutlineView.delegate = self sidebarOutlineView.dataSource = self // Setup the updates for the download status let videoManager = VideoManager.sharedInstance videoManager.addCallback { done, total in self.updateDownloads(done: done, total: total, progress: 0) } videoManager.addProgressCallback { done, total, progress in self.updateDownloads(done: done, total: total, progress: progress) } downloadIndicator.isHidden = true downloadIndicatorProgress.doubleValue = 0 } override func viewDidAppear() { self.reloadSidebar() } private func updateSidebarMenu(_ menu: SidebarMenus) { if menu != menuSelection { windowController?.switchFrom(menuSelection, to: menu) // Make sure we mark the current one menuSelection = menu sidebarOutlineView.reloadData() sidebarOutlineView.expandItem(nil, expandChildren: true) sidebarOutlineView.selectRowIndexes([1], byExtendingSelection: false) } } @IBAction func closeButton(_ sender: Any) { windowController!.stopVideo() if !downloadIndicator.isHidden { // swiftlint:disable:next line_length if !Aerial.helper.showAlert(question: "Downloads still in progress", text: "Your video downloads are still in progress. Are you sure you want to quit ? This will abandon your current downloads.", button1: "Quit and Abandon Downloads", button2: "Cancel") { return } } if Aerial.helper.appMode { NSApplication.shared.terminate(nil) } else { if Aerial.helper.underCompanion { windowController!.window?.close() } else { windowController!.window?.sheetParent?.endSheet(windowController!.window!) } } } // MARK: Download indicator // Update the status of the download bar at the bottom of the sidebar func updateDownloads(done: Int, total: Int, progress: Double) { if total == 0 { downloadIndicator.isHidden = true downloadIndicatorProgress.doubleValue = 0 windowController!.updateViewInPlace() // Sidebar.instance.refreshVideos() /* sidebarOutlineView.reloadData() sidebarOutlineView.expandItem(nil, expandChildren: true) sidebarOutlineView.selectRowIndexes([1], byExtendingSelection: false) */ } else if progress == 0 { downloadIndicator.isHidden = false downloadIndicatorProgress.doubleValue = Double(done) downloadIndicatorProgress.maxValue = Double(total) downloadIndicatorProgress.toolTip = "Downloading \(done) / \(total)" downloadIndicatorLabel.stringValue = "Downloading \(done) / \(total)" } else { downloadIndicator.isHidden = false downloadIndicatorProgress.doubleValue = Double(done) + progress } } @IBAction func cancelDownloads(_ sender: Any) { let videoManager = VideoManager.sharedInstance videoManager.cancelAll() } } extension SidebarViewController: NSOutlineViewDataSource { func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { if let header = item as? Sidebar.Header { return header.entries.count } debugLog("child count \(Sidebar.instance.modern.count)") return Sidebar.instance.modern.count } func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { if let header = item as? Sidebar.Header { return header.entries[index] } return Sidebar.instance.modern[index] } func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { if item is Sidebar.Header { return true } return false } func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat { if item is Sidebar.Header { return 24 } else { return 30 } } func reloadSidebar() { // We need to reload our sidebar // Sidebar.instance.refreshVideos() debugLog("reload sidebar") let set = sidebarOutlineView.selectedRowIndexes sidebarOutlineView.reloadData() sidebarOutlineView.expandItem(nil, expandChildren: true) if set.isEmpty { debugLog("empty set") sidebarOutlineView.selectRowIndexes([1], byExtendingSelection: false) } else { debugLog("re set ing") sidebarOutlineView.selectRowIndexes(set, byExtendingSelection: false) } } } extension SidebarViewController: NSOutlineViewDelegate { func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { var view: NSTableCellView? if let header = item as? Sidebar.Header { view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "HeaderCell"), owner: self) as? NSTableCellView if let textField = view?.textField { textField.stringValue = header.name } } else if let entry = item as? Sidebar.MenuEntry { view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "DataCell"), owner: self) as? NSTableCellView if let textField = view?.textField { textField.stringValue = entry.name } if let imageView = view?.imageView { imageView.image = Sidebar.iconFor(entry.path, name: entry.name) imageView.image?.isTemplate = true imageView.sizeThatFits(CGSize(width: 24, height: 24)) // Hmm } } // More code here return view } func outlineView(_ outlineView: NSOutlineView, shouldSelectItem item: Any) -> Bool { if item is Sidebar.Header { return false } return true } func outlineViewSelectionDidChange(_ notification: Notification) { guard let outlineView = notification.object as? NSOutlineView else { return } let selectedIndex = outlineView.selectedRow if let entry = outlineView.item(atRow: selectedIndex) as? Sidebar.MenuEntry { windowController!.switchTo(entry.path) } } func outlineView(_ outlineView: NSOutlineView, shouldShowOutlineCellForItem item: Any) -> Bool { return false } @available(OSX 10.16, *) func outlineView(_ outlineView: NSOutlineView, tintConfigurationForItem item: Any) -> NSTintConfiguration? { if let entry = item as? Sidebar.MenuEntry { if entry.name == "Favorites" { return NSTintConfiguration(fixedColor: .init(red: 0.996, green: 0.741, blue: 0.066, alpha: 1.0)) } else { return NSTintConfiguration.default } } return nil } } // Right click menu /*extension SidebarViewController: SidebarOutlineViewDelegate { // swiftlint:disable cyclomatic_complexity func outlineView(outlineView: NSOutlineView, menuForItem item: Any) -> NSMenu? { // Make sure we're right clicking a menu entry if let entry = item as? Sidebar.MenuEntry { if entry.path.starts(with: "videos:") { let idx = entry.path.firstIndex(of: ":") let path = String(entry.path[idx!...].dropFirst()) // Oh Swift... menuPath = path // Store it for later use in selectors, it's ugly I know // Grab all the videos var videos: [AerialVideo] if let mode = VideoList.instance.modeFromPath(path) { let index = Int(path.split(separator: ":")[1])! videos = VideoList.instance.getVideosForSource(index, mode: mode) } else { // all videos = VideoList.instance.videos.sorted { $0.secondaryName < $1.secondaryName } } guard !videos.isEmpty else { debugLog("empty videos") return nil } let menu = NSMenu() var hasUnfavs = false var hasFavs = false // Add a unhide menu just for the hidden category if let mode = VideoList.instance.modeFromPath(path) { if mode == .hidden { let item = NSMenuItem(title: "Show videos", action: #selector(showVideos(_:)), keyEquivalent: "") item.setIcons("eye") menu.addItem(item) return menu } } // Add/remove favorites if !videos.filter({ !PrefsVideos.favorites.contains($0.id) }).isEmpty { let item = NSMenuItem(title: "Favorite videos", action: #selector(favoriteVideos(_:)), keyEquivalent: "") item.setIcons("star.fill") menu.addItem(item) hasUnfavs = true } if !videos.filter({ PrefsVideos.favorites.contains($0.id) }).isEmpty { let item = NSMenuItem(title: "Unfavorite videos", action: #selector(unfavoriteVideos(_:)), keyEquivalent: "") item.setIcons("star") menu.addItem(item) hasFavs = true } // Don't show the hide videos option if we only have favs in that list if !(hasFavs && !hasUnfavs) { menu.addItem(NSMenuItem.separator()) let item = NSMenuItem(title: "Hide videos", action: #selector(hideVideos(_:)), keyEquivalent: "") item.setIcons("eye.slash") menu.addItem(item) } // Do we have uncached videos in here ? if !videos.filter({!$0.isAvailableOffline}).isEmpty { menu.addItem(NSMenuItem.separator()) let item = NSMenuItem(title: "Cache missing videos", action: #selector(cacheMissingVideos(_:)), keyEquivalent: "") item.setIcons("arrow.down.circle") menu.addItem(item) } if !videos.filter({ PrefsVideos.vibrance.keys.contains($0.id) }).isEmpty { let item = NSMenuItem(title: "Reset vibrance", action: #selector(resetVibrance(_:)), keyEquivalent: "") item.setIcons("slider.horizontal.3") menu.addItem(item) hasFavs = true } return menu } } return nil } @objc func cacheMissingVideos(_ sender: Any) { if menuPath == "" { errorLog("Right click cache missing with no menu") return } Cache.ensureDownload { let videos = VideoList.instance.getVideosForPath(self.menuPath) for video in videos.filter({ !$0.isAvailableOffline }) { VideoManager.sharedInstance.queueDownload(video) } } } @objc func favoriteVideos(_ sender: Any) { if menuPath == "" { errorLog("Right click missing path") return } let videos = VideoList.instance.getVideosForPath(self.menuPath) for video in videos.filter({ !PrefsVideos.favorites.contains($0.id) }) { PrefsVideos.favorites.append(video.id) } windowController!.updateViewInPlace() } @objc func unfavoriteVideos(_ sender: Any) { if menuPath == "" { errorLog("Right click missing path") return } let videos = VideoList.instance.getVideosForPath(self.menuPath) for video in videos.filter({ PrefsVideos.favorites.contains($0.id) }) { PrefsVideos.favorites.remove(at: PrefsVideos.favorites.firstIndex(of: video.id)!) } windowController!.updateViewInPlace() } @objc func showVideos(_ sender: Any) { if menuPath == "" { errorLog("Right click missing path") return } let videos = VideoList.instance.getVideosForPath(self.menuPath) for video in videos.filter({ PrefsVideos.hidden.contains($0.id) }) { PrefsVideos.hidden.remove(at: PrefsVideos.hidden.firstIndex(of: video.id)!) } // We need to reload our sidebar // Sidebar.instance.refreshVideos() sidebarOutlineView.reloadData() sidebarOutlineView.expandItem(nil, expandChildren: true) sidebarOutlineView.selectRowIndexes([1], byExtendingSelection: false) } @objc func hideVideos(_ sender: Any) { if menuPath == "" { errorLog("Right click missing path") return } let videos = VideoList.instance.getVideosForPath(self.menuPath) for video in videos.filter({ !PrefsVideos.hidden.contains($0.id) }) { PrefsVideos.hidden.append(video.id) } // We need to reload our sidebar // Sidebar.instance.refreshVideos() sidebarOutlineView.reloadData() sidebarOutlineView.expandItem(nil, expandChildren: true) sidebarOutlineView.selectRowIndexes([1], byExtendingSelection: false) } @objc func resetVibrance(_ sender: Any) { if menuPath == "" { errorLog("Right click missing path") return } let videos = VideoList.instance.getVideosForPath(self.menuPath) for video in videos.filter({ PrefsVideos.vibrance.keys.contains($0.id) }) { PrefsVideos.vibrance.removeValue(forKey: video.id) } windowController!.updateViewInPlace() } }*/ ================================================ FILE: Resources/MainUI/SidebarViewController.xib ================================================ ================================================ FILE: Resources/MainUI/VideosViewController.swift ================================================ // // VideosViewController.swift // Aerial // // Created by Guillaume Louel on 15/07/2020. // Copyright © 2020 Guillaume Louel. All rights reserved. // import Cocoa import AVKit // swiftlint:disable:next type_body_length class VideosViewController: NSViewController { // Top rotation view @IBOutlet var rotationView: NSView! @IBOutlet var rotationPopup: NSPopUpButton! @IBOutlet var rotationImage: NSImageView! @IBOutlet var rotationCacheNow: NSButton! @IBOutlet var rotationSecondaryPopup: NSPopUpButton! @IBOutlet var rotationSecondaryMenu: NSMenu! @IBOutlet var videoListTableView: NSTableView! @IBOutlet var videoListRuntimeLabel: NSTextField! @IBOutlet var heroPlayerView: AVPlayerView! @IBOutlet var heroImageView: NSImageView! @IBOutlet var titleLabel: NSTextField! @IBOutlet var locationLabel: NSTextField! @IBOutlet var durationLabel: NSTextField! @IBOutlet var sourceLabel: NSTextField! @IBOutlet var formatLabel: NSTextField! @IBOutlet var timeImageView: NSImageView! @IBOutlet var sceneTypeImageView: NSImageView! @IBOutlet var downloadButton: NSButton! @IBOutlet var hideButton: NSButton! @IBOutlet var showButton: NSButton! @IBOutlet var isCachedImageView: NSImageView! @IBOutlet var vibrancyLabel: NSTextField! @IBOutlet var vibrancySlider: NSSlider! @IBOutlet var playbackSpeedLabel: NSTextField! @IBOutlet var playbackSpeedSlider: NSSlider! var path: String? var mode: String? var currentVibrancy: Double = 0 override func viewDidLoad() { super.viewDidLoad() rotationView.isHidden = false // Our video list videoListTableView.delegate = self videoListTableView.dataSource = self // Our large player view heroPlayerView.player = AVPlayer() heroPlayerView.controlsStyle = .none if #available(OSX 10.10, *) { heroPlayerView.videoGravity = .resizeAspectFill } downloadButton.setIcons("arrow.down.circle") hideButton.setIcons("eye.slash") showButton.setIcons("eye") setShadows() fixIcons() updateVideoView() updateRotationMenu() // This needs cleanup someday rotationSecondaryPopup.isHidden = true rotationCacheNow.isHidden = true } @objc func playerItemDidReachEnd(_ aNotification: Notification) { debugLog("\(self.description) played did reach end") debugLog("\(self.description) notification: \(aNotification)") debugLog("Rewinding video!") if let playerItem = aNotification.object as? AVPlayerItem { playerItem.seek(to: CMTime.zero, completionHandler: nil) heroPlayerView.player?.play() } } // MARK: - UI init /// Set the shadows for the various UI elements that needs them func setShadows() { // Drop shadow we use on the overlayed items let shadow: NSShadow = NSShadow() shadow.shadowBlurRadius = 2 shadow.shadowOffset = NSSize(width: 0, height: 3) shadow.shadowColor = NSColor.black // For images the height coordinates are reversed, obviously... let imgShadow: NSShadow = NSShadow() imgShadow.shadowBlurRadius = 2 imgShadow.shadowOffset = NSSize(width: 0, height: -3) imgShadow.shadowColor = NSColor.black titleLabel.shadow = shadow locationLabel.shadow = shadow durationLabel.shadow = shadow hideButton.shadow = shadow showButton.shadow = shadow downloadButton.shadow = shadow vibrancyLabel.shadow = shadow vibrancySlider.shadow = shadow playbackSpeedLabel.shadow = shadow playbackSpeedSlider.shadow = shadow timeImageView.shadow = imgShadow sceneTypeImageView.shadow = imgShadow isCachedImageView.shadow = imgShadow } /// Since we can't directly use SF Symbols, we use our own icon wrappers func fixIcons() { // rotationPopup.item(at: 0)?.setIcons("film") // rotationPopup.item(at: 1)?.setIcons("star") // rotationPopup.item(at: 2)?.setIcons("mappin.and.ellipse") // rotationPopup.item(at: 3)?.setIcons("clock") // rotationPopup.item(at: 4)?.setIcons("tram.fill") // rotationPopup.item(at: 5)?.setIcons("antenna.radiowaves.left.and.right") rotationImage.image = Aerial.helper.getAccentedSymbol("film") rotationCacheNow.setIcons("arrow.down.circle") } func switchMainTo(filter: VideoList.FilterMode) { switch filter { case .location: rotationPopup.selectItem(at: 2) case .time: rotationPopup.selectItem(at: 3) case .scene: rotationPopup.selectItem(at: 4) case .source: rotationPopup.selectItem(at: 5) case .favorite: rotationPopup.selectItem(at: 1) case .hidden: rotationPopup.selectItem(at: 6) default: rotationPopup.selectItem(at: 1) } } /// Reload with a named path /// This is used by the func reloadPath(path: String) { if let foundMode = VideoList.instance.modeFromPath(path) { switchMainTo(filter: foundMode) rotationPopupChange(rotationPopup) let subpath = path.split(separator: ":")[1] //print(subpath) // Very unswift var index = 0 for item in VideoList.instance.getSources(mode: foundMode) { if subpath == item { rotationSecondaryPopup.selectItem(at: index) rotationSecondaryPopupChange(rotationSecondaryPopup) } index += 1 } } } /// Reload the video view for a given path func reloadFor(path: String) { self.path = path updateRotationMenu() updateRuntimeLabel() // Reload data and scroll back up if videoListTableView != nil { videoListTableView.reloadData() videoListTableView.selectRowIndexes([0], byExtendingSelection: false) videoListTableView.scrollRowToVisible(0) if videoListTableView.numberOfRows == 0 { updateVideoView() } } } /// Update the total runtime for the current view func updateRuntimeLabel() { guard let path = self.path else { videoListRuntimeLabel.stringValue = "" return } // Grab all videos in the path var videos: [AerialVideo] if let mode = VideoList.instance.modeFromPath(path) { let index = Int(path.split(separator: ":")[1])! videos = VideoList.instance.getVideosForSource(index, mode: mode) } else { // all videos = VideoList.instance.videos.filter({ !PrefsVideos.hidden.contains($0.id) }).sorted { $0.secondaryName < $1.secondaryName } } // Calculate their duration in minutes var duration: Double = 0 for video in videos { duration += video.duration } let minutes: Int = Int(duration) / 60 var minutesString: String if minutes < 2 { minutesString = "1 minute" } else { minutesString = "\(minutes) minutes" } // Update the label if videos.isEmpty { videoListRuntimeLabel.stringValue = "" } else if videos.count == 1 { videoListRuntimeLabel.stringValue = "1 video, \(minutesString)" } else { videoListRuntimeLabel.stringValue = "\(videos.count) videos, \(minutesString)" } } // MARK: - Rotation menu /// Cache missing rotation now ! @IBAction func rotationCacheNowClick(_ sender: NSButton) { Cache.ensureDownload { for video in VideoList.instance.currentRotation().filter({ !$0.isAvailableOffline }) { VideoManager.sharedInstance.queueDownload(video) } } } /// Main popup change event @IBAction func rotationPopupChange(_ sender: NSPopUpButton) { switch sender.indexOfSelectedItem { case 0: mode = "all" case 1: mode = "favorites" case 2: mode = "location" case 3: mode = "time" case 4: mode = "scene" case 5: mode = "source" case 6: mode = "hidden" default: mode = "all" } // Cascade to a secondary popup for the various filters if mode == "all" || mode == "favorites" || mode == "hidden" { rotationSecondaryPopup.isHidden = true path = mode! + ":0" reloadFor(path: path!) } else { rotationSecondaryPopup.isHidden = false updateRotationSecondaryMenu() } } /// Secondary popup change event @IBAction func rotationSecondaryPopupChange(_ sender: NSPopUpButton) { // PrefsVideos.shouldPlayString = sender.selectedItem!.title path = mode! + ":\(sender.indexOfSelectedItem)" reloadFor(path: path!) } func updateRotationMenu() { // rotationPopup.selectItem(at: PrefsVideos.intShouldPlay) // Cascade to a secondary popup for the various filters /* if PrefsVideos.shouldPlay == .everything || PrefsVideos.shouldPlay == .favorites { rotationSecondaryPopup.isHidden = true } else { rotationSecondaryPopup.isHidden = false // updateRotationSecondaryMenu() }*/ /* if VideoList.instance.currentRotation().filter({ !$0.isAvailableOffline }).isEmpty { rotationCacheNow.isHidden = true } else { rotationCacheNow.isHidden = false }*/ } func updateRotationSecondaryMenu() { var filter: VideoList.FilterMode filter = VideoList.instance.modeFromPath(mode!) ?? .location rotationSecondaryPopup.removeAllItems() // Very unswift var index = 0 for item in VideoList.instance.getSources(mode: filter) { rotationSecondaryPopup.addItem(withTitle: item) index += 1 } // We select the first one if rotationSecondaryPopup.numberOfItems > 0 { // PrefsVideos.shouldPlayString = rotationSecondaryPopup.itemTitle(at: 0) rotationSecondaryPopup.selectItem(at: 0) path = mode! + ":0" reloadFor(path: path!) } } // MARK: - Video view func updateVideoView() { if let video = getSelectedVideo() { debugLog("\"\(video.id)\": \"sunrise\", // \(video.name) - \(video.secondaryName)") debugLog("\"\(video.id)\": \"sunset\", // \(video.name) - \(video.secondaryName)") debugLog("\"\(video.id)\": \"night\", // \(video.name) - \(video.secondaryName)") titleLabel.isHidden = false locationLabel.isHidden = false sourceLabel.isHidden = false formatLabel.isHidden = false titleLabel.stringValue = video.secondaryName locationLabel.stringValue = video.name sourceLabel.stringValue = video.source.name formatLabel.stringValue = video.getCurrentFormat() if PrefsVideos.hidden.contains(video.id) { hideButton.isHidden = true showButton.isHidden = false } else { hideButton.isHidden = false showButton.isHidden = true } if video.isAvailableOffline { // System settings crash under macOS 13 with HDR videos if !Aerial.helper.canHDR() { if !Aerial.helper.underCompanion && (PrefsVideos.videoFormat == .v4KHDR || PrefsVideos.videoFormat == .v1080pHDR) { showImage(video) } else { showVideo(video) } } else { showVideo(video) } } else { showImage(video) } setTimeIcon(video) setSceneIcon(video) } else { hideEverything() } } func stopVideo() { if let player = heroPlayerView.player { debugLog("Stopping player") player.pause() } } func updateDurationLabel(_ video: AerialVideo) { let speed = PlaybackSpeed.forVideo(video.id) if speed == 1 { durationLabel.stringValue = "Duration: " + timeString(video.duration) } else if speed > 1 { durationLabel.stringValue = "Duration(sped up): " + timeString(video.duration/Double(speed)) } else { durationLabel.stringValue = "Duration(slowed down): " + timeString(video.duration/Double(speed)) } } /// Show video player func showVideo(_ video: AerialVideo) { playbackSpeedLabel.isHidden = false playbackSpeedSlider.isHidden = false heroPlayerView.isHidden = false heroImageView.isHidden = true isCachedImageView.isHidden = false durationLabel.isHidden = false updateDurationLabel(video) downloadButton.isHidden = true if let player = heroPlayerView.player { let path = VideoList.instance.localPathFor(video: video) debugLog("heropath : \(path)") let asset = AVAsset(url: URL(fileURLWithPath: path)) let localitem = AVPlayerItem(asset: asset) // Set slider for the playback speed playbackSpeedSlider.doubleValue = Double(PlaybackSpeed.forVideo(video.id)) // Vibrancy filter requires 10.14, and doesn't work on HDR content if #available(OSX 10.14, *), !video.isHDR() { if PrefsVideos.allowPerVideoVibrance { vibrancyLabel.isHidden = false vibrancySlider.isHidden = false if let value = PrefsVideos.vibrance[video.id] { vibrancySlider.doubleValue = value } else { vibrancySlider.doubleValue = 0.0 } if vibrancySlider.doubleValue == 0 { currentVibrancy = PrefsVideos.globalVibrance } else { currentVibrancy = vibrancySlider.doubleValue } } else { vibrancyLabel.isHidden = true vibrancySlider.isHidden = true } let filter = CIFilter(name: "CIVibrance")! localitem.videoComposition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in let source = request.sourceImage.clampedToExtent() filter.setValue(source, forKey: kCIInputImageKey) filter.setValue(self.currentVibrancy, forKey: kCIInputAmountKey) let output = filter.outputImage request.finish(with: output!, context: nil) }) } else { vibrancyLabel.isHidden = true vibrancySlider.isHidden = true } player.replaceCurrentItem(with: localitem) // We may not auto play the video if PrefsAdvanced.autoPlayPreviews { player.play() // Set the playback speed if changed, it defaults to normal speed if not overriden player.rate = PlaybackSpeed.forVideo(video.id) } // Set notification... NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd(_:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: localitem) } } /// Show image func showImage(_ video: AerialVideo) { heroPlayerView.isHidden = true heroImageView.isHidden = false isCachedImageView.isHidden = true vibrancyLabel.isHidden = true vibrancySlider.isHidden = true playbackSpeedLabel.isHidden = true playbackSpeedSlider.isHidden = true durationLabel.isHidden = true if PrefsCache.enableManagement { downloadButton.isHidden = true // Always hide in managed mode } else { downloadButton.isHidden = false } // Clear up any playing video if let player = heroPlayerView.player { player.replaceCurrentItem(with: nil) player.pause() } heroImageView.imageScaling = .scaleProportionallyDown Thumbnails.getLarge(forVideo: video) { [weak self] (img) in guard let _ = self else { return } if let img = img { self!.heroImageView.image = img } else { self!.heroImageView.image = nil } } } /// Hide everything ! func hideEverything() { titleLabel.isHidden = true locationLabel.isHidden = true durationLabel.isHidden = true sourceLabel.isHidden = true formatLabel.isHidden = true heroPlayerView.isHidden = true heroImageView.isHidden = true isCachedImageView.isHidden = true downloadButton.isHidden = true hideButton.isHidden = true showButton.isHidden = true vibrancyLabel.isHidden = true vibrancySlider.isHidden = true // Clear up any playing video if let player = heroPlayerView.player { player.replaceCurrentItem(with: nil) player.pause() } setTimeIcon(nil) setSceneIcon(nil) } @IBAction func vibrancySliderChange(_ sender: NSSlider) { currentVibrancy = vibrancySlider.doubleValue PrefsVideos.vibrance[getSelectedVideo()!.id] = vibrancySlider.doubleValue } @IBAction func playbackSpeedSliderChange(_ sender: NSSlider) { PlaybackSpeed.update(video: getSelectedVideo()!.id, value: playbackSpeedSlider.floatValue) // Make sure we update the duration updateDurationLabel(getSelectedVideo()!) if PrefsAdvanced.autoPlayPreviews { if heroPlayerView.player?.rate != 0 { heroPlayerView.player?.rate = PlaybackSpeed.forVideo(getSelectedVideo()!.id) } } } // MARK: - Helpers func timeString(_ double: Double) -> String { let intValue = Int(double) if intValue % 60 < 10 { return String(intValue / 60) + ":0" + String(intValue % 60) } else { return String(intValue / 60) + ":" + String(intValue % 60) } } func getSelectedVideo() -> AerialVideo? { if let path = path { if videoListTableView.selectedRow > -1 { if let mode = VideoList.instance.modeFromPath(path) { let index = Int(path.split(separator: ":")[1])! if index >= 0 && videoListTableView.selectedRow >= 0 { return VideoList.instance.getVideoForSource(index, item: videoListTableView.selectedRow, mode: mode) } } else { // all //print(VideoList.instance.videos.count) if VideoList.instance.videos.count > 0 { return VideoList.instance.videos .filter({ !PrefsVideos.hidden.contains($0.id) }) .sorted { $0.secondaryName < $1.secondaryName }[videoListTableView.selectedRow] } } } } return nil } // Set the time icon func setTimeIcon(_ video: AerialVideo?) { guard let tvideo = video else { timeImageView.image = nil return } switch tvideo.timeOfDay { case "sunset": timeImageView.image = Aerial.helper.getSymbol("sunset") case "sunrise": timeImageView.image = Aerial.helper.getSymbol("sunrise") case "night": timeImageView.image = Aerial.helper.getSymbol("moon.stars") default: // day timeImageView.image = Aerial.helper.getSymbol("sun.max") } } // Set the scene icon (landscape...) func setSceneIcon(_ video: AerialVideo?) { guard let tvideo = video else { sceneTypeImageView.image = nil return } switch tvideo.scene { case .nature: sceneTypeImageView.image = Aerial.helper.getSymbol("leaf") case .beach: sceneTypeImageView.image = Aerial.helper.getSymbol("leaf") case .countryside: sceneTypeImageView.image = Aerial.helper.getSymbol("leaf") case .city: sceneTypeImageView.image = Aerial.helper.getSymbol("tram.fill") case .space: sceneTypeImageView.image = Aerial.helper.getSymbol("sparkles") case .sea: sceneTypeImageView.image = Aerial.helper.getSymbol("helm") } } // MARK: - Buttons @IBAction func downloadButton(_ sender: Any) { downloadButton.isHidden = true if let video = getSelectedVideo() { let videoManager = VideoManager.sharedInstance Cache.ensureDownload { videoManager.queueDownload(video) } } } @IBAction func hideButtonClick(_ sender: Any) { if let video = getSelectedVideo() { PrefsVideos.hidden.append(video.id) hideButton.isHidden = true showButton.isHidden = false updateInPlace() } } @IBAction func showButtonClick(_ sender: Any) { if let video = getSelectedVideo() { if let index = PrefsVideos.hidden.firstIndex(of: video.id) { PrefsVideos.hidden.remove(at: index) hideButton.isHidden = false showButton.isHidden = true updateInPlace() } else { errorLog("Can't find video when unhiding, please report") } } } func updateInPlace() { let row = videoListTableView.selectedRow if videoListTableView != nil { videoListTableView.reloadData() videoListTableView.selectRowIndexes([row], byExtendingSelection: false) } updateRotationMenu() } } extension VideosViewController: NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { guard let path = path else { return 0 } //print("nor path : " + path) if let mode = VideoList.instance.modeFromPath(path) { let index = Int(path.split(separator: ":")[1])! return VideoList.instance.getVideosCountForSource(index, mode: mode) } else { // all return VideoList.instance.videos.filter({ !PrefsVideos.hidden.contains($0.id) }).count } } } extension VideosViewController: NSTableViewDelegate { func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { guard let path = path else { return nil } //print("tv path : " + path) var video: AerialVideo if let mode = VideoList.instance.modeFromPath(path) { let index = Int(path.split(separator: ":")[1])! video = VideoList.instance.getVideoForSource(index, item: row, mode: mode) } else { // all video = VideoList.instance.videos.filter({ !PrefsVideos.hidden.contains($0.id) }).sorted { $0.secondaryName < $1.secondaryName }[row] } if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "ImageCellID"), owner: nil) as? VideoCellView { cell.video = video cell.label.stringValue = video.secondaryName cell.checkButton.state = PrefsVideos.favorites.contains(video.id) ? .on : .off if PrefsVideos.hidden.contains(video.id) { cell.checkButton.isHidden = true } else { cell.checkButton.isHidden = false } if PrefsCache.enableManagement { cell.downloadButton.isHidden = true } else { cell.downloadButton.isHidden = video.isAvailableOffline } Thumbnails.get(forVideo: video) { [weak self] (img) in guard let _ = self else { return } if let img = img { cell.thumbView.image = img } else { cell.thumbView.image = nil } } return cell } return nil } func tableViewSelectionDidChange(_ notification: Notification) { updateVideoView() } } ================================================ FILE: Resources/MainUI/VideosViewController.xib ================================================ 5 videos 2 cached 4 videos 4 cached 25 videos 12 cached 4 videos 4 cached 4 videos 4 cached ================================================ FILE: Resources/Old stuff/CustomVideos.xib ================================================ Click the "+ Add Folder" button on top of the window. You can also drag a folder into the left column. Starting with macOS 10.15, we highly recommend you put your videos in a folder in /Users/Shared/. Because of security restrictions that were introduced, locations such as Desktop, Documents, Downloads, or external drive are not possible. ================================================ FILE: Resources/Old stuff/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier com.JohnCoates.Aerial CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) LSApplicationCategoryType LSMinimumSystemVersion ${MACOSX_DEPLOYMENT_TARGET} NSAppTransportSecurity NSAllowsArbitraryLoads NSLocationAlwaysUsageDescription Aerial uses location services to calculate Sunset and Sunrise times from your position NSLocationWhenInUseUsageDescription Aerial uses location services to calculate Sunset and Sunrise times from your position NSPrincipalClass AerialView SUFeedURL https://raw.githubusercontent.com/JohnCoates/Aerial/master/appcast.xml SUPublicEDKey fbiQGEFq55xl4bjwj2/SpIO4JMsKmEyhHEWlMMueyDY= ================================================ FILE: Tests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleVersion 1 ================================================ FILE: Tests/PreferencesTests.swift ================================================ // // PreferencesTests // Aerial Tests // // Created by John Coates on 9/22/16. // Copyright © 2016 John Coates. All rights reserved. // /* import XCTest @testable import AerialApp class PreferencesTests: XCTestCase { override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } func testPreferenceSaving() { let preferences = Preferences.sharedInstance preferences.cacheAerials = false let newPreferences = Preferences() XCTAssertFalse(newPreferences.cacheAerials, "Property write verified") } } */ ================================================ FILE: appcast.xml ================================================ Aerial 2.0.10 If you want automatic updates, please install the Aerial Companion app, as Sparkle updates are now deprecated: https://aerialscreensaver.github.io

What's new:

  • A lot! If you are upgrading from 1.9.X or older, check out our new site for more https://aerialscreensaver.github.io

Note: Big Sur Beta users, there's an issue currently regarding 3rd party screen savers, check this post on how to get back your settings and cache : #1036 This macOS bug affects not only all versions of Aerial, but all 3rd party screen savers.

Note : this build works on both Intel and Apple Silicon

]]>
jeu., 24 sep. 2020 21:00:00 +0100 10.12
1.9.1
  • Add an option to hide battery indicator when it's full
  • Fix the position of the weather info
  • Fix a crash with some settings
  • ]]>
    mer., 5 jui. 2020 15:00:00 +0100 10.12
    1.9.0 New features:

    • Aerial can now display current weather conditions. This is done through Yahoo! Weather's API and can be configured in the Info tab.
    • The textual battery indicator is now replaced with icons
    • Add a 12/24h override to the Time info.
    • New options to quickly select day/night videos
    • Added an option to unmute sound in custom videos
    • Add Italian translation, many thanks to @marguglio !
    ]]>
    mon., 25 mai. 2020 15:00:00 +0100 10.12
    1.8.2 New features:

    • Add a new Date Info
    • Add a new Timer Info similar to Countdown, except it's a timer (say, 5 minutes)

    Bug fixes:

    Fix text overlap bug
    • Fix Location description bug when skipping videos on multiple monitors
    • Fix and updates Countdown's display options

    Updated translations:

    • Polish translations thanks to @Soruk !
    • Chinese Simplified/Traditional translations thanks to @LinkeyLeo !
    • Arabic translations thanks to @kachikulu !
    ]]>
    mar., 24 mar. 2020 16:00:00 +0100 10.12
    1.8.1 New features:

    • Changed "Random" to "Random corner" for location info randomness. Bottom/top center positions are now ignored here, but they can be used manually.
    • Fix a preference panel crash if you disabled auto updates.
    ]]>
    mar., 3 mar. 2020 18:00:00 +0100 10.12
    1.8.0 New features:

    • New update system for macOS Catalina, you will now be notified and be able to download new versions. Aerial will still not be able to self update in Catalina, this is a sandboxing limitation that can't be worked around for the time being
    • Add shadow controls
    • Add translations for the new tvOS 13 videos in French
    • Add a new Countdown information option

    Bug fixes:

    • Fix the high sierra textfields workaround in new UI
    • Fix battery detection in macs with no battery (oops)
    • Fix localization that no longer worked in Catalina (come on...)

    Misc:

    • Moved Sparkle from Cocoapods to a git submodule, updated Sparkle to 1.23
    • Minimum macOS version is now 10.12
    ]]>
    mar., 18 jan. 2020 18:00:00 +0100 10.12
    1.7.1 Automatic updates are broken for the time being in Catalina. If you try to auto-update, it will look like it install correctly but the update process will silently fail because of new sandboxing restrictions. You will need to update manually for the time being, my apologies.
    • Brings back "Allow right arrow to skip" for macOS versions prior to Catalina. That feature still won't work on Catalina.
    • Add seamless looping if you only have one video in your playlist.
    • Fix "new style" settings that weren't saved immediately, causing a discrepancy if you didn't close the preferences panel before launching the screen saver (with a hot corner).
    ]]>
    mar., 11 jan. 2020 16:00:00 +0100 10.9
    1.7.0 Note : Catalina users, the auto-update process WILL still fail for the moment, please install this build manually. My apologies for the issue. This is a large rewrite, a few settings will be reset to default, including video format preference (4K, HDR), my apologies for this but those were needed to introduce some new features. New features :
    • New text rendering ! Aerial can now display information (such as location information, time, etc) on mulitple screens with new animations and better transitions. The settings for these are found in the new "Info" tab.
    • Support for the 11 new Sea videos that were just released. If you do not see the videos after the update, please go to the "Updates" tab and press "Check Now" in the New videos section.
    • New "Battery" Info can tell you your battery status (charging, %, etc)
    • Infos can now be shown in more positions on screen (top center, bottom center, and screen center)
    • Important fixes :
      • Fix the mini video issue on some systems using macOS Catalina, more info about this here.
      ]]> mer., 29 jan. 2020 21:00:00 +0100 10.9 1.6.3
    • Fix a bad crash with custom videos and HDR.

    If you are using or planning to upgrade to macOS Catalina, we very much suggest you check this issue about what to expect : https://github.com/JohnCoates/Aerial/issues/801

    ]]>
    jeu., 24 oct. 2019 14:00:00 +0100 10.9
    1.6.2
  • Fix a hang in Catalina when you can't/have a hard time resuming from screensaver.
  • Fix Custom Videos in Catalina. You can also drop a folder there to automatically add it.
  • If you are using or planning to upgrade to macOS Catalina, we very much suggest you check this issue about what to expect : https://github.com/JohnCoates/Aerial/issues/801

    ]]>
    mar., 8 oct. 2019 17:00:00 +0100 10.9
    1.6.1
  • Fixes the hang/crash happening when playing the next video on some systems with multiple monitor, since 1.5.0. Many thanks to @markusem !
  • If you are using or planning to upgrade to macOS Catalina, we very much suggest you check this issue about what to expect : https://github.com/JohnCoates/Aerial/issues/801

    ]]>
    mar., 1 oct. 2019 18:00:00 +0100 10.9
    1.6.0a
  • This version includes support for the 15 new videos introduced in tvOS 13, including the 10 Sea videos and 5 new Space videos 🎉
  • Adds HDR support for macOS Catalina 🎉
  • Starting with this release, Aerial is properly signed and notarized 🎉
  • Cache mechanism was changed to accommodate changes in macOS Catalina. We recommend you double check your cache settings after upgrading.
  • Add Dutch translation by @sebastiaanspeck !
  • Add Traditional Chinese and update Simplified Chinese translations by @LinkeyLeo !
  • Add advanced mode for finely tuning the Spanned viewing mode in multi monitor configurations
  • This version includes many fixes for various issues including with multi monitor setups and better support for older versions of macOS.
  • If you are using or planning to upgrade to macOS Catalina, we very much suggest you check this issue about what to expect : https://github.com/JohnCoates/Aerial/issues/801

    ]]>
    jeu., 26 sept. 2019 19:23:00 +0100 10.9
    1.5.0 1.5.0 brings some pretty big features to Aerial, most importantly:

    • - Completely rewritten multi monitor support. You can now enable and disable individual displays in the new Display tab.
    • - New "Spanned" viewing mode. Selecting this mode will span an Aerial video on all your (selected) screens. You can even adjust margins.
    • - Add your own videos to Aerial using the new Custom Videos features. You can add your own videos in the new video manager (found in the menu below the video list).

    This version also includes many, many bugfixes! You can check the changelog for more information https://github.com/JohnCoates/Aerial/blob/master/Documentation/ChangeLog.md

    ]]>
    ven., 31 mai. 2019 11:30:00 +0100 10.9
    1.4.9 This is a bugfix for a crash for homebrew users and users who never starts the control panel. Sorry about that !

    ]]>
    mer., 1 mai. 2019 13:58:29 +0100 10.9
    1.4.8 This new version includes many new features and enhancements:

    • Support for January 25th video updates (finally)
    • Automatic updates support through Sparkle. You can manage how Aerial updates itself in the settings
    • Localization for community support in Arabic, Chinese Simplified, English, French, German, Hebrew, Polish and Spanish! Thanks to all the contributors. If you want to help, check our repository we very much welcome new contributions
    • You can now skip an Aerial with the right arrow key
    • You can now save your favorite videos sets to enable them quickly
    ]]>
    mar., 30 avr. 2019 12:12:29 +0100 10.9
    ================================================ FILE: beta-appcast.xml ================================================ Aerial 2.0.10 If you want automatic updates, please install the Aerial Companion app, as Sparkle updates are now deprecated: https://aerialscreensaver.github.io

    What's new:

    • A lot! If you are upgrading from 1.9.X or older, check out our new site for more https://aerialscreensaver.github.io

    Note: Big Sur Beta users, there's an issue currently regarding 3rd party screen savers, check this post on how to get back your settings and cache : #1036 This macOS bug affects not only all versions of Aerial, but all 3rd party screen savers.

    Note : this build works on both Intel and Apple Silicon

    ]]>
    jeu., 24 sep. 2020 21:00:00 +0100 10.12
    2.0.0beta4 Please quit System Preferences before installing ! If you want auto updates, please give a try to Aerial's new installer/updater here : https://github.com/glouel/AerialUpdater

    This beta changes a lot of things (including the UI !), you will need to reconfigure your cache settings ! If you want more details on what's new, check this issue : https://github.com/JohnCoates/Aerial/issues/1006

    Fixes:

    • Weather

    Note: Big Sur Beta 3 users, there's an issue currently with that beta of macOS regarding 3rd party screen savers, check this post on how to get back your settings and cache : https://github.com/JohnCoates/Aerial/issues/1006#issuecomment-664290581

    This macOS bug affects not only all versions of Aerial, but all 3rd party screen savers.

    ]]>
    ven., 3 aug. 2020 15:00:00 +0100 10.12
    1.9.1
  • Add an option to hide battery indicator when it's full
  • Fix the position of the weather info
  • Fix a crash with some settings
  • ]]>
    mer., 5 jui. 2020 15:00:00 +0100 10.12
    1.9.1beta1
  • Add an option to hide battery indicator when it's full
  • Fix the position of the weather info
  • ]]>
    mer., 27 mai. 2020 19:00:00 +0100 10.12
    1.9.0 New features:

    • Aerial can now display current weather conditions. This is done through Yahoo! Weather's API and can be configured in the Info tab.
    • The textual battery indicator is now replaced with icons
    • Add a 12/24h override to the Time info.
    • New options to quickly select day/night videos
    • Added an option to unmute sound in custom videos
    • Add Italian translation, many thanks to @marguglio !
    ]]>
    mon., 25 mai. 2020 15:00:00 +0100 10.12
    1.8.3beta11
  • Fix weather icons (for good!)
  • ]]>
    sam., 2 mai. 2020 19:00:00 +0100 10.12
    1.8.3beta9
  • Weather icons should be sharper
  • Night time calculation should now be correct
  • Add a new battery icon indicator, set as default
  • ]]>
    ven., 1 mai. 2020 19:00:00 +0100 10.12
    1.8.3beta8
  • Flat icons should look much crispier now
  • Potential fix for issues with dates
  • ]]>
    mer., 29 avr. 2020 19:00:00 +0100 10.12
    1.8.3beta7
  • Fix sun icon being misnamed, causing a crash with sunny conditions (sorry!)
  • Made the plist more human readable/editable for 3rd party toolsAdd a new icon set exported from SF Symbols for weather
  • ]]>
    dim., 26 avr. 2020 19:00:00 +0100 10.12
    1.8.3beta6
  • Add a new icon set exported from SF Symbols for weather
  • Fixed night icons not showing, on SF Symbols and Yahoo's default icons
  • ]]>
    ven., 24 mar. 2020 19:00:00 +0100 10.12
    1.8.3beta4
  • Fix (again) the beta update feed (sorry again !)
  • Fix Location info not showing on some systems
  • ]]>
    mar., 21 mar. 2020 19:00:00 +0100 10.12
    1.8.3beta3
  • Add Fahrenheit on Weather, fix weather not working for some cities
  • Add new options to easily check day or night videos
  • ]]>
    lun., 20 mar. 2020 19:00:00 +0100 10.12
    1.8.3beta2 I mistakenly uploaded the wrong file for beta1, this is the correct new version, my apologies !

    New features:

    • Add a new Weather info. This is still super experimental and will be improved in next betas
    • Add a 12/24h override
    • Add Italian translation, many thanks to @marguglio !

    Bug fixes:

    • Fix some locale bugs with Catalina
    • Fix looping if you only had one video in your playlist
    ]]>
    dim., 19 mar. 2020 19:00:00 +0100 10.12
    1.8.2 New features:

    • Add a new Date Info
    • Add a new Timer Info similar to Countdown, except it's a timer (say, 5 minutes)

    Bug fixes:

    Fix text overlap bug
    • Fix Location description bug when skipping videos on multiple monitors
    • Fix and updates Countdown's display options

    Updated translations:

    • Polish translations thanks to @Soruk !
    • Chinese Simplified/Traditional translations thanks to @LinkeyLeo !
    • Arabic translations thanks to @kachikulu !
    ]]>
    mar., 24 mar. 2020 16:00:00 +0100 10.12
    1.8.2beta4
  • Fix the overlap bug for smaller screens/large fonts
  • Add a Date info to show the current date
  • ]]>
    lun., 23 mar. 2020 19:00:00 +0100 10.12
    1.8.2beta2 New features:

    • Fix the seconds thing not working in Countdown info, removed year/months from the countdown info
    • Add a new Timer Info similar to Countdown, except it's a timer (say, 5 minutes).
    ]]>
    jeu., 19 mar. 2020 18:00:00 +0100 10.12
    1.8.2beta1 New features:

    • Add updated .pl translations
    • Tentative fix for a description bug when skipping videos on multiple monitors.
    ]]>
    mar., 10 mar. 2020 18:00:00 +0100 10.12
    1.8.1 New features:

    • Changed "Random" to "Random corner" for location info randomness. Bottom/top center positions are now ignored here, but they can be used manually.
    • Fix a preference panel crash if you disabled auto updates.
    ]]>
    mar., 3 mar. 2020 18:00:00 +0100 10.12
    1.8.1beta1 Changed "Random" to "Random corner" for location info randomness. Bottom/top center positions are now ignored here, but they can be used manually.

    ]]>
    lun., 24 fev. 2020 19:00:00 +0100 10.12
    1.8.0 New features:

    • New update system for macOS Catalina, you will now be notified and be able to download new versions. Aerial will still not be able to self update in Catalina, this is a sandboxing limitation that can't be worked around for the time being
    • Add shadow controls
    • Add translations for the new tvOS 13 videos in French
    • Add a new Countdown information option

    Bug fixes:

    • Fix the high sierra textfields workaround in new UI
    • Fix battery detection in macs with no battery (oops)
    • Fix localization that no longer worked in Catalina (come on...)

    Misc:

    • Moved Sparkle from Cocoapods to a git submodule, updated Sparkle to 1.23
    • Minimum macOS version is now 10.12
    ]]>
    mar., 18 jan. 2020 18:00:00 +0100 10.12
    1.7.2beta3 Automatic updates are broken for the time being in Catalina. If you try to auto-update, it will look like it install correctly but the update process will silently fail because of new sandboxing restrictions. You will need to update manually for the time being, my apologies.
    • More shadow options
    • Fix High Sierra workarounds in new UI
    ]]>
    sam., 15 jan. 2020 19:00:00 +0100 10.9
    1.7.2beta2 Automatic updates are broken for the time being in Catalina. If you try to auto-update, it will look like it install correctly but the update process will silently fail because of new sandboxing restrictions. You will need to update manually for the time being, my apologies.
    • Fix text clipping issues with some fonts
    • More UI cleanup, add a new Text settings option in Info tab
    ]]>
    ven., 14 jan. 2020 19:00:00 +0100 10.9
    1.7.2beta1 Automatic updates are broken for the time being in Catalina. If you try to auto-update, it will look like it install correctly but the update process will silently fail because of new sandboxing restrictions. You will need to update manually for the time being, my apologies.
    • Add a new update check that shows a message at the top of the screen, when the screensaver is running, to let you know that a new version is available. This is still a work in progress and will be improved in the next beta. More info here
    • Add an option to change the shadow radius of the texts.
    • Add a new Countdown info type.
    ]]>
    jeu., 13 jan. 2020 16:00:00 +0100 10.9
    1.7.1 Automatic updates are broken for the time being in Catalina. If you try to auto-update, it will look like it install correctly but the update process will silently fail because of new sandboxing restrictions. You will need to update manually for the time being, my apologies.
    • Brings back "Allow right arrow to skip" for macOS versions prior to Catalina. That feature still won't work on Catalina.
    • Add seamless looping if you only have one video in your playlist.
    • Fix "new style" settings that weren't saved immediately, causing a discrepancy if you didn't close the preferences panel before launching the screen saver (with a hot corner).
    ]]>
    mar., 11 jan. 2020 16:00:00 +0100 10.9
    1.7.1beta1 Automatic updates are still broken for the time being in Catalina. If you try to update, it will look like it install correctly but the update process silently fails because of new sandboxing restrictions. You will need to update manually for the time being until a solution is found, my apologies.
    • Brings back "Allow right arrow to skip" for macOS versions prior to Catalina. That feature still won't work on Catalina.
    • Add seamless looping if you only have one video in your playlist
    ]]>
    lun., 03 jan. 2020 16:00:00 +0100 10.9
    1.7.0 Note : - Catalina users, the auto-update process WILL still fail for the moment, please install this build manually. My apologies for the issue. This is a large rewrite, a few settings will be reset to default, including video format preference (4K, HDR), my apologies for this but those were needed to introduce some new features. New features :
    • New text rendering ! Aerial can now display information (such as location information, time, etc) on mulitple screens with new animations and better transitions. The settings for these are found in the new "Info" tab.
    • Support for the 11 new Sea videos that were just released. If you do not see the videos after the update, please go to the "Updates" tab and press "Check Now" in the New videos section.
    • New "Battery" Info can tell you your battery status (charging, %, etc)
    • Infos can now be shown in more positions on screen (top center, bottom center, and screen center)
    • Important fixes :
      • Fix the mini video issue on some systems using macOS Catalina, more info about this here.
      ]]> mer., 29 jan. 2020 21:00:00 +0100 10.9 1.6.3
    • Fix a bad crash with custom videos and HDR.

    If you are using or planning to upgrade to macOS Catalina, we very much suggest you check this issue about what to expect : https://github.com/JohnCoates/Aerial/issues/801

    ]]>
    jeu., 24 oct. 2019 14:00:00 +0100 10.9
    1.6.2
  • Fix a hang in Catalina when you can't/have a hard time resuming from screensaver.
  • Fix Custom Videos in Catalina. You can also drop a folder there to automatically add it.
  • If you are using or planning to upgrade to macOS Catalina, we very much suggest you check this issue about what to expect : https://github.com/JohnCoates/Aerial/issues/801

    ]]>
    mar., 8 oct. 2019 17:00:00 +0100 10.9
    1.6.1
  • Fixes the hang/crash happening when playing the next video on some systems with multiple monitor, since 1.5.0. Many thanks to @markusem !
  • If you are using or planning to upgrade to macOS Catalina, we very much suggest you check this issue about what to expect : https://github.com/JohnCoates/Aerial/issues/801

    ]]>
    mar., 1 oct. 2019 14:00:00 +0100 10.9
    1.6.0a
  • This version includes support for the 15 new videos introduced in tvOS 13, including the 10 Sea videos and 5 new Space videos 🎉
  • Adds HDR support for macOS Catalina 🎉
  • Starting with this release, Aerial is properly signed and notarized 🎉
  • Cache mechanism was changed to accommodate changes in macOS Catalina. We recommend you double check your cache settings after upgrading.
  • Add Dutch translation by @sebastiaanspeck !
  • Add Traditional Chinese and update Simplified Chinese translations by @LinkeyLeo !
  • Add advanced mode for finely tuning the Spanned viewing mode in multi monitor configurations
  • This version includes many fixes for various issues including with multi monitor setups and better support for older versions of macOS.
  • If you are using or planning to upgrade to macOS Catalina, we very much suggest you check this issue about what to expect : https://github.com/JohnCoates/Aerial/issues/801

    ]]>
    jeu., 26 sept. 2019 19:23:00 +0100 10.9
    1.5.1beta13 This is the beta feed ! Aerial is properly signed and notarized 🎉

    • Includes the new tvOS 13 videos, including sea and new space 🎉
    • macOS 10.15 Catalina users, we suggest you first check this issue : https://github.com/JohnCoates/Aerial/issues/801

      ]]> mer., 25 sep. 2019 20:16:00 +0100 10.9 1.5.0 1.5.0 brings some pretty big features to Aerial, most importantly:

      • - Completely rewritten multi monitor support. You can now enable and disable individual displays in the new Display tab.
      • - New "Spanned" viewing mode. Selecting this mode will span an Aerial video on all your (selected) screens. You can even adjust margins.
      • - Add your own videos to Aerial using the new Custom Videos features. You can add your own videos in the new video manager (found in the menu below the video list).

      This version also includes many, many bugfixes! You can check the changelog for more information https://github.com/JohnCoates/Aerial/blob/master/Documentation/ChangeLog.md

      ]]>
      ven., 31 mai. 2019 11:30:00 +0100 10.9
      1.4.99beta7 This is the beta feed!

      This version fixes many bugs in the new custom videos feature :

      • Videos will now play muted (!)
      • Video manager now shows informations about resolution and duration of a video
      • Fix stepper not working when adding a poi, fixed seeking when adding a poi
      • 4K custom videos now have the correct 4K badge in main UI
      • customvideos.json is now pretty printed for easier manual editing
      • Closing the custom video manager now properly reloads the video list in main UI

      Find more information in this issue #786 !

      Please report any bug you find !

      ]]>
      mer., 29 mai. 2019 13:20:29 +0100 10.9
      1.4.9 This is the beta feed.

      This is a bugfix for a crash for homebrew users and users who never starts the control panel. Sorry about that !

      ]]>
      mer., 1 mai. 2019 13:58:29 +0100 10.9
      1.4.8 This is the beta feed.

      This new version includes many new features and enhancements:

      • Support for January 25th video updates (finally)
      • Automatic updates support through Sparkle. You can manage how Aerial updates itself in the settings
      • Localization for community support in Arabic, Chinese Simplified, English, French, German, Hebrew, Polish and Spanish! Thanks to all the contributors. If you want to help, check our repository we very much welcome new contributions
      • You can now skip an Aerial with the right arrow key
      • You can now save your favorite videos sets to enable them quickly
      ]]>
      mar., 30 avr. 2019 18:40:29 +0100 10.9
      ================================================ FILE: issue_template.md ================================================ #### General troubleshooting tips Before logging an issue please check that: - [ ] You have the latest version installed (There may be a beta version that fixes your issue), see here for the latest releases and bug fixes: https://github.com/JohnCoates/Aerial/releases - [ ] Your issue isn't already mentioned in our [issues](https://github.com/JohnCoates/Aerial/issues). You may find a workaround there or a similar request already made. - [ ] Your problem isn't mentioned in the [troubleshooting page](https://github.com/JohnCoates/Aerial/blob/master/Documentation/Troubleshooting.md). If none of this fixes your issue, tell us about the problem you are experiencing or the feature you'd like to request. #### Required information In order to help us sort your issue, we ask that you provide the following information: - [ ] Mac model: - [ ] macOS version: - [ ] Monitor setup: If appropriate, please enable `Debug mode` and `Log to disk` in `Advanced` tab and replicate your bug, then attach the `AerialLog.txt` file (You can access this file through the Advanced tab). #### Description of issue / Feature request {{Replace this}} ================================================ FILE: lokalise.example.cfg ================================================ Token = "YOUR_LOKALISE_TOKEN" Project = "526621525c59d35a304987.58424707"