mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-19 18:21:19 -05:00
add searching for tv shows and people
This commit is contained in:
@@ -91,6 +91,13 @@ class TmdbClient: KoinComponent {
|
|||||||
builder.addQueryParams(languageParam)
|
builder.addQueryParams(languageParam)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldIncludeRegionParam(segments)) {
|
||||||
|
val locale = Locale.current
|
||||||
|
val regionParam = QueryParam("region", locale.region)
|
||||||
|
|
||||||
|
builder.addQueryParams(regionParam)
|
||||||
|
}
|
||||||
|
|
||||||
val requestBuilder = chain.request().newBuilder().url(builder.build())
|
val requestBuilder = chain.request().newBuilder().url(builder.build())
|
||||||
|
|
||||||
val request = requestBuilder.build()
|
val request = requestBuilder.build()
|
||||||
@@ -121,6 +128,16 @@ class TmdbClient: KoinComponent {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun shouldIncludeRegionParam(urlSegments: List<String>): Boolean {
|
||||||
|
val includedRoutes = listOf("search")
|
||||||
|
for (route in includedRoutes) {
|
||||||
|
if (urlSegments.contains(route)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class V4Interceptor: Interceptor {
|
private inner class V4Interceptor: Interceptor {
|
||||||
|
|||||||
@@ -26,4 +26,7 @@ interface SearchApi {
|
|||||||
@GET("search/person")
|
@GET("search/person")
|
||||||
suspend fun searchPeople(@Query("query") query: String, @Query("page") page: Int): Response<SearchResult<SearchResultPerson>>
|
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)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -39,75 +39,4 @@ class SearchService: KoinComponent {
|
|||||||
return service.searchPeople(query, page)
|
return service.searchPeople(query, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun searchAll(query: String, onResultsUpdated: (List<Any>) -> Unit) {
|
|
||||||
val results = TreeSet<Any> {a, b ->
|
|
||||||
when (a) {
|
|
||||||
is Keyword -> {
|
|
||||||
if (b is Keyword) 0 else -3
|
|
||||||
}
|
|
||||||
is ProductionCompany -> {
|
|
||||||
if (b is ProductionCompany) 0 else -2
|
|
||||||
}
|
|
||||||
is Collection -> {
|
|
||||||
if (b is Collection) 0 else -1
|
|
||||||
}
|
|
||||||
is SortableSearchResult -> {
|
|
||||||
when (b) {
|
|
||||||
is SortableSearchResult -> {
|
|
||||||
when {
|
|
||||||
a.popularity > b.popularity -> 1
|
|
||||||
a.popularity < b.popularity -> -1
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
searchMovies(query).body()?.apply {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
results.addAll(results)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
searchTv(query).body()?.apply {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
results.addAll(results)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
searchPeople(query).body()?.apply {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
results.addAll(results)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
searchCompanies(query).body()?.apply {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
results.addAll(results)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
searchCollections(query).body()?.apply {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
results.addAll(results)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
searchKeywords(query).body()?.apply {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
results.addAll(results)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,8 @@ package com.owenlejeune.tvtime.api.tmdb.api.v3.deserializer
|
|||||||
|
|
||||||
import com.google.gson.*
|
import com.google.gson.*
|
||||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.KnownFor
|
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 com.owenlejeune.tvtime.ui.screens.main.MediaViewType
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
@@ -21,8 +23,8 @@ class KnownForDeserializer: JsonDeserializer<KnownFor> {
|
|||||||
if (obj.has(MEDIA_TYPE)) {
|
if (obj.has(MEDIA_TYPE)) {
|
||||||
val typeStr = obj.get(MEDIA_TYPE).asString
|
val typeStr = obj.get(MEDIA_TYPE).asString
|
||||||
return when (Gson().fromJson(typeStr, MediaViewType::class.java)) {
|
return when (Gson().fromJson(typeStr, MediaViewType::class.java)) {
|
||||||
MediaViewType.MOVIE -> Gson().fromJson(obj.toString(), KnownFor::class.java)
|
MediaViewType.MOVIE -> Gson().fromJson(obj.toString(), KnownForMovie::class.java)
|
||||||
MediaViewType.TV -> Gson().fromJson(obj.toString(), KnownFor::class.java)
|
MediaViewType.TV -> Gson().fromJson(obj.toString(), KnownForTv::class.java)
|
||||||
else -> throw JsonParseException("Not a valid MediaViewType: $typeStr")
|
else -> throw JsonParseException("Not a valid MediaViewType: $typeStr")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ class Collection(
|
|||||||
@SerializedName("name") val name: String,
|
@SerializedName("name") val name: String,
|
||||||
@SerializedName("poster_path") val posterPath: String?,
|
@SerializedName("poster_path") val posterPath: String?,
|
||||||
@SerializedName("backdrop_path") val backdropPath: String?
|
@SerializedName("backdrop_path") val backdropPath: String?
|
||||||
)
|
): Searchable
|
||||||
@@ -5,4 +5,4 @@ import com.google.gson.annotations.SerializedName
|
|||||||
class Keyword(
|
class Keyword(
|
||||||
@SerializedName("id") val id: Int,
|
@SerializedName("id") val id: Int,
|
||||||
@SerializedName("name") val name: String
|
@SerializedName("name") val name: String
|
||||||
)
|
): Searchable
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.owenlejeune.tvtime.api.tmdb.api.v3.model
|
|||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
|
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
|
||||||
|
|
||||||
class KnowForTv(
|
class KnownForTv(
|
||||||
backdropPath: String?,
|
backdropPath: String?,
|
||||||
releaseDate: String,
|
releaseDate: String,
|
||||||
genreIds: List<Int>,
|
genreIds: List<Int>,
|
||||||
@@ -7,4 +7,4 @@ data class ProductionCompany(
|
|||||||
@SerializedName("name") val name: String,
|
@SerializedName("name") val name: String,
|
||||||
@SerializedName("logo_path") val logoPath: String?,
|
@SerializedName("logo_path") val logoPath: String?,
|
||||||
@SerializedName("origin_country") val originCountry: String?
|
@SerializedName("origin_country") val originCountry: String?
|
||||||
)
|
): Searchable
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.owenlejeune.tvtime.api.tmdb.api.v3.model
|
|||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
abstract class SearchResult<T> (
|
class SearchResult<T: Searchable> (
|
||||||
@SerializedName("page") val page: Int,
|
@SerializedName("page") val page: Int,
|
||||||
@SerializedName("total_pages") val totalPages: Int,
|
@SerializedName("total_pages") val totalPages: Int,
|
||||||
@SerializedName("total_results") val totalResults: Int,
|
@SerializedName("total_results") val totalResults: Int,
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
|
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
|
||||||
|
|
||||||
abstract class SearchResultMedia(
|
abstract class SearchResultMedia(
|
||||||
var type: SearchResultType,
|
var type: MediaViewType,
|
||||||
@SerializedName("id") val id: Int,
|
|
||||||
@SerializedName("overview") val overview: String,
|
@SerializedName("overview") val overview: String,
|
||||||
@SerializedName("name", alternate = ["title"]) val name: String,
|
|
||||||
@SerializedName("vote_average") val voteAverage: Float,
|
@SerializedName("vote_average") val voteAverage: Float,
|
||||||
@SerializedName("vote_count") val voteCount: Int,
|
@SerializedName("vote_count") val voteCount: Int,
|
||||||
@SerializedName("release_date", alternate = ["first_air_date", "air_date"]) val releaseDate: String,
|
@SerializedName("release_date", alternate = ["first_air_date", "air_date"]) val releaseDate: String,
|
||||||
@@ -15,10 +14,7 @@ abstract class SearchResultMedia(
|
|||||||
@SerializedName("original_language") val originalLanguage: String,
|
@SerializedName("original_language") val originalLanguage: String,
|
||||||
@SerializedName("original_name", alternate = ["original_title"]) val originalName: String,
|
@SerializedName("original_name", alternate = ["original_title"]) val originalName: String,
|
||||||
@SerializedName("poster_path") val posterPath: String?,
|
@SerializedName("poster_path") val posterPath: String?,
|
||||||
|
id: Int,
|
||||||
|
name: String,
|
||||||
popularity: Float
|
popularity: Float
|
||||||
): SortableSearchResult(popularity) {
|
): SortableSearchResult(popularity, id, name)
|
||||||
enum class SearchResultType {
|
|
||||||
MOVIE,
|
|
||||||
TV
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
|
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
|
||||||
|
|
||||||
class SearchResultMovie(
|
class SearchResultMovie(
|
||||||
id: Int,
|
id: Int,
|
||||||
@@ -18,6 +19,6 @@ class SearchResultMovie(
|
|||||||
@SerializedName("adult") val isAdult: Boolean,
|
@SerializedName("adult") val isAdult: Boolean,
|
||||||
@SerializedName("video") val video: Boolean,
|
@SerializedName("video") val video: Boolean,
|
||||||
): SearchResultMedia(
|
): SearchResultMedia(
|
||||||
SearchResultType.MOVIE, id, overview, name, voteAverage, voteCount, releaseDate,
|
MediaViewType.MOVIE, overview, voteAverage, voteCount, releaseDate, backdropPath,
|
||||||
backdropPath, genreIds, originalLanguage, originalName, posterPath, popularity
|
genreIds, originalLanguage, originalName, posterPath, id, name, popularity
|
||||||
)
|
)
|
||||||
@@ -5,8 +5,8 @@ import com.google.gson.annotations.SerializedName
|
|||||||
class SearchResultPerson(
|
class SearchResultPerson(
|
||||||
@SerializedName("profile_path") val profilePath: String,
|
@SerializedName("profile_path") val profilePath: String,
|
||||||
@SerializedName("adult") val isAdult: Boolean,
|
@SerializedName("adult") val isAdult: Boolean,
|
||||||
@SerializedName("id") val id: Int,
|
|
||||||
@SerializedName("name") val name: String,
|
|
||||||
@SerializedName("known_for") val knownFor: List<KnownFor>,
|
@SerializedName("known_for") val knownFor: List<KnownFor>,
|
||||||
|
id: Int,
|
||||||
|
name: String,
|
||||||
popularity: Float
|
popularity: Float
|
||||||
): SortableSearchResult(popularity)
|
): SortableSearchResult(popularity, id, name)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
|
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
|
||||||
|
|
||||||
class SearchResultTv(
|
class SearchResultTv(
|
||||||
id: Int,
|
id: Int,
|
||||||
@@ -17,6 +18,6 @@ class SearchResultTv(
|
|||||||
releaseDate: String,
|
releaseDate: String,
|
||||||
@SerializedName("origin_country") val originCountry: List<String>,
|
@SerializedName("origin_country") val originCountry: List<String>,
|
||||||
): SearchResultMedia(
|
): SearchResultMedia(
|
||||||
SearchResultType.TV, id, overview, name, voteAverage, voteCount, releaseDate,
|
MediaViewType.TV, overview, voteAverage, voteCount, releaseDate, backdropPath,
|
||||||
backdropPath, genreIds, originalLanguage, originalName, posterPath, popularity
|
genreIds, originalLanguage, originalName, posterPath, id, name, popularity
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
|
||||||
|
|
||||||
|
|
||||||
|
interface Searchable
|
||||||
@@ -3,5 +3,7 @@ package com.owenlejeune.tvtime.api.tmdb.api.v3.model
|
|||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
abstract class SortableSearchResult(
|
abstract class SortableSearchResult(
|
||||||
@SerializedName("popularity") val popularity: Float
|
@SerializedName("popularity") val popularity: Float,
|
||||||
)
|
@SerializedName("id") val id: Int,
|
||||||
|
@SerializedName("name", alternate = ["title"]) val name: String
|
||||||
|
): Searchable
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.owenlejeune.tvtime.extensions
|
package com.owenlejeune.tvtime.extensions
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
|
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
|
||||||
import androidx.compose.foundation.lazy.grid.LazyGridScope
|
import androidx.compose.foundation.lazy.grid.LazyGridScope
|
||||||
@@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
fun <T: Any> LazyGridScope.lazyPagingItems(
|
fun <T: Any> LazyGridScope.lazyPagingItems(
|
||||||
lazyPagingItems: LazyPagingItems<T>,
|
lazyPagingItems: LazyPagingItems<T>,
|
||||||
itemContent: @Composable LazyGridItemScope.(value: T?) -> Unit
|
itemContent: @Composable LazyGridItemScope.(value: T?) -> Unit
|
||||||
@@ -19,7 +18,6 @@ fun <T: Any> LazyGridScope.lazyPagingItems(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
fun <T: Any> LazyGridScope.listItems(
|
fun <T: Any> LazyGridScope.listItems(
|
||||||
items: List<T>,
|
items: List<T>,
|
||||||
itemContent: @Composable (value: T) -> Unit
|
itemContent: @Composable (value: T) -> Unit
|
||||||
@@ -30,11 +28,20 @@ fun <T: Any> LazyGridScope.listItems(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun <T: Any> LazyListScope.listItems(
|
fun <T: Any> LazyListScope.listItems(
|
||||||
items: List<T>,
|
items: Collection<T>,
|
||||||
itemContent: @Composable (value: T) -> Unit
|
itemContent: @Composable (value: T) -> Unit
|
||||||
) {
|
) {
|
||||||
items(items.size) { index ->
|
items(items.size) { index ->
|
||||||
itemContent(items[index])
|
itemContent(items.elementAt(index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T: Any> LazyListScope.lazyPagingItems(
|
||||||
|
lazyPagingItems: LazyPagingItems<T>,
|
||||||
|
itemContent: @Composable LazyItemScope.(value: T?) -> Unit
|
||||||
|
) {
|
||||||
|
items(lazyPagingItems.itemCount) { index ->
|
||||||
|
itemContent(lazyPagingItems[index])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,44 @@
|
|||||||
package com.owenlejeune.tvtime.ui.screens
|
package com.owenlejeune.tvtime.ui.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Card
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Clear
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.blur
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
import coil.compose.AsyncImage
|
||||||
import com.owenlejeune.tvtime.R
|
import com.owenlejeune.tvtime.R
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.api.v3.DetailService
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.api.v3.MoviesService
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.api.v3.SearchService
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.api.v3.TvService
|
||||||
|
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
|
||||||
|
import com.owenlejeune.tvtime.extensions.listItems
|
||||||
|
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
|
||||||
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
|
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
|
||||||
|
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchScreen(
|
fun SearchScreen(
|
||||||
@@ -26,15 +51,14 @@ fun SearchScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(color = MaterialTheme.colorScheme.background)
|
.background(color = MaterialTheme.colorScheme.background)
|
||||||
) {
|
) {
|
||||||
var searchValue by remember { mutableStateOf("") }
|
val searchValue = rememberSaveable { mutableStateOf("") }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
SmallTopAppBar(
|
SmallTopAppBar(
|
||||||
modifier = Modifier,
|
|
||||||
title = {
|
title = {
|
||||||
TextField(
|
TextField(
|
||||||
value = searchValue,
|
value = searchValue.value,
|
||||||
onValueChange = { searchValue = it },
|
onValueChange = { searchValue.value = it },
|
||||||
placeholder = { Text(text = stringResource(id = R.string.search_placeholder, title)) },
|
placeholder = { Text(text = stringResource(id = R.string.search_placeholder, title)) },
|
||||||
colors = TextFieldDefaults.textFieldColors(
|
colors = TextFieldDefaults.textFieldColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
@@ -42,7 +66,15 @@ fun SearchScreen(
|
|||||||
unfocusedIndicatorColor = MaterialTheme.colorScheme.surface
|
unfocusedIndicatorColor = MaterialTheme.colorScheme.surface
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester),
|
||||||
|
singleLine = true,
|
||||||
|
trailingIcon = {
|
||||||
|
if (searchValue.value.isNotEmpty()) {
|
||||||
|
IconButton(onClick = { searchValue.value = "" }) {
|
||||||
|
Icon(imageVector = Icons.Filled.Clear, contentDescription = "Clear search query")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
@@ -54,9 +86,325 @@ fun SearchScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
Divider(thickness = 2.dp, color = MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
|
||||||
|
val showLoadingAnimation = remember { mutableStateOf(false) }
|
||||||
|
if (showLoadingAnimation.value) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
trackColor = MaterialTheme.colorScheme.background
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
trackColor = MaterialTheme.colorScheme.background,
|
||||||
|
progress = 0f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchValue.value.isNotEmpty()) {
|
||||||
|
when (mediaViewType) {
|
||||||
|
MediaViewType.TV -> {
|
||||||
|
SearchResultListView(
|
||||||
|
showLoadingAnimation = showLoadingAnimation,
|
||||||
|
currentQuery = searchValue,
|
||||||
|
resultsSortingStrategy = { o1, o2 -> o2.popularity.compareTo(o1.popularity) },
|
||||||
|
searchExecutor = { searchResults: MutableState<List<SearchResultTv>> ->
|
||||||
|
searchTv(searchValue.value, searchResults)
|
||||||
|
}
|
||||||
|
) { tv ->
|
||||||
|
TvSearchResultView(result = tv, appNavController = appNavController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MediaViewType.MOVIE -> {
|
||||||
|
SearchResultListView(
|
||||||
|
showLoadingAnimation = showLoadingAnimation,
|
||||||
|
currentQuery = searchValue,
|
||||||
|
resultsSortingStrategy = { o1, o2 -> o2.popularity.compareTo(o1.popularity) },
|
||||||
|
searchExecutor = { searchResults: MutableState<List<SearchResultMovie>> ->
|
||||||
|
searchMovies(searchValue.value, searchResults)
|
||||||
|
}
|
||||||
|
) { movie ->
|
||||||
|
MovieSearchResultView(result = movie, appNavController = appNavController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MediaViewType.PERSON -> {
|
||||||
|
SearchResultListView(
|
||||||
|
showLoadingAnimation = showLoadingAnimation,
|
||||||
|
currentQuery = searchValue,
|
||||||
|
resultsSortingStrategy = { o1, o2 -> o2.popularity.compareTo(o1.popularity) },
|
||||||
|
searchExecutor = { searchResults: MutableState<List<SearchResultPerson>> ->
|
||||||
|
searchPeople(searchValue.value, searchResults)
|
||||||
|
}
|
||||||
|
) { person ->
|
||||||
|
PeopleSearchResultView(result = person, appNavController = appNavController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MediaViewType.MIXED -> {
|
||||||
|
SearchResultListView(
|
||||||
|
showLoadingAnimation = showLoadingAnimation,
|
||||||
|
currentQuery = searchValue,
|
||||||
|
searchExecutor = { searchResults: MutableState<List<SortableSearchResult>> ->
|
||||||
|
|
||||||
|
},
|
||||||
|
resultsSortingStrategy = { o1, o2 -> o2.popularity.compareTo(o1.popularity) }
|
||||||
|
) { item ->
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(key1 = "") {
|
LaunchedEffect(key1 = "") {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
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>()) }
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = currentQuery.value) {
|
||||||
|
showLoadingAnimation.value = true
|
||||||
|
searchExecutor(searchResults)
|
||||||
|
showLoadingAnimation.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentQuery.value.isNotEmpty() && searchResults.value.isEmpty()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
text = "No search results found",
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
val items = resultsSortingStrategy?.let {
|
||||||
|
searchResults.value.sortedWith(resultsSortingStrategy)
|
||||||
|
} ?: searchResults.value
|
||||||
|
listItems(items) { item ->
|
||||||
|
viewRenderer(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun <T: SortableSearchResult> SearchResultItemView(
|
||||||
|
appNavController: NavHostController,
|
||||||
|
mediaViewType: MediaViewType,
|
||||||
|
searchResult: T,
|
||||||
|
posterModel: (T) -> Any?,
|
||||||
|
backdropModel: (T) -> Any?,
|
||||||
|
additionalDetails: (T) -> List<String> = { emptyList() }
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(
|
||||||
|
onClick = {
|
||||||
|
appNavController.navigate("${MainNavItem.DetailView.route}/${mediaViewType}/${searchResult.id}")
|
||||||
|
}
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
elevation = 8.dp
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.height(112.dp)
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = backdropModel(searchResult),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier
|
||||||
|
.blur(radius = 10.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.7f))
|
||||||
|
.blur(radius = 10.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(8.dp)
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = posterModel(searchResult),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
.size(width = 75.dp, height = 112.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = searchResult.name,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
additionalDetails(searchResult)
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
.forEach { item ->
|
||||||
|
Text(
|
||||||
|
text = item,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MovieSearchResultView(
|
||||||
|
appNavController: NavHostController,
|
||||||
|
result: SearchResultMovie
|
||||||
|
) {
|
||||||
|
val cast = remember { mutableStateOf<List<CastMember>?>(null) }
|
||||||
|
getCast(result.id, MoviesService(), cast)
|
||||||
|
|
||||||
|
SearchResultItemView(
|
||||||
|
appNavController = appNavController,
|
||||||
|
mediaViewType = MediaViewType.MOVIE,
|
||||||
|
searchResult = result,
|
||||||
|
posterModel = { TmdbUtils.getFullPosterPath(result.posterPath) },
|
||||||
|
backdropModel = { TmdbUtils.getFullBackdropPath(result.backdropPath) },
|
||||||
|
additionalDetails = {
|
||||||
|
listOf(
|
||||||
|
TmdbUtils.releaseYearFromData(result.releaseDate),
|
||||||
|
cast.value?.joinToString(separator = ", ") { it.name } ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TvSearchResultView(
|
||||||
|
appNavController: NavHostController,
|
||||||
|
result: SearchResultTv
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val cast = remember { mutableStateOf<List<CastMember>?>(null) }
|
||||||
|
getCast(result.id, TvService(), cast)
|
||||||
|
|
||||||
|
SearchResultItemView(
|
||||||
|
appNavController = appNavController,
|
||||||
|
mediaViewType = MediaViewType.TV,
|
||||||
|
searchResult = result,
|
||||||
|
posterModel = { TmdbUtils.getFullPosterPath(result.posterPath) },
|
||||||
|
backdropModel = { TmdbUtils.getFullBackdropPath(result.backdropPath) },
|
||||||
|
additionalDetails = {
|
||||||
|
listOf(
|
||||||
|
"${TmdbUtils.releaseYearFromData(result.releaseDate)} ${context.getString(R.string.search_result_tv_services)}",
|
||||||
|
cast.value?.joinToString(separator = ", ") { it.name } ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PeopleSearchResultView(
|
||||||
|
appNavController: NavHostController,
|
||||||
|
result: SearchResultPerson
|
||||||
|
) {
|
||||||
|
val mostKnownFor = result.knownFor.sortedBy { it.popularity }[0]
|
||||||
|
|
||||||
|
SearchResultItemView(
|
||||||
|
appNavController = appNavController,
|
||||||
|
mediaViewType = MediaViewType.PERSON,
|
||||||
|
searchResult = result,
|
||||||
|
posterModel = { TmdbUtils.getFullPersonImagePath(result.profilePath) },
|
||||||
|
backdropModel = { TmdbUtils.getFullBackdropPath(mostKnownFor.backdropPath) },
|
||||||
|
additionalDetails = {
|
||||||
|
listOf(
|
||||||
|
"${mostKnownFor.title} (${TmdbUtils.releaseYearFromData(mostKnownFor.releaseDate)})"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchMovies(
|
||||||
|
query: String,
|
||||||
|
searchResults: MutableState<List<SearchResultMovie>>
|
||||||
|
) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val response = SearchService().searchMovies(query)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
searchResults.value = response.body()?.results ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchTv(
|
||||||
|
query: String,
|
||||||
|
searchResults: MutableState<List<SearchResultTv>>
|
||||||
|
) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val response = SearchService().searchTv(query)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
searchResults.value = response.body()?.results ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchPeople(
|
||||||
|
query: String,
|
||||||
|
searchResults: MutableState<List<SearchResultPerson>>
|
||||||
|
) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val response = SearchService().searchPeople(query)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
searchResults.value = response.body()?.results ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCast(
|
||||||
|
id: Int,
|
||||||
|
detailService: DetailService,
|
||||||
|
cast: MutableState<List<CastMember>?>
|
||||||
|
) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val response = detailService.getCastAndCrew(id)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
cast.value = response.body()?.cast?.let {
|
||||||
|
val end = minOf(2, it.size)
|
||||||
|
it.subList(0, end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -104,32 +104,4 @@ fun MediaTabs(
|
|||||||
HorizontalPager(count = tabs.size, state = pagerState) { page ->
|
HorizontalPager(count = tabs.size, state = pagerState) { page ->
|
||||||
tabs[page].screen(appNavController, mediaViewType, tabs[page])
|
tabs[page].screen(appNavController, mediaViewType, tabs[page])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//@Composable
|
|
||||||
//private fun SearchView(
|
|
||||||
// title: String,
|
|
||||||
// appNavController: NavHostController,
|
|
||||||
// mediaType: MediaViewType,
|
|
||||||
// fab: MutableState<@Composable () -> Unit>,
|
|
||||||
// preferences: AppPreferences = get(AppPreferences::class.java)
|
|
||||||
//) {
|
|
||||||
// val route = "${MainNavItem.SearchView.route}/${mediaType.ordinal}"
|
|
||||||
// if (preferences.showSearchBar) {
|
|
||||||
// SearchBar(
|
|
||||||
// placeholder = title
|
|
||||||
// ) {
|
|
||||||
// appNavController.navigate(route)
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// fab.value = @Composable {
|
|
||||||
// FloatingActionButton(
|
|
||||||
// onClick = {
|
|
||||||
// appNavController.navigate(route)
|
|
||||||
// }
|
|
||||||
// ) {
|
|
||||||
// Icon(Icons.Filled.Search, stringResource(id = R.string.preference_heading_search))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
@@ -7,6 +7,7 @@ enum class MediaViewType {
|
|||||||
MOVIE,
|
MOVIE,
|
||||||
@SerializedName("tv")
|
@SerializedName("tv")
|
||||||
TV,
|
TV,
|
||||||
|
@SerializedName("person")
|
||||||
PERSON,
|
PERSON,
|
||||||
EPISODE,
|
EPISODE,
|
||||||
MIXED;
|
MIXED;
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ fun PersonDetailView(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(color = MaterialTheme.colorScheme.background)
|
.background(color = MaterialTheme.colorScheme.background)
|
||||||
.verticalScroll(state = rememberScrollState())
|
.verticalScroll(state = rememberScrollState())
|
||||||
.padding(bottom = 16.dp),
|
.padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
DetailHeader(
|
DetailHeader(
|
||||||
|
|||||||
@@ -174,4 +174,11 @@ object TmdbUtils {
|
|||||||
return "${AVATAR_BASE}${path}"
|
return "${AVATAR_BASE}${path}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun releaseYearFromData(releaseDate: String): String {
|
||||||
|
if (releaseDate.length >=4) {
|
||||||
|
return releaseDate.split("-").first { it.length == 4 }
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -58,6 +58,14 @@
|
|||||||
<string name="preference_wallpaper_color_heading">Wallpaper Color Picker</string>
|
<string name="preference_wallpaper_color_heading">Wallpaper Color Picker</string>
|
||||||
<string name="preference_no_wallpaper_colors">Unavailable for the current wallpaper or your current settings</string>
|
<string name="preference_no_wallpaper_colors">Unavailable for the current wallpaper or your current settings</string>
|
||||||
<string name="preference_wallpaper_color_default_selected">Wallpaper color already set to default</string>
|
<string name="preference_wallpaper_color_default_selected">Wallpaper color already set to default</string>
|
||||||
|
<string name="preferences_use_wallpaper_colors_title">Use wallpaper colors</string>
|
||||||
|
<string name="preferences_use_wallpaper_colors_subtitle">Use theme colors pulled from your device\'s wallpaper</string>
|
||||||
|
<string name="preference_system_colors_subtitle">Use wallpaper colors chosen by your device</string>
|
||||||
|
<string name="preference_subtitle_search">Search bar view settings</string>
|
||||||
|
<string name="preference_subtitle_design">Design and theming options</string>
|
||||||
|
<string name="preference_subtitle_debug">Secret developer options</string>
|
||||||
|
<string name="preference_heading_dark_mode">Dark mode</string>
|
||||||
|
<string name="preference_subtitle_dark_mode">Light, dark, or auto</string>
|
||||||
|
|
||||||
<!-- video type -->
|
<!-- video type -->
|
||||||
<string name="video_type_clip">Clips</string>
|
<string name="video_type_clip">Clips</string>
|
||||||
@@ -112,12 +120,7 @@
|
|||||||
<!-- onboarding -->
|
<!-- onboarding -->
|
||||||
<string name="get_started">Get started</string>
|
<string name="get_started">Get started</string>
|
||||||
<string name="example_page_desc">This is an example</string>
|
<string name="example_page_desc">This is an example</string>
|
||||||
<string name="preferences_use_wallpaper_colors_title">Use wallpaper colors</string>
|
|
||||||
<string name="preferences_use_wallpaper_colors_subtitle">Use theme colors pulled from your device\'s wallpaper</string>
|
<!-- search results -->
|
||||||
<string name="preference_system_colors_subtitle">Use wallpaper colors chosen by your device</string>
|
<string name="search_result_tv_services">TV Series</string>
|
||||||
<string name="preference_subtitle_search">Search bar view settings</string>
|
|
||||||
<string name="preference_subtitle_design">Design and theming options</string>
|
|
||||||
<string name="preference_subtitle_debug">Secret developer options</string>
|
|
||||||
<string name="preference_heading_dark_mode">Dark mode</string>
|
|
||||||
<string name="preference_subtitle_dark_mode">Light, dark, or auto</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user