diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index e24d703f..3c21777b 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -248,6 +248,19 @@ Popular Top Rated Recent + Most Voted + Watch region + ISO 3166-1 country code where the title is available. Example: US, GB. + Quick watch regions + Watch provider IDs + Use TMDB watch provider IDs. Separate multiple with commas for AND, or pipes for OR. + 8|337|350 + Quick watch providers + Netflix + Prime Video + Disney+ + Apple TV+ + Hulu TMDB List TMDB Movie Collection Production diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index 45d138f9..ae2f3728 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -471,6 +471,11 @@ fun App() { AuthScreen(modifier = Modifier.fillMaxSize()) } AppGateScreen.ProfileSelection.name -> { + PlatformBackHandler(enabled = gateScreen == AppGateScreen.ProfileSelection.name) { + if (!autoSkipProfileSelection) { + gateScreen = AppGateScreen.Main.name + } + } ProfileSelectionScreen( onProfileSelected = { profile -> ProfileRepository.selectProfile(profile.profileIndex) @@ -493,6 +498,9 @@ fun App() { ) } AppGateScreen.ProfileEdit.name -> { + PlatformBackHandler(enabled = gateScreen == AppGateScreen.ProfileEdit.name) { + gateScreen = AppGateScreen.ProfileSelection.name + } ProfileEditScreen( profile = editingProfile, onBack = { gateScreen = AppGateScreen.ProfileSelection.name }, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt index 7219395a..f5e0b6b8 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt @@ -1125,6 +1125,7 @@ private fun TmdbSourcePickerScreen( val sorts = listOf( TmdbCollectionSort.POPULAR_DESC, TmdbCollectionSort.VOTE_AVERAGE_DESC, + TmdbCollectionSort.VOTE_COUNT_DESC, if (state.tmdbMediaType == TmdbCollectionMediaType.TV && !state.tmdbMediaBoth) { TmdbCollectionSort.FIRST_AIR_DATE_DESC } else { @@ -1353,6 +1354,54 @@ private fun TmdbSourcePickerScreen( } }, ) + TmdbQuickChips( + label = stringResource(Res.string.collections_editor_tmdb_quick_watch_providers), + chips = listOf( + stringResource(Res.string.collections_editor_tmdb_watch_provider_netflix) to "8", + stringResource(Res.string.collections_editor_tmdb_watch_provider_prime) to "119", + stringResource(Res.string.collections_editor_tmdb_watch_provider_disney) to "337", + stringResource(Res.string.collections_editor_tmdb_watch_provider_apple) to "350", + stringResource(Res.string.collections_editor_tmdb_watch_provider_hulu) to "15", + ), + onSelect = { value -> + CollectionEditorRepository.updateTmdbFilters { it.copy(withWatchProviders = value) } + }, + ) + TmdbFilterField( + label = stringResource(Res.string.collections_editor_tmdb_watch_providers), + helper = stringResource(Res.string.collections_editor_tmdb_watch_providers_helper), + value = state.tmdbFilters.withWatchProviders.orEmpty(), + placeholder = stringResource(Res.string.collections_editor_tmdb_watch_providers_placeholder), + onValueChange = { value -> + CollectionEditorRepository.updateTmdbFilters { + it.copy(withWatchProviders = value.ifBlank { null }) + } + }, + ) + TmdbQuickChips( + label = stringResource(Res.string.collections_editor_tmdb_quick_watch_regions), + chips = listOf( + stringResource(Res.string.collections_editor_tmdb_country_us) to "US", + stringResource(Res.string.collections_editor_tmdb_country_uk) to "GB", + "Canada" to "CA", + "Australia" to "AU", + "Germany" to "DE", + ), + onSelect = { value -> + CollectionEditorRepository.updateTmdbFilters { it.copy(watchRegion = value) } + }, + ) + TmdbFilterField( + label = stringResource(Res.string.collections_editor_tmdb_watch_region), + helper = stringResource(Res.string.collections_editor_tmdb_watch_region_helper), + value = state.tmdbFilters.watchRegion.orEmpty(), + placeholder = "US", + onValueChange = { value -> + CollectionEditorRepository.updateTmdbFilters { + it.copy(watchRegion = value.ifBlank { null }) + } + }, + ) } } } @@ -2255,6 +2304,7 @@ private fun tmdbSortLabel(sort: TmdbCollectionSort): String = TmdbCollectionSort.ORIGINAL -> stringResource(Res.string.collections_editor_tmdb_sort_original) TmdbCollectionSort.POPULAR_DESC -> stringResource(Res.string.collections_editor_tmdb_sort_popular) TmdbCollectionSort.VOTE_AVERAGE_DESC -> stringResource(Res.string.collections_editor_tmdb_sort_top_rated) + TmdbCollectionSort.VOTE_COUNT_DESC -> stringResource(Res.string.collections_editor_tmdb_sort_vote_count) TmdbCollectionSort.RELEASE_DATE_DESC -> stringResource(Res.string.collections_editor_tmdb_sort_recent) TmdbCollectionSort.FIRST_AIR_DATE_DESC -> stringResource(Res.string.collections_editor_tmdb_sort_recent) } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionModels.kt index 31962922..578445ae 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionModels.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionModels.kt @@ -100,6 +100,7 @@ enum class TmdbCollectionSort(val value: String) { ORIGINAL("original"), POPULAR_DESC("popularity.desc"), VOTE_AVERAGE_DESC("vote_average.desc"), + VOTE_COUNT_DESC("vote_count.desc"), RELEASE_DATE_DESC("primary_release_date.desc"), FIRST_AIR_DATE_DESC("first_air_date.desc"), } @@ -149,6 +150,8 @@ data class TmdbCollectionFilters( val withCompanies: String? = null, val withNetworks: String? = null, val year: Int? = null, + val watchRegion: String? = null, + val withWatchProviders: String? = null, ) data class TmdbSourceImportMetadata( diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt index 3f37d3d8..8f683a73 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/TmdbCollectionSourceResolver.kt @@ -325,6 +325,11 @@ object TmdbCollectionSourceResolver { putIfNotBlank("with_original_language", filters.withOriginalLanguage) putIfNotBlank("with_origin_country", filters.withOriginCountry) putIfNotBlank("with_keywords", filters.withKeywords) + if (!filters.withWatchProviders.isNullOrBlank()) { + put("with_watch_providers", filters.withWatchProviders) + put("watch_region", filters.watchRegion?.takeIf { it.isNotBlank() } ?: "US") + put("with_watch_monetization_types", "flatrate|free|ads|rent|buy") + } putIfNotBlank("year", filters.year?.takeIf { mediaType == TmdbCollectionMediaType.MOVIE }?.toString()) putIfNotBlank("first_air_date_year", filters.year?.takeIf { mediaType == TmdbCollectionMediaType.TV }?.toString()) putIfNotBlank( @@ -358,6 +363,7 @@ object TmdbCollectionSourceResolver { compareByDescending { it.imdbRating?.toDoubleOrNull() ?: -1.0 } .thenByDescending { it.rawReleaseDate ?: it.releaseInfo.orEmpty() }, ) + TmdbCollectionSort.VOTE_COUNT_DESC.value -> sortedByDescending { it.voteCount ?: 0 } TmdbCollectionSort.RELEASE_DATE_DESC.value, TmdbCollectionSort.FIRST_AIR_DATE_DESC.value -> sortedByDescending { it.rawReleaseDate ?: it.releaseInfo.orEmpty() } TmdbCollectionSort.POPULAR_DESC.value, @@ -395,6 +401,7 @@ object TmdbCollectionSourceResolver { TmdbCollectionMediaType.TV -> firstAirDate }, popularity = popularity, + voteCount = voteCount, imdbRating = voteAverage?.let { ((it * 10).roundToInt() / 10.0).toString() }, ) } @@ -412,6 +419,7 @@ object TmdbCollectionSourceResolver { releaseInfo = releaseDate?.take(4), rawReleaseDate = releaseDate, popularity = popularity, + voteCount = voteCount, imdbRating = voteAverage?.let { ((it * 10).roundToInt() / 10.0).toString() }, ) } @@ -440,6 +448,7 @@ object TmdbCollectionSourceResolver { TmdbCollectionMediaType.TV -> firstAirDate }, popularity = popularity, + voteCount = voteCount, imdbRating = voteAverage?.let { ((it * 10).roundToInt() / 10.0).toString() }, ) } @@ -468,6 +477,7 @@ object TmdbCollectionSourceResolver { TmdbCollectionMediaType.TV -> firstAirDate }, popularity = popularity, + voteCount = voteCount, imdbRating = voteAverage?.let { ((it * 10).roundToInt() / 10.0).toString() }, ) } @@ -508,6 +518,7 @@ object TmdbCollectionSourceResolver { when (sortBy) { TmdbCollectionSort.FIRST_AIR_DATE_DESC.value -> TmdbCollectionSort.RELEASE_DATE_DESC.value TmdbCollectionSort.ORIGINAL.value -> TmdbCollectionSort.POPULAR_DESC.value + TmdbCollectionSort.VOTE_COUNT_DESC.value -> TmdbCollectionSort.VOTE_COUNT_DESC.value null, "" -> TmdbCollectionSort.POPULAR_DESC.value else -> sortBy } @@ -516,6 +527,7 @@ object TmdbCollectionSourceResolver { when (sortBy) { TmdbCollectionSort.RELEASE_DATE_DESC.value -> TmdbCollectionSort.FIRST_AIR_DATE_DESC.value TmdbCollectionSort.ORIGINAL.value -> TmdbCollectionSort.POPULAR_DESC.value + TmdbCollectionSort.VOTE_COUNT_DESC.value -> TmdbCollectionSort.VOTE_COUNT_DESC.value null, "" -> TmdbCollectionSort.POPULAR_DESC.value else -> sortBy } @@ -640,6 +652,7 @@ private data class TmdbPersonCreditCast( @SerialName("release_date") val releaseDate: String? = null, @SerialName("first_air_date") val firstAirDate: String? = null, @SerialName("vote_average") val voteAverage: Double? = null, + @SerialName("vote_count") val voteCount: Int? = null, val popularity: Double? = null, ) @@ -658,6 +671,7 @@ private data class TmdbPersonCreditCrew( @SerialName("first_air_date") val firstAirDate: String? = null, val job: String? = null, @SerialName("vote_average") val voteAverage: Double? = null, + @SerialName("vote_count") val voteCount: Int? = null, val popularity: Double? = null, ) @@ -675,6 +689,7 @@ private data class TmdbListItem( @SerialName("release_date") val releaseDate: String? = null, @SerialName("first_air_date") val firstAirDate: String? = null, @SerialName("vote_average") val voteAverage: Double? = null, + @SerialName("vote_count") val voteCount: Int? = null, val popularity: Double? = null, ) @@ -687,5 +702,6 @@ private data class TmdbCollectionPart( @SerialName("backdrop_path") val backdropPath: String? = null, @SerialName("release_date") val releaseDate: String? = null, @SerialName("vote_average") val voteAverage: Double? = null, + @SerialName("vote_count") val voteCount: Int? = null, val popularity: Double? = null, ) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt index 50add3ca..00afb459 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailMetaInfo.kt @@ -80,10 +80,12 @@ fun DetailMetaInfo( val runtimeText = formatRuntimeForDisplay(meta.runtime) val ageBadge = meta.ageRating?.trim()?.takeIf { it.isNotBlank() } val hasMdbImdbRating = meta.externalRatings.any { it.source == PROVIDER_IMDB } + val validImdbRating = meta.imdbRating + ?.takeIf { raw -> raw.toDoubleOrNull()?.let { it > 0.0 } == true } val hasMetaRow = releaseLine != null || runtimeText != null || ageBadge != null || - (meta.imdbRating != null && !hasMdbImdbRating) + (validImdbRating != null && !hasMdbImdbRating) if (hasMetaRow) { Row( verticalAlignment = Alignment.CenterVertically, @@ -108,7 +110,7 @@ fun DetailMetaInfo( ageBadge?.let { badge -> DetailHeroMetaBadge(text = badge) } - if (meta.imdbRating != null && !hasMdbImdbRating) { + if (validImdbRating != null && !hasMdbImdbRating) { Row( verticalAlignment = Alignment.CenterVertically, ) { @@ -129,7 +131,7 @@ fun DetailMetaInfo( } Spacer(modifier = Modifier.width(5.dp)) Text( - text = meta.imdbRating, + text = validImdbRating, style = MaterialTheme.typography.titleMedium, color = ImdbYellow, fontWeight = FontWeight.Bold, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeModels.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeModels.kt index b51ed66b..dbf8b793 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeModels.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/home/HomeModels.kt @@ -15,6 +15,7 @@ data class MetaPreview( val releaseInfo: String? = null, val rawReleaseDate: String? = null, val popularity: Double? = null, + val voteCount: Int? = null, val imdbRating: String? = null, val genres: List = emptyList(), )