From 4ac529123d4c00fd706985bb058c97a78079c233 Mon Sep 17 00:00:00 2001 From: Kyle LeNeau Date: Mon, 29 Oct 2018 20:20:41 -0500 Subject: [PATCH] Adding a Makefile and fixing Swiftlint --- .swiftlint.yml | 37 +- .travis.yml | 6 +- Aerial.xcodeproj/project.pbxproj | 28 +- Aerial/App/AppDelegate.swift | 25 +- Aerial/Source/Controllers/Preferences.swift | 145 ++-- .../PreferencesWindowController.swift | 727 +++++++++--------- Aerial/Source/Models/AerialVideo.swift | 184 +++-- .../Models/Cache/AssetLoaderDelegate.swift | 30 +- .../Models/Cache/PoiStringProvider.swift | 103 +-- Aerial/Source/Models/Cache/VideoCache.swift | 188 +++-- .../Source/Models/Cache/VideoDownload.swift | 135 ++-- Aerial/Source/Models/Cache/VideoLoader.swift | 96 +-- Aerial/Source/Models/Cache/VideoManager.swift | 62 +- .../Downloads/AsynchronousOperation.swift | 51 +- .../Models/Downloads/DownloadManager.swift | 113 +-- Aerial/Source/Models/ErrorLog.swift | 48 +- .../Extensions/AVPlayerViewExtension.swift | 6 +- .../Extensions/CollectionType+Shuffling.swift | 17 +- Aerial/Source/Models/ManifestLoader.swift | 301 ++++---- Aerial/Source/Models/Time/Solar.swift | 117 +-- .../Source/Models/Time/TimeManagement.swift | 208 +++-- Aerial/Source/Views/AerialPlayerItem.swift | 4 +- Aerial/Source/Views/AerialView.swift | 503 ++++++------ Aerial/Source/Views/CheckCellView.swift | 48 +- Makefile | 29 + Tests/PreferencesTests.swift | 8 +- 26 files changed, 1600 insertions(+), 1619 deletions(-) create mode 100644 Makefile diff --git a/.swiftlint.yml b/.swiftlint.yml index bb87c2d8..1f5427fa 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,13 +1,30 @@ disabled_rules: - - trailing_whitespace - - force_cast - - function_body_length - - type_body_length - - file_length + # 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 -variable_name: +opt_in_rules: + # Prefer checking `isEmpty` over `count > 0` + - empty_count + +file_length: + warning: 600 + error: 1000 +line_length: 150 +trailing_comma: + mandatory_comma: true +identifier_name: min_length: - warning: 2 - error: 2 -line_length: - warning: 120 + warning: 2 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index ce5cfaf1..44f95981 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: objective-c osx_image: xcode10 after_success: -- bash <(curl -s https://raw.githubusercontent.com/codecov/codecov-bash/04845559fe1caefe990968cc9e3851573012b756/codecov) -J 'AerialApp' + - bash <(curl -s https://raw.githubusercontent.com/codecov/codecov-bash/04845559fe1caefe990968cc9e3851573012b756/codecov) -J 'AerialApp' +before_script: + - make lint script: -- xcodebuild -scheme 'AerialApp' -enableCodeCoverage YES clean test + - make test \ No newline at end of file diff --git a/Aerial.xcodeproj/project.pbxproj b/Aerial.xcodeproj/project.pbxproj index 0e367f76..cef687a8 100644 --- a/Aerial.xcodeproj/project.pbxproj +++ b/Aerial.xcodeproj/project.pbxproj @@ -355,7 +355,7 @@ isa = PBXNativeTarget; buildConfigurationList = FA143CE01BDA3E880041A82B /* Build configuration list for PBXNativeTarget "AerialApp" */; buildPhases = ( - FA74B8481D94DCE0004FE056 /* ShellScript */, + FA74B8481D94DCE0004FE056 /* Run Script - Swiftlint */, FA143CD21BDA3E880041A82B /* Sources */, FA143CD31BDA3E880041A82B /* Frameworks */, FA143CD41BDA3E880041A82B /* Resources */, @@ -391,6 +391,7 @@ isa = PBXNativeTarget; buildConfigurationList = FACAF1AF1BD9FC6000E539DC /* Build configuration list for PBXNativeTarget "Aerial" */; buildPhases = ( + 7E9B50FB2187D302002895ED /* Run Script - Swiftlint */, FACAF1A01BD9FC6000E539DC /* Sources */, FACAF1A11BD9FC6000E539DC /* Frameworks */, FACAF1A21BD9FC6000E539DC /* Headers */, @@ -496,18 +497,33 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - FA74B8481D94DCE0004FE056 /* ShellScript */ = { + 7E9B50FB2187D302002895ED /* Run Script - Swiftlint */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); + name = "Run Script - Swiftlint"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = ""; + shellScript = "if which swiftlint >/dev/null; then\n if [ -z \"$CI\" ]; then\n make --directory=${SRCROOT} xcode-lint\n fi\nelse\n echo \"warning: SwiftLint not installed, install using `brew install swiftlint`\"\nfi"; + }; + 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\nelse\n echo \"warning: SwiftLint not installed, install using `brew install swiftlint`\"\nfi"; }; /* End PBXShellScriptBuildPhase section */ @@ -615,7 +631,6 @@ SWIFT_OBJC_BRIDGING_HEADER = "Aerial/Source/Models/Time/Aerial-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = On; - SWIFT_VERSION = 4.2; }; name = Debug; }; @@ -634,7 +649,6 @@ SWIFT_COMPILATION_MODE = singlefile; SWIFT_OBJC_BRIDGING_HEADER = "Aerial/Source/Models/Time/Aerial-Bridging-Header.h"; SWIFT_SWIFT3_OBJC_INFERENCE = On; - SWIFT_VERSION = 4.2; }; name = Release; }; @@ -655,7 +669,6 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = On; - SWIFT_VERSION = 4.2; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AerialApp.app/Contents/MacOS/AerialApp"; }; name = Debug; @@ -675,7 +688,6 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.johncoates.Aerial-Tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_SWIFT3_OBJC_INFERENCE = On; - SWIFT_VERSION = 4.2; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AerialApp.app/Contents/MacOS/AerialApp"; }; name = Release; @@ -795,7 +807,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Aerial/Source/Models/Time/Aerial-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.2; WRAPPER_EXTENSION = saver; }; name = Debug; @@ -813,7 +824,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.johncoates.Aerial; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Aerial/Source/Models/Time/Aerial-Bridging-Header.h"; - SWIFT_VERSION = 4.2; WRAPPER_EXTENSION = saver; }; name = Release; diff --git a/Aerial/App/AppDelegate.swift b/Aerial/App/AppDelegate.swift index c3174c51..d567e640 100644 --- a/Aerial/App/AppDelegate.swift +++ b/Aerial/App/AppDelegate.swift @@ -13,36 +13,39 @@ class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet weak var window: NSWindow! lazy var preferencesWindowController: PreferencesWindowController = PreferencesWindowController() - + func applicationDidFinishLaunching(_ notification: Notification) { let objects = objectsFromNib(loadNibNamed: "PreferencesWindow") // We need to find the correct window in our nib - for object in objects { - if object is NSWindow { - if (object as! NSWindow).identifier!.rawValue == "preferencesWindow" { - setUp(preferencesWindow: (object as! NSWindow)) - } + let object = objects.first { object in + if let window = object as? NSWindow, window.identifier?.rawValue == "preferencesWindow" { + return true } + return false + } + + if let window = object as? NSWindow { + setUp(preferencesWindow: window) } } - + private func setUp(preferencesWindow window: NSWindow) { window.makeKeyAndOrderFront(self) window.styleMask = [.closable, .titled, .miniaturizable] - + var frame = window.frame frame.origin = window.frame.origin window.setFrame(frame, display: true) } - + private func objectsFromNib(loadNibNamed nibName: String) -> [AnyObject] { let bundle = Bundle.main - var topLevelObjects:NSArray? = NSArray() + var topLevelObjects: NSArray? = NSArray() bundle.loadNibNamed(NSNib.Name(nibName), owner: preferencesWindowController, topLevelObjects: &topLevelObjects) - + return topLevelObjects! as [AnyObject] } } diff --git a/Aerial/Source/Controllers/Preferences.swift b/Aerial/Source/Controllers/Preferences.swift index d3b5715a..b6ff81a0 100644 --- a/Aerial/Source/Controllers/Preferences.swift +++ b/Aerial/Source/Controllers/Preferences.swift @@ -9,10 +9,11 @@ import Foundation import ScreenSaver +// swiftlint:disable:next type_body_length class Preferences { - + // MARK: - Types - + fileprivate enum Identifiers: String { case differentAerialsOnEachDisplay = "differentAerialsOnEachDisplay" case multiMonitorMode = "multiMonitorMode" @@ -53,69 +54,70 @@ class Preferences { case dimOnlyAtNight = "dimOnlyAtNight" case dimOnlyOnBattery = "dimOnlyOnBattery" case dimInMinutes = "dimInMinutes" - + case solarMode = "solarMode" - + case overrideMargins = "overrideMargins" case marginX = "marginX" case marginY = "marginY" } - - enum SolarMode : Int { + + enum SolarMode: Int { case strict, official, civil, nautical, astronomical } - - enum VersionCheck : Int { + + enum VersionCheck: Int { case never, daily, weekly, monthly } - - enum ExtraCorner : Int { + + enum ExtraCorner: Int { case same, hOpposed, dOpposed } - - enum DescriptionCorner : Int { + + enum DescriptionCorner: Int { case topLeft, topRight, bottomLeft, bottomRight, random } - - enum FadeMode : Int { + + enum FadeMode: Int { + // swiftlint:disable:next identifier_name case disabled, t0_5, t1, t2 } - - enum MultiMonitorMode : Int { + + enum MultiMonitorMode: Int { case mainOnly, mirrored, independant } - - enum TimeMode : Int { + + enum TimeMode: Int { case disabled, nightShift, manual, lightDarkMode, coordinates } - - enum VideoFormat : Int { + + enum VideoFormat: Int { case v1080pH264, v1080pHEVC, v4KHEVC } - - enum DescriptionMode : Int { + + enum DescriptionMode: Int { case fade10seconds, always } - + static let sharedInstance = Preferences() - + lazy var userDefaults: UserDefaults = { let module = "com.JohnCoates.Aerial" - + guard let userDefaults = ScreenSaverDefaults(forModuleWithName: module) else { warnLog("Couldn't create ScreenSaverDefaults, creating generic UserDefaults") return UserDefaults() } - + return userDefaults }() - + // MARK: - Setup - + init() { registerDefaultValues() } - + func registerDefaultValues() { var defaultValues = [Identifiers: Any]() defaultValues[.differentAerialsOnEachDisplay] = false @@ -160,18 +162,17 @@ class Preferences { defaultValues[.marginX] = 50 defaultValues[.marginY] = 50 - let defaults = defaultValues.reduce([String: Any]()) { - (result, pair:(key: Identifiers, value: Any)) -> [String: Any] in + let defaults = defaultValues.reduce([String: Any]()) { (result, pair:(key: Identifiers, value: Any)) -> [String: Any] in var mutable = result mutable[pair.key.rawValue] = pair.value return mutable } - + userDefaults.register(defaults: defaults) } - + // MARK: - Variables - + var useCommunityDescriptions: Bool { get { return value(forIdentifier: .useCommunityDescriptions) @@ -207,7 +208,7 @@ class Preferences { setValue(forIdentifier: .dimOnlyOnBattery, value: newValue) } } - + var overrideMargins: Bool { get { return value(forIdentifier: .overrideMargins) @@ -216,7 +217,7 @@ class Preferences { setValue(forIdentifier: .overrideMargins, value: newValue) } } - + var dimInMinutes: Int? { get { return optionalValue(forIdentifier: .dimInMinutes) @@ -225,7 +226,7 @@ class Preferences { setValue(forIdentifier: .dimInMinutes, value: newValue) } } - + var marginX: Int? { get { return optionalValue(forIdentifier: .marginX) @@ -252,7 +253,7 @@ class Preferences { setValue(forIdentifier: .solarMode, value: newValue) } } - + var debugMode: Bool { get { return value(forIdentifier: .debugMode) @@ -261,7 +262,7 @@ class Preferences { setValue(forIdentifier: .debugMode, value: newValue) } } - + var logToDisk: Bool { get { return value(forIdentifier: .logToDisk) @@ -271,7 +272,7 @@ class Preferences { } } - var alsoVersionCheckBeta : Bool { + var alsoVersionCheckBeta: Bool { get { return value(forIdentifier: .alsoVersionCheckBeta) } @@ -288,7 +289,7 @@ class Preferences { setValue(forIdentifier: .showClock, value: newValue) } } - + var withSeconds: Bool { get { return value(forIdentifier: .withSeconds) @@ -297,7 +298,7 @@ class Preferences { setValue(forIdentifier: .withSeconds, value: newValue) } } - + var showMessage: Bool { get { return value(forIdentifier: .showMessage) @@ -315,7 +316,7 @@ class Preferences { setValue(forIdentifier: .latitude, value: newValue) } } - + var longitude: String? { get { return optionalValue(forIdentifier: .longitude) @@ -324,7 +325,7 @@ class Preferences { setValue(forIdentifier: .longitude, value: newValue) } } - + var showMessageString: String? { get { return optionalValue(forIdentifier: .showMessageString) @@ -333,7 +334,7 @@ class Preferences { setValue(forIdentifier: .showMessageString, value: newValue) } } - + var differentAerialsOnEachDisplay: Bool { get { return value(forIdentifier: .differentAerialsOnEachDisplay) @@ -342,7 +343,7 @@ class Preferences { setValue(forIdentifier: .differentAerialsOnEachDisplay, value: newValue) } } - + var cacheAerials: Bool { get { return value(forIdentifier: .cacheAerials) @@ -351,7 +352,7 @@ class Preferences { setValue(forIdentifier: .cacheAerials, value: newValue) } } - + var neverStreamVideos: Bool { get { return value(forIdentifier: .neverStreamVideos) @@ -360,7 +361,7 @@ class Preferences { setValue(forIdentifier: .neverStreamVideos, value: newValue) } } - + var neverStreamPreviews: Bool { get { return value(forIdentifier: .neverStreamPreviews) @@ -378,7 +379,7 @@ class Preferences { setValue(forIdentifier: .localizeDescriptions, value: newValue) } } - + var fontName: String? { get { return optionalValue(forIdentifier: .fontName) @@ -405,7 +406,7 @@ class Preferences { setValue(forIdentifier: .endDim, value: newValue) } } - + var fontSize: Double? { get { return optionalValue(forIdentifier: .fontSize) @@ -413,9 +414,9 @@ class Preferences { set { setValue(forIdentifier: .fontSize, value: newValue) } - + } - + var extraFontName: String? { get { return optionalValue(forIdentifier: .extraFontName) @@ -424,7 +425,7 @@ class Preferences { setValue(forIdentifier: .extraFontName, value: newValue) } } - + var extraFontSize: Double? { get { return optionalValue(forIdentifier: .extraFontSize) @@ -432,7 +433,7 @@ class Preferences { set { setValue(forIdentifier: .extraFontSize, value: newValue) } - + } var manualSunrise: String? { get { @@ -442,7 +443,7 @@ class Preferences { setValue(forIdentifier: .manualSunrise, value: newValue) } } - + var manualSunset: String? { get { return optionalValue(forIdentifier: .manualSunset) @@ -451,7 +452,7 @@ class Preferences { setValue(forIdentifier: .manualSunset, value: newValue) } } - + var customCacheDirectory: String? { get { return optionalValue(forIdentifier: .customCacheDirectory) @@ -469,7 +470,7 @@ class Preferences { setValue(forIdentifier: .versionCheck, value: newValue) } } - + var descriptionCorner: Int? { get { return optionalValue(forIdentifier: .descriptionCorner) @@ -478,7 +479,7 @@ class Preferences { setValue(forIdentifier: .descriptionCorner, value: newValue) } } - + var extraCorner: Int? { get { return optionalValue(forIdentifier: .extraCorner) @@ -487,7 +488,7 @@ class Preferences { setValue(forIdentifier: .extraCorner, value: newValue) } } - + var fadeMode: Int? { get { return optionalValue(forIdentifier: .fadeMode) @@ -505,7 +506,7 @@ class Preferences { setValue(forIdentifier: .fadeModeText, value: newValue) } } - + var timeMode: Int? { get { return optionalValue(forIdentifier: .timeMode) @@ -532,7 +533,7 @@ class Preferences { setValue(forIdentifier: .showDescriptionsMode, value: newValue) } } - + var multiMonitorMode: Int? { get { return optionalValue(forIdentifier: .multiMonitorMode) @@ -541,7 +542,7 @@ class Preferences { setValue(forIdentifier: .multiMonitorMode, value: newValue) } } - + var showDescriptions: Bool { get { return value(forIdentifier: .showDescriptions) @@ -551,31 +552,31 @@ class Preferences { value: newValue) } } - + func videoIsInRotation(videoID: String) -> Bool { let key = "remove\(videoID)" let removed = userDefaults.bool(forKey: key) return !removed } - + func setVideo(videoID: String, inRotation: Bool, synchronize: Bool = true) { let key = "remove\(videoID)" let removed = !inRotation userDefaults.set(removed, forKey: key) - + if synchronize { self.synchronize() } } - + // MARK: - Setting, Getting - + fileprivate func value(forIdentifier identifier: Identifiers) -> Bool { let key = identifier.rawValue return userDefaults.bool(forKey: key) } - + fileprivate func optionalValue(forIdentifier identifier: Identifiers) -> String? { let key = identifier.rawValue return userDefaults.string(forKey: key) @@ -585,12 +586,12 @@ class Preferences { let key = identifier.rawValue return userDefaults.integer(forKey: key) } - + fileprivate func optionalValue(forIdentifier identifier: Identifiers) -> Double? { let key = identifier.rawValue return userDefaults.double(forKey: key) } - + fileprivate func setValue(forIdentifier identifier: Identifiers, value: Any?) { let key = identifier.rawValue if value == nil { @@ -600,8 +601,8 @@ class Preferences { } synchronize() } - + func synchronize() { userDefaults.synchronize() } -} +} //swiftlint:disable:this file_length diff --git a/Aerial/Source/Controllers/PreferencesWindowController.swift b/Aerial/Source/Controllers/PreferencesWindowController.swift index 2d74ad7b..dc2bee8f 100644 --- a/Aerial/Source/Controllers/PreferencesWindowController.swift +++ b/Aerial/Source/Controllers/PreferencesWindowController.swift @@ -15,7 +15,7 @@ import CoreLocation class TimeOfDay { let title: String var videos: [AerialVideo] = [AerialVideo]() - + init(title: String) { self.title = title } @@ -26,7 +26,7 @@ class City { var day: TimeOfDay = TimeOfDay(title: "day") let name: String //var videos: [AerialVideo] = [AerialVideo]() - + init(name: String) { self.name = name } @@ -43,9 +43,9 @@ class City { } @objc(PreferencesWindowController) -class PreferencesWindowController: NSWindowController, NSOutlineViewDataSource, -NSOutlineViewDelegate { - enum HEVCMain10Support : Int { +// swiftlint:disable:next type_body_length +class PreferencesWindowController: NSWindowController, NSOutlineViewDataSource, NSOutlineViewDelegate { + enum HEVCMain10Support: Int { case notsupported, unsure, partial, supported } @@ -63,22 +63,22 @@ NSOutlineViewDelegate { @IBOutlet var neverStreamVideosCheckbox: NSButton! @IBOutlet var neverStreamPreviewsCheckbox: NSButton! @IBOutlet weak var downloadNowButton: NSButton! - + @IBOutlet var multiMonitorModePopup: NSPopUpButton! @IBOutlet var popupVideoFormat: NSPopUpButton! @IBOutlet var descriptionModePopup: NSPopUpButton! @IBOutlet var fadeInOutModePopup: NSPopUpButton! @IBOutlet weak var fadeInOutTextModePopup: NSPopUpButton! - + @IBOutlet weak var downloadProgressIndicator: NSProgressIndicator! @IBOutlet weak var downloadStopButton: NSButton! @IBOutlet var versionLabel: NSTextField! - + @IBOutlet var popover: NSPopover! - + @IBOutlet var popoverTime: NSPopover! @IBOutlet var linkTimeWikipediaButton: NSButton! - + @IBOutlet var popoverH264Indicator: NSButton! @IBOutlet var popoverHEVCIndicator: NSButton! @IBOutlet var popoverH264Label: NSTextField! @@ -89,22 +89,22 @@ NSOutlineViewDelegate { @IBOutlet var timeManualRadio: NSButton! @IBOutlet var timeLightDarkModeRadio: NSButton! @IBOutlet var timeCalculateRadio: NSButton! - + @IBOutlet var nightShiftLabel: NSTextField! @IBOutlet var lightDarkModeLabel: NSTextField! @IBOutlet var latitudeTextField: NSTextField! @IBOutlet var longitudeTextField: NSTextField! @IBOutlet var findCoordinatesButton: NSButton! - + @IBOutlet var calculateCoordinatesLabel: NSTextField! - + @IBOutlet var latitudeFormatter: NumberFormatter! - + @IBOutlet var longitudeFormatter: NumberFormatter! - + @IBOutlet var solarModePopup: NSPopUpButton! - + @IBOutlet var sunriseTime: NSDatePicker! @IBOutlet var sunsetTime: NSDatePicker! @IBOutlet var iconTime1: NSImageCell! @@ -121,78 +121,79 @@ NSOutlineViewDelegate { @IBOutlet var changeCornerMargins: NSButton! @IBOutlet var marginHorizontalTextfield: NSTextField! @IBOutlet var marginVerticalTextfield: NSTextField! - + @IBOutlet var previewDisabledTextfield: NSTextField! @IBOutlet var fontPickerButton: NSButton! - + @IBOutlet var fontResetButton: NSButton! @IBOutlet var extraFontPickerButton: NSButton! @IBOutlet var extraFontResetButton: NSButton! @IBOutlet var currentFontLabel: NSTextField! @IBOutlet var currentLocaleLabel: NSTextField! - + @IBOutlet var showClockCheckbox: NSButton! @IBOutlet weak var withSecondsCheckbox: NSButton! @IBOutlet var showExtraMessage: NSButton! @IBOutlet var extraMessageTextField: NSTextField! @IBOutlet var extraMessageFontLabel: NSTextField! @IBOutlet weak var extraCornerPopup: NSPopUpButton! - + @IBOutlet var dimBrightness: NSButton! @IBOutlet var dimStartFrom: NSSlider! @IBOutlet var dimFadeTo: NSSlider! @IBOutlet var dimFadeInMinutes: NSTextField! @IBOutlet var dimOnlyAtNight: NSButton! @IBOutlet var dimOnlyOnBattery: NSButton! - + @IBOutlet var sleepAfterLabel: NSTextField! - + @IBOutlet var logPanel: NSPanel! @IBOutlet weak var showLogBottomClick: NSButton! @IBOutlet weak var logTableView: NSTableView! @IBOutlet weak var debugModeCheckbox: NSButton! @IBOutlet weak var logToDiskCheckbox: NSButton! - + @IBOutlet weak var cacheSizeTextField: NSTextField! - + var player: AVPlayer = AVPlayer() - + var videos: [AerialVideo]? // cities -> time of day -> videos var cities = [City]() - + static var loadedJSON: Bool = false - + lazy var preferences = Preferences.sharedInstance - + let fontManager: NSFontManager var fontEditing = 0 // To track the font we are changing - - var highestLevel : ErrorLevel? // To track the largest level of error received - + + var highestLevel: ErrorLevel? // To track the largest level of error received + var savedBrightness: Float? - + var locationManager: CLLocationManager? - + // MARK: - Init required init?(coder decoder: NSCoder) { self.fontManager = NSFontManager.shared debugLog("pwc init1") super.init(coder: decoder) } - + // We start here from SysPref and App mode override init(window: NSWindow?) { self.fontManager = NSFontManager.shared debugLog("pwc init2") super.init(window: window) } - + // MARK: - Lifecycle - + + // swiftlint:disable:next cyclomatic_complexity override func awakeFromNib() { super.awakeFromNib() - + // tmp let tm = TimeManagement.sharedInstance debugLog("isonbattery") @@ -200,18 +201,16 @@ NSOutlineViewDelegate { // let logger = Logger.sharedInstance logger.addCallback {level in - self.updateLogs(level:level) + self.updateLogs(level: level) } let videoManager = VideoManager.sharedInstance - videoManager.addCallback { done,total in - self.updateDownloads(done: done,total: total) + videoManager.addCallback { done, total in + self.updateDownloads(done: done, total: total) } self.fontManager.target = self latitudeFormatter.maximumSignificantDigits = 10 longitudeFormatter.maximumSignificantDigits = 10 - - // This used to grab the preview player and put it in our own video preview thing. // While kinda cool, it showed a random video that wasn't selected, and with new lifecycle, it was paused /*if let previewPlayer = AerialView.previewPlayer { @@ -239,29 +238,28 @@ NSOutlineViewDelegate { iconTime3.image = NSImage(named: NSImage.touchBarOpenInBrowserTemplateName) findCoordinatesButton.image = NSImage(named: NSImage.touchBarOpenInBrowserTemplateName) } - + // Help popover, GVA detection requires 10.13 if #available(OSX 10.13, *) { - if !VTIsHardwareDecodeSupported(kCMVideoCodecType_H264) - { + if !VTIsHardwareDecodeSupported(kCMVideoCodecType_H264) { popoverH264Label.stringValue = "H264 acceleration not supported" popoverH264Indicator.image = NSImage(named: NSImage.statusUnavailableName) } - if !VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) - { + if !VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) { popoverHEVCLabel.stringValue = "HEVC Main10 acceleration not supported" popoverHEVCIndicator.image = NSImage(named: NSImage.statusUnavailableName) } else { - if (isHEVCMain10HWDecodingAvailable() == .supported) { + switch isHEVCMain10HWDecodingAvailable() { + case .supported: popoverHEVCLabel.stringValue = "HEVC Main10 acceleration is supported" popoverHEVCIndicator.image = NSImage(named: NSImage.statusAvailableName) - } else if (isHEVCMain10HWDecodingAvailable() == .notsupported) { + case .notsupported: popoverHEVCLabel.stringValue = "HEVC Main10 acceleration is not supported" popoverHEVCIndicator.image = NSImage(named: NSImage.statusUnavailableName) - } else if (isHEVCMain10HWDecodingAvailable() == .partial) { + case .partial: popoverHEVCLabel.stringValue = "HEVC Main10 acceleration is partially supported" popoverHEVCIndicator.image = NSImage(named: NSImage.statusPartiallyAvailableName) - } else { + default: popoverHEVCLabel.stringValue = "HEVC Main10 acceleration status unknown" popoverHEVCIndicator.image = NSImage(named: NSImage.cautionName) } @@ -273,14 +271,14 @@ NSOutlineViewDelegate { popoverH264Label.stringValue = "macOS 10.13 or above required" popoverHEVCLabel.stringValue = "Hardware acceleration status unknown" } - + // Fonts for descriptions and extra (clock/msg) currentFontLabel.stringValue = preferences.fontName! + ", \(preferences.fontSize!) pt" extraMessageFontLabel.stringValue = preferences.extraFontName! + ", \(preferences.extraFontSize!) pt" // Extra message extraMessageTextField.stringValue = preferences.showMessageString! - + // Grab preferred language as proper string let printOutputLocale: NSLocale = NSLocale(localeIdentifier: Locale.preferredLanguages[0]) if let deviceLanguageName: String = printOutputLocale.displayName(forKey: NSLocale.Key.identifier, value: Locale.preferredLanguages[0]) { @@ -288,7 +286,7 @@ NSOutlineViewDelegate { } else { currentLocaleLabel.stringValue = "" } - + // Videos panel playerView.player = player playerView.controlsStyle = .none @@ -301,7 +299,7 @@ NSOutlineViewDelegate { selector: #selector(playerItemDidReachEnd(notification:)), name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem) - + if #available(OSX 10.12, *) { } else { showClockCheckbox.isEnabled = false @@ -311,21 +309,21 @@ NSOutlineViewDelegate { if preferences.debugMode { debugModeCheckbox.state = .on } - + if preferences.logToDisk { logToDiskCheckbox.state = .on } - + // Text panel if preferences.showClock { showClockCheckbox.state = .on withSecondsCheckbox.isEnabled = true } - + if preferences.withSeconds { withSecondsCheckbox.state = .on } - + if preferences.showMessage { showExtraMessage.state = .on extraMessageTextField.isEnabled = true @@ -341,7 +339,7 @@ NSOutlineViewDelegate { if preferences.localizeDescriptions { localizeForTvOS12Checkbox.state = .on } - + if preferences.overrideMargins { changeCornerMargins.state = .on marginHorizontalTextfield.isEnabled = true @@ -352,19 +350,19 @@ NSOutlineViewDelegate { if preferences.neverStreamVideos { neverStreamVideosCheckbox.state = .on } - + if preferences.neverStreamPreviews { neverStreamPreviewsCheckbox.state = .on } - + if !preferences.useCommunityDescriptions { useCommunityCheckbox.state = .off } - + if !preferences.cacheAerials { cacheAerialsAsTheyPlayCheckbox.state = .off } - + // Brightness panel if preferences.dimBrightness { dimBrightness.state = .on @@ -380,7 +378,7 @@ NSOutlineViewDelegate { dimFadeTo.isEnabled = false dimFadeInMinutes.isEnabled = false } - + if preferences.dimOnlyOnBattery { dimOnlyOnBattery.state = .on } @@ -390,7 +388,7 @@ NSOutlineViewDelegate { dimStartFrom.doubleValue = preferences.startDim ?? 0.5 dimFadeTo.doubleValue = preferences.endDim ?? 0.1 dimFadeInMinutes.stringValue = String(preferences.dimInMinutes!) - + // Time mode let timeManagement = TimeManagement.sharedInstance // Light/Dark mode only available on Mojave+ @@ -406,11 +404,10 @@ NSOutlineViewDelegate { timeNightShiftRadio.isEnabled = false } nightShiftLabel.stringValue = NSReason - + let (_, reason) = timeManagement.calculateFromCoordinates() calculateCoordinatesLabel.stringValue = reason - let dateFormatter = DateFormatter() dateFormatter.dateFormat = "HH:mm" if let dateSunrise = dateFormatter.date(from: preferences.manualSunrise!) { @@ -419,13 +416,13 @@ NSOutlineViewDelegate { if let dateSunset = dateFormatter.date(from: preferences.manualSunset!) { sunsetTime.dateValue = dateSunset } - + latitudeTextField.stringValue = preferences.latitude! longitudeTextField.stringValue = preferences.longitude! marginHorizontalTextfield.stringValue = String(preferences.marginX!) marginVerticalTextfield.stringValue = String(preferences.marginY!) - + // Handle the time radios switch preferences.timeMode { case Preferences.TimeMode.nightShift.rawValue: @@ -439,7 +436,7 @@ NSOutlineViewDelegate { default: timeDisabledRadio.state = NSControl.StateValue.on } - + // Handle the corner radios switch preferences.descriptionCorner { case Preferences.DescriptionCorner.topLeft.rawValue: @@ -455,36 +452,36 @@ NSOutlineViewDelegate { } solarModePopup.selectItem(at: preferences.solarMode!) - + multiMonitorModePopup.selectItem(at: preferences.multiMonitorMode!) - + popupVideoFormat.selectItem(at: preferences.videoFormat!) - + descriptionModePopup.selectItem(at: preferences.showDescriptionsMode!) - + fadeInOutModePopup.selectItem(at: preferences.fadeMode!) fadeInOutTextModePopup.selectItem(at: preferences.fadeModeText!) extraCornerPopup.selectItem(at: preferences.extraCorner!) - + colorizeProjectPageLinks() - + if let cacheDirectory = VideoCache.cacheDirectory { cacheLocation.url = URL(fileURLWithPath: cacheDirectory as String) } else { cacheLocation.url = nil } - + let sleepTime = timeManagement.getCurrentSleepTime() - if (sleepTime != 0) { + if sleepTime != 0 { sleepAfterLabel.stringValue = "Your Mac currently goes to sleep after \(sleepTime) minutes" } else { sleepAfterLabel.stringValue = "Unable to determine your Mac sleep settings" } } - + override func windowDidLoad() { super.windowDidLoad() @@ -492,28 +489,28 @@ NSOutlineViewDelegate { outlineView.reloadData() debugLog("wdl") } - + @IBAction func close(_ sender: AnyObject?) { logPanel.close() window?.sheetParent?.endSheet(window!) } - + // MARK: Video playback - + // Rewind preview video when reaching end @objc func playerItemDidReachEnd(notification: Notification) { if let playerItem: AVPlayerItem = notification.object as? AVPlayerItem { - let url:URL? = (playerItem.asset as? AVURLAsset)?.url + let url: URL? = (playerItem.asset as? AVURLAsset)?.url - if (url!.absoluteString.starts(with: "file")) { + if url!.absoluteString.starts(with: "file") { playerItem.seek(to: CMTime.zero, completionHandler: nil) self.player.play() } } } - + // MARK: - Setup - + fileprivate func colorizeProjectPageLinks() { let color = NSColor(calibratedRed: 0.18, green: 0.39, blue: 0.76, alpha: 1) var coloredLink = NSMutableAttributedString(attributedString: projectPageLink.attributedTitle) @@ -530,7 +527,7 @@ NSOutlineViewDelegate { value: color, range: fullRange) secondProjectPageLink.attributedTitle = coloredLink - + // We have an extra project link on the video format popover, color it too coloredLink = NSMutableAttributedString(attributedString: linkTimeWikipediaButton.attributedTitle) fullRange = NSRange(location: 0, length: coloredLink.length) @@ -539,15 +536,14 @@ NSOutlineViewDelegate { range: fullRange) linkTimeWikipediaButton.attributedTitle = coloredLink } - - + // MARK: - Video panel - - @IBAction func popupVideoFormatChange(_ sender:NSPopUpButton) { + + @IBAction func popupVideoFormatChange(_ sender: NSPopUpButton) { debugLog("UI popupVideoFormat: \(sender.indexOfSelectedItem)") preferences.videoFormat = sender.indexOfSelectedItem preferences.synchronize() - + outlineView.reloadData() } @@ -555,13 +551,13 @@ NSOutlineViewDelegate { popover.show(relativeTo: button.preparedContentRect, of: button, preferredEdge: .maxY) } - @IBAction func multiMonitorModePopupChange(_ sender:NSPopUpButton) { + @IBAction func multiMonitorModePopupChange(_ sender: NSPopUpButton) { debugLog("UI multiMonitorMode: \(sender.indexOfSelectedItem)") preferences.multiMonitorMode = sender.indexOfSelectedItem preferences.synchronize() } - - @IBAction func fadeInOutModePopupChange(_ sender:NSPopUpButton) { + + @IBAction func fadeInOutModePopupChange(_ sender: NSPopUpButton) { debugLog("UI fadeInOutMode: \(sender.indexOfSelectedItem)") preferences.fadeMode = sender.indexOfSelectedItem preferences.synchronize() @@ -569,7 +565,7 @@ NSOutlineViewDelegate { func updateDownloads(done: Int, total: Int) { print("VMQueue: done : \(done) \(total)") - if (total == 0) { + if total == 0 { downloadProgressIndicator.isHidden = true downloadStopButton.isHidden = true downloadNowButton.isEnabled = true @@ -586,43 +582,43 @@ NSOutlineViewDelegate { let videoManager = VideoManager.sharedInstance videoManager.cancelAll() } - + // MARK: - Mac Model detection and HEVC Main10 detection private func getMacModel() -> String { var size = 0 sysctlbyname("hw.model", nil, &size, nil, 0) - var machine = [CChar](repeating: 0, count: size) + 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) -> NSNumber { // Substring the thing - let s = macModel.dropFirst(macSubmodel.count) + let str = macModel.dropFirst(macSubmodel.count) let formatter = NumberFormatter() formatter.locale = Locale(identifier: "fr_FR") - if let n = formatter.number(from: String(s)) { - return n + if let number = formatter.number(from: String(str)) { + return number } else { return 0 } } - + private func getHEVCMain10Support(macModel: String, macSubmodel: String, partial: Double, full: Double) -> HEVCMain10Support { - let v = extractMacVersion(macModel: macModel, macSubmodel: macSubmodel) - - if v.doubleValue > full { + let ver = extractMacVersion(macModel: macModel, macSubmodel: macSubmodel) + + if ver.doubleValue > full { return .supported - } else if v.doubleValue > partial { + } else if ver.doubleValue > partial { return .partial } else { return .notsupported } } - + private func isHEVCMain10HWDecodingAvailable() -> HEVCMain10Support { let macModel = getMacModel() - + // iMacPro - always if macModel.starts(with: "iMacPro") { return .supported @@ -646,22 +642,19 @@ NSOutlineViewDelegate { // Hackintosh/new SKUs may fail this test return .unsure } - - - + // MARK: - Text panel - + @IBAction func showDescriptionsClick(button: NSButton?) { let state = showDescriptionsCheckbox.state let onState = (state == NSControl.StateValue.on) preferences.showDescriptions = onState debugLog("UI showDescriptions: \(onState)") - - changeTextState(to:onState) + + changeTextState(to: onState) } - - func changeTextState(to:Bool) - { + + func changeTextState(to: Bool) { // Location information useCommunityCheckbox.isEnabled = to localizeForTvOS12Checkbox.isEnabled = to @@ -681,7 +674,7 @@ NSOutlineViewDelegate { cornerBottomLeft.isEnabled = to cornerBottomRight.isEnabled = to cornerRandom.isEnabled = to - + // Extra info, linked too showClockCheckbox.isEnabled = to if (to && showClockCheckbox.state == .on) || !to { @@ -696,84 +689,83 @@ NSOutlineViewDelegate { extraMessageFontLabel.isEnabled = to extraCornerPopup.isEnabled = to } - + @IBAction func useCommunityClick(_ button: NSButton) { let state = useCommunityCheckbox.state let onState = (state == NSControl.StateValue.on) preferences.useCommunityDescriptions = onState debugLog("UI useCommunity: \(onState)") } - + @IBAction func localizeForTvOS12Click(button: NSButton?) { let state = localizeForTvOS12Checkbox.state let onState = (state == NSControl.StateValue.on) preferences.localizeDescriptions = onState debugLog("UI localizeDescriptions: \(onState)") } - - @IBAction func descriptionModePopupChange(_ sender:NSPopUpButton) { + + @IBAction func descriptionModePopupChange(_ sender: NSPopUpButton) { debugLog("UI descriptionMode: \(sender.indexOfSelectedItem)") preferences.showDescriptionsMode = sender.indexOfSelectedItem preferences.synchronize() } - - @IBAction func fontPickerClick(_ sender:NSButton?) { + + @IBAction func fontPickerClick(_ sender: NSButton?) { // Make a panel let fp = self.fontManager.fontPanel(true) - + // Set current font - if let font = NSFont(name: preferences.fontName!,size: CGFloat(preferences.fontSize!)) { + if let font = NSFont(name: preferences.fontName!, size: CGFloat(preferences.fontSize!)) { fp?.setPanelFont(font, isMultiple: false) - + } else { fp?.setPanelFont(NSFont(name: "Helvetica Neue Medium", size: 28)!, isMultiple: false) } - + // push the panel but mark which one we are editing fontEditing = 0 fp?.makeKeyAndOrderFront(sender) } - - @IBAction func fontResetClick(_ sender:NSButton?) { + + @IBAction func fontResetClick(_ sender: NSButton?) { preferences.fontName = "Helvetica Neue Medium" preferences.fontSize = 28 - + // Update our label currentFontLabel.stringValue = preferences.fontName! + ", \(preferences.fontSize!) pt" } - - @IBAction func extraFontPickerClick(_ sender:NSButton?) { + + @IBAction func extraFontPickerClick(_ sender: NSButton?) { // Make a panel let fp = self.fontManager.fontPanel(true) - + // Set current font - if let font = NSFont(name: preferences.extraFontName!,size: CGFloat(preferences.extraFontSize!)) { + if let font = NSFont(name: preferences.extraFontName!, size: CGFloat(preferences.extraFontSize!)) { fp?.setPanelFont(font, isMultiple: false) - + } else { fp?.setPanelFont(NSFont(name: "Helvetica Neue Medium", size: 28)!, isMultiple: false) } - + // push the panel but mark which one we are editing fontEditing = 1 fp?.makeKeyAndOrderFront(sender) } - - @IBAction func extraFontResetClick(_ sender:NSButton?) { + + @IBAction func extraFontResetClick(_ sender: NSButton?) { preferences.extraFontName = "Helvetica Neue Medium" preferences.extraFontSize = 28 - + // Update our label extraMessageFontLabel.stringValue = preferences.extraFontName! + ", \(preferences.extraFontSize!) pt" } - - + @IBAction func extraTextFieldChange(_ sender: NSTextField) { debugLog("UI extraTextField \(sender.stringValue)") preferences.showMessageString = sender.stringValue } - - @IBAction func descriptionCornerChange(_ sender:NSButton?) { + + @IBAction func descriptionCornerChange(_ sender: NSButton?) { if sender == cornerTopLeft { preferences.descriptionCorner = Preferences.DescriptionCorner.topLeft.rawValue } else if sender == cornerTopRight { @@ -793,13 +785,13 @@ NSOutlineViewDelegate { withSecondsCheckbox.isEnabled = onState debugLog("UI showClock: \(onState)") } - + @IBAction func withSecondsClick(_ sender: NSButton) { let onState = (sender.state == NSControl.StateValue.on) preferences.withSeconds = onState debugLog("UI withSeconds: \(onState)") } - + @IBAction func showExtraMessageClick(_ sender: NSButton) { let onState = (sender.state == NSControl.StateValue.on) // We also need to enable/disable our message field @@ -808,13 +800,13 @@ NSOutlineViewDelegate { debugLog("UI showExtraMessage: \(onState)") } - + @IBAction func fadeInOutTextModePopupChange(_ sender: NSPopUpButton) { debugLog("UI fadeInOutTextMode: \(sender.indexOfSelectedItem)") preferences.fadeModeText = sender.indexOfSelectedItem preferences.synchronize() } - + @IBAction func extraCornerPopupChange(_ sender: NSPopUpButton) { debugLog("UI extraCorner: \(sender.indexOfSelectedItem)") preferences.extraCorner = sender.indexOfSelectedItem @@ -824,7 +816,7 @@ NSOutlineViewDelegate { @IBAction func changeMarginsToCornerClick(_ sender: NSButton) { let onState = (sender.state == NSControl.StateValue.on) debugLog("UI changeMarginsToCorner: \(onState)") - + marginHorizontalTextfield.isEnabled = onState marginVerticalTextfield.isEnabled = onState preferences.overrideMargins = onState @@ -840,13 +832,13 @@ NSOutlineViewDelegate { debugLog("UI marginYChange: \(sender.stringValue)") } // MARK: - Cache panel - + func updateCacheSize() { // get your directory url let documentsDirectoryURL = URL(fileURLWithPath: VideoCache.cacheDirectory!) - + // FileManager.default.urls(for: VideoCache.cacheDirectory, in: .userDomainMask).first! - + // check if the url is a directory if (try? documentsDirectoryURL.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true { var folderSize = 0 @@ -861,32 +853,32 @@ NSOutlineViewDelegate { cacheSizeTextField.stringValue = "Cache all videos (current cache size \(sizeToDisplay))" } } - + @IBAction func cacheAerialsAsTheyPlayClick(_ button: NSButton!) { let onState = (button.state == NSControl.StateValue.on) preferences.cacheAerials = onState debugLog("UI cacheAerialAsTheyPlay: \(onState)") } - + @IBAction func neverStreamVideosClick(_ button: NSButton!) { let onState = (button.state == NSControl.StateValue.on) preferences.neverStreamVideos = onState debugLog("UI neverStreamVideos: \(onState)") } - + @IBAction func neverStreamPreviewsClick(_ button: NSButton!) { let onState = (button.state == NSControl.StateValue.on) preferences.neverStreamPreviews = onState debugLog("UI neverStreamPreviews: \(onState)") } - + @IBAction func showInFinder(_ button: NSButton!) { NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: VideoCache.cacheDirectory!) } @IBAction func userSetCacheLocation(_ button: NSButton?) { let openPanel = NSOpenPanel() - + openPanel.canChooseDirectories = true openPanel.canChooseFiles = false openPanel.canCreateDirectories = true @@ -894,13 +886,12 @@ NSOutlineViewDelegate { openPanel.title = "Choose Aerial Cache Directory" openPanel.prompt = "Choose" openPanel.directoryURL = cacheLocation.url - + openPanel.begin { result in - guard result.rawValue == NSFileHandlingPanelOKButton, - openPanel.urls.count > 0 else { - return + guard result.rawValue == NSFileHandlingPanelOKButton, !openPanel.urls.isEmpty else { + return } - + let cacheDirectory = openPanel.urls[0] self.preferences.customCacheDirectory = cacheDirectory.path self.cacheLocation.url = cacheDirectory @@ -922,7 +913,7 @@ NSOutlineViewDelegate { // MARK: - Time panel - @IBAction func timeModeChange(_ sender:NSButton?) { + @IBAction func timeModeChange(_ sender: NSButton?) { if sender == timeDisabledRadio { preferences.timeMode = Preferences.TimeMode.disabled.rawValue } else if sender == timeNightShiftRadio { @@ -935,15 +926,15 @@ NSOutlineViewDelegate { preferences.timeMode = Preferences.TimeMode.coordinates.rawValue } } - - @IBAction func sunriseChange(_ sender:NSDatePicker?) { + + @IBAction func sunriseChange(_ sender: NSDatePicker?) { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "HH:mm" let sunriseString = dateFormatter.string(from: (sender?.dateValue)!) preferences.manualSunrise = sunriseString } - @IBAction func sunsetChange(_ sender:NSDatePicker?) { + @IBAction func sunsetChange(_ sender: NSDatePicker?) { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "HH:mm" let sunsetString = dateFormatter.string(from: (sender?.dateValue)!) @@ -953,7 +944,7 @@ NSOutlineViewDelegate { @IBAction func latitudeChange(_ sender: NSTextField) { preferences.latitude = sender.stringValue updateLatitudeLongitude() - + } @IBAction func longitudeChange(_ sender: NSTextField) { @@ -961,46 +952,46 @@ NSOutlineViewDelegate { preferences.longitude = sender.stringValue updateLatitudeLongitude() } - + func updateLatitudeLongitude() { let timeManagement = TimeManagement.sharedInstance let (_, reason) = timeManagement.calculateFromCoordinates() calculateCoordinatesLabel.stringValue = reason } - + @IBAction func solarModePopupChange(_ sender: NSPopUpButton) { preferences.solarMode = sender.indexOfSelectedItem debugLog("UI solarModePopupChange: \(sender.indexOfSelectedItem)") updateLatitudeLongitude() } - + @IBAction func helpTimeButtonClick(_ button: NSButton) { popoverTime.show(relativeTo: button.preparedContentRect, of: button, preferredEdge: .maxY) } - + @IBAction func linkToWikipediaTimeClick(_ sender: NSButton) { let workspace = NSWorkspace.shared let url = URL(string: "https://en.wikipedia.org/wiki/Twilight")! workspace.open(url) } - + @IBAction func findCoordinatesButtonClick(_ sender: NSButton) { debugLog("UI findCoordinatesButton") /*let tm = TimeManagement.sharedInstance tm.startLocationDetection()*/ - + locationManager = CLLocationManager() locationManager!.delegate = self locationManager!.desiredAccuracy = kCLLocationAccuracyBest locationManager!.distanceFilter = 100 locationManager!.purpose = "Aerial uses your location to calculate sunrise and sunset times" - + if CLLocationManager.locationServicesEnabled() { debugLog("Location services enabled") //print(locationManager.location) - _ = CLLocationManager.authorizationStatus() + _ = CLLocationManager.authorizationStatus() /*if status == .restricted || status == .denied { print("Location Denied") } @@ -1023,9 +1014,8 @@ NSOutlineViewDelegate { locationManager.startUpdatingLocation() }*/ } - - func pushCoordinates(_ coordinates: CLLocationCoordinate2D) - { + + func pushCoordinates(_ coordinates: CLLocationCoordinate2D) { latitudeTextField.stringValue = String(coordinates.latitude) longitudeTextField.stringValue = String(coordinates.longitude) @@ -1034,88 +1024,91 @@ NSOutlineViewDelegate { updateLatitudeLongitude() } // MARK: - Brightness panel - + @IBAction func dimBrightnessClick(_ button: NSButton) { let onState = (button.state == .on) preferences.dimBrightness = onState - + dimOnlyAtNight.isEnabled = onState dimOnlyOnBattery.isEnabled = onState dimStartFrom.isEnabled = onState dimFadeTo.isEnabled = onState dimFadeInMinutes.isEnabled = onState - + debugLog("UI dimBrightness: \(onState)") } - + @IBAction func dimOnlyAtNightClick(_ button: NSButton) { let onState = (button.state == .on) preferences.dimOnlyAtNight = onState debugLog("UI dimOnlyAtNight: \(onState)") } - + @IBAction func dimOnlyOnBattery(_ button: NSButton) { let onState = (button.state == .on) preferences.dimOnlyOnBattery = onState debugLog("UI dimOnlyOnBattery: \(onState)") } - + @IBAction func dimStartFromChange(_ sender: NSSliderCell) { let timeManagement = TimeManagement.sharedInstance - let event = NSApplication.shared.currentEvent - if (event != nil) { - if (event!.type != .leftMouseUp && event!.type != .leftMouseDown && event!.type != .leftMouseDragged) - { - //warnLog("Unexepected event type \(event!.type)") - return + guard let event = NSApplication.shared.currentEvent else { + return + } + + if event.type != .leftMouseUp && event.type != .leftMouseDown && event.type != .leftMouseDragged { + //warnLog("Unexepected event type \(event.type)") + return + } + + if event.type == .leftMouseUp { + if savedBrightness != nil { + timeManagement.setBrightness(level: savedBrightness!) + savedBrightness = nil } - if event!.type == .leftMouseUp { - if savedBrightness != nil { - timeManagement.setBrightness(level: savedBrightness!) - savedBrightness = nil - } - preferences.startDim = sender.doubleValue - debugLog("UI startDim: \(sender.doubleValue)") - } else { - if savedBrightness == nil { - savedBrightness = timeManagement.getBrightness() - } - timeManagement.setBrightness(level: sender.floatValue) + preferences.startDim = sender.doubleValue + debugLog("UI startDim: \(sender.doubleValue)") + } else { + if savedBrightness == nil { + savedBrightness = timeManagement.getBrightness() } + timeManagement.setBrightness(level: sender.floatValue) } } - + @IBAction func dimFadeToChange(_ sender: NSSliderCell) { let timeManagement = TimeManagement.sharedInstance - let event = NSApplication.shared.currentEvent - if (event != nil) { - if (event!.type != .leftMouseUp && event!.type != .leftMouseDown && event!.type != .leftMouseDragged) - { - warnLog("Unexepected event type \(event!.type)") + guard let event = NSApplication.shared.currentEvent else { + return + } + + if event.type != .leftMouseUp && event.type != .leftMouseDown && event.type != .leftMouseDragged { + warnLog("Unexepected event type \(event.type)") + } + + if event.type == .leftMouseUp { + if savedBrightness != nil { + timeManagement.setBrightness(level: savedBrightness!) + savedBrightness = nil } - if event!.type == .leftMouseUp { - if savedBrightness != nil { - timeManagement.setBrightness(level: savedBrightness!) - savedBrightness = nil - } - preferences.endDim = sender.doubleValue - debugLog("UI endDim: \(sender.doubleValue)") - } else { - if savedBrightness == nil { - savedBrightness = timeManagement.getBrightness() - } - timeManagement.setBrightness(level: sender.floatValue) + preferences.endDim = sender.doubleValue + debugLog("UI endDim: \(sender.doubleValue)") + } else { + if savedBrightness == nil { + savedBrightness = timeManagement.getBrightness() } + timeManagement.setBrightness(level: sender.floatValue) } } - + @IBAction func dimInMinutes(_ sender: NSTextField) { - if let i = Int(sender.stringValue) { - preferences.dimInMinutes = i + if let intValue = Int(sender.stringValue) { + preferences.dimInMinutes = intValue } - + debugLog("UI dimInMinutes \(sender.stringValue)") } + // MARK: - Advanced panel @IBAction func logButtonClick(_ sender: NSButton) { @@ -1128,59 +1121,59 @@ NSOutlineViewDelegate { } @IBAction func logCopyToClipboardClick(_ sender: NSButton) { - if (errorMessages.count > 0) { - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .none - dateFormatter.timeStyle = .medium - - var clipboard = "" - for log in errorMessages { - clipboard += dateFormatter.string(from:log.date) + " : " + log.message + "\n" - } - - let pasteBoard = NSPasteboard.general - pasteBoard.clearContents() - pasteBoard.setString(clipboard, forType: NSPasteboard.PasteboardType.string) + guard !errorMessages.isEmpty else { + return } + + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .none + dateFormatter.timeStyle = .medium + + var clipboard = "" + for log in errorMessages { + clipboard += dateFormatter.string(from: log.date) + " : " + log.message + "\n" + } + + let pasteBoard = NSPasteboard.general + pasteBoard.clearContents() + pasteBoard.setString(clipboard, forType: NSPasteboard.PasteboardType.string) } @IBAction func logRefreshClick(_ sender: NSButton) { logTableView.reloadData() } - + @IBAction func debugModeClick(_ button: NSButton) { let onState = (button.state == NSControl.StateValue.on) preferences.debugMode = onState debugLog("UI debugMode: \(onState)") } - + @IBAction func logToDiskClick(_ button: NSButton) { let onState = (button.state == NSControl.StateValue.on) preferences.logToDisk = onState debugLog("UI logToDisk: \(onState)") } - + @IBAction func showLogInFinder(_ button: NSButton!) { let logfile = VideoCache.cacheDirectory!.appending("/AerialLog.txt") // If we don't have a log, just show the folder if FileManager.default.fileExists(atPath: logfile) == false { NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: VideoCache.cacheDirectory!) - } - else { + } else { NSWorkspace.shared.selectFile(logfile, inFileViewerRootedAtPath: VideoCache.cacheDirectory!) } } - func updateLogs(level:ErrorLevel) - { + func updateLogs(level: ErrorLevel) { logTableView.reloadData() - if (highestLevel == nil) { + if highestLevel == nil { highestLevel = level - } else if (level.rawValue > highestLevel!.rawValue) { + } else if level.rawValue > highestLevel!.rawValue { highestLevel = level } - + switch highestLevel! { case ErrorLevel.debug: showLogBottomClick.title = "Show Debug" @@ -1195,10 +1188,10 @@ NSOutlineViewDelegate { showLogBottomClick.title = "Show Error" showLogBottomClick.image = NSImage.init(named: NSImage.stopProgressFreestandingTemplateName) } - - + showLogBottomClick.isHidden = false } + // MARK: - Menu @IBAction func outlineViewSettingsClick(_ button: NSButton) { let menu = NSMenu() @@ -1228,7 +1221,7 @@ NSOutlineViewDelegate { action: #selector(PreferencesWindowController.outlineViewDownloadAll(button:)), keyEquivalent: "", at: 6) - + let event = NSApp.currentEvent NSMenu.popUpContextMenu(menu, with: event!, for: button) } @@ -1236,16 +1229,16 @@ NSOutlineViewDelegate { @objc func outlineViewUncheckAll(button: NSButton) { setAllVideos(inRotation: false) } - + @objc func outlineViewCheckAll(button: NSButton) { setAllVideos(inRotation: true) } - + @objc func outlineViewCheck4K(button: NSButton) { guard let videos = videos else { return } - + for video in videos { if video.url4KHEVC != "" { preferences.setVideo(videoID: video.id, @@ -1258,15 +1251,15 @@ NSOutlineViewDelegate { } } preferences.synchronize() - + outlineView.reloadData() } - + @objc func outlineViewCheckCached(button: NSButton) { guard let videos = videos else { return } - + for video in videos { if video.isAvailableOffline { preferences.setVideo(videoID: video.id, @@ -1279,10 +1272,10 @@ NSOutlineViewDelegate { } } preferences.synchronize() - + outlineView.reloadData() } - + @objc func outlineViewDownloadChecked(button: NSButton) { guard let videos = videos else { return @@ -1297,62 +1290,60 @@ NSOutlineViewDelegate { } } } - + @objc func outlineViewDownloadAll(button: NSButton) { downloadAllVideos() } - + func downloadAllVideos() { guard let videos = videos else { return } let videoManager = VideoManager.sharedInstance - - for video in videos { - if !video.isAvailableOffline { - if !videoManager.isVideoQueued(id: video.id) { - videoManager.queueDownload(video) - } + + for video in videos where !video.isAvailableOffline { + if !videoManager.isVideoQueued(id: video.id) { + videoManager.queueDownload(video) } } } - + func setAllVideos(inRotation: Bool) { guard let videos = videos else { return } - + for video in videos { preferences.setVideo(videoID: video.id, inRotation: inRotation, synchronize: false) } preferences.synchronize() - + outlineView.reloadData() } - + // MARK: - Links - + @IBAction func pageProjectClick(_ button: NSButton?) { let workspace = NSWorkspace.shared let url = URL(string: "http://github.com/JohnCoates/Aerial")! workspace.open(url) } - + // MARK: - Manifest - + func loadJSON() { if PreferencesWindowController.loadedJSON { return } PreferencesWindowController.loadedJSON = true - + ManifestLoader.instance.addCallback { manifestVideos in self.loaded(manifestVideos: manifestVideos) } } - + func loaded(manifestVideos: [AerialVideo]) { var videos = [AerialVideo]() var cities = [String: City]() @@ -1360,20 +1351,20 @@ NSOutlineViewDelegate { // First day, then night for video in manifestVideos { let name = video.name - + if cities.keys.contains(name) == false { cities[name] = City(name: name) } let city = cities[name]! - + let timeOfDay = video.timeOfDay city.addVideoForTimeOfDay(timeOfDay, video: video) - + videos.append(video) } self.videos = videos - + // sort cities by name let unsortedCities = cities.values let sortedCities = unsortedCities.sorted { $0.name < $1.name } @@ -1385,40 +1376,40 @@ NSOutlineViewDelegate { self.outlineView.expandItem(nil, expandChildren: true) } } - + // MARK: - Outline View Delegate & Data Source - + func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { if item == nil { return cities.count } - + switch item { - case is TimeOfDay: - let timeOfDay = item as! TimeOfDay - return timeOfDay.videos.count - case is City: - let city = item as! City - - var count = 0 - - if city.night.videos.count > 0 { - count += 1 - } - - if city.day.videos.count > 0 { - count += 1 - } - return count + case is TimeOfDay: + let timeOfDay = item as! TimeOfDay + return timeOfDay.videos.count + case is City: + let city = item as! City - //let city = item as! City - //return city.day.videos.count + city.night.videos.count - default: - return 0 + var count = 0 + + if !city.night.videos.isEmpty { + count += 1 + } + + if !city.day.videos.isEmpty { + count += 1 + } + return count + + //let city = item as! City + //return city.day.videos.count + city.night.videos.count + default: + return 0 } - + } - + func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { switch item { case is TimeOfDay: @@ -1429,34 +1420,33 @@ NSOutlineViewDelegate { return false } } - + func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { if item == nil { return cities[index] } - + switch item { case is City: - let city = item as! City - - if index == 0 && city.day.videos.count > 0 { + + if index == 0 && !city.day.videos.isEmpty { return city.day } else { return city.night } //let city = item as! City //return city.videos[index] - + case is TimeOfDay: let timeOfDay = item as! TimeOfDay return timeOfDay.videos[index] - + default: return false } } - + func outlineView(_ outlineView: NSOutlineView, objectValueFor tableColumn: NSTableColumn?, byItem item: Any?) -> Any? { switch item { @@ -1466,21 +1456,21 @@ NSOutlineViewDelegate { case is TimeOfDay: let timeOfDay = item as! TimeOfDay return timeOfDay.title - + default: return "untitled" } } - + func outlineView(_ outlineView: NSOutlineView, shouldEdit tableColumn: NSTableColumn?, item: Any) -> Bool { return false } - + func outlineView(_ outlineView: NSOutlineView, dataCellFor tableColumn: NSTableColumn?, item: Any) -> NSCell? { let row = outlineView.row(forItem: item) return tableColumn!.dataCell(forRow: row) as? NSCell } - + func outlineView(_ outlineView: NSOutlineView, isGroupItem item: Any) -> Bool { switch item { case is TimeOfDay: @@ -1492,34 +1482,33 @@ NSOutlineViewDelegate { } } - func outlineView(_ outlineView: NSOutlineView, - viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { + func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { switch item { case is City: let city = item as! City let view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "HeaderCell"), owner: nil) as! NSTableCellView // if owner = self, awakeFromNib will be called for each created cell ! view.textField?.stringValue = city.name - + return view case is TimeOfDay: let timeOfDay = item as! TimeOfDay let view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "DataCell"), owner: nil) as! NSTableCellView // if owner = self, awakeFromNib will be called for each created cell ! - + view.textField?.stringValue = timeOfDay.title.capitalized - + let bundle = Bundle(for: PreferencesWindowController.self) - + // Use -dark icons in macOS 10.14+ Dark Mode let timeManagement = TimeManagement.sharedInstance var postfix = "" if timeManagement.isDarkModeEnabled() { postfix = "-dark" } - + if let imagePath = bundle.path(forResource: "icon-\(timeOfDay.title)"+postfix, - ofType:"pdf") { + ofType: "pdf") { let image = NSImage(contentsOfFile: imagePath) image!.size.width = 13 image!.size.height = 13 @@ -1529,7 +1518,7 @@ NSOutlineViewDelegate { } else { errorLog("\(#file) failed to find time of day icon") } - + return view case is AerialVideo: let video = item as! AerialVideo @@ -1542,14 +1531,13 @@ NSOutlineViewDelegate { view.setVideo(video: video) // For our Add button view.adaptIndicators() - if (video.secondaryName != "") { + if video.secondaryName != "" { view.textField?.stringValue = video.secondaryName - } - else { + } else { // One based index let number = video.arrayPosition + 1 let numberFormatter = NumberFormatter() - + numberFormatter.numberStyle = NumberFormatter.Style.spellOut guard let numberString = numberFormatter.string(from: number as NSNumber) @@ -1557,20 +1545,18 @@ NSOutlineViewDelegate { errorLog("outlineView: failed to create number with formatter") return nil } - + view.textField?.stringValue = numberString.capitalized } - - let isInRotation = preferences.videoIsInRotation(videoID: video.id) - + if isInRotation { view.checkButton.state = NSControl.StateValue.on } else { view.checkButton.state = NSControl.StateValue.off } - + view.onCheck = { checked in self.preferences.setVideo(videoID: video.id, inRotation: checked) @@ -1581,13 +1567,13 @@ NSOutlineViewDelegate { return nil } } - + func outlineView(_ outlineView: NSOutlineView, shouldSelectItem item: Any) -> Bool { switch item { case is AerialVideo: player = AVPlayer() playerView.player = player - + let video = item as! AerialVideo debugLog("Playing this preview \(video)") // Workaround for cached videos generating online traffic @@ -1597,17 +1583,16 @@ NSOutlineViewDelegate { let localitem = AVPlayerItem(url: localurl) player.replaceCurrentItem(with: localitem) player.play() - } - else if !preferences.neverStreamPreviews { + } else if !preferences.neverStreamPreviews { previewDisabledTextfield.isHidden = true - let asset = CachedOrCachingAsset(video.url) + let asset = cachedOrCachingAsset(video.url) let item = AVPlayerItem(asset: asset) player.replaceCurrentItem(with: item) player.play() } else { previewDisabledTextfield.isHidden = false } - + return true case is TimeOfDay: return false @@ -1615,23 +1600,23 @@ NSOutlineViewDelegate { return false } } - + func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat { switch item { - case is AerialVideo: - return 19 - case is TimeOfDay: - return 18 - case is City: - return 17 - default: - fatalError("unhandled item in heightOfRowByItem for \(item)") + case is AerialVideo: + return 19 + case is TimeOfDay: + return 18 + case is City: + return 17 + default: + fatalError("unhandled item in heightOfRowByItem for \(item)") } } func outlineView(_ outlineView: NSOutlineView, sizeToFitWidthOfColumn column: Int) -> CGFloat { return 0 } - + // MARK: - Caching /* var currentVideoDownload: VideoDownload? @@ -1702,14 +1687,14 @@ NSOutlineViewDelegate { } // MARK: - Core Location Delegates -extension PreferencesWindowController : CLLocationManagerDelegate { +extension PreferencesWindowController: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { debugLog("LM Coordinates") let currentLocation = locations[locations.count - 1] pushCoordinates(currentLocation.coordinate) locationManager!.stopUpdatingLocation() // We only want them once } - + func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { debugLog("LMauth status change : \(status.rawValue)") } @@ -1719,46 +1704,44 @@ extension PreferencesWindowController : CLLocationManagerDelegate { }*/ } - // MARK: - Font Panel Delegates -extension PreferencesWindowController : NSFontChanging { +extension PreferencesWindowController: NSFontChanging { func validModesForFontPanel(_ fontPanel: NSFontPanel) -> NSFontPanel.ModeMask { return [.size, .collection, .face] } - + func changeFont(_ sender: NSFontManager?) { // Set current font var oldFont = NSFont(name: "Helvetica Neue Medium", size: 28) - if (fontEditing == 0) { - if let tryFont = NSFont(name: preferences.fontName!,size: CGFloat(preferences.fontSize!)) { + if fontEditing == 0 { + if let tryFont = NSFont(name: preferences.fontName!, size: CGFloat(preferences.fontSize!)) { oldFont = tryFont } } else { - if let tryFont = NSFont(name: preferences.extraFontName!,size: CGFloat(preferences.extraFontSize!)) { + if let tryFont = NSFont(name: preferences.extraFontName!, size: CGFloat(preferences.extraFontSize!)) { oldFont = tryFont } } - + let newFont = sender?.convert(oldFont!) - if (fontEditing == 0) { + if fontEditing == 0 { preferences.fontName = newFont?.fontName preferences.fontSize = Double((newFont?.pointSize)!) - + // Update our label currentFontLabel.stringValue = preferences.fontName! + ", \(preferences.fontSize!) pt" } else { preferences.extraFontName = newFont?.fontName preferences.extraFontSize = Double((newFont?.pointSize)!) - + // Update our label extraMessageFontLabel.stringValue = preferences.extraFontName! + ", \(preferences.extraFontSize!) pt" } preferences.synchronize() } - } // MARK: - Log TableView Delegates @@ -1769,23 +1752,23 @@ extension PreferencesWindowController: NSTableViewDataSource { } } -extension PreferencesWindowController : NSTableViewDelegate { +extension PreferencesWindowController: NSTableViewDelegate { fileprivate enum CellIdentifiers { static let DateCell = "DateCellID" static let MessageCell = "MessageCellID" } - + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { var image: NSImage? var text: String = "" var cellIdentifier: String = "" - + let dateFormatter = DateFormatter() dateFormatter.dateStyle = .none dateFormatter.timeStyle = .medium - + let item = errorMessages[row] - + if tableColumn == tableView.tableColumns[0] { text = dateFormatter.string(from: item.date) cellIdentifier = CellIdentifiers.DateCell @@ -1804,13 +1787,13 @@ extension PreferencesWindowController : NSTableViewDelegate { text = item.message cellIdentifier = CellIdentifiers.MessageCell } - + if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: cellIdentifier), owner: nil) as? NSTableCellView { cell.textField?.stringValue = text cell.imageView?.image = image ?? nil return cell } - + return nil } -} +} // swiftlint:disable:this file_length diff --git a/Aerial/Source/Models/AerialVideo.swift b/Aerial/Source/Models/AerialVideo.swift index 79c00b56..300c8c3c 100644 --- a/Aerial/Source/Models/AerialVideo.swift +++ b/Aerial/Source/Models/AerialVideo.swift @@ -9,11 +9,11 @@ import Foundation import AVFoundation -enum Manifests : String { +enum Manifests: String { case tvOS10 = "tvos10.json", tvOS11 = "tvos11.json", tvOS12 = "entries.json" } -let SpaceVideos = [ "A837FA8C-C643-4705-AE92-074EFDD067F7", +let spaceVideos = [ "A837FA8C-C643-4705-AE92-074EFDD067F7", "2F72BC1E-3D76-456C-81EB-842EBA488C27", "A2BE2E4A-AD4B-428A-9C41-BDAE1E78E816", "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8", @@ -25,33 +25,33 @@ let SpaceVideos = [ "A837FA8C-C643-4705-AE92-074EFDD067F7", "78911B7E-3C69-47AD-B635-9C2486F6301D", "D60B4DDA-69EB-4841-9690-E8BAE7BC4F80", "7719B48A-2005-4011-9280-2F64EEC6FD91", - "63C042F0-90EF-4A95-B7CC-CC9A64BF8421"] - -let TimeInformation = [ "A837FA8C-C643-4705-AE92-074EFDD067F7":"night", // Africa Night - "2F72BC1E-3D76-456C-81EB-842EBA488C27":"day", // Africa and the Middle East - "A2BE2E4A-AD4B-428A-9C41-BDAE1E78E816":"night", // California to Vegas - "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8":"day", // Carribean - "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589":"day", // Carribean day - "6A74D52E-2447-4B84-AE45-0DEF2836C3CC":"night", // China - "F439B0A7-D18C-4B14-9681-6520E6A74FE9":"night", // Iran and Afghanistan - "62A926BE-AA0B-4A34-9653-78C4F130543F":"night", // Ireland to Asia - "6C3D54AE-0871-498A-81D0-56ED24E5FE9F":"night", // Korean and Japan Night - "78911B7E-3C69-47AD-B635-9C2486F6301D":"day", // New Zealand (sunrise...) - "D60B4DDA-69EB-4841-9690-E8BAE7BC4F80":"day", // Sahara and Italy - "7719B48A-2005-4011-9280-2F64EEC6FD91":"day", // Southern California to Baja - "63C042F0-90EF-4A95-B7CC-CC9A64BF8421":"day", // Western Africa to the Alps (sunset...) - "BAF76353-3475-4855-B7E1-CE96CC9BC3A7":"night", // Dubai - "30313BC1-BF20-45EB-A7B1-5A6FFDBD2488":"night", // Hong Kong - "89B1643B-06DD-4DEC-B1B0-774493B0F7B7":"night", // Los Angeles - "EC67726A-8212-4C5E-83CF-8412932740D2":"night", // Los Angeles - "A284F0BF-E690-4C13-92E2-4672D93E8DE5":"night" + "63C042F0-90EF-4A95-B7CC-CC9A64BF8421", ] + +let timeInformation = [ "A837FA8C-C643-4705-AE92-074EFDD067F7": "night", // Africa Night + "2F72BC1E-3D76-456C-81EB-842EBA488C27": "day", // Africa and the Middle East + "A2BE2E4A-AD4B-428A-9C41-BDAE1E78E816": "night", // California to Vegas + "D5CFB2FF-5F8C-4637-816B-3E42FC1229B8": "day", // Carribean + "4F881F8B-A7D9-4FDB-A917-17BF6AC5A589": "day", // Carribean day + "6A74D52E-2447-4B84-AE45-0DEF2836C3CC": "night", // China + "F439B0A7-D18C-4B14-9681-6520E6A74FE9": "night", // Iran and Afghanistan + "62A926BE-AA0B-4A34-9653-78C4F130543F": "night", // Ireland to Asia + "6C3D54AE-0871-498A-81D0-56ED24E5FE9F": "night", // Korean and Japan Night + "78911B7E-3C69-47AD-B635-9C2486F6301D": "day", // New Zealand (sunrise...) + "D60B4DDA-69EB-4841-9690-E8BAE7BC4F80": "day", // Sahara and Italy + "7719B48A-2005-4011-9280-2F64EEC6FD91": "day", // Southern California to Baja + "63C042F0-90EF-4A95-B7CC-CC9A64BF8421": "day", // Western Africa to the Alps (sunset...) + "BAF76353-3475-4855-B7E1-CE96CC9BC3A7": "night", // Dubai + "30313BC1-BF20-45EB-A7B1-5A6FFDBD2488": "night", // Hong Kong + "89B1643B-06DD-4DEC-B1B0-774493B0F7B7": "night", // Los Angeles + "EC67726A-8212-4C5E-83CF-8412932740D2": "night", // Los Angeles + "A284F0BF-E690-4C13-92E2-4672D93E8DE5": "night", ] class AerialVideo: CustomStringConvertible, Equatable { static func ==(lhs: AerialVideo, rhs: AerialVideo) -> Bool { return lhs.id == rhs.id && lhs.url1080pHEVC == rhs.url1080pHEVC } - + let id: String let name: String let secondaryName: String @@ -64,69 +64,65 @@ class AerialVideo: CustomStringConvertible, Equatable { let poi: [String: String] let communityPoi: [String: String] var duration: Double - + var arrayPosition = 1 var contentLength = 0 var contentLengthChecked = false - + var isAvailableOffline: Bool { - get { - return VideoCache.isAvailableOffline(video: self) - } + return VideoCache.isAvailableOffline(video: self) } - - var url : URL { - get { - let preferences = Preferences.sharedInstance - - // We need to return the closest available format, not pretty - if (preferences.videoFormat == Preferences.VideoFormat.v4KHEVC.rawValue) - { - if (url4KHEVC != "") { - return URL(string: self.url4KHEVC)! - } - else if (url1080pHEVC != "") { - return URL(string: self.url1080pHEVC)! - } - else { - return URL(string: self.url1080pH264)! - } + + var url: URL { + let preferences = Preferences.sharedInstance + + // We need to return the closest available format, not pretty + if preferences.videoFormat == Preferences.VideoFormat.v4KHEVC.rawValue { + if url4KHEVC != "" { + return URL(string: self.url4KHEVC)! + } else if url1080pHEVC != "" { + return URL(string: self.url1080pHEVC)! + } else { + return URL(string: self.url1080pH264)! } - else if (preferences.videoFormat == Preferences.VideoFormat.v1080pHEVC.rawValue) - { - if (url1080pHEVC != "") { - return URL(string: self.url1080pHEVC)! - } - else if (url1080pH264 != "") { - return URL(string: self.url1080pH264)! - } - else { - return URL(string: self.url4KHEVC)! - } + } else if preferences.videoFormat == Preferences.VideoFormat.v1080pHEVC.rawValue { + if url1080pHEVC != "" { + return URL(string: self.url1080pHEVC)! + } else if url1080pH264 != "" { + return URL(string: self.url1080pH264)! + } else { + return URL(string: self.url4KHEVC)! } - else - { - if (url1080pH264 != "") { - return URL(string: self.url1080pH264)! - } - else if (url1080pHEVC != "") { - return URL(string: self.url1080pHEVC)! // With the latest versions, we should always have a H.264 fallback so this is just for future proofing - } - else { - return URL(string: self.url4KHEVC)! - } + } else { + if url1080pH264 != "" { + return URL(string: self.url1080pH264)! + } else if url1080pHEVC != "" { + // With the latest versions, we should always have a H.264 fallback so this is just for future proofing + return URL(string: self.url1080pHEVC)! + } else { + return URL(string: self.url4KHEVC)! } } } - - init(id: String, name: String, secondaryName: String, type: String, - timeOfDay: String, url1080pH264: String, url1080pHEVC: String, url4KHEVC: String, manifest: Manifests, poi: [String: String], communityPoi: [String:String]) { + + init(id: String, + name: String, + secondaryName: String, + type: String, + timeOfDay: String, + url1080pH264: String, + url1080pHEVC: String, + url4KHEVC: String, + manifest: Manifests, + poi: [String: String], + communityPoi: [String: String] + ) { self.id = id // We override names for known space videos - if (SpaceVideos.contains(id)) { + if spaceVideos.contains(id) { self.name = "Space" - if (secondaryName != "") { + if secondaryName != "" { self.secondaryName = secondaryName } else { self.secondaryName = name @@ -135,14 +131,13 @@ class AerialVideo: CustomStringConvertible, Equatable { 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 = TimeInformation[id] { + if let val = timeInformation[id] { self.timeOfDay = val - } - else { + } else { self.timeOfDay = timeOfDay } @@ -153,7 +148,7 @@ class AerialVideo: CustomStringConvertible, Equatable { self.poi = poi self.communityPoi = communityPoi self.duration = 0 - + updateDuration() } @@ -162,45 +157,46 @@ class AerialVideo: CustomStringConvertible, Equatable { // 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 - + // Not the prettiest code ! let cacheDirectoryPath = VideoCache.cacheDirectory! as NSString let fileManager = FileManager.default - + var videoCache1080pH264Path = "", videoCache1080pHEVCPath = "", videoCache4KHEVCPath = "" - if (self.url1080pH264 != "") - { + if self.url1080pH264 != "" { videoCache1080pH264Path = cacheDirectoryPath.appendingPathComponent((URL(string: url1080pH264)?.lastPathComponent)!) } - if (self.url1080pHEVC != "") - { + if self.url1080pHEVC != "" { videoCache1080pHEVCPath = cacheDirectoryPath.appendingPathComponent((URL(string: url1080pHEVC)?.lastPathComponent)!) } - if (self.url4KHEVC != "") - { + if self.url4KHEVC != "" { videoCache4KHEVCPath = cacheDirectoryPath.appendingPathComponent((URL(string: url4KHEVC)?.lastPathComponent)!) } - + if fileManager.fileExists(atPath: videoCache4KHEVCPath) { let asset = AVAsset(url: URL(fileURLWithPath: videoCache4KHEVCPath)) self.duration = CMTimeGetSeconds(asset.duration) - } - else if fileManager.fileExists(atPath: videoCache1080pHEVCPath) { + } else if fileManager.fileExists(atPath: videoCache1080pHEVCPath) { let asset = AVAsset(url: URL(fileURLWithPath: videoCache1080pHEVCPath)) self.duration = CMTimeGetSeconds(asset.duration) - } - else if fileManager.fileExists(atPath: videoCache1080pH264Path) { + } else if fileManager.fileExists(atPath: videoCache1080pH264Path) { let asset = AVAsset(url: URL(fileURLWithPath: videoCache1080pH264Path)) self.duration = CMTimeGetSeconds(asset.duration) - } - else - { + } else { debugLog("Could not determine duration, video is not cached in any format") self.duration = 0 } } var description: String { - return "id=\(id), name=\(name), type=\(type), timeofDay=\(timeOfDay), url1080pH264=\(url1080pH264), url1080pHEVC=\(url1080pHEVC), url4KHEVC=\(url4KHEVC)" + return """ + id=\(id), + name=\(name), + type=\(type), + timeofDay=\(timeOfDay), + url1080pH264=\(url1080pH264), + url1080pHEVC=\(url1080pHEVC), + url4KHEVC=\(url4KHEVC)" + """ } } diff --git a/Aerial/Source/Models/Cache/AssetLoaderDelegate.swift b/Aerial/Source/Models/Cache/AssetLoaderDelegate.swift index 6f43651e..49c520fc 100644 --- a/Aerial/Source/Models/Cache/AssetLoaderDelegate.swift +++ b/Aerial/Source/Models/Cache/AssetLoaderDelegate.swift @@ -11,7 +11,7 @@ import AVFoundation /// Returns an AVURLAsset that is automatically cached. If already cached /// then returns the cached asset. -func CachedOrCachingAsset(_ URL: Foundation.URL) -> AVURLAsset { +func cachedOrCachingAsset(_ URL: Foundation.URL) -> AVURLAsset { let assetLoader = AssetLoaderDelegate(URL: URL) let asset = AVURLAsset(url: assetLoader.URLWithCustomScheme) let queue = DispatchQueue.main @@ -22,11 +22,11 @@ func CachedOrCachingAsset(_ URL: Foundation.URL) -> AVURLAsset { } 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" @@ -37,47 +37,47 @@ class AssetLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, VideoLoaderD 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.index(of: removeLoader) { videoLoaders.remove(at: index) } } } - + func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool { @@ -89,12 +89,12 @@ class AssetLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, VideoLoaderD return true } }*/ - + // assign request to VideoLoader //debugLog("request to loader \(loadingRequest)") let videoLoader = VideoLoader(url: URL, loadingRequest: loadingRequest, delegate: self) videoLoaders.append(videoLoader) - + return true } } diff --git a/Aerial/Source/Models/Cache/PoiStringProvider.swift b/Aerial/Source/Models/Cache/PoiStringProvider.swift index c65f9d8b..83883312 100644 --- a/Aerial/Source/Models/Cache/PoiStringProvider.swift +++ b/Aerial/Source/Models/Cache/PoiStringProvider.swift @@ -9,11 +9,11 @@ import Foundation class CommunityStrings { - let id : String - let name : String - let poi : [String:String] - - init(id: String, name: String, poi: [String:String]) { + 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 @@ -24,10 +24,10 @@ class PoiStringProvider { static let sharedInstance = PoiStringProvider() var loadedDescriptions = false var loadedDescriptionsWasLocalized = false - + var stringBundle: Bundle? var stringDict: NSDictionary? - + var communityStrings = [CommunityStrings]() // MARK: - Lifecycle @@ -41,15 +41,14 @@ class PoiStringProvider { private func loadBundle() { // Idle string bundle let preferences = Preferences.sharedInstance - + var bundlePath = VideoCache.cacheDirectory! - if (preferences.localizeDescriptions) { + if preferences.localizeDescriptions { bundlePath.append(contentsOf: "/TVIdleScreenStrings.bundle") - } - else { + } else { bundlePath.append(contentsOf: "/TVIdleScreenStrings.bundle/en.lproj/") } - + if let sb = Bundle.init(path: bundlePath) { let dictPath = VideoCache.cacheDirectory!.appending("/TVIdleScreenStrings.bundle/en.lproj/Localizable.nocache.strings") @@ -69,7 +68,7 @@ class PoiStringProvider { // Make sure we have the correct bundle loaded private func ensureLoadedBundle() -> Bool { let preferences = Preferences.sharedInstance - + if loadedDescriptions && loadedDescriptionsWasLocalized == preferences.localizeDescriptions { return true } else { @@ -79,76 +78,72 @@ class PoiStringProvider { } // Return the Localized (or english) string for a key from the Strings Bundle - func getString(key:String, video:AerialVideo) -> String { + func getString(key: String, video: AerialVideo) -> String { if !ensureLoadedBundle() { return "" } let preferences = Preferences.sharedInstance let locale: NSLocale = NSLocale(localeIdentifier: Locale.preferredLanguages[0]) - + if #available(OSX 10.12, *) { - if (preferences.localizeDescriptions && locale.languageCode != "en") { + if preferences.localizeDescriptions && locale.languageCode != "en" { return stringBundle!.localizedString(forKey: key, value: "", table: "Localizable.nocache") } } - - if preferences.useCommunityDescriptions && video.communityPoi.count > 0 { + + if preferences.useCommunityDescriptions && !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]? { + func fetchExtraPoiForId(id: String) -> [String: String]? { if !ensureLoadedBundle() { return [:] } - - var found = [String:String]() + + var found = [String: String]() for kv in stringDict! { let key = (kv.key as! String) - if key.starts(with: id) - { + if key.starts(with: id) { found[String(key.split(separator: "_").last!)] = key } } - + return found } // - func getPoiKeys(video: AerialVideo) -> [String:String] { + func getPoiKeys(video: AerialVideo) -> [String: String] { let preferences = Preferences.sharedInstance let locale: NSLocale = NSLocale(localeIdentifier: Locale.preferredLanguages[0]) if #available(OSX 10.12, *) { - if (preferences.localizeDescriptions && locale.languageCode != "en") { + if preferences.localizeDescriptions && locale.languageCode != "en" { return video.poi } } - - if preferences.useCommunityDescriptions && video.communityPoi.count > 0 { + + if preferences.useCommunityDescriptions && !video.communityPoi.isEmpty { return video.communityPoi } else { return video.poi } } - - + // MARK: - Community data - + // Load the community strings - private func loadCommunity() - { + private func loadCommunity() { let preferences = Preferences.sharedInstance - + var bundlePath: String - if (preferences.localizeDescriptions) { + if preferences.localizeDescriptions { bundlePath = Bundle(for: PoiStringProvider.self).path(forResource: "en", ofType: "json")! //bundlePath = Bundle.main.path(forResource: "en", ofType: "json")! - } - else { + } else { // TODO bundlePath = Bundle(for: PoiStringProvider.self).path(forResource: "en", ofType: "json")! //bundlePath = Bundle.main.path(forResource: "en", ofType: "json")! @@ -157,19 +152,18 @@ class PoiStringProvider { do { let data = try Data(contentsOf: URL(fileURLWithPath: bundlePath), options: .mappedIfSafe) let batches = try JSONSerialization.jsonObject(with: data, options: .allowFragments) - + guard let batch = batches as? NSDictionary else { errorLog("Community : Encountered unexpected content type for batch, please report !") return } - - let assets = batch["assets"] as! Array - + + let assets = batch["assets"] as! [NSDictionary] for item in assets { let id = item["id"] as! String let name = item["name"] as! String let poi = item["pointsOfInterest"] as? [String: String] - + communityStrings.append(CommunityStrings(id: id, name: name, poi: poi ?? [:])) } } catch { @@ -180,25 +174,10 @@ class PoiStringProvider { } func getCommunityName(id: String) -> String? { - for obj in communityStrings { - if obj.id == id { - return obj.name - } - } - - return nil + return communityStrings.first(where: { $0.id == id }).map { $0.name } } - - func getCommunityPoi(id:String) -> [String:String] - { - for obj in communityStrings { - if obj.id == id { - return obj.poi - } - } - - return [:] + + func getCommunityPoi(id: String) -> [String: String] { + return communityStrings.first(where: { $0.id == id }).map { $0.poi } ?? [:] } - } - diff --git a/Aerial/Source/Models/Cache/VideoCache.swift b/Aerial/Source/Models/Cache/VideoCache.swift index a5746727..9ebd98e0 100644 --- a/Aerial/Source/Models/Cache/VideoCache.swift +++ b/Aerial/Source/Models/Cache/VideoCache.swift @@ -13,51 +13,49 @@ import ScreenSaver class VideoCache { var videoData: Data var mutableVideoData: NSMutableData? - + var loading: Bool var loadedRanges: [NSRange] = [] let URL: URL - + static var cacheDirectory: String? { - get { - var cacheDirectory: String? - - let preferences = Preferences.sharedInstance - if let customCacheDirectory = preferences.customCacheDirectory { - cacheDirectory = customCacheDirectory - } else { - let cachePaths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, - .userDomainMask, - true) - if cachePaths.count == 0 { - errorLog("Couldn't find cache paths!") - return nil - } - - let userCacheDirectory = cachePaths[0] as NSString - let defaultCacheDirectory = userCacheDirectory.appendingPathComponent("Aerial") - - cacheDirectory = defaultCacheDirectory - } + var cacheDirectory: String? - guard let appCacheDirectory = cacheDirectory else { + let preferences = Preferences.sharedInstance + if let customCacheDirectory = preferences.customCacheDirectory { + cacheDirectory = customCacheDirectory + } else { + let cachePaths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, + .userDomainMask, + true) + if cachePaths.isEmpty { + errorLog("Couldn't find cache paths!") return nil } - - let fileManager = FileManager.default - if fileManager.fileExists(atPath: appCacheDirectory as String) == false { - do { - try fileManager.createDirectory(atPath: appCacheDirectory as String, - withIntermediateDirectories: false, attributes: nil) - } catch let error { - errorLog("Couldn't create cache directory: \(error)") - return nil - } + + let userCacheDirectory = cachePaths[0] as NSString + let defaultCacheDirectory = userCacheDirectory.appendingPathComponent("Aerial") + + cacheDirectory = defaultCacheDirectory + } + + guard let appCacheDirectory = cacheDirectory else { + return nil + } + + let fileManager = FileManager.default + if fileManager.fileExists(atPath: appCacheDirectory as String) == false { + do { + try fileManager.createDirectory(atPath: appCacheDirectory as String, + withIntermediateDirectories: false, attributes: nil) + } catch let error { + errorLog("Couldn't create cache directory: \(error)") + return nil } - return appCacheDirectory } + return appCacheDirectory } - + static func isAvailableOffline(video: AerialVideo) -> Bool { guard let videoCachePath = cachePath(forVideo: video) else { errorLog("Couldn't get video cache path!") @@ -65,26 +63,26 @@ class VideoCache { } let fileManager = FileManager.default - + return fileManager.fileExists(atPath: videoCachePath) } - + static func cachePath(forVideo video: AerialVideo) -> String? { let vurl = video.url let filename = vurl.lastPathComponent return cachePath(forFilename: filename) } - + static func cachePath(forFilename filename: String) -> String? { guard let cacheDirectory = VideoCache.cacheDirectory else { return nil } - + let cacheDirectoryPath = cacheDirectory as NSString let videoCachePath = cacheDirectoryPath.appendingPathComponent(filename) return videoCachePath } - + init(URL: Foundation.URL) { debugLog("initvideocache") videoData = Data() @@ -92,33 +90,33 @@ class VideoCache { 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] @@ -129,38 +127,36 @@ class VideoCache { } } } - + // MARK: - Save / Load Cache - + var videoCachePath: String? { - get { - let filename = URL.lastPathComponent - return VideoCache.cachePath(forFilename: filename) - } + let filename = URL.lastPathComponent + return VideoCache.cachePath(forFilename: filename) } - + func saveCachedVideo() { let preferences = Preferences.sharedInstance - + guard preferences.cacheAerials else { debugLog("Cache disabled, not saving video") return } - + 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) { + if mutableVideoData == nil { errorLog("Missing video data for save.") return } @@ -168,128 +164,128 @@ class VideoCache { 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!) @@ -301,7 +297,7 @@ class VideoCache { } } } - + lastIndex = consolidatedRanges.count previousRange = range consolidatedRanges.append(range) diff --git a/Aerial/Source/Models/Cache/VideoDownload.swift b/Aerial/Source/Models/Cache/VideoDownload.swift index af56cd7f..04c64a62 100644 --- a/Aerial/Source/Models/Cache/VideoDownload.swift +++ b/Aerial/Source/Models/Cache/VideoDownload.swift @@ -21,7 +21,7 @@ class VideoDownloadStream { var response: URLResponse? var contentInformationRequest: Bool = false var downloadOffset = 0 - + init(connection: NSURLConnection) { self.connection = connection } @@ -35,32 +35,32 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { 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() @@ -68,68 +68,62 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { 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? { - for stream in streams { - if stream.connection == connection { - return stream - } - } - - return nil + 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 = 4 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 i in 0 ..< streamCount { - let isLastStream: Bool = i == (streamCount - 1) + 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) @@ -140,150 +134,149 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { 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() { guard let videoCachePath = VideoCache.cachePath(forVideo: video) 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.index(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.count == 0 { + + 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 @@ -296,25 +289,25 @@ class VideoDownload: NSObject, NSURLConnectionDataDelegate { 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 { + 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) } } diff --git a/Aerial/Source/Models/Cache/VideoLoader.swift b/Aerial/Source/Models/Cache/VideoLoader.swift index 8d9a61f5..c6662aa8 100644 --- a/Aerial/Source/Models/Cache/VideoLoader.swift +++ b/Aerial/Source/Models/Cache/VideoLoader.swift @@ -19,26 +19,26 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { 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 @@ -46,7 +46,7 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { 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") @@ -55,29 +55,29 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { //debugLog("loadedRange \(loadedRange)") //debugLog("requestedRange \(requestedRange)") 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 @@ -85,25 +85,25 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { } 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 } //debugLog("drl \(dataRequest.requestedLength) dro \(dataRequest.requestedOffset)") //debugLog("\(dataRequest)") - + /*if (data.count > 100000) { debugLog("NOTGOOD") dataLog(data) @@ -111,13 +111,13 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { let requestedRange = self.requestedRange let loadedRange = self.loadedRange let loadedLocation = loadedRange.location + loadedRange.length - + let dataRange = NSRange(location: loadedRange.location + loadedRange.length, length: data.count) //debugLog("\(dataRange)") - + self.delegate?.videoLoader(self, receivedData: data, forRange: dataRange) - + // check if we've already been sending content, or we're at right byte offset if loadedLocation >= requestedRange.location { //debugLog("case1") @@ -125,7 +125,7 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { let pendingDataEndOffset = loadedLocation + data.count //debugLog("r \(requestedEndOffset) p \(pendingDataEndOffset)") - + if pendingDataEndOffset > requestedEndOffset { let truncateDataLength = pendingDataEndOffset - requestedEndOffset let truncatedData = data.subdata(in: 0.. 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() @@ -159,13 +159,13 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { } else if inset < 1 { errorLog("Inset is invalid value: \(inset)") } - + } - + //debugLog("Received data with length: \(data.count)") - + self.loadedRange.length += data.count - + } } @@ -176,43 +176,43 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { 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 { @@ -223,27 +223,27 @@ class VideoLoader: NSObject, NSURLConnectionDataDelegate { 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 { + 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) } diff --git a/Aerial/Source/Models/Cache/VideoManager.swift b/Aerial/Source/Models/Cache/VideoManager.swift index 7e28b58c..de48bc52 100644 --- a/Aerial/Source/Models/Cache/VideoManager.swift +++ b/Aerial/Source/Models/Cache/VideoManager.swift @@ -7,9 +7,9 @@ // import Foundation -typealias VideoManagerCallback = (Int,Int) -> (Void) +typealias VideoManagerCallback = (Int, Int) -> Void -class VideoManager : NSObject { +class VideoManager: NSObject { static let sharedInstance = VideoManager() var managerCallbacks = [VideoManagerCallback]() @@ -18,7 +18,7 @@ class VideoManager : NSObject { /// List of queued videos, by video.id private var queuedVideos = [String]() - + /// Dictionary of operations, keyed by the video.id fileprivate var operations = [String: VideoDownloadOperation]() @@ -28,24 +28,25 @@ class VideoManager : NSObject { //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) } - + // Is the video queued for download ? func isVideoQueued(id: String) -> Bool { if queuedVideos.firstIndex(of: id) != nil { @@ -54,7 +55,7 @@ class VideoManager : NSObject { return false } } - + @discardableResult func queueDownload(_ video: AerialVideo) -> VideoDownloadOperation { print(queue.isSuspended) @@ -64,14 +65,14 @@ class VideoManager : NSObject { print(queue.operations) - let operation = VideoDownloadOperation(video:video, delegate: self) + let operation = VideoDownloadOperation(video: video, delegate: self) operations[video.id] = operation queue.addOperation(operation) queuedVideos.append(video.id) // Our Internal List of queued videos markAsQueued(id: video.id) // Callback the CheckCellView - totalQueued = totalQueued+1 // Increment our count - + totalQueued += 1 // Increment our count + DispatchQueue.main.async { // Callback the callbacks for callback in self.managerCallbacks { @@ -80,19 +81,18 @@ class VideoManager : NSObject { } return operation } - + // Callbacks for Items - func finishedDownload(id: String, success: Bool) - { + 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 { @@ -108,8 +108,8 @@ class VideoManager : NSObject { } } } - - func markAsQueued(id:String) { + + func markAsQueued(id: String) { // Manage our queuedVideo index if let cell = checkCells[id] { cell.markAsQueued() @@ -120,26 +120,24 @@ class VideoManager : NSObject { cell.updateProgressIndicator(progress: progress) } } - + /// Cancel all queued operations - + func cancelAll() { stopAll = true queue.cancelAllOperations() } } - - -class VideoDownloadOperation : AsynchronousOperation { +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 { @@ -153,12 +151,12 @@ class VideoDownloadOperation : AsynchronousOperation { self.download!.startDownload() } } - + override func cancel() { defer { finish() } let videoManager = VideoManager.sharedInstance - if ((self.download) != nil) { + if let _ = self.download { self.download!.cancel() } else { videoManager.finishedDownload(id: self.video.id, success: false) @@ -169,7 +167,7 @@ class VideoDownloadOperation : AsynchronousOperation { } } -extension VideoDownloadOperation : VideoDownloadDelegate { +extension VideoDownloadOperation: VideoDownloadDelegate { func videoDownload(_ videoDownload: VideoDownload, finished success: Bool, errorMessage: String?) { debugLog("Finished") @@ -180,14 +178,14 @@ extension VideoDownloadOperation : VideoDownloadDelegate { // Call up to clean the view videoManager.finishedDownload(id: videoDownload.video.id, success: true) } else { - if (errorMessage != nil) { + 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 diff --git a/Aerial/Source/Models/Downloads/AsynchronousOperation.swift b/Aerial/Source/Models/Downloads/AsynchronousOperation.swift index 766e72cc..e214425f 100644 --- a/Aerial/Source/Models/Downloads/AsynchronousOperation.swift +++ b/Aerial/Source/Models/Downloads/AsynchronousOperation.swift @@ -23,67 +23,68 @@ import Foundation /// 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 } - + + 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. - + + /// 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 } } diff --git a/Aerial/Source/Models/Downloads/DownloadManager.swift b/Aerial/Source/Models/Downloads/DownloadManager.swift index 85741cb7..dd7be401 100644 --- a/Aerial/Source/Models/Downloads/DownloadManager.swift +++ b/Aerial/Source/Models/Downloads/DownloadManager.swift @@ -10,34 +10,33 @@ import Cocoa /// Manager of asynchronous download `Operation` objects 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 _queue = OperationQueue() - _queue.name = "download" - _queue.maxConcurrentOperationCount = 3 - - return _queue + 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 /// /// - returns: The DownloadOperation of the operation that was queued - + @discardableResult func queueDownload(_ url: URL) -> DownloadOperation { let operation = DownloadOperation(session: session, url: url) @@ -45,55 +44,71 @@ class DownloadManager: NSObject { 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, + 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) + + 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?) { + + 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 -class DownloadOperation : AsynchronousOperation { +class DownloadOperation: AsynchronousOperation { let task: URLSessionTask - + init(session: URLSession, url: URL) { task = session.downloadTask(with: url) super.init() } - + override func cancel() { task.cancel() super.cancel() } - + override func main() { task.resume() } @@ -102,34 +117,38 @@ class DownloadOperation : AsynchronousOperation { // MARK: NSURLSessionDownloadDelegate methods // Customized for our usage extension DownloadOperation: URLSessionDownloadDelegate { - + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { do { let manager = FileManager.default var destinationURL = URL(fileURLWithPath: VideoCache.cacheDirectory!) - + // tvOS11 and tvOS10 JSONs are named entries.json, so we rename them here if downloadTask.originalRequest!.url!.absoluteString.contains("2x/entries.json") { debugLog("Caching tvos11.json") destinationURL.appendPathComponent("tvos11.json") - } - else if downloadTask.originalRequest!.url!.absoluteString.contains("Autumn") { + } else if downloadTask.originalRequest!.url!.absoluteString.contains("Autumn") { debugLog("Caching tvos10.json") destinationURL.appendPathComponent("tvos10.json") - } - else { + } else { debugLog("Caching \(downloadTask.originalRequest!.url!.lastPathComponent)") destinationURL.appendPathComponent(downloadTask.originalRequest!.url!.lastPathComponent) } - + try? manager.removeItem(at: destinationURL) try manager.moveItem(at: location, to: destinationURL) } catch { errorLog("\(error)") } } - - func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + + 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)") } @@ -138,10 +157,10 @@ extension DownloadOperation: URLSessionDownloadDelegate { // MARK: URLSessionTaskDelegate methods extension DownloadOperation: URLSessionTaskDelegate { - - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { defer { finish() } - + if let error = error { errorLog("\(error)") return @@ -152,18 +171,18 @@ extension DownloadOperation: URLSessionTaskDelegate { debugLog("untaring resources.tar") // Extract json - let process:Process = Process() + let process: Process = Process() let cacheDirectory = VideoCache.cacheDirectory! - + var cacheResourcesString = cacheDirectory cacheResourcesString.append(contentsOf: "/resources.tar") - + process.currentDirectoryPath = cacheDirectory process.launchPath = "/usr/bin/tar" - process.arguments = ["-xvf",cacheResourcesString] - + process.arguments = ["-xvf", cacheResourcesString] + process.launch() - + process.waitUntilExit() } diff --git a/Aerial/Source/Models/ErrorLog.swift b/Aerial/Source/Models/ErrorLog.swift index 8fb6236e..deda9ed6 100644 --- a/Aerial/Source/Models/ErrorLog.swift +++ b/Aerial/Source/Models/ErrorLog.swift @@ -9,17 +9,17 @@ import Cocoa import os.log -enum ErrorLevel : Int { +enum ErrorLevel: Int { case info, debug, warning, error } class LogMessage { - let date : Date - let level : ErrorLevel - let message : String - var actionName : String? - var actionBlock : BlockOperation? - + 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 @@ -27,17 +27,17 @@ class LogMessage { } } -typealias LoggerCallback = (ErrorLevel) -> (Void) +typealias LoggerCallback = (ErrorLevel) -> Void class Logger { static let sharedInstance = Logger() var callbacks = [LoggerCallback]() - + func addCallback(_ callback:@escaping LoggerCallback) { callbacks.append(callback) } - + func callBack(level: ErrorLevel) { DispatchQueue.main.async { for callback in self.callbacks { @@ -48,12 +48,12 @@ class Logger { } var errorMessages = [LogMessage]() +// swiftlint:disable:next identifier_name func Log(level: ErrorLevel, message: String) { errorMessages.append(LogMessage(level: level, message: message)) - // We throw errors to console, they always matter - if (level == .error) { + if level == .error { if #available(OSX 10.12, *) { // This is faster when available let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Screensaver") @@ -63,17 +63,17 @@ func Log(level: ErrorLevel, message: String) { NSLog("AerialError: \(message)") } } - + let preferences = Preferences.sharedInstance // We may callback - if (level == .warning || level == .error || (level == .debug && preferences.debugMode)) { + if level == .warning || level == .error || (level == .debug && preferences.debugMode) { let logger = Logger.sharedInstance logger.callBack(level: level) } - + // We may log to disk - if (preferences.logToDisk) { + if preferences.logToDisk { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .none dateFormatter.timeStyle = .medium @@ -82,10 +82,10 @@ func Log(level: ErrorLevel, message: String) { if let cacheDirectory = VideoCache.cacheDirectory { var cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) cacheFileUrl.appendPathComponent("AerialLog.txt") - + let data = string.data(using: String.Encoding.utf8, allowLossyConversion: false)! //let data = message.data(using: String.Encoding.utf8, allowLossyConversion: false)! - + if FileManager.default.fileExists(atPath: cacheFileUrl.path) { do { let fileHandle = try FileHandle(forWritingTo: cacheFileUrl) @@ -112,24 +112,24 @@ func debugLog(_ message: String) { #endif let preferences = Preferences.sharedInstance - if (preferences.debugMode) { - Log(level:.debug, message:message) + if preferences.debugMode { + Log(level: .debug, message: message) } } func infoLog(_ message: String) { - Log(level:.info, message:message) + Log(level: .info, message: message) } func warnLog(_ message: String) { - Log(level:.warning, message:message) + Log(level: .warning, message: message) } func errorLog(_ message: String) { - Log(level:.error, message:message) + Log(level: .error, message: message) } -func dataLog(_ data:Data) { +func dataLog(_ data: Data) { let cacheDirectory = VideoCache.cacheDirectory! var cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) cacheFileUrl.appendPathComponent("AerialData.txt") diff --git a/Aerial/Source/Models/Extensions/AVPlayerViewExtension.swift b/Aerial/Source/Models/Extensions/AVPlayerViewExtension.swift index 2857d89a..e4eca494 100644 --- a/Aerial/Source/Models/Extensions/AVPlayerViewExtension.swift +++ b/Aerial/Source/Models/Extensions/AVPlayerViewExtension.swift @@ -11,15 +11,15 @@ 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 diff --git a/Aerial/Source/Models/Extensions/CollectionType+Shuffling.swift b/Aerial/Source/Models/Extensions/CollectionType+Shuffling.swift index 9dd82e8b..ae30d949 100644 --- a/Aerial/Source/Models/Extensions/CollectionType+Shuffling.swift +++ b/Aerial/Source/Models/Extensions/CollectionType+Shuffling.swift @@ -1,4 +1,3 @@ - // // CollectionType+Shuffling.swift // Aerial @@ -9,14 +8,14 @@ import Foundation extension MutableCollection where Indices.Iterator.Element == Index { /// Shuffles the contents of this collection. mutating func shuffle() { - let c = count - guard c > 1 else { return } - - for (unshuffledCount, firstUnshuffled) in zip(stride(from: c, to: 1, by: -1), indices) { - let d: Int = numericCast(Int.random(in: 0.. 1 else { return } + + for (unshuffledCount, firstUnshuffled) in zip(stride(from: theCount, to: 1, by: -1), indices) { + let digit: Int = numericCast(Int.random(in: 0.. (Void) +typealias ManifestLoadCallback = ([AerialVideo]) -> Void +// swiftlint:disable:next type_body_length class ManifestLoader { static let instance: ManifestLoader = ManifestLoader() lazy var preferences = Preferences.sharedInstance - var callbacks = [manifestLoadCallback]() + var callbacks = [ManifestLoadCallback]() var loadedManifest = [AerialVideo]() var processedVideos = [AerialVideo]() var lastPluckedFromPlaylist: AerialVideo? - + var manifestTvOS10: Data? var manifestTvOS11: Data? var manifestTvOS12: Data? @@ -28,7 +29,7 @@ class ManifestLoader { 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) @@ -36,70 +37,68 @@ class ManifestLoader { "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 - + "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 - let dupePairs = ["88025454-6D58-48E8-A2DB-924988FAD7AC":"6E2FC8AC-832D-46CF-B306-BB2A05030C17"] // Liwa - + let dupePairs = ["88025454-6D58-48E8-A2DB-924988FAD7AC": "6E2FC8AC-832D-46CF-B306-BB2A05030C17"] // Liwa + // 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-4K-SDR":"https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_4K_SDR_HEVC.mov"]] // Dubai night 2 - - + ["url-1080-SDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_2K_SDR_HEVC.mov", + "url-4K-SDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_4K_SDR_HEVC.mov", ], ] // Dubai night 2 + // 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":"A013_C005_", // San Francisco day 2 - "b9-3":"A006_C003_", // San Francisco day 3 + "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": "A013_C005_", // 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 + "b3-3": "A012_C014_", // San Francisco day 5 // maybe A013_C004 ? - "b4-3":"A013_C012_", // 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 + "b4-3": "A013_C012_", // 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) { + func generatePlaylist(isRestricted: Bool, restrictedTo: String) { // Start fresh playlist = [AerialVideo]() playlistIsRestricted = isRestricted playlistRestrictedTo = restrictedTo - + // Start with a shuffled list let shuffled = loadedManifest.shuffled() for video in shuffled { // We exclude videos not in rotation let inRotation = preferences.videoIsInRotation(videoID: video.id) - + if !inRotation { //debugLog("randomVideo: video is disabled: \(video)") continue @@ -112,7 +111,7 @@ class ManifestLoader { continue } } - + // We may not want to stream if preferences.neverStreamVideos == true { if video.isAvailableOffline == false { @@ -124,29 +123,29 @@ class ManifestLoader { // All good ? Add to playlist playlist.append(video) } - + // On regenerating a new playlist, we try to avoid repeating - while (playlist.count > 1 && lastPluckedFromPlaylist == playlist.first) { + while playlist.count > 1 && lastPluckedFromPlaylist == playlist.first { playlist.shuffle() } } - + func randomVideo(excluding: [AerialVideo]) -> AerialVideo? { let timeManagement = TimeManagement.sharedInstance - let (shouldRestrictByDayNight,restrictTo) = timeManagement.shouldRestrictPlaybackToDayNightVideo() + let (shouldRestrictByDayNight, restrictTo) = timeManagement.shouldRestrictPlaybackToDayNightVideo() debugLog("shouldRestrictByDayNight : \(shouldRestrictByDayNight) (\(restrictTo))") - if (playlist.count == 0 || (restrictTo != playlistRestrictedTo) || (shouldRestrictByDayNight != playlistIsRestricted)) { + if playlist.isEmpty || restrictTo != playlistRestrictedTo || shouldRestrictByDayNight != playlistIsRestricted { generatePlaylist(isRestricted: shouldRestrictByDayNight, restrictedTo: restrictTo) } - - if playlist.count > 0 { + + if !playlist.isEmpty { lastPluckedFromPlaylist = playlist.removeFirst() return lastPluckedFromPlaylist } else { return findBestEffortVideo() } } - + // Find a backup plan when conditions are not met func findBestEffortVideo() -> AerialVideo? { // So this is embarassing. This can happen if : @@ -158,7 +157,7 @@ class ManifestLoader { // - 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 { @@ -167,18 +166,17 @@ class ManifestLoader { } else { // Start with a shuffled list let shuffled = loadedManifest.shuffled() - - if (shuffled.count == 0) - { + + 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 !") @@ -190,9 +188,9 @@ class ManifestLoader { return shuffled.first! } } - + // MARK: - Lifecycle - + init() { debugLog("Manifest init") // We try to load our video manifests in 3 steps : @@ -203,36 +201,33 @@ class ManifestLoader { debugLog("isManifestCached 10 \(isManifestCached(manifest: .tvOS10))") debugLog("isManifestCached 11 \(isManifestCached(manifest: .tvOS11))") debugLog("isManifestCached 12 \(isManifestCached(manifest: .tvOS12))") - + if areManifestsFilesLoaded() { debugLog("Files were already loaded") loadManifestsFromLoadedFiles() - } - else - { + } else { debugLog("Files were not already loaded") // Manifests are not in our preferences plist, are they cached on disk ? if areManifestsCached() { debugLog("Manifests are cached on disk, loading") loadCachedManifests() - } - else { + } else { // Ok then, we fetch them... debugLog("Fetching missing manifests online") let downloadManager = DownloadManager() - + var urls: [URL] = [] - + // For tvOS12, json is now in a tar file - if (!isManifestCached(manifest: .tvOS12)) { + if !isManifestCached(manifest: .tvOS12) { urls.append(URL(string: "https://sylvan.apple.com/Aerials/resources.tar")!) } - if (!isManifestCached(manifest: .tvOS11)) { + if !isManifestCached(manifest: .tvOS11) { urls.append(URL(string: "https://sylvan.apple.com/Aerials/2x/entries.json")!) } - - if (!isManifestCached(manifest: .tvOS10)) { + + if !isManifestCached(manifest: .tvOS10) { urls.append(URL(string: "http://a1.phobos.apple.com/us/r1000/000/Features/atv/AutumnResources/videos/entries.json")!) } @@ -240,21 +235,21 @@ class ManifestLoader { 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) completion.addDependency(operation) } - + OperationQueue.main.addOperation(completion) } } } - func addCallback(_ callback:@escaping manifestLoadCallback) { - if loadedManifest.count > 0 { + func addCallback(_ callback:@escaping ManifestLoadCallback) { + if !loadedManifest.isEmpty { callback(loadedManifest) } else { callbacks.append(callback) @@ -262,14 +257,13 @@ class ManifestLoader { } // MARK: - Manifests - + // Check if the Manifests have been loaded in this class already func areManifestsFilesLoaded() -> Bool { - if (manifestTvOS12 != nil && manifestTvOS11 != nil && manifestTvOS10 != nil) { + if manifestTvOS12 != nil && manifestTvOS11 != nil && manifestTvOS10 != nil { debugLog("Manifests files were loaded in class") return true - } - else { + } else { debugLog("Manifests files were not loaded in class") return false } @@ -279,27 +273,25 @@ class ManifestLoader { func areManifestsCached() -> Bool { return isManifestCached(manifest: .tvOS10) && isManifestCached(manifest: .tvOS11) && isManifestCached(manifest: .tvOS12) } - + // Check if a Manifest is saved in our cache directory func isManifestCached(manifest: Manifests) -> Bool { if let cacheDirectory = VideoCache.cacheDirectory { let fileManager = FileManager.default - + var cacheResourcesString = cacheDirectory cacheResourcesString.append(contentsOf: "/" + manifest.rawValue) - + if !fileManager.fileExists(atPath: cacheResourcesString) { return false } - } - else - { + } else { return false } - + return true } - + // Load the JSON Data cached on disk func loadCachedManifests() { if let cacheDirectory = VideoCache.cacheDirectory { @@ -309,19 +301,17 @@ class ManifestLoader { do { let ndata = try Data(contentsOf: cacheFileUrl) manifestTvOS12 = ndata - } - catch { + } catch { errorLog("Can't load entries.json from cached directory (tvOS12)") } - + // tvOS11 cacheFileUrl = URL(fileURLWithPath: cacheDirectory as String) cacheFileUrl.appendPathComponent("tvos11.json") do { let ndata = try Data(contentsOf: cacheFileUrl) manifestTvOS11 = ndata - } - catch { + } catch { errorLog("Can't load tvos11.json from cached directory") } @@ -331,8 +321,7 @@ class ManifestLoader { do { let ndata = try Data(contentsOf: cacheFileUrl) manifestTvOS10 = ndata - } - catch { + } catch { errorLog("Can't load tvos10.json from cached directory") } @@ -344,35 +333,36 @@ class ManifestLoader { } } } - + // Load Manifests from the saved preferences func loadManifestsFromLoadedFiles() { // Reset our array processedVideos = [] - if (manifestTvOS12 != nil) { + if manifestTvOS12 != nil { // We start with the more recent one, it has more information (poi, etc) readJSONFromData(manifestTvOS12!, manifest: .tvOS12) } else { warnLog("tvOS12 manifest is absent") } - - if (manifestTvOS11 != nil) { + + 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) { + + if manifestTvOS10 != nil { // The original manifest is in another format readOldJSONFromData(manifestTvOS10!, manifest: .tvOS10) } else { warnLog("tvOS10 manifest is absent") } - processedVideos = processedVideos.sorted { $0.secondaryName < $1.secondaryName } // We sort videos by secondary names, so they can display sorted in our view later - + // 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 /* // POI Extracter code @@ -384,29 +374,29 @@ class ManifestLoader { infoLog(poi.key + ": " + poiStringProvider.getString(key: poi.value)) } }*/ - + // 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! Array - + + let assets = batch["assets"] as! [NSDictionary] + for item in assets { let id = item["id"] as! String let url1080pH264 = item["url-1080-H264"] as? String @@ -421,10 +411,10 @@ class ManifestLoader { /* if let mergeName = mergeName[id] { secondaryName = mergeName }*/ - + let timeOfDay = "day" // TODO, this is hardcoded as it's no longer available in the modern JSONs let type = "video" - var poi : [String:String]? + var poi: [String: String]? if let mergeId = mergePOI[id] { poi = poiStringProvider.fetchExtraPoiForId(id: mergeId) } else { @@ -432,10 +422,9 @@ class ManifestLoader { } let communityPoi = poiStringProvider.getCommunityPoi(id: id) - - - let (isDupe,foundDupe) = findDuplicate(id: id, url1080pH264: url1080pH264 ?? "") - if (isDupe) { + + let (isDupe, foundDupe) = findDuplicate(id: id, url1080pH264: url1080pH264 ?? "") + if isDupe { foundDupe!.sources.append(manifest) } else { let video = AerialVideo(id: id, // Must have @@ -449,7 +438,7 @@ class ManifestLoader { manifest: manifest, poi: poi ?? [:], communityPoi: communityPoi) - + processedVideos.append(video) } } @@ -458,29 +447,29 @@ class ManifestLoader { return } } - + func readOldJSONFromData(_ data: Data, manifest: Manifests) { do { let poiStringProvider = PoiStringProvider.sharedInstance let options = JSONSerialization.ReadingOptions.allowFragments let batches = try JSONSerialization.jsonObject(with: data, - options: options) as! Array - + options: options) as! [NSDictionary] + for batch: NSDictionary in batches { - let assets = batch["assets"] as! Array - + 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) { @@ -488,7 +477,7 @@ class ManifestLoader { } // We may have POIs to merge - var poi : [String:String]? + var poi: [String: String]? if let mergeId = mergePOI[id] { let poiStringProvider = PoiStringProvider.sharedInstance poi = poiStringProvider.fetchExtraPoiForId(id: mergeId) @@ -497,12 +486,12 @@ class ManifestLoader { let communityPoi = poiStringProvider.getCommunityPoi(id: id) // We may have dupes... - let (isDupe,foundDupe) = findDuplicate(id: id, url1080pH264: url) + let (isDupe, foundDupe) = findDuplicate(id: id, url1080pH264: url) if isDupe { - if (foundDupe != nil) { + if foundDupe != nil { foundDupe!.sources.append(manifest) - - if (foundDupe?.url1080pH264 == "") { + + if foundDupe?.url1080pH264 == "" { foundDupe?.url1080pH264 = url } } @@ -527,7 +516,7 @@ class ManifestLoader { manifest: manifest, poi: poi ?? [:], communityPoi: communityPoi) - + processedVideos.append(video) } } @@ -537,7 +526,7 @@ class ManifestLoader { return } } - + // Look for a previously processed similar video // // tvOS11 and 12 JSON are using the same ID (and tvOS12 JSON always has better data, @@ -545,37 +534,31 @@ class ManifestLoader { // // 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?) - { + func findDuplicate(id: String, url1080pH264: String) -> (Bool, AerialVideo?) { // We blacklist some duplicates - if (url1080pH264 != "") { - if (blacklist.contains((URL(string:url1080pH264)?.lastPathComponent)!)) - { - return (true,nil) + 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 { - if (id == pid) { - for vid in processedVideos { - if vid.id == replace { - return (true,vid) - } - } + for (pid, replace) in dupePairs where id == pid { + for vid in processedVideos where vid.id == replace { + return (true, vid) } } - + for video in processedVideos { if id == video.id { - return (true,video) - } else if (url1080pH264 != "" && video.url1080pH264 != "") { - if (URL(string:url1080pH264)?.lastPathComponent == URL(string:video.url1080pH264)?.lastPathComponent) { - return (true,video) + return (true, video) + } else if url1080pH264 != "" && video.url1080pH264 != "" { + if URL(string: url1080pH264)?.lastPathComponent == URL(string: video.url1080pH264)?.lastPathComponent { + return (true, video) } } } - - return (false,nil) + + return (false, nil) } } diff --git a/Aerial/Source/Models/Time/Solar.swift b/Aerial/Source/Models/Time/Solar.swift index 07e3cbc4..cdf4c0ad 100644 --- a/Aerial/Source/Models/Time/Solar.swift +++ b/Aerial/Source/Models/Time/Solar.swift @@ -31,13 +31,13 @@ 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? @@ -48,24 +48,24 @@ public struct Solar { 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() { @@ -80,14 +80,14 @@ public struct Solar { 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 @@ -96,85 +96,87 @@ public struct Solar { 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 = RA + (Lquadrant - RAquadrant) - + RA += (Lquadrant - RAquadrant) + // Convert RA into hours - RA = RA / 15 - + 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 + // swiftlint:disable:next line_length 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) @@ -183,35 +185,36 @@ public struct Solar { } 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 { @@ -221,35 +224,35 @@ extension Solar { 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 { + public func isDaytime(zenith: Zenith) -> Bool { guard let _ = sunrise, let _ = sunset else { return false } - - var lsunrise, lsunset : Date + + var lsunrise, lsunset: Date switch zenith { case .strict: lsunrise = strictSunrise! @@ -267,14 +270,14 @@ extension Solar { 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 } } @@ -285,7 +288,7 @@ private extension Double { var degreesToRadians: Double { return Double(self) * (Double.pi / 180.0) } - + var radiansToDegrees: Double { return (Double(self) * 180.0) / Double.pi } diff --git a/Aerial/Source/Models/Time/TimeManagement.swift b/Aerial/Source/Models/Time/TimeManagement.swift index 04e0cdae..a0d77664 100644 --- a/Aerial/Source/Models/Time/TimeManagement.swift +++ b/Aerial/Source/Models/Time/TimeManagement.swift @@ -10,7 +10,8 @@ import Foundation import Cocoa import CoreLocation -class TimeManagement : NSObject { +// swiftlint:disable:next type_body_length +class TimeManagement: NSObject { static let sharedInstance = TimeManagement() // Night shift @@ -18,7 +19,7 @@ class TimeManagement : NSObject { var nightShiftAvailable = false var nightShiftSunrise = Date() var nightShiftSunset = Date() - var solar:Solar? + var solar: Solar? // MARK: - Lifecycle override init() { @@ -28,21 +29,20 @@ class TimeManagement : NSObject { } // MARK: - What should we play ? - func shouldRestrictPlaybackToDayNightVideo() -> (Bool,String) - { + // swiftlint:disable:next cyclomatic_complexity + func shouldRestrictPlaybackToDayNightVideo() -> (Bool, String) { let preferences = Preferences.sharedInstance if preferences.timeMode == Preferences.TimeMode.lightDarkMode.rawValue { - if (isDarkModeEnabled()) { + if isDarkModeEnabled() { return (true, "night") } else { return (true, "day") } - } - else if preferences.timeMode == Preferences.TimeMode.coordinates.rawValue { + } else if preferences.timeMode == Preferences.TimeMode.coordinates.rawValue { _ = calculateFromCoordinates() - - if (solar != nil) { - var zenith : Solar.Zenith + + if let _ = solar { + var zenith: Solar.Zenith switch preferences.solarMode { case Preferences.SolarMode.strict.rawValue: zenith = .strict @@ -63,42 +63,39 @@ class TimeManagement : NSObject { } } else { errorLog("You need to input latitude and longitude for calculations to work") - return (false,"") + return (false, "") } - } - else if preferences.timeMode == Preferences.TimeMode.nightShift.rawValue { + } else if preferences.timeMode == Preferences.TimeMode.nightShift.rawValue { let (isNSCapable, sunrise, sunset, _) = getNightShiftInformation() - if (!isNSCapable) { + if !isNSCapable { errorLog("Trying to use Night Shift on a non capable Mac") - return (false,"") + return (false, "") } - - return (true,dayNightCheck(sunrise: sunrise!, sunset: sunset!)) - } - else if preferences.timeMode == Preferences.TimeMode.manual.rawValue { + + return (true, dayNightCheck(sunrise: sunrise!, sunset: sunset!)) + } else if preferences.timeMode == Preferences.TimeMode.manual.rawValue { // 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: preferences.manualSunrise!) else { errorLog("Invalid sunrise time in preferences") - return(false,"") + return(false, "") } guard let dateSunset = dateFormatter.date(from: preferences.manualSunset!) else { errorLog("Invalid sunset time in preferences") - return(false,"") + return(false, "") } - - return (true,dayNightCheck(sunrise: dateSunrise, sunset: dateSunset)) + + return (true, dayNightCheck(sunrise: dateSunrise, sunset: dateSunset)) } - + // default is show anything return (false, "") } - + // Check if we are at day or night based on provided sunrise and sunset dates - private func dayNightCheck(sunrise:Date,sunset:Date) -> String - { + private func dayNightCheck(sunrise: Date, sunset: Date) -> String { var nsunrise = sunrise var nsunset = sunset let now = Date() @@ -114,27 +111,27 @@ class TimeManagement : NSObject { nsunrise = todayizeDate(date: sunrise)! nsunset = todayizeDate(date: sunset)! } - + // Then comparison is trivial ! - if (nsunrise < now && now < nsunset) { + if nsunrise < now && now < nsunset { return "day" } else { return "night" } } - + // Change a date's day to today - private func todayizeDate(date:Date) -> Date? { + 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) - + let today = dateFormatter.string(from: current) + // Extract hour from date dateFormatter.dateFormat = "HH:mm:ss +zzzz" - let format = today + " " + dateFormatter.string(from:date) - + 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) { @@ -143,14 +140,14 @@ class TimeManagement : NSObject { return nil } } - + // MARK: Calculate using Solar func calculateFromCoordinates() -> (Bool, String) { let preferences = Preferences.sharedInstance - if (preferences.latitude != "" && preferences.longitude != "") - { - solar = Solar.init(coordinate: CLLocationCoordinate2D(latitude: Double(preferences.latitude!) ?? 0, longitude: Double(preferences.longitude!) ?? 0)) + if preferences.latitude != "" && preferences.longitude != "" { + solar = Solar.init(coordinate: CLLocationCoordinate2D(latitude: Double(preferences.latitude!) ?? 0, + longitude: Double(preferences.longitude!) ?? 0)) if solar != nil { let dateFormatter = DateFormatter() dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "j:mm:ss", options: 0, locale: Locale.current) @@ -186,73 +183,71 @@ class TimeManagement : NSObject { return (false, "Can't process your coordinates, please verify") } - + // MARK: Dark Mode - func isLightDarkModeAvailable() -> (Bool,reason: String) { + func isLightDarkModeAvailable() -> (Bool, reason: String) { if #available(OSX 10.14, *) { - if (isDarkModeEnabled()) { - return (true,"Your Mac is currently in Dark Mode") - } - else { - return (true,"Your Mac is currently in Light Mode") + if isDarkModeEnabled() { + 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") + return (false, "macOS 10.14 Mojave or above is required") } } - + func isDarkModeEnabled() -> Bool { if #available(OSX 10.14, *) { let modeString = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") return (modeString == "Dark") - } - else { + } else { return false } } // MARK: Night Shift - func isNightShiftAvailable() -> (Bool,reason: String) { + func isNightShiftAvailable() -> (Bool, reason: String) { if #available(OSX 10.12.4, *) { - let (isAvailable,sunriseDate,sunsetDate, errorMessage) = getNightShiftInformation() - - if (isAvailable) { + let (isAvailable, sunriseDate, sunsetDate, errorMessage) = getNightShiftInformation() + + 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) + return (true, "Today’s sunrise: " + sunriseString + " Today’s sunset: " + sunsetString) } else { isNightShiftDataCached = true - return (false,errorMessage!) + return (false, errorMessage!) } } else { - return (false,"macOS 10.12.4 or above is required") + return (false, "macOS 10.12.4 or above is required") } } - func getNightShiftInformation() -> (Bool,sunrise: Date?, sunset: Date?, error: String?) - { - if (isNightShiftDataCached) { + // swiftlint:disable:next cyclomatic_complexity large_tuple + func getNightShiftInformation() -> (Bool, sunrise: Date?, sunset: Date?, error: String?) { + if isNightShiftDataCached { return (nightShiftAvailable, nightShiftSunrise, nightShiftSunset, nil) } - - let (nsInfo,ts) = shell(launchPath: "/usr/bin/corebrightnessdiag", arguments: ["nightshift-internal"]) - if (ts != 0) { + let (nsInfo, ts) = shell(launchPath: "/usr/bin/corebrightnessdiag", arguments: ["nightshift-internal"]) + + if ts != 0 { // Task didn't return correctly ? Abort - return (false,nil,nil,"Your Mac does not support Night Shift") + 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") + return (false, nil, nil, "Your Mac does not support Night Shift") } var sunrise: Date?, sunset: Date? - + for line in lines ?? [""] { if line.contains("sunrise") { let tmp = line.split(separator: "\"") @@ -280,41 +275,39 @@ class TimeManagement : NSObject { } } } - - if (sunset != nil && sunrise != nil) - { + + if sunset != nil && sunrise != nil { nightShiftSunrise = sunrise! nightShiftSunset = sunset! nightShiftAvailable = true isNightShiftDataCached = true - - return(true,sunrise,sunset, nil) + + 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") + return (false, nil, nil, "Location services may be disabled") } - - - private func shell(launchPath: String, arguments: [String] = []) -> (String? , Int32) { + + private 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 - + task.launch() - + let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) task.waitUntilExit() - + return (output, task.terminationStatus) } - + // MARK: - Brightness stuff (early, may get moved/will change) func getCurrentSleepTime() -> Int { // pmset -g | grep "^[ ]*sleep" | awk '{ print $2 }' @@ -322,54 +315,54 @@ class TimeManagement : NSObject { let pipe1 = Pipe() let pmset = Process() pmset.launchPath = "/usr/bin/env" - pmset.arguments = ["pmset","-g"] + pmset.arguments = ["pmset", "-g"] pmset.standardOutput = pipe1 - + let pipe2 = Pipe() let grep = Process() grep.launchPath = "/usr/bin/env" - grep.arguments = ["grep","^[ ]*sleep"] + 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.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) - + let output = String(data: data, encoding: .utf8) + if output != nil { let lines = output!.split(separator: "\n") if lines.count == 1 { - let n = Int(lines[0]) - if n != nil { - return n! + let newline = Int(lines[0]) + if let newLineIndex = newline { + return newLineIndex } } } - + return 0 } - + func getBrightness() -> Float { let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IODisplayConnect")) let pointer = UnsafeMutablePointer.allocate(capacity: 1) IODisplayGetFloatParameter(service, 0, kIODisplayBrightnessKey as CFString, pointer) - let c = pointer.pointee + let brightness = pointer.pointee IOObjectRelease(service) - return c + return brightness } - + func setBrightness(level: Float) { let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IODisplayConnect")) IODisplaySetFloatParameter(service, 0, kIODisplayBrightnessKey as CFString, level) @@ -385,20 +378,19 @@ class TimeManagement : NSObject { return true } } - + // MARK: - Location detection - func startLocationDetection() - { + 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!") } - + /*let status = CLLocationManager.authorizationStatus() if status == .restricted || status == .denied { @@ -425,22 +417,22 @@ class TimeManagement : NSObject { // Fallback on earlier versions } } - + } // MARK: - Core Location Delegates -extension TimeManagement : CLLocationManagerDelegate { +extension TimeManagement: CLLocationManagerDelegate { /* func locationManager(_ manager: CLLocationManager, didUpdateTo newLocation: CLLocation, from oldLocation: CLLocation) { print("\(newLocation) \(oldLocation)") }*/ - + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { let currentLocation = locations[locations.count - 1] print("\(currentLocation)") } - + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { errorLog("Location Manager error : \(error)") } diff --git a/Aerial/Source/Views/AerialPlayerItem.swift b/Aerial/Source/Views/AerialPlayerItem.swift index 9e52780e..1182a6f6 100644 --- a/Aerial/Source/Views/AerialPlayerItem.swift +++ b/Aerial/Source/Views/AerialPlayerItem.swift @@ -10,10 +10,10 @@ import AVKit class AerialPlayerItem: AVPlayerItem { var video: AerialVideo? - + init(video: AerialVideo) { let videoURL = video.url - let asset = CachedOrCachingAsset(videoURL) + let asset = cachedOrCachingAsset(videoURL) super.init(asset: asset, automaticallyLoadedAssetKeys: nil) self.video = video } diff --git a/Aerial/Source/Views/AerialView.swift b/Aerial/Source/Views/AerialView.swift index 88beb843..09a9aa88 100644 --- a/Aerial/Source/Views/AerialView.swift +++ b/Aerial/Source/Views/AerialView.swift @@ -12,35 +12,36 @@ import AVFoundation import AVKit @objc(AerialView) +// swiftlint:disable:next type_body_length class AerialView: ScreenSaverView { var playerLayer: AVPlayerLayer! var textLayer: CATextLayer! var clockLayer: CATextLayer! var messageLayer: CATextLayer! var lastCorner = -1 - var clockTimer : Timer? - + var clockTimer: Timer? + var preferencesController: PreferencesWindowController? static var players: [AVPlayer] = [AVPlayer]() static var previewPlayer: AVPlayer? static var previewView: AerialView? - + var player: AVPlayer? var currentVideo: AerialVideo? - + var observerWasSet = false var hasStartedPlaying = false var wasStopped = false var isDisabled = false - var timeObserver : Any? + var timeObserver: Any? var brightnessToRestore: Float? - + static var shouldFade: Bool { let preferences = Preferences.sharedInstance return (preferences.fadeMode != Preferences.FadeMode.disabled.rawValue) } - + static var fadeDuration: Double { let preferences = Preferences.sharedInstance switch preferences.fadeMode { @@ -54,7 +55,7 @@ class AerialView: ScreenSaverView { return 0.10 } } - + static var textFadeDuration: Double { let preferences = Preferences.sharedInstance switch preferences.fadeModeText { @@ -68,18 +69,20 @@ class AerialView: ScreenSaverView { return 0.10 } } - + static var sharingPlayers: Bool { let preferences = Preferences.sharedInstance return (preferences.multiMonitorMode == Preferences.MultiMonitorMode.mirrored.rawValue) } - + static var sharedViews: [AerialView] = [] - static var instanciatedViews: [AerialView] = [] // because of lifecycle in Preview, we may pile up old/no longer shared instanciated views that we need to track to not reuse + // 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] = [] //var instanciatedIndex: Int - + // MARK: - Shared Player - + static var singlePlayerAlreadySetup: Bool = false static var sharedPlayerIndex: Int? @@ -96,10 +99,10 @@ class AerialView: ScreenSaverView { return _player! } } - + return Static.player } - + // MARK: - Init / Setup // This is the one used by System Preferences override init?(frame: NSRect, isPreview: Bool) { @@ -108,53 +111,52 @@ class AerialView: ScreenSaverView { self.animationTimeInterval = 1.0 / 30.0 setup() } - + // This is the one used by App required init?(coder: NSCoder) { super.init(coder: coder) debugLog("avInit2") setup() } - + deinit { debugLog("\(self.description) deinit AerialView") NotificationCenter.default.removeObserver(self) - - - + // set player item to nil if not preview player if player != AerialView.previewPlayer { player?.rate = 0 player?.replaceCurrentItem(with: nil) } - + guard let player = self.player else { return } - + // Remove from player index - + let indexMaybe = AerialView.players.index(of: player) - + guard let index = indexMaybe else { return } - + AerialView.players.remove(at: index) } - + func setDimTimers() { if #available(OSX 10.12, *) { let preferences = Preferences.sharedInstance if preferences.dimBrightness && preferences.dimInMinutes! > 0 && preferences.startDim != preferences.endDim { + // swiftlint:disable:next line_length debugLog("seting brightness timers from \(String(describing: preferences.startDim)) to \(String(describing: preferences.endDim)) in \(String(describing: preferences.dimInMinutes))") let interval = preferences.dimInMinutes! * 6 // * 60 / 10, we make 10 intermediate steps - - for i in 1...10 { - _ = Timer.scheduledTimer(withTimeInterval: TimeInterval(interval*i), repeats: false) { (Timer) in + + for idx in 1...10 { + _ = Timer.scheduledTimer(withTimeInterval: TimeInterval(interval * idx), repeats: false) { (_) in let timeManagement = TimeManagement.sharedInstance - let val = preferences.startDim! - ((preferences.startDim!-preferences.endDim!)/10 * Double(i)) - debugLog("Firing event \(i) brightness to \(val)") + let val = preferences.startDim! - ((preferences.startDim! - preferences.endDim!) / 10 * Double(idx)) + debugLog("Firing event \(idx) brightness to \(val)") timeManagement.setBrightness(level: Float(val)) } } @@ -164,7 +166,8 @@ class AerialView: ScreenSaverView { warnLog("Brightness control not available < macOS 10.12") } } - + + // swiftlint:disable:next cyclomatic_complexity func setup() { debugLog("\(self.description) AerialView setup init") let preferences = Preferences.sharedInstance @@ -174,8 +177,8 @@ class AerialView: ScreenSaverView { if preferences.dimBrightness { if !isPreview && brightnessToRestore == nil { let timeManagement = TimeManagement.sharedInstance - let (should,to) = timeManagement.shouldRestrictPlaybackToDayNightVideo() - + let (should, to) = timeManagement.shouldRestrictPlaybackToDayNightVideo() + if !preferences.dimOnlyAtNight || (preferences.dimOnlyAtNight && should && to == "night") { if !preferences.dimOnlyOnBattery || (preferences.dimOnlyOnBattery && timeManagement.isOnBattery()) { brightnessToRestore = timeManagement.getBrightness() @@ -187,31 +190,31 @@ class AerialView: ScreenSaverView { } } - if (AerialView.singlePlayerAlreadySetup) { + if AerialView.singlePlayerAlreadySetup { debugLog("singlePlayerAlreadySetup, checking if was stopped to purge") // On previews, it's possible that our shared player was stopped and is not reusable if AerialView.instanciatedViews[AerialView.sharedPlayerIndex!].wasStopped { debugLog("Purging previous singlePlayer") AerialView.singlePlayerAlreadySetup = false AerialView.sharedPlayerIndex = nil - + AerialView.instanciatedViews = [AerialView]() // Clear the list of instanciated stuff AerialView.sharedViews = [AerialView]() // And the list of sharedViews } } - + var localPlayer: AVPlayer? - + let notPreview = !isPreview debugLog("\(self.description) isPreview : \(isPreview)") - + if notPreview { debugLog("\(self.description) singlePlayerAlreadySetup \(AerialView.singlePlayerAlreadySetup)") - if (AerialView.singlePlayerAlreadySetup && preferences.multiMonitorMode == Preferences.MultiMonitorMode.mainOnly.rawValue) { + if AerialView.singlePlayerAlreadySetup && preferences.multiMonitorMode == Preferences.MultiMonitorMode.mainOnly.rawValue { isDisabled = true return } - + // check if we should share preview's player //let noPlayers = (AerialView.players.count == 0) let previewPlayerExists = (AerialView.previewPlayer != nil) @@ -223,14 +226,14 @@ class AerialView: ScreenSaverView { } else { AerialView.previewView = self } - + if AerialView.sharingPlayers { AerialView.sharedViews.append(self) } - + // We track all views here to clean the sharing code AerialView.instanciatedViews.append(self) - + if localPlayer == nil { debugLog("\(self.description) no local player") @@ -238,30 +241,30 @@ class AerialView: ScreenSaverView { /*if AerialView.previewPlayer != nil { localPlayer = AerialView.previewPlayer } else {*/ - + localPlayer = AerialView.sharedPlayer //} } else { localPlayer = AVPlayer() } } - + guard let player = localPlayer else { errorLog("\(self.description) Couldn't create AVPlayer!") return } - + self.player = player - + if self.isPreview { AerialView.previewPlayer = player } else if !AerialView.sharingPlayers { // add to player list AerialView.players.append(player) } - + setupPlayerLayer(withPlayer: player) - + if AerialView.sharingPlayers && AerialView.singlePlayerAlreadySetup { self.playerLayer.player = AerialView.instanciatedViews[AerialView.sharedPlayerIndex!].player self.playerLayer.opacity = 0 @@ -273,16 +276,15 @@ class AerialView: ScreenSaverView { AerialView.singlePlayerAlreadySetup = true AerialView.sharedPlayerIndex = AerialView.instanciatedViews.count-1 } - - ManifestLoader.instance.addCallback { videos in + + ManifestLoader.instance.addCallback { _ in self.playNextVideo() } } - + override func viewDidChangeBackingProperties() { debugLog("\(self.description) backing change \((self.window?.backingScaleFactor) ?? 1.0) isDisabled: \(isDisabled)") - if (!isDisabled) - { + if !isDisabled { self.layer!.contentsScale = (self.window?.backingScaleFactor) ?? 1.0 self.playerLayer.contentsScale = (self.window?.backingScaleFactor) ?? 1.0 self.textLayer.contentsScale = (self.window?.backingScaleFactor) ?? 1.0 @@ -290,10 +292,10 @@ class AerialView: ScreenSaverView { self.messageLayer.contentsScale = (self.window?.backingScaleFactor) ?? 1.0 } } - + func setupPlayerLayer(withPlayer player: AVPlayer) { debugLog("\(self.description) setupPlayerLayer") - + self.layer = CALayer() guard let layer = self.layer else { errorLog("\(self.description) Couldn't create CALayer") @@ -306,7 +308,7 @@ class AerialView: ScreenSaverView { //self. debugLog("\(self.description) setting up player layer with frame: \(self.bounds) / \(self.frame)") - + playerLayer = AVPlayerLayer(player: player) if #available(OSX 10.10, *) { playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill @@ -315,7 +317,7 @@ class AerialView: ScreenSaverView { playerLayer.frame = layer.bounds //playerLayer.contentsScale = 1.0 // NSScreen.main?.backingScaleFactor ?? 1.0 layer.addSublayer(playerLayer) - + textLayer = CATextLayer() textLayer.frame = layer.bounds textLayer.opacity = 0 @@ -325,7 +327,7 @@ class AerialView: ScreenSaverView { textLayer.shadowColor = CGColor.black //textLayer.contentsScale = 1.0 // NSScreen.main?.backingScaleFactor ?? 1.0 layer.addSublayer(textLayer) - + // Clock Layer clockLayer = CATextLayer() clockLayer.opacity = 0 @@ -335,7 +337,7 @@ class AerialView: ScreenSaverView { textLayer.shadowColor = CGColor.black //clockLayer.contentsScale = 1.0 // NSScreen.main?.backingScaleFactor ?? 1.0 layer.addSublayer(clockLayer) - + // Message Layer messageLayer = CATextLayer() messageLayer.opacity = 0 @@ -346,7 +348,7 @@ class AerialView: ScreenSaverView { //messageLayer.contentsScale = 1.0 // NSScreen.main?.backingScaleFactor ?? 1.0 layer.addSublayer(messageLayer) } - + // MARK: - Lifecycle stuff /* override func draw(_ rect: NSRect) { }*/ @@ -354,13 +356,13 @@ class AerialView: ScreenSaverView { super.startAnimation() debugLog("\(self.description) startAnimation") - if !isDisabled{ + if !isDisabled { // Previews may be restarted, but our layer will get hidden (somehow) so show it back - if (isPreview && player?.currentTime() != CMTime.zero) { + if isPreview && player?.currentTime() != CMTime.zero { playerLayer.opacity = 1 player?.play() } - + /*if player?.rate == 0 { }*/ @@ -374,9 +376,9 @@ class AerialView: ScreenSaverView { if !isDisabled { player?.pause() } - + let preferences = Preferences.sharedInstance - + if preferences.dimBrightness { if !isPreview && brightnessToRestore != nil { let timeManagement = TimeManagement.sharedInstance @@ -388,29 +390,32 @@ class AerialView: ScreenSaverView { } // 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)") playNextVideo() debugLog("\(self.description) playing next video for player \(String(describing: player))") } - + // Wait for the player to be ready - internal override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + // swiftlint:disable:next block_based_kvo + internal override func observeValue(forKeyPath keyPath: String?, + of object: Any?, change: [NSKeyValueChangeKey: Any]?, + context: UnsafeMutableRawPointer?) { debugLog("\(self.description) observeValue \(String(describing: keyPath))") if self.playerLayer.isReadyForDisplay { self.player!.play() @@ -424,30 +429,30 @@ class AerialView: ScreenSaverView { } else { self.addPlayerFades(player: self.player!, playerLayer: self.playerLayer, video: self.currentVideo!) } - + // Descriptions on main only for now - + self.addDescriptions(player: self.player!, video: self.currentVideo!) } } - + // MARK: - playNextVideo() func playNextVideo() { //let timeManagement = TimeManagement.sharedInstance let notificationCenter = NotificationCenter.default // Clear everything - if (timeObserver != nil) { + if timeObserver != nil { self.player!.removeTimeObserver(timeObserver!) timeObserver = nil } self.textLayer.removeAllAnimations() self.clockLayer.removeAllAnimations() self.messageLayer.removeAllAnimations() - + // remove old entries notificationCenter.removeObserver(self) - + let player = AVPlayer() // play another video let oldPlayer = self.player @@ -461,16 +466,16 @@ class AerialView: ScreenSaverView { 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 } - + // 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 @@ -480,7 +485,7 @@ class AerialView: ScreenSaverView { } let randomVideo = ManifestLoader.instance.randomVideo(excluding: currentVideos) - + guard let video = randomVideo else { errorLog("\(self.description) Error grabbing random video!") return @@ -489,13 +494,10 @@ class AerialView: ScreenSaverView { // Workaround to avoid local playback making network calls let item = AerialPlayerItem(video: video) - if !video.isAvailableOffline - { + if !video.isAvailableOffline { player.replaceCurrentItem(with: item) debugLog("\(self.description) streaming video (not fully available offline) : \(video.url)") - } - else - { + } else { let localurl = URL(fileURLWithPath: VideoCache.cachePath(forVideo: video)!) let localitem = AVPlayerItem(url: localurl) player.replaceCurrentItem(with: localitem) @@ -511,7 +513,7 @@ class AerialView: ScreenSaverView { errorLog("\(self.description) No current item!") return } - + debugLog("\(self.description) observing current item \(currentItem)") // Descriptions and fades are set when we begin playback @@ -519,7 +521,7 @@ class AerialView: ScreenSaverView { observerWasSet = true playerLayer.addObserver(self, forKeyPath: "readyForDisplay", options: .initial, context: nil) } - + notificationCenter.addObserver(self, selector: #selector(AerialView.playerItemDidReachEnd(_:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, @@ -538,11 +540,10 @@ class AerialView: ScreenSaverView { object: currentItem) player.actionAtItemEnd = AVPlayer.ActionAtItemEnd.none } - + // MARK: - Extra Animations - private func addPlayerFades(player: AVPlayer, playerLayer: AVPlayerLayer, video: AerialVideo) - { + private func addPlayerFades(player: AVPlayer, playerLayer: AVPlayerLayer, video: AerialVideo) { // We only fade in/out if we have duration if video.duration > 0 && AerialView.shouldFade { playerLayer.opacity = 0 @@ -552,22 +553,20 @@ class AerialView: ScreenSaverView { fadeAnimation.duration = video.duration fadeAnimation.calculationMode = CAAnimationCalculationMode.cubic playerLayer.add(fadeAnimation, forKey: "mainfade") - } - else { + } else { playerLayer.opacity = 1.0 } } - - private func addDescriptions(player: AVPlayer, video: AerialVideo) - { + + // swiftlint:disable:next cyclomatic_complexity + private func addDescriptions(player: AVPlayer, video: AerialVideo) { let poiStringProvider = PoiStringProvider.sharedInstance let preferences = Preferences.sharedInstance - - if (preferences.showDescriptions) - { + + if preferences.showDescriptions { // Preventively, make sure we have poi as tvOS11/10 videos won't have them - if (video.poi.count > 0 && poiStringProvider.loadedDescriptions) || (preferences.useCommunityDescriptions && video.communityPoi.count > 0 && poiStringProvider.getPoiKeys(video: video).count > 0) - { + if (!video.poi.isEmpty && poiStringProvider.loadedDescriptions) || + (preferences.useCommunityDescriptions && !video.communityPoi.isEmpty && !poiStringProvider.getPoiKeys(video: video).isEmpty) { // Collect all the timestamps from the JSON var times = [NSValue]() let keys = poiStringProvider.getPoiKeys(video: video) @@ -577,56 +576,47 @@ class AerialView: ScreenSaverView { times.append(NSValue(time: CMTime(seconds: timeStamp, preferredTimescale: 1))) } // The JSON isn't sorted so we fix that - times.sort(by: { ($0 as! CMTime).seconds < ($1 as! CMTime).seconds } ) - + times.sort(by: { ($0 as! CMTime).seconds < ($1 as! CMTime).seconds }) + // Animate the very first one on it's own let str = poiStringProvider.getString(key: keys["0"]!, video: video) - var fadeAnimation:CAKeyframeAnimation - - if (preferences.showDescriptionsMode == Preferences.DescriptionMode.fade10seconds.rawValue) - { + var fadeAnimation: CAKeyframeAnimation + + if preferences.showDescriptionsMode == Preferences.DescriptionMode.fade10seconds.rawValue { fadeAnimation = createFadeInOutAnimation(duration: 11) - } - else - { + } else { // Always show mode, if there's more than one point, use that, if not either use known video duration or some hardcoded duration - if times.count > 1 - { + if times.count > 1 { let duration = (times[1] as! CMTime).seconds - 1 fadeAnimation = createFadeInOutAnimation(duration: duration) - } - else if video.duration > 0 - { + } else if video.duration > 0 { fadeAnimation = createFadeInOutAnimation(duration: video.duration - 1) - } - else - { + } else { // We should have the duration, if we don't, hardcode the longest known duration fadeAnimation = createFadeInOutAnimation(duration: 807) } } self.textLayer.add(fadeAnimation, forKey: "textfade") - if (video.duration > 0) { + if video.duration > 0 { setupTextLayer(string: str, duration: fadeAnimation.duration, isInitial: true, totalDuration: video.duration - 1) } else { setupTextLayer(string: str, duration: fadeAnimation.duration, isInitial: true, totalDuration: 807) } - + let mainQueue = DispatchQueue.main - // We then callback for each timestamp timeObserver = player.addBoundaryTimeObserver(forTimes: times, queue: mainQueue) { var isLastTimeStamp = true var intervalUntilNextTimeStamp = 0.0 - + // find closest timestamp to when we're waking up var closest = 1000.0 var closestTime = 0.0 - var closestTimeValue: NSValue = NSValue(time:CMTime.zero) - + var closestTimeValue: NSValue = NSValue(time: CMTime.zero) + for time in times { let ts = (time as! CMTime).seconds let distance = abs(ts - player.currentTime().seconds) @@ -636,79 +626,66 @@ class AerialView: ScreenSaverView { closestTimeValue = time } } - + // We also need the next timeStamp let index = times.firstIndex(of: closestTimeValue) if index! < times.count - 1 { isLastTimeStamp = false intervalUntilNextTimeStamp = (times[index!+1] as! CMTime).seconds - closestTime - 1 - } - else if video.duration > 0 { + } else if video.duration > 0 { isLastTimeStamp = true // If we have a duration for the video, we may not ! intervalUntilNextTimeStamp = video.duration - closestTime - 1 } - + // Animate text var fadeAnimation: CAKeyframeAnimation - - if (preferences.showDescriptionsMode == Preferences.DescriptionMode.fade10seconds.rawValue) - { + + if preferences.showDescriptionsMode == Preferences.DescriptionMode.fade10seconds.rawValue { fadeAnimation = self.createFadeInOutAnimation(duration: 11) - } - else - { + } else { if isLastTimeStamp, video.duration == 0 { // We have no idea when the video ends, so 2 minutes it is fadeAnimation = self.createFadeInOutAnimation(duration: 120) - } - else { + } else { fadeAnimation = self.createFadeInOutAnimation(duration: intervalUntilNextTimeStamp) } } // Get the string for the current timestamp - let key = String(format: "%.0f",closestTime) + let key = String(format: "%.0f", closestTime) let str = poiStringProvider.getString(key: keys[key]!, video: video) self.setupTextLayer(string: str, duration: fadeAnimation.duration, isInitial: false, totalDuration: video.duration-1) self.textLayer.add(fadeAnimation, forKey: "textfade") } - } - else - { + } else { // We don't have any extended description, using Secondary name (location) or video name (City) let str: String - if (video.secondaryName != "") { + if video.secondaryName != "" { str = video.secondaryName } else { str = video.name } - var fadeAnimation:CAKeyframeAnimation + var fadeAnimation: CAKeyframeAnimation - if (preferences.showDescriptionsMode == Preferences.DescriptionMode.fade10seconds.rawValue) - { + if preferences.showDescriptionsMode == Preferences.DescriptionMode.fade10seconds.rawValue { fadeAnimation = createFadeInOutAnimation(duration: 11) - } - else - { + } else { // Always show mode, use known video duration or some hardcoded duration - if video.duration > 0 - { + if video.duration > 0 { fadeAnimation = createFadeInOutAnimation(duration: video.duration - 1) - } - else - { + } else { // We should have the duration, if we don't, hardcode the longest known duration fadeAnimation = createFadeInOutAnimation(duration: 807) } } self.textLayer.add(fadeAnimation, forKey: "textfade") - setupTextLayer(string: str, duration : fadeAnimation.duration, isInitial: true, totalDuration: video.duration) + setupTextLayer(string: str, duration: fadeAnimation.duration, isInitial: true, totalDuration: video.duration) } } } - - func setupTextLayer(string:String, duration: CFTimeInterval, isInitial: Bool, totalDuration: Double) { + + func setupTextLayer(string: String, duration: CFTimeInterval, isInitial: Bool, totalDuration: Double) { // Setup string self.textLayer.string = string self.textLayer.isWrapped = true @@ -716,7 +693,7 @@ class AerialView: ScreenSaverView { // We override font size on previews var fontSize = CGFloat(preferences.fontSize!) - if (layer!.bounds.height < 200) { + if layer!.bounds.height < 200 { fontSize = 12 } @@ -733,26 +710,26 @@ class AerialView: ScreenSaverView { // Get font with a fallback in case var font = NSFont(name: "Helvetica Neue Medium", size: 28) - if let tryFont = NSFont(name: preferences.fontName!,size: fontSize) { + if let tryFont = NSFont(name: preferences.fontName!, size: fontSize) { font = tryFont } // Make sure we change the layer font/size self.textLayer.font = font self.textLayer.fontSize = fontSize - - let attributes: [NSAttributedString.Key : Any] = [NSAttributedString.Key.font : font as Any] + + let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font as Any] // Calculate bounding box - let s = NSAttributedString(string: string, attributes: attributes) - - var rect = s.boundingRect(with: boundingRect, options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin]) + let str = NSAttributedString(string: string, attributes: attributes) + + var rect = str.boundingRect(with: boundingRect, options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin]) // Last line won't appear if we don't adjust - rect = CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.width, height: rect.height+10) - + rect = CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.width, height: rect.height + 10) + // Rebind frame self.textLayer.frame = rect - + // At the position the user wants if preferences.descriptionCorner == Preferences.DescriptionCorner.random.rawValue { // Randomish, we still want something different @@ -761,7 +738,7 @@ class AerialView: ScreenSaverView { corner = Int.random(in: 0...3) } lastCorner = corner - + repositionTextLayer(position: corner) setupAndRepositionExtra(position: corner, duration: duration, isInitial: isInitial, totalDuration: totalDuration) } else { @@ -769,101 +746,100 @@ class AerialView: ScreenSaverView { setupAndRepositionExtra(position: preferences.descriptionCorner!, duration: duration, isInitial: isInitial, totalDuration: totalDuration) } } + private func reRectClock() { let preferences = Preferences.sharedInstance let dateFormatter = DateFormatter() - if (preferences.withSeconds) { + if preferences.withSeconds { dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "j:mm:ss", options: 0, locale: Locale.current) } else { dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "j:mm", options: 0, locale: Locale.current) } - + let dateString = dateFormatter.string(from: Date()) self.clockLayer.string = dateString // We override font size on previews var fontSize = CGFloat(preferences.extraFontSize!) - if (layer!.bounds.height < 200) { + if layer!.bounds.height < 200 { fontSize = 12 } - + // Get font with a fallback in case var font = NSFont(name: "Monaco", size: 28) - if let tryFont = NSFont(name: preferences.extraFontName!,size: fontSize) { + if let tryFont = NSFont(name: preferences.extraFontName!, size: fontSize) { font = tryFont } - + // Make sure we change the layer font/size self.clockLayer.font = font self.clockLayer.fontSize = fontSize - - let attributes: [NSAttributedString.Key : Any] = [NSAttributedString.Key.font : font as Any] - + + let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font as Any] + // Calculate bounding box - let s = NSAttributedString(string: dateString, attributes: attributes) - let rect = s.boundingRect(with: layer!.visibleRect.size, options: NSString.DrawingOptions.usesLineFragmentOrigin) - + let str = NSAttributedString(string: dateString, attributes: attributes) + let rect = str.boundingRect(with: layer!.visibleRect.size, options: NSString.DrawingOptions.usesLineFragmentOrigin) + // Rebind frame let oldRect = self.clockLayer.frame self.clockLayer.frame = CGRect(x: oldRect.minX, y: oldRect.minY, width: rect.maxX, height: rect.maxY) } - - private func setupAndRepositionExtra(position: Int, duration: CFTimeInterval, isInitial: Bool, totalDuration: Double) - { + + // swiftlint:disable:next cyclomatic_complexity + private func setupAndRepositionExtra(position: Int, duration: CFTimeInterval, isInitial: Bool, totalDuration: Double) { let preferences = Preferences.sharedInstance - if (preferences.showClock) - { - if (isInitial) { - if (clockTimer == nil) - { + if preferences.showClock { + if isInitial { + if clockTimer == nil { if #available(OSX 10.12, *) { - clockTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (Timer) in + clockTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (_) in self.reRectClock() }) } - + } let dateFormatter = DateFormatter() - if (preferences.withSeconds) { + if preferences.withSeconds { dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "j:mm:ss", options: 0, locale: Locale.current) } else { dateFormatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "j:mm", options: 0, locale: Locale.current) } let dateString = dateFormatter.string(from: Date()) - + self.clockLayer.string = dateString // We override font size on previews var fontSize = CGFloat(preferences.extraFontSize!) - if (layer!.bounds.height < 200) { + if layer!.bounds.height < 200 { fontSize = 12 } - + // Get font with a fallback in case var font = NSFont(name: "Monaco", size: 28) - if let tryFont = NSFont(name: preferences.extraFontName!,size: fontSize) { + if let tryFont = NSFont(name: preferences.extraFontName!, size: fontSize) { font = tryFont } - + // Make sure we change the layer font/size self.clockLayer.font = font self.clockLayer.fontSize = fontSize - - let attributes: [NSAttributedString.Key : Any] = [NSAttributedString.Key.font : font as Any] - + + let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font as Any] + // Calculate bounding box - let s = NSAttributedString(string: dateString, attributes: attributes) - let rect = s.boundingRect(with: layer!.visibleRect.size, options: NSString.DrawingOptions.usesLineFragmentOrigin) - + let str = NSAttributedString(string: dateString, attributes: attributes) + let rect = str.boundingRect(with: layer!.visibleRect.size, options: NSString.DrawingOptions.usesLineFragmentOrigin) + // Rebind frame self.clockLayer.frame = rect //clockLayer.anchorPoint = CGPoint(x: 0, y:0) //clockLayer.position = CGPoint(x:10 ,y:10+textLayer.visibleRect.height) //clockLayer.opacity = 1.0 } - - if (preferences.descriptionCorner == Preferences.DescriptionCorner.random.rawValue) { + + if preferences.descriptionCorner == Preferences.DescriptionCorner.random.rawValue { clockLayer.add(createFadeInOutAnimation(duration: duration), forKey: "textfade") } else if isInitial && preferences.showDescriptionsMode == Preferences.DescriptionMode.always.rawValue { clockLayer.add(createFadeInOutAnimation(duration: totalDuration), forKey: "textfade") @@ -872,65 +848,66 @@ class AerialView: ScreenSaverView { } } - if (preferences.showMessage && preferences.showMessageString != "") { + if preferences.showMessage && preferences.showMessageString != "" { self.messageLayer.string = preferences.showMessageString - + // We override font size on previews var fontSize = CGFloat(preferences.extraFontSize!) - if (layer!.bounds.height < 200) { + if layer!.bounds.height < 200 { fontSize = 12 } - + // Get font with a fallback in case var font = NSFont(name: "Helvetica Neue Medium", size: 28) - if let tryFont = NSFont(name: preferences.extraFontName!,size: fontSize) { + if let tryFont = NSFont(name: preferences.extraFontName!, size: fontSize) { font = tryFont } - + // Make sure we change the layer font/size self.messageLayer.font = font self.messageLayer.fontSize = fontSize - - let attributes: [NSAttributedString.Key : Any] = [NSAttributedString.Key.font : font as Any] - + + let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font as Any] + // Calculate bounding box - let s = NSAttributedString(string: preferences.showMessageString!, attributes: attributes) - let rect = s.boundingRect(with: layer!.visibleRect.size, options: NSString.DrawingOptions.usesLineFragmentOrigin) - + let str = NSAttributedString(string: preferences.showMessageString!, attributes: attributes) + let rect = str.boundingRect(with: layer!.visibleRect.size, options: NSString.DrawingOptions.usesLineFragmentOrigin) + // Rebind frame self.messageLayer.frame = rect //messageLayer.anchorPoint = CGPoint(x: 0, y:0) //messageLayer.position = CGPoint(x:10 ,y:10+textLayer.visibleRect.height) //messageLayer.opacity = 1.0 - if (preferences.descriptionCorner == Preferences.DescriptionCorner.random.rawValue) { + if preferences.descriptionCorner == Preferences.DescriptionCorner.random.rawValue { self.messageLayer.add(createFadeInOutAnimation(duration: duration), forKey: "textfade") } else if isInitial && preferences.showDescriptionsMode == Preferences.DescriptionMode.always.rawValue { self.messageLayer.add(createFadeInOutAnimation(duration: totalDuration), forKey: "textfade") } else if preferences.showDescriptionsMode == Preferences.DescriptionMode.fade10seconds.rawValue { self.messageLayer.add(createFadeInOutAnimation(duration: duration), forKey: "textfade") } - - + } - if (!isInitial && preferences.extraCorner == Preferences.ExtraCorner.same.rawValue && preferences.showDescriptionsMode == Preferences.DescriptionMode.always.rawValue && preferences.descriptionCorner != Preferences.DescriptionCorner.random.rawValue) { + if !isInitial && preferences.extraCorner == Preferences.ExtraCorner.same.rawValue && + preferences.showDescriptionsMode == Preferences.DescriptionMode.always.rawValue && + preferences.descriptionCorner != Preferences.DescriptionCorner.random.rawValue { animateClockAndMessageLayer(position: position) } else { - if preferences.extraCorner == Preferences.ExtraCorner.same.rawValue{ + if preferences.extraCorner == Preferences.ExtraCorner.same.rawValue { repositionClockAndMessageLayer(position: position, alone: false) - } else if preferences.extraCorner == Preferences.ExtraCorner.hOpposed.rawValue{ + } else if preferences.extraCorner == Preferences.ExtraCorner.hOpposed.rawValue { repositionClockAndMessageLayer(position: (position+2)%4, alone: true) - } else if preferences.extraCorner == Preferences.ExtraCorner.dOpposed.rawValue{ + } else if preferences.extraCorner == Preferences.ExtraCorner.dOpposed.rawValue { repositionClockAndMessageLayer(position: 3-position, alone: true) } } } - + private func animateClockAndMessageLayer(position: Int) { - var clockDecal : CGFloat = 0 - var messageDecal : CGFloat = 0 + var clockDecal: CGFloat = 0 + var messageDecal: CGFloat = 0 let preferences = Preferences.sharedInstance - + var mx = CGFloat(preferences.marginX!) var my = CGFloat(preferences.marginY!) if !preferences.overrideMargins { @@ -944,20 +921,20 @@ class AerialView: ScreenSaverView { clockDecal += textLayer.visibleRect.height messageDecal += textLayer.visibleRect.height - + if preferences.showMessage { clockDecal += messageLayer.visibleRect.height } let duration = 1 + AerialView.textFadeDuration - var cto, mto : CGPoint - if (position == Preferences.DescriptionCorner.topLeft.rawValue) { + var cto, mto: CGPoint + if position == Preferences.DescriptionCorner.topLeft.rawValue { cto = CGPoint(x: mx, y: layer!.bounds.height-my-clockDecal) mto = CGPoint(x: mx, y: layer!.bounds.height-my-messageDecal) - } else if (position == Preferences.DescriptionCorner.bottomLeft.rawValue) { + } else if position == Preferences.DescriptionCorner.bottomLeft.rawValue { cto = CGPoint(x: mx, y: my+clockDecal) mto = CGPoint(x: mx, y: my+messageDecal) - } else if (position == Preferences.DescriptionCorner.topRight.rawValue) { + } else if position == Preferences.DescriptionCorner.topRight.rawValue { cto = CGPoint(x: layer!.bounds.width-mx, y: layer!.bounds.height-my-clockDecal) mto = CGPoint(x: layer!.bounds.width-mx, y: layer!.bounds.height-my-messageDecal) } else { @@ -968,15 +945,12 @@ class AerialView: ScreenSaverView { self.clockLayer.add(createMoveAnimation(layer: clockLayer, to: cto, duration: duration), forKey: "position") self.messageLayer.add(createMoveAnimation(layer: messageLayer, to: mto, duration: duration), forKey: "position") } - - - - - private func repositionClockAndMessageLayer(position:Int, alone:Bool) { - var clockDecal : CGFloat = 0 - var messageDecal : CGFloat = 0 + + private func repositionClockAndMessageLayer(position: Int, alone: Bool) { + var clockDecal: CGFloat = 0 + var messageDecal: CGFloat = 0 let preferences = Preferences.sharedInstance - + var mx = CGFloat(preferences.marginX!) var my = CGFloat(preferences.marginY!) if !preferences.overrideMargins { @@ -988,32 +962,31 @@ class AerialView: ScreenSaverView { my = 10 } - if !alone { clockDecal += textLayer.visibleRect.height messageDecal += textLayer.visibleRect.height } - + if preferences.showMessage { clockDecal += messageLayer.visibleRect.height } - if (position == Preferences.DescriptionCorner.topLeft.rawValue) { + if position == Preferences.DescriptionCorner.topLeft.rawValue { self.clockLayer.anchorPoint = CGPoint(x: 0, y: 1) self.clockLayer.position = CGPoint(x: mx, y: layer!.bounds.height-my-clockDecal) self.messageLayer.anchorPoint = CGPoint(x: 0, y: 1) self.messageLayer.position = CGPoint(x: mx, y: layer!.bounds.height-my-messageDecal) - } else if (position == Preferences.DescriptionCorner.bottomLeft.rawValue) { + } else if position == Preferences.DescriptionCorner.bottomLeft.rawValue { self.clockLayer.anchorPoint = CGPoint(x: 0, y: 0) self.clockLayer.position = CGPoint(x: mx, y: my+clockDecal) self.messageLayer.anchorPoint = CGPoint(x: 0, y: 0) self.messageLayer.position = CGPoint(x: mx, y: my+messageDecal) - } else if (position == Preferences.DescriptionCorner.topRight.rawValue) { + } else if position == Preferences.DescriptionCorner.topRight.rawValue { self.clockLayer.anchorPoint = CGPoint(x: 1, y: 1) self.clockLayer.position = CGPoint(x: layer!.bounds.width-mx, y: layer!.bounds.height-my-clockDecal) self.messageLayer.anchorPoint = CGPoint(x: 1, y: 1) self.messageLayer.position = CGPoint(x: layer!.bounds.width-mx, y: layer!.bounds.height-my-messageDecal) - } else if (position == Preferences.DescriptionCorner.bottomRight.rawValue) { + } else if position == Preferences.DescriptionCorner.bottomRight.rawValue { self.clockLayer.anchorPoint = CGPoint(x: 1, y: 0) self.clockLayer.position = CGPoint(x: layer!.bounds.width-mx, y: my+clockDecal) self.messageLayer.anchorPoint = CGPoint(x: 1, y: 0) @@ -1021,7 +994,7 @@ class AerialView: ScreenSaverView { } } - private func repositionTextLayer(position:Int) { + private func repositionTextLayer(position: Int) { let preferences = Preferences.sharedInstance var mx = CGFloat(preferences.marginX!) var my = CGFloat(preferences.marginY!) @@ -1033,59 +1006,65 @@ class AerialView: ScreenSaverView { mx = 10 my = 10 } - - if (position == Preferences.DescriptionCorner.topLeft.rawValue) { + + if position == Preferences.DescriptionCorner.topLeft.rawValue { self.textLayer.anchorPoint = CGPoint(x: 0, y: 1) self.textLayer.position = CGPoint(x: mx, y: layer!.bounds.height-my) self.textLayer.alignmentMode = .left - } else if (position == Preferences.DescriptionCorner.bottomLeft.rawValue) { + } else if position == Preferences.DescriptionCorner.bottomLeft.rawValue { self.textLayer.anchorPoint = CGPoint(x: 0, y: 0) self.textLayer.position = CGPoint(x: mx, y: my) self.textLayer.alignmentMode = .left - } else if (position == Preferences.DescriptionCorner.topRight.rawValue) { + } else if position == Preferences.DescriptionCorner.topRight.rawValue { self.textLayer.anchorPoint = CGPoint(x: 1, y: 1) self.textLayer.position = CGPoint(x: layer!.bounds.width-mx, y: layer!.bounds.height-my) self.textLayer.alignmentMode = .right - } else if (position == Preferences.DescriptionCorner.bottomRight.rawValue) { + } else if position == Preferences.DescriptionCorner.bottomRight.rawValue { self.textLayer.anchorPoint = CGPoint(x: 1, y: 0) self.textLayer.position = CGPoint(x: layer!.bounds.width-mx, y: my) self.textLayer.alignmentMode = .right } } - + // 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.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 move animation - func createMoveAnimation(layer : CALayer, to: CGPoint, duration: Double) -> CABasicAnimation { + 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; + 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 = PreferencesWindowController(windowNibName: "PreferencesWindow") - + preferencesController = controller return controller.window } -} +} // swiftlint:disable:this file_length diff --git a/Aerial/Source/Views/CheckCellView.swift b/Aerial/Source/Views/CheckCellView.swift index 1560fdc3..a672ee1d 100644 --- a/Aerial/Source/Views/CheckCellView.swift +++ b/Aerial/Source/Views/CheckCellView.swift @@ -9,7 +9,7 @@ import Cocoa enum VideoStatus { - case unknown, notAvailable,queued,downloading,downloaded + case unknown, notAvailable, queued, downloading, downloaded } class CheckCellView: NSTableCellView { @@ -19,50 +19,49 @@ class CheckCellView: NSTableCellView { @IBOutlet var formatLabel: NSTextField! @IBOutlet var queuedImage: NSImageView! @IBOutlet var mainTextField: NSTextField! - - var onCheck: ((Bool) -> (Void))? + + 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?) { + + @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) { + } else if videoManager.isVideoQueued(id: video!.id) { status = .queued addButton.isHidden = true progressIndicator.isHidden = true @@ -80,7 +79,7 @@ class CheckCellView: NSTableCellView { formatLabel.isHidden = false } } - + func updateProgressIndicator(progress: Double) { if status != .downloading { addButton.isHidden = true @@ -91,31 +90,31 @@ class CheckCellView: NSTableCellView { progressIndicator.doubleValue = Double(progress) } - + // Add video handling - func setVideo(video:AerialVideo) { + 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 @@ -132,9 +131,8 @@ class CheckCellView: NSTableCellView { @IBAction func addClick(_ button: NSButton?) { queueVideo() } - -} +} class VerticallyAlignedTextFieldCell: NSTextFieldCell { override func drawingRect(forBounds rect: NSRect) -> NSRect { diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..5dee94cd --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +.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 + +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 diff --git a/Tests/PreferencesTests.swift b/Tests/PreferencesTests.swift index 42c2f763..b2a83867 100644 --- a/Tests/PreferencesTests.swift +++ b/Tests/PreferencesTests.swift @@ -10,19 +10,19 @@ 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") }