From 7a26e6968572b0ad6e5db501e50e869eaacc4ea6 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Wed, 4 May 2022 02:35:07 +0900 Subject: [PATCH 1/2] Add seek slide gesture --- Shared/Generated/Strings.swift | 2 + .../SwiftfinStore/SwiftfinStoreDefaults.swift | 35 +++++----- .../UIKit/PanDirectionGestureRecognizer.swift | 39 +++++++++++ .../VideoPlayerViewModel.swift | 62 +++++++++--------- Swiftfin.xcodeproj/project.pbxproj | 12 ++++ .../Views/SettingsView/SettingsView.swift | 4 ++ .../VideoPlayer/VLCPlayerViewController.swift | 53 +++++++++++++-- Translations/en.lproj/Localizable.strings | Bin 12762 -> 12878 bytes 8 files changed, 157 insertions(+), 50 deletions(-) create mode 100644 Shared/UIKit/PanDirectionGestureRecognizer.swift diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index e022f597..a9fe5576 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -310,6 +310,8 @@ internal enum L10n { internal static var seasons: String { return L10n.tr("Localizable", "seasons") } /// See All internal static var seeAll: String { return L10n.tr("Localizable", "seeAll") } + /// Seek Slide Gesture Enabled + internal static var seekSlideGestureEnabled: String { return L10n.tr("Localizable", "seekSlideGestureEnabled") } /// See More internal static var seeMore: String { return L10n.tr("Localizable", "seeMore") } /// Select Cast Destination diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index 763dbaac..20630909 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -10,21 +10,14 @@ import Defaults import Foundation extension SwiftfinStore { - enum Defaults { + static let generalSuite: UserDefaults = .init(suiteName: "swiftfinstore-general-defaults")! - static let generalSuite: UserDefaults = { - UserDefaults(suiteName: "swiftfinstore-general-defaults")! - }() - - static let universalSuite: UserDefaults = { - UserDefaults(suiteName: "swiftfinstore-universal-defaults")! - }() + static let universalSuite: UserDefaults = .init(suiteName: "swiftfinstore-universal-defaults")! } } extension Defaults.Keys { - // Universal settings static let defaultHTTPScheme = Key("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite) static let appAppearance = Key("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite) @@ -34,7 +27,8 @@ extension Defaults.Keys { static let inNetworkBandwidth = Key("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite) static let outOfNetworkBandwidth = Key("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite) static let isAutoSelectSubtitles = Key("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite) - static let autoSelectSubtitlesLangCode = Key("AutoSelectSubtitlesLangCode", default: "Auto", + static let autoSelectSubtitlesLangCode = Key("AutoSelectSubtitlesLangCode", + default: "Auto", suite: SwiftfinStore.Defaults.generalSuite) static let autoSelectAudioLangCode = Key("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite) @@ -46,13 +40,20 @@ extension Defaults.Keys { // Video player / overlay settings static let overlayType = Key("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite) static let jumpGesturesEnabled = Key("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite) - static let systemControlGesturesEnabled = Key("systemControlGesturesEnabled", default: true, + static let systemControlGesturesEnabled = Key("systemControlGesturesEnabled", + default: true, suite: SwiftfinStore.Defaults.generalSuite) - static let playerGesturesLockGestureEnabled = Key("playerGesturesLockGestureEnabled", default: true, + static let playerGesturesLockGestureEnabled = Key("playerGesturesLockGestureEnabled", + default: true, suite: SwiftfinStore.Defaults.generalSuite) - static let videoPlayerJumpForward = Key("videoPlayerJumpForward", default: .fifteen, + static let seekSlideGestureEnabled = Key("seekSlideGestureEnabled", + default: true, + suite: SwiftfinStore.Defaults.generalSuite) + static let videoPlayerJumpForward = Key("videoPlayerJumpForward", + default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite) - static let videoPlayerJumpBackward = Key("videoPlayerJumpBackward", default: .fifteen, + static let videoPlayerJumpBackward = Key("videoPlayerJumpBackward", + default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite) static let autoplayEnabled = Key("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let resumeOffset = Key("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite) @@ -68,12 +69,14 @@ extension Defaults.Keys { static let shouldShowMissingEpisodes = Key("shouldShowMissingEpisodes", default: true, suite: SwiftfinStore.Defaults.generalSuite) // Should show video player items in overlay menu - static let shouldShowJumpButtonsInOverlayMenu = Key("shouldShowJumpButtonsInMenu", default: true, + static let shouldShowJumpButtonsInOverlayMenu = Key("shouldShowJumpButtonsInMenu", + default: true, suite: SwiftfinStore.Defaults.generalSuite) // Experimental settings enum Experimental { - static let syncSubtitleStateWithAdjacent = Key("experimental.syncSubtitleState", default: false, + static let syncSubtitleStateWithAdjacent = Key("experimental.syncSubtitleState", + default: false, suite: SwiftfinStore.Defaults.generalSuite) static let forceDirectPlay = Key("forceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let nativePlayer = Key("nativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite) diff --git a/Shared/UIKit/PanDirectionGestureRecognizer.swift b/Shared/UIKit/PanDirectionGestureRecognizer.swift new file mode 100644 index 00000000..724ee962 --- /dev/null +++ b/Shared/UIKit/PanDirectionGestureRecognizer.swift @@ -0,0 +1,39 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import UIKit.UIGestureRecognizerSubclass + +enum PanDirection { + case vertical + case horizontal +} + +class PanDirectionGestureRecognizer: UIPanGestureRecognizer { + let direction: PanDirection + + init(direction: PanDirection, target: AnyObject, action: Selector) { + self.direction = direction + super.init(target: target, action: action) + } + + override func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if state == .began { + let vel = velocity(in: view) + switch direction { + case .horizontal where abs(vel.y) > abs(vel.x): + state = .cancelled + case .vertical where abs(vel.x) > abs(vel.y): + state = .cancelled + default: + break + } + } + } +} diff --git a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift index f65d15ed..87b8d54f 100644 --- a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift @@ -20,7 +20,6 @@ import UIKit #endif final class VideoPlayerViewModel: ViewModel { - // MARK: Published // Manually kept state because VLCKit doesn't properly set "played" @@ -32,6 +31,8 @@ final class VideoPlayerViewModel: ViewModel { @Published var rightLabelText: String = "--:--" @Published + var scrubbingTimeLabelText: String = "--:--" + @Published var playbackSpeed: PlaybackSpeed = .one @Published var subtitlesEnabled: Bool { @@ -74,7 +75,12 @@ final class VideoPlayerViewModel: ViewModel { } @Published - var sliderIsScrubbing: Bool = false + var sliderIsScrubbing: Bool = false { + didSet { + beganScrubbingCurrentSeconds = currentSeconds + } + } + @Published var sliderPercentage: Double = 0 { willSet { @@ -119,6 +125,7 @@ final class VideoPlayerViewModel: ViewModel { let overlayType: OverlayType let jumpGesturesEnabled: Bool let systemControlGesturesEnabled: Bool + let seekSlideGestureEnabled: Bool let playerGesturesLockGestureEnabled: Bool let resumeOffset: Bool let streamType: ServerStreamType @@ -144,6 +151,8 @@ final class VideoPlayerViewModel: ViewModel { // MARK: Current Time + private var beganScrubbingCurrentSeconds: Double = 0 + var currentSeconds: Double { let runTimeTicks = item.runTimeTicks ?? 0 let videoDuration = Double(runTimeTicks / 10_000_000) @@ -173,13 +182,12 @@ final class VideoPlayerViewModel: ViewModel { } var currentChapter: ChapterInfo? { - let chapterPairs = chapters.adjacentPairs().map { ($0, $1) } let chapterRanges = chapterPairs.map { ($0.startPositionTicks ?? 0, ($1.startPositionTicks ?? 1) - 1) } for chapterRangeIndex in 0 ..< chapterRanges.count { - if chapterRanges[chapterRangeIndex].0 <= currentSecondTicks && - currentSecondTicks < chapterRanges[chapterRangeIndex].1 + if chapterRanges[chapterRangeIndex].0 <= currentSecondTicks, + currentSecondTicks < chapterRanges[chapterRangeIndex].1 { return chapterPairs[chapterRangeIndex].0 } @@ -249,6 +257,7 @@ final class VideoPlayerViewModel: ViewModel { self.jumpGesturesEnabled = Defaults[.jumpGesturesEnabled] self.systemControlGesturesEnabled = Defaults[.systemControlGesturesEnabled] self.playerGesturesLockGestureEnabled = Defaults[.playerGesturesLockGestureEnabled] + self.seekSlideGestureEnabled = Defaults[.seekSlideGestureEnabled] self.shouldShowJumpButtonsInOverlayMenu = Defaults[.shouldShowJumpButtonsInOverlayMenu] self.resumeOffset = Defaults[.resumeOffset] @@ -271,9 +280,12 @@ final class VideoPlayerViewModel: ViewModel { leftLabelText = calculateTimeText(from: currentSeconds) rightLabelText = calculateTimeText(from: secondsScrubbedRemaining) + scrubbingTimeLabelText = calculateTimeText(from: currentSeconds - beganScrubbingCurrentSeconds) } private func calculateTimeText(from duration: Double) -> String { + let isNegative = duration < 0 + let duration = abs(duration) let hours = floor(duration / 3600) let minutes = duration.truncatingRemainder(dividingBy: 3600) / 60 let seconds = duration.truncatingRemainder(dividingBy: 3600).truncatingRemainder(dividingBy: 60) @@ -288,17 +300,15 @@ final class VideoPlayerViewModel: ViewModel { "\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))" } - return timeText + return "\(isNegative ? "-" : "") \(timeText)" } } // MARK: Injected Values extension VideoPlayerViewModel { - // Injects custom values that override certain settings func injectCustomValues(startFromBeginning: Bool = false) { - if startFromBeginning { item.userData?.playbackPositionTicks = 0 item.userData?.playedPercentage = 0 @@ -311,7 +321,6 @@ extension VideoPlayerViewModel { // MARK: Adjacent Items extension VideoPlayerViewModel { - func getAdjacentEpisodes() { guard let seriesID = item.seriesId, item.itemType == .episode else { return } @@ -412,21 +421,21 @@ extension VideoPlayerViewModel { guard let masterSubtitleStream = masterViewModel.subtitleStreams .first(where: { $0.index == masterViewModel.selectedSubtitleStreamIndex }), - let matchingSubtitleStream = self.subtitleStreams.first(where: { mediaStreamAboutEqual($0, masterSubtitleStream) }), + let matchingSubtitleStream = subtitleStreams.first(where: { mediaStreamAboutEqual($0, masterSubtitleStream) }), let matchingSubtitleStreamIndex = matchingSubtitleStream.index else { return } - self.selectedSubtitleStreamIndex = matchingSubtitleStreamIndex + selectedSubtitleStreamIndex = matchingSubtitleStreamIndex } private func matchAudioStream(with masterViewModel: VideoPlayerViewModel) { guard let currentAudioStream = masterViewModel.audioStreams.first(where: { $0.index == masterViewModel.selectedAudioStreamIndex }), - let matchingAudioStream = self.audioStreams.first(where: { mediaStreamAboutEqual($0, currentAudioStream) }) else { return } + let matchingAudioStream = audioStreams.first(where: { mediaStreamAboutEqual($0, currentAudioStream) }) else { return } - self.selectedAudioStreamIndex = matchingAudioStream.index ?? -1 + selectedAudioStreamIndex = matchingAudioStream.index ?? -1 } private func matchSubtitlesEnabled(with masterViewModel: VideoPlayerViewModel) { - self.subtitlesEnabled = masterViewModel.subtitlesEnabled + subtitlesEnabled = masterViewModel.subtitlesEnabled } private func mediaStreamAboutEqual(_ lhs: MediaStream, _ rhs: MediaStream) -> Bool { @@ -437,23 +446,23 @@ extension VideoPlayerViewModel { // MARK: Progress Report Timer extension VideoPlayerViewModel { - private func sendNewProgressReportWithTimer() { - self.progressReportTimer?.invalidate() - self.progressReportTimer = Timer.scheduledTimer(timeInterval: 0.7, target: self, selector: #selector(_sendProgressReport), - userInfo: nil, repeats: false) + progressReportTimer?.invalidate() + progressReportTimer = Timer.scheduledTimer(timeInterval: 0.7, + target: self, + selector: #selector(_sendProgressReport), + userInfo: nil, + repeats: false) } } // MARK: Updates extension VideoPlayerViewModel { - // MARK: sendPlayReport func sendPlayReport() { - - self.startTimeTicks = Int64(Date().timeIntervalSince1970) * 10_000_000 + startTimeTicks = Int64(Date().timeIntervalSince1970) * 10_000_000 let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil @@ -490,7 +499,6 @@ extension VideoPlayerViewModel { // MARK: sendPauseReport func sendPauseReport(paused: Bool) { - let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil let pauseInfo = PlaybackStartInfo(canSeek: true, @@ -526,7 +534,6 @@ extension VideoPlayerViewModel { // MARK: sendProgressReport func sendProgressReport() { - let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil let progressInfo = PlaybackProgressInfo(canSeek: true, @@ -550,9 +557,9 @@ extension VideoPlayerViewModel { nowPlayingQueue: nil, playlistItemId: "playlistItem0") - self.lastProgressReport = progressInfo + lastProgressReport = progressInfo - self.sendNewProgressReportWithTimer() + sendNewProgressReportWithTimer() } @objc @@ -573,7 +580,6 @@ extension VideoPlayerViewModel { // MARK: sendStopReport func sendStopReport() { - let stopInfo = PlaybackStopInfo(item: item, itemId: item.id, sessionId: response.playSessionId, @@ -600,9 +606,7 @@ extension VideoPlayerViewModel { // MARK: Embedded/Normal Subtitle Streams extension VideoPlayerViewModel { - func createEmbeddedSubtitleStream(with subtitleStream: MediaStream) -> URL { - guard let baseURL = URLComponents(url: directStreamURL, resolvingAgainstBaseURL: false) else { fatalError() } guard let queryItems = baseURL.queryItems else { fatalError() } @@ -622,7 +626,6 @@ extension VideoPlayerViewModel { // MARK: Equatable extension VideoPlayerViewModel: Equatable { - static func == (lhs: VideoPlayerViewModel, rhs: VideoPlayerViewModel) -> Bool { lhs.item.id == rhs.item.id && lhs.item.userData?.playbackPositionTicks == rhs.item.userData?.playbackPositionTicks @@ -632,7 +635,6 @@ extension VideoPlayerViewModel: Equatable { // MARK: Hashable extension VideoPlayerViewModel: Hashable { - func hash(into hasher: inout Hasher) { hasher.combine(item) hasher.combine(directStreamURL) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 537b1932..0f309690 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -155,6 +155,7 @@ 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */; }; 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; + 62553429282190A00087FE20 /* PanDirectionGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62553428282190A00087FE20 /* PanDirectionGestureRecognizer.swift */; }; 625CB56F2678C23300530A6E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56E2678C23300530A6E /* HomeView.swift */; }; 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5722678C32A00530A6E /* HomeViewModel.swift */; }; 625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5742678C33500530A6E /* LibraryListViewModel.swift */; }; @@ -667,6 +668,7 @@ 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppURLHandler.swift; sourceTree = ""; }; 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = ""; }; 624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = ""; }; + 62553428282190A00087FE20 /* PanDirectionGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanDirectionGestureRecognizer.swift; sourceTree = ""; }; 625CB56E2678C23300530A6E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 625CB5722678C32A00530A6E /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; 625CB5742678C33500530A6E /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = ""; }; @@ -1107,6 +1109,7 @@ 535870752669D60C00D05A09 /* Shared */ = { isa = PBXGroup; children = ( + 625534272821908D0087FE20 /* UIKit */, 6286F09F271C0AA500C40ED5 /* Generated */, 62C29E9D26D0FE5900C1D2E7 /* Coordinators */, E1FCD08E26C466F3007C8DCF /* Errors */, @@ -1430,6 +1433,14 @@ path = Extensions; sourceTree = ""; }; + 625534272821908D0087FE20 /* UIKit */ = { + isa = PBXGroup; + children = ( + 62553428282190A00087FE20 /* PanDirectionGestureRecognizer.swift */, + ); + path = UIKit; + sourceTree = ""; + }; 6286F09F271C0AA500C40ED5 /* Generated */ = { isa = PBXGroup; children = ( @@ -2498,6 +2509,7 @@ 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */, E1D4BF872719D27100A11E64 /* Bitrates.swift in Sources */, 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, + 62553429282190A00087FE20 /* PanDirectionGestureRecognizer.swift in Sources */, E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */, E10D87DE278510E400BD264C /* PosterSize.swift in Sources */, diff --git a/Swiftfin/Views/SettingsView/SettingsView.swift b/Swiftfin/Views/SettingsView/SettingsView.swift index 0ffb94f2..b7395be5 100644 --- a/Swiftfin/Views/SettingsView/SettingsView.swift +++ b/Swiftfin/Views/SettingsView/SettingsView.swift @@ -42,6 +42,8 @@ struct SettingsView: View { var systemControlGesturesEnabled @Default(.playerGesturesLockGestureEnabled) var playerGesturesLockGestureEnabled + @Default(.seekSlideGestureEnabled) + var seekSlideGestureEnabled @Default(.resumeOffset) var resumeOffset @Default(.subtitleSize) @@ -113,6 +115,8 @@ struct SettingsView: View { Toggle(L10n.systemControlGesturesEnabled, isOn: $systemControlGesturesEnabled) + Toggle(L10n.seekSlideGestureEnabled, isOn: $seekSlideGestureEnabled) + Toggle(L10n.playerGesturesLockGestureEnabled, isOn: $playerGesturesLockGestureEnabled) Toggle(L10n.resume5SecondOffset, isOn: $resumeOffset) diff --git a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift index 9deaf472..bf0dbf0e 100644 --- a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift @@ -45,6 +45,7 @@ class VLCPlayerViewController: UIViewController { private var panBeganBrightness = CGFloat.zero private var panBeganVolumeValue = Float.zero + private var panBeganSliderPercentage: Double = 0 private var panBeganPoint = CGPoint.zero private var tapLocationStack = [CGPoint]() private var isJumping = false @@ -236,7 +237,8 @@ class VLCPlayerViewController: UIViewController { let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(didPinch(_:))) - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPan(_:))) + let verticalGesture = PanDirectionGestureRecognizer(direction: .vertical, target: self, action: #selector(didVerticalPan(_:))) + let horizontalGesture = PanDirectionGestureRecognizer(direction: .horizontal, target: self, action: #selector(didHorizontalPan(_:))) let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress)) @@ -248,7 +250,11 @@ class VLCPlayerViewController: UIViewController { } if viewModel.systemControlGesturesEnabled { - view.addGestureRecognizer(panGesture) + view.addGestureRecognizer(verticalGesture) + } + + if viewModel.seekSlideGestureEnabled { + view.addGestureRecognizer(horizontalGesture) } return view @@ -322,7 +328,7 @@ class VLCPlayerViewController: UIViewController { } @objc - private func didPan(_ gestureRecognizer: UIPanGestureRecognizer) { + private func didVerticalPan(_ gestureRecognizer: UIPanGestureRecognizer) { switch gestureRecognizer.state { case .began: panBeganBrightness = UIScreen.main.brightness @@ -350,6 +356,26 @@ class VLCPlayerViewController: UIViewController { } } + @objc + private func didHorizontalPan(_ gestureRecognizer: UIPanGestureRecognizer) { + switch gestureRecognizer.state { + case .began: + panBeganPoint = gestureRecognizer.location(in: mainGestureView) + panBeganSliderPercentage = viewModel.sliderPercentage + viewModel.sliderIsScrubbing = true + case .changed: + let pos = gestureRecognizer.location(in: mainGestureView) + let moveDelta = panBeganPoint.x - pos.x + let changedValue = (moveDelta / mainGestureView.frame.width) + + viewModel.sliderPercentage = min(max(0, panBeganSliderPercentage - changedValue), 1) + showSliderOverlay() + default: + viewModel.sliderIsScrubbing = false + hideSystemControlOverlay() + } + } + // MARK: setupOverlayHostingController private func setupOverlayHostingController(viewModel: VideoPlayerViewModel) { @@ -746,6 +772,25 @@ extension VLCPlayerViewController { } } + private func showSliderOverlay() { + guard !displayingOverlay else { return } + + let imageAttachment = NSTextAttachment() + imageAttachment.image = UIImage(systemName: "clock.arrow.circlepath", + withConfiguration: UIImage.SymbolConfiguration(pointSize: 48))? + .withTintColor(.white) + + let attributedString = NSMutableAttributedString() + attributedString.append(.init(attachment: imageAttachment)) + attributedString.append(.init(string: " \(viewModel.scrubbingTimeLabelText) (\(viewModel.leftLabelText))")) + systemControlOverlayLabel.attributedText = attributedString + systemControlOverlayLabel.layer.removeAllAnimations() + + UIView.animate(withDuration: 0.1) { + self.systemControlOverlayLabel.alpha = 1 + } + } + private func hideSystemControlOverlay() { UIView.animate(withDuration: 0.75) { self.systemControlOverlayLabel.alpha = 0 @@ -1030,7 +1075,7 @@ extension VLCPlayerViewController: PlayerOverlayDelegate { } return } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.33) { [weak self] in guard let self = self else { return } guard !self.tapLocationStack.isEmpty else { return } self.tapLocationStack.removeFirst() diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 3f710b26d0f95d8851e38d893fb8d31af62f394b..c5e7a5181b4332c8ce7e5b16225cb6922b457d1f 100644 GIT binary patch delta 54 vcmcbWd@g0f83n;&hEyQTW(a1;VaQ}iVMv`Es-P|$3>8&Ch;Lr3U@iy%yGagD delta 12 TcmX??aw~bm8HLRbihP0qD&hq{ From 28c6f9e760810e8cff71daec4c668018df709b11 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Sat, 14 May 2022 11:40:50 +0900 Subject: [PATCH 2/2] Improvement UI/UX in scrubbing --- .../VideoPlayerViewModel.swift | 14 +++++++++--- .../Overlays/VLCPlayerOverlayView.swift | 1 + .../VideoPlayer/VLCPlayerViewController.swift | 22 ++++++++++++++----- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift index 87b8d54f..5c8a6ef3 100644 --- a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift @@ -74,9 +74,13 @@ final class VideoPlayerViewModel: ViewModel { } } + @Published + var isHiddenCenterViews = false + @Published var sliderIsScrubbing: Bool = false { didSet { + isHiddenCenterViews = sliderIsScrubbing beganScrubbingCurrentSeconds = currentSeconds } } @@ -280,10 +284,10 @@ final class VideoPlayerViewModel: ViewModel { leftLabelText = calculateTimeText(from: currentSeconds) rightLabelText = calculateTimeText(from: secondsScrubbedRemaining) - scrubbingTimeLabelText = calculateTimeText(from: currentSeconds - beganScrubbingCurrentSeconds) + scrubbingTimeLabelText = calculateTimeText(from: currentSeconds - beganScrubbingCurrentSeconds, isScrubbing: true) } - private func calculateTimeText(from duration: Double) -> String { + private func calculateTimeText(from duration: Double, isScrubbing: Bool = false) -> String { let isNegative = duration < 0 let duration = abs(duration) let hours = floor(duration / 3600) @@ -300,7 +304,11 @@ final class VideoPlayerViewModel: ViewModel { "\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))" } - return "\(isNegative ? "-" : "") \(timeText)" + if isScrubbing { + return "\(isNegative ? "-" : "+") \(timeText)" + } else { + return "\(isNegative ? "-" : "") \(timeText)" + } } } diff --git a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift index 3ec74867..2cb6ba19 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift @@ -323,6 +323,7 @@ struct VLCPlayerOverlayView: View { } } .font(.system(size: 48)) + .opacity(viewModel.isHiddenCenterViews ? 0 : 1) } Spacer() diff --git a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift index bf0dbf0e..cc0bee83 100644 --- a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift @@ -360,6 +360,7 @@ class VLCPlayerViewController: UIViewController { private func didHorizontalPan(_ gestureRecognizer: UIPanGestureRecognizer) { switch gestureRecognizer.state { case .began: + exchangeOverlayView(isBringToFrontThanGestureView: false) panBeganPoint = gestureRecognizer.location(in: mainGestureView) panBeganSliderPercentage = viewModel.sliderPercentage viewModel.sliderIsScrubbing = true @@ -370,8 +371,10 @@ class VLCPlayerViewController: UIViewController { viewModel.sliderPercentage = min(max(0, panBeganSliderPercentage - changedValue), 1) showSliderOverlay() + showOverlay() default: viewModel.sliderIsScrubbing = false + hideOverlay() hideSystemControlOverlay() } } @@ -502,6 +505,17 @@ class VLCPlayerViewController: UIViewController { currentJumpForwardOverlayView = newJumpForwardImageView } + + private var isOverlayViewBringToFrontThanGestureView = true + private func exchangeOverlayView(isBringToFrontThanGestureView: Bool) { + guard isBringToFrontThanGestureView != isOverlayViewBringToFrontThanGestureView, + let currentOverlayView = currentOverlayHostingController?.view, + let mainGestureViewIndex = view.subviews.firstIndex(of: mainGestureView), + let currentOVerlayViewIndex = view.subviews.firstIndex(of: currentOverlayView) else { return } + isOverlayViewBringToFrontThanGestureView = isBringToFrontThanGestureView + view.exchangeSubview(at: mainGestureViewIndex, + withSubviewAt: currentOVerlayViewIndex) + } } // MARK: setupMediaPlayer @@ -679,14 +693,12 @@ extension VLCPlayerViewController { guard overlayHostingController.view.alpha != 0 else { return } // for gestures UX - view.exchangeSubview(at: view.subviews.firstIndex(of: mainGestureView)!, - withSubviewAt: view.subviews.firstIndex(of: overlayHostingController.view)!) + exchangeOverlayView(isBringToFrontThanGestureView: false) 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.exchangeOverlayView(isBringToFrontThanGestureView: true) self.viewModel.isHiddenOverlay = true } } @@ -773,8 +785,6 @@ extension VLCPlayerViewController { } private func showSliderOverlay() { - guard !displayingOverlay else { return } - let imageAttachment = NSTextAttachment() imageAttachment.image = UIImage(systemName: "clock.arrow.circlepath", withConfiguration: UIImage.SymbolConfiguration(pointSize: 48))?