Add refresh (#59)
* Add refresh to home fragment * Remove forgotten code * Remove unnecessary condition and fix HomeSection equality check * Make HomeFragment fragment view model again * Add order dependent check for home items equality * Fix loading state overwriting error state on home refresh * Revert to older swiperefreshlayout version * Fixing error and loading state Co-authored-by: Jarne Demeulemeester <32322857+jarnedemeulemeester@users.noreply.github.com>
This commit is contained in:
parent
98cb038c24
commit
d7a47b0a3e
13 changed files with 234 additions and 152 deletions
|
@ -59,6 +59,8 @@ dependencies {
|
||||||
implementation("androidx.core:core-ktx:1.7.0")
|
implementation("androidx.core:core-ktx:1.7.0")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||||
implementation("androidx.appcompat:appcompat:1.3.1")
|
implementation("androidx.appcompat:appcompat:1.3.1")
|
||||||
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
|
|
||||||
|
|
||||||
// Material
|
// Material
|
||||||
implementation("com.google.android.material:material:1.4.0")
|
implementation("com.google.android.material:material:1.4.0")
|
||||||
|
|
|
@ -10,7 +10,7 @@ import dev.jdtech.jellyfin.databinding.NextUpSectionBinding
|
||||||
import dev.jdtech.jellyfin.databinding.ViewItemBinding
|
import dev.jdtech.jellyfin.databinding.ViewItemBinding
|
||||||
import dev.jdtech.jellyfin.models.HomeSection
|
import dev.jdtech.jellyfin.models.HomeSection
|
||||||
import dev.jdtech.jellyfin.models.View
|
import dev.jdtech.jellyfin.models.View
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
private const val ITEM_VIEW_TYPE_NEXT_UP = 0
|
private const val ITEM_VIEW_TYPE_NEXT_UP = 0
|
||||||
private const val ITEM_VIEW_TYPE_VIEW = 1
|
private const val ITEM_VIEW_TYPE_VIEW = 1
|
||||||
|
@ -51,7 +51,8 @@ class ViewListAdapter(
|
||||||
|
|
||||||
companion object DiffCallback : DiffUtil.ItemCallback<HomeItem>() {
|
companion object DiffCallback : DiffUtil.ItemCallback<HomeItem>() {
|
||||||
override fun areItemsTheSame(oldItem: HomeItem, newItem: HomeItem): Boolean {
|
override fun areItemsTheSame(oldItem: HomeItem, newItem: HomeItem): Boolean {
|
||||||
return oldItem.id == newItem.id
|
return oldItem.ids.size == newItem.ids.size
|
||||||
|
&& oldItem.ids.mapIndexed { i, old -> old == newItem.ids[i] }.all { it }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: HomeItem, newItem: HomeItem): Boolean {
|
override fun areContentsTheSame(oldItem: HomeItem, newItem: HomeItem): Boolean {
|
||||||
|
@ -106,12 +107,12 @@ class ViewListAdapter(
|
||||||
|
|
||||||
sealed class HomeItem {
|
sealed class HomeItem {
|
||||||
data class Section(val homeSection: HomeSection) : HomeItem() {
|
data class Section(val homeSection: HomeSection) : HomeItem() {
|
||||||
override val id = homeSection.id
|
override val ids = homeSection.items.map { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ViewItem(val view: View) : HomeItem() {
|
data class ViewItem(val view: View) : HomeItem() {
|
||||||
override val id = view.id
|
override val ids = view.items?.map { it.id }.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract val id: UUID
|
abstract val ids: List<UUID>
|
||||||
}
|
}
|
|
@ -7,8 +7,12 @@ import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import android.widget.Toast.LENGTH_LONG
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.R
|
import dev.jdtech.jellyfin.R
|
||||||
|
@ -17,8 +21,15 @@ 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.models.ContentType
|
||||||
|
import dev.jdtech.jellyfin.models.ContentType.EPISODE
|
||||||
|
import dev.jdtech.jellyfin.models.ContentType.MOVIE
|
||||||
|
import dev.jdtech.jellyfin.models.ContentType.TVSHOW
|
||||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||||
|
import dev.jdtech.jellyfin.utils.contentType
|
||||||
import dev.jdtech.jellyfin.viewmodels.HomeViewModel
|
import dev.jdtech.jellyfin.viewmodels.HomeViewModel
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.HomeViewModel.Loading
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.HomeViewModel.LoadingError
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
@ -57,49 +68,79 @@ class HomeFragment : Fragment() {
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
binding.viewsRecyclerView.adapter = ViewListAdapter(ViewListAdapter.OnClickListener {
|
|
||||||
navigateToLibraryFragment(it)
|
|
||||||
}, ViewItemListAdapter.OnClickListener {
|
|
||||||
navigateToMediaInfoFragment(it)
|
|
||||||
}, HomeEpisodeListAdapter.OnClickListener { item ->
|
|
||||||
when (item.type) {
|
|
||||||
"Episode" -> {
|
|
||||||
navigateToEpisodeBottomSheetFragment(item)
|
|
||||||
}
|
|
||||||
"Movie" -> {
|
|
||||||
navigateToMediaInfoFragment(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
setupView()
|
||||||
|
bindState()
|
||||||
|
|
||||||
viewModel.finishedLoading.observe(viewLifecycleOwner, {
|
return binding.root
|
||||||
binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE
|
}
|
||||||
})
|
|
||||||
|
|
||||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
override fun onResume() {
|
||||||
if (error != null) {
|
super.onResume()
|
||||||
checkIfLoginRequired(error)
|
|
||||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
|
||||||
binding.viewsRecyclerView.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
|
||||||
binding.viewsRecyclerView.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
viewModel.refreshData()
|
||||||
viewModel.loadData()
|
}
|
||||||
|
|
||||||
|
private fun setupView() {
|
||||||
|
binding.refreshLayout.setOnRefreshListener {
|
||||||
|
viewModel.refreshData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.viewsRecyclerView.adapter = ViewListAdapter(
|
||||||
|
onClickListener = ViewListAdapter.OnClickListener { navigateToLibraryFragment(it) },
|
||||||
|
onItemClickListener = ViewItemListAdapter.OnClickListener {
|
||||||
|
navigateToMediaInfoFragment(it)
|
||||||
|
},
|
||||||
|
onNextUpClickListener = HomeEpisodeListAdapter.OnClickListener { item ->
|
||||||
|
when (item.contentType()) {
|
||||||
|
EPISODE -> navigateToEpisodeBottomSheetFragment(item)
|
||||||
|
MOVIE -> navigateToMediaInfoFragment(item)
|
||||||
|
else -> Toast.makeText(requireContext(), R.string.unknown_error, LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindState() {
|
||||||
|
viewModel.onStateUpdate(lifecycleScope) { state ->
|
||||||
|
when (state) {
|
||||||
|
is Loading -> bindLoading(state)
|
||||||
|
is LoadingError -> bindError(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindError(state: LoadingError) {
|
||||||
|
checkIfLoginRequired(state.message)
|
||||||
|
binding.errorLayout.errorPanel.isVisible = true
|
||||||
|
binding.viewsRecyclerView.isVisible = false
|
||||||
|
binding.loadingIndicator.isVisible = false
|
||||||
|
binding.refreshLayout.isRefreshing = false
|
||||||
|
|
||||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(
|
ErrorDialogFragment(state.message).show(
|
||||||
parentFragmentManager,
|
parentFragmentManager,
|
||||||
"errordialog"
|
"errordialog"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return binding.root
|
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||||
|
viewModel.refreshData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindLoading(state: Loading) {
|
||||||
|
binding.errorLayout.errorPanel.isVisible = false
|
||||||
|
binding.viewsRecyclerView.isVisible = true
|
||||||
|
|
||||||
|
binding.loadingIndicator.visibility = when {
|
||||||
|
state.inProgress && binding.refreshLayout.isRefreshing -> View.GONE
|
||||||
|
state.inProgress -> View.VISIBLE
|
||||||
|
else -> {
|
||||||
|
binding.refreshLayout.isRefreshing = false
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToLibraryFragment(view: dev.jdtech.jellyfin.models.View) {
|
private fun navigateToLibraryFragment(view: dev.jdtech.jellyfin.models.View) {
|
||||||
|
@ -113,12 +154,12 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToMediaInfoFragment(item: BaseItemDto) {
|
private fun navigateToMediaInfoFragment(item: BaseItemDto) {
|
||||||
if (item.type == "Episode") {
|
if (item.contentType() == EPISODE) {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
HomeFragmentDirections.actionNavigationHomeToMediaInfoFragment(
|
HomeFragmentDirections.actionNavigationHomeToMediaInfoFragment(
|
||||||
item.seriesId!!,
|
item.seriesId!!,
|
||||||
item.seriesName,
|
item.seriesName,
|
||||||
"Series"
|
TVSHOW.type
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -126,7 +167,7 @@ class HomeFragment : Fragment() {
|
||||||
HomeFragmentDirections.actionNavigationHomeToMediaInfoFragment(
|
HomeFragmentDirections.actionNavigationHomeToMediaInfoFragment(
|
||||||
item.id,
|
item.id,
|
||||||
item.name,
|
item.name,
|
||||||
item.type ?: "Unknown"
|
item.type ?: ContentType.UNKNOWN.type
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package dev.jdtech.jellyfin.models
|
||||||
|
|
||||||
|
import dev.jdtech.jellyfin.models.CollectionType.Books
|
||||||
|
import dev.jdtech.jellyfin.models.CollectionType.HomeVideos
|
||||||
|
import dev.jdtech.jellyfin.models.CollectionType.LiveTv
|
||||||
|
import dev.jdtech.jellyfin.models.CollectionType.Music
|
||||||
|
import dev.jdtech.jellyfin.models.CollectionType.Playlists
|
||||||
|
|
||||||
|
enum class CollectionType (val type: String) {
|
||||||
|
HomeVideos("homevideos"),
|
||||||
|
Music("music"),
|
||||||
|
Playlists("playlists"),
|
||||||
|
Books("books"),
|
||||||
|
LiveTv("livetv")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unsupportedCollections() = listOf(
|
||||||
|
HomeVideos, Music, Playlists, Books, LiveTv
|
||||||
|
)
|
|
@ -3,5 +3,6 @@ package dev.jdtech.jellyfin.models
|
||||||
enum class ContentType(val type: String) {
|
enum class ContentType(val type: String) {
|
||||||
MOVIE("Movie"),
|
MOVIE("Movie"),
|
||||||
TVSHOW("Series"),
|
TVSHOW("Series"),
|
||||||
|
EPISODE("Episode"),
|
||||||
UNKNOWN("")
|
UNKNOWN("")
|
||||||
}
|
}
|
|
@ -1,10 +1,8 @@
|
||||||
package dev.jdtech.jellyfin.models
|
package dev.jdtech.jellyfin.models
|
||||||
|
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
data class HomeSection(
|
data class HomeSection(
|
||||||
val id: UUID,
|
val name: String,
|
||||||
val name: String?,
|
var items: List<BaseItemDto>
|
||||||
var items: List<BaseItemDto>? = null
|
|
||||||
)
|
)
|
|
@ -1,7 +1,7 @@
|
||||||
package dev.jdtech.jellyfin.models
|
package dev.jdtech.jellyfin.models
|
||||||
|
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
data class View(
|
data class View(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
|
|
|
@ -48,7 +48,7 @@ internal class HomeFragment : BrowseSupportFragment() {
|
||||||
setOnClickListener { navigateToSettingsFragment() }
|
setOnClickListener { navigateToSettingsFragment() }
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.views.observe(viewLifecycleOwner) { homeItems ->
|
viewModel.views().observe(viewLifecycleOwner) { homeItems ->
|
||||||
rowsAdapter.clear()
|
rowsAdapter.clear()
|
||||||
homeItems.map { section -> rowsAdapter.add(section.toListRow()) }
|
homeItems.map { section -> rowsAdapter.add(section.toListRow()) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,13 +16,11 @@ import dev.jdtech.jellyfin.models.DownloadMetadata
|
||||||
import dev.jdtech.jellyfin.models.DownloadRequestItem
|
import dev.jdtech.jellyfin.models.DownloadRequestItem
|
||||||
import dev.jdtech.jellyfin.models.PlayerItem
|
import dev.jdtech.jellyfin.models.PlayerItem
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.UserItemDataDto
|
import org.jellyfin.sdk.model.api.UserItemDataDto
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
fun requestDownload(uri: Uri, downloadRequestItem: DownloadRequestItem, context: Fragment) {
|
fun requestDownload(uri: Uri, downloadRequestItem: DownloadRequestItem, context: Fragment) {
|
||||||
// Storage permission for downloads isn't necessary from Android 10 onwards
|
// Storage permission for downloads isn't necessary from Android 10 onwards
|
||||||
|
@ -209,7 +207,7 @@ fun parseMetadataFile(metadataFile: List<String>) : DownloadMetadata {
|
||||||
|
|
||||||
suspend fun syncPlaybackProgress(jellyfinRepository: JellyfinRepository, context: Context) {
|
suspend fun syncPlaybackProgress(jellyfinRepository: JellyfinRepository, context: Context) {
|
||||||
val items = loadDownloadedEpisodes(context)
|
val items = loadDownloadedEpisodes(context)
|
||||||
items.forEach(){
|
items.forEach{
|
||||||
try {
|
try {
|
||||||
val localPlaybackProgress = it.metadata?.playbackPosition
|
val localPlaybackProgress = it.metadata?.playbackPosition
|
||||||
val localPlayedPercentage = it.metadata?.playedPercentage
|
val localPlayedPercentage = it.metadata?.playedPercentage
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package dev.jdtech.jellyfin.utils
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
|
||||||
|
fun View.toggleVisibility() {
|
||||||
|
isVisible = !isVisible
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ fun BaseItemDto.toView(): View {
|
||||||
fun BaseItemDto.contentType() = when (type) {
|
fun BaseItemDto.contentType() = when (type) {
|
||||||
"Movie" -> ContentType.MOVIE
|
"Movie" -> ContentType.MOVIE
|
||||||
"Series" -> ContentType.TVSHOW
|
"Series" -> ContentType.TVSHOW
|
||||||
|
"Episode" -> ContentType.EPISODE
|
||||||
else -> ContentType.UNKNOWN
|
else -> ContentType.UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,5 +33,6 @@ fun Fragment.checkIfLoginRequired(error: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
inline fun Context.toast(@StringRes text: Int, duration: Int = Toast.LENGTH_SHORT) =
|
inline fun Context.toast(@StringRes text: Int, duration: Int = Toast.LENGTH_SHORT) =
|
||||||
Toast.makeText(this, text, duration).show()
|
Toast.makeText(this, text, duration).show()
|
|
@ -1,6 +1,7 @@
|
||||||
package dev.jdtech.jellyfin.viewmodels
|
package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
@ -8,108 +9,104 @@ import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.R
|
import dev.jdtech.jellyfin.R
|
||||||
import dev.jdtech.jellyfin.adapters.HomeItem
|
import dev.jdtech.jellyfin.adapters.HomeItem
|
||||||
|
import dev.jdtech.jellyfin.adapters.HomeItem.Section
|
||||||
|
import dev.jdtech.jellyfin.adapters.HomeItem.ViewItem
|
||||||
import dev.jdtech.jellyfin.models.HomeSection
|
import dev.jdtech.jellyfin.models.HomeSection
|
||||||
import dev.jdtech.jellyfin.models.View
|
import dev.jdtech.jellyfin.models.unsupportedCollections
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import dev.jdtech.jellyfin.utils.syncPlaybackProgress
|
import dev.jdtech.jellyfin.utils.syncPlaybackProgress
|
||||||
import dev.jdtech.jellyfin.utils.toView
|
import dev.jdtech.jellyfin.utils.toView
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeViewModel
|
class HomeViewModel @Inject internal constructor(
|
||||||
@Inject
|
|
||||||
constructor(
|
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val jellyfinRepository: JellyfinRepository
|
private val repository: JellyfinRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val views = MutableLiveData<List<HomeItem>>()
|
||||||
|
private val state = MutableSharedFlow<State>(
|
||||||
|
replay = 0,
|
||||||
|
extraBufferCapacity = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadData(updateCapabilities = true)
|
||||||
|
}
|
||||||
|
|
||||||
private val continueWatchingString = application.resources.getString(R.string.continue_watching)
|
private val continueWatchingString = application.resources.getString(R.string.continue_watching)
|
||||||
private val nextUpString = application.resources.getString(R.string.next_up)
|
private val nextUpString = application.resources.getString(R.string.next_up)
|
||||||
|
|
||||||
private val _views = MutableLiveData<List<HomeItem>>()
|
fun views(): LiveData<List<HomeItem>> = views
|
||||||
val views: LiveData<List<HomeItem>> = _views
|
|
||||||
|
|
||||||
private val _items = MutableLiveData<List<BaseItemDto>>()
|
fun onStateUpdate(
|
||||||
val items: LiveData<List<BaseItemDto>> = _items
|
scope: LifecycleCoroutineScope,
|
||||||
|
collector: (State) -> Unit
|
||||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
) {
|
||||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
scope.launch { state.collect { collector(it) } }
|
||||||
|
|
||||||
private val _error = MutableLiveData<String>()
|
|
||||||
val error: LiveData<String> = _error
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadData()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadData() {
|
fun refreshData() = loadData(updateCapabilities = false)
|
||||||
_error.value = null
|
|
||||||
_finishedLoading.value = false
|
private fun loadData(updateCapabilities: Boolean) {
|
||||||
|
state.tryEmit(Loading(inProgress = true))
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
jellyfinRepository.postCapabilities()
|
if (updateCapabilities) repository.postCapabilities()
|
||||||
|
|
||||||
val items = mutableListOf<HomeItem>()
|
val updated = loadDynamicItems() + loadViews()
|
||||||
|
views.postValue(updated)
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
|
syncPlaybackProgress(repository, application)
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
state.tryEmit(Loading(inProgress = false))
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
|
||||||
syncPlaybackProgress(jellyfinRepository, application)
|
|
||||||
}
|
|
||||||
|
|
||||||
_views.value = items + views.map { HomeItem.ViewItem(it) }
|
|
||||||
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
_error.value = e.toString()
|
state.tryEmit(LoadingError(e.toString()))
|
||||||
}
|
}
|
||||||
_finishedLoading.value = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun loadDynamicItems() = withContext(Dispatchers.IO) {
|
||||||
|
val resumeItems = repository.getResumeItems()
|
||||||
|
val nextUpItems = repository.getNextUp()
|
||||||
|
|
||||||
|
val items = mutableListOf<HomeSection>()
|
||||||
|
if (resumeItems.isNotEmpty()) {
|
||||||
|
items.add(HomeSection(continueWatchingString, resumeItems))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextUpItems.isNotEmpty()) {
|
||||||
|
items.add(HomeSection(nextUpString, nextUpItems))
|
||||||
|
}
|
||||||
|
|
||||||
|
items.map { Section(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadViews() = withContext(Dispatchers.IO) {
|
||||||
|
repository
|
||||||
|
.getUserViews()
|
||||||
|
.filter { view -> unsupportedCollections().none { it.type == view.collectionType } }
|
||||||
|
.map { view -> view to repository.getLatestMedia(view.id) }
|
||||||
|
.filter { (_, latest) -> latest.isNotEmpty() }
|
||||||
|
.map { (view, latest) -> view.toView().apply { items = latest } }
|
||||||
|
.map { ViewItem(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class State
|
||||||
|
|
||||||
|
data class LoadingError(val message: String) : State()
|
||||||
|
data class Loading(val inProgress: Boolean) : State()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,48 +1,63 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
<layout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
>
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
|
|
||||||
<variable
|
<variable
|
||||||
name="viewModel"
|
name="viewModel"
|
||||||
type="dev.jdtech.jellyfin.viewmodels.HomeViewModel" />
|
type="dev.jdtech.jellyfin.viewmodels.HomeViewModel"
|
||||||
|
/>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
android:id="@+id/refresh_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".fragments.HomeFragment">
|
>
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/loading_indicator"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="0dp"
|
android:layout_height="match_parent"
|
||||||
android:layout_height="wrap_content"
|
tools:context=".fragments.HomeFragment"
|
||||||
android:indeterminate="true"
|
>
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<include
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
android:id="@+id/error_layout"
|
android:id="@+id/loading_indicator"
|
||||||
layout="@layout/error_panel" />
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:indeterminate="true"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:visibility="gone"
|
||||||
|
/>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<include
|
||||||
android:id="@+id/views_recycler_view"
|
android:id="@+id/error_layout"
|
||||||
android:layout_width="0dp"
|
layout="@layout/error_panel"
|
||||||
android:layout_height="0dp"
|
/>
|
||||||
android:clipToPadding="false"
|
|
||||||
android:paddingTop="16dp"
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
android:id="@+id/views_recycler_view"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:layout_width="0dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
android:clipToPadding="false"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
android:paddingTop="16dp"
|
||||||
app:views="@{viewModel.views}"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:itemCount="4"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
tools:listitem="@layout/view_item" />
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:views="@{viewModel.views()}"
|
||||||
|
tools:itemCount="4"
|
||||||
|
tools:listitem="@layout/view_item"
|
||||||
|
/>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
</layout>
|
</layout>
|
||||||
|
|
Loading…
Reference in a new issue