diff --git a/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/features/all/ui/AllUpNextShowView.kt b/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/features/all/ui/AllUpNextShowView.kt index b771a41fe..e40e14c7a 100644 --- a/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/features/all/ui/AllUpNextShowView.kt +++ b/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/features/all/ui/AllUpNextShowView.kt @@ -40,8 +40,8 @@ internal fun AllUpNextShowView( onShowClick: () -> Unit, modifier: Modifier = Modifier, ) { - val isPremiere = item.progress.nextEpisode?.isPremiere() == true - val isFinale = item.progress.nextEpisode?.isFinale() == true + val isPremiere = item.progress.nextEpisode?.isPremiere(item.progress.isLatestAired) == true + val isFinale = item.progress.nextEpisode?.isFinale(item.progress.isLatestAired) == true PanelMediaCard( enabled = enabled, diff --git a/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/features/context/UpNextItemContextView.kt b/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/features/context/UpNextItemContextView.kt index 51ddcacbf..06a51e737 100644 --- a/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/features/context/UpNextItemContextView.kt +++ b/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/features/context/UpNextItemContextView.kt @@ -226,6 +226,7 @@ private fun Preview() { ), nextEpisode = PreviewData.episode1, lastEpisode = null, + isLatestAired = false, ), show = PreviewData.show1, ), diff --git a/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/model/UpNextShow.kt b/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/model/UpNextShow.kt index 520931396..8ef1a3ccc 100644 --- a/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/model/UpNextShow.kt +++ b/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/model/UpNextShow.kt @@ -38,6 +38,7 @@ internal data class Progress( val stats: Stats?, val nextEpisode: Episode?, val lastEpisode: Episode?, + val isLatestAired: Boolean, ) { @Immutable internal data class Stats( diff --git a/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/ui/HomeUpNextShowView.kt b/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/ui/HomeUpNextShowView.kt index f68c1881c..f1faba2b0 100644 --- a/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/ui/HomeUpNextShowView.kt +++ b/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/ui/HomeUpNextShowView.kt @@ -66,26 +66,31 @@ internal fun HomeUpNextShowView( Column( verticalArrangement = Arrangement.spacedBy(3.dp), ) { + val isLatestAired = item.progress.isLatestAired when { - item.progress.nextEpisode?.isPremiere() == true -> PremiereChip( - contentTextStyle = TraktTheme.typography.meta.copy( - fontSize = 10.sp, - ), - modifier = Modifier - .shadow(2.dp, RoundedCornerShape(100)) - .height(20.dp), - ) - item.progress.nextEpisode?.isFinale() == true -> FinaleChip( - contentTextStyle = TraktTheme.typography.meta.copy( - fontSize = 10.sp, - ), - modifier = Modifier - .shadow( - 2.dp, - androidx.compose.foundation.shape.RoundedCornerShape(100), - ) - .height(20.dp), - ) + item.progress.nextEpisode?.isPremiere(isLatestAired) == true -> { + PremiereChip( + contentTextStyle = TraktTheme.typography.meta.copy( + fontSize = 10.sp, + ), + modifier = Modifier + .shadow(2.dp, RoundedCornerShape(100)) + .height(20.dp), + ) + } + item.progress.nextEpisode?.isFinale(isLatestAired) == true -> { + FinaleChip( + contentTextStyle = TraktTheme.typography.meta.copy( + fontSize = 10.sp, + ), + modifier = Modifier + .shadow( + 2.dp, + androidx.compose.foundation.shape.RoundedCornerShape(100), + ) + .height(20.dp), + ) + } } EpisodeProgressBar( diff --git a/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/usecases/GetUpNextUseCase.kt b/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/usecases/GetUpNextUseCase.kt index dc6b91158..9682f89de 100644 --- a/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/usecases/GetUpNextUseCase.kt +++ b/app/src/main/java/tv/trakt/trakt/core/home/sections/upnext/usecases/GetUpNextUseCase.kt @@ -83,6 +83,8 @@ internal class GetUpNextUseCase( return remoteItems .asyncMap { item -> + val nextEpisode = item.progress.nextEpisode?.let { Episode.fromDto(it) } + val lastEpisode = item.progress.lastEpisode?.let { Episode.fromDto(it) } UpNextShow( show = Show.fromDto(item.show), progress = Progress( @@ -96,12 +98,14 @@ internal class GetUpNextUseCase( minutesLeft = it.minutesLeft, ) }, - lastEpisode = item.progress.lastEpisode?.let { - Episode.fromDto(it) - }, - nextEpisode = item.progress.nextEpisode?.let { - Episode.fromDto(it) - }, + lastEpisode = lastEpisode, + nextEpisode = nextEpisode, + // FIXME: progress.last_episode is the user's furthest watched episode, not the + // show's latest aired episode, so we can't compare directly. As a proxy, treat + // the next episode as the latest aired when remaining (aired - completed) is 1 + // or less - i.e. no further aired episode exists beyond the displayed one. + // Replace once the API surfaces an absolute "latest aired episode" reference. + isLatestAired = (item.progress.aired - item.progress.completed) <= 1, ), ) } diff --git a/app/src/main/java/tv/trakt/trakt/core/summary/ui/DetailsMetaInfo.kt b/app/src/main/java/tv/trakt/trakt/core/summary/ui/DetailsMetaInfo.kt index e387e9ecd..b4e9251c4 100644 --- a/app/src/main/java/tv/trakt/trakt/core/summary/ui/DetailsMetaInfo.kt +++ b/app/src/main/java/tv/trakt/trakt/core/summary/ui/DetailsMetaInfo.kt @@ -1,6 +1,7 @@ package tv.trakt.trakt.core.summary.ui import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -21,6 +22,7 @@ import tv.trakt.trakt.common.helpers.extensions.rememberDurationFormat import tv.trakt.trakt.common.helpers.extensions.toLocal import tv.trakt.trakt.common.helpers.preview.PreviewData import tv.trakt.trakt.common.model.Episode +import tv.trakt.trakt.common.model.EpisodeType import tv.trakt.trakt.common.model.MediaGenre import tv.trakt.trakt.common.model.MediaStatus import tv.trakt.trakt.common.model.Movie @@ -77,6 +79,7 @@ internal fun DetailsMetaInfo( episode.releasedAt?.toLocal()?.toLocalDate() }, runtime = episode.runtime, + episodeType = episode.type, directors = episodeDirectors, writers = episodeWriters, episodeRowsOnly = true, @@ -120,6 +123,7 @@ private fun DetailsMetaInfo( network: String? = null, titleOriginal: String? = null, episodesCount: Int? = null, + episodeType: EpisodeType? = null, languages: ImmutableList = EmptyImmutableList, genres: ImmutableList = EmptyImmutableList, studios: ImmutableList? = null, @@ -201,6 +205,19 @@ private fun DetailsMetaInfo( } } + if (episodeType != null) { + Row( + horizontalArrangement = spacedBy(16.dp), + ) { + DetailsMeta( + title = stringResource(R.string.header_episode_type), + values = listOf(stringResource(episodeType.stringRes)), + modifier = Modifier.weight(1F), + ) + Box(modifier = Modifier.weight(1F)) + } + } + Row( horizontalArrangement = spacedBy(16.dp), ) { diff --git a/common/src/main/java/tv/trakt/trakt/common/helpers/preview/PreviewData.kt b/common/src/main/java/tv/trakt/trakt/common/helpers/preview/PreviewData.kt index 371b41fae..9eba3317b 100644 --- a/common/src/main/java/tv/trakt/trakt/common/helpers/preview/PreviewData.kt +++ b/common/src/main/java/tv/trakt/trakt/common/helpers/preview/PreviewData.kt @@ -148,7 +148,7 @@ object PreviewData { rating = Rating(rating = 4.34f, votes = 5394), commentCount = 4424, runtime = 24.minutes, - episodeType = null, + type = null, originalTitle = "Episode Original Title", images = Images( screenshot = listOf( diff --git a/common/src/main/java/tv/trakt/trakt/common/model/Episode.kt b/common/src/main/java/tv/trakt/trakt/common/model/Episode.kt index 9e07c8b19..bb5745c4d 100644 --- a/common/src/main/java/tv/trakt/trakt/common/model/Episode.kt +++ b/common/src/main/java/tv/trakt/trakt/common/model/Episode.kt @@ -7,6 +7,8 @@ import androidx.compose.ui.res.stringResource import kotlinx.collections.immutable.toImmutableList import tv.trakt.trakt.common.helpers.extensions.nowUtcInstant import tv.trakt.trakt.common.helpers.extensions.toInstant +import tv.trakt.trakt.common.model.EpisodeType.MID_SEASON_FINALE +import tv.trakt.trakt.common.model.EpisodeType.MID_SEASON_PREMIERE import tv.trakt.trakt.common.networking.EpisodeDto import tv.trakt.trakt.common.networking.EpisodeLikesDto import tv.trakt.trakt.common.networking.LastEpisodeDto @@ -18,6 +20,7 @@ import kotlin.time.Duration.Companion.minutes @Immutable data class Episode( val ids: Ids, + val type: EpisodeType?, val number: Int, val season: Int, val title: String, @@ -26,7 +29,6 @@ data class Episode( val rating: Rating, val commentCount: Int, val runtime: Duration?, - val episodeType: String?, val originalTitle: String, val images: Images?, val updatedAt: Instant?, @@ -56,7 +58,11 @@ data class Episode( @Composable fun seasonEpisodeString(): String { - val string = stringResource(R.string.episode_footer_season_episode, this.season, this.number) + val string = stringResource( + R.string.episode_footer_season_episode, + this.season, + this.number, + ) return when { title.isNotBlank() -> "$string - $title" else -> string @@ -64,21 +70,24 @@ data class Episode( } @Composable - fun isPremiere(): Boolean = - remember(episodeType) { - episodeType?.contains("premiere") == true + fun isPremiere(isLatestAired: Boolean = false): Boolean = + remember(type, isLatestAired) { + if (type?.isPremiere == true) return@remember true + type == MID_SEASON_PREMIERE && isLatestAired } @Composable - fun isFinale(): Boolean = - remember(episodeType) { - episodeType?.contains("finale") == true + fun isFinale(isLatestAired: Boolean = false): Boolean = + remember(type, isLatestAired) { + if (type?.isFinale == true) return@remember true + type == MID_SEASON_FINALE && isLatestAired } } fun Episode.Companion.fromDto(dto: EpisodeDto): Episode { return Episode( ids = Ids.fromDto(dto.ids), + type = dto.episodeType?.let { EpisodeType.fromValue(it.value) }, number = dto.number, season = dto.season, title = dto.title ?: "N/A", @@ -90,7 +99,6 @@ fun Episode.Companion.fromDto(dto: EpisodeDto): Episode { ), commentCount = dto.commentCount ?: 0, runtime = dto.runtime?.minutes, - episodeType = dto.episodeType?.value, originalTitle = dto.originalTitle ?: "", images = Images( screenshot = (dto.images?.screenshot ?: emptyList()).toImmutableList(), @@ -104,6 +112,7 @@ fun Episode.Companion.fromDto(dto: EpisodeDto): Episode { fun Episode.Companion.fromDto(dto: LastEpisodeDto): Episode { return Episode( ids = Ids.fromDto(dto.ids), + type = dto.episodeType?.let { EpisodeType.fromValue(it.value) }, number = dto.number, season = dto.season, title = dto.title ?: "N/A", @@ -115,7 +124,6 @@ fun Episode.Companion.fromDto(dto: LastEpisodeDto): Episode { ), commentCount = dto.commentCount ?: 0, runtime = dto.runtime?.minutes, - episodeType = dto.episodeType?.value, originalTitle = dto.originalTitle ?: "", images = Images( screenshot = (dto.images?.screenshot ?: emptyList()).toImmutableList(), @@ -129,6 +137,7 @@ fun Episode.Companion.fromDto(dto: LastEpisodeDto): Episode { fun Episode.Companion.fromDto(dto: EpisodeLikesDto): Episode { return Episode( ids = Ids.fromDto(dto.ids), + type = dto.episodeType?.let { EpisodeType.fromValue(it.value) }, number = dto.number, season = dto.season, title = dto.title ?: "N/A", @@ -140,7 +149,6 @@ fun Episode.Companion.fromDto(dto: EpisodeLikesDto): Episode { ), commentCount = dto.commentCount ?: 0, runtime = dto.runtime?.minutes, - episodeType = dto.episodeType?.value, originalTitle = dto.originalTitle ?: "", images = Images( screenshot = (dto.images?.screenshot ?: emptyList()).toImmutableList(), diff --git a/common/src/main/java/tv/trakt/trakt/common/model/EpisodeType.kt b/common/src/main/java/tv/trakt/trakt/common/model/EpisodeType.kt new file mode 100644 index 000000000..1663ace9e --- /dev/null +++ b/common/src/main/java/tv/trakt/trakt/common/model/EpisodeType.kt @@ -0,0 +1,27 @@ +package tv.trakt.trakt.common.model + +import androidx.annotation.StringRes +import tv.trakt.trakt.resources.R + +enum class EpisodeType( + val value: String, + @param:StringRes val stringRes: Int, +) { + SERIES_PREMIERE("series_premiere", R.string.tag_text_series_premiere), + SERIES_FINALE("series_finale", R.string.tag_text_series_finale), + SEASON_PREMIERE("season_premiere", R.string.tag_text_season_premiere), + SEASON_FINALE("season_finale", R.string.tag_text_season_finale), + MID_SEASON_PREMIERE("mid_season_premiere", R.string.tag_text_mid_season_premiere), + MID_SEASON_FINALE("mid_season_finale", R.string.tag_text_mid_season_finale), + ; + + val isPremiere: Boolean + get() = this == SERIES_PREMIERE || this == SEASON_PREMIERE + + val isFinale: Boolean + get() = this == SERIES_FINALE || this == SEASON_FINALE + + companion object { + fun fromValue(value: String): EpisodeType? = entries.find { it.value == value } + } +} diff --git a/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/HomeUpNextView.kt b/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/HomeUpNextView.kt index d87f6a380..2fce08813 100644 --- a/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/HomeUpNextView.kt +++ b/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/HomeUpNextView.kt @@ -223,8 +223,8 @@ private fun ContentShowListItem( verticalArrangement = spacedBy(3.dp), ) { when { - item.progress.nextEpisode?.isPremiere() == true -> PremiereChip() - item.progress.nextEpisode?.isFinale() == true -> FinaleChip() + item.progress.nextEpisode?.isPremiere(item.progress.isLatestAired) == true -> PremiereChip() + item.progress.nextEpisode?.isFinale(item.progress.isLatestAired) == true -> FinaleChip() } Row( diff --git a/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/model/ProgressShow.kt b/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/model/ProgressShow.kt index 542a0813c..4e199eae3 100644 --- a/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/model/ProgressShow.kt +++ b/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/model/ProgressShow.kt @@ -30,6 +30,7 @@ internal data class ProgressShow( val stats: Stats?, val nextEpisode: Episode?, val lastEpisode: Episode?, + val isLatestAired: Boolean, ) { @Immutable internal data class Stats( diff --git a/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/usecases/GetShowsUpNextUseCase.kt b/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/usecases/GetShowsUpNextUseCase.kt index 7e5b0dd4d..f8e3cf4c1 100644 --- a/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/usecases/GetShowsUpNextUseCase.kt +++ b/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/usecases/GetShowsUpNextUseCase.kt @@ -32,6 +32,8 @@ internal class GetShowsUpNextUseCase( ) return remoteItems .asyncMap { item -> + val nextEpisode = item.progress.nextEpisode?.let { Episode.fromDto(it) } + val lastEpisode = item.progress.lastEpisode?.let { Episode.fromDto(it) } ProgressShow( show = Show.fromDto(item.show), progress = Progress( @@ -45,12 +47,14 @@ internal class GetShowsUpNextUseCase( minutesLeft = it.minutesLeft, ) }, - lastEpisode = item.progress.lastEpisode?.let { - Episode.fromDto(it) - }, - nextEpisode = item.progress.nextEpisode?.let { - Episode.fromDto(it) - }, + lastEpisode = lastEpisode, + nextEpisode = nextEpisode, + // FIXME: progress.last_episode is the user's furthest watched episode, not the + // show's latest aired episode, so we can't compare directly. As a proxy, treat + // the next episode as the latest aired when remaining (aired - completed) is 1 + // or less - i.e. no further aired episode exists beyond the displayed one. + // Replace once the API surfaces an absolute "latest aired episode" reference. + isLatestAired = (item.progress.aired - item.progress.completed) <= 1, ), ) } diff --git a/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/viewall/UpNextViewAllScreen.kt b/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/viewall/UpNextViewAllScreen.kt index a9b4ba3d2..072ddfca6 100644 --- a/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/viewall/UpNextViewAllScreen.kt +++ b/tv/src/main/java/tv/trakt/trakt/app/core/home/sections/shows/upnext/viewall/UpNextViewAllScreen.kt @@ -249,14 +249,14 @@ private fun UpNextViewAllContent( verticalArrangement = spacedBy(3.dp), ) { when { - item.progress.nextEpisode?.isPremiere() == true -> { + item.progress.nextEpisode?.isPremiere(item.progress.isLatestAired) == true -> { PremiereChip( contentTextStyle = TraktTheme.typography.meta.copy( fontSize = 10.sp, ), ) } - item.progress.nextEpisode?.isFinale() == true -> { + item.progress.nextEpisode?.isFinale(item.progress.isLatestAired) == true -> { FinaleChip( contentTextStyle = TraktTheme.typography.meta.copy( fontSize = 10.sp,