diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/Double+Extension.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/Double+Extension.swift index dd7d7bd..8173515 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Components/Double+Extension.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Components/Double+Extension.swift @@ -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 -} \ No newline at end of file +} + +class VolumeViewModel: ObservableObject { + @Published var value: Double = 0.0 +} + diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift index 7401f8c..8841dcc 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift @@ -14,6 +14,7 @@ struct MusicProgressSlider: View { let inRange: ClosedRange 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: 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: 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) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/VerticalBrightnessSlider.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/VerticalBrightnessSlider.swift deleted file mode 100644 index 1da58ea..0000000 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Components/VerticalBrightnessSlider.swift +++ /dev/null @@ -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: 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/Components/VolumeSlider.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/VolumeSlider.swift new file mode 100644 index 0000000..15437c6 --- /dev/null +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Components/VolumeSlider.swift @@ -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: View { + @Binding var value: T + let inRange: ClosedRange + 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.. 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) + } +} diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 1324fa9..3b87c25 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -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 } + ) + } + } } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index cff7441..69deaf4 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -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) } diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index cba651e..9bc5cec 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; + 1E26E9E62DA9577900B9DC02 /* VolumeSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeSlider.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 = ""; }; @@ -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 */,