From 0ac516b39be5c4e306a4d770bdb9c4b8d97572dd Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Mon, 3 Oct 2022 12:52:24 +0200 Subject: [PATCH] Tweak code --- .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 3 + .../it/vfsfitvnm/vimusic/models/Artist.kt | 8 +- .../vimusic/savers/InnertubeItemsPageSaver.kt | 31 +- .../it/vfsfitvnm/vimusic/savers/Savers.kt | 15 + .../vimusic/ui/components/themed/Scaffold.kt | 13 - .../ui/components/themed/ShimmerHost.kt | 31 ++ .../ui/screens/artist/ArtistContent.kt | 152 ------- .../ui/screens/artist/ArtistLocalSongsList.kt | 219 +++------- .../ui/screens/artist/ArtistOverview.kt | 401 +++++++----------- .../vimusic/ui/screens/artist/ArtistScreen.kt | 348 ++++++++------- .../ui/screens/searchresult/ItemsPage.kt | 90 ++++ .../ui/screens/searchresult/SearchResult.kt | 172 -------- .../searchresult/SearchResultScreen.kt | 149 +++++-- .../vimusic/ui/views/InnertubeItems.kt | 14 +- .../vimusic/utils/ProduceSaveableState.kt | 94 ---- .../it/vfsfitvnm/youtubemusic/utils/Utils.kt | 44 +- 16 files changed, 699 insertions(+), 1085 deletions(-) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/ShimmerHost.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistContent.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemsPage.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index 729bd2f..19f90e4 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -152,6 +152,9 @@ interface Database { @Query("SELECT * FROM Artist WHERE id = :id") fun artist(id: String): Flow + @Query("SELECT timestamp FROM Artist WHERE id = :id") + fun artistTimestamp(id: String): Long? + @Query("SELECT * FROM Artist WHERE bookmarkedAt IS NOT NULL ORDER BY name DESC") fun artistsByNameDesc(): Flow> diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Artist.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Artist.kt index adf3a14..f0d3b37 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Artist.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Artist.kt @@ -8,9 +8,9 @@ import androidx.room.PrimaryKey @Entity data class Artist( @PrimaryKey val id: String, - val name: String?, - val thumbnailUrl: String?, - val info: String?, - val timestamp: Long?, + val name: String? = null, + val thumbnailUrl: String? = null, + val info: String? = null, + val timestamp: Long? = null, val bookmarkedAt: Long? = null, ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeItemsPageSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeItemsPageSaver.kt index 8378710..7ecd85b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeItemsPageSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InnertubeItemsPageSaver.kt @@ -1,31 +1,4 @@ package it.vfsfitvnm.vimusic.savers -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.SaverScope -import it.vfsfitvnm.youtubemusic.Innertube - -object InnertubeSongsPageSaver : Saver, List> { - override fun SaverScope.save(value: Innertube.ItemsPage) = listOf( - value.items?.let {with(InnertubeSongItemListSaver) { save(it) } }, - value.continuation - ) - - @Suppress("UNCHECKED_CAST") - override fun restore(value: List) = Innertube.ItemsPage( - items = (value[0] as List>?)?.let(InnertubeSongItemListSaver::restore), - continuation = value[1] as String? - ) -} - -object InnertubeAlbumsPageSaver : Saver, List> { - override fun SaverScope.save(value: Innertube.ItemsPage) = listOf( - value.items?.let {with(InnertubeAlbumItemListSaver) { save(it) } }, - value.continuation - ) - - @Suppress("UNCHECKED_CAST") - override fun restore(value: List) = Innertube.ItemsPage( - items = (value[0] as List>?)?.let(InnertubeAlbumItemListSaver::restore), - continuation = value[1] as String? - ) -} +val InnertubeSongsPageSaver = innertubeItemsPageSaver(InnertubeSongItemListSaver) +val InnertubeAlbumsPageSaver = innertubeItemsPageSaver(InnertubeAlbumItemListSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/Savers.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/Savers.kt index 09e545f..adf0fa0 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/Savers.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/Savers.kt @@ -2,6 +2,7 @@ package it.vfsfitvnm.vimusic.savers import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.Innertube interface ListSaver : Saver, List> { override fun SaverScope.save(value: List): List @@ -35,3 +36,17 @@ fun nullableSaver(saver: Saver) = override fun restore(value: Saveable): Original? = saver.restore(value) } + +fun innertubeItemsPageSaver(saver: ListSaver>) = + object : Saver, List> { + override fun SaverScope.save(value: Innertube.ItemsPage) = listOf( + value.items?.let { with(saver) { save(it) } }, + value.continuation + ) + + @Suppress("UNCHECKED_CAST") + override fun restore(value: List) = Innertube.ItemsPage( + items = (value[0] as List>?)?.let(saver::restore), + continuation = value[1] as String? + ) + } 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 11ffa02..72a2b2d 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,7 +1,6 @@ package it.vfsfitvnm.vimusic.ui.components.themed import android.annotation.SuppressLint -import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedVisibilityScope @@ -10,26 +9,14 @@ import androidx.compose.animation.core.Spring import androidx.compose.animation.core.VisibilityThreshold import androidx.compose.animation.core.spring import androidx.compose.animation.with -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.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape 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.IntOffset -import androidx.compose.ui.unit.dp -import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance @SuppressLint("ModifierParameter") diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/ShimmerHost.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/ShimmerHost.kt new file mode 100644 index 0000000..98bf7ba --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/ShimmerHost.kt @@ -0,0 +1,31 @@ +package it.vfsfitvnm.vimusic.ui.components.themed + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import com.valentinilk.shimmer.shimmer + +@Composable +fun ShimmerHost(content: @Composable ColumnScope.() -> Unit) { + Column( + modifier = Modifier + .shimmer() + .graphicsLayer(alpha = 0.99f) + .drawWithContent { + drawContent() + drawRect( + brush = Brush.verticalGradient( + listOf(Color.Black, Color.Transparent) + ), + blendMode = BlendMode.DstIn + ) + }, + content = content + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistContent.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistContent.kt deleted file mode 100644 index 87fe41f..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistContent.kt +++ /dev/null @@ -1,152 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.artist - -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyItemScope -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.BasicText -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.autoSaver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues -import it.vfsfitvnm.vimusic.models.Artist -import it.vfsfitvnm.vimusic.savers.ListSaver -import it.vfsfitvnm.vimusic.ui.components.themed.Header -import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder -import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance -import it.vfsfitvnm.vimusic.utils.center -import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableOneShotState -import it.vfsfitvnm.vimusic.utils.secondary -import it.vfsfitvnm.youtubemusic.Innertube -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -@ExperimentalAnimationApi -@Composable -inline fun ArtistContent( - artist: Artist?, - youtubeArtistPage: Innertube.ArtistPage?, - isLoading: Boolean, - isError: Boolean, - stateSaver: ListSaver>, - crossinline itemsPageProvider: suspend (String?) -> Result?>?, - crossinline bookmarkIconContent: @Composable () -> Unit, - crossinline shareIconContent: @Composable () -> Unit, - crossinline itemContent: @Composable LazyItemScope.(T) -> Unit, - noinline itemPlaceholderContent: @Composable () -> Unit, -) { - var items by rememberSaveable(stateSaver = stateSaver) { - mutableStateOf(listOf()) - } - - var isLoadingItems by remember { - mutableStateOf(false) - } - - var isErrorItems by remember { - mutableStateOf(false) - } - - val (continuationState, fetch) = produceSaveableRelaunchableOneShotState( - initialValue = null, - stateSaver = autoSaver(), - youtubeArtistPage - ) { - if (youtubeArtistPage == null) return@produceSaveableRelaunchableOneShotState - - println("loading... $value") - - isLoadingItems = true - withContext(Dispatchers.IO) { - itemsPageProvider(value)?.onSuccess { itemsPage -> - value = itemsPage?.continuation - itemsPage?.items?.let { - items = items.plus(it).distinctBy(Innertube.Item::key) - } - isErrorItems = false - isLoadingItems = false - }?.onFailure { - println("error (2): $it") - isErrorItems = true - isLoadingItems = false - } - } - } - - val continuation by continuationState - - when { - artist != null -> { - LazyColumn( - contentPadding = LocalPlayerAwarePaddingValues.current, - modifier = Modifier - .fillMaxSize() - ) { - item( - key = "header", - contentType = 0, - ) { - Header(title = artist.name ?: "Unknown") { - bookmarkIconContent() - shareIconContent() - } - } - - items( - items = items, - key = Innertube.Item::key, - itemContent = itemContent - ) - - if (isError || isErrorItems) { - item(key = "error") { - BasicText( - text = "An error has occurred", - style = LocalAppearance.current.typography.s.secondary.center, - modifier = Modifier - .padding(all = 16.dp) - ) - } - } else { - item("loading") { - val hasMore = continuation != null - - if (hasMore || items.isEmpty()) { - ShimmerHost { - repeat(if (hasMore) 3 else 8) { - itemPlaceholderContent() - } - } - -// if (hasMore && items.isNotEmpty()) { -// println("loading again!") -// SideEffect(fetch) -// } - } - } - } - } - } - isError -> BasicText( - text = "An error has occurred", - style = LocalAppearance.current.typography.s.secondary.center, - modifier = Modifier - .padding(all = 16.dp) - ) - isLoading -> ShimmerHost { - HeaderPlaceholder() - - repeat(5) { - itemPlaceholderContent() - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongsList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongsList.kt index 4fd4cdf..2511cf7 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongsList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongsList.kt @@ -2,55 +2,36 @@ package it.vfsfitvnm.vimusic.ui.screens.artist import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.models.Artist import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver -import it.vfsfitvnm.vimusic.ui.components.themed.Header -import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder +import it.vfsfitvnm.vimusic.savers.nullableSaver 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.components.themed.ShimmerHost import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.px -import it.vfsfitvnm.vimusic.ui.styling.shimmer import it.vfsfitvnm.vimusic.ui.views.SongItem +import it.vfsfitvnm.vimusic.ui.views.SongItemPlaceholder import it.vfsfitvnm.vimusic.utils.asMediaItem -import it.vfsfitvnm.vimusic.utils.center import it.vfsfitvnm.vimusic.utils.enqueue import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning import it.vfsfitvnm.vimusic.utils.produceSaveableState -import it.vfsfitvnm.vimusic.utils.secondary -import it.vfsfitvnm.vimusic.utils.thumbnail import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn @@ -58,18 +39,15 @@ import kotlinx.coroutines.flow.flowOn @Composable fun ArtistLocalSongsList( browseId: String, - artist: Artist?, - isLoading: Boolean, - isError: Boolean, - bookmarkIconContent: @Composable () -> Unit, - shareIconContent: @Composable () -> Unit, + thumbnailContent: @Composable ColumnScope.() -> Unit, + headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit, ) { - val (colorPalette, typography, thumbnailShape) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current + val (colorPalette) = LocalAppearance.current val songs by produceSaveableState( - initialValue = emptyList(), - stateSaver = DetailedSongListSaver + initialValue = null, + stateSaver = nullableSaver(DetailedSongListSaver) ) { Database .artistSongs(browseId) @@ -79,133 +57,70 @@ fun ArtistLocalSongsList( val songThumbnailSizePx = Dimensions.thumbnails.song.px - BoxWithConstraints { - val thumbnailSizeDp = maxWidth - Dimensions.navigationRailWidth - val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px - - when { - artist != null -> { - LazyColumn( - contentPadding = LocalPlayerAwarePaddingValues.current, - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - ) { - item( - key = "header", - contentType = 0 - ) { - Column { - Header(title = artist.name ?: "Unknown") { - SecondaryTextButton( - text = "Enqueue", - isEnabled = songs.isNotEmpty(), - onClick = { - binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem)) - } - ) - - Spacer( - modifier = Modifier - .weight(1f) - ) - - bookmarkIconContent() - shareIconContent() - } - - AsyncImage( - model = artist.thumbnailUrl?.thumbnail(thumbnailSizePx), - contentDescription = null, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(all = 16.dp) - .clip(CircleShape) - .size(thumbnailSizeDp) - ) - } - } - - itemsIndexed( - items = songs, - key = { _, song -> song.id } - ) { index, song -> - SongItem( - song = song, - thumbnailSizePx = songThumbnailSizePx, - onClick = { - binder?.stopRadio() - binder?.player?.forcePlayAtIndex( - songs.map(DetailedSong::asMediaItem), - index - ) - }, - menuContent = { - NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) - } - ) - } - } - - PrimaryButton( - iconId = R.drawable.shuffle, - isEnabled = songs.isNotEmpty(), - onClick = { - binder?.stopRadio() - binder?.player?.forcePlayFromBeginning( - songs.shuffled().map(DetailedSong::asMediaItem) - ) - } - ) - } - isError -> Box( - modifier = Modifier - .align(Alignment.Center) - .fillMaxSize() + Box { + LazyColumn( + contentPadding = LocalPlayerAwarePaddingValues.current, + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0 ) { - BasicText( - text = "An error has occurred.", - style = typography.s.secondary.center, - modifier = Modifier - .align(Alignment.Center) - ) - } - isLoading -> ShimmerHost { - HeaderPlaceholder() - - Spacer( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(all = 16.dp) - .clip(CircleShape) - .size(thumbnailSizeDp) - .background(colorPalette.shimmer) - ) - - repeat(3) { index -> - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - modifier = Modifier - .alpha(1f - index * 0.25f) - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding) - .height(Dimensions.thumbnails.song) - ) { - Spacer( - modifier = Modifier - .background(color = colorPalette.shimmer, shape = thumbnailShape) - .size(Dimensions.thumbnails.song) + Column { + headerContent { + SecondaryTextButton( + text = "Enqueue", + isEnabled = !songs.isNullOrEmpty(), + onClick = { + binder?.player?.enqueue(songs!!.map(DetailedSong::asMediaItem)) + } ) - - Column { - TextPlaceholder() - TextPlaceholder() - } } + + thumbnailContent() } } + songs?.let { songs -> + itemsIndexed( + items = songs, + key = { _, song -> song.id } + ) { index, song -> + SongItem( + song = song, + thumbnailSizePx = songThumbnailSizePx, + onClick = { + binder?.stopRadio() + binder?.player?.forcePlayAtIndex( + songs.map(DetailedSong::asMediaItem), + index + ) + }, + menuContent = { + NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) + } + ) + } + } ?: item(key = "loading") { + ShimmerHost { + repeat(4) { + SongItemPlaceholder(thumbnailSizeDp = Dimensions.thumbnails.song) + } + } + } } + + PrimaryButton( + iconId = R.drawable.shuffle, + isEnabled = !songs.isNullOrEmpty(), + onClick = { + 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 894938d..78e5ea9 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 @@ -5,15 +5,15 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState @@ -26,14 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import com.valentinilk.shimmer.shimmer import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R @@ -42,6 +35,7 @@ import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton +import it.vfsfitvnm.vimusic.ui.components.themed.ShimmerHost import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance @@ -56,23 +50,19 @@ import it.vfsfitvnm.vimusic.utils.center import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold -import it.vfsfitvnm.vimusic.utils.thumbnail import it.vfsfitvnm.youtubemusic.Innertube import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint @ExperimentalAnimationApi @Composable fun ArtistOverview( - artist: Artist?, youtubeArtistPage: Innertube.ArtistPage?, - isLoading: Boolean, - isError: Boolean, onViewAllSongsClick: () -> Unit, onViewAllAlbumsClick: () -> Unit, onViewAllSinglesClick: () -> Unit, onAlbumClick: (String) -> Unit, - bookmarkIconContent: @Composable () -> Unit, - shareIconContent: @Composable () -> Unit, + thumbnailContent: @Composable ColumnScope.() -> Unit, + headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit, ) { val (colorPalette, typography) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current @@ -86,10 +76,7 @@ fun ArtistOverview( .padding(horizontal = 16.dp) .padding(top = 24.dp, bottom = 8.dp) - BoxWithConstraints { - val thumbnailSizeDp = maxWidth - Dimensions.navigationRailWidth - val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px - + Box { Column( modifier = Modifier .background(colorPalette.background0) @@ -97,223 +84,167 @@ fun ArtistOverview( .verticalScroll(rememberScrollState()) .padding(LocalPlayerAwarePaddingValues.current) ) { - when { - artist != null -> { - Header(title = artist.name ?: "Unknown") { - youtubeArtistPage?.radioEndpoint?.let { radioEndpoint -> - SecondaryTextButton( - text = "Start radio", - onClick = { - binder?.stopRadio() - binder?.playRadio(radioEndpoint) - } - ) + headerContent { + youtubeArtistPage?.radioEndpoint?.let { radioEndpoint -> + SecondaryTextButton( + text = "Start radio", + onClick = { + binder?.stopRadio() + binder?.playRadio(radioEndpoint) } + ) + } + } - Spacer( - modifier = Modifier - .weight(1f) + thumbnailContent() + + if (youtubeArtistPage != null) { + youtubeArtistPage.songs?.let { songs -> + Row( + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxSize() + ) { + BasicText( + text = "Songs", + style = typography.m.semiBold, + modifier = sectionTextModifier ) - bookmarkIconContent() - shareIconContent() + youtubeArtistPage.songsEndpoint?.let { + BasicText( + text = "View all", + style = typography.xs.secondary, + modifier = sectionTextModifier + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = onViewAllSongsClick + ), + ) + } } - AsyncImage( - model = artist.thumbnailUrl?.thumbnail(thumbnailSizePx), - contentDescription = null, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(all = 16.dp) - .clip(CircleShape) - .size(thumbnailSizeDp) - ) - - when { - youtubeArtistPage != null -> { - youtubeArtistPage.songs?.let { songs -> - Row( - verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxSize() - ) { - BasicText( - text = "Songs", - style = typography.m.semiBold, - modifier = sectionTextModifier - ) - - youtubeArtistPage.songsEndpoint?.let { - BasicText( - text = "View all", - style = typography.xs.secondary, - modifier = sectionTextModifier - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() }, - onClick = onViewAllSongsClick - ), - ) - } - } - - songs.forEach { song -> - SongItem( - song = song, - thumbnailSizePx = songThumbnailSizePx, - onClick = { - val mediaItem = song.asMediaItem - binder?.stopRadio() - binder?.player?.forcePlay(mediaItem) - binder?.setupRadio( - NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId) - ) - } - ) - } - } - - youtubeArtistPage.albums?.let { albums -> - Row( - verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxSize() - ) { - BasicText( - text = "Albums", - style = typography.m.semiBold, - modifier = sectionTextModifier - ) - - youtubeArtistPage.albumsEndpoint?.let { - BasicText( - text = "View all", - style = typography.xs.secondary, - modifier = sectionTextModifier - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() }, - onClick = onViewAllAlbumsClick - ), - ) - } - } - - LazyRow( - modifier = Modifier - .fillMaxWidth() - ) { - items( - items = albums, - key = Innertube.AlbumItem::key - ) { album -> - AlbumItem( - album = album, - thumbnailSizePx = albumThumbnailSizePx, - thumbnailSizeDp = albumThumbnailSizeDp, - alternative = true, - modifier = Modifier - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() }, - onClick = { onAlbumClick(album.key) } - ) - ) - } - } - } - - youtubeArtistPage.singles?.let { singles -> - Row( - verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxSize() - ) { - BasicText( - text = "Singles", - style = typography.m.semiBold, - modifier = sectionTextModifier - ) - - youtubeArtistPage.singlesEndpoint?.let { - BasicText( - text = "View all", - style = typography.xs.secondary, - modifier = sectionTextModifier - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() }, - onClick = onViewAllSinglesClick - ), - ) - } - } - - LazyRow( - modifier = Modifier - .fillMaxWidth() - ) { - items( - items = singles, - key = Innertube.AlbumItem::key - ) { album -> - AlbumItem( - album = album, - thumbnailSizePx = albumThumbnailSizePx, - thumbnailSizeDp = albumThumbnailSizeDp, - alternative = true, - modifier = Modifier - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() }, - onClick = { onAlbumClick(album.key) } - ) - ) - } - } - } - } - isError -> ErrorText() - isLoading -> ShimmerHost { - TextPlaceholder(modifier = sectionTextModifier) - - repeat(5) { - SongItemPlaceholder( - thumbnailSizeDp = songThumbnailSizeDp, + songs.forEach { song -> + SongItem( + song = song, + thumbnailSizePx = songThumbnailSizePx, + onClick = { + val mediaItem = song.asMediaItem + binder?.stopRadio() + binder?.player?.forcePlay(mediaItem) + binder?.setupRadio( + NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId) ) } + ) + } + } - repeat(2) { - TextPlaceholder(modifier = sectionTextModifier) + youtubeArtistPage.albums?.let { albums -> + Row( + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxSize() + ) { + BasicText( + text = "Albums", + style = typography.m.semiBold, + modifier = sectionTextModifier + ) - Row { - repeat(2) { - AlbumItemPlaceholder( - thumbnailSizeDp = albumThumbnailSizeDp, - alternative = true - ) - } - } - } + youtubeArtistPage.albumsEndpoint?.let { + BasicText( + text = "View all", + style = typography.xs.secondary, + modifier = sectionTextModifier + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = onViewAllAlbumsClick + ), + ) + } + } + + LazyRow( + modifier = Modifier + .fillMaxWidth() + ) { + items( + items = albums, + key = Innertube.AlbumItem::key + ) { album -> + AlbumItem( + album = album, + thumbnailSizePx = albumThumbnailSizePx, + thumbnailSizeDp = albumThumbnailSizeDp, + alternative = true, + modifier = Modifier + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = { onAlbumClick(album.key) } + ) + ) } } } - isError -> ErrorText() - isLoading -> ShimmerHost { - HeaderPlaceholder() - Spacer( + youtubeArtistPage.singles?.let { singles -> + Row( + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(all = 16.dp) - .clip(CircleShape) - .size(thumbnailSizeDp) - .background(colorPalette.shimmer) - ) + .fillMaxSize() + ) { + BasicText( + text = "Singles", + style = typography.m.semiBold, + modifier = sectionTextModifier + ) + youtubeArtistPage.singlesEndpoint?.let { + BasicText( + text = "View all", + style = typography.xs.secondary, + modifier = sectionTextModifier + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = onViewAllSinglesClick + ), + ) + } + } + + LazyRow( + modifier = Modifier + .fillMaxWidth() + ) { + items( + items = singles, + key = Innertube.AlbumItem::key + ) { album -> + AlbumItem( + album = album, + thumbnailSizePx = albumThumbnailSizePx, + thumbnailSizeDp = albumThumbnailSizeDp, + alternative = true, + modifier = Modifier + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = { onAlbumClick(album.key) } + ) + ) + } + } + } + } else { + ShimmerHost { TextPlaceholder(modifier = sectionTextModifier) repeat(5) { @@ -349,33 +280,3 @@ fun ArtistOverview( } } } - -@Composable -fun ColumnScope.ErrorText() { - BasicText( - text = "An error has occurred", - style = LocalAppearance.current.typography.s.secondary.center, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(all = 16.dp) - ) -} - -@Composable -fun ShimmerHost(content: @Composable ColumnScope.() -> Unit) { - Column( - modifier = Modifier - .shimmer() - .graphicsLayer(alpha = 0.99f) - .drawWithContent { - drawContent() - drawRect( - brush = Brush.verticalGradient( - listOf(Color.Black, Color.Transparent) - ), - blendMode = BlendMode.DstIn - ) - }, - content = content - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt index 021bede..0f81e3a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt @@ -3,22 +3,31 @@ package it.vfsfitvnm.vimusic.ui.screens.artist import android.content.Intent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveableStateHolder -import androidx.compose.runtime.setValue +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.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.valentinilk.shimmer.shimmer import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder @@ -26,16 +35,20 @@ import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.PartialArtist import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.savers.ArtistSaver -import it.vfsfitvnm.vimusic.savers.InnertubeAlbumItemListSaver +import it.vfsfitvnm.vimusic.savers.InnertubeAlbumsPageSaver import it.vfsfitvnm.vimusic.savers.InnertubeArtistPageSaver -import it.vfsfitvnm.vimusic.savers.InnertubeSongItemListSaver +import it.vfsfitvnm.vimusic.savers.InnertubeSongsPageSaver import it.vfsfitvnm.vimusic.savers.nullableSaver +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.globalRoutes +import it.vfsfitvnm.vimusic.ui.screens.searchresult.ArtistContent import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.px +import it.vfsfitvnm.vimusic.ui.styling.shimmer import it.vfsfitvnm.vimusic.ui.views.AlbumItem import it.vfsfitvnm.vimusic.ui.views.AlbumItemPlaceholder import it.vfsfitvnm.vimusic.ui.views.SongItem @@ -43,9 +56,9 @@ import it.vfsfitvnm.vimusic.ui.views.SongItemPlaceholder import it.vfsfitvnm.vimusic.utils.artistScreenTabIndexKey import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.forcePlay -import it.vfsfitvnm.vimusic.utils.produceSaveableLazyOneShotState import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.rememberPreference +import it.vfsfitvnm.vimusic.utils.thumbnail import it.vfsfitvnm.youtubemusic.Innertube import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody @@ -53,7 +66,6 @@ import it.vfsfitvnm.youtubemusic.requests.artistPage import it.vfsfitvnm.youtubemusic.requests.itemsPage import it.vfsfitvnm.youtubemusic.utils.from import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext @@ -61,51 +73,12 @@ import kotlinx.coroutines.withContext @Composable fun ArtistScreen(browseId: String) { val saveableStateHolder = rememberSaveableStateHolder() + val (tabIndex, onTabIndexChanged) = rememberPreference( artistScreenTabIndexKey, defaultValue = 0 ) - var isLoading by remember { - mutableStateOf(false) - } - - var isError by remember { - mutableStateOf(false) - } - - val youtubeArtist by produceSaveableLazyOneShotState( - initialValue = null, - stateSaver = nullableSaver(InnertubeArtistPageSaver) - ) { - println("${System.currentTimeMillis()}, computing lazyEffect (youtubeArtistResult = ${value?.name})!") - - isLoading = true - withContext(Dispatchers.IO) { - Innertube.artistPage(browseId)?.onSuccess { artistPage -> - value = artistPage - - query { - Database.upsert( - PartialArtist( - id = browseId, - name = artistPage.name, - thumbnailUrl = artistPage.thumbnail?.url, - info = artistPage.description, - timestamp = System.currentTimeMillis() - ) - ) - } - isError = false - isLoading = false - }?.onFailure { - println("error (1): $it") - isError = true - isLoading = false - } - } - } - val artist by produceSaveableState( initialValue = null, stateSaver = nullableSaver(ArtistSaver), @@ -113,75 +86,140 @@ fun ArtistScreen(browseId: String) { Database .artist(browseId) .flowOn(Dispatchers.IO) - .filter { - val hasToFetch = it?.timestamp == null - if (hasToFetch) { - youtubeArtist?.name - } - !hasToFetch - } .collect { value = it } } + val youtubeArtist by produceSaveableState( + initialValue = null, + stateSaver = nullableSaver(InnertubeArtistPageSaver), + tabIndex < 4 + ) { + if (value != null || (tabIndex == 4 && withContext(Dispatchers.IO) { Database.artistTimestamp(browseId) } != null)) return@produceSaveableState + + withContext(Dispatchers.IO) { + Innertube.artistPage(browseId) + }?.onSuccess { artistPage -> + value = artistPage + + query { + Database.upsert( + PartialArtist( + id = browseId, + name = artistPage.name, + thumbnailUrl = artistPage.thumbnail?.url, + info = artistPage.description, + timestamp = System.currentTimeMillis() + ) + ) + } + } + } + RouteHandler(listenToGlobalEmitter = true) { globalRoutes() host { - val bookmarkIconContent: @Composable () -> Unit = { - Image( - painter = painterResource( - if (artist?.bookmarkedAt == null) { - R.drawable.bookmark_outline - } else { - R.drawable.bookmark - } - ), - contentDescription = null, - colorFilter = ColorFilter.tint(LocalAppearance.current.colorPalette.accent), - modifier = Modifier - .clickable { - val bookmarkedAt = - if (artist?.bookmarkedAt == null) System.currentTimeMillis() else null + val thumbnailContent: @Composable ColumnScope.() -> Unit = { + if (artist?.timestamp == null) { + Spacer( + modifier = Modifier + .shimmer() + .align(Alignment.CenterHorizontally) + .padding(all = 16.dp) + .clip(CircleShape) + .fillMaxWidth() + .aspectRatio(1f) + .background(LocalAppearance.current.colorPalette.shimmer) + ) + } else { + BoxWithConstraints( + modifier = Modifier + .align(Alignment.CenterHorizontally) + ) { + val thumbnailSizeDp = maxWidth - Dimensions.navigationRailWidth + val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px - query { - artist - ?.copy(bookmarkedAt = bookmarkedAt) - ?.let(Database::update) - } - } - .padding(all = 4.dp) - .size(18.dp) - ) + AsyncImage( + model = artist?.thumbnailUrl?.thumbnail(thumbnailSizePx), + contentDescription = null, + modifier = Modifier + .padding(all = 16.dp) + .clip(CircleShape) + .size(thumbnailSizeDp) + ) + } + } } - val shareIconContent: @Composable () -> Unit = { - val context = LocalContext.current + val headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit = { textButton -> + if (artist?.timestamp == null) { + HeaderPlaceholder( + modifier = Modifier + .shimmer() + ) + } else { + val context = LocalContext.current - Image( - painter = painterResource(R.drawable.share_social), - contentDescription = null, - colorFilter = ColorFilter.tint(LocalAppearance.current.colorPalette.text), - modifier = Modifier - .clickable { - val sendIntent = Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra( - Intent.EXTRA_TEXT, - "https://music.youtube.com/channel/$browseId" - ) - } + Header(title = artist?.name ?: "Unknown") { + textButton?.invoke() - context.startActivity( - Intent.createChooser( - sendIntent, - null - ) - ) - } - .padding(all = 4.dp) - .size(18.dp) - ) + Spacer( + modifier = Modifier + .weight(1f) + ) + + Image( + painter = painterResource( + if (artist?.bookmarkedAt == null) { + R.drawable.bookmark_outline + } else { + R.drawable.bookmark + } + ), + contentDescription = null, + colorFilter = ColorFilter.tint(LocalAppearance.current.colorPalette.accent), + modifier = Modifier + .clickable { + val bookmarkedAt = + if (artist?.bookmarkedAt == null) System.currentTimeMillis() else null + + query { + artist + ?.copy(bookmarkedAt = bookmarkedAt) + ?.let(Database::update) + } + } + .padding(all = 4.dp) + .size(18.dp) + ) + + Image( + painter = painterResource(R.drawable.share_social), + contentDescription = null, + colorFilter = ColorFilter.tint(LocalAppearance.current.colorPalette.text), + modifier = Modifier + .clickable { + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra( + Intent.EXTRA_TEXT, + "https://music.youtube.com/channel/$browseId" + ) + } + + context.startActivity( + Intent.createChooser( + sendIntent, + null + ) + ) + } + .padding(all = 4.dp) + .size(18.dp) + ) + } + } } Scaffold( @@ -195,17 +233,14 @@ fun ArtistScreen(browseId: String) { Item(2, "Albums", R.drawable.disc) Item(3, "Singles", R.drawable.disc) Item(4, "Library", R.drawable.library) - } + }, ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { when (currentTabIndex) { 0 -> ArtistOverview( - artist = artist, youtubeArtistPage = youtubeArtist, - isLoading = isLoading, - isError = isError, - bookmarkIconContent = bookmarkIconContent, - shareIconContent = shareIconContent, + thumbnailContent = thumbnailContent, + headerContent = headerContent, onAlbumClick = { albumRoute(it) }, onViewAllSongsClick = { onTabIndexChanged(1) }, onViewAllAlbumsClick = { onTabIndexChanged(2) }, @@ -218,14 +253,9 @@ fun ArtistScreen(browseId: String) { val thumbnailSizePx = thumbnailSizeDp.px ArtistContent( - artist = artist, - youtubeArtistPage = youtubeArtist, - isLoading = isLoading, - isError = isError, - stateSaver = InnertubeSongItemListSaver, - bookmarkIconContent = bookmarkIconContent, - shareIconContent = shareIconContent, - itemsPageProvider = { continuation -> + stateSaver = InnertubeSongsPageSaver, + headerContent = headerContent, + itemsPageProvider = youtubeArtist?.let {({ continuation -> continuation?.let { Innertube.itemsPage( body = ContinuationBody(continuation = continuation), @@ -233,14 +263,23 @@ fun ArtistScreen(browseId: String) { ) } ?: youtubeArtist ?.songsEndpoint - ?.browseId - ?.let { browseId -> + ?.takeIf { it.browseId != null } + ?.let { endpoint -> Innertube.itemsPage( - body = BrowseBody(browseId = browseId), + body = BrowseBody( + browseId = endpoint.browseId!!, + params = endpoint.params, + ), fromMusicResponsiveListItemRenderer = Innertube.SongItem::from, ) } - }, + ?: Result.success( + Innertube.ItemsPage( + items = youtubeArtist?.songs, + continuation = null + ) + ) + })}, itemContent = { song -> SongItem( song = song, @@ -263,29 +302,33 @@ fun ArtistScreen(browseId: String) { val thumbnailSizePx = thumbnailSizeDp.px ArtistContent( - artist = artist, - youtubeArtistPage = youtubeArtist, - isLoading = isLoading, - isError = isError, - stateSaver = InnertubeAlbumItemListSaver, - bookmarkIconContent = bookmarkIconContent, - shareIconContent = shareIconContent, - itemsPageProvider = { continuation -> + stateSaver = InnertubeAlbumsPageSaver, + headerContent = headerContent, + itemsPageProvider = youtubeArtist?.let {({ continuation -> continuation?.let { Innertube.itemsPage( body = ContinuationBody(continuation = continuation), fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from, ) } ?: youtubeArtist - ?.songsEndpoint - ?.browseId - ?.let { browseId -> + ?.albumsEndpoint + ?.takeIf { it.browseId != null } + ?.let { endpoint -> Innertube.itemsPage( - body = BrowseBody(browseId = browseId), + body = BrowseBody( + browseId = endpoint.browseId!!, + params = endpoint.params, + ), fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from, ) } - }, + ?: Result.success( + Innertube.ItemsPage( + items = youtubeArtist?.albums, + continuation = null + ) + ) + })}, itemContent = { album -> AlbumItem( album = album, @@ -310,29 +353,33 @@ fun ArtistScreen(browseId: String) { val thumbnailSizePx = thumbnailSizeDp.px ArtistContent( - artist = artist, - youtubeArtistPage = youtubeArtist, - isLoading = isLoading, - isError = isError, - stateSaver = InnertubeAlbumItemListSaver, - bookmarkIconContent = bookmarkIconContent, - shareIconContent = shareIconContent, - itemsPageProvider = { continuation -> + stateSaver = InnertubeAlbumsPageSaver, + headerContent = headerContent, + itemsPageProvider = youtubeArtist?.let {({ continuation -> continuation?.let { Innertube.itemsPage( body = ContinuationBody(continuation = continuation), fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from, ) } ?: youtubeArtist - ?.songsEndpoint - ?.browseId - ?.let { browseId -> + ?.singlesEndpoint + ?.takeIf { it.browseId != null } + ?.let { endpoint -> Innertube.itemsPage( - body = BrowseBody(browseId = browseId), + body = BrowseBody( + browseId = endpoint.browseId!!, + params = endpoint.params, + ), fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from, ) } - }, + ?: Result.success( + Innertube.ItemsPage( + items = youtubeArtist?.singles, + continuation = null + ) + ) + })}, itemContent = { album -> AlbumItem( album = album, @@ -354,11 +401,8 @@ fun ArtistScreen(browseId: String) { 4 -> ArtistLocalSongsList( browseId = browseId, - artist = artist, - isLoading = isLoading, - isError = isError, - bookmarkIconContent = bookmarkIconContent, - shareIconContent = shareIconContent + headerContent = headerContent, + thumbnailContent = thumbnailContent, ) } } 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 new file mode 100644 index 0000000..021fd4d --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemsPage.kt @@ -0,0 +1,90 @@ +package it.vfsfitvnm.vimusic.ui.screens.searchresult + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope +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.rememberUpdatedState +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.savers.nullableSaver +import it.vfsfitvnm.vimusic.ui.components.themed.ShimmerHost +import it.vfsfitvnm.vimusic.utils.produceSaveableState +import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.utils.plus +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@ExperimentalAnimationApi +@Composable +inline fun ArtistContent( + stateSaver: Saver, List>, + noinline itemsPageProvider: (suspend (String?) -> Result?>?)? = null, + crossinline headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit, + crossinline itemContent: @Composable LazyItemScope.(T) -> Unit, + noinline itemPlaceholderContent: @Composable () -> Unit, +) { + val lazyListState = rememberLazyListState() + val updatedItemsPageProvider by rememberUpdatedState(itemsPageProvider) + + val itemsPage by produceSaveableState( + initialValue = null, + stateSaver = nullableSaver(stateSaver), + lazyListState, updatedItemsPageProvider + ) { + val currentItemsPageProvider = updatedItemsPageProvider ?: return@produceSaveableState + + snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo.any { it.key == "loading" } } + .collect { shouldLoadMore -> + if (!shouldLoadMore) return@collect + + withContext(Dispatchers.IO) { + currentItemsPageProvider(value?.continuation) + }?.onSuccess { + if (it == null) { + if (value == null) { + value = Innertube.ItemsPage(null, null) + } + } else { + value += it + } + } + } + } + + LazyColumn( + state = lazyListState, + contentPadding = LocalPlayerAwarePaddingValues.current, + modifier = Modifier + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0, + ) { + headerContent(null) + } + + items( + items = itemsPage?.items ?: emptyList(), + key = Innertube.Item::key, + itemContent = itemContent + ) + + if (!(itemsPage != null && itemsPage?.continuation == null)) { + item(key = "loading") { + ShimmerHost { + repeat(if (itemsPage?.items.isNullOrEmpty()) 8 else 3) { + itemPlaceholderContent() + } + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt deleted file mode 100644 index ec141fc..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt +++ /dev/null @@ -1,172 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.searchresult - -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyItemScope -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.BasicText -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.autoSaver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.input.pointer.pointerInput -import com.valentinilk.shimmer.shimmer -import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues -import it.vfsfitvnm.vimusic.savers.ListSaver -import it.vfsfitvnm.vimusic.savers.resultSaver -import it.vfsfitvnm.vimusic.ui.components.themed.Header -import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance -import it.vfsfitvnm.vimusic.utils.center -import it.vfsfitvnm.vimusic.utils.medium -import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableOneShotState -import it.vfsfitvnm.vimusic.utils.secondary -import it.vfsfitvnm.youtubemusic.Innertube -import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer -import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody -import it.vfsfitvnm.youtubemusic.models.bodies.SearchBody -import it.vfsfitvnm.youtubemusic.requests.searchPage -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -@ExperimentalAnimationApi -@Composable -inline fun SearchResult( - query: String, - filter: String, - stateSaver: ListSaver>, - noinline fromMusicShelfRendererContent: (MusicShelfRenderer.Content) -> T?, - crossinline onSearchAgain: () -> Unit, - crossinline itemContent: @Composable LazyItemScope.(T) -> Unit, - noinline itemPlaceholderContent: @Composable BoxScope.() -> Unit, -) { - val (_, typography) = LocalAppearance.current - - var items by rememberSaveable(stateSaver = stateSaver) { - mutableStateOf(listOf()) - } - - val (continuationResultState, fetch) = produceSaveableRelaunchableOneShotState( - initialValue = null, - stateSaver = resultSaver(autoSaver()) - ) { - val token = value?.getOrNull() - - value = null - - value = withContext(Dispatchers.IO) { - if (token == null) { - Innertube.searchPage( - body = SearchBody(query = query, params = filter), - fromMusicShelfRendererContent = fromMusicShelfRendererContent - ) - } else { - Innertube.searchPage( - body = ContinuationBody(continuation = token), - fromMusicShelfRendererContent = fromMusicShelfRendererContent - ) - } - }?.map { itemsPage -> - itemsPage?.items?.let { - items = items.plus(it).distinctBy(Innertube.Item::key) - } - - itemsPage?.continuation - } - } - - val continuationResult by continuationResultState - - LazyColumn( - contentPadding = LocalPlayerAwarePaddingValues.current, - modifier = Modifier - .fillMaxSize() - ) { - item( - key = "header", - contentType = 0, - ) { - Header( - title = query, - modifier = Modifier - .pointerInput(Unit) { - detectTapGestures { - onSearchAgain() - } - } - ) - } - - items( - items = items, - key = Innertube.Item::key, - itemContent = itemContent - ) - - continuationResult?.getOrNull()?.let { - if (items.isNotEmpty()) { - item { - SideEffect(fetch) - } - } - } ?: continuationResult?.exceptionOrNull()?.let { - item { - Box( - modifier = Modifier - .pointerInput(Unit) { - detectTapGestures { - fetch() - } - } - .fillMaxSize() - ) { - BasicText( - text = "An error has occurred.\nTap to retry", - style = typography.s.medium.secondary.center, - modifier = Modifier - .align(Alignment.Center) - ) - } - } - } ?: continuationResult?.let { - if (items.isEmpty()) { - item { - Box( - modifier = Modifier - .fillMaxSize() - ) { - BasicText( - text = "No results found.\nPlease try a different query or category", - style = typography.s.medium.secondary.center, - modifier = Modifier - .align(Alignment.Center) - ) - } - } - } - } ?: item(key = "loading") { - Column( - modifier = Modifier - .shimmer() - ) { - repeat(if (items.isEmpty()) 8 else 3) { index -> - Box( - modifier = Modifier - .alpha(1f - index * 0.125f), - content = itemPlaceholderContent - ) - } - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt index dd7d28b..5ceb8cc 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt @@ -3,21 +3,25 @@ package it.vfsfitvnm.vimusic.ui.screens.searchresult import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.savers.InnertubeAlbumItemListSaver +import it.vfsfitvnm.vimusic.savers.InnertubeAlbumsPageSaver import it.vfsfitvnm.vimusic.savers.InnertubeArtistItemListSaver import it.vfsfitvnm.vimusic.savers.InnertubePlaylistItemListSaver -import it.vfsfitvnm.vimusic.savers.InnertubeSongItemListSaver +import it.vfsfitvnm.vimusic.savers.InnertubeSongsPageSaver import it.vfsfitvnm.vimusic.savers.InnertubeVideoItemListSaver +import it.vfsfitvnm.vimusic.savers.innertubeItemsPageSaver +import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.artistRoute @@ -40,6 +44,9 @@ import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.vimusic.utils.searchResultScreenTabIndexKey import it.vfsfitvnm.youtubemusic.Innertube +import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody +import it.vfsfitvnm.youtubemusic.models.bodies.SearchBody +import it.vfsfitvnm.youtubemusic.requests.searchPage import it.vfsfitvnm.youtubemusic.utils.from @ExperimentalFoundationApi @@ -53,6 +60,18 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { globalRoutes() host { + val headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit = { + Header( + title = query, + modifier = Modifier + .pointerInput(Unit) { + detectTapGestures { + onSearchAgain() + } + } + ) + } + Scaffold( topIconButtonId = R.drawable.chevron_back, onTopIconButtonClick = pop, @@ -67,16 +86,6 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { Item(5, "Featured", R.drawable.playlist) } ) { tabIndex -> - val searchFilter = when (tabIndex) { - 0 -> Innertube.SearchFilter.Song - 1 -> Innertube.SearchFilter.Album - 2 -> Innertube.SearchFilter.Artist - 3 -> Innertube.SearchFilter.Video - 4 -> Innertube.SearchFilter.CommunityPlaylist - 5 -> Innertube.SearchFilter.FeaturedPlaylist - else -> error("unreachable") - }.value - saveableStateHolder.SaveableStateProvider(tabIndex) { when (tabIndex) { 0 -> { @@ -84,12 +93,22 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailSizeDp = Dimensions.thumbnails.song val thumbnailSizePx = thumbnailSizeDp.px - SearchResult( - query = query, - filter = searchFilter, - onSearchAgain = onSearchAgain, - stateSaver = InnertubeSongItemListSaver, - fromMusicShelfRendererContent = Innertube.SongItem.Companion::from, + ArtistContent( + stateSaver = InnertubeSongsPageSaver, + itemsPageProvider = { continuation -> + if (continuation == null) { + Innertube.searchPage( + body = SearchBody(query = query, params = Innertube.SearchFilter.Song.value), + fromMusicShelfRendererContent = Innertube.SongItem.Companion::from + ) + } else { + Innertube.searchPage( + body = ContinuationBody(continuation = continuation), + fromMusicShelfRendererContent = Innertube.SongItem.Companion::from + ) + } + }, + headerContent = headerContent, itemContent = { song -> SongItem( song = song, @@ -111,12 +130,22 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailSizeDp = 108.dp val thumbnailSizePx = thumbnailSizeDp.px - SearchResult( - query = query, - filter = searchFilter, - stateSaver = InnertubeAlbumItemListSaver, - onSearchAgain = onSearchAgain, - fromMusicShelfRendererContent = Innertube.AlbumItem.Companion::from, + ArtistContent( + stateSaver = InnertubeAlbumsPageSaver, + itemsPageProvider = { continuation -> + if (continuation == null) { + Innertube.searchPage( + body = SearchBody(query = query, params = Innertube.SearchFilter.Album.value), + fromMusicShelfRendererContent = Innertube.AlbumItem::from + ) + } else { + Innertube.searchPage( + body = ContinuationBody(continuation = continuation), + fromMusicShelfRendererContent = Innertube.AlbumItem::from + ) + } + }, + headerContent = headerContent, itemContent = { album -> AlbumItem( album = album, @@ -141,12 +170,22 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailSizeDp = 64.dp val thumbnailSizePx = thumbnailSizeDp.px - SearchResult( - query = query, - filter = searchFilter, - stateSaver = InnertubeArtistItemListSaver, - onSearchAgain = onSearchAgain, - fromMusicShelfRendererContent = Innertube.ArtistItem.Companion::from, + ArtistContent( + stateSaver = innertubeItemsPageSaver(InnertubeArtistItemListSaver), + itemsPageProvider = { continuation -> + if (continuation == null) { + Innertube.searchPage( + body = SearchBody(query = query, params = Innertube.SearchFilter.Artist.value), + fromMusicShelfRendererContent = Innertube.ArtistItem::from + ) + } else { + Innertube.searchPage( + body = ContinuationBody(continuation = continuation), + fromMusicShelfRendererContent = Innertube.ArtistItem::from + ) + } + }, + headerContent = headerContent, itemContent = { artist -> ArtistItem( artist = artist, @@ -170,12 +209,22 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailHeightDp = 72.dp val thumbnailWidthDp = 128.dp - SearchResult( - query = query, - filter = searchFilter, - stateSaver = InnertubeVideoItemListSaver, - onSearchAgain = onSearchAgain, - fromMusicShelfRendererContent = Innertube.VideoItem.Companion::from, + ArtistContent( + stateSaver = innertubeItemsPageSaver(InnertubeVideoItemListSaver), + itemsPageProvider = { continuation -> + if (continuation == null) { + Innertube.searchPage( + body = SearchBody(query = query, params = Innertube.SearchFilter.Video.value), + fromMusicShelfRendererContent = Innertube.VideoItem::from + ) + } else { + Innertube.searchPage( + body = ContinuationBody(continuation = continuation), + fromMusicShelfRendererContent = Innertube.VideoItem::from + ) + } + }, + headerContent = headerContent, itemContent = { video -> VideoItem( video = video, @@ -201,12 +250,28 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailSizeDp = 108.dp val thumbnailSizePx = thumbnailSizeDp.px - SearchResult( - query = query, - filter = searchFilter, - stateSaver = InnertubePlaylistItemListSaver, - onSearchAgain = onSearchAgain, - fromMusicShelfRendererContent = Innertube.PlaylistItem.Companion::from, + ArtistContent( + stateSaver = innertubeItemsPageSaver(InnertubePlaylistItemListSaver), + itemsPageProvider = { continuation -> + if (continuation == null) { + val filter = if (tabIndex == 4) { + Innertube.SearchFilter.CommunityPlaylist + } else { + Innertube.SearchFilter.FeaturedPlaylist + } + + Innertube.searchPage( + body = SearchBody(query = query, params = filter.value), + fromMusicShelfRendererContent = Innertube.PlaylistItem::from + ) + } else { + Innertube.searchPage( + body = ContinuationBody(continuation = continuation), + fromMusicShelfRendererContent = Innertube.PlaylistItem::from + ) + } + }, + headerContent = headerContent, itemContent = { playlist -> PlaylistItem( playlist = playlist, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/InnertubeItems.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/InnertubeItems.kt index c8fee72..314c4f7 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/InnertubeItems.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/InnertubeItems.kt @@ -323,12 +323,14 @@ fun AlbumItem( ) if (!alternative) { - BasicText( - text = album.authors?.joinToString("") { it.name ?: "" } ?: "", - style = typography.xs.semiBold.secondary, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) + album.authors?.joinToString("") { it.name ?: "" }?.let { authorsText -> + BasicText( + text = authorsText, + style = typography.xs.semiBold.secondary, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } } BasicText( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt index 104fcb2..8e858dc 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt @@ -6,17 +6,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.ProduceStateScope -import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import kotlin.coroutines.CoroutineContext import kotlin.experimental.ExperimentalTypeInference -import kotlin.reflect.KProperty import kotlinx.coroutines.suspendCancellableCoroutine @Composable @@ -122,97 +119,6 @@ fun produceSaveableState( return result } -@Composable -fun produceSaveableRelaunchableOneShotState( - initialValue: T, - stateSaver: Saver, - @BuilderInference producer: suspend ProduceStateScope.() -> Unit -): Pair, () -> Unit> { - val result = rememberSaveable(stateSaver = stateSaver) { - mutableStateOf(initialValue) - } - - var produced by rememberSaveable { - mutableStateOf(false) - } - - val relaunchableEffect = relaunchableEffect(Unit) { - if (!produced) { - ProduceSaveableStateScope(result, coroutineContext).producer() - produced = true - } - } - - return result to { - produced = false - relaunchableEffect() - } -} - -@Composable -fun produceSaveableRelaunchableOneShotState( - initialValue: T, - stateSaver: Saver, - key1: Any?, - @BuilderInference producer: suspend ProduceStateScope.() -> Unit -): Pair, () -> Unit> { - val result = rememberSaveable(stateSaver = stateSaver) { - mutableStateOf(initialValue) - } - - var produced by rememberSaveable(key1) { - mutableStateOf(false) - } - - val relaunchableEffect = relaunchableEffect(key1) { - if (!produced) { - ProduceSaveableStateScope(result, coroutineContext).producer() - produced = true - } - } - - return result to { - produced = false - relaunchableEffect() - } -} - -@Composable -fun produceSaveableLazyOneShotState( - initialValue: T, - stateSaver: Saver, - @BuilderInference producer: suspend ProduceStateScope.() -> Unit -): State { - val state = rememberSaveable(stateSaver = stateSaver) { - mutableStateOf(initialValue) - } - - var produced by rememberSaveable { - mutableStateOf(false) - } - - val lazyEffect = lazyEffect(Unit) { - if (!produced) { - ProduceSaveableStateScope(state, coroutineContext).producer() - produced = true - } - } - - val delegate = remember { - object : State { - override val value: T - get() { - if (!produced) { - lazyEffect() - } - return state.value - } - } - } - - return delegate -} - private class ProduceSaveableStateScope( state: MutableState, override val coroutineContext: CoroutineContext diff --git a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/Utils.kt b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/Utils.kt index ac128e1..3f1dd23 100644 --- a/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/Utils.kt +++ b/innertube/src/main/kotlin/it/vfsfitvnm/youtubemusic/utils/Utils.kt @@ -1,24 +1,25 @@ package it.vfsfitvnm.youtubemusic.utils import io.ktor.utils.io.CancellationException +import it.vfsfitvnm.youtubemusic.Innertube import it.vfsfitvnm.youtubemusic.models.SectionListRenderer internal fun SectionListRenderer.findSectionByTitle(text: String): SectionListRenderer.Content? { return contents?.find { content -> - val title = content - .musicCarouselShelfRenderer - ?.header - ?.musicCarouselShelfBasicHeaderRenderer + val title = content + .musicCarouselShelfRenderer + ?.header + ?.musicCarouselShelfBasicHeaderRenderer + ?.title + ?: content + .musicShelfRenderer ?.title - ?: content - .musicShelfRenderer - ?.title - title - ?.runs - ?.firstOrNull() - ?.text == text - } + title + ?.runs + ?.firstOrNull() + ?.text == text + } } internal fun SectionListRenderer.findSectionByStrapline(text: String): SectionListRenderer.Content? { @@ -31,14 +32,19 @@ internal fun SectionListRenderer.findSectionByStrapline(text: String): SectionLi ?.runs ?.firstOrNull() ?.text == text - } + } } internal inline fun runCatchingNonCancellable(block: () -> R): Result? { - return Result.success(block()) -// val result = runCatching(block) -// return when (val ex = result.exceptionOrNull()) { -// is CancellationException -> null -// else -> result -// } + val result = runCatching(block) + return when (result.exceptionOrNull()) { + is CancellationException -> null + else -> result + } } + +infix operator fun Innertube.ItemsPage?.plus(other: Innertube.ItemsPage) = + other.copy( + items = this?.items?.plus(other.items ?: emptyList())?.distinctBy(Innertube.Item::key) + ?: other.items + )