Display downloaded episodes by series (#80)

* Display downloaded episodes by series

* Add offline playback to readme

* Remove accidentally commited changes

* Remove duplicate movie section in downloadviewmodel

* Fix issues with merging upstream

* Notify on download completion

* Fix trash icon color

* Update DownloadSeriesFragment to use new UiState system

* Clean up unused code

Co-authored-by: Jarne Demeulemeester <jarnedemeulemeester@gmail.com>
This commit is contained in:
Jcuhfehl 2022-06-11 13:35:52 +02:00 committed by GitHub
parent 795917d9d1
commit c1740c1b68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 438 additions and 67 deletions

View file

@ -21,6 +21,7 @@ Home | Library | Movie | Season | Episode
- Completely native interface
- Supported media items: movies, series, seasons, episodes
- Direct play only, (no transcoding)
- Offline playback / downloads
- ExoPlayer
- Video codecs: H.263, H.264, H.265, VP8, VP9, AV1
- Support depends on Android device

View file

@ -7,63 +7,122 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import dev.jdtech.jellyfin.databinding.HomeEpisodeItemBinding
import dev.jdtech.jellyfin.models.ContentType
import dev.jdtech.jellyfin.databinding.EpisodeItemBinding
import dev.jdtech.jellyfin.databinding.SeasonHeaderBinding
import dev.jdtech.jellyfin.models.DownloadSeriesMetadata
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
import timber.log.Timber
import org.jellyfin.sdk.model.api.BaseItemDto
import java.util.UUID
class DownloadEpisodeListAdapter(private val onClickListener: OnClickListener) : ListAdapter<PlayerItem, DownloadEpisodeListAdapter.EpisodeViewHolder>(DiffCallback) {
class EpisodeViewHolder(private var binding: HomeEpisodeItemBinding) :
private const val ITEM_VIEW_TYPE_HEADER = 0
private const val ITEM_VIEW_TYPE_EPISODE = 1
class DownloadEpisodeListAdapter(
private val onClickListener: OnClickListener,
private val downloadSeriesMetadata: DownloadSeriesMetadata
) :
ListAdapter<DownloadEpisodeItem, RecyclerView.ViewHolder>(DiffCallback) {
class HeaderViewHolder(private var binding: SeasonHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(episode: PlayerItem) {
val metadata = episode.item!!
binding.episode = downloadMetadataToBaseItemDto(episode.item)
if (metadata.playedPercentage != null) {
binding.progressBar.layoutParams.width = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
(metadata.playedPercentage.times(2.24)).toFloat(), binding.progressBar.context.resources.displayMetrics).toInt()
fun bind(
metadata: DownloadSeriesMetadata
) {
binding.seasonName.text = metadata.name
binding.seriesId = metadata.itemId
binding.seasonId = metadata.itemId
binding.executePendingBindings()
}
}
class EpisodeViewHolder(private var binding: EpisodeItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(episode: BaseItemDto) {
binding.episode = episode
if (episode.userData?.playedPercentage != null) {
binding.progressBar.layoutParams.width = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
(episode.userData?.playedPercentage?.times(.84))!!.toFloat(),
binding.progressBar.context.resources.displayMetrics
).toInt()
binding.progressBar.visibility = View.VISIBLE
}
if (metadata.type == ContentType.MOVIE) {
binding.primaryName.text = metadata.name
Timber.d(metadata.name)
binding.secondaryName.visibility = View.GONE
} else if (metadata.type == ContentType.EPISODE) {
binding.primaryName.text = metadata.seriesName
} else {
binding.progressBar.visibility = View.GONE
}
binding.executePendingBindings()
}
}
companion object DiffCallback : DiffUtil.ItemCallback<PlayerItem>() {
override fun areItemsTheSame(oldItem: PlayerItem, newItem: PlayerItem): Boolean {
return oldItem.itemId == newItem.itemId
companion object DiffCallback : DiffUtil.ItemCallback<DownloadEpisodeItem>() {
override fun areItemsTheSame(oldItem: DownloadEpisodeItem, newItem: DownloadEpisodeItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: PlayerItem, newItem: PlayerItem): Boolean {
override fun areContentsTheSame(oldItem: DownloadEpisodeItem, newItem: DownloadEpisodeItem): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder {
return EpisodeViewHolder(
HomeEpisodeItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_VIEW_TYPE_HEADER -> {
HeaderViewHolder(
SeasonHeaderBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
ITEM_VIEW_TYPE_EPISODE -> {
EpisodeViewHolder(
EpisodeItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
else -> throw ClassCastException("Unknown viewType $viewType")
}
}
override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) {
val item = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(item)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
ITEM_VIEW_TYPE_HEADER -> {
(holder as HeaderViewHolder).bind(downloadSeriesMetadata)
}
ITEM_VIEW_TYPE_EPISODE -> {
val item = getItem(position) as DownloadEpisodeItem.Episode
holder.itemView.setOnClickListener {
onClickListener.onClick(item.episode)
}
(holder as EpisodeViewHolder).bind(downloadMetadataToBaseItemDto(item.episode.item!!))
}
}
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is DownloadEpisodeItem.Header -> ITEM_VIEW_TYPE_HEADER
is DownloadEpisodeItem.Episode -> ITEM_VIEW_TYPE_EPISODE
}
holder.bind(item)
}
class OnClickListener(val clickListener: (item: PlayerItem) -> Unit) {
fun onClick(item: PlayerItem) = clickListener(item)
}
}
sealed class DownloadEpisodeItem {
abstract val id: UUID
object Header : DownloadEpisodeItem() {
override val id: UUID = UUID.randomUUID()
}
data class Episode(val episode: PlayerItem) : DownloadEpisodeItem() {
override val id = episode.itemId
}
}

View file

@ -0,0 +1,65 @@
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.R
import dev.jdtech.jellyfin.databinding.BaseItemBinding
import dev.jdtech.jellyfin.models.DownloadSeriesMetadata
import dev.jdtech.jellyfin.utils.downloadSeriesMetadataToBaseItemDto
class DownloadSeriesListAdapter(
private val onClickListener: OnClickListener,
private val fixedWidth: Boolean = false,
) :
ListAdapter<DownloadSeriesMetadata, DownloadSeriesListAdapter.ItemViewHolder>(DiffCallback) {
class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: DownloadSeriesMetadata, fixedWidth: Boolean) {
binding.item = downloadSeriesMetadataToBaseItemDto(item)
binding.itemName.text = item.name
binding.itemCount.text = item.episodes.size.toString()
if (fixedWidth) {
binding.itemLayout.layoutParams.width =
parent.resources.getDimension(R.dimen.overview_media_width).toInt()
(binding.itemLayout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = 0
}
binding.executePendingBindings()
}
}
companion object DiffCallback : DiffUtil.ItemCallback<DownloadSeriesMetadata>() {
override fun areItemsTheSame(oldItem: DownloadSeriesMetadata, newItem: DownloadSeriesMetadata): Boolean {
return oldItem.itemId == newItem.itemId
}
override fun areContentsTheSame(oldItem: DownloadSeriesMetadata, newItem: DownloadSeriesMetadata): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
return ItemViewHolder(
BaseItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
), parent
)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(item)
}
holder.bind(item, fixedWidth)
}
class OnClickListener(val clickListener: (item: DownloadSeriesMetadata) -> Unit) {
fun onClick(item: DownloadSeriesMetadata) = clickListener(item)
}
}

View file

@ -8,7 +8,6 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.databinding.BaseItemBinding
import dev.jdtech.jellyfin.models.ContentType
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
@ -23,11 +22,10 @@ class DownloadViewItemListAdapter(
fun bind(item: PlayerItem, fixedWidth: Boolean) {
val metadata = item.item!!
binding.item = downloadMetadataToBaseItemDto(metadata)
binding.itemName.text = if (metadata.type == ContentType.EPISODE) metadata.seriesName else item.name
binding.itemName.text = item.name
binding.itemCount.visibility = View.GONE
if (fixedWidth) {
binding.itemLayout.layoutParams.width =
parent.resources.getDimension(R.dimen.overview_media_width).toInt()
binding.itemLayout.layoutParams.width = parent.resources.getDimension(R.dimen.overview_media_width).toInt()
(binding.itemLayout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = 0
}
binding.executePendingBindings()

View file

@ -10,24 +10,25 @@ import dev.jdtech.jellyfin.models.DownloadSection
class DownloadsListAdapter(
private val onClickListener: DownloadViewItemListAdapter.OnClickListener,
private val onEpisodeClickListener: DownloadEpisodeListAdapter.OnClickListener
private val onSeriesClickListener: DownloadSeriesListAdapter.OnClickListener
) : ListAdapter<DownloadSection, DownloadsListAdapter.SectionViewHolder>(DiffCallback) {
class SectionViewHolder(private var binding: DownloadSectionBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
section: DownloadSection,
onClickListener: DownloadViewItemListAdapter.OnClickListener,
onEpisodeClickListener: DownloadEpisodeListAdapter.OnClickListener
onSeriesClickListener: DownloadSeriesListAdapter.OnClickListener
) {
binding.section = section
if (section.name == "Movies" || section.name == "Shows") {
binding.itemsRecyclerView.adapter =
DownloadViewItemListAdapter(onClickListener, fixedWidth = true)
(binding.itemsRecyclerView.adapter as DownloadViewItemListAdapter).submitList(section.items)
} else if (section.name == "Episodes") {
binding.itemsRecyclerView.adapter =
DownloadEpisodeListAdapter(onEpisodeClickListener)
(binding.itemsRecyclerView.adapter as DownloadEpisodeListAdapter).submitList(section.items)
when (section.name) {
"Movies" -> {
binding.itemsRecyclerView.adapter = DownloadViewItemListAdapter(onClickListener, fixedWidth = true)
(binding.itemsRecyclerView.adapter as DownloadViewItemListAdapter).submitList(section.items)
}
"Shows" -> {
binding.itemsRecyclerView.adapter = DownloadSeriesListAdapter(onSeriesClickListener, fixedWidth = true)
(binding.itemsRecyclerView.adapter as DownloadSeriesListAdapter).submitList(section.series)
}
}
binding.executePendingBindings()
}
@ -58,6 +59,6 @@ class DownloadsListAdapter(
override fun onBindViewHolder(holder: SectionViewHolder, position: Int) {
val collection = getItem(position)
holder.bind(collection, onClickListener, onEpisodeClickListener)
holder.bind(collection, onClickListener, onSeriesClickListener)
}
}

View file

@ -2,7 +2,6 @@ package dev.jdtech.jellyfin.database
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import dev.jdtech.jellyfin.models.DownloadItem
import java.util.*

View file

@ -15,6 +15,7 @@ import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.adapters.*
import dev.jdtech.jellyfin.databinding.FragmentDownloadBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.models.DownloadSeriesMetadata
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
import dev.jdtech.jellyfin.viewmodels.DownloadViewModel
@ -39,9 +40,10 @@ class DownloadFragment : Fragment() {
binding.downloadsRecyclerView.adapter = DownloadsListAdapter(
DownloadViewItemListAdapter.OnClickListener { item ->
navigateToMediaInfoFragment(item)
}, DownloadEpisodeListAdapter.OnClickListener { item ->
navigateToEpisodeBottomSheetFragment(item)
})
}, DownloadSeriesListAdapter.OnClickListener { item ->
navigateToDownloadSeriesFragment(item)
}
)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
@ -104,12 +106,11 @@ class DownloadFragment : Fragment() {
)
}
private fun navigateToEpisodeBottomSheetFragment(episode: PlayerItem) {
private fun navigateToDownloadSeriesFragment(series: DownloadSeriesMetadata) {
findNavController().navigate(
DownloadFragmentDirections.actionDownloadFragmentToEpisodeBottomSheetFragment(
UUID.randomUUID(),
episode,
isOffline = true
DownloadFragmentDirections.actionDownloadFragmentToDownloadSeriesFragment(
seriesMetadata = series,
seriesName = series.name
)
)
}

View file

@ -0,0 +1,99 @@
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.core.view.isVisible
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.DownloadEpisodeListAdapter
import dev.jdtech.jellyfin.databinding.FragmentDownloadSeriesBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.viewmodels.DownloadSeriesViewModel
import kotlinx.coroutines.launch
import java.util.*
@AndroidEntryPoint
class DownloadSeriesFragment : Fragment() {
private lateinit var binding: FragmentDownloadSeriesBinding
private val viewModel: DownloadSeriesViewModel by viewModels()
private lateinit var errorDialog: ErrorDialogFragment
private val args: DownloadSeriesFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentDownloadSeriesBinding.inflate(inflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.viewModel = viewModel
binding.episodesRecyclerView.adapter =
DownloadEpisodeListAdapter(DownloadEpisodeListAdapter.OnClickListener { episode ->
navigateToEpisodeBottomSheetFragment(episode)
}, args.seriesMetadata)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
when (uiState) {
is DownloadSeriesViewModel.UiState.Normal -> bindUiStateNormal(uiState)
is DownloadSeriesViewModel.UiState.Loading -> bindUiStateLoading(uiState)
is DownloadSeriesViewModel.UiState.Error -> bindUiStateError(uiState)
}
}
}
}
binding.errorLayout.errorRetryButton.setOnClickListener {
viewModel.loadEpisodes(args.seriesMetadata)
}
binding.errorLayout.errorDetailsButton.setOnClickListener {
errorDialog.show(parentFragmentManager, "errordialog")
}
viewModel.loadEpisodes(args.seriesMetadata)
}
private fun bindUiStateNormal(uiState: DownloadSeriesViewModel.UiState.Normal) {
val adapter = binding.episodesRecyclerView.adapter as DownloadEpisodeListAdapter
adapter.submitList(uiState.downloadEpisodes)
binding.episodesRecyclerView.isVisible = true
binding.errorLayout.errorPanel.isVisible = false
}
private fun bindUiStateLoading(uiState: DownloadSeriesViewModel.UiState.Loading) {}
private fun bindUiStateError(uiState: DownloadSeriesViewModel.UiState.Error) {
errorDialog = ErrorDialogFragment(uiState.error)
binding.episodesRecyclerView.isVisible = false
binding.errorLayout.errorPanel.isVisible = true
}
private fun navigateToEpisodeBottomSheetFragment(episode: PlayerItem) {
findNavController().navigate(
DownloadSeriesFragmentDirections.actionDownloadSeriesFragmentToEpisodeBottomSheetFragment(
UUID.randomUUID(),
episode,
isOffline = true
)
)
}
}

View file

@ -5,5 +5,6 @@ import java.util.*
data class DownloadSection(
val id: UUID,
val name: String,
var items: List<PlayerItem>
val items: List<PlayerItem>? = null,
val series: List<DownloadSeriesMetadata>? = null
)

View file

@ -0,0 +1,12 @@
package dev.jdtech.jellyfin.models
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.util.*
@Parcelize
data class DownloadSeriesMetadata(
val itemId: UUID,
val name: String? = null,
val episodes: List<PlayerItem>
) : Parcelable

View file

@ -9,6 +9,7 @@ import androidx.preference.PreferenceManager
import dev.jdtech.jellyfin.database.DownloadDatabaseDao
import dev.jdtech.jellyfin.models.DownloadItem
import dev.jdtech.jellyfin.models.DownloadRequestItem
import dev.jdtech.jellyfin.models.DownloadSeriesMetadata
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.repository.JellyfinRepository
import org.jellyfin.sdk.model.api.BaseItemDto
@ -175,6 +176,24 @@ fun baseItemDtoToDownloadMetadata(item: BaseItemDto): DownloadItem {
)
}
fun downloadSeriesMetadataToBaseItemDto(metadata: DownloadSeriesMetadata): BaseItemDto {
val userData = UserItemDataDto(
playbackPositionTicks = 0,
playedPercentage = 0.0,
isFavorite = false,
playCount = 0,
played = false,
unplayedItemCount = metadata.episodes.size
)
return BaseItemDto(
id = metadata.itemId,
type = "Series",
name = metadata.name,
userData = userData
)
}
suspend fun syncPlaybackProgress(
downloadDatabase: DownloadDatabaseDao,
jellyfinRepository: JellyfinRepository

View file

@ -0,0 +1,46 @@
package dev.jdtech.jellyfin.viewmodels
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.adapters.DownloadEpisodeItem
import dev.jdtech.jellyfin.models.DownloadSeriesMetadata
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class DownloadSeriesViewModel
@Inject
constructor() : ViewModel() {
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
sealed class UiState {
data class Normal(val downloadEpisodes: List<DownloadEpisodeItem>) : UiState()
object Loading : UiState()
data class Error(val error: Exception) : UiState()
}
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
scope.launch { uiState.collect { collector(it) } }
}
fun loadEpisodes(seriesMetadata: DownloadSeriesMetadata) {
viewModelScope.launch {
uiState.emit(UiState.Loading)
try {
uiState.emit(UiState.Normal(getEpisodes((seriesMetadata))))
} catch (e: Exception) {
uiState.emit(UiState.Error(e))
}
}
}
private fun getEpisodes(seriesMetadata: DownloadSeriesMetadata): List<DownloadEpisodeItem> {
val episodes = seriesMetadata.episodes
return listOf(DownloadEpisodeItem.Header) + episodes.sortedWith(compareBy(
{ it.item!!.parentIndexNumber },
{ it.item!!.indexNumber })).map { DownloadEpisodeItem.Episode(it) }
}
}

View file

@ -5,6 +5,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.database.DownloadDatabaseDao
import dev.jdtech.jellyfin.models.ContentType
import dev.jdtech.jellyfin.models.DownloadSection
import dev.jdtech.jellyfin.models.DownloadSeriesMetadata
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.utils.loadDownloadedEpisodes
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
@ -39,21 +41,31 @@ constructor(
uiState.emit(UiState.Loading)
try {
val items = loadDownloadedEpisodes(downloadDatabase)
val showsMap = mutableMapOf<UUID, MutableList<PlayerItem>>()
items.filter { it.item?.type == ContentType.EPISODE }.forEach {
showsMap.computeIfAbsent(it.item!!.seriesId!!) { mutableListOf() } += it
}
val shows = showsMap.map { DownloadSeriesMetadata(it.key, it.value[0].item!!.seriesName, it.value) }
val downloadSections = mutableListOf<DownloadSection>()
withContext(Dispatchers.Default) {
DownloadSection(
UUID.randomUUID(),
"Episodes",
items.filter { it.item?.type == ContentType.EPISODE }).let {
if (it.items.isNotEmpty()) downloadSections.add(
"Movies",
items.filter { it.item?.type == ContentType.MOVIE }
).let {
if (it.items!!.isNotEmpty()) downloadSections.add(
it
)
}
DownloadSection(
UUID.randomUUID(),
"Movies",
items.filter { it.item?.type == ContentType.MOVIE }).let {
if (it.items.isNotEmpty()) downloadSections.add(
"Shows",
null,
shows
).let {
if (it.series!!.isNotEmpty()) downloadSections.add(
it
)
}

View file

@ -221,7 +221,9 @@
android:contentDescription="@string/delete_button_description"
android:padding="12dp"
android:src="@drawable/ic_trash"
android:visibility="gone" />
android:visibility="gone"
app:tint="?attr/colorOnSecondaryContainer"
tools:visibility="visible" />
</LinearLayout>
<LinearLayout

View file

@ -0,0 +1,36 @@
<?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"
tools:context=".fragments.SeasonFragment">
<data>
<variable
name="viewModel"
type="dev.jdtech.jellyfin.viewmodels.DownloadSeriesViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/error_layout"
layout="@layout/error_panel" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/episodes_recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
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"
tools:itemCount="4"
tools:listitem="@layout/episode_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -163,6 +163,23 @@
android:id="@+id/action_seasonFragment_to_episodeBottomSheetFragment"
app:destination="@id/episodeBottomSheetFragment" />
</fragment>
<fragment
android:id="@+id/downloadSeriesFragment"
android:name="dev.jdtech.jellyfin.fragments.DownloadSeriesFragment"
android:label="{seriesName}"
tools:layout="@layout/fragment_season">
<argument
android:name="seriesMetadata"
app:argType="dev.jdtech.jellyfin.models.DownloadSeriesMetadata"/>
<argument
android:name="seriesName"
android:defaultValue="Series"
app:argType="string"
app:nullable="true" />
<action
android:id="@+id/action_downloadSeriesFragment_to_episodeBottomSheetFragment"
app:destination="@id/episodeBottomSheetFragment" />
</fragment>
<dialog
android:id="@+id/episodeBottomSheetFragment"
android:name="dev.jdtech.jellyfin.fragments.EpisodeBottomSheetFragment"
@ -207,6 +224,9 @@
<action
android:id="@+id/action_downloadFragment_to_mediaInfoFragment"
app:destination="@id/mediaInfoFragment" />
<action
android:id="@+id/action_downloadFragment_to_downloadSeriesFragment"
app:destination="@id/downloadSeriesFragment" />
</fragment>
<fragment
android:id="@+id/searchResultFragment"