From fe7775329aca82e8907f6c8a9ffdb6cdc2c75a56 Mon Sep 17 00:00:00 2001 From: jarnedemeulemeester Date: Thu, 29 Jul 2021 12:20:49 +0200 Subject: [PATCH] Add error handling to all fragments --- .../jellyfin/fragments/LibraryFragment.kt | 23 + .../jellyfin/fragments/MediaFragment.kt | 20 +- .../jellyfin/fragments/MediaInfoFragment.kt | 17 + .../jellyfin/fragments/SeasonFragment.kt | 23 + .../viewmodels/EpisodeBottomSheetViewModel.kt | 18 +- .../jellyfin/viewmodels/HomeViewModel.kt | 3 +- .../jellyfin/viewmodels/LibraryViewModel.kt | 13 +- .../jellyfin/viewmodels/MediaInfoViewModel.kt | 44 +- .../jellyfin/viewmodels/MediaViewModel.kt | 31 +- .../jellyfin/viewmodels/SeasonViewModel.kt | 17 +- app/src/main/res/layout/fragment_library.xml | 60 +- app/src/main/res/layout/fragment_media.xml | 68 +- .../main/res/layout/fragment_media_info.xml | 659 +++++++++--------- app/src/main/res/layout/fragment_season.xml | 18 + 14 files changed, 600 insertions(+), 414 deletions(-) diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt index 68c59992..ae4ae5c5 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt @@ -8,7 +8,9 @@ import android.view.ViewGroup import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint +import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.viewmodels.LibraryViewModel import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding @@ -36,6 +38,27 @@ class LibraryFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.viewModel = viewModel + + val snackbar = + Snackbar.make( + binding.mainLayout, + getString(R.string.error_loading_data), + Snackbar.LENGTH_INDEFINITE + ) + snackbar.setAction(getString(R.string.retry)) { + viewModel.loadItems(args.libraryId) + } + + viewModel.error.observe(viewLifecycleOwner, { error -> + if (error) { + snackbar.show() + } + }) + + viewModel.finishedLoading.observe(viewLifecycleOwner, { + binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE + }) + binding.itemsRecyclerView.adapter = ViewItemListAdapter(ViewItemListAdapter.OnClickListener { item -> navigateToMediaInfoFragment(item) diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt index 99a06634..2e4e084d 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt @@ -7,7 +7,9 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController +import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint +import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.adapters.CollectionListAdapter import dev.jdtech.jellyfin.databinding.FragmentMediaBinding import dev.jdtech.jellyfin.viewmodels.MediaViewModel @@ -26,6 +28,16 @@ class MediaFragment : Fragment() { ): View { binding = FragmentMediaBinding.inflate(inflater, container, false) + val snackbar = + Snackbar.make( + binding.mainLayout, + getString(R.string.error_loading_data), + Snackbar.LENGTH_INDEFINITE + ) + snackbar.setAction(getString(R.string.retry)) { + viewModel.loadData() + } + binding.lifecycleOwner = this binding.viewModel = viewModel binding.viewsRecyclerView.adapter = @@ -34,8 +46,12 @@ class MediaFragment : Fragment() { }) viewModel.finishedLoading.observe(viewLifecycleOwner, { - if (it) { - binding.loadingIndicator.visibility = View.GONE + binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE + }) + + viewModel.error.observe(viewLifecycleOwner, { error -> + if (error) { + snackbar.show() } }) diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt index f1f1afa5..a68c860d 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt @@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.adapters.PersonListAdapter @@ -42,8 +43,24 @@ class MediaInfoFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val snackbar = + Snackbar.make( + binding.mainLayout, + getString(R.string.error_loading_data), + Snackbar.LENGTH_INDEFINITE + ) + snackbar.setAction(getString(R.string.retry)) { + viewModel.loadData(args.itemId, args.itemType) + } + binding.viewModel = viewModel + viewModel.error.observe(viewLifecycleOwner, { error -> + if (error) { + snackbar.show() + } + }) + viewModel.item.observe(viewLifecycleOwner, { item -> if (item.originalTitle != item.name) { binding.originalTitle.visibility = View.VISIBLE diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt index c924d84e..7c6732d1 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt @@ -8,7 +8,9 @@ import android.view.ViewGroup import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint +import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.adapters.EpisodeListAdapter import dev.jdtech.jellyfin.databinding.FragmentSeasonBinding import dev.jdtech.jellyfin.viewmodels.SeasonViewModel @@ -34,6 +36,27 @@ class SeasonFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.viewModel = viewModel + + val snackbar = + Snackbar.make( + binding.mainLayout, + getString(R.string.error_loading_data), + Snackbar.LENGTH_INDEFINITE + ) + snackbar.setAction(getString(R.string.retry)) { + viewModel.loadEpisodes(args.seriesId, args.seasonId) + } + + viewModel.error.observe(viewLifecycleOwner, { error -> + if (error) { + snackbar.show() + } + }) + + viewModel.finishedLoading.observe(viewLifecycleOwner, { + binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE + }) + binding.episodesRecyclerView.adapter = EpisodeListAdapter(EpisodeListAdapter.OnClickListener { episode -> navigateToEpisodeBottomSheetFragment(episode) diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt index 9a836589..03728292 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt @@ -10,6 +10,7 @@ import dev.jdtech.jellyfin.repository.JellyfinRepository import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.MediaSourceInfo +import timber.log.Timber import java.text.DateFormat import java.time.ZoneOffset import java.util.* @@ -42,12 +43,17 @@ constructor( fun loadEpisode(episodeId: UUID) { viewModelScope.launch { - _item.value = jellyfinRepository.getItem(episodeId) - _runTime.value = "${_item.value?.runTimeTicks?.div(600000000)} min" - _dateString.value = getDateString(_item.value!!) - _mediaSources.value = jellyfinRepository.getMediaSources(episodeId) - _played.value = _item.value?.userData?.played - _favorite.value = _item.value?.userData?.isFavorite + try { + val item = jellyfinRepository.getItem(episodeId) + _item.value = item + _runTime.value = "${item.runTimeTicks?.div(600000000)} min" + _dateString.value = getDateString(item) + _mediaSources.value = jellyfinRepository.getMediaSources(episodeId) + _played.value = item.userData?.played + _favorite.value = item.userData?.isFavorite + } catch (e: Exception) { + Timber.e(e) + } } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt index 40091e68..cfba9866 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt @@ -85,12 +85,11 @@ constructor( _views.value = items + views.map { HomeItem.ViewItem(it) } - _finishedLoading.value = true } catch (e: Exception) { Timber.e(e) - _finishedLoading.value = true _error.value = true } + _finishedLoading.value = true } } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt index 176c23fe..04c48f53 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt @@ -5,6 +5,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dev.jdtech.jellyfin.repository.JellyfinRepository import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.BaseItemDto +import timber.log.Timber import java.util.* import javax.inject.Inject @@ -19,9 +20,19 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { private val _finishedLoading = MutableLiveData() val finishedLoading: LiveData = _finishedLoading + private val _error = MutableLiveData() + val error: LiveData = _error + fun loadItems(parentId: UUID) { + _error.value = false + _finishedLoading.value = false viewModelScope.launch { - _items.value = jellyfinRepository.getItems(parentId) + try { + _items.value = jellyfinRepository.getItems(parentId) + } catch (e: Exception) { + Timber.e(e) + _error.value = true + } _finishedLoading.value = true } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt index 77ca0103..74470bb6 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.withContext import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.MediaSourceInfo +import timber.log.Timber import java.util.* import javax.inject.Inject @@ -62,25 +63,34 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { private val _favorite = MutableLiveData() val favorite: LiveData = _favorite + private val _error = MutableLiveData() + val error: LiveData = _error + fun loadData(itemId: UUID, itemType: String) { + _error.value = false viewModelScope.launch { - _item.value = jellyfinRepository.getItem(itemId) - _actors.value = getActors(_item.value!!) - _director.value = getDirector(_item.value!!) - _writers.value = getWriters(_item.value!!) - _writersString.value = - _writers.value?.joinToString(separator = ", ") { it.name.toString() } - _genresString.value = _item.value?.genres?.joinToString(separator = ", ") - _runTime.value = "${_item.value?.runTimeTicks?.div(600000000)} min" - _dateString.value = getDateString(_item.value!!) - _played.value = _item.value?.userData?.played - _favorite.value = _item.value?.userData?.isFavorite - if (itemType == "Series") { - _nextUp.value = getNextUp(itemId) - _seasons.value = jellyfinRepository.getSeasons(itemId) - } - if (itemType == "Movie") { - _mediaSources.value = jellyfinRepository.getMediaSources(itemId) + try { + _item.value = jellyfinRepository.getItem(itemId) + _actors.value = getActors(_item.value!!) + _director.value = getDirector(_item.value!!) + _writers.value = getWriters(_item.value!!) + _writersString.value = + _writers.value?.joinToString(separator = ", ") { it.name.toString() } + _genresString.value = _item.value?.genres?.joinToString(separator = ", ") + _runTime.value = "${_item.value?.runTimeTicks?.div(600000000)} min" + _dateString.value = getDateString(_item.value!!) + _played.value = _item.value?.userData?.played + _favorite.value = _item.value?.userData?.isFavorite + if (itemType == "Series") { + _nextUp.value = getNextUp(itemId) + _seasons.value = jellyfinRepository.getSeasons(itemId) + } + if (itemType == "Movie") { + _mediaSources.value = jellyfinRepository.getMediaSources(itemId) + } + } catch (e: Exception) { + Timber.e(e) + _error.value = true } } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaViewModel.kt index a647badf..a23f9607 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaViewModel.kt @@ -5,6 +5,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dev.jdtech.jellyfin.repository.JellyfinRepository import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.BaseItemDto +import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -20,16 +21,30 @@ constructor( private val _finishedLoading = MutableLiveData() val finishedLoading: LiveData = _finishedLoading + private val _error = MutableLiveData() + val error: LiveData = _error + init { + loadData() + } + + fun loadData() { + _finishedLoading.value = false + _error.value = false viewModelScope.launch { - val items = jellyfinRepository.getItems() - _collections.value = - items.filter { - it.collectionType != "homevideos" && - it.collectionType != "music" && - it.collectionType != "playlists" && - it.collectionType != "boxsets" - } + try { + val items = jellyfinRepository.getItems() + _collections.value = + items.filter { + it.collectionType != "homevideos" && + it.collectionType != "music" && + it.collectionType != "playlists" && + it.collectionType != "boxsets" + } + } catch (e: Exception) { + Timber.e(e) + _error.value = true + } _finishedLoading.value = true } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SeasonViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SeasonViewModel.kt index ea6f1227..cf15b182 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SeasonViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SeasonViewModel.kt @@ -9,6 +9,7 @@ import dev.jdtech.jellyfin.adapters.EpisodeItem import dev.jdtech.jellyfin.repository.JellyfinRepository import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.ItemFields +import timber.log.Timber import java.util.* import javax.inject.Inject @@ -20,9 +21,23 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { private val _episodes = MutableLiveData>() val episodes: LiveData> = _episodes + private val _finishedLoading = MutableLiveData() + val finishedLoading: LiveData = _finishedLoading + + private val _error = MutableLiveData() + val error: LiveData = _error + fun loadEpisodes(seriesId: UUID, seasonId: UUID) { + _error.value = false + _finishedLoading.value = false viewModelScope.launch { - _episodes.value = getEpisodes(seriesId, seasonId) + try { + _episodes.value = getEpisodes(seriesId, seasonId) + } catch (e: Exception) { + Timber.e(e) + _error.value = true + } + _finishedLoading.value = true } } diff --git a/app/src/main/res/layout/fragment_library.xml b/app/src/main/res/layout/fragment_library.xml index 5396719e..68151a85 100644 --- a/app/src/main/res/layout/fragment_library.xml +++ b/app/src/main/res/layout/fragment_library.xml @@ -2,34 +2,54 @@ + + - + android:layout_height="match_parent"> - + + + + + + + + - diff --git a/app/src/main/res/layout/fragment_media.xml b/app/src/main/res/layout/fragment_media.xml index 7db4be56..94b8fa1b 100644 --- a/app/src/main/res/layout/fragment_media.xml +++ b/app/src/main/res/layout/fragment_media.xml @@ -10,39 +10,45 @@ type="dev.jdtech.jellyfin.viewmodels.MediaViewModel" /> - + android:layout_height="match_parent"> - + - + - + + + + diff --git a/app/src/main/res/layout/fragment_media_info.xml b/app/src/main/res/layout/fragment_media_info.xml index de3a4b57..6c90bd5d 100644 --- a/app/src/main/res/layout/fragment_media_info.xml +++ b/app/src/main/res/layout/fragment_media_info.xml @@ -12,366 +12,373 @@ type="dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel" /> - - + android:layout_height="match_parent"> - + android:layout_height="wrap_content" + android:orientation="vertical" + tools:context=".fragments.MediaInfoFragment"> - + android:layout_marginBottom="8dp"> - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + tools:text="Alita: Battle Angel" /> + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - + android:layout_marginBottom="12dp" + android:visibility="@{viewModel.item.genres.size() < 1 ? View.GONE : View.VISIBLE}"> + + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_marginHorizontal="24dp" + android:layout_marginBottom="12dp" + android:text="@string/next_up" + android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" + android:textSize="18sp" /> + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + diff --git a/app/src/main/res/layout/fragment_season.xml b/app/src/main/res/layout/fragment_season.xml index 33bc7154..bb1f63ae 100644 --- a/app/src/main/res/layout/fragment_season.xml +++ b/app/src/main/res/layout/fragment_season.xml @@ -12,10 +12,27 @@ + + + + + + \ No newline at end of file