mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
stretch overscroll everywhere, overlay the album bar, richer gradient
This commit is contained in:
@@ -0,0 +1,83 @@
|
|||||||
|
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.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.unit.Velocity
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
private const val MAX_STRETCH_FRACTION = 0.35f
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Modifier.stretchOverscroll(): Modifier {
|
||||||
|
val overscroll = remember { Animatable(0f) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val connection =
|
||||||
|
remember(overscroll) {
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource,
|
||||||
|
): Offset {
|
||||||
|
val current = overscroll.value
|
||||||
|
if (current == 0f || available.y == 0f) return Offset.Zero
|
||||||
|
val next = current + available.y
|
||||||
|
scope.launch {
|
||||||
|
overscroll.snapTo(if (current > 0f) next.coerceAtLeast(0f) else next.coerceAtMost(0f))
|
||||||
|
}
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostScroll(
|
||||||
|
consumed: Offset,
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource,
|
||||||
|
): Offset {
|
||||||
|
val current = overscroll.value
|
||||||
|
if (available.y == 0f) return Offset.Zero
|
||||||
|
val next = current + available.y
|
||||||
|
scope.launch {
|
||||||
|
overscroll.snapTo(if (current > 0f) next.coerceAtLeast(0f) else next.coerceAtMost(0f))
|
||||||
|
}
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPostFling(
|
||||||
|
consumed: Velocity,
|
||||||
|
available: Velocity,
|
||||||
|
): Velocity {
|
||||||
|
overscroll.animateTo(0f, spring(stiffness = Spring.StiffnessMediumLow))
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
.nestedScroll(connection)
|
||||||
|
.graphicsLayer {
|
||||||
|
val raw = overscroll.value
|
||||||
|
if (raw == 0f) {
|
||||||
|
scaleY = 1f
|
||||||
|
translationY = 0f
|
||||||
|
} else {
|
||||||
|
val limit = size.height * MAX_STRETCH_FRACTION
|
||||||
|
val capped = raw.coerceIn(-limit, limit)
|
||||||
|
val resisted = capped / (1f + abs(capped) / (size.height * 0.5f))
|
||||||
|
scaleY = 1f + abs(resisted) / size.height
|
||||||
|
translationY = resisted
|
||||||
|
transformOrigin = TransformOrigin(0.5f, if (resisted > 0f) 0f else 1f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,12 @@ import androidx.compose.animation.core.animateFloatAsState
|
|||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
@@ -15,6 +17,7 @@ import androidx.compose.foundation.layout.aspectRatio
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
@@ -36,7 +39,6 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
@@ -51,8 +53,12 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.layout.positionInParent
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@@ -65,6 +71,7 @@ import com.subtracks.data.model.QueueKind
|
|||||||
import com.subtracks.data.model.Song
|
import com.subtracks.data.model.Song
|
||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
import com.subtracks.ui.components.CoverArt
|
import com.subtracks.ui.components.CoverArt
|
||||||
|
import com.subtracks.ui.components.stretchOverscroll
|
||||||
import com.subtracks.ui.theme.ArtworkColors
|
import com.subtracks.ui.theme.ArtworkColors
|
||||||
import com.subtracks.ui.theme.ArtworkTheme
|
import com.subtracks.ui.theme.ArtworkTheme
|
||||||
import com.subtracks.ui.theme.HeroGradient
|
import com.subtracks.ui.theme.HeroGradient
|
||||||
@@ -76,8 +83,7 @@ import kotlin.math.roundToInt
|
|||||||
|
|
||||||
private const val DOT = "\u00B7"
|
private const val DOT = "\u00B7"
|
||||||
private const val GRADIENT_SCREENS = 1.5f
|
private const val GRADIENT_SCREENS = 1.5f
|
||||||
private const val FADE_DISTANCE_DP = 96
|
private const val FADE_DISTANCE_DP = 64
|
||||||
private const val CONTROLS_BLOCK_DP = 64
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AlbumDetailRoute(
|
fun AlbumDetailRoute(
|
||||||
@@ -122,12 +128,13 @@ fun AlbumDetailScreen(
|
|||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val statusBarTop = WindowInsets.statusBars.getTop(density)
|
val statusBarTop = WindowInsets.statusBars.getTop(density)
|
||||||
|
val navBarBottom = WindowInsets.navigationBars.getBottom(density)
|
||||||
val barHeightPx = statusBarTop + with(density) { TopAppBarDefaults.TopAppBarExpandedHeight.toPx() }
|
val barHeightPx = statusBarTop + with(density) { TopAppBarDefaults.TopAppBarExpandedHeight.toPx() }
|
||||||
val controlsBlockPx = with(density) { CONTROLS_BLOCK_DP.dp.toPx() }
|
|
||||||
val fadeDistancePx = with(density) { FADE_DISTANCE_DP.dp.toPx() }
|
val fadeDistancePx = with(density) { FADE_DISTANCE_DP.dp.toPx() }
|
||||||
|
|
||||||
var headerHeightPx by remember { mutableFloatStateOf(0f) }
|
var headerHeightPx by remember { mutableFloatStateOf(0f) }
|
||||||
var rowHeightPx by remember { mutableFloatStateOf(0f) }
|
var rowHeightPx by remember { mutableFloatStateOf(0f) }
|
||||||
|
var titleTopPx by remember { mutableFloatStateOf(0f) }
|
||||||
val scrollPx by remember {
|
val scrollPx by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val index = listState.firstVisibleItemIndex
|
val index = listState.firstVisibleItemIndex
|
||||||
@@ -137,12 +144,10 @@ fun AlbumDetailScreen(
|
|||||||
}
|
}
|
||||||
val barFraction by remember {
|
val barFraction by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
if (headerHeightPx == 0f) {
|
if (titleTopPx <= 0f || fadeDistancePx <= 0f) {
|
||||||
0f
|
0f
|
||||||
} else {
|
} else {
|
||||||
val end = headerHeightPx - controlsBlockPx - barHeightPx
|
((scrollPx - (titleTopPx - barHeightPx)) / fadeDistancePx).coerceIn(0f, 1f)
|
||||||
val start = end - fadeDistancePx
|
|
||||||
((scrollPx - start) / (end - start)).coerceIn(0f, 1f)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,9 +167,51 @@ fun AlbumDetailScreen(
|
|||||||
.alpha(gradientAlpha),
|
.alpha(gradientAlpha),
|
||||||
)
|
)
|
||||||
|
|
||||||
Scaffold(
|
LazyColumn(
|
||||||
containerColor = Color.Transparent,
|
state = listState,
|
||||||
topBar = {
|
contentPadding =
|
||||||
|
PaddingValues(
|
||||||
|
bottom = 16.dp + with(density) { navBarBottom.toDp() },
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxSize().stretchOverscroll(),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
AlbumHeader(
|
||||||
|
album = album,
|
||||||
|
hasSongs = songs.isNotEmpty(),
|
||||||
|
coverArt = coverArt,
|
||||||
|
onPlay = { onSongClick(0) },
|
||||||
|
onShuffle = onShuffle,
|
||||||
|
onDownload = onDownload,
|
||||||
|
onMore = onMore,
|
||||||
|
titleModifier = Modifier.onGloballyPositioned { titleTopPx = it.positionInParent().y },
|
||||||
|
modifier = Modifier.onSizeChanged { headerHeightPx = it.height.toFloat() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
itemsIndexed(songs, key = { _, song -> song.id }) { index, song ->
|
||||||
|
SongRow(
|
||||||
|
song = song,
|
||||||
|
isPlaying = song.id == playingSongId,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.onSizeChanged { rowHeightPx = it.height.toFloat() }
|
||||||
|
.clickable { onSongClick(index) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(with(density) { (barHeightPx + 24.dp.toPx()).toDp() })
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
listOf(Color.Black.copy(alpha = 0.8f), Color.Transparent),
|
||||||
|
),
|
||||||
|
).alpha(1f - barFraction),
|
||||||
|
)
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
@@ -180,39 +227,19 @@ fun AlbumDetailScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
|
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
|
||||||
modifier = Modifier.background(Color.Black.copy(alpha = barFraction)),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { padding ->
|
|
||||||
LazyColumn(state = listState, modifier = Modifier.padding(padding).fillMaxSize()) {
|
|
||||||
item {
|
|
||||||
AlbumHeader(
|
|
||||||
album = album,
|
|
||||||
hasSongs = songs.isNotEmpty(),
|
|
||||||
coverArt = coverArt,
|
|
||||||
onPlay = { onSongClick(0) },
|
|
||||||
onShuffle = onShuffle,
|
|
||||||
onDownload = onDownload,
|
|
||||||
onMore = onMore,
|
|
||||||
modifier = Modifier.onSizeChanged { headerHeightPx = it.height.toFloat() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
itemsIndexed(songs, key = { _, song -> song.id }) { index, song ->
|
|
||||||
SongRow(
|
|
||||||
song = song,
|
|
||||||
isPlaying = song.id == playingSongId,
|
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.onSizeChanged { rowHeightPx = it.height.toFloat() }
|
.align(Alignment.TopStart)
|
||||||
.clickable { onSongClick(index) },
|
.background(Color.Black.copy(alpha = barFraction))
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
) {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AlbumHeader(
|
private fun AlbumHeader(
|
||||||
@@ -223,16 +250,20 @@ private fun AlbumHeader(
|
|||||||
onShuffle: () -> Unit,
|
onShuffle: () -> Unit,
|
||||||
onDownload: () -> Unit,
|
onDownload: () -> Unit,
|
||||||
onMore: () -> Unit,
|
onMore: () -> Unit,
|
||||||
|
titleModifier: Modifier = Modifier,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp),
|
modifier =
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 24.dp, end = 24.dp, top = 8.dp, bottom = 16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
CoverArt(
|
CoverArt(
|
||||||
ref = coverArt(album?.coverArt, false),
|
ref = coverArt(album?.coverArt, false),
|
||||||
name = album?.name.orEmpty(),
|
name = album?.name.orEmpty(),
|
||||||
modifier = Modifier.fillMaxWidth(0.78f).aspectRatio(1f).clip(RoundedCornerShape(2.dp)),
|
modifier = Modifier.fillMaxWidth(0.86f).aspectRatio(1f).clip(RoundedCornerShape(2.dp)),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
Text(
|
Text(
|
||||||
@@ -241,6 +272,7 @@ private fun AlbumHeader(
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = titleModifier,
|
||||||
)
|
)
|
||||||
val subtitle =
|
val subtitle =
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.subtracks.data.model.CoverArtRef
|
|||||||
import com.subtracks.ui.components.CoverArt
|
import com.subtracks.ui.components.CoverArt
|
||||||
import com.subtracks.ui.components.EmptyState
|
import com.subtracks.ui.components.EmptyState
|
||||||
import com.subtracks.ui.components.LoadingState
|
import com.subtracks.ui.components.LoadingState
|
||||||
|
import com.subtracks.ui.components.stretchOverscroll
|
||||||
|
|
||||||
const val ALBUM_COVER_TAG = "album-cover"
|
const val ALBUM_COVER_TAG = "album-cover"
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ fun AlbumsContent(
|
|||||||
contentPadding = PaddingValues(start = 8.dp, top = 8.dp, end = 8.dp, bottom = bottomInset + 8.dp),
|
contentPadding = PaddingValues(start = 8.dp, top = 8.dp, end = 8.dp, bottom = bottomInset + 8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize().stretchOverscroll(),
|
||||||
) {
|
) {
|
||||||
items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
|
items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
|
||||||
val album = items[index]
|
val album = items[index]
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import com.subtracks.data.model.Album
|
|||||||
import com.subtracks.data.model.Artist
|
import com.subtracks.data.model.Artist
|
||||||
import com.subtracks.data.model.CoverArtRef
|
import com.subtracks.data.model.CoverArtRef
|
||||||
import com.subtracks.ui.components.CoverArt
|
import com.subtracks.ui.components.CoverArt
|
||||||
|
import com.subtracks.ui.components.stretchOverscroll
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
@@ -128,7 +129,7 @@ fun ArtistDetailScreen(
|
|||||||
contentPadding = PaddingValues(bottom = 16.dp + navBarBottom),
|
contentPadding = PaddingValues(bottom = 16.dp + navBarBottom),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize().stretchOverscroll(),
|
||||||
) {
|
) {
|
||||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
Box(Modifier.fillMaxWidth().padding(bottom = 12.dp)) {
|
Box(Modifier.fillMaxWidth().padding(bottom = 12.dp)) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import com.subtracks.data.model.CoverArtRef
|
|||||||
import com.subtracks.ui.components.CoverArt
|
import com.subtracks.ui.components.CoverArt
|
||||||
import com.subtracks.ui.components.EmptyState
|
import com.subtracks.ui.components.EmptyState
|
||||||
import com.subtracks.ui.components.LoadingState
|
import com.subtracks.ui.components.LoadingState
|
||||||
|
import com.subtracks.ui.components.stretchOverscroll
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ArtistsContent(
|
fun ArtistsContent(
|
||||||
@@ -44,7 +45,7 @@ fun ArtistsContent(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize().stretchOverscroll(),
|
||||||
contentPadding = PaddingValues(bottom = bottomInset),
|
contentPadding = PaddingValues(bottom = bottomInset),
|
||||||
) {
|
) {
|
||||||
items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
|
items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import com.subtracks.data.model.QueueKind
|
|||||||
import com.subtracks.data.model.SongListItem
|
import com.subtracks.data.model.SongListItem
|
||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
import com.subtracks.ui.components.CoverArt
|
import com.subtracks.ui.components.CoverArt
|
||||||
|
import com.subtracks.ui.components.stretchOverscroll
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
@@ -83,7 +84,7 @@ fun PlaylistDetailScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
LazyColumn(Modifier.padding(padding).fillMaxSize()) {
|
LazyColumn(Modifier.padding(padding).fillMaxSize().stretchOverscroll()) {
|
||||||
item {
|
item {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import com.subtracks.data.model.Playlist
|
|||||||
import com.subtracks.ui.components.CoverArt
|
import com.subtracks.ui.components.CoverArt
|
||||||
import com.subtracks.ui.components.EmptyState
|
import com.subtracks.ui.components.EmptyState
|
||||||
import com.subtracks.ui.components.LoadingState
|
import com.subtracks.ui.components.LoadingState
|
||||||
|
import com.subtracks.ui.components.stretchOverscroll
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PlaylistsContent(
|
fun PlaylistsContent(
|
||||||
@@ -44,7 +45,7 @@ fun PlaylistsContent(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize().stretchOverscroll(),
|
||||||
contentPadding = PaddingValues(bottom = bottomInset),
|
contentPadding = PaddingValues(bottom = bottomInset),
|
||||||
) {
|
) {
|
||||||
items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
|
items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import com.subtracks.data.model.SongListItem
|
|||||||
import com.subtracks.ui.components.CoverArt
|
import com.subtracks.ui.components.CoverArt
|
||||||
import com.subtracks.ui.components.EmptyState
|
import com.subtracks.ui.components.EmptyState
|
||||||
import com.subtracks.ui.components.LoadingState
|
import com.subtracks.ui.components.LoadingState
|
||||||
|
import com.subtracks.ui.components.stretchOverscroll
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SongsContent(
|
fun SongsContent(
|
||||||
@@ -53,7 +54,7 @@ fun SongsContent(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize().stretchOverscroll(),
|
||||||
contentPadding = PaddingValues(bottom = bottomInset),
|
contentPadding = PaddingValues(bottom = bottomInset),
|
||||||
) {
|
) {
|
||||||
items(count = items.itemCount, key = items.itemKey { it.song.id }) { index ->
|
items(count = items.itemCount, key = items.itemKey { it.song.id }) { index ->
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import com.subtracks.data.repo.SourceRepository
|
|||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
import com.subtracks.ui.components.EmptyState
|
import com.subtracks.ui.components.EmptyState
|
||||||
import com.subtracks.ui.components.LoadingState
|
import com.subtracks.ui.components.LoadingState
|
||||||
|
import com.subtracks.ui.components.stretchOverscroll
|
||||||
import com.subtracks.ui.library.SongRow
|
import com.subtracks.ui.library.SongRow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -340,7 +341,7 @@ fun QueueScreen(
|
|||||||
else -> {
|
else -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier.padding(padding).fillMaxSize(),
|
modifier = Modifier.padding(padding).fillMaxSize().stretchOverscroll(),
|
||||||
contentPadding = PaddingValues(bottom = 16.dp),
|
contentPadding = PaddingValues(bottom = 16.dp),
|
||||||
) {
|
) {
|
||||||
items(count = rows.size, key = { rows[it].id }) { index ->
|
items(count = rows.size, key = { rows[it].id }) { index ->
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.subtracks.ui.components.stretchOverscroll
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -105,6 +106,7 @@ fun AddSourceScreen(
|
|||||||
Modifier
|
Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.stretchOverscroll()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.subtracks.data.model.Source
|
import com.subtracks.data.model.Source
|
||||||
|
import com.subtracks.ui.components.stretchOverscroll
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
private val bitrateOptions = listOf(0, 24, 32, 64, 96, 128, 192, 256, 320)
|
private val bitrateOptions = listOf(0, 24, 32, 64, 96, 128, 192, 256, 320)
|
||||||
@@ -101,7 +102,7 @@ fun SettingsScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
LazyColumn(Modifier.padding(padding).fillMaxSize()) {
|
LazyColumn(Modifier.padding(padding).fillMaxSize().stretchOverscroll()) {
|
||||||
item { SectionHeader("Servers") }
|
item { SectionHeader("Servers") }
|
||||||
items(sources.size, key = { sources[it].id }) { index ->
|
items(sources.size, key = { sources[it].id }) { index ->
|
||||||
val source = sources[index]
|
val source = sources[index]
|
||||||
|
|||||||
@@ -82,9 +82,9 @@ fun artworkColorsFromSeed(seed: Int): ArtworkColors {
|
|||||||
|
|
||||||
return ArtworkColors(
|
return ArtworkColors(
|
||||||
scheme = scheme,
|
scheme = scheme,
|
||||||
gradientHigh = tone(hue, (s * 0.7f).coerceAtMost(0.55f), 0.25f),
|
gradientHigh = tone(hue, (s * 0.8f).coerceAtMost(0.60f), 0.30f),
|
||||||
gradientLow = tone(hue, s * 0.20f, 0.04f),
|
gradientLow = tone(hue, s * 0.25f, 0.04f),
|
||||||
accents = listOf(tone(hue, s, 0.55f), tone(hue, s * 0.7f, 0.42f)),
|
accents = listOf(tone(hue, s, 0.70f), tone(hue, s * 0.85f, 0.38f)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,9 +169,9 @@ fun HeroGradient(
|
|||||||
drawRect(
|
drawRect(
|
||||||
brush =
|
brush =
|
||||||
Brush.radialGradient(
|
Brush.radialGradient(
|
||||||
colors = listOf(accent.copy(alpha = 0.40f), accent.copy(alpha = 0f)),
|
colors = listOf(accent.copy(alpha = 0.55f), accent.copy(alpha = 0f)),
|
||||||
center = Offset(size.width * 0.15f, size.height * 0.06f),
|
center = Offset(size.width * 0.18f, size.height * 0.03f),
|
||||||
radius = size.width * 0.85f,
|
radius = size.width * 0.95f,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -179,9 +179,19 @@ fun HeroGradient(
|
|||||||
drawRect(
|
drawRect(
|
||||||
brush =
|
brush =
|
||||||
Brush.radialGradient(
|
Brush.radialGradient(
|
||||||
colors = listOf(accent.copy(alpha = 0.32f), accent.copy(alpha = 0f)),
|
colors = listOf(accent.copy(alpha = 0.50f), accent.copy(alpha = 0f)),
|
||||||
center = Offset(size.width * 0.90f, size.height * 0.24f),
|
center = Offset(size.width * 0.92f, size.height * 0.28f),
|
||||||
radius = size.width * 0.80f,
|
radius = size.width * 0.85f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
accents.getOrNull(0)?.let { accent ->
|
||||||
|
drawRect(
|
||||||
|
brush =
|
||||||
|
Brush.radialGradient(
|
||||||
|
colors = listOf(accent.copy(alpha = 0.30f), accent.copy(alpha = 0f)),
|
||||||
|
center = Offset(size.width * 0.85f, size.height * 0.02f),
|
||||||
|
radius = size.width * 0.70f,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -189,8 +199,8 @@ fun HeroGradient(
|
|||||||
brush =
|
brush =
|
||||||
Brush.radialGradient(
|
Brush.radialGradient(
|
||||||
colors = listOf(high.copy(alpha = 0.45f), high.copy(alpha = 0f)),
|
colors = listOf(high.copy(alpha = 0.45f), high.copy(alpha = 0f)),
|
||||||
center = Offset(size.width * 0.55f, size.height * 0.45f),
|
center = Offset(size.width * 0.35f, size.height * 0.52f),
|
||||||
radius = size.width * 0.90f,
|
radius = size.width * 0.95f,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
drawRect(
|
drawRect(
|
||||||
|
|||||||
Reference in New Issue
Block a user