Skip to content

Commit

Permalink
Merge pull request #945 from UweTrottmann/897-similar-movies
Browse files Browse the repository at this point in the history
897 similar movies
  • Loading branch information
UweTrottmann authored Sep 21, 2023
2 parents 40f981f + 38c4b13 commit 1cb2bb9
Show file tree
Hide file tree
Showing 57 changed files with 530 additions and 98 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ Version 70
----------
*in development*


* 🌟 Movies: support displaying similar movies.
* 🔧 Switch to TMDBs recommendations when looking for similar shows or movies.

Version 69
----------
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
<activity
android:name=".movies.details.MovieDetailsActivity"
android:parentActivityName="com.battlelancer.seriesguide.ui.MoviesActivity" />
<activity android:name=".movies.similar.SimilarMoviesActivity" />

<!-- Settings -->
<activity android:name=".preferences.MoreOptionsActivity" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.battlelancer.seriesguide.movies
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.battlelancer.seriesguide.R
import com.battlelancer.seriesguide.databinding.ItemDiscoverMovieBinding
Expand All @@ -12,7 +13,7 @@ import com.squareup.picasso.Picasso
import com.uwetrottmann.tmdb2.entities.BaseMovie
import java.text.DateFormat

internal class MovieViewHolder(
class MovieViewHolder(
val binding: ItemDiscoverMovieBinding,
itemClickListener: MovieClickListener?
) : RecyclerView.ViewHolder(binding.root) {
Expand Down Expand Up @@ -89,6 +90,17 @@ internal class MovieViewHolder(
}

companion object {

val DIFF_CALLBACK_BASE_MOVIE = object : DiffUtil.ItemCallback<BaseMovie>() {
override fun areItemsTheSame(oldItem: BaseMovie, newItem: BaseMovie): Boolean =
oldItem.id == newItem.id

override fun areContentsTheSame(oldItem: BaseMovie, newItem: BaseMovie): Boolean =
oldItem.title == newItem.title
&& oldItem.release_date == newItem.release_date
&& oldItem.poster_path == newItem.poster_path
}

@JvmStatic
fun inflate(parent: ViewGroup, itemClickListener: MovieClickListener?): MovieViewHolder {
return MovieViewHolder(
Expand All @@ -99,9 +111,5 @@ internal class MovieViewHolder(
)
}

fun areContentsTheSame(oldItem: BaseMovie, newItem: BaseMovie): Boolean =
oldItem.title == newItem.title
&& oldItem.release_date == newItem.release_date
&& oldItem.poster_path == newItem.poster_path
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.battlelancer.seriesguide.extensions.MovieActionsContract
import com.battlelancer.seriesguide.movies.MovieLoader
import com.battlelancer.seriesguide.movies.MovieLocalizationDialogFragment
import com.battlelancer.seriesguide.movies.MoviesSettings
import com.battlelancer.seriesguide.movies.similar.SimilarMoviesActivity
import com.battlelancer.seriesguide.movies.tools.MovieTools
import com.battlelancer.seriesguide.people.PeopleListHelper
import com.battlelancer.seriesguide.settings.TmdbSettings
Expand Down Expand Up @@ -111,6 +112,16 @@ class MovieDetailsFragment : Fragment(), MovieActionsContract {
binding.buttonMovieTrailer.isGone = true
binding.buttonMovieTrailer.isEnabled = false

// similar movies button
binding.buttonMovieSimilar.apply {
setOnClickListener {
movieDetails?.tmdbMovie()
?.title
?.let { startActivity(SimilarMoviesActivity.intent(requireContext(), tmdbId, it)) }
}
isGone = true
}

// important action buttons
binding.containerMovieButtons.root.isGone = true
binding.containerMovieButtons.buttonMovieCheckIn.setOnClickListener { onButtonCheckInClick() }
Expand Down Expand Up @@ -266,26 +277,31 @@ class MovieDetailsFragment : Fragment(), MovieActionsContract {
?.let { ShareUtils.shareMovie(activity, tmdbId, it) }
true
}

R.id.menu_open_imdb -> {
movieDetails?.tmdbMovie()
?.let { ServiceUtils.openImdb(it.imdb_id, activity) }
true
}

R.id.menu_open_metacritic -> {
// Metacritic only has English titles, so using the original title is the best bet.
val titleOrNull = movieDetails?.tmdbMovie()
?.let { it.original_title ?: it.title }
titleOrNull?.let { Metacritic.searchForMovie(requireContext(), it) }
true
}

R.id.menu_open_tmdb -> {
WebTools.openInApp(requireContext(), TmdbTools.buildMovieUrl(tmdbId))
true
}

R.id.menu_open_trakt -> {
WebTools.openInApp(requireContext(), TraktTools.buildMovieUrl(tmdbId))
true
}

else -> false
}
}
Expand Down Expand Up @@ -325,6 +341,8 @@ class MovieDetailsFragment : Fragment(), MovieActionsContract {

// show trailer button (but trailer is loaded separately, just for animation)
binding.buttonMovieTrailer.isGone = false
// movie title should be available now, can show similar movies button
binding.buttonMovieSimilar.isGone = false

// hide check-in if not connected to trakt or hexagon is enabled
val isConnectedToTrakt = TraktCredentials.get(requireContext()).hasCredentials()
Expand Down Expand Up @@ -564,6 +582,7 @@ class MovieDetailsFragment : Fragment(), MovieActionsContract {
} else {
Utils.advertiseSubscription(context)
}

R.id.watched_popup_menu_set_not_watched -> MovieTools.unwatchedMovie(
context,
movieTmdbId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import com.uwetrottmann.tmdb2.entities.BaseMovie
internal class MoviesSearchAdapter(
private val context: Context,
private val itemClickListener: MovieClickListener
) : PagingDataAdapter<BaseMovie, RecyclerView.ViewHolder>(DIFF_CALLBACK) {
) : PagingDataAdapter<BaseMovie, RecyclerView.ViewHolder>(MovieViewHolder.DIFF_CALLBACK_BASE_MOVIE) {

private val dateFormatMovieReleaseDate = MovieTools.getMovieShortDateFormat()
private val posterBaseUrl = TmdbSettings.getPosterBaseUrl(context)
Expand All @@ -29,14 +29,4 @@ internal class MoviesSearchAdapter(
)
}

companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<BaseMovie>() {
override fun areItemsTheSame(oldItem: BaseMovie, newItem: BaseMovie) =
oldItem.id == newItem.id

override fun areContentsTheSame(oldItem: BaseMovie, newItem: BaseMovie): Boolean =
MovieViewHolder.areContentsTheSame(oldItem, newItem)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.battlelancer.seriesguide.movies.similar

import android.content.Context
import android.content.Intent
import androidx.fragment.app.Fragment
import com.battlelancer.seriesguide.R
import com.battlelancer.seriesguide.shows.search.similar.SimilarShowsFragment
import com.battlelancer.seriesguide.ui.BaseSimilarActivity

class SimilarMoviesActivity : BaseSimilarActivity() {

override val liftOnScrollTargetViewId: Int = SimilarShowsFragment.liftOnScrollTargetViewId
override val titleStringRes: Int = R.string.title_similar_movies
override fun createFragment(tmdbId: Int, title: String?): Fragment =
SimilarMoviesFragment.newInstance(tmdbId, title)

companion object {
fun intent(context: Context, movieTmdbId: Int, title: String?): Intent {
return Intent(context, SimilarMoviesActivity::class.java)
.putExtras(movieTmdbId, title)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.battlelancer.seriesguide.movies.similar

import android.content.Context
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import com.battlelancer.seriesguide.movies.MovieClickListenerImpl
import com.battlelancer.seriesguide.movies.MovieViewHolder
import com.battlelancer.seriesguide.movies.tools.MovieTools
import com.battlelancer.seriesguide.settings.TmdbSettings
import com.uwetrottmann.tmdb2.entities.BaseMovie

class SimilarMoviesAdapter(
private val context: Context,
) : ListAdapter<BaseMovie, MovieViewHolder>(
MovieViewHolder.DIFF_CALLBACK_BASE_MOVIE
) {

private val dateFormatMovieReleaseDate = MovieTools.getMovieShortDateFormat()
private val posterBaseUrl = TmdbSettings.getPosterBaseUrl(context)
private val itemClickListener = MovieClickListenerImpl(context)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
return MovieViewHolder.inflate(parent, itemClickListener)
}

override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
holder.bindTo(getItem(position), context, dateFormatMovieReleaseDate, posterBaseUrl)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package com.battlelancer.seriesguide.movies.similar

import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuProvider
import androidx.core.view.isGone
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.recyclerview.widget.RecyclerView
import com.battlelancer.seriesguide.R
import com.battlelancer.seriesguide.movies.search.MoviesSearchActivity
import com.battlelancer.seriesguide.ui.AutoGridLayoutManager
import com.battlelancer.seriesguide.ui.widgets.EmptyView
import com.battlelancer.seriesguide.util.ThemeUtils
import com.battlelancer.seriesguide.util.ViewTools
import com.uwetrottmann.seriesguide.widgets.EmptyViewSwipeRefreshLayout

class SimilarMoviesFragment : Fragment() {

private var tmdbId: Int = 0
private var title: String? = null

private val viewModel: SimilarMoviesViewModel by viewModels(
extrasProducer = {
MutableCreationExtras(defaultViewModelCreationExtras).apply {
set(
SimilarMoviesViewModel.KEY_TMDB_ID_MOVIE,
requireArguments().getInt(ARG_TMDB_ID)
)
}
},
factoryProducer = { SimilarMoviesViewModel.Factory }
)

private lateinit var swipeRefreshLayout: EmptyViewSwipeRefreshLayout
private lateinit var emptyView: EmptyView
private lateinit var recyclerView: RecyclerView

private lateinit var adapter: SimilarMoviesAdapter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

tmdbId = requireArguments().getInt(ARG_TMDB_ID)
title = requireArguments().getString(ARG_TITLE)

(activity as AppCompatActivity).supportActionBar?.subtitle = title
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val v = inflater.inflate(R.layout.fragment_shows_similar, container, false)
swipeRefreshLayout = v.findViewById(R.id.swipeRefreshLayoutShowsSimilar)
emptyView = v.findViewById(R.id.emptyViewShowsSimilar)
recyclerView = v.findViewById(R.id.recyclerViewShowsSimilar)
return v
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
ThemeUtils.applyBottomPaddingForNavigationBar(recyclerView)
ThemeUtils.applyBottomMarginForNavigationBar(view.findViewById(R.id.textViewPoweredByShowsSimilar))

swipeRefreshLayout.apply {
ViewTools.setSwipeRefreshLayoutColors(requireActivity().theme, this)
setOnRefreshListener { load() }
}
emptyView.setButtonClickListener {
swipeRefreshLayout.isRefreshing = true
load()
}

swipeRefreshLayout.isRefreshing = true
emptyView.isGone = true

recyclerView.apply {
setHasFixedSize(true)
layoutManager =
AutoGridLayoutManager(
context,
R.dimen.movie_grid_columnWidth,
1,
1
)
}

adapter = SimilarMoviesAdapter(requireContext())
recyclerView.adapter = adapter

viewModel.resultLiveData.observe(viewLifecycleOwner) {
adapter.submitList(it.results)
emptyView.setMessage(it.emptyMessage)
recyclerView.isGone = it.results.isNullOrEmpty()
emptyView.isGone = !it.results.isNullOrEmpty()
swipeRefreshLayout.isRefreshing = false
}

requireActivity().addMenuProvider(
optionsMenuProvider,
viewLifecycleOwner,
Lifecycle.State.RESUMED
)
}

private fun load() {
viewModel.loadSimilarMovies(tmdbId)
}

private val optionsMenuProvider = object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menu.add(0, MENU_ITEM_SEARCH_ID, 0, R.string.search).apply {
setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
setIcon(R.drawable.ic_search_white_24dp)
}
}

override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
MENU_ITEM_SEARCH_ID -> {
startActivity(Intent(requireContext(), MoviesSearchActivity::class.java))
true
}

else -> false
}
}
}

companion object {
val liftOnScrollTargetViewId = R.id.recyclerViewShowsSimilar

private const val ARG_TMDB_ID = "ARG_TMDB_ID"
private const val ARG_TITLE = "ARG_TITLE"
private const val MENU_ITEM_SEARCH_ID = 1

fun newInstance(tmdbId: Int, title: String?): SimilarMoviesFragment {
return SimilarMoviesFragment().apply {
arguments = Bundle().apply {
putInt(ARG_TMDB_ID, tmdbId)
putString(ARG_TITLE, title)
}
}
}
}
}
Loading

0 comments on commit 1cb2bb9

Please sign in to comment.