stop the queue view extending itself mid-drag; restore context drag scroll

- the visible-items flow fired loadOlder/loadNewer during a drag, and the
  live reorder leaves row positions out of order, so the window re-added
  rows it already had — the queue duplicated under the dragged item.
  Skip loading while a drag is active, and extend from the min/max loaded
  position rather than the first/last row.
- auto-scroll is back for the context reorder state (a normal queue
  scrolls while dragging again); the up-next state keeps it disabled
  because its block is followed by context rows the scroller would run
  past with nowhere to drop.
This commit is contained in:
austinried
2026-10-04 00:30:00 +00:00
parent 5abc48848b
commit b189ae0cc5
@@ -158,11 +158,11 @@ class QueueViewModel(
}
fun loadOlder() {
val top = rows.firstOrNull()?.position ?: return
val top = rows.minOfOrNull { it.position } ?: return
if (top <= 0L) return
viewModelScope.launch {
mutex.withLock {
val first = rows.firstOrNull()?.position ?: return@withLock
val first = rows.minOfOrNull { it.position } ?: return@withLock
if (first <= 0L) return@withLock
val snapshot = queueRepository.snapshot()
val loaded = queueRepository.range(snapshot, (first - QUEUE_CHUNK).coerceAtLeast(0), first - 1)
@@ -174,10 +174,10 @@ class QueueViewModel(
}
fun loadNewer() {
if (rows.lastOrNull() == null) return
if (rows.isEmpty()) return
viewModelScope.launch {
mutex.withLock {
val last = rows.lastOrNull()?.position ?: return@withLock
val last = rows.maxOfOrNull { it.position } ?: return@withLock
val snapshot = queueRepository.snapshot()
if (last >= snapshot.size - 1) return@withLock
val to = (last + QUEUE_CHUNK).coerceAtMost(snapshot.size - 1)
@@ -329,14 +329,14 @@ fun QueueScreen(
// One reorder state per section: sh.calvin.reorderable only targets keys registered with the
// state that started the drag, so an up-next drag cannot reach the context rows and vice versa.
// scrollThreshold 0 disables its auto-scroll, which otherwise keeps scrolling past the section
// looking for a target that does not exist.
// The up-next state disables auto-scroll (scrollThreshold 0): its block is followed by context
// rows, so the scroller keeps going past the last place the item can actually drop.
val upNextReorder =
rememberReorderableLazyListState(listState, scrollThreshold = 0.dp) { from, to ->
onReorder(rows.rowIndexOf(from.key), rows.rowIndexOf(to.key))
}
val contextReorder =
rememberReorderableLazyListState(listState, scrollThreshold = 0.dp) { from, to ->
rememberReorderableLazyListState(listState) { from, to ->
onReorder(rows.rowIndexOf(from.key), rows.rowIndexOf(to.key))
}
@@ -349,6 +349,9 @@ fun QueueScreen(
val last = info.visibleItemsInfo.lastOrNull { it.key is Long }?.let { rows.rowIndexOf(it.key) } ?: -1
first to last
}.collect { (first, last) ->
// Mid-drag the live reorder leaves row positions inconsistent, so extending here would
// re-add rows that are already loaded.
if (dragId != null) return@collect
if (first in 0..LOAD_THRESHOLD) onLoadOlder()
if (last >= 0 && last >= rows.size - 1 - LOAD_THRESHOLD) onLoadNewer()
}