apply the prefetched theme on open and soften the hero darkening

This commit is contained in:
austinried
2026-10-04 00:29:53 +00:00
parent f32135d8d2
commit bcb3f20a09
4 changed files with 29 additions and 8 deletions
@@ -68,11 +68,14 @@ private object Routes {
const val LIBRARY = "library" const val LIBRARY = "library"
const val SETTINGS = "settings" const val SETTINGS = "settings"
const val ADD_SERVER = "add-server" const val ADD_SERVER = "add-server"
const val ALBUM_DETAIL = "album/{albumId}" const val ALBUM_DETAIL = "album/{albumId}?coverArt={coverArt}"
const val ARTIST_DETAIL = "artist/{artistId}" const val ARTIST_DETAIL = "artist/{artistId}"
const val PLAYLIST_DETAIL = "playlist/{playlistId}" const val PLAYLIST_DETAIL = "playlist/{playlistId}"
fun album(id: String) = "album/${Uri.encode(id)}" fun album(
id: String,
coverArt: String? = null,
) = "album/${Uri.encode(id)}?coverArt=${Uri.encode(coverArt.orEmpty())}"
fun artist(id: String) = "artist/${Uri.encode(id)}" fun artist(id: String) = "artist/${Uri.encode(id)}"
@@ -135,7 +138,7 @@ private fun MainNavigation() {
) { ) {
composable(Routes.LIBRARY) { composable(Routes.LIBRARY) {
LibraryRoute( LibraryRoute(
onAlbumClick = { album -> navController.navigate(Routes.album(album.id)) }, onAlbumClick = { album -> navController.navigate(Routes.album(album.id, album.coverArt)) },
onArtistClick = { artist -> navController.navigate(Routes.artist(artist.id)) }, onArtistClick = { artist -> navController.navigate(Routes.artist(artist.id)) },
onPlaylistClick = { playlist -> navController.navigate(Routes.playlist(playlist.id)) }, onPlaylistClick = { playlist -> navController.navigate(Routes.playlist(playlist.id)) },
onOpenSettings = { navController.navigate(Routes.SETTINGS) }, onOpenSettings = { navController.navigate(Routes.SETTINGS) },
@@ -150,10 +153,22 @@ private fun MainNavigation() {
} }
composable( composable(
route = Routes.ALBUM_DETAIL, route = Routes.ALBUM_DETAIL,
arguments = listOf(navArgument("albumId") { type = NavType.StringType }), arguments =
listOf(
navArgument("albumId") { type = NavType.StringType },
navArgument("coverArt") {
type = NavType.StringType
defaultValue = ""
},
),
) { entry -> ) { entry ->
AlbumDetailRoute( AlbumDetailRoute(
albumId = entry.arguments?.getString("albumId").orEmpty(), albumId = entry.arguments?.getString("albumId").orEmpty(),
coverArtId =
entry.arguments
?.getString("coverArt")
.orEmpty()
.ifEmpty { null },
onBack = { navController.popBackStack() }, onBack = { navController.popBackStack() },
) )
} }
@@ -164,7 +179,7 @@ private fun MainNavigation() {
ArtistDetailRoute( ArtistDetailRoute(
artistId = entry.arguments?.getString("artistId").orEmpty(), artistId = entry.arguments?.getString("artistId").orEmpty(),
onBack = { navController.popBackStack() }, onBack = { navController.popBackStack() },
onAlbumClick = { album -> navController.navigate(Routes.album(album.id)) }, onAlbumClick = { album -> navController.navigate(Routes.album(album.id, album.coverArt)) },
) )
} }
composable( composable(
@@ -89,6 +89,7 @@ private const val THEME_TRANSITION_MS = 100
@Composable @Composable
fun AlbumDetailRoute( fun AlbumDetailRoute(
albumId: String, albumId: String,
coverArtId: String?,
onBack: () -> Unit, onBack: () -> Unit,
viewModel: AlbumDetailViewModel = koinViewModel(key = albumId) { parametersOf(albumId) }, viewModel: AlbumDetailViewModel = koinViewModel(key = albumId) { parametersOf(albumId) },
playbackController: PlaybackController = koinInject(), playbackController: PlaybackController = koinInject(),
@@ -97,11 +98,12 @@ fun AlbumDetailRoute(
val songs by viewModel.songs.collectAsStateWithLifecycle(initialValue = emptyList()) val songs by viewModel.songs.collectAsStateWithLifecycle(initialValue = emptyList())
val playback by playbackController.state.collectAsStateWithLifecycle() val playback by playbackController.state.collectAsStateWithLifecycle()
val context = playback.context val context = playback.context
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
AlbumDetailScreen( AlbumDetailScreen(
album = album, album = album,
songs = songs, songs = songs,
coverArt = viewModel::coverArt, coverArt = viewModel::coverArt,
artwork = rememberArtworkColors(viewModel.coverArt(album?.coverArt, true), THEME_TRANSITION_MS), artwork = rememberArtworkColors(shortcutArt ?: viewModel.coverArt(album?.coverArt, true), THEME_TRANSITION_MS),
onBack = onBack, onBack = onBack,
onSongClick = viewModel::play, onSongClick = viewModel::play,
playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Album && context.refId == albumId }, playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Album && context.refId == albumId },
@@ -44,6 +44,8 @@ object ArtworkSeedCache {
this.store = store this.store = store
} }
fun cached(cacheKey: String): Pair<Int, Int?>? = cache.get(cacheKey)
suspend fun seeds( suspend fun seeds(
context: Context, context: Context,
ref: CoverArtRef, ref: CoverArtRef,
@@ -27,7 +27,7 @@ private const val BLOB_COUNT = 4
private const val ACCENT_MAX_LUMINANCE = 0.18f private const val ACCENT_MAX_LUMINANCE = 0.18f
private const val PRIMARY_MIN_LUMINANCE = 0.30f private const val PRIMARY_MIN_LUMINANCE = 0.30f
private const val MIN_GRADIENT_SATURATION = 0.30f private const val MIN_GRADIENT_SATURATION = 0.30f
private const val HERO_DARKEN_MAX = 1.0f private const val HERO_DARKEN_MAX = 0.88f
private const val DARKEN_START_SCREENS = 0.5f private const val DARKEN_START_SCREENS = 0.5f
private const val DARKEN_END_SCREENS = 1.5f private const val DARKEN_END_SCREENS = 1.5f
private const val DARKEN_KNEE = 0.2f private const val DARKEN_KNEE = 0.2f
@@ -166,8 +166,10 @@ fun rememberArtworkColors(
ref: CoverArtRef?, ref: CoverArtRef?,
durationMillis: Int = ARTWORK_THEME_TRANSITION_MS, durationMillis: Int = ARTWORK_THEME_TRANSITION_MS,
): ArtworkColors { ): ArtworkColors {
val cached = remember(ref?.cacheKey) { ref?.cacheKey?.let(ArtworkSeedCache::cached) }
val seeds by rememberArtworkSeed(ref) val seeds by rememberArtworkSeed(ref)
val target = remember(seeds) { seeds?.let { (primary, secondary) -> artworkColorsFromSeeds(primary, secondary) } } val effective = seeds ?: cached
val target = remember(effective) { effective?.let { (primary, secondary) -> artworkColorsFromSeeds(primary, secondary) } }
return rememberAnimatedArtworkColors(target, durationMillis) return rememberAnimatedArtworkColors(target, durationMillis)
} }