add multi search

This commit is contained in:
Owen LeJeune
2022-09-07 16:25:13 -04:00
parent b6ee061f01
commit dd0a395bf9
17 changed files with 158 additions and 80 deletions

View File

@@ -413,11 +413,20 @@ class MainActivity : MonetCompatActivity() {
)
) {
it.arguments?.let { arguments ->
val title = arguments.getString(NavConstants.SEARCH_TITLE_KEY) ?: ""
val type = if (preferences.multiSearch) {
MediaViewType.MIXED
// val title = arguments.getString(NavConstants.SEARCH_TITLE_KEY) ?: ""
// val type = if (preferences.multiSearch) {
// MediaViewType.MIXED
// } else {
// MediaViewType[arguments.getInt(NavConstants.SEARCH_ID_KEY)]
// }
val (type, title) = if (preferences.multiSearch) {
Pair(MediaViewType.MIXED, "")
} else {
MediaViewType[arguments.getInt(NavConstants.SEARCH_ID_KEY)]
Pair(
MediaViewType[arguments.getInt(NavConstants.SEARCH_ID_KEY)],
arguments.getString(NavConstants.SEARCH_TITLE_KEY) ?: ""
)
}
SearchScreen(

View File

@@ -1,5 +1,6 @@
package com.owenlejeune.tvtime.api
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializer
import org.koin.core.component.KoinComponent
@@ -9,17 +10,8 @@ import retrofit2.converter.gson.GsonConverterFactory
class GsonConverter: ConverterFactoryFactory, KoinComponent {
private val deserializers: Map<Class<*>, JsonDeserializer<*>> by inject()
private val gson: Gson by inject()
override fun get(): Converter.Factory {
val builder = GsonBuilder()
override fun get(): Converter.Factory = GsonConverterFactory.create(gson)
deserializers.forEach { deserializer ->
builder.registerTypeAdapter(deserializer.key, deserializer.value)
}
val gson = builder.create()
return GsonConverterFactory.create(gson)
}
}

View File

@@ -0,0 +1,26 @@
package com.owenlejeune.tvtime.api.tmdb.api
import com.google.gson.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.lang.reflect.Type
abstract class BaseDeserializer<T>: JsonDeserializer<T>, KoinComponent {
protected val gson: Gson by inject()
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): T {
if (json?.isJsonObject == true) {
val obj = json.asJsonObject
return processJson(obj)
}
throw JsonParseException("Not a json object")
}
protected abstract fun processJson(obj: JsonObject): T
}

View File

@@ -27,6 +27,6 @@ interface SearchApi {
suspend fun searchPeople(@Query("query") query: String, @Query("page") page: Int): Response<SearchResult<SearchResultPerson>>
@GET("search/multi")
suspend fun searchMulti(@Query("query") query: String, @Query("page") page: Int)
suspend fun searchMulti(@Query("query") query: String, @Query("page") page: Int): Response<SearchResult<SortableSearchResult>>
}

View File

@@ -39,4 +39,8 @@ class SearchService: KoinComponent {
return service.searchPeople(query, page)
}
suspend fun searchMulti(query: String, page: Int = 1): Response<SearchResult<SortableSearchResult>> {
return service.searchMulti(query, page)
}
}

View File

@@ -1,36 +1,26 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer
import com.google.gson.*
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import com.owenlejeune.tvtime.api.tmdb.api.BaseDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.KnownFor
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.KnownForMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.KnownForTv
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
import java.lang.reflect.Type
class KnownForDeserializer: JsonDeserializer<KnownFor> {
class KnownForDeserializer: BaseDeserializer<KnownFor>() {
companion object {
const val MEDIA_TYPE = "media_type"
}
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): KnownFor {
if (json?.isJsonObject == true) {
val obj = json.asJsonObject
if (obj.has(MEDIA_TYPE)) {
val typeStr = obj.get(MEDIA_TYPE).asString
return when (Gson().fromJson(typeStr, MediaViewType::class.java)) {
MediaViewType.MOVIE -> Gson().fromJson(obj.toString(), KnownForMovie::class.java)
MediaViewType.TV -> Gson().fromJson(obj.toString(), KnownForTv::class.java)
else -> throw JsonParseException("Not a valid MediaViewType: $typeStr")
}
override fun processJson(obj: JsonObject): KnownFor {
if (obj.has(MediaViewType.JSON_KEY)) {
val typeStr = obj.get(MediaViewType.JSON_KEY).asString
return when (gson.fromJson(typeStr, MediaViewType::class.java)) {
MediaViewType.MOVIE -> gson.fromJson(obj.toString(), KnownForMovie::class.java)
MediaViewType.TV -> gson.fromJson(obj.toString(), KnownForTv::class.java)
else -> throw JsonParseException("Not a valid MediaViewType: $typeStr")
}
throw JsonParseException("JSON object has no property $MEDIA_TYPE")
}
throw JsonParseException("Not a JSON object")
throw JsonParseException("JSON object has no property ${MediaViewType.JSON_KEY}")
}
}

View File

@@ -0,0 +1,28 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import com.owenlejeune.tvtime.api.tmdb.api.BaseDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultPerson
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultTv
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SortableSearchResult
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class SortableSearchResultDeserializer: BaseDeserializer<SortableSearchResult>() {
override fun processJson(obj: JsonObject): SortableSearchResult {
if (obj.has(MediaViewType.JSON_KEY)) {
val typeStr = obj.get(MediaViewType.JSON_KEY).asString
return when (gson.fromJson(typeStr, MediaViewType::class.java)) {
MediaViewType.PERSON -> gson.fromJson(obj.toString(), SearchResultPerson::class.java)
MediaViewType.MOVIE -> gson.fromJson(obj.toString(), SearchResultMovie::class.java)
MediaViewType.TV -> gson.fromJson(obj.toString(), SearchResultTv::class.java)
else -> throw JsonParseException("Not a valid MediaViewType: $typeStr")
}
}
throw JsonParseException("JSON object has no property ${MediaViewType.JSON_KEY}")
}
}

View File

@@ -4,7 +4,6 @@ import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
abstract class SearchResultMedia(
var type: MediaViewType,
@SerializedName("overview") val overview: String,
@SerializedName("vote_average") val voteAverage: Float,
@SerializedName("vote_count") val voteCount: Int,
@@ -14,7 +13,8 @@ abstract class SearchResultMedia(
@SerializedName("original_language") val originalLanguage: String,
@SerializedName("original_name", alternate = ["original_title"]) val originalName: String,
@SerializedName("poster_path") val posterPath: String?,
type: MediaViewType,
id: Int,
name: String,
popularity: Float
): SortableSearchResult(popularity, id, name)
): SortableSearchResult(type, popularity, id, name)

View File

@@ -19,6 +19,6 @@ class SearchResultMovie(
@SerializedName("adult") val isAdult: Boolean,
@SerializedName("video") val video: Boolean,
): SearchResultMedia(
MediaViewType.MOVIE, overview, voteAverage, voteCount, releaseDate, backdropPath,
genreIds, originalLanguage, originalName, posterPath, id, name, popularity
overview, voteAverage, voteCount, releaseDate, backdropPath, genreIds,
originalLanguage, originalName, posterPath, MediaViewType.MOVIE, id, name, popularity
)

View File

@@ -1,6 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class SearchResultPerson(
@SerializedName("profile_path") val profilePath: String,
@@ -9,4 +10,4 @@ class SearchResultPerson(
id: Int,
name: String,
popularity: Float
): SortableSearchResult(popularity, id, name)
): SortableSearchResult(MediaViewType.PERSON, popularity, id, name)

View File

@@ -18,6 +18,6 @@ class SearchResultTv(
releaseDate: String,
@SerializedName("origin_country") val originCountry: List<String>,
): SearchResultMedia(
MediaViewType.TV, overview, voteAverage, voteCount, releaseDate, backdropPath,
genreIds, originalLanguage, originalName, posterPath, id, name, popularity
overview, voteAverage, voteCount, releaseDate, backdropPath, genreIds,
originalLanguage, originalName, posterPath, MediaViewType.TV, id, name, popularity
)

View File

@@ -1,8 +1,10 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
abstract class SortableSearchResult(
@SerializedName("media_type") val mediaType: MediaViewType,
@SerializedName("popularity") val popularity: Float,
@SerializedName("id") val id: Int,
@SerializedName("name", alternate = ["title"]) val name: String

View File

@@ -1,35 +1,24 @@
package com.owenlejeune.tvtime.api.tmdb.api.v4.deserializer
import com.google.gson.*
import com.owenlejeune.tvtime.api.tmdb.api.BaseDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.ListItem
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.ListMovie
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.ListTv
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
import java.lang.reflect.Type
class ListItemDeserializer: JsonDeserializer<ListItem> {
class ListItemDeserializer: BaseDeserializer<ListItem>() {
companion object {
const val MEDIA_TYPE = "media_type"
}
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): ListItem? {
if (json?.isJsonObject == true) {
val obj = json.asJsonObject
if (obj.has(MEDIA_TYPE)) {
val typeStr = obj.get(MEDIA_TYPE).asString
return when (Gson().fromJson(typeStr, MediaViewType::class.java)) {
MediaViewType.MOVIE -> Gson().fromJson(obj.toString(), ListMovie::class.java)
MediaViewType.TV -> Gson().fromJson(obj.toString(), ListTv::class.java)
else -> throw JsonParseException("Not a valid MediaViewType: $typeStr")
}
override fun processJson(obj: JsonObject): ListItem {
if (obj.has(MediaViewType.JSON_KEY)) {
val typeStr = obj.get(MediaViewType.JSON_KEY).asString
return when (gson.fromJson(typeStr, MediaViewType::class.java)) {
MediaViewType.MOVIE -> gson.fromJson(obj.toString(), ListMovie::class.java)
MediaViewType.TV -> gson.fromJson(obj.toString(), ListTv::class.java)
else -> throw JsonParseException("Not a valid MediaViewType: $typeStr")
}
throw JsonParseException("JSON object has no property $MEDIA_TYPE")
}
throw JsonParseException("Not a JSON object")
throw JsonParseException("JSON object has no property ${MediaViewType.JSON_KEY}")
}
}

View File

@@ -1,11 +1,15 @@
package com.owenlejeune.tvtime.di.modules
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializer
import com.owenlejeune.tvtime.BuildConfig
import com.owenlejeune.tvtime.api.*
import com.owenlejeune.tvtime.api.tmdb.TmdbClient
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.KnownForDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer.SortableSearchResultDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.KnownFor
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SortableSearchResult
import com.owenlejeune.tvtime.api.tmdb.api.v4.deserializer.ListItemDeserializer
import com.owenlejeune.tvtime.api.tmdb.api.v4.model.ListItem
import com.owenlejeune.tvtime.preferences.AppPreferences
@@ -32,9 +36,18 @@ val networkModule = module {
single<Map<Class<*>, JsonDeserializer<*>>> {
mapOf(
ListItem::class.java to ListItemDeserializer(),
KnownFor::class.java to KnownForDeserializer()
KnownFor::class.java to KnownForDeserializer(),
SortableSearchResult::class.java to SortableSearchResultDeserializer()
)
}
single {
GsonBuilder().apply {
get<Map<Class<*>, JsonDeserializer<*>>>().forEach { des ->
registerTypeAdapter(des.key, des.value)
}
}.create()
}
}
val preferencesModule = module {

View File

@@ -108,7 +108,6 @@ fun SearchScreen(
SearchResultListView(
showLoadingAnimation = showLoadingAnimation,
currentQuery = searchValue,
resultsSortingStrategy = { o1, o2 -> o2.popularity.compareTo(o1.popularity) },
searchExecutor = { searchResults: MutableState<List<SearchResultTv>> ->
searchTv(searchValue.value, searchResults)
}
@@ -120,7 +119,6 @@ fun SearchScreen(
SearchResultListView(
showLoadingAnimation = showLoadingAnimation,
currentQuery = searchValue,
resultsSortingStrategy = { o1, o2 -> o2.popularity.compareTo(o1.popularity) },
searchExecutor = { searchResults: MutableState<List<SearchResultMovie>> ->
searchMovies(searchValue.value, searchResults)
}
@@ -132,7 +130,6 @@ fun SearchScreen(
SearchResultListView(
showLoadingAnimation = showLoadingAnimation,
currentQuery = searchValue,
resultsSortingStrategy = { o1, o2 -> o2.popularity.compareTo(o1.popularity) },
searchExecutor = { searchResults: MutableState<List<SearchResultPerson>> ->
searchPeople(searchValue.value, searchResults)
}
@@ -145,11 +142,24 @@ fun SearchScreen(
showLoadingAnimation = showLoadingAnimation,
currentQuery = searchValue,
searchExecutor = { searchResults: MutableState<List<SortableSearchResult>> ->
searchMulti(searchValue.value, searchResults)
},
resultsSortingStrategy = { o1, o2 -> o2.popularity.compareTo(o1.popularity) }
) { item ->
when (item.mediaType) {
MediaViewType.MOVIE -> MovieSearchResultView(
appNavController = appNavController,
result = item as SearchResultMovie
)
MediaViewType.TV -> TvSearchResultView(
appNavController = appNavController,
result = item as SearchResultTv
)
MediaViewType.PERSON -> PeopleSearchResultView(
appNavController = appNavController,
result = item as SearchResultPerson
)
else -> {}
}
}
}
else -> {}
@@ -167,7 +177,6 @@ private fun <T: SortableSearchResult> SearchResultListView(
showLoadingAnimation: MutableState<Boolean>,
currentQuery: MutableState<String>,
searchExecutor: (MutableState<List<T>>) -> Unit,
resultsSortingStrategy: Comparator<T>? = null,
viewRenderer: @Composable (T) -> Unit
) {
val searchResults = remember { mutableStateOf(emptyList<T>()) }
@@ -184,7 +193,7 @@ private fun <T: SortableSearchResult> SearchResultListView(
) {
Spacer(modifier = Modifier.weight(1f))
Text(
text = "No search results found",
text = stringResource(R.string.no_search_results),
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.align(Alignment.CenterHorizontally),
fontSize = 18.sp
@@ -196,9 +205,7 @@ private fun <T: SortableSearchResult> SearchResultListView(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
val items = resultsSortingStrategy?.let {
searchResults.value.sortedWith(resultsSortingStrategy)
} ?: searchResults.value
val items = searchResults.value.sortedByDescending { it.popularity }
listItems(items) { item ->
viewRenderer(item)
}
@@ -391,6 +398,20 @@ private fun searchPeople(
}
}
private fun searchMulti(
query: String,
searchResults: MutableState<List<SortableSearchResult>>
) {
CoroutineScope(Dispatchers.IO).launch {
val response = SearchService().searchMulti(query)
if (response.isSuccessful) {
withContext(Dispatchers.Main) {
searchResults.value = response.body()?.results ?: emptyList()
}
}
}
}
private fun getCast(
id: Int,
detailService: DetailService,

View File

@@ -13,6 +13,8 @@ enum class MediaViewType {
MIXED;
companion object {
const val JSON_KEY = "media_type"
operator fun get(oridinal: Int): MediaViewType {
return values()[oridinal]
}

View File

@@ -123,4 +123,5 @@
<!-- search results -->
<string name="search_result_tv_services">TV Series</string>
<string name="no_search_results">No search results found</string>
</resources>