Fix video notification for watchOS (#3846)

<!-- 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 -->

## 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. -->
This commit is contained in:
Bruno Pantaleão Gonçalves
2025-09-24 01:20:22 -03:00
committed by GitHub
parent e01318be23
commit 6e6722317a
3 changed files with 34 additions and 24 deletions

View File

@@ -6,6 +6,10 @@ import WatchKit
final class DynamicNotificationHostingController: WKUserNotificationHostingController<DynamicNotificationView> {
private let viewModel = DynamicNotificationViewModel()
override class var sashColor: Color? { .haPrimary }
override class var wantsSashBlur: Bool { true }
override class var isInteractive: Bool { true }
override var body: DynamicNotificationView {
DynamicNotificationView(viewModel: viewModel)
}

View File

@@ -9,12 +9,14 @@ import UserNotifications
struct DynamicNotificationView: View {
@ObservedObject var viewModel: DynamicNotificationViewModel
@State private var player = AVPlayer()
private let dynamicContentHeight: CGFloat = 150
var body: some View {
VStack(alignment: .leading, spacing: DesignSystem.Spaces.two) {
textContent
// Dynamic content is presented first to align with watchOS default notification style
dynamicContentView
textContent
loader
errorView
}
@@ -55,19 +57,18 @@ struct DynamicNotificationView: View {
}
private var textContent: some View {
VStack(alignment: .leading, spacing: DesignSystem.Spaces.half) {
VStack(alignment: .leading, spacing: .zero) {
if !viewModel.title.isEmpty {
Text(viewModel.title)
.font(.headline)
}
if !viewModel.subtitle.isEmpty {
Text(viewModel.subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
.font(.headline)
}
if !viewModel.body.isEmpty {
Text(viewModel.body)
.font(.footnote)
.font(.body)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
@@ -81,14 +82,24 @@ struct DynamicNotificationView: View {
.clipShape(RoundedRectangle(cornerRadius: 6))
}
@ViewBuilder
private func videoPlayer(url: URL) -> some View {
VideoPlayer(player: AVPlayer(url: url))
VideoPlayer(player: player)
.frame(maxWidth: .infinity)
.frame(height: dynamicContentHeight)
.clipShape(RoundedRectangle(cornerRadius: DesignSystem.CornerRadius.one))
.onAppear {
// Autoplay to mimic WKInterfaceInlineMovie.play()
AVPlayer(url: url).play()
// Build a new item each time we appear and autoplay shortly after.
let item = AVPlayerItem(url: url)
player.replaceCurrentItem(with: item)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
player.play()
}
}
.onDisappear {
// Release the item so file handles are closed; the view model owns security-scope.
player.pause()
player.replaceCurrentItem(with: nil)
}
}

View File

@@ -135,34 +135,29 @@ final class DynamicNotificationViewModel: ObservableObject {
}
}
// MARK: - Media handling
private func handleMediaURL(_ url: URL) {
let didStart = url.startAccessingSecurityScopedResource()
if didStart {
securityScopedURL = url
}
// Attempt to decode as image; otherwise treat as video
do {
let data = try Data(contentsOf: url, options: .alwaysMapped)
if let img = UIImage(data: data) {
DispatchQueue.main.async {
self.content = .image(img)
self.isLoading = false
}
} else {
DispatchQueue.main.async {
self.content = .video(url)
self.isLoading = false
}
}
} catch {
if let img = UIImage(contentsOfFile: url.path) {
DispatchQueue.main.async {
self.errorMessage = error.localizedDescription
self.content = .image(img)
self.isLoading = false
}
} else {
DispatchQueue.main.async {
self.content = .video(url)
self.isLoading = false
}
}
}
// MARK: - Map functions
private static func parseDegrees(_ any: Any?) -> CLLocationDegrees? {
if let d = any as? Double { return d }
if let n = any as? NSNumber { return n.doubleValue }