feat: add images to libraries on LibrariesContent screen

This commit is contained in:
2026-02-22 12:50:56 +01:00
parent 843bd749b1
commit ce126988d9
10 changed files with 103 additions and 81 deletions

View File

@@ -6,8 +6,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Collections
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Movie
import androidx.compose.material.icons.outlined.Tv
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
@@ -29,7 +27,6 @@ import hu.bbara.purefin.app.home.ui.HomeNavItem
import hu.bbara.purefin.app.home.ui.HomeTopBar
import hu.bbara.purefin.app.home.ui.LibrariesContent
import hu.bbara.purefin.feature.shared.home.HomePageViewModel
import org.jellyfin.sdk.model.api.CollectionType
@Composable
fun HomePage(
@@ -44,11 +41,7 @@ fun HomePage(
HomeNavItem(
id = it.id,
label = it.name,
icon = when (it.type) {
CollectionType.MOVIES -> Icons.Outlined.Movie
CollectionType.TVSHOWS -> Icons.Outlined.Tv
else -> Icons.Outlined.Collections
},
posterUrl = it.posterUrl
)
}
val continueWatching = viewModel.continueWatching.collectAsState()

View File

@@ -1,25 +0,0 @@
package hu.bbara.purefin.app.home.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Movie
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Tv
import org.jellyfin.sdk.model.UUID
object HomeMockData {
val user = HomeUser(name = "Alex User", plan = "Premium Account")
val primaryNavItems = listOf(
HomeNavItem(id = UUID.randomUUID(), label = "Home", icon = Icons.Outlined.Home, selected = true),
HomeNavItem(id = UUID.randomUUID(), label = "Movies", icon = Icons.Outlined.Movie),
HomeNavItem(id = UUID.randomUUID(), label = "TV Shows", icon = Icons.Outlined.Tv),
HomeNavItem(id = UUID.randomUUID(), label = "Search", icon = Icons.Outlined.Search)
)
val secondaryNavItems = listOf(
HomeNavItem(id = UUID.randomUUID(), label = "Settings", icon = Icons.Outlined.Settings)
)
}

View File

@@ -1,12 +1,11 @@
package hu.bbara.purefin.app.home.ui
import androidx.compose.ui.graphics.vector.ImageVector
import org.jellyfin.sdk.model.UUID
data class HomeNavItem(
val id: UUID,
val label: String,
val icon: ImageVector,
val posterUrl: String,
val selected: Boolean = false
)

View File

@@ -2,20 +2,22 @@ package hu.bbara.purefin.app.home.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
@Composable
fun LibrariesContent(
@@ -26,30 +28,38 @@ fun LibrariesContent(
LazyColumn(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(horizontal = 16.dp, vertical = 8.dp)
.background(MaterialTheme.colorScheme.background),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.dp),
verticalArrangement = Arrangement.spacedBy(32.dp),
) {
items(items, key = { it.id }) { item ->
ListItem(
headlineContent = {
Text(
text = item.label,
style = MaterialTheme.typography.titleMedium
)
},
leadingContent = {
Icon(
imageVector = item.icon,
contentDescription = item.label,
tint = MaterialTheme.colorScheme.primary
)
},
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clip(RoundedCornerShape(12.dp))
.clickable { onLibrarySelected(item) }
LibraryListItem(
item = item,
modifier = Modifier.clickable {
onLibrarySelected(item)
}
)
}
}
}
@Composable
fun LibraryListItem(
item: HomeNavItem,
modifier: Modifier = Modifier
) {
Column(modifier = modifier.fillMaxWidth()) {
PurefinAsyncImage(
model = item.posterUrl,
contentDescription = item.label,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(24.dp))
)
Text(
text = item.label,
style = MaterialTheme.typography.displaySmall
)
}
}

View File

@@ -1,38 +1,72 @@
package hu.bbara.purefin.common.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.BrokenImage
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
/**
* Async image that falls back to theme-synced color blocks so loading/error states
* stay aligned with PurefinTheme's colorScheme.
*
* Uses plain [AsyncImage] (no SubcomposeLayout) so it is safe inside lazy lists and
* composables that query intrinsic measurements (e.g. [ListItem]).
*
* - Loading: solid surfaceVariant block
* - Error / missing URL: surfaceVariant block with a centered [fallbackIcon]
*/
@Composable
fun PurefinAsyncImage(
model: Any?,
contentDescription: String?,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Crop
contentScale: ContentScale = ContentScale.Crop,
fallbackIcon: ImageVector = Icons.Outlined.BrokenImage
) {
val placeholderPainter = ColorPainter(MaterialTheme.colorScheme.surfaceVariant)
val surfaceVariant = MaterialTheme.colorScheme.surfaceVariant
val onSurfaceVariant = MaterialTheme.colorScheme.onSurfaceVariant
// Convert empty string to null to properly trigger fallback
val effectiveModel = when {
model is String && model.isEmpty() -> null
else -> model
}
val effectiveModel = if (model is String && model.isEmpty()) null else model
AsyncImage(
model = effectiveModel,
contentDescription = contentDescription,
modifier = modifier,
contentScale = contentScale,
placeholder = placeholderPainter,
error = placeholderPainter,
fallback = placeholderPainter
)
// Show icon immediately for null model (no request will be made); callbacks update it otherwise.
var showFallbackIcon by remember(effectiveModel) { mutableStateOf(effectiveModel == null) }
Box(modifier = modifier, contentAlignment = Alignment.Center) {
AsyncImage(
modifier = Modifier.fillMaxSize(),
model = effectiveModel,
contentDescription = contentDescription,
contentScale = contentScale,
placeholder = ColorPainter(surfaceVariant),
error = ColorPainter(surfaceVariant),
fallback = ColorPainter(surfaceVariant),
onLoading = { showFallbackIcon = false },
onSuccess = { showFallbackIcon = false },
onError = { showFallbackIcon = true },
)
if (showFallbackIcon) {
Icon(
imageVector = fallbackIcon,
contentDescription = null,
tint = onSurfaceVariant,
modifier = Modifier.size(32.dp)
)
}
}
}