From f718b01ebe4f4aaf4ce0daa0769c7e8f773b1cef Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Thu, 27 Jun 2024 00:03:30 -0400 Subject: [PATCH 01/25] Move the accessoryCircular button to a separate file --- HomeAssistant.xcodeproj/project.pbxproj | 4 +++ .../Widgets/Assist/WidgetAssistView.swift | 14 +--------- .../Widgets/Common/WidgetCircularView.swift | 28 +++++++++++++++++++ 3 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 Sources/Extensions/Widgets/Common/WidgetCircularView.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index 58fa618e5..ac8fd27ed 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -503,6 +503,7 @@ 399792712B7F909900231B54 /* MobileAppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399792702B7F909900231B54 /* MobileAppConfig.swift */; }; 399792722B7F909900231B54 /* MobileAppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399792702B7F909900231B54 /* MobileAppConfig.swift */; }; 39A32EE22C0E384E00985722 /* UIImage+scaledToSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A32EE12C0E384E00985722 /* UIImage+scaledToSize.swift */; }; + 4008F0262C2D0A1A00E24001 /* WidgetCircularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4008F0252C2D0A1A00E24001 /* WidgetCircularView.swift */; }; 42070EE82BAC43240031E96F /* AssistSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42070EE72BAC43240031E96F /* AssistSession.swift */; }; 42070EEB2BAC517A0031E96F /* AssistInAppIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42070EE92BAC49D70031E96F /* AssistInAppIntentHandler.swift */; }; 42070EEC2BAC517A0031E96F /* AssistInAppIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42070EE92BAC49D70031E96F /* AssistInAppIntentHandler.swift */; }; @@ -1636,6 +1637,7 @@ 399792702B7F909900231B54 /* MobileAppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileAppConfig.swift; sourceTree = ""; }; 39A32EE12C0E384E00985722 /* UIImage+scaledToSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+scaledToSize.swift"; sourceTree = ""; }; 3F4DFB087A3A43F9A526B851 /* Pods_iOS_Shared_iOS_Tests_Shared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOS_Shared_iOS_Tests_Shared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4008F0252C2D0A1A00E24001 /* WidgetCircularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetCircularView.swift; sourceTree = ""; }; 42070EE72BAC43240031E96F /* AssistSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistSession.swift; sourceTree = ""; }; 42070EE92BAC49D70031E96F /* AssistInAppIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistInAppIntentHandler.swift; sourceTree = ""; }; 420B10032B1CF6D800D383D8 /* SharedAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = SharedAssets.xcassets; sourceTree = ""; }; @@ -2526,6 +2528,7 @@ 115560E227010DAB00A8F818 /* WidgetBasicView.swift */, 424A7F452B188946008C8DF3 /* WidgetBackground.swift */, 424A7F472B188BF3008C8DF3 /* WidgetContentMargin.swift */, + 4008F0252C2D0A1A00E24001 /* WidgetCircularView.swift */, ); path = Common; sourceTree = ""; @@ -5936,6 +5939,7 @@ 115560E327010DAB00A8F818 /* WidgetBasicView.swift in Sources */, 1171507024DFCDE60065E874 /* Widgets.swift in Sources */, 424A7F462B188946008C8DF3 /* WidgetBackground.swift in Sources */, + 4008F0262C2D0A1A00E24001 /* WidgetCircularView.swift in Sources */, 42F958992BB4684700497981 /* WidgetAssist.swift in Sources */, 4296C3772B91F26A0051B63C /* IntentActionAppEntity.swift in Sources */, 115560F227012FE100A8F818 /* WidgetOpenPageProvider.swift in Sources */, diff --git a/Sources/Extensions/Widgets/Assist/WidgetAssistView.swift b/Sources/Extensions/Widgets/Assist/WidgetAssistView.swift index d9e743dcf..0b94f0fa4 100644 --- a/Sources/Extensions/Widgets/Assist/WidgetAssistView.swift +++ b/Sources/Extensions/Widgets/Assist/WidgetAssistView.swift @@ -36,19 +36,7 @@ struct WidgetAssistView: View { } private var accessoryCircular: some View { - VStack(spacing: 2) { - Image(uiImage: MaterialDesignIcons.messageProcessingOutlineIcon.image( - ofSize: .init(width: 24, height: 24), - color: .white - )) - .foregroundStyle(.ultraThickMaterial) - Image(imageAsset: Asset.SharedAssets.logo) - .resizable() - .frame(width: 10, height: 10) - } - .padding() - .background(Color(uiColor: .secondarySystemBackground)) - .clipShape(Circle()) + WidgetCircularView(icon: MaterialDesignIcons.messageProcessingOutlineIcon) } private var singleHomeScreenItem: some View { diff --git a/Sources/Extensions/Widgets/Common/WidgetCircularView.swift b/Sources/Extensions/Widgets/Common/WidgetCircularView.swift new file mode 100644 index 000000000..9e058445a --- /dev/null +++ b/Sources/Extensions/Widgets/Common/WidgetCircularView.swift @@ -0,0 +1,28 @@ +import SwiftUI +import Shared + +struct WidgetCircularView: View { + var icon: MaterialDesignIcons + + private static func scaleLogo(logo: UIImage, size: CGFloat) -> UIImage { + let canvas = CGSize(width: size, height: size) + let format = logo.imageRendererFormat + return UIGraphicsImageRenderer(size: canvas, format: format).image { + _ in logo.draw(in: CGRect(origin: .zero, size: canvas)) + } + } + + var body: some View { + VStack(spacing: 2) { + Image(uiImage: icon.image( + ofSize: .init(width: 24, height: 24), + color: .white + )) + .foregroundStyle(.ultraThickMaterial) + Image(uiImage: Self.scaleLogo(logo: Asset.SharedAssets.logo.image, size: 10)) + } + .padding() + .background(Color(uiColor: .secondarySystemBackground)) + .clipShape(Circle()) + } +} From f3b50eeb0b30e1f21083092bf4996fb5750037dd Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Thu, 27 Jun 2024 00:07:56 -0400 Subject: [PATCH 02/25] Add accessoryCircular family to the actions widget --- .../Widgets/Actions/WidgetActions.swift | 8 +- .../Common/WidgetBasicContainerView.swift | 22 ++- .../Widgets/Common/WidgetBasicView.swift | 153 ++++++++++-------- 3 files changed, 103 insertions(+), 80 deletions(-) diff --git a/Sources/Extensions/Widgets/Actions/WidgetActions.swift b/Sources/Extensions/Widgets/Actions/WidgetActions.swift index 1153b3fc8..e10e4451e 100644 --- a/Sources/Extensions/Widgets/Actions/WidgetActions.swift +++ b/Sources/Extensions/Widgets/Actions/WidgetActions.swift @@ -32,7 +32,7 @@ struct WidgetActions: Widget { .contentMarginsDisabledIfAvailable() .configurationDisplayName(L10n.Widgets.Actions.title) .description(L10n.Widgets.Actions.description) - .supportedFamilies(WidgetActionSupportedFamilies.families) + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge, .accessoryCircular]) } } @@ -65,13 +65,9 @@ struct LegacyWidgetActions: Widget { .contentMarginsDisabledIfAvailable() .configurationDisplayName(L10n.Widgets.Actions.title) .description(L10n.Widgets.Actions.description) - .supportedFamilies(WidgetActionSupportedFamilies.families) + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge]) .onBackgroundURLSessionEvents(matching: nil) { identifier, completion in Current.webhooks.handleBackground(for: identifier, completionHandler: completion) } } } - -enum WidgetActionSupportedFamilies { - static let families: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge] -} diff --git a/Sources/Extensions/Widgets/Common/WidgetBasicContainerView.swift b/Sources/Extensions/Widgets/Common/WidgetBasicContainerView.swift index cb1fd24ca..b3a7c6f1d 100644 --- a/Sources/Extensions/Widgets/Common/WidgetBasicContainerView.swift +++ b/Sources/Extensions/Widgets/Common/WidgetBasicContainerView.swift @@ -28,8 +28,10 @@ struct WidgetBasicContainerView: View { func singleView(for model: WidgetBasicViewModel) -> some View { ZStack { - model.backgroundColor - .opacity(0.8) + if !Self.clearFamilies.contains(family) { + model.backgroundColor + .opacity(0.8) + } if case let .widgetURL(url) = model.interactionType { WidgetBasicView(model: model, sizeStyle: .single) .widgetURL(url.withWidgetAuthenticity()) @@ -86,8 +88,10 @@ struct WidgetBasicContainerView: View { ForEach(column) { model in ZStack { // stacking the color under makes the Link's highlight state nicer - model.backgroundColor - .opacity(0.8) + if !Self.clearFamilies.contains(family) { + model.backgroundColor + .opacity(0.8) + } if case let .widgetURL(url) = model.interactionType { Link(destination: url.withWidgetAuthenticity()) { WidgetBasicView(model: model, sizeStyle: sizeStyle) @@ -170,4 +174,14 @@ struct WidgetBasicContainerView: View { @unknown default: return 8 } } + + private static var clearFamilies: [WidgetFamily] { + var supportedFamilies: [WidgetFamily] = [] + + if #available(iOSApplicationExtension 16.0, *) { + supportedFamilies = [.accessoryCircular, .accessoryInline, .accessoryRectangular] + } + + return supportedFamilies + } } diff --git a/Sources/Extensions/Widgets/Common/WidgetBasicView.swift b/Sources/Extensions/Widgets/Common/WidgetBasicView.swift index 4813d8ba0..ca6ef9bee 100644 --- a/Sources/Extensions/Widgets/Common/WidgetBasicView.swift +++ b/Sources/Extensions/Widgets/Common/WidgetBasicView.swift @@ -105,6 +105,8 @@ enum WidgetBasicSizeStyle { } struct WidgetBasicView: View { + @Environment(\.widgetFamily) private var widgetFamily + private let model: WidgetBasicViewModel private let sizeStyle: WidgetBasicSizeStyle @@ -115,85 +117,96 @@ struct WidgetBasicView: View { } var body: some View { - ZStack(alignment: .leading) { - Rectangle().fill( - LinearGradient( - gradient: .init(colors: [.white.opacity(0.06), .black.opacity(0.06)]), - startPoint: .top, - endPoint: .bottom - ) - ) - - let text = Text(verbatim: model.title) - .font(sizeStyle.textFont) - .fontWeight(.semibold) - .multilineTextAlignment(.leading) - .foregroundColor(model.textColor) - .lineLimit(nil) - .minimumScaleFactor(0.5) - - let subtext: AnyView? = { - guard let subtitle = model.subtitle else { - return nil - } - - return AnyView( - Text(verbatim: subtitle) - .font(sizeStyle.subtextFont) - .foregroundColor(model.textColor.opacity(0.7)) - .lineLimit(1) - .truncationMode(.middle) + switch widgetFamily { + case .accessoryCircular, .accessoryRectangular: + WidgetCircularView(icon: model.icon) + case .accessoryInline: + Label { + Text(model.title) + } icon: { + Image(uiImage: model.icon.image(ofSize: .init(width: 10, height: 10), color: .white)) + } + default: + ZStack(alignment: .leading) { + Rectangle().fill( + LinearGradient( + gradient: .init(colors: [.white.opacity(0.06), .black.opacity(0.06)]), + startPoint: .top, + endPoint: .bottom + ) ) - }() - - let icon = HStack(alignment: .top, spacing: -1) { - Text(verbatim: model.icon.unicode) - .font(sizeStyle.iconFont) - .minimumScaleFactor(0.2) - .foregroundColor(model.iconColor) - .fixedSize(horizontal: false, vertical: false) - - if model.showsChevron { - // this sfsymbols is a little more legible at smaller size than mdi:open-in-new - Image(systemName: "arrow.up.forward.app") - .font(sizeStyle.chevronFont) + + let text = Text(verbatim: model.title) + .font(sizeStyle.textFont) + .fontWeight(.semibold) + .multilineTextAlignment(.leading) + .foregroundColor(model.textColor) + .lineLimit(nil) + .minimumScaleFactor(0.5) + + let subtext: AnyView? = { + guard let subtitle = model.subtitle else { + return nil + } + + return AnyView( + Text(verbatim: subtitle) + .font(sizeStyle.subtextFont) + .foregroundColor(model.textColor.opacity(0.7)) + .lineLimit(1) + .truncationMode(.middle) + ) + }() + + let icon = HStack(alignment: .top, spacing: -1) { + Text(verbatim: model.icon.unicode) + .font(sizeStyle.iconFont) + .minimumScaleFactor(0.2) .foregroundColor(model.iconColor) + .fixedSize(horizontal: false, vertical: false) + + if model.showsChevron { + // this sfsymbols is a little more legible at smaller size than mdi:open-in-new + Image(systemName: "arrow.up.forward.app") + .font(sizeStyle.chevronFont) + .foregroundColor(model.iconColor) + } } - } - - switch sizeStyle { - case .regular, .condensed: - HStack(alignment: .center, spacing: 6.0) { - icon - if let subtext { - VStack(alignment: .leading, spacing: -2) { + + switch sizeStyle { + case .regular, .condensed: + HStack(alignment: .center, spacing: 6.0) { + icon + if let subtext { + VStack(alignment: .leading, spacing: -2) { + text + subtext + } + } else { text - subtext } - } else { + Spacer() + }.padding( + .leading, 12 + ) + case .single, .expanded: + VStack(alignment: .leading, spacing: 0) { + icon + Spacer() text + if let subtext { + subtext + } } - Spacer() - }.padding( - .leading, 12 - ) - case .single, .expanded: - VStack(alignment: .leading, spacing: 0) { - icon - Spacer() - text - if let subtext { - subtext - } + .padding( + [.leading, .trailing] + ).padding( + [.top, .bottom], + sizeStyle == .regular ? 10 : /* use default */ nil + ) } - .padding( - [.leading, .trailing] - ).padding( - [.top, .bottom], - sizeStyle == .regular ? 10 : /* use default */ nil - ) } + .background(model.backgroundColor) } - .background(model.backgroundColor) } } From aff4022d21e85975a957b4908e72af942e1f9b5d Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Thu, 27 Jun 2024 00:19:55 -0400 Subject: [PATCH 03/25] Add accessoryCircular family to the open page widget --- .../Extensions/Widgets/OpenPage/WidgetOpenPage.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Sources/Extensions/Widgets/OpenPage/WidgetOpenPage.swift b/Sources/Extensions/Widgets/OpenPage/WidgetOpenPage.swift index d0976b77e..2da33424e 100644 --- a/Sources/Extensions/Widgets/OpenPage/WidgetOpenPage.swift +++ b/Sources/Extensions/Widgets/OpenPage/WidgetOpenPage.swift @@ -37,9 +37,19 @@ struct WidgetOpenPage: Widget { .contentMarginsDisabledIfAvailable() .configurationDisplayName(L10n.Widgets.OpenPage.title) .description(L10n.Widgets.OpenPage.description) - .supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge]) + .supportedFamilies(supportedFamilies) .onBackgroundURLSessionEvents(matching: nil) { identifier, completion in Current.webhooks.handleBackground(for: identifier, completionHandler: completion) } } + + private var supportedFamilies: [WidgetFamily] { + var supportedFamilies: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] + + if #available(iOSApplicationExtension 16.0, *) { + supportedFamilies.append(.accessoryCircular) + } + + return supportedFamilies + } } From de5ed06dff1e0b9bd549cca3fb67d32d3a6357ef Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Fri, 28 Jun 2024 23:46:22 -0400 Subject: [PATCH 04/25] Add base gauge widget --- HomeAssistant.xcodeproj/project.pbxproj | 40 +++++ .../AppIntents/IntentServerAppEntitiy.swift | 60 ++++++++ .../Widget/Gauge/WidgetGaugeAppIntent.swift | 107 +++++++++++++ ...WidgetGaugeAppIntentTimelineProvider.swift | 144 ++++++++++++++++++ .../Widgets/Gauge/WidgetGauge.swift | 36 +++++ .../Widgets/Gauge/WidgetGaugeView.swift | 30 ++++ Sources/Extensions/Widgets/Widgets.swift | 3 + 7 files changed, 420 insertions(+) create mode 100644 Sources/Extensions/AppIntents/IntentServerAppEntitiy.swift create mode 100644 Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift create mode 100644 Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift create mode 100644 Sources/Extensions/Widgets/Gauge/WidgetGauge.swift create mode 100644 Sources/Extensions/Widgets/Gauge/WidgetGaugeView.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index ac8fd27ed..2d14c7c18 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -504,6 +504,13 @@ 399792722B7F909900231B54 /* MobileAppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399792702B7F909900231B54 /* MobileAppConfig.swift */; }; 39A32EE22C0E384E00985722 /* UIImage+scaledToSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A32EE12C0E384E00985722 /* UIImage+scaledToSize.swift */; }; 4008F0262C2D0A1A00E24001 /* WidgetCircularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4008F0252C2D0A1A00E24001 /* WidgetCircularView.swift */; }; + 403AE9092C2E220200D48147 /* WidgetGauge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403AE9082C2E220200D48147 /* WidgetGauge.swift */; }; + 403AE90E2C2E28B200D48147 /* WidgetGaugeAppIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403AE90B2C2E28B200D48147 /* WidgetGaugeAppIntent.swift */; }; + 403AE9102C2E28B200D48147 /* WidgetGaugeAppIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403AE90B2C2E28B200D48147 /* WidgetGaugeAppIntent.swift */; }; + 403AE9122C2E2BFC00D48147 /* WidgetGaugeAppIntentTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403AE9112C2E2BFC00D48147 /* WidgetGaugeAppIntentTimelineProvider.swift */; }; + 403AE9272C2F333A00D48147 /* WidgetGaugeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403AE9262C2F333A00D48147 /* WidgetGaugeView.swift */; }; + 403AE92A2C2F3A9200D48147 /* IntentServerAppEntitiy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403AE9292C2F3A9200D48147 /* IntentServerAppEntitiy.swift */; }; + 403AE92B2C2F3A9200D48147 /* IntentServerAppEntitiy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403AE9292C2F3A9200D48147 /* IntentServerAppEntitiy.swift */; }; 42070EE82BAC43240031E96F /* AssistSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42070EE72BAC43240031E96F /* AssistSession.swift */; }; 42070EEB2BAC517A0031E96F /* AssistInAppIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42070EE92BAC49D70031E96F /* AssistInAppIntentHandler.swift */; }; 42070EEC2BAC517A0031E96F /* AssistInAppIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42070EE92BAC49D70031E96F /* AssistInAppIntentHandler.swift */; }; @@ -1638,6 +1645,11 @@ 39A32EE12C0E384E00985722 /* UIImage+scaledToSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+scaledToSize.swift"; sourceTree = ""; }; 3F4DFB087A3A43F9A526B851 /* Pods_iOS_Shared_iOS_Tests_Shared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOS_Shared_iOS_Tests_Shared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4008F0252C2D0A1A00E24001 /* WidgetCircularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetCircularView.swift; sourceTree = ""; }; + 403AE9082C2E220200D48147 /* WidgetGauge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetGauge.swift; sourceTree = ""; }; + 403AE90B2C2E28B200D48147 /* WidgetGaugeAppIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WidgetGaugeAppIntent.swift; path = Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift; sourceTree = SOURCE_ROOT; }; + 403AE9112C2E2BFC00D48147 /* WidgetGaugeAppIntentTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetGaugeAppIntentTimelineProvider.swift; sourceTree = ""; }; + 403AE9262C2F333A00D48147 /* WidgetGaugeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetGaugeView.swift; sourceTree = ""; }; + 403AE9292C2F3A9200D48147 /* IntentServerAppEntitiy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentServerAppEntitiy.swift; sourceTree = ""; }; 42070EE72BAC43240031E96F /* AssistSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistSession.swift; sourceTree = ""; }; 42070EE92BAC49D70031E96F /* AssistInAppIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistInAppIntentHandler.swift; sourceTree = ""; }; 420B10032B1CF6D800D383D8 /* SharedAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = SharedAssets.xcassets; sourceTree = ""; }; @@ -2651,6 +2663,7 @@ 115560DF27010D6700A8F818 /* Common */, 110E693E24E770BD004AA96D /* Actions */, 115560EA27012ED000A8F818 /* OpenPage */, + 403AE9072C2E214D00D48147 /* Gauge */, 1171508324DFCF960065E874 /* Resources */, 1171506F24DFCDE60065E874 /* Widgets.swift */, 110E694524E771AB004AA96D /* Color+Hex.swift */, @@ -3229,6 +3242,24 @@ path = WebView; sourceTree = ""; }; + 403AE9072C2E214D00D48147 /* Gauge */ = { + isa = PBXGroup; + children = ( + 403AE9082C2E220200D48147 /* WidgetGauge.swift */, + 403AE9262C2F333A00D48147 /* WidgetGaugeView.swift */, + ); + path = Gauge; + sourceTree = ""; + }; + 403AE90A2C2E28A200D48147 /* Gauge */ = { + isa = PBXGroup; + children = ( + 403AE90B2C2E28B200D48147 /* WidgetGaugeAppIntent.swift */, + 403AE9112C2E2BFC00D48147 /* WidgetGaugeAppIntentTimelineProvider.swift */, + ); + path = Gauge; + sourceTree = ""; + }; 420FE8472B5569ED00878E06 /* Actions */ = { isa = PBXGroup; children = ( @@ -3353,6 +3384,7 @@ children = ( 4296C3722B91F06D0051B63C /* Widget */, 4296C36B2B90DB630051B63C /* IntentActionAppEntity.swift */, + 403AE9292C2F3A9200D48147 /* IntentServerAppEntitiy.swift */, 4296C36C2B90DB630051B63C /* PerformAction.swift */, ); path = AppIntents; @@ -3362,6 +3394,7 @@ isa = PBXGroup; children = ( 4296C3732B91F0730051B63C /* Actions */, + 403AE90A2C2E28A200D48147 /* Gauge */, ); path = Widget; sourceTree = ""; @@ -5943,11 +5976,16 @@ 42F958992BB4684700497981 /* WidgetAssist.swift in Sources */, 4296C3772B91F26A0051B63C /* IntentActionAppEntity.swift in Sources */, 115560F227012FE100A8F818 /* WidgetOpenPageProvider.swift in Sources */, + 403AE9102C2E28B200D48147 /* WidgetGaugeAppIntent.swift in Sources */, 1165705627018C4E003906A7 /* WidgetEmptyView.swift in Sources */, 1171508124DFCEC50065E874 /* WidgetActions.swift in Sources */, + 403AE92B2C2F3A9200D48147 /* IntentServerAppEntitiy.swift in Sources */, + 403AE9272C2F333A00D48147 /* WidgetGaugeView.swift in Sources */, 4296C3782B91F6260051B63C /* PerformAction.swift in Sources */, 4296C37B2B92054C0051B63C /* WidgetActionsAppIntent.swift in Sources */, + 403AE9092C2E220200D48147 /* WidgetGauge.swift in Sources */, 42F9589F2BB4707F00497981 /* WidgetAssistView.swift in Sources */, + 403AE9122C2E2BFC00D48147 /* WidgetGaugeAppIntentTimelineProvider.swift in Sources */, 42F9589C2BB4691D00497981 /* WidgetAssistProvider.swift in Sources */, 110E694624E771AB004AA96D /* Color+Hex.swift in Sources */, ); @@ -6001,6 +6039,7 @@ buildActionMask = 2147483647; files = ( 115F9D7025F4B7B700CC6A45 /* TemplateSection.swift in Sources */, + 403AE92A2C2F3A9200D48147 /* IntentServerAppEntitiy.swift in Sources */, 1101568524D770B2009424C9 /* NFCReader.swift in Sources */, B648AE262275918F006972AF /* Scenes.swift in Sources */, 1185DF9A271FE60F00ED7D9A /* OnboardingAuthStep.swift in Sources */, @@ -6173,6 +6212,7 @@ 42DD84162B14D7AC00936F16 /* WebViewExternalBusMessage.swift in Sources */, 11EFCDD324F5F39100314D85 /* WebViewWindowController.swift in Sources */, 11EFCDE024F60E5900314D85 /* BasicSceneDelegate.swift in Sources */, + 403AE90E2C2E28B200D48147 /* WidgetGaugeAppIntent.swift in Sources */, 42FCCFFF2B9B1C310057783F /* ThreadCredentialsSharingView.swift in Sources */, 429106872BA9D22500D452F9 /* AudioRecorder.swift in Sources */, 425573C92B5572DB00145217 /* CarPlayServerListViewModel.swift in Sources */, diff --git a/Sources/Extensions/AppIntents/IntentServerAppEntitiy.swift b/Sources/Extensions/AppIntents/IntentServerAppEntitiy.swift new file mode 100644 index 000000000..f249d6507 --- /dev/null +++ b/Sources/Extensions/AppIntents/IntentServerAppEntitiy.swift @@ -0,0 +1,60 @@ +import AppIntents +import Foundation +import Shared + +@available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) +struct IntentServerAppEntity: AppEntity, Sendable { + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "MaterialDesignIcons") + + struct IntentServerAppEntityQuery: EntityQuery, EntityStringQuery { + func entities(for identifiers: [IntentServerAppEntity.ID]) async throws -> [IntentServerAppEntity] { + getServerEntities().filter { identifiers.contains($0.id) } + } + + func entities(matching string: String) async throws -> [IntentServerAppEntity] { + getServerEntities().filter { $0.getInfo()?.remoteName.contains(string) ?? false } + } + + func suggestedEntities() async throws -> [IntentServerAppEntity] { + getServerEntities() + } + + private func getServerEntities() -> [IntentServerAppEntity] { + Current.servers.all.map { IntentServerAppEntity(from: $0) } + } + + func defaultResult() async -> IntentServerAppEntity? { + let server = Current.servers.all.first + if server == nil { + return nil + } else { + return IntentServerAppEntity(from: server!) + } + } + } + + static let defaultQuery = IntentServerAppEntityQuery() + + var id: String + var displayRepresentation: DisplayRepresentation { + DisplayRepresentation( + title: .init(stringLiteral: getInfo()?.remoteName ?? "Unknown") + ) + } + + init (identifier: Identifier) { + self.id = identifier.rawValue + } + + init(from server: Server) { + self.init(identifier: server.identifier) + } + + func getServer() -> Server? { + Current.servers.server(for: .init(rawValue: id)) + } + + func getInfo() -> ServerInfo? { + getServer()?.info + } +} diff --git a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift new file mode 100644 index 000000000..aff6a6cfa --- /dev/null +++ b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift @@ -0,0 +1,107 @@ +import AppIntents +import AudioToolbox +import Foundation +import Shared + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +struct WidgetGaugeAppIntent: WidgetConfigurationIntent { + static let title: LocalizedStringResource = "Gauge" + static let description = IntentDescription("Display a gauge using data from templates") + + @Parameter(title: "Gauge Type", default: .normal) + var gaugeType: GaugeTypeAppEnum + + @Parameter(title: "Server", default: nil) + var server: IntentServerAppEntity + + @Parameter(title: "Value Template (0-1)", default: "", inputOptions: .init(capitalizationType: .none, multiline: true, autocorrect: false, smartQuotes: false, smartDashes: false)) + var valueTemplate: String + + @Parameter(title: "Value Label Template", default: "", inputOptions: .init(capitalizationType: .none, multiline: true, autocorrect: false, smartQuotes: false, smartDashes: false)) + var valueLabelTemplate: String + + @Parameter(title: "Max Label Template", default: "", inputOptions: .init(capitalizationType: .none, multiline: true, autocorrect: false, smartQuotes: false, smartDashes: false)) + var maxTemplate: String + + @Parameter(title: "Min Label Template", default: "", inputOptions: .init(capitalizationType: .none, multiline: true, autocorrect: false, smartQuotes: false, smartDashes: false)) + var minTemplate: String + + @Parameter(title: "Icon", default: "home", inputOptions: .init(capitalizationType: .none, autocorrect: false, smartQuotes: false, smartDashes: false)) + var icon: String + + @Parameter(title: "Run Action", default: false) + var runAction: Bool + + @Parameter(title: "Action", default: nil) + var action: IntentActionAppEntity? + + static var parameterSummary: some ParameterSummary { + When(\WidgetGaugeAppIntent.$runAction, .equalTo, true) { + When(\.$gaugeType, .equalTo, .normal) { + Summary() { + \.$gaugeType + + \.$server + \.$valueTemplate + + \.$valueLabelTemplate + \.$maxTemplate + \.$minTemplate + + \.$runAction + \.$action + } + } otherwise: { + Summary() { + \.$gaugeType + + \.$server + \.$valueTemplate + + \.$valueLabelTemplate + + \.$runAction + \.$action + } + } + } otherwise: { + When(\.$gaugeType, .equalTo, .normal) { + Summary() { + \.$gaugeType + + \.$server + \.$valueTemplate + + \.$valueLabelTemplate + \.$maxTemplate + \.$minTemplate + + \.$runAction + } + } otherwise: { + Summary() { + \.$gaugeType + + \.$server + \.$valueTemplate + + \.$valueLabelTemplate + + \.$runAction + } + } + } + } +} + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +enum GaugeTypeAppEnum: String, Codable, Sendable, AppEnum { + case normal + case capacity + + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "GaugeType") + static var caseDisplayRepresentations: [GaugeTypeAppEnum: DisplayRepresentation] = [ + .normal: DisplayRepresentation(title: "Normal"), + .capacity: DisplayRepresentation(title: "Capactity") + ] +} diff --git a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift new file mode 100644 index 000000000..be87025db --- /dev/null +++ b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift @@ -0,0 +1,144 @@ +import AppIntents +import RealmSwift +import Shared +import WidgetKit +import HAKit + +@available(iOS 17, *) +struct WidgetGaugeAppIntentTimelineProvider: AppIntentTimelineProvider { + typealias Entry = WidgetGaugeEntry + typealias Intent = WidgetGaugeAppIntent + + func snapshot(for configuration: WidgetGaugeAppIntent, in context: Context) async -> WidgetGaugeEntry { + do { + return try await entry(for: configuration, in: context) + } catch { + Current.Log.debug("Using placeholder for gauge widget snapshot") + return placeholder(in: context) + } + } + + func timeline(for configuration: WidgetGaugeAppIntent, in context: Context) async -> Timeline { + do { + let snapshot = try await entry(for: configuration, in: context) + Current.Log.debug("Reloading gauge widget") + return .init( + entries: [snapshot], + policy: .after( + Current.date() + .addingTimeInterval(WidgetGaugeDataSource.expiration.converted(to: .seconds).value) + ) + ) + } catch { + Current.Log.debug("Using placeholder for gauge widget") + return .init( + entries: [placeholder(in: context)], + policy: .after( + Current.date() + .addingTimeInterval(WidgetGaugeDataSource.fastExpiration.converted(to: .seconds).value) + ) + ) + } + } + + func placeholder(in context: Context) -> WidgetGaugeEntry { + .init( + gaugeType: .normal, + value: 0.5, + valueLabel: "?", max: "?", min: "?", + runAction: false, action: nil + ) + } + + private func entry(for configuration: WidgetGaugeAppIntent, in context: Context) async throws -> Entry { + guard Current.servers.all.count > 0 else { + Current.Log.error("Failed to fetch data for gauge widget: No servers exist") + throw WidgetGaugeDataError.noServers + } + + let server = configuration.server.getServer() ?? Current.servers.all.first! + let api = Current.api(for: server) + + let valueTemplate = !configuration.valueTemplate.isEmpty ? configuration.valueTemplate : "0" + let valueLabelTemplate = !configuration.valueLabelTemplate.isEmpty ? configuration.valueLabelTemplate : "0" + let maxTemplate = configuration.gaugeType == .normal && !configuration.maxTemplate.isEmpty ? configuration.maxTemplate : "0" + let minTemplate = configuration.gaugeType == .normal && !configuration.minTemplate.isEmpty ? configuration.minTemplate : "0" + let template = "\(valueTemplate)|\(valueLabelTemplate)|\(maxTemplate)|\(minTemplate)" + + let result = await withCheckedContinuation { continuation in + api.connection.send(.init(type: .rest(.post, "template"), data: ["template": template], shouldRetry: true)) { result in + continuation.resume(returning: result) + } + } + + var data: HAData? + switch (result) { + case let .success(resultData): + data = resultData + case let .failure(error): + Current.Log.error("Failed to render template for gauge widget: \(error)") + throw WidgetGaugeDataError.apiError + } + + var renderedTemplate: String? + switch (data!) { + case let .primitive(response): + renderedTemplate = response as? String + default: + Current.Log.error("Failed to render template for gauge widget: Bad response data") + throw WidgetGaugeDataError.badResponse + } + + let params = renderedTemplate!.split(separator: "|") + guard params.count == 4 else { + Current.Log.error("Failed to render template for gauge widget: Wrong length response") + throw WidgetGaugeDataError.badResponse + } + + return .init( + gaugeType: configuration.gaugeType, + + value: Double(params[0]) ?? 0.0, + + valueLabel: String(params[1]), + max: String(params[2]), + min: String(params[3]), + + runAction: configuration.runAction, + action: configuration.action?.asAction() + ) + } +} + +enum WidgetGaugeDataSource { + static var expiration: Measurement { +// .init(value: 2, unit: .hours) + .init(value: 2, unit: .minutes) + } + + static var fastExpiration: Measurement { + .init(value: 1, unit: .hours) + } +} + +@available(iOS 17, *) +struct WidgetGaugeEntry: TimelineEntry { + var date = Date() + + var gaugeType: GaugeTypeAppEnum + + var value: Double + + var valueLabel: String + var max: String + var min: String + + var runAction: Bool + var action: Action? +} + +enum WidgetGaugeDataError: Error { + case noServers + case apiError + case badResponse +} diff --git a/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift new file mode 100644 index 000000000..8b4434215 --- /dev/null +++ b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift @@ -0,0 +1,36 @@ +import Intents +import Shared +import SwiftUI +import WidgetKit + +@available(iOS 17, *) +struct WidgetGauge: Widget { + var body: some WidgetConfiguration { + AppIntentConfiguration( + kind: "WidgetGague", + intent: WidgetGaugeAppIntent.self, + provider: WidgetGaugeAppIntentTimelineProvider() + ) { timelineEntry in + if timelineEntry.runAction && timelineEntry.action != nil { + Button(intent: intent(for: timelineEntry)) { + WidgetGaugeView(entry: timelineEntry) + .widgetBackground(Color.clear) + } + .buttonStyle(.plain) + } else { + WidgetGaugeView(entry: timelineEntry) + .widgetBackground(Color.clear) + } + } + .contentMarginsDisabledIfAvailable() + .configurationDisplayName(L10n.Widgets.Assist.title) + .description(L10n.Widgets.Assist.description) + .supportedFamilies([.accessoryCircular]) + } + + private func intent(for entry: WidgetGaugeEntry) -> PerformAction { + let intent = PerformAction() + intent.action = IntentActionAppEntity(id: entry.action!.ID, displayString: entry.action!.Text) + return intent + } +} diff --git a/Sources/Extensions/Widgets/Gauge/WidgetGaugeView.swift b/Sources/Extensions/Widgets/Gauge/WidgetGaugeView.swift new file mode 100644 index 000000000..7fc84a89d --- /dev/null +++ b/Sources/Extensions/Widgets/Gauge/WidgetGaugeView.swift @@ -0,0 +1,30 @@ +import SwiftUI +import WidgetKit + +@available(iOS 17.0, *) +struct WidgetGaugeView: View { + var entry: WidgetGaugeEntry + + var body: some View { + switch (entry.gaugeType) { + case .normal: + Gauge(value: entry.value) { + Text(entry.valueLabel) + } currentValueLabel: { + Text(entry.valueLabel) + } minimumValueLabel: { + Text(entry.min) + } maximumValueLabel: { + Text(entry.max) + } + .gaugeStyle(.accessoryCircular) + case .capacity: + Gauge(value: entry.value) { + Text(entry.valueLabel) + } currentValueLabel: { + Text(entry.valueLabel) + } + .gaugeStyle(.accessoryCircularCapacity) + } + } +} diff --git a/Sources/Extensions/Widgets/Widgets.swift b/Sources/Extensions/Widgets/Widgets.swift index d2091e2ec..ae679ffc4 100644 --- a/Sources/Extensions/Widgets/Widgets.swift +++ b/Sources/Extensions/Widgets/Widgets.swift @@ -7,6 +7,9 @@ struct Widgets: WidgetBundle { WidgetAssist() actionsWidget() WidgetOpenPage() + if #available(iOS 17, *) { + WidgetGauge() + } } private func actionsWidget() -> some Widget { From 996ab5dec873e74571847f336315991e7074c5d4 Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Sat, 29 Jun 2024 08:42:37 -0400 Subject: [PATCH 05/25] Add localization for the gauge widget --- Sources/App/Resources/en.lproj/Localizable.strings | 4 +++- Sources/Extensions/Widgets/Gauge/WidgetGauge.swift | 4 ++-- Sources/Shared/Resources/Swiftgen/Strings.swift | 6 ++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index 0394a33a1..eaab0641f 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -854,4 +854,6 @@ Home Assistant is free and open source home automation software with a focus on "widgets.open_page.description" = "Open a frontend page in Home Assistant."; "widgets.open_page.not_configured" = "No Pages Available"; "widgets.open_page.title" = "Open Page"; -"yes_label" = "Yes"; \ No newline at end of file +"widgets.gauge.description" = "Display numeric states from Home Assistant"; +"widgets.gauge.title" = "Gauge"; +"yes_label" = "Yes"; diff --git a/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift index 8b4434215..b784d5a79 100644 --- a/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift +++ b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift @@ -23,8 +23,8 @@ struct WidgetGauge: Widget { } } .contentMarginsDisabledIfAvailable() - .configurationDisplayName(L10n.Widgets.Assist.title) - .description(L10n.Widgets.Assist.description) + .configurationDisplayName(L10n.Widgets.Gauge.title) + .description(L10n.Widgets.Gauge.description) .supportedFamilies([.accessoryCircular]) } diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index 8815423f5..2ef2c23f5 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -2866,6 +2866,12 @@ public enum L10n { /// Reload all widgets public static var reloadTimeline: String { return L10n.tr("Localizable", "widgets.button.reload_timeline") } } + public enum Gauge { + /// Display numeric states from Home Assistant + public static var description: String { return L10n.tr("Localizable", "widgets.gauge.description") } + /// Gauge + public static var title: String { return L10n.tr("Localizable", "widgets.gauge.title") } + } public enum OpenPage { /// Open a frontend page in Home Assistant. public static var description: String { return L10n.tr("Localizable", "widgets.open_page.description") } From a76099b2dc2b4eb8240f4afae1240c6eb9fa9ae4 Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Sat, 29 Jun 2024 11:45:50 -0400 Subject: [PATCH 06/25] Add a push notification to reload the gauge widget --- .../Extensions/Widgets/Gauge/WidgetGauge.swift | 2 +- .../NotificationsCommandManager.swift | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift index b784d5a79..7bce900c8 100644 --- a/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift +++ b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift @@ -7,7 +7,7 @@ import WidgetKit struct WidgetGauge: Widget { var body: some WidgetConfiguration { AppIntentConfiguration( - kind: "WidgetGague", + kind: "WidgetGauge", intent: WidgetGaugeAppIntent.self, provider: WidgetGaugeAppIntentTimelineProvider() ) { timelineEntry in diff --git a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift index 8d5219971..61d54f6bb 100644 --- a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift +++ b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift @@ -1,4 +1,5 @@ import Communicator +import WidgetKit import PromiseKit import UserNotifications @@ -19,6 +20,9 @@ public class NotificationCommandManager { public init() { register(command: "request_location_update", handler: HandlerLocationUpdate()) register(command: "clear_notification", handler: HandlerClearNotification()) + if #available(iOS 14, watchOS 9, macOS 11, *) { + register(command: "reload_widgets", handler: HandlerReloadWidgets()) + } #if os(iOS) register(command: "update_complications", handler: HandlerUpdateComplications()) @@ -94,6 +98,19 @@ private struct HandlerClearNotification: NotificationCommandHandler { } } +@available(iOS 14, watchOS 9, macOS 11, *) +private struct HandlerReloadWidgets: NotificationCommandHandler { + func handle(_ payload: [String: Any]) -> Promise { + Current.Log.verbose("reloading widgtes") + return Promise { seal in + DispatchQueue.main.async { + WidgetCenter.shared.reloadTimelines(ofKind: "WidgetGauge") + seal.fulfill(()) + } + } + } +} + #if os(iOS) private struct HandlerUpdateComplications: NotificationCommandHandler { func handle(_ payload: [String: Any]) -> Promise { From cb1366e3c91d843afcea8075c164018b14ec4f32 Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Sun, 30 Jun 2024 08:31:41 -0400 Subject: [PATCH 07/25] Use server name instead of remote name for the server picker --- Sources/Extensions/AppIntents/IntentServerAppEntitiy.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Extensions/AppIntents/IntentServerAppEntitiy.swift b/Sources/Extensions/AppIntents/IntentServerAppEntitiy.swift index f249d6507..ccb503a5c 100644 --- a/Sources/Extensions/AppIntents/IntentServerAppEntitiy.swift +++ b/Sources/Extensions/AppIntents/IntentServerAppEntitiy.swift @@ -38,7 +38,7 @@ struct IntentServerAppEntity: AppEntity, Sendable { var id: String var displayRepresentation: DisplayRepresentation { DisplayRepresentation( - title: .init(stringLiteral: getInfo()?.remoteName ?? "Unknown") + title: .init(stringLiteral: getInfo()?.name ?? "Unknown") ) } From 4b32660c654304bf7d38ca14d85afecd5026295e Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Sun, 30 Jun 2024 09:25:17 -0400 Subject: [PATCH 08/25] Fix log message --- .../NotificationCommands/NotificationsCommandManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift index 61d54f6bb..ba72933be 100644 --- a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift +++ b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift @@ -101,7 +101,7 @@ private struct HandlerClearNotification: NotificationCommandHandler { @available(iOS 14, watchOS 9, macOS 11, *) private struct HandlerReloadWidgets: NotificationCommandHandler { func handle(_ payload: [String: Any]) -> Promise { - Current.Log.verbose("reloading widgtes") + Current.Log.verbose("reloading widgets") return Promise { seal in DispatchQueue.main.async { WidgetCenter.shared.reloadTimelines(ofKind: "WidgetGauge") From 41ab44251197bb7abddfe977902c7aa13cd0a8db Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Sun, 30 Jun 2024 11:14:28 -0400 Subject: [PATCH 09/25] Add placeholders and the details widget --- HomeAssistant.xcodeproj/project.pbxproj | 34 +++++ .../Resources/en.lproj/Localizable.strings | 4 +- .../Details/WidgetDetailsAppIntent.swift | 51 +++++++ ...dgetDetailsAppIntentTimelineProvider.swift | 136 ++++++++++++++++++ .../Widget/Gauge/WidgetGaugeAppIntent.swift | 5 +- ...WidgetGaugeAppIntentTimelineProvider.swift | 26 ++-- .../Widgets/Details/WidgetDetails.swift | 36 +++++ .../Widgets/Details/WidgetDetailsView.swift | 45 ++++++ .../Widgets/Gauge/WidgetGaugeView.swift | 42 +++++- Sources/Extensions/Widgets/Widgets.swift | 1 + .../NotificationsCommandManager.swift | 1 + .../Shared/Resources/Swiftgen/Strings.swift | 8 +- 12 files changed, 365 insertions(+), 24 deletions(-) create mode 100644 Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift create mode 100644 Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntentTimelineProvider.swift create mode 100644 Sources/Extensions/Widgets/Details/WidgetDetails.swift create mode 100644 Sources/Extensions/Widgets/Details/WidgetDetailsView.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index 2d14c7c18..56837be37 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -511,6 +511,11 @@ 403AE9272C2F333A00D48147 /* WidgetGaugeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403AE9262C2F333A00D48147 /* WidgetGaugeView.swift */; }; 403AE92A2C2F3A9200D48147 /* IntentServerAppEntitiy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403AE9292C2F3A9200D48147 /* IntentServerAppEntitiy.swift */; }; 403AE92B2C2F3A9200D48147 /* IntentServerAppEntitiy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403AE9292C2F3A9200D48147 /* IntentServerAppEntitiy.swift */; }; + 4080D5BE2C319AA000099C88 /* WidgetDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4080D5BD2C319AA000099C88 /* WidgetDetailsView.swift */; }; + 4080D5BF2C319AA000099C88 /* WidgetDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4080D5BC2C319AA000099C88 /* WidgetDetails.swift */; }; + 4080D5C42C319B0A00099C88 /* WidgetDetailsAppIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4080D5C12C319B0A00099C88 /* WidgetDetailsAppIntent.swift */; }; + 4080D5C52C319B0A00099C88 /* WidgetDetailsAppIntentTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4080D5C22C319B0A00099C88 /* WidgetDetailsAppIntentTimelineProvider.swift */; }; + 4080D5C62C319B0A00099C88 /* WidgetDetailsAppIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4080D5C12C319B0A00099C88 /* WidgetDetailsAppIntent.swift */; }; 42070EE82BAC43240031E96F /* AssistSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42070EE72BAC43240031E96F /* AssistSession.swift */; }; 42070EEB2BAC517A0031E96F /* AssistInAppIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42070EE92BAC49D70031E96F /* AssistInAppIntentHandler.swift */; }; 42070EEC2BAC517A0031E96F /* AssistInAppIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42070EE92BAC49D70031E96F /* AssistInAppIntentHandler.swift */; }; @@ -1650,6 +1655,10 @@ 403AE9112C2E2BFC00D48147 /* WidgetGaugeAppIntentTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetGaugeAppIntentTimelineProvider.swift; sourceTree = ""; }; 403AE9262C2F333A00D48147 /* WidgetGaugeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetGaugeView.swift; sourceTree = ""; }; 403AE9292C2F3A9200D48147 /* IntentServerAppEntitiy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentServerAppEntitiy.swift; sourceTree = ""; }; + 4080D5BC2C319AA000099C88 /* WidgetDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDetails.swift; sourceTree = ""; }; + 4080D5BD2C319AA000099C88 /* WidgetDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDetailsView.swift; sourceTree = ""; }; + 4080D5C12C319B0A00099C88 /* WidgetDetailsAppIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WidgetDetailsAppIntent.swift; path = Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift; sourceTree = SOURCE_ROOT; }; + 4080D5C22C319B0A00099C88 /* WidgetDetailsAppIntentTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDetailsAppIntentTimelineProvider.swift; sourceTree = ""; }; 42070EE72BAC43240031E96F /* AssistSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistSession.swift; sourceTree = ""; }; 42070EE92BAC49D70031E96F /* AssistInAppIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistInAppIntentHandler.swift; sourceTree = ""; }; 420B10032B1CF6D800D383D8 /* SharedAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = SharedAssets.xcassets; sourceTree = ""; }; @@ -2664,6 +2673,7 @@ 110E693E24E770BD004AA96D /* Actions */, 115560EA27012ED000A8F818 /* OpenPage */, 403AE9072C2E214D00D48147 /* Gauge */, + 4080D5BB2C319A9100099C88 /* Details */, 1171508324DFCF960065E874 /* Resources */, 1171506F24DFCDE60065E874 /* Widgets.swift */, 110E694524E771AB004AA96D /* Color+Hex.swift */, @@ -3260,6 +3270,24 @@ path = Gauge; sourceTree = ""; }; + 4080D5BB2C319A9100099C88 /* Details */ = { + isa = PBXGroup; + children = ( + 4080D5BC2C319AA000099C88 /* WidgetDetails.swift */, + 4080D5BD2C319AA000099C88 /* WidgetDetailsView.swift */, + ); + path = Details; + sourceTree = ""; + }; + 4080D5C02C319AF400099C88 /* Details */ = { + isa = PBXGroup; + children = ( + 4080D5C12C319B0A00099C88 /* WidgetDetailsAppIntent.swift */, + 4080D5C22C319B0A00099C88 /* WidgetDetailsAppIntentTimelineProvider.swift */, + ); + path = Details; + sourceTree = ""; + }; 420FE8472B5569ED00878E06 /* Actions */ = { isa = PBXGroup; children = ( @@ -3395,6 +3423,7 @@ children = ( 4296C3732B91F0730051B63C /* Actions */, 403AE90A2C2E28A200D48147 /* Gauge */, + 4080D5C02C319AF400099C88 /* Details */, ); path = Widget; sourceTree = ""; @@ -5974,6 +6003,8 @@ 424A7F462B188946008C8DF3 /* WidgetBackground.swift in Sources */, 4008F0262C2D0A1A00E24001 /* WidgetCircularView.swift in Sources */, 42F958992BB4684700497981 /* WidgetAssist.swift in Sources */, + 4080D5C52C319B0A00099C88 /* WidgetDetailsAppIntentTimelineProvider.swift in Sources */, + 4080D5C62C319B0A00099C88 /* WidgetDetailsAppIntent.swift in Sources */, 4296C3772B91F26A0051B63C /* IntentActionAppEntity.swift in Sources */, 115560F227012FE100A8F818 /* WidgetOpenPageProvider.swift in Sources */, 403AE9102C2E28B200D48147 /* WidgetGaugeAppIntent.swift in Sources */, @@ -5982,6 +6013,8 @@ 403AE92B2C2F3A9200D48147 /* IntentServerAppEntitiy.swift in Sources */, 403AE9272C2F333A00D48147 /* WidgetGaugeView.swift in Sources */, 4296C3782B91F6260051B63C /* PerformAction.swift in Sources */, + 4080D5BE2C319AA000099C88 /* WidgetDetailsView.swift in Sources */, + 4080D5BF2C319AA000099C88 /* WidgetDetails.swift in Sources */, 4296C37B2B92054C0051B63C /* WidgetActionsAppIntent.swift in Sources */, 403AE9092C2E220200D48147 /* WidgetGauge.swift in Sources */, 42F9589F2BB4707F00497981 /* WidgetAssistView.swift in Sources */, @@ -6129,6 +6162,7 @@ 111858DF24CB83DF00B8CDDC /* Intents.intentdefinition in Sources */, B64BB3A81E9C6551001E8B46 /* WebViewController.swift in Sources */, 42FCCFE22B9B1B610057783F /* BarcodeScannerCamera.swift in Sources */, + 4080D5C42C319B0A00099C88 /* WidgetDetailsAppIntent.swift in Sources */, 11A71C7124A4648000D9565F /* ZoneManagerEquatableRegion.swift in Sources */, 42FCD0132B9B29740057783F /* ThreadCredentialsManagementViewModel.swift in Sources */, 11E99A5027156854003C8A65 /* OnboardingTerminalViewController.swift in Sources */, diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index eaab0641f..2cdad1ac1 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -854,6 +854,8 @@ Home Assistant is free and open source home automation software with a focus on "widgets.open_page.description" = "Open a frontend page in Home Assistant."; "widgets.open_page.not_configured" = "No Pages Available"; "widgets.open_page.title" = "Open Page"; -"widgets.gauge.description" = "Display numeric states from Home Assistant"; +"widgets.gauge.description" = "Display numeric states from Home Assistant in a gauge"; "widgets.gauge.title" = "Gauge"; +"widgets.details.description" = "Display states using from Home Assistant in text"; +"widgets.details.title" = "Details"; "yes_label" = "Yes"; diff --git a/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift b/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift new file mode 100644 index 000000000..ca0ce3a5c --- /dev/null +++ b/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift @@ -0,0 +1,51 @@ +import AppIntents +import AudioToolbox +import Foundation +import Shared + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +struct WidgetDetailsAppIntent: WidgetConfigurationIntent { + static let title: LocalizedStringResource = "Details" + static let description = IntentDescription("Display states using from Home Assistant in text") + + @Parameter(title: "Server", default: nil) + var server: IntentServerAppEntity + + @Parameter(title: "Upper Text Template", default: "", inputOptions: .init(capitalizationType: .none, multiline: true, autocorrect: false, smartQuotes: false, smartDashes: false)) + var upperTemplate: String + + @Parameter(title: "Lower Text Template", default: "", inputOptions: .init(capitalizationType: .none, multiline: true, autocorrect: false, smartQuotes: false, smartDashes: false)) + var lowerTemplate: String + + @Parameter(title: "Details Text Template (only in rectangular family)", default: "", inputOptions: .init(capitalizationType: .none, multiline: true, autocorrect: false, smartQuotes: false, smartDashes: false)) + var detailsTemplate: String + + @Parameter(title: "Run Action (only in rectangular family)", default: false) + var runAction: Bool + + @Parameter(title: "Action", default: nil) + var action: IntentActionAppEntity? + + static var parameterSummary: some ParameterSummary { + When(\WidgetDetailsAppIntent.$runAction, .equalTo, true) { + Summary() { + \.$server + \.$upperTemplate + \.$lowerTemplate + \.$detailsTemplate + + \.$runAction + \.$action + } + } otherwise: { + Summary() { + \.$server + \.$upperTemplate + \.$lowerTemplate + \.$detailsTemplate + + \.$runAction + } + } + } +} diff --git a/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntentTimelineProvider.swift b/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntentTimelineProvider.swift new file mode 100644 index 000000000..91f039110 --- /dev/null +++ b/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntentTimelineProvider.swift @@ -0,0 +1,136 @@ +import AppIntents +import RealmSwift +import Shared +import WidgetKit +import HAKit + +@available(iOS 17, *) +struct WidgetDetailsAppIntentTimelineProvider: AppIntentTimelineProvider { + + typealias Entry = WidgetDetailsEntry + typealias Intent = WidgetDetailsAppIntent + + func snapshot(for configuration: WidgetDetailsAppIntent, in context: Context) async -> WidgetDetailsEntry { + do { + return try await entry(for: configuration, in: context) + } catch { + Current.Log.debug("Using placeholder for gauge widget snapshot") + return placeholder(in: context) + } + } + + func timeline(for configuration: WidgetDetailsAppIntent, in context: Context) async -> Timeline { + do { + let snapshot = try await entry(for: configuration, in: context) + Current.Log.debug("Reloading gauge widget") + return .init( + entries: [snapshot], + policy: .after( + Current.date() + .addingTimeInterval(WidgetDetailsDataSource.expiration.converted(to: .seconds).value) + ) + ) + } catch { + Current.Log.debug("Using placeholder for gauge widget") + return .init( + entries: [placeholder(in: context)], + policy: .after( + Current.date() + .addingTimeInterval(WidgetDetailsDataSource.fastExpiration.converted(to: .seconds).value) + ) + ) + } + } + + func placeholder(in context: Context) -> WidgetDetailsEntry { + .init( + upperText: nil, lowerText: nil, detailsText: nil, + runAction: false, action: nil + ) + } + + private func entry(for configuration: WidgetDetailsAppIntent, in context: Context) async throws -> Entry { + guard Current.servers.all.count > 0 else { + Current.Log.error("Failed to fetch data for details widget: No servers exist") + throw WidgetDetailsDataError.noServers + } + + let server = configuration.server.getServer() ?? Current.servers.all.first! + let api = Current.api(for: server) + + let upperTemplate = !configuration.upperTemplate.isEmpty ? configuration.upperTemplate : "?" + let lowerTemplate = !configuration.lowerTemplate.isEmpty ? configuration.lowerTemplate : "?" + let detailsTemplate = !configuration.detailsTemplate.isEmpty ? configuration.detailsTemplate : "?" + let template = "\(upperTemplate)|\(lowerTemplate)|\(detailsTemplate)" + + let result = await withCheckedContinuation { continuation in + api.connection.send(.init(type: .rest(.post, "template"), data: ["template": template], shouldRetry: true)) { result in + continuation.resume(returning: result) + } + } + + var data: HAData? + switch (result) { + case let .success(resultData): + data = resultData + case let .failure(error): + Current.Log.error("Failed to render template for details widget: \(error)") + throw WidgetDetailsDataError.apiError + } + + var renderedTemplate: String? + switch (data!) { + case let .primitive(response): + renderedTemplate = response as? String + default: + Current.Log.error("Failed to render template for details widget: Bad response data") + throw WidgetDetailsDataError.badResponse + } + + let params = renderedTemplate!.split(separator: "|") + guard params.count == 3 else { + Current.Log.error("Failed to render template for details widget: Wrong length response") + throw WidgetDetailsDataError.badResponse + } + + let upperText = String(params[0]) + let lowerText = String(params[1]) + let detailsText = String(params[2]) + return .init( + upperText: upperText != "?" ? upperText : nil, + lowerText: lowerText != "?" ? lowerText : nil, + detailsText: detailsText != "?" ? detailsText : nil, + + runAction: configuration.runAction, + action: configuration.action?.asAction() + ) + } +} + +enum WidgetDetailsDataSource { + static var expiration: Measurement { + .init(value: 2, unit: .hours) + } + + static var fastExpiration: Measurement { + .init(value: 1, unit: .hours) + } +} + +@available(iOS 17, *) +struct WidgetDetailsEntry: TimelineEntry { + var date = Date() + + var upperText: String? + var lowerText: String? + var detailsText: String? + + var runAction: Bool + var action: Action? +} + +enum WidgetDetailsDataError: Error { + case noServers + case apiError + case badResponse +} diff --git a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift index aff6a6cfa..178d5ee4f 100644 --- a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift +++ b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift @@ -6,7 +6,7 @@ import Shared @available(iOS 17.0, macOS 14.0, watchOS 10.0, *) struct WidgetGaugeAppIntent: WidgetConfigurationIntent { static let title: LocalizedStringResource = "Gauge" - static let description = IntentDescription("Display a gauge using data from templates") + static let description = IntentDescription("Display numeric states from Home Assistant in a gauge") @Parameter(title: "Gauge Type", default: .normal) var gaugeType: GaugeTypeAppEnum @@ -26,9 +26,6 @@ struct WidgetGaugeAppIntent: WidgetConfigurationIntent { @Parameter(title: "Min Label Template", default: "", inputOptions: .init(capitalizationType: .none, multiline: true, autocorrect: false, smartQuotes: false, smartDashes: false)) var minTemplate: String - @Parameter(title: "Icon", default: "home", inputOptions: .init(capitalizationType: .none, autocorrect: false, smartQuotes: false, smartDashes: false)) - var icon: String - @Parameter(title: "Run Action", default: false) var runAction: Bool diff --git a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift index be87025db..ee5a707fc 100644 --- a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift +++ b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift @@ -59,10 +59,10 @@ struct WidgetGaugeAppIntentTimelineProvider: AppIntentTimelineProvider { let server = configuration.server.getServer() ?? Current.servers.all.first! let api = Current.api(for: server) - let valueTemplate = !configuration.valueTemplate.isEmpty ? configuration.valueTemplate : "0" - let valueLabelTemplate = !configuration.valueLabelTemplate.isEmpty ? configuration.valueLabelTemplate : "0" - let maxTemplate = configuration.gaugeType == .normal && !configuration.maxTemplate.isEmpty ? configuration.maxTemplate : "0" - let minTemplate = configuration.gaugeType == .normal && !configuration.minTemplate.isEmpty ? configuration.minTemplate : "0" + let valueTemplate = !configuration.valueTemplate.isEmpty ? configuration.valueTemplate : "0.0" + let valueLabelTemplate = !configuration.valueLabelTemplate.isEmpty ? configuration.valueLabelTemplate : "?" + let maxTemplate = configuration.gaugeType == .normal && !configuration.maxTemplate.isEmpty ? configuration.maxTemplate : "?" + let minTemplate = configuration.gaugeType == .normal && !configuration.minTemplate.isEmpty ? configuration.minTemplate : "?" let template = "\(valueTemplate)|\(valueLabelTemplate)|\(maxTemplate)|\(minTemplate)" let result = await withCheckedContinuation { continuation in @@ -95,14 +95,17 @@ struct WidgetGaugeAppIntentTimelineProvider: AppIntentTimelineProvider { throw WidgetGaugeDataError.badResponse } + let valueText = String(params[1]) + let maxText = String(params[2]) + let minText = String(params[3]) return .init( gaugeType: configuration.gaugeType, value: Double(params[0]) ?? 0.0, - valueLabel: String(params[1]), - max: String(params[2]), - min: String(params[3]), + valueLabel: valueText != "?" ? valueText : nil, + max: maxText != "?" ? maxText : nil, + min: minText != "?" ? minText : nil, runAction: configuration.runAction, action: configuration.action?.asAction() @@ -112,8 +115,7 @@ struct WidgetGaugeAppIntentTimelineProvider: AppIntentTimelineProvider { enum WidgetGaugeDataSource { static var expiration: Measurement { -// .init(value: 2, unit: .hours) - .init(value: 2, unit: .minutes) + .init(value: 2, unit: .hours) } static var fastExpiration: Measurement { @@ -129,9 +131,9 @@ struct WidgetGaugeEntry: TimelineEntry { var value: Double - var valueLabel: String - var max: String - var min: String + var valueLabel: String? + var max: String? + var min: String? var runAction: Bool var action: Action? diff --git a/Sources/Extensions/Widgets/Details/WidgetDetails.swift b/Sources/Extensions/Widgets/Details/WidgetDetails.swift new file mode 100644 index 000000000..59e5df852 --- /dev/null +++ b/Sources/Extensions/Widgets/Details/WidgetDetails.swift @@ -0,0 +1,36 @@ +import Intents +import Shared +import SwiftUI +import WidgetKit + +@available(iOS 17, *) +struct WidgetDetails: Widget { + var body: some WidgetConfiguration { + AppIntentConfiguration( + kind: "WidgetDetails", + intent: WidgetDetailsAppIntent.self, + provider: WidgetDetailsAppIntentTimelineProvider() + ) { timelineEntry in + if timelineEntry.runAction && timelineEntry.action != nil { + Button(intent: intent(for: timelineEntry)) { + WidgetDetailsView(entry: timelineEntry) + .widgetBackground(Color.clear) + } + .buttonStyle(.plain) + } else { + WidgetDetailsView(entry: timelineEntry) + .widgetBackground(Color.clear) + } + } + .contentMarginsDisabledIfAvailable() + .configurationDisplayName(L10n.Widgets.Details.title) + .description(L10n.Widgets.Details.description) + .supportedFamilies([.accessoryInline, .accessoryRectangular]) + } + + private func intent(for entry: WidgetDetailsEntry) -> PerformAction { + let intent = PerformAction() + intent.action = IntentActionAppEntity(id: entry.action!.ID, displayString: entry.action!.Text) + return intent + } +} diff --git a/Sources/Extensions/Widgets/Details/WidgetDetailsView.swift b/Sources/Extensions/Widgets/Details/WidgetDetailsView.swift new file mode 100644 index 000000000..65a1b2eaa --- /dev/null +++ b/Sources/Extensions/Widgets/Details/WidgetDetailsView.swift @@ -0,0 +1,45 @@ +import SwiftUI +import WidgetKit + +@available(iOS 17.0, *) +struct WidgetDetailsView: View { + @Environment(\.widgetFamily) var family: WidgetFamily + + var entry: WidgetDetailsEntry + + var body: some View { + if family == .accessoryRectangular { + VStack(alignment: .leading) { + if entry.upperText != nil { + Text(entry.upperText!) + .frame(maxWidth: .infinity, alignment: .leading) + .fontWeight(.bold) + } else { + Text("Unknown upper") + .frame(maxWidth: .infinity, alignment: .leading) + .fontWeight(.bold) + .redacted(reason: .placeholder) + } + if entry.lowerText != nil { + Text(entry.lowerText!) + .frame(maxWidth: .infinity, alignment: .leading) + } else { + Text("Unknown lower") + .frame(maxWidth: .infinity, alignment: .leading) + .redacted(reason: .placeholder) + } + if entry.detailsText != nil { + Text(entry.detailsText!) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } else { + if entry.upperText != nil || entry.lowerText != nil { + Text((entry.upperText ?? "") + (entry.lowerText ?? "")) + } else { + Text("Unknown details") + .redacted(reason: .placeholder) + } + } + } +} diff --git a/Sources/Extensions/Widgets/Gauge/WidgetGaugeView.swift b/Sources/Extensions/Widgets/Gauge/WidgetGaugeView.swift index 7fc84a89d..4df97f134 100644 --- a/Sources/Extensions/Widgets/Gauge/WidgetGaugeView.swift +++ b/Sources/Extensions/Widgets/Gauge/WidgetGaugeView.swift @@ -9,20 +9,50 @@ struct WidgetGaugeView: View { switch (entry.gaugeType) { case .normal: Gauge(value: entry.value) { - Text(entry.valueLabel) + if entry.valueLabel != nil { + Text(entry.valueLabel!) + } else { + Text("00") + .redacted(reason: .placeholder) + } } currentValueLabel: { - Text(entry.valueLabel) + if entry.valueLabel != nil { + Text(entry.valueLabel!) + } else { + Text("00") + .redacted(reason: .placeholder) + } } minimumValueLabel: { - Text(entry.min) + if entry.min != nil { + Text(entry.min!) + } else { + Text("00") + .redacted(reason: .placeholder) + } } maximumValueLabel: { - Text(entry.max) + if entry.max != nil { + Text(entry.max!) + } else { + Text("00") + .redacted(reason: .placeholder) + } } .gaugeStyle(.accessoryCircular) case .capacity: Gauge(value: entry.value) { - Text(entry.valueLabel) + if entry.valueLabel != nil { + Text(entry.valueLabel!) + } else { + Text("00") + .redacted(reason: .placeholder) + } } currentValueLabel: { - Text(entry.valueLabel) + if entry.valueLabel != nil { + Text(entry.valueLabel!) + } else { + Text("00") + .redacted(reason: .placeholder) + } } .gaugeStyle(.accessoryCircularCapacity) } diff --git a/Sources/Extensions/Widgets/Widgets.swift b/Sources/Extensions/Widgets/Widgets.swift index ae679ffc4..62788e113 100644 --- a/Sources/Extensions/Widgets/Widgets.swift +++ b/Sources/Extensions/Widgets/Widgets.swift @@ -9,6 +9,7 @@ struct Widgets: WidgetBundle { WidgetOpenPage() if #available(iOS 17, *) { WidgetGauge() + WidgetDetails() } } diff --git a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift index ba72933be..95fa6880d 100644 --- a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift +++ b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift @@ -105,6 +105,7 @@ private struct HandlerReloadWidgets: NotificationCommandHandler { return Promise { seal in DispatchQueue.main.async { WidgetCenter.shared.reloadTimelines(ofKind: "WidgetGauge") + WidgetCenter.shared.reloadTimelines(ofKind: "WidgetDetails") seal.fulfill(()) } } diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index 2ef2c23f5..185a96e47 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -2866,8 +2866,14 @@ public enum L10n { /// Reload all widgets public static var reloadTimeline: String { return L10n.tr("Localizable", "widgets.button.reload_timeline") } } + public enum Details { + /// Display states using from Home Assistant in text + public static var description: String { return L10n.tr("Localizable", "widgets.details.description") } + /// Details + public static var title: String { return L10n.tr("Localizable", "widgets.details.title") } + } public enum Gauge { - /// Display numeric states from Home Assistant + /// Display numeric states from Home Assistant in a gauge public static var description: String { return L10n.tr("Localizable", "widgets.gauge.description") } /// Gauge public static var title: String { return L10n.tr("Localizable", "widgets.gauge.title") } From 15bcc7c13413b761155b90d74623c3225c58aed1 Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Sun, 30 Jun 2024 14:10:32 -0400 Subject: [PATCH 10/25] Chnage the gauge and details identifiers --- Sources/Extensions/Widgets/Details/WidgetDetails.swift | 2 +- Sources/Extensions/Widgets/Gauge/WidgetGauge.swift | 2 +- .../NotificationCommands/NotificationsCommandManager.swift | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Extensions/Widgets/Details/WidgetDetails.swift b/Sources/Extensions/Widgets/Details/WidgetDetails.swift index 59e5df852..957291fc9 100644 --- a/Sources/Extensions/Widgets/Details/WidgetDetails.swift +++ b/Sources/Extensions/Widgets/Details/WidgetDetails.swift @@ -7,7 +7,7 @@ import WidgetKit struct WidgetDetails: Widget { var body: some WidgetConfiguration { AppIntentConfiguration( - kind: "WidgetDetails", + kind: "io.robbie.HomeAssistant.widget-details", intent: WidgetDetailsAppIntent.self, provider: WidgetDetailsAppIntentTimelineProvider() ) { timelineEntry in diff --git a/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift index 7bce900c8..1ee8670c7 100644 --- a/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift +++ b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift @@ -7,7 +7,7 @@ import WidgetKit struct WidgetGauge: Widget { var body: some WidgetConfiguration { AppIntentConfiguration( - kind: "WidgetGauge", + kind: "io.robbie.HomeAssistant.widget-gauge", intent: WidgetGaugeAppIntent.self, provider: WidgetGaugeAppIntentTimelineProvider() ) { timelineEntry in diff --git a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift index 95fa6880d..bb8c76375 100644 --- a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift +++ b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift @@ -104,8 +104,8 @@ private struct HandlerReloadWidgets: NotificationCommandHandler { Current.Log.verbose("reloading widgets") return Promise { seal in DispatchQueue.main.async { - WidgetCenter.shared.reloadTimelines(ofKind: "WidgetGauge") - WidgetCenter.shared.reloadTimelines(ofKind: "WidgetDetails") + WidgetCenter.shared.reloadTimelines(ofKind: "io.robbie.HomeAssistant.widget-gauge") + WidgetCenter.shared.reloadTimelines(ofKind: "io.robbie.HomeAssistant.widget-details") seal.fulfill(()) } } From 9b53238f1b32976f5963718865dba6f987a23fec Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:16:01 -0400 Subject: [PATCH 11/25] Run linter --- .../AppIntents/IntentServerAppEntitiy.swift | 12 +-- .../Details/WidgetDetailsAppIntent.swift | 58 +++++++--- ...dgetDetailsAppIntentTimelineProvider.swift | 35 +++--- .../Widget/Gauge/WidgetGaugeAppIntent.swift | 102 ++++++++++++------ ...WidgetGaugeAppIntentTimelineProvider.swift | 48 +++++---- .../Common/WidgetBasicContainerView.swift | 6 +- .../Widgets/Common/WidgetBasicView.swift | 14 +-- .../Widgets/Common/WidgetCircularView.swift | 6 +- .../Widgets/Details/WidgetDetails.swift | 4 +- .../Widgets/Details/WidgetDetailsView.swift | 4 +- .../Widgets/Gauge/WidgetGauge.swift | 4 +- .../Widgets/Gauge/WidgetGaugeView.swift | 4 +- .../Widgets/OpenPage/WidgetOpenPage.swift | 2 +- .../NotificationsCommandManager.swift | 2 +- 14 files changed, 190 insertions(+), 111 deletions(-) diff --git a/Sources/Extensions/AppIntents/IntentServerAppEntitiy.swift b/Sources/Extensions/AppIntents/IntentServerAppEntitiy.swift index ccb503a5c..21351bb68 100644 --- a/Sources/Extensions/AppIntents/IntentServerAppEntitiy.swift +++ b/Sources/Extensions/AppIntents/IntentServerAppEntitiy.swift @@ -22,7 +22,7 @@ struct IntentServerAppEntity: AppEntity, Sendable { private func getServerEntities() -> [IntentServerAppEntity] { Current.servers.all.map { IntentServerAppEntity(from: $0) } } - + func defaultResult() async -> IntentServerAppEntity? { let server = Current.servers.all.first if server == nil { @@ -41,19 +41,19 @@ struct IntentServerAppEntity: AppEntity, Sendable { title: .init(stringLiteral: getInfo()?.name ?? "Unknown") ) } - - init (identifier: Identifier) { + + init(identifier: Identifier) { self.id = identifier.rawValue } - + init(from server: Server) { self.init(identifier: server.identifier) } - + func getServer() -> Server? { Current.servers.server(for: .init(rawValue: id)) } - + func getInfo() -> ServerInfo? { getServer()?.info } diff --git a/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift b/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift index ca0ce3a5c..f5a6f30cf 100644 --- a/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift +++ b/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift @@ -7,43 +7,73 @@ import Shared struct WidgetDetailsAppIntent: WidgetConfigurationIntent { static let title: LocalizedStringResource = "Details" static let description = IntentDescription("Display states using from Home Assistant in text") - + @Parameter(title: "Server", default: nil) var server: IntentServerAppEntity - - @Parameter(title: "Upper Text Template", default: "", inputOptions: .init(capitalizationType: .none, multiline: true, autocorrect: false, smartQuotes: false, smartDashes: false)) + + @Parameter( + title: "Upper Text Template", + default: "", + inputOptions: .init( + capitalizationType: .none, + multiline: true, + autocorrect: false, + smartQuotes: false, + smartDashes: false + ) + ) var upperTemplate: String - - @Parameter(title: "Lower Text Template", default: "", inputOptions: .init(capitalizationType: .none, multiline: true, autocorrect: false, smartQuotes: false, smartDashes: false)) + + @Parameter( + title: "Lower Text Template", + default: "", + inputOptions: .init( + capitalizationType: .none, + multiline: true, + autocorrect: false, + smartQuotes: false, + smartDashes: false + ) + ) var lowerTemplate: String - - @Parameter(title: "Details Text Template (only in rectangular family)", default: "", inputOptions: .init(capitalizationType: .none, multiline: true, autocorrect: false, smartQuotes: false, smartDashes: false)) + + @Parameter( + title: "Details Text Template (only in rectangular family)", + default: "", + inputOptions: .init( + capitalizationType: .none, + multiline: true, + autocorrect: false, + smartQuotes: false, + smartDashes: false + ) + ) var detailsTemplate: String - + @Parameter(title: "Run Action (only in rectangular family)", default: false) var runAction: Bool - + @Parameter(title: "Action", default: nil) var action: IntentActionAppEntity? - + static var parameterSummary: some ParameterSummary { When(\WidgetDetailsAppIntent.$runAction, .equalTo, true) { - Summary() { + Summary { \.$server \.$upperTemplate \.$lowerTemplate \.$detailsTemplate - + \.$runAction \.$action } } otherwise: { - Summary() { + Summary { \.$server \.$upperTemplate \.$lowerTemplate \.$detailsTemplate - + \.$runAction } } diff --git a/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntentTimelineProvider.swift b/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntentTimelineProvider.swift index 91f039110..43d8ce055 100644 --- a/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntentTimelineProvider.swift +++ b/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntentTimelineProvider.swift @@ -1,12 +1,11 @@ import AppIntents +import HAKit import RealmSwift import Shared import WidgetKit -import HAKit @available(iOS 17, *) struct WidgetDetailsAppIntentTimelineProvider: AppIntentTimelineProvider { - typealias Entry = WidgetDetailsEntry typealias Intent = WidgetDetailsAppIntent @@ -54,45 +53,49 @@ struct WidgetDetailsAppIntentTimelineProvider: AppIntentTimelineProvider { Current.Log.error("Failed to fetch data for details widget: No servers exist") throw WidgetDetailsDataError.noServers } - + let server = configuration.server.getServer() ?? Current.servers.all.first! let api = Current.api(for: server) - + let upperTemplate = !configuration.upperTemplate.isEmpty ? configuration.upperTemplate : "?" let lowerTemplate = !configuration.lowerTemplate.isEmpty ? configuration.lowerTemplate : "?" let detailsTemplate = !configuration.detailsTemplate.isEmpty ? configuration.detailsTemplate : "?" let template = "\(upperTemplate)|\(lowerTemplate)|\(detailsTemplate)" - + let result = await withCheckedContinuation { continuation in - api.connection.send(.init(type: .rest(.post, "template"), data: ["template": template], shouldRetry: true)) { result in + api.connection.send(.init( + type: .rest(.post, "template"), + data: ["template": template], + shouldRetry: true + )) { result in continuation.resume(returning: result) } } - + var data: HAData? - switch (result) { + switch result { case let .success(resultData): data = resultData case let .failure(error): Current.Log.error("Failed to render template for details widget: \(error)") throw WidgetDetailsDataError.apiError } - + var renderedTemplate: String? - switch (data!) { + switch data! { case let .primitive(response): renderedTemplate = response as? String default: Current.Log.error("Failed to render template for details widget: Bad response data") throw WidgetDetailsDataError.badResponse } - + let params = renderedTemplate!.split(separator: "|") guard params.count == 3 else { Current.Log.error("Failed to render template for details widget: Wrong length response") throw WidgetDetailsDataError.badResponse } - + let upperText = String(params[0]) let lowerText = String(params[1]) let detailsText = String(params[2]) @@ -100,7 +103,7 @@ struct WidgetDetailsAppIntentTimelineProvider: AppIntentTimelineProvider { upperText: upperText != "?" ? upperText : nil, lowerText: lowerText != "?" ? lowerText : nil, detailsText: detailsText != "?" ? detailsText : nil, - + runAction: configuration.runAction, action: configuration.action?.asAction() ) @@ -111,7 +114,7 @@ enum WidgetDetailsDataSource { static var expiration: Measurement { .init(value: 2, unit: .hours) } - + static var fastExpiration: Measurement { .init(value: 1, unit: .hours) } @@ -120,11 +123,11 @@ enum WidgetDetailsDataSource { @available(iOS 17, *) struct WidgetDetailsEntry: TimelineEntry { var date = Date() - + var upperText: String? var lowerText: String? var detailsText: String? - + var runAction: Bool var action: Action? } diff --git a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift index 178d5ee4f..75be48f3d 100644 --- a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift +++ b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift @@ -7,83 +7,123 @@ import Shared struct WidgetGaugeAppIntent: WidgetConfigurationIntent { static let title: LocalizedStringResource = "Gauge" static let description = IntentDescription("Display numeric states from Home Assistant in a gauge") - + @Parameter(title: "Gauge Type", default: .normal) var gaugeType: GaugeTypeAppEnum - + @Parameter(title: "Server", default: nil) var server: IntentServerAppEntity - - @Parameter(title: "Value Template (0-1)", default: "", inputOptions: .init(capitalizationType: .none, multiline: true, autocorrect: false, smartQuotes: false, smartDashes: false)) + + @Parameter( + title: "Value Template (0-1)", + default: "", + inputOptions: .init( + capitalizationType: .none, + multiline: true, + autocorrect: false, + smartQuotes: false, + smartDashes: false + ) + ) var valueTemplate: String - - @Parameter(title: "Value Label Template", default: "", inputOptions: .init(capitalizationType: .none, multiline: true, autocorrect: false, smartQuotes: false, smartDashes: false)) + + @Parameter( + title: "Value Label Template", + default: "", + inputOptions: .init( + capitalizationType: .none, + multiline: true, + autocorrect: false, + smartQuotes: false, + smartDashes: false + ) + ) var valueLabelTemplate: String - - @Parameter(title: "Max Label Template", default: "", inputOptions: .init(capitalizationType: .none, multiline: true, autocorrect: false, smartQuotes: false, smartDashes: false)) + + @Parameter( + title: "Max Label Template", + default: "", + inputOptions: .init( + capitalizationType: .none, + multiline: true, + autocorrect: false, + smartQuotes: false, + smartDashes: false + ) + ) var maxTemplate: String - - @Parameter(title: "Min Label Template", default: "", inputOptions: .init(capitalizationType: .none, multiline: true, autocorrect: false, smartQuotes: false, smartDashes: false)) + + @Parameter( + title: "Min Label Template", + default: "", + inputOptions: .init( + capitalizationType: .none, + multiline: true, + autocorrect: false, + smartQuotes: false, + smartDashes: false + ) + ) var minTemplate: String - + @Parameter(title: "Run Action", default: false) var runAction: Bool - + @Parameter(title: "Action", default: nil) var action: IntentActionAppEntity? - + static var parameterSummary: some ParameterSummary { When(\WidgetGaugeAppIntent.$runAction, .equalTo, true) { When(\.$gaugeType, .equalTo, .normal) { - Summary() { + Summary { \.$gaugeType - + \.$server \.$valueTemplate - + \.$valueLabelTemplate \.$maxTemplate \.$minTemplate - + \.$runAction \.$action } } otherwise: { - Summary() { + Summary { \.$gaugeType - + \.$server \.$valueTemplate - + \.$valueLabelTemplate - + \.$runAction \.$action } } } otherwise: { When(\.$gaugeType, .equalTo, .normal) { - Summary() { + Summary { \.$gaugeType - + \.$server \.$valueTemplate - + \.$valueLabelTemplate \.$maxTemplate \.$minTemplate - + \.$runAction } } otherwise: { - Summary() { + Summary { \.$gaugeType - + \.$server \.$valueTemplate - + \.$valueLabelTemplate - + \.$runAction } } @@ -95,10 +135,10 @@ struct WidgetGaugeAppIntent: WidgetConfigurationIntent { enum GaugeTypeAppEnum: String, Codable, Sendable, AppEnum { case normal case capacity - + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "GaugeType") static var caseDisplayRepresentations: [GaugeTypeAppEnum: DisplayRepresentation] = [ .normal: DisplayRepresentation(title: "Normal"), - .capacity: DisplayRepresentation(title: "Capactity") + .capacity: DisplayRepresentation(title: "Capactity"), ] } diff --git a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift index ee5a707fc..c0c5b273b 100644 --- a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift +++ b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift @@ -1,8 +1,8 @@ import AppIntents +import HAKit import RealmSwift import Shared import WidgetKit -import HAKit @available(iOS 17, *) struct WidgetGaugeAppIntentTimelineProvider: AppIntentTimelineProvider { @@ -55,58 +55,64 @@ struct WidgetGaugeAppIntentTimelineProvider: AppIntentTimelineProvider { Current.Log.error("Failed to fetch data for gauge widget: No servers exist") throw WidgetGaugeDataError.noServers } - + let server = configuration.server.getServer() ?? Current.servers.all.first! let api = Current.api(for: server) - + let valueTemplate = !configuration.valueTemplate.isEmpty ? configuration.valueTemplate : "0.0" let valueLabelTemplate = !configuration.valueLabelTemplate.isEmpty ? configuration.valueLabelTemplate : "?" - let maxTemplate = configuration.gaugeType == .normal && !configuration.maxTemplate.isEmpty ? configuration.maxTemplate : "?" - let minTemplate = configuration.gaugeType == .normal && !configuration.minTemplate.isEmpty ? configuration.minTemplate : "?" + let maxTemplate = configuration.gaugeType == .normal && !configuration.maxTemplate.isEmpty ? configuration + .maxTemplate : "?" + let minTemplate = configuration.gaugeType == .normal && !configuration.minTemplate.isEmpty ? configuration + .minTemplate : "?" let template = "\(valueTemplate)|\(valueLabelTemplate)|\(maxTemplate)|\(minTemplate)" - + let result = await withCheckedContinuation { continuation in - api.connection.send(.init(type: .rest(.post, "template"), data: ["template": template], shouldRetry: true)) { result in + api.connection.send(.init( + type: .rest(.post, "template"), + data: ["template": template], + shouldRetry: true + )) { result in continuation.resume(returning: result) } } - + var data: HAData? - switch (result) { + switch result { case let .success(resultData): data = resultData case let .failure(error): Current.Log.error("Failed to render template for gauge widget: \(error)") throw WidgetGaugeDataError.apiError } - + var renderedTemplate: String? - switch (data!) { + switch data! { case let .primitive(response): renderedTemplate = response as? String default: Current.Log.error("Failed to render template for gauge widget: Bad response data") throw WidgetGaugeDataError.badResponse } - + let params = renderedTemplate!.split(separator: "|") guard params.count == 4 else { Current.Log.error("Failed to render template for gauge widget: Wrong length response") throw WidgetGaugeDataError.badResponse } - + let valueText = String(params[1]) let maxText = String(params[2]) let minText = String(params[3]) return .init( gaugeType: configuration.gaugeType, - + value: Double(params[0]) ?? 0.0, - + valueLabel: valueText != "?" ? valueText : nil, max: maxText != "?" ? maxText : nil, min: minText != "?" ? minText : nil, - + runAction: configuration.runAction, action: configuration.action?.asAction() ) @@ -117,7 +123,7 @@ enum WidgetGaugeDataSource { static var expiration: Measurement { .init(value: 2, unit: .hours) } - + static var fastExpiration: Measurement { .init(value: 1, unit: .hours) } @@ -126,15 +132,15 @@ enum WidgetGaugeDataSource { @available(iOS 17, *) struct WidgetGaugeEntry: TimelineEntry { var date = Date() - + var gaugeType: GaugeTypeAppEnum - + var value: Double - + var valueLabel: String? var max: String? var min: String? - + var runAction: Bool var action: Action? } diff --git a/Sources/Extensions/Widgets/Common/WidgetBasicContainerView.swift b/Sources/Extensions/Widgets/Common/WidgetBasicContainerView.swift index b3a7c6f1d..5e5cfcf8f 100644 --- a/Sources/Extensions/Widgets/Common/WidgetBasicContainerView.swift +++ b/Sources/Extensions/Widgets/Common/WidgetBasicContainerView.swift @@ -174,14 +174,14 @@ struct WidgetBasicContainerView: View { @unknown default: return 8 } } - + private static var clearFamilies: [WidgetFamily] { var supportedFamilies: [WidgetFamily] = [] - + if #available(iOSApplicationExtension 16.0, *) { supportedFamilies = [.accessoryCircular, .accessoryInline, .accessoryRectangular] } - + return supportedFamilies } } diff --git a/Sources/Extensions/Widgets/Common/WidgetBasicView.swift b/Sources/Extensions/Widgets/Common/WidgetBasicView.swift index ca6ef9bee..2022bd397 100644 --- a/Sources/Extensions/Widgets/Common/WidgetBasicView.swift +++ b/Sources/Extensions/Widgets/Common/WidgetBasicView.swift @@ -106,7 +106,7 @@ enum WidgetBasicSizeStyle { struct WidgetBasicView: View { @Environment(\.widgetFamily) private var widgetFamily - + private let model: WidgetBasicViewModel private let sizeStyle: WidgetBasicSizeStyle @@ -135,7 +135,7 @@ struct WidgetBasicView: View { endPoint: .bottom ) ) - + let text = Text(verbatim: model.title) .font(sizeStyle.textFont) .fontWeight(.semibold) @@ -143,12 +143,12 @@ struct WidgetBasicView: View { .foregroundColor(model.textColor) .lineLimit(nil) .minimumScaleFactor(0.5) - + let subtext: AnyView? = { guard let subtitle = model.subtitle else { return nil } - + return AnyView( Text(verbatim: subtitle) .font(sizeStyle.subtextFont) @@ -157,14 +157,14 @@ struct WidgetBasicView: View { .truncationMode(.middle) ) }() - + let icon = HStack(alignment: .top, spacing: -1) { Text(verbatim: model.icon.unicode) .font(sizeStyle.iconFont) .minimumScaleFactor(0.2) .foregroundColor(model.iconColor) .fixedSize(horizontal: false, vertical: false) - + if model.showsChevron { // this sfsymbols is a little more legible at smaller size than mdi:open-in-new Image(systemName: "arrow.up.forward.app") @@ -172,7 +172,7 @@ struct WidgetBasicView: View { .foregroundColor(model.iconColor) } } - + switch sizeStyle { case .regular, .condensed: HStack(alignment: .center, spacing: 6.0) { diff --git a/Sources/Extensions/Widgets/Common/WidgetCircularView.swift b/Sources/Extensions/Widgets/Common/WidgetCircularView.swift index 9e058445a..f49db1f3e 100644 --- a/Sources/Extensions/Widgets/Common/WidgetCircularView.swift +++ b/Sources/Extensions/Widgets/Common/WidgetCircularView.swift @@ -1,9 +1,9 @@ -import SwiftUI import Shared +import SwiftUI struct WidgetCircularView: View { var icon: MaterialDesignIcons - + private static func scaleLogo(logo: UIImage, size: CGFloat) -> UIImage { let canvas = CGSize(width: size, height: size) let format = logo.imageRendererFormat @@ -11,7 +11,7 @@ struct WidgetCircularView: View { _ in logo.draw(in: CGRect(origin: .zero, size: canvas)) } } - + var body: some View { VStack(spacing: 2) { Image(uiImage: icon.image( diff --git a/Sources/Extensions/Widgets/Details/WidgetDetails.swift b/Sources/Extensions/Widgets/Details/WidgetDetails.swift index 957291fc9..2a3086013 100644 --- a/Sources/Extensions/Widgets/Details/WidgetDetails.swift +++ b/Sources/Extensions/Widgets/Details/WidgetDetails.swift @@ -11,7 +11,7 @@ struct WidgetDetails: Widget { intent: WidgetDetailsAppIntent.self, provider: WidgetDetailsAppIntentTimelineProvider() ) { timelineEntry in - if timelineEntry.runAction && timelineEntry.action != nil { + if timelineEntry.runAction, timelineEntry.action != nil { Button(intent: intent(for: timelineEntry)) { WidgetDetailsView(entry: timelineEntry) .widgetBackground(Color.clear) @@ -27,7 +27,7 @@ struct WidgetDetails: Widget { .description(L10n.Widgets.Details.description) .supportedFamilies([.accessoryInline, .accessoryRectangular]) } - + private func intent(for entry: WidgetDetailsEntry) -> PerformAction { let intent = PerformAction() intent.action = IntentActionAppEntity(id: entry.action!.ID, displayString: entry.action!.Text) diff --git a/Sources/Extensions/Widgets/Details/WidgetDetailsView.swift b/Sources/Extensions/Widgets/Details/WidgetDetailsView.swift index 65a1b2eaa..1d76ccc73 100644 --- a/Sources/Extensions/Widgets/Details/WidgetDetailsView.swift +++ b/Sources/Extensions/Widgets/Details/WidgetDetailsView.swift @@ -4,9 +4,9 @@ import WidgetKit @available(iOS 17.0, *) struct WidgetDetailsView: View { @Environment(\.widgetFamily) var family: WidgetFamily - + var entry: WidgetDetailsEntry - + var body: some View { if family == .accessoryRectangular { VStack(alignment: .leading) { diff --git a/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift index 1ee8670c7..d99895f2d 100644 --- a/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift +++ b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift @@ -11,7 +11,7 @@ struct WidgetGauge: Widget { intent: WidgetGaugeAppIntent.self, provider: WidgetGaugeAppIntentTimelineProvider() ) { timelineEntry in - if timelineEntry.runAction && timelineEntry.action != nil { + if timelineEntry.runAction, timelineEntry.action != nil { Button(intent: intent(for: timelineEntry)) { WidgetGaugeView(entry: timelineEntry) .widgetBackground(Color.clear) @@ -27,7 +27,7 @@ struct WidgetGauge: Widget { .description(L10n.Widgets.Gauge.description) .supportedFamilies([.accessoryCircular]) } - + private func intent(for entry: WidgetGaugeEntry) -> PerformAction { let intent = PerformAction() intent.action = IntentActionAppEntity(id: entry.action!.ID, displayString: entry.action!.Text) diff --git a/Sources/Extensions/Widgets/Gauge/WidgetGaugeView.swift b/Sources/Extensions/Widgets/Gauge/WidgetGaugeView.swift index 4df97f134..fd4047472 100644 --- a/Sources/Extensions/Widgets/Gauge/WidgetGaugeView.swift +++ b/Sources/Extensions/Widgets/Gauge/WidgetGaugeView.swift @@ -4,9 +4,9 @@ import WidgetKit @available(iOS 17.0, *) struct WidgetGaugeView: View { var entry: WidgetGaugeEntry - + var body: some View { - switch (entry.gaugeType) { + switch entry.gaugeType { case .normal: Gauge(value: entry.value) { if entry.valueLabel != nil { diff --git a/Sources/Extensions/Widgets/OpenPage/WidgetOpenPage.swift b/Sources/Extensions/Widgets/OpenPage/WidgetOpenPage.swift index 2da33424e..b8c9b93cb 100644 --- a/Sources/Extensions/Widgets/OpenPage/WidgetOpenPage.swift +++ b/Sources/Extensions/Widgets/OpenPage/WidgetOpenPage.swift @@ -42,7 +42,7 @@ struct WidgetOpenPage: Widget { Current.webhooks.handleBackground(for: identifier, completionHandler: completion) } } - + private var supportedFamilies: [WidgetFamily] { var supportedFamilies: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] diff --git a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift index bb8c76375..3e7aa764c 100644 --- a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift +++ b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift @@ -1,7 +1,7 @@ import Communicator -import WidgetKit import PromiseKit import UserNotifications +import WidgetKit public protocol NotificationCommandHandler { func handle(_ payload: [String: Any]) -> Promise From 017a77c57a3b676157f165c63007a4f922b4272d Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:25:14 -0400 Subject: [PATCH 12/25] Reorder min and max in the gauge widget app intent --- .../Widget/Gauge/WidgetGaugeAppIntent.swift | 12 ++++++------ .../Gauge/WidgetGaugeAppIntentTimelineProvider.swift | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift index 75be48f3d..42ac98f88 100644 --- a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift +++ b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift @@ -41,7 +41,7 @@ struct WidgetGaugeAppIntent: WidgetConfigurationIntent { var valueLabelTemplate: String @Parameter( - title: "Max Label Template", + title: "Min Label Template", default: "", inputOptions: .init( capitalizationType: .none, @@ -51,10 +51,10 @@ struct WidgetGaugeAppIntent: WidgetConfigurationIntent { smartDashes: false ) ) - var maxTemplate: String + var minTemplate: String @Parameter( - title: "Min Label Template", + title: "Max Label Template", default: "", inputOptions: .init( capitalizationType: .none, @@ -64,7 +64,7 @@ struct WidgetGaugeAppIntent: WidgetConfigurationIntent { smartDashes: false ) ) - var minTemplate: String + var maxTemplate: String @Parameter(title: "Run Action", default: false) var runAction: Bool @@ -82,8 +82,8 @@ struct WidgetGaugeAppIntent: WidgetConfigurationIntent { \.$valueTemplate \.$valueLabelTemplate - \.$maxTemplate \.$minTemplate + \.$maxTemplate \.$runAction \.$action @@ -110,8 +110,8 @@ struct WidgetGaugeAppIntent: WidgetConfigurationIntent { \.$valueTemplate \.$valueLabelTemplate - \.$maxTemplate \.$minTemplate + \.$maxTemplate \.$runAction } diff --git a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift index c0c5b273b..69384e91e 100644 --- a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift +++ b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift @@ -45,7 +45,7 @@ struct WidgetGaugeAppIntentTimelineProvider: AppIntentTimelineProvider { .init( gaugeType: .normal, value: 0.5, - valueLabel: "?", max: "?", min: "?", + valueLabel: "?", min: "?", max: "?", runAction: false, action: nil ) } @@ -110,8 +110,8 @@ struct WidgetGaugeAppIntentTimelineProvider: AppIntentTimelineProvider { value: Double(params[0]) ?? 0.0, valueLabel: valueText != "?" ? valueText : nil, - max: maxText != "?" ? maxText : nil, min: minText != "?" ? minText : nil, + max: maxText != "?" ? maxText : nil, runAction: configuration.runAction, action: configuration.action?.asAction() @@ -138,8 +138,8 @@ struct WidgetGaugeEntry: TimelineEntry { var value: Double var valueLabel: String? - var max: String? var min: String? + var max: String? var runAction: Bool var action: Action? From 5be18f63fd315a6f39ff02f6676ce9db2aed04ae Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Mon, 1 Jul 2024 19:29:15 -0400 Subject: [PATCH 13/25] Move widget families into enums --- .../Widgets/Actions/WidgetActions.swift | 22 +++++++++++++++++-- .../Widgets/Details/WidgetDetails.swift | 10 ++++++++- .../Widgets/Gauge/WidgetGauge.swift | 7 +++++- .../Widgets/OpenPage/WidgetOpenPage.swift | 16 +++++++------- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/Sources/Extensions/Widgets/Actions/WidgetActions.swift b/Sources/Extensions/Widgets/Actions/WidgetActions.swift index e10e4451e..ff2b75360 100644 --- a/Sources/Extensions/Widgets/Actions/WidgetActions.swift +++ b/Sources/Extensions/Widgets/Actions/WidgetActions.swift @@ -32,7 +32,7 @@ struct WidgetActions: Widget { .contentMarginsDisabledIfAvailable() .configurationDisplayName(L10n.Widgets.Actions.title) .description(L10n.Widgets.Actions.description) - .supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge, .accessoryCircular]) + .supportedFamilies(WidgetActionSupportedFamilies.families) } } @@ -65,9 +65,27 @@ struct LegacyWidgetActions: Widget { .contentMarginsDisabledIfAvailable() .configurationDisplayName(L10n.Widgets.Actions.title) .description(L10n.Widgets.Actions.description) - .supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge]) + .supportedFamilies(WidgetActionSupportedFamilies.legacyFamilies) .onBackgroundURLSessionEvents(matching: nil) { identifier, completion in Current.webhooks.handleBackground(for: identifier, completionHandler: completion) } } } + +enum WidgetActionSupportedFamilies { + @available(iOS 16.0, *) + static let families: [WidgetFamily] = [ + .systemSmall, + .systemMedium, + .systemLarge, + .systemExtraLarge, + .accessoryCircular, + ] + + static let legacyFamilies: [WidgetFamily] = [ + .systemSmall, + .systemMedium, + .systemLarge, + .systemExtraLarge, + ] +} diff --git a/Sources/Extensions/Widgets/Details/WidgetDetails.swift b/Sources/Extensions/Widgets/Details/WidgetDetails.swift index 2a3086013..592822e7e 100644 --- a/Sources/Extensions/Widgets/Details/WidgetDetails.swift +++ b/Sources/Extensions/Widgets/Details/WidgetDetails.swift @@ -25,7 +25,7 @@ struct WidgetDetails: Widget { .contentMarginsDisabledIfAvailable() .configurationDisplayName(L10n.Widgets.Details.title) .description(L10n.Widgets.Details.description) - .supportedFamilies([.accessoryInline, .accessoryRectangular]) + .supportedFamilies(WidgetDetailsSupportedFamilies.families) } private func intent(for entry: WidgetDetailsEntry) -> PerformAction { @@ -34,3 +34,11 @@ struct WidgetDetails: Widget { return intent } } + +@available(iOS 17, *) +enum WidgetDetailsSupportedFamilies { + static let families: [WidgetFamily] = [ + .accessoryInline, + .accessoryRectangular, + ] +} diff --git a/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift index d99895f2d..2fe0ea55e 100644 --- a/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift +++ b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift @@ -25,7 +25,7 @@ struct WidgetGauge: Widget { .contentMarginsDisabledIfAvailable() .configurationDisplayName(L10n.Widgets.Gauge.title) .description(L10n.Widgets.Gauge.description) - .supportedFamilies([.accessoryCircular]) + .supportedFamilies(WidgetGaugeSupportedFamilies.families) } private func intent(for entry: WidgetGaugeEntry) -> PerformAction { @@ -34,3 +34,8 @@ struct WidgetGauge: Widget { return intent } } + +@available(iOS 17, *) +enum WidgetGaugeSupportedFamilies { + static let families: [WidgetFamily] = [.accessoryCircular] +} diff --git a/Sources/Extensions/Widgets/OpenPage/WidgetOpenPage.swift b/Sources/Extensions/Widgets/OpenPage/WidgetOpenPage.swift index b8c9b93cb..2a0421b95 100644 --- a/Sources/Extensions/Widgets/OpenPage/WidgetOpenPage.swift +++ b/Sources/Extensions/Widgets/OpenPage/WidgetOpenPage.swift @@ -37,19 +37,19 @@ struct WidgetOpenPage: Widget { .contentMarginsDisabledIfAvailable() .configurationDisplayName(L10n.Widgets.OpenPage.title) .description(L10n.Widgets.OpenPage.description) - .supportedFamilies(supportedFamilies) + .supportedFamilies(WidgetOpenPageSupportedFamilies.families) .onBackgroundURLSessionEvents(matching: nil) { identifier, completion in Current.webhooks.handleBackground(for: identifier, completionHandler: completion) } } +} - private var supportedFamilies: [WidgetFamily] { - var supportedFamilies: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - if #available(iOSApplicationExtension 16.0, *) { - supportedFamilies.append(.accessoryCircular) +enum WidgetOpenPageSupportedFamilies { + static var families: [WidgetFamily] { + if #available(iOS 16.0, *) { + [.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge, .accessoryCircular] + } else { + [.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge] } - - return supportedFamilies } } From e59dcaf41c5159b1cd0ee0e3f6f2e33c0b2f0487 Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Mon, 1 Jul 2024 19:55:34 -0400 Subject: [PATCH 14/25] Add comments for transparent widget families variable --- .../Common/WidgetBasicContainerView.swift | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Sources/Extensions/Widgets/Common/WidgetBasicContainerView.swift b/Sources/Extensions/Widgets/Common/WidgetBasicContainerView.swift index 5e5cfcf8f..00fedc244 100644 --- a/Sources/Extensions/Widgets/Common/WidgetBasicContainerView.swift +++ b/Sources/Extensions/Widgets/Common/WidgetBasicContainerView.swift @@ -28,7 +28,8 @@ struct WidgetBasicContainerView: View { func singleView(for model: WidgetBasicViewModel) -> some View { ZStack { - if !Self.clearFamilies.contains(family) { + // Check if the widget should be transparent (on the lock screen) + if !Self.transparentFamilies.contains(family) { model.backgroundColor .opacity(0.8) } @@ -87,8 +88,9 @@ struct WidgetBasicContainerView: View { HStack(spacing: pixelLength) { ForEach(column) { model in ZStack { - // stacking the color under makes the Link's highlight state nicer - if !Self.clearFamilies.contains(family) { + // Check if the widget should be transparent (on the lock screen) + if !Self.transparentFamilies.contains(family) { + // stacking the color under makes the Link's highlight state nicer model.backgroundColor .opacity(0.8) } @@ -175,13 +177,13 @@ struct WidgetBasicContainerView: View { } } - private static var clearFamilies: [WidgetFamily] { - var supportedFamilies: [WidgetFamily] = [] - - if #available(iOSApplicationExtension 16.0, *) { - supportedFamilies = [.accessoryCircular, .accessoryInline, .accessoryRectangular] + // This is all widgets that are on the lock screen + // Lock screen widgets are transparent and don't need a colored background + private static var transparentFamilies: [WidgetFamily] { + if #available(iOS 16.0, *) { + [.accessoryCircular, .accessoryRectangular] + } else { + [] } - - return supportedFamilies } } From def26b43b870fef3ca8f99e352cfb6a9b7ea022b Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Mon, 1 Jul 2024 20:27:38 -0400 Subject: [PATCH 15/25] Add a legacy notification command for reloading widgets and move types to an enum --- .../Sources/NotificationParserLegacy.swift | 21 ++++++++++++++----- .../NotificationsCommandManager.swift | 6 +++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Sources/PushServer/SharedPush/Sources/NotificationParserLegacy.swift b/Sources/PushServer/SharedPush/Sources/NotificationParserLegacy.swift index f777ea2ad..ef14660a4 100644 --- a/Sources/PushServer/SharedPush/Sources/NotificationParserLegacy.swift +++ b/Sources/PushServer/SharedPush/Sources/NotificationParserLegacy.swift @@ -59,12 +59,12 @@ public struct LegacyNotificationParserImpl: LegacyNotificationParser { } let commandPayload: CommandPayload? = { - switch input["message"] as? String { - case "request_location_update", "request_location_updates": + switch LegacyNotificationCommandType(rawValue: input["message"] as? String ?? "") { + case .locationUpdate, .locationUpdates: return .init("request_location_update") - case "clear_badge": + case .clearBadge: return .init(isAlert: true, payload: ["aps": ["badge": 0]]) - case "clear_notification": + case .clearNotification: var homeassistant = [String: Any]() if let tag = data["tag"] { @@ -76,8 +76,10 @@ public struct LegacyNotificationParserImpl: LegacyNotificationParser { } return .init("clear_notification", homeassistant: homeassistant) - case "update_complications": + case .updateComplications: return .init("update_complications") + case .updateWidgets: + return .init("update_widgets") default: return nil } }() @@ -246,6 +248,15 @@ public struct LegacyNotificationParserImpl: LegacyNotificationParser { } } +enum LegacyNotificationCommandType: String { + case locationUpdate = "request_location_update" + case locationUpdates = "request_location_updates" + case clearBadge = "clear_badge" + case clearNotification = "clear_notification" + case updateComplications = "update_complications" + case updateWidgets = "update_wigets" +} + private extension Dictionary where Value == Any { mutating func mutate( _ key: Key, diff --git a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift index 3e7aa764c..86c46d860 100644 --- a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift +++ b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift @@ -20,7 +20,7 @@ public class NotificationCommandManager { public init() { register(command: "request_location_update", handler: HandlerLocationUpdate()) register(command: "clear_notification", handler: HandlerClearNotification()) - if #available(iOS 14, watchOS 9, macOS 11, *) { + if #available(watchOS 9, *) { register(command: "reload_widgets", handler: HandlerReloadWidgets()) } @@ -98,10 +98,10 @@ private struct HandlerClearNotification: NotificationCommandHandler { } } -@available(iOS 14, watchOS 9, macOS 11, *) +@available(watchOS 9, *) private struct HandlerReloadWidgets: NotificationCommandHandler { func handle(_ payload: [String: Any]) -> Promise { - Current.Log.verbose("reloading widgets") + Current.Log.verbose("Reloading widgets triggered by notification command") return Promise { seal in DispatchQueue.main.async { WidgetCenter.shared.reloadTimelines(ofKind: "io.robbie.HomeAssistant.widget-gauge") From 19e12d13d9467da635950ea273c9d5c3bf02e573 Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Mon, 1 Jul 2024 20:34:12 -0400 Subject: [PATCH 16/25] Fix the gauge and details widget kind --- Sources/Extensions/Widgets/Details/WidgetDetails.swift | 2 +- Sources/Extensions/Widgets/Gauge/WidgetGauge.swift | 2 +- .../NotificationCommands/NotificationsCommandManager.swift | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Extensions/Widgets/Details/WidgetDetails.swift b/Sources/Extensions/Widgets/Details/WidgetDetails.swift index 592822e7e..ad745bbe2 100644 --- a/Sources/Extensions/Widgets/Details/WidgetDetails.swift +++ b/Sources/Extensions/Widgets/Details/WidgetDetails.swift @@ -7,7 +7,7 @@ import WidgetKit struct WidgetDetails: Widget { var body: some WidgetConfiguration { AppIntentConfiguration( - kind: "io.robbie.HomeAssistant.widget-details", + kind: "WidgetDetails", intent: WidgetDetailsAppIntent.self, provider: WidgetDetailsAppIntentTimelineProvider() ) { timelineEntry in diff --git a/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift index 2fe0ea55e..77f0bddbc 100644 --- a/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift +++ b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift @@ -7,7 +7,7 @@ import WidgetKit struct WidgetGauge: Widget { var body: some WidgetConfiguration { AppIntentConfiguration( - kind: "io.robbie.HomeAssistant.widget-gauge", + kind: "WidgetGauge", intent: WidgetGaugeAppIntent.self, provider: WidgetGaugeAppIntentTimelineProvider() ) { timelineEntry in diff --git a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift index 86c46d860..e3de4af44 100644 --- a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift +++ b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift @@ -104,8 +104,8 @@ private struct HandlerReloadWidgets: NotificationCommandHandler { Current.Log.verbose("Reloading widgets triggered by notification command") return Promise { seal in DispatchQueue.main.async { - WidgetCenter.shared.reloadTimelines(ofKind: "io.robbie.HomeAssistant.widget-gauge") - WidgetCenter.shared.reloadTimelines(ofKind: "io.robbie.HomeAssistant.widget-details") + WidgetCenter.shared.reloadTimelines(ofKind: "WidgetGauge") + WidgetCenter.shared.reloadTimelines(ofKind: "WidgetDetails") seal.fulfill(()) } } From abd90c0695819f145d549f63d97694e61d3b46e2 Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Mon, 1 Jul 2024 20:39:26 -0400 Subject: [PATCH 17/25] Lower the widget reload time interval --- .../Details/WidgetDetailsAppIntentTimelineProvider.swift | 9 ++------- .../Gauge/WidgetGaugeAppIntentTimelineProvider.swift | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntentTimelineProvider.swift b/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntentTimelineProvider.swift index 43d8ce055..c4305bb37 100644 --- a/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntentTimelineProvider.swift +++ b/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntentTimelineProvider.swift @@ -21,7 +21,6 @@ struct WidgetDetailsAppIntentTimelineProvider: AppIntentTimelineProvider { func timeline(for configuration: WidgetDetailsAppIntent, in context: Context) async -> Timeline { do { let snapshot = try await entry(for: configuration, in: context) - Current.Log.debug("Reloading gauge widget") return .init( entries: [snapshot], policy: .after( @@ -35,7 +34,7 @@ struct WidgetDetailsAppIntentTimelineProvider: AppIntentTimelineProvider { entries: [placeholder(in: context)], policy: .after( Current.date() - .addingTimeInterval(WidgetDetailsDataSource.fastExpiration.converted(to: .seconds).value) + .addingTimeInterval(WidgetDetailsDataSource.expiration.converted(to: .seconds).value) ) ) } @@ -112,11 +111,7 @@ struct WidgetDetailsAppIntentTimelineProvider: AppIntentTimelineProvider { enum WidgetDetailsDataSource { static var expiration: Measurement { - .init(value: 2, unit: .hours) - } - - static var fastExpiration: Measurement { - .init(value: 1, unit: .hours) + .init(value: 15, unit: .minutes) } } diff --git a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift index 69384e91e..4960a57fe 100644 --- a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift +++ b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntentTimelineProvider.swift @@ -21,7 +21,6 @@ struct WidgetGaugeAppIntentTimelineProvider: AppIntentTimelineProvider { func timeline(for configuration: WidgetGaugeAppIntent, in context: Context) async -> Timeline { do { let snapshot = try await entry(for: configuration, in: context) - Current.Log.debug("Reloading gauge widget") return .init( entries: [snapshot], policy: .after( @@ -35,7 +34,7 @@ struct WidgetGaugeAppIntentTimelineProvider: AppIntentTimelineProvider { entries: [placeholder(in: context)], policy: .after( Current.date() - .addingTimeInterval(WidgetGaugeDataSource.fastExpiration.converted(to: .seconds).value) + .addingTimeInterval(WidgetGaugeDataSource.expiration.converted(to: .seconds).value) ) ) } @@ -121,11 +120,7 @@ struct WidgetGaugeAppIntentTimelineProvider: AppIntentTimelineProvider { enum WidgetGaugeDataSource { static var expiration: Measurement { - .init(value: 2, unit: .hours) - } - - static var fastExpiration: Measurement { - .init(value: 1, unit: .hours) + .init(value: 15, unit: .minutes) } } From fbeac49146cc3a1d759dda570352ab421959e26f Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Mon, 1 Jul 2024 22:38:44 -0400 Subject: [PATCH 18/25] Add strings for gauge and details widget parameters --- .../Resources/en.lproj/Localizable.strings | 15 ++++++++++ .../Shared/Resources/Swiftgen/Strings.swift | 30 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index 2cdad1ac1..fe068eec3 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -854,8 +854,23 @@ Home Assistant is free and open source home automation software with a focus on "widgets.open_page.description" = "Open a frontend page in Home Assistant."; "widgets.open_page.not_configured" = "No Pages Available"; "widgets.open_page.title" = "Open Page"; +"widgets.gauge.gauge_type" = "Gauge Type"; +"widgets.gauge.server" = "Server"; +"widgets.gauge.value_template" = "Value Template (0-1)"; +"widgets.gauge.value_label_template" = "Value Label Template"; +"widgets.gauge.min_label_template" = "Min Label Template"; +"widgets.gauge.max_label_template" = "Max Label Template"; +"widgets.gauge.run_action" = "Run Action"; +"widgets.gauge.action" = "Action"; "widgets.gauge.description" = "Display numeric states from Home Assistant in a gauge"; "widgets.gauge.title" = "Gauge"; +"widgets.details.server" = "Server"; +"widgets.details.upper_template" = "Upper Text Template"; +"widgets.details.lower_template" = "Lower Text Template"; +"widgets.details.details_template" = "Details Text Template (only in rectangular family)"; +"widgets.details.max_label_template" = "Max Label Template"; +"widgets.details.run_action" = "Run Action (only in rectangular family)"; +"widgets.details.action" = "Action"; "widgets.details.description" = "Display states using from Home Assistant in text"; "widgets.details.title" = "Details"; "yes_label" = "Yes"; diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index 185a96e47..39b2e1df9 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -2867,16 +2867,46 @@ public enum L10n { public static var reloadTimeline: String { return L10n.tr("Localizable", "widgets.button.reload_timeline") } } public enum Details { + /// Action + public static var action: String { return L10n.tr("Localizable", "widgets.details.action") } /// Display states using from Home Assistant in text public static var description: String { return L10n.tr("Localizable", "widgets.details.description") } + /// Details Text Template (only in rectangular family) + public static var detailsTemplate: String { return L10n.tr("Localizable", "widgets.details.details_template") } + /// Lower Text Template + public static var lowerTemplate: String { return L10n.tr("Localizable", "widgets.details.lower_template") } + /// Max Label Template + public static var maxLabelTemplate: String { return L10n.tr("Localizable", "widgets.details.max_label_template") } + /// Run Action (only in rectangular family) + public static var runAction: String { return L10n.tr("Localizable", "widgets.details.run_action") } + /// Server + public static var server: String { return L10n.tr("Localizable", "widgets.details.server") } /// Details public static var title: String { return L10n.tr("Localizable", "widgets.details.title") } + /// Upper Text Template + public static var upperTemplate: String { return L10n.tr("Localizable", "widgets.details.upper_template") } } public enum Gauge { + /// Action + public static var action: String { return L10n.tr("Localizable", "widgets.gauge.action") } /// Display numeric states from Home Assistant in a gauge public static var description: String { return L10n.tr("Localizable", "widgets.gauge.description") } + /// Gauge Type + public static var gaugeType: String { return L10n.tr("Localizable", "widgets.gauge.gauge_type") } + /// Max Label Template + public static var maxLabelTemplate: String { return L10n.tr("Localizable", "widgets.gauge.max_label_template") } + /// Min Label Template + public static var minLabelTemplate: String { return L10n.tr("Localizable", "widgets.gauge.min_label_template") } + /// Run Action + public static var runAction: String { return L10n.tr("Localizable", "widgets.gauge.run_action") } + /// Server + public static var server: String { return L10n.tr("Localizable", "widgets.gauge.server") } /// Gauge public static var title: String { return L10n.tr("Localizable", "widgets.gauge.title") } + /// Value Label Template + public static var valueLabelTemplate: String { return L10n.tr("Localizable", "widgets.gauge.value_label_template") } + /// Value Template (0-1) + public static var valueTemplate: String { return L10n.tr("Localizable", "widgets.gauge.value_template") } } public enum OpenPage { /// Open a frontend page in Home Assistant. From b1b5d8cd96f43f4b747aa7d6a5b6646b8a4bf109 Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:02:48 -0400 Subject: [PATCH 19/25] Move widget kinds to a shared enum --- HomeAssistant.xcodeproj/project.pbxproj | 6 ++++++ Sources/Extensions/Widgets/Details/WidgetDetails.swift | 2 +- Sources/Extensions/Widgets/Gauge/WidgetGauge.swift | 2 +- Sources/Shared/Common/AppIntentWidgetKinds.swift | 4 ++++ .../NotificationCommands/NotificationsCommandManager.swift | 4 ++-- 5 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 Sources/Shared/Common/AppIntentWidgetKinds.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index 56837be37..888848dab 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -511,6 +511,8 @@ 403AE9272C2F333A00D48147 /* WidgetGaugeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403AE9262C2F333A00D48147 /* WidgetGaugeView.swift */; }; 403AE92A2C2F3A9200D48147 /* IntentServerAppEntitiy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403AE9292C2F3A9200D48147 /* IntentServerAppEntitiy.swift */; }; 403AE92B2C2F3A9200D48147 /* IntentServerAppEntitiy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403AE9292C2F3A9200D48147 /* IntentServerAppEntitiy.swift */; }; + 404C79762C3482860010EB81 /* AppIntentWidgetKinds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404C79752C3482790010EB81 /* AppIntentWidgetKinds.swift */; }; + 404C79772C3482860010EB81 /* AppIntentWidgetKinds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404C79752C3482790010EB81 /* AppIntentWidgetKinds.swift */; }; 4080D5BE2C319AA000099C88 /* WidgetDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4080D5BD2C319AA000099C88 /* WidgetDetailsView.swift */; }; 4080D5BF2C319AA000099C88 /* WidgetDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4080D5BC2C319AA000099C88 /* WidgetDetails.swift */; }; 4080D5C42C319B0A00099C88 /* WidgetDetailsAppIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4080D5C12C319B0A00099C88 /* WidgetDetailsAppIntent.swift */; }; @@ -1655,6 +1657,7 @@ 403AE9112C2E2BFC00D48147 /* WidgetGaugeAppIntentTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetGaugeAppIntentTimelineProvider.swift; sourceTree = ""; }; 403AE9262C2F333A00D48147 /* WidgetGaugeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetGaugeView.swift; sourceTree = ""; }; 403AE9292C2F3A9200D48147 /* IntentServerAppEntitiy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentServerAppEntitiy.swift; sourceTree = ""; }; + 404C79752C3482790010EB81 /* AppIntentWidgetKinds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentWidgetKinds.swift; sourceTree = ""; }; 4080D5BC2C319AA000099C88 /* WidgetDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDetails.swift; sourceTree = ""; }; 4080D5BD2C319AA000099C88 /* WidgetDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDetailsView.swift; sourceTree = ""; }; 4080D5C12C319B0A00099C88 /* WidgetDetailsAppIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WidgetDetailsAppIntent.swift; path = Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift; sourceTree = SOURCE_ROOT; }; @@ -4444,6 +4447,7 @@ D0FF79D020D87CF60034574D /* Common */ = { isa = PBXGroup; children = ( + 404C79752C3482790010EB81 /* AppIntentWidgetKinds.swift */, B6A5D9F4215233EC0013963F /* SiriIntents+ConvenienceInits.swift */, B688AB4621193946002FCAD6 /* ObjectMapperTransformers.swift */, 11E5CF8024BBCE1B009AC30F /* ProcessInfo+BackgroundTask.swift */, @@ -6410,6 +6414,7 @@ B67CE8B222200F220034C1D0 /* CMMotion+StringExtensions.swift in Sources */, B6B74CB92283983300D58A68 /* WatchComplication.swift in Sources */, 42B94BEC2B96083C00DEE060 /* AssistModel.swift in Sources */, + 404C79762C3482860010EB81 /* AppIntentWidgetKinds.swift in Sources */, 119DE934263325C20099F7D8 /* IconDrawable+Settings.swift in Sources */, 114CBAE92839E49E00A9BAFF /* CustomServerTrustManager.swift in Sources */, 1128FF3D297F49D900BAAFD9 /* Locale+IntentLanguage.swift in Sources */, @@ -6700,6 +6705,7 @@ B6A258482232539900ADD202 /* WebhookUpdateLocation.swift in Sources */, B6B74CBA2283983800D58A68 /* CLKComplication+Strings.swift in Sources */, 119385A4249E8E360097F497 /* StorageSensor.swift in Sources */, + 404C79772C3482860010EB81 /* AppIntentWidgetKinds.swift in Sources */, D05A4D32216DD206009FD1EB /* MJPEGStreamer.swift in Sources */, 42CE8FB02B46C3D900C707F9 /* CoreStrings+Values.swift in Sources */, 11C4628B24B1230E00031902 /* WebhookResponseServiceCall.swift in Sources */, diff --git a/Sources/Extensions/Widgets/Details/WidgetDetails.swift b/Sources/Extensions/Widgets/Details/WidgetDetails.swift index ad745bbe2..80253b0a1 100644 --- a/Sources/Extensions/Widgets/Details/WidgetDetails.swift +++ b/Sources/Extensions/Widgets/Details/WidgetDetails.swift @@ -7,7 +7,7 @@ import WidgetKit struct WidgetDetails: Widget { var body: some WidgetConfiguration { AppIntentConfiguration( - kind: "WidgetDetails", + kind: AppIntentWidgetKinds.details, intent: WidgetDetailsAppIntent.self, provider: WidgetDetailsAppIntentTimelineProvider() ) { timelineEntry in diff --git a/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift index 77f0bddbc..dbee4c009 100644 --- a/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift +++ b/Sources/Extensions/Widgets/Gauge/WidgetGauge.swift @@ -7,7 +7,7 @@ import WidgetKit struct WidgetGauge: Widget { var body: some WidgetConfiguration { AppIntentConfiguration( - kind: "WidgetGauge", + kind: AppIntentWidgetKinds.gauge, intent: WidgetGaugeAppIntent.self, provider: WidgetGaugeAppIntentTimelineProvider() ) { timelineEntry in diff --git a/Sources/Shared/Common/AppIntentWidgetKinds.swift b/Sources/Shared/Common/AppIntentWidgetKinds.swift new file mode 100644 index 000000000..8006498bb --- /dev/null +++ b/Sources/Shared/Common/AppIntentWidgetKinds.swift @@ -0,0 +1,4 @@ +public enum AppIntentWidgetKinds { + public static let gauge = "WidgetGauge" + public static let details = "WidgetDetails" +} diff --git a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift index e3de4af44..71ce8c757 100644 --- a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift +++ b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift @@ -104,8 +104,8 @@ private struct HandlerReloadWidgets: NotificationCommandHandler { Current.Log.verbose("Reloading widgets triggered by notification command") return Promise { seal in DispatchQueue.main.async { - WidgetCenter.shared.reloadTimelines(ofKind: "WidgetGauge") - WidgetCenter.shared.reloadTimelines(ofKind: "WidgetDetails") + WidgetCenter.shared.reloadTimelines(ofKind: AppIntentWidgetKinds.gauge) + WidgetCenter.shared.reloadTimelines(ofKind: AppIntentWidgetKinds.details) seal.fulfill(()) } } From d32bd63d334d251523a10dbffeb011a144468d7f Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:50:52 -0400 Subject: [PATCH 20/25] Localize the parameters in the widget app intents --- HomeAssistant.xcodeproj/project.pbxproj | 18 ++--- .../Resources/en.lproj/Localizable.strings | 30 ++++----- .../Details/WidgetDetailsAppIntent.swift | 16 ++--- .../Widget/Gauge/WidgetGaugeAppIntent.swift | 20 +++--- .../AppIntents/WidgetActionsAppIntent.swift | 6 +- .../Shared/Resources/Swiftgen/Strings.swift | 66 ++++++++++--------- 6 files changed, 78 insertions(+), 78 deletions(-) diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index 5b582590b..394ea3300 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -513,6 +513,8 @@ 403AE92B2C2F3A9200D48147 /* IntentServerAppEntitiy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 403AE9292C2F3A9200D48147 /* IntentServerAppEntitiy.swift */; }; 404C79762C3482860010EB81 /* AppIntentWidgetKinds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404C79752C3482790010EB81 /* AppIntentWidgetKinds.swift */; }; 404C79772C3482860010EB81 /* AppIntentWidgetKinds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404C79752C3482790010EB81 /* AppIntentWidgetKinds.swift */; }; + 404C797F2C3491390010EB81 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B69933931E232AEA0054453D /* Localizable.strings */; }; + 404C79802C3491390010EB81 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B69933931E232AEA0054453D /* Localizable.strings */; }; 4080D5BE2C319AA000099C88 /* WidgetDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4080D5BD2C319AA000099C88 /* WidgetDetailsView.swift */; }; 4080D5BF2C319AA000099C88 /* WidgetDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4080D5BC2C319AA000099C88 /* WidgetDetails.swift */; }; 4080D5C42C319B0A00099C88 /* WidgetDetailsAppIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4080D5C12C319B0A00099C88 /* WidgetDetailsAppIntent.swift */; }; @@ -604,11 +606,9 @@ 42A818E32BBEA9780083D045 /* MockAudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42A818E22BBEA9780083D045 /* MockAudioRecorder.swift */; }; 42A818E52BBEAA3A0083D045 /* MockAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42A818E42BBEAA3A0083D045 /* MockAudioPlayer.swift */; }; 42A818E72BBEAAE80083D045 /* MockAssistService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42A818E62BBEAAE80083D045 /* MockAssistService.swift */; }; + 42AA4C842C2DACAD00EA2E99 /* UIImage+Circle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42AA4C832C2DACAD00EA2E99 /* UIImage+Circle.swift */; }; 42B1A7432C11E65100904548 /* WatchAssistService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B1A7422C11E65100904548 /* WatchAssistService.swift */; }; 42B1A7452C1305C300904548 /* WatchCommunicatorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B1A7442C1305C300904548 /* WatchCommunicatorService.swift */; }; - 42AA4C822C2DA56100EA2E99 /* SharedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 420B10032B1CF6D800D383D8 /* SharedAssets.xcassets */; }; - 42AA4C842C2DACAD00EA2E99 /* UIImage+Circle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42AA4C832C2DACAD00EA2E99 /* UIImage+Circle.swift */; }; - 42B94BDD2B9606CD00DEE060 /* AssistChatItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B94BDA2B9606CD00DEE060 /* AssistChatItem.swift */; }; 42B94BDE2B9606CD00DEE060 /* AssistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B94BDB2B9606CD00DEE060 /* AssistViewModel.swift */; }; 42B94BDF2B9606CD00DEE060 /* AssistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B94BDC2B9606CD00DEE060 /* AssistView.swift */; }; 42B94BEC2B96083C00DEE060 /* AssistModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B94BE72B9607D100DEE060 /* AssistModel.swift */; }; @@ -1812,9 +1812,9 @@ 42A818E22BBEA9780083D045 /* MockAudioRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAudioRecorder.swift; sourceTree = ""; }; 42A818E42BBEAA3A0083D045 /* MockAudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAudioPlayer.swift; sourceTree = ""; }; 42A818E62BBEAAE80083D045 /* MockAssistService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAssistService.swift; sourceTree = ""; }; + 42AA4C832C2DACAD00EA2E99 /* UIImage+Circle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Circle.swift"; sourceTree = ""; }; 42B1A7422C11E65100904548 /* WatchAssistService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchAssistService.swift; sourceTree = ""; }; 42B1A7442C1305C300904548 /* WatchCommunicatorService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchCommunicatorService.swift; sourceTree = ""; }; - 42AA4C832C2DACAD00EA2E99 /* UIImage+Circle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Circle.swift"; sourceTree = ""; }; 42B94BDA2B9606CD00DEE060 /* AssistChatItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssistChatItem.swift; sourceTree = ""; }; 42B94BDB2B9606CD00DEE060 /* AssistViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssistViewModel.swift; sourceTree = ""; }; 42B94BDC2B9606CD00DEE060 /* AssistView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssistView.swift; sourceTree = ""; }; @@ -3438,14 +3438,6 @@ path = Domains; sourceTree = ""; }; - 425573D82B57DDC700145217 /* Tests */ = { - isa = PBXGroup; - children = ( - 425573D92B57DDE000145217 /* WindowScenesManager.test.swift */, - ); - path = Tests; - sourceTree = ""; - }; 426266432C11B0070081A818 /* Watch */ = { isa = PBXGroup; children = ( @@ -5331,6 +5323,7 @@ B606168B1D1F117700249C11 /* US-EN-Alexa-Motion-Detected-Generic.wav in Resources */, 4279407F2B8369EC001D7E14 /* AppIntentVocabulary.plist in Resources */, B606168E1D1F117700249C11 /* US-EN-Alexa-Motion-In-Front-Yard.wav in Resources */, + 404C797F2C3491390010EB81 /* Localizable.strings in Resources */, B60616261D1F117700249C11 /* US-EN-Morgan-Freeman-Motion-In-Garage.wav in Resources */, 420B100C2B1D204400D383D8 /* Assets.xcassets in Resources */, B60616941D1F117800249C11 /* US-EN-Alexa-Smoke-Detected-In-Garage.wav in Resources */, @@ -5531,6 +5524,7 @@ buildActionMask = 2147483647; files = ( B6CC5D882159D10E00833E5D /* Assets.xcassets in Resources */, + 404C79802C3491390010EB81 /* Localizable.strings in Resources */, B6CC5D862159D10D00833E5D /* Interface.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index 108a080a9..bae1eecd3 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -844,6 +844,7 @@ Home Assistant is free and open source home automation software with a focus on "watch.labels.complication_text_areas.trailing.label" = "Trailing"; "watch.labels.no_action" = "No actions configured. Configure actions on your phone to dismiss this message."; "watch.placeholder_complication_name" = "Placeholder"; +"widgets.actions.parameters.action" = "Action"; "widgets.actions.description" = "Perform Home Assistant actions."; "widgets.actions.not_configured" = "No Actions Configured"; "widgets.actions.title" = "Actions"; @@ -855,23 +856,22 @@ Home Assistant is free and open source home automation software with a focus on "widgets.open_page.description" = "Open a frontend page in Home Assistant."; "widgets.open_page.not_configured" = "No Pages Available"; "widgets.open_page.title" = "Open Page"; -"widgets.gauge.gauge_type" = "Gauge Type"; -"widgets.gauge.server" = "Server"; -"widgets.gauge.value_template" = "Value Template (0-1)"; -"widgets.gauge.value_label_template" = "Value Label Template"; -"widgets.gauge.min_label_template" = "Min Label Template"; -"widgets.gauge.max_label_template" = "Max Label Template"; -"widgets.gauge.run_action" = "Run Action"; -"widgets.gauge.action" = "Action"; +"widgets.gauge.parameters.gauge_type" = "Gauge Type"; +"widgets.gauge.parameters.server" = "Server"; +"widgets.gauge.parameters.value_template" = "Value Template (0-1)"; +"widgets.gauge.parameters.value_label_template" = "Value Label Template"; +"widgets.gauge.parameters.min_label_template" = "Min Label Template"; +"widgets.gauge.parameters.max_label_template" = "Max Label Template"; +"widgets.gauge.parameters.run_action" = "Run Action"; +"widgets.gauge.parameters.action" = "Action"; "widgets.gauge.description" = "Display numeric states from Home Assistant in a gauge"; "widgets.gauge.title" = "Gauge"; -"widgets.details.server" = "Server"; -"widgets.details.upper_template" = "Upper Text Template"; -"widgets.details.lower_template" = "Lower Text Template"; -"widgets.details.details_template" = "Details Text Template (only in rectangular family)"; -"widgets.details.max_label_template" = "Max Label Template"; -"widgets.details.run_action" = "Run Action (only in rectangular family)"; -"widgets.details.action" = "Action"; +"widgets.details.parameters.server" = "Server"; +"widgets.details.parameters.upper_template" = "Upper Text Template"; +"widgets.details.parameters.lower_template" = "Lower Text Template"; +"widgets.details.parameters.details_template" = "Details Text Template (only in rectangular family)"; +"widgets.details.parameters.run_action" = "Run Action (only in rectangular family)"; +"widgets.details.parameters.action" = "Action"; "widgets.details.description" = "Display states using from Home Assistant in text"; "widgets.details.title" = "Details"; "yes_label" = "Yes"; diff --git a/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift b/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift index f5a6f30cf..2613db8c7 100644 --- a/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift +++ b/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift @@ -5,14 +5,14 @@ import Shared @available(iOS 17.0, macOS 14.0, watchOS 10.0, *) struct WidgetDetailsAppIntent: WidgetConfigurationIntent { - static let title: LocalizedStringResource = "Details" - static let description = IntentDescription("Display states using from Home Assistant in text") + static let title: LocalizedStringResource = "widgets.details.title" + static let description = IntentDescription("widgets.details.description") - @Parameter(title: "Server", default: nil) + @Parameter(title: "widgets.details.parameters.server", default: nil) var server: IntentServerAppEntity @Parameter( - title: "Upper Text Template", + title: "widgets.details.parameters.upper_template", default: "", inputOptions: .init( capitalizationType: .none, @@ -25,7 +25,7 @@ struct WidgetDetailsAppIntent: WidgetConfigurationIntent { var upperTemplate: String @Parameter( - title: "Lower Text Template", + title: "widgets.details.parameters.lower_template", default: "", inputOptions: .init( capitalizationType: .none, @@ -38,7 +38,7 @@ struct WidgetDetailsAppIntent: WidgetConfigurationIntent { var lowerTemplate: String @Parameter( - title: "Details Text Template (only in rectangular family)", + title: "widgets.details.parameters.details_template", default: "", inputOptions: .init( capitalizationType: .none, @@ -50,10 +50,10 @@ struct WidgetDetailsAppIntent: WidgetConfigurationIntent { ) var detailsTemplate: String - @Parameter(title: "Run Action (only in rectangular family)", default: false) + @Parameter(title: "widgets.details.parameters.run_action", default: false) var runAction: Bool - @Parameter(title: "Action", default: nil) + @Parameter(title: "widgets.details.parameters.action", default: nil) var action: IntentActionAppEntity? static var parameterSummary: some ParameterSummary { diff --git a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift index 42ac98f88..1e45eb863 100644 --- a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift +++ b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift @@ -5,17 +5,17 @@ import Shared @available(iOS 17.0, macOS 14.0, watchOS 10.0, *) struct WidgetGaugeAppIntent: WidgetConfigurationIntent { - static let title: LocalizedStringResource = "Gauge" - static let description = IntentDescription("Display numeric states from Home Assistant in a gauge") + static let title: LocalizedStringResource = "widgets.gauge.title" + static let description = IntentDescription("widgets.gauge.description") - @Parameter(title: "Gauge Type", default: .normal) + @Parameter(title: "widgets.gauge.parameters.gauge_type", default: .normal) var gaugeType: GaugeTypeAppEnum - @Parameter(title: "Server", default: nil) + @Parameter(title: "widgets.gauge.parameters.server", default: nil) var server: IntentServerAppEntity @Parameter( - title: "Value Template (0-1)", + title: "widgets.gauge.parameters.value_template", default: "", inputOptions: .init( capitalizationType: .none, @@ -28,7 +28,7 @@ struct WidgetGaugeAppIntent: WidgetConfigurationIntent { var valueTemplate: String @Parameter( - title: "Value Label Template", + title: "widgets.gauge.parameters.value_label_template", default: "", inputOptions: .init( capitalizationType: .none, @@ -41,7 +41,7 @@ struct WidgetGaugeAppIntent: WidgetConfigurationIntent { var valueLabelTemplate: String @Parameter( - title: "Min Label Template", + title: "widgets.gauge.parameters.min_label_template", default: "", inputOptions: .init( capitalizationType: .none, @@ -54,7 +54,7 @@ struct WidgetGaugeAppIntent: WidgetConfigurationIntent { var minTemplate: String @Parameter( - title: "Max Label Template", + title: "widgets.gauge.parameters.max_label_template", default: "", inputOptions: .init( capitalizationType: .none, @@ -66,10 +66,10 @@ struct WidgetGaugeAppIntent: WidgetConfigurationIntent { ) var maxTemplate: String - @Parameter(title: "Run Action", default: false) + @Parameter(title: "widgets.gauge.parameters.run_action", default: false) var runAction: Bool - @Parameter(title: "Action", default: nil) + @Parameter(title: "widgets.gauge.parameters.action", default: nil) var action: IntentActionAppEntity? static var parameterSummary: some ParameterSummary { diff --git a/Sources/Extensions/AppIntents/WidgetActionsAppIntent.swift b/Sources/Extensions/AppIntents/WidgetActionsAppIntent.swift index e51db4ae5..c590443a1 100644 --- a/Sources/Extensions/AppIntents/WidgetActionsAppIntent.swift +++ b/Sources/Extensions/AppIntents/WidgetActionsAppIntent.swift @@ -8,11 +8,11 @@ struct WidgetActionsAppIntent: AppIntent, WidgetConfigurationIntent, CustomInten ProgressReportingIntent { static let intentClassName = "WidgetActionsIntent" - static let title: LocalizedStringResource = "Actions" - static let description = IntentDescription("View and run actions") + static let title: LocalizedStringResource = "widgets.actions.title" + static let description = IntentDescription("widgets.actions.description") @Parameter( - title: "Actions", + title: "widgets.actions.parameters.action", size: [ .systemSmall: 1, .systemMedium: 8, diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index 28cc8ef34..d30599176 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -2857,6 +2857,10 @@ public enum L10n { public static var notConfigured: String { return L10n.tr("Localizable", "widgets.actions.not_configured") } /// Actions public static var title: String { return L10n.tr("Localizable", "widgets.actions.title") } + public enum Parameters { + /// Action + public static var action: String { return L10n.tr("Localizable", "widgets.actions.parameters.action") } + } } public enum Assist { /// Ask Assist @@ -2873,46 +2877,48 @@ public enum L10n { public static var reloadTimeline: String { return L10n.tr("Localizable", "widgets.button.reload_timeline") } } public enum Details { - /// Action - public static var action: String { return L10n.tr("Localizable", "widgets.details.action") } /// Display states using from Home Assistant in text public static var description: String { return L10n.tr("Localizable", "widgets.details.description") } - /// Details Text Template (only in rectangular family) - public static var detailsTemplate: String { return L10n.tr("Localizable", "widgets.details.details_template") } - /// Lower Text Template - public static var lowerTemplate: String { return L10n.tr("Localizable", "widgets.details.lower_template") } - /// Max Label Template - public static var maxLabelTemplate: String { return L10n.tr("Localizable", "widgets.details.max_label_template") } - /// Run Action (only in rectangular family) - public static var runAction: String { return L10n.tr("Localizable", "widgets.details.run_action") } - /// Server - public static var server: String { return L10n.tr("Localizable", "widgets.details.server") } /// Details public static var title: String { return L10n.tr("Localizable", "widgets.details.title") } - /// Upper Text Template - public static var upperTemplate: String { return L10n.tr("Localizable", "widgets.details.upper_template") } + public enum Parameters { + /// Action + public static var action: String { return L10n.tr("Localizable", "widgets.details.parameters.action") } + /// Details Text Template (only in rectangular family) + public static var detailsTemplate: String { return L10n.tr("Localizable", "widgets.details.parameters.details_template") } + /// Lower Text Template + public static var lowerTemplate: String { return L10n.tr("Localizable", "widgets.details.parameters.lower_template") } + /// Run Action (only in rectangular family) + public static var runAction: String { return L10n.tr("Localizable", "widgets.details.parameters.run_action") } + /// Server + public static var server: String { return L10n.tr("Localizable", "widgets.details.parameters.server") } + /// Upper Text Template + public static var upperTemplate: String { return L10n.tr("Localizable", "widgets.details.parameters.upper_template") } + } } public enum Gauge { - /// Action - public static var action: String { return L10n.tr("Localizable", "widgets.gauge.action") } /// Display numeric states from Home Assistant in a gauge public static var description: String { return L10n.tr("Localizable", "widgets.gauge.description") } - /// Gauge Type - public static var gaugeType: String { return L10n.tr("Localizable", "widgets.gauge.gauge_type") } - /// Max Label Template - public static var maxLabelTemplate: String { return L10n.tr("Localizable", "widgets.gauge.max_label_template") } - /// Min Label Template - public static var minLabelTemplate: String { return L10n.tr("Localizable", "widgets.gauge.min_label_template") } - /// Run Action - public static var runAction: String { return L10n.tr("Localizable", "widgets.gauge.run_action") } - /// Server - public static var server: String { return L10n.tr("Localizable", "widgets.gauge.server") } /// Gauge public static var title: String { return L10n.tr("Localizable", "widgets.gauge.title") } - /// Value Label Template - public static var valueLabelTemplate: String { return L10n.tr("Localizable", "widgets.gauge.value_label_template") } - /// Value Template (0-1) - public static var valueTemplate: String { return L10n.tr("Localizable", "widgets.gauge.value_template") } + public enum Parameters { + /// Action + public static var action: String { return L10n.tr("Localizable", "widgets.gauge.parameters.action") } + /// Gauge Type + public static var gaugeType: String { return L10n.tr("Localizable", "widgets.gauge.parameters.gauge_type") } + /// Max Label Template + public static var maxLabelTemplate: String { return L10n.tr("Localizable", "widgets.gauge.parameters.max_label_template") } + /// Min Label Template + public static var minLabelTemplate: String { return L10n.tr("Localizable", "widgets.gauge.parameters.min_label_template") } + /// Run Action + public static var runAction: String { return L10n.tr("Localizable", "widgets.gauge.parameters.run_action") } + /// Server + public static var server: String { return L10n.tr("Localizable", "widgets.gauge.parameters.server") } + /// Value Label Template + public static var valueLabelTemplate: String { return L10n.tr("Localizable", "widgets.gauge.parameters.value_label_template") } + /// Value Template (0-1) + public static var valueTemplate: String { return L10n.tr("Localizable", "widgets.gauge.parameters.value_template") } + } } public enum OpenPage { /// Open a frontend page in Home Assistant. From adbc065d0195b8519f47ba214d2a3b10ff8732a5 Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:43:26 -0400 Subject: [PATCH 21/25] Localize the strings in the GagueType app enum --- Sources/App/Resources/en.lproj/Localizable.strings | 2 ++ .../AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift | 6 +++--- Sources/Shared/Resources/Swiftgen/Strings.swift | 6 ++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index bae1eecd3..b37a68457 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -857,6 +857,8 @@ Home Assistant is free and open source home automation software with a focus on "widgets.open_page.not_configured" = "No Pages Available"; "widgets.open_page.title" = "Open Page"; "widgets.gauge.parameters.gauge_type" = "Gauge Type"; +"widgets.gauge.parameters.gauge_type.normal" = "Normal"; +"widgets.gauge.parameters.gauge_type.capacity" = "Capacity"; "widgets.gauge.parameters.server" = "Server"; "widgets.gauge.parameters.value_template" = "Value Template (0-1)"; "widgets.gauge.parameters.value_label_template" = "Value Label Template"; diff --git a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift index 1e45eb863..33b90bf8f 100644 --- a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift +++ b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift @@ -136,9 +136,9 @@ enum GaugeTypeAppEnum: String, Codable, Sendable, AppEnum { case normal case capacity - static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "GaugeType") + static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "widgets.gauge.parameters.gauge_type") static var caseDisplayRepresentations: [GaugeTypeAppEnum: DisplayRepresentation] = [ - .normal: DisplayRepresentation(title: "Normal"), - .capacity: DisplayRepresentation(title: "Capactity"), + .normal: DisplayRepresentation(title: "widgets.gauge.parameters.gauge_type.normal"), + .capacity: DisplayRepresentation(title: "widgets.gauge.parameters.gauge_type.capacity"), ] } diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index d30599176..ac1a544db 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -2918,6 +2918,12 @@ public enum L10n { public static var valueLabelTemplate: String { return L10n.tr("Localizable", "widgets.gauge.parameters.value_label_template") } /// Value Template (0-1) public static var valueTemplate: String { return L10n.tr("Localizable", "widgets.gauge.parameters.value_template") } + public enum GaugeType { + /// Capacity + public static var capacity: String { return L10n.tr("Localizable", "widgets.gauge.parameters.gauge_type.capacity") } + /// Normal + public static var normal: String { return L10n.tr("Localizable", "widgets.gauge.parameters.gauge_type.normal") } + } } } public enum OpenPage { From dc6ebf72440ea5eef01ed77a6612445fa8ff08cb Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Wed, 3 Jul 2024 19:13:57 -0400 Subject: [PATCH 22/25] Add english as a default when no localizaion exists --- .../Details/WidgetDetailsAppIntent.swift | 27 +++++++++----- .../Widget/Gauge/WidgetGaugeAppIntent.swift | 36 ++++++++++++------- .../AppIntents/WidgetActionsAppIntent.swift | 8 +++-- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift b/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift index 2613db8c7..6606de4e2 100644 --- a/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift +++ b/Sources/Extensions/AppIntents/Widget/Details/WidgetDetailsAppIntent.swift @@ -5,14 +5,16 @@ import Shared @available(iOS 17.0, macOS 14.0, watchOS 10.0, *) struct WidgetDetailsAppIntent: WidgetConfigurationIntent { - static let title: LocalizedStringResource = "widgets.details.title" - static let description = IntentDescription("widgets.details.description") + static let title: LocalizedStringResource = .init("widgets.details.title", defaultValue: "Details") + static let description = IntentDescription( + .init("widgets.details.description", defaultValue: "Display states using from Home Assistant in text") + ) - @Parameter(title: "widgets.details.parameters.server", default: nil) + @Parameter(title: .init("widgets.details.parameters.server", defaultValue: "Server"), default: nil) var server: IntentServerAppEntity @Parameter( - title: "widgets.details.parameters.upper_template", + title: .init("widgets.details.parameters.upper_template", defaultValue: "Upper Text Template"), default: "", inputOptions: .init( capitalizationType: .none, @@ -25,7 +27,7 @@ struct WidgetDetailsAppIntent: WidgetConfigurationIntent { var upperTemplate: String @Parameter( - title: "widgets.details.parameters.lower_template", + title: .init("widgets.details.parameters.lower_template", defaultValue: "Lower Text Template"), default: "", inputOptions: .init( capitalizationType: .none, @@ -38,7 +40,10 @@ struct WidgetDetailsAppIntent: WidgetConfigurationIntent { var lowerTemplate: String @Parameter( - title: "widgets.details.parameters.details_template", + title: .init( + "widgets.details.parameters.details_template", + defaultValue: "Details Text Template (only in rectangular family)" + ), default: "", inputOptions: .init( capitalizationType: .none, @@ -50,10 +55,16 @@ struct WidgetDetailsAppIntent: WidgetConfigurationIntent { ) var detailsTemplate: String - @Parameter(title: "widgets.details.parameters.run_action", default: false) + @Parameter( + title: .init("widgets.details.parameters.run_action", defaultValue: "Run Action (only in rectangular family)"), + default: false + ) var runAction: Bool - @Parameter(title: "widgets.details.parameters.action", default: nil) + @Parameter( + title: .init("widgets.details.parameters.action", defaultValue: "Action"), + default: nil + ) var action: IntentActionAppEntity? static var parameterSummary: some ParameterSummary { diff --git a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift index 33b90bf8f..455c8641e 100644 --- a/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift +++ b/Sources/Extensions/AppIntents/Widget/Gauge/WidgetGaugeAppIntent.swift @@ -5,17 +5,19 @@ import Shared @available(iOS 17.0, macOS 14.0, watchOS 10.0, *) struct WidgetGaugeAppIntent: WidgetConfigurationIntent { - static let title: LocalizedStringResource = "widgets.gauge.title" - static let description = IntentDescription("widgets.gauge.description") + static let title: LocalizedStringResource = .init("widgets.gauge.title", defaultValue: "Actions") + static let description = IntentDescription( + .init("widgets.gauge.description", defaultValue: "Display numeric states from Home Assistant in a gauge") + ) - @Parameter(title: "widgets.gauge.parameters.gauge_type", default: .normal) + @Parameter(title: .init("widgets.gauge.parameters.gauge_type", defaultValue: "Gauge Type"), default: .normal) var gaugeType: GaugeTypeAppEnum - @Parameter(title: "widgets.gauge.parameters.server", default: nil) + @Parameter(title: .init("widgets.gauge.parameters.server", defaultValue: "Server"), default: nil) var server: IntentServerAppEntity @Parameter( - title: "widgets.gauge.parameters.value_template", + title: .init("widgets.gauge.parameters.value_template", defaultValue: "Value Template (0-1)"), default: "", inputOptions: .init( capitalizationType: .none, @@ -28,7 +30,7 @@ struct WidgetGaugeAppIntent: WidgetConfigurationIntent { var valueTemplate: String @Parameter( - title: "widgets.gauge.parameters.value_label_template", + title: .init("widgets.gauge.parameters.value_label_template", defaultValue: "Value Label Template"), default: "", inputOptions: .init( capitalizationType: .none, @@ -41,7 +43,7 @@ struct WidgetGaugeAppIntent: WidgetConfigurationIntent { var valueLabelTemplate: String @Parameter( - title: "widgets.gauge.parameters.min_label_template", + title: .init("widgets.gauge.parameters.min_label_template", defaultValue: "Min Label Template"), default: "", inputOptions: .init( capitalizationType: .none, @@ -54,7 +56,7 @@ struct WidgetGaugeAppIntent: WidgetConfigurationIntent { var minTemplate: String @Parameter( - title: "widgets.gauge.parameters.max_label_template", + title: .init("widgets.gauge.parameters.max_label_template", defaultValue: "Max Label Template"), default: "", inputOptions: .init( capitalizationType: .none, @@ -66,10 +68,10 @@ struct WidgetGaugeAppIntent: WidgetConfigurationIntent { ) var maxTemplate: String - @Parameter(title: "widgets.gauge.parameters.run_action", default: false) + @Parameter(title: .init("widgets.gauge.parameters.run_action", defaultValue: "Run Action"), default: false) var runAction: Bool - @Parameter(title: "widgets.gauge.parameters.action", default: nil) + @Parameter(title: .init("widgets.gauge.parameters.action", defaultValue: "Action"), default: nil) var action: IntentActionAppEntity? static var parameterSummary: some ParameterSummary { @@ -136,9 +138,17 @@ enum GaugeTypeAppEnum: String, Codable, Sendable, AppEnum { case normal case capacity - static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "widgets.gauge.parameters.gauge_type") + static let typeDisplayRepresentation = TypeDisplayRepresentation( + name: .init("widgets.gauge.parameters.gauge_type", defaultValue: "GaugeType") + ) static var caseDisplayRepresentations: [GaugeTypeAppEnum: DisplayRepresentation] = [ - .normal: DisplayRepresentation(title: "widgets.gauge.parameters.gauge_type.normal"), - .capacity: DisplayRepresentation(title: "widgets.gauge.parameters.gauge_type.capacity"), + .normal: DisplayRepresentation(title: .init( + "widgets.gauge.parameters.gauge_type.normal", + defaultValue: "Normal" + )), + .capacity: DisplayRepresentation(title: .init( + "widgets.gauge.parameters.gauge_type.capacity", + defaultValue: "Capacity" + )), ] } diff --git a/Sources/Extensions/AppIntents/WidgetActionsAppIntent.swift b/Sources/Extensions/AppIntents/WidgetActionsAppIntent.swift index c590443a1..bef28c318 100644 --- a/Sources/Extensions/AppIntents/WidgetActionsAppIntent.swift +++ b/Sources/Extensions/AppIntents/WidgetActionsAppIntent.swift @@ -8,11 +8,13 @@ struct WidgetActionsAppIntent: AppIntent, WidgetConfigurationIntent, CustomInten ProgressReportingIntent { static let intentClassName = "WidgetActionsIntent" - static let title: LocalizedStringResource = "widgets.actions.title" - static let description = IntentDescription("widgets.actions.description") + static let title: LocalizedStringResource = .init("widgets.actions.title", defaultValue: "Actions") + static let description = IntentDescription( + .init("widgets.actions.description", defaultValue: "Perform Home Assistant actions.") + ) @Parameter( - title: "widgets.actions.parameters.action", + title: .init("widgets.actions.parameters.action", defaultValue: "Action"), size: [ .systemSmall: 1, .systemMedium: 8, From 8ca7e45424efe2c9c5c6e4fd5e4e1602edd1a2ee Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Wed, 3 Jul 2024 19:25:57 -0400 Subject: [PATCH 23/25] Update Sources/PushServer/SharedPush/Sources/NotificationParserLegacy.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bruno Pantaleão Gonçalves --- .../SharedPush/Sources/NotificationParserLegacy.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/PushServer/SharedPush/Sources/NotificationParserLegacy.swift b/Sources/PushServer/SharedPush/Sources/NotificationParserLegacy.swift index ef14660a4..abf88d9e5 100644 --- a/Sources/PushServer/SharedPush/Sources/NotificationParserLegacy.swift +++ b/Sources/PushServer/SharedPush/Sources/NotificationParserLegacy.swift @@ -254,7 +254,7 @@ enum LegacyNotificationCommandType: String { case clearBadge = "clear_badge" case clearNotification = "clear_notification" case updateComplications = "update_complications" - case updateWidgets = "update_wigets" + case updateWidgets = "update_widgets" } private extension Dictionary where Value == Any { From 09692fe2af4670e428243b513323c4928d3663ae Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Wed, 3 Jul 2024 19:33:51 -0400 Subject: [PATCH 24/25] Use the enum for the name of the command --- .../SharedPush/Sources/NotificationParserLegacy.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/PushServer/SharedPush/Sources/NotificationParserLegacy.swift b/Sources/PushServer/SharedPush/Sources/NotificationParserLegacy.swift index abf88d9e5..1989e5b62 100644 --- a/Sources/PushServer/SharedPush/Sources/NotificationParserLegacy.swift +++ b/Sources/PushServer/SharedPush/Sources/NotificationParserLegacy.swift @@ -61,7 +61,7 @@ public struct LegacyNotificationParserImpl: LegacyNotificationParser { let commandPayload: CommandPayload? = { switch LegacyNotificationCommandType(rawValue: input["message"] as? String ?? "") { case .locationUpdate, .locationUpdates: - return .init("request_location_update") + return .init(LegacyNotificationCommandType.locationUpdate.rawValue) case .clearBadge: return .init(isAlert: true, payload: ["aps": ["badge": 0]]) case .clearNotification: @@ -75,11 +75,11 @@ public struct LegacyNotificationParserImpl: LegacyNotificationParser { homeassistant["collapseId"] = collapseId } - return .init("clear_notification", homeassistant: homeassistant) + return .init(LegacyNotificationCommandType.clearNotification.rawValue, homeassistant: homeassistant) case .updateComplications: - return .init("update_complications") + return .init(LegacyNotificationCommandType.updateComplications.rawValue) case .updateWidgets: - return .init("update_widgets") + return .init(LegacyNotificationCommandType.updateWidgets.rawValue) default: return nil } }() From 831e7f2a1078e46636de1af226541e828102f178 Mon Sep 17 00:00:00 2001 From: Hunter Baker <62899372+literally-anything@users.noreply.github.com> Date: Fri, 5 Jul 2024 07:19:06 -0400 Subject: [PATCH 25/25] Make update_widgets only availible on iOS --- .../NotificationsCommandManager.swift | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift index 71ce8c757..c400ad3ff 100644 --- a/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift +++ b/Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift @@ -20,12 +20,9 @@ public class NotificationCommandManager { public init() { register(command: "request_location_update", handler: HandlerLocationUpdate()) register(command: "clear_notification", handler: HandlerClearNotification()) - if #available(watchOS 9, *) { - register(command: "reload_widgets", handler: HandlerReloadWidgets()) - } - #if os(iOS) register(command: "update_complications", handler: HandlerUpdateComplications()) + register(command: "update_widgets", handler: HandlerUpdateWidgets()) #endif } @@ -98,20 +95,6 @@ private struct HandlerClearNotification: NotificationCommandHandler { } } -@available(watchOS 9, *) -private struct HandlerReloadWidgets: NotificationCommandHandler { - func handle(_ payload: [String: Any]) -> Promise { - Current.Log.verbose("Reloading widgets triggered by notification command") - return Promise { seal in - DispatchQueue.main.async { - WidgetCenter.shared.reloadTimelines(ofKind: AppIntentWidgetKinds.gauge) - WidgetCenter.shared.reloadTimelines(ofKind: AppIntentWidgetKinds.details) - seal.fulfill(()) - } - } - } -} - #if os(iOS) private struct HandlerUpdateComplications: NotificationCommandHandler { func handle(_ payload: [String: Any]) -> Promise { @@ -130,4 +113,17 @@ private struct HandlerUpdateComplications: NotificationCommandHandler { } } } + +private struct HandlerUpdateWidgets: NotificationCommandHandler { + func handle(_ payload: [String: Any]) -> Promise { + Current.Log.verbose("Reloading widgets triggered by notification command") + return Promise { seal in + DispatchQueue.main.async { + WidgetCenter.shared.reloadTimelines(ofKind: AppIntentWidgetKinds.gauge) + WidgetCenter.shared.reloadTimelines(ofKind: AppIntentWidgetKinds.details) + seal.fulfill(()) + } + } + } +} #endif