import SwiftUI import FlixyDesign import MediaKit /// Die Mediatheken auf dem iPhone — mit einer Leiste zum Wechseln. /// /// **Was hier wegfällt, ist ein Zwischenschritt.** Vorher stand an dieser /// Stelle eine Liste aller Mediatheken; wer Filme sehen wollte, tippte auf /// „Bibliothek", dann auf „Filme", und bekam erst dann ein Raster. Zurück und /// in eine andere hiess: zweimal tippen. Der Zwischenbildschirm hat nie etwas /// erzählt, was die Leiste nicht auch zeigen kann. /// /// Auf dem iPad und am Mac gibt es ihn ohnehin nicht — dort steht jede /// Mediathek einzeln in der Seitenleiste, und auf dem Fernseher nebeneinander /// in der oberen Leiste. Das hier ist dieselbe Idee in der Form, die ein /// Telefon hergibt. /// /// **Warum eine zweite Leiste und nicht Plätze in der unteren.** Die hat /// fünf, und alles darüber schiebt das System in ein „Mehr"-Verzeichnis, /// dessen Einträge zwei Navigationsleisten übereinander tragen. Eine selbst /// gezeichnete Leiste kennt diese Grenze nicht. struct LibraryBrowser: View { @Environment(FlixySession.self) private var session /// Was gerade gezeigt wird. /// /// `nil` heisst „noch nichts gewählt" — dann gilt die erste Mediathek. /// Eine Vorauswahl beim Anlegen ginge nicht: die Mediatheken kommen vom /// Server und stehen beim ersten Aufbau noch nicht fest. @State private var auswahl: Auswahl? private enum Auswahl: Hashable { case library(String) case liveTV } var body: some View { inhalt .background(Color.flixyCanvas) // **Als Sicherheitsrand, nicht als Überzug.** So endet die Liste // über der Leiste, statt dahinter zu verschwinden — und das gilt // auch für die Ladeanzeige und den Fuss „alles geladen". .safeAreaInset(edge: .bottom, spacing: 0) { if eintraege.count > 1 { leiste } } } @ViewBuilder private var inhalt: some View { switch aktuell { case .library(let id): if let library = session.libraries.first(where: { $0.id == id }) { // **Mit `id`, damit der Wechsel sauber neu aufsetzt.** Ohne // das behielte die Ansicht die Titel der vorigen Mediathek, // bis der Server antwortet — und für einen Augenblick stünden // Filme unter „Serien". LibraryView(library: library).id(library.id) } case .liveTV: LiveTVView() case nil: ContentUnavailableView { Label("libraryHub.empty", systemImage: "books.vertical") } } } // MARK: - Die Leiste private var leiste: some View { // **So breit wie nötig, nicht wie möglich.** Über die ganze Breite // gezogen stand sie linksbündig in einer halbleeren Kapsel — eine // Leiste, die dreimal so lang ist wie ihr Inhalt, sieht aus wie ein // Fehler im Layout. // // `ViewThatFits` nimmt die erste Fassung, die hineinpasst: die // ungescrollte Reihe, solange sie passt, sonst die scrollbare. Damit // ist beides richtig, ohne dass irgendwo eine Zahl stünde, ab wann es // kippt — bei vier Mediatheken passt es, bei acht nicht, und keine // Fassung muss das wissen. ViewThatFits(in: .horizontal) { reihe ScrollView(.horizontal) { reihe } .scrollBounceBehavior(.basedOnSize) .scrollIndicators(.never) } // Dieselbe Fläche wie die Insel auf dem iPad — ein `static`-Bauteil, // kein nachgebautes Aussehen. .background { FlixySidebar.surface(in: .capsule) } .clipShape(.capsule) .shadow(color: .black.opacity(0.45), radius: 18, y: 6) .padding(.horizontal, FlixySpacing.regular) .padding(.bottom, FlixySpacing.snug) } private var reihe: some View { HStack(spacing: 0) { ForEach(eintraege, id: \.auswahl) { eintrag in pille(eintrag) } } .padding(.horizontal, FlixySpacing.snug) .padding(.vertical, FlixySpacing.snug) } private func pille(_ eintrag: Eintrag) -> some View { let gewaehlt = eintrag.auswahl == aktuell return Button { auswahl = eintrag.auswahl } label: { Label { Text(eintrag.titel).lineLimit(1) } icon: { Image(systemName: eintrag.symbol) } // Kleiner als in der Leiste auf dem iPad: dort stehen drei // Einträge, hier bis zu fünf — und auf einem Telefon. .font(.flixyCaption) .foregroundStyle(gewaehlt ? Color.flixyTextPrimary : Color.flixyTextSecondary) .padding(.horizontal, FlixySpacing.snug) .padding(.vertical, FlixySpacing.snug) .background { if gewaehlt { Capsule().fill(Color.flixyElevated) } } .contentShape(Capsule()) } .buttonStyle(.plain) .accessibilityAddTraits(gewaehlt ? .isSelected : []) } // MARK: - Was in der Leiste steht private struct Eintrag { let auswahl: Auswahl let titel: String let symbol: String } private var eintraege: [Eintrag] { var liste = session.libraries.map { Eintrag(auswahl: .library($0.id), titel: $0.title, symbol: $0.kind.symbol) } // Live-TV wohnte im Zwischenbildschirm und bräuchte sonst ein neues // Zuhause. Hier steht es, wo man es sucht: neben den Mediatheken. if session.supportsLiveTV { liste.append(Eintrag( auswahl: .liveTV, titel: String(localized: "sidebar.liveTV"), symbol: MediaLibrary.Kind.liveTV.symbol )) } return liste } /// Die gültige Auswahl — die gespeicherte, solange es sie noch gibt. /// /// **Nicht einfach `auswahl`.** Nach einem Serverwechsel zeigt die /// gespeicherte Kennung auf eine Mediathek, die es nicht mehr gibt; dann /// stünde ein leerer Bildschirm da, ohne dass irgendetwas erklärt, warum. private var aktuell: Auswahl? { if let auswahl, eintraege.contains(where: { $0.auswahl == auswahl }) { return auswahl } return eintraege.first?.auswahl } }