592 lines
22 KiB
Swift
592 lines
22 KiB
Swift
import ColorPickerRow
|
|
import Eureka
|
|
import Foundation
|
|
import PromiseKit
|
|
import Shared
|
|
import UIKit
|
|
|
|
class ComplicationEditViewController: HAFormViewController, TypedRowControllerType {
|
|
var row: RowOf<ButtonRow>!
|
|
/// A closure to be called when the controller disappears.
|
|
public var onDismissCallback: ((UIViewController) -> Void)?
|
|
|
|
let config: WatchComplication
|
|
private var displayTemplate: ComplicationTemplate
|
|
private var server: Server {
|
|
if let value = (form.rowBy(tag: "server") as? ServerSelectRow)?.value, let server = value.server {
|
|
return server
|
|
} else {
|
|
return Current.servers.all.first!
|
|
}
|
|
}
|
|
|
|
init(config: WatchComplication) {
|
|
self.config = config
|
|
self.displayTemplate = config.Template
|
|
|
|
super.init()
|
|
|
|
self.isModalInPresentation = true
|
|
}
|
|
|
|
@objc private func cancel() {
|
|
onDismissCallback?(self)
|
|
}
|
|
|
|
@objc private func save() {
|
|
let realm = Current.realm()
|
|
realm.reentrantWrite {
|
|
if let name = (form.rowBy(tag: "name") as? TextRow)?.value, name.isEmpty == false {
|
|
config.name = name
|
|
} else {
|
|
config.name = nil
|
|
}
|
|
if let IsPublic = (form.rowBy(tag: "IsPublic") as? SwitchRow)?.value {
|
|
config.IsPublic = IsPublic
|
|
} else {
|
|
config.IsPublic = true
|
|
}
|
|
config.serverIdentifier = server.identifier.rawValue
|
|
config.Template = displayTemplate
|
|
config.Data = getValuesGroupedBySection()
|
|
|
|
Current.Log.verbose("COMPLICATION \(config) \(config.Data)")
|
|
|
|
realm.add(config, update: .all)
|
|
}.then(on: nil) { [server] in
|
|
Current.api(for: server)?
|
|
.updateComplications(passively: false) ?? .init(error: HomeAssistantAPI.APIError.noAPIAvailable)
|
|
}.cauterize()
|
|
|
|
onDismissCallback?(self)
|
|
}
|
|
|
|
@objc private func deleteComplication(_ sender: UIView) {
|
|
precondition(config.realm != nil)
|
|
|
|
let alert = UIAlertController(
|
|
title: L10n.Watch.Configurator.Delete.title,
|
|
message: L10n.Watch.Configurator.Delete.message,
|
|
preferredStyle: .actionSheet
|
|
)
|
|
with(alert.popoverPresentationController) {
|
|
$0?.sourceView = sender
|
|
$0?.sourceRect = sender.bounds
|
|
}
|
|
alert.addAction(UIAlertAction(
|
|
title: L10n.Watch.Configurator.Delete.button, style: .destructive, handler: { [config, server] _ in
|
|
let realm = Current.realm()
|
|
realm.reentrantWrite {
|
|
realm.delete(config)
|
|
}.then(on: nil) {
|
|
Current.api(for: server)?
|
|
.updateComplications(passively: false) ?? .init(error: HomeAssistantAPI.APIError.noAPIAvailable)
|
|
}.cauterize()
|
|
|
|
self.onDismissCallback?(self)
|
|
}
|
|
))
|
|
alert.addAction(UIAlertAction(title: L10n.cancelLabel, style: .cancel, handler: nil))
|
|
present(alert, animated: true, completion: nil)
|
|
}
|
|
|
|
// swiftlint:disable:next cyclomatic_complexity
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
navigationItem.leftBarButtonItems = [
|
|
UIBarButtonItem(
|
|
barButtonSystemItem: .cancel,
|
|
target: self,
|
|
action: #selector(cancel)
|
|
),
|
|
]
|
|
|
|
let infoBarButtonItem = AppConstants.helpBarButtonItem
|
|
|
|
infoBarButtonItem.action = #selector(getInfoAction)
|
|
infoBarButtonItem.target = self
|
|
|
|
navigationItem.rightBarButtonItems = [
|
|
UIBarButtonItem(
|
|
barButtonSystemItem: .save,
|
|
target: self,
|
|
action: #selector(save)
|
|
),
|
|
infoBarButtonItem,
|
|
]
|
|
|
|
title = config.Family.name
|
|
|
|
let textSections = ComplicationTextAreas.allCases.map({ addComplicationTextAreaFormSection(location: $0) })
|
|
|
|
form
|
|
|
|
+++ Section {
|
|
$0.tag = "template"
|
|
}
|
|
|
|
<<< TextRow("name") {
|
|
$0.title = L10n.Watch.Configurator.Rows.DisplayName.title
|
|
$0.placeholder = config.Family.name
|
|
$0.value = config.name
|
|
}
|
|
|
|
<<< ServerSelectRow("server") {
|
|
if let server = Current.servers.server(forServerIdentifier: config.serverIdentifier) {
|
|
$0.value = .server(server)
|
|
} else {
|
|
$0.value = Current.servers.all.first.flatMap { .server($0) }
|
|
}
|
|
$0.onChange { [form] row in
|
|
for section in form.allSections {
|
|
if let section = section as? TemplateSection, let server = row.value?.server {
|
|
section.server = server
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
<<< PushRow<ComplicationTemplate> {
|
|
$0.tag = "template"
|
|
$0.title = L10n.Watch.Configurator.Rows.Template.title
|
|
$0.options = config.Family.templates
|
|
$0.value = displayTemplate
|
|
$0.selectorTitle = L10n.Watch.Configurator.Rows.Template.selectorTitle
|
|
}.onPresent { _, to in
|
|
to.enableDeselection = false
|
|
to.selectableRowSetup = { row in
|
|
row.cellStyle = .subtitle
|
|
}
|
|
to.selectableRowCellSetup = { cell, row in
|
|
cell.textLabel?.text = row.selectableValue?.style
|
|
cell.detailTextLabel?.text = row.selectableValue?.description
|
|
cell.detailTextLabel?.numberOfLines = 0
|
|
cell.detailTextLabel?.lineBreakMode = .byWordWrapping
|
|
cell.detailTextLabel?.textColor = .secondaryLabel
|
|
}
|
|
to.selectableRowCellUpdate = { cell, row in
|
|
cell.textLabel?.text = row.selectableValue?.style
|
|
cell.detailTextLabel?.text = row.selectableValue?.description
|
|
}
|
|
}.onChange { [weak self] row in
|
|
if let template = row.value {
|
|
self?.displayTemplate = template
|
|
self?.reloadForm()
|
|
}
|
|
}.cellUpdate { cell, row in
|
|
cell.detailTextLabel?.text = row.value?.style
|
|
}.cellSetup { cell, row in
|
|
cell.detailTextLabel?.text = row.value?.style
|
|
}
|
|
|
|
<<< SwitchRow("IsPublic") {
|
|
$0.title = L10n.Watch.Configurator.Rows.IsPublic.title
|
|
$0.value = config.IsPublic
|
|
}
|
|
|
|
form.append(contentsOf: textSections)
|
|
|
|
form
|
|
|
|
+++ Section {
|
|
$0.tag = "column2alignment"
|
|
$0.hidden = .function([], { [weak self] _ in
|
|
self?.displayTemplate.supportsColumn2Alignment == false
|
|
})
|
|
}
|
|
|
|
<<< SegmentedRow<String> {
|
|
$0.tag = "column2alignment"
|
|
$0.title = L10n.Watch.Configurator.Rows.Column2Alignment.title
|
|
$0.add(rule: RuleRequired())
|
|
$0.options = ["leading", "trailing"]
|
|
$0.displayValueFor = {
|
|
if $0?.lowercased() == "leading" {
|
|
return L10n.Watch.Configurator.Rows.Column2Alignment.Options.leading
|
|
} else {
|
|
return L10n.Watch.Configurator.Rows.Column2Alignment.Options.trailing
|
|
}
|
|
}
|
|
$0.value = $0.options?.first
|
|
if let info = config.Data["column2alignment"] as? [String: Any],
|
|
let value = info[$0.tag!] as? String {
|
|
$0.value = value
|
|
}
|
|
}
|
|
|
|
+++ TemplateSection(
|
|
header: L10n.Watch.Configurator.Sections.Gauge.header,
|
|
footer: L10n.Watch.Configurator.Sections.Gauge.footer,
|
|
displayResult: { try Self.validate(result: $0, expectingPercentile: true) },
|
|
server: server,
|
|
initializeInput: {
|
|
$0.tag = "gauge"
|
|
$0.title = L10n.Watch.Configurator.Rows.Gauge.title
|
|
$0.placeholder = "{{ range(1, 100) | random / 100.0 }}"
|
|
$0.add(rule: RuleRequired())
|
|
if let gaugeDict = config.Data["gauge"] as? [String: Any],
|
|
let value = gaugeDict[$0.tag!] as? String {
|
|
$0.value = value
|
|
}
|
|
|
|
}, initializeSection: {
|
|
$0.tag = "gauge"
|
|
$0.hidden = .function([], { [weak self] _ in
|
|
self?.displayTemplate.hasGauge == false
|
|
})
|
|
}
|
|
)
|
|
|
|
<<< InlineColorPickerRow("gauge_color") {
|
|
$0.title = L10n.Watch.Configurator.Rows.Gauge.Color.title
|
|
$0.isCircular = true
|
|
$0.showsPaletteNames = true
|
|
$0.value = UIColor.green
|
|
if let gaugeDict = config.Data["gauge"] as? [String: Any],
|
|
let value = gaugeDict[$0.tag!] as? String {
|
|
$0.value = UIColor(hex: value)
|
|
}
|
|
}.onChange { picker in
|
|
Current.Log.verbose("gauge color: \(picker.value!.hexString(false))")
|
|
}
|
|
|
|
<<< SegmentedRow<String> {
|
|
$0.tag = "gauge_type"
|
|
$0.title = L10n.Watch.Configurator.Rows.Gauge.GaugeType.title
|
|
$0.add(rule: RuleRequired())
|
|
$0.options = ["open", "closed"]
|
|
$0.displayValueFor = {
|
|
if $0?.lowercased() == "open" {
|
|
return L10n.Watch.Configurator.Rows.Gauge.GaugeType.Options.open
|
|
} else {
|
|
return L10n.Watch.Configurator.Rows.Gauge.GaugeType.Options.closed
|
|
}
|
|
}
|
|
$0.value = $0.options?.first
|
|
if let gaugeDict = config.Data["gauge"] as? [String: Any],
|
|
let value = gaugeDict[$0.tag!] as? String {
|
|
$0.value = value
|
|
}
|
|
}
|
|
|
|
<<< SegmentedRow<String> {
|
|
$0.tag = "gauge_style"
|
|
$0.title = L10n.Watch.Configurator.Rows.Gauge.Style.title
|
|
$0.add(rule: RuleRequired())
|
|
$0.options = ["fill", "ring"]
|
|
$0.displayValueFor = {
|
|
if $0?.lowercased() == "fill" {
|
|
return L10n.Watch.Configurator.Rows.Gauge.Style.Options.fill
|
|
} else {
|
|
return L10n.Watch.Configurator.Rows.Gauge.Style.Options.ring
|
|
}
|
|
}
|
|
$0.value = $0.options?.first
|
|
if let gaugeDict = config.Data["gauge"] as? [String: Any],
|
|
let value = gaugeDict[$0.tag!] as? String {
|
|
$0.value = value
|
|
}
|
|
}
|
|
|
|
+++ TemplateSection(
|
|
header: L10n.Watch.Configurator.Sections.Ring.header,
|
|
footer: L10n.Watch.Configurator.Sections.Ring.footer,
|
|
displayResult: { try Self.validate(result: $0, expectingPercentile: true) },
|
|
server: server,
|
|
initializeInput: {
|
|
$0.tag = "ring_value"
|
|
$0.title = L10n.Watch.Configurator.Rows.Ring.Value.title
|
|
$0.placeholder = "{{ range(1, 100) | random / 100.0 }}"
|
|
$0.add(rule: RuleRequired())
|
|
if let dict = config.Data["ring"] as? [String: Any],
|
|
let value = dict[$0.tag!] as? String {
|
|
$0.value = value
|
|
}
|
|
}, initializeSection: {
|
|
$0.tag = "ring"
|
|
$0.hidden = .function([], { [weak self] _ in
|
|
self?.displayTemplate.hasRing == false
|
|
})
|
|
}
|
|
)
|
|
|
|
<<< SegmentedRow<String> {
|
|
$0.tag = "ring_type"
|
|
$0.title = L10n.Watch.Configurator.Rows.Ring.RingType.title
|
|
$0.add(rule: RuleRequired())
|
|
$0.options = ["open", "closed"]
|
|
|
|
$0.displayValueFor = { value in
|
|
if value?.lowercased() == "open" {
|
|
return L10n.Watch.Configurator.Rows.Ring.RingType.Options.open
|
|
} else {
|
|
return L10n.Watch.Configurator.Rows.Ring.RingType.Options.closed
|
|
}
|
|
}
|
|
|
|
$0.value = $0.options?.first
|
|
if let dict = config.Data["ring"] as? [String: Any],
|
|
let value = dict[$0.tag!] as? String {
|
|
$0.value = value
|
|
}
|
|
}
|
|
|
|
<<< InlineColorPickerRow("ring_color") {
|
|
$0.title = L10n.Watch.Configurator.Rows.Ring.Color.title
|
|
$0.isCircular = true
|
|
$0.showsPaletteNames = true
|
|
$0.value = UIColor.green
|
|
if let dict = config.Data["ring"] as? [String: Any],
|
|
let value = dict[$0.tag!] as? String {
|
|
$0.value = UIColor(hex: value)
|
|
}
|
|
}.onChange { picker in
|
|
Current.Log.verbose("ring color: \(picker.value!.hexString(false))")
|
|
}
|
|
|
|
+++ Section(
|
|
header: L10n.Watch.Configurator.Sections.Icon.header,
|
|
footer: L10n.Watch.Configurator.Sections.Icon.footer
|
|
) {
|
|
$0.tag = "icon"
|
|
$0.hidden = .function([], { [weak self] _ in
|
|
self?.displayTemplate.hasImage == false
|
|
})
|
|
}
|
|
|
|
<<< SearchPushRow<MaterialDesignIcons> {
|
|
$0.options = MaterialDesignIcons.allCases
|
|
$0.value = $0.options?.first
|
|
$0.displayValueFor = { icon in
|
|
icon?.name
|
|
}
|
|
$0.selectorTitle = L10n.Watch.Configurator.Rows.Icon.Choose.title
|
|
$0.tag = "icon"
|
|
if let dict = config.Data["icon"] as? [String: Any],
|
|
let value = dict[$0.tag!] as? String {
|
|
$0.value = MaterialDesignIcons(named: value)
|
|
}
|
|
}.cellUpdate({ [weak self] cell, row in
|
|
if let value = row.value {
|
|
if let iconColorRow = self?.form.rowBy(tag: "icon_color") as? InlineColorPickerRow {
|
|
cell.imageView?.image = value.image(
|
|
ofSize: CGSize(
|
|
width: CGFloat(30),
|
|
height: CGFloat(30)
|
|
),
|
|
color: iconColorRow.value
|
|
)
|
|
}
|
|
}
|
|
}).onPresent { [weak self] _, to in
|
|
to.selectableRowCellUpdate = { cell, row in
|
|
if let value = row.selectableValue {
|
|
if let iconColorRow = self?.form.rowBy(tag: "icon_color") as? InlineColorPickerRow {
|
|
cell.imageView?.image = value.image(
|
|
ofSize: CGSize(
|
|
width: CGFloat(30),
|
|
height: CGFloat(30)
|
|
),
|
|
color: iconColorRow.value
|
|
)
|
|
}
|
|
|
|
cell.textLabel?.text = value.name
|
|
}
|
|
}
|
|
}
|
|
|
|
<<< InlineColorPickerRow("icon_color") {
|
|
$0.title = L10n.Watch.Configurator.Rows.Icon.Color.title
|
|
$0.isCircular = true
|
|
$0.showsPaletteNames = true
|
|
$0.value = UIColor.green
|
|
if let dict = config.Data["icon"] as? [String: Any],
|
|
let value = dict[$0.tag!] as? String {
|
|
$0.value = UIColor(hex: value)
|
|
}
|
|
}.onChange { [weak self] picker in
|
|
Current.Log.verbose("icon color: \(picker.value!.hexString(false))")
|
|
if let iconRow = self?.form.rowBy(tag: "icon") as? SearchPushRow<MaterialDesignIcons> {
|
|
if let value = iconRow.value {
|
|
iconRow.cell.imageView?.image = value.image(
|
|
ofSize: CGSize(
|
|
width: CGFloat(30),
|
|
height: CGFloat(30)
|
|
),
|
|
color: picker.value
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
+++ Section { [config] section in
|
|
section.tag = "delete"
|
|
|
|
if config.realm == nil {
|
|
// don't need to show a delete button for an unpersisted complication
|
|
section.hidden = true
|
|
}
|
|
}
|
|
<<< ButtonRow {
|
|
$0.title = L10n.Watch.Configurator.Delete.button
|
|
$0.onCellSelection { [weak self] cell, _ in
|
|
self?.deleteComplication(cell)
|
|
}
|
|
$0.cellUpdate { cell, _ in
|
|
cell.textLabel?.textColor = .systemRed
|
|
}
|
|
}
|
|
|
|
reloadForm()
|
|
}
|
|
|
|
@objc
|
|
func getInfoAction(_ sender: Any) {
|
|
openURLInBrowser(URL(string: "https://companion.home-assistant.io/app/ios/apple-watch")!, self)
|
|
}
|
|
|
|
enum RenderValueError: LocalizedError {
|
|
case expectedFloat(value: Any)
|
|
case outOfRange(value: Float)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case let .expectedFloat(value: value):
|
|
var displayType = String(describing: type(of: value))
|
|
|
|
if displayType.lowercased().contains("string") {
|
|
displayType = "string"
|
|
}
|
|
|
|
return L10n.Watch.Configurator.PreviewError.notNumber(displayType, value)
|
|
case let .outOfRange(value: value):
|
|
return L10n.Watch.Configurator.PreviewError.outOfRange(value)
|
|
}
|
|
}
|
|
}
|
|
|
|
static func validate(result: Any, expectingPercentile: Bool) throws -> String {
|
|
if expectingPercentile {
|
|
if let number = WatchComplication.percentileNumber(from: result) {
|
|
if !(0 ... 1 ~= number) {
|
|
throw RenderValueError.outOfRange(value: number)
|
|
}
|
|
} else {
|
|
throw RenderValueError.expectedFloat(value: result)
|
|
}
|
|
}
|
|
|
|
return String(describing: result)
|
|
}
|
|
|
|
func addComplicationTextAreaFormSection(location: ComplicationTextAreas) -> Section {
|
|
let key = "textarea_" + location.slug
|
|
var dataDict = [String: Any]()
|
|
|
|
if let textAreasDict = config.Data["textAreas"] as? [String: [String: Any]],
|
|
let slugDict = textAreasDict[location.slug] {
|
|
dataDict = slugDict
|
|
}
|
|
|
|
let section = TemplateSection(
|
|
header: location.label,
|
|
footer: location.description,
|
|
displayResult: { try Self.validate(result: $0, expectingPercentile: false) },
|
|
server: server,
|
|
initializeInput: {
|
|
$0.tag = key + "_text"
|
|
$0.title = location.label
|
|
$0.add(rule: RuleRequired())
|
|
$0.placeholder = "{{ states(\"weather.temperature\") }}"
|
|
if let value = dataDict["text"] as? String {
|
|
$0.value = value
|
|
}
|
|
}, initializeSection: {
|
|
$0.tag = location.slug
|
|
$0.hidden = .function([], { [weak self] _ in
|
|
self?.displayTemplate.textAreas.map(\.slug).contains(location.slug) == false
|
|
})
|
|
}
|
|
)
|
|
|
|
section.append(InlineColorPickerRow {
|
|
$0.tag = key + "_color"
|
|
$0.title = L10n.Watch.Configurator.Rows.Color.title
|
|
$0.isCircular = true
|
|
$0.showsPaletteNames = true
|
|
$0.value = UIColor.green
|
|
if let value = dataDict["color"] as? String {
|
|
$0.value = UIColor(hex: value)
|
|
}
|
|
}.onChange { picker in
|
|
Current.Log.verbose("color for " + location.rawValue + ": \(picker.value!.hexString(false))")
|
|
})
|
|
|
|
return section
|
|
}
|
|
|
|
func reloadForm() {
|
|
for section in form.allSections {
|
|
section.evaluateHidden()
|
|
}
|
|
|
|
if displayTemplate.hasGauge, let gaugeType = form.rowBy(tag: "gauge_type") as? SegmentedRow<String> {
|
|
if displayTemplate.gaugeIsOpenStyle {
|
|
gaugeType.value = gaugeType.options?[0]
|
|
} else if displayTemplate.gaugeIsClosedStyle {
|
|
gaugeType.value = gaugeType.options?[1]
|
|
}
|
|
gaugeType.disabled = true
|
|
gaugeType.evaluateDisabled()
|
|
gaugeType.reload()
|
|
}
|
|
}
|
|
|
|
func getValuesGroupedBySection() -> [String: Any] {
|
|
var groupedVals: [String: [String: Any]] = [:]
|
|
var textAreasDict: [String: [String: Any]] = [:]
|
|
|
|
for row in form.allRows {
|
|
if row.section!.isHidden || row.section!.tag == "template" || row.section!.tag == "delete" {
|
|
continue
|
|
}
|
|
|
|
if let section = row.section, let sectionTag = section.tag, var rowTag = row.tag,
|
|
var rowValue = row.baseValue {
|
|
if rowTag.contains("color"), let color = rowValue as? UIColor {
|
|
rowValue = color.hexString(true)
|
|
}
|
|
|
|
if let mdi = rowValue as? MaterialDesignIcons {
|
|
rowValue = mdi.name
|
|
}
|
|
|
|
let rowTagPrefix = "textarea_" + sectionTag + "_"
|
|
if rowTag.hasPrefix(rowTagPrefix) {
|
|
rowTag = rowTag.replacingOccurrences(of: rowTagPrefix, with: "")
|
|
|
|
if textAreasDict[sectionTag] == nil {
|
|
textAreasDict[sectionTag] = [String: Any]()
|
|
}
|
|
|
|
textAreasDict[sectionTag]![rowTag] = rowValue
|
|
} else {
|
|
if groupedVals[sectionTag] == nil {
|
|
groupedVals[sectionTag] = [String: Any]()
|
|
}
|
|
|
|
groupedVals[sectionTag]![rowTag] = rowValue
|
|
}
|
|
}
|
|
}
|
|
|
|
groupedVals["textAreas"] = textAreasDict
|
|
|
|
Current.Log.verbose("groupedVals \(groupedVals)")
|
|
|
|
return groupedVals
|
|
}
|
|
}
|