diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index a8bf70529..53edcb914 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -1,5 +1,6 @@ package com.nuvio.app +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Extension @@ -74,6 +75,7 @@ fun App() { Scaffold( containerColor = MaterialTheme.colorScheme.background, + contentWindowInsets = WindowInsets(0), bottomBar = { if (currentRoute == TabsRoute::class.qualifiedName) { NavigationBar( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioComponents.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioComponents.kt index eee214cce..7a6d68a72 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioComponents.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NuvioComponents.kt @@ -10,10 +10,13 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope @@ -45,17 +48,18 @@ fun NuvioScreen( modifier: Modifier = Modifier, content: LazyListScope.() -> Unit, ) { + val statusBarTop = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() LazyColumn( modifier = modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background), contentPadding = PaddingValues( - start = 18.dp, - top = 12.dp + nuvioPlatformExtraTopPadding, - end = 18.dp, - bottom = 22.dp + nuvioPlatformExtraBottomPadding, + start = 16.dp, + top = 10.dp + statusBarTop + nuvioPlatformExtraTopPadding, + end = 16.dp, + bottom = 18.dp + nuvioPlatformExtraBottomPadding, ), - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), content = content, ) } @@ -86,8 +90,12 @@ fun NuvioScreenHeader( modifier: Modifier = Modifier, actions: @Composable RowScope.() -> Unit = {}, ) { + val statusBarTop = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() Row( - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(top = statusBarTop, bottom = 4.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Bottom, ) { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonsScreen.kt index 038f172f5..ccb49ba0f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/addons/AddonsScreen.kt @@ -74,7 +74,7 @@ fun AddonsScreen( val overview = remember(sortedAddons) { sortedAddons.toOverview() } NuvioScreen(modifier = modifier) { - item { + stickyHeader { NuvioScreenHeader( title = "Addons", ) { diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt index 38bbc36d2..e9606909e 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeScreen.kt @@ -8,12 +8,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.nuvio.app.core.ui.NuvioIconActionButton import com.nuvio.app.core.ui.NuvioScreen -import com.nuvio.app.core.ui.NuvioScreenHeader import com.nuvio.app.features.addons.AddonRepository import com.nuvio.app.features.home.components.HomeCatalogRowSection import com.nuvio.app.features.home.components.HomeEmptyStateCard +import com.nuvio.app.features.home.components.HomeSkeletonRow @Composable fun HomeScreen( @@ -45,18 +44,6 @@ fun HomeScreen( } NuvioScreen(modifier = modifier) { - item { - NuvioScreenHeader( - title = "Home", - ) { - NuvioIconActionButton( - icon = Icons.Rounded.Refresh, - contentDescription = "Refresh catalog rows", - onClick = { HomeRepository.refresh(addonsUiState.addons, force = true) }, - ) - } - } - when { addonsUiState.addons.none { it.manifest != null } -> { item { @@ -68,11 +55,8 @@ fun HomeScreen( } homeUiState.isLoading && homeUiState.sections.isEmpty() -> { - item { - HomeEmptyStateCard( - title = "Loading catalogs", - message = "Pulling feed-compatible catalogs from your installed addons.", - ) + items(3) { + HomeSkeletonRow() } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeCatalogSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeCatalogSection.kt index 8e6b13b5f..6dd07ef61 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeCatalogSection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeCatalogSection.kt @@ -21,15 +21,14 @@ fun HomeCatalogRowSection( ) { Column( modifier = modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), ) { HomeCatalogSectionHeader( title = section.title, - subtitle = section.subtitle, onViewAllClick = onViewAllClick, ) LazyRow( - horizontalArrangement = Arrangement.spacedBy(14.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), ) { items( items = section.items, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeCatalogSectionHeader.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeCatalogSectionHeader.kt index 6d52f089a..a98ed0d87 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeCatalogSectionHeader.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeCatalogSectionHeader.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.unit.dp @Composable fun HomeCatalogSectionHeader( title: String, - subtitle: String, modifier: Modifier = Modifier, onViewAllClick: (() -> Unit)? = null, ) { @@ -39,29 +38,21 @@ fun HomeCatalogSectionHeader( ) { Text( text = title, - style = MaterialTheme.typography.headlineLarge, + style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, ) Box( modifier = Modifier - .padding(top = 8.dp) - .width(82.dp) - .height(6.dp) + .padding(top = 6.dp) + .width(60.dp) + .height(4.dp) .background( color = MaterialTheme.colorScheme.primary, shape = RoundedCornerShape(999.dp), ), ) - Text( - text = subtitle, - modifier = Modifier.padding(top = 10.dp), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) } ViewAllPill( onClick = onViewAllClick, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt index e4f3f55e6..531a2f14e 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomePosterCard.kt @@ -33,15 +33,15 @@ fun HomePosterCard( ) { Column( modifier = modifier - .width(142.dp) + .width(110.dp) .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier), - verticalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), ) { Box( modifier = Modifier .fillMaxWidth() .aspectRatio(item.posterShape.aspectRatio) - .clip(RoundedCornerShape(22.dp)) + .clip(RoundedCornerShape(16.dp)) .background(MaterialTheme.colorScheme.surface), contentAlignment = Alignment.Center, ) { @@ -66,7 +66,7 @@ fun HomePosterCard( } Text( text = item.name, - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -75,7 +75,7 @@ fun HomePosterCard( if (detailLine.isNotBlank()) { Text( text = detailLine, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeSkeletonLoading.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeSkeletonLoading.kt new file mode 100644 index 000000000..47e777edc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/components/HomeSkeletonLoading.kt @@ -0,0 +1,86 @@ +package com.nuvio.app.features.home.components + +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.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.unit.dp + +@Composable +fun HomeSkeletonRow(modifier: Modifier = Modifier) { + val shimmerColors = listOf( + MaterialTheme.colorScheme.surface, + MaterialTheme.colorScheme.surface.copy(alpha = 0.5f), + MaterialTheme.colorScheme.surface, + ) + val transition = rememberInfiniteTransition() + val translateAnim by transition.animateFloat( + initialValue = 0f, + targetValue = 1000f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 1200, easing = LinearEasing), + repeatMode = RepeatMode.Restart, + ), + ) + val brush = Brush.linearGradient( + colors = shimmerColors, + start = Offset(translateAnim - 200f, 0f), + end = Offset(translateAnim, 0f), + ) + + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + // Title placeholder + Box( + modifier = Modifier + .width(140.dp) + .height(18.dp) + .clip(RoundedCornerShape(6.dp)) + .background(brush), + ) + // Accent bar + Box( + modifier = Modifier + .width(60.dp) + .height(4.dp) + .clip(RoundedCornerShape(999.dp)) + .background(brush), + ) + Spacer(modifier = Modifier.height(2.dp)) + // Poster row + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + repeat(4) { + Box( + modifier = Modifier + .width(110.dp) + .height(163.dp) + .clip(RoundedCornerShape(16.dp)) + .background(brush), + ) + } + } + } +}