mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
align the swipe peek with the skip and harden its edges
Resolve previousItem/upcomingItem through the same offline download scan the transport uses, so the peeking art is the track the swipe will actually play. Clear a frozen strip when a fallback swipe fires, prefer the dragged direction over an opposite fling, and simplify the hand-back and ratio prefetch.
This commit is contained in:
@@ -912,26 +912,39 @@ class PlaybackController(
|
||||
suspend fun upcomingItem(): QueueItem? {
|
||||
var current: QueueSnapshot? = null
|
||||
var position: Long? = null
|
||||
var isOffline = false
|
||||
startLock.withLock {
|
||||
current = snapshot
|
||||
position = currentPosition()
|
||||
isOffline = offline
|
||||
}
|
||||
val snapshot = current ?: return null
|
||||
val at = position ?: return null
|
||||
return queueRepository.itemAt(snapshot, at + 1)?.toQueueItem()
|
||||
if (!isOffline) return queueRepository.itemAt(snapshot, at + 1)?.toQueueItem()
|
||||
downloads.awaitLoaded()
|
||||
val target =
|
||||
downloadedAtOrAfter(snapshot, at + 1)
|
||||
?: if (repeatMode != RepeatMode.Off) downloadedAtOrAfter(snapshot, 0L) else null
|
||||
return target?.let { queueRepository.itemAt(snapshot, it)?.toQueueItem() }
|
||||
}
|
||||
|
||||
suspend fun previousItem(): QueueItem? {
|
||||
var current: QueueSnapshot? = null
|
||||
var position: Long? = null
|
||||
var isOffline = false
|
||||
startLock.withLock {
|
||||
current = snapshot
|
||||
position = currentPosition()
|
||||
isOffline = offline
|
||||
}
|
||||
val snapshot = current ?: return null
|
||||
val at = position ?: return null
|
||||
if (at <= 0) return null
|
||||
return queueRepository.itemAt(snapshot, at - 1)?.toQueueItem()
|
||||
if (!isOffline) return if (at <= 0) null else queueRepository.itemAt(snapshot, at - 1)?.toQueueItem()
|
||||
downloads.awaitLoaded()
|
||||
val target =
|
||||
downloadedAtOrBefore(snapshot, at - 1)
|
||||
?: if (repeatMode == RepeatMode.All) downloadedAtOrBefore(snapshot, snapshot.size - 1) else null
|
||||
return target?.let { queueRepository.itemAt(snapshot, it)?.toQueueItem() }
|
||||
}
|
||||
|
||||
private fun play(
|
||||
|
||||
@@ -256,12 +256,13 @@ internal suspend fun prefetchArtworkRatio(
|
||||
) {
|
||||
if (ArtworkRatioCache.get(ref.cacheKey) != null) return
|
||||
val request =
|
||||
ImageRequest
|
||||
.Builder(context)
|
||||
.data(ref.url)
|
||||
.diskCacheKey(ref.cacheKey)
|
||||
.size(COVER_RATIO_PREFETCH_PX)
|
||||
.build()
|
||||
imageRequest(
|
||||
context,
|
||||
ref,
|
||||
crossfade = false,
|
||||
width = COVER_RATIO_PREFETCH_PX,
|
||||
height = COVER_RATIO_PREFETCH_PX,
|
||||
)
|
||||
val image =
|
||||
try {
|
||||
(SingletonImageLoader.get(context).execute(request) as? SuccessResult)?.image
|
||||
|
||||
@@ -131,9 +131,15 @@ internal fun swipeAction(
|
||||
): SwipeAction {
|
||||
if (width <= 0f) return SwipeAction.None
|
||||
val threshold = width * SWIPE_THRESHOLD_FRACTION
|
||||
val wantsNext = offsetX <= -threshold || velocity <= -SWIPE_FLING_VELOCITY
|
||||
val wantsPrevious = offsetX >= threshold || velocity >= SWIPE_FLING_VELOCITY
|
||||
val pastNext = offsetX <= -threshold
|
||||
val pastPrevious = offsetX >= threshold
|
||||
val wantsNext = pastNext || velocity <= -SWIPE_FLING_VELOCITY
|
||||
val wantsPrevious = pastPrevious || velocity >= SWIPE_FLING_VELOCITY
|
||||
return when {
|
||||
pastNext && canGoNext -> SwipeAction.CommitNext
|
||||
pastPrevious && canGoPrevious -> SwipeAction.CommitPrevious
|
||||
pastNext -> SwipeAction.Next
|
||||
pastPrevious -> SwipeAction.Previous
|
||||
wantsNext && canGoNext -> SwipeAction.CommitNext
|
||||
wantsPrevious && canGoPrevious -> SwipeAction.CommitPrevious
|
||||
wantsNext -> SwipeAction.Next
|
||||
@@ -346,7 +352,7 @@ fun NowPlayingScreen(
|
||||
frozenStrip = null
|
||||
} else {
|
||||
delay(HAND_BACK_TIMEOUT_MS)
|
||||
if (frozenStrip === pending) frozenStrip = null
|
||||
frozenStrip = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,11 +439,13 @@ fun NowPlayingScreen(
|
||||
}
|
||||
|
||||
SwipeAction.Next -> {
|
||||
frozenStrip = null
|
||||
onNext()
|
||||
animateSwipe(0f)
|
||||
}
|
||||
|
||||
SwipeAction.Previous -> {
|
||||
frozenStrip = null
|
||||
onSkipPrevious()
|
||||
animateSwipe(0f)
|
||||
}
|
||||
@@ -545,17 +553,17 @@ fun NowPlayingScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
var dragging by remember { mutableStateOf(false) }
|
||||
var sliderDragging by remember { mutableStateOf(false) }
|
||||
var dragPosition by remember { mutableFloatStateOf(0f) }
|
||||
Slider(
|
||||
value = if (dragging) dragPosition else positionMs.toFloat(),
|
||||
value = if (sliderDragging) dragPosition else positionMs.toFloat(),
|
||||
onValueChange = {
|
||||
dragging = true
|
||||
sliderDragging = true
|
||||
dragPosition = it
|
||||
},
|
||||
onValueChangeFinished = {
|
||||
onSeek(dragPosition.toLong())
|
||||
dragging = false
|
||||
sliderDragging = false
|
||||
},
|
||||
valueRange = 0f..state.durationMs.toFloat().coerceAtLeast(1f),
|
||||
enabled = state.durationMs > 0,
|
||||
|
||||
@@ -230,6 +230,36 @@ class PlaybackControllerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun offlinePreviousItemMatchesTheTrackPreviousSkipsTo() {
|
||||
seedAlbum(5, sourceId = 1)
|
||||
markDownloaded(1, "s1")
|
||||
markDownloaded(1, "s4")
|
||||
setOffline()
|
||||
controller.playAlbum(1, "al1", 3)
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id == "s4"
|
||||
}
|
||||
|
||||
assertEquals("s1", runBlocking { controller.previousItem()?.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun offlineUpcomingItemMatchesTheTrackNextPlays() {
|
||||
seedAlbum(4, sourceId = 1)
|
||||
markDownloaded(1, "s1")
|
||||
markDownloaded(1, "s3")
|
||||
setOffline()
|
||||
controller.playAlbum(1, "al1", 0)
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id == "s1"
|
||||
}
|
||||
|
||||
assertEquals("s3", runBlocking { controller.upcomingItem()?.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun advancingShiftsTheWindowAndKeepsItBounded() {
|
||||
seedAlbum(100, sourceId = 1)
|
||||
|
||||
@@ -231,6 +231,30 @@ class NowPlayingTapsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theSwipeActionPrefersTheDraggedDirectionOverAnOppositeFling() {
|
||||
assertEquals(
|
||||
SwipeAction.CommitPrevious,
|
||||
swipeAction(offsetX = 300f, velocity = -2_000f, width = 1_000f, canGoNext = true, canGoPrevious = true),
|
||||
)
|
||||
assertEquals(
|
||||
SwipeAction.CommitNext,
|
||||
swipeAction(offsetX = -300f, velocity = 2_000f, width = 1_000f, canGoNext = true, canGoPrevious = true),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun swipingLeftWithANeighbourRotatesTheStrip() {
|
||||
render(nextArt = adjacentArt("s-next"))
|
||||
|
||||
composeRule.onNodeWithTag(NOW_PLAYING_NEXT_TAG, useUnmergedTree = true).assertExists()
|
||||
composeRule.onNodeWithTag(NOW_PLAYING_COVER_TAG).performTouchInput { swipeLeft() }
|
||||
composeRule.waitForIdle()
|
||||
|
||||
composeRule.onNodeWithTag(NOW_PLAYING_NEXT_TAG, useUnmergedTree = true).assertDoesNotExist()
|
||||
composeRule.onNodeWithTag(NOW_PLAYING_PREVIOUS_TAG, useUnmergedTree = true).assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rotatingTheStripMovesTheNeighbourIntoTheCentre() {
|
||||
val a = adjacentArt("a")
|
||||
|
||||
Reference in New Issue
Block a user