diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/CollectionFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/CollectionFragment.kt
new file mode 100644
index 00000000..0da20fcc
--- /dev/null
+++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/CollectionFragment.kt
@@ -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
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt
index 262e59ff..dac20f81 100644
--- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt
+++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt
@@ -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
+ )
+ )
+ }
}
diff --git a/app/phone/src/main/res/navigation/app_navigation.xml b/app/phone/src/main/res/navigation/app_navigation.xml
index c9d6c6c2..4b259ce8 100644
--- a/app/phone/src/main/res/navigation/app_navigation.xml
+++ b/app/phone/src/main/res/navigation/app_navigation.xml
@@ -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" />
+
+
+
+
+
+
+
(UiState.Loading)
+ val uiState = _uiState.asStateFlow()
+
+ sealed class UiState {
+ data class Normal(val collectionSections: List) : 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()
+
+ 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))
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt b/core/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt
index 3cb3938a..a6c927e9 100644
--- a/core/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt
+++ b/core/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt
@@ -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