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:
co-authored by
Claude Opus 5
parent
d28df346f2
commit
26c909a2e2
@@ -0,0 +1,84 @@
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
/// Wo die schwebende Videokachel sitzt.
|
||||
///
|
||||
/// Eigener Typ statt Rechnungen in der Ansicht, aus demselben Grund wie bei
|
||||
/// `GuideLayout`: hier geht lautlos schief, was man nicht sieht. Eine Kachel,
|
||||
/// die halb aus dem Bild ragt, oder eine, die beim Loslassen in die falsche
|
||||
/// Ecke springt, fällt erst auf dem Gerät auf — und dann als Gefühl, nicht
|
||||
/// als Fehler.
|
||||
public struct MiniPlayerPlacement: Sendable, Equatable {
|
||||
public enum Corner: String, Sendable, CaseIterable, Codable {
|
||||
case topLeading, topTrailing, bottomLeading, bottomTrailing
|
||||
|
||||
var isTop: Bool { self == .topLeading || self == .topTrailing }
|
||||
var isLeading: Bool { self == .topLeading || self == .bottomLeading }
|
||||
}
|
||||
|
||||
/// Der sichtbare Bereich, in dem die Kachel sich bewegen darf.
|
||||
public let bounds: CGSize
|
||||
public let tile: CGSize
|
||||
/// Abstand zum Rand. Eine Kachel, die den Rand berührt, sieht aus wie ein
|
||||
/// Fehler statt wie eine Entscheidung.
|
||||
public let inset: CGFloat
|
||||
|
||||
public init(bounds: CGSize, tile: CGSize, inset: CGFloat = FlixySpacing.regular) {
|
||||
self.bounds = bounds
|
||||
self.tile = tile
|
||||
self.inset = inset
|
||||
}
|
||||
|
||||
/// Die Ecke, in die eine losgelassene Kachel gehört.
|
||||
///
|
||||
/// Entschieden wird nach dem **Mittelpunkt** der Kachel, nicht nach dem
|
||||
/// Finger: sonst springt eine Kachel, die man an der oberen Kante
|
||||
/// angefasst hat, in die falsche Hälfte.
|
||||
public func nearestCorner(toCenter center: CGPoint) -> Corner {
|
||||
let isLeading = center.x < bounds.width / 2
|
||||
let isTop = center.y < bounds.height / 2
|
||||
|
||||
switch (isTop, isLeading) {
|
||||
case (true, true): return .topLeading
|
||||
case (true, false): return .topTrailing
|
||||
case (false, true): return .bottomLeading
|
||||
case (false, false): return .bottomTrailing
|
||||
}
|
||||
}
|
||||
|
||||
/// Die linke obere Ecke der Kachel für eine Ecke des Bildschirms.
|
||||
public func origin(for corner: Corner) -> CGPoint {
|
||||
CGPoint(
|
||||
x: corner.isLeading ? inset : max(inset, bounds.width - tile.width - inset),
|
||||
y: corner.isTop ? inset : max(inset, bounds.height - tile.height - inset)
|
||||
)
|
||||
}
|
||||
|
||||
/// Der Mittelpunkt der Kachel in einer Ecke — damit rechnet SwiftUI.
|
||||
public func center(for corner: Corner) -> CGPoint {
|
||||
let start = origin(for: corner)
|
||||
return CGPoint(x: start.x + tile.width / 2, y: start.y + tile.height / 2)
|
||||
}
|
||||
|
||||
/// Hält einen Mittelpunkt im sichtbaren Bereich.
|
||||
///
|
||||
/// Während des Ziehens, damit die Kachel nicht hinter den Rand rutscht und
|
||||
/// dort unerreichbar wird — der Finger wäre dann schneller als das Ziel.
|
||||
public func clamped(center: CGPoint) -> CGPoint {
|
||||
let halfWidth = tile.width / 2
|
||||
let halfHeight = tile.height / 2
|
||||
|
||||
let minX = inset + halfWidth
|
||||
let maxX = bounds.width - inset - halfWidth
|
||||
let minY = inset + halfHeight
|
||||
let maxY = bounds.height - inset - halfHeight
|
||||
|
||||
return CGPoint(
|
||||
// `max(min…)` in dieser Reihenfolge, weil bei einem Bereich, der
|
||||
// schmaler ist als die Kachel, sonst der untere Anschlag verliert
|
||||
// und die Kachel rechts hinausragt.
|
||||
x: max(minX, min(maxX, center.x)),
|
||||
y: max(minY, min(maxY, center.y))
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user