diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 866574f9..f451b05a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -212,6 +212,7 @@ dependencies { implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors implementation("androidx.tvprovider:tvprovider:1.0.0") implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures + implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview // Extensions & Other Libs diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e665c3bc..a23ef725 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ - + diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 84c9d047..0dc1d693 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -308,6 +308,7 @@ object CommonActivity { "Banana" -> R.style.OverlayPrimaryColorBanana "Party" -> R.style.OverlayPrimaryColorParty "Pink" -> R.style.OverlayPrimaryColorPink + "Lavender" -> R.style.OverlayPrimaryColorLavender "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index afb2f76f..48798ce6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -28,6 +28,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.children import androidx.core.view.isGone +import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.view.marginStart import androidx.fragment.app.FragmentActivity @@ -112,6 +113,7 @@ import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.SearchFragment import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTruePhone import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv @@ -131,11 +133,15 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup +import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate @@ -166,7 +172,6 @@ import kotlin.math.absoluteValue import kotlin.reflect.KClass import kotlin.system.exitProcess - //https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898 //https://wiki.videolan.org/Android_Player_Intents/ @@ -285,7 +290,7 @@ var app = Requests(responseParser = object : ResponseParser { defaultHeaders = mapOf("user-agent" to USER_AGENT) } -class MainActivity : AppCompatActivity(), ColorPickerDialogListener { +class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAuthenticator.BiometricAuthCallback { companion object { const val TAG = "MAINACT" const val ANIMATED_OUTLINE: Boolean = false @@ -1171,7 +1176,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false) setContentView(newLocalBinding.root) - if(isTrueTvSettings() && ANIMATED_OUTLINE) { + if (isTrueTvSettings() && ANIMATED_OUTLINE) { TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) @@ -1184,13 +1189,29 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } if(isTrueTvSettings()) { + // Put here any button you don't want focusing it to center the view + val exceptionButtons = listOf( + R.id.home_preview_play_btt, + R.id.home_preview_info_btt, + R.id.home_preview_hidden_next_focus, + R.id.home_preview_hidden_prev_focus, + R.id.result_play_movie_button, + R.id.result_play_series_button, + R.id.result_resume_series_button, + R.id.result_play_trailer_button, + R.id.result_bookmark_Button, + R.id.result_favorite_Button, + R.id.result_subscribe_Button, + R.id.result_search_Button, + R.id.result_episodes_show_button, + ) + newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> + if (exceptionButtons.contains(newFocus?.id)) return@addOnGlobalFocusChangeListener centerView(newFocus) } } - - ActivityMainBinding.bind(newLocalBinding.root) // this may crash } else { val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false) @@ -1204,6 +1225,23 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { changeStatusBarState(isEmulatorSettings()) + /** Biometric stuff for users without accounts **/ + val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) + val noAccounts = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false) || accounts.count() <= 1 + + if (isTruePhone() && authEnabled && noAccounts) { + if (deviceHasPasswordPinLock(this)) { + startBiometricAuthentication(this, R.string.biometric_authentication_title, false) + + BiometricAuthenticator.promptInfo?.let { + BiometricAuthenticator.biometricPrompt?.authenticate(it) + } + + // hide background while authenticating, Sorry moms & dads 🙏 + binding?.navHostFragment?.isInvisible = true + } + } + // Automatically enable jsdelivr if cant connect to raw.githubusercontent.com if (this.getKey(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) { main { @@ -1743,6 +1781,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { ) } + /** Biometric stuff **/ + override fun onAuthenticationSuccess() { + // make background (nav host fragment) visible again + binding?.navHostFragment?.isInvisible = false + } + private var backPressedCallback: OnBackPressedCallback? = null private fun attachBackPressedCallback() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index eb575775..817d7db3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -49,11 +49,15 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) { } } +/** NOTE: Only one observer at a time per value */ fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) liveData.observe(this) { it?.let { t -> action(t) } } } +/** NOTE: Only one observer at a time per value */ fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) liveData.observe(this) { action(it) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt index 23071f59..1dae5a0f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui.account import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle +import android.util.Log import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager @@ -17,13 +18,17 @@ import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_EDIT_ACCOUNT import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_SELECT_ACCOUNT +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTruePhone import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute -class AccountSelectActivity : AppCompatActivity() { +class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.BiometricAuthCallback { lateinit var viewModel: AccountViewModel @@ -41,13 +46,36 @@ class AccountSelectActivity : AppCompatActivity() { ) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val skipStartup = settingsManager.getBoolean( - getString(R.string.skip_startup_account_select_key), - false + val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) + val skipStartup = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false ) || accounts.count() <= 1 viewModel = ViewModelProvider(this)[AccountViewModel::class.java] + fun askBiometricAuth() { + + if (isTruePhone() && authEnabled) { + if (deviceHasPasswordPinLock(this)) { + startBiometricAuthentication( + this, + R.string.biometric_authentication_title, + false + ) + + BiometricAuthenticator.promptInfo?.let { + BiometricAuthenticator.biometricPrompt?.authenticate(it) + } + } + } + } + + observe(viewModel.isAllowedLogin) { isAllowedLogin -> + if (isAllowedLogin) { + // We are allowed to continue to MainActivity + navigateToMainActivity() + } + } + // Don't show account selection if there is only // one account that exists if (!isEditingFromMainActivity && skipStartup) { @@ -55,12 +83,6 @@ class AccountSelectActivity : AppCompatActivity() { if (currentAccount?.lockPin != null) { CommonActivity.init(this) viewModel.handleAccountSelect(currentAccount, this, true) - observe(viewModel.isAllowedLogin) { isAllowedLogin -> - if (isAllowedLogin) { - // We are allowed to continue to MainActivity - navigateToMainActivity() - } - } } else { if (accounts.count() > 1) { showToast(this, getString( @@ -88,12 +110,6 @@ class AccountSelectActivity : AppCompatActivity() { // Handle the selected account accountSelectCallback = { viewModel.handleAccountSelect(it, this) - observe(viewModel.isAllowedLogin) { isAllowedLogin -> - if (isAllowedLogin) { - // We are allowed to continue to MainActivity - navigateToMainActivity() - } - } }, accountCreateCallback = { viewModel.handleAccountUpdate(it, this) }, accountEditCallback = { @@ -158,6 +174,8 @@ class AccountSelectActivity : AppCompatActivity() { } else 6 } } + + askBiometricAuth() } private fun navigateToMainActivity() { @@ -165,4 +183,8 @@ class AccountSelectActivity : AppCompatActivity() { startActivity(mainIntent) finish() // Finish the account selection activity } + + override fun onAuthenticationSuccess() { + Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity") + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index d54ea488..cd843517 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -529,6 +529,7 @@ class HomeFragment : Fragment() { super.onScrolled(recyclerView, dx, dy) } }) + } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index 8e9c8521..fa91d990 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -131,6 +131,18 @@ class LibraryFragment : Fragment() { super.onSaveInstanceState(outState) } + private fun updateRandom() { + val position = libraryViewModel.currentPage.value ?: 0 + val pages = (libraryViewModel.pages.value as? Resource.Success)?.value ?: return + if (toggleRandomButton) { + listLibraryItems.clear() + listLibraryItems.addAll(pages[position].items) + binding?.libraryRandom?.isVisible = listLibraryItems.isNotEmpty() + } else { + binding?.libraryRandom?.isGone = true + } + } + @SuppressLint("ResourceType", "CutPasteId") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -395,15 +407,7 @@ class LibraryFragment : Fragment() { binding?.viewpager?.setCurrentItem(page, false) } - observe(libraryViewModel.currentPage){ - if (toggleRandomButton) { - listLibraryItems.clear() - listLibraryItems.addAll(pages[it].items) - libraryRandom.isVisible = listLibraryItems.isNotEmpty() - } else { - libraryRandom.isGone = true - } - } + updateRandom() // Only stop loading after 300ms to hide the fade effect the viewpager produces when updating // Without this there would be a flashing effect: @@ -481,6 +485,7 @@ class LibraryFragment : Fragment() { } observe(libraryViewModel.currentPage) { position -> + updateRandom() val all = binding?.viewpager?.allViews?.toList() ?.filterIsInstance() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index 427e9cb3..85e948c2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -33,13 +33,13 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator import com.lagradost.cloudstream3.ui.player.GeneratorPlayer +import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.AppUtils.loadCache @@ -129,9 +129,9 @@ class ResultFragmentTv : Fragment() { * Note that this will steal any focus if the episode loading is too slow (unlikely). */ private fun focusPlayButton() { - binding?.resultPlayMovie?.requestFocus() - binding?.resultPlaySeries?.requestFocus() - binding?.resultResumeSeries?.requestFocus() + binding?.resultPlayMovieButton?.requestFocus() + binding?.resultPlaySeriesButton?.requestFocus() + binding?.resultResumeSeriesButton?.requestFocus() } private fun setRecommendations(rec: List?, validApiName: String?) { @@ -246,37 +246,15 @@ class ResultFragmentTv : Fragment() { storedData.start ) // ===== ===== ===== + var comingSoon = false binding?.apply { //episodesShadow.rotationX = 180.0f//if(episodesShadow.isRtl()) 180.0f else 0.0f - - val leftListener: View.OnFocusChangeListener = - View.OnFocusChangeListener { _, hasFocus -> - if (!hasFocus) return@OnFocusChangeListener - toggleEpisodes(false) - } - val rightListener: View.OnFocusChangeListener = - View.OnFocusChangeListener { _, hasFocus -> - if (!hasFocus) return@OnFocusChangeListener - toggleEpisodes(true) - } - - resultPlayMovie.onFocusChangeListener = leftListener - resultPlaySeries.onFocusChangeListener = leftListener - resultResumeSeries.onFocusChangeListener = leftListener - resultPlayTrailer.onFocusChangeListener = leftListener - resultEpisodesShow.onFocusChangeListener = rightListener - resultDescription.onFocusChangeListener = leftListener - resultBookmarkButton.onFocusChangeListener = leftListener - resultFavoriteButton.onFocusChangeListener = leftListener - resultEpisodesShow.setOnClickListener { - // toggle, to make it more touch accessable just in case someone thinks that a - // tv layout is better but is using a touch device - toggleEpisodes(!episodeHolderTv.isVisible) - } - - // resultEpisodes.onFocusChangeListener = leftListener + // parallax on background + resultFinishLoading.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { view, _, scrollY, _, oldScrollY -> + backgroundPosterHolder.translationY = -scrollY.toFloat() * 0.8f + }) redirectToPlay.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) return@setOnFocusChangeListener @@ -284,13 +262,14 @@ class ResultFragmentTv : Fragment() { binding?.apply { val views = listOf( - resultPlayMovie, - resultPlaySeries, - resultResumeSeries, - resultPlayTrailer, + resultPlayMovieButton, + resultPlaySeriesButton, + resultResumeSeriesButton, + resultPlayTrailerButton, resultBookmarkButton, resultFavoriteButton, - resultSubscribeButton + resultSubscribeButton, + resultSearchButton ) for (requestView in views) { if (!requestView.isVisible) continue @@ -299,11 +278,6 @@ class ResultFragmentTv : Fragment() { } } - // parallax on background - resultFinishLoading.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY -> - backgroundPosterHolder.translationY = -scrollY.toFloat() * 0.8f - }) - redirectToEpisodes.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) return@setOnFocusChangeListener toggleEpisodes(true) @@ -313,7 +287,7 @@ class ResultFragmentTv : Fragment() { resultSeasonSelection, resultRangeSelection, resultEpisodes, - resultPlayTrailer, + resultPlayTrailerButton, ) for (requestView in views) { if (!requestView.isShown) continue @@ -322,6 +296,45 @@ class ResultFragmentTv : Fragment() { } } + mapOf( + resultPlayMovieButton to resultPlayMovieText, + resultPlaySeriesButton to resultPlaySeriesText, + resultResumeSeriesButton to resultResumeSeriesText, + resultPlayTrailerButton to resultPlayTrailerText, + resultBookmarkButton to resultBookmarkText, + resultFavoriteButton to resultFavoriteText, + resultSubscribeButton to resultSubscribeText, + resultSearchButton to resultSearchText, + resultEpisodesShowButton to resultEpisodesShowText + ).forEach { (button , text) -> + + button.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + text.isSelected = false + return@setOnFocusChangeListener + } + + text.isSelected = true + if (button.tag == context?.getString(R.string.tv_no_focus_tag)){ + resultFinishLoading.scrollTo(0,0) + } + when (button.id) { + R.id.result_episodes_show_button -> { + toggleEpisodes(true) + } + else -> { + toggleEpisodes(false) + } + } + } + } + + resultEpisodesShowButton.setOnClickListener { + // toggle, to make it more touch accessible just in case someone thinks that a + // tv layout is better but is using a touch device + toggleEpisodes(!episodeHolderTv.isVisible) + } + resultEpisodes.setLinearListLayout( isHorizontal = false, nextUp = FOCUS_SELF, @@ -430,9 +443,9 @@ class ResultFragmentTv : Fragment() { val aboveCast = listOf( binding?.resultEpisodesShow, - binding?.resultBookmarkButton, - binding?.resultFavoriteButton, - binding?.resultSubscribeButton, + binding?.resultBookmark, + binding?.resultFavorite, + binding?.resultSubscribe, ).firstOrNull { it?.isVisible == true } @@ -443,8 +456,15 @@ class ResultFragmentTv : Fragment() { observeNullable(viewModel.resumeWatching) { resume -> binding?.apply { + + // > resultResumeSeries is visible when not null + if (resume == null) { + resultResumeSeries.isVisible = false + return@observeNullable + } + // show progress no matter if series or movie - resume?.progress?.let { progress -> + resume.progress?.let { progress -> resultResumeSeriesProgressText.setText(progress.progressLeft) resultResumeSeriesProgress.apply { isVisible = true @@ -456,37 +476,24 @@ class ResultFragmentTv : Fragment() { resultResumeProgressHolder.isVisible = false } - // if movie then hide both as movie button is - // always visible on movies, this is done in movie observe - - if (resume?.isMovie == true) { - resultPlaySeries.isVisible = false - resultResumeSeries.isVisible = false - return@observeNullable - } - - // if series then - // > resultPlaySeries is visible when null - // > resultResumeSeries is visible when not null - if (resume == null) { - resultPlaySeries.isVisible = true - resultResumeSeries.isVisible = false - return@observeNullable - } - + resultPlayMovie.isVisible = false resultPlaySeries.isVisible = false resultResumeSeries.isVisible = true focusPlayButton() + // Stops last button right focus if it is a movie + if (resume.isMovie) + resultSearchButton.nextFocusRightId = R.id.result_search_Button - resultResumeSeries.text = - if (resume.isMovie) context?.getString(R.string.play_movie_button) else context?.getNameFull( - null, // resume.result.name, we don't want episode title - resume.result.episode, - resume.result.season - ) + resultResumeSeriesText.text = + when { + resume.isMovie -> context?.getString(R.string.resume) + resume.result.season != null -> + "${getString(R.string.season_short)}${resume.result.season}:${getString(R.string.episode_short)}${resume.result.episode}" + else -> "${getString(R.string.episode)}${resume.result.episode}" + } - resultResumeSeries.setOnClickListener { + resultResumeSeriesButton.setOnClickListener { viewModel.handleAction( EpisodeClickEvent( storedData.playerAction, //?: ACTION_PLAY_EPISODE_IN_PLAYER, @@ -495,7 +502,7 @@ class ResultFragmentTv : Fragment() { ) } - resultResumeSeries.setOnLongClickListener { + resultResumeSeriesButton.setOnLongClickListener { viewModel.handleAction( EpisodeClickEvent(ACTION_SHOW_OPTIONS, resume.result) ) @@ -509,9 +516,9 @@ class ResultFragmentTv : Fragment() { context?.updateHasTrailers() if (!LoadResponse.isTrailersEnabled) return@observe val trailers = trailersLinks.flatMap { it.mirros } - binding?.resultPlayTrailer?.apply { - isGone = trailers.isEmpty() - setOnClickListener { + binding?.apply { + resultPlayTrailer.isGone = trailers.isEmpty() + resultPlayTrailerButton.setOnClickListener { if (trailers.isEmpty()) return@setOnClickListener activity.navigate( R.id.global_to_navigation_player, GeneratorPlayer.newInstance( @@ -526,24 +533,38 @@ class ResultFragmentTv : Fragment() { } observe(viewModel.watchStatus) { watchType -> - binding?.resultBookmarkButton?.apply { - setText(watchType.stringRes) - setOnClickListener { view -> - activity?.showBottomDialog( - WatchType.values().map { view.context.getString(it.stringRes) }.toList(), - watchType.ordinal, - view.context.getString(R.string.action_add_to_bookmarks), - showApply = false, - {}) { - viewModel.updateWatchStatus(WatchType.values()[it], context) + binding?.apply { + resultBookmarkText.setText(watchType.stringRes) + + resultBookmarkButton.apply { + + val drawable = if (watchType.stringRes == R.string.type_none) { + R.drawable.outline_bookmark_add_24 + } else { + R.drawable.ic_baseline_bookmark_24 + } + setIconResource(drawable) + + setOnClickListener { view -> + activity?.showBottomDialog( + WatchType.entries.map { view.context.getString(it.stringRes) }.toList(), + watchType.ordinal, + view.context.getString(R.string.action_add_to_bookmarks), + showApply = false, + {}) { + viewModel.updateWatchStatus(WatchType.entries[it], context) + } } } } } observeNullable(viewModel.favoriteStatus) { isFavorite -> + + binding?.resultFavorite?.isVisible = isFavorite != null + binding?.resultFavoriteButton?.apply { - isVisible = isFavorite != null + if (isFavorite == null) return@observeNullable val drawable = if (isFavorite) { @@ -552,14 +573,8 @@ class ResultFragmentTv : Fragment() { R.drawable.ic_baseline_favorite_border_24 } - val text = if (isFavorite) { - R.string.action_remove_from_favorites - } else { - R.string.action_add_to_favorites - } - setIconResource(drawable) - setText(text) + setOnClickListener { viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? -> if (newStatus == null) return@toggleFavoriteStatus @@ -576,11 +591,21 @@ class ResultFragmentTv : Fragment() { } } } + + binding?.resultFavoriteText?.apply { + val text = if (isFavorite == true) { + R.string.unfavorite + } else { + R.string.favorite + } + setText(text) + } } observeNullable(viewModel.subscribeStatus) { isSubscribed -> + binding?.resultSubscribe?.isVisible = isSubscribed != null && requireContext().isEmulatorSettings() binding?.resultSubscribeButton?.apply { - isVisible = isSubscribed != null && context.isEmulatorSettings() + if (isSubscribed == null) return@observeNullable val drawable = if (isSubscribed) { @@ -589,14 +614,8 @@ class ResultFragmentTv : Fragment() { R.drawable.baseline_notifications_none_24 } - val text = if (isSubscribed) { - R.string.action_unsubscribe - } else { - R.string.action_subscribe - } - setIconResource(drawable) - setText(text) + setOnClickListener { viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? -> if (newStatus == null) return@toggleSubscriptionStatus @@ -614,32 +633,47 @@ class ResultFragmentTv : Fragment() { CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) } } + + binding?.resultSubscribeText?.apply { + val text = if (isSubscribed) { + R.string.action_unsubscribe + } else { + R.string.action_subscribe + } + setText(text) + } } } observeNullable(viewModel.movie) { data -> + if (data == null) return@observeNullable + binding?.apply { - resultPlayMovie.isVisible = data is Resource.Success - resultPlaySeries.isVisible = data == null - seriesHolder.isVisible = data == null - resultEpisodesShow.isVisible = data == null + resultPlayMovie.isVisible = (data is Resource.Success) && !comingSoon + resultPlaySeries.isVisible = false + resultEpisodesShow.isVisible = false (data as? Resource.Success)?.value?.let { (text, ep) -> - resultPlayMovie.setText(text) - resultPlayMovie.setOnClickListener { + //resultPlayMovieText.setText(text) + resultPlayMovieButton.setOnClickListener { viewModel.handleAction( EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep) ) } - resultPlayMovie.setOnLongClickListener { + resultPlayMovieButton.setOnLongClickListener { viewModel.handleAction( EpisodeClickEvent(ACTION_SHOW_OPTIONS, ep) ) return@setOnLongClickListener true } - focusPlayButton() + //focusPlayButton() + resultPlayMovieButton.requestFocus() + + // Stops last button right focus + resultSearchButton.nextFocusRightId = R.id.result_search_Button } } + //focusPlayButton() } observeNullable(viewModel.selectPopup) { popup -> @@ -736,19 +770,26 @@ class ResultFragmentTv : Fragment() { // Used to request focus the first time the episodes are loaded. var hasLoadedEpisodesOnce = false observeNullable(viewModel.episodes) { episodes -> + if (episodes == null) return@observeNullable + binding?.apply { - resultEpisodes.isVisible = episodes is Resource.Success + + resultPlayMovie.isVisible = false + resultPlaySeries.isVisible = true && !comingSoon + resultEpisodes.isVisible = true && !comingSoon + resultEpisodesShow.isVisible = true && !comingSoon + // resultEpisodeLoading.isVisible = episodes is Resource.Loading if (episodes is Resource.Success) { val first = episodes.value.firstOrNull() if (first != null) { - resultPlaySeries.text = context?.getNameFull( - null, // resume.result.name, we don't want episode title - first.episode, - first.season - ) - - resultPlaySeries.setOnClickListener { + resultPlaySeriesText.text = //"${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}" + when { + first.season != null -> + "${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}" + else -> "${getString(R.string.episode)} ${first.episode}" + } + resultPlaySeriesButton.setOnClickListener { viewModel.handleAction( EpisodeClickEvent( ACTION_CLICK_DEFAULT, @@ -756,7 +797,7 @@ class ResultFragmentTv : Fragment() { ) ) } - resultPlaySeries.setOnLongClickListener { + resultPlaySeriesButton.setOnLongClickListener { viewModel.handleAction( EpisodeClickEvent(ACTION_SHOW_OPTIONS, first) ) @@ -765,6 +806,7 @@ class ResultFragmentTv : Fragment() { if (!hasLoadedEpisodesOnce) { hasLoadedEpisodesOnce = true focusPlayButton() + resultPlaySeries.requestFocus() } } @@ -826,6 +868,7 @@ class ResultFragmentTv : Fragment() { resultMetaYear.setText(d.yearText) resultMetaDuration.setText(d.durationText) resultMetaRating.setText(d.ratingText) + resultMetaStatus.setText(d.onGoingText) resultMetaContentRating.setText(d.contentRatingText) resultCastText.setText(d.actorsText) resultNextAiring.setText(d.nextAiringEpisode) @@ -859,8 +902,12 @@ class ResultFragmentTv : Fragment() { radius = 0, errorImageDrawable = error ) - resultComingSoon.isVisible = d.comingSoon + comingSoon = d.comingSoon + resultTvComingSoon.isVisible = d.comingSoon + resultPlayMovie.isGone = d.comingSoon + resultPlaySeries.isGone = d.comingSoon resultDataHolder.isGone = d.comingSoon + UIHelper.populateChips(resultTag, d.tags) resultCastItems.isGone = d.actors.isNullOrEmpty() (resultCastItems.adapter as? ActorAdaptor)?.updateList( @@ -871,6 +918,10 @@ class ResultFragmentTv : Fragment() { // If there is no rating to display, we don't want an empty gap resultMetaContentRating.width = 0 } + + resultSearchButton.setOnClickListener { + QuickSearchFragment.pushSearch(activity, d.title) + } } is Resource.Loading -> { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index c24efe56..a05b4059 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -20,6 +20,7 @@ import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.getCastSession @@ -31,6 +32,7 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie import com.lagradost.cloudstream3.metaproviders.SyncRedirector import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.syncproviders.AccountManager +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.secondsToReadable import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.syncproviders.providers.SimklApi @@ -261,8 +263,7 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { metaText = if (repo.providerType == ProviderType.MetaProvider) txt(R.string.provider_info_meta) else null, durationText = if (dur == null || dur <= 0) null else txt( - R.string.duration_format, - dur + secondsToReadable(dur * 60, "0 mins") ), onGoingText = if (this is EpisodeResponse) { txt( @@ -2464,7 +2465,7 @@ class ResultViewModel2 : ViewModel() { ResumeProgress( progress = (viewPos.position / 1000).toInt(), maxProgress = (viewPos.duration / 1000).toInt(), - txt(R.string.resume_time_left, (viewPos.duration - viewPos.position) / (60_000)) + txt(R.string.resume_remaining, secondsToReadable(((viewPos.duration - viewPos.position) / 1_000).toInt(), "0 mins")) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index aa5a3182..14f11c1e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -11,8 +11,10 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser +import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.AccountManagmentBinding @@ -32,7 +34,10 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSet import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar +import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage @@ -256,6 +261,24 @@ class SettingsAccount : PreferenceFragmentCompat() { hideKeyboard() setPreferencesFromResource(R.xml.settings_account, rootKey) + getPref(R.string.biometric_key)?.setOnPreferenceClickListener { + val authEnabled = PreferenceManager.getDefaultSharedPreferences( + context ?: return@setOnPreferenceClickListener false + ) + .getBoolean(getString(R.string.biometric_key), false) + + if (authEnabled) { + BackupUtils.backup(activity) + val title = activity?.getString(R.string.biometric_setting) + val warning = activity?.getString(R.string.biometric_warning) + activity?.showBottomDialogText( + title as String, + warning.html() + ) { onDialogDismissedEvent } + } + true + } + val syncApis = listOf( R.string.mal_key to malApi, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 37c71134..8dedd896 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -19,6 +19,7 @@ import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar +import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.mvvm.logError @@ -155,6 +156,11 @@ class SettingsFragment : Fragment() { return getLayoutInt() == 2 } + // phone exclusive + fun isTruePhone(): Boolean { + return !isTrueTvSettings() && !isTvSettings() && context?.isEmulatorSettings() != true + } + private fun Context.isAutoTv(): Boolean { val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager? // AFT = Fire TV diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index e50131fe..d3c97cd1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -65,12 +65,16 @@ object BackupUtils { PLUGINS_KEY_LOCAL, OPEN_SUBTITLES_USER_KEY, - "nginx_user", // Nginx user key + + DOWNLOAD_EPISODE_CACHE, + + "biometric_key", // can lock down users if backup is shared on a incompatible device + "nginx_user" // Nginx user key ) - /** false if blacklisted key */ + /** false if key should not be contained in backup */ private fun String.isTransferable(): Boolean { - return !nonTransferableKeys.contains(this) + return !nonTransferableKeys.any { this.contains(it) } } private var restoreFileSelector: ActivityResultLauncher>? = null @@ -256,4 +260,4 @@ object BackupUtils { setKeyRaw(it.key, it.value, isEditingAppSettings) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt new file mode 100644 index 00000000..de9b9963 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt @@ -0,0 +1,177 @@ +package com.lagradost.cloudstream3.utils + +import android.app.Activity +import android.app.KeyguardManager +import android.content.Context +import android.os.Build +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.R + +object BiometricAuthenticator { + + private const val MAX_FAILED_ATTEMPTS = 3 + private var failedAttempts = 0 + const val TAG = "cs3Auth" + + private var biometricManager: BiometricManager? = null + var biometricPrompt: BiometricPrompt? = null + var promptInfo: BiometricPrompt.PromptInfo? = null + + var authCallback: BiometricAuthCallback? = null // listen to authentication success + + private fun initializeBiometrics(activity: Activity) { + val executor = ContextCompat.getMainExecutor(activity) + + biometricManager = BiometricManager.from(activity) + + biometricPrompt = BiometricPrompt( + activity as FragmentActivity, + executor, + object : BiometricPrompt.AuthenticationCallback() { + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + showToast("$errString") + Log.e(TAG, "$errorCode") + failedAttempts++ + + if (failedAttempts >= MAX_FAILED_ATTEMPTS) { + failedAttempts = 0 + activity.finish() + } else { + failedAttempts = 0 + activity.finish() + } + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + failedAttempts = 0 + authCallback?.onAuthenticationSuccess() + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + failedAttempts++ + if (failedAttempts >= MAX_FAILED_ATTEMPTS) { + failedAttempts = 0 + activity.finish() + } + } + }) + } + + @Suppress("DEPRECATION") + // authentication dialog prompt builder + private fun authenticationDialog( + activity: Activity, + title: Int, + setDeviceCred: Boolean, + ) { + val description = activity.getString(R.string.biometric_prompt_description) + + if (setDeviceCred) { + // For API level > 30, Newer API setAllowedAuthenticators is used + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + + val authFlag = DEVICE_CREDENTIAL or BIOMETRIC_WEAK or BIOMETRIC_STRONG + promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(activity.getString(title)) + .setDescription(description) + .setAllowedAuthenticators(authFlag) + .build() + + } else { + // for apis < 30 + promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(activity.getString(title)) + .setDescription(description) + .setDeviceCredentialAllowed(true) + .build() + } + + } else { + // fallback for A12+ when both fingerprint & Face unlock is absent but PIN is set + promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(activity.getString(title)) + .setDescription(description) + .setDeviceCredentialAllowed(true) + .build() + } + } + + private fun isBiometricHardWareAvailable(): Boolean { + // authentication occurs only when this is true and device is truly capable + var result = false + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + + when (biometricManager?.canAuthenticate( + DEVICE_CREDENTIAL or BIOMETRIC_STRONG or BIOMETRIC_WEAK + )) { + BiometricManager.BIOMETRIC_SUCCESS -> result = true + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> result = false + BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> result = false + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> result = false + BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> result = true + BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> result = true + BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> result = false + } + + } else { + @Suppress("DEPRECATION") + when (biometricManager?.canAuthenticate()) { + BiometricManager.BIOMETRIC_SUCCESS -> result = true + BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> result = false + BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> result = false + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> result = false + BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> result = true + BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> result = true + BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> result = false + } + } + + return result + } + + // checks if device is secured i.e has at least some type of lock + fun deviceHasPasswordPinLock(context: Context?): Boolean { + val keyMgr = + context?.getSystemService(AppCompatActivity.KEYGUARD_SERVICE) as? KeyguardManager + return keyMgr?.isKeyguardSecure ?: false + } + + // function to start authentication in any fragment or activity + fun startBiometricAuthentication(activity: Activity, title: Int, setDeviceCred: Boolean) { + initializeBiometrics(activity) + + if (isBiometricHardWareAvailable()) { + authCallback = activity as? BiometricAuthCallback + authenticationDialog(activity, title, setDeviceCred) + promptInfo?.let { biometricPrompt?.authenticate(it) } + + } else { + if (deviceHasPasswordPinLock(activity)) { + authCallback = activity as? BiometricAuthCallback + authenticationDialog(activity, R.string.password_pin_authentication_title, true) + promptInfo?.let { biometricPrompt?.authenticate(it) } + + } else { + showToast(R.string.biometric_unsupported) + } + } + } + + interface BiometricAuthCallback { + fun onAuthenticationSuccess() + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index 76142f72..6e925d15 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -532,7 +532,6 @@ object UIHelper { WindowInsetsControllerCompat(window, View(this)).show(WindowInsetsCompat.Type.systemBars()) } else {*/ /** WINDOW COMPAT IS BUGGY DUE TO FU*KED UP PLAYER AND TRAILERS **/ - Suppress("DEPRECATION") window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) //} diff --git a/app/src/main/res/drawable/ic_baseline_film_roll_24.xml b/app/src/main/res/drawable/ic_baseline_film_roll_24.xml new file mode 100644 index 00000000..941d936f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_film_roll_24.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_resume_arrow.xml b/app/src/main/res/drawable/ic_baseline_resume_arrow.xml new file mode 100644 index 00000000..0326fbd4 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_resume_arrow.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_resume_arrow2.xml b/app/src/main/res/drawable/ic_baseline_resume_arrow2.xml new file mode 100644 index 00000000..fc533a0e --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_resume_arrow2.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_fingerprint.xml b/app/src/main/res/drawable/ic_fingerprint.xml new file mode 100644 index 00000000..5c96e5a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_fingerprint.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/library_icon.xml b/app/src/main/res/drawable/library_icon.xml new file mode 100644 index 00000000..f62dceac --- /dev/null +++ b/app/src/main/res/drawable/library_icon.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/outline_bookmark_add_24.xml b/app/src/main/res/drawable/outline_bookmark_add_24.xml new file mode 100644 index 00000000..a4e18af3 --- /dev/null +++ b/app/src/main/res/drawable/outline_bookmark_add_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/player_button_tv_attr.xml b/app/src/main/res/drawable/player_button_tv_attr.xml index 4c90a64e..ed83887d 100644 --- a/app/src/main/res/drawable/player_button_tv_attr.xml +++ b/app/src/main/res/drawable/player_button_tv_attr.xml @@ -3,13 +3,13 @@ - + - + \ No newline at end of file diff --git a/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml b/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml index b9b927da..0dd8c256 100644 --- a/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml +++ b/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml @@ -3,7 +3,7 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/cast_item.xml b/app/src/main/res/layout/cast_item.xml index f164384b..99a9750b 100644 --- a/app/src/main/res/layout/cast_item.xml +++ b/app/src/main/res/layout/cast_item.xml @@ -17,7 +17,6 @@ android:layout_width="100dp" android:layout_height="wrap_content" android:orientation="vertical" - android:focusable="true" android:padding="5dp"> diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index 70461518..045b9cc2 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -412,7 +412,7 @@ android:foreground="@drawable/outline_drawable" android:maxLength="1000" android:nextFocusUp="@id/result_back" - android:nextFocusDown="@id/result_bookmark_button" + android:nextFocusDown="@id/result_bookmark_Button" android:paddingTop="5dp" android:textColor="?attr/textColor" android:textSize="15sp" @@ -474,7 +474,7 @@ android:fadingEdge="horizontal" android:focusable="false" android:focusableInTouchMode="false" - android:nextFocusUp="@id/result_bookmark_button" + android:nextFocusUp="@id/result_bookmark_Button" android:nextFocusDown="@id/result_play_movie" android:orientation="horizontal" android:paddingTop="5dp" @@ -580,7 +580,7 @@ android:layout_marginStart="0dp" android:layout_marginEnd="0dp" android:layout_marginBottom="10dp" - android:nextFocusUp="@id/result_bookmark_button" + android:nextFocusUp="@id/result_bookmark_Button" android:nextFocusDown="@id/result_download_movie" android:text="@string/play_movie_button" android:visibility="visible" @@ -658,7 +658,7 @@ android:layout_marginStart="0dp" android:layout_marginEnd="0dp" android:layout_marginBottom="10dp" - android:nextFocusUp="@id/result_bookmark_button" + android:nextFocusUp="@id/result_bookmark_Button" android:nextFocusDown="@id/result_download_movie" android:text="@string/resume" android:visibility="visible" @@ -674,7 +674,7 @@ android:layout_marginStart="0dp" android:layout_marginEnd="0dp" android:layout_marginBottom="10dp" - android:nextFocusUp="@id/result_bookmark_button" + android:nextFocusUp="@id/result_bookmark_Button" android:nextFocusDown="@id/result_download_movie" android:text="@string/next_episode" android:visibility="gone" diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index a7ba4334..ba8b728e 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -78,6 +78,30 @@ https://developer.android.com/design/ui/tv/samples/jet-fit + + + + + + + + + - - - - - - - - - - + android:layout_marginTop="225dp"> @@ -221,157 +220,289 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:textStyle="normal" tools:text="5d 3h 30m" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_marginTop="10dp" + android:baselineAligned="false"> + app:layout_constraintTop_toTopOf="parent" + tools:ignore="UselessParent"> - - - - - - - - - - - - - @@ -513,10 +594,8 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:descendantFocusability="afterDescendants" android:fadingEdge="horizontal" - android:focusable="false" - android:focusableInTouchMode="false" - android:nextFocusUp="@id/result_episodes_show" - android:nextFocusDown="@id/result_recommendations_filter_selection" + android:nextFocusUp="@id/result_description" + android:nextFocusDown="@id/result_recommendations_list" android:orientation="horizontal" android:paddingTop="5dp" android:requiresFadingEdge="horizontal" @@ -525,8 +604,23 @@ https://developer.android.com/design/ui/tv/samples/jet-fit tools:listitem="@layout/cast_item" tools:visibility="visible" /> + + @@ -576,7 +670,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:layout_height="match_parent" android:layout_gravity="end" android:visibility="gone" - tools:visibility="visible"> + tools:visibility="invisible"> + - + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_custom_layout_tv.xml b/app/src/main/res/layout/player_custom_layout_tv.xml index 89355a72..d8406b35 100644 --- a/app/src/main/res/layout/player_custom_layout_tv.xml +++ b/app/src/main/res/layout/player_custom_layout_tv.xml @@ -533,18 +533,14 @@ android:id="@id/exo_position" android:layout_width="wrap_content" android:layout_height="30dp" - android:layout_gravity="center" - android:layout_marginStart="20dp" - android:gravity="end|center_vertical" + android:gravity="center" android:includeFontPadding="false" android:minWidth="50dp" - android:paddingLeft="4dp" - android:paddingRight="4dp" android:textColor="@android:color/white" android:textSize="14sp" android:textStyle="normal" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="@id/player_pause_play" + app:layout_constraintStart_toEndOf="@id/player_pause_play" tools:text="15:30" /> Banana Fiesta Dolor rosa + Lavanda Material You Material You (Secondary) @@ -235,6 +236,7 @@ Banana Party Pink + Lavender Monet Monet2 diff --git a/app/src/main/res/values-pl/array.xml b/app/src/main/res/values-pl/array.xml index 8384187f..9f76f423 100644 --- a/app/src/main/res/values-pl/array.xml +++ b/app/src/main/res/values-pl/array.xml @@ -221,6 +221,7 @@ Bananowy Łososiowy Świnko peppowy + Lawenda Material You Material You (drugorzędny) @@ -244,6 +245,7 @@ Banana Party Pink + Lavender Monet Monet2 diff --git a/app/src/main/res/values-tr/array.xml b/app/src/main/res/values-tr/array.xml index d14a3e2a..5c723f72 100644 --- a/app/src/main/res/values-tr/array.xml +++ b/app/src/main/res/values-tr/array.xml @@ -247,6 +247,7 @@ Muz Parti Pembe + Lavanta Material You Material You (İkincil) @@ -270,6 +271,7 @@ Banana Party Pink + Lavender Monet Monet2 diff --git a/app/src/main/res/values-vi/array.xml b/app/src/main/res/values-vi/array.xml index d32f37ce..aac94100 100644 --- a/app/src/main/res/values-vi/array.xml +++ b/app/src/main/res/values-vi/array.xml @@ -213,6 +213,7 @@ Vàng Hồng Hồng đậm + Hoa oải hương Material You Material You (Secondary) @@ -236,6 +237,7 @@ Banana Party Pink + Lavender Monet Monet2 diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index e38dd5c9..3be12510 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -284,6 +284,7 @@ Banana Party Pink Pain + Lavender Material You Material You (Secondary) @@ -307,6 +308,7 @@ Banana Party Pink + Lavender Monet Monet2 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c2c84d0d..7c9ccebe 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -84,6 +84,7 @@ #CE8500 #F5BB00 #408cac + #6F55AF #48E484 #ea596e diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 14bb9552..b4f9d14d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -69,6 +69,7 @@ enable_skip_op_from_database rotate_video_key auto_rotate_video_key + biometric_key %d %s | %s %s • %s @@ -247,7 +248,7 @@ Error backing up %s Search Library - Accounts + Accounts and Security Updates and backup Info Advanced Search @@ -306,6 +307,7 @@ +30 This will permanently delete %s\nAre you sure? %dm\nremaining + %s\nremaining Ongoing Completed Status @@ -745,4 +747,17 @@ Display a toggle button for screen orientation Enable automatic switching of screen orientation based on video orientation Auto rotate + Favorite + Unfavorite + + Unlock CloudStream + Lock with Biometrics + Password/PIN Authentication + Biometric authentication is not supported on this device + Unlock the app with Fingerprint, Face ID, PIN, Pattern and Password. + This window will close after few failed attempts. You\'ll have to restart the App. + Your CloudStream data has been backed up now, although probability of this rare case is very low but all + devices behave differently, in case you get locked down from accessing the app in worst case scenario, + Clear the app data wholly and restore the backup. Any inconvenience if arrived is deeply regretted. + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 2fa4eb41..0a693769 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -383,6 +383,16 @@ @color/colorPrimaryCoolBlue + + + + + +