From b6c1168ac702776417d02f6ff440b282037de247 Mon Sep 17 00:00:00 2001 From: Guillaume Louel Date: Thu, 13 Feb 2020 18:20:58 +0100 Subject: [PATCH] - Cleanup UI by making it more macOS like - Add countdown Info type - Fix WIP on Updates info that would only appear on first video --- Aerial.xcodeproj/project.pbxproj | 26 +- .../Controllers/PWC Tabs/PWC+Advanced.swift | 6 + .../Controllers/PWC Tabs/PWC+Info.swift | 15 +- .../PreferencesWindowController.swift | 3 + Aerial/Source/Models/AutoUpdates.swift | 73 ++- Aerial/Source/Models/Prefs/PrefsInfo.swift | 93 +++- Aerial/Source/Views/AerialView.swift | 12 +- .../Source/Views/Layers/AnimationLayer.swift | 2 +- .../Source/Views/Layers/CountdownLayer.swift | 142 ++++++ Aerial/Source/Views/Layers/LayerManager.swift | 4 + Aerial/Source/Views/Layers/UpdatesLayer.swift | 71 +++ .../Views/PrefPanel/InfoCommonView.swift | 10 + .../Views/PrefPanel/InfoCountdownView.swift | 67 +++ Resources/PreferencesWindow.xib | 471 +++++++++++------- 14 files changed, 797 insertions(+), 198 deletions(-) create mode 100644 Aerial/Source/Views/Layers/CountdownLayer.swift create mode 100644 Aerial/Source/Views/Layers/UpdatesLayer.swift create mode 100644 Aerial/Source/Views/PrefPanel/InfoCountdownView.swift diff --git a/Aerial.xcodeproj/project.pbxproj b/Aerial.xcodeproj/project.pbxproj index ad458193..a13a7aa4 100644 --- a/Aerial.xcodeproj/project.pbxproj +++ b/Aerial.xcodeproj/project.pbxproj @@ -83,6 +83,12 @@ 036A34B5227309FB00A49135 /* zh_CN.json in Resources */ = {isa = PBXBuildFile; fileRef = 036A34B4227309FB00A49135 /* zh_CN.json */; }; 036A34B622730A0700A49135 /* zh_CN.json in Resources */ = {isa = PBXBuildFile; fileRef = 036A34B4227309FB00A49135 /* zh_CN.json */; }; 036A34B722730A0700A49135 /* zh_CN.json in Resources */ = {isa = PBXBuildFile; fileRef = 036A34B4227309FB00A49135 /* zh_CN.json */; }; + 036A57D523F30DD00009DC02 /* UpdatesLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A57D423F30DD00009DC02 /* UpdatesLayer.swift */; }; + 036A57D623F30F490009DC02 /* UpdatesLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A57D423F30DD00009DC02 /* UpdatesLayer.swift */; }; + 036A57D823F470940009DC02 /* InfoCountdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A57D723F470940009DC02 /* InfoCountdownView.swift */; }; + 036A57D923F4747D0009DC02 /* InfoCountdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A57D723F470940009DC02 /* InfoCountdownView.swift */; }; + 036A57DB23F5820A0009DC02 /* CountdownLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A57DA23F5820A0009DC02 /* CountdownLayer.swift */; }; + 036A57DC23F5828E0009DC02 /* CountdownLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A57DA23F5820A0009DC02 /* CountdownLayer.swift */; }; 03893CB3217749F0008E7125 /* ErrorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03893CB2217749F0008E7125 /* ErrorLog.swift */; }; 03893CB4217753AC008E7125 /* ErrorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03893CB2217749F0008E7125 /* ErrorLog.swift */; }; 038C584723A9304800224630 /* InfoContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038C584623A9304800224630 /* InfoContainerView.swift */; }; @@ -246,6 +252,9 @@ 0361B9AA23D73D4500B6252D /* PrefsTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsTime.swift; sourceTree = ""; }; 0369985C2196103300E359D3 /* missingvideos.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = missingvideos.json; sourceTree = ""; }; 036A34B4227309FB00A49135 /* zh_CN.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = zh_CN.json; sourceTree = ""; }; + 036A57D423F30DD00009DC02 /* UpdatesLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatesLayer.swift; sourceTree = ""; }; + 036A57D723F470940009DC02 /* InfoCountdownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoCountdownView.swift; sourceTree = ""; }; + 036A57DA23F5820A0009DC02 /* CountdownLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownLayer.swift; sourceTree = ""; }; 03893CB2217749F0008E7125 /* ErrorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLog.swift; sourceTree = ""; }; 038C584623A9304800224630 /* InfoContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoContainerView.swift; sourceTree = ""; }; 038C584923A9394000224630 /* InfoCommonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoCommonView.swift; sourceTree = ""; }; @@ -368,6 +377,8 @@ 03BF51BE23A274CA008AD373 /* MessageLayer.swift */, 03BF51C123A2978B008AD373 /* ClockLayer.swift */, 038D2EE023B6523800CD91F7 /* BatteryLayer.swift */, + 036A57D423F30DD00009DC02 /* UpdatesLayer.swift */, + 036A57DA23F5820A0009DC02 /* CountdownLayer.swift */, ); path = Layers; sourceTree = ""; @@ -402,6 +413,7 @@ 03A596D223AA750F0097EA66 /* InfoMessageView.swift */, 03A596D423AA752F0097EA66 /* InfoClockView.swift */, 038D2EE323B6565900CD91F7 /* InfoBatteryView.swift */, + 036A57D723F470940009DC02 /* InfoCountdownView.swift */, ); path = PrefPanel; sourceTree = ""; @@ -1045,6 +1057,7 @@ 038D2EE523B6565900CD91F7 /* InfoBatteryView.swift in Sources */, 030D9B7C21551A8D00961E95 /* AerialPlayerItem.swift in Sources */, 03608A2D22A56465008F08A2 /* HardwareDetection.swift in Sources */, + 036A57D923F4747D0009DC02 /* InfoCountdownView.swift in Sources */, FAC36F5E1BE1756D007F2A20 /* CheckCellView.swift in Sources */, FAC36F5C1BE1756D007F2A20 /* AerialView.swift in Sources */, FAC36F681BE1778C007F2A20 /* ManifestLoader.swift in Sources */, @@ -1068,6 +1081,7 @@ 03608A1B22A55B9A008F08A2 /* PWC+Time.swift in Sources */, FA6F81DD1D939455007975FE /* Preferences.swift in Sources */, FAF450221BE2B45D00C1F98A /* VideoLoader.swift in Sources */, + 036A57DC23F5828E0009DC02 /* CountdownLayer.swift in Sources */, 0313F9EA2294338300B074BB /* CustomVideoController.swift in Sources */, FAC36F5A1BE1756D007F2A20 /* AerialVideo.swift in Sources */, 038D2EBD23AB91C300CD91F7 /* InfoLocationView.swift in Sources */, @@ -1079,6 +1093,7 @@ 038C584823A9308C00224630 /* InfoContainerView.swift in Sources */, 03BF51BA23A24B40008AD373 /* AnimationLayer.swift in Sources */, F008DAFE23AADCFB00739DE1 /* Brightness.swift in Sources */, + 036A57D623F30F490009DC02 /* UpdatesLayer.swift in Sources */, 03A596D623AA752F0097EA66 /* InfoClockView.swift in Sources */, FAF450251BE2D2FD00C1F98A /* VideoCache.swift in Sources */, ); @@ -1140,6 +1155,8 @@ 03AD45FF22981B0C00261325 /* CustomVideoFolders+helpers.swift in Sources */, FAF450211BE2B45D00C1F98A /* VideoLoader.swift in Sources */, 03E8731321675FE0002B469B /* TimeManagement.swift in Sources */, + 036A57D523F30DD00009DC02 /* UpdatesLayer.swift in Sources */, + 036A57DB23F5820A0009DC02 /* CountdownLayer.swift in Sources */, 03D1E78722842FB300D10CF7 /* DisplayView.swift in Sources */, 03A596D923AB8F000097EA66 /* InfoLocationView.swift in Sources */, FAB22A7E1BE17D7D0065C0F5 /* AssetLoaderDelegate.swift in Sources */, @@ -1155,6 +1172,7 @@ FA36BD3F1BE57F8E00D5E03B /* VideoDownload.swift in Sources */, 03E8730C2165013C002B469B /* DownloadManager.swift in Sources */, 03608A2622A55C03008F08A2 /* PWC+Advanced.swift in Sources */, + 036A57D823F470940009DC02 /* InfoCountdownView.swift in Sources */, 03A596D523AA752F0097EA66 /* InfoClockView.swift in Sources */, 0313F9E822942B4500B074BB /* CustomVideoController.swift in Sources */, ); @@ -1385,7 +1403,7 @@ CODE_SIGN_IDENTITY = "Developer ID Application: Guillaume Louel (3L54M5L5KK)"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1.7.1; + CURRENT_PROJECT_VERSION = 1.7.2beta1; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 3L54M5L5KK; ENABLE_HARDENED_RUNTIME = YES; @@ -1393,7 +1411,7 @@ INSTALL_PATH = "$(HOME)/Library/Screen Savers"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.9; - MARKETING_VERSION = 1.7.1; + MARKETING_VERSION = 1.7.2beta1; PRODUCT_BUNDLE_IDENTIFIER = com.johncoates.Aerial; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1414,7 +1432,7 @@ CODE_SIGN_IDENTITY = "Developer ID Application: Guillaume Louel (3L54M5L5KK)"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1.7.1; + CURRENT_PROJECT_VERSION = 1.7.2beta1; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 3L54M5L5KK; ENABLE_HARDENED_RUNTIME = YES; @@ -1422,7 +1440,7 @@ INSTALL_PATH = "$(HOME)/Library/Screen Savers"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.9; - MARKETING_VERSION = 1.7.1; + MARKETING_VERSION = 1.7.2beta1; OTHER_CODE_SIGN_FLAGS = "--timestamp"; PRODUCT_BUNDLE_IDENTIFIER = com.johncoates.Aerial; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Aerial/Source/Controllers/PWC Tabs/PWC+Advanced.swift b/Aerial/Source/Controllers/PWC Tabs/PWC+Advanced.swift index ab9b4d5f..db47dca2 100644 --- a/Aerial/Source/Controllers/PWC Tabs/PWC+Advanced.swift +++ b/Aerial/Source/Controllers/PWC Tabs/PWC+Advanced.swift @@ -42,8 +42,14 @@ extension PreferencesWindowController { secondaryMarginVerticalTextfield.stringValue = String(preferences.marginY!) fadeInOutTextModePopup.selectItem(at: preferences.fadeModeText!) + + shadowRadiusFormatter.allowsFloats = false + shadowRadiusTextField.stringValue = String(PrefsInfo.shadowRadius) } // MARK: - Advanced panel + @IBAction func shadowRadiusChange(_ sender: NSTextField) { + PrefsInfo.shadowRadius = Int(sender.intValue) + } @IBAction func logButtonClick(_ sender: NSButton) { logTableView.reloadData() diff --git a/Aerial/Source/Controllers/PWC Tabs/PWC+Info.swift b/Aerial/Source/Controllers/PWC Tabs/PWC+Info.swift index 94787f09..284ce572 100644 --- a/Aerial/Source/Controllers/PWC Tabs/PWC+Info.swift +++ b/Aerial/Source/Controllers/PWC Tabs/PWC+Info.swift @@ -17,6 +17,14 @@ extension PreferencesWindowController { PrefsInfo.layers.append(.battery) } + if !PrefsInfo.layers.contains(.updates) { + PrefsInfo.layers.append(.updates) + } + + if !PrefsInfo.layers.contains(.countdown) { + PrefsInfo.layers.append(.countdown) + } + infoSource = InfoTableSource() infoSource?.setController(self) infoTableView.dataSource = infoSource @@ -50,7 +58,12 @@ extension PreferencesWindowController { infoContainerView.addSubview(infoBatteryView) infoBatteryView.frame.origin.y = infoCommonView.frame.height infoBatteryView.setStates() - + case .updates: + break + case .countdown: + infoContainerView.addSubview(infoCountdownView) + infoCountdownView.frame.origin.y = infoCommonView.frame.height + infoCountdownView.setStates() } } diff --git a/Aerial/Source/Controllers/PreferencesWindowController.swift b/Aerial/Source/Controllers/PreferencesWindowController.swift index 12475a52..620387c9 100644 --- a/Aerial/Source/Controllers/PreferencesWindowController.swift +++ b/Aerial/Source/Controllers/PreferencesWindowController.swift @@ -91,6 +91,7 @@ final class PreferencesWindowController: NSWindowController, NSOutlineViewDataSo @IBOutlet var infoMessageView: InfoMessageView! @IBOutlet var infoBatteryView: InfoBatteryView! + @IBOutlet var infoCountdownView: InfoCountdownView! // Text tab @IBOutlet weak var fadeInOutTextModePopup: NSPopUpButton! @@ -176,6 +177,8 @@ final class PreferencesWindowController: NSWindowController, NSOutlineViewDataSo @IBOutlet weak var showLogBottomClick: NSButton! @IBOutlet weak var logToDiskCheckbox: NSButton! + @IBOutlet var shadowRadiusTextField: NSTextField! + @IBOutlet var shadowRadiusFormatter: NumberFormatter! @IBOutlet var videoVersionsLabel: NSTextField! @IBOutlet var moveOldVideosButton: NSButton! @IBOutlet var trashOldVideosButton: NSButton! diff --git a/Aerial/Source/Models/AutoUpdates.swift b/Aerial/Source/Models/AutoUpdates.swift index 4eaadca8..bd4c3c49 100644 --- a/Aerial/Source/Models/AutoUpdates.swift +++ b/Aerial/Source/Models/AutoUpdates.swift @@ -9,7 +9,13 @@ import Foundation import Sparkle -struct AutoUpdates { +class AutoUpdates: NSObject, SUUpdaterDelegate { + static let sharedInstance = AutoUpdates() + + var didProbeForUpdate = false + private var updateAvailable = false + private var updateVersion: String = "" + // This is what we use to look for updates while the screensaver is running // This code is not active in Catalina+ func doForcedUpdate() { @@ -26,17 +32,18 @@ struct AutoUpdates { // Make sure we can create SUUpdater if let suu = suup { + // We manually ensure the correct amount of time passed since last check + var distance = -86400 // 1 day + // We may need to change the feed for betas if preferences.allowBetas { suu.feedURL = URL(string: "https://raw.githubusercontent.com/JohnCoates/Aerial/master/beta-appcast.xml") - } - // We manually ensure the correct amount of time passed since last check - var distance = -86400 // 1 day - if preferences.betaCheckFrequency == 0 { - distance = -3600 // 1 hour - } else if preferences.betaCheckFrequency == 1 { - distance = -43200 // 12 hours + if preferences.betaCheckFrequency == 0 { + distance = -3600 // 1 hour + } else if preferences.betaCheckFrequency == 1 { + distance = -43200 // 12 hours + } } // If we never went into System Preferences, we may not have a lastUpdateCheckDate @@ -52,4 +59,54 @@ struct AutoUpdates { } } + // Probing update check + func doProbingCheck() { + let preferences = Preferences.sharedInstance + + debugLog("Probing availability of an update") + + let suup = SUUpdater.init(for: Bundle(for: AerialView.self)) + + // Make sure we can create SUUpdater + if let suu = suup { + suu.delegate = self + + // We may need to change the feed for betas + if preferences.allowBetas { + suu.feedURL = URL(string: "https://raw.githubusercontent.com/JohnCoates/Aerial/master/beta-appcast.xml") + } + + // Then force check/install udpates + debugLog("Checking for update (probe mode)") + suu.checkForUpdateInformation() + } + } + + func getUpdateString() -> String { + if updateAvailable { + return "A new version of Aerial (\(updateVersion)) is available" + } else { + return "" + } + } + + func isAnUpdateAvailable() -> Bool { + return updateAvailable + } + + func updaterDidNotFindUpdate(_ updater: SUUpdater) { + debugLog("//////// No update is available !") + didProbeForUpdate = true + } + + func updater(_ updater: SUUpdater, didFindValidUpdate item: SUAppcastItem) { + debugLog("//////// An update is available !") + didProbeForUpdate = true + updateAvailable = true + + // Grab the new version number + if let versionString = item.displayVersionString { + self.updateVersion = versionString + } + } } diff --git a/Aerial/Source/Models/Prefs/PrefsInfo.swift b/Aerial/Source/Models/Prefs/PrefsInfo.swift index 27382ecc..1d81314e 100644 --- a/Aerial/Source/Models/Prefs/PrefsInfo.swift +++ b/Aerial/Source/Models/Prefs/PrefsInfo.swift @@ -17,6 +17,7 @@ protocol CommonInfo { var displays: InfoDisplays { get set } } +// Helper Enums for the common infos enum InfoCorner: Int, Codable { case topLeft, topCenter, topRight, bottomLeft, bottomCenter, bottomRight, screenCenter, random } @@ -25,10 +26,6 @@ enum InfoDisplays: Int, Codable { case allDisplays, mainOnly, secondaryOnly } -enum InfoType: String, Codable { - case location, message, clock, battery -} - enum InfoTime: Int, Codable { case always, tenSeconds } @@ -37,6 +34,15 @@ enum InfoIconText: Int, Codable { case textOnly, iconAndText, iconOnly } +enum InfoCountdownMode: Int, Codable { + case preciseDate, timeOfDay +} + +// The various info types available +enum InfoType: String, Codable { + case location, message, clock, battery, updates, countdown +} + struct PrefsInfo { struct Location: CommonInfo, Codable { @@ -75,8 +81,29 @@ struct PrefsInfo { var mode: InfoIconText } + struct Updates: CommonInfo, Codable { + var isEnabled: Bool + var fontName: String + var fontSize: Double + var corner: InfoCorner + var displays: InfoDisplays + } + + struct Countdown: CommonInfo, Codable { + var isEnabled: Bool + var fontName: String + var fontSize: Double + var corner: InfoCorner + var displays: InfoDisplays + var mode: InfoCountdownMode + var targetDate: Date + var enforceInterval: Bool + var triggerDate: Date + var showSeconds: Bool + } + // Our array of Info layers. User can reorder the array, and we may periodically add new Info types - @Storage(key: "layers", defaultValue: [ .message, .clock, .location, .battery]) + @Storage(key: "layers", defaultValue: [ .message, .clock, .location, .battery, .updates, .countdown]) static var layers: [InfoType] // Location information @@ -115,6 +142,31 @@ struct PrefsInfo { mode: .textOnly)) static var battery: Battery + // Updates + @Storage(key: "LayerUpdates", defaultValue: Updates(isEnabled: true, + fontName: "Helvetica Neue Medium", + fontSize: 20, + corner: .topRight, + displays: .allDisplays)) + static var updates: Updates + + // Countdown + @Storage(key: "LayerCountdown", defaultValue: Countdown(isEnabled: false, + fontName: "Helvetica Neue Medium", + fontSize: 100, + corner: .screenCenter, + displays: .allDisplays, + mode: .timeOfDay, + targetDate: Date(), + enforceInterval: false, + triggerDate: Date(), + showSeconds: true)) + static var countdown: Countdown + + // Shadow radius (common) + @SimpleStorage(key: "shadowRadius", defaultValue: 20) + static var shadowRadius: Int + // Helper to quickly access a given struct (read-only as we return a copy of the struct) static func ofType(_ type: InfoType) -> CommonInfo { switch type { @@ -126,6 +178,10 @@ struct PrefsInfo { return clock case .battery: return battery + case .updates: + return updates + case .countdown: + return countdown } } @@ -140,6 +196,10 @@ struct PrefsInfo { clock.isEnabled = value case .battery: battery.isEnabled = value + case .updates: + updates.isEnabled = value + case .countdown: + countdown.isEnabled = value } } @@ -153,6 +213,10 @@ struct PrefsInfo { clock.fontName = name case .battery: battery.fontName = name + case .updates: + updates.fontName = name + case .countdown: + countdown.fontName = name } } @@ -166,6 +230,10 @@ struct PrefsInfo { clock.fontSize = size case .battery: battery.fontSize = size + case .updates: + updates.fontSize = size + case .countdown: + countdown.fontSize = size } } @@ -179,6 +247,10 @@ struct PrefsInfo { clock.corner = corner case .battery: battery.corner = corner + case .updates: + updates.corner = corner + case .countdown: + countdown.corner = corner } } @@ -192,6 +264,10 @@ struct PrefsInfo { clock.displays = mode case .battery: battery.displays = mode + case .updates: + updates.displays = mode + case .countdown: + countdown.displays = mode } } } @@ -232,6 +308,9 @@ struct PrefsInfo { // Set value to UserDefaults userDefaults.set(data, forKey: key) + // We force the sync so the settings are automatically saved + // This is needed as the System Preferences instance of Aerial + // is a separate instance from the screensaver ones userDefaults.synchronize() } else { errorLog("UserDefaults set failed for \(key)") @@ -240,8 +319,8 @@ struct PrefsInfo { } } -@propertyWrapper -struct SimpleStorage { +// This retrieves store "simple" types that are natively storable on plists +@propertyWrapper struct SimpleStorage { private let key: String private let defaultValue: T private let module = "com.JohnCoates.Aerial" diff --git a/Aerial/Source/Views/AerialView.swift b/Aerial/Source/Views/AerialView.swift index 7c0a32ff..ca3bae04 100644 --- a/Aerial/Source/Views/AerialView.swift +++ b/Aerial/Source/Views/AerialView.swift @@ -181,12 +181,17 @@ final class AerialView: ScreenSaverView, CAAnimationDelegate { let preferences = Preferences.sharedInstance + let au = AutoUpdates.sharedInstance // Run Sparkle updater if enabled - if !isPreview && preferences.updateWhileSaverMode { - let au = AutoUpdates() - au.doForcedUpdate() + if !isPreview { + if preferences.updateWhileSaverMode { + au.doForcedUpdate() + } } + // Run the probing check + au.doProbingCheck() + // Check early if we need to enable power saver mode, // black screen with minimal brightness // swiftlint:disable:next line_length @@ -499,6 +504,7 @@ final class AerialView: ScreenSaverView, CAAnimationDelegate { } override var acceptsFirstResponder: Bool { + // swiftlint:disable:next implicit_getter get { return true } diff --git a/Aerial/Source/Views/Layers/AnimationLayer.swift b/Aerial/Source/Views/Layers/AnimationLayer.swift index 61dd1ff2..529e127d 100644 --- a/Aerial/Source/Views/Layers/AnimationLayer.swift +++ b/Aerial/Source/Views/Layers/AnimationLayer.swift @@ -47,7 +47,7 @@ class AnimationLayer: CATextLayer { self.frame = withLayer.bounds // Starts hidden, with a bit of shadow for text separation self.opacity = 0 - self.shadowRadius = 2 + self.shadowRadius = CGFloat(PrefsInfo.shadowRadius) self.shadowOpacity = 1.0 self.shadowColor = CGColor.black } diff --git a/Aerial/Source/Views/Layers/CountdownLayer.swift b/Aerial/Source/Views/Layers/CountdownLayer.swift new file mode 100644 index 00000000..56f0b2df --- /dev/null +++ b/Aerial/Source/Views/Layers/CountdownLayer.swift @@ -0,0 +1,142 @@ +// +// CountdownLayer.swift +// Aerial +// +// Created by Guillaume Louel on 13/02/2020. +// Copyright © 2020 Guillaume Louel. All rights reserved. +// + +import Foundation +import AVKit + +class CountdownLayer: AnimationLayer { + var config: PrefsInfo.Countdown? + var wasSetup = false + var countdownTimer: Timer? + + override init(layer: Any) { + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // Our inits + override init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager) { + super.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) + + // Always on layers should start with full opacity + self.opacity = 1 + } + + convenience init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager, config: PrefsInfo.Countdown) { + self.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) + self.config = config + + // Set our layer's font & corner now + (self.font, self.fontSize) = getFont(name: config.fontName, + size: config.fontSize) + self.corner = config.corner + } + + // Called at each new video, we only setup once though ! + override func setupForVideo(video: AerialVideo, player: AVPlayer) { + // Only run this once + if !wasSetup { + wasSetup = true + + if shouldCountdown() { + if #available(OSX 10.12, *) { + countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (_) in + self.update(string: self.getTimeString()) + }) + } + + update(string: getTimeString()) + let fadeAnimation = self.createFadeInAnimation() + add(fadeAnimation, forKey: "textfade") + } + } + } + + // Transform a date by setting it to today (or tommorrow) + func todayizeDate(_ target: Date, strict: Bool) -> Date { + let now = Date() + + let calendar = Calendar.current + var targetComponent = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: target) + let nowComponent = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: now) + + targetComponent.year = nowComponent.year + targetComponent.month = nowComponent.month + targetComponent.day = nowComponent.day + + let candidate = Calendar.current.date(from: targetComponent) ?? target + + if strict { + return candidate + } else { + // In non strict mode, if the hour is passed already + // we return tomorrow + if candidate > now { + return candidate + } else { + return candidate.tomorrow ?? candidate + } + } + } + + func shouldCountdown() -> Bool { + let now = Date() + var target = PrefsInfo.countdown.targetDate + var trigger = PrefsInfo.countdown.triggerDate + + // We ignore the day, in timeOfDay mode by normalizing it to today + if config!.mode == .timeOfDay { + target = todayizeDate(target, strict: false) + trigger = todayizeDate(trigger, strict: true) + } + + // We only start the countdown if we're later than the trigger + if config!.enforceInterval { + if trigger > now { + return false + } + } + + // Are we still before the countdown date or not ? + if now < target { + return true + } + + return false + } + + func getTimeString() -> String { + if #available(OSX 10.12, *) { + let dateComponentsFormatter = DateComponentsFormatter() + dateComponentsFormatter.allowedUnits = [.year, .month, .day, .hour, .minute, .second] + dateComponentsFormatter.maximumUnitCount = 3 + dateComponentsFormatter.unitsStyle = .full + + var target = PrefsInfo.countdown.targetDate + + // We ignore the day, in timeOfDay mode by normalizing it to today + if config!.mode == .timeOfDay { + target = todayizeDate(target, strict: false) + } + + return dateComponentsFormatter.string(from: Date(), to: target) ?? "" + } else { + // Fallback on earlier versions + return "" + } + } +} + +extension Date { + var tomorrow: Date? { + return Calendar.current.date(byAdding: .day, value: 1, to: self) + } +} diff --git a/Aerial/Source/Views/Layers/LayerManager.swift b/Aerial/Source/Views/Layers/LayerManager.swift index 7194f1d7..f5fa217a 100644 --- a/Aerial/Source/Views/Layers/LayerManager.swift +++ b/Aerial/Source/Views/Layers/LayerManager.swift @@ -64,6 +64,10 @@ class LayerManager { newLayer = ClockLayer(withLayer: layer, isPreview: isPreview, offsets: offsets, manager: self, config: PrefsInfo.clock) case .battery: newLayer = BatteryLayer(withLayer: layer, isPreview: isPreview, offsets: offsets, manager: self, config: PrefsInfo.battery) + case .updates: + newLayer = UpdatesLayer(withLayer: layer, isPreview: isPreview, offsets: offsets, manager: self, config: PrefsInfo.updates) + case .countdown: + newLayer = CountdownLayer(withLayer: layer, isPreview: isPreview, offsets: offsets, manager: self, config: PrefsInfo.countdown) } } diff --git a/Aerial/Source/Views/Layers/UpdatesLayer.swift b/Aerial/Source/Views/Layers/UpdatesLayer.swift new file mode 100644 index 00000000..6b8af29e --- /dev/null +++ b/Aerial/Source/Views/Layers/UpdatesLayer.swift @@ -0,0 +1,71 @@ +// +// UpdatesLayer.swift +// Aerial +// +// Created by Guillaume Louel on 11/02/2020. +// Copyright © 2020 Guillaume Louel. All rights reserved. +// + +import Foundation +import AVKit + +class UpdatesLayer: AnimationLayer { + var config: PrefsInfo.Updates? + var wasSetup = false + var updateTimer: Timer? + + override init(layer: Any) { + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // Our inits + override init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager) { + super.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) + + // We start with a full opacity + self.opacity = 1 + } + + convenience init(withLayer: CALayer, isPreview: Bool, offsets: LayerOffsets, manager: LayerManager, config: PrefsInfo.Updates) { + self.init(withLayer: withLayer, isPreview: isPreview, offsets: offsets, manager: manager) + self.config = config + + // Set our layer's font & corner now + (self.font, self.fontSize) = getFont(name: config.fontName, + size: config.fontSize) + self.corner = config.corner + } + + override func setupForVideo(video: AerialVideo, player: AVPlayer) { + print("sfv") + if !wasSetup { + setupUpdateLayer() + } + } + + // Setup the layer, but give some time for the probe to complete + func setupUpdateLayer() { + let autoupd = AutoUpdates.sharedInstance + + if autoupd.didProbeForUpdate { + wasSetup = true + + update(string: autoupd.getUpdateString()) + + let fadeAnimation = self.createFadeInAnimation() + add(fadeAnimation, forKey: "textfade") + } else { + // Ok, let's try again in 10 seconds + if #available(OSX 10.12, *) { + updateTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false, block: { (_) in + self.setupUpdateLayer() + }) + } + } + + } +} diff --git a/Aerial/Source/Views/PrefPanel/InfoCommonView.swift b/Aerial/Source/Views/PrefPanel/InfoCommonView.swift index 4b343a9d..2c44df5c 100644 --- a/Aerial/Source/Views/PrefPanel/InfoCommonView.swift +++ b/Aerial/Source/Views/PrefPanel/InfoCommonView.swift @@ -57,6 +57,12 @@ class InfoCommonView: NSView { case .battery: descriptionLabel.stringValue = "Show current battery status." posRandom.isHidden = true + case .updates: + descriptionLabel.stringValue = "Display a message if a new version is available" + posRandom.isHidden = true + case .countdown: + descriptionLabel.stringValue = "Display a countdown to a time/date" + posRandom.isHidden = true } } @@ -166,6 +172,10 @@ class InfoCommonView: NSView { PrefsInfo.clock.fontSize = 50 case .battery: PrefsInfo.battery.fontSize = 20 + case .updates: + PrefsInfo.updates.fontSize = 20 + case .countdown: + PrefsInfo.countdown.fontSize = 100 } fontLabel.stringValue = PrefsInfo.ofType(forType).fontName + ", \(PrefsInfo.ofType(forType).fontSize) pt" diff --git a/Aerial/Source/Views/PrefPanel/InfoCountdownView.swift b/Aerial/Source/Views/PrefPanel/InfoCountdownView.swift new file mode 100644 index 00000000..06c2e627 --- /dev/null +++ b/Aerial/Source/Views/PrefPanel/InfoCountdownView.swift @@ -0,0 +1,67 @@ +// +// InfoCountdownView.swift +// Aerial +// +// Created by Guillaume Louel on 12/02/2020. +// Copyright © 2020 Guillaume Louel. All rights reserved. +// + +import Cocoa + +class InfoCountdownView: NSView { + + @IBOutlet var timeModePopup: NSPopUpButton! + + @IBOutlet var withSecondsCheckbox: NSButton! + @IBOutlet var targetTimeDatePicker: NSDatePicker! + @IBOutlet var limitToIntervalCheckbox: NSButton! + @IBOutlet var limitIntervalDatePicker: NSDatePicker! + + // Init(ish) + func setStates() { + timeModePopup.selectItem(at: PrefsInfo.countdown.mode.rawValue) + withSecondsCheckbox.state = PrefsInfo.countdown.showSeconds ? .on : .off + + targetTimeDatePicker.dateValue = PrefsInfo.countdown.targetDate + + updatePickerFormat() + + limitToIntervalCheckbox.state = PrefsInfo.countdown.enforceInterval ? .on : .off + limitIntervalDatePicker.dateValue = PrefsInfo.countdown.triggerDate + } + + func updatePickerFormat() { + switch PrefsInfo.countdown.mode { + case .preciseDate: + targetTimeDatePicker.datePickerElements = [.yearMonthDay, .hourMinuteSecond] + limitIntervalDatePicker.datePickerElements = [.yearMonthDay, .hourMinuteSecond] + case .timeOfDay: + targetTimeDatePicker.datePickerElements = [.hourMinuteSecond] + limitIntervalDatePicker.datePickerElements = [.hourMinuteSecond] + // TODO hide day + } + } + // UI Actions + @IBAction func timeModePopupChange(_ sender: NSPopUpButton) { + PrefsInfo.countdown.mode = InfoCountdownMode(rawValue: sender.indexOfSelectedItem)! + updatePickerFormat() + } + + @IBAction func withSecondsCheckboxClick(_ sender: NSButton) { + let onState = sender.state == .on + PrefsInfo.countdown.showSeconds = onState + } + + @IBAction func targetTimeDatePickerChange(_ sender: NSDatePicker) { + PrefsInfo.countdown.targetDate = sender.dateValue + } + + @IBAction func limitToIntervalClick(_ sender: NSButton) { + let onState = sender.state == .on + PrefsInfo.countdown.enforceInterval = onState + } + + @IBAction func limitIntervalDatePickerChange(_ sender: NSDatePicker) { + PrefsInfo.countdown.triggerDate = sender.dateValue + } +} diff --git a/Resources/PreferencesWindow.xib b/Resources/PreferencesWindow.xib index e3e34b63..9a28344f 100644 --- a/Resources/PreferencesWindow.xib +++ b/Resources/PreferencesWindow.xib @@ -1,9 +1,9 @@ - + - - + + @@ -69,6 +69,7 @@ + @@ -123,6 +124,8 @@ + + @@ -164,7 +167,7 @@ - + @@ -175,11 +178,11 @@ - + - + @@ -188,12 +191,11 @@ - - + @@ -207,7 +209,7 @@ - + @@ -230,7 +232,7 @@ - + @@ -249,13 +251,13 @@ - + - + @@ -264,7 +266,7 @@ - + @@ -274,7 +276,7 @@ - + @@ -327,7 +329,7 @@ - + @@ -351,9 +353,9 @@ - + - + @@ -386,7 +388,7 @@ is disabled - + @@ -395,7 +397,7 @@ is disabled - + @@ -405,20 +407,20 @@ is disabled - + - + - + - + - + - + - - - + + + @@ -463,16 +465,16 @@ is disabled - + - - - - - - - - - - + - + - - - + + + @@ -530,31 +523,40 @@ is disabled - + - + + + + + + + + + + - + - + - + @@ -576,30 +578,30 @@ is disabled - + - + - + - + - - + + - + - + @@ -608,16 +610,16 @@ is disabled - + - + - + @@ -642,15 +644,15 @@ is disabled - + - + - - + + @@ -658,19 +660,10 @@ is disabled - - - - - - - - - - + - + @@ -683,15 +676,6 @@ is disabled - - - - - - - - - @@ -706,7 +690,7 @@ is disabled - + @@ -715,9 +699,9 @@ is disabled - + - + @@ -737,7 +721,7 @@ is disabled - + @@ -767,25 +751,43 @@ is disabled + + + + + + + + + + + + + + + + + + - + - + - + - + @@ -794,7 +796,6 @@ is disabled - @@ -833,7 +834,6 @@ is disabled - @@ -885,7 +885,7 @@ is disabled - + @@ -1925,6 +1925,30 @@ Shift, but macOS 10.12.4 or above and a compatible Mac are required) + + + + + + + + + + + + + + + + + + + + + + + + @@ -1959,9 +1983,9 @@ Shift, but macOS 10.12.4 or above and a compatible Mac are required) - + @@ -1998,7 +2022,7 @@ Gw - + @@ -2106,11 +2130,11 @@ HDR videos are encoded using Dolby Vision to represent a Higher Dynamic range of - + - + @@ -2119,7 +2143,6 @@ HDR videos are encoded using Dolby Vision to represent a Higher Dynamic range of - @@ -2152,7 +2175,6 @@ HDR videos are encoded using Dolby Vision to represent a Higher Dynamic range of - @@ -2228,7 +2250,7 @@ HDR videos are encoded using Dolby Vision to represent a Higher Dynamic range of - + @@ -2293,7 +2315,7 @@ You can either enter those manually or try to use Location Services on your Mac - + @@ -2513,6 +2535,109 @@ You can still decide to disable Aerial when running on battery, or if your batte + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2542,7 +2667,7 @@ You can still decide to disable Aerial when running on battery, or if your batte - + - + - + - + - - + + - + @@ -2868,17 +2993,8 @@ After changing the folder, please close System Preferences for this to be taken - - - - - - - - - - + @@ -2909,12 +3025,12 @@ After changing the folder, please close System Preferences for this to be taken - + - + + + + + + + + + + - + - + @@ -3038,14 +3161,14 @@ After changing the folder, please close System Preferences for this to be taken - + - + @@ -3072,23 +3195,14 @@ After changing the folder, please close System Preferences for this to be taken - + - - - - - - - - - - + @@ -3104,27 +3218,27 @@ After changing the folder, please close System Preferences for this to be taken + + + + + + + + + - + - + - +