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.databinding.FragmentLibraryBinding
|
||||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||||
import dev.jdtech.jellyfin.dialogs.SortDialogFragment
|
import dev.jdtech.jellyfin.dialogs.SortDialogFragment
|
||||||
|
import dev.jdtech.jellyfin.models.CollectionType
|
||||||
import dev.jdtech.jellyfin.models.SortBy
|
import dev.jdtech.jellyfin.models.SortBy
|
||||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||||
import dev.jdtech.jellyfin.viewmodels.LibraryViewModel
|
import dev.jdtech.jellyfin.viewmodels.LibraryViewModel
|
||||||
|
@ -112,7 +113,11 @@ class LibraryFragment : Fragment() {
|
||||||
binding.itemsRecyclerView.adapter =
|
binding.itemsRecyclerView.adapter =
|
||||||
ViewItemPagingAdapter(
|
ViewItemPagingAdapter(
|
||||||
ViewItemPagingAdapter.OnClickListener { item ->
|
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:exitAnim="@anim/nav_default_exit_anim"
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||||
app:popExitAnim="@anim/nav_default_pop_exit_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
|
<argument
|
||||||
android:name="libraryType"
|
android:name="libraryType"
|
||||||
android:defaultValue="unknown"
|
android:defaultValue="unknown"
|
||||||
|
@ -226,6 +233,26 @@
|
||||||
android:id="@+id/action_favoriteFragment_to_mediaInfoFragment"
|
android:id="@+id/action_favoriteFragment_to_mediaInfoFragment"
|
||||||
app:destination="@id/mediaInfoFragment" />
|
app:destination="@id/mediaInfoFragment" />
|
||||||
</fragment>
|
</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
|
<fragment
|
||||||
android:id="@+id/downloadFragment"
|
android:id="@+id/downloadFragment"
|
||||||
android:name="dev.jdtech.jellyfin.fragments.DownloadFragment"
|
android:name="dev.jdtech.jellyfin.fragments.DownloadFragment"
|
||||||
|
|
|
@ -10,7 +10,7 @@ enum class CollectionType(val type: String) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val unsupportedCollections = listOf(
|
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")
|
Timber.d("$libraryType")
|
||||||
val itemType = when (libraryType) {
|
val itemType = when (libraryType) {
|
||||||
"movies" -> BaseItemKind.MOVIE
|
"movies" -> listOf(BaseItemKind.MOVIE)
|
||||||
"tvshows" -> BaseItemKind.SERIES
|
"tvshows" -> listOf(BaseItemKind.SERIES)
|
||||||
|
"boxsets" -> listOf(BaseItemKind.BOX_SET)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
@ -49,7 +50,7 @@ constructor(
|
||||||
try {
|
try {
|
||||||
val items = jellyfinRepository.getItemsPaging(
|
val items = jellyfinRepository.getItemsPaging(
|
||||||
parentId = parentId,
|
parentId = parentId,
|
||||||
includeTypes = if (itemType != null) listOf(itemType) else null,
|
includeTypes = itemType,
|
||||||
recursive = true,
|
recursive = true,
|
||||||
sortBy = sortBy,
|
sortBy = sortBy,
|
||||||
sortOrder = sortOrder
|
sortOrder = sortOrder
|
||||||
|
|
Loading…
Reference in a new issue