Downloads gab es nur im Downloads-Bereich. Wer wissen wollte, was im Flugzeug läuft, musste zwei Listen vergleichen: die Mediathek und die Downloads. Jetzt sind sie eine Reihe auf der Startseite, tragen in jeder Kachel eine Marke, und in der Mediathek gibt es „Nur Heruntergeladene" — der Filter fragt den Server gar nicht erst, weil der nicht weiss, was auf diesem Gerät liegt. Dabei kam heraus, dass Downloads nie ein Bild mitbrachten: der Dateiname dafür stand im Modell, gesetzt hat ihn niemand. Neue holen es mit, alten wird es beim nächsten Start nachgereicht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
107 lines
4.3 KiB
Swift
107 lines
4.3 KiB
Swift
import SwiftUI
|
|
import FlaroDesign
|
|
import FlaroDownloads
|
|
import MediaKit
|
|
|
|
/// Die Startseite: Aufmacher oben, darunter die Reihen vom Server.
|
|
///
|
|
/// **Nur noch Darstellung.** Was auf der Startseite steht — laden, ordnen,
|
|
/// den Aufmacher auslosen — liegt in `HomeFeed`, weil der Fernseher dieselben
|
|
/// Reihen zeigt und die Logik sonst zweimal dastünde. Kopiert wäre dabei
|
|
/// ausgerechnet der Teil gewesen, an dem schon mehrere Fehler steckten.
|
|
struct HomeView: View {
|
|
@Environment(FlaroSession.self) private var session
|
|
@Environment(HomeLayoutSettings.self) private var layout
|
|
@Environment(DownloadCoordinator.self) private var downloads
|
|
@State private var feed = HomeFeed()
|
|
|
|
/// Was auf dem Gerät liegt, als Reihe ganz oben.
|
|
///
|
|
/// Wird bei jeder Änderung an den Downloads neu berechnet, weil
|
|
/// `DownloadCoordinator` beobachtet wird — eine fertige Folge steht
|
|
/// ohne Neuladen der Startseite da.
|
|
private var heruntergeladen: [MediaItem] {
|
|
Heruntergeladenes.fertige(in: downloads).map {
|
|
Heruntergeladenes.titel($0, in: downloads)
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
Group {
|
|
switch feed.state {
|
|
case .loading:
|
|
ProgressView().frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
|
case .failed(let message):
|
|
ContentUnavailableView {
|
|
Label("error.title", systemImage: "exclamationmark.triangle")
|
|
} description: {
|
|
Text(message)
|
|
} actions: {
|
|
Button("action.retry") { Task { await feed.load(from: session.backend) } }
|
|
}
|
|
|
|
case .loaded:
|
|
let hubs = feed.rows(for: layout, downloaded: heruntergeladen)
|
|
if hubs.isEmpty {
|
|
ContentUnavailableView {
|
|
Label("home.empty.title", systemImage: "film.stack")
|
|
} description: {
|
|
Text("home.empty.description")
|
|
}
|
|
} else {
|
|
content(hubs: hubs)
|
|
}
|
|
}
|
|
}
|
|
.background(Color.flaroCanvas)
|
|
// Kein Titel: der Name der App gehört auf den Homescreen, nicht
|
|
// über jeden Bildschirm. Darunter liegt der Aufmacher, und der sagt
|
|
// ohnehin mehr.
|
|
.navigationTitle("")
|
|
.flaroInlineTitle()
|
|
// Gar kein Hintergrund statt eines weichgezeichneten: die Knöpfe
|
|
// bringen ihre eigene Glasfläche mit und bleiben auch über dem Bild
|
|
// lesbar. `.ultraThinMaterial` verschleierte dagegen den halben
|
|
// Aufmacher.
|
|
.flaroClearToolbarBackground()
|
|
// Gar keine Navigationsleiste auf der Startseite.
|
|
//
|
|
// Sie war leer — kein Titel, keine Knöpfe — und belegte trotzdem ihre
|
|
// Höhe. Der Aufmacher fing dadurch tiefer an als die Seitenleiste
|
|
// daneben, und genau diese Kante sah man. Auf den geschobenen
|
|
// Bildschirmen bleibt sie, dort trägt sie den Zurück-Weg.
|
|
.toolbar(.hidden, for: .navigationBar)
|
|
.task(id: session.serverName) {
|
|
await feed.load(from: session.backend)
|
|
// Siehe `DownloadService.plakateNachholen`: ältere Downloads
|
|
// haben kein Bild, und hier ist die Stelle, an der man es merken
|
|
// würde.
|
|
await DownloadService(backend: session.backend, coordinator: downloads)
|
|
.plakateNachholen()
|
|
}
|
|
.refreshable { await feed.load(from: session.backend) }
|
|
// Was eine Kachel am Server ändert, ändert auch, was hier steht:
|
|
// eine abgehakte Folge gehört nicht mehr in „Weiterschauen".
|
|
.environment(\.flaroItemChanged, { await feed.load(from: session.backend) })
|
|
}
|
|
|
|
private func content(hubs: [MediaRowGroup]) -> some View {
|
|
ScrollView {
|
|
LazyVStack(alignment: .leading, spacing: FlaroSpacing.section) {
|
|
let featured = feed.heroItems(for: layout)
|
|
if !featured.isEmpty {
|
|
HeroCarousel(items: featured)
|
|
}
|
|
|
|
ForEach(hubs) { hub in
|
|
MediaRow(row: hub)
|
|
}
|
|
}
|
|
.padding(.bottom, FlaroSpacing.section)
|
|
}
|
|
.scrollIndicators(.never)
|
|
.flaroRevealsTopChrome()
|
|
}
|
|
}
|