Platzhalter statt Ratespiel, und zwei Griffe im Player

**Das Raster sagt jetzt, ob noch etwas kommt.** Eine Mediathek lädt
seitenweise nach, und wer unten ankam, sah ein Raster, das aufhört — ohne
unterscheiden zu können, ob das alles war oder gerade nachgeladen wird.
Beides sah gleich aus.

Beide Server nennen die Gesamtzahl (Jellyfin `TotalRecordCount`, Plex
`totalSize`), und wir lesen sie längst. Also stehen die nächsten Kacheln
schon da, in der Form, die sie haben werden — gedeckelt auf ein Dutzend,
weil zweihundert Platzhalter niemandem helfen. Ist wirklich alles da,
steht es am Ende ausdrücklich: „Alle 252 Titel geladen".

Gegen die Attrappe mit 252 Titeln durchgeblättert und nachgesehen: die
Zeile steht da, und alle 252 sind angekommen.

**Herein sanft, hinaus zügig.** Das Einblenden der Bedienung begleitet
eine Bewegung des Zeigers und soll wirken, als käme sie der Hand
entgegen; zwei Zehntel wirkten wie ein Aufblitzen. Das Ausblenden
dagegen ist ein Aufräumen, und dabei muss niemand zusehen.

**Ein Klick ins Schwarze blendet um, statt anzuhalten.** Ein Film füllt
das Fenster fast nie ganz aus — die Balken daneben gehören nicht zum
Film, und dort zu klicken ist die natürliche Geste für „zeig mir die
Bedienung". Wo das Bild aufhört, wird aus `presentationSize` gerechnet,
also aus dem, was wirklich ankommt. Fehlen die Maße, verhält es sich wie
bisher.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-27 18:50:26 +02:00
co-authored by Claude Opus 5
parent 279d8e36ae
commit 10caa5fdd1
4 changed files with 190 additions and 5 deletions
+71 -3
View File
@@ -42,6 +42,9 @@ struct PlayerView: View {
@FocusState private var keyboardFocus: Bool
/// Wann die App zuletzt nach vorn geholt wurde.
@State private var becameActive: ContinuousClock.Instant?
/// Wie gross die Player-Fläche gerade ist — für die Frage, wo das Bild
/// aufhört und das Schwarze anfängt.
@State private var playerSize: CGSize = .zero
/// Ob Zeiger und Tastatur gelten. Einmal beim Start ermittelt.
private var isMac: Bool { FlixyPlatform.isMac }
/// Wann der Zeiger zuletzt die Bedienelemente geholt hat.
@@ -156,8 +159,10 @@ struct PlayerView: View {
.persistentSystemOverlays(showsControls ? .automatic : .hidden)
.landscapeWhileVisible(settings.locksLandscape)
.contentShape(.rect)
// Für alles außerhalb der Sprungflächen.
.onTapGesture { primaryTap() }
// Für alles außerhalb der Sprungflächen. Mit Ort, weil es einen
// Unterschied macht, **wo** geklickt wurde — siehe `tap(at:)`.
.onGeometryChange(for: CGSize.self) { $0.size } action: { playerSize = $0 }
.onTapGesture(coordinateSpace: .local) { tap(at: $0) }
.onReceive(
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
) { _ in
@@ -654,6 +659,64 @@ struct PlayerView: View {
/// **Mit dem Finger nicht.** Dort gibt es keine Bewegung ohne Berührung:
/// ein Tipp, der sofort anhält, nähme den einzigen Weg zu Scrubber,
/// Tonspur und Qualität.
/// Ein Klick — und die Frage, worauf.
///
/// **Auf das Bild** heißt anhalten: das ist der Griff, den man aus jedem
/// Player kennt.
///
/// **Ins Schwarze daneben** heißt ein- und ausblenden. Ein Film füllt das
/// Fenster fast nie ganz aus — oben und unten oder links und rechts
/// bleiben Balken, und die gehören nicht zum Film. Dort zu klicken ist
/// die natürliche Geste für „zeig mir die Bedienung" und war bisher
/// dasselbe wie ein Griff mitten ins Bild.
private func tap(at point: CGPoint) {
guard isMac, !videoRect.contains(point) else {
primaryTap()
return
}
toggleControls()
}
/// Wo das Bild tatsächlich liegt.
///
/// Der Player füllt das ganze Fenster, das Bild aber nur den Teil, der
/// zum Seitenverhältnis passt — der Rest ist schwarz. Die Maße kommen
/// aus `presentationSize`, also von dem, was wirklich ankommt, nicht aus
/// den Angaben der Datei.
///
/// Ohne Maße die ganze Fläche: dann verhält es sich wie bisher, statt
/// aus Unkenntnis überall das Falsche zu tun.
private var videoRect: CGRect {
let ganzes = CGRect(origin: .zero, size: playerSize)
guard let breite = model.statistics?.width,
let höhe = model.statistics?.height,
breite > 0, höhe > 0,
playerSize.width > 0, playerSize.height > 0
else { return ganzes }
let verhältnisBild = CGFloat(breite) / CGFloat(höhe)
let verhältnisFläche = playerSize.width / playerSize.height
if verhältnisBild > verhältnisFläche {
// Breiter als das Fenster: Balken oben und unten.
let sichtbar = playerSize.width / verhältnisBild
return CGRect(
x: 0,
y: (playerSize.height - sichtbar) / 2,
width: playerSize.width,
height: sichtbar
)
}
// Höher als das Fenster: Balken links und rechts.
let sichtbar = playerSize.height * verhältnisBild
return CGRect(
x: (playerSize.width - sichtbar) / 2,
y: 0,
width: sichtbar,
height: playerSize.height
)
}
private func primaryTap() {
guard isMac else {
toggleControls()
@@ -698,7 +761,12 @@ struct PlayerView: View {
/// Zeigt die Bedienelemente und setzt die Ausblendfrist neu.
private func revealControls() {
if !showsControls {
withAnimation(.easeInOut(duration: 0.2)) { showsControls = true }
// **Herein sanft, hinaus zügig.** Das Einblenden begleitet eine
// Bewegung des Zeigers und soll sich anfühlen, als käme die
// Bedienung der Hand entgegen — zwei Zehntel wirken dabei wie ein
// Aufblitzen. Das Ausblenden dagegen ist ein Aufräumen, und
// Aufräumen darf man nicht zusehen müssen.
withAnimation(.easeOut(duration: 0.45)) { showsControls = true }
// Beim Aufdecken nachsehen, was inzwischen läuft — genau dann
// will man es wissen.
if model.isLive {