Skip to content

Commit 5a100cb

Browse files
authored
Merge pull request #1658 from akshat-tumkur/main
feat(Mini Player): Replace static progress bar with interactive seek bar + timestamps in the desktop app
2 parents 685193b + 2963b07 commit 5a100cb

File tree

1 file changed

+123
-65
lines changed

1 file changed

+123
-65
lines changed

composeApp/src/jvmMain/kotlin/com/maxrave/simpmusic/ui/mini_player/MiniPlayerLayout.kt

Lines changed: 123 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import androidx.compose.foundation.MarqueeAnimationMode
1212
import androidx.compose.foundation.background
1313
import androidx.compose.foundation.basicMarquee
1414
import androidx.compose.foundation.focusable
15+
import androidx.compose.foundation.gestures.detectDragGestures
16+
import androidx.compose.foundation.gestures.detectTapGestures
1517
import androidx.compose.foundation.hoverable
1618
import androidx.compose.foundation.interaction.MutableInteractionSource
1719
import androidx.compose.foundation.interaction.collectIsHoveredAsState
@@ -24,18 +26,22 @@ import androidx.compose.foundation.layout.Spacer
2426
import androidx.compose.foundation.layout.fillMaxSize
2527
import androidx.compose.foundation.layout.fillMaxWidth
2628
import androidx.compose.foundation.layout.height
29+
import androidx.compose.foundation.layout.offset
2730
import androidx.compose.foundation.layout.padding
2831
import androidx.compose.foundation.layout.size
32+
import androidx.compose.foundation.layout.width
2933
import androidx.compose.foundation.layout.wrapContentHeight
34+
import androidx.compose.foundation.shape.CircleShape
3035
import androidx.compose.foundation.shape.RoundedCornerShape
3136
import androidx.compose.material.icons.Icons
37+
import androidx.compose.material.icons.automirrored.filled.VolumeOff
38+
import androidx.compose.material.icons.automirrored.filled.VolumeUp
3239
import androidx.compose.material.icons.filled.Favorite
3340
import androidx.compose.material.icons.filled.VolumeOff
3441
import androidx.compose.material.icons.filled.VolumeUp
3542
import androidx.compose.material.icons.outlined.FavoriteBorder
3643
import androidx.compose.material3.Icon
3744
import androidx.compose.material3.IconButton
38-
import androidx.compose.material3.LinearProgressIndicator
3945
import androidx.compose.material3.Surface
4046
import androidx.compose.material3.Text
4147
import androidx.compose.runtime.Composable
@@ -47,10 +53,11 @@ import androidx.compose.ui.draw.alpha
4753
import androidx.compose.ui.draw.clip
4854
import androidx.compose.ui.draw.scale
4955
import androidx.compose.ui.graphics.Color
50-
import androidx.compose.ui.graphics.StrokeCap
56+
import androidx.compose.ui.input.pointer.pointerInput
5157
import androidx.compose.ui.layout.ContentScale
5258
import androidx.compose.ui.text.style.TextAlign
5359
import androidx.compose.ui.text.style.TextOverflow
60+
import androidx.compose.ui.unit.Dp
5461
import androidx.compose.ui.unit.dp
5562
import androidx.compose.ui.unit.sp
5663
import coil3.compose.AsyncImage
@@ -69,6 +76,86 @@ import simpmusic.composeapp.generated.resources.baseline_skip_next_24
6976
import simpmusic.composeapp.generated.resources.baseline_skip_previous_24
7077
import 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

Comments
 (0)