#if os(tvOS) import SwiftUI /// Ein Plakat, das auf den Fokus antwortet. /// /// **Ein Knopf, kein Bild mit Geste.** Auf tvOS ist alles Bedienbare /// fokussierbar, und `.buttonStyle(.card)` bringt die Systemwirkung mit: /// Anheben, Schlagschatten und die Parallaxe, bei der sich die Ebenen beim /// Wischen gegeneinander verschieben. Die von Hand nachzubauen — /// `scaleEffect` und ein Schatten — sieht auf den ersten Blick gleich aus und /// auf den zweiten falsch: die Parallaxe ist der Teil, den man wiedererkennt. /// /// Liegt in FlixyDesign und nicht im Fernseh-Ziel, aus demselben Grund wie /// `PosterCard`: das Paket kennt weder `MediaItem` noch die Wiedergabe, und /// das soll so bleiben. Herein kommen Zeichenketten und Adressen. public struct TVPosterCard: View { private let title: String private let subtitle: String? private let artworkURL: URL? private let progress: Double? /// Meldet nach oben, wenn diese Karte den Fokus bekommt oder verliert. /// /// **Nach aussen als Rückruf, innen als `@FocusState`.** Die Umgebung /// könnte es nicht beantworten — `\.isFocused` meint den nächsten /// fokussierbaren Vorfahren, und das ist in einer Reihe aus Karten nicht /// verlässlich diese. Wer es wissen will, bekommt es deshalb gesagt. private let onFocusChange: (Bool) -> Void private let action: () -> Void /// **Ausdrücklich und nicht über `\.isFocused`.** Die Umgebungsvariable /// beantwortet die Frage nach dem nächstgelegenen fokussierbaren /// Vorfahren; in einer Reihe aus Karten ist das nicht verlässlich diese. @FocusState private var isFocused: Bool public init( title: String, subtitle: String? = nil, artworkURL: URL?, progress: Double? = nil, onFocusChange: @escaping (Bool) -> Void = { _ in }, action: @escaping () -> Void ) { self.title = title self.subtitle = subtitle self.artworkURL = artworkURL self.progress = progress self.onFocusChange = onFocusChange self.action = action } public var body: some View { TVCard( aspectRatio: PosterCard.aspectRatio, title: title, subtitle: subtitle, artworkURL: artworkURL, progress: progress, isFocused: $isFocused, onFocusChange: onFocusChange, action: action ) } } /// Dasselbe im Querformat — für „Weiterschauen". /// /// Angefangenes erkennt man an der Szene, an der man aufgehört hat; wer /// dagegen aussucht, was als Nächstes kommt, sucht nach Plakaten. public struct TVLandscapeCard: View { private let title: String private let subtitle: String? private let artworkURL: URL? private let progress: Double? /// Meldet nach oben, wenn diese Karte den Fokus bekommt oder verliert. /// /// **Nach aussen als Rückruf, innen als `@FocusState`.** Die Umgebung /// könnte es nicht beantworten — `\.isFocused` meint den nächsten /// fokussierbaren Vorfahren, und das ist in einer Reihe aus Karten nicht /// verlässlich diese. Wer es wissen will, bekommt es deshalb gesagt. private let onFocusChange: (Bool) -> Void private let action: () -> Void @FocusState private var isFocused: Bool public init( title: String, subtitle: String? = nil, artworkURL: URL?, progress: Double? = nil, onFocusChange: @escaping (Bool) -> Void = { _ in }, action: @escaping () -> Void ) { self.title = title self.subtitle = subtitle self.artworkURL = artworkURL self.progress = progress self.onFocusChange = onFocusChange self.action = action } public var body: some View { TVCard( aspectRatio: FlixyAspectRatio.landscape, title: title, subtitle: subtitle, artworkURL: artworkURL, progress: progress, isFocused: $isFocused, onFocusChange: onFocusChange, action: action ) } } /// Was beide gemeinsam haben. private struct TVCard: View { let aspectRatio: CGFloat let title: String let subtitle: String? let artworkURL: URL? let progress: Double? @FocusState.Binding var isFocused: Bool let onFocusChange: (Bool) -> Void let action: () -> Void var body: some View { VStack(alignment: .leading, spacing: FlixySpacing.snug) { Button(action: action) { FlixyArtwork(url: artworkURL, aspectRatio: aspectRatio) .overlay(alignment: .bottom) { if let progress { WatchProgressBar(fraction: progress) } } .clipShape(.rect(cornerRadius: FlixyRadius.card)) } .buttonStyle(.card) .focused($isFocused) // **Die Höhe wird immer belegt, der Text nicht immer gezeigt.** // // Sonst wächst die Karte beim Fokussieren nach unten und schiebt // die ganze Reihe — und man sieht nicht den Fokus, sondern ein // Zucken. Und dauerhaft beschriftet wären zwanzig Plakate aus drei // Metern eine Textwand. VStack(alignment: .leading, spacing: 2) { Text(title) .font(.flixyCardTitle) .foregroundStyle(Color.flixyTextPrimary) .lineLimit(1) if let subtitle { Text(subtitle) .font(.flixyCaption) .foregroundStyle(Color.flixyTextMuted) .lineLimit(1) } } .opacity(isFocused ? 1 : 0) .animation(.easeOut(duration: 0.15), value: isFocused) } .onChange(of: isFocused) { _, neu in onFocusChange(neu) } .accessibilityElement(children: .combine) } } #endif