@ -22,6 +22,8 @@ struct PlayerControlsView: View {
@ State private var dragValue : Double = 0
var body : some View {
VStack ( spacing : 0 ) {
progressTrack
HStack ( spacing : 0 ) {
nowPlayingSection
. frame ( maxWidth : . infinity , alignment : . leading )
@ -34,6 +36,7 @@ struct PlayerControlsView: View {
}
. padding ( . horizontal , 16 )
. padding ( . vertical , 8 )
}
. background ( . bar )
}
@ -75,8 +78,68 @@ struct PlayerControlsView: View {
}
}
private var progressTrack : some View {
let trackHeight : CGFloat = 4
let thumbWidth : CGFloat = 4
let thumbHeight : CGFloat = 12
let displayedTime = isDragging ? dragValue : currentTime
let maxDuration = max ( duration , 1 )
let fraction = displayedTime / maxDuration
return VStack ( spacing : 2 ) {
GeometryReader { geo in
let trackWidth = geo . size . width
ZStack ( alignment : . leading ) {
Rectangle ( )
. fill ( . quaternary )
. frame ( height : trackHeight )
Rectangle ( )
. fill ( . blue )
. frame ( width : trackWidth * fraction , height : trackHeight )
RoundedRectangle ( cornerRadius : 1 )
. fill ( . blue )
. frame ( width : thumbWidth , height : thumbHeight )
. offset ( x : trackWidth * fraction - thumbWidth / 2 )
}
. frame ( maxHeight : . infinity )
. contentShape ( Rectangle ( ) )
. gesture (
DragGesture ( minimumDistance : 0 )
. onChanged { value in
let newValue = min ( max ( Double ( value . location . x / trackWidth ) * maxDuration , 0 ) , maxDuration )
if ! isDragging {
isDragging = true
dragValue = currentTime
onScrubStart ( )
}
dragValue = newValue
onScrub ( newValue )
}
. onEnded { value in
let newValue = min ( max ( Double ( value . location . x / trackWidth ) * maxDuration , 0 ) , maxDuration )
onScrubEnd ( newValue )
isDragging = false
}
)
}
. frame ( height : thumbHeight )
HStack {
Text ( Self . formatTime ( displayedTime ) )
. font ( . system ( size : 10 ) . monospacedDigit ( ) )
. foregroundStyle ( . secondary )
Spacer ( )
Text ( Self . formatTime ( duration ) )
. font ( . system ( size : 10 ) . monospacedDigit ( ) )
. foregroundStyle ( . secondary )
}
. padding ( . horizontal , 8 )
}
}
private var transportSection : some View {
VStack ( spacing : 4 ) {
HStack ( spacing : 20 ) {
Button ( action : onShuffleToggle ) {
Image ( systemName : " shuffle " )
@ -103,46 +166,6 @@ struct PlayerControlsView: View {
. font ( . system ( size : 14 ) )
}
. buttonStyle ( . plain )
Spacer ( )
. frame ( width : 12 )
}
HStack ( spacing : 8 ) {
Text ( Self . formatTime ( isDragging ? dragValue : currentTime ) )
. font ( . system ( size : 10 ) . monospacedDigit ( ) )
. foregroundStyle ( . secondary )
. frame ( width : 45 , alignment : . trailing )
Slider (
value : Binding (
get : { isDragging ? dragValue : currentTime } ,
set : { newValue in
dragValue = newValue
if isDragging {
onScrub ( newValue )
}
}
) ,
in : 0. . . max ( duration , 1 ) ,
onEditingChanged : { editing in
if editing {
isDragging = true
dragValue = currentTime
onScrubStart ( )
} else {
onScrubEnd ( dragValue )
isDragging = false
}
}
)
. controlSize ( . small )
Text ( Self . formatTime ( duration ) )
. font ( . system ( size : 10 ) . monospacedDigit ( ) )
. foregroundStyle ( . secondary )
. frame ( width : 45 , alignment : . leading )
}
}
. frame ( maxWidth : 400 )
}