diff --git a/app/build.gradle b/app/build.gradle index fa04e160..f3f12b4a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,7 +5,7 @@ plugins { id 'kotlin-kapt' id 'androidx.navigation.safeargs.kotlin' id 'dagger.hilt.android.plugin' - id 'com.google.android.gms.oss-licenses-plugin' + id "com.mikepenz.aboutlibraries.plugin" } android { @@ -16,8 +16,8 @@ android { applicationId "dev.jdtech.jellyfin" minSdkVersion 24 targetSdkVersion 31 - versionCode 1 - versionName "0.1.0" + versionCode 2 + versionName "0.1.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -42,6 +42,7 @@ android { buildFeatures { dataBinding true + viewBinding true } } @@ -96,11 +97,12 @@ dependencies { implementation files('libs/extension-ffmpeg-release.aar') // Timber - def timber_version = "5.0.0" + def timber_version = "5.0.1" implementation "com.jakewharton.timber:timber:$timber_version" - def oss_licenses_version = "17.0.0" - implementation "com.google.android.gms:play-services-oss-licenses:$oss_licenses_version" + def about_libraries_version = "8.9.1" + implementation "com.mikepenz:aboutlibraries-core:$about_libraries_version" + implementation "com.mikepenz:aboutlibraries:$about_libraries_version" // Testing testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 75b71ba4..8afba6f1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,11 +27,6 @@ - - - diff --git a/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt b/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt index 1d74d115..ca3969bd 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt @@ -51,9 +51,10 @@ class MainActivity : AppCompatActivity() { navController.addOnDestinationChangedListener { _, destination, _ -> binding.navView.visibility = when (destination.id) { - R.id.settingsFragment, R.id.serverSelectFragment, R.id.addServerFragment, R.id.loginFragment -> View.GONE + R.id.settingsFragment, R.id.serverSelectFragment, R.id.addServerFragment, R.id.loginFragment, R.id.about_libraries_dest -> View.GONE else -> View.VISIBLE } + if (destination.id == R.id.about_libraries_dest) binding.mainToolbar.title = getString(R.string.app_info) } viewModel.navigateToAddServer.observe(this, { diff --git a/app/src/main/java/dev/jdtech/jellyfin/dialogs/ErrorDialogFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/dialogs/ErrorDialogFragment.kt new file mode 100644 index 00000000..6f3ead62 --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/dialogs/ErrorDialogFragment.kt @@ -0,0 +1,33 @@ +package dev.jdtech.jellyfin.dialogs + +import android.app.Dialog +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dev.jdtech.jellyfin.R +import java.lang.IllegalStateException + +class ErrorDialogFragment(private val errorMessage: String) : DialogFragment() { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return activity?.let { + val builder = MaterialAlertDialogBuilder(it, R.style.ErrorDialogStyle) + builder + .setMessage(errorMessage) + .setPositiveButton("close") { _, _ -> + } + .setNeutralButton("share") { _, _ -> + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, errorMessage) + type = "text/plain" + } + + val shareIntent = Intent.createChooser(sendIntent, null) + startActivity(shareIntent) + + } + builder.create() + } ?: throw IllegalStateException("Activity cannot be null") + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt index eb4a83a7..123391e0 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt @@ -13,6 +13,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.databinding.EpisodeBottomSheetBinding +import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.viewmodels.EpisodeBottomSheetViewModel @@ -98,17 +99,20 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { } }) - viewModel.playerItemsError.observe(viewLifecycleOwner, { - when (it) { - true -> { - binding.playerItemsError.visibility = View.VISIBLE - binding.playButton.setImageDrawable(ContextCompat.getDrawable(requireActivity(), R.drawable.ic_play)) - binding.progressCircular.visibility = View.INVISIBLE - } - false -> binding.playerItemsError.visibility = View.GONE + viewModel.playerItemsError.observe(viewLifecycleOwner, { errorMessage -> + if (errorMessage != null) { + binding.playerItemsError.visibility = View.VISIBLE + binding.playButton.setImageDrawable(ContextCompat.getDrawable(requireActivity(), R.drawable.ic_play)) + binding.progressCircular.visibility = View.INVISIBLE + } else { + binding.playerItemsError.visibility = View.GONE } }) + binding.playerItemsErrorDetails.setOnClickListener { + ErrorDialogFragment(viewModel.playerItemsError.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + } + viewModel.loadEpisode(args.episodeId) return binding.root diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt index d66f7011..d92e5a04 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/FavoriteFragment.kt @@ -7,13 +7,13 @@ import android.view.View import android.view.ViewGroup 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.FavoritesListAdapter import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.databinding.FragmentFavoriteBinding +import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.viewmodels.FavoriteViewModel import org.jellyfin.sdk.model.api.BaseItemDto @@ -29,16 +29,6 @@ class FavoriteFragment : Fragment() { ): View { binding = FragmentFavoriteBinding.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 = viewLifecycleOwner binding.viewModel = viewModel binding.favoritesRecyclerView.adapter = FavoritesListAdapter( @@ -53,11 +43,23 @@ class FavoriteFragment : Fragment() { }) viewModel.error.observe(viewLifecycleOwner, { error -> - if (error) { - snackbar.show() + if (error != null) { + binding.errorLayout.errorPanel.visibility = View.VISIBLE + binding.favoritesRecyclerView.visibility = View.GONE + } else { + binding.errorLayout.errorPanel.visibility = View.GONE + binding.favoritesRecyclerView.visibility = View.VISIBLE } }) + binding.errorLayout.errorRetryButton.setOnClickListener { + viewModel.loadData() + } + + binding.errorLayout.errorDetailsButton.setOnClickListener { + ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + } + viewModel.favoriteSections.observe(viewLifecycleOwner, { sections -> if (sections.isEmpty()) { binding.noFavoritesText.visibility = View.VISIBLE diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt index f2bc3b74..78891d63 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt @@ -5,13 +5,13 @@ import android.view.* 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.HomeEpisodeListAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.adapters.ViewListAdapter import dev.jdtech.jellyfin.databinding.FragmentHomeBinding +import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.viewmodels.HomeViewModel import org.jellyfin.sdk.model.api.BaseItemDto @@ -49,12 +49,6 @@ class HomeFragment : Fragment() { ): View { binding = FragmentHomeBinding.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 = viewLifecycleOwner binding.viewModel = viewModel binding.viewsRecyclerView.adapter = ViewListAdapter(ViewListAdapter.OnClickListener { @@ -78,11 +72,23 @@ class HomeFragment : Fragment() { }) viewModel.error.observe(viewLifecycleOwner, { error -> - if (error) { - snackbar.show() + if (error != null) { + 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.loadData() + } + + binding.errorLayout.errorDetailsButton.setOnClickListener { + ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + } + return binding.root } 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 5858ca06..3eba789f 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt @@ -8,12 +8,12 @@ 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 +import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import org.jellyfin.sdk.model.api.BaseItemDto @AndroidEntryPoint @@ -39,21 +39,23 @@ class LibraryFragment : Fragment() { 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.error.observe(viewLifecycleOwner, { error -> + if (error != null) { + binding.errorLayout.errorPanel.visibility = View.VISIBLE + binding.itemsRecyclerView.visibility = View.GONE + } else { + binding.errorLayout.errorPanel.visibility = View.GONE + binding.itemsRecyclerView.visibility = View.VISIBLE + } + }) + + binding.errorLayout.errorRetryButton.setOnClickListener { viewModel.loadItems(args.libraryId) } - viewModel.error.observe(viewLifecycleOwner, { error -> - if (error) { - snackbar.show() - } - }) + binding.errorLayout.errorDetailsButton.setOnClickListener { + ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + } viewModel.finishedLoading.observe(viewLifecycleOwner, { binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE 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 94a185ee..cb6b35d0 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaFragment.kt @@ -6,11 +6,11 @@ import androidx.appcompat.widget.SearchView 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.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.viewmodels.MediaViewModel import org.jellyfin.sdk.model.api.BaseItemDto @@ -53,16 +53,6 @@ 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 = viewLifecycleOwner binding.viewModel = viewModel binding.viewsRecyclerView.adapter = @@ -75,11 +65,23 @@ class MediaFragment : Fragment() { }) viewModel.error.observe(viewLifecycleOwner, { error -> - if (error) { - snackbar.show() + if (error != null) { + 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.loadData() + } + + binding.errorLayout.errorDetailsButton.setOnClickListener { + ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + } + return binding.root } 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 6e6ac673..a94aa985 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt @@ -11,12 +11,12 @@ 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 import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.databinding.FragmentMediaInfoBinding +import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel @@ -44,24 +44,26 @@ 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() + if (error != null) { + binding.errorLayout.errorPanel.visibility = View.VISIBLE + binding.mediaInfoScrollview.visibility = View.GONE + } else { + binding.errorLayout.errorPanel.visibility = View.GONE + binding.mediaInfoScrollview.visibility = View.VISIBLE } }) + binding.errorLayout.errorRetryButton.setOnClickListener { + viewModel.loadData(args.itemId, args.itemType) + } + + binding.errorLayout.errorDetailsButton.setOnClickListener { + ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + } + viewModel.item.observe(viewLifecycleOwner, { item -> if (item.originalTitle != item.name) { binding.originalTitle.visibility = View.VISIBLE @@ -91,7 +93,12 @@ class MediaInfoFragment : Fragment() { viewModel.item.value!!.userData!!.playbackPositionTicks.div(10000) ) viewModel.doneNavigatingToPlayer() - 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 } }) @@ -114,17 +121,25 @@ class MediaInfoFragment : Fragment() { binding.favoriteButton.setImageResource(drawable) }) - viewModel.playerItemsError.observe(viewLifecycleOwner, { - when (it) { - true -> { - binding.playerItemsError.visibility = View.VISIBLE - binding.playButton.setImageDrawable(ContextCompat.getDrawable(requireActivity(), R.drawable.ic_play)) - binding.progressCircular.visibility = View.INVISIBLE - } - false -> binding.playerItemsError.visibility = View.GONE + viewModel.playerItemsError.observe(viewLifecycleOwner, { errorMessage -> + if (errorMessage != null) { + binding.playerItemsError.visibility = View.VISIBLE + binding.playButton.setImageDrawable( + ContextCompat.getDrawable( + requireActivity(), + R.drawable.ic_play + ) + ) + binding.progressCircular.visibility = View.INVISIBLE + } else { + binding.playerItemsError.visibility = View.GONE } }) + binding.playerItemsErrorDetails.setOnClickListener { + ErrorDialogFragment(viewModel.playerItemsError.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + } + binding.trailerButton.setOnClickListener { val intent = Intent( Intent.ACTION_VIEW, @@ -155,10 +170,20 @@ class MediaInfoFragment : Fragment() { ) } else { navigateToPlayerActivity( - arrayOf(PlayerItem(args.itemId, viewModel.mediaSources.value!![0].id!!)), + 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.playButton.setImageDrawable( + ContextCompat.getDrawable( + requireActivity(), + R.drawable.ic_play + ) + ) binding.progressCircular.visibility = View.INVISIBLE } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/SearchResultFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/SearchResultFragment.kt index ca1b6374..f13de478 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/SearchResultFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/SearchResultFragment.kt @@ -8,13 +8,13 @@ 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.FavoritesListAdapter import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter import dev.jdtech.jellyfin.adapters.ViewItemListAdapter import dev.jdtech.jellyfin.databinding.FragmentSearchResultBinding +import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.viewmodels.SearchResultViewModel import org.jellyfin.sdk.model.api.BaseItemDto @@ -32,16 +32,6 @@ class SearchResultFragment : Fragment() { ): View { binding = FragmentSearchResultBinding.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(args.query) - } - binding.lifecycleOwner = viewLifecycleOwner binding.viewModel = viewModel binding.searchResultsRecyclerView.adapter = FavoritesListAdapter( @@ -56,11 +46,23 @@ class SearchResultFragment : Fragment() { }) viewModel.error.observe(viewLifecycleOwner, { error -> - if (error) { - snackbar.show() + if (error != null) { + binding.errorLayout.errorPanel.visibility = View.VISIBLE + binding.searchResultsRecyclerView.visibility = View.GONE + } else { + binding.errorLayout.errorPanel.visibility = View.GONE + binding.searchResultsRecyclerView.visibility = View.VISIBLE } }) + binding.errorLayout.errorRetryButton.setOnClickListener { + viewModel.loadData(args.query) + } + + binding.errorLayout.errorDetailsButton.setOnClickListener { + ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + } + viewModel.sections.observe(viewLifecycleOwner, { sections -> if (sections.isEmpty()) { binding.noSearchResultsText.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 32500867..7ae52879 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt @@ -8,11 +8,11 @@ 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.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.viewmodels.SeasonViewModel import org.jellyfin.sdk.model.api.BaseItemDto @@ -37,21 +37,23 @@ class SeasonFragment : Fragment() { 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.error.observe(viewLifecycleOwner, { error -> + if (error != null) { + binding.errorLayout.errorPanel.visibility = View.VISIBLE + binding.episodesRecyclerView.visibility = View.GONE + } else { + binding.errorLayout.errorPanel.visibility = View.GONE + binding.episodesRecyclerView.visibility = View.VISIBLE + } + }) + + binding.errorLayout.errorRetryButton.setOnClickListener { viewModel.loadEpisodes(args.seriesId, args.seasonId) } - viewModel.error.observe(viewLifecycleOwner, { error -> - if (error) { - snackbar.show() - } - }) + binding.errorLayout.errorDetailsButton.setOnClickListener { + ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog") + } viewModel.finishedLoading.observe(viewLifecycleOwner, { binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/ServerSelectFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/ServerSelectFragment.kt index d082b145..886db38a 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/ServerSelectFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/ServerSelectFragment.kt @@ -8,7 +8,6 @@ import android.view.ViewGroup import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint -import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.databinding.FragmentServerSelectBinding import dev.jdtech.jellyfin.dialogs.DeleteServerDialogFragment import dev.jdtech.jellyfin.adapters.ServerGridAdapter diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt index 9fd43144..79be8b10 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/SettingsFragment.kt @@ -8,7 +8,6 @@ import androidx.navigation.fragment.findNavController import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat -import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import dev.jdtech.jellyfin.R class SettingsFragment : PreferenceFragmentCompat() { @@ -38,8 +37,8 @@ class SettingsFragment : PreferenceFragmentCompat() { true } - findPreference("ossLicenses")?.setOnPreferenceClickListener { - startActivity(Intent(requireContext(), OssLicensesMenuActivity::class.java)) + findPreference("appInfo")?.setOnPreferenceClickListener { + findNavController().navigate(SettingsFragmentDirections.actionSettingsFragmentToAboutLibraries()) true } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index 297def39..1f393e98 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -66,7 +66,10 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep val items: List withContext(Dispatchers.IO) { items = - jellyfinApi.itemsApi.getResumeItems(jellyfinApi.userId!!).content.items ?: listOf() + jellyfinApi.itemsApi.getResumeItems( + jellyfinApi.userId!!, + includeItemTypes = listOf("Movie", "Episode"), + ).content.items ?: listOf() } return items } diff --git a/app/src/main/java/dev/jdtech/jellyfin/utils/VersionPreference.kt b/app/src/main/java/dev/jdtech/jellyfin/utils/VersionPreference.kt deleted file mode 100644 index 7ce9e9fc..00000000 --- a/app/src/main/java/dev/jdtech/jellyfin/utils/VersionPreference.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.jdtech.jellyfin.utils - -import android.content.Context -import android.util.AttributeSet -import androidx.preference.Preference - -class VersionPreference(context: Context, attrs: AttributeSet): Preference(context, attrs) { - init { - val packageManager = context.packageManager - val packageInfo = packageManager.getPackageInfo(context.packageName, 0) - val versionName = packageInfo.versionName - summary = versionName - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt index 829d6dc5..3f86da36 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt @@ -9,8 +9,11 @@ import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.database.Server import dev.jdtech.jellyfin.database.ServerDatabaseDao import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.jellyfin.sdk.discovery.RecommendedServerInfo +import org.jellyfin.sdk.discovery.RecommendedServerInfoScore import timber.log.Timber import javax.inject.Inject @@ -34,19 +37,32 @@ constructor( * - Connect to server and check if it is a Jellyfin server * - Check if server is not already in Database */ - fun checkServer(baseUrl: String) { + fun checkServer(inputValue: String) { _error.value = null viewModelScope.launch { - jellyfinApi.apply { - api.baseUrl = baseUrl - api.accessToken = null - } try { - val publicSystemInfo by jellyfinApi.systemApi.getPublicSystemInfo() - Timber.d("Remote server: ${publicSystemInfo.id}") + val candidates = jellyfinApi.jellyfin.discovery.getAddressCandidates(inputValue) + val recommended = jellyfinApi.jellyfin.discovery.getRecommendedServers( + candidates, + RecommendedServerInfoScore.GOOD + ) + val recommendedServer: RecommendedServerInfo - if (serverAlreadyInDatabase(publicSystemInfo.id)) { + try { + recommendedServer = recommended.first() + } catch (e: NoSuchElementException) { + throw Exception("Server not found") + } + + jellyfinApi.apply { + api.baseUrl = recommendedServer.address + api.accessToken = null + } + + Timber.d("Remote server: ${recommendedServer.systemInfo?.id}") + + if (serverAlreadyInDatabase(recommendedServer.systemInfo?.id)) { _error.value = "Server already added" _navigateToLogin.value = false } else { 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 51422e83..baa784df 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt @@ -43,8 +43,8 @@ constructor( var playerItems: MutableList = mutableListOf() - private val _playerItemsError = MutableLiveData() - val playerItemsError: LiveData = _playerItemsError + private val _playerItemsError = MutableLiveData() + val playerItemsError: LiveData = _playerItemsError fun loadEpisode(episodeId: UUID) { viewModelScope.launch { @@ -62,13 +62,13 @@ constructor( } fun preparePlayer() { - _playerItemsError.value = false + _playerItemsError.value = null viewModelScope.launch { try { createPlayerItems(_item.value!!) _navigateToPlayer.value = true } catch (e: Exception) { - _playerItemsError.value = true + _playerItemsError.value = e.message } } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/FavoriteViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/FavoriteViewModel.kt index e9138d09..eb339dc7 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/FavoriteViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/FavoriteViewModel.kt @@ -26,15 +26,15 @@ constructor( private val _finishedLoading = MutableLiveData() val finishedLoading: LiveData = _finishedLoading - private val _error = MutableLiveData() - val error: LiveData = _error + private val _error = MutableLiveData() + val error: LiveData = _error init { loadData() } fun loadData() { - _error.value = false + _error.value = null _finishedLoading.value = false viewModelScope.launch { try { @@ -78,7 +78,7 @@ constructor( _favoriteSections.value = tempFavoriteSections } catch (e: Exception) { Timber.e(e) - _error.value = true + _error.value = e.message } _finishedLoading.value = true } 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 cfba9866..70807509 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt @@ -38,15 +38,15 @@ constructor( private val _finishedLoading = MutableLiveData() val finishedLoading: LiveData = _finishedLoading - private val _error = MutableLiveData() - val error: LiveData = _error + private val _error = MutableLiveData() + val error: LiveData = _error init { loadData() } fun loadData() { - _error.value = false + _error.value = null _finishedLoading.value = false viewModelScope.launch { try { @@ -57,7 +57,8 @@ constructor( Timber.d("Collection type: ${view.collectionType}") if (view.collectionType == "homevideos" || view.collectionType == "music" || - view.collectionType == "playlists" + view.collectionType == "playlists" || + view.collectionType == "books" ) continue val latestItems = jellyfinRepository.getLatestMedia(view.id) if (latestItems.isEmpty()) continue @@ -87,7 +88,7 @@ constructor( } catch (e: Exception) { Timber.e(e) - _error.value = true + _error.value = e.message } _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 04c48f53..ac0d75e9 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/LibraryViewModel.kt @@ -20,18 +20,18 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { private val _finishedLoading = MutableLiveData() val finishedLoading: LiveData = _finishedLoading - private val _error = MutableLiveData() - val error: LiveData = _error + private val _error = MutableLiveData() + val error: LiveData = _error fun loadItems(parentId: UUID) { - _error.value = false + _error.value = null _finishedLoading.value = false viewModelScope.launch { try { _items.value = jellyfinRepository.getItems(parentId) } catch (e: Exception) { Timber.e(e) - _error.value = true + _error.value = e.message } _finishedLoading.value = true } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MainViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MainViewModel.kt index 92cab80b..f319617a 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MainViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MainViewModel.kt @@ -1,12 +1,10 @@ package dev.jdtech.jellyfin.viewmodels -import android.app.Application import android.content.SharedPreferences import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.preference.PreferenceManager import dagger.hilt.android.lifecycle.HiltViewModel import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.database.Server 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 6df01cbb..ea564c2d 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt @@ -64,16 +64,16 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { private val _favorite = MutableLiveData() val favorite: LiveData = _favorite - private val _error = MutableLiveData() - val error: LiveData = _error + private val _error = MutableLiveData() + val error: LiveData = _error var playerItems: MutableList = mutableListOf() - private val _playerItemsError = MutableLiveData() - val playerItemsError: LiveData = _playerItemsError + private val _playerItemsError = MutableLiveData() + val playerItemsError: LiveData = _playerItemsError fun loadData(itemId: UUID, itemType: String) { - _error.value = false + _error.value = null viewModelScope.launch { try { _item.value = jellyfinRepository.getItem(itemId) @@ -96,7 +96,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { } } catch (e: Exception) { Timber.e(e) - _error.value = true + _error.value = e.message } } } @@ -184,13 +184,13 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { } fun preparePlayer() { - _playerItemsError.value = false + _playerItemsError.value = null viewModelScope.launch { try { createPlayerItems(_item.value!!) _navigateToPlayer.value = playerItems.toTypedArray() } catch (e: Exception) { - _playerItemsError.value = true + _playerItemsError.value = e.message } } } 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 a23f9607..60e1e1c6 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaViewModel.kt @@ -21,8 +21,8 @@ constructor( private val _finishedLoading = MutableLiveData() val finishedLoading: LiveData = _finishedLoading - private val _error = MutableLiveData() - val error: LiveData = _error + private val _error = MutableLiveData() + val error: LiveData = _error init { loadData() @@ -30,7 +30,7 @@ constructor( fun loadData() { _finishedLoading.value = false - _error.value = false + _error.value = null viewModelScope.launch { try { val items = jellyfinRepository.getItems() @@ -39,11 +39,12 @@ constructor( it.collectionType != "homevideos" && it.collectionType != "music" && it.collectionType != "playlists" && - it.collectionType != "boxsets" + it.collectionType != "boxsets" && + it.collectionType != "books" } } catch (e: Exception) { Timber.e(e) - _error.value = true + _error.value = e.message } _finishedLoading.value = true } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SearchResultViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SearchResultViewModel.kt index f5f8f925..b9fc1e47 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SearchResultViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SearchResultViewModel.kt @@ -26,11 +26,11 @@ constructor( private val _finishedLoading = MutableLiveData() val finishedLoading: LiveData = _finishedLoading - private val _error = MutableLiveData() - val error: LiveData = _error + private val _error = MutableLiveData() + val error: LiveData = _error fun loadData(query: String) { - _error.value = false + _error.value = null _finishedLoading.value = false viewModelScope.launch { try { @@ -74,7 +74,7 @@ constructor( _sections.value = tempSections } catch (e: Exception) { Timber.e(e) - _error.value = true + _error.value = e.message } _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 cf15b182..39f7525c 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SeasonViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SeasonViewModel.kt @@ -24,18 +24,18 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { private val _finishedLoading = MutableLiveData() val finishedLoading: LiveData = _finishedLoading - private val _error = MutableLiveData() - val error: LiveData = _error + private val _error = MutableLiveData() + val error: LiveData = _error fun loadEpisodes(seriesId: UUID, seasonId: UUID) { - _error.value = false + _error.value = null _finishedLoading.value = false viewModelScope.launch { try { _episodes.value = getEpisodes(seriesId, seasonId) } catch (e: Exception) { Timber.e(e) - _error.value = true + _error.value = e.message } _finishedLoading.value = true } diff --git a/app/src/main/res/drawable/ic_alert_circle.xml b/app/src/main/res/drawable/ic_alert_circle.xml new file mode 100644 index 00000000..961d2ac4 --- /dev/null +++ b/app/src/main/res/drawable/ic_alert_circle.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/app/src/main/res/layout/activity_player.xml b/app/src/main/res/layout/activity_player.xml index 49020401..19680665 100644 --- a/app/src/main/res/layout/activity_player.xml +++ b/app/src/main/res/layout/activity_player.xml @@ -1,5 +1,5 @@ - - + diff --git a/app/src/main/res/layout/collection_item.xml b/app/src/main/res/layout/collection_item.xml index 6b78f8f1..2261215b 100644 --- a/app/src/main/res/layout/collection_item.xml +++ b/app/src/main/res/layout/collection_item.xml @@ -14,7 +14,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="12dp" - android:layout_marginBottom="32dp" + android:layout_marginBottom="24dp" android:foreground="?android:attr/selectableItemBackground" android:orientation="vertical"> @@ -37,7 +37,6 @@ android:layout_marginTop="4dp" android:text="@{collection.name}" android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" - android:textSize="18sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/collection_image" diff --git a/app/src/main/res/layout/episode_bottom_sheet.xml b/app/src/main/res/layout/episode_bottom_sheet.xml index 99044167..5bb2b42b 100644 --- a/app/src/main/res/layout/episode_bottom_sheet.xml +++ b/app/src/main/res/layout/episode_bottom_sheet.xml @@ -65,13 +65,14 @@ tools:text="1. To You, in 2000 Years: The Fall of Shiganshina, Part 1" /> + app:layout_constraintStart_toStartOf="@id/episode_image" + app:layout_constraintTop_toBottomOf="@id/episode_image"> + app:layout_constraintTop_toBottomOf="@id/episode_metadata"> @@ -163,21 +164,37 @@ android:src="@drawable/ic_heart" /> - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/buttons" + tools:visibility="visible"> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/error_panel.xml b/app/src/main/res/layout/error_panel.xml new file mode 100644 index 00000000..33ea0c78 --- /dev/null +++ b/app/src/main/res/layout/error_panel.xml @@ -0,0 +1,53 @@ + + + + + + + + + +