fix the queue paging refresh key and drag edge detection

This commit is contained in:
austinried
2026-10-04 00:29:52 +00:00
parent 2d64826164
commit c2ecc63fe5
4 changed files with 71 additions and 12 deletions
@@ -9,19 +9,27 @@ class QueuePagingSource(
private val repository: QueueRepository, private val repository: QueueRepository,
private val initialPosition: Long, private val initialPosition: Long,
) : PagingSource<Long, QueueWindowItem>() { ) : PagingSource<Long, QueueWindowItem>() {
override fun getRefreshKey(state: PagingState<Long, QueueWindowItem>): Long? = override fun getRefreshKey(state: PagingState<Long, QueueWindowItem>): Long? {
state.anchorPosition?.let { (it / QUEUE_PAGE_SIZE).toLong() } val anchor = state.anchorPosition ?: return null
val first =
state.pages
.firstOrNull()
?.data
?.firstOrNull()
?.position ?: return null
return (first + anchor) / QUEUE_PAGE_SIZE
}
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, QueueWindowItem> { override suspend fun load(params: LoadParams<Long>): LoadResult<Long, QueueWindowItem> {
val snapshot = repository.snapshot() val snapshot = repository.snapshot()
val page = params.key ?: (initialPosition.coerceAtLeast(0) / QUEUE_PAGE_SIZE) if (snapshot.size == 0L) return LoadResult.Page(emptyList(), null, null)
val lastPage = (snapshot.size - 1) / QUEUE_PAGE_SIZE
val page = (params.key ?: (initialPosition.coerceAtLeast(0) / QUEUE_PAGE_SIZE)).coerceIn(0, lastPage)
val offset = page * QUEUE_PAGE_SIZE val offset = page * QUEUE_PAGE_SIZE
val previousKey = if (page > 0) page - 1 else null
if (offset >= snapshot.size) return LoadResult.Page(emptyList(), previousKey, null)
val end = (offset + params.loadSize).coerceAtMost(snapshot.size) val end = (offset + params.loadSize).coerceAtMost(snapshot.size)
return LoadResult.Page( return LoadResult.Page(
data = repository.range(snapshot, offset, end - 1), data = repository.range(snapshot, offset, end - 1),
prevKey = previousKey, prevKey = if (page > 0) page - 1 else null,
nextKey = if (end < snapshot.size) end / QUEUE_PAGE_SIZE else null, nextKey = if (end < snapshot.size) end / QUEUE_PAGE_SIZE else null,
) )
} }
@@ -49,11 +49,13 @@ data class PlaybackState(
private sealed interface QueueUndo { private sealed interface QueueUndo {
val entries: List<QueueEntry> val entries: List<QueueEntry>
val cursor: Long val cursor: Long
val current: Long?
} }
private data class RemovedUndo( private data class RemovedUndo(
override val entries: List<QueueEntry>, override val entries: List<QueueEntry>,
override val cursor: Long, override val cursor: Long,
override val current: Long,
val position: Long, val position: Long,
val item: QueueItem?, val item: QueueItem?,
) : QueueUndo ) : QueueUndo
@@ -61,6 +63,7 @@ private data class RemovedUndo(
private data class MovedUndo( private data class MovedUndo(
override val entries: List<QueueEntry>, override val entries: List<QueueEntry>,
override val cursor: Long, override val cursor: Long,
override val current: Long,
val from: Long, val from: Long,
val to: Long, val to: Long,
) : QueueUndo ) : QueueUndo
@@ -68,7 +71,9 @@ private data class MovedUndo(
private data class ReloadUndo( private data class ReloadUndo(
override val entries: List<QueueEntry>, override val entries: List<QueueEntry>,
override val cursor: Long, override val cursor: Long,
) : QueueUndo ) : QueueUndo {
override val current: Long? = null
}
class PlaybackController( class PlaybackController(
private val sourceRepository: SourceRepository, private val sourceRepository: SourceRepository,
@@ -195,7 +200,7 @@ class PlaybackController(
if (removed == null) { if (removed == null) {
ReloadUndo(entries, cursor) ReloadUndo(entries, cursor)
} else { } else {
RemovedUndo(entries, cursor, position, removed) RemovedUndo(entries, cursor, target, position, removed)
} }
when { when {
position == current -> { position == current -> {
@@ -242,7 +247,7 @@ class PlaybackController(
val inWindow = from in windowStart..windowEnd && to in windowStart..windowEnd val inWindow = from in windowStart..windowEnd && to in windowStart..windowEnd
lastEdit = lastEdit =
if (inWindow) { if (inWindow) {
MovedUndo(entries, cursor, from, to) MovedUndo(entries, cursor, target, from, to)
} else { } else {
ReloadUndo(entries, cursor) ReloadUndo(entries, cursor)
} }
@@ -275,7 +280,11 @@ class PlaybackController(
val target = undo.cursor.coerceIn(0, restored.size - 1) val target = undo.cursor.coerceIn(0, restored.size - 1)
val restoredId = queueRepository.itemAt(restored, target)?.song?.id val restoredId = queueRepository.itemAt(restored, target)?.song?.id
windowJob?.cancel() windowJob?.cancel()
val canMirror = player.currentItem?.id != null && player.currentItem?.id == restoredId val canMirror =
undo.current != null &&
player.currentItem?.id != null &&
player.currentItem?.id == restoredId &&
currentPosition() == undo.current
val mirrored = val mirrored =
when { when {
!canMirror -> { !canMirror -> {
@@ -180,10 +180,11 @@ fun QueueScreen(
val from = dragging ?: break val from = dragging ?: break
val info = listState.layoutInfo.visibleItemsInfo.firstOrNull { items.peek(it.index)?.position == from } val info = listState.layoutInfo.visibleItemsInfo.firstOrNull { items.peek(it.index)?.position == from }
if (info != null) { if (info != null) {
val top = info.offset + dragOffset
val edge = val edge =
when { when {
info.offset < listState.layoutInfo.viewportStartOffset + threshold -> -step top < listState.layoutInfo.viewportStartOffset + threshold -> -step
info.offset + info.size > listState.layoutInfo.viewportEndOffset - threshold -> step top + info.size > listState.layoutInfo.viewportEndOffset - threshold -> step
else -> 0f else -> 0f
} }
if (edge != 0f) { if (edge != 0f) {
@@ -1,13 +1,16 @@
package com.subtracks.data.repo package com.subtracks.data.repo
import android.content.Context import android.content.Context
import androidx.paging.PagingConfig
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.room3.Room import androidx.room3.Room
import androidx.sqlite.driver.bundled.BundledSQLiteDriver import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.subtracks.data.db.SubtracksDatabase import com.subtracks.data.db.SubtracksDatabase
import com.subtracks.data.model.Song import com.subtracks.data.model.Song
import com.subtracks.data.model.SongListItem
import com.subtracks.data.model.Source import com.subtracks.data.model.Source
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.After import org.junit.After
@@ -74,6 +77,21 @@ class QueuePagingSourceTest {
assertEquals(0L, first.prevKey) assertEquals(0L, first.prevKey)
} }
@Test
fun theRefreshKeyUsesTheLoadedPositions() =
runTest {
val source = repository.pagingSource(initialPosition = 100)
val state =
PagingState(
pages = listOf(PagingSource.LoadResult.Page(listOf(itemAt(60)), prevKey = 0L, nextKey = 2L)),
anchorPosition = 5,
config = PagingConfig(pageSize = QUEUE_PAGE_SIZE, enablePlaceholders = false),
leadingPlaceholderCount = 0,
)
assertEquals(1L, source.getRefreshKey(state))
}
@Test @Test
fun anEmptyQueueLoadsNothing() = fun anEmptyQueueLoadsNothing() =
runTest { runTest {
@@ -84,6 +102,29 @@ class QueuePagingSourceTest {
assertEquals(null, result.prevKey) assertEquals(null, result.prevKey)
} }
private fun itemAt(position: Long) =
QueueWindowItem(
position,
SongListItem(
song =
Song(
sourceId = 1,
id = "s$position",
albumId = "al1",
artistId = "ar1",
title = "Song $position",
album = "Album",
artist = "Artist",
duration = 100,
track = position,
disc = 1,
starred = null,
genre = null,
),
coverArt = null,
),
)
private suspend fun load( private suspend fun load(
source: PagingSource<Long, QueueWindowItem>, source: PagingSource<Long, QueueWindowItem>,
key: Long?, key: Long?,