Files

324 lines
11 KiB
Swift

//
// NotificationController.swift
// PrivadoVPN
//
// Created by Juraldinio on 3/28/21.
// Copyright © 2021 Privado LLC. All rights reserved.
//
import Foundation
protocol NotificationControllerInput: NSViewController {
func updateView()
func refreshView(exists: Bool)
}
protocol NotificationControllerOutput: AnyObject {
func notificationListReady(_ view: NotificationControllerInput)
func notificationLostClose(_ view: NotificationControllerInput)
//
func notificationEmptyMessage(_ view: NotificationControllerInput) -> String
//
func notificationRecordsCount(in view: NotificationControllerInput) -> Int
func notificationRecord(in view: NotificationControllerInput, at index: Int) -> NotificationCellModel?
//
func viewDidAppear()
}
final class NotificationController: NSViewController {
typealias HeaderCreationClosure = () -> NSViewController
// swiftlint:disable nesting
private enum Constants {
static let columnIdentifier = NSUserInterfaceItemIdentifier("NotificationList")
static let cellIdentifier = NSUserInterfaceItemIdentifier("NotificationCell")
enum Geometry {
enum Close {
static let width: CGFloat = 23
static let height: CGFloat = 23
static let top: CGFloat = 11
static let trailing: CGFloat = 10
}
static let closeBetweenList: CGFloat = 3
}
enum Color {
static let background = NSColor(red: 45, green: 51, blue: 82)
static let emptyText = NSColor(red: 122, green: 134, blue: 190)
}
enum Font {
// swiftlint:disable nesting
enum Name {
static let emptyText = PrivadoConstants.Font.light
}
enum Size {
static let emptyText: CGFloat = 14
}
// swiftlint:enable nesting
}
static let imageCloseButton = "close"
}
// swiftlint:enable nesting
private let output: NotificationControllerOutput
private var header: NSViewController?
private var headerCreationClosure: HeaderCreationClosure
private let buttonClose: ImageView
private let scrollView: NSScrollView
private let notificationsView: NSTableView
private let emptyTextField: NSTextField
// MARK: - JSNavigationBarViewControllerProvider
var navigationController: JSNavigationController?
// MARK: - Init
init(output: NotificationControllerOutput, headerCreation: @escaping HeaderCreationClosure) {
self.output = output
self.headerCreationClosure = headerCreation
self.buttonClose = ImageView(frame: .zero)
self.notificationsView = NSTableView(frame: .zero)
self.scrollView = NSScrollView(frame: .zero)
self.emptyTextField = NSTextField(frame: .zero)
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.output.notificationListReady(self)
}
override func viewDidAppear() {
super.viewDidAppear()
self.output.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)
])
}
}
// MARK: - UI
private func configureUI() {
if let image = NSImage(named: Constants.imageCloseButton) {
self.buttonClose.image = image
self.buttonClose.useHandCursor = true
self.buttonClose.target = self
self.buttonClose.action = #selector(self.onCloseAction)
self.view.addSubview(self.buttonClose)
}
let column = NSTableColumn(identifier: Constants.columnIdentifier)
column.isEditable = false
column.width = 1
self.notificationsView.addTableColumn(column)
// self.outlineView.translatesAutoresizingMaskIntoConstraints = false
self.notificationsView.backgroundColor = .clear
self.notificationsView.selectionHighlightStyle = .none
self.notificationsView.rowSizeStyle = .custom
self.notificationsView.headerView = nil
self.notificationsView.delegate = self
self.notificationsView.dataSource = self
self.scrollView.isHidden = true
self.scrollView.drawsBackground = false
self.scrollView.translatesAutoresizingMaskIntoConstraints = false
self.scrollView.hasVerticalScroller = true
self.scrollView.documentView = self.notificationsView
self.view.addSubview(self.scrollView)
self.emptyTextField.isHidden = true
self.emptyTextField.translatesAutoresizingMaskIntoConstraints = false
self.emptyTextField.isBezeled = false
self.emptyTextField.drawsBackground = false
self.emptyTextField.isEditable = false
self.emptyTextField.isSelectable = false
self.emptyTextField.focusRingType = .none
self.emptyTextField.textColor = Constants.Color.emptyText
self.emptyTextField.font = NSFont(name: Constants.Font.Name.emptyText, size: Constants.Font.Size.emptyText)
self.emptyTextField.alignment = .center
self.emptyTextField.maximumNumberOfLines = 2
self.view.addSubview(self.emptyTextField)
// self.view.addSubview(self.outlineView)
}
private func layoutUI() {
var constraints = [NSLayoutConstraint]()
if self.buttonClose.superview.isExist {
constraints.append(contentsOf: [
self.buttonClose.topAnchor.constraint(equalTo: self.view.topAnchor, constant: Constants.Geometry.Close.top),
self.buttonClose.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -Constants.Geometry.Close.trailing),
self.buttonClose.widthAnchor.constraint(equalToConstant: Constants.Geometry.Close.width),
self.buttonClose.heightAnchor.constraint(equalToConstant: Constants.Geometry.Close.height)
])
//
constraints.append(self.scrollView.topAnchor.constraint(equalTo: self.buttonClose.bottomAnchor, constant: Constants.Geometry.closeBetweenList))
} else {
constraints.append(self.scrollView.topAnchor.constraint(equalTo: self.view.topAnchor))
}
constraints.append(contentsOf: [
self.scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
constraints.append(contentsOf: [
self.emptyTextField.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
self.emptyTextField.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
self.emptyTextField.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.5),
self.emptyTextField.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.2)
])
NSLayoutConstraint.activate(constraints)
}
// MARK: - Text height
func calculateHeightFor(string: NSAttributedString) -> CGFloat {
let textStorage = NSTextStorage(attributedString: string)
let textContainer = NSTextContainer(size: NSSize(width: PrivadoConstants.Window.sizeWidth, height: CGFloat.infinity))
let layotmanager = NSLayoutManager()
layotmanager.addTextContainer(textContainer)
textStorage.addLayoutManager(layotmanager)
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = .byWordWrapping
textStorage.font = NSFont(name: PrivadoConstants.Font.semiBold, size: 15)
layotmanager.ensureLayout(for: textContainer)
layotmanager.glyphRange(for: textContainer)
return layotmanager.usedRect(for: textContainer).height
}
// MARK: - On Handlers
@objc
private func onCloseAction() {
self.output.notificationLostClose(self)
}
}
// MARK: - NSTabViewDelegate
extension NotificationController: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard tableView.tableColumns[safe: 0] == tableColumn
, tableColumn.isExist else {
return nil
}
guard let model = self.output.notificationRecord(in: self, at: row) else { return nil }
tableView.makeView(withIdentifier: Constants.cellIdentifier, owner: nil)
let cell = NotificationRecordCell(frame: .zero)
cell.update(with: model)
return cell
}
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
guard let model = self.output.notificationRecord(in: self, at: row) else { return 150 }
let size = self.calculateHeightFor(string: model.markdown)
return size + 70
}
}
// MARK: - NSTableViewDataSource
extension NotificationController: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
self.output.notificationRecordsCount(in: self)
}
}
// MARK: - NotificationControllerInput
extension NotificationController: NotificationControllerInput {
func updateView() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.output.notificationListReady(self)
}
}
func refreshView(exists: Bool) {
if exists {
self.notificationsView.reloadData()
self.emptyTextField.isHidden = true
self.scrollView.isHidden = false
} else {
self.emptyTextField.stringValue = self.output.notificationEmptyMessage(self)
self.emptyTextField.isHidden = false
self.scrollView.isHidden = true
}
}
}
// MARK: - JSNavigationBarViewControllerProvider
extension NotificationController: JSNavigationBarViewControllerProvider {
func navigationBarViewController() -> NSViewController {
if let header = self.header { return header }
let header = self.headerCreationClosure()
self.header = header
return header
}
}