Hide floating action button when scrolling down and add scroll to top button to each screen

This commit is contained in:
vfsfitvnm
2022-10-06 16:14:08 +02:00
parent 78c44988d7
commit b30b282628
25 changed files with 902 additions and 573 deletions

View File

@@ -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)
)
}
}
}
}
}

View File

@@ -35,11 +35,11 @@ import it.vfsfitvnm.vimusic.utils.isLandscape
import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.semiBold
@Composable @Composable
fun NavigationRail( inline fun NavigationRail(
topIconButtonId: Int, topIconButtonId: Int,
onTopIconButtonClick: () -> Unit, noinline onTopIconButtonClick: () -> Unit,
tabIndex: Int, tabIndex: Int,
onTabIndexChanged: (Int) -> Unit, crossinline onTabIndexChanged: (Int) -> Unit,
content: @Composable ColumnScope.(@Composable (Int, String, Int) -> Unit) -> Unit, content: @Composable ColumnScope.(@Composable (Int, String, Int) -> Unit) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@@ -144,7 +144,7 @@ fun NavigationRail(
} }
} }
private fun Modifier.vertical(enabled: Boolean = true) = fun Modifier.vertical(enabled: Boolean = true) =
if (enabled) if (enabled)
layout { measurable, constraints -> layout { measurable, constraints ->
val placeable = measurable.measure(constraints) val placeable = measurable.measure(constraints)

View File

@@ -5,8 +5,6 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box 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.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable 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.graphics.ColorFilter
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
@Composable @Composable
fun BoxScope.PrimaryButton( fun PrimaryButton(
onClick: () -> Unit, onClick: () -> Unit,
@DrawableRes iconId: Int, @DrawableRes iconId: Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@@ -30,9 +27,6 @@ fun BoxScope.PrimaryButton(
Box( Box(
modifier = modifier modifier = modifier
.align(Alignment.BottomEnd)
.padding(all = 16.dp)
.padding(LocalPlayerAwarePaddingValues.current)
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.clickable(enabled = isEnabled, onClick = onClick) .clickable(enabled = isEnabled, onClick = onClick)
.background(colorPalette.background2) .background(colorPalette.background2)

View File

@@ -1,6 +1,5 @@
package it.vfsfitvnm.vimusic.ui.components.themed package it.vfsfitvnm.vimusic.ui.components.themed
import android.annotation.SuppressLint
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.AnimatedVisibilityScope
@@ -19,7 +18,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
@SuppressLint("ModifierParameter")
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun Scaffold( fun Scaffold(
@@ -28,8 +26,6 @@ fun Scaffold(
tabIndex: Int, tabIndex: Int,
onTabChanged: (Int) -> Unit, onTabChanged: (Int) -> Unit,
tabColumnContent: @Composable ColumnScope.(@Composable (Int, String, Int) -> Unit) -> Unit, tabColumnContent: @Composable ColumnScope.(@Composable (Int, String, Int) -> Unit) -> Unit,
primaryIconButtonId: Int? = null,
onPrimaryIconButtonClick: () -> Unit = {},
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
content: @Composable AnimatedVisibilityScope.(Int) -> Unit content: @Composable AnimatedVisibilityScope.(Int) -> Unit
) { ) {
@@ -69,14 +65,7 @@ fun Scaffold(
slideIntoContainer(slideDirection, animationSpec) with slideIntoContainer(slideDirection, animationSpec) with
slideOutOfContainer(slideDirection, animationSpec) slideOutOfContainer(slideDirection, animationSpec)
}, },
content = content, content = content
)
}
primaryIconButtonId?.let {
PrimaryButton(
iconId = primaryIconButtonId,
onClick = onPrimaryIconButtonClick
) )
} }
} }

View File

@@ -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)
)
}
}
}

View File

@@ -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)
)
}
}

View File

@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue 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.savers.DetailedSongListSaver
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost 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.LayoutWithAdaptiveThumbnail
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu 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.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.items.SongItem import it.vfsfitvnm.vimusic.ui.items.SongItem
import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
@@ -68,9 +69,12 @@ fun AlbumSongs(
val thumbnailSizeDp = Dimensions.thumbnails.song val thumbnailSizeDp = Dimensions.thumbnails.song
val lazyListState = rememberLazyListState()
LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) { LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
Box { Box {
LazyColumn( LazyColumn(
state = lazyListState,
contentPadding = LocalPlayerAwarePaddingValues.current, contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier modifier = Modifier
.background(colorPalette.background0) .background(colorPalette.background0)
@@ -152,14 +156,16 @@ fun AlbumSongs(
} }
} }
PrimaryButton( FloatingActionsContainerWithScrollToTop(
lazyListState = lazyListState,
iconId = R.drawable.shuffle, iconId = R.drawable.shuffle,
isEnabled = songs.isNotEmpty(),
onClick = { onClick = {
binder?.stopRadio() if (songs.isNotEmpty()) {
binder?.player?.forcePlayFromBeginning( binder?.stopRadio()
songs.shuffled().map(DetailedSong::asMediaItem) binder?.player?.forcePlayFromBeginning(
) songs.shuffled().map(DetailedSong::asMediaItem)
)
}
} }
) )
} }

