From c5b613e301b5797987f51e4c5015b16848a598c4 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Wed, 18 Feb 2026 19:55:51 +0100 Subject: [PATCH] refactor: combine ContinueWatching and NextUp sections. --- .../hu/bbara/purefin/app/home/HomePage.kt | 2 - .../purefin/app/home/HomePageViewModel.kt | 19 --- .../bbara/purefin/app/home/ui/HomeContent.kt | 10 -- .../bbara/purefin/app/home/ui/HomeSections.kt | 120 ------------------ .../bbara/purefin/client/JellyfinApiClient.kt | 5 +- .../common/ui/components/MediaProgressBar.kt | 1 + .../purefin/data/ActiveMediaRepository.kt | 4 - .../purefin/data/InMemoryMediaRepository.kt | 34 +---- .../hu/bbara/purefin/data/MediaRepository.kt | 1 - .../purefin/data/OfflineMediaRepository.kt | 1 - .../hu/bbara/purefin/data/cache/HomeCache.kt | 3 +- .../data/local/room/OfflineMediaDatabase.kt | 4 +- .../purefin/data/local/room/SeriesEntity.kt | 1 + 13 files changed, 10 insertions(+), 195 deletions(-) diff --git a/app/src/main/java/hu/bbara/purefin/app/home/HomePage.kt b/app/src/main/java/hu/bbara/purefin/app/home/HomePage.kt index b54cd8f..7f39a8a 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/HomePage.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/HomePage.kt @@ -50,7 +50,6 @@ fun HomePage( ) } val continueWatching = viewModel.continueWatching.collectAsState() - val nextUp = viewModel.nextUp.collectAsState() val latestLibraryContent = viewModel.latestLibraryContent.collectAsState() LifecycleResumeEffect(Unit) { @@ -96,7 +95,6 @@ fun HomePage( libraries = libraries, libraryContent = latestLibraryContent.value, continueWatching = continueWatching.value, - nextUp = nextUp.value, onMovieSelected = viewModel::onMovieSelected, onSeriesSelected = viewModel::onSeriesSelected, onEpisodeSelected = viewModel::onEpisodeSelected, diff --git a/app/src/main/java/hu/bbara/purefin/app/home/HomePageViewModel.kt b/app/src/main/java/hu/bbara/purefin/app/home/HomePageViewModel.kt index 9ec07ac..ae859fb 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/HomePageViewModel.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/HomePageViewModel.kt @@ -6,7 +6,6 @@ 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.data.MediaRepository import hu.bbara.purefin.data.model.Media @@ -87,24 +86,6 @@ 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, diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeContent.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeContent.kt index 80a2a1f..3547791 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeContent.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeContent.kt @@ -17,7 +17,6 @@ fun HomeContent( libraries: List, libraryContent: Map>, continueWatching: List, - nextUp: List, onMovieSelected: (UUID) -> Unit, onSeriesSelected: (UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit, @@ -41,15 +40,6 @@ fun HomeContent( item { Spacer(modifier = Modifier.height(16.dp)) } - item { - NextUpSection( - items = nextUp, - onEpisodeSelected = onEpisodeSelected - ) - } - item { - Spacer(modifier = Modifier.height(16.dp)) - } items( items = libraries.filter { libraryContent[it.id]?.isEmpty() != true }, key = { it.id } diff --git a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeSections.kt b/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeSections.kt index 8dfd1d5..2ba1427 100644 --- a/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeSections.kt +++ b/app/src/main/java/hu/bbara/purefin/app/home/ui/HomeSections.kt @@ -187,126 +187,6 @@ fun ContinueWatchingCard( } } -@Composable -fun NextUpSection( - items: List, - 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, diff --git a/app/src/main/java/hu/bbara/purefin/client/JellyfinApiClient.kt b/app/src/main/java/hu/bbara/purefin/client/JellyfinApiClient.kt index 5ba212f..ec2dae4 100644 --- a/app/src/main/java/hu/bbara/purefin/client/JellyfinApiClient.kt +++ b/app/src/main/java/hu/bbara/purefin/client/JellyfinApiClient.kt @@ -23,7 +23,6 @@ import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.CollectionType -import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.MediaSourceInfo import org.jellyfin.sdk.model.api.PlayMethod @@ -33,8 +32,6 @@ import org.jellyfin.sdk.model.api.PlaybackProgressInfo import org.jellyfin.sdk.model.api.PlaybackStartInfo import org.jellyfin.sdk.model.api.PlaybackStopInfo import org.jellyfin.sdk.model.api.RepeatMode -import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod -import org.jellyfin.sdk.model.api.SubtitleProfile import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.jellyfin.sdk.model.api.request.GetNextUpRequest import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest @@ -161,7 +158,7 @@ class JellyfinApiClient @Inject constructor( val getNextUpRequest = GetNextUpRequest( userId = getUserId(), fields = itemFields, - enableResumable = false, + enableResumable = true, ) val result = api.tvShowsApi.getNextUp(getNextUpRequest) Log.d("getNextUpEpisodes", result.content.toString()) diff --git a/app/src/main/java/hu/bbara/purefin/common/ui/components/MediaProgressBar.kt b/app/src/main/java/hu/bbara/purefin/common/ui/components/MediaProgressBar.kt index fc78788..7e8d9f0 100644 --- a/app/src/main/java/hu/bbara/purefin/common/ui/components/MediaProgressBar.kt +++ b/app/src/main/java/hu/bbara/purefin/common/ui/components/MediaProgressBar.kt @@ -29,6 +29,7 @@ fun MediaProgressBar( backgroundColor: Color = MaterialTheme.colorScheme.primary, modifier: Modifier ) { + if (progress == 0f) return Box( modifier = modifier .padding(bottom = 8.dp, start = 8.dp, end = 8.dp) diff --git a/app/src/main/java/hu/bbara/purefin/data/ActiveMediaRepository.kt b/app/src/main/java/hu/bbara/purefin/data/ActiveMediaRepository.kt index 45ca20b..b46e2a7 100644 --- a/app/src/main/java/hu/bbara/purefin/data/ActiveMediaRepository.kt +++ b/app/src/main/java/hu/bbara/purefin/data/ActiveMediaRepository.kt @@ -69,10 +69,6 @@ class ActiveMediaRepository @Inject constructor( activeRepository.flatMapLatest { it.continueWatching } .stateIn(scope, SharingStarted.Eagerly, emptyList()) - override val nextUp: StateFlow> = - activeRepository.flatMapLatest { it.nextUp } - .stateIn(scope, SharingStarted.Eagerly, emptyList()) - override val latestLibraryContent: StateFlow>> = activeRepository.flatMapLatest { it.latestLibraryContent } .stateIn(scope, SharingStarted.Eagerly, emptyMap()) diff --git a/app/src/main/java/hu/bbara/purefin/data/InMemoryMediaRepository.kt b/app/src/main/java/hu/bbara/purefin/data/InMemoryMediaRepository.kt index 2f1fe7a..6065cd4 100644 --- a/app/src/main/java/hu/bbara/purefin/data/InMemoryMediaRepository.kt +++ b/app/src/main/java/hu/bbara/purefin/data/InMemoryMediaRepository.kt @@ -79,9 +79,6 @@ class InMemoryMediaRepository @Inject constructor( private val _continueWatching: MutableStateFlow> = MutableStateFlow(emptyList()) override val continueWatching: StateFlow> = _continueWatching.asStateFlow() - private val _nextUp: MutableStateFlow> = MutableStateFlow(emptyList()) - override val nextUp: StateFlow> = _nextUp.asStateFlow() - private val _latestLibraryContent: MutableStateFlow>> = MutableStateFlow(emptyMap()) override val latestLibraryContent: StateFlow>> = _latestLibraryContent.asStateFlow() @@ -97,9 +94,6 @@ class InMemoryMediaRepository @Inject constructor( if (cache.continueWatching.isNotEmpty()) { _continueWatching.value = cache.continueWatching.mapNotNull { it.toMedia() } } - if (cache.nextUp.isNotEmpty()) { - _nextUp.value = cache.nextUp.mapNotNull { it.toMedia() } - } if (cache.latestLibraryContent.isNotEmpty()) { _latestLibraryContent.value = cache.latestLibraryContent.mapNotNull { (key, items) -> val uuid = runCatching { UUID.fromString(key) }.getOrNull() ?: return@mapNotNull null @@ -111,7 +105,6 @@ class InMemoryMediaRepository @Inject constructor( private suspend fun persistHomeCache() { val cache = HomeCache( continueWatching = _continueWatching.value.map { it.toCachedItem() }, - nextUp = _nextUp.value.map { it.toCachedItem() }, latestLibraryContent = _latestLibraryContent.value.map { (uuid, items) -> uuid.toString() to items.map { it.toCachedItem() } }.toMap() @@ -122,13 +115,13 @@ class InMemoryMediaRepository @Inject constructor( private fun Media.toCachedItem(): CachedMediaItem = when (this) { is Media.MovieMedia -> CachedMediaItem(type = "MOVIE", id = movieId.toString()) is Media.SeriesMedia -> CachedMediaItem(type = "SERIES", id = seriesId.toString()) - is Media.SeasonMedia -> CachedMediaItem(type = "SEASON", id = seasonId.toString(), seriesId = seriesId.toString()) - is Media.EpisodeMedia -> CachedMediaItem(type = "EPISODE", id = episodeId.toString(), seriesId = seriesId.toString()) + is Media.SeasonMedia -> CachedMediaItem(type = "SEASON", id = seasonId.toString(), mediaId = seriesId.toString()) + is Media.EpisodeMedia -> CachedMediaItem(type = "EPISODE", id = episodeId.toString(), mediaId = seriesId.toString()) } private fun CachedMediaItem.toMedia(): Media? { val uuid = runCatching { UUID.fromString(id) }.getOrNull() ?: return null - val seriesUuid = seriesId?.let { runCatching { UUID.fromString(it) }.getOrNull() } + val seriesUuid = mediaId?.let { runCatching { UUID.fromString(it) }.getOrNull() } return when (type) { "MOVIE" -> Media.MovieMedia(movieId = uuid) "SERIES" -> Media.SeriesMedia(seriesId = uuid) @@ -153,7 +146,6 @@ class InMemoryMediaRepository @Inject constructor( } loadLibraries() loadContinueWatching() - loadNextUp() loadLatestLibraryContent() persistHomeCache() _state.value = MediaRepositoryState.Ready @@ -228,7 +220,7 @@ class InMemoryMediaRepository @Inject constructor( } suspend fun loadContinueWatching() { - val continueWatchingItems = jellyfinApiClient.getContinueWatching() + val continueWatchingItems = jellyfinApiClient.getNextUpEpisodes() val items = continueWatchingItems.mapNotNull { item -> when (item.type) { BaseItemKind.MOVIE -> Media.MovieMedia(movieId = item.id) @@ -253,23 +245,6 @@ 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() @@ -351,7 +326,6 @@ class InMemoryMediaRepository @Inject constructor( loadLibraries() loadContinueWatching() - loadNextUp() loadLatestLibraryContent() persistHomeCache() initialLoadTimestamp = System.currentTimeMillis() diff --git a/app/src/main/java/hu/bbara/purefin/data/MediaRepository.kt b/app/src/main/java/hu/bbara/purefin/data/MediaRepository.kt index 489820d..30bce5c 100644 --- a/app/src/main/java/hu/bbara/purefin/data/MediaRepository.kt +++ b/app/src/main/java/hu/bbara/purefin/data/MediaRepository.kt @@ -18,7 +18,6 @@ interface MediaRepository { val state: StateFlow val continueWatching: StateFlow> - val nextUp: StateFlow> val latestLibraryContent: StateFlow>> fun observeSeriesWithContent(seriesId: UUID): Flow diff --git a/app/src/main/java/hu/bbara/purefin/data/OfflineMediaRepository.kt b/app/src/main/java/hu/bbara/purefin/data/OfflineMediaRepository.kt index b6a8e61..3630962 100644 --- a/app/src/main/java/hu/bbara/purefin/data/OfflineMediaRepository.kt +++ b/app/src/main/java/hu/bbara/purefin/data/OfflineMediaRepository.kt @@ -49,7 +49,6 @@ class OfflineMediaRepository @Inject constructor( // Offline mode doesn't support these server-side features override val continueWatching: StateFlow> = MutableStateFlow(emptyList()) - override val nextUp: StateFlow> = MutableStateFlow(emptyList()) override val latestLibraryContent: StateFlow>> = MutableStateFlow(emptyMap()) override fun observeSeriesWithContent(seriesId: UUID): Flow { diff --git a/app/src/main/java/hu/bbara/purefin/data/cache/HomeCache.kt b/app/src/main/java/hu/bbara/purefin/data/cache/HomeCache.kt index dc08861..3982f32 100644 --- a/app/src/main/java/hu/bbara/purefin/data/cache/HomeCache.kt +++ b/app/src/main/java/hu/bbara/purefin/data/cache/HomeCache.kt @@ -6,12 +6,11 @@ import kotlinx.serialization.Serializable data class CachedMediaItem( val type: String, val id: String, - val seriesId: String? = null + val mediaId: String? = null ) @Serializable data class HomeCache( val continueWatching: List = emptyList(), - val nextUp: List = emptyList(), val latestLibraryContent: Map> = emptyMap() ) diff --git a/app/src/main/java/hu/bbara/purefin/data/local/room/OfflineMediaDatabase.kt b/app/src/main/java/hu/bbara/purefin/data/local/room/OfflineMediaDatabase.kt index c6c8e8e..ae29c47 100644 --- a/app/src/main/java/hu/bbara/purefin/data/local/room/OfflineMediaDatabase.kt +++ b/app/src/main/java/hu/bbara/purefin/data/local/room/OfflineMediaDatabase.kt @@ -5,10 +5,10 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import hu.bbara.purefin.data.local.room.dao.CastMemberDao import hu.bbara.purefin.data.local.room.dao.EpisodeDao +import hu.bbara.purefin.data.local.room.dao.LibraryDao import hu.bbara.purefin.data.local.room.dao.MovieDao import hu.bbara.purefin.data.local.room.dao.SeasonDao import hu.bbara.purefin.data.local.room.dao.SeriesDao -import hu.bbara.purefin.data.local.room.dao.LibraryDao @Database( entities = [ @@ -19,7 +19,7 @@ import hu.bbara.purefin.data.local.room.dao.LibraryDao LibraryEntity::class, CastMemberEntity::class ], - version = 3, + version = 4, exportSchema = false ) @TypeConverters(UuidConverters::class) diff --git a/app/src/main/java/hu/bbara/purefin/data/local/room/SeriesEntity.kt b/app/src/main/java/hu/bbara/purefin/data/local/room/SeriesEntity.kt index cf363d6..c204f10 100644 --- a/app/src/main/java/hu/bbara/purefin/data/local/room/SeriesEntity.kt +++ b/app/src/main/java/hu/bbara/purefin/data/local/room/SeriesEntity.kt @@ -13,6 +13,7 @@ import java.util.UUID entity = LibraryEntity::class, parentColumns = ["id"], childColumns = ["libraryId"], + onDelete = ForeignKey.CASCADE ), ], indices = [Index("libraryId")]