Hide floating action button when scrolling down and add scroll to top button to each screen
This commit is contained in:
parent
78c44988d7
commit
b30b282628
25 changed files with 902 additions and 573 deletions
|
@ -0,0 +1,149 @@
|
|||
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.utils.isScrollingDown
|
||||
import it.vfsfitvnm.vimusic.utils.isScrollingDownToIsFar
|
||||
import it.vfsfitvnm.vimusic.utils.smoothScrollToTop
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun BoxScope.FloatingActionsContainerWithScrollToTop(
|
||||
lazyGridState: LazyGridState,
|
||||
modifier: Modifier = Modifier,
|
||||
iconId: Int? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val transitionState = remember {
|
||||
MutableTransitionState(false to false)
|
||||
}.apply { targetState = lazyGridState.isScrollingDownToIsFar() }
|
||||
|
||||
FloatingActions(
|
||||
transitionState = transitionState,
|
||||
onScrollToTop = lazyGridState::smoothScrollToTop,
|
||||
iconId = iconId,
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun BoxScope.FloatingActionsContainerWithScrollToTop(
|
||||
lazyListState: LazyListState,
|
||||
modifier: Modifier = Modifier,
|
||||
iconId: Int? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val transitionState = remember {
|
||||
MutableTransitionState(false to false)
|
||||
}.apply { targetState = lazyListState.isScrollingDownToIsFar() }
|
||||
|
||||
FloatingActions(
|
||||
transitionState = transitionState,
|
||||
onScrollToTop = lazyListState::smoothScrollToTop,
|
||||
iconId = iconId,
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun BoxScope.FloatingActionsContainerWithScrollToTop(
|
||||
scrollState: ScrollState,
|
||||
modifier: Modifier = Modifier,
|
||||
iconId: Int? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val transitionState = remember {
|
||||
MutableTransitionState(false to false)
|
||||
}.apply { targetState = scrollState.isScrollingDown() to false }
|
||||
|
||||
FloatingActions(
|
||||
transitionState = transitionState,
|
||||
iconId = iconId,
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun BoxScope.FloatingActions(
|
||||
transitionState: MutableTransitionState<Pair<Boolean, Boolean>>,
|
||||
modifier: Modifier = Modifier,
|
||||
onScrollToTop: (suspend () -> Unit)? = null,
|
||||
iconId: Int? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val transition = updateTransition(transitionState, "FloatingActionsContainer")
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
modifier = modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 16.dp)
|
||||
.padding(LocalPlayerAwarePaddingValues.current)
|
||||
) {
|
||||
onScrollToTop?.let {
|
||||
transition.AnimatedVisibility(
|
||||
visible = { it.first && it.second },
|
||||
enter = slideInVertically(tween(500, if (iconId == null) 0 else 100)) { it },
|
||||
exit = slideOutVertically(tween(500, 0)) { it },
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
SecondaryButton(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
onScrollToTop()
|
||||
}
|
||||
},
|
||||
iconId = R.drawable.chevron_up,
|
||||
modifier = Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
iconId?.let {
|
||||
onClick?.let {
|
||||
transition.AnimatedVisibility(
|
||||
visible = { it.first },
|
||||
enter = slideInVertically(tween(500, 0)) { it },
|
||||
exit = slideOutVertically(tween(500, 100)) { it },
|
||||
) {
|
||||
PrimaryButton(
|
||||
iconId = iconId,
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,11 +35,11 @@ import it.vfsfitvnm.vimusic.utils.isLandscape
|
|||
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||
|
||||
@Composable
|
||||
fun NavigationRail(
|
||||
inline fun NavigationRail(
|
||||
topIconButtonId: Int,
|
||||
onTopIconButtonClick: () -> Unit,
|
||||
noinline onTopIconButtonClick: () -> Unit,
|
||||
tabIndex: Int,
|
||||
onTabIndexChanged: (Int) -> Unit,
|
||||
crossinline onTabIndexChanged: (Int) -> Unit,
|
||||
content: @Composable ColumnScope.(@Composable (Int, String, Int) -> Unit) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
@ -144,7 +144,7 @@ fun NavigationRail(
|
|||
}
|
||||
}
|
||||
|
||||
private fun Modifier.vertical(enabled: Boolean = true) =
|
||||
fun Modifier.vertical(enabled: Boolean = true) =
|
||||
if (enabled)
|
||||
layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
|
|
|
@ -5,8 +5,6 @@ import androidx.compose.foundation.Image
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -16,11 +14,10 @@ import androidx.compose.ui.draw.clip
|
|||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
|
||||
@Composable
|
||||
fun BoxScope.PrimaryButton(
|
||||
fun PrimaryButton(
|
||||
onClick: () -> Unit,
|
||||
@DrawableRes iconId: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -30,9 +27,6 @@ fun BoxScope.PrimaryButton(
|
|||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(all = 16.dp)
|
||||
.padding(LocalPlayerAwarePaddingValues.current)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable(enabled = isEnabled, onClick = onClick)
|
||||
.background(colorPalette.background2)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedContentScope
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
|
@ -19,7 +18,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.IntOffset
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
|
||||
@SuppressLint("ModifierParameter")
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun Scaffold(
|
||||
|
@ -28,8 +26,6 @@ fun Scaffold(
|
|||
tabIndex: Int,
|
||||
onTabChanged: (Int) -> Unit,
|
||||
tabColumnContent: @Composable ColumnScope.(@Composable (Int, String, Int) -> Unit) -> Unit,
|
||||
primaryIconButtonId: Int? = null,
|
||||
onPrimaryIconButtonClick: () -> Unit = {},
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable AnimatedVisibilityScope.(Int) -> Unit
|
||||
) {
|
||||
|
@ -69,14 +65,7 @@ fun Scaffold(
|
|||
slideIntoContainer(slideDirection, animationSpec) with
|
||||
slideOutOfContainer(slideDirection, animationSpec)
|
||||
},
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
primaryIconButtonId?.let {
|
||||
PrimaryButton(
|
||||
iconId = primaryIconButtonId,
|
||||
onClick = onPrimaryIconButtonClick
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
import it.vfsfitvnm.vimusic.utils.smoothScrollToTop
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ScrollToTop(
|
||||
lazyListState: LazyListState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val showScrollTopButton by remember {
|
||||
derivedStateOf {
|
||||
lazyListState.firstVisibleItemIndex > lazyListState.layoutInfo.visibleItemsInfo.size
|
||||
}
|
||||
}
|
||||
|
||||
ScrollToTop(
|
||||
isVisible = showScrollTopButton,
|
||||
onClick = lazyListState::smoothScrollToTop,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScrollToTop(
|
||||
isVisible: Boolean,
|
||||
onClick: suspend () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isVisible,
|
||||
enter = slideInVertically { it },
|
||||
exit = slideOutVertically { it },
|
||||
modifier = modifier
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(all = 16.dp)
|
||||
.padding(LocalPlayerAwarePaddingValues.current)
|
||||
.clickable {
|
||||
coroutineScope.launch {
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
.size(32.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.chevron_down),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(LocalAppearance.current.colorPalette.text),
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.rotate(180f)
|
||||
.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
|
||||
@Composable
|
||||
fun SecondaryButton(
|
||||
onClick: () -> Unit,
|
||||
@DrawableRes iconId: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
isEnabled: Boolean = true,
|
||||
) {
|
||||
val (colorPalette) = LocalAppearance.current
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(CircleShape)
|
||||
.clickable(enabled = isEnabled, onClick = onClick)
|
||||
.background(colorPalette.background2)
|
||||
.size(48.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(iconId),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
|
@ -24,9 +25,9 @@ import it.vfsfitvnm.vimusic.models.DetailedSong
|
|||
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
|
||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.LayoutWithAdaptiveThumbnail
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||
import it.vfsfitvnm.vimusic.ui.items.SongItem
|
||||
import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
|
||||
|
@ -68,9 +69,12 @@ fun AlbumSongs(
|
|||
|
||||
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
|
||||
Box {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = Modifier
|
||||
.background(colorPalette.background0)
|
||||
|
@ -152,14 +156,16 @@ fun AlbumSongs(
|
|||
}
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
FloatingActionsContainerWithScrollToTop(
|
||||
lazyListState = lazyListState,
|
||||
iconId = R.drawable.shuffle,
|
||||
isEnabled = songs.isNotEmpty(),
|
||||
onClick = {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayFromBeginning(
|
||||
songs.shuffled().map(DetailedSong::asMediaItem)
|
||||
)
|
||||
if (songs.isNotEmpty()) {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayFromBeginning(
|
||||
songs.shuffled().map(DetailedSong::asMediaItem)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -21,9 +22,9 @@ import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
|
|||
import it.vfsfitvnm.vimusic.savers.nullableSaver
|
||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.LayoutWithAdaptiveThumbnail
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||
import it.vfsfitvnm.vimusic.ui.items.SongItem
|
||||
import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
|
||||
|
@ -63,9 +64,12 @@ fun ArtistLocalSongs(
|
|||
val songThumbnailSizeDp = Dimensions.thumbnails.song
|
||||
val songThumbnailSizePx = songThumbnailSizeDp.px
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
|
||||
Box {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = Modifier
|
||||
.background(colorPalette.background0)
|
||||
|
@ -128,14 +132,18 @@ fun ArtistLocalSongs(
|
|||
}
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
FloatingActionsContainerWithScrollToTop(
|
||||
lazyListState = lazyListState,
|
||||
iconId = R.drawable.shuffle,
|
||||
isEnabled = !songs.isNullOrEmpty(),
|
||||
onClick = {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayFromBeginning(
|
||||
songs!!.shuffled().map(DetailedSong::asMediaItem)
|
||||
)
|
||||
songs?.let { songs ->
|
||||
if (songs.isNotEmpty()) {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayFromBeginning(
|
||||
songs.shuffled().map(DetailedSong::asMediaItem)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -26,9 +26,9 @@ import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
|||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.LayoutWithAdaptiveThumbnail
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||
import it.vfsfitvnm.vimusic.ui.items.AlbumItem
|
||||
|
@ -70,6 +70,8 @@ fun ArtistOverview(
|
|||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 24.dp, bottom = 8.dp)
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
|
||||
Box {
|
||||
Column(
|
||||
|
@ -77,7 +79,7 @@ fun ArtistOverview(
|
|||
modifier = Modifier
|
||||
.background(colorPalette.background0)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.verticalScroll(scrollState)
|
||||
.padding(LocalPlayerAwarePaddingValues.current)
|
||||
) {
|
||||
headerContent {
|
||||
|
@ -258,7 +260,8 @@ fun ArtistOverview(
|
|||
}
|
||||
|
||||
youtubeArtistPage?.shuffleEndpoint?.let { shuffleEndpoint ->
|
||||
PrimaryButton(
|
||||
FloatingActionsContainerWithScrollToTop(
|
||||
scrollState = scrollState,
|
||||
iconId = R.drawable.shuffle,
|
||||
onClick = {
|
||||
binder?.stopRadio()
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -20,6 +21,7 @@ import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
|||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
|
||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.InFavoritesMediaItemMenu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
||||
|
@ -70,8 +72,11 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
|
|||
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||
val thumbnailSize = thumbnailSizeDp.px
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
Box {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = Modifier
|
||||
.background(colorPalette.background0)
|
||||
|
@ -120,6 +125,7 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
|
|||
song = song,
|
||||
onDismiss = menuState::hide
|
||||
)
|
||||
|
||||
BuiltInPlaylist.Offline -> InHistoryMediaItemMenu(
|
||||
song = song,
|
||||
onDismiss = menuState::hide
|
||||
|
@ -129,7 +135,10 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
|
|||
},
|
||||
onClick = {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayAtIndex(songs.map(DetailedSong::asMediaItem), index)
|
||||
binder?.player?.forcePlayAtIndex(
|
||||
songs.map(DetailedSong::asMediaItem),
|
||||
index
|
||||
)
|
||||
}
|
||||
)
|
||||
.animateItemPlacement()
|
||||
|
@ -137,14 +146,16 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
|
|||
}
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
FloatingActionsContainerWithScrollToTop(
|
||||
lazyListState = lazyListState,
|
||||
iconId = R.drawable.shuffle,
|
||||
isEnabled = songs.isNotEmpty(),
|
||||
onClick = {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayFromBeginning(
|
||||
songs.shuffled().map(DetailedSong::asMediaItem)
|
||||
)
|
||||
if (songs.isNotEmpty()) {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayFromBeginning(
|
||||
songs.shuffled().map(DetailedSong::asMediaItem)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,11 +7,13 @@ import androidx.compose.animation.core.tween
|
|||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
|
@ -25,6 +27,7 @@ import it.vfsfitvnm.vimusic.enums.AlbumSortBy
|
|||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||
import it.vfsfitvnm.vimusic.models.Album
|
||||
import it.vfsfitvnm.vimusic.savers.AlbumListSaver
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
|
||||
import it.vfsfitvnm.vimusic.ui.items.AlbumItem
|
||||
|
@ -42,7 +45,8 @@ import kotlinx.coroutines.flow.flowOn
|
|||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun HomeAlbums(
|
||||
onAlbumClick: (Album) -> Unit
|
||||
onAlbumClick: (Album) -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
) {
|
||||
val (colorPalette) = LocalAppearance.current
|
||||
|
||||
|
@ -68,62 +72,73 @@ fun HomeAlbums(
|
|||
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = Modifier
|
||||
.background(colorPalette.background0)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
item(
|
||||
key = "header",
|
||||
contentType = 0
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
Box {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = Modifier
|
||||
.background(colorPalette.background0)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Header(title = "Albums") {
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.calendar,
|
||||
color = if (sortBy == AlbumSortBy.Year) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = AlbumSortBy.Year }
|
||||
)
|
||||
item(
|
||||
key = "header",
|
||||
contentType = 0
|
||||
) {
|
||||
Header(title = "Albums") {
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.calendar,
|
||||
color = if (sortBy == AlbumSortBy.Year) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = AlbumSortBy.Year }
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.text,
|
||||
color = if (sortBy == AlbumSortBy.Title) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = AlbumSortBy.Title }
|
||||
)
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.text,
|
||||
color = if (sortBy == AlbumSortBy.Title) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = AlbumSortBy.Title }
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.time,
|
||||
color = if (sortBy == AlbumSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = AlbumSortBy.DateAdded }
|
||||
)
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.time,
|
||||
color = if (sortBy == AlbumSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = AlbumSortBy.DateAdded }
|
||||
)
|
||||
|
||||
Spacer(
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.width(2.dp)
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.arrow_up,
|
||||
color = colorPalette.text,
|
||||
onClick = { sortOrder = !sortOrder },
|
||||
modifier = Modifier
|
||||
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
items = items,
|
||||
key = Album::id
|
||||
) { album ->
|
||||
AlbumItem(
|
||||
album = album,
|
||||
thumbnailSizePx = thumbnailSizePx,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
modifier = Modifier
|
||||
.width(2.dp)
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.arrow_up,
|
||||
color = colorPalette.text,
|
||||
onClick = { sortOrder = !sortOrder },
|
||||
modifier = Modifier
|
||||
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||
.clickable(onClick = { onAlbumClick(album) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
items = items,
|
||||
key = Album::id
|
||||
) { album ->
|
||||
AlbumItem(
|
||||
album = album,
|
||||
thumbnailSizePx = thumbnailSizePx,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onAlbumClick(album) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
FloatingActionsContainerWithScrollToTop(
|
||||
lazyListState = lazyListState,
|
||||
iconId = R.drawable.search,
|
||||
onClick = onSearchClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.width
|
||||
|
@ -15,6 +16,7 @@ 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.items
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
|
@ -29,6 +31,7 @@ import it.vfsfitvnm.vimusic.enums.ArtistSortBy
|
|||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||
import it.vfsfitvnm.vimusic.models.Artist
|
||||
import it.vfsfitvnm.vimusic.savers.ArtistListSaver
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
|
||||
import it.vfsfitvnm.vimusic.ui.items.ArtistItem
|
||||
|
@ -46,7 +49,8 @@ import kotlinx.coroutines.flow.flowOn
|
|||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun HomeArtistList(
|
||||
onArtistClick: (Artist) -> Unit
|
||||
onArtistClick: (Artist) -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
) {
|
||||
val (colorPalette) = LocalAppearance.current
|
||||
|
||||
|
@ -72,61 +76,72 @@ fun HomeArtistList(
|
|||
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
||||
)
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
verticalArrangement = Arrangement.spacedBy(Dimensions.itemsVerticalPadding * 2),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
space = Dimensions.itemsVerticalPadding * 2,
|
||||
alignment = Alignment.CenterHorizontally
|
||||
),
|
||||
modifier = Modifier
|
||||
.background(colorPalette.background0)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
item(
|
||||
key = "header",
|
||||
contentType = 0,
|
||||
span = { GridItemSpan(maxLineSpan) }
|
||||
val lazyGridState = rememberLazyGridState()
|
||||
|
||||
Box {
|
||||
LazyVerticalGrid(
|
||||
state = lazyGridState,
|
||||
columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
verticalArrangement = Arrangement.spacedBy(Dimensions.itemsVerticalPadding * 2),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
space = Dimensions.itemsVerticalPadding * 2,
|
||||
alignment = Alignment.CenterHorizontally
|
||||
),
|
||||
modifier = Modifier
|
||||
.background(colorPalette.background0)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Header(title = "Artists") {
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.text,
|
||||
color = if (sortBy == ArtistSortBy.Name) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = ArtistSortBy.Name }
|
||||
)
|
||||
item(
|
||||
key = "header",
|
||||
contentType = 0,
|
||||
span = { GridItemSpan(maxLineSpan) }
|
||||
) {
|
||||
Header(title = "Artists") {
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.text,
|
||||
color = if (sortBy == ArtistSortBy.Name) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = ArtistSortBy.Name }
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.time,
|
||||
color = if (sortBy == ArtistSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = ArtistSortBy.DateAdded }
|
||||
)
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.time,
|
||||
color = if (sortBy == ArtistSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = ArtistSortBy.DateAdded }
|
||||
)
|
||||
|
||||
Spacer(
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.width(2.dp)
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.arrow_up,
|
||||
color = colorPalette.text,
|
||||
onClick = { sortOrder = !sortOrder },
|
||||
modifier = Modifier
|
||||
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(items = items, key = Artist::id) { artist ->
|
||||
ArtistItem(
|
||||
artist = artist,
|
||||
thumbnailSizePx = thumbnailSizePx,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.width(2.dp)
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.arrow_up,
|
||||
color = colorPalette.text,
|
||||
onClick = { sortOrder = !sortOrder },
|
||||
modifier = Modifier
|
||||
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||
.clickable(onClick = { onArtistClick(artist) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(items = items, key = Artist::id) { artist ->
|
||||
ArtistItem(
|
||||
artist = artist,
|
||||
thumbnailSizePx = thumbnailSizePx,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onArtistClick(artist) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
FloatingActionsContainerWithScrollToTop(
|
||||
lazyGridState = lazyGridState,
|
||||
iconId = R.drawable.search,
|
||||
onClick = onSearchClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package it.vfsfitvnm.vimusic.ui.screens.home
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
|
@ -7,6 +8,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.width
|
||||
|
@ -14,6 +16,7 @@ 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.items
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
@ -32,6 +35,7 @@ import it.vfsfitvnm.vimusic.enums.SortOrder
|
|||
import it.vfsfitvnm.vimusic.models.Playlist
|
||||
import it.vfsfitvnm.vimusic.query
|
||||
import it.vfsfitvnm.vimusic.savers.PlaylistPreviewListSaver
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||
|
@ -47,11 +51,13 @@ import it.vfsfitvnm.vimusic.utils.rememberPreference
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalFoundationApi
|
||||
@Composable
|
||||
fun HomePlaylists(
|
||||
onBuiltInPlaylist: (BuiltInPlaylist) -> Unit,
|
||||
onPlaylistClick: (Playlist) -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
) {
|
||||
val (colorPalette) = LocalAppearance.current
|
||||
|
||||
|
@ -95,101 +101,112 @@ fun HomePlaylists(
|
|||
val thumbnailSizeDp = 108.dp
|
||||
val thumbnailSizePx = thumbnailSizeDp.px
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
verticalArrangement = Arrangement.spacedBy(Dimensions.itemsVerticalPadding * 2),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
space = Dimensions.itemsVerticalPadding * 2,
|
||||
alignment = Alignment.CenterHorizontally
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(colorPalette.background0)
|
||||
) {
|
||||
item(key = "header", contentType = 0, span = { GridItemSpan(maxLineSpan) }) {
|
||||
Header(title = "Playlists") {
|
||||
SecondaryTextButton(
|
||||
text = "New playlist",
|
||||
onClick = { isCreatingANewPlaylist = true }
|
||||
)
|
||||
val lazyGridState = rememberLazyGridState()
|
||||
|
||||
Spacer(
|
||||
Box {
|
||||
LazyVerticalGrid(
|
||||
state = lazyGridState,
|
||||
columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
verticalArrangement = Arrangement.spacedBy(Dimensions.itemsVerticalPadding * 2),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
space = Dimensions.itemsVerticalPadding * 2,
|
||||
alignment = Alignment.CenterHorizontally
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(colorPalette.background0)
|
||||
) {
|
||||
item(key = "header", contentType = 0, span = { GridItemSpan(maxLineSpan) }) {
|
||||
Header(title = "Playlists") {
|
||||
SecondaryTextButton(
|
||||
text = "New playlist",
|
||||
onClick = { isCreatingANewPlaylist = true }
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.medical,
|
||||
color = if (sortBy == PlaylistSortBy.SongCount) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = PlaylistSortBy.SongCount }
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.text,
|
||||
color = if (sortBy == PlaylistSortBy.Name) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = PlaylistSortBy.Name }
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.time,
|
||||
color = if (sortBy == PlaylistSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = PlaylistSortBy.DateAdded }
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.width(2.dp)
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.arrow_up,
|
||||
color = colorPalette.text,
|
||||
onClick = { sortOrder = !sortOrder },
|
||||
modifier = Modifier
|
||||
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item(key = "favorites") {
|
||||
PlaylistItem(
|
||||
icon = R.drawable.heart,
|
||||
colorTint = colorPalette.red,
|
||||
name = "Favorites",
|
||||
songCount = null,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Favorites) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.medical,
|
||||
color = if (sortBy == PlaylistSortBy.SongCount) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = PlaylistSortBy.SongCount }
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.text,
|
||||
color = if (sortBy == PlaylistSortBy.Name) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = PlaylistSortBy.Name }
|
||||
)
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.time,
|
||||
color = if (sortBy == PlaylistSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled,
|
||||
onClick = { sortBy = PlaylistSortBy.DateAdded }
|
||||
)
|
||||
|
||||
Spacer(
|
||||
item(key = "offline") {
|
||||
PlaylistItem(
|
||||
icon = R.drawable.airplane,
|
||||
colorTint = colorPalette.blue,
|
||||
name = "Offline",
|
||||
songCount = null,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.width(2.dp)
|
||||
.clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Offline) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
|
||||
HeaderIconButton(
|
||||
icon = R.drawable.arrow_up,
|
||||
color = colorPalette.text,
|
||||
onClick = { sortOrder = !sortOrder },
|
||||
items(items = items, key = { it.playlist.id }) { playlistPreview ->
|
||||
PlaylistItem(
|
||||
playlist = playlistPreview,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
thumbnailSizePx = thumbnailSizePx,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||
.clickable(onClick = { onPlaylistClick(playlistPreview.playlist) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item(key = "favorites") {
|
||||
PlaylistItem(
|
||||
icon = R.drawable.heart,
|
||||
colorTint = colorPalette.red,
|
||||
name = "Favorites",
|
||||
songCount = null,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Favorites) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
|
||||
item(key = "offline") {
|
||||
PlaylistItem(
|
||||
icon = R.drawable.airplane,
|
||||
colorTint = colorPalette.blue,
|
||||
name = "Offline",
|
||||
songCount = null,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Offline) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
|
||||
items(items = items, key = { it.playlist.id }) { playlistPreview ->
|
||||
PlaylistItem(
|
||||
playlist = playlistPreview,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
thumbnailSizePx = thumbnailSizePx,
|
||||
alternative = true,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onPlaylistClick(playlistPreview.playlist) })
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
FloatingActionsContainerWithScrollToTop(
|
||||
lazyGridState = lazyGridState,
|
||||
iconId = R.drawable.search,
|
||||
onClick = onSearchClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,8 +114,6 @@ fun HomeScreen(onPlaylistUrl: (String) -> Unit) {
|
|||
Item(3, "Artists", R.drawable.person)
|
||||
Item(4, "Albums", R.drawable.disc)
|
||||
},
|
||||
primaryIconButtonId = R.drawable.search,
|
||||
onPrimaryIconButtonClick = { searchRoute("") }
|
||||
) { currentTabIndex ->
|
||||
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
||||
when (currentTabIndex) {
|
||||
|
@ -123,14 +121,24 @@ fun HomeScreen(onPlaylistUrl: (String) -> Unit) {
|
|||
onAlbumClick = { albumRoute(it) },
|
||||
onArtistClick = { artistRoute(it) },
|
||||
onPlaylistClick = { playlistRoute(it) },
|
||||
onSearchClick = { searchRoute("") }
|
||||
)
|
||||
1 -> HomeSongs(
|
||||
onSearchClick = { searchRoute("") }
|
||||
)
|
||||
1 -> HomeSongs()
|
||||
2 -> HomePlaylists(
|
||||
onBuiltInPlaylist = { builtInPlaylistRoute(it) },
|
||||
onPlaylistClick = { localPlaylistRoute(it.id) }
|
||||
onPlaylistClick = { localPlaylistRoute(it.id) },
|
||||
onSearchClick = { searchRoute("") }
|
||||
)
|
||||
3 -> HomeArtistList(
|
||||
onArtistClick = { artistRoute(it.id) },
|
||||
onSearchClick = { searchRoute("") }
|
||||
)
|
||||
4 -> HomeAlbums(
|
||||
onAlbumClick = { albumRoute(it.id) },
|
||||
onSearchClick = { searchRoute("") }
|
||||
)
|
||||
3 -> HomeArtistList(onArtistClick = { artistRoute(it.id) })
|
||||
4 -> HomeAlbums(onAlbumClick = { albumRoute(it.id) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
|
@ -37,10 +36,10 @@ import it.vfsfitvnm.vimusic.enums.SortOrder
|
|||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
|
||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.ScrollToTop
|
||||
import it.vfsfitvnm.vimusic.ui.items.SongItem
|
||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
|
@ -62,7 +61,9 @@ import kotlinx.coroutines.flow.flowOn
|
|||
@ExperimentalFoundationApi
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun HomeSongs() {
|
||||
fun HomeSongs(
|
||||
onSearchClick: () -> Unit
|
||||
) {
|
||||
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||
val binder = LocalPlayerServiceBinder.current
|
||||
val menuState = LocalMenuState.current
|
||||
|
@ -187,11 +188,10 @@ fun HomeSongs() {
|
|||
}
|
||||
}
|
||||
|
||||
ScrollToTop(
|
||||
FloatingActionsContainerWithScrollToTop(
|
||||
lazyListState = lazyListState,
|
||||
modifier = Modifier
|
||||
.offset(x = Dimensions.navigationRailIconOffset - Dimensions.navigationRailWidth)
|
||||
.align(Alignment.BottomStart)
|
||||
iconId = R.drawable.search,
|
||||
onClick = onSearchClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ import it.vfsfitvnm.vimusic.savers.nullableSaver
|
|||
import it.vfsfitvnm.vimusic.savers.resultSaver
|
||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||
|
@ -81,6 +82,7 @@ fun QuickPicks(
|
|||
onAlbumClick: (String) -> Unit,
|
||||
onArtistClick: (String) -> Unit,
|
||||
onPlaylistClick: (String) -> Unit,
|
||||
onSearchClick: () -> Unit,
|
||||
) {
|
||||
val (colorPalette, typography) = LocalAppearance.current
|
||||
val binder = LocalPlayerServiceBinder.current
|
||||
|
@ -129,6 +131,8 @@ fun QuickPicks(
|
|||
)
|
||||
}
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
BoxWithConstraints {
|
||||
val itemInHorizontalGridWidth = maxWidth * quickPicksLazyGridItemWidthFactor
|
||||
|
||||
|
@ -136,7 +140,7 @@ fun QuickPicks(
|
|||
modifier = Modifier
|
||||
.background(colorPalette.background0)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.verticalScroll(scrollState)
|
||||
.padding(LocalPlayerAwarePaddingValues.current)
|
||||
) {
|
||||
Header(title = "Quick picks")
|
||||
|
@ -345,5 +349,11 @@ fun QuickPicks(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingActionsContainerWithScrollToTop(
|
||||
scrollState = scrollState,
|
||||
iconId = R.drawable.search,
|
||||
onClick = onSearchClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,13 +35,13 @@ import it.vfsfitvnm.vimusic.savers.nullableSaver
|
|||
import it.vfsfitvnm.vimusic.transaction
|
||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.IconButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.InPlaylistMediaItemMenu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.Menu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
||||
import it.vfsfitvnm.vimusic.ui.items.SongItem
|
||||
|
@ -274,17 +274,18 @@ fun LocalPlaylistSongs(
|
|||
}
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
FloatingActionsContainerWithScrollToTop(
|
||||
lazyListState = lazyListState,
|
||||
iconId = R.drawable.shuffle,
|
||||
isEnabled = playlistWithSongs?.songs?.isNotEmpty() == true,
|
||||
onClick = {
|
||||
playlistWithSongs?.songs
|
||||
?.shuffled()
|
||||
?.map(DetailedSong::asMediaItem)
|
||||
?.let { mediaItems ->
|
||||
playlistWithSongs?.songs?.let { songs ->
|
||||
if (songs.isNotEmpty()) {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayFromBeginning(mediaItems)
|
||||
binder?.player?.forcePlayFromBeginning(
|
||||
songs.shuffled().map(DetailedSong::asMediaItem)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.saveable.autoSaver
|
||||
|
@ -29,12 +30,12 @@ import it.vfsfitvnm.vimusic.savers.nullableSaver
|
|||
import it.vfsfitvnm.vimusic.transaction
|
||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.LayoutWithAdaptiveThumbnail
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.adaptiveThumbnailContent
|
||||
import it.vfsfitvnm.vimusic.ui.items.SongItem
|
||||
|
@ -62,7 +63,7 @@ import kotlinx.coroutines.withContext
|
|||
fun PlaylistSongList(
|
||||
browseId: String,
|
||||
) {
|
||||
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||
val (colorPalette) = LocalAppearance.current
|
||||
val binder = LocalPlayerServiceBinder.current
|
||||
val context = LocalContext.current
|
||||
val menuState = LocalMenuState.current
|
||||
|
@ -162,9 +163,12 @@ fun PlaylistSongList(
|
|||
|
||||
val thumbnailContent = adaptiveThumbnailContent(playlistPage == null, playlistPage?.thumbnail?.url)
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
|
||||
Box {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = Modifier
|
||||
.background(colorPalette.background0)
|
||||
|
@ -219,13 +223,17 @@ fun PlaylistSongList(
|
|||
}
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
FloatingActionsContainerWithScrollToTop(
|
||||
lazyListState = lazyListState,
|
||||
iconId = R.drawable.shuffle,
|
||||
isEnabled = playlistPage?.songsPage?.items?.isNotEmpty() == true,
|
||||
onClick = {
|
||||
playlistPage?.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayFromBeginning(mediaItems.shuffled())
|
||||
playlistPage?.songsPage?.items?.let { songs ->
|
||||
if (songs.isNotEmpty()) {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayFromBeginning(
|
||||
songs.shuffled().map(Innertube.SongItem::asMediaItem)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -3,9 +3,11 @@ package it.vfsfitvnm.vimusic.ui.screens.search
|
|||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -21,6 +23,7 @@ import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
|||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
|
||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||
|
@ -65,68 +68,75 @@ fun LocalSongSearch(
|
|||
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||
val thumbnailSizePx = thumbnailSizeDp.px
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
item(
|
||||
key = "header",
|
||||
contentType = 0
|
||||
) {
|
||||
Header(
|
||||
titleContent = {
|
||||
BasicTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = onTextFieldValueChanged,
|
||||
textStyle = typography.xxl.medium.align(TextAlign.End),
|
||||
singleLine = true,
|
||||
maxLines = 1,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
cursorBrush = SolidColor(colorPalette.text),
|
||||
decorationBox = decorationBox
|
||||
)
|
||||
},
|
||||
actionsContent = {
|
||||
if (textFieldValue.text.isNotEmpty()) {
|
||||
SecondaryTextButton(
|
||||
text = "Clear",
|
||||
onClick = { onTextFieldValueChanged(TextFieldValue()) }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
items(
|
||||
items = items,
|
||||
key = DetailedSong::id,
|
||||
) { song ->
|
||||
SongItem(
|
||||
song = song,
|
||||
thumbnailSizePx = thumbnailSizePx,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
onLongClick = {
|
||||
menuState.display {
|
||||
InHistoryMediaItemMenu(
|
||||
song = song,
|
||||
onDismiss = menuState::hide
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
val mediaItem = song.asMediaItem
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlay(mediaItem)
|
||||
binder?.setupRadio(
|
||||
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
|
||||
Box {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
item(
|
||||
key = "header",
|
||||
contentType = 0
|
||||
) {
|
||||
Header(
|
||||
titleContent = {
|
||||
BasicTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = onTextFieldValueChanged,
|
||||
textStyle = typography.xxl.medium.align(TextAlign.End),
|
||||
singleLine = true,
|
||||
maxLines = 1,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
cursorBrush = SolidColor(colorPalette.text),
|
||||
decorationBox = decorationBox
|
||||
)
|
||||
},
|
||||
actionsContent = {
|
||||
if (textFieldValue.text.isNotEmpty()) {
|
||||
SecondaryTextButton(
|
||||
text = "Clear",
|
||||
onClick = { onTextFieldValueChanged(TextFieldValue()) }
|
||||
)
|
||||
}
|
||||
)
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
items(
|
||||
items = items,
|
||||
key = DetailedSong::id,
|
||||
) { song ->
|
||||
SongItem(
|
||||
song = song,
|
||||
thumbnailSizePx = thumbnailSizePx,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
onLongClick = {
|
||||
menuState.display {
|
||||
InHistoryMediaItemMenu(
|
||||
song = song,
|
||||
onDismiss = menuState::hide
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
val mediaItem = song.asMediaItem
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlay(mediaItem)
|
||||
binder?.setupRadio(
|
||||
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
|
||||
)
|
||||
}
|
||||
)
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FloatingActionsContainerWithScrollToTop(lazyListState = lazyListState)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package it.vfsfitvnm.vimusic.ui.screens.search
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
|
@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
|
@ -45,6 +47,7 @@ import it.vfsfitvnm.vimusic.query
|
|||
import it.vfsfitvnm.vimusic.savers.SearchQuerySaver
|
||||
import it.vfsfitvnm.vimusic.savers.listSaver
|
||||
import it.vfsfitvnm.vimusic.savers.resultSaver
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
|
@ -62,6 +65,7 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun OnlineSearch(
|
||||
textFieldValue: TextFieldValue,
|
||||
|
@ -112,139 +116,74 @@ fun OnlineSearch(
|
|||
FocusRequester()
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
item(
|
||||
key = "header",
|
||||
contentType = 0
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
Box {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Header(
|
||||
titleContent = {
|
||||
BasicTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = onTextFieldValueChanged,
|
||||
textStyle = typography.xxl.medium.align(TextAlign.End),
|
||||
singleLine = true,
|
||||
maxLines = 1,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(
|
||||
onSearch = {
|
||||
if (textFieldValue.text.isNotEmpty()) {
|
||||
onSearch(textFieldValue.text)
|
||||
}
|
||||
}
|
||||
),
|
||||
cursorBrush = SolidColor(colorPalette.text),
|
||||
decorationBox = decorationBox,
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
)
|
||||
},
|
||||
actionsContent = {
|
||||
if (playlistId != null) {
|
||||
val isAlbum = playlistId.startsWith("OLAK5uy_")
|
||||
|
||||
SecondaryTextButton(
|
||||
text = "View ${if (isAlbum) "album" else "playlist"}",
|
||||
onClick = { onViewPlaylist(textFieldValue.text) }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
if (textFieldValue.text.isNotEmpty()) {
|
||||
SecondaryTextButton(
|
||||
text = "Clear",
|
||||
onClick = { onTextFieldValueChanged(TextFieldValue()) }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
items(
|
||||
items = history,
|
||||
key = SearchQuery::id
|
||||
) { searchQuery ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onSearch(searchQuery.query) })
|
||||
.fillMaxWidth()
|
||||
.padding(all = 16.dp)
|
||||
item(
|
||||
key = "header",
|
||||
contentType = 0
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.size(20.dp)
|
||||
.paint(
|
||||
painter = timeIconPainter,
|
||||
colorFilter = ColorFilter.tint(colorPalette.textDisabled)
|
||||
)
|
||||
)
|
||||
|
||||
BasicText(
|
||||
text = searchQuery.query,
|
||||
style = typography.s.secondary,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = closeIconPainter,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.textDisabled),
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
indication = rippleIndication,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = {
|
||||
query {
|
||||
Database.delete(searchQuery)
|
||||
Header(
|
||||
titleContent = {
|
||||
BasicTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = onTextFieldValueChanged,
|
||||
textStyle = typography.xxl.medium.align(TextAlign.End),
|
||||
singleLine = true,
|
||||
maxLines = 1,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(
|
||||
onSearch = {
|
||||
if (textFieldValue.text.isNotEmpty()) {
|
||||
onSearch(textFieldValue.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
cursorBrush = SolidColor(colorPalette.text),
|
||||
decorationBox = decorationBox,
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
)
|
||||
.padding(horizontal = 8.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
},
|
||||
actionsContent = {
|
||||
if (playlistId != null) {
|
||||
val isAlbum = playlistId.startsWith("OLAK5uy_")
|
||||
|
||||
Image(
|
||||
painter = arrowForwardIconPainter,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.textDisabled),
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
indication = rippleIndication,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = {
|
||||
onTextFieldValueChanged(
|
||||
TextFieldValue(
|
||||
text = searchQuery.query,
|
||||
selection = TextRange(searchQuery.query.length)
|
||||
)
|
||||
)
|
||||
}
|
||||
SecondaryTextButton(
|
||||
text = "View ${if (isAlbum) "album" else "playlist"}",
|
||||
onClick = { onViewPlaylist(textFieldValue.text) }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
)
|
||||
.rotate(225f)
|
||||
.padding(horizontal = 8.dp)
|
||||
.size(22.dp)
|
||||
|
||||
if (textFieldValue.text.isNotEmpty()) {
|
||||
SecondaryTextButton(
|
||||
text = "Clear",
|
||||
onClick = { onTextFieldValueChanged(TextFieldValue()) }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suggestionsResult?.getOrNull()?.let { suggestions ->
|
||||
items(items = suggestions) { suggestion ->
|
||||
items(
|
||||
items = history,
|
||||
key = SearchQuery::id
|
||||
) { searchQuery ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onSearch(suggestion) })
|
||||
.clickable(onClick = { onSearch(searchQuery.query) })
|
||||
.fillMaxWidth()
|
||||
.padding(all = 16.dp)
|
||||
) {
|
||||
|
@ -252,16 +191,38 @@ fun OnlineSearch(
|
|||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.size(20.dp)
|
||||
.paint(
|
||||
painter = timeIconPainter,
|
||||
colorFilter = ColorFilter.tint(colorPalette.textDisabled)
|
||||
)
|
||||
)
|
||||
|
||||
BasicText(
|
||||
text = suggestion,
|
||||
text = searchQuery.query,
|
||||
style = typography.s.secondary,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = closeIconPainter,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.textDisabled),
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
indication = rippleIndication,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = {
|
||||
query {
|
||||
Database.delete(searchQuery)
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(horizontal = 8.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = arrowForwardIconPainter,
|
||||
contentDescription = null,
|
||||
|
@ -273,8 +234,8 @@ fun OnlineSearch(
|
|||
onClick = {
|
||||
onTextFieldValueChanged(
|
||||
TextFieldValue(
|
||||
text = suggestion,
|
||||
selection = TextRange(suggestion.length)
|
||||
text = searchQuery.query,
|
||||
selection = TextRange(searchQuery.query.length)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -285,21 +246,71 @@ fun OnlineSearch(
|
|||
)
|
||||
}
|
||||
}
|
||||
} ?: suggestionsResult?.exceptionOrNull()?.let {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
BasicText(
|
||||
text = "An error has occurred.",
|
||||
style = typography.s.secondary.center,
|
||||
|
||||
suggestionsResult?.getOrNull()?.let { suggestions ->
|
||||
items(items = suggestions) { suggestion ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
.clickable(onClick = { onSearch(suggestion) })
|
||||
.fillMaxWidth()
|
||||
.padding(all = 16.dp)
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
|
||||
BasicText(
|
||||
text = suggestion,
|
||||
style = typography.s.secondary,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = arrowForwardIconPainter,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.textDisabled),
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
indication = rippleIndication,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = {
|
||||
onTextFieldValueChanged(
|
||||
TextFieldValue(
|
||||
text = suggestion,
|
||||
selection = TextRange(suggestion.length)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
.rotate(225f)
|
||||
.padding(horizontal = 8.dp)
|
||||
.size(22.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
} ?: suggestionsResult?.exceptionOrNull()?.let {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
BasicText(
|
||||
text = "An error has occurred.",
|
||||
style = typography.s.secondary.center,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingActionsContainerWithScrollToTop(lazyListState = lazyListState)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package it.vfsfitvnm.vimusic.ui.screens.searchresult
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
@ -19,6 +20,7 @@ import androidx.compose.ui.unit.dp
|
|||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||
import it.vfsfitvnm.vimusic.savers.nullableSaver
|
||||
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
import it.vfsfitvnm.vimusic.utils.center
|
||||
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||
|
@ -70,51 +72,55 @@ inline fun <T : Innertube.Item> ItemsPage(
|
|||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
item(
|
||||
key = "header",
|
||||
contentType = "header",
|
||||
Box {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
headerContent(null)
|
||||
}
|
||||
|
||||
items(
|
||||
items = itemsPage?.items ?: emptyList(),
|
||||
key = Innertube.Item::key,
|
||||
itemContent = itemContent
|
||||
)
|
||||
|
||||
if (itemsPage != null && itemsPage?.items.isNullOrEmpty()) {
|
||||
item(key = "empty") {
|
||||
BasicText(
|
||||
text = emptyItemsText,
|
||||
style = typography.xs.secondary.center,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 32.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
item(
|
||||
key = "header",
|
||||
contentType = "header",
|
||||
) {
|
||||
headerContent(null)
|
||||
}
|
||||
}
|
||||
|
||||
if (!(itemsPage != null && itemsPage?.continuation == null)) {
|
||||
item(key = "loading") {
|
||||
val isFirstLoad = itemsPage?.items.isNullOrEmpty()
|
||||
ShimmerHost(
|
||||
modifier = Modifier
|
||||
.run {
|
||||
if (isFirstLoad) fillParentMaxSize() else this
|
||||
items(
|
||||
items = itemsPage?.items ?: emptyList(),
|
||||
key = Innertube.Item::key,
|
||||
itemContent = itemContent
|
||||
)
|
||||
|
||||
if (itemsPage != null && itemsPage?.items.isNullOrEmpty()) {
|
||||
item(key = "empty") {
|
||||
BasicText(
|
||||
text = emptyItemsText,
|
||||
style = typography.xs.secondary.center,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 32.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!(itemsPage != null && itemsPage?.continuation == null)) {
|
||||
item(key = "loading") {
|
||||
val isFirstLoad = itemsPage?.items.isNullOrEmpty()
|
||||
ShimmerHost(
|
||||
modifier = Modifier
|
||||
.run {
|
||||
if (isFirstLoad) fillParentMaxSize() else this
|
||||
}
|
||||
) {
|
||||
repeat(if (isFirstLoad) initialPlaceholderCount else continuationPlaceholderCount) {
|
||||
itemPlaceholderContent()
|
||||
}
|
||||
) {
|
||||
repeat(if (isFirstLoad) initialPlaceholderCount else continuationPlaceholderCount) {
|
||||
itemPlaceholderContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingActionsContainerWithScrollToTop(lazyListState = lazyListState)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package it.vfsfitvnm.vimusic.utils
|
||||
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
suspend fun LazyGridState.smoothScrollToTop() {
|
||||
if (firstVisibleItemIndex > layoutInfo.visibleItemsInfo.size) {
|
||||
scrollToItem(layoutInfo.visibleItemsInfo.size)
|
||||
}
|
||||
animateScrollToItem(0)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LazyGridState.isScrollingDownToIsFar(): Pair<Boolean, Boolean> {
|
||||
var previousIndex by remember(this) {
|
||||
mutableStateOf(firstVisibleItemIndex)
|
||||
}
|
||||
|
||||
var previousScrollOffset by remember(this) {
|
||||
mutableStateOf(firstVisibleItemScrollOffset)
|
||||
}
|
||||
|
||||
return remember(this) {
|
||||
derivedStateOf {
|
||||
if (previousIndex != firstVisibleItemIndex) {
|
||||
previousIndex > firstVisibleItemIndex
|
||||
} else {
|
||||
previousScrollOffset >= firstVisibleItemScrollOffset
|
||||
}.also {
|
||||
previousIndex = firstVisibleItemIndex
|
||||
previousScrollOffset = firstVisibleItemScrollOffset
|
||||
} to (firstVisibleItemIndex > layoutInfo.visibleItemsInfo.size)
|
||||
}
|
||||
}.value
|
||||
}
|
|
@ -1,6 +1,12 @@
|
|||
package it.vfsfitvnm.vimusic.utils
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
suspend fun LazyListState.smoothScrollToTop() {
|
||||
if (firstVisibleItemIndex > layoutInfo.visibleItemsInfo.size) {
|
||||
|
@ -8,3 +14,27 @@ suspend fun LazyListState.smoothScrollToTop() {
|
|||
}
|
||||
animateScrollToItem(0)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LazyListState.isScrollingDownToIsFar(): Pair<Boolean, Boolean> {
|
||||
var previousIndex by remember(this) {
|
||||
mutableStateOf(firstVisibleItemIndex)
|
||||
}
|
||||
|
||||
var previousScrollOffset by remember(this) {
|
||||
mutableStateOf(firstVisibleItemScrollOffset)
|
||||
}
|
||||
|
||||
return remember(this) {
|
||||
derivedStateOf {
|
||||
if (previousIndex != firstVisibleItemIndex) {
|
||||
previousIndex > firstVisibleItemIndex
|
||||
} else {
|
||||
previousScrollOffset >= firstVisibleItemScrollOffset
|
||||
}.also {
|
||||
previousIndex = firstVisibleItemIndex
|
||||
previousScrollOffset = firstVisibleItemScrollOffset
|
||||
} to (firstVisibleItemIndex > layoutInfo.visibleItemsInfo.size)
|
||||
}
|
||||
}.value
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package it.vfsfitvnm.vimusic.utils
|
||||
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
@Composable
|
||||
fun ScrollState.isScrollingDown(): Boolean {
|
||||
var previousValue by remember(this) {
|
||||
mutableStateOf(value)
|
||||
}
|
||||
|
||||
return remember(this) {
|
||||
derivedStateOf {
|
||||
(previousValue >= value).also {
|
||||
previousValue = value
|
||||
}
|
||||
}
|
||||
}.value
|
||||
}
|
13
app/src/main/res/drawable/chevron_up.xml
Normal file
13
app/src/main/res/drawable/chevron_up.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="512dp"
|
||||
android:height="512dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:pathData="M112,328l144,-144l144,144"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="48"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
Loading…
Reference in a new issue