mirror of
https://github.com/jellyfin/Swiftfin.git
synced 2024-11-27 08:10:23 +00:00
VideoPlayer's Double tap related UX improvement
Change VideoPlayer's overlay show implementation Add player gestures lock gesture settings
This commit is contained in:
parent
f98aa8a396
commit
0f92343970
@ -240,6 +240,8 @@ internal enum L10n {
|
|||||||
internal static var playbackSettings: String { return L10n.tr("Localizable", "playbackSettings") }
|
internal static var playbackSettings: String { return L10n.tr("Localizable", "playbackSettings") }
|
||||||
/// Playback Speed
|
/// Playback Speed
|
||||||
internal static var playbackSpeed: String { return L10n.tr("Localizable", "playbackSpeed") }
|
internal static var playbackSpeed: String { return L10n.tr("Localizable", "playbackSpeed") }
|
||||||
|
/// Player Gestures Lock Gesture Enabled
|
||||||
|
internal static var playerGesturesLockGestureEnabled: String { return L10n.tr("Localizable", "playerGesturesLockGestureEnabled") }
|
||||||
/// Play From Beginning
|
/// Play From Beginning
|
||||||
internal static var playFromBeginning: String { return L10n.tr("Localizable", "playFromBeginning") }
|
internal static var playFromBeginning: String { return L10n.tr("Localizable", "playFromBeginning") }
|
||||||
/// Play Next
|
/// Play Next
|
||||||
|
@ -48,6 +48,8 @@ extension Defaults.Keys {
|
|||||||
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let systemControlGesturesEnabled = Key<Bool>("systemControlGesturesEnabled", default: true,
|
static let systemControlGesturesEnabled = Key<Bool>("systemControlGesturesEnabled", default: true,
|
||||||
suite: SwiftfinStore.Defaults.generalSuite)
|
suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
|
static let playerGesturesLockGestureEnabled = Key<Bool>("playerGesturesLockGestureEnabled", default: true,
|
||||||
|
suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen,
|
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen,
|
||||||
suite: SwiftfinStore.Defaults.generalSuite)
|
suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen,
|
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen,
|
||||||
|
@ -95,6 +95,9 @@ final class VideoPlayerViewModel: ViewModel {
|
|||||||
@Published
|
@Published
|
||||||
var mediaItems: [BaseItemDto.ItemDetail]
|
var mediaItems: [BaseItemDto.ItemDetail]
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var isHiddenOverlay = false
|
||||||
|
|
||||||
// MARK: ShouldShowItems
|
// MARK: ShouldShowItems
|
||||||
|
|
||||||
let shouldShowPlayPreviousItem: Bool
|
let shouldShowPlayPreviousItem: Bool
|
||||||
@ -116,6 +119,7 @@ final class VideoPlayerViewModel: ViewModel {
|
|||||||
let overlayType: OverlayType
|
let overlayType: OverlayType
|
||||||
let jumpGesturesEnabled: Bool
|
let jumpGesturesEnabled: Bool
|
||||||
let systemControlGesturesEnabled: Bool
|
let systemControlGesturesEnabled: Bool
|
||||||
|
let playerGesturesLockGestureEnabled: Bool
|
||||||
let resumeOffset: Bool
|
let resumeOffset: Bool
|
||||||
let streamType: ServerStreamType
|
let streamType: ServerStreamType
|
||||||
let container: String
|
let container: String
|
||||||
@ -244,6 +248,7 @@ final class VideoPlayerViewModel: ViewModel {
|
|||||||
self.jumpForwardLength = Defaults[.videoPlayerJumpForward]
|
self.jumpForwardLength = Defaults[.videoPlayerJumpForward]
|
||||||
self.jumpGesturesEnabled = Defaults[.jumpGesturesEnabled]
|
self.jumpGesturesEnabled = Defaults[.jumpGesturesEnabled]
|
||||||
self.systemControlGesturesEnabled = Defaults[.systemControlGesturesEnabled]
|
self.systemControlGesturesEnabled = Defaults[.systemControlGesturesEnabled]
|
||||||
|
self.playerGesturesLockGestureEnabled = Defaults[.playerGesturesLockGestureEnabled]
|
||||||
self.shouldShowJumpButtonsInOverlayMenu = Defaults[.shouldShowJumpButtonsInOverlayMenu]
|
self.shouldShowJumpButtonsInOverlayMenu = Defaults[.shouldShowJumpButtonsInOverlayMenu]
|
||||||
|
|
||||||
self.resumeOffset = Defaults[.resumeOffset]
|
self.resumeOffset = Defaults[.resumeOffset]
|
||||||
|
@ -40,6 +40,8 @@ struct SettingsView: View {
|
|||||||
var jumpGesturesEnabled
|
var jumpGesturesEnabled
|
||||||
@Default(.systemControlGesturesEnabled)
|
@Default(.systemControlGesturesEnabled)
|
||||||
var systemControlGesturesEnabled
|
var systemControlGesturesEnabled
|
||||||
|
@Default(.playerGesturesLockGestureEnabled)
|
||||||
|
var playerGesturesLockGestureEnabled
|
||||||
@Default(.resumeOffset)
|
@Default(.resumeOffset)
|
||||||
var resumeOffset
|
var resumeOffset
|
||||||
@Default(.subtitleSize)
|
@Default(.subtitleSize)
|
||||||
@ -111,6 +113,8 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
Toggle(L10n.systemControlGesturesEnabled, isOn: $systemControlGesturesEnabled)
|
Toggle(L10n.systemControlGesturesEnabled, isOn: $systemControlGesturesEnabled)
|
||||||
|
|
||||||
|
Toggle(L10n.playerGesturesLockGestureEnabled, isOn: $playerGesturesLockGestureEnabled)
|
||||||
|
|
||||||
Toggle(L10n.resume5SecondOffset, isOn: $resumeOffset)
|
Toggle(L10n.resume5SecondOffset, isOn: $resumeOffset)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
|
@ -400,13 +400,11 @@ struct VLCPlayerOverlayView: View {
|
|||||||
.foregroundColor(Color.white)
|
.foregroundColor(Color.white)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
@ViewBuilder
|
||||||
|
var contents: some View {
|
||||||
if viewModel.overlayType == .normal {
|
if viewModel.overlayType == .normal {
|
||||||
mainBody
|
mainBody
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
|
||||||
viewModel.playerOverlayDelegate?.didGenerallyTap()
|
|
||||||
}
|
|
||||||
.background {
|
.background {
|
||||||
Color(uiColor: .black.withAlphaComponent(0.5))
|
Color(uiColor: .black.withAlphaComponent(0.5))
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
@ -414,11 +412,22 @@ struct VLCPlayerOverlayView: View {
|
|||||||
} else {
|
} else {
|
||||||
mainBody
|
mainBody
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
|
||||||
viewModel.playerOverlayDelegate?.didGenerallyTap()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
contents
|
||||||
|
.onLongPressGesture {
|
||||||
|
guard viewModel.playerGesturesLockGestureEnabled else { return }
|
||||||
|
viewModel.playerOverlayDelegate?.didGenerallyTap(point: nil)
|
||||||
|
viewModel.playerOverlayDelegate?.didLongPress()
|
||||||
|
}
|
||||||
|
.gesture(DragGesture(minimumDistance: 0)
|
||||||
|
.onEnded { value in
|
||||||
|
viewModel.playerOverlayDelegate?.didGenerallyTap(point: value.location)
|
||||||
|
})
|
||||||
|
.opacity(viewModel.isHiddenOverlay ? 0 : 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VLCPlayerCompactOverlayView_Previews: PreviewProvider {
|
struct VLCPlayerCompactOverlayView_Previews: PreviewProvider {
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
protocol PlayerOverlayDelegate {
|
protocol PlayerOverlayDelegate {
|
||||||
|
|
||||||
@ -19,7 +20,8 @@ protocol PlayerOverlayDelegate {
|
|||||||
func didSelectForward()
|
func didSelectForward()
|
||||||
func didSelectMain()
|
func didSelectMain()
|
||||||
|
|
||||||
func didGenerallyTap()
|
func didGenerallyTap(point: CGPoint?)
|
||||||
|
func didLongPress()
|
||||||
|
|
||||||
func didBeginScrubbing()
|
func didBeginScrubbing()
|
||||||
func didEndScrubbing()
|
func didEndScrubbing()
|
||||||
|
@ -46,6 +46,10 @@ class VLCPlayerViewController: UIViewController {
|
|||||||
private var panBeganBrightness = CGFloat.zero
|
private var panBeganBrightness = CGFloat.zero
|
||||||
private var panBeganVolumeValue = Float.zero
|
private var panBeganVolumeValue = Float.zero
|
||||||
private var panBeganPoint = CGPoint.zero
|
private var panBeganPoint = CGPoint.zero
|
||||||
|
private var tapLocationStack = [CGPoint]()
|
||||||
|
private var isJumping = false
|
||||||
|
private var jumpingCompletionWork: DispatchWorkItem?
|
||||||
|
private var isTapWhenJumping = false
|
||||||
|
|
||||||
private lazy var videoContentView = makeVideoContentView()
|
private lazy var videoContentView = makeVideoContentView()
|
||||||
private lazy var mainGestureView = makeMainGestureView()
|
private lazy var mainGestureView = makeMainGestureView()
|
||||||
@ -230,24 +234,17 @@ class VLCPlayerViewController: UIViewController {
|
|||||||
|
|
||||||
let singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
|
let singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
|
||||||
|
|
||||||
let doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(didDoubleTap))
|
|
||||||
doubleTapGesture.numberOfTapsRequired = 2
|
|
||||||
|
|
||||||
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(didPinch(_:)))
|
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(didPinch(_:)))
|
||||||
|
|
||||||
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPan(_:)))
|
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPan(_:)))
|
||||||
|
|
||||||
let longPeessGesture = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress))
|
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress))
|
||||||
|
|
||||||
view.addGestureRecognizer(singleTapGesture)
|
view.addGestureRecognizer(singleTapGesture)
|
||||||
view.addGestureRecognizer(pinchGesture)
|
view.addGestureRecognizer(pinchGesture)
|
||||||
view.addGestureRecognizer(longPeessGesture)
|
|
||||||
|
|
||||||
if viewModel.jumpGesturesEnabled {
|
if viewModel.playerGesturesLockGestureEnabled {
|
||||||
view.addGestureRecognizer(doubleTapGesture)
|
view.addGestureRecognizer(longPressGesture)
|
||||||
singleTapGesture.require(toFail: doubleTapGesture)
|
|
||||||
singleTapGesture.delaysTouchesBegan = true
|
|
||||||
doubleTapGesture.delaysTouchesBegan = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.systemControlGesturesEnabled {
|
if viewModel.systemControlGesturesEnabled {
|
||||||
@ -264,6 +261,7 @@ class VLCPlayerViewController: UIViewController {
|
|||||||
label.alpha = 0
|
label.alpha = 0
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
label.font = .systemFont(ofSize: 48)
|
label.font = .systemFont(ofSize: 48)
|
||||||
|
label.layer.zPosition = 1
|
||||||
return label
|
return label
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,11 +269,13 @@ class VLCPlayerViewController: UIViewController {
|
|||||||
|
|
||||||
private func makeGestureLockedOverlayView() -> UIView {
|
private func makeGestureLockedOverlayView() -> UIView {
|
||||||
let backgroundView = UIView()
|
let backgroundView = UIView()
|
||||||
|
backgroundView.layer.zPosition = 1
|
||||||
backgroundView.alpha = 0
|
backgroundView.alpha = 0
|
||||||
backgroundView.translatesAutoresizingMaskIntoConstraints = false
|
backgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
|
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
|
||||||
self?.isGesturesLocked = false
|
self?.isGesturesLocked = false
|
||||||
self?.hideLockedOverlay()
|
self?.hideLockedOverlay()
|
||||||
|
self?.didGenerallyTap()
|
||||||
}))
|
}))
|
||||||
button.translatesAutoresizingMaskIntoConstraints = false
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
button.setImage(UIImage(systemName: "lock.open", withConfiguration: UIImage.SymbolConfiguration(pointSize: 48))?
|
button.setImage(UIImage(systemName: "lock.open", withConfiguration: UIImage.SymbolConfiguration(pointSize: 48))?
|
||||||
@ -295,21 +295,12 @@ class VLCPlayerViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
private func didTap() {
|
private func didTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||||
didGenerallyTap()
|
didGenerallyTap(point: gestureRecognizer.location(in: mainGestureView))
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
private func didDoubleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
func didLongPress() {
|
||||||
if gestureRecognizer.location(in: mainGestureView).x > (mainGestureView.frame.width / 2) {
|
|
||||||
didSelectForward()
|
|
||||||
} else {
|
|
||||||
didSelectBackward()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
private func didLongPress() {
|
|
||||||
guard !isGesturesLocked else { return }
|
guard !isGesturesLocked else { return }
|
||||||
isGesturesLocked = true
|
isGesturesLocked = true
|
||||||
didGenerallyTap()
|
didGenerallyTap()
|
||||||
@ -647,9 +638,10 @@ extension VLCPlayerViewController {
|
|||||||
guard let overlayHostingController = currentOverlayHostingController else { return }
|
guard let overlayHostingController = currentOverlayHostingController else { return }
|
||||||
|
|
||||||
guard overlayHostingController.view.alpha != 1 else { return }
|
guard overlayHostingController.view.alpha != 1 else { return }
|
||||||
|
overlayHostingController.view.alpha = 1
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.2) {
|
withAnimation(.easeInOut(duration: 0.2)) { [weak self] in
|
||||||
overlayHostingController.view.alpha = 1
|
self?.viewModel.isHiddenOverlay = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -660,8 +652,16 @@ extension VLCPlayerViewController {
|
|||||||
|
|
||||||
guard overlayHostingController.view.alpha != 0 else { return }
|
guard overlayHostingController.view.alpha != 0 else { return }
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.2) {
|
// for gestures UX
|
||||||
|
view.exchangeSubview(at: view.subviews.firstIndex(of: mainGestureView)!,
|
||||||
|
withSubviewAt: view.subviews.firstIndex(of: overlayHostingController.view)!)
|
||||||
|
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut) {
|
||||||
overlayHostingController.view.alpha = 0
|
overlayHostingController.view.alpha = 0
|
||||||
|
} completion: { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.view.exchangeSubview(at: self.view.subviews.firstIndex(of: self.mainGestureView)!,
|
||||||
|
withSubviewAt: self.view.subviews.firstIndex(of: overlayHostingController.view)!)
|
||||||
|
self.viewModel.isHiddenOverlay = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -998,16 +998,65 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func didGenerallyTap() {
|
func didGenerallyTap(point: CGPoint? = nil) {
|
||||||
if isGesturesLocked {
|
if isGesturesLocked {
|
||||||
toggleLockedOverlay()
|
toggleLockedOverlay()
|
||||||
} else {
|
} else {
|
||||||
|
if viewModel.jumpGesturesEnabled,
|
||||||
|
let point = point
|
||||||
|
{
|
||||||
|
let tempStack = tapLocationStack
|
||||||
|
tapLocationStack.append(point)
|
||||||
|
|
||||||
|
if isSameLocationWithLast(point: point, in: tempStack) {
|
||||||
|
isTapWhenJumping = false
|
||||||
|
isJumping = true
|
||||||
|
tapLocationStack.removeAll()
|
||||||
|
jumpingCompletionWork?.cancel()
|
||||||
|
jumpingCompletionWork = DispatchWorkItem(block: { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.isJumping = false
|
||||||
|
guard self.isTapWhenJumping else { return }
|
||||||
|
self.isTapWhenJumping = false
|
||||||
|
self.toggleOverlay()
|
||||||
|
})
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: jumpingCompletionWork!)
|
||||||
|
|
||||||
|
hideOverlay()
|
||||||
|
if point.x > (mainGestureView.frame.width / 2) {
|
||||||
|
didSelectForward()
|
||||||
|
} else {
|
||||||
|
didSelectBackward()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard !self.tapLocationStack.isEmpty else { return }
|
||||||
|
self.tapLocationStack.removeFirst()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard !isJumping else {
|
||||||
|
isTapWhenJumping = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
toggleOverlay()
|
toggleOverlay()
|
||||||
}
|
}
|
||||||
|
|
||||||
restartOverlayDismissTimer(interval: 5)
|
restartOverlayDismissTimer(interval: 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func isSameLocationWithLast(point: CGPoint, in stack: [CGPoint]) -> Bool {
|
||||||
|
guard let last = stack.last else { return false }
|
||||||
|
if last.x > (mainGestureView.frame.width / 2) {
|
||||||
|
return point.x > (mainGestureView.frame.width / 2)
|
||||||
|
} else {
|
||||||
|
return point.x <= (mainGestureView.frame.width / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func didBeginScrubbing() {
|
func didBeginScrubbing() {
|
||||||
stopOverlayDismissTimer()
|
stopOverlayDismissTimer()
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user