Mini-Player: die Wiedergabe überlebt das Zurückgehen

Wer im Fernsehen etwas sieht und nebenher nachsehen will, was sonst
läuft, musste bisher abbrechen. Plex' eigene Handy-Apps können das auch
nicht — kein Grund, es hier genauso zu machen.

Der eigentliche Umbau ist nicht die Kachel, sondern der Besitz. Die
Wiedergabe hing an der Sichtbarkeit der Ansicht, und zwar dreifach: das
Modell war `@State` im Player, `onDisappear` hat den Motor abgeräumt, und
die Bildebene entstand in `makeUIView` neu. Zurückgehen hieß deshalb:
Motor tot, Transcoder aus, beim Live-TV der Tuner freigegeben.

Jetzt gehört das Modell der Wurzelansicht, die immer da ist. Verkleinern
und Vergrößern wechseln nur noch die Darstellung; beendet wird die
Wiedergabe an genau einer Stelle, und zwar weil jemand es will.

Die Bildebene hält das Modell selbst — dasselbe Muster, das VLC seit
jeher verwendet. Dieselbe `UIView` wandert zwischen Kachel und Vollbild,
statt neu zu entstehen; sonst setzte das Bild bei jedem Wechsel neu an.
Bild-in-Bild wandert mit und bekommt endlich einen Delegaten: bisher war
es eine Einbahnstraße, aus der man nicht in die App zurückkam.

Die Kachel schwebt und lässt sich in jede Ecke ziehen, statt als Leiste
unten zu kleben — eine Leiste nähme jeder Liste Platz weg und müsste in
jedem Bildschirm bedacht werden. Die Eckenberechnung steht in
MiniPlayerPlacement und ist geprüft: in einer View wäre eine Kachel, die
halb aus dem Bild ragt, erst auf dem Gerät aufgefallen.

Der Pfeil oben verkleinert, ein neuer Knopf daneben beendet wirklich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-12 17:09:22 +02:00
co-authored by Claude Opus 5
parent d28df346f2
commit 26c909a2e2
11 changed files with 3269 additions and 2668 deletions
+63 -4
View File
@@ -36,19 +36,78 @@ struct PlaybackRequest: Identifiable, Hashable {
@MainActor
@Observable
final class PlaybackCoordinator {
var request: PlaybackRequest?
/// Wie der Player gerade zu sehen ist.
///
/// Der Unterschied zwischen „klein" und „gar nicht" ist der Kern der
/// Sache: verkleinern darf die Wiedergabe **nicht** anhalten. Vorher gab
/// es diese Unterscheidung nicht — der Player war da oder weg, und mit
/// ihm der Motor, der Transcoder und beim Live-TV der belegte Tuner.
enum Presentation: Equatable {
case hidden
case mini
case full
}
private(set) var request: PlaybackRequest?
private(set) var presentation: Presentation = .hidden
func play(_ item: PlexItem, mediaIndex: Int = 0, resuming: Bool = true) {
request = PlaybackRequest(
play(PlaybackRequest(
ratingKey: item.ratingKey,
title: item.playbackTitle,
mediaIndex: mediaIndex,
startTime: resuming ? item.resumeTime : .zero
)
))
}
func dismiss() {
/// Schaltet einen laufenden Sender ein.
///
/// Eigener Weg, weil ein Sender nicht in der Mediathek liegt: Adresse und
/// Tuner-Belegung stehen schon fest und dürfen nicht neu ermittelt werden.
func playLive(
channelID: String,
title: String,
stream: URL,
subscriptionKey: String?
) {
play(PlaybackRequest(
ratingKey: channelID,
title: title,
mediaIndex: 0,
startTime: .zero,
streamURL: stream,
liveSubscriptionKey: subscriptionKey
))
}
/// Übernimmt einen neuen Titel.
///
/// Läuft bereits die Kachel, bleibt es dabei: wer nebenher stöbert, will
/// beim Antippen nicht ins Vollbild gerissen werden. Sonst Vollbild, wie
/// man es von einem Antippen erwartet.
func play(_ next: PlaybackRequest) {
presentation = presentation == .mini ? .mini : .full
request = next
}
/// Verkleinert den Player zur schwebenden Kachel. Die Wiedergabe läuft.
func minimize() {
guard request != nil else { return }
presentation = .mini
}
func expand() {
guard request != nil else { return }
presentation = .full
}
/// Beendet die Wiedergabe wirklich.
///
/// Die einzige Stelle, an der das passiert. Alles andere — verkleinern,
/// vergrößern, Bild-in-Bild — lässt den Motor am Leben.
func close() {
request = nil
presentation = .hidden
}
}