homescreen fix
This commit is contained in:
parent
1b5b37e10e
commit
c22fbc30be
7 changed files with 187 additions and 87 deletions
50
.idea/inspectionProfiles/Project_Default.xml
Normal file
50
.idea/inspectionProfiles/Project_Default.xml
Normal 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
6
.idea/vcs.xml
Normal 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>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue