mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +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? {
|
suspend fun upcomingItem(): QueueItem? {
|
||||||
var current: QueueSnapshot? = null
|
var current: QueueSnapshot? = null
|
||||||
var position: Long? = null
|
var position: Long? = null
|
||||||
|
var isOffline = false
|
||||||
startLock.withLock {
|
startLock.withLock {
|
||||||
current = snapshot
|
current = snapshot
|
||||||
position = currentPosition()
|
position = currentPosition()
|
||||||
|
isOffline = offline
|
||||||
}
|
}
|
||||||
val snapshot = current ?: return null
|
val snapshot = current ?: return null
|
||||||
val at = position ?: 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? {
|
suspend fun previousItem(): QueueItem? {
|
||||||
var current: QueueSnapshot? = null
|
var current: QueueSnapshot? = null
|
||||||
var position: Long? = null
|
var position: Long? = null
|
||||||
|
var isOffline = false
|
||||||
startLock.withLock {
|
startLock.withLock {
|
||||||
current = snapshot
|
current = snapshot
|
||||||
position = currentPosition()
|
position = currentPosition()
|
||||||
|
isOffline = offline
|
||||||
}
|
}
|
||||||
val snapshot = current ?: return null
|
val snapshot = current ?: return null
|
||||||
val at = position ?: return null
|
val at = position ?: return null
|
||||||
if (at <= 0) return null
|
if (!isOffline) return if (at <= 0) null else queueRepository.itemAt(snapshot, at - 1)?.toQueueItem()
|
||||||
return 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(
|
private fun play(
|
||||||
|
|||||||
@@ -256,12 +256,13 @@ internal suspend fun prefetchArtworkRatio(
|
|||||||
) {
|
) {
|
||||||
if (ArtworkRatioCache.get(ref.cacheKey) != null) return
|
if (ArtworkRatioCache.get(ref.cacheKey) != null) return
|
||||||
val request =
|
val request =
|
||||||
ImageRequest
|
imageRequest(
|
||||||
.Builder(context)
|
context,
|
||||||
.data(ref.url)
|
ref,
|
||||||
.diskCacheKey(ref.cacheKey)
|
crossfade = false,
|
||||||
.size(COVER_RATIO_PREFETCH_PX)
|
width = COVER_RATIO_PREFETCH_PX,
|
||||||
.build()
|
height = COVER_RATIO_PREFETCH_PX,
|
||||||
|
)
|
||||||
val image =
|
val image =
|
||||||
try {
|
try {
|
||||||
(SingletonImageLoader.get(context).execute(request) as? SuccessResult)?.image
|
(SingletonImageLoader.get(context).execute(request) as? SuccessResult)?.image
|
||||||
|
|||||||
@@ -131,9 +131,15 @@ internal fun swipeAction(
|
|||||||
): SwipeAction {
|
): SwipeAction {
|
||||||
if (width <= 0f) return SwipeAction.None
|
if (width <= 0f) return SwipeAction.None
|
||||||
val threshold = width * SWIPE_THRESHOLD_FRACTION
|
val threshold = width * SWIPE_THRESHOLD_FRACTION
|
||||||
val wantsNext = offsetX <= -threshold || velocity <= -SWIPE_FLING_VELOCITY
|
val pastNext = offsetX <= -threshold
|
||||||
val wantsPrevious = offsetX >= threshold || velocity >= SWIPE_FLING_VELOCITY
|
val pastPrevious = offsetX >= threshold
|
||||||
|
val wantsNext = pastNext || velocity <= -SWIPE_FLING_VELOCITY
|
||||||
|
val wantsPrevious = pastPrevious || velocity >= SWIPE_FLING_VELOCITY
|
||||||
return when {
|
return when {
|
||||||
|
pastNext && canGoNext -> SwipeAction.CommitNext
|
||||||
|
pastPrevious && canGoPrevious -> SwipeAction.CommitPrevious
|
||||||
|
pastNext -> SwipeAction.Next
|
||||||
|
pastPrevious -> SwipeAction.Previous
|
||||||
wantsNext && canGoNext -> SwipeAction.CommitNext
|
wantsNext && canGoNext -> SwipeAction.CommitNext
|
||||||
wantsPrevious && canGoPrevious -> SwipeAction.CommitPrevious
|
wantsPrevious && canGoPrevious -> SwipeAction.CommitPrevious
|
||||||
wantsNext -> SwipeAction.Next
|
wantsNext -> SwipeAction.Next
|
||||||
@@ -346,7 +352,7 @@ fun NowPlayingScreen(
|
|||||||
frozenStrip = null
|
frozenStrip = null
|
||||||
} else {
|
} else {
|
||||||
delay(HAND_BACK_TIMEOUT_MS)
|
delay(HAND_BACK_TIMEOUT_MS)
|
||||||
if (frozenStrip === pending) frozenStrip = null
|
frozenStrip = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,11 +439,13 @@ fun NowPlayingScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SwipeAction.Next -> {
|
SwipeAction.Next -> {
|
||||||
|
frozenStrip = null
|
||||||
onNext()
|
onNext()
|
||||||
animateSwipe(0f)
|
animateSwipe(0f)
|
||||||
}
|
}
|
||||||
|
|
||||||
SwipeAction.Previous -> {
|
SwipeAction.Previous -> {
|
||||||
|
frozenStrip = null
|
||||||
onSkipPrevious()
|
onSkipPrevious()
|
||||||
animateSwipe(0f)
|
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) }
|
var dragPosition by remember { mutableFloatStateOf(0f) }
|
||||||
Slider(
|
Slider(
|
||||||
value = if (dragging) dragPosition else positionMs.toFloat(),
|
value = if (sliderDragging) dragPosition else positionMs.toFloat(),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
dragging = true
|
sliderDragging = true
|
||||||
dragPosition = it
|
dragPosition = it
|
||||||
},
|
},
|
||||||
onValueChangeFinished = {
|
onValueChangeFinished = {
|
||||||
onSeek(dragPosition.toLong())
|
onSeek(dragPosition.toLong())
|
||||||
dragging = false
|
sliderDragging = false
|
||||||
},
|
},
|
||||||
valueRange = 0f..state.durationMs.toFloat().coerceAtLeast(1f),
|
valueRange = 0f..state.durationMs.toFloat().coerceAtLeast(1f),
|
||||||
enabled = state.durationMs > 0,
|
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
|
@Test
|
||||||
fun advancingShiftsTheWindowAndKeepsItBounded() {
|
fun advancingShiftsTheWindowAndKeepsItBounded() {
|
||||||
seedAlbum(100, sourceId = 1)
|
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
|
@Test
|
||||||
fun rotatingTheStripMovesTheNeighbourIntoTheCentre() {
|
fun rotatingTheStripMovesTheNeighbourIntoTheCentre() {
|
||||||
val a = adjacentArt("a")
|
val a = adjacentArt("a")
|
||||||
|
|||||||
Reference in New Issue
Block a user