Mediatheken nach oben — und das Regal sagt endlich, was es sieht

**Die Mediatheken sind Reiter geworden.** Sie standen unten auf der
Startseite, und dorthin musste man erst scrollen — an allen Vorschlägen
vorbei, obwohl „ich will in die Filme" genau der Fall ist, in dem einem
die Vorschläge egal sind. Damit fällt die Reihe unten weg: zwei Wege zum
selben Raster sind auf einem Fernseher kein Komfort, sondern eine Frage,
welcher der richtige ist.

Die Leiste liest sich jetzt „Start · Filme · Serien · Gemischtes ·
Suchen" und wird von tvOS mittig gehalten, egal wie viele Mediatheken
dazukommen. Die Stelle der Suche bestimmt Apple, nicht diese Datei:
`role: .search` setzt sie dorthin, wo sie in jeder anderen App auch
steht, und gibt ihr die Lupe.

**Und eine Statuszeile fürs Regal.** Bleibt es leer, gibt es genau drei
Möglichkeiten: die App-Gruppe fehlt, die Datei ist nicht da, oder sie ist
da und das System fragt trotzdem nicht. Von aussen sehen alle drei gleich
aus — nämlich nach dem grossen Plakat aus dem Asset-Katalog. Nach zwei
Runden Vermuten steht unten links jetzt, welcher Fall es ist: „3 Reihen,
9 Einträge, zuletzt 21:55" oder eben „Keine App-Gruppe".

