diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/VerticalBrightnessSlider.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/VerticalBrightnessSlider.swift new file mode 100644 index 0000000..1da58ea --- /dev/null +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Components/VerticalBrightnessSlider.swift @@ -0,0 +1,136 @@ +// +// VerticalBrightnessSlider.swift +// Custom Brighness bar +// +// Created by Pratik on 08/01/23. +// Modified to update screen brightness when used as a brightness slider. +// + +import SwiftUI + +struct VerticalBrightnessSlider: View { + @Binding var value: T + let inRange: ClosedRange + let activeFillColor: Color + let fillColor: Color + let emptyColor: Color + let width: CGFloat + let onEditingChanged: (Bool) -> Void + + // private variables + @State private var localRealProgress: T = 0 + @State private var localTempProgress: T = 0 + @GestureState private var isActive: Bool = false + + var body: some View { + GeometryReader { bounds in + ZStack { + GeometryReader { geo in + ZStack(alignment: .bottom) { + RoundedRectangle(cornerRadius: isActive ? width : width/2, style: .continuous) + .fill(emptyColor) + RoundedRectangle(cornerRadius: isActive ? width : width/2, style: .continuous) + .fill(isActive ? activeFillColor : fillColor) + .mask({ + VStack { + Spacer(minLength: 0) + Rectangle() + .frame(height: max(geo.size.height * CGFloat((localRealProgress + localTempProgress)), 0), + alignment: .leading) + } + }) + + Image(systemName: getIconName) + .font(.system(size: isActive ? 16 : 12, weight: .medium, design: .rounded)) + .foregroundColor(isActive ? fillColor : Color.white) + .animation(.spring(), value: isActive) + .frame(maxHeight: .infinity, alignment: .bottom) + .padding(.bottom) + .overlay { + Image(systemName: getIconName) + .font(.system(size: isActive ? 16 : 12, weight: .medium, design: .rounded)) + .foregroundColor(isActive ? Color.gray : Color.white.opacity(0.8)) + .animation(.spring(), value: isActive) + .frame(maxHeight: .infinity, alignment: .bottom) + .padding(.bottom) + .mask { + VStack { + Spacer(minLength: 0) + Rectangle() + .frame(height: max(geo.size.height * CGFloat((localRealProgress + localTempProgress)), 0), + alignment: .leading) + } + } + } + //.frame(maxWidth: isActive ? .infinity : 0) + // .opacity(isActive ? 1 : 0) + } + .clipped() + } + .frame(height: isActive ? bounds.size.height * 1.15 : bounds.size.height, alignment: .center) + // .shadow(color: .black.opacity(0.1), radius: isActive ? 20 : 0, x: 0, y: 0) + .animation(animation, value: isActive) + } + .frame(width: bounds.size.width, height: bounds.size.height, alignment: .center) + .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local) + .updating($isActive) { value, state, transaction in + state = true + } + .onChanged { gesture in + localTempProgress = T(-gesture.translation.height / bounds.size.height) + value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound) + } + .onEnded { _ in + localRealProgress = max(min(localRealProgress + localTempProgress, 1), 0) + localTempProgress = 0 + } + ) + .onChange(of: isActive) { newValue in + value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound) + onEditingChanged(newValue) + } + .onAppear { + localRealProgress = getPrgPercentage(value) + } + .onChange(of: value) { newValue in + if !isActive { + localRealProgress = getPrgPercentage(newValue) + } + } + } + .frame(width: isActive ? width * 1.9 : width, alignment: .center) + .offset(x: isActive ? -10 : 0) + .onChange(of: value) { newValue in + UIScreen.main.brightness = CGFloat(newValue) + } + } + + private var getIconName: String { + let brightnessLevel = CGFloat(localRealProgress + localTempProgress) + switch brightnessLevel { + case ..<0.2: + return "moon.fill" + case 0.2..<0.38: + return "sun.min" + case 0.38..<0.7: + return "sun.max" + default: + return "sun.max.fill" + } + } + + private var animation: Animation { + return .spring() + } + + private func getPrgPercentage(_ value: T) -> T { + let range = inRange.upperBound - inRange.lowerBound + let correctedStartValue = value - inRange.lowerBound + let percentage = correctedStartValue / range + return percentage + } + + private func getPrgValue() -> T { + return ((localRealProgress + localTempProgress) * (inRange.upperBound - inRange.lowerBound)) + inRange.lowerBound + } +} diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index c8c198f..5530058 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -40,8 +40,15 @@ class CustomMediaPlayerViewController: UIViewController { var currentTimeVal: Double = 0.0 var duration: Double = 0.0 var isVideoLoaded = false - var showWatchNextButton = true + var brightnessValue: Double = Double(UIScreen.main.brightness) + var brightnessSliderHostingController: UIHostingController>? + + private var isHoldPauseEnabled: Bool { + UserDefaults.standard.bool(forKey: "holdForPauseEnabled") + } + + var showWatchNextButton = true var watchNextButtonTimer: Timer? var isWatchNextRepositioned: Bool = false var isWatchNextVisible: Bool = false @@ -134,7 +141,9 @@ class CustomMediaPlayerViewController: UIViewController { let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(fullUrl)") if lastPlayedTime > 0 { let seekTime = CMTime(seconds: lastPlayedTime, preferredTimescale: 1) - self.player.seek(to: seekTime) + self.player.seek(to: seekTime) { [weak self] _ in + self?.updateBufferValue() + } } } @@ -151,6 +160,7 @@ class CustomMediaPlayerViewController: UIViewController { loadSubtitleSettings() setupPlayerViewController() setupControls() + brightnessControl() setupSkipAndDismissGestures() addInvisibleControlOverlays() setupSubtitleLabel() @@ -174,6 +184,10 @@ class CustomMediaPlayerViewController: UIViewController { self?.checkForHLSStream() } + if isHoldPauseEnabled { + holdForPause() + } + player.play() if let url = subtitlesURL, !url.isEmpty { @@ -405,6 +419,57 @@ class CustomMediaPlayerViewController: UIViewController { ]) } + func holdForPause() { + let holdForPauseGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleHoldForPause(_:))) + holdForPauseGesture.minimumPressDuration = 1 + holdForPauseGesture.numberOfTouchesRequired = 2 + view.addGestureRecognizer(holdForPauseGesture) + } + + func brightnessControl() { + let brightnessSlider = VerticalBrightnessSlider( + value: Binding( + get: { self.brightnessValue }, + set: { newValue in + self.brightnessValue = newValue + } + ), + inRange: 0...1, + activeFillColor: .white, + fillColor: .white.opacity(0.5), + emptyColor: .white.opacity(0.3), + width: 22, + onEditingChanged: { editing in + } + ) + + let brightnessContainer = UIView() + brightnessContainer.translatesAutoresizingMaskIntoConstraints = false + brightnessContainer.backgroundColor = .clear + + controlsContainerView.addSubview(brightnessContainer) + + NSLayoutConstraint.activate([ + brightnessContainer.leadingAnchor.constraint(equalTo: controlsContainerView.leadingAnchor, constant: -4), + brightnessContainer.centerYAnchor.constraint(equalTo: controlsContainerView.centerYAnchor, constant: -10), + brightnessContainer.widthAnchor.constraint(equalToConstant: 22), + brightnessContainer.heightAnchor.constraint(equalToConstant: 170) + ]) + + brightnessSliderHostingController = UIHostingController(rootView: brightnessSlider) + guard let brightnessSliderView = brightnessSliderHostingController?.view else { return } + brightnessSliderView.backgroundColor = .clear + brightnessSliderView.translatesAutoresizingMaskIntoConstraints = false + + brightnessContainer.addSubview(brightnessSliderView) + + NSLayoutConstraint.activate([ + brightnessSliderView.topAnchor.constraint(equalTo: brightnessContainer.topAnchor), + brightnessSliderView.bottomAnchor.constraint(equalTo: brightnessContainer.bottomAnchor), + brightnessSliderView.leadingAnchor.constraint(equalTo: brightnessContainer.leadingAnchor), + brightnessSliderView.trailingAnchor.constraint(equalTo: brightnessContainer.trailingAnchor) + ]) + } func addInvisibleControlOverlays() { let playPauseOverlay = UIButton(type: .custom) @@ -433,9 +498,8 @@ class CustomMediaPlayerViewController: UIViewController { } } - let swipeDownGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipeDown(_:))) - swipeDownGesture.direction = .down - view.addGestureRecognizer(swipeDownGesture) + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) + view.addGestureRecognizer(panGesture) } func showSkipFeedback(direction: String) { @@ -966,6 +1030,14 @@ class CustomMediaPlayerViewController: UIViewController { player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) } + @objc private func handleHoldForPause(_ gesture: UILongPressGestureRecognizer) { + guard isHoldPauseEnabled else { return } + + if gesture.state == .began { + togglePlayPause() + } + } + func speedChangerMenu() -> UIMenu { let speeds: [Double] = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0] let playbackSpeedActions = speeds.map { speed in @@ -1385,6 +1457,19 @@ class CustomMediaPlayerViewController: UIViewController { } } + @objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) { + let translation = gesture.translation(in: view) + + switch gesture.state { + case .ended: + if translation.y > 100 { + dismiss(animated: true, completion: nil) + } + default: + break + } + } + private func beginHoldSpeed() { guard let player = player else { return } originalRate = player.rate diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index 2792468..53c1ce9 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -15,6 +15,7 @@ struct SettingsViewPlayer: View { @AppStorage("holdSpeedPlayer") private var holdSpeedPlayer: Double = 2.0 @AppStorage("skipIncrement") private var skipIncrement: Double = 10.0 @AppStorage("skipIncrementHold") private var skipIncrementHold: Double = 30.0 + @AppStorage("holdForPauseEnabled") private var holdForPauseEnabled = false private let mediaPlayers = ["Default", "VLC", "OutPlayer", "Infuse", "nPlayer", "Sora"] @@ -40,6 +41,9 @@ struct SettingsViewPlayer: View { Toggle("Force Landscape", isOn: $isAlwaysLandscape) .tint(.accentColor) + + Toggle("Two Finger Hold for Pause",isOn: $holdForPauseEnabled) + .tint(.accentColor) } Section(header: Text("Speed Settings")) { diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 7915612..a0d0a49 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -62,6 +62,7 @@ 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */; }; 13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */; }; 13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; }; + 1E3F5EC82D9F16B7003F310F /* VerticalBrightnessSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E3F5EC72D9F16B7003F310F /* VerticalBrightnessSlider.swift */; }; 1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; }; 1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; }; 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */; }; @@ -123,6 +124,7 @@ 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = ""; }; 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = ""; }; + 1E3F5EC72D9F16B7003F310F /* VerticalBrightnessSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalBrightnessSlider.swift; sourceTree = ""; }; 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = ""; }; 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = ""; }; 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = ""; }; @@ -440,6 +442,7 @@ 13EA2BD22D32D97400C1EBD7 /* Components */ = { isa = PBXGroup; children = ( + 1E3F5EC72D9F16B7003F310F /* VerticalBrightnessSlider.swift */, 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */, 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */, ); @@ -571,6 +574,7 @@ 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */, 138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */, 1334FF4D2D786C93007E289F /* TMDB-Seasonal.swift in Sources */, + 1E3F5EC82D9F16B7003F310F /* VerticalBrightnessSlider.swift in Sources */, 13DB468D2D90093A008CBC03 /* Anilist-Login.swift in Sources */, 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */, 132AF1232D9995C300A0140B /* JSController-Details.swift in Sources */,