import SwiftUI import FlixyDesign import MediaKit /// Der Aufmacher oben auf der Startseite — mehrere Titel, die sich abwechseln. /// /// Ein einzelnes Bild sagt jedes Mal dasselbe. Hier wandert der Aufmacher /// alle paar Sekunden weiter und lässt sich auch von Hand durchblättern; die /// Punkte darunter zeigen, wie viele es sind und wo man gerade ist. /// /// Das Weiterwandern hält an, sobald jemand selbst blättert — der Zeitgeber /// hängt an der aktuellen Seite und fängt bei jedem Wechsel von vorn an. /// Sonst rutschte der Aufmacher genau in dem Moment weiter, in dem man sich /// einen Titel ansieht. struct HeroCarousel: View { let items: [MediaItem] /// Wie lange ein Titel stehen bleibt. /// /// Lang genug, um Titel, Beschreibung und Knöpfe zu lesen — bei drei /// Sekunden ist der Satz zu Ende, bevor man ihn gelesen hat. private static let dwell: Duration = .seconds(7) @State private var index = 0 /// Ob noch von selbst weitergewandert wird. /// /// Sobald jemand die Fläche berührt, hört es auf — und zwar für gut. Wer /// hier die Hand hat, fährt selbst. Andernfalls rutscht der Aufmacher /// genau in der Sekunde weiter, in der man nach „Abspielen" greift, und /// man startet den falschen Film. Das ist mir beim Ausprobieren selbst /// passiert. @State private var isAdvancing = true /// Bewegung, die von selbst losgeht, ist für manche Menschen unangenehm /// bis unbrauchbar. Wer sie abgestellt hat, blättert von Hand. @Environment(\.accessibilityReduceMotion) private var reduceMotion @Environment(\.flixyAccent) private var accent var body: some View { VStack(spacing: FlixySpacing.regular) { TabView(selection: $index) { ForEach(Array(items.enumerated()), id: \.element.id) { position, item in HeroSection(item: item) .tag(position) } } .tabViewStyle(.page(indexDisplayMode: .never)) // Die eigene Anzeige des TabView läge über dem Bild und damit über // den Knöpfen. Die Höhe kommt aus dem Seitenverhältnis, weil ein // TabView sonst nimmt, was es kriegen kann. .aspectRatio(FlixyAspectRatio.hero, contentMode: .fit) // Gleichzeitig und ohne Mindestweg: fängt jede Berührung ab, ohne // dem Blättern des TabView oder den Knöpfen darin etwas // wegzunehmen. .simultaneousGesture( DragGesture(minimumDistance: 0).onChanged { _ in isAdvancing = false } ) if items.count > 1 { indicator } } .task(id: index) { await advance() } .onChange(of: items.map(\.id)) { _, ids in // Nach dem Aktualisieren kann die Liste kürzer sein als die // Stelle, an der man gerade war. if index >= ids.count { index = 0 } } } private var indicator: some View { HStack(spacing: FlixySpacing.snug) { ForEach(items.indices, id: \.self) { position in Capsule() .fill(position == index ? accent : Color.flixyTextMuted.opacity(0.4)) .frame(width: position == index ? 18 : 6, height: 6) } } .animation(.snappy(duration: 0.25), value: index) .accessibilityHidden(true) } private func advance() async { guard items.count > 1, !reduceMotion, isAdvancing else { return } try? await Task.sleep(for: Self.dwell) guard !Task.isCancelled else { return } withAnimation(.easeInOut(duration: 0.5)) { index = (index + 1) % items.count } } }