Skip to content

Commit

Permalink
Unify Server & Connection settings into 1 row (#1255)
Browse files Browse the repository at this point in the history
Extremely long horizon: multiple of these could be shown to configure the settings of a particular server.

Copies the look and feel of the 'avatar' initials and image loading from the frontend. When an avatar isn't set or isn't loaded yet, this shows the initials view.
  • Loading branch information
zacwest committed Oct 31, 2020
1 parent f5b99fe commit de33f93
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 29 deletions.
8 changes: 8 additions & 0 deletions HomeAssistant.xcodeproj/project.pbxproj
Expand Up @@ -84,6 +84,7 @@
1155DD1E250F446F003405C0 /* Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03D891720E0A85200D4F28D /* Shared.framework */; };
1158D6282511DA68008C0C9F /* ManualPodLicenses.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1158D6272511DA67008C0C9F /* ManualPodLicenses.plist */; };
115DA29324F464DC00C00BB1 /* MenuManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 115DA28C24F4646500C00BB1 /* MenuManager.swift */; };
115EF6A72549152F0048597B /* AccountRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 115EF6A62549152F0048597B /* AccountRow.swift */; };
1161C01B24D7634300A0E3C4 /* NFCListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1161C01A24D7634300A0E3C4 /* NFCListViewController.swift */; };
116740732519907400F51626 /* MacBridgeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 116740722519907400F51626 /* MacBridgeProtocol.swift */; };
1167408E251990D500F51626 /* MacBridgeImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1167408D251990D500F51626 /* MacBridgeImpl.swift */; };
Expand Down Expand Up @@ -198,6 +199,7 @@
11BC9E5524FDB88200B9FBF7 /* ActiveStateManager.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11BC9E5424FDB88200B9FBF7 /* ActiveStateManager.test.swift */; };
11BC9E5724FDC1C900B9FBF7 /* ActiveSensor.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11BC9E5624FDC1C900B9FBF7 /* ActiveSensor.test.swift */; };
11BD8BBD24E76BAD004B9A54 /* WidgetActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11BD8BBC24E76BAD004B9A54 /* WidgetActions.swift */; };
11C05F2D254919210031D038 /* AccountInitialsImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C05F2C254919210031D038 /* AccountInitialsImage.swift */; };
11C4627F24B04CB800031902 /* Promise+RetryNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C4627E24B04CB800031902 /* Promise+RetryNetworking.swift */; };
11C4628024B04CB800031902 /* Promise+RetryNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C4627E24B04CB800031902 /* Promise+RetryNetworking.swift */; };
11C4628224B053A800031902 /* WebhookResponseUpdateSensors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C4628124B053A800031902 /* WebhookResponseUpdateSensors.swift */; };
Expand Down Expand Up @@ -934,6 +936,7 @@
1155DD1C250F42EA003405C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
1158D6272511DA67008C0C9F /* ManualPodLicenses.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = ManualPodLicenses.plist; sourceTree = "<group>"; };
115DA28C24F4646500C00BB1 /* MenuManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuManager.swift; sourceTree = "<group>"; };
115EF6A62549152F0048597B /* AccountRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRow.swift; sourceTree = "<group>"; };
1161C01624D75BD500A0E3C4 /* iOSTagManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSTagManager.swift; sourceTree = "<group>"; };
1161C01A24D7634300A0E3C4 /* NFCListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFCListViewController.swift; sourceTree = "<group>"; };
1167402225198F9A00F51626 /* MacBridge.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MacBridge.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -1032,6 +1035,7 @@
11BC9E5424FDB88200B9FBF7 /* ActiveStateManager.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveStateManager.test.swift; sourceTree = "<group>"; };
11BC9E5624FDC1C900B9FBF7 /* ActiveSensor.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSensor.test.swift; sourceTree = "<group>"; };
11BD8BBC24E76BAD004B9A54 /* WidgetActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetActions.swift; sourceTree = "<group>"; };
11C05F2C254919210031D038 /* AccountInitialsImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInitialsImage.swift; sourceTree = "<group>"; };
11C4627E24B04CB800031902 /* Promise+RetryNetworking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+RetryNetworking.swift"; sourceTree = "<group>"; };
11C4628124B053A800031902 /* WebhookResponseUpdateSensors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhookResponseUpdateSensors.swift; sourceTree = "<group>"; };
11C4628724B109C000031902 /* WebhookResponseLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebhookResponseLocation.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1958,6 +1962,8 @@
11B1FFC424CCD72F00F9BCB2 /* VoiceShortcutRow.swift */,
11948E8824DA5D50006F5657 /* InfoLabelRow.swift */,
11B62DBD24F2EDD800E5CB55 /* EurekaCondition+Additions.swift */,
115EF6A62549152F0048597B /* AccountRow.swift */,
11C05F2C254919210031D038 /* AccountInitialsImage.swift */,
);
path = Eureka;
sourceTree = "<group>";
Expand Down Expand Up @@ -4411,6 +4417,7 @@
B68EDD09215F45EB00DD6B28 /* NotificationIdentifierEurekaRow.swift in Sources */,
B6B6B14A215B137C003DE2DD /* ComplicationEditViewController.swift in Sources */,
B6DD5E6A24940F6F003A0154 /* OpenInFirefoxControllerSwift.swift in Sources */,
115EF6A72549152F0048597B /* AccountRow.swift in Sources */,
111858DF24CB83DF00B8CDDC /* Intents.intentdefinition in Sources */,
B64BB3A81E9C6551001E8B46 /* WebViewController.swift in Sources */,
11A71C7124A4648000D9565F /* ZoneManagerEquatableRegion.swift in Sources */,
Expand All @@ -4435,6 +4442,7 @@
B641BC1F1E2097EF002CCBC1 /* AboutViewController.swift in Sources */,
B60221F8226D9AEE00E8DBFE /* PermissionButton.swift in Sources */,
B675ECC3221BB0E600C65D31 /* SearchPushRow.swift in Sources */,
11C05F2D254919210031D038 /* AccountInitialsImage.swift in Sources */,
B605C891226E9DAC00EF46DD /* Permissions.swift in Sources */,
B616B29B227ED69500828165 /* DNSResolver.swift in Sources */,
B65B15052273188300635D5C /* Assets.swift in Sources */,
Expand Down
54 changes: 54 additions & 0 deletions Sources/App/Settings/Eureka/AccountInitialsImage.swift
@@ -0,0 +1,54 @@
import Shared
import UIKit

