// // 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 { 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() // 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 } }