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 initialPosition: Long,
) : PagingSource<Long, QueueWindowItem>() {
override fun getRefreshKey(state: PagingState<Long, QueueWindowItem>): Long? =
state.anchorPosition?.let { (it / QUEUE_PAGE_SIZE).toLong() }
override fun getRefreshKey(state: PagingState<Long, QueueWindowItem>): Long? {
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> {
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 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)
return LoadResult.Page(
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,
)
}
@@ -49,11 +49,13 @@ data class PlaybackState(
private sealed interface QueueUndo {
val entries: List<QueueEntry>
val cursor: Long
val current: Long?
}
private data class RemovedUndo(
override val entries: List<QueueEntry>,
override val cursor: Long,
override val current: Long,
val position: Long,
val item: QueueItem?,
) : QueueUndo
@@ -61,6 +63,7 @@ private data class RemovedUndo(
private data class MovedUndo(
override val entries: List<QueueEntry>,
override val cursor: Long,
override val current: Long,
val from: Long,
val to: Long,
) : QueueUndo
@@ -68,7 +71,9 @@ private data class MovedUndo(
private data class ReloadUndo(
override val entries: List<QueueEntry>,
override val cursor: Long,
) : QueueUndo
) : QueueUndo {
override val current: Long? = null
}
class PlaybackController(
private val sourceRepository: SourceRepository,
@@ -195,7 +200,7 @@ class PlaybackController(
if (removed == null) {
ReloadUndo(entries, cursor)
} else {
RemovedUndo(entries, cursor, position, removed)
RemovedUndo(entries, cursor, target, position, removed)
}
when {
position == current -> {
@@ -242,7 +247,7 @@ class PlaybackController(
val inWindow = from in windowStart..windowEnd && to in windowStart..windowEnd
lastEdit =
if (inWindow) {
MovedUndo(entries, cursor, from, to)
MovedUndo(entries, cursor, target, from, to)
} else {
ReloadUndo(entries, cursor)
}
@@ -275,7 +280,11 @@ class PlaybackController(
val target = undo.cursor.coerceIn(0, restored.size - 1)
val restoredId = queueRepository.itemAt(restored, target)?.song?.id
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 =
when {
!canMirror -> {
@@ -180,10 +180,11 @@ fun QueueScreen(
val from = dragging ?: break
val info = listState.layoutInfo.visibleItemsInfo.firstOrNull { items.peek(it.index)?.position == from }
if (info != null) {
val top = info.offset + dragOffset
val edge =
when {
info.offset < listState.layoutInfo.viewportStartOffset + threshold -> -step
info.offset + info.size > listState.layoutInfo.viewportEndOffset - threshold -> step
top < listState.layoutInfo.viewportStartOffset + threshold -> -step
top + info.size > listState.layoutInfo.viewportEndOffset - threshold -> step
else -> 0f
}
if (edge != 0f) {
@@ -1,13 +1,16 @@
package com.subtracks.data.repo
import android.content.Context
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.room3.Room
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.subtracks.data.db.SubtracksDatabase
import com.subtracks.data.model.Song
import com.subtracks.data.model.SongListItem
import com.subtracks.data.model.Source
import kotlinx.coroutines.test.runTest
import org.junit.After
@@ -74,6 +77,21 @@ class QueuePagingSourceTest {
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
fun anEmptyQueueLoadsNothing() =
runTest {
@@ -84,6 +102,29 @@ class QueuePagingSourceTest {
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(
source: PagingSource<Long, QueueWindowItem>,
key: Long?,