diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt index 53db95d..b82a8b5 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt @@ -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): Boolean { + val includedRoutes = listOf("search") + for (route in includedRoutes) { + if (urlSegments.contains(route)) { + return true + } + } + return false + } } private inner class V4Interceptor: Interceptor { diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/SearchApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/SearchApi.kt index 8e5c2de..5a50c8e 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/SearchApi.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/SearchApi.kt @@ -26,4 +26,7 @@ interface SearchApi { @GET("search/person") suspend fun searchPeople(@Query("query") query: String, @Query("page") page: Int): Response> + @GET("search/multi") + suspend fun searchMulti(@Query("query") query: String, @Query("page") page: Int) + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/SearchService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/SearchService.kt index 43da2e1..6311815 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/SearchService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/SearchService.kt @@ -39,75 +39,4 @@ class SearchService: KoinComponent { return service.searchPeople(query, page) } - suspend fun searchAll(query: String, onResultsUpdated: (List) -> Unit) { - val results = TreeSet {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) - } - } - } - } - } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/deserializer/KnownForDeserializer.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/deserializer/KnownForDeserializer.kt index bd00477..b34bb30 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/deserializer/KnownForDeserializer.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/deserializer/KnownForDeserializer.kt @@ -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 { 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") } } diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/Collection.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/Collection.kt index 576dfb6..2dfa165 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/Collection.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/Collection.kt @@ -7,4 +7,4 @@ class Collection( @SerializedName("name") val name: String, @SerializedName("poster_path") val posterPath: String?, @SerializedName("backdrop_path") val backdropPath: String? -) \ No newline at end of file +): Searchable \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/Keyword.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/Keyword.kt index 3d923d0..3eb2e31 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/Keyword.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/Keyword.kt @@ -5,4 +5,4 @@ import com.google.gson.annotations.SerializedName class Keyword( @SerializedName("id") val id: Int, @SerializedName("name") val name: String -) +): Searchable diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/KnowForTv.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/KnownForTv.kt similarity index 97% rename from app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/KnowForTv.kt rename to app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/KnownForTv.kt index ce4762d..6f5677a 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/KnowForTv.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/KnownForTv.kt @@ -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, diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/ProductionCompany.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/ProductionCompany.kt index 47dacec..489a184 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/ProductionCompany.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/ProductionCompany.kt @@ -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 diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResult.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResult.kt index c90fde7..8abcb25 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResult.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResult.kt @@ -2,7 +2,7 @@ package com.owenlejeune.tvtime.api.tmdb.api.v3.model import com.google.gson.annotations.SerializedName -abstract class SearchResult ( +class SearchResult ( @SerializedName("page") val page: Int, @SerializedName("total_pages") val totalPages: Int, @SerializedName("total_results") val totalResults: Int, diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultMedia.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultMedia.kt index 2d11c63..186a1ca 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultMedia.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultMedia.kt @@ -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 - } -} \ No newline at end of file +): SortableSearchResult(popularity, id, name) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultMovie.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultMovie.kt index a342e83..50503bf 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultMovie.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultMovie.kt @@ -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 ) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultPerson.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultPerson.kt index 7412fb8..292215d 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultPerson.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultPerson.kt @@ -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, + id: Int, + name: String, popularity: Float -): SortableSearchResult(popularity) \ No newline at end of file +): SortableSearchResult(popularity, id, name) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultTv.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultTv.kt index a0bacb4..c543033 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultTv.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SearchResultTv.kt @@ -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, ): 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 ) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/Searchable.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/Searchable.kt new file mode 100644 index 0000000..99e64ff --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/Searchable.kt @@ -0,0 +1,4 @@ +package com.owenlejeune.tvtime.api.tmdb.api.v3.model + + +interface Searchable \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SortableSearchResult.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SortableSearchResult.kt index a50f6ad..2d4906c 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SortableSearchResult.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/SortableSearchResult.kt @@ -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 -) \ No newline at end of file + @SerializedName("popularity") val popularity: Float, + @SerializedName("id") val id: Int, + @SerializedName("name", alternate = ["title"]) val name: String +): Searchable \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt b/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt index 21b7a09..66a2eca 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt @@ -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 LazyGridScope.lazyPagingItems( lazyPagingItems: LazyPagingItems, itemContent: @Composable LazyGridItemScope.(value: T?) -> Unit @@ -19,7 +18,6 @@ fun LazyGridScope.lazyPagingItems( } } -@OptIn(ExperimentalFoundationApi::class) fun LazyGridScope.listItems( items: List, itemContent: @Composable (value: T) -> Unit @@ -30,11 +28,20 @@ fun LazyGridScope.listItems( } fun LazyListScope.listItems( - items: List, + items: Collection, itemContent: @Composable (value: T) -> Unit ) { items(items.size) { index -> - itemContent(items[index]) + itemContent(items.elementAt(index)) + } +} + +fun LazyListScope.lazyPagingItems( + lazyPagingItems: LazyPagingItems, + itemContent: @Composable LazyItemScope.(value: T?) -> Unit +) { + items(lazyPagingItems.itemCount) { index -> + itemContent(lazyPagingItems[index]) } } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SearchScreen.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SearchScreen.kt index b9ec55d..b6e4e6a 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SearchScreen.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SearchScreen.kt @@ -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> -> + 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> -> + 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> -> + searchPeople(searchValue.value, searchResults) + } + ) { person -> + PeopleSearchResultView(result = person, appNavController = appNavController) + } + } + MediaViewType.MIXED -> { + SearchResultListView( + showLoadingAnimation = showLoadingAnimation, + currentQuery = searchValue, + searchExecutor = { searchResults: MutableState> -> + + }, + resultsSortingStrategy = { o1, o2 -> o2.popularity.compareTo(o1.popularity) } + ) { item -> + + } + } + else -> {} + } + } LaunchedEffect(key1 = "") { focusRequester.requestFocus() } } +} + +@Composable +private fun SearchResultListView( + showLoadingAnimation: MutableState, + currentQuery: MutableState, + searchExecutor: (MutableState>) -> Unit, + resultsSortingStrategy: Comparator? = null, + viewRenderer: @Composable (T) -> Unit +) { + val searchResults = remember { mutableStateOf(emptyList()) } + + 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 SearchResultItemView( + appNavController: NavHostController, + mediaViewType: MediaViewType, + searchResult: T, + posterModel: (T) -> Any?, + backdropModel: (T) -> Any?, + additionalDetails: (T) -> List = { 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?>(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?>(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> +) { + 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> +) { + 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> +) { + 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?> +) { + 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) + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaTab.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaTab.kt index 2b1da9d..7bf150e 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaTab.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaTab.kt @@ -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)) -// } -// } -// } -//} \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaViewType.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaViewType.kt index dbbd23a..ce6d226 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaViewType.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/MediaViewType.kt @@ -7,6 +7,7 @@ enum class MediaViewType { MOVIE, @SerializedName("tv") TV, + @SerializedName("person") PERSON, EPISODE, MIXED; diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/PeopleDetailView.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/PeopleDetailView.kt index 50e5313..007f4e1 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/PeopleDetailView.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/main/PeopleDetailView.kt @@ -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( diff --git a/app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt b/app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt index cc90c9a..79f3f21 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/utils/TmdbUtils.kt @@ -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 "" + } + } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 26534d9..74fb3f3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,6 +58,14 @@ Wallpaper Color Picker Unavailable for the current wallpaper or your current settings Wallpaper color already set to default + Use wallpaper colors + Use theme colors pulled from your device\'s wallpaper + Use wallpaper colors chosen by your device + Search bar view settings + Design and theming options + Secret developer options + Dark mode + Light, dark, or auto Clips @@ -112,12 +120,7 @@ Get started This is an example - Use wallpaper colors - Use theme colors pulled from your device\'s wallpaper - Use wallpaper colors chosen by your device - Search bar view settings - Design and theming options - Secret developer options - Dark mode - Light, dark, or auto + + + TV Series \ No newline at end of file