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 android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.core.view.WindowCompat import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.owenlejeune.tvtime.ui.navigation.MainNavigationRoutes import com.owenlejeune.tvtime.ui.navigation.MainNavigationRoutes
import com.owenlejeune.tvtime.ui.theme.TVTimeTheme import com.owenlejeune.tvtime.ui.theme.TVTimeTheme
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
// private val appNavControllerProvider: (@Composable () -> NavHostController) by inject(named(NavControllers.APP))
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
@@ -31,15 +29,20 @@ class MainActivity : ComponentActivity() {
// } // }
// val systemUiController = rememberSystemUiController() // val systemUiController = rememberSystemUiController()
// systemUiController.setStatusBarColor(statusBarColor, !isSystemInDarkTheme()) // systemUiController.setStatusBarColor(statusBarColor, !isSystemInDarkTheme())
MyApp(displayUnderStatusBar = displayUnderStatusBar) MyApp(
appNavController = rememberNavController(),
displayUnderStatusBar = displayUnderStatusBar
)
} }
} }
} }
@Composable @Composable
fun MyApp(displayUnderStatusBar: MutableState<Boolean> = mutableStateOf(false)) { fun MyApp(
appNavController: NavHostController = rememberNavController(),
displayUnderStatusBar: MutableState<Boolean> = mutableStateOf(false)
) {
TVTimeTheme { TVTimeTheme {
val appNavController = rememberNavController()
Box { Box {
MainNavigationRoutes(navController = appNavController, displayUnderStatusBar = displayUnderStatusBar) 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 { interface MoviesApi {
@GET("movie/popular") @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}") @GET("movie/{id}")
suspend fun getMovieById(@Path("id") id: Int): Response<DetailedMovie> 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 org.koin.core.component.KoinComponent
import retrofit2.Response import retrofit2.Response
class MoviesService: KoinComponent, DetailService { class MoviesService: KoinComponent, DetailService, HomePageService {
private val service by lazy { TmdbClient().createMovieService() } 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) 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> { suspend fun getReleaseDates(id: Int): Response<MovieReleaseResults> {
return service.getReleaseDates(id) return service.getReleaseDates(id)
} }

View File

@@ -9,7 +9,16 @@ import retrofit2.http.Query
interface TvApi { interface TvApi {
@GET("tv/popular") @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}") @GET("tv/{id}")
suspend fun getTvShowById(@Path("id") id: Int): Response<out DetailedTv> suspend fun getTvShowById(@Path("id") id: Int): Response<out DetailedTv>

View File

@@ -1,17 +1,28 @@
package com.owenlejeune.tvtime.api.tmdb package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.CastAndCrew import com.owenlejeune.tvtime.api.tmdb.model.*
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 org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import retrofit2.Response import retrofit2.Response
class TvService: KoinComponent, DetailService { class TvService: KoinComponent, DetailService, HomePageService {
private val service by lazy { TmdbClient().createTvService() } 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> { override suspend fun getById(id: Int): Response<out DetailedItem> {
return service.getTvShowById(id) return service.getTvShowById(id)

View File

@@ -11,4 +11,4 @@ abstract class DetailedItem(
@Transient open val status: String, @Transient open val status: String,
@Transient open val tagline: String?, @Transient open val tagline: String?,
@Transient open val voteAverage: Float @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 import com.google.gson.annotations.SerializedName
class DetailedMovie( class DetailedMovie(
@SerializedName("id") override val id: Int, id: Int,
posterPath: String?,
@SerializedName("original_title") override val title: String, @SerializedName("original_title") override val title: String,
@SerializedName("poster_path") override val posterPath: String?,
@SerializedName("backdrop_path") override val backdropPath: String?, @SerializedName("backdrop_path") override val backdropPath: String?,
@SerializedName("genres") override val genres: List<Genre>, @SerializedName("genres") override val genres: List<Genre>,
@SerializedName("overview") override val overview: String?, @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 import com.google.gson.annotations.SerializedName
class DetailedTv( class DetailedTv(
@SerializedName("id") override val id: Int, id: Int,
posterPath: String?,
@SerializedName("name") override val title: String, @SerializedName("name") override val title: String,
@SerializedName("poster_path") override val posterPath: String?,
@SerializedName("backdrop_path") override val backdropPath: String?, @SerializedName("backdrop_path") override val backdropPath: String?,
@SerializedName("genres") override val genres: List<Genre>, @SerializedName("genres") override val genres: List<Genre>,
@SerializedName("overview") override val overview: String?, @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 import com.google.gson.annotations.SerializedName
data class PopularTvResponse( abstract class HomePageResponse(
@SerializedName("total_results") val count: Int, @SerializedName("total_results") val count: Int,
@SerializedName("page") val page: 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 package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
abstract class TmdbItem( abstract class TmdbItem(
@Transient open val id: Int, @SerializedName("id") val id: Int,
@Transient open val title: String, @SerializedName("poster_path") val posterPath: String?,
@Transient open 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.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.navigation.NavController
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.owenlejeune.tvtime.ui.screens.DetailView 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.MainAppView
import com.owenlejeune.tvtime.ui.screens.tabs.FavouritesTab import com.owenlejeune.tvtime.ui.screens.MediaViewType
import com.owenlejeune.tvtime.ui.screens.tabs.MoviesTab import com.owenlejeune.tvtime.ui.screens.tabs.bottom.FavouritesTab
import com.owenlejeune.tvtime.ui.screens.tabs.SettingsTab import com.owenlejeune.tvtime.ui.screens.tabs.bottom.MediaTab
import com.owenlejeune.tvtime.ui.screens.tabs.TvTab import com.owenlejeune.tvtime.ui.screens.tabs.bottom.SettingsTab
object NavConstants { object NavConstants {
const val ID_KEY = "id_key" 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}}"), MainNavItem.DetailView.route.plus("/{${NavConstants.TYPE_KEY}}/{${NavConstants.ID_KEY}}"),
arguments = listOf( arguments = listOf(
navArgument(NavConstants.ID_KEY) { type = NavType.IntType }, 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 -> ) { navBackStackEntry ->
displayUnderStatusBar.value = true displayUnderStatusBar.value = true
@@ -41,7 +39,7 @@ fun MainNavigationRoutes(navController: NavHostController, displayUnderStatusBar
DetailView( DetailView(
appNavController = navController, appNavController = navController,
itemId = args?.getInt(NavConstants.ID_KEY), 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 @Composable
fun BottomNavigationRoutes( fun BottomNavigationRoutes(
appNavController: NavController, appNavController: NavHostController,
navController: NavHostController navController: NavHostController
) { ) {
NavHost(navController = navController, startDestination = BottomNavItem.Movies.route) { NavHost(navController = navController, startDestination = BottomNavItem.Movies.route) {
composable(BottomNavItem.Movies.route) { composable(BottomNavItem.Movies.route) {
MoviesTab(appNavController = appNavController) MediaTab(appNavController = appNavController, mediaType = MediaViewType.MOVIE)
} }
composable(BottomNavItem.TV.route) { composable(BottomNavItem.TV.route) {
TvTab(appNavController = appNavController) MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV)
} }
composable(BottomNavItem.Favourites.route) { composable(BottomNavItem.Favourites.route) {
FavouritesTab() FavouritesTab()

View File

@@ -14,7 +14,10 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text 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.Modifier
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -29,11 +32,14 @@ import coil.transform.RoundedCornersTransformation
import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.DetailService import com.owenlejeune.tvtime.api.tmdb.DetailService
import com.owenlejeune.tvtime.api.tmdb.MoviesService 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.TvService
import com.owenlejeune.tvtime.api.tmdb.model.* import com.owenlejeune.tvtime.api.tmdb.model.*
import com.owenlejeune.tvtime.extensions.dpToPx 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -43,11 +49,11 @@ import kotlinx.coroutines.withContext
fun DetailView( fun DetailView(
appNavController: NavController, appNavController: NavController,
itemId: Int?, itemId: Int?,
type: DetailViewType type: MediaViewType
) { ) {
val service = when(type) { val service = when(type) {
DetailViewType.MOVIE -> MoviesService() MediaViewType.MOVIE -> MoviesService()
DetailViewType.TV -> TvService() MediaViewType.TV -> TvService()
} }
val mediaItem = remember { mutableStateOf<DetailedItem?>(null) } val mediaItem = remember { mutableStateOf<DetailedItem?>(null) }
@@ -176,7 +182,7 @@ private fun ContentColumn(modifier: Modifier,
itemId: Int?, itemId: Int?,
mediaItem: MutableState<DetailedItem?>, mediaItem: MutableState<DetailedItem?>,
service: DetailService, service: DetailService,
mediaType: DetailViewType mediaType: MediaViewType
) { ) {
Column( Column(
modifier = modifier modifier = modifier
@@ -185,7 +191,7 @@ private fun ContentColumn(modifier: Modifier,
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
) { ) {
if (mediaType == DetailViewType.MOVIE) { if (mediaType == MediaViewType.MOVIE) {
MiscMovieDetails(mediaItem = mediaItem, service as MoviesService) MiscMovieDetails(mediaItem = mediaItem, service as MoviesService)
} else { } else {
MiscTvDetails(mediaItem = mediaItem, service as TvService) 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.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.google.accompanist.pager.ExperimentalPagerApi
import com.owenlejeune.tvtime.ui.components.SearchFab import com.owenlejeune.tvtime.ui.components.SearchFab
import com.owenlejeune.tvtime.ui.navigation.BottomNavItem import com.owenlejeune.tvtime.ui.navigation.BottomNavItem
import com.owenlejeune.tvtime.ui.navigation.BottomNavigationRoutes import com.owenlejeune.tvtime.ui.navigation.BottomNavigationRoutes
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
@Composable @Composable
fun MainAppView(appNavController: NavController) { fun MainAppView(appNavController: NavHostController) {
val navController = rememberNavController() val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route val currentRoute = navBackStackEntry?.destination?.route
@@ -83,7 +85,12 @@ private fun BottomNavBar(navController: NavController, appBarTitle: MutableState
appBarTitle = appBarTitle, appBarTitle = appBarTitle,
item = item 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.Column
import androidx.compose.foundation.layout.fillMaxSize 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.clickable
import androidx.compose.foundation.layout.Column 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_favourites_title">Favourites</string>
<string name="nav_settings_title">Settings</string> <string name="nav_settings_title">Settings</string>
<string name="cast_label">Cast</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> </resources>