// // MainController.swift // PrivadoVPN // // Created by Juraldinio on 1/27/21. // Copyright © 2021 Privado LLC. All rights reserved. // import Foundation import AppKit protocol MainControllerOutput: AnyObject { func viewIsReady() } protocol MainControllerInput: AnyObject { func loading(progress: Bool) func activate(components: [MainControllerComponent]) } enum MainControllerComponent: CaseIterable { case notification case connection case servers case killswitch case remaining } final class MainController: NSViewController { typealias HeaderCreationClosure = () -> NSViewController typealias ComponentCreationClosure = (MainControllerComponent) -> NSView private enum Constants { enum Geometry { // static let logoHeight: CGFloat = 87 static let progressSize = CGSize(width: 120, height: 120) static let textHeight: CGFloat = 20 static let textTopSpace: CGFloat = 5 } enum Color { static let background = NSColor(red: 81, green: 88, blue: 193) static let activityTitle = NSColor(red: 122, green: 134, blue: 190) } enum Font { // swiftlint:disable nesting enum Name { static let activityTitle = PrivadoConstants.Font.light } enum Size { static let activityTitle: CGFloat = 14 } // swiftlint:enable nesting } enum Image { static let background = "map.background" } } private let output: MainControllerOutput private var headerCreationClosure: HeaderCreationClosure private var componentCreationClosure: ComponentCreationClosure private lazy var activityIndicator: ITProgressIndicator = { let indicator = ITProgressIndicator(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) indicator.color = .white indicator.hideWhenStopped = true indicator.lengthOfLine = 13 indicator.widthOfLine = 5 indicator.numberOfLines = 9 indicator.innerMargin = 8 indicator.translatesAutoresizingMaskIntoConstraints = false return indicator }() private lazy var loader = PrivadoLoader(frame: .zero, isButton: false) private var header: NSViewController? private var firstLoading = true private var backgroundImageView: ImageView // MARK: - JSNavigationBarViewControllerProvider var navigationController: JSNavigationController? // MARK: - Init init(output: MainControllerOutput, headerCreation: @escaping HeaderCreationClosure, componentCreation: @escaping ComponentCreationClosure) { self.output = output self.headerCreationClosure = headerCreation self.componentCreationClosure = componentCreation self.backgroundImageView = ImageView() super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Lyfecycle override func loadView() { let view = NSView(frame: .zero) view.autoresizesSubviews = true view.translatesAutoresizingMaskIntoConstraints = false view.wantsLayer = true view.layer?.backgroundColor = Constants.Color.background.cgColor self.view = view } override func viewDidLoad() { super.viewDidLoad() self.output.viewIsReady() } override func viewDidAppear() { super.viewDidAppear() if let superview = self.view.superview { NSLayoutConstraint.activate([ self.view.topAnchor.constraint(equalTo: superview.topAnchor), self.view.leadingAnchor.constraint(equalTo: superview.leadingAnchor), self.view.trailingAnchor.constraint(equalTo: superview.trailingAnchor), self.view.bottomAnchor.constraint(equalTo: superview.bottomAnchor) ]) } self.view.makeKey() } // MARK: - UI private func configureBackground() { if let image = NSImage(named: Constants.Image.background) { let coloredImage = image.imageWithTintColor(tintColor: NSColor.black) self.backgroundImageView.image = coloredImage } self.backgroundImageView.imageScaling = .scaleNone self.view.addSubview(self.backgroundImageView) NSLayoutConstraint.activate([ self.backgroundImageView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: -50), self.backgroundImageView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), self.backgroundImageView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), self.backgroundImageView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor) ]) } } // MARK: - MainControllerInput extension MainController: MainControllerInput { func loading(progress: Bool) { if self.firstLoading { guard progress else { return } self.loader.translatesAutoresizingMaskIntoConstraints = false self.loader.arcColor = NSColor(red: 132, green: 138, blue: 217) self.loader.keyholeColor = NSColor.white self.loader.arcWidth = 5 self.view.addSubview(self.loader) NSLayoutConstraint.activate([ self.loader.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), self.loader.centerYAnchor.constraint(equalTo: self.view.centerYAnchor), self.loader.heightAnchor.constraint(equalToConstant: Constants.Geometry.progressSize.height), self.loader.widthAnchor.constraint(equalToConstant: Constants.Geometry.progressSize.height) ]) self.loader.startAnimation(clockwise: false) self.firstLoading = false } else { if !progress { self.loader.removeFromSuperview() } } } func delay(_ delay: Double, closure: @escaping () -> Void) { DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: closure) } func activate(components: [MainControllerComponent]) { self.loader.removeFromSuperview() self.configureBackground() let wholeComponents = MainControllerComponent.allCases.count == components.count let views = components.map { self.componentCreationClosure($0) } views.forEach { self.view.addSubview($0) } views.enumerated().forEach { index, view in let constraints: [NSLayoutConstraint] if index == 0 { constraints = [ view.topAnchor.constraint(equalTo: self.view.topAnchor), view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), ] } else if index == views.count - 1, wholeComponents { constraints = [ view.topAnchor.constraint(equalTo: views[index - 1].bottomAnchor), view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor) ] } else { constraints = [ view.topAnchor.constraint(equalTo: views[index - 1].bottomAnchor), view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor) ] } NSLayoutConstraint.activate(constraints) } } } extension MainController: JSNavigationBarViewControllerProvider { func navigationBarViewController() -> NSViewController { if let header = self.header { return header } let header = self.headerCreationClosure() self.header = header return header } }