452 lines
17 KiB
Swift
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
|
|
}
|
|
}
|