From 85fb2964fd11426265cd53f7609f08a0c99409c7 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Tue, 14 Jul 2020 17:28:46 +0200 Subject: [PATCH] - add option to select an application automatic checks for new version (at start/once per day/once per week/once per month/never) - adjust select box y position --- Modules/Sensors/settings.swift | 2 +- Stats.xcodeproj/project.pbxproj | 4 ++ Stats/AppDelegate.swift | 123 +++++--------------------------- Stats/Views/AppSettings.swift | 63 ++++++++++++---- Stats/Views/Settings.swift | 17 +++++ Stats/helpers.swift | 101 ++++++++++++++++++++++++++ StatsKit/extensions.swift | 21 ++++-- 7 files changed, 209 insertions(+), 122 deletions(-) create mode 100644 Stats/helpers.swift diff --git a/Modules/Sensors/settings.swift b/Modules/Sensors/settings.swift index 22ed3b41..438d9e9f 100644 --- a/Modules/Sensors/settings.swift +++ b/Modules/Sensors/settings.swift @@ -63,7 +63,7 @@ internal class Settings: NSView, Settings_v { let view: NSView = NSView(frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin, width: self.frame.width - (Constants.Settings.margin*2) - x, height: height)) self.addSubview(SelectTitleRow( - frame: NSRect(x: Constants.Settings.margin, y: height - rowHeight, width: self.frame.width - (Constants.Settings.margin*2), height: rowHeight), + frame: NSRect(x: Constants.Settings.margin, y: height - rowHeight, width: view.frame.width, height: rowHeight), title: "Update interval", action: #selector(changeUpdateInterval), items: self.listOfUpdateIntervals.map{ "\($0) sec" }, diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index 770b2a7c..115e821c 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -96,6 +96,7 @@ 9ABFF910248BEE7200C9041A /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ABFF90F248BEE7200C9041A /* readers.swift */; }; 9ABFF912248BF39500C9041A /* Battery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ABFF911248BF39500C9041A /* Battery.swift */; }; 9ABFF914248C30A800C9041A /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ABFF913248C30A800C9041A /* popup.swift */; }; + 9AD33AC624BCD3EE007E8820 /* helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD33AC524BCD3EE007E8820 /* helpers.swift */; }; 9AE29ADC249A50350071B02D /* Sensors.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AE29AD5249A50350071B02D /* Sensors.framework */; }; 9AE29ADD249A50350071B02D /* Sensors.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9AE29AD5249A50350071B02D /* Sensors.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9AE29AE1249A50640071B02D /* ModuleKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AABEADD243FB13500668CB0 /* ModuleKit.framework */; }; @@ -467,6 +468,7 @@ 9ABFF90F248BEE7200C9041A /* readers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = readers.swift; sourceTree = ""; }; 9ABFF911248BF39500C9041A /* Battery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Battery.swift; sourceTree = ""; }; 9ABFF913248C30A800C9041A /* popup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = popup.swift; sourceTree = ""; }; + 9AD33AC524BCD3EE007E8820 /* helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = helpers.swift; sourceTree = ""; }; 9AE29AD5249A50350071B02D /* Sensors.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Sensors.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9AE29AEC249A50960071B02D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = Modules/Sensors/Info.plist; sourceTree = SOURCE_ROOT; }; 9AE29AF1249A50CD0071B02D /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = main.swift; path = Modules/Sensors/main.swift; sourceTree = SOURCE_ROOT; }; @@ -634,6 +636,7 @@ 9A81C74A24499C4B00825D92 /* Views */, 9A5B1CB3229E72A7008B9D3C /* Supporting Files */, 9AABEB79243FD26200668CB0 /* AppDelegate.swift */, + 9AD33AC524BCD3EE007E8820 /* helpers.swift */, ); path = Stats; sourceTree = ""; @@ -1276,6 +1279,7 @@ 9A9EA9452476D34500E3B883 /* Update.swift in Sources */, 9A81C74E24499C7000825D92 /* Settings.swift in Sources */, 9A81C74D24499C7000825D92 /* AppSettings.swift in Sources */, + 9AD33AC624BCD3EE007E8820 /* helpers.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Stats/AppDelegate.swift b/Stats/AppDelegate.swift index a35528bb..d71ce692 100755 --- a/Stats/AppDelegate.swift +++ b/Stats/AppDelegate.swift @@ -37,14 +37,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele self.parseArguments() NSUserNotificationCenter.default.removeAllDeliveredNotifications() - NotificationCenter.default.addObserver(self, selector: #selector(toggleSettingsHandler), name: .toggleSettings, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(checkForNewVersion), name: .checkForUpdates, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(updateCron), name: .changeCronInterval, object: nil) modules.forEach{ $0.mount() } self.settingsWindow.setModules() - self.setVersion() + self.parseVersion() self.defaultValues() self.updateCron() os_log(.info, log: log, "Stats started in %.4f seconds", startingPoint.timeIntervalSinceNow * -1) @@ -52,7 +52,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele func userNotificationCenter(_ center: NSUserNotificationCenter, didActivate notification: NSUserNotification) { if let uri = notification.userInfo?["url"] as? String { - os_log(.error, log: log, "Downloading new version of app...") + os_log(.debug, log: log, "Downloading new version of app...") if let url = URL(string: uri) { updater.download(url) } @@ -65,7 +65,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele modules.forEach{ $0.terminate() } _ = smc.close() NotificationCenter.default.removeObserver(self) - self.updateActivity.invalidate() } func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { @@ -77,95 +76,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele return true } - @objc private func toggleSettingsHandler(_ notification: Notification) { - if !self.settingsWindow.isVisible { - self.settingsWindow.setIsVisible(true) - self.settingsWindow.makeKeyAndOrderFront(nil) - } - - if let name = notification.userInfo?["module"] as? String { - self.settingsWindow.openMenu(name) - } - } - - private func setVersion() { - let key = "version" - let currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String - - if !store.exist(key: key) { - store.reset() - os_log(.info, log: log, "Previous version not detected. Current version (%s) set", currentVersion) - } else { - let prevVersion = store.string(key: key, defaultValue: "") - if prevVersion == currentVersion { - return - } - - if IsNewestVersion(currentVersion: prevVersion, latestVersion: currentVersion) { - let notification = NSUserNotification() - notification.identifier = "updated-from-\(prevVersion)-to-\(currentVersion)" - notification.title = "Successfully updated" - notification.subtitle = "Stats was updated to the v\(currentVersion)" - notification.soundName = NSUserNotificationDefaultSoundName - notification.hasActionButton = false - - NSUserNotificationCenter.default.deliver(notification) - } - - os_log(.info, log: log, "Detected previous version %s. Current version (%s) set", prevVersion, currentVersion) - } - - store.set(key: key, value: currentVersion) - } - - private func parseArguments() { - let args = CommandLine.arguments - - if args.contains("--reset") { - os_log(.info, log: log, "Receive --reset argument. Reseting store (UserDefaults)...") - store.reset() - } - - if let disableIndex = args.firstIndex(of: "--disable") { - if args.indices.contains(disableIndex+1) { - let disableModules = args[disableIndex+1].split(separator: ",") - - disableModules.forEach { (moduleName: Substring) in - if let module = modules.first(where: { $0.config.name.lowercased() == moduleName.lowercased()}) { - module.unmount() - } - } - } - } - - if let mountIndex = args.firstIndex(of: "--mount-path") { - if args.indices.contains(mountIndex+1) { - let mountPath = args[mountIndex+1] - asyncShell("/usr/bin/hdiutil detach \(mountPath)") - asyncShell("/bin/rm -rf \(mountPath)") - } - } - - if let dmgIndex = args.firstIndex(of: "--dmg-path") { - if args.indices.contains(dmgIndex+1) { - asyncShell("/bin/rm -rf \(args[dmgIndex+1])") - } - } - } - - private func defaultValues() { - if !store.exist(key: "runAtLoginInitialized") { - store.set(key: "runAtLoginInitialized", value: true) - LaunchAtLogin.isEnabled = true - } - - if store.exist(key: "dockIcon") { - let dockIconStatus = store.bool(key: "dockIcon", defaultValue: false) ? NSApplication.ActivationPolicy.regular : NSApplication.ActivationPolicy.accessory - NSApp.setActivationPolicy(dockIconStatus) - } - } - - @objc private func checkForNewVersion(_ window: Bool = false) { + @objc internal func checkForNewVersion(_ window: Bool = false) { updater.check() { result, error in if error != nil { os_log(.error, log: log, "error updater.check(): %s", "\(error!.localizedDescription)") @@ -179,13 +90,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele DispatchQueue.main.async(execute: { if window { - os_log(.error, log: log, "open update window: %s", "\(version.latest)") + os_log(.debug, log: log, "open update window: %s", "\(version.latest)") self.updateWindow.open(version) return } if version.newest { - os_log(.error, log: log, "show update window because new version of app found: %s", "\(version.latest)") + os_log(.debug, log: log, "show update window because new version of app found: %s", "\(version.latest)") self.updateNotification.identifier = "new-version-\(version.latest)" self.updateNotification.title = "New version available" @@ -203,20 +114,24 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele } } - private func updateCron() { + @objc private func updateCron() { + self.updateActivity.invalidate() self.updateActivity.repeats = true - self.updateActivity.interval = 60 * 60 * 12 // once in 12 hour - if store.bool(key: "checkUpdatesOnLogin", defaultValue: true) { - self.checkForNewVersion(false) + guard let updateInterval = updateIntervals(rawValue: store.string(key: "update-interval", defaultValue: updateIntervals.atStart.rawValue)) else { + return + } + os_log(.debug, log: log, "Application update interval is '%s'", "\(updateInterval.rawValue)") + + switch updateInterval { + case .oncePerDay: self.updateActivity.interval = 60 * 60 * 24 + case .oncePerWeek: self.updateActivity.interval = 60 * 60 * 24 * 7 + case .oncePerMonth: self.updateActivity.interval = 60 * 60 * 24 * 30 + case .never, .atStart: return + default: return } self.updateActivity.schedule { (completion: @escaping NSBackgroundActivityScheduler.CompletionHandler) in - if !store.bool(key: "checkUpdatesOnLogin", defaultValue: true) { - completion(NSBackgroundActivityScheduler.Result.finished) - return - } - self.checkForNewVersion(false) completion(NSBackgroundActivityScheduler.Result.finished) } diff --git a/Stats/Views/AppSettings.swift b/Stats/Views/AppSettings.swift index e61ef926..5622a6b7 100644 --- a/Stats/Views/AppSettings.swift +++ b/Stats/Views/AppSettings.swift @@ -17,6 +17,12 @@ class ApplicationSettings: NSView { private let height: CGFloat = 480 private let deviceInfoHeight: CGFloat = 300 + private var updateIntervalValue: updateInterval { + get { + return store.string(key: "update-interval", defaultValue: updateIntervals.atStart.rawValue) + } + } + init() { super.init(frame: NSRect(x: 0, y: 0, width: width, height: height)) self.wantsLayer = true @@ -82,11 +88,12 @@ class ApplicationSettings: NSView { let rightPanel: NSView = NSView(frame: NSRect(x: self.width/2, y: 0, width: view.frame.width/2, height: view.frame.height)) - rightPanel.addSubview(makeSettingRow( + rightPanel.addSubview(makeSelectRow( frame: NSRect(x: rowHorizontalPadding*0.5, y: rowHeight*2, width: rightPanel.frame.width - (rowHorizontalPadding*1.5), height: rowHeight), title: "Check for updates", - action: #selector(self.toggleUpdates), - state: store.bool(key: "checkUpdatesOnLogin", defaultValue: true) + action: #selector(self.toggleUpdateInterval), + items: updateIntervals.allCases.map{ $0.rawValue }, + selected: self.updateIntervalValue )) rightPanel.addSubview(makeSettingRow( @@ -108,6 +115,42 @@ class ApplicationSettings: NSView { self.addSubview(view) } + func makeSelectRow(frame: NSRect, title: String, action: Selector, items: [String], selected: String) -> NSView { + let row: NSView = NSView(frame: frame) + + let rowTitle: NSTextField = LabelField(frame: NSRect(x: 0, y: (row.frame.height - 32)/2, width: row.frame.width - 52, height: 32), title) + rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light) + rowTitle.textColor = .secondaryLabelColor + + let select: NSPopUpButton = NSPopUpButton(frame: NSRect(x: row.frame.width - 50, y: (row.frame.height-28)/2, width: 50, height: 28)) + select.target = self + select.action = action + + let menu = NSMenu() + items.forEach { (color: String) in + if color.contains("separator") { + menu.addItem(NSMenuItem.separator()) + } else { + let interfaceMenu = NSMenuItem(title: color, action: nil, keyEquivalent: "") + menu.addItem(interfaceMenu) + if selected == color { + interfaceMenu.state = .on + } + } + } + + select.menu = menu + select.sizeToFit() + + rowTitle.setFrameSize(NSSize(width: row.frame.width - select.frame.width, height: rowTitle.frame.height)) + select.setFrameOrigin(NSPoint(x: row.frame.width - select.frame.width, y: select.frame.origin.y)) + + row.addSubview(select) + row.addSubview(rowTitle) + + return row + } + private func makeInfoRow(frame: NSRect, title: String, value: String) -> NSView { let row: NSView = NSView(frame: frame) let titleWidth = title.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light)) + 10 @@ -235,16 +278,10 @@ class ApplicationSettings: NSView { NotificationCenter.default.post(name: .checkForUpdates, object: nil, userInfo: nil) } - @objc func toggleUpdates(_ sender: NSObject) { - var state: NSControl.StateValue? = nil - if #available(OSX 10.15, *) { - state = sender is NSSwitch ? (sender as! NSSwitch).state: nil - } else { - state = sender is NSButton ? (sender as! NSButton).state: nil - } - - if state != nil { - store.set(key: "checkUpdatesOnLogin", value: state! == NSControl.StateValue.on) + @objc private func toggleUpdateInterval(_ sender: NSMenuItem) { + if let newUpdateInterval = updateIntervals(rawValue: sender.title) { + store.set(key: "update-interval", value: newUpdateInterval.rawValue) + NotificationCenter.default.post(name: .changeCronInterval, object: nil, userInfo: nil) } } diff --git a/Stats/Views/Settings.swift b/Stats/Views/Settings.swift index 3ed7a507..bc932fae 100644 --- a/Stats/Views/Settings.swift +++ b/Stats/Views/Settings.swift @@ -40,6 +40,23 @@ class SettingsWindow: NSWindow, NSWindowDelegate { let windowController = NSWindowController() windowController.window = self windowController.loadWindow() + + NotificationCenter.default.addObserver(self, selector: #selector(toggleSettingsHandler), name: .toggleSettings, object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc private func toggleSettingsHandler(_ notification: Notification) { + if !self.isVisible { + self.setIsVisible(true) + self.makeKeyAndOrderFront(nil) + } + + if let name = notification.userInfo?["module"] as? String { + self.viewController.openMenu(name) + } } public func setModules() { diff --git a/Stats/helpers.swift b/Stats/helpers.swift new file mode 100644 index 00000000..0301afa6 --- /dev/null +++ b/Stats/helpers.swift @@ -0,0 +1,101 @@ +// +// helpers.swift +// Stats +// +// Created by Serhiy Mytrovtsiy on 13/07/2020. +// Using Swift 5.0. +// Running on macOS 10.15. +// +// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa +import os.log +import StatsKit + +extension AppDelegate { + internal func parseArguments() { + let args = CommandLine.arguments + + if args.contains("--reset") { + os_log(.debug, log: log, "Receive --reset argument. Reseting store (UserDefaults)...") + store.reset() + } + + if let disableIndex = args.firstIndex(of: "--disable") { + if args.indices.contains(disableIndex+1) { + let disableModules = args[disableIndex+1].split(separator: ",") + + disableModules.forEach { (moduleName: Substring) in + if let module = modules.first(where: { $0.config.name.lowercased() == moduleName.lowercased()}) { + module.unmount() + } + } + } + } + + if let mountIndex = args.firstIndex(of: "--mount-path") { + if args.indices.contains(mountIndex+1) { + let mountPath = args[mountIndex+1] + asyncShell("/usr/bin/hdiutil detach \(mountPath)") + asyncShell("/bin/rm -rf \(mountPath)") + + os_log(.debug, log: log, "DMG was unmounted and mountPath deleted") + } + } + + if let dmgIndex = args.firstIndex(of: "--dmg-path") { + if args.indices.contains(dmgIndex+1) { + asyncShell("/bin/rm -rf \(args[dmgIndex+1])") + + os_log(.debug, log: log, "DMG was deleted") + } + } + } + + internal func parseVersion() { + let key = "version" + let currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String + + if !store.exist(key: key) { + store.reset() + os_log(.debug, log: log, "Previous version not detected. Current version (%s) set", currentVersion) + } else { + let prevVersion = store.string(key: key, defaultValue: "") + if prevVersion == currentVersion { + return + } + + if IsNewestVersion(currentVersion: prevVersion, latestVersion: currentVersion) { + let notification = NSUserNotification() + notification.identifier = "updated-from-\(prevVersion)-to-\(currentVersion)" + notification.title = "Successfully updated" + notification.subtitle = "Stats was updated to the v\(currentVersion)" + notification.soundName = NSUserNotificationDefaultSoundName + notification.hasActionButton = false + + NSUserNotificationCenter.default.deliver(notification) + } + + os_log(.debug, log: log, "Detected previous version %s. Current version (%s) set", prevVersion, currentVersion) + } + + store.set(key: key, value: currentVersion) + } + + internal func defaultValues() { + if !store.exist(key: "runAtLoginInitialized") { + store.set(key: "runAtLoginInitialized", value: true) + LaunchAtLogin.isEnabled = true + } + + if store.exist(key: "dockIcon") { + let dockIconStatus = store.bool(key: "dockIcon", defaultValue: false) ? NSApplication.ActivationPolicy.regular : NSApplication.ActivationPolicy.accessory + NSApp.setActivationPolicy(dockIconStatus) + } + + if updateIntervals(rawValue: store.string(key: "update-interval", defaultValue: updateIntervals.atStart.rawValue)) == .atStart { + self.checkForNewVersion(false) + } + } +} diff --git a/StatsKit/extensions.swift b/StatsKit/extensions.swift index 5925800b..92eb43be 100644 --- a/StatsKit/extensions.swift +++ b/StatsKit/extensions.swift @@ -351,7 +351,7 @@ public extension NSView { rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light) rowTitle.textColor = .textColor - let select: NSPopUpButton = NSPopUpButton(frame: NSRect(x: row.frame.width - 50, y: 0, width: 50, height: row.frame.height)) + let select: NSPopUpButton = NSPopUpButton(frame: NSRect(x: row.frame.width - 50, y: (row.frame.height-26)/2, width: 50, height: 26)) select.target = self select.action = action items.forEach { (item: String) in @@ -361,7 +361,7 @@ public extension NSView { select.sizeToFit() rowTitle.setFrameSize(NSSize(width: row.frame.width - select.frame.width, height: rowTitle.frame.height)) - select.setFrameOrigin(NSPoint(x: row.frame.width - select.frame.width, y: 0)) + select.setFrameOrigin(NSPoint(x: row.frame.width - select.frame.width, y: select.frame.origin.y)) row.addSubview(select) row.addSubview(rowTitle) @@ -376,7 +376,7 @@ public extension NSView { rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light) rowTitle.textColor = .textColor - let select: NSPopUpButton = NSPopUpButton(frame: NSRect(x: row.frame.width - 50, y: 0, width: 50, height: row.frame.height)) + let select: NSPopUpButton = NSPopUpButton(frame: NSRect(x: row.frame.width - 50, y: (row.frame.height-26)/2, width: 50, height: 26)) select.target = self select.action = action @@ -397,7 +397,7 @@ public extension NSView { select.sizeToFit() rowTitle.setFrameSize(NSSize(width: row.frame.width - select.frame.width, height: rowTitle.frame.height)) - select.setFrameOrigin(NSPoint(x: row.frame.width - select.frame.width, y: 0)) + select.setFrameOrigin(NSPoint(x: row.frame.width - select.frame.width, y: select.frame.origin.y)) row.addSubview(select) row.addSubview(rowTitle) @@ -412,6 +412,7 @@ public extension Notification.Name { static let openSettingsView = Notification.Name("openSettingsView") static let switchWidget = Notification.Name("switchWidget") static let checkForUpdates = Notification.Name("checkForUpdates") + static let changeCronInterval = Notification.Name("changeCronInterval") static let clickInSettings = Notification.Name("clickInSettings") static let updatePopupSize = Notification.Name("updatePopupSize") } @@ -754,3 +755,15 @@ public func IsNewestVersion(currentVersion: String, latestVersion: String) -> Bo return false } + +public typealias updateInterval = String +public enum updateIntervals: updateInterval { + case atStart = "At start" + case separator_1 = "separator_1" + case oncePerDay = "Once per day" + case oncePerWeek = "Once per week" + case oncePerMonth = "Once per month" + case separator_2 = "separator_2" + case never = "Never" +} +extension updateIntervals: CaseIterable {}