Files
iOS/Sources/App/Utilities/WebSocketStatusRow.swift
Zac West 5c104f76e9 Multi-server (#1906)
## Summary
Most, but not all, of the changes necessary to support multi-server throughout the app and all its features.

## Screenshots
| Light | Dark |
| ----- | ---- |
| ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 52 24](https://user-images.githubusercontent.com/74188/143670011-9b9905ac-1b5b-4a82-b9f3-1490465c4ec5.png) | ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 52 26](https://user-images.githubusercontent.com/74188/143670012-0080230a-8f68-4f34-9691-db9f5e825a83.png) |
| ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 52 30](https://user-images.githubusercontent.com/74188/143670015-ceeac558-e039-4639-a186-b5001ab418b8.png) | ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 52 29](https://user-images.githubusercontent.com/74188/143670016-d72bb69d-83f5-4197-a742-59d208467258.png) |
| ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 52 47](https://user-images.githubusercontent.com/74188/143670021-6c90c40f-c2f1-4a33-aad9-da6626e99d9d.png) | ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 52 45](https://user-images.githubusercontent.com/74188/143670024-e99de69d-61d8-4e12-be73-a172242806a0.png) |
| ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 53 05](https://user-images.githubusercontent.com/74188/143670033-1a41ac7e-d4d1-458b-974e-2efdaf8e2288.png) | ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 53 03](https://user-images.githubusercontent.com/74188/143670049-baf4db64-64db-4bfb-88cf-4930f9e5661b.png) |
| ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 53 21](https://user-images.githubusercontent.com/74188/143670053-7ec794f1-857c-4ef6-a92a-5318e90ac6b6.png) | ![Simulator Screen Shot - iPhone 13 Pro - 2021-11-26 at 21 53 19](https://user-images.githubusercontent.com/74188/143670056-a6a5207c-3bba-49fc-b5c6-fc6fa8141f9c.png) |

## Any other notes
- Encapsulates all connectivity, token & server-specific knowledge in a Server model object which gets passed around.
- Updates various places throughout the app to know about and use Server rather than accessing said information through non-server-specific methods.
- Visually requests/notes server in places where it's ambiguous. For example, the Open Page widget will gain a subtitle if multiple servers are set up.
- Allows switching which server is shown in the WebViews. Note that this doesn't take into account multi-window support on iPad/macOS yet.

Most things will migrate successfully however adding an additional server causes things like Shortcuts to start erroring requiring you specify which to use in the particular Shortcut.

Future work necessary:
- Model objects currently clobber each other if their identifiers match. For example, both servers having a zone named `home` means one of them wins the fight for which is known to the app.
- Being remotely logged out on any account causes the app to require onboarding again, when instead it should only do that if the last known server is logged out.
2021-11-27 12:33:46 -08:00

171 lines
5.9 KiB
Swift

import Eureka
import HAKit
import Shared
public final class WebSocketStatusCell: Cell<HAConnectionState>, CellType {
public let activityIndicator = with(UIActivityIndicatorView()) {
if #available(iOS 13, *) {
$0.style = .medium
} else {
$0.style = .gray
}
}
override public func update() {
super.update()
switch (row as? WebSocketStatusRow)?.displayStyle ?? .default {
case .default, .alert:
activityIndicator.removeFromSuperview()
case .loading:
if activityIndicator.superview == nil {
contentView.addSubview(activityIndicator)
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// not constraining to top/bottom because that causes the cell to shrink compared to a LabelRow
activityIndicator.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: contentView.layoutMarginsGuide.centerYAnchor),
])
}
activityIndicator.startAnimating()
}
}
}
public final class WebSocketStatusRow: Row<WebSocketStatusCell>, RowType {
public enum DisplayStyle {
case `default`
case loading
case alert
func title(for state: HAConnectionState) -> String? {
switch self {
case .default, .alert: return L10n.Settings.ConnectionSection.Websocket.title
case .loading: return nil
}
}
func message(for state: HAConnectionState) -> String? {
switch state {
case .connecting: return L10n.Settings.ConnectionSection.Websocket.Status.connecting
case .authenticating: return L10n.Settings.ConnectionSection.Websocket.Status.authenticating
case let .disconnected(reason):
let nonVerboseString = L10n.Settings.ConnectionSection.Websocket.Status.Disconnected.title
let verboseString: String
switch reason {
case let .waitingToReconnect(lastError: error, atLatest: atLatest, retryCount: count):
var components = [String]()
if let error = error {
components.append(L10n.Settings.ConnectionSection.Websocket.Status.Disconnected.error(
error.localizedDescription
))
}
components.append(L10n.Settings.ConnectionSection.Websocket.Status.Disconnected.retryCount(count))
components.append(L10n.Settings.ConnectionSection.Websocket.Status.Disconnected.nextRetry(
DateFormatter.localizedString(from: atLatest, dateStyle: .none, timeStyle: .medium)
))
verboseString = components.joined(separator: "\n\n")
case .disconnected:
verboseString = nonVerboseString
}
switch self {
case .default, .loading: return nonVerboseString
case .alert: return verboseString
}
case .ready:
switch self {
case .default, .alert: return L10n.Settings.ConnectionSection.Websocket.Status.connected
case .loading: return nil
}
}
}
}
public var displayStyle: DisplayStyle = .default {
didSet {
updateCell()
}
}
public required init(tag: String?) {
super.init(tag: tag)
displayValueFor = { [weak self] value in
if let value = value, let self = self {
return self.displayStyle.message(for: value)
} else {
return nil
}
}
onCellSelection { [weak self] _, _ in
self?.presentAlert()
}
NotificationCenter.default.addObserver(
self,
selector: #selector(stateDidChange),
name: HAConnectionState.didTransitionToStateNotification,
object: nil
)
stateDidChange()
}
public var connection: HAConnection? {
didSet {
stateDidChange()
}
}
@objc private func stateDidChange() {
value = connection?.state
updateCell()
}
override public func updateCell() {
super.updateCell()
cell.textLabel?.text = value.flatMap { displayStyle.title(for: $0) }
lastAlertController?.message = value.flatMap { DisplayStyle.alert.message(for: $0) }
cell.selectionStyle = .default
cell.accessibilityTraits.insert(.button)
switch value {
case .disconnected(reason: _):
let icon = MaterialDesignIcons.informationOutlineIcon.image(
ofSize: CGSize(width: 24, height: 24),
color: Constants.tintColor
)
cell.accessoryView = UIImageView(image: icon)
default:
cell.accessoryView = nil
}
}
private weak var lastAlertController: UIAlertController?
private func presentAlert() {
guard let value = value else { return }
let alert = UIAlertController(
title: DisplayStyle.alert.title(for: value),
message: DisplayStyle.alert.message(for: value),
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: L10n.copyLabel, style: .default, handler: { _ in
UIPasteboard.general.string = DisplayStyle.alert.message(for: value)
}))
alert.addAction(UIAlertAction(title: L10n.cancelLabel, style: .cancel, handler: nil))
cell.formViewController()?.present(alert, animated: true, completion: nil)
deselect(animated: true)
lastAlertController = alert
}
}