fix queue delete latency, reorder hitch and open scroll

This commit is contained in:
austinried
2026-10-04 00:29:52 +00:00
parent bb42975bb4
commit 147916ff06
2 changed files with 79 additions and 41 deletions
@@ -244,19 +244,40 @@ class PlaybackController(
this.snapshot = queueRepository.snapshot() this.snapshot = queueRepository.snapshot()
val target = movedCursor(current, from, to) val target = movedCursor(current, from, to)
windowJob?.cancel() windowJob?.cancel()
val inWindow = from in windowStart..windowEnd && to in windowStart..windowEnd val fromInWindow = from in windowStart..windowEnd
val toInWindow = to in windowStart..windowEnd
lastEdit = lastEdit =
if (inWindow) { if (fromInWindow == toInWindow) {
MovedUndo(entries, cursor, target, from, to) MovedUndo(entries, cursor, target, from, to)
} else { } else {
ReloadUndo(entries, cursor) ReloadUndo(entries, cursor)
} }
if (inWindow) { when {
updating = true fromInWindow && toInWindow -> {
player.move((from - windowStart).toInt(), (to - windowStart).toInt()) updating = true
updating = false player.move((from - windowStart).toInt(), (to - windowStart).toInt())
} else { updating = false
loadWindow(target, autoplay = player.playWhenReady, startPositionMs = player.currentPositionMs) }
!fromInWindow && !toInWindow -> {
// The moved track never enters the window, so the playlist is unchanged; only
// the window's positions shift, and a move that spans it shifts them by one.
when {
from < windowStart && to > windowEnd -> {
windowStart--
windowEnd--
}
from > windowEnd && to < windowStart -> {
windowStart++
windowEnd++
}
}
}
else -> {
loadWindow(target, autoplay = player.playWhenReady, startPositionMs = player.currentPositionMs)
}
} }
shiftWindowLocked(target) shiftWindowLocked(target)
queueRepository.setCursor(target) queueRepository.setCursor(target)
@@ -311,6 +332,21 @@ class PlaybackController(
true true
} }
undo is MovedUndo && undo.from !in windowStart..windowEnd && undo.to !in windowStart..windowEnd -> {
when {
undo.from < windowStart && undo.to > windowEnd -> {
windowStart++
windowEnd++
}
undo.from > windowEnd && undo.to < windowStart -> {
windowStart--
windowEnd--
}
}
true
}
else -> { else -> {
false false
} }
@@ -52,15 +52,18 @@ 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.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeoutOrNull
import org.koin.compose.koinInject import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState import sh.calvin.reorderable.rememberReorderableLazyListState
private const val LOAD_THRESHOLD = 5 private const val LOAD_THRESHOLD = 5
private const val POSITION_TIMEOUT_MS = 1_000L
data class QueueRow( data class QueueRow(
val id: Long, val id: Long,
@@ -94,7 +97,10 @@ class QueueViewModel(
first = 0 first = 0
last = -1 last = -1
if (size > 0L) { if (size > 0L) {
val cursor = (playbackController.state.value.position ?: 0L).coerceIn(0, size - 1) val cursor =
withTimeoutOrNull(POSITION_TIMEOUT_MS) {
playbackController.state.first { it.position != null }.position
}?.coerceIn(0, size - 1) ?: 0L
val start = (cursor - QUEUE_CHUNK / 2).coerceAtLeast(0) val start = (cursor - QUEUE_CHUNK / 2).coerceAtLeast(0)
val end = (start + QUEUE_CHUNK - 1).coerceAtMost(size - 1) val end = (start + QUEUE_CHUNK - 1).coerceAtMost(size - 1)
rows.addAll(queueRepository.range(snapshot, start, end).map(::newRow)) rows.addAll(queueRepository.range(snapshot, start, end).map(::newRow))
@@ -172,8 +178,31 @@ class QueueViewModel(
fun remove(position: Long) { fun remove(position: Long) {
viewModelScope.launch { viewModelScope.launch {
val index = rows.indexOfFirst { it.position == position }
if (index >= 0) {
rows.removeAt(index)
for (i in index until rows.size) {
val row = rows[i]
rows[i] = row.copy(position = row.position - 1)
}
}
val next = (rows.lastOrNull()?.position ?: (first - 1)) + 1
playbackController.removeAt(position) playbackController.removeAt(position)
mutex.withLock { removeRow(position) } mutex.withLock {
if (rows.isEmpty()) {
size = 0
first = 0
last = -1
return@withLock
}
val snapshot = queueRepository.snapshot()
size = snapshot.size
if (next <= size - 1) {
queueRepository.range(snapshot, next, next).firstOrNull()?.let { rows.add(newRow(it)) }
}
first = rows.first().position
last = rows.last().position
}
} }
} }
@@ -184,33 +213,6 @@ class QueueViewModel(
} }
} }
private suspend fun removeRow(position: Long) {
val index = rows.indexOfFirst { it.position == position }
if (index < 0) {
reload()
return
}
rows.removeAt(index)
for (i in index until rows.size) {
val row = rows[i]
rows[i] = row.copy(position = row.position - 1)
}
val snapshot = queueRepository.snapshot()
size = snapshot.size
if (rows.isEmpty()) {
first = 0
last = -1
return
}
first = rows.first().position
last = rows.last().position
val next = last + 1
if (next <= size - 1) {
queueRepository.range(snapshot, next, next).firstOrNull()?.let { rows.add(newRow(it)) }
last = rows.last().position
}
}
private suspend fun reload() { private suspend fun reload() {
val snapshot = queueRepository.snapshot() val snapshot = queueRepository.snapshot()
size = snapshot.size size = snapshot.size
@@ -292,10 +294,10 @@ fun QueueScreen(
if (centered || !ready || rows.isEmpty()) return@LaunchedEffect if (centered || !ready || rows.isEmpty()) return@LaunchedEffect
val position = currentPosition ?: return@LaunchedEffect val position = currentPosition ?: return@LaunchedEffect
val index = rows.indexOfFirst { it.position == position } val index = rows.indexOfFirst { it.position == position }
if (index >= 0) { if (index < 0) return@LaunchedEffect
listState.scrollToItem(index) snapshotFlow { listState.layoutInfo.totalItemsCount }.first { it > 0 }
centered = true listState.scrollToItem(index)
} centered = true
} }
val reorderState = val reorderState =