From 97e9889682de19b77c835bc2acd135afc6dacfef Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Wed, 29 Jun 2022 23:43:39 +0200 Subject: [PATCH] Minor tvOS controls and remote improvements --- Model/Player/PlayerControlsModel.swift | 5 ++ Model/Player/PlayerModel.swift | 9 ++- Shared/Player/Controls/PlayerControls.swift | 2 +- Shared/Player/Controls/TVControls.swift | 77 +++++++++++++++++++++ Shared/Player/VideoPlayerView.swift | 63 +++++++++++------ Yattee.xcodeproj/project.pbxproj | 6 +- 6 files changed, 137 insertions(+), 25 deletions(-) create mode 100644 Shared/Player/Controls/TVControls.swift diff --git a/Model/Player/PlayerControlsModel.swift b/Model/Player/PlayerControlsModel.swift index 94df7f44..34ffd1b0 100644 --- a/Model/Player/PlayerControlsModel.swift +++ b/Model/Player/PlayerControlsModel.swift @@ -1,3 +1,4 @@ +import Combine import CoreMedia import Foundation import SwiftUI @@ -9,6 +10,10 @@ final class PlayerControlsModel: ObservableObject { @Published var presentingControlsOverlay = false { didSet { handleOverlayPresentationChange() } } @Published var timer: Timer? + #if os(tvOS) + var reporter = PassthroughSubject() + #endif + var player: PlayerModel! init( diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 341b4e82..8a625d17 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -372,7 +372,14 @@ final class PlayerModel: ObservableObject { #endif DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in - self?.backend.setNeedsDrawing(self?.presentingPlayer ?? false) + guard let self = self else { return } + self.backend.setNeedsDrawing(self.presentingPlayer) + + #if os(tvOS) + if self.presentingPlayer { + self.controls.show() + } + #endif } controls.hide() diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index 0180c2bd..c996a1a8 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -70,7 +70,7 @@ struct PlayerControls: View { .zIndex(1) } #if os(tvOS) - .offset(y: -100) + .offset(y: -50) #endif .frame(maxWidth: 500) .padding(.bottom, 2) diff --git a/Shared/Player/Controls/TVControls.swift b/Shared/Player/Controls/TVControls.swift new file mode 100644 index 00000000..d98f1e41 --- /dev/null +++ b/Shared/Player/Controls/TVControls.swift @@ -0,0 +1,77 @@ +import Combine +import SwiftUI + +struct TVControls: UIViewRepresentable { + var model: PlayerControlsModel! + var player: PlayerModel! + var thumbnails: ThumbnailsModel! + + @State private var direction = "" + @State private var controlsArea = UIView() + + func makeUIView(context: Context) -> UIView { + let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(sender:))) + + let leftSwipe = UISwipeGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleSwipe(sender:))) + leftSwipe.direction = .left + + let rightSwipe = UISwipeGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleSwipe(sender:))) + rightSwipe.direction = .right + + let upSwipe = UISwipeGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleSwipe(sender:))) + upSwipe.direction = .up + + let downSwipe = UISwipeGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleSwipe(sender:))) + downSwipe.direction = .down + + controlsArea.addGestureRecognizer(tapGesture) + controlsArea.addGestureRecognizer(leftSwipe) + controlsArea.addGestureRecognizer(rightSwipe) + controlsArea.addGestureRecognizer(upSwipe) + controlsArea.addGestureRecognizer(downSwipe) + + let controls = UIHostingController(rootView: PlayerControls(player: player, thumbnails: thumbnails)) + controls.view.frame = .init( + origin: .zero, + size: .init(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height) + ) + + controlsArea.addSubview(controls.view) + + return controlsArea + } + + func updateUIView(_: UIView, context _: Context) {} + + func makeCoordinator() -> TVControls.Coordinator { + Coordinator(controlsArea, model: model) + } + + final class Coordinator: NSObject { + private let view: UIView + private let model: PlayerControlsModel + + init(_ view: UIView, model: PlayerControlsModel) { + self.view = view + self.model = model + super.init() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func handleTap(sender: UITapGestureRecognizer) { + let location = sender.location(in: view) + model.reporter.send("tap \(location)") + print("tap \(location)") + } + + @objc func handleSwipe(sender: UISwipeGestureRecognizer) { + let location = sender.location(in: view) + model.reporter.send("swipe \(location)") + print("swipe \(location)") + } + } +} diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 744ad72d..8255d3f0 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -138,29 +138,39 @@ struct VideoPlayerView: View { Group { ZStack(alignment: .bottomLeading) { #if os(tvOS) - playerView - .ignoresSafeArea(.all, edges: .all) - .onMoveCommand { direction in - if direction == .up || direction == .down { - playerControls.show() - } + ZStack { + playerView - playerControls.resetTimer() - - guard !playerControls.presentingControls else { - return - } - - if direction == .left { - player.backend.seek(relative: .secondsInDefaultTimescale(-10)) - } - if direction == .right { - player.backend.seek(relative: .secondsInDefaultTimescale(10)) - } + tvControls + } + .ignoresSafeArea(.all, edges: .all) + .onMoveCommand { direction in + if direction == .up || direction == .down { + playerControls.show() } - .onPlayPauseCommand { - player.togglePlay() + + playerControls.resetTimer() + + guard !playerControls.presentingControls else { return } + + if direction == .left { + player.backend.seek(relative: .secondsInDefaultTimescale(-10)) } + if direction == .right { + player.backend.seek(relative: .secondsInDefaultTimescale(10)) + } + } + .onPlayPauseCommand { + player.togglePlay() + } + + .onExitCommand { + if playerControls.presentingControls { + playerControls.hide() + } else { + player.hide() + } + } #else GeometryReader { geometry in VStack(spacing: 0) { @@ -308,9 +318,8 @@ struct VideoPlayerView: View { #if !os(tvOS) PlayerGestures() + PlayerControls(player: player, thumbnails: thumbnails) #endif - - PlayerControls(player: player, thumbnails: thumbnails) } } @@ -502,6 +511,16 @@ struct VideoPlayerView: View { } } #endif + + #if os(tvOS) + var tvControls: some View { + TVControls(model: playerControls, player: player, thumbnails: thumbnails) + .onReceive(playerControls.reporter) { _ in + playerControls.show() + playerControls.resetTimer() + } + } + #endif } struct VideoPlayerView_Previews: PreviewProvider { diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index cbb9f08e..1fec1a84 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -343,6 +343,7 @@ 3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; }; 3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */; }; 3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */; }; + 37648B69286CF5F1003D330B /* TVControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37648B68286CF5F1003D330B /* TVControls.swift */; }; 376527BB285F60F700102284 /* PlayerTimeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376527BA285F60F700102284 /* PlayerTimeModel.swift */; }; 376527BC285F60F700102284 /* PlayerTimeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376527BA285F60F700102284 /* PlayerTimeModel.swift */; }; 376527BD285F60F700102284 /* PlayerTimeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376527BA285F60F700102284 /* PlayerTimeModel.swift */; }; @@ -969,6 +970,7 @@ 375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsModel.swift; sourceTree = ""; }; 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = ""; }; 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarRecents.swift; sourceTree = ""; }; + 37648B68286CF5F1003D330B /* TVControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVControls.swift; sourceTree = ""; }; 376527BA285F60F700102284 /* PlayerTimeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTimeModel.swift; sourceTree = ""; }; 376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = ""; }; 376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = ""; }; @@ -1363,9 +1365,10 @@ isa = PBXGroup; children = ( 3756C2A428610F6D00E4B059 /* OSD */, - 37030FFE27B04DCC00ECDDAA /* PlayerControls.swift */, 37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */, 37F13B61285E43C000B137E4 /* ControlsOverlay.swift */, + 37030FFE27B04DCC00ECDDAA /* PlayerControls.swift */, + 37648B68286CF5F1003D330B /* TVControls.swift */, ); path = Controls; sourceTree = ""; @@ -3016,6 +3019,7 @@ 37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, 376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */, 37CEE4BF2677B670005A1EFE /* SingleAssetStream.swift in Sources */, + 37648B69286CF5F1003D330B /* TVControls.swift in Sources */, 374C053D2724614F009BDDBE /* PlayerTVMenu.swift in Sources */, 37BE0BD426A1D47D0092E2DB /* AppleAVPlayerView.swift in Sources */, 37977585268922F600DD52A8 /* InvidiousAPI.swift in Sources */,