mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
keep playback going through undo and polish the queue drag
This commit is contained in:
@@ -46,6 +46,11 @@ class Media3PlayerHandle(
|
|||||||
|
|
||||||
override fun addLast(item: QueueItem) = controller.addMediaItems(controller.mediaItemCount, listOf(toMediaItem(item)))
|
override fun addLast(item: QueueItem) = controller.addMediaItems(controller.mediaItemCount, listOf(toMediaItem(item)))
|
||||||
|
|
||||||
|
override fun insertAt(
|
||||||
|
index: Int,
|
||||||
|
item: QueueItem,
|
||||||
|
) = controller.addMediaItems(index, listOf(toMediaItem(item)))
|
||||||
|
|
||||||
override fun removeFirst() = controller.removeMediaItem(0)
|
override fun removeFirst() = controller.removeMediaItem(0)
|
||||||
|
|
||||||
override fun removeLast() = controller.removeMediaItem(controller.mediaItemCount - 1)
|
override fun removeLast() = controller.removeMediaItem(controller.mediaItemCount - 1)
|
||||||
|
|||||||
@@ -46,10 +46,29 @@ data class PlaybackState(
|
|||||||
val hasPrevious: Boolean = false,
|
val hasPrevious: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
private data class QueueUndo(
|
private sealed interface QueueUndo {
|
||||||
val entries: List<QueueEntry>,
|
val entries: List<QueueEntry>
|
||||||
val cursor: Long,
|
val cursor: Long
|
||||||
)
|
}
|
||||||
|
|
||||||
|
private data class RemovedUndo(
|
||||||
|
override val entries: List<QueueEntry>,
|
||||||
|
override val cursor: Long,
|
||||||
|
val position: Long,
|
||||||
|
val item: QueueItem?,
|
||||||
|
) : QueueUndo
|
||||||
|
|
||||||
|
private data class MovedUndo(
|
||||||
|
override val entries: List<QueueEntry>,
|
||||||
|
override val cursor: Long,
|
||||||
|
val from: Long,
|
||||||
|
val to: Long,
|
||||||
|
) : QueueUndo
|
||||||
|
|
||||||
|
private data class ReloadUndo(
|
||||||
|
override val entries: List<QueueEntry>,
|
||||||
|
override val cursor: Long,
|
||||||
|
) : QueueUndo
|
||||||
|
|
||||||
class PlaybackController(
|
class PlaybackController(
|
||||||
private val sourceRepository: SourceRepository,
|
private val sourceRepository: SourceRepository,
|
||||||
@@ -157,20 +176,27 @@ class PlaybackController(
|
|||||||
val snapshot = snapshot ?: return@withLock
|
val snapshot = snapshot ?: return@withLock
|
||||||
if (position !in 0 until snapshot.size) return@withLock
|
if (position !in 0 until snapshot.size) return@withLock
|
||||||
val player = player ?: return@withLock
|
val player = player ?: return@withLock
|
||||||
val undo = QueueUndo(snapshot.entries.map { it.entry }, queueRepository.cursor())
|
val cursor = queueRepository.cursor()
|
||||||
|
val entries = snapshot.entries.map { it.entry }
|
||||||
if (snapshot.size == 1L) {
|
if (snapshot.size == 1L) {
|
||||||
queueRepository.removeAt(snapshot, position)
|
queueRepository.removeAt(snapshot, position)
|
||||||
stopLocked()
|
stopLocked()
|
||||||
lastEdit = undo
|
lastEdit = ReloadUndo(entries, cursor)
|
||||||
return@withLock
|
return@withLock
|
||||||
}
|
}
|
||||||
val current = currentPosition() ?: return@withLock
|
val current = currentPosition() ?: return@withLock
|
||||||
val target = (if (position < current) current - 1 else current).coerceIn(0, snapshot.size - 2)
|
val target = (if (position < current) current - 1 else current).coerceIn(0, snapshot.size - 2)
|
||||||
val playing = player.playWhenReady
|
val playing = player.playWhenReady
|
||||||
|
val removed = if (position == current) null else queueRepository.itemAt(snapshot, position)?.toQueueItem()
|
||||||
queueRepository.removeAt(snapshot, position)
|
queueRepository.removeAt(snapshot, position)
|
||||||
this.snapshot = queueRepository.snapshot()
|
this.snapshot = queueRepository.snapshot()
|
||||||
windowJob?.cancel()
|
windowJob?.cancel()
|
||||||
lastEdit = undo
|
lastEdit =
|
||||||
|
if (removed == null) {
|
||||||
|
ReloadUndo(entries, cursor)
|
||||||
|
} else {
|
||||||
|
RemovedUndo(entries, cursor, position, removed)
|
||||||
|
}
|
||||||
when {
|
when {
|
||||||
position == current -> {
|
position == current -> {
|
||||||
loadWindow(target, autoplay = playing, startPositionMs = 0)
|
loadWindow(target, autoplay = playing, startPositionMs = 0)
|
||||||
@@ -207,13 +233,20 @@ class PlaybackController(
|
|||||||
if (from !in 0 until snapshot.size || to !in 0 until snapshot.size) return@withLock
|
if (from !in 0 until snapshot.size || to !in 0 until snapshot.size) return@withLock
|
||||||
val player = player ?: return@withLock
|
val player = player ?: return@withLock
|
||||||
val current = currentPosition() ?: return@withLock
|
val current = currentPosition() ?: return@withLock
|
||||||
val undo = QueueUndo(snapshot.entries.map { it.entry }, queueRepository.cursor())
|
val cursor = queueRepository.cursor()
|
||||||
|
val entries = snapshot.entries.map { it.entry }
|
||||||
if (!queueRepository.move(from, to)) return@withLock
|
if (!queueRepository.move(from, to)) return@withLock
|
||||||
this.snapshot = queueRepository.snapshot()
|
this.snapshot = queueRepository.snapshot()
|
||||||
val target = movedCursor(current, from, to)
|
val target = movedCursor(current, from, to)
|
||||||
windowJob?.cancel()
|
windowJob?.cancel()
|
||||||
lastEdit = undo
|
val inWindow = from in windowStart..windowEnd && to in windowStart..windowEnd
|
||||||
if (from in windowStart..windowEnd && to in windowStart..windowEnd) {
|
lastEdit =
|
||||||
|
if (inWindow) {
|
||||||
|
MovedUndo(entries, cursor, from, to)
|
||||||
|
} else {
|
||||||
|
ReloadUndo(entries, cursor)
|
||||||
|
}
|
||||||
|
if (inWindow) {
|
||||||
updating = true
|
updating = true
|
||||||
player.move((from - windowStart).toInt(), (to - windowStart).toInt())
|
player.move((from - windowStart).toInt(), (to - windowStart).toInt())
|
||||||
updating = false
|
updating = false
|
||||||
@@ -230,7 +263,6 @@ class PlaybackController(
|
|||||||
val undo = lastEdit ?: return@withLock
|
val undo = lastEdit ?: return@withLock
|
||||||
val player = player ?: return@withLock
|
val player = player ?: return@withLock
|
||||||
val playing = player.playWhenReady
|
val playing = player.playWhenReady
|
||||||
val playingId = player.currentItem?.id
|
|
||||||
val positionMs = player.currentPositionMs
|
val positionMs = player.currentPositionMs
|
||||||
queueRepository.replace(undo.entries)
|
queueRepository.replace(undo.entries)
|
||||||
val restored = queueRepository.snapshot()
|
val restored = queueRepository.snapshot()
|
||||||
@@ -241,9 +273,44 @@ class PlaybackController(
|
|||||||
return@withLock
|
return@withLock
|
||||||
}
|
}
|
||||||
val target = undo.cursor.coerceIn(0, restored.size - 1)
|
val target = undo.cursor.coerceIn(0, restored.size - 1)
|
||||||
|
val restoredId = queueRepository.itemAt(restored, target)?.song?.id
|
||||||
windowJob?.cancel()
|
windowJob?.cancel()
|
||||||
val same = queueRepository.itemAt(restored, target)?.song?.id == playingId
|
val canMirror = player.currentItem?.id != null && player.currentItem?.id == restoredId
|
||||||
loadWindow(target, autoplay = playing, startPositionMs = if (same) positionMs else 0)
|
val mirrored =
|
||||||
|
when {
|
||||||
|
!canMirror -> {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
undo is RemovedUndo && undo.item != null -> {
|
||||||
|
if (undo.position < windowStart) {
|
||||||
|
windowStart++
|
||||||
|
windowEnd++
|
||||||
|
} else if (undo.position <= windowEnd) {
|
||||||
|
updating = true
|
||||||
|
player.insertAt((undo.position - windowStart).toInt(), undo.item)
|
||||||
|
updating = false
|
||||||
|
windowEnd++
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
undo is MovedUndo && undo.from in windowStart..windowEnd && undo.to in windowStart..windowEnd -> {
|
||||||
|
updating = true
|
||||||
|
player.move((undo.to - windowStart).toInt(), (undo.from - windowStart).toInt())
|
||||||
|
updating = false
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mirrored) {
|
||||||
|
shiftWindowLocked(target)
|
||||||
|
} else {
|
||||||
|
loadWindow(target, autoplay = playing, startPositionMs = if (canMirror) positionMs else 0)
|
||||||
|
}
|
||||||
queueRepository.setCursor(target)
|
queueRepository.setCursor(target)
|
||||||
refresh(target)
|
refresh(target)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ interface PlayerHandle {
|
|||||||
|
|
||||||
fun addLast(item: QueueItem)
|
fun addLast(item: QueueItem)
|
||||||
|
|
||||||
|
fun insertAt(
|
||||||
|
index: Int,
|
||||||
|
item: QueueItem,
|
||||||
|
)
|
||||||
|
|
||||||
fun removeFirst()
|
fun removeFirst()
|
||||||
|
|
||||||
fun removeLast()
|
fun removeLast()
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
package com.subtracks.ui.playback
|
package com.subtracks.ui.playback
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@@ -17,6 +22,7 @@ import androidx.compose.material.icons.rounded.DragHandle
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarDuration
|
import androidx.compose.material3.SnackbarDuration
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
@@ -34,9 +40,11 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
@@ -58,6 +66,7 @@ import com.subtracks.playback.PlaybackController
|
|||||||
import com.subtracks.ui.components.EmptyState
|
import com.subtracks.ui.components.EmptyState
|
||||||
import com.subtracks.ui.components.LoadingState
|
import com.subtracks.ui.components.LoadingState
|
||||||
import com.subtracks.ui.library.SongRow
|
import com.subtracks.ui.library.SongRow
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
@@ -127,6 +136,8 @@ fun QueueRoute(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum class DropIndicator { Above, Below }
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun QueueScreen(
|
fun QueueScreen(
|
||||||
@@ -143,9 +154,11 @@ fun QueueScreen(
|
|||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val density = LocalDensity.current
|
||||||
var rowHeight by remember { mutableFloatStateOf(0f) }
|
var rowHeight by remember { mutableFloatStateOf(0f) }
|
||||||
var dragging by remember { mutableStateOf<Long?>(null) }
|
var dragging by remember { mutableStateOf<Long?>(null) }
|
||||||
var dragOffset by remember { mutableFloatStateOf(0f) }
|
var dragOffset by remember { mutableFloatStateOf(0f) }
|
||||||
|
var target by remember { mutableStateOf<Long?>(null) }
|
||||||
var centered by remember { mutableStateOf(false) }
|
var centered by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val firstPosition = if (items.itemCount > 0) items.peek(0)?.position else null
|
val firstPosition = if (items.itemCount > 0) items.peek(0)?.position else null
|
||||||
@@ -159,6 +172,30 @@ fun QueueScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(dragging) {
|
||||||
|
if (dragging == null) return@LaunchedEffect
|
||||||
|
val threshold = with(density) { 72.dp.toPx() }
|
||||||
|
val step = with(density) { 10.dp.toPx() }
|
||||||
|
while (true) {
|
||||||
|
val from = dragging ?: break
|
||||||
|
val info = listState.layoutInfo.visibleItemsInfo.firstOrNull { items.peek(it.index)?.position == from }
|
||||||
|
if (info != null) {
|
||||||
|
val edge =
|
||||||
|
when {
|
||||||
|
info.offset < listState.layoutInfo.viewportStartOffset + threshold -> -step
|
||||||
|
info.offset + info.size > listState.layoutInfo.viewportEndOffset - threshold -> step
|
||||||
|
else -> 0f
|
||||||
|
}
|
||||||
|
if (edge != 0f) {
|
||||||
|
val consumed = listState.scrollBy(edge)
|
||||||
|
dragOffset += consumed
|
||||||
|
target = dropTarget(items, from, dragOffset, rowHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val showUndo: (String) -> Unit = { message ->
|
val showUndo: (String) -> Unit = { message ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snackbarHostState.currentSnackbarData?.dismiss()
|
snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
@@ -205,6 +242,7 @@ fun QueueScreen(
|
|||||||
coverArt = coverArt,
|
coverArt = coverArt,
|
||||||
dragging = dragging == item.position,
|
dragging = dragging == item.position,
|
||||||
dragOffset = if (dragging == item.position) dragOffset else 0f,
|
dragOffset = if (dragging == item.position) dragOffset else 0f,
|
||||||
|
indicator = indicatorFor(item.position, dragging, target),
|
||||||
onClick = { onPlay(item.position) },
|
onClick = { onPlay(item.position) },
|
||||||
onRemove = {
|
onRemove = {
|
||||||
onRemove(item.position)
|
onRemove(item.position)
|
||||||
@@ -214,23 +252,27 @@ fun QueueScreen(
|
|||||||
onDragStart = {
|
onDragStart = {
|
||||||
dragging = item.position
|
dragging = item.position
|
||||||
dragOffset = 0f
|
dragOffset = 0f
|
||||||
|
target = item.position
|
||||||
|
},
|
||||||
|
onDrag = { amount ->
|
||||||
|
dragOffset += amount
|
||||||
|
target = dropTarget(items, item.position, dragOffset, rowHeight)
|
||||||
},
|
},
|
||||||
onDrag = { dragOffset += it },
|
|
||||||
onDragEnd = {
|
onDragEnd = {
|
||||||
val from = dragging
|
val from = dragging
|
||||||
if (from != null && rowHeight > 0f) {
|
val to = target
|
||||||
val to = (from + (dragOffset / rowHeight).roundToInt()).coerceAtLeast(0)
|
if (from != null && to != null && to != from) {
|
||||||
if (to != from) {
|
onMove(from, to)
|
||||||
onMove(from, to)
|
showUndo("Queue reordered")
|
||||||
showUndo("Queue reordered")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
dragging = null
|
dragging = null
|
||||||
dragOffset = 0f
|
dragOffset = 0f
|
||||||
|
target = null
|
||||||
},
|
},
|
||||||
onDragCancel = {
|
onDragCancel = {
|
||||||
dragging = null
|
dragging = null
|
||||||
dragOffset = 0f
|
dragOffset = 0f
|
||||||
|
target = null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -241,6 +283,30 @@ fun QueueScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun dropTarget(
|
||||||
|
items: LazyPagingItems<QueueWindowItem>,
|
||||||
|
from: Long,
|
||||||
|
offset: Float,
|
||||||
|
rowHeight: Float,
|
||||||
|
): Long {
|
||||||
|
if (items.itemCount == 0 || rowHeight <= 0f) return from
|
||||||
|
val first = items.peek(0)?.position ?: from
|
||||||
|
val last = items.peek(items.itemCount - 1)?.position ?: from
|
||||||
|
return (from + (offset / rowHeight).roundToInt()).coerceIn(minOf(first, last), maxOf(first, last))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun indicatorFor(
|
||||||
|
position: Long,
|
||||||
|
dragging: Long?,
|
||||||
|
target: Long?,
|
||||||
|
): DropIndicator? =
|
||||||
|
when {
|
||||||
|
dragging == null || target == null || position != target -> null
|
||||||
|
target > dragging -> DropIndicator.Below
|
||||||
|
target < dragging -> DropIndicator.Above
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun QueueRow(
|
private fun QueueRow(
|
||||||
item: QueueWindowItem,
|
item: QueueWindowItem,
|
||||||
@@ -248,6 +314,7 @@ private fun QueueRow(
|
|||||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
dragging: Boolean,
|
dragging: Boolean,
|
||||||
dragOffset: Float,
|
dragOffset: Float,
|
||||||
|
indicator: DropIndicator?,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onRemove: () -> Unit,
|
onRemove: () -> Unit,
|
||||||
onSize: (Int) -> Unit,
|
onSize: (Int) -> Unit,
|
||||||
@@ -256,46 +323,70 @@ private fun QueueRow(
|
|||||||
onDragEnd: () -> Unit,
|
onDragEnd: () -> Unit,
|
||||||
onDragCancel: () -> Unit,
|
onDragCancel: () -> Unit,
|
||||||
) {
|
) {
|
||||||
SongRow(
|
Column(
|
||||||
song = item.item.song,
|
|
||||||
coverArtId = item.item.coverArt,
|
|
||||||
coverArt = coverArt,
|
|
||||||
isPlaying = isPlaying,
|
|
||||||
trailingContent = {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
IconButton(onClick = onRemove) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.Close,
|
|
||||||
contentDescription = "Remove from queue",
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.pointerInput(item.position) {
|
|
||||||
detectDragGestures(
|
|
||||||
onDragStart = { onDragStart() },
|
|
||||||
onDrag = { change, amount ->
|
|
||||||
change.consume()
|
|
||||||
onDrag(amount.y)
|
|
||||||
},
|
|
||||||
onDragEnd = onDragEnd,
|
|
||||||
onDragCancel = onDragCancel,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Icon(imageVector = Icons.Rounded.DragHandle, contentDescription = "Reorder")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.zIndex(if (dragging) 1f else 0f)
|
.zIndex(if (dragging) 1f else 0f)
|
||||||
.graphicsLayer { translationY = dragOffset }
|
.graphicsLayer { translationY = dragOffset },
|
||||||
.onSizeChanged { size -> onSize(size.height) }
|
) {
|
||||||
.clickable(onClick = onClick),
|
if (indicator == DropIndicator.Above) DropIndicatorLine()
|
||||||
|
SongRow(
|
||||||
|
song = item.item.song,
|
||||||
|
coverArtId = item.item.coverArt,
|
||||||
|
coverArt = coverArt,
|
||||||
|
isPlaying = isPlaying,
|
||||||
|
trailingContent = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
IconButton(onClick = onRemove) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Close,
|
||||||
|
contentDescription = "Remove from queue",
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.pointerInput(item.position) {
|
||||||
|
detectDragGestures(
|
||||||
|
onDragStart = { onDragStart() },
|
||||||
|
onDrag = { change, amount ->
|
||||||
|
change.consume()
|
||||||
|
onDrag(amount.y)
|
||||||
|
},
|
||||||
|
onDragEnd = onDragEnd,
|
||||||
|
onDragCancel = onDragCancel,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(imageVector = Icons.Rounded.DragHandle, contentDescription = "Reorder")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.background(
|
||||||
|
if (dragging) {
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.95f)
|
||||||
|
} else {
|
||||||
|
Color.Transparent
|
||||||
|
},
|
||||||
|
).onSizeChanged { size -> onSize(size.height) }
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
)
|
||||||
|
if (indicator == DropIndicator.Below) DropIndicatorLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DropIndicatorLine() {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(2.dp)
|
||||||
|
.background(MaterialTheme.colorScheme.primary),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,15 @@ class FakePlayerHandle : PlayerHandle {
|
|||||||
items += item
|
items += item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun insertAt(
|
||||||
|
index: Int,
|
||||||
|
item: QueueItem,
|
||||||
|
) {
|
||||||
|
operations += "insertAt($index, ${item.id})"
|
||||||
|
items.add(index, item)
|
||||||
|
if (index <= this.index) this.index++
|
||||||
|
}
|
||||||
|
|
||||||
override fun removeFirst() {
|
override fun removeFirst() {
|
||||||
operations += "removeFirst"
|
operations += "removeFirst"
|
||||||
items.removeAt(0)
|
items.removeAt(0)
|
||||||
|
|||||||
Reference in New Issue
Block a user