diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index ab791504..86f82044 100755 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ 9A09C8A222B3D94D0018426F /* BatteryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09C8A122B3D94D0018426F /* BatteryView.swift */; }; 9A1410F9229E721100D29793 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1410F8229E721100D29793 /* AppDelegate.swift */; }; 9A141100229E721200D29793 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9A1410FE229E721200D29793 /* Main.storyboard */; }; + 9A426DB822C2B5EE00C064C4 /* macAppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A426DB722C2B5EE00C064C4 /* macAppUpdater.swift */; }; + 9A426DBE22C2BE0000C064C4 /* Updates.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9A426DBD22C2BE0000C064C4 /* Updates.storyboard */; }; 9A57A18522A1D26D0033E318 /* MenuBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A57A18422A1D26D0033E318 /* MenuBar.swift */; }; 9A57A19B22A1E1C50033E318 /* Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A57A19A22A1E1C50033E318 /* Module.swift */; }; 9A57A19D22A1E3270033E318 /* CPU.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A57A19C22A1E3270033E318 /* CPU.swift */; }; @@ -58,6 +60,8 @@ 9A1410FF229E721200D29793 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 9A141101229E721200D29793 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9A141102229E721200D29793 /* Stats.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Stats.entitlements; sourceTree = ""; }; + 9A426DB722C2B5EE00C064C4 /* macAppUpdater.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = macAppUpdater.swift; sourceTree = ""; }; + 9A426DBD22C2BE0000C064C4 /* Updates.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Updates.storyboard; sourceTree = ""; }; 9A57A18422A1D26D0033E318 /* MenuBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBar.swift; sourceTree = ""; }; 9A57A19A22A1E1C50033E318 /* Module.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Module.swift; sourceTree = ""; }; 9A57A19C22A1E3270033E318 /* CPU.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPU.swift; sourceTree = ""; }; @@ -157,8 +161,9 @@ isa = PBXGroup; children = ( 9A6CFC0022A1C9F5001E782D /* Assets.xcassets */, - 9AFFCB3A22B3FD0500B0E6D8 /* About.storyboard */, 9A1410FE229E721200D29793 /* Main.storyboard */, + 9AFFCB3A22B3FD0500B0E6D8 /* About.storyboard */, + 9A426DBD22C2BE0000C064C4 /* Updates.storyboard */, 9A141101229E721200D29793 /* Info.plist */, 9A141102229E721200D29793 /* Stats.entitlements */, ); @@ -183,6 +188,7 @@ 9A5B1CBE229E78F0008B9D3C /* Observable.swift */, 9A57A19A22A1E1C50033E318 /* Module.swift */, 9A5B1CC4229E7B40008B9D3C /* Extensions.swift */, + 9A426DB722C2B5EE00C064C4 /* macAppUpdater.swift */, ); path = libs; sourceTree = ""; @@ -296,6 +302,11 @@ TargetAttributes = { 9A1410F4229E721100D29793 = { CreatedOnToolsVersion = 10.2.1; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; }; 9AFA401D22AE49A100FE90BC = { CreatedOnToolsVersion = 10.2.1; @@ -329,6 +340,7 @@ 9A6CFC0122A1C9F5001E782D /* Assets.xcassets in Resources */, 9AFFCB3B22B3FD0500B0E6D8 /* About.storyboard in Resources */, 9A141100229E721200D29793 /* Main.storyboard in Resources */, + 9A426DBE22C2BE0000C064C4 /* Updates.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -349,6 +361,7 @@ buildActionMask = 2147483647; files = ( 9A09C8A222B3D94D0018426F /* BatteryView.swift in Sources */, + 9A426DB822C2B5EE00C064C4 /* macAppUpdater.swift in Sources */, 9A7B8F6F22A2C57000DEB352 /* DiskReader.swift in Sources */, 9A7B8F6922A2C3A100DEB352 /* Memory.swift in Sources */, 9A7B8F5E22A2A57600DEB352 /* CPUReader.swift in Sources */, @@ -525,6 +538,10 @@ COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = RP2S87B72W; ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); INFOPLIST_FILE = "$(SRCROOT)/Stats/Supporting Files/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -548,6 +565,10 @@ COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = RP2S87B72W; ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); INFOPLIST_FILE = "$(SRCROOT)/Stats/Supporting Files/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/Stats/AppDelegate.swift b/Stats/AppDelegate.swift index fe91ae30..b4e7d474 100755 --- a/Stats/AppDelegate.swift +++ b/Stats/AppDelegate.swift @@ -81,3 +81,65 @@ class AboutVC: NSViewController { } } } + +class UpdatesVC: NSViewController { + @IBOutlet weak var mainView: NSStackView! + @IBOutlet weak var spinnerView: NSView! + @IBOutlet weak var noInternetView: NSView! + @IBOutlet weak var mainTextLabel: NSTextFieldCell! + @IBOutlet weak var currentVersionLabel: NSTextField! + @IBOutlet weak var latestVersionLabel: NSTextField! + @IBOutlet weak var downloadButton: NSButton! + @IBOutlet weak var spinner: NSProgressIndicator! + + let updater = macAppUpdater(user: "exelban", repo: "stats") + var url: String? + + override func viewDidLoad() { + super.viewDidLoad() + self.view.wantsLayer = true + + self.spinner.startAnimation(self) + + updater.check() { result, error in + if error != nil && error as! String == "No internet connection" { + DispatchQueue.main.async(execute: { + self.spinnerView.isHidden = true + self.noInternetView.isHidden = false + }) + return + } + + guard error == nil, let version: version = result else { + print("Error: \(error ?? "check error")") + return + } + + DispatchQueue.main.async(execute: { + self.spinner.stopAnimation(self) + self.spinnerView.isHidden = true + self.mainView.isHidden = false + self.currentVersionLabel.stringValue = version.current + self.latestVersionLabel.stringValue = version.latest + self.url = version.url + + if !version.newest { + self.mainTextLabel.stringValue = "No new version available" + self.downloadButton.isEnabled = false + } + }) + } + } + + @IBAction func download(_ sender: Any) { + guard let urlString = self.url, let url = URL(string: urlString) else { + return + } + NSWorkspace.shared.open(url) + self.view.window?.close() + } + + @IBAction func exit(_ sender: Any) { + self.view.window?.close() + } +} diff --git a/Stats/MenuBar.swift b/Stats/MenuBar.swift index 40b8f0a6..ad72edff 100644 --- a/Stats/MenuBar.swift +++ b/Stats/MenuBar.swift @@ -74,14 +74,27 @@ class MenuBar { menu.addItem(preferences) menu.addItem(NSMenuItem.separator()) + + let updateMenu = NSMenuItem(title: "Check for updates", action: #selector(checkUpdate), keyEquivalent: "") + updateMenu.target = self + let aboutMenu = NSMenuItem(title: "About Stats", action: #selector(openAbout), keyEquivalent: "") aboutMenu.target = self + + menu.addItem(updateMenu) menu.addItem(aboutMenu) menu.addItem(NSMenuItem(title: "Quit Stats", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "")) return menu } + @objc func checkUpdate(_ sender : NSMenuItem) { + let updatesVC: NSWindowController? = NSStoryboard(name: "Updates", bundle: nil).instantiateController(withIdentifier: "UpdatesVC") as? NSWindowController + updatesVC?.window?.center() + updatesVC?.window?.level = .floating + updatesVC!.showWindow(self) + } + @objc func openAbout(_ sender : NSMenuItem) { let aboutVC: NSWindowController? = NSStoryboard(name: "About", bundle: nil).instantiateController(withIdentifier: "AboutVC") as? NSWindowController aboutVC?.window?.center() diff --git a/Stats/Supporting Files/Stats.entitlements b/Stats/Supporting Files/Stats.entitlements index f2ef3ae0..625af03d 100755 --- a/Stats/Supporting Files/Stats.entitlements +++ b/Stats/Supporting Files/Stats.entitlements @@ -2,9 +2,11 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + diff --git a/Stats/Supporting Files/Updates.storyboard b/Stats/Supporting Files/Updates.storyboard new file mode 100644 index 00000000..7269c41f --- /dev/null +++ b/Stats/Supporting Files/Updates.storyboard @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Stats/libs/macAppUpdater.swift b/Stats/libs/macAppUpdater.swift new file mode 100644 index 00000000..f86432d7 --- /dev/null +++ b/Stats/libs/macAppUpdater.swift @@ -0,0 +1,136 @@ +// +// macAppUpdater.swift +// Stats +// +// Created by Serhiy Mytrovtsiy on 25.06.2019. +// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved. +// + +import Foundation +import SystemConfiguration + +extension String: Error {} + +struct version { + let current: String + let latest: String + let newest: Bool + let url: String +} + +public class macAppUpdater { + let user: String + let repo: String + + let appName: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as! String + let currentVersion: String = "v\(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String)" + + var url: String { + return "https://api.github.com/repos/\(user)/\(repo)/releases/latest" + } + + init(user: String, repo: String) { + self.user = user + self.repo = repo + } + + func fetchLastVersion(completionHandler: @escaping (_ result: [String]?, _ error: Error?) -> Void) { + let task = URLSession.shared.dataTask(with: URL(string: self.url)!) { data, response, error in + guard let data = data, error == nil else { return } + + do { + let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) + guard let jsonArray = jsonResponse as? [String: Any] else { + completionHandler(nil, "parse json") + return + } + let lastVersion = jsonArray["tag_name"] as? String + + guard let assets = jsonArray["assets"] as? [[String: Any]] else { + completionHandler(nil, "parse assets") + return + } + if let asset = assets.first(where: {$0["name"] as! String == "\(self.appName).dmg"}) { + let downloadURL = asset["browser_download_url"] as? String + completionHandler([lastVersion!, downloadURL!], nil) + } + } catch let parsingError { + completionHandler(nil, parsingError) + } + } + task.resume() + } + + func checkIfNewer(current: String, latest: String) -> Bool { + guard let currentNumber: Int64 = Int64(current.replacingOccurrences(of: "[v.]", with: "", options: [.regularExpression])) else { + print("Error: wrong version tag \(current)") + return false + } + guard let latestNumber: Int64 = Int64(latest.replacingOccurrences(of: "[v.]", with: "", options: [.regularExpression])) else { + print("Error: wrong version tag \(latest)") + return false + } + return latestNumber>currentNumber + } + + func check(completionHandler: @escaping (_ result: version?, _ error: Error?) -> Void) { + if !Reachability.isConnectedToNetwork() { + completionHandler(nil, "No internet connection") + return + } + + fetchLastVersion() { result, error in + guard error == nil else { + completionHandler(nil, error) + return + } + + guard let results = result, results.count > 1 else { + completionHandler(nil, "wrong results") + return + } + + let downloadURL: String = result![1] + let lastVersion: String = result![0] + let newVersion: Bool = self.checkIfNewer(current: self.currentVersion, latest: lastVersion) + + completionHandler(version(current: self.currentVersion, latest: lastVersion, newest: newVersion, url: downloadURL), nil) + } + } +} + + +// https://stackoverflow.com/questions/30743408/check-for-internet-connection-with-swift +public class Reachability { + class func isConnectedToNetwork() -> Bool { + var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0)) + zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress)) + zeroAddress.sin_family = sa_family_t(AF_INET) + + let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in + SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress) + } + } + + var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0) + if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false { + return false + } + + /* Only Working for WIFI + let isReachable = flags == .reachable + let needsConnection = flags == .connectionRequired + + return isReachable && !needsConnection + */ + + // Working for Cellular and WIFI + let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0 + let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0 + let ret = (isReachable && !needsConnection) + + return ret + + } +}