mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
volume slider 🤤 (#77)
This commit is contained in:
parent
df3a9e8f74
commit
f99dc3e6f1
7 changed files with 314 additions and 165 deletions
|
|
@ -8,6 +8,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
extension Double {
|
||||
func asTimeString(style: DateComponentsFormatter.UnitsStyle) -> String {
|
||||
|
|
@ -37,4 +38,9 @@ extension BinaryFloatingPoint {
|
|||
enum TimeStringStyle {
|
||||
case positional
|
||||
case standard
|
||||
}
|
||||
}
|
||||
|
||||
class VolumeViewModel: ObservableObject {
|
||||
@Published var value: Double = 0.0
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
|||
let inRange: ClosedRange<T>
|
||||
let activeFillColor: Color
|
||||
let fillColor: Color
|
||||
let textColor: Color
|
||||
let emptyColor: Color
|
||||
let height: CGFloat
|
||||
let onEditingChanged: (Bool) -> Void
|
||||
|
|
@ -25,7 +26,7 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
|||
var body: some View {
|
||||
GeometryReader { bounds in
|
||||
ZStack {
|
||||
VStack {
|
||||
VStack (spacing: 8) {
|
||||
ZStack(alignment: .center) {
|
||||
Capsule()
|
||||
.fill(emptyColor)
|
||||
|
|
@ -53,8 +54,8 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
|||
Text("-" + (inRange.upperBound - value)
|
||||
.asTimeString(style: .positional, showHours: shouldShowHours))
|
||||
}
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(isActive ? fillColor : emptyColor)
|
||||
.font(.system(size: 12.5))
|
||||
.foregroundColor(textColor)
|
||||
}
|
||||
.frame(width: isActive ? bounds.size.width * 1.04 : bounds.size.width, alignment: .center)
|
||||
.animation(animation, value: isActive)
|
||||
|
|
|
|||
|
|
@ -1,136 +0,0 @@
|
|||
//
|
||||
// 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<T: BinaryFloatingPoint>: View {
|
||||
@Binding var value: T
|
||||
let inRange: ClosedRange<T>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
//
|
||||
// VolumeSlider.swift
|
||||
// Custom Seekbar
|
||||
//
|
||||
// Created by Pratik on 08/01/23.
|
||||
// Credits to Pratik https://github.com/pratikg29/Custom-Slider-Control/blob/main/AppleMusicSlider/AppleMusicSlider/VolumeSlider.swift
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct VolumeSlider<T: BinaryFloatingPoint>: View {
|
||||
@Binding var value: T
|
||||
let inRange: ClosedRange<T>
|
||||
let activeFillColor: Color
|
||||
let fillColor: Color
|
||||
let emptyColor: Color
|
||||
let height: CGFloat
|
||||
let onEditingChanged: (Bool) -> Void
|
||||
|
||||
@State private var localRealProgress: T = 0
|
||||
@State private var localTempProgress: T = 0
|
||||
@State private var lastVolumeValue: T = 0
|
||||
@GestureState private var isActive: Bool = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { bounds in
|
||||
ZStack {
|
||||
HStack {
|
||||
GeometryReader { geo in
|
||||
ZStack(alignment: .center) {
|
||||
Capsule().fill(emptyColor)
|
||||
Capsule().fill(isActive ? activeFillColor : fillColor)
|
||||
.mask {
|
||||
HStack {
|
||||
Rectangle()
|
||||
.frame(
|
||||
width: max(geo.size.width * CGFloat(localRealProgress + localTempProgress), 0),
|
||||
alignment: .leading
|
||||
)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image(systemName: getIconName)
|
||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
||||
.frame(width: 30)
|
||||
.foregroundColor(isActive ? activeFillColor : fillColor)
|
||||
.onTapGesture {
|
||||
handleIconTap()
|
||||
}
|
||||
}
|
||||
.frame(width: isActive ? bounds.size.width * 1.02 : bounds.size.width, alignment: .center)
|
||||
.animation(animation, value: isActive)
|
||||
}
|
||||
.frame(width: bounds.size.width, height: bounds.size.height)
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 0, coordinateSpace: .local)
|
||||
.updating($isActive) { _, state, _ in state = true }
|
||||
.onChanged { gesture in
|
||||
let delta = gesture.translation.width / bounds.size.width
|
||||
localTempProgress = T(delta)
|
||||
value = sliderValueInRange()
|
||||
}
|
||||
.onEnded { _ in
|
||||
localRealProgress = max(min(localRealProgress + localTempProgress, 1), 0)
|
||||
localTempProgress = 0
|
||||
}
|
||||
)
|
||||
.onChange(of: isActive) { newValue in
|
||||
if !newValue {
|
||||
value = sliderValueInRange()
|
||||
}
|
||||
onEditingChanged(newValue)
|
||||
}
|
||||
.onAppear {
|
||||
localRealProgress = progress(for: value)
|
||||
if value > 0 {
|
||||
lastVolumeValue = value
|
||||
}
|
||||
}
|
||||
.onChange(of: value) { newVal in
|
||||
if !isActive {
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
localRealProgress = progress(for: newVal)
|
||||
}
|
||||
if newVal > 0 {
|
||||
lastVolumeValue = newVal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: isActive ? height * 1.25 : height)
|
||||
}
|
||||
|
||||
private var getIconName: String {
|
||||
let p = max(0, min(localRealProgress + localTempProgress, 1))
|
||||
let muteThreshold: T = 0
|
||||
let lowThreshold: T = 0.2
|
||||
let midThreshold: T = 0.35
|
||||
let highThreshold: T = 0.7
|
||||
|
||||
switch p {
|
||||
case muteThreshold:
|
||||
return "speaker.slash.fill"
|
||||
case muteThreshold..<lowThreshold:
|
||||
return "speaker.fill"
|
||||
case lowThreshold..<midThreshold:
|
||||
return "speaker.wave.1.fill"
|
||||
case midThreshold..<highThreshold:
|
||||
return "speaker.wave.2.fill"
|
||||
default:
|
||||
return "speaker.wave.3.fill"
|
||||
}
|
||||
}
|
||||
|
||||
private func handleIconTap() {
|
||||
let currentProgress = localRealProgress + localTempProgress
|
||||
|
||||
withAnimation {
|
||||
if currentProgress <= 0 {
|
||||
value = lastVolumeValue
|
||||
localRealProgress = progress(for: lastVolumeValue)
|
||||
localTempProgress = 0
|
||||
} else {
|
||||
lastVolumeValue = sliderValueInRange()
|
||||
value = T(0)
|
||||
localRealProgress = 0
|
||||
localTempProgress = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var animation: Animation {
|
||||
isActive
|
||||
? .spring()
|
||||
: .spring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.6)
|
||||
}
|
||||
|
||||
private func progress(for val: T) -> T {
|
||||
let totalRange = inRange.upperBound - inRange.lowerBound
|
||||
let adjustedVal = val - inRange.lowerBound
|
||||
return adjustedVal / totalRange
|
||||
}
|
||||
|
||||
private func sliderValueInRange() -> T {
|
||||
let totalProgress = localRealProgress + localTempProgress
|
||||
let rawVal = totalProgress * (inRange.upperBound - inRange.lowerBound)
|
||||
+ inRange.lowerBound
|
||||
return max(min(rawVal, inRange.upperBound), inRange.lowerBound)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import MarqueeLabel
|
|||
import AVKit
|
||||
import SwiftUI
|
||||
import AVFoundation
|
||||
import MediaPlayer
|
||||
|
||||
// MARK: - SliderViewModel
|
||||
|
||||
|
|
@ -59,6 +60,13 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
return UserDefaults.standard.bool(forKey: "skip85Visible")
|
||||
}
|
||||
|
||||
private var isDoubleTapSkipEnabled: Bool {
|
||||
if UserDefaults.standard.object(forKey: "doubleTapSeekEnabled") == nil {
|
||||
return true
|
||||
}
|
||||
return UserDefaults.standard.bool(forKey: "doubleTapSeekEnabled")
|
||||
}
|
||||
|
||||
var showWatchNextButton = true
|
||||
var watchNextButtonTimer: Timer?
|
||||
var isWatchNextRepositioned: Bool = false
|
||||
|
|
@ -123,6 +131,15 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
private var loadedTimeRangesObservation: NSKeyValueObservation?
|
||||
private var playerTimeControlStatusObserver: NSKeyValueObservation?
|
||||
|
||||
private var volumeObserver: NSKeyValueObservation?
|
||||
private var audioSession = AVAudioSession.sharedInstance()
|
||||
private var hiddenVolumeView = MPVolumeView(frame: .zero)
|
||||
private var systemVolumeSlider: UISlider?
|
||||
private var volumeValue: Double = 0.0
|
||||
private var volumeViewModel = VolumeViewModel()
|
||||
var volumeSliderHostingView: UIView?
|
||||
|
||||
|
||||
init(module: ScrapingModule,
|
||||
urlString: String,
|
||||
fullUrl: String,
|
||||
|
|
@ -186,6 +203,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
setupWatchNextButton()
|
||||
setupSubtitleLabel()
|
||||
setupDismissButton()
|
||||
volumeSlider()
|
||||
setupSpeedButton()
|
||||
setupQualityButton()
|
||||
setupMenuButton()
|
||||
|
|
@ -209,6 +227,24 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
holdForPause()
|
||||
}
|
||||
|
||||
do {
|
||||
try audioSession.setActive(true)
|
||||
} catch {
|
||||
print("Error activating audio session: \(error)")
|
||||
}
|
||||
|
||||
volumeViewModel.value = Double(audioSession.outputVolume)
|
||||
|
||||
|
||||
volumeObserver = audioSession.observe(\.outputVolume, options: [.new]) { [weak self] session, change in
|
||||
guard let newVol = change.newValue else { return }
|
||||
DispatchQueue.main.async {
|
||||
self?.volumeViewModel.value = Double(newVol)
|
||||
Logger.shared.log("Hardware volume changed, new value: \(newVol)", type: "Debug")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if #available(iOS 16.0, *) {
|
||||
playerViewController.allowsVideoFrameAnalysis = false
|
||||
}
|
||||
|
|
@ -226,6 +262,21 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
self.watchNextButton.alpha = 1.0
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
hiddenVolumeView.showsRouteButton = false
|
||||
hiddenVolumeView.isHidden = true
|
||||
view.addSubview(hiddenVolumeView)
|
||||
|
||||
hiddenVolumeView.translatesAutoresizingMaskIntoConstraints = false
|
||||
hiddenVolumeView.widthAnchor.constraint(equalToConstant: 1).isActive = true
|
||||
hiddenVolumeView.heightAnchor.constraint(equalToConstant: 1).isActive = true
|
||||
hiddenVolumeView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
|
||||
hiddenVolumeView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
|
||||
|
||||
|
||||
if let slider = hiddenVolumeView.subviews.first(where: { $0 is UISlider }) as? UISlider {
|
||||
systemVolumeSlider = slider
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
|
|
@ -244,7 +295,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
let availableWidth = marqueeLabel.frame.width
|
||||
let textWidth = marqueeLabel.intrinsicContentSize.width
|
||||
|
||||
|
||||
if textWidth > availableWidth {
|
||||
marqueeLabel.lineBreakMode = .byTruncatingTail
|
||||
} else {
|
||||
|
|
@ -429,9 +480,10 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
),
|
||||
inRange: 0...(duration > 0 ? duration : 1.0),
|
||||
activeFillColor: .white,
|
||||
fillColor: .white.opacity(0.5),
|
||||
fillColor: .white.opacity(0.6),
|
||||
textColor: .white.opacity(0.7),
|
||||
emptyColor: .white.opacity(0.3),
|
||||
height: 30,
|
||||
height: 33,
|
||||
onEditingChanged: { editing in
|
||||
if editing {
|
||||
self.isSliderEditing = true
|
||||
|
|
@ -493,7 +545,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
holdForPauseGesture.numberOfTouchesRequired = 2
|
||||
view.addGestureRecognizer(holdForPauseGesture)
|
||||
}
|
||||
|
||||
|
||||
func addInvisibleControlOverlays() {
|
||||
let playPauseOverlay = UIButton(type: .custom)
|
||||
playPauseOverlay.backgroundColor = .clear
|
||||
|
|
@ -507,16 +559,18 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
playPauseOverlay.heightAnchor.constraint(equalTo: playPauseButton.heightAnchor, constant: 20)
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
func setupSkipAndDismissGestures() {
|
||||
let doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
|
||||
doubleTapGesture.numberOfTapsRequired = 2
|
||||
view.addGestureRecognizer(doubleTapGesture)
|
||||
|
||||
if let gestures = view.gestureRecognizers {
|
||||
for gesture in gestures {
|
||||
if let tapGesture = gesture as? UITapGestureRecognizer, tapGesture.numberOfTapsRequired == 1 {
|
||||
tapGesture.require(toFail: doubleTapGesture)
|
||||
if isDoubleTapSkipEnabled {
|
||||
let doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
|
||||
doubleTapGesture.numberOfTapsRequired = 2
|
||||
view.addGestureRecognizer(doubleTapGesture)
|
||||
|
||||
if let gestures = view.gestureRecognizers {
|
||||
for gesture in gestures {
|
||||
if let tapGesture = gesture as? UITapGestureRecognizer, tapGesture.numberOfTapsRequired == 1 {
|
||||
tapGesture.require(toFail: doubleTapGesture)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -527,7 +581,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
func showSkipFeedback(direction: String) {
|
||||
let diameter: CGFloat = 600
|
||||
|
||||
|
||||
if let existingFeedback = view.viewWithTag(999) {
|
||||
existingFeedback.layer.removeAllAnimations()
|
||||
existingFeedback.removeFromSuperview()
|
||||
|
|
@ -540,7 +594,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
circleView.translatesAutoresizingMaskIntoConstraints = false
|
||||
circleView.isUserInteractionEnabled = false
|
||||
circleView.tag = 999
|
||||
|
||||
|
||||
let iconName = (direction == "forward") ? "goforward" : "gobackward"
|
||||
let imageView = UIImageView(image: UIImage(systemName: iconName))
|
||||
imageView.tintColor = .black
|
||||
|
|
@ -704,18 +758,49 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
]
|
||||
updateMarqueeConstraints()
|
||||
}
|
||||
|
||||
func volumeSlider() {
|
||||
let container = VolumeSliderContainer(volumeVM: self.volumeViewModel) { newVal in
|
||||
if let sysSlider = self.systemVolumeSlider {
|
||||
sysSlider.value = Float(newVal)
|
||||
}
|
||||
}
|
||||
|
||||
let hostingController = UIHostingController(rootView: container)
|
||||
hostingController.view.backgroundColor = UIColor.clear
|
||||
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
controlsContainerView.addSubview(hostingController.view)
|
||||
addChild(hostingController)
|
||||
hostingController.didMove(toParent: self)
|
||||
|
||||
self.volumeSliderHostingView = hostingController.view
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
hostingController.view.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor),
|
||||
hostingController.view.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -16),
|
||||
hostingController.view.widthAnchor.constraint(equalToConstant: 160),
|
||||
hostingController.view.heightAnchor.constraint(equalToConstant: 30)
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
func updateMarqueeConstraints() {
|
||||
NSLayoutConstraint.deactivate(currentMarqueeConstraints)
|
||||
|
||||
let leftSpacing: CGFloat = 8
|
||||
let rightSpacing: CGFloat = 8
|
||||
let leftSpacing: CGFloat = 2
|
||||
let rightSpacing: CGFloat = 6
|
||||
|
||||
let trailingAnchor: NSLayoutXAxisAnchor
|
||||
if let volumeView = volumeSliderHostingView, !volumeView.isHidden {
|
||||
trailingAnchor = volumeView.leadingAnchor
|
||||
} else {
|
||||
trailingAnchor = view.safeAreaLayoutGuide.trailingAnchor
|
||||
}
|
||||
|
||||
currentMarqueeConstraints = [
|
||||
marqueeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: leftSpacing),
|
||||
|
||||
marqueeLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -rightSpacing-20),
|
||||
|
||||
marqueeLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -rightSpacing - 10),
|
||||
marqueeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor)
|
||||
]
|
||||
|
||||
|
|
@ -749,7 +834,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
NSLayoutConstraint.activate([
|
||||
menuButton.topAnchor.constraint(equalTo: qualityButton.topAnchor),
|
||||
menuButton.trailingAnchor.constraint(equalTo: qualityButton.leadingAnchor, constant: -8),
|
||||
menuButton.trailingAnchor.constraint(equalTo: qualityButton.leadingAnchor, constant: -6),
|
||||
menuButton.widthAnchor.constraint(equalToConstant: 40),
|
||||
menuButton.heightAnchor.constraint(equalToConstant: 40)
|
||||
])
|
||||
|
|
@ -770,7 +855,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
NSLayoutConstraint.activate([
|
||||
speedButton.topAnchor.constraint(equalTo: watchNextButton.topAnchor),
|
||||
speedButton.trailingAnchor.constraint(equalTo: watchNextButton.leadingAnchor, constant: 20),
|
||||
speedButton.trailingAnchor.constraint(equalTo: watchNextButton.leadingAnchor, constant: 18),
|
||||
speedButton.widthAnchor.constraint(equalToConstant: 40),
|
||||
speedButton.heightAnchor.constraint(equalToConstant: 40)
|
||||
])
|
||||
|
|
@ -843,7 +928,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
skip85Button.isHidden = !isSkip85Visible
|
||||
}
|
||||
|
||||
|
||||
|
||||
private func setupQualityButton() {
|
||||
let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold)
|
||||
|
|
@ -867,7 +952,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
NSLayoutConstraint.activate([
|
||||
qualityButton.topAnchor.constraint(equalTo: speedButton.topAnchor),
|
||||
qualityButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor, constant: -8),
|
||||
qualityButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor, constant: -6),
|
||||
qualityButton.widthAnchor.constraint(equalToConstant: 40),
|
||||
qualityButton.heightAnchor.constraint(equalToConstant: 40)
|
||||
])
|
||||
|
|
@ -968,8 +1053,9 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
inRange: 0...(self.duration > 0 ? self.duration : 1.0),
|
||||
activeFillColor: .white,
|
||||
fillColor: .white.opacity(0.6),
|
||||
textColor: .white.opacity(0.7),
|
||||
emptyColor: .white.opacity(0.3),
|
||||
height: 30,
|
||||
height: 33,
|
||||
onEditingChanged: { editing in
|
||||
if !editing {
|
||||
let targetTime = CMTime(
|
||||
|
|
@ -1571,6 +1657,13 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
} catch {
|
||||
Logger.shared.log("Didn't set up AVAudioSession: \(error)", type: "Debug")
|
||||
}
|
||||
|
||||
volumeObserver = audioSession.observe(\.outputVolume, options: [.new]) { [weak self] session, change in
|
||||
guard let newVol = change.newValue else { return }
|
||||
DispatchQueue.main.async {
|
||||
self?.volumeViewModel.value = Double(newVol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupHoldGesture() {
|
||||
|
|
@ -1635,6 +1728,29 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct VolumeSliderContainer: View {
|
||||
@ObservedObject var volumeVM: VolumeViewModel
|
||||
var updateSystemSlider: ((Double) -> Void)? = nil // Optional callback if needed
|
||||
|
||||
var body: some View {
|
||||
VolumeSlider(
|
||||
value: Binding(
|
||||
get: { volumeVM.value },
|
||||
set: { newVal in
|
||||
volumeVM.value = newVal
|
||||
updateSystemSlider?(newVal)
|
||||
}
|
||||
),
|
||||
inRange: 0...1,
|
||||
activeFillColor: .white,
|
||||
fillColor: .white.opacity(0.6),
|
||||
emptyColor: .white.opacity(0.3),
|
||||
height: 10,
|
||||
onEditingChanged: { _ in }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ struct SettingsViewPlayer: View {
|
|||
@AppStorage("skipIncrementHold") private var skipIncrementHold: Double = 30.0
|
||||
@AppStorage("holdForPauseEnabled") private var holdForPauseEnabled = false
|
||||
@AppStorage("skip85Visible") private var skip85Visible: Bool = true
|
||||
@AppStorage("doubleTapSeekEnabled") private var doubleTapSeekEnabled: Bool = true
|
||||
|
||||
|
||||
private let mediaPlayers = ["Default", "VLC", "OutPlayer", "Infuse", "nPlayer", "Sora"]
|
||||
|
|
@ -72,6 +73,10 @@ struct SettingsViewPlayer: View {
|
|||
Spacer()
|
||||
Stepper("\(Int(skipIncrementHold))s", value: $skipIncrementHold, in: 5...300, step: 5)
|
||||
}
|
||||
|
||||
Toggle("Double Tap to Seek", isOn: $doubleTapSeekEnabled)
|
||||
.tint(.accentColor)
|
||||
|
||||
Toggle("Show Skip 85s Button", isOn: $skip85Visible)
|
||||
.tint(.accentColor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,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 */; };
|
||||
1E26E9E72DA9577900B9DC02 /* VolumeSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E26E9E62DA9577900B9DC02 /* VolumeSlider.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 */; };
|
||||
|
|
@ -116,6 +117,7 @@
|
|||
13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = "<group>"; };
|
||||
13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
|
||||
13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = "<group>"; };
|
||||
1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeSlider.swift; sourceTree = "<group>"; };
|
||||
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = "<group>"; };
|
||||
1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = "<group>"; };
|
||||
73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -406,6 +408,7 @@
|
|||
13EA2BD22D32D97400C1EBD7 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */,
|
||||
13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */,
|
||||
1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */,
|
||||
);
|
||||
|
|
@ -503,6 +506,7 @@
|
|||
133D7C902D2BE2640075467E /* SettingsView.swift in Sources */,
|
||||
132AF1252D9995F900A0140B /* JSController-Search.swift in Sources */,
|
||||
13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */,
|
||||
1E26E9E72DA9577900B9DC02 /* VolumeSlider.swift in Sources */,
|
||||
13DB46902D900A38008CBC03 /* URL.swift in Sources */,
|
||||
130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */,
|
||||
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */,
|
||||
|
|
|
|||
Loading…
Reference in a new issue