mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
avoid reloading the window on queue edits; add clear up next
- moves that straddle the player window, and deletes that are not the current track, rebuilt the playlist with setWindow, restarting the playing track (a pause). They now reuse rebuildWindow, which re-lays the window around the playing item without replacing it. - deleting the currently playing queued track now removes it from the player in place so it advances to the next track without a reload. - the Up next section header has a clear button that drops the whole queued block and resumes the context after the anchor.
This commit is contained in:
@@ -378,6 +378,10 @@ class QueueRepository(
|
||||
return Reanchored(play, true)
|
||||
}
|
||||
|
||||
suspend fun clearUpNext() {
|
||||
writeUpNext(emptyList())
|
||||
}
|
||||
|
||||
suspend fun removeUpNextAt(
|
||||
snapshot: QueueSnapshot,
|
||||
index: Long,
|
||||
|
||||
@@ -346,7 +346,19 @@ class PlaybackController(
|
||||
}
|
||||
if (snapshot.upNextSize > 0L || updated.upNextSize > 0L) {
|
||||
queueRepository.setCursor(target)
|
||||
loadWindow(target, autoplay = playing, startPositionMs = if (position == current) 0 else player.currentPositionMs)
|
||||
if (position != current) {
|
||||
rebuildWindow(queueRepository.window(updated, target, QUEUE_WINDOW_RADIUS), target)
|
||||
refresh(target)
|
||||
} else if (target - windowStart in 0 until player.itemCount) {
|
||||
updating = true
|
||||
player.removeAt((target - windowStart).toInt())
|
||||
updating = false
|
||||
windowEnd--
|
||||
shiftWindowLocked(target)
|
||||
refresh(target)
|
||||
} else {
|
||||
loadWindow(target, autoplay = playing)
|
||||
}
|
||||
return@withLock
|
||||
}
|
||||
when {
|
||||
@@ -389,7 +401,8 @@ class PlaybackController(
|
||||
val entries = snapshot.entries.map { it.entry }
|
||||
val upNext = snapshot.upNext.map { it.entry }
|
||||
if (!queueRepository.move(snapshot, from, to)) return@withLock
|
||||
this.snapshot = readSnapshot()
|
||||
val updated = readSnapshot()
|
||||
this.snapshot = updated
|
||||
val target = movedCursor(current, from, to)
|
||||
lastPosition = target
|
||||
windowJob?.cancel()
|
||||
@@ -425,7 +438,7 @@ class PlaybackController(
|
||||
}
|
||||
|
||||
else -> {
|
||||
loadWindow(target, autoplay = player.playWhenReady, startPositionMs = player.currentPositionMs)
|
||||
rebuildWindow(queueRepository.window(updated, target, QUEUE_WINDOW_RADIUS), target)
|
||||
}
|
||||
}
|
||||
shiftWindowLocked(target)
|
||||
@@ -567,6 +580,34 @@ class PlaybackController(
|
||||
}
|
||||
}
|
||||
|
||||
fun clearUpNext() {
|
||||
scope.launch {
|
||||
startLock.withLock {
|
||||
val snapshot = snapshot ?: return@withLock
|
||||
if (snapshot.upNextSize == 0L) return@withLock
|
||||
player ?: return@withLock
|
||||
val current = currentPosition() ?: return@withLock
|
||||
val wasUpNext = snapshot.isUpNext(current)
|
||||
queueRepository.clearUpNext()
|
||||
val updated = readSnapshot()
|
||||
this@PlaybackController.snapshot = updated
|
||||
layoutVersion++
|
||||
windowJob?.cancel()
|
||||
endedHandled = false
|
||||
val contextPlay = if (wasUpNext) snapshot.anchorPlay + 1 else snapshot.contextPlay(current)
|
||||
val target = contextPlay.coerceIn(0, (updated.size - 1).coerceAtLeast(0))
|
||||
queueRepository.setCursor(target)
|
||||
lastPosition = target
|
||||
if (wasUpNext) {
|
||||
loadWindow(target, autoplay = true)
|
||||
} else {
|
||||
rebuildWindow(queueRepository.window(updated, target, QUEUE_WINDOW_RADIUS), target)
|
||||
refresh(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun previous() {
|
||||
val player = player ?: return
|
||||
if (player.currentPositionMs > RESTART_THRESHOLD_MS) {
|
||||
|
||||
@@ -299,6 +299,7 @@ fun QueueRoute(
|
||||
currentSongId = playback.item?.id,
|
||||
shuffle = playback.shuffle,
|
||||
contextTitle = sourceTitle.value,
|
||||
onClearUpNext = controller::clearUpNext,
|
||||
coverArt = sourceRepository::coverArt,
|
||||
onBack = onBack,
|
||||
onPlay = viewModel::play,
|
||||
@@ -332,6 +333,7 @@ fun QueueScreen(
|
||||
generation: Int = 0,
|
||||
shuffle: Boolean = false,
|
||||
contextTitle: String? = null,
|
||||
onClearUpNext: () -> Unit = {},
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val fill = rememberViewportFill(listState)
|
||||
@@ -403,7 +405,10 @@ fun QueueScreen(
|
||||
rows.forEachIndexed { index, row ->
|
||||
if (index == 0 || rows[index - 1].upNext != row.upNext) {
|
||||
item(key = "header-$index") {
|
||||
QueueSectionHeader(if (row.upNext) "Up next" else contextTitle ?: "Next up")
|
||||
QueueSectionHeader(
|
||||
label = if (row.upNext) "Up next" else contextTitle ?: "Next up",
|
||||
onClear = if (row.upNext) onClearUpNext else null,
|
||||
)
|
||||
}
|
||||
}
|
||||
val enabled = row.upNext || !shuffle
|
||||
@@ -471,13 +476,26 @@ fun QueueScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QueueSectionHeader(label: String) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 16.dp, bottom = 4.dp),
|
||||
)
|
||||
private fun QueueSectionHeader(
|
||||
label: String,
|
||||
onClear: (() -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 16.dp, end = 4.dp, bottom = 4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (onClear != null) {
|
||||
IconButton(onClick = onClear) {
|
||||
Icon(Icons.Rounded.Close, contentDescription = "Clear up next", modifier = Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<QueueRow>.rowIndexOf(key: Any?): Int {
|
||||
|
||||
@@ -851,6 +851,81 @@ class PlaybackControllerTest {
|
||||
assertFalse(handle.operations.any { it.startsWith("setWindow") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clearingTheQueueResumesTheContext() {
|
||||
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")
|
||||
controller.next()
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id == "x1"
|
||||
}
|
||||
|
||||
controller.clearUpNext()
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id == "s2"
|
||||
}
|
||||
|
||||
assertTrue(runBlocking { queues.snapshot().upNext }.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clearingTheQueueWhileOnTheContextKeepsPlaying() {
|
||||
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.clearUpNext()
|
||||
Thread.sleep(200)
|
||||
|
||||
assertEquals(
|
||||
"s1",
|
||||
controller.state.value.item
|
||||
?.id,
|
||||
)
|
||||
assertTrue(runBlocking { queues.snapshot().upNext }.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun movingAFarQueuedTrackDoesNotReloadTheWindow() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
seedAlbumTracks("al2", 60)
|
||||
|
||||
controller.playAlbum(1, "al1", 0)
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id == "s1"
|
||||
}
|
||||
controller.addToQueue(1, QueueKind.Album, "al2")
|
||||
await { runBlocking { controller.upcomingItem()?.id } == "al2-b1" }
|
||||
|
||||
handle.operations.clear()
|
||||
runBlocking { controller.move(50, 2) }
|
||||
Thread.sleep(300)
|
||||
|
||||
assertEquals(
|
||||
"s1",
|
||||
controller.state.value.item
|
||||
?.id,
|
||||
)
|
||||
assertFalse(handle.operations.any { it.startsWith("setWindow") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nextFollowsTheReorderedQueue() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
@@ -1846,6 +1921,32 @@ class PlaybackControllerTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun seedAlbumTracks(
|
||||
albumId: String,
|
||||
count: Int,
|
||||
) {
|
||||
runBlocking {
|
||||
db.libraryDao().upsertSongs(
|
||||
(1..count).map { track ->
|
||||
Song(
|
||||
sourceId = 1,
|
||||
id = "$albumId-b$track",
|
||||
albumId = albumId,
|
||||
artistId = "ar1",
|
||||
title = "Album $albumId track $track",
|
||||
album = "Album $albumId",
|
||||
artist = "Artist",
|
||||
duration = 100,
|
||||
track = track.toLong(),
|
||||
disc = 1,
|
||||
starred = null,
|
||||
genre = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun seedSong(
|
||||
id: String,
|
||||
albumId: String,
|
||||
|
||||
Reference in New Issue
Block a user