mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-19 10:11:13 -05:00
add searching for tv shows and people
This commit is contained in:
@@ -91,6 +91,13 @@ class TmdbClient: KoinComponent {
|
||||
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 request = requestBuilder.build()
|
||||
@@ -121,6 +128,16 @@ class TmdbClient: KoinComponent {
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -26,4 +26,7 @@ interface SearchApi {
|
||||
@GET("search/person")
|
||||
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)
|
||||
}
|
||||
|
||||
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.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
|
||||
|
||||
@@ -21,8 +23,8 @@ class KnownForDeserializer: JsonDeserializer<KnownFor> {
|
||||
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(), KnownFor::class.java)
|
||||
MediaViewType.TV -> Gson().fromJson(obj.toString(), KnownFor::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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,4 @@ class Collection(
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("poster_path") val posterPath: String?,
|
||||
@SerializedName("backdrop_path") val backdropPath: String?
|
||||
)
|
||||
): Searchable
|
||||
@@ -5,4 +5,4 @@ import com.google.gson.annotations.SerializedName
|
||||
class Keyword(
|
||||
@SerializedName("id") val id: Int,
|
||||
@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.owenlejeune.tvtime.ui.screens.main.MediaViewType
|
||||
|
||||
class KnowForTv(
|
||||
class KnownForTv(
|
||||
backdropPath: String?,
|
||||
releaseDate: String,
|
||||
genreIds: List<Int>,
|
||||
@@ -7,4 +7,4 @@ data class ProductionCompany(
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("logo_path") val logoPath: 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
|
||||
|
||||
abstract class SearchResult<T> (
|
||||
class SearchResult<T: Searchable> (
|
||||
@SerializedName("page") val page: Int,
|
||||
@SerializedName("total_pages") val totalPages: Int,
|
||||
@SerializedName("total_results") val totalResults: Int,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
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 SearchResultMedia(
|
||||
var type: SearchResultType,
|
||||
@SerializedName("id") val id: Int,
|
||||
var type: MediaViewType,
|
||||
@SerializedName("overview") val overview: String,
|
||||
@SerializedName("name", alternate = ["title"]) val name: String,
|
||||
@SerializedName("vote_average") val voteAverage: Float,
|
||||
@SerializedName("vote_count") val voteCount: Int,
|
||||
@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_name", alternate = ["original_title"]) val originalName: String,
|
||||
@SerializedName("poster_path") val posterPath: String?,
|
||||
id: Int,
|
||||
name: String,
|
||||
popularity: Float
|
||||
): SortableSearchResult(popularity) {
|
||||
enum class SearchResultType {
|
||||
MOVIE,
|
||||
TV
|
||||
}
|
||||
}
|
||||
): SortableSearchResult(popularity, id, name)
|
||||
@@ -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 SearchResultMovie(
|
||||
id: Int,
|
||||
@@ -18,6 +19,6 @@ class SearchResultMovie(
|
||||
@SerializedName("adult") val isAdult: Boolean,
|
||||
@SerializedName("video") val video: Boolean,
|
||||
): SearchResultMedia(
|
||||
SearchResultType.MOVIE, id, overview, name, voteAverage, voteCount, releaseDate,
|
||||
backdropPath, genreIds, originalLanguage, originalName, posterPath, popularity
|
||||
MediaViewType.MOVIE, overview, voteAverage, voteCount, releaseDate, backdropPath,
|
||||
genreIds, originalLanguage, originalName, posterPath, id, name, popularity
|
||||
)
|
||||
@@ -5,8 +5,8 @@ import com.google.gson.annotations.SerializedName
|
||||
class SearchResultPerson(
|
||||
@SerializedName("profile_path") val profilePath: String,
|
||||
@SerializedName("adult") val isAdult: Boolean,
|
||||
@SerializedName("id") val id: Int,
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("known_for") val knownFor: List<KnownFor>,
|
||||
id: Int,
|
||||
name: String,
|
||||
popularity: Float
|
||||
): SortableSearchResult(popularity)
|
||||
): SortableSearchResult(popularity, id, name)
|
||||
@@ -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 SearchResultTv(
|
||||
id: Int,
|
||||
@@ -17,6 +18,6 @@ class SearchResultTv(
|
||||
releaseDate: String,
|
||||
@SerializedName("origin_country") val originCountry: List<String>,
|
||||
): SearchResultMedia(
|
||||
SearchResultType.TV, id, overview, name, voteAverage, voteCount, releaseDate,
|
||||
backdropPath, genreIds, originalLanguage, originalName, posterPath, popularity
|
||||
MediaViewType.TV, overview, voteAverage, voteCount, releaseDate, backdropPath,
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridScope
|
||||
@@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
fun <T: Any> LazyGridScope.lazyPagingItems(
|
||||
lazyPagingItems: LazyPagingItems<T>,
|
||||
itemContent: @Composable LazyGridItemScope.(value: T?) -> Unit
|
||||
@@ -19,7 +18,6 @@ fun <T: Any> LazyGridScope.lazyPagingItems(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
fun <T: Any> LazyGridScope.listItems(
|
||||
items: List<T>,
|
||||
itemContent: @Composable (value: T) -> Unit
|
||||
@@ -30,11 +28,20 @@ fun <T: Any> LazyGridScope.listItems(
|
||||
}
|
||||
|
||||
fun <T: Any> LazyListScope.listItems(
|
||||
items: List<T>,
|
||||
items: Collection<T>,
|
||||
itemContent: @Composable (value: T) -> Unit
|
||||
) {
|
||||
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
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.clickable
|
||||
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.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavHostController
|
||||
import coil.compose.AsyncImage
|
||||
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.utils.TmdbUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun SearchScreen(
|
||||
@@ -26,15 +51,14 @@ fun SearchScreen(
|
||||
.fillMaxSize()
|
||||
.background(color = MaterialTheme.colorScheme.background)
|
||||
) {
|
||||
var searchValue by remember { mutableStateOf("") }
|
||||
val searchValue = rememberSaveable { mutableStateOf("") }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
SmallTopAppBar(
|
||||
modifier = Modifier,
|
||||
title = {
|
||||
TextField(
|
||||
value = searchValue,
|
||||
onValueChange = { searchValue = it },
|
||||
value = searchValue.value,
|
||||
onValueChange = { searchValue.value = it },
|
||||
placeholder = { Text(text = stringResource(id = R.string.search_placeholder, title)) },
|
||||
colors = TextFieldDefaults.textFieldColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
@@ -42,7 +66,15 @@ fun SearchScreen(
|
||||
unfocusedIndicatorColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
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 = {
|
||||
@@ -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 = "") {
|
||||
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 ->
|
||||
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,
|
||||
@SerializedName("tv")
|
||||
TV,
|
||||
@SerializedName("person")
|
||||
PERSON,
|
||||
EPISODE,
|
||||
MIXED;
|
||||
|
||||
@@ -80,7 +80,7 @@ fun PersonDetailView(
|
||||
modifier = Modifier
|
||||
.background(color = MaterialTheme.colorScheme.background)
|
||||
.verticalScroll(state = rememberScrollState())
|
||||
.padding(bottom = 16.dp),
|
||||
.padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
DetailHeader(
|
||||
|
||||
@@ -174,4 +174,11 @@ object TmdbUtils {
|
||||
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_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="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 -->
|
||||
<string name="video_type_clip">Clips</string>
|
||||
@@ -112,12 +120,7 @@
|
||||
<!-- onboarding -->
|
||||
<string name="get_started">Get started</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>
|
||||
<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>
|
||||
|
||||
<!-- search results -->
|
||||
<string name="search_result_tv_services">TV Series</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user