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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage 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
@ -62,7 +61,7 @@ fun AlbumScreen(
album?.takeIf { album?.takeIf {
album.thumbnailUrl != null album.thumbnailUrl != null
}?.let(Result.Companion::success) ?: YouTube.playlistOrAlbum(browseId) }?.let(Result.Companion::success) ?: YouTube.playlistOrAlbum(browseId)
.map { youtubeAlbum -> ?.map { youtubeAlbum ->
Album( Album(
id = browseId, id = browseId,
title = youtubeAlbum.title, title = youtubeAlbum.title,
@ -337,13 +336,9 @@ private fun LoadingOrError(
errorMessage: String? = null, errorMessage: String? = null,
onRetry: (() -> Unit)? = null onRetry: (() -> Unit)? = null
) { ) {
val colorPalette = LocalColorPalette.current LoadingOrError(
errorMessage = errorMessage,
Box { onRetry = onRetry
Column(
modifier = Modifier
.alpha(if (errorMessage == null) 1f else 0f)
.shimmer()
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
@ -354,7 +349,7 @@ private fun LoadingOrError(
) { ) {
Spacer( Spacer(
modifier = Modifier modifier = Modifier
.background(color = colorPalette.darkGray, shape = ThumbnailRoundness.shape) .background(color = LocalColorPalette.current.darkGray, shape = ThumbnailRoundness.shape)
.size(128.dp) .size(128.dp)
) )
@ -374,17 +369,4 @@ private fun LoadingOrError(
} }
} }
} }
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.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import coil.compose.AsyncImage 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
@ -35,6 +34,7 @@ import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.components.TopAppBar import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu 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.TextCard
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
@ -85,7 +85,7 @@ fun ArtistScreen(
artist?.takeIf { artist?.takeIf {
artist.shufflePlaylistId != null artist.shufflePlaylistId != null
}?.let(Result.Companion::success) ?: YouTube.artist(browseId) }?.let(Result.Companion::success) ?: YouTube.artist(browseId)
.map { youtubeArtist -> ?.map { youtubeArtist ->
Artist( Artist(
id = browseId, id = browseId,
name = youtubeArtist.name, name = youtubeArtist.name,
@ -312,12 +312,10 @@ private fun LoadingOrError(
) { ) {
val colorPalette = LocalColorPalette.current val colorPalette = LocalColorPalette.current
Box { LoadingOrError(
Column( errorMessage = errorMessage,
horizontalAlignment = Alignment.CenterHorizontally, onRetry = onRetry,
modifier = Modifier horizontalAlignment = Alignment.CenterHorizontally
.alpha(if (errorMessage == null) 1f else 0f)
.shimmer()
) { ) {
Spacer( Spacer(
modifier = Modifier modifier = Modifier
@ -339,17 +337,4 @@ private fun LoadingOrError(
) )
} }
} }
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage 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
@ -35,14 +34,12 @@ import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.SongPlaylistMap import it.vfsfitvnm.vimusic.models.SongPlaylistMap
import it.vfsfitvnm.vimusic.transaction import it.vfsfitvnm.vimusic.transaction
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState 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.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.* import it.vfsfitvnm.vimusic.ui.components.themed.*
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
import it.vfsfitvnm.vimusic.ui.views.SongItem import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.vimusic.utils.*
import it.vfsfitvnm.youtubemusic.Outcome
import it.vfsfitvnm.youtubemusic.YouTube import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -92,13 +89,13 @@ fun PlaylistScreen(
} }
} }
var playlistOrAlbum by remember { var playlist by remember {
mutableStateOf<Outcome<YouTube.PlaylistOrAlbum>>(Outcome.Loading) mutableStateOf<Result<YouTube.PlaylistOrAlbum>?>(null)
} }
val onLoad = relaunchableEffect(Unit) { val onLoad = relaunchableEffect(Unit) {
playlistOrAlbum = withContext(Dispatchers.IO) { playlist = withContext(Dispatchers.IO) {
YouTube.playlistOrAlbum2(browseId) YouTube.playlistOrAlbum(browseId)
} }
} }
@ -140,7 +137,7 @@ fun PlaylistScreen(
text = "Enqueue", text = "Enqueue",
onClick = { onClick = {
menuState.hide() menuState.hide()
playlistOrAlbum.valueOrNull?.let { album -> playlist?.getOrNull()?.let { album ->
album.items album.items
?.mapNotNull { song -> ?.mapNotNull { song ->
song.toMediaItem(browseId, album) song.toMediaItem(browseId, album)
@ -160,7 +157,7 @@ fun PlaylistScreen(
onClick = { onClick = {
menuState.hide() menuState.hide()
playlistOrAlbum.valueOrNull?.let { album -> playlist?.getOrNull()?.let { album ->
transaction { transaction {
val playlistId = val playlistId =
Database.insert( Database.insert(
@ -196,7 +193,7 @@ fun PlaylistScreen(
onClick = { onClick = {
menuState.hide() menuState.hide()
(playlistOrAlbum.valueOrNull?.url (playlist?.getOrNull()?.url
?: "https://music.youtube.com/playlist?list=${ ?: "https://music.youtube.com/playlist?list=${
browseId.removePrefix( browseId.removePrefix(
"VL" "VL"
@ -227,13 +224,7 @@ fun PlaylistScreen(
} }
item { item {
OutcomeItem( playlist?.getOrNull()?.let { playlist ->
outcome = playlistOrAlbum,
onRetry = onLoad,
onLoading = {
Loading()
}
) { playlistOrAlbum ->
Row( Row(
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier modifier = Modifier
@ -243,7 +234,7 @@ fun PlaylistScreen(
.padding(bottom = 16.dp) .padding(bottom = 16.dp)
) { ) {
AsyncImage( AsyncImage(
model = playlistOrAlbum.thumbnail?.size(thumbnailSizePx), model = playlist.thumbnail?.size(thumbnailSizePx),
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = Modifier modifier = Modifier
@ -258,19 +249,19 @@ fun PlaylistScreen(
) { ) {
Column { Column {
BasicText( BasicText(
text = playlistOrAlbum.title ?: "Unknown", text = playlist.title ?: "Unknown",
style = typography.m.semiBold style = typography.m.semiBold
) )
BasicText( BasicText(
text = buildString { text = buildString {
val authors = val authors =
playlistOrAlbum.authors?.joinToString("") { it.name } playlist.authors?.joinToString("") { it.name }
append(authors) append(authors)
if (authors?.isNotEmpty() == true && playlistOrAlbum.year != null) { if (authors?.isNotEmpty() == true && playlist.year != null) {
append("") append("")
} }
append(playlistOrAlbum.year) append(playlist.year)
}, },
style = typography.xs.secondary.semiBold, style = typography.xs.secondary.semiBold,
maxLines = 2, maxLines = 2,
@ -291,10 +282,10 @@ fun PlaylistScreen(
modifier = Modifier modifier = Modifier
.clickable { .clickable {
binder?.stopRadio() binder?.stopRadio()
playlistOrAlbum.items playlist.items
?.shuffled() ?.shuffled()
?.mapNotNull { song -> ?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum) song.toMediaItem(browseId, playlist)
} }
?.let { mediaItems -> ?.let { mediaItems ->
binder?.player?.forcePlayFromBeginning( binder?.player?.forcePlayFromBeginning(
@ -318,9 +309,9 @@ fun PlaylistScreen(
modifier = Modifier modifier = Modifier
.clickable { .clickable {
binder?.stopRadio() binder?.stopRadio()
playlistOrAlbum.items playlist.items
?.mapNotNull { song -> ?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum) song.toMediaItem(browseId, playlist)
} }
?.let { mediaItems -> ?.let { mediaItems ->
binder?.player?.forcePlayFromBeginning( binder?.player?.forcePlayFromBeginning(
@ -339,22 +330,27 @@ fun PlaylistScreen(
} }
} }
} }
} } ?: playlist?.exceptionOrNull()?.let { throwable ->
LoadingOrError(
errorMessage = throwable.javaClass.canonicalName,
onRetry = onLoad
)
} ?: LoadingOrError()
} }
itemsIndexed( itemsIndexed(
items = playlistOrAlbum.valueOrNull?.items ?: emptyList(), items = playlist?.getOrNull()?.items ?: emptyList(),
contentType = { _, song -> song } contentType = { _, song -> song }
) { index, song -> ) { index, song ->
SongItem( SongItem(
title = song.info.name, title = song.info.name,
authors = (song.authors authors = (song.authors
?: playlistOrAlbum.valueOrNull?.authors)?.joinToString("") { it.name }, ?: playlist?.getOrNull()?.authors)?.joinToString("") { it.name },
durationText = song.durationText, durationText = song.durationText,
onClick = { onClick = {
binder?.stopRadio() binder?.stopRadio()
playlistOrAlbum.valueOrNull?.items?.mapNotNull { song -> playlist?.getOrNull()?.items?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!) song.toMediaItem(browseId, playlist?.getOrNull()!!)
}?.let { mediaItems -> }?.let { mediaItems ->
binder?.player?.forcePlayAtIndex(mediaItems, index) binder?.player?.forcePlayAtIndex(mediaItems, index)
} }
@ -384,7 +380,7 @@ fun PlaylistScreen(
NonQueuedMediaItemMenu( NonQueuedMediaItemMenu(
mediaItem = song.toMediaItem( mediaItem = song.toMediaItem(
browseId, browseId,
playlistOrAlbum.valueOrNull!! playlist?.getOrNull()!!
) )
?: return@SongItem, ?: return@SongItem,
onDismiss = menuState::hide, onDismiss = menuState::hide,
@ -397,15 +393,16 @@ fun PlaylistScreen(
} }
} }
@Composable @Composable
private fun Loading() { private fun LoadingOrError(
errorMessage: String? = null,
onRetry: (() -> Unit)? = null
) {
val colorPalette = LocalColorPalette.current val colorPalette = LocalColorPalette.current
Column( LoadingOrError(
modifier = Modifier errorMessage = errorMessage,
.shimmer() onRetry = onRetry
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),

View file

@ -653,11 +653,11 @@ object YouTube {
class Lyrics( class Lyrics(
val browseId: String?, val browseId: String?,
) { ) {
suspend fun text(): Result<String?> { suspend fun text(): Result<String?>? {
return if (browseId == null) { return if (browseId == null) {
Result.success(null) Result.success(null)
} else { } else {
browse2(browseId).map { body -> browse2(browseId)?.map { body ->
body.contents body.contents
.sectionListRenderer .sectionListRenderer
?.contents ?.contents
@ -689,8 +689,8 @@ object YouTube {
}.bodyCatching() }.bodyCatching()
} }
suspend fun browse2(browseId: String): Result<BrowseResponse> { suspend fun browse2(browseId: String): Result<BrowseResponse>? {
return runCatching { return runCatching<YouTube, BrowseResponse> {
client.post("/youtubei/v1/browse") { client.post("/youtubei/v1/browse") {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
setBody( setBody(
@ -702,7 +702,7 @@ object YouTube {
parameter("key", Key) parameter("key", Key)
parameter("prettyPrint", false) parameter("prettyPrint", false)
}.body() }.body()
} }.recoverIfCancelled()
} }
open class PlaylistOrAlbum( open class PlaylistOrAlbum(
@ -723,115 +723,8 @@ object YouTube {
) )
} }
suspend fun playlistOrAlbum(browseId: String): Result<PlaylistOrAlbum> { suspend fun playlistOrAlbum(browseId: String): Result<PlaylistOrAlbum>? {
return browse2(browseId).map { body -> 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 ->
PlaylistOrAlbum( PlaylistOrAlbum(
title = body title = body
.header .header
@ -945,8 +838,8 @@ object YouTube {
val radioEndpoint: NavigationEndpoint.Endpoint.Watch? val radioEndpoint: NavigationEndpoint.Endpoint.Watch?
) )
suspend fun artist(browseId: String): Result<Artist> { suspend fun artist(browseId: String): Result<Artist>? {
return browse2(browseId).map { body -> return browse2(browseId)?.map { body ->
Artist( Artist(
name = body name = body
.header .header