mirror of
https://github.com/jellyfin/Swiftfin.git
synced 2025-02-22 16:23:57 +00:00
Add seek slide gesture
This commit is contained in:
parent
92e3c47214
commit
7a26e69685
@ -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
|
||||
|
@ -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<HTTPScheme>("defaultHTTPScheme", default: .http, 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 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 autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto",
|
||||
static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode",
|
||||
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
|
||||
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 systemControlGesturesEnabled = Key<Bool>("systemControlGesturesEnabled", default: true,
|
||||
static let systemControlGesturesEnabled = Key<Bool>("systemControlGesturesEnabled",
|
||||
default: true,
|
||||
suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let playerGesturesLockGestureEnabled = Key<Bool>("playerGesturesLockGestureEnabled", default: true,
|
||||
static let playerGesturesLockGestureEnabled = Key<Bool>("playerGesturesLockGestureEnabled",
|
||||
default: true,
|
||||
suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen,
|
||||
static let seekSlideGestureEnabled = Key<Bool>("seekSlideGestureEnabled",
|
||||
default: true,
|
||||
suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward",
|
||||
default: .fifteen,
|
||||
suite: SwiftfinStore.Defaults.generalSuite)
|
||||
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen,
|
||||
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward",
|
||||
default: .fifteen,
|
||||
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)
|
||||
@ -68,12 +69,14 @@ extension Defaults.Keys {
|
||||
static let shouldShowMissingEpisodes = Key<Bool>("shouldShowMissingEpisodes", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||
|
||||
// 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)
|
||||
|
||||
// Experimental settings
|
||||
enum Experimental {
|
||||
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false,
|
||||
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState",
|
||||
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)
|
||||
|
39
Shared/UIKit/PanDirectionGestureRecognizer.swift
Normal file
39
Shared/UIKit/PanDirectionGestureRecognizer.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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 = "<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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -1107,6 +1109,7 @@
|
||||
535870752669D60C00D05A09 /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
625534272821908D0087FE20 /* UIKit */,
|
||||
6286F09F271C0AA500C40ED5 /* Generated */,
|
||||
62C29E9D26D0FE5900C1D2E7 /* Coordinators */,
|
||||
E1FCD08E26C466F3007C8DCF /* Errors */,
|
||||
@ -1430,6 +1433,14 @@
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
625534272821908D0087FE20 /* UIKit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
62553428282190A00087FE20 /* PanDirectionGestureRecognizer.swift */,
|
||||
);
|
||||
path = UIKit;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user