mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
float the up-next block behind the current track
Re-anchor the block whenever playback lands on a different context track (skip, previous, jump, shuffle toggle, restore), so "Up next" always plays next instead of stranding the block behind a track the listener has moved past. reanchorFor reports whether the anchor actually moved, since the current track's combined position can be unchanged even when the block shifts.
This commit is contained in:
@@ -110,6 +110,11 @@ data class QueueWindowItem(
|
||||
val upNext: Boolean = false,
|
||||
)
|
||||
|
||||
data class Reanchored(
|
||||
val position: Long,
|
||||
val changed: Boolean,
|
||||
)
|
||||
|
||||
class QueueRepository(
|
||||
private val db: SubtracksDatabase,
|
||||
) {
|
||||
@@ -336,6 +341,18 @@ class QueueRepository(
|
||||
return if (currentPosition >= snapshot.anchorPlay + 1 + before) currentPosition + 1 else currentPosition
|
||||
}
|
||||
|
||||
suspend fun reanchorFor(
|
||||
snapshot: QueueSnapshot,
|
||||
target: Long,
|
||||
): Reanchored {
|
||||
if (snapshot.upNextSize == 0L || snapshot.isUpNext(target)) return Reanchored(target, false)
|
||||
val play = snapshot.contextPlay(target)
|
||||
val flat = snapshot.flatContext(play) ?: return Reanchored(target, false)
|
||||
if (flat == snapshot.upNextAnchor) return Reanchored(target, false)
|
||||
cursorMutex.withLock { dao.setCursor(cursorRow().copy(upNextAnchor = flat)) }
|
||||
return Reanchored(play, true)
|
||||
}
|
||||
|
||||
suspend fun removeUpNextAt(
|
||||
snapshot: QueueSnapshot,
|
||||
index: Long,
|
||||
|
||||
@@ -273,13 +273,20 @@ class PlaybackController(
|
||||
fun playAt(position: Long) {
|
||||
scope.launch {
|
||||
startLock.withLock {
|
||||
val snapshot = snapshot ?: return@withLock
|
||||
if (snapshot.size == 0L) return@withLock
|
||||
val player = player ?: return@withLock
|
||||
val target = position.coerceIn(0, snapshot.size - 1)
|
||||
var snapshot = snapshot ?: return@withLock
|
||||
if (snapshot.size == 0L) return@withLock
|
||||
val anchor = queueRepository.reanchorFor(snapshot, position)
|
||||
if (anchor.changed) {
|
||||
snapshot = readSnapshot()
|
||||
this@PlaybackController.snapshot = snapshot
|
||||
}
|
||||
val target = anchor.position.coerceIn(0, snapshot.size - 1)
|
||||
windowJob?.cancel()
|
||||
queueRepository.setCursor(target)
|
||||
if (target in windowStart..windowEnd) {
|
||||
if (anchor.changed) {
|
||||
loadWindow(target, autoplay = true)
|
||||
} else if (target in windowStart..windowEnd) {
|
||||
player.ensurePrepared()
|
||||
lastPosition = target
|
||||
player.seekToIndex((target - windowStart).toInt())
|
||||
@@ -635,10 +642,13 @@ class PlaybackController(
|
||||
enabling -> reordered.combined(0)
|
||||
else -> reordered.combined(flat)
|
||||
}
|
||||
queueRepository.setCursor(position)
|
||||
rebuildWindow(queueRepository.window(reordered, position, QUEUE_WINDOW_RADIUS), position)
|
||||
val anchor = queueRepository.reanchorFor(reordered, position)
|
||||
val finalSnapshot = if (anchor.changed) readSnapshot() else reordered
|
||||
this@PlaybackController.snapshot = finalSnapshot
|
||||
queueRepository.setCursor(anchor.position)
|
||||
rebuildWindow(queueRepository.window(finalSnapshot, anchor.position, QUEUE_WINDOW_RADIUS), anchor.position)
|
||||
endedHandled = false
|
||||
refresh(position)
|
||||
refresh(anchor.position)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -741,9 +751,15 @@ class PlaybackController(
|
||||
return@withLock
|
||||
}
|
||||
this.queueSourceId = queueSourceId
|
||||
val position = queueRepository.cursor().coerceIn(0, snapshot.size - 1)
|
||||
val cursor = queueRepository.cursor().coerceIn(0, snapshot.size - 1)
|
||||
val anchor = queueRepository.reanchorFor(snapshot, cursor)
|
||||
if (anchor.changed) {
|
||||
snapshot = queueRepository.snapshot()
|
||||
this.snapshot = snapshot
|
||||
queueRepository.setCursor(anchor.position)
|
||||
}
|
||||
lastSavedPositionMs = queueRepository.cursorPositionMs().coerceAtLeast(0)
|
||||
loadWindow(position, autoplay = false, startPositionMs = lastSavedPositionMs)
|
||||
loadWindow(anchor.position, autoplay = false, startPositionMs = lastSavedPositionMs)
|
||||
}
|
||||
|
||||
private suspend fun stop() = startLock.withLock { stopLocked() }
|
||||
@@ -809,17 +825,24 @@ class PlaybackController(
|
||||
private fun jumpTo(target: Long) {
|
||||
scope.launch {
|
||||
startLock.withLock {
|
||||
val snapshot = readSnapshot()
|
||||
this@PlaybackController.snapshot = snapshot
|
||||
val player = player ?: return@withLock
|
||||
var snapshot = readSnapshot()
|
||||
if (snapshot.size == 0L) return@withLock
|
||||
val anchor = queueRepository.reanchorFor(snapshot, target)
|
||||
if (anchor.changed) snapshot = readSnapshot()
|
||||
this@PlaybackController.snapshot = snapshot
|
||||
val current = currentPosition() ?: return@withLock
|
||||
val dest = target.coerceIn(0, snapshot.size - 1)
|
||||
val wasEnded = player?.isEnded == true
|
||||
val dest = anchor.position.coerceIn(0, snapshot.size - 1)
|
||||
val wasEnded = player.isEnded
|
||||
if (dest == current && !wasEnded) return@withLock
|
||||
windowJob?.cancel()
|
||||
queueRepository.setCursor(dest)
|
||||
if (anchor.changed) {
|
||||
loadWindow(dest, autoplay = player.playWhenReady || wasEnded)
|
||||
endedHandled = false
|
||||
return@withLock
|
||||
}
|
||||
shiftWindowLocked(dest)
|
||||
val player = player ?: return@withLock
|
||||
player.ensurePrepared()
|
||||
player.seekToIndex((dest - windowStart).toInt())
|
||||
if (wasEnded) player.play()
|
||||
|
||||
@@ -353,6 +353,82 @@ class PlaybackControllerTest {
|
||||
assertTrue(runBlocking { queues.snapshot().upNext }.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun skippingPastTheQueueBringsItBackBehindTheCurrentTrack() {
|
||||
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" }
|
||||
|
||||
controller.playAt(4)
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id == "s4"
|
||||
}
|
||||
|
||||
await { runBlocking { controller.upcomingItem()?.id } == "x1" }
|
||||
controller.next()
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id == "x1"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun goingBackInTheContextBringsTheQueueBackWithIt() {
|
||||
seedAlbum(4, sourceId = 1)
|
||||
seedSong("x1", "al2")
|
||||
|
||||
controller.playAlbum(1, "al1", 2)
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id == "s3"
|
||||
}
|
||||
controller.addToQueue(1, QueueKind.Song, "x1")
|
||||
|
||||
controller.previous()
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id == "s2"
|
||||
}
|
||||
|
||||
await { runBlocking { controller.upcomingItem()?.id } == "x1" }
|
||||
controller.next()
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id == "x1"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun togglingShuffleKeepsTheQueueBehindTheCurrentTrack() {
|
||||
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")
|
||||
|
||||
controller.toggleShuffle()
|
||||
await { controller.state.value.shuffle }
|
||||
|
||||
await { runBlocking { controller.upcomingItem()?.id } == "x1" }
|
||||
controller.next()
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id == "x1"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theDurationUsesTheNextTracksMetadataWhileItLoads() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
|
||||
Reference in New Issue
Block a user