homescreen fix

This commit is contained in:
tapframe 2026-01-29 13:43:12 +05:30
parent 1b5b37e10e
commit c22fbc30be
7 changed files with 187 additions and 87 deletions

View file

@ -0,0 +1,50 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
</profile>
</component>

6
.idea/vcs.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View file

@ -80,7 +80,7 @@ fun CatalogRowSection(
) {
items(
items = catalogRow.items,
key = { "${catalogRow.catalogId}_${it.id}" }
key = { "${catalogRow.type}_${catalogRow.catalogId}_${it.id}" }
) { item ->
ContentCard(
item = item,

View file

@ -122,41 +122,94 @@ private fun MetaDetailsContent(
val isSeries = meta.type == ContentType.SERIES || meta.videos.isNotEmpty()
val nextEpisode = episodesForSeason.firstOrNull()
TvLazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 48.dp)
) {
// Hero section with backdrop
item {
HeroSection(
meta = meta,
nextEpisode = nextEpisode,
onPlayClick = {
val videoId = if (isSeries && nextEpisode != null) {
nextEpisode.id
} else {
meta.id
}
onPlayClick(videoId)
}
Box(modifier = Modifier.fillMaxSize()) {
// Sticky background image - stays fixed in place while content scrolls
Box(modifier = Modifier.fillMaxSize()) {
AsyncImage(
model = meta.background ?: meta.poster,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
// Light global dim so text remains readable; the main fade happens in the section below the hero.
Box(
modifier = Modifier
.fillMaxSize()
.background(NuvioColors.Background.copy(alpha = 0.08f))
)
// Left side gradient for better text readability
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.horizontalGradient(
colors = listOf(
NuvioColors.Background.copy(alpha = 0.85f),
NuvioColors.Background.copy(alpha = 0.5f),
Color.Transparent
),
startX = 0f,
endX = 600f
)
)
)
}
// Season tabs and episodes for series
if (isSeries && seasons.isNotEmpty()) {
// Scrollable content on top of the fixed background
TvLazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 48.dp)
) {
// Hero content section (fills screen height, content at bottom)
item {
SeasonTabs(
seasons = seasons,
selectedSeason = selectedSeason,
onSeasonSelected = onSeasonSelected
HeroContentSection(
meta = meta,
nextEpisode = nextEpisode,
onPlayClick = {
val videoId = if (isSeries && nextEpisode != null) {
nextEpisode.id
} else {
meta.id
}
onPlayClick(videoId)
}
)
}
item {
EpisodesRow(
episodes = episodesForSeason,
onEpisodeClick = onEpisodeClick
)
// Season tabs and episodes for series - fully transparent over backdrop
if (isSeries && seasons.isNotEmpty()) {
item {
// This section owns the fade-to-dark background (smooth at top, darker as you scroll down).
Box(
modifier = Modifier
.fillMaxWidth()
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
NuvioColors.Background.copy(alpha = 0.55f),
NuvioColors.Background.copy(alpha = 0.85f),
NuvioColors.Background
)
)
)
) {
Column(modifier = Modifier.fillMaxWidth()) {
SeasonTabs(
seasons = seasons,
selectedSeason = selectedSeason,
onSeasonSelected = onSeasonSelected
)
EpisodesRow(
episodes = episodesForSeason,
onEpisodeClick = onEpisodeClick
)
Spacer(modifier = Modifier.height(140.dp))
}
}
}
}
}
}
@ -164,62 +217,22 @@ private fun MetaDetailsContent(
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun HeroSection(
private fun HeroContentSection(
meta: Meta,
nextEpisode: Video?,
onPlayClick: () -> Unit
) {
// Full height hero with content at bottom
Box(
modifier = Modifier
.fillMaxSize()
.fillMaxWidth()
.height(540.dp), // Full screen height for TV
contentAlignment = Alignment.BottomStart
) {
// Background image
AsyncImage(
model = meta.background ?: meta.poster,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
// Gradient overlay
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
NuvioColors.Background.copy(alpha = 0.3f),
NuvioColors.Background.copy(alpha = 0.7f),
NuvioColors.Background
),
startY = 0f,
endY = Float.POSITIVE_INFINITY
)
)
)
// Left side gradient for better text readability
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.horizontalGradient(
colors = listOf(
NuvioColors.Background.copy(alpha = 0.8f),
NuvioColors.Background.copy(alpha = 0.4f),
Color.Transparent
),
startX = 0f,
endX = 800f
)
)
)
// Content
// Content at the bottom
Column(
modifier = Modifier
.fillMaxSize()
.fillMaxWidth()
.padding(start = 48.dp, end = 48.dp, bottom = 32.dp),
verticalArrangement = Arrangement.Bottom
) {
@ -488,12 +501,21 @@ private fun SeasonTabs(
Column(
modifier = Modifier
.fillMaxWidth()
.background(
Brush.verticalGradient(
colors = listOf(
NuvioColors.Background.copy(alpha = 0.2f),
NuvioColors.Background.copy(alpha = 0.4f)
)
)
)
.padding(horizontal = 48.dp, vertical = 24.dp)
) {
TabRow(
selectedTabIndex = selectedIndex,
modifier = Modifier.fillMaxWidth(),
separator = { Spacer(modifier = Modifier.width(24.dp)) }
separator = { Spacer(modifier = Modifier.width(24.dp)) },
containerColor = Color.Transparent
) {
seasons.forEachIndexed { index, season ->
Tab(
@ -524,8 +546,17 @@ private fun EpisodesRow(
onEpisodeClick: (Video) -> Unit
) {
TvLazyRow(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = 48.dp),
modifier = Modifier
.fillMaxWidth()
.background(
Brush.verticalGradient(
colors = listOf(
NuvioColors.Background.copy(alpha = 0.3f),
NuvioColors.Background.copy(alpha = 0.5f)
)
)
),
contentPadding = PaddingValues(horizontal = 48.dp, vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
items(episodes, key = { it.id }) { episode ->

View file

@ -61,7 +61,7 @@ fun HomeScreen(
) {
items(
items = uiState.catalogRows,
key = { "${it.addonId}_${it.catalogId}" }
key = { "${it.addonId}_${it.type}_${it.catalogId}" }
) { catalogRow ->
CatalogRowSection(
catalogRow = catalogRow,
@ -72,7 +72,8 @@ fun HomeScreen(
viewModel.onEvent(
HomeEvent.OnLoadMoreCatalog(
catalogId = catalogRow.catalogId,
addonId = catalogRow.addonId
addonId = catalogRow.addonId,
type = catalogRow.type.toApiString()
)
)
}

View file

@ -11,6 +11,6 @@ data class HomeUiState(
sealed class HomeEvent {
data class OnItemClick(val itemId: String, val itemType: String) : HomeEvent()
data class OnLoadMoreCatalog(val catalogId: String, val addonId: String) : HomeEvent()
data class OnLoadMoreCatalog(val catalogId: String, val addonId: String, val type: String) : HomeEvent()
data object OnRetry : HomeEvent()
}

View file

@ -37,7 +37,7 @@ class HomeViewModel @Inject constructor(
fun onEvent(event: HomeEvent) {
when (event) {
is HomeEvent.OnItemClick -> navigateToDetail(event.itemId, event.itemType)
is HomeEvent.OnLoadMoreCatalog -> loadMoreCatalogItems(event.catalogId, event.addonId)
is HomeEvent.OnLoadMoreCatalog -> loadMoreCatalogItems(event.catalogId, event.addonId, event.type)
HomeEvent.OnRetry -> loadAllCatalogs()
}
}
@ -59,7 +59,11 @@ class HomeViewModel @Inject constructor(
// Build catalog order based on addon manifest order
addons.forEach { addon ->
addon.catalogs.forEach { catalog ->
val key = "${addon.id}_${catalog.id}"
val key = catalogKey(
addonId = addon.id,
type = catalog.type.toApiString(),
catalogId = catalog.id
)
if (key !in catalogOrder) {
catalogOrder.add(key)
}
@ -91,7 +95,11 @@ class HomeViewModel @Inject constructor(
).collect { result ->
when (result) {
is NetworkResult.Success -> {
val key = "${addon.id}_${catalog.id}"
val key = catalogKey(
addonId = addon.id,
type = catalog.type.toApiString(),
catalogId = catalog.id
)
catalogsMap[key] = result.data
updateCatalogRows()
}
@ -104,8 +112,8 @@ class HomeViewModel @Inject constructor(
}
}
private fun loadMoreCatalogItems(catalogId: String, addonId: String) {
val key = "${addonId}_${catalogId}"
private fun loadMoreCatalogItems(catalogId: String, addonId: String, type: String) {
val key = catalogKey(addonId = addonId, type = type, catalogId = catalogId)
val currentRow = catalogsMap[key] ?: return
if (currentRow.isLoading || !currentRow.hasMore) return
@ -157,4 +165,8 @@ class HomeViewModel @Inject constructor(
private fun navigateToDetail(itemId: String, itemType: String) {
_uiState.update { it.copy(selectedItemId = itemId) }
}
private fun catalogKey(addonId: String, type: String, catalogId: String): String {
return "${addonId}_${type}_${catalogId}"
}
}