feat: add unwatched episode count indicators to PosterItems and enhance media item data structure

This commit is contained in:
2026-02-07 00:10:12 +01:00
parent 4c7d6317c8
commit 7951315048
9 changed files with 96 additions and 14 deletions

View File

@@ -106,7 +106,8 @@ class JellyfinApiClient @Inject constructor(
ItemFields.CHILD_COUNT, ItemFields.CHILD_COUNT,
ItemFields.PARENT_ID, ItemFields.PARENT_ID,
ItemFields.DATE_LAST_REFRESHED, ItemFields.DATE_LAST_REFRESHED,
ItemFields.OVERVIEW ItemFields.OVERVIEW,
ItemFields.SEASON_USER_DATA
) )
suspend fun getLibraryContent(libraryId: UUID): List<BaseItemDto> { suspend fun getLibraryContent(libraryId: UUID): List<BaseItemDto> {

View File

@@ -3,6 +3,7 @@ package hu.bbara.purefin.common.ui
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -11,6 +12,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
@@ -23,6 +25,8 @@ import androidx.compose.ui.unit.sp
import coil3.request.ImageRequest import coil3.request.ImageRequest
import hu.bbara.purefin.app.home.ui.PosterItem import hu.bbara.purefin.app.home.ui.PosterItem
import hu.bbara.purefin.common.ui.components.PurefinAsyncImage import hu.bbara.purefin.common.ui.components.PurefinAsyncImage
import hu.bbara.purefin.common.ui.components.UnwatchedEpisodeIndicator
import hu.bbara.purefin.common.ui.components.WatchStateIndicator
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
@@ -62,17 +66,39 @@ fun PosterCard(
modifier = Modifier modifier = Modifier
.width(posterWidth) .width(posterWidth)
) { ) {
PurefinAsyncImage( Box() {
model = imageRequest, PurefinAsyncImage(
contentDescription = null, model = imageRequest,
modifier = Modifier contentDescription = null,
.aspectRatio(2f / 3f) modifier = Modifier
.clip(RoundedCornerShape(14.dp)) .aspectRatio(2f / 3f)
.border(1.dp, scheme.outlineVariant.copy(alpha = 0.3f), RoundedCornerShape(14.dp)) .clip(RoundedCornerShape(14.dp))
.background(scheme.surfaceVariant) .border(1.dp, scheme.outlineVariant.copy(alpha = 0.3f), RoundedCornerShape(14.dp))
.clickable(onClick = { openItem(item) }), .background(scheme.surfaceVariant)
contentScale = ContentScale.Crop .clickable(onClick = { openItem(item) }),
) contentScale = ContentScale.Crop
)
when (item.type) {
BaseItemKind.MOVIE -> WatchStateIndicator(
modifier = Modifier.align(Alignment.TopEnd)
.padding(8.dp),
watched = item.movie!!.watched,
started = (item.movie.progress ?: 0.0) > 0
)
BaseItemKind.EPISODE -> WatchStateIndicator(
modifier = Modifier.align(Alignment.TopEnd)
.padding(8.dp),
watched = item.episode!!.watched,
started = (item.episode.progress ?: 0.0) > 0
)
BaseItemKind.SERIES -> UnwatchedEpisodeIndicator(
modifier = Modifier.align(Alignment.TopEnd)
.padding(8.dp),
unwatchedCount = item.series!!.unwatchedEpisodeCount
)
else -> {}
}
}
Text( Text(
text = item.title, text = item.title,
color = scheme.onBackground, color = scheme.onBackground,

View File

@@ -0,0 +1,43 @@
package hu.bbara.purefin.common.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun UnwatchedEpisodeIndicator(
unwatchedCount: Int,
foregroundColor: Color = MaterialTheme.colorScheme.onPrimary,
backgroundColor: Color = MaterialTheme.colorScheme.primary,
size: Int = 24,
modifier: Modifier = Modifier
) {
if (unwatchedCount == 0) {
return
}
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.border(1.dp, backgroundColor.copy(alpha = 0.8f), CircleShape)
.background(backgroundColor.copy(alpha = 0.8f), CircleShape)
.size(size.dp)
.clip(CircleShape)
) {
Text(
text = unwatchedCount.toString(),
color = foregroundColor.copy(alpha = 0.8f),
style = MaterialTheme.typography.bodyMedium
)
}
}

View File

@@ -364,6 +364,7 @@ class InMemoryMediaRepository @Inject constructor(
itemId = this.id, itemId = this.id,
type = ImageType.PRIMARY type = ImageType.PRIMARY
), ),
unwatchedEpisodeCount = this.userData!!.unplayedItemCount!!,
seasonCount = this.childCount!!, seasonCount = this.childCount!!,
seasons = emptyList(), seasons = emptyList(),
cast = emptyList() cast = emptyList()
@@ -376,6 +377,7 @@ class InMemoryMediaRepository @Inject constructor(
seriesId = this.seriesId!!, seriesId = this.seriesId!!,
name = this.name ?: "Unknown", name = this.name ?: "Unknown",
index = this.indexNumber!!, index = this.indexNumber!!,
unwatchedEpisodeCount = this.userData!!.unplayedItemCount!!,
episodeCount = this.childCount!!, episodeCount = this.childCount!!,
episodes = emptyList() episodes = emptyList()
) )

View File

@@ -161,6 +161,7 @@ class RoomMediaLocalDataSource @Inject constructor(
synopsis = synopsis, synopsis = synopsis,
year = year, year = year,
heroImageUrl = heroImageUrl, heroImageUrl = heroImageUrl,
unwatchedEpisodeCount = unwatchedEpisodeCount,
seasonCount = seasonCount seasonCount = seasonCount
) )
@@ -169,6 +170,7 @@ class RoomMediaLocalDataSource @Inject constructor(
seriesId = seriesId, seriesId = seriesId,
name = name, name = name,
index = index, index = index,
unwatchedEpisodeCount = unwatchedEpisodeCount,
episodeCount = episodeCount episodeCount = episodeCount
) )
@@ -212,6 +214,7 @@ class RoomMediaLocalDataSource @Inject constructor(
synopsis = synopsis, synopsis = synopsis,
year = year, year = year,
heroImageUrl = heroImageUrl, heroImageUrl = heroImageUrl,
unwatchedEpisodeCount = unwatchedEpisodeCount,
seasonCount = seasonCount, seasonCount = seasonCount,
seasons = seasons, seasons = seasons,
cast = cast cast = cast
@@ -222,6 +225,7 @@ class RoomMediaLocalDataSource @Inject constructor(
seriesId = seriesId, seriesId = seriesId,
name = name, name = name,
index = index, index = index,
unwatchedEpisodeCount = unwatchedEpisodeCount,
episodeCount = episodeCount, episodeCount = episodeCount,
episodes = episodes episodes = episodes
) )

