Files

321 lines
13 KiB
Swift

//
// CityServerCell.swift
// PrivadoVPN
//
// Created by Juraldinio on 3/15/21.
// Copyright © 2021 Privado LLC. All rights reserved.
//
import Foundation
protocol CityServerDelegate: AnyObject {
func location(isFavourite: Bool, for view: NSView)
}
final class CityServerCell: View {
// swiftlint:disable nesting
enum Constants {
static let height: CGFloat = 41
enum Images {
static let location = "pinLocation"
static let latencyLow = "Latency-low"
static let latencyMedium = "Latency-medium"
static let latencyHigh = "Latency-high"
//
static let pinLocation = "pinLocation"
static let pinLocationHover = "pinLocationHover"
//
static let favouriteFull = "favourite.full"
static let favouriteEmpty = "favourite.empty"
//
static let premium = "icon.premium"
}
enum Geometry {
static let premiumLeading: CGFloat = 17
static let premiumSize = CGSize(width: 14, height: 12)
static let locationLeading: CGFloat = 57
static let locationSize = CGSize(width: 13, height: 18)
static let locationBetweenTitle: CGFloat = 23
static let titleHeight: CGFloat = 20
static let titleBetweenLatency: CGFloat = 23
static let latencySize = CGSize(width: 15, height: 20)
static let latencyTrailing: CGFloat = 58
static let bottomLineHeight: CGFloat = 1.0
static let favouriteSize = CGSize(width: 18, height: 18)
}
enum Font {
enum Name {
static let title = PrivadoConstants.Font.medium
}
enum Size {
static let title: CGFloat = 13.0
}
}
enum Color {
static let title = NSColor(red: 218, green: 219, blue: 243)
static let bottomLine = NSColor(red: 25, green: 33, blue: 71)
//
static let hoverTitle = NSColor.white
static let hoverBackground = NSColor(red: 64, green: 71, blue: 110)
}
}
// swiftlint:enable nesting
private let locationImage = ImageView()
private let titleField = DynamicTextField(frame: .zero)
private let latencyImage = ImageView()
private let favouriteImage = ImageView()
private let premiumImage = ImageView()
private let bottomLine = NSView()
private var isFavourite: Bool
private var isLast: Bool
private var backgroundLayer = CAShapeLayer()
private var backgroundColor: NSColor?
private var setupStrokeBackground: Bool
private weak var delegate: CityServerDelegate?
// MARK: - Initializers
init(city: String, latency: ServerListCityRecord.LatencyType, isFavourite: Bool, setupStrokeBackground: Bool, showPremiumIcon: Bool, delegate: CityServerDelegate?, isLast: Bool, output: ServerCellOutput) {
self.isFavourite = isFavourite
self.isLast = isLast
self.setupStrokeBackground = setupStrokeBackground
super.init(frame: .zero)
output.scrollEmitter.addReaction { [weak self] isScrolling in
if isScrolling {
self?.backgroundLayer.fillColor = NSColor.clear.cgColor
self?.backgroundLayer.strokeColor = NSColor.clear.cgColor
}
return self.isExist
}
self.delegate = delegate
self.isHoverEnabled = true
self.useHandCursor = true
self.backgroundColor = showPremiumIcon && !isFavourite ? NSColor(red: 69, green: 77, blue: 181) : .clear
self.viewBackgroundColor = self.backgroundColor
self.wantsLayer = true
self.layer?.masksToBounds = false
self.layer?.addSublayer(self.strokeLayer)
self.layer?.addSublayer(self.backgroundLayer)
self.configure(city: city, latency: latency, isFavourite: isFavourite, showPremiumIcon: showPremiumIcon)
}
// MARK: - LifeCycle
override func updateTrackingAreas() {
super.updateTrackingAreas()
for trackingArea in self.trackingAreas {
self.removeTrackingArea(trackingArea)
}
let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways]
let trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil)
self.addTrackingArea(trackingArea)
}
// override func hitTest(_ point: NSPoint) -> NSView? { nil }
override func onMouseHover(_ hover: Bool) {
self.backgroundLayer.fillColor = hover ? NSColor(red: 43, green: 50, blue: 137).cgColor : NSColor.clear.cgColor
self.backgroundLayer.strokeColor = hover ? NSColor(red: 105, green: 112, blue: 208).cgColor : NSColor.clear.cgColor
}
var strokeLayer: CAShapeLayer = CAShapeLayer()
var cornerRadius: CGFloat = 6
func setupStrokeLayer() {
let path = NSBezierPath()
let rect = CGRect(x: self.bounds.minX + 5, y: self.bounds.minY, width: self.bounds.width - 10, height: self.bounds.height)
let topRight = NSPoint(x: rect.maxX, y: rect.maxY)
let topLeft = NSPoint(x: rect.minX, y: rect.maxY)
let bottomLeft = NSPoint(x: rect.minX, y: rect.minY)
let bottomRight = NSPoint(x: rect.maxX, y: rect.minY)
path.move(to: bottomLeft)
path.line(to: topLeft)
path.line(to: topRight)
path.line(to: bottomRight)
path.close()
self.strokeLayer.path = path.CGPath
self.strokeLayer.lineWidth = 1
self.strokeLayer.strokeColor = .clear
self.strokeLayer.fillRule = .evenOdd
self.strokeLayer.fillColor = NSColor(red: 58, green: 66, blue: 183).cgColor
}
func setupCurvedStrokeLayer() {
let path = NSBezierPath()
let rect = CGRect(x: self.bounds.minX + 5, y: self.bounds.minY + 5, width: self.bounds.width - 10, height: self.bounds.height - 5)
let topRight = NSPoint(x: rect.maxX, y: rect.maxY)
let topLeft = NSPoint(x: rect.minX, y: rect.maxY)
let bottomLeft = NSPoint(x: rect.minX, y: rect.minY)
let bottomRight = NSPoint(x: rect.maxX, y: rect.minY)
path.move(to: topRight)
path.line(to: CGPoint(x: rect.maxX, y: rect.minY + cornerRadius))
path.appendArc(from: bottomRight, to: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY), radius: cornerRadius)
path.line(to: CGPoint(x: rect.minX + cornerRadius, y: rect.minY))
path.appendArc(from: bottomLeft, to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius), radius: cornerRadius)
path.line(to: topLeft)
path.close()
self.strokeLayer.path = path.CGPath
self.strokeLayer.lineWidth = 1
self.strokeLayer.strokeColor = .clear
self.strokeLayer.fillRule = .evenOdd
self.strokeLayer.fillColor = NSColor(red: 58, green: 66, blue: 183).cgColor
}
func setupBackgroundLayer() {
let rect = CGRect(x: self.bounds.minX + 10, y: self.bounds.minY + 7, width: self.bounds.width - 15, height: self.bounds.height - 10)
let path = NSBezierPath(roundedRect: rect, xRadius: 6, yRadius: 6)
self.backgroundLayer.path = path.CGPath
self.backgroundLayer.lineWidth = 1
self.backgroundLayer.strokeColor = .clear
self.backgroundLayer.fillRule = .evenOdd
self.backgroundLayer.fillColor = NSColor.clear.cgColor
}
override func layout() {
super.layout()
self.setupBackgroundLayer()
guard self.setupStrokeBackground else { return }
if self.isLast {
self.setupCurvedStrokeLayer()
} else {
self.setupStrokeLayer()
}
}
// MARK: - Lifecycle View
override func addSubviews() {
self.addSubview(self.locationImage)
self.addSubview(self.titleField)
self.addSubview(self.latencyImage)
self.addSubview(self.bottomLine)
self.addSubview(self.favouriteImage)
self.addSubview(self.premiumImage)
// titleField
self.titleField.translatesAutoresizingMaskIntoConstraints = false
self.titleField.isEditable = false
self.titleField.isSelectable = false
self.titleField.usesSingleLineMode = true
self.titleField.isBezeled = false
self.titleField.drawsBackground = false
self.titleField.focusRingType = .none
self.titleField.textColor = Constants.Color.title
self.titleField.font = NSFont(name: Constants.Font.Name.title, size: Constants.Font.Size.title)
self.titleField.fontName = Constants.Font.Name.title
self.bottomLine.translatesAutoresizingMaskIntoConstraints = false
self.favouriteImage.translatesAutoresizingMaskIntoConstraints = false
self.favouriteImage.useHandCursor = true
self.favouriteImage.target = self
self.favouriteImage.action = #selector(self.favouriteImageTapped)
self.premiumImage.translatesAutoresizingMaskIntoConstraints = false
self.premiumImage.image = NSImage(named: Constants.Images.premium)
self.premiumImage.isHidden = true
}
@objc
private func favouriteImageTapped() {
self.delegate?.location(isFavourite: !self.isFavourite, for: self)
self.isFavourite = !self.isFavourite
self.favouriteImage.image = self.isFavourite ? NSImage(named: Constants.Images.favouriteFull) : NSImage(named: Constants.Images.favouriteEmpty)
}
override func addConstraints() {
NSLayoutConstraint.activate([
self.premiumImage.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: Constants.Geometry.premiumLeading),
self.premiumImage.centerYAnchor.constraint(equalTo: self.centerYAnchor),
self.premiumImage.widthAnchor.constraint(equalToConstant: Constants.Geometry.premiumSize.width),
self.premiumImage.heightAnchor.constraint(equalToConstant: Constants.Geometry.premiumSize.height),
//
self.locationImage.widthAnchor.constraint(equalToConstant: Constants.Geometry.locationSize.width),
self.locationImage.heightAnchor.constraint(equalToConstant: Constants.Geometry.locationSize.height),
self.locationImage.centerYAnchor.constraint(equalTo: self.centerYAnchor),
self.locationImage.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: Constants.Geometry.locationLeading),
//
self.titleField.leadingAnchor.constraint(equalTo: self.locationImage.trailingAnchor, constant: Constants.Geometry.locationBetweenTitle),
self.titleField.centerYAnchor.constraint(equalTo: self.centerYAnchor),
self.titleField.heightAnchor.constraint(equalToConstant: Constants.Geometry.titleHeight),
//
self.latencyImage.widthAnchor.constraint(equalToConstant: Constants.Geometry.latencySize.width),
self.latencyImage.heightAnchor.constraint(equalToConstant: Constants.Geometry.latencySize.height),
self.latencyImage.leadingAnchor.constraint(greaterThanOrEqualTo: self.titleField.trailingAnchor,
constant: Constants.Geometry.titleBetweenLatency),
self.latencyImage.centerYAnchor.constraint(equalTo: self.centerYAnchor),
self.latencyImage.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -Constants.Geometry.latencyTrailing),
//
self.favouriteImage.widthAnchor.constraint(equalToConstant: Constants.Geometry.favouriteSize.width),
self.favouriteImage.heightAnchor.constraint(equalToConstant: Constants.Geometry.favouriteSize.height),
self.favouriteImage.centerYAnchor.constraint(equalTo: self.centerYAnchor),
self.favouriteImage.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -17),
//
self.bottomLine.leadingAnchor.constraint(equalTo: self.leadingAnchor),
self.bottomLine.trailingAnchor.constraint(equalTo: self.trailingAnchor),
self.bottomLine.bottomAnchor.constraint(equalTo: self.bottomAnchor),
self.bottomLine.heightAnchor.constraint(equalToConstant: Constants.Geometry.bottomLineHeight)
])
}
// MARK: - Private
private func configure(city: String, latency: ServerListCityRecord.LatencyType, isFavourite: Bool, showPremiumIcon: Bool) {
self.locationImage.image = NSImage(named: Constants.Images.pinLocation)
self.titleField.stringValue = city
let imageName: String
switch latency {
case .low: imageName = Constants.Images.latencyLow
case .medium: imageName = Constants.Images.latencyMedium
case .high: imageName = Constants.Images.latencyHigh
}
self.latencyImage.image = NSImage(named: imageName)
self.favouriteImage.image = isFavourite ? NSImage(named: Constants.Images.favouriteFull) : NSImage(named: Constants.Images.favouriteEmpty)
self.premiumImage.isHidden = !showPremiumIcon
}
}