From c0ab909114057221506a6105916bd0f4b993f9e5 Mon Sep 17 00:00:00 2001 From: jarnedemeulemeester Date: Fri, 30 Jul 2021 22:22:50 +0200 Subject: [PATCH] Add favorites fragment + switch settings for favorites on bottom nav --- .../dev/jdtech/jellyfin/BindingAdapters.kt | 7 ++ .../java/dev/jdtech/jellyfin/MainActivity.kt | 2 +- .../jellyfin/adapters/FavoritesListAdapter.kt | 63 +++++++++++++ .../jellyfin/fragments/FavoriteFragment.kt | 89 +++++++++++++++++++ .../jdtech/jellyfin/models/FavoriteSection.kt | 10 +++ .../jellyfin/repository/JellyfinRepository.kt | 2 + .../repository/JellyfinRepositoryImpl.kt | 13 +++ .../jellyfin/viewmodels/FavoriteViewModel.kt | 86 ++++++++++++++++++ app/src/main/res/layout/favorite_section.xml | 41 +++++++++ app/src/main/res/layout/fragment_favorite.xml | 63 +++++++++++++ app/src/main/res/menu/bottom_nav_menu.xml | 6 +- .../main/res/navigation/main_navigation.xml | 12 +++ app/src/main/res/values/strings.xml | 2 + 13 files changed, 392 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/dev/jdtech/jellyfin/adapters/FavoritesListAdapter.kt create mode 100644 app/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt create mode 100644 app/src/main/java/dev/jdtech/jellyfin/models/FavoriteSection.kt create mode 100644 app/src/main/java/dev/jdtech/jellyfin/viewmodels/FavoriteViewModel.kt create mode 100644 app/src/main/res/layout/favorite_section.xml create mode 100644 app/src/main/res/layout/fragment_favorite.xml diff --git a/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt b/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt index 8fbea6dd..40315453 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt @@ -8,6 +8,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import dev.jdtech.jellyfin.adapters.* import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.database.Server +import dev.jdtech.jellyfin.models.FavoriteSection import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemPerson import java.util.* @@ -166,4 +167,10 @@ fun bindItemPrimaryImage(imageView: ImageView, item: BaseItemDto?) { imageView.contentDescription = "${item.name} poster" } +} + +@BindingAdapter("favoriteSections") +fun bindFavoriteSections(recyclerView: RecyclerView, data: List?) { + val adapter = recyclerView.adapter as FavoritesListAdapter + adapter.submitList(data) } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt b/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt index 461f7b8e..fe8eb76b 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt @@ -34,7 +34,7 @@ class MainActivity : AppCompatActivity() { // menu should be considered as top level destinations. val appBarConfiguration = AppBarConfiguration( setOf( - R.id.navigation_home, R.id.navigation_media, R.id.navigation_settings + R.id.navigation_home, R.id.navigation_media, R.id.favoriteFragment ) ) setupActionBarWithNavController(navController, appBarConfiguration) diff --git a/app/src/main/java/dev/jdtech/jellyfin/adapters/FavoritesListAdapter.kt b/app/src/main/java/dev/jdtech/jellyfin/adapters/FavoritesListAdapter.kt new file mode 100644 index 00000000..e69f3ec3 --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/adapters/FavoritesListAdapter.kt @@ -0,0 +1,63 @@ +package dev.jdtech.jellyfin.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import dev.jdtech.jellyfin.databinding.FavoriteSectionBinding +import dev.jdtech.jellyfin.models.FavoriteSection + +class FavoritesListAdapter( + private val onClickListener: ViewItemListAdapter.OnClickListener, + private val onEpisodeClickListener: HomeEpisodeListAdapter.OnClickListener +) : ListAdapter(DiffCallback) { + class SectionViewHolder(private var binding: FavoriteSectionBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind( + section: FavoriteSection, + onClickListener: ViewItemListAdapter.OnClickListener, + onEpisodeClickListener: HomeEpisodeListAdapter.OnClickListener + ) { + binding.section = section + if (section.name == "Movies" || section.name == "Shows") { + binding.itemsRecyclerView.adapter = + ViewItemListAdapter(onClickListener, fixedWidth = true) + (binding.itemsRecyclerView.adapter as ViewItemListAdapter).submitList(section.items) + } else if (section.name == "Episodes") { + binding.itemsRecyclerView.adapter = + HomeEpisodeListAdapter(onEpisodeClickListener) + (binding.itemsRecyclerView.adapter as HomeEpisodeListAdapter).submitList(section.items) + } + binding.executePendingBindings() + } + } + + companion object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: FavoriteSection, newItem: FavoriteSection): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: FavoriteSection, + newItem: FavoriteSection + ): Boolean { + return oldItem == newItem + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionViewHolder { + return SectionViewHolder( + FavoriteSectionBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: SectionViewHolder, position: Int) { + val collection = getItem(position) + holder.bind(collection, onClickListener, onEpisodeClickListener) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt new file mode 100644 index 00000000..2f0b17bd --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt @@ -0,0 +1,89 @@ +package dev.jdtech.jellyfin.fragments + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.google.android.material.snackbar.Snackbar +import dagger.hilt.android.AndroidEntryPoint +import dev.jdtech.jellyfin.R +import dev.jdtech.jellyfin.adapters.FavoritesListAdapter +import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter +import dev.jdtech.jellyfin.adapters.ViewItemListAdapter +import dev.jdtech.jellyfin.databinding.FragmentFavoriteBinding +import dev.jdtech.jellyfin.viewmodels.FavoriteViewModel +import org.jellyfin.sdk.model.api.BaseItemDto + +@AndroidEntryPoint +class FavoriteFragment : Fragment() { + + private lateinit var binding: FragmentFavoriteBinding + private val viewModel: FavoriteViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentFavoriteBinding.inflate(inflater, container, false) + + val snackbar = + Snackbar.make( + binding.mainLayout, + getString(R.string.error_loading_data), + Snackbar.LENGTH_INDEFINITE + ) + snackbar.setAction(getString(R.string.retry)) { + viewModel.loadData() + } + + binding.lifecycleOwner = this + binding.viewModel = viewModel + binding.favoritesRecyclerView.adapter = FavoritesListAdapter( + ViewItemListAdapter.OnClickListener { item -> + navigateToMediaInfoFragment(item) + }, HomeEpisodeListAdapter.OnClickListener { item -> + navigateToEpisodeBottomSheetFragment(item) + }) + + viewModel.finishedLoading.observe(viewLifecycleOwner, { isFinished -> + binding.loadingIndicator.visibility = if (isFinished) View.GONE else View.VISIBLE + }) + + viewModel.error.observe(viewLifecycleOwner, { error -> + if (error) { + snackbar.show() + } + }) + + viewModel.favoriteSections.observe(viewLifecycleOwner, { sections -> + if (sections.isEmpty()) { + binding.noFavoritesText.visibility = View.VISIBLE + } else { + binding.noFavoritesText.visibility = View.GONE + } + }) + + return binding.root + } + + private fun navigateToMediaInfoFragment(item: BaseItemDto) { + findNavController().navigate( + FavoriteFragmentDirections.actionFavoriteFragmentToMediaInfoFragment( + item.id, + item.name, + item.type ?: "Unknown" + ) + ) + } + + private fun navigateToEpisodeBottomSheetFragment(episode: BaseItemDto) { + findNavController().navigate( + FavoriteFragmentDirections.actionFavoriteFragmentToEpisodeBottomSheetFragment( + episode.id + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/models/FavoriteSection.kt b/app/src/main/java/dev/jdtech/jellyfin/models/FavoriteSection.kt new file mode 100644 index 00000000..57279f99 --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/models/FavoriteSection.kt @@ -0,0 +1,10 @@ +package dev.jdtech.jellyfin.models + +import org.jellyfin.sdk.model.api.BaseItemDto +import java.util.* + +data class FavoriteSection( + val id: UUID, + val name: String, + var items: List +) \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt index a11c5ffe..e4648c27 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt @@ -12,6 +12,8 @@ interface JellyfinRepository { suspend fun getItems(parentId: UUID? = null): List + suspend fun getFavoriteItems(): List + suspend fun getResumeItems(): List suspend fun getLatestMedia(parentId: UUID): List diff --git a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index 0eb5f5c0..18f7916b 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -36,6 +36,19 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep return items } + override suspend fun getFavoriteItems(): List { + val items: List + withContext(Dispatchers.IO) { + items = jellyfinApi.itemsApi.getItems( + jellyfinApi.userId!!, + filters = listOf(ItemFilter.IS_FAVORITE), + includeItemTypes = listOf("Movie", "Series", "Episode"), + recursive = true + ).content.items ?: listOf() + } + return items + } + override suspend fun getResumeItems(): List { val items: List withContext(Dispatchers.IO) { diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/FavoriteViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/FavoriteViewModel.kt new file mode 100644 index 00000000..e9138d09 --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/FavoriteViewModel.kt @@ -0,0 +1,86 @@ +package dev.jdtech.jellyfin.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.jdtech.jellyfin.models.FavoriteSection +import dev.jdtech.jellyfin.repository.JellyfinRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.util.* +import javax.inject.Inject + +@HiltViewModel +class FavoriteViewModel +@Inject +constructor( + private val jellyfinRepository: JellyfinRepository +) : ViewModel() { + private val _favoriteSections = MutableLiveData>() + val favoriteSections: LiveData> = _favoriteSections + + private val _finishedLoading = MutableLiveData() + val finishedLoading: LiveData = _finishedLoading + + private val _error = MutableLiveData() + val error: LiveData = _error + + init { + loadData() + } + + fun loadData() { + _error.value = false + _finishedLoading.value = false + viewModelScope.launch { + try { + val items = jellyfinRepository.getFavoriteItems() + + if (items.isEmpty()) { + _favoriteSections.value = listOf() + _finishedLoading.value = true + return@launch + } + + val tempFavoriteSections = mutableListOf() + + withContext(Dispatchers.Default) { + FavoriteSection( + UUID.randomUUID(), + "Movies", + items.filter { it.type == "Movie" }).let { + if (it.items.isNotEmpty()) tempFavoriteSections.add( + it + ) + } + FavoriteSection( + UUID.randomUUID(), + "Shows", + items.filter { it.type == "Series" }).let { + if (it.items.isNotEmpty()) tempFavoriteSections.add( + it + ) + } + FavoriteSection( + UUID.randomUUID(), + "Episodes", + items.filter { it.type == "Episode" }).let { + if (it.items.isNotEmpty()) tempFavoriteSections.add( + it + ) + } + } + + _favoriteSections.value = tempFavoriteSections + } catch (e: Exception) { + Timber.e(e) + _error.value = true + } + _finishedLoading.value = true + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/favorite_section.xml b/app/src/main/res/layout/favorite_section.xml new file mode 100644 index 00000000..67fc3242 --- /dev/null +++ b/app/src/main/res/layout/favorite_section.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_favorite.xml b/app/src/main/res/layout/fragment_favorite.xml new file mode 100644 index 00000000..a4660e90 --- /dev/null +++ b/app/src/main/res/layout/fragment_favorite.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index 3cf04c3f..854dc67c 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -12,8 +12,8 @@ android:title="@string/title_media" /> + android:id="@+id/favoriteFragment" + android:icon="@drawable/ic_heart" + android:title="@string/title_favorite" /> \ No newline at end of file diff --git a/app/src/main/res/navigation/main_navigation.xml b/app/src/main/res/navigation/main_navigation.xml index 9ba73a34..7452d3b7 100644 --- a/app/src/main/res/navigation/main_navigation.xml +++ b/app/src/main/res/navigation/main_navigation.xml @@ -154,4 +154,16 @@ android:name="playbackPosition" app:argType="long" /> + + + + \ 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 24f3abd6..27989468 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ MainActivity Home My media + Favorites Settings View all Error loading data @@ -38,6 +39,7 @@ Continue Watching Latest %1$s Series poster + You have no favorites Language Preferred audio language