mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 23:39:15 +01:00
drop the custom overscroll, keep lists a hair scrollable, restore colours
This commit is contained in:
@@ -1,114 +0,0 @@
|
||||
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.OverscrollEffect
|
||||
import androidx.compose.foundation.OverscrollFactory
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.NestedScrollSource
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.node.DelegatableNode
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import kotlin.math.abs
|
||||
|
||||
private const val MAX_STRETCH_FRACTION = 0.14f
|
||||
|
||||
private class StretchOverscrollEffect : OverscrollEffect {
|
||||
val overscroll = mutableFloatStateOf(0f)
|
||||
var limitPx: Float = 1f
|
||||
|
||||
private val emptyNode = object : Modifier.Node() {}
|
||||
|
||||
override val node: DelegatableNode get() = emptyNode
|
||||
|
||||
override val isInProgress: Boolean get() = overscroll.floatValue != 0f
|
||||
|
||||
override fun applyToScroll(
|
||||
delta: Offset,
|
||||
source: NestedScrollSource,
|
||||
performScroll: (Offset) -> Offset,
|
||||
): Offset {
|
||||
if (delta.y == 0f) return performScroll(delta)
|
||||
var remainingY = delta.y
|
||||
if (overscroll.floatValue != 0f) {
|
||||
val current = overscroll.floatValue
|
||||
val next = current + remainingY
|
||||
val clamped = if (current > 0f) next.coerceAtLeast(0f) else next.coerceAtMost(0f)
|
||||
remainingY -= clamped - current
|
||||
overscroll.floatValue = clamped
|
||||
}
|
||||
val consumed = performScroll(Offset(delta.x, remainingY))
|
||||
val leftover = remainingY - consumed.y
|
||||
if (leftover != 0f) {
|
||||
val current = overscroll.floatValue
|
||||
val next = (current + leftover).coerceIn(-limitPx, limitPx)
|
||||
overscroll.floatValue =
|
||||
when {
|
||||
current > 0f -> next.coerceAtLeast(0f)
|
||||
current < 0f -> next.coerceAtMost(0f)
|
||||
else -> next
|
||||
}
|
||||
}
|
||||
return Offset(consumed.x, delta.y)
|
||||
}
|
||||
|
||||
override suspend fun applyToFling(
|
||||
velocity: Velocity,
|
||||
performFling: suspend (Velocity) -> Velocity,
|
||||
) {
|
||||
if (overscroll.floatValue != 0f) {
|
||||
Animatable(overscroll.floatValue)
|
||||
.animateTo(0f, spring(stiffness = Spring.StiffnessMedium)) {
|
||||
overscroll.floatValue = value
|
||||
}
|
||||
}
|
||||
performFling(velocity)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StretchOverscroll(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val effect = remember { StretchOverscrollEffect() }
|
||||
val factory =
|
||||
remember {
|
||||
object : OverscrollFactory {
|
||||
override fun createOverscrollEffect(): OverscrollEffect = effect
|
||||
|
||||
override fun hashCode(): Int = 0x5EED_1234
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === this
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalOverscrollFactory provides factory) {
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.onSizeChanged { effect.limitPx = (it.height * MAX_STRETCH_FRACTION).coerceAtLeast(1f) }
|
||||
.graphicsLayer {
|
||||
val value = effect.overscroll.floatValue
|
||||
if (value == 0f) return@graphicsLayer
|
||||
val height = size.height
|
||||
val resisted = value / (1f + abs(value) / (height * 0.35f))
|
||||
scaleY = 1f + abs(resisted) / height
|
||||
translationY = resisted
|
||||
transformOrigin = TransformOrigin(0.5f, if (resisted > 0f) 0f else 1f)
|
||||
},
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.subtracks.ui.components
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
private const val FILL_EPSILON_PX = 1
|
||||
|
||||
@Composable
|
||||
fun rememberViewportFill(state: LazyListState): Dp {
|
||||
val density = LocalDensity.current
|
||||
val fillPx by remember {
|
||||
derivedStateOf {
|
||||
val info = state.layoutInfo
|
||||
val realCount = info.totalItemsCount - 1
|
||||
if (realCount <= 0) {
|
||||
FILL_EPSILON_PX
|
||||
} else {
|
||||
val visibleReal = info.visibleItemsInfo.filter { it.index < realCount }
|
||||
if (visibleReal.size < realCount) {
|
||||
FILL_EPSILON_PX
|
||||
} else {
|
||||
(info.viewportSize.height - visibleReal.sumOf { it.size }).coerceAtLeast(0) + FILL_EPSILON_PX
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return with(density) { fillPx.toDp() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberViewportFill(state: LazyGridState): Dp {
|
||||
val density = LocalDensity.current
|
||||
val fillPx by remember {
|
||||
derivedStateOf {
|
||||
val info = state.layoutInfo
|
||||
val realCount = info.totalItemsCount - 1
|
||||
if (realCount <= 0) {
|
||||
FILL_EPSILON_PX
|
||||
} else {
|
||||
val visibleReal = info.visibleItemsInfo.filter { it.index < realCount }
|
||||
if (visibleReal.size < realCount) {
|
||||
FILL_EPSILON_PX
|
||||
} else {
|
||||
(info.viewportSize.height - visibleReal.sumOf { it.size.height }).coerceAtLeast(0) + FILL_EPSILON_PX
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return with(density) { fillPx.toDp() }
|
||||
}
|
||||
@@ -73,7 +73,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.rememberViewportFill
|
||||
import com.subtracks.ui.theme.ArtworkColors
|
||||
import com.subtracks.ui.theme.ArtworkTheme
|
||||
import com.subtracks.ui.theme.HeroGradient
|
||||
@@ -128,6 +128,7 @@ fun AlbumDetailScreen(
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
|
||||
BoxWithConstraints(modifier.fillMaxSize().background(Color.Black)) {
|
||||
val listState = rememberLazyListState()
|
||||
val fill = rememberViewportFill(listState)
|
||||
val density = LocalDensity.current
|
||||
val statusBarTop = WindowInsets.statusBars.getTop(density)
|
||||
val navBarBottom = WindowInsets.navigationBars.getBottom(density)
|
||||
@@ -170,7 +171,6 @@ fun AlbumDetailScreen(
|
||||
.alpha(gradientAlpha),
|
||||
)
|
||||
|
||||
StretchOverscroll(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
contentPadding =
|
||||
@@ -206,7 +206,7 @@ fun AlbumDetailScreen(
|
||||
.clickable { onSongClick(index) },
|
||||
)
|
||||
}
|
||||
}
|
||||
item { Spacer(Modifier.height(fill)) }
|
||||
}
|
||||
|
||||
Box(
|
||||
@@ -252,6 +252,7 @@ fun AlbumDetailScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.background((artwork?.gradientHigh ?: Color.Black).copy(alpha = barFraction))
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
|
||||
@@ -3,11 +3,15 @@ package com.subtracks.ui.library
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -23,7 +27,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.rememberViewportFill
|
||||
|
||||
const val ALBUM_COVER_TAG = "album-cover"
|
||||
|
||||
@@ -45,13 +49,15 @@ fun AlbumsContent(
|
||||
}
|
||||
|
||||
else -> {
|
||||
StretchOverscroll(modifier = modifier.fillMaxSize()) {
|
||||
val gridState = rememberLazyGridState()
|
||||
val fill = rememberViewportFill(gridState)
|
||||
LazyVerticalGrid(
|
||||
state = gridState,
|
||||
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(),
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
|
||||
val album = items[index]
|
||||
@@ -69,6 +75,8 @@ fun AlbumsContent(
|
||||
)
|
||||
}
|
||||
}
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
Spacer(Modifier.height(fill))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -50,7 +52,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.rememberViewportFill
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@@ -90,6 +92,7 @@ fun ArtistDetailScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val listState = rememberLazyGridState()
|
||||
val fill = rememberViewportFill(listState)
|
||||
val density = LocalDensity.current
|
||||
val lineHeight =
|
||||
with(density) {
|
||||
@@ -123,7 +126,6 @@ fun ArtistDetailScreen(
|
||||
)
|
||||
|
||||
Box(modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {
|
||||
StretchOverscroll(modifier = Modifier.fillMaxSize()) {
|
||||
LazyVerticalGrid(
|
||||
state = listState,
|
||||
columns = GridCells.Fixed(2),
|
||||
@@ -178,6 +180,8 @@ fun ArtistDetailScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
Spacer(Modifier.height(fill))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,12 @@ package com.subtracks.ui.library
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
@@ -24,7 +27,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.rememberViewportFill
|
||||
|
||||
@Composable
|
||||
fun ArtistsContent(
|
||||
@@ -44,9 +47,11 @@ fun ArtistsContent(
|
||||
}
|
||||
|
||||
else -> {
|
||||
StretchOverscroll(modifier = modifier.fillMaxSize()) {
|
||||
val listState = rememberLazyListState()
|
||||
val fill = rememberViewportFill(listState)
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = bottomInset),
|
||||
) {
|
||||
items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
|
||||
@@ -79,7 +84,7 @@ fun ArtistsContent(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item { Spacer(Modifier.height(fill)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
@@ -35,7 +38,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.rememberViewportFill
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
@@ -71,6 +74,8 @@ fun PlaylistDetailScreen(
|
||||
playingSongId: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val fill = rememberViewportFill(listState)
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
@@ -84,8 +89,10 @@ fun PlaylistDetailScreen(
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
StretchOverscroll(modifier = Modifier.padding(padding).fillMaxSize()) {
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.padding(padding).fillMaxSize(),
|
||||
) {
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
@@ -119,7 +126,7 @@ fun PlaylistDetailScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item { Spacer(Modifier.height(fill)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ package com.subtracks.ui.library
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
@@ -24,7 +27,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.rememberViewportFill
|
||||
|
||||
@Composable
|
||||
fun PlaylistsContent(
|
||||
@@ -44,9 +47,11 @@ fun PlaylistsContent(
|
||||
}
|
||||
|
||||
else -> {
|
||||
StretchOverscroll(modifier = modifier.fillMaxSize()) {
|
||||
val listState = rememberLazyListState()
|
||||
val fill = rememberViewportFill(listState)
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = bottomInset),
|
||||
) {
|
||||
items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
|
||||
@@ -79,7 +84,7 @@ fun PlaylistsContent(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item { Spacer(Modifier.height(fill)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,12 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.PlayArrow
|
||||
@@ -20,6 +23,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -32,7 +36,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.rememberViewportFill
|
||||
|
||||
@Composable
|
||||
fun SongsContent(
|
||||
@@ -53,9 +57,11 @@ fun SongsContent(
|
||||
}
|
||||
|
||||
else -> {
|
||||
StretchOverscroll(modifier = modifier.fillMaxSize()) {
|
||||
val listState = rememberLazyListState()
|
||||
val fill = rememberViewportFill(listState)
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = bottomInset),
|
||||
) {
|
||||
items(count = items.itemCount, key = items.itemKey { it.song.id }) { index ->
|
||||
@@ -70,7 +76,7 @@ fun SongsContent(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item { Spacer(Modifier.height(fill)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +122,7 @@ fun SongRow(
|
||||
Text(
|
||||
text = song.title,
|
||||
color = if (isPlaying) MaterialTheme.colorScheme.primary else Color.Unspecified,
|
||||
fontWeight = if (isPlaying) FontWeight.SemiBold else null,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
|
||||
@@ -6,7 +6,9 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -51,7 +53,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.rememberViewportFill
|
||||
import com.subtracks.ui.library.SongRow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -284,6 +286,7 @@ fun QueueScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val fill = rememberViewportFill(listState)
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
var dragId by remember { mutableStateOf<Long?>(null) }
|
||||
@@ -339,10 +342,9 @@ fun QueueScreen(
|
||||
}
|
||||
|
||||
else -> {
|
||||
StretchOverscroll(modifier = Modifier.padding(padding).fillMaxSize()) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier.padding(padding).fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = 16.dp),
|
||||
) {
|
||||
items(count = rows.size, key = { rows[it].id }) { index ->
|
||||
@@ -391,7 +393,7 @@ fun QueueScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item { Spacer(Modifier.height(fill)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material.icons.rounded.Add
|
||||
@@ -40,7 +42,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.rememberViewportFill
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
private val bitrateOptions = listOf(0, 24, 32, 64, 96, 128, 192, 256, 320)
|
||||
@@ -88,6 +90,8 @@ fun SettingsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var dialog by remember { mutableStateOf<SettingsDialog?>(null) }
|
||||
val listState = rememberLazyListState()
|
||||
val fill = rememberViewportFill(listState)
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
@@ -102,8 +106,10 @@ fun SettingsScreen(
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
StretchOverscroll(modifier = Modifier.padding(padding).fillMaxSize()) {
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.padding(padding).fillMaxSize(),
|
||||
) {
|
||||
item { SectionHeader("Servers") }
|
||||
items(sources.size, key = { sources[it].id }) { index ->
|
||||
val source = sources[index]
|
||||
@@ -155,7 +161,7 @@ fun SettingsScreen(
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
)
|
||||
}
|
||||
}
|
||||
item { Spacer(Modifier.height(fill)) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.palette.graphics.Palette
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.compose.LocalPlatformContext
|
||||
@@ -37,16 +36,22 @@ data class ArtworkColors(
|
||||
val accents: List<Color>,
|
||||
)
|
||||
|
||||
fun artworkColorsFromSeed(seed: Int): ArtworkColors {
|
||||
val (hue, saturation, _) = Color(seed or 0xFF000000.toInt()).toHsl()
|
||||
fun artworkColorsFromSeed(seed: Int): ArtworkColors = artworkColorsFromSeeds(seed, null)
|
||||
|
||||
fun artworkColorsFromSeeds(
|
||||
primarySeed: Int,
|
||||
secondarySeed: Int?,
|
||||
): ArtworkColors {
|
||||
val (hue, saturation, _) = Color(primarySeed or 0xFF000000.toInt()).toHsl()
|
||||
val s = saturation.coerceAtMost(0.85f)
|
||||
val secondary = secondarySeed?.let { Color(it or 0xFF000000.toInt()) }
|
||||
|
||||
fun tone(
|
||||
h: Float,
|
||||
sat: Float,
|
||||
l: Float,
|
||||
) = Color.hsl(h % 360f, sat.coerceIn(0f, 1f), l.coerceIn(0f, 1f))
|
||||
val primary = toneMaxLuminance(hue, s, 0.62f, 0.45f)
|
||||
val primary = tone(hue, s, 0.62f)
|
||||
val background = tone(hue, (s * 0.35f).coerceAtMost(0.20f), 0.06f)
|
||||
val onBackground = tone(hue, (s * 0.10f).coerceAtMost(0.08f), 0.95f)
|
||||
|
||||
@@ -84,36 +89,20 @@ fun artworkColorsFromSeed(seed: Int): ArtworkColors {
|
||||
scheme = scheme,
|
||||
gradientHigh = tone(hue, (s * 0.6f).coerceAtMost(0.50f), 0.22f),
|
||||
gradientLow = tone(hue, s * 0.30f, 0.03f),
|
||||
accents = listOf(tone(hue, s * 0.75f, 0.95f), tone(hue, s, 0.08f)),
|
||||
accents = listOf(tone(hue, s * 0.8f, 0.90f), secondary ?: tone(hue, s, 0.10f)),
|
||||
)
|
||||
}
|
||||
|
||||
private fun toneMaxLuminance(
|
||||
h: Float,
|
||||
s: Float,
|
||||
lightness: Float,
|
||||
maxLuminance: Float,
|
||||
): Color {
|
||||
var l = lightness
|
||||
var color = Color.hsl(h % 360f, s.coerceIn(0f, 1f), l.coerceIn(0f, 1f))
|
||||
var guard = 0
|
||||
while (color.luminance() > maxLuminance && guard++ < 30) {
|
||||
l = (l - 0.02f).coerceAtLeast(0.05f)
|
||||
color = Color.hsl(h % 360f, s.coerceIn(0f, 1f), l)
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberArtworkColors(ref: CoverArtRef?): ArtworkColors? {
|
||||
val seed by rememberArtworkSeed(ref)
|
||||
return remember(seed) { seed?.let(::artworkColorsFromSeed) }
|
||||
val seeds by rememberArtworkSeed(ref)
|
||||
return remember(seeds) { seeds?.let { (primary, secondary) -> artworkColorsFromSeeds(primary, secondary) } }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberArtworkSeed(ref: CoverArtRef?): State<Int?> {
|
||||
private fun rememberArtworkSeed(ref: CoverArtRef?): State<Pair<Int, Int?>?> {
|
||||
val context = LocalPlatformContext.current
|
||||
return produceState<Int?>(initialValue = null, ref?.cacheKey) {
|
||||
return produceState<Pair<Int, Int?>?>(initialValue = null, ref?.cacheKey) {
|
||||
value = null
|
||||
val art = ref ?: return@produceState
|
||||
value =
|
||||
@@ -129,7 +118,7 @@ private fun rememberArtworkSeed(ref: CoverArtRef?): State<Int?> {
|
||||
.allowHardware(false)
|
||||
.build()
|
||||
val image = (SingletonImageLoader.get(context).execute(request) as? SuccessResult)?.image
|
||||
image?.let { seedFrom(it.toBitmap()) }
|
||||
image?.let { seedsFrom(it.toBitmap()) }
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (_: Exception) {
|
||||
@@ -139,15 +128,21 @@ private fun rememberArtworkSeed(ref: CoverArtRef?): State<Int?> {
|
||||
}
|
||||
}
|
||||
|
||||
private fun seedFrom(bitmap: Bitmap): Int? {
|
||||
private fun seedsFrom(bitmap: Bitmap): Pair<Int, Int?>? {
|
||||
val palette = Palette.from(bitmap).generate()
|
||||
return (
|
||||
val primary =
|
||||
palette.vibrantSwatch
|
||||
?: palette.lightVibrantSwatch
|
||||
?: palette.darkVibrantSwatch
|
||||
?: palette.mutedSwatch
|
||||
?: palette.dominantSwatch
|
||||
)?.rgb
|
||||
?: return null
|
||||
val secondary =
|
||||
palette.darkVibrantSwatch
|
||||
?: palette.mutedSwatch
|
||||
?: palette.darkMutedSwatch
|
||||
?: palette.dominantSwatch
|
||||
return primary.rgb to secondary?.takeIf { it.rgb != primary.rgb }?.rgb
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
Reference in New Issue
Block a user