View File

@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier 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.savers.nullableSaver
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost 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.LayoutWithAdaptiveThumbnail
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu 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.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.items.SongItem import it.vfsfitvnm.vimusic.ui.items.SongItem
import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
@@ -63,9 +64,12 @@ fun ArtistLocalSongs(
val songThumbnailSizeDp = Dimensions.thumbnails.song val songThumbnailSizeDp = Dimensions.thumbnails.song
val songThumbnailSizePx = songThumbnailSizeDp.px val songThumbnailSizePx = songThumbnailSizeDp.px
val lazyListState = rememberLazyListState()
LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) { LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
Box { Box {
LazyColumn( LazyColumn(
state = lazyListState,
contentPadding = LocalPlayerAwarePaddingValues.current, contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier modifier = Modifier
.background(colorPalette.background0) .background(colorPalette.background0)
@@ -128,14 +132,18 @@ fun ArtistLocalSongs(
} }
} }
PrimaryButton( FloatingActionsContainerWithScrollToTop(
lazyListState = lazyListState,
iconId = R.drawable.shuffle, iconId = R.drawable.shuffle,
isEnabled = !songs.isNullOrEmpty(),
onClick = { onClick = {
binder?.stopRadio() songs?.let { songs ->
binder?.player?.forcePlayFromBeginning( if (songs.isNotEmpty()) {
songs!!.shuffled().map(DetailedSong::asMediaItem) binder?.stopRadio()
) binder?.player?.forcePlayFromBeginning(
songs.shuffled().map(DetailedSong::asMediaItem)
)
}
}
} }
) )
} }

View File

