Continue removing Outcome class in favor of Result

This commit is contained in:
vfsfitvnm 2022-07-01 18:51:01 +02:00
parent f7012c9134
commit 21ef7e8d5e
4 changed files with 90 additions and 233 deletions

View file

@ -28,7 +28,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
@ -62,7 +61,7 @@ fun AlbumScreen(
album?.takeIf {
album.thumbnailUrl != null
}?.let(Result.Companion::success) ?: YouTube.playlistOrAlbum(browseId)
.map { youtubeAlbum ->
?.map { youtubeAlbum ->
Album(
id = browseId,
title = youtubeAlbum.title,
@ -337,54 +336,37 @@ private fun LoadingOrError(
errorMessage: String? = null,
onRetry: (() -> Unit)? = null
) {
val colorPalette = LocalColorPalette.current
Box {
Column(
LoadingOrError(
errorMessage = errorMessage,
onRetry = onRetry
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.alpha(if (errorMessage == null) 1f else 0f)
.shimmer()
.height(IntrinsicSize.Max)
.padding(vertical = 8.dp, horizontal = 16.dp)
.padding(bottom = 16.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
Spacer(
modifier = Modifier
.height(IntrinsicSize.Max)
.padding(vertical = 8.dp, horizontal = 16.dp)
.padding(bottom = 16.dp)
.background(color = LocalColorPalette.current.darkGray, shape = ThumbnailRoundness.shape)
.size(128.dp)
)
Column(
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxHeight()
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.darkGray, shape = ThumbnailRoundness.shape)
.size(128.dp)
)
Column {
TextPlaceholder()
Column(
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxHeight()
) {
Column {
TextPlaceholder()
TextPlaceholder(
modifier = Modifier
.alpha(0.7f)
)
}
TextPlaceholder(
modifier = Modifier
.alpha(0.7f)
)
}
}
}
errorMessage?.let {
TextCard(
icon = R.drawable.alert_circle,
onClick = onRetry,
modifier = Modifier
.align(Alignment.Center)
) {
Title(text = onRetry?.let { "Tap to retry" } ?: "Error")
Text(text = "An error has occurred:\n$errorMessage")
}
}
}
}

View file

@ -25,7 +25,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import coil.compose.AsyncImage
import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
@ -35,6 +34,7 @@ import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
import it.vfsfitvnm.vimusic.ui.components.themed.TextCard
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
@ -85,7 +85,7 @@ fun ArtistScreen(
artist?.takeIf {
artist.shufflePlaylistId != null
}?.let(Result.Companion::success) ?: YouTube.artist(browseId)
.map { youtubeArtist ->
?.map { youtubeArtist ->
Artist(
id = browseId,
name = youtubeArtist.name,
@ -312,44 +312,29 @@ private fun LoadingOrError(
) {
val colorPalette = LocalColorPalette.current
Box {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
LoadingOrError(
errorMessage = errorMessage,
onRetry = onRetry,
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(
modifier = Modifier
.alpha(if (errorMessage == null) 1f else 0f)
.shimmer()
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.darkGray, shape = CircleShape)
.size(192.dp)
)
.background(color = colorPalette.darkGray, shape = CircleShape)
.size(192.dp)
)
TextPlaceholder(
modifier = Modifier
.alpha(0.9f)
.padding(vertical = 8.dp, horizontal = 16.dp)
)
repeat(3) {
TextPlaceholder(
modifier = Modifier
.alpha(0.9f)
.padding(vertical = 8.dp, horizontal = 16.dp)
.alpha(0.8f)
.padding(horizontal = 16.dp)
)
repeat(3) {
TextPlaceholder(
modifier = Modifier
.alpha(0.8f)
.padding(horizontal = 16.dp)
)
}
}
errorMessage?.let {
TextCard(
icon = R.drawable.alert_circle,
onClick = onRetry,
modifier = Modifier
.align(Alignment.Center)
) {
Title(text = onRetry?.let { "Tap to retry" } ?: "Error")
Text(text = "An error has occurred:\n$errorMessage")
}
}
}
}

View file

@ -25,7 +25,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
@ -35,14 +34,12 @@ import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
import it.vfsfitvnm.vimusic.transaction
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.OutcomeItem
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.*
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.*
import it.vfsfitvnm.youtubemusic.Outcome
import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -92,13 +89,13 @@ fun PlaylistScreen(
}
}
var playlistOrAlbum by remember {
mutableStateOf<Outcome<YouTube.PlaylistOrAlbum>>(Outcome.Loading)
var playlist by remember {
mutableStateOf<Result<YouTube.PlaylistOrAlbum>?>(null)
}
val onLoad = relaunchableEffect(Unit) {
playlistOrAlbum = withContext(Dispatchers.IO) {
YouTube.playlistOrAlbum2(browseId)
playlist = withContext(Dispatchers.IO) {
YouTube.playlistOrAlbum(browseId)
}
}
@ -140,7 +137,7 @@ fun PlaylistScreen(
text = "Enqueue",
onClick = {
menuState.hide()
playlistOrAlbum.valueOrNull?.let { album ->
playlist?.getOrNull()?.let { album ->
album.items
?.mapNotNull { song ->
song.toMediaItem(browseId, album)
@ -160,7 +157,7 @@ fun PlaylistScreen(
onClick = {
menuState.hide()
playlistOrAlbum.valueOrNull?.let { album ->
playlist?.getOrNull()?.let { album ->
transaction {
val playlistId =
Database.insert(
@ -196,7 +193,7 @@ fun PlaylistScreen(
onClick = {
menuState.hide()
(playlistOrAlbum.valueOrNull?.url
(playlist?.getOrNull()?.url
?: "https://music.youtube.com/playlist?list=${
browseId.removePrefix(
"VL"
@ -227,13 +224,7 @@ fun PlaylistScreen(
}
item {
OutcomeItem(
outcome = playlistOrAlbum,
onRetry = onLoad,
onLoading = {
Loading()
}
) { playlistOrAlbum ->
playlist?.getOrNull()?.let { playlist ->
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
@ -243,7 +234,7 @@ fun PlaylistScreen(
.padding(bottom = 16.dp)
) {
AsyncImage(
model = playlistOrAlbum.thumbnail?.size(thumbnailSizePx),
model = playlist.thumbnail?.size(thumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
@ -258,19 +249,19 @@ fun PlaylistScreen(
) {
Column {
BasicText(
text = playlistOrAlbum.title ?: "Unknown",
text = playlist.title ?: "Unknown",
style = typography.m.semiBold
)
BasicText(
text = buildString {
val authors =
playlistOrAlbum.authors?.joinToString("") { it.name }
playlist.authors?.joinToString("") { it.name }
append(authors)
if (authors?.isNotEmpty() == true && playlistOrAlbum.year != null) {
if (authors?.isNotEmpty() == true && playlist.year != null) {
append("")
}
append(playlistOrAlbum.year)
append(playlist.year)
},
style = typography.xs.secondary.semiBold,
maxLines = 2,
@ -291,10 +282,10 @@ fun PlaylistScreen(
modifier = Modifier
.clickable {
binder?.stopRadio()
playlistOrAlbum.items
playlist.items
?.shuffled()
?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum)
song.toMediaItem(browseId, playlist)
}
?.let { mediaItems ->
binder?.player?.forcePlayFromBeginning(
@ -318,9 +309,9 @@ fun PlaylistScreen(
modifier = Modifier
.clickable {
binder?.stopRadio()
playlistOrAlbum.items
playlist.items
?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum)
song.toMediaItem(browseId, playlist)
}
?.let { mediaItems ->
binder?.player?.forcePlayFromBeginning(
@ -339,22 +330,27 @@ fun PlaylistScreen(
}
}
}
}
} ?: playlist?.exceptionOrNull()?.let { throwable ->
LoadingOrError(
errorMessage = throwable.javaClass.canonicalName,
onRetry = onLoad
)
} ?: LoadingOrError()
}
itemsIndexed(
items = playlistOrAlbum.valueOrNull?.items ?: emptyList(),
items = playlist?.getOrNull()?.items ?: emptyList(),
contentType = { _, song -> song }
) { index, song ->
SongItem(
title = song.info.name,
authors = (song.authors
?: playlistOrAlbum.valueOrNull?.authors)?.joinToString("") { it.name },
?: playlist?.getOrNull()?.authors)?.joinToString("") { it.name },
durationText = song.durationText,
onClick = {
binder?.stopRadio()
playlistOrAlbum.valueOrNull?.items?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!)
playlist?.getOrNull()?.items?.mapNotNull { song ->
song.toMediaItem(browseId, playlist?.getOrNull()!!)
}?.let { mediaItems ->
binder?.player?.forcePlayAtIndex(mediaItems, index)
}
@ -384,7 +380,7 @@ fun PlaylistScreen(
NonQueuedMediaItemMenu(
mediaItem = song.toMediaItem(
browseId,
playlistOrAlbum.valueOrNull!!
playlist?.getOrNull()!!
)
?: return@SongItem,
onDismiss = menuState::hide,
@ -397,15 +393,16 @@ fun PlaylistScreen(
}
}
@Composable
private fun Loading() {
private fun LoadingOrError(
errorMessage: String? = null,
onRetry: (() -> Unit)? = null
) {
val colorPalette = LocalColorPalette.current
Column(
modifier = Modifier
.shimmer()
LoadingOrError(
errorMessage = errorMessage,
onRetry = onRetry
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
@ -471,4 +468,4 @@ private fun Loading() {
}
}
}
}
}

View file

@ -653,11 +653,11 @@ object YouTube {
class Lyrics(
val browseId: String?,
) {
suspend fun text(): Result<String?> {
suspend fun text(): Result<String?>? {
return if (browseId == null) {
Result.success(null)
} else {
browse2(browseId).map { body ->
browse2(browseId)?.map { body ->
body.contents
.sectionListRenderer
?.contents
@ -689,8 +689,8 @@ object YouTube {
}.bodyCatching()
}
suspend fun browse2(browseId: String): Result<BrowseResponse> {
return runCatching {
suspend fun browse2(browseId: String): Result<BrowseResponse>? {
return runCatching<YouTube, BrowseResponse> {
client.post("/youtubei/v1/browse") {
contentType(ContentType.Application.Json)
setBody(
@ -702,7 +702,7 @@ object YouTube {
parameter("key", Key)
parameter("prettyPrint", false)
}.body()
}
}.recoverIfCancelled()
}
open class PlaylistOrAlbum(
@ -723,115 +723,8 @@ object YouTube {
)
}
suspend fun playlistOrAlbum(browseId: String): Result<PlaylistOrAlbum> {
return browse2(browseId).map { body ->
PlaylistOrAlbum(
title = body
.header
?.musicDetailHeaderRenderer
?.title
?.text,
thumbnail = body
.header
?.musicDetailHeaderRenderer
?.thumbnail
?.musicThumbnailRenderer
?.thumbnail
?.thumbnails
?.firstOrNull(),
authors = body
.header
?.musicDetailHeaderRenderer
?.subtitle
?.splitBySeparator()
?.getOrNull(1)
?.map { Info.from(it) },
year = body
.header
?.musicDetailHeaderRenderer
?.subtitle
?.splitBySeparator()
?.getOrNull(2)
?.firstOrNull()
?.text,
items = body
.contents
.singleColumnBrowseResultsRenderer
?.tabs
?.firstOrNull()
?.tabRenderer
?.content
?.sectionListRenderer
?.contents
?.firstOrNull()
?.musicShelfRenderer
?.contents
?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
?.mapNotNull { renderer ->
PlaylistOrAlbum.Item(
info = renderer
.flexColumns
.getOrNull(0)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.getOrNull(0)
?.let { Info.from(it) } ?: return@mapNotNull null,
authors = renderer
.flexColumns
.getOrNull(1)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.map { Info.from<NavigationEndpoint.Endpoint.Browse>(it) }
?.takeIf { it.isNotEmpty() },
durationText = renderer
.fixedColumns
?.getOrNull(0)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.getOrNull(0)
?.text,
album = renderer
.flexColumns
.getOrNull(2)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.firstOrNull()
?.let { Info.from(it) },
thumbnail = renderer
.thumbnail
?.musicThumbnailRenderer
?.thumbnail
?.thumbnails
?.firstOrNull()
)
}
?.filter { it.info.endpoint != null },
url = body
.microformat
?.microformatDataRenderer
?.urlCanonical,
continuation = body
.contents
.singleColumnBrowseResultsRenderer
?.tabs
?.firstOrNull()
?.tabRenderer
?.content
?.sectionListRenderer
?.continuations
?.firstOrNull()
?.nextRadioContinuationData
?.continuation
)
}
}
suspend fun playlistOrAlbum2(browseId: String): Outcome<PlaylistOrAlbum> {
return browse(browseId).map { body ->
suspend fun playlistOrAlbum(browseId: String): Result<PlaylistOrAlbum>? {
return browse2(browseId)?.map { body ->
PlaylistOrAlbum(
title = body
.header
@ -945,8 +838,8 @@ object YouTube {
val radioEndpoint: NavigationEndpoint.Endpoint.Watch?
)
suspend fun artist(browseId: String): Result<Artist> {
return browse2(browseId).map { body ->
suspend fun artist(browseId: String): Result<Artist>? {
return browse2(browseId)?.map { body ->
Artist(
name = body
.header