mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
fetch full-res artist art and match the old artist layout
This commit is contained in:
@@ -65,6 +65,15 @@ class SourceRepository(
|
||||
|
||||
fun streamUri(songId: String): String? = active?.streamUri(songId)?.toString()
|
||||
|
||||
suspend fun artistArt(
|
||||
artistId: String,
|
||||
thumbnail: Boolean = false,
|
||||
): CoverArtRef? {
|
||||
val source = active ?: return null
|
||||
val url = source.artistArtUri(artistId, thumbnail) ?: return null
|
||||
return CoverArtRef(url = url, cacheKey = "${source.id}:artist:$artistId:$thumbnail")
|
||||
}
|
||||
|
||||
suspend fun addSource(
|
||||
name: String,
|
||||
address: String,
|
||||
|
||||
@@ -93,6 +93,16 @@ class SubsonicSource(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun artistArtUri(
|
||||
artistId: String,
|
||||
thumbnail: Boolean = false,
|
||||
): String? =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
SubsonicXml.artistImageUrl(client.get("getArtistInfo2", mapOf("id" to artistId)), thumbnail)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun fetchRanks(): Pair<Map<String, Long>, Map<String, Long>> = fetchRank("frequent") to fetchRank("recent")
|
||||
|
||||
private fun fetchRank(type: String): Map<String, Long> {
|
||||
|
||||
@@ -109,6 +109,25 @@ object SubsonicXml {
|
||||
position = index.toLong(),
|
||||
)
|
||||
}
|
||||
|
||||
fun artistImageUrl(
|
||||
document: Document,
|
||||
thumbnail: Boolean,
|
||||
): String? {
|
||||
val tag = if (thumbnail) "smallImageUrl" else "largeImageUrl"
|
||||
return document
|
||||
.elements("artistInfo2")
|
||||
.firstOrNull()
|
||||
?.elements(tag)
|
||||
?.firstOrNull()
|
||||
?.textContent
|
||||
?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Element.elements(tag: String): List<Element> {
|
||||
val nodes = getElementsByTagName(tag)
|
||||
return (0 until nodes.length).mapNotNull { nodes.item(it) as? Element }
|
||||
}
|
||||
|
||||
private fun Document.elements(tag: String): List<Element> {
|
||||
|
||||
@@ -2,19 +2,17 @@ package com.subtracks.ui.library
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
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.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
@@ -27,7 +25,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -49,9 +46,11 @@ fun ArtistDetailRoute(
|
||||
) {
|
||||
val artist by viewModel.artist.collectAsStateWithLifecycle(initialValue = null)
|
||||
val albums by viewModel.albums.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val art by viewModel.art.collectAsStateWithLifecycle()
|
||||
ArtistDetailScreen(
|
||||
artist = artist,
|
||||
albums = albums,
|
||||
art = art,
|
||||
coverArt = viewModel::coverArt,
|
||||
onBack = onBack,
|
||||
onAlbumClick = onAlbumClick,
|
||||
@@ -63,6 +62,7 @@ fun ArtistDetailRoute(
|
||||
fun ArtistDetailScreen(
|
||||
artist: Artist?,
|
||||
albums: List<Album>,
|
||||
art: CoverArtRef?,
|
||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||
onBack: () -> Unit,
|
||||
onAlbumClick: (Album) -> Unit,
|
||||
@@ -72,7 +72,14 @@ fun ArtistDetailScreen(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(artist?.name.orEmpty(), maxLines = 1) },
|
||||
title = {
|
||||
Text(
|
||||
text = artist?.name.orEmpty(),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back")
|
||||
@@ -81,50 +88,50 @@ fun ArtistDetailScreen(
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.padding(padding).fillMaxSize(),
|
||||
) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
BoxWithConstraints(Modifier.padding(padding).fillMaxSize()) {
|
||||
val artHeight = maxHeight * 0.3f
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
CoverArt(
|
||||
ref = art,
|
||||
name = artist?.name.orEmpty(),
|
||||
modifier = Modifier.fillMaxWidth().height(artHeight),
|
||||
)
|
||||
Text(
|
||||
text = "${albums.size} albums",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
)
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
) {
|
||||
CoverArt(
|
||||
ref = coverArt(artist?.coverArt, false),
|
||||
name = artist?.name.orEmpty(),
|
||||
modifier = Modifier.size(120.dp).clip(CircleShape),
|
||||
)
|
||||
Column {
|
||||
Text(artist?.name.orEmpty(), style = MaterialTheme.typography.titleLarge)
|
||||
Text(
|
||||
text = "${albums.size} albums",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
items(albums, key = { it.id }) { album ->
|
||||
Column(Modifier.clickable { onAlbumClick(album) }) {
|
||||
CoverArt(
|
||||
ref = coverArt(album.coverArt, false),
|
||||
name = album.name,
|
||||
modifier = Modifier.fillMaxWidth().aspectRatio(1f).clip(RoundedCornerShape(2.dp)),
|
||||
)
|
||||
Text(
|
||||
text = album.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(top = 6.dp),
|
||||
)
|
||||
Text(
|
||||
text = album.year?.takeIf { it > 0 }?.toString() ?: "\u00A0",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
items(albums, key = { it.id }) { album ->
|
||||
Column(Modifier.clickable { onAlbumClick(album) }) {
|
||||
CoverArt(
|
||||
ref = coverArt(album.coverArt, true),
|
||||
name = album.name,
|
||||
modifier = Modifier.fillMaxWidth().aspectRatio(1f).clip(RoundedCornerShape(2.dp)),
|
||||
)
|
||||
Text(
|
||||
text = album.name,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.subtracks.ui.library
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.CoverArtRef
|
||||
@@ -8,8 +9,12 @@ import com.subtracks.data.repo.LibraryRepository
|
||||
import com.subtracks.data.repo.SourceRepository
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ArtistDetailViewModel(
|
||||
@@ -23,6 +28,16 @@ class ArtistDetailViewModel(
|
||||
|
||||
val albums: Flow<List<Album>> = sourceId.flatMapLatest { libraryRepository.artistAlbums(it, artistId) }
|
||||
|
||||
private val _art = MutableStateFlow<CoverArtRef?>(null)
|
||||
val art: StateFlow<CoverArtRef?> = _art
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
val loaded = artist.filterNotNull().first()
|
||||
_art.value = sourceRepository.artistArt(loaded.id) ?: sourceRepository.coverArt(loaded.coverArt)
|
||||
}
|
||||
}
|
||||
|
||||
fun coverArt(
|
||||
coverArt: String?,
|
||||
thumbnail: Boolean,
|
||||
|
||||
Reference in New Issue
Block a user