Implement collections (#252)

* Implement collections

* Set collection name in top app bar
This commit is contained in:
Jarne Demeulemeester 2023-01-28 21:07:45 +01:00 committed by GitHub
parent 352a418d20
commit 2356bf5d6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 256 additions and 5 deletions

View file

@ -0,0 +1,123 @@
package dev.jdtech.jellyfin.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import dagger.hilt.android.AndroidEntryPoint
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.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
import dev.jdtech.jellyfin.viewmodels.CollectionViewModel
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto
import timber.log.Timber
@AndroidEntryPoint
class CollectionFragment : Fragment() {
private lateinit var binding: FragmentFavoriteBinding
private val viewModel: CollectionViewModel by viewModels()
private val args: CollectionFragmentArgs by navArgs()
private lateinit var errorDialog: ErrorDialogFragment
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentFavoriteBinding.inflate(inflater, container, false)
binding.favoritesRecyclerView.adapter = FavoritesListAdapter(
ViewItemListAdapter.OnClickListener { item ->
navigateToMediaInfoFragment(item)
},
HomeEpisodeListAdapter.OnClickListener { item ->
navigateToEpisodeBottomSheetFragment(item)
}
)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
Timber.d("$uiState")
when (uiState) {
is CollectionViewModel.UiState.Normal -> bindUiStateNormal(uiState)
is CollectionViewModel.UiState.Loading -> bindUiStateLoading()
is CollectionViewModel.UiState.Error -> bindUiStateError(uiState)
}
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.loadItems(args.collectionId)
}
}
binding.errorLayout.errorRetryButton.setOnClickListener {
viewModel.loadItems(args.collectionId)
}
binding.errorLayout.errorDetailsButton.setOnClickListener {
errorDialog.show(parentFragmentManager, ErrorDialogFragment.TAG)
}
return binding.root
}
private fun bindUiStateNormal(uiState: CollectionViewModel.UiState.Normal) {
uiState.apply {
binding.noFavoritesText.isVisible = collectionSections.isEmpty()
val adapter = binding.favoritesRecyclerView.adapter as FavoritesListAdapter
adapter.submitList(collectionSections)
}
binding.loadingIndicator.isVisible = false
binding.favoritesRecyclerView.isVisible = true
binding.errorLayout.errorPanel.isVisible = false
}
private fun bindUiStateLoading() {
binding.loadingIndicator.isVisible = true
binding.errorLayout.errorPanel.isVisible = false
}
private fun bindUiStateError(uiState: CollectionViewModel.UiState.Error) {
errorDialog = ErrorDialogFragment.newInstance(uiState.error)
binding.loadingIndicator.isVisible = false
binding.favoritesRecyclerView.isVisible = false
binding.errorLayout.errorPanel.isVisible = true
checkIfLoginRequired(uiState.error.message)
}
private fun navigateToMediaInfoFragment(item: BaseItemDto) {
findNavController().navigate(
CollectionFragmentDirections.actionCollectionFragmentToMediaInfoFragment(
item.id,
item.name,
item.type
)
)
}
private fun navigateToEpisodeBottomSheetFragment(episode: BaseItemDto) {
findNavController().navigate(
CollectionFragmentDirections.actionCollectionFragmentToEpisodeBottomSheetFragment(
episode.id
)
)
}
}

View file

@ -25,6 +25,7 @@ import dev.jdtech.jellyfin.adapters.ViewItemPagingAdapter
import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.dialogs.SortDialogFragment
import dev.jdtech.jellyfin.models.CollectionType
import dev.jdtech.jellyfin.models.SortBy
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
import dev.jdtech.jellyfin.viewmodels.LibraryViewModel
@ -112,7 +113,11 @@ class LibraryFragment : Fragment() {
binding.itemsRecyclerView.adapter =
ViewItemPagingAdapter(
ViewItemPagingAdapter.OnClickListener { item ->
navigateToMediaInfoFragment(item)
if (args.libraryType == CollectionType.BoxSets.type) {
navigateToCollectionFragment(item)
} else {
navigateToMediaInfoFragment(item)
}
}
)
@ -199,4 +204,13 @@ class LibraryFragment : Fragment() {
)
)
}
private fun navigateToCollectionFragment(collection: BaseItemDto) {
findNavController().navigate(
LibraryFragmentDirections.actionLibraryFragmentToCollectionFragment(
collection.id,
collection.name
)
)
}
}

