From 44683af3c1a6d5a89b45ebfcfb6c22fe1d4abdae Mon Sep 17 00:00:00 2001 From: KhooLy <73142442+KhooLy@users.noreply.github.com> Date: Fri, 3 Jul 2026 12:19:54 +0300 Subject: [PATCH] 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. --- .../com/fluxa/app/ui/catalog/LoginScreen.kt | 16 +- .../app/ui/catalog/ParentsGuideComponents.kt | 12 +- .../fluxa/app/ui/catalog/PlayerOverlays.kt | 6 +- .../app/ui/catalog/PlayerRuntimeOverlays.kt | 16 +- .../app/ui/catalog/PlayerSidebarUtils.kt | 2 +- .../app/ui/catalog/PlayerSourceSidebars.kt | 10 +- .../app/ui/catalog/TvProfileComponents.kt | 4 +- .../ui/catalog/mobile/MobileExploreScreen.kt | 341 ++++++++++-------- .../mobile/detail/MobileDetailScreen.kt | 111 ++++-- .../mobile/library/MobileLibraryComponents.kt | 8 +- .../mobile/player/MobilePlayerDialogs.kt | 2 +- .../mobile/search/MobileSearchScreen.kt | 2 +- .../mobile/source/SourceSelectionScreen.kt | 14 +- .../com/fluxa/app/ui/routes/AppRoutesHost.kt | 37 +- 14 files changed, 347 insertions(+), 234 deletions(-) diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/LoginScreen.kt b/app/src/main/java/com/fluxa/app/ui/catalog/LoginScreen.kt index b5f4ebc..14f86ed 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/LoginScreen.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/LoginScreen.kt @@ -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 = "" ) diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/ParentsGuideComponents.kt b/app/src/main/java/com/fluxa/app/ui/catalog/ParentsGuideComponents.kt index f33c1f7..276e883 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/ParentsGuideComponents.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/ParentsGuideComponents.kt @@ -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() diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerOverlays.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerOverlays.kt index fe878cf..47df0e5 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerOverlays.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerOverlays.kt @@ -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" diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerRuntimeOverlays.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerRuntimeOverlays.kt index 565e00f..d10cb67 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerRuntimeOverlays.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerRuntimeOverlays.kt @@ -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) diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerSidebarUtils.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerSidebarUtils.kt index 53310f6..8d20a8d 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerSidebarUtils.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerSidebarUtils.kt @@ -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" ) diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerSourceSidebars.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerSourceSidebars.kt index 71fe4fe..e298173 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerSourceSidebars.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerSourceSidebars.kt @@ -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() } diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/TvProfileComponents.kt b/app/src/main/java/com/fluxa/app/ui/catalog/TvProfileComponents.kt index 47dd6b5..ddaa487 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/TvProfileComponents.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/TvProfileComponents.kt @@ -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 diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/mobile/MobileExploreScreen.kt b/app/src/main/java/com/fluxa/app/ui/catalog/mobile/MobileExploreScreen.kt index d3f3e94..4994e13 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/mobile/MobileExploreScreen.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/mobile/MobileExploreScreen.kt @@ -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)) + ) + } +} diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/mobile/detail/MobileDetailScreen.kt b/app/src/main/java/com/fluxa/app/ui/catalog/mobile/detail/MobileDetailScreen.kt index 63b910f..71b5cb9 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/mobile/detail/MobileDetailScreen.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/mobile/detail/MobileDetailScreen.kt @@ -84,6 +84,7 @@ internal fun MobileDetailScreen( onPlayPrimary: () -> Unit, onStreamClick: (Stream) -> Unit, onPlayEpisode: (Video) -> Unit, + onSimilarClick: (Meta) -> Unit = {}, onDownloadEpisode: (Video?) -> Unit = {}, onDownloadEpisodes: (List