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

View File

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

View File

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

View File

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

View File

@@ -7,4 +7,4 @@ class Collection(
@SerializedName("name") val name: String,
@SerializedName("poster_path") val posterPath: String?,
@SerializedName("backdrop_path") val backdropPath: String?
)
): Searchable

View File

@@ -5,4 +5,4 @@ import com.google.gson.annotations.SerializedName
class Keyword(
@SerializedName("id") val id: Int,
@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.owenlejeune.tvtime.ui.screens.main.MediaViewType
class KnowForTv(
class KnownForTv(
backdropPath: String?,
releaseDate: String,
genreIds: List<Int>,

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class 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
)

View File

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

View File

@@ -1,6 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.ui.screens.main.MediaViewType
class 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
)

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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