285 lines
10 KiB
Swift
285 lines
10 KiB
Swift
import PromiseKit
|
|
import Shared
|
|
import UIKit
|
|
|
|
class OnboardingManualURLViewController: UIViewController, UITextFieldDelegate {
|
|
private let urlField = UITextField()
|
|
private var connectButton: UIButton?
|
|
private var connectLoading: UIActivityIndicatorView?
|
|
private var scrollView: UIScrollView?
|
|
private var bottomSpacer: UIView?
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
urlField.becomeFirstResponder()
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
view.backgroundColor = .systemBackground
|
|
|
|
let (scrollView, stackView, equalSpacers) = UIView.contentStackView(in: view, scrolling: true)
|
|
self.scrollView = scrollView
|
|
|
|
stackView.addArrangedSubview(with(UILabel()) {
|
|
$0.text = L10n.Onboarding.ManualSetup.title
|
|
Current.style.onboardingTitle($0)
|
|
})
|
|
|
|
stackView.addArrangedSubview(with(UILabel()) {
|
|
$0.text = L10n.Onboarding.ManualSetup.description
|
|
$0.font = .preferredFont(forTextStyle: .body)
|
|
$0.textColor = Asset.Colors.haPrimary.color
|
|
$0.textAlignment = .natural
|
|
$0.numberOfLines = 0
|
|
})
|
|
|
|
stackView.addArrangedSubview(with(urlField) {
|
|
$0.delegate = self
|
|
$0.backgroundColor = UIColor(white: 0, alpha: 0.12)
|
|
$0.borderStyle = .roundedRect
|
|
$0.placeholder = "http://homeassistant.local:8123"
|
|
$0.textContentType = .URL
|
|
$0.keyboardType = .URL
|
|
$0.autocapitalizationType = .none
|
|
$0.autocorrectionType = .no
|
|
$0.spellCheckingType = .no
|
|
$0.smartDashesType = .no
|
|
$0.smartQuotesType = .no
|
|
$0.keyboardAppearance = .dark
|
|
$0.returnKeyType = .continue
|
|
$0.enablesReturnKeyAutomatically = true
|
|
$0.clearButtonMode = .whileEditing
|
|
|
|
let font = UIFont.preferredFont(forTextStyle: .body)
|
|
$0.font = font
|
|
$0.heightAnchor.constraint(greaterThanOrEqualToConstant: font.lineHeight * 2.5)
|
|
.isActive = true
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(updateConnectButton),
|
|
name: UITextField.textDidChangeNotification,
|
|
object: $0
|
|
)
|
|
})
|
|
|
|
switch traitCollection.userInterfaceIdiom {
|
|
case .pad, .mac:
|
|
urlField.widthAnchor.constraint(equalTo: stackView.readableContentGuide.widthAnchor)
|
|
.isActive = true
|
|
default:
|
|
urlField.widthAnchor.constraint(equalTo: stackView.layoutMarginsGuide.widthAnchor)
|
|
.isActive = true
|
|
}
|
|
|
|
let button = with(UIButton(type: .custom)) {
|
|
$0.setTitle(L10n.Onboarding.ManualSetup.connect, for: .normal)
|
|
$0.addTarget(self, action: #selector(connectTapped(_:)), for: .touchUpInside)
|
|
Current.style.onboardingButtonPrimary($0)
|
|
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
$0.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
}
|
|
let loading: UIActivityIndicatorView = {
|
|
let indicator: UIActivityIndicatorView
|
|
indicator = UIActivityIndicatorView(style: .medium)
|
|
|
|
indicator.hidesWhenStopped = true
|
|
indicator.color = button.titleColor(for: .normal)
|
|
return indicator
|
|
}()
|
|
|
|
connectButton = button
|
|
connectLoading = loading
|
|
|
|
with(button) {
|
|
$0.addSubview(loading)
|
|
loading.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
loading.centerYAnchor.constraint(equalTo: $0.centerYAnchor),
|
|
loading.trailingAnchor.constraint(equalTo: $0.trailingAnchor, constant: -16),
|
|
])
|
|
}
|
|
|
|
if Current.isCatalyst {
|
|
// iPad and iPhone unconditionally show the input view, but mac never does
|
|
stackView.addArrangedSubview(button)
|
|
} else {
|
|
urlField.inputAccessoryView = with(InputAccessoryView()) {
|
|
$0.directionalLayoutMargins = stackView.directionalLayoutMargins
|
|
$0.contentView = button
|
|
}
|
|
}
|
|
|
|
stackView.addArrangedSubview(with(equalSpacers.next()) {
|
|
bottomSpacer = $0
|
|
})
|
|
|
|
updateConnectButton()
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(keyboardWillChangeFrame(_:)),
|
|
name: UIResponder.keyboardWillChangeFrameNotification,
|
|
object: nil
|
|
)
|
|
}
|
|
|
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
connect()
|
|
return false
|
|
}
|
|
|
|
func textField(
|
|
_ textField: UITextField,
|
|
shouldChangeCharactersIn range: NSRange,
|
|
replacementString string: String
|
|
) -> Bool {
|
|
!isConnecting
|
|
}
|
|
|
|
@objc private func connectTapped(_ sender: UIButton) {
|
|
Current.Log.verbose("Connect button tapped")
|
|
connect()
|
|
}
|
|
|
|
@objc private func updateConnectButton() {
|
|
connectButton?.isEnabled = urlField.text?.isEmpty == false
|
|
}
|
|
|
|
private var isConnecting: Bool = false {
|
|
didSet {
|
|
if isConnecting {
|
|
connectLoading?.startAnimating()
|
|
connectButton?.isUserInteractionEnabled = false
|
|
} else {
|
|
connectLoading?.stopAnimating()
|
|
connectButton?.isUserInteractionEnabled = true
|
|
}
|
|
}
|
|
}
|
|
|
|
private func connect() {
|
|
guard !isConnecting else { return }
|
|
|
|
isConnecting = true
|
|
|
|
let authentication = OnboardingAuth()
|
|
|
|
firstly {
|
|
validatedURL(from: urlField.text)
|
|
}.recover { [self] error -> Promise<URL> in
|
|
Current.Log.error("Couldn't make a URL: \(error)")
|
|
|
|
let alert = UIAlertController(
|
|
title: L10n.Onboarding.ManualSetup.CouldntMakeUrl.title,
|
|
message: L10n.Onboarding.ManualSetup.CouldntMakeUrl.message(urlField.text ?? ""),
|
|
preferredStyle: .alert
|
|
)
|
|
alert.addAction(UIAlertAction(title: L10n.okLabel, style: UIAlertAction.Style.default, handler: nil))
|
|
present(alert, animated: true, completion: nil)
|
|
|
|
return .init(error: PMKError.cancelled)
|
|
}.then { [self] (url: URL) -> Promise<Server> in
|
|
let instance = DiscoveredHomeAssistant(manualURL: url)
|
|
return authentication.authenticate(to: instance, sender: self)
|
|
}.ensure { [self] in
|
|
isConnecting = false
|
|
}.done { [self] server in
|
|
show(authentication.successController(server: server), sender: self)
|
|
}.catch { [self] error in
|
|
show(authentication.failureController(error: error), sender: self)
|
|
}
|
|
}
|
|
|
|
enum ValidateError: Error, CancellableError {
|
|
case emptyString
|
|
case cannotConvert
|
|
case invalidScheme
|
|
case noSchemeCancelled
|
|
|
|
var isCancelled: Bool {
|
|
switch self {
|
|
case .emptyString, .cannotConvert, .invalidScheme:
|
|
return false
|
|
case .noSchemeCancelled:
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
private func promptForScheme(for string: String) -> Promise<String> {
|
|
Promise { seal in
|
|
let alert = UIAlertController(
|
|
title: L10n.Onboarding.ManualSetup.NoScheme.title,
|
|
message: L10n.Onboarding.ManualSetup.NoScheme.message,
|
|
preferredStyle: .actionSheet
|
|
)
|
|
|
|
with(alert.popoverPresentationController) {
|
|
$0?.sourceView = urlField
|
|
$0?.sourceRect = urlField.bounds
|
|
}
|
|
|
|
func action(for scheme: String) -> UIAlertAction {
|
|
UIAlertAction(title: scheme, style: .default, handler: { _ in
|
|
seal.fulfill(scheme + string)
|
|
})
|
|
}
|
|
|
|
alert.addAction(action(for: "http://"))
|
|
alert.addAction(action(for: "https://"))
|
|
alert.addAction(UIAlertAction(title: L10n.cancelLabel, style: .cancel, handler: { _ in
|
|
seal.reject(ValidateError.noSchemeCancelled)
|
|
}))
|
|
self.present(alert, animated: true, completion: nil)
|
|
}
|
|
}
|
|
|
|
private func validatedURL(from inputString: String?) -> Promise<URL> {
|
|
let start = Promise<String?>.value(inputString)
|
|
|
|
return start
|
|
.map { (string: String?) -> String in
|
|
if let trimmed = string?.trimmingCharacters(in: .whitespacesAndNewlines), trimmed.isEmpty == false {
|
|
return trimmed
|
|
} else {
|
|
throw ValidateError.emptyString
|
|
}
|
|
}.then { (string: String) -> Promise<String> in
|
|
if string.starts(with: "http://") || string.starts(with: "https://") {
|
|
return .value(string)
|
|
} else if string.contains("://") == false {
|
|
return self.promptForScheme(for: string)
|
|
} else {
|
|
throw ValidateError.invalidScheme
|
|
}
|
|
}.map { (string: String) -> URL in
|
|
if let url = URL(string: string) {
|
|
return url
|
|
} else {
|
|
throw ValidateError.cannotConvert
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func keyboardWillChangeFrame(_ note: Notification) {
|
|
guard let scrollView,
|
|
let frameValue = note.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
|
|
return
|
|
}
|
|
|
|
UIView.performWithoutAnimation {
|
|
view.layoutIfNeeded()
|
|
}
|
|
|
|
let intersectHeight = view.convert(frameValue.cgRectValue, from: nil).intersection(scrollView.frame).height
|
|
let insetHeight = max(0, intersectHeight - (bottomSpacer?.bounds.height ?? 0))
|
|
|
|
scrollView.contentInset.bottom = insetHeight
|
|
scrollView.verticalScrollIndicatorInsets.bottom = insetHeight
|
|
}
|
|
}
|