├── data ├── .gitignore ├── consumer-rules.pro ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── ang │ │ │ └── acb │ │ │ └── movienight │ │ │ └── data │ │ │ ├── source │ │ │ ├── remote │ │ │ │ ├── response │ │ │ │ │ ├── NetworkGenre.kt │ │ │ │ │ ├── NetworkVideo.kt │ │ │ │ │ ├── NetworkCast.kt │ │ │ │ │ ├── CreditsResponse.kt │ │ │ │ │ ├── MoviesResponse.kt │ │ │ │ │ ├── VideosResponse.kt │ │ │ │ │ ├── NetworkCastDetails.kt │ │ │ │ │ ├── NetworkMovie.kt │ │ │ │ │ └── NetworkMovieDetails.kt │ │ │ │ ├── RemoteMovieDataSource.kt │ │ │ │ ├── MovieService.kt │ │ │ │ └── RemoteMovieMappers.kt │ │ │ └── local │ │ │ │ ├── MovieDatabase.kt │ │ │ │ ├── MovieMappers.kt │ │ │ │ ├── FavoriteMovie.kt │ │ │ │ ├── MovieDao.kt │ │ │ │ └── LocalMovieDataSource.kt │ │ │ └── MovieRepository.kt │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── ang │ │ │ └── acb │ │ │ └── movienight │ │ │ └── data │ │ │ ├── utils │ │ │ └── MainCoroutineRule.kt │ │ │ └── source │ │ │ └── local │ │ │ ├── MovieDaoTest.kt │ │ │ └── LocalMovieDataSourceTest.kt │ └── test │ │ ├── resources │ │ └── response │ │ │ └── cast_details_alpacino.json │ │ └── java │ │ └── com │ │ └── ang │ │ └── acb │ │ └── movienight │ │ └── data │ │ └── source │ │ └── remote │ │ └── MovieServiceTest.kt ├── proguard-rules.pro └── build.gradle.kts ├── domain ├── .gitignore ├── src │ └── main │ │ └── java │ │ └── com │ │ └── ang │ │ └── acb │ │ └── movienight │ │ └── domain │ │ ├── entities │ │ ├── Genre.kt │ │ ├── MovieFilter.kt │ │ ├── Movies.kt │ │ ├── MovieDetails.kt │ │ ├── Cast.kt │ │ ├── CastDetails.kt │ │ ├── Movie.kt │ │ └── Trailer.kt │ │ ├── usecases │ │ ├── DeleteFavoriteMovieUseCase.kt │ │ ├── GetPopularMoviesUseCase.kt │ │ ├── SaveFavoriteMovieUseCase.kt │ │ ├── GetTopRatedMoviesUseCase.kt │ │ ├── GetUpcomingMoviesUseCase.kt │ │ ├── GetCastDetailsUseCase.kt │ │ ├── GetNowPlayingMoviesUseCase.kt │ │ ├── SearchMoviesUseCase.kt │ │ ├── GetMovieDetailsUseCase.kt │ │ ├── GetSimilarMoviesUseCase.kt │ │ ├── GetAllFavoriteMoviesUseCase.kt │ │ ├── GetFavoriteMovieUseCase.kt │ │ └── GetFilteredMoviesUseCase.kt │ │ ├── utils │ │ └── Constants.kt │ │ └── gateways │ │ └── MovieGateway.kt └── build.gradle.kts ├── .idea ├── .gitignore ├── compiler.xml ├── vcs.xml ├── misc.xml └── gradle.xml ├── settings.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── app ├── src │ ├── main │ │ ├── res │ │ │ ├── font │ │ │ │ ├── poppins_bold.ttf │ │ │ │ ├── poppins_light.ttf │ │ │ │ ├── poppins_medium.ttf │ │ │ │ └── poppins_regular.ttf │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ ├── themes.xml │ │ │ │ └── strings.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── ang │ │ │ │ └── acb │ │ │ │ └── movienight │ │ │ │ ├── ui │ │ │ │ ├── theme │ │ │ │ │ ├── Shape.kt │ │ │ │ │ ├── Color.kt │ │ │ │ │ ├── Theme.kt │ │ │ │ │ └── Type.kt │ │ │ │ ├── common │ │ │ │ │ ├── MovieFilterMap.kt │ │ │ │ │ ├── PagingLoadingView.kt │ │ │ │ │ ├── LoadingBox.kt │ │ │ │ │ ├── PagingLoadingItem.kt │ │ │ │ │ ├── MessageBox.kt │ │ │ │ │ ├── MovieItem.kt │ │ │ │ │ ├── PagingErrorMessage.kt │ │ │ │ │ ├── PagingErrorItem.kt │ │ │ │ │ ├── Carousel.kt │ │ │ │ │ ├── MoviePoster.kt │ │ │ │ │ └── MovieItemDetails.kt │ │ │ │ ├── details │ │ │ │ │ ├── CastCard.kt │ │ │ │ │ ├── MovieInfoHeader.kt │ │ │ │ │ ├── CastCarousel.kt │ │ │ │ │ ├── TrailerCarousel.kt │ │ │ │ │ ├── MovieInfoBackdrop.kt │ │ │ │ │ ├── CastDetailsTopBar.kt │ │ │ │ │ ├── SimilarMoviesCarousel.kt │ │ │ │ │ ├── CastDetailsViewModel.kt │ │ │ │ │ ├── TrailerCard.kt │ │ │ │ │ ├── CastProfileImage.kt │ │ │ │ │ ├── MovieInfoPosterRow.kt │ │ │ │ │ ├── MovieDetailsTopBar.kt │ │ │ │ │ ├── MovieInfoRating.kt │ │ │ │ │ ├── CastInfoAvatarRow.kt │ │ │ │ │ ├── CastDetailsScreen.kt │ │ │ │ │ ├── MovieDetailsViewModel.kt │ │ │ │ │ └── MovieDetailsScreen.kt │ │ │ │ ├── search │ │ │ │ │ ├── SearchMoviesPagingSource.kt │ │ │ │ │ ├── SearchResultEmpty.kt │ │ │ │ │ ├── SearchMoviesViewModel.kt │ │ │ │ │ ├── SearchMoviesScreen.kt │ │ │ │ │ ├── SearchMoviesTextField.kt │ │ │ │ │ └── SearchMoviesResults.kt │ │ │ │ ├── filter │ │ │ │ │ ├── FilterMoviesPagingSource.kt │ │ │ │ │ ├── FilterMoviesViewModel.kt │ │ │ │ │ ├── FilterMoviesTopBar.kt │ │ │ │ │ └── FilterMoviesScreen.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── favorites │ │ │ │ │ ├── FavoritesViewModel.kt │ │ │ │ │ └── FavoriteMoviesScreen.kt │ │ │ │ └── main │ │ │ │ │ ├── MainScreen.kt │ │ │ │ │ ├── MovieNightScreens.kt │ │ │ │ │ ├── MoviesBottomBar.kt │ │ │ │ │ └── MoviesNavHost.kt │ │ │ │ ├── MovieNightApplication.kt │ │ │ │ └── di │ │ │ │ ├── RepoModule.kt │ │ │ │ ├── RoomModule.kt │ │ │ │ └── NetworkModule.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── ang │ │ │ └── acb │ │ │ └── movienight │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── ang │ │ └── acb │ │ └── movienight │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro ├── .gitignore └── build.gradle.kts ├── gradle.properties ├── .gitignore ├── README.md ├── gradlew.bat └── gradlew /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "MovieNight" 2 | include(":app", ":domain", ":data") 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angela-aciobanitei/kotlin-movie-night/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angela-aciobanitei/kotlin-movie-night/HEAD/app/src/main/res/font/poppins_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angela-aciobanitei/kotlin-movie-night/HEAD/app/src/main/res/font/poppins_light.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angela-aciobanitei/kotlin-movie-night/HEAD/app/src/main/res/font/poppins_medium.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angela-aciobanitei/kotlin-movie-night/HEAD/app/src/main/res/font/poppins_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angela-aciobanitei/kotlin-movie-night/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angela-aciobanitei/kotlin-movie-night/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angela-aciobanitei/kotlin-movie-night/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angela-aciobanitei/kotlin-movie-night/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angela-aciobanitei/kotlin-movie-night/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angela-aciobanitei/kotlin-movie-night/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angela-aciobanitei/kotlin-movie-night/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angela-aciobanitei/kotlin-movie-night/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angela-aciobanitei/kotlin-movie-night/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angela-aciobanitei/kotlin-movie-night/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/entities/Genre.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.entities 2 | 3 | data class Genre( 4 | val id: Long, 5 | val movieId: Long, 6 | val name: String? 7 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/entities/MovieFilter.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.entities 2 | 3 | enum class MovieFilter { 4 | POPULAR, 5 | TOP_RATED, 6 | NOW_PLAYING, 7 | UPCOMING 8 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/entities/Movies.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.entities 2 | 3 | data class Movies( 4 | val movies: List, 5 | val currentPage: Int, 6 | val totalPages: Int, 7 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/entities/MovieDetails.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.entities 2 | 3 | data class MovieDetails( 4 | val movie: Movie, 5 | val genres: List, 6 | val cast: List, 7 | val trailers: List, 8 | ) -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jun 07 12:50:48 BST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-all.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /data/src/main/java/com/ang/acb/movienight/data/source/remote/response/NetworkGenre.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.data.source.remote.response 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class NetworkGenre( 6 | @SerializedName("id") val id: Long, 7 | @SerializedName("name") val name: String? 8 | ) -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /data/src/main/java/com/ang/acb/movienight/data/source/remote/response/NetworkVideo.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.data.source.remote.response 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class NetworkVideo( 6 | @SerializedName("id") val id: String, 7 | @SerializedName("key") val key: String?, 8 | @SerializedName("name") val name: String? 9 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/ang/acb/movienight/data/source/local/MovieDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.data.source.local 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | 6 | @Database(entities = [FavoriteMovie::class], version = 2, exportSchema = false) 7 | abstract class MoviesDatabase : RoomDatabase() { 8 | 9 | abstract val movieDao: MovieDao 10 | } 11 | -------------------------------------------------------------------------------- /data/src/main/java/com/ang/acb/movienight/data/source/remote/response/NetworkCast.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.data.source.remote.response 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class NetworkCast( 6 | @SerializedName("id") val id: Long, 7 | @SerializedName("name") val actorName: String?, 8 | @SerializedName("profile_path") val profileImagePath: String? 9 | ) -------------------------------------------------------------------------------- /domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | } 4 | 5 | java { 6 | sourceCompatibility = JavaVersion.VERSION_17 7 | targetCompatibility = JavaVersion.VERSION_17 8 | } 9 | 10 | dependencies { 11 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.kotlin_coroutines}") 12 | implementation("javax.inject:javax.inject:${Versions.jvm_inject}") 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/ang/acb/movienight/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # Gradle settings configured through the IDE *will override* 3 | # any settings specified in this file. 4 | # http://www.gradle.org/docs/current/userguide/build_environment.html 5 | 6 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 7 | android.useAndroidX=true 8 | kotlin.code.style=official 9 | android.nonTransitiveRClass=true 10 | 11 | TMDB_API_KEY="" -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/entities/Cast.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.entities 2 | 3 | import com.ang.acb.movienight.domain.utils.Constants.CAST_AVATAR_URL 4 | 5 | data class Cast( 6 | val id: Long, 7 | val movieId: Long, 8 | val actorName: String?, 9 | val profileImagePath: String? 10 | ) { 11 | val profileImageUrl = if (profileImagePath != null) CAST_AVATAR_URL + profileImagePath else null 12 | } 13 | -------------------------------------------------------------------------------- /data/src/main/java/com/ang/acb/movienight/data/source/remote/response/CreditsResponse.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.data.source.remote.response 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /** 6 | * The response from themoviedb.org for movie credits. 7 | * See: https://developers.themoviedb.org/3/movies/get-movie-credits 8 | */ 9 | data class CreditsResponse( 10 | @SerializedName("cast") val cast: List? 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/ang/acb/movienight/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ang/acb/movienight/MovieNightApplication.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | import timber.log.Timber 6 | 7 | @HiltAndroidApp 8 | class MovieNightApplication : Application() { 9 | 10 | override fun onCreate() { 11 | super.onCreate() 12 | 13 | if (BuildConfig.DEBUG){ 14 | Timber.plant(Timber.DebugTree()) 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/usecases/DeleteFavoriteMovieUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.usecases 2 | 3 | import com.ang.acb.movienight.domain.gateways.MovieGateway 4 | import javax.inject.Inject 5 | 6 | class DeleteFavoriteMovieUseCase @Inject constructor( 7 | private val movieGateway: MovieGateway, 8 | ) { 9 | suspend operator fun invoke(movieId: Long): Int { 10 | return movieGateway.deleteFavoriteMovie(movieId) 11 | } 12 | } -------------------------------------------------------------------------------- /data/src/main/java/com/ang/acb/movienight/data/source/remote/response/MoviesResponse.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.data.source.remote.response 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class MoviesResponse( 6 | @SerializedName("page") val page: Int, 7 | @SerializedName("results") val results: List, 8 | @SerializedName("total_pages") val totalPages: Int, 9 | @SerializedName("total_results") val totalResults: Int, 10 | ) 11 | 12 | -------------------------------------------------------------------------------- /data/src/main/java/com/ang/acb/movienight/data/source/remote/response/VideosResponse.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.data.source.remote.response 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /** 6 | * The response from themoviedb.org when getting the videos associated with a specific movie. 7 | * See: https://developers.themoviedb.org/3/movies/get-movie-videos 8 | */ 9 | data class VideosResponse( 10 | @SerializedName("results") val videos: List? 11 | ) 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/ang/acb/movienight/ui/common/MovieFilterMap.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.ui.common 2 | 3 | import com.ang.acb.movienight.R 4 | import com.ang.acb.movienight.domain.entities.MovieFilter 5 | 6 | fun MovieFilter.asStringResId() = when (this) { 7 | MovieFilter.POPULAR -> R.string.filter_by_popular 8 | MovieFilter.TOP_RATED -> R.string.filter_by_top_rated 9 | MovieFilter.NOW_PLAYING -> R.string.filter_by_now_playing 10 | MovieFilter.UPCOMING -> R.string.filter_by_upcoming 11 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/usecases/GetPopularMoviesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.usecases 2 | 3 | import com.ang.acb.movienight.domain.entities.Movies 4 | import com.ang.acb.movienight.domain.gateways.MovieGateway 5 | import javax.inject.Inject 6 | 7 | class GetPopularMoviesUseCase @Inject constructor( 8 | private val movieGateway: MovieGateway, 9 | ) { 10 | 11 | suspend operator fun invoke(page: Int): Movies { 12 | return movieGateway.getPopularMovies(page) 13 | } 14 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/usecases/SaveFavoriteMovieUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.usecases 2 | 3 | import com.ang.acb.movienight.domain.entities.Movie 4 | import com.ang.acb.movienight.domain.gateways.MovieGateway 5 | import javax.inject.Inject 6 | 7 | class SaveFavoriteMovieUseCase @Inject constructor( 8 | private val movieGateway: MovieGateway, 9 | ) { 10 | suspend operator fun invoke(movie: Movie): Long { 11 | return movieGateway.saveFavoriteMovie(movie) 12 | } 13 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/usecases/GetTopRatedMoviesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.usecases 2 | 3 | import com.ang.acb.movienight.domain.entities.Movies 4 | import com.ang.acb.movienight.domain.gateways.MovieGateway 5 | import javax.inject.Inject 6 | 7 | class GetTopRatedMoviesUseCase @Inject constructor( 8 | private val movieGateway: MovieGateway, 9 | ) { 10 | 11 | suspend operator fun invoke(page: Int): Movies { 12 | return movieGateway.getTopRatedMovies(page) 13 | } 14 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/usecases/GetUpcomingMoviesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.usecases 2 | 3 | import com.ang.acb.movienight.domain.entities.Movies 4 | import com.ang.acb.movienight.domain.gateways.MovieGateway 5 | import javax.inject.Inject 6 | 7 | class GetUpcomingMoviesUseCase @Inject constructor( 8 | private val movieGateway: MovieGateway, 9 | ) { 10 | 11 | suspend operator fun invoke(page: Int): Movies { 12 | return movieGateway.getUpcomingMovies(page) 13 | } 14 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/usecases/GetCastDetailsUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.usecases 2 | 3 | import com.ang.acb.movienight.domain.entities.CastDetails 4 | import com.ang.acb.movienight.domain.gateways.MovieGateway 5 | import javax.inject.Inject 6 | 7 | class GetCastDetailsUseCase @Inject constructor( 8 | private val movieGateway: MovieGateway, 9 | ) { 10 | 11 | suspend operator fun invoke(castId: Long): CastDetails { 12 | return movieGateway.getCastDetails(castId) 13 | } 14 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/usecases/GetNowPlayingMoviesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.usecases 2 | 3 | import com.ang.acb.movienight.domain.entities.Movies 4 | import com.ang.acb.movienight.domain.gateways.MovieGateway 5 | import javax.inject.Inject 6 | 7 | class GetNowPlayingMoviesUseCase @Inject constructor( 8 | private val movieGateway: MovieGateway, 9 | ) { 10 | 11 | suspend operator fun invoke(page: Int): Movies { 12 | return movieGateway.getNowPlayingMovies(page) 13 | } 14 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/usecases/SearchMoviesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.usecases 2 | 3 | import com.ang.acb.movienight.domain.entities.Movies 4 | import com.ang.acb.movienight.domain.gateways.MovieGateway 5 | import javax.inject.Inject 6 | 7 | class SearchMoviesUseCase @Inject constructor( 8 | private val movieGateway: MovieGateway, 9 | ) { 10 | 11 | suspend operator fun invoke(query: String, page: Int): Movies { 12 | return movieGateway.searchMovies(query, page) 13 | } 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle 2 | .gradle 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | /local.properties 7 | 8 | # Android Studio generated files and folders 9 | captures/ 10 | .externalNativeBuild/ 11 | .cxx/ 12 | *.apk 13 | output.json 14 | 15 | # IntelliJ 16 | *.iml 17 | .idea/ 18 | gradle.xml 19 | 20 | # General 21 | .DS_Store 22 | .externalNativeBuild 23 | 24 | # Keystore files 25 | *.jks 26 | *.keystore 27 | 28 | # Google Services (e.g. APIs or Firebase) 29 | google-services.json 30 | 31 | # Android Profiling 32 | *.hprof 33 | -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/usecases/GetMovieDetailsUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.usecases 2 | 3 | import com.ang.acb.movienight.domain.entities.MovieDetails 4 | import com.ang.acb.movienight.domain.gateways.MovieGateway 5 | import javax.inject.Inject 6 | 7 | class GetMovieDetailsUseCase @Inject constructor( 8 | private val movieGateway: MovieGateway, 9 | ) { 10 | 11 | suspend operator fun invoke(movieId: Long): MovieDetails { 12 | return movieGateway.getAllMovieDetails(movieId) 13 | } 14 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/usecases/GetSimilarMoviesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.usecases 2 | 3 | import com.ang.acb.movienight.domain.entities.Movie 4 | import com.ang.acb.movienight.domain.gateways.MovieGateway 5 | import javax.inject.Inject 6 | 7 | class GetSimilarMoviesUseCase @Inject constructor( 8 | private val movieGateway: MovieGateway, 9 | ) { 10 | 11 | suspend operator fun invoke(movieId: Long): List { 12 | return movieGateway.getSimilarMovies(movieId).movies 13 | } 14 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/usecases/GetAllFavoriteMoviesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.usecases 2 | 3 | import com.ang.acb.movienight.domain.entities.Movie 4 | import com.ang.acb.movienight.domain.gateways.MovieGateway 5 | import kotlinx.coroutines.flow.Flow 6 | import javax.inject.Inject 7 | 8 | class GetAllFavoriteMoviesUseCase @Inject constructor( 9 | private val movieGateway: MovieGateway, 10 | ) { 11 | 12 | operator fun invoke(): Flow> { 13 | return movieGateway.getAllFavoriteMovies() 14 | } 15 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/usecases/GetFavoriteMovieUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.usecases 2 | 3 | import com.ang.acb.movienight.domain.entities.Movie 4 | import com.ang.acb.movienight.domain.gateways.MovieGateway 5 | import kotlinx.coroutines.flow.Flow 6 | import javax.inject.Inject 7 | 8 | class GetFavoriteMovieUseCase @Inject constructor( 9 | private val movieGateway: MovieGateway, 10 | ) { 11 | operator fun invoke(movieId: Long): Flow { 12 | return movieGateway.getFavoriteMovie(movieId) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ang/acb/movienight/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val red200 = Color(0xfff297a2) 6 | val red300 = Color(0xffea6d7e) 7 | val red700 = Color(0xffdd0d3c) 8 | val red800 = Color(0xffd00036) 9 | val red900 = Color(0xffc20029) 10 | 11 | val midnight0 = Color(0xFFFEFEFF) 12 | val midnight50 = Color(0xFFEFF5FD) 13 | val midnight100 = Color(0xFFCAD5E2) 14 | val midnight200 = Color(0xFFADBDD2) 15 | val midnight300 = Color(0xFF98ABC6) 16 | val midnight400 = Color(0xFF7F8FA6) 17 | val midnight800 = Color(0xFF1A1E23) -------------------------------------------------------------------------------- /data/src/main/java/com/ang/acb/movienight/data/source/local/MovieMappers.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.data.source.local 2 | 3 | import com.ang.acb.movienight.domain.entities.Movie 4 | 5 | fun FavoriteMovie.asMovie() = Movie( 6 | id = id, 7 | title = title, 8 | overview = overview, 9 | releaseDate = releaseDate, 10 | posterPath = posterPath, 11 | backdropPath = backdropPath, 12 | popularity = popularity, 13 | voteAverage = voteAverage, 14 | voteCount = voteCount, 15 | isFavorite = isFavorite, 16 | ) 17 | 18 | fun List.asMovies() = this.map { it.asMovie() } -------------------------------------------------------------------------------- /app/src/main/java/com/ang/acb/movienight/ui/common/PagingLoadingView.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.ui.common 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.material.CircularProgressIndicator 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.Modifier 8 | 9 | @Composable 10 | fun PagingLoadingView( 11 | modifier: Modifier = Modifier 12 | ) { 13 | Box( 14 | modifier = modifier, 15 | contentAlignment = Alignment.Center, 16 | content = { CircularProgressIndicator() } 17 | ) 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ang/acb/movienight/di/RepoModule.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.di 2 | 3 | import com.ang.acb.movienight.data.MovieRepository 4 | import com.ang.acb.movienight.domain.gateways.MovieGateway 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | object RepoModule { 14 | 15 | @Provides 16 | @Singleton 17 | fun provideMovieGateway( 18 | movieRepository: MovieRepository 19 | ): MovieGateway = movieRepository 20 | } -------------------------------------------------------------------------------- /data/src/main/java/com/ang/acb/movienight/data/source/remote/response/NetworkCastDetails.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.data.source.remote.response 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class NetworkCastDetails( 6 | @SerializedName("id") val id: Long, 7 | @SerializedName("name") val name: String, 8 | @SerializedName("birthday") val birthday: String?, 9 | @SerializedName("place_of_birth") val placeOfBirth: String?, 10 | @SerializedName("biography") val biography: String?, 11 | @SerializedName("profile_path") val profilePath: String?, 12 | @SerializedName("imdb_id") val imdbId: String?, 13 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.utils 2 | 3 | object Constants { 4 | const val TMDB_BASE_URL = "https://api.themoviedb.org/3/" 5 | 6 | private const val IMAGE_BASE_URL = "https://image.tmdb.org/t/p/" 7 | private const val IMAGE_SIZE_W185 = "w185" 8 | private const val IMAGE_SIZE_W780 = "w780" 9 | 10 | const val CAST_AVATAR_URL = IMAGE_BASE_URL + IMAGE_SIZE_W185 11 | const val CAST_IMDB_URL = "https://www.imdb.com/name/" 12 | const val POSTER_URL = IMAGE_BASE_URL + IMAGE_SIZE_W185 13 | const val BACKDROP_URL = IMAGE_BASE_URL + IMAGE_SIZE_W780 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ang/acb/movienight/ui/common/LoadingBox.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.ui.common 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material.CircularProgressIndicator 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | 10 | @Composable 11 | fun LoadingBox() { 12 | Box( 13 | contentAlignment = Alignment.Center, 14 | modifier = Modifier.fillMaxSize(), 15 | content = { 16 | CircularProgressIndicator() 17 | }, 18 | ) 19 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/entities/CastDetails.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.entities 2 | 3 | import com.ang.acb.movienight.domain.utils.Constants.CAST_AVATAR_URL 4 | import com.ang.acb.movienight.domain.utils.Constants.CAST_IMDB_URL 5 | 6 | data class CastDetails( 7 | val id: Long, 8 | val name: String?, 9 | val birthday: String?, 10 | val placeOfBirth: String?, 11 | val biography: String?, 12 | val profileImagePath: String?, 13 | val imdbId: String? 14 | ) { 15 | val profileImageUrl = if (profileImagePath != null) CAST_AVATAR_URL + profileImagePath else null 16 | val imdbUrl = if (imdbId != null) CAST_IMDB_URL + imdbId else null 17 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/entities/Movie.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.entities 2 | 3 | import com.ang.acb.movienight.domain.utils.Constants 4 | 5 | data class Movie( 6 | val id: Long, 7 | val title: String?, 8 | val overview: String?, 9 | val releaseDate: String?, 10 | val posterPath: String?, 11 | val backdropPath: String?, 12 | val popularity: Double?, 13 | val voteAverage: Double?, 14 | val voteCount: Int?, 15 | val isFavorite: Boolean?, 16 | ) { 17 | val posterUrl = if (posterPath != null) Constants.POSTER_URL + posterPath else null 18 | val backdropUrl = if (backdropPath != null) Constants.BACKDROP_URL + backdropPath else null 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ang/acb/movienight/ui/details/CastCard.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.ui.details 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.material.Card 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import com.ang.acb.movienight.domain.entities.Cast 8 | 9 | @Composable 10 | fun CastCard( 11 | cast: Cast, 12 | modifier: Modifier = Modifier, 13 | onItemClick: (cast: Cast) -> Unit, 14 | ) { 15 | Card(modifier = modifier) { 16 | CastProfileImage( 17 | profileImageUrl = cast.profileImageUrl, 18 | modifier = Modifier.clickable { onItemClick(cast) }, 19 | ) 20 | } 21 | } 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /data/src/main/java/com/ang/acb/movienight/data/source/remote/response/NetworkMovie.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.data.source.remote.response 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class NetworkMovie( 6 | @SerializedName("id") val id: Long, 7 | @SerializedName("title") val title: String?, 8 | @SerializedName("overview") val overview: String?, 9 | @SerializedName("release_date") val releaseDate: String?, 10 | @SerializedName("poster_path") val posterPath: String?, 11 | @SerializedName("backdrop_path") val backdropPath: String?, 12 | @SerializedName("popularity") val popularity: Double?, 13 | @SerializedName("vote_average") val voteAverage: Double?, 14 | @SerializedName("vote_count") val voteCount: Int? 15 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/ang/acb/movienight/ui/common/PagingLoadingItem.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.ui.common 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.layout.wrapContentWidth 6 | import androidx.compose.material.CircularProgressIndicator 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | fun PagingLoadingItem() { 14 | CircularProgressIndicator( 15 | modifier = Modifier 16 | .fillMaxWidth() 17 | .padding(16.dp) 18 | .wrapContentWidth(Alignment.CenterHorizontally) 19 | ) 20 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/ang/acb/movienight/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.ang.acb.movienight", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /data/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /data/src/main/java/com/ang/acb/movienight/data/source/local/FavoriteMovie.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.data.source.local 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "movie") 8 | data class FavoriteMovie( 9 | @PrimaryKey 10 | val id: Long, 11 | val title: String?, 12 | val overview: String?, 13 | @ColumnInfo(name = "release_date") 14 | val releaseDate: String?, 15 | @ColumnInfo(name = "poster_path") 16 | val posterPath: String?, 17 | @ColumnInfo(name = "backdrop_path") 18 | val backdropPath: String?, 19 | val popularity: Double?, 20 | @ColumnInfo(name = "vote_average") 21 | val voteAverage: Double?, 22 | @ColumnInfo(name = "vote_count") 23 | val voteCount: Int?, 24 | @ColumnInfo(name = "is_favorite") 25 | val isFavorite: Boolean 26 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/entities/Trailer.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.entities 2 | 3 | private const val YOUTUBE_APP_BASE_URL = "vnd.youtube:" 4 | private const val YOUTUBE_WEB_BASE_URL = "https://www.youtube.com/watch?v=" 5 | private const val YOUTUBE_TRAILER_THUMBNAIL_BASE_URL = "https://img.youtube.com/vi/" 6 | private const val YOUTUBE_TRAILER_THUMBNAIL_HQ_SUFFIX = "/hqdefault.jpg" 7 | 8 | data class Trailer( 9 | val id: String, 10 | val movieId: Long, 11 | val key: String?, 12 | val name: String? 13 | ) { 14 | val youTubeThumbnailUrl = if (key != null) { 15 | YOUTUBE_TRAILER_THUMBNAIL_BASE_URL + key + YOUTUBE_TRAILER_THUMBNAIL_HQ_SUFFIX 16 | } else null 17 | 18 | val youTubeAppUrl = if (key != null) YOUTUBE_APP_BASE_URL + key else null 19 | val youTubeWebUrl = if (key != null) YOUTUBE_WEB_BASE_URL + key else null 20 | } -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/ang/acb/movienight/ui/common/MessageBox.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.ui.common 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.stringResource 12 | import androidx.compose.ui.text.style.TextAlign 13 | import androidx.compose.ui.unit.dp 14 | 15 | 16 | @Composable 17 | fun MessageBox( 18 | @StringRes messageResId: Int 19 | ) { 20 | Box( 21 | contentAlignment = Alignment.Center, 22 | modifier = Modifier 23 | .fillMaxSize() 24 | .padding(32.dp), 25 | ) { 26 | Text( 27 | text = stringResource(id = messageResId), 28 | textAlign = TextAlign.Center, 29 | ) 30 | } 31 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/usecases/GetFilteredMoviesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.usecases 2 | 3 | import com.ang.acb.movienight.domain.entities.MovieFilter 4 | import com.ang.acb.movienight.domain.entities.Movies 5 | import javax.inject.Inject 6 | 7 | class GetFilteredMoviesUseCase @Inject constructor( 8 | private val getPopularMoviesUseCase: GetPopularMoviesUseCase, 9 | private val getTopRatedMoviesUseCase: GetTopRatedMoviesUseCase, 10 | private val getNowPlayingMoviesUseCase: GetNowPlayingMoviesUseCase, 11 | private val getUpcomingMoviesUseCase: GetUpcomingMoviesUseCase, 12 | ) { 13 | 14 | suspend operator fun invoke(filter: MovieFilter, page: Int): Movies { 15 | return when (filter) { 16 | MovieFilter.POPULAR -> getPopularMoviesUseCase(page) 17 | MovieFilter.TOP_RATED -> getTopRatedMoviesUseCase(page) 18 | MovieFilter.NOW_PLAYING -> getNowPlayingMoviesUseCase(page) 19 | MovieFilter.UPCOMING -> getUpcomingMoviesUseCase(page) 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ang/acb/movienight/ui/details/MovieInfoHeader.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.ui.details 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.text.TextStyle 10 | import androidx.compose.ui.text.font.FontWeight 11 | import androidx.compose.ui.unit.dp 12 | import androidx.compose.ui.unit.sp 13 | import com.ang.acb.movienight.ui.theme.moviesFontFamily 14 | 15 | @Composable 16 | fun MovieInfoHeader(title: String) { 17 | Box( 18 | modifier = Modifier 19 | .fillMaxWidth() 20 | .padding(horizontal = 16.dp, vertical = 8.dp) 21 | ) { 22 | Text( 23 | text = title, 24 | style = TextStyle( 25 | fontFamily = moviesFontFamily, 26 | fontWeight = FontWeight.Medium, 27 | fontSize = 16.sp, 28 | letterSpacing = 0.15.sp, 29 | ) 30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ang/acb/movienight/ui/details/CastCarousel.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.ui.details 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.aspectRatio 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.unit.dp 9 | import com.ang.acb.movienight.domain.entities.Cast 10 | import com.ang.acb.movienight.ui.common.Carousel 11 | 12 | @Composable 13 | fun CastCarousel( 14 | cast: List, 15 | onItemClick: (cast: Cast) -> Unit, 16 | modifier: Modifier = Modifier, 17 | ) { 18 | Carousel( 19 | items = cast, 20 | contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), 21 | itemSpacing = 4.dp, 22 | modifier = modifier 23 | ) { item, padding -> 24 | CastCard( 25 | cast = item, 26 | onItemClick = onItemClick, 27 | modifier = Modifier 28 | .padding(padding) 29 | .fillParentMaxHeight() 30 | .aspectRatio(2 / 3f), 31 | ) 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ang/acb/movienight/ui/details/TrailerCarousel.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.ui.details 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.aspectRatio 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.unit.dp 9 | import com.ang.acb.movienight.domain.entities.Trailer 10 | import com.ang.acb.movienight.ui.common.Carousel 11 | 12 | @Composable 13 | fun TrailerCarousel( 14 | trailers: List, 15 | modifier: Modifier = Modifier, 16 | onItemClick: (trailer: Trailer) -> Unit, 17 | ) { 18 | Carousel( 19 | items = trailers, 20 | contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), 21 | itemSpacing = 4.dp, 22 | modifier = modifier 23 | ) { item, padding -> 24 | TrailerCard( 25 | trailer = item, 26 | modifier = Modifier 27 | .padding(padding) 28 | .fillParentMaxHeight() 29 | .aspectRatio(16 / 9f), 30 | onItemClick = onItemClick 31 | ) 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ang/acb/movienight/ui/details/MovieInfoBackdrop.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.ui.details 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.aspectRatio 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.layout.ContentScale 10 | import androidx.compose.ui.platform.LocalContext 11 | import coil.compose.AsyncImage 12 | import coil.request.ImageRequest 13 | 14 | @Composable 15 | fun MovieBackdropImage( 16 | backdropUrl: String, 17 | ) { 18 | Box( 19 | modifier = Modifier 20 | .fillMaxWidth() 21 | .aspectRatio(16f / 9), 22 | contentAlignment = Alignment.Center, 23 | ) { 24 | AsyncImage( 25 | model = ImageRequest.Builder(LocalContext.current) 26 | .data(backdropUrl) 27 | .crossfade(500) 28 | .build(), 29 | contentDescription = null, 30 | contentScale = ContentScale.Crop, 31 | modifier = Modifier.matchParentSize(), 32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/ang/acb/movienight/domain/gateways/MovieGateway.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.domain.gateways 2 | 3 | import com.ang.acb.movienight.domain.entities.CastDetails 4 | import com.ang.acb.movienight.domain.entities.Movie 5 | import com.ang.acb.movienight.domain.entities.MovieDetails 6 | import com.ang.acb.movienight.domain.entities.Movies 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | interface MovieGateway { 10 | suspend fun getPopularMovies(page: Int): Movies 11 | suspend fun getTopRatedMovies(page: Int): Movies 12 | suspend fun getNowPlayingMovies(page: Int): Movies 13 | suspend fun getUpcomingMovies(page: Int): Movies 14 | suspend fun searchMovies(query: String, page: Int): Movies 15 | suspend fun getAllMovieDetails(movieId: Long): MovieDetails 16 | suspend fun getSimilarMovies(movieId: Long): Movies 17 | suspend fun getCastDetails(castId: Long): CastDetails 18 | 19 | suspend fun saveFavoriteMovie(movie: Movie): Long 20 | suspend fun updateFavoriteFlag(movieId: String, isFavorite: Boolean) 21 | suspend fun deleteFavoriteMovie(movieId: Long): Int 22 | suspend fun deleteAllFavoriteMovies() 23 | fun getFavoriteMovie(movieId: Long): Flow 24 | fun getAllFavoriteMovies(): Flow> 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/ang/acb/movienight/di/RoomModule.kt: -------------------------------------------------------------------------------- 1 | package com.ang.acb.movienight.di 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import com.ang.acb.movienight.data.source.local.MovieDao 6 | import com.ang.acb.movienight.data.source.local.MoviesDatabase 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.android.qualifiers.ApplicationContext 11 | import dagger.hilt.components.SingletonComponent 12 | import kotlinx.coroutines.Dispatchers 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | object RoomModule { 18 | 19 | @Singleton 20 | @Provides 21 | fun provideDataBase(@ApplicationContext context: Context): MoviesDatabase { 22 | return Room.databaseBuilder( 23 | context.applicationContext, 24 | MoviesDatabase::class.java, 25 | "movies.db" 26 | ) 27 | .fallbackToDestructiveMigration() 28 | .build() 29 | } 30 | 31 | @Singleton 32 | @Provides 33 | fun provideMovieDao(database: MoviesDatabase): MovieDao { 34 | return database.movieDao 35 | } 36 | 37 | @Singleton 38 | @Provides 39 | fun provideIoDispatcher() = Dispatchers.IO 40 | } -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 21 | 22 |