Ein Vollbildknopf im Player — wenn der Mac ihn annimmt

Fuer eine „fuer iPad angepasste\" App gibt es dafuer keine oeffentliche
Schnittstelle. UIKit kennt `isFullScreen` nur lesend und nur fuer Catalyst;
einen Umschalter gibt es nirgends. Das Fenster gehoert AppKit, und an
AppKit kommt diese App nicht heran.

Was bleibt, ist die Antwortkette: `toggleFullScreen:` ist dieselbe Aktion,
die der gruene Knopf und Strg-Cmd-F ausloesen, und UIKit reicht
unbeantwortete Aktionen an das tragende AppKit-Fenster weiter. Ob es das
tut, ist nirgends zugesagt.

Deshalb wird vorher gefragt statt hinterher gehofft: `target(forAction:)`
sagt, ob die Aktion jemand entgegennimmt, **ohne sie auszuloesen**. Erst
wenn es einen Empfaenger gibt, erscheint der Knopf. Ein Knopf, der nichts
tut, ist schlimmer als keiner — er laesst den Nutzer an sich zweifeln statt
an der Plattform.

Das Symbol zaehlt den Zustand selbst mit, weil er sich nicht abfragen
laesst. Wer mit dem gruenen Knopf umschaltet, bringt es aus dem Tritt; das
Umschalten funktioniert trotzdem, es zeigt nur kurz das falsche Bild.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-29 06:31:46 +02:00
co-authored by Claude Opus 5
parent 4323c47db6
commit 1ff65e823a
3 changed files with 102 additions and 0 deletions
+34
View File
@@ -3538,6 +3538,23 @@
} }
} }
}, },
"player.exitFullScreen": {
"extractionState": "manual",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Vollbild beenden"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Exit full screen"
}
}
}
},
"player.forgetRememberedTrack": { "player.forgetRememberedTrack": {
"extractionState": "manual", "extractionState": "manual",
"localizations": { "localizations": {
@@ -3571,6 +3588,23 @@
} }
} }
}, },
"player.fullScreen": {
"extractionState": "manual",
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Vollbild"
}
},
"en": {
"stringUnit": {
"state": "translated",
"value": "Enter full screen"
}
}
}
},
"player.jellyfinPending": { "player.jellyfinPending": {
"extractionState": "stale", "extractionState": "stale",
"localizations": { "localizations": {
+38
View File
@@ -0,0 +1,38 @@
import UIKit
/// Das Fenster auf dem Mac ins Vollbild und zurück.
///
/// **Und warum das umständlich ist.** Für eine „für iPad angepasste" App gibt
/// es dafür keine öffentliche Schnittstelle. UIKit kennt `isFullScreen` nur
/// lesend und nur für Mac Catalyst; einen Umschalter gibt es nirgends. Das
/// Fenster gehört AppKit, und an AppKit kommt diese App nicht heran.
///
/// Was bleibt, ist die **Antwortkette**: `toggleFullScreen:` ist die Aktion,
/// die auch der grüne Knopf und ⌃⌘F auslösen, und UIKit reicht unbeantwortete
/// Aktionen an das tragende AppKit-Fenster weiter. Ob es das tut, ist
/// nirgends zugesagt.
///
/// **Deshalb wird vorher gefragt, nicht hinterher gehofft.**
/// `target(forAction:)` sagt, ob die Aktion jemand entgegennimmt — **ohne sie
/// auszulösen**. Erst wenn es einen Empfänger gibt, erscheint der Knopf. Ein
/// Knopf, der nichts tut, ist schlimmer als keiner: er lässt den Nutzer an
/// sich zweifeln statt an der Plattform.
@MainActor
enum MacFullScreen {
private static let action = NSSelectorFromString("toggleFullScreen:")
/// Ob es hier überhaupt jemanden gibt, der umschalten kann.
///
/// Bei jedem Zeichnen gefragt statt einmal gemerkt: die Antwortkette
/// hängt am gerade aktiven Fenster, und ein einmal ermittelter Wert wäre
/// falsch, sobald es keins gibt — etwa vor dem ersten Erscheinen.
static var isAvailable: Bool {
UIApplication.shared.target(forAction: action, withSender: nil) != nil
}
/// Schaltet um. Gibt zurück, ob jemand zugehört hat.
@discardableResult
static func toggle() -> Bool {
UIApplication.shared.sendAction(action, to: nil, from: nil, for: nil)
}
}
+30
View File
@@ -45,6 +45,15 @@ struct PlayerView: View {
@State private var becameActive: ContinuousClock.Instant? @State private var becameActive: ContinuousClock.Instant?
/// Wo der Zeiger zuletzt war — um Bewegung von Neuzeichnen zu trennen. /// Wo der Zeiger zuletzt war — um Bewegung von Neuzeichnen zu trennen.
@State private var lastPointerLocation: CGPoint = .zero @State private var lastPointerLocation: CGPoint = .zero
/// Ob das Fenster gerade im Vollbild ist — soweit wir es wissen können.
///
/// **Selbst mitgezählt, nicht abgefragt.** `UIWindowScene.isFullScreen`
/// gibt es für eine „für iPad angepasste" App nicht. Wer mit dem grünen
/// Knopf oder ⌃⌘F umschaltet, bringt das Symbol also aus dem Tritt — das
/// Umschalten selbst funktioniert trotzdem, es zeigt nur kurz das falsche
/// Bild. Ein falsches Symbol ist der kleinere Preis gegenüber gar keinem
/// Knopf.
@State private var isFullScreen = false
/// 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 }
@@ -350,6 +359,27 @@ struct PlayerView: View {
Spacer() Spacer()
// **Vollbild — nur auf dem Mac, und nur wenn es dort geht.**
//
// Ob die Aktion jemand entgegennimmt, steht nicht fest; siehe
// `MacFullScreen`. Gefragt wird vorher, damit kein Knopf dasteht,
// der nichts tut — das lässt den Nutzer an sich zweifeln statt an
// der Plattform.
if isMac, MacFullScreen.isAvailable {
Button {
if MacFullScreen.toggle() { isFullScreen.toggle() }
revealControls()
} label: {
Image(systemName: isFullScreen
? "arrow.down.right.and.arrow.up.left"
: "arrow.up.left.and.arrow.down.right")
.font(.callout.weight(.semibold))
.padding(FlixySpacing.snug)
}
.accessibilityLabel(Text(isFullScreen ? "player.exitFullScreen" : "player.fullScreen"))
.keyboardShortcut("f", modifiers: [.command, .control])
}
// 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 Sheet bedeutete jedes Mal Scrollen an der // Film. Ein gemeinsames Sheet bedeutete jedes Mal Scrollen an der