mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-18 04:55:58 +00:00
Migrate tween literals to AnimDuration tokens across 14 files
Replace all raw tween(N) integer literals with named FluxaDimensions.AnimDuration constants in player overlays, sidebar, source panel, profile components, parents guide, login, explore, detail, library, source, search, and route transitions.
This commit is contained in:
parent
c9ecb5477a
commit
44683af3c1
14 changed files with 347 additions and 234 deletions
|
|
@ -178,7 +178,7 @@ private fun TvQrLoginView(
|
|||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(Color(0xFF221F1F), Color(0xFF0F0F0F)),
|
||||
colors = listOf(Color(0xFF221F1F), FluxaColors.backgroundNearBlack),
|
||||
center = androidx.compose.ui.geometry.Offset(0.5f, 0.5f)
|
||||
)
|
||||
),
|
||||
|
|
@ -292,9 +292,9 @@ private fun TvKeyboardAuthForm(
|
|||
fun continueAsGuest() {
|
||||
val guest = UserProfile(
|
||||
id = UUID.randomUUID().toString(),
|
||||
email = AppStrings.t(lang, "auth.guest_name"),
|
||||
email = AppStrings.t(lang, "auth.primary_profile_name"),
|
||||
authKey = "",
|
||||
isGuest = true
|
||||
isGuest = false
|
||||
)
|
||||
profileManager.saveProfile(guest)
|
||||
profileManager.setLastActiveProfile(guest)
|
||||
|
|
@ -350,7 +350,7 @@ private fun TvKeyboardAuthForm(
|
|||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(Color(0xFF221F1F), Color(0xFF0F0F0F)),
|
||||
colors = listOf(Color(0xFF221F1F), FluxaColors.backgroundNearBlack),
|
||||
center = androidx.compose.ui.geometry.Offset(0.5f, 0.5f)
|
||||
)
|
||||
),
|
||||
|
|
@ -416,7 +416,7 @@ private fun TvKeyboardAuthForm(
|
|||
|
||||
errorMessage?.let {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(it, color = Color(0xFFFF6B6B))
|
||||
Text(it, color = FluxaColors.errorRed)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(20.dp))
|
||||
|
|
@ -462,7 +462,7 @@ private fun TvNuvioLoginView(
|
|||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(Color(0xFF221F1F), Color(0xFF0F0F0F)),
|
||||
colors = listOf(Color(0xFF221F1F), FluxaColors.backgroundNearBlack),
|
||||
center = androidx.compose.ui.geometry.Offset(0.5f, 0.5f)
|
||||
)
|
||||
),
|
||||
|
|
@ -517,7 +517,7 @@ private fun TvNuvioLoginView(
|
|||
)
|
||||
errorMessage?.let {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(it, color = Color(0xFFFF6B6B))
|
||||
Text(it, color = FluxaColors.errorRed)
|
||||
}
|
||||
Spacer(Modifier.height(20.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
|
|
@ -586,7 +586,7 @@ fun PremiumLoadingIndicator(text: String) {
|
|||
initialValue = 0f,
|
||||
targetValue = 360f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(1000, easing = LinearEasing)
|
||||
animation = tween(FluxaDimensions.AnimDuration.loginPulse, easing = LinearEasing)
|
||||
),
|
||||
label = ""
|
||||
)
|
||||
|
|
|
|||
|
|
@ -57,26 +57,26 @@ fun PlayerParentsGuideOverlay(
|
|||
if (isVisible && !animating) {
|
||||
animating = true
|
||||
|
||||
containerAlpha.animateTo(1f, tween(300))
|
||||
lineHeightFraction.animateTo(1f, tween(400, easing = FastOutSlowInEasing))
|
||||
containerAlpha.animateTo(1f, tween(FluxaDimensions.AnimDuration.parentsContainer))
|
||||
lineHeightFraction.animateTo(1f, tween(FluxaDimensions.AnimDuration.parentsExpand, easing = FastOutSlowInEasing))
|
||||
|
||||
for (i in 0 until count) {
|
||||
delay(80)
|
||||
itemAlphas[i].animateTo(1f, tween(200))
|
||||
itemAlphas[i].animateTo(1f, tween(FluxaDimensions.AnimDuration.fadeIn))
|
||||
}
|
||||
|
||||
delay(5000)
|
||||
|
||||
for (i in (count - 1) downTo 0) {
|
||||
delay(60)
|
||||
itemAlphas[i].animateTo(0f, tween(150))
|
||||
itemAlphas[i].animateTo(0f, tween(FluxaDimensions.AnimDuration.heroSnap))
|
||||
}
|
||||
|
||||
delay(100)
|
||||
lineHeightFraction.animateTo(0f, tween(300, easing = FastOutSlowInEasing))
|
||||
lineHeightFraction.animateTo(0f, tween(FluxaDimensions.AnimDuration.parentsContainer, easing = FastOutSlowInEasing))
|
||||
|
||||
delay(200)
|
||||
containerAlpha.animateTo(0f, tween(200))
|
||||
containerAlpha.animateTo(0f, tween(FluxaDimensions.AnimDuration.fadeIn))
|
||||
|
||||
animating = false
|
||||
onAnimationComplete()
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ internal fun SegmentSkipChevronFeedback() {
|
|||
initialValue = 0f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(560, easing = FastOutSlowInEasing),
|
||||
animation = tween(FluxaDimensions.AnimDuration.nextEpisode, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
),
|
||||
label = "segmentSkipPhase"
|
||||
|
|
@ -301,7 +301,7 @@ internal fun ArtisticLoadingOverlay(bg: String, logo: String, title: String, sta
|
|||
}
|
||||
val loadProgress by animateFloatAsState(
|
||||
targetValue = targetProgress,
|
||||
animationSpec = tween(520, easing = FastOutSlowInEasing),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.progressRing, easing = FastOutSlowInEasing),
|
||||
label = "logoLoadProgress"
|
||||
)
|
||||
val hasProgress = bufferingMedia || loadProgress > 0.015f || targetProgress > 0.015f
|
||||
|
|
@ -311,7 +311,7 @@ internal fun ArtisticLoadingOverlay(bg: String, logo: String, title: String, sta
|
|||
initialValue = 0.42f,
|
||||
targetValue = 0.66f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(1120, easing = FastOutSlowInEasing),
|
||||
animation = tween(FluxaDimensions.AnimDuration.marquee, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "loadingLogoAlpha"
|
||||
|
|
|
|||
|
|
@ -76,8 +76,8 @@ internal fun PlayerSkipSegmentOverlay(
|
|||
|
||||
AnimatedVisibility(
|
||||
visible = activeSkipSegment != null || showNextEpisodeCard,
|
||||
enter = fadeIn(animationSpec = tween(180)) + slideInVertically(animationSpec = tween(220, easing = FastOutSlowInEasing)) { it / 2 },
|
||||
exit = fadeOut(animationSpec = tween(140)) + slideOutVertically(animationSpec = tween(180, easing = FastOutSlowInEasing)) { it / 2 },
|
||||
enter = fadeIn(animationSpec = tween(FluxaDimensions.AnimDuration.scaleAlpha)) + slideInVertically(animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand, easing = FastOutSlowInEasing)) { it / 2 },
|
||||
exit = fadeOut(animationSpec = tween(FluxaDimensions.AnimDuration.quick)) + slideOutVertically(animationSpec = tween(FluxaDimensions.AnimDuration.scaleAlpha, easing = FastOutSlowInEasing)) { it / 2 },
|
||||
modifier = modifier
|
||||
.padding(
|
||||
start = 18.dp,
|
||||
|
|
@ -135,8 +135,8 @@ internal fun BoxScope.PlayerTransientOverlays(
|
|||
) {
|
||||
AnimatedVisibility(
|
||||
visible = showSegmentSkipFeedback,
|
||||
enter = fadeIn(animationSpec = tween(90)) + scaleIn(animationSpec = tween(140), initialScale = 0.82f),
|
||||
exit = fadeOut(animationSpec = tween(260)) + scaleOut(animationSpec = tween(260), targetScale = 1.08f),
|
||||
enter = fadeIn(animationSpec = tween(FluxaDimensions.AnimDuration.blink)) + scaleIn(animationSpec = tween(FluxaDimensions.AnimDuration.quick), initialScale = 0.82f),
|
||||
exit = fadeOut(animationSpec = tween(FluxaDimensions.AnimDuration.settingsExpandAlt)) + scaleOut(animationSpec = tween(FluxaDimensions.AnimDuration.settingsExpandAlt), targetScale = 1.08f),
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.zIndex(320f)
|
||||
|
|
@ -148,8 +148,8 @@ internal fun BoxScope.PlayerTransientOverlays(
|
|||
|
||||
AnimatedVisibility(
|
||||
visible = showZoomOverlay,
|
||||
enter = fadeIn(animationSpec = tween(100)) + scaleIn(animationSpec = tween(160, easing = FastOutSlowInEasing), initialScale = 0.80f),
|
||||
exit = fadeOut(animationSpec = tween(220)) + scaleOut(animationSpec = tween(220), targetScale = 1.06f),
|
||||
enter = fadeIn(animationSpec = tween(FluxaDimensions.AnimDuration.routeExit)) + scaleIn(animationSpec = tween(FluxaDimensions.AnimDuration.routeExit, easing = FastOutSlowInEasing), initialScale = 0.80f),
|
||||
exit = fadeOut(animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand)) + scaleOut(animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand), targetScale = 1.06f),
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = if (deviceType == DeviceType.Mobile) 34.dp else 54.dp)
|
||||
|
|
@ -188,8 +188,8 @@ internal fun BoxScope.PlayerTransientOverlays(
|
|||
|
||||
AnimatedVisibility(
|
||||
visible = holdSpeedVisible,
|
||||
enter = fadeIn(animationSpec = tween(120)) + scaleIn(animationSpec = tween(180, easing = FastOutSlowInEasing), initialScale = 0.86f),
|
||||
exit = fadeOut(animationSpec = tween(140)) + scaleOut(animationSpec = tween(140), targetScale = 0.92f),
|
||||
enter = fadeIn(animationSpec = tween(FluxaDimensions.AnimDuration.heightAnim)) + scaleIn(animationSpec = tween(FluxaDimensions.AnimDuration.scaleAlpha, easing = FastOutSlowInEasing), initialScale = 0.86f),
|
||||
exit = fadeOut(animationSpec = tween(FluxaDimensions.AnimDuration.quick)) + scaleOut(animationSpec = tween(FluxaDimensions.AnimDuration.quick), targetScale = 0.92f),
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = if (deviceType == DeviceType.Mobile) 34.dp else 54.dp)
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ fun SeekFeedback(direction: Int, seekMs: Long) {
|
|||
initialValue = 0f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(620, easing = FastOutSlowInEasing),
|
||||
animation = tween(FluxaDimensions.AnimDuration.sidebarSlide, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
), label = "chevronShift"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -255,8 +255,8 @@ fun PlayerSidebarShell(
|
|||
) {
|
||||
var shown by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) { shown = true }
|
||||
val panelAlpha by animateFloatAsState(if (shown) 1f else 0f, animationSpec = tween(180), label = "sidebarAlpha")
|
||||
val panelOffset by animateFloatAsState(if (shown) 0f else 44f, animationSpec = tween(220, easing = FastOutSlowInEasing), label = "sidebarOffset")
|
||||
val panelAlpha by animateFloatAsState(if (shown) 1f else 0f, animationSpec = tween(FluxaDimensions.AnimDuration.scaleAlpha), label = "sidebarAlpha")
|
||||
val panelOffset by animateFloatAsState(if (shown) 0f else 44f, animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand, easing = FastOutSlowInEasing), label = "sidebarOffset")
|
||||
val isMobile = deviceType == DeviceType.Mobile
|
||||
val panelShape = if (isMobile) {
|
||||
if (compactCenterOnMobile) {
|
||||
|
|
@ -409,7 +409,7 @@ fun TrackItem(
|
|||
isFocused -> Color.White.copy(alpha = 0.18f)
|
||||
else -> Color.White.copy(alpha = 0.04f)
|
||||
},
|
||||
animationSpec = tween(150),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.heroSnap),
|
||||
label = "bg"
|
||||
)
|
||||
val textColor = if (isSelected) Color.Black else Color.White
|
||||
|
|
@ -423,7 +423,7 @@ fun TrackItem(
|
|||
isFocused -> 1.01f
|
||||
else -> 1f
|
||||
},
|
||||
animationSpec = tween(170, easing = FastOutSlowInEasing),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.scaleAlpha, easing = FastOutSlowInEasing),
|
||||
label = "trackScale"
|
||||
)
|
||||
val minHeight = if (subtitle.isNullOrBlank()) {
|
||||
|
|
@ -440,7 +440,7 @@ fun TrackItem(
|
|||
scaleX = scale
|
||||
scaleY = scale
|
||||
}
|
||||
.animateContentSize(animationSpec = tween(220, easing = FastOutSlowInEasing))
|
||||
.animateContentSize(animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand, easing = FastOutSlowInEasing))
|
||||
.clip(RoundedCornerShape(if (resolvedDeviceType == DeviceType.TV) 18.dp else 16.dp))
|
||||
.background(bgColor)
|
||||
.clickable { onClick() }
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ internal fun TvProfileOrb(
|
|||
onClick: () -> Unit
|
||||
) {
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(if (isFocused) 1.04f else 1f, tween(180), label = "profileScale")
|
||||
val scale by animateFloatAsState(if (isFocused) 1.04f else 1f, tween(FluxaDimensions.AnimDuration.scaleAlpha), label = "profileScale")
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
|
|
@ -263,7 +263,7 @@ internal fun TvAddProfileOrb(
|
|||
onClick: () -> Unit
|
||||
) {
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(if (isFocused) 1.04f else 1f, tween(180), label = "addProfileScale")
|
||||
val scale by animateFloatAsState(if (isFocused) 1.04f else 1f, tween(FluxaDimensions.AnimDuration.scaleAlpha), label = "addProfileScale")
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@ import com.fluxa.app.data.remote.*
|
|||
import com.fluxa.app.data.repository.*
|
||||
import com.fluxa.app.domain.discovery.*
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -14,6 +20,7 @@ import androidx.compose.foundation.layout.*
|
|||
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 as mobileGridItems
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed as mobileGridItemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
|
@ -83,168 +90,179 @@ fun MobileExploreScreen(
|
|||
}
|
||||
}
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
modifier = Modifier.fillMaxSize().background(Color(0xFF040508)),
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 18.dp, bottom = 120.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().background(Color(0xFF040508))
|
||||
) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = AppStrings.t(lang, "nav.discover"),
|
||||
color = Color.White,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Black,
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = AppStrings.t(lang, "nav.discover"),
|
||||
color = Color.White,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Black,
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 18.dp, bottom = 12.dp)
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(Color.White.copy(alpha = 0.08f))
|
||||
.border(1.dp, Color.White.copy(alpha = 0.05f), RoundedCornerShape(24.dp))
|
||||
.padding(horizontal = 16.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(Color.White.copy(alpha = 0.08f))
|
||||
.border(1.dp, Color.White.copy(alpha = 0.05f), RoundedCornerShape(24.dp))
|
||||
.padding(horizontal = 16.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(FluxaIcons.Search, null, tint = Color.White.copy(alpha = 0.4f), modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(10.dp))
|
||||
androidx.compose.foundation.text.BasicTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = {
|
||||
searchQuery = it
|
||||
viewModel.search(it)
|
||||
},
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(onSearch = { focusManager.clearFocus() }),
|
||||
textStyle = androidx.compose.ui.text.TextStyle(color = Color.White, fontSize = 15.sp),
|
||||
cursorBrush = androidx.compose.ui.graphics.SolidColor(Color(activeProfile?.safeAccentColorArgb ?: 0xFFFFFFFF.toInt())),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
decorationBox = { innerTextField ->
|
||||
if (searchQuery.isEmpty()) {
|
||||
Text(
|
||||
text = AppStrings.t(lang, "auto.explore_68dd9e4b"),
|
||||
color = Color.White.copy(alpha = 0.3f),
|
||||
fontSize = 15.sp
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(FluxaIcons.Search, null, tint = Color.White.copy(alpha = 0.4f), modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(10.dp))
|
||||
androidx.compose.foundation.text.BasicTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(onSearch = {
|
||||
viewModel.search(searchQuery)
|
||||
focusManager.clearFocus()
|
||||
}),
|
||||
textStyle = androidx.compose.ui.text.TextStyle(color = Color.White, fontSize = 15.sp),
|
||||
cursorBrush = androidx.compose.ui.graphics.SolidColor(Color(activeProfile?.safeAccentColorArgb ?: 0xFFFFFFFF.toInt())),
|
||||
modifier = Modifier.weight(1f),
|
||||
decorationBox = { innerTextField ->
|
||||
if (searchQuery.isEmpty()) {
|
||||
Text(
|
||||
text = AppStrings.t(lang, "auto.explore_68dd9e4b"),
|
||||
color = Color.White.copy(alpha = 0.3f),
|
||||
fontSize = 15.sp
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
)
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
Icon(
|
||||
FluxaIcons.Close,
|
||||
null,
|
||||
tint = Color.White.copy(alpha = 0.5f),
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.clickable {
|
||||
searchQuery = ""
|
||||
viewModel.search("")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
val exploreAccent = Color(activeProfile?.safeAccentColorArgb ?: 0xFFFFFFFF.toInt())
|
||||
Box(Modifier.weight(1f)) {
|
||||
ExploreDropdownFilter(
|
||||
title = AppStrings.t(lang, "auto.content_type"),
|
||||
options = exploreTypeOptions(lang),
|
||||
selected = selectedType,
|
||||
onSelect = { it?.let(onSelectType) },
|
||||
showTitle = false,
|
||||
accentColor = exploreAccent
|
||||
)
|
||||
}
|
||||
Box(Modifier.weight(1f)) {
|
||||
ExploreDropdownFilter(
|
||||
title = AppStrings.t(lang, "explore.catalog"),
|
||||
options = catalogOptions,
|
||||
selected = selectedCatalog,
|
||||
onSelect = onSelectCatalog,
|
||||
showTitle = false,
|
||||
accentColor = exploreAccent
|
||||
)
|
||||
}
|
||||
Box(Modifier.weight(1f)) {
|
||||
ExploreDropdownFilter(
|
||||
title = AppStrings.t(lang, "auto.genre_78cde1de"),
|
||||
options = genreOptions,
|
||||
selected = selectedGenre,
|
||||
onSelect = onSelectGenre,
|
||||
showTitle = false,
|
||||
accentColor = exploreAccent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSearchMode) {
|
||||
Text(
|
||||
text = AppStrings.t(lang, "auto.results"),
|
||||
color = Color.White,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Black,
|
||||
modifier = Modifier.padding(start = 16.dp, top = 24.dp, bottom = 12.dp)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
val exploreAccent = Color(activeProfile?.safeAccentColorArgb ?: 0xFFFFFFFF.toInt())
|
||||
Box(Modifier.weight(1f)) {
|
||||
ExploreDropdownFilter(
|
||||
title = AppStrings.t(lang, "auto.content_type"),
|
||||
options = exploreTypeOptions(lang),
|
||||
selected = selectedType,
|
||||
onSelect = { it?.let(onSelectType) },
|
||||
showTitle = false,
|
||||
accentColor = exploreAccent
|
||||
)
|
||||
} else {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
}
|
||||
Box(Modifier.weight(1f)) {
|
||||
ExploreDropdownFilter(
|
||||
title = AppStrings.t(lang, "explore.catalog"),
|
||||
options = catalogOptions,
|
||||
selected = selectedCatalog,
|
||||
onSelect = onSelectCatalog,
|
||||
showTitle = false,
|
||||
accentColor = exploreAccent
|
||||
)
|
||||
}
|
||||
Box(Modifier.weight(1f)) {
|
||||
ExploreDropdownFilter(
|
||||
title = AppStrings.t(lang, "auto.genre_78cde1de"),
|
||||
options = genreOptions,
|
||||
selected = selectedGenre,
|
||||
onSelect = onSelectGenre,
|
||||
showTitle = false,
|
||||
accentColor = exploreAccent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSearchMode) {
|
||||
Text(
|
||||
text = AppStrings.t(lang, "auto.results"),
|
||||
color = Color.White,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Black,
|
||||
modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 12.dp)
|
||||
)
|
||||
} else {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSearchMode && displayedResults.isEmpty() && isDiscoverLoading) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 48.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
androidx.compose.material3.CircularProgressIndicator(color = Color.White)
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 120.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
if (!isSearchMode && displayedResults.isEmpty() && isDiscoverLoading) {
|
||||
mobileGridItems(items = List(9) { it }) {
|
||||
MobileExplorePosterCardSkeleton()
|
||||
}
|
||||
}
|
||||
} else if (isSearchMode && displayedSearchRows.isNotEmpty()) {
|
||||
displayedSearchRows.forEachIndexed { index, row ->
|
||||
item(
|
||||
key = "search-row:${row.title}:$index",
|
||||
span = { GridItemSpan(maxLineSpan) }
|
||||
) {
|
||||
MobileBrowseCategoryRow(
|
||||
title = row.title,
|
||||
items = row.items,
|
||||
onMovieClick = { meta -> onMovieClick(meta, row.sourceAddonTransportUrl, row.sourceAddonCatalogType) },
|
||||
onSearchHistory = viewModel::addToSearchHistory,
|
||||
cardLayout = if (activeProfile?.safePosterLandscapeMode == true) "horizontal" else activeProfile?.safeCardLayout ?: "vertical",
|
||||
activeProfile = activeProfile
|
||||
} else if (isSearchMode && displayedSearchRows.isNotEmpty()) {
|
||||
displayedSearchRows.forEachIndexed { index, row ->
|
||||
item(
|
||||
key = "search-row:${row.title}:$index",
|
||||
span = { GridItemSpan(maxLineSpan) }
|
||||
) {
|
||||
MobileBrowseCategoryRow(
|
||||
title = row.title,
|
||||
items = row.items,
|
||||
onMovieClick = { meta -> onMovieClick(meta, row.sourceAddonTransportUrl, row.sourceAddonCatalogType) },
|
||||
onSearchHistory = viewModel::addToSearchHistory,
|
||||
cardLayout = if (activeProfile?.safePosterLandscapeMode == true) "horizontal" else activeProfile?.safeCardLayout ?: "vertical",
|
||||
activeProfile = activeProfile
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (displayedResults.isEmpty()) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
SearchEmptyState(
|
||||
title = if (isSearchMode) {
|
||||
AppStrings.t(lang, "auto.no_results_found")
|
||||
} else {
|
||||
AppStrings.t(lang, "auto.no_results_yet")
|
||||
},
|
||||
subtitle = if (isSearchMode) {
|
||||
AppStrings.t(lang, "auto.try_a_shorter_or_slightly_different_title")
|
||||
} else {
|
||||
AppStrings.t(lang, "auto.try_different_filters_to_discover_something__1ba561eb")
|
||||
},
|
||||
icon = if (isSearchMode) FluxaIcons.Search else FluxaIcons.Explore
|
||||
)
|
||||
}
|
||||
} else {
|
||||
mobileGridItemsIndexed(
|
||||
items = displayedResults,
|
||||
key = { _, movie -> "${movie.type}:${movie.id}" }
|
||||
) { _, movie ->
|
||||
val source = discoverSourceFor(movie, resultSources)
|
||||
MobileExplorePosterCard(
|
||||
movie = movie,
|
||||
onClick = { onMovieClick(movie, source?.transportUrl, source?.type) }
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (displayedResults.isEmpty()) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
SearchEmptyState(
|
||||
title = if (isSearchMode) {
|
||||
AppStrings.t(lang, "auto.no_results_found")
|
||||
} else {
|
||||
AppStrings.t(lang, "auto.no_results_yet")
|
||||
},
|
||||
subtitle = if (isSearchMode) {
|
||||
AppStrings.t(lang, "auto.try_a_shorter_or_slightly_different_title")
|
||||
} else {
|
||||
AppStrings.t(lang, "auto.try_different_filters_to_discover_something__1ba561eb")
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
mobileGridItemsIndexed(
|
||||
items = displayedResults,
|
||||
key = { _, movie -> "${movie.type}:${movie.id}" }
|
||||
) { _, movie ->
|
||||
val source = discoverSourceFor(movie, resultSources)
|
||||
MobileExplorePosterCard(
|
||||
movie = movie,
|
||||
onClick = { onMovieClick(movie, source?.transportUrl, source?.type) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -282,3 +300,34 @@ private fun MobileExplorePosterCard(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MobileExplorePosterCardSkeleton() {
|
||||
val transition = rememberInfiniteTransition(label = "explore-skeleton")
|
||||
val alpha by transition.animateFloat(
|
||||
initialValue = 0.05f,
|
||||
targetValue = 0.12f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(FluxaDimensions.AnimDuration.ambientColor, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "explore-skeleton-alpha"
|
||||
)
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(2f / 3f)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(Color.White.copy(alpha = alpha))
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.7f)
|
||||
.height(14.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(Color.White.copy(alpha = alpha))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ internal fun MobileDetailScreen(
|
|||
onPlayPrimary: () -> Unit,
|
||||
onStreamClick: (Stream) -> Unit,
|
||||
onPlayEpisode: (Video) -> Unit,
|
||||
onSimilarClick: (Meta) -> Unit = {},
|
||||
onDownloadEpisode: (Video?) -> Unit = {},
|
||||
onDownloadEpisodes: (List<Video>) -> Unit = {},
|
||||
onMarkEpisodeWatched: (Video, Boolean) -> Unit,
|
||||
|
|
@ -114,7 +115,21 @@ internal fun MobileDetailScreen(
|
|||
?: initialMeta?.poster
|
||||
var heroTrailer by remember(detail?.id) { mutableStateOf<DetailTrailer?>(null) }
|
||||
val listState = rememberLazyListState()
|
||||
val showStickyBar by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }
|
||||
val heroCollapse by remember {
|
||||
derivedStateOf {
|
||||
val hero = listState.layoutInfo.visibleItemsInfo.firstOrNull()
|
||||
when {
|
||||
listState.firstVisibleItemIndex > 0 -> 1f
|
||||
hero == null || hero.size == 0 -> 0f
|
||||
else -> (listState.firstVisibleItemScrollOffset.toFloat() / hero.size).coerceIn(0f, 1f)
|
||||
}
|
||||
}
|
||||
}
|
||||
val showStickyBar = heroCollapse > 0.6f
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.firstVisibleItemIndex > 0 }
|
||||
.collect { heroGone -> if (heroGone) heroTrailer = null }
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
var showSeasonSelector by remember(detail?.id) { mutableStateOf(false) }
|
||||
var showEpisodeSortSelector by remember(detail?.id, selectedSeason) { mutableStateOf(false) }
|
||||
|
|
@ -149,23 +164,48 @@ internal fun MobileDetailScreen(
|
|||
.filter { !detailIsUpcoming(it.released) }
|
||||
.sortedWith(compareBy<Video>({ it.season ?: selectedSeason }, { it.number ?: Int.MAX_VALUE }))
|
||||
}
|
||||
val horizontalEpisodesState = rememberLazyListState()
|
||||
var scrolledToResume by remember(detail?.id) { mutableStateOf(false) }
|
||||
LaunchedEffect(visibleEpisodes, resumeVideoId, activeTab) {
|
||||
if (scrolledToResume || activeTab != "episodes" || resumeVideoId == null) return@LaunchedEffect
|
||||
val idx = visibleEpisodes.indexOfFirst { it.id == resumeVideoId }
|
||||
if (idx < 0) return@LaunchedEffect
|
||||
scrolledToResume = true
|
||||
if (idx <= 2) return@LaunchedEffect
|
||||
if (episodeCardsLayout == "horizontal") {
|
||||
horizontalEpisodesState.scrollToItem(idx)
|
||||
} else {
|
||||
listState.scrollToItem(4 + idx)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize().background(Color(0xFF050505))) {
|
||||
Box(modifier = Modifier.fillMaxSize().background(FluxaColors.background)) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = 120.dp)
|
||||
) {
|
||||
item(key = "detail-hero") {
|
||||
MobileDetailHero(
|
||||
heroImage = heroImage,
|
||||
onBack = onBack,
|
||||
lang = lang,
|
||||
activeTrailer = heroTrailer,
|
||||
onStopTrailer = { heroTrailer = null },
|
||||
onSelectTrailer = if (trailerOnHero) ({ heroTrailer = it }) else null,
|
||||
accentColor = accentColor
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.graphicsLayer {
|
||||
if (listState.firstVisibleItemIndex == 0) {
|
||||
val offset = listState.firstVisibleItemScrollOffset.toFloat()
|
||||
translationY = offset * 0.5f
|
||||
alpha = 1f - (offset / size.height.coerceAtLeast(1f)) * 0.85f
|
||||
}
|
||||
}
|
||||
) {
|
||||
MobileDetailHero(
|
||||
heroImage = heroImage,
|
||||
onBack = onBack,
|
||||
lang = lang,
|
||||
activeTrailer = heroTrailer,
|
||||
featuredTrailer = if (trailerOnHero) trailers.firstOrNull() else null,
|
||||
onStopTrailer = { heroTrailer = null },
|
||||
onSelectTrailer = if (trailerOnHero) ({ heroTrailer = it }) else null,
|
||||
accentColor = accentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
|
|
@ -189,9 +229,10 @@ internal fun MobileDetailScreen(
|
|||
}
|
||||
|
||||
item {
|
||||
val tabs = remember(type, trailers.isNotEmpty(), similarItems.isNotEmpty(), lang) {
|
||||
val tabs = remember(type, hasStreamProviders, streams.isNotEmpty(), trailers.isNotEmpty(), similarItems.isNotEmpty(), lang) {
|
||||
buildList {
|
||||
if (type == "series") add("episodes" to AppStrings.t(lang, "auto.episodes"))
|
||||
if (hasStreamProviders || streams.isNotEmpty()) add("sources" to AppStrings.t(lang, "auto.sources"))
|
||||
if (trailers.isNotEmpty()) add("trailers" to AppStrings.t(lang, "auto.trailers"))
|
||||
if (similarItems.isNotEmpty()) add("similar" to AppStrings.t(lang, "auto.similar_titles"))
|
||||
}
|
||||
|
|
@ -206,12 +247,12 @@ internal fun MobileDetailScreen(
|
|||
val selected = activeTab == key
|
||||
val textColor by animateColorAsState(
|
||||
targetValue = if (selected) Color.White else Color.White.copy(alpha = 0.6f),
|
||||
animationSpec = tween(180),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.scaleAlpha),
|
||||
label = "detailTabText"
|
||||
)
|
||||
val indicatorWidth by animateDpAsState(
|
||||
targetValue = if (selected) 44.dp else 0.dp,
|
||||
animationSpec = tween(190),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.scaleAlpha),
|
||||
label = "detailTabIndicator"
|
||||
)
|
||||
Column(
|
||||
|
|
@ -359,16 +400,18 @@ internal fun MobileDetailScreen(
|
|||
}
|
||||
}
|
||||
if (visibleEpisodes.isEmpty()) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 28.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(color = Color.White.copy(alpha = 0.7f))
|
||||
} else {
|
||||
if (isLoading) {
|
||||
mobileItems((0 until 4).toList(), key = { "episode-skeleton-$it" }) {
|
||||
MobileEpisodeRowSkeleton()
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 28.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = AppStrings.t(lang, "auto.no_episode_information_found"),
|
||||
color = Color.White.copy(alpha = 0.58f),
|
||||
|
|
@ -381,6 +424,7 @@ internal fun MobileDetailScreen(
|
|||
if (episodeCardsLayout == "horizontal" && visibleEpisodes.isNotEmpty()) {
|
||||
item(key = "episodes-horizontal") {
|
||||
LazyRow(
|
||||
state = horizontalEpisodesState,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
|
|
@ -509,7 +553,9 @@ internal fun MobileDetailScreen(
|
|||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
mobileItems(similarItems, key = { "${it.type}:${it.id}" }) { item ->
|
||||
SimilarContentCard(item.toSimilarItemUiModel(), Color.White) {}
|
||||
SimilarContentCard(item.toSimilarItemUiModel(), Color.White) {
|
||||
onSimilarClick(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -556,14 +602,14 @@ internal fun MobileDetailScreen(
|
|||
|
||||
AnimatedVisibility(
|
||||
visible = showStickyBar,
|
||||
enter = fadeIn(tween(150)) + slideInVertically { -it },
|
||||
exit = fadeOut(tween(150)) + slideOutVertically { -it },
|
||||
enter = fadeIn(tween(FluxaDimensions.AnimDuration.heroSnap)) + slideInVertically { -it },
|
||||
exit = fadeOut(tween(FluxaDimensions.AnimDuration.heroSnap)) + slideOutVertically { -it },
|
||||
modifier = Modifier.align(Alignment.TopStart).fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(0xFF050505).copy(alpha = 0.96f))
|
||||
.background(FluxaColors.background.copy(alpha = 0.96f))
|
||||
.statusBarsPadding()
|
||||
.padding(start = 4.dp, end = 16.dp, top = 4.dp, bottom = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
|
@ -575,7 +621,12 @@ internal fun MobileDetailScreen(
|
|||
.clickable { onBack() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(FluxaIcons.ArrowBack, null, tint = Color.White, modifier = Modifier.size(22.dp))
|
||||
Icon(
|
||||
FluxaIcons.ArrowBack,
|
||||
contentDescription = AppStrings.t(lang, "common.back"),
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Box(
|
||||
|
|
@ -615,7 +666,7 @@ internal fun MobileDetailScreen(
|
|||
) {
|
||||
Icon(
|
||||
if (isInWatchlist) FluxaIcons.Check else FluxaIcons.Add,
|
||||
null,
|
||||
contentDescription = AppStrings.t(lang, if (isInWatchlist) "auto.in_list" else "auto.my_list"),
|
||||
tint = if (isInWatchlist) accentColor else Color.White,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ internal fun MobileLibraryDashboard(
|
|||
}
|
||||
}
|
||||
val backgroundColor = if (amoledMode) Color.Black else Color(0xFF05070B)
|
||||
val cardColor = if (amoledMode) Color(0xFF050505) else Color(0xFF111620).copy(alpha = 0.82f)
|
||||
val cardColor = if (amoledMode) FluxaColors.backgroundAmoled else FluxaColors.backgroundNavy.copy(alpha = 0.82f)
|
||||
val plannedChunked = remember(plannedItems) { plannedItems.chunked(3) }
|
||||
val completedChunked = remember(completedItems) { completedItems.chunked(3) }
|
||||
val favoritesChunked = remember(favoriteItems) { favoriteItems.chunked(3) }
|
||||
|
|
@ -188,17 +188,17 @@ internal fun MobileLibraryDashboard(
|
|||
val onAccentColor = if (accent.luminance() > 0.5f) Color.Black else Color.White
|
||||
val chipBackground by animateColorAsState(
|
||||
targetValue = if (selected) accent else cardColor,
|
||||
animationSpec = tween(180, easing = FastOutSlowInEasing),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.scaleAlpha, easing = FastOutSlowInEasing),
|
||||
label = "libraryChipBackground"
|
||||
)
|
||||
val chipBorder by animateColorAsState(
|
||||
targetValue = if (selected) accent else Color.White.copy(0.1f),
|
||||
animationSpec = tween(180, easing = FastOutSlowInEasing),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.scaleAlpha, easing = FastOutSlowInEasing),
|
||||
label = "libraryChipBorder"
|
||||
)
|
||||
val chipScale by animateFloatAsState(
|
||||
targetValue = if (selected) 1.04f else 1f,
|
||||
animationSpec = tween(180, easing = FastOutSlowInEasing),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.scaleAlpha, easing = FastOutSlowInEasing),
|
||||
label = "libraryChipScale"
|
||||
)
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -425,7 +425,7 @@ internal fun MobileTrackPickerOverlay(
|
|||
|
||||
Crossfade(
|
||||
targetState = settingsMode,
|
||||
animationSpec = tween(180),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.scaleAlpha),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ fun MobileBrowseCategoryRow(
|
|||
cardLayout: String,
|
||||
activeProfile: UserProfile? = null
|
||||
) {
|
||||
Column(modifier = modifier.animateContentSize(animationSpec = tween(220))) {
|
||||
Column(modifier = modifier.animateContentSize(animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand))) {
|
||||
SearchSectionTitle(title)
|
||||
androidx.compose.foundation.lazy.LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
|
|
|
|||
|
|
@ -414,8 +414,8 @@ fun MobileSourceSelectionScreen(
|
|||
}
|
||||
AnimatedVisibility(
|
||||
visible = downloadActionStream != null,
|
||||
enter = fadeIn(tween(140)) + slideInVertically(tween(220, easing = FastOutSlowInEasing)) { it / 4 },
|
||||
exit = fadeOut(tween(120)) + slideOutVertically(tween(180, easing = FastOutSlowInEasing)) { it / 4 }
|
||||
enter = fadeIn(tween(FluxaDimensions.AnimDuration.quick)) + slideInVertically(tween(FluxaDimensions.AnimDuration.contentExpand, easing = FastOutSlowInEasing)) { it / 4 },
|
||||
exit = fadeOut(tween(FluxaDimensions.AnimDuration.heightAnim)) + slideOutVertically(tween(FluxaDimensions.AnimDuration.scaleAlpha, easing = FastOutSlowInEasing)) { it / 4 }
|
||||
) {
|
||||
downloadActionStream?.let { stream ->
|
||||
MobileSourceDownloadActionSheet(
|
||||
|
|
@ -482,12 +482,12 @@ private fun SourceChip(
|
|||
) {
|
||||
val background by animateColorAsState(
|
||||
targetValue = if (selected) accent else Color.White.copy(alpha = 0.08f),
|
||||
animationSpec = tween(180, easing = FastOutSlowInEasing),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.scaleAlpha, easing = FastOutSlowInEasing),
|
||||
label = "sourceChipBackground"
|
||||
)
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (selected) 1.04f else 1f,
|
||||
animationSpec = tween(180, easing = FastOutSlowInEasing),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.scaleAlpha, easing = FastOutSlowInEasing),
|
||||
label = "sourceChipScale"
|
||||
)
|
||||
Box(
|
||||
|
|
@ -526,7 +526,7 @@ private fun SourcePageStreamRow(
|
|||
val rawTitle = stream.streamRawBody()
|
||||
val rowAlpha by animateFloatAsState(
|
||||
targetValue = if (isLoading) 0.72f else 1f,
|
||||
animationSpec = tween(180),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.scaleAlpha),
|
||||
label = "sourceRowAlpha"
|
||||
)
|
||||
Column(
|
||||
|
|
@ -534,7 +534,7 @@ private fun SourcePageStreamRow(
|
|||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 5.dp)
|
||||
.graphicsLayer { alpha = rowAlpha }
|
||||
.animateContentSize(animationSpec = tween(220, easing = FastOutSlowInEasing))
|
||||
.animateContentSize(animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand, easing = FastOutSlowInEasing))
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color(0xFF2B2A34))
|
||||
.border(1.dp, Color.White.copy(alpha = 0.05f), RoundedCornerShape(8.dp))
|
||||
|
|
@ -586,7 +586,7 @@ private fun MobileSourceDownloadActionSheet(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp))
|
||||
.background(Color(0xFF171717))
|
||||
.background(FluxaColors.surfaceDark)
|
||||
.pointerInput(stream) {
|
||||
detectTapGestures(onTap = {})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,8 +48,10 @@ import com.fluxa.app.ui.catalog.ProfileEditScreen
|
|||
import com.fluxa.app.ui.catalog.ProfileScreen
|
||||
import com.fluxa.app.ui.catalog.SearchScreen
|
||||
import com.fluxa.app.ui.catalog.UpdateManager
|
||||
import com.fluxa.app.ui.catalog.tvNavDestination
|
||||
import com.fluxa.app.ui.catalog.WatchlistScreen
|
||||
import com.fluxa.app.ui.catalog.WelcomeScreen
|
||||
import com.fluxa.app.ui.catalog.FluxaDimensions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@Composable
|
||||
|
|
@ -75,6 +77,13 @@ internal fun AppRoutesHost(
|
|||
onUpdateInfoChanged: (UpdateManager.UpdateInfo?) -> Unit,
|
||||
navigateBackSafely: () -> Unit
|
||||
) {
|
||||
val tvNavActions = com.fluxa.app.ui.catalog.TvNavActions(
|
||||
onHome = { if (currentScreen.tvNavDestination() != com.fluxa.app.ui.catalog.TvNavDestination.Home) navigator.navigateTo(Screen.Home, clearStack = true) },
|
||||
onSearch = { if (currentScreen.tvNavDestination() != com.fluxa.app.ui.catalog.TvNavDestination.Search) navigator.navigateTo(Screen.Search, clearStack = true) },
|
||||
onWatchlist = { if (currentScreen.tvNavDestination() != com.fluxa.app.ui.catalog.TvNavDestination.Library) navigator.navigateTo(Screen.Watchlist, clearStack = true) },
|
||||
onExplore = { if (currentScreen.tvNavDestination() != com.fluxa.app.ui.catalog.TvNavDestination.Discover) navigator.navigateTo(Screen.Explore(), clearStack = true) },
|
||||
onSettings = { if (currentScreen.tvNavDestination() != com.fluxa.app.ui.catalog.TvNavDestination.Settings) navigator.navigateTo(Screen.Settings(), clearStack = true) }
|
||||
)
|
||||
AnimatedContent(
|
||||
targetState = currentScreen,
|
||||
modifier = Modifier.then(
|
||||
|
|
@ -93,23 +102,23 @@ internal fun AppRoutesHost(
|
|||
val direction = navDirection(initialState, targetState)
|
||||
val distance = if (deviceType == DeviceType.Mobile) 92 else 54
|
||||
(
|
||||
fadeIn(animationSpec = tween(220, easing = FastOutSlowInEasing)) +
|
||||
fadeIn(animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand, easing = FastOutSlowInEasing)) +
|
||||
slideInHorizontally(
|
||||
animationSpec = tween(260, easing = FastOutSlowInEasing),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.settingsExpandAlt, easing = FastOutSlowInEasing),
|
||||
initialOffsetX = { direction * distance }
|
||||
) +
|
||||
scaleIn(
|
||||
animationSpec = tween(260, easing = FastOutSlowInEasing),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.settingsExpandAlt, easing = FastOutSlowInEasing),
|
||||
initialScale = 0.985f
|
||||
)
|
||||
) togetherWith (
|
||||
fadeOut(animationSpec = tween(160, easing = LinearOutSlowInEasing)) +
|
||||
fadeOut(animationSpec = tween(FluxaDimensions.AnimDuration.routeExit, easing = LinearOutSlowInEasing)) +
|
||||
slideOutHorizontally(
|
||||
animationSpec = tween(220, easing = FastOutSlowInEasing),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand, easing = FastOutSlowInEasing),
|
||||
targetOffsetX = { -direction * distance }
|
||||
) +
|
||||
scaleOut(
|
||||
animationSpec = tween(220, easing = FastOutSlowInEasing),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand, easing = FastOutSlowInEasing),
|
||||
targetScale = 0.992f
|
||||
)
|
||||
)
|
||||
|
|
@ -155,9 +164,9 @@ internal fun AppRoutesHost(
|
|||
onContinueWithoutAccount = {
|
||||
val guest = UserProfile(
|
||||
id = java.util.UUID.randomUUID().toString(),
|
||||
email = AppStrings.t("en", "auth.guest_name"),
|
||||
email = AppStrings.t("en", "auth.primary_profile_name"),
|
||||
authKey = "",
|
||||
isGuest = true
|
||||
isGuest = false
|
||||
)
|
||||
profileManager.saveProfile(guest)
|
||||
profileManager.setLastActiveProfile(guest)
|
||||
|
|
@ -191,7 +200,8 @@ internal fun AppRoutesHost(
|
|||
navigateBackSafely,
|
||||
homeViewModel,
|
||||
initialType = screen.initialType,
|
||||
initialGenre = screen.initialGenre
|
||||
initialGenre = screen.initialGenre,
|
||||
tvNavActions = tvNavActions
|
||||
)
|
||||
is Screen.Search -> SearchScreen(
|
||||
activeProfile,
|
||||
|
|
@ -201,7 +211,8 @@ internal fun AppRoutesHost(
|
|||
navigator.navigateTo(meta.detailScreen(sourceAddonTransportUrl, sourceAddonCatalogType))
|
||||
},
|
||||
navigateBackSafely,
|
||||
homeViewModel
|
||||
homeViewModel,
|
||||
tvNavActions
|
||||
)
|
||||
is Screen.Calendar -> CalendarScreen(
|
||||
activeProfile = activeProfile,
|
||||
|
|
@ -248,7 +259,8 @@ internal fun AppRoutesHost(
|
|||
onActiveProfileChanged(it)
|
||||
profileManager.saveProfile(it)
|
||||
homeViewModel.applyUpdatedProfile(it)
|
||||
}
|
||||
},
|
||||
tvNavActions = tvNavActions
|
||||
)
|
||||
is Screen.Detail -> DetailRoute(screen, activeProfile, navigator, navigateBackSafely)
|
||||
is Screen.Sources -> SourcesRoute(screen, activeProfile, navigator, navigateBackSafely)
|
||||
|
|
@ -293,7 +305,8 @@ internal fun AppRoutesHost(
|
|||
onShowSimklSheet = onShowSimklSheet,
|
||||
onTraktDeviceAuthChanged = onTraktDeviceAuthChanged,
|
||||
onPendingMalCodeVerifierChanged = onPendingMalCodeVerifierChanged,
|
||||
onUpdateInfoChanged = onUpdateInfoChanged
|
||||
onUpdateInfoChanged = onUpdateInfoChanged,
|
||||
tvNavActions = tvNavActions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue