mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
address review: re-anchoring and consume edge cases
- jumpTo no longer returns early on a stale current position after a re-anchor, which could leave the player and snapshot unsynchronised. - playNext from inside the block inserts after the entry holding the current track, not always at index 1. - expose a layout version in PlaybackState so the queue view reloads when a re-anchor shifts the combined positions under it. - removeAt recomputes the current track's combined position from the refreshed snapshot instead of assuming a one-down shift. - restore keeps the saved resume position when the anchor moves. - next on the last queued track with repeat off no longer consumes it and leaves it playing; repeat wraps instead. - drop a redundant sort in range; revert a rejected cross-section drag by reloading.
This commit is contained in:
@@ -87,6 +87,18 @@ data class QueueSnapshot(
|
|||||||
fun locateContext(contextPlay: Long): Pair<QueueEntry, Long>? = locateIn(entries, flatContext(contextPlay) ?: return null)
|
fun locateContext(contextPlay: Long): Pair<QueueEntry, Long>? = locateIn(entries, flatContext(contextPlay) ?: return null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun entryIndexAfter(
|
||||||
|
entries: List<ResolvedQueueEntry>,
|
||||||
|
index: Long,
|
||||||
|
): Int {
|
||||||
|
var start = 0L
|
||||||
|
entries.forEachIndexed { position, resolved ->
|
||||||
|
if (index < start + resolved.length) return position + 1
|
||||||
|
start += resolved.length
|
||||||
|
}
|
||||||
|
return entries.size
|
||||||
|
}
|
||||||
|
|
||||||
private fun locateIn(
|
private fun locateIn(
|
||||||
entries: List<ResolvedQueueEntry>,
|
entries: List<ResolvedQueueEntry>,
|
||||||
index: Long,
|
index: Long,
|
||||||
@@ -274,6 +286,13 @@ class QueueRepository(
|
|||||||
): Long? {
|
): Long? {
|
||||||
val upIndex = idsFor(snapshot.upNext).indexOf(songId)
|
val upIndex = idsFor(snapshot.upNext).indexOf(songId)
|
||||||
if (upIndex >= 0) return snapshot.anchorPlay + 1 + upIndex
|
if (upIndex >= 0) return snapshot.anchorPlay + 1 + upIndex
|
||||||
|
return combinedContextIndexOf(snapshot, songId)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun combinedContextIndexOf(
|
||||||
|
snapshot: QueueSnapshot,
|
||||||
|
songId: String,
|
||||||
|
): Long? {
|
||||||
val flat = flatIds(snapshot).indexOf(songId)
|
val flat = flatIds(snapshot).indexOf(songId)
|
||||||
if (flat < 0) return null
|
if (flat < 0) return null
|
||||||
val play = snapshot.shuffleOrder?.indexOf(flat.toLong())?.toLong() ?: flat.toLong()
|
val play = snapshot.shuffleOrder?.indexOf(flat.toLong())?.toLong() ?: flat.toLong()
|
||||||
@@ -299,7 +318,6 @@ class QueueRepository(
|
|||||||
if (blockFirst <= blockLast) items += blockPlayRange(snapshot, blockFirst - anchor - 1, blockLast - anchor - 1)
|
if (blockFirst <= blockLast) items += blockPlayRange(snapshot, blockFirst - anchor - 1, blockLast - anchor - 1)
|
||||||
val afterFirst = maxOf(from, anchor + block + 1)
|
val afterFirst = maxOf(from, anchor + block + 1)
|
||||||
if (afterFirst <= to) items += contextPlayRange(snapshot, afterFirst - block, to - block)
|
if (afterFirst <= to) items += contextPlayRange(snapshot, afterFirst - block, to - block)
|
||||||
items.sortBy { it.position }
|
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,12 +337,19 @@ class QueueRepository(
|
|||||||
playNext: Boolean,
|
playNext: Boolean,
|
||||||
): Long {
|
): Long {
|
||||||
val list = snapshot.upNext.map { it.entry }.toMutableList()
|
val list = snapshot.upNext.map { it.entry }.toMutableList()
|
||||||
val active = snapshot.isUpNext(currentPosition)
|
|
||||||
val insertIndex =
|
val insertIndex =
|
||||||
when {
|
when {
|
||||||
playNext && active -> 1
|
playNext && snapshot.isUpNext(currentPosition) -> {
|
||||||
playNext -> 0
|
entryIndexAfter(snapshot.upNext, snapshot.upNextIndex(currentPosition))
|
||||||
else -> list.size
|
}
|
||||||
|
|
||||||
|
playNext -> {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
list.size
|
||||||
|
}
|
||||||
}.coerceIn(0, list.size)
|
}.coerceIn(0, list.size)
|
||||||
list.add(insertIndex, entry.copy(id = 0, position = 0))
|
list.add(insertIndex, entry.copy(id = 0, position = 0))
|
||||||
val anchor =
|
val anchor =
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ data class PlaybackState(
|
|||||||
val hasPrevious: Boolean = false,
|
val hasPrevious: Boolean = false,
|
||||||
val shuffle: Boolean = false,
|
val shuffle: Boolean = false,
|
||||||
val repeat: RepeatMode = RepeatMode.Off,
|
val repeat: RepeatMode = RepeatMode.Off,
|
||||||
|
val layout: Long = 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
private sealed interface QueueUndo {
|
private sealed interface QueueUndo {
|
||||||
@@ -130,6 +131,7 @@ class PlaybackController(
|
|||||||
private var windowStart = 0L
|
private var windowStart = 0L
|
||||||
private var windowEnd = -1L
|
private var windowEnd = -1L
|
||||||
private var lastPosition: Long? = null
|
private var lastPosition: Long? = null
|
||||||
|
private var layoutVersion = 0L
|
||||||
private var updating = false
|
private var updating = false
|
||||||
private var lastEdit: QueueUndo? = null
|
private var lastEdit: QueueUndo? = null
|
||||||
private var shuffleEnabled = false
|
private var shuffleEnabled = false
|
||||||
@@ -280,6 +282,7 @@ class PlaybackController(
|
|||||||
if (anchor.changed) {
|
if (anchor.changed) {
|
||||||
snapshot = readSnapshot()
|
snapshot = readSnapshot()
|
||||||
this@PlaybackController.snapshot = snapshot
|
this@PlaybackController.snapshot = snapshot
|
||||||
|
layoutVersion++
|
||||||
}
|
}
|
||||||
val target = anchor.position.coerceIn(0, snapshot.size - 1)
|
val target = anchor.position.coerceIn(0, snapshot.size - 1)
|
||||||
windowJob?.cancel()
|
windowJob?.cancel()
|
||||||
@@ -315,18 +318,37 @@ class PlaybackController(
|
|||||||
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 playing = player.playWhenReady
|
val playing = player.playWhenReady
|
||||||
|
val wasUpNext = snapshot.isUpNext(current)
|
||||||
val removed = if (position == current) null else queueRepository.itemAt(snapshot, position)?.toQueueItem()
|
val removed = if (position == current) null else queueRepository.itemAt(snapshot, position)?.toQueueItem()
|
||||||
queueRepository.removeAt(snapshot, position)
|
queueRepository.removeAt(snapshot, position)
|
||||||
this.snapshot = readSnapshot()
|
val updated = readSnapshot()
|
||||||
|
this.snapshot = updated
|
||||||
windowJob?.cancel()
|
windowJob?.cancel()
|
||||||
|
if (updated.size == 0L) {
|
||||||
|
stopLocked()
|
||||||
|
lastEdit = ReloadUndo(entries, cursor, snapshot.shuffleOrder, upNext, snapshot.upNextAnchor)
|
||||||
|
return@withLock
|
||||||
|
}
|
||||||
|
val currentId = player.currentItem?.id
|
||||||
|
val target =
|
||||||
|
(
|
||||||
|
currentId?.let {
|
||||||
|
if (wasUpNext) queueRepository.combinedIndexOf(updated, it) else queueRepository.combinedContextIndexOf(updated, it)
|
||||||
|
} ?: (if (position < current) current - 1 else current)
|
||||||
|
).coerceIn(0, updated.size - 1)
|
||||||
lastEdit =
|
lastEdit =
|
||||||
if (removed == null) {
|
if (removed == null) {
|
||||||
ReloadUndo(entries, cursor, snapshot.shuffleOrder, upNext, snapshot.upNextAnchor)
|
ReloadUndo(entries, cursor, snapshot.shuffleOrder, upNext, snapshot.upNextAnchor)
|
||||||
} else {
|
} else {
|
||||||
RemovedUndo(entries, cursor, target, position, removed, snapshot.shuffleOrder, upNext, snapshot.upNextAnchor)
|
RemovedUndo(entries, cursor, target, position, removed, snapshot.shuffleOrder, upNext, snapshot.upNextAnchor)
|
||||||
}
|
}
|
||||||
|
if (snapshot.upNextSize > 0L || updated.upNextSize > 0L) {
|
||||||
|
layoutVersion++
|
||||||
|
queueRepository.setCursor(target)
|
||||||
|
loadWindow(target, autoplay = playing, startPositionMs = if (position == current) 0 else player.currentPositionMs)
|
||||||
|
return@withLock
|
||||||
|
}
|
||||||
when {
|
when {
|
||||||
position == current -> {
|
position == current -> {
|
||||||
loadWindow(target, autoplay = playing, startPositionMs = 0)
|
loadWindow(target, autoplay = playing, startPositionMs = 0)
|
||||||
@@ -521,17 +543,16 @@ class PlaybackController(
|
|||||||
startLock.withLock {
|
startLock.withLock {
|
||||||
val snapshot = snapshot ?: return@withLock
|
val snapshot = snapshot ?: return@withLock
|
||||||
if (!snapshot.isUpNext(position)) return@withLock
|
if (!snapshot.isUpNext(position)) return@withLock
|
||||||
|
if (position >= snapshot.size - 1 && repeatMode == RepeatMode.Off) return@withLock
|
||||||
queueRepository.removeUpNextAt(snapshot, snapshot.upNextIndex(position))
|
queueRepository.removeUpNextAt(snapshot, snapshot.upNextIndex(position))
|
||||||
val updated = readSnapshot()
|
val updated = readSnapshot()
|
||||||
this.snapshot = updated
|
this.snapshot = updated
|
||||||
|
layoutVersion++
|
||||||
windowJob?.cancel()
|
windowJob?.cancel()
|
||||||
endedHandled = false
|
endedHandled = false
|
||||||
queueRepository.setCursor(position)
|
val next = if (position < updated.size) position else 0L
|
||||||
if (position < updated.size) {
|
queueRepository.setCursor(next)
|
||||||
loadWindow(position, autoplay = true)
|
loadWindow(next, autoplay = true)
|
||||||
} else {
|
|
||||||
refresh(position)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun previous() {
|
fun previous() {
|
||||||
@@ -595,6 +616,7 @@ class PlaybackController(
|
|||||||
val position = queueRepository.addUpNext(snapshot, current, entry, playNext)
|
val position = queueRepository.addUpNext(snapshot, current, entry, playNext)
|
||||||
val updated = readSnapshot()
|
val updated = readSnapshot()
|
||||||
this.snapshot = updated
|
this.snapshot = updated
|
||||||
|
layoutVersion++
|
||||||
queueRepository.setCursor(position)
|
queueRepository.setCursor(position)
|
||||||
windowJob?.cancel()
|
windowJob?.cancel()
|
||||||
rebuildWindow(queueRepository.window(updated, position, QUEUE_WINDOW_RADIUS), position)
|
rebuildWindow(queueRepository.window(updated, position, QUEUE_WINDOW_RADIUS), position)
|
||||||
@@ -644,6 +666,7 @@ class PlaybackController(
|
|||||||
}
|
}
|
||||||
val anchor = queueRepository.reanchorFor(reordered, position)
|
val anchor = queueRepository.reanchorFor(reordered, position)
|
||||||
val finalSnapshot = if (anchor.changed) readSnapshot() else reordered
|
val finalSnapshot = if (anchor.changed) readSnapshot() else reordered
|
||||||
|
if (anchor.changed) layoutVersion++
|
||||||
this@PlaybackController.snapshot = finalSnapshot
|
this@PlaybackController.snapshot = finalSnapshot
|
||||||
queueRepository.setCursor(anchor.position)
|
queueRepository.setCursor(anchor.position)
|
||||||
rebuildWindow(queueRepository.window(finalSnapshot, anchor.position, QUEUE_WINDOW_RADIUS), anchor.position)
|
rebuildWindow(queueRepository.window(finalSnapshot, anchor.position, QUEUE_WINDOW_RADIUS), anchor.position)
|
||||||
@@ -751,15 +774,17 @@ class PlaybackController(
|
|||||||
return@withLock
|
return@withLock
|
||||||
}
|
}
|
||||||
this.queueSourceId = queueSourceId
|
this.queueSourceId = queueSourceId
|
||||||
|
val savedPositionMs = queueRepository.cursorPositionMs().coerceAtLeast(0)
|
||||||
val cursor = queueRepository.cursor().coerceIn(0, snapshot.size - 1)
|
val cursor = queueRepository.cursor().coerceIn(0, snapshot.size - 1)
|
||||||
val anchor = queueRepository.reanchorFor(snapshot, cursor)
|
val anchor = queueRepository.reanchorFor(snapshot, cursor)
|
||||||
if (anchor.changed) {
|
if (anchor.changed) {
|
||||||
snapshot = queueRepository.snapshot()
|
snapshot = queueRepository.snapshot()
|
||||||
this.snapshot = snapshot
|
this.snapshot = snapshot
|
||||||
queueRepository.setCursor(anchor.position)
|
queueRepository.setCursor(anchor.position)
|
||||||
|
queueRepository.setPosition(savedPositionMs)
|
||||||
}
|
}
|
||||||
lastSavedPositionMs = queueRepository.cursorPositionMs().coerceAtLeast(0)
|
lastSavedPositionMs = savedPositionMs
|
||||||
loadWindow(anchor.position, autoplay = false, startPositionMs = lastSavedPositionMs)
|
loadWindow(anchor.position, autoplay = false, startPositionMs = savedPositionMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun stop() = startLock.withLock { stopLocked() }
|
private suspend fun stop() = startLock.withLock { stopLocked() }
|
||||||
@@ -829,12 +854,15 @@ class PlaybackController(
|
|||||||
var snapshot = readSnapshot()
|
var snapshot = readSnapshot()
|
||||||
if (snapshot.size == 0L) return@withLock
|
if (snapshot.size == 0L) return@withLock
|
||||||
val anchor = queueRepository.reanchorFor(snapshot, target)
|
val anchor = queueRepository.reanchorFor(snapshot, target)
|
||||||
if (anchor.changed) snapshot = readSnapshot()
|
if (anchor.changed) {
|
||||||
|
snapshot = readSnapshot()
|
||||||
|
layoutVersion++
|
||||||
|
}
|
||||||
this@PlaybackController.snapshot = snapshot
|
this@PlaybackController.snapshot = snapshot
|
||||||
val current = currentPosition() ?: return@withLock
|
val current = currentPosition() ?: return@withLock
|
||||||
val dest = anchor.position.coerceIn(0, snapshot.size - 1)
|
val dest = anchor.position.coerceIn(0, snapshot.size - 1)
|
||||||
val wasEnded = player.isEnded
|
val wasEnded = player.isEnded
|
||||||
if (dest == current && !wasEnded) return@withLock
|
if (!anchor.changed && dest == current && !wasEnded) return@withLock
|
||||||
windowJob?.cancel()
|
windowJob?.cancel()
|
||||||
queueRepository.setCursor(dest)
|
queueRepository.setCursor(dest)
|
||||||
if (anchor.changed) {
|
if (anchor.changed) {
|
||||||
@@ -1056,6 +1084,7 @@ class PlaybackController(
|
|||||||
hasPrevious = true,
|
hasPrevious = true,
|
||||||
shuffle = shuffleEnabled,
|
shuffle = shuffleEnabled,
|
||||||
repeat = repeatMode,
|
repeat = repeatMode,
|
||||||
|
layout = layoutVersion,
|
||||||
)
|
)
|
||||||
if (player.isPlaying) startPositionTicker() else stopPositionTicker()
|
if (player.isPlaying) startPositionTicker() else stopPositionTicker()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ fun QueueRoute(
|
|||||||
LaunchedEffect(queueContext) {
|
LaunchedEffect(queueContext) {
|
||||||
sourceTitle.value = controller.sourceTitle(queueContext)?.takeIf { it.isNotBlank() } ?: fallbackTitle
|
sourceTitle.value = controller.sourceTitle(queueContext)?.takeIf { it.isNotBlank() } ?: fallbackTitle
|
||||||
}
|
}
|
||||||
LaunchedEffect(playback.shuffle, open) { if (open) viewModel.open() }
|
LaunchedEffect(playback.shuffle, playback.layout, open) { if (open) viewModel.open() }
|
||||||
QueueScreen(
|
QueueScreen(
|
||||||
rows = viewModel.rows,
|
rows = viewModel.rows,
|
||||||
ready = viewModel.ready,
|
ready = viewModel.ready,
|
||||||
@@ -283,6 +283,7 @@ fun QueueRoute(
|
|||||||
onLoadOlder = viewModel::loadOlder,
|
onLoadOlder = viewModel::loadOlder,
|
||||||
onLoadNewer = viewModel::loadNewer,
|
onLoadNewer = viewModel::loadNewer,
|
||||||
onUndo = viewModel::undo,
|
onUndo = viewModel::undo,
|
||||||
|
onRefresh = viewModel::open,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -307,6 +308,7 @@ fun QueueScreen(
|
|||||||
generation: Int = 0,
|
generation: Int = 0,
|
||||||
shuffle: Boolean = false,
|
shuffle: Boolean = false,
|
||||||
contextTitle: String? = null,
|
contextTitle: String? = null,
|
||||||
|
onRefresh: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val fill = rememberViewportFill(listState)
|
val fill = rememberViewportFill(listState)
|
||||||
@@ -415,6 +417,8 @@ fun QueueScreen(
|
|||||||
if (to != from && sameSection) {
|
if (to != from && sameSection) {
|
||||||
onMove(from, to)
|
onMove(from, to)
|
||||||
showUndo("Queue reordered")
|
showUndo("Queue reordered")
|
||||||
|
} else if (to != from) {
|
||||||
|
onRefresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dragId = null
|
dragId = null
|
||||||
|
|||||||
@@ -429,6 +429,189 @@ class PlaybackControllerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun skippingPastTheBlockAfterConsumingKeepsGoing() {
|
||||||
|
seedAlbum(3, sourceId = 1)
|
||||||
|
seedSong("x1", "al2")
|
||||||
|
seedSong("x2", "al2")
|
||||||
|
|
||||||
|
controller.playAlbum(1, "al1", 0)
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "s1"
|
||||||
|
}
|
||||||
|
controller.addToQueue(1, QueueKind.Song, "x1")
|
||||||
|
controller.addToQueue(1, QueueKind.Song, "x2")
|
||||||
|
|
||||||
|
controller.playAt(2)
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "x2"
|
||||||
|
}
|
||||||
|
controller.next()
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "s2"
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.next()
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "s3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun playNextFromInsideTheBlockGoesAfterTheCurrentTrack() {
|
||||||
|
seedAlbum(3, sourceId = 1)
|
||||||
|
seedSong("x1", "al2")
|
||||||
|
seedSong("x2", "al2")
|
||||||
|
seedSong("x3", "al2")
|
||||||
|
|
||||||
|
controller.playAlbum(1, "al1", 0)
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "s1"
|
||||||
|
}
|
||||||
|
controller.addToQueue(1, QueueKind.Song, "x1")
|
||||||
|
controller.addToQueue(1, QueueKind.Song, "x2")
|
||||||
|
controller.playAt(2)
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "x2"
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.playNext(1, QueueKind.Song, "x3")
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "x2"
|
||||||
|
}
|
||||||
|
await { runBlocking { controller.upcomingItem()?.id } == "x3" }
|
||||||
|
|
||||||
|
controller.next()
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "x3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removingAContextTrackUnderTheBlockKeepsTheCursorOnThePlayingTrack() {
|
||||||
|
seedAlbum(3, sourceId = 1)
|
||||||
|
seedSong("x1", "al2")
|
||||||
|
|
||||||
|
controller.playAlbum(1, "al1", 0)
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "s1"
|
||||||
|
}
|
||||||
|
controller.addToQueue(1, QueueKind.Song, "x1")
|
||||||
|
await { runBlocking { controller.upcomingItem()?.id } == "x1" }
|
||||||
|
controller.next()
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "x1"
|
||||||
|
}
|
||||||
|
|
||||||
|
runBlocking { controller.removeAt(0) }
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "x1"
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(1L, runBlocking { queues.cursor() })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun restoringKeepsTheSavedPositionWhenTheAnchorMoves() {
|
||||||
|
seedAlbum(3, sourceId = 1)
|
||||||
|
seedSong("x1", "al2")
|
||||||
|
seedSong("x2", "al2")
|
||||||
|
|
||||||
|
controller.playAlbum(1, "al1", 0)
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "s1"
|
||||||
|
}
|
||||||
|
controller.addToQueue(1, QueueKind.Song, "x1")
|
||||||
|
controller.addToQueue(1, QueueKind.Song, "x2")
|
||||||
|
controller.playAt(2)
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "x2"
|
||||||
|
}
|
||||||
|
controller.next()
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "s2"
|
||||||
|
}
|
||||||
|
|
||||||
|
runBlocking { queues.setPosition(12_000L) }
|
||||||
|
|
||||||
|
val restoredHandle = FakePlayerHandle()
|
||||||
|
val restored = PlaybackController(sources, queues, FakePlayerConnection(restoredHandle), dispatcher = dispatcher)
|
||||||
|
restored.connect()
|
||||||
|
await {
|
||||||
|
restored.state.value.item
|
||||||
|
?.id == "s2"
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(12_000L, restored.positionMs.value)
|
||||||
|
restored.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nextOnTheLastQueuedTrackDoesNotStrandIt() {
|
||||||
|
seedAlbum(3, sourceId = 1)
|
||||||
|
seedSong("x1", "al2")
|
||||||
|
|
||||||
|
controller.playAlbum(1, "al1", 2)
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "s3"
|
||||||
|
}
|
||||||
|
controller.addToQueue(1, QueueKind.Song, "x1")
|
||||||
|
await { runBlocking { controller.upcomingItem()?.id } == "x1" }
|
||||||
|
controller.next()
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "x1"
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.next()
|
||||||
|
Thread.sleep(200)
|
||||||
|
|
||||||
|
assertFalse(runBlocking { queues.snapshot().upNext }.isEmpty())
|
||||||
|
assertEquals(
|
||||||
|
"x1",
|
||||||
|
controller.state.value.item
|
||||||
|
?.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun reanchoringBumpsTheLayoutVersion() {
|
||||||
|
seedAlbum(4, sourceId = 1)
|
||||||
|
seedSong("x1", "al2")
|
||||||
|
|
||||||
|
controller.playAlbum(1, "al1", 1)
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "s2"
|
||||||
|
}
|
||||||
|
controller.addToQueue(1, QueueKind.Song, "x1")
|
||||||
|
await { runBlocking { controller.upcomingItem()?.id } == "x1" }
|
||||||
|
val before = controller.state.value.layout
|
||||||
|
|
||||||
|
controller.playAt(4)
|
||||||
|
await {
|
||||||
|
controller.state.value.item
|
||||||
|
?.id == "s4"
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(controller.state.value.layout > before)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun theDurationUsesTheNextTracksMetadataWhileItLoads() {
|
fun theDurationUsesTheNextTracksMetadataWhileItLoads() {
|
||||||
seedAlbum(3, sourceId = 1)
|
seedAlbum(3, sourceId = 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user