Move backdrop into a composable

This commit is contained in:
Damontecres 2026-03-06 12:50:51 -05:00
parent d5f5c5fb0a
commit 29269faa63
No known key found for this signature in database
2 changed files with 182 additions and 159 deletions

View file

@ -1,31 +1,13 @@
package com.github.damontecres.wholphin.ui.nav
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSerializable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavBackStack
@ -36,23 +18,16 @@ import androidx.navigation3.runtime.serialization.NavBackStackSerializer
import androidx.navigation3.runtime.serialization.NavKeySerializer
import androidx.navigation3.ui.NavDisplay
import androidx.tv.material3.DrawerValue
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.rememberDrawerState
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.transitionFactory
import com.github.damontecres.wholphin.data.model.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.preferences.BackdropStyle
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.ui.CrossFadeFactory
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.launchIO
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
// Top scrim configuration for text readability (clock, season tabs)
const val TOP_SCRIM_ALPHA = 0.55f
@ -94,144 +69,17 @@ fun ApplicationContent(
NavBackStack(startDestination)
}
navigationManager.backStack = backStack
val backdrop by viewModel.backdropService.backdropFlow.collectAsStateWithLifecycle()
val backdropStyle = preferences.appPreferences.interfacePreferences.backdropStyle
val drawerState = rememberDrawerState(DrawerValue.Closed)
Box(
modifier = modifier,
) {
val baseBackgroundColor = MaterialTheme.colorScheme.background
if (backdrop.hasColors &&
(backdropStyle == BackdropStyle.BACKDROP_DYNAMIC_COLOR || backdropStyle == BackdropStyle.UNRECOGNIZED)
) {
val animPrimary by animateColorAsState(
backdrop.primaryColor,
animationSpec = tween(1250),
label = "dynamic_backdrop_primary",
)
val animSecondary by animateColorAsState(
backdrop.secondaryColor,
animationSpec = tween(1250),
label = "dynamic_backdrop_secondary",
)
val animTertiary by animateColorAsState(
backdrop.tertiaryColor,
animationSpec = tween(1250),
label = "dynamic_backdrop_tertiary",
)
Box(
modifier =
Modifier
.fillMaxSize()
.drawBehind {
drawRect(color = baseBackgroundColor)
// Top Left (Vibrant/Muted)
drawRect(
brush =
Brush.radialGradient(
colors = listOf(animSecondary, Color.Transparent),
center = Offset(0f, 0f),
radius = size.width * 0.8f,
),
)
// Bottom Right (DarkVibrant/DarkMuted)
drawRect(
brush =
Brush.radialGradient(
colors = listOf(animPrimary, Color.Transparent),
center = Offset(size.width, size.height),
radius = size.width * 0.8f,
),
)
// Bottom Left (Dark / Bridge)
drawRect(
brush =
Brush.radialGradient(
colors =
listOf(
baseBackgroundColor,
Color.Transparent,
),
center = Offset(0f, size.height),
radius = size.width * 0.8f,
),
)
// Top Right (Under Image - Vibrant/Bright)
drawRect(
brush =
Brush.radialGradient(
colors = listOf(animTertiary, Color.Transparent),
center = Offset(size.width, 0f),
radius = size.width * 0.8f,
),
)
},
)
}
if (backdropStyle != BackdropStyle.BACKDROP_NONE) {
Box(
modifier = Modifier.fillMaxSize(),
) {
AsyncImage(
model =
ImageRequest
.Builder(LocalContext.current)
.data(backdrop.imageUrl)
.transitionFactory(CrossFadeFactory(800.milliseconds))
.build(),
contentDescription = null,
contentScale = ContentScale.Fit,
alignment = Alignment.TopEnd,
modifier =
Modifier
.align(Alignment.TopEnd)
.fillMaxHeight(.7f)
.fillMaxWidth(.7f)
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithContent {
drawContent()
if (drawerState.isOpen) {
drawRect(
brush = SolidColor(Color.Black),
alpha = .75f,
)
}
// Subtle top scrim for system UI readability (clock, tabs)
if (enableTopScrim) {
drawRect(
brush =
Brush.verticalGradient(
colorStops =
arrayOf(
0f to Color.Black.copy(alpha = TOP_SCRIM_ALPHA),
TOP_SCRIM_END_FRACTION to Color.Transparent,
),
),
blendMode = BlendMode.Multiply,
)
}
drawRect(
brush =
Brush.horizontalGradient(
colors = listOf(Color.Transparent, Color.Black),
startX = 0f,
endX = size.width * 0.6f,
),
blendMode = BlendMode.DstIn,
)
drawRect(
brush =
Brush.verticalGradient(
colors = listOf(Color.Black, Color.Transparent),
startY = 0f,
endY = size.height,
),
blendMode = BlendMode.DstIn,
)
},
)
}
}
val backdropStyle = preferences.appPreferences.interfacePreferences.backdropStyle
Backdrop(
drawerIsOpen = drawerState.isOpen,
backdropStyle = backdropStyle,
enableTopScrim = enableTopScrim,
viewModel = viewModel,
)
val navDrawerListState = rememberLazyListState()
NavDisplay(
backStack = navigationManager.backStack,

View file

@ -0,0 +1,175 @@
package com.github.damontecres.wholphin.ui.nav
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.tv.material3.MaterialTheme
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.transitionFactory
import com.github.damontecres.wholphin.preferences.BackdropStyle
import com.github.damontecres.wholphin.ui.CrossFadeFactory
import kotlin.time.Duration.Companion.milliseconds
@Composable
fun Backdrop(
drawerIsOpen: Boolean,
backdropStyle: BackdropStyle,
viewModel: ApplicationContentViewModel = hiltViewModel(),
modifier: Modifier = Modifier,
enableTopScrim: Boolean = true,
) {
val backdrop by viewModel.backdropService.backdropFlow.collectAsStateWithLifecycle()
val baseBackgroundColor = MaterialTheme.colorScheme.background
if (backdrop.hasColors &&
(backdropStyle == BackdropStyle.BACKDROP_DYNAMIC_COLOR || backdropStyle == BackdropStyle.UNRECOGNIZED)
) {
val animPrimary by animateColorAsState(
backdrop.primaryColor,
animationSpec = tween(1250),
label = "dynamic_backdrop_primary",
)
val animSecondary by animateColorAsState(
backdrop.secondaryColor,
animationSpec = tween(1250),
label = "dynamic_backdrop_secondary",
)
val animTertiary by animateColorAsState(
backdrop.tertiaryColor,
animationSpec = tween(1250),
label = "dynamic_backdrop_tertiary",
)
Box(
modifier =
modifier
.fillMaxSize()
.drawBehind {
drawRect(color = baseBackgroundColor)
// Top Left (Vibrant/Muted)
drawRect(
brush =
Brush.radialGradient(
colors = listOf(animSecondary, Color.Transparent),
center = Offset(0f, 0f),
radius = size.width * 0.8f,
),
)
// Bottom Right (DarkVibrant/DarkMuted)
drawRect(
brush =
Brush.radialGradient(
colors = listOf(animPrimary, Color.Transparent),
center = Offset(size.width, size.height),
radius = size.width * 0.8f,
),
)
// Bottom Left (Dark / Bridge)
drawRect(
brush =
Brush.radialGradient(
colors =
listOf(
baseBackgroundColor,
Color.Transparent,
),
center = Offset(0f, size.height),
radius = size.width * 0.8f,
),
)
// Top Right (Under Image - Vibrant/Bright)
drawRect(
brush =
Brush.radialGradient(
colors = listOf(animTertiary, Color.Transparent),
center = Offset(size.width, 0f),
radius = size.width * 0.8f,
),
)
},
)
}
if (backdropStyle != BackdropStyle.BACKDROP_NONE) {
Box(
modifier = modifier.fillMaxSize(),
) {
AsyncImage(
model =
ImageRequest
.Builder(LocalContext.current)
.data(backdrop.imageUrl)
.transitionFactory(CrossFadeFactory(800.milliseconds))
.build(),
contentDescription = null,
contentScale = ContentScale.Fit,
alignment = Alignment.TopEnd,
modifier =
Modifier
.align(Alignment.TopEnd)
.fillMaxHeight(.7f)
.fillMaxWidth(.7f)
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithContent {
drawContent()
if (drawerIsOpen) {
drawRect(
brush = SolidColor(Color.Black),
alpha = .75f,
)
}
// Subtle top scrim for system UI readability (clock, tabs)
if (enableTopScrim) {
drawRect(
brush =
Brush.verticalGradient(
colorStops =
arrayOf(
0f to Color.Black.copy(alpha = TOP_SCRIM_ALPHA),
TOP_SCRIM_END_FRACTION to Color.Transparent,
),
),
blendMode = BlendMode.Multiply,
)
}
drawRect(
brush =
Brush.horizontalGradient(
colors = listOf(Color.Transparent, Color.Black),
startX = 0f,
endX = size.width * 0.6f,
),
blendMode = BlendMode.DstIn,
)
drawRect(
brush =
Brush.verticalGradient(
colors = listOf(Color.Black, Color.Transparent),
startY = 0f,
endY = size.height,
),
blendMode = BlendMode.DstIn,
)
},
)
}
}
}