Merge remote-tracking branch 'origin/master' into bread_toast

This commit is contained in:
IndusAryan 2024-03-12 21:03:45 +05:30
commit 7dfd9c454c
38 changed files with 1004 additions and 623 deletions

View file

@ -212,6 +212,7 @@ dependencies {
implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors
implementation("androidx.tvprovider:tvprovider:1.0.0") implementation("androidx.tvprovider:tvprovider:1.0.0")
implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures 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 implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview
// Extensions & Other Libs // Extensions & Other Libs

View file

@ -14,7 +14,7 @@
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next --> <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next -->
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt --> <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- Required for getting arbitrary Aniyomi packages --> <!-- Required for getting arbitrary Aniyomi packages -->

View file

@ -308,6 +308,7 @@ object CommonActivity {
"Banana" -> R.style.OverlayPrimaryColorBanana "Banana" -> R.style.OverlayPrimaryColorBanana
"Party" -> R.style.OverlayPrimaryColorParty "Party" -> R.style.OverlayPrimaryColorParty
"Pink" -> R.style.OverlayPrimaryColorPink "Pink" -> R.style.OverlayPrimaryColorPink
"Lavender" -> R.style.OverlayPrimaryColorLavender
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal

View file

@ -28,6 +28,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.marginStart import androidx.core.view.marginStart
import androidx.fragment.app.FragmentActivity 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.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings 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.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv 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.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.backup
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup 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.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper 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.DataStoreHelper.migrateResumeWatching
import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
@ -166,7 +172,6 @@ import kotlin.math.absoluteValue
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.system.exitProcess 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://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/ //https://wiki.videolan.org/Android_Player_Intents/
@ -285,7 +290,7 @@ var app = Requests(responseParser = object : ResponseParser {
defaultHeaders = mapOf("user-agent" to USER_AGENT) defaultHeaders = mapOf("user-agent" to USER_AGENT)
} }
class MainActivity : AppCompatActivity(), ColorPickerDialogListener { class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAuthenticator.BiometricAuthCallback {
companion object { companion object {
const val TAG = "MAINACT" const val TAG = "MAINACT"
const val ANIMATED_OUTLINE: Boolean = false const val ANIMATED_OUTLINE: Boolean = false
@ -1171,7 +1176,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false) val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false)
setContentView(newLocalBinding.root) setContentView(newLocalBinding.root)
if(isTrueTvSettings() && ANIMATED_OUTLINE) { if (isTrueTvSettings() && ANIMATED_OUTLINE) {
TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
@ -1184,13 +1189,29 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
if(isTrueTvSettings()) { 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 -> newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
if (exceptionButtons.contains(newFocus?.id)) return@addOnGlobalFocusChangeListener
centerView(newFocus) centerView(newFocus)
} }
} }
ActivityMainBinding.bind(newLocalBinding.root) // this may crash ActivityMainBinding.bind(newLocalBinding.root) // this may crash
} else { } else {
val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false) val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false)
@ -1204,6 +1225,23 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
changeStatusBarState(isEmulatorSettings()) 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 // Automatically enable jsdelivr if cant connect to raw.githubusercontent.com
if (this.getKey<Boolean>(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) { if (this.getKey<Boolean>(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) {
main { 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 var backPressedCallback: OnBackPressedCallback? = null
private fun attachBackPressedCallback() { private fun attachBackPressedCallback() {

View file

@ -49,11 +49,15 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) {
} }
} }
/** NOTE: Only one observer at a time per value */
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) { fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.removeObservers(this)
liveData.observe(this) { it?.let { t -> action(t) } } liveData.observe(this) { it?.let { t -> action(t) } }
} }
/** NOTE: Only one observer at a time per value */
fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) { fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.removeObservers(this)
liveData.observe(this) { action(it) } liveData.observe(this) { action(it) }
} }

View file

@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui.account
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager 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.AutofitRecyclerView
import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_EDIT_ACCOUNT 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.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.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.accounts
import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex
import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
class AccountSelectActivity : AppCompatActivity() { class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.BiometricAuthCallback {
lateinit var viewModel: AccountViewModel lateinit var viewModel: AccountViewModel
@ -41,13 +46,36 @@ class AccountSelectActivity : AppCompatActivity() {
) )
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val skipStartup = settingsManager.getBoolean( val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false)
getString(R.string.skip_startup_account_select_key), val skipStartup = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false
false
) || accounts.count() <= 1 ) || accounts.count() <= 1
viewModel = ViewModelProvider(this)[AccountViewModel::class.java] 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 // Don't show account selection if there is only
// one account that exists // one account that exists
if (!isEditingFromMainActivity && skipStartup) { if (!isEditingFromMainActivity && skipStartup) {
@ -55,12 +83,6 @@ class AccountSelectActivity : AppCompatActivity() {
if (currentAccount?.lockPin != null) { if (currentAccount?.lockPin != null) {
CommonActivity.init(this) CommonActivity.init(this)
viewModel.handleAccountSelect(currentAccount, this, true) viewModel.handleAccountSelect(currentAccount, this, true)
observe(viewModel.isAllowedLogin) { isAllowedLogin ->
if (isAllowedLogin) {
// We are allowed to continue to MainActivity
navigateToMainActivity()
}
}
} else { } else {
if (accounts.count() > 1) { if (accounts.count() > 1) {
showToast(this, getString( showToast(this, getString(
@ -88,12 +110,6 @@ class AccountSelectActivity : AppCompatActivity() {
// Handle the selected account // Handle the selected account
accountSelectCallback = { accountSelectCallback = {
viewModel.handleAccountSelect(it, this) viewModel.handleAccountSelect(it, this)
observe(viewModel.isAllowedLogin) { isAllowedLogin ->
if (isAllowedLogin) {
// We are allowed to continue to MainActivity
navigateToMainActivity()
}
}
}, },
accountCreateCallback = { viewModel.handleAccountUpdate(it, this) }, accountCreateCallback = { viewModel.handleAccountUpdate(it, this) },
accountEditCallback = { accountEditCallback = {
@ -158,6 +174,8 @@ class AccountSelectActivity : AppCompatActivity() {
} else 6 } else 6
} }
} }
askBiometricAuth()
} }
private fun navigateToMainActivity() { private fun navigateToMainActivity() {
@ -165,4 +183,8 @@ class AccountSelectActivity : AppCompatActivity() {
startActivity(mainIntent) startActivity(mainIntent)
finish() // Finish the account selection activity finish() // Finish the account selection activity
} }
override fun onAuthenticationSuccess() {
Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity")
}
} }

View file

@ -529,6 +529,7 @@ class HomeFragment : Fragment() {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
} }
}) })
} }

View file

