Merge pull request #428 from PangMo5/PangMo5/player-slider-pan-gesture

This commit is contained in:
Kwangmin Bae 2022-05-15 16:40:27 +09:00 committed by GitHub
commit d91fb73822
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 181 additions and 55 deletions

View File

@ -310,6 +310,8 @@ internal enum L10n {
internal static var seasons: String { return L10n.tr("Localizable", "seasons") } internal static var seasons: String { return L10n.tr("Localizable", "seasons") }
/// See All /// See All
internal static var seeAll: String { return L10n.tr("Localizable", "seeAll") } 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 /// See More
internal static var seeMore: String { return L10n.tr("Localizable", "seeMore") } internal static var seeMore: String { return L10n.tr("Localizable", "seeMore") }
/// Select Cast Destination /// Select Cast Destination

View File

@ -10,21 +10,14 @@ import Defaults
import Foundation import Foundation
extension SwiftfinStore { extension SwiftfinStore {
enum Defaults { enum Defaults {
static let generalSuite: UserDefaults = .init(suiteName: "swiftfinstore-general-defaults")!
static let generalSuite: UserDefaults = { static let universalSuite: UserDefaults = .init(suiteName: "swiftfinstore-universal-defaults")!
UserDefaults(suiteName: "swiftfinstore-general-defaults")!
}()
static let universalSuite: UserDefaults = {
UserDefaults(suiteName: "swiftfinstore-universal-defaults")!
}()
} }
} }
extension Defaults.Keys { extension Defaults.Keys {
// Universal settings // Universal settings
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite) static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite)
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite) static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite)
@ -34,7 +27,8 @@ extension Defaults.Keys {
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite) static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite) static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto", static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode",
default: "Auto",
suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite) static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
@ -46,13 +40,20 @@ extension Defaults.Keys {
// Video player / overlay settings // Video player / overlay settings
static let overlayType = Key<OverlayType>("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite) static let overlayType = Key<OverlayType>("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite)
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, static let playerGesturesLockGestureEnabled = Key<Bool>("playerGesturesLockGestureEnabled",
default: true,
suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen, static let seekSlideGestureEnabled = Key<Bool>("seekSlideGestureEnabled",
default: true,
suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen, static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward",
default: .fifteen,
suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward",
default: .fifteen,
suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite)
@ -68,12 +69,14 @@ extension Defaults.Keys {
static let shouldShowMissingEpisodes = Key<Bool>("shouldShowMissingEpisodes", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let shouldShowMissingEpisodes = Key<Bool>("shouldShowMissingEpisodes", default: true, suite: SwiftfinStore.Defaults.generalSuite)
// Should show video player items in overlay menu // Should show video player items in overlay menu
static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu", default: true, static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu",
default: true,
suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
// Experimental settings // Experimental settings
enum Experimental { enum Experimental {
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false, static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState",
default: false,
suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let forceDirectPlay = Key<Bool>("forceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let forceDirectPlay = Key<Bool>("forceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let nativePlayer = Key<Bool>("nativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let nativePlayer = Key<Bool>("nativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite)

View File

@ -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<UITouch>, 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
}
}
}
}

View File

@ -20,7 +20,6 @@ import UIKit
#endif #endif
final class VideoPlayerViewModel: ViewModel { final class VideoPlayerViewModel: ViewModel {
// MARK: Published // MARK: Published
// Manually kept state because VLCKit doesn't properly set "played" // Manually kept state because VLCKit doesn't properly set "played"
@ -32,6 +31,8 @@ final class VideoPlayerViewModel: ViewModel {
@Published @Published
var rightLabelText: String = "--:--" var rightLabelText: String = "--:--"
@Published @Published
var scrubbingTimeLabelText: String = "--:--"
@Published
var playbackSpeed: PlaybackSpeed = .one var playbackSpeed: PlaybackSpeed = .one
@Published @Published
var subtitlesEnabled: Bool { var subtitlesEnabled: Bool {
@ -74,7 +75,16 @@ final class VideoPlayerViewModel: ViewModel {
} }
@Published @Published
var sliderIsScrubbing: Bool = false var isHiddenCenterViews = false
@Published
var sliderIsScrubbing: Bool = false {
didSet {
isHiddenCenterViews = sliderIsScrubbing
beganScrubbingCurrentSeconds = currentSeconds
}
}
@Published @Published
var sliderPercentage: Double = 0 { var sliderPercentage: Double = 0 {
willSet { willSet {
@ -119,6 +129,7 @@ final class VideoPlayerViewModel: ViewModel {
let overlayType: OverlayType let overlayType: OverlayType
let jumpGesturesEnabled: Bool let jumpGesturesEnabled: Bool
let systemControlGesturesEnabled: Bool let systemControlGesturesEnabled: Bool
let seekSlideGestureEnabled: Bool
let playerGesturesLockGestureEnabled: Bool let playerGesturesLockGestureEnabled: Bool
let resumeOffset: Bool let resumeOffset: Bool
let streamType: ServerStreamType let streamType: ServerStreamType
@ -144,6 +155,8 @@ final class VideoPlayerViewModel: ViewModel {
// MARK: Current Time // MARK: Current Time
private var beganScrubbingCurrentSeconds: Double = 0
var currentSeconds: Double { var currentSeconds: Double {
let runTimeTicks = item.runTimeTicks ?? 0 let runTimeTicks = item.runTimeTicks ?? 0
let videoDuration = Double(runTimeTicks / 10_000_000) let videoDuration = Double(runTimeTicks / 10_000_000)
@ -173,12 +186,11 @@ final class VideoPlayerViewModel: ViewModel {
} }
var currentChapter: ChapterInfo? { var currentChapter: ChapterInfo? {
let chapterPairs = chapters.adjacentPairs().map { ($0, $1) } let chapterPairs = chapters.adjacentPairs().map { ($0, $1) }
let chapterRanges = chapterPairs.map { ($0.startPositionTicks ?? 0, ($1.startPositionTicks ?? 1) - 1) } let chapterRanges = chapterPairs.map { ($0.startPositionTicks ?? 0, ($1.startPositionTicks ?? 1) - 1) }
for chapterRangeIndex in 0 ..< chapterRanges.count { for chapterRangeIndex in 0 ..< chapterRanges.count {
if chapterRanges[chapterRangeIndex].0 <= currentSecondTicks && if chapterRanges[chapterRangeIndex].0 <= currentSecondTicks,
currentSecondTicks < chapterRanges[chapterRangeIndex].1 currentSecondTicks < chapterRanges[chapterRangeIndex].1
{ {
return chapterPairs[chapterRangeIndex].0 return chapterPairs[chapterRangeIndex].0
@ -249,6 +261,7 @@ final class VideoPlayerViewModel: ViewModel {
self.jumpGesturesEnabled = Defaults[.jumpGesturesEnabled] self.jumpGesturesEnabled = Defaults[.jumpGesturesEnabled]
self.systemControlGesturesEnabled = Defaults[.systemControlGesturesEnabled] self.systemControlGesturesEnabled = Defaults[.systemControlGesturesEnabled]
self.playerGesturesLockGestureEnabled = Defaults[.playerGesturesLockGestureEnabled] self.playerGesturesLockGestureEnabled = Defaults[.playerGesturesLockGestureEnabled]
self.seekSlideGestureEnabled = Defaults[.seekSlideGestureEnabled]
self.shouldShowJumpButtonsInOverlayMenu = Defaults[.shouldShowJumpButtonsInOverlayMenu] self.shouldShowJumpButtonsInOverlayMenu = Defaults[.shouldShowJumpButtonsInOverlayMenu]
self.resumeOffset = Defaults[.resumeOffset] self.resumeOffset = Defaults[.resumeOffset]
@ -271,9 +284,12 @@ final class VideoPlayerViewModel: ViewModel {
leftLabelText = calculateTimeText(from: currentSeconds) leftLabelText = calculateTimeText(from: currentSeconds)
rightLabelText = calculateTimeText(from: secondsScrubbedRemaining) rightLabelText = calculateTimeText(from: secondsScrubbedRemaining)
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) let hours = floor(duration / 3600)
let minutes = duration.truncatingRemainder(dividingBy: 3600) / 60 let minutes = duration.truncatingRemainder(dividingBy: 3600) / 60
let seconds = duration.truncatingRemainder(dividingBy: 3600).truncatingRemainder(dividingBy: 60) let seconds = duration.truncatingRemainder(dividingBy: 3600).truncatingRemainder(dividingBy: 60)
@ -288,17 +304,19 @@ final class VideoPlayerViewModel: ViewModel {
"\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))" "\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))"
} }
return timeText if isScrubbing {
return "\(isNegative ? "-" : "+") \(timeText)"
} else {
return "\(isNegative ? "-" : "") \(timeText)"
}
} }
} }
// MARK: Injected Values // MARK: Injected Values
extension VideoPlayerViewModel { extension VideoPlayerViewModel {
// Injects custom values that override certain settings // Injects custom values that override certain settings
func injectCustomValues(startFromBeginning: Bool = false) { func injectCustomValues(startFromBeginning: Bool = false) {
if startFromBeginning { if startFromBeginning {
item.userData?.playbackPositionTicks = 0 item.userData?.playbackPositionTicks = 0
item.userData?.playedPercentage = 0 item.userData?.playedPercentage = 0
@ -311,7 +329,6 @@ extension VideoPlayerViewModel {
// MARK: Adjacent Items // MARK: Adjacent Items
extension VideoPlayerViewModel { extension VideoPlayerViewModel {
func getAdjacentEpisodes() { func getAdjacentEpisodes() {
guard let seriesID = item.seriesId, item.itemType == .episode else { return } guard let seriesID = item.seriesId, item.itemType == .episode else { return }
@ -412,21 +429,21 @@ extension VideoPlayerViewModel {
guard let masterSubtitleStream = masterViewModel.subtitleStreams guard let masterSubtitleStream = masterViewModel.subtitleStreams
.first(where: { $0.index == masterViewModel.selectedSubtitleStreamIndex }), .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 } let matchingSubtitleStreamIndex = matchingSubtitleStream.index else { return }
self.selectedSubtitleStreamIndex = matchingSubtitleStreamIndex selectedSubtitleStreamIndex = matchingSubtitleStreamIndex
} }
private func matchAudioStream(with masterViewModel: VideoPlayerViewModel) { private func matchAudioStream(with masterViewModel: VideoPlayerViewModel) {
guard let currentAudioStream = masterViewModel.audioStreams.first(where: { $0.index == masterViewModel.selectedAudioStreamIndex }), 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) { private func matchSubtitlesEnabled(with masterViewModel: VideoPlayerViewModel) {
self.subtitlesEnabled = masterViewModel.subtitlesEnabled subtitlesEnabled = masterViewModel.subtitlesEnabled
} }
private func mediaStreamAboutEqual(_ lhs: MediaStream, _ rhs: MediaStream) -> Bool { private func mediaStreamAboutEqual(_ lhs: MediaStream, _ rhs: MediaStream) -> Bool {
@ -437,23 +454,23 @@ extension VideoPlayerViewModel {
// MARK: Progress Report Timer // MARK: Progress Report Timer
extension VideoPlayerViewModel { extension VideoPlayerViewModel {
private func sendNewProgressReportWithTimer() { private func sendNewProgressReportWithTimer() {
self.progressReportTimer?.invalidate() progressReportTimer?.invalidate()
self.progressReportTimer = Timer.scheduledTimer(timeInterval: 0.7, target: self, selector: #selector(_sendProgressReport), progressReportTimer = Timer.scheduledTimer(timeInterval: 0.7,
userInfo: nil, repeats: false) target: self,
selector: #selector(_sendProgressReport),
userInfo: nil,
repeats: false)
} }
} }
// MARK: Updates // MARK: Updates
extension VideoPlayerViewModel { extension VideoPlayerViewModel {
// MARK: sendPlayReport // MARK: sendPlayReport
func sendPlayReport() { func sendPlayReport() {
startTimeTicks = Int64(Date().timeIntervalSince1970) * 10_000_000
self.startTimeTicks = Int64(Date().timeIntervalSince1970) * 10_000_000
let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil
@ -490,7 +507,6 @@ extension VideoPlayerViewModel {
// MARK: sendPauseReport // MARK: sendPauseReport
func sendPauseReport(paused: Bool) { func sendPauseReport(paused: Bool) {
let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil
let pauseInfo = PlaybackStartInfo(canSeek: true, let pauseInfo = PlaybackStartInfo(canSeek: true,
@ -526,7 +542,6 @@ extension VideoPlayerViewModel {
// MARK: sendProgressReport // MARK: sendProgressReport
func sendProgressReport() { func sendProgressReport() {
let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil let subtitleStreamIndex = subtitlesEnabled ? selectedSubtitleStreamIndex : nil
let progressInfo = PlaybackProgressInfo(canSeek: true, let progressInfo = PlaybackProgressInfo(canSeek: true,
@ -550,9 +565,9 @@ extension VideoPlayerViewModel {
nowPlayingQueue: nil, nowPlayingQueue: nil,
playlistItemId: "playlistItem0") playlistItemId: "playlistItem0")
self.lastProgressReport = progressInfo lastProgressReport = progressInfo
self.sendNewProgressReportWithTimer() sendNewProgressReportWithTimer()
} }
@objc @objc
@ -573,7 +588,6 @@ extension VideoPlayerViewModel {
// MARK: sendStopReport // MARK: sendStopReport
func sendStopReport() { func sendStopReport() {
let stopInfo = PlaybackStopInfo(item: item, let stopInfo = PlaybackStopInfo(item: item,
itemId: item.id, itemId: item.id,
sessionId: response.playSessionId, sessionId: response.playSessionId,
@ -600,9 +614,7 @@ extension VideoPlayerViewModel {
// MARK: Embedded/Normal Subtitle Streams // MARK: Embedded/Normal Subtitle Streams
extension VideoPlayerViewModel { extension VideoPlayerViewModel {
func createEmbeddedSubtitleStream(with subtitleStream: MediaStream) -> URL { func createEmbeddedSubtitleStream(with subtitleStream: MediaStream) -> URL {
guard let baseURL = URLComponents(url: directStreamURL, resolvingAgainstBaseURL: false) else { fatalError() } guard let baseURL = URLComponents(url: directStreamURL, resolvingAgainstBaseURL: false) else { fatalError() }
guard let queryItems = baseURL.queryItems else { fatalError() } guard let queryItems = baseURL.queryItems else { fatalError() }
@ -622,7 +634,6 @@ extension VideoPlayerViewModel {
// MARK: Equatable // MARK: Equatable
extension VideoPlayerViewModel: Equatable { extension VideoPlayerViewModel: Equatable {
static func == (lhs: VideoPlayerViewModel, rhs: VideoPlayerViewModel) -> Bool { static func == (lhs: VideoPlayerViewModel, rhs: VideoPlayerViewModel) -> Bool {
lhs.item.id == rhs.item.id && lhs.item.id == rhs.item.id &&
lhs.item.userData?.playbackPositionTicks == rhs.item.userData?.playbackPositionTicks lhs.item.userData?.playbackPositionTicks == rhs.item.userData?.playbackPositionTicks
@ -632,7 +643,6 @@ extension VideoPlayerViewModel: Equatable {
// MARK: Hashable // MARK: Hashable
extension VideoPlayerViewModel: Hashable { extension VideoPlayerViewModel: Hashable {
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
hasher.combine(item) hasher.combine(item)
hasher.combine(directStreamURL) hasher.combine(directStreamURL)

View File

@ -155,6 +155,7 @@
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */; }; 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */; };
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.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 */; }; 625CB56F2678C23300530A6E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56E2678C23300530A6E /* HomeView.swift */; };
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5722678C32A00530A6E /* HomeViewModel.swift */; }; 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5722678C32A00530A6E /* HomeViewModel.swift */; };
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5742678C33500530A6E /* LibraryListViewModel.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 = "<group>"; }; 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppURLHandler.swift; sourceTree = "<group>"; };
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = "<group>"; }; 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = "<group>"; };
624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = "<group>"; }; 624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = "<group>"; };
62553428282190A00087FE20 /* PanDirectionGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanDirectionGestureRecognizer.swift; sourceTree = "<group>"; };
625CB56E2678C23300530A6E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; }; 625CB56E2678C23300530A6E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
625CB5722678C32A00530A6E /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; }; 625CB5722678C32A00530A6E /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
625CB5742678C33500530A6E /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = "<group>"; }; 625CB5742678C33500530A6E /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = "<group>"; };
@ -1107,6 +1109,7 @@
535870752669D60C00D05A09 /* Shared */ = { 535870752669D60C00D05A09 /* Shared */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
625534272821908D0087FE20 /* UIKit */,
6286F09F271C0AA500C40ED5 /* Generated */, 6286F09F271C0AA500C40ED5 /* Generated */,
62C29E9D26D0FE5900C1D2E7 /* Coordinators */, 62C29E9D26D0FE5900C1D2E7 /* Coordinators */,
E1FCD08E26C466F3007C8DCF /* Errors */, E1FCD08E26C466F3007C8DCF /* Errors */,
@ -1430,6 +1433,14 @@
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
625534272821908D0087FE20 /* UIKit */ = {
isa = PBXGroup;
children = (
62553428282190A00087FE20 /* PanDirectionGestureRecognizer.swift */,
);
path = UIKit;
sourceTree = "<group>";
};
6286F09F271C0AA500C40ED5 /* Generated */ = { 6286F09F271C0AA500C40ED5 /* Generated */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -2498,6 +2509,7 @@
09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */, 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */,
E1D4BF872719D27100A11E64 /* Bitrates.swift in Sources */, E1D4BF872719D27100A11E64 /* Bitrates.swift in Sources */,
6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */,
62553429282190A00087FE20 /* PanDirectionGestureRecognizer.swift in Sources */,
E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */,
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */, 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
E10D87DE278510E400BD264C /* PosterSize.swift in Sources */, E10D87DE278510E400BD264C /* PosterSize.swift in Sources */,

View File

@ -42,6 +42,8 @@ struct SettingsView: View {
var systemControlGesturesEnabled var systemControlGesturesEnabled
@Default(.playerGesturesLockGestureEnabled) @Default(.playerGesturesLockGestureEnabled)
var playerGesturesLockGestureEnabled var playerGesturesLockGestureEnabled
@Default(.seekSlideGestureEnabled)
var seekSlideGestureEnabled
@Default(.resumeOffset) @Default(.resumeOffset)
var resumeOffset var resumeOffset
@Default(.subtitleSize) @Default(.subtitleSize)
@ -113,6 +115,8 @@ struct SettingsView: View {
Toggle(L10n.systemControlGesturesEnabled, isOn: $systemControlGesturesEnabled) Toggle(L10n.systemControlGesturesEnabled, isOn: $systemControlGesturesEnabled)
Toggle(L10n.seekSlideGestureEnabled, isOn: $seekSlideGestureEnabled)
Toggle(L10n.playerGesturesLockGestureEnabled, isOn: $playerGesturesLockGestureEnabled) Toggle(L10n.playerGesturesLockGestureEnabled, isOn: $playerGesturesLockGestureEnabled)
Toggle(L10n.resume5SecondOffset, isOn: $resumeOffset) Toggle(L10n.resume5SecondOffset, isOn: $resumeOffset)

View File

@ -323,6 +323,7 @@ struct VLCPlayerOverlayView: View {
} }
} }
.font(.system(size: 48)) .font(.system(size: 48))
.opacity(viewModel.isHiddenCenterViews ? 0 : 1)
} }
Spacer() Spacer()

View File

@ -45,6 +45,7 @@ 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 panBeganSliderPercentage: Double = 0
private var panBeganPoint = CGPoint.zero private var panBeganPoint = CGPoint.zero
private var tapLocationStack = [CGPoint]() private var tapLocationStack = [CGPoint]()
private var isJumping = false private var isJumping = false
@ -236,7 +237,8 @@ class VLCPlayerViewController: UIViewController {
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 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)) let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress))
@ -248,7 +250,11 @@ class VLCPlayerViewController: UIViewController {
} }
if viewModel.systemControlGesturesEnabled { if viewModel.systemControlGesturesEnabled {
view.addGestureRecognizer(panGesture) view.addGestureRecognizer(verticalGesture)
}
if viewModel.seekSlideGestureEnabled {
view.addGestureRecognizer(horizontalGesture)
} }
return view return view
@ -322,7 +328,7 @@ class VLCPlayerViewController: UIViewController {
} }
@objc @objc
private func didPan(_ gestureRecognizer: UIPanGestureRecognizer) { private func didVerticalPan(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state { switch gestureRecognizer.state {
case .began: case .began:
panBeganBrightness = UIScreen.main.brightness panBeganBrightness = UIScreen.main.brightness
@ -350,6 +356,29 @@ class VLCPlayerViewController: UIViewController {
} }
} }
@objc
private func didHorizontalPan(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
exchangeOverlayView(isBringToFrontThanGestureView: false)
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()
showOverlay()
default:
viewModel.sliderIsScrubbing = false
hideOverlay()
hideSystemControlOverlay()
}
}
// MARK: setupOverlayHostingController // MARK: setupOverlayHostingController
private func setupOverlayHostingController(viewModel: VideoPlayerViewModel) { private func setupOverlayHostingController(viewModel: VideoPlayerViewModel) {
@ -476,6 +505,17 @@ class VLCPlayerViewController: UIViewController {
currentJumpForwardOverlayView = newJumpForwardImageView 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 // MARK: setupMediaPlayer
@ -653,14 +693,12 @@ extension VLCPlayerViewController {
guard overlayHostingController.view.alpha != 0 else { return } guard overlayHostingController.view.alpha != 0 else { return }
// for gestures UX // for gestures UX
view.exchangeSubview(at: view.subviews.firstIndex(of: mainGestureView)!, exchangeOverlayView(isBringToFrontThanGestureView: false)
withSubviewAt: view.subviews.firstIndex(of: overlayHostingController.view)!)
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut) { UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut) {
overlayHostingController.view.alpha = 0 overlayHostingController.view.alpha = 0
} completion: { [weak self] _ in } completion: { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
self.view.exchangeSubview(at: self.view.subviews.firstIndex(of: self.mainGestureView)!, self.exchangeOverlayView(isBringToFrontThanGestureView: true)
withSubviewAt: self.view.subviews.firstIndex(of: overlayHostingController.view)!)
self.viewModel.isHiddenOverlay = true self.viewModel.isHiddenOverlay = true
} }
} }
@ -746,6 +784,23 @@ extension VLCPlayerViewController {
} }
} }
private func showSliderOverlay() {
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() { private func hideSystemControlOverlay() {
UIView.animate(withDuration: 0.75) { UIView.animate(withDuration: 0.75) {
self.systemControlOverlayLabel.alpha = 0 self.systemControlOverlayLabel.alpha = 0
@ -1030,7 +1085,7 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
} }
return return
} else { } 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 let self = self else { return }
guard !self.tapLocationStack.isEmpty else { return } guard !self.tapLocationStack.isEmpty else { return }
self.tapLocationStack.removeFirst() self.tapLocationStack.removeFirst()