import SwiftUI import FlixyDesign import MediaKit /// Die Startseite: Aufmacher oben, darunter die Reihen vom Server. struct HomeView: View { @Environment(FlixySession.self) private var session @Environment(HomeLayoutSettings.self) private var layout @State private var state: LoadState<[MediaRowGroup]> = .loading var body: some View { Group { switch 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 load() } } } case .loaded(let allRows): let hubs = layout.apply(to: allRows) if hubs.isEmpty { ContentUnavailableView { Label("home.empty.title", systemImage: "film.stack") } description: { Text("home.empty.description") } } else { content(hubs: hubs) } } } .background(Color.flixyCanvas) // 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("") .navigationBarTitleDisplayMode(.inline) // 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. .toolbarBackground(.hidden, for: .navigationBar) // Helle Symbole, weil darunter ein Bild liegt und kein heller Grund. .toolbarColorScheme(.dark, for: .navigationBar) .toolbar { SearchToolbarItem() } .navigationDestination(for: MediaRoute.self) { MediaDetailView(route: $0) } .task(id: session.serverName) { await load() } .refreshable { await load() } } private func content(hubs: [MediaRowGroup]) -> some View { ScrollView { LazyVStack(alignment: .leading, spacing: FlixySpacing.section) { let featured = heroItems(in: hubs) if !featured.isEmpty { HeroCarousel(items: featured) } ForEach(hubs) { hub in MediaRow(row: hub) } } .padding(.bottom, FlixySpacing.section) } .scrollIndicators(.hidden) } /// Wie viele Titel der Aufmacher durchwechselt. private static let heroCount = 5 /// Was oben groß gezeigt wird. /// /// Angefangenes zuerst — wer die App öffnet, will meistens weiterschauen. /// Aufgefüllt wird aus den übrigen Reihen: bei einer einzigen angefangenen /// Folge gäbe es sonst nichts zu blättern. private func heroItems(in hubs: [MediaRowGroup]) -> [MediaItem] { let continuing = hubs.first(where: \.isContinueWatching)?.items ?? [] let rest = hubs.filter { !$0.isContinueWatching }.flatMap(\.items) var seen = Set() let candidates = (continuing + rest).filter { seen.insert($0.id).inserted } // Ohne Hintergrundbild ist der Aufmacher eine graue Fläche mit Text. // Lieber einen Titel überspringen als eine leere Bühne zeigen — es sei // denn, keiner hätte eines. let withArtwork = candidates.filter { $0.art != nil || $0.seriesArt != nil } return Array((withArtwork.isEmpty ? candidates : withArtwork).prefix(Self.heroCount)) } private func load() async { guard let backend = session.backend else { return } state = await LoadState.load(keepingPrevious: state) { try await backend.homeRows() } } }