Implement SeasonFragment

This commit is contained in:
Jarne Demeulemeester 2021-06-25 13:49:55 +02:00
parent a5d6fcd621
commit 151ee6cae7
No known key found for this signature in database
GPG key ID: 60884A0C1EBA43E5
9 changed files with 316 additions and 14 deletions

View file

@ -86,3 +86,23 @@ fun bindPersonImage(imageView: ImageView, person: BaseItemPerson) {
imageView.contentDescription = "${person.name} poster" imageView.contentDescription = "${person.name} poster"
} }
@BindingAdapter("episodes")
fun bindEpisodes(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
val adapter = recyclerView.adapter as EpisodeListAdapter
adapter.submitList(data)
}
@BindingAdapter("episodeImage")
fun bindEpisodeImage(imageView: ImageView, episode: BaseItemDto) {
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
Glide
.with(imageView.context)
.load(jellyfinApi.api.baseUrl.plus("/items/${episode.id}/Images/Primary"))
.transition(DrawableTransitionOptions.withCrossFade())
.placeholder(R.color.neutral_800)
.into(imageView)
imageView.contentDescription = "${episode.name} poster"
}

View file

@ -0,0 +1,46 @@
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.databinding.EpisodeItemBinding
import org.jellyfin.sdk.model.api.BaseItemDto
class EpisodeListAdapter :
ListAdapter<BaseItemDto, EpisodeListAdapter.EpisodeViewHolder>(DiffCallback) {
class EpisodeViewHolder(private var binding: EpisodeItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(episode: BaseItemDto) {
binding.episode = episode
binding.executePendingBindings()
}
}
companion object DiffCallback : DiffUtil.ItemCallback<BaseItemDto>() {
override fun areItemsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder {
return EpisodeViewHolder(
EpisodeItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item)
}
}

View file

@ -1,16 +1,17 @@
package dev.jdtech.jellyfin.fragments package dev.jdtech.jellyfin.fragments
import androidx.lifecycle.ViewModelProvider
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import dev.jdtech.jellyfin.adapters.PersonListAdapter import dev.jdtech.jellyfin.adapters.PersonListAdapter
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
import dev.jdtech.jellyfin.databinding.FragmentMediaInfoBinding import dev.jdtech.jellyfin.databinding.FragmentMediaInfoBinding
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModelFactory import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModelFactory
class MediaInfoFragment : Fragment() { class MediaInfoFragment : Fragment() {
@ -34,7 +35,8 @@ class MediaInfoFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val viewModelFactory = MediaInfoViewModelFactory(requireNotNull(this.activity).application, args.itemId) val viewModelFactory =
MediaInfoViewModelFactory(requireNotNull(this.activity).application, args.itemId)
viewModel = ViewModelProvider(this, viewModelFactory).get(MediaInfoViewModel::class.java) viewModel = ViewModelProvider(this, viewModelFactory).get(MediaInfoViewModel::class.java)
binding.viewModel = viewModel binding.viewModel = viewModel
@ -46,7 +48,16 @@ class MediaInfoFragment : Fragment() {
} }
}) })
binding.seasonsRecyclerView.adapter = ViewItemListAdapter(ViewItemListAdapter.OnClickListener {}, fixedWidth = true) binding.seasonsRecyclerView.adapter =
ViewItemListAdapter(ViewItemListAdapter.OnClickListener {
findNavController().navigate(
MediaInfoFragmentDirections.actionMediaInfoFragmentToSeasonFragment(
it.seriesId!!,
it.id,
it.name
)
)
}, fixedWidth = true)
binding.peopleRecyclerView.adapter = PersonListAdapter() binding.peopleRecyclerView.adapter = PersonListAdapter()
} }

View file

@ -0,0 +1,43 @@
package dev.jdtech.jellyfin.fragments
import androidx.lifecycle.ViewModelProvider
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.navArgs
import dev.jdtech.jellyfin.adapters.EpisodeListAdapter
import dev.jdtech.jellyfin.databinding.FragmentSeasonBinding
import dev.jdtech.jellyfin.viewmodels.SeasonViewModel
import dev.jdtech.jellyfin.viewmodels.SeasonViewModelFactory
class SeasonFragment : Fragment() {
private lateinit var viewModel: SeasonViewModel
private lateinit var binding: FragmentSeasonBinding
private val args: SeasonFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentSeasonBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val viewModelFactory = SeasonViewModelFactory(
requireNotNull(this.activity).application,
args.seriesId,
args.seasonId
)
viewModel = ViewModelProvider(this, viewModelFactory).get(SeasonViewModel::class.java)
binding.viewModel = viewModel
binding.episodesRecyclerView.adapter = EpisodeListAdapter()
}
}

View file

@ -0,0 +1,40 @@
package dev.jdtech.jellyfin.viewmodels
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import dev.jdtech.jellyfin.api.JellyfinApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.ItemFields
import java.util.*
class SeasonViewModel(application: Application, seriesId: UUID, seasonId: UUID) :
AndroidViewModel(application) {
private val jellyfinApi = JellyfinApi.getInstance(application, "")
private val _episodes = MutableLiveData<List<BaseItemDto>>()
val episodes: LiveData<List<BaseItemDto>> = _episodes
init {
viewModelScope.launch {
_episodes.value = getEpisodes(seriesId, seasonId)
}
}
private suspend fun getEpisodes(seriesId: UUID, seasonId: UUID): List<BaseItemDto>? {
val episodes: List<BaseItemDto>?
withContext(Dispatchers.IO) {
episodes = jellyfinApi.showsApi.getEpisodes(
seriesId, jellyfinApi.userId!!, seasonId = seasonId, fields = listOf(
ItemFields.OVERVIEW
)
).content.items
}
return episodes
}
}

