tvOS - Show and interact with the video menu (#1066)

* Made the menu accessable and fixed visual padding bug

* Moved away from .onExitCommand etc

* Minor refactoring

* wip

* Update Overlay.swift

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Mats Eikeland Mollestad 2024-09-05 21:44:00 +02:00 committed by GitHub
parent 5913c308a6
commit 081a316843
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 183 additions and 33 deletions

View File

@ -43,3 +43,14 @@ struct SupportedOrientationsPreferenceKey: PreferenceKey {
}
}
#endif
#if os(tvOS)
struct PressCommandsPreferenceKey: PreferenceKey {
static var defaultValue: [PressCommandAction] = []
static func reduce(value: inout [PressCommandAction], nextValue: () -> [PressCommandAction]) {
value.append(contentsOf: nextValue())
}
}
#endif

View File

@ -0,0 +1,34 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//
import Foundation
import SwiftUI
public struct PressCommandAction {
let title: String
let press: UIPress.PressType
let action: () -> Void
public init(
title: String,
press: UIPress.PressType,
action: @escaping () -> Void
) {
self.title = title
self.press = press
self.action = action
}
}
extension PressCommandAction: Equatable {
public static func == (lhs: PressCommandAction, rhs: PressCommandAction) -> Bool {
lhs.press == rhs.press
}
}

View File

@ -0,0 +1,37 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//
import Foundation
@resultBuilder
public enum PressCommandsBuilder {
public static func buildBlock(_ components: [PressCommandAction]...) -> [PressCommandAction] {
components.flatMap { $0 }
}
public static func buildExpression(_ expression: PressCommandAction) -> [PressCommandAction] {
[expression]
}
public static func buildOptional(_ component: [PressCommandAction]?) -> [PressCommandAction] {
component ?? []
}
public static func buildEither(first component: [PressCommandAction]) -> [PressCommandAction] {
component
}
public static func buildEither(second component: [PressCommandAction]) -> [PressCommandAction] {
component
}
public static func buildArray(_ components: [[PressCommandAction]]) -> [PressCommandAction] {
components.flatMap { $0 }
}
}

View File

@ -27,6 +27,10 @@ public class UIPreferencesHostingController: UIHostingController<AnyView> {
.onPreferenceChange(SupportedOrientationsPreferenceKey.self) {
box.value?._orientations = $0
}
#elseif os(tvOS)
.onPreferenceChange(PressCommandsPreferenceKey.self) {
box.value?._pressCommandActions = $0
}
#endif
)
@ -112,6 +116,30 @@ public class UIPreferencesHostingController: UIHostingController<AnyView> {
}
#endif
#if os(tvOS)
override public func viewDidLoad() {
super.viewDidLoad()
let gesture = UITapGestureRecognizer(target: self, action: #selector(ignorePress))
gesture.allowedPressTypes = [NSNumber(value: UIPress.PressType.menu.rawValue)]
view.addGestureRecognizer(gesture)
}
@objc
func ignorePress() {}
private var _pressCommandActions: [PressCommandAction] = []
override public func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
guard let buttonPress = presses.first?.type else { return }
guard let action = _pressCommandActions
.first(where: { $0.press == buttonPress }) else { return }
action.action()
}
#endif
}
// TODO: remove after iOS 15 support removed

View File

@ -27,4 +27,10 @@ public extension View {
preference(key: SupportedOrientationsPreferenceKey.self, value: supportedOrientations)
}
#endif
#if os(tvOS)
func pressCommands(@PressCommandsBuilder _ commands: @escaping () -> [PressCommandAction]) -> some View {
preference(key: PressCommandsPreferenceKey.self, value: commands())
}
#endif
}

View File

@ -76,6 +76,7 @@ final class VideoPlayerCoordinator: NavigationCoordinatable {
PreferencesView {
VideoPlayer(manager: self.videoPlayerManager)
}
.ignoresSafeArea()
} else {
NativeVideoPlayer(manager: self.videoPlayerManager)
}

View File

@ -6,6 +6,7 @@
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//
import PreferencesView
import SwiftUI
import VLCUI
@ -21,6 +22,8 @@ extension VideoPlayer {
private var proxy: VLCVideoPlayer.Proxy
@EnvironmentObject
private var router: VideoPlayerCoordinator.Router
@EnvironmentObject
private var videoPlayerManager: VideoPlayerManager
@State
private var confirmCloseWorkItem: DispatchWorkItem?
@ -50,6 +53,11 @@ extension VideoPlayer {
.animation(.linear(duration: 0.1), value: currentOverlayType)
.environment(\.currentOverlayType, $currentOverlayType)
.environmentObject(overlayTimer)
.onChange(of: isPresentingOverlay) {
if !isPresentingOverlay {
currentOverlayType = .main
}
}
.onChange(of: currentOverlayType) { _, newValue in
if [.smallMenu, .chapters].contains(newValue) {
overlayTimer.pause()
@ -64,39 +72,64 @@ extension VideoPlayer {
isPresentingOverlay = false
}
}
// .onSelectPressed {
// currentOverlayType = .main
// isPresentingOverlay = true
// overlayTimer.start(5)
// }
// .onMenuPressed {
//
// overlayTimer.start(5)
// confirmCloseWorkItem?.cancel()
//
// if isPresentingOverlay && currentOverlayType == .confirmClose {
// proxy.stop()
// router.dismissCoordinator()
// } else if isPresentingOverlay && currentOverlayType == .smallMenu {
// currentOverlayType = .main
// } else {
// withAnimation {
// currentOverlayType = .confirmClose
// isPresentingOverlay = true
// }
//
// let task = DispatchWorkItem {
// withAnimation {
// isPresentingOverlay = false
// overlayTimer.stop()
// }
// }
//
// confirmCloseWorkItem = task
//
// DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: task)
// }
// }
.pressCommands {
PressCommandAction(title: L10n.back, press: .menu, action: menuPress)
PressCommandAction(title: L10n.playAndPause, press: .playPause) {
if videoPlayerManager.state == .playing {
videoPlayerManager.proxy.pause()
withAnimation(.linear(duration: 0.3)) {
isPresentingOverlay = true
}
} else if videoPlayerManager.state == .paused {
videoPlayerManager.proxy.play()
withAnimation(.linear(duration: 0.3)) {
isPresentingOverlay = false
}
}
}
PressCommandAction(title: L10n.pressDownForMenu, press: .upArrow, action: arrowPress)
PressCommandAction(title: L10n.pressDownForMenu, press: .downArrow, action: arrowPress)
PressCommandAction(title: L10n.pressDownForMenu, press: .leftArrow, action: arrowPress)
PressCommandAction(title: L10n.pressDownForMenu, press: .rightArrow, action: arrowPress)
PressCommandAction(title: L10n.pressDownForMenu, press: .select, action: arrowPress)
}
}
func arrowPress() {
if isPresentingOverlay { return }
currentOverlayType = .main
overlayTimer.start(5)
withAnimation {
isPresentingOverlay = true
}
}
func menuPress() {
overlayTimer.start(5)
confirmCloseWorkItem?.cancel()
if isPresentingOverlay && currentOverlayType == .confirmClose {
proxy.stop()
router.dismissCoordinator()
} else if isPresentingOverlay && currentOverlayType == .smallMenu {
currentOverlayType = .main
} else {
withAnimation {
currentOverlayType = .confirmClose
isPresentingOverlay = true
}
let task = DispatchWorkItem {
withAnimation {
isPresentingOverlay = false
overlayTimer.stop()
}
}
confirmCloseWorkItem = task
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: task)
}
}
}
}