import SwiftUI import FlixyDesign import MediaKit /// Eine waagerechte Reihe von Titeln. struct MediaRow: View { let row: MediaRowGroup @Environment(FlixySession.self) private var session /// Breit genug, dass die übernächste Kachel angeschnitten ist — das zeigt, /// dass es weitergeht, ohne einen Pfeil dafür zu brauchen. private let landscapeWidth: CGFloat = 220 /// Schmaler, weil hochkant: bei gleicher Höhe passen so mehr Plakate /// nebeneinander, und mehr Auswahl auf einen Blick ist genau der Zweck. private let posterWidth: CGFloat = 116 var body: some View { VStack(alignment: .leading, spacing: FlixySpacing.regular) { Text(row.title) .font(.flixyRowHeader) .foregroundStyle(Color.flixyTextPrimary) .padding(.horizontal, FlixySpacing.screenMargin) ScrollView(.horizontal) { LazyHStack(spacing: FlixySpacing.regular) { ForEach(row.items) { item in NavigationLink(value: MediaRoute(ratingKey: item.id, title: item.title)) { card(for: item) } .buttonStyle(.plain) } } .padding(.horizontal, FlixySpacing.screenMargin) // Ohne dieses Ziel rasten die Kacheln nicht ein und die Reihe // bleibt an beliebiger Stelle stehen. .scrollTargetLayout() } .scrollTargetBehavior(.viewAligned) .scrollIndicators(.never) } } /// Szenenbild beim Weiterschauen, sonst das Plakat. /// /// Angefangenes erkennt man an der Szene, an der man aufgehört hat — /// dafür ist das Querformat richtig. Wer dagegen aussucht, was er als /// Nächstes sehen will, sucht nach Plakaten. @ViewBuilder private func card(for item: MediaItem) -> some View { if row.isContinueWatching { LandscapeCard( title: item.rowTitle, subtitle: item.cardSubtitle, artworkURL: session.backend?.artworkURL( path: item.rowArtworkPath, width: 440, height: 248 ), progress: item.watchProgress ) .frame(width: landscapeWidth) } else { PosterCard( title: item.rowTitle, subtitle: item.cardSubtitle, artworkURL: session.backend?.artworkURL( path: item.posterArtworkPath, width: 300, height: 450 ), progress: item.watchProgress ) .frame(width: posterWidth) } } } extension MediaItem { /// Der Titel auf einer Kachel in einer Reihe. /// /// Groß steht, worum es geht: bei einer Folge ihr eigener Titel, bei /// einer Staffel der Name der Serie. Die Einordnung — Serie, Staffel — /// gehört darunter. „Staffel 10" allein als Überschrift sagt nichts /// darüber, welche Serie gemeint ist. var rowTitle: String { switch kind { case .episode: title // Die Serie steht je nach Antwort mal unter `parentTitle`, mal unter // `seriesTitle`. Fehlt beides, bleibt „Staffel 2" — besser als // gar keine Beschriftung. case .season: parentTitle ?? seriesTitle ?? title default: title } } /// Welches Bild auf die Querformat-Kachel gehört. /// /// Bei Folgen ist `thumb` das Szenenbild der Folge — genau richtig. Bei /// Filmen ist `thumb` das Hochkant-Plakat, das im Querformat beschnitten /// würde; dort ist `art` (das Hintergrundbild) die bessere Wahl. var rowArtworkPath: String? { switch kind { case .episode: thumb ?? seriesThumb default: art ?? thumb } } /// Welches Bild auf die Hochkant-Kachel gehört. /// /// Bei Folgen das Plakat der Serie statt des Szenenbilds: ein 16:9-Bild /// hochkant beschnitten zeigt einen zufälligen Ausschnitt und ist zum /// Wiedererkennen wertlos. var posterArtworkPath: String? { switch kind { case .episode: seriesThumb ?? parentThumb ?? thumb default: thumb } } }