mirror of
https://github.com/bbara04/Purefin.git
synced 2026-04-01 01:30:08 +02:00
feature: Add next up section to homepage
This commit is contained in:
@@ -49,6 +49,7 @@ fun HomePage(
|
||||
)
|
||||
}
|
||||
val continueWatching = viewModel.continueWatching.collectAsState()
|
||||
val nextUp = viewModel.nextUp.collectAsState()
|
||||
val latestLibraryContent = viewModel.latestLibraryContent.collectAsState()
|
||||
|
||||
LifecycleResumeEffect(Unit) {
|
||||
@@ -92,6 +93,7 @@ fun HomePage(
|
||||
libraries = libraries,
|
||||
libraryContent = latestLibraryContent.value,
|
||||
continueWatching = continueWatching.value,
|
||||
nextUp = nextUp.value,
|
||||
onMovieSelected = viewModel::onMovieSelected,
|
||||
onSeriesSelected = viewModel::onSeriesSelected,
|
||||
onEpisodeSelected = viewModel::onEpisodeSelected,
|
||||
|
||||
@@ -6,6 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import hu.bbara.purefin.app.home.ui.ContinueWatchingItem
|
||||
import hu.bbara.purefin.app.home.ui.HomeNavItem
|
||||
import hu.bbara.purefin.app.home.ui.LibraryItem
|
||||
import hu.bbara.purefin.app.home.ui.NextUpItem
|
||||
import hu.bbara.purefin.app.home.ui.PosterItem
|
||||
import hu.bbara.purefin.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.data.InMemoryMediaRepository
|
||||
@@ -77,6 +78,24 @@ class HomePageViewModel @Inject constructor(
|
||||
initialValue = emptyList()
|
||||
)
|
||||
|
||||
val nextUp = combine(
|
||||
mediaRepository.nextUp,
|
||||
mediaRepository.episodes
|
||||
) { list, episodesMap ->
|
||||
list.mapNotNull { media ->
|
||||
when (media) {
|
||||
is Media.EpisodeMedia -> episodesMap[media.episodeId]?.let {
|
||||
NextUpItem(episode = it)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}.distinctBy { it.id }
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptyList()
|
||||
)
|
||||
|
||||
val latestLibraryContent = combine(
|
||||
mediaRepository.latestLibraryContent,
|
||||
mediaRepository.movies,
|
||||
|
||||
@@ -17,6 +17,7 @@ fun HomeContent(
|
||||
libraries: List<LibraryItem>,
|
||||
libraryContent: Map<UUID, List<PosterItem>>,
|
||||
continueWatching: List<ContinueWatchingItem>,
|
||||
nextUp: List<NextUpItem>,
|
||||
onMovieSelected: (UUID) -> Unit,
|
||||
onSeriesSelected: (UUID) -> Unit,
|
||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||
@@ -37,6 +38,12 @@ fun HomeContent(
|
||||
onEpisodeSelected = onEpisodeSelected
|
||||
)
|
||||
}
|
||||
item {
|
||||
NextUpSection(
|
||||
items = nextUp,
|
||||
onEpisodeSelected = onEpisodeSelected
|
||||
)
|
||||
}
|
||||
items(
|
||||
items = libraries.filter { libraryContent[it.id]?.isEmpty() != true },
|
||||
key = { it.id }
|
||||
|
||||
@@ -35,6 +35,14 @@ data class ContinueWatchingItem(
|
||||
}
|
||||
}
|
||||
|
||||
data class NextUpItem(
|
||||
val episode: Episode
|
||||
) {
|
||||
val id: UUID = episode.id
|
||||
val primaryText: String = episode.title
|
||||
val secondaryText: String = episode.releaseDate
|
||||
}
|
||||
|
||||
data class LibraryItem(
|
||||
val id: UUID,
|
||||
val name: String,
|
||||
|
||||
@@ -188,6 +188,126 @@ fun ContinueWatchingCard(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NextUpSection(
|
||||
items: List<NextUpItem>,
|
||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
SectionHeader(
|
||||
title = "Next Up",
|
||||
action = null
|
||||
)
|
||||
LazyRow(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
items(
|
||||
items = items, key = { it.id }) { item ->
|
||||
NextUpCard(
|
||||
item = item,
|
||||
onEpisodeSelected = onEpisodeSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NextUpCard(
|
||||
item: NextUpItem,
|
||||
modifier: Modifier = Modifier,
|
||||
onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
val context = LocalContext.current
|
||||
val density = LocalDensity.current
|
||||
|
||||
val imageUrl = item.episode.heroImageUrl
|
||||
|
||||
val cardWidth = 280.dp
|
||||
val cardHeight = cardWidth * 9 / 16
|
||||
|
||||
fun openItem(item: NextUpItem) {
|
||||
val episode = item.episode
|
||||
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
|
||||
}
|
||||
|
||||
val imageRequest = ImageRequest.Builder(context)
|
||||
.data(imageUrl)
|
||||
.size(with(density) { cardWidth.roundToPx() }, with(density) { cardHeight.roundToPx() })
|
||||
.build()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(cardWidth)
|
||||
.wrapContentHeight()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(cardWidth)
|
||||
.aspectRatio(16f / 9f)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.border(1.dp, scheme.outlineVariant.copy(alpha = 0.3f), RoundedCornerShape(16.dp))
|
||||
.background(scheme.surfaceVariant)
|
||||
) {
|
||||
PurefinAsyncImage(
|
||||
model = imageRequest,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable {
|
||||
openItem(item)
|
||||
},
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 8.dp, bottom = 16.dp)
|
||||
.clip(CircleShape)
|
||||
.background(scheme.secondary)
|
||||
.size(36.dp),
|
||||
onClick = {
|
||||
val intent = Intent(context, PlayerActivity::class.java)
|
||||
intent.putExtra("MEDIA_ID", item.id.toString())
|
||||
context.startActivity(intent)
|
||||
},
|
||||
colors = IconButtonColors(
|
||||
containerColor = scheme.secondary,
|
||||
contentColor = scheme.onSecondary,
|
||||
disabledContainerColor = scheme.secondary,
|
||||
disabledContentColor = scheme.onSecondary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.PlayArrow,
|
||||
contentDescription = "Play",
|
||||
modifier = Modifier.size(28.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(modifier = Modifier.padding(top = 12.dp)) {
|
||||
Text(
|
||||
text = item.primaryText,
|
||||
color = scheme.onBackground,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = item.secondaryText,
|
||||
color = scheme.onSurfaceVariant,
|
||||
fontSize = 13.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LibraryPosterSection(
|
||||
title: String,
|
||||
|
||||
@@ -67,6 +67,9 @@ class InMemoryMediaRepository @Inject constructor(
|
||||
private val _continueWatching: MutableStateFlow<List<Media>> = MutableStateFlow(emptyList())
|
||||
val continueWatching: StateFlow<List<Media>> = _continueWatching.asStateFlow()
|
||||
|
||||
private val _nextUp: MutableStateFlow<List<Media>> = MutableStateFlow(emptyList())
|
||||
val nextUp: StateFlow<List<Media>> = _nextUp.asStateFlow()
|
||||
|
||||
private val _latestLibraryContent: MutableStateFlow<Map<UUID, List<Media>>> = MutableStateFlow(emptyMap())
|
||||
val latestLibraryContent: StateFlow<Map<UUID, List<Media>>> = _latestLibraryContent.asStateFlow()
|
||||
|
||||
@@ -82,6 +85,7 @@ class InMemoryMediaRepository @Inject constructor(
|
||||
try {
|
||||
loadLibraries()
|
||||
loadContinueWatching()
|
||||
loadNextUp()
|
||||
loadLatestLibraryContent()
|
||||
_state.value = MediaRepositoryState.Ready
|
||||
ready.complete(Unit)
|
||||
@@ -171,6 +175,23 @@ class InMemoryMediaRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadNextUp() {
|
||||
val nextUpItems = jellyfinApiClient.getNextUpEpisodes()
|
||||
val items = nextUpItems.map { item ->
|
||||
Media.EpisodeMedia(
|
||||
episodeId = item.id,
|
||||
seriesId = item.seriesId!!
|
||||
)
|
||||
}
|
||||
_nextUp.value = items
|
||||
|
||||
// Load episodes
|
||||
nextUpItems.forEach { item ->
|
||||
val episode = item.toEpisode(serverUrl())
|
||||
localDataSource.saveEpisode(episode)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadLatestLibraryContent() {
|
||||
// TODO Make libraries accessible in a field or something that is not this ugly.
|
||||
val librariesItem = jellyfinApiClient.getLibraries()
|
||||
@@ -243,6 +264,7 @@ class InMemoryMediaRepository @Inject constructor(
|
||||
override suspend fun refreshHomeData() {
|
||||
loadLibraries()
|
||||
loadContinueWatching()
|
||||
loadNextUp()
|
||||
loadLatestLibraryContent()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user