// // 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 Defaults import PreferencesView import SwiftUI import VLCUI extension View { func videoPlayerKeyCommands( gestureStateHandler: VideoPlayer.GestureStateHandler, updateViewProxy: UpdateViewProxy ) -> some View { modifier( VideoPlayerKeyCommandsModifier( gestureStateHandler: gestureStateHandler, updateViewProxy: updateViewProxy ) ) } } struct VideoPlayerKeyCommandsModifier: ViewModifier { @Default(.VideoPlayer.jumpBackwardLength) private var jumpBackwardLength @Default(.VideoPlayer.jumpForwardLength) private var jumpForwardLength @Environment(\.aspectFilled) private var isAspectFilled @EnvironmentObject private var videoPlayerManager: VideoPlayerManager @EnvironmentObject private var videoPlayerProxy: VLCVideoPlayer.Proxy let gestureStateHandler: VideoPlayer.GestureStateHandler let updateViewProxy: UpdateViewProxy func body(content: Content) -> some View { content.keyCommands { // MARK: play/pause KeyCommandAction( title: L10n.playAndPause, input: " " ) { if videoPlayerManager.state == .playing { videoPlayerManager.proxy.pause() updateViewProxy.present(systemName: "pause.fill", title: "Pause") } else { videoPlayerManager.proxy.play() updateViewProxy.present(systemName: "play.fill", title: "Play") } } // MARK: jump forward KeyCommandAction( title: L10n.jumpForward, input: UIKeyCommand.inputRightArrow ) { if gestureStateHandler.jumpForwardKeyPressActive { gestureStateHandler.jumpForwardKeyPressAmount += 1 gestureStateHandler.jumpForwardKeyPressWorkItem?.cancel() videoPlayerProxy.jumpForward(Int(jumpForwardLength.rawValue)) let task = DispatchWorkItem { gestureStateHandler.jumpForwardKeyPressActive = false gestureStateHandler.jumpForwardKeyPressAmount = 0 } gestureStateHandler.jumpForwardKeyPressWorkItem = task DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: task) } else { gestureStateHandler.jumpForwardKeyPressActive = true gestureStateHandler.jumpForwardKeyPressAmount += 1 videoPlayerProxy.jumpForward(Int(jumpForwardLength.rawValue)) let task = DispatchWorkItem { gestureStateHandler.jumpForwardKeyPressActive = false gestureStateHandler.jumpForwardKeyPressAmount = 0 } gestureStateHandler.jumpForwardKeyPressWorkItem = task DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: task) } } // MARK: jump backward KeyCommandAction( title: L10n.jumpBackward, input: UIKeyCommand.inputLeftArrow ) { if gestureStateHandler.jumpBackwardKeyPressActive { gestureStateHandler.jumpBackwardKeyPressAmount += 1 gestureStateHandler.jumpBackwardKeyPressWorkItem?.cancel() videoPlayerProxy.jumpBackward(Int(jumpBackwardLength.rawValue)) let task = DispatchWorkItem { gestureStateHandler.jumpBackwardKeyPressActive = false gestureStateHandler.jumpBackwardKeyPressAmount = 0 } gestureStateHandler.jumpBackwardKeyPressWorkItem = task DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: task) } else { gestureStateHandler.jumpBackwardKeyPressActive = true gestureStateHandler.jumpBackwardKeyPressAmount += 1 videoPlayerProxy.jumpBackward(Int(jumpBackwardLength.rawValue)) let task = DispatchWorkItem { gestureStateHandler.jumpBackwardKeyPressActive = false gestureStateHandler.jumpBackwardKeyPressAmount = 0 } gestureStateHandler.jumpBackwardKeyPressWorkItem = task DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: task) } } // MARK: aspect fill KeyCommandAction( title: "Aspect Fill", input: "f", modifierFlags: .command ) { DispatchQueue.main.async { isAspectFilled.wrappedValue.toggle() } } // MARK: decrease playback speed KeyCommandAction( title: "Decrease Playback Speed", input: "[", modifierFlags: .command ) { let clampedPlaybackSpeed = clamp( videoPlayerManager.playbackSpeed.rawValue - 0.25, min: 0.25, max: 2.0 ) let newPlaybackSpeed = PlaybackSpeed(rawValue: clampedPlaybackSpeed) ?? .one videoPlayerManager.playbackSpeed = newPlaybackSpeed videoPlayerManager.proxy.setRate(.absolute(Float(newPlaybackSpeed.rawValue))) updateViewProxy.present(systemName: "speedometer", title: newPlaybackSpeed.rawValue.rateLabel) } // MARK: increase playback speed KeyCommandAction( title: "Increase Playback Speed", input: "]", modifierFlags: .command ) { let clampedPlaybackSpeed = clamp( videoPlayerManager.playbackSpeed.rawValue + 0.25, min: 0.25, max: 2.0 ) let newPlaybackSpeed = PlaybackSpeed(rawValue: clampedPlaybackSpeed) ?? .one videoPlayerManager.playbackSpeed = newPlaybackSpeed videoPlayerManager.proxy.setRate(.absolute(Float(newPlaybackSpeed.rawValue))) updateViewProxy.present(systemName: "speedometer", title: newPlaybackSpeed.rawValue.rateLabel) } // MARK: reset playback speed KeyCommandAction( title: "Reset Playback Speed", input: "\\", modifierFlags: .command ) { let newPlaybackSpeed = PlaybackSpeed.one videoPlayerManager.playbackSpeed = newPlaybackSpeed videoPlayerManager.proxy.setRate(.absolute(Float(newPlaybackSpeed.rawValue))) updateViewProxy.present(systemName: "speedometer", title: newPlaybackSpeed.rawValue.rateLabel) } // MARK: next item KeyCommandAction( title: L10n.nextItem, input: UIKeyCommand.inputRightArrow, modifierFlags: .command ) { videoPlayerManager.selectNextViewModel() } // MARK: previous item KeyCommandAction( title: L10n.previousItem, input: UIKeyCommand.inputLeftArrow, modifierFlags: .command ) { videoPlayerManager.selectPreviousViewModel() } } } }