View file

@ -0,0 +1,21 @@
package dev.jdtech.jellyfin.viewmodels
import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import java.lang.IllegalArgumentException
import java.util.*
class SeasonViewModelFactory(
private val application: Application,
private val seriesId: UUID,
private val seasonId: UUID
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SeasonViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return SeasonViewModel(application, seriesId, seasonId) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View file

@ -4,7 +4,8 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<data> <data>
<import type="android.view.View"/>
<import type="android.view.View" />
<variable <variable
name="episode" name="episode"
@ -13,13 +14,15 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="100dp"> android:layout_height="100dp"
android:layout_marginBottom="24dp">
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/episode_image" android:id="@+id/episode_image"
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="100dp" android:layout_height="100dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:episodeImage="@{episode}"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@ -31,19 +34,20 @@
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:padding="4dp"
android:visibility="@{episode.userData.played == true ? View.VISIBLE : View.GONE}"
android:background="@drawable/circle_background" android:background="@drawable/circle_background"
android:contentDescription="@string/episode_watched_indicator"
android:padding="4dp"
android:src="@drawable/ic_check" android:src="@drawable/ic_check"
android:visibility="@{episode.userData.played == true ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="@id/episode_image" app:layout_constraintEnd_toEndOf="@id/episode_image"
app:layout_constraintTop_toTopOf="@id/episode_image" app:layout_constraintTop_toTopOf="@id/episode_image" />
android:contentDescription="@string/episode_watched_indicator" />
<TextView <TextView
android:id="@+id/episode_title" android:id="@+id/episode_title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
android:text="@{episode.name}"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textAppearance="@style/TextAppearance.AppCompat.Body1"
app:layout_constraintBottom_toTopOf="@id/episode_desc" app:layout_constraintBottom_toTopOf="@id/episode_desc"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -55,6 +59,7 @@
android:id="@+id/episode_desc" android:id="@+id/episode_desc"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:text="@{episode.overview}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View file

@ -0,0 +1,96 @@
<?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.SeasonViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/play_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:background="@drawable/button_setup_background"
android:contentDescription="@string/play_button_description"
android:paddingHorizontal="24dp"
android:paddingVertical="12dp"
android:src="@drawable/ic_play" />
<ImageButton
android:id="@+id/trailer_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:background="@drawable/button_accent_background"
android:contentDescription="@string/trailer_button_description"
android:padding="12dp"
android:src="@drawable/ic_film" />
<ImageButton
android:id="@+id/shuffle_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:background="@drawable/button_accent_background"
android:contentDescription="@string/shuffle_button_description"
android:padding="12dp"
android:src="@drawable/ic_shuffle" />
<ImageButton
android:id="@+id/check_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:background="@drawable/button_accent_background"
android:contentDescription="@string/check_button_description"
android:padding="12dp"
android:src="@drawable/ic_check" />
<ImageButton
android:id="@+id/favorite_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button_accent_background"
android:contentDescription="@string/favorite_button_description"
android:padding="12dp"
android:src="@drawable/ic_heart" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/episodes_recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingHorizontal="24dp"
android:paddingTop="16dp"
app:episodes="@{viewModel.episodes}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/buttons"
tools:itemCount="4"
tools:listitem="@layout/episode_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -9,7 +9,7 @@
android:id="@+id/navigation_home" android:id="@+id/navigation_home"
android:name="dev.jdtech.jellyfin.fragments.HomeFragment" android:name="dev.jdtech.jellyfin.fragments.HomeFragment"
android:label="@string/title_home" android:label="@string/title_home"
tools:layout="@layout/fragment_home" > tools:layout="@layout/fragment_home">
<action <action
android:id="@+id/action_navigation_home_to_libraryFragment" android:id="@+id/action_navigation_home_to_libraryFragment"
app:destination="@id/libraryFragment" app:destination="@id/libraryFragment"
@ -76,8 +76,28 @@
app:argType="java.util.UUID" /> app:argType="java.util.UUID" />
<argument <argument
android:name="itemName" android:name="itemName"
android:defaultValue="Media Info"
app:argType="string" app:argType="string"
app:nullable="true" app:nullable="true" />
android:defaultValue="Media Info" /> <action
android:id="@+id/action_mediaInfoFragment_to_seasonFragment"
app:destination="@id/seasonFragment" />
</fragment>
<fragment
android:id="@+id/seasonFragment"
android:name="dev.jdtech.jellyfin.fragments.SeasonFragment"
android:label="{seasonName}"
tools:layout="@layout/fragment_season">
<argument
android:name="seriesId"
app:argType="java.util.UUID" />
<argument
android:name="seasonId"
app:argType="java.util.UUID" />
<argument
android:name="seasonName"
android:defaultValue="Season"
app:argType="string"
app:nullable="true" />
</fragment> </fragment>
</navigation> </navigation>