@@ -26,9 +26,9 @@ import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost 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.LayoutWithAdaptiveThumbnail
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu 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.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.items.AlbumItem import it.vfsfitvnm.vimusic.ui.items.AlbumItem
@@ -70,6 +70,8 @@ fun ArtistOverview(
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.padding(top = 24.dp, bottom = 8.dp) .padding(top = 24.dp, bottom = 8.dp)
val scrollState = rememberScrollState()
LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) { LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
Box { Box {
Column( Column(
@@ -77,7 +79,7 @@ fun ArtistOverview(
modifier = Modifier modifier = Modifier
.background(colorPalette.background0) .background(colorPalette.background0)
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(scrollState)
.padding(LocalPlayerAwarePaddingValues.current) .padding(LocalPlayerAwarePaddingValues.current)
) { ) {
headerContent { headerContent {
@@ -258,7 +260,8 @@ fun ArtistOverview(
} }
youtubeArtistPage?.shuffleEndpoint?.let { shuffleEndpoint -> youtubeArtistPage?.shuffleEndpoint?.let { shuffleEndpoint ->
PrimaryButton( FloatingActionsContainerWithScrollToTop(
scrollState = scrollState,
iconId = R.drawable.shuffle, iconId = R.drawable.shuffle,
onClick = { onClick = {
binder?.stopRadio() binder?.stopRadio()

View File

@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier 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.models.DetailedSong
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState 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.Header
import it.vfsfitvnm.vimusic.ui.components.themed.InFavoritesMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.InFavoritesMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
@@ -70,8 +72,11 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
val thumbnailSizeDp = Dimensions.thumbnails.song val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSize = thumbnailSizeDp.px val thumbnailSize = thumbnailSizeDp.px
val lazyListState = rememberLazyListState()
Box { Box {
LazyColumn( LazyColumn(
state = lazyListState,
contentPadding = LocalPlayerAwarePaddingValues.current, contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier modifier = Modifier
.background(colorPalette.background0) .background(colorPalette.background0)
@@ -120,6 +125,7 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
song = song, song = song,
onDismiss = menuState::hide onDismiss = menuState::hide
) )
BuiltInPlaylist.Offline -> InHistoryMediaItemMenu( BuiltInPlaylist.Offline -> InHistoryMediaItemMenu(
song = song, song = song,
onDismiss = menuState::hide onDismiss = menuState::hide
@@ -129,7 +135,10 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
}, },
onClick = { onClick = {
binder?.stopRadio() binder?.stopRadio()
binder?.player?.forcePlayAtIndex(songs.map(DetailedSong::asMediaItem), index) binder?.player?.forcePlayAtIndex(
songs.map(DetailedSong::asMediaItem),
index
)
} }
) )
.animateItemPlacement() .animateItemPlacement()
@@ -137,14 +146,16 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
} }
} }
PrimaryButton( FloatingActionsContainerWithScrollToTop(
lazyListState = lazyListState,
iconId = R.drawable.shuffle, iconId = R.drawable.shuffle,
isEnabled = songs.isNotEmpty(),
onClick = { onClick = {
binder?.stopRadio() if (songs.isNotEmpty()) {
binder?.player?.forcePlayFromBeginning( binder?.stopRadio()
songs.shuffled().map(DetailedSong::asMediaItem) binder?.player?.forcePlayFromBeginning(
) songs.shuffled().map(DetailedSong::asMediaItem)
)
}
} }
) )
} }

View File

@@ -7,11 +7,13 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue 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.enums.SortOrder
import it.vfsfitvnm.vimusic.models.Album import it.vfsfitvnm.vimusic.models.Album
import it.vfsfitvnm.vimusic.savers.AlbumListSaver 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.Header
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
import it.vfsfitvnm.vimusic.ui.items.AlbumItem import it.vfsfitvnm.vimusic.ui.items.AlbumItem
@@ -42,7 +45,8 @@ import kotlinx.coroutines.flow.flowOn
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun HomeAlbums( fun HomeAlbums(
onAlbumClick: (Album) -> Unit onAlbumClick: (Album) -> Unit,
onSearchClick: () -> Unit,
) { ) {
val (colorPalette) = LocalAppearance.current val (colorPalette) = LocalAppearance.current
@@ -68,62 +72,73 @@ fun HomeAlbums(
animationSpec = tween(durationMillis = 400, easing = LinearEasing) animationSpec = tween(durationMillis = 400, easing = LinearEasing)
) )
LazyColumn( val lazyListState = rememberLazyListState()
contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier Box {
.background(colorPalette.background0) LazyColumn(
.fillMaxSize() state = lazyListState,
) { contentPadding = LocalPlayerAwarePaddingValues.current,
item( modifier = Modifier
key = "header", .background(colorPalette.background0)
contentType = 0 .fillMaxSize()
) { ) {
Header(title = "Albums") { item(
HeaderIconButton( key = "header",
icon = R.drawable.calendar, contentType = 0
color = if (sortBy == AlbumSortBy.Year) colorPalette.text else colorPalette.textDisabled, ) {
onClick = { sortBy = AlbumSortBy.Year } Header(title = "Albums") {
) HeaderIconButton(
icon = R.drawable.calendar,
color = if (sortBy == AlbumSortBy.Year) colorPalette.text else colorPalette.textDisabled,
onClick = { sortBy = AlbumSortBy.Year }
)
HeaderIconButton( HeaderIconButton(
icon = R.drawable.text, icon = R.drawable.text,
color = if (sortBy == AlbumSortBy.Title) colorPalette.text else colorPalette.textDisabled, color = if (sortBy == AlbumSortBy.Title) colorPalette.text else colorPalette.textDisabled,
onClick = { sortBy = AlbumSortBy.Title } onClick = { sortBy = AlbumSortBy.Title }
) )
HeaderIconButton( HeaderIconButton(
icon = R.drawable.time, icon = R.drawable.time,
color = if (sortBy == AlbumSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled, color = if (sortBy == AlbumSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled,
onClick = { sortBy = AlbumSortBy.DateAdded } 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 modifier = Modifier
.width(2.dp) .clickable(onClick = { onAlbumClick(album) })
) .animateItemPlacement()
HeaderIconButton(
icon = R.drawable.arrow_up,
color = colorPalette.text,
onClick = { sortOrder = !sortOrder },
modifier = Modifier
.graphicsLayer { rotationZ = sortOrderIconRotation }
) )
} }
} }
items( FloatingActionsContainerWithScrollToTop(
items = items, lazyListState = lazyListState,
key = Album::id iconId = R.drawable.search,
) { album -> onClick = onSearchClick
AlbumItem( )
album = album,
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
modifier = Modifier
.clickable(onClick = { onAlbumClick(album) })
.animateItemPlacement()
)
}
} }
} }

View File

@@ -8,6 +8,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width 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.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue 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.enums.SortOrder
import it.vfsfitvnm.vimusic.models.Artist import it.vfsfitvnm.vimusic.models.Artist
import it.vfsfitvnm.vimusic.savers.ArtistListSaver 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.Header
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
import it.vfsfitvnm.vimusic.ui.items.ArtistItem import it.vfsfitvnm.vimusic.ui.items.ArtistItem
@@ -46,7 +49,8 @@ import kotlinx.coroutines.flow.flowOn
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun HomeArtistList( fun HomeArtistList(
onArtistClick: (Artist) -> Unit onArtistClick: (Artist) -> Unit,
onSearchClick: () -> Unit,
) { ) {
val (colorPalette) = LocalAppearance.current val (colorPalette) = LocalAppearance.current
@@ -72,61 +76,72 @@ fun HomeArtistList(
animationSpec = tween(durationMillis = 400, easing = LinearEasing) animationSpec = tween(durationMillis = 400, easing = LinearEasing)
) )
LazyVerticalGrid( val lazyGridState = rememberLazyGridState()
columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
contentPadding = LocalPlayerAwarePaddingValues.current, Box {
verticalArrangement = Arrangement.spacedBy(Dimensions.itemsVerticalPadding * 2), LazyVerticalGrid(
horizontalArrangement = Arrangement.spacedBy( state = lazyGridState,
space = Dimensions.itemsVerticalPadding * 2, columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
alignment = Alignment.CenterHorizontally contentPadding = LocalPlayerAwarePaddingValues.current,
), verticalArrangement = Arrangement.spacedBy(Dimensions.itemsVerticalPadding * 2),
modifier = Modifier horizontalArrangement = Arrangement.spacedBy(
.background(colorPalette.background0) space = Dimensions.itemsVerticalPadding * 2,
.fillMaxSize() alignment = Alignment.CenterHorizontally
) { ),
item( modifier = Modifier
key = "header", .background(colorPalette.background0)
contentType = 0, .fillMaxSize()
span = { GridItemSpan(maxLineSpan) }
) { ) {
Header(title = "Artists") { item(
HeaderIconButton( key = "header",
icon = R.drawable.text, contentType = 0,
color = if (sortBy == ArtistSortBy.Name) colorPalette.text else colorPalette.textDisabled, span = { GridItemSpan(maxLineSpan) }
onClick = { sortBy = ArtistSortBy.Name } ) {
) Header(title = "Artists") {
HeaderIconButton(
icon = R.drawable.text,
color = if (sortBy == ArtistSortBy.Name) colorPalette.text else colorPalette.textDisabled,
onClick = { sortBy = ArtistSortBy.Name }
)
HeaderIconButton( HeaderIconButton(
icon = R.drawable.time, icon = R.drawable.time,
color = if (sortBy == ArtistSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled, color = if (sortBy == ArtistSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled,
onClick = { sortBy = ArtistSortBy.DateAdded } 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 modifier = Modifier
.width(2.dp) .clickable(onClick = { onArtistClick(artist) })
) .animateItemPlacement()
HeaderIconButton(
icon = R.drawable.arrow_up,
color = colorPalette.text,
onClick = { sortOrder = !sortOrder },
modifier = Modifier
.graphicsLayer { rotationZ = sortOrderIconRotation }
) )
} }
} }
items(items = items, key = Artist::id) { artist -> FloatingActionsContainerWithScrollToTop(
ArtistItem( lazyGridState = lazyGridState,
artist = artist, iconId = R.drawable.search,
thumbnailSizePx = thumbnailSizePx, onClick = onSearchClick
thumbnailSizeDp = thumbnailSizeDp, )
alternative = true,
modifier = Modifier
.clickable(onClick = { onArtistClick(artist) })
.animateItemPlacement()
)
}
} }
} }

View File

@@ -1,5 +1,6 @@
package it.vfsfitvnm.vimusic.ui.screens.home package it.vfsfitvnm.vimusic.ui.screens.home
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
@@ -7,6 +8,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width 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.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf 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.models.Playlist
import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.savers.PlaylistPreviewListSaver 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.Header
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton 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.Dispatchers
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@ExperimentalAnimationApi
@ExperimentalFoundationApi @ExperimentalFoundationApi
@Composable @Composable
fun HomePlaylists( fun HomePlaylists(
onBuiltInPlaylist: (BuiltInPlaylist) -> Unit, onBuiltInPlaylist: (BuiltInPlaylist) -> Unit,
onPlaylistClick: (Playlist) -> Unit, onPlaylistClick: (Playlist) -> Unit,
onSearchClick: () -> Unit,
) { ) {
val (colorPalette) = LocalAppearance.current val (colorPalette) = LocalAppearance.current
@@ -95,101 +101,112 @@ fun HomePlaylists(
val thumbnailSizeDp = 108.dp val thumbnailSizeDp = 108.dp
val thumbnailSizePx = thumbnailSizeDp.px val thumbnailSizePx = thumbnailSizeDp.px
LazyVerticalGrid( val lazyGridState = rememberLazyGridState()
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( 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 modifier = Modifier
.weight(1f) .clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Favorites) })
.animateItemPlacement()
) )
}
HeaderIconButton( item(key = "offline") {
icon = R.drawable.medical, PlaylistItem(
color = if (sortBy == PlaylistSortBy.SongCount) colorPalette.text else colorPalette.textDisabled, icon = R.drawable.airplane,
onClick = { sortBy = PlaylistSortBy.SongCount } colorTint = colorPalette.blue,
) name = "Offline",
songCount = null,
HeaderIconButton( thumbnailSizeDp = thumbnailSizeDp,
icon = R.drawable.text, alternative = true,
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 modifier = Modifier
.width(2.dp) .clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Offline) })
.animateItemPlacement()
) )
}
HeaderIconButton( items(items = items, key = { it.playlist.id }) { playlistPreview ->
icon = R.drawable.arrow_up, PlaylistItem(
color = colorPalette.text, playlist = playlistPreview,
onClick = { sortOrder = !sortOrder }, thumbnailSizeDp = thumbnailSizeDp,
thumbnailSizePx = thumbnailSizePx,
alternative = true,
modifier = Modifier modifier = Modifier
.graphicsLayer { rotationZ = sortOrderIconRotation } .clickable(onClick = { onPlaylistClick(playlistPreview.playlist) })
.animateItemPlacement()
) )
} }
} }
item(key = "favorites") { FloatingActionsContainerWithScrollToTop(
PlaylistItem( lazyGridState = lazyGridState,
icon = R.drawable.heart, iconId = R.drawable.search,
colorTint = colorPalette.red, onClick = onSearchClick
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()
)
}
} }
} }

View File

@@ -114,8 +114,6 @@ fun HomeScreen(onPlaylistUrl: (String) -> Unit) {
Item(3, "Artists", R.drawable.person) Item(3, "Artists", R.drawable.person)
Item(4, "Albums", R.drawable.disc) Item(4, "Albums", R.drawable.disc)
}, },
primaryIconButtonId = R.drawable.search,
onPrimaryIconButtonClick = { searchRoute("") }
) { currentTabIndex -> ) { currentTabIndex ->
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
when (currentTabIndex) { when (currentTabIndex) {
@@ -123,14 +121,24 @@ fun HomeScreen(onPlaylistUrl: (String) -> Unit) {
onAlbumClick = { albumRoute(it) }, onAlbumClick = { albumRoute(it) },
onArtistClick = { artistRoute(it) }, onArtistClick = { artistRoute(it) },
onPlaylistClick = { playlistRoute(it) }, onPlaylistClick = { playlistRoute(it) },
onSearchClick = { searchRoute("") }
)
1 -> HomeSongs(
onSearchClick = { searchRoute("") }
) )
1 -> HomeSongs()
2 -> HomePlaylists( 2 -> HomePlaylists(
onBuiltInPlaylist = { builtInPlaylistRoute(it) }, 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) })
} }
} }
} }

