Tweak code
This commit is contained in:
@@ -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>>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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?
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user