diff --git a/app/src/main/kotlin/com/subtracks/ui/components/StretchOverscroll.kt b/app/src/main/kotlin/com/subtracks/ui/components/StretchOverscroll.kt index b7005d2a..1431abad 100644 --- a/app/src/main/kotlin/com/subtracks/ui/components/StretchOverscroll.kt +++ b/app/src/main/kotlin/com/subtracks/ui/components/StretchOverscroll.kt @@ -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,26 +64,33 @@ 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 - .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) - } + CompositionLocalProvider(LocalOverscrollFactory provides null) { + Box( + modifier = + modifier + .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) + } + }, + ) { + content() } + } } diff --git a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt index 8f762737..cb3f38be 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt @@ -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,36 +169,39 @@ fun AlbumDetailScreen( .alpha(gradientAlpha), ) - LazyColumn( - state = listState, - 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) }, - ) + StretchOverscroll(modifier = Modifier.fillMaxSize()) { + LazyColumn( + state = listState, + contentPadding = + PaddingValues( + bottom = 16.dp + with(density) { navBarBottom.toDp() }, + ), + modifier = Modifier.fillMaxSize(), + ) { + item { + AlbumHeader( + album = album, + hasSongs = songs.isNotEmpty(), + coverArt = coverArt, + onPlay = { onSongClick(0) }, + onShuffle = onShuffle, + onDownload = onDownload, + onMore = onMore, + topInset = statusBarDp, + 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) }, + ) + } } } @@ -204,7 +209,7 @@ fun AlbumDetailScreen( 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, diff --git a/app/src/main/kotlin/com/subtracks/ui/library/AlbumsContent.kt b/app/src/main/kotlin/com/subtracks/ui/library/AlbumsContent.kt index 069ab97e..df6901d8 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/AlbumsContent.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/AlbumsContent.kt @@ -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,27 +45,29 @@ fun AlbumsContent( } else -> { - 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(), - ) { - items(count = items.itemCount, key = items.itemKey { it.id }) { index -> - val album = items[index] - if (album != null) { - CoverArt( - ref = coverArt(album.coverArt, true), - name = album.name, - modifier = - Modifier - .fillMaxWidth() - .aspectRatio(1f) - .clip(RoundedCornerShape(2.dp)) - .clickable { onAlbumClick(album) } - .testTag(ALBUM_COVER_TAG), - ) + 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(), + ) { + items(count = items.itemCount, key = items.itemKey { it.id }) { index -> + val album = items[index] + if (album != null) { + CoverArt( + ref = coverArt(album.coverArt, true), + name = album.name, + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(2.dp)) + .clickable { onAlbumClick(album) } + .testTag(ALBUM_COVER_TAG), + ) + } } } } diff --git a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt index db4cbc0b..67c0c17e 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt @@ -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,58 +123,60 @@ fun ArtistDetailScreen( ) Box(modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) { - 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(), - ) { - item(span = { GridItemSpan(maxLineSpan) }) { - Box(Modifier.fillMaxWidth().padding(bottom = 12.dp)) { - CoverArt( - ref = art, - name = artist?.name.orEmpty(), - modifier = Modifier.fillMaxWidth().height(ART_HEIGHT), - ) - Text( - text = artist?.name.orEmpty(), - style = imageNameStyle, - color = Color.White, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.align(Alignment.BottomStart).padding(TITLE_INSET), - ) + 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(), + ) { + item(span = { GridItemSpan(maxLineSpan) }) { + Box(Modifier.fillMaxWidth().padding(bottom = 12.dp)) { + CoverArt( + ref = art, + name = artist?.name.orEmpty(), + modifier = Modifier.fillMaxWidth().height(ART_HEIGHT), + ) + Text( + text = artist?.name.orEmpty(), + style = imageNameStyle, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.align(Alignment.BottomStart).padding(TITLE_INSET), + ) + } } - } - itemsIndexed(albums, key = { _, album -> album.id }) { index, album -> - Column( - modifier = - Modifier - .padding( - start = if (index % 2 == 0) 16.dp else 0.dp, - end = if (index % 2 == 1) 16.dp else 0.dp, - ).clickable { onAlbumClick(album) }, - ) { - CoverArt( - ref = coverArt(album.coverArt, false), - name = album.name, - modifier = Modifier.fillMaxWidth().aspectRatio(1f).clip(RoundedCornerShape(2.dp)), - ) - Text( - text = album.name, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onBackground, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(top = 6.dp), - ) - Text( - text = album.year?.takeIf { it > 0 }?.toString() ?: "\u00A0", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + itemsIndexed(albums, key = { _, album -> album.id }) { index, album -> + Column( + modifier = + Modifier + .padding( + start = if (index % 2 == 0) 16.dp else 0.dp, + end = if (index % 2 == 1) 16.dp else 0.dp, + ).clickable { onAlbumClick(album) }, + ) { + CoverArt( + ref = coverArt(album.coverArt, false), + name = album.name, + modifier = Modifier.fillMaxWidth().aspectRatio(1f).clip(RoundedCornerShape(2.dp)), + ) + Text( + text = album.name, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 6.dp), + ) + Text( + text = album.year?.takeIf { it > 0 }?.toString() ?: "\u00A0", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } } } diff --git a/app/src/main/kotlin/com/subtracks/ui/library/ArtistsContent.kt b/app/src/main/kotlin/com/subtracks/ui/library/ArtistsContent.kt index 28a0f5e7..47d8067f 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/ArtistsContent.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/ArtistsContent.kt @@ -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,38 +44,40 @@ fun ArtistsContent( } else -> { - LazyColumn( - modifier = modifier.fillMaxSize().stretchOverscroll(), - contentPadding = PaddingValues(bottom = bottomInset), - ) { - items(count = items.itemCount, key = items.itemKey { it.id }) { index -> - val artist = items[index] - if (artist != null) { - ListItem( - headlineContent = { - Text( - text = artist.name, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - supportingContent = { - Text( - text = "${artist.albumCount} albums", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - leadingContent = { - CoverArt( - ref = coverArt(artist.coverArt, true), - name = artist.name, - modifier = Modifier.size(48.dp).clip(CircleShape), - ) - }, - modifier = Modifier.clickable { onArtistClick(artist) }, - colors = ListItemDefaults.colors(containerColor = Color.Transparent), - ) + StretchOverscroll(modifier = modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = bottomInset), + ) { + items(count = items.itemCount, key = items.itemKey { it.id }) { index -> + val artist = items[index] + if (artist != null) { + ListItem( + headlineContent = { + Text( + text = artist.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + supportingContent = { + Text( + text = "${artist.albumCount} albums", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + leadingContent = { + CoverArt( + ref = coverArt(artist.coverArt, true), + name = artist.name, + modifier = Modifier.size(48.dp).clip(CircleShape), + ) + }, + modifier = Modifier.clickable { onArtistClick(artist) }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + ) + } } } } diff --git a/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailScreen.kt index a4217a15..24b4ec95 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailScreen.kt @@ -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,38 +84,40 @@ fun PlaylistDetailScreen( ) }, ) { padding -> - LazyColumn(Modifier.padding(padding).fillMaxSize().stretchOverscroll()) { - item { - Row( - modifier = Modifier.fillMaxWidth().padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - CoverArt( - ref = coverArt(playlist?.coverArt, false), - name = playlist?.name.orEmpty(), - modifier = Modifier.size(120.dp).clip(RoundedCornerShape(2.dp)), - ) - Column { - Text(playlist?.name.orEmpty(), style = MaterialTheme.typography.titleLarge) - Text( - text = "${playlist?.songCount ?: 0} songs", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + StretchOverscroll(modifier = Modifier.padding(padding).fillMaxSize()) { + LazyColumn(Modifier.fillMaxSize()) { + item { + Row( + modifier = Modifier.fillMaxWidth().padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CoverArt( + ref = coverArt(playlist?.coverArt, false), + name = playlist?.name.orEmpty(), + modifier = Modifier.size(120.dp).clip(RoundedCornerShape(2.dp)), ) + Column { + Text(playlist?.name.orEmpty(), style = MaterialTheme.typography.titleLarge) + Text( + text = "${playlist?.songCount ?: 0} songs", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } } - } - items(count = songs.itemCount, key = songs.itemKey { it.song.id }) { index -> - val item = songs[index] - if (item != null) { - SongRow( - song = item.song, - coverArtId = item.coverArt, - coverArt = coverArt, - isPlaying = item.song.id == playingSongId, - modifier = Modifier.clickable { onSongClick(index) }, - ) + items(count = songs.itemCount, key = songs.itemKey { it.song.id }) { index -> + val item = songs[index] + if (item != null) { + SongRow( + song = item.song, + coverArtId = item.coverArt, + coverArt = coverArt, + isPlaying = item.song.id == playingSongId, + modifier = Modifier.clickable { onSongClick(index) }, + ) + } } } } diff --git a/app/src/main/kotlin/com/subtracks/ui/library/PlaylistsContent.kt b/app/src/main/kotlin/com/subtracks/ui/library/PlaylistsContent.kt index 0211b26a..2511cf96 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/PlaylistsContent.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/PlaylistsContent.kt @@ -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,38 +44,40 @@ fun PlaylistsContent( } else -> { - LazyColumn( - modifier = modifier.fillMaxSize().stretchOverscroll(), - contentPadding = PaddingValues(bottom = bottomInset), - ) { - items(count = items.itemCount, key = items.itemKey { it.id }) { index -> - val playlist = items[index] - if (playlist != null) { - ListItem( - headlineContent = { - Text( - text = playlist.name, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - supportingContent = { - Text( - text = playlist.comment?.takeIf { it.isNotBlank() } ?: "${playlist.songCount} songs", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - leadingContent = { - CoverArt( - ref = coverArt(playlist.coverArt, true), - name = playlist.name, - modifier = Modifier.size(48.dp).clip(RoundedCornerShape(8.dp)), - ) - }, - modifier = Modifier.clickable { onPlaylistClick(playlist) }, - colors = ListItemDefaults.colors(containerColor = Color.Transparent), - ) + StretchOverscroll(modifier = modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = bottomInset), + ) { + items(count = items.itemCount, key = items.itemKey { it.id }) { index -> + val playlist = items[index] + if (playlist != null) { + ListItem( + headlineContent = { + Text( + text = playlist.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + supportingContent = { + Text( + text = playlist.comment?.takeIf { it.isNotBlank() } ?: "${playlist.songCount} songs", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + leadingContent = { + CoverArt( + ref = coverArt(playlist.coverArt, true), + name = playlist.name, + modifier = Modifier.size(48.dp).clip(RoundedCornerShape(8.dp)), + ) + }, + modifier = Modifier.clickable { onPlaylistClick(playlist) }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + ) + } } } } diff --git a/app/src/main/kotlin/com/subtracks/ui/library/SongsContent.kt b/app/src/main/kotlin/com/subtracks/ui/library/SongsContent.kt index e92916b3..eb53e15a 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/SongsContent.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/SongsContent.kt @@ -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,20 +53,22 @@ fun SongsContent( } else -> { - LazyColumn( - modifier = modifier.fillMaxSize().stretchOverscroll(), - contentPadding = PaddingValues(bottom = bottomInset), - ) { - items(count = items.itemCount, key = items.itemKey { it.song.id }) { index -> - val item = items[index] - if (item != null) { - SongRow( - song = item.song, - coverArtId = item.coverArt, - coverArt = coverArt, - isPlaying = item.song.id == playingSongId, - modifier = Modifier.clickable { onSongClick(index) }, - ) + StretchOverscroll(modifier = modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = bottomInset), + ) { + items(count = items.itemCount, key = items.itemKey { it.song.id }) { index -> + val item = items[index] + if (item != null) { + SongRow( + song = item.song, + coverArtId = item.coverArt, + coverArt = coverArt, + isPlaying = item.song.id == playingSongId, + modifier = Modifier.clickable { onSongClick(index) }, + ) + } } } } diff --git a/app/src/main/kotlin/com/subtracks/ui/playback/QueueScreen.kt b/app/src/main/kotlin/com/subtracks/ui/playback/QueueScreen.kt index c9e7d35e..c6a14eda 100644 --- a/app/src/main/kotlin/com/subtracks/ui/playback/QueueScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/playback/QueueScreen.kt @@ -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,55 +339,57 @@ fun QueueScreen( } else -> { - LazyColumn( - state = listState, - modifier = Modifier.padding(padding).fillMaxSize().stretchOverscroll(), - contentPadding = PaddingValues(bottom = 16.dp), - ) { - items(count = rows.size, key = { rows[it].id }) { index -> - val row = rows[index] - ReorderableItem( - state = reorderState, - key = row.id, - animateItemModifier = - Modifier.animateItem( - fadeInSpec = tween(150), - fadeOutSpec = tween(150), - placementSpec = tween(200), - ), - ) { isDragging -> - QueueRowItem( - row = row, - isPlaying = row.position == currentPosition, - floating = isDragging, - coverArt = coverArt, - dragHandle = - Modifier.draggableHandle( - onDragStarted = { - dragId = row.id - dragFrom = row.position - }, - onDragStopped = { - val id = dragId - val from = dragFrom - if (id != null && from != null) { - val index = rows.indexOfFirst { it.id == id } - val to = if (index < 0) from else dropTarget(rows, index, from) - if (to != from) { - onMove(from, to) - showUndo("Queue reordered") - } - } - dragId = null - dragFrom = null - }, + StretchOverscroll(modifier = Modifier.padding(padding).fillMaxSize()) { + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = 16.dp), + ) { + items(count = rows.size, key = { rows[it].id }) { index -> + val row = rows[index] + ReorderableItem( + state = reorderState, + key = row.id, + animateItemModifier = + Modifier.animateItem( + fadeInSpec = tween(150), + fadeOutSpec = tween(150), + placementSpec = tween(200), ), - onClick = { onPlay(row.position) }, - onRemove = { - onRemove(row.position) - showUndo("Removed from queue") - }, - ) + ) { isDragging -> + QueueRowItem( + row = row, + isPlaying = row.position == currentPosition, + floating = isDragging, + coverArt = coverArt, + dragHandle = + Modifier.draggableHandle( + onDragStarted = { + dragId = row.id + dragFrom = row.position + }, + onDragStopped = { + val id = dragId + val from = dragFrom + if (id != null && from != null) { + val index = rows.indexOfFirst { it.id == id } + val to = if (index < 0) from else dropTarget(rows, index, from) + if (to != from) { + onMove(from, to) + showUndo("Queue reordered") + } + } + dragId = null + dragFrom = null + }, + ), + onClick = { onPlay(row.position) }, + onRemove = { + onRemove(row.position) + showUndo("Removed from queue") + }, + ) + } } } } diff --git a/app/src/main/kotlin/com/subtracks/ui/settings/AddSourceScreen.kt b/app/src/main/kotlin/com/subtracks/ui/settings/AddSourceScreen.kt index 3b620393..d1279631 100644 --- a/app/src/main/kotlin/com/subtracks/ui/settings/AddSourceScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/settings/AddSourceScreen.kt @@ -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), diff --git a/app/src/main/kotlin/com/subtracks/ui/settings/SettingsScreen.kt b/app/src/main/kotlin/com/subtracks/ui/settings/SettingsScreen.kt index 6ff91761..d2a04547 100644 --- a/app/src/main/kotlin/com/subtracks/ui/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/settings/SettingsScreen.kt @@ -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,57 +102,59 @@ fun SettingsScreen( ) }, ) { padding -> - LazyColumn(Modifier.padding(padding).fillMaxSize().stretchOverscroll()) { - item { SectionHeader("Servers") } - items(sources.size, key = { sources[it].id }) { index -> - val source = sources[index] - ListItem( - headlineContent = { Text(source.name) }, - supportingContent = { - Text(source.address, maxLines = 1, overflow = TextOverflow.Ellipsis) - }, - leadingContent = { - RadioButton( - selected = source.id == activeSourceId, - onClick = { onSelectSource(source.id) }, - ) - }, - trailingContent = { - IconButton(onClick = { onDeleteSource(source.id) }) { - Icon(Icons.Rounded.Delete, contentDescription = "Remove ${source.name}") + StretchOverscroll(modifier = Modifier.padding(padding).fillMaxSize()) { + LazyColumn(Modifier.fillMaxSize()) { + item { SectionHeader("Servers") } + items(sources.size, key = { sources[it].id }) { index -> + val source = sources[index] + ListItem( + headlineContent = { Text(source.name) }, + supportingContent = { + Text(source.address, maxLines = 1, overflow = TextOverflow.Ellipsis) + }, + leadingContent = { + RadioButton( + selected = source.id == activeSourceId, + onClick = { onSelectSource(source.id) }, + ) + }, + trailingContent = { + IconButton(onClick = { onDeleteSource(source.id) }) { + Icon(Icons.Rounded.Delete, contentDescription = "Remove ${source.name}") + } + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + ) + } + item { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.End, + ) { + Button(onClick = onAddServer) { + Icon(Icons.Rounded.Add, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(8.dp)) + Text("Add server") } - }, - colors = ListItemDefaults.colors(containerColor = Color.Transparent), - ) - } - item { - Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.End, - ) { - Button(onClick = onAddServer) { - Icon(Icons.Rounded.Add, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(Modifier.width(8.dp)) - Text("Add server") } } - } - item { SectionHeader("Network") } - item { - ListItem( - headlineContent = { Text("Maximum bitrate") }, - supportingContent = { Text(bitrateLabel(maxBitrate)) }, - modifier = Modifier.clickable { dialog = SettingsDialog.Bitrate }, - colors = ListItemDefaults.colors(containerColor = Color.Transparent), - ) - } - item { - ListItem( - headlineContent = { Text("Preferred stream format") }, - supportingContent = { Text(streamFormat ?: "Use server default") }, - modifier = Modifier.clickable { dialog = SettingsDialog.Format }, - colors = ListItemDefaults.colors(containerColor = Color.Transparent), - ) + item { SectionHeader("Network") } + item { + ListItem( + headlineContent = { Text("Maximum bitrate") }, + supportingContent = { Text(bitrateLabel(maxBitrate)) }, + modifier = Modifier.clickable { dialog = SettingsDialog.Bitrate }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + ) + } + item { + ListItem( + headlineContent = { Text("Preferred stream format") }, + supportingContent = { Text(streamFormat ?: "Use server default") }, + modifier = Modifier.clickable { dialog = SettingsDialog.Format }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + ) + } } } } diff --git a/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt b/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt index af84b1d0..22cabb29 100644 --- a/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt +++ b/app/src/main/kotlin/com/subtracks/ui/theme/ArtworkTheme.kt @@ -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)), ) }