Skip to content

Commit

Permalink
tvOS - Show and interact with the video menu (jellyfin#1066)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
MatsMoll and LePips authored Sep 5, 2024
1 parent 5913c30 commit 081a316
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 33 deletions.
11 changes: 11 additions & 0 deletions PreferencesView/Sources/PreferencesView/PreferenceKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
34 changes: 34 additions & 0 deletions PreferencesView/Sources/PreferencesView/PressCommandAction.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
37 changes: 37 additions & 0 deletions PreferencesView/Sources/PreferencesView/PressCommandBuilder.swift
Original file line number Diff line number Diff line change
@@ -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 }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions PreferencesView/Sources/PreferencesView/ViewExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions Shared/Coordinators/VideoPlayerCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ final class VideoPlayerCoordinator: NavigationCoordinatable {
PreferencesView {
VideoPlayer(manager: self.videoPlayerManager)
}
.ignoresSafeArea()
} else {
NativeVideoPlayer(manager: self.videoPlayerManager)
}
Expand Down
99 changes: 66 additions & 33 deletions Swiftfin tvOS/Views/VideoPlayer/Overlays/Overlay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//

import PreferencesView
import SwiftUI
import VLCUI

Expand All @@ -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?
Expand Down Expand Up @@ -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()
Expand All @@ -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)
}
}
}
}

0 comments on commit 081a316

Please sign in to comment.