View File

@@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn 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.models.DetailedSong
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState 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.Header
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu 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.items.SongItem
import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
@@ -62,7 +61,9 @@ import kotlinx.coroutines.flow.flowOn
@ExperimentalFoundationApi @ExperimentalFoundationApi
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun HomeSongs() { fun HomeSongs(
onSearchClick: () -> Unit
) {
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
val menuState = LocalMenuState.current val menuState = LocalMenuState.current
@@ -187,11 +188,10 @@ fun HomeSongs() {
} }
} }
ScrollToTop( FloatingActionsContainerWithScrollToTop(
lazyListState = lazyListState, lazyListState = lazyListState,
modifier = Modifier iconId = R.drawable.search,
.offset(x = Dimensions.navigationRailIconOffset - Dimensions.navigationRailWidth) onClick = onSearchClick
.align(Alignment.BottomStart)
) )
} }
} }

View File

@@ -43,6 +43,7 @@ import it.vfsfitvnm.vimusic.savers.nullableSaver
import it.vfsfitvnm.vimusic.savers.resultSaver import it.vfsfitvnm.vimusic.savers.resultSaver
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost 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.Header
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
@@ -81,6 +82,7 @@ fun QuickPicks(
onAlbumClick: (String) -> Unit, onAlbumClick: (String) -> Unit,
onArtistClick: (String) -> Unit, onArtistClick: (String) -> Unit,
onPlaylistClick: (String) -> Unit, onPlaylistClick: (String) -> Unit,
onSearchClick: () -> Unit,
) { ) {
val (colorPalette, typography) = LocalAppearance.current val (colorPalette, typography) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
@@ -129,6 +131,8 @@ fun QuickPicks(
) )
} }
val scrollState = rememberScrollState()
BoxWithConstraints { BoxWithConstraints {
val itemInHorizontalGridWidth = maxWidth * quickPicksLazyGridItemWidthFactor val itemInHorizontalGridWidth = maxWidth * quickPicksLazyGridItemWidthFactor
@@ -136,7 +140,7 @@ fun QuickPicks(
modifier = Modifier modifier = Modifier
.background(colorPalette.background0) .background(colorPalette.background0)
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(scrollState)
.padding(LocalPlayerAwarePaddingValues.current) .padding(LocalPlayerAwarePaddingValues.current)
) { ) {
Header(title = "Quick picks") Header(title = "Quick picks")
@@ -345,5 +349,11 @@ fun QuickPicks(
} }
} }
} }
FloatingActionsContainerWithScrollToTop(
scrollState = scrollState,
iconId = R.drawable.search,
onClick = onSearchClick
)
} }
} }

