Files

452 lines
17 KiB
Swift

//
// ServersController.swift
// PrivadoVPN
//
// Created by Juraldinio on 3/11/21.
// Copyright © 2021 Privado LLC. All rights reserved.
//
import Foundation
import AppKit
protocol ServerCellOutput: AnyObject {
typealias EmitterScrollState = Bool
var scrollEmitter: Emitter<EmitterScrollState> { get set }
}
protocol InfoCellOutput: AnyObject {
func upgradeButtonTapped()
}
protocol ServersControllerInput: AnyObject {
func bestLocationWith(title: String, flag: String)
func configureSortPanel(with options: [SortPanelOrder])
func removeSortPanel()
}
protocol ServersControllerOutput: AnyObject {
func serversViewIsReady()
func upgradeButtonTapped()
//
func serverListChildrensCount(for record: ServerListRecord?) -> Int
func serverListChild(at index: Int, for record: ServerListRecord?) -> ServerListRecord
func serverListSelected(_ record: ServerListRecord, _ country: ServerListCountryRecord?)
func serverListSelectedFavourite(_ record: ServerListCityRecord)
func serverListWillDisappear()
//
func favouriteViewIsReady()
func favouriteRecord(at index: Int) -> ServerListCityRecord
func favouriteRecordsCount() -> Int
func serverListSelectedFavourite(at row: Int)
func serverSelected(at row: Int)
//
func sortPanelSelected(at index: Int)
func bestLocationSelected()
}
final class ServersController: NSViewController, ServerCellOutput, MenuDelegate {
typealias HeaderCreationClosure = () -> NSViewController
typealias ComponentCreationClosure = () -> NSView
private enum Constants {
static let columnIdentifier = NSUserInterfaceItemIdentifier("ServerList")
enum Color {
static let background = NSColor(red:80, green: 88, blue: 200)
}
enum Localized {
static let bestLocation = "serverlist.location.best"
static let sortBy = "serverlist.sortBy.text"
}
// static let imageLogoLogin = "logo.login"
}
private let output: ServersControllerOutput
private var header: NSViewController?
private var headerCreationClosure: HeaderCreationClosure
private var sortPanelCreationClosure: ComponentCreationClosure
private var sortPanel: NSView?
private var topView: View?
private var countyImageView: ImageView?
private var bestLocationTitleField: NSTextField?
private var bestLocationField: NSTextField?
private var sortPanelButton: DropButton
private var sortByField: NSTextField
private var segmentedController = SegmentedControl()
private var contentView: NSView
private var searchImageView: ImageView
var sortMenu: Menu?
var scrollEmitter = Emitter<EmitterScrollState>()
// MARK: - JSNavigationBarViewControllerProvider
var navigationController: JSNavigationController?
// MARK: - Init0
init(output: ServersControllerOutput,
headerCreation: @escaping HeaderCreationClosure,
sections: [ServerSection],
sortPanelCreationClosure: @escaping ComponentCreationClosure) {
self.output = output
self.sections = sections
self.headerCreationClosure = headerCreation
self.sortPanelCreationClosure = sortPanelCreationClosure
self.sortPanelButton = DropButton()
self.sortByField = NSTextField()
self.contentView = NSView()
self.searchImageView = 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 = View(frame: .zero)
view.autoresizesSubviews = true
view.translatesAutoresizingMaskIntoConstraints = false
view.wantsLayer = true
view.viewBackgroundColor = Constants.Color.background
self.view = view
}
override func viewDidLoad() {
super.viewDidLoad()
self.configureUI()
self.layoutUI()
self.showInitialSection()
self.output.serversViewIsReady()
}
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)
])
}
}
override func viewWillDisappear() {
super.viewWillDisappear()
self.output.serverListWillDisappear()
}
// MARK: - UI
var activeSectionIndex: Int?
var sections: [ServerSection]
private func serverSectionLabels() -> [String] {
var labels = [String]()
self.sections.forEach { labels.append($0.sectionTitle) }
return labels
}
func activateSection(index: Int) {
defer {
self.activeSectionIndex = index
}
if let activeIndex = activeSectionIndex {
self.switchContent(from: self.sections[safe: activeIndex], to: self.sections[safe: index])
} else {
self.show(sections[safe: index])
}
}
private func showInitialSection() {
self.show(self.sections[safe: 0])
self.activeSectionIndex = 0
}
private func show(_ vc: ServerSection?) {
guard let vc = vc else { return }
self.contentView.addSubview(vc.view)
}
private func switchContent(from fromvc: ServerSection?, to tovc: ServerSection?) {
guard let fromvc = fromvc, let tovc = tovc else { return }
fromvc.view.removeFromSuperview()
self.contentView.addSubview(tovc.view)
}
@objc
private func tabSelected(_ sender: SegmentedControl) {
self.activateSection(index: sender.index)
}
private func configureUI() {
self.contentView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(self.contentView)
self.sortByField = self.initializeTextField(textColor: .white, font: .init(name: PrivadoConstants.Font.semiBold, size: 11))
self.sortByField.stringValue = NSLocalizedString(Constants.Localized.sortBy, comment: "")
self.view.addSubview(self.sortByField)
let image = NSImage(named: "arrow-sortpanel")
sortPanelButton.image = image
sortPanelButton.imageXOffset = 6
sortPanelButton.translatesAutoresizingMaskIntoConstraints = false
sortPanelButton.backgroundNormalColor = NSColor(red: 48, green: 55, blue: 152)
sortPanelButton.titleNormalColor = .white
sortPanelButton.strokeWidth = 1
sortPanelButton.strokerColor = NSColor(red: 152, green: 155, blue: 203)
sortPanelButton.cornerRadius = 6
sortPanelButton.titleXOffset = 5
sortPanelButton.font = NSFont(name: PrivadoConstants.Font.semiBold, size: 11)
sortPanelButton.target = self
sortPanelButton.action = #selector(sortPanelTapped(_:))
sortPanelButton.imagePosition = .imageRight
self.view.addSubview(sortPanelButton)
self.segmentedController.translatesAutoresizingMaskIntoConstraints = false
self.segmentedController.target = self
self.segmentedController.action = #selector(self.tabSelected(_:))
self.segmentedController.xSegmentOffset = 8
self.segmentedController.segmentHeight = 34
self.view.addSubview(self.segmentedController)
var segments = [ServerSegment]()
self.sections.forEach {
segments.append(ServerSegment(text: $0.sectionTitle, normalBackgroundColor: NSColor(red: 48, green: 55, blue: 152), normalFont: .systemFont(ofSize: 14), normalTextColor: .white, selectedBackgroundColor: .white, selectedFont: .systemFont(ofSize: 14), selectedTextColor: .black, normalImage: NSImage(named: $0.sectionDefaultImageName) ?? NSImage(), selectedImage: NSImage(named: $0.sectionSelectedImageName) ?? NSImage()))
}
self.segmentedController.segments = segments
let topView = View(frame: .zero)
topView.translatesAutoresizingMaskIntoConstraints = false
topView.viewBackgroundColor = NSColor(red: 105, green: 112, blue: 208)
topView.useHandCursor = true
topView.target = self
topView.action = #selector(self.bestLocationTapped)
self.view.addSubview(topView)
self.topView = topView
let countyImageView = ImageView(frame: .zero)
self.countyImageView = countyImageView
topView.addSubview(countyImageView)
let titleField = self.initializeTextField(textColor: NSColor(red: 230, green: 231, blue: 247), font: NSFont(name: PrivadoConstants.Font.semiBold, size: 12))
titleField.stringValue = NSLocalizedString(Constants.Localized.bestLocation, comment: "bestLocation")
topView.addSubview(titleField)
self.bestLocationTitleField = titleField
let bestLocationField = self.initializeTextField(textColor: .white, font: NSFont(name: PrivadoConstants.Font.semiBold, size: 14))
topView.addSubview(bestLocationField)
self.bestLocationField = bestLocationField
let sortPanel = self.sortPanelCreationClosure()
self.view.addSubview(sortPanel)
self.sortPanel = sortPanel
self.sortPanel?.isHidden = true
if let image = NSImage(named: "search") {
self.searchImageView.image = image
}
self.searchImageView.translatesAutoresizingMaskIntoConstraints = false
self.searchImageView.useHandCursor = true
self.searchImageView.target = self
self.searchImageView.action = #selector(self.showSearchPanel)
self.view.addSubview(self.searchImageView)
}
private func layoutUI() {
guard let topView = topView,
let bestLocationField = self.bestLocationField,
let bestLocationTitleField = self.bestLocationTitleField,
let countyImageView = self.countyImageView else { return }
NSLayoutConstraint.activate([
topView.topAnchor.constraint(equalTo: self.view.topAnchor),
topView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
topView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
topView.heightAnchor.constraint(equalToConstant: 60),
//
countyImageView.heightAnchor.constraint(equalToConstant: 24),
countyImageView.widthAnchor.constraint(equalToConstant: 24),
countyImageView.leadingAnchor.constraint(equalTo: topView.leadingAnchor, constant: 26),
countyImageView.topAnchor.constraint(equalTo: topView.topAnchor, constant: 17),
//
bestLocationTitleField.topAnchor.constraint(equalTo: topView.topAnchor, constant: 13),
bestLocationTitleField.leadingAnchor.constraint(equalTo: countyImageView.trailingAnchor, constant: 10),
//
bestLocationField.leadingAnchor.constraint(equalTo: countyImageView.trailingAnchor, constant: 10),
bestLocationField.topAnchor.constraint(equalTo: bestLocationTitleField.bottomAnchor, constant: 3),
self.segmentedController.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: 20),
self.segmentedController.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20),
self.segmentedController.widthAnchor.constraint(equalToConstant: 230),
self.segmentedController.heightAnchor.constraint(equalToConstant: 40),
self.searchImageView.widthAnchor.constraint(equalToConstant: 34),
self.searchImageView.heightAnchor.constraint(equalToConstant: 34),
self.searchImageView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20),
self.searchImageView.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: 25),
self.sortByField.centerYAnchor.constraint(equalTo: sortPanelButton.centerYAnchor),
self.sortByField.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20),
self.sortPanelButton.leadingAnchor.constraint(equalTo: sortByField.trailingAnchor, constant: 8),
self.sortPanelButton.topAnchor.constraint(equalTo: segmentedController.bottomAnchor, constant: 20),
self.sortPanelButton.heightAnchor.constraint(equalToConstant: 34),
self.sortPanelButton.widthAnchor.constraint(equalToConstant: 230),
])
NSLayoutConstraint.activate([
self.contentView.topAnchor.constraint(equalTo: sortPanelButton.bottomAnchor),
self.contentView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.contentView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.contentView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
}
private func initializeTextField(textColor: NSColor, font: NSFont?, alignment: NSTextAlignment = .center, backgroundExist: Bool = false) -> NSTextField {
let textField = NSTextField()
textField.usesSingleLineMode = false
textField.maximumNumberOfLines = 0
textField.isBezeled = false
textField.drawsBackground = backgroundExist
textField.isEditable = false
textField.isSelectable = false
textField.translatesAutoresizingMaskIntoConstraints = false
textField.focusRingType = .none
textField.alignment = alignment
textField.textColor = textColor
textField.font = font
return textField
}
func menuItemDidTap(_ index: Int?) {
if index != nil {
guard let index = index, let title = sortMenu?.items[index].title else { return }
self.sortPanelButton.title = title
self.sortPanelButton.state = sortPanelButton.state == .on ? .off : .on
self.output.sortPanelSelected(at: index)
} else {
self.sortPanelButton.changeState()
}
}
@objc
private func sortPanelTapped(_ sender: NSButton) {
switch sender.state {
case .on:
self.sortMenu?.show(from: sender, animated: true, isPopover: UserSettings.shared.isDocked)
case .off:
self.sortMenu?.dismiss(animated: true)
default:
break
}
}
@objc
private func showSearchPanel() {
guard let sortPanel = sortPanel, let topView = topView else {
return
}
self.segmentedController.isHidden = true
self.searchImageView.isHidden = true
sortPanel.isHidden = false
NSLayoutConstraint.activate([
sortPanel.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20),
sortPanel.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: 23),
sortPanel.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20),
sortPanel.heightAnchor.constraint(equalToConstant: 35)
])
}
@objc
private func bestLocationTapped() {
self.output.bestLocationSelected()
}
}
// MARK: - ServersControllerInput
extension ServersController: ServersControllerInput {
func configureSortPanel(with options: [SortPanelOrder]) {
let menu = Menu(with: nil, configuration: SortPanelConfiguration())
menu.delegate = self
options.forEach { option in
menu.addItem(WindowMenuItem(NSLocalizedString(option.localizedSting, comment: ""), isSelectable: true, action: { }))
}
if let option = options.first {
self.sortPanelButton.title = NSLocalizedString(option.localizedSting, comment: "")
menu.selectItem(at: 0)
}
self.sortMenu = menu
}
func bestLocationWith(title: String, flag: String) {
guard let bestLocationField = self.bestLocationField,
let countryImageView = self.countyImageView else { return }
bestLocationField.stringValue = title
countryImageView.image = NSImage(named: flag)
}
func removeSortPanel() {
self.sortPanel?.isHidden = true
self.searchImageView.isHidden = false
self.segmentedController.isHidden = false
}
}
// MARK: - JSNavigationBarViewControllerProvider
extension ServersController: JSNavigationBarViewControllerProvider {
func navigationBarViewController() -> NSViewController {
if let header = self.header { return header }
let header = self.headerCreationClosure()
self.header = header
return header
}
}
extension ServerListController: ServerSection {
var sectionDefaultImageName: String {
return ServerSectionImageNames.allServersDefault
}
var sectionSelectedImageName: String {
return ServerSectionImageNames.allServersSelected
}
var sectionIdentifier: ServerSectionIdentifier {
return ServerSectionIdentifiers.allServers
}
var sectionTitle: String {
return ServerSectionTitleLocalized.allServers
}
}