show the now playing gradient behind the library lists

This commit is contained in:
austinried
2026-10-04 00:29:55 +00:00
parent da7af592d6
commit 5535a41f5d
4 changed files with 52 additions and 36 deletions
@@ -56,8 +56,7 @@ import com.subtracks.ui.playback.NowPlayingRoute
import com.subtracks.ui.playback.QueueRoute import com.subtracks.ui.playback.QueueRoute
import com.subtracks.ui.settings.AddSourceRoute import com.subtracks.ui.settings.AddSourceRoute
import com.subtracks.ui.settings.SettingsRoute import com.subtracks.ui.settings.SettingsRoute
import com.subtracks.ui.theme.LocalPlayerSurfaceColor import com.subtracks.ui.theme.LocalPlayerArtwork
import com.subtracks.ui.theme.playerSurfaceColor
import com.subtracks.ui.theme.rememberArtworkColors import com.subtracks.ui.theme.rememberArtworkColors
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@@ -148,11 +147,11 @@ private fun MainNavigation() {
} }
val miniArt = playbackController.coverArt(playback.item, thumbnail = true) val miniArt = playbackController.coverArt(playback.item, thumbnail = true)
val miniArtwork = rememberArtworkColors(miniArt) val miniArtwork = rememberArtworkColors(miniArt)
val playerSurface = playerSurfaceColor(miniArtwork) val libraryArtwork = miniArtwork.takeIf { miniArt != null }
val density = LocalDensity.current val density = LocalDensity.current
val navBarInset = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() } val navBarInset = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
CompositionLocalProvider(LocalPlayerSurfaceColor provides playerSurface) { CompositionLocalProvider(LocalPlayerArtwork provides libraryArtwork) {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
Column(Modifier.fillMaxSize()) { Column(Modifier.fillMaxSize()) {
NavHost( NavHost(
@@ -51,7 +51,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -69,7 +69,9 @@ import com.subtracks.data.model.Artist
import com.subtracks.data.model.CoverArtRef import com.subtracks.data.model.CoverArtRef
import com.subtracks.data.model.Playlist import com.subtracks.data.model.Playlist
import com.subtracks.data.model.SongListItem import com.subtracks.data.model.SongListItem
import com.subtracks.ui.theme.LocalPlayerSurfaceColor import com.subtracks.ui.theme.HeroGradient
import com.subtracks.ui.theme.LocalPlayerArtwork
import com.subtracks.ui.theme.playerSurfaceColor
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -169,10 +171,8 @@ fun LibraryScreen(
val titleFraction = scrollBehavior.state.collapsedFraction val titleFraction = scrollBehavior.state.collapsedFraction
val statusBarTop = with(density) { WindowInsets.statusBars.getTop(this).toDp() } val statusBarTop = with(density) { WindowInsets.statusBars.getTop(this).toDp() }
val headerColor = val artwork = LocalPlayerArtwork.current
LocalPlayerSurfaceColor.current.let { val headerColor = artwork?.let(::playerSurfaceColor) ?: MaterialTheme.colorScheme.surfaceContainerHigh
if (it.isSpecified) it else MaterialTheme.colorScheme.surfaceContainerHigh
}
Column( Column(
modifier = modifier =
@@ -213,12 +213,21 @@ fun LibraryScreen(
syncing = syncing, syncing = syncing,
onSync = onSync, onSync = onSync,
onOpenSettings = onOpenSettings, onOpenSettings = onOpenSettings,
selectedColor = headerColor,
) )
} }
Box(Modifier.fillMaxWidth().weight(1f)) {
if (artwork != null) {
HeroGradient(
colors = artwork,
scrollPx = { 0f },
modifier = Modifier.fillMaxSize(),
)
}
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
modifier = Modifier.fillMaxWidth().weight(1f), modifier = Modifier.fillMaxSize(),
) { page -> ) { page ->
when (LibraryTab.entries[page]) { when (LibraryTab.entries[page]) {
LibraryTab.Albums -> AlbumsContent(albums, coverArt, bottomInset, onAlbumClick) LibraryTab.Albums -> AlbumsContent(albums, coverArt, bottomInset, onAlbumClick)
@@ -229,6 +238,7 @@ fun LibraryScreen(
} }
} }
} }
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -238,6 +248,7 @@ private fun LibraryTabs(
syncing: Boolean, syncing: Boolean,
onSync: () -> Unit, onSync: () -> Unit,
onOpenSettings: () -> Unit, onOpenSettings: () -> Unit,
selectedColor: Color,
) { ) {
val iconFadeThreshold = 0.65f val iconFadeThreshold = 0.65f
val indicatorStretch = 18.dp val indicatorStretch = 18.dp
@@ -281,6 +292,7 @@ private fun LibraryTabs(
((iconFadeThreshold - abs(position - index)) / iconFadeThreshold) ((iconFadeThreshold - abs(position - index)) / iconFadeThreshold)
.coerceIn(0f, 1f), .coerceIn(0f, 1f),
onClick = { onTabSelected(tab) }, onClick = { onTabSelected(tab) },
selectedColor = selectedColor,
modifier = modifier =
Modifier.onGloballyPositioned { coordinates -> Modifier.onGloballyPositioned { coordinates ->
bounds[index] = coordinates.boundsInParent() bounds[index] = coordinates.boundsInParent()
@@ -318,9 +330,10 @@ private fun TabButton(
tab: LibraryTab, tab: LibraryTab,
progress: Float, progress: Float,
onClick: () -> Unit, onClick: () -> Unit,
selectedColor: Color,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val content = lerp(MaterialTheme.colorScheme.onBackground, MaterialTheme.colorScheme.background, progress) val content = lerp(MaterialTheme.colorScheme.onBackground, selectedColor, progress)
Box( Box(
modifier = modifier =
modifier modifier
@@ -206,7 +206,7 @@ fun ArtworkTheme(
} }
} }
val LocalPlayerSurfaceColor = compositionLocalOf { Color.Unspecified } val LocalPlayerArtwork = compositionLocalOf<ArtworkColors?> { null }
fun playerSurfaceColor(artwork: ArtworkColors): Color = lerp(artwork.scheme.surfaceContainerHigh, artwork.gradientHigh, 0.5f) fun playerSurfaceColor(artwork: ArtworkColors): Color = lerp(artwork.scheme.surfaceContainerHigh, artwork.gradientHigh, 0.5f)
@@ -9,6 +9,7 @@ import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
@@ -49,6 +50,7 @@ import com.subtracks.ui.playback.QueueScreen
import com.subtracks.ui.settings.AddSourceScreen import com.subtracks.ui.settings.AddSourceScreen
import com.subtracks.ui.settings.AddSourceState import com.subtracks.ui.settings.AddSourceState
import com.subtracks.ui.settings.SettingsScreen import com.subtracks.ui.settings.SettingsScreen
import com.subtracks.ui.theme.LocalPlayerArtwork
import com.subtracks.ui.theme.SubtracksTheme import com.subtracks.ui.theme.SubtracksTheme
import com.subtracks.ui.theme.artworkColorsFromSeed import com.subtracks.ui.theme.artworkColorsFromSeed
import com.subtracks.ui.theme.artworkColorsFromSeeds import com.subtracks.ui.theme.artworkColorsFromSeeds
@@ -327,6 +329,7 @@ class AppScreenshotTest {
private fun setLibraryContent(tab: LibraryTab) { private fun setLibraryContent(tab: LibraryTab) {
composeRule.setContent { composeRule.setContent {
SubtracksTheme { SubtracksTheme {
CompositionLocalProvider(LocalPlayerArtwork provides artworkColorsFromSeed(Color.rgb(120, 80, 200))) {
LibraryScreen( LibraryScreen(
selectedTab = tab, selectedTab = tab,
onTabSelected = {}, onTabSelected = {},
@@ -345,6 +348,7 @@ class AppScreenshotTest {
} }
} }
} }
}
private fun awaitText(text: String) { private fun awaitText(text: String) {
composeRule.waitUntil(timeoutMillis = 5_000) { composeRule.waitUntil(timeoutMillis = 5_000) {