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>
This commit is contained in:
Scarriffle
2026-09-13 14:19:06 +02:00
co-authored by Claude Opus 5
parent fb418c842d
commit 5763d866bf
4 changed files with 95 additions and 61 deletions
+46 -57
View File
@@ -26,8 +26,6 @@ struct MiniPlayer: View {
/// Verschiebung während des Ziehens, noch nicht eingerastet.
@State private var drag: CGSize = .zero
@State private var showsControls = false
@State private var hideTask: Task<Void, Never>?
private var side: MiniPlayerPlacement.Side {
MiniPlayerPlacement.Side(rawValue: storedSide) ?? .trailing
@@ -92,9 +90,7 @@ struct MiniPlayer: View {
ProgressView().tint(.white)
}
if showsControls {
controls.transition(.opacity)
}
corners
// Bei einem laufenden Sender hat ein Fortschritt keine Bedeutung —
// es gibt kein Ende, auf das er zulaufen könnte.
@@ -112,53 +108,59 @@ struct MiniPlayer: View {
}
.shadow(color: .black.opacity(0.5), radius: 12, y: 4)
.contentShape(.rect)
.onTapGesture {
// Antippen zeigt erst die Bedienelemente, dann öffnet es. Ein
// sofortiges Aufklappen bei jeder Berührung wäre lästig, wenn man
// die Kachel nur anfassen will.
if showsControls {
playback.expand()
} else {
reveal()
}
}
// Antippen vergrößert, sofort. Vorher brauchte es zwei Berührungen:
// die erste holte die Bedienelemente hervor, die zweite sollte
// öffnen — traf dabei aber den Abspielknopf, der genau in der Mitte
// lag. Vergrößern war damit überhaupt nicht möglich.
.onTapGesture { playback.expand() }
.accessibilityElement(children: .contain)
.accessibilityLabel(Text(model.title))
}
private var controls: some View {
ZStack {
Color.black.opacity(0.45)
Button {
model.togglePlayPause()
reveal()
} label: {
Image(systemName: model.isPlaying ? "pause.fill" : "play.fill")
.font(.title2)
.foregroundStyle(.white)
.padding(FlixySpacing.snug)
.contentShape(.rect)
}
.accessibilityLabel(Text(model.isPlaying ? "player.pause" : "player.play"))
VStack {
HStack {
Spacer()
Button {
playback.close()
} label: {
Image(systemName: "xmark")
.font(.caption.weight(.bold))
.foregroundStyle(.white)
.padding(FlixySpacing.snug)
.contentShape(.rect)
}
.accessibilityLabel(Text("miniPlayer.close"))
/// Die beiden Knöpfe in den oberen Ecken.
///
/// Dauerhaft sichtbar statt auf Antippen hin. Die Fläche in der Mitte
/// gehört dem Vergrößern — sie ist das, was man an einer solchen Kachel
/// am häufigsten tut, und sie darf nicht mit einem Knopf belegt sein.
/// Unten bleibt frei für den Fortschrittsbalken.
private var corners: some View {
VStack {
HStack {
tileButton(
model.isPlaying ? "pause.fill" : "play.fill",
label: model.isPlaying ? "player.pause" : "player.play"
) {
model.togglePlayPause()
}
Spacer()
tileButton("xmark", label: "miniPlayer.close") {
playback.close()
}
}
Spacer()
}
.padding(FlixySpacing.tight)
}
/// Ein kleiner Knopf, der auf jedem Bild lesbar bleibt.
///
/// Die dunkle Scheibe darunter ist nicht Zierde: ein weißes Symbol auf
/// einer hellen Szene verschwindet, und die Kachel zeigt Filmbilder, deren
/// Helligkeit sich jede Sekunde ändert.
private func tileButton(
_ symbol: String,
label: LocalizedStringKey,
action: @escaping () -> Void
) -> some View {
Button(action: action) {
Image(systemName: symbol)
.font(.caption.weight(.bold))
.foregroundStyle(.white)
.frame(width: 26, height: 26)
.background(.black.opacity(0.55), in: .circle)
}
.buttonStyle(.plain)
.accessibilityLabel(Text(label))
}
/// Größer und kleiner durch Zusammen- und Auseinanderziehen.
@@ -168,7 +170,6 @@ struct MiniPlayer: View {
private func zoomGesture(in bounds: CGSize) -> some Gesture {
MagnifyGesture()
.onChanged { value in
hideTask?.cancel()
zoom = value.magnification
}
.onEnded { _ in
@@ -191,7 +192,6 @@ struct MiniPlayer: View {
) -> some Gesture {
DragGesture()
.onChanged { value in
hideTask?.cancel()
drag = value.translation
}
.onEnded { value in
@@ -209,17 +209,6 @@ struct MiniPlayer: View {
drag = .zero
}
}
/// Zeigt die Bedienelemente und blendet sie nach kurzer Zeit wieder aus.
private func reveal() {
withAnimation(.easeInOut(duration: 0.2)) { showsControls = true }
hideTask?.cancel()
hideTask = Task {
try? await Task.sleep(for: .seconds(3))
guard !Task.isCancelled else { return }
withAnimation(.easeInOut(duration: 0.2)) { showsControls = false }
}
}
}
extension View {