From 0f923439707ddbd7abe961a187ba85a14d849a34 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Sun, 1 May 2022 05:12:23 +0900 Subject: [PATCH] VideoPlayer's Double tap related UX improvement Change VideoPlayer's overlay show implementation Add player gestures lock gesture settings --- Shared/Generated/Strings.swift | 2 + .../SwiftfinStore/SwiftfinStoreDefaults.swift | 2 + .../VideoPlayerViewModel.swift | 5 + .../Views/SettingsView/SettingsView.swift | 4 + .../Overlays/VLCPlayerOverlayView.swift | 23 ++-- .../VideoPlayer/PlayerOverlayDelegate.swift | 4 +- .../VideoPlayer/VLCPlayerViewController.swift | 101 +++++++++++++----- Translations/en.lproj/Localizable.strings | Bin 12520 -> 12674 bytes 8 files changed, 107 insertions(+), 34 deletions(-) diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index 3f18c5d0..aab098bd 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -240,6 +240,8 @@ internal enum L10n { internal static var playbackSettings: String { return L10n.tr("Localizable", "playbackSettings") } /// Playback Speed 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 internal static var playFromBeginning: String { return L10n.tr("Localizable", "playFromBeginning") } /// Play Next diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index d2300763..763dbaac 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -48,6 +48,8 @@ extension Defaults.Keys { static let jumpGesturesEnabled = Key("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let systemControlGesturesEnabled = Key("systemControlGesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite) + static let playerGesturesLockGestureEnabled = Key("playerGesturesLockGestureEnabled", default: true, + suite: SwiftfinStore.Defaults.generalSuite) static let videoPlayerJumpForward = Key("videoPlayerJumpForward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite) static let videoPlayerJumpBackward = Key("videoPlayerJumpBackward", default: .fifteen, diff --git a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift index 10dc2140..db64d182 100644 --- a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift @@ -95,6 +95,9 @@ final class VideoPlayerViewModel: ViewModel { @Published var mediaItems: [BaseItemDto.ItemDetail] + @Published + var isHiddenOverlay = false + // MARK: ShouldShowItems let shouldShowPlayPreviousItem: Bool @@ -116,6 +119,7 @@ final class VideoPlayerViewModel: ViewModel { let overlayType: OverlayType let jumpGesturesEnabled: Bool let systemControlGesturesEnabled: Bool + let playerGesturesLockGestureEnabled: Bool let resumeOffset: Bool let streamType: ServerStreamType let container: String @@ -244,6 +248,7 @@ final class VideoPlayerViewModel: ViewModel { self.jumpForwardLength = Defaults[.videoPlayerJumpForward] self.jumpGesturesEnabled = Defaults[.jumpGesturesEnabled] self.systemControlGesturesEnabled = Defaults[.systemControlGesturesEnabled] + self.playerGesturesLockGestureEnabled = Defaults[.playerGesturesLockGestureEnabled] self.shouldShowJumpButtonsInOverlayMenu = Defaults[.shouldShowJumpButtonsInOverlayMenu] self.resumeOffset = Defaults[.resumeOffset] diff --git a/Swiftfin/Views/SettingsView/SettingsView.swift b/Swiftfin/Views/SettingsView/SettingsView.swift index e1a0ffae..0ffb94f2 100644 --- a/Swiftfin/Views/SettingsView/SettingsView.swift +++ b/Swiftfin/Views/SettingsView/SettingsView.swift @@ -40,6 +40,8 @@ struct SettingsView: View { var jumpGesturesEnabled @Default(.systemControlGesturesEnabled) var systemControlGesturesEnabled + @Default(.playerGesturesLockGestureEnabled) + var playerGesturesLockGestureEnabled @Default(.resumeOffset) var resumeOffset @Default(.subtitleSize) @@ -111,6 +113,8 @@ struct SettingsView: View { Toggle(L10n.systemControlGesturesEnabled, isOn: $systemControlGesturesEnabled) + Toggle(L10n.playerGesturesLockGestureEnabled, isOn: $playerGesturesLockGestureEnabled) + Toggle(L10n.resume5SecondOffset, isOn: $resumeOffset) Button { diff --git a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift index 5530e798..3ec74867 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift @@ -400,13 +400,11 @@ struct VLCPlayerOverlayView: View { .foregroundColor(Color.white) } - var body: some View { + @ViewBuilder + var contents: some View { if viewModel.overlayType == .normal { mainBody .contentShape(Rectangle()) - .onTapGesture { - viewModel.playerOverlayDelegate?.didGenerallyTap() - } .background { Color(uiColor: .black.withAlphaComponent(0.5)) .ignoresSafeArea() @@ -414,11 +412,22 @@ struct VLCPlayerOverlayView: View { } else { mainBody .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 { diff --git a/Swiftfin/Views/VideoPlayer/PlayerOverlayDelegate.swift b/Swiftfin/Views/VideoPlayer/PlayerOverlayDelegate.swift index 4977b3a4..466ca609 100644 --- a/Swiftfin/Views/VideoPlayer/PlayerOverlayDelegate.swift +++ b/Swiftfin/Views/VideoPlayer/PlayerOverlayDelegate.swift @@ -8,6 +8,7 @@ import Foundation import JellyfinAPI +import UIKit protocol PlayerOverlayDelegate { @@ -19,7 +20,8 @@ protocol PlayerOverlayDelegate { func didSelectForward() func didSelectMain() - func didGenerallyTap() + func didGenerallyTap(point: CGPoint?) + func didLongPress() func didBeginScrubbing() func didEndScrubbing() diff --git a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift index 5f8a3f96..857bde5c 100644 --- a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift @@ -46,6 +46,10 @@ class VLCPlayerViewController: UIViewController { private var panBeganBrightness = CGFloat.zero private var panBeganVolumeValue = Float.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 mainGestureView = makeMainGestureView() @@ -230,24 +234,17 @@ class VLCPlayerViewController: UIViewController { 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 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(pinchGesture) - view.addGestureRecognizer(longPeessGesture) - if viewModel.jumpGesturesEnabled { - view.addGestureRecognizer(doubleTapGesture) - singleTapGesture.require(toFail: doubleTapGesture) - singleTapGesture.delaysTouchesBegan = true - doubleTapGesture.delaysTouchesBegan = true + if viewModel.playerGesturesLockGestureEnabled { + view.addGestureRecognizer(longPressGesture) } if viewModel.systemControlGesturesEnabled { @@ -264,6 +261,7 @@ class VLCPlayerViewController: UIViewController { label.alpha = 0 label.translatesAutoresizingMaskIntoConstraints = false label.font = .systemFont(ofSize: 48) + label.layer.zPosition = 1 return label } @@ -271,11 +269,13 @@ class VLCPlayerViewController: UIViewController { private func makeGestureLockedOverlayView() -> UIView { let backgroundView = UIView() + backgroundView.layer.zPosition = 1 backgroundView.alpha = 0 backgroundView.translatesAutoresizingMaskIntoConstraints = false let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in self?.isGesturesLocked = false self?.hideLockedOverlay() + self?.didGenerallyTap() })) button.translatesAutoresizingMaskIntoConstraints = false button.setImage(UIImage(systemName: "lock.open", withConfiguration: UIImage.SymbolConfiguration(pointSize: 48))? @@ -295,21 +295,12 @@ class VLCPlayerViewController: UIViewController { } @objc - private func didTap() { - didGenerallyTap() + private func didTap(_ gestureRecognizer: UITapGestureRecognizer) { + didGenerallyTap(point: gestureRecognizer.location(in: mainGestureView)) } @objc - private func didDoubleTap(_ gestureRecognizer: UITapGestureRecognizer) { - if gestureRecognizer.location(in: mainGestureView).x > (mainGestureView.frame.width / 2) { - didSelectForward() - } else { - didSelectBackward() - } - } - - @objc - private func didLongPress() { + func didLongPress() { guard !isGesturesLocked else { return } isGesturesLocked = true didGenerallyTap() @@ -647,9 +638,10 @@ extension VLCPlayerViewController { guard let overlayHostingController = currentOverlayHostingController else { return } guard overlayHostingController.view.alpha != 1 else { return } + overlayHostingController.view.alpha = 1 - UIView.animate(withDuration: 0.2) { - overlayHostingController.view.alpha = 1 + withAnimation(.easeInOut(duration: 0.2)) { [weak self] in + self?.viewModel.isHiddenOverlay = false } } @@ -660,8 +652,16 @@ extension VLCPlayerViewController { 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 + } 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 { toggleLockedOverlay() } 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() } 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() { stopOverlayDismissTimer() } diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 6554c737257d7b2fd798593430cb209e84c69ad6..268945c509b96192aafd5b8db83b2c963f22c82c 100644 GIT binary patch delta 106 zcmaEn*p$4%NWrjxA%`K6p^_n$p@_j9NES1cFq8t>AeIk9K0`7?HmdmKczMamZ)7-_ ZfjTDF%ZrM`R4JgTn3==CkF+V