Nur im Entwicklerbau (`#if DEBUG`) und als Überlagerung, nicht als letzte
Zeile der Liste: dorthin müsste man erst an allen Reihen vorbeiscrollen,
und wer nachsieht, warum das Regal leer ist, soll es sofort sehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-29 21:56:31 +02:00
co-authored by Claude Opus 5
parent 08514eb564
commit 41719595b9
4 changed files with 162 additions and 51 deletions
+24 -37
View File
@@ -38,9 +38,6 @@ struct TVHomeView: View {
.navigationDestination(for: MediaRoute.self) { route in
TVDetailView(route: route)
}
.navigationDestination(for: LibraryRoute.self) { route in
TVLibraryView(libraryID: route.id)
}
}
.task(id: session.serverName) { await feed.load(from: session.backend) }
.onChange(of: deepLinkTarget) { _, neu in
@@ -96,10 +93,6 @@ struct TVHomeView: View {
onFocus: { aufmacher = $0 }
)
}
if !session.libraries.isEmpty {
mediatheken
}
}
.padding(.bottom, FlixySpacing.screenMargin)
}
@@ -108,40 +101,34 @@ struct TVHomeView: View {
// Aufmacher muss bis an die Bildschirmkante reichen; ein Bild, das
// achtzig Punkte davor aufhört, sieht nach Fehler aus.
.ignoresSafeArea()
// Als Überlagerung und nicht als letzte Zeile der Liste: dorthin
// müsste man erst an allen Reihen vorbeiscrollen, und wer nachsieht,
// warum das Regal leer ist, soll es sofort sehen.
.overlay(alignment: .bottom) { regalStatus }
}
/// Die Mediatheken als eigene Reihe.
/// Was das Regal über der App zuletzt bekommen hat.
///
/// Unten und nicht oben: wer den Fernseher einschaltet, will meistens
/// weiterschauen, nicht stöbern. Und als Reihe statt als Registerkarten,
/// weil eine Reihe am Fokus hängt und Registerkarten eine zweite Bedienart
/// wären.
private var mediatheken: some View {
VStack(alignment: .leading, spacing: FlixySpacing.regular) {
Text("library.title")
.font(.flixyRowHeader)
.foregroundStyle(Color.flixyTextPrimary)
.padding(.horizontal, FlixySpacing.screenMargin)
ScrollView(.horizontal) {
LazyHStack(spacing: FlixySpacing.regular) {
ForEach(session.libraries, id: \.id) { mediathek in
Button {
pfad.append(LibraryRoute(id: mediathek.id))
} label: {
Text(mediathek.title)
.font(.flixyRowHeader)
.frame(width: 360, height: 180)
}
.buttonStyle(.card)
}
}
.padding(.horizontal, FlixySpacing.screenMargin)
.padding(.vertical, FlixySpacing.regular)
}
.scrollClipDisabled()
.scrollIndicators(.never)
/// **Nur im Entwicklerbau.** Es gehört nicht in die fertige App — aber
/// solange das Regal leer bleibt, sieht man von aussen nicht, woran es
/// liegt: fehlende App-Gruppe, fehlende Datei und ein System, das nicht
/// fragt, sehen alle gleich aus. Diese Zeile unterscheidet sie.
@ViewBuilder
private var regalStatus: some View {
#if DEBUG
VStack(alignment: .leading, spacing: FlixySpacing.tight) {
Text("topShelf.title")
.font(.flixyCaption)
.foregroundStyle(Color.flixyTextMuted)
Text(TopShelfWriter.status)
.font(.flixyCaption)
.foregroundStyle(Color.flixyTextSecondary)
}
.padding(.horizontal, FlixySpacing.screenMargin)
.padding(.bottom, FlixySpacing.regular)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.black.opacity(0.6))
#endif
}
// MARK: - Was ein Druck auslöst
+31 -14
View File
@@ -1,22 +1,21 @@
import SwiftUI
import FlixyDesign
import MediaKit
/// Start und Suche nebeneinander.
/// Start, die Mediatheken und die Suche nebeneinander.
///
/// **Ein `TabView`, und erst jetzt.** Bis hierher gab es genau einen
/// Bildschirm, und eine Leiste mit einem einzigen Eintrag wäre Zierrat
/// gewesen. Mit der Suche gibt es zwei Orte, zwischen denen man wechselt —
/// und dafür ist die Leiste oben die Bedienung, die jeder vom Fernseher
/// kennt: sie erscheint, wenn man vom obersten Element aus nach oben wischt,
/// und verschwindet wieder.
/// **Die Mediatheken als Reiter, nicht als Reihe.** Sie standen unten auf der
/// Startseite, und dorthin musste man erst scrollen — an allen Vorschlägen
/// vorbei, obwohl „ich will in die Filme" genau der Fall ist, in dem einem
/// die Vorschläge egal sind. Oben sind sie einen Wisch entfernt.
///
/// **Kein Reiter für die Mediatheken.** Die stehen als Reihe auf der
/// Startseite. Ein dritter Reiter dafür verspräche einen eigenen Bereich, und
/// dahinter läge dasselbe Raster, das eine Reihe tiefer ohnehin erreichbar
/// ist.
/// Damit fällt die Reihe unten weg: zwei Wege zum selben Raster sind auf
/// einem Fernseher kein Komfort, sondern eine Frage, welcher der richtige ist.
struct TVShell: View {
@Binding var deepLinkTarget: MediaRoute?
@Environment(FlixySession.self) private var session
/// Welcher Reiter offen ist.
///
/// Ausdrücklich gehalten, nicht dem System überlassen: ein Link aus dem
@@ -24,7 +23,11 @@ struct TVShell: View {
/// Suche offen, passierte sichtbar nichts.
@State private var reiter = Reiter.start
private enum Reiter: Hashable { case start, suche }
private enum Reiter: Hashable {
case start
case mediathek(String)
case suche
}
var body: some View {
TabView(selection: $reiter) {
@@ -34,9 +37,23 @@ struct TVShell: View {
Text("sidebar.home")
}
ForEach(session.libraries, id: \.id) { mediathek in
Tab(value: Reiter.mediathek(mediathek.id)) {
// Jeder Reiter bekommt seinen eigenen Stapel. Ein
// gemeinsamer hiesse, dass ein Wechsel des Reiters die
// Detailseite des vorigen mitnimmt.
NavigationStack {
TVLibraryView(libraryID: mediathek.id)
}
} label: {
Text(mediathek.title)
}
}
// **`role: .search` und nicht ein gewöhnlicher Reiter.** tvOS
// stellt ihn dadurch ans Ende der Leiste und gibt ihm das
// Lupensymbol — dieselbe Stelle wie in Apples eigenen Apps.
// setzt ihn dadurch dorthin, wo er in jeder anderen App auch
// steht, und gibt ihm die Lupe. Die Stelle bestimmt also Apple,
// nicht diese Datei.
Tab(value: Reiter.suche, role: .search) {
TVSearchView()
}