Implement collections (#252)
* Implement collections * Set collection name in top app bar
This commit is contained in:
parent
352a418d20
commit
2356bf5d6b
6 changed files with 256 additions and 5 deletions
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue