diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..56cc6425
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,85 @@
+# Built application files
+*.apk
+*.aar
+*.ap_
+*.aab
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+# Uncomment the following line in case you need and you don't have the release build type files in your app
+# release/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# IntelliJ
+*.iml
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/gradle.xml
+.idea/assetWizardSettings.xml
+.idea/dictionaries
+.idea/libraries
+# Android Studio 3 in .gitignore file.
+.idea/caches
+.idea/modules.xml
+# Comment next line if keeping position of elements in Navigation Editor is relevant for you
+.idea/navEditor.xml
+
+# Keystore files
+# Uncomment the following lines if you do not want to check your keystore files in.
+#*.jks
+#*.keystore
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
+.cxx/
+
+# Google Services (e.g. APIs or Firebase)
+# google-services.json
+
+# Freeline
+freeline.py
+freeline/
+freeline_project_description.json
+
+# fastlane
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+fastlane/readme.md
+
+# Version control
+vcs.xml
+
+# lint
+lint/intermediates/
+lint/generated/
+lint/outputs/
+lint/tmp/
+# lint/reports/
diff --git a/Design/Compilation.png b/Design/Compilation.png
new file mode 100644
index 00000000..c4f16bf6
Binary files /dev/null and b/Design/Compilation.png differ
diff --git a/Design/Favorite page.png b/Design/Favorite page.png
new file mode 100644
index 00000000..eb7585a8
Binary files /dev/null and b/Design/Favorite page.png differ
diff --git a/Design/Movie details page.png b/Design/Movie details page.png
new file mode 100644
index 00000000..9e3ab476
Binary files /dev/null and b/Design/Movie details page.png differ
diff --git a/Design/Movies page.png b/Design/Movies page.png
new file mode 100644
index 00000000..4255cd0e
Binary files /dev/null and b/Design/Movies page.png differ
diff --git a/Design/Notes.png b/Design/Notes.png
new file mode 100644
index 00000000..751039c2
Binary files /dev/null and b/Design/Notes.png differ
diff --git a/Design/Search page.png b/Design/Search page.png
new file mode 100644
index 00000000..7342aa8f
Binary files /dev/null and b/Design/Search page.png differ
diff --git a/Design/UI Design.psd b/Design/UI Design.psd
new file mode 100644
index 00000000..b259ece4
Binary files /dev/null and b/Design/UI Design.psd differ
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..35bb1dd1
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Casper BL
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Movies/.gitignore b/Movies/.gitignore
new file mode 100644
index 00000000..aa724b77
--- /dev/null
+++ b/Movies/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/Movies/.idea/.gitignore b/Movies/.idea/.gitignore
new file mode 100644
index 00000000..26d33521
--- /dev/null
+++ b/Movies/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/Movies/.idea/compiler.xml b/Movies/.idea/compiler.xml
new file mode 100644
index 00000000..61a9130c
--- /dev/null
+++ b/Movies/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/.idea/dictionaries/Casper.xml b/Movies/.idea/dictionaries/Casper.xml
new file mode 100644
index 00000000..a2d072e5
--- /dev/null
+++ b/Movies/.idea/dictionaries/Casper.xml
@@ -0,0 +1,9 @@
+
+
+
+ gson
+ themoviedb
+ tmdb
+
+
+
\ No newline at end of file
diff --git a/Movies/.idea/gradle.xml b/Movies/.idea/gradle.xml
new file mode 100644
index 00000000..23a89bbb
--- /dev/null
+++ b/Movies/.idea/gradle.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/.idea/inspectionProfiles/Project_Default.xml b/Movies/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 00000000..c83a369c
--- /dev/null
+++ b/Movies/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/.idea/jarRepositories.xml b/Movies/.idea/jarRepositories.xml
new file mode 100644
index 00000000..a5f05cd8
--- /dev/null
+++ b/Movies/.idea/jarRepositories.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/.idea/markdown-navigator-enh.xml b/Movies/.idea/markdown-navigator-enh.xml
new file mode 100644
index 00000000..a8fcc84d
--- /dev/null
+++ b/Movies/.idea/markdown-navigator-enh.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/.idea/markdown-navigator.xml b/Movies/.idea/markdown-navigator.xml
new file mode 100644
index 00000000..a2fc0864
--- /dev/null
+++ b/Movies/.idea/markdown-navigator.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/.idea/misc.xml b/Movies/.idea/misc.xml
new file mode 100644
index 00000000..d5d35ec4
--- /dev/null
+++ b/Movies/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/.idea/vcs.xml b/Movies/.idea/vcs.xml
new file mode 100644
index 00000000..6c0b8635
--- /dev/null
+++ b/Movies/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/.gitignore b/Movies/app/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/Movies/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/Movies/app/build.gradle b/Movies/app/build.gradle
new file mode 100644
index 00000000..4fd4ae22
--- /dev/null
+++ b/Movies/app/build.gradle
@@ -0,0 +1,111 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+ id 'kotlin-kapt'
+ id 'dagger.hilt.android.plugin'
+}
+
+android {
+ compileSdkVersion 30
+ buildToolsVersion "30.0.3"
+
+ buildFeatures {
+ viewBinding true
+ dataBinding true
+ }
+ defaultConfig {
+ applicationId "com.daresay.movies"
+ minSdkVersion 23
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0.0"
+
+ buildConfigField("String", "TMDB_API_KEY", "\"" + getTmdbApiKey() + "\"")
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled 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 {
+ // Google
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$rootProject.kotlin_version"
+ implementation "androidx.core:core-ktx:$rootProject.ktx_version"
+ implementation "androidx.appcompat:appcompat:$rootProject.app_compat_version"
+ implementation "com.google.android.material:material:$rootProject.material_design_version"
+ implementation "androidx.constraintlayout:constraintlayout:$rootProject.constraint_layout_version"
+ implementation "androidx.recyclerview:recyclerview:$rootProject.recycler_view_version"
+ implementation "androidx.viewpager2:viewpager2:$rootProject.view_pager_version"
+ implementation "com.google.android:flexbox:$rootProject.flexbox_version"
+ implementation "androidx.paging:paging-runtime-ktx:$rootProject.paging_version"
+
+ // Navigation
+ implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.nav_version"
+ implementation "androidx.navigation:navigation-ui-ktx:$rootProject.nav_version"
+
+ // Facebook
+ implementation "com.facebook.shimmer:shimmer:$rootProject.facebook_shimmer_version"
+
+ // Room
+ implementation "androidx.room:room-runtime:$rootProject.room_version"
+ kapt "androidx.room:room-compiler:$rootProject.room_version"
+ implementation "androidx.room:room-ktx:$rootProject.room_version"
+
+ // Retrofit
+ implementation "com.squareup.retrofit2:converter-gson:$rootProject.retrofit_version"
+ implementation "com.squareup.retrofit2:retrofit:$rootProject.retrofit_version"
+ implementation "com.squareup.okhttp3:logging-interceptor:$rootProject.okhttp_logging_version"
+
+ // Hilt
+ implementation "com.google.dagger:hilt-android:$rootProject.hilt_version"
+ implementation "androidx.hilt:hilt-lifecycle-viewmodel:$rootProject.hilt_compiler_version"
+ kapt "com.google.dagger:hilt-android-compiler:$rootProject.hilt_version"
+ kapt "androidx.hilt:hilt-compiler:$rootProject.hilt_compiler_version"
+
+ // Ktx Coroutines
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines_version"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines_version"
+
+ // Lifecycle
+ implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.lifecycle_version_ktx"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.lifecycle_version_ktx"
+
+ // Timber (logging)
+ implementation "com.jakewharton.timber:timber:$rootProject.timer_version"
+
+ // SpinKit (loading animations)
+ implementation "com.github.ybq:Android-SpinKit:$rootProject.spinkit_version"
+
+ // Glide
+ implementation "com.github.bumptech.glide:glide:$rootProject.glide_version"
+ kapt "com.github.bumptech.glide:compiler:$rootProject.glide_version"
+
+ // Debug database
+ debugImplementation "com.amitshekhar.android:debug-db:$rootProject.debug_db_version"
+
+ // Testing
+ testImplementation "junit:junit:$rootProject.junit_version"
+ androidTestImplementation "androidx.test.ext:junit:$rootProject.junit_test_version"
+ androidTestImplementation "androidx.test.espresso:espresso-core:$rootProject.espresso_core_version"
+ testImplementation "androidx.room:room-testing:$rootProject.room_version"
+}
+
+kapt {
+ generateStubs = true
+}
+
+def getTmdbApiKey() {
+ return project.findProperty("tmdb_api_key")
+}
\ No newline at end of file
diff --git a/Movies/app/proguard-rules.pro b/Movies/app/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/Movies/app/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/Movies/app/src/androidTest/java/com/daresay/movies/ExampleInstrumentedTest.kt b/Movies/app/src/androidTest/java/com/daresay/movies/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..e8d7652d
--- /dev/null
+++ b/Movies/app/src/androidTest/java/com/daresay/movies/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.daresay.movies
+
+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.daresay.movies", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/AndroidManifest.xml b/Movies/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..cb4d9930
--- /dev/null
+++ b/Movies/app/src/main/AndroidManifest.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/App.kt b/Movies/app/src/main/java/com/daresay/movies/App.kt
new file mode 100644
index 00000000..9686883d
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/App.kt
@@ -0,0 +1,15 @@
+package com.daresay.movies
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+import timber.log.Timber
+
+@HiltAndroidApp
+class App : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ if (BuildConfig.DEBUG) {
+ Timber.plant(Timber.DebugTree())
+ }
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/api/BaseDataSource.kt b/Movies/app/src/main/java/com/daresay/movies/data/api/BaseDataSource.kt
new file mode 100644
index 00000000..d8f0853f
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/api/BaseDataSource.kt
@@ -0,0 +1,25 @@
+package com.daresay.movies.data.api
+
+import com.daresay.movies.utils.Resource
+import retrofit2.Response
+
+abstract class BaseDataSource {
+ protected suspend fun getResult(call: suspend () -> Response): Resource {
+ try {
+ val response = call()
+ if (response.isSuccessful) {
+ val body = response.body()
+ if (body != null)
+ return Resource.success(body)
+ }
+ return error(" ${response.code()} ${response.message()}")
+ }
+ catch (e: Exception) {
+ return error(e.message ?: e.toString())
+ }
+ }
+
+ private fun error(message: String): Resource {
+ return Resource.error("Network call has failed for a following reason: $message")
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/api/TmdbRemoteDataSource.kt b/Movies/app/src/main/java/com/daresay/movies/data/api/TmdbRemoteDataSource.kt
new file mode 100644
index 00000000..14bf630e
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/api/TmdbRemoteDataSource.kt
@@ -0,0 +1,30 @@
+package com.daresay.movies.data.api
+
+import com.daresay.movies.data.models.authentication.SessionRequestBody
+import javax.inject.Inject
+
+class TmdbRemoteDataSource @Inject constructor(private val tmdbService: TmdbService) : BaseDataSource() {
+ suspend fun getToken() = getResult {
+ tmdbService.getToken()
+ }
+
+ suspend fun createSession(requestBody: SessionRequestBody) = getResult {
+ tmdbService.createSession(requestBody = requestBody)
+ }
+
+ suspend fun getTopRatedMovies(page: Int) = getResult {
+ tmdbService.getTopRatedMovies(page = page)
+ }
+
+ suspend fun getPopularMovies(page: Int) = getResult {
+ tmdbService.getPopularMovies(page = page)
+ }
+
+ suspend fun getMovieDetails(movieId: Int) = getResult {
+ tmdbService.getMovieDetails(movieId = movieId)
+ }
+
+ suspend fun getMovieReviews(movieId: Int, page: Int) = getResult {
+ tmdbService.getMovieReviews(movieId = movieId, page = page)
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/api/TmdbRepository.kt b/Movies/app/src/main/java/com/daresay/movies/data/api/TmdbRepository.kt
new file mode 100644
index 00000000..f7a4c39b
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/api/TmdbRepository.kt
@@ -0,0 +1,57 @@
+package com.daresay.movies.data.api
+
+import com.daresay.movies.data.local.MovieDao
+import com.daresay.movies.data.models.authentication.SessionRequestBody
+import com.daresay.movies.data.models.favorites.Favorite
+import com.daresay.movies.utils.performDatabaseGetOperation
+import com.daresay.movies.utils.performGetOperation
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class TmdbRepository @Inject constructor(
+ private val remoteDataSource: TmdbRemoteDataSource,
+ private val localDataSource: MovieDao) {
+
+ fun getUserToken() = performGetOperation(
+ networkCall = { remoteDataSource.getToken() }
+ )
+
+ fun createSession(requestBody: SessionRequestBody) = performGetOperation(
+ networkCall = { remoteDataSource.createSession(requestBody = requestBody) }
+ )
+
+ fun getTopRatedMovies(page: Int) = performGetOperation(
+ networkCall = { remoteDataSource.getTopRatedMovies(page = page) }
+ )
+
+ fun getPopularMovies(page: Int) = performGetOperation(
+ networkCall = { remoteDataSource.getPopularMovies(page = page) }
+ )
+
+ fun getMovieDetails(movieId: Int) = performGetOperation(
+ databaseQuery = { localDataSource.getMovie(movieId) },
+ networkCall = { remoteDataSource.getMovieDetails(movieId) },
+ saveCallResult = { localDataSource.insert(it) })
+
+ fun getMovieReviews(movieId: Int, page: Int) = performGetOperation(
+ networkCall = { remoteDataSource.getMovieReviews(movieId = movieId, page = page) }
+ )
+
+ fun setMovieFavorite(movieId: Int, favorite: Boolean) {
+ CoroutineScope(IO).launch {
+ localDataSource.insert(Favorite(movieId, favorite))
+ }
+ }
+
+ fun getFavorite(movieId: Int) = performDatabaseGetOperation(
+ databaseQuery = { localDataSource.getFavorite(movieId) }
+ )
+
+ fun getAllFavorites() = performDatabaseGetOperation(
+ databaseQuery = { localDataSource.getAllFavorites() }
+ )
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/api/TmdbService.kt b/Movies/app/src/main/java/com/daresay/movies/data/api/TmdbService.kt
new file mode 100644
index 00000000..4f94920b
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/api/TmdbService.kt
@@ -0,0 +1,50 @@
+package com.daresay.movies.data.api
+
+import com.daresay.movies.BuildConfig
+import com.daresay.movies.data.models.authentication.RequestToken
+import com.daresay.movies.data.models.authentication.SessionRequestBody
+import com.daresay.movies.data.models.authentication.SessionToken
+import com.daresay.movies.data.models.movie.MovieList
+import com.daresay.movies.data.models.moviedetails.MovieDetails
+import com.daresay.movies.data.models.moviedetails.Reviews
+import retrofit2.Response
+import retrofit2.http.*
+
+interface TmdbService {
+ @GET("authentication/token/new")
+ suspend fun getToken(
+ @Query("api_key") apiKey: String = BuildConfig.TMDB_API_KEY
+ ): Response
+
+ @POST("authentication/session/new")
+ suspend fun createSession(
+ @Body() requestBody: SessionRequestBody,
+ @Query("api_key") apiKey: String = BuildConfig.TMDB_API_KEY
+ ): Response
+
+ @GET("movie/top_rated")
+ suspend fun getTopRatedMovies(
+ @Query("page") page: Int,
+ @Query("api_key") apiKey: String = BuildConfig.TMDB_API_KEY
+ ): Response
+
+ @GET("movie/popular")
+ suspend fun getPopularMovies(
+ @Query("page") page: Int,
+ @Query("api_key") apiKey: String = BuildConfig.TMDB_API_KEY
+ ): Response
+
+ @GET("movie/{movie_id}")
+ suspend fun getMovieDetails(
+ @Path("movie_id") movieId: Int,
+ @Query("api_key") apiKey: String = BuildConfig.TMDB_API_KEY,
+ @Query("append_to_response") reviews: String = "reviews"
+ ): Response
+
+ @GET("movie/{movie_id}")
+ suspend fun getMovieReviews(
+ @Path("movie_id") movieId: Int,
+ @Query("page") page: Int,
+ @Query("api_key") apiKey: String = BuildConfig.TMDB_API_KEY,
+ ): Response
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/local/AppDatabase.kt b/Movies/app/src/main/java/com/daresay/movies/data/local/AppDatabase.kt
new file mode 100644
index 00000000..e95d9c69
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/local/AppDatabase.kt
@@ -0,0 +1,27 @@
+package com.daresay.movies.data.local
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import com.daresay.movies.data.models.favorites.Favorite
+import com.daresay.movies.data.models.moviedetails.MovieDetails
+
+@Database(entities = [MovieDetails::class, Favorite::class], version = 1, exportSchema = false)
+@TypeConverters(MyTypeConverters::class)
+abstract class AppDatabase : RoomDatabase() {
+ abstract fun movieDao() : MovieDao
+
+ companion object {
+ @Volatile private var instance: AppDatabase? = null
+
+ fun getDatabase(context: Context) : AppDatabase =
+ instance ?: synchronized(this) { instance ?: buildDatabase(context).also { instance = it } }
+
+ private fun buildDatabase(appContext: Context) =
+ Room.databaseBuilder(appContext, AppDatabase::class.java, "Movies")
+ .fallbackToDestructiveMigration()
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/local/MovieDao.kt b/Movies/app/src/main/java/com/daresay/movies/data/local/MovieDao.kt
new file mode 100644
index 00000000..ba7ec0d3
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/local/MovieDao.kt
@@ -0,0 +1,25 @@
+package com.daresay.movies.data.local
+
+import androidx.lifecycle.LiveData
+import androidx.room.*
+import com.daresay.movies.data.models.favorites.Favorite
+import com.daresay.movies.data.models.favorites.FavoriteWithMovieDetails
+import com.daresay.movies.data.models.moviedetails.MovieDetails
+
+@Dao
+interface MovieDao {
+ @Query("SELECT * FROM MovieDetails WHERE id = :movieId")
+ fun getMovie(movieId: Int) : LiveData
+
+ @Query("SELECT * FROM Favorites WHERE movieId = :movieId")
+ fun getFavorite(movieId: Int) : LiveData
+
+ @Query("SELECT * FROM Favorites WHERE favorite = 1")
+ fun getAllFavorites() : LiveData>
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insert(movie: MovieDetails)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insert(favorite: Favorite)
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/local/MyTypeConverters.kt b/Movies/app/src/main/java/com/daresay/movies/data/local/MyTypeConverters.kt
new file mode 100644
index 00000000..5f7d310e
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/local/MyTypeConverters.kt
@@ -0,0 +1,33 @@
+package com.daresay.movies.data.local
+
+import androidx.room.TypeConverter
+import com.daresay.movies.data.models.moviedetails.Genre
+import com.daresay.movies.data.models.moviedetails.Reviews
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import com.google.gson.reflect.TypeToken
+import javax.inject.Inject
+
+class MyTypeConverters {
+ @TypeConverter
+ fun genreToJson(value: List?): String {
+ return GsonBuilder().create().toJson(value)
+ }
+
+ @TypeConverter
+ fun genreFromJson(value: String): List? {
+ val type = object : TypeToken>(){}.type
+ return GsonBuilder().create().fromJson(value, type)
+ }
+
+ @TypeConverter
+ fun reviewsToJson(value: Reviews?): String {
+ return GsonBuilder().create().toJson(value)
+ }
+
+ @TypeConverter
+ fun reviewsFromJson(value: String): Reviews? {
+ val type = object : TypeToken(){}.type
+ return GsonBuilder().create().fromJson(value, type)
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/models/authentication/RequestToken.kt b/Movies/app/src/main/java/com/daresay/movies/data/models/authentication/RequestToken.kt
new file mode 100644
index 00000000..52b01bf9
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/models/authentication/RequestToken.kt
@@ -0,0 +1,7 @@
+package com.daresay.movies.data.models.authentication
+
+data class RequestToken(
+ val expires_at: String,
+ val request_token: String,
+ val success: Boolean
+)
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/models/authentication/SessionRequestBody.kt b/Movies/app/src/main/java/com/daresay/movies/data/models/authentication/SessionRequestBody.kt
new file mode 100644
index 00000000..b26a7b7e
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/models/authentication/SessionRequestBody.kt
@@ -0,0 +1,5 @@
+package com.daresay.movies.data.models.authentication
+
+data class SessionRequestBody(
+ val request_token: String
+)
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/models/authentication/SessionToken.kt b/Movies/app/src/main/java/com/daresay/movies/data/models/authentication/SessionToken.kt
new file mode 100644
index 00000000..b70b1046
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/models/authentication/SessionToken.kt
@@ -0,0 +1,6 @@
+package com.daresay.movies.data.models.authentication
+
+data class SessionToken(
+ val session_id: String,
+ val success: Boolean
+)
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/models/favorites/Favorite.kt b/Movies/app/src/main/java/com/daresay/movies/data/models/favorites/Favorite.kt
new file mode 100644
index 00000000..595dadf0
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/models/favorites/Favorite.kt
@@ -0,0 +1,14 @@
+package com.daresay.movies.data.models.favorites
+
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.ForeignKey.CASCADE
+import androidx.room.PrimaryKey
+import com.daresay.movies.data.models.moviedetails.MovieDetails
+
+@Entity(tableName = "Favorites")
+data class Favorite(
+ @PrimaryKey
+ val movieId: Int,
+ val favorite: Boolean
+)
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/models/favorites/FavoriteWithMovieDetails.kt b/Movies/app/src/main/java/com/daresay/movies/data/models/favorites/FavoriteWithMovieDetails.kt
new file mode 100644
index 00000000..096c6406
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/models/favorites/FavoriteWithMovieDetails.kt
@@ -0,0 +1,14 @@
+package com.daresay.movies.data.models.favorites
+
+import androidx.room.Embedded
+import androidx.room.Relation
+import com.daresay.movies.data.models.moviedetails.MovieDetails
+
+data class FavoriteWithMovieDetails (
+ @Embedded val favorite: Favorite,
+ @Relation(
+ parentColumn = "movieId",
+ entityColumn = "id"
+ )
+ val movieDetails: MovieDetails
+)
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/models/movie/Movie.kt b/Movies/app/src/main/java/com/daresay/movies/data/models/movie/Movie.kt
new file mode 100644
index 00000000..a35ef6ea
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/models/movie/Movie.kt
@@ -0,0 +1,20 @@
+package com.daresay.movies.data.models.movie
+
+import java.io.Serializable
+
+data class Movie(
+ val adult: Boolean,
+ val backdrop_path: String,
+ val genre_ids: List,
+ val id: Int,
+ val original_language: String,
+ val original_title: String,
+ val overview: String,
+ val popularity: Double,
+ val poster_path: String,
+ val release_date: String,
+ val title: String,
+ val video: Boolean,
+ val vote_average: Float,
+ val vote_count: Int
+)
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/models/movie/MovieList.kt b/Movies/app/src/main/java/com/daresay/movies/data/models/movie/MovieList.kt
new file mode 100644
index 00000000..2f6ae0dd
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/models/movie/MovieList.kt
@@ -0,0 +1,8 @@
+package com.daresay.movies.data.models.movie
+
+data class MovieList (
+ val page: Int,
+ val results: List,
+ val total_pages: Int,
+ val total_results: Int
+)
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/models/moviedetails/AuthorDetails.kt b/Movies/app/src/main/java/com/daresay/movies/data/models/moviedetails/AuthorDetails.kt
new file mode 100644
index 00000000..0c221f50
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/models/moviedetails/AuthorDetails.kt
@@ -0,0 +1,8 @@
+package com.daresay.movies.data.models.moviedetails
+
+data class AuthorDetails(
+ val avatar_path: String?,
+ val name: String,
+ val rating: Float?,
+ val username: String
+)
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/models/moviedetails/Genre.kt b/Movies/app/src/main/java/com/daresay/movies/data/models/moviedetails/Genre.kt
new file mode 100644
index 00000000..ab71921a
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/models/moviedetails/Genre.kt
@@ -0,0 +1,6 @@
+package com.daresay.movies.data.models.moviedetails
+
+data class Genre(
+ val id: Int,
+ val name: String
+)
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/models/moviedetails/MovieDetails.kt b/Movies/app/src/main/java/com/daresay/movies/data/models/moviedetails/MovieDetails.kt
new file mode 100644
index 00000000..f3e342c6
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/models/moviedetails/MovieDetails.kt
@@ -0,0 +1,31 @@
+package com.daresay.movies.data.models.moviedetails
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "MovieDetails")
+data class MovieDetails(
+ val adult: Boolean,
+ val backdrop_path: String,
+ val budget: Int,
+ val genres: List,
+ val homepage: String,
+ @PrimaryKey
+ val id: Int,
+ val imdb_id: String,
+ val original_language: String,
+ val original_title: String,
+ val overview: String,
+ val popularity: Double,
+ val poster_path: String,
+ val release_date: String,
+ val revenue: Int,
+ val runtime: Int,
+ val status: String,
+ val tagline: String,
+ val title: String,
+ val video: Boolean,
+ val vote_average: Float,
+ val vote_count: Int,
+ val reviews: Reviews
+)
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/models/moviedetails/Result.kt b/Movies/app/src/main/java/com/daresay/movies/data/models/moviedetails/Result.kt
new file mode 100644
index 00000000..84b28a67
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/models/moviedetails/Result.kt
@@ -0,0 +1,11 @@
+package com.daresay.movies.data.models.moviedetails
+
+data class Result(
+ val author: String,
+ val author_details: AuthorDetails,
+ val content: String,
+ val created_at: String,
+ val id: String,
+ val updated_at: String,
+ val url: String
+)
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/models/moviedetails/Reviews.kt b/Movies/app/src/main/java/com/daresay/movies/data/models/moviedetails/Reviews.kt
new file mode 100644
index 00000000..fb22be35
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/models/moviedetails/Reviews.kt
@@ -0,0 +1,8 @@
+package com.daresay.movies.data.models.moviedetails
+
+data class Reviews(
+ val page: Int,
+ val results: List,
+ val total_pages: Int,
+ val total_results: Int
+)
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/data/user/UserData.kt b/Movies/app/src/main/java/com/daresay/movies/data/user/UserData.kt
new file mode 100644
index 00000000..93da290f
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/data/user/UserData.kt
@@ -0,0 +1,23 @@
+package com.daresay.movies.data.user
+
+import android.content.Context
+import android.content.SharedPreferences
+
+class UserData(private val context: Context) {
+ private fun privatePreferences(): SharedPreferences
+ = context.getSharedPreferences(PREFERENCE_KEY, Context.MODE_PRIVATE)
+
+ // Why yes, I do realize that after implementing this, the TMDB api calls that you instructed to use in this demo
+ // does not actually need a user session token. But I figured once I am employed we can scrap all the active Daresay
+ // projects in favor of building a movie application. And at that point we already have the functionality for getting a session token.
+ //
+ // And who's laughing then?
+ var sessionToken: String?
+ get() = privatePreferences().getString(SESSION_TOKEN, null)
+ set(value) = privatePreferences().edit().putString(SESSION_TOKEN, value).apply()
+
+ companion object {
+ private const val PREFERENCE_KEY = "ryBZOaZ8Smw8xxo"
+ private const val SESSION_TOKEN = "session_token"
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/di/AppModule.kt b/Movies/app/src/main/java/com/daresay/movies/di/AppModule.kt
new file mode 100644
index 00000000..719f8483
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/di/AppModule.kt
@@ -0,0 +1,68 @@
+package com.daresay.movies.di
+
+import android.content.Context
+import com.daresay.movies.data.api.TmdbRemoteDataSource
+import com.daresay.movies.data.api.TmdbRepository
+import com.daresay.movies.data.api.TmdbService
+import com.daresay.movies.data.local.AppDatabase
+import com.daresay.movies.data.local.MovieDao
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import javax.inject.Singleton
+
+
+@Module
+@InstallIn(SingletonComponent::class)
+object AppModule {
+ @Singleton
+ @Provides
+ fun provideRetrofit(gson: Gson) : Retrofit {
+ val httpClient = OkHttpClient.Builder()
+ httpClient.addInterceptor(
+ HttpLoggingInterceptor()
+ .setLevel(HttpLoggingInterceptor.Level.BODY)
+ )
+
+ return Retrofit.Builder()
+ .baseUrl("https://api.themoviedb.org/3/")
+ .addConverterFactory(GsonConverterFactory.create(gson))
+ .client(httpClient.build())
+ .build()
+ }
+
+ @Provides
+ fun provideGson() : Gson {
+ return GsonBuilder()
+ .setDateFormat("yyyy-MM-dd HH:mm:ss")
+ .create()
+ }
+
+ @Provides
+ fun provideTmdbService(retrofit: Retrofit): TmdbService = retrofit.create(TmdbService::class.java)
+
+ @Singleton
+ @Provides
+ fun provideTmdbRemoteDataSource(tmdbService: TmdbService) = TmdbRemoteDataSource(tmdbService)
+
+ @Singleton
+ @Provides
+ fun provideTmdbRepository(remoteDataSource: TmdbRemoteDataSource, localDataSource: MovieDao) =
+ TmdbRepository(remoteDataSource, localDataSource)
+
+ @Singleton
+ @Provides
+ fun provideDatabase(@ApplicationContext appContext: Context) = AppDatabase.getDatabase(appContext)
+
+ @Singleton
+ @Provides
+ fun provideMovieDetailsDao(db: AppDatabase) = db.movieDao()
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/extensions/Extensions.kt b/Movies/app/src/main/java/com/daresay/movies/extensions/Extensions.kt
new file mode 100644
index 00000000..897538b9
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/extensions/Extensions.kt
@@ -0,0 +1,9 @@
+package com.daresay.movies.extensions
+
+import android.view.View
+import androidx.annotation.StringRes
+import com.google.android.material.snackbar.Snackbar
+
+fun View.snack(@StringRes textRes: Int, length: Int = Snackbar.LENGTH_LONG) {
+ Snackbar.make(this, textRes, length).show()
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/extensions/UIExtensions.kt b/Movies/app/src/main/java/com/daresay/movies/extensions/UIExtensions.kt
new file mode 100644
index 00000000..425efc27
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/extensions/UIExtensions.kt
@@ -0,0 +1,10 @@
+package com.daresay.movies.extensions
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.LayoutRes
+
+fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View {
+ return LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/activities/MainActivity.kt b/Movies/app/src/main/java/com/daresay/movies/ui/activities/MainActivity.kt
new file mode 100644
index 00000000..4afba56d
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/activities/MainActivity.kt
@@ -0,0 +1,137 @@
+package com.daresay.movies.ui.activities
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.navigation.findNavController
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.ui.setupWithNavController
+import com.daresay.movies.R
+import com.daresay.movies.data.models.authentication.SessionRequestBody
+import com.daresay.movies.data.user.UserData
+import com.daresay.movies.databinding.ActivityMainBinding
+import com.daresay.movies.extensions.snack
+import com.daresay.movies.ui.viewmodels.AuthenticationViewModel
+import com.daresay.movies.utils.Resource
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class MainActivity : AppCompatActivity() {
+ private val viewModel: AuthenticationViewModel by viewModels()
+ private lateinit var binding: ActivityMainBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+
+ // If we have a request token set we will go go home screen
+ if (UserData(this).sessionToken == null) {
+ // We don't have a session token, so start the process of getting one
+ setUpRequestTokenObserver()
+ }
+ else {
+ moveToHome()
+ }
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+
+ // Check for incoming intent data from authentication
+ intent?.data?.let {
+ parseUrlData(it)
+ }
+ }
+
+ /**
+ * Here we will parse the uri from the intent after authenticating a user.
+ * If the authentication was successful, we will move to home screen.
+ */
+ private fun parseUrlData(uri: Uri) {
+ // Check if the request was approved
+ val approved = uri.getBooleanQueryParameter("approved", false)
+ if (approved) {
+ // Get the request_token and save it to shared_preferences
+ val requestToken = uri.getQueryParameter("request_token")
+ if (requestToken != null) {
+ setUpSessionTokenObserver(requestToken)
+ }
+ }
+ else {
+ // TODO: Show error dialog
+ }
+ }
+
+ /**
+ * Here we retrieve the user request token so we can make further API calls.
+ * After getting request token we will open up with tmdb authentication site and wait for user authentication.
+ * We will save the token in SharedPreferences for easier and automatic access by other API calls.
+ */
+ private fun setUpRequestTokenObserver() {
+ viewModel.userToken.observe(this, { it ->
+ when (it.status) {
+ Resource.Status.SUCCESS -> {
+ it.data?.let { token ->
+ openAuthenticationUrl(token.request_token)
+ }
+ }
+
+ Resource.Status.ERROR -> {
+ binding.root.snack(R.string.main_error_getting_request_token)
+ }
+
+ Resource.Status.LOADING -> {
+
+ }
+ }
+ })
+ }
+
+ /**
+ * Using the request token from above method we will fetch the session token so that we can use the api.
+ */
+ private fun setUpSessionTokenObserver(requestToken: String) {
+ viewModel.createSession(SessionRequestBody(requestToken)).observe(this, {
+ when (it.status) {
+ Resource.Status.SUCCESS -> {
+ it.data?.let { sessionToken ->
+ if (sessionToken.success) {
+ UserData(this).sessionToken = sessionToken.session_id
+ moveToHome()
+ }
+ }
+ }
+
+ Resource.Status.ERROR -> {
+ binding.root.snack(R.string.main_error_getting_session_token)
+ }
+
+ Resource.Status.LOADING -> {
+
+ }
+ }
+ })
+ }
+
+ /**
+ * Open browser to authenticate the request token.
+ * Read deep link data in @onNewIntent
+ */
+ private fun openAuthenticationUrl(requestToken: String) {
+ val url = "https://www.themoviedb.org/authenticate/${requestToken}?redirect_to=movies://authentication/?status=1"
+ val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
+ startActivity(browserIntent)
+ }
+
+ /**
+ * Navigate to the home screen.
+ * This can be called straight away if user has a request token set already, or after authenticating a new request token.
+ */
+ private fun moveToHome() {
+ findNavController(R.id.nav_host).navigate(R.id.homeViewPagerFragment)
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/activities/MovieDetailsActivity.kt b/Movies/app/src/main/java/com/daresay/movies/ui/activities/MovieDetailsActivity.kt
new file mode 100644
index 00000000..3bdfd6d0
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/activities/MovieDetailsActivity.kt
@@ -0,0 +1,165 @@
+package com.daresay.movies.ui.activities
+
+import android.content.res.ColorStateList
+import android.os.Bundle
+import android.view.View
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.CenterCrop
+import com.daresay.movies.R
+import com.daresay.movies.databinding.ActivityMovieDetailsBinding
+import com.daresay.movies.extensions.snack
+import com.daresay.movies.ui.adapters.GenreItemAdapter
+import com.daresay.movies.ui.adapters.ReviewAdapter
+import com.daresay.movies.ui.viewmodels.MovieDetailsViewModel
+import com.daresay.movies.utils.Resource
+import com.daresay.movies.utils.getMoviePosterBigUrl
+import dagger.hilt.android.AndroidEntryPoint
+import kotlin.collections.ArrayList
+
+@AndroidEntryPoint
+class MovieDetailsActivity : AppCompatActivity() {
+ private val viewModel: MovieDetailsViewModel by viewModels()
+ private lateinit var binding: ActivityMovieDetailsBinding
+ private val genreItemAdapter = GenreItemAdapter()
+ private val reviewItemAdapter = ReviewAdapter()
+
+ private var reviewPage = 1
+ private var reviewsMaxPage = 1
+ private var reviewPageLoading = false
+
+ private var movieId: Int = -1
+ private var isFavorite = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityMovieDetailsBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+
+ movieId = intent.getIntExtra("movie_id", -1)
+ if (movieId == -1) {
+ // TODO: Throw error
+ }
+
+ setUpGenreRecyclerView()
+ setUpReviewRecyclerView()
+ setUpMovieDetailsObserver(movieId = movieId)
+ setUpFavoriteObserver(movieId)
+
+ binding.loading = true
+ }
+
+ private fun setUpGenreRecyclerView() {
+ binding.genreRecyclerView.adapter = genreItemAdapter
+ }
+
+ private fun setUpReviewRecyclerView() {
+ val layoutManager = LinearLayoutManager(this)
+ layoutManager.orientation = LinearLayoutManager.VERTICAL
+ binding.reviewsRecyclerView.layoutManager = layoutManager
+
+ binding.reviewsRecyclerView.adapter = reviewItemAdapter
+ binding.reviewsRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ super.onScrolled(recyclerView, dx, dy)
+ if (reviewPage == reviewsMaxPage)
+ return
+
+ if (dx > 0) {
+ val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
+ if (!reviewPageLoading && lastVisibleItem > reviewItemAdapter.itemCount - 5) {
+ reviewPageLoading = true
+ loadReviews(++reviewPage)
+ }
+ }
+ }
+ })
+ }
+
+ private fun setUpMovieDetailsObserver(movieId: Int) {
+ viewModel.getMovieDetails(movieId).observe(this, { it ->
+ when (it.status) {
+ Resource.Status.SUCCESS -> {
+ it.data?.let { movieDetails ->
+ binding.movieDetails = movieDetails
+ reviewsMaxPage = movieDetails.reviews.total_pages
+ reviewItemAdapter.setItems(ArrayList(movieDetails.reviews.results))
+ genreItemAdapter.setItems(movieDetails.genres.map { genre -> genre.name})
+
+ Glide.with(binding.root)
+ .load(getMoviePosterBigUrl(movieDetails.poster_path))
+ .transform(CenterCrop())
+ .into(binding.movieImage)
+
+ binding.loading = false
+ }
+ }
+
+ Resource.Status.ERROR -> {
+ binding.root.snack(R.string.activity_movie_details_error_failed_to_load_movie)
+ }
+
+ Resource.Status.LOADING -> {
+ binding.loading = true
+ }
+ }
+ })
+ }
+
+ private fun loadReviews(page: Int) {
+ viewModel.getMovieReviews(movieId, page).observe(this, {
+ when (it.status) {
+ Resource.Status.SUCCESS -> {
+ it.data?.let { reviews ->
+ reviewItemAdapter.addItems(ArrayList(reviews.results))
+ reviewPageLoading = false
+ }
+ }
+
+ Resource.Status.ERROR -> {
+
+ }
+
+ Resource.Status.LOADING -> {
+
+ }
+ }
+ })
+ }
+
+ private fun setUpFavoriteObserver(movieId: Int) {
+ viewModel.getFavorite(movieId).observe(this, {
+ when (it.status) {
+ Resource.Status.SUCCESS -> {
+ it.data?.let { obj ->
+ setFavoriteButtonTint(obj.favorite)
+ isFavorite = obj.favorite
+ }
+ }
+
+ Resource.Status.ERROR -> {
+
+ }
+
+ Resource.Status.LOADING -> {
+
+ }
+ }
+ })
+ }
+
+ fun onFavoriteClicked(view: View) {
+ isFavorite = !isFavorite
+ viewModel.setMovieFavorite(movieId, isFavorite)
+ }
+
+ private fun setFavoriteButtonTint(isFavorite: Boolean) {
+ val colorRes = if (isFavorite) R.color.red else R.color.white
+ binding.favorite.imageTintList = ColorStateList.valueOf(ContextCompat.getColor(this, colorRes))
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/adapters/FavoriteMoviesAdapter.kt b/Movies/app/src/main/java/com/daresay/movies/ui/adapters/FavoriteMoviesAdapter.kt
new file mode 100644
index 00000000..a6fafa11
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/adapters/FavoriteMoviesAdapter.kt
@@ -0,0 +1,50 @@
+package com.daresay.movies.ui.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.CenterCrop
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.daresay.movies.data.models.moviedetails.MovieDetails
+import com.daresay.movies.databinding.AdapterFavoriteMovieItemBinding
+import com.daresay.movies.databinding.AdapterGenreItemBinding
+import com.daresay.movies.ui.callbacks.MovieOnClickListener
+import com.daresay.movies.utils.getGenreList
+import com.daresay.movies.utils.getMoviePosterUrl
+
+class FavoriteMoviesAdapter(private val onMovieOnClickListener: MovieOnClickListener) : RecyclerView.Adapter() {
+ private var items: List = emptyList()
+
+ fun setItems(items: List) {
+ this.items = items
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavoriteMoviesAdapter.GenreHolder {
+ val binding = AdapterFavoriteMovieItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return GenreHolder(binding)
+ }
+
+ override fun getItemCount() = items.size
+
+ override fun onBindViewHolder(holder: GenreHolder, position: Int) = holder.bind(items[position])
+
+ inner class GenreHolder(private val binding: AdapterFavoriteMovieItemBinding) : RecyclerView.ViewHolder(binding.root) {
+ private lateinit var movie: MovieDetails
+
+ fun bind(movie: MovieDetails) {
+ binding.movieDetails = movie
+ binding.movieTags.text = movie.genres.joinToString { it.name }
+
+ Glide.with(binding.root)
+ .load(getMoviePosterUrl(movie.poster_path))
+ .transform(CenterCrop(), RoundedCorners(25))
+ .into(binding.movieImage)
+
+ binding.root.setOnClickListener {
+ onMovieOnClickListener.onMovieClicked(movie.id)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/adapters/GenreItemAdapter.kt b/Movies/app/src/main/java/com/daresay/movies/ui/adapters/GenreItemAdapter.kt
new file mode 100644
index 00000000..dc8d5955
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/adapters/GenreItemAdapter.kt
@@ -0,0 +1,33 @@
+package com.daresay.movies.ui.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.daresay.movies.databinding.AdapterGenreItemBinding
+
+class GenreItemAdapter : RecyclerView.Adapter() {
+ private var items: List = emptyList()
+
+ fun setItems(items: List) {
+ this.items = items
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreItemAdapter.GenreHolder {
+ val binding = AdapterGenreItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return GenreHolder(binding)
+ }
+
+ override fun getItemCount() = items.size
+
+ override fun onBindViewHolder(holder: GenreHolder, position: Int) = holder.bind(items[position])
+
+ inner class GenreHolder(private val binding: AdapterGenreItemBinding) : RecyclerView.ViewHolder(binding.root) {
+ private lateinit var text: String
+
+ fun bind(text: String) {
+ this.text = text
+ binding.name = text
+ }
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/adapters/HomePagerAdapter.kt b/Movies/app/src/main/java/com/daresay/movies/ui/adapters/HomePagerAdapter.kt
new file mode 100644
index 00000000..eec41b5c
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/adapters/HomePagerAdapter.kt
@@ -0,0 +1,25 @@
+package com.daresay.movies.ui.adapters
+
+import androidx.fragment.app.Fragment
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import com.daresay.movies.ui.fragments.FavoritesFragment
+import com.daresay.movies.ui.fragments.MoviesFragment
+import java.lang.IndexOutOfBoundsException
+
+class HomePagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
+ private val tabFragments: Map Fragment> = mapOf(
+ MOVIES_PAGE_INDEX to { MoviesFragment() },
+ FAVORITES_PAGE_INDEX to { FavoritesFragment() }
+ )
+
+ override fun getItemCount() = tabFragments.size
+
+ override fun createFragment(position: Int): Fragment {
+ return tabFragments[position]?.invoke() ?: throw IndexOutOfBoundsException()
+ }
+
+ companion object {
+ const val MOVIES_PAGE_INDEX = 0
+ const val FAVORITES_PAGE_INDEX = 1
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/adapters/MoviePopularAdapter.kt b/Movies/app/src/main/java/com/daresay/movies/ui/adapters/MoviePopularAdapter.kt
new file mode 100644
index 00000000..e99b1deb
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/adapters/MoviePopularAdapter.kt
@@ -0,0 +1,53 @@
+package com.daresay.movies.ui.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.CenterCrop
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.daresay.movies.data.models.movie.Movie
+import com.daresay.movies.databinding.AdapterMoviePopularItemBinding
+import com.daresay.movies.databinding.AdapterMovieTopRatedItemBinding
+import com.daresay.movies.ui.callbacks.MovieOnClickListener
+import com.daresay.movies.utils.getGenreList
+import com.daresay.movies.utils.getMoviePosterUrl
+
+class MoviePopularAdapter(private val onMovieOnClickListener: MovieOnClickListener) : RecyclerView.Adapter() {
+ private var items: ArrayList = arrayListOf()
+
+ fun addItems(movies: ArrayList) {
+ val insertPosition = items.size
+ this.items.addAll(movies)
+ notifyItemRangeInserted(insertPosition, movies.size)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MoviePopularAdapter.MovieHolder {
+ val binding = AdapterMoviePopularItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return MovieHolder(binding)
+ }
+
+ override fun getItemCount() = items.size
+
+ override fun onBindViewHolder(holder: MovieHolder, position: Int) = holder.bind(items[position])
+
+ inner class MovieHolder(private val binding: AdapterMoviePopularItemBinding) : RecyclerView.ViewHolder(binding.root) {
+ private lateinit var movie: Movie
+
+ fun bind(movie: Movie) {
+ this.movie = movie
+
+ binding.movie = movie
+ binding.movieTags.text = getGenreList(movie.genre_ids).joinToString()
+
+ Glide.with(binding.root)
+ .load(getMoviePosterUrl(movie.poster_path))
+ .transform(CenterCrop(), RoundedCorners(25))
+ .into(binding.movieImage)
+
+ binding.root.setOnClickListener {
+ onMovieOnClickListener.onMovieClicked(movie.id)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/adapters/MovieTopRatedAdapter.kt b/Movies/app/src/main/java/com/daresay/movies/ui/adapters/MovieTopRatedAdapter.kt
new file mode 100644
index 00000000..dadb38f7
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/adapters/MovieTopRatedAdapter.kt
@@ -0,0 +1,54 @@
+package com.daresay.movies.ui.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.databinding.BindingAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.CenterCrop
+import com.bumptech.glide.load.resource.bitmap.CircleCrop
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.daresay.movies.data.models.movie.Movie
+import com.daresay.movies.databinding.AdapterMovieTopRatedItemBinding
+import com.daresay.movies.ui.callbacks.MovieOnClickListener
+import com.daresay.movies.utils.getGenreList
+import com.daresay.movies.utils.getMoviePosterUrl
+
+class MovieTopRatedAdapter(private val onMovieOnClickListener: MovieOnClickListener) : RecyclerView.Adapter() {
+ private var items: ArrayList = arrayListOf()
+
+ fun addItems(movies: ArrayList) {
+ val insertPosition = items.size
+ this.items.addAll(movies)
+ notifyItemRangeInserted(insertPosition, movies.size)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieTopRatedAdapter.MovieHolder {
+ val binding = AdapterMovieTopRatedItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return MovieHolder(binding)
+ }
+
+ override fun getItemCount() = items.size
+
+ override fun onBindViewHolder(holder: MovieHolder, position: Int) = holder.bind(items[position])
+
+ inner class MovieHolder(private val binding: AdapterMovieTopRatedItemBinding) : RecyclerView.ViewHolder(binding.root) {
+ private lateinit var movie: Movie
+
+ fun bind(movie: Movie) {
+ this.movie = movie
+
+ binding.movie = movie
+ binding.movieTags.text = getGenreList(movie.genre_ids).joinToString()
+
+ Glide.with(binding.root)
+ .load(getMoviePosterUrl(movie.poster_path))
+ .transform(CenterCrop(), RoundedCorners(25))
+ .into(binding.movieImage)
+
+ binding.root.setOnClickListener {
+ onMovieOnClickListener.onMovieClicked(movie.id)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/adapters/ReviewAdapter.kt b/Movies/app/src/main/java/com/daresay/movies/ui/adapters/ReviewAdapter.kt
new file mode 100644
index 00000000..ead83b3a
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/adapters/ReviewAdapter.kt
@@ -0,0 +1,52 @@
+package com.daresay.movies.ui.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.CenterCrop
+import com.bumptech.glide.load.resource.bitmap.CircleCrop
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.daresay.movies.R
+import com.daresay.movies.data.models.movie.Movie
+import com.daresay.movies.data.models.moviedetails.Result
+import com.daresay.movies.databinding.AdapterReviewItemBinding
+
+class ReviewAdapter : RecyclerView.Adapter() {
+ private var items: ArrayList = arrayListOf()
+
+ fun setItems(items: ArrayList) {
+ this.items = items
+ notifyDataSetChanged()
+ }
+
+ fun addItems(reviews: ArrayList) {
+ val insertPosition = items.size
+ this.items.addAll(reviews)
+ notifyItemRangeInserted(insertPosition, reviews.size)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReviewAdapter.ReviewHolder {
+ val binding = AdapterReviewItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return ReviewHolder(binding)
+ }
+
+ override fun getItemCount() = items.size
+
+ override fun onBindViewHolder(holder: ReviewHolder, position: Int) = holder.bind(items[position])
+
+ inner class ReviewHolder(private val binding: AdapterReviewItemBinding) : RecyclerView.ViewHolder(binding.root) {
+ private lateinit var review: Result
+
+ fun bind(review: Result) {
+ this.review = review
+ binding.result = review
+
+ Glide.with(binding.root)
+ .load(review.author_details.avatar_path?.removePrefix("/"))
+ .transform(CenterCrop())
+ .error(R.drawable.avatar_placeholder)
+ .into(binding.avatar)
+ }
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/callbacks/MovieOnClickListener.kt b/Movies/app/src/main/java/com/daresay/movies/ui/callbacks/MovieOnClickListener.kt
new file mode 100644
index 00000000..502cf087
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/callbacks/MovieOnClickListener.kt
@@ -0,0 +1,7 @@
+package com.daresay.movies.ui.callbacks
+
+import com.daresay.movies.data.models.movie.Movie
+
+interface MovieOnClickListener {
+ fun onMovieClicked(movieId: Int)
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/dialogs/ErrorDialog.kt b/Movies/app/src/main/java/com/daresay/movies/ui/dialogs/ErrorDialog.kt
new file mode 100644
index 00000000..e233eabf
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/dialogs/ErrorDialog.kt
@@ -0,0 +1,24 @@
+package com.daresay.movies.ui.dialogs
+
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import android.view.Window
+import com.daresay.movies.R
+import com.daresay.movies.databinding.DialogErrorBinding
+import com.daresay.movies.databinding.FragmentHomeViewPagerBinding
+
+class ErrorDialog(context: Context, private val errorMessage: String) : Dialog(context) {
+ private lateinit var binding: DialogErrorBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ requestWindowFeature(Window.FEATURE_NO_TITLE)
+ setContentView(R.layout.dialog_error)
+
+ binding.errorText.text = errorMessage
+ binding.close.setOnClickListener {
+ dismiss()
+ }
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/fragments/FavoritesFragment.kt b/Movies/app/src/main/java/com/daresay/movies/ui/fragments/FavoritesFragment.kt
new file mode 100644
index 00000000..77c1eb05
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/fragments/FavoritesFragment.kt
@@ -0,0 +1,71 @@
+package com.daresay.movies.ui.fragments
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.daresay.movies.data.models.movie.Movie
+import com.daresay.movies.databinding.FragmentFavoritesBinding
+import com.daresay.movies.ui.activities.MovieDetailsActivity
+import com.daresay.movies.ui.adapters.FavoriteMoviesAdapter
+import com.daresay.movies.ui.callbacks.MovieOnClickListener
+import com.daresay.movies.ui.viewmodels.MovieFavoritesViewModel
+import com.daresay.movies.utils.Resource
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class FavoritesFragment : Fragment(), MovieOnClickListener {
+ private val viewModel: MovieFavoritesViewModel by viewModels()
+ private lateinit var binding: FragmentFavoritesBinding
+ private val favoriteMoviesAdapter = FavoriteMoviesAdapter(this)
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentFavoritesBinding.inflate(inflater, container, false)
+
+ setUpFavoriteMoviesList()
+ setUpFavoriteMoviesObserver()
+
+ return binding.root
+ }
+
+ private fun setUpFavoriteMoviesList() {
+ val layoutManager = LinearLayoutManager(context)
+ layoutManager.orientation = LinearLayoutManager.VERTICAL
+ binding.favoriteRecyclerView.layoutManager = layoutManager
+ binding.favoriteRecyclerView.adapter = favoriteMoviesAdapter
+ }
+
+ private fun setUpFavoriteMoviesObserver() {
+ viewModel.getAllFavorites().observe(viewLifecycleOwner, { resource ->
+ when (resource.status) {
+ Resource.Status.SUCCESS -> {
+ resource.data?.let { movies ->
+ favoriteMoviesAdapter.setItems(movies.map { it.movieDetails })
+ }
+ }
+
+ Resource.Status.ERROR -> {
+
+ }
+
+ Resource.Status.LOADING -> {
+
+ }
+ }
+ })
+ }
+
+ override fun onMovieClicked(movieId: Int) {
+ val intent = Intent(requireContext(), MovieDetailsActivity::class.java)
+ intent.putExtra("movie_id", movieId)
+ startActivity(intent)
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/fragments/HomeViewPagerFragment.kt b/Movies/app/src/main/java/com/daresay/movies/ui/fragments/HomeViewPagerFragment.kt
new file mode 100644
index 00000000..5744eef1
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/fragments/HomeViewPagerFragment.kt
@@ -0,0 +1,52 @@
+package com.daresay.movies.ui.fragments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import com.daresay.movies.R
+import com.daresay.movies.ui.adapters.HomePagerAdapter
+import com.daresay.movies.databinding.FragmentHomeViewPagerBinding
+import com.google.android.material.tabs.TabLayoutMediator
+import dagger.hilt.android.AndroidEntryPoint
+import java.lang.IndexOutOfBoundsException
+
+@AndroidEntryPoint
+class HomeViewPagerFragment : Fragment() {
+ private lateinit var binding: FragmentHomeViewPagerBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentHomeViewPagerBinding.inflate(inflater, container, false)
+ binding.viewPager.adapter = HomePagerAdapter(this)
+ binding.viewPager.isUserInputEnabled = false
+
+ TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
+ tab.setIcon(getTabIcon(position))
+ tab.text = getTabText(position)
+ }.attach()
+
+ return binding.root
+ }
+
+ private fun getTabIcon(position: Int): Int {
+ return when (position) {
+ HomePagerAdapter.MOVIES_PAGE_INDEX -> R.drawable.ic_baseline_local_movies_24
+ HomePagerAdapter.FAVORITES_PAGE_INDEX -> R.drawable.ic_baseline_favorite_24
+ else -> throw IndexOutOfBoundsException()
+ }
+ }
+
+ private fun getTabText(position: Int): String {
+ return when (position) {
+ HomePagerAdapter.MOVIES_PAGE_INDEX -> getString(R.string.main_bottom_navigation_movies)
+ HomePagerAdapter.FAVORITES_PAGE_INDEX -> getString(R.string.main_bottom_navigation_favorites)
+ else -> throw IndexOutOfBoundsException()
+ }
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/fragments/LoadingFragment.kt b/Movies/app/src/main/java/com/daresay/movies/ui/fragments/LoadingFragment.kt
new file mode 100644
index 00000000..231e7e75
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/fragments/LoadingFragment.kt
@@ -0,0 +1,20 @@
+package com.daresay.movies.ui.fragments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import com.daresay.movies.R
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class LoadingFragment : Fragment() {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_loading, container, false)
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/fragments/MoviesFragment.kt b/Movies/app/src/main/java/com/daresay/movies/ui/fragments/MoviesFragment.kt
new file mode 100644
index 00000000..24254a24
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/fragments/MoviesFragment.kt
@@ -0,0 +1,157 @@
+package com.daresay.movies.ui.fragments
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.daresay.movies.R
+import com.daresay.movies.data.models.movie.Movie
+import com.daresay.movies.databinding.FragmentMoviesBinding
+import com.daresay.movies.extensions.snack
+import com.daresay.movies.ui.activities.MovieDetailsActivity
+import com.daresay.movies.ui.adapters.MoviePopularAdapter
+import com.daresay.movies.ui.adapters.MovieTopRatedAdapter
+import com.daresay.movies.ui.callbacks.MovieOnClickListener
+import com.daresay.movies.ui.viewmodels.MovieListViewModel
+import com.daresay.movies.utils.Resource
+import com.google.android.flexbox.*
+import dagger.hilt.android.AndroidEntryPoint
+
+
+@AndroidEntryPoint
+class MoviesFragment : Fragment(), MovieOnClickListener {
+ private val viewModel: MovieListViewModel by viewModels()
+ private lateinit var binding: FragmentMoviesBinding
+
+ private var topRatedMoviesPage = 1
+ private var topRatedMoviesPageSize = 1
+ private var topRatedMoviesPageLoading = false
+
+ private var popularMoviesPage = 1
+ private var popularMoviesPageSize = 1
+ private var popularMoviesPageLoading = false
+
+ private val topRatedMoviesAdapter = MovieTopRatedAdapter(this)
+ private val popularMoviesAdapter = MoviePopularAdapter(this)
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentMoviesBinding.inflate(inflater, container, false)
+
+ setUpTopRatedMoviesList()
+ setUpPopularMoviesList()
+
+ loadTopRatedMovies(topRatedMoviesPage)
+ loadPopularMovies(popularMoviesPage)
+
+ return binding.root
+ }
+
+ private fun setUpTopRatedMoviesList() {
+ val layoutManager = LinearLayoutManager(context)
+ layoutManager.orientation = LinearLayoutManager.HORIZONTAL
+
+ binding.topRatingLoading = true
+ binding.topRatedRecyclerView.layoutManager = layoutManager
+ binding.topRatedRecyclerView.adapter = topRatedMoviesAdapter
+ binding.topRatedRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ super.onScrolled(recyclerView, dx, dy)
+ if (topRatedMoviesPage == topRatedMoviesPageSize)
+ return
+
+ if (dx > 0) {
+ val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
+ if (!topRatedMoviesPageLoading && lastVisibleItem > topRatedMoviesAdapter.itemCount - 5) {
+ topRatedMoviesPageLoading = true
+ loadTopRatedMovies(++topRatedMoviesPage)
+ }
+ }
+ }
+ })
+ }
+
+ private fun loadTopRatedMovies(page: Int) {
+ viewModel.getTopRatedMovies(page).observe(viewLifecycleOwner, {
+ when (it.status) {
+ Resource.Status.SUCCESS -> {
+ it.data?.let { movies ->
+ topRatedMoviesAdapter.addItems(ArrayList(movies.results))
+ topRatedMoviesPageSize = movies.total_pages
+ binding.topRatingLoading = false
+ topRatedMoviesPageLoading = false
+ }
+ }
+
+ Resource.Status.ERROR -> {
+ view?.snack(R.string.fragment_movie_error_failed_to_load_top_rated_list)
+ }
+
+ Resource.Status.LOADING -> {
+
+ }
+ }
+ })
+ }
+
+ private fun setUpPopularMoviesList() {
+ binding.popularLoading = true
+ val layoutManager = FlexboxLayoutManager(context)
+ layoutManager.justifyContent = JustifyContent.SPACE_EVENLY
+ binding.popularRecyclerView.layoutManager = layoutManager
+ binding.popularRecyclerView.adapter = popularMoviesAdapter
+ binding.popularRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ super.onScrolled(recyclerView, dx, dy)
+ if (popularMoviesPage == popularMoviesPageSize)
+ return
+
+ if (dy > 0) {
+ val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
+ if (!popularMoviesPageLoading && lastVisibleItem > popularMoviesAdapter.itemCount - 5) {
+ popularMoviesPageLoading = true
+ loadPopularMovies(++popularMoviesPage)
+ }
+ }
+ }
+ })
+ }
+
+ private fun loadPopularMovies(page: Int) {
+ viewModel.getPopularMovies(page).observe(viewLifecycleOwner, {
+ when (it.status) {
+ Resource.Status.SUCCESS -> {
+ it.data?.let { movies ->
+ popularMoviesAdapter.addItems(ArrayList(movies.results))
+ popularMoviesPageSize = movies.total_pages
+ binding.popularLoading = false
+ popularMoviesPageLoading = false
+ }
+ }
+
+ Resource.Status.ERROR -> {
+ view?.snack(R.string.fragment_movie_error_failed_to_load_popular_list)
+ }
+
+ Resource.Status.LOADING -> {
+
+ }
+ }
+ })
+ }
+
+ override fun onMovieClicked(movieId: Int) {
+ val intent = Intent(requireContext(), MovieDetailsActivity::class.java)
+ intent.putExtra("movie_id", movieId)
+ startActivity(intent)
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/fragments/ReviewFragment.kt b/Movies/app/src/main/java/com/daresay/movies/ui/fragments/ReviewFragment.kt
new file mode 100644
index 00000000..84303681
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/fragments/ReviewFragment.kt
@@ -0,0 +1,25 @@
+package com.daresay.movies.ui.fragments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import com.daresay.movies.databinding.FragmentMoviesBinding
+import com.daresay.movies.databinding.FragmentReviewBinding
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class ReviewFragment : Fragment() {
+ private lateinit var binding: FragmentReviewBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentReviewBinding.inflate(inflater, container, false)
+
+ return binding.root
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/viewmodels/AuthenticationViewModel.kt b/Movies/app/src/main/java/com/daresay/movies/ui/viewmodels/AuthenticationViewModel.kt
new file mode 100644
index 00000000..9a8b873c
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/viewmodels/AuthenticationViewModel.kt
@@ -0,0 +1,13 @@
+package com.daresay.movies.ui.viewmodels
+
+import androidx.lifecycle.ViewModel
+import com.daresay.movies.data.api.TmdbRepository
+import com.daresay.movies.data.models.authentication.SessionRequestBody
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class AuthenticationViewModel @Inject constructor(private val repository: TmdbRepository) : ViewModel() {
+ val userToken = repository.getUserToken()
+ fun createSession(requestBody: SessionRequestBody) = repository.createSession(requestBody = requestBody)
+}
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/viewmodels/MovieDetailsViewModel.kt b/Movies/app/src/main/java/com/daresay/movies/ui/viewmodels/MovieDetailsViewModel.kt
new file mode 100644
index 00000000..42a83ef3
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/viewmodels/MovieDetailsViewModel.kt
@@ -0,0 +1,30 @@
+package com.daresay.movies.ui.viewmodels
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.ViewModel
+import com.daresay.movies.data.api.TmdbRepository
+import com.daresay.movies.data.models.favorites.Favorite
+import com.daresay.movies.data.models.moviedetails.MovieDetails
+import com.daresay.movies.data.models.moviedetails.Reviews
+import com.daresay.movies.utils.Resource
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class MovieDetailsViewModel @Inject constructor(private val repository: TmdbRepository) : ViewModel() {
+ fun getMovieDetails(movieId: Int) : LiveData> {
+ return repository.getMovieDetails(movieId = movieId)
+ }
+
+ fun getMovieReviews(movieId: Int, page: Int) : LiveData> {
+ return repository.getMovieReviews(movieId, page)
+ }
+
+ fun setMovieFavorite(movieId: Int, favorite: Boolean) {
+ repository.setMovieFavorite(movieId, favorite)
+ }
+
+ fun getFavorite(movieId: Int) : LiveData> {
+ return repository.getFavorite(movieId)
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/viewmodels/MovieFavoritesViewModel.kt b/Movies/app/src/main/java/com/daresay/movies/ui/viewmodels/MovieFavoritesViewModel.kt
new file mode 100644
index 00000000..45982992
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/viewmodels/MovieFavoritesViewModel.kt
@@ -0,0 +1,17 @@
+package com.daresay.movies.ui.viewmodels
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.ViewModel
+import com.daresay.movies.data.api.TmdbRepository
+import com.daresay.movies.data.models.favorites.FavoriteWithMovieDetails
+import com.daresay.movies.data.models.movie.MovieList
+import com.daresay.movies.utils.Resource
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class MovieFavoritesViewModel @Inject constructor(private val repository: TmdbRepository) : ViewModel() {
+ fun getAllFavorites() : LiveData>> {
+ return repository.getAllFavorites()
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/ui/viewmodels/MovieListViewModel.kt b/Movies/app/src/main/java/com/daresay/movies/ui/viewmodels/MovieListViewModel.kt
new file mode 100644
index 00000000..7d3a5781
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/ui/viewmodels/MovieListViewModel.kt
@@ -0,0 +1,22 @@
+package com.daresay.movies.ui.viewmodels
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.daresay.movies.data.api.TmdbRepository
+import com.daresay.movies.data.models.movie.Movie
+import com.daresay.movies.data.models.movie.MovieList
+import com.daresay.movies.utils.Resource
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class MovieListViewModel @Inject constructor(private val repository: TmdbRepository) : ViewModel() {
+ fun getTopRatedMovies(page: Int) : LiveData> {
+ return repository.getTopRatedMovies(page = page)
+ }
+
+ fun getPopularMovies(page: Int) : LiveData> {
+ return repository.getPopularMovies(page = page)
+ }
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/utils/DataAccessStrategy.kt b/Movies/app/src/main/java/com/daresay/movies/utils/DataAccessStrategy.kt
new file mode 100644
index 00000000..d0074477
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/utils/DataAccessStrategy.kt
@@ -0,0 +1,57 @@
+package com.daresay.movies.utils
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.liveData
+import androidx.lifecycle.map
+import com.daresay.movies.data.models.moviedetails.MovieDetails
+import kotlinx.coroutines.Dispatchers
+import com.daresay.movies.utils.Resource.Status.*
+
+/**
+ * Save response to database
+ */
+fun performGetOperation(
+ databaseQuery: () -> LiveData,
+ networkCall: suspend () -> Resource,
+ saveCallResult: suspend (A) -> Unit): LiveData> =
+ liveData(Dispatchers.IO) {
+ emit(Resource.loading())
+ val source = databaseQuery.invoke().map { Resource.success(it) }
+ emitSource(source)
+
+ val responseStatus = networkCall.invoke()
+ if (responseStatus.status == SUCCESS) {
+ saveCallResult(responseStatus.data!!)
+
+ } else if (responseStatus.status == ERROR) {
+ emit(Resource.error(responseStatus.message!!))
+ emitSource(source)
+ }
+ }
+
+/**
+ * Reduce boilerplate code for regular network operations
+ */
+fun performGetOperation(networkCall: suspend () -> Resource) : LiveData> =
+ liveData(Dispatchers.IO) {
+ emit(Resource.loading())
+
+ val responseStatus = networkCall.invoke()
+ if (responseStatus.status == SUCCESS) {
+ emit(responseStatus)
+
+ } else if (responseStatus.status == ERROR) {
+ emit(Resource.error(responseStatus.message!!))
+ }
+ }
+
+/**
+ * Reduce boilerplate code for regular database operations
+ */
+fun performDatabaseGetOperation(databaseQuery: () -> LiveData) : LiveData> =
+ liveData(Dispatchers.IO) {
+ emit(Resource.loading())
+
+ val source = databaseQuery.invoke().map { Resource.success(it) }
+ emitSource(source)
+ }
\ No newline at end of file
diff --git a/Movies/app/src/main/java/com/daresay/movies/utils/Resource.kt b/Movies/app/src/main/java/com/daresay/movies/utils/Resource.kt
new file mode 100644
index 00000000..c951e470
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/utils/Resource.kt
@@ -0,0 +1,21 @@
+package com.daresay.movies.utils
+
+data class Resource(val status: Status, val data: T?, val message: String?) {
+
+ enum class Status {
+ SUCCESS,
+ ERROR,
+ LOADING
+ }
+
+ companion object {
+ fun success(data: T): Resource =
+ Resource(Status.SUCCESS, data, null)
+
+ fun error(message: String, data: T? = null): Resource =
+ Resource(Status.ERROR, data, message)
+
+ fun loading(data: T? = null): Resource =
+ Resource(Status.LOADING, data, null)
+ }
+}
diff --git a/Movies/app/src/main/java/com/daresay/movies/utils/TmdbUtils.kt b/Movies/app/src/main/java/com/daresay/movies/utils/TmdbUtils.kt
new file mode 100644
index 00000000..f08ad6c0
--- /dev/null
+++ b/Movies/app/src/main/java/com/daresay/movies/utils/TmdbUtils.kt
@@ -0,0 +1,37 @@
+package com.daresay.movies.utils
+
+fun getMoviePosterUrl(image: String) = "https://image.tmdb.org/t/p/w500/${image}"
+fun getMoviePosterBigUrl(image: String) = "https://image.tmdb.org/t/p/w1280/${image}"
+
+val genreMap = mapOf(
+ 28 to "Action",
+ 12 to "Adventure",
+ 16 to "Animation",
+ 35 to "Comedy",
+ 80 to "Crime",
+ 99 to "Documentary",
+ 18 to "Drama",
+ 10751 to "Family",
+ 14 to "Fantasy",
+ 36 to "History",
+ 27 to "Horror",
+ 10402 to "Music",
+ 9648 to "Mystery",
+ 10749 to "Romance",
+ 878 to "Science Fiction",
+ 10770 to "TV Movie",
+ 53 to "Thriller",
+ 10752 to "War",
+ 37 to "Western")
+
+fun getGenreList(tags: List) : List {
+ var returnList = emptyList()
+
+ for (tag in tags) {
+ genreMap[tag]?.let {
+ returnList += it
+ }
+ }
+
+ return returnList
+}
\ No newline at end of file
diff --git a/Movies/app/src/main/res/anim/slide_in_bottom.xml b/Movies/app/src/main/res/anim/slide_in_bottom.xml
new file mode 100644
index 00000000..69070259
--- /dev/null
+++ b/Movies/app/src/main/res/anim/slide_in_bottom.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/anim/slide_out_bottom.xml b/Movies/app/src/main/res/anim/slide_out_bottom.xml
new file mode 100644
index 00000000..ecb5817f
--- /dev/null
+++ b/Movies/app/src/main/res/anim/slide_out_bottom.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/color/button_blue_selector.xml b/Movies/app/src/main/res/color/button_blue_selector.xml
new file mode 100644
index 00000000..959a8874
--- /dev/null
+++ b/Movies/app/src/main/res/color/button_blue_selector.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/color/tab_layout_item_selector.xml b/Movies/app/src/main/res/color/tab_layout_item_selector.xml
new file mode 100644
index 00000000..71909b20
--- /dev/null
+++ b/Movies/app/src/main/res/color/tab_layout_item_selector.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/drawable-v24/avatar_placeholder.png b/Movies/app/src/main/res/drawable-v24/avatar_placeholder.png
new file mode 100644
index 00000000..09892098
Binary files /dev/null and b/Movies/app/src/main/res/drawable-v24/avatar_placeholder.png differ
diff --git a/Movies/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/Movies/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..2b068d11
--- /dev/null
+++ b/Movies/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/drawable-v24/placehorder.jpg b/Movies/app/src/main/res/drawable-v24/placehorder.jpg
new file mode 100644
index 00000000..f38368ea
Binary files /dev/null and b/Movies/app/src/main/res/drawable-v24/placehorder.jpg differ
diff --git a/Movies/app/src/main/res/drawable/button_blue.xml b/Movies/app/src/main/res/drawable/button_blue.xml
new file mode 100644
index 00000000..e200fe5b
--- /dev/null
+++ b/Movies/app/src/main/res/drawable/button_blue.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/drawable/content_background_rounded_top.xml b/Movies/app/src/main/res/drawable/content_background_rounded_top.xml
new file mode 100644
index 00000000..b8805bb3
--- /dev/null
+++ b/Movies/app/src/main/res/drawable/content_background_rounded_top.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/drawable/content_blue.xml b/Movies/app/src/main/res/drawable/content_blue.xml
new file mode 100644
index 00000000..3026b453
--- /dev/null
+++ b/Movies/app/src/main/res/drawable/content_blue.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/drawable/dialog_window.xml b/Movies/app/src/main/res/drawable/dialog_window.xml
new file mode 100644
index 00000000..47189640
--- /dev/null
+++ b/Movies/app/src/main/res/drawable/dialog_window.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/drawable/ic_baseline_calendar_today_24.xml b/Movies/app/src/main/res/drawable/ic_baseline_calendar_today_24.xml
new file mode 100644
index 00000000..1cf969c7
--- /dev/null
+++ b/Movies/app/src/main/res/drawable/ic_baseline_calendar_today_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/Movies/app/src/main/res/drawable/ic_baseline_favorite_24.xml b/Movies/app/src/main/res/drawable/ic_baseline_favorite_24.xml
new file mode 100644
index 00000000..52d4d9bb
--- /dev/null
+++ b/Movies/app/src/main/res/drawable/ic_baseline_favorite_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/Movies/app/src/main/res/drawable/ic_baseline_list_24.xml b/Movies/app/src/main/res/drawable/ic_baseline_list_24.xml
new file mode 100644
index 00000000..b0e68e0e
--- /dev/null
+++ b/Movies/app/src/main/res/drawable/ic_baseline_list_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/Movies/app/src/main/res/drawable/ic_baseline_local_movies_24.xml b/Movies/app/src/main/res/drawable/ic_baseline_local_movies_24.xml
new file mode 100644
index 00000000..afcfb9c8
--- /dev/null
+++ b/Movies/app/src/main/res/drawable/ic_baseline_local_movies_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/Movies/app/src/main/res/drawable/ic_baseline_star_24.xml b/Movies/app/src/main/res/drawable/ic_baseline_star_24.xml
new file mode 100644
index 00000000..33832947
--- /dev/null
+++ b/Movies/app/src/main/res/drawable/ic_baseline_star_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/Movies/app/src/main/res/drawable/ic_baseline_timer_24.xml b/Movies/app/src/main/res/drawable/ic_baseline_timer_24.xml
new file mode 100644
index 00000000..18975721
--- /dev/null
+++ b/Movies/app/src/main/res/drawable/ic_baseline_timer_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/Movies/app/src/main/res/drawable/ic_launcher_background.xml b/Movies/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..07d5da9c
--- /dev/null
+++ b/Movies/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Movies/app/src/main/res/drawable/shimmer_background.xml b/Movies/app/src/main/res/drawable/shimmer_background.xml
new file mode 100644
index 00000000..e3131cd2
--- /dev/null
+++ b/Movies/app/src/main/res/drawable/shimmer_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/activity_main.xml b/Movies/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..22eb9f7c
--- /dev/null
+++ b/Movies/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/activity_movie_details.xml b/Movies/app/src/main/res/layout/activity_movie_details.xml
new file mode 100644
index 00000000..ded51595
--- /dev/null
+++ b/Movies/app/src/main/res/layout/activity_movie_details.xml
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/activity_movie_details_placeholder.xml b/Movies/app/src/main/res/layout/activity_movie_details_placeholder.xml
new file mode 100644
index 00000000..7996d878
--- /dev/null
+++ b/Movies/app/src/main/res/layout/activity_movie_details_placeholder.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/adapter_favorite_movie_item.xml b/Movies/app/src/main/res/layout/adapter_favorite_movie_item.xml
new file mode 100644
index 00000000..5e23f135
--- /dev/null
+++ b/Movies/app/src/main/res/layout/adapter_favorite_movie_item.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/adapter_genre_item.xml b/Movies/app/src/main/res/layout/adapter_genre_item.xml
new file mode 100644
index 00000000..e22020a5
--- /dev/null
+++ b/Movies/app/src/main/res/layout/adapter_genre_item.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/adapter_movie_popular_item.xml b/Movies/app/src/main/res/layout/adapter_movie_popular_item.xml
new file mode 100644
index 00000000..a9f8014d
--- /dev/null
+++ b/Movies/app/src/main/res/layout/adapter_movie_popular_item.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/adapter_movie_popular_item_placeholder.xml b/Movies/app/src/main/res/layout/adapter_movie_popular_item_placeholder.xml
new file mode 100644
index 00000000..e4c8e44f
--- /dev/null
+++ b/Movies/app/src/main/res/layout/adapter_movie_popular_item_placeholder.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/adapter_movie_top_rated_item.xml b/Movies/app/src/main/res/layout/adapter_movie_top_rated_item.xml
new file mode 100644
index 00000000..ad87e6ec
--- /dev/null
+++ b/Movies/app/src/main/res/layout/adapter_movie_top_rated_item.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/adapter_movie_top_rated_item_placeholder.xml b/Movies/app/src/main/res/layout/adapter_movie_top_rated_item_placeholder.xml
new file mode 100644
index 00000000..91420f8b
--- /dev/null
+++ b/Movies/app/src/main/res/layout/adapter_movie_top_rated_item_placeholder.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/adapter_review_item.xml b/Movies/app/src/main/res/layout/adapter_review_item.xml
new file mode 100644
index 00000000..cd6449c7
--- /dev/null
+++ b/Movies/app/src/main/res/layout/adapter_review_item.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/dialog_error.xml b/Movies/app/src/main/res/layout/dialog_error.xml
new file mode 100644
index 00000000..c023972f
--- /dev/null
+++ b/Movies/app/src/main/res/layout/dialog_error.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/fragment_authentication.xml b/Movies/app/src/main/res/layout/fragment_authentication.xml
new file mode 100644
index 00000000..44ee7867
--- /dev/null
+++ b/Movies/app/src/main/res/layout/fragment_authentication.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/fragment_favorites.xml b/Movies/app/src/main/res/layout/fragment_favorites.xml
new file mode 100644
index 00000000..7c9b89bf
--- /dev/null
+++ b/Movies/app/src/main/res/layout/fragment_favorites.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/fragment_home_view_pager.xml b/Movies/app/src/main/res/layout/fragment_home_view_pager.xml
new file mode 100644
index 00000000..65350794
--- /dev/null
+++ b/Movies/app/src/main/res/layout/fragment_home_view_pager.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/fragment_loading.xml b/Movies/app/src/main/res/layout/fragment_loading.xml
new file mode 100644
index 00000000..44ee7867
--- /dev/null
+++ b/Movies/app/src/main/res/layout/fragment_loading.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/fragment_movies.xml b/Movies/app/src/main/res/layout/fragment_movies.xml
new file mode 100644
index 00000000..845c592a
--- /dev/null
+++ b/Movies/app/src/main/res/layout/fragment_movies.xml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/layout/fragment_review.xml b/Movies/app/src/main/res/layout/fragment_review.xml
new file mode 100644
index 00000000..95eaf8cd
--- /dev/null
+++ b/Movies/app/src/main/res/layout/fragment_review.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/menu/bottom_navigation_menu.xml b/Movies/app/src/main/res/menu/bottom_navigation_menu.xml
new file mode 100644
index 00000000..2050d1d3
--- /dev/null
+++ b/Movies/app/src/main/res/menu/bottom_navigation_menu.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Movies/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/Movies/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Movies/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/Movies/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Movies/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..a571e600
Binary files /dev/null and b/Movies/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/Movies/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/Movies/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..61da551c
Binary files /dev/null and b/Movies/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/Movies/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Movies/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..c41dd285
Binary files /dev/null and b/Movies/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/Movies/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/Movies/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..db5080a7
Binary files /dev/null and b/Movies/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/Movies/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Movies/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..6dba46da
Binary files /dev/null and b/Movies/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/Movies/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/Movies/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..da31a871
Binary files /dev/null and b/Movies/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/Movies/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Movies/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..15ac6817
Binary files /dev/null and b/Movies/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/Movies/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/Movies/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..b216f2d3
Binary files /dev/null and b/Movies/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/Movies/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Movies/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..f25a4197
Binary files /dev/null and b/Movies/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/Movies/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/Movies/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..e96783cc
Binary files /dev/null and b/Movies/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/Movies/app/src/main/res/navigation/nav_bottom.xml b/Movies/app/src/main/res/navigation/nav_bottom.xml
new file mode 100644
index 00000000..3ff0dd2c
--- /dev/null
+++ b/Movies/app/src/main/res/navigation/nav_bottom.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/values-night/themes.xml b/Movies/app/src/main/res/values-night/themes.xml
new file mode 100644
index 00000000..749fc177
--- /dev/null
+++ b/Movies/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/values/colors.xml b/Movies/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..22bc267c
--- /dev/null
+++ b/Movies/app/src/main/res/values/colors.xml
@@ -0,0 +1,39 @@
+
+
+
+ #121212
+ #484848
+ #000000
+ #006495
+ #1ab4ff
+ #121212
+
+
+ #FFFFFF
+
+
+ #80000000
+ #FFFFFF
+ #252525
+ #f0f0f0
+ #D4D4D4
+ #ED615B
+ #EA4B44
+ #E91E63
+ #9C27B0
+ #673AB7
+ #3F51B5
+ #2196F3
+ #03A9F4
+ #00BCD4
+ #009688
+ #85BA75
+ #8BC34A
+ #CDDC39
+ #FFEB3B
+ #FFC107
+ #FF9800
+ #FF5722
+ #795548
+ #607D8B
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/values/dimens.xml b/Movies/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..d5e35bb7
--- /dev/null
+++ b/Movies/app/src/main/res/values/dimens.xml
@@ -0,0 +1,27 @@
+
+
+
+ 5dp
+ 10dp
+ 16dp
+ 32dp
+
+
+ 12sp
+ 15sp
+ 18sp
+ 38sp
+ 28sp
+
+
+ 5dp
+ 16dp
+ 32dp
+ 999dp
+
+
+ 40dp
+
+
+ 2dp
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/values/integer.xml b/Movies/app/src/main/res/values/integer.xml
new file mode 100644
index 00000000..0d2c4cc4
--- /dev/null
+++ b/Movies/app/src/main/res/values/integer.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/values/strings.xml b/Movies/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..68b3da17
--- /dev/null
+++ b/Movies/app/src/main/res/values/strings.xml
@@ -0,0 +1,40 @@
+
+ Movies
+
+
+ Placeholder
+ Error
+
+
+ No internet available
+
+
+ Movies
+ Favorites
+ Failed to get request token
+ Failed to get session token
+
+
+ Top rated
+ Popular
+ Failed to load top rated movies
+ Failed to load popular movies
+
+
+ Favorites
+
+
+ Story
+ Reviews
+ %d Minutes
+ Add to favorites
+ Movie poster
+ Failed to load popular movie details
+
+
+ Error
+
+
+ Movie poster
+
+
\ No newline at end of file
diff --git a/Movies/app/src/main/res/values/themes.xml b/Movies/app/src/main/res/values/themes.xml
new file mode 100644
index 00000000..60113d62
--- /dev/null
+++ b/Movies/app/src/main/res/values/themes.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Movies/app/src/test/java/com/daresay/movies/ExampleUnitTest.kt b/Movies/app/src/test/java/com/daresay/movies/ExampleUnitTest.kt
new file mode 100644
index 00000000..7ebc902a
--- /dev/null
+++ b/Movies/app/src/test/java/com/daresay/movies/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.daresay.movies
+
+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/Movies/build.gradle b/Movies/build.gradle
new file mode 100644
index 00000000..190c41fb
--- /dev/null
+++ b/Movies/build.gradle
@@ -0,0 +1,53 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ ext {
+ // App dependencies
+ kotlin_version = "1.4.31"
+ ktx_version = "1.3.2"
+ room_version = "2.2.6"
+ flexbox_version = "2.0.1"
+ facebook_shimmer_version = "0.5.0"
+ lifecycle_version = "2.2.0"
+ lifecycle_version_ktx = "2.3.0"
+ recycler_view_version = "1.1.0"
+ view_pager_version = "1.0.0"
+ retrofit_version = "2.9.0"
+ coroutines_version = "1.4.2"
+ gradle_version = "4.1.2"
+ app_compat_version = "1.2.0"
+ material_design_version = "1.3.0"
+ constraint_layout_version = "2.0.4"
+ nav_version = "2.3.3"
+ junit_version = "4.13.2"
+ junit_test_version = "1.1.2"
+ okhttp_logging_version = "4.7.2"
+ espresso_core_version = "3.3.0"
+ hilt_version = "2.31.2-alpha"
+ hilt_compiler_version = "1.0.0-alpha03"
+ timer_version = "4.7.1"
+ spinkit_version = "1.4.0"
+ glide_version = "4.11.0"
+ paging_version = "3.0.0-beta01"
+ debug_db_version = "1.0.6"
+ }
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
+ classpath "com.android.tools.build:gradle:$gradle_version"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/Movies/gradle.properties b/Movies/gradle.properties
new file mode 100644
index 00000000..769a63d4
--- /dev/null
+++ b/Movies/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# The Movie DB api key
+tmdb_api_key=f515f6ab960606a625e74c426a1ab8a0
\ No newline at end of file
diff --git a/Movies/gradle/wrapper/gradle-wrapper.jar b/Movies/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..f6b961fd
Binary files /dev/null and b/Movies/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/Movies/gradle/wrapper/gradle-wrapper.properties b/Movies/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..04f87ce0
--- /dev/null
+++ b/Movies/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Mar 04 17:58:03 CET 2021
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
diff --git a/Movies/gradlew b/Movies/gradlew
new file mode 100644
index 00000000..cccdd3d5
--- /dev/null
+++ b/Movies/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/Movies/gradlew.bat b/Movies/gradlew.bat
new file mode 100644
index 00000000..f9553162
--- /dev/null
+++ b/Movies/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/Movies/settings.gradle b/Movies/settings.gradle
new file mode 100644
index 00000000..53e0c60d
--- /dev/null
+++ b/Movies/settings.gradle
@@ -0,0 +1,2 @@
+include ':app'
+rootProject.name = "Movies"
\ No newline at end of file