Add favorites fragment + switch settings for favorites on bottom nav

This commit is contained in:
jarnedemeulemeester 2021-07-30 22:22:50 +02:00
parent edb0b15694
commit c0ab909114
No known key found for this signature in database
GPG key ID: 60884A0C1EBA43E5
13 changed files with 392 additions and 4 deletions

View file

@ -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<FavoriteSection>?) {
val adapter = recyclerView.adapter as FavoritesListAdapter
adapter.submitList(data)
}

View file

@ -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)

View file

@ -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<FavoriteSection, FavoritesListAdapter.SectionViewHolder>(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<FavoriteSection>() {
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)
}
}

View file

@ -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
)
)
}
}

View file

@ -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<BaseItemDto>
)

View file

@ -12,6 +12,8 @@ interface JellyfinRepository {
suspend fun getItems(parentId: UUID? = null): List<BaseItemDto>
suspend fun getFavoriteItems(): List<BaseItemDto>
suspend fun getResumeItems(): List<BaseItemDto>
suspend fun getLatestMedia(parentId: UUID): List<BaseItemDto>

View file

@ -36,6 +36,19 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
return items
}
override suspend fun getFavoriteItems(): List<BaseItemDto> {
val items: List<BaseItemDto>
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<BaseItemDto> {
val items: List<BaseItemDto>
withContext(Dispatchers.IO) {

View file

@ -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<List<FavoriteSection>>()
val favoriteSections: LiveData<List<FavoriteSection>> = _favoriteSections
private val _finishedLoading = MutableLiveData<Boolean>()
val finishedLoading: LiveData<Boolean> = _finishedLoading
private val _error = MutableLiveData<Boolean>()
val error: LiveData<Boolean> = _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<FavoriteSection>()
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
}
}
}

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="section"
type="dev.jdtech.jellyfin.models.FavoriteSection" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="12dp">
<TextView
android:id="@+id/section_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginBottom="12dp"
android:text="@{section.name}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:textSize="18sp"
tools:text="Movies" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/items_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:layoutAnimation="@anim/overview_media_animation"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/home_episode_item" />
</LinearLayout>
</layout>

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="dev.jdtech.jellyfin.viewmodels.FavoriteViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.FavoriteFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loading_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:trackCornerRadius="10dp" />
<TextView
android:id="@+id/no_favorites_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_favorites"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/favorites_recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingTop="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:favoriteSections="@{viewModel.favoriteSections}"
tools:itemCount="4"
tools:listitem="@layout/favorite_section" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -12,8 +12,8 @@
android:title="@string/title_media" />
<item
android:id="@+id/navigation_settings"
android:icon="@drawable/ic_settings"
android:title="@string/title_settings" />
android:id="@+id/favoriteFragment"
android:icon="@drawable/ic_heart"
android:title="@string/title_favorite" />
</menu>

View file

@ -154,4 +154,16 @@
android:name="playbackPosition"
app:argType="long" />
</activity>
<fragment
android:id="@+id/favoriteFragment"
android:name="dev.jdtech.jellyfin.fragments.FavoriteFragment"
android:label="@string/title_favorite"
tools:layout="@layout/fragment_favorite">
<action
android:id="@+id/action_favoriteFragment_to_episodeBottomSheetFragment"
app:destination="@id/episodeBottomSheetFragment" />
<action
android:id="@+id/action_favoriteFragment_to_mediaInfoFragment"
app:destination="@id/mediaInfoFragment" />
</fragment>
</navigation>

View file

@ -17,6 +17,7 @@
<string name="title_activity_main">MainActivity</string>
<string name="title_home">Home</string>
<string name="title_media">My media</string>
<string name="title_favorite">Favorites</string>
<string name="title_settings">Settings</string>
<string name="view_all">View all</string>
<string name="error_loading_data">Error loading data</string>
@ -38,6 +39,7 @@
<string name="continue_watching">Continue Watching</string>
<string name="latest_library">Latest %1$s</string>
<string name="series_poster">Series poster</string>
<string name="no_favorites">You have no favorites</string>
<string name="settings_category_language">Language</string>
<string name="settings_preferred_audio_language">Preferred audio language</string>