add searching for tv shows and people

This commit is contained in:
Owen LeJeune
2022-09-07 14:22:36 -04:00
parent 096fcdaea4
commit b6ee061f01
22 changed files with 439 additions and 146 deletions

View File

@@ -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 {

View File

@@ -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)
} }

View File

@@ -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)
}
}
}
}
} }

View File

@@ -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")
} }
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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>,

View File

@@ -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

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -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
) )

View File

@@ -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)

View File

@@ -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
) )

View File

@@ -0,0 +1,4 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
interface Searchable

View File

@@ -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

View File

@@ -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])
} }
} }

View File

@@ -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)
}
}
}
}
}

View File

@@ -105,31 +105,3 @@ fun MediaTabs(
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))
// }
// }
// }
//}

View File

@@ -7,6 +7,7 @@ enum class MediaViewType {
MOVIE, MOVIE,
@SerializedName("tv") @SerializedName("tv")
TV, TV,
@SerializedName("person")
PERSON, PERSON,
EPISODE, EPISODE,
MIXED; MIXED;

View File

@@ -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(

View File

@@ -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 ""
}
} }

View File

@@ -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>