From 4381d19229392bd5083df54ed069a3df664957e5 Mon Sep 17 00:00:00 2001 From: kmshaw1990 Date: Thu, 14 May 2026 02:04:18 -0500 Subject: [PATCH 1/6] added tmdb watch providers for streaming services, and sorting by most voted --- .../composeResources/values/strings.xml | 13 +++++ .../collection/CollectionEditorScreen.kt | 50 +++++++++++++++++++ .../features/collection/CollectionModels.kt | 3 ++ .../TmdbCollectionSourceResolver.kt | 16 ++++++ .../com/nuvio/app/features/home/HomeModels.kt | 1 + 5 files changed, 83 insertions(+) diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 782a24e0..8ed05d6c 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -247,6 +247,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 + Pipe-separated TMDB provider IDs. Example: 8|337 for Netflix and Disney+. + 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/features/collection/CollectionEditorScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/collection/CollectionEditorScreen.kt index 7219395a..97b44414 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_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 }) + } + }, + ) + 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 }) + } + }, + ) } } } @@ -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..816d1228 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") + } 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/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(), ) From 47b81ae51ca8e7cb5049da2481e0e910cb3cffc9 Mon Sep 17 00:00:00 2001 From: kmshaw1990 Date: Thu, 14 May 2026 13:34:17 -0500 Subject: [PATCH 2/6] Reorder Watch Provider Above Watch Region --- .../collection/CollectionEditorScreen.kt | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) 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 97b44414..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 @@ -1354,30 +1354,6 @@ private fun TmdbSourcePickerScreen( } }, ) - 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 }) - } - }, - ) TmdbQuickChips( label = stringResource(Res.string.collections_editor_tmdb_quick_watch_providers), chips = listOf( @@ -1402,6 +1378,30 @@ private fun TmdbSourcePickerScreen( } }, ) + 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 }) + } + }, + ) } } } From 87d325bdac104494aff924bedda7912eb91276e8 Mon Sep 17 00:00:00 2001 From: kmshaw1990 Date: Thu, 14 May 2026 13:40:28 -0500 Subject: [PATCH 3/6] add more monetization types for watch provider queries --- .../app/features/collection/TmdbCollectionSourceResolver.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 816d1228..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 @@ -328,7 +328,7 @@ object TmdbCollectionSourceResolver { 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") + 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()) From c83d766f637e2705639df833bcd97c4e309a416e Mon Sep 17 00:00:00 2001 From: kmshaw1990 Date: Thu, 14 May 2026 14:04:39 -0500 Subject: [PATCH 4/6] Update watch provider string description --- composeApp/src/commonMain/composeResources/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 5713e531..51490c7e 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -252,7 +252,7 @@ ISO 3166-1 country code where the title is available. Example: US, GB. Quick watch regions Watch provider IDs - Pipe-separated TMDB provider IDs. Example: 8|337 for Netflix and Disney+. + Use TMDB watch provider IDs. Separate multiple with commas for AND, or pipes for OR. 8|337|350 Quick watch providers Netflix From 7635ceb8d23144fa51685e17bae185b25a3cfa1b Mon Sep 17 00:00:00 2001 From: VenusIsJaded Date: Thu, 14 May 2026 16:10:05 -0500 Subject: [PATCH 5/6] fix: hide IMDb 0.0 rating on unreleased content closes #1046 --- .../app/features/details/components/DetailMetaInfo.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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, From 5de09e1fa6014ffec423b31b8e20010b5e288688 Mon Sep 17 00:00:00 2001 From: VenusIsJaded Date: Thu, 14 May 2026 17:43:29 -0500 Subject: [PATCH 6/6] fix: back button on profile screens closes app instead of navigating back closes #1058 --- composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index 2069679d..0ecca10c 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -467,6 +467,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) @@ -489,6 +494,9 @@ fun App() { ) } AppGateScreen.ProfileEdit.name -> { + PlatformBackHandler(enabled = gateScreen == AppGateScreen.ProfileEdit.name) { + gateScreen = AppGateScreen.ProfileSelection.name + } ProfileEditScreen( profile = editingProfile, onBack = { gateScreen = AppGateScreen.ProfileSelection.name },