enum AccountInitialsImage {
private static func initials(for string: String?) -> String {
// swiftlint:disable:next line_length
// matching https://github.com/home-assistant/frontend/blob/42bf350034b7a53f0c6ba76791ea9d2a65bf6d67/src/components/user/ha-user-badge.ts

guard let string = string else {
return "?"
}

return string
.trimmingCharacters(in: .whitespacesAndNewlines)
.split(separator: " ")
.prefix(3)
.compactMap { $0.first.map(String.init(_:)) }
.joined()
}

static func image(for name: String?, size: CGSize) -> UIImage {
let initials = self.initials(for: name)

let rect = CGRect(origin: .zero, size: size)
let image = UIGraphicsImageRenderer(size: size).image { context in
Constants.tintColor.setFill()
context.fill(rect)

let fontSize = size.height / (initials.count >= 3 ? 3 : 2)

let initials = NSMutableAttributedString(
string: initials,
attributes: [
.font: UIFont.systemFont(ofSize: fontSize, weight: .regular),
.foregroundColor: UIColor.white
]
)
let initialsSize = initials
.boundingRect(with: size, options: .usesLineFragmentOrigin, context: nil)
.size
let initialsRect = rect.insetBy(
dx: max(4, (size.width - initialsSize.width) / 2.0),
dy: max(4, (size.height - initialsSize.height) / 2.0)
)
initials.draw(
with: initialsRect,
options: [.usesLineFragmentOrigin],
context: nil
)
}
image.accessibilityLabel = initials
return image
}
}
133 changes: 133 additions & 0 deletions Sources/App/Settings/Eureka/AccountRow.swift
@@ -0,0 +1,133 @@
import Foundation
import Eureka
import Shared
import PromiseKit

class AccountCell: Cell<HomeAssistantAccountRowInfo>, CellType {
private var accountRow: HomeAssistantAccountRow? { return row as? HomeAssistantAccountRow }

override func setup() {
super.setup()

imageView?.layer.masksToBounds = true

textLabel?.font = UIFont.preferredFont(forTextStyle: .body)
detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .body)

selectionStyle = .default
accessoryType = .disclosureIndicator
}

override func update() {
super.update()

let userName = accountRow?.value?.user?.Name
let locationName = accountRow?.value?.locationName

let height = min(64, UIFont.preferredFont(forTextStyle: .body).lineHeight * 2.0)
let size = CGSize(width: height, height: height)

if let imageView = imageView {
if let image = accountRow?.cachedImage {
UIView.transition(
with: imageView,
duration: imageView.image != nil ? 0.25 : 0,
options: [.transitionCrossDissolve]
) {
// scaled down because the cell sizes to fit too much
imageView.image = image.scaledToSize(size)
} completion: { _ in

}
} else {
imageView.image = AccountInitialsImage
.image(
for: userName,
size: CGSize(width: height, height: height)
)
}

imageView.layer.cornerRadius = ceil(height / 2.0)
}

textLabel?.text = locationName
detailTextLabel?.text = userName

if #available(iOS 13, *) {
detailTextLabel?.textColor = .secondaryLabel
} else {
detailTextLabel?.textColor = .darkGray
}
}
}

