address artist detail review findings

This commit is contained in:
austinried
2026-10-04 00:29:52 +00:00
parent 754323bfdc
commit a78d093042
3 changed files with 54 additions and 13 deletions
@@ -6,6 +6,7 @@ import com.subtracks.data.model.Playlist
import com.subtracks.data.model.PlaylistSong import com.subtracks.data.model.PlaylistSong
import com.subtracks.data.model.Song import com.subtracks.data.model.Song
import com.subtracks.data.source.MusicSource import com.subtracks.data.source.MusicSource
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -98,9 +99,13 @@ class SubsonicSource(
thumbnail: Boolean = false, thumbnail: Boolean = false,
): String? = ): String? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
runCatching { try {
SubsonicXml.artistImageUrl(client.get("getArtistInfo2", mapOf("id" to artistId)), thumbnail) SubsonicXml.artistImageUrl(client.get("getArtistInfo2", mapOf("id" to artistId)), thumbnail)
}.getOrNull() } catch (e: CancellationException) {
throw e
} catch (_: Exception) {
null
}
} }
private fun fetchRanks(): Pair<Map<String, Long>, Map<String, Long>> = fetchRank("frequent") to fetchRank("recent") private fun fetchRanks(): Pair<Map<String, Long>, Map<String, Long>> = fetchRank("frequent") to fetchRank("recent")
@@ -2,6 +2,7 @@ package com.subtracks.ui.library
import androidx.compose.foundation.background 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.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -11,6 +12,7 @@ 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.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
@@ -94,16 +96,30 @@ fun ArtistDetailScreen(
.toDp() .toDp()
} }
val statusBarTop = with(density) { WindowInsets.statusBars.getTop(this).toDp() } val statusBarTop = with(density) { WindowInsets.statusBars.getTop(this).toDp() }
val navBarBottom = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
val barHeight = statusBarTop + TopAppBarDefaults.TopAppBarExpandedHeight val barHeight = statusBarTop + TopAppBarDefaults.TopAppBarExpandedHeight
val imageTitleTop = ART_HEIGHT - TITLE_INSET - lineHeight val imageTitleTop = ART_HEIGHT - TITLE_INSET - lineHeight
val fadeStartPx = with(density) { (imageTitleTop - barHeight - FADE_LEAD).toPx() } val fadeStartPx = with(density) { (imageTitleTop - barHeight - FADE_LEAD).toPx() }
val fadeEndPx = with(density) { (imageTitleTop + lineHeight / 2 - barHeight).toPx() } val fadeEndPx = with(density) { (imageTitleTop + lineHeight / 2 - barHeight).toPx() }
val barFraction by remember { val barFraction by remember(fadeStartPx, fadeEndPx) {
derivedStateOf { derivedStateOf {
if (listState.firstVisibleItemIndex > 0) { when {
listState.layoutInfo.visibleItemsInfo.isEmpty() -> {
0f
}
!listState.canScrollForward -> {
1f 1f
} else { }
((listState.firstVisibleItemScrollOffset - fadeStartPx) / (fadeEndPx - fadeStartPx)).coerceIn(0f, 1f)
listState.firstVisibleItemIndex > 0 -> {
1f
}
else -> {
((listState.firstVisibleItemScrollOffset - fadeStartPx) / (fadeEndPx - fadeStartPx))
.coerceIn(0f, 1f)
}
} }
} }
} }
@@ -118,11 +134,11 @@ fun ArtistDetailScreen(
), ),
) )
Box(modifier.fillMaxSize()) { Box(modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {
LazyVerticalGrid( LazyVerticalGrid(
state = listState, state = listState,
columns = GridCells.Fixed(2), columns = GridCells.Fixed(2),
contentPadding = PaddingValues(bottom = 16.dp), contentPadding = PaddingValues(bottom = 16.dp + navBarBottom),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -207,7 +223,14 @@ fun ArtistDetailScreen(
} }
}, },
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
modifier = Modifier.align(Alignment.TopStart).background(Color.Black.copy(alpha = barFraction)), modifier =
Modifier
.align(Alignment.TopStart)
.background(Color.Black.copy(alpha = barFraction))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
) {},
) )
} }
} }
@@ -8,11 +8,13 @@ import com.subtracks.data.model.CoverArtRef
import com.subtracks.data.repo.LibraryRepository import com.subtracks.data.repo.LibraryRepository
import com.subtracks.data.repo.SourceRepository import com.subtracks.data.repo.SourceRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -33,11 +35,22 @@ class ArtistDetailViewModel(
init { init {
viewModelScope.launch { viewModelScope.launch {
val loaded = artist.filterNotNull().first() artist
_art.value = sourceRepository.artistArt(loaded.id) ?: sourceRepository.coverArt(loaded.coverArt) .filterNotNull()
.distinctUntilChangedBy { it.id }
.collectLatest { loaded -> _art.value = resolveArt(loaded) }
} }
} }
private suspend fun resolveArt(artist: Artist): CoverArtRef? {
repeat(3) { attempt ->
val ref = sourceRepository.artistArt(artist.id) ?: sourceRepository.coverArt(artist.coverArt)
if (ref != null) return ref
if (attempt < 2) delay(300)
}
return null
}
fun coverArt( fun coverArt(
coverArt: String?, coverArt: String?,
thumbnail: Boolean, thumbnail: Boolean,