diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index 4faf91c..68db7f2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -110,6 +110,11 @@ interface Database { @Query("SELECT thumbnailUrl FROM Song JOIN SongInPlaylist ON id = songId WHERE playlistId = :id ORDER BY position LIMIT 4") fun playlistThumbnailUrls(id: Long): Flow> + + @Transaction + @RewriteQueriesToDropUnusedColumns + @Query("SELECT * FROM Info JOIN SongWithAuthors ON Info.id = SongWithAuthors.authorInfoId JOIN Song ON SongWithAuthors.songId = Song.id WHERE browseId = :artistId ORDER BY Song.ROWID DESC") + fun artistSongs(artistId: String): Flow> } @androidx.room.Database( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt index d4b073f..dfe31d6 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt @@ -1,9 +1,14 @@ package it.vfsfitvnm.vimusic.ui.screens +import android.os.Bundle import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.* +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.* @@ -17,18 +22,22 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign 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.R +import it.vfsfitvnm.vimusic.models.SongWithInfo import it.vfsfitvnm.vimusic.services.StartArtistRadioCommand -import it.vfsfitvnm.vimusic.ui.components.ExpandableText -import it.vfsfitvnm.vimusic.ui.components.Message +import it.vfsfitvnm.vimusic.services.StopRadioCommand import it.vfsfitvnm.vimusic.ui.components.OutcomeItem import it.vfsfitvnm.vimusic.ui.components.TopAppBar +import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder 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 @@ -41,7 +50,7 @@ import kotlinx.coroutines.withContext fun ArtistScreen( browseId: String, ) { - val scrollState = rememberScrollState() + val lazyListState = rememberLazyListState() var artist by remember { mutableStateOf>(Outcome.Loading) @@ -81,107 +90,196 @@ fun ArtistScreen( } } - Column( + val songThumbnailSizePx = remember { + density.run { + 54.dp.roundToPx() + } + } + + val songs by remember(browseId) { + Database.artistSongs(browseId) + }.collectAsState(initial = emptyList(), context = Dispatchers.IO) + + LazyColumn( + state = lazyListState, + contentPadding = PaddingValues(bottom = 72.dp), horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .background(colorPalette.background) .fillMaxSize() - .verticalScroll(scrollState) - .padding(bottom = 72.dp) ) { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), + item { + TopAppBar( modifier = Modifier - .clickable(onClick = pop) - .padding(vertical = 8.dp) - .padding(horizontal = 16.dp) - .size(24.dp) - ) + .height(52.dp) + ) { + Image( + painter = painterResource(R.drawable.chevron_back), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable(onClick = pop) + .padding(vertical = 8.dp) + .padding(horizontal = 16.dp) + .size(24.dp) + ) + } } - OutcomeItem( - outcome = artist, - onRetry = onLoad, - onLoading = { - Loading() + item { + OutcomeItem( + outcome = artist, + onRetry = onLoad, + onLoading = { + Loading() + } + ) { artist -> + AsyncImage( + model = artist.thumbnail?.size(thumbnailSizePx), + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .size(thumbnailSizeDp) + ) + + BasicText( + text = artist.name, + style = typography.l.semiBold, + modifier = Modifier + .padding(vertical = 8.dp, horizontal = 16.dp) + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Image( + painter = painterResource(R.drawable.shuffle), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + player?.mediaController?.sendCustomCommand( + StartArtistRadioCommand, + artist.shuffleEndpoint.asBundle + ) + } + .shadow(elevation = 2.dp, shape = CircleShape) + .background( + color = colorPalette.elevatedBackground, + shape = CircleShape + ) + .padding(horizontal = 16.dp, vertical = 16.dp) + .size(20.dp) + ) + + Image( + painter = painterResource(R.drawable.radio), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + player?.mediaController?.sendCustomCommand( + StartArtistRadioCommand, + artist.radioEndpoint.asBundle + ) + } + .shadow(elevation = 2.dp, shape = CircleShape) + .background( + color = colorPalette.elevatedBackground, + shape = CircleShape + ) + .padding(horizontal = 16.dp, vertical = 16.dp) + .size(20.dp) + ) + } + + } - ) { artist -> - AsyncImage( - model = artist.thumbnail?.size(thumbnailSizePx), - contentDescription = null, - modifier = Modifier - .clip(CircleShape) - .size(thumbnailSizeDp) + } - ) - - BasicText( - text = artist.name, - style = typography.l.semiBold, - modifier = Modifier - .padding(vertical = 8.dp, horizontal = 16.dp) - ) + item { + if (songs.isEmpty()) return@item Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier - .padding(horizontal = 16.dp, vertical = 8.dp) + .zIndex(1f) + .fillMaxWidth() + .padding(horizontal = 8.dp) + .padding(top = 32.dp) ) { + BasicText( + text = "Local tracks", + style = typography.m.semiBold, + modifier = Modifier + .padding(horizontal = 8.dp) + ) + Image( painter = painterResource(R.drawable.shuffle), contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier - .clickable { - player?.mediaController?.sendCustomCommand(StartArtistRadioCommand, artist.shuffleEndpoint.asBundle) + .clickable(enabled = songs.isNotEmpty()) { + player?.mediaController?.let { + it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY) + it.forcePlayFromBeginning( + songs + .shuffled() + .map(SongWithInfo::asMediaItem) + ) + } } - .shadow(elevation = 2.dp, shape = CircleShape) - .background(color = colorPalette.elevatedBackground, shape = CircleShape) - .padding(horizontal = 16.dp, vertical = 16.dp) - .size(20.dp) - ) - - Image( - painter = painterResource(R.drawable.radio), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { - player?.mediaController?.sendCustomCommand(StartArtistRadioCommand, artist.radioEndpoint.asBundle) - } - .shadow(elevation = 2.dp, shape = CircleShape) - .background(color = colorPalette.elevatedBackground, shape = CircleShape) - .padding(horizontal = 16.dp, vertical = 16.dp) + .padding(horizontal = 8.dp, vertical = 8.dp) .size(20.dp) ) } + } - artist.description?.let { description -> - ExpandableText( - text = description, - style = typography.xxs.secondary.align(TextAlign.Justify), - minimizedMaxLines = 4, - backgroundColor = colorPalette.background, - showMoreTextStyle = typography.xxs.bold, - modifier = Modifier - .animateContentSize() - .padding(horizontal = 16.dp) - ) - } - - Message( - text = "Page under construction", - icon = R.drawable.sad, - modifier = Modifier - .padding(vertical = 64.dp) + itemsIndexed( + items = songs, + key = { _, song -> song.song.id }, + contentType = { _, song -> song }, + ) { index, song -> + SongItem( + song = song, + thumbnailSize = songThumbnailSizePx, + onClick = { + player?.mediaController?.let { + it.sendCustomCommand(StopRadioCommand, Bundle.EMPTY) + it.forcePlayAtIndex(songs.map(SongWithInfo::asMediaItem), index) + } + }, + menuContent = { + InHistoryMediaItemMenu(song = song) + } ) } + + artist.valueOrNull?.description?.let { description -> + item { + Column( + modifier = Modifier + .padding(top = 32.dp) + .padding(horizontal = 16.dp, vertical = 16.dp) + .background(colorPalette.lightBackground) + .padding(horizontal = 16.dp, vertical = 16.dp) + ) { + BasicText( + text = "Information", + style = typography.xxs.semiBold + ) + + BasicText( + text = description, + style = typography.xxs.secondary.align(TextAlign.Justify) + ) + } + } + } } } } diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt index 3a7de43..2d223df 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt +++ b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt @@ -820,7 +820,8 @@ object YouTube { .header ?.musicImmersiveHeaderRenderer ?.description - ?.text, + ?.text + ?.substringBeforeLast("\n\nFrom Wikipedia"), thumbnail = body .header ?.musicImmersiveHeaderRenderer