Add person detail screen (#47)
* Add person detail screen Displays actor/actresses portrait and text info + list of movies/show this person starred in. Text info is max 5 lines with View More button if ellipsized. View More toggle is reset upon orientation change since in landscape mode ellipsize might not be necessary. * Remove useless StarredInAdapter.kt * Fix image view shape * Improve UI Not exactly how I would like it but will do for now * Add error handling Adds a lot of LiveData which may not be ideal, but is better than crashing due to connection errors. Co-authored-by: jarnedemeulemeester <jarnedemeulemeester@gmail.com>
This commit is contained in:
parent
e9e849d9e4
commit
62d09b3566
15 changed files with 477 additions and 20 deletions
|
@ -5,14 +5,23 @@ import androidx.databinding.BindingAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||||
import dev.jdtech.jellyfin.adapters.*
|
import dev.jdtech.jellyfin.adapters.CollectionListAdapter
|
||||||
|
import dev.jdtech.jellyfin.adapters.EpisodeItem
|
||||||
|
import dev.jdtech.jellyfin.adapters.EpisodeListAdapter
|
||||||
|
import dev.jdtech.jellyfin.adapters.FavoritesListAdapter
|
||||||
|
import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter
|
||||||
|
import dev.jdtech.jellyfin.adapters.HomeItem
|
||||||
|
import dev.jdtech.jellyfin.adapters.PersonListAdapter
|
||||||
|
import dev.jdtech.jellyfin.adapters.ServerGridAdapter
|
||||||
|
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
||||||
|
import dev.jdtech.jellyfin.adapters.ViewListAdapter
|
||||||
import dev.jdtech.jellyfin.api.JellyfinApi
|
import dev.jdtech.jellyfin.api.JellyfinApi
|
||||||
import dev.jdtech.jellyfin.database.Server
|
import dev.jdtech.jellyfin.database.Server
|
||||||
import dev.jdtech.jellyfin.models.FavoriteSection
|
import dev.jdtech.jellyfin.models.FavoriteSection
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.BaseItemPerson
|
import org.jellyfin.sdk.model.api.BaseItemPerson
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
@BindingAdapter("servers")
|
@BindingAdapter("servers")
|
||||||
fun bindServers(recyclerView: RecyclerView, data: List<Server>?) {
|
fun bindServers(recyclerView: RecyclerView, data: List<Server>?) {
|
||||||
|
|
|
@ -8,7 +8,8 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import dev.jdtech.jellyfin.databinding.PersonItemBinding
|
import dev.jdtech.jellyfin.databinding.PersonItemBinding
|
||||||
import org.jellyfin.sdk.model.api.BaseItemPerson
|
import org.jellyfin.sdk.model.api.BaseItemPerson
|
||||||
|
|
||||||
class PersonListAdapter :ListAdapter<BaseItemPerson, PersonListAdapter.PersonViewHolder>(DiffCallback) {
|
class PersonListAdapter(private val clickListener: (item: BaseItemPerson) -> Unit) :ListAdapter<BaseItemPerson, PersonListAdapter.PersonViewHolder>(DiffCallback) {
|
||||||
|
|
||||||
class PersonViewHolder(private var binding: PersonItemBinding) :
|
class PersonViewHolder(private var binding: PersonItemBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(person: BaseItemPerson) {
|
fun bind(person: BaseItemPerson) {
|
||||||
|
@ -40,5 +41,6 @@ class PersonListAdapter :ListAdapter<BaseItemPerson, PersonListAdapter.PersonVie
|
||||||
override fun onBindViewHolder(holder: PersonViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: PersonViewHolder, position: Int) {
|
||||||
val item = getItem(position)
|
val item = getItem(position)
|
||||||
holder.bind(item)
|
holder.bind(item)
|
||||||
|
holder.itemView.setOnClickListener { clickListener(item) }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -20,6 +20,7 @@ class ViewListAdapter(
|
||||||
private val onItemClickListener: ViewItemListAdapter.OnClickListener,
|
private val onItemClickListener: ViewItemListAdapter.OnClickListener,
|
||||||
private val onNextUpClickListener: HomeEpisodeListAdapter.OnClickListener
|
private val onNextUpClickListener: HomeEpisodeListAdapter.OnClickListener
|
||||||
) : ListAdapter<HomeItem, RecyclerView.ViewHolder>(DiffCallback) {
|
) : ListAdapter<HomeItem, RecyclerView.ViewHolder>(DiffCallback) {
|
||||||
|
|
||||||
class ViewViewHolder(private var binding: ViewItemBinding) :
|
class ViewViewHolder(private var binding: ViewItemBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(
|
fun bind(
|
||||||
|
|
|
@ -7,7 +7,6 @@ import org.jellyfin.sdk.createJellyfin
|
||||||
import org.jellyfin.sdk.model.ClientInfo
|
import org.jellyfin.sdk.model.ClientInfo
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Jellyfin API class using org.jellyfin.sdk:jellyfin-platform-android
|
* Jellyfin API class using org.jellyfin.sdk:jellyfin-platform-android
|
||||||
*
|
*
|
||||||
|
|
|
@ -6,6 +6,7 @@ import android.os.Bundle
|
||||||
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 android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
@ -22,6 +23,8 @@ import dev.jdtech.jellyfin.models.PlayerItem
|
||||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||||
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
|
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MediaInfoFragment : Fragment() {
|
class MediaInfoFragment : Fragment() {
|
||||||
|
@ -163,7 +166,14 @@ class MediaInfoFragment : Fragment() {
|
||||||
ViewItemListAdapter(ViewItemListAdapter.OnClickListener { season ->
|
ViewItemListAdapter(ViewItemListAdapter.OnClickListener { season ->
|
||||||
navigateToSeasonFragment(season)
|
navigateToSeasonFragment(season)
|
||||||
}, fixedWidth = true)
|
}, fixedWidth = true)
|
||||||
binding.peopleRecyclerView.adapter = PersonListAdapter()
|
binding.peopleRecyclerView.adapter = PersonListAdapter { person ->
|
||||||
|
val uuid = person.id?.toUUID()
|
||||||
|
if (uuid != null) {
|
||||||
|
navigateToPersonDetail(uuid)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(requireContext(), R.string.error_getting_person_id, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
binding.playButton.setOnClickListener {
|
binding.playButton.setOnClickListener {
|
||||||
binding.playButton.setImageResource(android.R.color.transparent)
|
binding.playButton.setImageResource(android.R.color.transparent)
|
||||||
|
@ -229,4 +239,10 @@ class MediaInfoFragment : Fragment() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun navigateToPersonDetail(personId: UUID) {
|
||||||
|
findNavController().navigate(
|
||||||
|
MediaInfoFragmentDirections.actionMediaInfoFragmentToPersonDetailFragment(personId)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
package dev.jdtech.jellyfin.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dev.jdtech.jellyfin.R
|
||||||
|
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
||||||
|
import dev.jdtech.jellyfin.bindItemImage
|
||||||
|
import dev.jdtech.jellyfin.databinding.FragmentPersonDetailBinding
|
||||||
|
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||||
|
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.PersonDetailViewModel
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
internal class PersonDetailFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentPersonDetailBinding
|
||||||
|
private val viewModel: PersonDetailViewModel by viewModels()
|
||||||
|
|
||||||
|
private val args: PersonDetailFragmentArgs by navArgs()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
binding = FragmentPersonDetailBinding.inflate(inflater, container, false)
|
||||||
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.moviesList.adapter = adapter()
|
||||||
|
binding.showList.adapter = adapter()
|
||||||
|
|
||||||
|
viewModel.data.observe(viewLifecycleOwner) { data ->
|
||||||
|
binding.name.text = data.name
|
||||||
|
binding.overview.text = data.overview
|
||||||
|
|
||||||
|
setupOverviewExpansion()
|
||||||
|
|
||||||
|
bindItemImage(binding.personImage, data.dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.finishedLoading.observe(viewLifecycleOwner, {
|
||||||
|
binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE
|
||||||
|
})
|
||||||
|
|
||||||
|
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||||
|
if (error != null) {
|
||||||
|
checkIfLoginRequired(error)
|
||||||
|
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||||
|
binding.fragmentContent.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
binding.errorLayout.errorPanel.visibility = View.GONE
|
||||||
|
binding.fragmentContent.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||||
|
viewModel.loadData(args.personId)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||||
|
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(
|
||||||
|
parentFragmentManager,
|
||||||
|
"errordialog"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.loadData(args.personId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adapter() = ViewItemListAdapter(
|
||||||
|
fixedWidth = true,
|
||||||
|
onClickListener = ViewItemListAdapter.OnClickListener { navigateToMediaInfoFragment(it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun setupOverviewExpansion() = binding.overview.post {
|
||||||
|
binding.readAll.setOnClickListener {
|
||||||
|
with(binding.overview) {
|
||||||
|
if (layoutParams.height == ConstraintLayout.LayoutParams.WRAP_CONTENT) {
|
||||||
|
updateLayoutParams { height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT }
|
||||||
|
binding.readAll.text = getString(R.string.view_all)
|
||||||
|
binding.overviewGradient.isVisible = true
|
||||||
|
} else {
|
||||||
|
updateLayoutParams { height = ConstraintLayout.LayoutParams.WRAP_CONTENT }
|
||||||
|
binding.readAll.text = getString(R.string.hide)
|
||||||
|
binding.overviewGradient.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToMediaInfoFragment(item: BaseItemDto) {
|
||||||
|
findNavController().navigate(
|
||||||
|
PersonDetailFragmentDirections.actionPersonDetailFragmentToMediaInfoFragment(
|
||||||
|
itemId = item.id,
|
||||||
|
itemName = item.name,
|
||||||
|
itemType = item.type ?: "Unknown"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dev.jdtech.jellyfin.models
|
||||||
|
|
||||||
|
enum class ContentType(val type: String) {
|
||||||
|
MOVIE("Movie"),
|
||||||
|
TVSHOW("Series"),
|
||||||
|
UNKNOWN("")
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
package dev.jdtech.jellyfin.repository
|
package dev.jdtech.jellyfin.repository
|
||||||
|
|
||||||
|
|
||||||
|
import dev.jdtech.jellyfin.models.ContentType
|
||||||
import dev.jdtech.jellyfin.utils.SortBy
|
import dev.jdtech.jellyfin.utils.SortBy
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.ItemFields
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
|
@ -20,6 +22,12 @@ interface JellyfinRepository {
|
||||||
sortOrder: SortOrder = SortOrder.ASCENDING
|
sortOrder: SortOrder = SortOrder.ASCENDING
|
||||||
): List<BaseItemDto>
|
): List<BaseItemDto>
|
||||||
|
|
||||||
|
suspend fun getPersonItems(
|
||||||
|
personIds: List<UUID>,
|
||||||
|
includeTypes: List<ContentType>? = null,
|
||||||
|
recursive: Boolean = true
|
||||||
|
): List<BaseItemDto>
|
||||||
|
|
||||||
suspend fun getFavoriteItems(): List<BaseItemDto>
|
suspend fun getFavoriteItems(): List<BaseItemDto>
|
||||||
|
|
||||||
suspend fun getSearchItems(searchQuery: String): List<BaseItemDto>
|
suspend fun getSearchItems(searchQuery: String): List<BaseItemDto>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package dev.jdtech.jellyfin.repository
|
package dev.jdtech.jellyfin.repository
|
||||||
|
|
||||||
import dev.jdtech.jellyfin.api.JellyfinApi
|
import dev.jdtech.jellyfin.api.JellyfinApi
|
||||||
|
import dev.jdtech.jellyfin.models.ContentType
|
||||||
import dev.jdtech.jellyfin.utils.SortBy
|
import dev.jdtech.jellyfin.utils.SortBy
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -13,7 +14,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
val views: List<BaseItemDto>
|
val views: List<BaseItemDto>
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
views =
|
views =
|
||||||
jellyfinApi.viewsApi.getUserViews(jellyfinApi.userId!!).content.items ?: listOf()
|
jellyfinApi.viewsApi.getUserViews(jellyfinApi.userId!!).content.items ?: emptyList()
|
||||||
}
|
}
|
||||||
return views
|
return views
|
||||||
}
|
}
|
||||||
|
@ -43,7 +44,24 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
recursive = recursive,
|
recursive = recursive,
|
||||||
sortBy = listOf(sortBy.SortString),
|
sortBy = listOf(sortBy.SortString),
|
||||||
sortOrder = listOf(sortOrder)
|
sortOrder = listOf(sortOrder)
|
||||||
).content.items ?: listOf()
|
).content.items ?: emptyList()
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPersonItems(
|
||||||
|
personIds: List<UUID>,
|
||||||
|
includeTypes: List<ContentType>?,
|
||||||
|
recursive: Boolean
|
||||||
|
): List<BaseItemDto> {
|
||||||
|
val items: List<BaseItemDto>
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
items = jellyfinApi.itemsApi.getItems(
|
||||||
|
jellyfinApi.userId!!,
|
||||||
|
personIds = personIds,
|
||||||
|
includeItemTypes = includeTypes?.map { it.type },
|
||||||
|
recursive = recursive
|
||||||
|
).content.items ?: emptyList()
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
@ -56,7 +74,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
filters = listOf(ItemFilter.IS_FAVORITE),
|
filters = listOf(ItemFilter.IS_FAVORITE),
|
||||||
includeItemTypes = listOf("Movie", "Series", "Episode"),
|
includeItemTypes = listOf("Movie", "Series", "Episode"),
|
||||||
recursive = true
|
recursive = true
|
||||||
).content.items ?: listOf()
|
).content.items ?: emptyList()
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
@ -69,7 +87,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
searchTerm = searchQuery,
|
searchTerm = searchQuery,
|
||||||
includeItemTypes = listOf("Movie", "Series", "Episode"),
|
includeItemTypes = listOf("Movie", "Series", "Episode"),
|
||||||
recursive = true
|
recursive = true
|
||||||
).content.items ?: listOf()
|
).content.items ?: emptyList()
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
@ -81,7 +99,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
jellyfinApi.itemsApi.getResumeItems(
|
jellyfinApi.itemsApi.getResumeItems(
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
includeItemTypes = listOf("Movie", "Episode"),
|
includeItemTypes = listOf("Movie", "Episode"),
|
||||||
).content.items ?: listOf()
|
).content.items ?: emptyList()
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
@ -101,7 +119,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
val seasons: List<BaseItemDto>
|
val seasons: List<BaseItemDto>
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
seasons = jellyfinApi.showsApi.getSeasons(seriesId, jellyfinApi.userId!!).content.items
|
seasons = jellyfinApi.showsApi.getSeasons(seriesId, jellyfinApi.userId!!).content.items
|
||||||
?: listOf()
|
?: emptyList()
|
||||||
}
|
}
|
||||||
return seasons
|
return seasons
|
||||||
}
|
}
|
||||||
|
@ -112,7 +130,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
nextUpItems = jellyfinApi.showsApi.getNextUp(
|
nextUpItems = jellyfinApi.showsApi.getNextUp(
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
seriesId = seriesId?.toString(),
|
seriesId = seriesId?.toString(),
|
||||||
).content.items ?: listOf()
|
).content.items ?: emptyList()
|
||||||
}
|
}
|
||||||
return nextUpItems
|
return nextUpItems
|
||||||
}
|
}
|
||||||
|
@ -131,7 +149,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
seasonId = seasonId,
|
seasonId = seasonId,
|
||||||
fields = fields,
|
fields = fields,
|
||||||
startItemId = startItemId
|
startItemId = startItemId
|
||||||
).content.items ?: listOf()
|
).content.items ?: emptyList()
|
||||||
}
|
}
|
||||||
return episodes
|
return episodes
|
||||||
}
|
}
|
||||||
|
@ -145,15 +163,15 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
name = "Direct play all",
|
name = "Direct play all",
|
||||||
maxStaticBitrate = 1_000_000_000,
|
maxStaticBitrate = 1_000_000_000,
|
||||||
maxStreamingBitrate = 1_000_000_000,
|
maxStreamingBitrate = 1_000_000_000,
|
||||||
codecProfiles = listOf(),
|
codecProfiles = emptyList(),
|
||||||
containerProfiles = listOf(),
|
containerProfiles = emptyList(),
|
||||||
directPlayProfiles = listOf(
|
directPlayProfiles = listOf(
|
||||||
DirectPlayProfile(
|
DirectPlayProfile(
|
||||||
type = DlnaProfileType.VIDEO
|
type = DlnaProfileType.VIDEO
|
||||||
), DirectPlayProfile(type = DlnaProfileType.AUDIO)
|
), DirectPlayProfile(type = DlnaProfileType.AUDIO)
|
||||||
),
|
),
|
||||||
transcodingProfiles = listOf(),
|
transcodingProfiles = emptyList(),
|
||||||
responseProfiles = listOf(),
|
responseProfiles = emptyList(),
|
||||||
enableAlbumArtInDidl = false,
|
enableAlbumArtInDidl = false,
|
||||||
enableMsMediaReceiverRegistrar = false,
|
enableMsMediaReceiverRegistrar = false,
|
||||||
enableSingleAlbumArtLimit = false,
|
enableSingleAlbumArtLimit = false,
|
||||||
|
@ -171,7 +189,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
maxStreamingBitrate = 1_000_000_000,
|
maxStreamingBitrate = 1_000_000_000,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
mediaSourceInfoList = mediaInfo.mediaSources ?: listOf()
|
mediaSourceInfoList = mediaInfo.mediaSources ?: emptyList()
|
||||||
return mediaSourceInfoList
|
return mediaSourceInfoList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,7 +296,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
intros =
|
intros =
|
||||||
jellyfinApi.userLibraryApi.getIntros(jellyfinApi.userId!!, itemId).content.items
|
jellyfinApi.userLibraryApi.getIntros(jellyfinApi.userId!!, itemId).content.items
|
||||||
?: listOf()
|
?: emptyList()
|
||||||
}
|
}
|
||||||
return intros
|
return intros
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package dev.jdtech.jellyfin.utils
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import dev.jdtech.jellyfin.MainNavigationDirections
|
import dev.jdtech.jellyfin.MainNavigationDirections
|
||||||
|
import dev.jdtech.jellyfin.models.ContentType
|
||||||
import dev.jdtech.jellyfin.models.View
|
import dev.jdtech.jellyfin.models.View
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -15,6 +16,12 @@ fun BaseItemDto.toView(): View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BaseItemDto.contentType() = when (type) {
|
||||||
|
"Movie" -> ContentType.MOVIE
|
||||||
|
"Series" -> ContentType.TVSHOW
|
||||||
|
else -> ContentType.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
fun Fragment.checkIfLoginRequired(error: String) {
|
fun Fragment.checkIfLoginRequired(error: String) {
|
||||||
if (error.contains("401")) {
|
if (error.contains("401")) {
|
||||||
Timber.d("Login required!")
|
Timber.d("Login required!")
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dev.jdtech.jellyfin.models.ContentType.MOVIE
|
||||||
|
import dev.jdtech.jellyfin.models.ContentType.TVSHOW
|
||||||
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
|
import dev.jdtech.jellyfin.utils.contentType
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.lang.Exception
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
internal class PersonDetailViewModel @Inject internal constructor(
|
||||||
|
private val jellyfinRepository: JellyfinRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val data = MutableLiveData<PersonOverview>()
|
||||||
|
val starredIn = MutableLiveData<StarredIn>()
|
||||||
|
|
||||||
|
private val _finishedLoading = MutableLiveData<Boolean>()
|
||||||
|
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
||||||
|
|
||||||
|
private val _error = MutableLiveData<String>()
|
||||||
|
val error: LiveData<String> = _error
|
||||||
|
|
||||||
|
fun loadData(personId: UUID) {
|
||||||
|
_error.value = null
|
||||||
|
_finishedLoading.value = false
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val personDetail = jellyfinRepository.getItem(personId)
|
||||||
|
|
||||||
|
data.postValue(
|
||||||
|
PersonOverview(
|
||||||
|
name = personDetail.name.orEmpty(),
|
||||||
|
overview = personDetail.overview.orEmpty(),
|
||||||
|
dto = personDetail
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val items = jellyfinRepository.getPersonItems(
|
||||||
|
personIds = listOf(personId),
|
||||||
|
includeTypes = listOf(MOVIE, TVSHOW),
|
||||||
|
recursive = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val movies = items.filter { it.contentType() == MOVIE }
|
||||||
|
val shows = items.filter { it.contentType() == TVSHOW }
|
||||||
|
|
||||||
|
starredIn.postValue(StarredIn(movies, shows))
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
_error.value = e.toString()
|
||||||
|
}
|
||||||
|
_finishedLoading.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PersonOverview(
|
||||||
|
val name: String,
|
||||||
|
val overview: String,
|
||||||
|
val dto: BaseItemDto
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StarredIn(
|
||||||
|
val movies: List<BaseItemDto>,
|
||||||
|
val shows: List<BaseItemDto>
|
||||||
|
)
|
||||||
|
}
|
169
app/src/main/res/layout/fragment_person_detail.xml
Normal file
169
app/src/main/res/layout/fragment_person_detail.xml
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
<?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">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<import type="android.view.View" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="viewModel"
|
||||||
|
type="dev.jdtech.jellyfin.viewmodels.PersonDetailViewModel" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
android:id="@+id/loading_indicator"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:indeterminate="true"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/error_layout"
|
||||||
|
layout="@layout/error_panel" />
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/fragment_content"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_marginBottom="24dp">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/person_image"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="210dp"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintDimensionRatio="H,3:2"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Findroid.Image" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/person_image"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Actor/Actress name" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/overview"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/read_all"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/person_image"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/name" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/overview_gradient"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:background="@drawable/header_gradient"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/overview"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/overview"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/overview" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/read_all"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:text="@string/view_all"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/movie_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:text="@string/movies_label"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||||
|
android:visibility="@{viewModel.starredIn.movies.empty ? View.GONE : View.VISIBLE}" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/movies_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
app:items="@{viewModel.starredIn.movies}"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:itemCount="4"
|
||||||
|
tools:listitem="@layout/base_item" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/show_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:text="@string/shows_label"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||||
|
android:visibility="@{viewModel.starredIn.shows.empty ? View.GONE : View.VISIBLE}" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/show_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
app:items="@{viewModel.starredIn.shows}"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:itemCount="4"
|
||||||
|
tools:listitem="@layout/base_item" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</layout>
|
|
@ -15,7 +15,9 @@
|
||||||
android:layout_width="110dp"
|
android:layout_width="110dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:layout_marginHorizontal="8dp"
|
||||||
android:foreground="?android:attr/selectableItemBackground"
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="@drawable/ripple_background"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
|
|
@ -116,6 +116,10 @@
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_mediaInfoFragment_to_playerActivity"
|
android:id="@+id/action_mediaInfoFragment_to_playerActivity"
|
||||||
app:destination="@id/playerActivity" />
|
app:destination="@id/playerActivity" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_mediaInfoFragment_to_personDetailFragment"
|
||||||
|
app:destination="@id/personDetailFragment"
|
||||||
|
/>
|
||||||
<argument
|
<argument
|
||||||
android:name="itemType"
|
android:name="itemType"
|
||||||
app:argType="string" />
|
app:argType="string" />
|
||||||
|
@ -228,6 +232,21 @@
|
||||||
app:popUpToInclusive="true" />
|
app:popUpToInclusive="true" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/personDetailFragment"
|
||||||
|
android:name="dev.jdtech.jellyfin.fragments.PersonDetailFragment"
|
||||||
|
android:label="@string/person_detail_title"
|
||||||
|
tools:layout="@layout/fragment_person_detail">
|
||||||
|
|
||||||
|
<argument
|
||||||
|
android:name="personId"
|
||||||
|
app:argType="java.util.UUID" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_personDetailFragment_to_mediaInfoFragment"
|
||||||
|
app:destination="@id/mediaInfoFragment" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
<include app:graph="@navigation/aboutlibs_navigation" />
|
<include app:graph="@navigation/aboutlibs_navigation" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_global_loginFragment"
|
android:id="@+id/action_global_loginFragment"
|
||||||
|
|
|
@ -65,6 +65,11 @@
|
||||||
<string name="mpv_player_summary">Use the experimental MPV Player to play videos. MPV has support for more video, audio and subtitle codecs.</string>
|
<string name="mpv_player_summary">Use the experimental MPV Player to play videos. MPV has support for more video, audio and subtitle codecs.</string>
|
||||||
<string name="force_software_decoding">Force software decoding</string>
|
<string name="force_software_decoding">Force software decoding</string>
|
||||||
<string name="force_software_decoding_summary">Disable hardware decoding and use software decoding. Can be useful if hardware decoding gives weird artifacts.</string>
|
<string name="force_software_decoding_summary">Disable hardware decoding and use software decoding. Can be useful if hardware decoding gives weird artifacts.</string>
|
||||||
|
<string name="person_detail_title">Person Detail</string>
|
||||||
|
<string name="error_getting_person_id">Detail unavailable</string>
|
||||||
|
<string name="movies_label">Movies</string>
|
||||||
|
<string name="shows_label">TV Shows</string>
|
||||||
|
<string name="hide">Hide</string>
|
||||||
<string name="sort_by">Sort by</string>
|
<string name="sort_by">Sort by</string>
|
||||||
<string name="sort_order">Sort order</string>
|
<string name="sort_order">Sort order</string>
|
||||||
<string name="close">Close</string>
|
<string name="close">Close</string>
|
||||||
|
|
Loading…
Reference in a new issue