Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tvOS - Show and interact with the video menu #1066

Merged
merged 6 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
LePips marked this conversation as resolved.
Show resolved Hide resolved

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) {
LePips marked this conversation as resolved.
Show resolved Hide resolved
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 {
LePips marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
}
}
Loading