show poster on details screen

This commit is contained in:
Owen LeJeune
2022-02-11 10:27:10 -05:00
parent 5e7a0852de
commit a7ba8cfb4c
21 changed files with 183 additions and 72 deletions

View File

@@ -65,6 +65,7 @@ dependencies {
implementation "com.google.accompanist:accompanist-systemuicontroller:${Versions.compose_accompanist}"
implementation "androidx.navigation:navigation-compose:${Versions.compose_navigation}"
implementation "androidx.paging:paging-compose:${Versions.compose_paging}"
implementation "androidx.constraintlayout:constraintlayout-compose:${Versions.compose_constraint_layout}"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle_runtime}"

View File

@@ -0,0 +1,10 @@
package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
import retrofit2.Response
interface DetailService {
suspend fun getById(id: Int): Response<DetailedItem>
}

View File

@@ -1,8 +1,11 @@
package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.DetailedMovie
import com.owenlejeune.tvtime.api.tmdb.model.PopularMovie
import com.owenlejeune.tvtime.api.tmdb.model.PopularMoviesResponse
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface MoviesApi {
@@ -10,4 +13,7 @@ interface MoviesApi {
@GET("movie/popular")
suspend fun getPopularMovies(@Query("page") page: Int = 1): Response<PopularMoviesResponse>
@GET("movie/{id}")
suspend fun getMovieById(@Path("id") id: Int): Response<DetailedMovie>
}

View File

@@ -1,11 +1,15 @@
package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
import org.koin.core.component.KoinComponent
import retrofit2.Response
class MoviesService: KoinComponent {
class MoviesService: KoinComponent, DetailService {
private val service by lazy { TmdbClient().createMovieService() }
suspend fun getPopularMovies(page: Int = 1) = service.getPopularMovies(page)
override suspend fun getById(id: Int): Response<DetailedItem> = service.getMovieById(id) as Response<DetailedItem>
}

View File

@@ -1,6 +1,6 @@
package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.MediaItem
import com.owenlejeune.tvtime.api.tmdb.model.TmdbItem
object TmdbUtils {
@@ -8,8 +8,8 @@ object TmdbUtils {
return "https://image.tmdb.org/t/p/original${posterPath}"
}
fun getFullPosterPath(mediaItem: MediaItem): String {
return getFullPosterPath(mediaItem.posterPath)
fun getFullPosterPath(tmdbItem: TmdbItem): String {
return getFullPosterPath(tmdbItem.posterPath)
}
}

View File

@@ -1,8 +1,10 @@
package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.PopularTvResponse
import com.owenlejeune.tvtime.api.tmdb.model.DetailedTv
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface TvApi {
@@ -10,4 +12,7 @@ interface TvApi {
@GET("tv/popular")
suspend fun getPoplarTv(@Query("page") page: Int = 1): Response<PopularTvResponse>
@GET("tv/{id}")
suspend fun getTvShowById(@Path("id") id: Int): Response<DetailedTv>
}

View File

@@ -1,10 +1,16 @@
package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
import org.koin.core.component.KoinComponent
import retrofit2.Response
class TvService: KoinComponent {
class TvService: KoinComponent, DetailService {
private val service by lazy { TmdbClient().createTvService() }
suspend fun getPopularTv(page: Int = 1) = service.getPoplarTv(page)
override suspend fun getById(id: Int): Response<DetailedItem> {
return service.getTvShowById(id) as Response<DetailedItem>
}
}

View File

@@ -0,0 +1,3 @@
package com.owenlejeune.tvtime.api.tmdb.model
abstract class DetailedItem(id: Int, title: String, posterPath: String?): TmdbItem(id, title, posterPath)

View File

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

View File

@@ -0,0 +1,9 @@
package com.owenlejeune.tvtime.api.tmdb.model
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
class DetailedTv(
id: Int,
title: String,
posterPath: String?
): DetailedItem(id, title, posterPath)

View File

@@ -1,7 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.model
abstract class MediaItem(
open val posterPath: String?,
@Transient open val title: String,
@Transient open val id: Int
)

View File

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

View File

@@ -3,7 +3,7 @@ package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
data class PopularTv(
@SerializedName("poster_path") override val posterPath: String?,
@SerializedName("name") val name: String,
@SerializedName("id") override val id: Int
): MediaItem(posterPath, name, id)
@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

@@ -0,0 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.model
abstract class TmdbItem(
@Transient open val id: Int,
@Transient open val title: String,
@Transient open val posterPath: String?
)

View File

@@ -1,6 +1,5 @@
package com.owenlejeune.tvtime.ui.components
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
@@ -15,25 +14,29 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.navigation.NavController
import coil.compose.rememberImagePainter
import coil.transform.RoundedCornersTransformation
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.TmdbUtils
import com.owenlejeune.tvtime.api.tmdb.model.MediaItem
import com.owenlejeune.tvtime.api.tmdb.model.TmdbItem
import com.owenlejeune.tvtime.extensions.dpToPx
import com.owenlejeune.tvtime.extensions.listItems
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.ui.screens.DetailViewType
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PosterGrid(
appNavController: NavController,
fetchMedia: (MutableState<List<MediaItem>>) -> Unit
type: DetailViewType,
fetchMedia: (MutableState<List<TmdbItem>>) -> Unit
) {
val mediaList = remember { mutableStateOf(emptyList<MediaItem>()) }
val mediaList = remember { mutableStateOf(emptyList<TmdbItem>()) }
fetchMedia(mediaList)
LazyVerticalGrid(
@@ -41,36 +44,50 @@ fun PosterGrid(
contentPadding = PaddingValues(8.dp)
) {
listItems(mediaList.value) { item ->
PosterItem(appNavController = appNavController, mediaItem = item)
PosterItem(
appNavController = appNavController,
mediaItem = item,
type = type
)
}
}
}
@Composable
fun PosterItem(
modifier: Modifier = Modifier,
appNavController: NavController,
mediaItem: MediaItem
mediaItem: TmdbItem?,
type: DetailViewType? = null,
width: Dp = 127.dp,
height: Dp = 190.dp
) {
val context = LocalContext.current
val poster = TmdbUtils.getFullPosterPath(mediaItem)
val poster = mediaItem?.let { TmdbUtils.getFullPosterPath(mediaItem) }
Image(
painter = rememberImagePainter(
data = poster,
builder = {
transformations(RoundedCornersTransformation(5f.dpToPx(context)))
placeholder(R.drawable.placeholder)
}
),
contentDescription = mediaItem.title,
modifier = Modifier
.size(190.dp)
painter = if (mediaItem != null) {
rememberImagePainter(
data = poster,
builder = {
transformations(RoundedCornersTransformation(5f.dpToPx(context)))
placeholder(R.drawable.placeholder)
}
)
} else {
rememberImagePainter(ContextCompat.getDrawable(context, R.drawable.placeholder))
},
contentDescription = mediaItem?.title,
modifier = modifier
.size(width = width, height = height)
.padding(5.dp)
.clickable {
appNavController.navigate("${MainNavItem.DetailView.route}/${mediaItem.id}")
// appNavController.n
// Toast
// .makeText(context, "${mediaItem.title} clicked", Toast.LENGTH_SHORT)
// .show()
type?.let {
mediaItem?.let {
appNavController.navigate(
"${MainNavItem.DetailView.route}/${type}/${mediaItem.id}"
)
}
}
}
)
}

View File

@@ -6,9 +6,6 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.icons.Icons
@@ -26,20 +23,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.rememberImagePainter
import coil.transform.RoundedCornersTransformation
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.TmdbUtils
import com.owenlejeune.tvtime.api.tmdb.model.MediaItem
import com.owenlejeune.tvtime.extensions.dpToPx
import com.owenlejeune.tvtime.extensions.listItems
@Composable
fun TopLevelSwitch(

View File

@@ -7,7 +7,9 @@ import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.owenlejeune.tvtime.ui.screens.*
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
@@ -20,13 +22,17 @@ fun MainNavigationRoutes(navController: NavHostController) {
MainAppView(appNavController = navController)
}
composable(
MainNavItem.DetailView.route.plus("/{ID_KEY}"),
arguments = listOf(navArgument("ID_KEY") { type = NavType.IntType })
MainNavItem.DetailView.route.plus("/{TYPE_KEY}/{ID_KEY}"),
arguments = listOf(
navArgument("ID_KEY") { type = NavType.IntType },
navArgument("TYPE_KEY") { type = NavType.EnumType(DetailViewType::class.java) }
)
) { navBackStackEntry ->
val args = navBackStackEntry.arguments
DetailView(
appNavController = navController,
itemId = navBackStackEntry.arguments?.getInt("ID_KEY")
itemId = args?.getInt("ID_KEY"),
type = args?.getSerializable("TYPE_KEY") as DetailViewType
)
}
}

View File

@@ -1,28 +1,71 @@
package com.owenlejeune.tvtime.ui.screens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.navigation.NavController
import com.owenlejeune.tvtime.api.tmdb.DetailService
import com.owenlejeune.tvtime.api.tmdb.MoviesService
import com.owenlejeune.tvtime.api.tmdb.TvService
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
import com.owenlejeune.tvtime.ui.components.PosterItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable
fun DetailView(
appNavController: NavController,
itemId: Int?
itemId: Int?,
type: DetailViewType
) {
Column(
modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center)
val mediaItem = remember { mutableStateOf<DetailedItem?>(null) }
val service = when(type) {
DetailViewType.MOVIE -> MoviesService()
DetailViewType.TV -> TvService()
}
itemId?.let {
fetchMediaItem(itemId, service, mediaItem)
}
ConstraintLayout(
modifier = Modifier.fillMaxSize()
) {
Text(
text = itemId.toString(),
color = MaterialTheme.colorScheme.onBackground
val (
posterImage,
title
) = createRefs()
PosterItem(
appNavController = appNavController,
mediaItem = mediaItem.value,
modifier = Modifier
.constrainAs(posterImage) {
top.linkTo(parent.top, margin = 16.dp)
start.linkTo(parent.start, margin = 16.dp)
}
)
}
}
private fun fetchMediaItem(id: Int, service: DetailService, mediaItem: MutableState<DetailedItem?>) {
CoroutineScope(Dispatchers.IO).launch {
val response = service.getById(id)
if (response.isSuccessful) {
withContext(Dispatchers.Main) {
mediaItem.value = response.body()!!
}
}
}
}
enum class DetailViewType {
MOVIE,
TV
}

View File

@@ -5,6 +5,7 @@ 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.screens.DetailViewType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -16,7 +17,7 @@ fun MoviesTab(appNavController: NavController) {
// val moviesViewModel = viewModel(PopularMovieViewModel::class.java)
// val moviesList = moviesViewModel.moviePage
// val movieListItems: LazyPagingItems<PopularMovie> = moviesList.collectAsLazyPagingItems()
PosterGrid(appNavController = appNavController) { moviesList ->
PosterGrid(appNavController = appNavController, type = DetailViewType.MOVIE) { moviesList ->
val service = MoviesService()
CoroutineScope(Dispatchers.IO).launch {
val response = service.getPopularMovies()

View File

@@ -5,6 +5,7 @@ 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.screens.DetailViewType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -13,7 +14,7 @@ import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TvTab(appNavController: NavController) {
PosterGrid(appNavController = appNavController) { tvList ->
PosterGrid(appNavController = appNavController, type = DetailViewType.TV) { tvList ->
val service = TvService()
CoroutineScope(Dispatchers.IO).launch {
val response = service.getPopularTv()

View File

@@ -7,6 +7,7 @@ object Versions {
const val compose_accompanist = "0.22.1-rc"
const val compose_navigation = "2.4.0"
const val compose_paging = "1.0.0-alpha14"
const val compose_constraint_layout = "1.0.0"
const val gradle = "7.1.0"
const val junit = "4.13.2"
const val androidx_junit = "1.1.3"