import SwiftUI import FlixyDesign import MediaKit /// Die Suche am Fernseher. /// /// **`.searchable` und keine eigene Tastatur.** tvOS macht daraus einen /// eigenen Bildschirm mit Rastertastatur links und Ergebnissen rechts — die /// Bedienung, die jeder von Apples eigenen Apps kennt. Etwas Eigenes zu bauen /// hiesse, sie nachzuahmen und dabei die Diktierfunktion zu verlieren. /// /// Das Modell dahinter ist dasselbe wie am Telefon: `SearchFeed` entprellt, /// fragt und merkt sich die letzten Begriffe. struct TVSearchView: View { @Environment(FlixySession.self) private var session @Environment(PlaybackCoordinator.self) private var playback @Environment(QuickPlay.self) private var quickPlay @State private var text = "" @State private var feed = SearchFeed() @State private var pfad = NavigationPath() var body: some View { NavigationStack(path: $pfad) { inhalt .navigationDestination(for: MediaRoute.self) { route in TVDetailView(route: route) } } .searchable(text: $text, prompt: Text("search.hint")) // Jeder Tastendruck löst eine Anfrage aus — aber erst nach einer // kurzen Pause. Siehe `SearchFeed`. .task(id: text) { await feed.search(text, from: session.backend) } } @ViewBuilder private var inhalt: some View { if text.isEmpty { zuletzt } else if feed.hubs.isEmpty { // Kein Unterschied zwischen „sucht noch" und „nichts gefunden": // auf dem Fernseher tippt man langsam, und ein Text, der bei // jedem Buchstaben zwischen beiden wechselt, flackert nur. leer } else { ergebnisse } } private var ergebnisse: some View { ScrollView { LazyVStack(alignment: .leading, spacing: FlixySpacing.section) { ForEach(feed.hubs) { reihe in TVMediaRow( row: reihe, onSelect: { quickPlay.start($0, backend: session.backend, coordinator: playback) } ) } } .padding(.vertical, FlixySpacing.section) } .scrollClipDisabled() .scrollIndicators(.never) } /// Die letzten Begriffe — als Knöpfe, nicht als Liste. /// /// Am Fernseher ist das der eigentliche Wert der Suche: einmal getippt, /// danach ein Druck. Eine Zeile mit Wischen zum Löschen gibt es hier /// nicht, deshalb steht das Vergessen als eigener Knopf daneben. @ViewBuilder private var zuletzt: some View { if feed.recent.isEmpty { hinweis("search.hint") } else { VStack(alignment: .leading, spacing: FlixySpacing.regular) { Text("search.recent") .font(.flixyRowHeader) .foregroundStyle(Color.flixyTextPrimary) ForEach(feed.recent, id: \.self) { eintrag in Button(eintrag) { text = eintrag } } Button("search.forgetRecent", role: .destructive) { feed.forgetRecent() } .padding(.top, FlixySpacing.regular) } .frame(maxWidth: 700, alignment: .leading) .padding(FlixySpacing.section) } } private var leer: some View { hinweis("search.noResults") } private func hinweis(_ text: LocalizedStringKey) -> some View { Text(text) .font(.flixyBody) .foregroundStyle(Color.flixyTextMuted) .frame(maxWidth: 700) .padding(FlixySpacing.section) } }