Vorher stand dort eine Liste aller Mediatheken: auf „Bibliothek" tippen, dann auf „Filme", dann endlich ein Raster. In eine andere wechseln hiess zurück und wieder hinein. Der Zwischenbildschirm hat nie etwas erzählt, was eine Leiste nicht auch zeigen kann — er war ein Platzproblem, kein Entwurf. Jetzt steht man sofort in einer Mediathek, und darüber der unteren Leiste liegt eine zweite mit den Mediatheken und Live-TV. Auf dem iPad und am Mac ändert sich nichts: dort steht jede Mediathek einzeln in der Seitenleiste, auf dem Fernseher nebeneinander in der oberen Leiste — es 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. Genau daran ist der Zwischenbildschirm seinerzeit entstanden. Eine selbst gezeichnete Leiste kennt diese Grenze nicht — und sie sieht aus wie die Insel auf dem iPad, weil sie dieselbe Fläche benutzt: `FlixySidebar.surface(in:)` ist ein Bauteil, kein nachgebautes Aussehen, und wird jetzt zum dritten Mal gerufen. Als Sicherheitsrand und nicht als Überzug, damit die Liste über der Leiste endet statt dahinter zu verschwinden. Bei mehr als vier Mediatheken wird sie waagerecht scrollbar. **Das Monogramm steht jetzt in der Werkzeugleiste**, am rechten Ende, auf derselben Höhe wie Suche, Filter und Ansicht — die rücken dafür nach links. Als Überzug lag es über dem Inhalt, auf einem Raster also über einem Plakat. Dass es dabei an den Anfang der Reihe rutschte, hat einen Grund, der im Quelltext steht: SwiftUI reiht Werkzeugleisten von aussen nach innen, ein von aussen angehängter Eintrag steht deshalb **vor** den übrigen. Wo in der Reihe etwas steht, kann nur der entscheiden, der die Reihe schreibt. Auf der Startseite bleibt es ein Überzug — dort gibt es keine Navigationsleiste. Dazu: die Zuordnung Mediathekenart → Symbol stand dreimal im Projekt, eine davon falsch (jede Mediathek bekam „film", auch eine mit Serien). Jetzt einmal, neben den übrigen Ableitungen aus den Modellen. Geprüft im Simulator: „Bibliothek" öffnet sofort das Raster, die Leiste wechselt zwischen Filme, Serien, Gemischtes und Live-TV, und das Monogramm steht ganz rechts in der Werkzeugleiste. iOS und tvOS bauen, 469 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
155 lines
5.8 KiB
Swift
155 lines
5.8 KiB
Swift
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 {
|
|
ScrollView(.horizontal) {
|
|
HStack(spacing: 0) {
|
|
ForEach(eintraege, id: \.auswahl) { eintrag in
|
|
pille(eintrag)
|
|
}
|
|
}
|
|
.padding(.horizontal, FlixySpacing.snug)
|
|
.padding(.vertical, FlixySpacing.snug)
|
|
}
|
|
// Bei vier Einträgen passt alles; die Leiste soll trotzdem nicht
|
|
// brechen, wenn jemand sieben Mediatheken führt.
|
|
.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 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
|
|
}
|
|
}
|