diff --git a/Flixy/Resources/Localizable.xcstrings b/Flixy/Resources/Localizable.xcstrings index f172568..00a30ac 100644 --- a/Flixy/Resources/Localizable.xcstrings +++ b/Flixy/Resources/Localizable.xcstrings @@ -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": { "extractionState": "manual", "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": { "extractionState": "stale", "localizations": { diff --git a/Flixy/Views/Player/MacFullScreen.swift b/Flixy/Views/Player/MacFullScreen.swift new file mode 100644 index 0000000..7506af7 --- /dev/null +++ b/Flixy/Views/Player/MacFullScreen.swift @@ -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) + } +} diff --git a/Flixy/Views/Player/PlayerView.swift b/Flixy/Views/Player/PlayerView.swift index 7550205..5a93b5d 100644 --- a/Flixy/Views/Player/PlayerView.swift +++ b/Flixy/Views/Player/PlayerView.swift @@ -45,6 +45,15 @@ struct PlayerView: View { @State private var becameActive: ContinuousClock.Instant? /// Wo der Zeiger zuletzt war — um Bewegung von Neuzeichnen zu trennen. @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. private var isMac: Bool { FlixyPlatform.isMac } @@ -350,6 +359,27 @@ struct PlayerView: View { 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 // Tonspur stellt man einmal am Anfang um, die Untertitel mitten im // Film. Ein gemeinsames Sheet bedeutete jedes Mal Scrollen an der