mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
peek the adjacent artwork while swiping the now playing screen
Resolve the previous/next queue items and render a three-slot cover strip that follows the drag, so the neighbouring cover slides in from the side on release. The strip is rotated optimistically at commit and hands back to live state once the controller reports the new track (or after a grace period), so a prop update mid-animation cannot desync it. Ratio prefetch and layered cover loading keep the incoming art from snapping as it loads.
This commit is contained in:
@@ -47,6 +47,7 @@ import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -167,6 +168,64 @@ class PlaybackControllerTest {
|
||||
assertEquals("cover-al1", runBlocking { controller.upcomingItem()?.coverArtId })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun previousItemIsThePreviousQueueItemWithItsCoverArt() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
runBlocking {
|
||||
db.libraryDao().upsertAlbums(
|
||||
listOf(
|
||||
Album(
|
||||
sourceId = 1,
|
||||
id = "al1",
|
||||
artistId = "ar1",
|
||||
name = "Album",
|
||||
albumArtist = "Artist",
|
||||
created = 0,
|
||||
coverArt = "cover-al1",
|
||||
genre = null,
|
||||
year = null,
|
||||
starred = null,
|
||||
songCount = 3,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
controller.playAlbum(1, "al1", 1)
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id == "s2"
|
||||
}
|
||||
|
||||
assertEquals("s1", runBlocking { controller.previousItem()?.id })
|
||||
assertEquals("cover-al1", runBlocking { controller.previousItem()?.coverArtId })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun previousItemIsNullAtTheStartOfTheQueue() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
controller.playAlbum(1, "al1", 0)
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id == "s1"
|
||||
}
|
||||
|
||||
assertNull(runBlocking { controller.previousItem() })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun previousItemIsNullOncePreviousWouldRestartTheTrack() {
|
||||
seedAlbum(3, sourceId = 1)
|
||||
controller.playAlbum(1, "al1", 1)
|
||||
await {
|
||||
controller.state.value.item
|
||||
?.id == "s2"
|
||||
}
|
||||
|
||||
handle.positionMs = 3_100
|
||||
|
||||
assertNull(runBlocking { controller.previousItem() })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun advancingShiftsTheWindowAndKeepsItBounded() {
|
||||
seedAlbum(100, sourceId = 1)
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.subtracks.playback.PlaybackState
|
||||
import com.subtracks.playback.QueueItem
|
||||
import com.subtracks.ui.theme.SubtracksTheme
|
||||
import com.subtracks.ui.theme.artworkColorsFromSeed
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -55,6 +56,8 @@ class NowPlayingTapsTest {
|
||||
onArtistClick: (() -> Unit)? = null,
|
||||
onNext: () -> Unit = {},
|
||||
onPrevious: () -> Unit = {},
|
||||
previousArt: NowPlayingArt? = null,
|
||||
nextArt: NowPlayingArt? = null,
|
||||
) {
|
||||
composeRule.setContent {
|
||||
SubtracksTheme {
|
||||
@@ -72,11 +75,21 @@ class NowPlayingTapsTest {
|
||||
onAlbumClick = onAlbumClick,
|
||||
onArtistClick = onArtistClick,
|
||||
onSeek = {},
|
||||
previousArt = previousArt,
|
||||
nextArt = nextArt,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun adjacentArt(id: String) =
|
||||
NowPlayingArt(
|
||||
id = id,
|
||||
ref = CoverArtRef(id, "test:$id"),
|
||||
thumbnailRef = null,
|
||||
name = id,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun tappingTheCoverArtOpensTheAlbum() {
|
||||
var opened = false
|
||||
@@ -158,6 +171,72 @@ class NowPlayingTapsTest {
|
||||
assertTrue("a right swipe should not skip to the next track", !next)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theSwipeActionCommitsWhenANeighbourExists() {
|
||||
assertEquals(
|
||||
SwipeAction.CommitNext,
|
||||
swipeAction(offsetX = -300f, velocity = 0f, width = 1000f, canGoNext = true, canGoPrevious = false),
|
||||
)
|
||||
assertEquals(
|
||||
SwipeAction.CommitPrevious,
|
||||
swipeAction(offsetX = 300f, velocity = 0f, width = 1000f, canGoNext = false, canGoPrevious = true),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theSwipeActionFallsBackWhenTheNeighbourIsMissing() {
|
||||
assertEquals(
|
||||
SwipeAction.Next,
|
||||
swipeAction(offsetX = -300f, velocity = 0f, width = 1000f, canGoNext = false, canGoPrevious = false),
|
||||
)
|
||||
assertEquals(
|
||||
SwipeAction.Previous,
|
||||
swipeAction(offsetX = 300f, velocity = 0f, width = 1000f, canGoNext = false, canGoPrevious = false),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theSwipeActionIgnoresShortAndUnmeasuredSwipes() {
|
||||
assertEquals(
|
||||
SwipeAction.None,
|
||||
swipeAction(offsetX = -50f, velocity = 0f, width = 1000f, canGoNext = true, canGoPrevious = true),
|
||||
)
|
||||
assertEquals(
|
||||
SwipeAction.None,
|
||||
swipeAction(offsetX = -300f, velocity = 0f, width = 0f, canGoNext = true, canGoPrevious = true),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rotatingTheStripMovesTheNeighbourIntoTheCentre() {
|
||||
val a = adjacentArt("a")
|
||||
val b = adjacentArt("b")
|
||||
val c = adjacentArt("c")
|
||||
val base = listOf(a, b, c)
|
||||
|
||||
assertEquals(listOf(b, c, null), rotateStrip(base, 1))
|
||||
assertEquals(listOf(null, a, b), rotateStrip(base, -1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theAdjacentArtRendersBesideTheCurrentArt() {
|
||||
render(
|
||||
previousArt = adjacentArt("s-previous"),
|
||||
nextArt = adjacentArt("s-next"),
|
||||
)
|
||||
|
||||
composeRule.onNodeWithTag(NOW_PLAYING_PREVIOUS_TAG, useUnmergedTree = true).assertExists()
|
||||
composeRule.onNodeWithTag(NOW_PLAYING_NEXT_TAG, useUnmergedTree = true).assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theAdjacentArtIsAbsentWithoutNeighbours() {
|
||||
render()
|
||||
|
||||
composeRule.onNodeWithTag(NOW_PLAYING_PREVIOUS_TAG, useUnmergedTree = true).assertDoesNotExist()
|
||||
composeRule.onNodeWithTag(NOW_PLAYING_NEXT_TAG, useUnmergedTree = true).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aShortSlowDragOnTheCoverDoesNotSkip() {
|
||||
var next = false
|
||||
|
||||
Reference in New Issue
Block a user