Der Klick in den schwarzen Rand trifft jetzt den schwarzen Rand
Er hat weiterhin angehalten statt ein- und auszublenden, und der Grund steckte in der Rechnung: der schwarze Rand wurde aus der Fenstergrösse ausgerechnet, das Bild liegt aber in einer Fläche mit `ignoresSafeArea()`. Auf dem Mac ist der Einzug der Fensterleiste genau der Unterschied zwischen beiden — die Rechnung lag um diesen Betrag daneben. Jetzt wird die Bildfläche gemessen statt angenommen, und beide Zahlen stehen in einem gemeinsamen, benannten Koordinatenraum. Vor allem aber wird zuerst `AVPlayerLayer.videoRect` gefragt: der Player weiss selbst, wo Bild steht. Die eigene Rechnung bleibt für den Moment vor dem ersten Bild und für VLC, das keine solche Ebene hat. Dazu zwei Worte: „Nachschub" heisst jetzt „Nachladen", und „Blatt" heisst wieder „Sheet" — das ist ein Fachbegriff und bleibt englisch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f47c6385e3
commit
18958aadbc
+4146
-4146
File diff suppressed because it is too large
Load Diff
@@ -141,7 +141,7 @@ extension Comparable {
|
|||||||
///
|
///
|
||||||
/// Gespeicherte Werte werden beim Lesen geprüft, nicht blind übernommen:
|
/// Gespeicherte Werte werden beim Lesen geprüft, nicht blind übernommen:
|
||||||
/// eine ältere Fassung mit anderen Grenzen — oder ein von Hand
|
/// eine ältere Fassung mit anderen Grenzen — oder ein von Hand
|
||||||
/// verändertes Voreinstellungsblatt — soll nicht zu einer Kachel führen,
|
/// verändertes Voreinstellungs-Sheet — soll nicht zu einer Kachel führen,
|
||||||
/// die den halben Bildschirm einnimmt.
|
/// die den halben Bildschirm einnimmt.
|
||||||
func clamped(to range: ClosedRange<Self>) -> Self {
|
func clamped(to range: ClosedRange<Self>) -> Self {
|
||||||
min(max(self, range.lowerBound), range.upperBound)
|
min(max(self, range.lowerBound), range.upperBound)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ struct LibraryFilterSheet: View {
|
|||||||
|
|
||||||
/// Ein Arbeitsstand, der erst beim Bestätigen übernommen wird.
|
/// Ein Arbeitsstand, der erst beim Bestätigen übernommen wird.
|
||||||
///
|
///
|
||||||
/// Sonst löste jeder Tipp im Blatt sofort eine neue Serverabfrage aus —
|
/// Sonst löste jeder Tipp im Sheet sofort eine neue Serverabfrage aus —
|
||||||
/// bei drei Änderungen hintereinander drei verworfene Ladevorgänge.
|
/// bei drei Änderungen hintereinander drei verworfene Ladevorgänge.
|
||||||
@State private var draft: MediaQuery
|
@State private var draft: MediaQuery
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ struct LibraryView: View {
|
|||||||
@State private var state: LoadState<Void> = .loading
|
@State private var state: LoadState<Void> = .loading
|
||||||
@State private var query = MediaQuery()
|
@State private var query = MediaQuery()
|
||||||
@State private var page: MediaPage?
|
@State private var page: MediaPage?
|
||||||
/// Ob der letzte Nachschub misslungen ist.
|
/// Ob der letzte Nachladen misslungen ist.
|
||||||
///
|
///
|
||||||
/// Dann steht unten eine Zeile zum Weiterversuchen: von selbst passiert
|
/// Dann steht unten eine Zeile zum Weiterversuchen: von selbst passiert
|
||||||
/// nichts mehr, weil die Kacheln, die das Nachladen auslösen, längst
|
/// nichts mehr, weil die Kacheln, die das Nachladen auslösen, längst
|
||||||
@@ -171,7 +171,7 @@ struct LibraryView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Der Weg weiter, wenn der Nachschub hängen geblieben ist.
|
/// Der Weg weiter, wenn das Nachladen hängen geblieben ist.
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var moreButton: some View {
|
private var moreButton: some View {
|
||||||
if loadMoreFailed, let page, page.hasMore {
|
if loadMoreFailed, let page, page.hasMore {
|
||||||
@@ -261,7 +261,7 @@ struct LibraryView: View {
|
|||||||
/// Lädt nach, sobald die Ansicht sich dem Ende der Liste nähert.
|
/// Lädt nach, sobald die Ansicht sich dem Ende der Liste nähert.
|
||||||
private func loadMoreIfNeeded(reaching item: MediaItem) async {
|
private func loadMoreIfNeeded(reaching item: MediaItem) async {
|
||||||
guard
|
guard
|
||||||
// Fünf Einträge vor Schluss: früh genug, dass der Nachschub da
|
// Fünf Einträge vor Schluss: früh genug, dass das Nachgeladene da
|
||||||
// ist, bevor jemand am Ende ankommt, und spät genug, dass nicht
|
// ist, bevor jemand am Ende ankommt, und spät genug, dass nicht
|
||||||
// jedes Scrollen eine Anfrage auslöst.
|
// jedes Scrollen eine Anfrage auslöst.
|
||||||
items.suffix(5).contains(where: { $0.id == item.id }),
|
items.suffix(5).contains(where: { $0.id == item.id }),
|
||||||
@@ -305,7 +305,7 @@ struct LibraryView: View {
|
|||||||
// hindeutete. Genau so sieht „es sind nicht alle Filme drin" aus.
|
// hindeutete. Genau so sieht „es sind nicht alle Filme drin" aus.
|
||||||
//
|
//
|
||||||
// Die Seite bleibt also, damit ein zweiter Versuch möglich ist.
|
// Die Seite bleibt also, damit ein zweiter Versuch möglich ist.
|
||||||
// Ein Fehlerblatt mitten im Scrollen wäre trotzdem störend —
|
// Ein Fehler-Sheet mitten im Scrollen wäre trotzdem störend —
|
||||||
// stattdessen erscheint unten eine Zeile zum Weiterladen.
|
// stattdessen erscheint unten eine Zeile zum Weiterladen.
|
||||||
loadMoreFailed = true
|
loadMoreFailed = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ struct LiveTVView: View {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Ein Blatt statt eines Dialogs: die Antwort des Servers ist
|
// Ein Sheet statt eines Dialogs: die Antwort des Servers ist
|
||||||
// mehrzeilig, und ein Dialog schneidet sie ab — dann steht dort
|
// mehrzeilig, und ein Dialog schneidet sie ab — dann steht dort
|
||||||
// wieder nur, dass es nicht ging.
|
// wieder nur, dass es nicht ging.
|
||||||
.sheet(item: $tuneFailure) { failure in
|
.sheet(item: $tuneFailure) { failure in
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ struct AccountBar: View {
|
|||||||
.background(Color.flixySurface)
|
.background(Color.flixySurface)
|
||||||
// Auf dem iPhone gäbe es diese Leiste gar nicht; die Angabe
|
// Auf dem iPhone gäbe es diese Leiste gar nicht; die Angabe
|
||||||
// steht trotzdem da, damit aus dem Popover dort kein
|
// steht trotzdem da, damit aus dem Popover dort kein
|
||||||
// bildschirmfüllendes Blatt wird.
|
// bildschirmfüllendes Sheet wird.
|
||||||
.presentationCompactAdaptation(.popover)
|
.presentationCompactAdaptation(.popover)
|
||||||
}
|
}
|
||||||
.confirmationDialog(
|
.confirmationDialog(
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ struct RootView: View {
|
|||||||
/// Jahr vorgibt.
|
/// Jahr vorgibt.
|
||||||
///
|
///
|
||||||
/// Vorher standen hier drei selbstgebaute Formen nebeneinander: ein
|
/// Vorher standen hier drei selbstgebaute Formen nebeneinander: ein
|
||||||
/// ausfahrbares Blatt, eine geteilte Ansicht und eine Reihe oben. Sie
|
/// ausfahrbares Sheet, eine geteilte Ansicht und eine Reihe oben. Sie
|
||||||
/// konnten zusammen weniger als das eine Bauteil und mussten dreimal
|
/// konnten zusammen weniger als das eine Bauteil und mussten dreimal
|
||||||
/// gepflegt werden.
|
/// gepflegt werden.
|
||||||
/// Läuft diese App gerade auf einem Mac?
|
/// Läuft diese App gerade auf einem Mac?
|
||||||
|
|||||||
@@ -45,8 +45,19 @@ struct PlayerView: View {
|
|||||||
/// Wie gross die Player-Fläche gerade ist — für die Frage, wo das Bild
|
/// Wie gross die Player-Fläche gerade ist — für die Frage, wo das Bild
|
||||||
/// aufhört und das Schwarze anfängt.
|
/// aufhört und das Schwarze anfängt.
|
||||||
@State private var playerSize: CGSize = .zero
|
@State private var playerSize: CGSize = .zero
|
||||||
|
/// Wo die Bildfläche im Player liegt — gemessen, nicht angenommen.
|
||||||
|
///
|
||||||
|
/// **Der Unterschied zur Fenstergrösse.** Die Bildfläche hat
|
||||||
|
/// `ignoresSafeArea()`, der Player selbst nicht. Auf dem Mac liegt oben
|
||||||
|
/// die Fensterleiste als Einzug: die beiden Flächen sind gegeneinander
|
||||||
|
/// verschoben. Wer den schwarzen Rand aus der Fenstergrösse ausrechnet,
|
||||||
|
/// rechnet ihn um genau diesen Einzug falsch — und trifft daneben.
|
||||||
|
@State private var videoFrame: CGRect = .zero
|
||||||
/// Ob Zeiger und Tastatur gelten. Einmal beim Start ermittelt.
|
/// Ob Zeiger und Tastatur gelten. Einmal beim Start ermittelt.
|
||||||
private var isMac: Bool { FlixyPlatform.isMac }
|
private var isMac: Bool { FlixyPlatform.isMac }
|
||||||
|
|
||||||
|
/// Der gemeinsame Raum für Messen und Tippen.
|
||||||
|
private static let playerSpace = "flixy.player"
|
||||||
/// Wann der Zeiger zuletzt die Bedienelemente geholt hat.
|
/// Wann der Zeiger zuletzt die Bedienelemente geholt hat.
|
||||||
@State private var lastPointerReveal: Date = .distantPast
|
@State private var lastPointerReveal: Date = .distantPast
|
||||||
|
|
||||||
@@ -64,6 +75,9 @@ struct PlayerView: View {
|
|||||||
isActive: playback.presentation == .full
|
isActive: playback.presentation == .full
|
||||||
)
|
)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
.onGeometryChange(for: CGRect.self) {
|
||||||
|
$0.frame(in: .named(Self.playerSpace))
|
||||||
|
} action: { videoFrame = $0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
// **Unter** den Bedienelementen, nicht darüber. Als Überzug
|
// **Unter** den Bedienelementen, nicht darüber. Als Überzug
|
||||||
@@ -162,7 +176,11 @@ struct PlayerView: View {
|
|||||||
// Für alles außerhalb der Sprungflächen. Mit Ort, weil es einen
|
// Für alles außerhalb der Sprungflächen. Mit Ort, weil es einen
|
||||||
// Unterschied macht, **wo** geklickt wurde — siehe `tap(at:)`.
|
// Unterschied macht, **wo** geklickt wurde — siehe `tap(at:)`.
|
||||||
.onGeometryChange(for: CGSize.self) { $0.size } action: { playerSize = $0 }
|
.onGeometryChange(for: CGSize.self) { $0.size } action: { playerSize = $0 }
|
||||||
.onTapGesture(coordinateSpace: .local) { tap(at: $0) }
|
// **Ein benannter Raum, kein `.local`.** Gemessen wird die Bildfläche,
|
||||||
|
// getippt wird auf den Player — zwei verschiedene Ansichten. Nur in
|
||||||
|
// einem gemeinsamen Raum sind die beiden Zahlen vergleichbar.
|
||||||
|
.coordinateSpace(.named(Self.playerSpace))
|
||||||
|
.onTapGesture(coordinateSpace: .named(Self.playerSpace)) { tap(at: $0) }
|
||||||
.onReceive(
|
.onReceive(
|
||||||
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
|
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
|
||||||
) { _ in
|
) { _ in
|
||||||
@@ -196,7 +214,7 @@ struct PlayerView: View {
|
|||||||
}
|
}
|
||||||
// **Die Umgebung wird den Blättern mitgegeben, nicht vererbt.**
|
// **Die Umgebung wird den Blättern mitgegeben, nicht vererbt.**
|
||||||
//
|
//
|
||||||
// Ein Blatt hängt an einem eigenen Darstellungsbaum. Auf dem iPad kam
|
// Ein Sheet hängt an einem eigenen Darstellungsbaum. Auf dem iPad kam
|
||||||
// die Umgebung des Players dort bisher an, auf dem Mac nicht — und
|
// die Umgebung des Players dort bisher an, auf dem Mac nicht — und
|
||||||
// `@Environment(PlaybackSettings.self)` ist kein Wert mit
|
// `@Environment(PlaybackSettings.self)` ist kein Wert mit
|
||||||
// Ersatzangabe, sondern eine Behauptung: fehlt der Gegenstand, hält
|
// Ersatzangabe, sondern eine Behauptung: fehlt der Gegenstand, hält
|
||||||
@@ -288,7 +306,7 @@ struct PlayerView: View {
|
|||||||
|
|
||||||
// Getrennte Knöpfe, weil es zwei verschiedene Fragen sind: die
|
// Getrennte Knöpfe, weil es zwei verschiedene Fragen sind: die
|
||||||
// Tonspur stellt man einmal am Anfang um, die Untertitel mitten im
|
// Tonspur stellt man einmal am Anfang um, die Untertitel mitten im
|
||||||
// Film. Ein gemeinsames Blatt bedeutete jedes Mal Scrollen an der
|
// Film. Ein gemeinsames Sheet bedeutete jedes Mal Scrollen an der
|
||||||
// anderen Liste vorbei.
|
// anderen Liste vorbei.
|
||||||
if !model.audioTracks.isEmpty {
|
if !model.audioTracks.isEmpty {
|
||||||
trackButton(
|
trackButton(
|
||||||
@@ -679,41 +697,58 @@ struct PlayerView: View {
|
|||||||
|
|
||||||
/// Wo das Bild tatsächlich liegt.
|
/// Wo das Bild tatsächlich liegt.
|
||||||
///
|
///
|
||||||
/// Der Player füllt das ganze Fenster, das Bild aber nur den Teil, der
|
/// **Zuerst die Ebene fragen.** `AVPlayerLayer.videoRect` ist die Fläche,
|
||||||
/// zum Seitenverhältnis passt — der Rest ist schwarz. Die Maße kommen
|
/// auf der wirklich Bild steht — vom Player selbst ausgerechnet, aus dem,
|
||||||
/// aus `presentationSize`, also von dem, was wirklich ankommt, nicht aus
|
/// was gerade ankommt, und einschliesslich dessen, was die Füllart damit
|
||||||
/// den Angaben der Datei.
|
/// macht. Jede eigene Rechnung ist daneben eine Nachbildung, die
|
||||||
|
/// irgendwann abweicht.
|
||||||
///
|
///
|
||||||
/// Ohne Maße die ganze Fläche: dann verhält es sich wie bisher, statt
|
/// **Dann die eigene Rechnung.** Solange die Ebene noch nichts zeigt —
|
||||||
|
/// und bei VLC, das keine hat — bleibt das Seitenverhältnis aus den
|
||||||
|
/// Messwerten. Es wird auf `videoFrame` angewandt, nicht auf die
|
||||||
|
/// Fenstergrösse: die beiden unterscheiden sich um den Einzug der
|
||||||
|
/// Fensterleiste, und **das** war der Fehler, wegen dem ein Klick in den
|
||||||
|
/// schwarzen Rand bisher angehalten hat statt einzublenden.
|
||||||
|
///
|
||||||
|
/// Ohne beides die ganze Fläche: dann verhält es sich wie früher, statt
|
||||||
/// aus Unkenntnis überall das Falsche zu tun.
|
/// aus Unkenntnis überall das Falsche zu tun.
|
||||||
private var videoRect: CGRect {
|
private var videoRect: CGRect {
|
||||||
let ganzes = CGRect(origin: .zero, size: playerSize)
|
let ganzes = CGRect(origin: .zero, size: playerSize)
|
||||||
|
let fläche = videoFrame.isEmpty ? ganzes : videoFrame
|
||||||
|
guard fläche.width > 0, fläche.height > 0 else { return ganzes }
|
||||||
|
|
||||||
|
// Die Ebene liegt in der Bildfläche; ihre Angabe ist relativ zu deren
|
||||||
|
// Ecke und muss dorthin verschoben werden.
|
||||||
|
let ausDerEbene = model.surface.videoRect
|
||||||
|
if ausDerEbene.width > 1, ausDerEbene.height > 1 {
|
||||||
|
return ausDerEbene.offsetBy(dx: fläche.minX, dy: fläche.minY)
|
||||||
|
}
|
||||||
|
|
||||||
guard let breite = model.statistics?.width,
|
guard let breite = model.statistics?.width,
|
||||||
let höhe = model.statistics?.height,
|
let höhe = model.statistics?.height,
|
||||||
breite > 0, höhe > 0,
|
breite > 0, höhe > 0
|
||||||
playerSize.width > 0, playerSize.height > 0
|
|
||||||
else { return ganzes }
|
else { return ganzes }
|
||||||
|
|
||||||
let verhältnisBild = CGFloat(breite) / CGFloat(höhe)
|
let verhältnisBild = CGFloat(breite) / CGFloat(höhe)
|
||||||
let verhältnisFläche = playerSize.width / playerSize.height
|
let verhältnisFläche = fläche.width / fläche.height
|
||||||
|
|
||||||
if verhältnisBild > verhältnisFläche {
|
if verhältnisBild > verhältnisFläche {
|
||||||
// Breiter als das Fenster: Balken oben und unten.
|
// Breiter als die Fläche: Balken oben und unten.
|
||||||
let sichtbar = playerSize.width / verhältnisBild
|
let sichtbar = fläche.width / verhältnisBild
|
||||||
return CGRect(
|
return CGRect(
|
||||||
x: 0,
|
x: fläche.minX,
|
||||||
y: (playerSize.height - sichtbar) / 2,
|
y: fläche.minY + (fläche.height - sichtbar) / 2,
|
||||||
width: playerSize.width,
|
width: fläche.width,
|
||||||
height: sichtbar
|
height: sichtbar
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Höher als das Fenster: Balken links und rechts.
|
// Höher als die Fläche: Balken links und rechts.
|
||||||
let sichtbar = playerSize.height * verhältnisBild
|
let sichtbar = fläche.height * verhältnisBild
|
||||||
return CGRect(
|
return CGRect(
|
||||||
x: (playerSize.width - sichtbar) / 2,
|
x: fläche.minX + (fläche.width - sichtbar) / 2,
|
||||||
y: 0,
|
y: fläche.minY,
|
||||||
width: sichtbar,
|
width: sichtbar,
|
||||||
height: playerSize.height
|
height: fläche.height
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import FlixyPlayback
|
|||||||
|
|
||||||
/// Die Qualitätswahl im Player, samt dem, was gerade tatsächlich läuft.
|
/// Die Qualitätswahl im Player, samt dem, was gerade tatsächlich läuft.
|
||||||
///
|
///
|
||||||
/// Ein Blatt statt eines Dialogs: die Leiter hat je nach Quelle bis zu
|
/// Ein Sheet statt eines Dialogs: die Leiter hat je nach Quelle bis zu
|
||||||
/// dreizehn Stufen, und `confirmationDialog` zeigt nur eine Liste von Knöpfen
|
/// dreizehn Stufen, und `confirmationDialog` zeigt nur eine Liste von Knöpfen
|
||||||
/// ohne Ordnung. Hier stehen sie nach Auflösung gruppiert — und ganz oben,
|
/// ohne Ordnung. Hier stehen sie nach Auflösung gruppiert — und ganz oben,
|
||||||
/// was gerade über die Leitung kommt.
|
/// was gerade über die Leitung kommt.
|
||||||
@@ -15,7 +15,7 @@ struct QualitySheet: View {
|
|||||||
/// **Als Fragezeichen**, nicht als Behauptung.
|
/// **Als Fragezeichen**, nicht als Behauptung.
|
||||||
///
|
///
|
||||||
/// `@Environment(PlaybackSettings.self)` ohne Optional hält die Anwendung
|
/// `@Environment(PlaybackSettings.self)` ohne Optional hält die Anwendung
|
||||||
/// an, wenn der Gegenstand nicht im Baum steht — und ein Blatt hängt an
|
/// an, wenn der Gegenstand nicht im Baum steht — und ein Sheet hängt an
|
||||||
/// einem eigenen Baum. Genau daran ist der Player eingefroren, und zu
|
/// einem eigenen Baum. Genau daran ist der Player eingefroren, und zu
|
||||||
/// sehen war nichts davon. Der Player reicht die Einstellungen jetzt
|
/// sehen war nichts davon. Der Player reicht die Einstellungen jetzt
|
||||||
/// ausdrücklich herein; das Fragezeichen sorgt dafür, dass ein Vergessen
|
/// ausdrücklich herein; das Fragezeichen sorgt dafür, dass ein Vergessen
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import FlixyPlayback
|
|||||||
/// Auswahl einer Spur — Ton **oder** Untertitel, nicht beides.
|
/// Auswahl einer Spur — Ton **oder** Untertitel, nicht beides.
|
||||||
///
|
///
|
||||||
/// Getrennt, weil es zwei verschiedene Fragen sind: die Tonspur wechselt man
|
/// Getrennt, weil es zwei verschiedene Fragen sind: die Tonspur wechselt man
|
||||||
/// einmal am Anfang, die Untertitel mitten im Film. Beide in einem Blatt
|
/// einmal am Anfang, die Untertitel mitten im Film. Beide in einem Sheet
|
||||||
/// bedeutete jedes Mal Scrollen an der anderen Liste vorbei.
|
/// bedeutete jedes Mal Scrollen an der anderen Liste vorbei.
|
||||||
struct TrackPickerSheet: View {
|
struct TrackPickerSheet: View {
|
||||||
enum Kind: Identifiable {
|
enum Kind: Identifiable {
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ struct VideoSurface: View {
|
|||||||
final class PlayerLayerView: UIView {
|
final class PlayerLayerView: UIView {
|
||||||
override static var layerClass: AnyClass { AVPlayerLayer.self }
|
override static var layerClass: AnyClass { AVPlayerLayer.self }
|
||||||
var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
|
var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
|
||||||
|
|
||||||
|
/// Die Fläche, auf der wirklich Bild steht — ohne die schwarzen Ränder.
|
||||||
|
///
|
||||||
|
/// Vom Player ausgerechnet, aus dem, was gerade ankommt. Vor dem ersten
|
||||||
|
/// Bild ist sie leer; wer sie benutzt, muss das prüfen.
|
||||||
|
var videoRect: CGRect { playerLayer.videoRect }
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
/// Auf dem Mac gibt es kein `layerClass`: eine `NSView` bekommt ihre Ebene
|
/// Auf dem Mac gibt es kein `layerClass`: eine `NSView` bekommt ihre Ebene
|
||||||
@@ -87,6 +93,9 @@ final class PlayerLayerView: UIView {
|
|||||||
final class PlayerLayerView: NSView {
|
final class PlayerLayerView: NSView {
|
||||||
let playerLayer = AVPlayerLayer()
|
let playerLayer = AVPlayerLayer()
|
||||||
|
|
||||||
|
/// Siehe die UIKit-Fassung oben.
|
||||||
|
var videoRect: CGRect { playerLayer.videoRect }
|
||||||
|
|
||||||
override init(frame: NSRect) {
|
override init(frame: NSRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
wantsLayer = true
|
wantsLayer = true
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ struct JellyfinSignInView: View {
|
|||||||
model = fresh
|
model = fresh
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
// Wird das Blatt weggewischt, läuft sonst die Abfrageschleife
|
// Wird das Sheet weggewischt, läuft sonst die Abfrageschleife
|
||||||
// noch minutenlang weiter — unsichtbar und für nichts.
|
// noch minutenlang weiter — unsichtbar und für nichts.
|
||||||
model?.cancel()
|
model?.cancel()
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ struct JellyfinSignInView: View {
|
|||||||
.submitLabel(.go)
|
.submitLabel(.go)
|
||||||
.focused($focusedField, equals: .address)
|
.focused($focusedField, equals: .address)
|
||||||
.onSubmit { Task { await model.findServer() } }
|
.onSubmit { Task { await model.findServer() } }
|
||||||
// Hier statt beim Öffnen des Blattes: so bekommt das Feld den
|
// Hier statt beim Öffnen des Sheets: so bekommt das Feld den
|
||||||
// Fokus auch dann, wenn man nach einem Vertipper zurückkommt.
|
// Fokus auch dann, wenn man nach einem Vertipper zurückkommt.
|
||||||
.task { focusedField = .address }
|
.task { focusedField = .address }
|
||||||
.padding(FlixySpacing.regular)
|
.padding(FlixySpacing.regular)
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ extension View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ein Blatt, das die halbe Höhe einnimmt.
|
/// Ein Sheet, das die halbe Höhe einnimmt.
|
||||||
///
|
///
|
||||||
/// Auf dem Mac ist ein Blatt ein Fenster: dort zählt keine Rastung,
|
/// Auf dem Mac ist ein Sheet ein Fenster: dort zählt keine Rastung,
|
||||||
/// sondern eine vernünftige Mindestgröße.
|
/// sondern eine vernünftige Mindestgröße.
|
||||||
public func flixyMediumSheet() -> some View {
|
public func flixyMediumSheet() -> some View {
|
||||||
// **Zur Laufzeit, nicht beim Übersetzen.** Der `#else`-Zweig für
|
// **Zur Laufzeit, nicht beim Übersetzen.** Der `#else`-Zweig für
|
||||||
// macOS griff nie: auf dem Mac läuft der iOS-Bau, und damit bekam ein
|
// macOS griff nie: auf dem Mac läuft der iOS-Bau, und damit bekam ein
|
||||||
// grosses Fenster eine Blattkante in iPhone-Höhe — ein schmaler
|
// grosses Fenster eine Sheet-Kante in iPhone-Höhe — ein schmaler
|
||||||
// Streifen, der unten im Nichts klebt.
|
// Streifen, der unten im Nichts klebt.
|
||||||
presentationDetents(FlixyPlatform.isMac ? [.large] : [.medium])
|
presentationDetents(FlixyPlatform.isMac ? [.large] : [.medium])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ extension JellyfinBackend: LiveTVBackend {
|
|||||||
channelID: channelID,
|
channelID: channelID,
|
||||||
title: item.name ?? "—",
|
title: item.name ?? "—",
|
||||||
// `EpisodeTitle` und nicht `Name`: hier stand der Reihentitel ein
|
// `EpisodeTitle` und nicht `Name`: hier stand der Reihentitel ein
|
||||||
// zweites Mal, und im Blatt hätte derselbe Satz zweimal
|
// zweites Mal, und im Sheet hätte derselbe Satz zweimal
|
||||||
// untereinander gestanden.
|
// untereinander gestanden.
|
||||||
subtitle: item.episodeTitle,
|
subtitle: item.episodeTitle,
|
||||||
summary: item.overview,
|
summary: item.overview,
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ public struct TranscodeSettings: Sendable, Equatable {
|
|||||||
/// angegebene Bitrate bleibt dabei die **oberste** Sprosse; was darunter
|
/// angegebene Bitrate bleibt dabei die **oberste** Sprosse; was darunter
|
||||||
/// liegt, erfindet der Server.
|
/// liegt, erfindet der Server.
|
||||||
///
|
///
|
||||||
/// Nur bei `automatic` gesetzt. Wer im Blatt ausdrücklich „720p · 3 Mbps"
|
/// Nur bei `automatic` gesetzt. Wer im Sheet ausdrücklich „720p · 3 Mbps"
|
||||||
/// antippt, soll das bekommen und nicht „höchstens 3, meistens weniger"
|
/// antippt, soll das bekommen und nicht „höchstens 3, meistens weniger"
|
||||||
/// unter unverändertem Etikett.
|
/// unter unverändertem Etikett.
|
||||||
public let prefersAdaptive: Bool
|
public let prefersAdaptive: Bool
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ public actor PlexAuthenticator {
|
|||||||
do {
|
do {
|
||||||
try await clock.sleep(for: pollInterval)
|
try await clock.sleep(for: pollInterval)
|
||||||
} catch {
|
} catch {
|
||||||
// Abbruch durch die Oberfläche, etwa weil das Anmeldeblatt
|
// Abbruch durch die Oberfläche, etwa weil das Anmelde-Sheet
|
||||||
// geschlossen wurde. Kein Fehlerfall.
|
// geschlossen wurde. Kein Fehlerfall.
|
||||||
throw .transport(description: "Anmeldung abgebrochen")
|
throw .transport(description: "Anmeldung abgebrochen")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,14 +77,14 @@ struct LibraryQueryTests {
|
|||||||
|
|
||||||
@Suite("Seitenweises Nachladen")
|
@Suite("Seitenweises Nachladen")
|
||||||
struct PlexPageTests {
|
struct PlexPageTests {
|
||||||
@Test("Eine volle Seite kündigt Nachschub an")
|
@Test("Eine volle Seite kündigt weitere Titel an")
|
||||||
func fullPageHasMore() {
|
func fullPageHasMore() {
|
||||||
let page = PlexPage(items: Array(0..<60), offset: 0, totalSize: 250)
|
let page = PlexPage(items: Array(0..<60), offset: 0, totalSize: 250)
|
||||||
#expect(page.hasMore)
|
#expect(page.hasMore)
|
||||||
#expect(page.nextOffset == 60)
|
#expect(page.nextOffset == 60)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Die letzte Seite kündigt keinen Nachschub an")
|
@Test("Die letzte Seite kündigt keine weiteren Titel an")
|
||||||
func lastPageHasNoMore() {
|
func lastPageHasNoMore() {
|
||||||
// Genau die Bedingung, die sonst zu einer Endlosschleife führt: die
|
// Genau die Bedingung, die sonst zu einer Endlosschleife führt: die
|
||||||
// Ansicht lädt nach, bekommt nichts, lädt wieder nach.
|
// Ansicht lädt nach, bekommt nichts, lädt wieder nach.
|
||||||
|
|||||||
Reference in New Issue
Block a user