mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-08 04:32:43 -05:00
add trending content to home pages
This commit is contained in:
@@ -53,7 +53,7 @@ class BasePagingSource<T: Any, S>(
|
||||
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) {
|
||||
|
||||
@@ -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<SearchResult<SearchResultMovie>>
|
||||
|
||||
@GET("trending/movie/{time_window}")
|
||||
suspend fun trending(@Path("time_window") timeWindow: String, @Query("page") page: Int): Response<SearchResult<SearchResultMovie>>
|
||||
|
||||
}
|
||||
@@ -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<out SearchResult<out SearchResultMedia>> {
|
||||
return movieService.trending(timeWindow.name.lowercase(), page)
|
||||
}
|
||||
|
||||
override suspend fun discover(keywords: String?, page: Int): Response<out SearchResult<out SearchResultMedia>> {
|
||||
return movieService.discover(keywords, page)
|
||||
}
|
||||
|
||||
@@ -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<ExternalIds>
|
||||
|
||||
@GET("trending/person/{time_window}")
|
||||
suspend fun trending(@Path("time_window") timeWindow: String, @Query("page") page: Int): Response<SearchResult<SearchResultPerson>>
|
||||
|
||||
}
|
||||
@@ -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<SearchResult<SearchResultPerson>> {
|
||||
return service.trending(timeWindow.name.lowercase(), page)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<SearchResult<SearchResultTv>>
|
||||
|
||||
@GET("trending/tv/{time_window}")
|
||||
suspend fun trending(@Path("time_window") timeWindow: String, @Query("page") page: Int): Response<SearchResult<SearchResultTv>>
|
||||
}
|
||||
@@ -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<out SearchResult<out SearchResultMedia>> {
|
||||
return service.trending(timeWindow.name.lowercase(), page)
|
||||
}
|
||||
|
||||
override suspend fun discover(keywords: String?, page: Int): Response<out SearchResult<out SearchResultMedia>> {
|
||||
return service.discover(page, keywords)
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ abstract class SearchResultMedia(
|
||||
@SerializedName("genre_ids") val genreIds: List<Int>,
|
||||
@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)
|
||||
): SortableSearchResult(type, popularity, id, name, posterPath)
|
||||
@@ -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<KnownFor>,
|
||||
profilePath: String,
|
||||
id: Int,
|
||||
name: String,
|
||||
popularity: Float
|
||||
): SortableSearchResult(MediaViewType.PERSON, popularity, id, name)
|
||||
): SortableSearchResult(MediaViewType.PERSON, popularity, id, name, profilePath)
|
||||
@@ -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
|
||||
id: Int,
|
||||
name: String,
|
||||
posterPath: String?
|
||||
): TmdbItem(id, posterPath, name), Searchable
|
||||
@@ -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
|
||||
)
|
||||
@@ -52,11 +52,13 @@ private val POSTER_HEIGHT = 180.dp
|
||||
|
||||
@Composable
|
||||
fun PagingPosterGrid(
|
||||
modifier: Modifier = Modifier,
|
||||
lazyPagingItems: LazyPagingItems<TmdbItem>?,
|
||||
onClick: (id: Int) -> Unit = {}
|
||||
) {
|
||||
lazyPagingItems?.let {
|
||||
LazyVerticalGrid(
|
||||
modifier = modifier,
|
||||
columns = GridCells.Adaptive(minSize = POSTER_WIDTH),
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <T: SortableSearchResult> 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 }
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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<MainViewModel>()
|
||||
|
||||
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(
|
||||
|
||||
@@ -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<MediaTabNavItem> {
|
||||
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<out HomePageResponse>
|
||||
@@ -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<MainViewModel>()
|
||||
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<MainViewModel>()
|
||||
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<MainViewModel>()
|
||||
|
||||
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<PeopleTabNavItem>,
|
||||
pagerState: PagerState,
|
||||
appNavController: NavHostController = rememberNavController()
|
||||
) {
|
||||
HorizontalPager(count = tabs.size, state = pagerState) { page ->
|
||||
tabs[page].screen(appNavController)
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 <T> 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<PagingData<TmdbItem>> {
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
<string name="nav_upcoming_title">Upcoming</string>
|
||||
<string name="nav_tv_airing_today_title">Airing Today</string>
|
||||
<string name="nav_tv_on_the_air">On The Air</string>
|
||||
<string name="nav_trending_title">Trending</string>
|
||||
<string name="nav_account_title">Account</string>
|
||||
<string name="nav_rated_movies_title">Rated Movies</string>
|
||||
<string name="nav_rated_shows_title">Rated TV Shows</string>
|
||||
@@ -216,6 +217,9 @@
|
||||
<string name="recommended_tv_title">Recommended TV</string>
|
||||
<string name="no_recommended_tv">No Recommended TV</string>
|
||||
<string name="no_result_found">No more results found</string>
|
||||
<string name="search_multi_title">Multi</string>
|
||||
<string name="time_window_day">Day</string>
|
||||
<string name="time_window_week">Week</string>
|
||||
|
||||
<string name="attribution_text">This product uses the TMDB API but is not endorsed or certified by TMDB.</string>
|
||||
<string name="tmdb_home_page">"https://www.themoviedb.org"</string>
|
||||
|
||||
Reference in New Issue
Block a user