Files
Flaro/Flixy/Views/Home/HeroCarousel.swift
T
ScarriffleandClaude Opus 5 5763d866bf Miniplayer: antippen vergrößert wieder
Die Kachel ließ sich nicht mehr öffnen, und das war kein Zufall: das
Antippen zeigte erst die Bedienelemente, und die zweite Berührung sollte
vergrößern — traf dabei aber den Abspielknopf, der genau in der Mitte lag.
Jede weitere Berührung landete wieder dort. Vergrößern war damit überhaupt
nicht möglich.

Jetzt gehört die Mitte dem Vergrößern, und zwar beim ersten Antippen. Die
zwei Knöpfe — Pause und Schließen — sitzen dauerhaft sichtbar in den oberen
Ecken, auf einer dunklen Scheibe, damit sie auf hellen Filmbildern nicht
verschwinden. Unten bleibt der Fortschrittsbalken.

Dass die Kachel „einfach weg" war, konnte ich nicht nachstellen; der eine
Fall, den ich reproduziert habe, war mein eigener Attrappenserver mit einer
Fortsetzungsstelle jenseits des Dateiendes. Zwei Dinge machen es trotzdem
unwahrscheinlicher: die Knöpfe sind auch dann zu sehen, wenn das Bild
schwarz bliebe — vorher war eine schwarze Kachel in einer fast schwarzen
Oberfläche praktisch unsichtbar —, und die Bildfläche hängt jetzt in einer
eigenen Hülle je Ansicht. Beim Wechsel zwischen Kachel und Vollbild gibt es
beide kurz gleichzeitig, und eine `UIView` hat nur einen Vorgesetzten: wer
sie zuletzt bekommt, gewinnt. Gab SwiftUI sie unmittelbar heraus, konnte das
Abräumen der einen der anderen die Bildfläche wegnehmen.

Dazu etwas, das mir beim Ausprobieren selbst passiert ist: der Aufmacher
wanderte weiter, während ich nach „Abspielen" griff, und ich startete den
falschen Film. Er hört jetzt bei der ersten Berührung auf zu wandern. Wer
die Hand darauf hat, fährt selbst.

Nachgesehen auf iPhone und iPad: verkleinern, antippen, wieder im Vollbild.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 14:19:06 +02:00

89 lines
3.7 KiB
Swift

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
}
}
}