add tabs to top of movies and tv screens

This commit is contained in:
Owen LeJeune
2022-02-17 13:18:20 -05:00
parent 98728552ef
commit c90c699a27
30 changed files with 415 additions and 159 deletions

View File

@@ -3,22 +3,20 @@ package com.owenlejeune.tvtime
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.view.WindowCompat
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.owenlejeune.tvtime.ui.navigation.MainNavigationRoutes
import com.owenlejeune.tvtime.ui.theme.TVTimeTheme
class MainActivity : ComponentActivity() {
// private val appNavControllerProvider: (@Composable () -> NavHostController) by inject(named(NavControllers.APP))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
@@ -31,15 +29,20 @@ class MainActivity : ComponentActivity() {
// }
// val systemUiController = rememberSystemUiController()
// systemUiController.setStatusBarColor(statusBarColor, !isSystemInDarkTheme())
MyApp(displayUnderStatusBar = displayUnderStatusBar)
MyApp(
appNavController = rememberNavController(),
displayUnderStatusBar = displayUnderStatusBar
)
}
}
}
@Composable
fun MyApp(displayUnderStatusBar: MutableState<Boolean> = mutableStateOf(false)) {
fun MyApp(
appNavController: NavHostController = rememberNavController(),
displayUnderStatusBar: MutableState<Boolean> = mutableStateOf(false)
) {
TVTimeTheme {
val appNavController = rememberNavController()
Box {
MainNavigationRoutes(navController = appNavController, displayUnderStatusBar = displayUnderStatusBar)
}

View File

@@ -0,0 +1,16 @@
package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.HomePageResponse
import retrofit2.Response
interface HomePageService {
suspend fun getNowPlaying(page: Int = 1): Response<out HomePageResponse>
suspend fun getPopular(page: Int = 1): Response<out HomePageResponse>
suspend fun getTopRated(page: Int = 1): Response<out HomePageResponse>
suspend fun getUpcoming(page: Int = 1): Response<out HomePageResponse>
}

View File

@@ -9,7 +9,16 @@ import retrofit2.http.Query
interface MoviesApi {
@GET("movie/popular")
suspend fun getPopularMovies(@Query("page") page: Int = 1): Response<PopularMoviesResponse>
suspend fun getPopularMovies(@Query("page") page: Int = 1): Response<HomePageMoviesResponse>
@GET("movie/now_playing")
suspend fun getNowPlayingMovies(@Query("page") page: Int = 1): Response<HomePageMoviesResponse>
@GET("movie/top_rated")
suspend fun getTopRatedMovies(@Query("page") page: Int = 1): Response<HomePageMoviesResponse>
@GET("movie/upcoming")
suspend fun getUpcomingMovies(@Query("page") page: Int = 1): Response<HomePageMoviesResponse>
@GET("movie/{id}")
suspend fun getMovieById(@Path("id") id: Int): Response<DetailedMovie>

View File

@@ -4,14 +4,26 @@ import com.owenlejeune.tvtime.api.tmdb.model.*
import org.koin.core.component.KoinComponent
import retrofit2.Response
class MoviesService: KoinComponent, DetailService {
class MoviesService: KoinComponent, DetailService, HomePageService {
private val service by lazy { TmdbClient().createMovieService() }
suspend fun getPopularMovies(page: Int = 1): Response<PopularMoviesResponse> {
override suspend fun getPopular(page: Int): Response<out HomePageResponse> {
return service.getPopularMovies(page)
}
override suspend fun getNowPlaying(page: Int): Response<out HomePageResponse> {
return service.getNowPlayingMovies(page)
}
override suspend fun getTopRated(page: Int): Response<out HomePageResponse> {
return service.getTopRatedMovies(page)
}
override suspend fun getUpcoming(page: Int): Response<out HomePageResponse> {
return service.getUpcomingMovies(page)
}
suspend fun getReleaseDates(id: Int): Response<MovieReleaseResults> {
return service.getReleaseDates(id)
}

View File

@@ -9,7 +9,16 @@ import retrofit2.http.Query
interface TvApi {
@GET("tv/popular")
suspend fun getPoplarTv(@Query("page") page: Int = 1): Response<PopularTvResponse>
suspend fun getPoplarTv(@Query("page") page: Int = 1): Response<HomePageTvResponse>
@GET("tv/top_rated")
suspend fun getTopRatedTv(@Query("page") page: Int = 1): Response<HomePageTvResponse>
@GET("tv/airing_today")
suspend fun getTvAiringToday(@Query("page") page: Int = 1): Response<HomePageTvResponse>
@GET("tv/on_the_air")
suspend fun getTvOnTheAir(@Query("page") page: Int = 1): Response<HomePageTvResponse>
@GET("tv/{id}")
suspend fun getTvShowById(@Path("id") id: Int): Response<out DetailedTv>

View File

@@ -1,17 +1,28 @@
package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.CastAndCrew
import com.owenlejeune.tvtime.api.tmdb.model.ImageCollection
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
import com.owenlejeune.tvtime.api.tmdb.model.TvContentRatings
import com.owenlejeune.tvtime.api.tmdb.model.*
import org.koin.core.component.KoinComponent
import retrofit2.Response
class TvService: KoinComponent, DetailService {
class TvService: KoinComponent, DetailService, HomePageService {
private val service by lazy { TmdbClient().createTvService() }
suspend fun getPopularTv(page: Int = 1) = service.getPoplarTv(page)
override suspend fun getPopular(page: Int): Response<out HomePageResponse> {
return service.getPoplarTv(page)
}
override suspend fun getNowPlaying(page: Int): Response<out HomePageResponse> {
return service.getTvAiringToday(page)
}
override suspend fun getTopRated(page: Int): Response<out HomePageResponse> {
return service.getTopRatedTv(page)
}
override suspend fun getUpcoming(page: Int): Response<out HomePageResponse> {
return service.getTvOnTheAir(page)
}
override suspend fun getById(id: Int): Response<out DetailedItem> {
return service.getTvShowById(id)

View File

@@ -11,4 +11,4 @@ abstract class DetailedItem(
@Transient open val status: String,
@Transient open val tagline: String?,
@Transient open val voteAverage: Float
): TmdbItem(id, title, posterPath)
): TmdbItem(id, posterPath, title)

View File

@@ -3,9 +3,9 @@ package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
class DetailedMovie(
@SerializedName("id") override val id: Int,
id: Int,
posterPath: String?,
@SerializedName("original_title") override val title: String,
@SerializedName("poster_path") override val posterPath: String?,
@SerializedName("backdrop_path") override val backdropPath: String?,
@SerializedName("genres") override val genres: List<Genre>,
@SerializedName("overview") override val overview: String?,

View File

@@ -3,9 +3,9 @@ package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
class DetailedTv(
@SerializedName("id") override val id: Int,
id: Int,
posterPath: String?,
@SerializedName("name") override val title: String,
@SerializedName("poster_path") override val posterPath: String?,
@SerializedName("backdrop_path") override val backdropPath: String?,
@SerializedName("genres") override val genres: List<Genre>,
@SerializedName("overview") override val overview: String?,

View File

@@ -0,0 +1,9 @@
package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
class HomePageMovie(
id: Int,
posterPath: String?,
@SerializedName("title") override val title: String
): TmdbItem(id, posterPath, title)

View File

@@ -0,0 +1,9 @@
package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
class HomePageMoviesResponse(
count: Int,
page: Int,
@SerializedName("results") override val results: List<HomePageMovie>
): HomePageResponse(count, page, results)

View File

@@ -2,8 +2,8 @@ package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
data class PopularTvResponse(
abstract class HomePageResponse(
@SerializedName("total_results") val count: Int,
@SerializedName("page") val page: Int,
@SerializedName("results") val tv: List<PopularTv>
@Transient open val results: List<TmdbItem>
)

View File

@@ -0,0 +1,9 @@
package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
class HomePageTv(
id: Int,
posterPath: String?,
@SerializedName("name") override val title: String,
): TmdbItem(id, posterPath, title)

View File

@@ -0,0 +1,9 @@
package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
class HomePageTvResponse(
count: Int,
page: Int,
@SerializedName("results") override val results: List<HomePageTv>
): HomePageResponse(count, page, results)

View File

@@ -1,9 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
data class PopularMovie(
@SerializedName("id") override val id: Int,
@SerializedName("title") override val title: String,
@SerializedName("poster_path") override val posterPath: String?
): TmdbItem(id, title, posterPath)

View File

@@ -1,9 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
data class PopularMoviesResponse(
@SerializedName("total_results") val count: Int,
@SerializedName("page") val page: Int,
@SerializedName("results") val movies: List<PopularMovie>
)

View File

@@ -1,9 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
data class PopularTv(
@SerializedName("id") override val id: Int,
@SerializedName("name") override val title: String,
@SerializedName("poster_path") override val posterPath: String?
): TmdbItem(id, title, posterPath)

View File

@@ -1,7 +1,9 @@
package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
abstract class TmdbItem(
@Transient open val id: Int,
@Transient open val title: String,
@Transient open val posterPath: String?
@SerializedName("id") val id: Int,
@SerializedName("poster_path") val posterPath: String?,
@Transient open val title: String
)

View File

@@ -0,0 +1,51 @@
package com.owenlejeune.tvtime.ui.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.HomePageService
import com.owenlejeune.tvtime.api.tmdb.model.HomePageResponse
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.MediaTabContent
import com.owenlejeune.tvtime.utils.ResourceUtils
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import retrofit2.Response
typealias NavComposableFun = @Composable (NavHostController, MediaViewType, MediaFetchFun) -> Unit
private val screenContent: NavComposableFun = { appNavController, mediaViewType, mediaFetchFun ->
MediaTabContent(appNavController = appNavController, mediaType = mediaViewType, mediaFetchFun = mediaFetchFun)
}
typealias MediaFetchFun = suspend (service: HomePageService, page: Int) -> Response<out HomePageResponse>
abstract class TabNavItem(val route: String, val screen: NavComposableFun, val mediaFetchFun: MediaFetchFun): KoinComponent {
abstract val name: String
}
sealed class MainTabNavItem(stringRes: Int, route: String, screen: NavComposableFun, mediaFetchFun: MediaFetchFun)
: TabNavItem(route, screen, mediaFetchFun)
{
private val resourceUtils: ResourceUtils by inject()
override val name = resourceUtils.getString(stringRes)
companion object {
val MovieItems = listOf(Popular, TopRated, NowPlaying, Upcoming)
val TvItems = listOf(Popular, TopRated, AiringToday, OnTheAir)
private val Items = listOf(NowPlaying, Popular, TopRated, Upcoming, AiringToday, OnTheAir)
fun getByRoute(route: String?): MainTabNavItem? {
return Items.firstOrNull { it.route == route }
}
}
object Popular: MainTabNavItem(R.string.nav_popular_title, "popular_route", screenContent, { s, p -> s.getPopular(p) } )
object TopRated: MainTabNavItem(R.string.nav_top_rated_title, "top_rated_route", screenContent, { s, p -> s.getTopRated(p) } )
object NowPlaying: MainTabNavItem(R.string.nav_now_playing_title, "now_playing_route", screenContent, { s, p -> s.getNowPlaying(p) } )
object Upcoming: MainTabNavItem(R.string.nav_upcoming_title, "upcoming_route", screenContent, { s, p -> s.getUpcoming(p) } )
object AiringToday: MainTabNavItem(R.string.nav_tv_airing_today_title, "airing_today_route", screenContent, { s, p -> s.getNowPlaying(p) } )
object OnTheAir: MainTabNavItem(R.string.nav_tv_on_the_air, "on_the_air_route", screenContent, { s, p -> s.getUpcoming(p) } )
}

View File

@@ -3,19 +3,17 @@ package com.owenlejeune.tvtime.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.owenlejeune.tvtime.ui.screens.DetailView
import com.owenlejeune.tvtime.ui.screens.DetailViewType
import com.owenlejeune.tvtime.ui.screens.MainAppView
import com.owenlejeune.tvtime.ui.screens.tabs.FavouritesTab
import com.owenlejeune.tvtime.ui.screens.tabs.MoviesTab
import com.owenlejeune.tvtime.ui.screens.tabs.SettingsTab
import com.owenlejeune.tvtime.ui.screens.tabs.TvTab
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.FavouritesTab
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.MediaTab
import com.owenlejeune.tvtime.ui.screens.tabs.bottom.SettingsTab
object NavConstants {
const val ID_KEY = "id_key"
@@ -33,7 +31,7 @@ fun MainNavigationRoutes(navController: NavHostController, displayUnderStatusBar
MainNavItem.DetailView.route.plus("/{${NavConstants.TYPE_KEY}}/{${NavConstants.ID_KEY}}"),
arguments = listOf(
navArgument(NavConstants.ID_KEY) { type = NavType.IntType },
navArgument(NavConstants.TYPE_KEY) { type = NavType.EnumType(DetailViewType::class.java) }
navArgument(NavConstants.TYPE_KEY) { type = NavType.EnumType(MediaViewType::class.java) }
)
) { navBackStackEntry ->
displayUnderStatusBar.value = true
@@ -41,7 +39,7 @@ fun MainNavigationRoutes(navController: NavHostController, displayUnderStatusBar
DetailView(
appNavController = navController,
itemId = args?.getInt(NavConstants.ID_KEY),
type = args?.getSerializable(NavConstants.TYPE_KEY) as DetailViewType
type = args?.getSerializable(NavConstants.TYPE_KEY) as MediaViewType
)
}
}
@@ -49,15 +47,15 @@ fun MainNavigationRoutes(navController: NavHostController, displayUnderStatusBar
@Composable
fun BottomNavigationRoutes(
appNavController: NavController,
appNavController: NavHostController,
navController: NavHostController
) {
NavHost(navController = navController, startDestination = BottomNavItem.Movies.route) {
composable(BottomNavItem.Movies.route) {
MoviesTab(appNavController = appNavController)
MediaTab(appNavController = appNavController, mediaType = MediaViewType.MOVIE)
}
composable(BottomNavItem.TV.route) {
TvTab(appNavController = appNavController)
MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV)
}
composable(BottomNavItem.Favourites.route) {
FavouritesTab()

View File

@@ -14,7 +14,10 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
@@ -29,11 +32,14 @@ import coil.transform.RoundedCornersTransformation
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.DetailService
import com.owenlejeune.tvtime.api.tmdb.MoviesService
import com.owenlejeune.tvtime.utils.TmdbUtils
import com.owenlejeune.tvtime.api.tmdb.TvService
import com.owenlejeune.tvtime.api.tmdb.model.*
import com.owenlejeune.tvtime.extensions.dpToPx
import com.owenlejeune.tvtime.ui.components.*
import com.owenlejeune.tvtime.ui.components.BackdropImage
import com.owenlejeune.tvtime.ui.components.ChipGroup
import com.owenlejeune.tvtime.ui.components.MinLinesText
import com.owenlejeune.tvtime.ui.components.PosterItem
import com.owenlejeune.tvtime.utils.TmdbUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -43,11 +49,11 @@ import kotlinx.coroutines.withContext
fun DetailView(
appNavController: NavController,
itemId: Int?,
type: DetailViewType
type: MediaViewType
) {
val service = when(type) {
DetailViewType.MOVIE -> MoviesService()
DetailViewType.TV -> TvService()
MediaViewType.MOVIE -> MoviesService()
MediaViewType.TV -> TvService()
}
val mediaItem = remember { mutableStateOf<DetailedItem?>(null) }
@@ -176,7 +182,7 @@ private fun ContentColumn(modifier: Modifier,
itemId: Int?,
mediaItem: MutableState<DetailedItem?>,
service: DetailService,
mediaType: DetailViewType
mediaType: MediaViewType
) {
Column(
modifier = modifier
@@ -185,7 +191,7 @@ private fun ContentColumn(modifier: Modifier,
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
) {
if (mediaType == DetailViewType.MOVIE) {
if (mediaType == MediaViewType.MOVIE) {
MiscMovieDetails(mediaItem = mediaItem, service as MoviesService)
} else {
MiscTvDetails(mediaItem = mediaItem, service as TvService)
@@ -439,8 +445,3 @@ private fun fetchTvContentRating(id: Int, service: TvService, contentRating: Mut
}
}
}
enum class DetailViewType {
MOVIE,
TV
}

View File

@@ -10,15 +10,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.pager.ExperimentalPagerApi
import com.owenlejeune.tvtime.ui.components.SearchFab
import com.owenlejeune.tvtime.ui.navigation.BottomNavItem
import com.owenlejeune.tvtime.ui.navigation.BottomNavigationRoutes
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
@Composable
fun MainAppView(appNavController: NavController) {
fun MainAppView(appNavController: NavHostController) {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
@@ -83,7 +85,12 @@ private fun BottomNavBar(navController: NavController, appBarTitle: MutableState
appBarTitle = appBarTitle,
item = item
)
}
},
colors = NavigationBarItemDefaults
.colors(
selectedIconColor = MaterialTheme.colorScheme.onPrimary,
indicatorColor = MaterialTheme.colorScheme.primary
)
)
}
}

View File

@@ -0,0 +1,6 @@
package com.owenlejeune.tvtime.ui.screens
enum class MediaViewType {
MOVIE,
TV
}

View File

@@ -1,39 +0,0 @@
package com.owenlejeune.tvtime.ui.screens.tabs
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import com.owenlejeune.tvtime.api.tmdb.MoviesService
import com.owenlejeune.tvtime.ui.components.PosterGrid
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.ui.screens.DetailViewType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MoviesTab(appNavController: NavController) {
// val moviesViewModel = viewModel(PopularMovieViewModel::class.java)
// val moviesList = moviesViewModel.moviePage
// val movieListItems: LazyPagingItems<PopularMovie> = moviesList.collectAsLazyPagingItems()
PosterGrid(
fetchMedia = { moviesList ->
val service = MoviesService()
CoroutineScope(Dispatchers.IO).launch {
val response = service.getPopularMovies()
if (response.isSuccessful) {
withContext(Dispatchers.Main) {
moviesList.value = response.body()!!.movies
}
}
}
},
onClick = { id ->
appNavController.navigate(
"${MainNavItem.DetailView.route}/${DetailViewType.MOVIE}/${id}"
)
}
)
}

View File

@@ -1,36 +0,0 @@
package com.owenlejeune.tvtime.ui.screens.tabs
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import com.owenlejeune.tvtime.api.tmdb.TvService
import com.owenlejeune.tvtime.ui.components.PosterGrid
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.ui.screens.DetailViewType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TvTab(appNavController: NavController) {
PosterGrid(
fetchMedia = { tvList ->
val service = TvService()
CoroutineScope(Dispatchers.IO).launch {
val response = service.getPopularTv()
if (response.isSuccessful) {
withContext(Dispatchers.Main) {
tvList.value = response.body()!!.tv
}
}
}
},
onClick = { id ->
appNavController.navigate(
"${MainNavItem.DetailView.route}/${DetailViewType.TV}/${id}"
)
}
)
}

View File

@@ -1,4 +1,4 @@
package com.owenlejeune.tvtime.ui.screens.tabs
package com.owenlejeune.tvtime.ui.screens.tabs.bottom
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize

View File

@@ -0,0 +1,69 @@
package com.owenlejeune.tvtime.ui.screens.tabs.bottom
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.rememberPagerState
import com.owenlejeune.tvtime.api.tmdb.HomePageService
import com.owenlejeune.tvtime.api.tmdb.MoviesService
import com.owenlejeune.tvtime.api.tmdb.TvService
import com.owenlejeune.tvtime.ui.components.PosterGrid
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.ui.navigation.MainTabNavItem
import com.owenlejeune.tvtime.ui.navigation.MediaFetchFun
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.tabs.top.Tabs
import com.owenlejeune.tvtime.ui.screens.tabs.top.TabsContent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(ExperimentalPagerApi::class)
@Composable
fun MediaTab(appNavController: NavHostController, mediaType: MediaViewType) {
Column {
val tabs = when (mediaType) {
MediaViewType.MOVIE -> MainTabNavItem.MovieItems
MediaViewType.TV -> MainTabNavItem.TvItems
}
val pagerState = rememberPagerState()
Tabs(tabs = tabs, pagerState = pagerState)
TabsContent(
tabs = tabs,
pagerState = pagerState,
appNavController = appNavController,
mediaViewType = mediaType
)
}
}
@Composable
fun MediaTabContent(appNavController: NavHostController, mediaType: MediaViewType, mediaFetchFun: MediaFetchFun) {
val service: HomePageService = when(mediaType) {
MediaViewType.MOVIE -> MoviesService()
MediaViewType.TV -> TvService()
}
PosterGrid(
fetchMedia = { mediaList ->
CoroutineScope(Dispatchers.IO).launch {
val response = mediaFetchFun.invoke(service, 1)
if (response.isSuccessful) {
withContext(Dispatchers.Main) {
mediaList.value = response.body()?.results ?: emptyList()
}
}
}
},
onClick = { id ->
appNavController.navigate(
"${MainNavItem.DetailView.route}/${mediaType}/${id}"
)
}
)
}
// val moviesViewModel = viewModel(PopularMovieViewModel::class.java)
// val moviesList = moviesViewModel.moviePage
// val movieListItems: LazyPagingItems<PopularMovie> = moviesList.collectAsLazyPagingItems()

View File

@@ -1,4 +1,4 @@
package com.owenlejeune.tvtime.ui.screens.tabs
package com.owenlejeune.tvtime.ui.screens.tabs.bottom
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column

View File

@@ -0,0 +1,122 @@
package com.owenlejeune.tvtime.ui.screens.tabs.top
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ScrollableTabRow
import androidx.compose.material.Tab
import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
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.ui.navigation.MainTabNavItem
import com.owenlejeune.tvtime.ui.navigation.TabNavItem
import com.owenlejeune.tvtime.ui.screens.MediaViewType
import kotlinx.coroutines.launch
@OptIn(ExperimentalPagerApi::class)
@Composable
fun Tabs(
tabs: List<TabNavItem>,
pagerState: PagerState,
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colorScheme.background,
contentColor: Color = MaterialTheme.colorScheme.primary,
selectedTabTextColor: Color = MaterialTheme.colorScheme.primary,
unselectedTabTextColor: Color = MaterialTheme.colorScheme.onBackground,
tabTextStyle: TextStyle = MaterialTheme.typography.bodySmall,
tabIndicatorColor: Color = MaterialTheme.colorScheme.primary
) {
val scope = rememberCoroutineScope()
ScrollableTabRow(
modifier = modifier,
selectedTabIndex = pagerState.currentPage,
backgroundColor = backgroundColor,
contentColor = contentColor,
edgePadding = 8.dp,
indicator = { tabPositions ->
SmallTabIndicator(
modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]),
color = tabIndicatorColor
)
}
) {
tabs.forEachIndexed { index, tab ->
Tab(
text = {
Text(
text = tab.name,
style = tabTextStyle,
color = if (pagerState.currentPage == index) selectedTabTextColor else unselectedTabTextColor
)
},
selected = pagerState.currentPage == index,
onClick = {
scope.launch {
pagerState.animateScrollToPage(index)
}
}
)
}
}
}
@Composable
private fun SmallTabIndicator(
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colorScheme.primary
) {
Spacer(
modifier
.padding(horizontal = 28.dp)
.height(2.dp)
.background(color, RoundedCornerShape(topStartPercent = 100, topEndPercent = 100))
)
}
@OptIn(ExperimentalPagerApi::class)
@Preview(showBackground = true)
@Composable
fun TabsPreview() {
val tabs = MainTabNavItem.MovieItems
val pagerState = rememberPagerState()
Tabs(tabs = tabs, pagerState = pagerState)
}
@OptIn(ExperimentalPagerApi::class)
@Composable
fun TabsContent(
tabs: List<TabNavItem>,
pagerState: PagerState,
mediaViewType: MediaViewType,
appNavController: NavHostController = rememberNavController()
) {
HorizontalPager(count = tabs.size, state = pagerState) { page ->
tabs[page].screen(appNavController, mediaViewType, tabs[page].mediaFetchFun)
}
}
@OptIn(ExperimentalPagerApi::class)
@Preview(showBackground = true)
@Composable
fun TabsContentPreview() {
val tabs = MainTabNavItem.MovieItems
val pagerState = rememberPagerState()
TabsContent(tabs = tabs, pagerState = pagerState, MediaViewType.MOVIE)
}

View File

@@ -5,4 +5,10 @@
<string name="nav_favourites_title">Favourites</string>
<string name="nav_settings_title">Settings</string>
<string name="cast_label">Cast</string>
<string name="nav_now_playing_title">Now Playing</string>
<string name="nav_popular_title">Popular</string>
<string name="nav_top_rated_title">Top Rated</string>
<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>
</resources>