commit
905332e097
41 changed files with 419 additions and 215 deletions
|
@ -16,8 +16,8 @@ android {
|
|||
applicationId "dev.jdtech.jellyfin"
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 31
|
||||
versionCode 2
|
||||
versionName "0.1.1"
|
||||
versionCode 3
|
||||
versionName "0.1.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
|
@ -20,4 +20,4 @@
|
|||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-keep public class dev.jdtech.jellyfin.models.PlayerItem
|
||||
-keepnames class dev.jdtech.jellyfin.models.PlayerItem
|
|
@ -11,6 +11,7 @@ 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.*
|
||||
|
||||
@BindingAdapter("servers")
|
||||
|
@ -35,11 +36,12 @@ fun bindItems(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
|
|||
fun bindItemImage(imageView: ImageView, item: BaseItemDto) {
|
||||
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
|
||||
|
||||
val itemId = if (item.type == "Episode") item.seriesId else item.id
|
||||
val itemId =
|
||||
if (item.type == "Episode" || item.type == "Season" && item.imageTags.isNullOrEmpty()) item.seriesId else item.id
|
||||
|
||||
Glide
|
||||
.with(imageView.context)
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${itemId}/Images/Primary"))
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${itemId}/Images/${ImageType.PRIMARY}"))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.placeholder(R.color.neutral_800)
|
||||
.into(imageView)
|
||||
|
@ -49,17 +51,16 @@ fun bindItemImage(imageView: ImageView, item: BaseItemDto) {
|
|||
|
||||
@BindingAdapter("itemBackdropImage")
|
||||
fun bindItemBackdropImage(imageView: ImageView, item: BaseItemDto?) {
|
||||
if (item != null) {
|
||||
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
|
||||
if (item == null) return
|
||||
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
|
||||
|
||||
Glide
|
||||
.with(imageView.context)
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${item.id}/Images/Backdrop"))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(imageView)
|
||||
Glide
|
||||
.with(imageView.context)
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${item.id}/Images/${ImageType.BACKDROP}"))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(imageView)
|
||||
|
||||
imageView.contentDescription = "${item.name} backdrop"
|
||||
}
|
||||
imageView.contentDescription = "${item.name} backdrop"
|
||||
}
|
||||
|
||||
@BindingAdapter("itemBackdropById")
|
||||
|
@ -68,7 +69,7 @@ fun bindItemBackdropById(imageView: ImageView, itemId: UUID) {
|
|||
|
||||
Glide
|
||||
.with(imageView.context)
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${itemId}/Images/Backdrop"))
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${itemId}/Images/${ImageType.BACKDROP}"))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(imageView)
|
||||
}
|
||||
|
@ -79,20 +80,6 @@ fun bindCollections(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
|
|||
adapter.submitList(data)
|
||||
}
|
||||
|
||||
@BindingAdapter("collectionImage")
|
||||
fun bindCollectionImage(imageView: ImageView, item: BaseItemDto) {
|
||||
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
|
||||
|
||||
Glide
|
||||
.with(imageView.context)
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${item.id}/Images/Primary"))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.placeholder(R.color.neutral_800)
|
||||
.into(imageView)
|
||||
|
||||
imageView.contentDescription = "${item.name} image"
|
||||
}
|
||||
|
||||
@BindingAdapter("people")
|
||||
fun bindPeople(recyclerView: RecyclerView, data: List<BaseItemPerson>?) {
|
||||
val adapter = recyclerView.adapter as PersonListAdapter
|
||||
|
@ -105,7 +92,7 @@ fun bindPersonImage(imageView: ImageView, person: BaseItemPerson) {
|
|||
|
||||
Glide
|
||||
.with(imageView.context)
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${person.id}/Images/Primary"))
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${person.id}/Images/${ImageType.PRIMARY}"))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.placeholder(R.color.neutral_800)
|
||||
.into(imageView)
|
||||
|
@ -125,15 +112,38 @@ fun bindHomeEpisodes(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
|
|||
adapter.submitList(data)
|
||||
}
|
||||
|
||||
@BindingAdapter("episodeImage")
|
||||
fun bindEpisodeImage(imageView: ImageView, episode: BaseItemDto) {
|
||||
@BindingAdapter("baseItemImage")
|
||||
fun bindBaseItemImage(imageView: ImageView, episode: BaseItemDto?) {
|
||||
if (episode == null) return
|
||||
|
||||
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
|
||||
|
||||
val imageType = if (episode.type == "Movie") "Backdrop" else "Primary"
|
||||
var imageItemId = episode.id
|
||||
var imageType = ImageType.PRIMARY
|
||||
|
||||
if (!episode.imageTags.isNullOrEmpty()) {
|
||||
when (episode.type) {
|
||||
"Movie" -> {
|
||||
if (!episode.backdropImageTags.isNullOrEmpty()) {
|
||||
imageType = ImageType.BACKDROP
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (!episode.imageTags!!.keys.contains(ImageType.PRIMARY)) {
|
||||
imageType = ImageType.BACKDROP
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (episode.type == "Episode") {
|
||||
imageItemId = episode.seriesId!!
|
||||
imageType = ImageType.BACKDROP
|
||||
}
|
||||
}
|
||||
|
||||
Glide
|
||||
.with(imageView.context)
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${episode.id}/Images/$imageType"))
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${imageItemId}/Images/$imageType"))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.placeholder(R.color.neutral_800)
|
||||
.into(imageView)
|
||||
|
@ -147,28 +157,12 @@ fun bindSeasonPoster(imageView: ImageView, seasonId: UUID) {
|
|||
|
||||
Glide
|
||||
.with(imageView.context)
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${seasonId}/Images/Primary"))
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${seasonId}/Images/${ImageType.PRIMARY}"))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.placeholder(R.color.neutral_800)
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
@BindingAdapter("itemPrimaryImage")
|
||||
fun bindItemPrimaryImage(imageView: ImageView, item: BaseItemDto?) {
|
||||
if (item != null) {
|
||||
val jellyfinApi = JellyfinApi.getInstance(imageView.context.applicationContext, "")
|
||||
|
||||
Glide
|
||||
.with(imageView.context)
|
||||
.load(jellyfinApi.api.baseUrl.plus("/items/${item.id}/Images/Primary"))
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.placeholder(R.color.neutral_800)
|
||||
.into(imageView)
|
||||
|
||||
imageView.contentDescription = "${item.name} poster"
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("favoriteSections")
|
||||
fun bindFavoriteSections(recyclerView: RecyclerView, data: List<FavoriteSection>?) {
|
||||
val adapter = recyclerView.adapter as FavoritesListAdapter
|
||||
|
|
|
@ -38,7 +38,7 @@ class PlayerActivity : AppCompatActivity() {
|
|||
})
|
||||
|
||||
if (viewModel.player.value == null) {
|
||||
viewModel.initializePlayer(args.items, args.playbackPosition)
|
||||
viewModel.initializePlayer(args.items)
|
||||
}
|
||||
hideSystemUI()
|
||||
}
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
package dev.jdtech.jellyfin.database
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import androidx.room.*
|
||||
|
||||
@Dao
|
||||
interface ServerDatabaseDao {
|
||||
@Insert
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(server: Server)
|
||||
|
||||
@Update
|
||||
|
|
|
@ -11,12 +11,12 @@ class VideoVersionDialogFragment(
|
|||
private val viewModel: MediaInfoViewModel
|
||||
) : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val items = viewModel.mediaSources.value!!.map { it.name }
|
||||
val items = viewModel.item.value?.mediaSources?.map { it.name }
|
||||
return activity?.let {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setTitle("Select a version")
|
||||
.setItems(items.toTypedArray()) { _, which ->
|
||||
viewModel.navigateToPlayer(viewModel.mediaSources.value!![which])
|
||||
.setItems(items?.toTypedArray()) { _, which ->
|
||||
viewModel.preparePlayerItems(which)
|
||||
}
|
||||
builder.create()
|
||||
} ?: throw IllegalStateException("Activity cannot be null")
|
||||
|
|
|
@ -37,7 +37,7 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
binding.playButton.setOnClickListener {
|
||||
binding.playButton.setImageResource(android.R.color.transparent)
|
||||
binding.progressCircular.visibility = View.VISIBLE
|
||||
viewModel.preparePlayer()
|
||||
viewModel.preparePlayerItems()
|
||||
}
|
||||
|
||||
binding.checkButton.setOnClickListener {
|
||||
|
@ -91,10 +91,14 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
if (it) {
|
||||
navigateToPlayerActivity(
|
||||
viewModel.playerItems.toTypedArray(),
|
||||
viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000)
|
||||
)
|
||||
viewModel.doneNavigateToPlayer()
|
||||
binding.playButton.setImageDrawable(ContextCompat.getDrawable(requireActivity(), R.drawable.ic_play))
|
||||
binding.playButton.setImageDrawable(
|
||||
ContextCompat.getDrawable(
|
||||
requireActivity(),
|
||||
R.drawable.ic_play
|
||||
)
|
||||
)
|
||||
binding.progressCircular.visibility = View.INVISIBLE
|
||||
}
|
||||
})
|
||||
|
@ -102,7 +106,12 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
viewModel.playerItemsError.observe(viewLifecycleOwner, { errorMessage ->
|
||||
if (errorMessage != null) {
|
||||
binding.playerItemsError.visibility = View.VISIBLE
|
||||
binding.playButton.setImageDrawable(ContextCompat.getDrawable(requireActivity(), R.drawable.ic_play))
|
||||
binding.playButton.setImageDrawable(
|
||||
ContextCompat.getDrawable(
|
||||
requireActivity(),
|
||||
R.drawable.ic_play
|
||||
)
|
||||
)
|
||||
binding.progressCircular.visibility = View.INVISIBLE
|
||||
} else {
|
||||
binding.playerItemsError.visibility = View.GONE
|
||||
|
@ -110,7 +119,9 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
})
|
||||
|
||||
binding.playerItemsErrorDetails.setOnClickListener {
|
||||
ErrorDialogFragment(viewModel.playerItemsError.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog")
|
||||
ErrorDialogFragment(
|
||||
viewModel.playerItemsError.value ?: getString(R.string.unknown_error)
|
||||
).show(parentFragmentManager, "errordialog")
|
||||
}
|
||||
|
||||
viewModel.loadEpisode(args.episodeId)
|
||||
|
@ -120,12 +131,10 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
|
||||
private fun navigateToPlayerActivity(
|
||||
playerItems: Array<PlayerItem>,
|
||||
playbackPosition: Long
|
||||
) {
|
||||
findNavController().navigate(
|
||||
EpisodeBottomSheetFragmentDirections.actionEpisodeBottomSheetFragmentToPlayerActivity(
|
||||
playerItems,
|
||||
playbackPosition
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter
|
|||
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
||||
import dev.jdtech.jellyfin.databinding.FragmentFavoriteBinding
|
||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import dev.jdtech.jellyfin.viewmodels.FavoriteViewModel
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
|
||||
|
@ -44,6 +45,7 @@ class FavoriteFragment : Fragment() {
|
|||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.favoritesRecyclerView.visibility = View.GONE
|
||||
} else {
|
||||
|
|
|
@ -12,6 +12,7 @@ import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
|||
import dev.jdtech.jellyfin.adapters.ViewListAdapter
|
||||
import dev.jdtech.jellyfin.databinding.FragmentHomeBinding
|
||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import dev.jdtech.jellyfin.viewmodels.HomeViewModel
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
|
||||
|
@ -73,6 +74,7 @@ class HomeFragment : Fragment() {
|
|||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.viewsRecyclerView.visibility = View.GONE
|
||||
} else {
|
||||
|
@ -86,7 +88,10 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
|
||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog")
|
||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(
|
||||
parentFragmentManager,
|
||||
"errordialog"
|
||||
)
|
||||
}
|
||||
|
||||
return binding.root
|
||||
|
@ -96,7 +101,8 @@ class HomeFragment : Fragment() {
|
|||
findNavController().navigate(
|
||||
HomeFragmentDirections.actionNavigationHomeToLibraryFragment(
|
||||
view.id,
|
||||
view.name
|
||||
view.name,
|
||||
view.type
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import dev.jdtech.jellyfin.viewmodels.LibraryViewModel
|
|||
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
||||
import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding
|
||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
@ -41,6 +42,7 @@ class LibraryFragment : Fragment() {
|
|||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.itemsRecyclerView.visibility = View.GONE
|
||||
} else {
|
||||
|
@ -50,7 +52,7 @@ class LibraryFragment : Fragment() {
|
|||
})
|
||||
|
||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||
viewModel.loadItems(args.libraryId)
|
||||
viewModel.loadItems(args.libraryId, args.libraryType)
|
||||
}
|
||||
|
||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||
|
@ -65,7 +67,7 @@ class LibraryFragment : Fragment() {
|
|||
ViewItemListAdapter(ViewItemListAdapter.OnClickListener { item ->
|
||||
navigateToMediaInfoFragment(item)
|
||||
})
|
||||
viewModel.loadItems(args.libraryId)
|
||||
viewModel.loadItems(args.libraryId, args.libraryType)
|
||||
}
|
||||
|
||||
private fun navigateToMediaInfoFragment(item: BaseItemDto) {
|
||||
|
|
|
@ -11,6 +11,7 @@ import dev.jdtech.jellyfin.R
|
|||
import dev.jdtech.jellyfin.adapters.CollectionListAdapter
|
||||
import dev.jdtech.jellyfin.databinding.FragmentMediaBinding
|
||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import dev.jdtech.jellyfin.viewmodels.MediaViewModel
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
|
||||
|
@ -66,6 +67,7 @@ class MediaFragment : Fragment() {
|
|||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.viewsRecyclerView.visibility = View.GONE
|
||||
} else {
|
||||
|
@ -89,7 +91,8 @@ class MediaFragment : Fragment() {
|
|||
findNavController().navigate(
|
||||
MediaFragmentDirections.actionNavigationMediaToLibraryFragment(
|
||||
library.id,
|
||||
library.name
|
||||
library.name,
|
||||
library.collectionType,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import dev.jdtech.jellyfin.databinding.FragmentMediaInfoBinding
|
|||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment
|
||||
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
|
||||
|
||||
|
@ -48,6 +49,7 @@ class MediaInfoFragment : Fragment() {
|
|||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.mediaInfoScrollview.visibility = View.GONE
|
||||
} else {
|
||||
|
@ -61,7 +63,10 @@ class MediaInfoFragment : Fragment() {
|
|||
}
|
||||
|
||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog")
|
||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(
|
||||
parentFragmentManager,
|
||||
"errordialog"
|
||||
)
|
||||
}
|
||||
|
||||
viewModel.item.observe(viewLifecycleOwner, { item ->
|
||||
|
@ -89,8 +94,7 @@ class MediaInfoFragment : Fragment() {
|
|||
viewModel.navigateToPlayer.observe(viewLifecycleOwner, { playerItems ->
|
||||
if (playerItems != null) {
|
||||
navigateToPlayerActivity(
|
||||
playerItems,
|
||||
viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000)
|
||||
playerItems
|
||||
)
|
||||
viewModel.doneNavigatingToPlayer()
|
||||
binding.playButton.setImageDrawable(
|
||||
|
@ -137,7 +141,9 @@ class MediaInfoFragment : Fragment() {
|
|||
})
|
||||
|
||||
binding.playerItemsErrorDetails.setOnClickListener {
|
||||
ErrorDialogFragment(viewModel.playerItemsError.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog")
|
||||
ErrorDialogFragment(
|
||||
viewModel.playerItemsError.value ?: getString(R.string.unknown_error)
|
||||
).show(parentFragmentManager, "errordialog")
|
||||
}
|
||||
|
||||
binding.trailerButton.setOnClickListener {
|
||||
|
@ -162,33 +168,18 @@ class MediaInfoFragment : Fragment() {
|
|||
binding.playButton.setImageResource(android.R.color.transparent)
|
||||
binding.progressCircular.visibility = View.VISIBLE
|
||||
if (args.itemType == "Movie") {
|
||||
if (!viewModel.mediaSources.value.isNullOrEmpty()) {
|
||||
if (viewModel.mediaSources.value!!.size > 1) {
|
||||
if (viewModel.item.value?.mediaSources != null) {
|
||||
if (viewModel.item.value?.mediaSources?.size!! > 1) {
|
||||
VideoVersionDialogFragment(viewModel).show(
|
||||
parentFragmentManager,
|
||||
"videoversiondialog"
|
||||
)
|
||||
} else {
|
||||
navigateToPlayerActivity(
|
||||
arrayOf(
|
||||
PlayerItem(
|
||||
args.itemId,
|
||||
viewModel.mediaSources.value!![0].id!!
|
||||
)
|
||||
),
|
||||
viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000),
|
||||
)
|
||||
binding.playButton.setImageDrawable(
|
||||
ContextCompat.getDrawable(
|
||||
requireActivity(),
|
||||
R.drawable.ic_play
|
||||
)
|
||||
)
|
||||
binding.progressCircular.visibility = View.INVISIBLE
|
||||
viewModel.preparePlayerItems()
|
||||
}
|
||||
}
|
||||
} else if (args.itemType == "Series") {
|
||||
viewModel.preparePlayer()
|
||||
viewModel.preparePlayerItems()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -230,12 +221,10 @@ class MediaInfoFragment : Fragment() {
|
|||
|
||||
private fun navigateToPlayerActivity(
|
||||
playerItems: Array<PlayerItem>,
|
||||
playbackPosition: Long,
|
||||
) {
|
||||
findNavController().navigate(
|
||||
MediaInfoFragmentDirections.actionMediaInfoFragmentToPlayerActivity(
|
||||
playerItems,
|
||||
playbackPosition
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter
|
|||
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
||||
import dev.jdtech.jellyfin.databinding.FragmentSearchResultBinding
|
||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import dev.jdtech.jellyfin.viewmodels.SearchResultViewModel
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
|
||||
|
@ -47,6 +48,7 @@ class SearchResultFragment : Fragment() {
|
|||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.searchResultsRecyclerView.visibility = View.GONE
|
||||
} else {
|
||||
|
|
|
@ -13,6 +13,7 @@ import dev.jdtech.jellyfin.R
|
|||
import dev.jdtech.jellyfin.adapters.EpisodeListAdapter
|
||||
import dev.jdtech.jellyfin.databinding.FragmentSeasonBinding
|
||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import dev.jdtech.jellyfin.viewmodels.SeasonViewModel
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
|
||||
|
@ -39,6 +40,7 @@ class SeasonFragment : Fragment() {
|
|||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.episodesRecyclerView.visibility = View.GONE
|
||||
} else {
|
||||
|
|
|
@ -7,5 +7,6 @@ import java.util.*
|
|||
@Parcelize
|
||||
data class PlayerItem(
|
||||
val itemId: UUID,
|
||||
val mediaSourceId: String
|
||||
val mediaSourceId: String,
|
||||
val playbackPosition: Long
|
||||
) : Parcelable
|
|
@ -6,5 +6,6 @@ import java.util.*
|
|||
data class View(
|
||||
val id: UUID,
|
||||
val name: String?,
|
||||
var items: List<BaseItemDto>? = null
|
||||
var items: List<BaseItemDto>? = null,
|
||||
val type: String?
|
||||
)
|
|
@ -10,7 +10,11 @@ interface JellyfinRepository {
|
|||
|
||||
suspend fun getItem(itemId: UUID): BaseItemDto
|
||||
|
||||
suspend fun getItems(parentId: UUID? = null): List<BaseItemDto>
|
||||
suspend fun getItems(
|
||||
parentId: UUID? = null,
|
||||
includeTypes: List<String>? = null,
|
||||
recursive: Boolean = false
|
||||
): List<BaseItemDto>
|
||||
|
||||
suspend fun getFavoriteItems(): List<BaseItemDto>
|
||||
|
||||
|
@ -28,7 +32,7 @@ interface JellyfinRepository {
|
|||
seriesId: UUID,
|
||||
seasonId: UUID,
|
||||
fields: List<ItemFields>? = null,
|
||||
startIndex: Int? = null
|
||||
startItemId: UUID? = null
|
||||
): List<BaseItemDto>
|
||||
|
||||
suspend fun getMediaSources(itemId: UUID): List<MediaSourceInfo>
|
||||
|
@ -50,4 +54,6 @@ interface JellyfinRepository {
|
|||
suspend fun markAsPlayed(itemId: UUID)
|
||||
|
||||
suspend fun markAsUnplayed(itemId: UUID)
|
||||
|
||||
suspend fun getIntros(itemId: UUID): List<BaseItemDto>
|
||||
}
|
|
@ -25,12 +25,18 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
return item
|
||||
}
|
||||
|
||||
override suspend fun getItems(parentId: UUID?): List<BaseItemDto> {
|
||||
override suspend fun getItems(
|
||||
parentId: UUID?,
|
||||
includeTypes: List<String>?,
|
||||
recursive: Boolean
|
||||
): List<BaseItemDto> {
|
||||
val items: List<BaseItemDto>
|
||||
withContext(Dispatchers.IO) {
|
||||
items = jellyfinApi.itemsApi.getItems(
|
||||
jellyfinApi.userId!!,
|
||||
parentId = parentId
|
||||
parentId = parentId,
|
||||
includeItemTypes = includeTypes,
|
||||
recursive = recursive
|
||||
).content.items ?: listOf()
|
||||
}
|
||||
return items
|
||||
|
@ -109,7 +115,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
seriesId: UUID,
|
||||
seasonId: UUID,
|
||||
fields: List<ItemFields>?,
|
||||
startIndex: Int?
|
||||
startItemId: UUID?
|
||||
): List<BaseItemDto> {
|
||||
val episodes: List<BaseItemDto>
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -118,7 +124,7 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
jellyfinApi.userId!!,
|
||||
seasonId = seasonId,
|
||||
fields = fields,
|
||||
startIndex = startIndex
|
||||
startItemId = startItemId
|
||||
).content.items ?: listOf()
|
||||
}
|
||||
return episodes
|
||||
|
@ -260,4 +266,14 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
|
|||
jellyfinApi.playStateApi.markUnplayedItem(jellyfinApi.userId!!, itemId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getIntros(itemId: UUID): List<BaseItemDto> {
|
||||
val intros: List<BaseItemDto>
|
||||
withContext(Dispatchers.IO) {
|
||||
intros =
|
||||
jellyfinApi.userLibraryApi.getIntros(jellyfinApi.userId!!, itemId).content.items
|
||||
?: listOf()
|
||||
}
|
||||
return intros
|
||||
}
|
||||
}
|
|
@ -1,11 +1,23 @@
|
|||
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.View
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import timber.log.Timber
|
||||
|
||||
fun BaseItemDto.toView(): View {
|
||||
return View(
|
||||
id = id,
|
||||
name = name
|
||||
name = name,
|
||||
type = collectionType
|
||||
)
|
||||
}
|
||||
|
||||
fun Fragment.checkIfLoginRequired(error: String) {
|
||||
if (error.contains("401")) {
|
||||
Timber.d("Login required!")
|
||||
findNavController().navigate(MainNavigationDirections.actionGlobalLoginFragment())
|
||||
}
|
||||
}
|
|
@ -10,6 +10,8 @@ import dev.jdtech.jellyfin.models.PlayerItem
|
|||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.LocationType
|
||||
import timber.log.Timber
|
||||
import java.text.DateFormat
|
||||
import java.time.ZoneOffset
|
||||
|
@ -61,28 +63,53 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun preparePlayer() {
|
||||
fun preparePlayerItems() {
|
||||
_playerItemsError.value = null
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
createPlayerItems(_item.value!!)
|
||||
_navigateToPlayer.value = true
|
||||
} catch (e: Exception) {
|
||||
_playerItemsError.value = e.message
|
||||
_playerItemsError.value = e.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createPlayerItems(startEpisode: BaseItemDto) {
|
||||
playerItems.clear()
|
||||
|
||||
val playbackPosition = startEpisode.userData?.playbackPositionTicks?.div(10000) ?: 0
|
||||
// Intros
|
||||
var introsCount = 0
|
||||
|
||||
if (playbackPosition <= 0) {
|
||||
val intros = jellyfinRepository.getIntros(startEpisode.id)
|
||||
for (intro in intros) {
|
||||
if (intro.mediaSources.isNullOrEmpty()) continue
|
||||
playerItems.add(PlayerItem(intro.id, intro.mediaSources?.get(0)?.id!!, 0))
|
||||
introsCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
val episodes = jellyfinRepository.getEpisodes(
|
||||
startEpisode.seriesId!!,
|
||||
startEpisode.seasonId!!,
|
||||
startIndex = startEpisode.indexNumber?.minus(1)
|
||||
startItemId = startEpisode.id,
|
||||
fields = listOf(ItemFields.MEDIA_SOURCES)
|
||||
)
|
||||
for (episode in episodes) {
|
||||
val mediaSources = jellyfinRepository.getMediaSources(episode.id)
|
||||
playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!))
|
||||
if (episode.mediaSources.isNullOrEmpty()) continue
|
||||
if (episode.locationType == LocationType.VIRTUAL) continue
|
||||
playerItems.add(
|
||||
PlayerItem(
|
||||
episode.id,
|
||||
episode.mediaSources?.get(0)?.id!!,
|
||||
playbackPosition
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (playerItems.isEmpty() || playerItems.count() == introsCount) throw Exception("No playable items found")
|
||||
}
|
||||
|
||||
fun markAsPlayed(itemId: UUID) {
|
||||
|
|
|
@ -78,7 +78,7 @@ constructor(
|
|||
_favoriteSections.value = tempFavoriteSections
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.message
|
||||
_error.value = e.toString()
|
||||
}
|
||||
_finishedLoading.value = true
|
||||
}
|
||||
|
|
|
@ -12,7 +12,9 @@ import dev.jdtech.jellyfin.models.HomeSection
|
|||
import dev.jdtech.jellyfin.models.View
|
||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||
import dev.jdtech.jellyfin.utils.toView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
@ -50,45 +52,59 @@ constructor(
|
|||
_finishedLoading.value = false
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
|
||||
|
||||
jellyfinRepository.postCapabilities()
|
||||
val views: MutableList<View> = mutableListOf()
|
||||
val userViews = jellyfinRepository.getUserViews()
|
||||
for (view in userViews) {
|
||||
Timber.d("Collection type: ${view.collectionType}")
|
||||
if (view.collectionType == "homevideos" ||
|
||||
view.collectionType == "music" ||
|
||||
view.collectionType == "playlists" ||
|
||||
view.collectionType == "books"
|
||||
) continue
|
||||
val latestItems = jellyfinRepository.getLatestMedia(view.id)
|
||||
if (latestItems.isEmpty()) continue
|
||||
val v = view.toView()
|
||||
v.items = latestItems
|
||||
views.add(v)
|
||||
}
|
||||
|
||||
val items = mutableListOf<HomeItem>()
|
||||
|
||||
val resumeItems = jellyfinRepository.getResumeItems()
|
||||
val resumeSection =
|
||||
HomeSection(UUID.randomUUID(), continueWatchingString, resumeItems)
|
||||
withContext(Dispatchers.Default) {
|
||||
|
||||
if (!resumeItems.isNullOrEmpty()) {
|
||||
items.add(HomeItem.Section(resumeSection))
|
||||
val resumeItems = jellyfinRepository.getResumeItems()
|
||||
val resumeSection =
|
||||
HomeSection(UUID.randomUUID(), continueWatchingString, resumeItems)
|
||||
|
||||
if (!resumeItems.isNullOrEmpty()) {
|
||||
items.add(HomeItem.Section(resumeSection))
|
||||
}
|
||||
|
||||
val nextUpItems = jellyfinRepository.getNextUp()
|
||||
val nextUpSection = HomeSection(UUID.randomUUID(), nextUpString, nextUpItems)
|
||||
|
||||
if (!nextUpItems.isNullOrEmpty()) {
|
||||
items.add(HomeItem.Section(nextUpSection))
|
||||
}
|
||||
}
|
||||
|
||||
val nextUpItems = jellyfinRepository.getNextUp()
|
||||
val nextUpSection = HomeSection(UUID.randomUUID(), nextUpString, nextUpItems)
|
||||
_views.value = items
|
||||
|
||||
if (!nextUpItems.isNullOrEmpty()) {
|
||||
items.add(HomeItem.Section(nextUpSection))
|
||||
val views: MutableList<View> = mutableListOf()
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
val userViews = jellyfinRepository.getUserViews()
|
||||
|
||||
for (view in userViews) {
|
||||
Timber.d("Collection type: ${view.collectionType}")
|
||||
if (view.collectionType == "homevideos" ||
|
||||
view.collectionType == "music" ||
|
||||
view.collectionType == "playlists" ||
|
||||
view.collectionType == "books" ||
|
||||
view.collectionType == "livetv"
|
||||
) continue
|
||||
val latestItems = jellyfinRepository.getLatestMedia(view.id)
|
||||
if (latestItems.isEmpty()) continue
|
||||
val v = view.toView()
|
||||
v.items = latestItems
|
||||
views.add(v)
|
||||
}
|
||||
}
|
||||
|
||||
_views.value = items + views.map { HomeItem.ViewItem(it) }
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.message
|
||||
_error.value = e.toString()
|
||||
}
|
||||
_finishedLoading.value = true
|
||||
}
|
||||
|
|
|
@ -23,15 +23,25 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
|||
private val _error = MutableLiveData<String>()
|
||||
val error: LiveData<String> = _error
|
||||
|
||||
fun loadItems(parentId: UUID) {
|
||||
fun loadItems(parentId: UUID, libraryType: String?) {
|
||||
_error.value = null
|
||||
_finishedLoading.value = false
|
||||
Timber.d("$libraryType")
|
||||
val itemType = when (libraryType) {
|
||||
"movies" -> "Movie"
|
||||
"tvshows" -> "Series"
|
||||
else -> null
|
||||
}
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
_items.value = jellyfinRepository.getItems(parentId)
|
||||
_items.value = jellyfinRepository.getItems(
|
||||
parentId,
|
||||
includeTypes = if (itemType != null) listOf(itemType) else null,
|
||||
recursive = true
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.message
|
||||
_error.value = e.toString()
|
||||
}
|
||||
_finishedLoading.value = true
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ constructor(
|
|||
_navigateToMain.value = true
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.message
|
||||
_error.value = e.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,8 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemPerson
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.LocationType
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
@ -52,9 +53,6 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
|||
private val _seasons = MutableLiveData<List<BaseItemDto>>()
|
||||
val seasons: LiveData<List<BaseItemDto>> = _seasons
|
||||
|
||||
private val _mediaSources = MutableLiveData<List<MediaSourceInfo>>()
|
||||
val mediaSources: LiveData<List<MediaSourceInfo>> = _mediaSources
|
||||
|
||||
private val _navigateToPlayer = MutableLiveData<Array<PlayerItem>>()
|
||||
val navigateToPlayer: LiveData<Array<PlayerItem>> = _navigateToPlayer
|
||||
|
||||
|
@ -91,12 +89,9 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
|||
_nextUp.value = getNextUp(itemId)
|
||||
_seasons.value = jellyfinRepository.getSeasons(itemId)
|
||||
}
|
||||
if (itemType == "Movie") {
|
||||
_mediaSources.value = jellyfinRepository.getMediaSources(itemId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.message
|
||||
_error.value = e.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -183,11 +178,11 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
fun preparePlayer() {
|
||||
fun preparePlayerItems(mediaSourceIndex: Int? = null) {
|
||||
_playerItemsError.value = null
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
createPlayerItems(_item.value!!)
|
||||
createPlayerItems(_item.value!!, mediaSourceIndex)
|
||||
_navigateToPlayer.value = playerItems.toTypedArray()
|
||||
} catch (e: Exception) {
|
||||
_playerItemsError.value = e.message
|
||||
|
@ -195,28 +190,78 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun createPlayerItems(series: BaseItemDto) {
|
||||
if (nextUp.value != null) {
|
||||
val startEpisode = nextUp.value!!
|
||||
val episodes = jellyfinRepository.getEpisodes(startEpisode.seriesId!!, startEpisode.seasonId!!, startIndex = startEpisode.indexNumber?.minus(1))
|
||||
for (episode in episodes) {
|
||||
val mediaSources = jellyfinRepository.getMediaSources(episode.id)
|
||||
playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!))
|
||||
private suspend fun createPlayerItems(series: BaseItemDto, mediaSourceIndex: Int? = null) {
|
||||
playerItems.clear()
|
||||
|
||||
val playbackPosition = item.value?.userData?.playbackPositionTicks?.div(10000) ?: 0
|
||||
|
||||
// Intros
|
||||
var introsCount = 0
|
||||
|
||||
if (playbackPosition <= 0) {
|
||||
val intros = jellyfinRepository.getIntros(series.id)
|
||||
for (intro in intros) {
|
||||
if (intro.mediaSources.isNullOrEmpty()) continue
|
||||
playerItems.add(PlayerItem(intro.id, intro.mediaSources?.get(0)?.id!!, 0))
|
||||
introsCount += 1
|
||||
}
|
||||
} else {
|
||||
for (season in seasons.value!!) {
|
||||
if (season.indexNumber == 0) continue
|
||||
val episodes = jellyfinRepository.getEpisodes(series.id, season.id)
|
||||
for (episode in episodes) {
|
||||
val mediaSources = jellyfinRepository.getMediaSources(episode.id)
|
||||
playerItems.add(PlayerItem(episode.id, mediaSources[0].id!!))
|
||||
}
|
||||
|
||||
when (series.type) {
|
||||
"Movie" -> {
|
||||
playerItems.add(
|
||||
PlayerItem(
|
||||
series.id,
|
||||
series.mediaSources?.get(mediaSourceIndex ?: 0)?.id!!,
|
||||
playbackPosition
|
||||
)
|
||||
)
|
||||
}
|
||||
"Series" -> {
|
||||
if (nextUp.value != null) {
|
||||
val startEpisode = nextUp.value!!
|
||||
val episodes = jellyfinRepository.getEpisodes(
|
||||
startEpisode.seriesId!!,
|
||||
startEpisode.seasonId!!,
|
||||
startItemId = startEpisode.id,
|
||||
fields = listOf(ItemFields.MEDIA_SOURCES)
|
||||
)
|
||||
for (episode in episodes) {
|
||||
if (episode.mediaSources.isNullOrEmpty()) continue
|
||||
if (episode.locationType == LocationType.VIRTUAL) continue
|
||||
playerItems.add(
|
||||
PlayerItem(
|
||||
episode.id,
|
||||
episode.mediaSources?.get(0)?.id!!,
|
||||
0
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
for (season in seasons.value!!) {
|
||||
if (season.indexNumber == 0) continue
|
||||
val episodes = jellyfinRepository.getEpisodes(
|
||||
series.id,
|
||||
season.id,
|
||||
fields = listOf(ItemFields.MEDIA_SOURCES)
|
||||
)
|
||||
for (episode in episodes) {
|
||||
if (episode.mediaSources.isNullOrEmpty()) continue
|
||||
if (episode.locationType == LocationType.VIRTUAL) continue
|
||||
playerItems.add(
|
||||
PlayerItem(
|
||||
episode.id,
|
||||
episode.mediaSources?.get(0)?.id!!,
|
||||
0
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateToPlayer(mediaSource: MediaSourceInfo) {
|
||||
_navigateToPlayer.value = arrayOf(PlayerItem(item.value!!.id, mediaSource.id!!))
|
||||
if (playerItems.isEmpty() || playerItems.count() == introsCount) throw Exception("No playable items found")
|
||||
}
|
||||
|
||||
fun doneNavigatingToPlayer() {
|
||||
|
|
|
@ -44,7 +44,7 @@ constructor(
|
|||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.message
|
||||
_error.value = e.toString()
|
||||
}
|
||||
_finishedLoading.value = true
|
||||
}
|
||||
|
|
|
@ -39,8 +39,7 @@ constructor(
|
|||
private val sp = PreferenceManager.getDefaultSharedPreferences(application)
|
||||
|
||||
fun initializePlayer(
|
||||
items: Array<PlayerItem>,
|
||||
playbackPosition: Long
|
||||
items: Array<PlayerItem>
|
||||
) {
|
||||
|
||||
val renderersFactory =
|
||||
|
@ -61,18 +60,22 @@ constructor(
|
|||
viewModelScope.launch {
|
||||
val mediaItems: MutableList<MediaItem> = mutableListOf()
|
||||
|
||||
for (item in items) {
|
||||
val streamUrl = jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId)
|
||||
Timber.d("Stream url: $streamUrl")
|
||||
val mediaItem =
|
||||
MediaItem.Builder()
|
||||
.setMediaId(item.itemId.toString())
|
||||
.setUri(streamUrl)
|
||||
.build()
|
||||
mediaItems.add(mediaItem)
|
||||
try {
|
||||
for (item in items) {
|
||||
val streamUrl = jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId)
|
||||
Timber.d("Stream url: $streamUrl")
|
||||
val mediaItem =
|
||||
MediaItem.Builder()
|
||||
.setMediaId(item.itemId.toString())
|
||||
.setUri(streamUrl)
|
||||
.build()
|
||||
mediaItems.add(mediaItem)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
|
||||
player.setMediaItems(mediaItems, currentWindow, playbackPosition)
|
||||
player.setMediaItems(mediaItems, currentWindow, items[0].playbackPosition)
|
||||
player.playWhenReady = playWhenReady
|
||||
player.prepare()
|
||||
_player.value = player
|
||||
|
@ -84,10 +87,14 @@ constructor(
|
|||
private fun releasePlayer() {
|
||||
_player.value?.let { player ->
|
||||
runBlocking {
|
||||
jellyfinRepository.postPlaybackStop(
|
||||
UUID.fromString(player.currentMediaItem?.mediaId),
|
||||
player.currentPosition.times(10000)
|
||||
)
|
||||
try {
|
||||
jellyfinRepository.postPlaybackStop(
|
||||
UUID.fromString(player.currentMediaItem?.mediaId),
|
||||
player.currentPosition.times(10000)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,11 +114,15 @@ constructor(
|
|||
override fun run() {
|
||||
viewModelScope.launch {
|
||||
if (player.currentMediaItem != null) {
|
||||
jellyfinRepository.postPlaybackProgress(
|
||||
UUID.fromString(player.currentMediaItem!!.mediaId),
|
||||
player.currentPosition.times(10000),
|
||||
!player.isPlaying
|
||||
)
|
||||
try {
|
||||
jellyfinRepository.postPlaybackProgress(
|
||||
UUID.fromString(player.currentMediaItem!!.mediaId),
|
||||
player.currentPosition.times(10000),
|
||||
!player.isPlaying
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.postDelayed(this, 2000)
|
||||
|
@ -123,7 +134,11 @@ constructor(
|
|||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
Timber.d("Playing MediaItem: ${mediaItem?.mediaId}")
|
||||
viewModelScope.launch {
|
||||
jellyfinRepository.postPlaybackStart(UUID.fromString(mediaItem?.mediaId))
|
||||
try {
|
||||
jellyfinRepository.postPlaybackStart(UUID.fromString(mediaItem?.mediaId))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ constructor(
|
|||
_sections.value = tempSections
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.message
|
||||
_error.value = e.toString()
|
||||
}
|
||||
_finishedLoading.value = true
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
|||
_episodes.value = getEpisodes(seriesId, seasonId)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.message
|
||||
_error.value = e.toString()
|
||||
}
|
||||
_finishedLoading.value = true
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment_activity_main"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/nav_view"
|
||||
|
@ -32,7 +32,7 @@
|
|||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/main_toolbar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/windowBackground"
|
||||
app:layout_constraintBottom_toTopOf="@id/nav_host_fragment_activity_main"
|
||||
|
@ -47,6 +47,5 @@
|
|||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
android:layout_height="0dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
app:collectionImage="@{collection}"
|
||||
app:baseItemImage="@{collection}"
|
||||
app:layout_constraintDimensionRatio="H,16:9"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
|
||||
<data>
|
||||
|
||||
<import type="android.view.View" />
|
||||
|
||||
<import type="org.jellyfin.sdk.model.api.LocationType" />
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="dev.jdtech.jellyfin.viewmodels.EpisodeBottomSheetViewModel" />
|
||||
|
@ -33,11 +37,32 @@
|
|||
android:layout_height="85dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:itemPrimaryImage="@{viewModel.item}"
|
||||
app:baseItemImage="@{viewModel.item}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/holder"
|
||||
app:shapeAppearance="@style/roundedImageView" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/missing_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/circle_background"
|
||||
android:backgroundTint="@color/red"
|
||||
android:visibility="@{viewModel.item.locationType == LocationType.VIRTUAL ? View.VISIBLE : View.GONE}"
|
||||
app:layout_constraintEnd_toEndOf="@id/episode_image"
|
||||
app:layout_constraintTop_toTopOf="@id/episode_image">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="M"
|
||||
android:textColor="@color/white"
|
||||
tools:ignore="HardcodedText" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
<import type="android.view.View" />
|
||||
|
||||
<import type="org.jellyfin.sdk.model.api.LocationType" />
|
||||
|
||||
<variable
|
||||
name="episode"
|
||||
type="org.jellyfin.sdk.model.api.BaseItemDto" />
|
||||
|
@ -24,7 +26,7 @@
|
|||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:episodeImage="@{episode}"
|
||||
app:baseItemImage="@{episode}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
@ -44,6 +46,27 @@
|
|||
app:layout_constraintEnd_toEndOf="@id/episode_image"
|
||||
app:layout_constraintTop_toTopOf="@id/episode_image" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/missing_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/circle_background"
|
||||
android:backgroundTint="@color/red"
|
||||
android:visibility="@{episode.locationType == LocationType.VIRTUAL ? View.VISIBLE : View.GONE}"
|
||||
app:layout_constraintEnd_toStartOf="@id/played_icon"
|
||||
app:layout_constraintTop_toTopOf="@id/episode_image">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="M"
|
||||
android:textColor="@color/white"
|
||||
tools:ignore="HardcodedText" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -74,8 +97,8 @@
|
|||
android:id="@+id/episode_desc"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:text="@{episode.overview}"
|
||||
android:scrollbars="none"
|
||||
android:text="@{episode.overview}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
@ -16,18 +16,18 @@
|
|||
android:layout_height="match_parent"
|
||||
tools:context=".fragments.HomeFragment">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/loading_indicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:trackCornerRadius="10dp" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<include android:id="@+id/error_layout" layout="@layout/error_panel" />
|
||||
<include
|
||||
android:id="@+id/error_layout"
|
||||
layout="@layout/error_panel" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/views_recycler_view"
|
||||
|
|
|
@ -358,7 +358,7 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:adjustViewBounds="true"
|
||||
app:itemPrimaryImage="@{viewModel.nextUp}"
|
||||
app:baseItemImage="@{viewModel.nextUp}"
|
||||
app:layout_constraintDimensionRatio="H,16:9"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:episodeImage="@{episode}"
|
||||
app:baseItemImage="@{episode}"
|
||||
app:layout_constraintDimensionRatio="H,16:9"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:layoutAnimation="@anim/overview_media_animation"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="12dp"
|
||||
app:homeEpisodes="@{section.items}"
|
||||
|
|
|
@ -42,7 +42,6 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:clipToPadding="false"
|
||||
android:layoutAnimation="@anim/overview_media_animation"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="12dp"
|
||||
app:items="@{view.items}"
|
||||
|
|
|
@ -84,6 +84,10 @@
|
|||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
<argument
|
||||
android:name="libraryType"
|
||||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/mediaInfoFragment"
|
||||
|
@ -156,9 +160,6 @@
|
|||
<argument
|
||||
android:name="items"
|
||||
app:argType="dev.jdtech.jellyfin.models.PlayerItem[]" />
|
||||
<argument
|
||||
android:name="playbackPosition"
|
||||
app:argType="long" />
|
||||
</activity>
|
||||
<fragment
|
||||
android:id="@+id/favoriteFragment"
|
||||
|
@ -239,5 +240,8 @@
|
|||
</fragment>
|
||||
|
||||
<include app:graph="@navigation/aboutlibs_navigation" />
|
||||
<action
|
||||
android:id="@+id/action_global_loginFragment"
|
||||
app:destination="@id/loginFragment" />
|
||||
|
||||
</navigation>
|
|
@ -51,7 +51,7 @@
|
|||
<string name="theme">Theme</string>
|
||||
<string name="error_preparing_player_items">Error preparing player items.</string>
|
||||
<string name="view_details">View details</string>
|
||||
<string name="view_details_underlined"><u>@string/view_details</u></string>
|
||||
<string name="view_details_underlined"><u>View details</u></string>
|
||||
<string name="about">About</string>
|
||||
<string name="privacy_policy">Privacy policy</string>
|
||||
<string name="app_info">App info</string>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = "1.5.21"
|
||||
ext.kotlin_version = "1.5.30"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
|
Loading…
Reference in a new issue