11package org.akanework.gramophone.ui.components
22
3+ import androidx.compose.animation.animateContentSize
34import androidx.compose.animation.core.animateDpAsState
45import androidx.compose.foundation.Image
56import androidx.compose.foundation.background
@@ -14,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxSize
1415import androidx.compose.foundation.layout.fillMaxWidth
1516import androidx.compose.foundation.layout.height
1617import androidx.compose.foundation.layout.heightIn
18+ import androidx.compose.foundation.layout.offset
1719import androidx.compose.foundation.layout.padding
1820import androidx.compose.foundation.layout.size
1921import androidx.compose.foundation.lazy.LazyColumn
@@ -28,9 +30,11 @@ import androidx.compose.material.icons.rounded.ExpandMore
2830import androidx.compose.material.icons.rounded.MusicNote
2931import androidx.compose.material3.Icon
3032import androidx.compose.material3.IconButton
33+ import androidx.compose.material3.LocalContentColor
3134import androidx.compose.material3.MaterialTheme
3235import androidx.compose.material3.Text
3336import androidx.compose.runtime.Composable
37+ import androidx.compose.runtime.collectAsState
3438import androidx.compose.runtime.getValue
3539import androidx.compose.runtime.mutableStateListOf
3640import androidx.compose.runtime.mutableStateOf
@@ -48,18 +52,23 @@ import androidx.compose.ui.res.painterResource
4852import androidx.compose.ui.res.stringResource
4953import androidx.compose.ui.text.style.TextOverflow
5054import 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
5160import androidx.media3.session.MediaBrowser
5261import kotlinx.coroutines.CoroutineScope
53- import kotlinx.coroutines.Dispatchers
5462import kotlinx.coroutines.delay
63+ import kotlinx.coroutines.flow.MutableStateFlow
5564import kotlinx.coroutines.launch
5665import org.akanework.gramophone.R
5766import org.akanework.gramophone.logic.MultiQueueObject
5867import org.akanework.gramophone.logic.deleteQueue
5968import org.akanework.gramophone.logic.getInactiveQueues
6069import org.akanework.gramophone.logic.getQueue
6170import 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
6574fun 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
343455class 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
459632fun rememberMqState (
460633 coroutineScope : CoroutineScope ,
461- instance : MediaBrowser ? ,
634+ instance : MediaBrowser ,
462635 playlistQueueSheet : PlaylistQueueSheet ? ,
463636): MqState {
464637 return remember {
0 commit comments