View file

@ -102,6 +102,13 @@
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_libraryFragment_to_collectionFragment"
app:destination="@id/collectionFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<argument
android:name="libraryType"
android:defaultValue="unknown"
@ -226,6 +233,26 @@
android:id="@+id/action_favoriteFragment_to_mediaInfoFragment"
app:destination="@id/mediaInfoFragment" />
</fragment>
<fragment
android:id="@+id/collectionFragment"
android:name="dev.jdtech.jellyfin.fragments.CollectionFragment"
android:label="{collectionName}"
tools:layout="@layout/fragment_favorite">
<argument
android:name="collectionId"
app:argType="java.util.UUID" />
<argument
android:name="collectionName"
android:defaultValue="Collection"
app:argType="string"
app:nullable="true" />
<action
android:id="@+id/action_collectionFragment_to_episodeBottomSheetFragment"
app:destination="@id/episodeBottomSheetFragment" />
<action
android:id="@+id/action_collectionFragment_to_mediaInfoFragment"
app:destination="@id/mediaInfoFragment" />
</fragment>
<fragment
android:id="@+id/downloadFragment"
android:name="dev.jdtech.jellyfin.fragments.DownloadFragment"

View file

@ -10,7 +10,7 @@ enum class CollectionType(val type: String) {
companion object {
val unsupportedCollections = listOf(
HomeVideos, Music, Playlists, Books, LiveTv, BoxSets
HomeVideos, Music, Playlists, Books, LiveTv
)
}
}

View file

@ -0,0 +1,86 @@
package dev.jdtech.jellyfin.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.Constants
import dev.jdtech.jellyfin.core.R
import dev.jdtech.jellyfin.models.FavoriteSection
import dev.jdtech.jellyfin.models.UiText
import dev.jdtech.jellyfin.repository.JellyfinRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemKind
import java.util.UUID
import javax.inject.Inject
@HiltViewModel
class CollectionViewModel
@Inject
constructor(
private val jellyfinRepository: JellyfinRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState = _uiState.asStateFlow()
sealed class UiState {
data class Normal(val collectionSections: List<FavoriteSection>) : UiState()
object Loading : UiState()
data class Error(val error: Exception) : UiState()
}
fun loadItems(parentId: UUID) {
viewModelScope.launch {
_uiState.emit(UiState.Loading)
try {
val items = jellyfinRepository.getItems(parentId = parentId)
if (items.isEmpty()) {
_uiState.emit(UiState.Normal(emptyList()))
return@launch
}
val favoriteSections = mutableListOf<FavoriteSection>()
withContext(Dispatchers.Default) {
FavoriteSection(
Constants.FAVORITE_TYPE_MOVIES,
UiText.StringResource(R.string.movies_label),
items.filter { it.type == BaseItemKind.MOVIE }
).let {
if (it.items.isNotEmpty()) favoriteSections.add(
it
)
}
FavoriteSection(
Constants.FAVORITE_TYPE_SHOWS,
UiText.StringResource(R.string.shows_label),
items.filter { it.type == BaseItemKind.SERIES }
).let {
if (it.items.isNotEmpty()) favoriteSections.add(
it
)
}
FavoriteSection(
Constants.FAVORITE_TYPE_EPISODES,
UiText.StringResource(R.string.episodes_label),
items.filter { it.type == BaseItemKind.EPISODE }
).let {
if (it.items.isNotEmpty()) favoriteSections.add(
it
)
}
}
_uiState.emit(UiState.Normal(favoriteSections))
} catch (e: Exception) {
_uiState.emit(UiState.Error(e))
}
}
}
}

View file

@ -40,8 +40,9 @@ constructor(
) {
Timber.d("$libraryType")
val itemType = when (libraryType) {
"movies" -> BaseItemKind.MOVIE
"tvshows" -> BaseItemKind.SERIES
"movies" -> listOf(BaseItemKind.MOVIE)
"tvshows" -> listOf(BaseItemKind.SERIES)
"boxsets" -> listOf(BaseItemKind.BOX_SET)
else -> null
}
viewModelScope.launch {
@ -49,7 +50,7 @@ constructor(
try {
val items = jellyfinRepository.getItemsPaging(
parentId = parentId,
includeTypes = if (itemType != null) listOf(itemType) else null,
includeTypes = itemType,
recursive = true,
sortBy = sortBy,
sortOrder = sortOrder