diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 18a7897..0f91314 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,6 +4,7 @@ import java.util.Properties plugins { alias(libs.plugins.androidApplication) alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.kotlinSerialization) } android { @@ -67,6 +68,24 @@ dependencies { implementation(libs.ui.graphics) implementation(libs.ui.tooling.preview) implementation(libs.material3) + // + implementation(libs.viewmodel.compose) + implementation(libs.compose.ui.util) + + //retrofit + implementation(libs.retrofit) + //okhttp + implementation(platform(libs.okhttp.bom)) + implementation(libs.okhttp) + implementation(libs.okhttp.interceptor) + //kotlin serialization + implementation(libs.kotlin.serialization) + implementation(libs.kotlin.serialization.converter) + //coil + implementation(libs.coil) + implementation(project(mapOf("path" to ":karousel"))) + + //testing libraries testImplementation(libs.junit) androidTestImplementation(libs.androidx.test.ext.junit) androidTestImplementation(libs.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9e57fba..c7c83a6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + +) diff --git a/app/src/main/java/com/pramodbharti/filmo/data/repositories/MoviesRepository.kt b/app/src/main/java/com/pramodbharti/filmo/data/repositories/MoviesRepository.kt new file mode 100644 index 0000000..a7b7750 --- /dev/null +++ b/app/src/main/java/com/pramodbharti/filmo/data/repositories/MoviesRepository.kt @@ -0,0 +1,7 @@ +package com.pramodbharti.filmo.data.repositories + +import com.pramodbharti.filmo.data.network.models.MoviesResponse + +interface MoviesRepository { + suspend fun getDiscoverMovies(): MoviesResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/pramodbharti/filmo/data/repositories/NetworkMoviesRepository.kt b/app/src/main/java/com/pramodbharti/filmo/data/repositories/NetworkMoviesRepository.kt new file mode 100644 index 0000000..d0728f9 --- /dev/null +++ b/app/src/main/java/com/pramodbharti/filmo/data/repositories/NetworkMoviesRepository.kt @@ -0,0 +1,7 @@ +package com.pramodbharti.filmo.data.repositories + +import com.pramodbharti.filmo.data.network.MoviesApiService + +class NetworkMoviesRepository(private val moviesApiService: MoviesApiService) : MoviesRepository { + override suspend fun getDiscoverMovies() = moviesApiService.getDiscoverMovies() +} \ No newline at end of file diff --git a/app/src/main/java/com/pramodbharti/filmo/ui/Constants.kt b/app/src/main/java/com/pramodbharti/filmo/ui/Constants.kt new file mode 100644 index 0000000..ca88317 --- /dev/null +++ b/app/src/main/java/com/pramodbharti/filmo/ui/Constants.kt @@ -0,0 +1,7 @@ +package com.pramodbharti.filmo.ui + +object Constants { + const val IMAGE_URL_ORIGINAL = "https://image.tmdb.org/t/p/original" + const val IMAGE_URL_500 = "https://image.tmdb.org/t/p/w500" + const val IMAGE_URL_300 = "https://image.tmdb.org/t/p/w300" +} \ No newline at end of file diff --git a/app/src/main/java/com/pramodbharti/filmo/ui/components/Utils.kt b/app/src/main/java/com/pramodbharti/filmo/ui/components/Utils.kt new file mode 100644 index 0000000..2067ae6 --- /dev/null +++ b/app/src/main/java/com/pramodbharti/filmo/ui/components/Utils.kt @@ -0,0 +1,25 @@ +package com.pramodbharti.filmo.ui.components + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.pager.PagerState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.util.lerp +import kotlin.math.absoluteValue + +@OptIn(ExperimentalFoundationApi::class) +fun Modifier.carouselTransition(page: Int, pagerState: PagerState) = + graphicsLayer { + val pageOffset = + ((pagerState.currentPage - page) + pagerState.currentPageOffsetFraction).absoluteValue + + val transformation = + lerp( + start = 0.7f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn(0f, 1f) + ) + alpha = transformation + scaleY = transformation + } + diff --git a/app/src/main/java/com/pramodbharti/filmo/ui/models/MovieItem.kt b/app/src/main/java/com/pramodbharti/filmo/ui/models/MovieItem.kt new file mode 100644 index 0000000..657ba52 --- /dev/null +++ b/app/src/main/java/com/pramodbharti/filmo/ui/models/MovieItem.kt @@ -0,0 +1,15 @@ +package com.pramodbharti.filmo.ui.models + +import androidx.annotation.DrawableRes + +data class MovieItem( + val id: Int, + val title: String, + @DrawableRes + val poster: Int, + @DrawableRes + val backdrop: Int, + val releaseDate: String +) + + diff --git a/app/src/main/java/com/pramodbharti/filmo/ui/screens/FilmoApp.kt b/app/src/main/java/com/pramodbharti/filmo/ui/screens/FilmoApp.kt new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/java/com/pramodbharti/filmo/ui/screens/details/Cast.kt b/app/src/main/java/com/pramodbharti/filmo/ui/screens/details/Cast.kt new file mode 100644 index 0000000..9bf2ab2 --- /dev/null +++ b/app/src/main/java/com/pramodbharti/filmo/ui/screens/details/Cast.kt @@ -0,0 +1,9 @@ +package com.pramodbharti.filmo.ui.screens.details + +import androidx.annotation.DrawableRes + +data class Cast( + val name: String, + @DrawableRes + val photo: Int +) diff --git a/app/src/main/java/com/pramodbharti/filmo/ui/screens/details/DetailsScreen.kt b/app/src/main/java/com/pramodbharti/filmo/ui/screens/details/DetailsScreen.kt new file mode 100644 index 0000000..b863401 --- /dev/null +++ b/app/src/main/java/com/pramodbharti/filmo/ui/screens/details/DetailsScreen.kt @@ -0,0 +1,297 @@ +package com.pramodbharti.filmo.ui.screens.details + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.pramodbharti.filmo.R +import com.pramodbharti.filmo.ui.models.MovieItem +import com.pramodbharti.filmo.ui.screens.home.MoviePosterList +import com.pramodbharti.filmo.ui.screens.home.MoviesScreen +import com.pramodbharti.filmo.ui.screens.home.MoviesSection +import com.pramodbharti.filmo.ui.screens.home.dummyMovies +import com.pramodbharti.filmo.ui.theme.FilmoTheme + +@Composable +fun ItemDetailsScreen(movieItem: MovieItem, modifier: Modifier = Modifier) { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + ItemDetails(movieItem = movieItem) + MoviesSection(title = "Cast") { + CastItemRow(casts = dummyCastData) + } + MoviesSection(title = "Similar") { + MoviePosterList(movies = dummyMovies) + } + MoviesSection(title = "Recommended for you") { + MoviePosterList(movies = dummyMovies) + } + } +} + +@Preview(showBackground = true) +@Composable +fun ItemDetailsScreen() { + FilmoTheme { + ItemDetailsScreen(movieItem = dummyMovies[1]) + } +} + +@Composable +fun ItemDetails(movieItem: MovieItem, modifier: Modifier = Modifier) { + Box(modifier) { + AsyncImage( + model = ImageRequest + .Builder(context = LocalContext.current) + .data(movieItem.backdrop) + .crossfade(true) + .build(), + placeholder = painterResource(id = movieItem.backdrop), + error = painterResource(id = movieItem.backdrop), + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + ) + + ItemDetailsSection( + movieItem = movieItem, + modifier = Modifier.align(Alignment.BottomCenter) + ) + } +} + +@Composable +fun ItemDetailsSection(movieItem: MovieItem, modifier: Modifier = Modifier) { + Column( + modifier = modifier.background( + brush = Brush.verticalGradient( + colorStops = arrayOf( + 0.0f to Color.Black.copy(alpha = 0.0f), + 0.1f to Color.Black.copy(alpha = 0.5f), + 0.2f to Color.Black.copy(alpha = 0.7f), + 0.7f to Color.Black.copy(alpha = 0.9f), + 1f to Color.Black.copy(alpha = 1f) + ) + ) + ) + ) { + TitleAndFavorite(title = movieItem.title) + TagItemsRow(tags = dummyGenreList) + AboutSection() + } +} + +@Composable +fun ActionButton(modifier: Modifier = Modifier) { +Row { + +} +} + +@Composable +fun TitleAndFavorite(title: String, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = Color.LightGray, + modifier = Modifier.weight(1f), + fontWeight = FontWeight.Bold + ) + IconButton(onClick = { /*TODO*/ }) { + Icon( + painter = painterResource(id = R.drawable.baseline_favorite), + contentDescription = null, + tint = Color.White, + modifier = Modifier + .background( + color = Color.Gray.copy(alpha = 0.5f), + shape = CircleShape + ) + .padding(8.dp) + .size(20.dp) + ) + } + } +} + +@Composable +fun TagItemsRow(tags: List, modifier: Modifier = Modifier) { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier, + contentPadding = PaddingValues( + start = 16.dp, + end = 16.dp, + bottom = 8.dp + ) + ) { + items(tags) { + TagItem(tagName = it) + } + } +} + +@Composable +fun TagItem(tagName: String, modifier: Modifier = Modifier) { + Text( + text = tagName, + style = MaterialTheme.typography.bodySmall, + color = Color.White, + fontSize = 10.sp, + modifier = modifier + .background(Color.DarkGray) + .padding(horizontal = 4.dp, vertical = 2.dp) + ) +} + +@Composable +fun AboutSection(modifier: Modifier = Modifier) { + Text( + text = "How to scroll view pager (accompanist library) on button click in jetpack compose Android".repeat( + 3 + ), + style = MaterialTheme.typography.titleMedium, + color = Color.LightGray, + fontSize = 10.sp, + fontWeight = FontWeight.Light, + modifier = modifier.padding(start = 16.dp, bottom = 16.dp), + lineHeight = 18.sp + ) +} + +@Composable +fun CastItemRow(casts: List, modifier: Modifier = Modifier) { + LazyRow( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) + ) { + items(casts) { + CastItem(cast = it) + } + } +} + +@Composable +fun CastItem(cast: Cast, modifier: Modifier = Modifier) { + Column( + modifier = modifier.width(80.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AsyncImage( + model = ImageRequest + .Builder(context = LocalContext.current) + .data(cast.photo) + .crossfade(true) + .build(), + placeholder = painterResource(id = cast.photo), + error = painterResource(id = cast.photo), + contentScale = ContentScale.Crop, + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .height(80.dp) + ) + Text( + text = cast.name, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center + ) + } +} + +@Preview +@Composable +fun ItemDetailsPreview() { + ItemDetails(movieItem = dummyMovies[0]) +} + +@Preview(showBackground = true) +@Composable +fun TitleAndFavoritePreview() { + TitleAndFavorite(title = dummyGenreList[0]) +} + +@Preview(showBackground = true) +@Composable +fun TagItemsRowPreview() { + TagItemsRow(tags = dummyGenreList) +} + +@Preview(showBackground = true) +@Composable +fun AboutSectionPReview() { + AboutSection() +} + +@Preview(showBackground = true) +@Composable +fun CastItemRowPreview() { + CastItemRow(casts = dummyCastData) +} + +@Preview(showBackground = true) +@Composable +fun CastItemPreview() { + CastItem(cast = dummyCastData[0]) +} + +val dummyGenreList = listOf( + "Adventure", + "Action", + "Animation", + "Documentary", + "Fantasy", + "History", + "Science Fiction" +) + +val dummyCastData = listOf( + Cast("Pramod Bharti", R.drawable.dddd), + Cast("Konark Chakra", R.drawable.placeholder), + Cast("Unknown Profile", R.drawable.profile_picture), + Cast("Pramod Bharti", R.drawable.dddd), + Cast("Konark Chakra", R.drawable.placeholder), + Cast("Unknown Profile", R.drawable.profile_picture) +) \ No newline at end of file diff --git a/app/src/main/java/com/pramodbharti/filmo/ui/screens/home/FilmoCarousel.kt b/app/src/main/java/com/pramodbharti/filmo/ui/screens/home/FilmoCarousel.kt new file mode 100644 index 0000000..17acbc2 --- /dev/null +++ b/app/src/main/java/com/pramodbharti/filmo/ui/screens/home/FilmoCarousel.kt @@ -0,0 +1,165 @@ +package com.pramodbharti.filmo.ui.screens.home + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.collectIsDraggedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.pramodbharti.filmo.ui.components.carouselTransition +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun FilmoCarousel( + itemsCount: Int, + modifier: Modifier = Modifier, + indicatorShape: Shape = CircleShape, + indicatorSize: Dp = 6.dp, + selectedColor: Color = Color.White, + unSelectedColor: Color = Color.DarkGray, + indicatorBackgroundColor: Color = Color.Transparent, + indicatorBackgroundShape: Shape = CircleShape, + indicatorPadding: PaddingValues = PaddingValues(bottom = 28.dp), + indicatorAlignment: Alignment = Alignment.BottomCenter, + autoSlideDuration: Long = 2000L, + pagerState: PagerState = rememberPagerState { itemsCount }, + itemContent: @Composable (index: Int, pagerState: PagerState) -> Unit +) { + val isDragged by pagerState.interactionSource.collectIsDraggedAsState() + if (isDragged.not()) { + with(pagerState) { + var currentPageKey by remember { mutableIntStateOf(0) } + LaunchedEffect(key1 = currentPageKey) { + launch { + delay(autoSlideDuration) + val nextPage = (currentPage + 1).mod(itemsCount) + pagerState.animateScrollToPage(nextPage) + currentPageKey = nextPage + } + } + } + } + + Box(modifier = modifier.fillMaxWidth()) { + HorizontalPager( + state = pagerState + ) { page -> + itemContent(page, pagerState) + } + + Surface( + modifier = Modifier + .align(indicatorAlignment) + .padding(indicatorPadding), + shape = indicatorBackgroundShape, + color = indicatorBackgroundColor + ) { + CarouselIndicators( + totalDots = itemsCount, + selectedIndex = if (isDragged) + pagerState.currentPage + else + pagerState.targetPage, + selectedColor = selectedColor, + unSelectedColor = unSelectedColor, + shape = indicatorShape, + dotSize = indicatorSize, + modifier = Modifier.padding(6.dp) + ) + } + + } + + +} + +@Composable +fun CarouselIndicators( + totalDots: Int, + selectedIndex: Int, + selectedColor: Color, + unSelectedColor: Color, + shape: Shape, + dotSize: Dp, + modifier: Modifier = Modifier +) { + LazyRow(modifier = modifier) { + items(totalDots) { dot -> + CarouselIndicator( + size = dotSize, + color = if (dot == selectedIndex) selectedColor else unSelectedColor, + shape = shape + ) + + if (dot != totalDots - 1) { + Spacer(modifier = Modifier.padding(horizontal = 2.dp)) + } + } + } +} + +@Composable +fun CarouselIndicator( + size: Dp, + color: Color, + shape: Shape, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .size(size) + .clip(shape) + .background(color) + ) +} + +@OptIn(ExperimentalFoundationApi::class) +@Preview +@Composable +fun FilmoCarouselPreview() { + + FilmoCarousel(itemsCount = dummyMovies.size) { index, pagerSate -> + CarouselItem( + movieItem = dummyMovies[0], + modifier = Modifier.carouselTransition(index, pagerState = pagerSate) + ) + } +} + +@Preview +@Composable +fun ComposeIndicatorPreview() { + CarouselIndicator(8.dp, Color.Red, CircleShape) +} + +@Preview +@Composable +fun ComposeIndicatorsPreview() { + CarouselIndicators(5, 3, Color.Red, Color.White, CircleShape, 8.dp) +} \ No newline at end of file diff --git a/app/src/main/java/com/pramodbharti/filmo/ui/screens/home/HomeScreen.kt b/app/src/main/java/com/pramodbharti/filmo/ui/screens/home/HomeScreen.kt new file mode 100644 index 0000000..4098e3e --- /dev/null +++ b/app/src/main/java/com/pramodbharti/filmo/ui/screens/home/HomeScreen.kt @@ -0,0 +1,263 @@ +package com.pramodbharti.filmo.ui.screens.home + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.paddingFromBaseline +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.pramodbharti.filmo.R +import com.pramodbharti.filmo.ui.Constants +import com.pramodbharti.filmo.ui.models.MovieItem +import com.pramodbharti.filmo.ui.theme.FilmoTheme + + +@Composable +fun CarouselItem(movieItem: MovieItem, modifier: Modifier = Modifier) { + Card( + modifier = modifier, + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), + shape = RectangleShape + ) { + Box { + AsyncImage( + model = ImageRequest.Builder(context = LocalContext.current) + .data("${Constants.IMAGE_URL_500}${movieItem.poster}") + .crossfade(true) + .build(), + contentDescription = movieItem.title, + contentScale = ContentScale.Crop, + placeholder = painterResource(id = movieItem.backdrop), + error = painterResource(id = movieItem.backdrop), + modifier = Modifier + .height(230.dp) + .fillMaxWidth() + ) + + Column( + modifier = Modifier + .align(Alignment.BottomStart) + .background( + brush = Brush.verticalGradient( + colorStops = arrayOf( + 0.0f to Color.Black.copy(alpha = 0.0f), + 0.6f to Color.Black.copy(alpha = 0.8f), + 1f to Color.Black.copy(alpha = 0.8f) + ) + ) + ) + .fillMaxWidth() + ) { + Text( + text = "John Wick 4", + style = TextStyle( + color = Color.White, + fontWeight = FontWeight.Bold + ), + modifier = Modifier.padding(start = 16.dp, top = 16.dp) + ) + Text( + text = "Crime, Thriller", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + modifier = Modifier.padding(start = 16.dp, bottom = 16.dp) + ) + } + + } + } +} + + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun MoviesScreen(modifier: Modifier = Modifier) { + Column(modifier.verticalScroll(rememberScrollState())) { + FilmoCarousel(itemsCount = dummyMovies.size) { index, pagerState -> + CarouselItem( + movieItem = dummyMovies[index] + ) + } + MoviesSection(title = "Trending") { + MoviePosterList(movies = dummyMovies) + } + MoviesSection(title = "Popular") { + MoviePosterList(movies = dummyMovies) + } + MoviesSection(title = "Trending") { + MoviePosterList(movies = dummyMovies) + } + MoviesSection(title = "Popular") { + MoviePosterList(movies = dummyMovies) + } + } +} + +@Composable +fun MoviesSection( + title: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Column(modifier) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .paddingFromBaseline(top = 32.dp) + .padding(horizontal = 16.dp) + ) + content() + } +} + +@Composable +fun MoviePosterList(movies: List, modifier: Modifier = Modifier) { + LazyRow( + modifier = modifier, + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(movies) { movie -> + MoviePoster(movieItem = movie) + } + } +} + +@Composable +fun MoviePoster(movieItem: MovieItem, modifier: Modifier = Modifier) { + Card( + modifier = modifier, + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) + ) { + AsyncImage( + model = ImageRequest.Builder(context = LocalContext.current) + .data("${Constants.IMAGE_URL_500}${movieItem.poster}") + .crossfade(true) + .build(), + contentDescription = movieItem.title, + contentScale = ContentScale.Crop, + placeholder = painterResource(id = movieItem.poster), + error = painterResource(id = movieItem.poster), + modifier = Modifier.size(150.dp, 230.dp) + ) + } +} + +@Preview(showBackground = true) +@Composable +fun CarouselItemPReview() { + FilmoTheme { + CarouselItem(movieItem = dummyMovies[1]) + } +} + +@Preview(showBackground = true, showSystemUi = true, uiMode = UI_MODE_NIGHT_YES) +@Composable +fun MoviesScreenPreview() { + FilmoTheme { + MoviesScreen() + } +} + +@Preview(showBackground = true) +@Composable +fun MovieSectionPreview() { + FilmoTheme { + MoviesSection(title = "Trending") { + MoviePosterList(movies = dummyMovies) + } + } +} + +@Preview(showBackground = true) +@Composable +fun MoviePosterListPreview() { + FilmoTheme { + MoviePosterList( + movies = dummyMovies + ) + } +} + +@Preview(showBackground = true) +@Composable +fun MoviePosterPreview() { + FilmoTheme { + MoviePoster( + movieItem = dummyMovies[0] + ) + } +} + +val dummyMovies: List = listOf( + MovieItem( + 123, + "Testing one", + R.drawable.poster1, + R.drawable.back_drop1, + "" + ), MovieItem( + 133, + "Testing one", + R.drawable.poster2, + R.drawable.back_drop2, + "" + ), MovieItem( + 1230, + "Testing one", + R.drawable.poster1, + R.drawable.back_drop1, + "" + ), + MovieItem( + 123, + "Testing one", + R.drawable.poster1, + R.drawable.back_drop1, + "" + ), MovieItem( + 133, + "Testing one", + R.drawable.poster2, + R.drawable.back_drop2, + "" + ), MovieItem( + 1230, + "Testing one", + R.drawable.poster1, + R.drawable.back_drop1, + "" + ) +) + + diff --git a/app/src/main/java/com/pramodbharti/filmo/ui/screens/home/HomeViewModel.kt b/app/src/main/java/com/pramodbharti/filmo/ui/screens/home/HomeViewModel.kt new file mode 100644 index 0000000..33c36c9 --- /dev/null +++ b/app/src/main/java/com/pramodbharti/filmo/ui/screens/home/HomeViewModel.kt @@ -0,0 +1,60 @@ +package com.pramodbharti.filmo.ui.screens.home + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import com.pramodbharti.filmo.FilmoApplication +import com.pramodbharti.filmo.R +import com.pramodbharti.filmo.data.network.models.MovieResponse +import com.pramodbharti.filmo.data.network.models.MoviesResponse +import com.pramodbharti.filmo.data.repositories.MoviesRepository +import com.pramodbharti.filmo.ui.models.MovieItem +import kotlinx.coroutines.launch +import java.io.IOException + +class HomeViewModel(private val moviesRepository: MoviesRepository) : ViewModel() { + var movieUiState: MovieUiState by mutableStateOf(MovieUiState.Loading) + private set + + init { + getMovies() + } + + private fun getMovies() { + viewModelScope.launch { + movieUiState = try { + val movieList = moviesRepository.getDiscoverMovies() + MovieUiState.Success(movieList.results.map { it.toMovieItem() }) + } catch (e: IOException) { + MovieUiState.Error + } + } + } + + companion object { + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val application = (this[APPLICATION_KEY] as FilmoApplication) + val moviesRepository = application.container.moviesRepository + HomeViewModel(moviesRepository) + } + } + } + + + private fun MovieResponse.toMovieItem(): MovieItem = + MovieItem( + id = id, + title = title, + poster = R.drawable.poster1, + backdrop = R.drawable.back_drop1, + releaseDate = releaseDate + ) +} + diff --git a/app/src/main/java/com/pramodbharti/filmo/ui/screens/home/MovieUiState.kt b/app/src/main/java/com/pramodbharti/filmo/ui/screens/home/MovieUiState.kt new file mode 100644 index 0000000..6f2aff7 --- /dev/null +++ b/app/src/main/java/com/pramodbharti/filmo/ui/screens/home/MovieUiState.kt @@ -0,0 +1,9 @@ +package com.pramodbharti.filmo.ui.screens.home + +import com.pramodbharti.filmo.ui.models.MovieItem + +sealed interface MovieUiState { + data class Success(val movies: List): MovieUiState + object Error : MovieUiState + object Loading : MovieUiState +} \ No newline at end of file diff --git a/app/src/main/java/com/pramodbharti/filmo/ui/theme/Theme.kt b/app/src/main/java/com/pramodbharti/filmo/ui/theme/Theme.kt index 7cd8749..fe85427 100644 --- a/app/src/main/java/com/pramodbharti/filmo/ui/theme/Theme.kt +++ b/app/src/main/java/com/pramodbharti/filmo/ui/theme/Theme.kt @@ -4,12 +4,14 @@ import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView diff --git a/app/src/main/res/drawable-nodpi/back_drop1.jpg b/app/src/main/res/drawable-nodpi/back_drop1.jpg new file mode 100644 index 0000000..2e80826 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/back_drop1.jpg differ diff --git a/app/src/main/res/drawable-nodpi/back_drop2.jpg b/app/src/main/res/drawable-nodpi/back_drop2.jpg new file mode 100644 index 0000000..945d777 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/back_drop2.jpg differ diff --git a/app/src/main/res/drawable-nodpi/poster1.jpg b/app/src/main/res/drawable-nodpi/poster1.jpg new file mode 100644 index 0000000..a1971f9 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/poster1.jpg differ diff --git a/app/src/main/res/drawable-nodpi/poster2.jpg b/app/src/main/res/drawable-nodpi/poster2.jpg new file mode 100644 index 0000000..8296e09 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/poster2.jpg differ diff --git a/app/src/main/res/drawable/baseline_favorite.xml b/app/src/main/res/drawable/baseline_favorite.xml new file mode 100644 index 0000000..59a05be --- /dev/null +++ b/app/src/main/res/drawable/baseline_favorite.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_share.xml b/app/src/main/res/drawable/baseline_share.xml new file mode 100644 index 0000000..becdc03 --- /dev/null +++ b/app/src/main/res/drawable/baseline_share.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_smart.xml b/app/src/main/res/drawable/baseline_smart.xml new file mode 100644 index 0000000..a4a0a73 --- /dev/null +++ b/app/src/main/res/drawable/baseline_smart.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_thumb.xml b/app/src/main/res/drawable/baseline_thumb.xml new file mode 100644 index 0000000..f6de54a --- /dev/null +++ b/app/src/main/res/drawable/baseline_thumb.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/dddd.jpg b/app/src/main/res/drawable/dddd.jpg new file mode 100644 index 0000000..fb3bec0 Binary files /dev/null and b/app/src/main/res/drawable/dddd.jpg differ diff --git a/app/src/main/res/drawable/ic_broken_image.xml b/app/src/main/res/drawable/ic_broken_image.xml new file mode 100644 index 0000000..c3b995b --- /dev/null +++ b/app/src/main/res/drawable/ic_broken_image.xml @@ -0,0 +1,25 @@ + + + + diff --git a/app/src/main/res/drawable/ic_connection_error.xml b/app/src/main/res/drawable/ic_connection_error.xml new file mode 100644 index 0000000..a961d63 --- /dev/null +++ b/app/src/main/res/drawable/ic_connection_error.xml @@ -0,0 +1,25 @@ + + + + diff --git a/app/src/main/res/drawable/loading_img.xml b/app/src/main/res/drawable/loading_img.xml new file mode 100644 index 0000000..0b64932 --- /dev/null +++ b/app/src/main/res/drawable/loading_img.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/placeholder.jpeg b/app/src/main/res/drawable/placeholder.jpeg new file mode 100644 index 0000000..7c209e5 Binary files /dev/null and b/app/src/main/res/drawable/placeholder.jpeg differ diff --git a/app/src/main/res/drawable/profile_picture.png b/app/src/main/res/drawable/profile_picture.png new file mode 100644 index 0000000..eab3c65 Binary files /dev/null and b/app/src/main/res/drawable/profile_picture.png differ diff --git a/build.gradle.kts b/build.gradle.kts index 20d87a7..2eda13e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,5 +3,6 @@ plugins { alias(libs.plugins.androidApplication) apply false alias(libs.plugins.kotlinAndroid) apply false + alias(libs.plugins.com.android.library) apply false } true // Needed to make the Suppress annotation work for the plugins block \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cc59e08..1857ca3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,15 @@ androidx-test-ext-junit = "1.1.5" espresso-core = "3.5.1" lifecycle-runtime-ktx = "2.6.2" activity-compose = "1.7.2" -compose-bom = "2023.08.00" +compose-bom = "2023.09.01" +# +retrofit = "2.9.0" +kotlin-serialization = "1.6.0" +okhttp-bom = "4.11.0" +kotlin-serialization-converter = "1.0.0" +coil = "2.4.0" +appcompat = "1.6.1" +material = "1.9.0" [libraries] core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } @@ -24,8 +32,23 @@ ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } material3 = { group = "androidx.compose.material3", name = "material3" } +# +compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" } +viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle-runtime-ktx" } +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +kotlin-serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlin-serialization" } +okhttp-bom = { group = "com.squareup.okhttp3", name = "okhttp-bom", version.ref = "okhttp-bom" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp" } +okhttp-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor" } +kotlin-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "kotlin-serialization-converter" } +coil = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } +appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +# +kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +com-android-library = { id = "com.android.library", version.ref = "agp" } diff --git a/karousel/.gitignore b/karousel/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/karousel/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/karousel/build.gradle.kts b/karousel/build.gradle.kts new file mode 100644 index 0000000..f68f3ff --- /dev/null +++ b/karousel/build.gradle.kts @@ -0,0 +1,44 @@ +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + alias(libs.plugins.com.android.library) + alias(libs.plugins.kotlinAndroid) +} + +android { + namespace = "com.pramodbharti.karousel" + compileSdk = 33 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation(libs.core.ktx) + implementation(libs.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.espresso.core) +} \ No newline at end of file diff --git a/karousel/consumer-rules.pro b/karousel/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/karousel/proguard-rules.pro b/karousel/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/karousel/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/karousel/src/androidTest/java/com/pramodbharti/karousel/ExampleInstrumentedTest.kt b/karousel/src/androidTest/java/com/pramodbharti/karousel/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..9b46b67 --- /dev/null +++ b/karousel/src/androidTest/java/com/pramodbharti/karousel/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.pramodbharti.karousel + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.pramodbharti.karousel.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/karousel/src/main/AndroidManifest.xml b/karousel/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/karousel/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/karousel/src/test/java/com/pramodbharti/karousel/ExampleUnitTest.kt b/karousel/src/test/java/com/pramodbharti/karousel/ExampleUnitTest.kt new file mode 100644 index 0000000..21919af --- /dev/null +++ b/karousel/src/test/java/com/pramodbharti/karousel/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.pramodbharti.karousel + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index cbee2fc..8e3586c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,4 +21,4 @@ dependencyResolutionManagement { rootProject.name = "Filmo" include(":app") - \ No newline at end of file +include(":karousel")