diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 83100db8..31e225de 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 759f99d4..80de223e 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
@@ -319,6 +319,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..fa6cae18 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
@@ -19,6 +19,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.Toast
+import android.widget.Toast.LENGTH_LONG
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IdRes
@@ -28,6 +29,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 +114,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 +134,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 +173,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 +291,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 +1177,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 +1190,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 +1226,25 @@ 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
+ } else {
+ showToast(R.string.phone_not_secured, LENGTH_LONG)
+ }
+ }
+
// Automatically enable jsdelivr if cant connect to raw.githubusercontent.com
if (this.getKey(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) {
main {
@@ -1743,6 +1784,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..ccaa38f0 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,8 @@ package com.lagradost.cloudstream3.ui.account
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
+import android.util.Log
+import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
@@ -17,13 +19,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 +47,34 @@ 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)
+ }
+ }
+ } else {
+ showToast(R.string.phone_not_secured, Toast.LENGTH_LONG)
+ }
+ }
+
+ 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 +82,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 +109,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 +173,8 @@ class AccountSelectActivity : AppCompatActivity() {
} else 6
}
}
+
+ askBiometricAuth()
}
private fun navigateToMainActivity() {
@@ -165,4 +182,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 3801ea21..bd7105ee 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,6 +33,7 @@ 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
@@ -40,7 +41,6 @@ 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.ui.settings.SettingsFragment.Companion.isTrueTvSettings
-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
@@ -130,9 +130,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?) {
@@ -247,37 +247,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
@@ -285,13 +263,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
@@ -300,11 +279,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)
@@ -314,7 +288,7 @@ class ResultFragmentTv : Fragment() {
resultSeasonSelection,
resultRangeSelection,
resultEpisodes,
- resultPlayTrailer,
+ resultPlayTrailerButton,
)
for (requestView in views) {
if (!requestView.isShown) continue
@@ -323,6 +297,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,
@@ -431,9 +444,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
}
@@ -444,8 +457,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
@@ -457,37 +477,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,
@@ -496,7 +503,7 @@ class ResultFragmentTv : Fragment() {
)
}
- resultResumeSeries.setOnLongClickListener {
+ resultResumeSeriesButton.setOnLongClickListener {
viewModel.handleAction(
EpisodeClickEvent(ACTION_SHOW_OPTIONS, resume.result)
)
@@ -510,9 +517,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(
@@ -527,24 +534,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) {
@@ -553,14 +574,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
@@ -577,11 +592,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) {
@@ -590,14 +615,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
@@ -615,32 +634,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 ->
@@ -740,19 +774,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,
@@ -760,7 +801,7 @@ class ResultFragmentTv : Fragment() {
)
)
}
- resultPlaySeries.setOnLongClickListener {
+ resultPlaySeriesButton.setOnLongClickListener {
viewModel.handleAction(
EpisodeClickEvent(ACTION_SHOW_OPTIONS, first)
)
@@ -769,6 +810,7 @@ class ResultFragmentTv : Fragment() {
if (!hasLoadedEpisodesOnce) {
hasLoadedEpisodesOnce = true
focusPlayButton()
+ resultPlaySeries.requestFocus()
}
}
@@ -830,6 +872,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)
@@ -874,8 +917,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(
@@ -886,6 +933,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..d04757da 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
@@ -13,6 +13,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceFragmentCompat
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 +33,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 +260,19 @@ class SettingsAccount : PreferenceFragmentCompat() {
hideKeyboard()
setPreferencesFromResource(R.xml.settings_account, rootKey)
+ getPref(R.string.biometric_key)?.setOnPreferenceClickListener {
+
+ 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..db001ef5 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt
@@ -66,6 +66,7 @@ object BackupUtils {
OPEN_SUBTITLES_USER_KEY,
"nginx_user", // Nginx user key
+ "biometric_key" // can lock down users if backup is shared on a incompatible device
)
/** false if blacklisted key */
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 961ea52d..22d2e52f 100644
--- a/app/src/main/res/layout/fragment_result.xml
+++ b/app/src/main/res/layout/fragment_result.xml
@@ -412,7 +412,7 @@
android:maxLines="10"
android:foreground="@drawable/outline_drawable"
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..d7d19ddf 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -247,7 +247,7 @@
Error backing up %s
Search
Library
- Accounts
+ Accounts and Security
Updates and backup
Info
Advanced Search
@@ -306,6 +306,7 @@
+30
This will permanently delete %s\nAre you sure?
%dm\nremaining
+ %s\nremaining
Ongoing
Completed
Status
@@ -745,4 +746,19 @@
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
+ biometric_key
+ Password/PIN Authentication
+ Biometric authentication is not supported on this device
+ Please disable fingerprint authentication if device has no lock.
+ 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
+
+
+
+
+
+