View File

@@ -23,5 +23,6 @@ data class SeasonEntity(
val seriesId: UUID, val seriesId: UUID,
val name: String, val name: String,
val index: Int, val index: Int,
val unwatchedEpisodeCount: Int,
val episodeCount: Int val episodeCount: Int
) )

View File

@@ -12,5 +12,6 @@ data class SeriesEntity(
val synopsis: String, val synopsis: String,
val year: String, val year: String,
val heroImageUrl: String, val heroImageUrl: String,
val unwatchedEpisodeCount: Int,
val seasonCount: Int val seasonCount: Int
) )

View File

@@ -7,6 +7,8 @@ data class Season(
val seriesId: UUID, val seriesId: UUID,
val name: String, val name: String,
val index: Int, val index: Int,
val unwatchedEpisodeCount: Int,
val episodeCount: Int, val episodeCount: Int,
val episodes: List<Episode> val episodes: List<Episode>
) ) {
}

View File

@@ -9,7 +9,9 @@ data class Series(
val synopsis: String, val synopsis: String,
val year: String, val year: String,
val heroImageUrl: String, val heroImageUrl: String,
val unwatchedEpisodeCount: Int,
val seasonCount: Int, val seasonCount: Int,
val seasons: List<Season>, val seasons: List<Season>,
val cast: List<CastMember> val cast: List<CastMember>
) ) {
}