From c554aa87b1b175510d1a016d2fb173196c31b4c3 Mon Sep 17 00:00:00 2001 From: Uwe Trottmann Date: Fri, 15 Sep 2023 16:29:45 +0200 Subject: [PATCH 1/5] Use defaultViewModelCreationExtras and ViewModel factory helpers. --- .../CustomReleaseTimeDialogFragment.kt | 4 +--- .../overview/CustomReleaseTimeDialogModel.kt | 19 +++++++------------ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/overview/CustomReleaseTimeDialogFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/overview/CustomReleaseTimeDialogFragment.kt index a6cc2fd7fc..f6a93a05d8 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/overview/CustomReleaseTimeDialogFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/overview/CustomReleaseTimeDialogFragment.kt @@ -7,7 +7,6 @@ import androidx.appcompat.app.AppCompatDialogFragment import androidx.core.os.bundleOf import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle -import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.viewmodel.MutableCreationExtras @@ -34,8 +33,7 @@ class CustomReleaseTimeDialogFragment() : AppCompatDialogFragment() { private val model: CustomReleaseTimeDialogModel by viewModels( extrasProducer = { - MutableCreationExtras().apply { - set(APPLICATION_KEY, requireActivity().application) + MutableCreationExtras(defaultViewModelCreationExtras).apply { set( CustomReleaseTimeDialogModel.SHOW_ID_KEY, requireArguments().getLong(ARG_SHOW_ID) diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/overview/CustomReleaseTimeDialogModel.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/overview/CustomReleaseTimeDialogModel.kt index 7353e17085..bc905fb9f7 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/overview/CustomReleaseTimeDialogModel.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/overview/CustomReleaseTimeDialogModel.kt @@ -3,11 +3,11 @@ package com.battlelancer.seriesguide.shows.overview import android.app.Application import android.content.Context import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory import com.battlelancer.seriesguide.R import com.battlelancer.seriesguide.SgApp import com.battlelancer.seriesguide.provider.SgRoomDatabase @@ -138,16 +138,11 @@ class CustomReleaseTimeDialogModel(application: Application, private val showId: val SHOW_ID_KEY = object : CreationExtras.Key {} - val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create( - modelClass: Class, - extras: CreationExtras - ): T { - val application = extras[APPLICATION_KEY]!! - val showId = extras[SHOW_ID_KEY]!! - - return CustomReleaseTimeDialogModel(application, showId) as T + val Factory = viewModelFactory { + initializer { + val application = this[APPLICATION_KEY]!! + val showId = this[SHOW_ID_KEY]!! + CustomReleaseTimeDialogModel(application, showId) } } } From 7d43d6647440ac3624a00a558fb91488a9d394d7 Mon Sep 17 00:00:00 2001 From: Uwe Trottmann Date: Fri, 15 Sep 2023 17:17:48 +0200 Subject: [PATCH 2/5] MovieViewHolder: inline diff callback declaration. --- .../seriesguide/movies/MovieViewHolder.kt | 18 +++++++++++++----- .../movies/search/MoviesSearchAdapter.kt | 12 +----------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/battlelancer/seriesguide/movies/MovieViewHolder.kt b/app/src/main/java/com/battlelancer/seriesguide/movies/MovieViewHolder.kt index 464baec969..76d56d27b7 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/movies/MovieViewHolder.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/movies/MovieViewHolder.kt @@ -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 @@ -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) { @@ -89,6 +90,17 @@ internal class MovieViewHolder( } companion object { + + val DIFF_CALLBACK_BASE_MOVIE = object : DiffUtil.ItemCallback() { + 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( @@ -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 } } diff --git a/app/src/main/java/com/battlelancer/seriesguide/movies/search/MoviesSearchAdapter.kt b/app/src/main/java/com/battlelancer/seriesguide/movies/search/MoviesSearchAdapter.kt index 6d1ba1d0bb..0325bf35b0 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/movies/search/MoviesSearchAdapter.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/movies/search/MoviesSearchAdapter.kt @@ -14,7 +14,7 @@ import com.uwetrottmann.tmdb2.entities.BaseMovie internal class MoviesSearchAdapter( private val context: Context, private val itemClickListener: MovieClickListener -) : PagingDataAdapter(DIFF_CALLBACK) { +) : PagingDataAdapter(MovieViewHolder.DIFF_CALLBACK_BASE_MOVIE) { private val dateFormatMovieReleaseDate = MovieTools.getMovieShortDateFormat() private val posterBaseUrl = TmdbSettings.getPosterBaseUrl(context) @@ -29,14 +29,4 @@ internal class MoviesSearchAdapter( ) } - companion object { - private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: BaseMovie, newItem: BaseMovie) = - oldItem.id == newItem.id - - override fun areContentsTheSame(oldItem: BaseMovie, newItem: BaseMovie): Boolean = - MovieViewHolder.areContentsTheSame(oldItem, newItem) - } - } - } \ No newline at end of file From 7d3c838e213e4c88ee78876955be2555e7c70eba Mon Sep 17 00:00:00 2001 From: Uwe Trottmann Date: Fri, 15 Sep 2023 17:43:55 +0200 Subject: [PATCH 3/5] Movie details: add similar movies screen. --- CHANGELOG.md | 2 +- app/src/main/AndroidManifest.xml | 1 + .../movies/details/MovieDetailsFragment.kt | 19 +++ .../movies/similar/SimilarMoviesActivity.kt | 24 +++ .../movies/similar/SimilarMoviesAdapter.kt | 30 ++++ .../movies/similar/SimilarMoviesFragment.kt | 156 ++++++++++++++++++ .../movies/similar/SimilarMoviesViewModel.kt | 103 ++++++++++++ .../search/similar/SimilarShowsActivity.kt | 71 ++------ .../search/similar/SimilarShowsFragment.kt | 1 + .../seriesguide/ui/BaseSimilarActivity.kt | 81 +++++++++ .../res/layout-w1024dp/fragment_movie.xml | 14 +- .../main/res/layout-w590dp/fragment_movie.xml | 17 +- app/src/main/res/layout/fragment_movie.xml | 14 +- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 15 files changed, 469 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesActivity.kt create mode 100644 app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesAdapter.kt create mode 100644 app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesFragment.kt create mode 100644 app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesViewModel.kt create mode 100644 app/src/main/java/com/battlelancer/seriesguide/ui/BaseSimilarActivity.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a0bac263e..829636fb6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ Version 70 ---------- *in development* - +* 🌟 Movies: support displaying similar movies. Version 69 ---------- diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 82fb08a47e..37ac68d47f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -170,6 +170,7 @@ + diff --git a/app/src/main/java/com/battlelancer/seriesguide/movies/details/MovieDetailsFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/movies/details/MovieDetailsFragment.kt index c82cfe9931..f524fb8a24 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/movies/details/MovieDetailsFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/movies/details/MovieDetailsFragment.kt @@ -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 @@ -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() } @@ -266,11 +277,13 @@ 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() @@ -278,14 +291,17 @@ class MovieDetailsFragment : Fragment(), MovieActionsContract { 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 } } @@ -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() @@ -564,6 +582,7 @@ class MovieDetailsFragment : Fragment(), MovieActionsContract { } else { Utils.advertiseSubscription(context) } + R.id.watched_popup_menu_set_not_watched -> MovieTools.unwatchedMovie( context, movieTmdbId diff --git a/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesActivity.kt b/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesActivity.kt new file mode 100644 index 0000000000..de1b2fb7b8 --- /dev/null +++ b/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesActivity.kt @@ -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) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesAdapter.kt b/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesAdapter.kt new file mode 100644 index 0000000000..fc2c522fa7 --- /dev/null +++ b/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesAdapter.kt @@ -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( + 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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesFragment.kt new file mode 100644 index 0000000000..78de01e8b6 --- /dev/null +++ b/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesFragment.kt @@ -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) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesViewModel.kt b/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesViewModel.kt new file mode 100644 index 0000000000..ef62573aae --- /dev/null +++ b/app/src/main/java/com/battlelancer/seriesguide/movies/similar/SimilarMoviesViewModel.kt @@ -0,0 +1,103 @@ +package com.battlelancer.seriesguide.movies.similar + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import com.battlelancer.seriesguide.R +import com.battlelancer.seriesguide.SgApp +import com.battlelancer.seriesguide.movies.MoviesSettings +import com.battlelancer.seriesguide.util.Errors +import com.uwetrottmann.androidutils.AndroidUtils +import com.uwetrottmann.tmdb2.entities.BaseMovie +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import retrofit2.awaitResponse + +/** + * Loads similar movies from TMDB. + */ +class SimilarMoviesViewModel( + application: Application, + movieTmdbId: Int +) : AndroidViewModel(application) { + + val resultLiveData = MutableLiveData() + + init { + loadSimilarMovies(movieTmdbId) + } + + fun loadSimilarMovies(movieTmdbId: Int) { + viewModelScope.launch(Dispatchers.IO) { + val languageCode = MoviesSettings.getMoviesLanguage(getApplication()) + val page = try { + val response = SgApp.getServicesComponent(getApplication()).tmdb() + .moviesService() + .similar(movieTmdbId, null, languageCode) + .awaitResponse() + if (response.isSuccessful) { + response.body() + } else { + Errors.logAndReport("get similar movies", response) + postFailedResult() + return@launch + } + } catch (e: Exception) { + Errors.logAndReport("get similar movies", e) + postFailedResult() + return@launch + } + + val results = if (page?.results == null) { + postFailedResult() + return@launch + } else { + page.results + } + + postSuccessfulResult(results) + } + } + + private fun postFailedResult() { + val context = getApplication() + val message = if (AndroidUtils.isNetworkConnected(context)) { + context.getString(R.string.api_error_generic, context.getString(R.string.tmdb)) + } else { + context.getString(R.string.offline) + } + resultLiveData.postValue(Result(message, null)) + } + + private fun postSuccessfulResult(results: List) { + resultLiveData.postValue( + Result( + getApplication().getString(R.string.empty_no_results), + results + ) + ) + } + + data class Result( + val emptyMessage: String, + val results: List? + ) + + companion object { + val KEY_TMDB_ID_MOVIE = object : CreationExtras.Key {} + + val Factory = viewModelFactory { + initializer { + val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]!! + val movieTmdbId = this[KEY_TMDB_ID_MOVIE]!! + SimilarMoviesViewModel(application, movieTmdbId) + } + } + } + +} diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsActivity.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsActivity.kt index 90b62af678..fae4697740 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsActivity.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsActivity.kt @@ -3,54 +3,26 @@ package com.battlelancer.seriesguide.shows.search.similar import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.MenuItem +import androidx.fragment.app.Fragment import com.battlelancer.seriesguide.R import com.battlelancer.seriesguide.shows.search.discover.AddShowDialogFragment import com.battlelancer.seriesguide.shows.search.discover.SearchResult -import com.battlelancer.seriesguide.ui.BaseActivity -import com.battlelancer.seriesguide.ui.SinglePaneActivity +import com.battlelancer.seriesguide.ui.BaseSimilarActivity import com.battlelancer.seriesguide.util.TaskManager -class SimilarShowsActivity : BaseActivity(), AddShowDialogFragment.OnAddShowListener { +class SimilarShowsActivity : BaseSimilarActivity(), AddShowDialogFragment.OnAddShowListener { + + override val liftOnScrollTargetViewId: Int = SimilarShowsFragment.liftOnScrollTargetViewId + override val titleStringRes: Int = R.string.title_similar_shows + override fun createFragment(tmdbId: Int, title: String?): Fragment = + SimilarShowsFragment.newInstance(tmdbId, title) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val binding = SinglePaneActivity.onCreateFor(this) - binding.sgAppBarLayout.sgAppBarLayout.liftOnScrollTargetViewId = - SimilarShowsFragment.liftOnScrollTargetViewId - setupActionBar() - - val showTmdbId = intent.getIntExtra(EXTRA_SHOW_TMDB_ID, 0) - if (showTmdbId <= 0) { - finish() - return - } - val showTitle = intent.getStringExtra(EXTRA_SHOW_TITLE) - - if (savedInstanceState == null) { - addFragmentWithSimilarShows(showTmdbId, showTitle) - } + // Can display similar shows from the add dialog, so build a stack of them. SimilarShowsFragment.displaySimilarShowsEventLiveData.observe(this) { - addFragmentWithSimilarShows(it.tmdbId, it.title, true) - } - } - - override fun setupActionBar() { - super.setupActionBar() - supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_clear_24dp) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - setTitle(R.string.title_similar_shows) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> { - // Go to the last activity. - finish() - true - } - else -> super.onOptionsItemSelected(item) + addFragment(it.tmdbId, it.title, true) } } @@ -58,31 +30,10 @@ class SimilarShowsActivity : BaseActivity(), AddShowDialogFragment.OnAddShowList TaskManager.getInstance().performAddTask(this, show) } - private fun addFragmentWithSimilarShows( - showTmdbId: Int, - showTitle: String?, - addToBackStack: Boolean = false - ) { - val fragment = SimilarShowsFragment.newInstance(showTmdbId, showTitle) - supportFragmentManager.beginTransaction().apply { - if (addToBackStack) { - replace(R.id.content_frame, fragment) - addToBackStack(null) - } else { - add(R.id.content_frame, fragment) - } - }.commit() - } - companion object { - private const val EXTRA_SHOW_TMDB_ID = "EXTRA_SHOW_TMDB_ID" - private const val EXTRA_SHOW_TITLE = "EXTRA_SHOW_TITLE" - - @JvmStatic fun intent(context: Context, showTmdbId: Int, showTitle: String?): Intent { return Intent(context, SimilarShowsActivity::class.java) - .putExtra(EXTRA_SHOW_TMDB_ID, showTmdbId) - .putExtra(EXTRA_SHOW_TITLE, showTitle) + .putExtras(showTmdbId, showTitle) } } diff --git a/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsFragment.kt b/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsFragment.kt index 3e8e63054e..7f1e2fef5c 100644 --- a/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsFragment.kt +++ b/app/src/main/java/com/battlelancer/seriesguide/shows/search/similar/SimilarShowsFragment.kt @@ -51,6 +51,7 @@ class SimilarShowsFragment : BaseAddShowsFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View? { + // Note: this layout is currently re-used by SimilarMoviesFragment. val v = inflater.inflate(R.layout.fragment_shows_similar, container, false) swipeRefreshLayout = v.findViewById(R.id.swipeRefreshLayoutShowsSimilar) emptyView = v.findViewById(R.id.emptyViewShowsSimilar) diff --git a/app/src/main/java/com/battlelancer/seriesguide/ui/BaseSimilarActivity.kt b/app/src/main/java/com/battlelancer/seriesguide/ui/BaseSimilarActivity.kt new file mode 100644 index 0000000000..f8b01c4152 --- /dev/null +++ b/app/src/main/java/com/battlelancer/seriesguide/ui/BaseSimilarActivity.kt @@ -0,0 +1,81 @@ +package com.battlelancer.seriesguide.ui + +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import androidx.fragment.app.Fragment +import com.battlelancer.seriesguide.R + +abstract class BaseSimilarActivity : BaseActivity() { + + abstract val liftOnScrollTargetViewId: Int + abstract val titleStringRes: Int + abstract fun createFragment(tmdbId: Int, title: String?): Fragment + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val binding = SinglePaneActivity.onCreateFor(this) + binding.sgAppBarLayout.sgAppBarLayout.liftOnScrollTargetViewId = + liftOnScrollTargetViewId + setupActionBar() + + val tmdbId = intent.getIntExtra(EXTRA_TMDB_ID, 0) + if (tmdbId <= 0) { + finish() + return + } + + val title = intent.getStringExtra(EXTRA_TITLE) + if (savedInstanceState == null) { + addFragment(tmdbId, title) + } + } + + override fun setupActionBar() { + super.setupActionBar() + supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_clear_24dp) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + setTitle(titleStringRes) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + // Go to the previous activity. + finish() + true + } + + else -> super.onOptionsItemSelected(item) + } + } + + + fun addFragment( + tmdbId: Int, + title: String?, + addToBackStack: Boolean = false + ) { + val fragment = createFragment(tmdbId, title) + supportFragmentManager.beginTransaction().apply { + if (addToBackStack) { + replace(R.id.content_frame, fragment) + addToBackStack(null) + } else { + add(R.id.content_frame, fragment) + } + }.commit() + } + + companion object { + private const val EXTRA_TMDB_ID = "EXTRA_TMDB_ID" + private const val EXTRA_TITLE = "EXTRA_TITLE" + + fun Intent.putExtras(showTmdbId: Int, title: String?): Intent { + return this + .putExtra(EXTRA_TMDB_ID, showTmdbId) + .putExtra(EXTRA_TITLE, title) + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout-w1024dp/fragment_movie.xml b/app/src/main/res/layout-w1024dp/fragment_movie.xml index c0c2ded1b7..2a265ea9e7 100644 --- a/app/src/main/res/layout-w1024dp/fragment_movie.xml +++ b/app/src/main/res/layout-w1024dp/fragment_movie.xml @@ -100,12 +100,24 @@ android:layout_height="wrap_content" android:layout_below="@+id/textViewMovieDate" android:layout_marginRight="@dimen/large_padding" - android:layout_marginBottom="@dimen/large_padding" android:layout_toRightOf="@id/frameLayoutMoviePoster" android:text="@string/trailer" app:icon="@drawable/ic_movie_white_24dp" app:iconGravity="start" /> +