Tweak code

This commit is contained in:
vfsfitvnm
2022-10-03 12:52:24 +02:00
parent 5f5a763675
commit 0ac516b39b
16 changed files with 699 additions and 1085 deletions

View File

@@ -152,6 +152,9 @@ interface Database {
@Query("SELECT * FROM Artist WHERE id = :id") @Query("SELECT * FROM Artist WHERE id = :id")
fun artist(id: String): Flow<Artist?> fun artist(id: String): Flow<Artist?>
@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") @Query("SELECT * FROM Artist WHERE bookmarkedAt IS NOT NULL ORDER BY name DESC")
fun artistsByNameDesc(): Flow<List<Artist>> fun artistsByNameDesc(): Flow<List<Artist>>

View File

@@ -8,9 +8,9 @@ import androidx.room.PrimaryKey
@Entity @Entity
data class Artist( data class Artist(
@PrimaryKey val id: String, @PrimaryKey val id: String,
val name: String?, val name: String? = null,
val thumbnailUrl: String?, val thumbnailUrl: String? = null,
val info: String?, val info: String? = null,
val timestamp: Long?, val timestamp: Long? = null,
val bookmarkedAt: Long? = null, val bookmarkedAt: Long? = null,
) )

View File

@@ -1,31 +1,4 @@
package it.vfsfitvnm.vimusic.savers package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver val InnertubeSongsPageSaver = innertubeItemsPageSaver(InnertubeSongItemListSaver)
import androidx.compose.runtime.saveable.SaverScope val InnertubeAlbumsPageSaver = innertubeItemsPageSaver(InnertubeAlbumItemListSaver)
import it.vfsfitvnm.youtubemusic.Innertube
object InnertubeSongsPageSaver : Saver<Innertube.ItemsPage<Innertube.SongItem>, List<Any?>> {
override fun SaverScope.save(value: Innertube.ItemsPage<Innertube.SongItem>) = listOf(
value.items?.let {with(InnertubeSongItemListSaver) { save(it) } },
value.continuation
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = Innertube.ItemsPage(
items = (value[0] as List<List<Any?>>?)?.let(InnertubeSongItemListSaver::restore),
continuation = value[1] as String?
)
}
object InnertubeAlbumsPageSaver : Saver<Innertube.ItemsPage<Innertube.AlbumItem>, List<Any?>> {
override fun SaverScope.save(value: Innertube.ItemsPage<Innertube.AlbumItem>) = listOf(
value.items?.let {with(InnertubeAlbumItemListSaver) { save(it) } },
value.continuation
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = Innertube.ItemsPage(
items = (value[0] as List<List<Any?>>?)?.let(InnertubeAlbumItemListSaver::restore),
continuation = value[1] as String?
)
}

View File

@@ -2,6 +2,7 @@ package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.Innertube
interface ListSaver<Original, Saveable : Any> : Saver<List<Original>, List<Saveable>> { interface ListSaver<Original, Saveable : Any> : Saver<List<Original>, List<Saveable>> {
override fun SaverScope.save(value: List<Original>): List<Saveable> override fun SaverScope.save(value: List<Original>): List<Saveable>
@@ -35,3 +36,17 @@ fun <Original, Saveable : Any> nullableSaver(saver: Saver<Original, Saveable>) =
override fun restore(value: Saveable): Original? = override fun restore(value: Saveable): Original? =
saver.restore(value) saver.restore(value)
} }
fun <Original : Innertube.Item> innertubeItemsPageSaver(saver: ListSaver<Original, List<Any?>>) =
object : Saver<Innertube.ItemsPage<Original>, List<Any?>> {
override fun SaverScope.save(value: Innertube.ItemsPage<Original>) = listOf(
value.items?.let { with(saver) { save(it) } },
value.continuation
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = Innertube.ItemsPage(
items = (value[0] as List<List<Any?>>?)?.let(saver::restore),
continuation = value[1] as String?
)
}

View File

@@ -1,7 +1,6 @@
package it.vfsfitvnm.vimusic.ui.components.themed package it.vfsfitvnm.vimusic.ui.components.themed
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.AnimatedVisibilityScope
@@ -10,26 +9,14 @@ import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.VisibilityThreshold import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.spring import androidx.compose.animation.core.spring
import androidx.compose.animation.with import androidx.compose.animation.with
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize 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.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.IntOffset
import androidx.compose.ui.unit.dp
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
@SuppressLint("ModifierParameter") @SuppressLint("ModifierParameter")

View File

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

View File

@@ -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 <T : Innertube.Item> ArtistContent(
artist: Artist?,
youtubeArtistPage: Innertube.ArtistPage?,
isLoading: Boolean,
isError: Boolean,
stateSaver: ListSaver<T, List<Any?>>,
crossinline itemsPageProvider: suspend (String?) -> Result<Innertube.ItemsPage<T>?>?,
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<String?>(),
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()
}
}
}
}

View File

@@ -2,55 +2,36 @@ package it.vfsfitvnm.vimusic.ui.screens.artist
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.Artist
import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.savers.nullableSaver
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.ShimmerHost
import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px 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.SongItem
import it.vfsfitvnm.vimusic.ui.views.SongItemPlaceholder
import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.enqueue import it.vfsfitvnm.vimusic.utils.enqueue
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
import it.vfsfitvnm.vimusic.utils.produceSaveableState 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.Dispatchers
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@@ -58,18 +39,15 @@ import kotlinx.coroutines.flow.flowOn
@Composable @Composable
fun ArtistLocalSongsList( fun ArtistLocalSongsList(
browseId: String, browseId: String,
artist: Artist?, thumbnailContent: @Composable ColumnScope.() -> Unit,
isLoading: Boolean, headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
isError: Boolean,
bookmarkIconContent: @Composable () -> Unit,
shareIconContent: @Composable () -> Unit,
) { ) {
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
val (colorPalette) = LocalAppearance.current
val songs by produceSaveableState( val songs by produceSaveableState(
initialValue = emptyList(), initialValue = null,
stateSaver = DetailedSongListSaver stateSaver = nullableSaver(DetailedSongListSaver)
) { ) {
Database Database
.artistSongs(browseId) .artistSongs(browseId)
@@ -79,133 +57,70 @@ fun ArtistLocalSongsList(
val songThumbnailSizePx = Dimensions.thumbnails.song.px val songThumbnailSizePx = Dimensions.thumbnails.song.px
BoxWithConstraints { Box {
val thumbnailSizeDp = maxWidth - Dimensions.navigationRailWidth LazyColumn(
val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier
when { .background(colorPalette.background0)
artist != null -> { .fillMaxSize()
LazyColumn( ) {
contentPadding = LocalPlayerAwarePaddingValues.current, item(
modifier = Modifier key = "header",
.background(colorPalette.background0) contentType = 0
.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()
) { ) {
BasicText( Column {
text = "An error has occurred.", headerContent {
style = typography.s.secondary.center, SecondaryTextButton(
modifier = Modifier text = "Enqueue",
.align(Alignment.Center) isEnabled = !songs.isNullOrEmpty(),
) onClick = {
} binder?.player?.enqueue(songs!!.map(DetailedSong::asMediaItem))
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 {
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)
)
}
)
} }
} }

View File

@@ -5,15 +5,15 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement 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.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@@ -26,14 +26,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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 androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R 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.HeaderPlaceholder
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.components.themed.ShimmerHost
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
@@ -56,23 +50,19 @@ import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.youtubemusic.Innertube import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun ArtistOverview( fun ArtistOverview(
artist: Artist?,
youtubeArtistPage: Innertube.ArtistPage?, youtubeArtistPage: Innertube.ArtistPage?,
isLoading: Boolean,
isError: Boolean,
onViewAllSongsClick: () -> Unit, onViewAllSongsClick: () -> Unit,
onViewAllAlbumsClick: () -> Unit, onViewAllAlbumsClick: () -> Unit,
onViewAllSinglesClick: () -> Unit, onViewAllSinglesClick: () -> Unit,
onAlbumClick: (String) -> Unit, onAlbumClick: (String) -> Unit,
bookmarkIconContent: @Composable () -> Unit, thumbnailContent: @Composable ColumnScope.() -> Unit,
shareIconContent: @Composable () -> Unit, headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
) { ) {
val (colorPalette, typography) = LocalAppearance.current val (colorPalette, typography) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
@@ -86,10 +76,7 @@ fun ArtistOverview(
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.padding(top = 24.dp, bottom = 8.dp) .padding(top = 24.dp, bottom = 8.dp)
BoxWithConstraints { Box {
val thumbnailSizeDp = maxWidth - Dimensions.navigationRailWidth
val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px
Column( Column(
modifier = Modifier modifier = Modifier
.background(colorPalette.background0) .background(colorPalette.background0)
@@ -97,223 +84,167 @@ fun ArtistOverview(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(LocalPlayerAwarePaddingValues.current) .padding(LocalPlayerAwarePaddingValues.current)
) { ) {
when { headerContent {
artist != null -> { youtubeArtistPage?.radioEndpoint?.let { radioEndpoint ->
Header(title = artist.name ?: "Unknown") { SecondaryTextButton(
youtubeArtistPage?.radioEndpoint?.let { radioEndpoint -> text = "Start radio",
SecondaryTextButton( onClick = {
text = "Start radio", binder?.stopRadio()
onClick = { binder?.playRadio(radioEndpoint)
binder?.stopRadio()
binder?.playRadio(radioEndpoint)
}
)
} }
)
}
}
Spacer( thumbnailContent()
modifier = Modifier
.weight(1f) 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() youtubeArtistPage.songsEndpoint?.let {
shareIconContent() BasicText(
text = "View all",
style = typography.xs.secondary,
modifier = sectionTextModifier
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = onViewAllSongsClick
),
)
}
} }
AsyncImage( songs.forEach { song ->
model = artist.thumbnailUrl?.thumbnail(thumbnailSizePx), SongItem(
contentDescription = null, song = song,
modifier = Modifier thumbnailSizePx = songThumbnailSizePx,
.align(Alignment.CenterHorizontally) onClick = {
.padding(all = 16.dp) val mediaItem = song.asMediaItem
.clip(CircleShape) binder?.stopRadio()
.size(thumbnailSizeDp) binder?.player?.forcePlay(mediaItem)
) binder?.setupRadio(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
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,
) )
} }
)
}
}
repeat(2) { youtubeArtistPage.albums?.let { albums ->
TextPlaceholder(modifier = sectionTextModifier) Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxSize()
) {
BasicText(
text = "Albums",
style = typography.m.semiBold,
modifier = sectionTextModifier
)
Row { youtubeArtistPage.albumsEndpoint?.let {
repeat(2) { BasicText(
AlbumItemPlaceholder( text = "View all",
thumbnailSizeDp = albumThumbnailSizeDp, style = typography.xs.secondary,
alternative = true 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 modifier = Modifier
.align(Alignment.CenterHorizontally) .fillMaxSize()
.padding(all = 16.dp) ) {
.clip(CircleShape) BasicText(
.size(thumbnailSizeDp) text = "Singles",
.background(colorPalette.shimmer) 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) TextPlaceholder(modifier = sectionTextModifier)
repeat(5) { 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
)
}

View File

@@ -3,22 +3,31 @@ package it.vfsfitvnm.vimusic.ui.screens.artist
import android.content.Intent import android.content.Intent
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource 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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveableStateHolder 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.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
@@ -26,16 +35,20 @@ import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.PartialArtist import it.vfsfitvnm.vimusic.models.PartialArtist
import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.savers.ArtistSaver 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.InnertubeArtistPageSaver
import it.vfsfitvnm.vimusic.savers.InnertubeSongItemListSaver import it.vfsfitvnm.vimusic.savers.InnertubeSongsPageSaver
import it.vfsfitvnm.vimusic.savers.nullableSaver 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.components.themed.Scaffold
import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.albumRoute
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes 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.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px 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.AlbumItem
import it.vfsfitvnm.vimusic.ui.views.AlbumItemPlaceholder import it.vfsfitvnm.vimusic.ui.views.AlbumItemPlaceholder
import it.vfsfitvnm.vimusic.ui.views.SongItem 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.artistScreenTabIndexKey
import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.produceSaveableLazyOneShotState
import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.youtubemusic.Innertube import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody 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.requests.itemsPage
import it.vfsfitvnm.youtubemusic.utils.from import it.vfsfitvnm.youtubemusic.utils.from
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -61,51 +73,12 @@ import kotlinx.coroutines.withContext
@Composable @Composable
fun ArtistScreen(browseId: String) { fun ArtistScreen(browseId: String) {
val saveableStateHolder = rememberSaveableStateHolder() val saveableStateHolder = rememberSaveableStateHolder()
val (tabIndex, onTabIndexChanged) = rememberPreference( val (tabIndex, onTabIndexChanged) = rememberPreference(
artistScreenTabIndexKey, artistScreenTabIndexKey,
defaultValue = 0 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( val artist by produceSaveableState(
initialValue = null, initialValue = null,
stateSaver = nullableSaver(ArtistSaver), stateSaver = nullableSaver(ArtistSaver),
@@ -113,75 +86,140 @@ fun ArtistScreen(browseId: String) {
Database Database
.artist(browseId) .artist(browseId)
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.filter {
val hasToFetch = it?.timestamp == null
if (hasToFetch) {
youtubeArtist?.name
}
!hasToFetch
}
.collect { value = it } .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) { RouteHandler(listenToGlobalEmitter = true) {
globalRoutes() globalRoutes()
host { host {
val bookmarkIconContent: @Composable () -> Unit = { val thumbnailContent: @Composable ColumnScope.() -> Unit = {
Image( if (artist?.timestamp == null) {
painter = painterResource( Spacer(
if (artist?.bookmarkedAt == null) { modifier = Modifier
R.drawable.bookmark_outline .shimmer()
} else { .align(Alignment.CenterHorizontally)
R.drawable.bookmark .padding(all = 16.dp)
} .clip(CircleShape)
), .fillMaxWidth()
contentDescription = null, .aspectRatio(1f)
colorFilter = ColorFilter.tint(LocalAppearance.current.colorPalette.accent), .background(LocalAppearance.current.colorPalette.shimmer)
modifier = Modifier )
.clickable { } else {
val bookmarkedAt = BoxWithConstraints(
if (artist?.bookmarkedAt == null) System.currentTimeMillis() else null modifier = Modifier
.align(Alignment.CenterHorizontally)
) {
val thumbnailSizeDp = maxWidth - Dimensions.navigationRailWidth
val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px
query { AsyncImage(
artist model = artist?.thumbnailUrl?.thumbnail(thumbnailSizePx),
?.copy(bookmarkedAt = bookmarkedAt) contentDescription = null,
?.let(Database::update) modifier = Modifier
} .padding(all = 16.dp)
} .clip(CircleShape)
.padding(all = 4.dp) .size(thumbnailSizeDp)
.size(18.dp) )
) }
}
} }
val shareIconContent: @Composable () -> Unit = { val headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit = { textButton ->
val context = LocalContext.current if (artist?.timestamp == null) {
HeaderPlaceholder(
modifier = Modifier
.shimmer()
)
} else {
val context = LocalContext.current
Image( Header(title = artist?.name ?: "Unknown") {
painter = painterResource(R.drawable.share_social), textButton?.invoke()
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( Spacer(
Intent.createChooser( modifier = Modifier
sendIntent, .weight(1f)
null )
)
) Image(
} painter = painterResource(
.padding(all = 4.dp) if (artist?.bookmarkedAt == null) {
.size(18.dp) 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( Scaffold(
@@ -195,17 +233,14 @@ fun ArtistScreen(browseId: String) {
Item(2, "Albums", R.drawable.disc) Item(2, "Albums", R.drawable.disc)
Item(3, "Singles", R.drawable.disc) Item(3, "Singles", R.drawable.disc)
Item(4, "Library", R.drawable.library) Item(4, "Library", R.drawable.library)
} },
) { currentTabIndex -> ) { currentTabIndex ->
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
when (currentTabIndex) { when (currentTabIndex) {
0 -> ArtistOverview( 0 -> ArtistOverview(
artist = artist,
youtubeArtistPage = youtubeArtist, youtubeArtistPage = youtubeArtist,
isLoading = isLoading, thumbnailContent = thumbnailContent,
isError = isError, headerContent = headerContent,
bookmarkIconContent = bookmarkIconContent,
shareIconContent = shareIconContent,
onAlbumClick = { albumRoute(it) }, onAlbumClick = { albumRoute(it) },
onViewAllSongsClick = { onTabIndexChanged(1) }, onViewAllSongsClick = { onTabIndexChanged(1) },
onViewAllAlbumsClick = { onTabIndexChanged(2) }, onViewAllAlbumsClick = { onTabIndexChanged(2) },
@@ -218,14 +253,9 @@ fun ArtistScreen(browseId: String) {
val thumbnailSizePx = thumbnailSizeDp.px val thumbnailSizePx = thumbnailSizeDp.px
ArtistContent( ArtistContent(
artist = artist, stateSaver = InnertubeSongsPageSaver,
youtubeArtistPage = youtubeArtist, headerContent = headerContent,
isLoading = isLoading, itemsPageProvider = youtubeArtist?.let {({ continuation ->
isError = isError,
stateSaver = InnertubeSongItemListSaver,
bookmarkIconContent = bookmarkIconContent,
shareIconContent = shareIconContent,
itemsPageProvider = { continuation ->
continuation?.let { continuation?.let {
Innertube.itemsPage( Innertube.itemsPage(
body = ContinuationBody(continuation = continuation), body = ContinuationBody(continuation = continuation),
@@ -233,14 +263,23 @@ fun ArtistScreen(browseId: String) {
) )
} ?: youtubeArtist } ?: youtubeArtist
?.songsEndpoint ?.songsEndpoint
?.browseId ?.takeIf { it.browseId != null }
?.let { browseId -> ?.let { endpoint ->
Innertube.itemsPage( Innertube.itemsPage(
body = BrowseBody(browseId = browseId), body = BrowseBody(
browseId = endpoint.browseId!!,
params = endpoint.params,
),
fromMusicResponsiveListItemRenderer = Innertube.SongItem::from, fromMusicResponsiveListItemRenderer = Innertube.SongItem::from,
) )
} }
}, ?: Result.success(
Innertube.ItemsPage(
items = youtubeArtist?.songs,
continuation = null
)
)
})},
itemContent = { song -> itemContent = { song ->
SongItem( SongItem(
song = song, song = song,
@@ -263,29 +302,33 @@ fun ArtistScreen(browseId: String) {
val thumbnailSizePx = thumbnailSizeDp.px val thumbnailSizePx = thumbnailSizeDp.px
ArtistContent( ArtistContent(
artist = artist, stateSaver = InnertubeAlbumsPageSaver,
youtubeArtistPage = youtubeArtist, headerContent = headerContent,
isLoading = isLoading, itemsPageProvider = youtubeArtist?.let {({ continuation ->
isError = isError,
stateSaver = InnertubeAlbumItemListSaver,
bookmarkIconContent = bookmarkIconContent,
shareIconContent = shareIconContent,
itemsPageProvider = { continuation ->
continuation?.let { continuation?.let {
Innertube.itemsPage( Innertube.itemsPage(
body = ContinuationBody(continuation = continuation), body = ContinuationBody(continuation = continuation),
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from, fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
) )
} ?: youtubeArtist } ?: youtubeArtist
?.songsEndpoint ?.albumsEndpoint
?.browseId ?.takeIf { it.browseId != null }
?.let { browseId -> ?.let { endpoint ->
Innertube.itemsPage( Innertube.itemsPage(
body = BrowseBody(browseId = browseId), body = BrowseBody(
browseId = endpoint.browseId!!,
params = endpoint.params,
),
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from, fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
) )
} }
}, ?: Result.success(
Innertube.ItemsPage(
items = youtubeArtist?.albums,
continuation = null
)
)
})},
itemContent = { album -> itemContent = { album ->
AlbumItem( AlbumItem(
album = album, album = album,
@@ -310,29 +353,33 @@ fun ArtistScreen(browseId: String) {
val thumbnailSizePx = thumbnailSizeDp.px val thumbnailSizePx = thumbnailSizeDp.px
ArtistContent( ArtistContent(
artist = artist, stateSaver = InnertubeAlbumsPageSaver,
youtubeArtistPage = youtubeArtist, headerContent = headerContent,
isLoading = isLoading, itemsPageProvider = youtubeArtist?.let {({ continuation ->
isError = isError,
stateSaver = InnertubeAlbumItemListSaver,
bookmarkIconContent = bookmarkIconContent,
shareIconContent = shareIconContent,
itemsPageProvider = { continuation ->
continuation?.let { continuation?.let {
Innertube.itemsPage( Innertube.itemsPage(
body = ContinuationBody(continuation = continuation), body = ContinuationBody(continuation = continuation),
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from, fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
) )
} ?: youtubeArtist } ?: youtubeArtist
?.songsEndpoint ?.singlesEndpoint
?.browseId ?.takeIf { it.browseId != null }
?.let { browseId -> ?.let { endpoint ->
Innertube.itemsPage( Innertube.itemsPage(
body = BrowseBody(browseId = browseId), body = BrowseBody(
browseId = endpoint.browseId!!,
params = endpoint.params,
),
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from, fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
) )
} }
}, ?: Result.success(
Innertube.ItemsPage(
items = youtubeArtist?.singles,
continuation = null
)
)
})},
itemContent = { album -> itemContent = { album ->
AlbumItem( AlbumItem(
album = album, album = album,
@@ -354,11 +401,8 @@ fun ArtistScreen(browseId: String) {
4 -> ArtistLocalSongsList( 4 -> ArtistLocalSongsList(
browseId = browseId, browseId = browseId,
artist = artist, headerContent = headerContent,
isLoading = isLoading, thumbnailContent = thumbnailContent,
isError = isError,
bookmarkIconContent = bookmarkIconContent,
shareIconContent = shareIconContent
) )
} }
} }

View File

@@ -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 <T : Innertube.Item> ArtistContent(
stateSaver: Saver<Innertube.ItemsPage<T>, List<Any?>>,
noinline itemsPageProvider: (suspend (String?) -> Result<Innertube.ItemsPage<T>?>?)? = 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()
}
}
}
}
}
}

View File

@@ -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 <T : Innertube.Item> SearchResult(
query: String,
filter: String,
stateSaver: ListSaver<T, List<Any?>>,
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<String?>())
) {
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
)
}
}
}
}
}

View File

@@ -3,21 +3,25 @@ package it.vfsfitvnm.vimusic.ui.screens.searchresult
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R 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.InnertubeArtistItemListSaver
import it.vfsfitvnm.vimusic.savers.InnertubePlaylistItemListSaver 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.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.components.themed.Scaffold
import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.albumRoute
import it.vfsfitvnm.vimusic.ui.screens.artistRoute 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.rememberPreference
import it.vfsfitvnm.vimusic.utils.searchResultScreenTabIndexKey import it.vfsfitvnm.vimusic.utils.searchResultScreenTabIndexKey
import it.vfsfitvnm.youtubemusic.Innertube 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 import it.vfsfitvnm.youtubemusic.utils.from
@ExperimentalFoundationApi @ExperimentalFoundationApi
@@ -53,6 +60,18 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
globalRoutes() globalRoutes()
host { host {
val headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit = {
Header(
title = query,
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures {
onSearchAgain()
}
}
)
}
Scaffold( Scaffold(
topIconButtonId = R.drawable.chevron_back, topIconButtonId = R.drawable.chevron_back,
onTopIconButtonClick = pop, onTopIconButtonClick = pop,
@@ -67,16 +86,6 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
Item(5, "Featured", R.drawable.playlist) Item(5, "Featured", R.drawable.playlist)
} }
) { tabIndex -> ) { 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) { saveableStateHolder.SaveableStateProvider(tabIndex) {
when (tabIndex) { when (tabIndex) {
0 -> { 0 -> {
@@ -84,12 +93,22 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
val thumbnailSizeDp = Dimensions.thumbnails.song val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSizePx = thumbnailSizeDp.px val thumbnailSizePx = thumbnailSizeDp.px
SearchResult( ArtistContent(
query = query, stateSaver = InnertubeSongsPageSaver,
filter = searchFilter, itemsPageProvider = { continuation ->
onSearchAgain = onSearchAgain, if (continuation == null) {
stateSaver = InnertubeSongItemListSaver, Innertube.searchPage(
fromMusicShelfRendererContent = Innertube.SongItem.Companion::from, 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 -> itemContent = { song ->
SongItem( SongItem(
song = song, song = song,
@@ -111,12 +130,22 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
val thumbnailSizeDp = 108.dp val thumbnailSizeDp = 108.dp
val thumbnailSizePx = thumbnailSizeDp.px val thumbnailSizePx = thumbnailSizeDp.px
SearchResult( ArtistContent(
query = query, stateSaver = InnertubeAlbumsPageSaver,
filter = searchFilter, itemsPageProvider = { continuation ->
stateSaver = InnertubeAlbumItemListSaver, if (continuation == null) {
onSearchAgain = onSearchAgain, Innertube.searchPage(
fromMusicShelfRendererContent = Innertube.AlbumItem.Companion::from, 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 -> itemContent = { album ->
AlbumItem( AlbumItem(
album = album, album = album,
@@ -141,12 +170,22 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
val thumbnailSizeDp = 64.dp val thumbnailSizeDp = 64.dp
val thumbnailSizePx = thumbnailSizeDp.px val thumbnailSizePx = thumbnailSizeDp.px
SearchResult( ArtistContent(
query = query, stateSaver = innertubeItemsPageSaver(InnertubeArtistItemListSaver),
filter = searchFilter, itemsPageProvider = { continuation ->
stateSaver = InnertubeArtistItemListSaver, if (continuation == null) {
onSearchAgain = onSearchAgain, Innertube.searchPage(
fromMusicShelfRendererContent = Innertube.ArtistItem.Companion::from, 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 -> itemContent = { artist ->
ArtistItem( ArtistItem(
artist = artist, artist = artist,
@@ -170,12 +209,22 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
val thumbnailHeightDp = 72.dp val thumbnailHeightDp = 72.dp
val thumbnailWidthDp = 128.dp val thumbnailWidthDp = 128.dp
SearchResult( ArtistContent(
query = query, stateSaver = innertubeItemsPageSaver(InnertubeVideoItemListSaver),
filter = searchFilter, itemsPageProvider = { continuation ->
stateSaver = InnertubeVideoItemListSaver, if (continuation == null) {
onSearchAgain = onSearchAgain, Innertube.searchPage(
fromMusicShelfRendererContent = Innertube.VideoItem.Companion::from, 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 -> itemContent = { video ->
VideoItem( VideoItem(
video = video, video = video,
@@ -201,12 +250,28 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
val thumbnailSizeDp = 108.dp val thumbnailSizeDp = 108.dp
val thumbnailSizePx = thumbnailSizeDp.px val thumbnailSizePx = thumbnailSizeDp.px
SearchResult( ArtistContent(
query = query, stateSaver = innertubeItemsPageSaver(InnertubePlaylistItemListSaver),
filter = searchFilter, itemsPageProvider = { continuation ->
stateSaver = InnertubePlaylistItemListSaver, if (continuation == null) {
onSearchAgain = onSearchAgain, val filter = if (tabIndex == 4) {
fromMusicShelfRendererContent = Innertube.PlaylistItem.Companion::from, 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 -> itemContent = { playlist ->
PlaylistItem( PlaylistItem(
playlist = playlist, playlist = playlist,

View File

@@ -323,12 +323,14 @@ fun AlbumItem(
) )
if (!alternative) { if (!alternative) {
BasicText( album.authors?.joinToString("") { it.name ?: "" }?.let { authorsText ->
text = album.authors?.joinToString("") { it.name ?: "" } ?: "", BasicText(
style = typography.xs.semiBold.secondary, text = authorsText,
maxLines = 2, style = typography.xs.semiBold.secondary,
overflow = TextOverflow.Ellipsis, maxLines = 2,
) overflow = TextOverflow.Ellipsis,
)
}
} }
BasicText( BasicText(

View File

@@ -6,17 +6,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.ProduceStateScope import androidx.compose.runtime.ProduceStateScope
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.experimental.ExperimentalTypeInference import kotlin.experimental.ExperimentalTypeInference
import kotlin.reflect.KProperty
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
@Composable @Composable
@@ -122,97 +119,6 @@ fun <T> produceSaveableState(
return result return result
} }
@Composable
fun <T> produceSaveableRelaunchableOneShotState(
initialValue: T,
stateSaver: Saver<T, out Any>,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): Pair<State<T>, () -> 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 <T> produceSaveableRelaunchableOneShotState(
initialValue: T,
stateSaver: Saver<T, out Any>,
key1: Any?,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): Pair<State<T>, () -> 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 <T> produceSaveableLazyOneShotState(
initialValue: T,
stateSaver: Saver<T, out Any>,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
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<T> {
override val value: T
get() {
if (!produced) {
lazyEffect()
}
return state.value
}
}
}
return delegate
}
private class ProduceSaveableStateScope<T>( private class ProduceSaveableStateScope<T>(
state: MutableState<T>, state: MutableState<T>,
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext

View File

@@ -1,24 +1,25 @@
package it.vfsfitvnm.youtubemusic.utils package it.vfsfitvnm.youtubemusic.utils
import io.ktor.utils.io.CancellationException import io.ktor.utils.io.CancellationException
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.SectionListRenderer import it.vfsfitvnm.youtubemusic.models.SectionListRenderer
internal fun SectionListRenderer.findSectionByTitle(text: String): SectionListRenderer.Content? { internal fun SectionListRenderer.findSectionByTitle(text: String): SectionListRenderer.Content? {
return contents?.find { content -> return contents?.find { content ->
val title = content val title = content
.musicCarouselShelfRenderer .musicCarouselShelfRenderer
?.header ?.header
?.musicCarouselShelfBasicHeaderRenderer ?.musicCarouselShelfBasicHeaderRenderer
?.title
?: content
.musicShelfRenderer
?.title ?.title
?: content
.musicShelfRenderer
?.title
title title
?.runs ?.runs
?.firstOrNull() ?.firstOrNull()
?.text == text ?.text == text
} }
} }
internal fun SectionListRenderer.findSectionByStrapline(text: String): SectionListRenderer.Content? { internal fun SectionListRenderer.findSectionByStrapline(text: String): SectionListRenderer.Content? {
@@ -31,14 +32,19 @@ internal fun SectionListRenderer.findSectionByStrapline(text: String): SectionLi
?.runs ?.runs
?.firstOrNull() ?.firstOrNull()
?.text == text ?.text == text
} }
} }
internal inline fun <R> runCatchingNonCancellable(block: () -> R): Result<R>? { internal inline fun <R> runCatchingNonCancellable(block: () -> R): Result<R>? {
return Result.success(block()) val result = runCatching(block)
// val result = runCatching(block) return when (result.exceptionOrNull()) {
// return when (val ex = result.exceptionOrNull()) { is CancellationException -> null
// is CancellationException -> null else -> result
// else -> result }
// }
} }
infix operator fun <T : Innertube.Item> Innertube.ItemsPage<T>?.plus(other: Innertube.ItemsPage<T>) =
other.copy(
items = this?.items?.plus(other.items ?: emptyList())?.distinctBy(Innertube.Item::key)
?: other.items
)