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 com.bumptech.glide.Glide
|
||||
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.database.Server
|
||||
import dev.jdtech.jellyfin.models.FavoriteSection
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemPerson
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
@BindingAdapter("servers")
|
||||
fun bindServers(recyclerView: RecyclerView, data: List<Server>?) {
|
||||
|
|
|
@ -8,7 +8,8 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import dev.jdtech.jellyfin.databinding.PersonItemBinding
|
||||
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) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(person: BaseItemPerson) {
|
||||
|
@ -40,5 +41,6 @@ class PersonListAdapter :ListAdapter<BaseItemPerson, PersonListAdapter.PersonVie
|
|||
override fun onBindViewHolder(holder: PersonViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
holder.bind(item)
|
||||
holder.itemView.setOnClickListener { clickListener(item) }
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ class ViewListAdapter(
|
|||
private val onItemClickListener: ViewItemListAdapter.OnClickListener,
|
||||
private val onNextUpClickListener: HomeEpisodeListAdapter.OnClickListener
|
||||
) : ListAdapter<HomeItem, RecyclerView.ViewHolder>(DiffCallback) {
|
||||
|
||||
class ViewViewHolder(private var binding: ViewItemBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(
|
||||
|
|
|
@ -7,7 +7,6 @@ import org.jellyfin.sdk.createJellyfin
|
|||
import org.jellyfin.sdk.model.ClientInfo
|
||||
import java.util.*
|
||||
|
||||
|
||||
/**
|
||||
* 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.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
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.viewmodels.MediaInfoViewModel
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import java.util.UUID
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MediaInfoFragment : Fragment() {
|
||||
|
@ -163,7 +166,14 @@ class MediaInfoFragment : Fragment() {
|
|||
ViewItemListAdapter(ViewItemListAdapter.OnClickListener { season ->
|
||||
navigateToSeasonFragment(season)
|
||||
}, 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.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
|
||||
|
||||
|
||||
import dev.jdtech.jellyfin.models.ContentType
|
||||
import dev.jdtech.jellyfin.utils.SortBy
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
|
@ -20,6 +22,12 @@ interface JellyfinRepository {
|
|||
sortOrder: SortOrder = SortOrder.ASCENDING
|
||||
): List<BaseItemDto>
|
||||
|
||||
suspend fun getPersonItems(
|
||||
personIds: List<UUID>,
|
||||
includeTypes: List<ContentType>? = null,
|
||||
recursive: Boolean = true
|
||||
): List<BaseItemDto>
|
||||
|
||||
suspend fun getFavoriteItems(): List<BaseItemDto>
|
||||
|
||||
suspend fun getSearchItems(searchQuery: String): List<BaseItemDto>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package dev.jdtech.jellyfin.repository
|
||||
|
||||
import dev.jdtech.jellyfin.api.JellyfinApi
|
||||
import dev.jdtech.jellyfin.models.ContentType
|
||||
import dev.jdtech.jellyfin.utils.SortBy
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -13,7 +14,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
val views: List<BaseItemDto>
|
||||
withContext(Dispatchers.IO) {
|
||||
views =
|
||||
jellyfinApi.viewsApi.getUserViews(jellyfinApi.userId!!).content.items ?: listOf()
|
||||
jellyfinApi.viewsApi.getUserViews(jellyfinApi.userId!!).content.items ?: emptyList()
|
||||
}
|
||||
return views
|
||||
}
|
||||
|
@ -43,7 +44,24 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
recursive = recursive,
|
||||
sortBy = listOf(sortBy.SortString),
|
||||
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
|
||||
}
|
||||
|
@ -56,7 +74,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
filters = listOf(ItemFilter.IS_FAVORITE),
|
||||
includeItemTypes = listOf("Movie", "Series", "Episode"),
|
||||
recursive = true
|
||||
).content.items ?: listOf()
|
||||
).content.items ?: emptyList()
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
@ -69,7 +87,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
searchTerm = searchQuery,
|
||||
includeItemTypes = listOf("Movie", "Series", "Episode"),
|
||||
recursive = true
|
||||
).content.items ?: listOf()
|
||||
).content.items ?: emptyList()
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
@ -81,7 +99,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
jellyfinApi.itemsApi.getResumeItems(
|
||||
jellyfinApi.userId!!,
|
||||
includeItemTypes = listOf("Movie", "Episode"),
|
||||
).content.items ?: listOf()
|
||||
).content.items ?: emptyList()
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
@ -101,7 +119,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
val seasons: List<BaseItemDto>
|
||||
withContext(Dispatchers.IO) {
|
||||
seasons = jellyfinApi.showsApi.getSeasons(seriesId, jellyfinApi.userId!!).content.items
|
||||
?: listOf()
|
||||
?: emptyList()
|
||||
}
|
||||
return seasons
|
||||
}
|
||||
|
@ -112,7 +130,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
nextUpItems = jellyfinApi.showsApi.getNextUp(
|
||||
jellyfinApi.userId!!,
|
||||
seriesId = seriesId?.toString(),
|
||||
).content.items ?: listOf()
|
||||
).content.items ?: emptyList()
|
||||
}
|
||||
return nextUpItems
|
||||
}
|
||||
|
@ -131,7 +149,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
seasonId = seasonId,
|
||||
fields = fields,
|
||||
startItemId = startItemId
|
||||
).content.items ?: listOf()
|
||||
).content.items ?: emptyList()
|
||||
}
|
||||
return episodes
|
||||
}
|
||||
|
@ -145,15 +163,15 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
name = "Direct play all",
|
||||
maxStaticBitrate = 1_000_000_000,
|
||||
maxStreamingBitrate = 1_000_000_000,
|
||||
codecProfiles = listOf(),
|
||||
containerProfiles = listOf(),
|
||||
codecProfiles = emptyList(),
|
||||
containerProfiles = emptyList(),
|
||||
directPlayProfiles = listOf(
|
||||
DirectPlayProfile(
|
||||
type = DlnaProfileType.VIDEO
|
||||
), DirectPlayProfile(type = DlnaProfileType.AUDIO)
|
||||
),
|
||||
transcodingProfiles = listOf(),
|
||||
responseProfiles = listOf(),
|
||||
transcodingProfiles = emptyList(),
|
||||
responseProfiles = emptyList(),
|
||||
enableAlbumArtInDidl = false,
|
||||
enableMsMediaReceiverRegistrar = false,
|
||||
enableSingleAlbumArtLimit = false,
|
||||
|
@ -171,7 +189,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
maxStreamingBitrate = 1_000_000_000,
|
||||
)
|
||||
)
|
||||
mediaSourceInfoList = mediaInfo.mediaSources ?: listOf()
|
||||
mediaSourceInfoList = mediaInfo.mediaSources ?: emptyList()
|
||||
return mediaSourceInfoList
|
||||
}
|
||||
|
||||
|
@ -278,7 +296,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
withContext(Dispatchers.IO) {
|
||||
intros =
|
||||
jellyfinApi.userLibraryApi.getIntros(jellyfinApi.userId!!, itemId).content.items
|
||||
?: listOf()
|
||||
?: emptyList()
|
||||
}
|
||||
return intros
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package dev.jdtech.jellyfin.utils
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import dev.jdtech.jellyfin.MainNavigationDirections
|
||||
import dev.jdtech.jellyfin.models.ContentType
|
||||
import dev.jdtech.jellyfin.models.View
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
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) {
|
||||
if (error.contains("401")) {
|
||||
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_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="@drawable/ripple_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
|
|
|
@ -116,6 +116,10 @@
|
|||
<action
|
||||
android:id="@+id/action_mediaInfoFragment_to_playerActivity"
|
||||
app:destination="@id/playerActivity" />
|
||||
<action
|
||||
android:id="@+id/action_mediaInfoFragment_to_personDetailFragment"
|
||||
app:destination="@id/personDetailFragment"
|
||||
/>
|
||||
<argument
|
||||
android:name="itemType"
|
||||
app:argType="string" />
|
||||
|
@ -228,6 +232,21 @@
|
|||
app:popUpToInclusive="true" />
|
||||
</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" />
|
||||
<action
|
||||
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="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="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_order">Sort order</string>
|
||||
<string name="close">Close</string>
|
||||
|
|
Loading…
Reference in a new issue