mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-15 00:42:54 +01:00
ui: serialize queue moves with row updates
This commit is contained in:
@@ -194,13 +194,11 @@ class QueueViewModel(
|
|||||||
rows.addAll(updated)
|
rows.addAll(updated)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Only the head of a sparse offline window was resolved; keep the tail on screen,
|
|
||||||
// dropping any tail row whose id was reused for a head row.
|
|
||||||
val usedIds = updated.mapTo(HashSet()) { it.id }
|
val usedIds = updated.mapTo(HashSet()) { it.id }
|
||||||
val tail = rows.filter { it.position > end && it.id !in usedIds }
|
val unresolvedTail = rows.filter { it.id !in usedIds }
|
||||||
rows.clear()
|
rows.clear()
|
||||||
rows.addAll(updated)
|
rows.addAll(updated)
|
||||||
rows.addAll(tail)
|
rows.addAll(unresolvedTail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,10 +305,12 @@ class QueueViewModel(
|
|||||||
reorderPending = true
|
reorderPending = true
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val applied = playbackController.move(from, to, resetVersion)
|
val applied =
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
|
val applied = playbackController.move(from, to, resetVersion)
|
||||||
if (!applied) rollbackReorder(edit)
|
if (!applied) rollbackReorder(edit)
|
||||||
reconcileLocked()
|
reconcileLocked()
|
||||||
|
applied
|
||||||
}
|
}
|
||||||
onComplete(applied)
|
onComplete(applied)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -509,6 +509,34 @@ class QueueViewModelTest {
|
|||||||
assertEquals(before.map { it.position }, rowsSnapshot().map { it.position })
|
assertEquals(before.map { it.position }, rowsSnapshot().map { it.position })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun windowLoadingWaitsForRejectedMoveRollback() {
|
||||||
|
openReconciledSparseOfflineQueue()
|
||||||
|
markDownloaded(1, "s0")
|
||||||
|
val canonical = resolvedQueueIds()
|
||||||
|
val before = rowsSnapshot()
|
||||||
|
val dragged = before[1]
|
||||||
|
val paused = pauseQueueMutation()
|
||||||
|
val completion = CompletableDeferred<Boolean>()
|
||||||
|
try {
|
||||||
|
runOnMainSuspending {
|
||||||
|
viewModel.reorder(1, 2)
|
||||||
|
val move = viewModel.rows.dropMoveFor(dragged.id)!!
|
||||||
|
assertTrue(viewModel.move(move.from, move.to, move.resetVersion, completion::complete))
|
||||||
|
yield()
|
||||||
|
viewModel.loadOlder()
|
||||||
|
yield()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
resumeQueueMutation(paused)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFalse(awaitCompletion(completion))
|
||||||
|
assertEquals(canonical, resolvedQueueIds())
|
||||||
|
assertEquals(listOf("s0", "s1", "s801", "s901"), rowsSnapshot().map { it.song.song.id })
|
||||||
|
assertEquals(listOf(0L, 1L, 800L, 900L), rowsSnapshot().map { it.position })
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun rejectedSparseTailRemovalRestoresTheVisibleOrder() {
|
fun rejectedSparseTailRemovalRestoresTheVisibleOrder() {
|
||||||
openReconciledSparseOfflineQueue()
|
openReconciledSparseOfflineQueue()
|
||||||
@@ -667,13 +695,13 @@ class QueueViewModelTest {
|
|||||||
val secondMove = viewModel.rows.dropMoveFor(before[2].id)!!
|
val secondMove = viewModel.rows.dropMoveFor(before[2].id)!!
|
||||||
assertFalse(viewModel.move(secondMove.from, secondMove.to, secondMove.resetVersion, secondCompletion::complete))
|
assertFalse(viewModel.move(secondMove.from, secondMove.to, secondMove.resetVersion, secondCompletion::complete))
|
||||||
}
|
}
|
||||||
await { rowsSnapshot().map { it.song.song.id } == expectedFirst }
|
|
||||||
} finally {
|
} finally {
|
||||||
resumeQueueMutation(paused)
|
resumeQueueMutation(paused)
|
||||||
}
|
}
|
||||||
|
|
||||||
assertTrue(awaitCompletion(firstCompletion))
|
assertTrue(awaitCompletion(firstCompletion))
|
||||||
assertFalse(awaitCompletion(secondCompletion))
|
assertFalse(awaitCompletion(secondCompletion))
|
||||||
|
assertEquals(expectedFirst, rowsSnapshot().map { it.song.song.id })
|
||||||
assertEquals(expectedFirst, resolvedQueueIds().filter { it in visibleIds })
|
assertEquals(expectedFirst, resolvedQueueIds().filter { it in visibleIds })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,6 +727,47 @@ class QueueViewModelTest {
|
|||||||
assertEquals(listOf("s1", "s801"), resolvedQueueIds().filter { it in setOf("s1", "s801", "s901") })
|
assertEquals(listOf("s1", "s801"), resolvedQueueIds().filter { it in setOf("s1", "s801", "s901") })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removalQueuedBehindMoveDoesNotUseItsOldPosition() {
|
||||||
|
runBlocking { seedSongs(3) }
|
||||||
|
controller.playAlbum(1, "al1", 0)
|
||||||
|
await { controller.state.value.position == 0L }
|
||||||
|
viewModel.open()
|
||||||
|
await { viewModel.ready }
|
||||||
|
val before = rowsSnapshot()
|
||||||
|
val paused = pauseQueueMutation()
|
||||||
|
val moveCompletion = CompletableDeferred<Boolean>()
|
||||||
|
val removalCompletion = CompletableDeferred<Boolean>()
|
||||||
|
try {
|
||||||
|
runOnMainSuspending {
|
||||||
|
assertTrue(
|
||||||
|
viewModel.move(
|
||||||
|
before[0].position,
|
||||||
|
before[2].position,
|
||||||
|
before[0].resetVersion,
|
||||||
|
moveCompletion::complete,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
yield()
|
||||||
|
assertEquals(
|
||||||
|
"s2",
|
||||||
|
viewModel.rows[1]
|
||||||
|
.song.song.id,
|
||||||
|
)
|
||||||
|
viewModel.remove(before[1].position, removalCompletion::complete)
|
||||||
|
yield()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
resumeQueueMutation(paused)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(awaitCompletion(moveCompletion))
|
||||||
|
assertFalse(awaitCompletion(removalCompletion))
|
||||||
|
assertEquals(listOf("s2", "s3", "s1"), resolvedQueueIds())
|
||||||
|
assertEquals(listOf("s2", "s3", "s1"), rowsSnapshot().map { it.song.song.id })
|
||||||
|
assertEquals(listOf(0L, 1L, 2L), rowsSnapshot().map { it.position })
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun moveRejectsRowsFromDifferentResetGenerations() {
|
fun moveRejectsRowsFromDifferentResetGenerations() {
|
||||||
runOnMain {
|
runOnMain {
|
||||||
|
|||||||
Reference in New Issue
Block a user