From ef11a88a90336edf65660bdc6523ba801ca15877 Mon Sep 17 00:00:00 2001 From: Owen LeJeune Date: Sat, 24 Jun 2023 00:01:47 -0400 Subject: [PATCH] add trending content to home pages --- .../tvtime/api/tmdb/api/BasePagingSource.kt | 2 +- .../tvtime/api/tmdb/api/v3/MoviesApi.kt | 4 + .../tvtime/api/tmdb/api/v3/MoviesService.kt | 5 + .../tvtime/api/tmdb/api/v3/PeopleApi.kt | 6 + .../tvtime/api/tmdb/api/v3/PeopleService.kt | 8 ++ .../tvtime/api/tmdb/api/v3/TvApi.kt | 4 + .../tvtime/api/tmdb/api/v3/TvService.kt | 5 + .../tmdb/api/v3/model/SearchResultMedia.kt | 4 +- .../tmdb/api/v3/model/SearchResultPerson.kt | 4 +- .../tmdb/api/v3/model/SortableSearchResult.kt | 7 +- .../tvtime/api/tmdb/api/v3/model/TmdbItem.kt | 2 +- .../tvtime/ui/components/Posters.kt | 2 + .../tvtime/ui/components/Widgets.kt | 13 +- .../tvtime/ui/screens/AccountScreen.kt | 2 +- .../tvtime/ui/screens/KeywordResultsScreen.kt | 2 +- .../tvtime/ui/screens/PeopleDetailScreen.kt | 26 ++-- .../tvtime/ui/screens/SearchScreen.kt | 12 +- .../tabs}/AccountTabNavItem.kt | 4 +- .../tvtime/ui/screens/tabs/MediaTab.kt | 61 ++++++++- .../tabs}/MediaTabNavItem.kt | 27 ++-- .../tvtime/ui/screens/tabs/PeopleTab.kt | 103 +++++++++++++-- .../ui/screens/tabs/PeopleTabNavItem.kt | 34 +++++ .../tvtime/ui/viewmodel/AccountViewModel.kt | 2 +- .../tvtime/ui/viewmodel/MainViewModel.kt | 119 ++++++++++++------ .../tvtime/utils/types/TimeWindow.kt | 10 ++ app/src/main/res/values/strings.xml | 4 + 26 files changed, 375 insertions(+), 97 deletions(-) rename app/src/main/java/com/owenlejeune/tvtime/ui/{navigation => screens/tabs}/AccountTabNavItem.kt (98%) rename app/src/main/java/com/owenlejeune/tvtime/ui/{navigation => screens/tabs}/MediaTabNavItem.kt (79%) create mode 100644 app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/PeopleTabNavItem.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/utils/types/TimeWindow.kt diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/BasePagingSource.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/BasePagingSource.kt index f63220e..c193722 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/BasePagingSource.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/BasePagingSource.kt @@ -53,7 +53,7 @@ class BasePagingSource( nextKey = if (results.isEmpty()) { null } else { page + 1} ) } else { - Toast.makeText(context, context.getString(R.string.no_result_found), Toast.LENGTH_SHORT).show() +// Toast.makeText(context, context.getString(R.string.no_result_found), Toast.LENGTH_SHORT).show() LoadResult.Invalid() } } catch (e: Exception) { diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/MoviesApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/MoviesApi.kt index 33026ab..bac69a3 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/MoviesApi.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/MoviesApi.kt @@ -1,6 +1,7 @@ package com.owenlejeune.tvtime.api.tmdb.api.v3 import com.owenlejeune.tvtime.api.tmdb.api.v3.model.* +import com.owenlejeune.tvtime.utils.types.TimeWindow import retrofit2.Response import retrofit2.http.* @@ -67,4 +68,7 @@ interface MoviesApi { @GET("discover/movie") suspend fun discover(@Query("with_keywords") keywords: String? = null, @Query("page") page: Int): Response> + @GET("trending/movie/{time_window}") + suspend fun trending(@Path("time_window") timeWindow: String, @Query("page") page: Int): Response> + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/MoviesService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/MoviesService.kt index f359b33..6cacc8e 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/MoviesService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/MoviesService.kt @@ -28,6 +28,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviders import com.owenlejeune.tvtime.utils.SessionManager +import com.owenlejeune.tvtime.utils.types.TimeWindow import kotlinx.coroutines.flow.Flow import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -137,6 +138,10 @@ class MoviesService: KoinComponent, DetailService, HomePageService { } } + suspend fun getTrending(timeWindow: TimeWindow, page: Int): Response> { + return movieService.trending(timeWindow.name.lowercase(), page) + } + override suspend fun discover(keywords: String?, page: Int): Response> { return movieService.discover(keywords, page) } diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/PeopleApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/PeopleApi.kt index df51d36..5b46e60 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/PeopleApi.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/PeopleApi.kt @@ -5,6 +5,9 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ExternalIds import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePagePeopleResponse import com.owenlejeune.tvtime.api.tmdb.api.v3.model.PersonCreditsResponse import com.owenlejeune.tvtime.api.tmdb.api.v3.model.PersonImageCollection +import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResult +import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultPerson +import com.owenlejeune.tvtime.utils.types.TimeWindow import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path @@ -36,4 +39,7 @@ interface PeopleApi { @GET("person/{id}/external_ids") suspend fun getExternalIds(@Path("id") id: Int): Response + @GET("trending/person/{time_window}") + suspend fun trending(@Path("time_window") timeWindow: String, @Query("page") page: Int): Response> + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/PeopleService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/PeopleService.kt index 9f78eb7..0ca30e9 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/PeopleService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/PeopleService.kt @@ -12,6 +12,10 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePagePeopleResponse import com.owenlejeune.tvtime.api.tmdb.api.v3.model.PersonCreditsResponse import com.owenlejeune.tvtime.api.tmdb.api.v3.model.PersonImage import com.owenlejeune.tvtime.api.tmdb.api.v3.model.PersonImageCollection +import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResult +import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultMedia +import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultPerson +import com.owenlejeune.tvtime.utils.types.TimeWindow import okhttp3.internal.notify import org.koin.core.component.KoinComponent import retrofit2.Response @@ -90,4 +94,8 @@ class PeopleService: KoinComponent { return service.getPopular(page) } + suspend fun getTrending(timeWindow: TimeWindow, page: Int): Response> { + return service.trending(timeWindow.name.lowercase(), page) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/TvApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/TvApi.kt index 504755d..b9279df 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/TvApi.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/TvApi.kt @@ -1,6 +1,7 @@ package com.owenlejeune.tvtime.api.tmdb.api.v3 import com.owenlejeune.tvtime.api.tmdb.api.v3.model.* +import com.owenlejeune.tvtime.utils.types.TimeWindow import retrofit2.Response import retrofit2.http.* @@ -69,4 +70,7 @@ interface TvApi { @GET("discover/tv") suspend fun discover(@Query("page") page: Int, @Query("with_keywords") keywords: String? = null): Response> + + @GET("trending/tv/{time_window}") + suspend fun trending(@Path("time_window") timeWindow: String, @Query("page") page: Int): Response> } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/TvService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/TvService.kt index a9db23a..76f9425 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/TvService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/TvService.kt @@ -32,6 +32,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.VideoResponse import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviderResponse import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviders import com.owenlejeune.tvtime.utils.SessionManager +import com.owenlejeune.tvtime.utils.types.TimeWindow import kotlinx.coroutines.flow.Flow import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -140,6 +141,10 @@ class TvService: KoinComponent, DetailService, HomePageService { } } + suspend fun getTrending(timeWindow: TimeWindow, page: Int): Response> { + return service.trending(timeWindow.name.lowercase(), page) + } + override suspend fun discover(keywords: String?, page: Int): Response> { return service.discover(page, keywords) } 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 19e33ba..0834751 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 @@ -12,9 +12,9 @@ abstract class SearchResultMedia( @SerializedName("genre_ids") val genreIds: List, @SerializedName("original_language") val originalLanguage: String, @SerializedName("original_name", alternate = ["original_title"]) val originalName: String, - @SerializedName("poster_path") val posterPath: String?, + posterPath: String?, type: MediaViewType, id: Int, name: String, popularity: Float -): SortableSearchResult(type, popularity, id, name) \ No newline at end of file +): SortableSearchResult(type, popularity, id, name, posterPath) \ 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 933cb8c..279f299 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 @@ -4,10 +4,10 @@ import com.google.gson.annotations.SerializedName import com.owenlejeune.tvtime.utils.types.MediaViewType class SearchResultPerson( - @SerializedName("profile_path") val profilePath: String, @SerializedName("adult") val isAdult: Boolean, @SerializedName("known_for") val knownFor: List, + profilePath: String, id: Int, name: String, popularity: Float -): SortableSearchResult(MediaViewType.PERSON, popularity, id, name) \ No newline at end of file +): SortableSearchResult(MediaViewType.PERSON, popularity, id, name, profilePath) \ 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 acb063b..e2f6f99 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 @@ -6,6 +6,7 @@ import com.owenlejeune.tvtime.utils.types.MediaViewType abstract class SortableSearchResult( @SerializedName("media_type") val mediaType: MediaViewType, @SerializedName("popularity") val popularity: Float, - @SerializedName("id") val id: Int, - @SerializedName("name", alternate = ["title"]) val name: String -): Searchable \ No newline at end of file + id: Int, + name: String, + posterPath: String? +): TmdbItem(id, posterPath, name), Searchable \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/TmdbItem.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/TmdbItem.kt index bb5ea12..de7dfb6 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/TmdbItem.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/v3/model/TmdbItem.kt @@ -4,6 +4,6 @@ import com.google.gson.annotations.SerializedName abstract class TmdbItem( @SerializedName("id") val id: Int, - @SerializedName("poster_path") val posterPath: String?, + @SerializedName("poster_path", alternate = ["profile_path"]) val posterPath: String?, @SerializedName("name", alternate = ["title"]) val title: String ) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt index 5dbe198..1f0b361 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Posters.kt @@ -52,11 +52,13 @@ private val POSTER_HEIGHT = 180.dp @Composable fun PagingPosterGrid( + modifier: Modifier = Modifier, lazyPagingItems: LazyPagingItems?, onClick: (id: Int) -> Unit = {} ) { lazyPagingItems?.let { LazyVerticalGrid( + modifier = modifier, columns = GridCells.Adaptive(minSize = POSTER_WIDTH), contentPadding = PaddingValues(8.dp), horizontalArrangement = Arrangement.SpaceBetween diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt index 62f120f..28ed9fd 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt @@ -1117,15 +1117,18 @@ fun SelectableTextChip( selected: Boolean, onSelected: () -> Unit, text: String, + modifier: Modifier = Modifier, selectedColor: Color = MaterialTheme.colorScheme.secondary, unselectedColor: Color = MaterialTheme.colorScheme.surfaceVariant ) { Box( - modifier = Modifier - .clip(RoundedCornerShape(percent = 25)) - .border(width = 1.dp, color = selectedColor, shape = RoundedCornerShape(percent = 25)) - .background(color = if(selected) selectedColor else unselectedColor) - .clickable(onClick = onSelected) + modifier = modifier.then( + Modifier + .clip(RoundedCornerShape(percent = 25)) + .border(width = 1.dp, color = selectedColor, shape = RoundedCornerShape(percent = 25)) + .background(color = if(selected) selectedColor else unselectedColor) + .clickable(onClick = onSelected) + ) ) { Text( text = text, diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/AccountScreen.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/AccountScreen.kt index eab4931..ce6778f 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/AccountScreen.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/AccountScreen.kt @@ -40,8 +40,8 @@ import com.owenlejeune.tvtime.ui.components.AccountIcon import com.owenlejeune.tvtime.ui.components.MediaResultCard import com.owenlejeune.tvtime.ui.components.PagingPosterGrid import com.owenlejeune.tvtime.ui.components.ScrollableTabs -import com.owenlejeune.tvtime.ui.navigation.AccountTabNavItem import com.owenlejeune.tvtime.ui.navigation.AppNavItem +import com.owenlejeune.tvtime.ui.screens.tabs.AccountTabNavItem import com.owenlejeune.tvtime.ui.viewmodel.AccountViewModel import com.owenlejeune.tvtime.utils.SessionManager import com.owenlejeune.tvtime.utils.TmdbUtils diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/KeywordResultsScreen.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/KeywordResultsScreen.kt index 06798d7..9c9c023 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/KeywordResultsScreen.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/KeywordResultsScreen.kt @@ -79,7 +79,7 @@ fun KeywordResultsScreen( id = result.id, backdropPath = TmdbUtils.getFullBackdropPath(result.backdropPath), posterPath = TmdbUtils.getFullPosterPath(result.posterPath), - title = result.name, + title = result.title, additionalDetails = listOf(result.overview) ) } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/PeopleDetailScreen.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/PeopleDetailScreen.kt index 50bf802..ef4c8f4 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/PeopleDetailScreen.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/PeopleDetailScreen.kt @@ -204,17 +204,19 @@ fun PersonDetailScreen( @Composable private fun BiographyCard(person: DetailPerson?) { - ExpandableContentCard { isExpanded -> - Text( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(top = 12.dp, start = 16.dp, end = 16.dp), - text = person?.biography ?: "", - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium, - maxLines = if (isExpanded) Int.MAX_VALUE else 3, - overflow = TextOverflow.Ellipsis - ) + if (person != null && person.biography.isNotEmpty()) { + ExpandableContentCard { isExpanded -> + Text( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 12.dp, start = 16.dp, end = 16.dp), + text = person.biography, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + maxLines = if (isExpanded) Int.MAX_VALUE else 3, + overflow = TextOverflow.Ellipsis + ) + } } } \ No newline at end of file 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 1f2daf6..925a57b 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 @@ -112,22 +112,22 @@ fun SearchScreen( SelectableTextChip( selected = viewType.value == MediaViewType.MOVIE, onSelected = { viewType.value = MediaViewType.MOVIE }, - text = "Movies" + text = stringResource(id = R.string.nav_movies_title) ) SelectableTextChip( selected = viewType.value == MediaViewType.TV, onSelected = { viewType.value = MediaViewType.TV }, - text = "TV" + text = stringResource(id = R.string.nav_tv_title) ) SelectableTextChip( selected = viewType.value == MediaViewType.PERSON, onSelected = { viewType.value = MediaViewType.PERSON }, - text = "People" + text = stringResource(id = R.string.nav_people_title) ) SelectableTextChip( selected = viewType.value == MediaViewType.MIXED, onSelected = { viewType.value = MediaViewType.MIXED }, - text = "Multi" + text = stringResource(id = R.string.search_multi_title) ) } @@ -374,7 +374,7 @@ private fun SearchResultItemView( id = searchResult.id, backdropPath = backdropModel(searchResult), posterPath = posterModel(searchResult), - title = searchResult.name, + title = searchResult.title, additionalDetails = additionalDetails(searchResult) ) } @@ -453,7 +453,7 @@ private fun PeopleSearchResultView( appNavController = appNavController, mediaViewType = MediaViewType.PERSON, searchResult = result, - posterModel = { TmdbUtils.getFullPersonImagePath(result.profilePath) }, + posterModel = { TmdbUtils.getFullPersonImagePath(result.posterPath) }, backdropModel = { TmdbUtils.getFullBackdropPath(mostKnownFor?.backdropPath) }, additionalDetails = { additional } ) diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/AccountTabNavItem.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/AccountTabNavItem.kt similarity index 98% rename from app/src/main/java/com/owenlejeune/tvtime/ui/navigation/AccountTabNavItem.kt rename to app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/AccountTabNavItem.kt index a343436..2d8b3c8 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/AccountTabNavItem.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/AccountTabNavItem.kt @@ -1,4 +1,4 @@ -package com.owenlejeune.tvtime.ui.navigation +package com.owenlejeune.tvtime.ui.screens.tabs import androidx.compose.runtime.Composable import androidx.navigation.NavHostController @@ -84,7 +84,7 @@ sealed class AccountTabNavItem( R.string.no_favorite_movies, MediaViewType.MOVIE, screenContent, - AccountTabType.FAVORITE, + AccountTabType.FAVORITE, FavoriteMovie::class, 3 ) diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/MediaTab.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/MediaTab.kt index fe2056c..c2f5a9a 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/MediaTab.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/MediaTab.kt @@ -1,8 +1,16 @@ package com.owenlejeune.tvtime.ui.screens.tabs +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController @@ -13,13 +21,14 @@ import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.rememberPagerState import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.ui.components.PagingPosterGrid +import com.owenlejeune.tvtime.ui.components.ScrollableTabs import com.owenlejeune.tvtime.ui.components.SearchView -import com.owenlejeune.tvtime.ui.components.Tabs +import com.owenlejeune.tvtime.ui.components.SelectableTextChip import com.owenlejeune.tvtime.ui.navigation.AppNavItem -import com.owenlejeune.tvtime.ui.navigation.MediaTabNavItem import com.owenlejeune.tvtime.ui.viewmodel.HomeScreenViewModel import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel import com.owenlejeune.tvtime.utils.types.MediaViewType +import com.owenlejeune.tvtime.utils.types.TimeWindow @OptIn(ExperimentalPagerApi::class) @Composable @@ -44,7 +53,7 @@ fun MediaTab( val tabs = MediaTabNavItem.itemsForType(type = mediaType) val pagerState = rememberPagerState() - Tabs(tabs = tabs, pagerState = pagerState) + ScrollableTabs(tabs = tabs, pagerState = pagerState) MediaTabs( tabs = tabs, pagerState = pagerState, @@ -73,6 +82,52 @@ fun MediaTabContent( ) } +@Composable +fun MediaTabTrendingContent( + appNavController: NavHostController, + mediaType: MediaViewType, + mediaTabItem: MediaTabNavItem +) { + val viewModel = viewModel() + + val timeWindow = remember { mutableStateOf(TimeWindow.DAY) } + val flow = remember { mutableStateOf(viewModel.produceTrendingFor(mediaType, timeWindow.value)) } + + LaunchedEffect(timeWindow.value) { + flow.value = viewModel.produceTrendingFor(mediaType, timeWindow.value) + } + + val mediaListItems = flow.value.collectAsLazyPagingItems() + + Column { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(all = 12.dp) + ) { + SelectableTextChip( + selected = timeWindow.value == TimeWindow.DAY, + onSelected = { timeWindow.value = TimeWindow.DAY }, + text = stringResource(id = R.string.time_window_day), + modifier = Modifier.weight(1f) + ) + SelectableTextChip( + selected = timeWindow.value == TimeWindow.WEEK, + onSelected = { timeWindow.value = TimeWindow.WEEK }, + text = stringResource(id = R.string.time_window_week), + modifier = Modifier.weight(1f) + ) + } + PagingPosterGrid( + lazyPagingItems = mediaListItems, + onClick = { id -> + appNavController.navigate( + AppNavItem.DetailView.withArgs(mediaType, id) + ) + } + ) + } +} + @OptIn(ExperimentalPagerApi::class) @Composable fun MediaTabs( diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MediaTabNavItem.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/MediaTabNavItem.kt similarity index 79% rename from app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MediaTabNavItem.kt rename to app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/MediaTabNavItem.kt index 495fd43..1b74e36 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/MediaTabNavItem.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/MediaTabNavItem.kt @@ -1,4 +1,4 @@ -package com.owenlejeune.tvtime.ui.navigation +package com.owenlejeune.tvtime.ui.screens.tabs import androidx.compose.runtime.Composable import androidx.navigation.NavHostController @@ -6,6 +6,7 @@ import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.api.tmdb.api.v3.HomePageService import com.owenlejeune.tvtime.api.tmdb.api.v3.model.HomePageResponse import com.owenlejeune.tvtime.ui.screens.tabs.MediaTabContent +import com.owenlejeune.tvtime.ui.screens.tabs.MediaTabTrendingContent import com.owenlejeune.tvtime.utils.ResourceUtils import com.owenlejeune.tvtime.utils.types.MediaViewType import com.owenlejeune.tvtime.utils.types.TabNavItem @@ -27,12 +28,13 @@ sealed class MediaTabNavItem( POPULAR, NOW_PLAYING, UPCOMING, - TOP_RATED + TOP_RATED, + TRENDING } companion object { - val MovieItems = listOf(NowPlaying, Popular, Upcoming, TopRated) - val TvItems = listOf(OnTheAir, Popular, AiringToday, TopRated) + private val MovieItems = listOf(NowPlaying, Popular, Trending, Upcoming, TopRated) + private val TvItems = listOf(OnTheAir, Popular, Trending, AiringToday, TopRated) fun itemsForType(type: MediaViewType): List { return when (type) { @@ -41,12 +43,6 @@ sealed class MediaTabNavItem( else -> throw ViewableMediaTypeException(type) } } - - private val Items = listOf(NowPlaying, Popular, TopRated, Upcoming, AiringToday, OnTheAir) - - fun getByRoute(route: String?): MediaTabNavItem? { - return Items.firstOrNull { it.route == route } - } } object Popular: MediaTabNavItem( @@ -85,12 +81,23 @@ sealed class MediaTabNavItem( screen = screenContent, type = Type.UPCOMING ) + + object Trending: MediaTabNavItem( + stringRes = R.string.nav_trending_title, + route = "trending_route", + screen = trendingScreenContent, + type = Type.TRENDING + ) } private val screenContent: MediaNavComposableFun = { appNavController, mediaViewType, mediaTabItem -> MediaTabContent(appNavController = appNavController, mediaType = mediaViewType, mediaTabItem = mediaTabItem) } +private val trendingScreenContent: MediaNavComposableFun = { appNavController, mediaViewType, mediaTabItem -> + MediaTabTrendingContent(appNavController = appNavController, mediaType = mediaViewType, mediaTabItem = mediaTabItem) +} + typealias MediaNavComposableFun = @Composable (NavHostController, MediaViewType, MediaTabNavItem) -> Unit typealias MediaFetchFun = suspend (service: HomePageService, page: Int) -> Response \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/PeopleTab.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/PeopleTab.kt index 3f9e815..0b4b2e9 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/PeopleTab.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/PeopleTab.kt @@ -1,19 +1,38 @@ package com.owenlejeune.tvtime.ui.screens.tabs +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController import androidx.paging.compose.collectAsLazyPagingItems +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.PagerState +import com.google.accompanist.pager.rememberPagerState import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.ui.components.PagingPeoplePosterGrid +import com.owenlejeune.tvtime.ui.components.PagingPosterGrid import com.owenlejeune.tvtime.ui.components.SearchView +import com.owenlejeune.tvtime.ui.components.SelectableTextChip +import com.owenlejeune.tvtime.ui.components.Tabs import com.owenlejeune.tvtime.ui.navigation.AppNavItem import com.owenlejeune.tvtime.ui.viewmodel.HomeScreenViewModel import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel import com.owenlejeune.tvtime.utils.types.MediaViewType +import com.owenlejeune.tvtime.utils.types.TimeWindow +@OptIn(ExperimentalPagerApi::class) @Composable fun PeopleTab( appNavController: NavHostController, @@ -28,17 +47,69 @@ fun PeopleTab( mediaType = MediaViewType.PERSON ) - val mainViewModel = viewModel() - val peopleList = mainViewModel.popularPeople.collectAsLazyPagingItems() + val tabs = PeopleTabNavItem.Items + val pagerState = rememberPagerState() + Tabs(tabs = tabs, pagerState = pagerState) + PeopleTabs( + tabs = tabs, + pagerState = pagerState, + appNavController = appNavController + ) + } +} - PagingPeoplePosterGrid( - lazyPagingItems = peopleList, - header = { -// Text( -// text = stringResource(R.string.popular_today_header), -// modifier = Modifier.padding(start = 8.dp) -// ) - }, +@Composable +fun PopularPeopleContent( + appNavController: NavController +) { + val mainViewModel = viewModel() + val peopleList = mainViewModel.popularPeople.collectAsLazyPagingItems() + + PagingPeoplePosterGrid( + lazyPagingItems = peopleList, + onClick = { id -> + appNavController.navigate( + AppNavItem.DetailView.withArgs(MediaViewType.PERSON, id) + ) + } + ) +} + +@Composable +fun TrendingPeopleContent( + appNavController: NavController +) { + val viewModel = viewModel() + + val timeWindow = remember { mutableStateOf(TimeWindow.DAY) } + val flow = remember { mutableStateOf(viewModel.produceTrendingFor(MediaViewType.PERSON, timeWindow.value)) } + + LaunchedEffect(timeWindow.value) { + flow.value = viewModel.produceTrendingFor(MediaViewType.PERSON, timeWindow.value) + } + + val mediaListItems = flow.value.collectAsLazyPagingItems() + + Column { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(all = 12.dp) + ) { + SelectableTextChip( + selected = timeWindow.value == TimeWindow.DAY, + onSelected = { timeWindow.value = TimeWindow.DAY }, + text = stringResource(id = R.string.time_window_day), + modifier = Modifier.weight(1f) + ) + SelectableTextChip( + selected = timeWindow.value == TimeWindow.WEEK, + onSelected = { timeWindow.value = TimeWindow.WEEK }, + text = stringResource(id = R.string.time_window_week), + modifier = Modifier.weight(1f) + ) + } + PagingPosterGrid( + lazyPagingItems = mediaListItems, onClick = { id -> appNavController.navigate( AppNavItem.DetailView.withArgs(MediaViewType.PERSON, id) @@ -46,4 +117,16 @@ fun PeopleTab( } ) } +} + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun PeopleTabs( + tabs: List, + pagerState: PagerState, + appNavController: NavHostController = rememberNavController() +) { + HorizontalPager(count = tabs.size, state = pagerState) { page -> + tabs[page].screen(appNavController) + } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/PeopleTabNavItem.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/PeopleTabNavItem.kt new file mode 100644 index 0000000..29ea201 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/PeopleTabNavItem.kt @@ -0,0 +1,34 @@ +package com.owenlejeune.tvtime.ui.screens.tabs + +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import com.owenlejeune.tvtime.R +import com.owenlejeune.tvtime.utils.ResourceUtils +import com.owenlejeune.tvtime.utils.types.TabNavItem +import org.koin.core.component.inject + +sealed class PeopleTabNavItem( + stringRes: Int, + route: String, + val screen: @Composable (NavController) -> Unit +): TabNavItem(route) { + private val resourceUtils: ResourceUtils by inject() + + override val name: String = resourceUtils.getString(stringRes) + + companion object { + val Items by lazy { listOf(Popular, Trending) } + } + + object Popular: PeopleTabNavItem( + stringRes = R.string.popular_today_header, + route = "people_popular_route", + screen = { PopularPeopleContent(appNavController = it) } + ) + + object Trending: PeopleTabNavItem( + stringRes = R.string.nav_trending_title, + route = "people_trending_route", + screen = { TrendingPeopleContent(appNavController = it) } + ) +} diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/AccountViewModel.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/AccountViewModel.kt index 9857611..8762f7d 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/AccountViewModel.kt @@ -11,7 +11,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v4.ListV4Service import com.owenlejeune.tvtime.api.tmdb.api.v4.model.DeleteListItemsBody import com.owenlejeune.tvtime.api.tmdb.api.v4.model.DeleteListItemsItem import com.owenlejeune.tvtime.api.tmdb.api.v4.model.ListUpdateBody -import com.owenlejeune.tvtime.ui.navigation.AccountTabNavItem +import com.owenlejeune.tvtime.ui.screens.tabs.AccountTabNavItem import com.owenlejeune.tvtime.utils.SessionManager import com.owenlejeune.tvtime.utils.types.MediaViewType import com.owenlejeune.tvtime.utils.types.ViewableMediaTypeException diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/MainViewModel.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/MainViewModel.kt index 7a23aee..b92f81b 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/MainViewModel.kt @@ -19,8 +19,9 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultMedia import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviders -import com.owenlejeune.tvtime.ui.navigation.MediaTabNavItem +import com.owenlejeune.tvtime.ui.screens.tabs.MediaTabNavItem import com.owenlejeune.tvtime.utils.types.MediaViewType +import com.owenlejeune.tvtime.utils.types.TimeWindow import com.owenlejeune.tvtime.utils.types.ViewableMediaTypeException import kotlinx.coroutines.flow.Flow import org.koin.core.component.KoinComponent @@ -46,22 +47,30 @@ class MainViewModel: ViewModel(), KoinComponent { val movieAccountStates = movieService.accountStates val movieKeywordResults = movieService.keywordResults - val popularMovies = createPagingFlow( - fetcher = { p -> movieService.getPopular(p) }, - processor = { it.results } - ) - val topRatedMovies = createPagingFlow( - fetcher = { p -> movieService.getTopRated(p) }, - processor = { it.results } - ) - val nowPlayingMovies = createPagingFlow( - fetcher = { p -> movieService.getNowPlaying(p) }, - processor = { it.results } - ) - val upcomingMovies = createPagingFlow( - fetcher = { p -> movieService.getUpcoming(p) }, - processor = { it.results } - ) + val popularMovies by lazy { + createPagingFlow( + fetcher = { p -> movieService.getPopular(p) }, + processor = { it.results } + ) + } + val topRatedMovies by lazy { + createPagingFlow( + fetcher = { p -> movieService.getTopRated(p) }, + processor = { it.results } + ) + } + val nowPlayingMovies by lazy { + createPagingFlow( + fetcher = { p -> movieService.getNowPlaying(p) }, + processor = { it.results } + ) + } + val upcomingMovies by lazy { + createPagingFlow( + fetcher = { p -> movieService.getUpcoming(p) }, + processor = { it.results } + ) + } val detailedTv = tvService.detailTv val tvImages = tvService.images @@ -78,22 +87,30 @@ class MainViewModel: ViewModel(), KoinComponent { val tvAccountStates = tvService.accountStates val tvKeywordResults = tvService.keywordResults - val popularTv = createPagingFlow( - fetcher = { p -> tvService.getPopular(p) }, - processor = { it.results } - ) - val topRatedTv = createPagingFlow( - fetcher = { p -> tvService.getTopRated(p) }, - processor = { it.results } - ) - val airingTodayTv = createPagingFlow( - fetcher = { p -> tvService.getNowPlaying(p) }, - processor = { it.results } - ) - val onTheAirTv = createPagingFlow( - fetcher = { p -> tvService.getUpcoming(p) }, - processor = { it.results } - ) + val popularTv by lazy { + createPagingFlow( + fetcher = { p -> tvService.getPopular(p) }, + processor = { it.results } + ) + } + val topRatedTv by lazy{ + createPagingFlow( + fetcher = { p -> tvService.getTopRated(p) }, + processor = { it.results } + ) + } + val airingTodayTv by lazy { + createPagingFlow( + fetcher = { p -> tvService.getNowPlaying(p) }, + processor = { it.results } + ) + } + val onTheAirTv by lazy { + createPagingFlow( + fetcher = { p -> tvService.getUpcoming(p) }, + processor = { it.results } + ) + } val peopleMap = peopleService.peopleMap val peopleCastMap = peopleService.castMap @@ -101,10 +118,12 @@ class MainViewModel: ViewModel(), KoinComponent { val peopleImagesMap = peopleService.imagesMap val peopleExternalIdsMap = peopleService.externalIdsMap - val popularPeople = createPagingFlow( - fetcher = { p -> peopleService.getPopular(p) }, - processor = { it.results } - ) + val popularPeople by lazy { + createPagingFlow( + fetcher = { p -> peopleService.getPopular(p) }, + processor = { it.results } + ) + } private fun providesForType(type: MediaViewType, movies: () -> T, tv: () -> T): T { return when (type) { @@ -167,6 +186,7 @@ class MainViewModel: ViewModel(), KoinComponent { MediaTabNavItem.Type.TOP_RATED -> topRatedMovies MediaTabNavItem.Type.NOW_PLAYING -> nowPlayingMovies MediaTabNavItem.Type.POPULAR -> popularMovies + else -> throw Exception("Can't produce media flow for $mediaType") } }, { @@ -175,6 +195,7 @@ class MainViewModel: ViewModel(), KoinComponent { MediaTabNavItem.Type.TOP_RATED -> topRatedTv MediaTabNavItem.Type.NOW_PLAYING -> airingTodayTv MediaTabNavItem.Type.POPULAR -> popularTv + else -> throw Exception("Can't produce media flow for $mediaType") } } ) @@ -184,6 +205,30 @@ class MainViewModel: ViewModel(), KoinComponent { return providesForType(mediaType, { movieKeywordResults }, { tvKeywordResults }) } + fun produceTrendingFor(mediaType: MediaViewType, timeWindow: TimeWindow): Flow> { + return when (mediaType) { + MediaViewType.MOVIE -> { + createPagingFlow( + fetcher = { p -> movieService.getTrending(timeWindow, p) }, + processor = { it.results } + ) + } + MediaViewType.TV -> { + createPagingFlow( + fetcher = { p -> tvService.getTrending(timeWindow, p) }, + processor = { it.results } + ) + } + MediaViewType.PERSON -> { + createPagingFlow( + fetcher = { p -> peopleService.getTrending(timeWindow, p) }, + processor = { it.results } + ) + } + else -> throw ViewableMediaTypeException(mediaType) + } + } + suspend fun getById(id: Int, type: MediaViewType) { when (type) { MediaViewType.MOVIE -> movieService.getById(id) diff --git a/app/src/main/java/com/owenlejeune/tvtime/utils/types/TimeWindow.kt b/app/src/main/java/com/owenlejeune/tvtime/utils/types/TimeWindow.kt new file mode 100644 index 0000000..d4b9ae1 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/utils/types/TimeWindow.kt @@ -0,0 +1,10 @@ +package com.owenlejeune.tvtime.utils.types + +import com.google.gson.annotations.SerializedName + +enum class TimeWindow { + @SerializedName("day") + DAY, + @SerializedName("week") + WEEK +} \ 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 bae9dc5..1bdc411 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,7 @@ Upcoming Airing Today On The Air + Trending Account Rated Movies Rated TV Shows @@ -216,6 +217,9 @@ Recommended TV No Recommended TV No more results found + Multi + Day + Week This product uses the TMDB API but is not endorsed or certified by TMDB. "https://www.themoviedb.org"