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