struct HomeAssistantAccountRowInfo: Equatable {
var user: AuthenticatedUser?
var locationName: String?

static func == (lhs: HomeAssistantAccountRowInfo, rhs: HomeAssistantAccountRowInfo) -> Bool {
return lhs.user?.ID == rhs.user?.ID &&
lhs.locationName == rhs.locationName
}
}

final class HomeAssistantAccountRow: Row<AccountCell>, RowType {
var presentationMode: PresentationMode<UIViewController>?

override func customDidSelect() {
super.customDidSelect()
if !isDisabled {
if let presentationMode = presentationMode {
if let controller = presentationMode.makeController() {
presentationMode.present(controller, row: self, presentingController: cell.formViewController()!)
} else {
presentationMode.present(nil, row: self, presentingController: cell.formViewController()!)
}
}
}
}

required init(tag: String?) {
super.init(tag: tag)
self.cellStyle = .subtitle
}

fileprivate var cachedImage: UIImage?

override var value: Cell.Value? {
didSet {
if value != oldValue {
fetchAvatar()
}
}
}

private func fetchAvatar() {
guard let user = value?.user else {
cachedImage = nil
return
}

firstly {
HomeAssistantAPI.authenticatedAPIPromise
}.then {
$0.GetStates()
}.firstValue {
$0.Attributes["user_id"] as? String == user.ID
}.compactMap {
$0.Attributes["entity_picture"] as? String
}.compactMap {
Current.settingsStore.connectionInfo?.activeURL.appendingPathComponent($0)
}.then {
URLSession.shared.dataTask(.promise, with: $0)
}.compactMap {
UIImage(data: $0.data)
}.done { [self] image in
Current.Log.verbose("got image \(image.size)")
cachedImage = image
updateCell()
}.catch { error in
Current.Log.error("failed to grab thumbnail: \(error)")
}
}
}
33 changes: 9 additions & 24 deletions Sources/App/Settings/SettingsViewController.swift
Expand Up @@ -64,25 +64,17 @@ class SettingsViewController: FormViewController {
self.navigationItem.setRightBarButton(doneButton, animated: true)
}

form +++ Section(L10n.Settings.StatusSection.header) {
$0.tag = "status"
}
<<< LabelRow("locationName") {
$0.title = L10n.Settings.StatusSection.LocationNameRow.title
$0.value = L10n.Settings.StatusSection.LocationNameRow.placeholder
if let locationName = prefs.string(forKey: "location_name") {
$0.value = locationName
}
}
<<< LabelRow("version") {
$0.title = L10n.Settings.StatusSection.VersionRow.title
$0.value = L10n.Settings.StatusSection.VersionRow.placeholder
if let version = prefs.string(forKey: "version") {
$0.value = version
}
form +++ HomeAssistantAccountRow {
$0.value = .init(
user: Current.settingsStore.authenticatedUser,
locationName: prefs.string(forKey: "location_name")
)
$0.presentationMode = .show(controllerProvider: ControllerProvider.callback {
return ConnectionSettingsViewController()
}, onDismiss: nil)
}

+++ Section(L10n.Settings.NavigationBar.title)
form +++ Section()
<<< ButtonRow("generalSettings") {
$0.title = L10n.Settings.GeneralSettingsButton.title
$0.presentationMode = .show(controllerProvider: ControllerProvider.callback {
Expand All @@ -92,13 +84,6 @@ class SettingsViewController: FormViewController {
}, onDismiss: nil)
}

<<< ButtonRow {
$0.title = L10n.Settings.ConnectionSection.header
$0.presentationMode = .show(controllerProvider: ControllerProvider.callback {
return ConnectionSettingsViewController()
}, onDismiss: nil)
}

<<< ButtonRow("locationSettings") {
$0.title = L10n.Settings.DetailsSection.LocationSettingsRow.title
$0.presentationMode = .show(controllerProvider: ControllerProvider.callback {
Expand Down
13 changes: 8 additions & 5 deletions Sources/App/Utilities/Utils.swift
Expand Up @@ -110,10 +110,13 @@ func setDefaults() {

extension UIImage {
func scaledToSize(_ size: CGSize) -> UIImage {
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
self.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return newImage
UIGraphicsImageRenderer(
size: size,
format: with(UIGraphicsImageRendererFormat.preferred()) {
$0.opaque = imageRendererFormat.opaque
}
).image { _ in
draw(in: CGRect(origin: .zero, size: size))
}
}
}

0 comments on commit de33f93

Please sign in to comment.