diff --git a/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt b/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt index 47d39f98..b267e59c 100644 --- a/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt +++ b/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt @@ -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( diff --git a/app/src/main/kotlin/com/subtracks/ui/components/Components.kt b/app/src/main/kotlin/com/subtracks/ui/components/Components.kt index 77b5b3ef..ab00bff0 100644 --- a/app/src/main/kotlin/com/subtracks/ui/components/Components.kt +++ b/app/src/main/kotlin/com/subtracks/ui/components/Components.kt @@ -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 diff --git a/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt b/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt index 4164aa39..124d85e2 100644 --- a/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt @@ -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, diff --git a/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt b/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt index 29400916..7146c32f 100644 --- a/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt +++ b/app/src/test/kotlin/com/subtracks/playback/PlaybackControllerTest.kt @@ -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) diff --git a/app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingTapsTest.kt b/app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingTapsTest.kt index 26fd869b..6c92da7c 100644 --- a/app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingTapsTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingTapsTest.kt @@ -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")