View File

@@ -35,13 +35,13 @@ import it.vfsfitvnm.vimusic.savers.nullableSaver
import it.vfsfitvnm.vimusic.transaction import it.vfsfitvnm.vimusic.transaction
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog 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.Header
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
import it.vfsfitvnm.vimusic.ui.components.themed.IconButton import it.vfsfitvnm.vimusic.ui.components.themed.IconButton
import it.vfsfitvnm.vimusic.ui.components.themed.InPlaylistMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.InPlaylistMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.Menu import it.vfsfitvnm.vimusic.ui.components.themed.Menu
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry 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.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
import it.vfsfitvnm.vimusic.ui.items.SongItem import it.vfsfitvnm.vimusic.ui.items.SongItem
@@ -274,17 +274,18 @@ fun LocalPlaylistSongs(
} }
} }
PrimaryButton( FloatingActionsContainerWithScrollToTop(
lazyListState = lazyListState,
iconId = R.drawable.shuffle, iconId = R.drawable.shuffle,
isEnabled = playlistWithSongs?.songs?.isNotEmpty() == true,
onClick = { onClick = {
playlistWithSongs?.songs playlistWithSongs?.songs?.let { songs ->
?.shuffled() if (songs.isNotEmpty()) {
?.map(DetailedSong::asMediaItem)
?.let { mediaItems ->
binder?.stopRadio() binder?.stopRadio()
binder?.player?.forcePlayFromBeginning(mediaItems) binder?.player?.forcePlayFromBeginning(
songs.shuffled().map(DetailedSong::asMediaItem)
)
} }
}
} }
) )
} }

