@@ -12,6 +12,8 @@ import androidx.compose.foundation.MarqueeAnimationMode
1212import androidx.compose.foundation.background
1313import androidx.compose.foundation.basicMarquee
1414import androidx.compose.foundation.focusable
15+ import androidx.compose.foundation.gestures.detectDragGestures
16+ import androidx.compose.foundation.gestures.detectTapGestures
1517import androidx.compose.foundation.hoverable
1618import androidx.compose.foundation.interaction.MutableInteractionSource
1719import androidx.compose.foundation.interaction.collectIsHoveredAsState
@@ -24,18 +26,22 @@ import androidx.compose.foundation.layout.Spacer
2426import androidx.compose.foundation.layout.fillMaxSize
2527import androidx.compose.foundation.layout.fillMaxWidth
2628import androidx.compose.foundation.layout.height
29+ import androidx.compose.foundation.layout.offset
2730import androidx.compose.foundation.layout.padding
2831import androidx.compose.foundation.layout.size
32+ import androidx.compose.foundation.layout.width
2933import androidx.compose.foundation.layout.wrapContentHeight
34+ import androidx.compose.foundation.shape.CircleShape
3035import androidx.compose.foundation.shape.RoundedCornerShape
3136import androidx.compose.material.icons.Icons
37+ import androidx.compose.material.icons.automirrored.filled.VolumeOff
38+ import androidx.compose.material.icons.automirrored.filled.VolumeUp
3239import androidx.compose.material.icons.filled.Favorite
3340import androidx.compose.material.icons.filled.VolumeOff
3441import androidx.compose.material.icons.filled.VolumeUp
3542import androidx.compose.material.icons.outlined.FavoriteBorder
3643import androidx.compose.material3.Icon
3744import androidx.compose.material3.IconButton
38- import androidx.compose.material3.LinearProgressIndicator
3945import androidx.compose.material3.Surface
4046import androidx.compose.material3.Text
4147import androidx.compose.runtime.Composable
@@ -47,10 +53,11 @@ import androidx.compose.ui.draw.alpha
4753import androidx.compose.ui.draw.clip
4854import androidx.compose.ui.draw.scale
4955import androidx.compose.ui.graphics.Color
50- import androidx.compose.ui.graphics.StrokeCap
56+ import androidx.compose.ui.input.pointer.pointerInput
5157import androidx.compose.ui.layout.ContentScale
5258import androidx.compose.ui.text.style.TextAlign
5359import androidx.compose.ui.text.style.TextOverflow
60+ import androidx.compose.ui.unit.Dp
5461import androidx.compose.ui.unit.dp
5562import androidx.compose.ui.unit.sp
5663import coil3.compose.AsyncImage
@@ -69,6 +76,86 @@ import simpmusic.composeapp.generated.resources.baseline_skip_next_24
6976import simpmusic.composeapp.generated.resources.baseline_skip_previous_24
7077import simpmusic.composeapp.generated.resources.holder
7178
79+ @Composable
80+ private fun MiniPlayerSeekBar (
81+ timeline : TimeLine ,
82+ onUIEvent : (UIEvent ) -> Unit ,
83+ modifier : Modifier = Modifier ,
84+ trackHeight : Dp = 4.dp,
85+ thumbSize : Dp = 6.dp,
86+ hitHeight : Dp = 24.dp,
87+ ) {
88+ if (timeline.total <= 0L ) return
89+
90+ val progress =
91+ (timeline.current.toFloat() / timeline.total.toFloat())
92+ .coerceIn(0f , 1f )
93+
94+ BoxWithConstraints (
95+ modifier =
96+ modifier
97+ .fillMaxWidth()
98+ .height(hitHeight)
99+ .pointerInput(Unit ) {
100+ detectTapGestures { offset ->
101+ val percent =
102+ (offset.x / size.width)
103+ .coerceIn(0f , 1f ) * 100f
104+ onUIEvent(UIEvent .UpdateProgress (percent))
105+ }
106+ }.pointerInput(Unit ) {
107+ detectDragGestures(
108+ onDragStart = { offset ->
109+ val percent =
110+ (offset.x / size.width)
111+ .coerceIn(0f , 1f ) * 100f
112+ onUIEvent(UIEvent .UpdateProgress (percent))
113+ },
114+ onDrag = { change, _ ->
115+ val percent =
116+ (change.position.x / size.width)
117+ .coerceIn(0f , 1f ) * 100f
118+ onUIEvent(UIEvent .UpdateProgress (percent))
119+ },
120+ )
121+ },
122+ contentAlignment = Alignment .CenterStart ,
123+ ) {
124+ // Track
125+ Box (
126+ Modifier
127+ .fillMaxWidth()
128+ .height(trackHeight)
129+ .align(Alignment .Center )
130+ .background(
131+ Color .White .copy(alpha = 0.25f ),
132+ RoundedCornerShape (50 ),
133+ ),
134+ )
135+
136+ // Progress
137+ Box (
138+ Modifier
139+ .width(maxWidth * progress)
140+ .height(trackHeight)
141+ .align(Alignment .CenterStart )
142+ .background(
143+ Color .White ,
144+ RoundedCornerShape (50 ),
145+ ),
146+ )
147+
148+ // Thumb
149+ Box (
150+ Modifier
151+ .offset(x = (maxWidth * progress) - (thumbSize / 2 ))
152+ .size(thumbSize)
153+ .align(Alignment .CenterStart )
154+ .background(Color .White , CircleShape ),
155+ )
156+ }
157+ }
158+
72159/* *
73160 * Compact layout (< 260dp): Controls only, no artwork or text
74161 * Perfect for very narrow windows
@@ -138,8 +225,15 @@ fun CompactMiniLayout(
138225 }
139226 }
140227
141- // Progress bar
142- ProgressBar (timeline)
228+ // Seek bar
229+ Box (
230+ modifier = Modifier .padding(horizontal = 12 .dp),
231+ ) {
232+ MiniPlayerSeekBar (
233+ timeline = timeline,
234+ onUIEvent = onUIEvent,
235+ )
236+ }
143237 }
144238 }
145239}
@@ -350,37 +444,20 @@ fun MediumMiniLayout(
350444 }
351445 }
352446
353- // Progress bar
354- ProgressBar (timeline)
447+ // Seek bar
448+ Box (
449+ modifier = Modifier .padding(horizontal = 12 .dp),
450+ ) {
451+ MiniPlayerSeekBar (
452+ timeline = timeline,
453+ onUIEvent = onUIEvent,
454+ )
455+ }
355456 }
356457 }
357458 }
358459}
359460
360- /* *
361- * Progress bar component shared across all layouts
362- */
363- @Composable
364- private fun ProgressBar (timeline : TimeLine ) {
365- Box (
366- modifier =
367- Modifier
368- .fillMaxWidth()
369- .height(3 .dp)
370- .background(Color (0xFF2C2C2E )),
371- ) {
372- if (timeline.total > 0L && timeline.current >= 0L ) {
373- LinearProgressIndicator (
374- progress = { timeline.current.toFloat() / timeline.total },
375- modifier = Modifier .fillMaxSize(),
376- color = Color .White ,
377- trackColor = Color .Transparent ,
378- strokeCap = StrokeCap .Round ,
379- )
380- }
381- }
382- }
383-
384461/* *
385462 * Square/Tall layout (Spotify-style): Large artwork centered with controls below
386463 * Appears when window is square or taller (aspect ratio <= 1.3)
@@ -514,25 +591,15 @@ fun SquareMiniLayout(
514591 }
515592 }
516593
517- // Progress bar
594+ // Seek bar
518595 Box (
519- modifier =
520- Modifier
521- .fillMaxWidth()
522- .height(4 .dp)
523- .background(Color (0xFF2C2C2E ), RoundedCornerShape (2 .dp)),
596+ modifier = Modifier .padding(horizontal = 12 .dp),
524597 ) {
525- if (timeline.total > 0L && timeline.current >= 0L ) {
526- LinearProgressIndicator (
527- progress = { timeline.current.toFloat() / timeline.total },
528- modifier = Modifier .fillMaxSize(),
529- color = Color .White ,
530- trackColor = Color .Transparent ,
531- strokeCap = StrokeCap .Round ,
532- )
533- }
598+ MiniPlayerSeekBar (
599+ timeline = timeline,
600+ onUIEvent = onUIEvent,
601+ )
534602 }
535-
536603 Spacer (modifier = Modifier .height(12 .dp))
537604
538605 // Main playback controls
@@ -652,7 +719,7 @@ fun EmptyMiniPlayerState() {
652719}
653720
654721/* *
655- * Legacy full layout - now used only when BoxWithConstraints shows > 360dp
722+ * Legacy full layout - now used only when Box shows > 360dp
656723 * Kept for backwards compatibility
657724 */
658725@Composable
@@ -804,9 +871,9 @@ fun ExpandedMiniLayout(
804871 Icon (
805872 imageVector =
806873 if (controllerState.volume > 0f ) {
807- Icons .Filled .VolumeUp
874+ Icons .AutoMirrored . Filled .VolumeUp
808875 } else {
809- Icons .Filled .VolumeOff
876+ Icons .AutoMirrored . Filled .VolumeOff
810877 },
811878 contentDescription = if (controllerState.volume > 0f ) " Mute" else " Unmute" ,
812879 tint = Color .White .copy(alpha = 0.7f ),
@@ -832,7 +899,7 @@ fun ExpandedMiniLayout(
832899 Modifier
833900 .fillMaxWidth()
834901 .padding(horizontal = 12 .dp)
835- .padding(bottom = 8 .dp),
902+ .padding(bottom = 4 .dp),
836903 ) {
837904 if (lyricsData.lyrics.syncType == " RICH_SYNCED" ) {
838905 val parsedLine =
@@ -875,23 +942,14 @@ fun ExpandedMiniLayout(
875942 }
876943 }
877944
878- // Progress bar
945+ // Seek bar
879946 Box (
880- modifier =
881- Modifier
882- .fillMaxWidth()
883- .height(3 .dp)
884- .background(Color (0xFF2C2C2E )),
947+ modifier = Modifier .padding(horizontal = 12 .dp),
885948 ) {
886- if (timeline.total > 0L && timeline.current >= 0L ) {
887- LinearProgressIndicator (
888- progress = { timeline.current.toFloat() / timeline.total },
889- modifier = Modifier .fillMaxSize(),
890- color = Color .White ,
891- trackColor = Color .Transparent ,
892- strokeCap = StrokeCap .Round ,
893- )
894- }
949+ MiniPlayerSeekBar (
950+ timeline = timeline,
951+ onUIEvent = onUIEvent,
952+ )
895953 }
896954 }
897955 }
0 commit comments