import Combine import Shared import SwiftUI import UIKit /// The Home Assistant web frontend as a SwiftUI view: the web view (`FrontendView`) plus SwiftUI overlay /// content layered on top in a `ZStack`. Blocking screens (the disconnected/unauthenticated empty state and /// the no-active-URL screen) live here as state-driven overlays rather than UIKit modals/subviews on the /// `WebViewController`, so app-level sheets (Settings) can float over them without tearing them down. /// /// Rendered by `ContainerView` when onboarding is complete; conforms to `WebFrontendView`. struct HomeAssistantView: View, WebFrontendView { let server: Server var onWebViewController: ((WebViewController) -> Void)? /// Published by the embedded `WebViewController`; drives the SwiftUI overlays below. @StateObject private var overlayState = WebFrontendOverlayState() /// Drives status-bar / home-indicator hiding from full-screen and kiosk settings (the status-bar /// *style* stays on `WebViewController`, as SwiftUI has no equivalent). @StateObject private var chrome = WebViewChromeState() init(server: Server, onWebViewController: @escaping (WebViewController) -> Void) { self.server = server self.onWebViewController = onWebViewController } /// Edges the web view ignores. When a themed status-bar bar is shown (edge-to-edge off), the web view's /// top is inset to sit below the bar; otherwise it runs fully edge-to-edge. Sides and bottom always bleed. private var webViewIgnoredSafeAreaEdges: Edge.Set { overlayState.statusBarColor == nil ? .all : [.horizontal, .bottom] } /// A theme-colored layer filling the top safe-area inset above the web view. Shown only when /// `overlayState` publishes a color (iOS, edge-to-edge off), in which case the web view's top is inset to /// sit below it. Drawn full-bleed behind the web view so only the otherwise-uncovered status-bar inset /// shows the color — the web view (opaque) covers everything below the top inset. @ViewBuilder private var themedStatusBar: some View { if let color = overlayState.statusBarColor { Color(uiColor: color) .ignoresSafeArea() } } var body: some View { ZStack { FrontendView(server: server, onWebViewController: onWebViewController, overlayState: overlayState) .ignoresSafeArea(edges: webViewIgnoredSafeAreaEdges) if overlayState.showsNoActiveURL { ConnectionSecurityLevelBlockView(server: server) .transition(.opacity) } else if let emptyState = overlayState.emptyState { WebViewEmptyStateView( style: emptyState.style, server: emptyState.server, showsErrorDetailsButton: emptyState.showsErrorDetailsButton, availableReauthURLTypes: emptyState.availableReauthURLTypes, retryAction: emptyState.retryAction, settingsAction: emptyState.settingsAction, errorDetailsAction: emptyState.errorDetailsAction, reauthAction: emptyState.reauthAction, dismissAction: emptyState.dismissAction ) .transition(.opacity) } } .background(themedStatusBar) .animation(DesignSystem.Animation.easeInOutFaster, value: overlayState.emptyState != nil) .animation(DesignSystem.Animation.easeInOutFaster, value: overlayState.showsNoActiveURL) .statusBarHidden(chrome.statusBarHidden) .modify { view in if #available(iOS 16.0, *) { view.persistentSystemOverlays(chrome.homeIndicatorHidden ? .hidden : .automatic) } else { view } } } } /// Observes the settings that drive system-chrome hiding (full-screen, kiosk hide-status-bar) so /// `HomeAssistantView` can hide the status bar / home indicator in SwiftUI rather than via UIKit overrides /// on `WebViewController`. @MainActor final class WebViewChromeState: ObservableObject { @Published private(set) var statusBarHidden: Bool @Published private(set) var homeIndicatorHidden: Bool private var cancellables = Set() init() { self.statusBarHidden = Self.resolveStatusBarHidden() self.homeIndicatorHidden = Current.settingsStore.fullScreen Current.kiosk.settingsPublisher .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.refresh() } .store(in: &cancellables) NotificationCenter.default.publisher(for: SettingsStore.webViewRelatedSettingDidChange) .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.refresh() } .store(in: &cancellables) } private func refresh() { statusBarHidden = Self.resolveStatusBarHidden() homeIndicatorHidden = Current.settingsStore.fullScreen } private static func resolveStatusBarHidden() -> Bool { Current.settingsStore.fullScreen || (Current.kioskSettings.enabled && Current.kioskSettings.hideStatusBar) } }