diff --git a/Flixy/Views/Player/MiniPlayer.swift b/Flixy/Views/Player/MiniPlayer.swift index 81147e0..3b40b78 100644 --- a/Flixy/Views/Player/MiniPlayer.swift +++ b/Flixy/Views/Player/MiniPlayer.swift @@ -84,7 +84,11 @@ struct MiniPlayer: View { ZStack { Color.black - VideoSurface(engine: model.engine, surface: model.surface) + VideoSurface( + engine: model.engine, + surface: model.surface, + isActive: playback.presentation == .mini + ) if model.isBuffering { ProgressView().tint(.white) diff --git a/Flixy/Views/Player/PlayerView.swift b/Flixy/Views/Player/PlayerView.swift index 49723c9..1161dc5 100644 --- a/Flixy/Views/Player/PlayerView.swift +++ b/Flixy/Views/Player/PlayerView.swift @@ -40,6 +40,8 @@ struct PlayerView: View { @State private var hintTask: Task? /// Ob die Tastatur bei diesem Bildschirm ankommt. Nur auf dem Mac gesetzt. @FocusState private var keyboardFocus: Bool + /// Wann die App zuletzt nach vorn geholt wurde. + @State private var becameActive: ContinuousClock.Instant? /// Ob Zeiger und Tastatur gelten. Einmal beim Start ermittelt. private var isMac: Bool { FlixyPlatform.isMac } /// Wann der Zeiger zuletzt die Bedienelemente geholt hat. @@ -53,7 +55,11 @@ struct PlayerView: View { // Bei „nur Ton" bleibt der Schirm dunkel — das ist der halbe // Zweck der Stufe. if model.showsVideo { - VideoSurface(engine: model.engine, surface: model.surface) + VideoSurface( + engine: model.engine, + surface: model.surface, + isActive: playback.presentation == .full + ) .ignoresSafeArea() } @@ -152,6 +158,11 @@ struct PlayerView: View { .contentShape(.rect) // Für alles außerhalb der Sprungflächen. .onTapGesture { primaryTap() } + .onReceive( + NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) + ) { _ in + becameActive = .now + } // **Tastatur und Zeiger — nur auf dem Mac.** // // Dort ist beides selbstverständlich: Leertaste hält an, die Pfeile @@ -648,6 +659,22 @@ struct PlayerView: View { toggleControls() return } + + // **Der Klick, der das Fenster nach vorn holt, zählt nicht.** + // + // Auf dem Mac pausiert ein Klick ins Bild. Wer aus einem anderen + // Fenster zurückkommt, klickt aber zuerst einmal, um dieses Fenster + // zu aktivieren — und genau dieser Klick kam ebenfalls hier an. Die + // Wiedergabe sprang dadurch von selbst wieder an, als hätte man auf + // Play gedrückt. Hat man ja auch, nur nicht absichtlich. + // + // Eine Viertelsekunde reicht: länger braucht niemand vom Aktivieren + // bis zum ersten gemeinten Klick, und kürzer erwischt den Fall nicht. + if let becameActive, ContinuousClock.now - becameActive < .milliseconds(250) { + self.becameActive = nil + return + } + model.togglePlayPause() revealControls() } diff --git a/Flixy/Views/Player/VideoSurface.swift b/Flixy/Views/Player/VideoSurface.swift index 03f01ff..660cc77 100644 --- a/Flixy/Views/Player/VideoSurface.swift +++ b/Flixy/Views/Player/VideoSurface.swift @@ -38,18 +38,32 @@ struct VideoSurface: View { /// hier ist es jetzt derselbe Weg. let surface: PlayerLayerView + /// Ob **diese** Ansicht die Bildfläche gerade haben soll. + /// + /// **Der Kern des Schwarzbilds.** Kachel und Vollbild bestehen beim + /// Wechsel kurz gleichzeitig, und beide holen sich die Fläche in ihre + /// Hülle. Eine Ansicht hat aber nur einen Vorgesetzten — wer zuletzt + /// aktualisiert, gewinnt, und das war nicht verlässlich die richtige. + /// Blieb die Fläche bei der Kachel, die gleich darauf verschwand, lief + /// im Vollbild der Ton weiter und das Bild blieb schwarz. + /// + /// Die Hülle allein genügte dagegen nicht: sie verhindert, dass das + /// Abräumen der einen der anderen etwas wegnimmt, entscheidet aber + /// nicht, wer sie bekommen **soll**. Das steht hier. + var isActive = true + var body: some View { #if canImport(FlixyVLC) if let vlcEngine = engine as? VLCPlayerEngine { - SurfaceContainer(view: vlcEngine.videoView) + SurfaceContainer(view: vlcEngine.videoView, isActive: isActive) } else if engine is AVPlayerEngine { - SurfaceContainer(view: surface) + SurfaceContainer(view: surface, isActive: isActive) } else { Color.black } #else if engine is AVPlayerEngine { - SurfaceContainer(view: surface) + SurfaceContainer(view: surface, isActive: isActive) } else { Color.black } @@ -106,33 +120,38 @@ final class PlayerLayerView: NSView { #if canImport(UIKit) private struct SurfaceContainer: UIViewRepresentable { let view: PlatformView + let isActive: Bool func makeUIView(context: Context) -> PlatformView { let host = UIView() host.backgroundColor = .black - adopt(view, into: host) + if isActive { adopt(view, into: host) } return host } func updateUIView(_ host: PlatformView, context: Context) { - guard view.superview !== host else { return } + // Wer nicht dran ist, greift auch nicht zu. Ohne diese Zeile holen + // sich beide Ansichten dieselbe Fläche, und die Reihenfolge + // entscheidet — mal richtig, mal schwarz. + guard isActive, view.superview !== host else { return } adopt(view, into: host) } } #else private struct SurfaceContainer: NSViewRepresentable { let view: PlatformView + let isActive: Bool func makeNSView(context: Context) -> PlatformView { let host = NSView() host.wantsLayer = true host.layer?.backgroundColor = NSColor.black.cgColor - adopt(view, into: host) + if isActive { adopt(view, into: host) } return host } func updateNSView(_ host: PlatformView, context: Context) { - guard view.superview !== host else { return } + guard isActive, view.superview !== host else { return } adopt(view, into: host) } }