@ -131,6 +131,18 @@ class LibraryFragment : Fragment() {
super.onSaveInstanceState(outState) 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") @SuppressLint("ResourceType", "CutPasteId")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -395,15 +407,7 @@ class LibraryFragment : Fragment() {
binding?.viewpager?.setCurrentItem(page, false) binding?.viewpager?.setCurrentItem(page, false)
} }
observe(libraryViewModel.currentPage){ updateRandom()
if (toggleRandomButton) {
listLibraryItems.clear()
listLibraryItems.addAll(pages[it].items)
libraryRandom.isVisible = listLibraryItems.isNotEmpty()
} else {
libraryRandom.isGone = true
}
}
// Only stop loading after 300ms to hide the fade effect the viewpager produces when updating // Only stop loading after 300ms to hide the fade effect the viewpager produces when updating
// Without this there would be a flashing effect: // Without this there would be a flashing effect:
@ -481,6 +485,7 @@ class LibraryFragment : Fragment() {
} }
observe(libraryViewModel.currentPage) { position -> observe(libraryViewModel.currentPage) { position ->
updateRandom()
val all = binding?.viewpager?.allViews?.toList() val all = binding?.viewpager?.allViews?.toList()
?.filterIsInstance<AutofitRecyclerView>() ?.filterIsInstance<AutofitRecyclerView>()

View file

@ -33,13 +33,13 @@ import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup
import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer 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.getStoredData
import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings 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.html
import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.AppUtils.isRtl
import com.lagradost.cloudstream3.utils.AppUtils.loadCache 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). * Note that this will steal any focus if the episode loading is too slow (unlikely).
*/ */
private fun focusPlayButton() { private fun focusPlayButton() {
binding?.resultPlayMovie?.requestFocus() binding?.resultPlayMovieButton?.requestFocus()
binding?.resultPlaySeries?.requestFocus() binding?.resultPlaySeriesButton?.requestFocus()
binding?.resultResumeSeries?.requestFocus() binding?.resultResumeSeriesButton?.requestFocus()
} }
private fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) { private fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
@ -246,37 +246,15 @@ class ResultFragmentTv : Fragment() {
storedData.start storedData.start
) )
// ===== ===== ===== // ===== ===== =====
var comingSoon = false
binding?.apply { binding?.apply {
//episodesShadow.rotationX = 180.0f//if(episodesShadow.isRtl()) 180.0f else 0.0f //episodesShadow.rotationX = 180.0f//if(episodesShadow.isRtl()) 180.0f else 0.0f
val leftListener: View.OnFocusChangeListener = // parallax on background
View.OnFocusChangeListener { _, hasFocus -> resultFinishLoading.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { view, _, scrollY, _, oldScrollY ->
if (!hasFocus) return@OnFocusChangeListener backgroundPosterHolder.translationY = -scrollY.toFloat() * 0.8f
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
redirectToPlay.setOnFocusChangeListener { _, hasFocus -> redirectToPlay.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) return@setOnFocusChangeListener if (!hasFocus) return@setOnFocusChangeListener
@ -284,13 +262,14 @@ class ResultFragmentTv : Fragment() {
binding?.apply { binding?.apply {
val views = listOf( val views = listOf(
resultPlayMovie, resultPlayMovieButton,
resultPlaySeries, resultPlaySeriesButton,
resultResumeSeries, resultResumeSeriesButton,
resultPlayTrailer, resultPlayTrailerButton,
resultBookmarkButton, resultBookmarkButton,
resultFavoriteButton, resultFavoriteButton,
resultSubscribeButton resultSubscribeButton,
resultSearchButton
) )
for (requestView in views) { for (requestView in views) {
if (!requestView.isVisible) continue 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 -> redirectToEpisodes.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) return@setOnFocusChangeListener if (!hasFocus) return@setOnFocusChangeListener
toggleEpisodes(true) toggleEpisodes(true)
@ -313,7 +287,7 @@ class ResultFragmentTv : Fragment() {
resultSeasonSelection, resultSeasonSelection,
resultRangeSelection, resultRangeSelection,
resultEpisodes, resultEpisodes,
resultPlayTrailer, resultPlayTrailerButton,
) )
for (requestView in views) { for (requestView in views) {
if (!requestView.isShown) continue 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( resultEpisodes.setLinearListLayout(
isHorizontal = false, isHorizontal = false,
nextUp = FOCUS_SELF, nextUp = FOCUS_SELF,
@ -430,9 +443,9 @@ class ResultFragmentTv : Fragment() {
val aboveCast = listOf( val aboveCast = listOf(
binding?.resultEpisodesShow, binding?.resultEpisodesShow,
binding?.resultBookmarkButton, binding?.resultBookmark,
binding?.resultFavoriteButton, binding?.resultFavorite,
binding?.resultSubscribeButton, binding?.resultSubscribe,
).firstOrNull { ).firstOrNull {
it?.isVisible == true it?.isVisible == true
} }
@ -443,8 +456,15 @@ class ResultFragmentTv : Fragment() {
observeNullable(viewModel.resumeWatching) { resume -> observeNullable(viewModel.resumeWatching) { resume ->
binding?.apply { binding?.apply {
// > resultResumeSeries is visible when not null
if (resume == null) {
resultResumeSeries.isVisible = false
return@observeNullable
}
// show progress no matter if series or movie // show progress no matter if series or movie
resume?.progress?.let { progress -> resume.progress?.let { progress ->
resultResumeSeriesProgressText.setText(progress.progressLeft) resultResumeSeriesProgressText.setText(progress.progressLeft)
resultResumeSeriesProgress.apply { resultResumeSeriesProgress.apply {
isVisible = true isVisible = true
@ -456,37 +476,24 @@ class ResultFragmentTv : Fragment() {
resultResumeProgressHolder.isVisible = false resultResumeProgressHolder.isVisible = false
} }
// if movie then hide both as movie button is resultPlayMovie.isVisible = false
// 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
}
resultPlaySeries.isVisible = false resultPlaySeries.isVisible = false
resultResumeSeries.isVisible = true resultResumeSeries.isVisible = true
focusPlayButton() focusPlayButton()
// Stops last button right focus if it is a movie
if (resume.isMovie)
resultSearchButton.nextFocusRightId = R.id.result_search_Button
resultResumeSeries.text = resultResumeSeriesText.text =
if (resume.isMovie) context?.getString(R.string.play_movie_button) else context?.getNameFull( when {
null, // resume.result.name, we don't want episode title resume.isMovie -> context?.getString(R.string.resume)
resume.result.episode, resume.result.season != null ->
resume.result.season "${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( viewModel.handleAction(
EpisodeClickEvent( EpisodeClickEvent(
storedData.playerAction, //?: ACTION_PLAY_EPISODE_IN_PLAYER, storedData.playerAction, //?: ACTION_PLAY_EPISODE_IN_PLAYER,
@ -495,7 +502,7 @@ class ResultFragmentTv : Fragment() {
) )
} }
resultResumeSeries.setOnLongClickListener { resultResumeSeriesButton.setOnLongClickListener {
viewModel.handleAction( viewModel.handleAction(
EpisodeClickEvent(ACTION_SHOW_OPTIONS, resume.result) EpisodeClickEvent(ACTION_SHOW_OPTIONS, resume.result)
) )
@ -509,9 +516,9 @@ class ResultFragmentTv : Fragment() {
context?.updateHasTrailers() context?.updateHasTrailers()
if (!LoadResponse.isTrailersEnabled) return@observe if (!LoadResponse.isTrailersEnabled) return@observe
val trailers = trailersLinks.flatMap { it.mirros } val trailers = trailersLinks.flatMap { it.mirros }
binding?.resultPlayTrailer?.apply { binding?.apply {
isGone = trailers.isEmpty() resultPlayTrailer.isGone = trailers.isEmpty()
setOnClickListener { resultPlayTrailerButton.setOnClickListener {
if (trailers.isEmpty()) return@setOnClickListener if (trailers.isEmpty()) return@setOnClickListener
activity.navigate( activity.navigate(
R.id.global_to_navigation_player, GeneratorPlayer.newInstance( R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
@ -526,24 +533,38 @@ class ResultFragmentTv : Fragment() {
} }
observe(viewModel.watchStatus) { watchType -> observe(viewModel.watchStatus) { watchType ->
binding?.resultBookmarkButton?.apply { binding?.apply {
setText(watchType.stringRes) resultBookmarkText.setText(watchType.stringRes)
setOnClickListener { view ->
activity?.showBottomDialog( resultBookmarkButton.apply {
WatchType.values().map { view.context.getString(it.stringRes) }.toList(),
watchType.ordinal, val drawable = if (watchType.stringRes == R.string.type_none) {
view.context.getString(R.string.action_add_to_bookmarks), R.drawable.outline_bookmark_add_24
showApply = false, } else {
{}) { R.drawable.ic_baseline_bookmark_24
viewModel.updateWatchStatus(WatchType.values()[it], context) }
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 -> observeNullable(viewModel.favoriteStatus) { isFavorite ->
binding?.resultFavorite?.isVisible = isFavorite != null
binding?.resultFavoriteButton?.apply { binding?.resultFavoriteButton?.apply {
isVisible = isFavorite != null
if (isFavorite == null) return@observeNullable if (isFavorite == null) return@observeNullable
val drawable = if (isFavorite) { val drawable = if (isFavorite) {
@ -552,14 +573,8 @@ class ResultFragmentTv : Fragment() {
R.drawable.ic_baseline_favorite_border_24 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) setIconResource(drawable)
setText(text)
setOnClickListener { setOnClickListener {
viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? -> viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? ->
if (newStatus == null) return@toggleFavoriteStatus 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 -> observeNullable(viewModel.subscribeStatus) { isSubscribed ->
binding?.resultSubscribe?.isVisible = isSubscribed != null && requireContext().isEmulatorSettings()
binding?.resultSubscribeButton?.apply { binding?.resultSubscribeButton?.apply {
isVisible = isSubscribed != null && context.isEmulatorSettings()
if (isSubscribed == null) return@observeNullable if (isSubscribed == null) return@observeNullable
val drawable = if (isSubscribed) { val drawable = if (isSubscribed) {
@ -589,14 +614,8 @@ class ResultFragmentTv : Fragment() {
R.drawable.baseline_notifications_none_24 R.drawable.baseline_notifications_none_24
} }
val text = if (isSubscribed) {
R.string.action_unsubscribe
} else {
R.string.action_subscribe
}
setIconResource(drawable) setIconResource(drawable)
setText(text)
setOnClickListener { setOnClickListener {
viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? -> viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? ->
if (newStatus == null) return@toggleSubscriptionStatus if (newStatus == null) return@toggleSubscriptionStatus
@ -614,32 +633,47 @@ class ResultFragmentTv : Fragment() {
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) 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 -> observeNullable(viewModel.movie) { data ->
if (data == null) return@observeNullable
binding?.apply { binding?.apply {
resultPlayMovie.isVisible = data is Resource.Success resultPlayMovie.isVisible = (data is Resource.Success) && !comingSoon
resultPlaySeries.isVisible = data == null resultPlaySeries.isVisible = false
seriesHolder.isVisible = data == null resultEpisodesShow.isVisible = false
resultEpisodesShow.isVisible = data == null
(data as? Resource.Success)?.value?.let { (text, ep) -> (data as? Resource.Success)?.value?.let { (text, ep) ->
resultPlayMovie.setText(text) //resultPlayMovieText.setText(text)
resultPlayMovie.setOnClickListener { resultPlayMovieButton.setOnClickListener {
viewModel.handleAction( viewModel.handleAction(
EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep) EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep)
) )
} }
resultPlayMovie.setOnLongClickListener { resultPlayMovieButton.setOnLongClickListener {
viewModel.handleAction( viewModel.handleAction(
EpisodeClickEvent(ACTION_SHOW_OPTIONS, ep) EpisodeClickEvent(ACTION_SHOW_OPTIONS, ep)
) )
return@setOnLongClickListener true return@setOnLongClickListener true
} }
focusPlayButton() //focusPlayButton()
resultPlayMovieButton.requestFocus()
// Stops last button right focus
resultSearchButton.nextFocusRightId = R.id.result_search_Button
} }
} }
//focusPlayButton()
} }
observeNullable(viewModel.selectPopup) { popup -> observeNullable(viewModel.selectPopup) { popup ->
@ -736,19 +770,26 @@ class ResultFragmentTv : Fragment() {
// Used to request focus the first time the episodes are loaded. // Used to request focus the first time the episodes are loaded.
var hasLoadedEpisodesOnce = false var hasLoadedEpisodesOnce = false
observeNullable(viewModel.episodes) { episodes -> observeNullable(viewModel.episodes) { episodes ->
if (episodes == null) return@observeNullable
binding?.apply { 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 // resultEpisodeLoading.isVisible = episodes is Resource.Loading
if (episodes is Resource.Success) { if (episodes is Resource.Success) {
val first = episodes.value.firstOrNull() val first = episodes.value.firstOrNull()
if (first != null) { if (first != null) {
resultPlaySeries.text = context?.getNameFull( resultPlaySeriesText.text = //"${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}"
null, // resume.result.name, we don't want episode title when {
first.episode, first.season != null ->
first.season "${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}"
) else -> "${getString(R.string.episode)} ${first.episode}"
}
resultPlaySeries.setOnClickListener { resultPlaySeriesButton.setOnClickListener {
viewModel.handleAction( viewModel.handleAction(
EpisodeClickEvent( EpisodeClickEvent(
ACTION_CLICK_DEFAULT, ACTION_CLICK_DEFAULT,
@ -756,7 +797,7 @@ class ResultFragmentTv : Fragment() {
) )
) )
} }
resultPlaySeries.setOnLongClickListener { resultPlaySeriesButton.setOnLongClickListener {
viewModel.handleAction( viewModel.handleAction(
EpisodeClickEvent(ACTION_SHOW_OPTIONS, first) EpisodeClickEvent(ACTION_SHOW_OPTIONS, first)
) )
@ -765,6 +806,7 @@ class ResultFragmentTv : Fragment() {
if (!hasLoadedEpisodesOnce) { if (!hasLoadedEpisodesOnce) {
hasLoadedEpisodesOnce = true hasLoadedEpisodesOnce = true
focusPlayButton() focusPlayButton()
resultPlaySeries.requestFocus()
} }
} }
@ -826,6 +868,7 @@ class ResultFragmentTv : Fragment() {
resultMetaYear.setText(d.yearText) resultMetaYear.setText(d.yearText)
resultMetaDuration.setText(d.durationText) resultMetaDuration.setText(d.durationText)
resultMetaRating.setText(d.ratingText) resultMetaRating.setText(d.ratingText)
resultMetaStatus.setText(d.onGoingText)
resultMetaContentRating.setText(d.contentRatingText) resultMetaContentRating.setText(d.contentRatingText)
resultCastText.setText(d.actorsText) resultCastText.setText(d.actorsText)
resultNextAiring.setText(d.nextAiringEpisode) resultNextAiring.setText(d.nextAiringEpisode)
@ -859,8 +902,12 @@ class ResultFragmentTv : Fragment() {
radius = 0, radius = 0,
errorImageDrawable = error 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 resultDataHolder.isGone = d.comingSoon
UIHelper.populateChips(resultTag, d.tags) UIHelper.populateChips(resultTag, d.tags)
resultCastItems.isGone = d.actors.isNullOrEmpty() resultCastItems.isGone = d.actors.isNullOrEmpty()
(resultCastItems.adapter as? ActorAdaptor)?.updateList( (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 // If there is no rating to display, we don't want an empty gap
resultMetaContentRating.width = 0 resultMetaContentRating.width = 0
} }
resultSearchButton.setOnClickListener {
QuickSearchFragment.pushSearch(activity, d.title)
}
} }
is Resource.Loading -> { is Resource.Loading -> {

View file

@ -20,6 +20,7 @@ import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.CommonActivity.getCastSession 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.metaproviders.SyncRedirector
import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.syncproviders.AccountManager 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.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
@ -261,8 +263,7 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData {
metaText = metaText =
if (repo.providerType == ProviderType.MetaProvider) txt(R.string.provider_info_meta) else null, if (repo.providerType == ProviderType.MetaProvider) txt(R.string.provider_info_meta) else null,
durationText = if (dur == null || dur <= 0) null else txt( durationText = if (dur == null || dur <= 0) null else txt(
R.string.duration_format, secondsToReadable(dur * 60, "0 mins")
dur
), ),
onGoingText = if (this is EpisodeResponse) { onGoingText = if (this is EpisodeResponse) {
txt( txt(
@ -2464,7 +2465,7 @@ class ResultViewModel2 : ViewModel() {
ResumeProgress( ResumeProgress(
progress = (viewPos.position / 1000).toInt(), progress = (viewPos.position / 1000).toInt(),
maxProgress = (viewPos.duration / 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"))
) )
} }

View file

@ -11,8 +11,10 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.AccountManagmentBinding 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.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar 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.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
@ -256,6 +261,24 @@ class SettingsAccount : PreferenceFragmentCompat() {
hideKeyboard() hideKeyboard()
setPreferencesFromResource(R.xml.settings_account, rootKey) 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 = val syncApis =
listOf( listOf(
R.string.mal_key to malApi, R.string.mal_key to malApi,

View file

@ -19,6 +19,7 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.databinding.MainSettingsBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
@ -155,6 +156,11 @@ class SettingsFragment : Fragment() {
return getLayoutInt() == 2 return getLayoutInt() == 2
} }
// phone exclusive
fun isTruePhone(): Boolean {
return !isTrueTvSettings() && !isTvSettings() && context?.isEmulatorSettings() != true
}
private fun Context.isAutoTv(): Boolean { private fun Context.isAutoTv(): Boolean {
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager? val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
// AFT = Fire TV // AFT = Fire TV

View file

@ -65,12 +65,16 @@ object BackupUtils {
PLUGINS_KEY_LOCAL, PLUGINS_KEY_LOCAL,
OPEN_SUBTITLES_USER_KEY, 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 { private fun String.isTransferable(): Boolean {
return !nonTransferableKeys.contains(this) return !nonTransferableKeys.any { this.contains(it) }
} }
private var restoreFileSelector: ActivityResultLauncher<Array<String>>? = null private var restoreFileSelector: ActivityResultLauncher<Array<String>>? = null

View file

@ -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()
}
}

View file

@ -532,7 +532,6 @@ object UIHelper {
WindowInsetsControllerCompat(window, View(this)).show(WindowInsetsCompat.Type.systemBars()) WindowInsetsControllerCompat(window, View(this)).show(WindowInsetsCompat.Type.systemBars())
} else {*/ /** WINDOW COMPAT IS BUGGY DUE TO FU*KED UP PLAYER AND TRAILERS **/ } else {*/ /** WINDOW COMPAT IS BUGGY DUE TO FU*KED UP PLAYER AND TRAILERS **/
Suppress("DEPRECATION")
window.decorView.systemUiVisibility = window.decorView.systemUiVisibility =
(View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
//} //}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M240,760L240,800Q240,817 228.5,828.5Q217,840 200,840Q183,840 171.5,828.5Q160,817 160,800L160,160Q160,143 171.5,131.5Q183,120 200,120Q217,120 228.5,131.5Q240,143 240,160L240,200L320,200L320,160Q320,143 331.5,131.5Q343,120 360,120L600,120Q617,120 628.5,131.5Q640,143 640,160L640,200L720,200L720,160Q720,143 731.5,131.5Q743,120 760,120Q777,120 788.5,131.5Q800,143 800,160L800,800Q800,817 788.5,828.5Q777,840 760,840Q743,840 731.5,828.5Q720,817 720,800L720,760L640,760L640,800Q640,817 628.5,828.5Q617,840 600,840L360,840Q343,840 331.5,828.5Q320,817 320,800L320,760L240,760ZM240,680L320,680L320,600L240,600L240,680ZM240,520L320,520L320,440L240,440L240,520ZM240,360L320,360L320,280L240,280L240,360ZM640,680L720,680L720,600L640,600L640,680ZM640,520L720,520L720,440L640,440L640,520ZM640,360L720,360L720,280L640,280L640,360Z"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="m8.46,5l0,14l11,-7l-11,-7z"
android:strokeLineJoin="round"
android:strokeWidth="0.006"
android:fillColor="#000000"/>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="m8.46,5l0,14l11,-7l-11,-7z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M4.92,5.04h2.31v13.98h-2.31z"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="?attr/white"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="@android:color/white"
android:pathData="M17.81,4.47c-0.08,0 -0.16,-0.02 -0.23,-0.06C15.66,3.42 14,3 12.01,3c-1.98,0 -3.86,0.47 -5.57,1.41 -0.24,0.13 -0.54,0.04 -0.68,-0.2 -0.13,-0.24 -0.04,-0.55 0.2,-0.68C7.82,2.52 9.86,2 12.01,2c2.13,0 3.99,0.47 6.03,1.52 0.25,0.13 0.34,0.43 0.21,0.67 -0.09,0.18 -0.26,0.28 -0.44,0.28zM3.5,9.72c-0.1,0 -0.2,-0.03 -0.29,-0.09 -0.23,-0.16 -0.28,-0.47 -0.12,-0.7 0.99,-1.4 2.25,-2.5 3.75,-3.27C9.98,4.04 14,4.03 17.15,5.65c1.5,0.77 2.76,1.86 3.75,3.25 0.16,0.22 0.11,0.54 -0.12,0.7 -0.23,0.16 -0.54,0.11 -0.7,-0.12 -0.9,-1.26 -2.04,-2.25 -3.39,-2.94 -2.87,-1.47 -6.54,-1.47 -9.4,0.01 -1.36,0.7 -2.5,1.7 -3.4,2.96 -0.08,0.14 -0.23,0.21 -0.39,0.21zM9.75,21.79c-0.13,0 -0.26,-0.05 -0.35,-0.15 -0.87,-0.87 -1.34,-1.43 -2.01,-2.64 -0.69,-1.23 -1.05,-2.73 -1.05,-4.34 0,-2.97 2.54,-5.39 5.66,-5.39s5.66,2.42 5.66,5.39c0,0.28 -0.22,0.5 -0.5,0.5s-0.5,-0.22 -0.5,-0.5c0,-2.42 -2.09,-4.39 -4.66,-4.39s-4.66,1.97 -4.66,4.39c0,1.44 0.32,2.77 0.93,3.85 0.64,1.15 1.08,1.64 1.85,2.42 0.19,0.2 0.19,0.51 0,0.71 -0.11,0.1 -0.24,0.15 -0.37,0.15zM16.92,19.94c-1.19,0 -2.24,-0.3 -3.1,-0.89 -1.49,-1.01 -2.38,-2.65 -2.38,-4.39 0,-0.28 0.22,-0.5 0.5,-0.5s0.5,0.22 0.5,0.5c0,1.41 0.72,2.74 1.94,3.56 0.71,0.48 1.54,0.71 2.54,0.71 0.24,0 0.64,-0.03 1.04,-0.1 0.27,-0.05 0.53,0.13 0.58,0.41 0.05,0.27 -0.13,0.53 -0.41,0.58 -0.57,0.11 -1.07,0.12 -1.21,0.12zM14.91,22c-0.04,0 -0.09,-0.01 -0.13,-0.02 -1.59,-0.44 -2.63,-1.03 -3.72,-2.1 -1.4,-1.39 -2.17,-3.24 -2.17,-5.22 0,-1.62 1.38,-2.94 3.08,-2.94s3.08,1.32 3.08,2.94c0,1.07 0.93,1.94 2.08,1.94s2.08,-0.87 2.08,-1.94c0,-3.77 -3.25,-6.83 -7.25,-6.83 -2.84,0 -5.44,1.58 -6.61,4.03 -0.39,0.81 -0.59,1.76 -0.59,2.8 0,0.78 0.07,2.01 0.67,3.61 0.1,0.26 -0.03,0.55 -0.29,0.64 -0.26,0.1 -0.55,-0.04 -0.64,-0.29 -0.49,-1.31 -0.73,-2.61 -0.73,-3.96 0,-1.2 0.23,-2.29 0.68,-3.24 1.33,-2.79 4.28,-4.6 7.51,-4.6 4.55,0 8.25,3.51 8.25,7.83 0,1.62 -1.38,2.94 -3.08,2.94s-3.08,-1.32 -3.08,-2.94c0,-1.07 -0.93,-1.94 -2.08,-1.94s-2.08,0.87 -2.08,1.94c0,1.71 0.66,3.31 1.87,4.51 0.95,0.94 1.86,1.46 3.27,1.85 0.27,0.07 0.42,0.35 0.35,0.61 -0.05,0.23 -0.26,0.38 -0.47,0.38z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/white">
<path
android:fillColor="@android:color/white"
android:pathData="M160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L400,160L480,240L800,240Q833,240 856.5,263.5Q880,287 880,320L447,320L367,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720L256,400L940,400L837,743Q829,769 807.5,784.5Q786,800 760,800L160,800ZM244,720L760,720L832,480L316,480L244,720ZM244,720L316,480L316,480L244,720L244,720ZM160,320L160,240Q160,240 160,240Q160,240 160,240L160,240L160,320L160,320Z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M17,11v6.97l-5,-2.14l-5,2.14V5h6V3H7C5.9,3 5,3.9 5,5v16l7,-3l7,3V11H17zM21,7h-2v2h-2V7h-2V5h2V3h2v2h2V7z"/>
</vector>

View file

@ -3,13 +3,13 @@
<item android:state_focused="true"> <item android:state_focused="true">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="?attr/white" /> <solid android:color="?attr/white" />
<corners android:radius="3dp"/> <corners android:radius="@dimen/rounded_image_radius"/>
</shape> </shape>
</item> </item>
<item> <item>
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="@color/white_attr_20" /> <solid android:color="@color/white_attr_20" />
<corners android:radius="3dp"/> <corners android:radius="@dimen/rounded_image_radius"/>
</shape> </shape>
</item> </item>
</selector> </selector>

View file

@ -3,7 +3,7 @@
<item android:state_focused="true"> <item android:state_focused="true">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="?attr/white" /> <solid android:color="?attr/white" />
<corners android:radius="3dp"/> <corners android:radius="@dimen/rounded_image_radius"/>
</shape> </shape>
</item> </item>
</selector> </selector>

View file

@ -17,7 +17,6 @@
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:focusable="true"
android:padding="5dp"> android:padding="5dp">
<!--app:cardCornerRadius="@dimen/roundedImageRadius"--> <!--app:cardCornerRadius="@dimen/roundedImageRadius"-->
<FrameLayout <FrameLayout

View file

@ -157,6 +157,7 @@
android:id="@+id/home_preview_hidden_prev_focus" android:id="@+id/home_preview_hidden_prev_focus"
android:layout_width="1dp" android:layout_width="1dp"
android:layout_height="1dp" android:layout_height="1dp"
android:tag="@string/tv_no_focus_tag"
android:focusable="false" /> android:focusable="false" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
@ -189,6 +190,7 @@
android:id="@+id/home_preview_hidden_next_focus" android:id="@+id/home_preview_hidden_next_focus"
android:layout_width="1dp" android:layout_width="1dp"
android:layout_height="1dp" android:layout_height="1dp"
android:tag="@string/tv_no_focus_tag"
android:focusable="false" /> android:focusable="false" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View file

@ -412,7 +412,7 @@
android:foreground="@drawable/outline_drawable" android:foreground="@drawable/outline_drawable"
android:maxLength="1000" android:maxLength="1000"
android:nextFocusUp="@id/result_back" android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_bookmark_button" android:nextFocusDown="@id/result_bookmark_Button"
android:paddingTop="5dp" android:paddingTop="5dp"
android:textColor="?attr/textColor" android:textColor="?attr/textColor"
android:textSize="15sp" android:textSize="15sp"
@ -474,7 +474,7 @@
android:fadingEdge="horizontal" android:fadingEdge="horizontal"
android:focusable="false" android:focusable="false"
android:focusableInTouchMode="false" android:focusableInTouchMode="false"
android:nextFocusUp="@id/result_bookmark_button" android:nextFocusUp="@id/result_bookmark_Button"
android:nextFocusDown="@id/result_play_movie" android:nextFocusDown="@id/result_play_movie"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingTop="5dp" android:paddingTop="5dp"
@ -580,7 +580,7 @@
android:layout_marginStart="0dp" android:layout_marginStart="0dp"
android:layout_marginEnd="0dp" android:layout_marginEnd="0dp"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:nextFocusUp="@id/result_bookmark_button" android:nextFocusUp="@id/result_bookmark_Button"
android:nextFocusDown="@id/result_download_movie" android:nextFocusDown="@id/result_download_movie"
android:text="@string/play_movie_button" android:text="@string/play_movie_button"
android:visibility="visible" android:visibility="visible"
@ -658,7 +658,7 @@
android:layout_marginStart="0dp" android:layout_marginStart="0dp"
android:layout_marginEnd="0dp" android:layout_marginEnd="0dp"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:nextFocusUp="@id/result_bookmark_button" android:nextFocusUp="@id/result_bookmark_Button"
android:nextFocusDown="@id/result_download_movie" android:nextFocusDown="@id/result_download_movie"
android:text="@string/resume" android:text="@string/resume"
android:visibility="visible" android:visibility="visible"
@ -674,7 +674,7 @@
android:layout_marginStart="0dp" android:layout_marginStart="0dp"
android:layout_marginEnd="0dp" android:layout_marginEnd="0dp"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:nextFocusUp="@id/result_bookmark_button" android:nextFocusUp="@id/result_bookmark_Button"
android:nextFocusDown="@id/result_download_movie" android:nextFocusDown="@id/result_download_movie"
android:text="@string/next_episode" android:text="@string/next_episode"
android:visibility="gone" android:visibility="gone"

View file

@ -78,6 +78,30 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
</LinearLayout> </LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout> </com.facebook.shimmer.ShimmerFrameLayout>
<FrameLayout
android:id="@+id/background_poster_holder"
android:layout_width="match_parent"
android:layout_height="275dp"
android:visibility="visible">
<com.lagradost.cloudstream3.utils.PercentageCropImageView
android:id="@+id/background_poster"
android:layout_width="match_parent"
android:layout_height="275dp"
android:layout_gravity="center"
android:alpha="0.8"
android:scaleType="matrix"
tools:src="@drawable/profile_bg_dark_blue" >
</com.lagradost.cloudstream3.utils.PercentageCropImageView>
<ImageView
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_gravity="bottom"
android:src="@drawable/background_shadow">
</ImageView>
</FrameLayout>
<LinearLayout <LinearLayout
android:id="@+id/result_loading_error" android:id="@+id/result_loading_error"
@ -124,31 +148,6 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:textColor="?attr/textColor" /> android:textColor="?attr/textColor" />
</LinearLayout> </LinearLayout>
<FrameLayout
android:id="@+id/background_poster_holder"
android:layout_width="match_parent"
android:layout_height="250dp"
android:visibility="visible">
<com.lagradost.cloudstream3.utils.PercentageCropImageView
android:id="@+id/background_poster"
android:layout_width="match_parent"
android:layout_height="275dp"
android:layout_gravity="center"
android:alpha="0.8"
android:scaleType="matrix"
tools:src="@drawable/profile_bg_dark_blue" >
</com.lagradost.cloudstream3.utils.PercentageCropImageView>
<ImageView
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_gravity="bottom"
android:src="@drawable/background_shadow">
</ImageView>
</FrameLayout>
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:id="@+id/result_finish_loading" android:id="@+id/result_finish_loading"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -165,7 +164,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:layout_marginTop="175dp"> android:layout_marginTop="225dp">
<TextView <TextView
android:id="@+id/result_title" android:id="@+id/result_title"
@ -175,7 +174,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:gravity="center_vertical" android:gravity="center_vertical"
android:singleLine="true" android:singleLine="true"
android:textColor="?attr/textColor" android:textColor="?attr/textColor"
android:textSize="20sp" android:textSize="25sp"
android:textStyle="bold" android:textStyle="bold"
tools:text="The Perfect Run The Perfect Run" /> tools:text="The Perfect Run The Perfect Run" />
@ -221,157 +220,289 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:textStyle="normal" android:textStyle="normal"
tools:text="5d 3h 30m" /> tools:text="5d 3h 30m" />
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/result_resume_progress_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/result_resume_series_progress"
app:trackCornerRadius="50dp"
android:layout_width="100dp"
android:layout_height="20dp"
android:layout_gravity="end|center_vertical"
android:layout_weight="1"
android:indeterminate="false"
android:max="100"
android:paddingEnd="10dp"
android:progress="0"
android:progressBackgroundTint="?attr/colorPrimary"
tools:progress="50"
tools:visibility="visible"
tools:ignore="RtlSymmetry" />
<TextView
android:id="@+id/result_resume_series_progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:textColor="?attr/grayTextColor"
tools:ignore="RtlSymmetry"
tools:text="69m remaining" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/result_play_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="5dp"
android:descendantFocusability="afterDescendants"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:id="@+id/result_play_movie"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_play_movie_button"
android:focusable="true"
style="@style/ResultSmallButtonTV"
app:iconPadding="0dp"
app:icon="@drawable/ic_baseline_play_arrow_24"
android:nextFocusUp="@id/result_play_movie_button"
android:nextFocusDown="@id/result_description"
android:tag="@string/tv_no_focus_tag">
</com.google.android.material.button.MaterialButton>
<TextView
android:id="@+id/result_play_movie_text"
style="@style/ResultMarqueeButtonText"
android:text="@string/movies_singular" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:id="@+id/result_play_series"
android:visibility="gone"
tools:visibility="visible"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_play_series_button"
android:focusable="true"
style="@style/ResultSmallButtonTV"
app:iconPadding="0dp"
app:icon="@drawable/ic_baseline_play_arrow_24"
android:nextFocusUp="@id/result_play_series_button"
android:nextFocusDown="@id/result_description"
android:tag="@string/tv_no_focus_tag">
</com.google.android.material.button.MaterialButton>
<TextView
android:id="@+id/result_play_series_text"
style="@style/ResultMarqueeButtonText"
android:text="@string/episode" />
</LinearLayout>
<LinearLayout
android:id="@+id/result_resume_series"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_resume_series_button"
android:focusable="true"
style="@style/ResultSmallButtonTV"
app:iconPadding="0dp"
app:icon="@drawable/ic_baseline_resume_arrow2"
android:nextFocusUp="@id/result_resume_series_button"
android:nextFocusDown="@id/result_description"
android:tag="@string/tv_no_focus_tag">
</com.google.android.material.button.MaterialButton>
<TextView
android:id="@+id/result_resume_series_text"
style="@style/ResultMarqueeButtonText"
android:text="@string/resume" />
</LinearLayout>
<LinearLayout
android:id="@+id/result_play_trailer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_play_trailer_button"
android:focusable="true"
style="@style/ResultSmallButtonTV"
app:iconPadding="0dp"
app:icon="@drawable/ic_baseline_film_roll_24"
android:nextFocusUp="@id/result_play_trailer_button"
android:nextFocusDown="@id/result_description"
android:tag="@string/tv_no_focus_tag">
</com.google.android.material.button.MaterialButton>
<TextView
android:id="@+id/result_play_trailer_text"
style="@style/ResultMarqueeButtonText"
android:text="@string/play_trailer_button" />
</LinearLayout>
<LinearLayout
android:id="@+id/result_bookmark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_bookmark_Button"
android:focusable="true"
style="@style/ResultSmallButtonTV"
app:iconPadding="0dp"
app:icon="@drawable/outline_bookmark_add_24"
android:nextFocusUp="@id/result_bookmark_Button"
android:nextFocusDown="@id/result_description"
android:tag="@string/tv_no_focus_tag">
</com.google.android.material.button.MaterialButton>
<TextView
android:id="@+id/result_bookmark_text"
style="@style/ResultMarqueeButtonText"
android:text="@string/type_none" />
</LinearLayout>
<LinearLayout
android:id="@+id/result_favorite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_favorite_Button"
android:focusable="true"
style="@style/ResultSmallButtonTV"
app:iconPadding="0dp"
app:icon="@drawable/ic_baseline_favorite_border_24"
android:nextFocusUp="@id/result_favorite_Button"
android:nextFocusDown="@id/result_description"
android:tag="@string/tv_no_focus_tag">
</com.google.android.material.button.MaterialButton>
<TextView
android:id="@+id/result_favorite_Text"
style="@style/ResultMarqueeButtonText"
android:text="@string/favorite" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:id="@+id/result_subscribe"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_subscribe_Button"
android:focusable="true"
style="@style/ResultSmallButtonTV"
app:iconPadding="0dp"
app:icon="@drawable/baseline_notifications_none_24"
android:nextFocusUp="@id/result_subscribe_Button"
android:nextFocusDown="@id/result_description"
android:tag="@string/tv_no_focus_tag">
</com.google.android.material.button.MaterialButton>
<TextView
android:id="@+id/result_subscribe_text"
style="@style/ResultMarqueeButtonText"
android:text="@string/action_subscribe" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:id="@+id/result_search"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_search_Button"
android:focusable="true"
style="@style/ResultSmallButtonTV"
app:iconPadding="0dp"
app:icon="@drawable/search_icon"
android:nextFocusUp="@id/result_search_Button"
android:nextFocusDown="@id/result_description"
android:tag="@string/tv_no_focus_tag">
</com.google.android.material.button.MaterialButton>
<TextView
android:id="@+id/result_search_text"
style="@style/ResultMarqueeButtonText"
android:text="@string/title_search" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:id="@+id/result_episodes_show"
android:visibility="gone"
tools:visibility="visible"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_episodes_show_button"
android:focusable="true"
style="@style/ResultSmallButtonTV"
app:iconPadding="0dp"
app:icon="@drawable/ic_baseline_sort_24"
android:nextFocusUp="@id/result_episodes_show_button"
android:nextFocusRight="@id/redirect_to_episodes"
android:nextFocusDown="@id/result_description"
android:tag="@string/tv_no_focus_tag">
</com.google.android.material.button.MaterialButton>
<TextView
android:id="@+id/result_episodes_show_text"
style="@style/ResultMarqueeButtonText"
android:text="@string/episodes" />
</LinearLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_marginTop="10dp"> android:layout_marginTop="10dp"
android:baselineAligned="false">
<LinearLayout
android:layout_width="250dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_weight="0"
android:orientation="vertical">
<LinearLayout
android:id="@+id/result_movie_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:animateLayoutChanges="true"
android:orientation="vertical"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_play_movie"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_play_movie"
android:nextFocusDown="@id/result_play_series"
android:text="@string/play_movie_button"
android:visibility="visible"
app:icon="@drawable/ic_baseline_play_arrow_24" />
<LinearLayout
android:id="@+id/series_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_play_series"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_play_movie"
android:nextFocusDown="@id/result_resume_series"
android:text="@string/play_episode"
android:visibility="visible"
app:icon="@drawable/ic_baseline_play_arrow_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_resume_series"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_play_series"
android:nextFocusDown="@id/result_play_trailer"
android:text="@string/resume"
android:visibility="visible"
app:icon="@drawable/ic_baseline_play_arrow_24" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/result_play_trailer"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_resume_series"
android:nextFocusDown="@id/result_bookmark_button"
android:text="@string/play_trailer_button"
android:visibility="gone"
app:icon="@drawable/ic_baseline_play_arrow_24"
tools:visibility="visible" />
<!-- <com.lagradost.cloudstream3.ui.download.button.DownloadButton
android:visibility="gone"
android:id="@+id/download_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:minWidth="250dp"
app:download_layout="@layout/download_button_layout" />
-->
<com.google.android.material.button.MaterialButton
android:id="@+id/result_bookmark_button"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_play_trailer"
android:nextFocusDown="@id/result_favorite_button"
android:text="@string/type_none"
android:visibility="visible"
app:icon="@drawable/ic_baseline_bookmark_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_favorite_button"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_bookmark_button"
android:nextFocusDown="@id/result_subscribe_button"
android:text="@string/action_add_to_favorites"
android:visibility="visible"
app:icon="@drawable/ic_baseline_favorite_border_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_subscribe_button"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_favorite_button"
android:nextFocusDown="@id/result_episodes_show"
android:text="@string/action_subscribe"
android:visibility="visible"
app:icon="@drawable/ic_baseline_favorite_border_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_episodes_show"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/redirect_to_episodes"
android:nextFocusUp="@id/result_subscribe_button"
android:nextFocusDown="@id/result_cast_items"
android:text="@string/episodes"
android:visibility="visible"
app:icon="@drawable/ic_baseline_sort_24"
tools:visibility="visible" />
<View
android:id="@+id/redirect_to_episodes"
android:layout_width="1dp"
android:layout_height="1dp"
android:focusable="true"
android:focusableInTouchMode="true" />
<View
android:id="@+id/redirect_to_play"
android:layout_width="1dp"
android:layout_height="1dp"
android:focusable="true"
android:focusableInTouchMode="true" />
</LinearLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/right_layout" android:id="@+id/right_layout"
@ -382,7 +513,8 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/linearLayout2" app:layout_constraintStart_toEndOf="@+id/linearLayout2"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent"
tools:ignore="UselessParent">
<com.lagradost.cloudstream3.widget.FlowLayout <com.lagradost.cloudstream3.widget.FlowLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -439,8 +571,8 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:foreground="@drawable/outline_drawable" android:foreground="@drawable/outline_drawable"
android:maxLines="7" android:maxLines="7"
android:focusable="true" android:focusable="true"
android:nextFocusUp="@id/result_back" android:nextFocusUp="@id/result_play_parent"
android:nextFocusDown="@id/result_bookmark_button" android:nextFocusDown="@id/result_cast_items"
android:padding="5dp" android:padding="5dp"
android:requiresFadingEdge="vertical" android:requiresFadingEdge="vertical"
android:textColor="?attr/textColor" android:textColor="?attr/textColor"
@ -450,59 +582,8 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
<com.google.android.material.chip.ChipGroup <com.google.android.material.chip.ChipGroup
android:id="@+id/result_tag" android:id="@+id/result_tag"
style="@style/ChipParent" style="@style/ChipParent"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone">
<LinearLayout
android:id="@+id/result_resume_progress_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/result_resume_series_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_gravity="end|center_vertical"
android:layout_weight="1"
android:indeterminate="false"
android:max="100"
android:paddingEnd="10dp"
android:progress="0"
android:progressBackgroundTint="?attr/colorPrimary"
android:visibility="gone"
tools:progress="50"
tools:visibility="visible" />
<TextView
android:id="@+id/result_resume_series_progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="0"
android:gravity="center"
android:maxLines="1"
android:paddingEnd="5dp"
android:textColor="?attr/grayTextColor"
android:visibility="gone"
tools:ignore="RtlSymmetry"
tools:text="69m remaining" />
</LinearLayout>
</FrameLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
@ -513,10 +594,8 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:descendantFocusability="afterDescendants" android:descendantFocusability="afterDescendants"
android:fadingEdge="horizontal" android:fadingEdge="horizontal"
android:focusable="false" android:nextFocusUp="@id/result_description"
android:focusableInTouchMode="false" android:nextFocusDown="@id/result_recommendations_list"
android:nextFocusUp="@id/result_episodes_show"
android:nextFocusDown="@id/result_recommendations_filter_selection"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingTop="5dp" android:paddingTop="5dp"
android:requiresFadingEdge="horizontal" android:requiresFadingEdge="horizontal"
@ -525,8 +604,23 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
tools:listitem="@layout/cast_item" tools:listitem="@layout/cast_item"
tools:visibility="visible" /> tools:visibility="visible" />
<TextView
android:id="@+id/result_tv_coming_soon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:paddingTop="50dp"
android:text="@string/coming_soon"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold"
tools:visibility="visible"
android:visibility="gone" />
<LinearLayout <LinearLayout
android:id="@+id/result_recommendations_holder" android:id="@+id/result_recommendations_holder"
android:descendantFocusability="afterDescendants"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
@ -540,7 +634,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:nextFocusUp="@id/result_cast_items" android:nextFocusUp="@id/result_cast_items"
android:nextFocusDown="@id/result_recommendations_list" android:nextFocusDown="@id/result_recommendations_list"
android:descendantFocusability="afterDescendants"
android:orientation="horizontal" android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="2" tools:itemCount="2"
@ -563,7 +657,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipToPadding="false" android:clipToPadding="false"
android:descendantFocusability="afterDescendants" android:descendantFocusability="afterDescendants"
android:nextFocusUp="@id/result_recommendations_filter_selection" android:nextFocusUp="@id/result_cast_items"
android:orientation="vertical" android:orientation="vertical"
app:spanCount="8" app:spanCount="8"
tools:listitem="@layout/search_result_grid" /> tools:listitem="@layout/search_result_grid" />
@ -576,7 +670,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="end" android:layout_gravity="end"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="invisible">
<!-- <!--
@ -765,152 +859,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:orientation="horizontal">
<!--
<LinearLayout
android:id="@+id/result_movie_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:animateLayoutChanges="true"
android:orientation="vertical"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_play_movie"
style="@style/RegularButtonTV"
android:layout_width="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="10dp"
android:layout_weight="1"
android:minWidth="250dp"
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_play_series"
android:text="@string/play_movie_button"
android:visibility="visible"
app:icon="@drawable/ic_baseline_play_arrow_24">
</com.google.android.material.button.MaterialButton>
<LinearLayout
android:id="@+id/series_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_play_series"
style="@style/RegularButtonTV"
android:layout_width="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="10dp"
android:layout_weight="1"
android:minWidth="250dp"
android:nextFocusUp="@id/result_play_movie"
android:nextFocusDown="@id/result_resume_series"
android:text="@string/play_episode"
android:visibility="visible"
app:icon="@drawable/ic_baseline_play_arrow_24"
tools:visibility="gone" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_resume_series"
style="@style/RegularButtonTV"
android:layout_width="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="10dp"
android:layout_weight="1"
android:minWidth="250dp"
android:nextFocusUp="@id/result_play_series"
android:nextFocusDown="@id/result_play_trailer"
android:text="@string/resume"
android:visibility="visible"
app:icon="@drawable/ic_baseline_play_arrow_24"
tools:visibility="gone" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/result_play_trailer"
style="@style/RegularButtonTV"
android:layout_width="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="10dp"
android:layout_weight="1"
android:minWidth="250dp"
android:nextFocusUp="@id/result_resume_series"
android:nextFocusDown="@id/result_bookmark_button"
android:text="@string/play_trailer_button"
android:visibility="gone"
app:icon="@drawable/ic_baseline_play_arrow_24"
tools:visibility="visible">
</com.google.android.material.button.MaterialButton>
<com.google.android.material.button.MaterialButton
android:id="@+id/result_bookmark_button"
style="@style/RegularButtonTV"
android:layout_width="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="10dp"
android:layout_weight="1"
android:minWidth="250dp"
android:nextFocusUp="@id/result_play_trailer"
android:nextFocusDown="@id/result_resume_series_button"
android:text="@string/type_none"
android:visibility="visible" />
<LinearLayout
android:id="@+id/result_resume_progress_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/result_resume_series_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_gravity="end|center_vertical"
android:layout_weight="1"
android:indeterminate="false"
android:max="100"
android:paddingEnd="10dp"
android:progress="0"
android:progressBackgroundTint="?attr/colorPrimary"
android:visibility="visible"
tools:progress="50"
tools:visibility="visible" />
<TextView
android:id="@+id/result_resume_series_progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="0"
android:gravity="center"
android:maxLines="1"
android:paddingEnd="5dp"
android:textColor="?attr/grayTextColor"
tools:ignore="RtlSymmetry"
tools:text="69m remaining" />
</LinearLayout>
</LinearLayout>
-->
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:id="@+id/result_poster_holder" android:id="@+id/result_poster_holder"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -1067,70 +1016,47 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:visibility="gone" android:visibility="gone"
app:icon="@drawable/ic_baseline_add_24" /> app:icon="@drawable/ic_baseline_add_24" />
</LinearLayout>
<!--<LinearLayout <LinearLayout
android:layout_width="250dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_weight="0"
android:orientation="vertical">
android:id="@+id/result_resume_parent" <LinearLayout
android:layout_width="match_parent" android:id="@+id/result_movie_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="5dp" android:layout_gravity="start"
android:animateLayoutChanges="true"
android:orientation="vertical" android:orientation="vertical"
android:visibility="gone" tools:visibility="visible">
tools:visibility="gone">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_next_series_button"
style="@style/WhiteButton"
android:layout_width="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="10dp"
android:nextFocusUp="@id/result_bookmark_button"
android:nextFocusDown="@id/result_download_movie"
android:text="@string/next_episode"
android:visibility="gone"
app:icon="@drawable/cast_ic_mini_controller_skip_next" />
<LinearLayout <LinearLayout
android:id="@+id/series_holder"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:orientation="vertical">
<ImageView <View
android:id="@+id/result_resume_series_button" android:id="@+id/redirect_to_episodes"
android:layout_width="30dp" android:layout_width="1dp"
android:layout_height="30dp" android:layout_height="1dp"
android:focusable="true"
android:layout_gravity="center" android:focusableInTouchMode="true" />
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/download"
android:nextFocusUp="@id/result_play_movie"
android:nextFocusDown="@id/result_season_selection"
android:src="@drawable/ic_baseline_play_arrow_24"
android:visibility="visible"
app:tint="?attr/white" />
<TextView
android:id="@+id/result_resume_series_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="bold"
tools:text="S1E1 Episode 1" />
<View
android:id="@+id/redirect_to_play"
android:layout_width="1dp"
android:layout_height="1dp"
android:focusable="true"
android:focusableInTouchMode="true" />
</LinearLayout> </LinearLayout>
</LinearLayout>
</LinearLayout>-->
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</FrameLayout> </FrameLayout>

View file

@ -533,18 +533,14 @@
android:id="@id/exo_position" android:id="@id/exo_position"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="30dp" android:layout_height="30dp"
android:layout_gravity="center" android:gravity="center"
android:layout_marginStart="20dp"
android:gravity="end|center_vertical"
android:includeFontPadding="false" android:includeFontPadding="false"
android:minWidth="50dp" android:minWidth="50dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="normal" android:textStyle="normal"
app:layout_constraintBottom_toBottomOf="parent" 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" /> tools:text="15:30" />
<FrameLayout <FrameLayout

View file

@ -10,7 +10,7 @@
android:title="@string/title_search" /> android:title="@string/title_search" />
<item <item
android:id="@+id/navigation_library" android:id="@+id/navigation_library"
android:icon="@drawable/ic_outline_account_circle_24" android:icon="@drawable/library_icon"
android:title="@string/library" /> android:title="@string/library" />
<item <item
android:id="@+id/navigation_downloads" android:id="@+id/navigation_downloads"

View file

@ -212,6 +212,7 @@
<item>Banana</item> <item>Banana</item>
<item>Fiesta</item> <item>Fiesta</item>
<item>Dolor rosa</item> <item>Dolor rosa</item>
<item>Lavanda</item>
<item>Material You</item> <item>Material You</item>
<item>Material You (Secondary)</item> <item>Material You (Secondary)</item>
</string-array> </string-array>
@ -235,6 +236,7 @@
<item>Banana</item> <item>Banana</item>
<item>Party</item> <item>Party</item>
<item>Pink</item> <item>Pink</item>
<item>Lavender</item>
<item>Monet</item> <item>Monet</item>
<item>Monet2</item> <item>Monet2</item>
</string-array> </string-array>

View file

@ -221,6 +221,7 @@
<item>Bananowy</item> <item>Bananowy</item>
<item>Łososiowy</item> <item>Łososiowy</item>
<item>Świnko peppowy</item> <item>Świnko peppowy</item>
<item>Lawenda</item>
<item>Material You</item> <item>Material You</item>
<item>Material You (drugorzędny)</item> <item>Material You (drugorzędny)</item>
</string-array> </string-array>
@ -244,6 +245,7 @@
<item>Banana</item> <item>Banana</item>
<item>Party</item> <item>Party</item>
<item>Pink</item> <item>Pink</item>
<item>Lavender</item>
<item>Monet</item> <item>Monet</item>
<item>Monet2</item> <item>Monet2</item>
</string-array> </string-array>

View file

@ -247,6 +247,7 @@
<item>Muz</item> <item>Muz</item>
<item>Parti</item> <item>Parti</item>
<item>Pembe</item> <item>Pembe</item>
<item>Lavanta</item>
<item>Material You</item> <item>Material You</item>
<item>Material You (İkincil)</item> <item>Material You (İkincil)</item>
</string-array> </string-array>
@ -270,6 +271,7 @@
<item>Banana</item> <item>Banana</item>
<item>Party</item> <item>Party</item>
<item>Pink</item> <item>Pink</item>
<item>Lavender</item>
<item>Monet</item> <item>Monet</item>
<item>Monet2</item> <item>Monet2</item>
</string-array> </string-array>

View file

@ -213,6 +213,7 @@
<item>Vàng</item> <item>Vàng</item>
<item>Hồng</item> <item>Hồng</item>
<item>Hồng đậm</item> <item>Hồng đậm</item>
<item>Hoa oải hương</item>
<item>Material You</item> <item>Material You</item>
<item>Material You (Secondary)</item> <item>Material You (Secondary)</item>
</string-array> </string-array>
@ -236,6 +237,7 @@
<item>Banana</item> <item>Banana</item>
<item>Party</item> <item>Party</item>
<item>Pink</item> <item>Pink</item>
<item>Lavender</item>
<item>Monet</item> <item>Monet</item>
<item>Monet2</item> <item>Monet2</item>
</string-array> </string-array>

View file

@ -284,6 +284,7 @@
<item>Banana</item> <item>Banana</item>
<item>Party</item> <item>Party</item>
<item>Pink Pain</item> <item>Pink Pain</item>
<item>Lavender</item>
<item>Material You</item> <item>Material You</item>
<item>Material You (Secondary)</item> <item>Material You (Secondary)</item>
</string-array> </string-array>
@ -307,6 +308,7 @@
<item>Banana</item> <item>Banana</item>
<item>Party</item> <item>Party</item>
<item>Pink</item> <item>Pink</item>
<item>Lavender</item>
<item>Monet</item> <item>Monet</item>
<item>Monet2</item> <item>Monet2</item>
</string-array> </string-array>

View file

@ -84,6 +84,7 @@
<color name="colorPrimaryOrange">#CE8500</color> <color name="colorPrimaryOrange">#CE8500</color>
<color name="colorPrimaryDandelionYellow">#F5BB00</color> <color name="colorPrimaryDandelionYellow">#F5BB00</color>
<color name="colorPrimaryCoolBlue">#408cac</color> <color name="colorPrimaryCoolBlue">#408cac</color>
<color name="colorPrimaryLavender">#6F55AF</color>
<color name="colorTestPass">#48E484</color> <color name="colorTestPass">#48E484</color>
<color name="colorTestFail">#ea596e</color> <color name="colorTestFail">#ea596e</color>

View file

@ -69,6 +69,7 @@
<string name="enable_skip_op_from_database" translatable="false">enable_skip_op_from_database</string> <string name="enable_skip_op_from_database" translatable="false">enable_skip_op_from_database</string>
<string name="rotate_video_key" translatable="false">rotate_video_key</string> <string name="rotate_video_key" translatable="false">rotate_video_key</string>
<string name="auto_rotate_video_key" translatable="false">auto_rotate_video_key</string> <string name="auto_rotate_video_key" translatable="false">auto_rotate_video_key</string>
<string name="biometric_key" translatable="false">biometric_key</string>
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG --> <!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
<string name="extra_info_format" formatted="true" translatable="false">%d %s | %s</string> <string name="extra_info_format" formatted="true" translatable="false">%d %s | %s</string>
<string name="storage_size_format" formatted="true" translatable="false">%s • %s</string> <string name="storage_size_format" formatted="true" translatable="false">%s • %s</string>
@ -247,7 +248,7 @@
<string name="backup_failed_error_format">Error backing up %s</string> <string name="backup_failed_error_format">Error backing up %s</string>
<string name="search">Search</string> <string name="search">Search</string>
<string name="library">Library</string> <string name="library">Library</string>
<string name="category_account">Accounts</string> <string name="category_account">Accounts and Security</string>
<string name="category_updates">Updates and backup</string> <string name="category_updates">Updates and backup</string>
<string name="settings_info">Info</string> <string name="settings_info">Info</string>
<string name="advanced_search">Advanced Search</string> <string name="advanced_search">Advanced Search</string>
@ -306,6 +307,7 @@
<string name="go_forward_30">+30</string> <string name="go_forward_30">+30</string>
<string name="delete_message" formatted="true">This will permanently delete %s\nAre you sure?</string> <string name="delete_message" formatted="true">This will permanently delete %s\nAre you sure?</string>
<string name="resume_time_left" formatted="true">%dm\nremaining</string> <string name="resume_time_left" formatted="true">%dm\nremaining</string>
<string name="resume_remaining" formatted="true">%s\nremaining</string>
<string name="status_ongoing">Ongoing</string> <string name="status_ongoing">Ongoing</string>
<string name="status_completed">Completed</string> <string name="status_completed">Completed</string>
<string name="status">Status</string> <string name="status">Status</string>
@ -745,4 +747,17 @@
<string name="rotate_video_desc">Display a toggle button for screen orientation</string> <string name="rotate_video_desc">Display a toggle button for screen orientation</string>
<string name="auto_rotate_video_desc">Enable automatic switching of screen orientation based on video orientation</string> <string name="auto_rotate_video_desc">Enable automatic switching of screen orientation based on video orientation</string>
<string name="auto_rotate_video">Auto rotate</string> <string name="auto_rotate_video">Auto rotate</string>
<string name="favorite">Favorite</string>
<string name="unfavorite">Unfavorite</string>
<!-- For Biometrics -->
<string name="biometric_authentication_title">Unlock CloudStream</string>
<string name="biometric_setting">Lock with Biometrics</string>
<string name="password_pin_authentication_title">Password/PIN Authentication</string>
<string name="biometric_unsupported">Biometric authentication is not supported on this device</string>
<string name="biometric_setting_summary">Unlock the app with Fingerprint, Face ID, PIN, Pattern and Password.</string>
<string name="biometric_prompt_description">This window will close after few failed attempts. You\'ll have to restart the App.</string>
<string name="biometric_warning">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.</string>
</resources> </resources>

View file

@ -383,6 +383,16 @@
<item name="android:colorAccent">@color/colorPrimaryCoolBlue</item> <item name="android:colorAccent">@color/colorPrimaryCoolBlue</item>
</style> </style>
<style name="OverlayPrimaryColorLavender">
<item name="colorPrimary">@color/colorPrimaryLavender</item>
<item name="android:colorPrimary">@color/colorPrimaryLavender</item>
<item name="colorPrimaryDark">#6B51AB</item>
<item name="colorAccent">#7961B4</item>
<item name="colorOnPrimary">@color/whiteText</item>
<!-- Needed for leanback fuckery -->
<item name="android:colorAccent">@color/colorPrimaryLavender</item>
</style>
<style name="customRatingBar" parent="@style/Widget.AppCompat.RatingBar"> <style name="customRatingBar" parent="@style/Widget.AppCompat.RatingBar">
<item name="android:progressDrawable">@drawable/abc_ratingbar_indicator_material</item> <item name="android:progressDrawable">@drawable/abc_ratingbar_indicator_material</item>
@ -804,6 +814,35 @@
<item name="android:insetTop">0dp</item> <item name="android:insetTop">0dp</item>
</style> </style>
<style name="ResultSmallButtonTV">
<item name="android:tag">@string/tv_no_focus_tag</item>
<item name="android:stateListAnimator">@null</item>
<item name="strokeColor">@color/transparent</item>
<item name="backgroundTint">@null</item>
<item name="android:background">@drawable/player_button_tv_attr</item>
<item name="rippleColor">@color/white</item>
<item name="android:shadowColor">@color/transparent</item>
<item name="iconTint">@color/player_on_button_tv_attr</item>
<item name="iconGravity">textStart</item>
<item name="android:layout_width">60dp</item>
<item name="android:layout_height">40dp</item>
<item name="android:gravity">center</item>
<item name="android:layout_gravity">center</item>
<item name="android:layout_marginStart">4dp</item>
<item name="android:layout_marginEnd">4dp</item>
<item name="android:layout_marginBottom">4dp</item>
</style>
<style name="ResultMarqueeButtonText">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:gravity">top|center_horizontal</item>
<item name="android:singleLine">true</item>
<item name="android:scrollHorizontally">true</item>
<item name="android:marqueeRepeatLimit">marquee_forever</item>
<item name="android:ellipsize">marquee</item>
</style>
<style name="VideoButtonTV"> <style name="VideoButtonTV">
<item name="android:tag">@string/tv_no_focus_tag</item> <item name="android:tag">@string/tv_no_focus_tag</item>
<item name="android:stateListAnimator">@null</item> <item name="android:stateListAnimator">@null</item>

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/ic_outline_account_circle_24"
android:key="@string/skip_startup_account_select_key"
android:title="@string/skip_startup_account_select_pref" />
<Preference <Preference
android:icon="@drawable/mal_logo" android:icon="@drawable/mal_logo"
@ -22,17 +16,18 @@
<Preference <Preference
android:icon="@drawable/open_subtitles_icon" android:icon="@drawable/open_subtitles_icon"
android:key="@string/opensubtitles_key" /> android:key="@string/opensubtitles_key" />
<!-- <Preference-->
<!-- android:key="@string/nginx_key"-->
<!-- android:icon="@drawable/nginx" />-->
<!-- <Preference--> <SwitchPreference
<!-- android:title="@string/nginx_info_title"--> android:defaultValue="false"
<!-- android:icon="@drawable/nginx_question"--> android:icon="@drawable/ic_outline_account_circle_24"
<!-- android:summary="@string/nginx_info_summary">--> android:key="@string/skip_startup_account_select_key"
<!-- <intent--> android:title="@string/skip_startup_account_select_pref" />
<!-- android:action="android.intent.action.VIEW"-->
<!-- android:data="https://www.sarlays.com/use-nginx-with-cloudstream/" />--> <SwitchPreferenceCompat
<!-- </Preference>--> android:key="@string/biometric_key"
android:defaultValue="false"
android:summary="@string/biometric_setting_summary"
android:icon="@drawable/ic_fingerprint"
android:title="@string/biometric_setting" />
</PreferenceScreen> </PreferenceScreen>