import SwiftUI import FlixyDesign import MediaKit /// Eine Kachel, die zur Detailseite führt — mit einem Knopf, der sofort /// startet. /// /// **Der Umweg, den es spart.** Wer weiß, was er sehen will, klickte bisher /// auf die Kachel, wartete auf die Detailseite und drückte dort „Abspielen". /// Zwei Schritte für eine Absicht. /// /// **Warum eine Hülle und keine Änderung an der Karte.** `PosterCard` liegt /// in `FlixyDesign`, und das Paket ist ohne Abhängigkeiten deklariert — es /// kennt weder `MediaItem` noch `PlaybackCoordinator`. Ein Überzug **in** der /// Karte fiele ausserdem unter ihr `.accessibilityElement(children: .combine)` /// und wäre für VoiceOver gar nicht mehr da. struct QuickPlayTile: View { let item: MediaItem @ViewBuilder let card: () -> Card @Environment(FlixySession.self) private var session @Environment(PlaybackCoordinator.self) private var playback @Environment(QuickPlay.self) private var quickPlay @Environment(\.flixyAccent) private var accent @State private var isPointerInside = false private var isMac: Bool { FlixyPlatform.isMac } /// Ob es überhaupt etwas zu starten gibt. /// /// Eine Sammlung ist kein Titel — dort wäre der Knopf eine Behauptung. private var isPlayable: Bool { item.kind != .collection } var body: some View { NavigationLink(value: MediaRoute(ratingKey: item.id, title: item.title)) { card() } .buttonStyle(.plain) // **Als Überzug auf dem Link, nicht als Kind darin.** // // Ein `Button` im *Label* eines `NavigationLink` löst auf iOS nie aus: // der Link macht sein Label zu einer einzigen Fläche, verschachtelte // Bedienelemente werden nicht eigens getroffen — und `.plain` macht // das Versagen lautlos. Als Geschwister darüber gewinnt er. .overlay(alignment: .topTrailing) { if isPlayable, isMac { playButton } } .onContinuousHover { phase in // Zur Laufzeit, nicht beim Übersetzen: „für iPad angepasst" ist // der iOS-Bau und läuft trotzdem auf dem Mac. guard isMac else { return } if case .active = phase { isPointerInside = true } else { isPointerInside = false } } .animation(.easeOut(duration: 0.15), value: isPointerInside) // Der Weg ohne Zeiger: langes Drücken auf dem iPad, Rechtsklick auf // dem Mac. Später der natürliche Ort für „Als gesehen markieren". .contextMenu { if isPlayable { Button("tile.play", systemImage: "play.fill") { trigger() } } } // Für VoiceOver eine **Aktion**, kein zweites Element: die Kachel // bleibt einer, und im Aktionen-Rotor steht „Abspielen". Zwanzig // Kacheln mit je zwei Elementen wären sechzig Wischer. .accessibilityAction(named: Text("tile.play")) { trigger() } } /// Oben rechts, weil der Fortschrittsbalken unten liegt. private var playButton: some View { Button(action: trigger) { Group { if quickPlay.resolvingID == item.id { ProgressView().controlSize(.small).tint(.white) } else { Image(systemName: "play.fill") .font(.system(size: 11, weight: .bold)) .foregroundStyle(.white) } } // Feste Grösse, damit der Kreisel den Knopf nicht wachsen lässt // und nichts springt, wenn nachgeschlagen wird. .frame(width: 28, height: 28) .background(accent.opacity(0.92), in: .circle) .shadow(color: .black.opacity(0.35), radius: 3, y: 1) } .buttonStyle(.plain) .padding(FlixySpacing.snug) .opacity(isPointerInside ? 1 : 0) // Er ist eine Zeigergeste; für VoiceOver gibt es die benannte Aktion // oben. Beides anzubieten hiesse, jede Kachel zu verdoppeln. .accessibilityHidden(true) } private func trigger() { quickPlay.start(item, backend: session.backend, coordinator: playback) } }