fix overscroll stretch, pad the album hero, push the gradient fade down

This commit is contained in:
austinried
2026-10-04 00:29:52 +00:00
parent 461eadb630
commit 651d548c41
12 changed files with 374 additions and 340 deletions
@@ -3,7 +3,10 @@ package com.subtracks.ui.components
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.LocalOverscrollFactory
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
@@ -17,10 +20,13 @@ import androidx.compose.ui.unit.Velocity
import kotlinx.coroutines.launch
import kotlin.math.abs
private const val MAX_STRETCH_FRACTION = 0.35f
private const val MAX_STRETCH_FRACTION = 0.30f
@Composable
fun Modifier.stretchOverscroll(): Modifier {
fun StretchOverscroll(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
val overscroll = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
@@ -58,13 +64,16 @@ fun Modifier.stretchOverscroll(): Modifier {
consumed: Velocity,
available: Velocity,
): Velocity {
overscroll.animateTo(0f, spring(stiffness = Spring.StiffnessMediumLow))
overscroll.animateTo(0f, spring(stiffness = Spring.StiffnessMedium))
return available
}
}
}
return this
CompositionLocalProvider(LocalOverscrollFactory provides null) {
Box(
modifier =
modifier
.nestedScroll(connection)
.graphicsLayer {
val raw = overscroll.value
@@ -79,5 +88,9 @@ fun Modifier.stretchOverscroll(): Modifier {
translationY = resisted
transformOrigin = TransformOrigin(0.5f, if (resisted > 0f) 0f else 1f)
}
},
) {
content()
}
}
}
@@ -62,6 +62,7 @@ import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -71,7 +72,7 @@ import com.subtracks.data.model.QueueKind
import com.subtracks.data.model.Song
import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.components.stretchOverscroll
import com.subtracks.ui.components.StretchOverscroll
import com.subtracks.ui.theme.ArtworkColors
import com.subtracks.ui.theme.ArtworkTheme
import com.subtracks.ui.theme.HeroGradient
@@ -82,7 +83,7 @@ import org.koin.core.parameter.parametersOf
import kotlin.math.roundToInt
private const val DOT = "\u00B7"
private const val GRADIENT_SCREENS = 1.5f
private const val GRADIENT_SCREENS = 2.0f
private const val FADE_DISTANCE_DP = 64
@Composable
@@ -129,6 +130,7 @@ fun AlbumDetailScreen(
val density = LocalDensity.current
val statusBarTop = WindowInsets.statusBars.getTop(density)
val navBarBottom = WindowInsets.navigationBars.getBottom(density)
val statusBarDp = with(density) { statusBarTop.toDp() }
val barHeightPx = statusBarTop + with(density) { TopAppBarDefaults.TopAppBarExpandedHeight.toPx() }
val fadeDistancePx = with(density) { FADE_DISTANCE_DP.dp.toPx() }
@@ -167,13 +169,14 @@ fun AlbumDetailScreen(
.alpha(gradientAlpha),
)
StretchOverscroll(modifier = Modifier.fillMaxSize()) {
LazyColumn(
state = listState,
contentPadding =
PaddingValues(
bottom = 16.dp + with(density) { navBarBottom.toDp() },
),
modifier = Modifier.fillMaxSize().stretchOverscroll(),
modifier = Modifier.fillMaxSize(),
) {
item {
AlbumHeader(
@@ -184,6 +187,7 @@ fun AlbumDetailScreen(
onShuffle = onShuffle,
onDownload = onDownload,
onMore = onMore,
topInset = statusBarDp,
titleModifier = Modifier.onGloballyPositioned { titleTopPx = it.positionInParent().y },
modifier = Modifier.onSizeChanged { headerHeightPx = it.height.toFloat() },
)
@@ -199,12 +203,13 @@ fun AlbumDetailScreen(
)
}
}
}
Box(
Modifier
.align(Alignment.TopStart)
.fillMaxWidth()
.height(with(density) { (barHeightPx + 24.dp.toPx()).toDp() })
.height(statusBarDp + 8.dp)
.background(
Brush.verticalGradient(
listOf(Color.Black.copy(alpha = 0.8f), Color.Transparent),
@@ -216,6 +221,7 @@ fun AlbumDetailScreen(
title = {
Text(
text = album?.name.orEmpty(),
style = MaterialTheme.typography.headlineLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.alpha(barFraction),
@@ -250,6 +256,7 @@ private fun AlbumHeader(
onShuffle: () -> Unit,
onDownload: () -> Unit,
onMore: () -> Unit,
topInset: Dp,
titleModifier: Modifier = Modifier,
modifier: Modifier = Modifier,
) {
@@ -257,7 +264,7 @@ private fun AlbumHeader(
modifier =
modifier
.fillMaxWidth()
.padding(start = 24.dp, end = 24.dp, top = 8.dp, bottom = 16.dp),
.padding(start = 24.dp, end = 24.dp, top = topInset + 8.dp, bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
CoverArt(
@@ -268,7 +275,7 @@ private fun AlbumHeader(
Spacer(Modifier.height(20.dp))
Text(
text = album?.name.orEmpty(),
style = MaterialTheme.typography.headlineSmall,
style = MaterialTheme.typography.headlineLarge,
textAlign = TextAlign.Center,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
@@ -23,7 +23,7 @@ import com.subtracks.data.model.CoverArtRef
import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.components.EmptyState
import com.subtracks.ui.components.LoadingState
import com.subtracks.ui.components.stretchOverscroll
import com.subtracks.ui.components.StretchOverscroll
const val ALBUM_COVER_TAG = "album-cover"
@@ -45,12 +45,13 @@ fun AlbumsContent(
}
else -> {
StretchOverscroll(modifier = modifier.fillMaxSize()) {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
contentPadding = PaddingValues(start = 8.dp, top = 8.dp, end = 8.dp, bottom = bottomInset + 8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier.fillMaxSize().stretchOverscroll(),
modifier = Modifier.fillMaxSize(),
) {
items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
val album = items[index]
@@ -71,4 +72,5 @@ fun AlbumsContent(
}
}
}
}
}
@@ -50,7 +50,7 @@ import com.subtracks.data.model.Album
import com.subtracks.data.model.Artist
import com.subtracks.data.model.CoverArtRef
import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.components.stretchOverscroll
import com.subtracks.ui.components.StretchOverscroll
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.parameter.parametersOf
@@ -123,13 +123,14 @@ fun ArtistDetailScreen(
)
Box(modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {
StretchOverscroll(modifier = Modifier.fillMaxSize()) {
LazyVerticalGrid(
state = listState,
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(bottom = 16.dp + navBarBottom),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxSize().stretchOverscroll(),
modifier = Modifier.fillMaxSize(),
) {
item(span = { GridItemSpan(maxLineSpan) }) {
Box(Modifier.fillMaxWidth().padding(bottom = 12.dp)) {
@@ -178,6 +179,7 @@ fun ArtistDetailScreen(
}
}
}
}
Box(
modifier =
@@ -24,7 +24,7 @@ import com.subtracks.data.model.CoverArtRef
import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.components.EmptyState
import com.subtracks.ui.components.LoadingState
import com.subtracks.ui.components.stretchOverscroll
import com.subtracks.ui.components.StretchOverscroll
@Composable
fun ArtistsContent(
@@ -44,8 +44,9 @@ fun ArtistsContent(
}
else -> {
StretchOverscroll(modifier = modifier.fillMaxSize()) {
LazyColumn(
modifier = modifier.fillMaxSize().stretchOverscroll(),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = bottomInset),
) {
items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
@@ -81,4 +82,5 @@ fun ArtistsContent(
}
}
}
}
}
@@ -35,7 +35,7 @@ import com.subtracks.data.model.QueueKind
import com.subtracks.data.model.SongListItem
import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.components.stretchOverscroll
import com.subtracks.ui.components.StretchOverscroll
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.parameter.parametersOf
@@ -84,7 +84,8 @@ fun PlaylistDetailScreen(
)
},
) { padding ->
LazyColumn(Modifier.padding(padding).fillMaxSize().stretchOverscroll()) {
StretchOverscroll(modifier = Modifier.padding(padding).fillMaxSize()) {
LazyColumn(Modifier.fillMaxSize()) {
item {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
@@ -120,4 +121,5 @@ fun PlaylistDetailScreen(
}
}
}
}
}
@@ -24,7 +24,7 @@ import com.subtracks.data.model.Playlist
import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.components.EmptyState
import com.subtracks.ui.components.LoadingState
import com.subtracks.ui.components.stretchOverscroll
import com.subtracks.ui.components.StretchOverscroll
@Composable
fun PlaylistsContent(
@@ -44,8 +44,9 @@ fun PlaylistsContent(
}
else -> {
StretchOverscroll(modifier = modifier.fillMaxSize()) {
LazyColumn(
modifier = modifier.fillMaxSize().stretchOverscroll(),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = bottomInset),
) {
items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
@@ -81,4 +82,5 @@ fun PlaylistsContent(
}
}
}
}
}
@@ -32,7 +32,7 @@ import com.subtracks.data.model.SongListItem
import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.components.EmptyState
import com.subtracks.ui.components.LoadingState
import com.subtracks.ui.components.stretchOverscroll
import com.subtracks.ui.components.StretchOverscroll
@Composable
fun SongsContent(
@@ -53,8 +53,9 @@ fun SongsContent(
}
else -> {
StretchOverscroll(modifier = modifier.fillMaxSize()) {
LazyColumn(
modifier = modifier.fillMaxSize().stretchOverscroll(),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = bottomInset),
) {
items(count = items.itemCount, key = items.itemKey { it.song.id }) { index ->
@@ -72,6 +73,7 @@ fun SongsContent(
}
}
}
}
}
@Composable
@@ -51,7 +51,7 @@ import com.subtracks.data.repo.SourceRepository
import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.EmptyState
import com.subtracks.ui.components.LoadingState
import com.subtracks.ui.components.stretchOverscroll
import com.subtracks.ui.components.StretchOverscroll
import com.subtracks.ui.library.SongRow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -339,9 +339,10 @@ fun QueueScreen(
}
else -> {
StretchOverscroll(modifier = Modifier.padding(padding).fillMaxSize()) {
LazyColumn(
state = listState,
modifier = Modifier.padding(padding).fillMaxSize().stretchOverscroll(),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 16.dp),
) {
items(count = rows.size, key = { rows[it].id }) { index ->
@@ -394,6 +395,7 @@ fun QueueScreen(
}
}
}
}
}
internal fun dropTarget(
@@ -32,7 +32,6 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.subtracks.ui.components.stretchOverscroll
import org.koin.compose.viewmodel.koinViewModel
@Composable
@@ -106,7 +105,6 @@ fun AddSourceScreen(
Modifier
.padding(padding)
.fillMaxSize()
.stretchOverscroll()
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
@@ -40,7 +40,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.subtracks.data.model.Source
import com.subtracks.ui.components.stretchOverscroll
import com.subtracks.ui.components.StretchOverscroll
import org.koin.compose.viewmodel.koinViewModel
private val bitrateOptions = listOf(0, 24, 32, 64, 96, 128, 192, 256, 320)
@@ -102,7 +102,8 @@ fun SettingsScreen(
)
},
) { padding ->
LazyColumn(Modifier.padding(padding).fillMaxSize().stretchOverscroll()) {
StretchOverscroll(modifier = Modifier.padding(padding).fillMaxSize()) {
LazyColumn(Modifier.fillMaxSize()) {
item { SectionHeader("Servers") }
items(sources.size, key = { sources[it].id }) { index ->
val source = sources[index]
@@ -156,6 +157,7 @@ fun SettingsScreen(
}
}
}
}
when (dialog) {
SettingsDialog.Bitrate -> {
@@ -27,7 +27,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
private const val SEED_ART_SIZE_PX = 128
private const val GRADIENT_FADE_START = 0.8f / 1.5f
private const val GRADIENT_FADE_START = 1.3f / 2.0f
data class ArtworkColors(
val scheme: ColorScheme,
@@ -84,7 +84,7 @@ fun artworkColorsFromSeed(seed: Int): ArtworkColors {
scheme = scheme,
gradientHigh = tone(hue, (s * 0.8f).coerceAtMost(0.60f), 0.30f),
gradientLow = tone(hue, s * 0.25f, 0.04f),
accents = listOf(tone(hue, s, 0.70f), tone(hue, s * 0.85f, 0.38f)),
accents = listOf(tone(hue, s, 0.78f), tone(hue, s * 0.9f, 0.20f)),
)
}