From 8ad58aeed28daabae65795e824c28119dd18ba45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o=20Gon=C3=A7alves?= Date: Tue, 30 Jan 2024 09:10:39 +0100 Subject: [PATCH] Improve iOS Actions, facilitate automation creation (#2541) --- .../Resources/en.lproj/Localizable.strings | 5 ++ Sources/App/Settings/ActionConfigurator.swift | 90 +++++++------------ Sources/App/Settings/Eureka/YamlSection.swift | 7 +- .../SettingsDetailViewController.swift | 9 ++ Sources/App/WebView/WebViewController.swift | 30 +++++++ .../API/WebSocket/WebSocketMessage.swift | 6 +- Sources/Shared/Environment/Constants.swift | 1 + .../Shared/Resources/Swiftgen/Strings.swift | 16 ++++ 8 files changed, 106 insertions(+), 58 deletions(-) diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index c894ef86a..3eeb15cbb 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -25,6 +25,9 @@ "actions_configurator.title" = "New Action"; "actions_configurator.trigger_example.share" = "Share Contents"; "actions_configurator.trigger_example.title" = "Example Trigger"; +"actions_configurator.action.title" = "Execute"; +"actions_configurator.action.create_automation" = "Create automation"; +"actions_configurator.action.footer" = "Define what will be executed when Action is performed, alternatively you can use the example trigger below manually."; "actions_configurator.visual_section.scene_defined" = "The appearance of this action is controlled by the scene configuration."; "actions_configurator.visual_section.scene_hint_footer" = "You can also change these by customizing the Scene attributes: %@"; "actions_configurator.visual_section.server_defined" = "The appearance of this action is controlled by the server configuration."; @@ -42,6 +45,8 @@ "alerts.open_url_from_notification.title" = "Open URL?"; "alerts.prompt.cancel" = "Cancel"; "alerts.prompt.ok" = "OK"; +"alerts.action_automation_editor.unavailable.title" = "Please update Home Assistant"; +"alerts.action_automation_editor.unavailable.body" = "To automatically create an automation for an Action please update your Home Assistant to at least version 2024.2"; "always_open_label" = "Always Open"; "announcement.drop_support.button" = "Continue"; "announcement.drop_support.button_read_more" = "Read more"; diff --git a/Sources/App/Settings/ActionConfigurator.swift b/Sources/App/Settings/ActionConfigurator.swift index 77c417e98..6f68743ca 100644 --- a/Sources/App/Settings/ActionConfigurator.swift +++ b/Sources/App/Settings/ActionConfigurator.swift @@ -18,9 +18,10 @@ class ActionConfigurator: HAFormViewController, TypedRowControllerType { } } - var newAction: Bool = true - var shouldSave: Bool = false - var preview = ActionPreview(frame: CGRect(x: 0, y: 0, width: 169, height: 44)) + private var newAction: Bool = true + private(set) var shouldSave: Bool = false + private(set) var shouldOpenAutomationEditor: Bool = false + private var preview = ActionPreview(frame: CGRect(x: 0, y: 0, width: 169, height: 55)) convenience init(action: Action?) { self.init() @@ -52,6 +53,18 @@ class ActionConfigurator: HAFormViewController, TypedRowControllerType { } } + form +++ ViewRow("preview") { [weak self] row in + row.hidden = Condition.function(["showInWatch"], { _ in + !(self?.action.showInWatch ?? true) + }) + }.cellSetup { [weak self] cell, _ in + guard let self else { return } + cell.backgroundColor = UIColor.clear + cell.preservesSuperviewLayoutMargins = false + self.updatePreviews() + cell.view = self.preview + } + let firstSection = Section() form +++ firstSection @@ -71,8 +84,7 @@ class ActionConfigurator: HAFormViewController, TypedRowControllerType { } } - let visuals = Section( - ) + let visuals = Section() if action.canConfigure(\Action.Text) || action.isServerControlled { let section: Section @@ -135,12 +147,6 @@ class ActionConfigurator: HAFormViewController, TypedRowControllerType { } } - // After text if uneditable - firstSection <<< VoiceShortcutRow { - $0.buttonStyle = .automaticOutline - $0.value = .intent(PerformActionIntent(action: action)) - } - if action.canConfigure(\Action.TextColor) { visuals <<< InlineColorPickerRow("text_color") { $0.title = L10n.ActionsConfigurator.Rows.TextColor.title @@ -230,15 +236,6 @@ class ActionConfigurator: HAFormViewController, TypedRowControllerType { } } } else { - // only show cancel/save flow for editable actions - navigationItem.leftBarButtonItems = [ - UIBarButtonItem( - barButtonSystemItem: .cancel, - target: self, - action: #selector(cancel) - ), - ] - navigationItem.rightBarButtonItems = [ UIBarButtonItem( barButtonSystemItem: .save, @@ -261,15 +258,16 @@ class ActionConfigurator: HAFormViewController, TypedRowControllerType { form.append(visuals) } - form +++ ViewRow("preview") { [weak self] row in - row.hidden = Condition.function(["showInWatch"], { _ in - !(self?.action.showInWatch ?? true) + form +++ Section(header: L10n.ActionsConfigurator.Action.title, footer: L10n.ActionsConfigurator.Action.footer) + <<< ButtonRow { + $0.title = L10n.ActionsConfigurator.Action.createAutomation + }.onCellSelection({ [weak self] _, _ in + self?.saveAndAutomate() }) - }.cellSetup { [weak self] cell, _ in - cell.backgroundColor = UIColor.clear - cell.preservesSuperviewLayoutMargins = false - self?.updatePreviews() - cell.view = self?.preview + + form +++ VoiceShortcutRow { + $0.buttonStyle = .automaticOutline + $0.value = .intent(PerformActionIntent(action: action)) } form +++ YamlSection( @@ -288,11 +286,6 @@ class ActionConfigurator: HAFormViewController, TypedRowControllerType { ) } - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - @objc func getInfoAction(_ sender: Any) { Current.Log.verbose("getInfoAction hit, open docs page!") @@ -304,20 +297,18 @@ class ActionConfigurator: HAFormViewController, TypedRowControllerType { if form.validate().count == 0 { Current.Log.verbose("Category form is valid, calling dismiss callback!") - shouldSave = true - onDismissCallback?(self) } } - @objc - func cancel(_ sender: Any) { - Current.Log.verbose("Cancel hit, calling dismiss") - - shouldSave = false - - onDismissCallback?(self) + private func saveAndAutomate() { + if form.validate().count == 0 { + Current.Log.verbose("Category form is valid, calling dismiss callback!") + shouldSave = true + shouldOpenAutomationEditor = true + onDismissCallback?(self) + } } private func updatePreviews() { @@ -343,20 +334,7 @@ class ActionPreview: UIView { override func layoutSubviews() { super.layoutSubviews() - layer.cornerRadius = 5.0 - - layer.cornerRadius = 2.0 - layer.borderWidth = 1.0 - layer.borderColor = UIColor.clear.cgColor - layer.masksToBounds = true - - layer.shadowColor = UIColor.black.cgColor - layer.shadowOffset = CGSize(width: 0, height: 2.0) - layer.shadowRadius = 2.0 - layer.shadowOpacity = 0.5 - layer.masksToBounds = false - layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath - + layer.cornerRadius = 8 let centerY = (frame.size.height / 2) - 50 title = UILabel(frame: CGRect(x: 60, y: centerY, width: 200, height: 100)) diff --git a/Sources/App/Settings/Eureka/YamlSection.swift b/Sources/App/Settings/Eureka/YamlSection.swift index 72f1c17e1..e329493b8 100644 --- a/Sources/App/Settings/Eureka/YamlSection.swift +++ b/Sources/App/Settings/Eureka/YamlSection.swift @@ -19,6 +19,7 @@ public final class YamlSection: Section { tag: String, header: String, yamlGetter: @escaping () -> String, + isEditable: Bool = false, present: @escaping (UIViewController) -> Void ) { self.yamlGetter = yamlGetter @@ -31,7 +32,11 @@ public final class YamlSection: Section { self.tag = tag self - <<< yamlRow + <<< yamlRow.cellSetup({ cell, _ in + cell.textView.isEditable = false + }).cellUpdate({ cell, _ in + cell.textView.isEditable = false + }) <<< ButtonRow { row in row.title = L10n.ActionsConfigurator.TriggerExample.share diff --git a/Sources/App/Settings/SettingsDetailViewController.swift b/Sources/App/Settings/SettingsDetailViewController.swift index 26e31bd1f..30d228e97 100644 --- a/Sources/App/Settings/SettingsDetailViewController.swift +++ b/Sources/App/Settings/SettingsDetailViewController.swift @@ -734,6 +734,15 @@ class SettingsDetailViewController: HAFormViewController, TypedRowControllerType }.done { self?.updatePositions() }.cauterize() + + if vc.shouldOpenAutomationEditor { + vc.navigationController?.dismiss(animated: true, completion: { + Current.sceneManager.webViewWindowControllerPromise.then(\.webViewControllerPromise) + .done { controller in + controller.openActionAutomationEditor(actionId: vc.action.ID) + } + }) + } } }) } diff --git a/Sources/App/WebView/WebViewController.swift b/Sources/App/WebView/WebViewController.swift index 54b5b0c0f..f3b354356 100644 --- a/Sources/App/WebView/WebViewController.swift +++ b/Sources/App/WebView/WebViewController.swift @@ -833,6 +833,36 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg } ) } + + public func openActionAutomationEditor(actionId: String) { + guard server.info.version >= .externalBusCommandAutomationEditor else { + showActionAutomationEditorNotAvailable() + return + } + sendExternalBus(message: .init(command: "automation/editor/show", payload: [ + "config": [ + "trigger": [ + [ + "platform": "event", + "event_type": "ios.action_fired", + "event_data": [ + "actionID": actionId, + ], + ], + ], + ], + ])) + } + + private func showActionAutomationEditorNotAvailable() { + let alert = UIAlertController( + title: L10n.Alerts.ActionAutomationEditor.Unavailable.title, + message: L10n.Alerts.ActionAutomationEditor.Unavailable.body, + preferredStyle: .alert + ) + alert.addAction(.init(title: L10n.okLabel, style: .default)) + present(alert, animated: true) + } } extension String { diff --git a/Sources/Shared/API/WebSocket/WebSocketMessage.swift b/Sources/Shared/API/WebSocket/WebSocketMessage.swift index c9f53375e..bd14bbf32 100644 --- a/Sources/Shared/API/WebSocket/WebSocketMessage.swift +++ b/Sources/Shared/API/WebSocket/WebSocketMessage.swift @@ -61,10 +61,11 @@ public class WebSocketMessage: Codable { self.command = nil } - public init(command: String) { + public init(command: String, payload: [String: Any]? = nil) { self.ID = -1 self.MessageType = "command" self.command = command + self.Payload = payload } public func encode(to encoder: Encoder) throws { @@ -82,6 +83,9 @@ public class WebSocketMessage: Codable { if let Result = Result { try container.encode(Result, forKey: .Result) } + if let Payload { + try container.encode(Payload, forKey: .Payload) + } try container.encodeIfPresent(command, forKey: .command) } diff --git a/Sources/Shared/Environment/Constants.swift b/Sources/Shared/Environment/Constants.swift index 27ad96bb9..9e5c2a2cb 100644 --- a/Sources/Shared/Environment/Constants.swift +++ b/Sources/Shared/Environment/Constants.swift @@ -146,6 +146,7 @@ public extension Version { static var fullWebhookSecretKey: Version = .init(major: 2022, minor: 3) static var conversationWebhook: Version = .init(major: 2023, minor: 2, prerelease: "any0") static var externalBusCommandSidebar: Version = .init(major: 2023, minor: 4, prerelease: "b3") + static var externalBusCommandAutomationEditor: Version = .init(major: 2024, minor: 2, prerelease: "any0") var coreRequiredString: String { L10n.requiresVersion(String(format: "core-%d.%d", major, minor ?? -1)) diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index 4a837a31d..58388db1e 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -125,6 +125,14 @@ public enum L10n { public enum ActionsConfigurator { /// New Action public static var title: String { return L10n.tr("Localizable", "actions_configurator.title") } + public enum Action { + /// Create automation + public static var createAutomation: String { return L10n.tr("Localizable", "actions_configurator.action.create_automation") } + /// Define what will be executed when Action is performed, alternatively you can use the example trigger below manually. + public static var footer: String { return L10n.tr("Localizable", "actions_configurator.action.footer") } + /// Execute + public static var title: String { return L10n.tr("Localizable", "actions_configurator.action.title") } + } public enum Rows { public enum BackgroundColor { /// Background Color @@ -170,6 +178,14 @@ public enum L10n { } public enum Alerts { + public enum ActionAutomationEditor { + public enum Unavailable { + /// To automatically create an automation for an Action please update your Home Assistant to at least version 2024.2 + public static var body: String { return L10n.tr("Localizable", "alerts.action_automation_editor.unavailable.body") } + /// Please update Home Assistant + public static var title: String { return L10n.tr("Localizable", "alerts.action_automation_editor.unavailable.title") } + } + } public enum Alert { /// OK public static var ok: String { return L10n.tr("Localizable", "alerts.alert.ok") }