From b30b282628b872019626bd41ac05227d65f139ad Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Thu, 6 Oct 2022 16:14:08 +0200 Subject: [PATCH] Hide floating action button when scrolling down and add scroll to top button to each screen --- .../themed/FloatingActionsContainer.kt | 149 ++++++++++ .../ui/components/themed/NavigationRail.kt | 8 +- .../ui/components/themed/PrimaryButton.kt | 8 +- .../vimusic/ui/components/themed/Scaffold.kt | 13 +- .../ui/components/themed/ScrollToTop.kt | 83 ------ .../ui/components/themed/SecondaryButton.kt | 44 +++ .../vimusic/ui/screens/album/AlbumSongs.kt | 20 +- .../ui/screens/artist/ArtistLocalSongs.kt | 22 +- .../ui/screens/artist/ArtistOverview.kt | 9 +- .../builtinplaylist/BuiltInPlaylistSongs.kt | 25 +- .../vimusic/ui/screens/home/HomeAlbums.kt | 113 ++++--- .../vimusic/ui/screens/home/HomeArtists.kt | 113 ++++--- .../vimusic/ui/screens/home/HomePlaylists.kt | 185 ++++++------ .../vimusic/ui/screens/home/HomeScreen.kt | 20 +- .../vimusic/ui/screens/home/HomeSongs.kt | 14 +- .../vimusic/ui/screens/home/QuickPicks.kt | 12 +- .../localplaylist/LocalPlaylistSongs.kt | 17 +- .../ui/screens/playlist/PlaylistSongList.kt | 22 +- .../ui/screens/search/LocalSongSearch.kt | 128 ++++---- .../vimusic/ui/screens/search/OnlineSearch.kt | 281 +++++++++--------- .../ui/screens/searchresult/ItemsPage.kt | 82 ++--- .../vfsfitvnm/vimusic/utils/LazyGridState.kt | 40 +++ .../vfsfitvnm/vimusic/utils/LazyListState.kt | 30 ++ .../it/vfsfitvnm/vimusic/utils/ScrollState.kt | 24 ++ app/src/main/res/drawable/chevron_up.xml | 13 + 25 files changed, 902 insertions(+), 573 deletions(-) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/FloatingActionsContainer.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/ScrollToTop.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/SecondaryButton.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/LazyGridState.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ScrollState.kt create mode 100644 app/src/main/res/drawable/chevron_up.xml diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/FloatingActionsContainer.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/FloatingActionsContainer.kt new file mode 100644 index 0000000..f76ae84 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/FloatingActionsContainer.kt @@ -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>, + 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) + ) + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt index e6bfaf3..0b974a4 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt @@ -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) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/PrimaryButton.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/PrimaryButton.kt index 28524e3..6a832cf 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/PrimaryButton.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/PrimaryButton.kt @@ -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) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt index 72a2b2d..ecc0d91 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt @@ -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 ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/ScrollToTop.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/ScrollToTop.kt deleted file mode 100644 index 5735aaa..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/ScrollToTop.kt +++ /dev/null @@ -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) - ) - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/SecondaryButton.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/SecondaryButton.kt new file mode 100644 index 0000000..9ceffc6 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/SecondaryButton.kt @@ -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) + ) + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt index 4828f06..d546a71 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt @@ -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) + ) + } } ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt index 39dbb6f..ba0ccb9 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt @@ -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) + ) + } + } } ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt index d14e058..9f28d88 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt @@ -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() diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistSongs.kt index 51441fe..de8ca30 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistSongs.kt @@ -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) + ) + } } ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbums.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbums.kt index a3fc80f..9cee34b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbums.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbums.kt @@ -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 + ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtists.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtists.kt index 79de16e..dd66630 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtists.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtists.kt @@ -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 + ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylists.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylists.kt index 68584be..4ec37f4 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylists.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylists.kt @@ -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 + ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt index 2263c30..1e677ab 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt @@ -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) }) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongs.kt index bac8c4c..3586e77 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongs.kt @@ -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 ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt index 0141d2a..372dac3 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt @@ -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 + ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongs.kt index 7b5fa09..cc623e9 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongs.kt @@ -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) + ) } + } } ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt index 54addbc..d9688e8 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt @@ -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) + ) + } } } ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt index f95a105..09d144f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt @@ -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) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt index 4df29b4..bec014a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt @@ -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) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemsPage.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemsPage.kt index 72ff325..2ebf7a5 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemsPage.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemsPage.kt @@ -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 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) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/LazyGridState.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/LazyGridState.kt new file mode 100644 index 0000000..59fa682 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/LazyGridState.kt @@ -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 { + 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 +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/LazyListState.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/LazyListState.kt index ccd3aec..9163459 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/LazyListState.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/LazyListState.kt @@ -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 { + 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 +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ScrollState.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ScrollState.kt new file mode 100644 index 0000000..cd3bdeb --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ScrollState.kt @@ -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 +} diff --git a/app/src/main/res/drawable/chevron_up.xml b/app/src/main/res/drawable/chevron_up.xml new file mode 100644 index 0000000..257133c --- /dev/null +++ b/app/src/main/res/drawable/chevron_up.xml @@ -0,0 +1,13 @@ + + +