View File

@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.autoSaver 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.transaction
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost 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.Header
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
import it.vfsfitvnm.vimusic.ui.components.themed.LayoutWithAdaptiveThumbnail import it.vfsfitvnm.vimusic.ui.components.themed.LayoutWithAdaptiveThumbnail
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu 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.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.components.themed.adaptiveThumbnailContent import it.vfsfitvnm.vimusic.ui.components.themed.adaptiveThumbnailContent
import it.vfsfitvnm.vimusic.ui.items.SongItem import it.vfsfitvnm.vimusic.ui.items.SongItem
@@ -62,7 +63,7 @@ import kotlinx.coroutines.withContext
fun PlaylistSongList( fun PlaylistSongList(
browseId: String, browseId: String,
) { ) {
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current val (colorPalette) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
val context = LocalContext.current val context = LocalContext.current
val menuState = LocalMenuState.current val menuState = LocalMenuState.current
@@ -162,9 +163,12 @@ fun PlaylistSongList(
val thumbnailContent = adaptiveThumbnailContent(playlistPage == null, playlistPage?.thumbnail?.url) val thumbnailContent = adaptiveThumbnailContent(playlistPage == null, playlistPage?.thumbnail?.url)
val lazyListState = rememberLazyListState()
LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) { LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
Box { Box {
LazyColumn( LazyColumn(
state = lazyListState,
contentPadding = LocalPlayerAwarePaddingValues.current, contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier modifier = Modifier
.background(colorPalette.background0) .background(colorPalette.background0)
@@ -219,13 +223,17 @@ fun PlaylistSongList(
} }
} }
PrimaryButton( FloatingActionsContainerWithScrollToTop(
lazyListState = lazyListState,
iconId = R.drawable.shuffle, iconId = R.drawable.shuffle,
isEnabled = playlistPage?.songsPage?.items?.isNotEmpty() == true,
onClick = { onClick = {
playlistPage?.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems -> playlistPage?.songsPage?.items?.let { songs ->
binder?.stopRadio() if (songs.isNotEmpty()) {
binder?.player?.forcePlayFromBeginning(mediaItems.shuffled()) binder?.stopRadio()
binder?.player?.forcePlayFromBeginning(
songs.shuffled().map(Innertube.SongItem::asMediaItem)
)
}
} }
} }
) )

View File

@@ -3,9 +3,11 @@ package it.vfsfitvnm.vimusic.ui.screens.search
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -21,6 +23,7 @@ import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState 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.Header
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
@@ -65,68 +68,75 @@ fun LocalSongSearch(
val thumbnailSizeDp = Dimensions.thumbnails.song val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSizePx = thumbnailSizeDp.px val thumbnailSizePx = thumbnailSizeDp.px
LazyColumn( val lazyListState = rememberLazyListState()
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()) }
)
}
}
)
}
items( Box {
items = items, LazyColumn(
key = DetailedSong::id, state = lazyListState,
) { song -> contentPadding = LocalPlayerAwarePaddingValues.current,
SongItem( modifier = Modifier
song = song, .fillMaxSize()
thumbnailSizePx = thumbnailSizePx, ) {
thumbnailSizeDp = thumbnailSizeDp, item(
modifier = Modifier key = "header",
.combinedClickable( contentType = 0
onLongClick = { ) {
menuState.display { Header(
InHistoryMediaItemMenu( titleContent = {
song = song, BasicTextField(
onDismiss = menuState::hide value = textFieldValue,
) onValueChange = onTextFieldValueChanged,
} textStyle = typography.xxl.medium.align(TextAlign.End),
}, singleLine = true,
onClick = { maxLines = 1,
val mediaItem = song.asMediaItem keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
binder?.stopRadio() cursorBrush = SolidColor(colorPalette.text),
binder?.player?.forcePlay(mediaItem) decorationBox = decorationBox
binder?.setupRadio( )
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId) },
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)
} }
} }

View File

@@ -1,5 +1,6 @@
package it.vfsfitvnm.vimusic.ui.screens.search package it.vfsfitvnm.vimusic.ui.screens.search
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource 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.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions 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.SearchQuerySaver
import it.vfsfitvnm.vimusic.savers.listSaver import it.vfsfitvnm.vimusic.savers.listSaver
import it.vfsfitvnm.vimusic.savers.resultSaver 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.Header
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
@@ -62,6 +65,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@ExperimentalAnimationApi
@Composable @Composable
fun OnlineSearch( fun OnlineSearch(
textFieldValue: TextFieldValue, textFieldValue: TextFieldValue,
@@ -112,139 +116,74 @@ fun OnlineSearch(
FocusRequester() FocusRequester()
} }
LazyColumn( val lazyListState = rememberLazyListState()
contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier Box {
.fillMaxSize() LazyColumn(
) { state = lazyListState,
item( contentPadding = LocalPlayerAwarePaddingValues.current,
key = "header", modifier = Modifier
contentType = 0 .fillMaxSize()
) { ) {
Header( item(
titleContent = { key = "header",
BasicTextField( contentType = 0
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)
) { ) {
Spacer( Header(
modifier = Modifier titleContent = {
.padding(horizontal = 8.dp) BasicTextField(
.size(20.dp) value = textFieldValue,
.paint( onValueChange = onTextFieldValueChanged,
painter = timeIconPainter, textStyle = typography.xxl.medium.align(TextAlign.End),
colorFilter = ColorFilter.tint(colorPalette.textDisabled) singleLine = true,
) maxLines = 1,
) keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
BasicText( onSearch = {
text = searchQuery.query, if (textFieldValue.text.isNotEmpty()) {
style = typography.s.secondary, onSearch(textFieldValue.text)
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)
} }
} ),
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( SecondaryTextButton(
painter = arrowForwardIconPainter, text = "View ${if (isAlbum) "album" else "playlist"}",
contentDescription = null, onClick = { onViewPlaylist(textFieldValue.text) }
colorFilter = ColorFilter.tint(colorPalette.textDisabled), )
modifier = Modifier }
.clickable(
indication = rippleIndication, Spacer(
interactionSource = remember { MutableInteractionSource() }, modifier = Modifier
onClick = { .weight(1f)
onTextFieldValueChanged(
TextFieldValue(
text = searchQuery.query,
selection = TextRange(searchQuery.query.length)
)
)
}
) )
.rotate(225f)
.padding(horizontal = 8.dp) if (textFieldValue.text.isNotEmpty()) {
.size(22.dp) SecondaryTextButton(
text = "Clear",
onClick = { onTextFieldValueChanged(TextFieldValue()) }
)
}
}
) )
} }
}
suggestionsResult?.getOrNull()?.let { suggestions -> items(
items(items = suggestions) { suggestion -> items = history,
key = SearchQuery::id
) { searchQuery ->
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.clickable(onClick = { onSearch(suggestion) }) .clickable(onClick = { onSearch(searchQuery.query) })
.fillMaxWidth() .fillMaxWidth()
.padding(all = 16.dp) .padding(all = 16.dp)
) { ) {
@@ -252,16 +191,38 @@ fun OnlineSearch(
modifier = Modifier modifier = Modifier
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.size(20.dp) .size(20.dp)
.paint(
painter = timeIconPainter,
colorFilter = ColorFilter.tint(colorPalette.textDisabled)
)
) )
BasicText( BasicText(
text = suggestion, text = searchQuery.query,
style = typography.s.secondary, style = typography.s.secondary,
modifier = Modifier modifier = Modifier
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.weight(1f) .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( Image(
painter = arrowForwardIconPainter, painter = arrowForwardIconPainter,
contentDescription = null, contentDescription = null,
@@ -273,8 +234,8 @@ fun OnlineSearch(
onClick = { onClick = {
onTextFieldValueChanged( onTextFieldValueChanged(
TextFieldValue( TextFieldValue(
text = suggestion, text = searchQuery.query,
selection = TextRange(suggestion.length) selection = TextRange(searchQuery.query.length)
) )
) )
} }
@@ -285,21 +246,71 @@ fun OnlineSearch(
) )
} }
} }
} ?: suggestionsResult?.exceptionOrNull()?.let {
item { suggestionsResult?.getOrNull()?.let { suggestions ->
Box( items(items = suggestions) { suggestion ->
modifier = Modifier Row(
.fillMaxSize() verticalAlignment = Alignment.CenterVertically,
) {
BasicText(
text = "An error has occurred.",
style = typography.s.secondary.center,
modifier = Modifier 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) { LaunchedEffect(Unit) {

View File

@@ -1,6 +1,7 @@
package it.vfsfitvnm.vimusic.ui.screens.searchresult package it.vfsfitvnm.vimusic.ui.screens.searchresult
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.savers.nullableSaver import it.vfsfitvnm.vimusic.savers.nullableSaver
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost 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.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.center import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.produceSaveableState
@@ -70,51 +72,55 @@ inline fun <T : Innertube.Item> ItemsPage(
} }
} }
LazyColumn( Box {
state = lazyListState, LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current, state = lazyListState,
modifier = modifier contentPadding = LocalPlayerAwarePaddingValues.current,
.fillMaxSize() modifier = modifier
) { .fillMaxSize()
item(
key = "header",
contentType = "header",
) { ) {
headerContent(null) item(
} key = "header",
contentType = "header",
items( ) {
items = itemsPage?.items ?: emptyList(), headerContent(null)
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)) { items(
item(key = "loading") { items = itemsPage?.items ?: emptyList(),
val isFirstLoad = itemsPage?.items.isNullOrEmpty() key = Innertube.Item::key,
ShimmerHost( itemContent = itemContent
modifier = Modifier )
.run {
if (isFirstLoad) fillParentMaxSize() else this 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)
} }
} }

View File

@@ -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
}

View File

@@ -1,6 +1,12 @@
package it.vfsfitvnm.vimusic.utils package it.vfsfitvnm.vimusic.utils
import androidx.compose.foundation.lazy.LazyListState 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() { suspend fun LazyListState.smoothScrollToTop() {
if (firstVisibleItemIndex > layoutInfo.visibleItemsInfo.size) { if (firstVisibleItemIndex > layoutInfo.visibleItemsInfo.size) {
@@ -8,3 +14,27 @@ suspend fun LazyListState.smoothScrollToTop() {
} }
animateScrollToItem(0) 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
}

View File

@@ -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
}

View 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>