iOS/Sources/App/WebView/ExternalMessageBus/WidgetSelectionView.swift
Bruno Pantaleão Gonçalves 6b2f83e97e
Add "Add to" frontend compatibility for CarPlay, Widgets and Apple Watch (#4273)
<!-- Thank you for submitting a Pull Request and helping to improve Home
Assistant. Please complete the following sections to help the processing
and review of your changes. Please do not delete anything from this
template. -->

## Summary
<!-- Provide a brief summary of the changes you have made and most
importantly what they aim to achieve -->
This PR allows adding supported domain entities to CarPlay, Widgets and
Apple watch directly from the entity more info dialog

## Screenshots
<!-- If this is a user-facing change not in the frontend, please include
screenshots in light and dark mode. -->

## Link to pull request in Documentation repository
<!-- Pull requests that add, change or remove functionality must have a
corresponding pull request in the Companion App Documentation repository
(https://github.com/home-assistant/companion.home-assistant). Please add
the number of this pull request after the "#" -->
Documentation: home-assistant/companion.home-assistant#

## Any other notes
<!-- If there is any other information of note, like if this Pull
Request is part of a bigger change, please include it here. -->

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-01-29 17:00:17 +01:00

162 lines
5.0 KiB
Swift

import SFSafeSymbols
import Shared
import SwiftUI
/// A bottom sheet view that allows users to select an existing widget to add an entity to,
/// or create a new widget.
struct WidgetSelectionView: View {
@Environment(\.dismiss) private var dismiss
@StateObject private var viewModel: WidgetSelectionViewModel
/// Called when a widget is selected or a new one should be created
/// - Parameter widget: The selected widget, or nil if creating a new one
private let onSelection: (CustomWidget?) -> Void
init(
entityId: String,
serverId: String,
onSelection: @escaping (CustomWidget?) -> Void
) {
self._viewModel = .init(wrappedValue: WidgetSelectionViewModel(
entityId: entityId,
serverId: serverId
))
self.onSelection = onSelection
}
var body: some View {
NavigationView {
List {
if viewModel.widgets.isEmpty {
emptyStateView
} else {
widgetsSection
createNewSection
}
}
.navigationTitle(L10n.Settings.Widgets.Select.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
if #unavailable(iOS 16.0) {
Button(L10n.cancelLabel) {
dismiss()
}
}
}
}
.onAppear {
viewModel.loadWidgets()
}
}
}
private var emptyStateView: some View {
Section {
VStack(spacing: DesignSystem.Spaces.two) {
Image(systemSymbol: {
if #available(iOS 17.0, *) {
return .squareBadgePlusFill
} else {
return .squareshapeDashedSquareshape
}
}())
.font(.system(size: 50))
.foregroundStyle(Color.haPrimary)
Text(L10n.Settings.Widgets.Select.Empty.title)
.font(.headline)
Text(L10n.Settings.Widgets.Select.Empty.subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
Button {
dismiss()
onSelection(nil)
} label: {
Label(L10n.Settings.Widgets.Create.title, systemSymbol: .plus)
}
.buttonStyle(.borderedProminent)
.padding(.top, DesignSystem.Spaces.one)
}
.frame(maxWidth: .infinity)
.padding(.vertical, DesignSystem.Spaces.four)
}
.listRowBackground(Color.clear)
}
private var widgetsSection: some View {
Section {
ForEach(viewModel.widgets, id: \.id) { widget in
Button {
dismiss()
onSelection(widget)
} label: {
HStack {
VStack(alignment: .leading, spacing: DesignSystem.Spaces.half) {
Text(widget.name)
.font(.body)
.foregroundStyle(Color.primary)
Text(L10n.Settings.Widgets.Select.ItemCount.title(widget.items.count))
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Image(systemSymbol: .chevronRight)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
} header: {
Text(L10n.Settings.Widgets.YourWidgets.title)
} footer: {
Text(L10n.Settings.Widgets.Select.Footer.title)
}
}
private var createNewSection: some View {
Section {
Button {
dismiss()
onSelection(nil)
} label: {
Label(L10n.Settings.Widgets.Create.title, systemSymbol: .plus)
}
}
}
}
// MARK: - ViewModel
final class WidgetSelectionViewModel: ObservableObject {
@Published var widgets: [CustomWidget] = []
let entityId: String
let serverId: String
init(entityId: String, serverId: String) {
self.entityId = entityId
self.serverId = serverId
}
func loadWidgets() {
do {
widgets = try CustomWidget.widgets()?.sorted(by: { $0.name < $1.name }) ?? []
} catch {
Current.Log.error("Failed to load widgets: \(error)")
}
}
}
#Preview {
WidgetSelectionView(entityId: "light.living_room", serverId: "server-1") { widget in
print("Selected: \(widget?.name ?? "Create new")")
}
}