diff --git a/app/build.gradle b/app/build.gradle index 3207070..d13da5f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -99,6 +99,7 @@ dependencies { // Moshi implementation 'com.squareup.moshi:moshi:1.9.2' + implementation 'com.github.bumptech.glide:glide:4.11.0' // MaterialColors implementation 'com.theah64.materialcolors:materialcolors:1.0.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cdc23fb..7acded4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,7 +16,7 @@ android:usesCleartextTraffic="true" tools:ignore="GoogleAppIndexingWarning" tools:targetApi="m"> - + - - - + - - + android:theme="@style/AppTheme.NoActionBar" /> diff --git a/app/src/main/java/com/theapache64/topcorn/data/local/AppDatabase.kt b/app/src/main/java/com/theapache64/topcorn/data/local/AppDatabase.kt index 171a2e3..d513eb8 100644 --- a/app/src/main/java/com/theapache64/topcorn/data/local/AppDatabase.kt +++ b/app/src/main/java/com/theapache64/topcorn/data/local/AppDatabase.kt @@ -6,7 +6,7 @@ import androidx.room.TypeConverters import com.theapache64.topcorn.data.local.daos.MoviesDao import com.theapache64.topcorn.data.remote.Movie -@Database(entities = [Movie::class], version = 1) +@Database(entities = [Movie::class, FavoriteMovie::class], version = 1) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun movieDao(): MoviesDao diff --git a/app/src/main/java/com/theapache64/topcorn/data/local/FavoriteMovie.kt b/app/src/main/java/com/theapache64/topcorn/data/local/FavoriteMovie.kt new file mode 100644 index 0000000..bd07bd5 --- /dev/null +++ b/app/src/main/java/com/theapache64/topcorn/data/local/FavoriteMovie.kt @@ -0,0 +1,34 @@ +package com.theapache64.topcorn.data.local + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.squareup.moshi.Json + +@Entity(tableName = "favorites") +data class FavoriteMovie( + @Json(name = "actors") + val actors: List?, + @Json(name = "desc") + val desc: String?, + @Json(name = "directors") + val directors: List?, + @Json(name = "genre") + val genre: List?, + @Json(name = "image_url") + val imageUrl: String?, + @Json(name = "thumb_url") + val thumbUrl: String?, + @Json(name = "imdb_url") + val imdbUrl: String?, + @Json(name = "name") + val name: String?, + @Json(name = "rating") + val rating: Float?, + @Json(name = "year") + val year: Int? +) { + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "id") + var id: Long = 0 +} \ No newline at end of file diff --git a/app/src/main/java/com/theapache64/topcorn/data/local/daos/MoviesDao.kt b/app/src/main/java/com/theapache64/topcorn/data/local/daos/MoviesDao.kt index 988b7cc..f4db97b 100644 --- a/app/src/main/java/com/theapache64/topcorn/data/local/daos/MoviesDao.kt +++ b/app/src/main/java/com/theapache64/topcorn/data/local/daos/MoviesDao.kt @@ -2,7 +2,9 @@ package com.theapache64.topcorn.data.local.daos import androidx.room.Dao import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.Query +import com.theapache64.topcorn.data.local.FavoriteMovie import com.theapache64.topcorn.data.remote.Movie import kotlinx.coroutines.flow.Flow @@ -16,4 +18,13 @@ interface MoviesDao { @Insert fun addAll(data: List) + + @Query("SELECT * FROM favorites") + suspend fun getAllFavoriteMovies(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(favoriteMovie: FavoriteMovie) + + @Query("DELETE FROM favorites WHERE imageUrl = :url") + suspend fun deleteByUrl(url: String) } diff --git a/app/src/main/java/com/theapache64/topcorn/data/repositories/movies/MoviesRepo.kt b/app/src/main/java/com/theapache64/topcorn/data/repositories/movies/MoviesRepo.kt index 5d3a198..716465e 100644 --- a/app/src/main/java/com/theapache64/topcorn/data/repositories/movies/MoviesRepo.kt +++ b/app/src/main/java/com/theapache64/topcorn/data/repositories/movies/MoviesRepo.kt @@ -2,6 +2,7 @@ package com.theapache64.topcorn.data.repositories.movies import android.content.SharedPreferences import androidx.core.content.edit +import com.theapache64.topcorn.data.local.FavoriteMovie import com.theapache64.topcorn.data.local.daos.MoviesDao import com.theapache64.topcorn.data.remote.ApiInterface import com.theapache64.topcorn.data.remote.Movie @@ -10,6 +11,8 @@ import com.theapache64.topcorn.utils.test.OpenForTesting import com.theapache64.twinkill.network.utils.Resource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import javax.inject.Inject @@ -61,6 +64,20 @@ class MoviesRepo @Inject constructor( }.asFlow().flowOn(Dispatchers.IO) } + suspend fun getAllFavoriteMovies() = moviesDao.getAllFavoriteMovies() + + suspend fun insertToFavoritesAsync(favoriteMovie: FavoriteMovie) = coroutineScope { + async { + moviesDao.insert(favoriteMovie) + } + } + + suspend fun deleteByUrlAsync(url: String) = coroutineScope { + async { + moviesDao.deleteByUrl(url) + } + } + @ExperimentalTime private fun isExpired(lastSynced: Long): Boolean { val currentTime = System.currentTimeMillis() diff --git a/app/src/main/java/com/theapache64/topcorn/di/modules/ActivitiesBuilderModule.kt b/app/src/main/java/com/theapache64/topcorn/di/modules/ActivitiesBuilderModule.kt index 214d074..a3c4f67 100644 --- a/app/src/main/java/com/theapache64/topcorn/di/modules/ActivitiesBuilderModule.kt +++ b/app/src/main/java/com/theapache64/topcorn/di/modules/ActivitiesBuilderModule.kt @@ -1,6 +1,7 @@ package com.theapache64.topcorn.di.modules +import com.theapache64.topcorn.ui.activities.favorites.FavoritesActivity import com.theapache64.topcorn.ui.activities.feed.FeedActivity import com.theapache64.topcorn.ui.activities.movie.MovieActivity import com.theapache64.topcorn.ui.activities.splash.SplashActivity @@ -22,4 +23,7 @@ abstract class ActivitiesBuilderModule { @ContributesAndroidInjector abstract fun getMovieActivity(): MovieActivity + @ContributesAndroidInjector + abstract fun getFavoritesActivity(): FavoritesActivity + } \ No newline at end of file diff --git a/app/src/main/java/com/theapache64/topcorn/di/modules/ViewModelModule.kt b/app/src/main/java/com/theapache64/topcorn/di/modules/ViewModelModule.kt index 56d42dc..9e2160b 100644 --- a/app/src/main/java/com/theapache64/topcorn/di/modules/ViewModelModule.kt +++ b/app/src/main/java/com/theapache64/topcorn/di/modules/ViewModelModule.kt @@ -1,6 +1,7 @@ package com.theapache64.topcorn.di.modules import androidx.lifecycle.ViewModel +import com.theapache64.topcorn.ui.activities.favorites.FavoritesViewModel import com.theapache64.topcorn.ui.activities.feed.FeedViewModel import com.theapache64.topcorn.ui.activities.movie.MovieViewModel import com.theapache64.topcorn.ui.activities.splash.SplashViewModel @@ -33,4 +34,9 @@ abstract class ViewModelModule { @ViewModelKey(MovieViewModel::class) abstract fun bindMovieViewModel(viewModel: MovieViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(FavoritesViewModel::class) + abstract fun bindFavoritesViewModel(viewModel: FavoritesViewModel): ViewModel + } \ No newline at end of file diff --git a/app/src/main/java/com/theapache64/topcorn/ui/activities/favorites/FavoritesActivity.kt b/app/src/main/java/com/theapache64/topcorn/ui/activities/favorites/FavoritesActivity.kt new file mode 100644 index 0000000..1879af2 --- /dev/null +++ b/app/src/main/java/com/theapache64/topcorn/ui/activities/favorites/FavoritesActivity.kt @@ -0,0 +1,56 @@ +package com.theapache64.topcorn.ui.activities.favorites + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.view.MenuItem +import android.view.View.GONE +import android.view.View.VISIBLE +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import com.theapache64.topcorn.R +import com.theapache64.topcorn.ui.activities.movie.MovieActivity +import com.theapache64.topcorn.ui.adapters.FavoritesAdapter +import dagger.android.AndroidInjection +import kotlinx.android.synthetic.main.activity_favorites.* +import javax.inject.Inject + +class FavoritesActivity : AppCompatActivity() { + + @Inject + lateinit var factory: ViewModelProvider.Factory + private lateinit var viewModel: FavoritesViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + AndroidInjection.inject(this) + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_favorites) + + supportActionBar?.apply { + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + setTitle(R.string.favorites) + } + + viewModel = ViewModelProvider(this, factory).get(FavoritesViewModel::class.java) + + viewModel.favoritesMovies.observe(this, Observer { + favorites_recycler.adapter = FavoritesAdapter(it) { movie -> + startActivity(MovieActivity.getStartIntent(this, movie)) + } + }) + + viewModel.isListEmpty.observe(this, Observer { empty -> + empty_text.visibility = if (empty) VISIBLE else GONE + }) + } + + override fun onResume() { + super.onResume() + viewModel.getFavorites() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) onBackPressed() + return super.onOptionsItemSelected(item) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/theapache64/topcorn/ui/activities/favorites/FavoritesViewModel.kt b/app/src/main/java/com/theapache64/topcorn/ui/activities/favorites/FavoritesViewModel.kt new file mode 100644 index 0000000..a54a9db --- /dev/null +++ b/app/src/main/java/com/theapache64/topcorn/ui/activities/favorites/FavoritesViewModel.kt @@ -0,0 +1,25 @@ +package com.theapache64.topcorn.ui.activities.favorites + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.theapache64.topcorn.data.local.FavoriteMovie +import com.theapache64.topcorn.data.repositories.movies.MoviesRepo +import kotlinx.coroutines.launch +import javax.inject.Inject + +class FavoritesViewModel @Inject constructor( + private val moviesRepo: MoviesRepo +) : ViewModel() { + + val favoritesMovies = MutableLiveData>() + val isListEmpty = MutableLiveData() + + fun getFavorites() { + viewModelScope.launch { + val favorites = moviesRepo.getAllFavoriteMovies() + favoritesMovies.postValue(favorites) + isListEmpty.value = favorites.isNullOrEmpty() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/theapache64/topcorn/ui/activities/feed/FeedActivity.kt b/app/src/main/java/com/theapache64/topcorn/ui/activities/feed/FeedActivity.kt index c4e3203..31ee2bb 100644 --- a/app/src/main/java/com/theapache64/topcorn/ui/activities/feed/FeedActivity.kt +++ b/app/src/main/java/com/theapache64/topcorn/ui/activities/feed/FeedActivity.kt @@ -15,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView import com.theapache64.topcorn.R import com.theapache64.topcorn.data.remote.Movie import com.theapache64.topcorn.databinding.ActivityFeedBinding +import com.theapache64.topcorn.ui.activities.favorites.FavoritesActivity import com.theapache64.topcorn.ui.activities.movie.MovieActivity import com.theapache64.topcorn.ui.adapters.FeedAdapter import com.theapache64.twinkill.logger.info @@ -94,13 +95,8 @@ class FeedActivity : BaseAppCompatActivity() { AppCompatDelegate.setDefaultNightMode(darkModeFlag) }) - // Watching for github home - viewModel.openGithub.observe(this, Observer { - val intent = Intent( - Intent.ACTION_VIEW, - Uri.parse(GITHUB_URL) - ) - startActivity(intent) + viewModel.openFavorites.observe(this, Observer { + startActivity(Intent(this, FavoritesActivity::class.java)) }) // Watching for toast diff --git a/app/src/main/java/com/theapache64/topcorn/ui/activities/feed/FeedViewModel.kt b/app/src/main/java/com/theapache64/topcorn/ui/activities/feed/FeedViewModel.kt index 88e52e8..0591099 100644 --- a/app/src/main/java/com/theapache64/topcorn/ui/activities/feed/FeedViewModel.kt +++ b/app/src/main/java/com/theapache64/topcorn/ui/activities/feed/FeedViewModel.kt @@ -69,7 +69,7 @@ class FeedViewModel @Inject constructor( private val _toast = MutableLiveData() val toast: LiveData = _toast - val openGithub = SingleLiveEvent() + val openFavorites = SingleLiveEvent() private val sortedOrder = MutableLiveData() @@ -115,7 +115,7 @@ class FeedViewModel @Inject constructor( } fun onHeartClicked() { - openGithub.value = true + openFavorites.value = true } /* diff --git a/app/src/main/java/com/theapache64/topcorn/ui/activities/movie/MovieActivity.kt b/app/src/main/java/com/theapache64/topcorn/ui/activities/movie/MovieActivity.kt index 391efd0..51e82f8 100644 --- a/app/src/main/java/com/theapache64/topcorn/ui/activities/movie/MovieActivity.kt +++ b/app/src/main/java/com/theapache64/topcorn/ui/activities/movie/MovieActivity.kt @@ -12,6 +12,7 @@ import com.theapache64.topcorn.databinding.ActivityMovieBinding import com.theapache64.twinkill.ui.activities.base.BaseAppCompatActivity import com.theapache64.twinkill.utils.extensions.bindContentView import dagger.android.AndroidInjection +import kotlinx.android.synthetic.main.activity_movie.* import javax.inject.Inject class MovieActivity : BaseAppCompatActivity() { @@ -44,6 +45,10 @@ class MovieActivity : BaseAppCompatActivity() { val movie = intent.getSerializableExtra(KEY_MOVIE) as Movie viewModel.init(movie) + viewModel.isFavorite.observe(this, Observer { + ib_favorite_toggle.setImageResource(if (it) R.drawable.ic_star_24 else R.drawable.ic_star_border_24) + }) + viewModel.closeActivity.observe(this, Observer { finish() }) diff --git a/app/src/main/java/com/theapache64/topcorn/ui/activities/movie/MovieViewModel.kt b/app/src/main/java/com/theapache64/topcorn/ui/activities/movie/MovieViewModel.kt index 2dd3ee6..3bb6747 100644 --- a/app/src/main/java/com/theapache64/topcorn/ui/activities/movie/MovieViewModel.kt +++ b/app/src/main/java/com/theapache64/topcorn/ui/activities/movie/MovieViewModel.kt @@ -1,14 +1,27 @@ package com.theapache64.topcorn.ui.activities.movie +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.theapache64.topcorn.data.local.FavoriteMovie import com.theapache64.topcorn.data.remote.Movie +import com.theapache64.topcorn.data.repositories.movies.MoviesRepo import com.theapache64.twinkill.utils.livedata.SingleLiveEvent +import kotlinx.coroutines.launch import javax.inject.Inject -class MovieViewModel @Inject constructor() : ViewModel() { +class MovieViewModel @Inject constructor( + private val moviesRepo: MoviesRepo +) : ViewModel() { + + val isFavorite = MutableLiveData() fun init(movie: Movie) { this.movie = movie + viewModelScope.launch { + val favorites = moviesRepo.getAllFavoriteMovies() + isFavorite.postValue(favorites.any { it.imageUrl == movie.imageUrl }) + } } val openImdb = SingleLiveEvent() @@ -19,6 +32,31 @@ class MovieViewModel @Inject constructor() : ViewModel() { closeActivity.value = true } + fun onFavoriteButtonClicked() { + viewModelScope.launch { + when (isFavorite.value) { + false -> { + moviesRepo.insertToFavoritesAsync( + FavoriteMovie( + movie?.actors, + movie?.desc, + movie?.directors, + movie?.genre, + movie?.imageUrl, + movie?.thumbUrl, + movie?.imdbUrl, + movie?.name, + movie?.rating, + movie?.year + ) + ) + } + true -> moviesRepo.deleteByUrlAsync(movie?.imageUrl ?: "") + } + } + isFavorite.value = isFavorite.value != true + } + fun onGoToImdbClicked() { openImdb.value = true } diff --git a/app/src/main/java/com/theapache64/topcorn/ui/adapters/FavoritesAdapter.kt b/app/src/main/java/com/theapache64/topcorn/ui/adapters/FavoritesAdapter.kt new file mode 100644 index 0000000..8d857c5 --- /dev/null +++ b/app/src/main/java/com/theapache64/topcorn/ui/adapters/FavoritesAdapter.kt @@ -0,0 +1,58 @@ +package com.theapache64.topcorn.ui.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.theapache64.topcorn.R +import com.theapache64.topcorn.data.local.FavoriteMovie +import com.theapache64.topcorn.data.remote.Movie +import kotlinx.android.synthetic.main.item_favorite.view.* + +class FavoritesAdapter( + private val favorites: List, + private val clickListener: (Movie) -> Unit +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = FavoritesViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_favorite, parent, false) + ) + + override fun onBindViewHolder(holder: FavoritesViewHolder, position: Int) { + val favorite = favorites[position] + with(holder) { + title.text = favorite.name + + Glide.with(itemView) + .load(favorite.imageUrl) + .into(poster) + + itemView.setOnClickListener { + clickListener( + Movie( + favorite.actors ?: emptyList(), + favorite.desc ?: "", + favorite.directors ?: emptyList(), + favorite.genre ?: emptyList(), + favorite.imageUrl ?: "", + favorite.thumbUrl ?: "", + favorite.imdbUrl ?: "", + favorite.name ?: "", + favorite.rating ?: 0F, + favorite.year ?: 0 + ) + ) + } + } + } + + override fun getItemCount() = favorites.size + + class FavoritesViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val title: TextView = view.tv_title + val poster: ImageView = view.iv_poster + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_star_24.xml b/app/src/main/res/drawable/ic_star_24.xml new file mode 100644 index 0000000..6335165 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_border_24.xml b/app/src/main/res/drawable/ic_star_border_24.xml new file mode 100644 index 0000000..a1c9b57 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_border_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_favorites.xml b/app/src/main/res/layout/activity_favorites.xml new file mode 100644 index 0000000..5631659 --- /dev/null +++ b/app/src/main/res/layout/activity_favorites.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_movie.xml b/app/src/main/res/layout/activity_movie.xml index fbf3dfd..b4d4d7a 100644 --- a/app/src/main/res/layout/activity_movie.xml +++ b/app/src/main/res/layout/activity_movie.xml @@ -33,6 +33,19 @@ app:layout_constraintTop_toTopOf="parent" tools:ignore="ContentDescription" /> + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a2231c5..8eb1b9c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,6 +8,6 @@ OPEN IMDB Sorted by year Sorted by rating - - + Favorites + Favorite list is empty diff --git a/app/src/test/java/com/theapache64/topcorn/ui/activities/feed/FeedActivityTest.kt b/app/src/test/java/com/theapache64/topcorn/ui/activities/feed/FeedActivityTest.kt index 7f2505a..b8599ec 100644 --- a/app/src/test/java/com/theapache64/topcorn/ui/activities/feed/FeedActivityTest.kt +++ b/app/src/test/java/com/theapache64/topcorn/ui/activities/feed/FeedActivityTest.kt @@ -1,21 +1,12 @@ package com.theapache64.topcorn.ui.activities.feed -import android.app.Activity -import android.app.Instrumentation import android.content.Context -import android.content.Intent import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.espresso.Espresso.onView import androidx.test.espresso.ViewAssertion -import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.intent.Intents -import androidx.test.espresso.intent.Intents.intended -import androidx.test.espresso.intent.Intents.intending -import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction -import androidx.test.espresso.intent.matcher.IntentMatchers.hasData import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry @@ -36,7 +27,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest -import org.hamcrest.CoreMatchers.allOf import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before @@ -113,20 +103,6 @@ class FeedActivityTest { ac.close() } - @Test - fun feed_onHeartClicked_goToGitHub() = runBlockingTest { - Intents.init() - val intentResult = Instrumentation.ActivityResult(Activity.RESULT_OK, Intent()) - val sendingIntent = allOf( - hasAction(Intent.ACTION_VIEW), - hasData(FeedActivity.GITHUB_URL) - ) - intending(sendingIntent).respondWith(intentResult) - onView(withId(R.id.ib_heart)).check(matches(isDisplayed())).perform(click()) - intended(sendingIntent) - Intents.release() - } - @Test fun feed_load_click() { diff --git a/build.gradle b/build.gradle index b1e5652..b419e66 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.0.0-beta05' + classpath 'com.android.tools.build:gradle:4.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module app.build.gradle files