// // Update.swift // Stats // // Created by Serhiy Mytrovtsiy on 21/05/2020. // Using Swift 5.0. // Running on macOS 10.15. // // Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved. // import Cocoa import Kit internal class UpdateWindow: NSWindow, NSWindowDelegate { private let viewController: UpdateViewController = UpdateViewController() init() { super.init( contentRect: NSRect( x: NSScreen.main!.frame.width - self.viewController.view.frame.width, y: NSScreen.main!.frame.height - self.viewController.view.frame.height, width: self.viewController.view.frame.width, height: self.viewController.view.frame.height ), styleMask: [.closable, .titled], backing: .buffered, defer: true ) self.title = "Stats" self.contentViewController = self.viewController self.titlebarAppearsTransparent = true self.positionCenter() self.setIsVisible(false) let windowController = NSWindowController() windowController.window = self windowController.loadWindow() } internal func open(_ v: version_s, settingButton: Bool = false) { if !self.isVisible || settingButton { self.setIsVisible(true) self.makeKeyAndOrderFront(nil) } self.viewController.open(v) } private func positionCenter() { self.setFrameOrigin(NSPoint( x: (NSScreen.main!.frame.width - self.viewController.view.frame.width)/2, y: (NSScreen.main!.frame.height - self.viewController.view.frame.height)/1.75 )) } } private class UpdateViewController: NSViewController { private var update: UpdateView public init() { self.update = UpdateView(frame: NSRect(x: 0, y: 0, width: 280, height: 176)) super.init(nibName: nil, bundle: nil) self.view = self.update } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } internal func open(_ v: version_s) { self.update.clear() if v.newest { self.update.newVersion(v) return } self.update.noUpdates() } } private class UpdateView: NSView { private var version: version_s? = nil private var path: String = "" override init(frame: NSRect) { super.init(frame: CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.width, height: frame.height)) self.wantsLayer = true let sidebar = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)) sidebar.material = .sidebar sidebar.blendingMode = .behindWindow sidebar.state = .active self.addSubview(sidebar) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } internal func newVersion(_ version: version_s) { self.version = version let view: NSStackView = NSStackView(frame: NSRect( x: Constants.Settings.margin, y: 0, width: self.frame.width-(Constants.Settings.margin*2), height: self.frame.height )) view.orientation = .vertical view.alignment = .centerX view.distribution = .fillEqually view.spacing = Constants.Settings.margin view.edgeInsets = NSEdgeInsets( top: Constants.Settings.margin*2, left: 0, bottom: Constants.Settings.margin, right: 0 ) let header: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: 0, height: 44)) header.heightAnchor.constraint(equalToConstant: header.frame.height).isActive = true header.orientation = .horizontal header.spacing = 10 header.distribution = .equalCentering let icon: NSImageView = NSImageView(image: NSImage(named: NSImage.Name("AppIcon"))!) icon.setFrameSize(NSSize(width: 44, height: 44)) icon.widthAnchor.constraint(equalToConstant: 44).isActive = true let title: NSTextField = TextView() title.font = NSFont.systemFont(ofSize: 14, weight: .medium) title.stringValue = localizedString("New version available") header.addArrangedSubview(icon) header.addArrangedSubview(title) let versions: NSGridView = NSGridView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 32)) versions.heightAnchor.constraint(equalToConstant: versions.frame.height).isActive = true versions.rowSpacing = 0 versions.yPlacement = .fill versions.xPlacement = .fill let currentVersionTitle: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 16)) currentVersionTitle.stringValue = localizedString("Current version: ") let currentVersion: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 0)) currentVersion.stringValue = version.current let latestVersionTitle: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 16)) latestVersionTitle.stringValue = localizedString("Latest version: ") let latestVersion: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 0)) latestVersion.stringValue = version.latest versions.addRow(with: [currentVersionTitle, currentVersion]) versions.addRow(with: [latestVersionTitle, latestVersion]) let buttons: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 26)) buttons.heightAnchor.constraint(equalToConstant: buttons.frame.height).isActive = true buttons.orientation = .horizontal buttons.spacing = 10 buttons.distribution = .fillEqually let closeButton: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: view.frame.width/2, height: 26)) closeButton.title = localizedString("Close") closeButton.bezelStyle = .rounded closeButton.action = #selector(self.close) closeButton.target = self let changelogButton: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: 0, height: 26)) changelogButton.title = localizedString("Changelog") changelogButton.bezelStyle = .rounded changelogButton.action = #selector(self.changelog) changelogButton.target = self let downloadButton: NSButton = NSButton(frame: NSRect(x: view.frame.width/2, y: 0, width: view.frame.width/2, height: 26)) downloadButton.title = localizedString("Download") downloadButton.bezelStyle = .rounded downloadButton.action = #selector(self.download) downloadButton.target = self buttons.addArrangedSubview(closeButton) buttons.addArrangedSubview(changelogButton) buttons.addArrangedSubview(downloadButton) view.addArrangedSubview(header) view.addArrangedSubview(NSView()) view.addArrangedSubview(versions) view.addArrangedSubview(NSView()) view.addArrangedSubview(buttons) self.addSubview(view) } internal func noUpdates() { let view: NSView = NSView(frame: NSRect(x: 10, y: 10, width: self.frame.width - 20, height: self.frame.height - 20)) let title: NSTextField = TextView(frame: NSRect(x: 0, y: ((view.frame.height - 18)/2), width: view.frame.width, height: 34)) title.font = NSFont.systemFont(ofSize: 14, weight: .light) title.alignment = .center title.stringValue = localizedString("The latest version of Stats installed") let button: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 26)) button.title = localizedString("Close") button.bezelStyle = .rounded button.action = #selector(self.close) button.target = self view.addSubview(button) view.addSubview(title) self.addSubview(view) } internal func clear() { self.subviews.filter{ !($0 is NSVisualEffectView) }.forEach{ $0.removeFromSuperview() } } @objc private func download() { guard let urlString = self.version?.url, let url = URL(string: urlString) else { return } self.clear() let view: NSView = NSView(frame: NSRect(x: 10, y: 10, width: self.frame.width - 20, height: self.frame.height - 20 - 26)) let title: NSTextField = TextView(frame: NSRect(x: 0, y: view.frame.height - 28, width: view.frame.width, height: 18)) title.font = NSFont.systemFont(ofSize: 14, weight: .semibold) title.alignment = .center title.stringValue = localizedString("Downloading...") let progressBar: NSProgressIndicator = NSProgressIndicator() progressBar.frame = NSRect(x: 20, y: 64, width: view.frame.width - 40, height: 22) progressBar.minValue = 0 progressBar.maxValue = 1 progressBar.isIndeterminate = false let state: NSTextField = TextView(frame: NSRect(x: 0, y: 48, width: view.frame.width, height: 18)) state.font = NSFont.systemFont(ofSize: 12, weight: .light) state.alignment = .center state.textColor = .secondaryLabelColor state.stringValue = "0%" let closeButton: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 26)) closeButton.title = localizedString("Cancel") closeButton.bezelStyle = .rounded closeButton.action = #selector(self.close) closeButton.target = self let installButton: NSButton = NSButton(frame: NSRect(x: view.frame.width/2, y: 0, width: view.frame.width/2, height: 26)) installButton.title = localizedString("Install") installButton.bezelStyle = .rounded installButton.action = #selector(self.install) installButton.target = self installButton.isHidden = true updater.download(url, progress: { progress in DispatchQueue.main.async { progressBar.doubleValue = progress.fractionCompleted state.stringValue = "\(Int(progress.fractionCompleted*100))%" } }, completion: { path in self.path = path DispatchQueue.main.async { closeButton.setFrameSize(NSSize(width: view.frame.width/2, height: closeButton.frame.height)) installButton.isHidden = false } }) view.addSubview(title) view.addSubview(progressBar) view.addSubview(state) view.addSubview(closeButton) view.addSubview(installButton) self.addSubview(view) } @objc private func close() { self.window?.close() } @objc private func changelog() { if let version = self.version { NSWorkspace.shared.open(URL(string: "https://github.com/exelban/stats/releases/tag/\(version.latest)")!) } } @objc private func install() { updater.install(path: self.path) { error in if let error { showAlert("Error update Stats", error, .critical) } } } }