volume slider 🤤 (#77)

This commit is contained in:
Seiike 2025-04-12 12:49:25 +02:00 committed by GitHub
parent df3a9e8f74
commit f99dc3e6f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 314 additions and 165 deletions

View file

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

View file

@ -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)

View file

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

View file

@ -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)
}
}

View file

@ -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 }
)
}
}
}

View file

@ -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)
}

View file

@ -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 */,