Verified Commit 69c05bd6 authored by asahiocean's avatar asahiocean 🚀
Browse files

Adding buttons and KVO

parent 8949a757
......@@ -37,6 +37,27 @@
3DD5F33725F85B3F00923766 /* DashLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F33625F85B3F00923766 /* DashLine.swift */; };
3DD5F33825F85B3F00923766 /* DashLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F33625F85B3F00923766 /* DashLine.swift */; };
3DD5F33925F85B3F00923766 /* DashLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F33625F85B3F00923766 /* DashLine.swift */; };
3DD5F34625F85CAB00923766 /* buttonsSetup+ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F34525F85CAB00923766 /* buttonsSetup+ViewController.swift */; };
3DD5F34725F85CAB00923766 /* buttonsSetup+ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F34525F85CAB00923766 /* buttonsSetup+ViewController.swift */; };
3DD5F34825F85CAB00923766 /* buttonsSetup+ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F34525F85CAB00923766 /* buttonsSetup+ViewController.swift */; };
3DD5F35325F85D3B00923766 /* trackerButton+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F35225F85D3B00923766 /* trackerButton+Model.swift */; };
3DD5F35425F85D3B00923766 /* trackerButton+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F35225F85D3B00923766 /* trackerButton+Model.swift */; };
3DD5F35525F85D3B00923766 /* trackerButton+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F35225F85D3B00923766 /* trackerButton+Model.swift */; };
3DD5F35E25F85DE100923766 /* buttons+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F35D25F85DE100923766 /* buttons+Model.swift */; };
3DD5F35F25F85DE100923766 /* buttons+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F35D25F85DE100923766 /* buttons+Model.swift */; };
3DD5F36025F85DE100923766 /* buttons+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F35D25F85DE100923766 /* buttons+Model.swift */; };
3DD5F36825F85DFB00923766 /* UIEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F36725F85DFB00923766 /* UIEdgeInsets.swift */; };
3DD5F36925F85DFB00923766 /* UIEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F36725F85DFB00923766 /* UIEdgeInsets.swift */; };
3DD5F36A25F85DFB00923766 /* UIEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F36725F85DFB00923766 /* UIEdgeInsets.swift */; };
3DD5F36F25F85E1E00923766 /* action+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F36E25F85E1E00923766 /* action+Model.swift */; };
3DD5F37025F85E1E00923766 /* action+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F36E25F85E1E00923766 /* action+Model.swift */; };
3DD5F37125F85E1E00923766 /* action+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F36E25F85E1E00923766 /* action+Model.swift */; };
3DD5F37625F85E5700923766 /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F37525F85E5700923766 /* UIButton.swift */; };
3DD5F37725F85E5700923766 /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F37525F85E5700923766 /* UIButton.swift */; };
3DD5F37825F85E5700923766 /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F37525F85E5700923766 /* UIButton.swift */; };
3DD5F37D25F85F0400923766 /* clearButtonSetup+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F37C25F85F0400923766 /* clearButtonSetup+Model.swift */; };
3DD5F37E25F85F0400923766 /* clearButtonSetup+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F37C25F85F0400923766 /* clearButtonSetup+Model.swift */; };
3DD5F37F25F85F0400923766 /* clearButtonSetup+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD5F37C25F85F0400923766 /* clearButtonSetup+Model.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
......@@ -77,6 +98,13 @@
3DD5F32125F85A7100923766 /* MKMarkerAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MKMarkerAnnotationView.swift; sourceTree = "<group>"; };
3DD5F32C25F85B2000923766 /* PinAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinAnnotation.swift; sourceTree = "<group>"; };
3DD5F33625F85B3F00923766 /* DashLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashLine.swift; sourceTree = "<group>"; };
3DD5F34525F85CAB00923766 /* buttonsSetup+ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "buttonsSetup+ViewController.swift"; sourceTree = "<group>"; };
3DD5F35225F85D3B00923766 /* trackerButton+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "trackerButton+Model.swift"; sourceTree = "<group>"; };
3DD5F35D25F85DE100923766 /* buttons+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "buttons+Model.swift"; sourceTree = "<group>"; };
3DD5F36725F85DFB00923766 /* UIEdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIEdgeInsets.swift; sourceTree = "<group>"; };
3DD5F36E25F85E1E00923766 /* action+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "action+Model.swift"; sourceTree = "<group>"; };
3DD5F37525F85E5700923766 /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = "<group>"; };
3DD5F37C25F85F0400923766 /* clearButtonSetup+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "clearButtonSetup+Model.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
......@@ -186,7 +214,7 @@
3DD5F2C725F8555500923766 /* Controllers */ = {
isa = PBXGroup;
children = (
3DD5F26D25F853E900923766 /* ViewController.swift */,
3DD5F34425F85C9300923766 /* ViewController */,
);
path = Controllers;
sourceTree = "<group>";
......@@ -194,6 +222,7 @@
3DD5F2D125F8567C00923766 /* Model */ = {
isa = PBXGroup;
children = (
3DD5F35025F85D2800923766 /* Buttons */,
3DD5F32B25F85B1A00923766 /* PinAnnotation */,
3DD5F2EC25F8585D00923766 /* Model.swift */,
3DD5F33625F85B3F00923766 /* DashLine.swift */,
......@@ -246,6 +275,8 @@
isa = PBXGroup;
children = (
3DD5F32125F85A7100923766 /* MKMarkerAnnotationView.swift */,
3DD5F36725F85DFB00923766 /* UIEdgeInsets.swift */,
3DD5F37525F85E5700923766 /* UIButton.swift */,
);
path = Extensions;
sourceTree = "<group>";
......@@ -258,6 +289,34 @@
path = PinAnnotation;
sourceTree = "<group>";
};
3DD5F34325F85C8900923766 /* Extensions */ = {
isa = PBXGroup;
children = (
3DD5F34525F85CAB00923766 /* buttonsSetup+ViewController.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
3DD5F34425F85C9300923766 /* ViewController */ = {
isa = PBXGroup;
children = (
3DD5F26D25F853E900923766 /* ViewController.swift */,
3DD5F34325F85C8900923766 /* Extensions */,
);
path = ViewController;
sourceTree = "<group>";
};
3DD5F35025F85D2800923766 /* Buttons */ = {
isa = PBXGroup;
children = (
3DD5F35225F85D3B00923766 /* trackerButton+Model.swift */,
3DD5F35D25F85DE100923766 /* buttons+Model.swift */,
3DD5F36E25F85E1E00923766 /* action+Model.swift */,
3DD5F37C25F85F0400923766 /* clearButtonSetup+Model.swift */,
);
path = Buttons;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
......@@ -392,12 +451,19 @@
3DD5F30425F8590700923766 /* PinMarkerView.swift in Sources */,
3DD5F2ED25F8585D00923766 /* Model.swift in Sources */,
3DD5F2F925F858B100923766 /* MapView.swift in Sources */,
3DD5F37625F85E5700923766 /* UIButton.swift in Sources */,
3DD5F33725F85B3F00923766 /* DashLine.swift in Sources */,
3DD5F35E25F85DE100923766 /* buttons+Model.swift in Sources */,
3DD5F26E25F853E900923766 /* ViewController.swift in Sources */,
3DD5F2D425F856A100923766 /* UIResponder+defaultViewController.swift in Sources */,
3DD5F26A25F853E900923766 /* AppDelegate.swift in Sources */,
3DD5F36F25F85E1E00923766 /* action+Model.swift in Sources */,
3DD5F36825F85DFB00923766 /* UIEdgeInsets.swift in Sources */,
3DD5F26C25F853E900923766 /* SceneDelegate.swift in Sources */,
3DD5F35325F85D3B00923766 /* trackerButton+Model.swift in Sources */,
3DD5F32D25F85B2000923766 /* PinAnnotation.swift in Sources */,
3DD5F34625F85CAB00923766 /* buttonsSetup+ViewController.swift in Sources */,
3DD5F37D25F85F0400923766 /* clearButtonSetup+Model.swift in Sources */,
3DD5F32225F85A7100923766 /* MKMarkerAnnotationView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
......@@ -408,11 +474,18 @@
files = (
3DD5F30525F8590700923766 /* PinMarkerView.swift in Sources */,
3DD5F33825F85B3F00923766 /* DashLine.swift in Sources */,
3DD5F36925F85DFB00923766 /* UIEdgeInsets.swift in Sources */,
3DD5F37025F85E1E00923766 /* action+Model.swift in Sources */,
3DD5F2EE25F8585D00923766 /* Model.swift in Sources */,
3DD5F2FA25F858B100923766 /* MapView.swift in Sources */,
3DD5F32325F85A7100923766 /* MKMarkerAnnotationView.swift in Sources */,
3DD5F28125F853EB00923766 /* SZRCAITests.swift in Sources */,
3DD5F35F25F85DE100923766 /* buttons+Model.swift in Sources */,
3DD5F37E25F85F0400923766 /* clearButtonSetup+Model.swift in Sources */,
3DD5F35425F85D3B00923766 /* trackerButton+Model.swift in Sources */,
3DD5F2D525F856A100923766 /* UIResponder+defaultViewController.swift in Sources */,
3DD5F37725F85E5700923766 /* UIButton.swift in Sources */,
3DD5F34725F85CAB00923766 /* buttonsSetup+ViewController.swift in Sources */,
3DD5F32E25F85B2000923766 /* PinAnnotation.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
......@@ -423,11 +496,18 @@
files = (
3DD5F30625F8590700923766 /* PinMarkerView.swift in Sources */,
3DD5F33925F85B3F00923766 /* DashLine.swift in Sources */,
3DD5F36A25F85DFB00923766 /* UIEdgeInsets.swift in Sources */,
3DD5F37125F85E1E00923766 /* action+Model.swift in Sources */,
3DD5F2EF25F8585D00923766 /* Model.swift in Sources */,
3DD5F2FB25F858B100923766 /* MapView.swift in Sources */,
3DD5F32425F85A7100923766 /* MKMarkerAnnotationView.swift in Sources */,
3DD5F28C25F853EB00923766 /* SZRCAIUITests.swift in Sources */,
3DD5F36025F85DE100923766 /* buttons+Model.swift in Sources */,
3DD5F37F25F85F0400923766 /* clearButtonSetup+Model.swift in Sources */,
3DD5F35525F85D3B00923766 /* trackerButton+Model.swift in Sources */,
3DD5F2D625F856A100923766 /* UIResponder+defaultViewController.swift in Sources */,
3DD5F37825F85E5700923766 /* UIButton.swift in Sources */,
3DD5F34825F85CAB00923766 /* buttonsSetup+ViewController.swift in Sources */,
3DD5F32F25F85B2000923766 /* PinAnnotation.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
......
import UIKit
extension ViewController {
internal func buttonsSetup() {
guard let tracker = mapView.model.trackerButton(mv: mapView) else { fatalError("Solve the button problem") }
view.addSubview(tracker)
buttonKVO = mapView.model.observe(\.mainButton, options: [.initial,.new], changeHandler: { obj, change in
guard let config = change.newValue, let button = config else { return }
self.mainButton = button // Monitoring the state of the button through the KVO
})
view.addSubview(mainButton)
if let button = mapView.model.clearButton {
DispatchQueue.main.async {
// For an acceptable location of the button on the screen
button.center.x = tracker.center.x
button.center.y = self.mainButton.center.y
}
clearButton = button
view.addSubview(clearButton)
}
}
internal func pinsKVOFunc() {
enum state: CGFloat { case show = 0.9, hide = 1 }
// Simple animation of button positions
func buttonsAnimator(state: state) {
let value = view.bounds.maxY * state.rawValue
UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseInOut], animations: { [weak self] in
// - - Main button - -
self?.mainButton.frame.origin.y = value
self?.mainButton.layoutIfNeeded()
// - - Clear button - -
self?.clearButton.frame.origin.y = value
self?.clearButton.layoutIfNeeded()
})
}
pinsKVO = mapView.model.observe(\.pins, options: .new, changeHandler: { obj, change in
guard let pins = change.newValue else { return }
//MARK: После добавления хотя бы 3 точек должны появляться кнопки
if pins.count > 2 {
buttonsAnimator(state: .show)
} else {
buttonsAnimator(state: .hide)
}
})
}
}
......@@ -3,7 +3,14 @@ import MapKit
// Тестовое задание «СЗ РЦАИ» iOS Developer
class ViewController: UIViewController, MKMapViewDelegate {
class ViewController: UIViewController, MKMapViewDelegate, ModelDelegate {
func deselectPin(pin: PinAnnotation) {
}
func modelButtonExecute(sender: UIButton) {
}
var mapView: MapView!
......@@ -19,5 +26,10 @@ class ViewController: UIViewController, MKMapViewDelegate {
mapView = .init(frame: view.frame)
mapView.delegate = self
view.addSubview(mapView)
mapView.model.delegate = self
buttonsSetup()
pinsKVOFunc()
}
}
import UIKit
extension UIButton {
func shake() {
let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
animation.timingFunction = .init(name: .linear)
animation.duration = 0.5
animation.values = [-15, 15, -15, 15, -5, 5, -2.5, 2.5, 0]
layer.add(animation, forKey: "shake")
}
}
import UIKit
extension UIEdgeInsets {
init(equal inset: CGFloat) {
self.init(top: inset, left: inset, bottom: inset, right: inset)
}
}
import UIKit
/// - Tag: action
extension Model {
/// [Method](x-source-tag://action) that determines what will happen when the button is pressed
/// - Parameter sender: where will you need to get `.tag`
@objc func buttonAction(_ sender: UIButton?) {
guard let button = sender else { return }
// MARK: Delegate for further actions
delegate?.modelButtonExecute(sender: button)
// MARK: Animate + configuration switch
func switching(next type: buttonTag) {
DispatchQueue.main.async {
button.transform = .init(scaleX: 0.9, y: 0.9)
UIView.animate(withDuration: 0.25, animations: {
button.transform = .identity
}, completion: { [self] _ in
buttonAnimator(b: button, {
buttonConfig(type: type)
})
})
}
}
func buttonAnimator(b button: UIButton, _ completion: (() -> Void)? = nil) {
// simple animator for positioning button on the screen
UIView.animate(withDuration: 0.25, delay: 0.25, options: [.curveEaseInOut], animations: {
button.frame.origin.y += 200
button.layoutIfNeeded()
}, completion: { _ in
completion?()
UIView.animate(withDuration: 0.25, animations: {
button.frame.origin.y -= 200
button.layoutIfNeeded()
})
})
}
func check(pin point: inout PinAnnotation?, cl: UIColor?, _ closure: @autoclosure () -> ()) {
guard let pin = point else {
button.shake()
return
}
if let color = cl { pin.markerColor = color }
delegate?.deselectPin(pin: pin)
route.append(pin)
point = nil // deleting the value the selected point
closure()
}
switch button.tag {
case Model.buttonTag.graphs.rawValue:
guard graphsBuilt else { return }
switching(next: .start)
case Model.buttonTag.start.rawValue:
check(pin: &selected, cl: #colorLiteral(red: 0.1960784314, green: 0.8431372549, blue: 0.2941176471, alpha: 1), {
switching(next: .finish)
}())
case Model.buttonTag.finish.rawValue:
check(pin: &selected, cl: #colorLiteral(red: 0.03921568627, green: 0.5176470588, blue: 1, alpha: 1), {
switching(next: .routecalc)
}())
case Model.buttonTag.clear.rawValue:
switching(next: .graphs)
default: break
}
}
}
import UIKit
extension Model {
enum buttonTag: Int {
case clear
case graphs
case start
case finish
case routecalc
}
//MARK: - Main Button Setup
internal func mainButtonSetup() {
let button = UIButton(frame: .zero)
button.contentMode = .center
button.backgroundColor = .white
button.contentEdgeInsets = UIEdgeInsets(equal: 15)
button.titleLabel?.adjustsFontSizeToFitWidth = true
DispatchQueue.main.async {
let bounds = UIScreen.main.bounds
button.frame.origin.y = bounds.maxY
button.center.x = bounds.midX
}
self.mainButton = button
buttonConfig(type: .graphs)
}
//MARK: - Main Button Config
internal func buttonConfig(type: buttonTag) {
guard let button = mainButton else { return }
button.tag = type.rawValue
button.addTarget(self, action: #selector(buttonAction(_:)), for: .touchUpInside)
switch type {
case .graphs:
self.graphsBuilt = false
button.setTitle("Построить графы", for: .normal)
button.sizeToFit()
button.layer.cornerRadius = button.bounds.height / 3
let gradient = CAGradientLayer()
gradient.frame = button.bounds
gradient.colors = [#colorLiteral(red: 0.8165418145, green: 0.250980407, blue: 0.7970164812, alpha: 1).cgColor, #colorLiteral(red: 0.2392156869, green: 0.4640353456, blue: 0.9686274529, alpha: 1).cgColor]
gradient.startPoint = .init(x: 0, y: 0.5)
gradient.endPoint = .init(x: 1.0, y: 0.5)
gradient.cornerRadius = button.layer.cornerRadius
button.layer.insertSublayer(gradient, at: 0)
case .start:
DispatchQueue.main.async {
button.layer.sublayers?.remove(at: 0)
}
button.backgroundColor = .systemGreen
button.setTitle("Выберите старт", for: .normal)
case .finish:
button.backgroundColor = .systemBlue
button.setTitle("Выберите финиш", for: .normal)
case .routecalc:
button.backgroundColor = .systemRed
button.setTitle("В путь!", for: .normal)
default: break
}
self.mainButtonTitle = button.titleLabel?.text ?? ""
}
}
import UIKit
extension Model {
func clearButtonSetup() {
let button = UIButton()
button.tag = Model.buttonTag.clear.rawValue
let h = mainButton?.bounds.height ?? .zero
button.frame.size = .init(width: h, height: h)
button.setImage(#imageLiteral(resourceName: "clean"), for: .normal)
button.layer.cornerRadius = button.bounds.width / 2
button.backgroundColor = .white
button.addTarget(self, action: #selector(buttonAction(_:)), for: .touchUpInside)
clearButton = button
}
}
import UIKit
import MapKit
extension Model {
func trackerButton(mv mapView: MKMapView?) -> UIButton? {
guard let mapView = mapView else { return nil }
let sqroot = mapView.frame.width.squareRoot()
// MARK: Map Kit Tracking Button
let button = MKUserTrackingBarButtonItem(mapView: mapView)
button.customView?.tintColor = .systemBlue
let size = CGSize(width: sqroot * 2.5, height: sqroot * 2.5)
button.customView?.frame.size = size
guard let customView = button.customView else { return nil }
// MARK: Toolbar
// Used to display the states of MKUserTrackingBarButtonItem
let toolbar = UIToolbar(frame: customView.frame)
toolbar.barTintColor = .white
toolbar.isTranslucent = true
let flex = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
toolbar.items = [flex, button, flex]
let view = UIButton(frame: toolbar.frame)
view.layer.cornerRadius = view.bounds.width / 2
view.layer.borderWidth = 2
view.layer.borderColor = toolbar.tintColor.cgColor
view.layer.masksToBounds = true
view.center.y = mapView.frame.midY
view.frame.origin.x = mapView.frame.maxX - (view.bounds.width + sqroot)
view.addSubview(toolbar)
self.trackingButton = view
return trackingButton
}
}
import Foundation
import UIKit
protocol ModelDelegate: class {
func deselectPin(pin: PinAnnotation)
func modelButtonExecute(sender: UIButton)
}
@objc class Model: NSObject {
public static let shared = Model()
weak var trackingButton: UIButton!
@objc dynamic var mainButton: UIButton?
@objc dynamic var clearButton: UIButton?
weak var delegate: ModelDelegate?
@objc dynamic var graphsBuilt: Bool = false
@objc dynamic var pins: [PinAnnotation] = []
@objc dynamic var selected: PinAnnotation?
public var route: [PinAnnotation] = []
var selectedPinsKVO: NSKeyValueObservation!
var mainButtonTitle: String = "" // To save the button name
fileprivate override init() {
super.init()
mainButtonSetup()
clearButtonSetup()
selectedPinsKVO = observe(\.selected, options: .new, changeHandler: { [self] model, change in
guard let newValue = change.newValue else { return }
if newValue != nil {
mainButton?.setTitle("OK", for: .normal)
} else {
mainButton?.setTitle(mainButtonTitle, for: .normal)
}
})
}
}
{
"images" : [
{
"filename" : "clean.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
{
"images" : [
{
"filename" : "trash.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment