Skip to content

Commit 33caec9

Browse files
committed
mq: Add queue player controls
1 parent c6ae8cb commit 33caec9

3 files changed

Lines changed: 197 additions & 19 deletions

File tree

app/src/main/java/org/akanework/gramophone/ui/components/ComposeComponentsTemp.kt

Lines changed: 189 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.akanework.gramophone.ui.components
22

3+
import androidx.compose.animation.animateContentSize
34
import androidx.compose.animation.core.animateDpAsState
45
import androidx.compose.foundation.Image
56
import androidx.compose.foundation.background
@@ -14,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxSize
1415
import androidx.compose.foundation.layout.fillMaxWidth
1516
import androidx.compose.foundation.layout.height
1617
import androidx.compose.foundation.layout.heightIn
18+
import androidx.compose.foundation.layout.offset
1719
import androidx.compose.foundation.layout.padding
1820
import androidx.compose.foundation.layout.size
1921
import androidx.compose.foundation.lazy.LazyColumn
@@ -28,9 +30,11 @@ import androidx.compose.material.icons.rounded.ExpandMore
2830
import androidx.compose.material.icons.rounded.MusicNote
2931
import androidx.compose.material3.Icon
3032
import androidx.compose.material3.IconButton
33+
import androidx.compose.material3.LocalContentColor
3134
import androidx.compose.material3.MaterialTheme
3235
import androidx.compose.material3.Text
3336
import androidx.compose.runtime.Composable
37+
import androidx.compose.runtime.collectAsState
3438
import androidx.compose.runtime.getValue
3539
import androidx.compose.runtime.mutableStateListOf
3640
import androidx.compose.runtime.mutableStateOf
@@ -48,18 +52,23 @@ import androidx.compose.ui.res.painterResource
4852
import androidx.compose.ui.res.stringResource
4953
import androidx.compose.ui.text.style.TextOverflow
5054
import androidx.compose.ui.unit.dp
55+
import androidx.media3.common.Player
56+
import androidx.media3.common.Player.REPEAT_MODE_ALL
57+
import androidx.media3.common.Player.REPEAT_MODE_OFF
58+
import androidx.media3.common.Player.REPEAT_MODE_ONE
59+
import androidx.media3.common.util.Log
5160
import androidx.media3.session.MediaBrowser
5261
import kotlinx.coroutines.CoroutineScope
53-
import kotlinx.coroutines.Dispatchers
5462
import kotlinx.coroutines.delay
63+
import kotlinx.coroutines.flow.MutableStateFlow
5564
import kotlinx.coroutines.launch
5665
import org.akanework.gramophone.R
5766
import org.akanework.gramophone.logic.MultiQueueObject
5867
import org.akanework.gramophone.logic.deleteQueue
5968
import org.akanework.gramophone.logic.getInactiveQueues
6069
import org.akanework.gramophone.logic.getQueue
6170
import org.akanework.gramophone.logic.loadQueue
62-
import org.akanework.gramophone.logic.utils.Flags.MQ_PREVIEW
71+
import org.akanework.gramophone.logic.playOrPause
6372

6473
@Composable
6574
fun MqListItem(
@@ -145,6 +154,10 @@ fun MqContent(
145154
) {
146155
val haptic = LocalHapticFeedback.current
147156

157+
val isPlaying by mqState.isPlaying.collectAsState()
158+
val repeatMode by mqState.repeatMode.collectAsState()
159+
val shuffleModeEnabled by mqState.shuffleModeEnabled.collectAsState()
160+
148161
val mqExpand = mqState.expanded
149162
val animatedMinHeight by animateDpAsState(
150163
targetValue = if (mqExpand) 300.dp else 0.dp,
@@ -267,27 +280,126 @@ fun MqContent(
267280
)
268281
}
269282
}
270-
if (mqState.isDetached())
271-
item {
283+
284+
// action bar
285+
item {
286+
Row(
287+
horizontalArrangement = Arrangement.SpaceBetween,
288+
verticalAlignment = Alignment.CenterVertically,
289+
modifier = Modifier
290+
.fillMaxWidth()
291+
.padding(horizontal = 16.dp)
292+
.animateContentSize(),
293+
) {
294+
// left options
295+
Row(
296+
horizontalArrangement = Arrangement.End,
297+
verticalAlignment = Alignment.CenterVertically,
298+
modifier = Modifier
299+
) {
300+
IconButton(
301+
onClick = {
302+
mqState.toggleRepeatMode()
303+
},
304+
enabled = !mqState.isDetached(),
305+
) {
306+
Icon(
307+
painter = painterResource(
308+
when (repeatMode) {
309+
REPEAT_MODE_OFF, REPEAT_MODE_ALL -> R.drawable.ic_repeat
310+
else -> R.drawable.ic_repeat_one
311+
}
312+
),
313+
contentDescription = null,
314+
tint = LocalContentColor.current.copy(if (repeatMode == REPEAT_MODE_OFF) 0.5f else 1f)
315+
)
316+
}
317+
IconButton(
318+
onClick = {
319+
mqState.toggleShuffleMode()
320+
},
321+
enabled = !mqState.isDetached(),
322+
) {
323+
Icon(
324+
painter = painterResource(R.drawable.ic_shuffle),
325+
contentDescription = null,
326+
tint = LocalContentColor.current.copy(if (shuffleModeEnabled) 1f else 0.5f)
327+
)
328+
}
329+
}
330+
331+
// center options
272332
Row(
273333
horizontalArrangement = Arrangement.End,
274334
verticalAlignment = Alignment.CenterVertically,
275335
modifier = Modifier
276-
.fillMaxWidth()
277336
.padding(horizontal = 16.dp)
278337
) {
279338
IconButton(
280339
onClick = {
281-
mqState.loadDetached()
340+
mqState.seekPrev()
341+
},
342+
enabled = !mqState.isDetached(),
343+
) {
344+
Icon(
345+
painter = painterResource(R.drawable.ic_skip_previous),
346+
contentDescription = null,
347+
)
348+
}
349+
IconButton(
350+
onClick = {
351+
mqState.togglePlayPause()
352+
},
353+
enabled = !mqState.isDetached(),
354+
) {
355+
Icon(
356+
painter = painterResource(if (isPlaying) R.drawable.ic_pause_filled else R.drawable.ic_play_arrow),
357+
contentDescription = null,
358+
)
359+
}
360+
IconButton(
361+
onClick = {
362+
mqState.seekNext()
363+
},
364+
enabled = !mqState.isDetached(),
365+
) {
366+
Icon(
367+
painter = painterResource(R.drawable.ic_skip_next),
368+
contentDescription = null,
369+
)
370+
}
371+
}
372+
373+
// right options
374+
Row(
375+
horizontalArrangement = Arrangement.End,
376+
verticalAlignment = Alignment.CenterVertically,
377+
modifier = Modifier
378+
) {
379+
IconButton(
380+
onClick = {
282381
},
283382
) {
284383
Icon(
285-
painter = painterResource(R.drawable.ic_play_arrow),
286-
contentDescription = null
384+
painter = painterResource(R.drawable.ic_more_vert_alt),
385+
contentDescription = null,
287386
)
288387
}
388+
if (mqState.isDetached()) {
389+
IconButton(
390+
onClick = {
391+
mqState.loadDetached()
392+
},
393+
) {
394+
Icon(
395+
painter = painterResource(R.drawable.ic_play_arrow),
396+
contentDescription = null,
397+
)
398+
}
399+
}
289400
}
290401
}
402+
}
291403
}
292404
}
293405
}
@@ -342,9 +454,14 @@ fun EmptyPlaceholder(
342454

343455
class MqState(
344456
private val coroutineScope: CoroutineScope,
345-
private val instance: MediaBrowser?,
457+
private val instance: MediaBrowser,
346458
private val playlistQueueSheet: PlaylistQueueSheet?,
347-
) {
459+
) : Player.Listener {
460+
val isPlaying = MutableStateFlow(instance.isPlaying)
461+
462+
// shuffle and repeat modes do not need to be manually set for queue loads, they will be set automatically
463+
val shuffleModeEnabled = MutableStateFlow(instance.shuffleModeEnabled)
464+
val repeatMode = MutableStateFlow(instance.repeatMode)
348465
var expanded by mutableStateOf(false)
349466
private set
350467

@@ -358,6 +475,7 @@ class MqState(
358475
private set
359476

360477
init {
478+
instance.addListener(this)
361479
init()
362480
}
363481

@@ -367,10 +485,10 @@ class MqState(
367485
detachedQueue = null
368486
inactiveQueues.clear()
369487

370-
instance?.getQueue()?.let {
488+
instance.getQueue()?.let {
371489
activeQueue = it
372490
}
373-
instance?.getInactiveQueues()?.toMutableList()?.let {
491+
instance.getInactiveQueues().toMutableList().let {
374492
inactiveQueues.addAll(it)
375493
}
376494
}
@@ -395,7 +513,7 @@ class MqState(
395513
fun getQueuePositionStr(): String {
396514
return if (!isDetached()) {
397515
activeQueue?.let {
398-
"${(instance?.currentMediaItemIndex ?: -1) + 1} / ${it.getSize()}"
516+
"${(instance.currentMediaItemIndex) + 1} / ${it.getSize()}"
399517
}
400518
} else {
401519
detachedQueue?.let {
@@ -408,16 +526,34 @@ class MqState(
408526

409527
fun detach(index: Int) {
410528
detachedQueue = inactiveQueues.getOrNull(index)
529+
detachedQueue?.repeatMode?.let {
530+
onRepeatModeChanged(it)
531+
}
532+
detachedQueue?.shuffleModeEnabled?.let {
533+
onShuffleModeEnabledChanged(it)
534+
}
411535
}
412536

413537
fun detach(mq: MultiQueueObject) {
414538
detachedQueue = mq
415539
playlistQueueSheet?.forceUpdate(inactiveQueues.indexOf(mq))
540+
detachedQueue?.repeatMode?.let {
541+
onRepeatModeChanged(it)
542+
}
543+
detachedQueue?.shuffleModeEnabled?.let {
544+
onShuffleModeEnabledChanged(it)
545+
}
416546
}
417547

418548
fun resetHead() {
419549
detachedQueue = null
420550
playlistQueueSheet?.forceUpdate(-1)
551+
detachedQueue?.repeatMode?.let {
552+
onRepeatModeChanged(it)
553+
}
554+
detachedQueue?.shuffleModeEnabled?.let {
555+
onShuffleModeEnabledChanged(it)
556+
}
421557
}
422558

423559
fun toggleExpand() {
@@ -438,27 +574,64 @@ class MqState(
438574
}
439575

440576
fun removeQueue(index: Int) {
441-
instance?.deleteQueue(index)
577+
instance.deleteQueue(index)
442578
coroutineScope.launch {
443579
init()
444580
}
581+
detachedQueue?.repeatMode?.let {
582+
onRepeatModeChanged(it)
583+
}
584+
detachedQueue?.shuffleModeEnabled?.let {
585+
onShuffleModeEnabledChanged(it)
586+
}
445587
}
446588

447589
fun loadDetached() {
448-
instance?.loadQueue(inactiveQueues.indexOf(detachedQueue))
590+
instance.loadQueue(inactiveQueues.indexOf(detachedQueue))
449591
expanded = false
450592
resetHead()
451593
coroutineScope.launch {
452594
delay(500)
453595
init()
454596
}
455597
}
598+
599+
600+
fun togglePlayPause() = instance.playOrPause()
601+
fun seekPrev() = instance.seekToPrevious()
602+
fun seekNext() = instance.seekToNext()
603+
604+
fun toggleRepeatMode() {
605+
instance.repeatMode = when (instance.repeatMode) {
606+
REPEAT_MODE_OFF -> REPEAT_MODE_ALL
607+
REPEAT_MODE_ALL -> REPEAT_MODE_ONE
608+
REPEAT_MODE_ONE -> REPEAT_MODE_OFF
609+
else -> REPEAT_MODE_OFF
610+
}
611+
}
612+
613+
fun toggleShuffleMode() {
614+
instance.shuffleModeEnabled = !instance.shuffleModeEnabled
615+
}
616+
617+
618+
override fun onIsPlayingChanged(isPlaying: Boolean) {
619+
this.isPlaying.value = isPlaying
620+
}
621+
622+
override fun onRepeatModeChanged(repeatMode: @Player.RepeatMode Int) {
623+
this.repeatMode.value = repeatMode
624+
}
625+
626+
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
627+
this.shuffleModeEnabled.value = shuffleModeEnabled
628+
}
456629
}
457630

458631
@Composable
459632
fun rememberMqState(
460633
coroutineScope: CoroutineScope,
461-
instance: MediaBrowser?,
634+
instance: MediaBrowser,
462635
playlistQueueSheet: PlaylistQueueSheet?,
463636
): MqState {
464637
return remember {

app/src/main/java/org/akanework/gramophone/ui/components/PlaylistQueueSheet.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.os.SystemClock
66
import android.view.LayoutInflater
77
import android.view.View
88
import android.widget.Button
9+
import androidx.compose.animation.animateContentSize
910
import androidx.compose.foundation.background
1011
import androidx.compose.foundation.clickable
1112
import androidx.compose.foundation.layout.Arrangement
@@ -60,6 +61,7 @@ import org.akanework.gramophone.ui.MainActivity
6061
import java.util.LinkedList
6162

6263
// TODO: support listening to externally caused changes to playlist (ie MCT).
64+
// TODO: Playing indicator does not update when shuffling
6365
class PlaylistQueueSheet(
6466
context: Context, private val activity: MainActivity
6567
) : BottomSheetDialog(context), Player.Listener {
@@ -197,7 +199,7 @@ class PlaylistQueueSheet(
197199
pureDark = false, // TODO: I don't want to do this rn. Does not respect light/dark mode
198200
) {
199201
val mqState =
200-
rememberMqState(coroutineScope, instance, this@PlaylistQueueSheet)
202+
rememberMqState(coroutineScope, instance!!, this@PlaylistQueueSheet, )
201203
val pagerState = rememberPagerState(pageCount = { 2 })
202204

203205
LaunchedEffect(mqState) {
@@ -233,7 +235,8 @@ class PlaylistQueueSheet(
233235
state = pagerState,
234236
modifier = Modifier
235237
.fillMaxWidth()
236-
.padding(bottom = 14.dp),
238+
.padding(bottom = if (mqState.expanded) 0.dp else 20.dp)
239+
.animateContentSize(),
237240
beyondViewportPageCount = 1,
238241
userScrollEnabled = !mqState.expanded
239242
) { page ->
@@ -256,8 +259,10 @@ class PlaylistQueueSheet(
256259
modifier = Modifier
257260
.wrapContentHeight()
258261
.fillMaxWidth()
262+
.padding(bottom = 8.dp)
259263
.align(Alignment.BottomCenter)
260264
.alpha(if (!mqState.expanded) 1f else 0f)
265+
.animateContentSize()
261266
) {
262267
repeat(pagerState.pageCount) { iteration ->
263268
val color = if (pagerState.currentPage == iteration) {

0 commit comments

Comments
 (0)