mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-26 11:02:53 +00:00
feat: sharedElements transition for cast avatars
This commit is contained in:
parent
c95c498d9b
commit
ff61659e6b
6 changed files with 165 additions and 32 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
package com.nuvio.app
|
package com.nuvio.app
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||||
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
|
@ -168,6 +170,7 @@ data class DetailRoute(val type: String, val id: String)
|
||||||
data class PersonDetailRoute(
|
data class PersonDetailRoute(
|
||||||
val personId: Int,
|
val personId: Int,
|
||||||
val personName: String,
|
val personName: String,
|
||||||
|
val personPhoto: String? = null,
|
||||||
val preferCrew: Boolean = false,
|
val preferCrew: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -387,7 +390,7 @@ fun App() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun MainAppContent(
|
private fun MainAppContent(
|
||||||
onSwitchProfile: () -> Unit = {},
|
onSwitchProfile: () -> Unit = {},
|
||||||
|
|
@ -619,11 +622,12 @@ private fun MainAppContent(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.background),
|
.background(MaterialTheme.colorScheme.background),
|
||||||
) {
|
) {
|
||||||
NavHost(
|
SharedTransitionLayout {
|
||||||
navController = navController,
|
NavHost(
|
||||||
startDestination = TabsRoute,
|
navController = navController,
|
||||||
modifier = Modifier.fillMaxSize(),
|
startDestination = TabsRoute,
|
||||||
) {
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
composable<TabsRoute> {
|
composable<TabsRoute> {
|
||||||
PlatformBackHandler(
|
PlatformBackHandler(
|
||||||
enabled = selectedTab != AppScreenTab.Home,
|
enabled = selectedTab != AppScreenTab.Home,
|
||||||
|
|
@ -769,6 +773,7 @@ private fun MainAppContent(
|
||||||
PersonDetailRoute(
|
PersonDetailRoute(
|
||||||
personId = tmdbId,
|
personId = tmdbId,
|
||||||
personName = person.name,
|
personName = person.name,
|
||||||
|
personPhoto = person.photo,
|
||||||
preferCrew = person.role?.let {
|
preferCrew = person.role?.let {
|
||||||
it.equals("Director", ignoreCase = true) ||
|
it.equals("Director", ignoreCase = true) ||
|
||||||
it.equals("Writer", ignoreCase = true) ||
|
it.equals("Writer", ignoreCase = true) ||
|
||||||
|
|
@ -791,6 +796,8 @@ private fun MainAppContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
sharedTransitionScope = this@SharedTransitionLayout,
|
||||||
|
animatedVisibilityScope = this,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -799,6 +806,7 @@ private fun MainAppContent(
|
||||||
PersonDetailScreen(
|
PersonDetailScreen(
|
||||||
personId = route.personId,
|
personId = route.personId,
|
||||||
personName = route.personName,
|
personName = route.personName,
|
||||||
|
initialProfilePhoto = route.personPhoto,
|
||||||
preferCrew = route.preferCrew,
|
preferCrew = route.preferCrew,
|
||||||
onBack = { navController.popBackStack() },
|
onBack = { navController.popBackStack() },
|
||||||
onOpenMeta = { preview ->
|
onOpenMeta = { preview ->
|
||||||
|
|
@ -822,6 +830,8 @@ private fun MainAppContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
sharedTransitionScope = this@SharedTransitionLayout,
|
||||||
|
animatedVisibilityScope = this,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1290,6 +1300,7 @@ private fun MainAppContent(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NuvioPosterActionSheet(
|
NuvioPosterActionSheet(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package com.nuvio.app.features.details
|
||||||
|
|
||||||
|
internal fun castAvatarSharedTransitionKey(personId: Int): String = "cast-avatar:$personId"
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package com.nuvio.app.features.details
|
package com.nuvio.app.features.details
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibilityScope
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||||
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
|
@ -96,6 +99,7 @@ import com.nuvio.app.features.watching.application.WatchingState
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
fun MetaDetailsScreen(
|
fun MetaDetailsScreen(
|
||||||
type: String,
|
type: String,
|
||||||
id: String,
|
id: String,
|
||||||
|
|
@ -105,6 +109,8 @@ fun MetaDetailsScreen(
|
||||||
onOpenMeta: ((MetaPreview) -> Unit)? = null,
|
onOpenMeta: ((MetaPreview) -> Unit)? = null,
|
||||||
onCastClick: ((MetaPerson) -> Unit)? = null,
|
onCastClick: ((MetaPerson) -> Unit)? = null,
|
||||||
onCompanyClick: ((MetaCompany, String) -> Unit)? = null,
|
onCompanyClick: ((MetaCompany, String) -> Unit)? = null,
|
||||||
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val uiState by MetaDetailsRepository.uiState.collectAsStateWithLifecycle()
|
val uiState by MetaDetailsRepository.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
@ -639,6 +645,8 @@ fun MetaDetailsScreen(
|
||||||
onOpenMeta = onOpenMeta,
|
onOpenMeta = onOpenMeta,
|
||||||
onCastClick = onCastClick,
|
onCastClick = onCastClick,
|
||||||
onCompanyClick = onCompanyClick,
|
onCompanyClick = onCompanyClick,
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp + nuvioPlatformExtraBottomPadding))
|
Spacer(modifier = Modifier.height(32.dp + nuvioPlatformExtraBottomPadding))
|
||||||
|
|
@ -877,6 +885,7 @@ fun MetaDetailsScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
private fun ConfiguredMetaSections(
|
private fun ConfiguredMetaSections(
|
||||||
settings: MetaScreenSettingsUiState,
|
settings: MetaScreenSettingsUiState,
|
||||||
meta: MetaDetails,
|
meta: MetaDetails,
|
||||||
|
|
@ -912,6 +921,8 @@ private fun ConfiguredMetaSections(
|
||||||
onOpenMeta: ((MetaPreview) -> Unit)?,
|
onOpenMeta: ((MetaPreview) -> Unit)?,
|
||||||
onCastClick: ((MetaPerson) -> Unit)?,
|
onCastClick: ((MetaPerson) -> Unit)?,
|
||||||
onCompanyClick: ((MetaCompany, String) -> Unit)?,
|
onCompanyClick: ((MetaCompany, String) -> Unit)?,
|
||||||
|
sharedTransitionScope: SharedTransitionScope?,
|
||||||
|
animatedVisibilityScope: AnimatedVisibilityScope?,
|
||||||
) {
|
) {
|
||||||
val enabledItems = settings.items.filter { it.enabled }
|
val enabledItems = settings.items.filter { it.enabled }
|
||||||
|
|
||||||
|
|
@ -954,7 +965,13 @@ private fun ConfiguredMetaSections(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MetaScreenSectionKey.CAST -> {
|
MetaScreenSectionKey.CAST -> {
|
||||||
DetailCastSection(cast = meta.cast, showHeader = showHeader, onCastClick = onCastClick)
|
DetailCastSection(
|
||||||
|
cast = meta.cast,
|
||||||
|
showHeader = showHeader,
|
||||||
|
onCastClick = onCastClick,
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
MetaScreenSectionKey.COMMENTS -> {
|
MetaScreenSectionKey.COMMENTS -> {
|
||||||
if (shouldShowComments && (isCommentsLoading || comments.isNotEmpty() || !commentsError.isNullOrBlank())) {
|
if (shouldShowComments && (isCommentsLoading || comments.isNotEmpty() || !commentsError.isNullOrBlank())) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package com.nuvio.app.features.details
|
package com.nuvio.app.features.details
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibilityScope
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||||
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
@ -64,12 +66,16 @@ private sealed interface PersonDetailUiState {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
fun PersonDetailScreen(
|
fun PersonDetailScreen(
|
||||||
personId: Int,
|
personId: Int,
|
||||||
personName: String,
|
personName: String,
|
||||||
|
initialProfilePhoto: String? = null,
|
||||||
preferCrew: Boolean = false,
|
preferCrew: Boolean = false,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onOpenMeta: (MetaPreview) -> Unit,
|
onOpenMeta: (MetaPreview) -> Unit,
|
||||||
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var uiState by remember(personId) { mutableStateOf<PersonDetailUiState>(PersonDetailUiState.Loading) }
|
var uiState by remember(personId) { mutableStateOf<PersonDetailUiState>(PersonDetailUiState.Loading) }
|
||||||
|
|
@ -92,25 +98,29 @@ fun PersonDetailScreen(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.background),
|
.background(MaterialTheme.colorScheme.background),
|
||||||
) {
|
) {
|
||||||
Crossfade(
|
when (val state = uiState) {
|
||||||
targetState = uiState,
|
is PersonDetailUiState.Loading -> PersonDetailSkeleton(
|
||||||
label = "PersonDetailCrossfade",
|
personId = personId,
|
||||||
) { state ->
|
personName = personName,
|
||||||
when (state) {
|
profilePhoto = initialProfilePhoto,
|
||||||
is PersonDetailUiState.Loading -> PersonDetailSkeleton(personName = personName)
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
is PersonDetailUiState.Error -> PersonDetailError(
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
message = state.message,
|
)
|
||||||
onRetry = {
|
is PersonDetailUiState.Error -> PersonDetailError(
|
||||||
uiState = PersonDetailUiState.Loading
|
message = state.message,
|
||||||
// Retry will be triggered by the LaunchedEffect above if we reset
|
onRetry = {
|
||||||
},
|
uiState = PersonDetailUiState.Loading
|
||||||
)
|
// Retry will be triggered by the LaunchedEffect above if we reset
|
||||||
is PersonDetailUiState.Success -> PersonDetailContent(
|
},
|
||||||
person = state.personDetail,
|
)
|
||||||
onOpenMeta = onOpenMeta,
|
is PersonDetailUiState.Success -> PersonDetailContent(
|
||||||
)
|
person = state.personDetail,
|
||||||
|
onOpenMeta = onOpenMeta,
|
||||||
|
initialProfilePhoto = initialProfilePhoto,
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Back button overlaid on top
|
// Back button overlaid on top
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
@ -130,9 +140,13 @@ fun PersonDetailScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
private fun PersonDetailContent(
|
private fun PersonDetailContent(
|
||||||
person: PersonDetail,
|
person: PersonDetail,
|
||||||
onOpenMeta: (MetaPreview) -> Unit,
|
onOpenMeta: (MetaPreview) -> Unit,
|
||||||
|
initialProfilePhoto: String? = null,
|
||||||
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
) {
|
) {
|
||||||
val accentColor = MaterialTheme.colorScheme.primary
|
val accentColor = MaterialTheme.colorScheme.primary
|
||||||
|
|
||||||
|
|
@ -223,6 +237,9 @@ private fun PersonDetailContent(
|
||||||
HeroSection(
|
HeroSection(
|
||||||
person = person,
|
person = person,
|
||||||
collapseProgress = collapseProgress,
|
collapseProgress = collapseProgress,
|
||||||
|
fallbackProfilePhoto = initialProfilePhoto,
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (popularCredits.isNotEmpty()) {
|
if (popularCredits.isNotEmpty()) {
|
||||||
|
|
@ -268,13 +285,30 @@ private const val HERO_COLLAPSE_SCROLL_RANGE = 220f
|
||||||
private const val HAPTIC_TRIGGER_SCROLL_THRESHOLD_PX = 56
|
private const val HAPTIC_TRIGGER_SCROLL_THRESHOLD_PX = 56
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
private fun HeroSection(
|
private fun HeroSection(
|
||||||
person: PersonDetail,
|
person: PersonDetail,
|
||||||
collapseProgress: Float = 0f,
|
collapseProgress: Float = 0f,
|
||||||
|
fallbackProfilePhoto: String? = null,
|
||||||
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
) {
|
) {
|
||||||
val avatarSize = lerp(140.dp, 72.dp, collapseProgress)
|
val avatarSize = lerp(140.dp, 72.dp, collapseProgress)
|
||||||
val heroScale = 1f - (collapseProgress * 0.12f)
|
val heroScale = 1f - (collapseProgress * 0.12f)
|
||||||
val heroAlpha = 1f - (collapseProgress * 0.35f)
|
val heroAlpha = 1f - (collapseProgress * 0.35f)
|
||||||
|
val avatarUrl = person.profilePhoto?.takeIf { it.isNotBlank() } ?: fallbackProfilePhoto
|
||||||
|
val avatarSharedElementModifier = if (sharedTransitionScope != null && animatedVisibilityScope != null) {
|
||||||
|
with(sharedTransitionScope) {
|
||||||
|
Modifier.sharedElement(
|
||||||
|
sharedContentState = rememberSharedContentState(
|
||||||
|
key = castAvatarSharedTransitionKey(person.tmdbId),
|
||||||
|
),
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -291,14 +325,15 @@ private fun HeroSection(
|
||||||
// Profile Photo
|
// Profile Photo
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.then(avatarSharedElementModifier)
|
||||||
.size(avatarSize)
|
.size(avatarSize)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
if (!person.profilePhoto.isNullOrBlank()) {
|
if (!avatarUrl.isNullOrBlank()) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = person.profilePhoto,
|
model = avatarUrl,
|
||||||
contentDescription = person.name,
|
contentDescription = person.name,
|
||||||
modifier = Modifier.matchParentSize(),
|
modifier = Modifier.matchParentSize(),
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
|
|
@ -378,7 +413,14 @@ private fun HeroSection(
|
||||||
// ─── Loading / Error States ───
|
// ─── Loading / Error States ───
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PersonDetailSkeleton(personName: String) {
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
|
private fun PersonDetailSkeleton(
|
||||||
|
personId: Int,
|
||||||
|
personName: String,
|
||||||
|
profilePhoto: String? = null,
|
||||||
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
|
) {
|
||||||
val accentColor = MaterialTheme.colorScheme.primary
|
val accentColor = MaterialTheme.colorScheme.primary
|
||||||
val accentGradient = remember(accentColor) {
|
val accentGradient = remember(accentColor) {
|
||||||
Brush.verticalGradient(
|
Brush.verticalGradient(
|
||||||
|
|
@ -416,12 +458,41 @@ private fun PersonDetailSkeleton(personName: String) {
|
||||||
.padding(horizontal = 20.dp),
|
.padding(horizontal = 20.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
|
val avatarSharedElementModifier = if (sharedTransitionScope != null && animatedVisibilityScope != null) {
|
||||||
|
with(sharedTransitionScope) {
|
||||||
|
Modifier.sharedElement(
|
||||||
|
sharedContentState = rememberSharedContentState(
|
||||||
|
key = castAvatarSharedTransitionKey(personId),
|
||||||
|
),
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.then(avatarSharedElementModifier)
|
||||||
.size(140.dp)
|
.size(140.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
)
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (!profilePhoto.isNullOrBlank()) {
|
||||||
|
AsyncImage(
|
||||||
|
model = profilePhoto,
|
||||||
|
contentDescription = personName,
|
||||||
|
modifier = Modifier.matchParentSize(),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = personName.firstOrNull()?.uppercase() ?: "?",
|
||||||
|
style = MaterialTheme.typography.displayMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package com.nuvio.app.features.details.components
|
package com.nuvio.app.features.details.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibilityScope
|
||||||
|
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||||
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
@ -27,13 +30,17 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.nuvio.app.features.details.MetaPerson
|
import com.nuvio.app.features.details.MetaPerson
|
||||||
|
import com.nuvio.app.features.details.castAvatarSharedTransitionKey
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
fun DetailCastSection(
|
fun DetailCastSection(
|
||||||
cast: List<MetaPerson>,
|
cast: List<MetaPerson>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
showHeader: Boolean = true,
|
showHeader: Boolean = true,
|
||||||
onCastClick: ((MetaPerson) -> Unit)? = null,
|
onCastClick: ((MetaPerson) -> Unit)? = null,
|
||||||
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
) {
|
) {
|
||||||
if (cast.isEmpty()) return
|
if (cast.isEmpty()) return
|
||||||
|
|
||||||
|
|
@ -55,6 +62,8 @@ fun DetailCastSection(
|
||||||
CastItem(
|
CastItem(
|
||||||
person = person,
|
person = person,
|
||||||
sizing = sizing,
|
sizing = sizing,
|
||||||
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
onClick = if (onCastClick != null && person.tmdbId != null && person.tmdbId > 0) {
|
onClick = if (onCastClick != null && person.tmdbId != null && person.tmdbId > 0) {
|
||||||
{ onCastClick(person) }
|
{ onCastClick(person) }
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -68,12 +77,33 @@ fun DetailCastSection(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
private fun CastItem(
|
private fun CastItem(
|
||||||
person: MetaPerson,
|
person: MetaPerson,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
sizing: CastSectionSizing,
|
sizing: CastSectionSizing,
|
||||||
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
|
val avatarSharedElementModifier = if (
|
||||||
|
sharedTransitionScope != null &&
|
||||||
|
animatedVisibilityScope != null &&
|
||||||
|
person.tmdbId != null &&
|
||||||
|
person.tmdbId > 0
|
||||||
|
) {
|
||||||
|
with(sharedTransitionScope) {
|
||||||
|
Modifier.sharedElement(
|
||||||
|
sharedContentState = rememberSharedContentState(
|
||||||
|
key = castAvatarSharedTransitionKey(person.tmdbId),
|
||||||
|
),
|
||||||
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.width(sizing.itemWidth)
|
.width(sizing.itemWidth)
|
||||||
|
|
@ -83,6 +113,7 @@ private fun CastItem(
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.then(avatarSharedElementModifier)
|
||||||
.size(sizing.avatarSize)
|
.size(sizing.avatarSize)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
CURRENT_PROJECT_VERSION=18
|
CURRENT_PROJECT_VERSION=19
|
||||||
MARKETING_VERSION=0.1.0-alpha18
|
MARKETING_VERSION=0.1.0-alpha19
|
||||||
Loading…
Reference in a new issue